thincoder 0.12.59 → 0.12.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/CHANGELOG.md +38 -3
  2. package/README.md +2 -2
  3. package/bin/thincoder.mjs +80 -19
  4. package/package.json +4 -3
  5. package/src/acp/bridge.mjs +7 -4
  6. package/src/advisor/messages.mjs +24 -4
  7. package/src/advisor/run.mjs +35 -33
  8. package/src/advisor.mjs +25 -6
  9. package/src/agent/completion.mjs +17 -11
  10. package/src/agent/dispatch.mjs +102 -19
  11. package/src/agent/helpers.mjs +36 -0
  12. package/src/agent/record-results.mjs +46 -10
  13. package/src/agent/run-stages.mjs +227 -0
  14. package/src/agent/setup-reminders.mjs +62 -0
  15. package/src/agent/setup.mjs +18 -2
  16. package/src/agent/spawn-child.mjs +29 -4
  17. package/src/agent-tools/advisor-async.mjs +456 -0
  18. package/src/agent-tools/advisor.mjs +110 -108
  19. package/src/agent-tools/async-settle.mjs +191 -0
  20. package/src/agent-tools/consult.mjs +121 -102
  21. package/src/agent-tools/design-token.mjs +104 -0
  22. package/src/agent-tools/eng.mjs +24 -29
  23. package/src/agent-tools/escalate-async.mjs +286 -0
  24. package/src/agent-tools/read-history.mjs +155 -31
  25. package/src/agent-tools/recent-changes.mjs +2 -1
  26. package/src/agent-tools/settings.mjs +7 -17
  27. package/src/agent-tools/subagent-actions.mjs +168 -130
  28. package/src/agent-tools/subagent-async.mjs +129 -174
  29. package/src/agent-tools/subagent-panel.mjs +153 -0
  30. package/src/agent-tools/subagent-run.mjs +202 -0
  31. package/src/agent-tools/subagent-scheduler.mjs +45 -21
  32. package/src/agent-tools/subagent-spawn.mjs +406 -0
  33. package/src/agent-tools/subagent.mjs +107 -555
  34. package/src/agent-tools/verify.mjs +118 -270
  35. package/src/agent.mjs +57 -190
  36. package/src/cli/distill-command.mjs +10 -4
  37. package/src/cli/make-agent.mjs +3 -1
  38. package/src/cli/memory-command.mjs +2 -1
  39. package/src/cli/permission.mjs +2 -2
  40. package/src/cli/setup-wizard.mjs +17 -12
  41. package/src/config.mjs +56 -8
  42. package/src/context.mjs +5 -147
  43. package/src/crash-reports.mjs +123 -0
  44. package/src/distill.mjs +11 -11
  45. package/src/explore-distill.mjs +155 -0
  46. package/src/memory/code-sync.mjs +2 -1
  47. package/src/memory/core.mjs +6 -193
  48. package/src/memory/delete.mjs +234 -0
  49. package/src/memory/docs.mjs +58 -48
  50. package/src/memory.mjs +3 -1
  51. package/src/peer-domains.mjs +265 -0
  52. package/src/peer-instances.mjs +231 -0
  53. package/src/prompt-overlays.mjs +25 -0
  54. package/src/prompts/advisor-design.md +9 -76
  55. package/src/prompts/advisor-round1.md +9 -68
  56. package/src/prompts/advisor-round2.md +7 -54
  57. package/src/prompts/advisor-round3.md +7 -54
  58. package/src/prompts/coder.md +7 -50
  59. package/src/prompts/consult-base.md +4 -24
  60. package/src/prompts/discipline.md +26 -44
  61. package/src/prompts/eng-coder.md +7 -32
  62. package/src/prompts/engineering-sub.md +3 -23
  63. package/src/prompts/engineering.md +53 -306
  64. package/src/prompts/explore.md +3 -12
  65. package/src/prompts/main.md +10 -32
  66. package/src/prompts/methodology-template.md +28 -48
  67. package/src/prompts/plan.md +2 -9
  68. package/src/prompts/system.md +16 -35
  69. package/src/provider/core.mjs +6 -67
  70. package/src/provider/errors.mjs +76 -0
  71. package/src/provider/retry.mjs +8 -45
  72. package/src/session-gc.mjs +214 -0
  73. package/src/session-guard.mjs +47 -0
  74. package/src/session-rename.mjs +38 -0
  75. package/src/session-slots.mjs +181 -58
  76. package/src/session.mjs +48 -89
  77. package/src/token-ttl.mjs +273 -0
  78. package/src/tools/checklist-sync.mjs +181 -0
  79. package/src/tools/checklist.mjs +52 -39
  80. package/src/tools/edit-batch.mjs +109 -10
  81. package/src/tools/edit-diff.mjs +110 -27
  82. package/src/tools/edit.md +17 -12
  83. package/src/tools/execute.mjs +31 -4
  84. package/src/tools/file.mjs +11 -6
  85. package/src/tools/git.mjs +14 -6
  86. package/src/tools/glob-dialect.mjs +130 -0
  87. package/src/tools/glob.md +3 -3
  88. package/src/tools/grep.md +1 -1
  89. package/src/tools/index.mjs +5 -6
  90. package/src/tools/ops.mjs +175 -3
  91. package/src/tools/patch.mjs +3 -3
  92. package/src/tools/question.md +3 -0
  93. package/src/tools/read.md +0 -1
  94. package/src/tools/shared.mjs +14 -13
  95. package/src/tools/system.mjs +44 -9
  96. package/src/tools/wait_for.md +22 -0
  97. package/src/tui/agent-turn.mjs +17 -228
  98. package/src/tui/cmd-config.mjs +48 -7
  99. package/src/tui/cmd-eng.mjs +20 -16
  100. package/src/tui/cmd-mcp.mjs +8 -2
  101. package/src/tui/cmd-new.mjs +3 -2
  102. package/src/tui/cmd-session.mjs +19 -4
  103. package/src/tui/cmd-think.mjs +10 -10
  104. package/src/tui/cmd-upgrade.mjs +19 -4
  105. package/src/tui/config-helpers.mjs +28 -16
  106. package/src/tui/distill-cmd.mjs +1 -1
  107. package/src/tui/index.mjs +3 -2
  108. package/src/tui/interaction.mjs +3 -3
  109. package/src/tui/mouse.mjs +7 -1
  110. package/src/tui/pickers.mjs +40 -22
  111. package/src/tui/render-segments.mjs +27 -10
  112. package/src/tui/startup.mjs +4 -0
  113. package/src/tui/subagent-blocks.mjs +95 -263
  114. package/src/tui/subagent-children.mjs +176 -0
  115. package/src/tui/subagent-freeze.mjs +172 -0
  116. package/src/tui/subagent-panel.mjs +61 -23
  117. package/src/tui/suspension-drive.mjs +351 -0
  118. package/src/tui/tool-args.mjs +3 -3
  119. package/src/tui/tool-display.mjs +142 -0
  120. package/src/tui/tool-events.mjs +37 -173
  121. package/src/tui/tui-lifecycle.mjs +29 -0
  122. package/src/tui/update-notice.mjs +4 -0
  123. package/src/tui/wizard.mjs +12 -6
  124. package/src/tools/pdf-parse-text.mjs +0 -497
  125. package/src/tools/pdf-parse-xref.mjs +0 -499
  126. package/src/tools/pdf.mjs +0 -155
  127. package/src/tools/read_pdf.md +0 -21
