@thincoder/core 0.10.4 → 0.10.6

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 (153) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/README.md +1 -1
  3. package/advisor/compaction.mjs +11 -6
  4. package/advisor/run.mjs +4 -4
  5. package/agent/assemble.mjs +1 -1
  6. package/agent/chat-call.mjs +1 -1
  7. package/agent/child-marks.mjs +2 -2
  8. package/agent/digest-account.mjs +5 -4
  9. package/agent/dispatch-gates.mjs +8 -0
  10. package/agent/dispatch.mjs +39 -18
  11. package/agent/family-tools.mjs +22 -11
  12. package/agent/helpers.mjs +4 -3
  13. package/agent/light-round.mjs +66 -0
  14. package/agent/post-turn.mjs +8 -4
  15. package/agent/run-stages.mjs +5 -3
  16. package/agent/run-start.mjs +3 -2
  17. package/agent/setup-reminders.mjs +1 -1
  18. package/agent/setup.mjs +18 -3
  19. package/agent/subagent-event.mjs +1 -1
  20. package/agent/suspension.mjs +8 -5
  21. package/agent/timers.mjs +56 -5
  22. package/agent/turn-loop.mjs +5 -3
  23. package/agent/write-gate.mjs +7 -7
  24. package/agent-tools/advisor-async.mjs +2 -2
  25. package/agent-tools/advisor-settle.mjs +2 -2
  26. package/agent-tools/advisor.mjs +1 -1
  27. package/agent-tools/async-discard.mjs +34 -0
  28. package/agent-tools/async-settle.mjs +3 -2
  29. package/agent-tools/audit-block.mjs +1 -1
  30. package/agent-tools/bash-async.mjs +1 -1
  31. package/agent-tools/batch-lifecycle.mjs +35 -14
  32. package/agent-tools/batch-paths.mjs +4 -4
  33. package/agent-tools/batch-skeleton.mjs +29 -11
  34. package/agent-tools/batch.mjs +28 -15
  35. package/agent-tools/browser-async.mjs +198 -0
  36. package/agent-tools/checkpoint.mjs +1 -1
  37. package/agent-tools/design-slots.mjs +232 -0
  38. package/agent-tools/digest-budget.mjs +1 -1
  39. package/agent-tools/manifest.mjs +193 -0
  40. package/agent-tools/parent-channel.mjs +7 -4
  41. package/agent-tools/review-facts.mjs +13 -13
  42. package/agent-tools/settings.mjs +1 -1
  43. package/agent-tools/skill.mjs +1 -1
  44. package/agent-tools/subagent-async.mjs +21 -12
  45. package/agent-tools/subagent-scheduler.mjs +8 -5
  46. package/agent-tools/subagent-spawn.mjs +29 -55
  47. package/agent-tools/subagent.mjs +21 -8
  48. package/agent-tools/timer.mjs +186 -12
  49. package/agent-tools/zero-write-watch.mjs +2 -2
  50. package/agent-tools.mjs +2 -1
  51. package/agent.mjs +1 -1
  52. package/attachments.mjs +1 -1
  53. package/browser/actions.mjs +163 -0
  54. package/browser/cdp.mjs +161 -0
  55. package/browser/clipboard.mjs +142 -0
  56. package/browser/input-actions.mjs +197 -0
  57. package/browser/input.mjs +256 -0
  58. package/browser/launch.mjs +143 -0
  59. package/browser/queue.mjs +171 -0
  60. package/browser/session.mjs +494 -0
  61. package/browser/snapshot.mjs +270 -0
  62. package/config-io.mjs +18 -13
  63. package/config-migrate.mjs +13 -29
  64. package/config-presets.mjs +33 -29
  65. package/config.mjs +68 -25
  66. package/context-degrade.mjs +1 -1
  67. package/context-echo.mjs +1 -1
  68. package/context-push.mjs +2 -2
  69. package/context.mjs +1 -1
  70. package/conventions.mjs +2 -2
  71. package/declaration.mjs +18 -2
  72. package/file-links.mjs +1 -1
  73. package/file-refs.mjs +1 -1
  74. package/history-window.mjs +1 -1
  75. package/i18n.mjs +2 -0
  76. package/ledger-cmd.mjs +62 -16
  77. package/ledger-db.mjs +6 -0
  78. package/ledger-surface.mjs +49 -5
  79. package/ledger-tools.mjs +16 -8
  80. package/ledger.mjs +9 -16
  81. package/log.mjs +1 -1
  82. package/manifest-discovery.mjs +1 -1
  83. package/manifest-schema.mjs +1 -1
  84. package/mcp/transport-stdio.mjs +1 -1
  85. package/mcp.mjs +1 -1
  86. package/memory/code-index.mjs +1 -1
  87. package/memory/code-search.mjs +1 -1
  88. package/memory/code-sync.mjs +1 -1
  89. package/memory/docs.mjs +6 -2
  90. package/memory/file-list.mjs +1 -1
  91. package/memory/file-walk.mjs +17 -5
  92. package/memory/maintain.mjs +410 -0
  93. package/memory/memory-tool.mjs +21 -6
  94. package/memory/schema.mjs +19 -7
  95. package/memory/sweep.mjs +8 -5
  96. package/memory/sync-tail.mjs +16 -10
  97. package/model-ref.mjs +16 -13
  98. package/model-specs.mjs +21 -9
  99. package/package.json +2 -1
  100. package/peer-instances.mjs +1 -1
  101. package/permission.mjs +33 -0
  102. package/process-probe.mjs +2 -2
  103. package/prompts/advisor-design.md +4 -2
  104. package/prompts/advisor-round1.md +1 -1
  105. package/prompts/advisor-round2.md +1 -1
  106. package/prompts/advisor-round3.md +1 -1
  107. package/prompts/common.md +36 -2
  108. package/prompts/consult-base.md +1 -1
  109. package/prompts/discipline-engineering.md +32 -15
  110. package/prompts/discipline-normal.md +46 -2
  111. package/prompts/persona-eng-designer.md +6 -6
  112. package/prompts/persona-engineering.md +14 -6
  113. package/prompts/persona-normal.md +2 -0
  114. package/prompts/subagent-base.md +3 -0
  115. package/provider/core.mjs +6 -0
  116. package/provider/errors.mjs +5 -3
  117. package/provider/google.mjs +9 -8
  118. package/provider/list-models.mjs +3 -2
  119. package/provider/sse.mjs +3 -0
  120. package/provider-flows.mjs +14 -15
  121. package/proxy-chunked.mjs +102 -0
  122. package/proxy-target.mjs +54 -0
  123. package/proxy-transport.mjs +298 -0
  124. package/proxy.mjs +17 -248
  125. package/session-gc.mjs +2 -2
  126. package/session-lifecycle.mjs +18 -14
  127. package/session-slot-claims.mjs +2 -2
  128. package/session-slots-manifest.mjs +1 -1
  129. package/session-slots.mjs +3 -3
  130. package/team-ledger.mjs +282 -0
  131. package/team.mjs +223 -0
  132. package/tool-docs/bash.md +1 -0
  133. package/tool-docs/batch.md +1 -1
  134. package/tool-docs/browser.md +48 -0
  135. package/tool-docs/ledger.md +3 -3
  136. package/tool-docs/manifest.md +16 -0
  137. package/tool-docs/memory.md +1 -0
  138. package/tool-docs/notify_parent.md +5 -0
  139. package/tool-docs/process.md +4 -4
  140. package/tool-docs/question.md +1 -0
  141. package/tool-docs/subagent.md +4 -3
  142. package/tool-docs/team_ledger.md +9 -0
  143. package/tool-docs/timer.md +6 -0
  144. package/tool-docs/wait_for.md +1 -0
  145. package/tools/browser.mjs +161 -0
  146. package/tools/git.mjs +1 -1
  147. package/tools/index.mjs +3 -0
  148. package/tools/ops.mjs +26 -10
  149. package/tools/repomap.mjs +1 -1
  150. package/tools/write-path.mjs +2 -2
  151. package/traces/trace-cleanup.mjs +1 -1
  152. package/traces/trace-store.mjs +1 -1
  153. package/vision-reader.mjs +10 -16
