@thincoder/core 0.9.1

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 (203) hide show
  1. package/abort-provenance.mjs +116 -0
  2. package/advisor/citations.mjs +139 -0
  3. package/advisor/compaction.mjs +174 -0
  4. package/advisor/convergence.mjs +80 -0
  5. package/advisor/history.mjs +77 -0
  6. package/advisor/loop.mjs +288 -0
  7. package/advisor/messages.mjs +299 -0
  8. package/advisor/notice.mjs +141 -0
  9. package/advisor/project-context.mjs +197 -0
  10. package/advisor/repos.mjs +150 -0
  11. package/advisor/run.mjs +190 -0
  12. package/advisor/truncate.mjs +57 -0
  13. package/advisor.mjs +281 -0
  14. package/agent/child-marks.mjs +24 -0
  15. package/agent/completion.mjs +145 -0
  16. package/agent/dispatch.mjs +493 -0
  17. package/agent/family-tools.mjs +174 -0
  18. package/agent/helpers.mjs +412 -0
  19. package/agent/post-turn.mjs +70 -0
  20. package/agent/record-results.mjs +174 -0
  21. package/agent/relay-prefix.mjs +39 -0
  22. package/agent/run-stages.mjs +244 -0
  23. package/agent/setup-reminders.mjs +199 -0
  24. package/agent/setup.mjs +234 -0
  25. package/agent/spawn-child.mjs +258 -0
  26. package/agent/suspension.mjs +240 -0
  27. package/agent/write-gate.mjs +87 -0
  28. package/agent-tools/advisor-async.mjs +481 -0
  29. package/agent-tools/advisor-settle.mjs +240 -0
  30. package/agent-tools/advisor.mjs +280 -0
  31. package/agent-tools/async-discard.mjs +143 -0
  32. package/agent-tools/async-settle.mjs +299 -0
  33. package/agent-tools/batch-segment.mjs +265 -0
  34. package/agent-tools/child-permission.mjs +45 -0
  35. package/agent-tools/consult.mjs +471 -0
  36. package/agent-tools/design-token.mjs +117 -0
  37. package/agent-tools/digest-budget.mjs +76 -0
  38. package/agent-tools/eng.mjs +102 -0
  39. package/agent-tools/escalate-async.mjs +302 -0
  40. package/agent-tools/goal.mjs +119 -0
  41. package/agent-tools/panel-blocks.mjs +24 -0
  42. package/agent-tools/parent-channel.mjs +231 -0
  43. package/agent-tools/plan.mjs +86 -0
  44. package/agent-tools/read-history.mjs +309 -0
  45. package/agent-tools/recent-changes.mjs +24 -0
  46. package/agent-tools/review-facts.mjs +31 -0
  47. package/agent-tools/settings.mjs +268 -0
  48. package/agent-tools/skill.mjs +63 -0
  49. package/agent-tools/spawn-gates.mjs +109 -0
  50. package/agent-tools/subagent-actions.mjs +496 -0
  51. package/agent-tools/subagent-async.mjs +456 -0
  52. package/agent-tools/subagent-panel.mjs +160 -0
  53. package/agent-tools/subagent-run.mjs +208 -0
  54. package/agent-tools/subagent-scheduler.mjs +446 -0
  55. package/agent-tools/subagent-spawn.mjs +478 -0
  56. package/agent-tools/subagent.mjs +419 -0
  57. package/agent-tools/task.mjs +87 -0
  58. package/agent-tools/timer.mjs +46 -0
  59. package/agent-tools/verify.mjs +295 -0
  60. package/agent-tools.mjs +23 -0
  61. package/agent.mjs +430 -0
  62. package/auto-think.mjs +115 -0
  63. package/compress-form.mjs +24 -0
  64. package/config-io.mjs +277 -0
  65. package/config-migrate.mjs +178 -0
  66. package/config-presets.mjs +49 -0
  67. package/config.mjs +419 -0
  68. package/context.mjs +495 -0
  69. package/conventions.mjs +223 -0
  70. package/embedding.mjs +120 -0
  71. package/escape.mjs +152 -0
  72. package/expand-home.mjs +16 -0
  73. package/explore-distill.mjs +152 -0
  74. package/generate-title.mjs +123 -0
  75. package/git/checkpoint.mjs +448 -0
  76. package/git/gitmem.mjs +100 -0
  77. package/history-window.mjs +179 -0
  78. package/hooks.mjs +108 -0
  79. package/i18n.mjs +106 -0
  80. package/index-bin.mjs +48 -0
  81. package/index-discover.mjs +176 -0
  82. package/ledger-cmd.mjs +209 -0
  83. package/ledger-db.mjs +86 -0
  84. package/ledger-surface.mjs +76 -0
  85. package/ledger.mjs +202 -0
  86. package/log.mjs +195 -0
  87. package/manifest.mjs +338 -0
  88. package/markdown.mjs +106 -0
  89. package/mcp/helpers.mjs +51 -0
  90. package/mcp/transport-http.mjs +248 -0
  91. package/mcp/transport-stdio.mjs +140 -0
  92. package/mcp/transport-ws.mjs +122 -0
  93. package/mcp.mjs +295 -0
  94. package/memory/code-index.mjs +219 -0
  95. package/memory/code-sync.mjs +427 -0
  96. package/memory/core.mjs +318 -0
  97. package/memory/delete.mjs +242 -0
  98. package/memory/docs.mjs +431 -0
  99. package/memory/file-walk.mjs +109 -0
  100. package/memory/origin.mjs +24 -0
  101. package/memory/scan.mjs +177 -0
  102. package/memory/schema.mjs +460 -0
  103. package/memory.mjs +21 -0
  104. package/model-ref.mjs +66 -0
  105. package/model-specs.mjs +277 -0
  106. package/package.json +32 -0
  107. package/peer-domains.mjs +265 -0
  108. package/peer-instances.mjs +178 -0
  109. package/permission.mjs +79 -0
  110. package/process-probe.mjs +315 -0
  111. package/prompt-files.mjs +113 -0
  112. package/prompt-overlays.mjs +78 -0
  113. package/prompts/advisor-design.md +43 -0
  114. package/prompts/advisor-round1.md +41 -0
  115. package/prompts/advisor-round2.md +46 -0
  116. package/prompts/advisor-round3.md +42 -0
  117. package/prompts/common.md +158 -0
  118. package/prompts/consult-base.md +19 -0
  119. package/prompts/discipline-engineering.md +123 -0
  120. package/prompts/discipline-normal.md +206 -0
  121. package/prompts/persona-coder.md +21 -0
  122. package/prompts/persona-eng-coder.md +41 -0
  123. package/prompts/persona-eng-designer.md +80 -0
  124. package/prompts/persona-engineering.md +160 -0
  125. package/prompts/persona-explore.md +15 -0
  126. package/prompts/persona-normal.md +35 -0
  127. package/prompts/persona-plan.md +27 -0
  128. package/provider/anthropic.mjs +225 -0
  129. package/provider/core.mjs +491 -0
  130. package/provider/errors.mjs +101 -0
  131. package/provider/google.mjs +257 -0
  132. package/provider/index.mjs +7 -0
  133. package/provider/list-models.mjs +163 -0
  134. package/provider/normalize.mjs +81 -0
  135. package/provider/rate.mjs +168 -0
  136. package/provider/responses.mjs +495 -0
  137. package/provider/retry.mjs +88 -0
  138. package/provider/sse.mjs +264 -0
  139. package/provider/wait-status.mjs +59 -0
  140. package/proxy.mjs +274 -0
  141. package/rules.mjs +53 -0
  142. package/session-gc.mjs +248 -0
  143. package/session-guard.mjs +59 -0
  144. package/session-lifecycle.mjs +305 -0
  145. package/session-migrate.mjs +48 -0
  146. package/session-rename.mjs +38 -0
  147. package/session-segments.mjs +99 -0
  148. package/session-slot-write.mjs +168 -0
  149. package/session-slots-manifest.mjs +264 -0
  150. package/session-slots.mjs +298 -0
  151. package/session-store.mjs +441 -0
  152. package/session.mjs +244 -0
  153. package/skills.mjs +234 -0
  154. package/text-budget.mjs +79 -0
  155. package/token-ttl.mjs +285 -0
  156. package/tool-docs/apply_patch.md +15 -0
  157. package/tool-docs/bash.md +38 -0
  158. package/tool-docs/delete.md +13 -0
  159. package/tool-docs/edit.md +30 -0
  160. package/tool-docs/execute.md +21 -0
  161. package/tool-docs/fetch.md +12 -0
  162. package/tool-docs/file_ops.md +17 -0
  163. package/tool-docs/get_current_time.md +8 -0
  164. package/tool-docs/git.md +54 -0
  165. package/tool-docs/glob.md +11 -0
  166. package/tool-docs/grep.md +19 -0
  167. package/tool-docs/hashline_edit.md +14 -0
  168. package/tool-docs/insert_after.md +15 -0
  169. package/tool-docs/lint.md +10 -0
  170. package/tool-docs/ls.md +12 -0
  171. package/tool-docs/lsp.md +10 -0
  172. package/tool-docs/process.md +10 -0
  173. package/tool-docs/question.md +16 -0
  174. package/tool-docs/read.md +20 -0
  175. package/tool-docs/read_image.md +8 -0
  176. package/tool-docs/tree.md +14 -0
  177. package/tool-docs/wait_for.md +22 -0
  178. package/tool-docs/websearch.md +16 -0
  179. package/tool-docs/write.md +11 -0
  180. package/tools/bash.mjs +276 -0
  181. package/tools/edit-batch.mjs +204 -0
  182. package/tools/edit-diff.mjs +388 -0
  183. package/tools/exec-run.mjs +43 -0
  184. package/tools/execute.mjs +243 -0
  185. package/tools/file.mjs +464 -0
  186. package/tools/git-checkpoint.mjs +143 -0
  187. package/tools/git-ext.mjs +173 -0
  188. package/tools/git.mjs +415 -0
  189. package/tools/glob-dialect.mjs +130 -0
  190. package/tools/index.mjs +76 -0
  191. package/tools/linter.mjs +120 -0
  192. package/tools/lsp.mjs +335 -0
  193. package/tools/ops.mjs +293 -0
  194. package/tools/patch.mjs +290 -0
  195. package/tools/question.mjs +26 -0
  196. package/tools/repomap.mjs +314 -0
  197. package/tools/search.mjs +248 -0
  198. package/tools/shared.mjs +467 -0
  199. package/tools/tree.mjs +81 -0
  200. package/tools/web.mjs +224 -0
  201. package/tools/write-path.mjs +191 -0
  202. package/traces/trace-store.mjs +303 -0
  203. package/undo-stack.mjs +47 -0
