thincoder 0.12.59 → 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 (192) hide show
  1. package/CHANGELOG.md +62 -4
  2. package/README.md +10 -8
  3. package/bin/thincoder.mjs +99 -133
  4. package/package.json +6 -4
  5. package/src/abort-provenance.mjs +116 -0
  6. package/src/acp/bridge.mjs +45 -21
  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 +59 -137
  12. package/src/advisor/project-context.mjs +194 -0
  13. package/src/advisor/repos.mjs +17 -40
  14. package/src/advisor/run.mjs +156 -359
  15. package/src/advisor/truncate.mjs +57 -0
  16. package/src/advisor.mjs +27 -7
  17. package/src/agent/completion.mjs +17 -11
  18. package/src/agent/dispatch.mjs +145 -27
  19. package/src/agent/helpers.mjs +107 -13
  20. package/src/agent/record-results.mjs +55 -11
  21. package/src/agent/relay-prefix.mjs +39 -0
  22. package/src/agent/run-stages.mjs +242 -0
  23. package/src/agent/setup-reminders.mjs +69 -0
  24. package/src/agent/setup.mjs +107 -127
  25. package/src/agent/spawn-child.mjs +55 -13
  26. package/src/agent-tools/advisor-async.mjs +346 -0
  27. package/src/agent-tools/advisor-settle.mjs +231 -0
  28. package/src/agent-tools/advisor.mjs +167 -116
  29. package/src/agent-tools/async-settle.mjs +191 -0
  30. package/src/agent-tools/batch-segment.mjs +195 -0
  31. package/src/agent-tools/consult.mjs +139 -107
  32. package/src/agent-tools/design-token.mjs +117 -0
  33. package/src/agent-tools/digest-budget.mjs +76 -0
  34. package/src/agent-tools/eng.mjs +24 -29
  35. package/src/agent-tools/escalate-async.mjs +289 -0
  36. package/src/agent-tools/read-history.mjs +166 -32
  37. package/src/agent-tools/recent-changes.mjs +2 -1
  38. package/src/agent-tools/review-streak.mjs +93 -0
  39. package/src/agent-tools/settings.mjs +137 -34
  40. package/src/agent-tools/subagent-actions.mjs +180 -133
  41. package/src/agent-tools/subagent-async.mjs +184 -177
  42. package/src/agent-tools/subagent-panel.mjs +160 -0
  43. package/src/agent-tools/subagent-run.mjs +205 -0
  44. package/src/agent-tools/subagent-scheduler.mjs +100 -27
  45. package/src/agent-tools/subagent-spawn.mjs +453 -0
  46. package/src/agent-tools/subagent.mjs +256 -578
  47. package/src/agent-tools/verify.mjs +119 -292
  48. package/src/agent-tools.mjs +1 -0
  49. package/src/agent.mjs +89 -205
  50. package/src/cli/distill-command.mjs +12 -6
  51. package/src/cli/make-agent.mjs +26 -8
  52. package/src/cli/memory-command.mjs +4 -3
  53. package/src/cli/permission.mjs +2 -2
  54. package/src/cli/setup-wizard.mjs +42 -17
  55. package/src/completions.mjs +114 -0
  56. package/src/config-migrate.mjs +70 -0
  57. package/src/config.mjs +180 -63
  58. package/src/context.mjs +5 -147
  59. package/src/conventions.mjs +223 -0
  60. package/src/crash-reports.mjs +128 -0
  61. package/src/distill.mjs +11 -11
  62. package/src/expand-home.mjs +16 -0
  63. package/src/explore-distill.mjs +155 -0
  64. package/src/generate-title.mjs +1 -1
  65. package/src/hooks.mjs +7 -3
  66. package/src/memory/code-index.mjs +9 -3
  67. package/src/memory/code-sync.mjs +72 -32
  68. package/src/memory/core.mjs +6 -193
  69. package/src/memory/delete.mjs +236 -0
  70. package/src/memory/docs.mjs +68 -54
  71. package/src/memory/file-walk.mjs +109 -0
  72. package/src/memory/schema.mjs +15 -3
  73. package/src/memory.mjs +3 -1
  74. package/src/model-ref.mjs +66 -0
  75. package/src/model-specs.mjs +42 -8
  76. package/src/peer-domains.mjs +265 -0
  77. package/src/peer-instances.mjs +231 -0
  78. package/src/prompt-overlays.mjs +82 -0
  79. package/src/prompts/advisor-design.md +18 -75
  80. package/src/prompts/advisor-round1.md +14 -67
  81. package/src/prompts/advisor-round2.md +15 -51
  82. package/src/prompts/advisor-round3.md +15 -51
  83. package/src/prompts/common.md +115 -0
  84. package/src/prompts/consult-base.md +5 -23
  85. package/src/prompts/discipline-engineering.md +217 -0
  86. package/src/prompts/discipline-normal.md +179 -0
  87. package/src/prompts/persona-coder.md +21 -0
  88. package/src/prompts/persona-eng-coder.md +37 -0
  89. package/src/prompts/persona-eng-designer.md +55 -0
  90. package/src/prompts/persona-engineering.md +54 -0
  91. package/src/prompts/persona-explore.md +15 -0
  92. package/src/prompts/persona-normal.md +27 -0
  93. package/src/prompts/persona-plan.md +26 -0
  94. package/src/provider/anthropic.mjs +4 -4
  95. package/src/provider/core.mjs +18 -98
  96. package/src/provider/errors.mjs +101 -0
  97. package/src/provider/google.mjs +5 -6
  98. package/src/provider/index.mjs +2 -1
  99. package/src/provider/list-models.mjs +93 -0
  100. package/src/provider/rate.mjs +2 -1
  101. package/src/provider/responses.mjs +5 -3
  102. package/src/provider/retry.mjs +8 -45
  103. package/src/provider/sse.mjs +3 -4
  104. package/src/proxy.mjs +9 -14
  105. package/src/session-gc.mjs +214 -0
  106. package/src/session-guard.mjs +47 -0
  107. package/src/session-rename.mjs +38 -0
  108. package/src/session-slots.mjs +188 -60
  109. package/src/session.mjs +104 -124
  110. package/src/token-ttl.mjs +274 -0
  111. package/src/tools/{system.mjs → bash.mjs} +19 -221
  112. package/src/tools/checklist-sync.mjs +181 -0
  113. package/src/tools/checklist.mjs +52 -39
  114. package/src/tools/edit-batch.mjs +109 -10
  115. package/src/tools/edit-diff.mjs +110 -27
  116. package/src/tools/edit.md +17 -12
  117. package/src/tools/execute.mjs +31 -4
  118. package/src/tools/file.mjs +41 -16
  119. package/src/tools/git.md +1 -1
  120. package/src/tools/git.mjs +23 -34
  121. package/src/tools/glob-dialect.mjs +130 -0
  122. package/src/tools/glob.md +3 -3
  123. package/src/tools/grep.md +1 -1
  124. package/src/tools/index.mjs +9 -8
  125. package/src/tools/ops.mjs +188 -3
  126. package/src/tools/patch.mjs +3 -3
  127. package/src/tools/question.md +4 -0
  128. package/src/tools/question.mjs +26 -0
  129. package/src/tools/read.md +1 -2
  130. package/src/tools/read_image.md +1 -1
  131. package/src/tools/search.mjs +236 -0
  132. package/src/tools/shared.mjs +14 -13
  133. package/src/tools/wait_for.md +22 -0
  134. package/src/tui/agent-turn.mjs +36 -228
  135. package/src/tui/ansi.mjs +2 -0
  136. package/src/tui/clipboard.mjs +7 -1
  137. package/src/tui/cmd-advisor.mjs +3 -2
  138. package/src/tui/cmd-config.mjs +142 -30
  139. package/src/tui/cmd-eng.mjs +28 -40
  140. package/src/tui/cmd-exit.mjs +6 -8
  141. package/src/tui/cmd-mcp.mjs +8 -2
  142. package/src/tui/cmd-model.mjs +14 -12
  143. package/src/tui/cmd-new.mjs +3 -2
  144. package/src/tui/cmd-reindex.mjs +7 -0
  145. package/src/tui/cmd-session.mjs +19 -4
  146. package/src/tui/cmd-submodel.mjs +8 -5
  147. package/src/tui/cmd-think.mjs +10 -10
  148. package/src/tui/cmd-undo.mjs +4 -3
  149. package/src/tui/cmd-upgrade.mjs +19 -4
  150. package/src/tui/config-helpers.mjs +28 -16
  151. package/src/tui/distill-cmd.mjs +1 -1
  152. package/src/tui/index.mjs +40 -38
  153. package/src/tui/interaction.mjs +3 -3
  154. package/src/tui/key-handler.mjs +61 -17
  155. package/src/tui/key-modes.mjs +86 -8
  156. package/src/tui/layout.mjs +18 -10
  157. package/src/tui/model-catalog.mjs +89 -0
  158. package/src/tui/model-picker.mjs +498 -0
  159. package/src/tui/mouse.mjs +52 -9
  160. package/src/tui/pickers.mjs +28 -392
  161. package/src/tui/render-frame.mjs +32 -16
  162. package/src/tui/render-loop.mjs +2 -0
  163. package/src/tui/render-segments.mjs +12 -9
  164. package/src/tui/render.mjs +37 -5
  165. package/src/tui/slash-commands.mjs +2 -2
  166. package/src/tui/startup.mjs +4 -0
  167. package/src/tui/subagent-blocks.mjs +106 -295
  168. package/src/tui/subagent-children.mjs +162 -0
  169. package/src/tui/subagent-freeze.mjs +169 -0
  170. package/src/tui/subagent-panel.mjs +24 -31
  171. package/src/tui/suspension-drive.mjs +297 -0
  172. package/src/tui/tool-args.mjs +7 -5
  173. package/src/tui/tool-display.mjs +143 -0
  174. package/src/tui/tool-events.mjs +56 -185
  175. package/src/tui/tui-lifecycle.mjs +46 -4
  176. package/src/tui/update-notice.mjs +4 -0
  177. package/src/tui/wizard.mjs +61 -21
  178. package/src/tui/wrapped-spawn.mjs +38 -0
  179. package/src/prompts/coder.md +0 -56
  180. package/src/prompts/discipline.md +0 -102
  181. package/src/prompts/eng-coder.md +0 -44
  182. package/src/prompts/engineering-sub.md +0 -34
  183. package/src/prompts/engineering.md +0 -340
  184. package/src/prompts/explore.md +0 -21
  185. package/src/prompts/main.md +0 -56
  186. package/src/prompts/methodology-template.md +0 -58
  187. package/src/prompts/plan.md +0 -16
  188. package/src/prompts/system.md +0 -63
  189. package/src/tools/pdf-parse-text.mjs +0 -497
  190. package/src/tools/pdf-parse-xref.mjs +0 -499
  191. package/src/tools/pdf.mjs +0 -155
  192. package/src/tools/read_pdf.md +0 -21