@@ -0,0 +1,193 @@
1
+ /**
2
+ * agent-tools/manifest.mjs — `manifest` 工具:M1 项目状态档(`PROJECT-MANIFEST.json`)的
3
+ * agent 侧读写出口(2026-10-08 · 台账 #1098;权威设计 = `docs/core/design/MANIFEST.md` §2.10)。
4
+ *
5
+ * 三动作(KD-M1-38):
6
+ * - `read` ⇒ `projectView(target)` 五态报明(稳定锚 `state=ok|missing|invalid|ambiguous|no-project`);
7
+ * `ok` 态再 `readManifest(view.root)` 取细节面(`projectView` 面不含 `missingKeys` /
8
+ * `unknownKeys`)——**不抛**(缺档报明,非拒)。
9
+ * - `init` ⇒ 缺档建默认档(`initManifest`——内容 = `DEFAULT_MANIFEST`);**已带档 ⇒ 拒**
10
+ * (不覆盖——`initManifest` 本体无覆盖保护,工具面补此闸)。
11
+ * - `write` ⇒ **点改**:读现档 → 设节点 → 落盘(`writeManifest` 内先 `validateManifest`——
12
+ * 编辑结果非法 ⇒ 拒 + 盘零变);**缺档 ⇒ 拒** + 引导 `init`(写不建档)。
13
+ *
14
+ * 写面白名单(判据单源 = `MANIFEST_SCHEMA`——钉死谓词):`keys` 中**非** `nestedKeys` 父键者
15
+ * (`version` / `phase` / `promptsLanding` / `codePaths` 整键)+ `nestedKeys` 子键
16
+ * (`docRoot.*` / `checkConfig.*` / `index.*`(含 `publicRepos`)/ `advisor.*`);**四族整键写
17
+ * (传整个对象)⇒ 拒**;白名单外 ⇒ 拒(拼写保护——未知键写盘 = 静默死键,KD-M1-36 问题本体)。
18
+ * `value` 解析 = 同款口径 `settings.mjs:216-228`(先 `JSON.parse`,失败取字面串;`"null"` ⇒ `null`
19
+ * ——`docRoot.*` 的「本面无」,KD-M1-37)。
20
+ *
21
+ * 目标面(KD-M1-39——判据复用零新解析器):`read` = `projectView`;`init` / `write` =
22
+ * `projectRootView`(歧义 ⇒ 拒 + 候选全列——不猜);`resolveProjectRoot` 对歧义塌缩 `null` 再回落 cwd
23
+ * ⇒ 写 / 建两动作**不得**直用。
24
+ *
25
+ * 写路径复用(零新写路径):落盘恒经 `writeManifest` / `initManifest`——`writer` 恒显式 `'main'`
26
+ * (缺省 `'subagent'` = 写门 fail-closed 缺省,KD-M1-3 / AC-M1-5 零改)。
27
+ *
28
+ * 报告面随动(KD-M1-40):本工具**不写** `agent.manifest`(第二值源否——工具目标可为非锚项目)——
29
+ * 写盘 = 盘面变化,情境行经既有 ③b mtime 门控于下回合采纳(`agent/setup-reminders.mjs`)。
30
+ */
31
+ import { existsSync } from "node:fs"
32
+ import { join, resolve } from "node:path"
33
+ import { MANIFEST_REL, MANIFEST_SCHEMA, initManifest, projectRootView, projectView, readManifest, writeManifest } from "../manifest.mjs"
34
+ import { DESC } from "../tools/shared.mjs"
35
+
36
+ /** 拒面统一前缀(设计 §2.10:`manifest <action>: refused — …`)——盘零变类拒绝一律经此抛错。 */
37
+ function refuse(action, reason) {
38
+ return new Error(`manifest ${action}: refused — ${reason}`)
39
+ }
40
+
41
+ /** `target` 解析(先例 `eng.mjs:81` 缺省会话锚 ∥ `git.mjs:32-35` 相对形归一):缺省 = 会话锚
42
+ * (`ctx.agent.cwd ?? ctx.cwd ?? process.cwd()`);显式相对形按会话锚归一(绝对形原样)。 */
43
+ function resolveTarget(args, ctx) {
44
+ const base = ctx?.agent?.cwd ?? ctx?.cwd ?? process.cwd()
45
+ return args?.target ? resolve(base, String(args.target)) : resolve(base)
46
+ }
47
+
48
+ /** 写面白名单谓词(**钉死**——设计 §2.10):点分路径恰两形——
49
+ * ① 单段:`keys` ∩ ∉ `nestedKeys`(= `version` / `phase` / `promptsLanding` / `codePaths` 整键);
50
+ * ② 双段:`nestedKeys[父]` 含子键(四族子键);其余(四族整键写 / 未知键 / 更深路径)⇒ 拒。
51
+ * `Array.isArray` 守卫:原型链名(`__proto__` / `constructor` / `toString` 类)取到非数组 ⇒ 落拒面
52
+ * (不得 TypeError 直出——白名单外一律 `refused` 统一前缀)。 */
53
+ function isWritableKey(key) {
54
+ const segs = String(key).split(".")
55
+ if (segs.length === 1) return MANIFEST_SCHEMA.keys.includes(segs[0]) && !Object.hasOwn(MANIFEST_SCHEMA.nestedKeys, segs[0])
56
+ if (segs.length === 2) {
57
+ const subs = MANIFEST_SCHEMA.nestedKeys[segs[0]]
58
+ return Array.isArray(subs) && subs.includes(segs[1])
59
+ }
60
+ return false
61
+ }
62
+
63
+ /** `value` 解析(同款口径 = `settings.mjs:216-228`):先 `JSON.parse`(`"null"` ⇒ `null`;数值 /
64
+ * 布尔 / 数组皆 JSON 形),失败取字面串(裸串如 `docs/design`)。 */
65
+ function parseValue(raw) {
66
+ const s = String(raw)
67
+ try { return JSON.parse(s) } catch { return s }
68
+ }
69
+
70
+ /** 点分路径设值(自动建中间对象——settings `setKeyPath` 同款;白名单谓词已保父段为已知族)。 */
71
+ function setKeyPath(obj, path, value) {
72
+ const segs = String(path).split(".")
73
+ let cur = obj
74
+ for (let i = 0; i < segs.length - 1; i++) {
75
+ if (cur[segs[i]] === null || typeof cur[segs[i]] !== "object") cur[segs[i]] = {}
76
+ cur = cur[segs[i]]
77
+ }
78
+ cur[segs[segs.length - 1]] = value
79
+ }
80
+
81
+ /** 歧义拒文案(候选全列——不猜;init / write 共用):多候选 = 显式指定 target,机制不代选。 */
82
+ function ambiguousReason(target, candidates) {
83
+ return `ambiguous target ${target} — ${candidates.length} candidate projects (the mechanism never picks one); pass target explicitly:\n` +
84
+ candidates.map((c) => `- ${c}`).join("\n")
85
+ }
86
+
87
+ /** read 动作(五态报明——**不抛**):`state=` 稳定锚 + 各态附行(ok 细节面 / missing 引导 /
88
+ * ambiguous 候选全列 / invalid 错误行)。 */
89
+ function readAction(target) {
90
+ const view = projectView(target)
91
+ const lines = [`state=${view.state}`]
92
+ if (view.path) lines.push(`path=${view.path}`)
93
+ if (view.state === "ok") {
94
+ const m = readManifest(view.root) // 细节面:missingKeys / unknownKeys / errors(非空才出)
95
+ lines.push(`manifest=${JSON.stringify(m.manifest)}`)
96
+ if (m.missingKeys?.length) lines.push(`missingKeys=${m.missingKeys.join(", ")}`)
97
+ if (m.unknownKeys?.length) lines.push(`unknownKeys=${m.unknownKeys.join(", ")}`)
98
+ if (m.errors?.length) lines.push(`errors=${m.errors.join(";")}`)
99
+ } else if (view.state === "missing") {
100
+ lines.push(`hint: run manifest action=init to create the default manifest here`)
101
+ } else if (view.state === "no-project") {
102
+ lines.push(`hint: no project on the ancestor chain and none below — manifest action=init can create one at the target`)
103
+ } else if (view.state === "ambiguous") {
104
+ lines.push(`candidates (${(view.candidates ?? []).length}):`)
105
+ for (const c of view.candidates ?? []) lines.push(`- ${c}`)
106
+ lines.push(`hint: pass target explicitly — the mechanism never picks one`)
107
+ } else if (view.state === "invalid") {
108
+ if (view.errors?.length) lines.push(`errors=${view.errors.join(";")}`)
109
+ }
110
+ return lines.join("\n")
111
+ }
112
+
113
+ export const manifestTool = {
114
+ name: "manifest",
115
+ description: DESC("manifest"),
116
+ parameters: {
117
+ type: "object",
118
+ properties: {
119
+ action: { type: "string", enum: ["read", "init", "write"], description: "read — current manifest + validation state (five-state anchor, never throws); init — create the default manifest when missing (refused when one already exists); write — point-edit one node (key + value) of an existing manifest" },
120
+ target: { type: "string", description: "Explicit target directory — defaults to the session anchor; relative paths resolve against it. Ambiguous targets are refused with the full candidate list (never picks one)." },
121
+ key: { type: "string", description: "(write) Dot path of the node to set — version / phase / promptsLanding / codePaths, or a sub-key of docRoot / checkConfig / index / advisor (e.g. docRoot.design / index.publicRepos / advisor.docMap); whole-object writes of those four families are refused", },
122
+ value: { type: "string", description: "(write) New value — parsed as JSON first (null / true / 5 / [\"a\"]), otherwise kept as a literal string; \"null\" is the explicit no-such-face for docRoot.<key>" },
123
+ },
124
+ required: ["action"],
125
+ },
126
+ readonly: false, // 动作级分类(dispatch-gates `isSubagentReadonlyAction`):read = 只读类;init / write 保持侧效门
127
+ async execute(args, ctx) {
128
+ const action = args?.action
129
+ if (!["read", "init", "write"].includes(action)) {
130
+ throw new Error(`manifest: action must be one of read/init/write — got ${JSON.stringify(action)}`)
131
+ }
132
+ const target = resolveTarget(args, ctx)
133
+
134
+ if (action === "read") return readAction(target)
135
+
136
+ // init / write:目标面 = `projectRootView`(歧义直读形——不猜;KD-M1-39)
137
+ const rootView = projectRootView(target)
138
+ if (rootView.state === "ambiguous") {
139
+ throw refuse(action, ambiguousReason(target, rootView.candidates))
140
+ }
141
+
142
+ if (action === "init") {
143
+ const root = rootView.state === "ok" ? rootView.root : target // 梯⑤(无项目)⇒ 落点 = target 自身
144
+ const file = join(root, MANIFEST_REL)
145
+ if (existsSync(file)) {
146
+ throw refuse("init", `${file} already carries a manifest — init never overwrites (use action=write to edit it)`)
147
+ }
148
+ try {
149
+ initManifest(root, { writer: "main" }) // 落盘恒经写门(writer 闸 + 校验前闸——零新写路径)
150
+ } catch (e) {
151
+ throw refuse("init", `could not write the default manifest at ${file} — ${e?.message ?? String(e)}`)
152
+ }
153
+ return `manifest init: created default manifest (DEFAULT_MANIFEST)\npath=${file}`
154
+ }
155
+
156
+ // write:缺档 ⇒ 拒 + 引导 init(写不建档——多写权面唯一的建档动作 = init)
157
+ if (rootView.state === "none") {
158
+ throw refuse("write", `no project at ${target} — write requires an existing ${MANIFEST_REL}; run manifest action=init first (write never creates one)`)
159
+ }
160
+ const root = rootView.root
161
+ const file = join(root, MANIFEST_REL)
162
+ const key = args?.key
163
+ if (!key) throw new Error("manifest write: key is required (dot path, e.g. docRoot.design)")
164
+ if (args?.value === undefined) throw new Error("manifest write: value is required (JSON-parsed first, otherwise a literal string)")
165
+ if (!isWritableKey(key)) {
166
+ throw refuse("write", `key "${key}" is not writable — writable nodes: version / phase / promptsLanding / codePaths + sub-keys of docRoot / checkConfig / index / advisor (whole-object writes of the four families are refused)`)
167
+ }
168
+ let current
169
+ try {
170
+ current = readManifest(root)
171
+ } catch (e) {
172
+ throw refuse("write", `could not read ${file} — ${e?.message ?? String(e)}`)
173
+ }
174
+ if (!current.ok) {
175
+ if (current.reason === "missing") {
176
+ throw refuse("write", `${file} does not exist — run manifest action=init first (write never creates one)`)
177
+ }
178
+ if (!current.manifest) {
179
+ throw refuse("write", `${file} is not editable (fail-closed): ${(current.errors ?? []).join(";")}`)
180
+ }
181
+ // 现档可解析但校验非法:照常点改——编辑结果仍经落盘前 `validateManifest`(非法 ⇒ 拒 + 盘零变)
182
+ }
183
+ const value = parseValue(args.value)
184
+ const manifest = current.manifest
185
+ setKeyPath(manifest, String(key), value)
186
+ try {
187
+ writeManifest(root, manifest, { writer: "main" }) // 落盘前 validateManifest(编辑结果非法 ⇒ 拒 + 盘零变)
188
+ } catch (e) {
189
+ throw refuse("write", `the edited manifest fails the pre-write validation (disk unchanged) — ${e?.message ?? String(e)}`)
190
+ }
191
+ return `manifest write: set ${key} = ${JSON.stringify(value)}\npath=${file}`
192
+ },
193
+ }
@@ -20,7 +20,7 @@
20
20
  * 生命周期与子代理生命周期**解耦**(settle / cancel 不迁移、不清队列——F3)。
