planning-with-files 静默故障自检指南:/plan-doctor 命令原理与实战
【免费下载链接】planning-with-filesPersistent file-based planning for AI coding agents and long-running tasks. Crash-proof markdown plans, session recovery after /clear and compaction, per-turn re-injection against context rot, deterministic completion gate. Manus-style. Install from npm, the Claude Code plugin marketplace, or npx skills. Codex, Cursor, OpenCode, 60+ agents.项目地址: https://gitcode.com/GitHub_Trending/pl/planning-with-files
本指南围绕 planning-with-files 自 v3.6.0 起提供的/plan-doctor自检命令展开,讲解它如何一次性暴露 hook 注入、计划解析、路径规范化、防篡改认证、安装面与单次 hook 延迟六类"静默失败"机制的真实状态。读完你将掌握在 hooks 突然"哑火"或新机器安装后如何运行自检、如何解读 PASS/WARN/FAIL 输出、以及每类 FAIL 对应的修复路径。
为什么需要 plan-doctor:静默失败是默认设计
planning-with-files 的核心机制——hook 注入与计划解析——在设计上有一个反直觉的特性:出错时它们照样以退出码 0 静默返回。这是刻意的("exit 0 and stay silent by design"),目的是任何情况下都不让 hook 打断 Agent 的主循环。但副作用是:当安装损坏时,坏掉的迹象与"还没有计划"看起来完全一致,用户无法区分"hooks 正常工作只是没有计划"和"hooks 已经彻底失效"。
正如 plan-doctor 命令文档 所述,这类故障分布在多个层面:
- 计划解析(plan resolution)失败;
- hook 注入(hook injection)没有发出任何计划上下文;
- 路径规范化器(canonicalizer)输出的路径形态不可比较(Windows 原生 coreutils 输出
C:\风格路径); - 认证状态(attestation)缺失或不匹配;
- 安装面(install surface)不存在;
- 单次 hook 触发延迟异常。
/plan-doctor就是为这个"看不见的故障类别"而生的:一次运行,把这些机制的真实状态全部摊开。该命令自 v3.6.0 起可用,其定位在 CHANGELOG.md 中有明确记载:它来自 2026 年 7 月基准测试的改进积压,属于"静默失败机制的一次性自检"。
运行方式:一条命令,全平台
在项目根目录执行(Linux / macOS / Git Bash):
sh ${CLAUDE_PLUGIN_ROOT}/scripts/plan-doctor.sh在未通过插件安装、直接使用仓库源码的场景下,等价写法是:
sh scripts/plan-doctor.shWindows 上如果 Git Bash 不在 PATH 中,需要通过git.exe定位其usr\bin\sh.exe兄弟程序,再用它运行同一个脚本。底层实现见 scripts/plan-doctor.sh:脚本以#!/bin/sh编写,只依赖 POSIX sh 与常见 coreutils,无 Python 硬依赖;它"只读诊断",唯一会写入的内容是inject-plan.sh自身的 SHA 缓存,且无论检查结果如何总是以退出码 0 结束(exit 0),延续了"绝不打断 Agent 循环"的项目契约。
命令前端约定
/plan-doctor是一个"禁用模型自发调用"(disable-model-invocation: true)的命令,且只允许使用 Bash 工具。这意味着它由用户在需要时显式触发,而不是让模型在任意时刻自行调用。触发场景有两个:hooks 看起来变安静时,以及在新机器上安装之后。
输出协议:PASS / WARN / FAIL / info
脚本定义了四级输出(scripts/plan-doctor.sh 第 24-27 行):
ok() { printf 'PASS %s\n' "$1"; } warn() { printf 'WARN %s\n' "$1"; } fail() { printf 'FAIL %s\n' "$1"; } info() { printf 'info %s\n' "$1"; }命令文档要求:把 PASS/WARN/FAIL 行原样报告给用户,因为这些文本是为"按原样阅读"设计的,不要转述或改写。另有一个约定:若出现 FAIL 行,需要给出对应的修复说明(见下文"故障与修复对照")。
六大检查项逐项拆解
/plan-doctor一次运行完成六项检查,对应脚本的六个编号段。
1. 规范化器探测(canonicalizer probe)
CANON="$(realpath . 2>/dev/null)" || CANON="" [ -z "${CANON}" ] && { CANON="$(readlink -f . 2>/dev/null)" || CANON=""; }脚本先尝试realpath,再回退readlink -f。探测结果有三种:
- 两者都不存在 →
WARN:遏制(containment)检查将回退到每次 spawn 一个 Python 进程,开销更高; - 输出含反斜杠(
*\\*)→info:规范化器输出 Windows 风格路径。脚本会提示"v3.6.0 之后已处理;更老的 pwf 版本在这台机器上什么也解析不出来"; - 其他情况 →
info:正常输出规范路径。
这一项直接对应 v3.6.0 修复的经典故障:Windows 原生 coreutils(如C:\Program Files\coreutils出现在 PATH 中且优先于 Git 的usr/bin)会把 MSYS 风格的/c/...输入规范化成C:\反斜杠输出,而遏制前缀匹配是用正斜杠书写的,于是每次规范化的结果对都不匹配,解析在静默中失效。脚本对反斜杠路径的处理见 scripts/resolve-plan-dir.sh 中的norm_slashes(纯 sh 参数展开循环,把反斜杠逐个归一为正斜杠,不 fork 子进程),以及is_windowsapps_path对 Microsoft Store WindowsApps 别名目录的识别——Store 应用别名不是稳定的解释器二进制,可能显示为可执行却拒绝运行脚本,因此必须排除。
2. 计划解析(plan resolution)
脚本调用同目录的 scripts/resolve-plan-dir.sh 并捕获其 stdout:
- 解析成功 →
PASS,并输出活动计划目录(active plan dir = ...); - 解析为空但项目根存在
task_plan.md→PASS,报告 legacy 根计划(./task_plan.md); .planning目录存在但什么都解析不出来 →FAIL,提示检查.planning/.active_plan内容和计划目录名;- 完全没有计划 →
info:本目录没有计划(提示先运行init-session.sh创建); - 若
resolve-plan-dir.sh不在脚本旁边 →WARN,提示安装布局异常。
要理解这条检查在验证什么,需要知道解析器的完整决议顺序(scripts/resolve-plan-dir.sh 头部注释):
$PLAN_ID环境变量 →./.planning/$PLAN_ID/(若存在);./.planning/.active_plan文件内容 → 对应目录(若存在);- 按 mtime 取最新的
./.planning/<dir>/; - 否则输出空,调用方回退 legacy 根
./task_plan.md。
三个关键语义值得注意:PLAN_ID是绑定而非提示——一旦设置,无论被拒绝(slug 形状非法、目录不存在、遏制失败)都会在此终止决议,绝不回退到另一个计划,防止一个字符的拼写错误静默切换到别的计划(issue #237);PWF_PLAN_ROOT是最高优先级绑定,用于 cwd 位于真实项目共享父目录的线程,一个坏的 pin 会"失败关闭"(fail closed),stdout 保持干净(stdout 是数据通道,通知文本由注入路由负责);slug 校验用纯 sh case 模式匹配^[A-Za-z0-9_][A-Za-z0-9._-]*$,以过滤.active_plan中的垃圾内容(纯空白或乱码)而不强制日期前缀。此外还有遏制守卫:解析出的计划目录必须规范化到项目根之下,符号链接逃逸(如 slug 目录内指向/etc的软链)会被拒绝,两侧路径都先做反斜杠归一化再比较。
3. hook 注入(hook injection)
这是 doctor 最核心、也最讲究的一项。它运行:
sh "${SCRIPT_DIR}/inject-plan.sh" --context=userprompt然后对输出做基于数据帧(data framing)的结构化分类,而不是对整段输出做子串匹配(issue #236)。为什么必须这样:inject-plan.sh的输出把计划正文原样放在===BEGIN-PWF-DATA===栅栏内,如果对整段 blob 做子串测试,计划正文里的普通文字就可能误触发分类——历史上确实发生过:某计划的一个阶段写着"fix the false PLAN TAMPERED warning",导致 doctor 对一个正确认证的计划误报哈希不匹配。
分类逻辑(scripts/plan-doctor.sh 第 94-121 行):
| 输出特征 | 判定 | 含义 |
|---|---|---|
含===BEGIN-PWF-DATA帧 | PASS+ 字节数 | 注入真实发生,计划上下文到达模型。因为inject-plan.sh的所有拒绝路径都会在frame_file运行前打印横幅并退出,所以输出中出现帧就能证明注入成功,并排除所有拒绝路径 |
含[PLAN TAMPERED | WARN | 计划已认证但哈希不匹配,需运行/plan-attest或attest-plan.sh重新批准 |
含requires attested plan | WARN | v3 模式但未认证,运行一次 attest-plan 以武装注入 |
含Session isolation is armed | WARN | 会话隔离拒绝该会话,用PWF_SESSION_ID=<id>加.planning/sessions/<id>.attached附着,或删除过期的.planning/sessions/目录关闭隔离 |
含Ambiguous plan | WARN | 嵌套计划歧义:cwd 的正下方项目自带计划,hooks 拒绝猜测,用PWF_PLAN_ROOT=<绝对项目根>或PLAN_ID=<slug>钉住线程 |
含PWF_PLAN_ROOT is not a supported absolute local directory | WARN | pin 指向的不是绝对本地目录,修复或取消 pin(坏 pin 失败关闭,什么都不注入) |
含PLAN_ID does not name a plan directory | WARN | PLAN_ID未命名.planning下任何计划目录,修复或取消 pin(设置即绑定,失败关闭而非另选计划) |
| 其他情况 | WARN+ 字节数 | 输出了内容但没有帧,说明计划上下文没有到达模型,这是 doctor 无法识别的拒绝通知,需直接运行sh scripts/inject-plan.sh --context=userprompt阅读原文 |
这个"默认 arm 警告而非通过"的设计是刻意的:横幅措辞漂移时应退化为响亮的警告,而不是静默的 PASS。这正是旧版 bug 的教训——旧代码里PWF_PLAN_ROOT is not a directory字面量从未是inject-plan.sh实际输出内容的子串,该 arm 是死代码,执行落入成功臂,于是一个完全"暗 hooks"的状态被报告成 PASS,还带着拒绝通知自身的字节数。参见 tests/test_plan_doctor_classification.py 中的test_dark_hooks_never_report_pass与test_an_unrecognized_refusal_banner_warns_instead_of_passing。
另外两个注入相关的边界情况:有可解析计划但注入输出为空 →FAIL,并列出已知静默原因(v3.6.0 之前 + PATH 上的 Windows 原生 realpath、PLANNING_DISABLED=1、计划目录位于项目根之外、过期.planning/sessions/目录没有附着会话——后者会让 pretool/precompact 触发整体静默);没有计划时注入静默 →PASS,这是正确行为。若inject-plan.sh缺失则WARN,并提示查看 docs/installation.md 的安装矩阵。
4. 认证状态(attestation)
doctor 查找认证文件:优先${RES}/.attestation(平行计划模式,与计划目录同目录),其次.plan-attestation(legacy 根计划模式)。找到则info报告路径,否则info说明"legacy 模式可选、v3 模式默认开启,批准计划后运行/plan-attest"。
认证机制的完整语义见 commands/plan-attest.md 与 scripts/attest-plan.sh:attest-plan.sh对解析出的task_plan.md计算 SHA-256,把十六进制摘要写入.planning/<active-plan>/.attestation(平行模式)或./.plan-attestation(legacy 模式),随后每个 UserPromptSubmit / PreToolUse hook 触发时都会把当前文件与存储哈希比对,一旦发散就输出[PLAN TAMPERED — injection blocked]而不是向模型喂计划内容。支持--show(打印已存哈希与存储位置)和--clear(移除认证、重新开放编辑)。
5. 安装面(install surface)
doctor 逐一探测四个技能目录安装面是否存在:
.claude/skills/planning-with-files(项目级)${HOME}/.claude/skills/planning-with-files(用户级).agents/skills/planning-with-files(项目级,OpenCode 等读取)${HOME}/.agents/skills/planning-with-files(用户级)
一个都没找到时输出info(插件路线的安装位于插件缓存下,而不是技能目录)。随后还有一条"路线提醒":插件路线随包提供 commands/ 与 hooks;npx-skills 路线只提供技能本身。项目级技能安装后 hooks 静默?需要检查项目信任(hasTrustDialogAccepted)以及 docs/installation.md 的安装矩阵。
6. 单次 hook 延迟(hook latency)
doctor 用纳秒时钟测量一次inject-plan.sh --context=userprompt的墙钟耗时:
T0="$(date +%s%N 2>/dev/null)" || T0="" sh "${INJ}" --context=userprompt >/dev/null 2>&1 T1="$(date +%s%N 2>/dev/null)" || T1="" MS=$(( (T1 - T0) / 1000000 ))若date二进制不支持纳秒时钟(T0/T1非数字),则info说明跳过。这条检查对排查"每次 hook 触发是否明显拖慢对话"非常直接:每轮 UserPromptSubmit、每个匹配的 PreToolUse 都各触发一次注入,单次成本乘以触发次数就是可见的感知延迟。
故障与修复对照
命令文档明确给出了 FAIL 行的修复指引:
| 故障行 | 修复路径 |
|---|---|
| resolver FAIL | 检查.planning/.active_plan内容与计划目录名 |
| 有可解析计划但 injection FAIL | hooks 处于静默暗态。若属 v3.6.0 之前 + Windows 原生 coreutils realpath 的根因,升级到 v3.6.0 以上修复;同时检查PLANNING_DISABLED |
| tamper WARN | 若计划编辑是有意为之,用/plan-attest重新批准 |
doctor 的另一个实用信号在脚本开头:PLANNING_DISABLED=1已设置时,会提前输出WARN——该环境下每个 hook 都会立即退出(这是 issue #195 引入的按次调用退出开关,供与计划共享 cwd 但从未选择加入的一次性/CI 会话使用,见 scripts/inject-plan.sh 第 77-79 行)。
诊断原则:只报告,不修复
命令文档与脚本都强调:doctor 是纯诊断工具,不做任何自动修复。遇到 FAIL 时,正确的动作是解释对应的修复步骤(上述对照表),然后让用户或配套命令(/plan-attest、init-session.sh、环境变量修正)来执行。这与项目整体的"绝不破坏 Agent 循环、失败关闭"哲学一致:诊断信号走人可读的输出通道,修复动作走显式的用户批准流程。
源码与测试佐证
- 命令定义:commands/plan-doctor.md——frontmatter 声明
disable-model-invocation: true、allowed-tools: "Bash",正文给出执行步骤、输出报告约定与修复指引。 - 实现:scripts/plan-doctor.sh——六段检查的完整 sh 实现,约 170 行,无外部依赖,
set -u防未定义变量。 - 解析依赖:scripts/resolve-plan-dir.sh——PLAN_ID / .active_plan / newest-by-mtime 三级决议 + slug 校验 + 遏制守卫 + 反斜杠归一化。
- 注入依赖:scripts/inject-plan.sh——按
--context=userprompt|pretool|precompact|preflight|validate区分注入形态,含会话附着守卫与多根消歧。 - 认证依赖:scripts/attest-plan.sh 与 commands/plan-attest.md——SHA-256 锁定计划内容,
--show/--clear两个旗标。 - 回归测试:tests/test_plan_doctor_classification.py——覆盖"真实篡改必报 WARN"、"计划正文引用全部控制字面量仍必须 PASS"、"暗 hooks 绝不报 PASS"、"被拒 PLAN_ID 绑定绝不报 PASS"、"无计划时的静默即正确"以及"无法识别的拒绝横幅降级为 WARN"六类场景,其中第三条与第六条正是历史缺陷 2(假 PASS)的回归锁。
- 发布记录:CHANGELOG.md 3.6.0 条目——记载 doctor 的引入动机(该故障类别全部静默、损坏安装与"尚无计划"外观一致)与六项检查内容。
- 安装矩阵:docs/installation.md——不同路线(插件 / npx-skills / 手工技能拷贝 / OpenCode / Hermes)分别提供哪些面(SKILL.md、命令、hooks),以及两条会导致独立技能路线无活跃 hook 的条件(项目信任未接受、技能未被调用)。
何时该跑一次 doctor
把/plan-doctor当作安装后与故障期的固定动作:
- 新机器 / 新项目装完插件或技能后跑一次,确认六项全绿再开始长任务;
- hooks 突然变安静——计划明明存在,模型却不再看到计划上下文——立刻跑一次,用 injection 行的分类结果定位静默原因;
- Windows 环境升级后跑一次,确认规范化器输出的路径形态被正确识别(v3.6.0 起已处理,旧版本在这类机器上会整体失效);
- 怀疑会话隔离 / 嵌套项目 / pin 失效时,doctor 会用对应的 WARN 文案直接把根因指出来。
一句话总结:在 planning-with-files 里,"没有报错"不等于"一切正常";/plan-doctor用一次只读的、永不失败的运行,把那些设计上就静默的机制照出原形——PASS 让你安心,WARN 告诉你该修什么,FAIL 告诉你修复从哪查起。
【免费下载链接】planning-with-filesPersistent file-based planning for AI coding agents and long-running tasks. Crash-proof markdown plans, session recovery after /clear and compaction, per-turn re-injection against context rot, deterministic completion gate. Manus-style. Install from npm, the Claude Code plugin marketplace, or npx skills. Codex, Cursor, OpenCode, 60+ agents.项目地址: https://gitcode.com/GitHub_Trending/pl/planning-with-files
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考