@@ -0,0 +1,453 @@
1
+ /**
2
+ * subagent-spawn.mjs — spawn 路径装配(2026-09-05 module-split:subagent.mjs 726
3
+ * > 500 硬限——spawn 前置 helpers(summarizeEngTaskBook/effectiveSubagentModel/
4
+ * resolveDesignSlot)+ §20 调度参数准入(prepareScheduling)+ child 装配
5
+ * (buildSpawnChild)verbatim 迁入(仅闭包变量参数化),语义零变;executeAsyncSpawn
6
+ * 另在 subagent-run.mjs。subagent.mjs execute 经本文件 import 调用。
7
+ * 2026-09-07:executeConsumeDesignAction 消费执行器(token 链终消费制——与 spawn 侧
8
+ * slot 族同域——ENGINEERING-MODE.md §2.6 F1——removeDesignTokenSlot 自 token-ttl.mjs)。
9
+ */
10
+
11
+ import { resolve } from "node:path"
12
+ import { existsSync, statSync } from "node:fs"
13
+ import {
14
+ createAgent,
15
+ readonlyToolNames, escapeXml,
16
+ } from "../agent.mjs"
17
+ import { makeRelay, wrapChildCallbacks, relayPrefixOf } from "../agent/spawn-child.mjs"
18
+ import { validateDesignToken } from "./advisor.mjs"
19
+ import { tokenExpired, removeDesignTokenSlot, reconcileEngTokensFromSlot, persistEngTokens } from "../token-ttl.mjs"
20
+ import { resolveChildProvider, buildChildRunOpts, enqueueAsk } from "./subagent-async.mjs"
21
+ import { nextSubagentId } from "./subagent-scheduler.mjs"
22
+ import {
23
+ normalizeFileList, describeBlockers, assertNoDepCycle, depInfo,
24
+ } from "./subagent-scheduler.mjs"
25
+
26
+ /**
27
+ * §18.7 D-TS5 (A2): mechanically summarize the parent spawn task book for the
28
+ * audit spawn — the three audit-relevant elements VERBATIM (design doc paths /
29
+ * affected-file list / acceptance criteria); verbose context/background is
30
+ * dropped (the auditor can read the design docs themselves — they stay
31
+ * available outside this input). Independence preserved: the input is
32
+ * _engTaskInput (mechanically kept by the parent spawn) — never the
33
+ * eng-coder's self-report. Sections are located by header marker, prioritizing
34
+ * header lines (structured task books: "## 文件清单 …") and falling back to
35
+ * inline markers (flat one-line task books); a section runs to the next header
36
+ * of the SAME OR HIGHER level ("## 文件清单" survives a "### 修改" sub-header).
37
+ * Marker not found → the section is reported as missing (never fabricate).
38
+ */
39
+ function summarizeEngTaskBook(taskInput) {
40
+ if (!taskInput) return "(unavailable)"
41
+ const SECTIONS = [
42
+ { name: "Design docs involved", markers: [/Docs? involved/i, /涉及文档/] },
43
+ { name: "Affected-file list", markers: [/Files? (?:list|to (?:modify|change)|modified)/i, /受影响文件/, /文件清单/, /涉及文件/] },
44
+ { name: "Acceptance criteria", markers: [/Acceptance(?: criteria)?/i, /验收标准/] },
45
+ ]
46
+ const lines = taskInput.split("\n")
47
+ const headerLevel = (l) => {
48
+ const m = l.match(/^\s*(#{1,6})\s/)
49
+ return m ? m[1].length : 0
50
+ }
51
+ const headerIdx = lines.map((l, i) => (headerLevel(l) > 0 ? i : -1)).filter((i) => i >= 0)
52
+ const boundsFor = (from, level) => {
53
+ for (const j of headerIdx) {
54
+ if (j > from && (level === 0 || headerLevel(lines[j]) <= level)) return j
55
+ }
56
+ return lines.length
57
+ }
58
+ const out = []
59
+ for (const { name, markers } of SECTIONS) {
60
+ let from = -1
61
+ let level = 0
62
+ for (const i of headerIdx) {
63
+ if (markers.some((m) => m.test(lines[i]))) { from = i; level = headerLevel(lines[i]); break }
64
+ }
65
+ if (from === -1) {
66
+ for (let i = 0; i < lines.length; i++) {
67
+ if (markers.some((m) => m.test(lines[i]))) { from = i; level = 0; break }
68
+ }
69
+ }
70
+ if (from === -1) { out.push(`${name}: (not found in the parent task book)`); continue }
71
+ const body = lines.slice(from, boundsFor(from, level)).join("\n").trim()
72
+ out.push(body || `${name}: (empty section)`)
73
+ }
74
+ return out.join("\n\n")
75
+ }
76
+
77
+ /**
78
+ * Effective subagent model override for a role (CLI parity shared with VS Code):
79
+ * priority — subagent tool `model` arg > config.agent.subagentModels[role] > config.agent.subagentModel > null (inherit parent).
80
+ * The literal "default" (case-insensitive) means "no tool-arg override → run the
81
+ * chain" — same as omitting the parameter (2026-09-05 user ruling).
82
+ */
83
+ export function effectiveSubagentModel(parent, role, modelArg) {
84
+ // "default" alias (2026-09-05 user ruling — ARCHITECTURE.md 子 agent 模型指定):
85
+ // the literal "default", matched case-insensitively, explicitly declares "no
86
+ // override at the tool-arg level → run the default priority chain" (type-level
87
+ // subagentModels[role] → global subagentModel → null = inherit parent). It is
88
+ // equivalent to omitting the parameter / passing ""/null/undefined (which fall
89
+ // through below). Any other value still overrides.
90
+ if (modelArg && String(modelArg).toLowerCase() !== "default") return modelArg
91
+ const cfg = parent.config?.agent ?? {}
92
+ return cfg.subagentModels?.[role] ?? cfg.subagentModel ?? null
93
+ }
94
+
95
+ /**
96
+ * Resolve the design-token slot for an eng-coder spawn (2026-09-01 multi-design, FR3):
97
+ * - designId given → exact slot lookup (no match = explicit error, never a fuzzy guess)
98
+ * - designId omitted → exactly ONE slot must exist (single-design compatibility); with
99
+ * multiple slots we refuse rather than pick one (T16: never silently aim the wrong design)
100
+ * Returns { token } on success; throws with a parent-actionable message otherwise.
101
+ * R16 (2026-09-06): the slot Map is the authority; the format/TTL check itself stays in
102
+ * validateDesignToken (2026-09-06: HMAC 防伪层已删——token 为无签名流程凭证——见
103
+ * ENGINEERING-MODE.md 2026-09-06 段)。
104
+ * DESIGN-TOKEN-SETTLEMENT D2(2026-09-08):内存 Map miss(指定 designId 不在 / 空 Map)
105
+ * 时回读槽文件权威台账 reconcile + 判定(reconcileEngTokensFromSlot——与 dispatch 写门
106
+ * D3 同源)——覆盖"进程重启后槽有但 Map 未及回填/缓存与槽不一致"(槽文件 = 权威台账)。
107
+ * TTL 过滤保留(reconcile 只并入未过期项)。单值镜像 `_engDesignToken` 已随 D3 退役——
108
+ * 本门禁不再读镜像(AC3 零命中)。
109
+ */
110
+ export function resolveDesignSlot(parent, designIdArg) {
111
+ let slots = parent._engDesignTokens
112
+ const hasSlots = slots instanceof Map && slots.size > 0
113
+ // D2:内存 miss(指定 designId 不在 或 空 Map)→ 权威槽回读 reconcile + 判定
114
+ if (designIdArg ? !(hasSlots && slots.has(designIdArg)) : !hasSlots) {
115
+ const rec = reconcileEngTokensFromSlot(parent)
116
+ if (rec && rec !== slots) slots = rec
117
+ }
118
+ // F2d (§29.1): both refusal branches carry the HELD id list — after a
119
+ // persistence restore the parent has no digest to look the id up in; the error
120
+ // list is the only discovery path (ids are not credentials — the token is).
121
+ const size = slots instanceof Map ? slots.size : 0
122
+ const heldIds = size > 0 ? [...slots.keys()].join(", ") : "(none)"
123
+ if (designIdArg) {
124
+ if (!(slots instanceof Map) || !slots.has(designIdArg)) {
125
+ throw new Error(`designId not found — no approved design review holds this id. Run advisor with type='design' again and pass the designId echoed with the token. (session holds ${size} approved design slot(s); held design ids: ${heldIds})`)
126
+ }
127
+ return { token: slots.get(designIdArg) }
128
+ }
129
+ if (size > 1) {
130
+ throw new Error(`Multiple approved designs in this session (${size}) — pass the designId parameter (echoed with each token) to choose which design this eng-coder spawn belongs to. Held design ids: ${heldIds}`)
131
+ }
132
+ if (size === 1) return { token: [...slots.values()][0] }
133
+ throw new Error("Invalid or missing design token — run advisor with type='design' first and pass the returned token as designToken.")
134
+ }
135
+
136
+ /**
137
+ * consume-design 动作执行器(2026-09-07 token 链终消费制——ENGINEERING-MODE.md §2.6 F1):
138
+ * 父侧验收核销时显式调用——读槽值 → removeDesignTokenSlot(token-ttl.mjs——移除该
139
+ * designId 槽)→ 消费后同 designId 再 spawn = resolveDesignSlot not found 机械拒。调用
140
+ * 形态定死(评审 #3):参数 designId(单设计会话可省略——FR3 spawn 同款语义);未知
141
+ * designId 与重复消费同款 no-op 提示(幂等——不报错)。DESIGN-TOKEN-SETTLEMENT D3
142
+ * (2026-09-08):单值镜像 `_engDesignToken` 已退役——无镜像兼容值清/兜底读(AC3)。
143
+ * consume 落盘对称(2026-09-08 D1 段 + AC7):删内存槽后当场 persistEngTokens 同步
144
+ * 落盘删除(旧台账不留盘——防 D2 门禁 miss 回读复活已消费 token);落盘失败回滚
145
+ * 内存槽 + 抛错(不留半消费态,可重试——D1 评审 #1 同款失败语义)。
146
+ * dispatch 分类(评审 #7d):非只读控制动作——depth-0 + 工程模式
147
+ * 限定(受限变体门在 subagent.mjs 分流处;本器自持工程模式门)——planMode 拒绝
148
+ * (dispatch 不豁免)——不入批审批分组(dispatch 免审直行——无文件写)。
149
+ */
150
+ export function executeConsumeDesignAction(args, ctx) {
151
+ const parent = ctx.agent
152
+ if (!parent?.config?.agent?.engineering) {
153
+ throw new Error("Engineering mode is not active — consume-design applies only to engineering-mode design tokens (spawn 同门).")
154
+ }
155
+ const designId = args?.designId ? String(args.designId) : undefined
156
+ const slots = parent?._engDesignTokens
157
+ const hasSlots = slots instanceof Map && slots.size > 0
158
+ // 多槽缺 designId → 拒(spawn 同款语义——不误消费任一槽)
159
+ if (!designId && hasSlots && slots.size > 1) {
160
+ throw new Error(`consume-design: Multiple approved designs in this session (${slots.size}) — pass the designId parameter (echoed with each token) to choose which design to close out.`)
161
+ }
162
+ // 读槽值:给定 designId → 精确槽(未知/已消费 → undefined);缺省 → 唯一槽
163
+ let token = null
164
+ if (designId && hasSlots) token = slots.get(designId) ?? null
165
+ else if (!designId && hasSlots) token = [...slots.values()][0]
166
+ // 未知 designId / 已消费 / 无任何槽 → 幂等 no-op 提示(不报错——评审 #3 定死)
167
+ if (!token) {
168
+ return `consume-design: no live slot${designId ? ` for designId ${designId}` : ""} (already consumed or never issued) — idempotent no-op, nothing changed.`
169
+ }
170
+ // consume 落盘对称(DESIGN-TOKEN-SETTLEMENT D1 段 + AC7——交付 🔴 复活洞修复):
171
+ // 删内存槽后当场同步落盘删除(D1 同款 persistEngTokens——空 Map → 槽文件
172
+ // engDesignTokens 字段删除,旧台账不留盘)。否则消费→回合尾 saveSession 窗口内
173
+ // spawn 门禁 miss 回读(D2)会从盘上复活已消费 token。
174
+ const slotId = designId ?? [...slots.keys()][0]
175
+ removeDesignTokenSlot(parent, designId, token)
176
+ try {
177
+ persistEngTokens(parent)
178
+ } catch (e) {
179
+ // 落盘失败 → 回滚内存槽 + 抛错(不留半消费态——盘上仍有旧台账时消费不得报
180
+ // 成功;可重试——D1 评审 #1 settle 同款失败语义)
181
+ if (slotId) parent._engDesignTokens.set(slotId, token)
182
+ throw new Error(`consume-design: the slot was removed in memory but could NOT be durably deleted from the slot file (${e.message}) — the slot is restored in memory; retry consume-design.`)
183
+ }
184
+ 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.`
185
+ }
186
+
187
+ // ── §20 spawn 调度参数准入(AGENT-LOOP.md §20 D-SD1/D-SD3 + 20.4 round2 #5/#7)──
188
+ // files/dependsOn 声明即契约(v1:不做任务书文本自动解析——不可靠)。缺省(两者皆
189
+ // 缺)= 既有语义零改动(不参与冲突检测/无校验——legacy spawn 零开销直通)。
190
+ // 校验序:参数形态 → 依赖 unknown id(非 consumed 墓碑——T-SD10)→ 依赖环可达
191
+ // (T-SD5——防御断言:自然流程不可达)→ 等待态判定。判定结果:wait/depc 阻塞 →
192
+ // async 入 queued 等位(spawn 返回带 reason——D-SD3b);**sync spawn(async:false)
193
+ // 命中阻塞 → 明确错误——不队列化 sync——sync 语义零变更(round2 #7——T-SD13)**。
194
+ /** §20 准入(参数化提取——2026-09-05 module-split):返回归一化 { files, dependsOn }。
195
+ * filesRaw 目录声明 fail-closed(检测器 throw → 错误即工具结果 JSON)。 */
196
+ export function prepareScheduling(parent, filesRaw, dependsRaw, wantAsync) {
197
+ const files = []
198
+ if (filesRaw !== undefined && filesRaw !== null) {
199
+ try {
200
+ files.push(...normalizeFileList(filesRaw, parent.cwd))
201
+ } catch (e) {
202
+ return { files: [], dependsOn: [], errorJson: JSON.stringify({ status: "error", error: e.message }) }
203
+ }
204
+ }
205
+ if (filesRaw !== undefined && filesRaw !== null && !Array.isArray(filesRaw)) {
206
+ throw new Error("subagent files must be an array of file paths (the write domain this task declares)")
207
+ }
208
+ const dependsOn = []
209
+ if (dependsRaw !== undefined && dependsRaw !== null) {
210
+ if (!Array.isArray(dependsRaw)) throw new Error("subagent dependsOn must be an array of async subagent ids (from prior spawn returns)")
211
+ for (const d of dependsRaw) {
212
+ if (typeof d !== "string" && typeof d !== "number") {
213
+ throw new Error(`subagent dependsOn entries must be async subagent ids — got ${JSON.stringify(d)}`)
214
+ }
215
+ dependsOn.push(String(d))
216
+ }
217
+ }
218
+ if (files.length > 0 || dependsOn.length > 0) {
219
+ for (const d of dependsOn) {
220
+ if (depInfo(parent, d).state === "unknown") {
221
+ throw new Error(`subagent dependsOn: unknown async subagent id: ${d} — dependsOn references ids from prior async spawn returns; an id already consumed (auto-delivered to the model) counts as satisfied, anything else is a mistake (AGENT-LOOP.md §20 D-SD5)`)
222
+ }
223
+ }
224
+ assertNoDepCycle(parent, dependsOn)
225
+ const block = describeBlockers(parent, { _files: files, _dependsOn: dependsOn })
226
+ if (!wantAsync && block.kind !== "slot") {
227
+ throw new Error(`sync spawn (async:false) cannot queue behind a scheduling conflict: ${block.detail} — pass async:true to queue the task (the scheduler starts it when the blockers clear), or wait for them to finish first (AGENT-LOOP.md §20 round2 #7)`)
228
+ }
229
+ }
230
+ return { files, dependsOn, errorJson: null }
231
+ }
232
+
233
+ /**
234
+ * §20 准入通过后的 child 装配(2026-09-05 module-split——自 execute 参数化提取,
235
+ * 原 318-488 段 verbatim——语义零变)。副作用保留:relay 计数器/子代理 _logId/
236
+ * makeRelay 注册/_engTaskInput 携带全部在此发生。返回阻塞/异步两路径共用的
237
+ * { child, input, childOpts, childRunOpts, relayPrefix, childProvider }。
238
+ */
239
+ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, dependsOn, engAuditAttempt) {
240
+ // Provider/model override: tool `model` arg > subagentModels[role] > subagentModel > parent provider
241
+ const childProvider = resolveChildProvider(parent, effectiveSubagentModel(parent, role, args.model))
242
+
243
+ // ── batchDoc gate(ENGINEERING-MODE.md §2.12 + §2.15 D1——FR20 #9 机械面)────────
244
+ // 工程模式下 spawn 工程角色(**eng-coder / eng-designer**——第 2 批扩为角色集合)必传
245
+ // 批次档路径(需求 §1.11 铁律 #5 随件传递的机械面)。落点 = 本装配点(token 门之前):
246
+ // sync 与 async 两条 spawn 路径都经过 buildSpawnChild——一处校验双路生效,错误出口与
247
+ // token 门一致(§2.13.1 选型 A)。
248
+ // 判据只到"参数在 + 路径可读"——**不校验内容/措辞**(不做"已收口"正则、不匹配模板、
249
+ // 不生成;内容够不够由执行者拒收兜底——需求 §1.14 #9 行为面)。路径语义照
250
+ // `files` 先例:cwd 相对或绝对均可,`\` 归一为 `/`。错误文案带**实际角色名**(§2.15
251
+ // 越界文案参数化——designer 撞门时不误导)。
252
+ let batchDocAbs = null
253
+ if (role === "eng-coder" || role === "eng-designer") {
254
+ const given = typeof args.batchDoc === "string" ? args.batchDoc.trim() : ""
255
+ const refusal = (suffix = "") => new Error(
256
+ `batchDoc is required for role='${role}' — pass the batch record path (docs/batches/<batch>-<topic>.md); spawn refused without it.` + suffix)
257
+ if (!given) throw refusal()
258
+ batchDocAbs = resolve(parent.cwd ?? process.cwd(), given.replace(/\\/g, "/"))
259
+ let readable = false
260
+ try { readable = existsSync(batchDocAbs) && statSync(batchDocAbs).isFile() } catch { readable = false }
261
+ if (!readable) throw refusal(" (given path is not a readable file)")
262
+ }
263
+
264
+ // eng-coder token gate: the design review must have passed and the caller must
265
+ // present the exact token advisor issued — otherwise the child is not authorized to code.
266
+ // 2026-09-01: multi-design slots — the token is located by designId (exact slot,
267
+ // single-slot fallthrough); token validation itself is unchanged (2026-09-06:
268
+ // HMAC removed — validateDesignToken now checks format + TTL only).
269
+ let issuedToken
270
+ if (role === "eng-coder") {
271
+ issuedToken = resolveDesignSlot(parent, args.designId).token
272
+ // R16 D-R16c ③ — spawn-gate cleanup: only an EXPIRY rejection removes the slot
273
+ // (the caller passed the slot's own token and it is past TTL — 长跑不重启也清);
274
+ // mismatch / format rejections never delete — the slot's own token may still be
275
+ // valid and a caller error must not destroy it (T-R16d). 单值镜像已退役(D3)——
276
+ // 无镜像同步清。
277
+ const expiredReject = !!issuedToken && args.designToken === issuedToken && tokenExpired(issuedToken)
278
+ if (!issuedToken || args.designToken !== issuedToken || !validateDesignToken(args.designToken)) {
279
+ if (expiredReject) removeDesignTokenSlot(parent, args.designId, issuedToken)
280
+ throw new Error("Invalid or missing design token — run advisor with type='design' first and pass the returned token as designToken.")
281
+ }
282
+ }
283
+
284
+ // Filter tool set by role: explore/plan are read-only (plan is a planning agent, its deliverable is the plan itself)
285
+ let tools
286
+ if (role === "explore" || role === "plan") {
287
+ const allowed = readonlyToolNames(parent.tools)
288
+ tools = parent.tools.filter((t) => allowed.has(t.name))
289
+ } else {
290
+ tools = parent.tools
291
+ }
292
+
293
+ // PROMPT-SYSTEM 施工② G3(2026-09-10):overlay(人格)装载随装配改造退役——人格槽
294
+ // 由 assemblePrompt 的 D1 场景表按 role 承载(persona-{role},explore/coder/plan =
295
+ // PERSONA_NORMAL 同源);child.overlay 恒空(setup 不再前缀叠加)。
296
+ const overlay = ""
297
+
298
+ // explore/plan: force read-only permission; coder/default: AUTO passes through directly,
299
+ // manual mode queues permission requests for the parent agent's approval UI (human in the loop, child agent is no longer silently rejected)
300
+ let childPermission
301
+ if (role === "explore" || role === "plan") {
302
+ childPermission = async () => false
303
+ } else if (parent.autoApprove) {
304
+ childPermission = async () => true
305
+ } else {
306
+ childPermission = async (name, toolArgs) => {
307
+ if (!ctx.onPermissionRequest) return false
308
+ // SYNC-CANCEL v2(用户裁——2026-09-09):工具权限 ask 加 owner key 标识——
309
+ // name `${key}/${tool}`(原 `${role}/${tool}` 无 key——模态 deny 归属判定 +
310
+ // 显示可辨 child);relayPrefix 在 buildSpawnChild 尾部定值——闭包运行于 child
311
+ // 实际 runAgent(buildSpawnChild 返回后)——无 TDZ 风险
312
+ const ownerKey = relayPrefix.slice(0, -1)
313
+ const ask = () => {
314
+ // ⏹ 后(entry.stopped)不再弹模态——直接拒绝(_permQueue 排队 ask 到达时查
315
+ // stopped 旗标——子代理随即在 abort 检出点解绕——v2 模态 deny)
316
+ if (parent._syncChildAborts?.get(ownerKey)?.stopped) return Promise.resolve(false)
317
+ return ctx.onPermissionRequest(`${ownerKey}/${name}`, toolArgs)
318
+ }
319
+ // Queue parallel child agent permission requests to avoid two popups simultaneously overwriting each other (lesson from question tool)
320
+ return enqueueAsk(parent, "_permQueue", ask)
321
+ }
322
+ }
323
+
324
+ // G6(施工②)+ §2.15 B4(第 2 批):工程角色场景即工程纪律——engineering=true 使 setup
325
+ // 场景映射落到 assemblePrompt("eng-coder"/"eng-designer")(persona-{role} + common +
326
+ // discipline-engineering);designer 也必须 engineering=true 才能装配工程纪律槽。
327
+ const engineeringRole = role === "eng-coder" || role === "eng-designer"
328
+ const childConfig = engineeringRole
329
+ ? { ...parent.config, agent: { ...parent.config.agent, engineering: true } }
330
+ : parent.config
331
+
332
+ const child = createAgent({
333
+ provider: childProvider,
334
+ tools,
335
+ config: childConfig,
336
+ cwd: parent.cwd,
337
+ memory: parent.memory,
338
+ overlay,
339
+ role,
340
+ })
341
+
342
+ // §2.20.2 批次档段写入通道绑定(第 4 批):工程角色(eng-coder/eng-designer)把批次档
343
+ // 绝对路径记在 child 上——setup 挂载 batch_segment 时读它。无路径参数的工具靠这条
344
+ // 绑定决定目标档(spawn 门已保证「参数在 + 路径可读」)。
345
+ if (engineeringRole) child._batchDoc = batchDocAbs
346
+
347
+ // Token-verified design review → child is authorized to modify files without re-reviewing
348
+ if (role === "eng-coder") child._engDesignReviewed = true
349
+ // §18 D-E3 task-domain authorization: approved design + spawn task = authorization.
350
+ // The child's OWN tools skip ONLY the onPermissionRequest ask (autoApprove
351
+ // equivalent — dispatch.mjs permission stage); every other gate (JSON parse /
352
+ // unknown tool / planMode / design-token) still applies (T-E14). Non-eng-coder
353
+ // children keep the manual per-write parent approval (human in the loop).
354
+ if (role === "eng-coder") child._engTaskAuthorized = true
355
+ // designId rides the child bookkeeping: the delivery report carries the designId
356
+ // so the divergence-audit fix round re-spawns with the SAME slot (2026-09-01 FR3).
357
+ // DESIGN-TOKEN-SETTLEMENT D3 (2026-09-08): the single-value `_engDesignToken` mirror
358
+ // is retired — no mirror copy on the child (nothing reads it; the slot ledger + the
359
+ // spawn-time token validation are the only authorities).
360
+ if (role === "eng-coder" && issuedToken) {
361
+ child._engDesignId = args.designId ?? null
362
+ }
363
+
364
+ // §18.5 子代理零 git(D-AG1——2026-09-04 用户裁定):explore/plan 一律不注入
365
+ // git 上下文——子代理证据链 = 任务书 ∪ 磁盘当前状态(read/glob/grep)∪(审计时)
366
+ // _touchedFiles,无一项来自 git;注入的全工作区脏状态快照与任务域无关,会误导
367
+ // 审计/探索("status 里这个文件算不算超清单?")。注入分支整体删除(B 方案
368
+ // git 只读变体亦随裁定废弃——D-AG5)。顶层主 agent 注入保留(§3 prepareRun——
369
+ // setup.mjs depth===0——D-AG7 范围边界)。
370
+ let input = args.context ? `Context:\n${args.context}\n\nTask:\n${args.task}` : args.task
371
+ // §2.11 第 1 点(FR16 载体 + FR17 铁律 #5 的机械面):批次档**绝对路径**随任务输入
372
+ // 下发给工程角色(eng-coder 实现 / eng-designer 写稿)——追加一行,任务文本本身不动
373
+ // (批次档 §2 才是任务书本体;本行让子代理"拿到本档路径")。行文不含
374
+ // summarizeEngTaskBook 的三组段 marker(Docs involved / Files list / Acceptance
375
+ // criteria)——段匹配不受影响。仅工程角色注入。
376
+ if (engineeringRole) input += `\n\nBatch record (batchDoc): ${batchDocAbs}`
377
+ // §18 D-E2 ③ (round4 #4, T-E13/T-E15): an eng-coder audit spawn's task book is
378
+ // the eng-coder's OWN spawn task — mechanically kept as _engTaskInput by the
379
+ // parent spawn and injected as the D-TS5 A2 mechanical summary (design docs /
380
+ // affected-file list / acceptance criteria verbatim, verbose context dropped)
381
+ // — ∪ the mechanically tracked _touchedFiles — NEVER the eng-coder's
382
+ // self-written list: a self-report could omit exactly the out-of-scope file
383
+ // the audit must catch.
384
+ if (engAuditAttempt !== null) {
385
+ const touched = (ctx.agent._touchedFiles ?? []).map((f) => `- ${f}`).join("\n") || "- (none yet)"
386
+ input += `\n\n[Audit scope — mechanical context, independent of the eng-coder's self-report:]\n` +
387
+ // §18.7 D-TS4 A1:审计指令模板(四类偏差 + 范围限制 + 校验清单格式)——审计语义
388
+ // 不再靠模型自悟;范围限制是 §18.5 D-AG3 声明(下方 Zero-git scope authority)
389
+ // 的同源一句指注,不重复声明。
390
+ `[Audit instructions — mechanical template (AGENT-LOOP.md §18.7 D-TS4 A1):]\n` +
391
+ `You are auditing an eng-coder delivery against its approved design — audit for EXACTLY these four deviation categories:\n` +
392
+ `- PARTIAL: an acceptance criterion implemented partially or not at all;\n` +
393
+ `- SILENT-SIMPLIFICATION: a "simpler approximation" of a specified behavior substituted for the spec;\n` +
394
+ `- DOC-DRIFT: code changed without the owning design-doc section (module map / affected-files table) updated in the same delivery;\n` +
395
+ `- OUT-OF-LIST: changes outside the approved file list.\n` +
396
+ `Audit scope = _touchedFiles above UNION the files confirmed by the parent task book (single source — the Zero-git scope authority note below, AGENT-LOOP.md §18.5 D-AG3; NOT a second copy): ` +
397
+ `workspace changes not listed there are unrelated to this delivery and are NOT grounds for an out-of-list finding.\n` +
398
+ `Scope discipline (F-TS6 A1): read ONLY the audited files and the design-doc sections relevant to this delivery — do NOT re-read whole documents.\n` +
399
+ `Every deviation item MUST be fieldized: file:line + design reference (doc path + section/AC id) + severity + evidence (quoted code or doc text).\n` +
400
+ // §18.7 D-TS5 A2:任务书从全量 verbatim 改机械摘要块(三要素逐字——排除冗长上下文)。
401
+ `[Parent spawn task book — mechanical summary (AGENT-LOOP.md §18.7 D-TS5 A2): design docs + affected-file list + acceptance criteria verbatim; verbose context/background dropped — the design docs are still available for reading outside this input:]\n` +
402
+ `${summarizeEngTaskBook(ctx.agent._engTaskInput)}\n` +
403
+ `Files actually touched by the eng-coder (mechanical union — audit these against the file list):\n${touched}\n` +
404
+ // §18.5 D-AG3(2026-09-04):审计零 git 范围权威声明——本审计任务零 git(不注入
405
+ // git 上下文——§18.5 全角色零 git);_touchedFiles 为审计范围;工作区未列于
406
+ // _touchedFiles 的改动与本任务无关,不作超清单依据(VS Code auditTaskBook 同款措辞)。
407
+ "Zero-git scope authority (AGENT-LOOP.md §18.5 D-AG3): this audit task receives NO git context — nothing is injected. " +
408
+ "The evidence base is the design documents, the current disk state (read/glob/grep), and the _touchedFiles list above. " +
409
+ "Workspace changes NOT listed in _touchedFiles are unrelated to this delivery — they are NOT grounds for an out-of-file-list finding." +
410
+ // §18.13 D-A1.2:审计预算句——A1 指令模板 + A2 摘要块之后、A3 报告模板之前(定序——评审 #7)。
411
+ // 逐字设计锚(D-A1.2 代码块):只读该读的——10 轮机械预算——超时报 PROBLEM 下结论。
412
+ // 前导 \n 与 A3 同款块分隔约定(上一句 Zero-git 句末无换行——不触碰既有句)。
413
+ `\n[Audit budget — mechanical]: read ONLY the touched files listed above and the design-doc sections the parent task book names (affected-files table, acceptance criteria, status line). Do NOT read whole documents. Budget = 10 tool rounds max — if you cannot conclude within it, report PROBLEM (inconclusive) rather than continuing to explore.\n` +
414
+ // §18.7 D-TS6 A3:审计输出报告格式模板(三态——字段化行——不让模型自由发挥)。
415
+ `\n[Audit report format — mechanical template (AGENT-LOOP.md §18.7 D-TS6 A3):]\n` +
416
+ `Report EXACTLY one of three states:\n` +
417
+ `- CLEAN — no deviation across the four categories: reply the line "Four deviation categories: none found." (四类偏差均未发现);\n` +
418
+ `- DEVIATIONS — one row per deviation, every row fieldized: | category | file:line | design reference | severity | evidence |;\n` +
419
+ `- PROBLEM — the audit itself could not run / inconclusive: state what blocked it.\n`
420
+ }
421
+ // The child's own task input rides the child object: an eng-coder's audit
422
+ // spawns reuse it as the task-book SOURCE — injected as the D-TS5 A2
423
+ // mechanical summary, not verbatim (see above).
424
+ if (role === "eng-coder") child._engTaskInput = input
425
+
426
+ // Relay content/reasoning/tool/output to the parent TUI via the unified spawn-child
427
+ // pipeline (AGENT-LOOP.md §7.2 D3). Prefix includes a unique id: parallel child agents
428
+ // with the same role stay independent and don't overwrite each other.
429
+ // Format: role#id/ → onToken("coder#2/writing..."), onToolCall("coder#2/read", args)
430
+ // Async id allocation (AGENT-LOOP.md §15 D-A1): reserve the relay counter at
431
+ // spawn time — the returned id must be stable while the item sits in the queue.
432
+ // The [model] token (TUI block creation) is DEFERRED to actual start so queued
433
+ // children don't paint an empty panel block ("queued 态不显示").
434
+ let relayPrefix
435
+ if (wantAsync) {
436
+ // SUBAGENT-ID-COUNTER-AGENT(2026-09-09):async id 取号统一走 nextSubagentId
437
+ // (池活续号兜底——counter 载体= agent 本体 _subAgentCounter——跨 run/跨压缩
438
+ // 存活——per-run reset 清单不含它)。sync 分支 makeRelay 不进池——照旧。
439
+ const id = nextSubagentId(parent)
440
+ relayPrefix = relayPrefixOf(role, id)
441
+ } else {
442
+ relayPrefix = makeRelay(parent, role ?? "sub", ctx.callbacks?.onToken, childProvider.model ?? "")
443
+ }
444
+ // LOGGING(LOGGING.md):子代理内部事件(子内 llm:*/tool:*)以 childId 归属——
445
+ // agent._logId 随 runAgent 的 logCtx 透出(主文件单文件全记、按 childId grep)。
446
+ child._logId = relayPrefix.slice(0, -1)
447
+ const childOpts = {
448
+ onPermissionRequest: childPermission,
449
+ ...wrapChildCallbacks(relayPrefix, ctx.callbacks),
450
+ }
451
+ const childRunOpts = buildChildRunOpts(ctx)
452
+ return { child, input, childOpts, childRunOpts, relayPrefix, childProvider }
453
+ }