thincoder 0.12.58 → 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 (158) hide show
  1. package/CHANGELOG.md +78 -2
  2. package/README.md +3 -3
  3. package/bin/thincoder.mjs +88 -19
  4. package/package.json +4 -3
  5. package/src/acp/bridge.mjs +135 -26
  6. package/src/advisor/messages.mjs +57 -4
  7. package/src/advisor/run.mjs +119 -79
  8. package/src/advisor.mjs +34 -7
  9. package/src/agent/completion.mjs +17 -11
  10. package/src/agent/dispatch.mjs +182 -22
  11. package/src/agent/helpers.mjs +71 -4
  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 +107 -20
  16. package/src/agent/spawn-child.mjs +54 -4
  17. package/src/agent-tools/advisor-async.mjs +456 -0
  18. package/src/agent-tools/advisor.mjs +133 -109
  19. package/src/agent-tools/async-settle.mjs +191 -0
  20. package/src/agent-tools/consult.mjs +154 -104
  21. package/src/agent-tools/design-token.mjs +104 -0
  22. package/src/agent-tools/eng.mjs +26 -30
  23. package/src/agent-tools/escalate-async.mjs +286 -0
  24. package/src/agent-tools/goal.mjs +11 -1
  25. package/src/agent-tools/read-history.mjs +284 -0
  26. package/src/agent-tools/recent-changes.mjs +2 -1
  27. package/src/agent-tools/settings.mjs +152 -0
  28. package/src/agent-tools/skill.mjs +2 -1
  29. package/src/agent-tools/subagent-actions.mjs +470 -0
  30. package/src/agent-tools/subagent-async.mjs +382 -0
  31. package/src/agent-tools/subagent-panel.mjs +153 -0
  32. package/src/agent-tools/subagent-run.mjs +202 -0
  33. package/src/agent-tools/subagent-scheduler.mjs +343 -0
  34. package/src/agent-tools/subagent-spawn.mjs +406 -0
  35. package/src/agent-tools/subagent.mjs +203 -377
  36. package/src/agent-tools/task.mjs +4 -3
  37. package/src/agent-tools/timer.mjs +9 -4
  38. package/src/agent-tools/verify.mjs +198 -238
  39. package/src/agent-tools.mjs +1 -0
  40. package/src/agent.mjs +145 -242
  41. package/src/auto-think.mjs +14 -0
  42. package/src/cli/distill-command.mjs +10 -4
  43. package/src/cli/make-agent.mjs +4 -1
  44. package/src/cli/memory-command.mjs +2 -1
  45. package/src/cli/permission.mjs +8 -1
  46. package/src/cli/setup-wizard.mjs +17 -12
  47. package/src/config.mjs +61 -8
  48. package/src/context.mjs +81 -163
  49. package/src/crash-reports.mjs +123 -0
  50. package/src/distill.mjs +30 -12
  51. package/src/escape.mjs +6 -4
  52. package/src/explore-distill.mjs +155 -0
  53. package/src/log.mjs +195 -0
  54. package/src/memory/code-sync.mjs +2 -1
  55. package/src/memory/core.mjs +11 -72
  56. package/src/memory/delete.mjs +234 -0
  57. package/src/memory/docs.mjs +206 -87
  58. package/src/memory.mjs +3 -1
  59. package/src/model-specs.mjs +15 -1
  60. package/src/peer-domains.mjs +265 -0
  61. package/src/peer-instances.mjs +231 -0
  62. package/src/prompt-overlays.mjs +25 -0
  63. package/src/prompts/advisor-design.md +18 -39
  64. package/src/prompts/advisor-round1.md +20 -32
  65. package/src/prompts/advisor-round2.md +16 -16
  66. package/src/prompts/advisor-round3.md +16 -16
  67. package/src/prompts/coder.md +7 -28
  68. package/src/prompts/consult-base.md +4 -11
  69. package/src/prompts/discipline.md +31 -44
  70. package/src/prompts/eng-coder.md +9 -34
  71. package/src/prompts/engineering-sub.md +10 -8
  72. package/src/prompts/engineering.md +61 -264
  73. package/src/prompts/explore.md +4 -14
  74. package/src/prompts/main.md +18 -35
  75. package/src/prompts/methodology-template.md +32 -38
  76. package/src/prompts/plan.md +2 -9
  77. package/src/prompts/system.md +18 -35
  78. package/src/provider/core.mjs +62 -69
  79. package/src/provider/errors.mjs +76 -0
  80. package/src/provider/retry.mjs +8 -45
  81. package/src/session-gc.mjs +214 -0
  82. package/src/session-guard.mjs +47 -0
  83. package/src/session-rename.mjs +38 -0
  84. package/src/session-slots.mjs +181 -58
  85. package/src/session.mjs +48 -89
  86. package/src/token-ttl.mjs +273 -0
  87. package/src/tools/apply_patch.md +3 -1
  88. package/src/tools/bash.md +1 -1
  89. package/src/tools/checklist-sync.mjs +181 -0
  90. package/src/tools/checklist.mjs +52 -39
  91. package/src/tools/delete.md +1 -0
  92. package/src/tools/edit-batch.mjs +131 -44
  93. package/src/tools/edit-diff.mjs +348 -0
  94. package/src/tools/edit.md +20 -13
  95. package/src/tools/execute.md +7 -7
  96. package/src/tools/execute.mjs +55 -24
  97. package/src/tools/file.mjs +25 -70
  98. package/src/tools/file_ops.md +2 -1
  99. package/src/tools/get_current_time.md +3 -1
  100. package/src/tools/git.mjs +14 -6
  101. package/src/tools/glob-dialect.mjs +130 -0
  102. package/src/tools/glob.md +3 -3
  103. package/src/tools/grep.md +1 -1
  104. package/src/tools/hashline_edit.md +2 -0
  105. package/src/tools/index.mjs +3 -3
  106. package/src/tools/insert_after.md +2 -1
  107. package/src/tools/lint.md +2 -0
  108. package/src/tools/lsp.md +4 -1
  109. package/src/tools/ops.mjs +175 -3
  110. package/src/tools/patch.mjs +84 -13
  111. package/src/tools/question.md +5 -1
  112. package/src/tools/repomap.mjs +1 -1
  113. package/src/tools/shared.mjs +18 -25
  114. package/src/tools/system.mjs +50 -30
  115. package/src/tools/tree.md +2 -1
  116. package/src/tools/wait_for.md +22 -0
  117. package/src/tools/web.mjs +5 -3
  118. package/src/tools/websearch.md +2 -1
  119. package/src/tools/write.md +2 -0
  120. package/src/traces/trace-store.mjs +224 -0
  121. package/src/tui/agent-turn.mjs +179 -27
  122. package/src/tui/clipboard.mjs +15 -4
  123. package/src/tui/cmd-config.mjs +77 -16
  124. package/src/tui/cmd-eng.mjs +20 -16
  125. package/src/tui/cmd-extract.mjs +1 -1
  126. package/src/tui/cmd-mcp.mjs +17 -2
  127. package/src/tui/cmd-new.mjs +3 -2
  128. package/src/tui/cmd-session.mjs +19 -4
  129. package/src/tui/cmd-think.mjs +11 -11
  130. package/src/tui/cmd-upgrade.mjs +19 -4
  131. package/src/tui/config-helpers.mjs +28 -16
  132. package/src/tui/distill-cmd.mjs +1 -1
  133. package/src/tui/index.mjs +31 -96
  134. package/src/tui/interaction.mjs +13 -2
  135. package/src/tui/key-handler.mjs +105 -155
  136. package/src/tui/key-modes.mjs +215 -0
  137. package/src/tui/layout.mjs +22 -1
  138. package/src/tui/mouse.mjs +46 -0
  139. package/src/tui/pickers.mjs +51 -25
  140. package/src/tui/render-conversation.mjs +13 -161
  141. package/src/tui/render-frame.mjs +27 -10
  142. package/src/tui/render-loop.mjs +4 -1
  143. package/src/tui/render-segments.mjs +182 -0
  144. package/src/tui/startup.mjs +40 -0
  145. package/src/tui/subagent-blocks.mjs +272 -262
  146. package/src/tui/subagent-children.mjs +176 -0
  147. package/src/tui/subagent-freeze.mjs +172 -0
  148. package/src/tui/subagent-panel.mjs +125 -12
  149. package/src/tui/suspension-drive.mjs +351 -0
  150. package/src/tui/tool-args.mjs +10 -2
  151. package/src/tui/tool-display.mjs +142 -0
  152. package/src/tui/tool-events.mjs +127 -231
  153. package/src/tui/tui-lifecycle.mjs +29 -0
  154. package/src/tui/update-notice.mjs +76 -0
  155. package/src/tui/wizard.mjs +48 -12
  156. package/src/agent-tools/escalate.mjs +0 -179
  157. package/src/agent-tools/subagent-check.mjs +0 -107
  158. package/src/tools/exec-prelude.mjs +0 -84
