@thincoder/core 0.9.3 → 0.9.5

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 (52) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/advisor/loop.mjs +2 -2
  3. package/advisor/run.mjs +1 -1
  4. package/agent/dispatch.mjs +21 -18
  5. package/agent/family-tools.mjs +6 -6
  6. package/agent/helpers.mjs +12 -2
  7. package/agent/run-stages.mjs +5 -5
  8. package/agent/suspension.mjs +1 -1
  9. package/agent-tools/advisor-async.mjs +2 -2
  10. package/agent-tools/advisor-settle.mjs +5 -5
  11. package/agent-tools/advisor.mjs +3 -3
  12. package/agent-tools/async-settle.mjs +2 -2
  13. package/agent-tools/batch-lifecycle.mjs +8 -16
  14. package/agent-tools/batch-paths.mjs +142 -0
  15. package/agent-tools/batch.mjs +10 -54
  16. package/agent-tools/consult.mjs +2 -2
  17. package/agent-tools/escalate-async.mjs +2 -2
  18. package/agent-tools/panel-blocks.mjs +1 -1
  19. package/agent-tools/subagent-actions.mjs +21 -21
  20. package/agent-tools/subagent-async.mjs +11 -11
  21. package/agent-tools/subagent-panel.mjs +5 -5
  22. package/agent-tools/subagent-run.mjs +8 -8
  23. package/agent-tools/subagent-scheduler.mjs +1 -1
  24. package/agent-tools/subagent-spawn.mjs +13 -12
  25. package/agent-tools/subagent.mjs +13 -13
  26. package/agent.mjs +13 -7
  27. package/config-presets.mjs +3 -3
  28. package/git/checkpoint.mjs +4 -8
  29. package/ledger-db.mjs +25 -5
  30. package/ledger-migrate.mjs +299 -0
  31. package/ledger.mjs +2 -1
  32. package/model-specs.mjs +131 -60
  33. package/package.json +1 -1
  34. package/peer-claims.mjs +263 -0
  35. package/peer-domains.mjs +93 -60
  36. package/prompts/common.md +4 -0
  37. package/prompts/persona-coder.md +1 -1
  38. package/prompts/persona-eng-coder.md +2 -2
  39. package/prompts/persona-eng-designer.md +2 -2
  40. package/prompts/persona-engineering.md +9 -0
  41. package/prompts/persona-explore.md +1 -1
  42. package/prompts/persona-normal.md +9 -0
  43. package/prompts/persona-plan.md +3 -3
  44. package/session-gc.mjs +67 -6
  45. package/session-slots-manifest.mjs +5 -0
  46. package/tool-docs/question.md +1 -0
  47. package/tools/edit-batch.mjs +6 -7
  48. package/tools/edit-diff.mjs +29 -2
  49. package/tools/file.mjs +7 -3
  50. package/tools/glob-dialect.mjs +3 -3
  51. package/tools/search.mjs +2 -2
  52. package/tools/shared.mjs +1 -1
@@ -27,16 +27,15 @@
27
27
  * 语义零行为变;端装配经 `configureBatchSegment({ onWrite })` 覆盖(VSC 侧现形 = `_touchedFiles`
28
28
  * 记账)。核内零端名分支(契约 5——只认 `onWrite` 函数名)。
29
29
  */
30
- import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs"
31
- import { resolve } from "node:path"
30
+ import { readFileSync, writeFileSync } from "node:fs"
32
31
 
33
- import { docRootBase, docRootPaths, readManifest } from "../manifest.mjs"
34
32
  import { SEGMENT_BY_ROLE, readBatchStatusLine, sectionHeaderRe, findPlaceholderResidue, placeholderResidueError } from "./batch-skeleton.mjs"
33
+ import { batchDocBases, resolveBatchDocPath } from "./batch-paths.mjs"
35
34
  import { closeBatchRecord, createBatchRecord, findInFlightBatch, statusBatchRecord } from "./batch-lifecycle.mjs"
36
35
 
37
- export { SEGMENT_BY_ROLE }
36
+ export { SEGMENT_BY_ROLE, batchDocBases, resolveBatchDocPath }
38
37
 
39
- /** text 单次上限(§2.20.1——超出引导分段追加,不承诺"不新盖戳")。append 迁移域私有区常量
38
+ /** text 单次上限(BATCH-RECORD.md §4.1——超出引导分段追加,不承诺"不新盖戳")。append 迁移域私有区常量
40
39
  * (随执行体自 batch-segment.mjs 迁入——骨架档只住骨架/状态行单源,lifecycle 档头枚举口径);
41
40
  * export = shim 导出全超集(旧 `batch-segment.mjs` 导出面等价保持)。 */
42
41
  export const MAX_TEXT_CHARS = 20000
