tianshu-mcp 0.6.4 → 0.6.6

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,63 @@ Chinese version: [CHANGELOG.md](CHANGELOG.md)
8
8
 
9
9
  ---
10
10
 
11
+ ## [0.6.6] - 2026-09-24
12
+
13
+ ### Added
14
+
15
+ - **Dry-run mode `dryRun` for `run_task`** ([issue #21](https://github.com/lanlan0811/tianshu-mcp/issues/21)): the agent analyses and plans only — outputting the file list and approach **without touching source** — and the acceptance engine performs static analysis only (do the referenced files exist, do the proposed edit locations exist, any obvious logical conflicts), skipping typecheck/test/build. See [dryRun mode](docs/dry-run.en.md).
16
+ - **`src/verify/dry-run.ts`**: plan schema (`path` / `action` / `reason` / `edits`), the static checks, and the report / plan-document renderers.
17
+ - **A review-then-do loop**: the plan document produced by a dry run lands **inside the project** at `.tianshu-mcp/dry-run-plan-<taskId>.md`, with `meta.dryRunPlanDoc` giving the project-relative path, directly usable as a later real `run_task`'s `planDoc`.
18
+
19
+ ### Changed
20
+
21
+ - **`TaskMeta` gained `dryRun` / `dryRunReportMd` / `dryRunReportJson` / `dryRunPlanMd`**; `metaFromTask` exposes `dryRun` / `dryRunReportFiles` / `dryRunPlanDoc`.
22
+ - Report artifacts are **strictly separated**: a dry run writes `dry-run-report-<round>.md` / `.json` (the JSON carries `kind: "dry-run"`) and **never touches** `report-<round>.*`, so it consumes no acceptance round and does not pollute the regular report list.
23
+
24
+ ### Compatibility
25
+
26
+ - **`dryRun` is a new optional argument, off by default**: when omitted, `run_task` behaves exactly as in v0.6.5.
27
+ - No tool contract or breaking data-model changes; MCP annotations unchanged.
28
+
29
+ ### Notes (disclosed honestly)
30
+
31
+ - **The verdict is `needs_attention`, not `failed`**: a bad plan needs a human decision, not automatic rework; a dry run **never enters the rework loop**, **ignores `autoVerify`** and **requires `projectPath`** (project-less mode rejects it explicitly).
32
+ - **A missing plan degrades but stays visible**: `planExtracted: false` plus a `fallbackReason` stating why, with checks degrading to the zero-change gate only; both the report and the message state "plan extraction: failed" honestly instead of passing silently.
33
+ - **The zero-change gate is the core evidence**: diffing against the pre-work baseline, changes remaining after excluding MCP-owned artifacts (the plan file and any `planDoc` named in the task book) yield `dry_run_violation` (blocking). It **does not depend on the plan being correct** — it still works when the agent produces no plan at all.
34
+ - **The agent is not guaranteed to obey the read-only constraint**: that relies on explicit task-book instructions plus the post-hoc gate. **Violations are caught and reported honestly, but changes that already happened are not rolled back** (the MCP never auto-commits, auto-stashes or auto-checks-out).
35
+ - **Static checks cannot judge whether a plan is sensible**: they only verify "the file exists, the location matches, no obvious contradiction" — which is exactly what the human review step is for.
36
+ - **Adapter differences around `planDoc`**: it is currently consumed only by the **Codex and Qoder CN** prompt builders; CLI agents and ZCode / Kimi Code / TraeWork do not read it, so for those the plan path must go into the `task` text (the file is inside the project, so they can read it). This is documented in both `docs/dry-run` files and the README.
37
+
38
+ ## [0.6.5] - 2026-09-24
39
+
40
+ ### Added
41
+
42
+ - **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).
43
+ - **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.
44
+ - **`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`.
45
+ - New `src/config/acceptance-merge.ts` (a purpose-scoped merge helper, **deliberately not a general-purpose deep merge**).
46
+
47
+ ### Fixed
48
+
49
+ - **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.
50
+
51
+ ### Changed
52
+
53
+ - `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).
54
+ - After the unified `LOCKFILE_PATTERN` export (v0.6.4), the bilingual `acceptance-config` precedence tables were rewritten around the three-level chain.
55
+
56
+ ### Compatibility
57
+
58
+ - **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).
59
+ - 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).
60
+ - The existing semantics of the project-level `.tianshu-mcp/acceptance.json` are unchanged; error semantics remain fail-closed (only `ENOENT` counts as "layer absent").
61
+
62
+ ### Notes (disclosed honestly)
63
+
64
+ - **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".
65
+ - **`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.
66
+ - **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.
67
+
11
68
  ## [0.6.4] - 2026-09-24
12
69
 
13
70
  ### Added
package/CHANGELOG.md CHANGED
@@ -7,6 +7,63 @@
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.6.6] - 2026-09-24
11
+
12
+ ### 新增
13
+
14
+ - **`run_task` 干跑模式 `dryRun`**([issue #21](https://github.com/lanlan0811/tianshu-mcp/issues/21)):agent 只分析规划、输出将要修改的文件清单与方案、**不动源码**;验收引擎只做静态分析(引用文件是否存在、拟改位置是否存在、明显逻辑冲突),跳过 typecheck/test/build。详见 [dryRun 干跑模式](docs/dry-run.md)。
15
+ - **`src/verify/dry-run.ts`**:计划 schema(`path` / `action` / `reason` / `edits`)、静态检查、报告与方案文档渲染。
16
+ - **「先审后做」闭环**:dryRun 产出的方案文档落在**项目内** `.tianshu-mcp/dry-run-plan-<taskId>.md`,`meta.dryRunPlanDoc` 给出项目相对路径,可直接作为后续正式 `run_task` 的 `planDoc`。
17
+
18
+ ### 变更
19
+
20
+ - **`TaskMeta` 新增 `dryRun` / `dryRunReportMd` / `dryRunReportJson` / `dryRunPlanMd`**;`metaFromTask` 暴露 `dryRun` / `dryRunReportFiles` / `dryRunPlanDoc`。
21
+ - 报告产物**严格分离**:dryRun 写 `dry-run-report-<round>.md` / `.json`(json 带 `kind: "dry-run"`),**不碰** `report-<round>.*`,因此不消耗验收轮次、也不污染常规报告列表。
22
+
23
+ ### 兼容性
24
+
25
+ - **`dryRun` 是新增可选参数,默认关闭**:不传时 `run_task` 行为与 v0.6.5 完全一致。
26
+ - 无工具契约、数据模型破坏性变更;MCP 注解不变。
27
+
28
+ ### 说明(如实披露)
29
+
30
+ - **判定为 `needs_attention` 而非 `failed`**:方案有问题属人工裁决,不是可以自动返修的代码缺陷;dryRun **不进入返修循环**、**忽略 `autoVerify`**、**需要 `projectPath`**(无项目模式显式拒绝)。
31
+ - **计划缺失时降级但可见**:`planExtracted: false` + `fallbackReason` 写明原因,检查降级为仅零改动门禁;报告与文案都如实标注「计划提取: 失败」,不静默通过。
32
+ - **零改动门禁是核心证据**:相对动工前基线求差,排除 MCP 自有产物(计划文件、任务书点名的 planDoc)后仍有变更 → `dry_run_violation`(阻断)。它**不依赖计划写对** —— agent 完全不产出计划时这条仍然有效。
33
+ - **不保证 agent 遵守只读约束**:靠任务书里的明确指令 + 事后门禁。**违反会被拦下并如实报告,但已发生的改动不会自动回滚**(MCP 从不自动 commit / stash / checkout)。
34
+ - **静态检查无法判断方案是否合理**:只能验证「文件存在、位置对得上、无明显矛盾」——那正是「先审」要人工做的事。
35
+ - **`planDoc` 的适配器差异**:目前只由 **Codex 与 Qoder CN** 的提示词构造消费;CLI 类 agent 与 ZCode / Kimi Code / TraeWork 不读取它,对这些 agent 需把方案路径写进 `task` 文本(文件在项目内,它们能读)。这一点已写入 `docs/dry-run` 双语与 README。
36
+
37
+ ## [0.6.5] - 2026-09-24
38
+
39
+ ### 新增
40
+
41
+ - **验收配置三级继承**([issue #20](https://github.com/lanlan0811/tianshu-mcp/issues/20)):`<数据目录>/acceptance.default.json`(全局兜底)→ `<project>/.tianshu-mcp/acceptance.json`(项目覆盖)→ 任务级临时覆盖。一个宿主下挂多个同类项目时,公共策略写全局层即可,不必逐项目建文件。详见 [验收配置规范](docs/acceptance-config.md)。
42
+ - **`run_task` / `verify_task` 新增可选 `acceptanceOverride`**:任务级临时验收配置覆盖(格式同 `acceptance.json`),**仅当次生效**、随任务快照保存、不写入任何 `acceptance*.json`、不影响同项目其他任务。无项目模式显式拒绝该参数。
43
+ - **`tianshu-mcp config acceptance [projectPath] [--task <taskId>]` 调试命令**:打印各层是否存在、实际生效顺序与最终取值,排障无需靠猜。每轮验收另往 `server.log` 写一行同源摘要。
44
+ - 新增 `src/config/acceptance-merge.ts`(按用途收敛的合并工具,**刻意不做通用深合并**)。
45
+
46
+ ### 修复
47
+
48
+ - **分层解析的 `.default()` 污染隐患**:`AcceptanceConfigSchema` 的 `requireChanges` 带 `.default(true)`,若用它解析「只写了 `verifyConcurrency`」的项目文件会 materialize 出 `requireChanges: true`,在三级继承里**反过来覆盖全局层的 `false`**。新增无默认值的 `PartialAcceptanceConfigSchema` 供分层解析,默认值只在最终取值缺省时兜底。
49
+
50
+ ### 变更
51
+
52
+ - `resolveChecks()` 改为三级合并,并把合并后的 `visual` 一并返回;`executeVerify` 不再二次读取项目文件(否则 override/全局层的 `visual` 会随项目文件是否存在而改变语义)。
53
+ - `LOCKFILE_PATTERN` 的统一导出(v0.6.4)之后,`acceptance-config` 双语优先级表按三级继承重写。
54
+
55
+ ### 兼容性
56
+
57
+ - **无工具契约、数据模型或 MCP 注解变更**(`acceptanceOverride` 是新增可选参数;`extraChecks` 语义与优先级不变,仍高于基础集)。
58
+ - 不创建全局 `acceptance.default.json` 时,单项目行为与 v0.6.4 完全一致(全局层缺失 = 空配置、不报错)。
59
+ - 项目级 `.tianshu-mcp/acceptance.json` 的既有语义不变;错误语义仍是 fail-closed(仅 `ENOENT` 视为「该层不存在」)。
60
+
61
+ ### 说明(如实披露)
62
+
63
+ - **合并粒度是「字段」**:高优先级层显式书写的字段整体取胜;**数组(`checks`)整体覆盖而非拼接** —— 拼接会让「项目追加一项检查」变成「项目无法移除全局检查」。
64
+ - **`visual` 整体覆盖、不做跨层深合并**:`visual` 的 schema 几乎每个字段都带默认值,深合并会让低优先级层的**显式**取值被高优先级层「未书写、仅因默认值而出现」的字段静默覆盖(与 `requireChanges` 同类的污染)。需要跨层复用视觉配置时请在项目层写完整 `visual` 段。**这是与 issue 建议的「深合并」的一处有意偏离**,理由记录在 `docs/acceptance-config.md` 与 ARCHITECTURE §7.4。
65
+ - **任务级覆盖计入幂等入参摘要**:同键换一套验收策略会被 fail-closed 拒绝,而不是返回策略不同的旧任务。
66
+
10
67
  ## [0.6.4] - 2026-09-24
11
68
 
12
69
  ### 新增
package/README.en.md CHANGED
@@ -41,8 +41,10 @@ Tianshu plays the role of the overall commander; this MCP server is the **schedu
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
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).
44
+ - **Dry-run mode (issue #21)**: `run_task(dryRun=true)` makes the agent **analyse and plan only — outputting the file list and approach without touching source**; the acceptance engine then does static analysis only (do the referenced files exist, do the proposed edit locations exist, any obvious logical conflicts) and skips typecheck/test/build. A bad plan yields `needs_attention` (a human decision), never enters automatic rework, and **consumes no acceptance round**. Two separate artifacts: a static-analysis report plus a plan document inside the project (`meta.dryRunPlanDoc`, directly usable as a later real task's `planDoc`), forming a review-then-do loop. See [dryRun mode](docs/dry-run.en.md).
44
45
  - **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`).
45
46
  - **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).
47
+ - **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).
46
48
  - **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.
47
49
  - **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.
48
50
  - **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).
@@ -188,7 +190,7 @@ run_task(projectPath=D:/xxx/my-app, agentId=qoder, planDoc=./plans/development.m
188
190
 
189
191
  | Tool | Capability / approval | Purpose |
190
192
  |---|---|---|
191
- | `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 |
193
+ | `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), `acceptanceOverride` (transient acceptance-config override applying only to this task) and `dryRun` (dry-run mode: plan only without touching source; its artifact feeds a later `planDoc`) |
192
194
  | `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) |
193
195
  | `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" |
194
196
  | `list_tasks` | read | Filtered history of tasks |
package/README.md CHANGED
@@ -42,8 +42,10 @@
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` 等天枢裁决。
46
47
  - **结构化修复指令(issue #19)**:失败轮次会把原因解析为**可直接执行的动作**(`文件:行 / 问题 / 做什么`),随返修计划与返修消息一起喂给 agent,省去它从整篇报告里定位的开销;提取不到时**显式回退**到完整报告(不静默留空)。`rework_task` 另可选 `repairHint` 自带提示。详见 [结构化修复指令](docs/repair-directives.md)。
48
+ - **干跑模式 dryRun(issue #21)**:`run_task(dryRun=true)` 让 agent **只分析规划、输出将要修改的文件清单与方案、不动源码**;验收引擎只做静态分析(引用文件是否存在、拟改位置是否存在、明显逻辑冲突),跳过 typecheck/test/build。方案有问题 → `needs_attention`(人工裁决),不进入自动返修、**不消耗验收轮次**。产物分两份:静态分析报告 + 项目内的方案文档(`meta.dryRunPlanDoc`,可直接作为后续正式任务的 `planDoc`),构成「先审后做」闭环。详见 [dryRun 干跑模式](docs/dry-run.md)。
47
49
  - **执行面**:`driver: "gui"` 由显式 adapter 驱动桌面 UI(Codex / TraeWork / ZCode / Kimi Code 各自使用隔离的 CDP 流程);`driver: "spawn"` 走外部 CLI 子进程。
48
50
  - **无项目派发(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)。
49
51
  - **幂等重试(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>)。
@@ -184,7 +186,7 @@ run_task(projectPath=D:/xxx/my-app, agentId=qoder, planDoc=./plans/development.m
184
186
 
185
187
  | 工具 | 能力 / 审批 | 作用 |
186
188
  |---|---|---|
187
- | `run_task` | write + 审批 | 派活(可带自动验收/自动返修),异步返回 `taskId`;可选 `idempotencyKey`:同键重试恒返回原 `taskId`,不新建任务 |
189
+ | `run_task` | write + 审批 | 派活(可带自动验收/自动返修),异步返回 `taskId`;可选 `idempotencyKey`(同键重试恒返回原 `taskId`,不新建任务)、`acceptanceOverride`(任务级临时验收配置覆盖,仅本任务生效)与 `dryRun`(干跑模式:只分析规划不改源码,产物可作后续 `planDoc`) |
188
190
  | `continue_task` | write + 审批 | 恢复 `needs_user` 的原会话(ZCode 恢复原会话;Codex 按 `user_confirmation` 重新观察 / `login_required` 重派;Kimi Code 恢复原会话并区分提问续答 / 重新观察 / 补发任务书) |
189
191
  | `query_task` | read | 轮询状态 / 进度 / 日志尾 / 最近细粒度事件。可选 `eventLimit`(1..50,默认 10)控制 meta 的 `recentEvents` 条数,长任务下可区分「正常执行」与「卡在弹窗等人」 |
190
192
  | `list_tasks` | read | 历史任务过滤列表 |
@@ -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,21 @@ 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(),
142
+ /**
143
+ * 干跑模式(issue #21):agent **只分析、只规划**,输出将要修改的文件清单与方案,
144
+ * 不动源码;验收引擎只做静态分析(引用文件是否存在、拟改位置是否存在、明显逻辑冲突),
145
+ * 跳过 typecheck/test/build 等需要实际改动的检查。
146
+ *
147
+ * 默认关闭(缺省 = 与既有行为完全一致)。**需要 projectPath**:无项目模式没有可静态分析的基线。
148
+ * dryRun 下忽略 `autoVerify`、不进入自动返修。
149
+ */
150
+ dryRun: z.boolean().optional(),
102
151
  });
103
152
  /** query_task 返回的细粒度事件条数默认值(issue #18) */
104
153
  export const QUERY_TASK_EVENT_LIMIT_DEFAULT = 10;
@@ -126,18 +175,6 @@ export const CancelTaskParamsSchema = z.object({
126
175
  taskId: z.string().min(1),
127
176
  reason: z.string().optional(),
128
177
  });
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
178
  export const VerifyTaskParamsSchema = z.object({
142
179
  taskId: z.string().optional(),
143
180
  projectPath: AbsPath.optional(),
@@ -155,6 +192,8 @@ export const VerifyTaskParamsSchema = z.object({
155
192
  * 已完成直接返回已有报告与轮次;同一键携带不同参数会被 fail-closed 拒绝。
156
193
  */
157
194
  idempotencyKey: IdempotencyKeySchema.optional(),
195
+ /** 任务级临时验收配置覆盖(issue #20):三级继承的最高优先级,仅本次验收生效。 */
196
+ acceptanceOverride: PartialAcceptanceConfigSchema.optional(),
158
197
  });
159
198
  export const ReworkTaskParamsSchema = z.object({
160
199
  taskId: z.string().min(1),
@@ -427,14 +466,5 @@ export const ProjectRecordSchema = z.object({
427
466
  });
428
467
  /** projects.json 整体文件 schema(S5:不再用对象强制类型转换) */
429
468
  export const ProjectsFileSchema = z.record(z.string().min(1), ProjectRecordSchema);
430
- /* ---------------- 项目内 .tianshu-mcp/acceptance.json ---------------- */
431
- export const AcceptanceConfigSchema = z.object({
432
- checks: z.array(AcceptanceCheckSchema).optional(),
433
- visual: VisualConfigSchema.optional(),
434
- requireChanges: z.boolean().default(true),
435
- /** 命令检查并行度:1=串行(与历史行为一致);缺省继承 server config.json 的 verifyConcurrency(默认 2)。越界值 clamp 到 1..4(不再株连整份 acceptance.json 失效) */
436
- verifyConcurrency: z
437
- .number()
438
- .transform((n) => (Number.isFinite(n) ? Math.min(4, Math.max(1, Math.round(n))) : 1))
439
- .optional(),
440
- });
469
+ /* 项目内 .tianshu-mcp/acceptance.json 的 schema 定义见文件上方(须早于 RunTaskParamsSchema,
470
+ 因为 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";
@@ -6,11 +6,12 @@
6
6
  *
7
7
  * 并发闸/队列由 TaskManager 负责;这里只负责单任务推进(支持 AbortSignal 中止)。
8
8
  */
9
- import { readTextSafe, exists, readDirSafe, mkdirp, writeJsonAtomic, readJsonSafe, } from "../util/fs.js";
9
+ import { readTextSafe, exists, readDirSafe, mkdirp, writeJsonAtomic, readJsonSafe, writeTextAtomic, } from "../util/fs.js";
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
13
  import { renderDirectiveLines } from "../verify/directives.js";
14
+ import { DRY_RUN_PLAN_REL_PATH, dryRunPlanDocRelPath, renderDryRunPlanDoc, } from "../verify/dry-run.js";
14
15
  import { writeRepairPlan } from "./repair-plan.js";
15
16
  import { writeCodexFixPlan } from "../agents/codex/fixplan.js";
16
17
  import { buildFixPrompt } from "../agents/codex/input.js";
@@ -189,6 +190,47 @@ export class TaskOrchestrator {
189
190
  if (runRes.killed) {
190
191
  return this.abortTerminal(runRes.guiStop);
191
192
  }
193
+ // ---- issue #21:dryRun 只做静态分析 ----
194
+ // 放在这里(agent 已返回、正常验收之前)的理由:先让 agent 跑完只读预演与计划产出,
195
+ // 再静态核对;硬失败/超时/取消/等人已在上面处理,不会走到这里。
196
+ // dryRun 刻意**忽略 autoVerify**(它本来就是「先审」而不是「验收」),也**不进返修循环**
197
+ // —— 方案有问题属人工决策,不是可以自动返修的代码缺陷。
198
+ if (meta.dryRun) {
199
+ const dryReport = await this.deps.engine.runDryRun({
200
+ taskId: meta.taskId,
201
+ projectPath: meta.projectPath,
202
+ round,
203
+ baseline,
204
+ taskText: meta.task,
205
+ // 允许 agent 写的产物:计划文件本身,以及任务书里点名的 planDoc(若有)
206
+ allowedArtifacts: [
207
+ DRY_RUN_PLAN_REL_PATH,
208
+ ...(meta.planDoc ? [meta.planDoc] : []),
209
+ ],
210
+ store,
211
+ });
212
+ meta.dryRunReportMd = store.dryRunReportMdPath(meta.taskId, round);
213
+ meta.dryRunReportJson = store.dryRunReportJsonPath(meta.taskId, round);
214
+ // 计划渲染成项目内 markdown,供后续正式任务作 planDoc 复用(「先审后做」闭环)
215
+ if (dryReport.plan) {
216
+ const rel = dryRunPlanDocRelPath(meta.taskId);
217
+ const abs = path.join(meta.projectPath, ...rel.split("/"));
218
+ await mkdirp(path.dirname(abs));
219
+ await writeTextAtomic(abs, renderDryRunPlanDoc(dryReport));
220
+ meta.dryRunPlanMd = abs;
221
+ }
222
+ const tail = [
223
+ dryReport.summary,
224
+ `静态分析报告:${meta.dryRunReportMd}`,
225
+ meta.dryRunPlanMd
226
+ ? `方案文档(可作为后续正式任务的 planDoc):${dryRunPlanDocRelPath(meta.taskId)}`
227
+ : "(未取得结构化计划,无法作为 planDoc 复用)",
228
+ ].join("\n");
229
+ // 方案有问题 → needs_attention(人工裁决),不是 failed(并非代码缺陷)
230
+ return dryReport.passed
231
+ ? this.finish("succeeded", null, tail)
232
+ : this.finish("needs_attention", "verify_failed", tail);
233
+ }
192
234
  if (!runRes.ok && !meta.autoVerify) {
193
235
  return this.finish("failed", "agent_failed", runRes.error ??
194
236
  `agent 执行失败(exit=${runRes.exitCode ?? "n/a"})。日志 ${runRes.logFile}`);
@@ -457,6 +499,8 @@ export class TaskOrchestrator {
457
499
  round,
458
500
  config: await this.deps.dataHome.loadConfig(),
459
501
  projectVerify,
502
+ // issue #20:任务级临时覆盖(三级继承最高优先级),随任务快照持久化
503
+ acceptanceOverride: meta.acceptanceOverride,
460
504
  baseline,
461
505
  signal: this.signal,
462
506
  store,
@@ -1,3 +1,28 @@
1
+ /**
2
+ * dryRun 的只读预演约束(issue #21)。
3
+ *
4
+ * 注入点刻意选在这里而不是各个适配器的 prompt builder:全部 5 个适配器的提示词构造
5
+ * 都会拼 `ctx.context`(codex `input.ts`、traework `ui/composer.ts`、cli `cli.ts`、
6
+ * zcode/kimicode `run.ts`),因此**一次改动即覆盖全部适配器**;且 dryRun 是 round 0 的
7
+ * 首次派发,zcode/kimicode 仅在 `initialDispatch` 时附加 context 的守卫不会把它吞掉。
8
+ */
9
+ export const DRY_RUN_CONSTRAINT = [
10
+ "【本任务是 dryRun 只读预演 —— 必须严格遵守】",
11
+ "",
12
+ "1. **不得创建、修改或删除任何源文件**;不要运行会改动工作区的命令(如格式化、自动修复、构建产物写入)。",
13
+ " 允许且**必须**写入的唯一文件是下面第 2 条要求的计划文件。",
14
+ "2. 必须把修改方案写成机器可读的计划文件:`.tianshu-mcp/dry-run-plan.json`(相对项目根),格式:",
15
+ " ```json",
16
+ ' { "summary": "一句话方案", "files": [ { "path": "src/foo.ts", "action": "modify",',
17
+ ' "reason": "为什么要改", "edits": [ { "line": 42, "symbol": "functionName", "action": "怎么改" } ] } ] }',
18
+ " ```",
19
+ ' `path` 必须是**项目相对路径**(不得写绝对路径、不得包含 `..`、不得指向 `.git` 或 `node_modules`);',
20
+ ' `action` 取值为 `create` / `modify` / `delete`;`edits` 可选,但写了就要与文件真实内容对得上',
21
+ " (行号必须在文件范围内、`symbol` 必须在文件中真实存在)—— 验收引擎会逐条静态核对。",
22
+ "3. 验收引擎只做静态分析(文件是否存在、拟改位置是否存在、是否有明显逻辑冲突),",
23
+ " **不跑 typecheck / test / build**。因此请不要在回复里声称测试已通过。",
24
+ "4. 在回复里用自然语言说明方案要点即可;**不要**开始实施改动。",
25
+ ].join("\n");
1
26
  export function makeBuildCtx(services) {
2
27
  return (meta, round, feedback) => ({
3
28
  taskId: meta.taskId,
@@ -6,7 +31,10 @@ export function makeBuildCtx(services) {
6
31
  displayPath: meta.displayPath,
7
32
  agentId: meta.agentId,
8
33
  task: meta.task,
9
- context: meta.context,
34
+ // dryRun:把只读预演约束并入 context(既有的用户 context 保留在前)
35
+ context: meta.dryRun
36
+ ? [meta.context, DRY_RUN_CONSTRAINT].filter((s) => s && s.trim() !== "").join("\n\n")
37
+ : meta.context,
10
38
  model: meta.model,
11
39
  reasoningLevel: meta.reasoningLevel,
12
40
  modelSource: meta.modelSource,