@@ -1,92 +1,43 @@
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, emitNestedChildEvent } from "../agent/spawn-child.mjs"
9
23
  import { logEvent, errText } from "../log.mjs"
10
24
  import {
11
- runChildPipeline, resolveChildProvider, ASYNC_SUBAGENT_LIMIT,
12
- buildChildRunOpts, executeCheckAction, executeCancelAction,
25
+ runChildPipeline, executeCancelAction,
13
26
  } 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"
21
-
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。
27
+ import { executeStatusAction, executeEscalateAction, executePanelAction, executeObserveAction, executeSendAction } from "./subagent-actions.mjs"
28
+ import { prepareScheduling, buildSpawnChild, executeConsumeDesignAction } from "./subagent-spawn.mjs"
29
+ import { executeAsyncSpawn } from "./subagent-run.mjs"
32
30
 
33
31
  /**
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 themselvesthey 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).
45
- */
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
64
- }
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)`)
80
- }
81
- return out.join("\n\n")
82
- }
83
-
84
- /**
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).
32
+ * subagent tool ONE tool, EIGHT actions (AGENT-LOOP.md §19/§19.5/§19.6/§19.8 +
33
+ * SUBAGENT-OBSERVE-SEND): spawn (default) / status (non-blocking pool query) / observe
34
+ * (inspect a running/queued/done async child's recent activity + current tool — §7.2) /
35
+ * send (inject a direction into a RUNNING async childconsumed at its next turn
36
+ * boundary as an ordinary instruction — §7.2) / escalate (飞刀 hand implementation to
37
+ * a stronger model) / cancel (stop ONE background subagent §19.5) / panel (view + fix
38
+ * the live subagent panel §19.6) / consume-design (parent-side chain-terminal token
39
+ * consumption ENGINEERING-MODE.md §2.6, 2026-09-07). The check
40
+ * action was deleted (§19.8): async results reach the model only via the auto channel.
90
41
  * - action:"spawn" roles: "explore" — read-only tools, search/read/analyze
91
42
  * (suitable for codebase exploration); "coder" — full tool set, self-contained
92
43
  * implementation tasks; "plan" — read-only planning; "eng-coder" —
@@ -97,65 +48,17 @@ function summarizeEngTaskBook(taskInput) {
97
48
  * - non-recursive: child agents do not get the subagent tool (depth > 0 is not injected)
98
49
  */
99
50
 
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
51
  export const subagentTool = {
151
52
  name: "subagent",
152
53
  description:
153
- "ONE tool, SIX actions (AGENT-LOOP.md §19/§19.5/§19.6) — pick by what you need:\n" +
54
+ "ONE tool, EIGHT actions (AGENT-LOOP.md §19/§19.5/§19.6/§19.8 + SUBAGENT-OBSERVE-SEND) — pick by what you need:\n" +
154
55
  "- 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 turnthe 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" +
56
+ "- 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).\n" +
57
+ "- 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" +
58
+ "- 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" +
59
+ "- 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; pass async:false to run it synchronously.\n" +
158
60
  "- 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" +
61
+ "- 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
62
  "- 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
63
  "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
64
  "Available roles (which roles are exposed depends on the active mode — see Mode filtering below):\n" +
@@ -164,8 +67,8 @@ export const subagentTool = {
164
67
  "- 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
68
  "- 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
69
  "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" +
70
+ "Async spawn (AGENT-LOOP.md §15/§18/§24): 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. The DEFAULT is depth-gated (AGENT-LOOP.md §18 D-E1a): at the top level (depth 0) EVERY role spawns async by default — eng-coder's delivery protocol runs fully inside the child (implementation → audit → self-fix → advisor re-review → converged delivery); depth>0 spawns are always synchronous. Pass async:false only when you must handle the report synchronously. Use a blocking spawn when you must see the report before continuing. Async spawns are pooled per role domain (AGENT-LOOP.md §24): 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. If your next step genuinely needs the report, use a synchronous spawn instead — pass `async:false` (at depth 0 every role defaults to async — async:false is the only way to block; depth>0 is always sync).\n\n" +
71
+ "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
72
  "Writing the prompt:\n" +
170
73
  "- 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
74
  "- 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 +77,21 @@ export const subagentTool = {
174
77
  parameters: {
175
78
  type: "object",
176
79
  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." },
80
+ 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
81
  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
82
  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." },
83
+ 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
84
  context: { type: "string", description: "Optional background the sub-agent needs (it cannot see this conversation); action:'spawn' only." },
182
85
  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)." },
183
86
  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
87
  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 waiting — returns {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)." },
88
+ 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)." },
89
+ 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). Pass async:false to force the blocking spawn when you must process the report before continuing. action:'escalate': same semantics (AGENT-LOOP.md §25 D-R17b) default async at depth 0; async:false keeps the legacy synchronous flight." },
187
90
  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)." },
91
+ 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." },
92
+ 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)." },
93
+ 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)." },
94
+ 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
95
  },
192
96
  required: [],
193
97
  },
@@ -203,10 +107,10 @@ export const subagentTool = {
203
107
  if (action !== "spawn") {
204
108
  // §19 restricted-variant action gate (round2 #3): the eng-coder audit
205
109
  // channel (depth>0, role eng-coder) is spawn-only — escalate spawns a
206
- // coder+WRITE child (violates explore-only intent) and check/status/panel
207
- // have no async pool / panel mirror to query in a child context.
110
+ // coder+WRITE child (violates explore-only intent) and status/panel/
111
+ // observe/send have no async pool / panel mirror to query in a child context.
208
112
  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)`)
