tianshu-mcp 0.6.3 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.en.md CHANGED
@@ -8,6 +8,62 @@ Chinese version: [CHANGELOG.md](CHANGELOG.md)
8
8
 
9
9
  ---
10
10
 
11
+ ## [0.6.5] - 2026-09-24
12
+
13
+ ### Added
14
+
15
+ - **Three-level acceptance config inheritance** ([issue #20](https://github.com/lanlan0811/tianshu-mcp/issues/20)): `<data home>/acceptance.default.json` (global fallback) → `<project>/.tianshu-mcp/acceptance.json` (project override) → transient task-level override. With several similar projects under one host, the shared policy goes in the global layer instead of a file per project. See the [acceptance config spec](docs/acceptance-config.en.md).
16
+ - **New optional `acceptanceOverride` on `run_task` / `verify_task`**: a transient task-level acceptance-config override (same format as `acceptance.json`) that applies **only to that task**, is saved with the task snapshot, writes no `acceptance*.json` and affects no other task on the same project. Project-less mode explicitly rejects the argument.
17
+ - **`tianshu-mcp config acceptance [projectPath] [--task <taskId>]` debug command**: prints which layers exist, the effective order and the final values, so troubleshooting needs no guesswork. Each acceptance round also writes a same-source summary line to `server.log`.
18
+ - New `src/config/acceptance-merge.ts` (a purpose-scoped merge helper, **deliberately not a general-purpose deep merge**).
19
+
20
+ ### Fixed
21
+
22
+ - **The `.default()` pollution hazard in layered parsing**: `AcceptanceConfigSchema` puts `.default(true)` on `requireChanges`, so parsing a project file that only sets `verifyConcurrency` materializes `requireChanges: true`, which in the three-level chain would **in turn override the global layer's `false`**. A default-free `PartialAcceptanceConfigSchema` is now used for layered parsing, with defaults applied only when the final value is absent.
23
+
24
+ ### Changed
25
+
26
+ - `resolveChecks()` now performs the three-level merge and returns the merged `visual` too; `executeVerify` no longer re-reads the project file (otherwise the `visual` from override/global layers would change meaning based on whether a project file happens to exist).
27
+ - After the unified `LOCKFILE_PATTERN` export (v0.6.4), the bilingual `acceptance-config` precedence tables were rewritten around the three-level chain.
28
+
29
+ ### Compatibility
30
+
31
+ - **No tool contract, data model or MCP annotation changes** (`acceptanceOverride` is a new optional argument; `extraChecks` semantics and precedence are unchanged and still rank above the base set).
32
+ - Without creating a global `acceptance.default.json`, single-project behaviour is exactly as in v0.6.4 (an absent global layer = empty config, no error).
33
+ - The existing semantics of the project-level `.tianshu-mcp/acceptance.json` are unchanged; error semantics remain fail-closed (only `ENOENT` counts as "layer absent").
34
+
35
+ ### Notes (disclosed honestly)
36
+
37
+ - **Merge granularity is per field**: a field a higher layer explicitly writes wins outright; **arrays (`checks`) replace wholesale rather than concatenating** — concatenating would turn "the project adds one check" into "the project can never remove a global check".
38
+ - **`visual` replaces wholesale and is not deep-merged across layers**: nearly every field of the `visual` schema carries a default, so deep merging would let a higher layer's "not written, present only as a default" fields silently clobber a lower layer's **explicit** values (the same pollution class as `requireChanges`). To reuse visual config across layers, write the full `visual` block in the project layer. **This is a deliberate deviation from the issue's suggested "deep merge"**, with the reasoning recorded in `docs/acceptance-config.en.md` and ARCHITECTURE §7.4.
39
+ - **The task override is included in the idempotency argument digest**: replaying the same key with a different acceptance policy is rejected fail-closed rather than returning an old task built on a different policy.
40
+
41
+ ## [0.6.4] - 2026-09-24
42
+
43
+ ### Added
44
+
45
+ - **Structured repair directives `repairDirectives`** ([issue #19](https://github.com/lanlan0811/tianshu-mcp/issues/19)): failed rounds parse acceptance failure reasons into **directly executable actions** (`file? / line? / issue / action / source`) carried in both the repair plan and the rework message, sparing the agent the cost of locating "which line has the type mismatch, which file has a TODO" in a full narrative report. See [structured repair directives](docs/repair-directives.en.md).
46
+ - **Two built-in extraction sources**: `typecheck` (parses pretty / plain tsc errors out of failed typecheck checks' output tails; absolute paths normalized to project-relative POSIX; duplicates collapsed) and `diffstat` (oversized single-file changes, modified lockfiles, and line-level counts for TODO / debug output / secret-like patterns).
47
+ - **New optional `repairHint` on `rework_task`** (free-form string, max 4000 chars): the caller supplies its own structured repair hint, rendered in the next round's task book as a `【结构化修复提示】` block placed **before** `feedback`.
48
+
49
+ ### Changed
50
+
51
+ - **`report-<round>.md` gained a `## Structured repair directives` section**; `report-<round>.json` gained a `repairDirectives` field (**failed rounds only**).
52
+ - **Both repair-plan variants (generic `rework-*.md` and Codex `codex-fix-r*.md`) gained a `## 2.5 Structured repair directives` section**, placed between section 2 (failures) and section 3 (passing checks).
53
+ - `LOCKFILE_PATTERN` is now exported from `code-analysis.ts` so the analysis warnings and the extractor **share one list**, preventing drift between two copies.
54
+
55
+ ### Compatibility
56
+
57
+ - **No tool contract, data model or MCP annotation changes.** `repairDirectives` is a new optional field inside the report, which readers may simply ignore; when `repairHint` is omitted, `rework_task` behaves exactly as in v0.6.3.
58
+ - Extraction runs **only on failed acceptance rounds**; passing rounds do not produce the field (no report bloat).
59
+
60
+ ### Notes (disclosed honestly)
61
+
62
+ - **Explicit fallback when extraction fails**: a non-empty `fallbackReason` means renderers state "unavailable, falling back to the full report" and tell the agent to return to the full failure output — a **silent gap is not allowed**. An exception from a single source is swallowed and recorded in the reason while other sources keep working — the extractors never throw.
63
+ - **No extraction for test-class failures**: test-framework output has no stable file/line; parsing it anyway would produce **wrong** locations, which is worse than producing none.
64
+ - **`diffstat`'s line-level signals never fake a location**: `signals.ts` only counts and has no stable file or line, so those directives omit the `file` field.
65
+ - **Known limitation**: `outputTail` is truncated to the last 4000 characters, so a large project only yields tail type errors and the rest is covered by the fallback — a deliberately accepted trade-off.
66
+
11
67
  ## [0.6.3] - 2026-09-24
12
68
 
13
69
  ### Added
package/CHANGELOG.md CHANGED
@@ -7,6 +7,62 @@
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.6.5] - 2026-09-24
11
+
12
+ ### 新增
13
+
14
+ - **验收配置三级继承**([issue #20](https://github.com/lanlan0811/tianshu-mcp/issues/20)):`<数据目录>/acceptance.default.json`(全局兜底)→ `<project>/.tianshu-mcp/acceptance.json`(项目覆盖)→ 任务级临时覆盖。一个宿主下挂多个同类项目时,公共策略写全局层即可,不必逐项目建文件。详见 [验收配置规范](docs/acceptance-config.md)。
15
+ - **`run_task` / `verify_task` 新增可选 `acceptanceOverride`**:任务级临时验收配置覆盖(格式同 `acceptance.json`),**仅当次生效**、随任务快照保存、不写入任何 `acceptance*.json`、不影响同项目其他任务。无项目模式显式拒绝该参数。
16
+ - **`tianshu-mcp config acceptance [projectPath] [--task <taskId>]` 调试命令**:打印各层是否存在、实际生效顺序与最终取值,排障无需靠猜。每轮验收另往 `server.log` 写一行同源摘要。
17
+ - 新增 `src/config/acceptance-merge.ts`(按用途收敛的合并工具,**刻意不做通用深合并**)。
18
+
19
+ ### 修复
20
+
21
+ - **分层解析的 `.default()` 污染隐患**:`AcceptanceConfigSchema` 的 `requireChanges` 带 `.default(true)`,若用它解析「只写了 `verifyConcurrency`」的项目文件会 materialize 出 `requireChanges: true`,在三级继承里**反过来覆盖全局层的 `false`**。新增无默认值的 `PartialAcceptanceConfigSchema` 供分层解析,默认值只在最终取值缺省时兜底。
22
+
23
+ ### 变更
24
+
25
+ - `resolveChecks()` 改为三级合并,并把合并后的 `visual` 一并返回;`executeVerify` 不再二次读取项目文件(否则 override/全局层的 `visual` 会随项目文件是否存在而改变语义)。
26
+ - `LOCKFILE_PATTERN` 的统一导出(v0.6.4)之后,`acceptance-config` 双语优先级表按三级继承重写。
27
+
28
+ ### 兼容性
29
+
30
+ - **无工具契约、数据模型或 MCP 注解变更**(`acceptanceOverride` 是新增可选参数;`extraChecks` 语义与优先级不变,仍高于基础集)。
31
+ - 不创建全局 `acceptance.default.json` 时,单项目行为与 v0.6.4 完全一致(全局层缺失 = 空配置、不报错)。
32
+ - 项目级 `.tianshu-mcp/acceptance.json` 的既有语义不变;错误语义仍是 fail-closed(仅 `ENOENT` 视为「该层不存在」)。
33
+
34
+ ### 说明(如实披露)
35
+
36
+ - **合并粒度是「字段」**:高优先级层显式书写的字段整体取胜;**数组(`checks`)整体覆盖而非拼接** —— 拼接会让「项目追加一项检查」变成「项目无法移除全局检查」。
37
+ - **`visual` 整体覆盖、不做跨层深合并**:`visual` 的 schema 几乎每个字段都带默认值,深合并会让低优先级层的**显式**取值被高优先级层「未书写、仅因默认值而出现」的字段静默覆盖(与 `requireChanges` 同类的污染)。需要跨层复用视觉配置时请在项目层写完整 `visual` 段。**这是与 issue 建议的「深合并」的一处有意偏离**,理由记录在 `docs/acceptance-config.md` 与 ARCHITECTURE §7.4。
38
+ - **任务级覆盖计入幂等入参摘要**:同键换一套验收策略会被 fail-closed 拒绝,而不是返回策略不同的旧任务。
39
+
40
+ ## [0.6.4] - 2026-09-24
41
+
42
+ ### 新增
43
+
44
+ - **结构化修复指令 `repairDirectives`**([issue #19](https://github.com/lanlan0811/tianshu-mcp/issues/19)):失败轮次把验收失败原因解析为**可直接执行的动作**(`file? / line? / issue / action / source`),随返修计划、返修消息一起喂给 agent,省去它从整篇报告里定位「哪一行类型不匹配、哪个文件有 TODO」的开销。详见 [结构化修复指令](docs/repair-directives.md)。
45
+ - **两个内置提取来源**:`typecheck`(解析失败类型检查项输出尾部的 tsc pretty / plain 两式报错,绝对路径归一化为项目相对 posix 路径,同处报错去重)与 `diffstat`(超大单文件改动、被改动的锁文件、TODO / 调试输出 / 疑似密钥的行级计数)。
46
+ - **`rework_task` 新增可选 `repairHint`**(自由字符串,上限 4000 字符):调用方自带结构化修复提示,在下一轮任务书的 `【结构化修复提示】` 块中**排在 `feedback` 之前**。
47
+
48
+ ### 变更
49
+
50
+ - **`report-<round>.md` 新增 `## 结构化修复指令` 小节**;`report-<round>.json` 新增 `repairDirectives` 字段(**仅失败轮次**)。
51
+ - **两块返修计划(通用 `rework-*.md` 与 Codex `codex-fix-r*.md`)新增 `## 2.5 结构化修复指令` 小节**,位于第 2 节(失败项)与第 3 节(通过项)之间。
52
+ - `LOCKFILE_PATTERN` 由 `code-analysis.ts` 导出,供分析告警与提取器**共用一份清单**,避免两处漂移。
53
+
54
+ ### 兼容性
55
+
56
+ - **无工具契约、数据模型或 MCP 注解变更**。`repairDirectives` 是报告内的新增可选字段,读取方按缺省忽略即可;`repairHint` 不传时 `rework_task` 行为与 v0.6.3 完全一致。
57
+ - 提取**只在验收失败的轮次**执行;通过的轮次不产出该字段(不徒增报告体积)。
58
+
59
+ ### 说明(如实披露)
60
+
61
+ - **提取不到时显式回退**:`fallbackReason` 非空 ⇒ 渲染方写明「不可用,回退完整报告」并要求 agent 回到完整失败输出,**不允许静默留空**。单个来源抛错会被吞掉并记入原因,其余来源继续工作 —— 提取器永不抛错。
62
+ - **测试类失败不做提取**:测试框架输出没有稳定的文件/行号,强行解析会产出**错误**定位,比不给更糟。
63
+ - **`diffstat` 的行级信号不伪造位置**:`signals.ts` 只做计数、无稳定文件与行号,故对应指令省略 `file` 字段。
64
+ - **已知限制**:`outputTail` 被截断到最后 4000 字符,大型项目只能提取到尾部类型错误,其余靠回退兜底 —— 有意接受的取舍。
65
+
10
66
  ## [0.6.3] - 2026-09-24
11
67
 
12
68
  ### 新增
package/README.en.md CHANGED
@@ -40,8 +40,10 @@ Tianshu plays the role of the overall commander; this MCP server is the **schedu
40
40
  - **11 MCP tools**: `run_task / continue_task / query_task / list_tasks / get_task_report / cancel_task / verify_task / rework_task / get_profiles`, plus `prepare_visual_baseline / approve_visual_baseline` for visual acceptance
41
41
  - **Async contract**: `run_task` returns a `taskId` immediately; long-running work is polled via `query_task` (never blocks `tools/call`).
42
42
  - **Long-task observability (issue #18)**: adapters report fine-grained events at key nodes (`task_dispatched` / `confirmation_dialog_detected` / `awaiting_user_authorization` / `file_modification_started` / `rework_triggered`), and `query_task` returns the most recent N via `eventLimit` (default 10) — **so you can tell "the agent is working" apart from "stuck on a dialog waiting for a human"**. Event reporting is an optional capability: adapters that don't implement it behave unchanged. codex and traework report in this version. See [event stream](docs/event-stream.en.md).
43
+ - **Structured repair directives (issue #19)**: failed rounds parse the reasons into **directly executable actions** (`file:line / issue / action`) carried in both the repair plan and the rework message, sparing the agent the cost of locating problems in a full narrative report; when extraction is unavailable it **explicitly falls back** to the full report (never a silent gap). `rework_task` also accepts an optional `repairHint`. See [structured repair directives](docs/repair-directives.en.md).
43
44
  - **Objective acceptance**: automated command checks (typecheck/lint/test/build — skipped when absent, plus tech-stack derivation) + programmatic code analysis (changed-file list / diffstat / suspicious signals such as TODO, debugger, secret-like patterns), all relative to a **git baseline**; never auto-commits or stashes. The acceptance engine is **fail-closed**: a test check fails when its output reports zero executed tests even if the exit code is 0; git projects must produce changes relative to the pre-work baseline by default (pure analysis tasks can opt out with `"requireChanges": false` in `.tianshu-mcp/acceptance.json`).
44
45
  - **Acceptance parallelism**: command checks run **bounded-parallel** by default (`verifyConcurrency`, default 2, range 1–4). When checks depend on an order (a later check reading build output, `--fix`, shared cache dirs), set it to `1` for fully serial behaviour; a project can override it in `.tianshu-mcp/acceptance.json`, and the server level lives in `config.json`. Report and log formats are unchanged (results are returned in declaration order).
46
+ - **Three-level acceptance config inheritance (issue #20)**: `<data home>/acceptance.default.json` (global fallback) → `<project>/.tianshu-mcp/acceptance.json` (project override) → the `acceptanceOverride` argument of `run_task`/`verify_task` (transient task override, applying only to that task and never written to disk as config). For the same field a higher layer wins; arrays replace wholesale rather than concatenating. Inspect the final effective configuration with `tianshu-mcp config acceptance <projectPath> [--task <id>]`. See the [acceptance config spec](docs/acceptance-config.en.md).
45
47
  - **Rework loop**: automatic rework (`autoFixRounds`) + manual `rework_task`; on verification failure a repair-plan file is generated and fed back to the agent; when rounds run out → `needs_attention` awaiting Tianshu's verdict.
46
48
  - **Execution surfaces**: `driver: "gui"` selects an explicit, isolated Codex/TraeWork/ZCode/Kimi Code/Qoder CN CDP adapter; `driver: "spawn"` runs an external CLI child process.
47
49
  - **Project-less dispatch (ZCode, issue #12)**: `run_task`'s `projectPath` may be omitted — ZCode runs the task in its `default` workspace without registering/importing a project, collecting a Git baseline, or running project acceptance (the result is marked structurally as `verificationNotApplicable: "no_project"` and `verify_task`/`get_task_report` return a not-applicable explanation). The companion `allowCreateProject: false` stops dispatch before any import side effect when the target directory is unregistered. See the [ZCode CDP adapter](docs/zcode-cdp.en.md).
@@ -187,14 +189,14 @@ run_task(projectPath=D:/xxx/my-app, agentId=qoder, planDoc=./plans/development.m
187
189
 
188
190
  | Tool | Capability / approval | Purpose |
189
191
  |---|---|---|
190
- | `run_task` | write + approval | Dispatch work (optional auto-verify / auto-rework); returns `taskId` asynchronously. Optional `idempotencyKey`: a retry with the same key returns the original `taskId` instead of creating a task |
192
+ | `run_task` | write + approval | Dispatch work (optional auto-verify / auto-rework); returns `taskId` asynchronously. Optional `idempotencyKey` (a retry with the same key returns the original `taskId` instead of creating a task) and `acceptanceOverride` (transient acceptance-config override applying only to this task) |
191
193
  | `continue_task` | write + approval | Resume the session behind `needs_user` (ZCode resumes the recorded session; Codex re-observes for `user_confirmation` / re-dispatches for `login_required`; Kimi Code resumes the recorded session and distinguishes question answering / re-observation / full re-dispatch) |
192
194
  | `query_task` | read | Poll status / progress / log tail / recent fine-grained events. The optional `eventLimit` (1..50, default 10) controls how many `recentEvents` the meta block carries, so long tasks can be told apart as "working normally" versus "stuck on a dialog waiting for a human" |
193
195
  | `list_tasks` | read | Filtered history of tasks |
194
196
  | `get_task_report` | read | Full text of a verification round's report (`report.md`) |
195
197
  | `cancel_task` | write + approval | Cancel a running task: CLI agents kill the process tree; GUI agents click the in-app stop control over CDP and bounded-wait (`gui.cancelWaitMs`, default 15s) for the GUI to go idle, stating so explicitly in the final message when the stop is unconfirmed. For a terminal GUI task this call doubles as the manual acknowledgement entry point — after verifying the window holds no residual run, it clears the `guiStopUnconfirmed` marker |
196
198
  | `verify_task` | execute (no source changes, no approval) | Run one verification pass on a task/project path. Capability is `execute`: it runs the project's configured commands and may produce build artifacts, so MCP `readOnlyHint` is `false` — but it **does not modify sources and still needs no approval**. Optional `idempotencyKey`: a retry with the same key never re-runs (a running pass answers "in progress", a finished one returns the existing report) |
197
- | `rework_task` | write + approval | Manual rework (feed the failure report back to the same agent) |
199
+ | `rework_task` | write + approval | Manual rework (feed the failure report back to the same agent). Optional `repairHint` (≤4000 chars) supplies a structured repair hint rendered as a block before `feedback` |
198
200
  | `get_profiles` | read | Inspect agent adapters and executable discovery results |
199
201
  | `prepare_visual_baseline` | write + approval | Capture or import reference images into a reviewable candidate with a digest |
200
202
  | `approve_visual_baseline` | write + approval | Validate the reviewed digest and write the baseline and approval record |
package/README.md CHANGED
@@ -42,7 +42,9 @@
42
42
  - **长任务可观测(issue #18)**:适配器在关键节点上报细粒度事件(`task_dispatched` / `confirmation_dialog_detected` / `awaiting_user_authorization` / `file_modification_started` / `rework_triggered`),`query_task` 经 `eventLimit`(默认 10)回传最近 N 条——**能区分「agent 正在干活」与「卡在弹窗等人工介入」**。事件上报是可选能力:未实现的适配器行为不变。本版 codex 与 traework 已落地上报。详见 [事件流](docs/event-stream.md)。
43
43
  - **客观验收**:自动命令检查(typecheck/lint/test/build,缺则跳过 + 技术栈推导)+ 程序化代码分析(变更清单/diffstat/TODO·debugger·密钥形态等可疑标记),全部相对 **git 基线**,不自动 commit/stash。验收引擎 **fail-closed**:测试命令退出码为 0 但输出显示零用例时判失败;git 项目默认要求相对动工前基线产生变更(纯分析任务可在 `.tianshu-mcp/acceptance.json` 设 `"requireChanges": false` 显式关闭)。
44
44
  - **验收并行度**:命令检查默认**有界并行**(`verifyConcurrency`,默认 2、范围 1–4)。检查项之间有顺序依赖时(后续检查读取 build 产物、带 `--fix`、共享缓存目录)请设 `1` 完全退化为串行;项目级 `.tianshu-mcp/acceptance.json` 可覆盖,server 级在 `config.json`。报告与日志格式不变(结果按声明顺序返回)。
45
+ - **验收配置三级继承(issue #20)**:`<数据目录>/acceptance.default.json`(全局兜底)→ `<project>/.tianshu-mcp/acceptance.json`(项目覆盖)→ `run_task`/`verify_task` 的 `acceptanceOverride` 参数(任务级临时覆盖,仅当次生效、不落盘为配置)。同名字段高优先级取胜、数组整体覆盖不拼接。用 `tianshu-mcp config acceptance <projectPath> [--task <id>]` 查看最终生效配置。详见 [验收配置规范](docs/acceptance-config.md)。
45
46
  - **失败返修闭环**:自动返修(`autoFixRounds`)+ 手动 `rework_task`;验收失败时自动生成修复计划文件并回填给 agent;轮次用尽 → `needs_attention` 等天枢裁决。
47
+ - **结构化修复指令(issue #19)**:失败轮次会把原因解析为**可直接执行的动作**(`文件:行 / 问题 / 做什么`),随返修计划与返修消息一起喂给 agent,省去它从整篇报告里定位的开销;提取不到时**显式回退**到完整报告(不静默留空)。`rework_task` 另可选 `repairHint` 自带提示。详见 [结构化修复指令](docs/repair-directives.md)。
46
48
  - **执行面**:`driver: "gui"` 由显式 adapter 驱动桌面 UI(Codex / TraeWork / ZCode / Kimi Code 各自使用隔离的 CDP 流程);`driver: "spawn"` 走外部 CLI 子进程。
47
49
  - **无项目派发(ZCode,issue #12)**:`run_task` 的 `projectPath` 可省略——ZCode 在 `default` 工作区承接任务,不登记/导入项目、不采集 Git 基线、不执行项目验收(结果以 `verificationNotApplicable: "no_project"` 结构化标注,`verify_task`/`get_task_report` 返回不适用说明)。配套 `allowCreateProject: false` 可在目标目录未登记时于任何导入副作用之前停止派发。详见 [ZCode CDP 适配器](docs/zcode-cdp.md)。
48
50
  - **幂等重试(issue #15)**:`run_task` / `verify_task` 接受可选 `idempotencyKey`——同一 key 在 TTL(默认 24h)内的重试**不会**重复派单(恒返回原 `taskId` 与当前状态)或重复跑验收(执行中返回进行中提示,已完成直接返回既有报告);同键异参 fail-closed 报错。映射落盘于 `<数据目录>/idempotency.json`,跨 server 重启仍生效。详见 [v0.5.10 发布说明](<docs/release-v0.5.10.md>)。
@@ -183,14 +185,14 @@ run_task(projectPath=D:/xxx/my-app, agentId=qoder, planDoc=./plans/development.m
183
185
 
184
186
  | 工具 | 能力 / 审批 | 作用 |
185
187
  |---|---|---|
186
- | `run_task` | write + 审批 | 派活(可带自动验收/自动返修),异步返回 `taskId`;可选 `idempotencyKey`:同键重试恒返回原 `taskId`,不新建任务 |
188
+ | `run_task` | write + 审批 | 派活(可带自动验收/自动返修),异步返回 `taskId`;可选 `idempotencyKey`(同键重试恒返回原 `taskId`,不新建任务)与 `acceptanceOverride`(任务级临时验收配置覆盖,仅本任务生效) |
187
189
  | `continue_task` | write + 审批 | 恢复 `needs_user` 的原会话(ZCode 恢复原会话;Codex 按 `user_confirmation` 重新观察 / `login_required` 重派;Kimi Code 恢复原会话并区分提问续答 / 重新观察 / 补发任务书) |
188
190
  | `query_task` | read | 轮询状态 / 进度 / 日志尾 / 最近细粒度事件。可选 `eventLimit`(1..50,默认 10)控制 meta 的 `recentEvents` 条数,长任务下可区分「正常执行」与「卡在弹窗等人」 |
189
191
  | `list_tasks` | read | 历史任务过滤列表 |
190
192
  | `get_task_report` | read | 某轮验收报告全文(`report.md`) |
191
193
  | `cancel_task` | write + 审批 | 取消运行中任务:CLI agent kill 进程树;GUI agent 经 CDP 点击停止并在 `gui.cancelWaitMs`(默认 15s)内有界等待 GUI 空闲,未确认停止时终态明示。对已终态的 GUI 任务,本调用兼任人工确认入口——核实窗口无残留运行后调用可清除 `guiStopUnconfirmed` 待确认标记 |
192
194
  | `verify_task` | execute(不改源码,免审批) | 对任务/项目路径做一次验收。能力归 `execute`:会跑项目配置命令、可能产生构建产物,故 MCP `readOnlyHint` 为 `false`——但**不改源码、仍免审批**。可选 `idempotencyKey`:同键重试不重跑(执行中返回进行中提示,已完成返回既有报告) |
193
- | `rework_task` | write + 审批 | 手动返修(把失败报告喂回同一 agent) |
195
+ | `rework_task` | write + 审批 | 手动返修(把失败报告喂回同一 agent)。可选 `repairHint`(≤4000 字符)自带结构化修复提示,以【结构化修复提示】块置于 `feedback` 之前 |
194
196
  | `get_profiles` | read | 查看 agent 适配与可执行探测结果 |
195
197
  | `prepare_visual_baseline` | write + 审批 | 截图或导入参考图,生成待审阅候选和摘要 |
196
198
  | `approve_visual_baseline` | write + 审批 | 用户审阅后校验摘要并写入基准与审批记录 |
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import path from "node:path";
12
12
  import { visualEvidence } from "../../visual/report.js";
13
+ import { renderDirectiveSection } from "../../verify/directives.js";
13
14
  import { mkdirp, writeTextAtomic } from "../../util/fs.js";
14
15
  import { fixPlanRelPath, fixPlanAbsPath } from "./input.js";
15
16
  /** 渲染修复计划正文(含失败证据、通过项、代码分析、修复要求) */
@@ -50,6 +51,7 @@ export function renderCodexFixPlan(input) {
50
51
  lines.push("", "输出尾部:", "```text", (c.outputTail || "(无输出)").slice(-2000), "```", "");
51
52
  });
52
53
  }
54
+ lines.push(renderDirectiveSection(report));
53
55
  lines.push("## 3. 通过的项(勿破坏)", "");
54
56
  if (passed.length === 0)
55
57
  lines.push("(无)", "");
@@ -3,6 +3,7 @@
3
3
  * 同时提供修复指令模板(决策 11/12:修复计划文档由 MCP 自动生成并固定命名)。
4
4
  */
5
5
  import path from "node:path";
6
+ import { renderDirectiveLines } from "../../verify/directives.js";
6
7
  /** 修复计划文档文件名(含轮次号,每轮独立、不覆盖) */
7
8
  export function fixPlanFileName(round) {
8
9
  return `codex-fix-r${round}.md`;
@@ -47,6 +48,9 @@ export function buildFixPrompt(input) {
47
48
  "",
48
49
  input.summary,
49
50
  ];
51
+ if (input.directives?.items.length) {
52
+ lines.push("", "【结构化修复指令(摘要,最多 10 条;完整清单见修复计划文档)】", ...renderDirectiveLines(input.directives, 10));
53
+ }
50
54
  if (input.evidence?.trim())
51
55
  lines.push("", "关键证据:", "```text", input.evidence.trim().slice(-2000), "```");
52
56
  if (input.reportPath)
@@ -0,0 +1,36 @@
1
+ /**
2
+ * 合并两层配置:override 中**显式书写**的字段整体取代 base 的同名字段。
3
+ * `undefined` 视为「本层未书写」,不参与覆盖(否则 `{requireChanges: undefined}` 会误清空下层取值)。
4
+ */
5
+ export function mergeAcceptanceConfig(base, override) {
6
+ const out = { ...(base ?? {}) };
7
+ const target = out;
8
+ for (const [key, value] of Object.entries(override ?? {})) {
9
+ if (value !== undefined)
10
+ target[key] = value;
11
+ }
12
+ return out;
13
+ }
14
+ /** 按数组顺序(低 → 高优先级)依次合并;缺失层直接跳过 */
15
+ export function resolveAcceptanceLayers(layers) {
16
+ let config = {};
17
+ const applied = [];
18
+ for (const layer of layers) {
19
+ if (!layer.config)
20
+ continue;
21
+ config = mergeAcceptanceConfig(config, layer.config);
22
+ applied.push({ source: layer.source, path: layer.path });
23
+ }
24
+ return { config, applied };
25
+ }
26
+ /** 供调试命令/日志使用的一行摘要(不打印任何凭证,只打层与最终取值) */
27
+ export function summarizeResolved(resolved) {
28
+ const c = resolved.config;
29
+ return [
30
+ `生效层=${resolved.applied.map((a) => a.source).join(">") || "(无)"}`,
31
+ `checks=${c.checks ? c.checks.length : "(默认集)"}`,
32
+ `requireChanges=${c.requireChanges ?? "(默认 true)"}`,
33
+ `verifyConcurrency=${c.verifyConcurrency ?? "(继承 server config)"}`,
34
+ `visual=${c.visual ? "已覆盖" : "(未配置)"}`,
35
+ ].join(" ");
36
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * `tianshu-mcp config …` CLI 子命令(issue #20)。
3
+ *
4
+ * 目的:三级继承(全局 / 项目 / 任务级覆盖)生效后,「最终到底用了哪几层、每层贡献了什么」
5
+ * 必须可查——否则排障只能靠猜。输出走 `console.log`,**绝不触碰协议 stdout**
6
+ * (与 `visual/cli.ts` 同一约定;本命令在创建 MCP server 之前返回)。
7
+ *
8
+ * 不挂在 `visual` 命名空间下:`acceptance.json` 是验收引擎的配置,视觉验收只是共用一个文件,
9
+ * 混进 visual 会误导使用者。
10
+ *
11
+ * 用法:
12
+ * tianshu-mcp config acceptance [projectPath] [--task <taskId>]
13
+ */
14
+ import path from "node:path";
15
+ import { readAcceptanceLayer } from "../visual/config.js";
16
+ import { resolveAcceptanceLayers, summarizeResolved } from "./acceptance-merge.js";
17
+ import { resolveDataHome } from "./store.js";
18
+ import { readJsonSafe } from "../util/fs.js";
19
+ export async function runConfigCli(args) {
20
+ const [scope, ...rest] = args;
21
+ if (scope !== "acceptance") {
22
+ console.log(JSON.stringify({
23
+ ok: false,
24
+ error: `未知的 config 子命令: ${scope ?? "(空)"}`,
25
+ usage: [
26
+ "tianshu-mcp config acceptance [projectPath] [--task <taskId>]",
27
+ "",
28
+ "打印验收配置三级继承(全局 / 项目 / 任务级覆盖)的生效情况与最终取值。",
29
+ ],
30
+ }, null, 2));
31
+ return;
32
+ }
33
+ const result = await inspectAcceptance(rest);
34
+ console.log(JSON.stringify(result, null, 2));
35
+ }
36
+ /** 解析 `[projectPath] [--task <taskId>]`(顺序无关,两者都可省) */
37
+ function parseArgs(rest) {
38
+ let projectPath;
39
+ let taskId;
40
+ for (let i = 0; i < rest.length; i++) {
41
+ const a = rest[i];
42
+ if (a === "--task") {
43
+ taskId = rest[i + 1];
44
+ if (!taskId)
45
+ return { error: "--task 需要一个 taskId 参数" };
46
+ i++;
47
+ }
48
+ else if (a.startsWith("--")) {
49
+ return { error: `未知选项: ${a}` };
50
+ }
51
+ else if (projectPath === undefined) {
52
+ projectPath = a;
53
+ }
54
+ else {
55
+ return { error: `多余的参数: ${a}` };
56
+ }
57
+ }
58
+ return { projectPath, taskId };
59
+ }
60
+ /**
61
+ * 收集并解析三层配置。**不抛错**:某层写坏时把错误放进该层的 `error` 字段并让整体
62
+ * `ok: false`,这样调试命令仍然能把「是哪一层坏了」显示出来(而不是只吐一句异常)。
63
+ */
64
+ export async function inspectAcceptance(rest) {
65
+ const parsed = parseArgs(rest);
66
+ if (parsed.error) {
67
+ return { ok: false, error: parsed.error, layers: [], appliedOrder: [], effective: emptyEffective(), summary: "" };
68
+ }
69
+ const home = resolveDataHome();
70
+ const globalPath = path.join(home, "acceptance.default.json");
71
+ const projectPath = parsed.projectPath ? path.resolve(parsed.projectPath) : undefined;
72
+ const projectFile = projectPath ? path.join(projectPath, ".tianshu-mcp", "acceptance.json") : undefined;
73
+ const layers = [];
74
+ let taskError;
75
+ // 层的 label 统一用 source 名(global / project):它与 CLI 输出的 source 字段、
76
+ // 以及验收引擎报错文案里的层名同源,便于按字符串定位是哪一层出了问题。
77
+ const global = await readLayerInto(layers, "global", globalPath);
78
+ const project = projectFile
79
+ ? await readLayerInto(layers, "project", projectFile)
80
+ : (layers.push({ source: "project", path: "(未提供 projectPath)", present: false }),
81
+ undefined);
82
+ // 任务级覆盖来自任务快照(issue #20 把 override 持久化在 TaskMeta 上)
83
+ let override;
84
+ if (parsed.taskId) {
85
+ const metaPath = path.join(home, "tasks", parsed.taskId, "task.json");
86
+ const meta = await readJsonSafe(metaPath);
87
+ if (!meta) {
88
+ layers.push({ source: "override", path: metaPath, present: false });
89
+ taskError = `任务快照不可读或不存在: ${parsed.taskId}`;
90
+ }
91
+ else {
92
+ override = meta.acceptanceOverride;
93
+ layers.push({
94
+ source: "override",
95
+ path: `${metaPath}(TaskMeta.acceptanceOverride)`,
96
+ present: override !== undefined,
97
+ ...(override ? { config: override } : {}),
98
+ });
99
+ }
100
+ }
101
+ else {
102
+ layers.push({ source: "override", path: "(未提供 --task,无任务级覆盖)", present: false });
103
+ }
104
+ const mergedLayers = [
105
+ { source: "global", path: globalPath, config: global },
106
+ { source: "project", path: projectFile ?? "(未提供)", config: project },
107
+ { source: "override", path: "TaskMeta.acceptanceOverride", config: override },
108
+ ];
109
+ const resolved = resolveAcceptanceLayers(mergedLayers);
110
+ const anyLayerError = layers.some((l) => l.error);
111
+ const ok = !anyLayerError && !taskError;
112
+ return {
113
+ ok,
114
+ ...(projectPath ? { project: projectPath } : {}),
115
+ ...(parsed.taskId ? { taskId: parsed.taskId } : {}),
116
+ layers,
117
+ appliedOrder: resolved.applied.map((a) => a.source),
118
+ effective: {
119
+ checks: resolved.config.checks ? resolved.config.checks.length : "(默认集)",
120
+ requireChanges: resolved.config.requireChanges ?? "(默认 true)",
121
+ verifyConcurrency: resolved.config.verifyConcurrency ?? "(继承 server config)",
122
+ visual: resolved.config.visual ? "已覆盖" : "(未配置)",
123
+ },
124
+ summary: summarizeResolved(resolved),
125
+ ...(taskError ? { error: taskError } : {}),
126
+ };
127
+ }
128
+ async function readLayerInto(layers, source, absPath) {
129
+ try {
130
+ const config = await readAcceptanceLayer(absPath, source);
131
+ layers.push({
132
+ source,
133
+ path: absPath,
134
+ present: config !== null,
135
+ ...(config ? { config } : {}),
136
+ });
137
+ return config ?? undefined;
138
+ }
139
+ catch (e) {
140
+ // 该层写坏/读不了:如实登记为错误层,其余层继续解析(调试命令的价值正在于此)
141
+ layers.push({
142
+ source,
143
+ path: absPath,
144
+ present: true,
145
+ error: {
146
+ code: e.code ?? "CONFIG_ERROR",
147
+ message: e instanceof Error ? e.message : String(e),
148
+ },
149
+ });
150
+ return undefined;
151
+ }
152
+ }
153
+ function emptyEffective() {
154
+ return {
155
+ checks: "(默认集)",
156
+ requireChanges: "(默认 true)",
157
+ verifyConcurrency: "(继承 server config)",
158
+ visual: "(未配置)",
159
+ };
160
+ }
@@ -47,6 +47,40 @@ export const IdempotencyScopeSchema = z.enum(["run_task", "verify_task"]);
47
47
  /** 幂等映射默认 TTL(24h)与条目上限(超限逐出最旧)。 */
48
48
  export const IDEMPOTENCY_TTL_DEFAULT_MS = 24 * 60 * 60_000;
49
49
  export const IDEMPOTENCY_MAX_ENTRIES_DEFAULT = 2000;
50
+ /**
51
+ * 验收命令:**推荐数组形态**(结构化 argv,无歧义)。
52
+ * 字符串形态为兼容保留:经极简分词(不经过 shell),**不支持转义**,引号不闭合
53
+ * 不报错,含空格参数必须手写整段引号,写错会静默拆成多个 argv——详见 docs/acceptance-config.md。
54
+ */
55
+ const CheckCmd = z.union([z.string().min(1), z.array(z.string().min(1)).min(1)]);
56
+ export const AcceptanceCheckSchema = z.object({
57
+ name: z.string().min(1),
58
+ cmd: CheckCmd,
59
+ timeoutMs: z.number().int().positive().optional(),
60
+ optional: z.boolean().optional(),
61
+ });
62
+ /**
63
+ * **分层**验收配置(issue #20):每个字段都可选、**任何字段都不带 `.default()`**。
64
+ *
65
+ * 为什么必须与 `AcceptanceConfigSchema` 分开:`requireChanges` 在后者上有 `.default(true)`。
66
+ * 若用带默认值的 schema 去解析「只写了 verifyConcurrency」的项目文件,会 materialize 出
67
+ * `requireChanges: true`,在三级继承里**反过来把全局层的 `false` 覆盖掉** —— 这正是本次要修的隐患。
68
+ * 分层解析一律用本 schema,默认值只在最终生效值缺省时由消费方兜底。
69
+ */
70
+ export const PartialAcceptanceConfigSchema = z.object({
71
+ checks: z.array(AcceptanceCheckSchema).optional(),
72
+ /** 注意:`visual` **不做跨层深合并**,最高优先级的层整段生效(理由见 docs/acceptance-config.md) */
73
+ visual: VisualConfigSchema.optional(),
74
+ requireChanges: z.boolean().optional(),
75
+ verifyConcurrency: z
76
+ .number()
77
+ .transform((n) => (Number.isFinite(n) ? Math.min(4, Math.max(1, Math.round(n))) : 1))
78
+ .optional(),
79
+ });
80
+ /** 兼容形态:在分层 schema 之上补回 `requireChanges` 的历史默认值(供既有 visual/legacy 调用方) */
81
+ export const AcceptanceConfigSchema = PartialAcceptanceConfigSchema.extend({
82
+ requireChanges: z.boolean().default(true),
83
+ });
50
84
  export const RunTaskParamsSchema = z.object({
51
85
  /**
52
86
  * 项目绝对路径。**省略** = 无项目模式(issue #12):目前仅 ZCode 支持——任务在其 `default`
@@ -99,6 +133,12 @@ export const RunTaskParamsSchema = z.object({
99
133
  * 不新建任务;同一键携带不同参数会被 fail-closed 拒绝。不传 = 保持既有行为。
100
134
  */
101
135
  idempotencyKey: IdempotencyKeySchema.optional(),
136
+ /**
137
+ * 任务级临时验收配置覆盖(issue #20):三级继承的最高优先级,**仅当次任务生效**。
138
+ * 会随任务快照保存(属任务数据,不是配置文件),rework/continue 沿用同一任务时继续生效,
139
+ * 不影响其他任务或项目。不传则完全沿用全局/项目层配置。
140
+ */
141
+ acceptanceOverride: PartialAcceptanceConfigSchema.optional(),
102
142
  });
103
143
  /** query_task 返回的细粒度事件条数默认值(issue #18) */
104
144
  export const QUERY_TASK_EVENT_LIMIT_DEFAULT = 10;
@@ -126,18 +166,6 @@ export const CancelTaskParamsSchema = z.object({
126
166
  taskId: z.string().min(1),
127
167
  reason: z.string().optional(),
128
168
  });
129
- /**
130
- * 验收命令:**推荐数组形态**(结构化 argv,无歧义)。
131
- * 字符串形态为兼容保留:经极简分词(不经过 shell),**不支持转义**,引号不闭合
132
- * 不报错,含空格参数必须手写整段引号,写错会静默拆成多个 argv——详见 docs/acceptance-config.md。
133
- */
134
- const CheckCmd = z.union([z.string().min(1), z.array(z.string().min(1)).min(1)]);
135
- export const AcceptanceCheckSchema = z.object({
136
- name: z.string().min(1),
137
- cmd: CheckCmd,
138
- timeoutMs: z.number().int().positive().optional(),
139
- optional: z.boolean().optional(),
140
- });
141
169
  export const VerifyTaskParamsSchema = z.object({
142
170
  taskId: z.string().optional(),
143
171
  projectPath: AbsPath.optional(),
@@ -155,10 +183,18 @@ export const VerifyTaskParamsSchema = z.object({
155
183
  * 已完成直接返回已有报告与轮次;同一键携带不同参数会被 fail-closed 拒绝。
156
184
  */
157
185
  idempotencyKey: IdempotencyKeySchema.optional(),
186
+ /** 任务级临时验收配置覆盖(issue #20):三级继承的最高优先级,仅本次验收生效。 */
187
+ acceptanceOverride: PartialAcceptanceConfigSchema.optional(),
158
188
  });
159
189
  export const ReworkTaskParamsSchema = z.object({
160
190
  taskId: z.string().min(1),
161
191
  feedback: z.string().optional(),
192
+ /**
193
+ * 结构化修复提示(issue #19):调用方自带的一小段「文件 / 行 / 做什么」,会以
194
+ * 【结构化修复提示】块置于 feedback 之前,便于 agent 先精确定位再读整段说明。
195
+ * 自由字符串(上限 4000 字符);不传则行为与既有版本一致。
196
+ */
197
+ repairHint: z.string().max(4000).optional(),
162
198
  });
163
199
  export const ContinueTaskParamsSchema = z.object({
164
200
  taskId: z.string().min(1),
@@ -421,14 +457,5 @@ export const ProjectRecordSchema = z.object({
421
457
  });
422
458
  /** projects.json 整体文件 schema(S5:不再用对象强制类型转换) */
423
459
  export const ProjectsFileSchema = z.record(z.string().min(1), ProjectRecordSchema);
424
- /* ---------------- 项目内 .tianshu-mcp/acceptance.json ---------------- */
425
- export const AcceptanceConfigSchema = z.object({
426
- checks: z.array(AcceptanceCheckSchema).optional(),
427
- visual: VisualConfigSchema.optional(),
428
- requireChanges: z.boolean().default(true),
429
- /** 命令检查并行度:1=串行(与历史行为一致);缺省继承 server config.json 的 verifyConcurrency(默认 2)。越界值 clamp 到 1..4(不再株连整份 acceptance.json 失效) */
430
- verifyConcurrency: z
431
- .number()
432
- .transform((n) => (Number.isFinite(n) ? Math.min(4, Math.max(1, Math.round(n))) : 1))
433
- .optional(),
434
- });
460
+ /* 项目内 .tianshu-mcp/acceptance.json 的 schema 定义见文件上方(须早于 RunTaskParamsSchema,
461
+ 因为 run_task/verify_task 的 acceptanceOverride 参数引用它)。 */
package/dist/index.js CHANGED
@@ -17,6 +17,12 @@ async function main() {
17
17
  await runVisualCli(process.argv.slice(3));
18
18
  return;
19
19
  }
20
+ // issue #20:验收配置三级继承的调试命令(在创建 server 之前返回,不触碰协议 stdout)
21
+ if (process.argv[2] === "config") {
22
+ const { runConfigCli } = await import("./config/cli.js");
23
+ await runConfigCli(process.argv.slice(3));
24
+ return;
25
+ }
20
26
  const home = resolveDataHome();
21
27
  const logger = await Logger.create(path.join(home, "logs"));
22
28
  const skipSkillInstall = process.argv.includes("--no-skill-install") || process.env.TIANSHU_MCP_NO_SKILL_INSTALL === "1";
@@ -10,6 +10,7 @@ import { readTextSafe, exists, readDirSafe, mkdirp, writeJsonAtomic, readJsonSaf
10
10
  import { runChild } from "../agents/spawn.js";
11
11
  import { captureBaseline } from "../verify/git-baseline.js";
12
12
  import { summarizeReport } from "../verify/report.js";
13
+ import { renderDirectiveLines } from "../verify/directives.js";
13
14
  import { writeRepairPlan } from "./repair-plan.js";
14
15
  import { writeCodexFixPlan } from "../agents/codex/fixplan.js";
15
16
  import { buildFixPrompt } from "../agents/codex/input.js";
@@ -257,6 +258,7 @@ export class TaskOrchestrator {
257
258
  planRelPath: plan.relPath,
258
259
  reportPath: verdict.mdPath,
259
260
  evidence: extractFailureEvidence(verdict.report),
261
+ directives: verdict.report.repairDirectives,
260
262
  });
261
263
  logger.info(`[codex] 第 ${roundNo} 轮返修指令已引用修复计划 ${plan.relPath}`);
262
264
  continue;
@@ -279,7 +281,7 @@ export class TaskOrchestrator {
279
281
  if (planReadable == null || reportReadable == null) {
280
282
  return this.finish("failed", "internal", "返修计划或验收报告在任务数据目录中不可读,拒绝发送降级摘要");
281
283
  }
282
- feedback = buildFixFeedback(meta.task, verdict.summary, verdict.mdPath, plan.taskPath);
284
+ feedback = buildFixFeedback(meta.task, verdict.summary, verdict.mdPath, plan.taskPath, verdict.report.repairDirectives);
283
285
  if (meta.agentId === "qoder")
284
286
  feedback += `\n\n修复计划全文(${path.basename(plan.taskPath)}):\n${planReadable}`;
285
287
  continue;
@@ -455,6 +457,8 @@ export class TaskOrchestrator {
455
457
  round,
456
458
  config: await this.deps.dataHome.loadConfig(),
457
459
  projectVerify,
460
+ // issue #20:任务级临时覆盖(三级继承最高优先级),随任务快照持久化
461
+ acceptanceOverride: meta.acceptanceOverride,
458
462
  baseline,
459
463
  signal: this.signal,
460
464
  store,
@@ -478,11 +482,17 @@ export class TaskOrchestrator {
478
482
  };
479
483
  }
480
484
  }
481
- function buildFixFeedback(taskText, verifySummary, reportMd, planPath) {
485
+ function buildFixFeedback(taskText, verifySummary, reportMd, planPath, directives) {
482
486
  const lines = ["【上一轮验收失败反馈 —— 请针对下列失败项定向修复,不要大范围重构】", ""];
483
487
  if (planPath) {
484
488
  lines.push(`修复计划文档:\`${planPath}\`(MCP 任务数据目录绝对路径;请先读取并逐条处理)`, "");
485
489
  }
490
+ // issue #19:把结构化指令摘要直接放进返修消息,省去 agent 从整篇报告里定位的开销。
491
+ // 仅在确有指令时追加;提取失败时**不**在这里说明(计划文档的 2.5 节已如实交代),
492
+ // 避免返修消息被「提取失败」的噪声占据。
493
+ if (directives?.items.length) {
494
+ lines.push("【结构化修复指令(摘要,最多 10 条;完整清单见修复计划文档)】", ...renderDirectiveLines(directives, 10), "");
495
+ }
486
496
  lines.push(verifySummary, "", `完整验收报告:${reportMd}`, "修复完成后正常结束本轮即可。");
487
497
  return lines.join("\n");
488
498
  }
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import path from "node:path";
10
10
  import { visualEvidence } from "../visual/report.js";
11
+ import { renderDirectiveSection } from "../verify/directives.js";
11
12
  import { mkdirp, writeTextAtomic } from "../util/fs.js";
12
13
  /** 生成修复计划 markdown 正文 */
13
14
  export function renderRepairPlan(input) {
@@ -50,6 +51,7 @@ export function renderRepairPlan(input) {
50
51
  lines.push("", "输出尾部:", "```text", (c.outputTail || "(无输出)").slice(-2000), "```", "");
51
52
  });
52
53
  }
54
+ lines.push(renderDirectiveSection(report));
53
55
  lines.push("## 3. 通过的项(勿破坏)", "");
54
56
  if (passed.length === 0)
55
57
  lines.push("(无)", "");