@@ -1,106 +1,74 @@
1
- import {
2
- createAgent, runAgent,
3
- readonlyToolNames, collectGitContext, escapeXml,
4
- EXPLORE_OVERLAY, CODER_OVERLAY, PLAN_OVERLAY, ENG_CODER_OVERLAY,
5
- MIN_REPORT_CHARS, REPORT_CONTINUATION, DEFAULT_SUBAGENT_TURNS,
6
- } from "../agent.mjs"
7
- import { makeRelay, wrapChildCallbacks, runWithContinue, TURN_CAP_MARK } from "../agent/spawn-child.mjs"
8
- import { validateDesignToken } from "./advisor.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
+ */
9
21
 
10
- // Async subagent limits (AGENT-LOOP.md §15 D-A4): mechanical concurrency cap for
11
- // background spawns + the per-turn check budget (consult-style loop guard).
12
- export const ASYNC_SUBAGENT_LIMIT = 4
13
- export const MAX_ASYNC_CHECKS = 3
22
+ import { gateEngCoderSpawn, TURN_CAP_MARK, emitNestedChildEvent } from "../agent/spawn-child.mjs"
23
+ import { logEvent, errText } from "../log.mjs"
24
+ import {
25
+ runChildPipeline, executeCancelAction,
26
+ } from "./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"
14
30
 
