From d2e8ddaa060e7300f872d90c1e3e9ab8ab1d3b92 Mon Sep 17 00:00:00 2001 From: auYeCoding Date: Thu, 12 Nov 2026 05:44:41 -0800 Subject: [PATCH] =?UTF-8?q?feat(waypoint):=20project-navigator=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E5=8F=91=E5=B8=83=E5=89=8D=E6=A0=B8=E5=AF=B9,=20?= =?UTF-8?q?=E5=BE=85=E5=AE=9A=E5=88=A4=E6=8D=AE,=20=E7=AD=89=E5=BE=85?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=E4=B8=8E=E5=AD=90=E4=BB=A3=E7=90=86=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增核对子代理与 precheck-brief, order precheck: 工单审阅之前核对其中的事实 - 回执新增待定判据: 个别判据做不到时做完其余部分, 由用户在验收时决定 - 新增回复等待操作: 需要用户亲手操作时中途交回, 不写回执 - 新增 model 命令: 登记各类子代理的模型, 下一动作写出派出参数 - 核对, 验收, 阅读, 调研子代理默认改用 sonnet - 更新参考文件, 执行手册, 中英文文档与测试 工单中未经核对的事实拖到执行阶段才暴露, 每个错误都要整张工单往返一轮. --- plugins/waypoint/CHANGELOG.md | 12 ++ plugins/waypoint/agents/navigator-checker.md | 25 +++ .../docs/project-navigator-maintaining.en.md | 129 ++++++++------ .../docs/project-navigator-maintaining.md | 129 ++++++++------ plugins/waypoint/docs/project-navigator.en.md | 58 +++++-- plugins/waypoint/docs/project-navigator.md | 44 ++++- .../skills/project-navigator/SKILL.md | 54 +++--- .../project-navigator/guide/executor.md | 25 ++- .../project-navigator/references/files.md | 2 +- .../references/order-loop.md | 26 +-- .../project-navigator/references/replies.md | 6 + .../runtime/commands/brief.mjs | 40 ++++- .../runtime/commands/enter.mjs | 2 + .../runtime/commands/handlers.mjs | 3 + .../runtime/commands/model.mjs | 54 ++++++ .../runtime/commands/order.mjs | 48 ++++-- .../runtime/commands/reply.mjs | 23 +-- .../skills/project-navigator/runtime/hook.mjs | 12 +- .../project-navigator/runtime/lib/briefs.mjs | 65 ++++++- .../project-navigator/runtime/lib/guard.mjs | 23 ++- .../runtime/lib/guidance.mjs | 97 ++++++++--- .../project-navigator/runtime/lib/state.mjs | 4 + .../runtime/lib/subagent-models.mjs | 163 ++++++++++++++++++ .../runtime/lib/workflow-orders.mjs | 54 +++++- .../project-navigator/runtime/navigator.mjs | 3 +- .../project-navigator/spec/templates.json | 17 +- scripts/navigator-docs/executor-guide.md | 23 ++- tests/project-navigator/flow.test.mjs | 38 +++- tests/project-navigator/guard.test.mjs | 11 +- tests/project-navigator/workflow.test.mjs | 69 +++++++- 30 files changed, 1011 insertions(+), 248 deletions(-) create mode 100644 plugins/waypoint/agents/navigator-checker.md create mode 100644 plugins/waypoint/skills/project-navigator/runtime/commands/model.mjs create mode 100644 plugins/waypoint/skills/project-navigator/runtime/lib/subagent-models.mjs diff --git a/plugins/waypoint/CHANGELOG.md b/plugins/waypoint/CHANGELOG.md index 8969a9f..09567f5 100644 --- a/plugins/waypoint/CHANGELOG.md +++ b/plugins/waypoint/CHANGELOG.md @@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## [Unreleased] +### Added + +- `project-navigator`: a precheck before the order review. A new read-only subagent `waypoint:navigator-checker`, dispatched with the output of the new `precheck-brief` command, checks each fact written in the work order (file paths, ids in the data, the release form and license of external resources) and whether each criterion can be verified. The orchestrator fixes the order and runs the new `order precheck`; until then `reply approve` and `order set issued` refuse a drafting order. In a real project every blocked round of the implementation orders came from an unchecked fact in the order. +- `project-navigator`: pending criteria. When only a few criteria cannot be met as written, the executor finishes the rest and lists them under the new "待定判据" (pending criteria) item of the receipt instead of ending as blocked; the reviewer marks them unverified, and the user decides in the review report. +- `project-navigator`: a new reply "等待操作" (waiting for your action, no option block). When the executor only needs the user to do something by hand, it hands back midway without writing a receipt, and continues after the user replies; the order status does not change. Before, each such step was a full blocked round trip. +- `project-navigator`: subagent models. The new `model` command shows the assignment and `model set [--kind ]` changes it; the "next action" and a new "子代理模型" (subagent models) line of `status` spell out the `model` parameter to pass when dispatching. + +### Changed + +- `project-navigator`: the checker, reviewer, reader, and researcher subagents now run on Sonnet by default; the executor still follows the orchestrator's model. Run `project-navigator model set inherit` to restore the earlier behavior for a role. +- `project-navigator`: a drafting order written before this version needs `order precheck` once before it can be reviewed. + ## [1.3.2] - 2026-11-12 ### Fixed diff --git a/plugins/waypoint/agents/navigator-checker.md b/plugins/waypoint/agents/navigator-checker.md new file mode 100644 index 0000000..1ad3501 --- /dev/null +++ b/plugins/waypoint/agents/navigator-checker.md @@ -0,0 +1,25 @@ +--- +name: navigator-checker +description: project-navigator 的核对子代理, 只由编排会话按 precheck-brief 生成的提示词派出, 在工单发布之前逐条核对其中写下的事实. Pre-issue fact checker for project-navigator; dispatched only by the orchestrator session with the generated precheck brief. Do not use for anything else. +disallowedTools: Write, Edit, MultiEdit, NotebookEdit +--- + +你是 project-navigator 的核对子代理, 在全新的上下文中核对一张还没有发布的工单: 其中写下的事实是否成立, 判据能否验证. + +## 立场 + +- 只读: 不写, 不改, 不删任何文件, 也不尝试这样做. 不安装依赖, 不改动项目. +- 只报告事实与证据, 不改写工单, 不给实现方案, 不评价工单写得好不好. +- 委派提示词中的 "核对要求" 与 "交回格式" 是唯一的任务说明, 逐条完成, 不增不减. + +## 取证 + +- 每条结论都附证据: 文件路径与行号, 命令的原样输出, 或网页地址与摘录. 不凭记忆下结论. +- 只运行只读命令: 读文件, 搜索, 只读 git 命令, 以及查询数据的命令 (例如对项目数据文件的只读查询). 不运行构建, 测试与会写文件的脚本. +- 外部资源 (下载地址, 发布形式, 许可) 用网络工具查看原始页面. +- 结论只用委派提示词列出的几种. 查不到证据时写 "无法核对", 并写明缺少什么; 不凭推测写 "成立". + +## 交回 + +- 按委派提示词的 "交回格式" 输出, 用简体中文. +- 标点使用英文 (ASCII) 标点, 每个标点后加一个空格; 标点属于路径, 版本号等记号时不加. diff --git a/plugins/waypoint/docs/project-navigator-maintaining.en.md b/plugins/waypoint/docs/project-navigator-maintaining.en.md index ca57126..3b82c01 100644 --- a/plugins/waypoint/docs/project-navigator-maintaining.en.md +++ b/plugins/waypoint/docs/project-navigator-maintaining.en.md @@ -23,7 +23,7 @@ Plugin-level wiring: | `hooks/hooks.json` | Four events (PreToolUse matching write tools only, UserPromptSubmit, SessionStart, Stop) all call `runtime/hook.mjs` | | `bin/project-navigator` | Command entry for the Bash tool: `exec node .../navigator.mjs "$@"` | | `bin/project-navigator.cmd` | Command entry for the PowerShell tool | -| `agents/navigator-*.md` | Executor, reviewer, researcher, and reader subagents | +| `agents/navigator-*.md` | Checker, executor, reviewer, researcher, and reader subagents | In a user's project, the skill uses these locations: @@ -47,13 +47,15 @@ Each entry states the decision, the reason, and what happens if it is changed. - **The orchestrator is not registered; it is recognized by a marker in the transcript.** `enter` outputs "编排标记: project-navigator 编排标记 ", where the id is the first 12 hex digits of the SHA-256 of the project path (lowercased on Windows). The output is injected into the skill content, which is recorded in the session transcript, and the hook looks for the marker through `transcript_path`. The source code contains only the prefix, so a session that reads the source is not mistaken for the orchestrator; `status` does not print the marker, so a subagent running `status` does not affect the main session. A forked session copies the transcript and carries the marker, so rewinding or editing a message needs no handling. 0.x registered the identity in `.git/navigator/orchestrator.json`, with rules for taking over, being superseded, claiming forks, and re-registering. If changed: back to registration and claiming, with a chance of misjudging identity every time the session id changes. - **An unchanged blocked order continues with the same executor.** `order set issued` records a digest of the order file at issue time (`issuedDigest`). When a blocked order's file still matches, no new review is needed, and the output tells the orchestrator to resume the original subagent with SendMessage (measured: SendMessage resumes a finished subagent with its context), or to dispatch a new one if it cannot. A changed order needs a new review. If changed: a small user action costs a new review, or the subagent starts over every time. - **The executor subagent omits `tools` and inherits every tool of the session.** It writes code, runs builds and tests, and may need the user's MCP tools. Plugin subagents do not support the `hooks`, `mcpServers`, and `permissionMode` fields (official documentation); its write scope is limited by the plugin-level guard. If changed: listing `tools` misses the user's tools. -- **Delegation prompts are generated by scripts and used verbatim; the guard does not compare them.** `exec-brief`, `review-brief`, and `research-brief` are the single source of the prompts. The 0.x guard compared the review prompt character by character and occasionally refused a dispatch over whitespace. If changed: prompts scatter across rule files and drift between versions. +- **A subagent's model is set at dispatch, and the assignment lives in the state file.** Defaults: the executor follows the orchestrator; the checker, reviewer, reader, and researcher use sonnet (`lib/subagent-models.mjs`). `model set` changes it, and the executor can be set per order kind. The "next action" and the "子代理模型" (subagent models) line of `status` spell out the `model` parameter of the Agent tool. It is not written into the `model` field of the subagent definitions for three reasons: the official documentation says the dispatch parameter outranks the definition and is kept when the subagent is resumed with SendMessage, while it does not say whether a definition's `model` is kept on resume; a fixed value would affect every project; and users can change models without editing plugin files. The executor is not downgraded by default: in a real project, all 5 blocked rounds of implementation orders were the executor proving the order wrong with evidence, and a weaker model is more likely to force a result. If changed: a resumed subagent may fall back to the session model, or every project is forced onto one executor model. +- **When the user must do something by hand, the executor hands back midway without a receipt.** The first line of the handback is "执行状态: 等待操作" (status: waiting for an action); the orchestrator replies with "等待操作" (no option block) and forwards the user's answer to the same subagent with SendMessage; the order stays issued throughout. In the same project a reverse-analysis order was blocked 8 times, each time only to ask the user for one action in the program under analysis, yet each round rewrote the receipt, moved the order to blocked, replied "受阻处理" with options, and issued again. Decisions and order changes still go through blocked. The original subagent is lost when the session ends, so the guide requires writing what is already known into `artifacts/` before handing back. If changed: every step of an interactive analysis is a full blocked round trip. +- **Delegation prompts are generated by scripts and used verbatim; the guard does not compare them.** `precheck-brief`, `exec-brief`, `review-brief`, and `research-brief` are the single source of the prompts. The 0.x guard compared the review prompt character by character and occasionally refused a dispatch over whitespace. If changed: prompts scatter across rule files and drift between versions. ### Guard and reply checks -- **The guard looks only at the target path of write tools, with four rules.** Nobody writes the state file or the generated views directly; the reviewer writes no files; under `.navigator/` the executor writes only the current order's receipt and `artifacts/`, writes no business files on record-only orders, and writes no business files when the order is not running; the orchestrator writes only under `.navigator/`. Subagents are told apart by `agent_type` in the hook input (`waypoint:navigator-executor` and so on; measured: plugin subagents report ":"). 0.x also parsed Bash command text, whitelisted the orchestrator's tools, and checked prompts, record structure, user test output, and changes to `.claude/settings.json`; most of these rules were added for cross-session scenarios and past incidents, and they caused most denials and misjudgments. If changed: each rule added back blocks some normal operation. +- **The guard looks only at the target path of write tools, with four rules.** Nobody writes the state file or the generated views directly; the checker and the reviewer write no files; under `.navigator/` the executor writes only the current order's receipt and `artifacts/`, writes no business files on record-only orders, and writes no business files when the order is not running; the orchestrator writes only under `.navigator/`. Subagents are told apart by `agent_type` in the hook input (`waypoint:navigator-executor` and so on; measured: plugin subagents report ":"). 0.x also parsed Bash command text, whitelisted the orchestrator's tools, and checked prompts, record structure, user test output, and changes to `.claude/settings.json`; most of these rules were added for cross-session scenarios and past incidents, and they caused most denials and misjudgments. If changed: each rule added back blocks some normal operation. - **"The orchestrator writes no business files" stays.** It is not about malice but about role collapse: one "just fix it" from the user and the model writes code, and the orchestrator's context is muddled from then on. The transcript is read only when the main session writes a project file outside `.navigator/`. If changed: the orchestrator gradually turns into an executor. -- **Stop checks only reply structure: the heading is a reply type, the first line under it is the conclusion, there is exactly one option block matching the spec, code blocks are closed, and no placeholder remains.** The heading identifies the reply type and the option block identifies the user's choice (order review approval depends on it), so these two cannot be relaxed. A reply whose `optionSets` is empty in the spec ("工单执行", "工单验收", and "等待结果", used while waiting for a researcher or reader subagent) needs no choice from the user and has no option block; adding one sends the reply back. In 1.0.0 and 1.1.0 "工单执行" had a "wait" option, and users thought they had to choose A before the wait started. Sections, keys, progress values, and writing rules are reminders in the fill-in instructions. 0.x sent replies back for writing rules and summary length, leaving two copies on screen. A reply is sent back at most once (`stop_hook_active` passes). Replies are checked while background tasks run too: up to 1.2.1 they were not, and with the reviewer in the background the model wrote an untitled note that blended into the replies around it; both kinds of waiting now have a reply type. The progress line shows the stage after the order id (`format.orderStages`; "验收报告" sets `orderStage` to "待你决定"), which tells consecutive replies apart. Recommendations go only in the "处理建议" section of "验收报告", and the fill-in instructions say to add no text outside the sections. If changed: writing rules become frequent send-backs again. +- **Stop checks only reply structure: the heading is a reply type, the first line under it is the conclusion, there is exactly one option block matching the spec, code blocks are closed, and no placeholder remains.** The heading identifies the reply type and the option block identifies the user's choice (order review approval depends on it), so these two cannot be relaxed. A reply whose `optionSets` is empty in the spec ("工单执行", "工单验收", "等待结果", used while waiting for a checker, researcher, or reader subagent, and "等待操作", used while waiting for the user's action) needs no choice from the user and has no option block; adding one sends the reply back. In 1.0.0 and 1.1.0 "工单执行" had a "wait" option, and users thought they had to choose A before the wait started. Sections, keys, progress values, and writing rules are reminders in the fill-in instructions. 0.x sent replies back for writing rules and summary length, leaving two copies on screen. A reply is sent back at most once (`stop_hook_active` passes). Replies are checked while background tasks run too: up to 1.2.1 they were not, and with the reviewer in the background the model wrote an untitled note that blended into the replies around it; both kinds of waiting now have a reply type. The progress line shows the stage after the order id (`format.orderStages`; "验收报告" sets `orderStage` to "待你决定"), which tells consecutive replies apart. Recommendations go only in the "处理建议" section of "验收报告", and the fill-in instructions say to add no text outside the sections. If changed: writing rules become frequent send-backs again. - **Option groups with two choices also become buttons; the option block stays.** The Claude desktop app has a tool that fills its "next step" box (`mcp__ccd_turn__wrap_up`, see `BUTTON_TOOL` in `lib/reply-guide.mjs`). The box takes at most 2 buttons, and a click sends the button text as the user's reply. The fill-in instructions from `reply` include a "按钮" (buttons) line; the button text is the first clause of each choice (`choiceLead`), so a click is recognized the same way as a typed answer (`isChoiceSelected`). Groups with three or more choices get no buttons, and neither do groups marked `"buttons": false` in the spec (the user must type more after choosing, or the choice leads to a commit: the app does not allow a reply button to approve a commit). Buttons never replace the option block: the box can be dismissed and is not kept in the transcript, and both the Stop check and order review approval read the option block. The tool is internal to the app with no public contract, and no hook checks that it was called; if it is renamed or removed, replies fall back to the option block alone without errors. If changed: without the option block, other environments and users who dismiss the box cannot see the choices. - **Replies start with a one-line conclusion; the human summary block at the end is gone.** A reader with little time sees only what comes first; the summary at the end repeated the conclusion. The conclusion length is only a reminder (`conclusionMaxLength`). Progress went from four lines to one. If changed: the key information ends up at the bottom. - **Record files are not checked at write time; `check ` self-checks after writing.** Scripts that read records (review point prefill, the receipt's execution status, the review's verdicts) read key lines and tables and give a clear reason when they cannot. `check` with files assigns no checkup number and writes no state, so the executor can use it too. If changed: writes are denied as often as in 0.x. @@ -61,6 +63,8 @@ Each entry states the decision, the reason, and what happens if it is changed. ### Work orders and review - **A work order needs two user decisions: publish it in "工单审阅" (order review), and accept and commit it in "验收报告" (review report).** The review card lists only the "审阅要点" (review points: goal, decisions needed, out of scope, sizing reason) and the order file path, prefilled by `reply approve` from the order file. Approval is still bound to the digest of the order file (the hook records the digest when the review reply ends and the choice when the user answers), so a shorter display does not weaken the binding. 0.x quoted five sections of the order verbatim; users could not read it in a few seconds, and most details were beyond their judgment anyway. If changed: the review becomes a formality, or users read every order in full. +- **A checker subagent checks each work order once before the review.** The orchestrator writes orders from the records it already has; when a concrete fact in one (a file path, an id in the data, the release form or license of an external resource) does not hold, the executor finds out halfway through. In the same project all 5 blocked rounds of implementation orders were of this kind, and the orchestrator had read almost no project files before writing those orders. `precheck-brief` generates the prompt; `waypoint:navigator-checker` checks read-only and returns the items that do not hold or cannot be checked; the orchestrator fixes the order and runs `order precheck`. Without that mark, `reply approve` and `order set issued` refuse a drafting order. The mark records only that the check ran once and is not bound to the order content: changes made from the findings, or after a block, need no second check, otherwise every edit costs another subagent run. The findings are not stored in a file of their own; they end up in the corrected order. If changed: mistakes in an order surface during execution again, one round trip per mistake. +- **When a few criteria cannot be met, the executor finishes the rest and lists them as pending.** The receipt's "偏差记录" (deviations) has a "待定判据" (pending criteria) item, and the status is still completed. The reviewer confirms the stated reason and marks those criteria unverified with evidence starting with "待定:" (pending); they go through the existing option set 3 of "验收报告", where the user accepts as is or issues a follow-up order. No new status or option set. In the same project, 3 blocked receipts said the rest of the work was already done and only one criterion was stuck. If the user's decision would change how the rest is done, the order is still blocked. If changed: the wording of one criterion costs the whole order another review and issue. - **Test authorization is gone.** The executor does not run tests with external effects and lists them under "外部影响" (external effects) in the receipt's test record; the reviewer runs the code checks and the receipt's other test commands, not the external ones, and judges those from the receipt's output. MCP evidence tools are no longer registered; the reviewer makes read-only queries at the places named in the receipt's evidence record, with the constraint stated in the prompt and the subagent definition. If changed: every order with an external test costs one more user choice. - **Unverified criteria can be accepted explicitly by the user.** The third option group of "验收报告" (only unverified, nothing failed) lets the user accept as is and commit, via `order set accepted --accept-unverified`; the unverified criteria are recorded as risks. 0.x always failed the order, so criteria that depend on external evidence were rejected over and over. Failed criteria still cannot be accepted. If changed: orders cannot converge when external conditions are missing. - **Committing is not an order state.** Order states are drafting, issued, blocked, reviewing, accepted, rejected, and voided; after `order set accepted` the orchestrator calls commit-message as the output says, and the state file goes in with that commit. 0.x had committing and a commit settlement record to match `lastCommit`; without reconciliation, neither is needed. When old state is read, accepted and committing orders count as accepted and ended. If changed: the state file stays modified after each commit. @@ -116,7 +120,8 @@ Entry points and commands: | `commands/plan.mjs` | Roadmap, milestone and slice status | | `commands/order.mjs` | Work order creation and status changes, archiving old receipts | | `commands/records.mjs` | Risks, decisions, changes | -| `commands/brief.mjs` | Executor, review, and research delegation prompts | +| `commands/brief.mjs` | Precheck, executor, review, and research delegation prompts | +| `commands/model.mjs` | Show and set subagent models | | `commands/evidence.mjs` | Fetches the source lines cited by a selection receipt's evidence table | | `commands/check.mjs` | Checkups and self-checks | | `commands/standards.mjs` | Writes the project standards | @@ -126,16 +131,16 @@ Entry points and commands: Modules in `runtime/lib/`: -| Group | Files | -| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Paths and state | `paths.mjs`, `state.mjs`, `atomic-file.mjs`, `numbering.mjs`, `validation.mjs`, `workflow-error.mjs`, `tracked-paths.mjs` | -| Spec and layout | `spec.mjs`, `markdown.mjs`, `layout.mjs`, `reply-checks.mjs`, `file-checks.mjs`, `writing-checks.mjs`, `text-units.mjs`, `freeform-sections.mjs` | -| Generation | `render.mjs`, `render-plan.mjs`, `table.mjs`, `briefs.mjs`, `reminders.mjs`, `guidance.mjs`, `reply-guide.mjs`, `file-guide.mjs`, `writing-rules.mjs` | -| Guard and identity | `guard.mjs` (four write rules), `orchestrator-marker.mjs` (recognizes the orchestrator from the transcript) | -| Workflow | `workflow-orders.mjs`, `workflow-plan.mjs`, `workflow-records.mjs`, `code-checks.mjs`, `review-record.mjs`, `receipt-record.mjs`, `order-approval.mjs`, `order-base.mjs`, `option-answer.mjs`, `registry.mjs` | -| Source evidence | `source-evidence.mjs`, `source-fetch.mjs` | -| Anomaly self-checks | `diagnostics.mjs` (judging and recording anomalies, stalls), `state-checks.mjs` (state self-check) | -| Repository and environment | `repo.mjs`, `environment.mjs`, `version.mjs`, `settings.mjs` | +| Group | Files | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Paths and state | `paths.mjs`, `state.mjs`, `atomic-file.mjs`, `numbering.mjs`, `validation.mjs`, `workflow-error.mjs`, `tracked-paths.mjs` | +| Spec and layout | `spec.mjs`, `markdown.mjs`, `layout.mjs`, `reply-checks.mjs`, `file-checks.mjs`, `writing-checks.mjs`, `text-units.mjs`, `freeform-sections.mjs` | +| Generation | `render.mjs`, `render-plan.mjs`, `table.mjs`, `briefs.mjs`, `reminders.mjs`, `guidance.mjs`, `reply-guide.mjs`, `file-guide.mjs`, `writing-rules.mjs` | +| Guard and identity | `guard.mjs` (four write rules), `orchestrator-marker.mjs` (recognizes the orchestrator from the transcript) | +| Workflow | `workflow-orders.mjs`, `workflow-plan.mjs`, `workflow-records.mjs`, `code-checks.mjs`, `review-record.mjs`, `receipt-record.mjs`, `order-approval.mjs`, `order-base.mjs`, `option-answer.mjs`, `registry.mjs`, `subagent-models.mjs` | +| Source evidence | `source-evidence.mjs`, `source-fetch.mjs` | +| Anomaly self-checks | `diagnostics.mjs` (judging and recording anomalies, stalls), `state-checks.mjs` (state self-check) | +| Repository and environment | `repo.mjs`, `environment.mjs`, `version.mjs`, `settings.mjs` | Other parts of the repository: @@ -144,33 +149,36 @@ 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. 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` | +| 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` | +| `reply approve` or `order set issued` is refused, mentioning the precheck | The drafting order never had `order precheck` run (orders drafted before 1.4.0 need it once too) | `lib/workflow-orders.mjs` | Check `prechecked` on the current order in `state.json` | +| A subagent did not use the assigned model | The orchestrator dispatched without the `model` parameter, or the user set `CLAUDE_CODE_SUBAGENT_MODEL_FORCE` | `lib/subagent-models.mjs`, `lib/guidance.mjs` | The "子代理模型" line of `status`; the parameters of the Agent call in the session | +| The executor ended without a receipt and its first line is "执行状态: 等待操作" | A midway handback, not an anomaly | `lib/briefs.mjs`, `scripts/navigator-docs/executor-guide.md` | Reply "等待操作", then resume with SendMessage after the user answers | +| 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 @@ -204,7 +212,7 @@ Other parts of the repository: | `code-checks.test.mjs` | Check command registration, the fixed criterion, checks at creation and before issue, the check commands to run, skeleton prefill | | `evidence.test.mjs` | Reading and validating the evidence table, fetching lines at a version from a local temporary repository, the single-entry self-check | | `guard.test.mjs` | An allow and a deny case for each of the four write rules, record-only orders, orders not running, judging the orchestrator only when needed | -| `workflow.test.mjs` | Order states (withdrawing blocked orders, issue digests, no committing state), roadmap, stages, records, the orchestrator marker, option recognition, verdict classification, delegation prompts, next actions | +| `workflow.test.mjs` | Order states (withdrawing blocked orders, issue digests, no committing state), roadmap, stages, records, the orchestrator marker, option recognition, verdict classification, delegation prompts (including the precheck), next actions, subagent models | | `settings.test.mjs` | Merging and removing permission rules, recognizing and removing 0.x project-level hooks, the wiring of the plugin's `hooks.json` and `bin/` | | `cli.test.mjs` | Entering and initialization (marker, no hooks written, no scripts copied), resume, ignore rule checks, upgrading from 0.x (cleanup, order withdrawal, old orchestrator registration), migrating old committing orders, uninstall, `ref`, reply skeletons, self-checks | | `flow.test.mjs` | Projects that are not initialized, recognizing the orchestrator from the transcript, reminders, reply structure checks, the order round trip to review and the commit step, changing an approved order, accepting unverified criteria, review prefill, continuing a blocked order, next actions after rejection, check commands, the stage commit step | @@ -250,6 +258,18 @@ Fetching source evidence (git 2.55, 2026-09-23): fetching a few lines of one fil Without limits, one framing research ran more than 80 web calls and took about 13 minutes; the frame now caps them at 20 in total. +### Subagent models (official documentation, 2026-11-12, not tested) + +Taken from the sub-agents, plugins/components, and model-config pages of the Claude Code documentation, as excerpted by a lookup subagent: + +| Point | What the documentation says | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------- | +| Order of precedence | The `model` parameter at dispatch, the definition's `model`, the `CLAUDE_CODE_SUBAGENT_MODEL` variable, the session model | +| Dispatch values | Only the aliases `sonnet`, `opus`, `haiku`, `fable`; no full model id and no `inherit` | +| Resume | The dispatch parameter is kept on a SendMessage resume; whether a definition's `model` is kept is not stated | +| Forced override | With `CLAUDE_CODE_SUBAGENT_MODEL_FORCE=1` set by the user, every subagent uses the session model | +| Reasoning effort | The session's effort does not carry to a subagent on another model; definitions have an `effort` field. Not set here | + ## Manual tests before a release In a temporary repository with `claude --plugin-dir ./plugins/waypoint`, confirm each item: @@ -276,14 +296,18 @@ In a temporary repository with `claude --plugin-dir ./plugins/waypoint`, confirm ### Work order round trip -1. "工单审阅" (order review) shows only the review points and the order file path, matching the order file; after B the order is changed and reviewed again; after A it is issued, an executor is dispatched in the background, and the reply is "工单执行" (order running); changing the order file after approval makes the issue fail. -2. While the executor works in the background, the user can keep talking to the orchestrator; when it finishes, the orchestrator runs `status` on its own and starts the review. -3. In the default permission mode, the executor's write prompts appear in the orchestrator session; after switching to Accept edits they stop. -4. The executor is blocked (for example it needs an account): the reply is "受阻处理" (blocked handling); after the user acts, no new review is needed and the original subagent continues through SendMessage. -5. "工单执行" has no option block; replying "停止执行" (stop) during execution: the subagent stops, the order goes back to drafting, and it is reviewed again after changes. -6. The reviewer returns verdicts and evidence and is denied when it tries to write files; it does not run tests the receipt lists as external effects. -7. After a passing review, A commits and B commits and pushes; with unverified criteria the third group is used, and A accepts the order and records risks; with failures a follow-up order is issued, and for an implementation order the orchestrator first judges whether it was too large. -8. The executor's writes to records other than its order's receipt and `artifacts/` are denied; the orchestrator's business file writes are denied. +1. After writing an order the orchestrator first sends the checker subagent (the Agent call carries `model: "sonnet"`), and the checker is refused when it tries to write a file; put a nonexistent file into a criterion on purpose, the findings point it out, and the orchestrator fixes the order and runs `order precheck`; `reply approve` without the precheck is refused. +2. The executor meets one criterion it cannot satisfy: it finishes the rest, the receipt lists the criterion as pending, and the review report uses option set 3 and describes that criterion. +3. On an analysis order the executor needs a user action: no receipt, the handback is "执行状态: 等待操作"; the orchestrator replies "等待操作" (no option block), the same subagent continues after the user replies "操作完成", and `status` shows the order as issued throughout. +4. After `model set executor sonnet --kind fix`, the next action of a fix order says `model 参数写 "sonnet"` while an implementation order still says not to pass it; after a resume (SendMessage) the subagent still uses its dispatch model. +5. "工单审阅" (order review) shows only the review points and the order file path, matching the order file; after B the order is changed and reviewed again; after A it is issued, an executor is dispatched in the background, and the reply is "工单执行" (order running); changing the order file after approval makes the issue fail. +6. While the executor works in the background, the user can keep talking to the orchestrator; when it finishes, the orchestrator runs `status` on its own and starts the review. +7. In the default permission mode, the executor's write prompts appear in the orchestrator session; after switching to Accept edits they stop. +8. The executor is blocked (for example it needs an account): the reply is "受阻处理" (blocked handling); after the user acts, no new review is needed and the original subagent continues through SendMessage. +9. "工单执行" has no option block; replying "停止执行" (stop) during execution: the subagent stops, the order goes back to drafting, and it is reviewed again after changes. +10. The reviewer returns verdicts and evidence and is denied when it tries to write files; it does not run tests the receipt lists as external effects. +11. After a passing review, A commits and B commits and pushes; with unverified criteria the third group is used, and A accepts the order and records risks; with failures a follow-up order is issued, and for an implementation order the orchestrator first judges whether it was too large. +12. The executor's writes to records other than its order's receipt and `artifacts/` are denied; the orchestrator's business file writes are denied. ### Changes, milestone reviews, and anomalies @@ -302,4 +326,7 @@ In a temporary repository with `claude --plugin-dir ./plugins/waypoint`, confirm - Recognizing the orchestrator depends on the marker in the transcript, whose format is internal to Claude Code; when the transcript cannot be read, rule 4 does not apply (the write is allowed). - The executor subagent belongs to the session that started it; closing that session interrupts its work, and a new session continues according to whether the receipt was written. - A background subagent's write prompt does not name the subagent; users can infer it only from the "工单执行" (order running) reply and the task list. +- The precheck records only that it ran, not whether the orchestrator changed the order accordingly; the checker examines only the facts written in the order and cannot find unstated assumptions. +- The subagent model is only a parameter written in the "next action"; no hook checks that the orchestrator passes it. +- While waiting for a user action the order status does not change, and the original executor is lost with the session; a newly dispatched one can continue only from the documents under `artifacts/`. - Source evidence supports only public git repositories over https; `evidence` checks only the cited lines, and whether they support the "说明" (note) is still up to the reviewer. diff --git a/plugins/waypoint/docs/project-navigator-maintaining.md b/plugins/waypoint/docs/project-navigator-maintaining.md index 601c7ee..a4b95d4 100644 --- a/plugins/waypoint/docs/project-navigator-maintaining.md +++ b/plugins/waypoint/docs/project-navigator-maintaining.md @@ -23,7 +23,7 @@ | `hooks/hooks.json` | 四个事件 (PreToolUse 只匹配写入类工具, UserPromptSubmit, SessionStart, Stop) 都调用 `runtime/hook.mjs` | | `bin/project-navigator` | Bash 工具中的命令入口, `exec node .../navigator.mjs "$@"` | | `bin/project-navigator.cmd` | PowerShell 工具中的命令入口 | -| `agents/navigator-*.md` | 执行, 验收, 调研与阅读子代理 | +| `agents/navigator-*.md` | 核对, 执行, 验收, 调研与阅读子代理 | 在用户项目中, 技能会用到以下位置: @@ -47,13 +47,15 @@ - **编排会话不做登记, 按转录中的编排标记认出.** `enter` 的输出带 "编排标记: project-navigator 编排标记 <编号>", 编号是项目路径 SHA-256 的前 12 位 (Windows 上先转小写); 输出注入技能内容, 技能内容记在会话转录中, hook 读 `transcript_path` 找这个标记. 源代码里只有前缀, 读源代码的会话不会被误认; status 不输出标记, 子代理运行 status 不影响主会话. 分叉出的会话复制了转录, 自然带着标记, 回退与编辑消息之后不需要任何处理. 0.x 把身份登记在 `.git/navigator/orchestrator.json`, 为此有接管, 被接管, 分叉认领, 重新登记一系列规则. 改掉的后果: 回到登记与认领, 每次换编号都可能误判身份. - **受阻后工单没有改动时直接继续, 由原执行子代理接着做.** `order set issued` 记下发布时工单文件的摘要 (`issuedDigest`); 受阻工单的文件与摘要一致时不要求重新审阅, 输出要求用 SendMessage 让原子代理继续 (实测 SendMessage 能带着上下文续接已结束的子代理), 找不到时重新派出. 工单改过就要重新审阅. 改掉的后果: 用户完成一个小操作也要重新审阅一次, 或子代理每次从头再来. - **执行子代理不写 `tools`, 继承会话的全部工具.** 它要写代码, 运行构建与测试, 用户装的 MCP 工具也可能用到. 插件子代理不支持 `hooks`, `mcpServers`, `permissionMode` 字段 (官方文档), 写入范围由插件级 hook 的守卫限制. 改掉的后果: 列出 `tools` 时漏掉用户的工具. -- **委派提示词由脚本生成, 编排会话原样使用, 守卫不核对.** `exec-brief`, `review-brief`, `research-brief` 是提示词的唯一来源. 0.x 守卫逐字核对验收提示词, 偶尔因空白差异拒绝派出. 改掉的后果: 提示词散落在规则文件里, 版本之间不一致. +- **子代理的模型在派出时指定, 分配记在状态文件中.** 默认: 执行跟随编排会话, 核对, 验收, 阅读与调研用 sonnet (`lib/subagent-models.mjs`); `model set` 可以改, 执行子代理可以按工单类型单独指定. "下一动作" 与 `status` 的 "子代理模型" 一行写出 Agent 工具 `model` 参数的写法. 不写进子代理定义的 `model` 字段有三个原因: 官方文档写明派出参数优先于定义, 用 SendMessage 续接时保留, 而定义中的 `model` 续接时是否保留没有说明; 写死会影响所有项目; 用户换模型不用改插件文件. 执行默认不降: 在一个实际项目中, 实现工单的 5 轮受阻都是执行子代理拿出证据指出工单有错, 较弱的模型更可能硬凑结果. 改掉的后果: 续接时模型可能变回会话模型, 或所有项目的执行都被迫用同一个模型. +- **需要用户亲手操作时, 执行子代理中途交回, 不写回执.** 交回第一行写 "执行状态: 等待操作", 编排会话回复 "等待操作" (没有选项块), 用户回复后用 SendMessage 转给原子代理; 工单状态一直是 issued. 同一项目的逆向分析工单受阻 8 轮, 每轮都只是请用户在被分析的程序里操作一次, 却要重写回执, 转为受阻, 回复带选项的 "受阻处理", 再重新发布. 要用户做决定或改工单时仍走受阻. 会话中断后原子代理找不回, 所以手册要求交回前把已查清的内容写进 `artifacts/`. 改掉的后果: 交互式的分析任务每一步都是一轮完整的受阻往返. +- **委派提示词由脚本生成, 编排会话原样使用, 守卫不核对.** `precheck-brief`, `exec-brief`, `review-brief`, `research-brief` 是提示词的唯一来源. 0.x 守卫逐字核对验收提示词, 偶尔因空白差异拒绝派出. 改掉的后果: 提示词散落在规则文件里, 版本之间不一致. ### 守卫与回复校验 -- **守卫只看写入类工具的目标路径, 四条规则.** 状态文件与生成的视图谁都不能直接写; 验收子代理不写文件; 执行子代理在 `.navigator/` 下只写当前工单的回执与 `artifacts/`, 只产出记录的工单不写业务文件, 工单不在执行中时不写业务文件; 编排会话只写 `.navigator/`. 子代理按 hook 输入的 `agent_type` 区分 (`waypoint:navigator-executor` 等, 实测插件子代理的值为 "<插件>:<名称>"). 0.x 还解析 Bash 命令文字, 按白名单放行编排会话的工具, 核对提示词, 记录文件结构, 用户测试输出与 `.claude/settings.json` 的改动; 这些规则多数为跨会话场景与历次事故补上, 也是拒绝与误判的主要来源. 改掉的后果: 加回去的每条规则都会在某个正常操作上误伤. +- **守卫只看写入类工具的目标路径, 四条规则.** 状态文件与生成的视图谁都不能直接写; 核对与验收子代理不写文件; 执行子代理在 `.navigator/` 下只写当前工单的回执与 `artifacts/`, 只产出记录的工单不写业务文件, 工单不在执行中时不写业务文件; 编排会话只写 `.navigator/`. 子代理按 hook 输入的 `agent_type` 区分 (`waypoint:navigator-executor` 等, 实测插件子代理的值为 "<插件>:<名称>"). 0.x 还解析 Bash 命令文字, 按白名单放行编排会话的工具, 核对提示词, 记录文件结构, 用户测试输出与 `.claude/settings.json` 的改动; 这些规则多数为跨会话场景与历次事故补上, 也是拒绝与误判的主要来源. 改掉的后果: 加回去的每条规则都会在某个正常操作上误伤. - **保留 "编排会话不写业务文件".** 目的不是防恶意, 是防角色坍塌: 用户一句 "顺手改一下", 模型就会自己写代码, 编排会话的上下文从此混乱. 只在主会话写项目内 `.navigator/` 之外的文件时才读转录判断身份. 改掉的后果: 编排会话逐渐变成执行会话. -- **Stop 只校验回复结构: 标题是回复类型, 标题下第一行是结论, 恰好一个与规格一致的选项块, 代码块都关闭, 没有占位标记.** 标题用来识别回复类型, 选项块用来识别用户的选择 (工单审阅的认可依赖它), 这两项不能放松. 规格中 `optionSets` 为空的回复 ("工单执行", "工单验收" 与等待调研, 阅读子代理的 "等待结果") 不需要用户选择, 不带选项块, 带了会被打回: 1.0.0 与 1.1.0 的 "工单执行" 有 "等待完成" 选项, 用户以为要先选 A 才开始等待. 节, 键名, 进展值与写作规则只在填写要求中提醒. 0.x 按写作规则与总结字数打回, 打回后屏幕上留着两遍回复. 打回最多一次 (`stop_hook_active` 时放行). 有后台任务时照常校验: 1.2.1 及之前不校验, 验收子代理在后台时模型写了一段没有标题的说明, 与前后的回复混在一起; 现在等待子代理的两种情况都有回复类型. 进展行的工单编号之后写环节 (`format.orderStages`, "验收报告" 用 `orderStage` 写 "待你决定"), 连续几条回复靠它区分. 建议只写在 "验收报告" 的 "处理建议" 一节, 填写要求写明各节之外不加文字. 改掉的后果: 写作规则重新变成频繁打回. +- **Stop 只校验回复结构: 标题是回复类型, 标题下第一行是结论, 恰好一个与规格一致的选项块, 代码块都关闭, 没有占位标记.** 标题用来识别回复类型, 选项块用来识别用户的选择 (工单审阅的认可依赖它), 这两项不能放松. 规格中 `optionSets` 为空的回复 ("工单执行", "工单验收", 等待核对, 调研, 阅读子代理的 "等待结果", 与等用户操作的 "等待操作") 不需要用户选择, 不带选项块, 带了会被打回: 1.0.0 与 1.1.0 的 "工单执行" 有 "等待完成" 选项, 用户以为要先选 A 才开始等待. 节, 键名, 进展值与写作规则只在填写要求中提醒. 0.x 按写作规则与总结字数打回, 打回后屏幕上留着两遍回复. 打回最多一次 (`stop_hook_active` 时放行). 有后台任务时照常校验: 1.2.1 及之前不校验, 验收子代理在后台时模型写了一段没有标题的说明, 与前后的回复混在一起; 现在等待子代理的两种情况都有回复类型. 进展行的工单编号之后写环节 (`format.orderStages`, "验收报告" 用 `orderStage` 写 "待你决定"), 连续几条回复靠它区分. 建议只写在 "验收报告" 的 "处理建议" 一节, 填写要求写明各节之外不加文字. 改掉的后果: 写作规则重新变成频繁打回. - **两个选项的组同时做成按钮, 选项块照常保留.** Claude 桌面应用有一个填写 "下一步框" 的工具 (`mcp__ccd_turn__wrap_up`, 见 `lib/reply-guide.mjs` 的 `BUTTON_TOOL`), 最多 2 个按钮, 点按钮等于把按钮文字作为回复发出. `reply` 的填写要求给出 "按钮" 一条, 按钮文字是选项的第一个分句 (`choiceLead`), 与打字回复走同一条识别 (`isChoiceSelected`). 三项以上的组, 以及规格中写了 `"buttons": false` 的组 (选择后还要打字补充, 或选了就会提交: 应用规定提交不能用回复按钮) 不做按钮. 选项块不能因为有按钮而去掉: 框可以被关掉, 不留在对话记录中, Stop 的校验与工单审阅的认可都读选项块. 这个工具是应用内部的, 没有公开约定, hook 也不核对是否调用; 工具改名或消失时退回只有选项块, 不会出错. 改掉的后果: 去掉选项块, 其它环境与关掉框的用户看不到选项. - **回复以一行结论开头, 删掉末尾的人类总结块.** 读者注意力有限, 结论放在最前面才看得到; 末尾的总结与结论重复. 结论字数只作提醒 (`conclusionMaxLength`). 进展从四行压成一行. 改掉的后果: 关键信息落到回复末尾. - **记录文件写入时不校验结构, 写完用 `check <文件>` 自查.** 读记录的脚本 (审阅要点预填, 回执的执行状态, 验收记录的判据结论) 按键值行与表格读取, 读不到时命令给出明确原因. `check` 带文件时不分配体检编号, 不写状态, 执行子代理也能用. 改掉的后果: 写入被拦下的次数回到 0.x 的水平. @@ -61,6 +63,8 @@ ### 工单与验收 - **一张工单只要用户做两次决定: 工单审阅中发布, 验收报告中通过并提交.** 审阅卡片只列 "审阅要点" (任务目标, 需要决定, 明确不做, 粒度依据) 与工单文件路径, 由 `reply approve` 从工单文件预填. 认可仍按工单文件的摘要绑定 (hook 在回复工单审阅时记下摘要, 用户回答时记下选择), 展示的内容变短不影响绑定. 0.x 原样摘录工单五节, 用户三秒内读不完, 而且多数细节用户判断不了. 改掉的后果: 审阅变成走过场, 或用户每张工单都要读完整工单. +- **工单在审阅之前由核对子代理核对一次.** 编排会话凭已有的记录写工单, 其中的具体事实 (文件路径, 数据中的编号, 外部资源的发布形式与许可) 不成立时, 执行子代理做到一半才发现. 在同一个项目中, 实现工单的 5 轮受阻都属于这种情况, 而写这几张工单之前编排会话几乎没有读项目文件. `precheck-brief` 生成提示词, `waypoint:navigator-checker` 只读核对并交回不成立与无法核对的条目, 编排会话改好工单后运行 `order precheck`; 起草中的工单没有这个标记时, `reply approve` 与 `order set issued` 拒绝. 标记只记做过一次, 不绑定工单内容: 按核对结果修改, 或受阻后修改, 都不用再核对, 否则每改一次就多一轮子代理. 核对结果不另存文件, 落在改好的工单里. 改掉的后果: 工单中的错误回到执行阶段才暴露, 每个错误一轮往返. +- **个别判据做不到时, 执行子代理做完其余部分, 把这几条列为待定.** 回执 "偏差记录" 有 "待定判据" 一项, 执行状态仍写完成; 验收子代理核对原因属实后把这几条判为未验证, 证据以 "待定:" 开头, 走已有的 "验收报告" 第 3 组, 由用户接受现状或发补正工单. 没有新增状态与选项组. 同一项目中有 3 轮受阻的回执写明 "其余工作已全部完成", 只卡在一条判据上. 用户的决定会改变其余部分的做法时仍然受阻. 改掉的后果: 一条判据的措辞问题让整张工单多走一轮审阅与发布. - **测试授权去掉.** 执行子代理不运行有外部影响的测试, 写进回执 "测试记录" 的 "外部影响"; 验收子代理运行代码检查与回执中其它测试命令, 不重跑外部影响的命令, 按回执输出判断. MCP 取证工具不再登记, 验收子代理按回执 "取证记录" 做只读查询, 约束写在提示词与子代理定义中. 改掉的后果: 每张带外部测试的工单多一次用户选择. - **未验证的判据可以由用户明确接受.** "验收报告" 第 3 组 (只有未验证, 没有不通过) 由用户选择接受现状并提交, 对应 `order set accepted --accept-unverified`, 未验证的判据登记为风险. 0.x 一律判不通过, 依赖外部取证的判据反复被拒. 不通过的判据仍然不能接受. 改掉的后果: 外部条件不足时工单无法收敛. - **提交不是工单状态.** 工单状态为 drafting, issued, blocked, reviewing, accepted, rejected, voided; `order set accepted` 之后编排会话按输出调用 commit-message, 状态文件随这次提交入库. 0.x 有 committing 与提交结算记录, 用来对上 `lastCommit`; 去掉对账之后不再需要. 读取旧状态时, accepted 与 committing 的工单视为已通过并结束. 改掉的后果: 每次提交后状态文件留在工作区. @@ -116,7 +120,8 @@ | `commands/plan.mjs` | 推进路线, 里程与切片状态 | | `commands/order.mjs` | 工单新建与状态转换, 旧回执归档 | | `commands/records.mjs` | 风险, 决策, 变更 | -| `commands/brief.mjs` | 执行, 验收与调研的委派提示词 | +| `commands/brief.mjs` | 核对, 执行, 验收与调研的委派提示词 | +| `commands/model.mjs` | 查看与登记子代理模型 | | `commands/evidence.mjs` | 从原仓库取回选型回执证据表引用的源码行 | | `commands/check.mjs` | 体检与自查 | | `commands/standards.mjs` | 写入项目规范 | @@ -126,16 +131,16 @@ `runtime/lib/` 中的模块: -| 分组 | 文件 | -| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 路径与状态 | `paths.mjs`, `state.mjs`, `atomic-file.mjs`, `numbering.mjs`, `validation.mjs`, `workflow-error.mjs`, `tracked-paths.mjs` | -| 规格与版式 | `spec.mjs`, `markdown.mjs`, `layout.mjs`, `reply-checks.mjs`, `file-checks.mjs`, `writing-checks.mjs`, `text-units.mjs`, `freeform-sections.mjs` | -| 生成 | `render.mjs`, `render-plan.mjs`, `table.mjs`, `briefs.mjs`, `reminders.mjs`, `guidance.mjs`, `reply-guide.mjs`, `file-guide.mjs`, `writing-rules.mjs` | -| 守卫与身份 | `guard.mjs` (四条写入规则), `orchestrator-marker.mjs` (按转录认出编排会话) | -| 工作流 | `workflow-orders.mjs`, `workflow-plan.mjs`, `workflow-records.mjs`, `code-checks.mjs`, `review-record.mjs`, `receipt-record.mjs`, `order-approval.mjs`, `order-base.mjs`, `option-answer.mjs`, `registry.mjs` | -| 源码证据 | `source-evidence.mjs`, `source-fetch.mjs` | -| 异常自检 | `diagnostics.mjs` (异常的判断, 记录与原地打转), `state-checks.mjs` (状态自检) | -| 仓库与环境 | `repo.mjs`, `environment.mjs`, `version.mjs`, `settings.mjs` | +| 分组 | 文件 | +| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| 路径与状态 | `paths.mjs`, `state.mjs`, `atomic-file.mjs`, `numbering.mjs`, `validation.mjs`, `workflow-error.mjs`, `tracked-paths.mjs` | +| 规格与版式 | `spec.mjs`, `markdown.mjs`, `layout.mjs`, `reply-checks.mjs`, `file-checks.mjs`, `writing-checks.mjs`, `text-units.mjs`, `freeform-sections.mjs` | +| 生成 | `render.mjs`, `render-plan.mjs`, `table.mjs`, `briefs.mjs`, `reminders.mjs`, `guidance.mjs`, `reply-guide.mjs`, `file-guide.mjs`, `writing-rules.mjs` | +| 守卫与身份 | `guard.mjs` (四条写入规则), `orchestrator-marker.mjs` (按转录认出编排会话) | +| 工作流 | `workflow-orders.mjs`, `workflow-plan.mjs`, `workflow-records.mjs`, `code-checks.mjs`, `review-record.mjs`, `receipt-record.mjs`, `order-approval.mjs`, `order-base.mjs`, `option-answer.mjs`, `registry.mjs`, `subagent-models.mjs` | +| 源码证据 | `source-evidence.mjs`, `source-fetch.mjs` | +| 异常自检 | `diagnostics.mjs` (异常的判断, 记录与原地打转), `state-checks.mjs` (状态自检) | +| 仓库与环境 | `repo.mjs`, `environment.mjs`, `version.mjs`, `settings.mjs` | 仓库中的其它部分: @@ -144,33 +149,36 @@ ## 排查手册 -| 症状 | 根因 | 相关文件 | 验证方法 | -| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------- | -| 调用技能时直接中止, 提示权限检查失败 | `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` | +| 症状 | 根因 | 相关文件 | 验证方法 | +| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | --------------------------------------------------------------------------------------- | +| 调用技能时直接中止, 提示权限检查失败 | `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` 的工单编号, 摘要与状态 | +| `reply approve` 或 `order set issued` 被拒, 提到发布前核对 | 起草中的工单没有运行过 `order precheck` (1.4.0 之前起草的工单也要补一次) | `lib/workflow-orders.mjs` | 查看 `state.json` 中当前工单的 `prechecked` | +| 子代理没有用上登记的模型 | 编排会话派出时没有传 `model` 参数, 或用户设置了 `CLAUDE_CODE_SUBAGENT_MODEL_FORCE` | `lib/subagent-models.mjs`, `lib/guidance.mjs` | `status` 的 "子代理模型" 一行; 会话中 Agent 调用的参数 | +| 执行子代理结束了, 没有回执, 交回第一行是 "执行状态: 等待操作" | 这是中途交回, 不是异常 | `lib/briefs.mjs`, `scripts/navigator-docs/executor-guide.md` | 回复 "等待操作", 用户回复后用 SendMessage 续接 | +| 受阻后继续发布被拒 | 工单文件在发布后改过, `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` | ## 修改流程 @@ -204,7 +212,7 @@ | `code-checks.test.mjs` | 检查命令登记, 固定判据, 新建与发布前核对, 要运行的检查命令, 骨架预填 | | `evidence.test.mjs` | 证据表的读取与写法校验, 从本机临时仓库按版本取行, 单条自查命令 | | `guard.test.mjs` | 四条写入规则各有放行例与拒绝例, 只产出记录的工单, 工单不在执行中, 只在需要时判断编排会话 | -| `workflow.test.mjs` | 工单状态 (含受阻撤回, 发布摘要, 没有提交中的状态), 路线, 阶段, 记录, 编排标记, 选项识别, 验收结论分类, 委派提示词, 下一动作 | +| `workflow.test.mjs` | 工单状态 (含受阻撤回, 发布摘要, 没有提交中的状态), 路线, 阶段, 记录, 编排标记, 选项识别, 验收结论分类, 委派提示词 (含发布前核对), 下一动作, 子代理模型 | | `settings.test.mjs` | 放行规则的合并与移除, 旧版本项目级 hook 的识别与清理, 插件 `hooks.json` 与 `bin/` 的接线 | | `cli.test.mjs` | 进入与初始化 (编排标记, 不写 hook 不复制脚本), 恢复进度, 忽略规则检查, 从 0.x 升级 (清理与工单撤回, 旧编排登记), 旧提交中工单的迁移, 卸载, `ref`, 回复骨架, 自查 | | `flow.test.mjs` | 未初始化的项目, 按转录认出编排会话, 提醒, 回复结构校验, 工单往返到验收与提交步骤, 认可后改工单, 接受未验证判据, 审阅预填, 受阻继续, 验收不通过的下一动作, 检查命令, 阶段提交步骤 | @@ -250,6 +258,18 @@ 调研框架不设范围时, 一次立项调研超过 80 次网络调用, 耗时约 13 分钟; 框架现在限制为合计 20 次. +### 子代理模型 (官方文档, 2026-11-12, 没有实测) + +以下内容摘自 Claude Code 文档的 sub-agents, plugins/components 与 model-config 三页, 由查询子代理摘录: + +| 待验证点 | 文档的说法 | +| -------------- | -------------------------------------------------------------------------------------------------- | +| 取值顺序 | 派出时的 `model` 参数, 子代理定义的 `model`, 环境变量 `CLAUDE_CODE_SUBAGENT_MODEL`, 编排会话的模型 | +| 派出参数的取值 | 只接受别名 `sonnet`, `opus`, `haiku`, `fable`, 不接受完整模型编号与 `inherit` | +| 续接 | 派出参数在 SendMessage 续接时保留; 定义中的 `model` 续接时是否保留, 文档没有说明 | +| 强制覆盖 | 用户设置 `CLAUDE_CODE_SUBAGENT_MODEL_FORCE=1` 时子代理一律用会话模型 | +| 推理强度 | 会话的推理强度不带到另一个模型的子代理上; 子代理定义有 `effort` 字段. 本技能目前不设置 | + ## 发版前手动测试 在临时仓库中用 `claude --plugin-dir ./plugins/waypoint` 逐项确认: @@ -276,14 +296,18 @@ ### 工单往返 -1. "工单审阅" 只有审阅要点与工单文件路径, 与工单文件一致; 选 B 后修改再审阅; 选 A 后发布并在后台派出执行子代理, 回复 "工单执行"; 认可之后改工单文件, 发布被拒. -2. 执行子代理在后台工作时, 能继续与编排会话对话; 子代理结束后编排会话自动运行 status 并进入验收. -3. 默认权限模式下, 执行子代理的写入确认弹在编排会话中; 切到 Accept edits 后不再弹出. -4. 执行子代理受阻 (例如需要用户提供账号): 回复 "受阻处理"; 用户完成操作后不需要重新审阅, 原子代理用 SendMessage 继续. -5. "工单执行" 没有选项块; 执行期间回复 "停止执行": 子代理停止, 工单撤回为起草中, 修改后重新审阅. -6. 验收子代理交回结论与证据, 它尝试写文件时被拒绝; 回执中列为外部影响的测试它不运行. -7. 验收通过选 A 提交, 选 B 提交并推送; 有未验证判据时用第 3 组, 选 A 后工单通过并登记风险; 有不通过时补正, 实现工单先判断是否太大. -8. 执行子代理写本工单回执与 `artifacts/` 之外的编排记录被拒; 编排会话写业务文件被拒. +1. 写好工单后编排会话先派核对子代理 (Agent 调用带 `model: "sonnet"`), 核对子代理尝试写文件时被拒; 故意在判据里写一个不存在的文件, 核对结果指出来, 编排会话改好后运行 `order precheck`; 跳过核对直接 `reply approve` 被拒. +2. 执行子代理遇到一条做不到的判据: 做完其余部分, 回执列为待定, 验收报告用第 3 组并写明这条判据的情况. +3. 分析工单中执行子代理需要用户操作: 不写回执, 交回 "执行状态: 等待操作"; 编排会话回复 "等待操作" (没有选项块), 用户回复 "操作完成" 后原子代理继续, `status` 的工单状态一直是 issued. +4. `model set executor sonnet --kind fix` 之后, 修复工单的下一动作写 `model 参数写 "sonnet"`, 实现工单仍写不传; 续接 (SendMessage) 之后子代理仍用派出时的模型. +5. "工单审阅" 只有审阅要点与工单文件路径, 与工单文件一致; 选 B 后修改再审阅; 选 A 后发布并在后台派出执行子代理, 回复 "工单执行"; 认可之后改工单文件, 发布被拒. +6. 执行子代理在后台工作时, 能继续与编排会话对话; 子代理结束后编排会话自动运行 status 并进入验收. +7. 默认权限模式下, 执行子代理的写入确认弹在编排会话中; 切到 Accept edits 后不再弹出. +8. 执行子代理受阻 (例如需要用户提供账号): 回复 "受阻处理"; 用户完成操作后不需要重新审阅, 原子代理用 SendMessage 继续. +9. "工单执行" 没有选项块; 执行期间回复 "停止执行": 子代理停止, 工单撤回为起草中, 修改后重新审阅. +10. 验收子代理交回结论与证据, 它尝试写文件时被拒绝; 回执中列为外部影响的测试它不运行. +11. 验收通过选 A 提交, 选 B 提交并推送; 有未验证判据时用第 3 组, 选 A 后工单通过并登记风险; 有不通过时补正, 实现工单先判断是否太大. +12. 执行子代理写本工单回执与 `artifacts/` 之外的编排记录被拒; 编排会话写业务文件被拒. ### 变更, 复查与异常 @@ -302,4 +326,7 @@ - 编排会话的认定依赖转录中的标记, 转录格式是 Claude Code 的内部格式; 读不到转录时规则 4 不生效 (放行). - 执行子代理属于启动它的会话, 会话关闭后子代理的工作中断; 新会话按回执是否写入继续. - 后台子代理的写入确认不写明是子代理, 用户只能从 "工单执行" 回复与任务列表推断. +- 发布前核对只记是否做过, 不核对编排会话是否按结果改了工单; 核对子代理只查工单中写出来的事实, 没写出来的隐含假设查不到. +- 子代理模型只是写在 "下一动作" 里的参数, hook 不核对编排会话是否照传. +- 等待操作期间工单状态不变, 原执行子代理随会话结束而丢失; 新派出的子代理只能靠 `artifacts/` 下的文档接着做. - 源码证据只支持公开 git 仓库的 https 地址; `evidence` 只核对被引用的行, 这些行是否真能支持 "说明" 仍由验收子代理判断. diff --git a/plugins/waypoint/docs/project-navigator.en.md b/plugins/waypoint/docs/project-navigator.en.md index 4b8d01e..4534825 100644 --- a/plugins/waypoint/docs/project-navigator.en.md +++ b/plugins/waypoint/docs/project-navigator.en.md @@ -65,7 +65,7 @@ Take "build a tool that collects the team's weekly reports" as an example, start 2. The skill may recommend technologies from the research; key dependencies such as the language, framework, and core libraries are verified with selection work orders. 3. When a work order is written, the skill replies with "工单审阅" (work order review): the goal in one sentence, the decisions it needs from you, what is out of scope, and why the order has this size. Answer `A` if it is right, or `B` with the changes you want. The full order is at the path shown under the review points. 4. After you answer `A`, the skill dispatches an executor subagent in the background and replies with "工单执行" (order running). You can do something else; the orchestrator continues on its own when the executor finishes. -5. When the executor is blocked (for example, it needs an account from you, or a choice between approaches), the skill replies with "受阻处理" (blocked handling) for you to decide; the executor then continues. +5. When the executor is blocked (for example, the specification contradicts itself, or it needs a choice between approaches), the skill replies with "受阻处理" (blocked handling) for you to decide; the executor then continues. When it only needs you to do something by hand, the skill replies with "等待操作" (waiting for your action), and you reply "操作完成" (done) afterwards. 6. When the work is done, the skill sends the reviewer subagent and replies with "验收报告" (review report). If everything passes, choose `A` to commit, `B` to commit and push, or `C` if you found a problem. For a selection order, you decide whether to adopt the result or choose another option. The skeleton and every later slice follow the same work order round trip. Normally a work order needs two decisions from you: publish it at review, and accept and commit it after the review. @@ -97,15 +97,39 @@ All records are kept. The upgrade is committed through commit-message. ## Roles -| Role | Who | What it does | -| ----------------- | ------------------------------------------------ | --------------------------------------------------------------------------------------------- | -| Orchestrator | The session that invokes the skill | Plans, records, writes work orders, organizes reviews and checkups; writes only `.navigator/` | -| Executor subagent | Dispatched by the orchestrator in the background | Writes code, tests, and the receipt for one work order; never commits or pushes | -| Reviewer subagent | Sent by the orchestrator, with a fresh context | Checks each criterion, runs the code checks and tests, returns evidence; read-only | -| Research subagent | Sent by the orchestrator during framing | Researches the problem domain: similar products, users, practice, constraints; read-only | -| You | The only decision maker | Every decision: publishing orders, handling blocks, accepting reviews, how to commit | +| Role | Who | What it does | +| ----------------- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------- | +| Orchestrator | The session that invokes the skill | Plans, records, writes work orders, organizes reviews and checkups; writes only `.navigator/` | +| Checker subagent | Sent by the orchestrator before the order review | Checks that each fact written in the order holds and each criterion can be verified; read-only | +| Executor subagent | Dispatched by the orchestrator in the background | Writes code, tests, and the receipt for one work order; never commits or pushes | +| Reviewer subagent | Sent by the orchestrator, with a fresh context | Checks each criterion, runs the code checks and tests, returns evidence; read-only | +| Research subagent | Sent by the orchestrator during framing | Researches the problem domain: similar products, users, practice, constraints; read-only | +| You | The only decision maker | Every decision: publishing orders, handling blocks, accepting reviews, how to commit | -The hooks guard only a few boundaries: the orchestrator cannot write files outside `.navigator/`; the state file and the generated views can only be changed by plugin commands; under `.navigator/` the executor can write only its order's receipt and evidence files, and on run-check and analysis orders it cannot change business files; the reviewer cannot write files. Everything else is left to the skill's rules and is not blocked. +The hooks guard only a few boundaries: the orchestrator cannot write files outside `.navigator/`; the state file and the generated views can only be changed by plugin commands; under `.navigator/` the executor can write only its order's receipt and evidence files, and on run-check and analysis orders it cannot change business files; the checker and the reviewer cannot write files. Everything else is left to the skill's rules and is not blocked. + +### Which model each subagent uses + +The model of a subagent is set when it is dispatched. The defaults are below; `project-navigator model` shows the current assignment: + +| Subagent | Default model | Why | +| ------------------------------------- | ------------------------ | ---------------------------------------------------------------------------- | +| Executor | Follows the orchestrator | It designs and decides, and it is the one that points out a wrong work order | +| Checker, reviewer, reader, researcher | Sonnet | They gather evidence against a list and need no design judgement | + +To change it, tell the orchestrator or run the command yourself. A model is `sonnet`, `opus`, `haiku`, `fable`, or `inherit` (follow the orchestrator): + +```bash +project-navigator model set reviewer inherit +``` + +The executor can be set per order kind, for example Sonnet for fix orders only: + +```bash +project-navigator model set executor sonnet --kind fix +``` + +Before moving implementation orders to a smaller model, look at how often recent orders were blocked or rejected: one round of rework makes the orchestrator read the receipt again, change the order, and dispatch again, which can cancel the saving. ### Permission prompts @@ -127,14 +151,16 @@ A requirement change can be inserted at any stage, and the work returns to where ## One work order round trip -1. **Review.** The orchestrator writes the work order and replies with "工单审阅" (work order review), listing only the review points and the order file path. The order can be issued only after you choose A; if the order changes after that, it is reviewed again. -2. **Execution.** The orchestrator dispatches an executor subagent in the background and replies with "工单执行" (order running). The executor checks the commit and assumptions, plans, implements, checks itself, and writes the receipt. "工单执行" needs no choice from you, so it has no option block; to stop and change the order midway, reply "停止执行" (stop). -3. **Blocked.** When the executor hits a specification problem, or needs an action or decision from you, it explains in the receipt and stops. The orchestrator replies with "受阻处理" (blocked handling); once you have handled it and the order does not need to change, the same executor continues without a new review. -4. **Review of the work.** The orchestrator sends the reviewer subagent to check each criterion; the only verdicts are pass, fail, and unverified. While the reviewer works in the background, the orchestrator first replies with "工单验收" (order under review, no option block), then with "验收报告" (review report) when the result is back; its recommendation goes only in the "处理建议" (recommendation) section. In "验收报告": +1. **Precheck.** After writing the work order, the orchestrator sends a checker subagent to check the facts in it one by one: file paths, ids in the data, the release form and license of external resources, and whether each criterion can be verified. The orchestrator corrects what does not hold and puts trade-offs that need you under "需要决定" (decisions needed) in the review points. This step needs nothing from you. +2. **Review.** The orchestrator replies with "工单审阅" (work order review), listing only the review points and the order file path. The order can be issued only after you choose A; if the order changes after that, it is reviewed again. +3. **Execution.** The orchestrator dispatches an executor subagent in the background and replies with "工单执行" (order running). The executor checks the commit and assumptions, plans, implements, checks itself, and writes the receipt. "工单执行" needs no choice from you, so it has no option block; to stop and change the order midway, reply "停止执行" (stop). +4. **Waiting for your action.** When the order is fine and the executor only needs you to do something by hand (for example, one action in the program being debugged), it writes no receipt and hands back midway; the orchestrator replies with "等待操作" (waiting for your action) and lists the steps. Reply "操作完成" (done) with what you actually did, and the executor continues. This can repeat; the order status does not change. +5. **Blocked.** When the executor hits a specification problem, or needs a decision from you, it explains in the receipt and stops. The orchestrator replies with "受阻处理" (blocked handling); once you have handled it and the order does not need to change, the same executor continues without a new review. When only a few criteria cannot be met as written and the rest is unaffected, this is not blocked: the executor finishes the rest and lists those criteria as pending in the receipt, for you to decide at acceptance. +6. **Review of the work.** The orchestrator sends the reviewer subagent to check each criterion; the only verdicts are pass, fail, and unverified. While the reviewer works in the background, the orchestrator first replies with "工单验收" (order under review, no option block), then with "验收报告" (review report) when the result is back; its recommendation goes only in the "处理建议" (recommendation) section. In "验收报告": - Everything passes: you may verify by hand following the "人工验收" (manual verification) steps, then choose to commit, or commit and push. - Something fails: the orchestrator first judges why; an order that was too large is split, otherwise a fix order follows. - - Only unverified criteria remain: you decide to accept as is and commit, or to follow up; accepted unverified criteria are recorded as risks. -5. **Commit.** After acceptance, the orchestrator calls commit-message, and the business changes and records of the round become one commit. Changes outside the receipt are listed for you to decide. + - Only unverified criteria remain: you decide to accept as is and commit, or to follow up; accepted unverified criteria are recorded as risks. Criteria the receipt lists as pending are handled here too; "处理建议" (recommendation) states the facts and the options for each. +7. **Commit.** After acceptance, the orchestrator calls commit-message, and the business changes and records of the round become one commit. Changes outside the receipt are listed for you to decide. Work orders run strictly one after another. The orchestrator sizes each order by four limits (one executor run can finish it, one review can check it, it runs afterwards, risky parts are separate), one slice per order by default, and states the reason in the review points. @@ -173,7 +199,7 @@ B. 需要修改, 请提需求. ``` ```` -The conclusion line alone tells you what is asked of you; the details follow for when you need them. The parentheses at the end of the progress line show where the work order stands (起草中 drafting, 执行中 running, 受阻 blocked, 验收中 under review, 待你决定 awaiting your decision), which tells consecutive replies apart. +The conclusion line alone tells you what is asked of you; the details follow for when you need them. The parentheses at the end of the progress line show where the work order stands (起草中 drafting, 执行中 running, 待你操作 awaiting your action, 受阻 blocked, 验收中 under review, 待你决定 awaiting your decision), which tells consecutive replies apart. ## Record files diff --git a/plugins/waypoint/docs/project-navigator.md b/plugins/waypoint/docs/project-navigator.md index 8877b6e..ff68cb7 100644 --- a/plugins/waypoint/docs/project-navigator.md +++ b/plugins/waypoint/docs/project-navigator.md @@ -63,7 +63,7 @@ 2. 技能可以根据调研给出技术建议; 语言, 框架, 核心库这类关键依赖, 写成选型工单核实. 3. 工单写好后, 技能回复 "工单审阅": 一句话目标, 需要你拍板的事, 明确不做的事, 以及为什么这样划分工单. 无误回复 `A`, 需要修改回复 `B` 并写明要求. 工单文件的完整内容在审阅要点下方给出的路径中. 4. 你选 `A` 后, 技能在后台派出执行子代理, 回复 "工单执行". 你可以去做别的事, 子代理做完时编排会话会自动接着处理. -5. 执行子代理受阻时 (例如需要你提供账号, 或在几种做法中选一种), 技能回复 "受阻处理" 请你决定; 处理完后子代理接着做. +5. 执行子代理受阻时 (例如规格有矛盾, 或要在几种做法中选一种), 技能回复 "受阻处理" 请你决定; 处理完后子代理接着做. 只是要你亲手做一件事时, 技能回复 "等待操作", 你做完后回复 "操作完成". 6. 做完后技能派验收子代理核对, 回复 "验收报告". 全部通过时选 `A` 提交, 选 `B` 提交并推送, 发现问题选 `C`. 选型工单则由你决定采用还是换方案. 骨架与之后的每个切片, 都按同样的工单往返推进. 正常情况下, 一张工单只需要你做两次决定: 审阅时发布, 验收时通过并提交. @@ -98,12 +98,36 @@ hook 由插件提供, 不写进项目配置; 没有初始化的项目中, 插件 | 角色 | 由谁担任 | 做什么 | | ---------- | ------------------------ | ------------------------------------------------------ | | 编排会话 | 调用本技能的会话 | 规划, 记录, 写工单, 组织验收, 体检; 只写 `.navigator/` | +| 核对子代理 | 编排会话在工单审阅前派出 | 逐条核对工单中写下的事实是否成立, 判据能否验证, 只读 | | 执行子代理 | 编排会话在后台派出 | 按一张工单写代码, 测试, 写回执; 不提交, 不推送 | | 验收子代理 | 编排会话派出, 上下文全新 | 逐条核对判据, 运行代码检查与测试, 交回证据, 只读 | | 调研子代理 | 编排会话在立项时派出 | 调研问题域: 同类产品, 使用人群, 行业做法, 约束, 只读 | | 你 | 唯一拍板的人 | 所有决定: 发布工单, 处理受阻, 决定验收结果与提交方式 | -hook 只守几条边界: 编排会话不能写 `.navigator/` 之外的文件; 状态文件与脚本生成的视图只能由插件命令修改; 执行子代理在 `.navigator/` 下只能写本工单的回执与证据文件, 运行检查与分析工单不能改业务文件; 验收子代理不能写文件. 其余靠技能规则, 不拦截. +hook 只守几条边界: 编排会话不能写 `.navigator/` 之外的文件; 状态文件与脚本生成的视图只能由插件命令修改; 执行子代理在 `.navigator/` 下只能写本工单的回执与证据文件, 运行检查与分析工单不能改业务文件; 核对与验收子代理不能写文件. 其余靠技能规则, 不拦截. + +### 子代理用哪个模型 + +子代理的模型在派出时指定, 默认分配如下, 用 `project-navigator model` 查看: + +| 子代理 | 默认模型 | 原因 | +| ---------------------- | ------------ | ---------------------------------------------- | +| 执行 | 跟随编排会话 | 要设计与判断, 也是指出工单有错的那一方 | +| 核对, 验收, 阅读, 调研 | Sonnet | 照单取证, 不需要设计判断, 用较小的模型更省用量 | + +要调整时告诉编排会话, 或自己运行命令. 模型可以写 `sonnet`, `opus`, `haiku`, `fable`, 或 `inherit` (跟随编排会话): + +```bash +project-navigator model set reviewer inherit +``` + +执行子代理可以按工单类型单独指定, 例如只让修复工单用 Sonnet: + +```bash +project-navigator model set executor sonnet --kind fix +``` + +实现类工单换用较小的模型之前, 先看几张工单的受阻与验收不通过的情况: 返工一次要编排会话重新读回执, 改工单, 再派一轮, 可能抵掉省下的用量. ### 权限确认 @@ -125,14 +149,16 @@ hook 只守几条边界: 编排会话不能写 `.navigator/` 之外的文件; ## 一张工单的往返 -1. **审阅.** 编排会话写好工单, 回复 "工单审阅", 只列审阅要点与工单文件路径. 你选 A 后工单才能发布; 选 A 之后工单再有改动, 要再审阅一次. -2. **执行.** 编排会话在后台派出执行子代理, 回复 "工单执行". 执行子代理核对提交与前提, 做计划, 实施, 自检, 写回执. "工单执行" 不需要你选择, 所以没有选项块; 中途想停下改工单时, 回复 "停止执行". -3. **受阻.** 执行子代理遇到规格问题, 需要你操作或决定时, 写回执说明并停下. 编排会话回复 "受阻处理"; 你处理完而工单不用改时, 原来的子代理接着做, 不需要重新审阅. -4. **验收.** 编排会话派验收子代理逐条核对判据, 结论只有通过, 不通过, 未验证三种. 验收子代理在后台核对时, 编排会话先回复 "工单验收" (没有选项块), 结果回来后回复 "验收报告"; 编排会话的建议只写在其中的 "处理建议" 一节. 回复 "验收报告" 时: +1. **核对.** 编排会话写好工单后, 先派核对子代理逐条核对其中的事实: 文件路径, 数据中的编号, 外部资源的发布形式与许可, 判据能否验证. 不成立的由编排会话改正, 需要你取舍的写进审阅要点的 "需要决定". 这一步不需要你参与. +2. **审阅.** 编排会话回复 "工单审阅", 只列审阅要点与工单文件路径. 你选 A 后工单才能发布; 选 A 之后工单再有改动, 要再审阅一次. +3. **执行.** 编排会话在后台派出执行子代理, 回复 "工单执行". 执行子代理核对提交与前提, 做计划, 实施, 自检, 写回执. "工单执行" 不需要你选择, 所以没有选项块; 中途想停下改工单时, 回复 "停止执行". +4. **等待操作.** 工单没有问题, 只是要你亲手做一件事才能继续时 (例如在被调试的程序中操作一次), 执行子代理不写回执, 中途交回; 编排会话回复 "等待操作", 列出操作步骤. 你做完后回复 "操作完成" 并写明做了什么, 子代理接着做. 这一步可以重复多次, 工单状态不变. +5. **受阻.** 执行子代理遇到规格问题, 或需要你做决定时, 写回执说明并停下. 编排会话回复 "受阻处理"; 你处理完而工单不用改时, 原来的子代理接着做, 不需要重新审阅. 只有个别判据照原文做不到, 其余部分不受影响时, 不算受阻: 执行子代理把其余工作做完, 这几条判据在回执中列为待定, 由你在验收时决定. +6. **验收.** 编排会话派验收子代理逐条核对判据, 结论只有通过, 不通过, 未验证三种. 验收子代理在后台核对时, 编排会话先回复 "工单验收" (没有选项块), 结果回来后回复 "验收报告"; 编排会话的建议只写在其中的 "处理建议" 一节. 回复 "验收报告" 时: - 全部通过: 你可以按 "人工验收" 的步骤亲手验证, 然后选择提交, 或提交并推送. - 有不通过: 编排会话先判断原因, 工单太大就拆小, 否则发修复工单. - - 只有未验证: 由你决定接受现状并提交, 或补正; 接受的未验证判据登记为风险. -5. **提交.** 验收通过后, 编排会话调用 commit-message, 本轮业务改动与记录合成一条提交. 有回执之外的改动时, commit-message 会列出来问你是否纳入. + - 只有未验证: 由你决定接受现状并提交, 或补正; 接受的未验证判据登记为风险. 回执列为待定的判据也在这里处理, "处理建议" 中写明每条的实际情况与可选做法. +7. **提交.** 验收通过后, 编排会话调用 commit-message, 本轮业务改动与记录合成一条提交. 有回执之外的改动时, commit-message 会列出来问你是否纳入. 工单严格一张接一张. 工单的大小由编排会话按 "执行一次能做完, 验收能看清, 做完能运行, 高风险部分单独成工单" 判断, 默认一个切片一张, 审阅要点中写明划分依据. @@ -171,7 +197,7 @@ B. 需要修改, 请提需求. ``` ```` -结论一行就能看出要你做什么; 细节在后面, 需要时再看. 进展行末尾的括号是工单所处的环节 (起草中, 执行中, 受阻, 验收中, 待你决定), 连续几条回复靠它区分先后. +结论一行就能看出要你做什么; 细节在后面, 需要时再看. 进展行末尾的括号是工单所处的环节 (起草中, 执行中, 待你操作, 受阻, 验收中, 待你决定), 连续几条回复靠它区分先后. ## 记录文件 diff --git a/plugins/waypoint/skills/project-navigator/SKILL.md b/plugins/waypoint/skills/project-navigator/SKILL.md index f2500b9..4a8c0cb 100644 --- a/plugins/waypoint/skills/project-navigator/SKILL.md +++ b/plugins/waypoint/skills/project-navigator/SKILL.md @@ -52,38 +52,42 @@ allowed-tools: Bash(node *) PowerShell(node *) Bash(project-navigator *) PowerSh 初始化之后, 命令一律写成 `project-navigator <命令>`, 由插件提供, 在 Bash 与 PowerShell 中都能直接运行. 一次只运行一条命令. -| 命令 | 用途与草稿格式 | -| -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | -| `init` | 初始化, 或升级旧版本初始化的项目 | -| `status` | 查看位置与下一动作 | -| `reply <编号> [--option <组号>]` | 取得填写要求与骨架; "下一动作" 已写出参数时照抄; 不带编号时列出全部类型与编号 | -| `template <种类> [--option <结构>]` | 取得记录文件骨架; 种类见 `ref files` | -| `ref <名称>` | 输出参考文件, 执行手册 (`executor`) 或工程规范默认包 (`standards`) | -| `stage <编号>`, `step <标识>`, `finish` | 记录阶段与步骤; `finish` 在 "项目收尾" 中用户选 A 后运行 | -| `skip <编号> --from <草稿>` | 记录用户认可的跳过: `{"reason"}` | -| `roadmap --from <草稿>` | 更新推进路线: `{"milestones": [{"name", "goal", "metrics": [], "slices": [{"name"}]}]}`, 已有条目保留 `id` | -| `milestone <编号> <状态>`, `slice <编号> <状态>` | 状态为 pending, active, done | -| `order new --kind <类型> --slug <短名> [--slice <编号>]` | 新建工单; 不带 `--slice` 时挂在当前切片 | -| `order set <状态> [--accept-unverified]` | 转换工单状态 | -| `exec-brief`, `review-brief`, `research-brief` | 取得执行, 验收与调研的委派提示词 | -| `evidence` | 从原仓库取回选型回执证据表引用的源码行; 由验收子代理运行 | -| `risk add --from <草稿>` | 登记风险: `{"description", "severity": "high/medium/low", "source", "handling"}` | -| `risk set <编号> <状态> [--from <草稿>]` | 状态为 open, investigating, resolved, accepted; 草稿 `{"handling"}` | -| `decision add --from <草稿>` | 登记决策: `{"title"}` | -| `decision supersede <编号> --by <编号>` | 标记决策被取代 | -| `change add --from <草稿>` | 登记变更: `{"title", "status": "accepted/deferred/dropped"}` | -| `check [文件...]` | 体检; 带文件时只自查这些文件 | -| `standards --from <草稿>` | 写入项目规范: `{"commentLanguage", "conventions": []}` | -| `codecheck set --from <草稿>` | 登记代码检查命令: `{"commands": [], "reason"}`, 没有命令时必须写原因 | -| `report [--from <草稿>]` | 生成技能问题报告: `{"doing", "expected", "actual", "reproduce", "blocked": false}`, 只写事实 | +| 命令 | 用途与草稿格式 | +| ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | +| `init` | 初始化, 或升级旧版本初始化的项目 | +| `status` | 查看位置与下一动作 | +| `reply <编号> [--option <组号>]` | 取得填写要求与骨架; "下一动作" 已写出参数时照抄; 不带编号时列出全部类型与编号 | +| `template <种类> [--option <结构>]` | 取得记录文件骨架; 种类见 `ref files` | +| `ref <名称>` | 输出参考文件, 执行手册 (`executor`) 或工程规范默认包 (`standards`) | +| `stage <编号>`, `step <标识>`, `finish` | 记录阶段与步骤; `finish` 在 "项目收尾" 中用户选 A 后运行 | +| `skip <编号> --from <草稿>` | 记录用户认可的跳过: `{"reason"}` | +| `roadmap --from <草稿>` | 更新推进路线: `{"milestones": [{"name", "goal", "metrics": [], "slices": [{"name"}]}]}`, 已有条目保留 `id` | +| `milestone <编号> <状态>`, `slice <编号> <状态>` | 状态为 pending, active, done | +| `order new --kind <类型> --slug <短名> [--slice <编号>]` | 新建工单; 不带 `--slice` 时挂在当前切片 | +| `order set <状态> [--accept-unverified]` | 转换工单状态 | +| `order precheck` | 记下当前工单已做过发布前核对, 之后才能审阅 | +| `precheck-brief`, `exec-brief`, `review-brief`, `research-brief` | 取得核对, 执行, 验收与调研的委派提示词 | +| `model`, `model set <角色> <模型> [--kind <工单类型>]` | 查看与登记子代理模型; 角色为 executor, checker, reviewer, reader, researcher; 模型为 sonnet, opus, haiku, fable, inherit | +| `evidence` | 从原仓库取回选型回执证据表引用的源码行; 由验收子代理运行 | +| `risk add --from <草稿>` | 登记风险: `{"description", "severity": "high/medium/low", "source", "handling"}` | +| `risk set <编号> <状态> [--from <草稿>]` | 状态为 open, investigating, resolved, accepted; 草稿 `{"handling"}` | +| `decision add --from <草稿>` | 登记决策: `{"title"}` | +| `decision supersede <编号> --by <编号>` | 标记决策被取代 | +| `change add --from <草稿>` | 登记变更: `{"title", "status": "accepted/deferred/dropped"}` | +| `check [文件...]` | 体检; 带文件时只自查这些文件 | +| `standards --from <草稿>` | 写入项目规范: `{"commentLanguage", "conventions": []}` | +| `codecheck set --from <草稿>` | 登记代码检查命令: `{"commands": [], "reason"}`, 没有命令时必须写原因 | +| `report [--from <草稿>]` | 生成技能问题报告: `{"doing", "expected", "actual", "reproduce", "blocked": false}`, 只写事实 | ## 子代理 +- 发布前核对: `waypoint:navigator-checker`, 提示词是 `precheck-brief` 的输出原文. 每张工单审阅之前核对一次. - 执行: `waypoint:navigator-executor`, 提示词是 `exec-brief` 的输出原文, 用 Agent 工具在后台派出 (`run_in_background`). 子代理结束后会自动通知编排会话. - 验收: `waypoint:navigator-reviewer`, 提示词是 `review-brief` 的输出原文. - 问题域调研: `waypoint:navigator-researcher`, 提示词以 `research-brief` 的输出开头, 其后只写调研问题. - 盘点, 规范对照与语义体检: `waypoint:navigator-reader`, 或 Explore. -- 调研与阅读子代理在后台运行, 本轮要先结束时回复 "等待结果" (`reply waiting`): "等待对象" 写子代理, "任务内容" 写它在查什么, "后续动作" 写结果回来后做什么; 不写其它说明. 这条回复不带选项块. +- 模型: 派子代理时, Agent 工具的 `model` 参数按 `status` 输出的 "子代理模型" 一行填写 (执行, 核对, 验收, 阅读, 调研各一项); 写 "跟随会话" 的不传这个参数. "下一动作" 已写出参数时照抄. 用户要调整时运行 `model set`. +- 核对, 调研与阅读子代理在后台运行, 本轮要先结束时回复 "等待结果" (`reply waiting`): "等待对象" 写子代理, "任务内容" 写它在查什么, "后续动作" 写结果回来后做什么; 不写其它说明. 这条回复不带选项块. - 委派提示词原样使用, 不增删内容. 不建 Agent Team. ## 初始设置 diff --git a/plugins/waypoint/skills/project-navigator/guide/executor.md b/plugins/waypoint/skills/project-navigator/guide/executor.md index bf959c8..8472e1d 100644 --- a/plugins/waypoint/skills/project-navigator/guide/executor.md +++ b/plugins/waypoint/skills/project-navigator/guide/executor.md @@ -15,7 +15,7 @@ ## 执行流程 1. 对账: 运行 `git status` 与 `git log -1`. 当前提交应等于工单概况中的 "基准提交", 工作区不应有与本工单无关的改动. 不一致时不动手, 写回执, 执行状态写 "受阻", 受阻类型写 "对账不符". -2. 核对前提: 逐条检查工单 "前提假设" 是否成立, 读代码与配置核实, 不凭记忆. 不成立时以受阻结束, 受阻类型写 "前提不符". +2. 核对前提: 逐条检查工单 "前提假设" 是否成立, 读代码与配置核实, 不凭记忆. 不成立时以受阻结束, 受阻类型写 "前提不符"; 只影响个别判据时按下方 "待定判据" 处理. 3. 做计划: 先定模块划分, 写法见下方 "模块划分"; 再列出完成每条验收判据的步骤与验证方式. 计划不需要用户确认, 但要写进回执的 "模块划分" 与 "判据自检". 4. 实施: 只做工单 "工作范围" 中要做的事; 遵守工单 "执行守则" 与项目规范 `.claude/rules/engineering.md`. 5. 自检: 逐条核对验收判据, 运行代码检查与测试. 需要真实账号, 会产生费用或外部副作用的测试不要运行, 把命令写进回执 "测试记录" 的 "外部影响", 由用户决定. @@ -46,7 +46,7 @@ ## 受阻情形 -遇到以下情况立即停下, 写回执, 执行状态写 "受阻", 不自行变通: +遇到以下情况立即停下, 写回执, 执行状态写 "受阻", 不自行变通. 两种情况例外, 先看下方 "待定判据" 与 "等待操作": - 对账不符, 或工单前提不成立. - 规格有歧义, 或判据之间互相矛盾. @@ -56,6 +56,25 @@ 回执 "偏差记录" 的 "受阻详情" 写受阻类型 (对账不符, 前提不符, 规格问题, 范围过大, 需要用户操作, 需要用户决定), 规格原文, 实际情况与证据位置. 用户处理后, 编排会话可能让你接着做: 收到继续的消息时, 先运行 `git status` 核对工作区, 再从受阻处继续, 最后重写回执. +## 待定判据 + +工单的其余部分不受影响, 只有个别判据照原文做不到时, 不以受阻结束: 把其余工作做完, 这几条判据留作待定, 由用户在验收时决定. 这样用户只决定一次, 不用整张工单往返一轮. + +- 适用情形: 判据点名的实例不存在或不带所要求的特征; 判据依赖的事实不成立; 判据要用户在两种说法中选一种, 而选哪种都不改变已经完成的工作. +- 不适用: 用户的决定会改变其余部分的做法 (例如随包带哪种格式的文件); 前提不成立导致主要工作无法进行; 要改动工作范围之外的文件. 这些情况仍按受阻处理. +- 写法: 回执 "执行状态" 写 "完成"; "偏差记录" 的 "待定判据" 逐条写判据编号, 规格原文, 实际情况与证据位置, 以及可选的处理办法 (例如 "改用实例 X 核对" 或 "以单元测试代替"). "判据自检" 中这几条写 "待定". 没有待定判据时写 "无". +- 不替用户选: 不按自己选的办法改写判据后写 "通过". + +## 等待操作 + +工单没有问题, 只是要用户亲手做一件事才能继续时 (例如在被调试的程序中做一次操作, 连接一台设备, 登录一个账号), 不写回执, 中途交回: + +- 交回之前把断点, 监听等准备工作做好, 把已经查清的内容写进 `artifacts/` 下的文档: 会话中断后, 新的执行子代理靠这些文档接着做. +- 交回内容第一行写 "执行状态: 等待操作", 其后逐条写要用户做的操作: 写到用户不用追问就能照做, 写明做完后要告诉你什么. +- 编排会话转来用户的回复后, 从停下的地方继续. 需要再次操作时照此再交回一次. +- 全部做完后照常写回执. 回执 "偏差记录" 的 "需要操作" 写 "无", 中途请用户做过的操作写进 "实现决定". +- 要用户做决定, 或要修改工单时, 不用这种方式, 按受阻处理. + ## 技能问题 - 问题出在技能自身时 (插件命令输出 "技能异常" 一行, 本手册与骨架或自查命令对不上, 守卫拒绝了本手册允许的写入), 不绕开, 不修改技能文件. @@ -80,7 +99,7 @@ 回执以 "# 工单回执" 开头, 第一节为 "## 当前进展". 之后各节: -- 实现类工单 (`--option 1`): 回执概况 (执行状态, 基准提交, 完成时间); 前提核对; 模块划分 (新增模块, 改动模块, 入口改动); 判据自检; 改动清单; 测试记录 (测试命令, 测试结果, 测试改动, 外部影响, 取证记录); 运行方式 (启动命令, 验证步骤); 偏差记录 (实现决定, 需要操作, 发现风险, 受阻详情) +- 实现类工单 (`--option 1`): 回执概况 (执行状态, 基准提交, 完成时间); 前提核对; 模块划分 (新增模块, 改动模块, 入口改动); 判据自检; 改动清单; 测试记录 (测试命令, 测试结果, 测试改动, 外部影响, 取证记录); 运行方式 (启动命令, 验证步骤); 偏差记录 (实现决定, 需要操作, 发现风险, 待定判据, 受阻详情) - 选型类工单 (`--option 2`): 回执概况 (执行状态, 基准提交, 完成时间); 前提核对; 现有能力; 候选方案; 需求核对 (表格, 列为 需求, 结论, 依据, "结论" 只能填 能/不能); 能力核实 (表格, 列为 能力, 仓库, 版本, 路径, 行号, 说明); 选型结论; 一致检查; 改动清单; 偏差记录 (实现决定, 需要操作, 发现风险, 受阻详情) 括号中是键名. 键值节中每个键只占一行 "- 键名: 值", 值写在同一行, 节中不加其它行, 列表, 表格或段落, 编排会话的脚本按键值行读取回执. 表格与多行内容写进没有键名的节, 完整输出放进证据文件后在值中写出路径. diff --git a/plugins/waypoint/skills/project-navigator/references/files.md b/plugins/waypoint/skills/project-navigator/references/files.md index 0b403d8..f501f1b 100644 --- a/plugins/waypoint/skills/project-navigator/references/files.md +++ b/plugins/waypoint/skills/project-navigator/references/files.md @@ -16,7 +16,7 @@ - 种类: `receipt` - 路径: `.navigator/orders/*/receipt.md` - 当前进展: 有 -- 固定节 (结构 1): 回执概况 (执行状态, 基准提交, 完成时间); 前提核对; 模块划分 (新增模块, 改动模块, 入口改动); 判据自检; 改动清单; 测试记录 (测试命令, 测试结果, 测试改动, 外部影响, 取证记录); 运行方式 (启动命令, 验证步骤); 偏差记录 (实现决定, 需要操作, 发现风险, 受阻详情) +- 固定节 (结构 1): 回执概况 (执行状态, 基准提交, 完成时间); 前提核对; 模块划分 (新增模块, 改动模块, 入口改动); 判据自检; 改动清单; 测试记录 (测试命令, 测试结果, 测试改动, 外部影响, 取证记录); 运行方式 (启动命令, 验证步骤); 偏差记录 (实现决定, 需要操作, 发现风险, 待定判据, 受阻详情) - 固定节 (结构 2): 回执概况 (执行状态, 基准提交, 完成时间); 前提核对; 现有能力; 候选方案; 需求核对 (表格, 列为 需求, 结论, 依据, "结论" 只能填 能/不能); 能力核实 (表格, 列为 能力, 仓库, 版本, 路径, 行号, 说明); 选型结论; 一致检查; 改动清单; 偏差记录 (实现决定, 需要操作, 发现风险, 受阻详情) ## 验收记录 diff --git a/plugins/waypoint/skills/project-navigator/references/order-loop.md b/plugins/waypoint/skills/project-navigator/references/order-loop.md index 4cce8d6..97288d1 100644 --- a/plugins/waypoint/skills/project-navigator/references/order-loop.md +++ b/plugins/waypoint/skills/project-navigator/references/order-loop.md @@ -28,12 +28,12 @@ ## 工单往返 -| 工单状态 | 含义 | 你的动作 | -| --------- | ---------------------- | ----------------------------------------------------------------------------- | -| drafting | 起草中 | 写工单文件, 回复 "工单审阅"; 用户选 A 后发布并派出执行子代理, 回复 "工单执行" | -| issued | 已发布, 执行子代理工作 | 等子代理结束; 读回执后进入受阻处理或验收 | -| blocked | 受阻 | 回复 "受阻处理"; 工单没有改动时直接继续, 改动后回复 "工单审阅" | -| reviewing | 验收中 | 派验收子代理, 写验收记录, 回复 "验收报告" | +| 工单状态 | 含义 | 你的动作 | +| --------- | ---------------------- | ------------------------------------------------------------------------------------------- | +| drafting | 起草中 | 写工单文件, 做发布前核对, 回复 "工单审阅"; 用户选 A 后发布并派出执行子代理, 回复 "工单执行" | +| issued | 已发布, 执行子代理工作 | 等子代理结束; 读回执后进入受阻处理或验收 | +| blocked | 受阻 | 回复 "受阻处理"; 工单没有改动时直接继续, 改动后回复 "工单审阅" | +| reviewing | 验收中 | 派验收子代理, 写验收记录, 回复 "验收报告" | 用户的决定只有两处: "工单审阅" 中发布工单, "验收报告" 中通过并提交 (或补正). 其余步骤由编排会话按下一动作直接进行. @@ -41,9 +41,10 @@ 1. 运行 `order new --kind <类型> --slug <短名> [--slice <切片编号>]`. 类型: implementation 实现, selection 选型, runcheck 运行检查, analysis 分析, fix 修复. 不带 `--slice` 时工单挂在当前切片; 切片编号是 `.navigator/plan/roadmap.md` 中的四位数字, 不是进展行中的序号. 2. 运行 `template order` 取得骨架, 按下表逐项填写后用 Write 写入. -3. 运行 `reply approve`, 回复 "工单审阅". 骨架已从工单文件的 "审阅要点" 预填, 核对后原样保留. 实现与修复工单要求项目已登记代码检查命令, 且验收判据含有代码检查判据; 没有登记时 `order new` 就会拒绝, 判据不全时 `reply approve` 拒绝, 按输出补救后再取骨架. -4. "工单审阅" 中用户选 A: 运行 `order set issued`, 再运行 `exec-brief`, 把输出原样作为提示词, 用 Agent 工具在后台 (`run_in_background`) 派 `waypoint:navigator-executor`, 然后运行 `reply running`, 回复 "工单执行". 发布只接受用户认可过的内容: 认可之后改过工单文件, `order set issued` 会拒绝. -5. "工单审阅" 中用户选 B: 按要求修改工单文件, 再次回复 "工单审阅". +3. 发布前核对: 运行 `precheck-brief`, 把输出原样作为提示词派 `waypoint:navigator-checker`. 它逐条核对工单中写下的事实. 按结果改工单: "不成立" 的改正; "无法核对" 的自己查证, 或改写成能核对的说法; 遗漏的决定写进 "需要决定". 改好后运行 `order precheck`. 每张工单核对一次, 按结果修改之后不用再核对. +4. 运行 `reply approve`, 回复 "工单审阅". 骨架已从工单文件的 "审阅要点" 预填, 核对后原样保留. 没有做发布前核对时 `reply approve` 拒绝. 实现与修复工单要求项目已登记代码检查命令, 且验收判据含有代码检查判据; 没有登记时 `order new` 就会拒绝, 判据不全时 `reply approve` 拒绝, 按输出补救后再取骨架. +5. "工单审阅" 中用户选 A: 运行 `order set issued`, 再运行 `exec-brief`, 把输出原样作为提示词, 用 Agent 工具在后台 (`run_in_background`) 派 `waypoint:navigator-executor`, 然后运行 `reply running`, 回复 "工单执行". 发布只接受用户认可过的内容: 认可之后改过工单文件, `order set issued` 会拒绝. +6. "工单审阅" 中用户选 B: 按要求修改工单文件, 再次回复 "工单审阅". | 工单中的项 | 写法 | | ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -60,6 +61,8 @@ 执行子代理工作期间不能向用户提问, 所以工单要写到不用追问就能动手: 前提假设可以核对, 判据可以验证, 需要用户决定的事在审阅时就问清. +工单中的具体事实只写查证过的: 文件路径, 接口名, 数据中的编号或实例, 引用的记录中的结论, 外部资源的发布形式与许可. 凭记忆写下的事实不成立时, 执行子代理要做到一半才发现. 判据尽量写要满足的特征 (例如 "任选一条带附件的记录"), 只在实例本身是要求时才点名实例. 发布前核对会逐条核对这些事实, 但核对子代理只报告, 改工单仍由你完成. + 只产出记录的工单: runcheck 与 analysis 的执行子代理只能写回执与本工单的 `artifacts/`, 守卫拒绝改动业务文件; 不需要代码检查命令, 任何阶段都能发布. - runcheck: 按已有命令构建, 启动, 测试, 原样记录结果, 用于盘点. @@ -73,6 +76,7 @@ - "工单执行" 中 "执行方式" 写 "后台执行子代理"; 回复之后本轮结束, 子代理结束时编排会话会收到通知. 这条回复不需要用户选择, 不带选项块. - 执行期间用户要求停止 (例如回复 "停止执行"): 停止子代理, 运行 `order set drafting` 撤回工单, 按要求修改后回复 "工单审阅". - 收到子代理结束的通知后运行 `status`. 回执未写入时用 SendMessage 请子代理按回执骨架补写. +- 子代理交回的第一行是 "执行状态: 等待操作" 时, 它没有写回执, 在等用户亲手做一件事: 回复 "等待操作" (`reply paused`), "等待原因" 写一句为什么要用户操作, "操作步骤" 照抄子代理写的操作. 这条回复不带选项块. 用户回复之后, 用 SendMessage 把用户的话原样转给子代理, 再回复 "工单执行". 工单状态不变, 不运行 `order set`. 找不到原子代理时运行 `exec-brief` 重新派出, 并在提示词之后附一句 "先读 artifacts/ 下已有的文档, 接着做". - 子代理在最终回复中交回了 `artifacts/` 下某个文档的路径与全文 (运行环境拒绝它写这个文件) 时, 按该路径原样写入, 不改内容, 再继续. ### 收回执 @@ -90,7 +94,7 @@ - 用户完成所需操作, 或回答了问题而工单内容不用改: 运行 `order set issued` (内容没有改动时不需要重新审阅), 用 SendMessage 让原执行子代理继续, 说明用户做了什么或回答了什么; 找不到原子代理时运行 `exec-brief` 重新派出. 然后回复 "工单执行". - 需要修改工单: 改好工单文件后回复 "工单审阅"; 用户选 A 后发布并重新派出执行子代理. 旧回执在发布时自动改名归档. - 用户选 "调整范围, 进入变更" 或 "转为变更, 重新评估" 时, 按 `ref change-milestone` 走需求变更. -- 执行状态为完成: 进入验收. +- 执行状态为完成: 进入验收. 回执 "偏差记录" 的 "待定判据" 不是 "无" 时照常验收, 这几条判据由用户在 "验收报告" 中决定. ### 验收 @@ -100,7 +104,7 @@ 4. 回复 "验收报告", 按判据结论选择选项组 (`reply review --option <组号>` 会核对). 建议只写在 "处理建议" 一节: "建议选项" 写选项字母, "主要依据" 写一句话; 选项块之前不另加说明: - 全部通过: 第 1 组. 用户选 A 或 B: 在 `review.md` 写 "用户结论", 运行 `order set accepted`, 按输出调用 `waypoint:commit-message` 提交 (B 同时推送). 用户选 C: 把用户描述的现象写进 `review.md`, 运行 `order set rejected`, 按输出用第 2 组再回复 "验收报告". - 有判据不通过: 运行 `order set rejected`, 按输出使用第 2 组. - - 没有不通过, 但有未验证的判据: 第 3 组. 用户选 A (接受现状): 在 `review.md` 写 "用户结论", 运行 `order set accepted --accept-unverified`, 按输出提交; 未验证的判据用 `risk add` 各登记一条风险. 用户选 B: 运行 `order set rejected`, 按输出新建补正工单. + - 没有不通过, 但有未验证的判据: 第 3 组. 其中有回执列为待定的判据时 (证据以 "待定:" 开头), "处理建议" 的 "主要依据" 写明每条待定判据的实际情况与可选的处理办法: 用户选 A 表示按现状接受, 选 B 表示发补正工单. 用户选 A (接受现状): 在 `review.md` 写 "用户结论", 运行 `order set accepted --accept-unverified`, 按输出提交; 未验证的判据用 `risk add` 各登记一条风险. 用户选 B: 运行 `order set rejected`, 按输出新建补正工单. - 选型工单: 第 4 组, 见 `ref selection`. 5. 第 2 组用户选 A: 按 `order set rejected` 输出的命令新建补正工单, 判据写明要补正的问题. 实现与修复工单补正时先判断原因: 工单太大就拆小, 否则发修复工单 (`--kind fix`); 运行检查与分析工单补正时重发同类工单. diff --git a/plugins/waypoint/skills/project-navigator/references/replies.md b/plugins/waypoint/skills/project-navigator/references/replies.md index 09c5ec8..794c104 100644 --- a/plugins/waypoint/skills/project-navigator/references/replies.md +++ b/plugins/waypoint/skills/project-navigator/references/replies.md @@ -89,6 +89,12 @@ - 各节: 等待情况 (等待对象, 任务内容, 后续动作) - 选项: 无, 不需要用户选择, 回复不带选项块. +## 等待操作 + +- 编号: `paused` +- 各节: 操作内容 (等待原因, 完成之后 [骨架预填: 回复 "操作完成", 并写明实际做了什么; 不做时回复 "停止执行"]); 操作步骤 +- 选项: 无, 不需要用户选择, 回复不带选项块. + ## 验收报告 - 编号: `review` diff --git a/plugins/waypoint/skills/project-navigator/runtime/commands/brief.mjs b/plugins/waypoint/skills/project-navigator/runtime/commands/brief.mjs index 4214dba..2507f98 100644 --- a/plugins/waypoint/skills/project-navigator/runtime/commands/brief.mjs +++ b/plugins/waypoint/skills/project-navigator/runtime/commands/brief.mjs @@ -1,5 +1,6 @@ /** - * @file exec-brief, review-brief, research-brief 命令: 输出派子代理时使用的固定提示词. + * @file precheck-brief, exec-brief, review-brief, research-brief 命令: 输出派子代理 + * 时使用的固定提示词. * * 提示词由脚本生成, 编排会话原样使用, 不在其中加入倾向性说明. */ @@ -7,19 +8,25 @@ import { RESEARCH_FRAME, buildExecutorBrief, + buildPrecheckBrief, buildReviewBrief, } from "../lib/briefs.mjs"; import { orderCheckCommands } from "../lib/code-checks.mjs"; +import { readOrderText } from "../lib/order-approval.mjs"; +import { orderFilePath } from "../lib/paths.mjs"; import { criteriaTableSpec } from "../lib/review-record.mjs"; import { WorkflowError } from "../lib/workflow-error.mjs"; -import { ISSUED_ORDER_STATUS } from "../lib/workflow-orders.mjs"; +import { + ISSUED_ORDER_STATUS, + REVIEWABLE_ORDER_STATUSES, +} from "../lib/workflow-orders.mjs"; import { openProject } from "./support.mjs"; /** * 执行提示词命令. * * @param {object} options 命令参数. - * @param {"exec-brief" | "review-brief" | "research-brief"} options.command 命令名. + * @param {"precheck-brief" | "exec-brief" | "review-brief" | "research-brief"} options.command 命令名. * @param {string} options.cwd 会话工作目录. * @returns {string[]} 输出各行. * @throws {WorkflowError} 没有当前工单, 工单不在需要的状态, 或命令未知时. @@ -34,6 +41,12 @@ export function runBrief({ command, cwd }) { throw new WorkflowError("当前没有工单."); } switch (command) { + case "precheck-brief": + assertPrecheckable(context.projectRoot, order); + return buildPrecheckBrief({ + projectRoot: context.projectRoot, + order, + }).split("\n"); case "exec-brief": if (order.status !== ISSUED_ORDER_STATUS) { throw new WorkflowError( @@ -55,3 +68,24 @@ export function runBrief({ command, cwd }) { throw new WorkflowError(`未知命令 ${command}.`); } } + +/** + * 核对当前工单可以做发布前核对: 还没有发布, 工单文件已写好. + * + * @param {string} projectRoot 项目根目录. + * @param {import("../lib/state.mjs").OrderState} order 当前工单. + * @returns {void} + * @throws {WorkflowError} 工单已发布, 或工单文件还没有写入时. + */ +function assertPrecheckable(projectRoot, order) { + if (!REVIEWABLE_ORDER_STATUSES.includes(order.status)) { + throw new WorkflowError( + `工单 ${order.id} 处于 ${order.status}, 发布前核对只用于 ${REVIEWABLE_ORDER_STATUSES.join(", ")} 状态的工单.`, + ); + } + if (readOrderText(projectRoot, order) === undefined) { + throw new WorkflowError( + `工单文件 ${orderFilePath(order.folder, "order")} 还没有写入.`, + ); + } +} diff --git a/plugins/waypoint/skills/project-navigator/runtime/commands/enter.mjs b/plugins/waypoint/skills/project-navigator/runtime/commands/enter.mjs index 9d45f3f..4584d54 100644 --- a/plugins/waypoint/skills/project-navigator/runtime/commands/enter.mjs +++ b/plugins/waypoint/skills/project-navigator/runtime/commands/enter.mjs @@ -55,6 +55,7 @@ import { summarizeIgnoredPaths, } from "../lib/tracked-paths.mjs"; import { checkStateConsistency } from "../lib/state-checks.mjs"; +import { describeModels } from "../lib/subagent-models.mjs"; import { ISSUED_ORDER_STATUS } from "../lib/workflow-orders.mjs"; import { isProjectFinished } from "../lib/workflow-plan.mjs"; @@ -207,6 +208,7 @@ function describeProgress({ projectRoot, state, isEntry, spec, now }) { `- 工单状态: ${order.id} ${order.status}, 回执${describeReceipt(receiptStatus)}`, ]), `- 工作区域: ${uncommitted.length === 0 ? "技能产物都已入库" : `${uncommitted.length} 个技能产物未入库`}`, + `- 子代理模型: ${describeModels(state.models)}`, ...(pending.length === 0 ? [] : [ diff --git a/plugins/waypoint/skills/project-navigator/runtime/commands/handlers.mjs b/plugins/waypoint/skills/project-navigator/runtime/commands/handlers.mjs index b36f142..4a12f37 100644 --- a/plugins/waypoint/skills/project-navigator/runtime/commands/handlers.mjs +++ b/plugins/waypoint/skills/project-navigator/runtime/commands/handlers.mjs @@ -8,6 +8,7 @@ import { runCodeCheck } from "./codecheck.mjs"; import { runEnter } from "./enter.mjs"; import { runEvidence } from "./evidence.mjs"; import { runInit } from "./init.mjs"; +import { runModel } from "./model.mjs"; import { runOrder } from "./order.mjs"; import { runPlan } from "./plan.mjs"; import { runRecords } from "./records.mjs"; @@ -56,6 +57,7 @@ export const COMMAND_HANDLERS = Object.freeze({ risk: runRecords, decision: runRecords, change: runRecords, + "precheck-brief": runBrief, "exec-brief": runBrief, "review-brief": runBrief, "research-brief": runBrief, @@ -63,5 +65,6 @@ export const COMMAND_HANDLERS = Object.freeze({ check: runCheck, standards: runStandards, codecheck: runCodeCheck, + model: runModel, report: runReport, }); diff --git a/plugins/waypoint/skills/project-navigator/runtime/commands/model.mjs b/plugins/waypoint/skills/project-navigator/runtime/commands/model.mjs new file mode 100644 index 0000000..b66df0a --- /dev/null +++ b/plugins/waypoint/skills/project-navigator/runtime/commands/model.mjs @@ -0,0 +1,54 @@ +/** + * @file model 命令: 查看与登记各类子代理使用的模型. + * + * 用法: + * - model: 查看当前分配. + * - model set <角色> <模型> [--kind <工单类型>]: 登记; `--kind` 只用于执行子代理, + * 按工单类型单独指定. + */ + +import { + MODEL_CHOICES, + MODEL_ROLES, + assignModel, + describeModels, +} from "../lib/subagent-models.mjs"; +import { WorkflowError } from "../lib/workflow-error.mjs"; +import { openProject, resultLines, saveState } from "./support.mjs"; + +/** + * 执行 model 命令. + * + * @param {object} options 命令参数. + * @param {string[]} options.positionals 命令名之后的位置参数. + * @param {Record} options.values 选项值. + * @param {string} options.cwd 会话工作目录. + * @param {string} options.now 当前时间, ISO 格式. + * @returns {string[]} 输出各行. + * @throws {WorkflowError} 用法不对, 或角色, 模型, 工单类型不合法时. + */ +export function runModel({ positionals, values, cwd, now }) { + const context = openProject(cwd); + const [action, role, model] = positionals; + if (action === undefined) { + return [`- 子代理模型: ${describeModels(context.state.models)}`]; + } + if (action !== "set" || role === undefined || model === undefined) { + throw new WorkflowError( + `model 的用法: model; model set <角色> <模型> [--kind <工单类型>]. 角色: ${Object.values(MODEL_ROLES).join(", ")}; 模型: ${MODEL_CHOICES.join(", ")}.`, + ); + } + const state = saveState( + context, + assignModel(context.state, { + role, + model, + kind: typeof values.kind === "string" ? values.kind : undefined, + }), + now, + ); + return [ + ...resultLines(state), + `- 子代理模型: ${describeModels(state.models)}`, + ]; +} diff --git a/plugins/waypoint/skills/project-navigator/runtime/commands/order.mjs b/plugins/waypoint/skills/project-navigator/runtime/commands/order.mjs index 520c5b9..14acec7 100644 --- a/plugins/waypoint/skills/project-navigator/runtime/commands/order.mjs +++ b/plugins/waypoint/skills/project-navigator/runtime/commands/order.mjs @@ -4,17 +4,20 @@ * 用法: * - order new --kind <类型> --slug <短名> [--slice <切片编号>] * - order set <状态> [--accept-unverified] + * - order precheck: 记下当前工单已做过发布前核对 */ import { existsSync, renameSync } from "node:fs"; import path from "node:path"; import { creationBlocker, issueBlocker } from "../lib/code-checks.mjs"; -import { COMMIT_SKILL, SUBAGENT_TYPES } from "../lib/guard.mjs"; +import { COMMIT_SKILL } from "../lib/guard.mjs"; import { REVIEW_OPTIONS, REVIEW_REPLY_TYPE, RUNNING_REPLY_TYPE, + executorDispatchAction, + precheckStep, replyStep, } from "../lib/guidance.mjs"; import { formatNumber } from "../lib/numbering.mjs"; @@ -41,6 +44,8 @@ import { consecutiveRejections, createOrder, isRecordOnlyOrderKind, + markPrechecked, + precheckBlocker, setOrderStatus, } from "../lib/workflow-orders.mjs"; import { openProject, resultLines, saveState } from "./support.mjs"; @@ -51,12 +56,6 @@ import { openProject, resultLines, saveState } from "./support.mjs"; */ const RESPLIT_THRESHOLD = 2; -/** - * 派出执行子代理并回复 "工单执行" 的一串动作. - * @type {string} - */ -const DISPATCH_ACTION = `运行 exec-brief, 把输出原样作为提示词, 用 Agent 工具在后台 (run_in_background) 派 ${SUBAGENT_TYPES.executor}`; - /** * 提交范围的写法, 验收通过后的提交使用. * @type {string} @@ -83,11 +82,29 @@ export function runOrder({ positionals, values, cwd, now }) { return createNewOrder(context, values, now); case "set": return changeStatus(context, argument, values, now); + case "precheck": + return recordPrecheck(context, now); default: - throw new WorkflowError("order 的用法: new, set <状态>."); + throw new WorkflowError("order 的用法: new, set <状态>, precheck."); } } +/** + * 记下当前工单已做过发布前核对, 之后才能请用户审阅. + * + * @param {import("./support.mjs").ProjectContext} context 项目上下文. + * @param {string} now 当前时间. + * @returns {string[]} 输出各行. + * @throws {WorkflowError} 没有当前工单, 或工单已经发布时. + */ +function recordPrecheck(context, now) { + const state = saveState(context, markPrechecked(context.state), now); + return [ + ...resultLines(state), + `- 下一动作: ${replyStep(context.spec, APPROVAL_REPLY_TYPE)}; 用户选 A 后运行 order set issued`, + ]; +} + /** * 新建工单并输出工单文件的路径. 会改动代码的工单要求项目已登记检查命令. * @@ -119,7 +136,7 @@ function createNewOrder(context, values, now) { ); return [ ...resultLines(state), - `- 下一动作: 运行 template order 取得骨架, 用 Write 写入 ${orderFile}, 再${replyStep(context.spec, APPROVAL_REPLY_TYPE)}; 用户选 A 后运行 order set issued`, + `- 下一动作: 运行 template order 取得骨架, 用 Write 写入 ${orderFile}, 再${precheckStep(context.spec, state.models)}, 再${replyStep(context.spec, APPROVAL_REPLY_TYPE)}; 用户选 A 后运行 order set issued`, ]; } @@ -185,17 +202,19 @@ function issueOrder(context, now) { ); consumeApproval(context.projectRoot); const running = replyStep(context.spec, RUNNING_REPLY_TYPE); + const dispatch = executorDispatchAction(state.models, order); return [ ...resultLines(state), ...(archived === undefined ? [] : [`- 旧回执已归档: ${archived}`]), isContinuing - ? `- 下一动作: 用 SendMessage 让原执行子代理继续 (说明用户已完成的操作); 找不到它时${DISPATCH_ACTION}; 然后${running}` - : `- 下一动作: ${DISPATCH_ACTION}, 然后${running}`, + ? `- 下一动作: 用 SendMessage 让原执行子代理继续 (说明用户已完成的操作); 找不到它时${dispatch}; 然后${running}` + : `- 下一动作: ${dispatch}, 然后${running}`, ]; } /** - * 核对当前工单能否发布: 会改动代码的工单必须带代码检查判据; 用户必须已在 + * 核对当前工单能否发布: 会改动代码的工单必须带代码检查判据; 起草中的工单 + * 必须做过发布前核对; 用户必须已在 * "工单审阅" 中认可当前的工单内容, 且工单文件中的基准提交是当前提交; 或工单 * 受阻后内容与上次发布时相同. * @@ -211,8 +230,9 @@ function assertIssuable(context) { context.state, readOrderText(context.projectRoot, order), ); - if (blocker !== undefined) { - throw new WorkflowError(blocker); + const unchecked = blocker ?? precheckBlocker(order); + if (unchecked !== undefined) { + throw new WorkflowError(unchecked); } const approval = readApprovalStatus(context.projectRoot, order); if (approval === APPROVAL_STATUSES.approved) { diff --git a/plugins/waypoint/skills/project-navigator/runtime/commands/reply.mjs b/plugins/waypoint/skills/project-navigator/runtime/commands/reply.mjs index 8159e56..b1dbc8e 100644 --- a/plugins/waypoint/skills/project-navigator/runtime/commands/reply.mjs +++ b/plugins/waypoint/skills/project-navigator/runtime/commands/reply.mjs @@ -20,7 +20,11 @@ import { readOrderVerdicts } from "../lib/review-record.mjs"; import { loadSpec, resolveReplyType } from "../lib/spec.mjs"; import { readStateIfValid } from "../lib/state.mjs"; import { renderTable } from "../lib/table.mjs"; -import { SELECTION_ORDER_KIND } from "../lib/workflow-orders.mjs"; +import { + REVIEWABLE_ORDER_STATUSES, + SELECTION_ORDER_KIND, + precheckBlocker, +} from "../lib/workflow-orders.mjs"; /** * 工单文件中写给人看的节, 与规格 files.order 中的节标题一致. @@ -40,12 +44,6 @@ const ORDER_PATH_KEY = "工单文件"; */ const REVIEWING_STATUS = "reviewing"; -/** - * 可以请用户审阅的工单状态: 起草中, 以及受阻后修改待重新发布. - * @type {readonly string[]} - */ -const REVIEWABLE_STATUSES = Object.freeze(["drafting", "blocked"]); - /** * 执行 reply 命令. * @@ -100,7 +98,8 @@ export function runReply({ cwd, type, optionSet }) { /** * 为 "工单审阅" 取出预填的值: 工单文件 "审阅要点" 中的各键, 以及工单文件路径. - * 工单要处于可审阅的状态, 工单文件已写好, 且内容已满足发布条件 (含基准提交是当前提交). + * 工单要处于可审阅的状态, 起草中的工单已做过发布前核对, 工单文件已写好, 且内容 + * 已满足发布条件 (含基准提交是当前提交). * * @param {string | undefined} projectRoot 项目根目录. * @param {import("../lib/state.mjs").NavigatorState | undefined} state 当前状态. @@ -113,12 +112,16 @@ function approvalValues(projectRoot, state) { projectRoot === undefined || state === undefined || order === null || - !REVIEWABLE_STATUSES.includes(order.status) + !REVIEWABLE_ORDER_STATUSES.includes(order.status) ) { throw new Error( - `reply: 当前没有待审阅的工单; 只有 ${REVIEWABLE_STATUSES.join(", ")} 状态的工单需要审阅.`, + `reply: 当前没有待审阅的工单; 只有 ${REVIEWABLE_ORDER_STATUSES.join(", ")} 状态的工单需要审阅.`, ); } + const unchecked = precheckBlocker(order); + if (unchecked !== undefined) { + throw new Error(`reply: ${unchecked}`); + } const orderText = readOrderText(projectRoot, order); const orderPath = orderFilePath(order.folder, "order"); if (orderText === undefined) { diff --git a/plugins/waypoint/skills/project-navigator/runtime/hook.mjs b/plugins/waypoint/skills/project-navigator/runtime/hook.mjs index 0cd3341..dca34d8 100644 --- a/plugins/waypoint/skills/project-navigator/runtime/hook.mjs +++ b/plugins/waypoint/skills/project-navigator/runtime/hook.mjs @@ -43,6 +43,7 @@ import { import { checkReply, locateReply } from "./lib/reply-checks.mjs"; import { loadSpec } from "./lib/spec.mjs"; import { INIT_UNINSTALLED, readStateIfValid } from "./lib/state.mjs"; +import { REVIEWABLE_ORDER_STATUSES } from "./lib/workflow-orders.mjs"; /** * 回复结构不合格时, 打回原因的开头. 只让模型改列出的问题: 重写整条回复 @@ -64,12 +65,6 @@ const COMPACT_SOURCE = "compact"; */ const UNKNOWN_EVENT = "未知事件"; -/** - * 可以请用户审阅的工单状态: 起草中, 以及受阻后修改待重新发布. - * @type {readonly string[]} - */ -const REVIEWABLE_STATUSES = Object.freeze(["drafting", "blocked"]); - /** * @typedef {object} HookEvent 一次 hook 调用的上下文. * @property {Record} input hook 输入. @@ -239,7 +234,8 @@ function handleSessionStart({ input, state, isOrchestrator }) { * 或本轮不校验时, 开始等待用户对工单审阅的选择. * * `stop_hook_active` 为 true 表示本轮已被打回过, 此时不再校验, 避免循环. - * 有后台任务时照常校验: 等待子代理的回复有自己的类型 ("工单执行", "工单验收", "等待结果"). + * 有后台任务时照常校验: 等待子代理或用户操作的回复有自己的类型 ("工单执行", + * "工单验收", "等待结果", "等待操作"). * * @param {HookEvent} event hook 调用上下文. * @returns {Record | undefined} 要输出的 JSON. @@ -268,7 +264,7 @@ function handleStop({ input, projectRoot, state, isOrchestrator, now }) { if ( locateReply(text).heading?.text === APPROVAL_REPLY_TYPE && state.order !== null && - REVIEWABLE_STATUSES.includes(state.order.status) + REVIEWABLE_ORDER_STATUSES.includes(state.order.status) ) { markAwaitingApproval(projectRoot, state.order, now); } diff --git a/plugins/waypoint/skills/project-navigator/runtime/lib/briefs.mjs b/plugins/waypoint/skills/project-navigator/runtime/lib/briefs.mjs index acdfaf4..3dfa0a8 100644 --- a/plugins/waypoint/skills/project-navigator/runtime/lib/briefs.mjs +++ b/plugins/waypoint/skills/project-navigator/runtime/lib/briefs.mjs @@ -1,5 +1,5 @@ /** - * @file 由脚本生成的委派提示词: 执行子代理, 验收子代理与调研子代理. + * @file 由脚本生成的委派提示词: 核对子代理, 执行子代理, 验收子代理与调研子代理. * * 提示词由脚本生成, 编排会话原样使用, 不在其中加入倾向性说明或删掉检查项. * 这里是这些提示词的唯一来源. @@ -37,6 +37,18 @@ const RESEARCH_CALL_LIMIT = 20; */ const EXECUTOR_SUMMARY_LINES = 3; +/** + * 执行子代理中途交回, 等用户完成操作时, 交回内容第一行的写法. + * @type {string} + */ +export const PAUSED_HANDBACK = "执行状态: 等待操作"; + +/** + * 回执 "偏差记录" 中列出待定判据的键, 与规格 files.receipt 一致. + * @type {string} + */ +export const PENDING_CRITERIA_KEY = "待定判据"; + /** * 问题域调研提示词的固定框架; 编排会话只在其后填写调研问题. * @type {string} @@ -62,6 +74,52 @@ export const RESEARCH_FRAME = [ "", ].join("\n"); +/** + * 发布前核对的三种结论. + * @type {Readonly<{holds: string, fails: string, unknown: string}>} + */ +export const PRECHECK_VERDICTS = Object.freeze({ + holds: "成立", + fails: "不成立", + unknown: "无法核对", +}); + +/** + * 生成发布前核对的委派提示词: 工单发布之前, 由核对子代理逐条核对工单中写下的 + * 事实. 工单由编排会话凭已有记录写成, 其中的具体事实不成立时, 执行子代理要做到 + * 一半才发现, 整张工单往返一轮. + * + * @param {object} options 生成参数. + * @param {string} options.projectRoot 项目根目录. + * @param {{id: string, folder: string}} options.order 当前工单. + * @returns {string} 提示词全文. + */ +export function buildPrecheckBrief({ projectRoot, order }) { + const { holds, fails, unknown } = PRECHECK_VERDICTS; + return [ + `# 发布前核对 ${order.id}`, + "", + "你是核对子代理: 只读, 不写任何文件, 不改写工单, 只报告事实与证据.", + "", + "## 核对对象", + "", + `- 工单文件: ${path.join(projectRoot, orderFilePath(order.folder, "order"))}`, + "", + "## 核对要求", + "", + '1. 前提假设: 逐条核对工单 "前提假设", 读代码, 配置与数据核实.', + '2. 具体事实: 找出 "执行守则", "工作范围" 与 "验收判据" 中的每个具体事实, 逐个核对: 文件与目录路径, 函数与接口名, 命令, 数据中的编号或实例, 引用的记录中的结论, 外部资源的地址, 发布形式与许可. 判据点名某个实例时, 核对这个实例存在, 并且带有判据要求的特征.', + '3. 判据可验证: 逐条看 "验证方式" 能否照做: 命令是否存在, 需要的数据, 工具与环境是否具备.', + '4. 遗漏的决定: 列出执行中必须由用户决定, 而工单 "需要决定" 没有写的事, 例如许可条款的解释, 要改动仓库之外的文件, 几种做法的取舍.', + `5. 每条给出结论与证据 (文件路径与行号, 命令输出, 或网页地址与摘录). 结论只能是 ${holds}, ${fails}, ${unknown} 之一; ${unknown} 时写明缺少什么. ${holds} 的条目可以合并成一行, 只写条数.`, + "", + "## 交回格式", + "", + `1. 核对结果: 表格, 列为 位置, 内容, 结论, 证据; 只列 ${fails} 与 ${unknown} 的条目, 最后一行写 ${holds} 的条数.`, + '2. 遗漏的决定: 逐条列出, 没有时写 "无".', + ].join("\n"); +} + /** * 生成执行子代理的委派提示词: 手册与工单的位置, 改动范围, 交回格式. * 细节都在执行手册与工单中. 只产出记录的工单 (运行检查与分析) 只写回执与证据文件. @@ -89,7 +147,8 @@ export function buildExecutorBrief({ projectRoot, order }) { "## 交回格式", "", `- 第一行: 执行状态 (完成或受阻) 与回执路径.`, - `- 其后不超过 ${EXECUTOR_SUMMARY_LINES} 行要点: 受阻时写受阻类型与需要的决定或操作; 完成时写最重要的实现决定与风险.`, + `- 其后不超过 ${EXECUTOR_SUMMARY_LINES} 行要点: 受阻时写受阻类型与需要的决定或操作; 完成时写最重要的实现决定与风险, 有待定判据时写明是哪几条.`, + `- 例外: 按手册 "等待操作" 一节中途交回时不写回执, 第一行写 "${PAUSED_HANDBACK}", 其后逐条写要用户做的操作.`, ].join("\n"); } @@ -143,7 +202,7 @@ export function buildReviewBrief({ "", "## 核对要求", "", - `1. 逐条对照工单的 "验收判据", 每条给出结论与证据 (文件路径与行号, 或命令输出). 结论只能是 ${verdicts} 之一, 不用其它说法; 无法确认的写 ${unverified}, 并写明缺少什么.`, + `1. 逐条对照工单的 "验收判据", 每条给出结论与证据 (文件路径与行号, 或命令输出). 结论只能是 ${verdicts} 之一, 不用其它说法; 无法确认的写 ${unverified}, 并写明缺少什么. 回执 "偏差记录" 的 "${PENDING_CRITERIA_KEY}" 列出的判据, 核对回执给出的原因是否属实: 属实时结论写 ${unverified}, 证据写 "待定: " 加上原因与你核对到的事实; 不属实时按实际情况下结论.`, `2. 回执一致: 用 git diff ${order.baseCommit} --stat 与回执的 "改动清单" 比对, 列出不一致之处.`, wiringCheckItem(order.kind), '4. 范围核查: 列出工单 "工作范围" 之外的改动.', diff --git a/plugins/waypoint/skills/project-navigator/runtime/lib/guard.mjs b/plugins/waypoint/skills/project-navigator/runtime/lib/guard.mjs index ccaddd4..256efa1 100644 --- a/plugins/waypoint/skills/project-navigator/runtime/lib/guard.mjs +++ b/plugins/waypoint/skills/project-navigator/runtime/lib/guard.mjs @@ -2,7 +2,7 @@ * @file 写入守卫: 只看写入类工具的目标路径, 四条规则. * * 1. 任何会话与子代理都不能直接写状态文件与脚本生成的视图文件. - * 2. 验收子代理不能写任何文件. + * 2. 核对与验收子代理不能写任何文件. * 3. 执行子代理在 `.navigator/` 下只能写当前工单的回执与证据文件; 只产出记录的 * 工单 (运行检查与分析) 不能写业务文件. * 4. 编排会话不能写 `.navigator/` 之外的文件. @@ -38,15 +38,25 @@ export const WRITE_TOOLS = Object.freeze([ /** * 插件自带的子代理类型. - * @type {Readonly<{reader: string, researcher: string, reviewer: string, executor: string}>} + * @type {Readonly<{reader: string, researcher: string, checker: string, reviewer: string, executor: string}>} */ export const SUBAGENT_TYPES = Object.freeze({ reader: "waypoint:navigator-reader", researcher: "waypoint:navigator-researcher", + checker: "waypoint:navigator-checker", reviewer: "waypoint:navigator-reviewer", executor: "waypoint:navigator-executor", }); +/** + * 只读的子代理类型: 可以运行命令取证, 不能写任何文件. + * @type {readonly string[]} + */ +const READ_ONLY_SUBAGENT_TYPES = Object.freeze([ + SUBAGENT_TYPES.checker, + SUBAGENT_TYPES.reviewer, +]); + /** * 生成提交消息并提交的技能. * @type {string} @@ -61,10 +71,11 @@ const GENERATED_REASON = "这个文件由插件命令生成: 状态只用插件命令修改, 推进路线与风险清单随状态自动更新."; /** - * 验收子代理写文件时的拒绝理由. + * 核对与验收子代理写文件时的拒绝理由. * @type {string} */ -const REVIEWER_REASON = "验收子代理只读, 不写任何文件; 只报告事实与证据."; +const READ_ONLY_REASON = + "核对与验收子代理只读, 不写任何文件; 只报告事实与证据."; /** * 执行子代理写本工单回执与证据文件之外的编排记录时的拒绝理由. @@ -128,8 +139,8 @@ export function decideWrite(request) { if (GENERATED_FILES.some((file) => relativePathsEqual(relativePath, file))) { return deny(GENERATED_REASON); } - if (request.agentType === SUBAGENT_TYPES.reviewer) { - return deny(REVIEWER_REASON); + if (READ_ONLY_SUBAGENT_TYPES.includes(request.agentType ?? "")) { + return deny(READ_ONLY_REASON); } if (request.agentType === SUBAGENT_TYPES.executor) { return decideExecutorWrite(relativePath, request.order); diff --git a/plugins/waypoint/skills/project-navigator/runtime/lib/guidance.mjs b/plugins/waypoint/skills/project-navigator/runtime/lib/guidance.mjs index c80904c..dd1c7c4 100644 --- a/plugins/waypoint/skills/project-navigator/runtime/lib/guidance.mjs +++ b/plugins/waypoint/skills/project-navigator/runtime/lib/guidance.mjs @@ -5,9 +5,11 @@ * 运行哪条命令. "运行 X" 指运行插件命令 `project-navigator X`. */ +import { PAUSED_HANDBACK, PRECHECK_VERDICTS } from "./briefs.mjs"; import { COMMIT_SKILL, SUBAGENT_TYPES } from "./guard.mjs"; import { APPROVAL_REPLY_TYPE, APPROVAL_STATUSES } from "./order-approval.mjs"; import { lastStageNumber } from "./spec.mjs"; +import { MODEL_ROLES, modelArgument } from "./subagent-models.mjs"; import { SELECTION_ORDER_KIND } from "./workflow-orders.mjs"; import { isProjectFinished } from "./workflow-plan.mjs"; @@ -71,19 +73,60 @@ export const COMPLETE_REPLY_TYPE = "项目完成"; export const BLOCKED_RECEIPT_KEYWORD = "受阻"; /** - * 发布并派出执行子代理的一串动作. + * 等待调研, 阅读与核对子代理期间的回复类型. + * @type {string} + */ +const WAITING_REPLY_TYPE = "等待结果"; + +/** + * 执行子代理中途交回, 等用户完成操作期间的回复类型. * @type {string} */ -const DISPATCH_STEP = `运行 order set issued, 再运行 exec-brief, 把输出原样作为提示词, 用 Agent 工具在后台 (run_in_background) 派 ${SUBAGENT_TYPES.executor}, 然后`; +const PAUSED_REPLY_TYPE = "等待操作"; + +/** + * 派出执行子代理的动作, 写明这种工单使用的模型. + * + * @param {Record | undefined} models 状态中登记的模型分配. + * @param {{kind: string}} order 当前工单. + * @returns {string} 动作. + */ +export function executorDispatchAction(models, order) { + return `运行 exec-brief, 把输出原样作为提示词, 用 Agent 工具在后台 (run_in_background) 派 ${SUBAGENT_TYPES.executor} (${modelArgument(models, MODEL_ROLES.executor, order.kind)})`; +} + +/** + * 发布并派出执行子代理的一串动作. + * + * @param {Record | undefined} models 状态中登记的模型分配. + * @param {{kind: string}} order 当前工单. + * @returns {string} 动作. + */ +function dispatchStep(models, order) { + return `运行 order set issued, 再${executorDispatchAction(models, order)}, 然后`; +} + +/** + * 派出核对子代理并按结果改好工单的一串动作; 子代理在后台运行, 本轮要先结束时 + * 回复 "等待结果". + * + * @param {import("./spec.mjs").TemplateSpec} spec 模板规格. + * @param {Record | undefined} models 状态中登记的模型分配. + * @returns {string} 动作. + */ +export function precheckStep(spec, models) { + return `运行 precheck-brief, 把输出原样作为提示词派 ${SUBAGENT_TYPES.checker} (${modelArgument(models, MODEL_ROLES.checker)}); 子代理在后台运行, 本轮要先结束时${replyStep(spec, WAITING_REPLY_TYPE)}; 结果回来后改正 "${PRECHECK_VERDICTS.fails}" 的事实, "${PRECHECK_VERDICTS.unknown}" 的条目自己查证或改写成能核对的说法, 遗漏的决定写进 "需要决定", 然后运行 order precheck`; +} /** * 派出验收子代理的一串动作; 子代理在后台运行, 本轮要先结束时回复 "工单验收". * * @param {import("./spec.mjs").TemplateSpec} spec 模板规格. + * @param {Record | undefined} models 状态中登记的模型分配. * @returns {string} 动作. */ -function reviewDispatchStep(spec) { - return `运行 review-brief, 把输出原样作为提示词派 ${SUBAGENT_TYPES.reviewer}; 子代理在后台运行, 本轮要先结束时${replyStep(spec, REVIEWING_REPLY_TYPE)}, 不写其它说明`; +function reviewDispatchStep(spec, models) { + return `运行 review-brief, 把输出原样作为提示词派 ${SUBAGENT_TYPES.reviewer} (${modelArgument(models, MODEL_ROLES.reviewer)}); 子代理在后台运行, 本轮要先结束时${replyStep(spec, REVIEWING_REPLY_TYPE)}, 不写其它说明`; } /** @@ -156,68 +199,77 @@ function finishedAction(spec, hasUncommittedRecords) { * @returns {string} 下一动作. */ function orderAction(spec, order, input) { + const models = input.state.models; switch (order.status) { case "drafting": - return draftingAction(spec, order, input.orderApproval); + return draftingAction(spec, order, input); case "issued": - return issuedAction(spec, order, input.receiptStatus); + return issuedAction(spec, order, input); case "blocked": return blockedAction(spec, order, input); case "reviewing": - return reviewingAction(spec, order); + return reviewingAction(spec, order, models); default: return `工单 ${order.id} 处于未知状态 ${order.status}`; } } /** - * 起草中的工单的下一动作: 先请用户审阅, 用户认可当前内容后发布并派出执行子代理. + * 起草中的工单的下一动作: 先做发布前核对, 再请用户审阅, 用户认可当前内容后 + * 发布并派出执行子代理. * * @param {import("./spec.mjs").TemplateSpec} spec 模板规格. - * @param {{id: string}} order 当前工单. - * @param {string | undefined} orderApproval 工单的审阅状态. + * @param {import("./state.mjs").OrderState} order 当前工单. + * @param {GuidanceInput} input 判断信息. * @returns {string} 下一动作. */ -function draftingAction(spec, order, orderApproval) { - const dispatch = `${DISPATCH_STEP}${replyStep(spec, RUNNING_REPLY_TYPE)}`; - switch (orderApproval) { +function draftingAction(spec, order, input) { + const models = input.state.models; + const dispatch = `${dispatchStep(models, order)}${replyStep(spec, RUNNING_REPLY_TYPE)}`; + const approve = `${replyStep(spec, APPROVAL_REPLY_TYPE)}; 用户选 A 后${dispatch}`; + if (order.prechecked !== true) { + return `写完工单 ${order.id} 的 order.md, ${precheckStep(spec, models)}, 再${approve}`; + } + switch (input.orderApproval) { case APPROVAL_STATUSES.approved: return `工单 ${order.id} 已经用户审阅: ${dispatch}`; case APPROVAL_STATUSES.awaiting: return `等用户在 "${APPROVAL_REPLY_TYPE}" 中选择: 选 A 后${dispatch}; 用户要修改时, 改好工单文件后再${replyStep(spec, APPROVAL_REPLY_TYPE)}`; default: - return `写完工单 ${order.id} 的 order.md, ${replyStep(spec, APPROVAL_REPLY_TYPE)}; 用户选 A 后${dispatch}`; + return `工单 ${order.id} 已做发布前核对: ${approve}`; } } /** * 已发布工单的下一动作: 回执写入前等执行子代理, 写入后按执行状态处理. + * 执行子代理也可能不写回执, 中途交回等用户操作. * * @param {import("./spec.mjs").TemplateSpec} spec 模板规格. * @param {{id: string}} order 当前工单. - * @param {string | undefined} receiptStatus 回执中的执行状态. + * @param {GuidanceInput} input 判断信息. * @returns {string} 下一动作. */ -function issuedAction(spec, order, receiptStatus) { +function issuedAction(spec, order, input) { + const { receiptStatus } = input; if (receiptStatus === undefined) { - return `工单 ${order.id} 由执行子代理处理中: ${replyStep(spec, RUNNING_REPLY_TYPE)}; 子代理结束后会自动通知, 届时运行 status. 子代理已结束但回执没有写入时, 用 SendMessage 请它按回执骨架补写`; + return `工单 ${order.id} 由执行子代理处理中: ${replyStep(spec, RUNNING_REPLY_TYPE)}; 子代理结束后会自动通知, 届时运行 status. 子代理交回的第一行是 "${PAUSED_HANDBACK}" 时, ${replyStep(spec, PAUSED_REPLY_TYPE)}, 用户做完后用 SendMessage 把用户的话转给它继续, 再${replyStep(spec, RUNNING_REPLY_TYPE)}. 子代理已结束, 既没有等待操作也没有写回执时, 用 SendMessage 请它按回执骨架补写`; } return receiptStatus.includes(BLOCKED_RECEIPT_KEYWORD) ? `工单 ${order.id} 执行受阻: 读回执, 运行 order set blocked, 再${replyStep(spec, BLOCKED_REPLY_TYPE)}, 按受阻类型选择选项组` - : `工单 ${order.id} 执行完成: 运行 order set reviewing, 再${reviewDispatchStep(spec)}`; + : `工单 ${order.id} 执行完成: 运行 order set reviewing, 再${reviewDispatchStep(spec, input.state.models)}`; } /** * 受阻工单的下一动作: 内容没有改动时直接继续, 改动后要重新审阅. * * @param {import("./spec.mjs").TemplateSpec} spec 模板规格. - * @param {{id: string}} order 当前工单. + * @param {{id: string, kind: string}} order 当前工单. * @param {GuidanceInput} input 判断信息. * @returns {string} 下一动作. */ function blockedAction(spec, order, input) { if (input.orderApproval === APPROVAL_STATUSES.approved) { - return `工单 ${order.id} 的修改已经用户审阅: ${DISPATCH_STEP}${replyStep(spec, RUNNING_REPLY_TYPE)}`; + return `工单 ${order.id} 的修改已经用户审阅: ${dispatchStep(input.state.models, order)}${replyStep(spec, RUNNING_REPLY_TYPE)}`; } return `工单 ${order.id} 受阻: ${replyStep(spec, BLOCKED_REPLY_TYPE)}; 用户完成所需操作而工单没有改动时, 运行 order set issued, 用 SendMessage 让原执行子代理继续 (找不到它时运行 exec-brief 重新派出), 再${replyStep(spec, RUNNING_REPLY_TYPE)}; 要修改工单时, 改好后${replyStep(spec, APPROVAL_REPLY_TYPE)}${input.isOrderUnchanged === false ? ". 工单文件已在发布后改动, 必须重新审阅" : ""}`; } @@ -227,10 +279,11 @@ function blockedAction(spec, order, input) { * * @param {import("./spec.mjs").TemplateSpec} spec 模板规格. * @param {{id: string, kind: string}} order 当前工单. + * @param {Record | undefined} models 状态中登记的模型分配. * @returns {string} 下一动作. */ -function reviewingAction(spec, order) { - const brief = `${reviewDispatchStep(spec)}; 结果回来后把结论写入 review.md, 再`; +function reviewingAction(spec, order, models) { + const brief = `${reviewDispatchStep(spec, models)}; 结果回来后把结论写入 review.md, 再`; return order.kind === SELECTION_ORDER_KIND ? `选型工单 ${order.id} 验收中: ${brief}${replyStep(spec, REVIEW_REPLY_TYPE, REVIEW_OPTIONS.selection)}, 由用户决定是否采用` : `工单 ${order.id} 验收中: ${brief}${replyStep(spec, REVIEW_REPLY_TYPE)}: 判据全部通过用第 ${REVIEW_OPTIONS.passed} 组, 有不通过用第 ${REVIEW_OPTIONS.fix} 组, 只有未验证用第 ${REVIEW_OPTIONS.unverified} 组`; diff --git a/plugins/waypoint/skills/project-navigator/runtime/lib/state.mjs b/plugins/waypoint/skills/project-navigator/runtime/lib/state.mjs index 5ac9f47..dee8792 100644 --- a/plugins/waypoint/skills/project-navigator/runtime/lib/state.mjs +++ b/plugins/waypoint/skills/project-navigator/runtime/lib/state.mjs @@ -73,6 +73,7 @@ export class StateConflictError extends Error { * @property {number} round 发布轮次. * @property {string} baseCommit 基准提交. * @property {string} [issuedDigest] 最近一次发布时工单文件内容的摘要. + * @property {boolean} [prechecked] 是否已做过发布前核对. */ /** @@ -93,6 +94,7 @@ export class StateConflictError extends Error { * @property {object[]} decisions 决策列表. * @property {object[]} changes 变更列表. * @property {import("./code-checks.mjs").CodeChecks | null} codeChecks 登记的代码检查命令; 尚未登记时为 null. + * @property {Record} models 子代理模型分配中与默认不同的部分, 键见 subagent-models.mjs. * @property {string} lastAction 最后一个动作的中文描述. * @property {string} updatedAt 最后写入时间. * @property {object[]} skipped 被跳过的阶段及原因. @@ -125,6 +127,7 @@ export function createInitialState({ skillVersion, now }) { decisions: [], changes: [], codeChecks: null, + models: {}, lastAction: "初始化", updatedAt: now, skipped: [], @@ -233,6 +236,7 @@ function normalizeState(raw) { decisions: raw.decisions ?? [], changes: raw.changes ?? [], codeChecks: raw.codeChecks ?? null, + models: raw.models ?? {}, skipped: raw.skipped ?? [], }; } diff --git a/plugins/waypoint/skills/project-navigator/runtime/lib/subagent-models.mjs b/plugins/waypoint/skills/project-navigator/runtime/lib/subagent-models.mjs new file mode 100644 index 0000000..27c5c17 --- /dev/null +++ b/plugins/waypoint/skills/project-navigator/runtime/lib/subagent-models.mjs @@ -0,0 +1,163 @@ +/** + * @file 子代理使用的模型: 默认分配, 项目中的改动, 以及派出时的写法. + * + * 模型在派出子代理时用 Agent 工具的 `model` 参数指定: 这个参数优先于子代理定义 + * 与环境变量, 用 SendMessage 续接时也保留 (Claude Code 官方文档). 所以分配写在 + * 状态文件中, 由下一动作写出参数, 不写进子代理定义: 定义写死之后所有项目都受 + * 影响, 续接时是否保留也没有文档依据. + * + * 默认分配: 照单取证的子代理 (核对, 验收, 阅读, 调研) 用 sonnet; 执行子代理跟随 + * 编排会话, 因为它要设计与判断, 也是指出工单有错的那一方. + */ + +import { WorkflowError } from "./workflow-error.mjs"; +import { ORDER_KINDS } from "./workflow-orders.mjs"; + +/** + * 表示跟随编排会话模型的取值: 派出时不传 `model` 参数. + * @type {string} + */ +export const INHERIT_MODEL = "inherit"; + +/** + * 可以指定的模型: Agent 工具 `model` 参数接受的别名, 以及跟随编排会话. + * @type {readonly string[]} + */ +export const MODEL_CHOICES = Object.freeze([ + "sonnet", + "opus", + "haiku", + "fable", + INHERIT_MODEL, +]); + +/** + * 子代理的角色. + * @type {Readonly<{executor: string, checker: string, reviewer: string, reader: string, researcher: string}>} + */ +export const MODEL_ROLES = Object.freeze({ + executor: "executor", + checker: "checker", + reviewer: "reviewer", + reader: "reader", + researcher: "researcher", +}); + +/** + * 各角色的中文名, 用于输出. + * @type {Readonly>} + */ +const ROLE_LABELS = Object.freeze({ + [MODEL_ROLES.executor]: "执行", + [MODEL_ROLES.checker]: "核对", + [MODEL_ROLES.reviewer]: "验收", + [MODEL_ROLES.reader]: "阅读", + [MODEL_ROLES.researcher]: "调研", +}); + +/** + * 各角色的默认模型. + * @type {Readonly>} + */ +const DEFAULT_MODELS = Object.freeze({ + [MODEL_ROLES.executor]: INHERIT_MODEL, + [MODEL_ROLES.checker]: "sonnet", + [MODEL_ROLES.reviewer]: "sonnet", + [MODEL_ROLES.reader]: "sonnet", + [MODEL_ROLES.researcher]: "sonnet", +}); + +/** + * 按工单类型单独指定执行模型时, 状态中键的分隔符, 例如 "executor:fix". + * @type {string} + */ +const KIND_SEPARATOR = ":"; + +/** + * 取得某个角色使用的模型. 执行子代理先看这种工单类型的指定, 再看执行的指定. + * + * @param {Record | undefined} models 状态中登记的分配. + * @param {string} role 角色. + * @param {string} [kind] 工单类型; 只对执行子代理有意义. + * @returns {string} MODEL_CHOICES 中的一个值. + */ +export function resolveModel(models, role, kind) { + const assigned = models ?? {}; + return ( + (kind === undefined + ? undefined + : assigned[`${role}${KIND_SEPARATOR}${kind}`]) ?? + assigned[role] ?? + DEFAULT_MODELS[role] + ); +} + +/** + * 写出派出子代理时 `model` 参数的写法. + * + * @param {Record | undefined} models 状态中登记的分配. + * @param {string} role 角色. + * @param {string} [kind] 工单类型. + * @returns {string} 例如 `model 参数写 "sonnet"`, 或 "不传 model 参数". + */ +export function modelArgument(models, role, kind) { + const model = resolveModel(models, role, kind); + return model === INHERIT_MODEL + ? "不传 model 参数" + : `model 参数写 "${model}"`; +} + +/** + * 登记某个角色使用的模型, 返回新状态. + * + * @param {import("./state.mjs").NavigatorState} state 当前状态. + * @param {object} options 参数. + * @param {string} options.role 角色. + * @param {string} options.model 模型. + * @param {string} [options.kind] 工单类型; 只能与执行子代理一起用. + * @returns {import("./state.mjs").NavigatorState} 新状态. + * @throws {WorkflowError} 角色, 模型或工单类型不合法时. + */ +export function assignModel(state, { role, model, kind }) { + const roles = Object.values(MODEL_ROLES); + if (!roles.includes(role)) { + throw new WorkflowError(`角色应为 ${roles.join(", ")} 之一.`); + } + if (!MODEL_CHOICES.includes(model)) { + throw new WorkflowError(`模型应为 ${MODEL_CHOICES.join(", ")} 之一.`); + } + if (kind !== undefined && role !== MODEL_ROLES.executor) { + throw new WorkflowError("--kind 只能与 executor 一起用."); + } + if (kind !== undefined && !ORDER_KINDS.includes(kind)) { + throw new WorkflowError(`工单类型应为 ${ORDER_KINDS.join(", ")} 之一.`); + } + const key = kind === undefined ? role : `${role}${KIND_SEPARATOR}${kind}`; + return { + ...state, + models: { ...state.models, [key]: model }, + lastAction: `子代理模型 ${key} 设为 ${model}`, + }; +} + +/** + * 描述当前的模型分配, 一行写完; 执行子代理按工单类型另有指定时一并列出. + * + * @param {Record | undefined} models 状态中登记的分配. + * @returns {string} 例如 "执行 跟随会话, 核对 sonnet, 验收 sonnet". + */ +export function describeModels(models) { + const label = (model) => (model === INHERIT_MODEL ? "跟随会话" : model); + const overrides = Object.entries(models ?? {}) + .filter(([key]) => key.includes(KIND_SEPARATOR)) + .map( + ([key, model]) => + `${ROLE_LABELS[MODEL_ROLES.executor]} (${key.split(KIND_SEPARATOR)[1]} 工单) ${label(model)}`, + ); + return [ + ...Object.values(MODEL_ROLES).map( + (role) => `${ROLE_LABELS[role]} ${label(resolveModel(models, role))}`, + ), + ...overrides, + ].join(", "); +} diff --git a/plugins/waypoint/skills/project-navigator/runtime/lib/workflow-orders.mjs b/plugins/waypoint/skills/project-navigator/runtime/lib/workflow-orders.mjs index 73a6fbc..024fec9 100644 --- a/plugins/waypoint/skills/project-navigator/runtime/lib/workflow-orders.mjs +++ b/plugins/waypoint/skills/project-navigator/runtime/lib/workflow-orders.mjs @@ -40,6 +40,21 @@ export const TERMINAL_ORDER_STATUSES = Object.freeze([ */ export const ISSUED_ORDER_STATUS = "issued"; +/** + * 起草状态: 发布前核对只对这个状态的工单强制要求. + * @type {string} + */ +export const DRAFTING_ORDER_STATUS = "drafting"; + +/** + * 可以请用户审阅的工单状态: 起草中, 以及受阻后修改待重新发布. + * @type {readonly string[]} + */ +export const REVIEWABLE_ORDER_STATUSES = Object.freeze([ + DRAFTING_ORDER_STATUS, + "blocked", +]); + /** * 实现工单的类型名. * @type {string} @@ -153,7 +168,7 @@ export function createOrder(state, { kind, slug, slice, baseCommit }) { folder: `${id}-${slug}`, kind, slice: slice ?? state.slice ?? null, - status: "drafting", + status: DRAFTING_ORDER_STATUS, round: 0, baseCommit, }, @@ -216,6 +231,43 @@ export function setOrderStatus( }; } +/** + * 记下当前工单已做过发布前核对. 核对一次即可: 按核对结果修改工单, 或受阻后 + * 修改工单, 都不要求再核对. + * + * @param {import("./state.mjs").NavigatorState} state 当前状态. + * @returns {import("./state.mjs").NavigatorState} 新状态. + * @throws {WorkflowError} 没有当前工单, 或工单已经发布时. + */ +export function markPrechecked(state) { + const order = state.order; + if (order === null) { + throw new WorkflowError("当前没有工单."); + } + if (!REVIEWABLE_ORDER_STATUSES.includes(order.status)) { + throw new WorkflowError( + `工单 ${order.id} 处于 ${order.status}, 发布前核对只用于 ${REVIEWABLE_ORDER_STATUSES.join(", ")} 状态的工单.`, + ); + } + return { + ...state, + order: { ...order, prechecked: true }, + lastAction: `工单 ${order.id} 已做发布前核对`, + }; +} + +/** + * 说明当前工单为什么还不能请用户审阅: 起草中的工单要先做发布前核对. + * + * @param {import("./state.mjs").OrderState} order 当前工单. + * @returns {string | undefined} 原因; 可以审阅时为 undefined. + */ +export function precheckBlocker(order) { + return order.status === DRAFTING_ORDER_STATUS && order.prechecked !== true + ? `工单 ${order.id} 还没有做发布前核对: 运行 precheck-brief, 派核对子代理, 按结果改好工单后运行 order precheck.` + : undefined; +} + /** * 统计某个切片连续验收不通过的工单数, 从最近一张往前数. * diff --git a/plugins/waypoint/skills/project-navigator/runtime/navigator.mjs b/plugins/waypoint/skills/project-navigator/runtime/navigator.mjs index 3326550..5ba1e79 100644 --- a/plugins/waypoint/skills/project-navigator/runtime/navigator.mjs +++ b/plugins/waypoint/skills/project-navigator/runtime/navigator.mjs @@ -9,7 +9,8 @@ * - 骨架与参考: reply <回复类型> [--option <组号>], template <文件种类> [--option <结构编号>], ref <名称> * - 阶段: stage <阶段编号>, step <步骤标识>, skip <阶段编号> --from <草稿>, finish * - 推进路线: roadmap --from <草稿>, milestone <编号> <状态>, slice <编号> <状态> - * - 工单: order new|set, exec-brief, review-brief, research-brief + * - 工单: order new|set|precheck, precheck-brief, exec-brief, review-brief, research-brief + * - 子代理模型: model, model set <角色> <模型> [--kind <工单类型>] * - 源码证据: evidence, evidence check <仓库> <版本> <路径> <行号> * - 记录: risk, decision, change * - 体检与规范: check [文件...], standards --from <草稿>, codecheck set --from <草稿> diff --git a/plugins/waypoint/skills/project-navigator/spec/templates.json b/plugins/waypoint/skills/project-navigator/spec/templates.json index 9957732..0c16edc 100644 --- a/plugins/waypoint/skills/project-navigator/spec/templates.json +++ b/plugins/waypoint/skills/project-navigator/spec/templates.json @@ -320,6 +320,21 @@ ], "optionSets": [] }, + "等待操作": { + "id": "paused", + "orderStage": "待你操作", + "sections": [ + { + "title": "操作内容", + "keys": ["等待原因", "完成之后"], + "presets": { + "完成之后": "回复 \"操作完成\", 并写明实际做了什么; 不做时回复 \"停止执行\"" + } + }, + { "title": "操作步骤" } + ], + "optionSets": [] + }, "验收报告": { "id": "review", "orderStage": "待你决定", @@ -574,7 +589,7 @@ { "title": "运行方式", "keys": ["启动命令", "验证步骤"] }, { "title": "偏差记录", - "keys": ["实现决定", "需要操作", "发现风险", "受阻详情"] + "keys": ["实现决定", "需要操作", "发现风险", "待定判据", "受阻详情"] } ], [ diff --git a/scripts/navigator-docs/executor-guide.md b/scripts/navigator-docs/executor-guide.md index 9ebf951..9a155a6 100644 --- a/scripts/navigator-docs/executor-guide.md +++ b/scripts/navigator-docs/executor-guide.md @@ -13,7 +13,7 @@ ## 执行流程 1. 对账: 运行 `git status` 与 `git log -1`. 当前提交应等于工单概况中的 "基准提交", 工作区不应有与本工单无关的改动. 不一致时不动手, 写回执, 执行状态写 "受阻", 受阻类型写 "对账不符". -2. 核对前提: 逐条检查工单 "前提假设" 是否成立, 读代码与配置核实, 不凭记忆. 不成立时以受阻结束, 受阻类型写 "前提不符". +2. 核对前提: 逐条检查工单 "前提假设" 是否成立, 读代码与配置核实, 不凭记忆. 不成立时以受阻结束, 受阻类型写 "前提不符"; 只影响个别判据时按下方 "待定判据" 处理. 3. 做计划: 先定模块划分, 写法见下方 "模块划分"; 再列出完成每条验收判据的步骤与验证方式. 计划不需要用户确认, 但要写进回执的 "模块划分" 与 "判据自检". 4. 实施: 只做工单 "工作范围" 中要做的事; 遵守工单 "执行守则" 与项目规范 `.claude/rules/engineering.md`. 5. 自检: 逐条核对验收判据, 运行代码检查与测试. 需要真实账号, 会产生费用或外部副作用的测试不要运行, 把命令写进回执 "测试记录" 的 "外部影响", 由用户决定. @@ -44,7 +44,7 @@ ## 受阻情形 -遇到以下情况立即停下, 写回执, 执行状态写 "受阻", 不自行变通: +遇到以下情况立即停下, 写回执, 执行状态写 "受阻", 不自行变通. 两种情况例外, 先看下方 "待定判据" 与 "等待操作": - 对账不符, 或工单前提不成立. - 规格有歧义, 或判据之间互相矛盾. @@ -54,6 +54,25 @@ 回执 "偏差记录" 的 "受阻详情" 写受阻类型 (对账不符, 前提不符, 规格问题, 范围过大, 需要用户操作, 需要用户决定), 规格原文, 实际情况与证据位置. 用户处理后, 编排会话可能让你接着做: 收到继续的消息时, 先运行 `git status` 核对工作区, 再从受阻处继续, 最后重写回执. +## 待定判据 + +工单的其余部分不受影响, 只有个别判据照原文做不到时, 不以受阻结束: 把其余工作做完, 这几条判据留作待定, 由用户在验收时决定. 这样用户只决定一次, 不用整张工单往返一轮. + +- 适用情形: 判据点名的实例不存在或不带所要求的特征; 判据依赖的事实不成立; 判据要用户在两种说法中选一种, 而选哪种都不改变已经完成的工作. +- 不适用: 用户的决定会改变其余部分的做法 (例如随包带哪种格式的文件); 前提不成立导致主要工作无法进行; 要改动工作范围之外的文件. 这些情况仍按受阻处理. +- 写法: 回执 "执行状态" 写 "完成"; "偏差记录" 的 "待定判据" 逐条写判据编号, 规格原文, 实际情况与证据位置, 以及可选的处理办法 (例如 "改用实例 X 核对" 或 "以单元测试代替"). "判据自检" 中这几条写 "待定". 没有待定判据时写 "无". +- 不替用户选: 不按自己选的办法改写判据后写 "通过". + +## 等待操作 + +工单没有问题, 只是要用户亲手做一件事才能继续时 (例如在被调试的程序中做一次操作, 连接一台设备, 登录一个账号), 不写回执, 中途交回: + +- 交回之前把断点, 监听等准备工作做好, 把已经查清的内容写进 `artifacts/` 下的文档: 会话中断后, 新的执行子代理靠这些文档接着做. +- 交回内容第一行写 "执行状态: 等待操作", 其后逐条写要用户做的操作: 写到用户不用追问就能照做, 写明做完后要告诉你什么. +- 编排会话转来用户的回复后, 从停下的地方继续. 需要再次操作时照此再交回一次. +- 全部做完后照常写回执. 回执 "偏差记录" 的 "需要操作" 写 "无", 中途请用户做过的操作写进 "实现决定". +- 要用户做决定, 或要修改工单时, 不用这种方式, 按受阻处理. + ## 技能问题 - 问题出在技能自身时 (插件命令输出 "技能异常" 一行, 本手册与骨架或自查命令对不上, 守卫拒绝了本手册允许的写入), 不绕开, 不修改技能文件. diff --git a/tests/project-navigator/flow.test.mjs b/tests/project-navigator/flow.test.mjs index b04c013..0423b48 100644 --- a/tests/project-navigator/flow.test.mjs +++ b/tests/project-navigator/flow.test.mjs @@ -172,6 +172,12 @@ function approveAndIssue(setup, kind) { `${folder}/order.md`, fill(commandSkeleton(project(["template", "order"]).stdout)), ); + assert.match( + project(["reply", "approve"]).stdout, + /还没有做发布前核对: 运行 precheck-brief/u, + ); + assert.match(project(["precheck-brief"]).stdout, /^# 发布前核对 \d{4}/u); + project(["order", "precheck"]); const review = fill(commandSkeleton(project(["reply", "approve"]).stdout)); assert.equal(stop(root, orchestrator, review), undefined); prompt(root, orchestrator, "A"); @@ -303,7 +309,7 @@ test("hook: 只校验编排会话回复的结构, 打回一次后放行, 后台 ); const background = { background_tasks: [{ id: "t" }] }; assert.equal(stop(root, orchestrator, broken, background)?.decision, "block"); - for (const id of ["reviewing", "waiting"]) { + for (const id of ["reviewing", "waiting", "paused"]) { const waiting = fill(commandSkeleton(project(["reply", id]).stdout)); assert.doesNotMatch(waiting, /```/u); assert.equal(stop(root, orchestrator, waiting, background), undefined); @@ -333,15 +339,37 @@ test("工单往返: 审阅发布, 执行子代理写回执, 验收通过, 输出 }), "deny", ); - assert.match(project(["status"]).stdout, /回复 "工单执行"/u); + const running = project(["status"]).stdout; + assert.match(running, /回复 "工单执行"/u); + assert.match( + running, + /第一行是 "执行状态: 等待操作" 时, 回复 "等待操作" \(reply paused\)/u, + ); + assert.match( + running, + /- 子代理模型: 执行 跟随会话, 核对 sonnet, 验收 sonnet, 阅读 sonnet, 调研 sonnet\n/u, + ); assert.match( project(["reply", "running"]).stdout, /· 工单 0001 \(执行中\)\n/u, ); + assert.match( + project(["reply", "paused"]).stdout, + /· 工单 0001 \(待你操作\)\n[\s\S]*- 完成之后: 回复 "操作完成"/u, + ); + assert.match( + project(["template", "receipt"]).stdout, + /- 待定判据: <待填写>/u, + ); + assert.match( + project(["model", "set", "reviewer", "inherit"]).stdout, + /验收 跟随会话/u, + ); + assert.equal(project(["model", "set", "reviewer", "gpt"]).status, 1); writeReceipt(setup, folder, "完成"); assert.match( project(["status"]).stdout, - /执行状态 完成[\s\S]*order set reviewing, 再运行 review-brief[\s\S]*回复 "工单验收" \(reply reviewing\)/u, + /执行状态 完成[\s\S]*order set reviewing, 再运行 review-brief, 把输出原样作为提示词派 waypoint:navigator-reviewer \(不传 model 参数\)[\s\S]*回复 "工单验收" \(reply reviewing\)/u, ); project(["order", "set", "reviewing"]); assert.match( @@ -373,6 +401,7 @@ test("工单往返: 认可之后改过工单, 发布被拒; 未验证的判据 const folder = ".navigator/orders/0001-export"; const order = fill(commandSkeleton(project(["template", "order"]).stdout)); writeProjectFile(root, `${folder}/order.md`, order); + project(["order", "precheck"]); stop( root, orchestrator, @@ -411,6 +440,7 @@ test("工单审阅: 没有选发布之后, 写明发布工单的消息仍然认 const folder = ".navigator/orders/0001-export"; const order = fill(commandSkeleton(project(["template", "order"]).stdout)); writeProjectFile(root, `${folder}/order.md`, order); + project(["order", "precheck"]); stop( root, orchestrator, @@ -440,6 +470,7 @@ test("基准提交: 新建工单之后仓库有新提交时, 审阅拒绝旧值, const head = commitPaths(root, ["CHANGES.md"], "升级").trim(); assert.doesNotMatch(stale, new RegExp(head, "u")); writeProjectFile(root, `${folder}/order.md`, stale); + project(["order", "precheck"]); const refused = project(["reply", "approve"]); assert.equal(refused.status, 1); assert.match(refused.stdout, new RegExp(`把 "基准提交" 改为 ${head}`, "u")); @@ -468,6 +499,7 @@ test("工单审阅: 骨架从工单文件的审阅要点预填, 并写出工单 commandSkeleton(project(["template", "order"]).stdout), ).replace("- 需要决定: 已填写", "- 需要决定: 是否在仓库内维护补丁"); writeProjectFile(root, ".navigator/orders/0001-export/order.md", order); + project(["order", "precheck"]); const skeleton = commandSkeleton(project(["reply", "approve"]).stdout); assert.match(skeleton, /- 需要决定: 是否在仓库内维护补丁/u); assert.match( diff --git a/tests/project-navigator/guard.test.mjs b/tests/project-navigator/guard.test.mjs index 9e957ca..eff89f0 100644 --- a/tests/project-navigator/guard.test.mjs +++ b/tests/project-navigator/guard.test.mjs @@ -84,10 +84,15 @@ test("守卫: 状态文件与生成的视图文件谁都不能直接写", () => } }); -test("守卫: 验收子代理不能写任何项目文件", () => { +test("守卫: 核对与验收子代理不能写任何项目文件", () => { + for (const agentType of [SUBAGENT_TYPES.reviewer, SUBAGENT_TYPES.checker]) { + assert.equal(decide({ target: "src/app.js", agentType }).decision, "deny"); + } assert.equal( - decide({ target: "src/app.js", agentType: SUBAGENT_TYPES.reviewer }) - .decision, + decide({ + target: `.navigator/orders/${FOLDER}/order.md`, + agentType: SUBAGENT_TYPES.checker, + }).decision, "deny", ); assert.equal( diff --git a/tests/project-navigator/workflow.test.mjs b/tests/project-navigator/workflow.test.mjs index 076a059..6e68138 100644 --- a/tests/project-navigator/workflow.test.mjs +++ b/tests/project-navigator/workflow.test.mjs @@ -8,6 +8,7 @@ import { test } from "node:test"; import { buildExecutorBrief, + buildPrecheckBrief, buildReviewBrief, } from "../../plugins/waypoint/skills/project-navigator/runtime/lib/briefs.mjs"; import { nextAction } from "../../plugins/waypoint/skills/project-navigator/runtime/lib/guidance.mjs"; @@ -26,10 +27,17 @@ import { STATE_SCHEMA_VERSION, createInitialState, } from "../../plugins/waypoint/skills/project-navigator/runtime/lib/state.mjs"; +import { + MODEL_ROLES, + assignModel, + describeModels, + resolveModel, +} from "../../plugins/waypoint/skills/project-navigator/runtime/lib/subagent-models.mjs"; import { WorkflowError } from "../../plugins/waypoint/skills/project-navigator/runtime/lib/workflow-error.mjs"; import { consecutiveRejections, createOrder, + markPrechecked, setOrderStatus, } from "../../plugins/waypoint/skills/project-navigator/runtime/lib/workflow-orders.mjs"; import { @@ -432,11 +440,16 @@ test("下一动作: 首次接入, 进入时恢复进度, 项目完成", () => { ); }); -test("下一动作: 起草中的工单先审阅, 认可后发布并在后台派执行子代理", () => { - const drafting = draft(stateWithRoadmap()); +test("下一动作: 起草中的工单先核对再审阅, 认可后发布并在后台派执行子代理", () => { + const unchecked = draft(stateWithRoadmap()); + assert.match( + nextAction({ ...GUIDANCE_BASE, state: unchecked }), + /运行 precheck-brief, 把输出原样作为提示词派 waypoint:navigator-checker \(model 参数写 "sonnet"\).+然后运行 order precheck, 再回复 "工单审阅"/u, + ); + const drafting = markPrechecked(unchecked); assert.match( nextAction({ ...GUIDANCE_BASE, state: drafting }), - /回复 "工单审阅" \(reply approve\); 用户选 A 后运行 order set issued, 再运行 exec-brief/u, + /已做发布前核对: 回复 "工单审阅" \(reply approve\); 用户选 A 后运行 order set issued, 再运行 exec-brief/u, ); assert.match( nextAction({ @@ -452,7 +465,55 @@ test("下一动作: 起草中的工单先审阅, 认可后发布并在后台派 state: drafting, orderApproval: APPROVAL_STATUSES.approved, }), - /run_in_background\) 派 waypoint:navigator-executor, 然后回复 "工单执行"/u, + /run_in_background\) 派 waypoint:navigator-executor \(不传 model 参数\), 然后回复 "工单执行"/u, + ); +}); + +test("子代理模型: 默认分配, 按工单类型指定执行模型, 不合法的取值被拒", () => { + const state = draft(stateWithRoadmap()); + assert.equal(resolveModel(state.models, MODEL_ROLES.reviewer), "sonnet"); + assert.equal(resolveModel(state.models, MODEL_ROLES.executor), "inherit"); + const assigned = assignModel( + assignModel(state, { role: "executor", model: "sonnet", kind: "fix" }), + { role: "reviewer", model: "inherit" }, + ); + assert.equal(resolveModel(assigned.models, "executor", "fix"), "sonnet"); + assert.equal( + resolveModel(assigned.models, "executor", "implementation"), + "inherit", + ); + assert.match( + describeModels(assigned.models), + /^执行 跟随会话, 核对 sonnet, 验收 跟随会话, 阅读 sonnet, 调研 sonnet, 执行 \(fix 工单\) sonnet$/u, + ); + const fix = { ...assigned, order: { ...assigned.order, kind: "fix" } }; + assert.match( + nextAction({ + ...GUIDANCE_BASE, + state: markPrechecked(fix), + orderApproval: APPROVAL_STATUSES.approved, + }), + /派 waypoint:navigator-executor \(model 参数写 "sonnet"\)/u, + ); + assert.throws( + () => assignModel(state, { role: "executor", model: "gpt" }), + /模型应为/u, + ); + assert.throws( + () => assignModel(state, { role: "reviewer", model: "opus", kind: "fix" }), + /只能与 executor 一起用/u, + ); +}); + +test("委派: 核对提示词写明工单路径与三种结论; 执行提示词写明等待操作的交回", () => { + const order = stateWithIssuedOrder().order; + const precheck = buildPrecheckBrief({ projectRoot: "/项目", order }); + assert.match(precheck, /^# 发布前核对 0001/u); + assert.match(precheck, /order\.md/u); + assert.match(precheck, /成立, 不成立, 无法核对 之一/u); + assert.match( + buildExecutorBrief({ projectRoot: "/项目", order }), + /第一行写 "执行状态: 等待操作"/u, ); });