tianshu-mcp 0.6.3 → 0.6.4

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,32 @@ Chinese version: [CHANGELOG.md](CHANGELOG.md)
8
8
 
9
9
  ---
10
10
 
11
+ ## [0.6.4] - 2026-09-24
12
+
13
+ ### Added
14
+
15
+ - **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).
16
+ - **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).
17
+ - **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`.
18
+
19
+ ### Changed
20
+
21
+ - **`report-<round>.md` gained a `## Structured repair directives` section**; `report-<round>.json` gained a `repairDirectives` field (**failed rounds only**).
22
+ - **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).
23
+ - `LOCKFILE_PATTERN` is now exported from `code-analysis.ts` so the analysis warnings and the extractor **share one list**, preventing drift between two copies.
24
+
25
+ ### Compatibility
26
+
27
+ - **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.
28
+ - Extraction runs **only on failed acceptance rounds**; passing rounds do not produce the field (no report bloat).
29
+
30
+ ### Notes (disclosed honestly)
31
+
32
+ - **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.
33
+ - **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.
34
+ - **`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.
35
+ - **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.
36
+
11
37
  ## [0.6.3] - 2026-09-24
12
38
 
13
39
  ### Added
package/CHANGELOG.md CHANGED
@@ -7,6 +7,32 @@
7
7
 
8
8
  ---
9
9
 
10
+ ## [0.6.4] - 2026-09-24
11
+
12
+ ### 新增
13
+
14
+ - **结构化修复指令 `repairDirectives`**([issue #19](https://github.com/lanlan0811/tianshu-mcp/issues/19)):失败轮次把验收失败原因解析为**可直接执行的动作**(`file? / line? / issue / action / source`),随返修计划、返修消息一起喂给 agent,省去它从整篇报告里定位「哪一行类型不匹配、哪个文件有 TODO」的开销。详见 [结构化修复指令](docs/repair-directives.md)。
15
+ - **两个内置提取来源**:`typecheck`(解析失败类型检查项输出尾部的 tsc pretty / plain 两式报错,绝对路径归一化为项目相对 posix 路径,同处报错去重)与 `diffstat`(超大单文件改动、被改动的锁文件、TODO / 调试输出 / 疑似密钥的行级计数)。
16
+ - **`rework_task` 新增可选 `repairHint`**(自由字符串,上限 4000 字符):调用方自带结构化修复提示,在下一轮任务书的 `【结构化修复提示】` 块中**排在 `feedback` 之前**。
17
+
18
+ ### 变更
19
+
20
+ - **`report-<round>.md` 新增 `## 结构化修复指令` 小节**;`report-<round>.json` 新增 `repairDirectives` 字段(**仅失败轮次**)。
21
+ - **两块返修计划(通用 `rework-*.md` 与 Codex `codex-fix-r*.md`)新增 `## 2.5 结构化修复指令` 小节**,位于第 2 节(失败项)与第 3 节(通过项)之间。
22
+ - `LOCKFILE_PATTERN` 由 `code-analysis.ts` 导出,供分析告警与提取器**共用一份清单**,避免两处漂移。
23
+
24
+ ### 兼容性
25
+
26
+ - **无工具契约、数据模型或 MCP 注解变更**。`repairDirectives` 是报告内的新增可选字段,读取方按缺省忽略即可;`repairHint` 不传时 `rework_task` 行为与 v0.6.3 完全一致。
27
+ - 提取**只在验收失败的轮次**执行;通过的轮次不产出该字段(不徒增报告体积)。
28
+
29
+ ### 说明(如实披露)
30
+
31
+ - **提取不到时显式回退**:`fallbackReason` 非空 ⇒ 渲染方写明「不可用,回退完整报告」并要求 agent 回到完整失败输出,**不允许静默留空**。单个来源抛错会被吞掉并记入原因,其余来源继续工作 —— 提取器永不抛错。
32
+ - **测试类失败不做提取**:测试框架输出没有稳定的文件/行号,强行解析会产出**错误**定位,比不给更糟。
33
+ - **`diffstat` 的行级信号不伪造位置**:`signals.ts` 只做计数、无稳定文件与行号,故对应指令省略 `file` 字段。
34
+ - **已知限制**:`outputTail` 被截断到最后 4000 字符,大型项目只能提取到尾部类型错误,其余靠回退兜底 —— 有意接受的取舍。
35
+
10
36
  ## [0.6.3] - 2026-09-24
11
37
 
12
38
  ### 新增
package/README.en.md CHANGED
@@ -40,6 +40,7 @@ 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).
45
46
  - **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.
@@ -194,7 +195,7 @@ run_task(projectPath=D:/xxx/my-app, agentId=qoder, planDoc=./plans/development.m
194
195
  | `get_task_report` | read | Full text of a verification round's report (`report.md`) |
195
196
  | `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
197
  | `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) |
