thincoder 0.12.59 → 0.12.60

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.
Files changed (127) hide show
  1. package/CHANGELOG.md +38 -3
  2. package/README.md +2 -2
  3. package/bin/thincoder.mjs +80 -19
  4. package/package.json +4 -3
  5. package/src/acp/bridge.mjs +7 -4
  6. package/src/advisor/messages.mjs +24 -4
  7. package/src/advisor/run.mjs +35 -33
  8. package/src/advisor.mjs +25 -6
  9. package/src/agent/completion.mjs +17 -11
  10. package/src/agent/dispatch.mjs +102 -19
  11. package/src/agent/helpers.mjs +36 -0
  12. package/src/agent/record-results.mjs +46 -10
  13. package/src/agent/run-stages.mjs +227 -0
  14. package/src/agent/setup-reminders.mjs +62 -0
  15. package/src/agent/setup.mjs +18 -2
  16. package/src/agent/spawn-child.mjs +29 -4
  17. package/src/agent-tools/advisor-async.mjs +456 -0
  18. package/src/agent-tools/advisor.mjs +110 -108
  19. package/src/agent-tools/async-settle.mjs +191 -0
  20. package/src/agent-tools/consult.mjs +121 -102
  21. package/src/agent-tools/design-token.mjs +104 -0
  22. package/src/agent-tools/eng.mjs +24 -29
  23. package/src/agent-tools/escalate-async.mjs +286 -0
  24. package/src/agent-tools/read-history.mjs +155 -31
  25. package/src/agent-tools/recent-changes.mjs +2 -1
  26. package/src/agent-tools/settings.mjs +7 -17
  27. package/src/agent-tools/subagent-actions.mjs +168 -130
  28. package/src/agent-tools/subagent-async.mjs +129 -174
  29. package/src/agent-tools/subagent-panel.mjs +153 -0
  30. package/src/agent-tools/subagent-run.mjs +202 -0
  31. package/src/agent-tools/subagent-scheduler.mjs +45 -21
  32. package/src/agent-tools/subagent-spawn.mjs +406 -0
  33. package/src/agent-tools/subagent.mjs +107 -555
  34. package/src/agent-tools/verify.mjs +118 -270
  35. package/src/agent.mjs +57 -190
  36. package/src/cli/distill-command.mjs +10 -4
  37. package/src/cli/make-agent.mjs +3 -1
  38. package/src/cli/memory-command.mjs +2 -1
  39. package/src/cli/permission.mjs +2 -2
  40. package/src/cli/setup-wizard.mjs +17 -12
  41. package/src/config.mjs +56 -8
  42. package/src/context.mjs +5 -147
  43. package/src/crash-reports.mjs +123 -0
  44. package/src/distill.mjs +11 -11
  45. package/src/explore-distill.mjs +155 -0
  46. package/src/memory/code-sync.mjs +2 -1
  47. package/src/memory/core.mjs +6 -193
  48. package/src/memory/delete.mjs +234 -0
  49. package/src/memory/docs.mjs +58 -48
  50. package/src/memory.mjs +3 -1
  51. package/src/peer-domains.mjs +265 -0
  52. package/src/peer-instances.mjs +231 -0
  53. package/src/prompt-overlays.mjs +25 -0
  54. package/src/prompts/advisor-design.md +9 -76
  55. package/src/prompts/advisor-round1.md +9 -68
  56. package/src/prompts/advisor-round2.md +7 -54
  57. package/src/prompts/advisor-round3.md +7 -54
  58. package/src/prompts/coder.md +7 -50
  59. package/src/prompts/consult-base.md +4 -24
  60. package/src/prompts/discipline.md +26 -44
  61. package/src/prompts/eng-coder.md +7 -32
  62. package/src/prompts/engineering-sub.md +3 -23
  63. package/src/prompts/engineering.md +53 -306
  64. package/src/prompts/explore.md +3 -12
  65. package/src/prompts/main.md +10 -32
  66. package/src/prompts/methodology-template.md +28 -48
  67. package/src/prompts/plan.md +2 -9
  68. package/src/prompts/system.md +16 -35
  69. package/src/provider/core.mjs +6 -67
  70. package/src/provider/errors.mjs +76 -0
  71. package/src/provider/retry.mjs +8 -45
  72. package/src/session-gc.mjs +214 -0
  73. package/src/session-guard.mjs +47 -0
  74. package/src/session-rename.mjs +38 -0
  75. package/src/session-slots.mjs +181 -58
  76. package/src/session.mjs +48 -89
  77. package/src/token-ttl.mjs +273 -0
  78. package/src/tools/checklist-sync.mjs +181 -0
  79. package/src/tools/checklist.mjs +52 -39
  80. package/src/tools/edit-batch.mjs +109 -10
  81. package/src/tools/edit-diff.mjs +110 -27
  82. package/src/tools/edit.md +17 -12
  83. package/src/tools/execute.mjs +31 -4
  84. package/src/tools/file.mjs +11 -6
  85. package/src/tools/git.mjs +14 -6
  86. package/src/tools/glob-dialect.mjs +130 -0
  87. package/src/tools/glob.md +3 -3
  88. package/src/tools/grep.md +1 -1
  89. package/src/tools/index.mjs +5 -6
  90. package/src/tools/ops.mjs +175 -3
  91. package/src/tools/patch.mjs +3 -3
  92. package/src/tools/question.md +3 -0
  93. package/src/tools/read.md +0 -1
  94. package/src/tools/shared.mjs +14 -13
  95. package/src/tools/system.mjs +44 -9
  96. package/src/tools/wait_for.md +22 -0
  97. package/src/tui/agent-turn.mjs +17 -228
  98. package/src/tui/cmd-config.mjs +48 -7
  99. package/src/tui/cmd-eng.mjs +20 -16
  100. package/src/tui/cmd-mcp.mjs +8 -2
  101. package/src/tui/cmd-new.mjs +3 -2
  102. package/src/tui/cmd-session.mjs +19 -4
  103. package/src/tui/cmd-think.mjs +10 -10
  104. package/src/tui/cmd-upgrade.mjs +19 -4
  105. package/src/tui/config-helpers.mjs +28 -16
  106. package/src/tui/distill-cmd.mjs +1 -1
  107. package/src/tui/index.mjs +3 -2
  108. package/src/tui/interaction.mjs +3 -3
  109. package/src/tui/mouse.mjs +7 -1
  110. package/src/tui/pickers.mjs +40 -22
  111. package/src/tui/render-segments.mjs +27 -10
  112. package/src/tui/startup.mjs +4 -0
  113. package/src/tui/subagent-blocks.mjs +95 -263
  114. package/src/tui/subagent-children.mjs +176 -0
  115. package/src/tui/subagent-freeze.mjs +172 -0
  116. package/src/tui/subagent-panel.mjs +61 -23
  117. package/src/tui/suspension-drive.mjs +351 -0
  118. package/src/tui/tool-args.mjs +3 -3
  119. package/src/tui/tool-display.mjs +142 -0
  120. package/src/tui/tool-events.mjs +37 -173
  121. package/src/tui/tui-lifecycle.mjs +29 -0
  122. package/src/tui/update-notice.mjs +4 -0
  123. package/src/tui/wizard.mjs +12 -6
  124. package/src/tools/pdf-parse-text.mjs +0 -497
  125. package/src/tools/pdf-parse-xref.mjs +0 -499
  126. package/src/tools/pdf.mjs +0 -155
  127. package/src/tools/read_pdf.md +0 -21