113
+ throw new Error(`only action:'spawn' (sync explore audits) is available inside an eng-coder — escalate/status/cancel/panel/consume-design/observe/send are not (AGENT-LOOP.md §19 D-M3)`)
210
114
  }
211
115
  // §17 N3/D-S6 spawn gate (manual tier): auto-turn digests may not spawn —
212
116
  // async OR blocking — the digest must stay organize-only. The escalate
@@ -215,16 +119,23 @@ export const subagentTool = {
215
119
  if (action === "escalate" && ctx.agent?._inAutoTurn && !ctx.agent?.autoApprove) {
216
120
  return JSON.stringify({ status: "error", error: "cannot spawn subagents from a manual auto-turn — wait for user input" })
217
121
  }
218
- if (action === "check") return await executeCheckAction(args, ctx)
219
122
  if (action === "status") return executeStatusAction(args, ctx)
220
123
  if (action === "escalate") return await executeEscalateAction(args, ctx)
221
124
  // §19.5 控制类动作:digest 内放行(D-S7 分类——控制/自省;dispatch 控制类
222
125
  // 豁免同批生效——19.5.2b round2 #4;escalate 的 digest 拒绝在上一分支)
223
126
  if (action === "cancel") return executeCancelAction(args, ctx)
127
+ // 2026-09-07 token 链终消费制(ENGINEERING-MODE.md §2.6 F1):父侧核销消费——
128
+ // 非只读控制动作——depth-0 + 工程模式限定(本分流已过受限变体门;工程模式门在
129
+ // 执行器内)——planMode 拒绝(dispatch 不豁免)——不入批审批分组(dispatch 免审)。
130
+ if (action === "consume-design") return executeConsumeDesignAction(args, ctx)
224
131
  // §19.6 panel 动作:view(readonly 面——digest 内放行——自省类)与 freeze
225
132
  // (控制类——同 cancel——digest 内放行)。深度/门控检查在 executePanelAction 内。
226
133
  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.`)
134
+ // SUBAGENT-OBSERVE-SEND:observe = readonly 查询(同 status——digest/planMode 放行);
135
+ // send = 控制类豁免(同 cancel——父回合内显式调用即授权)。深度门在各自执行器内。
136
+ if (action === "observe") return executeObserveAction(args, ctx)
137
+ if (action === "send") return executeSendAction(args, ctx)
138
+ throw new Error(`Unknown subagent action: ${JSON.stringify(action)}. Valid actions: spawn, status, escalate, cancel, panel, consume-design, observe, send.`)
228
139
  }
229
140
 
230
141
  const parent = ctx.agent
@@ -235,9 +146,12 @@ export const subagentTool = {
235
146
  if (typeof args.task !== "string" || !args.task.trim()) {
236
147
  throw new Error("subagent action:'spawn' requires a task (the self-contained task brief).")
237
148
  }
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"
149
+ // §18 D-E1a depth-gated async default (2026-09-06 需求池 R12): depth-0 spawns
150
+ // default to async for EVERY role (the old role-level default — eng-coder only —
151
+ // is superseded); depth>0 spawns default to sync (子代理内部强制同步现状保留).
152
+ // async:false remains the explicit escape hatch; async:true at depth>0 is
153
+ // refused downstream (executeAsyncSpawn top-level gate).
154
+ const wantAsync = args.async ?? ((ctx.depth ?? 0) === 0)
241
155
 
242
156
  // Role normalization + whitelist (2026-08-25, coder-leak fix): exact-string gates let
243
157
  // variant roles ("Coder", " coder") bypass BOTH mode gates and fall through to
@@ -271,208 +185,17 @@ export const subagentTool = {
271
185
  return JSON.stringify({ status: "error", error: "cannot spawn subagents from a manual auto-turn — wait for user input" })
272
186
  }
273
187
 
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
- }
188
+ // §20 准入(2026-09-05 module-split——prepareScheduling verbatim
189
+ // subagent-spawn.mjs:参数形态/unknown id/依赖环/阻塞 sync 判定;files 目录声明
190
+ // fail-closed——检测器错误即工具结果 JSON)
191
+ const prep = prepareScheduling(parent, args.files, args.dependsOn, wantAsync)
192
+ if (prep.errorJson) return prep.errorJson
342
193
 
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
- })
381
-
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)
194
+ // child 装配(2026-09-05 module-split——buildSpawnChild verbatim
195
+ // subagent-spawn.mjs:provider/model 覆盖、角色门、design-token 门、工具集/
196
+ // overlay/permission 装配、审计任务书注入、relay 前缀分配、childOpts/runOpts)
197
+ const built = buildSpawnChild(parent, ctx, args, role, wantAsync, prep.files, prep.dependsOn, engAuditAttempt)
198
+ const { child, input, childOpts, childRunOpts, relayPrefix } = built
476
199
  // Turn-cap continue loop (TURN-CAP-CONTINUE.md) via runWithContinue (§7.2 D3):
477
200
  // hitting the cap asks the user via the SAME y/n panel the main agent uses —
478
201
  // unlimited continues, resume:true keeps the child's history + mutation bookkeeping,
@@ -487,199 +210,10 @@ export const subagentTool = {
487
210
  return parent._permQueue
488
211
  }
489
212
 
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.
213
+ // ── Async branch(2026-09-05 module-split——executeAsyncSpawn verbatim
214
+ // subagent-run.mjs:条目构建/等位/启动/controller 链/turn 镜像/补位释放)──
497
215
  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" })
216
+ return executeAsyncSpawn(parent, ctx, role, args, child, input, childOpts, childRunOpts, relayPrefix, built.childProvider, prep.files, prep.dependsOn)
683
217
  }
684
218
 
685
219
  // ── Blocking path (unchanged semantics): await the full pipeline ──
@@ -692,6 +226,11 @@ export const subagentTool = {
692
226
  parent, role, args,
693
227
  askContinue: askSubagentContinue,
694
228
  })
229
+ // §27 R23 D-R23c1(评审 #1 🅰——生成侧补发射):sync spawn 同步收尾——若本 spawn
230
+ // 处于嵌套上下文(ctx.callbacks 已是嵌套 wrapper——eng-coder 内 explore 审计)→
231
+ // 发内层 ⟦ev⟧done(完整嵌套前缀——wrapper 链自动补外层)→ 主 TUI 路由子块定格
232
+ // (T-R23c.1)。非嵌套(depth-0)零变化——冻结仍由 dispatch subKey 精确冻承接。
233
+ emitNestedChildEvent(ctx, relayPrefix, "done")
695
234
  logEvent("child:done", { role, id: child._logId, ms: Date.now() - blockT0, kind: String(pipelineReport).includes(TURN_CAP_MARK) ? "partial" : "ok" })
696
235
  // §7.2.3 sync spawn 完成精确冻结(方案 e):execute 返回前 ctx 留子代理 key
697
236
  // (relayPrefix 去尾 = `role#N`)——dispatch runOne 读它作 onToolResult 第 4 参 →
@@ -702,25 +241,38 @@ export const subagentTool = {
702
241
  ctx._subagentKey = relayPrefix.slice(0, -1)
703
242
  return pipelineReport
704
243
  } catch (e) {
705
- if (ctx.signal?.aborted || e?.name === "AbortError") throw e // 用户停——不落错误事件
244
+ if (ctx.signal?.aborted || e?.name === "AbortError") {
245
+ // §27 R23:外层 abort 传播的中断——内层开块随之外层冻结前先收尾定格
246
+ // (D-R23c1 stopped——T-R23c.2a 生成侧路径;TUI 冻结兜底仍在 freezeSubTaskLines)
247
+ emitNestedChildEvent(ctx, relayPrefix, "stopped")
248
+ throw e // 用户停——不落错误事件
249
+ }
250
+ // §27 R23 error-run 映射(实现批补一行):run 错误(非 abort)→ 同样发 stopped
251
+ // ——内层子块定格不悬空(T-R23a.3——工具错/运行错误路径)。
252
+ emitNestedChildEvent(ctx, relayPrefix, "stopped")
706
253
  logEvent("child:error", { role, id: child._logId, ms: Date.now() - blockT0, err: errText(e, 200) })
707
254
  throw e
708
255
  }
709
256
  },
