From 9866d0382e2346de12d55096f7d40a1c03e528c0 Mon Sep 17 00:00:00 2001 From: auYeCoding Date: Thu, 12 Nov 2026 02:27:01 -0800 Subject: [PATCH] =?UTF-8?q?fix(waypoint):=20project-navigator=20=E6=92=A4?= =?UTF-8?q?=E5=9B=9E=E6=B6=88=E6=81=AF=E5=90=8E=E9=87=8D=E6=96=B0=E5=9B=9E?= =?UTF-8?q?=E7=AD=94=E8=83=BD=E5=8F=91=E5=B8=83=E5=B7=A5=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 用户没有选发布时保留审阅记录, 状态记为 declined - 之后以 "发布工单" 开头的消息仍认可审阅时的内容, 单独的字母不算 - 发布被拒的提示写明只运行 reply 命令不算审阅 - 新增测试, 更新维护文档排查表与 CHANGELOG 撤回消息不会恢复被删除的审阅记录, 重新回答后工单无法发布. --- plugins/waypoint/CHANGELOG.md | 4 ++ .../docs/project-navigator-maintaining.en.md | 54 +++++++++--------- .../docs/project-navigator-maintaining.md | 54 +++++++++--------- .../runtime/commands/order.mjs | 2 +- .../runtime/lib/option-answer.mjs | 23 +++++++- .../runtime/lib/order-approval.mjs | 55 +++++++++++++------ .../runtime/lib/registry.mjs | 4 +- tests/project-navigator/flow.test.mjs | 26 +++++++++ 8 files changed, 145 insertions(+), 77 deletions(-) diff --git a/plugins/waypoint/CHANGELOG.md b/plugins/waypoint/CHANGELOG.md index 39cf68d..d55a71f 100644 --- a/plugins/waypoint/CHANGELOG.md +++ b/plugins/waypoint/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## [Unreleased] +### Fixed + +- `project-navigator`: answering "工单审阅" (order review) with anything other than A discarded the review, so withdrawing that message and answering "发布工单" (issue the order) left the order unissuable until it was reviewed again. The review is now kept: a later message that starts with "发布工单" approves the reviewed content as long as the order file is unchanged; a bare letter counts only right after the review. The refusal message also says that running the `reply` command alone is not a review. + ## [1.3.1] - 2026-11-11 ### Fixed diff --git a/plugins/waypoint/docs/project-navigator-maintaining.en.md b/plugins/waypoint/docs/project-navigator-maintaining.en.md index 8a1b448..ca57126 100644 --- a/plugins/waypoint/docs/project-navigator-maintaining.en.md +++ b/plugins/waypoint/docs/project-navigator-maintaining.en.md @@ -144,33 +144,33 @@ Other parts of the repository: ## Troubleshooting -| Symptom | Cause | Files | How to verify | -| ---------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | -| Invoking the skill aborts with a permission check failure | `allowed-tools` was removed or changed | `SKILL.md` | Invoke once in the default permission mode | -| `project-navigator` is not found | The plugin is not enabled, or the session started before the plugin update; PowerShell cannot find the `.cmd` | `bin/` | Check with `claude plugin list`; restart the session | -| Plugin commands or record writes ask for permission | The local settings lack the rules, or the command does not start with `project-navigator` | `lib/settings.mjs` | Check the rules in `.claude/settings.local.json` | -| The orchestrator's write outside `.navigator/` is denied | This is rule 4; issue a work order instead | `lib/guard.mjs` | Drive `hook.mjs` through stdin with an orchestrator transcript | -| A session that is not the orchestrator cannot write business files | The project's orchestrator marker appears in its transcript, for example it once invoked the skill | `lib/orchestrator-marker.mjs` | Search the transcript for "project-navigator 编排标记" | -| The executor's business file write is denied | The order is not issued, or it is a run-check or analysis order | `lib/guard.mjs`, `lib/workflow-orders.mjs` | The "工单状态" (order status) line of `status` | -| The executor's record write is denied | Under `.navigator/` only the current order's receipt and `artifacts/` are writable | `lib/guard.mjs` | The denial reason states the writable scope | -| The executor's document write under `artifacts/` is denied, and the message is not the guard's | Claude Code itself refuses report-like file writes from subagents | `scripts/navigator-docs/executor-guide.md` | The subagent returns the full text in its final reply and the orchestrator writes it to that path | -| A reply is sent back | It lacks the heading or the conclusion line, or the option block differs from the spec | `lib/reply-checks.mjs`, `spec/templates.json` | Take the skeleton with `reply`, fill it in, and check again | -| The executor finished, but `status` says no receipt | The subagent did not write the receipt from the skeleton, or wrote it elsewhere | `commands/brief.mjs`, `lib/receipt-record.mjs` | Ask the subagent to write it with SendMessage; look in the order folder | -| The receipt exists, but the next action cannot tell blocked from done | "执行状态" (execution status) in "回执概况" (receipt overview) is missing, or the section was renamed | `lib/receipt-record.mjs` | Self-check with `check ` | -| `order set issued` is refused, mentioning the review | The user did not choose A in "工单审阅" (order review), or the order file changed after approval | `lib/order-approval.mjs`, `commands/order.mjs` | Check the order id, digest, and status in `.git/navigator/order-approval.json` | -| Continuing a blocked order is refused | The order file changed after issue and `issuedDigest` no longer matches; a new review is needed | `commands/order.mjs` | The next action in `status` says a new review is required | -| `order set accepted` is refused | A criterion failed; unverified criteria without `--accept-unverified`; or "用户结论" (user verdict) is still "待用户确认" (awaiting the user) | `lib/review-record.mjs`, `commands/order.mjs` | Check the verdict table and manual verification section of `review.md` | -| `reply approve` or `order set issued` is refused, mentioning the base commit | "基准提交" (base commit) in the order file is not the current commit: the repository got a new commit after the order was written (for example the commit after a plugin upgrade) | `lib/order-base.mjs` | Change "基准提交" in the order file to the current commit shown in the message and review again | -| `reply review --option 1` or `--option 3` is refused | The option group does not match the verdicts | `commands/reply.mjs` | Check the verdict table of `review.md` | -| Creating or issuing an implementation or fix order is refused, mentioning code checks | No check commands are registered, or the criteria lack the check criterion verbatim | `lib/code-checks.mjs`, `commands/order.mjs` | Check `codeChecks` in `state.json`; take `template order` again | -| Entering says the project was initialized by an older version | The project settings still have 0.x project-level hooks, or `.navigator/bin/` still exists | `commands/enter.mjs`, `commands/init.mjs` | Run `project-navigator init` to upgrade | -| Before the upgrade, commands in the old orchestrator session are denied | The old project-level hook still runs; the 0.x `orchestrator.json` was not deleted | `commands/enter.mjs` | Invoke the skill again (entering deletes the old registration), or invoke it in a new session | -| `init` prints "设置结果: 未执行" (not done), or entering asks for "运行受阻" (blocked run) | The project's ignore rules ignore `.navigator/`, the configuration under `.claude/`, or files that must be committed | `commands/init.mjs`, `lib/tracked-paths.mjs` | `git check-ignore -v --no-index ` | -| `evidence` cannot fetch a version or file | The version differs from the repository's tag naming, the path is the installed package path, the repository needs an account, or the network is down | `lib/source-fetch.mjs` | Retry one entry with `evidence check` and read git's original error in the output | -| The hook still outputs after uninstalling | `init.status` in the state file is not `uninstalled` | `commands/uninstall.mjs`, `runtime/hook.mjs` | Check `init` in `state.json` | -| A user forwards a problem report | "脚本记录的异常" (recorded anomalies) gives the kind, source command, and stack; "状态" (state) gives the position and order status at the time | `lib/diagnostics.mjs`, `commands/report.mjs` | Reproduce in a temporary repository from "复现步骤" (steps to reproduce); `<技能目录>` in the stack stands for `skills/project-navigator` | -| `status` keeps showing "技能异常" (skill anomaly) | There are unreported anomalies; they are marked as reported only after `report` runs | `lib/diagnostics.mjs` | Look for entries without `reportId` in `.git/navigator/diagnostics.json` | -| A slip was recorded as a skill anomaly | The script threw an error that is not a `WorkflowError`, so it counts as internal; or the next action text names a command that gets refused | `lib/diagnostics.mjs`, `lib/guidance.mjs` | Check the anomaly's kind and message; workflow refusals always throw `WorkflowError` | +| Symptom | Cause | Files | How to verify | +| ---------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| Invoking the skill aborts with a permission check failure | `allowed-tools` was removed or changed | `SKILL.md` | Invoke once in the default permission mode | +| `project-navigator` is not found | The plugin is not enabled, or the session started before the plugin update; PowerShell cannot find the `.cmd` | `bin/` | Check with `claude plugin list`; restart the session | +| Plugin commands or record writes ask for permission | The local settings lack the rules, or the command does not start with `project-navigator` | `lib/settings.mjs` | Check the rules in `.claude/settings.local.json` | +| The orchestrator's write outside `.navigator/` is denied | This is rule 4; issue a work order instead | `lib/guard.mjs` | Drive `hook.mjs` through stdin with an orchestrator transcript | +| A session that is not the orchestrator cannot write business files | The project's orchestrator marker appears in its transcript, for example it once invoked the skill | `lib/orchestrator-marker.mjs` | Search the transcript for "project-navigator 编排标记" | +| The executor's business file write is denied | The order is not issued, or it is a run-check or analysis order | `lib/guard.mjs`, `lib/workflow-orders.mjs` | The "工单状态" (order status) line of `status` | +| The executor's record write is denied | Under `.navigator/` only the current order's receipt and `artifacts/` are writable | `lib/guard.mjs` | The denial reason states the writable scope | +| The executor's document write under `artifacts/` is denied, and the message is not the guard's | Claude Code itself refuses report-like file writes from subagents | `scripts/navigator-docs/executor-guide.md` | The subagent returns the full text in its final reply and the orchestrator writes it to that path | +| A reply is sent back | It lacks the heading or the conclusion line, or the option block differs from the spec | `lib/reply-checks.mjs`, `spec/templates.json` | Take the skeleton with `reply`, fill it in, and check again | +| The executor finished, but `status` says no receipt | The subagent did not write the receipt from the skeleton, or wrote it elsewhere | `commands/brief.mjs`, `lib/receipt-record.mjs` | Ask the subagent to write it with SendMessage; look in the order folder | +| The receipt exists, but the next action cannot tell blocked from done | "执行状态" (execution status) in "回执概况" (receipt overview) is missing, or the section was renamed | `lib/receipt-record.mjs` | Self-check with `check ` | +| `order set issued` is refused, mentioning the review | The user did not choose A in "工单审阅" (order review), or the order file changed after approval. Status `declined` means the user answered something else (including a message withdrawn later): a later message starting with "发布工单" (issue the order) still approves, a bare letter does not | `lib/order-approval.mjs`, `commands/order.mjs` | Check the order id, digest, and status in `.git/navigator/order-approval.json` | +| Continuing a blocked order is refused | The order file changed after issue and `issuedDigest` no longer matches; a new review is needed | `commands/order.mjs` | The next action in `status` says a new review is required | +| `order set accepted` is refused | A criterion failed; unverified criteria without `--accept-unverified`; or "用户结论" (user verdict) is still "待用户确认" (awaiting the user) | `lib/review-record.mjs`, `commands/order.mjs` | Check the verdict table and manual verification section of `review.md` | +| `reply approve` or `order set issued` is refused, mentioning the base commit | "基准提交" (base commit) in the order file is not the current commit: the repository got a new commit after the order was written (for example the commit after a plugin upgrade) | `lib/order-base.mjs` | Change "基准提交" in the order file to the current commit shown in the message and review again | +| `reply review --option 1` or `--option 3` is refused | The option group does not match the verdicts | `commands/reply.mjs` | Check the verdict table of `review.md` | +| Creating or issuing an implementation or fix order is refused, mentioning code checks | No check commands are registered, or the criteria lack the check criterion verbatim | `lib/code-checks.mjs`, `commands/order.mjs` | Check `codeChecks` in `state.json`; take `template order` again | +| Entering says the project was initialized by an older version | The project settings still have 0.x project-level hooks, or `.navigator/bin/` still exists | `commands/enter.mjs`, `commands/init.mjs` | Run `project-navigator init` to upgrade | +| Before the upgrade, commands in the old orchestrator session are denied | The old project-level hook still runs; the 0.x `orchestrator.json` was not deleted | `commands/enter.mjs` | Invoke the skill again (entering deletes the old registration), or invoke it in a new session | +| `init` prints "设置结果: 未执行" (not done), or entering asks for "运行受阻" (blocked run) | The project's ignore rules ignore `.navigator/`, the configuration under `.claude/`, or files that must be committed | `commands/init.mjs`, `lib/tracked-paths.mjs` | `git check-ignore -v --no-index ` | +| `evidence` cannot fetch a version or file | The version differs from the repository's tag naming, the path is the installed package path, the repository needs an account, or the network is down | `lib/source-fetch.mjs` | Retry one entry with `evidence check` and read git's original error in the output | +| The hook still outputs after uninstalling | `init.status` in the state file is not `uninstalled` | `commands/uninstall.mjs`, `runtime/hook.mjs` | Check `init` in `state.json` | +| A user forwards a problem report | "脚本记录的异常" (recorded anomalies) gives the kind, source command, and stack; "状态" (state) gives the position and order status at the time | `lib/diagnostics.mjs`, `commands/report.mjs` | Reproduce in a temporary repository from "复现步骤" (steps to reproduce); `<技能目录>` in the stack stands for `skills/project-navigator` | +| `status` keeps showing "技能异常" (skill anomaly) | There are unreported anomalies; they are marked as reported only after `report` runs | `lib/diagnostics.mjs` | Look for entries without `reportId` in `.git/navigator/diagnostics.json` | +| A slip was recorded as a skill anomaly | The script threw an error that is not a `WorkflowError`, so it counts as internal; or the next action text names a command that gets refused | `lib/diagnostics.mjs`, `lib/guidance.mjs` | Check the anomaly's kind and message; workflow refusals always throw `WorkflowError` | ## Change workflow diff --git a/plugins/waypoint/docs/project-navigator-maintaining.md b/plugins/waypoint/docs/project-navigator-maintaining.md index 04e1220..601c7ee 100644 --- a/plugins/waypoint/docs/project-navigator-maintaining.md +++ b/plugins/waypoint/docs/project-navigator-maintaining.md @@ -144,33 +144,33 @@ ## 排查手册 -| 症状 | 根因 | 相关文件 | 验证方法 | -| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------- | -| 调用技能时直接中止, 提示权限检查失败 | `allowed-tools` 被删除或改写 | `SKILL.md` | 默认权限模式下调用一次 | -| 运行 `project-navigator` 提示找不到命令 | 插件没有启用, 或会话在插件更新前启动; PowerShell 中找不到 `.cmd` | `bin/` | `claude plugin list` 看插件是否启用; 重启会话 | -| 插件命令或写记录弹出权限确认 | 本机配置缺少放行规则, 或命令没有以 `project-navigator` 开头 | `lib/settings.mjs` | 检查 `.claude/settings.local.json` 中的规则 | -| 编排会话写 `.navigator/` 之外的文件被拒 | 这是规则 4; 需要改动时发工单 | `lib/guard.mjs` | 用标准输入驱动 `hook.mjs`, 带编排会话的转录 | -| 不是编排会话的会话写业务文件被拒 | 会话转录中出现了本项目的编排标记, 例如该会话曾调用过技能 | `lib/orchestrator-marker.mjs` | 在转录中查找 "project-navigator 编排标记" | -| 执行子代理写业务文件被拒 | 工单不在已发布状态, 或是运行检查与分析工单 | `lib/guard.mjs`, `lib/workflow-orders.mjs` | `status` 的 "工单状态" 一行 | -| 执行子代理写编排记录被拒 | `.navigator/` 下只能写当前工单的回执与 `artifacts/` | `lib/guard.mjs` | 拒绝理由写明可写范围 | -| 执行子代理写 `artifacts/` 下的文档被拒, 提示不是守卫的 | Claude Code 自身拒绝子代理写报告类文件 | `scripts/navigator-docs/executor-guide.md` | 子代理在最终回复中交回全文, 编排会话按路径写入 | -| 回复被打回 | 缺少一级标题, 结论行, 或选项块与规格不一致 | `lib/reply-checks.mjs`, `spec/templates.json` | `reply` 取骨架后填写再校验 | -| 执行子代理结束了, `status` 仍说回执未写入 | 子代理没有按骨架写回执, 或写到了别的路径 | `commands/brief.mjs`, `lib/receipt-record.mjs` | 用 SendMessage 请子代理补写; 查看工单文件夹 | -| 回执已写入, 下一动作分不清受阻还是完成 | "回执概况" 的 "执行状态" 没有写, 或节名被改 | `lib/receipt-record.mjs` | `check <回执路径>` 自查结构 | -| `order set issued` 被拒, 提到审阅 | 用户没有在 "工单审阅" 中选 A, 或认可之后改过工单文件 | `lib/order-approval.mjs`, `commands/order.mjs` | 查看 `.git/navigator/order-approval.json` 的工单编号, 摘要与状态 | -| 受阻后继续发布被拒 | 工单文件在发布后改过, `issuedDigest` 对不上, 要重新审阅 | `commands/order.mjs` | `status` 的下一动作写明 "必须重新审阅" | -| `order set accepted` 被拒 | 有判据不通过; 有未验证但没带 `--accept-unverified`; 或 "用户结论" 还是 "待用户确认" | `lib/review-record.mjs`, `commands/order.mjs` | 查看 `review.md` 的判据核对表与人工验收一节 | -| `reply approve` 或 `order set issued` 被拒, 提到基准提交 | 工单文件 "工单概况" 的 "基准提交" 不是当前提交: 写好工单之后仓库有了新提交 (例如升级插件后的提交) | `lib/order-base.mjs` | 把工单文件的 "基准提交" 改为提示中的当前提交, 重新审阅 | -| `reply review --option 1` 或 `--option 3` 被拒 | 选项组与判据结论不符 | `commands/reply.mjs` | 查看 `review.md` 的判据核对表 | -| 实现或修复工单新建或发布被拒, 提到代码检查 | 项目未登记检查命令, 或工单的验收判据不含检查判据原文 | `lib/code-checks.mjs`, `commands/order.mjs` | 查看 `state.json` 的 `codeChecks`; 重新取 `template order` | -| 进入时说项目由旧版本初始化 | 项目配置中还有 0.x 的项目级 hook, 或 `.navigator/bin/` 还在 | `commands/enter.mjs`, `commands/init.mjs` | 运行 `project-navigator init` 升级 | -| 升级之前在旧编排会话中运行命令被拒 | 旧的项目级 hook 仍在运行; 0.x 的 `orchestrator.json` 没有被删掉 | `commands/enter.mjs` | 重新调用技能 (进入时删掉旧登记), 或新开会话调用 | -| init 输出 "设置结果: 未执行", 或进入时要求回复 "运行受阻" | 项目的忽略规则忽略了 `.navigator/`, `.claude/` 下的配置, 或其中必须入库的文件 | `commands/init.mjs`, `lib/tracked-paths.mjs` | `git check-ignore -v --no-index <路径>` | -| `evidence` 报取不到版本或文件 | 版本与仓库的 tag 写法不同, 路径写成了安装后的包内路径, 仓库需要账号, 或网络不通 | `lib/source-fetch.mjs` | 用 `evidence check` 单条重试, 看输出中 git 的原始错误 | -| 卸载后 hook 仍有输出 | 状态文件的 `init.status` 不是 `uninstalled` | `commands/uninstall.mjs`, `runtime/hook.mjs` | 查看 `state.json` 的 `init` | -| 收到用户转来的问题报告 | 报告的 "脚本记录的异常" 给出种类, 来源命令与调用栈; "状态" 给出当时的位置与工单状态 | `lib/diagnostics.mjs`, `commands/report.mjs` | 按 "复现步骤" 在临时仓库中重现; 调用栈中的 `<技能目录>` 对应 `skills/project-navigator` | -| `status` 一直显示 "技能异常" | 有未上报的异常; 运行 `report` 后才标为已上报 | `lib/diagnostics.mjs` | 查看 `.git/navigator/diagnostics.json` 中没有 `reportId` 的条目 | -| 操作失误被记成了技能异常 | 脚本抛出的错误不是 `WorkflowError`, 被当成内部错误; 或 "下一动作" 的文字里写了会被拒绝的命令 | `lib/diagnostics.mjs`, `lib/guidance.mjs` | 查看异常的种类与信息; 流程规则的拒绝一律抛 `WorkflowError` | +| 症状 | 根因 | 相关文件 | 验证方法 | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------- | +| 调用技能时直接中止, 提示权限检查失败 | `allowed-tools` 被删除或改写 | `SKILL.md` | 默认权限模式下调用一次 | +| 运行 `project-navigator` 提示找不到命令 | 插件没有启用, 或会话在插件更新前启动; PowerShell 中找不到 `.cmd` | `bin/` | `claude plugin list` 看插件是否启用; 重启会话 | +| 插件命令或写记录弹出权限确认 | 本机配置缺少放行规则, 或命令没有以 `project-navigator` 开头 | `lib/settings.mjs` | 检查 `.claude/settings.local.json` 中的规则 | +| 编排会话写 `.navigator/` 之外的文件被拒 | 这是规则 4; 需要改动时发工单 | `lib/guard.mjs` | 用标准输入驱动 `hook.mjs`, 带编排会话的转录 | +| 不是编排会话的会话写业务文件被拒 | 会话转录中出现了本项目的编排标记, 例如该会话曾调用过技能 | `lib/orchestrator-marker.mjs` | 在转录中查找 "project-navigator 编排标记" | +| 执行子代理写业务文件被拒 | 工单不在已发布状态, 或是运行检查与分析工单 | `lib/guard.mjs`, `lib/workflow-orders.mjs` | `status` 的 "工单状态" 一行 | +| 执行子代理写编排记录被拒 | `.navigator/` 下只能写当前工单的回执与 `artifacts/` | `lib/guard.mjs` | 拒绝理由写明可写范围 | +| 执行子代理写 `artifacts/` 下的文档被拒, 提示不是守卫的 | Claude Code 自身拒绝子代理写报告类文件 | `scripts/navigator-docs/executor-guide.md` | 子代理在最终回复中交回全文, 编排会话按路径写入 | +| 回复被打回 | 缺少一级标题, 结论行, 或选项块与规格不一致 | `lib/reply-checks.mjs`, `spec/templates.json` | `reply` 取骨架后填写再校验 | +| 执行子代理结束了, `status` 仍说回执未写入 | 子代理没有按骨架写回执, 或写到了别的路径 | `commands/brief.mjs`, `lib/receipt-record.mjs` | 用 SendMessage 请子代理补写; 查看工单文件夹 | +| 回执已写入, 下一动作分不清受阻还是完成 | "回执概况" 的 "执行状态" 没有写, 或节名被改 | `lib/receipt-record.mjs` | `check <回执路径>` 自查结构 | +| `order set issued` 被拒, 提到审阅 | 用户没有在 "工单审阅" 中选 A, 或认可之后改过工单文件. 状态为 `declined` 表示用户回答了别的内容 (包括之后撤回的消息): 此后以 "发布工单" 开头的消息仍会认可, 单独的字母不会 | `lib/order-approval.mjs`, `commands/order.mjs` | 查看 `.git/navigator/order-approval.json` 的工单编号, 摘要与状态 | +| 受阻后继续发布被拒 | 工单文件在发布后改过, `issuedDigest` 对不上, 要重新审阅 | `commands/order.mjs` | `status` 的下一动作写明 "必须重新审阅" | +| `order set accepted` 被拒 | 有判据不通过; 有未验证但没带 `--accept-unverified`; 或 "用户结论" 还是 "待用户确认" | `lib/review-record.mjs`, `commands/order.mjs` | 查看 `review.md` 的判据核对表与人工验收一节 | +| `reply approve` 或 `order set issued` 被拒, 提到基准提交 | 工单文件 "工单概况" 的 "基准提交" 不是当前提交: 写好工单之后仓库有了新提交 (例如升级插件后的提交) | `lib/order-base.mjs` | 把工单文件的 "基准提交" 改为提示中的当前提交, 重新审阅 | +| `reply review --option 1` 或 `--option 3` 被拒 | 选项组与判据结论不符 | `commands/reply.mjs` | 查看 `review.md` 的判据核对表 | +| 实现或修复工单新建或发布被拒, 提到代码检查 | 项目未登记检查命令, 或工单的验收判据不含检查判据原文 | `lib/code-checks.mjs`, `commands/order.mjs` | 查看 `state.json` 的 `codeChecks`; 重新取 `template order` | +| 进入时说项目由旧版本初始化 | 项目配置中还有 0.x 的项目级 hook, 或 `.navigator/bin/` 还在 | `commands/enter.mjs`, `commands/init.mjs` | 运行 `project-navigator init` 升级 | +| 升级之前在旧编排会话中运行命令被拒 | 旧的项目级 hook 仍在运行; 0.x 的 `orchestrator.json` 没有被删掉 | `commands/enter.mjs` | 重新调用技能 (进入时删掉旧登记), 或新开会话调用 | +| init 输出 "设置结果: 未执行", 或进入时要求回复 "运行受阻" | 项目的忽略规则忽略了 `.navigator/`, `.claude/` 下的配置, 或其中必须入库的文件 | `commands/init.mjs`, `lib/tracked-paths.mjs` | `git check-ignore -v --no-index <路径>` | +| `evidence` 报取不到版本或文件 | 版本与仓库的 tag 写法不同, 路径写成了安装后的包内路径, 仓库需要账号, 或网络不通 | `lib/source-fetch.mjs` | 用 `evidence check` 单条重试, 看输出中 git 的原始错误 | +| 卸载后 hook 仍有输出 | 状态文件的 `init.status` 不是 `uninstalled` | `commands/uninstall.mjs`, `runtime/hook.mjs` | 查看 `state.json` 的 `init` | +| 收到用户转来的问题报告 | 报告的 "脚本记录的异常" 给出种类, 来源命令与调用栈; "状态" 给出当时的位置与工单状态 | `lib/diagnostics.mjs`, `commands/report.mjs` | 按 "复现步骤" 在临时仓库中重现; 调用栈中的 `<技能目录>` 对应 `skills/project-navigator` | +| `status` 一直显示 "技能异常" | 有未上报的异常; 运行 `report` 后才标为已上报 | `lib/diagnostics.mjs` | 查看 `.git/navigator/diagnostics.json` 中没有 `reportId` 的条目 | +| 操作失误被记成了技能异常 | 脚本抛出的错误不是 `WorkflowError`, 被当成内部错误; 或 "下一动作" 的文字里写了会被拒绝的命令 | `lib/diagnostics.mjs`, `lib/guidance.mjs` | 查看异常的种类与信息; 流程规则的拒绝一律抛 `WorkflowError` | ## 修改流程 diff --git a/plugins/waypoint/skills/project-navigator/runtime/commands/order.mjs b/plugins/waypoint/skills/project-navigator/runtime/commands/order.mjs index 63d1e50..520c5b9 100644 --- a/plugins/waypoint/skills/project-navigator/runtime/commands/order.mjs +++ b/plugins/waypoint/skills/project-navigator/runtime/commands/order.mjs @@ -229,7 +229,7 @@ function assertIssuable(context) { throw new WorkflowError( approval === APPROVAL_STATUSES.awaiting ? `工单 ${order.id} 正在等用户审阅: 用户在 "${APPROVAL_REPLY_TYPE}" 中选 A 之后才能发布.` - : `工单 ${order.id} 还没有经用户审阅, 或审阅之后内容有改动: 先${replyStep(context.spec, APPROVAL_REPLY_TYPE)}, 用户选 A 后再运行 order set issued.`, + : `工单 ${order.id} 还没有经用户审阅, 或审阅之后内容有改动: 先${replyStep(context.spec, APPROVAL_REPLY_TYPE)}并结束本轮, 用户选 A 后再运行 order set issued. 只运行 reply 命令不算审阅.`, ); } diff --git a/plugins/waypoint/skills/project-navigator/runtime/lib/option-answer.mjs b/plugins/waypoint/skills/project-navigator/runtime/lib/option-answer.mjs index 143fd5b..ff00581 100644 --- a/plugins/waypoint/skills/project-navigator/runtime/lib/option-answer.mjs +++ b/plugins/waypoint/skills/project-navigator/runtime/lib/option-answer.mjs @@ -39,13 +39,30 @@ export function isChoiceSelected(prompt, optionSet, index) { if (letter === undefined || choice === undefined) { return false; } - const trimmed = prompt.trim(); const letterPattern = new RegExp( `^[${letter}${letter.toLowerCase()}](?:[\\s.,:;)]|$)`, "u", ); - const lead = choiceLead(choice); return ( - letterPattern.test(trimmed) || (lead !== "" && trimmed.startsWith(lead)) + letterPattern.test(prompt.trim()) || isChoiceNamed(prompt, optionSet, index) ); } + +/** + * 判断用户消息是否以某一项选项文字的第一个分句开头. 选项字母在每条回复中都有, + * 只有紧跟着那条回复时才指向这一项; 选项文字本身说明了用户要做什么, 隔了几条 + * 消息之后仍然有效. + * + * @param {string} prompt 用户消息. + * @param {import("./spec.mjs").OptionSetSpec} optionSet 选项组. + * @param {number} index 选项下标, 从 0 开始. + * @returns {boolean} 以该项的第一个分句开头时返回 true. + */ +export function isChoiceNamed(prompt, optionSet, index) { + const choice = optionSet.choices[index]; + if (choice === undefined) { + return false; + } + const lead = choiceLead(choice); + return lead !== "" && prompt.trim().startsWith(lead); +} diff --git a/plugins/waypoint/skills/project-navigator/runtime/lib/order-approval.mjs b/plugins/waypoint/skills/project-navigator/runtime/lib/order-approval.mjs index 1a852fc..4bbb73e 100644 --- a/plugins/waypoint/skills/project-navigator/runtime/lib/order-approval.mjs +++ b/plugins/waypoint/skills/project-navigator/runtime/lib/order-approval.mjs @@ -4,6 +4,7 @@ * hook 在编排会话回复 "工单审阅" 时记下当时工单文件的摘要, 在用户回答时记下 * 选择; `order set issued` 只接受摘要与当前工单文件一致的认可. 认可在发布时 * 用掉; 工单内容改过之后重新发布要重新审阅, 受阻后内容没有改动时可以直接继续. + * 用户没有选发布时记录保留: 之后的消息写明 "发布工单" 时仍可认可同一份内容. * 审阅记录存在运行期登记目录中, hook 不写状态文件. */ @@ -11,7 +12,7 @@ import { createHash } from "node:crypto"; import { existsSync, readFileSync } from "node:fs"; import path from "node:path"; -import { isChoiceSelected } from "./option-answer.mjs"; +import { isChoiceNamed, isChoiceSelected } from "./option-answer.mjs"; import { orderFilePath } from "./paths.mjs"; import { readOrderApproval, @@ -41,6 +42,21 @@ export const APPROVAL_STATUSES = Object.freeze({ none: "none", }); +/** + * 审阅记录中的状态: 用户看过审阅, 但没有选发布. 对外等同于没有有效的审阅. + * @type {string} + */ +const DECLINED_STATUS = "declined"; + +/** + * 用户的消息还能改变结果的记录状态: 正在等用户选择, 或用户没有选发布. + * @type {readonly string[]} + */ +const ANSWERABLE_STATUSES = Object.freeze([ + APPROVAL_STATUSES.awaiting, + DECLINED_STATUS, +]); + /** * 计算工单文件内容的摘要; 统一换行符, 避免编辑器的换行差异让认可失效. * @@ -95,9 +111,10 @@ export function readApprovalStatus(projectRoot, order) { ) { return APPROVAL_STATUSES.none; } - return record.status === APPROVAL_STATUSES.approved - ? APPROVAL_STATUSES.approved - : APPROVAL_STATUSES.awaiting; + return record.status === APPROVAL_STATUSES.approved || + record.status === APPROVAL_STATUSES.awaiting + ? record.status + : APPROVAL_STATUSES.none; } /** @@ -124,8 +141,12 @@ export function markAwaitingApproval(projectRoot, order, now) { } /** - * 记录用户对 "工单审阅" 的回答: 选 A 时认可审阅时的内容; 其它回答撤销等待, - * 编排会话按要求修改后要重新回复 "工单审阅". 没有等待中的审阅时不做任何事. + * 记录用户对 "工单审阅" 的回答. 紧跟着审阅的那条消息选 A (字母或选项文字) 时 + * 认可审阅时的内容, 其它回答记为没有选发布. 没有选发布之后, 用户的消息以 + * "发布工单" 的选项文字开头时仍然认可: 用户撤回上一条消息重新回答时, 审阅回复 + * 不会再结束一次, 只能靠这条记录认出来; 单独的字母此时可能在回答别的回复, 不算. + * 认可的始终是审阅时的内容, 工单文件改过之后要重新审阅. 没有针对当前工单的 + * 审阅记录时不做任何事. * * @param {object} options 参数. * @param {string} options.projectRoot 项目根目录. @@ -139,22 +160,22 @@ export function applyApprovalAnswer({ projectRoot, order, prompt, spec, now }) { const record = readOrderApproval(projectRoot); if ( record === undefined || - record.status !== APPROVAL_STATUSES.awaiting || - record.order !== order?.id + record.order !== order?.id || + !ANSWERABLE_STATUSES.includes(record.status) ) { return; } const optionSet = spec.replies[APPROVAL_REPLY_TYPE].optionSets[APPROVE_CHOICE.optionSet]; - if (isChoiceSelected(prompt, optionSet, APPROVE_CHOICE.choice)) { - writeOrderApproval(projectRoot, { - ...record, - status: APPROVAL_STATUSES.approved, - updatedAt: now, - }); - } else { - removeOrderApproval(projectRoot); - } + const isApproved = + record.status === APPROVAL_STATUSES.awaiting + ? isChoiceSelected(prompt, optionSet, APPROVE_CHOICE.choice) + : isChoiceNamed(prompt, optionSet, APPROVE_CHOICE.choice); + writeOrderApproval(projectRoot, { + ...record, + status: isApproved ? APPROVAL_STATUSES.approved : DECLINED_STATUS, + updatedAt: now, + }); } /** diff --git a/plugins/waypoint/skills/project-navigator/runtime/lib/registry.mjs b/plugins/waypoint/skills/project-navigator/runtime/lib/registry.mjs index 6538119..985b84c 100644 --- a/plugins/waypoint/skills/project-navigator/runtime/lib/registry.mjs +++ b/plugins/waypoint/skills/project-navigator/runtime/lib/registry.mjs @@ -15,7 +15,7 @@ import { ORDER_APPROVAL_FILE, registryDirectory } from "./paths.mjs"; * @typedef {object} OrderApprovalRecord 用户对一张工单的审阅. * @property {string} order 工单编号. * @property {string} digest 审阅时工单文件内容的摘要. - * @property {"awaiting" | "approved"} status 等待用户选择, 或用户已选发布. + * @property {"awaiting" | "approved" | "declined"} status 等待用户选择, 用户已选发布, 或用户没有选发布. * @property {string} updatedAt 最后更新时间. */ @@ -42,7 +42,7 @@ export function writeOrderApproval(worktreeRoot, record) { } /** - * 删除工单审阅记录: 审阅被用户否决, 或已用于发布工单. + * 删除工单审阅记录: 已用于发布工单, 或工单文件不存在. * * @param {string} worktreeRoot 工作区根目录. * @returns {void} diff --git a/tests/project-navigator/flow.test.mjs b/tests/project-navigator/flow.test.mjs index a6de1b5..b04c013 100644 --- a/tests/project-navigator/flow.test.mjs +++ b/tests/project-navigator/flow.test.mjs @@ -404,6 +404,32 @@ test("工单往返: 认可之后改过工单, 发布被拒; 未验证的判据 ); }); +test("工单审阅: 没有选发布之后, 写明发布工单的消息仍然认可, 单独的字母不算", (context) => { + const setup = setupProject(context); + const { root, project, orchestrator } = setup; + project(["order", "new", "--kind", "implementation", "--slug", "export"]); + const folder = ".navigator/orders/0001-export"; + const order = fill(commandSkeleton(project(["template", "order"]).stdout)); + writeProjectFile(root, `${folder}/order.md`, order); + stop( + root, + orchestrator, + fill(commandSkeleton(project(["reply", "approve"]).stdout)), + ); + prompt(root, orchestrator, "判据 3 是什么意思?"); + const refused = project(["order", "set", "issued"]); + assert.equal(refused.status, 1); + assert.match(refused.stdout, /只运行 reply 命令不算审阅/u); + prompt(root, orchestrator, "A"); + assert.equal(project(["order", "set", "issued"]).status, 1); + writeProjectFile(root, `${folder}/order.md`, `${order}\n补一句.\n`); + prompt(root, orchestrator, "发布工单"); + assert.equal(project(["order", "set", "issued"]).status, 1); + writeProjectFile(root, `${folder}/order.md`, order); + prompt(root, orchestrator, "发布工单"); + assert.equal(project(["order", "set", "issued"]).status, 0); +}); + test("基准提交: 新建工单之后仓库有新提交时, 审阅拒绝旧值, 发布后验收提示词用当前提交", (context) => { const setup = setupProject(context); const { root, project, orchestrator } = setup;