@@ -0,0 +1,406 @@
1
+ /**
2
+ * subagent-spawn.mjs — spawn 路径装配(2026-09-05 module-split:subagent.mjs 726
3
+ * > 500 硬限——spawn 前置 helpers(summarizeEngTaskBook/effectiveSubagentModel/
4
+ * resolveDesignSlot)+ §20 调度参数准入(prepareScheduling)+ child 装配
5
+ * (buildSpawnChild)verbatim 迁入(仅闭包变量参数化),语义零变;executeAsyncSpawn
6
+ * 另在 subagent-run.mjs。subagent.mjs execute 经本文件 import 调用。
7
+ * 2026-09-07:executeConsumeDesignAction 消费执行器(token 链终消费制——与 spawn 侧
8
+ * slot 族同域——ENGINEERING-MODE.md §2.6 F1——removeDesignTokenSlot 自 token-ttl.mjs)。
9
+ */
10
+
11
+ import {
12
+ createAgent,
13
+ readonlyToolNames, escapeXml,
14
+ EXPLORE_OVERLAY, CODER_OVERLAY, PLAN_OVERLAY, ENG_CODER_OVERLAY,
15
+ } from "../agent.mjs"
16
+ import { makeRelay, wrapChildCallbacks } from "../agent/spawn-child.mjs"
17
+ import { validateDesignToken } from "./advisor.mjs"
18
+ import { tokenExpired, removeDesignTokenSlot, reconcileEngTokensFromSlot, persistEngTokens } from "../token-ttl.mjs"
19
+ import { resolveChildProvider, buildChildRunOpts } from "./subagent-async.mjs"
20
+ import {
21
+ normalizeFileList, describeBlockers, assertNoDepCycle, depInfo,
22
+ } from "./subagent-scheduler.mjs"
23
+
24
+ /**
25
+ * §18.7 D-TS5 (A2): mechanically summarize the parent spawn task book for the
26
+ * audit spawn — the three audit-relevant elements VERBATIM (design doc paths /
27
+ * affected-file list / acceptance criteria); verbose context/background is
28
+ * dropped (the auditor can read the design docs themselves — they stay
29
+ * available outside this input). Independence preserved: the input is
30
+ * _engTaskInput (mechanically kept by the parent spawn) — never the
31
+ * eng-coder's self-report. Sections are located by header marker, prioritizing
32
+ * header lines (structured task books: "## 文件清单 …") and falling back to
33
+ * inline markers (flat one-line task books); a section runs to the next header
34
+ * of the SAME OR HIGHER level ("## 文件清单" survives a "### 修改" sub-header).
35
+ * Marker not found → the section is reported as missing (never fabricate).
36
+ */
37
+ function summarizeEngTaskBook(taskInput) {
38
+ if (!taskInput) return "(unavailable)"
39
+ const SECTIONS = [
40
+ { name: "Design docs involved", markers: [/Docs? involved/i, /涉及文档/] },
41
+ { name: "Affected-file list", markers: [/Files? (?:list|to (?:modify|change)|modified)/i, /受影响文件/, /文件清单/, /涉及文件/] },
42
+ { name: "Acceptance criteria", markers: [/Acceptance(?: criteria)?/i, /验收标准/] },
43
+ ]
44
+ const lines = taskInput.split("\n")
45
+ const headerLevel = (l) => {
46
+ const m = l.match(/^\s*(#{1,6})\s/)
47
+ return m ? m[1].length : 0
48
+ }
49
+ const headerIdx = lines.map((l, i) => (headerLevel(l) > 0 ? i : -1)).filter((i) => i >= 0)
50
+ const boundsFor = (from, level) => {
51
+ for (const j of headerIdx) {
52
+ if (j > from && (level === 0 || headerLevel(lines[j]) <= level)) return j
53
+ }
54
+ return lines.length
55
+ }
56
+ const out = []
57
+ for (const { name, markers } of SECTIONS) {
58
+ let from = -1
59
+ let level = 0
60
+ for (const i of headerIdx) {
61
+ if (markers.some((m) => m.test(lines[i]))) { from = i; level = headerLevel(lines[i]); break }
62
+ }
63
+ if (from === -1) {
64
+ for (let i = 0; i < lines.length; i++) {
65
+ if (markers.some((m) => m.test(lines[i]))) { from = i; level = 0; break }
66
+ }
67
+ }
68
+ if (from === -1) { out.push(`${name}: (not found in the parent task book)`); continue }
69
+ const body = lines.slice(from, boundsFor(from, level)).join("\n").trim()
70
+ out.push(body || `${name}: (empty section)`)
71
+ }
72
+ return out.join("\n\n")
73
+ }
74
+
75
+ /**
76
+ * Effective subagent model override for a role (CLI parity shared with VS Code):
77
+ * priority — subagent tool `model` arg > config.agent.subagentModels[role] > config.agent.subagentModel > null (inherit parent).
78
+ * The literal "default" (case-insensitive) means "no tool-arg override → run the
79
+ * chain" — same as omitting the parameter (2026-09-05 user ruling).
80
+ */
81
+ export function effectiveSubagentModel(parent, role, modelArg) {
82
+ // "default" alias (2026-09-05 user ruling — ARCHITECTURE.md 子 agent 模型指定):
83
+ // the literal "default", matched case-insensitively, explicitly declares "no
84
+ // override at the tool-arg level → run the default priority chain" (type-level
85
+ // subagentModels[role] → global subagentModel → null = inherit parent). It is
86
+ // equivalent to omitting the parameter / passing ""/null/undefined (which fall
87
+ // through below). Any other value still overrides.
88
+ if (modelArg && String(modelArg).toLowerCase() !== "default") return modelArg
89
+ const cfg = parent.config?.agent ?? {}
90
+ return cfg.subagentModels?.[role] ?? cfg.subagentModel ?? null
91
+ }
92
+
93
+ /**
94
+ * Resolve the design-token slot for an eng-coder spawn (2026-09-01 multi-design, FR3):
95
+ * - designId given → exact slot lookup (no match = explicit error, never a fuzzy guess)
96
+ * - designId omitted → exactly ONE slot must exist (single-design compatibility); with
97
+ * multiple slots we refuse rather than pick one (T16: never silently aim the wrong design)
98
+ * Returns { token } on success; throws with a parent-actionable message otherwise.
99
+ * R16 (2026-09-06): the slot Map is the authority; the format/TTL check itself stays in
100
+ * validateDesignToken (2026-09-06: HMAC 防伪层已删——token 为无签名流程凭证——见
101
+ * ENGINEERING-MODE.md 2026-09-06 段)。
102
+ * DESIGN-TOKEN-SETTLEMENT D2(2026-09-08):内存 Map miss(指定 designId 不在 / 空 Map)
103
+ * 时回读槽文件权威台账 reconcile + 判定(reconcileEngTokensFromSlot——与 dispatch 写门
104
+ * D3 同源)——覆盖"进程重启后槽有但 Map 未及回填/缓存与槽不一致"(槽文件 = 权威台账)。
105
+ * TTL 过滤保留(reconcile 只并入未过期项)。单值镜像 `_engDesignToken` 已随 D3 退役——
106
+ * 本门禁不再读镜像(AC3 零命中)。
107
+ */
108
+ export function resolveDesignSlot(parent, designIdArg) {
109
+ let slots = parent._engDesignTokens
110
+ const hasSlots = slots instanceof Map && slots.size > 0
111
+ // D2:内存 miss(指定 designId 不在 或 空 Map)→ 权威槽回读 reconcile + 判定
112
+ if (designIdArg ? !(hasSlots && slots.has(designIdArg)) : !hasSlots) {
113
+ const rec = reconcileEngTokensFromSlot(parent)
114
+ if (rec && rec !== slots) slots = rec
115
+ }
116
+ // F2d (§29.1): both refusal branches carry the HELD id list — after a
117
+ // persistence restore the parent has no digest to look the id up in; the error
118
+ // list is the only discovery path (ids are not credentials — the token is).
119
+ const size = slots instanceof Map ? slots.size : 0
120
+ const heldIds = size > 0 ? [...slots.keys()].join(", ") : "(none)"
121
+ if (designIdArg) {
122
+ if (!(slots instanceof Map) || !slots.has(designIdArg)) {
123
+ throw new Error(`designId not found — no approved design review holds this id. Run advisor with type='design' again and pass the designId echoed with the token. (session holds ${size} approved design slot(s); held design ids: ${heldIds})`)
124
+ }
125
+ return { token: slots.get(designIdArg) }
126
+ }
127
+ if (size > 1) {
128
+ throw new Error(`Multiple approved designs in this session (${size}) — pass the designId parameter (echoed with each token) to choose which design this eng-coder spawn belongs to. Held design ids: ${heldIds}`)
129
+ }
130
+ if (size === 1) return { token: [...slots.values()][0] }
131
+ throw new Error("Invalid or missing design token — run advisor with type='design' first and pass the returned token as designToken.")
132
+ }
133
+
134
+ /**
135
+ * consume-design 动作执行器(2026-09-07 token 链终消费制——ENGINEERING-MODE.md §2.6 F1):
136
+ * 父侧验收核销时显式调用——读槽值 → removeDesignTokenSlot(token-ttl.mjs——移除该
137
+ * designId 槽)→ 消费后同 designId 再 spawn = resolveDesignSlot not found 机械拒。调用
138
+ * 形态定死(评审 #3):参数 designId(单设计会话可省略——FR3 spawn 同款语义);未知
139
+ * designId 与重复消费同款 no-op 提示(幂等——不报错)。DESIGN-TOKEN-SETTLEMENT D3
140
+ * (2026-09-08):单值镜像 `_engDesignToken` 已退役——无镜像兼容值清/兜底读(AC3)。
141
+ * consume 落盘对称(2026-09-08 D1 段 + AC7):删内存槽后当场 persistEngTokens 同步
142
+ * 落盘删除(旧台账不留盘——防 D2 门禁 miss 回读复活已消费 token);落盘失败回滚
143
+ * 内存槽 + 抛错(不留半消费态,可重试——D1 评审 #1 同款失败语义)。
144
+ * dispatch 分类(评审 #7d):非只读控制动作——depth-0 + 工程模式
145
+ * 限定(受限变体门在 subagent.mjs 分流处;本器自持工程模式门)——planMode 拒绝
146
+ * (dispatch 不豁免)——不入批审批分组(dispatch 免审直行——无文件写)。
147
+ */
148
+ export function executeConsumeDesignAction(args, ctx) {
149
+ const parent = ctx.agent
150
+ if (!parent?.config?.agent?.engineering) {
151
+ throw new Error("Engineering mode is not active — consume-design applies only to engineering-mode design tokens (spawn 同门).")
152
+ }
153
+ const designId = args?.designId ? String(args.designId) : undefined
154
+ const slots = parent?._engDesignTokens
155
+ const hasSlots = slots instanceof Map && slots.size > 0
156
+ // 多槽缺 designId → 拒(spawn 同款语义——不误消费任一槽)
157
+ if (!designId && hasSlots && slots.size > 1) {
158
+ throw new Error(`consume-design: Multiple approved designs in this session (${slots.size}) — pass the designId parameter (echoed with each token) to choose which design to close out.`)
159
+ }
160
+ // 读槽值:给定 designId → 精确槽(未知/已消费 → undefined);缺省 → 唯一槽
161
+ let token = null
162
+ if (designId && hasSlots) token = slots.get(designId) ?? null
163
+ else if (!designId && hasSlots) token = [...slots.values()][0]
164
+ // 未知 designId / 已消费 / 无任何槽 → 幂等 no-op 提示(不报错——评审 #3 定死)
165
+ if (!token) {
166
+ return `consume-design: no live slot${designId ? ` for designId ${designId}` : ""} (already consumed or never issued) — idempotent no-op, nothing changed.`
167
+ }
168
+ // consume 落盘对称(DESIGN-TOKEN-SETTLEMENT D1 段 + AC7——交付 🔴 复活洞修复):
169
+ // 删内存槽后当场同步落盘删除(D1 同款 persistEngTokens——空 Map → 槽文件
170
+ // engDesignTokens 字段删除,旧台账不留盘)。否则消费→回合尾 saveSession 窗口内
171
+ // spawn 门禁 miss 回读(D2)会从盘上复活已消费 token。
172
+ const slotId = designId ?? [...slots.keys()][0]
173
+ removeDesignTokenSlot(parent, designId, token)
174
+ try {
175
+ persistEngTokens(parent)
176
+ } catch (e) {
177
+ // 落盘失败 → 回滚内存槽 + 抛错(不留半消费态——盘上仍有旧台账时消费不得报
178
+ // 成功;可重试——D1 评审 #1 settle 同款失败语义)
179
+ if (slotId) parent._engDesignTokens.set(slotId, token)
180
+ throw new Error(`consume-design: the slot was removed in memory but could NOT be durably deleted from the slot file (${e.message}) — the slot is restored in memory; retry consume-design.`)
181
+ }
182
+ return `design slot consumed — designId ${designId ?? "(single-design session)"} is closed out; a further eng-coder spawn for this design is mechanically rejected, and any new work (including deviation fixes) requires a fresh advisor(type='design') review and token.`
183
+ }
184
+
185
+ // ── §20 spawn 调度参数准入(AGENT-LOOP.md §20 D-SD1/D-SD3 + 20.4 round2 #5/#7)──
186
+ // files/dependsOn 声明即契约(v1:不做任务书文本自动解析——不可靠)。缺省(两者皆
187
+ // 缺)= 既有语义零改动(不参与冲突检测/无校验——legacy spawn 零开销直通)。
188
+ // 校验序:参数形态 → 依赖 unknown id(非 consumed 墓碑——T-SD10)→ 依赖环可达
189
+ // (T-SD5——防御断言:自然流程不可达)→ 等待态判定。判定结果:wait/depc 阻塞 →
190
+ // async 入 queued 等位(spawn 返回带 reason——D-SD3b);**sync spawn(async:false)
191
+ // 命中阻塞 → 明确错误——不队列化 sync——sync 语义零变更(round2 #7——T-SD13)**。
192
+ /** §20 准入(参数化提取——2026-09-05 module-split):返回归一化 { files, dependsOn }。
193
+ * filesRaw 目录声明 fail-closed(检测器 throw → 错误即工具结果 JSON)。 */
194
+ export function prepareScheduling(parent, filesRaw, dependsRaw, wantAsync) {
195
+ const files = []
196
+ if (filesRaw !== undefined && filesRaw !== null) {
197
+ try {
198
+ files.push(...normalizeFileList(filesRaw, parent.cwd))
199
+ } catch (e) {
200
+ return { files: [], dependsOn: [], errorJson: JSON.stringify({ status: "error", error: e.message }) }
201
+ }
202
+ }
203
+ if (filesRaw !== undefined && filesRaw !== null && !Array.isArray(filesRaw)) {
204
+ throw new Error("subagent files must be an array of file paths (the write domain this task declares)")
205
+ }
206
+ const dependsOn = []
207
+ if (dependsRaw !== undefined && dependsRaw !== null) {
208
+ if (!Array.isArray(dependsRaw)) throw new Error("subagent dependsOn must be an array of async subagent ids (from prior spawn returns)")
209
+ for (const d of dependsRaw) {
210
+ if (typeof d !== "string" && typeof d !== "number") {
211
+ throw new Error(`subagent dependsOn entries must be async subagent ids — got ${JSON.stringify(d)}`)
212
+ }
213
+ dependsOn.push(String(d))
214
+ }
215
+ }
216
+ if (files.length > 0 || dependsOn.length > 0) {
217
+ for (const d of dependsOn) {
218
+ if (depInfo(parent, d).state === "unknown") {
219
+ throw new Error(`subagent dependsOn: unknown async subagent id: ${d} — dependsOn references ids from prior async spawn returns; an id already consumed (auto-delivered to the model) counts as satisfied, anything else is a mistake (AGENT-LOOP.md §20 D-SD5)`)
220
+ }
221
+ }
222
+ assertNoDepCycle(parent, dependsOn)
223
+ const block = describeBlockers(parent, { _files: files, _dependsOn: dependsOn })
224
+ if (!wantAsync && block.kind !== "slot") {
225
+ throw new Error(`sync spawn (async:false) cannot queue behind a scheduling conflict: ${block.detail} — pass async:true to queue the task (the scheduler starts it when the blockers clear), or wait for them to finish first (AGENT-LOOP.md §20 round2 #7)`)
226
+ }
227
+ }
228
+ return { files, dependsOn, errorJson: null }
229
+ }
230
+
231
+ /**
232
+ * §20 准入通过后的 child 装配(2026-09-05 module-split——自 execute 参数化提取,
233
+ * 原 318-488 段 verbatim——语义零变)。副作用保留:relay 计数器/子代理 _logId/
234
+ * makeRelay 注册/_engTaskInput 携带全部在此发生。返回阻塞/异步两路径共用的
235
+ * { child, input, childOpts, childRunOpts, relayPrefix, childProvider }。
236
+ */
237
+ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, dependsOn, engAuditAttempt) {
238
+ // Provider/model override: tool `model` arg > subagentModels[role] > subagentModel > parent provider
239
+ const childProvider = resolveChildProvider(parent, effectiveSubagentModel(parent, role, args.model))
240
+
241
+ // eng-coder token gate: the design review must have passed and the caller must
242
+ // present the exact token advisor issued — otherwise the child is not authorized to code.
243
+ // 2026-09-01: multi-design slots — the token is located by designId (exact slot,
244
+ // single-slot fallthrough); token validation itself is unchanged (2026-09-06:
245
+ // HMAC removed — validateDesignToken now checks format + TTL only).
246
+ let issuedToken
247
+ if (role === "eng-coder") {
248
+ issuedToken = resolveDesignSlot(parent, args.designId).token
249
+ // R16 D-R16c ③ — spawn-gate cleanup: only an EXPIRY rejection removes the slot
250
+ // (the caller passed the slot's own token and it is past TTL — 长跑不重启也清);
251
+ // mismatch / format rejections never delete — the slot's own token may still be
252
+ // valid and a caller error must not destroy it (T-R16d). 单值镜像已退役(D3)——
253
+ // 无镜像同步清。
254
+ const expiredReject = !!issuedToken && args.designToken === issuedToken && tokenExpired(issuedToken)
255
+ if (!issuedToken || args.designToken !== issuedToken || !validateDesignToken(args.designToken)) {
256
+ if (expiredReject) removeDesignTokenSlot(parent, args.designId, issuedToken)
257
+ throw new Error("Invalid or missing design token — run advisor with type='design' first and pass the returned token as designToken.")
258
+ }
259
+ }
260
+
261
+ // Filter tool set by role: explore/plan are read-only (plan is a planning agent, its deliverable is the plan itself)
262
+ let tools
263
+ if (role === "explore" || role === "plan") {
264
+ const allowed = readonlyToolNames(parent.tools)
265
+ tools = parent.tools.filter((t) => allowed.has(t.name))
266
+ } else {
267
+ tools = parent.tools
268
+ }
269
+
270
+ // Select prompt overlay by role
271
+ let overlay = ""
272
+ if (role === "explore") overlay = EXPLORE_OVERLAY
273
+ else if (role === "coder") overlay = CODER_OVERLAY
274
+ else if (role === "plan") overlay = PLAN_OVERLAY
275
+ else if (role === "eng-coder") overlay = ENG_CODER_OVERLAY
276
+
277
+ // explore/plan: force read-only permission; coder/default: AUTO passes through directly,
278
+ // manual mode queues permission requests for the parent agent's approval UI (human in the loop, child agent is no longer silently rejected)
279
+ let childPermission
280
+ if (role === "explore" || role === "plan") {
281
+ childPermission = async () => false
282
+ } else if (parent.autoApprove) {
283
+ childPermission = async () => true
284
+ } else {
285
+ childPermission = async (name, toolArgs) => {
286
+ if (!ctx.onPermissionRequest) return false
287
+ const ask = () => ctx.onPermissionRequest(`${role ?? "sub"}/${name}`, toolArgs)
288
+ // Queue parallel child agent permission requests to avoid two popups simultaneously overwriting each other (lesson from question tool)
289
+ parent._permQueue = (parent._permQueue ?? Promise.resolve()).then(ask, ask)
290
+ return parent._permQueue
291
+ }
292
+ }
293
+
294
+ // eng-coder: force engineering=true on child config so setup.mjs applies engineering prompt
295
+ const childConfig = role === "eng-coder"
296
+ ? { ...parent.config, agent: { ...parent.config.agent, engineering: true } }
297
+ : parent.config
298
+
299
+ const child = createAgent({
300
+ provider: childProvider,
301
+ tools,
302
+ config: childConfig,
303
+ cwd: parent.cwd,
304
+ memory: parent.memory,
305
+ overlay,
306
+ role,
307
+ })
308
+
309
+ // Token-verified design review → child is authorized to modify files without re-reviewing
310
+ if (role === "eng-coder") child._engDesignReviewed = true
311
+ // §18 D-E3 task-domain authorization: approved design + spawn task = authorization.
312
+ // The child's OWN tools skip ONLY the onPermissionRequest ask (autoApprove
313
+ // equivalent — dispatch.mjs permission stage); every other gate (JSON parse /
314
+ // unknown tool / planMode / design-token) still applies (T-E14). Non-eng-coder
315
+ // children keep the manual per-write parent approval (human in the loop).
316
+ if (role === "eng-coder") child._engTaskAuthorized = true
317
+ // designId rides the child bookkeeping: the delivery report carries the designId
318
+ // so the divergence-audit fix round re-spawns with the SAME slot (2026-09-01 FR3).
319
+ // DESIGN-TOKEN-SETTLEMENT D3 (2026-09-08): the single-value `_engDesignToken` mirror
320
+ // is retired — no mirror copy on the child (nothing reads it; the slot ledger + the
321
+ // spawn-time token validation are the only authorities).
322
+ if (role === "eng-coder" && issuedToken) {
323
+ child._engDesignId = args.designId ?? null
324
+ }
325
+
326
+ // §18.5 子代理零 git(D-AG1——2026-09-04 用户裁定):explore/plan 一律不注入
327
+ // git 上下文——子代理证据链 = 任务书 ∪ 磁盘当前状态(read/glob/grep)∪(审计时)
328
+ // _touchedFiles,无一项来自 git;注入的全工作区脏状态快照与任务域无关,会误导
329
+ // 审计/探索("status 里这个文件算不算超清单?")。注入分支整体删除(B 方案
330
+ // git 只读变体亦随裁定废弃——D-AG5)。顶层主 agent 注入保留(§3 prepareRun——
331
+ // setup.mjs depth===0——D-AG7 范围边界)。
332
+ let input = args.context ? `Context:\n${args.context}\n\nTask:\n${args.task}` : args.task
333
+ // §18 D-E2 ③ (round4 #4, T-E13/T-E15): an eng-coder audit spawn's task book is
334
+ // the eng-coder's OWN spawn task — mechanically kept as _engTaskInput by the
335
+ // parent spawn and injected as the D-TS5 A2 mechanical summary (design docs /
336
+ // affected-file list / acceptance criteria verbatim, verbose context dropped)
337
+ // — ∪ the mechanically tracked _touchedFiles — NEVER the eng-coder's
338
+ // self-written list: a self-report could omit exactly the out-of-scope file
339
+ // the audit must catch.
340
+ if (engAuditAttempt !== null) {
341
+ const touched = (ctx.agent._touchedFiles ?? []).map((f) => `- ${f}`).join("\n") || "- (none yet)"
342
+ input += `\n\n[Audit scope — mechanical context, independent of the eng-coder's self-report:]\n` +
343
+ // §18.7 D-TS4 A1:审计指令模板(四类偏差 + 范围限制 + 校验清单格式)——审计语义
344
+ // 不再靠模型自悟;范围限制是 §18.5 D-AG3 声明(下方 Zero-git scope authority)
345
+ // 的同源一句指注,不重复声明。
346
+ `[Audit instructions — mechanical template (AGENT-LOOP.md §18.7 D-TS4 A1):]\n` +
347
+ `You are auditing an eng-coder delivery against its approved design — audit for EXACTLY these four deviation categories:\n` +
348
+ `- PARTIAL: an acceptance criterion implemented partially or not at all;\n` +
349
+ `- SILENT-SIMPLIFICATION: a "simpler approximation" of a specified behavior substituted for the spec;\n` +
350
+ `- DOC-DRIFT: code changed without the owning design-doc section (module map / affected-files table) updated in the same delivery;\n` +
351
+ `- OUT-OF-LIST: changes outside the approved file list.\n` +
352
+ `Audit scope = _touchedFiles above UNION the files confirmed by the parent task book (single source — the Zero-git scope authority note below, AGENT-LOOP.md §18.5 D-AG3; NOT a second copy): ` +
353
+ `workspace changes not listed there are unrelated to this delivery and are NOT grounds for an out-of-list finding.\n` +
354
+ `Scope discipline (F-TS6 A1): read ONLY the audited files and the design-doc sections relevant to this delivery — do NOT re-read whole documents.\n` +
355
+ `Every deviation item MUST be fieldized: file:line + design reference (doc path + section/AC id) + severity + evidence (quoted code or doc text).\n` +
356
+ // §18.7 D-TS5 A2:任务书从全量 verbatim 改机械摘要块(三要素逐字——排除冗长上下文)。
357
+ `[Parent spawn task book — mechanical summary (AGENT-LOOP.md §18.7 D-TS5 A2): design docs + affected-file list + acceptance criteria verbatim; verbose context/background dropped — the design docs are still available for reading outside this input:]\n` +
358
+ `${summarizeEngTaskBook(ctx.agent._engTaskInput)}\n` +
359
+ `Files actually touched by the eng-coder (mechanical union — audit these against the file list):\n${touched}\n` +
360
+ // §18.5 D-AG3(2026-09-04):审计零 git 范围权威声明——本审计任务零 git(不注入
361
+ // git 上下文——§18.5 全角色零 git);_touchedFiles 为审计范围;工作区未列于
362
+ // _touchedFiles 的改动与本任务无关,不作超清单依据(VS Code auditTaskBook 同款措辞)。
363
+ "Zero-git scope authority (AGENT-LOOP.md §18.5 D-AG3): this audit task receives NO git context — nothing is injected. " +
364
+ "The evidence base is the design documents, the current disk state (read/glob/grep), and the _touchedFiles list above. " +
365
+ "Workspace changes NOT listed in _touchedFiles are unrelated to this delivery — they are NOT grounds for an out-of-file-list finding." +
366
+ // §18.13 D-A1.2:审计预算句——A1 指令模板 + A2 摘要块之后、A3 报告模板之前(定序——评审 #7)。
367
+ // 逐字设计锚(D-A1.2 代码块):只读该读的——10 轮机械预算——超时报 PROBLEM 下结论。
368
+ // 前导 \n 与 A3 同款块分隔约定(上一句 Zero-git 句末无换行——不触碰既有句)。
369
+ `\n[Audit budget — mechanical]: read ONLY the touched files listed above and the design-doc sections the parent task book names (affected-files table, acceptance criteria, status line). Do NOT read whole documents. Budget = 10 tool rounds max — if you cannot conclude within it, report PROBLEM (inconclusive) rather than continuing to explore.\n` +
370
+ // §18.7 D-TS6 A3:审计输出报告格式模板(三态——字段化行——不让模型自由发挥)。
371
+ `\n[Audit report format — mechanical template (AGENT-LOOP.md §18.7 D-TS6 A3):]\n` +
372
+ `Report EXACTLY one of three states:\n` +
373
+ `- CLEAN — no deviation across the four categories: reply the line "Four deviation categories: none found." (四类偏差均未发现);\n` +
374
+ `- DEVIATIONS — one row per deviation, every row fieldized: | category | file:line | design reference | severity | evidence |;\n` +
375
+ `- PROBLEM — the audit itself could not run / inconclusive: state what blocked it.\n`
376
+ }
377
+ // The child's own task input rides the child object: an eng-coder's audit
378
+ // spawns reuse it as the task-book SOURCE — injected as the D-TS5 A2
379
+ // mechanical summary, not verbatim (see above).
380
+ if (role === "eng-coder") child._engTaskInput = input
381
+
382
+ // Relay content/reasoning/tool/output to the parent TUI via the unified spawn-child
383
+ // pipeline (AGENT-LOOP.md §7.2 D3). Prefix includes a unique id: parallel child agents
384
+ // with the same role stay independent and don't overwrite each other.
385
+ // Format: role#id/ → onToken("coder#2/writing..."), onToolCall("coder#2/read", args)
386
+ // Async id allocation (AGENT-LOOP.md §15 D-A1): reserve the relay counter at
387
+ // spawn time — the returned id must be stable while the item sits in the queue.
388
+ // The [model] token (TUI block creation) is DEFERRED to actual start so queued
389
+ // children don't paint an empty panel block ("queued 态不显示").
390
+ let relayPrefix
391
+ if (wantAsync) {
392
+ parent._subAgentCounter = (parent._subAgentCounter ?? 0) + 1
393
+ relayPrefix = `${role}#${parent._subAgentCounter}/`
394
+ } else {
395
+ relayPrefix = makeRelay(parent, role ?? "sub", ctx.callbacks?.onToken, childProvider.model ?? "")
396
+ }
397
+ // LOGGING(LOGGING.md):子代理内部事件(子内 llm:*/tool:*)以 childId 归属——
398
+ // agent._logId 随 runAgent 的 logCtx 透出(主文件单文件全记、按 childId grep)。
399
+ child._logId = relayPrefix.slice(0, -1)
400
+ const childOpts = {
401
+ onPermissionRequest: childPermission,
402
+ ...wrapChildCallbacks(relayPrefix, ctx.callbacks),
403
+ }
404
+ const childRunOpts = buildChildRunOpts(ctx)
405
+ return { child, input, childOpts, childRunOpts, relayPrefix, childProvider }
406
+ }