@@ -60,58 +59,15 @@ export function configureBatchSegment(impl) {
60
59
  /** 撤销注入(测试与端装配生命周期用——缺省态 = 零记账)。 */
61
60
  export function resetBatchSegment() { injectedOnWrite = null }
62
61
 
63
- /** 可读文件判据(存在且为文件——目录/缺失同判不可读;与 lifecycle 档同名 helper 同型——
64
- * KD-4 单向依赖下各自持有,三行谓词不构成第二权威源)。 */
65
- function readableFile(abs) {
66
- try { return existsSync(abs) && statSync(abs).isFile() } catch { return false }
67
- }
68
-
69
- /**
70
- * 批次档路径门禁(评审侧 §2.20.2 口径 = **「若传则须可读」**):空/非字符串/不可读 → throw。
71
- * 非空且可读 → 返回绝对路径(`\` 归一——照 `files`/`batchDoc` spawn 门先例)。
72
- * v2:cwd 不可读 → manifest `docRoot.batches` 复判(M3 模块设计 §2.1#4——N3 可迁移;
73
- * 值形态 = 串 | 多根数组——逐基底按序复判,首个可读者胜)。
74
- */
75
- export function resolveBatchDocPath(cwd, given) {
76
- const base = cwd ?? process.cwd()
77
- const raw = typeof given === "string" ? given.trim() : ""
78
- if (!raw) {
79
- throw new Error("batchDoc must be a non-empty path to the batch record — pass the batch record currently in flight, or omit the parameter entirely when no batch record is in flight.")
80
- }
81
- const abs = resolve(base, raw.replace(/\\/g, "/"))
82
- if (readableFile(abs)) return abs
83
- // 双基底(M3 模块设计 §2.1#4):cwd 不可读 → manifest docRoot.batches 复判(逐基底按序,
84
- // 首个可读者胜);manifest 缺失 / 非法 / 读错 → 无第二基底(v1 单基底语义零变),
85
- // 全不可读 → throw。
86
- let man = { ok: false }
87
- try { man = readManifest(base) } catch { /* 权限等读错——按无 manifest 处理(v1 语义) */ }
88
- if (man.ok) {
89
- for (const root of docRootPaths(man.manifest?.docRoot?.batches, base)) {
90
- const alt = resolve(root, raw.replace(/\\/g, "/"))
91
- if (readableFile(alt)) return alt
92
- }
93
- }
94
- throw new Error(`batchDoc is not a readable file: ${raw} — pass the path of the batch record currently in flight (a path that resolves to an existing file), or omit the parameter when no batch record is in flight.`)
95
- }
96
-
97
62
  /**
98
- * 批次档基底根数组(**声明面单源**——轮 2 裁定:create 相对基底 / depth-0 在飞扫描共用,
99
- * 不写死 `docs/batches`):manifest `docRoot.batches`(串 | 多根数组)→ `docRootPaths`
100
- * 归一去重(相对基底按项目根解析);manifest 缺失/非法/读错 → 回退默认基底 `"docs/batches"`
101
- * (v1 单基底语义零变)。恒返回非空数组(create 的 bases[0] / findInFlightBatch 扫描面消费)。
102
- * @param {string} cwd
103
- * @returns {string[]}
63
+ * 批次档路径解析单源(`BATCH-RECORD.md` §4.15 · 台账 #287——实现住 `batch-paths.mjs`,本档 re-export):
64
+ * 候选序 cwd → 项目根 → 逐基底 · 读面取首个**可读**(口径仍「若传则须可读」,错误文案逐字零变)·
65
+ * create 取首个落基底内 · 锚定防嵌套(按路径段判)。四处调用点(create / 读面 / 评审门 / spawn 门)
66
+ * 均经该叶档——本档不再持第二份解析实现。
104
67
  */
105
- export function batchDocBases(cwd) {
106
- const base = cwd ?? process.cwd()
107
- let man = { ok: false }
108
- try { man = readManifest(base) } catch { /* 读错 → 按无 manifest 处理(回退默认基底) */ }
109
- const roots = man.ok ? docRootPaths(man.manifest?.docRoot?.batches, base) : []
110
- return roots.length ? roots : [resolve(docRootBase(base), "docs/batches")]
111
- }
112
68
 
113
69
  /**
114
- * 设计评审的实例绑定解析(§2.20.2/§2.20.3——评审实例的唯一路径来源):
70
+ * 设计评审的实例绑定解析(BATCH-RECORD.md §4.2/§4.3——评审实例的唯一路径来源):
115
71
  * - 同步路径:调用方(advisor 工具)把实例绑定显式放进 callbacks(**带 batchDoc 键**,
116
72
  * 未绑定即 null)——以此为准,不再回看池条目(防同步/异步混跑时串档);
117
73
  * - 异步路径:本评审所在**池条目**的 `run.batchDoc`(文档集 = 实例键,各评审各取各条)。
@@ -143,7 +99,7 @@ function allowedSegment(agent, review) {
143
99
  }
144
100
 
145
101
  /**
146
- * 凭证剥除 + 伪造轮次标题丢弃(§2.20.1 F6/AC30/AC34)。
102
+ * 凭证剥除 + 伪造轮次标题丢弃(BATCH-RECORD.md §4.1 F6/AC30/AC34)。
147
103
  * 逐行处理:含凭证形态的行 → 剥除该子串,剥后为空则整行丢弃(其余内容逐字保留)。
148
104
  * `dropStamp`(仅 §3 目标段):调用方自带的 `### 轮次 N(评审子代理)` 行被丢弃——
149
105
  * 工具生成才是唯一来源戳(否则伪造戳会污染 N 计数)。
@@ -22,7 +22,7 @@
22
22
  * Each child settles its own TUI block with a ⟦ev⟧done event at settle (R17 —
23
23
  * the old in-turn check consumption is gone).
24
24
  */
25
- import { createAgent, runAgent, readonlyToolNames } from "../agent.mjs"
25
+ import { createAgent, runAgent, readonlyToolNames, excludeSubagentTools } from "../agent.mjs"
26
26
  import { resolveChildProvider } from "./subagent.mjs"
27
27
  import { pushReal } from "../context.mjs"
28
28
  import { offloadToolResult, escapeXml } from "../agent/helpers.mjs"
@@ -268,7 +268,7 @@ async function runConsultChild(ctx, session, id, m, problem, ctrl) {
268
268
  // Read-only consultant: filter the parent tool set down to readonly tools + main_history.
269
269
  const allowed = readonlyToolNames(agent.tools ?? [])
270
270
  const tools = [
271
- ...(agent.tools ?? []).filter((t) => allowed.has(t.name)),
271
+ ...excludeSubagentTools((agent.tools ?? []).filter((t) => allowed.has(t.name))), // TOOLS.md §6.16:继承面过谓词
272
272
  makeMainHistoryTool(agent),
273
273
  ]
274
274
 
@@ -25,7 +25,7 @@
25
25
  * domain still follows the consuming turn's tier — no family exception).
26
26
  */
27
27
  import { relative, isAbsolute } from "node:path"
28
- import { runAgent, createAgent, DEFAULT_SUBAGENT_TURNS } from "../agent.mjs"
28
+ import { runAgent, createAgent, DEFAULT_SUBAGENT_TURNS, excludeSubagentTools } from "../agent.mjs"
29
29
  import { runWithContinue, TURN_CAP_MARK, wrapChildCallbacks } from "../agent/spawn-child.mjs"
30
30
  // TUI-OOM-ROOTCAUSE §23.3.1:子代理人读线窗口常量(单源——store 零依赖)。
31
31
  import { RECORD_WINDOW_MESSAGES } from "../session-store.mjs"
@@ -192,7 +192,7 @@ export function launchEscalateAsync(parent, ctx, launch) {
192
192
  ctx.callbacks?.onToken?.(relayPrefix + "[model]" + (provider.model ?? ""))
193
193
  const child = createAgent({
194
194
  provider,
195
- tools: parent.tools,
195
+ tools: excludeSubagentTools(parent.tools), // TOOLS.md §6.16:子代面按面排除(行内改——本档 Δ±0)
196
196
  config: parent.config,
197
197
  cwd: parent.cwd,
198
198
  memory: parent.memory,
@@ -1,5 +1,5 @@
1
1
  /**
2
- * panel-blocks.mjs — §19.6 D-P1 面板块列表(读时现算,纯函数)。
2
+ * panel-blocks.mjs — AGENT-LOOP-SUBAGENT.md §6.7.2 D-P1 面板块列表(读时现算,纯函数)。
3
3
  *
4
4
  * 来源 = `thincoder-cli/src/tui/subagent-freeze.mjs` 的 `computePanelBlocks`——**逐字随迁**
5
5
  * (CORE-UNIFICATION §2.5 #159:`panel` 动作以 CLI 为准;#99:VSC 无 panel = **有意端差**
@@ -1,18 +1,18 @@
1
1
  /**
2
- * subagent-actions.mjs — subagent 动作执行器(§19/§19.5/§19.6——2026-09-05 自
2
+ * subagent-actions.mjs — subagent 动作执行器(AGENT-LOOP-SUBAGENT.md §6.7——2026-09-05 自
3
3
  * subagent-async.mjs 拆分——Module Split Policy §20.9——纯迁移零行为变化)。
4
- * 内容:executeStatusAction(§19.5 D-M5——非阻塞池查询——touchedSummary/
4
+ * 内容:executeStatusAction(AGENT-LOOP-SUBAGENT.md §6.7.2 D-M5——非阻塞池查询——touchedSummary/
5
5
  * shortTouchedPath/statusFields 摘要助手随行)/ executeObserveAction + executeSendAction
6
- * (SUBAGENT-OBSERVE-SEND——observe 只读/send 控制豁免注入)/ executeEscalateAction(§19
6
+ * (SUBAGENT-OBSERVE-SEND——observe 只读/send 控制豁免注入)/ executeEscalateAction(AGENT-LOOP-SUBAGENT.md §6.7
7
7
  * D-M4——飞刀——touchedFilesNote 随行)。
8
- * §19.8 check 删除后仅 cancel 动作执行器与共享 post-spawn 管线留在 subagent-async.mjs;
8
+ * AGENT-LOOP-SUBAGENT.md §6.7.5 check 删除后仅 cancel 动作执行器与共享 post-spawn 管线留在 subagent-async.mjs;
9
9
  * §20 调度器 + 文件域组在 ./subagent-scheduler.mjs(describeBlockers 由此导入)。
10
- * 2026-09-08 二次拆分:§19.6 面板段迁至 ./subagent-panel.mjs(601 > 500 硬限跨档——
10
+ * 2026-09-08 二次拆分:AGENT-LOOP-SUBAGENT.md §6.7.2 面板段迁至 ./subagent-panel.mjs(601 > 500 硬限跨档——
11
11
  * 尾部 re-export executePanelAction 保 subagent.mjs 既有 import 面)+ ASYNC-RESULT-
12
12
  * CONTAINER.md D1 落地(池访问点改 getAsyncPool accessor)。
13
13
  */
14
14
  import { isAbsolute, relative } from "node:path"
15
- import { runAgent, createAgent, DEFAULT_SUBAGENT_TURNS } from "../agent.mjs"
15
+ import { runAgent, createAgent, DEFAULT_SUBAGENT_TURNS, excludeSubagentTools } from "../agent.mjs"
16
16
  import {
17
17
  runWithContinue, TURN_CAP_MARK, makeRelay, wrapChildCallbacks,
18
18
  ensureChildApiKey, clampEffort,
@@ -27,11 +27,11 @@ import { RECORD_WINDOW_MESSAGES } from "../session-store.mjs"
27
27
  import { getAsyncPool } from "./async-settle.mjs"
28
28
 
29
29
  /**
30
- * subagent action:"status" (§19 D-M2, new): NON-BLOCKING async-pool query —
30
+ * subagent action:"status" (AGENT-LOOP-SUBAGENT.md §6.7 D-M2, new): NON-BLOCKING async-pool query —
31
31
  * returns immediately and never consumes a result (results belong to the auto
32
- * channel — §19.8: no check action any more). Source of truth = the pool
32
+ * channel — AGENT-LOOP-SUBAGENT.md §6.7.5: no check action any more). Source of truth = the pool
33
33
  * (_asyncSubagents): entries moved to _pendingAsyncResults during a suspension
34
- * (§17 D-S3 ② — injected at the next run start) are no longer in the pool and
34
+ * (AGENT-LOOP-ASYNC-POOL.md §6.8 D-S3 ② — injected at the next run start) are no longer in the pool and
35
35
  * are NOT counted as done-waiting.
36
36
  * - id given → { id, role, status, model?, elapsedSec?, turn?, maxTurns?,
37
37
  * touchedFiles?/touchedMore?/touched? ... } for that entry; unknown id → error
@@ -39,20 +39,20 @@ import { getAsyncPool } from "./async-settle.mjs"
39
39
  * - id omitted → { overview: { running: [{id, role, model, elapsedSec, turn,
40
40
  * maxTurns, touchedFiles?/touched?}], queued: [{id, role, position, touched?}],
41
41
  * done: [{id, role}] } } — live queue positions (index in _asyncQueue + 1).
42
- * §19.5.6: running 条目带 touched files 摘要(touchedFiles 前 5 + touchedMore 超出
42
+ * AGENT-LOOP-SUBAGENT.md §6.7.2: running 条目带 touched files 摘要(touchedFiles 前 5 + touchedMore 超出
43
43
  * 计数——相对查询方 cwd;0 改动 → touched 占位);queued 条目带 touched 占位
44
44
  * "—(未启动)";done/error/取消条目无 touched 字段(round3 #9)。
45
45
  * A settled-but-unconsumed entry (settled during a NORMAL turn) reports done
46
46
  * with a "not yet consumed" note — the auto channel (turn-end collection / the
47
- * suspension digest) still delivers it afterwards (§19.8: sole consumption path).
47
+ * suspension digest) still delivers it afterwards (AGENT-LOOP-SUBAGENT.md §6.7.5: sole consumption path).
48
48
  */
49
- /** §19.5 D-M5 decision-field assembly (F9): running entries report
49
+ /** AGENT-LOOP-SUBAGENT.md §6.7.2 D-M5 decision-field assembly (F9): running entries report
50
50
  * {id, role, model, elapsedSec, turn, maxTurns} — the data needed to decide
51
51
  * WHO to cancel. Model is recorded at spawn (childProvider), startedAt at
52
52
  * ACTUAL start (queued waits don't count), turn/maxTurns mirrored from the
53
53
  * child's ⟦ev⟧turn events at the callbacks-wrap layer (subagent.mjs tracker).
54
54
  * elapsedSec computed at call time from startedAt. */
55
- /** §19.5.6 D-SF2/N-SF1 摘要形态:status 调用时实时读 entry.childAgent._touchedFiles
55
+ /** AGENT-LOOP-SUBAGENT.md §6.7.2 D-SF2/N-SF1 摘要形态:status 调用时实时读 entry.childAgent._touchedFiles
56
56
  * (绝对路径——per-run 记账——§18.12)→ 相对查询方 cwd;cwd 之外保留绝对形态 +
57
57
  * "../" 前缀;>80 字符截尾(不超行);前 5 个 + 独立截断字段 touchedMore(超出
58
58
  * 计数——不混入数组——消费方按类型区分)。占位:running 0 改动 → "—(尚无改动)"
@@ -94,10 +94,10 @@ function statusFields(entry, cwd) {
94
94
  base.elapsedSec = entry.startedAt ? Math.max(0, Math.floor((Date.now() - entry.startedAt) / 1000)) : 0
95
95
  base.turn = entry.turn ?? 0
96
96
  base.maxTurns = entry.maxTurns ?? 0
97
- // §19.5.6:touched files 摘要(T-SF1..4——新字段追加——既有字段零破坏)
97
+ // AGENT-LOOP-SUBAGENT.md §6.7.2:touched files 摘要(T-SF1..4——新字段追加——既有字段零破坏)
98
98
  Object.assign(base, touchedSummary(entry, cwd))
99
99
  } else if (entry.status === "queued") {
100
- // §19.5.6 T-SF2b:未启动——确定性占位(不崩;无对象可读)
100
+ // AGENT-LOOP-SUBAGENT.md §6.7.2 T-SF2b:未启动——确定性占位(不崩;无对象可读)
101
101
  base.touched = "—(未启动)"
102
102
  }
103
103
  return base
@@ -151,7 +151,7 @@ export function executeStatusAction(args, ctx) {
151
151
  return JSON.stringify(out)
152
152
  }
153
153
  // done = settled during this turn and not yet consumed — the auto channel still
154
- // delivers it (§19.8: turn-end collection / the suspension digest — no fetch action).
154
+ // delivers it (AGENT-LOOP-SUBAGENT.md §6.7.5: turn-end collection / the suspension digest — no fetch action).
155
155
  target.status = "done"
156
156
  target.done = true
157
157
  if (entry.error) target.error = entry.error
@@ -164,7 +164,7 @@ export function executeStatusAction(args, ctx) {
164
164
  if (entry.status === "running") overview.running.push(statusFields(entry, agent.cwd))
165
165
  else if (entry.status === "queued") {
166
166
  // §20:queued 条目补 waiting/reason(F-SD4——依赖/冲突原因模型可见);
167
- // §19.5.6 T-SF2b:未启动占位(确定性——不崩)。
167
+ // AGENT-LOOP-SUBAGENT.md §6.7.2 T-SF2b:未启动占位(确定性——不崩)。
168
168
  const blk = describeBlockers(agent, entry)
169
169
  const row = statusFields(entry, agent.cwd)
170
170
  row.position = queuedPosition(String(entry.id)) ?? entry.position
@@ -327,16 +327,16 @@ export function executeSendAction(args, ctx) {
327
327
  }
328
328
 
329
329
  // ═══════════════════════════════════════════════════════════════════════════
330
- // §19.6 subagent panel 检查工具(AGENT-LOOP.md §19.6——F-P1..P3/D-P1..P4)
330
+ // subagent panel 检查工具(AGENT-LOOP-SUBAGENT.md §6.7.2——F-P1..P3/D-P1..P4)
331
331
  // ═══════════════════════════════════════════════════════════════════════════
332
- // 2026-09-08 拆分(Module Split Policy——601 > 500 硬限跨档):§19.6 面板段迁至
332
+ // 2026-09-08 拆分(Module Split Policy——601 > 500 硬限跨档):AGENT-LOOP-SUBAGENT.md §6.7.2 面板段迁至
333
333
  // ./subagent-panel.mjs(executePanelAction/panelFreezeGate/blockKeyIn——verbatim +
334
334
  // ASYNC-RESULT-CONTAINER D1/D2 落地);本面保留 re-export 保 subagent.mjs 既有
335
335
  // import 面(subagent-async 尾部 re-export 先例)。
336
336
  export { executePanelAction } from "./subagent-panel.mjs"
337
337
 
338
338
  /**
339
- * subagent action:"escalate"(§19 D-M4——退役 escalate 工具语义原样,ESCALATE.md;
339
+ * subagent action:"escalate"(AGENT-LOOP-SUBAGENT.md §6.7 D-M4——退役 escalate 工具语义原样,ESCALATE.md;
340
340
  * §25 D-R17b——R17:缺省 async——后台飞刀 + settle 三分类 digest——async:false 保
341
341
  * 同步旧路径)。飞刀——交给 consultModels 池里更强模型(WRITE + 术后报告)。约束全
342
342
  * 保留:depth-0 only / 工程模式拒 / consultModels 空拒 / relay 前缀 `escalate#N/`
@@ -408,7 +408,7 @@ export async function executeEscalateAction(args, ctx) {
408
408
  // G3(施工②):overlay 摘除——coder 人格槽由 assemblePrompt 场景表承载(G3 映射)。
409
409
  child = createAgent({
410
410
  provider,
411
- tools: parent.tools,
411
+ tools: excludeSubagentTools(parent.tools), // TOOLS.md §6.16:子代面按面排除(行内改——本档 Δ±0)
412
412
  config: parent.config,
413
413
  cwd: parent.cwd,
414
414
  memory: parent.memory,
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * subagent-async.mjs — async subagent 机械 + 共享 post-spawn 管线 + cancel 动作执行器
3
- * (AGENT-LOOP.md §19:subagent 单工具多动作——spawn/status/escalate/cancel/panel/
4
- * consume-design + observe/send(SUBAGENT-OBSERVE-SEND)——§19.8 check 已删;spawn 路径与
5
- * 工具面在 subagent.mjs;cancel 动作执行器与机械、管线在本模块——check 执行器随 §19.8 删除)。
3
+ * (AGENT-LOOP-SUBAGENT.md §6.7:subagent 单工具多动作——spawn/status/escalate/cancel/panel/
4
+ * consume-design + observe/send(SUBAGENT-OBSERVE-SEND)——AGENT-LOOP-SUBAGENT.md §6.7.5 check 已删;spawn 路径与
5
+ * 工具面在 subagent.mjs;cancel 动作执行器与机械、管线在本模块——check 执行器随 AGENT-LOOP-SUBAGENT.md §6.7.5 删除)。
6
6
  * 内容:resolveChildProvider / async 池常量与域助手(ASYNC_POOL_LIMITS/poolDomainOf/
7
7
  * resolvePoolLimits/poolLimitsFor/runningPoolCount——§11.1)/ executeCancelAction +
8
- * cancelAsyncSubagent(§19.5 D-M6——工具与 TUI ⏹ 共用)/ runChildPipeline /
8
+ * cancelAsyncSubagent(AGENT-LOOP-SUBAGENT.md §6.7.2 D-M6——工具与 TUI ⏹ 共用)/ runChildPipeline /
9
9
  * injectAsyncResult / buildChildRunOpts / mergeChildMutations / enqueueAsk(批 6——
10
10
  * _permQueue 审批/继续弹窗串行收 helper——消费位 subagent.mjs/subagent-spawn.mjs/
11
11
  * escalate-async.mjs)。
@@ -48,7 +48,7 @@ export function enqueueAsk(owner, key, ask) {
48
48
  // independent pools — eng-coder 4 / other roles 4 (user ruling "eng-coder 四路,
49
49
  // 其他 4 路") — a full engCoder pool never blocks an explore spawn and vice versa
50
50
  // (same-domain cap still 4, cross-domain total up to 8). The per-turn check
51
- // budget was deleted with the check action (§19.8 — results arrive only via the
51
+ // budget was deleted with the check action (AGENT-LOOP-SUBAGENT.md §6.7.5 — results arrive only via the
52
52
  // auto channel; no loop guard needed).
53
53
  // ⚠ 与 config.mjs DEFAULTS.agent.poolLimits 的 SUBAGENT 两键(engCoder/other)逐键同值
54
54
  // (loadConfig/settings 默认源)——耦合锚 T-24a4 断言锁住——勿单侧改默认。advisor 第三键
@@ -163,7 +163,7 @@ export function resolveChildProvider(parent, modelArg) {
163
163
  }
164
164
 
165
165
  /**
166
- * §19.5 D-M6 cancel 核心(工具路径与 TUI ⏹ 共用):定向中止单个后台子代理,id 必填。
166
+ * AGENT-LOOP-SUBAGENT.md §6.7.2 D-M6 cancel 核心(工具路径与 TUI ⏹ 共用):定向中止单个后台子代理,id 必填。
167
167
  * - queued 目标(未启动无 controller):出队 + 后续 position 前移 + settle waiter
168
168
  * → { id, status:"cancelled", was:"queued" }——无 abort(T-M27)
169
169
  * - running 目标:置 entry.cancelled + 条目 controller abort(child runAgent signal)
@@ -183,7 +183,7 @@ export function cancelAsyncSubagent(agent, id) {
183
183
  }
184
184
  if (entry.cancelled) return { id: key, status: "cancelled" } // abort already in flight — idempotent
185
185
  if (entry.status === "queued") {
186
- // 出队 + position 释放/前移(§19.5 D-M6——无 abort)
186
+ // 出队 + position 释放/前移(AGENT-LOOP-SUBAGENT.md §6.7.2 D-M6——无 abort)
187
187
  const queue = agent._asyncQueue ?? []
188
188
  const qi = queue.indexOf(entry)
189
189
  if (qi >= 0) {
@@ -237,7 +237,7 @@ export function cancelSyncChild(agent, key) {
237
237
  return { id: k, status: "cancelled" }
238
238
  }
239
239
 
240
- /** subagent action:"cancel" (§19.5 D-M6): depth-0 main-session control only.
240
+ /** subagent action:"cancel" (AGENT-LOOP-SUBAGENT.md §6.7.2 D-M6): depth-0 main-session control only.
241
241
  * §20 D-SD5/round1 #4(queued 依赖取消——无 settle 事件):出队后**返回即**重估
242
242
  * 依赖者——依赖者留 queued 标 dependency cancelled(refreshQueuedTokens 发更新块头
243
243
  * token)+ 工具结果内注依赖者(模型可见——工具结果内——round1 #4 明示通道)+ 补位
@@ -349,7 +349,7 @@ export async function runChildPipeline(child, input, childOpts, childRunOpts, {
349
349
 
350
350
  /**
351
351
  * Inject one settled async entry into the parent history as a user-role reminder
352
- * (§17 D-S3 — the auto channel, sole consumption path since §19.8: turn-end
352
+ * (AGENT-LOOP-ASYNC-POOL.md §6.8 D-S3 — the auto channel, sole consumption path since AGENT-LOOP-SUBAGENT.md §6.7.5: turn-end
353
353
  * collection (collectSettledAsync, agent.mjs) and the run-start _pendingAsyncResults
354
354
  * injection; the message shape is identical to the §15 collector's). Consumed =
355
355
  * the caller removes the entry from its container; no double-inject across the
@@ -386,7 +386,7 @@ export async function injectAsyncResult(agent, entry) {
386
386
  })
387
387
  // §20 D-SD5 终态墓碑:本函数是全部自动注入路径的共享形态(回合尾 collect + 挂起
388
388
  // digest 首行注入)——注入即消费(调用方随即从容器移除)——dependsOn 引用该 id 的
389
- // 后续 spawn 视为已满足(T-SD14 消费终态语义——§19.8 后 check 消费路径删除,本自动
389
+ // 后续 spawn 视为已满足(T-SD14 消费终态语义——AGENT-LOOP-SUBAGENT.md §6.7.5 后 check 消费路径删除,本自动
390
390
  // 通道为唯一消费方;error 条目记 failed——依赖取消/失败分支照旧,不误标成功)。
391
391
  writeTombstone(agent, entry.id, entry.error != null ? "failed" : "consumed", entry.role)
392
392
  }
@@ -396,7 +396,7 @@ export async function injectAsyncResult(agent, entry) {
396
396
  * child: without it, Ctrl+C aborts the parent's controller but the child keeps
397
397
  * running its full turn budget (up to subagentTurns) while the parent awaits —
398
398
  * the interrupt appears to do nothing.
399
- * §17 D-S9: during a suspension session children share the SESSION signal instead
399
+ * AGENT-LOOP-ASYNC-POOL.md §6.8 D-S9: during a suspension session children share the SESSION signal instead
400
400
  * (agent._sessionSignal) — a digest's own Ctrl+I/Ctrl+C must not abort the whole
401
401
  * pool; the session driver aborts the session controller to stop everything.
402
402
  */
@@ -1,6 +1,6 @@
1
1
  /**
2
- * subagent-panel.mjs — subagent action:"panel" 执行器(§19.6——2026-09-08 自
3
- * subagent-actions.mjs 拆分——Module Split Policy:601 > 500 硬限跨档,§19.6 面板段
2
+ * subagent-panel.mjs — subagent action:"panel" 执行器(AGENT-LOOP-SUBAGENT.md §6.7.2——2026-09-08 自
3
+ * subagent-actions.mjs 拆分——Module Split Policy:601 > 500 硬限跨档,AGENT-LOOP-SUBAGENT.md §6.7.2 面板段
4
4
  * verbatim 迁入 + ASYNC-RESULT-CONTAINER.md D1/D2 落地(池 accessor getAsyncPool +
5
5
  * pending 单容器 _pendingAsyncResults 四族统一——escalate/consult 独立族退役))。
6
6
  * 内容:executePanelAction(D-P2——readonly 视图面 + 门控 freeze)+ panelFreezeGate
@@ -21,7 +21,7 @@ function blockKeyIn(container, key) {
21
21
  }
22
22
 
23
23
  /**
24
- * §19.6 D-P3 冻结门控(安全):仅允许冻结 awaitingDigest 且池(_asyncSubagents/
24
+ * AGENT-LOOP-SUBAGENT.md §6.7.2 D-P3 冻结门控(安全):仅允许冻结 awaitingDigest 且池(_asyncSubagents/
25
25
  * _asyncAdvisors——经 getAsyncPool accessor——D1)无对应运行条目 + pending 单容器
26
26
  * (_pendingAsyncResults——D2)无对应条目的块(= 已消化驻留块——报告已入模型上下文
27
27
  * ——pending 已消费——状态滞后——补发冻结不破坏任何顺序)。
@@ -63,7 +63,7 @@ function panelFreezeGate(ctx, key) {
63
63
  }
64
64
 
65
65
  /**
66
- * §19.6 subagent action:"panel"(D-P2——readonly 视图面 + 门控干预面——单动作双参,
66
+ * AGENT-LOOP-SUBAGENT.md §6.7.2 subagent action:"panel"(D-P2——readonly 视图面 + 门控干预面——单动作双参,
67
67
  * freeze 优先):
68
68
  * - view(缺省——返回面板块列表):ctx.state(= agent._tuiState——CLI TUI 装配)经
69
69
  * computePanelBlocks **读时现算**(F-3 单账本——与用户所见一致)。awaitingDigest 条目
@@ -72,7 +72,7 @@ function panelFreezeGate(ctx, key) {
72
72
  * = 报告已消化但块仍驻留——异常块——freeze 候选;模型可定位解释 UI 怪相)。
73
73
  * - freeze:key(D-P3 门控通过 → 发 key + "/" + ⟦ev⟧done 哨兵字面 token——
74
74
  * onToken——TUI routeSubToken 冻结回收——落位复用 sub._freezeAt settle 锚点
75
- * splice,无锚点尾推兜底——§17.5.5 同口径——round1 #2)。
75
+ * splice,无锚点尾推兜底——AGENT-LOOP-ASYNC-POOL.md §6.8 同口径——round1 #2)。
76
76
  * 无 TUI state(headless/VS Code——D-P2 round1 #1:webview 无 state.subTasks 对应物——
77
77
  * 7.2.3.2 #8 先例)→ view 恒降级池视图(双池经 getAsyncPool + pending 单容器
78
78
  * 合成)+ no panel 注;freeze 报不可用。CLI-only 完整能力(AC-P4)。
@@ -36,7 +36,7 @@ export function drainInjectedQueue(entry, agent) {
36
36
  * The child runs the EXACT blocking pipeline (runChildPipeline — relay /
37
37
  * turn-cap / permission / MIN_REPORT_CHARS / mergeChildMutations all unchanged),
38
38
  * but the parent does not await it: the promise is parked in _asyncSubagents and
39
- * consumed by the auto channel (§19.8 — turn-end collection / suspension digest;
39
+ * consumed by the auto channel (AGENT-LOOP-SUBAGENT.md §6.7.5 — turn-end collection / suspension digest;
40
40
  * the check action is gone). Slot queue (§11.1 D-24a/R14 — 分域): the entry carries a
41
41
  * pool domain (_pool = poolDomainOf(role)); running count < limit[its domain]
42
42
  * (agent.poolLimits — default engCoder 4 / other 4) → start now; ≥ → enqueue
@@ -64,13 +64,13 @@ export function executeAsyncSpawn(parent, ctx, role, args, child, input, childOp
64
64
  position: undefined,
65
65
  report: null, error: null, done: false, cancelled: false,
66
66
  promise: null, _settle: null, _settleSeq: 0,
67
- // §19.5 D-M5 可决策字段(status 数据装配锚点):model 在 spawn 时记录;
67
+ // AGENT-LOOP-SUBAGENT.md §6.7.2 D-M5 可决策字段(status 数据装配锚点):model 在 spawn 时记录;
68
68
  // startedAt 在 ACTUAL start(queued 等待不计 elapsed);turn/maxTurns 由
69
69
  // 下方 onToken 拦截层从子代理 ⟦ev⟧turn 事件镜像(T-M18 正确性断言)。
70
70
  model: childProvider?.model ?? null,
71
71
  startedAt: null,
72
72
  turn: 0, maxTurns: 0,
73
- // §19.5 D-M6 (round2 #2):条目级 AbortController——cancel 定向 abort 本
73
+ // AGENT-LOOP-SUBAGENT.md §6.7.2 D-M6 (round2 #2):条目级 AbortController——cancel 定向 abort 本
74
74
  // 条目(runAgent signal 链);Ctrl+C 全停语义不变(基信号 abort 逐链传播)。
75
75
  controller: null,
76
76
  // §20 D-SD2 域元数据(AGENT-LOOP.md §20):running ∪ queued 条目全带——
@@ -97,15 +97,15 @@ export function executeAsyncSpawn(parent, ctx, role, args, child, input, childOp
97
97
  }
98
98
  // The settle signal — resolves when the run chain settles (never rejects).
99
99
  entry.promise = new Promise((res) => { entry._settle = res })
100
- // §19.5 D-M6:条目 controller 链到会话/回合基信号(_sessionSignal 优先——
101
- // §17 挂起会话内 children 持会话 signal,digest 自身 Ctrl+C 不误伤)。
100
+ // AGENT-LOOP-SUBAGENT.md §6.7.2 D-M6:条目 controller 链到会话/回合基信号(_sessionSignal 优先——
101
+ // AGENT-LOOP-ASYNC-POOL.md §6.8 挂起会话内 children 持会话 signal,digest 自身 Ctrl+C 不误伤)。
102
102
  // D6 buildChildSignal 单点(ASYNC-RESULT-CONTAINER.md)。
103
103
  const ctrl = new AbortController()
104
104
  entry.controller = ctrl
105
105
  // §20.3 站点 #10(第 24 批):hop 逐跳保 reason;#98 链结单点(interrupt 豁免面——
106
106
  // Ctrl+I 不逐链中止池内子代理)。
107
107
  bindChildController(ctrl, buildChildSignal(parent, ctx))
108
- // §19.5 D-M5:turn 镜像拦截层(callbacks 包装层——在既有
108
+ // AGENT-LOOP-SUBAGENT.md §6.7.2 D-M5:turn 镜像拦截层(callbacks 包装层——在既有
109
109
  // wrapChildCallbacks 之外再包一层,只解析 ⟦ev⟧turn 更新条目,其余原样转发)。
110
110
  const trackOpts = { ...childOpts }
111
111
  const parentOnToken = trackOpts.onToken
@@ -127,7 +127,7 @@ export function executeAsyncSpawn(parent, ctx, role, args, child, input, childOp
127
127
  entry.status = "running"
128
128
  entry.position = undefined
129
129
  entry.startedAt = Date.now()
130
- // §19.5.6 D-SF1(round3 #1):绑定子代理对象引用——绑定时刻 = 实际启动时
130
+ // AGENT-LOOP-SUBAGENT.md §6.7.2 D-SF1(round3 #1):绑定子代理对象引用——绑定时刻 = 实际启动时
131
131
  // (queued 条目 spawn-ack 时刻尚无子代理对象——§20 D-SD3b);绑定对象 = 子代理
132
132
  // 对象(不是 _touchedFiles 数组引用——per-run 记账在 prepareRun 重置——数组
133
133
  // 引用会陈旧——对象引用保证 status 查询时实时读——杀前一刻最新)。
@@ -139,7 +139,7 @@ export function executeAsyncSpawn(parent, ctx, role, args, child, input, childOp
139
139
  // 持 entry + child;空队列 no-op(每轮尝试——零开销)。N2:不打断在跑工具——入队
140
140
  // 消息只在下一回合边界(当前工具完成后的下轮 chat 前)进上下文。
141
141
  const consumeInjected = (ag) => { drainInjectedQueue(entry, ag ?? child) }
142
- // §19.5 D-M7b ①: async 标记事件——零字段 ⟦ev⟧async token(sync 不发)。
142
+ // AGENT-LOOP-SUBAGENT.md §6.7.2 D-M7b ①: async 标记事件——零字段 ⟦ev⟧async token(sync 不发)。
143
143
  // 锚点 = 实际启动(与 [model] 同步——queued 入队不 paint,补位启动才发);
144
144
  // 先于 [model] 发出——区块创建即知 sub.async(routeSubToken 解析——
145
145
  // ⏹ 门控与头标 async 的判定源)。父级直接 emit(depth-0 专属路径——
@@ -191,7 +191,7 @@ export function describeBlockers(parent, entry) {
191
191
  * 文件冲突只阻断后入者——id 序)——混合环仅人工注入可构造(T-SD5 同族防御断言)——
192
192
  * 但状态既可能成环即机械可检——不依赖可达性论证。maybeRefillAsync 空转处不调用:refill
193
193
  * 嵌在 settle/cancel 链上无模型可见输出通道(抛错即破坏池操作)——status 视图(actions
194
- * 模块)即 F-SL2 的报错/标记落点(§19.8:check 守卫随 check 删除)。 */
194
+ * 模块)即 F-SL2 的报错/标记落点(AGENT-LOOP-SUBAGENT.md §6.7.5:check 守卫随 check 删除)。 */
195
195
  export const STALL_NOTE = "cancel one task in the loop (action:'cancel') to break the cycle, then re-spawn it"
196
196
 
197
197
  export function detectStall(parent) {
@@ -11,11 +11,9 @@
11
11
  * (`child._spawnSystemBlock` 单点绑定——消费点 = 核 `prepareRun`),`input` 只留任务书。
12
12
  */
13
13
 
14
- import { resolve } from "node:path"
15
- import { existsSync, statSync } from "node:fs"
16
14
  import {
17
15
  createAgent,
18
- readonlyToolNames, escapeXml,
16
+ readonlyToolNames, escapeXml, excludeSubagentTools,
19
17
  } from "../agent.mjs"
20
18
  import { allocRelay, wrapChildCallbacks, relayPrefixOf } from "../agent/spawn-child.mjs"
21
19
  // TUI-OOM-ROOTCAUSE(AGENT-LOOP.md §23.3.1):子代理人读线窗口常量单源(store 零依赖)。
@@ -33,6 +31,9 @@ import { validateTaskBookFields } from "./spawn-gates.mjs"
33
31
  // 台账 #23(AGENT-LOOP-SUBAGENT.md §6.26 · D23-3):审计块构造器外提(`summarizeEngTaskBook`
34
32
  // + `buildAuditBlock` 纯函数)——spawn 档只做「收集 + 绑定」;块文本逐字搬移(D23-4)。
35
33
  import { buildAuditBlock } from "./audit-block.mjs"
34
+ // 批次档路径解析单源(BATCH-RECORD.md §4.15 · 台账 #287):门内解析改经叶档(读面非抛形),
35
+ // 与 append / create 同源——门判据仍「参数在 + 路径可读」,错误文案逐字零变。
36
+ import { resolveBatchReadPath } from "./batch-paths.mjs"
36
37
 
37
38
  /**
38
39
  * Effective subagent model override for a role (CLI parity shared with VS Code):
@@ -206,20 +207,17 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
206
207
  // 批次档路径(需求 §1.11 铁律 #5 随件传递的机械面)。落点 = 本装配点(token 门之前):
207
208
  // sync 与 async 两条 spawn 路径都经过 buildSpawnChild——一处校验双路生效,错误出口与
208
209
  // token 门一致(§2.13.1 选型 A)。
209
- // 判据只到"参数在 + 路径可读"——**不校验内容/措辞**(不做"已收口"正则、不匹配模板、
210
- // 不生成;内容够不够由执行者拒收兜底——需求 §1.14 #9 行为面)。路径语义照
211
- // `files` 先例:cwd 相对或绝对均可,`\` 归一为 `/`。错误文案带**实际角色名**(§2.15
212
- // 越界文案参数化——designer 撞门时不误导)。
210
+ // 路径语义照 `files` 先例:cwd 相对或绝对均可,`\` 归一为 `/`;解析序单源 = §4.15(cwd → 项目根 →
211
+ // 逐基底,读面取首个可读)——cwd 非项目根时根相对串不再必拒(BR-29/BR-33)。错误文案带
212
+ // **实际角色名**(§2.15 越界文案参数化——designer 撞门时不误导)与逐字不可读后缀。
213
213
  let batchDocAbs = null
214
214
  if (role === "eng-coder" || role === "eng-designer") {
215
215
  const given = typeof args.batchDoc === "string" ? args.batchDoc.trim() : ""
216
216
  const refusal = (suffix = "") => new Error(
217
217
  `batchDoc is required for role='${role}' — pass the batch record path (docs/batches/<batch>-<topic>.md); spawn refused without it.` + suffix)
218
218
  if (!given) throw refusal()
219
- batchDocAbs = resolve(parent.cwd ?? process.cwd(), given.replace(/\\/g, "/"))
220
- let readable = false
221
- try { readable = existsSync(batchDocAbs) && statSync(batchDocAbs).isFile() } catch { readable = false }
222
- if (!readable) throw refusal(" (given path is not a readable file)")
219
+ batchDocAbs = resolveBatchReadPath(parent.cwd ?? process.cwd(), given)
220
+ if (!batchDocAbs) throw refusal(" (given path is not a readable file)")
223
221
  }
224
222
 
225
223
  // M5 F2(ENGINEERING-MODE-V2-MODULE-DELEGATION §2.2):任务书六强制字段门——
@@ -258,6 +256,9 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
258
256
  } else {
259
257
  tools = parent.tools
260
258
  }
259
+ // Then the depth>0 exclusion (TOOLS.md §6.16): role selection above, exclusion LAST — both
260
+ // branches converge here (`question` = interactive main-session tool; a child has no user to ask).
261
+ tools = excludeSubagentTools(tools)
261
262
 
262
263
  // PROMPT-SYSTEM 施工② G3(2026-09-10):overlay(人格)装载随装配改造退役——人格槽
263
264
  // 由 assemblePrompt 的 D1 场景表按 role 承载(persona-{role},explore/coder/plan =
@@ -312,7 +313,7 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
312
313
  // 每个 child 原先各自一份永不压缩的 _fullHistory(勘察 C2 乘数面)。
313
314
  child._historyWindow = RECORD_WINDOW_MESSAGES
314
315
 
315
- // §2.20.2 批次档段写入通道绑定(第 4 批):工程角色(eng-coder/eng-designer)把批次档
316
+ // BATCH-RECORD.md §2 批次档段写入通道绑定(第 4 批):工程角色(eng-coder/eng-designer)把批次档
316
317
  // 绝对路径记在 child 上——setup 挂载 batch_segment 时读它。无路径参数的工具靠这条
317
318
  // 绑定决定目标档(spawn 门已保证「参数在 + 路径可读」)。
318
319
  if (engineeringRole) child._batchDoc = batchDocAbs
@@ -6,10 +6,10 @@
6
6
  * 2026-09-03 拆分轮: subagent.mjs 超 500 硬顶——async 常量、共享 post-spawn 管线
7
7
  *(runChildPipeline)与队列/注入/并账机械迁至 ./subagent-async.mjs。execute
8
8
  *(async 分支 + 阻塞路径)原样保留于本文件;导出面由文末 re-export shim 兜住。
9
- * 2026-09-03 §19 合体轮: subagent_check/escalate 工具退役——status/escalate
9
+ * 2026-09-03 合体轮(AGENT-LOOP-SUBAGENT.md §6.7): subagent_check/escalate 工具退役——status/escalate
10
10
  * 动作执行器并入 ./subagent-async.mjs,本文件只承载工具面(action schema)与
11
11
  * spawn 路径 + 动作分流。
12
- * 2026-09-06 §19.8 删 check 轮: check 动作删除——工具面五动作(spawn/status/
12
+ * 2026-09-06 删 check 轮(AGENT-LOOP-SUBAGENT.md §6.7.5): check 动作删除——工具面五动作(spawn/status/
13
13
  * escalate/cancel/panel)——async 结果仅自动通道送达。
14
14
  * 2026-09-05 拆分轮: status/escalate/panel 动作执行器 → ./subagent-actions.mjs;§20
15
15
  * 调度器全套 → ./subagent-scheduler.mjs——本文件 import 源随之改写。
@@ -100,15 +100,15 @@ export function buildSyncStoppedReport(role, capturedOutput, designId) {
100
100
  }
101
101
 
102
102
  /**
103
- * subagent tool — ONE tool, EIGHT actions (AGENT-LOOP.md §19/§19.5/§19.6/§19.8 +
103
+ * subagent tool — ONE tool, EIGHT actions (AGENT-LOOP-SUBAGENT.md §6.7 +
104
104
  * SUBAGENT-OBSERVE-SEND): spawn (default) / status (non-blocking pool query) / observe
105
105
  * (inspect a running/queued/done async child's recent activity + current tool — §7.2) /
106
106
  * send (inject a direction into a RUNNING async child — consumed at its next turn
107
107
  * boundary as an ordinary instruction — §7.2) / escalate (飞刀 — hand implementation to
108
- * a stronger model) / cancel (stop ONE background subagent — §19.5) / panel (view + fix
109
- * the live subagent panel — §19.6) / consume-design (parent-side chain-terminal token
108
+ * a stronger model) / cancel (stop ONE background subagent — AGENT-LOOP-SUBAGENT.md §6.7.2) / panel (view + fix
109
+ * the live subagent panel — AGENT-LOOP-SUBAGENT.md §6.7.2) / consume-design (parent-side chain-terminal token
110
110
  * consumption — ENGINEERING-MODE.md §2.6, 2026-09-07). The check
111
- * action was deleted (§19.8): async results reach the model only via the auto channel.
111
+ * action was deleted (AGENT-LOOP-SUBAGENT.md §6.7.5): async results reach the model only via the auto channel.
112
112
  * - action:"spawn" roles: "explore" — read-only tools, search/read/analyze
113
113
  * (suitable for codebase exploration); "coder" — full tool set, self-contained
114
114
  * implementation tasks; "plan" — read-only planning; "eng-coder" —
@@ -174,20 +174,20 @@ export const subagentTool = {
174
174
  sideEffectExempt: true, // child agent may write files; parent can't introspect its _mutatedThisRun
175
175
  parallel: true,
176
176
  async execute(args, ctx) {
177
- // §19 action dispatch: default spawn keeps every legacy call unchanged
177
+ // AGENT-LOOP-SUBAGENT.md §6.7 action dispatch: default spawn keeps every legacy call unchanged
178
178
  // (no action parameter → the spawn path below, byte-identical semantics).
179
179
  const action = args?.action !== undefined && args?.action !== null && String(args.action) !== ""
180
180
  ? String(args.action)
181
181
  : "spawn"
182
182
  if (action !== "spawn") {
183
- // §19 restricted-variant action gate (round2 #3): the engineering-child channel
183
+ // AGENT-LOOP-SUBAGENT.md §6.7 restricted-variant action gate (round2 #3): the engineering-child channel
184
184
  // (depth>0, role eng-coder or eng-designer) is spawn-only — escalate spawns a
185
185
  // coder+WRITE child (violates explore-only intent) and status/panel/observe/send
186
186
  // have no async pool / panel mirror to query in a child context.
187
187
  if ((ctx.depth ?? 0) > 0 && (ctx.agent?._role === "eng-coder" || ctx.agent?._role === "eng-designer")) {
188
188
  throw new Error(`only action:'spawn' (sync explore children) is available inside an ${ctx.agent._role} — escalate/status/cancel/panel/consume-design/observe/send are not`)
189
189
  }
190
- // §17 N3/D-S6 spawn gate (manual tier): auto-turn digests may not spawn —
190
+ // AGENT-LOOP-ASYNC-POOL.md §6.8 N3/D-S6 spawn gate (manual tier): auto-turn digests may not spawn —
191
191
  // async OR blocking — the digest must stay organize-only. The escalate
192
192
  // action spawns a write child too, so the same mechanical refusal applies
193
193
  // (AUTO tier exempt — user authorized unattended continuation).
@@ -196,14 +196,14 @@ export const subagentTool = {
196
196
  }
197
197
  if (action === "status") return executeStatusAction(args, ctx)
198
198
  if (action === "escalate") return await executeEscalateAction(args, ctx)
199
- // §19.5 控制类动作:digest 内放行(D-S7 分类——控制/自省;dispatch 控制类
199
+ // AGENT-LOOP-SUBAGENT.md §6.7.2 控制类动作:digest 内放行(D-S7 分类——控制/自省;dispatch 控制类
200
200
  // 豁免同批生效——19.5.2b round2 #4;escalate 的 digest 拒绝在上一分支)
201
201
  if (action === "cancel") return executeCancelAction(args, ctx)
202
202
  // 2026-09-07 token 链终消费制(ENGINEERING-MODE.md §2.6 F1):父侧核销消费——
203
203
  // 非只读控制动作——depth-0 + 工程模式限定(本分流已过受限变体门;工程模式门在
204
204
  // 执行器内)——planMode 拒绝(dispatch 不豁免)——不入批审批分组(dispatch 免审)。
205
205
  if (action === "consume-design") return executeConsumeDesignAction(args, ctx)
206
- // §19.6 panel 动作:view(readonly 面——digest 内放行——自省类)与 freeze
206
+ // AGENT-LOOP-SUBAGENT.md §6.7.2 panel 动作:view(readonly 面——digest 内放行——自省类)与 freeze
207
207
  // (控制类——同 cancel——digest 内放行)。深度/门控检查在 executePanelAction 内。
208
208
  // CLI-ACTIVITY-DEBLOAT F-3(2026-09-10)接线:executePanelAction 经 ctx.state
209
209
  // (= agent._tuiState——startTUI 反向挂载)读时现算面板块(computePanelBlocks)——
@@ -263,7 +263,7 @@ export const subagentTool = {
263
263
  throw new Error("Engineering mode is not active — role='eng-designer' is engineering-mode only (it writes the requirements/design documents inside the engineering workflow); use role='explore' or role='plan' for read-only work.")
264
264
  }
265
265
 
266
- // §17 N3/D-S6 spawn gate (manual tier): auto-turn digests may not spawn — async
266
+ // AGENT-LOOP-ASYNC-POOL.md §6.8 N3/D-S6 spawn gate (manual tier): auto-turn digests may not spawn — async
267
267
  // OR blocking — the digest must stay organize-only. AUTO tier (autoApprove) is
268
268
  // exempt (推进型 — user authorized unattended continuation). Mechanical refusal
269
269
  // so the digest never pops a permission panel or chains new background work.
@@ -397,7 +397,7 @@ export const subagentTool = {
397
397
  },
398
398
  }
399
399
 
400
- // Re-export shim (2026-09-03 拆分轮 + §19 合体轮 + 2026-09-05 拆分轮): 机械与动作
400
+ // Re-export shim (2026-09-03 拆分轮 + 合体轮 + 2026-09-05 拆分轮): 机械与动作
401
401
  // 执行器迁至 ./subagent-async.mjs、./subagent-actions.mjs、./subagent-scheduler.mjs
402
402
  // ——本文件保留导出面,消费点(agent.mjs / agent-turn.mjs / consult.mjs / 测试)导入
403
403
  // 路径零改动;池逻辑/准入见 subagent-run.mjs(executeAsyncSpawn)与 subagent-scheduler.mjs