198
+ | `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
199
  | `get_profiles` | read | Inspect agent adapters and executable discovery results |
199
200
  | `prepare_visual_baseline` | write + approval | Capture or import reference images into a reviewable candidate with a digest |
200
201
  | `approve_visual_baseline` | write + approval | Validate the reviewed digest and write the baseline and approval record |
package/README.md CHANGED
@@ -43,6 +43,7 @@
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
45
  - **失败返修闭环**:自动返修(`autoFixRounds`)+ 手动 `rework_task`;验收失败时自动生成修复计划文件并回填给 agent;轮次用尽 → `needs_attention` 等天枢裁决。
46
+ - **结构化修复指令(issue #19)**:失败轮次会把原因解析为**可直接执行的动作**(`文件:行 / 问题 / 做什么`),随返修计划与返修消息一起喂给 agent,省去它从整篇报告里定位的开销;提取不到时**显式回退**到完整报告(不静默留空)。`rework_task` 另可选 `repairHint` 自带提示。详见 [结构化修复指令](docs/repair-directives.md)。
46
47
  - **执行面**:`driver: "gui"` 由显式 adapter 驱动桌面 UI(Codex / TraeWork / ZCode / Kimi Code 各自使用隔离的 CDP 流程);`driver: "spawn"` 走外部 CLI 子进程。
47
48
  - **无项目派发(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
49
  - **幂等重试(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>)。
@@ -190,7 +191,7 @@ run_task(projectPath=D:/xxx/my-app, agentId=qoder, planDoc=./plans/development.m
190
191
  | `get_task_report` | read | 某轮验收报告全文(`report.md`) |
191
192
  | `cancel_task` | write + 审批 | 取消运行中任务:CLI agent kill 进程树;GUI agent 经 CDP 点击停止并在 `gui.cancelWaitMs`(默认 15s)内有界等待 GUI 空闲,未确认停止时终态明示。对已终态的 GUI 任务,本调用兼任人工确认入口——核实窗口无残留运行后调用可清除 `guiStopUnconfirmed` 待确认标记 |
192
193
  | `verify_task` | execute(不改源码,免审批) | 对任务/项目路径做一次验收。能力归 `execute`:会跑项目配置命令、可能产生构建产物,故 MCP `readOnlyHint` 为 `false`——但**不改源码、仍免审批**。可选 `idempotencyKey`:同键重试不重跑(执行中返回进行中提示,已完成返回既有报告) |
193
- | `rework_task` | write + 审批 | 手动返修(把失败报告喂回同一 agent) |
194
+ | `rework_task` | write + 审批 | 手动返修(把失败报告喂回同一 agent)。可选 `repairHint`(≤4000 字符)自带结构化修复提示,以【结构化修复提示】块置于 `feedback` 之前 |
194
195
  | `get_profiles` | read | 查看 agent 适配与可执行探测结果 |
195
196
  | `prepare_visual_baseline` | write + 审批 | 截图或导入参考图,生成待审阅候选和摘要 |
196
197
  | `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)
