@thincoder/core 0.9.4 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +1 -1
  3. package/abort-provenance.mjs +10 -10
  4. package/advisor/citations.mjs +5 -5
  5. package/advisor/compaction.mjs +16 -15
  6. package/advisor/loop.mjs +14 -14
  7. package/advisor/messages.mjs +6 -6
  8. package/advisor/project-context.mjs +5 -5
  9. package/advisor/repos.mjs +3 -3
  10. package/advisor/run.mjs +23 -13
  11. package/advisor.mjs +4 -4
  12. package/agent/assemble.mjs +111 -0
  13. package/agent/chat-call.mjs +68 -0
  14. package/agent/child-marks.mjs +2 -2
  15. package/agent/completion.mjs +9 -2
  16. package/agent/continue-decision.mjs +25 -0
  17. package/agent/dispatch-gates.mjs +140 -0
  18. package/agent/dispatch-run.mjs +167 -0
  19. package/agent/dispatch.mjs +55 -294
  20. package/agent/family-tools.mjs +11 -11
  21. package/agent/helpers.mjs +63 -3
  22. package/agent/live-beat.mjs +49 -0
  23. package/agent/post-turn.mjs +4 -8
  24. package/agent/record-results.mjs +23 -20
  25. package/agent/relay-prefix.mjs +2 -2
  26. package/agent/run-stages.mjs +19 -15
  27. package/agent/run-start.mjs +111 -0
  28. package/agent/setup-reminders.mjs +9 -9
  29. package/agent/setup.mjs +32 -4
  30. package/agent/spawn-child.mjs +9 -9
  31. package/agent/suspension.mjs +90 -30
  32. package/agent/timers.mjs +113 -0
  33. package/agent/turn-loop.mjs +244 -0
  34. package/agent/write-gate.mjs +103 -6
  35. package/agent-tools/advisor-async.mjs +11 -11
  36. package/agent-tools/advisor-settle.mjs +17 -17
  37. package/agent-tools/advisor.mjs +38 -43
  38. package/agent-tools/async-discard.mjs +44 -1
  39. package/agent-tools/async-settle.mjs +23 -20
  40. package/agent-tools/audit-block.mjs +7 -7
  41. package/agent-tools/bash-async.mjs +281 -0
  42. package/agent-tools/batch-lifecycle.mjs +65 -34
  43. package/agent-tools/batch-paths.mjs +186 -0
  44. package/agent-tools/batch-skeleton.mjs +19 -1
  45. package/agent-tools/batch.mjs +22 -119
  46. package/agent-tools/checkpoint.mjs +121 -0
  47. package/agent-tools/child-permission.mjs +5 -3
  48. package/agent-tools/consult.mjs +58 -58
  49. package/agent-tools/context.mjs +4 -20
  50. package/agent-tools/design-token.mjs +3 -3
  51. package/agent-tools/digest-budget.mjs +1 -1
  52. package/agent-tools/eng.mjs +5 -5
  53. package/agent-tools/escalate-async.mjs +18 -13
  54. package/agent-tools/goal.mjs +3 -10
  55. package/agent-tools/panel-blocks.mjs +3 -3
  56. package/agent-tools/parent-channel.mjs +3 -13
  57. package/agent-tools/plan.mjs +3 -4
  58. package/agent-tools/read-history.mjs +12 -28
  59. package/agent-tools/recent-changes.mjs +4 -5
  60. package/agent-tools/review-facts.mjs +1 -1
  61. package/agent-tools/settings.mjs +47 -20
  62. package/agent-tools/skill.mjs +3 -5
  63. package/agent-tools/spawn-gates.mjs +3 -3
  64. package/agent-tools/subagent-actions-query.mjs +270 -0
  65. package/agent-tools/subagent-actions.mjs +29 -273
  66. package/agent-tools/subagent-async.mjs +36 -31
  67. package/agent-tools/subagent-panel.mjs +38 -9
  68. package/agent-tools/subagent-run.mjs +44 -29
  69. package/agent-tools/subagent-scheduler.mjs +29 -28
  70. package/agent-tools/subagent-spawn.mjs +57 -25
  71. package/agent-tools/subagent.mjs +46 -66
  72. package/agent-tools/task.mjs +3 -9
  73. package/agent-tools/timer.mjs +12 -12
  74. package/agent-tools/verify.mjs +14 -13
  75. package/agent-tools.mjs +0 -1
  76. package/agent.mjs +20 -351
  77. package/attachments.mjs +125 -0
  78. package/auto-think.mjs +1 -1
  79. package/config-presets.mjs +12 -7
  80. package/config-watch.mjs +83 -0
  81. package/config.mjs +17 -4
  82. package/context-degrade.mjs +81 -0
  83. package/context-echo.mjs +88 -0
  84. package/context-push.mjs +51 -0
  85. package/context.mjs +8 -187
  86. package/conventions.mjs +110 -161
  87. package/declaration.mjs +194 -0
  88. package/embedding.mjs +3 -1
  89. package/env-ref.mjs +81 -0
  90. package/explore-distill.mjs +2 -3
  91. package/file-links.mjs +53 -0
  92. package/file-refs.mjs +103 -0
  93. package/generate-title.mjs +28 -4
  94. package/git/checkpoint.mjs +4 -8
  95. package/history-window.mjs +17 -2
  96. package/i18n.mjs +2 -0
  97. package/index-discover.mjs +7 -7
  98. package/ledger-cmd.mjs +34 -126
  99. package/ledger-db.mjs +35 -7
  100. package/ledger-migrate.mjs +299 -0
  101. package/ledger-tools.mjs +188 -0
  102. package/ledger.mjs +11 -5
  103. package/log.mjs +12 -7
  104. package/manifest-discovery.mjs +131 -0
  105. package/manifest-schema.mjs +166 -0
  106. package/manifest.mjs +18 -217
  107. package/mcp.mjs +4 -1
  108. package/memory/code-search.mjs +149 -0
  109. package/memory/code-sync.mjs +89 -230
  110. package/memory/core.mjs +47 -32
  111. package/memory/docs.mjs +63 -262
  112. package/memory/file-list.mjs +89 -0
  113. package/memory/file-walk.mjs +7 -4
  114. package/memory/memory-tool.mjs +229 -0
  115. package/memory/schema.mjs +18 -3
  116. package/memory/sweep.mjs +289 -0
  117. package/memory/sync-tail.mjs +81 -0
  118. package/memory-status.mjs +69 -0
  119. package/memory.mjs +3 -0
  120. package/model-specs.mjs +131 -66
  121. package/notify-policy.mjs +51 -0
  122. package/package.json +1 -1
  123. package/peer-claims.mjs +263 -0
  124. package/peer-domains.mjs +100 -71
  125. package/peer-instances.mjs +9 -10
  126. package/process-probe-exec.mjs +187 -0
  127. package/process-probe.mjs +27 -175
  128. package/prompt-overlays.mjs +11 -10
  129. package/prompts/advisor-design.md +2 -2
  130. package/prompts/advisor-round2.md +1 -1
  131. package/prompts/advisor-round3.md +1 -1
  132. package/prompts/common.md +19 -7
  133. package/prompts/discipline-engineering.md +57 -16
  134. package/prompts/discipline-normal.md +6 -3
  135. package/prompts/persona-coder.md +0 -1
  136. package/prompts/persona-eng-coder.md +1 -5
  137. package/prompts/persona-eng-designer.md +1 -2
  138. package/prompts/persona-engineering.md +41 -11
  139. package/prompts/persona-explore.md +0 -1
  140. package/prompts/persona-normal.md +10 -1
  141. package/prompts/persona-plan.md +3 -4
  142. package/prompts/subagent-base.md +14 -0
  143. package/provider/core.mjs +11 -48
  144. package/provider/list-models.mjs +3 -0
  145. package/provider/responses-request.mjs +237 -0
  146. package/provider/responses.mjs +6 -228
  147. package/provider-flows.mjs +248 -0
  148. package/queued.mjs +90 -0
  149. package/rules.mjs +209 -2
  150. package/session-gc.mjs +67 -6
  151. package/session-guard.mjs +27 -7
  152. package/session-lifecycle.mjs +90 -18
  153. package/session-rename.mjs +5 -1
  154. package/session-slot-claims.mjs +230 -0
  155. package/session-slot-scan.mjs +299 -0
  156. package/session-slot-verify.mjs +304 -0
  157. package/session-slot-write.mjs +74 -8
  158. package/session-slots-manifest.mjs +163 -236
  159. package/session-slots.mjs +99 -71
  160. package/session.mjs +30 -10
  161. package/shell-candidates.mjs +78 -0
  162. package/text-budget.mjs +2 -2
  163. package/think-off.mjs +49 -0
  164. package/token-ttl.mjs +15 -4
  165. package/tool-args.mjs +73 -0
  166. package/tool-docs/advisor.md +9 -0
  167. package/tool-docs/batch.md +1 -0
  168. package/tool-docs/code_search.md +1 -0
  169. package/tool-docs/consult_start.md +5 -0
  170. package/tool-docs/consult_stop.md +3 -0
  171. package/tool-docs/context.md +7 -0
  172. package/tool-docs/doc_search.md +1 -0
  173. package/tool-docs/edit.md +2 -13
  174. package/tool-docs/eng.md +1 -0
  175. package/tool-docs/execute.md +1 -9
  176. package/tool-docs/git.md +25 -51
  177. package/tool-docs/goal.md +6 -0
  178. package/tool-docs/ledger_add.md +1 -0
  179. package/tool-docs/ledger_close.md +1 -0
  180. package/tool-docs/ledger_count.md +1 -0
  181. package/tool-docs/ledger_query.md +1 -0
  182. package/tool-docs/ledger_update.md +1 -0
  183. package/tool-docs/main_history.md +1 -0
  184. package/tool-docs/memory.md +10 -0
  185. package/tool-docs/notify_parent.md +9 -0
  186. package/tool-docs/peer_instances.md +1 -0
  187. package/tool-docs/plan.md +5 -0
  188. package/tool-docs/process.md +7 -3
  189. package/tool-docs/question.md +1 -0
  190. package/tool-docs/read_history.md +7 -0
  191. package/tool-docs/recent_changes.md +1 -0
  192. package/tool-docs/repo_outline.md +1 -0
  193. package/tool-docs/settings.md +6 -0
  194. package/tool-docs/skill.md +5 -0
  195. package/tool-docs/subagent.md +31 -0
  196. package/tool-docs/task.md +7 -0
  197. package/tool-docs/timer.md +6 -0
  198. package/tool-docs/verify.md +1 -0
  199. package/tool-docs/wait_for.md +2 -1
  200. package/tools/bash.mjs +28 -4
  201. package/tools/edit-batch.mjs +10 -11
  202. package/tools/edit-diff.mjs +33 -25
  203. package/tools/exec-run.mjs +102 -4
  204. package/tools/execute.mjs +5 -2
  205. package/tools/file.mjs +11 -12
  206. package/tools/git-run.mjs +1 -1
  207. package/tools/git.mjs +0 -24
  208. package/tools/glob-dialect.mjs +3 -3
  209. package/tools/index.mjs +2 -1
  210. package/tools/linter.mjs +2 -1
  211. package/tools/node-child.mjs +21 -0
  212. package/tools/ops.mjs +50 -13
  213. package/tools/patch.mjs +4 -2
  214. package/tools/repomap.mjs +20 -9
  215. package/tools/search.mjs +2 -2
  216. package/tools/shared.mjs +9 -7
  217. package/tools/web.mjs +5 -2
  218. package/tools/write-path.mjs +1 -1
  219. package/traces/trace-store.mjs +17 -14
  220. package/vision-reader.mjs +76 -0
  221. package/agent-tools/batch-segment.mjs +0 -18
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * subagent-spawn.mjs — spawn 路径装配(2026-09-05 module-split:subagent.mjs 726
3
3
  * > 500 硬限——spawn 前置 helpers(effectiveSubagentModel/resolveDesignSlot——审计块
4
- * 构造器 2026-09-22 起另立 ./audit-block.mjs)+ §20 调度参数准入(prepareScheduling)+ child 装配
4
+ * 构造器 2026-09-22 起另立 ./audit-block.mjs)+ AGENT-LOOP-SUBAGENT.md §6.9 调度参数准入(prepareScheduling)+ child 装配
5
5
  * (buildSpawnChild)verbatim 迁入(仅闭包变量参数化),语义零变;executeAsyncSpawn
