@thincoder/core 0.10.2 → 0.10.4

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 (106) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/advisor/citations.mjs +40 -4
  3. package/advisor/compaction.mjs +21 -2
  4. package/advisor/convergence.mjs +1 -1
  5. package/advisor/history.mjs +8 -6
  6. package/advisor/loop.mjs +29 -27
  7. package/advisor/messages.mjs +5 -4
  8. package/advisor/run.mjs +5 -1
  9. package/advisor/timeline.mjs +26 -0
  10. package/advisor.mjs +4 -2
  11. package/agent/completion.mjs +3 -1
  12. package/agent/digest-account.mjs +188 -0
  13. package/agent/dispatch-run.mjs +29 -12
  14. package/agent/family-tools.mjs +11 -11
  15. package/agent/helpers.mjs +8 -0
  16. package/agent/record-results.mjs +10 -1
  17. package/agent/run-stages.mjs +27 -4
  18. package/agent/run-start.mjs +49 -3
  19. package/agent/spawn-child.mjs +2 -1
  20. package/agent/subagent-event.mjs +71 -0
  21. package/agent/suspension.mjs +60 -12
  22. package/agent/turn-loop.mjs +31 -2
  23. package/agent/write-gate.mjs +13 -25
  24. package/agent-tools/advisor-async.mjs +25 -7
  25. package/agent-tools/advisor-settle.mjs +7 -9
  26. package/agent-tools/advisor.mjs +34 -33
  27. package/agent-tools/async-settle.mjs +29 -5
  28. package/agent-tools/batch-lifecycle.mjs +29 -7
  29. package/agent-tools/batch-paths.mjs +22 -2
  30. package/agent-tools/batch-read.mjs +36 -0
  31. package/agent-tools/batch-skeleton.mjs +59 -33
  32. package/agent-tools/batch.mjs +16 -6
  33. package/agent-tools/design-token.mjs +76 -16
  34. package/agent-tools/escalate-async.mjs +4 -2
  35. package/agent-tools/review-facts.mjs +143 -16
  36. package/agent-tools/subagent-actions-query.mjs +13 -6
  37. package/agent-tools/subagent-async.mjs +16 -4
  38. package/agent-tools/subagent-panel.mjs +121 -37
  39. package/agent-tools/subagent-spawn.mjs +1 -1
  40. package/agent-tools/subagent.mjs +5 -3
  41. package/agent-tools/verify.mjs +5 -0
  42. package/agent-tools/zero-write-watch.mjs +77 -0
  43. package/agent.mjs +1 -1
  44. package/auto-think.mjs +26 -2
  45. package/config-presets.mjs +8 -0
  46. package/config.mjs +30 -0
  47. package/conventions.mjs +26 -6
  48. package/declaration.mjs +26 -2
  49. package/embedding.mjs +35 -3
  50. package/escape.mjs +1 -1
  51. package/i18n.mjs +5 -0
  52. package/ledger-cmd.mjs +13 -21
  53. package/ledger-db.mjs +34 -1
  54. package/ledger-migrate.mjs +4 -3
  55. package/ledger-read.mjs +94 -0
  56. package/ledger-surface.mjs +15 -46
  57. package/ledger-tools.mjs +148 -85
  58. package/ledger-variant-notice.mjs +47 -0
  59. package/ledger.mjs +38 -64
  60. package/manifest-schema.mjs +19 -7
  61. package/manifest.mjs +32 -11
  62. package/mcp/transport-http.mjs +52 -4
  63. package/mcp/transport-stdio.mjs +33 -8
  64. package/mcp.mjs +13 -5
  65. package/memory/code-search.mjs +5 -3
  66. package/memory/core.mjs +6 -4
  67. package/memory/docs.mjs +5 -3
  68. package/memory/schema.mjs +2 -0
  69. package/package.json +1 -1
  70. package/prompts/advisor-design.md +3 -2
  71. package/prompts/advisor-round1.md +1 -0
  72. package/prompts/advisor-round2.md +8 -3
  73. package/prompts/advisor-round3.md +7 -2
  74. package/prompts/common.md +2 -2
  75. package/prompts/discipline-normal.md +2 -1
  76. package/prompts/persona-engineering.md +7 -4
  77. package/provider/anthropic.mjs +14 -2
  78. package/provider/core.mjs +10 -4
  79. package/provider/errors.mjs +23 -0
  80. package/provider/google.mjs +31 -4
  81. package/provider/normalize.mjs +57 -0
  82. package/provider/responses-request.mjs +3 -1
  83. package/provider/responses.mjs +38 -4
  84. package/provider/sse.mjs +45 -13
  85. package/proxy.mjs +13 -3
  86. package/session.mjs +13 -2
  87. package/shell-identity.mjs +107 -0
  88. package/stream-destroy.mjs +62 -0
  89. package/tool-docs/advisor.md +1 -1
  90. package/tool-docs/bash.md +1 -1
  91. package/tool-docs/batch.md +1 -1
  92. package/tool-docs/delete.md +7 -5
  93. package/tool-docs/ledger.md +6 -0
  94. package/tool-docs/subagent.md +10 -0
  95. package/tool-docs/verify.md +1 -1
  96. package/tools/bash.mjs +9 -4
  97. package/tools/edit-batch.mjs +7 -2
  98. package/tools/index.mjs +1 -4
  99. package/tools/patch.mjs +8 -2
  100. package/tools/write-path.mjs +6 -4
  101. package/undo-stack.mjs +23 -9
  102. package/tool-docs/ledger_add.md +0 -1
  103. package/tool-docs/ledger_close.md +0 -1
  104. package/tool-docs/ledger_count.md +0 -1
  105. package/tool-docs/ledger_query.md +0 -1
  106. package/tool-docs/ledger_update.md +0 -1
