thincoder 0.12.60 → 0.12.61

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/README.md +8 -6
  3. package/bin/thincoder.mjs +27 -122
  4. package/package.json +3 -2
  5. package/src/abort-provenance.mjs +116 -0
  6. package/src/acp/bridge.mjs +38 -17
  7. package/src/acp.mjs +6 -1
  8. package/src/advisor/citations.mjs +83 -21
  9. package/src/advisor/compaction.mjs +174 -0
  10. package/src/advisor/loop.mjs +293 -0
  11. package/src/advisor/messages.mjs +36 -134
  12. package/src/advisor/project-context.mjs +194 -0
  13. package/src/advisor/repos.mjs +17 -40
  14. package/src/advisor/run.mjs +124 -329
  15. package/src/advisor/truncate.mjs +57 -0
  16. package/src/advisor.mjs +3 -2
  17. package/src/agent/completion.mjs +1 -1
  18. package/src/agent/dispatch.mjs +47 -12
  19. package/src/agent/helpers.mjs +71 -13
  20. package/src/agent/record-results.mjs +13 -5
  21. package/src/agent/relay-prefix.mjs +39 -0
  22. package/src/agent/run-stages.mjs +21 -6
  23. package/src/agent/setup-reminders.mjs +16 -9
  24. package/src/agent/setup.mjs +92 -128
  25. package/src/agent/spawn-child.mjs +26 -9
  26. package/src/agent-tools/advisor-async.mjs +70 -180
  27. package/src/agent-tools/advisor-settle.mjs +231 -0
  28. package/src/agent-tools/advisor.mjs +69 -20
  29. package/src/agent-tools/batch-segment.mjs +195 -0
  30. package/src/agent-tools/consult.mjs +23 -10
  31. package/src/agent-tools/design-token.mjs +14 -1
  32. package/src/agent-tools/digest-budget.mjs +76 -0
  33. package/src/agent-tools/eng.mjs +3 -3
  34. package/src/agent-tools/escalate-async.mjs +16 -13
  35. package/src/agent-tools/read-history.mjs +13 -3
  36. package/src/agent-tools/review-streak.mjs +93 -0
  37. package/src/agent-tools/settings.mjs +130 -17
  38. package/src/agent-tools/subagent-actions.mjs +15 -6
  39. package/src/agent-tools/subagent-async.mjs +66 -14
  40. package/src/agent-tools/subagent-panel.mjs +22 -15
  41. package/src/agent-tools/subagent-run.mjs +9 -6
  42. package/src/agent-tools/subagent-scheduler.mjs +57 -8
  43. package/src/agent-tools/subagent-spawn.mjs +63 -16
  44. package/src/agent-tools/subagent.mjs +175 -49
  45. package/src/agent-tools/verify.mjs +13 -34
  46. package/src/agent-tools.mjs +1 -0
  47. package/src/agent.mjs +38 -21
  48. package/src/cli/distill-command.mjs +2 -2
  49. package/src/cli/make-agent.mjs +23 -7
  50. package/src/cli/memory-command.mjs +2 -2
  51. package/src/cli/setup-wizard.mjs +29 -9
  52. package/src/completions.mjs +114 -0
  53. package/src/config-migrate.mjs +70 -0
  54. package/src/config.mjs +132 -63
  55. package/src/conventions.mjs +223 -0
  56. package/src/crash-reports.mjs +7 -2
  57. package/src/expand-home.mjs +16 -0
  58. package/src/generate-title.mjs +1 -1
  59. package/src/hooks.mjs +7 -3
  60. package/src/memory/code-index.mjs +9 -3
  61. package/src/memory/code-sync.mjs +70 -31
  62. package/src/memory/delete.mjs +2 -0
  63. package/src/memory/docs.mjs +10 -6
  64. package/src/memory/file-walk.mjs +109 -0
  65. package/src/memory/schema.mjs +15 -3
  66. package/src/model-ref.mjs +66 -0
  67. package/src/model-specs.mjs +42 -8
  68. package/src/prompt-overlays.mjs +73 -16
  69. package/src/prompts/advisor-design.md +18 -8
  70. package/src/prompts/advisor-round1.md +7 -1
  71. package/src/prompts/advisor-round2.md +13 -2
  72. package/src/prompts/advisor-round3.md +13 -2
  73. package/src/prompts/common.md +115 -0
  74. package/src/prompts/consult-base.md +2 -0
  75. package/src/prompts/discipline-engineering.md +217 -0
  76. package/src/prompts/discipline-normal.md +179 -0
  77. package/src/prompts/persona-coder.md +21 -0
  78. package/src/prompts/persona-eng-coder.md +37 -0
  79. package/src/prompts/persona-eng-designer.md +55 -0
  80. package/src/prompts/persona-engineering.md +54 -0
  81. package/src/prompts/persona-explore.md +15 -0
  82. package/src/prompts/persona-normal.md +27 -0
  83. package/src/prompts/persona-plan.md +26 -0
  84. package/src/provider/anthropic.mjs +4 -4
  85. package/src/provider/core.mjs +13 -32
  86. package/src/provider/errors.mjs +26 -1
  87. package/src/provider/google.mjs +5 -6
  88. package/src/provider/index.mjs +2 -1
  89. package/src/provider/list-models.mjs +93 -0
  90. package/src/provider/rate.mjs +2 -1
  91. package/src/provider/responses.mjs +5 -3
  92. package/src/provider/sse.mjs +3 -4
  93. package/src/proxy.mjs +9 -14
  94. package/src/session-slots.mjs +7 -2
  95. package/src/session.mjs +59 -38
  96. package/src/token-ttl.mjs +2 -1
  97. package/src/tools/{system.mjs → bash.mjs} +6 -243
  98. package/src/tools/file.mjs +30 -10
  99. package/src/tools/git.md +1 -1
  100. package/src/tools/git.mjs +15 -34
  101. package/src/tools/index.mjs +4 -2
  102. package/src/tools/ops.mjs +20 -7
  103. package/src/tools/question.md +1 -0
  104. package/src/tools/question.mjs +26 -0
  105. package/src/tools/read.md +1 -1
  106. package/src/tools/read_image.md +1 -1
  107. package/src/tools/search.mjs +236 -0
  108. package/src/tui/agent-turn.mjs +32 -13
  109. package/src/tui/ansi.mjs +2 -0
  110. package/src/tui/clipboard.mjs +7 -1
  111. package/src/tui/cmd-advisor.mjs +3 -2
  112. package/src/tui/cmd-config.mjs +108 -37
  113. package/src/tui/cmd-eng.mjs +11 -27
  114. package/src/tui/cmd-exit.mjs +6 -8
  115. package/src/tui/cmd-model.mjs +14 -12
  116. package/src/tui/cmd-reindex.mjs +7 -0
  117. package/src/tui/cmd-submodel.mjs +8 -5
  118. package/src/tui/cmd-undo.mjs +4 -3
  119. package/src/tui/index.mjs +38 -37
  120. package/src/tui/key-handler.mjs +61 -17
  121. package/src/tui/key-modes.mjs +86 -8
  122. package/src/tui/layout.mjs +18 -10
  123. package/src/tui/model-catalog.mjs +89 -0
  124. package/src/tui/model-picker.mjs +498 -0
  125. package/src/tui/mouse.mjs +47 -10
  126. package/src/tui/pickers.mjs +28 -410
  127. package/src/tui/render-frame.mjs +32 -16
  128. package/src/tui/render-loop.mjs +2 -0
  129. package/src/tui/render-segments.mjs +5 -19
  130. package/src/tui/render.mjs +37 -5
  131. package/src/tui/slash-commands.mjs +2 -2
  132. package/src/tui/subagent-blocks.mjs +69 -90
  133. package/src/tui/subagent-children.mjs +50 -64
  134. package/src/tui/subagent-freeze.mjs +40 -43
  135. package/src/tui/subagent-panel.mjs +21 -66
  136. package/src/tui/suspension-drive.mjs +28 -82
  137. package/src/tui/tool-args.mjs +4 -2
  138. package/src/tui/tool-display.mjs +4 -3
  139. package/src/tui/tool-events.mjs +23 -16
  140. package/src/tui/tui-lifecycle.mjs +18 -5
  141. package/src/tui/wizard.mjs +52 -18
  142. package/src/tui/wrapped-spawn.mjs +38 -0
  143. package/src/prompts/coder.md +0 -13
  144. package/src/prompts/discipline.md +0 -84
  145. package/src/prompts/eng-coder.md +0 -19
  146. package/src/prompts/engineering-sub.md +0 -14
  147. package/src/prompts/engineering.md +0 -87
  148. package/src/prompts/explore.md +0 -12
  149. package/src/prompts/main.md +0 -34
  150. package/src/prompts/methodology-template.md +0 -38
  151. package/src/prompts/plan.md +0 -9
  152. package/src/prompts/system.md +0 -44
