@thincoder/core 0.9.1

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