15
31
  /**
16
- * subagent tool: spawn a child agent to handle an independent subtask (isolated context, only the report is returned).
17
- * - role: "explore" read-only tools, search/read/analyze (suitable for codebase exploration)
18
- * - role: "coder" full tool set, self-contained implementation tasks (suitable for isolated coding)
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 child — consumed 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.
41
+ * - action:"spawn" roles: "explore" — read-only tools, search/read/analyze
42
+ * (suitable for codebase exploration); "coder" — full tool set, self-contained
43
+ * implementation tasks; "plan" — read-only planning; "eng-coder" —
44
+ * engineering-mode implementation (design-token gated).
19
45
  * - no role specified — invalid by design since the 2026-08-25 fail-closed gate
20
46
  * (role is mandatory; "no role → same tool set as parent" was removed with the
21
47
  * coder-leak fix and the header text above predates it)
22
- * - parallel subagent calls via the parallel channel (parallel: true)
23
48
  * - non-recursive: child agents do not get the subagent tool (depth > 0 is not injected)
24
49
  */
25
50
 
26
- /**
27
- * Effective subagent model override for a role (CLI parity shared with VS Code):
28
- * priority — subagent tool `model` arg > config.agent.subagentModels[role] > config.agent.subagentModel > null (inherit parent).
29
- */
30
- export function effectiveSubagentModel(parent, role, modelArg) {
31
- if (modelArg) return modelArg
32
- const cfg = parent.config?.agent ?? {}
33
- return cfg.subagentModels?.[role] ?? cfg.subagentModel ?? null
34
- }
35
-
36
- /**
37
- * Resolve the sub-agent's provider from a model override string (shared with the
38
- * VS Code port). Forms accepted:
39
- * "provider:model" → the named provider with the named model
40
- * "provider" → the named provider's configured model
41
- * "model" → same provider as the parent, different model
42
- * null → parent's provider unchanged.
43
- * API keys come from config.json only (env vars are not a key source).
44
- */
45
- export function resolveChildProvider(parent, modelArg) {
46
- if (!modelArg) return { ...parent.provider }
47
- const providers = parent.config?.providersList ?? []
48
- const withKey = (p) => (p.apiKey?.trim() ? { ...p, apiKey: p.apiKey.trim() } : { ...p })
49
- if (modelArg.includes(":")) {
50
- const [pname, mname] = modelArg.split(":")
51
- const p = providers.find((x) => x.name === pname)
52
- if (!p) throw new Error(`subagent model: unknown provider "${pname}" (available: ${providers.map((x) => x.name).join(", ") || "none"})`)
53
- return { ...withKey(p), model: mname || p.model }
54
- }
55
- const byName = providers.find((x) => x.name === modelArg)
56
- if (byName) return withKey(byName)
57
- return { ...parent.provider, model: modelArg }
58
- }
59
-
60
- /**
61
- * Resolve the design-token slot for an eng-coder spawn (2026-09-01 multi-design, FR3):
62
- * - designId given → exact slot lookup (no match = explicit error, never a fuzzy guess)
63
- * - designId omitted → exactly ONE slot must exist (single-design compatibility); with
64
- * multiple slots we refuse rather than pick one (T16: never silently aim the wrong design)
65
- * Returns { token } on success; throws with a parent-actionable message otherwise.
66
- * The HMAC/TTL check itself stays in validateDesignToken (unchanged).
67
- */
68
- export function resolveDesignSlot(parent, designIdArg) {
69
- const slots = parent._engDesignTokens
70
- const hasSlots = slots instanceof Map && slots.size > 0
71
- const legacy = parent._engDesignToken
72
- // eng(exit/enter) resets the single mirror to force a fresh review (eng.mjs) —
73
- // a non-empty slot map surviving that reset must NOT resurrect stale tokens:
74
- // mirror cleared + slots present = re-entered engineering mode → re-review.
75
- if (!legacy && hasSlots) {
76
- 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.")
77
- }
78
- if (designIdArg) {
79
- if (!hasSlots || !slots.has(designIdArg)) {
80
- 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))`)
81
- }
82
- return { token: slots.get(designIdArg) }
83
- }
84
- if (hasSlots && slots.size > 1) {
85
- 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.`)
86
- }
87
- if (hasSlots && slots.size === 1) return { token: [...slots.values()][0] }
88
- if (legacy) return { token: legacy } // single-slot mirror fallback (pre-multi-slot sessions)
89
- throw new Error("Invalid or missing design token — run advisor with type='design' first and pass the returned token as designToken.")
90
- }
91
-
92
51
  export const subagentTool = {
93
52
  name: "subagent",
94
53
  description:
95
- "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" +
54
+ "ONE tool, EIGHT actions (AGENT-LOOP.md §19/§19.5/§19.6/§19.8 + SUBAGENT-OBSERVE-SEND) pick by what you need:\n" +
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" +
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" +
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" +
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" +
96
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" +
97
64
  "Available roles (which roles are exposed depends on the active mode — see Mode filtering below):\n" +
98
- "- explore — read-only search & analysis. Toolset: the read/search family (grep, read, glob, code_search, doc_search, repo_outline, lsp, tree...). Receives git context auto-injected (branch, recent commits, working-tree state) when the project is a git repo. 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" +
65
+ "- 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" +
99
66
  "- plan — read-only implementation planning. Same read/search toolset; NEVER edits files. Returns a step-by-step plan for the parent to execute.\n" +
100
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" +
101
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" +
102
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" +
103
- "Async spawn (AGENT-LOOP.md §15): 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. Collect results with subagent_checkmultiple async children return in completion (arrival) order, first finished first, so fast results are handled immediately. Use async when your own turn must keep moving; use the default 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" +
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" +
104
72
  "Writing the prompt:\n" +
105
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" +
106
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" +
@@ -109,22 +77,81 @@ export const subagentTool = {
109
77
  parameters: {
110
78
  type: "object",
111
79
  properties: {
112
- task: { type: "string", description: "Self-contained task description for the sub-agent" },
113
- context: { type: "string", description: "Optional background the sub-agent needs (it cannot see this conversation)" },
114
- 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." },
115
- model: { type: "string", description: "Provider/model override for this sub-agent: 'provider:model', a provider name from config, or a model name on the parent's provider. Defaults to the agent.subagentModel config, then the parent's provider. Useful for offloading heavy work to a cheaper model." },
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." },
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)." },
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." },
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." },
84
+ context: { type: "string", description: "Optional background the sub-agent needs (it cannot see this conversation); action:'spawn' only." },
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)." },
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)." },
116
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." },
117
- 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." },
118
- async: { type: "boolean", description: "true = spawn without waiting — returns {id} immediately, fetch results later via subagent_check. Default 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." },
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)." },
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)." },
119
95
  },