21
21
  * - 父侧唤醒 = ask 入队尾调 `wakeAsyncWaiters(parent)`(**同步**,零 await;`note` 不唤醒)——
22
22
  * 零新字段 / 零新容器 / 零新注册:等待栓数组由挂起驱动注册,非挂起期数组空 ⇒ no-op。
23
- * - 父侧消费 = `drainChildUpstream(agent)`(`thincoder-core/agent.mjs` 循环头单点,紧邻
23
+ * - 父侧消费 = `drainChildUpstream(agent)`(`thincoder-core/agent/turn-loop.mjs` 循环头单点,紧邻
24
24
  * `consumeInjected?.(agent)`——同址反向):全部 pending **合并一条** user 消息(`pushReal`
25
25
  * 非 transient——事务性事件落盘,D-UC5),每条附结束注脚(已 settle / 已 cancel 两形态;
26
26
  * 其余态不附——不臆断)。
@@ -151,10 +151,13 @@ function endNote(agent, entry) {
151
151
  return ""
152
152
  }
153
153
 
154
+ /** kind → 投递标识(单点渲染——机读面 / 日志留原 kind;用户 2026-10-06 裁定逐字)。 */
155
+ const KIND_MARK = { ask: "[上抛·待裁]", note: "[上抛·知会]" }
156
+
154
157
  /**
155
- * 父侧消费点(回合边界单点——`agent.mjs` 循环头;空队列 no-op = 零历史变更、零开销):
158
+ * 父侧消费点(回合边界单点——`agent/turn-loop.mjs` 循环头;空队列 no-op = 零历史变更、零开销):
156
159
  * 全部 pending 消息**合并为一条** user 消息注入(`pushReal`——不带 `transient`,事件落盘),
157
- * 按入队序逐条列示(来源 `role#id` + `kind` + message(`escapeXml`)+ 结束注脚)。
160
+ * 按入队序逐条列示(来源 `role#id` + **kind 标识**(`ask` ⇒ `[上抛·待裁]` ∥ `note` ⇒ `[上抛·知会]`)+ message(`escapeXml`)+ 结束注脚)。
158
161
  * @param {object} agent 父(消费方)agent 形态。
159
162
  * @returns {number} 本次消费条目数(0 = 空队列 no-op)。
160
163
  */
@@ -168,7 +171,7 @@ export function drainChildUpstream(agent) {
168
171
  + "Answer with subagent action:'send' (id + message) if the decision is yours; an unanswered ask means that child skips the part and reports it as not done.]"
169
172
  : `[System reminder: in-flight message from your subagent ${entries[0].from} — it keeps working on the unaffected parts. `
170
173
  + "Answer with subagent action:'send' (id + message) if the decision is yours; an unanswered ask means the child skips that part and reports it as not done.]"
171
- const rows = entries.map((e) => `${many ? "- " : ""}${e.kind} · ${e.from}: ${escapeXml(e.message)}${endNote(agent, e)}`)
174
+ const rows = entries.map((e) => `${many ? "- " : ""}${KIND_MARK[e.kind] ?? e.kind} · ${e.from}: ${escapeXml(e.message)}${endNote(agent, e)}`)
172
175
  pushReal(agent, { role: "user", content: [header, ...rows].join("\n") })
173
176
  return entries.length
174
177
  }
@@ -6,7 +6,7 @@
6
6
  * - 2026-10-04(工具路径基面根治批 · #921):import 面扩为「node:* + manifest 发现/声明族
7
7
  * (叶向——manifest 族零依赖 advisor 面,零新模块边;KD-2)」——`REVIEW_ROOT_KEYS` +
8
8
  * `resolveReviewRootsFor` 判定本体自 `agent/write-gate.mjs` 迁入(write-gate 同名再出口保
9
- * 既有 import 面),新增 `resolveReviewDocPaths` 文档解析四腿单源。
9
+ * 既有 import 面),新增 `resolveReviewDocPaths` 文档解析四分支单源。
10
10
  */
11
11
  import { existsSync, statSync } from "node:fs"
12
12
  import { dirname, join, resolve } from "node:path"
@@ -50,23 +50,23 @@ function readableFile(abs) {
50
50
  try { return existsSync(abs) && statSync(abs).isFile() } catch { return false }
51
51
  }
52
52
 
53
- /** 会话根集(腿①判据集——既有 advisor 面 `resolveReviewTargetPaths(agent)` 目录形态的函数式封装;档内私有——零外部消费面)。 */
53
+ /** 会话根集(分支①判据集——既有 advisor 面 `resolveReviewTargetPaths(agent)` 目录形态的函数式封装;档内私有——零外部消费面)。 */
54
54
  function sessionReviewRoots(cwd) {
55
55
  return resolveReviewRootsFor(cwd ?? process.cwd())
56
56
  }
57
57
 
58
58
  /**
59
- * **文档解析四腿单源**(2026-10-04 · 批档 §2.2 A 本体):逐文档
60
- * 腿① 零变:normAbs(doc, cwd) 落会话根集 ⇒ 受理;腿② 零变:所属项目根集命中 ⇒ 受理;
61
- * 腿③ 根治面:相对形 ∧ 前两腿未中 ⇒ 候选底座逐个试探(底座 = projectRootView(cwd) 现行集 +
59
+ * **文档解析四分支单源**(2026-10-04 · 批档 §2.2 A 本体):逐文档
60
+ * 分支① 零变:normAbs(doc, cwd) 落会话根集 ⇒ 受理;分支② 零变:所属项目根集命中 ⇒ 受理;
61
+ * 分支③ 根治面:相对形 ∧ 前两分支未中 ⇒ 候选底座逐个试探(底座 = projectRootView(cwd) 现行集 +
62
62
  * cwd 祖先链全量目录——自父目录起至盘根、最近→远、去重、现行集在前)——resolve(c, doc)
63
- * 所属项目根集命中 = 结构命中;绝对形不进本腿(未中 = 真越界,无候选可试);
64
- * 腿④ 歧义归一(KD-3):结构命中 ≥2 ⇒ 可读文件存在性唯一化(batch-paths 读面「首个可读」
63
+ * 所属项目根集命中 = 结构命中;绝对形不进本分支(未中 = 真越界,无候选可试);
64
+ * 分支④ 歧义归一(KD-3):结构命中 ≥2 ⇒ 可读文件存在性唯一化(batch-paths 读面「首个可读」
65
65
  * 同款判据):恰一可读 ⇒ 胜;≥2 可读 ⇒ fail-closed 拒(scope-doc-ambiguous 列全部
66
66
  * 可读命中);0 可读 ⇒ not-doc 拒(诊断列全部结构命中)。
67
67
  * @returns {{ resolved: Map<string,string>, invalid: Array<{doc, attempted:string[]}>,
68
68
  * ambiguous: {doc, hits:string[]}|null, candidates: string[] }} ambiguous = 首个歧义文档
69
- * (fail-closed 首拒——消费面拒发即止);candidates = 腿③候选集(B 文案候选提示同源)。
69
+ * (fail-closed 首拒——消费面拒发即止);candidates = 分支③候选集(B 文案候选提示同源)。
70
70
  * 非字符串 / 空白串项**静默跳过**(同 docSetKey 既有过滤语义——不进 resolved/invalid)。
71
71
  */
72
72
  export function resolveReviewDocPaths(documents, cwd) {
@@ -97,11 +97,11 @@ export function resolveReviewDocPaths(documents, cwd) {
97
97
  if (typeof doc !== "string" || !doc.trim()) continue
98
98
  const isAbs = /^[a-zA-Z]:[\\/]/.test(doc) || doc.startsWith("/") || doc.startsWith("\\\\")
99
99
  const n = normAbs(doc, cwd)
100
- // 腿① 会话根集 / 腿② 所属项目根集(既有行为零变——判定序同 advisor.mjs 旧面)。
100
+ // 分支① 会话根集 / 分支② 所属项目根集(既有行为零变——判定序同 advisor.mjs 旧面)。
101
101
  if (under(n, sessionRoots)) { resolved.set(doc, n); continue }
102
102
  const owner = owningProject(n)
103
103
  if (owner && under(n, ownRoots(owner))) { resolved.set(doc, n); continue }
104
- // 腿③ 候选项目根试探(相对形 only——绝对形未中 = 真越界,无候选可试)。
104
+ // 分支③ 候选项目根试探(相对形 only——绝对形未中 = 真越界,无候选可试)。
105
105
  let hits = []
106
106
  if (!isAbs && candidates.length > 0) {
107
107
  for (const c of candidates) {
@@ -110,8 +110,8 @@ export function resolveReviewDocPaths(documents, cwd) {
110
110
  if (cOwner && under(t, ownRoots(cOwner))) hits.push(t)
111
111
  }
112
112
  }
113
- // 腿④ 歧义归一:结构命中 ≥2 ⇒ 可读文件存在性唯一化;恰 1 命中 ⇒ 受理(存在性仅作
114
- // 歧义唯一化判据——单命中无判别力,同腿①声明根集受理面一致不查存在)。
113
+ // 分支④ 歧义归一:结构命中 ≥2 ⇒ 可读文件存在性唯一化;恰 1 命中 ⇒ 受理(存在性仅作
114
+ // 歧义唯一化判据——单命中无判别力,同分支①声明根集受理面一致不查存在)。
115
115
  if (hits.length >= 2) {
116
116
  const readable = hits.filter((h) => readableFile(h))
117
117
  if (readable.length === 1) { resolved.set(doc, readable[0]); continue }
@@ -141,7 +141,7 @@ export function noteReviewDelivered(agent, run) {
141
141
  run.historyAnchorIdx = Array.isArray(agent?.history) ? agent.history.length : 0
142
142
  }
143
143
 
144
- /** 腿③候选底座(`MANIFEST.md` §2.5「路径归一增量」族第四笔 · 2026-10-05 · #945):现行
144
+ /** 分支③候选底座(`MANIFEST.md` §2.5「路径归一增量」族第四笔 · 2026-10-05 · #945):现行
145
145
  * `projectRootView(cwd)` 集(ok → [root];ambiguous → candidates 按名序;none → 空集)+
146
146
  * cwd 祖先链全量目录(自父目录起至盘根、最近→远、去重、现行集在前)——容器相对形在子仓锚下
147
147
  * 可解析;受理判据(目标解析位最近 manifest 声明面)∥ 围栏 ∥ 可读唯一化 ∥ fail-closed 歧义零改。 */
@@ -44,7 +44,7 @@ const _NULL_LEAF_SHAPES = {
44
44
  "memory.team": { kind: "team", expects: "object { repo: string, name?, dir? }" },
45
45
  }
46
46
 
47
- /** 同族/跨端形状表(**存在性断言面**——不参与完备性锁的集合相等)。W3 同族:`agent.subagentModels`
47
+ /** 同族/跨端形状表(**存在性断言**——不参与完备性锁的集合相等)。W3 同族:`agent.subagentModels`
48
48
  * 默认 `{}` ⇒ 派生表零条目 ⇒ 零约束(字符串被静默忽略——回落 subagentModel)。 */
49
49
  const _SIBLING_SHAPES = {
50
50
  "agent.subagentModels": { kind: "roleMap", expects: "object of role→non-empty string" },
@@ -2,7 +2,7 @@ import { loadSkills, readSkill } from "../skills.mjs"
2
2
  import { escapeXml } from "../agent.mjs"
3
3
  import { DESC } from "../tools/shared.mjs"
4
4
 
5
- // ─── skill loader 注入缝(#88——「同步 loader 面按核内结构归一」的端侧覆盖面)──────────
5
+ // ─── skill loader 注入缝(#88——「同步 loader 面按核内结构归一」的端侧覆盖范围)──────────
6
6
  /**
7
7
  * skill loader 注入位(**缺省不覆盖** = 核内异步 loader——CLI 语义,零行为变;端装配层
8
8
  * 可覆盖为端形态(如核内同步面 `loadSkillsSync` / `readSkillSync` 的绑定)。核内零端名分支
@@ -38,6 +38,8 @@ import { getAsyncPool, tombstoneOf, writeTombstone } from "./async-settle.mjs"
38
38
  import { noteReviewDelivered } from "./review-facts.mjs"
39
39
  // #9(TOOLS.md §6.19):后台 bash 任务注入器(digest 单点族按 role="bg" 分发到本档)。
40
40
  import { injectBgResult } from "./bash-async.mjs"
41
+ // 批 browser-async-fix:后台浏览器动作注入器(同族——role="browser" 分发到 browser-async.mjs)。
42
+ import { injectBrowserResult } from "./browser-async.mjs"
41
43
 
42
44
  // agent-tools 共享:并行子代理的审批/继续弹窗经 owner 上命名 promise 链串行——
43
45
  // 永不叠弹窗(返回链供调用方 .then 续接)。
@@ -131,13 +133,15 @@ export function runningPoolCount(parent, pool) {
131
133
  /**
132
134
  * Resolve the sub-agent's provider from a model override string (shared with the
133
135
  * VS Code port). Forms accepted:
134
- * "provider:model" → the named provider with the named model
135
- * "provider" → the named provider's configured model
136
+ * "provider:model" → the named provider with the named model (first-colon split —
137
+ * "a:b:c" keeps model "b:c"; multi-colon model names intact)
136
138
  * "model" → same provider as the parent, different model
137
139
  * "default" → same as null: the parent's provider unchanged (explicit
138
140
  * "use the default model" — 2026-09-05 user ruling; matched
139
141
  * case-insensitively — parser-layer alias guard)
140
142
  * null → parent's provider unchanged.
143
+ * Rejected: bare provider names (2026-10-09 清除批——渠道无默认模型,无渠道级 model 可派生;
144
+ * 可读错误「渠道无默认模型,请用 provider:model」) ∥ "provider:" 空模型段。
141
145
  * API keys come from config.json only (env vars are not a key source).
142
146
  */
143
147
  export function resolveChildProvider(parent, modelArg) {
@@ -145,29 +149,32 @@ export function resolveChildProvider(parent, modelArg) {
145
149
  // #861(AGENT-LOOP-SUBAGENT.md §6.7.1 ②)运行期防御——四消费链汇点(config ∥ tool model 参数 ∥
146
150
  // consult ∥ vision-reader):其余非字符串(对象 ∕ 数组 ∕ 数字 ∕ 布尔)明确报错(不裸 TypeError——可自纠)。
147
151
  if (typeof modelArg !== "string") {
148
- throw new Error(`subagent model override must be a string ("provider:model" | provider | model); got ${Array.isArray(modelArg) ? "array" : typeof modelArg}`)
152
+ throw new Error(`subagent model override must be a string ("provider:model" | model); got ${Array.isArray(modelArg) ? "array" : typeof modelArg}`)
149
153
  }
150
154
  // "default" alias (2026-09-05 user ruling — ARCHITECTURE.md 子 agent 模型指定):
151
155
  // the literal "default", matched case-insensitively, declares "no override →
152
156
  // inherit the default model" — equivalent to null/omission (parent provider
153
157
  // unchanged). Checked BEFORE the provider-name lookup so the alias is reserved
154
158
  // (≡ omission — providers are not consulted) and never falls through to the
155
- // model-name swap branch. Any other single-segment value keeps the legacy
156
- // semantics below.
159
+ // resolution branches below. Any other value runs the resolution below.
157
160
  if (String(modelArg).toLowerCase() === "default") return { ...parent.provider }
158
161
  const providers = parent.config?.providersList ?? []
159
162
  const withKey = (p) => (p.apiKey?.trim() ? { ...p, apiKey: p.apiKey.trim() } : { ...p })
160
- if (modelArg.includes(":")) {
161
- const [pname, mname] = modelArg.split(":")
163
+ // 首冒号分割(2026-10-09 清除批):多冒号模型名不截断——`a:b:c` ⇒ provider `a` ∥ model `b:c`。
164
+ const sep = modelArg.indexOf(":")
165
+ if (sep >= 0) {
166
+ const pname = modelArg.slice(0, sep)
167
+ const mname = modelArg.slice(sep + 1)
162
168
  const p = providers.find((x) => x.name === pname)
163
169
  if (!p) throw new Error(`subagent model: unknown provider "${pname}" (available: ${providers.map((x) => x.name).join(", ") || "none"})`)
164
- return { ...withKey(p), model: mname || p.model }
170
+ // 空 mname 一律拒(2026-10-09 清除批):渠道不携模型,无渠道级 model 可回落。
171
+ if (!mname) throw new Error(`subagent model: "${modelArg}" — model part is empty (expected provider:model)`)
172
+ return { ...withKey(p), model: mname }
165
173
  }
166
174
  const byName = providers.find((x) => x.name === modelArg)
167
- // F-2c (MODEL-400-FIX):裸渠道名克隆须重派生 model——渠道裸克隆会丢 model 键 → 无 model 请求
168
- // → serde 400。MODEL-SELECTION v2(M3④):取渠道默认模型(`provider.model` 单值——2026-09-10
169
- // 起;老 models[] 首候选形态已退场);无默认模型 → 主 provider 的 model 兜底(T28)。
170
- if (byName) return { ...withKey(byName), model: byName.model ?? parent.provider?.model }
175
+ // 2026-10-09 清除批(PROVIDER.md §6.16 M3):渠道单值模型退场——裸渠名无从派生 model,
176
+ // 一律拒(不回落任何渠道级 model)。
177
+ if (byName) throw new Error("渠道无默认模型,请用 provider:model")
171
178
  return { ...parent.provider, model: modelArg }
172
179
  }
173
180
 
@@ -368,6 +375,8 @@ export async function injectAsyncResult(agent, entry) {
368
375
  // #9:后台 bash 任务族(role="bg")单列——专用注入器(状态行 + 尾截 + 全量在 log);
369
376
  // 本函为 digest 注入单点族成员,消费面(run-start pending / 端壳残余注入)零改。
370
377
  if (entry.role === "bg") return injectBgResult(agent, entry)
378
+ // 批 browser-async-fix:后台浏览器动作族(role="browser")同式单列(摘要文法 = ack 族同源)。
379
+ if (entry.role === "browser") return injectBrowserResult(agent, entry)
371
380
  const body = entry.error ?? entry.report ?? "(no report)"
372
381
  // F-2(BATCH-3-STRUCTURE)+ AGENT-LOOP.md §6.14 D-DG2(群 B 批 B5):注入前查轮累计(单源)——超限条目不
373
382
  // inline 全文,改清单行(全文经 persistOverflowReport 落盘——path 随行)。首条豁免
@@ -397,14 +397,15 @@ export function maybeRefillAsync(parent) {
397
397
  * 域 = 进程内(不做槽持久化——reload 后池清块消失,新进程从 1 无冲突——设计范围边界)。
398
398
  * spawn(subagent-spawn.mjs async 分支)、escalate(escalate-async.mjs)、async-advisor
399
399
  * 池(advisor-async.mjs——§6.10 跨池共号)、**后台 bash 任务池**(bash-async.mjs——#9:
400
- * `_bgTasks` 同入扫描域——VSC 形计数器不随 run 存活,缺此扫描会与在途 bg 条目撞号)共用;
400
+ * `_bgTasks` 同入扫描域——VSC 形计数器不随 run 存活,缺此扫描会与在途 bg 条目撞号)、
401
+ * **后台浏览器动作池**(browser-async.mjs——批 browser-async-fix:`_browserTasks` 同入)共用;
401
402
  * executeAsyncSpawn 直读 counter(分配与
402
403
  * 消费同步——无 await 间隙)。CLI 两池键均为字符串(set(String(id))——解析分支防御保留)。
403
404
  * @returns {number} 全池唯一的下一 id(单调——进程内)
404
405
  */
405
406
  export function nextSubagentId(parent) {
406
407
  let poolMax = 0
407
- for (const pool of [carrierField(parent, "_asyncSubagents"), carrierField(parent, "_asyncAdvisors"), carrierField(parent, "_bgTasks")]) {
408
+ for (const pool of [carrierField(parent, "_asyncSubagents"), carrierField(parent, "_asyncAdvisors"), carrierField(parent, "_bgTasks"), carrierField(parent, "_browserTasks")]) {
408
409
  if (!pool || pool.size === 0) continue
409
410
  for (const k of pool.keys()) {
410
411
  const n = typeof k === "number" ? k : Number.parseInt(k, 10)
@@ -426,7 +427,8 @@ export function nextSubagentId(parent) {
426
427
  * nextSubagentId ⇒ 令牌缺位/错号 ⇒ 抛错——防直读陈旧 counter 的静默覆写),断言
427
428
  * 通过即置 `undefined`(一次性——同一令牌跨站点复用第二消费必抛)。
428
429
  * 调用点:executeAsyncSpawn(subagent-run.mjs)· launchEscalateAsync(escalate-async.mjs)·
429
- * launchAsyncAdvisor(advisor-async.mjs)——site = 站点名(错误文案定位面)。
430
+ * launchAsyncAdvisor(advisor-async.mjs)· launchBrowserTask(browser-async.mjs——批 browser-async-fix
431
+ * 第四站点)——site = 站点名(错误文案定位面)。
430
432
  */
431
433
  export function consumeSubagentToken(parent, id, site, role) {
432
434
  if (parent?._lastSubagentId !== id) {
@@ -436,9 +438,10 @@ export function consumeSubagentToken(parent, id, site, role) {
436
438
  }
437
439
 
438
440
  /**
439
- * ED-5(§6.21)入池键守卫——三入池点同族:`set(String(id))` 前断言键不存在;命中 =
441
+ * ED-5(§6.21)入池键守卫——四入池点同族:`set(String(id))` 前断言键不存在;命中 =
440
442
  * 覆写(静默丢报告 + status/cancel 错址)⇒ 抛错。调用点:子代理池(subagent-run.mjs)·
441
- * escalate(escalate-async.mjs)· advisor 池(advisor-async.mjs)。
443
+ * escalate(escalate-async.mjs)· advisor 池(advisor-async.mjs)· browser 池(browser-async.mjs——
444
+ * 批 browser-async-fix 同族实例)。
442
445
  */
443
446
  export function assertPoolKeyFree(pool, id, role) {
444
447
  if (pool.has(String(id))) {
@@ -20,7 +20,7 @@ import { allocRelay, wrapChildCallbacks, relayPrefixOf } from "../agent/spawn-ch
20
20
  // TUI-OOM-ROOTCAUSE(AGENT-LOOP.md §6.15):子代理人读线窗口常量单源(store 零依赖)。
21
21
  import { RECORD_WINDOW_MESSAGES } from "../session-store.mjs"
22
22
  import { validateDesignToken } from "./advisor.mjs"
23
- import { tokenExpired, removeDesignTokenSlot, reconcileEngTokensFromSlot, persistEngTokens } from "../token-ttl.mjs"
23
+ import { tokenExpired, removeDesignTokenSlot, reconcileEngTokensFromSlot } from "../token-ttl.mjs"
24
24
  import { resolveChildProvider, buildChildRunOpts, enqueueAsk } from "./subagent-async.mjs"
25
25
  import { nextSubagentId } from "./subagent-scheduler.mjs"
26
26
  import {
@@ -50,7 +50,7 @@ function logBatchDocRefs(parent, role, task, boundAbs) {
50
50
  const cwd = parent?.cwd ?? process.cwd()
51
51
  const text = typeof task === "string" ? task : ""
52
52
  const key = (p) => (process.platform === "win32" ? p.toLowerCase() : p)
53
- const bases = batchDocBases(cwd) // 歧义锚 ⇒ 候选项目基底并集(#828——入基底判据覆盖面随之对齐)
53
+ const bases = batchDocBases(cwd) // 歧义锚 ⇒ 候选项目基底并集(#828——入基底判据覆盖范围随之对齐)
54
54
  const seen = new Set()
55
55
  for (const m of text.matchAll(MD_PATH_TOKEN)) {
56
56
  const token = m[0]
@@ -125,54 +125,14 @@ export function resolveDesignSlot(parent, designIdArg) {
125
125
 
126
126
  /**
127
127
  * consume-design 动作执行器(2026-09-07 token 链终消费制——ENGINEERING-MODE.md §2.6 F1):
128
- * 父侧验收核销时显式调用——读槽值 → removeDesignTokenSlot(token-ttl.mjs——移除该
129
- * designId 槽)→ 消费后同 designId 再 spawn = resolveDesignSlot not found 机械拒。调用
130
- * 形态定死(评审 #3):参数 designId(单设计会话可省略——FR3 spawn 同款语义);未知
131
- * designId 与重复消费同款 no-op 提示(幂等——不报错)。DESIGN-TOKEN-SETTLEMENT D3
132
- * (2026-09-08):单值镜像 `_engDesignToken` 已退役——无镜像兼容值清/兜底读(AC3)。
133
- * consume 落盘对称(2026-09-08 D1 段 + AC7):删内存槽后当场 persistEngTokens 同步
134
- * 落盘删除(旧台账不留盘——防 D2 门禁 miss 回读复活已消费 token);落盘失败回滚
135
- * 内存槽 + 抛错(不留半消费态,可重试——D1 评审 #1 同款失败语义)。
136
- * dispatch 分类(评审 #7d):非只读控制动作——depth-0 + 工程模式
137
- * 限定(受限变体门在 subagent.mjs 分流处;本器自持工程模式门)——planMode 拒绝
138
- * (dispatch 不豁免)——不入批审批分组(dispatch 免审直行——无文件写)。
128
+ * 父侧验收核销时显式调用——读槽值 → removeDesignTokenSlot(移除该 designId 槽)→ 消费后同
129
+ * designId 再 spawn = resolveDesignSlot not found 机械拒;落盘失败回滚内存槽 + 抛错
130
+ * (不留半消费态,可重试——D1 评审 #1 同款失败语义)。
131
+ * 2026-10-07 批 ledger-tool(#927 并入):选择器扩展(designIds / expired / olderThanDays——
132
+ * 恰一,fail-closed)+ 清点面 `design-slots` 落 `./design-slots.mjs`(清点 / 批量选择 / 消费核
133
+ * 单源迁入——单枚形态拒文 / 回执逐字不变);本处仅保本档 import 面(subagent.mjs / 既有消费点零改)。
139
134
  */
140
- export function executeConsumeDesignAction(args, ctx) {
141
- const parent = ctx.agent
142
- if (!parent?.config?.agent?.engineering) {
143
- throw new Error("Engineering mode is not active — consume-design applies only to engineering-mode design tokens (spawn 同门).")
144
- }
145
- const designId = args?.designId ? String(args.designId) : undefined
146
- const slots = parent?._engDesignTokens
147
- const hasSlots = slots instanceof Map && slots.size > 0
148
- // 多槽缺 designId → 拒(spawn 同款语义——不误消费任一槽)
149
- if (!designId && hasSlots && slots.size > 1) {
150
- 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.`)
151
- }
152
- // 读槽值:给定 designId → 精确槽(未知/已消费 → undefined);缺省 → 唯一槽
153
- let token = null
154
- if (designId && hasSlots) token = slots.get(designId) ?? null
155
- else if (!designId && hasSlots) token = [...slots.values()][0]
156
- // 未知 designId / 已消费 / 无任何槽 → 幂等 no-op 提示(不报错——评审 #3 定死)
157
- if (!token) {
158
- return `consume-design: no live slot${designId ? ` for designId ${designId}` : ""} (already consumed or never issued) — idempotent no-op, nothing changed.`
159
- }
160
- // consume 落盘对称(DESIGN-TOKEN-SETTLEMENT D1 段 + AC7——交付 🔴 复活洞修复):
161
- // 删内存槽后当场同步落盘删除(D1 同款 persistEngTokens——空 Map → 槽文件
162
- // engDesignTokens 字段删除,旧台账不留盘)。否则消费→回合尾 saveSession 窗口内
163
- // spawn 门禁 miss 回读(D2)会从盘上复活已消费 token。
164
- const slotId = designId ?? [...slots.keys()][0]
165
- removeDesignTokenSlot(parent, designId, token)
166
- try {
167
- persistEngTokens(parent)
168
- } catch (e) {
169
- // 落盘失败 → 回滚内存槽 + 抛错(不留半消费态——盘上仍有旧台账时消费不得报
170
- // 成功;可重试——D1 评审 #1 settle 同款失败语义)
171
- if (slotId) parent._engDesignTokens.set(slotId, token)
172
- 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.`)
173
- }
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.`
175
- }
135
+ export { executeConsumeDesignAction } from "./design-slots.mjs"
176
136
 
177
137
  // ── spawn 调度参数准入(AGENT-LOOP-SUBAGENT.md §6.9 D-SD1/D-SD3)────────────
178
138
  // files/dependsOn 声明即契约(v1:不做任务书文本自动解析——不可靠)。缺省(两者皆
@@ -239,6 +199,9 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
239
199
  // 路径语义照 `files` 先例:cwd 相对或绝对均可,`\` 归一为 `/`;解析序单源 = §4.15(cwd → 项目根 →
240
200
  // 逐基底,读面取首个可读)——cwd 非项目根时根相对串不再必拒(BR-29/BR-33)。错误文案带
241
201
  // **实际角色名**(§2.15 越界文案参数化——designer 撞门时不误导)与逐字不可读后缀。
202
+ // §4.16(2026-10-09):coder(普通模式)**可选**携 `batchDoc`——非强制(「spawn 强制 batchDoc」=
203
+ // 工程专属、不移植);传则须可读(同评审面判据)⇒ `child._batchDoc` 绑定 §5 记录通道;
204
+ // 未携 ⇒ 不绑定、照常 spawn。
242
205
  let batchDocAbs = null
243
206
  if (role === "eng-coder" || role === "eng-designer") {
244
207
  const given = typeof args.batchDoc === "string" ? args.batchDoc.trim() : ""
@@ -249,6 +212,14 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
249
212
  if (!batchDocAbs) throw refusal(" (given path is not a readable file)")
250
213
  // #309 形三:任务书中的他批批次档引用观测留痕(不阻断——放行不受影响)。
251
214
  logBatchDocRefs(parent, role, args.task, batchDocAbs)
215
+ } else if (role === "coder") {
216
+ const given = typeof args.batchDoc === "string" ? args.batchDoc.trim() : ""
217
+ if (given) {
218
+ batchDocAbs = resolveBatchReadPath(parent.cwd ?? process.cwd(), given)
219
+ if (!batchDocAbs) {
220
+ throw new Error("batchDoc for role='coder' — pass the batch record path (e.g. <repo>/docs/batches/<batch>-<topic>.md; relative paths resolve against the session cwd first, then candidate project roots — or absolute); when provided it must resolve to a readable file (omit batchDoc to spawn without the §5 record-write binding).")
221
+ }
222
+ }
252
223
  }
253
224
 
254
225
  // M5 F2(ENGINEERING-MODE-V2-MODULE-DELEGATION §2.2):任务书六强制字段门——
@@ -344,10 +315,12 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
344
315
  // 每个 child 原先各自一份永不压缩的 _fullHistory(勘察 C2 乘数面)。
345
316
  child._historyWindow = RECORD_WINDOW_MESSAGES
346
317
 
347
- // BATCH-RECORD.md §2 批次档段写入通道绑定(第 4 批):工程角色(eng-coder/eng-designer)把批次档
348
- // 绝对路径记在 child 上——setup 挂载 `batch` 时读它。无路径参数的工具靠这条
349
- // 绑定决定目标档(spawn 门已保证「参数在 + 路径可读」)。
350
- if (engineeringRole) child._batchDoc = batchDocAbs
318
+ // BATCH-RECORD.md §2 批次档段写入通道绑定(第 4 批;§4.16 扩 coder 可选分支):工程角色
319
+ // (eng-coder/eng-designer)把批次档绝对路径记在 child 上——setup 挂载 `batch` 时读它;
320
+ // 携绑定的 coder(普通模式,可选)同点绑定。无路径参数的工具靠这条绑定决定目标档
321
+ // (spawn 门已保证「参数在 + 路径可读」)。
322
+ const batchBound = engineeringRole || (role === "coder" && !!batchDocAbs)
323
+ if (batchBound) child._batchDoc = batchDocAbs
351
324
 
352
325
  // Token-verified design review → child is authorized to modify files without re-reviewing
353
326
  if (role === "eng-coder") child._engDesignReviewed = true
@@ -383,8 +356,9 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
383
356
  // §2.11 第 1 点(FR16 载体 + FR17 铁律 #5 的机械面):批次档**绝对路径**下发给工程角色
384
357
  // (eng-coder 实现 / eng-designer 写稿)——任务文本本身不动(批次档 §2 才是任务书本体;
385
358
  // 本行让子代理"拿到本档路径")。行文不含 summarizeEngTaskBook 的三组段 marker
386
- // (Docs involved / Files list / Acceptance criteria)——段匹配不受影响。仅工程角色注入。
387
- if (engineeringRole) spawnBlocks.push(`Batch record (batchDoc): ${batchDocAbs}`)
359
+ // (Docs involved / Files list / Acceptance criteria)——段匹配不受影响。工程角色恒注入;
360
+ // 携绑定 coder(§4.16)随绑定注入(绑定 coder 要能读到本档)。
361
+ if (batchBound) spawnBlocks.push(`Batch record (batchDoc): ${batchDocAbs}`)
388
362
  // AGENT-LOOP-SUBAGENT.md §6.7.6 D-E2 ③ (round4 #4, T-E13/T-E15): an eng-coder audit spawn's task book is
389
363
  // the eng-coder's OWN spawn task — mechanically kept as _engTaskInput by the
390
364
  // parent spawn and injected as the D-TS5 A2 mechanical summary (design docs /