@@ -0,0 +1,419 @@
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, emitRelayModel } from "../agent/spawn-child.mjs"
23
+ import { logEvent, errText } from "../log.mjs"
24
+ import { abortError, deathLine } from "../abort-provenance.mjs"
25
+ import {
26
+ runChildPipeline, executeCancelAction, enqueueAsk, mergeChildMutations,
27
+ } from "./subagent-async.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"
35
+ import { ROUND_VALUES } from "./spawn-gates.mjs"
36
+
37
+ // ─── SYNC-CANCEL 纯函数(可测——无 io)──────────────────────────────────────────
38
+
39
+ /**
40
+ * SYNC-CANCEL F2 catch 三分支分类(可测纯函数——R3 收紧):
41
+ * ① "base" 整回合停:ctx.signal 或 baseSignal(= parent._sessionSignal ?? ctx.signal——
42
+ * buildChildSignal——挂起会话场景 base 命中而 ctx.signal 未 abort——R2)aborted →
43
+ * 现状保留(emitNestedChildEvent stopped + rethrow);
44
+ * ② "targeted" 定向中止:err 是 AbortError 且自属 ctrl aborted(且非整回合停)→
45
+ * 折叠 stopped partial 报告(父回合继续——merge/STOPPED_MARK/警示);
46
+ * ③ "error" 其他错误 → 现状保留(child:error + rethrow)。
47
+ * ⚠ 查 baseSignal 非仅 ctx.signal——挂起 digest 场景 child 链 _sessionSignal(R2——
48
+ * digest 自身 Ctrl+I/Ctrl+C 不误伤;会话 Stop 逐链中止必须归 ①)。
49
+ */
50
+ export function classifySyncAbort(ctxSignal, baseSignal, ctrlSignal, err) {
51
+ if (ctxSignal?.aborted || baseSignal?.aborted) return "base"
52
+ if (err?.name === "AbortError" && ctrlSignal?.aborted) return "targeted"
53
+ return "error"
54
+ }
55
+
56
+ /**
57
+ * SYNC-CANCEL F1/F5 中止控制器装配(可测):sync 阻塞 spawn 建**自属** AbortController
58
+ * (childRunOpts.signal 覆写为 ctrl.signal——照抄 async 分支 subagent-run.mjs 覆写模式)
59
+ * ——ctrl 链到基信号:baseSignal aborted → ctrl.abort();否则 addEventListener("abort",
60
+ * → ctrl.abort(), { once:true })——Ctrl+C/I 整回合停语义不变(base abort 逐链传播——
61
+ * AC2);嵌套 sync spawn 递归可中止(内层链外层 ctrl.signal——逐层自属——AC4)。
62
+ * 注册 `parent._syncChildAborts`(key = relayPrefix 去尾——{ ctrl, stopped:false }——
63
+ * TUI ⏹ 门控 live 判据 + cancelSyncChild 定向中止目标——与 async 条目 controller 存池
64
+ * 分层一致)。返回 { ctrl, disarm }——disarm 注销 registry(调用方 try/finally 三路径
65
+ * 共用——R7 防跨回合残留)。
66
+ * **序即契约(#133)**:第 4 参 `announce`(可选)在 registry 写入**之后**当场调用——sync
67
+ * 出生声明(`[model]`)与登记同函数、序不被调用方拆散;先宣告后登记会使载荷产者(VSC
68
+ * `panel-subagent-relay.mjs` `syncLiveOf` 采样该 registry)必空 ⇒ sync 块 ⏹ 运行期不可达。
69
+ * 同序先例 = async 支(池登记 `subagent-run.mjs` 先于 `[model]` 发射)。
70
+ */
71
+ export function armSyncChildAbort(parent, key, baseSignal, announce = null) {
72
+ const ctrl = new AbortController()
73
+ if (baseSignal) {
74
+ // §20.3 站点 #10(第 24 批):hop 逐跳保 reason(下游可判定「谁杀的」)
75
+ if (baseSignal.aborted) ctrl.abort(baseSignal.reason)
76
+ else baseSignal.addEventListener("abort", () => ctrl.abort(baseSignal.reason), { once: true })
77
+ }
78
+ const registry = (parent._syncChildAborts ??= new Map())
79
+ // #133:序即契约——登记完成之后才宣告出生(见 doc;不得上移)。宣告抛错(announce 链
80
+ // 经显示面 onToken)时自清该键再上抛:登记/注销配对不因异常破坏(改序前的发射在装配面、
81
+ // 无 registry 可残留;改序后本函数自持该不变式——否则条目永久残留)。
82
+ registry.set(key, { ctrl, stopped: false })
83
+ try { announce?.() } catch (e) { registry.delete(key); throw e }
84
+ const disarm = () => { registry.delete(key) }
85
+ return { ctrl, disarm }
86
+ }
87
+
88
+ /**
89
+ * SYNC-CANCEL ② 折叠报告构建(可测纯函数——仿 runChildPipeline onDeclined partial 形态,
90
+ * subagent-async.mjs onDeclined:STOPPED_MARK + partial 警示 + 捕获输出 + eng-coder
91
+ * designId 后缀——AC3)。capturedOutput = child._capturedOutput(spawn-child.mjs
92
+ * runWithContinue capture 累积——子代理已流式输出的剥哨兵文本)。
93
+ */
94
+ export function buildSyncStoppedReport(role, capturedOutput, designId) {
95
+ let report = `Subagent (${role}) ${STOPPED_MARK} — work may be partial; review recent_changes before deciding next steps.\nPartial output: ${capturedOutput || ""}`
96
+ if (role === "eng-coder") {
97
+ report += `\ndesignId: ${designId ?? "(single-design session — designId optional)"} — reuse it (with the same designToken) when re-spawning this eng-coder.`
98
+ }
99
+ return report
100
+ }
101
+
102
+ /**
103
+ * subagent tool — ONE tool, EIGHT actions (AGENT-LOOP.md §19/§19.5/§19.6/§19.8 +
104
+ * SUBAGENT-OBSERVE-SEND): spawn (default) / status (non-blocking pool query) / observe
105
+ * (inspect a running/queued/done async child's recent activity + current tool — §7.2) /
106
+ * send (inject a direction into a RUNNING async child — consumed at its next turn
107
+ * boundary as an ordinary instruction — §7.2) / escalate (飞刀 — hand implementation to
108
+ * a stronger model) / cancel (stop ONE background subagent — §19.5) / panel (view + fix
109
+ * the live subagent panel — §19.6) / consume-design (parent-side chain-terminal token
110
+ * consumption — ENGINEERING-MODE.md §2.6, 2026-09-07). The check
111
+ * action was deleted (§19.8): async results reach the model only via the auto channel.
112
+ * - action:"spawn" roles: "explore" — read-only tools, search/read/analyze
113
+ * (suitable for codebase exploration); "coder" — full tool set, self-contained
114
+ * implementation tasks; "plan" — read-only planning; "eng-coder" —
115
+ * engineering-mode implementation (design-token gated); "eng-designer" —
116
+ * engineering-mode design writing (requirements + design docs, batchDoc gated).
117
+ * - no role specified — invalid by design since the 2026-08-25 fail-closed gate
118
+ * (role is mandatory; "no role → same tool set as parent" was removed with the
119
+ * coder-leak fix and the header text above predates it)
120
+ * - non-recursive: child agents do not get the subagent tool (depth > 0 is not injected)
121
+ */
122
+
123
+ export const subagentTool = {
124
+ name: "subagent",
125
+ description:
126
+ "ONE tool, EIGHT actions — pick by what you need:\n" +
127
+ "- 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" +
128
+ "- 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}]}}). 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: 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" +
129
+ "- 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" +
130
+ "- 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" +
131
+ "- 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: 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" +
132
+ "- 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: pass the review id — its controller aborts, no token is issued for a cancelled review.\n" +
133
+ "- 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" +
134
+ "- 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" +
135
+ "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" +
136
+ "Available roles (which roles are exposed depends on the active mode — see Mode filtering below):\n" +
137
+ "- 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" +
138
+ "- plan — read-only implementation planning. Same read/search toolset; NEVER edits files. Returns a step-by-step plan for the parent to execute.\n" +
139
+ "- 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" +
140
+ "- 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" +
141
+ "- 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" +
142
+ "Mode filtering: normal mode exposes explore/plan/coder (eng-coder and eng-designer are refused); engineering mode exposes explore/eng-designer/eng-coder (plan and coder are refused — plan is a normal-mode role, coder is replaced by eng-coder). The role enum is filtered per mode at assembly, and the spawn gate refuses out-of-mode roles mechanically.\n\n" +
143
+ "Async spawn: 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: 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" +
144
+ "Task scheduling: 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" +
145
+ "Writing the prompt:\n" +
146
+ "- 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" +
147
+ "- Put exact paths and commands in the prompt when you know them. The sub-agent should not search for things you already know.\n" +
148
+ "- Do not delegate understanding: if the task hinges on a file path or line number, find it yourself first and write it into the prompt.\n" +
149
+ "- Once a sub-agent is running, leave that scope to it: don't redo its searches in parallel, and don't abandon it midway to finish manually.",
150
+ parameters: {
151
+ type: "object",
152
+ properties: {
153
+ 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), send (inject a direction into a RUNNING async subagent, consumed at its next turn boundary as an ordinary instruction), 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), 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, 2026-09-07). See the tool description for the full action matrix." },
154
+ 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)." },
155
+ 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." },
156
+ 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." },
157
+ context: { type: "string", description: "Optional background the sub-agent needs (it cannot see this conversation); action:'spawn' only." },
158
+ 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)." },
159
+ 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)." },
160
+ 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." },
161
+ 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)." },
162
+ 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." },
163
+ round: { type: "string", enum: ROUND_VALUES, description: "REQUIRED for role='eng-coder' and role='eng-designer' (explore/plan/coder exempt — and advisor is review-only, never a spawn write role): the implementation round this spawn is — initial (first implementation round for this design) or fix (a correction round reusing the same designId+designToken; docs FIRST). No default: a missing round is a dispatch defect and the spawn is mechanically refused." },
164
+ 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); depth>0 → sync (forced). action:'escalate': same semantics — default async at depth 0; async:false keeps the legacy synchronous flight (mechanism parameter — see the Async spawn section for top-level guidance)." },
165
+ 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)." },
166
+ 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." },
167
+ 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)." },
168
+ 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)." },
169
+ recent: { type: "integer", description: "action:'observe' only (optional, default 5): how many recent-turn summaries to return (clamped 1..20 — N2 keeps observe terse)." },
170
+ },
171
+ required: [],
172
+ },
173
+ readonly: false,
174
+ sideEffectExempt: true, // child agent may write files; parent can't introspect its _mutatedThisRun
175
+ parallel: true,
176
+ async execute(args, ctx) {
177
+ // §19 action dispatch: default spawn keeps every legacy call unchanged
178
+ // (no action parameter → the spawn path below, byte-identical semantics).
179
+ const action = args?.action !== undefined && args?.action !== null && String(args.action) !== ""
180
+ ? String(args.action)
181
+ : "spawn"
182
+ if (action !== "spawn") {
183
+ // §19 restricted-variant action gate (round2 #3): the engineering-child channel
184
+ // (depth>0, role eng-coder or eng-designer) is spawn-only — escalate spawns a
185
+ // coder+WRITE child (violates explore-only intent) and status/panel/observe/send
186
+ // have no async pool / panel mirror to query in a child context.
187
+ if ((ctx.depth ?? 0) > 0 && (ctx.agent?._role === "eng-coder" || ctx.agent?._role === "eng-designer")) {
188
+ 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`)
189
+ }
190
+ // §17 N3/D-S6 spawn gate (manual tier): auto-turn digests may not spawn —
191
+ // async OR blocking — the digest must stay organize-only. The escalate
192
+ // action spawns a write child too, so the same mechanical refusal applies
193
+ // (AUTO tier exempt — user authorized unattended continuation).
194
+ if (action === "escalate" && ctx.agent?._inAutoTurn && !ctx.agent?.autoApprove) {
195
+ return JSON.stringify({ status: "error", error: "cannot spawn subagents from a manual auto-turn — wait for user input" })
196
+ }
197
+ if (action === "status") return executeStatusAction(args, ctx)
198
+ if (action === "escalate") return await executeEscalateAction(args, ctx)
199
+ // §19.5 控制类动作:digest 内放行(D-S7 分类——控制/自省;dispatch 控制类
200
+ // 豁免同批生效——19.5.2b round2 #4;escalate 的 digest 拒绝在上一分支)
201
+ if (action === "cancel") return executeCancelAction(args, ctx)
202
+ // 2026-09-07 token 链终消费制(ENGINEERING-MODE.md §2.6 F1):父侧核销消费——
203
+ // 非只读控制动作——depth-0 + 工程模式限定(本分流已过受限变体门;工程模式门在
204
+ // 执行器内)——planMode 拒绝(dispatch 不豁免)——不入批审批分组(dispatch 免审)。
205
+ if (action === "consume-design") return executeConsumeDesignAction(args, ctx)
206
+ // §19.6 panel 动作:view(readonly 面——digest 内放行——自省类)与 freeze
207
+ // (控制类——同 cancel——digest 内放行)。深度/门控检查在 executePanelAction 内。
208
+ // CLI-ACTIVITY-DEBLOAT F-3(2026-09-10)接线:executePanelAction 经 ctx.state
209
+ // (= agent._tuiState——startTUI 反向挂载)读时现算面板块(computePanelBlocks)——
210
+ // 手工面板镜像已退役。headless/VSC 无挂载 → 现算返 null → 降级照旧。
211
+ if (action === "panel") return executePanelAction(args, { ...ctx, state: ctx.agent?._tuiState })
212
+ // SUBAGENT-OBSERVE-SEND:observe = readonly 查询(同 status——digest/planMode 放行);
213
+ // send = 控制类豁免(同 cancel——父回合内显式调用即授权)。深度门在各自执行器内。
214
+ if (action === "observe") return executeObserveAction(args, ctx)
215
+ if (action === "send") return executeSendAction(args, ctx)
216
+ throw new Error(`Unknown subagent action: ${JSON.stringify(action)}. Valid actions: spawn, status, escalate, cancel, panel, consume-design, observe, send.`)
217
+ }
218
+
219
+ const parent = ctx.agent
220
+ const role = args.role
221
+ // Spawn requires a task brief — schema `required` is advisory (multi-action
222
+ // schema), so the mechanical check lives here: an absent task would otherwise
223
+ // flow downstream as `content: undefined` and surface as an obscure error.
224
+ if (typeof args.task !== "string" || !args.task.trim()) {
225
+ throw new Error("subagent action:'spawn' requires a task (the self-contained task brief).")
226
+ }
227
+ // §18 D-E1a depth-gated async default (2026-09-06 需求池 R12): depth-0 spawns
228
+ // default to async for EVERY role (the old role-level default — eng-coder only —
229
+ // is superseded); depth>0 spawns default to sync (子代理内部强制同步现状保留).
230
+ // async:false remains the explicit escape hatch; async:true at depth>0 is
231
+ // refused downstream (executeAsyncSpawn top-level gate).
232
+ const wantAsync = args.async ?? ((ctx.depth ?? 0) === 0)
233
+
234
+ // Role normalization + whitelist (2026-08-25, coder-leak fix): exact-string gates let
235
+ // variant roles ("Coder", " coder") bypass BOTH mode gates and fall through to
236
+ // full tools / no overlay — a full-write coder without design review. Schema enums are
237
+ // advisory; providers don't enforce them. Fail closed on unknown roles.
238
+ const ROLES = new Set(["explore", "plan", "coder", "eng-coder", "eng-designer"])
239
+ if (!ROLES.has(role)) {
240
+ throw new Error(`Unknown subagent role: ${JSON.stringify(role)}. Valid roles: explore, plan, coder, eng-coder, eng-designer (exact spelling).`)
241
+ }
242
+ // §18 D-E3 internal-spawn gate: an eng-coder sub-agent may only spawn sync
243
+ // explore (audit) children — non-explore roles and async are refused here
244
+ // (mechanical), the audit budget is enforced (7th audit spawn refused), and
245
+ // the returned attempt number marks this spawn as an audit for the task-book
246
+ // augmentation below. Runs BEFORE the mode gates so the eng-coder-specific
247
+ // error (not the generic engineering-mode one) surfaces.
248
+ const engAuditAttempt = gateEngCoderSpawn(ctx.agent, ctx.depth, role, args.async)
249
+ // Role is mutually exclusive per mode: normal mode → "coder", engineering mode → "eng-coder"/"eng-designer"
250
+ if (parent.config?.agent?.engineering && role === "coder") {
251
+ throw new Error("Engineering mode: role='coder' is disabled — use role='eng-coder' for implementation tasks (or role='eng-designer' for design writing).")
252
+ }
253
+ if (parent.config?.agent?.engineering && role === "plan") {
254
+ throw new Error("Engineering mode: role='plan' is disabled — the engineering-mode enum is explore / eng-designer / eng-coder (plan is a normal-mode role).")
255
+ }
256
+ if (!parent.config?.agent?.engineering && role === "eng-coder") {
257
+ throw new Error("Engineering mode is not active — use role='coder' for implementation tasks.")
258
+ }
259
+ // Third mode gate (ENGINEERING-MODE.md §2.15 A—— symmetric completion):
260
+ // eng-designer is engineering-mode-only, same family as eng-coder (both carry
261
+ // the engineering discipline overlay + the batchDoc gate).
262
+ if (!parent.config?.agent?.engineering && role === "eng-designer") {
263
+ 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.")
264
+ }
265
+
266
+ // §17 N3/D-S6 spawn gate (manual tier): auto-turn digests may not spawn — async
267
+ // OR blocking — the digest must stay organize-only. AUTO tier (autoApprove) is
268
+ // exempt (推进型 — user authorized unattended continuation). Mechanical refusal
269
+ // so the digest never pops a permission panel or chains new background work.
270
+ // (escalate 动作的同类拒绝在 action 分流处——本检查只管 spawn 路径。)
271
+ if (parent._inAutoTurn && !parent.autoApprove) {
272
+ return JSON.stringify({ status: "error", error: "cannot spawn subagents from a manual auto-turn — wait for user input" })
273
+ }
274
+
275
+ // §20 准入(2026-09-05 module-split——prepareScheduling verbatim 迁
276
+ // subagent-spawn.mjs:参数形态/unknown id/依赖环/阻塞 sync 判定;files 目录声明
277
+ // fail-closed——检测器错误即工具结果 JSON)
278
+ const prep = prepareScheduling(parent, args.files, args.dependsOn, wantAsync)
279
+ if (prep.errorJson) return prep.errorJson
280
+
281
+ // child 装配(2026-09-05 module-split——buildSpawnChild verbatim 迁
282
+ // subagent-spawn.mjs:provider/model 覆盖、角色门、design-token 门、工具集/
283
+ // overlay/permission 装配、审计任务书注入、relay 前缀分配、childOpts/runOpts)
284
+ const built = buildSpawnChild(parent, ctx, args, role, wantAsync, prep.files, prep.dependsOn, engAuditAttempt)
285
+ const { child, input, childOpts, childRunOpts, relayPrefix } = built
286
+ // Turn-cap continue loop (TURN-CAP-CONTINUE.md) via runWithContinue (§7.2 D3):
287
+ // hitting the cap asks the user via the SAME y/n panel the main agent uses —
288
+ // unlimited continues, resume:true keeps the child's history + mutation bookkeeping,
289
+ // fresh budget each run. Prompts queue through parent._permQueue (same as write
290
+ // approval) so parallel children never pop two panels at once. Declined / headless
291
+ // → partial-work return. Non-ContinueError errors still propagate (dispatch.mjs
292
+ // turns them into Error tool results — unchanged behavior).
293
+ const askSubagentContinue = (e) => {
294
+ if (!ctx.onPermissionRequest) return Promise.resolve(false)
295
+ const key = relayPrefix.slice(0, -1)
296
+ const ask = async () => {
297
+ // SYNC-CANCEL v2(模态 deny——用户裁):⏹ 后 entry.stopped——不再弹模态。
298
+ // ⚠ 不能直接 resolve(false) 走 onDeclined 降级(TURN_CAP partial——child 已撞
299
+ // cap——runWithContinue 的 decline 是正常 return——永远到不了 abort 检出点——
300
+ // stopped 折叠语义丢失:无 ⟦ev⟧stopped/无 STOPPED_MARK——块冻结标 done 而非
301
+ // stopped——评审 🟡#2)。stopped 分支改抛 AbortError——runWithContinue 只捕
302
+ // ContinueError——原样上抛 → 阻塞 catch 三分支②折叠("child 随即在 abort 检出点
303
+ // 解绕折叠"——AGENT-LOOP §7.2 机制文)。abort 恒已在途(stopped 只由
304
+ // cancelSyncChild 与 ctrl.abort 同时置位)——信号语义真实。
305
+ if (parent._syncChildAborts?.get(key)?.stopped) throw abortError(ctrl.signal, "settle", "sync-stopped")
306
+ const go = await ctx.onPermissionRequest("continue", { turns: e.turn, agent: key })
307
+ // ⏹ deny(denyModalForOwner resolve(false))与用户按 n 同形——旗标区分:
308
+ // stopped → 同上抛(折叠——abort 先于 deny 已在途);普通 n → false 走 decline
309
+ // (现状——cap partial 报告)。
310
+ if (parent._syncChildAborts?.get(key)?.stopped) throw abortError(ctrl.signal, "settle", "sync-stopped")
311
+ return go
312
+ }
313
+ return enqueueAsk(parent, "_permQueue", ask)
314
+ }
315
+
316
+ // ── Async branch(2026-09-05 module-split——executeAsyncSpawn verbatim 迁
317
+ // subagent-run.mjs:条目构建/等位/启动/controller 链/turn 镜像/补位释放)──
318
+ if (wantAsync) {
319
+ return executeAsyncSpawn(parent, ctx, role, args, child, input, childOpts, childRunOpts, relayPrefix, built.childProvider, prep.files, prep.dependsOn)
320
+ }
321
+
322
+ // ── Blocking path (unchanged semantics + SYNC-CANCEL targeted stop) ──
323
+ // SYNC-CANCEL F1/F5(2026-09-09):自属 AbortController(armSyncChildAbort——
324
+ // childRunOpts.signal 覆写 ctrl.signal——照抄 async 分支 subagent-run.mjs 的
325
+ // 覆写模式——buildChildRunOpts 不改——escalate/consult 零触碰)——⏹ 定向中止
326
+ // (cancelSyncChild → ctrl.abort)与整回合停(base abort 逐链传播)解耦;registry
327
+ // 注册/注销(try/finally 三路径——R7 防跨回合残留)。LOGGING(LOGGING.md):
328
+ // child:*(阻塞 spawn——runChildPipeline 前后;declined partial 由 TURN_CAP_MARK
329
+ // 检出;⏹ 折叠由 STOPPED_MARK 检出——kind partial;错误原样上抛(dispatch 转
330
+ // tool:error))
331
+ const blockT0 = Date.now()
332
+ logEvent("child:spawn", { role, id: child._logId, kind: "blocking" })
333
+ const syncKey = relayPrefix.slice(0, -1)
334
+ // baseSignal 一次性快照(spawn 时刻)——catch 分类复用同一信号对象(会话收尾把
335
+ // _sessionSignal 置 null 的窗口内重读会漂移——快照防误判)
336
+ const baseSignal = buildChildSignal(parent, ctx)
337
+ // #133 sync 出生序:`[model]` 出生声明由 arm 单点在 registry 写入**之后**宣告(不早于登记;
338
+ // 生产形 = 第 4 参 announce 闭包,模型取自 built.childProvider——T-S1e 结构机检锚)。
339
+ const { ctrl, disarm } = armSyncChildAbort(parent, syncKey, baseSignal, () => emitRelayModel(ctx.callbacks?.onToken, relayPrefix, built.childProvider?.model ?? ""))
340
+ let pipelineReport
341
+ try {
342
+ pipelineReport = await runChildPipeline(child, input, childOpts, { ...childRunOpts, signal: ctrl.signal }, {
343
+ parent, role, args,
344
+ askContinue: askSubagentContinue,
345
+ })
346
+ } catch (e) {
347
+ // SYNC-CANCEL F2 三分支(classifySyncAbort 纯函数):
348
+ const cls = classifySyncAbort(ctx.signal, baseSignal, ctrl.signal, e)
349
+ if (cls === "base") {
350
+ // ① 整回合停(现状逐字保留——挂起场景 base 命中而 ctx.signal 未 abort——R2):
351
+ // §27 R23:外层 abort 传播的中断——内层开块随之外层冻结前先收尾定格
352
+ // (D-R23c1 stopped——T-R23c.2a 生成侧路径;TUI 冻结兜底仍在 freezeSubTaskLines)
353
+ emitNestedChildEvent(ctx, relayPrefix, "stopped")
354
+ throw e // 用户停——不落错误事件
355
+ }
356
+ if (cls === "error") {
357
+ // ③ 其他错误(现状逐字保留——:249-253):
358
+ // §27 R23 error-run 映射(实现批补一行):run 错误(非 abort)→ 同样发 stopped
359
+ // ——内层子块定格不悬空(T-R23a.3——工具错/运行错误路径)。
360
+ emitNestedChildEvent(ctx, relayPrefix, "stopped")
361
+ logEvent("child:error", { role, id: child._logId, ms: Date.now() - blockT0, err: errText(deathLine(e, ctrl?.signal), 200) })
362
+ throw e
363
+ }
364
+ // ② targeted 折叠(err AbortError && 自属 ctrl aborted && 非整回合停):merge +
365
+ // stopped partial 报告(父回合继续拿报告——AC1/AC3)。merge 镜像 escalate sync
366
+ // runner 先例(subagent-actions.mjs runner 包装层——guard 在 mergeChildMutations
367
+ // 内——见子代理已写文件才传播)。
368
+ if (role === "eng-coder" && child._mutatedThisRun) mergeChildMutations(parent, child)
369
+ pipelineReport = buildSyncStoppedReport(role, child._capturedOutput ?? "", args?.designId)
370
+ // 块冻结标 stopped(R6——非 done):⏹ 定向中止的 TUI 顶层块立即定格 stopped
371
+ // (async settle cancelled 分支同款直发——async-settle.mjs settleAsyncEntry);
372
+ // 嵌套(eng-coder 内 explore 审计)经 emitNestedChildEvent 定格子块——stopped
373
+ // 幂等无害(重复/迟到 done 由 §27.1 F2 done 子块定格丢弃兜底)。
374
+ ctx.callbacks?.onToken?.(`${relayPrefix}⟦ev⟧stopped\x1e0\x1e0\x1estopped\x1e`)
375
+ emitNestedChildEvent(ctx, relayPrefix, "stopped")
376
+ } finally {
377
+ // R7 防跨回合残留:成功/折叠②/整回合停①/错误③ 四出口统一注销(设计"三路径"
378
+ // 口径 = 成功/折叠/整回合停——错误③ 同样 rethrow 经 finally——同归本注销)
379
+ disarm()
380
+ }
381
+ // §27 R23 D-R23c1(评审 #1 🅰——生成侧补发射):sync spawn 同步收尾——若本 spawn
382
+ // 处于嵌套上下文(ctx.callbacks 已是嵌套 wrapper——eng-coder 内 explore 审计)→
383
+ // 发内层 ⟦ev⟧done(完整嵌套前缀——wrapper 链自动补外层)→ 主 TUI 路由子块定格
384
+ // (T-R23c.1)。非嵌套(depth-0)零变化——冻结仍由 dispatch subKey 精确冻承接。
385
+ // ② 折叠 = 正常 return(本共用出口:done 补发照设 + ctx._subagentKey 照设——成功
386
+ // 冻结管线复用——迟到 done 对已定格 stopped 块被丢弃——幂等无害)。
387
+ emitNestedChildEvent(ctx, relayPrefix, "done")
388
+ 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" })
389
+ // §7.2.3 sync spawn 完成精确冻结(方案 e):execute 返回前 ctx 留子代理 key
390
+ // (relayPrefix 去尾 = `role#N`)——dispatch runOne 读它作 onToolResult 第 4 参 →
391
+ // TUI finishSubTaskKey 按 key 精确冻(async eng-coder 先启动时不再误冻其块——
392
+ // T-F2)。仅成功/折叠路径设置:async 分支不设(round2 #2——ack 带 status:running 由
393
+ // isAsyncSpawnResult 跳过冻结);base/error 路径到此之前已 throw——ctx 未设
394
+ // ——中止/错误路径不触发冻结(round1 #1——T-F5)。
395
+ ctx._subagentKey = syncKey
396
+ return pipelineReport
397
+ },
398
+ }
399
+
400
+ // Re-export shim (2026-09-03 拆分轮 + §19 合体轮 + 2026-09-05 拆分轮): 机械与动作
401
+ // 执行器迁至 ./subagent-async.mjs、./subagent-actions.mjs、./subagent-scheduler.mjs
402
+ // ——本文件保留导出面,消费点(agent.mjs / agent-turn.mjs / consult.mjs / 测试)导入
403
+ // 路径零改动;池逻辑/准入见 subagent-run.mjs(executeAsyncSpawn)与 subagent-scheduler.mjs
404
+ // (maybeRefillAsync——execute 不再直接使用池常量)。
405
+ // 2026-09-05 拆分轮: maybeRefillAsync 随 §20 调度器独立(./subagent-scheduler.mjs)——
406
+ // 再导出源改写,消费面(agent.mjs 动态 import 等)不变。
407
+ // 2026-09-06 §11.1 拆分轮: ASYNC_SUBAGENT_LIMIT 导出 → ASYNC_POOL_LIMITS(分域常量——
408
+ // 定义在 subagent-async.mjs——re-export 面同步)。
409
+ export {
410
+ ASYNC_POOL_LIMITS,
411
+ resolveChildProvider,
412
+ injectAsyncResult,
413
+ buildChildRunOpts,
414
+ mergeChildMutations,
415
+ } from "./subagent-async.mjs"
416
+ export { maybeRefillAsync } from "./subagent-scheduler.mjs"
417
+ // 2026-09-05 module-split:spawn 装配 helpers 迁 subagent-spawn.mjs——re-export 保测试
418
+ // import 面(subagent-core.test.mjs 从本文件动态 import)
419
+ export { effectiveSubagentModel, resolveDesignSlot } from "./subagent-spawn.mjs"
@@ -0,0 +1,87 @@
1
+ /** Common synonyms LLMs tend to use — normalize to canonical values */
2
+ const STATUS_ALIASES = {
3
+ completed: "done",
4
+ finished: "done",
5
+ complete: "done",
6
+ done: "done",
7
+ pending: "pending",
8
+ todo: "pending",
9
+ open: "pending",
10
+ waiting: "pending",
11
+ in_progress: "in_progress",
12
+ inprogress: "in_progress",
13
+ active: "in_progress",
14
+ running: "in_progress",
15
+ working: "in_progress",
16
+ }
17
+
18
+ function normalizeStatus(raw) {
19
+ if (!raw) return "pending"
20
+ const key = String(raw).toLowerCase().replace(/[\s_-]+/g, "")
21
+ return STATUS_ALIASES[key] ?? STATUS_ALIASES[raw] ?? null
22
+ }
23
+
24
+ /**
25
+ * task tool: multi-step task planning and progress tracking (Claude Code's todo mode).
26
+ * Each call replaces the entire list; only modifies agent internal state (no external world), so readonly.
27
+ * Accesses the caller agent via ctx.agent (injected by runAgent).
28
+ */
29
+ export const taskTool = {
30
+ name: "task",
31
+ description:
32
+ "Plan and track a task list for complex multi-step work. Each call replaces the entire list. " +
33
+ "Keep exactly one item in_progress at a time; mark items done as you complete them; never mark done if tests fail or work is partial. " +
34
+ "Statuses: pending | in_progress | done — synonyms (completed/finished/complete, todo/open/waiting, active/running/working, …) are accepted and normalized with a warning. " +
35
+ "IMPORTANT: title is required and must be a non-empty string — items with empty titles are silently dropped. " +
36
+ "For cross-session / project-level tracking, use `/ledger`. " +
37
+ "Returns the updated task list (or the new item's ID on add).",
38
+ parameters: {
39
+ type: "object",
40
+ properties: {
41
+ items: {
42
+ type: "array",
43
+ items: {
44
+ type: "object",
45
+ properties: {
46
+ title: { type: "string" },
47
+ status: { type: "string", enum: ["pending", "in_progress", "done"] },
48
+ },
49
+ required: ["title", "status"],
50
+ },
51
+ },
52
+ },
53
+ required: ["items"],
54
+ },
55
+ readonly: true,
56
+ async execute(args, ctx) {
57
+ // Keep only non-done items + the 3 most recently completed (for context reference), max 20 to prevent accumulation
58
+ const warnings = []
59
+ const raw = (args.items ?? []).map((it) => {
60
+ const normalized = normalizeStatus(it.status)
61
+ if (normalized && normalized !== it.status) {
62
+ warnings.push(`status "${it.status}" normalized to "${normalized}"`)
63
+ } else if (!normalized) {
64
+ warnings.push(`"${it.status}" is not valid (use: pending | in_progress | done)`)
65
+ }
66
+ const title = String(it.title ?? "").trim()
67
+ if (!title) {
68
+ warnings.push(`empty title skipped (item was: ${JSON.stringify(it).slice(0, 100)})`)
69
+ }
70
+ return {
71
+ title,
72
+ status: normalized ?? "pending",
73
+ }
74
+ }).filter((t) => t.title.length > 0)
75
+ const pending = raw.filter((t) => t.status !== "done")
76
+ const recentDone = raw.filter((t) => t.status === "done").slice(-3)
77
+ const items = [...pending, ...recentDone].slice(0, 20)
78
+ ctx.agent.tasks = items
79
+ ctx.agent._taskPushbacks = 0 // task list changed — the completion gate earns a fresh reminder
80
+ ctx.agent._onTaskUpdate?.(items)
81
+ const done = items.filter((i) => i.status === "done").length
82
+ const open = items.length - done
83
+ const warningText = warnings.length > 0 ? ` ⚠️ ${warnings.join("; ")}` : ""
84
+ return `Task list updated: ${done}/${items.length} done` +
85
+ (open > 0 ? ` — ${open} item(s) still open.` : " — all done.") + warningText
86
+ },
87
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * timer tool: set a time budget for thinking before the agent insists on action.
3
+ * Call this when starting to analyze code or debug — it gives a bounded
4
+ * thinking window. When the timer fires, a system reminder is injected
5
+ * suggesting the model try running code, adding logs, or otherwise acting
6
+ * instead of continuing to think.
7
+ */
8
+ export const timerTool = {
9
+ name: "timer",
10
+ description:
11
+ "Set a timer before you start analyzing code. When the timer fires, " +
12
+ "a system reminder will be injected suggesting you try running code or " +
13
+ "adding debug logs. Use this to enforce a thinking budget: you get " +
14
+ "N seconds to reason, then the timer reminds you to act. " +
15
+ "Returns the set confirmation — the reminder fires at the deadline.",
16
+ parameters: {
17
+ type: "object",
18
+ properties: {
19
+ seconds: {
20
+ type: "number",
21
+ description: "Thinking budget in seconds (default 180). Longer for complex reasoning, shorter for simple tasks.",
22
+ },
23
+ message: {
24
+ type: "string",
25
+ description: "Custom reminder message to show when time is up. Default: a suggestion to add debug logs or run the code.",
26
+ },
27
+ },
28
+ required: [],
29
+ },
30
+ readonly: true,
31
+ sideEffectExempt: true,
32
+ execute(args, ctx) {
33
+ // D15.3#1:默认 180;非法值(非数字/非正数)显式报错——静默 NaN 定时器永不触发的坑
34
+ const seconds = Number(args.seconds ?? 180)
35
+ if (!Number.isFinite(seconds) || seconds <= 0) {
36
+ throw new Error(`timer seconds must be a positive number, got: ${args.seconds}`)
37
+ }
38
+ const expiresAt = Date.now() + seconds * 1000
39
+ const message = args.message || `⏰ Time's up (${seconds}s). Have you tried running the code, adding a console.log, or checking the output? Thinking more without data is guessing.`
40
+
41
+ ctx.agent._pendingTimers = ctx.agent._pendingTimers ?? []
42
+ ctx.agent._pendingTimers.push({ id: Date.now(), expiresAt, message })
43
+
44
+ return `Timer set for ${seconds} seconds. A reminder will appear when time is up.`
45
+ },
46
+ }