120
- required: ["task"],
96
+ required: [],
121
97
  },
122
98
  readonly: false,
123
99
  sideEffectExempt: true, // child agent may write files; parent can't introspect its _mutatedThisRun
124
100
  parallel: true,
125
101
  async execute(args, ctx) {
102
+ // §19 action dispatch: default spawn keeps every legacy call unchanged
103
+ // (no action parameter → the spawn path below, byte-identical semantics).
104
+ const action = args?.action !== undefined && args?.action !== null && String(args.action) !== ""
105
+ ? String(args.action)
106
+ : "spawn"
107
+ if (action !== "spawn") {
108
+ // §19 restricted-variant action gate (round2 #3): the eng-coder audit
109
+ // channel (depth>0, role eng-coder) is spawn-only — escalate spawns a
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.
112
+ if ((ctx.depth ?? 0) > 0 && ctx.agent?._role === "eng-coder") {
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)`)
114
+ }
115
+ // §17 N3/D-S6 spawn gate (manual tier): auto-turn digests may not spawn —
116
+ // async OR blocking — the digest must stay organize-only. The escalate
117
+ // action spawns a write child too, so the same mechanical refusal applies
118
+ // (AUTO tier exempt — user authorized unattended continuation).
119
+ if (action === "escalate" && ctx.agent?._inAutoTurn && !ctx.agent?.autoApprove) {
120
+ return JSON.stringify({ status: "error", error: "cannot spawn subagents from a manual auto-turn — wait for user input" })
121
+ }
122
+ if (action === "status") return executeStatusAction(args, ctx)
123
+ if (action === "escalate") return await executeEscalateAction(args, ctx)
124
+ // §19.5 控制类动作:digest 内放行(D-S7 分类——控制/自省;dispatch 控制类
125
+ // 豁免同批生效——19.5.2b round2 #4;escalate 的 digest 拒绝在上一分支)
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)
131
+ // §19.6 panel 动作:view(readonly 面——digest 内放行——自省类)与 freeze
132
+ // (控制类——同 cancel——digest 内放行)。深度/门控检查在 executePanelAction 内。
133
+ if (action === "panel") return executePanelAction(args, ctx)
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.`)
139
+ }
140
+
126
141
  const parent = ctx.agent
127
142
  const role = args.role
143
+ // Spawn requires a task brief — schema `required` is advisory (multi-action
144
+ // schema), so the mechanical check lives here: an absent task would otherwise
145
+ // flow downstream as `content: undefined` and surface as an obscure error.
146
+ if (typeof args.task !== "string" || !args.task.trim()) {
147
+ throw new Error("subagent action:'spawn' requires a task (the self-contained task brief).")
148
+ }
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)
128
155
 
129
156
  // Role normalization + whitelist (2026-08-25, coder-leak fix): exact-string gates let
130
157
  // variant roles ("Coder", " coder") bypass BOTH mode gates and fall through to