6
6
  * 另在 subagent-run.mjs。subagent.mjs execute 经本文件 import 调用。
7
7
  * 2026-09-07:executeConsumeDesignAction 消费执行器(token 链终消费制——与 spawn 侧
@@ -11,14 +11,13 @@
11
11
  * (`child._spawnSystemBlock` 单点绑定——消费点 = 核 `prepareRun`),`input` 只留任务书。
12
12
  */
13
13
 
14
- import { resolve } from "node:path"
15
- import { existsSync, statSync } from "node:fs"
14
+ import { basename, isAbsolute, relative } from "node:path"
16
15
  import {
17
16
  createAgent,
18
- readonlyToolNames, escapeXml,
17
+ readonlyToolNames, escapeXml, excludeSubagentTools,
19
18
  } from "../agent.mjs"
20
19
  import { allocRelay, wrapChildCallbacks, relayPrefixOf } from "../agent/spawn-child.mjs"
21
- // TUI-OOM-ROOTCAUSE(AGENT-LOOP.md §23.3.1):子代理人读线窗口常量单源(store 零依赖)。
20
+ // TUI-OOM-ROOTCAUSE(AGENT-LOOP.md §6.15):子代理人读线窗口常量单源(store 零依赖)。
22
21
  import { RECORD_WINDOW_MESSAGES } from "../session-store.mjs"
23
22
  import { validateDesignToken } from "./advisor.mjs"
24
23
  import { tokenExpired, removeDesignTokenSlot, reconcileEngTokensFromSlot, persistEngTokens } from "../token-ttl.mjs"
@@ -33,6 +32,37 @@ import { validateTaskBookFields } from "./spawn-gates.mjs"
33
32
  // 台账 #23(AGENT-LOOP-SUBAGENT.md §6.26 · D23-3):审计块构造器外提(`summarizeEngTaskBook`
34
33
  // + `buildAuditBlock` 纯函数)——spawn 档只做「收集 + 绑定」;块文本逐字搬移(D23-4)。
35
34
  import { buildAuditBlock } from "./audit-block.mjs"
35
+ // 批次档路径解析单源(BATCH-RECORD.md §4.15 · 台账 #287):门内解析改经叶档(读面非抛形),
36
+ // 与 append / create 同源——门判据仍「参数在 + 路径可读」,错误文案逐字零变。
37
+ import { resolveBatchReadPath, batchDocBases } from "./batch-paths.mjs"
38
+ // LOGGING(LOGGING.md):#309 形三观测留痕走同一事件通道。
39
+ import { logEvent } from "../log.mjs"
40
+
41
+ /** #309 任务书 token 判据(`AGENT-LOOP-SUBAGENT.md` §6.29.3):`.md` 结尾的连续路径型字面
42
+ * (含盘符形态——裸名 `AGENTS.md` 无分隔符且非绝对,见调用点过滤)。 */
43
+ const MD_PATH_TOKEN = /[A-Za-z0-9_@.:\\/-]+\.md\b/g
44
+
45
+ /** #309 形三·task↔batchDoc 观测留痕(§6.29.3——**不阻断**,spawn 照常放行):任务书文本提及
46
+ * **他批存在档** ⇒ `logEvent` 留痕一条(键面 = 两侧基名,零任务书内容);只提绑定档 / 无路径型
47
+ * `.md` 字面 ⇒ 零事件。token 逐条经 `resolveBatchReadPath` 解析(路径单源),命中 = 可读 ∧
48
+ * 落基底内 ∧ ≠ 绑定档(同一档多 token 按解析后绝对路径去重)。 */
49
+ function logBatchDocRefs(parent, role, task, boundAbs) {
50
+ const cwd = parent?.cwd ?? process.cwd()
51
+ const text = typeof task === "string" ? task : ""
52
+ const key = (p) => (process.platform === "win32" ? p.toLowerCase() : p)
53
+ const bases = batchDocBases(cwd) // 歧义锚 ⇒ 候选项目基底并集(#828——入基底判据覆盖面随之对齐)
54
+ const seen = new Set()
55
+ for (const m of text.matchAll(MD_PATH_TOKEN)) {
56
+ const token = m[0]
57
+ if (!/[/\\]/.test(token) && !isAbsolute(token)) continue // 裸名(AGENTS.md 形态)不取
58
+ const abs = resolveBatchReadPath(cwd, token)
59
+ if (!abs || key(abs) === key(boundAbs ?? "")) continue // 不可读 / 就是绑定档 ⇒ 零事件
60
+ const inBase = bases.some((b) => { const r = relative(b, abs); return r && !r.startsWith("..") && !isAbsolute(r) })
61
+ if (!inBase || seen.has(key(abs))) continue // 他 .md 引用(设计档 / 需求档等)/ 同档重复
62
+ seen.add(key(abs))
63
+ logEvent("child:batchdoc-ref", { role, batchDocBase: basename(boundAbs ?? ""), refBase: basename(abs) })
64
+ }
65
+ }
36
66
 
37
67
  /**
38
68
  * Effective subagent model override for a role (CLI parity shared with VS Code):
@@ -75,7 +105,7 @@ export function resolveDesignSlot(parent, designIdArg) {
75
105
  const rec = reconcileEngTokensFromSlot(parent)
76
106
  if (rec && rec !== slots) slots = rec
77
107
  }
78
- // F2d (§29.1): both refusal branches carry the HELD id list — after a
108
+ // F2d: both refusal branches carry the HELD id list — after a
79
109
  // persistence restore the parent has no digest to look the id up in; the error
80
110
  // list is the only discovery path (ids are not credentials — the token is).
81
111
  const size = slots instanceof Map ? slots.size : 0
@@ -144,14 +174,14 @@ export function executeConsumeDesignAction(args, ctx) {
144
174
  return `design slot consumed — designId ${designId ?? "(single-design session)"} is closed out; a further eng-coder spawn for this design is mechanically rejected, and any new work (including deviation fixes) requires a fresh advisor(type='design') review and token.`
145
175
  }
146
176
 
147
- // ── §20 spawn 调度参数准入(AGENT-LOOP.md §20 D-SD1/D-SD3 + 20.4 round2 #5/#7)──
177
+ // ── spawn 调度参数准入(AGENT-LOOP-SUBAGENT.md §6.9 D-SD1/D-SD3)────────────
148
178
  // files/dependsOn 声明即契约(v1:不做任务书文本自动解析——不可靠)。缺省(两者皆
149
179
  // 缺)= 既有语义零改动(不参与冲突检测/无校验——legacy spawn 零开销直通)。
150
180
  // 校验序:参数形态 → 依赖 unknown id(非 consumed 墓碑——T-SD10)→ 依赖环可达
151
181
  // (T-SD5——防御断言:自然流程不可达)→ 等待态判定。判定结果:wait/depc 阻塞 →
152
182
  // async 入 queued 等位(spawn 返回带 reason——D-SD3b);**sync spawn(async:false)
153
183
  // 命中阻塞 → 明确错误——不队列化 sync——sync 语义零变更(round2 #7——T-SD13)**。
154
- /** §20 准入(参数化提取——2026-09-05 module-split):返回归一化 { files, dependsOn }。
184
+ /** 准入(参数化提取——2026-09-05 module-split):返回归一化 { files, dependsOn }。
155
185
  * filesRaw 目录声明 fail-closed(检测器 throw → 错误即工具结果 JSON)。 */
156
186
  export function prepareScheduling(parent, filesRaw, dependsRaw, wantAsync) {
157
187
  const files = []
@@ -191,7 +221,7 @@ export function prepareScheduling(parent, filesRaw, dependsRaw, wantAsync) {
191
221
  }
192
222
 
193
223
  /**
194
- * §20 准入通过后的 child 装配(2026-09-05 module-split——自 execute 参数化提取,
224
+ * AGENT-LOOP-SUBAGENT.md §6.9 准入通过后的 child 装配(2026-09-05 module-split——自 execute 参数化提取,
195
225
  * 原 318-488 段 verbatim——语义零变)。副作用保留:relay 取号(sync 支 allocRelay——
196
226
  * `[model]` 出生声明归 SYNC-CANCEL 单点,#133)/ 子代理 _logId / _engTaskInput 携带 /
197
227
  * _spawnSystemBlock 绑定(台账 #23——spawn 固块单点)全部在此发生。返回阻塞/异步两路径共用的
@@ -206,20 +236,19 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
206
236
  // 批次档路径(需求 §1.11 铁律 #5 随件传递的机械面)。落点 = 本装配点(token 门之前):
207
237
  // sync 与 async 两条 spawn 路径都经过 buildSpawnChild——一处校验双路生效,错误出口与
208
238
  // token 门一致(§2.13.1 选型 A)。
209
- // 判据只到"参数在 + 路径可读"——**不校验内容/措辞**(不做"已收口"正则、不匹配模板、
210
- // 不生成;内容够不够由执行者拒收兜底——需求 §1.14 #9 行为面)。路径语义照
211
- // `files` 先例:cwd 相对或绝对均可,`\` 归一为 `/`。错误文案带**实际角色名**(§2.15
212
- // 越界文案参数化——designer 撞门时不误导)。
239
+ // 路径语义照 `files` 先例:cwd 相对或绝对均可,`\` 归一为 `/`;解析序单源 = §4.15(cwd → 项目根 →
240
+ // 逐基底,读面取首个可读)——cwd 非项目根时根相对串不再必拒(BR-29/BR-33)。错误文案带
241
+ // **实际角色名**(§2.15 越界文案参数化——designer 撞门时不误导)与逐字不可读后缀。
213
242
  let batchDocAbs = null
214
243
  if (role === "eng-coder" || role === "eng-designer") {
215
244
  const given = typeof args.batchDoc === "string" ? args.batchDoc.trim() : ""
216
245
  const refusal = (suffix = "") => new Error(
217
246
  `batchDoc is required for role='${role}' — pass the batch record path (docs/batches/<batch>-<topic>.md); spawn refused without it.` + suffix)
218
247
  if (!given) throw refusal()
219
- batchDocAbs = resolve(parent.cwd ?? process.cwd(), given.replace(/\\/g, "/"))
220
- let readable = false
221
- try { readable = existsSync(batchDocAbs) && statSync(batchDocAbs).isFile() } catch { readable = false }
222
- if (!readable) throw refusal(" (given path is not a readable file)")
248
+ batchDocAbs = resolveBatchReadPath(parent.cwd ?? process.cwd(), given)
249
+ if (!batchDocAbs) throw refusal(" (given path is not a readable file)")
250
+ // #309 形三:任务书中的他批批次档引用观测留痕(不阻断——放行不受影响)。
251
+ logBatchDocRefs(parent, role, args.task, batchDocAbs)
223
252
  }
224
253
 
225
254
  // M5 F2(ENGINEERING-MODE-V2-MODULE-DELEGATION §2.2):任务书六强制字段门——
@@ -258,6 +287,9 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
258
287
  } else {
259
288
  tools = parent.tools
260
289
  }
290
+ // Then the depth>0 exclusion (TOOLS.md §6.16): role selection above, exclusion LAST — both
291
+ // branches converge here (`question` = interactive main-session tool; a child has no user to ask).
292
+ tools = excludeSubagentTools(tools)
261
293
 
262
294
  // PROMPT-SYSTEM 施工② G3(2026-09-10):overlay(人格)装载随装配改造退役——人格槽
263
295
  // 由 assemblePrompt 的 D1 场景表按 role 承载(persona-{role},explore/coder/plan =
@@ -307,19 +339,19 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
307
339
  overlay,
308
340
  role,
309
341
  })
310
- // TUI-OOM-ROOTCAUSE(AGENT-LOOP.md §23.3.1):子代理人读线窗口置位(常量单源
342
+ // TUI-OOM-ROOTCAUSE(AGENT-LOOP.md §6.15):子代理人读线窗口置位(常量单源
311
343
  // session-store.mjs)——机制复用主 agent 同路径(context.mjs pushReal 驱逐);
312
344
  // 每个 child 原先各自一份永不压缩的 _fullHistory(勘察 C2 乘数面)。
313
345
  child._historyWindow = RECORD_WINDOW_MESSAGES
314
346
 
315
- // §2.20.2 批次档段写入通道绑定(第 4 批):工程角色(eng-coder/eng-designer)把批次档
316
- // 绝对路径记在 child 上——setup 挂载 batch_segment 时读它。无路径参数的工具靠这条
347
+ // BATCH-RECORD.md §2 批次档段写入通道绑定(第 4 批):工程角色(eng-coder/eng-designer)把批次档
348
+ // 绝对路径记在 child 上——setup 挂载 `batch` 时读它。无路径参数的工具靠这条
317
349
  // 绑定决定目标档(spawn 门已保证「参数在 + 路径可读」)。
318
350
  if (engineeringRole) child._batchDoc = batchDocAbs
319
351
 
320
352
  // Token-verified design review → child is authorized to modify files without re-reviewing
321
353
  if (role === "eng-coder") child._engDesignReviewed = true
322
- // §18 D-E3 task-domain authorization: approved design + spawn task = authorization.
354
+ // AGENT-LOOP-SUBAGENT.md §6.7.6 D-E3 task-domain authorization: approved design + spawn task = authorization.
323
355
  // The child's OWN tools skip ONLY the onPermissionRequest ask (autoApprove
324
356
  // equivalent — dispatch.mjs permission stage); every other gate (JSON parse /
325
357
  // unknown tool / planMode / design-token) still applies (T-E14). Non-eng-coder
@@ -334,7 +366,7 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
334
366
  child._engDesignId = args.designId ?? null
335
367
  }
336
368
 
337
- // §18.5 子代理零 git(D-AG1——2026-09-04 用户裁定):explore/plan 一律不注入
369
+ // §6.7.4 子代理零 git(D-AG1——2026-09-04 用户裁定):explore/plan 一律不注入
338
370
  // git 上下文——子代理证据链 = 任务书 ∪ 磁盘当前状态(read/glob/grep)∪(审计时)
339
371
  // _touchedFiles,无一项来自 git;注入的全工作区脏状态快照与任务域无关,会误导
340
372
  // 审计/探索("status 里这个文件算不算超清单?")。注入分支整体删除(B 方案
@@ -353,7 +385,7 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
353
385
  // 本行让子代理"拿到本档路径")。行文不含 summarizeEngTaskBook 的三组段 marker
354
386
  // (Docs involved / Files list / Acceptance criteria)——段匹配不受影响。仅工程角色注入。
355
387
  if (engineeringRole) spawnBlocks.push(`Batch record (batchDoc): ${batchDocAbs}`)
356
- // §18 D-E2 ③ (round4 #4, T-E13/T-E15): an eng-coder audit spawn's task book is
388
+ // AGENT-LOOP-SUBAGENT.md §6.7.6 D-E2 ③ (round4 #4, T-E13/T-E15): an eng-coder audit spawn's task book is
357
389
  // the eng-coder's OWN spawn task — mechanically kept as _engTaskInput by the
358
390
  // parent spawn and injected as the D-TS5 A2 mechanical summary (design docs /
359
391
  // affected-file list / acceptance criteria verbatim, verbose context dropped)
@@ -370,10 +402,10 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
370
402
  if (spawnBlocks.length > 0) child._spawnSystemBlock = spawnBlocks.join("\n\n")
371
403
 
372
404
  // Relay content/reasoning/tool/output to the parent TUI via the unified spawn-child
373
- // pipeline (AGENT-LOOP.md §7.2 D3). Prefix includes a unique id: parallel child agents
405
+ // pipeline (AGENT-LOOP-SUBAGENT.md §6.7.2 D3). Prefix includes a unique id: parallel child agents
374
406
  // with the same role stay independent and don't overwrite each other.
375
407
  // Format: role#id/ → onToken("coder#2/writing..."), onToolCall("coder#2/read", args)
376
- // Async id allocation (AGENT-LOOP.md §15 D-A1): reserve the relay counter at
408
+ // Async id allocation (AGENT-LOOP-SUBAGENT.md §6.7.3 D-A1): reserve the relay counter at
377
409
  // spawn time — the returned id must be stable while the item sits in the queue.
378
410
  // The [model] token (TUI block creation) is DEFERRED to actual start so queued
379
411
  // children don't paint an empty panel block ("queued 态不显示").
@@ -6,15 +6,15 @@
6
6
  * 2026-09-03 拆分轮: subagent.mjs 超 500 硬顶——async 常量、共享 post-spawn 管线
7
7
  *(runChildPipeline)与队列/注入/并账机械迁至 ./subagent-async.mjs。execute
8
8
  *(async 分支 + 阻塞路径)原样保留于本文件;导出面由文末 re-export shim 兜住。
9
- * 2026-09-03 §19 合体轮: subagent_check/escalate 工具退役——status/escalate
9
+ * 2026-09-03 合体轮(AGENT-LOOP-SUBAGENT.md §6.7): subagent_check/escalate 工具退役——status/escalate
10
10
  * 动作执行器并入 ./subagent-async.mjs,本文件只承载工具面(action schema)与
11
11
  * spawn 路径 + 动作分流。
12
- * 2026-09-06 §19.8 删 check 轮: check 动作删除——工具面五动作(spawn/status/
12
+ * 2026-09-06 删 check 轮(AGENT-LOOP-SUBAGENT.md §6.7.5): check 动作删除——工具面五动作(spawn/status/
13
13
  * escalate/cancel/panel)——async 结果仅自动通道送达。
14
14
  * 2026-09-05 拆分轮: status/escalate/panel 动作执行器 → ./subagent-actions.mjs;§20
15
15
  * 调度器全套 → ./subagent-scheduler.mjs——本文件 import 源随之改写。
16
16
  * 2026-09-05 模块拆分轮(726 > 500 硬限): spawn 前置 helpers(summarizeEngTaskBook/
17
- * effectiveSubagentModel/resolveDesignSlot)+ §20 准入(prepareScheduling)+ child
17
+ * effectiveSubagentModel/resolveDesignSlot)+ AGENT-LOOP-SUBAGENT.md §6.9 准入(prepareScheduling)+ child
18
18
  * 装配(buildSpawnChild)→ ./subagent-spawn.mjs;async 分支(executeAsyncSpawn)
19
19
  * → ./subagent-run.mjs——execute 只保留动作分流 + 装配调用 + 阻塞路径。
20
20
  */
@@ -33,6 +33,8 @@ import { executeStatusAction, executeEscalateAction, executePanelAction, execute
33
33
  import { prepareScheduling, buildSpawnChild, executeConsumeDesignAction } from "./subagent-spawn.mjs"
34
34
  import { executeAsyncSpawn } from "./subagent-run.mjs"
35
35
  import { ROUND_VALUES } from "./spawn-gates.mjs"
36
+ // #15 描述外置:描述文本单点 = tool-docs/subagent.md(DESC 单解析面,缺档抛错语义不变)
37
+ import { DESC } from "../tools/shared.mjs"
36
38
 
37
39
  // ─── SYNC-CANCEL 纯函数(可测——无 io)──────────────────────────────────────────
38
40
 
@@ -71,7 +73,7 @@ export function classifySyncAbort(ctxSignal, baseSignal, ctrlSignal, err) {
71
73
  export function armSyncChildAbort(parent, key, baseSignal, announce = null) {
72
74
  const ctrl = new AbortController()
73
75
  if (baseSignal) {
74
- // §20.3 站点 #10(第 24 批):hop 逐跳保 reason(下游可判定「谁杀的」)
76
+ // AGENT-LOOP-SUBAGENT.md §6.12 站点 #10(第 24 批):hop 逐跳保 reason(下游可判定「谁杀的」)
75
77
  if (baseSignal.aborted) ctrl.abort(baseSignal.reason)
76
78
  else baseSignal.addEventListener("abort", () => ctrl.abort(baseSignal.reason), { once: true })
77
79
  }
@@ -100,15 +102,15 @@ export function buildSyncStoppedReport(role, capturedOutput, designId) {
100
102
  }
101
103
 
102
104
  /**
103
- * subagent tool — ONE tool, EIGHT actions (AGENT-LOOP.md §19/§19.5/§19.6/§19.8 +
105
+ * subagent tool — ONE tool, EIGHT actions (AGENT-LOOP-SUBAGENT.md §6.7 +
104
106
  * SUBAGENT-OBSERVE-SEND): spawn (default) / status (non-blocking pool query) / observe
105
- * (inspect a running/queued/done async child's recent activity + current tool — §7.2) /
107
+ * (inspect a running/queued/done async child's recent activity + current tool — docs/cli/design/TUI.md §6.8) /
106
108
  * send (inject a direction into a RUNNING async child — consumed at its next turn
107
- * boundary as an ordinary instruction — §7.2) / escalate (飞刀 — hand implementation to
108
- * a stronger model) / cancel (stop ONE background subagent — §19.5) / panel (view + fix
109
- * the live subagent panel — §19.6) / consume-design (parent-side chain-terminal token
109
+ * boundary as an ordinary instruction — §6.8) / escalate (飞刀 — hand implementation to
110
+ * a stronger model) / cancel (stop ONE background subagent — AGENT-LOOP-SUBAGENT.md §6.7.2) / panel (view + fix
111
+ * the live subagent panel — AGENT-LOOP-SUBAGENT.md §6.7.2) / consume-design (parent-side chain-terminal token
110
112
  * consumption — ENGINEERING-MODE.md §2.6, 2026-09-07). The check
111
- * action was deleted (§19.8): async results reach the model only via the auto channel.
113
+ * action was deleted (AGENT-LOOP-SUBAGENT.md §6.7.5): async results reach the model only via the auto channel.
112
114
  * - action:"spawn" roles: "explore" — read-only tools, search/read/analyze
113
115
  * (suitable for codebase exploration); "coder" — full tool set, self-contained
114
116
  * implementation tasks; "plan" — read-only planning; "eng-coder" —
@@ -117,56 +119,34 @@ export function buildSyncStoppedReport(role, capturedOutput, designId) {
117
119
  * - no role specified — invalid by design since the 2026-08-25 fail-closed gate
118
120
  * (role is mandatory; "no role → same tool set as parent" was removed with the
119
121
  * coder-leak fix and the header text above predates it)
120
- * - non-recursive: child agents do not get the subagent tool (depth > 0 is not injected)
122
+ * - non-recursive for non-eng children: child agents do not get the subagent tool (depth > 0 is not
123
+ * injected); eng-coder / eng-designer children get a spawn-only variant (`agent/family-tools.mjs`)
124
+ * - 描述文本单点 = `thincoder-core/tool-docs/subagent.md`(模型可见文本不在此复述——本块只留实现注记)
121
125
  */
122
126
 
123
127
  export const subagentTool = {
124
128
  name: "subagent",
125
- description:
126
- "ONE tool, EIGHT actions — pick by what you need:\n" +
127
- "- action:'spawn' (DEFAULT): spawn a sub-agent to handle an independent subtask in an isolated context; the sub-agent returns only its final report. Spawn MULTIPLE subagents in the SAME response for parallel work—they run concurrently.\n" +
128
- "- action:'status': NON-BLOCKING progress query — returns immediately and consumes nothing. Give the spawn's id for one child ({id, role, status: running|queued|done, model, elapsedSec, turn, maxTurns, position?), or omit it for an overview of the whole pool ({overview: {running: [{id, role, model, elapsedSec, turn, maxTurns}], queued: [{id, role, position}], done: [{id, role}]}}). Touched-files summary: running entries also carry touchedFiles (first 5, relative to your cwd), touchedMore (count beyond 5) and, when nothing was touched yet, the placeholder touched (\"—(尚无改动)\"); queued (not yet started) entries carry the placeholder touched (\"—(未启动)\") — see what a running child has changed BEFORE deciding to cancel it. Use THIS to see progress — it never blocks and never consumes a result (async results are delivered to you automatically). Background advisor reviews share this status surface: pass a review id and it answers in the same shape with role:'advisor' plus reviewType (design|code) / round / elapsedSec — and the overview lists reviews alongside subagents.\n" +
129
- "- action:'observe': SEE what a running async subagent is DOING right now (progress vs stuck) — pass the spawn id. Returns {id, role, status, turn, maxTurns, touched…, currentTool? — array of tool name(s) currently executing (read from its dispatch state; omitted when none in flight — present when stuck in a long tool call), recentTurns: [last N one-line turn summaries, newest-first; default 5, parameterizable via recent]}. Readonly — observable on running/queued/done: running shows live activity, queued (not started) returns a placeholder, done (settled, report auto-delivered) returns the activity summary only — NOT the full report (that rides the auto channel; observe stays terse to keep your context lean). Use it to judge whether a long-running child is stuck vs progressing BEFORE deciding to cancel or steer it.\n" +
130
- "- action:'send': STEER a running async subagent mid-flight — pass the spawn id + message (a direction like \"check X, don't fixate on Y\"). The message queues and the child consumes it at its next turn boundary as an ORDINARY user instruction (non-interrupting — its current tool finishes first; its convergence/audit discipline is unchanged — injection is guidance, not a deviation waiver). Returns {id, status:'delivered', queued}. Only a RUNNING async subagent is targetable — sync (you're waiting on it, no relay), queued (not started), settled or unknown ids error clearly. If the child settles before its next turn boundary, its settle report carries an 'undelivered' note so you don't assume the guidance landed.\n" +
131
- "- action:'escalate' (飞刀 — a flown-in expert): hand an implementation task to a STRONGER model from your consult models (agent.consultModels). It gets WRITE access and does the work itself — reads, edits, runs tests — then returns a post-op report (what changed, why, verification). Use it when YOU judge the task calls for stronger hands (complex multi-file refactoring, an intractable bug, intricate algorithm work — or work beyond your comfortable ability); escalate EARLY, not after burning attempts. model: pick a candidate as 'provider:model' (default = the first consult model). Not available in engineering mode (implementation goes through eng-coder spawns there). DEFAULT-ASYNC at depth 0: the launch returns an ack {id, role:'escalate', status:'running'} and the flight runs in the background (pooled with the other role-domain spawns) — its post-op report is delivered to you automatically with its mutations merged into your bookkeeping.\n" +
132
- "- action:'cancel': STOP one background subagent — pass the id from the async spawn return (REQUIRED — omitting it errors; a blanket cancel is unsupported, Ctrl+C stops everything). Running target aborts immediately ({id, status:'cancelled'}); a queued target is removed from the queue ({id, status:'cancelled', was:'queued'} and later queue positions shift forward). Other children and the session keep running — cancellation is targeted. Use it when a background child is going the wrong way (e.g. burning turns) and you must stop it before its report arrives. Cancel is a last resort: verify alarming signals with reliable checks (git/node — not guesses) first; prefer scoped recovery (restore a single affected file) over killing the child — a running child's in-flight work dies with it, partial changes stay unmerged and unaudited. Background advisor reviews are cancelable on this same action: pass the review id — its controller aborts, no token is issued for a cancelled review.\n" +
133
- "- action:'consume-design' (engineering mode, parent side — chain-terminal token consumption): after the delivery is verified and the chain closes out, consume this design's token slot — pass the designId (optional for a single-design session). The slot is consumed; a further spawn for the same designId is mechanically rejected, and any new work (including deviation fixes) requires a fresh design review and token. Idempotent: an unknown designId / already-consumed slot is a no-op notice, never an error. Do NOT call it while the chain is still open — fix rounds reuse the same slot (same designId + designToken).\n" +
134
- "- action:'panel': DIAGNOSE + fix the subagent panel — the collapsible blocks under the conversation the user sees (CLI TUI panel mirror; headless/VS Code degrade to a 'no panel' pool view). view (default — call it with no params or view:true): returns the live panel blocks [{key, role, status: running|done|awaitingDigest} — running entries also carry elapsedSec; awaitingDigest entries whose report is ALREADY digested carry digested:true (stuck blocks — the freezable ones — explain odd panel states here)] exactly as the user sees them. freeze: pass the block key of a digested-stuck block ({action:'panel', freeze:'role#N'}) to reclaim it into the conversation — the freeze ONLY passes for awaitingDigest blocks with no live pool entry and no pending report (gated); freezing a block whose report is still pending would break the digestion order and is refused with a clear error.\n\n" +
135
- "Why delegate? A sub-agent runs in its own isolated context — its reads, searches, tool calls and edits never enter your history or pollute your window; only its final report comes back. Delegation keeps your working context lean (you see the whole session, not the child's noise) and the child single-mindedly focused on one task. Parallel children run concurrently, saving wall-clock time. Every coder/eng-coder child carries its own verify + advisor self-review discipline — handed-off work is already verified before you read a word of it.\n\n" +
136
- "Available roles (which roles are exposed depends on the active mode — see Mode filtering below):\n" +
137
- "- explore — read-only search & analysis. Toolset: the read/search family (grep, read, glob, code_search, doc_search, repo_outline, lsp, tree...). No git context injected—evidence from read/glob/grep and the task book. Its report must list what it searched and what it did NOT find. Fast — specify thoroughness in the task: quick / medium / thorough (default medium).\n" +
138
- "- plan — read-only implementation planning. Same read/search toolset; NEVER edits files. Returns a step-by-step plan for the parent to execute.\n" +
139
- "- coder — full implementation. The parent's complete read/write/execute toolset plus verify and advisor for self-review. Its final report must include a delivery transparency table with one row per task requirement (Done / Simplified / Not done — no deferred column).\n" +
140
- "- eng-coder — engineering-mode coder (available only in engineering mode, replacing coder). Same full toolset as coder plus the design-driven methodology overlay; REQUIRES a valid designToken arg obtained from a passed advisor(type='design') review. The advisor's Approved reply also echoes a designId — pass it as the designId arg: required to pick between designs when several approved reviews are active, optional for a single design. The delivery report echoes the designId back for the audit fix round. ALSO REQUIRES a batchDoc arg — the batch record path (docs/batches/<batch>-<topic>.md), the batch §2 task book this spawn implements: a spawn without it, or with a path that does not resolve to a readable file, is mechanically refused.\n" +
141
- "- eng-designer — engineering-mode design writer (available only in engineering mode): the SOLE author of the requirements + design documents and of the batch record §2 (the batch task book) — revisions included. Writes no implementation code, does not edit prompt files, does not fire reviews, and needs NO designToken (its authorization is the confirmed requirements). It surveys on its own, but may only spawn read-only 'explore' children (sync, ≤6 per batch). ALSO REQUIRES a batchDoc arg — the batch record path (docs/batches/<batch>-<topic>.md); the same mechanical gate as eng-coder: a spawn without it, or with a path that does not resolve to a readable file, is mechanically refused.\n" +
142
- "Mode filtering: normal mode exposes explore/plan/coder (eng-coder and eng-designer are refused); engineering mode exposes explore/eng-designer/eng-coder (plan and coder are refused — plan is a normal-mode role, coder is replaced by eng-coder). The role enum is filtered per mode at assembly, and the spawn gate refuses out-of-mode roles mechanically.\n\n" +
143
- "Async spawn: pass async:true to spawn WITHOUT waiting — returns {id, role, status:\"running\"} immediately so you can keep working in your own turn (read/check files, run other tools) while the child runs in the background. The child's report is delivered to you automatically — there is no fetch action; use action:'status' only to see progress, never to wait for the result. Top-level spawns are ALWAYS async — never pass `async:false` at depth-0 (the report arrives automatically; if your next step needs it, end the turn and let the digest deliver it). Inside subagents (depth>0) spawns are always synchronous (platform rule). Eng-coder's delivery protocol runs fully inside the child (implementation → audit → self-fix → advisor re-review → converged delivery). Async spawns are pooled per role domain: at most 4 concurrent eng-coders and 4 concurrent other-role spawns by default (agent.poolLimits overrides both) — a full domain queues further spawns with a position while the other domain keeps starting (domains never block each other), and top-level only. After an async spawn the turn winds down normally — nothing expects you to wait for it: the child runs in the background and its report is delivered to you automatically — before your next turn, or digested in the suspension session — so end the turn; do not poll or wait for the result.\n\n" +
144
- "Task scheduling: declare the scheduling metadata to let the SCHEDULER order your spawns — files: the file paths this task will modify, dependsOn: ids from prior async spawn returns whose outcome this task needs. Overlapping-file tasks are serialized and dependent tasks are started in order automatically: a spawn that would conflict, or whose dependencies have not settled, queues instead of running ({id, status:\"queued\", position, reason} — the waiting task auto-starts when the conflict clears / its dependency settles; cancel a queued task to drop it). A spawn whose dependency was cancelled or failed stays queued and marked \"dependency cancelled\" until you decide (cancel it) — in an AUTO session it starts by itself. Referencing an unknown id errors; an id already consumed (auto-delivered by the auto channel) counts as satisfied. Omit both parameters for the plain immediate spawn (no scheduler involvement).\n\n" +
145
- "Writing the prompt:\n" +
146
- "- The sub-agent starts with zero context — it has not seen this conversation. Brief it like a colleague who just walked into the room: state the goal, list what you already know, hand over the specifics.\n" +
147
- "- Put exact paths and commands in the prompt when you know them. The sub-agent should not search for things you already know.\n" +
148
- "- Do not delegate understanding: if the task hinges on a file path or line number, find it yourself first and write it into the prompt.\n" +
149
- "- Once a sub-agent is running, leave that scope to it: don't redo its searches in parallel, and don't abandon it midway to finish manually.",
129
+ description: DESC("subagent"),
150
130
  parameters: {
151
131
  type: "object",
152
132
  properties: {
153
- action: { type: "string", enum: ["spawn", "status", "escalate", "cancel", "panel", "consume-design", "observe", "send"], description: "Which subagent-family action — spawn (default), status (non-blocking progress query — never consumes; async results arrive automatically — no fetch action), observe (inspect a running/queued/done async subagent's recent activity — recent turn summaries + current in-flight tool + turn/touched), send (inject a direction into a RUNNING async subagent, consumed at its next turn boundary as an ordinary instruction), escalate (飞刀 — hand implementation to a stronger consult model), cancel (stop ONE background subagent — pass its id; never omit), panel (view the live subagent panel / freeze a digested-stuck block), consume-design (engineering mode, parent side: chain-terminal token consumption — close out a design's token slot after the delivery is verified and the chain closes out, 2026-09-07). See the tool description for the full action matrix." },
154
- view: { type: "boolean", description: "action:'panel' only: true (default) = return the live panel blocks (the mirror of what the user sees). false with no freeze = nothing to do — error. Mutually exclusive with freeze (freeze wins)." },
155
- freeze: { type: "string", description: "action:'panel' only: block key of a digested-stuck awaitingDigest block (e.g. \"eng-coder#9\") to reclaim into the conversation via the gated done-freeze event. Refused when the block is running/done/unknown or its report is still pending digestion (would break the digestion order). Requires the CLI TUI panel mirror — headless/VS Code report the freeze unavailable." },
156
- task: { type: "string", description: "Required for action:'spawn' (the self-contained task brief) and action:'escalate' (goal, constraints, entry files, acceptance criteria). Not used by status." },
133
+ action: { type: "string", enum: ["spawn", "status", "escalate", "cancel", "panel", "consume-design", "observe", "send"], description: "Which action — spawn (default) / status / observe / send / escalate / cancel / panel / consume-design; semantics per action in the tool description." },
134
+ view: { type: "boolean", description: "action:'panel' only: true (default) = the live panel blocks as the user sees them; false without freeze = error. Mutually exclusive with freeze (freeze wins)." },
135
+ freeze: { type: "string", description: "action:'panel' only: block key of a digested-stuck block — reclaims it into the conversation; refused for running/done/unknown blocks or a still-pending report; requires the CLI TUI panel mirror." },
136
+ task: { type: "string", description: "Required for action:'spawn' (the self-contained task brief) and action:'escalate' (goal, constraints, entry files, acceptance criteria)." },
157
137
  context: { type: "string", description: "Optional background the sub-agent needs (it cannot see this conversation); action:'spawn' only." },
158
- role: { type: "string", enum: ["explore", "plan", "coder", "eng-coder", "eng-designer"], description: "The sub-agent role — see the tool description for the role capability matrix. Exact spelling required. action:'spawn' only (escalate spawns its own expert internally)." },
159
- model: { type: "string", description: "action:'spawn': provider/model override for this sub-agent ('provider:model', a provider name, or a model name on the parent's provider — defaults to config.agent.subagentModels[role], then config.agent.subagentModel, then the parent's provider). pass \"default\" to explicitly inherit the default model — equivalent to omitting the parameter. action:'escalate': pick a consult candidate as 'provider:model' (default = the first consult model)." },
160
- designToken: { type: "string", description: "Required when role='eng-coder': the token returned by advisor(type='design') after the design review passed. Without a valid token, eng-coder cannot modify files." },
161
- designId: { type: "string", description: "Optional when role='eng-coder': the designId echoed with the approved token by advisor(type='design'). Required to pick between designs when several approved reviews are active in the session — each eng-coder carries its own designId+token pair so parallel implementations never overwrite each other. Optional for a single design. action:'consume-design': the design whose slot to close out — optional for a single-design session; required to pick when several approved designs are active (the consume gate refuses to guess)." },
162
- batchDoc: { type: "string", description: "REQUIRED for role='eng-coder' and role='eng-designer': the batch record path (docs/batches/<batch>-<topic>.md) — the batch §2 task book this spawn implements (or writes). The spawn is mechanically refused without it, and also when the path does not resolve (cwd-relative or absolute) to a readable file; the CONTENT is never validated (the batch record owns that). explore/plan/coder spawns ignore it." },
163
- round: { type: "string", enum: ROUND_VALUES, description: "REQUIRED for role='eng-coder' and role='eng-designer' (explore/plan/coder exempt — and advisor is review-only, never a spawn write role): the implementation round this spawn is — initial (first implementation round for this design) or fix (a correction round reusing the same designId+designToken; docs FIRST). No default: a missing round is a dispatch defect and the spawn is mechanically refused." },
164
- async: { type: "boolean", description: "action:'spawn': true = spawn without waiting — returns {id, status:\"running\"} immediately; the report is delivered to you automatically (there is no fetch action). Default: depth-0 → true (async — every role); depth>0 → sync (forced). action:'escalate': same semantics — default async at depth 0; async:false keeps the legacy synchronous flight (mechanism parameter — see the Async spawn section for top-level guidance)." },
165
- files: { type: "array", items: { type: "string" }, description: "action:'spawn' only: the file write-domain this task declares (cwd-relative or absolute paths). files must be file-level paths (one per file you will modify). Directory declarations are NOT supported — they bypass the conflict detector and are rejected with an error. Tasks with overlapping files are serialized automatically — a conflicting spawn queues ({id, status:\"queued\", position, reason}) instead of running concurrently and starts when the conflict clears. Omit to skip conflict detection (plain immediate spawn)." },
166
- dependsOn: { type: "array", items: { type: "string" }, description: "action:'spawn' only: ids from prior async spawn returns whose outcome this task needs — the task queues ({id, status:\"queued\", position, reason}) until every dependency settles, then starts automatically. Ids already consumed (auto-delivered to the model) count as satisfied; a dependency cancelled or failed leaves the task queued marked 'dependency cancelled' until you decide (cancel it — AUTO sessions auto-start). Unknown ids error." },
167
- id: { type: "string", description: "action:'status'/'observe'/'send'/'cancel': the subagent id from the async spawn return. status: omit = overview of the whole pool; observe/send/cancel: REQUIRED (observe needs the child to inspect; send needs the target; never omit on cancel — a blanket cancel is unsupported)." },
168
- message: { type: "string", description: "action:'send' only (REQUIRED there): the direction to inject — the running async subagent consumes it at its next turn boundary as an ordinary user instruction (non-interrupting; its convergence/audit discipline is unchanged — injection is guidance, not a deviation waiver)." },
169
- recent: { type: "integer", description: "action:'observe' only (optional, default 5): how many recent-turn summaries to return (clamped 1..20 — N2 keeps observe terse)." },
138
+ role: { type: "string", enum: ["explore", "plan", "coder", "eng-coder", "eng-designer"], description: "The sub-agent role — see the role matrix in the tool description. Exact spelling required. action:'spawn' only (escalate spawns its own expert internally)." },
139
+ model: { type: "string", description: "action:'spawn': provider/model override ('provider:model', a provider, or a model name on the parent's provider) — defaults config.agent.subagentModels[role] → config.agent.subagentModel → the parent's provider; \"default\" inherits. action:'escalate': a consult candidate (default = the first)." },
140
+ designToken: { type: "string", description: "Required when role='eng-coder': the token from advisor(type='design') after the review passed — without it, eng-coder cannot modify files." },
141
+ designId: { type: "string", description: "role='eng-coder': the designId echoed with the approved token — required to pick between concurrent designs (optional for one). action:'consume-design': the slot to close out (required to pick between several; the gate refuses to guess)." },
142
+ batchDoc: { type: "string", description: "REQUIRED for role='eng-coder'/'eng-designer': the batch record path (docs/batches/<batch>-<topic>.md) — the task book this spawn implements/writes. Refused when absent or when the path does not resolve to a readable file; the content is never validated." },
143
+ round: { type: "string", enum: ROUND_VALUES, description: "REQUIRED for role='eng-coder'/'eng-designer': initial (first round for this design) or fix (a correction round reusing the same designId+designToken; docs FIRST). No default — missing = refused." },
144
+ async: { type: "boolean", description: "action:'spawn'/'escalate': default at depth 0 = true (async; the report arrives automatically); depth>0 always sync. async:false forces the blocking flight." },
145
+ files: { type: "array", items: { type: "string" }, description: "action:'spawn' only: the file write-domain this task declares — file-level paths only (cwd-relative or absolute; directories rejected — they bypass the conflict detector). Overlapping-file tasks serialize automatically. Omit = no conflict detection." },
146
+ dependsOn: { type: "array", items: { type: "string" }, description: "action:'spawn' only: ids from prior async returns whose outcome this task needs — queues until every dependency settles, then starts. Consumed ids count as satisfied; a cancelled/failed dependency stays queued marked 'dependency cancelled' until you decide (AUTO sessions auto-start). Unknown ids error." },
147
+ id: { type: "string", description: "action:'status'/'observe'/'send'/'cancel': the subagent id from the spawn return. status: omit = pool overview; observe/send/cancel: REQUIRED." },
148
+ message: { type: "string", description: "action:'send' only (REQUIRED there): the direction to inject — consumed by the running child at its next turn boundary as an ordinary instruction (non-interrupting)." },
149
+ recent: { type: "integer", description: "action:'observe' only (optional, default 5): how many recent-turn summaries (1..20)." },
170
150
  },
171
151
  required: [],
172
152
  },
@@ -174,20 +154,20 @@ export const subagentTool = {
174
154
  sideEffectExempt: true, // child agent may write files; parent can't introspect its _mutatedThisRun
175
155
  parallel: true,
176
156
  async execute(args, ctx) {
177
- // §19 action dispatch: default spawn keeps every legacy call unchanged
157
+ // AGENT-LOOP-SUBAGENT.md §6.7 action dispatch: default spawn keeps every legacy call unchanged
178
158
  // (no action parameter → the spawn path below, byte-identical semantics).
179
159
  const action = args?.action !== undefined && args?.action !== null && String(args.action) !== ""
180
160
  ? String(args.action)
181
161
  : "spawn"
182
162
  if (action !== "spawn") {
183
- // §19 restricted-variant action gate (round2 #3): the engineering-child channel
163
+ // AGENT-LOOP-SUBAGENT.md §6.7 restricted-variant action gate (round2 #3): the engineering-child channel
184
164
  // (depth>0, role eng-coder or eng-designer) is spawn-only — escalate spawns a
185
165
  // coder+WRITE child (violates explore-only intent) and status/panel/observe/send
186
166
  // have no async pool / panel mirror to query in a child context.
187
167
  if ((ctx.depth ?? 0) > 0 && (ctx.agent?._role === "eng-coder" || ctx.agent?._role === "eng-designer")) {
188
168
  throw new Error(`only action:'spawn' (sync explore children) is available inside an ${ctx.agent._role} — escalate/status/cancel/panel/consume-design/observe/send are not`)
189
169
  }
190
- // §17 N3/D-S6 spawn gate (manual tier): auto-turn digests may not spawn —
170
+ // AGENT-LOOP-ASYNC-POOL.md §6.8 N3/D-S6 spawn gate (manual tier): auto-turn digests may not spawn —
191
171
  // async OR blocking — the digest must stay organize-only. The escalate
192
172
  // action spawns a write child too, so the same mechanical refusal applies
193
173
  // (AUTO tier exempt — user authorized unattended continuation).
@@ -196,14 +176,14 @@ export const subagentTool = {
196
176
  }
197
177
  if (action === "status") return executeStatusAction(args, ctx)
198
178
  if (action === "escalate") return await executeEscalateAction(args, ctx)
199
- // §19.5 控制类动作:digest 内放行(D-S7 分类——控制/自省;dispatch 控制类
179
+ // AGENT-LOOP-SUBAGENT.md §6.7.2 控制类动作:digest 内放行(D-S7 分类——控制/自省;dispatch 控制类
200
180
  // 豁免同批生效——19.5.2b round2 #4;escalate 的 digest 拒绝在上一分支)
201
181
  if (action === "cancel") return executeCancelAction(args, ctx)
202
182
  // 2026-09-07 token 链终消费制(ENGINEERING-MODE.md §2.6 F1):父侧核销消费——
203
183
  // 非只读控制动作——depth-0 + 工程模式限定(本分流已过受限变体门;工程模式门在
204
184
  // 执行器内)——planMode 拒绝(dispatch 不豁免)——不入批审批分组(dispatch 免审)。
205
185
  if (action === "consume-design") return executeConsumeDesignAction(args, ctx)
206
- // §19.6 panel 动作:view(readonly 面——digest 内放行——自省类)与 freeze
186
+ // AGENT-LOOP-SUBAGENT.md §6.7.2 panel 动作:view(readonly 面——digest 内放行——自省类)与 freeze
207
187
  // (控制类——同 cancel——digest 内放行)。深度/门控检查在 executePanelAction 内。
208
188
  // CLI-ACTIVITY-DEBLOAT F-3(2026-09-10)接线:executePanelAction 经 ctx.state
209
189
  // (= agent._tuiState——startTUI 反向挂载)读时现算面板块(computePanelBlocks)——
@@ -239,7 +219,7 @@ export const subagentTool = {
239
219
  if (!ROLES.has(role)) {
240
220
  throw new Error(`Unknown subagent role: ${JSON.stringify(role)}. Valid roles: explore, plan, coder, eng-coder, eng-designer (exact spelling).`)
241
221
  }
242
- // §18 D-E3 internal-spawn gate: an eng-coder sub-agent may only spawn sync
222
+ // AGENT-LOOP-SUBAGENT.md §6.7.6 D-E3 internal-spawn gate: an eng-coder sub-agent may only spawn sync
243
223
  // explore (audit) children — non-explore roles and async are refused here
244
224
  // (mechanical), the audit budget is enforced (7th audit spawn refused), and
245
225
  // the returned attempt number marks this spawn as an audit for the task-book
@@ -263,7 +243,7 @@ export const subagentTool = {
263
243
  throw new Error("Engineering mode is not active — role='eng-designer' is engineering-mode only (it writes the requirements/design documents inside the engineering workflow); use role='explore' or role='plan' for read-only work.")
264
244
  }
265
245
 
266
- // §17 N3/D-S6 spawn gate (manual tier): auto-turn digests may not spawn — async
246
+ // AGENT-LOOP-ASYNC-POOL.md §6.8 N3/D-S6 spawn gate (manual tier): auto-turn digests may not spawn — async
267
247
  // OR blocking — the digest must stay organize-only. AUTO tier (autoApprove) is
268
248
  // exempt (推进型 — user authorized unattended continuation). Mechanical refusal
269
249
  // so the digest never pops a permission panel or chains new background work.
@@ -300,7 +280,7 @@ export const subagentTool = {
300
280
  // stopped 折叠语义丢失:无 ⟦ev⟧stopped/无 STOPPED_MARK——块冻结标 done 而非
301
281
  // stopped——评审 🟡#2)。stopped 分支改抛 AbortError——runWithContinue 只捕
302
282
  // ContinueError——原样上抛 → 阻塞 catch 三分支②折叠("child 随即在 abort 检出点
303
- // 解绕折叠"——AGENT-LOOP §7.2 机制文)。abort 恒已在途(stopped 只由
283
+ // 解绕折叠"——AGENT-LOOP-SUBAGENT.md §6.7.2 机制文)。abort 恒已在途(stopped 只由
304
284
  // cancelSyncChild 与 ctrl.abort 同时置位)——信号语义真实。
305
285
  if (parent._syncChildAborts?.get(key)?.stopped) throw abortError(ctrl.signal, "settle", "sync-stopped")
306
286
  const go = await ctx.onPermissionRequest("continue", { turns: e.turn, agent: key })
@@ -348,14 +328,14 @@ export const subagentTool = {
348
328
  const cls = classifySyncAbort(ctx.signal, baseSignal, ctrl.signal, e)
349
329
  if (cls === "base") {
350
330
  // ① 整回合停(现状逐字保留——挂起场景 base 命中而 ctx.signal 未 abort——R2):
351
- // §27 R23:外层 abort 传播的中断——内层开块随之外层冻结前先收尾定格
331
+ // docs/cli/design/TUI.md §6.8.2 R23:外层 abort 传播的中断——内层开块随之外层冻结前先收尾定格
352
332
  // (D-R23c1 stopped——T-R23c.2a 生成侧路径;TUI 冻结兜底仍在 freezeSubTaskLines)
353
333
  emitNestedChildEvent(ctx, relayPrefix, "stopped")
354
334
  throw e // 用户停——不落错误事件
355
335
  }
356
336
  if (cls === "error") {
357
337
  // ③ 其他错误(现状逐字保留——:249-253):
358
- // §27 R23 error-run 映射(实现批补一行):run 错误(非 abort)→ 同样发 stopped
338
+ // §6.8.2 R23 error-run 映射(实现批补一行):run 错误(非 abort)→ 同样发 stopped
359
339
  // ——内层子块定格不悬空(T-R23a.3——工具错/运行错误路径)。
360
340
  emitNestedChildEvent(ctx, relayPrefix, "stopped")
361
341
  logEvent("child:error", { role, id: child._logId, ms: Date.now() - blockT0, err: errText(deathLine(e, ctrl?.signal), 200) })
@@ -370,7 +350,7 @@ export const subagentTool = {
370
350
  // 块冻结标 stopped(R6——非 done):⏹ 定向中止的 TUI 顶层块立即定格 stopped
371
351
  // (async settle cancelled 分支同款直发——async-settle.mjs settleAsyncEntry);
372
352
  // 嵌套(eng-coder 内 explore 审计)经 emitNestedChildEvent 定格子块——stopped
373
- // 幂等无害(重复/迟到 done 由 §27.1 F2 done 子块定格丢弃兜底)。
353
+ // 幂等无害(重复/迟到 done 由 docs/cli/design/TUI.md §6.8.2 F2 done 子块定格丢弃兜底)。
374
354
  ctx.callbacks?.onToken?.(`${relayPrefix}⟦ev⟧stopped\x1e0\x1e0\x1estopped\x1e`)
375
355
  emitNestedChildEvent(ctx, relayPrefix, "stopped")
376
356
  } finally {
@@ -378,7 +358,7 @@ export const subagentTool = {
378
358
  // 口径 = 成功/折叠/整回合停——错误③ 同样 rethrow 经 finally——同归本注销)
379
359
  disarm()
380
360
  }
381
- // §27 R23 D-R23c1(评审 #1 🅰——生成侧补发射):sync spawn 同步收尾——若本 spawn
361
+ // §6.8.2 R23 D-R23c1(评审 #1 🅰——生成侧补发射):sync spawn 同步收尾——若本 spawn
382
362
  // 处于嵌套上下文(ctx.callbacks 已是嵌套 wrapper——eng-coder 内 explore 审计)→
383
363
  // 发内层 ⟦ev⟧done(完整嵌套前缀——wrapper 链自动补外层)→ 主 TUI 路由子块定格
384
364
  // (T-R23c.1)。非嵌套(depth-0)零变化——冻结仍由 dispatch subKey 精确冻承接。
@@ -386,7 +366,7 @@ export const subagentTool = {
386
366
  // 冻结管线复用——迟到 done 对已定格 stopped 块被丢弃——幂等无害)。
387
367
  emitNestedChildEvent(ctx, relayPrefix, "done")
388
368
  logEvent("child:done", { role, id: child._logId, ms: Date.now() - blockT0, kind: String(pipelineReport).includes(TURN_CAP_MARK) || String(pipelineReport).includes(STOPPED_MARK) ? "partial" : "ok" })
389
- // §7.2.3 sync spawn 完成精确冻结(方案 e):execute 返回前 ctx 留子代理 key
369
+ // docs/cli/design/TUI.md §6.8 sync spawn 完成精确冻结(方案 e):execute 返回前 ctx 留子代理 key
390
370
  // (relayPrefix 去尾 = `role#N`)——dispatch runOne 读它作 onToolResult 第 4 参 →
391
371
  // TUI finishSubTaskKey 按 key 精确冻(async eng-coder 先启动时不再误冻其块——
392
372
  // T-F2)。仅成功/折叠路径设置:async 分支不设(round2 #2——ack 带 status:running 由
@@ -397,7 +377,7 @@ export const subagentTool = {
397
377
  },
398
378
  }
399
379
 
400
- // Re-export shim (2026-09-03 拆分轮 + §19 合体轮 + 2026-09-05 拆分轮): 机械与动作
380
+ // Re-export shim (2026-09-03 拆分轮 + 合体轮 + 2026-09-05 拆分轮): 机械与动作
401
381
  // 执行器迁至 ./subagent-async.mjs、./subagent-actions.mjs、./subagent-scheduler.mjs
402
382
  // ——本文件保留导出面,消费点(agent.mjs / agent-turn.mjs / consult.mjs / 测试)导入
403
383
  // 路径零改动;池逻辑/准入见 subagent-run.mjs(executeAsyncSpawn)与 subagent-scheduler.mjs
@@ -1,5 +1,6 @@
1
1
  // F-CC4(CONTEXT-COMPACTION.md §6.16.5):task 列表变更 ⇒ 方向转换轻推(本档只调用;文案/去重单源 = agent-tools/context.mjs)
2
2
  import { pushContextNudge } from "./context.mjs"
3
+ import { DESC } from "../tools/shared.mjs"
3
4
 
4
5
  /** Common synonyms LLMs tend to use — normalize to canonical values */
5
6
  const STATUS_ALIASES = {
@@ -26,18 +27,11 @@ function normalizeStatus(raw) {
26
27
 
27
28
  /**
28
29
  * task tool: multi-step task planning and progress tracking (Claude Code's todo mode).
29
- * Each call replaces the entire list; only modifies agent internal state (no external world), so readonly.
30
- * Accesses the caller agent via ctx.agent (injected by runAgent).
30
+ * 描述面 = `tool-docs/task.md`(DESC() 加载);仅改 agent 内部状态(readonly);ctx.agent 由 runAgent 注入。
31
31
  */
32
32
  export const taskTool = {
33
33
  name: "task",
34
- description:
35
- "Plan and track a task list for complex multi-step work. Each call replaces the entire list. " +
36
- "Keep exactly one item in_progress at a time; mark items done as you complete them; never mark done if tests fail or work is partial. " +
37
- "Statuses: pending | in_progress | done — synonyms (completed/finished/complete, todo/open/waiting, active/running/working, …) are accepted and normalized with a warning. " +
38
- "IMPORTANT: title is required and must be a non-empty string — items with empty titles are silently dropped. " +
39
- "For cross-session / project-level tracking, use `/ledger`. " +
40
- "Returns the updated task list (or the new item's ID on add).",
34
+ description: DESC("task"),
41
35
  parameters: {
42
36
  type: "object",
43
37
  properties: {
@@ -1,18 +1,13 @@
1
+ import { TIMER_MAX_PENDING } from "../agent/timers.mjs"
2
+ import { DESC } from "../tools/shared.mjs"
3
+
1
4
  /**
2
5
  * timer tool: set a time budget for thinking before the agent insists on action.
3
- * Call this when starting to analyze code or debug — it gives a bounded
4
- * thinking window. When the timer fires, a system reminder is injected
5
- * suggesting the model try running code, adding logs, or otherwise acting
6
- * instead of continuing to think.
6
+ * 描述面 = `tool-docs/timer.md`(DESC() 加载;机制 = AGENT-LOOP-ASYNC-POOL.md §6.30.3 D-TW8)。
7
7
  */
8
8
  export const timerTool = {
9
9
  name: "timer",
10
- description:
11
- "Set a timer before you start analyzing code. When the timer fires, " +
12
- "a system reminder will be injected suggesting you try running code or " +
13
- "adding debug logs. Use this to enforce a thinking budget: you get " +
14
- "N seconds to reason, then the timer reminds you to act. " +
15
- "Returns the set confirmation — the reminder fires at the deadline.",
10
+ description: DESC("timer"),
16
11
  parameters: {
17
12
  type: "object",
18
13
  properties: {
@@ -38,8 +33,13 @@ export const timerTool = {
38
33
  const expiresAt = Date.now() + seconds * 1000
39
34
  const message = args.message || `⏰ Time's up (${seconds}s). Have you tried running the code, adding a console.log, or checking the output? Thinking more without data is guessing.`
40
35
 
41
- ctx.agent._pendingTimers = ctx.agent._pendingTimers ?? []
42
- ctx.agent._pendingTimers.push({ id: Date.now(), expiresAt, message })
36
+ // §6.30.3 门三件②(D-TW6):在途帽 8——超限显式拒(不静默丢 / 不静默清;形态先例 = 队满
37
+ // 「拒 + 提示 + 保留」)。拒点时在途表零变动(本次不推入)。
38
+ const pending = (ctx.agent._pendingTimers = ctx.agent._pendingTimers ?? [])
39
+ if (pending.length >= TIMER_MAX_PENDING) {
40
+ throw new Error(`timer: ${pending.length} timers already pending (max ${TIMER_MAX_PENDING}) — wait for one to fire before setting another`)
41
+ }
42
+ pending.push({ id: Date.now(), expiresAt, message })
43
43
 
44
44
  return `Timer set for ${seconds} seconds. A reminder will appear when time is up.`
45
45
  },