@@ -159,6 +159,12 @@ export const VerifyTaskParamsSchema = z.object({
159
159
  export const ReworkTaskParamsSchema = z.object({
160
160
  taskId: z.string().min(1),
161
161
  feedback: z.string().optional(),
162
+ /**
163
+ * 结构化修复提示(issue #19):调用方自带的一小段「文件 / 行 / 做什么」,会以
164
+ * 【结构化修复提示】块置于 feedback 之前,便于 agent 先精确定位再读整段说明。
165
+ * 自由字符串(上限 4000 字符);不传则行为与既有版本一致。
166
+ */
167
+ repairHint: z.string().max(4000).optional(),
162
168
  });
163
169
  export const ContinueTaskParamsSchema = z.object({
164
170
  taskId: z.string().min(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;
@@ -478,11 +480,17 @@ export class TaskOrchestrator {
478
480
  };
479
481
  }
480
482
  }
481
- function buildFixFeedback(taskText, verifySummary, reportMd, planPath) {
483
+ function buildFixFeedback(taskText, verifySummary, reportMd, planPath, directives) {
482
484
  const lines = ["【上一轮验收失败反馈 —— 请针对下列失败项定向修复,不要大范围重构】", ""];
483
485
  if (planPath) {
484
486
  lines.push(`修复计划文档:\`${planPath}\`(MCP 任务数据目录绝对路径;请先读取并逐条处理)`, "");
485
487
  }
488
+ // issue #19:把结构化指令摘要直接放进返修消息,省去 agent 从整篇报告里定位的开销。
489
+ // 仅在确有指令时追加;提取失败时**不**在这里说明(计划文档的 2.5 节已如实交代),
490
+ // 避免返修消息被「提取失败」的噪声占据。
491
+ if (directives?.items.length) {
492
+ lines.push("【结构化修复指令(摘要,最多 10 条;完整清单见修复计划文档)】", ...renderDirectiveLines(directives, 10), "");
493
+ }
486
494
  lines.push(verifySummary, "", `完整验收报告:${reportMd}`, "修复完成后正常结束本轮即可。");
487
495
  return lines.join("\n");
488
496
  }
@@ -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("(无)", "");
@@ -947,14 +947,18 @@ function reworkTaskHandler(ctx) {
947
947
  const meta = await manager.getMeta(args.taskId);
948
948
  if (!meta)
949
949
  return errorResult(`任务不存在: ${args.taskId}`);
950
- const res = await manager.rework(args.taskId, args.feedback);
950
+ const res = await manager.rework(args.taskId, args.feedback, args.repairHint);
951
951
  if (!res.found)
952
952
  return errorResult(res.reason ?? `无法 rework ${args.taskId}`);
953
953
  const m = await manager.getMeta(args.taskId);
954
954
  if (!m)
955
955
  return errorResult(`任务不存在: ${args.taskId}`);
956
+ const extras = [
957
+ args.feedback ? "带追加指示" : "",
958
+ args.repairHint ? `带结构化修复提示(${args.repairHint.length} 字符)` : "",
959
+ ].filter((s) => s !== "");
956
960
  const lines = [
957
- `任务 ${args.taskId} 已重新入队(手动返修)${args.feedback ? ",带追加指示" : ""}。`,
961
+ `任务 ${args.taskId} 已重新入队(手动返修)${extras.length ? `,${extras.join(",")}` : ""}。`,
958
962
  `当前状态: ${m.status},已用轮次 ${m.roundsUsed}。`,
959
963
  `请用 query_task(${args.taskId}) 轮询新一轮结果。`,
960
964
  ];
package/dist/mcp/tools.js CHANGED
@@ -80,7 +80,10 @@ export const TOOL_DEFS = [
80
80
  },
81
81
  {
82
82
  name: "rework_task",
83
- description: "手动返修:把终态任务(failed/needs_attention)重新入队续跑,同一 agent/项目与轮次记账。feedback 为追加指示(建议带上一次验收失败摘要)。",
83
+ description: "手动返修:把终态任务(failed/needs_attention)重新入队续跑,同一 agent/项目与轮次记账。" +
84
+ "feedback 为追加指示(建议带上一次验收失败摘要)。" +
85
+ "repairHint 为可选的结构化修复提示(自由字符串,上限 4000 字符)——写「文件:行 / 问题 / 做什么」," +
86
+ "会以【结构化修复提示】块置于 feedback 之前,便于 agent 先精确定位再读整段说明;不传则行为不变。",
84
87
  inputSchema: ReworkTaskParamsSchema,
85
88
  capability: "write",
86
89
  requireApproval: true,
@@ -199,7 +199,7 @@ export class TaskManager {
199
199
  * rework_task:对终态任务(failed/needs_attention/甚至 succeeded)手动续跑。
200
200
  * 复用原 agent/项目/验收设置与轮次记账,feedback 作用于下一轮 agent。
201
201
  */
202
- async rework(taskId, feedback) {
202
+ async rework(taskId, feedback, repairHint) {
203
203
  const meta = (await this.getMeta(taskId)) ?? undefined;
204
204
  if (!meta)
205
205
  return { found: false, reason: `任务不存在: ${taskId}` };
@@ -219,11 +219,13 @@ export class TaskManager {
219
219
  meta.updatedAt = nowIso();
220
220
  meta.finishedAt = undefined;
221
221
  meta.reworkFeedback = feedback?.trim() || undefined;
222
+ meta.reworkHint = repairHint?.trim() || undefined;
222
223
  // 类型化事件(issue #18):手动返修是引擎侧节点,事件名与自动返修统一为 rework_triggered,
223
224
  // 便于调用方用同一条规则观察「返修是否被触发」;mode 区分人工 / 自动。
225
+ const hintSuffix = meta.reworkHint ? `,结构化提示 ${meta.reworkHint.length} 字符` : "";
224
226
  await this.store.appendEvent(meta.taskId, "rework_triggered", "queued", meta.reworkFeedback
225
- ? `rework 请求,追加指示: ${meta.reworkFeedback.slice(0, 200)}`
226
- : "rework 请求(无追加指示)", { mode: "manual" });
227
+ ? `rework 请求,追加指示: ${meta.reworkFeedback.slice(0, 200)}${hintSuffix}`
228
+ : `rework 请求(无追加指示${meta.reworkHint ? hintSuffix.replace(",", ";") : ""})`, { mode: "manual" });
227
229
  await this.store.writeSnapshot(meta);
228
230
  this.tasks.set(taskId, meta);
229
231
  this.enqueue(meta);
@@ -542,11 +544,21 @@ export class TaskManager {
542
544
  // 必须在启动时清空,而不是运行结束后的收尾里——终态快照先落盘,调用方看到
543
545
  // failed 后可立即 rework_task 写入新的 reworkFeedback,而上一轮的收尾 delete
544
546
  // 会把这条新反馈一起抹掉,导致返修轮拿不到指示(实测负载下偶发)。
547
+ // reworkHint(issue #19)与 reworkFeedback 同批取走,避免出现「只清了一半」的窗口。
545
548
  const reworkFeedback = meta.reworkFeedback;
546
- if (reworkFeedback) {
549
+ const reworkHint = meta.reworkHint;
550
+ if (reworkFeedback || reworkHint) {
547
551
  delete meta.reworkFeedback;
552
+ delete meta.reworkHint;
548
553
  await this.store.writeSnapshot(meta);
549
554
  }
555
+ // 结构化修复提示排在用户反馈之前:先给出精确定位,再给整段说明。
556
+ const initialFeedback = [
557
+ reworkHint ? `【结构化修复提示】\n${reworkHint}` : "",
558
+ reworkFeedback ?? "",
559
+ ]
560
+ .filter((s) => s !== "")
561
+ .join("\n\n");
550
562
  // 超时兜底(R2):runChild 在 meta.taskTimeoutMs 处自行 kill 并返回 timeout → orchestrator 落 failed(timeout)。
551
563
  // 此 guard 只在非子进程阶段(resolve/编排卡死)长时间未返回时兜底,附一小段有文档说明的 kill grace。
552
564
  const KILL_GRACE_MS = 15_000;
@@ -569,7 +581,7 @@ export class TaskManager {
569
581
  engine: this.engine,
570
582
  logger: this.logger,
571
583
  buildCtx: this.buildCtx,
572
- }, meta, ac.signal, reworkFeedback);
584
+ }, meta, ac.signal, initialFeedback || undefined);
573
585
  const result = await orch.run();
574
586
  // 防御:以持久化 meta 为准 —— orchestrator 返回 status 与持久化 status 不一致时告警。
575
587
  const persisted = (await this.store.readSnapshot(meta.taskId)) ?? meta;
@@ -17,6 +17,7 @@ import { exists, mkdirp, readJsonSafe, readTextSafe } from "../util/fs.js";
17
17
  import { toAcceptanceDef } from "../config/store.js";
18
18
  import { runVerifyCommand, makeSkipResult } from "./runner.js";
19
19
  import { analyzeChanges } from "./code-analysis.js";
20
+ import { extractRepairDirectives } from "./directives.js";
20
21
  import { captureBaseline, gitDiffCheckSince } from "./git-baseline.js";
21
22
  import { nowIso } from "../util/id.js";
22
23
  const SCRIPT_NAMES = ["typecheck", "lint", "test", "build"];
@@ -388,6 +389,11 @@ export class AcceptanceEngine {
388
389
  ...(blockingIssues.length ? { blockingIssues } : {}),
389
390
  ...(visual ? { visual } : {}),
390
391
  };
392
+ // issue #19:把失败原因解析为可直接执行的指令。**通过的轮次不提取**(没有要修的东西,
393
+ // 徒增报告体积);失败的轮次一律挂载——即使只得到 fallbackReason,渲染方也要如实说明
394
+ // 「结构化指令不可用,请回退完整报告」,而不是假装没有这个能力。
395
+ if (!passed)
396
+ report.repairDirectives = extractRepairDirectives(report);
391
397
  await this.store.saveReport(req.taskId, report);
392
398
  this.logger.info(`任务 ${req.taskId} 第 ${req.round} 轮验收: ${passed ? "通过" : "失败"}(${checks.length} 项检查)`);
393
399
  return { report, passed };
@@ -13,7 +13,8 @@ const BIG_FILE_THRESHOLD = 500;
13
13
  const HASH_MAX_BYTES = 4 * 1024 * 1024;
14
14
  /** 文件读取/探测的有界并发上限(worker 池,与 acceptance.ts 命令检查同构) */
15
15
  const FILE_IO_CONCURRENCY = 8;
16
- const LOCKFILE_PATTERN = /(^|\/)(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|Cargo\.lock|go\.sum|Pipfile\.lock|poetry\.lock|composer\.lock)$/;
16
+ /** 锁文件形态(供分析告警与 issue #19 的结构化修复指令共用,避免两处清单漂移) */
17
+ export const LOCKFILE_PATTERN = /(^|\/)(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|Cargo\.lock|go\.sum|Pipfile\.lock|poetry\.lock|composer\.lock)$/;
17
18
  const BINARY_EXT = new Set([
18
19
  ".png", ".jpg", ".jpeg", ".gif", ".webp", ".ico", ".bmp",
19
20
  ".zip", ".gz", ".tar", ".7z", ".rar",
@@ -0,0 +1,190 @@
1
+ /**
2
+ * 结构化修复指令提取(issue #19)。
3
+ *
4
+ * 背景:验收失败时的返修报告是**整篇叙述**,agent 需要自己从报告里定位具体问题
5
+ * (哪一行类型不匹配、哪个文件有 TODO、哪个文件改动行数异常),既增加推理开销,
6
+ * 也提高理解偏差导致返修失败的概率。本模块把失败原因解析为**可直接执行的动作**:
7
+ *
8
+ * { file: "src/foo.ts", line: 42, issue: "TS2322: 类型不匹配", action: "修正该处类型错误(依据 TS2322 提示)" }
9
+ *
10
+ * 设计要点:
11
+ * - 仓库内**没有** per-verifier 模块(typecheck/test/build 都是通用 argv 命令检查),
12
+ * 因此 source 按「检查项 name / argv 启发式」+ 报告内的结构化分析结果匹配。
13
+ * - **永不抛错**:提取失败以数据形式表达(`fallbackReason`),渲染方据此**回退整份报告**。
14
+ * 这是 issue 要求的鲁棒性兜底 —— 提取器出问题绝不能反而让返修失去上下文。
15
+ * - 已知限制:`CheckResult.outputTail` 被截断到最后 4000 字符(runner.ts),大型项目
16
+ * 只能提取到尾部报错;提取不到即自然回退。
17
+ */
18
+ import path from "node:path";
19
+ import { toPosix } from "../util/fs.js";
20
+ import { LOCKFILE_PATTERN } from "./code-analysis.js";
21
+ /** 检查项是否属于类型检查(按 name + argv 启发式,与 isTestCheck 同构) */
22
+ function isTypecheckCheck(name, cmd) {
23
+ return /typecheck|tsc|--noEmit|mypy|pyright/i.test(`${name} ${cmd}`);
24
+ }
25
+ /** 把可能是绝对的路径归一化为「项目相对 posix 路径」 */
26
+ function toProjectRelative(file, projectPath) {
27
+ const trimmed = file.trim();
28
+ if (!trimmed)
29
+ return trimmed;
30
+ if (path.isAbsolute(trimmed)) {
31
+ const rel = path.relative(projectPath, trimmed);
32
+ // 落在项目外的路径保留原样(绝对),不做无法验证的裁剪
33
+ return rel && !rel.startsWith("..") ? toPosix(rel) : toPosix(trimmed);
34
+ }
35
+ return toPosix(trimmed);
36
+ }
37
+ /** tsc 人性化输出:`src/foo.ts(42,5): error TS2322: Type 'x' is not assignable…` */
38
+ const TS_PRETTY = /^(.+?)\((\d+),(\d+)\):\s*error\s+(TS\d+):\s*(.+)$/;
39
+ /** tsc --pretty false:`src/foo.ts:42:5 - error TS2322: Type 'x' is not assignable…` */
40
+ const TS_PLAIN = /^(.+?):(\d+):(\d+)\s+-\s+error\s+(TS\d+):\s*(.+)$/;
41
+ const typecheckSource = {
42
+ id: "typecheck",
43
+ extract(report) {
44
+ const out = [];
45
+ const seen = new Set();
46
+ const checks = report.checks.filter((c) => !c.passed && !c.skipped && isTypecheckCheck(c.name, c.cmd));
47
+ for (const check of checks) {
48
+ for (const rawLine of check.outputTail.split("\n")) {
49
+ const line = rawLine.trim();
50
+ if (!line)
51
+ continue;
52
+ const m = TS_PRETTY.exec(line) ?? TS_PLAIN.exec(line);
53
+ if (!m)
54
+ continue;
55
+ const [, file, lineNo, , code, message] = m;
56
+ const rel = toProjectRelative(file, report.projectPath);
57
+ const key = `${rel}:${lineNo}:${code}:${message}`;
58
+ if (seen.has(key))
59
+ continue;
60
+ seen.add(key);
61
+ out.push({
62
+ file: rel,
63
+ line: Number(lineNo),
64
+ issue: `${code}: ${message.trim()}`,
65
+ action: `修正该处类型错误(依据 ${code} 提示)`,
66
+ source: "typecheck",
67
+ });
68
+ }
69
+ }
70
+ return out;
71
+ },
72
+ };
73
+ const diffstatSource = {
74
+ id: "diffstat",
75
+ extract(report) {
76
+ const a = report.analysis;
77
+ const out = [];
78
+ for (const f of a.bigFileChanges) {
79
+ out.push({
80
+ file: toPosix(f),
81
+ issue: "单文件改动过大(>500 行)",
82
+ action: "拆分改动或确认如此大范围改动确有必要",
83
+ source: "diffstat",
84
+ });
85
+ }
86
+ for (const f of [...a.changedFiles, ...a.untrackedFiles]) {
87
+ const p = toPosix(f);
88
+ if (LOCKFILE_PATTERN.test(p)) {
89
+ out.push({
90
+ file: p,
91
+ issue: "锁文件被修改",
92
+ action: "确认依赖变更是有意的;若非有意请还原该锁文件",
93
+ source: "diffstat",
94
+ });
95
+ }
96
+ }
97
+ // 行级信号没有稳定的文件/行号(signals.ts 只做计数),故产出文件无关的指令
98
+ const sig = a.signals;
99
+ if (sig.todo > 0) {
100
+ out.push({
101
+ issue: `新增/变更行含 TODO/FIXME/HACK 共 ${sig.todo} 处`,
102
+ action: "实现或移除这些待办标记",
103
+ source: "diffstat",
104
+ });
105
+ }
106
+ if (sig.consoleDebug > 0) {
107
+ out.push({
108
+ issue: `新增/变更行含 console.log/debugger 共 ${sig.consoleDebug} 处`,
109
+ action: "移除调试输出",
110
+ source: "diffstat",
111
+ });
112
+ }
113
+ if (sig.secretLike > 0) {
114
+ out.push({
115
+ issue: `新增/变更行含疑似密钥/令牌形态 ${sig.secretLike} 处`,
116
+ action: "改为从环境变量或配置读取,不要硬编码凭证",
117
+ source: "diffstat",
118
+ });
119
+ }
120
+ return out;
121
+ },
122
+ };
123
+ /**
124
+ * 提取器注册表。新增验收器支持时在此追加 —— 每个 source 只依赖报告内的结构化数据,
125
+ * 因此可以独立单测(不需要真的跑命令)。
126
+ */
127
+ export const DIRECTIVE_SOURCES = [typecheckSource, diffstatSource];
128
+ /**
129
+ * 从一轮验收报告提取结构化修复指令。
130
+ * **绝不抛错**:单个 source 异常只记录到 `fallbackReason`,其余 source 继续工作。
131
+ */
132
+ export function extractRepairDirectives(report) {
133
+ const items = [];
134
+ const sources = [];
135
+ const errors = [];
136
+ for (const src of DIRECTIVE_SOURCES) {
137
+ try {
138
+ const got = src.extract(report);
139
+ if (got.length) {
140
+ items.push(...got);
141
+ sources.push(src.id);
142
+ }
143
+ }
144
+ catch (e) {
145
+ errors.push(`${src.id}: ${e instanceof Error ? e.message : String(e)}`);
146
+ }
147
+ }
148
+ const out = { items, sources };
149
+ if (items.length === 0) {
150
+ out.fallbackReason = errors.length
151
+ ? `结构化提取失败(${errors.join(";")})`
152
+ : "本轮失败原因无法解析为可直接执行的指令(如测试类失败无稳定的文件/行号)";
153
+ }
154
+ return out;
155
+ }
156
+ /** 渲染为面向 agent 的紧凑指令块(最多 maxItems 条) */
157
+ export function renderDirectiveLines(directives, maxItems = 10) {
158
+ return directives.items.slice(0, maxItems).map((d) => {
159
+ const loc = d.file ? `\`${d.file}${d.line ? `:${d.line}` : ""}\`` : "(无具体文件)";
160
+ return `- ${loc} — ${d.issue} → ${d.action}`;
161
+ });
162
+ }
163
+ /**
164
+ * 返修计划里的「2.5 结构化修复指令」小节(通用 + Codex 两套渲染共用,避免文案漂移)。
165
+ *
166
+ * **回退是显式且自觉的**:没有可用指令时不留空段,而是写明原因并明确要求 agent 回到
167
+ * 完整失败输出定位问题 —— 让「提取失败」成为一个可观测的事实,而不是静默降级。
168
+ */
169
+ export function renderDirectiveSection(report) {
170
+ const d = report.repairDirectives;
171
+ if (d?.items.length) {
172
+ return [
173
+ "## 2.5 结构化修复指令(可直接执行)",
174
+ "",
175
+ ...d.items.map((it) => {
176
+ const loc = it.file ? `\`${it.file}${it.line ? `:${it.line}` : ""}\`` : "(无具体文件)";
177
+ return `- ${loc} — ${it.issue} → **${it.action}**`;
178
+ }),
179
+ "",
180
+ ].join("\n");
181
+ }
182
+ return [
183
+ "## 2.5 结构化修复指令(不可用,回退完整报告)",
184
+ "",
185
+ `原因:${d?.fallbackReason ?? "本轮报告未生成结构化指令"}`,
186
+ "",
187
+ "> 请**阅读第 2 节的完整失败输出**自行定位问题,不要依赖本节的省略形式。",
188
+ "",
189
+ ].join("\n");
190
+ }
@@ -14,6 +14,9 @@ export function reportToJsonable(report) {
14
14
  message: report.message,
15
15
  ...(report.blockingIssues ? { blockingIssues: report.blockingIssues } : {}),
16
16
  ...(report.visual ? { visual: report.visual } : {}),
17
+ // issue #19:持久化结构化修复指令。手动返修路径(fix-loop 的 qoder 分支)会重读 report.json,
18
+ // 且跨 server 重启后仍要能拿到指令,故必须落盘而非仅存内存。
19
+ ...(report.repairDirectives ? { repairDirectives: report.repairDirectives } : {}),
17
20
  };
18
21
  }
19
22
  export function reportToMd(report) {
@@ -77,6 +80,16 @@ export function reportToMd(report) {
77
80
  }
78
81
  for (const n of a.notes)
79
82
  L.push(`- [INFO] ${n}`);
83
+ L.push("", "## 结构化修复指令", "");
84
+ if (report.repairDirectives?.items.length) {
85
+ L.push(...report.repairDirectives.items.map((d) => {
86
+ const loc = d.file ? `\`${d.file}${d.line ? `:${d.line}` : ""}\`` : "(无具体文件)";
87
+ return `- ${loc} — ${d.issue} → ${d.action}${d.source ? `(来源 ${d.source})` : ""}`;
88
+ }), "");
89
+ }
90
+ else {
91
+ L.push(`(不可用,请改看上方各检查项的输出尾部)原因:${report.repairDirectives?.fallbackReason ?? "本轮报告未生成结构化指令"}`, "");
92
+ }
80
93
  L.push("", visualEvidence(report), "", "---", "", report.message, "");
81
94
  return L.join("\n");
82
95
  }
@@ -4,4 +4,4 @@
4
4
  * 本文件由 scripts/sync-version.mjs 在每次 build 前重新生成。
5
5
  */
6
6
  // generated: 勿手改 —— 运行 `npm run build` 自动同步
7
- export const MCP_SERVER_VERSION = "0.6.3";
7
+ export const MCP_SERVER_VERSION = "0.6.4";
@@ -0,0 +1,103 @@
1
+ # Structured repair directives (issue #19)
2
+
3
+ Chinese version: [repair-directives.md](repair-directives.md)
4
+
5
+ ## 1. Why this exists
6
+
7
+ When acceptance fails, the MCP generates a repair plan and feeds it back to the agent. But that
8
+ report is a **full narrative** — the agent has to locate by itself "which line has the type
9
+ mismatch, which file has a TODO, which file has an anomalous changed-line count". That raises its
10
+ reasoning cost and increases the chance that a misreading makes the rework fail.
11
+
12
+ Structured repair directives parse the failure reasons straight into **executable actions**:
13
+
14
+ ```
15
+ - `src/foo.ts:42` — TS2322: Type 'string' is not assignable to type 'number'. → 修正该处类型错误(依据 TS2322 提示)
16
+ - `package-lock.json` — 锁文件被修改 → 确认依赖变更是有意的;若非有意请还原该锁文件
17
+ - (无具体文件) — 新增/变更行含 TODO/FIXME/HACK 共 3 处 → 实现或移除这些待办标记
18
+ ```
19
+
20
+ Each directive is `{ file?, line?, issue, action, source }`: `issue` says **what** is wrong, `action`
21
+ says **what to do**.
22
+
23
+ ## 2. Extractors: two built-in sources
24
+
25
+ Extraction happens inside the acceptance engine (`extractRepairDirectives()` in
26
+ `src/verify/directives.ts`) and **only on failed rounds** — a passing round has nothing to fix, so
27
+ extracting there would only bloat the report.
28
+
29
+ | Source | Input | Output |
30
+ |---|---|---|
31
+ | `typecheck` | The `outputTail` of failed checks whose name/argv matches `typecheck\|tsc\|--noEmit\|mypy\|pyright` | One directive per `file(line,col): error TSxxxx` / `file:line:col - error TSxxxx` line (absolute paths normalized to project-relative POSIX; duplicates collapsed) |
32
+ | `diffstat` | The report's `analysis` section | One directive each for oversized single-file changes (>500 lines) and modified lockfiles (`package-lock.json` / `yarn.lock` / `pnpm-lock.yaml` / `Cargo.lock` / `go.sum` / `Pipfile.lock` / `poetry.lock` / `composer.lock`); plus one each for line-level signal counts (TODO/FIXME, console.log/debugger, secret-like patterns) |
33
+
34
+ > **Why there is no "test checker" source**: test-failure command output has no stable file/line
35
+ > (every framework formats differently). Parsing it anyway would produce **wrong** locations, which
36
+ > is worse than producing none. Those failures always take the fallback path (below).
37
+ >
38
+ > The `diffstat` line-level signals (TODO / debug output / secret-like) are **counts only** with no
39
+ > stable file or line, so their directives deliberately **omit** `file` — locations are never faked.
40
+
41
+ ## 3. Fallback: when extraction fails, fall back to the full report
42
+
43
+ **This is the robustness floor of the capability**: the extractors never throw, and failure is
44
+ expressed as data in `fallbackReason`:
45
+
46
+ ```jsonc
47
+ // report-<round>.json
48
+ "repairDirectives": {
49
+ "items": [],
50
+ "sources": [],
51
+ "fallbackReason": "本轮失败原因无法解析为可直接执行的指令(如测试类失败无稳定的文件/行号)"
52
+ }
53
+ ```
54
+
55
+ Renderers (the repair plan and the rework message) therefore **state the unavailability
56
+ explicitly** instead of silently leaving a gap:
57
+
58
+ ```markdown
59
+ ## 2.5 结构化修复指令(不可用,回退完整报告)
60
+
61
+ 原因:本轮失败原因无法解析为可直接执行的指令(如测试类失败无稳定的文件/行号)
62
+
63
+ > 请**阅读第 2 节的完整失败输出**自行定位问题,不要依赖本节的省略形式。
64
+ ```
65
+
66
+ If a single source throws, the error is swallowed and recorded in `fallbackReason` while the
67
+ **other sources keep working** — one broken extractor must not strip the rework of all context.
68
+
69
+ ## 4. Three consumption paths
70
+
71
+ | Carrier | Content |
72
+ |---|---|
73
+ | `report-<round>.json` | The full `repairDirectives` field (persisted: needed across server restarts and by the manual-rework path, which re-reads this file) |
74
+ | `report-<round>.md` | A `## 结构化修复指令` section (each item annotated with its source) |
75
+ | Repair plan doc (`rework-<id>-r<n>.md` / Codex's `codex-fix-r<n>.md`) | A `## 2.5 结构化修复指令` section, inserted between section 2 (failures) and section 3 (passing checks) |
76
+ | Rework message (the body sent to the agent) | A `【结构化修复指令(摘要,最多 10 条)】` block; when extraction fails it is **not** added here (the plan doc's section 2.5 already states it honestly) |
77
+
78
+ ## 5. `rework_task`'s `repairHint`
79
+
80
+ Besides the engine's automatic extraction, the caller can supply its own hint:
81
+
82
+ ```jsonc
83
+ rework_task(taskId, feedback="请按提示修复后重跑验收。",
84
+ repairHint="src/done.txt:1 — 内容应为 PASS 而非 TODO → 把该行改为 PASS")
85
+ ```
86
+
87
+ - A free-form string, **max 4000 characters** (rejected at the protocol layer beyond that).
88
+ - Rendered in the next round's task book as a `【结构化修复提示】` block, placed **before**
89
+ `feedback` — precise locations first, the longer explanation after.
90
+ - When omitted, behaviour is exactly as in previous versions.
91
+
92
+ ## 6. Known limitations (disclosed honestly)
93
+
94
+ - **`outputTail` truncation**: a check's output tail is truncated to the last **4000 characters**
95
+ (`src/verify/runner.ts`). A large TypeScript project's total error count can far exceed that, so
96
+ **only the tail errors are extractable**; what cannot be extracted does not appear out of nowhere —
97
+ go back to the full report. This limitation is accepted deliberately: rather than inflating report
98
+ size to extract more, the fallback path carries the burden.
99
+ - **Path normalization**: paths relative to the project root are kept as-is; an absolute path inside
100
+ the project becomes relative, while one outside the project is **kept as-is** (no unverifiable
101
+ trimming).
102
+ - **Directives are a to-do list, not proof of a fix**: they describe what to do; whether it is
103
+ actually fixed is still decided by the next acceptance round.
@@ -0,0 +1,93 @@
1
+ # 结构化修复指令(issue #19)
2
+
3
+ 英文版:[repair-directives.en.md](repair-directives.en.md)
4
+
5
+ ## 一、为什么需要它
6
+
7
+ 验收失败时,MCP 会生成返修计划并回填给 agent。但返修报告偏向**完整叙述**——agent 需要自己
8
+ 从整份报告里定位「哪一行类型不匹配、哪个文件有 TODO、哪个文件改动行数异常」。这既增加它的
9
+ 推理开销,也提高了理解偏差导致返修失败的概率。
10
+
11
+ 结构化的修复指令把失败原因直接解析为**可执行动作**:
12
+
13
+ ```
14
+ - `src/foo.ts:42` — TS2322: Type 'string' is not assignable to type 'number'. → 修正该处类型错误(依据 TS2322 提示)
15
+ - `package-lock.json` — 锁文件被修改 → 确认依赖变更是有意的;若非有意请还原该锁文件
16
+ - (无具体文件) — 新增/变更行含 TODO/FIXME/HACK 共 3 处 → 实现或移除这些待办标记
17
+ ```
18
+
19
+ 每条指令形如 `{ file?, line?, issue, action, source }`:`issue` 说明**是什么**,`action` 说明**做什么**。
20
+
21
+ ## 二、提取器:两个内置来源
22
+
23
+ 提取发生在验收引擎内部(`src/verify/directives.ts` 的 `extractRepairDirectives()`),
24
+ **只在验收失败的轮次**执行——通过的轮次没有要修的东西,不提取以免徒增报告体积。
25
+
26
+ | 来源 | 输入 | 产出 |
27
+ |---|---|---|
28
+ | `typecheck` | 失败检查项中 name/argv 命中 `typecheck\|tsc\|--noEmit\|mypy\|pyright` 的 `outputTail` | 每条 `file(line,col): error TSxxxx` / `file:line:col - error TSxxxx` 解析为一条指令(绝对路径归一化为项目相对 posix 路径;同一处报错去重) |
29
+ | `diffstat` | 报告的 `analysis` 段 | 超大单文件改动(>500 行)、被改动的锁文件(`package-lock.json` / `yarn.lock` / `pnpm-lock.yaml` / `Cargo.lock` / `go.sum` / `Pipfile.lock` / `poetry.lock` / `composer.lock`)各一条;以及行级信号统计(TODO/FIXME、console.log/debugger、疑似密钥形态)各一条 |
30
+
31
+ > **为什么没有「test 检查器」来源**:测试类失败的命令行输出没有稳定的文件/行号(不同测试框架
32
+ > 格式各异),强行解析会产出**错误**的定位,比不给更糟。这类失败一律走回退路径(见下)。
33
+ >
34
+ > `diffstat` 的行级信号(TODO / 调试输出 / 疑似密钥)只做**计数**,没有稳定的文件与行号,
35
+ > 因此对应指令**不带** `file` 字段 —— 不伪造定位。
36
+
37
+ ## 三、回退:提取不到就退回整份报告
38
+
39
+ **这是本能力的鲁棒性底线**:提取器永不抛错,失败以数据形式表达在 `fallbackReason` 里。
40
+
41
+ ```jsonc
42
+ // report-<round>.json
43
+ "repairDirectives": {
44
+ "items": [],
45
+ "sources": [],
46
+ "fallbackReason": "本轮失败原因无法解析为可直接执行的指令(如测试类失败无稳定的文件/行号)"
47
+ }
48
+ ```
49
+
50
+ 渲染方(返修计划、返修消息)据此**显式声明不可用**,而不是静默留空:
51
+
52
+ ```markdown
53
+ ## 2.5 结构化修复指令(不可用,回退完整报告)
54
+
55
+ 原因:本轮失败原因无法解析为可直接执行的指令(如测试类失败无稳定的文件/行号)
56
+
57
+ > 请**阅读第 2 节的完整失败输出**自行定位问题,不要依赖本节的省略形式。
58
+ ```
59
+
60
+ 单个来源抛错时会被吞掉、记入 `fallbackReason`,**其余来源继续工作**——一个提取器写坏了不该
61
+ 让返修彻底失去上下文。
62
+
63
+ ## 四、三步消费路径
64
+
65
+ | 载体 | 内容 |
66
+ |---|---|
67
+ | `report-<round>.json` | `repairDirectives` 完整字段(持久化:跨 server 重启、以及手动返修路径会重读该文件) |
68
+ | `report-<round>.md` | `## 结构化修复指令` 小节(每条带来源标注) |
69
+ | 返修计划文档(`rework-<id>-r<n>.md` / Codex 的 `codex-fix-r<n>.md`) | `## 2.5 结构化修复指令` 小节,插在第 2 节(失败项)与第 3 节(通过项)之间 |
70
+ | 返修消息(发给 agent 的正文) | `【结构化修复指令(摘要,最多 10 条)】` 块;提取失败时**不**在此处加噪声(计划文档的 2.5 节已如实交代) |
71
+
72
+ ## 五、`rework_task` 的 `repairHint`
73
+
74
+ 除了引擎自动提取,调用方也可以自带一条提示:
75
+
76
+ ```jsonc
77
+ rework_task(taskId, feedback="请按提示修复后重跑验收。",
78
+ repairHint="src/done.txt:1 — 内容应为 PASS 而非 TODO → 把该行改为 PASS")
79
+ ```
80
+
81
+ - 自由字符串,**上限 4000 字符**(超出由协议层拒绝)。
82
+ - 在下一轮任务书里以 `【结构化修复提示】` 块渲染,并**排在 `feedback` 之前** —— 先给精确定位,再给整段说明。
83
+ - 不传时行为与既有版本完全一致。
84
+
85
+ ## 六、已知限制(如实披露)
86
+
87
+ - **`outputTail` 截断**:检查项的输出尾部被截断到最后 **4000 字符**(`src/verify/runner.ts`)。
88
+ 大型 TypeScript 项目的类型错误总量可能远超此数,因此**只能提取到尾部错误**;提取不到的部分
89
+ 不会凭空出现——请回看完整报告。这是有意接受的限制:与其为了提取而放大报告体积,
90
+ 不如让回退路径承担兜底。
91
+ - **路径归一化**:相对项目根的路径原样保留;绝对路径若落在项目内则转为相对,若落在项目外
92
+ 则**原样保留**(不做无法验证的裁剪)。
93
+ - **指令是「待办清单」不是「已修复证明」**:它只描述要做什么,修没修好仍由下一轮验收判定。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tianshu-mcp",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "天枢 × AI-Agent 编排 MCP server —— 驱动 Codex、TraeWork、ZCode、Kimi Code 与 Qoder CN 完成项目开发、验收、失败返修与再验收闭环。",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -21,6 +21,8 @@
21
21
  "assets",
22
22
  "docs/event-stream.md",
23
23
  "docs/event-stream.en.md",
24
+ "docs/repair-directives.md",
25
+ "docs/repair-directives.en.md",
24
26
  "docs/visual-acceptance.md",
25
27
  "docs/visual-acceptance.en.md",
26
28
  "docs/visual-validation.md",