thincoder 0.12.59 → 0.12.61

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 (192) hide show
  1. package/CHANGELOG.md +62 -4
  2. package/README.md +10 -8
  3. package/bin/thincoder.mjs +99 -133
  4. package/package.json +6 -4
  5. package/src/abort-provenance.mjs +116 -0
  6. package/src/acp/bridge.mjs +45 -21
  7. package/src/acp.mjs +6 -1
  8. package/src/advisor/citations.mjs +83 -21
  9. package/src/advisor/compaction.mjs +174 -0
  10. package/src/advisor/loop.mjs +293 -0
  11. package/src/advisor/messages.mjs +59 -137
  12. package/src/advisor/project-context.mjs +194 -0
  13. package/src/advisor/repos.mjs +17 -40
  14. package/src/advisor/run.mjs +156 -359
  15. package/src/advisor/truncate.mjs +57 -0
  16. package/src/advisor.mjs +27 -7
  17. package/src/agent/completion.mjs +17 -11
  18. package/src/agent/dispatch.mjs +145 -27
  19. package/src/agent/helpers.mjs +107 -13
  20. package/src/agent/record-results.mjs +55 -11
  21. package/src/agent/relay-prefix.mjs +39 -0
  22. package/src/agent/run-stages.mjs +242 -0
  23. package/src/agent/setup-reminders.mjs +69 -0
  24. package/src/agent/setup.mjs +107 -127
  25. package/src/agent/spawn-child.mjs +55 -13
  26. package/src/agent-tools/advisor-async.mjs +346 -0
  27. package/src/agent-tools/advisor-settle.mjs +231 -0
  28. package/src/agent-tools/advisor.mjs +167 -116
  29. package/src/agent-tools/async-settle.mjs +191 -0
  30. package/src/agent-tools/batch-segment.mjs +195 -0
  31. package/src/agent-tools/consult.mjs +139 -107
  32. package/src/agent-tools/design-token.mjs +117 -0
  33. package/src/agent-tools/digest-budget.mjs +76 -0
  34. package/src/agent-tools/eng.mjs +24 -29
  35. package/src/agent-tools/escalate-async.mjs +289 -0
  36. package/src/agent-tools/read-history.mjs +166 -32
  37. package/src/agent-tools/recent-changes.mjs +2 -1
  38. package/src/agent-tools/review-streak.mjs +93 -0
  39. package/src/agent-tools/settings.mjs +137 -34
  40. package/src/agent-tools/subagent-actions.mjs +180 -133
  41. package/src/agent-tools/subagent-async.mjs +184 -177
  42. package/src/agent-tools/subagent-panel.mjs +160 -0
  43. package/src/agent-tools/subagent-run.mjs +205 -0
  44. package/src/agent-tools/subagent-scheduler.mjs +100 -27
  45. package/src/agent-tools/subagent-spawn.mjs +453 -0
  46. package/src/agent-tools/subagent.mjs +256 -578
  47. package/src/agent-tools/verify.mjs +119 -292
  48. package/src/agent-tools.mjs +1 -0
  49. package/src/agent.mjs +89 -205
  50. package/src/cli/distill-command.mjs +12 -6
  51. package/src/cli/make-agent.mjs +26 -8
  52. package/src/cli/memory-command.mjs +4 -3
  53. package/src/cli/permission.mjs +2 -2
  54. package/src/cli/setup-wizard.mjs +42 -17
  55. package/src/completions.mjs +114 -0
  56. package/src/config-migrate.mjs +70 -0
  57. package/src/config.mjs +180 -63
  58. package/src/context.mjs +5 -147
  59. package/src/conventions.mjs +223 -0
  60. package/src/crash-reports.mjs +128 -0
  61. package/src/distill.mjs +11 -11
  62. package/src/expand-home.mjs +16 -0
  63. package/src/explore-distill.mjs +155 -0
  64. package/src/generate-title.mjs +1 -1
  65. package/src/hooks.mjs +7 -3
  66. package/src/memory/code-index.mjs +9 -3
  67. package/src/memory/code-sync.mjs +72 -32
  68. package/src/memory/core.mjs +6 -193
  69. package/src/memory/delete.mjs +236 -0
  70. package/src/memory/docs.mjs +68 -54
  71. package/src/memory/file-walk.mjs +109 -0
  72. package/src/memory/schema.mjs +15 -3
  73. package/src/memory.mjs +3 -1
  74. package/src/model-ref.mjs +66 -0
  75. package/src/model-specs.mjs +42 -8
  76. package/src/peer-domains.mjs +265 -0
  77. package/src/peer-instances.mjs +231 -0
  78. package/src/prompt-overlays.mjs +82 -0
  79. package/src/prompts/advisor-design.md +18 -75
  80. package/src/prompts/advisor-round1.md +14 -67
  81. package/src/prompts/advisor-round2.md +15 -51
  82. package/src/prompts/advisor-round3.md +15 -51
  83. package/src/prompts/common.md +115 -0
  84. package/src/prompts/consult-base.md +5 -23
  85. package/src/prompts/discipline-engineering.md +217 -0
  86. package/src/prompts/discipline-normal.md +179 -0
  87. package/src/prompts/persona-coder.md +21 -0
  88. package/src/prompts/persona-eng-coder.md +37 -0
  89. package/src/prompts/persona-eng-designer.md +55 -0
  90. package/src/prompts/persona-engineering.md +54 -0
  91. package/src/prompts/persona-explore.md +15 -0
  92. package/src/prompts/persona-normal.md +27 -0
  93. package/src/prompts/persona-plan.md +26 -0
  94. package/src/provider/anthropic.mjs +4 -4
  95. package/src/provider/core.mjs +18 -98
  96. package/src/provider/errors.mjs +101 -0
  97. package/src/provider/google.mjs +5 -6
  98. package/src/provider/index.mjs +2 -1
  99. package/src/provider/list-models.mjs +93 -0
  100. package/src/provider/rate.mjs +2 -1
  101. package/src/provider/responses.mjs +5 -3
  102. package/src/provider/retry.mjs +8 -45
  103. package/src/provider/sse.mjs +3 -4
  104. package/src/proxy.mjs +9 -14
  105. package/src/session-gc.mjs +214 -0
  106. package/src/session-guard.mjs +47 -0
  107. package/src/session-rename.mjs +38 -0
  108. package/src/session-slots.mjs +188 -60
  109. package/src/session.mjs +104 -124
  110. package/src/token-ttl.mjs +274 -0
  111. package/src/tools/{system.mjs → bash.mjs} +19 -221
  112. package/src/tools/checklist-sync.mjs +181 -0
  113. package/src/tools/checklist.mjs +52 -39
  114. package/src/tools/edit-batch.mjs +109 -10
  115. package/src/tools/edit-diff.mjs +110 -27
  116. package/src/tools/edit.md +17 -12
  117. package/src/tools/execute.mjs +31 -4
  118. package/src/tools/file.mjs +41 -16
  119. package/src/tools/git.md +1 -1
  120. package/src/tools/git.mjs +23 -34
  121. package/src/tools/glob-dialect.mjs +130 -0
  122. package/src/tools/glob.md +3 -3
  123. package/src/tools/grep.md +1 -1
  124. package/src/tools/index.mjs +9 -8
  125. package/src/tools/ops.mjs +188 -3
  126. package/src/tools/patch.mjs +3 -3
  127. package/src/tools/question.md +4 -0
  128. package/src/tools/question.mjs +26 -0
  129. package/src/tools/read.md +1 -2
  130. package/src/tools/read_image.md +1 -1
  131. package/src/tools/search.mjs +236 -0
  132. package/src/tools/shared.mjs +14 -13
  133. package/src/tools/wait_for.md +22 -0
  134. package/src/tui/agent-turn.mjs +36 -228
  135. package/src/tui/ansi.mjs +2 -0
  136. package/src/tui/clipboard.mjs +7 -1
  137. package/src/tui/cmd-advisor.mjs +3 -2
  138. package/src/tui/cmd-config.mjs +142 -30
  139. package/src/tui/cmd-eng.mjs +28 -40
  140. package/src/tui/cmd-exit.mjs +6 -8
  141. package/src/tui/cmd-mcp.mjs +8 -2
  142. package/src/tui/cmd-model.mjs +14 -12
  143. package/src/tui/cmd-new.mjs +3 -2
  144. package/src/tui/cmd-reindex.mjs +7 -0
  145. package/src/tui/cmd-session.mjs +19 -4
  146. package/src/tui/cmd-submodel.mjs +8 -5
  147. package/src/tui/cmd-think.mjs +10 -10
  148. package/src/tui/cmd-undo.mjs +4 -3
  149. package/src/tui/cmd-upgrade.mjs +19 -4
  150. package/src/tui/config-helpers.mjs +28 -16
  151. package/src/tui/distill-cmd.mjs +1 -1
  152. package/src/tui/index.mjs +40 -38
  153. package/src/tui/interaction.mjs +3 -3
  154. package/src/tui/key-handler.mjs +61 -17
  155. package/src/tui/key-modes.mjs +86 -8
  156. package/src/tui/layout.mjs +18 -10
  157. package/src/tui/model-catalog.mjs +89 -0
  158. package/src/tui/model-picker.mjs +498 -0
  159. package/src/tui/mouse.mjs +52 -9
  160. package/src/tui/pickers.mjs +28 -392
  161. package/src/tui/render-frame.mjs +32 -16
  162. package/src/tui/render-loop.mjs +2 -0
  163. package/src/tui/render-segments.mjs +12 -9
  164. package/src/tui/render.mjs +37 -5
  165. package/src/tui/slash-commands.mjs +2 -2
  166. package/src/tui/startup.mjs +4 -0
  167. package/src/tui/subagent-blocks.mjs +106 -295
  168. package/src/tui/subagent-children.mjs +162 -0
  169. package/src/tui/subagent-freeze.mjs +169 -0
  170. package/src/tui/subagent-panel.mjs +24 -31
  171. package/src/tui/suspension-drive.mjs +297 -0
  172. package/src/tui/tool-args.mjs +7 -5
  173. package/src/tui/tool-display.mjs +143 -0
  174. package/src/tui/tool-events.mjs +56 -185
  175. package/src/tui/tui-lifecycle.mjs +46 -4
  176. package/src/tui/update-notice.mjs +4 -0
  177. package/src/tui/wizard.mjs +61 -21
  178. package/src/tui/wrapped-spawn.mjs +38 -0
  179. package/src/prompts/coder.md +0 -56
  180. package/src/prompts/discipline.md +0 -102
  181. package/src/prompts/eng-coder.md +0 -44
  182. package/src/prompts/engineering-sub.md +0 -34
  183. package/src/prompts/engineering.md +0 -340
  184. package/src/prompts/explore.md +0 -21
  185. package/src/prompts/main.md +0 -56
  186. package/src/prompts/methodology-template.md +0 -58
  187. package/src/prompts/plan.md +0 -16
  188. package/src/prompts/system.md +0 -63
  189. package/src/tools/pdf-parse-text.mjs +0 -497
  190. package/src/tools/pdf-parse-xref.mjs +0 -499
  191. package/src/tools/pdf.mjs +0 -155
  192. package/src/tools/read_pdf.md +0 -21
@@ -1,171 +1,138 @@
1
- import {
2
- createAgent,
3
- readonlyToolNames, escapeXml,
4
- EXPLORE_OVERLAY, CODER_OVERLAY, PLAN_OVERLAY, ENG_CODER_OVERLAY,
5
- } from "../agent.mjs"
6
- import { makeRelay, wrapChildCallbacks, gateEngCoderSpawn, TURN_CAP_MARK } from "../agent/spawn-child.mjs"
7
- import { validateDesignToken } from "./advisor.mjs"
8
- import { pushReal } from "../context.mjs"
1
+ /**
2
+ * subagent.mjs — subagent tool(ONE tool, EIGHT actions + spawn 路径驱动器)。
3
+ * 2026-09-07 token 链终消费制:+action: consume-design(ENGINEERING-MODE.md §2.6 F1——
4
+ * 父侧链终核销消费 designId 槽——执行器 executeConsumeDesignAction 在 subagent-spawn.mjs)。
5
+ *
6
+ * 2026-09-03 拆分轮: subagent.mjs 500 硬顶——async 常量、共享 post-spawn 管线
7
+ *(runChildPipeline)与队列/注入/并账机械迁至 ./subagent-async.mjs。execute
8
+ *(async 分支 + 阻塞路径)原样保留于本文件;导出面由文末 re-export shim 兜住。
9
+ * 2026-09-03 §19 合体轮: subagent_check/escalate 工具退役——status/escalate
10
+ * 动作执行器并入 ./subagent-async.mjs,本文件只承载工具面(action schema)与
11
+ * spawn 路径 + 动作分流。
12
+ * 2026-09-06 §19.8 删 check 轮: check 动作删除——工具面五动作(spawn/status/
13
+ * escalate/cancel/panel)——async 结果仅自动通道送达。
14
+ * 2026-09-05 拆分轮: status/escalate/panel 动作执行器 → ./subagent-actions.mjs;§20
15
+ * 调度器全套 → ./subagent-scheduler.mjs——本文件 import 源随之改写。
16
+ * 2026-09-05 模块拆分轮(726 > 500 硬限): spawn 前置 helpers(summarizeEngTaskBook/
17
+ * effectiveSubagentModel/resolveDesignSlot)+ §20 准入(prepareScheduling)+ child
18
+ * 装配(buildSpawnChild)→ ./subagent-spawn.mjs;async 分支(executeAsyncSpawn)
19
+ * → ./subagent-run.mjs——execute 只保留动作分流 + 装配调用 + 阻塞路径。
20
+ */
21
+
22
+ import { gateEngCoderSpawn, TURN_CAP_MARK, STOPPED_MARK, emitNestedChildEvent } from "../agent/spawn-child.mjs"
9
23
  import { logEvent, errText } from "../log.mjs"