@@ -19,15 +19,77 @@
19
19
  * → ./subagent-run.mjs——execute 只保留动作分流 + 装配调用 + 阻塞路径。
20
20
  */
21
21
 
22
- import { gateEngCoderSpawn, TURN_CAP_MARK, emitNestedChildEvent } from "../agent/spawn-child.mjs"
22
+ import { gateEngCoderSpawn, TURN_CAP_MARK, STOPPED_MARK, emitNestedChildEvent } from "../agent/spawn-child.mjs"
23
23
  import { logEvent, errText } from "../log.mjs"
24
+ import { abortError, deathLine } from "../abort-provenance.mjs"
24
25
  import {
25
- runChildPipeline, executeCancelAction,
26
+ runChildPipeline, executeCancelAction, enqueueAsk, mergeChildMutations,
26
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"
27
32
  import { executeStatusAction, executeEscalateAction, executePanelAction, executeObserveAction, executeSendAction } from "./subagent-actions.mjs"
28
33
  import { prepareScheduling, buildSpawnChild, executeConsumeDesignAction } from "./subagent-spawn.mjs"
29
34
  import { executeAsyncSpawn } from "./subagent-run.mjs"
30
35
 
36
+ // ─── SYNC-CANCEL 纯函数(可测——无 io)──────────────────────────────────────────
37
+
38
+ /**
39
+ * SYNC-CANCEL F2 catch 三分支分类(可测纯函数——R3 收紧):
40
+ * ① "base" 整回合停:ctx.signal 或 baseSignal(= parent._sessionSignal ?? ctx.signal——
41
+ * buildChildSignal——挂起会话场景 base 命中而 ctx.signal 未 abort——R2)aborted →
42
+ * 现状保留(emitNestedChildEvent stopped + rethrow);
43
+ * ② "targeted" 定向中止:err 是 AbortError 且自属 ctrl aborted(且非整回合停)→
44
+ * 折叠 stopped partial 报告(父回合继续——merge/STOPPED_MARK/警示);
45
+ * ③ "error" 其他错误 → 现状保留(child:error + rethrow)。
46
+ * ⚠ 查 baseSignal 非仅 ctx.signal——挂起 digest 场景 child 链 _sessionSignal(R2——
47
+ * digest 自身 Ctrl+I/Ctrl+C 不误伤;会话 Stop 逐链中止必须归 ①)。
48
+ */
49
+ export function classifySyncAbort(ctxSignal, baseSignal, ctrlSignal, err) {
50
+ if (ctxSignal?.aborted || baseSignal?.aborted) return "base"
51
+ if (err?.name === "AbortError" && ctrlSignal?.aborted) return "targeted"
52
+ return "error"
53
+ }
54
+
55
+ /**
56
+ * SYNC-CANCEL F1/F5 中止控制器装配(可测):sync 阻塞 spawn 建**自属** AbortController
57
+ * (childRunOpts.signal 覆写为 ctrl.signal——照抄 async 分支 subagent-run.mjs 覆写模式)
58
+ * ——ctrl 链到基信号:baseSignal aborted → ctrl.abort();否则 addEventListener("abort",
59
+ * → ctrl.abort(), { once:true })——Ctrl+C/I 整回合停语义不变(base abort 逐链传播——
60
+ * AC2);嵌套 sync spawn 递归可中止(内层链外层 ctrl.signal——逐层自属——AC4)。
61
+ * 注册 `parent._syncChildAborts`(key = relayPrefix 去尾——{ ctrl, stopped:false }——
62
+ * TUI ⏹ 门控 live 判据 + cancelSyncChild 定向中止目标——与 async 条目 controller 存池
63
+ * 分层一致)。返回 { ctrl, disarm }——disarm 注销 registry(调用方 try/finally 三路径
64
+ * 共用——R7 防跨回合残留)。
65
+ */
66
+ export function armSyncChildAbort(parent, key, baseSignal) {
67
+ const ctrl = new AbortController()
68
+ if (baseSignal) {
69
+ // §20.3 站点 #10(第 24 批):hop 逐跳保 reason(下游可判定「谁杀的」)
70
+ if (baseSignal.aborted) ctrl.abort(baseSignal.reason)
71
+ else baseSignal.addEventListener("abort", () => ctrl.abort(baseSignal.reason), { once: true })
72
+ }
73
+ const registry = (parent._syncChildAborts ??= new Map())
74
+ registry.set(key, { ctrl, stopped: false })
75
+ const disarm = () => { registry.delete(key) }
76
+ return { ctrl, disarm }
77
+ }
78
+
79
+ /**
80
+ * SYNC-CANCEL ② 折叠报告构建(可测纯函数——仿 runChildPipeline onDeclined partial 形态,
81
+ * subagent-async.mjs onDeclined:STOPPED_MARK + partial 警示 + 捕获输出 + eng-coder
82
+ * designId 后缀——AC3)。capturedOutput = child._capturedOutput(spawn-child.mjs
83
+ * runWithContinue capture 累积——子代理已流式输出的剥哨兵文本)。
84
+ */
85
+ export function buildSyncStoppedReport(role, capturedOutput, designId) {
86
+ let report = `Subagent (${role}) ${STOPPED_MARK} — work may be partial; review recent_changes before deciding next steps.\nPartial output: ${capturedOutput || ""}`
87
+ if (role === "eng-coder") {
88
+ report += `\ndesignId: ${designId ?? "(single-design session — designId optional)"} — reuse it (with the same designToken) when re-spawning this eng-coder.`
89
+ }
90
+ return report
91
+ }
92
+
31
93
  /**
32
94
  * subagent tool — ONE tool, EIGHT actions (AGENT-LOOP.md §19/§19.5/§19.6/§19.8 +
33
95
  * SUBAGENT-OBSERVE-SEND): spawn (default) / status (non-blocking pool query) / observe
@@ -41,7 +103,8 @@ import { executeAsyncSpawn } from "./subagent-run.mjs"
41
103
  * - action:"spawn" roles: "explore" — read-only tools, search/read/analyze
42
104
  * (suitable for codebase exploration); "coder" — full tool set, self-contained
43
105
  * implementation tasks; "plan" — read-only planning; "eng-coder" —
44
- * engineering-mode implementation (design-token gated).
106
+ * engineering-mode implementation (design-token gated); "eng-designer" —
107
+ * engineering-mode design writing (requirements + design docs, batchDoc gated).
45
108
  * - no role specified — invalid by design since the 2026-08-25 fail-closed gate
46
109
  * (role is mandatory; "no role → same tool set as parent" was removed with the
47
110
  * coder-leak fix and the header text above predates it)
@@ -53,11 +116,11 @@ export const subagentTool = {
53
116
  description:
54
117
  "ONE tool, EIGHT actions (AGENT-LOOP.md §19/§19.5/§19.6/§19.8 + SUBAGENT-OBSERVE-SEND) — pick by what you need:\n" +
55
118
  "- action:'spawn' (DEFAULT): spawn a sub-agent to handle an independent subtask in an isolated context; the sub-agent returns only its final report. Spawn MULTIPLE subagents in the SAME response for parallel work—they run concurrently.\n" +
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" +
119
+ "- action:'status': NON-BLOCKING progress query — returns immediately and consumes nothing. Give the spawn's id for one child ({id, role, status: running|queued|done, model, elapsedSec, turn, maxTurns, position?), or omit it for an overview of the whole pool ({overview: {running: [{id, role, model, elapsedSec, turn, maxTurns}], queued: [{id, role, position}], done: [{id, role}]}}). §19.5.6 touched-files summary: running entries also carry touchedFiles (first 5, relative to your cwd), touchedMore (count beyond 5) and, when nothing was touched yet, the placeholder touched (\"—(尚无改动)\"); queued (not yet started) entries carry the placeholder touched (\"—(未启动)\") — see what a running child has changed BEFORE deciding to cancel it. Use THIS to see progress — it never blocks and never consumes a result (async results are delivered to you automatically). Background advisor reviews share this status surface (AGENT-LOOP.md §18): pass a review id and it answers in the same shape with role:'advisor' plus reviewType (design|code) / round / elapsedSec — and the overview lists reviews alongside subagents.\n" +
57
120
  "- action:'observe': SEE what a running async subagent is DOING right now (progress vs stuck) — pass the spawn id. Returns {id, role, status, turn, maxTurns, touched…, currentTool? — array of tool name(s) currently executing (read from its dispatch state; omitted when none in flight — present when stuck in a long tool call), recentTurns: [last N one-line turn summaries, newest-first; default 5, parameterizable via recent]}. Readonly — observable on running/queued/done: running shows live activity, queued (not started) returns a placeholder, done (settled, report auto-delivered) returns the activity summary only — NOT the full report (that rides the auto channel; observe stays terse to keep your context lean). Use it to judge whether a long-running child is stuck vs progressing BEFORE deciding to cancel or steer it.\n" +
58
121
  "- action:'send': STEER a running async subagent mid-flight — pass the spawn id + message (a direction like \"check X, don't fixate on Y\"). The message queues and the child consumes it at its next turn boundary as an ORDINARY user instruction (non-interrupting — its current tool finishes first; its convergence/audit discipline is unchanged — injection is guidance, not a deviation waiver). Returns {id, status:'delivered', queued}. Only a RUNNING async subagent is targetable — sync (you're waiting on it, no relay), queued (not started), settled or unknown ids error clearly. If the child settles before its next turn boundary, its settle report carries an 'undelivered' note so you don't assume the guidance landed.\n" +
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" +
122
+ "- action:'escalate' (飞刀 — a flown-in expert): hand an implementation task to a STRONGER model from your consult models (agent.consultModels). It gets WRITE access and does the work itself — reads, edits, runs tests — then returns a post-op report (what changed, why, verification). Use it when YOU judge the task calls for stronger hands (complex multi-file refactoring, an intractable bug, intricate algorithm work — or work beyond your comfortable ability); escalate EARLY, not after burning attempts. model: pick a candidate as 'provider:model' (default = the first consult model). Not available in engineering mode (implementation goes through eng-coder spawns there). DEFAULT-ASYNC at depth 0 (AGENT-LOOP.md §25 D-R17b): the launch returns an ack {id, role:'escalate', status:'running'} and the flight runs in the background (pooled with the other role-domain spawns) — its post-op report is delivered to you automatically with its mutations merged into your bookkeeping.\n" +
123
+ "- action:'cancel': STOP one background subagent — pass the id from the async spawn return (REQUIRED — omitting it errors; a blanket cancel is unsupported, Ctrl+C stops everything). Running target aborts immediately ({id, status:'cancelled'}); a queued target is removed from the queue ({id, status:'cancelled', was:'queued'} and later queue positions shift forward). Other children and the session keep running — cancellation is targeted. Use it when a background child is going the wrong way (e.g. burning turns) and you must stop it before its report arrives. Cancel is a last resort: verify alarming signals with reliable checks (git/node — not guesses) first; prefer scoped recovery (restore a single affected file) over killing the child — a running child's in-flight work dies with it, partial changes stay unmerged and unaudited. Background advisor reviews are cancelable on this same action (AGENT-LOOP.md §18): pass the review id — its controller aborts, no token is issued for a cancelled review.\n" +
61
124
  "- action:'consume-design' (engineering mode, parent side — chain-terminal token consumption): after the delivery is verified and the chain closes out, consume this design's token slot — pass the designId (optional for a single-design session). The slot is consumed; a further spawn for the same designId is mechanically rejected, and any new work (including deviation fixes) requires a fresh design review and token. Idempotent: an unknown designId / already-consumed slot is a no-op notice, never an error. Do NOT call it while the chain is still open — fix rounds reuse the same slot (same designId + designToken).\n" +
62
125
  "- action:'panel': DIAGNOSE + fix the subagent panel — the collapsible blocks under the conversation the user sees (CLI TUI panel mirror; headless/VS Code degrade to a 'no panel' pool view). view (default — call it with no params or view:true): returns the live panel blocks [{key, role, status: running|done|awaitingDigest} — running entries also carry elapsedSec; awaitingDigest entries whose report is ALREADY digested carry digested:true (stuck blocks — the freezable ones — explain odd panel states here)] exactly as the user sees them. freeze: pass the block key of a digested-stuck block ({action:'panel', freeze:'role#N'}) to reclaim it into the conversation — the freeze ONLY passes for awaitingDigest blocks with no live pool entry and no pending report (gated); freezing a block whose report is still pending would break the digestion order and is refused with a clear error.\n\n" +
63
126
  "Why delegate? A sub-agent runs in its own isolated context — its reads, searches, tool calls and edits never enter your history or pollute your window; only its final report comes back. Delegation keeps your working context lean (you see the whole session, not the child's noise) and the child single-mindedly focused on one task. Parallel children run concurrently, saving wall-clock time. Every coder/eng-coder child carries its own verify + advisor self-review discipline — handed-off work is already verified before you read a word of it.\n\n" +
@@ -65,9 +128,10 @@ export const subagentTool = {
65
128
  "- explore — read-only search & analysis. Toolset: the read/search family (grep, read, glob, code_search, doc_search, repo_outline, lsp, tree...). No git context injected—evidence from read/glob/grep and the task book. Its report must list what it searched and what it did NOT find. Fast — specify thoroughness in the task: quick / medium / thorough (default medium).\n" +
66
129
  "- plan — read-only implementation planning. Same read/search toolset; NEVER edits files. Returns a step-by-step plan for the parent to execute.\n" +
67
130
  "- coder — full implementation. The parent's complete read/write/execute toolset plus verify and advisor for self-review. Its final report must include a delivery transparency table with one row per task requirement (Done / Simplified / Not done — no deferred column).\n" +
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" +
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" +
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" +
131
+ "- eng-coder — engineering-mode coder (available only in engineering mode, replacing coder). Same full toolset as coder plus the design-driven methodology overlay; REQUIRES a valid designToken arg obtained from a passed advisor(type='design') review. The advisor's Approved reply also echoes a designId — pass it as the designId arg: required to pick between designs when several approved reviews are active, optional for a single design. The delivery report echoes the designId back for the audit fix round. ALSO REQUIRES a batchDoc arg — the batch record path (docs/batches/<batch>-<topic>.md), the batch §2 task book this spawn implements: a spawn without it, or with a path that does not resolve to a readable file, is mechanically refused.\n" +
132
+ "- eng-designer engineering-mode design writer (available only in engineering mode): the SOLE author of the requirements + design documents and of the batch record §2 (the batch task book) — revisions included. Writes no implementation code, does not edit prompt files, does not fire reviews, and needs NO designToken (its authorization is the confirmed requirements). It surveys on its own, but may only spawn read-only 'explore' children (sync, ≤6 per batch). ALSO REQUIRES a batchDoc arg — the batch record path (docs/batches/<batch>-<topic>.md); the same mechanical gate as eng-coder: a spawn without it, or with a path that does not resolve to a readable file, is mechanically refused.\n" +
133
+ "Mode filtering: normal mode exposes explore/plan/coder; engineering mode exposes explore/plan/eng-designer/eng-coder. The schema enum reflects the active mode.\n\n" +
134
+ "Async spawn (AGENT-LOOP.md §15/§18/§11.1): pass async:true to spawn WITHOUT waiting — returns {id, role, status:\"running\"} immediately so you can keep working in your own turn (read/check files, run other tools) while the child runs in the background. The child's report is delivered to you automatically — there is no fetch action; use action:'status' only to see progress, never to wait for the result. Top-level spawns are ALWAYS async — never pass `async:false` at depth-0 (the report arrives automatically; if your next step needs it, end the turn and let the digest deliver it). Inside subagents (depth>0) spawns are always synchronous (platform rule). Eng-coder's delivery protocol runs fully inside the child (implementation → audit → self-fix → advisor re-review → converged delivery). Async spawns are pooled per role domain (AGENT-LOOP.md §11.1): at most 4 concurrent eng-coders and 4 concurrent other-role spawns by default (agent.poolLimits overrides both) — a full domain queues further spawns with a position while the other domain keeps starting (domains never block each other), and top-level only. After an async spawn the turn winds down normally — nothing expects you to wait for it: the child runs in the background and its report is delivered to you automatically — before your next turn, or digested in the suspension session — so end the turn; do not poll or wait for the result.\n\n" +
71
135
  "Task scheduling (AGENT-LOOP.md §20): declare the scheduling metadata to let the SCHEDULER order your spawns — files: the file paths this task will modify, dependsOn: ids from prior async spawn returns whose outcome this task needs. Overlapping-file tasks are serialized and dependent tasks are started in order automatically: a spawn that would conflict, or whose dependencies have not settled, queues instead of running ({id, status:\"queued\", position, reason} — the waiting task auto-starts when the conflict clears / its dependency settles; cancel a queued task to drop it). A spawn whose dependency was cancelled or failed stays queued and marked \"dependency cancelled\" until you decide (cancel it) — in an AUTO session it starts by itself. Referencing an unknown id errors; an id already consumed (auto-delivered by the auto channel) counts as satisfied. Omit both parameters for the plain immediate spawn (no scheduler involvement).\n\n" +
72
136
  "Writing the prompt:\n" +
73
137
  "- The sub-agent starts with zero context — it has not seen this conversation. Brief it like a colleague who just walked into the room: state the goal, list what you already know, hand over the specifics.\n" +
@@ -82,11 +146,12 @@ export const subagentTool = {
82
146
  freeze: { type: "string", description: "action:'panel' only: block key of a digested-stuck awaitingDigest block (e.g. \"eng-coder#9\") to reclaim into the conversation via the gated done-freeze event. Refused when the block is running/done/unknown or its report is still pending digestion (would break the digestion order). Requires the CLI TUI panel mirror — headless/VS Code report the freeze unavailable." },
83
147
  task: { type: "string", description: "Required for action:'spawn' (the self-contained task brief) and action:'escalate' (goal, constraints, entry files, acceptance criteria). Not used by status." },
84
148
  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)." },
149
+ role: { type: "string", enum: ["explore", "plan", "coder", "eng-coder", "eng-designer"], description: "The sub-agent role — see the tool description for the role capability matrix. Exact spelling required. action:'spawn' only (escalate spawns its own expert internally)." },
86
150
  model: { type: "string", description: "action:'spawn': provider/model override for this sub-agent ('provider:model', a provider name, or a model name on the parent's provider — defaults to config.agent.subagentModels[role], then config.agent.subagentModel, then the parent's provider). pass \"default\" to explicitly inherit the default model — equivalent to omitting the parameter. action:'escalate': pick a consult candidate as 'provider:model' (default = the first consult model)." },
87
151
  designToken: { type: "string", description: "Required when role='eng-coder': the token returned by advisor(type='design') after the design review passed. Without a valid token, eng-coder cannot modify files." },
88
152
  designId: { type: "string", description: "Optional when role='eng-coder': the designId echoed with the approved token by advisor(type='design'). Required to pick between designs when several approved reviews are active in the session — each eng-coder carries its own designId+token pair so parallel implementations never overwrite each other. Optional for a single design. action:'consume-design': the design whose slot to close out — optional for a single-design session; required to pick when several approved designs are active (the consume gate refuses to guess)." },
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." },
153
+ batchDoc: { type: "string", description: "REQUIRED for role='eng-coder' and role='eng-designer': the batch record path (docs/batches/<batch>-<topic>.md) the batch §2 task book this spawn implements (or writes). The spawn is mechanically refused without it, and also when the path does not resolve (cwd-relative or absolute) to a readable file; the CONTENT is never validated (the batch record owns that). explore/plan/coder spawns ignore it." },
154
+ async: { type: "boolean", description: "action:'spawn': true = spawn without waiting — returns {id, status:\"running\"} immediately; the report is delivered to you automatically (there is no fetch action). Default: depth-0 → true (async — every role, AGENT-LOOP.md §18 D-E1a); depth>0 → sync (forced). action:'escalate': same semantics (AGENT-LOOP.md §25 D-R17b) — default async at depth 0; async:false keeps the legacy synchronous flight (mechanism parameter — see the Async spawn section for top-level guidance)." },
90
155
  files: { type: "array", items: { type: "string" }, description: "action:'spawn' only: the file write-domain this task declares (cwd-relative or absolute paths). files must be file-level paths (one per file you will modify). Directory declarations are NOT supported — they bypass the conflict detector and are rejected with an error. Tasks with overlapping files are serialized automatically — a conflicting spawn queues ({id, status:\"queued\", position, reason}) instead of running concurrently and starts when the conflict clears. Omit to skip conflict detection (plain immediate spawn)." },
91
156
  dependsOn: { type: "array", items: { type: "string" }, description: "action:'spawn' only: ids from prior async spawn returns whose outcome this task needs — the task queues ({id, status:\"queued\", position, reason}) until every dependency settles, then starts automatically. Ids already consumed (auto-delivered to the model) count as satisfied; a dependency cancelled or failed leaves the task queued marked 'dependency cancelled' until you decide (cancel it — AUTO sessions auto-start). Unknown ids error." },
92
157
  id: { type: "string", description: "action:'status'/'observe'/'send'/'cancel': the subagent id from the async spawn return. status: omit = overview of the whole pool; observe/send/cancel: REQUIRED (observe needs the child to inspect; send needs the target; never omit on cancel — a blanket cancel is unsupported)." },
@@ -105,12 +170,12 @@ export const subagentTool = {
105
170
  ? String(args.action)
106
171
  : "spawn"
107
172
  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)`)
173
+ // §19 restricted-variant action gate (round2 #3): the engineering-child channel
174
+ // (depth>0, role eng-coder or eng-designer) is spawn-only — escalate spawns a
175
+ // coder+WRITE child (violates explore-only intent) and status/panel/observe/send
176
+ // have no async pool / panel mirror to query in a child context.
177
+ if ((ctx.depth ?? 0) > 0 && (ctx.agent?._role === "eng-coder" || ctx.agent?._role === "eng-designer")) {
178
+ throw new Error(`only action:'spawn' (sync explore children) is available inside an ${ctx.agent._role} — escalate/status/cancel/panel/consume-design/observe/send are not (AGENT-LOOP.md §19 D-M3)`)
114
179
  }
115
180
  // §17 N3/D-S6 spawn gate (manual tier): auto-turn digests may not spawn —
116
181
  // async OR blocking — the digest must stay organize-only. The escalate
@@ -130,7 +195,10 @@ export const subagentTool = {
130
195
  if (action === "consume-design") return executeConsumeDesignAction(args, ctx)
131
196
  // §19.6 panel 动作:view(readonly 面——digest 内放行——自省类)与 freeze
132
197
  // (控制类——同 cancel——digest 内放行)。深度/门控检查在 executePanelAction 内。
133
- if (action === "panel") return executePanelAction(args, ctx)
198
+ // CLI-ACTIVITY-DEBLOAT F-3(2026-09-10)接线:executePanelAction ctx.state
199
+ // (= agent._tuiState——startTUI 反向挂载)读时现算面板块(computePanelBlocks)——
200
+ // 手工面板镜像已退役。headless/VSC 无挂载 → 现算返 null → 降级照旧。
201
+ if (action === "panel") return executePanelAction(args, { ...ctx, state: ctx.agent?._tuiState })
134
202
  // SUBAGENT-OBSERVE-SEND:observe = readonly 查询(同 status——digest/planMode 放行);
135
203
  // send = 控制类豁免(同 cancel——父回合内显式调用即授权)。深度门在各自执行器内。
136
204
  if (action === "observe") return executeObserveAction(args, ctx)
@@ -157,9 +225,9 @@ export const subagentTool = {
157
225
  // variant roles ("Coder", " coder") bypass BOTH mode gates and fall through to
158
226
  // full tools / no overlay — a full-write coder without design review. Schema enums are
159
227
  // advisory; providers don't enforce them. Fail closed on unknown roles.
160
- const ROLES = new Set(["explore", "plan", "coder", "eng-coder"])
228
+ const ROLES = new Set(["explore", "plan", "coder", "eng-coder", "eng-designer"])
161
229
  if (!ROLES.has(role)) {
162
- throw new Error(`Unknown subagent role: ${JSON.stringify(role)}. Valid roles: explore, plan, coder, eng-coder (exact spelling).`)
230
+ throw new Error(`Unknown subagent role: ${JSON.stringify(role)}. Valid roles: explore, plan, coder, eng-coder, eng-designer (exact spelling).`)
163
231
  }
164
232
  // §18 D-E3 internal-spawn gate: an eng-coder sub-agent may only spawn sync
165
233
  // explore (audit) children — non-explore roles and async are refused here
@@ -168,13 +236,19 @@ export const subagentTool = {
168
236
  // augmentation below. Runs BEFORE the mode gates so the eng-coder-specific
169
237
  // error (not the generic engineering-mode one) surfaces.
170
238
  const engAuditAttempt = gateEngCoderSpawn(ctx.agent, ctx.depth, role, args.async)
171
- // Role is mutually exclusive per mode: normal mode → "coder", engineering mode → "eng-coder"
239
+ // Role is mutually exclusive per mode: normal mode → "coder", engineering mode → "eng-coder"/"eng-designer"
172
240
  if (parent.config?.agent?.engineering && role === "coder") {
173
- throw new Error("Engineering mode: use role='eng-coder' for implementation tasks.")
241
+ throw new Error("Engineering mode: role='coder' is disabled — use role='eng-coder' for implementation tasks (or role='eng-designer' for design writing).")
174
242
  }
175
243
  if (!parent.config?.agent?.engineering && role === "eng-coder") {
176
244
  throw new Error("Engineering mode is not active — use role='coder' for implementation tasks.")
177
245
  }
246
+ // Third mode gate (ENGINEERING-MODE.md §2.15 A—— symmetric completion):
247
+ // eng-designer is engineering-mode-only, same family as eng-coder (both carry
248
+ // the engineering discipline overlay + the batchDoc gate).
249
+ if (!parent.config?.agent?.engineering && role === "eng-designer") {
250
+ throw new Error("Engineering mode is not active — role='eng-designer' is engineering-mode only (it writes the requirements/design documents inside the engineering workflow); use role='explore' or role='plan' for read-only work.")
251
+ }
178
252
 
179
253
  // §17 N3/D-S6 spawn gate (manual tier): auto-turn digests may not spawn — async
180
254
  // OR blocking — the digest must stay organize-only. AUTO tier (autoApprove) is
@@ -205,9 +279,25 @@ export const subagentTool = {
205
279
  // turns them into Error tool results — unchanged behavior).
206
280
  const askSubagentContinue = (e) => {
207
281
  if (!ctx.onPermissionRequest) return Promise.resolve(false)
208
- const ask = () => ctx.onPermissionRequest("continue", { turns: e.turn, agent: relayPrefix.slice(0, -1) })
209
- parent._permQueue = (parent._permQueue ?? Promise.resolve()).then(ask, ask)
210
- return parent._permQueue
282
+ const key = relayPrefix.slice(0, -1)
283
+ const ask = async () => {
284
+ // SYNC-CANCEL v2(模态 deny——用户裁):⏹ 后 entry.stopped——不再弹模态。
285
+ // ⚠ 不能直接 resolve(false) 走 onDeclined 降级(TURN_CAP partial——child 已撞
286
+ // cap——runWithContinue 的 decline 是正常 return——永远到不了 abort 检出点——
287
+ // stopped 折叠语义丢失:无 ⟦ev⟧stopped/无 STOPPED_MARK——块冻结标 done 而非
288
+ // stopped——评审 🟡#2)。stopped 分支改抛 AbortError——runWithContinue 只捕
289
+ // ContinueError——原样上抛 → 阻塞 catch 三分支②折叠("child 随即在 abort 检出点
290
+ // 解绕折叠"——AGENT-LOOP §7.2 机制文)。abort 恒已在途(stopped 只由
291
+ // cancelSyncChild 与 ctrl.abort 同时置位)——信号语义真实。
292
+ if (parent._syncChildAborts?.get(key)?.stopped) throw abortError(ctrl.signal, "settle", "sync-stopped")
293
+ const go = await ctx.onPermissionRequest("continue", { turns: e.turn, agent: key })
294
+ // ⏹ deny(denyModalForOwner resolve(false))与用户按 n 同形——旗标区分:
295
+ // stopped → 同上抛(折叠——abort 先于 deny 已在途);普通 n → false 走 decline
296
+ // (现状——cap partial 报告)。
297
+ if (parent._syncChildAborts?.get(key)?.stopped) throw abortError(ctrl.signal, "settle", "sync-stopped")
298
+ return go
299
+ }
300
+ return enqueueAsk(parent, "_permQueue", ask)
211
301
  }
212
302
 
213
303
  // ── Async branch(2026-09-05 module-split——executeAsyncSpawn verbatim 迁
@@ -216,43 +306,79 @@ export const subagentTool = {
216
306
  return executeAsyncSpawn(parent, ctx, role, args, child, input, childOpts, childRunOpts, relayPrefix, built.childProvider, prep.files, prep.dependsOn)
217
307
  }
218
308
 
219
- // ── Blocking path (unchanged semantics): await the full pipeline ──
220
- // LOGGINGLOGGING.md):child:*(阻塞 spawn——runChildPipeline 前后;declined
221
- // partial TURN_CAP_MARK 检出;错误原样上抛(dispatch tool:error))
309
+ // ── Blocking path (unchanged semantics + SYNC-CANCEL targeted stop) ──
310
+ // SYNC-CANCEL F1/F52026-09-09):自属 AbortController(armSyncChildAbort——
311
+ // childRunOpts.signal 覆写 ctrl.signal——照抄 async 分支 subagent-run.mjs 的
312
+ // 覆写模式——buildChildRunOpts 不改——escalate/consult 零触碰)——⏹ 定向中止
313
+ // (cancelSyncChild → ctrl.abort)与整回合停(base abort 逐链传播)解耦;registry
314
+ // 注册/注销(try/finally 三路径——R7 防跨回合残留)。LOGGING(LOGGING.md):
315
+ // child:*(阻塞 spawn——runChildPipeline 前后;declined partial 由 TURN_CAP_MARK
316
+ // 检出;⏹ 折叠由 STOPPED_MARK 检出——kind partial;错误原样上抛(dispatch 转
317
+ // tool:error))
222
318
  const blockT0 = Date.now()
223
319
  logEvent("child:spawn", { role, id: child._logId, kind: "blocking" })
320
+ const syncKey = relayPrefix.slice(0, -1)
321
+ // baseSignal 一次性快照(spawn 时刻)——catch 分类复用同一信号对象(会话收尾把
322
+ // _sessionSignal 置 null 的窗口内重读会漂移——快照防误判)
323
+ const baseSignal = buildChildSignal(parent, ctx)
324
+ const { ctrl, disarm } = armSyncChildAbort(parent, syncKey, baseSignal)
325
+ let pipelineReport
224
326
  try {
225
- const pipelineReport = await runChildPipeline(child, input, childOpts, childRunOpts, {
327
+ pipelineReport = await runChildPipeline(child, input, childOpts, { ...childRunOpts, signal: ctrl.signal }, {
226
328
  parent, role, args,
227
329
  askContinue: askSubagentContinue,
228
330
  })
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
331
  } catch (e) {
244
- if (ctx.signal?.aborted || e?.name === "AbortError") {
332
+ // SYNC-CANCEL F2 三分支(classifySyncAbort 纯函数):
333
+ const cls = classifySyncAbort(ctx.signal, baseSignal, ctrl.signal, e)
334
+ if (cls === "base") {
335
+ // ① 整回合停(现状逐字保留——挂起场景 base 命中而 ctx.signal 未 abort——R2):
245
336
  // §27 R23:外层 abort 传播的中断——内层开块随之外层冻结前先收尾定格
246
337
  // (D-R23c1 stopped——T-R23c.2a 生成侧路径;TUI 冻结兜底仍在 freezeSubTaskLines)
247
338
  emitNestedChildEvent(ctx, relayPrefix, "stopped")
248
339
  throw e // 用户停——不落错误事件
249
340
  }
250
- // §27 R23 error-run 映射(实现批补一行):run 错误(非 abort)→ 同样发 stopped
251
- // ——内层子块定格不悬空(T-R23a.3——工具错/运行错误路径)。
341
+ if (cls === "error") {
342
+ // ③ 其他错误(现状逐字保留——:249-253):
343
+ // §27 R23 error-run 映射(实现批补一行):run 错误(非 abort)→ 同样发 stopped
344
+ // ——内层子块定格不悬空(T-R23a.3——工具错/运行错误路径)。
345
+ emitNestedChildEvent(ctx, relayPrefix, "stopped")
346
+ logEvent("child:error", { role, id: child._logId, ms: Date.now() - blockT0, err: errText(deathLine(e, ctrl?.signal), 200) })
347
+ throw e
348
+ }
349
+ // ② targeted 折叠(err AbortError && 自属 ctrl aborted && 非整回合停):merge +
350
+ // stopped partial 报告(父回合继续拿报告——AC1/AC3)。merge 镜像 escalate sync
351
+ // runner 先例(subagent-actions.mjs runner 包装层——guard 在 mergeChildMutations
352
+ // 内——见子代理已写文件才传播)。
353
+ if (role === "eng-coder" && child._mutatedThisRun) mergeChildMutations(parent, child)
354
+ pipelineReport = buildSyncStoppedReport(role, child._capturedOutput ?? "", args?.designId)
355
+ // 块冻结标 stopped(R6——非 done):⏹ 定向中止的 TUI 顶层块立即定格 stopped
356
+ // (async settle cancelled 分支同款直发——async-settle.mjs settleAsyncEntry);
357
+ // 嵌套(eng-coder 内 explore 审计)经 emitNestedChildEvent 定格子块——stopped
358
+ // 幂等无害(重复/迟到 done 由 §27.1 F2 done 子块定格丢弃兜底)。
359
+ ctx.callbacks?.onToken?.(`${relayPrefix}⟦ev⟧stopped\x1e0\x1e0\x1estopped\x1e`)
252
360
  emitNestedChildEvent(ctx, relayPrefix, "stopped")
253
- logEvent("child:error", { role, id: child._logId, ms: Date.now() - blockT0, err: errText(e, 200) })
254
- throw e
361
+ } finally {
362
+ // R7 防跨回合残留:成功/折叠②/整回合停①/错误③ 四出口统一注销(设计"三路径"
363
+ // 口径 = 成功/折叠/整回合停——错误③ 同样 rethrow 经 finally——同归本注销)
364
+ disarm()
255
365
  }
366
+ // §27 R23 D-R23c1(评审 #1 🅰——生成侧补发射):sync spawn 同步收尾——若本 spawn
367
+ // 处于嵌套上下文(ctx.callbacks 已是嵌套 wrapper——eng-coder 内 explore 审计)→
368
+ // 发内层 ⟦ev⟧done(完整嵌套前缀——wrapper 链自动补外层)→ 主 TUI 路由子块定格
369
+ // (T-R23c.1)。非嵌套(depth-0)零变化——冻结仍由 dispatch subKey 精确冻承接。
370
+ // ② 折叠 = 正常 return(本共用出口:done 补发照设 + ctx._subagentKey 照设——成功
371
+ // 冻结管线复用——迟到 done 对已定格 stopped 块被丢弃——幂等无害)。
372
+ emitNestedChildEvent(ctx, relayPrefix, "done")
373
+ logEvent("child:done", { role, id: child._logId, ms: Date.now() - blockT0, kind: String(pipelineReport).includes(TURN_CAP_MARK) || String(pipelineReport).includes(STOPPED_MARK) ? "partial" : "ok" })
374
+ // §7.2.3 sync spawn 完成精确冻结(方案 e):execute 返回前 ctx 留子代理 key
375
+ // (relayPrefix 去尾 = `role#N`)——dispatch runOne 读它作 onToolResult 第 4 参 →
376
+ // TUI finishSubTaskKey 按 key 精确冻(async eng-coder 先启动时不再误冻其块——
377
+ // T-F2)。仅成功/折叠路径设置:async 分支不设(round2 #2——ack 带 status:running 由
378
+ // isAsyncSpawnResult 跳过冻结);base/error 路径到此之前已 throw——ctx 未设
379
+ // ——中止/错误路径不触发冻结(round1 #1——T-F5)。
380
+ ctx._subagentKey = syncKey
381
+ return pipelineReport
256
382
  },
257
383
  }
258
384
 
@@ -263,7 +389,7 @@ export const subagentTool = {
263
389
  // (maybeRefillAsync——execute 不再直接使用池常量)。
264
390
  // 2026-09-05 拆分轮: maybeRefillAsync 随 §20 调度器独立(./subagent-scheduler.mjs)——
265
391
  // 再导出源改写,消费面(agent.mjs 动态 import 等)不变。
266
- // 2026-09-06 §24 拆分轮: ASYNC_SUBAGENT_LIMIT 导出 → ASYNC_POOL_LIMITS(分域常量——
392
+ // 2026-09-06 §11.1 拆分轮: ASYNC_SUBAGENT_LIMIT 导出 → ASYNC_POOL_LIMITS(分域常量——
267
393
  // 定义在 subagent-async.mjs——re-export 面同步)。
268
394
  export {
269
395
  ASYNC_POOL_LIMITS,
@@ -11,10 +11,10 @@
11
11
  * path (doc-only changes return early) — the task list and self-review checklist.
12
12
  */
13
13
 
14
- import { isDocFile } from "../advisor/repos.mjs"
14
+ import { isCodePath, isDocPath, loadConventions } from "../conventions.mjs"
15
15
  import { execSync, spawnSync } from "node:child_process"
16
16
  import { existsSync } from "node:fs"
17
- import { dirname, join, resolve } from "node:path"
17
+ import { resolve } from "node:path"
18
18
 
19
19
  /**
20
20
  * §18.12 D-VR1 path normalization — mirrors the §20.5 file-domain handling:
@@ -31,32 +31,6 @@ function changedFileKey(p) {
31
31
  return process.platform === "win32" ? p.toLowerCase() : p
32
32
  }
33
33
 
34
- /**
35
- * Nearest ancestor of an absolute path holding package.json or .git — the
36
- * project/repo root. Walk stops at the filesystem root; returns null when no anchor exists.
37
- */
38
- function findProjectRoot(absPath) {
39
- let dir = dirname(resolve(absPath))
40
- for (;;) {
41
- if (existsSync(join(dir, "package.json")) || existsSync(join(dir, ".git"))) return dir
42
- const parent = dirname(dir)
43
- if (parent === dir) return null
44
- dir = parent
45
- }
46
- }
47
-
48
- /**
49
- * True when the path is under <projectRoot>/src/ (anchored via findProjectRoot —
50
- * a parent-path /src/ segment outside the project must not classify a file as
51
- * product code). Falls back to the loose /src/ pattern when no anchor exists.
52
- */
53
- function isUnderSrc(absPath) {
54
- const root = findProjectRoot(absPath)
55
- if (!root) return /(?:^|[\\/])src[\\/]/.test(absPath)
56
- const norm = normalizeChangedPath(absPath, root)
57
- return norm.startsWith(normalizeChangedPath("src", root) + "/")
58
- }
59
-
60
34
  /**
61
35
  * D-V3 block guidance — a block message must not just say "not verified": it lists
62
36
  * the changed code files and points to the project's AGENTS.md natural-language
@@ -180,19 +154,24 @@ export const verifyTool = {
180
154
  // 1b. Doc-only fast path: every changed file is documentation (docs/, *.md,
181
155
  // LICENSE…) — syntax checks and a verification declaration are meaningless
182
156
  // for doc changes, and the task list/self-review checklist add nothing either.
183
- // src/** (incl. prompts/*.md) is product code excluded from the fast path,
184
- // consistent with isProductCode. Empty list (no changes / git unavailable)
157
+ // Paths inside a declared code segment (default: src incl. prompts/*.md) are
158
+ // product code excluded from the fast path, consistent with the design gate.
159
+ // The project's own layout is declarable (.thincoder/conventions.json).
160
+ // Empty list (no changes / git unavailable)
185
161
  // intentionally falls through to the normal path below.
186
- if (changedFiles.length > 0 && changedFiles.every((f) => !isUnderSrc(f) && isDocFile(f))) {
162
+ const conv = loadConventions(cwd)
163
+ if (changedFiles.length > 0 && changedFiles.every((f) => isDocPath(f, conv))) {
187
164
  lines.push("")
188
165
  lines.push("Documentation-only changes — skipping syntax checks and tests.")
189
166
  ctx.agent._verifyPassed = true
190
167
  return lines.join("\n")
191
168
  }
192
169
 
193
- // Code files needing verification: anything under src/ (incl. src/prompts/*.md)
194
- // or any non-doc file (D-V1 — these require a verification declaration).
195
- const codeFiles = changedFiles.filter((f) => isUnderSrc(f) || !isDocFile(f))
170
+ // Code files needing verification: anything the shared classifier calls product
171
+ // code (D-V1 — these require a verification declaration). Temp scratch files are
172
+ // neither code nor docs and fall out of this list (a throwaway diagnostic script
173
+ // is not a code change — no declaration is demanded for it).
174
+ const codeFiles = changedFiles.filter((f) => isCodePath(f, conv))
196
175
 
197
176
  // 2. Advisory syntax hint (D-V5) — node --check on changed .js/.mjs when node
198
177
  // exists. SOFT hint only — it does NOT gate done (no language-specific
@@ -14,3 +14,4 @@ export { timerTool } from "./agent-tools/timer.mjs"
14
14
  export { advisorTool } from "./agent-tools/advisor.mjs"
15
15
  export { engTool } from "./agent-tools/eng.mjs"
16
16
  export { readHistoryTool } from "./agent-tools/read-history.mjs"
17
+ export { batchSegmentTool } from "./agent-tools/batch-segment.mjs"
package/src/agent.mjs CHANGED
@@ -3,39 +3,43 @@
3
3
  * LLM ↔ tool-call loop, until the task is done.
4
4
  */
5
5
  import { chat } from "./provider/index.mjs"
6
+ import { abortError, annotateAbort } from "./abort-provenance.mjs"
6
7
  import { pushReal, summarizeRunExplorations } from "./context.mjs"
7
8
  import { specForModel } from "./config.mjs"
8
- import { readFileSync } from "node:fs"
9
- import { join, dirname, resolve } from "node:path"
10
- import { fileURLToPath } from "node:url"
9
+ import { resolve } from "node:path"
11
10
  import { executeToolCalls } from "./agent/dispatch.mjs"
12
11
  import { recordToolResults } from "./agent/record-results.mjs"
13
12
  import { FILE_MUTATORS } from "./agent/helpers.mjs"
14
13
  import { prepareRun } from "./agent/setup.mjs"
15
14
  import { injectPostTurn } from "./agent/post-turn.mjs"
16
15
  import { handleCompletion } from "./agent/completion.mjs"
16
+ // PROMPT-SYSTEM 施工② G1(2026-09-10):六件槽位常量装载收口 prompt-overlays.mjs
17
+ // (与子代理角色常量同源——单一权威锚 D1);旧三件文件读取随本批退役。
18
+ // 槽位常量通过下方 re-export 面(mod: rel — re-export 保 import 面);本文件自身零直接消费。
17
19
  // 主循环阶段函数(压缩检查/注入组/回合收尾)2026-09-05 实践轮迁 agent/run-stages.mjs
18
20
  import { runCompactionCheck, injectTurnReminders, finalizeAgentTurn, injectResponseReminders } from "./agent/run-stages.mjs"
19
21
  import {
20
22
  escapeXml, repairHistory, listWorkDir,
21
23
  readonlyToolNames, collectGitContext, loadProjectInstructions,
22
24
  ContinueError,
25
+ turnFrame, // 第 19 批(TURN-ACROSS-SEGMENTS):跨段累计编号帧(设计 §19.3)
23
26
  DEFAULT_MAX_TURNS, DEFAULT_SUBAGENT_TURNS,
24
27
  MIN_REPORT_CHARS, REPORT_CONTINUATION,
25
28
  AUTO_TURN_DIGEST_DOMAIN,
26
29
  } from "./agent/helpers.mjs"
27
30
  // ENG 提醒族 + auto-turn domain 2026-09-05 迁 agent/helpers.mjs(agent.mjs 530 > 500 硬限)
28
- // overlay 载荷(explore/coder/plan/eng-coder/consult)迁 prompt-overlays.mjs——re-export 保面
31
+ // PROMPT-SYSTEM 施工② G1(2026-09-10):六件槽位常量装载收口 prompt-overlays.mjs
32
+ // (与子代理角色常量同源——单一权威锚 D1);本文件 re-export 保 import 面。
29
33
  export {
30
- EXPLORE_OVERLAY, CODER_OVERLAY, PLAN_OVERLAY, ENG_CODER_OVERLAY, CONSULT_BASE,
34
+ PERSONA_ENGINEERING, PERSONA_NORMAL, COMMON,
35
+ DISCIPLINE_ENGINEERING, DISCIPLINE_NORMAL,
36
+ CONSULT_BASE,
31
37
  } from "./prompt-overlays.mjs"
32
38
  export { ENG_ON_REMINDER, ENG_OFF_REMINDER } from "./agent/helpers.mjs"
33
39
 
34
- // Prompt files (byte-stable, loaded once)
35
- const __dirname = dirname(fileURLToPath(import.meta.url))
36
- const SYSTEM_PROMPT = readFileSync(join(__dirname, "prompts", "system.md"), "utf8")
37
- const DISCIPLINE_RULES = readFileSync(join(__dirname, "prompts", "discipline.md"), "utf8")
38
- const MAIN_OVERLAY = readFileSync(join(__dirname, "prompts", "main.md"), "utf8")
40
+ // Role persona slot mapping note(施工② G3): 子代理人格由 assemblePrompt 场景表按
41
+ // role 承载(persona-explore/coder/plan/eng-coder.md——D1 表 = 蓝图 §3.2 1:1);
42
+ // spawn overlay 概念已退役(prompt-overlays.mjs 不再导出 OVERLAY 别名常量)。
39
43
 
40
44
  // exported for consumption by agent-tools.mjs
41
45
  export {
@@ -66,8 +70,8 @@ export function createAgent({
66
70
  // (AC3 零写) — no field initializer; the multi-slot Map `_engDesignTokens` is the
67
71
  // authoritative ledger (hydrated by restoreEngTokens / written by settle).
68
72
  _touchedFiles: [], _verifyRetries: 0, _advisorRound: 0, _advisorSession: null,
69
- _advisorRuns: new Map(), // §24 D-24b: per-review convergence instances (rounds/prior/designId)
70
- _mutationSeq: 0, _mutLog: [], // §24 D-24b: mutation log (in-flight review staleness scan)
73
+ _advisorRuns: new Map(), // §11.2 D-24b: per-review convergence instances (rounds/prior/designId)
74
+ _mutationSeq: 0, _mutLog: [], // §11.2 D-24b: mutation log (in-flight review staleness scan)
71
75
  _lastAdvisorOutput: null, // full review output from the most recent advisor call (convergence rounds inject it verbatim)
72
76
  _lastEngState: false,
73
77
  _pendingReminders: [],
@@ -109,8 +113,10 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
109
113
  }
110
114
  agent._inAutoTurn = autoTurn // spawn gate for manual-tier digests (§17 D-S6/N3)
111
115
  const { maxTurns, threshold, tools, toolSchemas, toolByName, systemPrompt } = await prepareRun(
116
+ // G1/G2(施工②):prompt 装配收口 prepareRun 内部(assemblePrompt——prompt-overlays.mjs
117
+ // 槽位常量,与子代理角色常量同源——单一权威锚 D1);本调用不再携带 prompt 常量。
112
118
  agent, input, callbacks,
113
- { depth, signal, overrideTurns, resume: resume || autoTurn, systemPrompt: SYSTEM_PROMPT, disciplineRules: DISCIPLINE_RULES, mainOverlay: MAIN_OVERLAY },
119
+ { depth, signal, overrideTurns, resume: resume || autoTurn },
114
120
  )
115
121
 
116
122
  // Exploration-distillation boundary (CONTEXT-COMPACTION §5): prepareRun already
@@ -120,6 +126,10 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
120
126
  // Per-run bookkeeping reset — PRESERVED on `resume` (ContinueError continuation):
121
127
  // mutation/guard continuity and the convergence budget must survive a continuation.
122
128
  if (!resume) {
129
+ // 第 19 批(TURN-ACROSS-SEGMENTS——设计 TURN-CAP-CONTINUE.md §19.3):链内累计编号
130
+ // 只在链起点复位——续跑(resume:true)不重置、不回退(编号帧公式见 helpers.mjs
131
+ // turnFrame)。与下方 mutation/guard 复位同条件同点(全档唯一复位点)。
132
+ agent._turnSeq = 0
123
133
  // §17 D-S6: an auto-turn's guard marks are inherited by the next USER run (not
124
134
  // reset) so auto-turn changes never escape the guard silently.
125
135
  const g = agent._inheritedGuard
@@ -172,14 +182,20 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
172
182
  let thrownError = null
173
183
  try {
174
184
  for (let turn = 0; turn < maxTurns; turn++) {
175
- // Update turn counter for status bar display
176
- agent._currentTurn = turn + 1
177
- agent._maxTurns = maxTurns
185
+ // 19 批(TURN-ACROSS-SEGMENTS——设计 TURN-CAP-CONTINUE.md §19.3):编号帧——
186
+ // `_turnSeq` 每轮 +1(跨段累计,仅 `!resume` 链起点复位);面向消费面的两字段
187
+ // (状态行 + ⟦ev⟧turn / ⟦ev⟧approval 事件共用)在此同点赋值(编号唯一权威;帧
188
+ // 公式 = helpers.mjs turnFrame)。段内帽判定不读帧(下行循环条件只读段内
189
+ // turn / maxTurns——N6)。
190
+ const frame = turnFrame(++agent._turnSeq, turn, maxTurns)
191
+ agent._currentTurn = frame.turn
192
+ agent._maxTurns = frame.maxTurns
178
193
  // D2 (AGENT-LOOP.md §7.2): depth>0 children emit a ⟦ev⟧turn progress token each turn —
179
194
  // single emit point covering all three spawn tools; phase=llm (tool/done progress rides
180
- // the onToolCall/onToolResult relay — no token for those).
195
+ // the onToolCall/onToolResult relay — no token for those). 第 19 批:载荷取上方帧值
196
+ // (agent._currentTurn / _maxTurns——字段形态 / 字段数 / phase 零变化,N5)。
181
197
  if (depth > 0 && callbacks.onToken) {
182
- callbacks.onToken(`⟦ev⟧turn\x1e${turn + 1}\x1e${maxTurns}\x1ellm\x1e`)
198
+ callbacks.onToken(`⟦ev⟧turn\x1e${agent._currentTurn}\x1e${agent._maxTurns}\x1ellm\x1e`)
183
199
  }
184
200
 
185
201
  // SUBAGENT-OBSERVE-SEND D2: 子代理回合边界消费点——每轮开头把父侧经 subagent
@@ -286,7 +302,8 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
286
302
  role: "user",
287
303
  content: `[User interrupt: ${response.interruptMessage}]`,
288
304
  })
289
- throw Object.assign(new Error("User interrupted"), { name: "AbortError" })
305
+ // §20.3 站点 #8(第 24 批):错误对象已自带 name/message——只补来源标注(缺 abortInfo 才补)
306
+ throw annotateAbort(Object.assign(new Error("User interrupted"), { name: "AbortError" }), signal, "agent", "interrupted-response")
290
307
  }
291
308
 
292
309
  if (response.usage) {
@@ -318,7 +335,7 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
318
335
  }
319
336
 
320
337
  // abort after chat completes, before committing history: don't commit a half-finished turn
321
- if (signal?.aborted) throw new DOMException("Aborted", "AbortError")
338
+ if (signal?.aborted) throw abortError(signal, "agent", "post-chat")
322
339
 
323
340
  pushReal(agent, {
324
341
  role: "assistant",
@@ -391,6 +408,6 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
391
408
  // 2026-09-05 实践轮:回合收尾(consult 清理/async 池分流/guard 继承——原 425-462
392
409
  // 段 + collectSettledAsync 466-494 整体迁 agent/run-stages.mjs finalizeAgentTurn——
393
410
  // CLI 对位 VS run-stages——finally 只剩一行调用 + 骨架注释)。
394
- await finalizeAgentTurn(agent, { signal, autoTurn, suspDriven, thrownError })
411
+ await finalizeAgentTurn(agent, { signal, autoTurn, suspDriven, thrownError, depth })
395
412
  }
396
413
  }