@@ -134,6 +161,13 @@ export const subagentTool = {
134
161
  if (!ROLES.has(role)) {
135
162
  throw new Error(`Unknown subagent role: ${JSON.stringify(role)}. Valid roles: explore, plan, coder, eng-coder (exact spelling).`)
136
163
  }
164
+ // §18 D-E3 internal-spawn gate: an eng-coder sub-agent may only spawn sync
165
+ // explore (audit) children — non-explore roles and async are refused here
166
+ // (mechanical), the audit budget is enforced (7th audit spawn refused), and
167
+ // the returned attempt number marks this spawn as an audit for the task-book
168
+ // augmentation below. Runs BEFORE the mode gates so the eng-coder-specific
169
+ // error (not the generic engineering-mode one) surfaces.
170
+ const engAuditAttempt = gateEngCoderSpawn(ctx.agent, ctx.depth, role, args.async)
137
171
  // Role is mutually exclusive per mode: normal mode → "coder", engineering mode → "eng-coder"
138
172
  if (parent.config?.agent?.engineering && role === "coder") {
139
173
  throw new Error("Engineering mode: use role='eng-coder' for implementation tasks.")
@@ -142,105 +176,26 @@ export const subagentTool = {
142
176
  throw new Error("Engineering mode is not active — use role='coder' for implementation tasks.")
143
177
  }
144
178
 
145
- // Provider/model override: tool `model` arg > subagentModels[role] > subagentModel > parent provider
146
- const childProvider = resolveChildProvider(parent, effectiveSubagentModel(parent, role, args.model))
147
-
148
- // eng-coder token gate: the design review must have passed and the caller must
149
- // present the exact token advisor issued — otherwise the child is not authorized to code.
150
- // 2026-09-01: multi-design slots — the token is located by designId (exact slot,
151
- // single-slot fallthrough); HMAC/TTL validation itself is unchanged.
152
- let issuedToken
153
- if (role === "eng-coder") {
154
- issuedToken = resolveDesignSlot(parent, args.designId).token
155
- if (!issuedToken || args.designToken !== issuedToken || !validateDesignToken(args.designToken)) {
156
- throw new Error("Invalid or missing design token — run advisor with type='design' first and pass the returned token as designToken.")
157
- }
158
- }
159
-
160
- // Filter tool set by role: explore/plan are read-only (plan is a planning agent, its deliverable is the plan itself)
161
- let tools
162
- if (role === "explore" || role === "plan") {
163
- const allowed = readonlyToolNames(parent.tools)
164
- tools = parent.tools.filter((t) => allowed.has(t.name))
165
- } else {
166
- tools = parent.tools
167
- }
168
-
169
- // Select prompt overlay by role
170
- let overlay = ""
171
- if (role === "explore") overlay = EXPLORE_OVERLAY
172
- else if (role === "coder") overlay = CODER_OVERLAY
173
- else if (role === "plan") overlay = PLAN_OVERLAY
174
- else if (role === "eng-coder") overlay = ENG_CODER_OVERLAY
175
-
176
- // explore/plan: force read-only permission; coder/default: AUTO passes through directly,
177
- // manual mode queues permission requests for the parent agent's approval UI (human in the loop, child agent is no longer silently rejected)
178
- let childPermission
179
- if (role === "explore" || role === "plan") {
180
- childPermission = async () => false
181
- } else if (parent.autoApprove) {
182
- childPermission = async () => true
183
- } else {
184
- childPermission = async (name, toolArgs) => {
185
- if (!ctx.onPermissionRequest) return false
186
- const ask = () => ctx.onPermissionRequest(`${role ?? "sub"}/${name}`, toolArgs)
187
- // Queue parallel child agent permission requests to avoid two popups simultaneously overwriting each other (lesson from question tool)
188
- parent._permQueue = (parent._permQueue ?? Promise.resolve()).then(ask, ask)
189
- return parent._permQueue
190
- }
179
+ // §17 N3/D-S6 spawn gate (manual tier): auto-turn digests may not spawn async
180
+ // OR blocking the digest must stay organize-only. AUTO tier (autoApprove) is
181
+ // exempt (推进型 — user authorized unattended continuation). Mechanical refusal
182
+ // so the digest never pops a permission panel or chains new background work.
183
+ // (escalate 动作的同类拒绝在 action 分流处——本检查只管 spawn 路径。)
184
+ if (parent._inAutoTurn && !parent.autoApprove) {
185
+ return JSON.stringify({ status: "error", error: "cannot spawn subagents from a manual auto-turn wait for user input" })
191
186
  }
192
187
 
193
- // eng-coder: force engineering=true on child config so setup.mjs applies engineering prompt
194
- const childConfig = role === "eng-coder"
195
- ? { ...parent.config, agent: { ...parent.config.agent, engineering: true } }
196
- : parent.config
197
-
198
- const child = createAgent({
199
- provider: childProvider,
200
- tools,
201
- config: childConfig,
202
- cwd: parent.cwd,
203
- memory: parent.memory,
204
- overlay,
205
- role,
206
- })
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
207
193
 
208
- // Token-verified design review → child is authorized to modify files without re-reviewing
209
- if (role === "eng-coder") child._engDesignReviewed = true
210
- // designId+token ride the child bookkeeping: the delivery report carries the designId
211
- // so the divergence-audit fix round re-spawns with the SAME slot (2026-09-01 FR3).
212
- if (role === "eng-coder" && issuedToken) {
213
- child._engDesignId = args.designId ?? null
214
- child._engDesignToken = issuedToken
215
- }
216
-
217
- // explore/plan: inject git context (branch/recent commits/working tree state) — exploration and planning both relate to current repo state (inspired by kimi-code's promptPrefix)
218
- let input = args.context ? `Context:\n${args.context}\n\nTask:\n${args.task}` : args.task
219
- if (role === "explore" || role === "plan") {
220
- const gitCtx = collectGitContext(parent.cwd)
221
- if (gitCtx) input = `<untrusted_git_context>\n${escapeXml(gitCtx)}\n</untrusted_git_context>\n\n${input}`
222
- }
223
-
224
- // Relay content/reasoning/tool/output to the parent TUI via the unified spawn-child
225
- // pipeline (AGENT-LOOP.md §7.2 D3). Prefix includes a unique id: parallel child agents
226
- // with the same role stay independent and don't overwrite each other.
227
- // Format: role#id/ → onToken("coder#2/writing..."), onToolCall("coder#2/read", args)
228
- // Async id allocation (AGENT-LOOP.md §15 D-A1): reserve the relay counter at
229
- // spawn time — the returned id must be stable while the item sits in the queue.
230
- // The [model] token (TUI block creation) is DEFERRED to actual start so queued
231
- // children don't paint an empty panel block ("queued 态不显示").
232
- let relayPrefix
233
- if (args.async === true) {
234
- parent._subAgentCounter = (parent._subAgentCounter ?? 0) + 1
235
- relayPrefix = `${role}#${parent._subAgentCounter}/`
236
- } else {
237
- relayPrefix = makeRelay(parent, role ?? "sub", ctx.callbacks?.onToken, childProvider.model ?? "")
238
- }
239
- const childOpts = {
240
- onPermissionRequest: childPermission,
241
- ...wrapChildCallbacks(relayPrefix, ctx.callbacks),
242
- }
243
- 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
244
199
  // Turn-cap continue loop (TURN-CAP-CONTINUE.md) via runWithContinue (§7.2 D3):
245
200
  // hitting the cap asks the user via the SAME y/n panel the main agent uses —
246
201
  // unlimited continues, resume:true keeps the child's history + mutation bookkeeping,
@@ -255,198 +210,69 @@ export const subagentTool = {
255
210
  return parent._permQueue
256
211
  }
257
212
 
258
- // ── Async branch (AGENT-LOOP.md §15 D-A1/D-A6): spawn without waiting ──
259
- // The child runs the EXACT blocking pipeline (runChildPipeline below — relay /
260
- // turn-cap / permission / MIN_REPORT_CHARS / mergeChildMutations all unchanged),
261
- // but the parent does not await it: the promise is parked in _asyncSubagents and
262
- // consumed via subagent_check or the turn-end auto-wait. Slot queue: running
263
- // count < ASYNC_SUBAGENT_LIMIT → start now; ≥ limit → enqueue (status "queued",
264
- // position = queue index) — never rejected, never requiring the model to batch.
265
- if (args.async === true) {
266
- if ((ctx.depth ?? 0) > 0) {
267
- throw new Error("async spawn only available at the top level")
268
- }
269
- parent._asyncSubagents ??= new Map()
270
- parent._asyncQueue ??= []
271
- const running = [...parent._asyncSubagents.values()].filter((e) => e.status === "running").length
272
- const id = parent._subAgentCounter
273
- const entry = {
274
- id, role, relayPrefix,
275
- status: running >= ASYNC_SUBAGENT_LIMIT ? "queued" : "running",
276
- position: undefined,
277
- report: null, error: null, done: false,
278
- promise: null, _settle: null, _settleSeq: 0,
279
- }
280
- // The settle signal — resolves when the run chain settles (never rejects).
281
- entry.promise = new Promise((res) => { entry._settle = res })
282
- entry.start = () => {
283
- entry.status = "running"
284
- entry.position = undefined
285
- // Deferred [model] emit: the TUI block is created at ACTUAL start.
286
- ctx.callbacks?.onToken?.(relayPrefix + "[model]" + (childProvider.model ?? ""))
287
- // Turn-cap on background children NEVER pops the continue panel (D-A3):
288
- // auto-decline, the partial-work report carries the cap reason.
289
- runChildPipeline(child, input, childOpts, childRunOpts, {
290
- parent, role, args,
291
- askContinue: () => Promise.resolve(false),
292
- })
293
- .then((report) => { entry.report = report })
294
- .catch((err) => { entry.error = err?.message ?? String(err) })
295
- .finally(() => {
296
- entry.status = "done" // running 数口径(D-A1/D-A2/T6):已完成未消费不计入
297
- entry.done = true
298
- // D-A3 发射时机(2026-09-02 用户实证修正):settle 同刻发射 ⟦ev⟧done——
299
- // TUI routeSubToken 立即冻结区块,冻结位置 = 完成时刻的会话流位置
300
- // (回合收尾统一发会把块堆在结论之后)。父会话已 abort 不发:TUI 已按
301
- // interrupted 冻结,晚到 token 经 tombstone 丢弃——显式守卫更干净。
302
- if (!ctx.signal?.aborted) {
303
- ctx.callbacks?.onToken?.(`${entry.relayPrefix}⟦ev⟧done\x1e0\x1e0\x1edone\x1e`)
304
- }
305
- entry._settleSeq = (parent._asyncSettleSeq = (parent._asyncSettleSeq ?? 0) + 1)
306
- entry._settle()
307
- for (const w of parent._asyncWaiters?.splice(0) ?? []) { try { w() } catch { /* noop */ } }
308
- maybeRefillAsync(parent)
309
- })
310
- }
311
- parent._asyncSubagents.set(String(id), entry)
312
- if (entry.status === "queued") {
313
- parent._asyncQueue.push(entry)
314
- entry.position = parent._asyncQueue.length
315
- return JSON.stringify({ id: String(id), role, status: "queued", position: entry.position })
316
- }
317
- entry.start()
318
- return JSON.stringify({ id: String(id), role, status: "running" })
213
+ // ── Async branch(2026-09-05 module-split——executeAsyncSpawn verbatim
214
+ // subagent-run.mjs:条目构建/等位/启动/controller 链/turn 镜像/补位释放)──
215
+ if (wantAsync) {
216
+ return executeAsyncSpawn(parent, ctx, role, args, child, input, childOpts, childRunOpts, relayPrefix, built.childProvider, prep.files, prep.dependsOn)
319
217
  }
320
218
 
321
219
  // ── Blocking path (unchanged semantics): await the full pipeline ──
322
- return await runChildPipeline(child, input, childOpts, childRunOpts, {
323
- parent, role, args,
324
- askContinue: askSubagentContinue,
325
- })
220
+ // LOGGING(LOGGING.md):child:*(阻塞 spawn——runChildPipeline 前后;declined
221
+ // partial 由 TURN_CAP_MARK 检出;错误原样上抛(dispatch 转 tool:error))
222
+ const blockT0 = Date.now()
223
+ logEvent("child:spawn", { role, id: child._logId, kind: "blocking" })
224
+ try {
225
+ const pipelineReport = await runChildPipeline(child, input, childOpts, childRunOpts, {
226
+ parent, role, args,
227
+ askContinue: askSubagentContinue,
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")
234
+ logEvent("child:done", { role, id: child._logId, ms: Date.now() - blockT0, kind: String(pipelineReport).includes(TURN_CAP_MARK) ? "partial" : "ok" })
235
+ // §7.2.3 sync spawn 完成精确冻结(方案 e):execute 返回前 ctx 留子代理 key
236
+ // (relayPrefix 去尾 = `role#N`)——dispatch runOne 读它作 onToolResult 第 4 参 →
237
+ // TUI finishSubTaskKey 按 key 精确冻(async eng-coder 先启动时不再误冻其块——
238
+ // T-F2)。仅成功路径设置:async 分支不设(round2 #2——ack 带 status:running 由
239
+ // isAsyncSpawnResult 跳过冻结);错误/拒绝路径到此之前已 throw/return——ctx 未设
240
+ // ——错误路径不触发冻结(round1 #1——T-F5)。
241
+ ctx._subagentKey = relayPrefix.slice(0, -1)
242
+ return pipelineReport
243
+ } catch (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")
253
+ logEvent("child:error", { role, id: child._logId, ms: Date.now() - blockT0, err: errText(e, 200) })
254
+ throw e
255
+ }
326
256
  },
327
257
  }
328
258
 
329
- /**
330
- * Shared post-spawn pipeline (blocking AND async — AGENT-LOOP.md §15 D-A1: the
331
- * async branch reuses the exact same spawn-child pipeline, "全不变"):
332
- * turn-cap continue loop → declined partial-work return → MIN_REPORT_CHARS
333
- * expansion → eng-coder mutation merge → designId suffix. Returns the report.
334
- * onDeclined lives here (identical for both paths) — only askContinue differs:
335
- * blocking asks the user via the permission panel, async auto-declines.
336
- */
337
- async function runChildPipeline(child, input, childOpts, childRunOpts, { parent, role, args, askContinue }) {
338
- const declined = { partial: null }
339
- let report = await runWithContinue(
340
- (child, input, cbs, opts) => runAgent(child, input, cbs, opts), // opts = childRunOpts + resume (managed by the pipeline)
341
- child, input, childOpts, childRunOpts,
342
- {
343
- askContinue,
344
- onDeclined: (e, output) => {
345
- if (role === "eng-coder" && child._mutatedThisRun) mergeChildMutations(parent, child)
346
- // Early return semantics (unchanged from the inline loop): the declined
347
- // partial-work message is returned WITHOUT the MIN_REPORT_CHARS expansion —
348
- // re-prompting a capped child for a longer report is wrong.
349
- // Review #2 fix: use the pipeline-captured output (the `report` variable is
350
- // still "" at this point — runWithContinue hasn't returned yet).
351
- declined.partial = `Subagent (${role}) ${TURN_CAP_MARK} (${e.turn} turns) — work may be partial; review recent_changes before deciding next steps.\nPartial output: ${output || ""}`
352
- },
353
- },
354
- )
355
- if (declined.partial !== null) {
356
- // declined eng-coder delivery still carries its designId — the fix round
357
- // re-spawns with the same slot (2026-09-01).
358
- if (role === "eng-coder") declined.partial += `\ndesignId: ${args.designId ?? "(single-design session — designId optional)"} — reuse it (with the same designToken) when re-spawning this eng-coder.`
359
- return declined.partial
360
- }
361
-
362
- // Report too short = incomplete handoff: send back for expansion once (inspired by kimi-code's summaryPolicy: min 200 chars, retry 1 time).
363
- // The child agent's history is still intact; the continuation instruction is appended as new input so it can see its own earlier work.
364
- if (report.length < MIN_REPORT_CHARS) {
365
- report = await runAgent(child, REPORT_CONTINUATION, childOpts, childRunOpts)
366
- }
367
-
368
- // Engineering mode mechanical code gate: delegated file changes must not
369
- // bypass the parent's advisor/verify guards. Merge the child's mutations
370
- // into the parent so "advisor mandatory at both gates" is enforced, not just
371
- // promised in the engineering prompt.
372
- // CRITICAL: Only merge if child actually mutated files (defense-in-depth against
373
- // runAgent throwing before any writes occurred).
374
- // Review #8 clarification: eng-coder ONLY is intentional — the mechanical
375
- // two-gate merge exists for engineering mode; plain `coder` children carry
376
- // their own verify/advisor self-review discipline (per tool description), and
377
- // normal mode has no parent advisor/verify gate to feed.
378
- if (role === "eng-coder" && child._mutatedThisRun) {
379
- mergeChildMutations(parent, child)
380
- }
381
-
382
- // designId rides the delivery report (2026-09-01): the divergence-audit fix round
383
- // re-spawns with the SAME designId+token — the parent copies it from here, and the
384
- // prompt tells the model exactly where the matching token came from.
385
- if (role === "eng-coder") {
386
- report += `\ndesignId: ${args.designId ?? "(single-design session — designId optional)"} — reuse this designId with the same designToken (from the approved advisor type='design' review) when re-spawning this eng-coder for an audit fix round.`
387
- }
388
-
389
- return report
390
- }
391
-
392
- /** Slot-queue refill (AGENT-LOOP.md §15 D-A1/D-A6): start queue heads while a
393
- * running slot is free — called from every settle (completion frees a slot) and
394
- * from the turn-end collection's refill loop. Serial by construction: one slot
395
- * frees per settle, one head starts per call. */
396
- export function maybeRefillAsync(parent) {
397
- const queue = parent._asyncQueue ?? []
398
- while (queue.length > 0) {
399
- const running = [...(parent._asyncSubagents?.values() ?? [])].filter((e) => e.status === "running").length
400
- if (running >= ASYNC_SUBAGENT_LIMIT) return
401
- queue.shift().start()
402
- }
403
- }
404
-
405
- /**
406
- * Child agent run options — the parent's abort signal MUST propagate to the
407
- * child: without it, Ctrl+C aborts the parent's controller but the child keeps
408
- * running its full turn budget (up to subagentTurns) while the parent awaits —
409
- * the interrupt appears to do nothing.
410
- */
411
- export function buildChildRunOpts(ctx) {
412
- return {
413
- depth: (ctx.depth ?? 0) + 1,
414
- maxTurns: ctx.agent?.config?.agent?.subagentTurns ?? DEFAULT_SUBAGENT_TURNS,
415
- signal: ctx.signal ?? null,
416
- }
417
- }
418
-
419
- /**
420
- * Merge an eng-coder child's mutations into the parent agent's bookkeeping.
421
- * The parent must stay aware of delegated file changes: `_touchedFiles` enables
422
- * the advisor guard (completion.mjs) to detect that code was modified and
423
- * pushback for review. Prior verify/advisor state is invalidated because it
424
- * judged an older state.
425
- *
426
- * `_advisorRound` is NOT reset: merged code enters the CURRENT convergence
427
- * cycle. Resetting here would break the review→fix→re-review loop (the parent
428
- * reviews, spawns an eng-coder to fix, merges, reviews again — every merge
429
- * would restart at round 1 and the 5-round cap could never be reached).
430
- * `_calledAdvisorThisRun` IS cleared so the merged code triggers a fresh
431
- * advisor call (the guard demands review of new mutations).
432
- *
433
- * Returns true when mutations were merged (kept for future caller checks).
434
- */
435
- export function mergeChildMutations(parent, child) {
436
- // A child claiming mutations without any touched file is a misbehaving
437
- // child (or a bookkeeping bug) — do not propagate an empty mutation claim
438
- // to the parent's guard state.
439
- if (!child._mutatedThisRun || !(child._touchedFiles?.length)) return false
440
- parent._mutatedThisRun = true
441
- for (const abs of child._touchedFiles ?? []) {
442
- if (!parent._touchedFiles.includes(abs)) parent._touchedFiles.push(abs)
443
- }
444
- if (parent._calledAdvisorThisRun) parent._calledAdvisorThisRun = false
445
- if (parent._verifiedThisRun) {
446
- parent._verifiedThisRun = false
447
- parent._verifyPassed = undefined
448
- }
449
- // Stale session cleanup only — the round counter survives (see above).
450
- parent._advisorSession = null
451
- return true
452
- }
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 不再直接使用池常量)。
264
+ // 2026-09-05 拆分轮: maybeRefillAsync §20 调度器独立(./subagent-scheduler.mjs)——
265
+ // 再导出源改写,消费面(agent.mjs 动态 import 等)不变。
266
+ // 2026-09-06 §24 拆分轮: ASYNC_SUBAGENT_LIMIT 导出 → ASYNC_POOL_LIMITS(分域常量——
267
+ // 定义在 subagent-async.mjs——re-export 面同步)。
268
+ export {
269
+ ASYNC_POOL_LIMITS,
270
+ resolveChildProvider,
271
+ injectAsyncResult,
272
+ buildChildRunOpts,
273
+ mergeChildMutations,
274
+ } from "./subagent-async.mjs"
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"