24
+ import { abortError, deathLine } from "../abort-provenance.mjs"
10
25
  import {
11
- runChildPipeline, resolveChildProvider, ASYNC_SUBAGENT_LIMIT,
12
- buildChildRunOpts, executeCheckAction, executeCancelAction,
26
+ runChildPipeline, executeCancelAction, enqueueAsk, mergeChildMutations,
13
27
  } from "./subagent-async.mjs"
14
- import { executeStatusAction, executeEscalateAction, executePanelAction } from "./subagent-actions.mjs"
15
- import {
16
- // §20 调度器(AGENT-LOOP.md §20——D-SD1..SD5):文件域归一化/等待态派生/环防御/
17
- // 排队态面板刷新/依赖者标注/补位——2026-09-05 拆分轮后独立模块。
18
- normalizeFileList, describeBlockers, queueRunnable, refreshQueuedTokens,
19
- assertNoDepCycle, depInfo, dependentLabels, maybeRefillAsync,
20
- } from "./subagent-scheduler.mjs"
28
+ // SYNC-CANCEL(L52——2026-09-09):阻塞路径自属 AbortController 链到会话/回合基信号
29
+ // 的单点(async-settle.mjs D6——_sessionSignal ?? ctx.signal——与 async 条目 controller
30
+ // 链同一语义——挂起场景 base 命中而 ctx.signal 未 abort——R2)。
31
+ import { buildChildSignal } from "./async-settle.mjs"
32
+ import { executeStatusAction, executeEscalateAction, executePanelAction, executeObserveAction, executeSendAction } from "./subagent-actions.mjs"
33
+ import { prepareScheduling, buildSpawnChild, executeConsumeDesignAction } from "./subagent-spawn.mjs"
34
+ import { executeAsyncSpawn } from "./subagent-run.mjs"
21
35
 
22
- // 2026-09-03 拆分轮: subagent.mjs 超 500 硬顶——async 常量、共享 post-spawn 管线
23
- //(runChildPipeline)与队列/注入/并账机械迁至 ./subagent-async.mjs。execute
24
- //(async 分支 + 阻塞路径)原样保留于本文件;导出面由文末 re-export shim 兜住。
25
- // 2026-09-03 §19 合体轮: subagent_check/escalate 工具退役——check/status/escalate
26
- // 动作执行器并入 ./subagent-async.mjs,本文件只承载工具面(action schema)与
27
- // spawn 路径 + 动作分流。
28
- // 2026-09-05 拆分轮: status/escalate/panel 动作执行器 → ./subagent-actions.mjs;§20
29
- // 调度器全套(normalizeFileList/describeBlockers/queueRunnable/refreshQueuedTokens/
30
- // assertNoDepCycle/depInfo/dependentLabels/maybeRefillAsync)→ ./subagent-scheduler.mjs
31
- // ——本文件 import 源随之改写;check/cancel 与机械/管线仍来自 ./subagent-async.mjs。
36
+ // ─── SYNC-CANCEL 纯函数(可测——无 io)──────────────────────────────────────────
32
37
 
33
38
  /**
34
- * §18.7 D-TS5 (A2): mechanically summarize the parent spawn task book for the
35
- * audit spawn the three audit-relevant elements VERBATIM (design doc paths /
36
- * affected-file list / acceptance criteria); verbose context/background is
37
- * dropped (the auditor can read the design docs themselves — they stay
38
- * available outside this input). Independence preserved: the input is
39
- * _engTaskInput (mechanically kept by the parent spawn) — never the
40
- * eng-coder's self-report. Sections are located by header marker, prioritizing
41
- * header lines (structured task books: "## 文件清单 …") and falling back to
42
- * inline markers (flat one-line task books); a section runs to the next header
43
- * of the SAME OR HIGHER level ("## 文件清单" survives a "### 修改" sub-header).
44
- * Marker not found → the section is reported as missing (never fabricate).
39
+ * SYNC-CANCEL F2 catch 三分支分类(可测纯函数——R3 收紧):
40
+ * "base" 整回合停:ctx.signal baseSignal(= parent._sessionSignal ?? ctx.signal——
41
+ * buildChildSignal——挂起会话场景 base 命中而 ctx.signal abort——R2)aborted
42
+ * 现状保留(emitNestedChildEvent stopped + rethrow);
43
+ * "targeted" 定向中止:err AbortError 且自属 ctrl aborted(且非整回合停)→
44
+ * 折叠 stopped partial 报告(父回合继续——merge/STOPPED_MARK/警示);
45
+ * "error" 其他错误 现状保留(child:error + rethrow)。
46
+ * baseSignal 非仅 ctx.signal——挂起 digest 场景 child _sessionSignal(R2——
47
+ * digest 自身 Ctrl+I/Ctrl+C 不误伤;会话 Stop 逐链中止必须归 ①)。
45
48
  */