@@ -49,14 +49,22 @@ export async function runTurnLoop(agent, {
49
49
 
50
50
  // SUBAGENT-UPSTREAM-CHANNEL(AGENT-LOOP-UPSTREAM.md §6.27.4 消费点):子 → 父在飞消息的
51
51
  // 回合边界注入单点取用一次(模块缓存 ⇒ 每 run 一次代价);动态 import = 零新增静态边
52
- // (先例 = 上方 injectAsyncResult :113-117)。
52
+ // (同款先例 = run-start.mjs:43 ∥ :66——injectAsyncResult 动态 import)。
53
53
  const { drainChildUpstream } = await import("../agent-tools/parent-channel.mjs")
54
+ // #934(零落笔看门狗——AGENT-LOOP-UPSTREAM.md §6.32):连续零写越阈 ⇒ 自动上行提醒(只推——
55
+ // 不杀 / 不转向);单源叶 = zero-write-watch.mjs,动态 import 沿上条同款(零新增静态边)。
56
+ const { maybeZeroWriteAlert } = await import("../agent-tools/zero-write-watch.mjs")
57
+ // §6.31.5 落痕写点门(#939):载体吸收经 `carrierField` 单点(§6.31.4 同式);动态 import = 零新增静态边(沿上方两条同款)。
58
+ const { carrierField } = await import("../agent-tools/async-settle.mjs")
54
59
 
55
60
  // #417(撞帽载荷「本段零落盘轮数」——只报数:零阈值 / 零自动动作):计数基线——回合环逐轮
56
61
  // 比对 `_touchedFiles` 增量(轮顶对上一轮结账;撞帽收尾轮在环外单结)。
57
62
  agent._turnFilesMark = agent._touchedFiles.length
58
63
 
59
64
  for (let turn = 0; turn < maxTurns; turn++) {
65
+ // #793(核面小修批——AGENT-LOOP.md §6.2 环边界中止前置 ∥ 需求 F5):环头检查点——中止恒以
66
+ // AbortError 收束、不开启新轮(环头注入族 / 压缩 / 模型调用零触)。
67
+ if (signal?.aborted) throw abortError(signal, "agent", "turn-head")
60
68
  // 第 19 批(TURN-ACROSS-SEGMENTS——设计 TURN-CAP-CONTINUE.md §4):编号帧——
61
69
  // `_turnSeq` 每轮 +1(跨段累计,仅 `!resume` 链起点复位);面向消费面的两字段
62
70
  // (状态行 + ⟦ev⟧turn / ⟦ev⟧approval 事件共用)在此同点赋值(编号唯一权威;帧
@@ -67,7 +75,18 @@ export async function runTurnLoop(agent, {
67
75
  agent._maxTurns = frame.maxTurns
68
76
  // #417(撞帽载荷「本段零落盘轮数」——采集点与 `_turnSeq` 同源面):上一轮至今 `_touchedFiles`
69
77
  // 零增 ⇒ 上一轮零落盘,计数 +1;mark 前移(收尾轮在环外结账——见下方 ContinueError 前)。
70
- if (turn > 0 && agent._touchedFiles.length === agent._turnFilesMark) agent._zeroWriteTurns += 1
78
+ // #934(零落笔看门狗——§6.32.2 ① 单点采集):streak 与越阈判定同点——上轮零写 ⇒ +1 并判越阈;
79
+ // 有写 ⇒ streak 归零 + 清闩(写后重臂——每连续零写段恰一条)。#417 计数语义逐字保持。
80
+ if (turn > 0) {
81
+ if (agent._touchedFiles.length === agent._turnFilesMark) {
82
+ agent._zeroWriteTurns += 1
83
+ agent._zeroWriteStreak += 1
84
+ maybeZeroWriteAlert(agent) // #934:连续零写越阈 ⇒ 自动上行提醒(只推——§6.32)
85
+ } else {
86
+ agent._zeroWriteStreak = 0
87
+ agent._zeroWriteAlerted = false // 写后重臂
88
+ }
89
+ }
71
90
  agent._turnFilesMark = agent._touchedFiles.length
72
91
  // D2 (AGENT-LOOP-SUBAGENT.md §6.7.2): depth>0 children emit a ⟦ev⟧turn progress token each turn —
73
92
  // single emit point covering all three spawn tools; phase=llm (tool/done progress rides
@@ -106,6 +125,13 @@ export async function runTurnLoop(agent, {
106
125
  systemPrompt, toolSchemas, streamOutput, callbacks, signal, streamRuleFired, autoTurn, depth, turn,
107
126
  })
108
127
 
128
+ // §6.31.5 落痕(消化账务批 · 2026-10-05 · 台账 #930 ∥ #939 收正):本 run 内**逐轮追加**——
129
+ // 每模型轮返回即写(会话态);首轮签收不为收口轮覆盖(见账取件读累积痕)。
130
+ // 落父字段(抗 `history` 整体替换——`context.mjs` `applyCompression`);非会话零动作。
131
+ if (carrierField(agent, "_daSession") === true && response.content) {
132
+ agent._lastRunOutput = `${agent._lastRunOutput ?? ""}${response.content}\n`
133
+ }
134
+
109
135
  // 内置工具(Responses web_search)结果本地化:服务端已执行——入历史为 tool 消息;
110
136
  // 服务端 item id 是 msg_xxx 非 web_search_call_ 前缀——必须合成前缀(toItems 识别锚点),
111
137
  // 原始 id 存入 content(真机冒烟 2026-08-31 验证)。
@@ -240,5 +266,8 @@ export async function runTurnLoop(agent, {
240
266
  // #417(撞帽载荷「本段零落盘轮数」):收尾轮(撞帽轮)若零落盘同样计入——轮顶计数只
241
267
  // 覆盖到倒数第二轮(maxTurns ≤ 0 时环未跑,不虚计)。
242
268
  if (maxTurns > 0 && agent._touchedFiles.length === agent._turnFilesMark) agent._zeroWriteTurns += 1
269
+ // #793(核面小修批——AGENT-LOOP.md §6.2 环边界中止前置 ∥ 需求 F5):环尾检查点——中止恒以
270
+ // AbortError 收束(不落 ContinueError——用户 Stop 优先于继续提示;消费契约 = 外层按 reason 判定)。
271
+ if (signal?.aborted) throw abortError(signal, "agent", "turn-tail")
243
272
  throw new ContinueError(maxTurns)
244
273
  }
@@ -6,8 +6,10 @@
6
6
  * (KD-M4-3)——本档承载三个单点:
7
7
  *
8
8
  * 1. `resolveReviewTargetPaths(agent)` —— 评审目标解析单点:读 manifest `docRoot`
9
- * 产出评审对象 / 被审文件的绝对路径集合(目录级;子键值形态 = 串 | 多根数组,
10
- * 展开走 `docRootPaths`)。缺 `docRoot` 键 → `readManifest`
9
+ * 产出评审对象 / 被审文件的绝对路径集合(目录级;子键值形态 = 串 | 多根数组)。
10
+ * 判定本体(`REVIEW_ROOT_KEYS` 键集 + `resolveReviewRootsFor(dir)`——2026-10-02 收口 ·
11
+ * 台账 #828 按用点形)自本档迁 `review-facts.mjs`(2026-10-04 · #921 · KD-2——本档同名
12
+ * 再出口保既有 import 面);缺 `docRoot` 键 → `readManifest`
11
13
  * 的 fillDefaults 已补默认(架构 §2.3 E2 便利 fallback 落点);整档缺失 / 非法 JSON →
12
14
  * `{ok:false}` → 回退 DEFAULT_MANIFEST.docRoot(本函数是壳门之下的便利层——E2 的
13
15
  * 「整档缺失」由壳面 requireManifest 按用点处置(判 + 报明 / 建档,**不抛**——
@@ -15,8 +17,6 @@
15
17
  * KD-M1-12):CLI make-agent.mjs / VSC setup.mjs 同判);读错(权限等)→ 上抛不伪装成缺失(manifest.mjs 契约)。
16
18
  * M6 的 `advisor.mjs` design-review 分类分支消费本导出
17
19
  * (KD-M6-1——同源不重复实现;落 dispatch.mjs 会让 advisor 反向 import 门禁簇成环)。
18
- * **按用点形(2026-10-02 收口 · 台账 #828)**:`resolveReviewRootsFor(dir)`——同单点
19
- * 非第二实现(按目标目录 ∥ 所属项目根解析;多档锚下各文档按各自项目判定)。
20
20
  * 2. `freezeWindowConflict(agent, absPaths)` —— D5 冻结窗口判据组装:被审文件集 =
21
21
  * 声明文档集(`inflightDesignReviewConflict` 同源 docAbs 腿)+ 批次档(`run.batchDoc`
22
22
  * 腿——评审绑定批档在途时,写批档同样致 stale)。dispatch.mjs
@@ -30,7 +30,6 @@
30
30
  * 代码路径判定(conventions.mjs 单一权威分类——KD-M4-1 保留为代码路径判定用)。
31
31
  */
32
32
  import { basename, dirname, resolve } from "node:path"
33
- import { DEFAULT_MANIFEST, docRootPaths, readManifest } from "../manifest.mjs"
34
33
  // advisor-settle 无上游依赖本簇(dispatch → write-gate → advisor-settle 单向)。normAbs
35
34
  // 权威本体 = review-facts.mjs(第 33 批迁入),advisor-settle re-export;本档再 re-export
36
35
  // (指针链非副本),M6 经本档一行 import 取齐。
@@ -38,13 +37,17 @@ import { inflightDesignReviewConflict, normAbs } from "../agent-tools/advisor-se
38
37
  // 批次档路径单源(叶档——`BATCH-RECORD.md` §4.15):批次档写门基底集取 `batchDocBases`。
39
38
  import { batchDocBases } from "../agent-tools/batch-paths.mjs"
40
39
  export { normAbs }
41
-
42
- /** 评审目标解析读的 docRoot 键集(评审对象/被审文件可住的所有文档层——M6 分类据此判定)。 */
43
- const REVIEW_ROOT_KEYS = ["requirements", "specs", "design", "modules", "batches"]
40
+ // 2026-10-04(工具路径基面根治批 · #921 · KD-2 同名再出口):REVIEW_ROOT_KEYS +
41
+ // resolveReviewRootsFor 判定本体迁 `review-facts.mjs`(评审事实面——advisor-async 既有
42
+ // import 面零新模块边);本档同名再出口(指针非副本——normAbs 同款先例)——既有 import 面
43
+ // (advisor.mjs / 测试)零改;本档消费点 `resolveReviewTargetPaths` 薄委托同名函数。
44
+ export { REVIEW_ROOT_KEYS, resolveReviewRootsFor } from "../agent-tools/review-facts.mjs"
45
+ import { resolveReviewRootsFor } from "../agent-tools/review-facts.mjs"
44
46
 
45
47
  /**
46
- * 评审目标解析单点(M4 §2.1#1):读 manifest `docRoot` → 评审对象 / 被审文件的绝对路径
47
- * 集合(目录级,反斜杠归一;子键值形态 = 串 | 多根数组,逐键经 `docRootPaths` 展开)。
48
+ * 评审目标解析单点(M4 §2.1#1):评审对象 / 被审文件的绝对路径集合(目录级)——
49
+ * 判定本体(manifest `docRoot` 读面 / 缺档回退 / 读错上抛语义)2026-10-04 迁
50
+ * `review-facts.mjs` 单源(KD-2——本档薄委托;详述见该档 resolveReviewRootsFor JSDoc)。
48
51
  * 缺 `docRoot` 键 → readManifest 已补默认;整档缺失 / 非法 →
49
52
  * `{ok:false}` → 回退 DEFAULT_MANIFEST.docRoot(便利层——E2 的「整档缺失」由壳面
50
53
  * requireManifest 按用点处置(判 + 报明 / 建档,**不抛**——启动侧恒不拒,KD-M1-25;**工程模式会话**口径——KD-M1-12),本函数不重复拦);
@@ -56,21 +59,6 @@ export function resolveReviewTargetPaths(agent) {
56
59
  return resolveReviewRootsFor(agent?.cwd ?? process.cwd())
57
60
  }
58
61
 
59
- /** 同单点之**按用点解析**增量(2026-09-21「参数按用点解析」之裁收口 · 台账 #828):按**目标目录**
60
- * (其所属项目根——最近带档祖先)解析评审目标根集——多档锚下各文档按各自项目判定。
61
- * @param {string} dir 目标目录(项目根——最近带档祖先)
62
- * @returns {string[]} 评审目标目录绝对路径(去重)
63
- */
64
- export function resolveReviewRootsFor(dir) {
65
- const man = readManifest(dir)
66
- const root = man.ok && man.manifest?.docRoot && typeof man.manifest.docRoot === "object"
67
- ? man.manifest.docRoot
68
- : DEFAULT_MANIFEST.docRoot
69
- const out = []
70
- for (const key of REVIEW_ROOT_KEYS) out.push(...docRootPaths(root?.[key], dir))
71
- return [...new Set(out)]
72
- }
73
-
74
62
  /**
75
63
  * D5 冻结窗口判据组装(M4 §2.1#2):评审在途(点火 → 结算)期间父侧对被审文件集
76
64
  * (声明文档集 + 批次档)的写入 → 返回冲突 { id, path };无冲突 → null。
@@ -47,13 +47,13 @@ export {
47
47
  // 第 11 批拆分(2026-09-11):settle 记账 + 变更日志(noteMutations)+ 陈旧判定
48
48
  // (reviewIsStale)+ 冻结冲突(inflightDesignReviewConflict)迁 advisor-settle.mjs——
49
49
  // 既有 import 面(dispatch / subagent-async / escalate-async / 测试)经此 re-export 不变。
50
- import { normAbs, mutationSeqOf, settleAdvisorRun } from "./advisor-settle.mjs"
50
+ import { mutationSeqOf, settleAdvisorRun } from "./advisor-settle.mjs"
51
51
  export {
52
52
  mutationSeqOf, noteMutations, reviewIsStale, settleAdvisorRun, inflightDesignReviewConflict,
53
53
  } from "./advisor-settle.mjs"
54
54
  // 第 33 批(评审失败护栏):doc-set 键迁事实面档(实例续跑与陈旧判定同锚单源;原为私有——零 import 面)。
55
55
  // 本文件继续在实例解析 / 池 entries 上消费它。
56
- import { docSetKey } from "./review-facts.mjs"
56
+ import { docSetKey, resolveReviewDocPaths, scopeAdvisorMirror } from "./review-facts.mjs"
57
57
  // F31(2026-09-18 顾问面治理批):拒回文案对象标识行单源。
58
58
  import { withIdentityLine, scopeSummary } from "../advisor/notice.mjs"
59
59
  import { runAdvisorReview, resolveAdvisorProvider, ADVISOR_THINKING_PLACEHOLDER } from "../advisor/run.mjs"
@@ -65,6 +65,8 @@ import { deathLine } from "../abort-provenance.mjs"
65
65
  // ASYNC-RESULT-CONTAINER.md D3/D6:settle 公共收尾单点 + child signal 构建单点
66
66
  import { bindChildController, buildChildSignal, carrierField, getAsyncPool, settleAsyncEntry, tombstoneOf, writeTombstone } from "./async-settle.mjs"
67
67
  import { nextSubagentId, consumeSubagentToken, assertPoolKeyFree, entryTerminal } from "./subagent-scheduler.mjs"
68
+ // #934(零落笔看门狗——AGENT-LOOP-UPSTREAM.md §6.32.6 触发接线②):评审族推送单源(叶档——恒零抛)。
69
+ import { pushAdvisorAlert } from "./zero-write-watch.mjs"
68
70
 
69
71
  // ─────────────────────────────────────────────────────────────────────────────
70
72
  // Review-instance registry (_advisorRuns — per-review rounds/prior/cap)
@@ -135,6 +137,11 @@ export function resolveAdvisorLaunch(agent, reviewType, { documents = null } = {
135
137
  // the same id (no slot residue — T14: the old token is then rejected at
136
138
  // the gate); old slots die at TTL only, kept as the fail-safe fallback.
137
139
  const prior = [...runs.values()].reverse().find((r) => r.reviewType === "design" && r.docSetKey === key)
140
+ // #863 实例回收(§6.10):同 docSetKey 保最新一条 closed(= prior——F2h 只取 newest,语义零变);
141
+ // 余者去重回收(实例随删释放——closed 零消费面)。
142
+ for (const r of [...runs.values()]) {
143
+ if (r !== prior && r.reviewType === "design" && !r.open && r.docSetKey === key) runs.delete(r.reviewId)
144
+ }
138
145
  run = {
139
146
  reviewId, reviewType, designId: prior?.designId ?? reviewId,
140
147
  round: 0, priorOutput: null, stale: false, open: true, docSetKey: key,
@@ -144,6 +151,10 @@ export function resolveAdvisorLaunch(agent, reviewType, { documents = null } = {
144
151
  } else {
145
152
  run = openCodeRun(agent)
146
153
  if (!run) {
154
+ // #863 实例回收(§6.10):新 code 实例创建 ⇒ closed 实例清除(openCodeRun 仅取 open——零消费)。
155
+ for (const r of [...runs.values()]) {
156
+ if (r.reviewType === "code" && !r.open) runs.delete(r.reviewId)
157
+ }
147
158
  const reviewId = randomUUID()
148
159
  run = {
149
160
  reviewId, reviewType, designId: null,
@@ -152,9 +163,9 @@ export function resolveAdvisorLaunch(agent, reviewType, { documents = null } = {
152
163
  runs.set(reviewId, run)
153
164
  }
154
165
  }
155
- // Scope the legacy mirror fields (message builder + run.mjs cap + displays).
156
- agent._advisorRound = run.priorOutput ? run.round : 0
157
- agent._lastAdvisorOutput = run.priorOutput
166
+ // Scope the mirror fields to THIS instance (message builder + run.mjs cap +
167
+ // displays; #949 ① — the queued-launch window is re-secured at entry.start).
168
+ scopeAdvisorMirror(agent, run)
158
169
  return { run, isNew: run.round === 0 && !run.priorOutput, reviewId: run.reviewId, designId: run.designId }
159
170
  }
160
171
 
@@ -381,8 +392,11 @@ export function launchAsyncAdvisor(parent, ctx, launch) {
381
392
  id, role: "advisor", reviewType, run,
382
393
  reviewId: run.reviewId, designId, designToken, documents, paths, object,
383
394
  launchSeq: mutationSeqOf(parent),
395
+ // docAbs 产出面同源归一(2026-10-04 · #921 · A 随件):四腿单源 resolved(裸形/前缀形
396
+ // 命中候选项目根即归一为绝对路径)——冻结窗/陈旧判定对**裸形受理档**照护成立(否则 A
397
+ // 开冻结窗保护洞);绝对形与旧 normAbs 面逐字等价。
384
398
  docAbs: reviewType === "design" && Array.isArray(documents)
385
- ? documents.filter((d) => typeof d === "string").map((d) => normAbs(d, parent.cwd))
399
+ ? [...resolveReviewDocPaths(documents, parent.cwd).resolved.values()]
386
400
  : [],
387
401
  relayPrefix: `advisor#${id}/`,
388
402
  status: queued ? "queued" : "running", position: undefined,
@@ -411,9 +425,12 @@ export function launchAsyncAdvisor(parent, ctx, launch) {
411
425
  // Async mark + [model] — the TUI block opens at ACTUAL start (⏹ gating reads it).
412
426
  ctx?.callbacks?.onToken?.(entry.relayPrefix + "⟦ev⟧async\x1e")
413
427
  ctx?.callbacks?.onToken?.(entry.relayPrefix + "[model]" + (entry.model ?? ""))
428
+ // #949 ①:排队 / 延迟窗口封死——构建前按本评审实例复核镜像(消费点定域;幂等)。
429
+ scopeAdvisorMirror(parent, run)
414
430
  runAdvisorReview(parent, reviewType, {
415
431
  onOutput: (chunk) => relayAdvisorOutput(ctx?.callbacks, entry.relayPrefix, chunk),
416
432
  signal: entry.controller.signal,
433
+ onStall: (rounds) => pushAdvisorAlert(parent, `advisor#${id}`, rounds), // #934:连续无产出越阈 ⇒ 自动上行提醒(只推——§6.32)
417
434
  }, designToken, documents, paths, object, designId)
418
435
  .then((report) => { entry.report = report })
419
436
  // §6.12 第 3 条合成器(第 24 批):原 message 前缀逐字保留 + 来源后缀
@@ -474,7 +491,8 @@ export function closeOpenCodeAdvisorRuns(agent) {
474
491
  const runs = advisorRunsRead(agent)
475
492
  if (runs instanceof Map) {
476
493
  for (const r of runs.values()) {
477
- if (r.reviewType === "code" && r.open) { r.open = false; closed = true }
494
+ // #863 关闭点轻量化:闭合同拍释放 priorOutput(closed 实例零消费面——续跑仅取 open)。
495
+ if (r.reviewType === "code" && r.open) { r.open = false; r.priorOutput = null; closed = true }
478
496
  }
479
497
  }
480
498
  return closed
@@ -21,7 +21,7 @@
21
21
  * ADVISOR-GUARDS.md §7)。记账本体 / 陈旧判定 / 轮次 / token 落盘零改。
22
22
  */
23
23
  import { persistEngTokens } from "../token-ttl.mjs"
24
- import { settleDesignReview, makeDesignTokenRegex, stripApprovedSuffix } from "./design-token.mjs"
24
+ import { settleDesignReview, stripDesignTokenEcho, stripApprovedSuffix } from "./design-token.mjs"
25
25
  import { looksLikeReviewOutput, advisorIncompleteMarker, ADVISOR_LAUNCH_REFUSAL_PREFIX } from "../advisor/run.mjs"
26
26
  import { isCodePath, loadProjectDeclaration } from "../conventions.mjs"
27
27
  import { logEvent } from "../log.mjs"
@@ -176,9 +176,7 @@ export function settleAdvisorRun(agent, entry) {
176
176
  run.approvedSuffix = null
177
177
  persistFailed = true
178
178
  logEvent("advisor:error", { id: `advisor#${entry.id}`, err: "engDesignTokens slot persist failed — settle failed (re-review)" })
179
- const stripped = String(result)
180
- .replace(makeDesignTokenRegex(entry.designToken, "g"), "")
181
- .trim()
179
+ const stripped = stripDesignTokenEcho(String(result), entry.designToken).trim()
182
180
  report = `${stripped}\n\nD1: the design review passed but the token could NOT be durably written to the session ledger (slot persist failed) — re-run advisor(type='design') to re-issue; no eng-coder spawn is authorized for this review (评审通过但 token 未能持久化——需重评).`.trim()
183
181
  }
184
182
  } else {
@@ -210,17 +208,17 @@ export function settleAdvisorRun(agent, entry) {
210
208
  }
211
209
  } else if (run.reviewType === "design" && entry.designToken && result != null) {
212
210
  // fix B(stale 分支):陈旧评审不签发——digest 不得展示未注册 token——先剥
213
- // 方括号回显 + 前置 "评审目标已变更——token 未签发"(不变式——两分支都清洗)。
214
- const stripped = String(result)
215
- .replace(makeDesignTokenRegex(entry.designToken, "g"), "")
216
- .trim()
211
+ // token 回显(单源三形)+ 前置 "评审目标已变更——token 未签发"(不变式——两分支都清洗)。
212
+ const stripped = stripDesignTokenEcho(String(result), entry.designToken).trim()
217
213
  report = `评审目标已变更——token 未签发 (review target changed after launch — this review judged a stale state; no design token was issued — re-run the review on the current state)\n\n${stripped}`.trim()
218
214
  }
219
215
  // Prior of round 2+ = the last REVIEW-LOOKING output (mirror of run.mjs's guard).
220
216
  // F2e: strip the engine-approved suffix FIRST — the prior must never
221
217
  // carry the raw token / designId (exact truncation — zero collateral).
222
218
  // 结论块(下方)刻意晚于 prior 归一:prior 只承载评审正文,不携带机制文案。
223
- if (report && looksLikeReviewOutput(report)) {
219
+ // #863 关闭点轻量化(父侧批·五裁定——§2.3 表外追加):closed 实例零消费面(续跑仅取 open)——
220
+ // 关闭后不再写回 priorOutput(否则 settleDesignReview 关闭点的释放被覆盖)。
221
+ if (run.open && report && looksLikeReviewOutput(report)) {
224
222
  run.priorOutput = stripApprovedSuffix(report, run.approvedSuffix)
225
223
  }
226
224
  // F28/F29 失败结论(2026-09-18 顾问面治理批——**两轨共用**,ADVISOR-GUARDS.md §7 契约二):
@@ -9,12 +9,11 @@
9
9
  */
10
10
  import { runAdvisorReview, advisorIncompleteMarker, ADVISOR_LAUNCH_REFUSAL_PREFIX } from "../advisor/run.mjs"
11
11
  import { resolveBatchDocPath } from "./batch.mjs"
12
- // M6(模块设计 §2.1 F3):评审对象来源读 manifest docRoot(声明面)——复用 M4 的
13
- // write-gate.mjs 单一权威源(KD-M6-1),替代 v1 的逐档分类面(isDocPath);
14
- // normAbs 同源 re-export(指针非副本)。不 import dispatch.mjs(簇间回边,环风险)。
15
- import { resolveReviewTargetPaths, resolveReviewRootsFor, normAbs } from "../agent/write-gate.mjs"
16
- import { owningProject } from "../manifest-discovery.mjs"
17
- import { sep } from "node:path"
12
+ // M6(模块设计 §2.1 F3):评审对象来源读 manifest docRoot(声明面)——单一权威源 =
13
+ // review-facts.mjs 的 resolveReviewDocPaths(2026-10-04 · #921 · KD-2——分类四腿 + B 文案
14
+ // 诊断数据单源;write-gate 同名再出口保 normAbs 面)。不 import dispatch.mjs(簇间回边,环风险)。
15
+ import { normAbs } from "../agent/write-gate.mjs"
16
+ import { resolveReviewDocPaths } from "./review-facts.mjs"
18
17
  import {
19
18
  generateDesignToken,
20
19
  settleDesignReview,
@@ -63,11 +62,11 @@ export const advisorTool = {
63
62
  documents: {
64
63
  type: "array",
65
64
  items: { type: "string" },
66
- description: "Doc paths to review (design / requirements / referenced docs). The advisor reviews ONLY these — no git diff scan. Use for both review types to pass the task's docs list.",
65
+ description: "Doc paths to review (design / requirements / referenced docs). Relative paths resolve against the session cwd first, then candidate project roots (project-root-relative e.g. <repo>/docs/…, or absolute). The advisor reviews ONLY these — no git diff scan. Use for both review types to pass the task's docs list.",
67
66
  },
68
67
  batchDoc: {
69
68
  type: "string",
70
- description: "Design review only: the in-flight batch record path — validated whenever passed (unreadable ⇒ refused, not ignored); the reviewer then also gets the `batch` write channel to record its findings + verdict. Omit when no batch record is in flight (no write channel — zero regression).",
69
+ description: "Design review only: the in-flight batch record path — validated whenever passed (unreadable ⇒ refused, not ignored); the reviewer then also gets the `batch` write channel to record its findings + verdict. Relative paths resolve against the session cwd first, then candidate project roots (project-root-relative e.g. <repo>/docs/batches/…, or absolute). Omit when no batch record is in flight (no write channel — zero regression).",
71
70
  },
72
71
  },
73
72
  required: ["type"],
@@ -112,34 +111,36 @@ export const advisorTool = {
112
111
  )
113
112
  }
114
113
 
115
- // Design review: the review scope must be documentation files. Classification
116
- // comes from the manifest-declared review-target roots (M4 write-gate.mjs single
117
- // authority — FR12: no directory-name hardcoding; a project whose docs live
118
- // elsewhere declares them in PROJECT-MANIFEST.json docRoot).
114
+ // Design review: the review scope must be documentation files. Classification =
115
+ // resolveReviewDocPaths (2026-10-04 · #921 · the four-leg single authority in
116
+ // review-facts.mjs): legs ① session-root set / ② owning-project root set keep the
117
+ // legacy behavior verbatim; leg ③ probes candidate project roots for bare
118
+ // project-root-relative forms; leg ④ disambiguates by readable-file uniqueness
119
+ // (fail-closed — criterion=scope-doc-ambiguous when ≥2 candidates are readable).
119
120
  if (reviewType === "design" && documents) {
120
- const roots = resolveReviewTargetPaths(agent).map((r) => r.replace(/[\\/]/g, sep))
121
- // 按用点解析(2026-09-21 之裁收口 · 台账 #828):逐文档按**其所属项目**(最近带档祖先)
122
- // 的 docRoot 判定;无主档文档回退会话根集判定(原行为零变)。
123
- const ownerRootsCache = new Map()
124
- const ownRoots = (abs) => {
125
- const owner = owningProject(abs)
126
- if (!owner) return []
127
- let r = ownerRootsCache.get(owner)
128
- if (!r) {
129
- r = resolveReviewRootsFor(owner).map((x) => x.replace(/[\\/]/g, sep))
130
- ownerRootsCache.set(owner, r)
131
- }
132
- return r
133
- }
134
- const invalidDocs = documents.filter((doc) => {
135
- const n = normAbs(doc, agent.cwd).replace(/[\\/]/g, sep)
136
- const under = (r) => n === r || n.startsWith(r + sep)
137
- return !roots.some(under) && !ownRoots(n).some(under)
138
- })
139
- if (invalidDocs.length > 0) {
121
+ const { resolved, invalid, ambiguous, candidates } = resolveReviewDocPaths(documents, agent.cwd)
122
+ if (invalid.length > 0 || ambiguous) {
140
123
  if (ctx._toolCallId !== undefined) (agent._advisorRefusals ??= new Set()).add(ctx._toolCallId)
124
+ if (ambiguous) {
125
+ // 歧义拒(KD-4 独立判据——不混入 not-doc;列全部可读命中——fail-closed 不静默挑一)。
126
+ return withIdentityLine(
127
+ `Advisor: design review document is ambiguous — "${ambiguous.doc}" matches several candidate projects: ${ambiguous.hits.join(", ")}. Pass a project-root-relative or absolute path.`,
128
+ { type: reviewType, scope: scopeSummary(documents), round: "—", criterion: "scope-doc-ambiguous" },
129
+ )
130
+ }
131
+ // not-doc 拒:既有稳定前缀逐字(行首)+ 解析诊断尾(B 面根治——逐文档「解析后绝对
132
+ // 路径 ← 基面 = 会话 cwd」+ 候选项目根提示 cap 5 + 重试指引——纠正「误报因」缺陷)。
133
+ const invalidList = invalid.map((v) => v.doc).join(", ")
134
+ const attemptedList = invalid
135
+ .map((v) => `${v.doc} → ${v.attempted.length > 0 ? v.attempted.join(" | ") : normAbs(v.doc, agent.cwd)}`)
136
+ .join("; ")
137
+ const shown = candidates.slice(0, 5)
138
+ const more = candidates.length - shown.length
139
+ const candidateNote = shown.length > 0
140
+ ? `(candidates: ${shown.join(", ")}${more > 0 ? ` +${more} more` : ""}) — pass project-root-relative (e.g. ${shown[0]}/docs/…) or absolute paths`
141
+ : "— pass project-root-relative or absolute paths"
141
142
  return withIdentityLine(
142
- `Advisor: design review documents must be documentation files (per the project's conventions). Invalid: ${invalidDocs.join(", ")}`,
143
+ `Advisor: design review documents must be documentation files (per the project's conventions). Invalid: ${invalidList}\nDiagnosis: resolved against session cwd ${agent.cwd} → ${attemptedList}; no candidate project docRoot contains it ${candidateNote}.`,
143
144
  { type: reviewType, scope: scopeSummary(documents), round: "—", criterion: "scope-not-doc" },
144
145
  )
145
146
  }
@@ -134,6 +134,17 @@ export function parkAsyncPending(parent, entry) {
134
134
  pend.push(entry)
135
135
  }
136
136
 
137
+ /**
138
+ * §6.31.5 升级账本写助手(消化账务批):`_unsettledDigests` 追加一行
139
+ * (`{ id, role, failures, report, at }`)。载体吸收(父字段优先 / history 借用——形态同
140
+ * `parkAsyncPending`);行序 = 升级序(读面 = `digest-account.mjs` `unsettledRows`)。
141
+ */
142
+ export function appendUnsettledDigest(parent, row) {
143
+ const existing = carrierField(parent, "_unsettledDigests")
144
+ const list = (parent._unsettledDigests ??= Array.isArray(existing) ? existing : [])
145
+ list.push(row)
146
+ }
147
+
137
148
  // ─── D4 守卫统一(严格版)+ D6 信号兜底单点 ──────────────────────────────────
138
149
 
139
150
  /** D4 settle 守卫统一(escalate 严格版):父侧中止 = 回合 signal aborted(#98 VSC 侧
@@ -161,7 +172,9 @@ export function buildChildSignal(parent, ctx) {
161
172
  * 对子代理对象的引用(`childAgent`/`report`——条目前此从不显式释放,挂起期 = 分钟级驻留)。
162
173
  * 幂等(重入不抛);池内/挂起未消化窗口语义零变(settle 时刻不释放——表 2 候选 2 否决:
163
174
  * status/observe 在窗口内仍读 child 摘要)。三消费点(回合尾收集 / run 起始 pending 注入 /
164
- * 挂起残差注入)在注入完成后调本 helper。
175
+ * 挂起残差注入)——fallback 面(无 `_daSession`)注入完成后调本 helper(逐字沿用);会话态
176
+ * (`_daSession`)注入后改调 `releaseChildHold`(仅释放 `childAgent`——`report` 保留至销账 /
177
+ * 升级;§6.31.4 ∥ KD-DA7)。
165
178
  */
166
179
  export function releaseSettledEntry(entry) {
167
180
  if (!entry || typeof entry !== "object") return
@@ -169,6 +182,16 @@ export function releaseSettledEntry(entry) {
169
182
  entry.report = null
170
183
  }
171
184
 
185
+ /**
186
+ * §6.31.4 投递面(消化账务批 · 2026-10-05 · 台账 #930):会话态注入后**仅**释放 `childAgent`——
187
+ * `report` 保留至销账 / 升级(重投需原文;驻留增量以报告量级 ∥ 重投上限封顶)。fallback 面零调用
188
+ * (既有 `releaseSettledEntry` 逐字沿用——注入即释放)。
189
+ */
190
+ export function releaseChildHold(entry) {
191
+ if (!entry || typeof entry !== "object") return
192
+ entry.childAgent = null
193
+ }
194
+
172
195
  /**
173
196
  * D3 settle 共享 helper——四族 settle 回调公共收尾单点(ASYNC-RESULT-CONTAINER.md
174
197
  * AC1:settle 记账单点,无逐字重复)。流程:
@@ -273,10 +296,11 @@ export function settleAsyncEntry(parent, entry, opts = {}) {
273
296
  parkAsyncPending(parent, entry)
274
297
  pool?.delete(String(entry.id))
275
298
  }
276
- // 发射单点(块到达时点归位批 · #746|§6.8「settle 延迟冻结(两态统一)」):settle 一律 ⟦ev⟧settled
277
- // (区块头恒中间态「done · awaiting digestion」驻留面板);⟦ev⟧done = 消费面补发(消费窗清单单源
278
- // = §6.8)⇒ 冻结 ∥ 归档恒落消费时点——回合运行中零归档触发器(「S 不夹运行中回合」结构不变式)。
279
- ctx?.callbacks?.onToken?.(`${entry.relayPrefix}⟦ev⟧settled\x1e0\x1e0\x1esettled\x1e`)
299
+ // 发射单点(块到达时点归位批 · #746|§6.8「settle 延迟冻结(两态统一)」):settle 一律 ⟦ev⟧settled
300
+ // (区块头恒中间态「done · awaiting digestion」驻留面板);⟦ev⟧done = 消费面补发(消费窗清单单源
301
+ // = §6.8)⇒ 冻结 ∥ 归档恒落投递时点(消化账务批 · 2026-10-05——归档 ≠ 销账;销账后移见账 = §6.31);
302
+ // 回合运行中零归档触发器(「S 不夹运行中回合」结构不变式)。
303
+ ctx?.callbacks?.onToken?.(`${entry.relayPrefix}⟦ev⟧settled\x1e0\x1e0\x1esettled\x1e`)
280
304
  }
281
305
  // ④ 公共尾部:settleSeq 递增 + _settle 唤醒(never rejects)+ 唤醒挂起驱动 waiter
282
306
  entry._settleSeq = (parent._asyncSettleSeq = (parent._asyncSettleSeq ?? 0) + 1)
@@ -49,6 +49,18 @@ function segmentNumber(raw) {
49
49
  return m ? Number(m[1]) : null
50
50
  }
51
51
 
52
+ /** 档头两可选参归一(`BATCH-RECORD.md` §4.11 契约——逐字文本面 = `TOOLS.md` §6.20):trim ∥
53
+ * 单行(拒句逐字)∥ 空串 ≡ 未给(null);`ledger` 前导 `#` 补齐(board 无补齐)。给参 ⇒ 建即填
54
+ * 台账行(batchSkeleton 实参面);未给(任一)⇒ 占位留存 + 回执填法句。 */
55
+ function normalizeHeaderParam(name, raw) {
56
+ const v = typeof raw === "string" ? raw.trim() : ""
57
+ if (!v) return null
58
+ if (/\r?\n/.test(v)) {
59
+ throw new Error("batch: create ledger / board must be single lines — a multi-line value would break the header's 台账 line. Nothing was written.")
60
+ }
61
+ return name === "ledger" && !v.startsWith("#") ? `#${v}` : v
62
+ }
63
+
52
64
  /** main-agent-only 门(BR-19/BR-24):create/close 仅 depth-0 放行(eng 子代理 / 评审皆拒)。 */
53
65
  function assertMainAgentOnly(ctx, action, review) {
54
66
  if (review || !isDepthZero(ctx)) {
@@ -58,8 +70,9 @@ function assertMainAgentOnly(ctx, action, review) {
58
70
 
59
71
  /** 递归收集目录下全部 `*.md`(#369 · KD-4:显式嵌套路径为合法 create 面 ⇒ 顶层平扫会欠计
60
72
  * ⇒ 复数情形漏判、默认定位可落非预期档)。只认真目录(`dirent.isDirectory()`——symlink 不入,
61
- * 防环);单目录读错按无候选处理(尽力面,同旧平扫)。 */
62
- function collectMarkdownFiles(dir, out = []) {
73
+ * 防环);单目录读错按无候选处理(尽力面,同旧平扫)。**转导出**(read-data-interface 批——
74
+ * `batch/list` 读面文件集同源复用;零行为变)。 */
75
+ export function collectMarkdownFiles(dir, out = []) {
63
76
  let entries = []
64
77
  try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return out }
65
78
  for (const e of entries) {
@@ -107,6 +120,8 @@ export function findInFlightBatch(cwd, bases) {
107
120
  * 路径解析 = `resolveBatchCreatePath`(§4.15 单源:候选序首个落基底内者 / 锚定串只解析项目根形)。
108
121
  * F11-B:`source` **必填**(topic 同款空拒——骨架编制行实参化;不传 = 死占位残留必被 F11-C 拒
109
122
  * ⇒ create 即拒,不留死锁);`prev` 传入值**幂等剥**「前情 = 」前缀(值规范化;默认值零变)。
123
+ * 2026-10-05(A-2):`ledger` / `board` 可选参**建即填**台账行(归一契约 = §4.11;回执两态 =
124
+ * 两参皆给 ⇒ 标注已填 ∥ 未给(任一)⇒ 填法句)。
110
125
  * @returns {string} 成功消息(含落盘绝对路径)
111
126
  */
112
127
  export function createBatchRecord({ args, ctx, review, cwd, bases, onWritten }) {
@@ -137,15 +152,22 @@ export function createBatchRecord({ args, ctx, review, cwd, bases, onWritten })
137
152
  if (/\r?\n/.test(source)) {
138
153
  throw new Error("batch: create source must be a single line — a multi-line source would break the header's 编制 line. Nothing was written.")
139
154
  }
155
+ // 档头两可选参(§4.11 契约——给则建即填台账行;未给任一 ⇒ 占位留存 + 回执填法句)
156
+ const ledger = normalizeHeaderParam("ledger", args?.ledger)
157
+ const board = normalizeHeaderParam("board", args?.board)
140
158
  const date = typeof args?.date === "string" && args.date.trim() ? args.date.trim() : todayLocal()
141
159
  // F11-B:prev 幂等 strip——调用方自带「前情 = 」前缀(单或重复)一并剥净(`+` 量词;归一化目的
142
160
  // = 值规范化,无前缀值原样通过)。默认值形态「无(独立批)」零变。
143
161
  const prevRaw = typeof args?.prev === "string" ? args.prev.trim() : ""
144
162
  const prev = prevRaw ? (prevRaw.replace(/^(?:前情\s*[=::]\s*)+/g, "").trim() || "无(独立批)") : "无(独立批)"
145
163
  mkdirSync(dirname(abs), { recursive: true })
146
- writeFileSync(abs, batchSkeleton({ date, topic, source, prev }))
164
+ writeFileSync(abs, batchSkeleton({ date, topic, source, prev, ledger, board }))
147
165
  onWritten?.(ctx?.agent ?? {}, abs)
148
- return `batch: created ${abs} — six-section skeleton written (§1 status line carries the gate-legal 「进行中」 placeholder; register the ledger entry yourself — create does not).`
166
+ // 回执两态(`TOOLS.md` §6.20 逐字):两参皆给 ⇒ 标注已填;未给(任一)⇒ 填法句(填法 = 文件编辑)
167
+ const headerNote = ledger !== null && board !== null
168
+ ? `header 台账 line filled (${ledger} · ${board})`
169
+ : "header 台账 placeholder(s) remain — fill them by file edit before the record's first append/status opens"
170
+ return `batch: created ${abs} — six-section skeleton written (§1 status line carries the gate-legal 「进行中」 placeholder; register the ledger entry yourself — create does not). ${headerNote}`
149
171
  }
150
172
 
151
173
  /**
@@ -264,7 +286,7 @@ function assertStatusNote(note) {
264
286
  * 恰一词(**余核 = 关键词**——F11-A 谓词收紧),散文说明走独立 `note` 字段(落状态行括注);
265
287
  * 冻结真值不变(gate 只读 §1)。path 参数 = 仅 depth-0(D-BR21)——子代理/评审传 path
266
288
  * ⇒ 拒(目标 = spawn/实例注入,语法上写不到别处)。F11-C 挂点 = 写盘前(assertStatusValue 后):
267
- * 档头 / 目标段含骨架死占位 ⇒ 拒(close 不拦——收口是主 agent 终态动作)。
289
+ * 档头结构行含骨架死占位 ⇒ 拒(段体引用豁免;close 不拦——收口是主 agent 终态动作)。
268
290
  * @returns {string} 成功消息
269
291
  */
270
292
  export function statusBatchRecord({ args, ctx, review, pickTarget, onWritten }) {
@@ -297,8 +319,8 @@ export function statusBatchRecord({ args, ctx, review, pickTarget, onWritten })
297
319
  const abs = pickTarget(args?.path)
298
320
  const src = readFileSync(abs, "utf8")
299
321
  assertGateOpen(src)
300
- // F11-C(写盘前):档头 + 本段死占位残留 ⇒ 拒(他段占位不归本段作者管)
301
- const residues = findPlaceholderResidue(src, seg)
322
+ // F11-C(写盘前):档头结构行死占位残留 ⇒ 拒(段体引用豁免——判据可辨性收正,`TOOLS.md` §6.20)
323
+ const residues = findPlaceholderResidue(src)
302
324
  if (residues.length) throw new Error(placeholderResidueError(residues))
303
325
  const written = updateSectionStatusLine(src, seg, lineValue)
304
326
  writeFileSync(abs, written)
@@ -10,6 +10,7 @@
10
10
  * **歧义锚**(条 5 · 2026-10-02 · #828)= create 按目标所属项目落基底 ∥ 无所属显式拒;读面候选腿追加;
11
11
  * 在飞 ∥ 写门基底集 = 候选并集(`batchDocBases`);**零变面** = 绝对路径照旧、三个错误文案逐字、
12
12
  * 「参数在 + 路径可读」判据句、`ok` / `none` 两态。
13
+ * **基底自有项目根腿 + 基底相对单段收窄**(条 6 · 2026-10-05 · #942)= 逐基底 `owningProject` 恰一落点收 ∥ 多落点显式拒(`ambiguousLandingError`);基底相对腿限单段(多段串不二次拼接)。
13
14
  *
14
15
  * 依赖(KD-4 单向):本档为**叶档**——只 import `node:*` 与 `../manifest.mjs`;主档 re-export 保既有面。
15
16
  */
@@ -98,6 +99,7 @@ const outsideBasesError = (raw) => new Error(`batch: create path resolves outsid
98
99
  const anchoredEscapeError = (raw) => new Error(`batch: create path is anchored at a batch base root but does not resolve under the declared docRoot.batches root(s) — refusing to nest it (fail-closed). Path: ${raw}`)
99
100
  /** 歧义锚 create 拒面(**文案逐字** = `BATCH-RECORD.md` §4.15 条 5——机检锚 `ambiguous session anchor`)。 */
100
101
  const ambiguousCreateError = (raw, candidates) => new Error(`batch: ambiguous session anchor — create target "${raw}" belongs to no candidate project; pass an explicit project path (e.g. "${candidates[0]}/docs/batches/<file>.md"). Candidates: ${candidates.join(", ")}`)
102
+ const ambiguousLandingError = (raw, landings) => new Error(`batch: create path resolves under more than one declared batch-record base — refusing to pick one (fail-closed); pass an explicit path (absolute preferred). Path: ${raw}. Candidates: ${landings.join(", ")}`)
101
103
 
102
104
  /** 歧义锚读面候选腿(条 5):逐候选项目——项目根形(`resolve(candidate, p)`)∥ 声明基底形
103
105
  * (候选按名排序、并集保序——「首个可读」在此即为确定判读)。 */
@@ -149,7 +151,7 @@ export function resolveBatchDocPath(cwd, given) {
149
151
  }
150
152
 
151
153
  /**
152
- * **create 面**解析(§4.15 条 2/3):绝对路径照旧取用(仍过越基底判据);相对路径 = 候选序中首个**落基底
154
+ * **create 面**解析(§4.15 条 2/3/6):绝对路径照旧取用(仍过越基底判据);相对路径 = 候选序中首个**落基底
153
155
  * 根内**者(新档不存在——可读性无判别力);锚定串只解析项目根形,不落基底内 ⇒ fail-closed 新文案。
154
156
  * **歧义锚**(条 5):目标所属项目(`owningProject`)∈ 锚候选集 ⇒ 基底 = 该项目声明面(落其基底内;
155
157
  * 越出 ⇒ 既有 fail-closed);无所属 ⇒ **显式拒**(列候选 + 显式路径指引——不静默落锚)。
@@ -179,8 +181,26 @@ export function resolveBatchCreatePath(cwd, given, bases) {
179
181
  if (!insideBases(abs, roots)) throw anchoredEscapeError(raw)
180
182
  return abs
181
183
  }
182
- for (const abs of [resolve(base, p), resolve(batchProjectRoot(base), p), ...roots.map((b) => resolve(b, p))]) {
184
+ for (const abs of [resolve(base, p), resolve(batchProjectRoot(base), p)]) {
183
185
  if (insideBases(abs, roots)) return abs
184
186
  }
187
+ // ③ 基底自有项目根形(#942 条 6):owningProject 可判者逐基底取值;distinct 落点 ≥2 ⇒ 拒
188
+ const owned = []
189
+ for (const b of roots) {
190
+ const ownRoot = owningProject(b)
191
+ if (!ownRoot) continue
192
+ const abs = resolve(ownRoot, p)
193
+ if (insideBases(abs, roots) && !owned.some((x) => samePath(x, abs))) owned.push(abs)
194
+ }
195
+ if (owned.length > 1) throw ambiguousLandingError(raw, owned)
196
+ if (owned.length === 1) return owned[0]
197
+ // ④ 基底相对形收窄单段(归一后无 `/` 才走原基底腿——多段串不二次拼接)
198
+ const leafRel = roots.length ? relative(roots[0], resolve(roots[0], p)) : ""
199
+ if (leafRel && !leafRel.startsWith("..") && !leafRel.includes(sep)) {
200
+ for (const b of roots) {
201
+ const abs = resolve(b, p)
202
+ if (insideBases(abs, roots)) return abs
203
+ }
204
+ }
185
205
  throw outsideBasesError(raw)
186
206
  }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * agent-tools/batch-read.mjs — 批次档只读读面(read-data-interface 批 · 设计档
3
+ * `docs/cli/design/READ-DATA-INTERFACE.md` §2.5 / §3.3 · 台账 #887)。
4
+ *
5
+ * `listBatchRecords({ cwd, bases })` —— ACP `batch/list` 与变更观察器共用的**文件集 + 段状态单源**:
6
+ * 文件集 = 基底根(`batchDocBases`——声明面单源)下全部 `*.md`(递归;收集器 `collectMarkdownFiles`
7
+ * 转导出复用);路径升序稳定;单档读错跳过(尽力面)。各段状态 = `readSectionStatusWord`(段头 /
8
+ * 状态行正则 / 词表全消费 batch-skeleton 单源——与冻结门字面零漂移;`§4` / `§6` = 字面「无状态词」)。
9
+ */
10
+ import { readFileSync } from "node:fs"
11
+ import { basename } from "node:path"
12
+
13
+ import { collectMarkdownFiles } from "./batch-lifecycle.mjs"
14
+ import { batchDocBases } from "./batch-paths.mjs"
15
+ import { readSectionStatusWord } from "./batch-skeleton.mjs"
16
+
17
+ /** 段号表(1–6——输出映射键序固定;`readSectionStatusWord` 消费数形段号)。 */
18
+ const SEGMENTS = [1, 2, 3, 4, 5, 6]
19
+
20
+ /** 批次档列举(只读):`{ batches: [{ file, path, sections }] }`——`path` = 绝对路径(`/` 归一)、
21
+ * `file` = basename、`batches` 按路径升序;单档读错跳过(尽力面)。`bases` 缺省 = `batchDocBases(cwd)`。 */
22
+ export function listBatchRecords({ cwd, bases } = {}) {
23
+ const base = cwd ?? process.cwd()
24
+ const roots = bases ?? batchDocBases(base)
25
+ const files = new Set()
26
+ for (const root of roots) for (const abs of collectMarkdownFiles(root)) files.add(abs.replace(/\\/g, "/"))
27
+ const batches = []
28
+ for (const p of [...files].sort()) {
29
+ let src
30
+ try { src = readFileSync(p, "utf8") } catch { continue } // 单档读错跳过(尽力面)
31
+ const sections = {}
32
+ for (const seg of SEGMENTS) sections[`§${seg}`] = readSectionStatusWord(src, seg)
33
+ batches.push({ file: basename(p), path: p, sections })
34
+ }
35
+ return { batches }
36
+ }