710
257
  }
711
258
 
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)。
259
+ // Re-export shim (2026-09-03 拆分轮 + §19 合体轮 + 2026-09-05 拆分轮): 机械与动作
260
+ // 执行器迁至 ./subagent-async.mjs、./subagent-actions.mjs、./subagent-scheduler.mjs
261
+ // ——本文件保留导出面,消费点(agent.mjs / agent-turn.mjs / consult.mjs / 测试)导入
262
+ // 路径零改动;池逻辑/准入见 subagent-run.mjs(executeAsyncSpawn)与 subagent-scheduler.mjs
263
+ // (maybeRefillAsync——execute 不再直接使用池常量)。
716
264
  // 2026-09-05 拆分轮: maybeRefillAsync 随 §20 调度器独立(./subagent-scheduler.mjs)——
717
265
  // 再导出源改写,消费面(agent.mjs 动态 import 等)不变。
266
+ // 2026-09-06 §24 拆分轮: ASYNC_SUBAGENT_LIMIT 导出 → ASYNC_POOL_LIMITS(分域常量——
267
+ // 定义在 subagent-async.mjs——re-export 面同步)。
718
268
  export {
719
- ASYNC_SUBAGENT_LIMIT,
720
- MAX_ASYNC_CHECKS,
269
+ ASYNC_POOL_LIMITS,
721
270
  resolveChildProvider,
722
271
  injectAsyncResult,
723
272
  buildChildRunOpts,
724
273
  mergeChildMutations,
725
274
  } from "./subagent-async.mjs"
726
275
  export { maybeRefillAsync } from "./subagent-scheduler.mjs"
276
+ // 2026-09-05 module-split:spawn 装配 helpers 迁 subagent-spawn.mjs——re-export 保测试
277
+ // import 面(subagent-core.test.mjs 从本文件动态 import)
278
+ export { effectiveSubagentModel, resolveDesignSlot } from "./subagent-spawn.mjs"