46
- function summarizeEngTaskBook(taskInput) {
47
- if (!taskInput) return "(unavailable)"
48
- const SECTIONS = [
49
- { name: "Design docs involved", markers: [/Docs? involved/i, /涉及文档/] },
50
- { name: "Affected-file list", markers: [/Files? (?:list|to (?:modify|change)|modified)/i, /受影响文件/, /文件清单/, /涉及文件/] },
51
- { name: "Acceptance criteria", markers: [/Acceptance(?: criteria)?/i, /验收标准/] },
52
- ]
53
- const lines = taskInput.split("\n")
54
- const headerLevel = (l) => {
55
- const m = l.match(/^\s*(#{1,6})\s/)
56
- return m ? m[1].length : 0
57
- }
58
- const headerIdx = lines.map((l, i) => (headerLevel(l) > 0 ? i : -1)).filter((i) => i >= 0)
59
- const boundsFor = (from, level) => {
60
- for (const j of headerIdx) {
61
- if (j > from && (level === 0 || headerLevel(lines[j]) <= level)) return j
62
- }
63
- return lines.length
49
+ export function classifySyncAbort(ctxSignal, baseSignal, ctrlSignal, err) {
50
+ if (ctxSignal?.aborted || baseSignal?.aborted) return "base"
51
+ if (err?.name === "AbortError" && ctrlSignal?.aborted) return "targeted"
52
+ return "error"
53
+ }
54
+
55
+ /**
56
+ * SYNC-CANCEL F1/F5 中止控制器装配(可测):sync 阻塞 spawn 建**自属** AbortController
57
+ * (childRunOpts.signal 覆写为 ctrl.signal——照抄 async 分支 subagent-run.mjs 覆写模式)
58
+ * ——ctrl 链到基信号:baseSignal aborted → ctrl.abort();否则 addEventListener("abort",
59
+ * ctrl.abort(), { once:true })——Ctrl+C/I 整回合停语义不变(base abort 逐链传播——
60
+ * AC2);嵌套 sync spawn 递归可中止(内层链外层 ctrl.signal——逐层自属——AC4)。
61
+ * 注册 `parent._syncChildAborts`(key = relayPrefix 去尾——{ ctrl, stopped:false }——
62
+ * TUI 门控 live 判据 + cancelSyncChild 定向中止目标——与 async 条目 controller 存池
63
+ * 分层一致)。返回 { ctrl, disarm }——disarm 注销 registry(调用方 try/finally 三路径
64
+ * 共用——R7 防跨回合残留)。
65
+ */
66
+ export function armSyncChildAbort(parent, key, baseSignal) {
67
+ const ctrl = new AbortController()
68
+ if (baseSignal) {
69
+ // §20.3 站点 #10(第 24 批):hop 逐跳保 reason(下游可判定「谁杀的」)
70
+ if (baseSignal.aborted) ctrl.abort(baseSignal.reason)
71
+ else baseSignal.addEventListener("abort", () => ctrl.abort(baseSignal.reason), { once: true })
64
72
  }
65
- const out = []
66
- for (const { name, markers } of SECTIONS) {
67
- let from = -1
68
- let level = 0
69
- for (const i of headerIdx) {
70
- if (markers.some((m) => m.test(lines[i]))) { from = i; level = headerLevel(lines[i]); break }
71
- }
72
- if (from === -1) {
73
- for (let i = 0; i < lines.length; i++) {
74
- if (markers.some((m) => m.test(lines[i]))) { from = i; level = 0; break }
75
- }
76
- }
77
- if (from === -1) { out.push(`${name}: (not found in the parent task book)`); continue }
78
- const body = lines.slice(from, boundsFor(from, level)).join("\n").trim()
79
- out.push(body || `${name}: (empty section)`)
73
+ const registry = (parent._syncChildAborts ??= new Map())
74
+ registry.set(key, { ctrl, stopped: false })
75
+ const disarm = () => { registry.delete(key) }
76
+ return { ctrl, disarm }
77
+ }
78
+
79
+ /**
80
+ * SYNC-CANCEL ② 折叠报告构建(可测纯函数——仿 runChildPipeline onDeclined partial 形态,
81
+ * subagent-async.mjs onDeclined:STOPPED_MARK + partial 警示 + 捕获输出 + eng-coder
82
+ * designId 后缀——AC3)。capturedOutput = child._capturedOutput(spawn-child.mjs
83
+ * runWithContinue capture 累积——子代理已流式输出的剥哨兵文本)。
84
+ */
85
+ export function buildSyncStoppedReport(role, capturedOutput, designId) {
86
+ let report = `Subagent (${role}) ${STOPPED_MARK} — work may be partial; review recent_changes before deciding next steps.\nPartial output: ${capturedOutput || ""}`
87
+ if (role === "eng-coder") {
88
+ report += `\ndesignId: ${designId ?? "(single-design session — designId optional)"} — reuse it (with the same designToken) when re-spawning this eng-coder.`
80
89
  }
81
- return out.join("\n\n")
90
+ return report
82
91
  }
83
92
 
84
93
  /**
85
- * subagent tool — ONE tool, SIX actions (AGENT-LOOP.md §19/§19.5/§19.6): spawn
86
- * (default) / check (fetch async results, blocking + consuming) / status
87
- * (non-blocking pool query) / escalate (飞刀 hand implementation to a
88
- * stronger model) / cancel (stop ONE background subagent§19.5) / panel
89
- * (view + fix the live subagent panel — §19.6).
94
+ * subagent tool — ONE tool, EIGHT actions (AGENT-LOOP.md §19/§19.5/§19.6/§19.8 +
95
+ * SUBAGENT-OBSERVE-SEND): spawn (default) / status (non-blocking pool query) / observe
96
+ * (inspect a running/queued/done async child's recent activity + current tool — §7.2) /
97
+ * send (inject a direction into a RUNNING async child consumed at its next turn
98
+ * boundary as an ordinary instruction — §7.2) / escalate (飞刀 — hand implementation to
99
+ * a stronger model) / cancel (stop ONE background subagent — §19.5) / panel (view + fix
100
+ * the live subagent panel — §19.6) / consume-design (parent-side chain-terminal token
101
+ * consumption — ENGINEERING-MODE.md §2.6, 2026-09-07). The check
102
+ * action was deleted (§19.8): async results reach the model only via the auto channel.
90
103
  * - action:"spawn" roles: "explore" — read-only tools, search/read/analyze
91
104
  * (suitable for codebase exploration); "coder" — full tool set, self-contained
92
105
  * implementation tasks; "plan" — read-only planning; "eng-coder" —
93
- * engineering-mode implementation (design-token gated).
106
+ * engineering-mode implementation (design-token gated); "eng-designer" —
107
+ * engineering-mode design writing (requirements + design docs, batchDoc gated).
94
108
  * - no role specified — invalid by design since the 2026-08-25 fail-closed gate
95
109
  * (role is mandatory; "no role → same tool set as parent" was removed with the
96
110
  * coder-leak fix and the header text above predates it)
97
111
  * - non-recursive: child agents do not get the subagent tool (depth > 0 is not injected)
98
112
  */
99
113
 
100
- /**
101
- * Effective subagent model override for a role (CLI parity shared with VS Code):
102
- * priority — subagent tool `model` arg > config.agent.subagentModels[role] > config.agent.subagentModel > null (inherit parent).
103
- * The literal "default" (case-insensitive) means "no tool-arg override → run the
104
- * chain" — same as omitting the parameter (2026-09-05 user ruling).
105
- */
106
- export function effectiveSubagentModel(parent, role, modelArg) {
107
- // "default" alias (2026-09-05 user ruling — ARCHITECTURE.md 子 agent 模型指定):
108
- // the literal "default", matched case-insensitively, explicitly declares "no
109
- // override at the tool-arg level → run the default priority chain" (type-level
110
- // subagentModels[role] → global subagentModel → null = inherit parent). It is
111
- // equivalent to omitting the parameter / passing ""/null/undefined (which fall
112
- // through below). Any other value still overrides.
113
- if (modelArg && String(modelArg).toLowerCase() !== "default") return modelArg
114
- const cfg = parent.config?.agent ?? {}
115
- return cfg.subagentModels?.[role] ?? cfg.subagentModel ?? null
116
- }
117
-
118
- /**
119
- * Resolve the design-token slot for an eng-coder spawn (2026-09-01 multi-design, FR3):
120
- * - designId given → exact slot lookup (no match = explicit error, never a fuzzy guess)
121
- * - designId omitted → exactly ONE slot must exist (single-design compatibility); with
122
- * multiple slots we refuse rather than pick one (T16: never silently aim the wrong design)
123
- * Returns { token } on success; throws with a parent-actionable message otherwise.
124
- * The HMAC/TTL check itself stays in validateDesignToken (unchanged).
125
- */
126
- export function resolveDesignSlot(parent, designIdArg) {
127
- const slots = parent._engDesignTokens
128
- const hasSlots = slots instanceof Map && slots.size > 0
129
- const legacy = parent._engDesignToken
130
- // eng(exit/enter) resets the single mirror to force a fresh review (eng.mjs) —
131
- // a non-empty slot map surviving that reset must NOT resurrect stale tokens:
132
- // mirror cleared + slots present = re-entered engineering mode → re-review.
133
- if (!legacy && hasSlots) {
134
- throw new Error("Design tokens were reset (engineering mode was re-entered) — run advisor with type='design' again and spawn with the fresh designId+token pair.")
135
- }
136
- if (designIdArg) {
137
- if (!hasSlots || !slots.has(designIdArg)) {
138
- 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 ${hasSlots ? slots.size : 0} approved design slot(s))`)
139
- }
140
- return { token: slots.get(designIdArg) }
141
- }
142
- if (hasSlots && slots.size > 1) {
143
- throw new Error(`Multiple approved designs in this session (${slots.size}) — pass the designId parameter (echoed with each token) to choose which design this eng-coder spawn belongs to.`)
144
- }
145
- if (hasSlots && slots.size === 1) return { token: [...slots.values()][0] }
146
- if (legacy) return { token: legacy } // single-slot mirror fallback (pre-multi-slot sessions)
147
- throw new Error("Invalid or missing design token — run advisor with type='design' first and pass the returned token as designToken.")
148
- }
149
-
150
114
  export const subagentTool = {
151
115
  name: "subagent",
152
116
  description:
153
- "ONE tool, SIX actions (AGENT-LOOP.md §19/§19.5/§19.6) — pick by what you need:\n" +
117
+ "ONE tool, EIGHT actions (AGENT-LOOP.md §19/§19.5/§19.6/§19.8 + SUBAGENT-OBSERVE-SEND) — pick by what you need:\n" +
154
118
  "- action:'spawn' (DEFAULT): spawn a sub-agent to handle an independent subtask in an isolated context; the sub-agent returns only its final report. Spawn MULTIPLE subagents in the SAME response for parallel work—they run concurrently.\n" +
155
- "- action:'check': fetch the report of an async spawn (async:true). BLOCKS until the target finishes this is the explicit consuming fetch, NOT a progress query. Multiple async children return in completion (arrival) order, first finished first. Pass n = 1 on the first check of the turn, 2 on the next... (loop guard; at most 3 per turn the rest arrive at turn end).\n" +
156
- "- action:'status': NON-BLOCKING progress query returns immediately and consumes nothing. Give the spawn's id for one child ({id, role, status: running|queued|done, model, elapsedSec, turn, maxTurns, position?), or omit it for an overview of the whole pool ({overview: {running: [{id, role, model, elapsedSec, turn, maxTurns}], queued: [{id, role, position}], done: [{id, role}]}}). §19.5.6 touched-files summary: running entries also carry touchedFiles (first 5, relative to your cwd), touchedMore (count beyond 5) and, when nothing was touched yet, the placeholder touched (\"—(尚无改动)\"); queued (not yet started) entries carry the placeholder touched (\"—(未启动)\") see what a running child has changed BEFORE deciding to cancel it. Use THIS to check progress action:'check' blocks until the target finishes (checking progress with check is what hangs a parallel turn).\n" +
157
- "- action:'escalate' (飞刀 a flown-in expert): hand an implementation task to a STRONGER model from your consult models (agent.consultModels). It gets WRITE access and does the work itself reads, edits, runs tests then returns a post-op report (what changed, why, verification). Use it when YOU judge the task calls for stronger hands (complex multi-file refactoring, an intractable bug, intricate algorithm workor work beyond your comfortable ability); escalate EARLY, not after burning attempts. model: pick a candidate as 'provider:model' (default = the first consult model). Not available in engineering mode (implementation goes through eng-coder spawns there).\n" +
158
- "- action:'cancel': STOP one background subagent pass the id from the async spawn return (REQUIRED omitting it errors; a blanket cancel is unsupported, Ctrl+C stops everything). Running target aborts immediately ({id, status:'cancelled'}); a queued target is removed from the queue ({id, status:'cancelled', was:'queued'} and later queue positions shift forward). Other children and the session keep running cancellation is targeted. Use it when a background child is going the wrong way (e.g. burning turns) and you must stop it before its report arrives. Cancel is a last resort: verify alarming signals with reliable checks (git/node not guesses) first; prefer scoped recovery (restore a single affected file) over killing the child a running child's in-flight work dies with it, partial changes stay unmerged and unaudited.\n" +
119
+ "- action:'status': NON-BLOCKING progress query — returns immediately and consumes nothing. Give the spawn's id for one child ({id, role, status: running|queued|done, model, elapsedSec, turn, maxTurns, position?), or omit it for an overview of the whole pool ({overview: {running: [{id, role, model, elapsedSec, turn, maxTurns}], queued: [{id, role, position}], done: [{id, role}]}}). §19.5.6 touched-files summary: running entries also carry touchedFiles (first 5, relative to your cwd), touchedMore (count beyond 5) and, when nothing was touched yet, the placeholder touched (\"—(尚无改动)\"); queued (not yet started) entries carry the placeholder touched (\"—(未启动)\") see what a running child has changed BEFORE deciding to cancel it. Use THIS to see progress — it never blocks and never consumes a result (async results are delivered to you automatically). Background advisor reviews share this status surface (AGENT-LOOP.md §18): pass a review id and it answers in the same shape with role:'advisor' plus reviewType (design|code) / round / elapsedSec and the overview lists reviews alongside subagents.\n" +
120
+ "- action:'observe': SEE what a running async subagent is DOING right now (progress vs stuck) — pass the spawn id. Returns {id, role, status, turn, maxTurns, touched…, currentTool? — array of tool name(s) currently executing (read from its dispatch state; omitted when none in flight present when stuck in a long tool call), recentTurns: [last N one-line turn summaries, newest-first; default 5, parameterizable via recent]}. Readonly observable on running/queued/done: running shows live activity, queued (not started) returns a placeholder, done (settled, report auto-delivered) returns the activity summary only NOT the full report (that rides the auto channel; observe stays terse to keep your context lean). Use it to judge whether a long-running child is stuck vs progressing BEFORE deciding to cancel or steer it.\n" +
121
+ "- action:'send': STEER a running async subagent mid-flight pass the spawn id + message (a direction like \"check X, don't fixate on Y\"). The message queues and the child consumes it at its next turn boundary as an ORDINARY user instruction (non-interrupting its current tool finishes first; its convergence/audit discipline is unchanged injection is guidance, not a deviation waiver). Returns {id, status:'delivered', queued}. Only a RUNNING async subagent is targetable sync (you're waiting on it, no relay), queued (not started), settled or unknown ids error clearly. If the child settles before its next turn boundary, its settle report carries an 'undelivered' note so you don't assume the guidance landed.\n" +
122
+ "- action:'escalate' (飞刀 — a flown-in expert): hand an implementation task to a STRONGER model from your consult models (agent.consultModels). It gets WRITE access and does the work itself — reads, edits, runs tests then returns a post-op report (what changed, why, verification). Use it when YOU judge the task calls for stronger hands (complex multi-file refactoring, an intractable bug, intricate algorithm work or work beyond your comfortable ability); escalate EARLY, not after burning attempts. model: pick a candidate as 'provider:model' (default = the first consult model). Not available in engineering mode (implementation goes through eng-coder spawns there). DEFAULT-ASYNC at depth 0 (AGENT-LOOP.md §25 D-R17b): the launch returns an ack {id, role:'escalate', status:'running'} and the flight runs in the background (pooled with the other role-domain spawns) its post-op report is delivered to you automatically with its mutations merged into your bookkeeping.\n" +
123
+ "- action:'cancel': STOP one background subagent — pass the id from the async spawn return (REQUIRED — omitting it errors; a blanket cancel is unsupported, Ctrl+C stops everything). Running target aborts immediately ({id, status:'cancelled'}); a queued target is removed from the queue ({id, status:'cancelled', was:'queued'} and later queue positions shift forward). Other children and the session keep running — cancellation is targeted. Use it when a background child is going the wrong way (e.g. burning turns) and you must stop it before its report arrives. Cancel is a last resort: verify alarming signals with reliable checks (git/node — not guesses) first; prefer scoped recovery (restore a single affected file) over killing the child — a running child's in-flight work dies with it, partial changes stay unmerged and unaudited. Background advisor reviews are cancelable on this same action (AGENT-LOOP.md §18): pass the review id — its controller aborts, no token is issued for a cancelled review.\n" +
124
+ "- action:'consume-design' (engineering mode, parent side — chain-terminal token consumption): after the delivery is verified and the chain closes out, consume this design's token slot — pass the designId (optional for a single-design session). The slot is consumed; a further spawn for the same designId is mechanically rejected, and any new work (including deviation fixes) requires a fresh design review and token. Idempotent: an unknown designId / already-consumed slot is a no-op notice, never an error. Do NOT call it while the chain is still open — fix rounds reuse the same slot (same designId + designToken).\n" +
159
125
  "- action:'panel': DIAGNOSE + fix the subagent panel — the collapsible blocks under the conversation the user sees (CLI TUI panel mirror; headless/VS Code degrade to a 'no panel' pool view). view (default — call it with no params or view:true): returns the live panel blocks [{key, role, status: running|done|awaitingDigest} — running entries also carry elapsedSec; awaitingDigest entries whose report is ALREADY digested carry digested:true (stuck blocks — the freezable ones — explain odd panel states here)] exactly as the user sees them. freeze: pass the block key of a digested-stuck block ({action:'panel', freeze:'role#N'}) to reclaim it into the conversation — the freeze ONLY passes for awaitingDigest blocks with no live pool entry and no pending report (gated); freezing a block whose report is still pending would break the digestion order and is refused with a clear error.\n\n" +
160
126
  "Why delegate? A sub-agent runs in its own isolated context — its reads, searches, tool calls and edits never enter your history or pollute your window; only its final report comes back. Delegation keeps your working context lean (you see the whole session, not the child's noise) and the child single-mindedly focused on one task. Parallel children run concurrently, saving wall-clock time. Every coder/eng-coder child carries its own verify + advisor self-review discipline — handed-off work is already verified before you read a word of it.\n\n" +
161
127
  "Available roles (which roles are exposed depends on the active mode — see Mode filtering below):\n" +
162
128
  "- explore — read-only search & analysis. Toolset: the read/search family (grep, read, glob, code_search, doc_search, repo_outline, lsp, tree...). No git context injected—evidence from read/glob/grep and the task book. Its report must list what it searched and what it did NOT find. Fast — specify thoroughness in the task: quick / medium / thorough (default medium).\n" +
163
129
  "- plan — read-only implementation planning. Same read/search toolset; NEVER edits files. Returns a step-by-step plan for the parent to execute.\n" +
164
130
  "- coder — full implementation. The parent's complete read/write/execute toolset plus verify and advisor for self-review. Its final report must include a delivery transparency table with one row per task requirement (Done / Simplified / Not done — no deferred column).\n" +
165
- "- eng-coder — engineering-mode coder (available only in engineering mode, replacing coder). Same full toolset as coder plus the design-driven methodology overlay; REQUIRES a valid designToken arg obtained from a passed advisor(type='design') review. The advisor's Approved reply also echoes a designId — pass it as the designId arg: required to pick between designs when several approved reviews are active, optional for a single design. The delivery report echoes the designId back for the audit fix round.\n" +
166
- "Mode filtering: normal mode exposes explore/plan/coder; engineering mode exposes explore/plan/eng-coder. The schema enum reflects the active mode.\n\n" +
167
- "Async spawn (AGENT-LOOP.md §15/§18): pass async:true to spawn WITHOUT waiting returns {id, role, status:\"running\"} immediately so you can keep working in your own turn (read/check files, run other tools) while the child runs in the background. Fetch the report later with action:'check' — multiple async children return in completion (arrival) order, first finished first, so fast results are handled immediately. Query progress with action:'status' (non-blocking) — action:'check' BLOCKS until the target finishes. The DEFAULT is role-level: role='eng-coder' spawns async (its delivery protocol runs fully inside the child — implementation → audit → self-fix → advisor re-review → converged delivery; pass async:false only when you must handle the report synchronously); every other role defaults to blocking. Use async when your own turn must keep moving; use a blocking spawn when you must see the report before continuing. Async spawns are capped at 4 concurrent (further spawns queue with a position), and top-level only.\n\n" +
168
- "Task scheduling (AGENT-LOOP.md §20): declare the scheduling metadata to let the SCHEDULER order your spawns files: the file paths this task will modify, dependsOn: ids from prior async spawn returns whose outcome this task needs. Overlapping-file tasks are serialized and dependent tasks are started in order automatically: a spawn that would conflict, or whose dependencies have not settled, queues instead of running ({id, status:\"queued\", position, reason} the waiting task auto-starts when the conflict clears / its dependency settles; cancel a queued task to drop it). A spawn whose dependency was cancelled or failed stays queued and marked \"dependency cancelled\" until you decide (cancel it) — in an AUTO session it starts by itself. Referencing an unknown id errors; an id already consumed by check counts as satisfied. Omit both parameters for the plain immediate spawn (no scheduler involvement).\n\n" +
131
+ "- eng-coder — engineering-mode coder (available only in engineering mode, replacing coder). Same full toolset as coder plus the design-driven methodology overlay; REQUIRES a valid designToken arg obtained from a passed advisor(type='design') review. The advisor's Approved reply also echoes a designId — pass it as the designId arg: required to pick between designs when several approved reviews are active, optional for a single design. The delivery report echoes the designId back for the audit fix round. ALSO REQUIRES a batchDoc arg — the batch record path (docs/batches/<batch>-<topic>.md), the batch §2 task book this spawn implements: a spawn without it, or with a path that does not resolve to a readable file, is mechanically refused.\n" +
132
+ "- eng-designer engineering-mode design writer (available only in engineering mode): the SOLE author of the requirements + design documents and of the batch record §2 (the batch task book) — revisions included. Writes no implementation code, does not edit prompt files, does not fire reviews, and needs NO designToken (its authorization is the confirmed requirements). It surveys on its own, but may only spawn read-only 'explore' children (sync, ≤6 per batch). ALSO REQUIRES a batchDoc arg — the batch record path (docs/batches/<batch>-<topic>.md); the same mechanical gate as eng-coder: a spawn without it, or with a path that does not resolve to a readable file, is mechanically refused.\n" +
133
+ "Mode filtering: normal mode exposes explore/plan/coder; engineering mode exposes explore/plan/eng-designer/eng-coder. The schema enum reflects the active mode.\n\n" +
134
+ "Async spawn (AGENT-LOOP.md §15/§18/§11.1): pass async:true to spawn WITHOUT waiting returns {id, role, status:\"running\"} immediately so you can keep working in your own turn (read/check files, run other tools) while the child runs in the background. The child's report is delivered to you automatically — there is no fetch action; use action:'status' only to see progress, never to wait for the result. Top-level spawns are ALWAYS async never pass `async:false` at depth-0 (the report arrives automatically; if your next step needs it, end the turn and let the digest deliver it). Inside subagents (depth>0) spawns are always synchronous (platform rule). Eng-coder's delivery protocol runs fully inside the child (implementation audit self-fix advisor re-review converged delivery). Async spawns are pooled per role domain (AGENT-LOOP.md §11.1): at most 4 concurrent eng-coders and 4 concurrent other-role spawns by default (agent.poolLimits overrides both) — a full domain queues further spawns with a position while the other domain keeps starting (domains never block each other), and top-level only. After an async spawn the turn winds down normally — nothing expects you to wait for it: the child runs in the background and its report is delivered to you automatically — before your next turn, or digested in the suspension session — so end the turn; do not poll or wait for the result.\n\n" +
135
+ "Task scheduling (AGENT-LOOP.md §20): declare the scheduling metadata to let the SCHEDULER order your spawns — files: the file paths this task will modify, dependsOn: ids from prior async spawn returns whose outcome this task needs. Overlapping-file tasks are serialized and dependent tasks are started in order automatically: a spawn that would conflict, or whose dependencies have not settled, queues instead of running ({id, status:\"queued\", position, reason} — the waiting task auto-starts when the conflict clears / its dependency settles; cancel a queued task to drop it). A spawn whose dependency was cancelled or failed stays queued and marked \"dependency cancelled\" until you decide (cancel it) — in an AUTO session it starts by itself. Referencing an unknown id errors; an id already consumed (auto-delivered by the auto channel) counts as satisfied. Omit both parameters for the plain immediate spawn (no scheduler involvement).\n\n" +
169
136
  "Writing the prompt:\n" +
170
137
  "- The sub-agent starts with zero context — it has not seen this conversation. Brief it like a colleague who just walked into the room: state the goal, list what you already know, hand over the specifics.\n" +
171
138
  "- Put exact paths and commands in the prompt when you know them. The sub-agent should not search for things you already know.\n" +
@@ -174,20 +141,22 @@ export const subagentTool = {
174
141
  parameters: {
175
142
  type: "object",
176
143
  properties: {
177
- action: { type: "string", enum: ["spawn", "check", "status", "escalate", "cancel", "panel"], description: "Which subagent-family action — spawn (default), check (fetch async results — BLOCKS until the target finishes and consumes the report), status (non-blocking progress querynever consumes), escalate (飞刀 — hand implementation to a stronger consult model), cancel (stop ONE background subagent — pass its id; never omit), panel (view the live subagent panel / freeze a digested-stuck block — §19.6). See the tool description for the full action matrix." },
144
+ action: { type: "string", enum: ["spawn", "status", "escalate", "cancel", "panel", "consume-design", "observe", "send"], description: "Which subagent-family action — spawn (default), status (non-blocking progress query — never consumes; async results arrive automatically no fetch action), observe (inspect a running/queued/done async subagent's recent activity — recent turn summaries + current in-flight tool + turn/touched — §7.2), send (inject a direction into a RUNNING async subagent, consumed at its next turn boundary as an ordinary instruction §7.2), escalate (飞刀 — hand implementation to a stronger consult model), cancel (stop ONE background subagent — pass its id; never omit), panel (view the live subagent panel / freeze a digested-stuck block — §19.6), consume-design (engineering mode, parent side: chain-terminal token consumption — close out a design's token slot after the delivery is verified and the chain closes out — §2.6, 2026-09-07). See the tool description for the full action matrix." },
178
145
  view: { type: "boolean", description: "action:'panel' only: true (default) = return the live panel blocks (the mirror of what the user sees). false with no freeze = nothing to do — error. Mutually exclusive with freeze (freeze wins)." },
179
146
  freeze: { type: "string", description: "action:'panel' only: block key of a digested-stuck awaitingDigest block (e.g. \"eng-coder#9\") to reclaim into the conversation via the gated done-freeze event. Refused when the block is running/done/unknown or its report is still pending digestion (would break the digestion order). Requires the CLI TUI panel mirror — headless/VS Code report the freeze unavailable." },
180
- task: { type: "string", description: "Required for action:'spawn' (the self-contained task brief) and action:'escalate' (goal, constraints, entry files, acceptance criteria). Not used by check/status." },
147
+ task: { type: "string", description: "Required for action:'spawn' (the self-contained task brief) and action:'escalate' (goal, constraints, entry files, acceptance criteria). Not used by status." },
181
148
  context: { type: "string", description: "Optional background the sub-agent needs (it cannot see this conversation); action:'spawn' only." },
182
- role: { type: "string", enum: ["explore", "plan", "coder", "eng-coder"], description: "The sub-agent role — see the tool description for the role capability matrix. Exact spelling required. action:'spawn' only (escalate spawns its own expert internally)." },
149
+ role: { type: "string", enum: ["explore", "plan", "coder", "eng-coder", "eng-designer"], description: "The sub-agent role — see the tool description for the role capability matrix. Exact spelling required. action:'spawn' only (escalate spawns its own expert internally)." },
183
150
  model: { type: "string", description: "action:'spawn': provider/model override for this sub-agent ('provider:model', a provider name, or a model name on the parent's provider — defaults to config.agent.subagentModels[role], then config.agent.subagentModel, then the parent's provider). pass \"default\" to explicitly inherit the default model — equivalent to omitting the parameter. action:'escalate': pick a consult candidate as 'provider:model' (default = the first consult model)." },
184
151
  designToken: { type: "string", description: "Required when role='eng-coder': the token returned by advisor(type='design') after the design review passed. Without a valid token, eng-coder cannot modify files." },
185
- designId: { type: "string", description: "Optional when role='eng-coder': the designId echoed with the approved token by advisor(type='design'). Required to pick between designs when several approved reviews are active in the session — each eng-coder carries its own designId+token pair so parallel implementations never overwrite each other. Optional for a single design." },
186
- async: { type: "boolean", description: "true = spawn without waitingreturns {id, status:\"running\"} immediately, fetch results later via action:'check'. Default is role-level: role='eng-coder' true (async; its internal delivery protocol runs in the background pass async:false to force the blocking spawn when you must process the report before continuing); all other roles → false (blocking)." },
152
+ designId: { type: "string", description: "Optional when role='eng-coder': the designId echoed with the approved token by advisor(type='design'). Required to pick between designs when several approved reviews are active in the session — each eng-coder carries its own designId+token pair so parallel implementations never overwrite each other. Optional for a single design. action:'consume-design': the design whose slot to close out — optional for a single-design session; required to pick when several approved designs are active (the consume gate refuses to guess)." },
153
+ batchDoc: { type: "string", description: "REQUIRED for role='eng-coder' and role='eng-designer': the batch record path (docs/batches/<batch>-<topic>.md) the batch §2 task book this spawn implements (or writes). The spawn is mechanically refused without it, and also when the path does not resolve (cwd-relative or absolute) to a readable file; the CONTENT is never validated (the batch record owns that). explore/plan/coder spawns ignore it." },
154
+ async: { type: "boolean", description: "action:'spawn': true = spawn without waiting — returns {id, status:\"running\"} immediately; the report is delivered to you automatically (there is no fetch action). Default: depth-0 → true (async — every role, AGENT-LOOP.md §18 D-E1a); depth>0 → sync (forced). action:'escalate': same semantics (AGENT-LOOP.md §25 D-R17b) — default async at depth 0; async:false keeps the legacy synchronous flight (mechanism parameter — see the Async spawn section for top-level guidance)." },
187
155
  files: { type: "array", items: { type: "string" }, description: "action:'spawn' only: the file write-domain this task declares (cwd-relative or absolute paths). files must be file-level paths (one per file you will modify). Directory declarations are NOT supported — they bypass the conflict detector and are rejected with an error. Tasks with overlapping files are serialized automatically — a conflicting spawn queues ({id, status:\"queued\", position, reason}) instead of running concurrently and starts when the conflict clears. Omit to skip conflict detection (plain immediate spawn)." },
188
- dependsOn: { type: "array", items: { type: "string" }, description: "action:'spawn' only: ids from prior async spawn returns whose outcome this task needs — the task queues ({id, status:\"queued\", position, reason}) until every dependency settles, then starts automatically. Ids consumed by action:'check' count as satisfied; a dependency cancelled or failed leaves the task queued marked 'dependency cancelled' until you decide (cancel it — AUTO sessions auto-start). Unknown ids error." },
189
- id: { type: "string", description: "action:'check'/'status'/'cancel': the subagent id from the async spawn return. check: omit = the next completed child (arrival order); status: omit = overview of the whole pool; cancel: REQUIRED (never omit — a blanket cancel is unsupported)." },
190
- n: { type: "number", description: "action:'check' (required): 1-based read counter1 for the first check of the turn, incrementing with each subsequent check (loop detectorconsecutive checks must be distinct tool calls)." },
156
+ dependsOn: { type: "array", items: { type: "string" }, description: "action:'spawn' only: ids from prior async spawn returns whose outcome this task needs — the task queues ({id, status:\"queued\", position, reason}) until every dependency settles, then starts automatically. Ids already consumed (auto-delivered to the model) count as satisfied; a dependency cancelled or failed leaves the task queued marked 'dependency cancelled' until you decide (cancel it — AUTO sessions auto-start). Unknown ids error." },
157
+ id: { type: "string", description: "action:'status'/'observe'/'send'/'cancel': the subagent id from the async spawn return. status: omit = overview of the whole pool; observe/send/cancel: REQUIRED (observe needs the child to inspect; send needs the target; never omit on cancel — a blanket cancel is unsupported)." },
158
+ message: { type: "string", description: "action:'send' only (REQUIRED there): the direction to inject the running async subagent consumes it at its next turn boundary as an ordinary user instruction (non-interrupting; its convergence/audit discipline is unchanged injection is guidance, not a deviation waiver)." },
159
+ recent: { type: "integer", description: "action:'observe' only (optional, default 5): how many recent-turn summaries to return (clamped 1..20 — N2 keeps observe terse)." },
191
160
  },
192
161
  required: [],
193
162
  },
@@ -201,12 +170,12 @@ export const subagentTool = {
201
170
  ? String(args.action)
202
171
  : "spawn"
203
172
  if (action !== "spawn") {
204
- // §19 restricted-variant action gate (round2 #3): the eng-coder audit
205
- // channel (depth>0, role eng-coder) is spawn-only — escalate spawns a
206
- // coder+WRITE child (violates explore-only intent) and check/status/panel
173
+ // §19 restricted-variant action gate (round2 #3): the engineering-child channel
174
+ // (depth>0, role eng-coder or eng-designer) is spawn-only — escalate spawns a
175
+ // coder+WRITE child (violates explore-only intent) and status/panel/observe/send
207
176
  // have no async pool / panel mirror to query in a child context.
208
- if ((ctx.depth ?? 0) > 0 && ctx.agent?._role === "eng-coder") {
209
- throw new Error(`only action:'spawn' (sync explore audits) is available inside an eng-coder — escalate/check/status/cancel/panel are not (AGENT-LOOP.md §19 D-M3)`)
177
+ if ((ctx.depth ?? 0) > 0 && (ctx.agent?._role === "eng-coder" || ctx.agent?._role === "eng-designer")) {
178
+ throw new Error(`only action:'spawn' (sync explore children) is available inside an ${ctx.agent._role} — escalate/status/cancel/panel/consume-design/observe/send are not (AGENT-LOOP.md §19 D-M3)`)
210
179
  }
211
180
  // §17 N3/D-S6 spawn gate (manual tier): auto-turn digests may not spawn —
212
181
  // async OR blocking — the digest must stay organize-only. The escalate
@@ -215,16 +184,26 @@ export const subagentTool = {
215
184
  if (action === "escalate" && ctx.agent?._inAutoTurn && !ctx.agent?.autoApprove) {
216
185
  return JSON.stringify({ status: "error", error: "cannot spawn subagents from a manual auto-turn — wait for user input" })
217
186
  }
218
- if (action === "check") return await executeCheckAction(args, ctx)
219
187
  if (action === "status") return executeStatusAction(args, ctx)
220
188
  if (action === "escalate") return await executeEscalateAction(args, ctx)
221
189
  // §19.5 控制类动作:digest 内放行(D-S7 分类——控制/自省;dispatch 控制类
222
190
  // 豁免同批生效——19.5.2b round2 #4;escalate 的 digest 拒绝在上一分支)
223
191
  if (action === "cancel") return executeCancelAction(args, ctx)
192
+ // 2026-09-07 token 链终消费制(ENGINEERING-MODE.md §2.6 F1):父侧核销消费——
193
+ // 非只读控制动作——depth-0 + 工程模式限定(本分流已过受限变体门;工程模式门在
194
+ // 执行器内)——planMode 拒绝(dispatch 不豁免)——不入批审批分组(dispatch 免审)。
195
+ if (action === "consume-design") return executeConsumeDesignAction(args, ctx)
224
196
  // §19.6 panel 动作:view(readonly 面——digest 内放行——自省类)与 freeze
225
197
  // (控制类——同 cancel——digest 内放行)。深度/门控检查在 executePanelAction 内。
226
- if (action === "panel") return executePanelAction(args, ctx)
227
- throw new Error(`Unknown subagent action: ${JSON.stringify(action)}. Valid actions: spawn, check, status, escalate, cancel, panel.`)
198
+ // CLI-ACTIVITY-DEBLOAT F-3(2026-09-10)接线:executePanelAction ctx.state
199
+ // (= agent._tuiState——startTUI 反向挂载)读时现算面板块(computePanelBlocks)——
200
+ // 手工面板镜像已退役。headless/VSC 无挂载 → 现算返 null → 降级照旧。
201
+ if (action === "panel") return executePanelAction(args, { ...ctx, state: ctx.agent?._tuiState })
202
+ // SUBAGENT-OBSERVE-SEND:observe = readonly 查询(同 status——digest/planMode 放行);
203
+ // send = 控制类豁免(同 cancel——父回合内显式调用即授权)。深度门在各自执行器内。
204
+ if (action === "observe") return executeObserveAction(args, ctx)
205
+ if (action === "send") return executeSendAction(args, ctx)
206
+ throw new Error(`Unknown subagent action: ${JSON.stringify(action)}. Valid actions: spawn, status, escalate, cancel, panel, consume-design, observe, send.`)
228
207
  }
229
208
 
230
209
  const parent = ctx.agent
@@ -235,17 +214,20 @@ export const subagentTool = {
235
214
  if (typeof args.task !== "string" || !args.task.trim()) {
236
215
  throw new Error("subagent action:'spawn' requires a task (the self-contained task brief).")
237
216
  }
238
- // §18 F1/D-E1 role-level async default: eng-coder spawns async unless the
239
- // caller explicitly passes async:false; every other role stays blocking.
240
- const wantAsync = args.async ?? role === "eng-coder"
217
+ // §18 D-E1a depth-gated async default (2026-09-06 需求池 R12): depth-0 spawns
218
+ // default to async for EVERY role (the old role-level default — eng-coder only —
219
+ // is superseded); depth>0 spawns default to sync (子代理内部强制同步现状保留).
220
+ // async:false remains the explicit escape hatch; async:true at depth>0 is
221
+ // refused downstream (executeAsyncSpawn top-level gate).
222
+ const wantAsync = args.async ?? ((ctx.depth ?? 0) === 0)
241
223
 
242
224
  // Role normalization + whitelist (2026-08-25, coder-leak fix): exact-string gates let
243
225
  // variant roles ("Coder", " coder") bypass BOTH mode gates and fall through to
244
226
  // full tools / no overlay — a full-write coder without design review. Schema enums are
245
227
  // advisory; providers don't enforce them. Fail closed on unknown roles.
246
- const ROLES = new Set(["explore", "plan", "coder", "eng-coder"])
228
+ const ROLES = new Set(["explore", "plan", "coder", "eng-coder", "eng-designer"])
247
229
  if (!ROLES.has(role)) {
248
- throw new Error(`Unknown subagent role: ${JSON.stringify(role)}. Valid roles: explore, plan, coder, eng-coder (exact spelling).`)
230
+ throw new Error(`Unknown subagent role: ${JSON.stringify(role)}. Valid roles: explore, plan, coder, eng-coder, eng-designer (exact spelling).`)
249
231
  }
250
232
  // §18 D-E3 internal-spawn gate: an eng-coder sub-agent may only spawn sync
251
233
  // explore (audit) children — non-explore roles and async are refused here
@@ -254,13 +236,19 @@ export const subagentTool = {
254
236
  // augmentation below. Runs BEFORE the mode gates so the eng-coder-specific
255
237
  // error (not the generic engineering-mode one) surfaces.
256
238
  const engAuditAttempt = gateEngCoderSpawn(ctx.agent, ctx.depth, role, args.async)
257
- // Role is mutually exclusive per mode: normal mode → "coder", engineering mode → "eng-coder"
239
+ // Role is mutually exclusive per mode: normal mode → "coder", engineering mode → "eng-coder"/"eng-designer"
258
240
  if (parent.config?.agent?.engineering && role === "coder") {
259
- throw new Error("Engineering mode: use role='eng-coder' for implementation tasks.")
241
+ throw new Error("Engineering mode: role='coder' is disabled — use role='eng-coder' for implementation tasks (or role='eng-designer' for design writing).")
260
242
  }
261
243
  if (!parent.config?.agent?.engineering && role === "eng-coder") {
262
244
  throw new Error("Engineering mode is not active — use role='coder' for implementation tasks.")
263
245
  }
246
+ // Third mode gate (ENGINEERING-MODE.md §2.15 A—— symmetric completion):
247
+ // eng-designer is engineering-mode-only, same family as eng-coder (both carry
248
+ // the engineering discipline overlay + the batchDoc gate).
249
+ if (!parent.config?.agent?.engineering && role === "eng-designer") {
250
+ throw new Error("Engineering mode is not active — role='eng-designer' is engineering-mode only (it writes the requirements/design documents inside the engineering workflow); use role='explore' or role='plan' for read-only work.")
251
+ }
264
252
 
265
253
  // §17 N3/D-S6 spawn gate (manual tier): auto-turn digests may not spawn — async
266
254
  // OR blocking — the digest must stay organize-only. AUTO tier (autoApprove) is
@@ -271,208 +259,17 @@ export const subagentTool = {
271
259
  return JSON.stringify({ status: "error", error: "cannot spawn subagents from a manual auto-turn — wait for user input" })
272
260
  }
273
261
 
274
- // ── §20 spawn 调度参数准入(AGENT-LOOP.md §20 D-SD1/D-SD3 + 20.4 round2 #5/#7)──
275
- // files/dependsOn 声明即契约(v1:不做任务书文本自动解析——不可靠)。缺省(两者皆
276
- // 缺)= 既有语义零改动(不参与冲突检测/无校验——legacy spawn 零开销直通)。
277
- // 校验序:参数形态 依赖 unknown id(非 consumed 墓碑——T-SD10)→ 依赖环可达
278
- // (T-SD5——防御断言:自然流程不可达)→ 等待态判定。判定结果:wait/depc 阻塞 →
279
- // async 入 queued 等位(spawn 返回带 reason——D-SD3b);**sync spawn(async:false)
280
- // 命中阻塞 → 明确错误——不队列化 sync——sync 语义零变更(round2 #7——T-SD13)**。
281
- const filesRaw = args.files
282
- const dependsRaw = args.dependsOn
283
- // §20.8 D-F1.1:目录声明 fail-closed——检测器 throw → catch → 错误即工具结果
284
- // (模型可见"文件级明细"提示——不加静默——目录绕过冲突检测的通道闭合)。
285
- let files = []
286
- if (filesRaw !== undefined && filesRaw !== null) {
287
- try {
288
- files = normalizeFileList(filesRaw, parent.cwd)
289
- } catch (e) {
290
- return JSON.stringify({ status: "error", error: e.message })
291
- }
292
- }
293
- if (filesRaw !== undefined && filesRaw !== null && !Array.isArray(filesRaw)) {
294
- throw new Error("subagent files must be an array of file paths (the write domain this task declares)")
295
- }
296
- const dependsOn = []
297
- if (dependsRaw !== undefined && dependsRaw !== null) {
298
- if (!Array.isArray(dependsRaw)) throw new Error("subagent dependsOn must be an array of async subagent ids (from prior spawn returns)")
299
- for (const d of dependsRaw) {
300
- if (typeof d !== "string" && typeof d !== "number") {
301
- throw new Error(`subagent dependsOn entries must be async subagent ids — got ${JSON.stringify(d)}`)
302
- }
303
- dependsOn.push(String(d))
304
- }
305
- }
306
- if (files.length > 0 || dependsOn.length > 0) {
307
- for (const d of dependsOn) {
308
- if (depInfo(parent, d).state === "unknown") {
309
- throw new Error(`subagent dependsOn: unknown async subagent id: ${d} — dependsOn references ids from prior async spawn returns; an id already consumed by action:'check' (or auto-injected) counts as satisfied, anything else is a mistake (AGENT-LOOP.md §20 D-SD5)`)
310
- }
311
- }
312
- assertNoDepCycle(parent, dependsOn)
313
- const block = describeBlockers(parent, { _files: files, _dependsOn: dependsOn })
314
- if (!wantAsync && block.kind !== "slot") {
315
- 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)`)
316
- }
317
- }
318
-
319
- // Provider/model override: tool `model` arg > subagentModels[role] > subagentModel > parent provider
320
- const childProvider = resolveChildProvider(parent, effectiveSubagentModel(parent, role, args.model))
321
-
322
- // eng-coder token gate: the design review must have passed and the caller must
323
- // present the exact token advisor issued — otherwise the child is not authorized to code.
324
- // 2026-09-01: multi-design slots — the token is located by designId (exact slot,
325
- // single-slot fallthrough); HMAC/TTL validation itself is unchanged.
326
- let issuedToken
327
- if (role === "eng-coder") {
328
- issuedToken = resolveDesignSlot(parent, args.designId).token
329
- if (!issuedToken || args.designToken !== issuedToken || !validateDesignToken(args.designToken)) {
330
- throw new Error("Invalid or missing design token — run advisor with type='design' first and pass the returned token as designToken.")
331
- }
332
- }
333
-
334
- // Filter tool set by role: explore/plan are read-only (plan is a planning agent, its deliverable is the plan itself)
335
- let tools
336
- if (role === "explore" || role === "plan") {
337
- const allowed = readonlyToolNames(parent.tools)
338
- tools = parent.tools.filter((t) => allowed.has(t.name))
339
- } else {
340
- tools = parent.tools
341
- }
342
-
343
- // Select prompt overlay by role
344
- let overlay = ""
345
- if (role === "explore") overlay = EXPLORE_OVERLAY
346
- else if (role === "coder") overlay = CODER_OVERLAY
347
- else if (role === "plan") overlay = PLAN_OVERLAY
348
- else if (role === "eng-coder") overlay = ENG_CODER_OVERLAY
349
-
350
- // explore/plan: force read-only permission; coder/default: AUTO passes through directly,
351
- // manual mode queues permission requests for the parent agent's approval UI (human in the loop, child agent is no longer silently rejected)
352
- let childPermission
353
- if (role === "explore" || role === "plan") {
354
- childPermission = async () => false
355
- } else if (parent.autoApprove) {
356
- childPermission = async () => true
357
- } else {
358
- childPermission = async (name, toolArgs) => {
359
- if (!ctx.onPermissionRequest) return false
360
- const ask = () => ctx.onPermissionRequest(`${role ?? "sub"}/${name}`, toolArgs)
361
- // Queue parallel child agent permission requests to avoid two popups simultaneously overwriting each other (lesson from question tool)
362
- parent._permQueue = (parent._permQueue ?? Promise.resolve()).then(ask, ask)
363
- return parent._permQueue
364
- }
365
- }
366
-
367
- // eng-coder: force engineering=true on child config so setup.mjs applies engineering prompt
368
- const childConfig = role === "eng-coder"
369
- ? { ...parent.config, agent: { ...parent.config.agent, engineering: true } }
370
- : parent.config
371
-
372
- const child = createAgent({
373
- provider: childProvider,
374
- tools,
375
- config: childConfig,
376
- cwd: parent.cwd,
377
- memory: parent.memory,
378
- overlay,
379
- role,
380
- })
262
+ // §20 准入(2026-09-05 module-split——prepareScheduling verbatim
263
+ // subagent-spawn.mjs:参数形态/unknown id/依赖环/阻塞 sync 判定;files 目录声明
264
+ // fail-closed——检测器错误即工具结果 JSON)
265
+ const prep = prepareScheduling(parent, args.files, args.dependsOn, wantAsync)
266
+ if (prep.errorJson) return prep.errorJson
381
267
 
382
- // Token-verified design review → child is authorized to modify files without re-reviewing
383
- if (role === "eng-coder") child._engDesignReviewed = true
384
- // §18 D-E3 task-domain authorization: approved design + spawn task = authorization.
385
- // The child's OWN tools skip ONLY the onPermissionRequest ask (autoApprove
386
- // equivalent dispatch.mjs permission stage); every other gate (JSON parse /
387
- // unknown tool / planMode / design-token) still applies (T-E14). Non-eng-coder
388
- // children keep the manual per-write parent approval (human in the loop).
389
- if (role === "eng-coder") child._engTaskAuthorized = true
390
- // designId+token ride the child bookkeeping: the delivery report carries the designId
391
- // so the divergence-audit fix round re-spawns with the SAME slot (2026-09-01 FR3).
392
- if (role === "eng-coder" && issuedToken) {
393
- child._engDesignId = args.designId ?? null
394
- child._engDesignToken = issuedToken
395
- }
396
-
397
- // §18.5 子代理零 git(D-AG1——2026-09-04 用户裁定):explore/plan 一律不注入
398
- // git 上下文——子代理证据链 = 任务书 ∪ 磁盘当前状态(read/glob/grep)∪(审计时)
399
- // _touchedFiles,无一项来自 git;注入的全工作区脏状态快照与任务域无关,会误导
400
- // 审计/探索("status 里这个文件算不算超清单?")。注入分支整体删除(B 方案
401
- // git 只读变体亦随裁定废弃——D-AG5)。顶层主 agent 注入保留(§3 prepareRun——
402
- // setup.mjs depth===0——D-AG7 范围边界)。
403
- let input = args.context ? `Context:\n${args.context}\n\nTask:\n${args.task}` : args.task
404
- // §18 D-E2 ③ (round4 #4, T-E13/T-E15): an eng-coder audit spawn's task book is
405
- // the eng-coder's OWN spawn task — mechanically kept as _engTaskInput by the
406
- // parent spawn and injected as the D-TS5 A2 mechanical summary (design docs /
407
- // affected-file list / acceptance criteria verbatim, verbose context dropped)
408
- // — ∪ the mechanically tracked _touchedFiles — NEVER the eng-coder's
409
- // self-written list: a self-report could omit exactly the out-of-scope file
410
- // the audit must catch.
411
- if (engAuditAttempt !== null) {
412
- const touched = (ctx.agent._touchedFiles ?? []).map((f) => `- ${f}`).join("\n") || "- (none yet)"
413
- input += `\n\n[Audit scope — mechanical context, independent of the eng-coder's self-report:]\n` +
414
- // §18.7 D-TS4 A1:审计指令模板(四类偏差 + 范围限制 + 校验清单格式)——审计语义
415
- // 不再靠模型自悟;范围限制是 §18.5 D-AG3 声明(下方 Zero-git scope authority)
416
- // 的同源一句指注,不重复声明。
417
- `[Audit instructions — mechanical template (AGENT-LOOP.md §18.7 D-TS4 A1):]\n` +
418
- `You are auditing an eng-coder delivery against its approved design — audit for EXACTLY these four deviation categories:\n` +
419
- `- PARTIAL: an acceptance criterion implemented partially or not at all;\n` +
420
- `- SILENT-SIMPLIFICATION: a "simpler approximation" of a specified behavior substituted for the spec;\n` +
421
- `- DOC-DRIFT: code changed without the owning design-doc section (module map / affected-files table) updated in the same delivery;\n` +
422
- `- OUT-OF-LIST: changes outside the approved file list.\n` +
423
- `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): ` +
424
- `workspace changes not listed there are unrelated to this delivery and are NOT grounds for an out-of-list finding.\n` +
425
- `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` +
426
- `Every deviation item MUST be fieldized: file:line + design reference (doc path + section/AC id) + severity + evidence (quoted code or doc text).\n` +
427
- // §18.7 D-TS5 A2:任务书从全量 verbatim 改机械摘要块(三要素逐字——排除冗长上下文)。
428
- `[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` +
429
- `${summarizeEngTaskBook(ctx.agent._engTaskInput)}\n` +
430
- `Files actually touched by the eng-coder (mechanical union — audit these against the file list):\n${touched}\n` +
431
- // §18.5 D-AG3(2026-09-04):审计零 git 范围权威声明——本审计任务零 git(不注入
432
- // git 上下文——§18.5 全角色零 git);_touchedFiles 为审计范围;工作区未列于
433
- // _touchedFiles 的改动与本任务无关,不作超清单依据(VS Code auditTaskBook 同款措辞)。
434
- "Zero-git scope authority (AGENT-LOOP.md §18.5 D-AG3): this audit task receives NO git context — nothing is injected. " +
435
- "The evidence base is the design documents, the current disk state (read/glob/grep), and the _touchedFiles list above. " +
436
- "Workspace changes NOT listed in _touchedFiles are unrelated to this delivery — they are NOT grounds for an out-of-file-list finding." +
437
- // §18.13 D-A1.2:审计预算句——A1 指令模板 + A2 摘要块之后、A3 报告模板之前(定序——评审 #7)。
438
- // 逐字设计锚(D-A1.2 代码块):只读该读的——10 轮机械预算——超时报 PROBLEM 下结论。
439
- // 前导 \n 与 A3 同款块分隔约定(上一句 Zero-git 句末无换行——不触碰既有句)。
440
- `\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` +
441
- // §18.7 D-TS6 A3:审计输出报告格式模板(三态——字段化行——不让模型自由发挥)。
442
- `\n[Audit report format — mechanical template (AGENT-LOOP.md §18.7 D-TS6 A3):]\n` +
443
- `Report EXACTLY one of three states:\n` +
444
- `- CLEAN — no deviation across the four categories: reply the line "Four deviation categories: none found." (四类偏差均未发现);\n` +
445
- `- DEVIATIONS — one row per deviation, every row fieldized: | category | file:line | design reference | severity | evidence |;\n` +
446
- `- PROBLEM — the audit itself could not run / inconclusive: state what blocked it.\n`
447
- }
448
- // The child's own task input rides the child object: an eng-coder's audit
449
- // spawns reuse it as the task-book SOURCE — injected as the D-TS5 A2
450
- // mechanical summary, not verbatim (see above).
451
- if (role === "eng-coder") child._engTaskInput = input
452
-
453
- // Relay content/reasoning/tool/output to the parent TUI via the unified spawn-child
454
- // pipeline (AGENT-LOOP.md §7.2 D3). Prefix includes a unique id: parallel child agents
455
- // with the same role stay independent and don't overwrite each other.
456
- // Format: role#id/ → onToken("coder#2/writing..."), onToolCall("coder#2/read", args)
457
- // Async id allocation (AGENT-LOOP.md §15 D-A1): reserve the relay counter at
458
- // spawn time — the returned id must be stable while the item sits in the queue.
459
- // The [model] token (TUI block creation) is DEFERRED to actual start so queued
460
- // children don't paint an empty panel block ("queued 态不显示").
461
- let relayPrefix
462
- if (wantAsync) {
463
- parent._subAgentCounter = (parent._subAgentCounter ?? 0) + 1
464
- relayPrefix = `${role}#${parent._subAgentCounter}/`
465
- } else {
466
- relayPrefix = makeRelay(parent, role ?? "sub", ctx.callbacks?.onToken, childProvider.model ?? "")
467
- }
468
- // LOGGING(LOGGING.md):子代理内部事件(子内 llm:*/tool:*)以 childId 归属——
469
- // agent._logId 随 runAgent 的 logCtx 透出(主文件单文件全记、按 childId grep)。
470
- child._logId = relayPrefix.slice(0, -1)
471
- const childOpts = {
472
- onPermissionRequest: childPermission,
473
- ...wrapChildCallbacks(relayPrefix, ctx.callbacks),
474
- }
475
- const childRunOpts = buildChildRunOpts(ctx)
268
+ // child 装配(2026-09-05 module-split——buildSpawnChild verbatim
269
+ // subagent-spawn.mjs:provider/model 覆盖、角色门、design-token 门、工具集/
270
+ // overlay/permission 装配、审计任务书注入、relay 前缀分配、childOpts/runOpts)
271
+ const built = buildSpawnChild(parent, ctx, args, role, wantAsync, prep.files, prep.dependsOn, engAuditAttempt)
272
+ const { child, input, childOpts, childRunOpts, relayPrefix } = built
476
273
  // Turn-cap continue loop (TURN-CAP-CONTINUE.md) via runWithContinue (§7.2 D3):
477
274
  // hitting the cap asks the user via the SAME y/n panel the main agent uses —
478
275
  // unlimited continues, resume:true keeps the child's history + mutation bookkeeping,
@@ -482,245 +279,126 @@ export const subagentTool = {
482
279
  // turns them into Error tool results — unchanged behavior).
483
280
  const askSubagentContinue = (e) => {
484
281
  if (!ctx.onPermissionRequest) return Promise.resolve(false)
485
- const ask = () => ctx.onPermissionRequest("continue", { turns: e.turn, agent: relayPrefix.slice(0, -1) })
486
- parent._permQueue = (parent._permQueue ?? Promise.resolve()).then(ask, ask)
487
- return parent._permQueue
282
+ const key = relayPrefix.slice(0, -1)
283
+ const ask = async () => {
284
+ // SYNC-CANCEL v2(模态 deny——用户裁):⏹ 后 entry.stopped——不再弹模态。
285
+ // ⚠ 不能直接 resolve(false) 走 onDeclined 降级(TURN_CAP partial——child 已撞
286
+ // cap——runWithContinue 的 decline 是正常 return——永远到不了 abort 检出点——
287
+ // stopped 折叠语义丢失:无 ⟦ev⟧stopped/无 STOPPED_MARK——块冻结标 done 而非
288
+ // stopped——评审 🟡#2)。stopped 分支改抛 AbortError——runWithContinue 只捕
289
+ // ContinueError——原样上抛 → 阻塞 catch 三分支②折叠("child 随即在 abort 检出点
290
+ // 解绕折叠"——AGENT-LOOP §7.2 机制文)。abort 恒已在途(stopped 只由
291
+ // cancelSyncChild 与 ctrl.abort 同时置位)——信号语义真实。
292
+ if (parent._syncChildAborts?.get(key)?.stopped) throw abortError(ctrl.signal, "settle", "sync-stopped")
293
+ const go = await ctx.onPermissionRequest("continue", { turns: e.turn, agent: key })
294
+ // ⏹ deny(denyModalForOwner resolve(false))与用户按 n 同形——旗标区分:
295
+ // stopped → 同上抛(折叠——abort 先于 deny 已在途);普通 n → false 走 decline
296
+ // (现状——cap partial 报告)。
297
+ if (parent._syncChildAborts?.get(key)?.stopped) throw abortError(ctrl.signal, "settle", "sync-stopped")
298
+ return go
299
+ }
300
+ return enqueueAsk(parent, "_permQueue", ask)
488
301
  }
489
302
 
490
- // ── Async branch (AGENT-LOOP.md §15 D-A1/D-A6): spawn without waiting ──
491
- // The child runs the EXACT blocking pipeline (runChildPipeline below — relay /
492
- // turn-cap / permission / MIN_REPORT_CHARS / mergeChildMutations all unchanged),
493
- // but the parent does not await it: the promise is parked in _asyncSubagents and
494
- // consumed via action:'check' or the turn-end auto-wait. Slot queue: running
495
- // count < ASYNC_SUBAGENT_LIMIT → start now; ≥ limit → enqueue (status "queued",
496
- // position = queue index) — never rejected, never requiring the model to batch.
303
+ // ── Async branch(2026-09-05 module-split——executeAsyncSpawn verbatim
304
+ // subagent-run.mjs:条目构建/等位/启动/controller 链/turn 镜像/补位释放)──
497
305
  if (wantAsync) {
498
- if ((ctx.depth ?? 0) > 0) {
499
- throw new Error("async spawn only available at the top level")
500
- }
501
- parent._asyncSubagents ??= new Map()
502
- parent._asyncQueue ??= []
503
- const running = [...parent._asyncSubagents.values()].filter((e) => e.status === "running").length
504
- const id = parent._subAgentCounter
505
- const entry = {
506
- id, role, relayPrefix,
507
- status: "queued", // 下面按等待态/槽位重定(避免两处判断漂移)
508
- position: undefined,
509
- report: null, error: null, done: false, cancelled: false,
510
- promise: null, _settle: null, _settleSeq: 0,
511
- // §19.5 D-M5 可决策字段(status 数据装配锚点):model 在 spawn 时记录;
512
- // startedAt 在 ACTUAL start(queued 等待不计 elapsed);turn/maxTurns 由
513
- // 下方 onToken 拦截层从子代理 ⟦ev⟧turn 事件镜像(T-M18 正确性断言)。
514
- model: childProvider?.model ?? null,
515
- startedAt: null,
516
- turn: 0, maxTurns: 0,
517
- // §19.5 D-M6 (round2 #2):条目级 AbortController——cancel 定向 abort 本
518
- // 条目(runAgent signal 链);Ctrl+C 全停语义不变(基信号 abort 逐链传播)。
519
- controller: null,
520
- // §20 D-SD2 域元数据(AGENT-LOOP.md §20):running ∪ queued 条目全带——
521
- // _files(归一化绝对路径)/ _dependsOn(字符串 id)——冲突检测与补位判据
522
- // 的事实源;无调度参数 spawn 两字段皆空(legacy——不参与冲突检测——零改动)。
523
- _files: files,
524
- _dependsOn: dependsOn,
525
- _lastQueuedSig: null, // ⟦ev⟧queued 去重 sig(refreshQueuedTokens)
526
- }
527
- // §20 D-SD3 准入落点:等待态(依赖未满足/域冲突/depc)→ queued(waiting-deps——
528
- // 不占槽不启动——即使槽空);纯槽满(kind slot)→ queued(等位);否则立即启动。
529
- // 派生实时计算(describeBlockers——池状态在 spawn 同步段内不变——与前面准入一致)。
530
- const blockers = describeBlockers(parent, entry)
531
- if (blockers.kind === "slot") {
532
- entry.status = running >= ASYNC_SUBAGENT_LIMIT ? "queued" : "running"
533
- } else {
534
- entry.status = "queued" // waiting-deps / dependency-cancelled——slot 空也不启动
535
- }
536
- // The settle signal — resolves when the run chain settles (never rejects).
537
- entry.promise = new Promise((res) => { entry._settle = res })
538
- // §19.5 D-M6:条目 controller 链到会话/回合基信号(_sessionSignal 优先——
539
- // §17 挂起会话内 children 持会话 signal,digest 自身 Ctrl+C 不误伤)。
540
- const ctrl = new AbortController()
541
- entry.controller = ctrl
542
- const baseSignal = parent._sessionSignal ?? ctx.signal ?? null
543
- if (baseSignal) {
544
- if (baseSignal.aborted) ctrl.abort()
545
- else baseSignal.addEventListener("abort", () => ctrl.abort(), { once: true })
546
- }
547
- // §19.5 D-M5:turn 镜像拦截层(callbacks 包装层——选改动最小方案:在既有
548
- // wrapChildCallbacks 之外再包一层,只解析 ⟦ev⟧turn 更新条目,其余原样转发)。
549
- const trackOpts = { ...childOpts }
550
- const parentOnToken = trackOpts.onToken
551
- if (parentOnToken) {
552
- trackOpts.onToken = (t) => {
553
- const ev = String(t).match(/^⟦ev⟧turn\x1e(\d+)\x1e(\d+)\x1e/)
554
- if (ev) {
555
- entry.turn = Number(ev[1]) || 0
556
- entry.maxTurns = Number(ev[2]) || 0
557
- }
558
- return parentOnToken(t)
559
- }
560
- }
561
- entry.start = () => {
562
- entry.status = "running"
563
- entry.position = undefined
564
- entry.startedAt = Date.now()
565
- // §19.5.6 D-SF1(round3 #1):绑定子代理对象引用——绑定时刻 = 实际启动时
566
- // (queued 条目 spawn-ack 时刻尚无子代理对象——§20 D-SD3b);绑定对象 = 子代理
567
- // 对象(不是 _touchedFiles 数组引用——per-run 记账在 prepareRun 重置——数组
568
- // 引用会陈旧——对象引用保证 status 查询时实时读——杀前一刻最新)。
569
- entry.childAgent = child
570
- // §19.5 D-M7b ①: async 标记事件——零字段 ⟦ev⟧async token(sync 不发)。
571
- // 锚点 = 实际启动(与 [model] 同步——queued 入队不 paint,补位启动才发);
572
- // 先于 [model] 发出——区块创建即知 sub.async(routeSubToken 解析——
573
- // ⏹ 门控与头标 async 的判定源)。父级直接 emit(depth-0 专属路径——
574
- // 不经子代理文本 strip 白名单——与 ⟦ev⟧stopped/settled 同族)。
575
- ctx.callbacks?.onToken?.(relayPrefix + "⟦ev⟧async\x1e")
576
- // Deferred [model] emit: the TUI block is created at ACTUAL start.
577
- ctx.callbacks?.onToken?.(relayPrefix + "[model]" + (childProvider.model ?? ""))
578
- // Turn-cap on background children NEVER pops the continue panel (D-A3):
579
- // §15 D-A3 exception (2026-09-02 unified rule, AGENT-LOOP.md §2): in an
580
- // engineering && AUTO session the child auto-resumes — the user authorized
581
- // unattended runs, no one is at the panel. Every other tier auto-declines
582
- // and the partial-work report carries the cap reason. §18 D-E2 relies on
583
- // this exception as the turn-cap fallback for the default-async eng-coder
584
- // delivery (the internal protocol does not raise the 100-turn cap).
585
- runChildPipeline(child, input, trackOpts, { ...childRunOpts, signal: entry.controller.signal }, {
586
- parent, role, args,
587
- askContinue: () => Promise.resolve(Boolean(parent.config?.agent?.engineering && parent.autoApprove)),
588
- })
589
- .then((report) => { entry.report = report })
590
- .catch((err) => { entry.error = err?.message ?? String(err) })
591
- .finally(() => {
592
- entry.status = "done" // running 数口径(D-A1/D-A2/T6):已完成未消费不计入
593
- entry.done = true
594
- // LOGGING(LOGGING.md):settle 分流事件——child:done/child:error(结果)+
595
- // ev:cancelled/ev:settled(settle 回调分流——取消/挂起移交;正常回合内 settle
596
- // 由 child:done 覆盖不另发 ev——ev:stopped 见中止清池点)
597
- const childLogId = `${entry.role}#${entry.id}`
598
- const childMs = entry.startedAt ? Date.now() - entry.startedAt : 0
599
- // 中止守卫(2026-09-03 code review #5):Ctrl+C/会话中止时子代理以 error 形态
600
- // settle——不落 child:error/done/ev:settled(ev:stopped 已在中止清池点表达;
601
- // 同文件阻塞路径同款抑制——"用户停——不落错误事件")。定向 cancel 走 ev:cancelled。
602
- const parentAborted = ctx.signal?.aborted || entry.controller?.signal?.aborted
603
- if (entry.cancelled) {
604
- logEvent("ev:cancelled", { id: childLogId })
605
- } else if (!parentAborted) {
606
- if (entry.error != null) logEvent("child:error", { role: entry.role, id: childLogId, ms: childMs, err: errText(entry.error, 200) })
607
- else logEvent("child:done", { role: entry.role, id: childLogId, ms: childMs, kind: String(entry.report ?? "").includes(TURN_CAP_MARK) ? "partial" : "ok" })
608
- if (parent._suspended) logEvent("ev:settled", { id: childLogId, kind: "suspended" })
609
- }
610
- // §19.5 cancelled settle 分支(D-M6 round1 #1 + round2 #3):cancel 定向
611
- // 中止的条目——不入 _pendingAsyncResults、不参与 collectSettledAsync 直注入
612
- // (清池规则同 Ctrl+C 全停但只清该条目——陈旧错误零注入);发 ⟦ev⟧stopped
613
- // 冻结事件(TUI 区块 interrupted 语义冻结——标题 "stopped");取消事实与半成品
614
- // 警示对模型可见(user-role 提醒——形态仿 injectAsyncResult、XML 转义——防基于
615
- // 半成品树继续:mergeChildMutations 不覆盖 abort 路径)。
616
- // §20 D-SD5:running 依赖取消的 settle 终态点——写终态墓碑(running 取消无
617
- // 出队事件——出池在 settle);queued 依赖者随之标注 dependency cancelled
618
- // (refreshQueuedTokens——settle 后统一段);提醒文本列出依赖者(供模型决策)。
619
- if (entry.cancelled) {
620
- parent._asyncSubagents?.delete(String(entry.id))
621
- const tombstones = (parent._asyncTombstones ??= new Map())
622
- tombstones.set(String(entry.id), { status: "cancelled", role: entry.role })
623
- ctx.callbacks?.onToken?.(`${entry.relayPrefix}⟦ev⟧stopped\x1e0\x1e0\x1estopped\x1e`)
624
- const dependents = dependentLabels(parent, String(entry.id))
625
- const autoNote = parent.autoApprove
626
- ? " — AUTO session: they auto-start on slot availability (round2 #3)"
627
- : " — they stay queued until you cancel them or an AUTO session starts them"
628
- pushReal(parent, {
629
- role: "user",
630
- content: `[System reminder: subagent ${escapeXml(entry.role)}#${entry.id} cancelled by user — partial changes not merged/audited${dependents.length > 0 ? `; queued dependents ${dependents.join(", ")} marked "dependency cancelled"${autoNote}` : ""}]`,
631
- })
632
- } else if (!ctx.signal?.aborted) {
633
- // 完成信号按会话态分流(§17 D-S8 冻结门控 + D-S3 记账——以读取时刻为准,确定性):
634
- // - 非挂起态(普通回合内 settle):照发 ⟦ev⟧done —— TUI 立即冻结区块,冻结位置 =
635
- // 完成时刻的流位置(§15 D-A3 2026-09-02 用户实证修正:收尾统一发会把块堆在结论之后)。
636
- // §17.5 supersede:回合尾 collectSettledAsync(suspDriven)不再直注入——条目留池
637
- // 由挂起会话首轮 sweep → digest 消化(17.5.2 方案 B——块已冻结不受影响)。
638
- // - 挂起态(_suspended:回合已结束或 auto-turn 消化中):冻结延迟——改发 ⟦ev⟧settled
639
- // (区块显示 "done · awaiting digestion" 驻留面板),条目移交 _pendingAsyncResults
640
- // 由下个回合 prepareRun 前注入(D-S3 ②;注入即从池/pending 移除,无重复);
641
- // §17.5.5:注入完成后 freezeReclaimDigestedBlocks 逐条回收冻结(不等池空)。
642
- // 父会话 abort 两种都不发:TUI 已按 interrupted 冻结,晚到 token 经 tombstone 丢弃。
643
- if (parent._suspended) {
644
- parent._pendingAsyncResults ??= []
645
- parent._pendingAsyncResults.push(entry)
646
- parent._asyncSubagents?.delete(String(entry.id))
647
- ctx.callbacks?.onToken?.(`${entry.relayPrefix}⟦ev⟧settled\x1e0\x1e0\x1esettled\x1e`)
648
- } else {
649
- ctx.callbacks?.onToken?.(`${entry.relayPrefix}⟦ev⟧done\x1e0\x1e0\x1edone\x1e`)
650
- }
651
- }
652
- entry._settleSeq = (parent._asyncSettleSeq = (parent._asyncSettleSeq ?? 0) + 1)
653
- entry._settle()
654
- for (const w of parent._asyncWaiters?.splice(0) ?? []) { try { w() } catch { /* noop */ } }
655
- // §20 D-SD4 释放点:settle 腾槽 + 依赖终态转移 → 补位(依赖满足者/域冲突
656
- // 解除者自动启动——槽 ≤4)→ 排队态面板刷新(等待块头标注随终态更新——
657
- // dependency cancelled / 位置前移)。
658
- maybeRefillAsync(parent)
659
- refreshQueuedTokens(parent, ctx.callbacks?.onToken)
660
- })
661
- }
662
- parent._asyncSubagents.set(String(id), entry)
663
- // LOGGING(LOGGING.md):child:spawn(async——注册即事件;status 记 queued/running 分流;
664
- // 实际启动由补位 start() 触发——运行中由子内 llm/tool 事件可见)
665
- logEvent("child:spawn", { role, id: `${role}#${id}`, kind: "async", status: entry.status, ms: 0 })
666
- if (entry.status === "queued") {
667
- parent._asyncQueue.push(entry)
668
- entry.position = parent._asyncQueue.length
669
- // §20 D-SD3b:排队 spawn 返回即建面板 waiting 块(⟦ev⟧queued 事件——spawn 侧
670
- // 发——TUI routeSubToken 消费建块/更新头;启动后 ⟦ev⟧async 转 running——同 key
671
- // 不重建)。refreshQueuedTokens 同时校正既有排队条目的位置/等待态头。
672
- refreshQueuedTokens(parent, ctx.callbacks?.onToken)
673
- const blk = describeBlockers(parent, entry)
674
- const out = { id: String(id), role, status: "queued", position: entry.position }
675
- if (blk.kind !== "slot") {
676
- out.waiting = blk.kind === "depc" ? "dependency-cancelled" : "waiting-deps"
677
- out.reason = blk.detail
678
- }
679
- return JSON.stringify(out)
680
- }
681
- entry.start()
682
- return JSON.stringify({ id: String(id), role, status: "running" })
306
+ return executeAsyncSpawn(parent, ctx, role, args, child, input, childOpts, childRunOpts, relayPrefix, built.childProvider, prep.files, prep.dependsOn)
683
307
  }
684
308
 
685
- // ── Blocking path (unchanged semantics): await the full pipeline ──
686
- // LOGGINGLOGGING.md):child:*(阻塞 spawn——runChildPipeline 前后;declined
687
- // partial TURN_CAP_MARK 检出;错误原样上抛(dispatch tool:error))
309
+ // ── Blocking path (unchanged semantics + SYNC-CANCEL targeted stop) ──
310
+ // SYNC-CANCEL F1/F52026-09-09):自属 AbortController(armSyncChildAbort——
311
+ // childRunOpts.signal 覆写 ctrl.signal——照抄 async 分支 subagent-run.mjs 的
312
+ // 覆写模式——buildChildRunOpts 不改——escalate/consult 零触碰)——⏹ 定向中止
313
+ // (cancelSyncChild → ctrl.abort)与整回合停(base abort 逐链传播)解耦;registry
314
+ // 注册/注销(try/finally 三路径——R7 防跨回合残留)。LOGGING(LOGGING.md):
315
+ // child:*(阻塞 spawn——runChildPipeline 前后;declined partial 由 TURN_CAP_MARK
316
+ // 检出;⏹ 折叠由 STOPPED_MARK 检出——kind partial;错误原样上抛(dispatch 转
317
+ // tool:error))
688
318
  const blockT0 = Date.now()
689
319
  logEvent("child:spawn", { role, id: child._logId, kind: "blocking" })
320
+ const syncKey = relayPrefix.slice(0, -1)
321
+ // baseSignal 一次性快照(spawn 时刻)——catch 分类复用同一信号对象(会话收尾把
322
+ // _sessionSignal 置 null 的窗口内重读会漂移——快照防误判)
323
+ const baseSignal = buildChildSignal(parent, ctx)
324
+ const { ctrl, disarm } = armSyncChildAbort(parent, syncKey, baseSignal)
325
+ let pipelineReport
690
326
  try {
691
- const pipelineReport = await runChildPipeline(child, input, childOpts, childRunOpts, {
327
+ pipelineReport = await runChildPipeline(child, input, childOpts, { ...childRunOpts, signal: ctrl.signal }, {
692
328
  parent, role, args,
693
329
  askContinue: askSubagentContinue,
694
330
  })
695
- logEvent("child:done", { role, id: child._logId, ms: Date.now() - blockT0, kind: String(pipelineReport).includes(TURN_CAP_MARK) ? "partial" : "ok" })
696
- // §7.2.3 sync spawn 完成精确冻结(方案 e):execute 返回前 ctx 留子代理 key
697
- // (relayPrefix 去尾 = `role#N`)——dispatch runOne 读它作 onToolResult 第 4 参 →
698
- // TUI finishSubTaskKey 按 key 精确冻(async eng-coder 先启动时不再误冻其块——
699
- // T-F2)。仅成功路径设置:async 分支不设(round2 #2——ack 带 status:running 由
700
- // isAsyncSpawnResult 跳过冻结);错误/拒绝路径到此之前已 throw/return——ctx 未设
701
- // ——错误路径不触发冻结(round1 #1——T-F5)。
702
- ctx._subagentKey = relayPrefix.slice(0, -1)
703
- return pipelineReport
704
331
  } catch (e) {
705
- if (ctx.signal?.aborted || e?.name === "AbortError") throw e // 用户停——不落错误事件
706
- logEvent("child:error", { role, id: child._logId, ms: Date.now() - blockT0, err: errText(e, 200) })
707
- throw e
332
+ // SYNC-CANCEL F2 三分支(classifySyncAbort 纯函数):
333
+ const cls = classifySyncAbort(ctx.signal, baseSignal, ctrl.signal, e)
334
+ if (cls === "base") {
335
+ // ① 整回合停(现状逐字保留——挂起场景 base 命中而 ctx.signal 未 abort——R2):
336
+ // §27 R23:外层 abort 传播的中断——内层开块随之外层冻结前先收尾定格
337
+ // (D-R23c1 stopped——T-R23c.2a 生成侧路径;TUI 冻结兜底仍在 freezeSubTaskLines)
338
+ emitNestedChildEvent(ctx, relayPrefix, "stopped")
339
+ throw e // 用户停——不落错误事件
340
+ }
341
+ if (cls === "error") {
342
+ // ③ 其他错误(现状逐字保留——:249-253):
343
+ // §27 R23 error-run 映射(实现批补一行):run 错误(非 abort)→ 同样发 stopped
344
+ // ——内层子块定格不悬空(T-R23a.3——工具错/运行错误路径)。
345
+ emitNestedChildEvent(ctx, relayPrefix, "stopped")
346
+ logEvent("child:error", { role, id: child._logId, ms: Date.now() - blockT0, err: errText(deathLine(e, ctrl?.signal), 200) })
347
+ throw e
348
+ }
349
+ // ② targeted 折叠(err AbortError && 自属 ctrl aborted && 非整回合停):merge +
350
+ // stopped partial 报告(父回合继续拿报告——AC1/AC3)。merge 镜像 escalate sync
351
+ // runner 先例(subagent-actions.mjs runner 包装层——guard 在 mergeChildMutations
352
+ // 内——见子代理已写文件才传播)。
353
+ if (role === "eng-coder" && child._mutatedThisRun) mergeChildMutations(parent, child)
354
+ pipelineReport = buildSyncStoppedReport(role, child._capturedOutput ?? "", args?.designId)
355
+ // 块冻结标 stopped(R6——非 done):⏹ 定向中止的 TUI 顶层块立即定格 stopped
356
+ // (async settle cancelled 分支同款直发——async-settle.mjs settleAsyncEntry);
357
+ // 嵌套(eng-coder 内 explore 审计)经 emitNestedChildEvent 定格子块——stopped
358
+ // 幂等无害(重复/迟到 done 由 §27.1 F2 done 子块定格丢弃兜底)。
359
+ ctx.callbacks?.onToken?.(`${relayPrefix}⟦ev⟧stopped\x1e0\x1e0\x1estopped\x1e`)
360
+ emitNestedChildEvent(ctx, relayPrefix, "stopped")
361
+ } finally {
362
+ // R7 防跨回合残留:成功/折叠②/整回合停①/错误③ 四出口统一注销(设计"三路径"
363
+ // 口径 = 成功/折叠/整回合停——错误③ 同样 rethrow 经 finally——同归本注销)
364
+ disarm()
708
365
  }
366
+ // §27 R23 D-R23c1(评审 #1 🅰——生成侧补发射):sync spawn 同步收尾——若本 spawn
367
+ // 处于嵌套上下文(ctx.callbacks 已是嵌套 wrapper——eng-coder 内 explore 审计)→
368
+ // 发内层 ⟦ev⟧done(完整嵌套前缀——wrapper 链自动补外层)→ 主 TUI 路由子块定格
369
+ // (T-R23c.1)。非嵌套(depth-0)零变化——冻结仍由 dispatch subKey 精确冻承接。
370
+ // ② 折叠 = 正常 return(本共用出口:done 补发照设 + ctx._subagentKey 照设——成功
371
+ // 冻结管线复用——迟到 done 对已定格 stopped 块被丢弃——幂等无害)。
372
+ emitNestedChildEvent(ctx, relayPrefix, "done")
373
+ logEvent("child:done", { role, id: child._logId, ms: Date.now() - blockT0, kind: String(pipelineReport).includes(TURN_CAP_MARK) || String(pipelineReport).includes(STOPPED_MARK) ? "partial" : "ok" })
374
+ // §7.2.3 sync spawn 完成精确冻结(方案 e):execute 返回前 ctx 留子代理 key
375
+ // (relayPrefix 去尾 = `role#N`)——dispatch runOne 读它作 onToolResult 第 4 参 →
376
+ // TUI finishSubTaskKey 按 key 精确冻(async eng-coder 先启动时不再误冻其块——
377
+ // T-F2)。仅成功/折叠路径设置:async 分支不设(round2 #2——ack 带 status:running 由
378
+ // isAsyncSpawnResult 跳过冻结);base/error 路径到此之前已 throw——ctx 未设
379
+ // ——中止/错误路径不触发冻结(round1 #1——T-F5)。
380
+ ctx._subagentKey = syncKey
381
+ return pipelineReport
709
382
  },
710
383
  }
711
384
 
712
- // Re-export shim (2026-09-03 拆分轮 + §19 合体轮): 机械与合体动作执行器迁至
713
- // ./subagent-async.mjs——保留本文件导出面,消费点(agent.mjs / agent-turn.mjs /
714
- // consult.mjs / 测试)导入路径零改动。execute 仍直接使用 ASYNC_SUBAGENT_LIMIT /
715
- // maybeRefillAsync / buildChildRunOpts / runChildPipeline(文件头部 import)。
385
+ // Re-export shim (2026-09-03 拆分轮 + §19 合体轮 + 2026-09-05 拆分轮): 机械与动作
386
+ // 执行器迁至 ./subagent-async.mjs、./subagent-actions.mjs、./subagent-scheduler.mjs
387
+ // ——本文件保留导出面,消费点(agent.mjs / agent-turn.mjs / consult.mjs / 测试)导入
388
+ // 路径零改动;池逻辑/准入见 subagent-run.mjs(executeAsyncSpawn)与 subagent-scheduler.mjs
389
+ // (maybeRefillAsync——execute 不再直接使用池常量)。
716
390
  // 2026-09-05 拆分轮: maybeRefillAsync 随 §20 调度器独立(./subagent-scheduler.mjs)——
717
391
  // 再导出源改写,消费面(agent.mjs 动态 import 等)不变。
392
+ // 2026-09-06 §11.1 拆分轮: ASYNC_SUBAGENT_LIMIT 导出 → ASYNC_POOL_LIMITS(分域常量——
393
+ // 定义在 subagent-async.mjs——re-export 面同步)。
718
394
  export {
719
- ASYNC_SUBAGENT_LIMIT,
720
- MAX_ASYNC_CHECKS,
395
+ ASYNC_POOL_LIMITS,
721
396
  resolveChildProvider,
722
397
  injectAsyncResult,
723
398
  buildChildRunOpts,
724
399
  mergeChildMutations,
725
400
  } from "./subagent-async.mjs"
726
401
  export { maybeRefillAsync } from "./subagent-scheduler.mjs"
402
+ // 2026-09-05 module-split:spawn 装配 helpers 迁 subagent-spawn.mjs——re-export 保测试
403
+ // import 面(subagent-core.test.mjs 从本文件动态 import)
404
+ export { effectiveSubagentModel, resolveDesignSlot } from "./subagent-spawn.mjs"