@thincoder/core 0.10.13 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/advisor/messages.mjs +4 -2
  3. package/advisor/notice.mjs +1 -1
  4. package/advisor/project-context.mjs +121 -11
  5. package/agent/completion.mjs +13 -1
  6. package/agent/digest-account.mjs +73 -46
  7. package/agent/dispatch-run.mjs +1 -1
  8. package/agent/dispatch.mjs +29 -21
  9. package/agent/family-tools.mjs +9 -5
  10. package/agent/helpers.mjs +1 -94
  11. package/agent/offload.mjs +88 -0
  12. package/agent/record-results.mjs +1 -6
  13. package/agent/run-stages.mjs +69 -48
  14. package/agent/run-start.mjs +8 -8
  15. package/agent/setup.mjs +3 -11
  16. package/agent/suspension.mjs +1 -1
  17. package/agent/turn-loop.mjs +32 -3
  18. package/agent/write-face.mjs +99 -0
  19. package/agent/write-gate.mjs +354 -5
  20. package/agent-tools/advisor.mjs +127 -11
  21. package/agent-tools/async-settle.mjs +2 -2
  22. package/agent-tools/bash-async.mjs +8 -2
  23. package/agent-tools/batch-closeout.mjs +206 -0
  24. package/agent-tools/batch-gate.mjs +287 -73
  25. package/agent-tools/batch-ledger-bind.mjs +183 -0
  26. package/agent-tools/batch-ledger-link.mjs +73 -0
  27. package/agent-tools/batch-lifecycle.mjs +113 -367
  28. package/agent-tools/batch-paths.mjs +45 -40
  29. package/agent-tools/batch-read.mjs +1 -1
  30. package/agent-tools/batch-reopen.mjs +142 -0
  31. package/agent-tools/batch-skeleton.mjs +275 -80
  32. package/agent-tools/batch.mjs +243 -50
  33. package/agent-tools/context.mjs +6 -3
  34. package/agent-tools/design-slots.mjs +24 -24
  35. package/agent-tools/design-token.mjs +96 -15
  36. package/agent-tools/doc-check.mjs +18 -2
  37. package/agent-tools/eng.mjs +1 -1
  38. package/agent-tools/escalate-async.mjs +3 -3
  39. package/agent-tools/manifest.mjs +8 -5
  40. package/agent-tools/read-history.mjs +5 -5
  41. package/agent-tools/review-facts.mjs +31 -13
  42. package/agent-tools/spawn-gates.mjs +10 -14
  43. package/agent-tools/ssh-session.mjs +120 -111
  44. package/agent-tools/ssh-transfer.mjs +210 -0
  45. package/agent-tools/ssh-transport.mjs +277 -46
  46. package/agent-tools/subagent-actions-query.mjs +1 -1
  47. package/agent-tools/subagent-actions.mjs +1 -1
  48. package/agent-tools/subagent-async.mjs +3 -2
  49. package/agent-tools/subagent-scheduler.mjs +43 -9
  50. package/agent-tools/subagent-spawn.mjs +68 -9
  51. package/agent-tools/subagent.mjs +16 -14
  52. package/client-identity.mjs +22 -6
  53. package/config-presets.mjs +5 -3
  54. package/context.mjs +34 -3
  55. package/declaration.mjs +2 -2
  56. package/doc-check/anchors.mjs +128 -28
  57. package/doc-check/index.mjs +68 -15
  58. package/doc-check/line-refs.mjs +112 -0
  59. package/doc-check/paragraph.mjs +18 -5
  60. package/explore-distill.mjs +1 -1
  61. package/i18n.mjs +1 -1
  62. package/index-discover.mjs +4 -1
  63. package/ledger-close-proof.mjs +19 -37
  64. package/ledger-cmd.mjs +71 -195
  65. package/ledger-db.mjs +29 -28
  66. package/ledger-migrate.mjs +4 -4
  67. package/ledger-read.mjs +10 -8
  68. package/ledger-surface.mjs +1 -1
  69. package/ledger-tools.mjs +6 -6
  70. package/ledger-variant-notice.mjs +1 -1
  71. package/ledger-write-ops.mjs +373 -0
  72. package/ledger.mjs +2 -2
  73. package/manifest-discovery.mjs +2 -2
  74. package/manifest-schema.mjs +4 -0
  75. package/memory/memory-tool.mjs +5 -2
  76. package/memory/schema.mjs +5 -26
  77. package/package.json +1 -1
  78. package/path-resolve.mjs +217 -0
  79. package/permission.mjs +4 -0
  80. package/prompt-overlays.mjs +5 -0
  81. package/prompts/advisor-design.md +9 -6
  82. package/prompts/advisor-round1.md +4 -3
  83. package/prompts/advisor-round2.md +6 -5
  84. package/prompts/advisor-round3.md +6 -5
  85. package/prompts/common.md +23 -10
  86. package/prompts/consult-base.md +2 -1
  87. package/prompts/discipline-engineering.md +29 -48
  88. package/prompts/discipline-normal.md +10 -5
  89. package/prompts/persona-eng-coder.md +8 -0
  90. package/prompts/persona-eng-designer.md +22 -12
  91. package/prompts/persona-engineering.md +81 -68
  92. package/prompts/persona-plan.md +2 -1
  93. package/prompts/persona-tester.md +55 -0
  94. package/provider/core.mjs +2 -2
  95. package/provider/google.mjs +15 -5
  96. package/provider/rate.mjs +11 -3
  97. package/provider/sse.mjs +15 -4
  98. package/session-identity.mjs +21 -0
  99. package/session-lifecycle.mjs +9 -4
  100. package/session-slots.mjs +9 -14
  101. package/session.mjs +5 -0
  102. package/skip-dirs.mjs +48 -0
  103. package/team-ledger.mjs +4 -4
  104. package/text-budget.mjs +4 -0
  105. package/token-window.mjs +90 -14
  106. package/tool-docs/advisor.md +2 -2
  107. package/tool-docs/apply_patch.md +2 -0
  108. package/tool-docs/bash.md +4 -0
  109. package/tool-docs/batch.md +85 -1
  110. package/tool-docs/browser.md +3 -1
  111. package/tool-docs/code_search.md +3 -1
  112. package/tool-docs/consult_start.md +6 -2
  113. package/tool-docs/consult_stop.md +3 -1
  114. package/tool-docs/delete.md +3 -0
  115. package/tool-docs/doc_check.md +43 -8
  116. package/tool-docs/doc_search.md +5 -1
  117. package/tool-docs/edit.md +22 -1
  118. package/tool-docs/execute.md +7 -1
  119. package/tool-docs/fetch.md +3 -1
  120. package/tool-docs/file_ops.md +3 -1
  121. package/tool-docs/git.md +9 -1
  122. package/tool-docs/goal.md +2 -0
  123. package/tool-docs/grep.md +1 -1
  124. package/tool-docs/hashline_edit.md +2 -0
  125. package/tool-docs/insert_after.md +3 -1
  126. package/tool-docs/ledger.md +5 -5
  127. package/tool-docs/lint.md +1 -0
  128. package/tool-docs/manifest-read.md +2 -1
  129. package/tool-docs/manifest.md +9 -2
  130. package/tool-docs/memory.md +11 -1
  131. package/tool-docs/notify_parent.md +1 -1
  132. package/tool-docs/plan.md +2 -0
  133. package/tool-docs/question.md +1 -1
  134. package/tool-docs/read_history.md +15 -3
  135. package/tool-docs/read_image.md +3 -1
  136. package/tool-docs/recent_changes.md +3 -1
  137. package/tool-docs/repo_outline.md +3 -1
  138. package/tool-docs/settings.md +10 -2
  139. package/tool-docs/skill.md +2 -0
  140. package/tool-docs/ssh.md +30 -17
  141. package/tool-docs/subagent.md +50 -13
  142. package/tool-docs/task.md +2 -0
  143. package/tool-docs/team_ledger.md +1 -1
  144. package/tool-docs/timer.md +7 -0
  145. package/tool-docs/verify.md +13 -1
  146. package/tool-docs/websearch.md +3 -1
  147. package/tool-docs/write.md +2 -0
  148. package/tools/bash.mjs +12 -2
  149. package/tools/edit-batch.mjs +40 -33
  150. package/tools/edit-diff.mjs +69 -1
  151. package/tools/execute.mjs +10 -4
  152. package/tools/file.mjs +17 -27
  153. package/tools/git.mjs +27 -1
  154. package/tools/ops.mjs +2 -1
  155. package/tools/patch.mjs +12 -3
  156. package/tools/process-doc-mark.mjs +139 -0
  157. package/tools/search.mjs +15 -6
  158. package/tools/shared.mjs +24 -1
  159. package/tools/ssh.mjs +48 -7
  160. package/tools/tree.mjs +4 -1
  161. package/tools/write-path.mjs +64 -3
  162. package/traces/trace-store.mjs +4 -3
  163. package/agent/light-round.mjs +0 -63
  164. package/agent/unbooked-pen.mjs +0 -62
@@ -1,9 +1,9 @@
1
1
  /**
2
- * agent/write-gate.mjs — M4 写权门禁(评审目标解析 + D5 冻结窗口判据组装)。
2
+ * agent/write-gate.mjs — M4 写权门禁(评审目标解析 + D5 冻结窗口判据组装 + 写锁)。
3
3
  *
4
4
  * v2 唯一增量(模块设计 §2.1):评审对象 / 被审文件路径来源改读 manifest `docRoot`
5
5
  * (去硬编码 `docs/`),token 门(`anyLiveDesignSlot`)与冻结窗口谓词本体继承 v1 零改
6
- * (KD-M4-3)——本档承载三个单点:
6
+ * (KD-M4-3)——本档承载四个单点:
7
7
  *
8
8
  * 1. `resolveReviewTargetPaths(agent)` —— 评审目标解析单点:读 manifest `docRoot`
9
9
  * 产出评审对象 / 被审文件的绝对路径集合(目录级;子键值形态 = 串 | 多根数组)。
@@ -25,17 +25,36 @@
25
25
  * `AGENT-LOOP-SUBAGENT.md` §6.29.1):判据与拒绝文案同址(文案字面只出本档)。
26
26
  * 2026-09-29 收窄(台账 #545):自身批次伴随件(非 `.md` · 同目录 · 词干族 = 三合取)放行——
27
27
  * 批次档本体 ∕ 他批伴随件 ∕ 异目录伴随件照拒;两端取用 = 核 `agent/dispatch.mjs` Phase 1(两端同径)。
28
+ * 4. `ledgerBindWriteConflict(agent, depth, tool, toolName, args)` —— 写锁(第三道门 · 设计档 §2.3):
29
+ * 判据 = 「**本笔报的** `#N` 是目标所属项目台账里一条 `status = 在途` 的行」(笔-账绑定——用户
30
+ * 2026-11-03 02:02 裁「甲 · 逐笔带号」)。判据函数 `ledgerRowAt` 住本档模块内(不导出)。
31
+ * 2026-11-03 不分面批:文档面 / 项目状态档 / 产品面 / 产品测面读同一条判据;面在判据里退场。
32
+ * 2026-11-03 笔-账绑定批:入口集扩到十二把(写盘七件 + `bash` ∥ `execute` + `git` + `manifest` ∥ `memory`),
33
+ * `git` 只对写型 action 要号(枚举可判,不猜命令字面),拒文成为唯一教学面(`ledger` 不进 `required`)。
34
+ * 2026-11-03 项目声明面批:**判据点从会话锚换成调用方声明的项目**(决策 40 ∥ 41)——定位参数优先,
35
+ * `project` 补位;会话锚永不充当判据点(`no project on this call` ∥ `declared project is not a project`)。
36
+ * 2026-11-04 ssh 传输面批:入口集扩到十三把(+ `ssh`——只有 `get` 往本地落盘,按 action 判要号:
37
+ * `connect` / `send` / `read` / `list` 不写项目内档,不得被本门问到)。
28
38
  *
29
39
  * 继承零改面(本档不碰):token 门资格判据(anyLiveDesignSlot / validateDesignToken)、
30
40
  * 代码路径判定(conventions.mjs 单一权威分类——KD-M4-1 保留为代码路径判定用)。
31
41
  */
32
- import { basename, dirname, resolve } from "node:path"
42
+ import { existsSync } from "node:fs"
43
+ import { basename, dirname, join, resolve } from "node:path"
33
44
  // advisor-settle 无上游依赖本簇(dispatch → write-gate → advisor-settle 单向)。normAbs
34
45
  // 权威本体 = review-facts.mjs(第 33 批迁入),advisor-settle re-export;本档再 re-export
35
46
  // (指针链非副本),M6 经本档一行 import 取齐。
36
47
  import { inflightDesignReviewConflict, normAbs } from "../agent-tools/advisor-settle.mjs"
37
48
  // 批次档路径单源(叶档——`BATCH-RECORD.md` §4.15):批次档写门基底集取 `batchDocBases`。
38
49
  import { batchDocBases } from "../agent-tools/batch-paths.mjs"
50
+ // 项目根推导 + manifest 读面(写锁判据 · 2026-11-03 笔-账绑定批):判据按**调用方声明的项目**问该根台账(§2.3)。
51
+ // `MANIFEST_REL` = 「自身即带 manifest」那一支的判据(决策 41 的 (a) 支)。
52
+ import { MANIFEST_REL, readManifest, resolveProjectRoot } from "../manifest.mjs"
53
+ // 动作级只读谓词(`git` 写型 ∥ 只读的判法单源 = 既有 `readonlyActionOf`,决策 37——不新写一套分类)。
54
+ // 钩子优先 ∥ 核名面谓词回落:`git` 的名单落 `tools/git.mjs` 的 `isReadonlyAction`(单源,端覆写同笔删)。
55
+ import { readonlyActionOf } from "./dispatch-gates.mjs"
56
+ // 工具自报触达面单源谓词(写锁的路径集由判据函数自己算——§2.3;与门禁 / 变更记账同一枚谓词)。
57
+ import { toolTouchPaths } from "./helpers.mjs"
39
58
  export { normAbs }
40
59
  // 2026-10-04(工具路径基面根治批 · #921 · KD-2 同名再出口):REVIEW_ROOT_KEYS +
41
60
  // resolveReviewRootsFor 判定本体迁 `review-facts.mjs`(评审事实面——advisor-async 既有
@@ -56,7 +75,7 @@ import { resolveReviewRootsFor } from "../agent-tools/review-facts.mjs"
56
75
  * @returns {string[]} 评审目标目录的绝对路径(去重)
57
76
  */
58
77
  export function resolveReviewTargetPaths(agent) {
59
- return resolveReviewRootsFor(agent?.cwd ?? process.cwd())
78
+ return resolveReviewRootsFor(agent?.cwd)
60
79
  }
61
80
 
62
81
  /**
@@ -151,7 +170,7 @@ export function batchRecordWriteConflict(agent, depth, absPaths) {
151
170
  if (typeof bound !== "string" || bound.length === 0) return null
152
171
  const wanted = (absPaths ?? []).filter((p) => typeof p === "string" && p.length > 0)
153
172
  if (wanted.length === 0) return null
154
- const cwd = typeof agent?.cwd === "string" && agent.cwd.length > 0 ? agent.cwd : process.cwd()
173
+ const cwd = agent.cwd
155
174
  const bases = memoizedBatchBases(agent, cwd)
156
175
  const boundAbs = normAbs(bound, cwd)
157
176
  const boundKey = gateKey(boundAbs)
@@ -170,3 +189,333 @@ export function batchRecordWriteConflict(agent, depth, absPaths) {
170
189
  return null
171
190
  }
172
191
 
192
+ // ─── 写锁(第三道门 · 2026-11-03 笔-账绑定批 · 设计档 §2.3)───────────────────────────
193
+
194
+ /** 写门入口集(设计档 §2.3):判据 = 「凡能把字节落到项目内的工具」。十三把 = 写盘七件 +
195
+ * 三把命令工具 + 两把结构化 action 工具(`manifest` ∥ `memory` 按各自的枚举判——决策 39)+ `ssh`
196
+ * (只有 `get` 落盘,按 action 判)。
197
+ * `settings`(写 `~/.thincoder/config.json`)∥ `browser`(自有 profile 与截图目录)不写项目内档 ⇒ 不进。
198
+ * `ledger` ∥ `batch` 也不进——挂行与开档不经过写面,「没号就挂不了账」的锁死因此不存在。 */
199
+ const LEDGER_ENTRY_TOOLS = new Set([
200
+ "write", "edit", "insert_after", "apply_patch", "delete", "hashline_edit", "file_ops",
201
+ "bash", "execute", "git", "manifest", "memory", "ssh",
202
+ ])
203
+ /** 结构化 action 工具的写型枚举(按值判,不解析内容——决策 37 ∥ 39 同法)。 */
204
+ const MANIFEST_WRITE_ACTIONS = new Set(["init", "write"])
205
+ /** `memory` 里**能落 project 层档**的写型 action:`put` 按 `layer`、`delete` 按 `layer` ∥ id 前缀。
206
+ * `clear` 只对 personal 开放(该 action 自己拒 shared 层)∥ `maintain` 无 `layer` 参数(库级维护——MEMORY.md §6.6.1)
207
+ * ⇒ 两者都落不到 `<项目>/.thincoder/memory/**`,故不进枚举(枚举只收真能落项目内档的值——空的覆盖项比没有更坏)。 */
208
+ const MEMORY_WRITE_ACTIONS = new Set(["put", "delete"])
209
+ /** `memory` 的落盘目标按层分:只有 `project` 层写 `<项目>/.thincoder/memory/**`(`personal` ∥ `team` 落用户数据目录)。 */
210
+ const MEMORY_PROJECT_LAYER = "project"
211
+ /** 单条删除的层由 id 前缀定(`layer` 可省——`thincoder-core/memory/memory-tool.mjs` 的 `execDeleteSingle`
212
+ * 逐字「layer is OPTIONAL … else the id prefix routes the delete」)⇒ 前缀与 `layer` 同判:
213
+ * 只看 `layer` 会漏掉「不带 layer、按 `project:` 前缀删 project 层档」这条写项目内字节的路。 */
214
+ const MEMORY_PROJECT_UID_PREFIX = "project:"
215
+ /** 在途行的状态词(台账六态之一):只有它携带写权。 */
216
+ const IN_FLIGHT_STATUS = "在途"
217
+
218
+ /** 拒文(逐字 = 设计档 §2.3——机检可断言):一条基串 + 按情形选的 `<case>` + 用法句 + 本项目在途清单 + 挂号两步。
219
+ * `ledger` ∥ `project` 都不进 `required`(决策 35 ∥ 42)⇒ 模型不知道有这两个参数,第一次撞的就是这条拒文,
220
+ * 它是这条设计唯一的教学面(决策 38):缺什么 / 要带什么(**两个输入一次点出**)/ 当下有哪些号可带,缺一不可。 */
221
+ const LEDGER_PARAM_SENTENCE = 'Pass project:<the project root that carries PROJECT-MANIFEST.json> and ledger:"#N" on this call — engineering mode binds every write to a project and to the in-flight ledger row it serves; a call that names neither is refused.'
222
+ const LEDGER_HANG_STEPS = 'If none is in flight, hang one first — neither step is a write: ① ledger({action:"add", title, kind, cwd:<project root>}) hangs the row; ② batch({action:"create", ledger:"#N", topic, source}) opens the record over it (row → 在途).'
223
+ /** 清单上限(设计档 §2.3):在途行多时逐条列全会把教学面变成台账导出。 */
224
+ const LEDGER_LIST_LIMIT = 10
225
+
226
+ const CASE_NO_NUMBER = "no ledger number on this call"
227
+ const CASE_NO_PROJECT = "no project on this call"
228
+ /** 无项目时清单位的三种取值(见 `ledgerNoProjectRefusal`):缺声明 ∥ 声明无效 ∥ 写盘目标不在任何项目里。
229
+ * 三种各用自己的一句:一句里两个结论会让读拒文的人分不清该改哪一边。 */
230
+ const LIST_UNKNOWN_NO_PROJECT = "unknown — no project on this call"
231
+ const LIST_UNKNOWN_NOT_A_PROJECT = "unknown — the declared project is not a project"
232
+ const LIST_UNKNOWN_TARGET_NOT_A_PROJECT = "unknown — no project owns this write's target"
233
+ const CASE_UNREADABLE = "no ledger can be read for this project"
234
+ const caseNotRowNumber = (raw) => `ledger number ${JSON.stringify(String(raw))} is not a row number — pass "#N" or "N"`
235
+ const caseNoSuchRow = (no) => `row #${no} does not exist in this project's ledger`
236
+ const caseNotInFlight = (no, status) => `row #${no} is ${JSON.stringify(String(status))} — only a ${IN_FLIGHT_STATUS} row carries writes`
237
+ /** 声明无效(设计档 §2.3 拒文表逐字:`declared project is not a project (no PROJECT-MANIFEST.json at this path or
238
+ * within the session anchor): "<path>"`)—— 声明与写盘目标两种申报形共用这一句:两者都没在每个被搜过的位置
239
+ * 找到 manifest(写盘目标沿祖先链搜得更远,那句括注仍然为真)。 */
240
+ const caseNotAProject = (p) => `declared project is not a project (no PROJECT-MANIFEST.json at this path or within the session anchor): ${JSON.stringify(String(p))}`
241
+
242
+ /** 当下在途行清单(决策 38 的第三件):`#N, #M …`;无 ⇒ `none`;超上限折 `… +K more`。 */
243
+ function ledgerInFlightList(rows) {
244
+ const nums = (Array.isArray(rows) ? rows : [])
245
+ .filter((r) => r?.status === IN_FLIGHT_STATUS)
246
+ .map((r) => `#${r.id}`)
247
+ if (nums.length === 0) return "none"
248
+ if (nums.length <= LEDGER_LIST_LIMIT) return nums.join(", ")
249
+ return `${nums.slice(0, LEDGER_LIST_LIMIT).join(", ")}, … +${nums.length - LEDGER_LIST_LIMIT} more`
250
+ }
251
+
252
+ /** 拒文组装(可读台账形):基串 + `<case>` + 用法句 + 在途清单 + 挂号两步。
253
+ * 清单按「**本项目的**账」说(判据点换成声明后,号只在那一个项目的账上成立)。 */
254
+ function ledgerRefusal(caseText, rows) {
255
+ return `write refused — ledger row not bound: ${caseText}. ${LEDGER_PARAM_SENTENCE} In-flight rows in this project's ledger right now: ${ledgerInFlightList(rows)}. ${LEDGER_HANG_STEPS}`
256
+ }
257
+
258
+ /** 拒文组装(无项目形):没有可判的项目就没有「本项目的账」可问——清单位写 `unknown` 并点名哪一种无效,
259
+ * **不写 `none`**(那会把「不知道判哪个项目」说成「本项目一条在途行都没有」,模型会照它去挂号而忘了声明项目)。
260
+ * 两种无效的项目各用自己的清单句:声明缺失 ≠ 声明了个不是项目的东西,一句里两个结论会让读拒文的人分不清。
261
+ * 用法句仍把两个输入一起点出(R33:三件在每一支都在位)。 */
262
+ function ledgerNoProjectRefusal(caseText, listText) {
263
+ return `write refused — ledger row not bound: ${caseText}. ${LEDGER_PARAM_SENTENCE} In-flight rows in this project's ledger right now: ${listText}. ${LEDGER_HANG_STEPS}`
264
+ }
265
+
266
+ /** 拒文组装(台账读不到形):基串 + `<case>` + 用法句 + 挂号两步,另起一行带原始错误;
267
+ * **不列在途清单**——读不到就没有清单,写个 `none` 是把「读不到」说成「一条都没有」。 */
268
+ function ledgerUnreadableRefusal(caseText, error) {
269
+ return `write refused — ledger row not bound: ${caseText}. ${LEDGER_PARAM_SENTENCE} ${LEDGER_HANG_STEPS}\n(ledger unreadable — ${error})`
270
+ }
271
+
272
+ /** 台账号解析(`#` 可省——`"#2016"` 与 `"2016"` 同判,设计档 §2.3):三种结果
273
+ * `{ case: "missing" }`(未传 / 空串 / 全空白)· `{ case: "form", raw }`(剥前导 `#` 后非纯数字)· `{ no }`。 */
274
+ function parseLedgerNo(raw) {
275
+ if (raw === undefined || raw === null) return { case: "missing" }
276
+ const text = String(raw).trim()
277
+ if (text === "") return { case: "missing" }
278
+ const m = /^#?(\d+)$/.exec(text)
279
+ if (!m) return { case: "form", raw: text }
280
+ return { no: Number(m[1]) }
281
+ }
282
+
283
+ /** 该根台账的行集(判据的第二个输入)。两种「根」不是一回事(§2.3 的钉子):根**不在**(声明无效)
284
+ * 由调用方在取根那一步拒(`declared project is not a project`);manifest **在而不可用**(读不动 / 坏 JSON)
285
+ * 与库开不了 ⇒ 返回 `error`,一律落拒——判的是「是不是项目」,不是「项目好不好用」。 */
286
+ async function ledgerRowsOf(root) {
287
+ try {
288
+ const man = readManifest(root)
289
+ if (!man?.ok) {
290
+ const detail = (man?.errors ?? []).filter((s) => typeof s === "string" && s).join(";")
291
+ return { rows: [], error: detail || String(man?.reason ?? "manifest unusable") }
292
+ }
293
+ } catch (e) {
294
+ return { rows: [], error: String(e?.message ?? e) }
295
+ }
296
+ try {
297
+ // W8 契约②:node:sqlite 链只经动态 import 进入(勿升静态)。
298
+ // `boundary` 必传、传的就是刚算出的那个根:`openLedger` 首行即 `resolve(boundary)`,
299
+ // 缺省直接抛 ⇒ 每一笔写都被判成「台账读不到」(§2.3 读法)。
300
+ const { ledgerQuery } = await import("../ledger.mjs")
301
+ const rows = ledgerQuery({ cwd: root, boundary: root })
302
+ return { rows: Array.isArray(rows) ? rows : [], error: null }
303
+ } catch (e) {
304
+ return { rows: [], error: String(e?.message ?? e) } // fail-closed:读不到不等于放行
305
+ }
306
+ }
307
+
308
+ /**
309
+ * 判据函数(**模块内,不导出**——唯一消费方是本门的判据本体;导出去就是下一个零消费符号):
310
+ * 已算出的项目根 → 该根台账的行集 → 按 `id` 找本笔报的那一行(设计档 §2.3「读法」)。
311
+ *
312
+ * **判据点不再是「目标自身推根」**(2026-11-03 项目声明面批 · 决策 40):项目由调用方声明,本函数只负责
313
+ * 「拿一个已成立的项目根 + 一个号」这一半;「根从哪来」由 `ledgerGateTargets` + `resolveDeclaredRoot` 回答。
314
+ * 判据点不存在(不在任何项目里)现在由调用方落 `declared project is not a project`,不再是「门不管」。
315
+ *
316
+ * **不为门新增按 id 的列过滤**(§2.3):`ledgerQuery` 现有过滤键只有 `status` / `kind` / `board` /
317
+ * `trigger` / `task_book`,行集取全量后按 `id` 找——同一笔写的多根按 `root` 缓存,一次写只读一遍账。
318
+ *
319
+ * @param {string} root — 已成立的判据点(项目根绝对路径)
320
+ * @param {number|null} ledgerNo — 本笔报的行号(缺号 / 非行号形时传 null)
321
+ * @param {Map<string, object>|null} [cache] — 同一笔写的根 → 行集(多目标同项目时只问一次台账)
322
+ * @returns {Promise<{row: object|null, rows: object[], error: string|null}>}
323
+ * `row` = 找到的那一行(查不到 ⇒ null);`error` = 读不到时的原始错误(fail-closed 拒文点名它)
324
+ */
325
+ async function ledgerRowAt(root, ledgerNo, cache = null) {
326
+ let base = cache instanceof Map ? cache.get(root) : null
327
+ if (!base) {
328
+ base = await ledgerRowsOf(root)
329
+ if (cache instanceof Map) cache.set(root, base)
330
+ }
331
+ if (base.error) return { row: null, rows: [], error: base.error }
332
+ const row = ledgerNo === null ? null : base.rows.find((r) => Number(r?.id) === ledgerNo) ?? null
333
+ return { row, rows: base.rows, error: null }
334
+ }
335
+
336
+ /** 声明项目的取根(设计档 §2.3「两个分支的落法」/ §5 R31)——**界 = 相关性判据,不是单向的「不许越界」**:
337
+ * (b) 支 = 既有单源 `resolveLedgerRoot(声明, agent.cwd)`:起点在会话锚之内 ⇒ 沿祖先链上溯到最近带 manifest 的目录;
338
+ * (b) 取不到再走 (a) 支 = 声明路径**自身即带 manifest**且会话锚在它之下 ⇒ 根 = 声明路径(`basis = self`,不上溯)。
339
+ * **为什么 (a) 支要门自己判一次**:`resolveLedgerRoot` 的上溯界就是会话锚,起点落在界之上时它取不到根;
340
+ * 而「会话 cwd 在项目子目录」正是项目指南记录的启动形(`cd thincoder/thincoder-cli && node bin/thincoder.cjs`)——
341
+ * 不认这一支,那种会话里 `bash` ∥ `memory` 声明根也被拒、声明子目录也被拒 = 无解。
342
+ * 两支都取不到(不存在 ∥ 平级目录 ∥ 不含锚的更远祖先 ∥ 界内上溯无 manifest)⇒ null(拒,点名该路径)。
343
+ * **会话锚在这两支里都不是「判哪个项目」的答案**(决策 41):它只用来定界与判相关性。
344
+ * @param {string} declared — 调用方给的项目声明(绝对 ∥ 相对;相对按会话 cwd 解析)
345
+ * @param {string} cwd — 会话锚
346
+ * @returns {Promise<{root: string, basis: "self"|"ancestor"}|null>} */
347
+ async function resolveDeclaredRoot(declared, cwd) {
348
+ const start = resolve(cwd, String(declared))
349
+ // 声明路径不存在 ⇒ 拒(§5 R31 四态第一态):不加这一行,`owningProject` 会从一条不存在的路径上溯,
350
+ // 把拼错的声明归到会话自己那个项目上——那正是本批要关掉的「会话锚当判据点」(决策 41)。
351
+ if (!existsSync(start)) return null
352
+ // W8 契约②:node:sqlite 链只经动态 import 进入(`resolveLedgerRoot` 与 `ledgerQuery` 同住该链;模块已缓存)。
353
+ const { resolveLedgerRoot } = await import("../ledger.mjs")
354
+ const owned = resolveLedgerRoot(start, cwd)
355
+ if (owned) return { root: owned.root, basis: owned.basis }
356
+ if (existsSync(join(start, MANIFEST_REL)) && underKey(start, cwd)) return { root: start, basis: "self" }
357
+ return null
358
+ }
359
+
360
+ /** `child` 是否等于 `root` 或在其子树内(比较键 = `gateKey`:绝对化 + `/` 归一 + win32 大小写归一)。 */
361
+ function underKey(root, child) {
362
+ const r = gateKey(root)
363
+ const c = gateKey(child)
364
+ return c === r || c.startsWith(r.endsWith("/") ? r : r + "/")
365
+ }
366
+
367
+ /** 本笔的判据点(设计档 §2.3「判据点怎么来」· 决策 40):**只有两个来源,都来自调用方**——
368
+ * ① 该工具自带的定位参数:写盘七件的目标路径(`toolTouchPaths`)· `execute.workdir` · `git.path` ∥ `workdir`
369
+ * (`path` 相对 `workdir`——`thincoder-core/tools/git.mjs` 的 `resolveBaseDir` 同源;设计档要求「`toolTouchPaths`
370
+ * 再补 `path` ∥ `workdir`」,而 `git` 无 `touchedPaths` 钩子 ⇒ `toolTouchPaths` 的回落形正是 `[args.path]`,
371
+ * 不用它是因为它按会话 cwd 归一,而 `git` 的 `path` 相对 `workdir`)· `manifest.target`;
372
+ * ② 新参数 `project`(项目根绝对路径,纯声明)。**定位参数优先**,`project` 只在定位参数缺位时被读。
373
+ * **会话锚永不充当判据点**(决策 41):`bash` ∥ `memory` 没有定位参数,缺 `project` 即拒;
374
+ * `execute` ∥ `git` ∥ `manifest` 缺定位参数时同样不回落到 cwd。
375
+ * 三种结果:`{ paths }`(按路径所属项目判)· `{ declared }`(按声明取根)· `{ none: true }`(无判据点 ⇒ 拒);
376
+ * 写盘工具连路径都没有 ⇒ null(工具自己拒,本门不问)。 */
377
+ function ledgerGateTargets(agent, toolName, tool, args) {
378
+ const cwd = resolve(agent?.cwd ?? ".")
379
+ const asAbs = (p) => (typeof p === "string" && p.trim() ? resolve(cwd, p) : null)
380
+ const declared = typeof args?.project === "string" && args.project.trim() ? args.project.trim() : null
381
+ const onlyDeclared = () => (declared ? { declared } : { none: true })
382
+ if (toolName === "bash" || toolName === "memory") return onlyDeclared()
383
+ if (toolName === "execute") {
384
+ const wd = asAbs(args?.workdir)
385
+ return wd ? { paths: [wd] } : onlyDeclared()
386
+ }
387
+ if (toolName === "git") {
388
+ const wd = asAbs(args?.workdir)
389
+ const p = typeof args?.path === "string" && args.path.trim() ? resolve(wd ?? cwd, args.path) : null
390
+ if (p) return { paths: wd ? [p, wd] : [p] } // 两个都在 ⇒ 两个项目都要带号(§2.3「一笔写跨两个项目」)
391
+ return wd ? { paths: [wd] } : onlyDeclared()
392
+ }
393
+ if (toolName === "manifest") {
394
+ const t = asAbs(args?.target)
395
+ return t ? { paths: [t] } : onlyDeclared()
396
+ }
397
+ const paths = toolTouchPaths(tool, args)
398
+ .filter((p) => typeof p === "string" && p.trim())
399
+ .map((p) => resolve(cwd, p))
400
+ return paths.length === 0 ? null : { paths }
401
+ }
402
+
403
+ /** 本笔是否需要号(工具自己的枚举判写型——不解析命令字面,决策 36 的禁令不碰):
404
+ * `git` 只对写型 action 要号(只读名单 = 核钩子,决策 37);`manifest` 的 `init` ∥ `write` 都要号——
405
+ * `init` 的豁免是条件形(决策 43:目标沿祖先链取不到项目根时在取根步跳过本门),不是摘出要号集;
406
+ * `memory` 只对落 project 层档的写型 action 要号(`layer:"project"` ∥ 单条删除的 `project:` 前缀——
407
+ * 后者不带 `layer` 也能删 project 层档,只看 `layer` 就漏了那条路);`ssh` 只对 `get` 要号(`put` 写的是
408
+ * 远端主机,其余动作不写项目内档——缺此分支则整个会话面都要号,把会话面整条打死)。 */
409
+ function ledgerGateApplies(toolName, tool, args) {
410
+ if (toolName === "git") return !readonlyActionOf(tool, "git", args)
411
+ if (toolName === "manifest") return MANIFEST_WRITE_ACTIONS.has(String(args?.action ?? ""))
412
+ if (toolName === "memory") return memoryTargetsProjectLayer(args)
413
+ if (toolName === "ssh") return String(args?.action ?? "") === "get"
414
+ return true
415
+ }
416
+
417
+ /** `memory` 这一笔是否落 project 层:`layer:"project"`,或单条删除的 id 前缀 = `project:`(`layer` 省略时
418
+ * 工具按前缀路由到 project 层)。两个输入都是工具自己的枚举 ∥ 约定,不解析笔记内容(决策 36 的禁令不碰)。
419
+ * 显式给了别的层 ⇒ 不过本门(工具自己按该层处置——前缀不符它会拒)。 */
420
+ function memoryTargetsProjectLayer(args) {
421
+ const action = String(args?.action ?? "")
422
+ if (!MEMORY_WRITE_ACTIONS.has(action)) return false
423
+ const layer = String(args?.layer ?? "").trim()
424
+ if (layer === MEMORY_PROJECT_LAYER) return true
425
+ if (layer !== "") return false
426
+ const id = args?.id
427
+ return typeof id === "string" && id.startsWith(MEMORY_PROJECT_UID_PREFIX)
428
+ }
429
+
430
+ /** 判序第三步:按号找行、判 `status`(号四情形——缺 ∥ 非行号形 ∥ 不在盘 ∥ 非在途)。
431
+ * `at.error` 非空 = 该根台账读不到 ⇒ 拒(真业务门:读不到不等于放行)。 */
432
+ function ledgerRowRefusal(parsed, at) {
433
+ if (at.error) return ledgerUnreadableRefusal(CASE_UNREADABLE, at.error)
434
+ if (parsed.case === "missing") return ledgerRefusal(CASE_NO_NUMBER, at.rows)
435
+ if (parsed.case === "form") return ledgerRefusal(caseNotRowNumber(parsed.raw), at.rows)
436
+ if (!at.row) return ledgerRefusal(caseNoSuchRow(parsed.no), at.rows)
437
+ if (at.row.status !== IN_FLIGHT_STATUS) return ledgerRefusal(caseNotInFlight(parsed.no, at.row.status), at.rows)
438
+ return null
439
+ }
440
+
441
+ /**
442
+ * 写锁判据组装(**第三道门**——与跨批档写 / D5 冻结窗并列挂 `dispatch.mjs` Phase 1):
443
+ * 辖内任何写盘 ∥ 跑命令 ∧ 本笔**同时给出项目与号**而号不是该项目台账里的 `在途` 行 ⇒ 冲突;否则 null。
444
+ *
445
+ * **判据只有一条,零上位条件**(§2.3):不判批档状态、不判指针互指。多一个合取项,就会在某个真实场景
446
+ * 变成「机制走不通」的证据,接着被用来把整套机制废掉。
447
+ *
448
+ * **为什么是笔-账绑定**:写盘工具的参数原本只有路径与内容,门只能问「这个仓有没有在途行」——一条在途行
449
+ * 于是放行该根下任何一笔写,它拦不住「有账但在做别的事」。判据换成「这笔写替哪条账服务」,门才第一次
450
+ * 看得见写者报的是哪条账(决策 34;形状 = 用户 2026-11-03 02:02 裁的「甲 · 逐笔带号」——轮内绑定会把
451
+ * 几件异步在做的事压成一条账)。
452
+ *
453
+ * **`bash` ∥ `execute` 与写盘工具同一判据**(决策 36):命令行内容无法可靠静态分类(`node -e` /
454
+ * `npm run <脚本>` / 参数拼接都能落盘),按字面分类本身就是第二个洞。代价 = 工程模式下父侧任何
455
+ * `bash` ∥ `execute` 都要有在途行(含 `npm test` / `git status`),正路 = 先挂行再干活。
456
+ *
457
+ * **面不参与放行**:文档面、项目状态档、产品代码面、产品测试面读同一条判据。
458
+ * 挂门范围(工程模式 ∧ `depth = 0`)说的是「谁被这条门问到」,不是判据的合取项。
459
+ *
460
+ * **判据点 = 调用方声明的项目**(2026-11-03 项目声明面批 · 决策 40):会话锚可以是容器根 / 工作区根,
461
+ * 拿它当判据点时那种形态下本门永不设防(实机:2026-11-03 02:53 重启后 `bash` 不带号照放行)。
462
+ * 现在只有两个来源、都来自调用方:① 工具自带的定位参数;② `project`(项目根绝对路径)。
463
+ * **会话锚永不充当判据点**(决策 41):声明缺位 ⇒ `no project on this call`;声明无关键或取不到根 ⇒
464
+ * `declared project is not a project`——没有「推不出 ⇒ 门不管」那条臂。
465
+ *
466
+ * 与既有「工程模式父侧门」(同 Phase 1 的设计门,非读取路径按面判拒)是**两件事**:前门问「设计做没做」,
467
+ * 本锁问「这笔写替哪条在途行服务」。所以并列、不替换。
468
+ *
469
+ * **业务门语义**:台账读不到(库不可开 / 根推得出而 manifest 不可用)⇒ **拒**,拒文另起
470
+ * `(ledger unreadable — <原错>)` 一行——读不到不等于放行。
471
+ * **只在工程模式生效**(开关 = `agent.config?.agent?.engineering`,与设计门同一枚);普通模式按
472
+ * `docs/core/requirements/NORMAL-MODE.md` 的「纪律自持」不动。
473
+ *
474
+ * @param {Object} agent — 会话 agent(读 `config.agent.engineering` / `cwd`)
475
+ * @param {number} depth — 执行深度(只在 depth 0 生效;子代理走既有 token 门,本门不核它)
476
+ * @param {Object} tool — 工具对象(`toolTouchPaths` 的钩子源 ∥ `readonlyActionOf` 的动作谓词钩子源)
477
+ * @param {string} toolName — 工具名(= `toolCall.name`;决定入口集与各工具的判法)
478
+ * @param {Object} args — 本次调用入参(判据点、`args.project` ∥ `args.ledger` 都由本函数自己取)
479
+ * @returns {Promise<{target: string, message: string}|null>} 命中 = 拒文 + 首个被拒目标;否则 null
480
+ */
481
+ export async function ledgerBindWriteConflict(agent, depth, tool, toolName, args) {
482
+ if (!agent?.config?.agent?.engineering) return null // 普通模式零改(纪律自持)
483
+ if (!(depth === 0)) return null // 子代理走既有 token 门
484
+ const name = String(toolName ?? tool?.name ?? "")
485
+ if (!LEDGER_ENTRY_TOOLS.has(name)) return null // 入口集之外 ⇒ 本门不问
486
+ if (!ledgerGateApplies(name, tool, args)) return null // 只读 action ∥ 不写项目内档的枚举值 ⇒ 不要号
487
+ const claim = ledgerGateTargets(agent, name, tool, args)
488
+ if (!claim) return null // 没有可判的目标(写盘工具缺 path 由工具自己拒)
489
+ const parsed = parseLedgerNo(args?.ledger)
490
+ const cwd = resolve(agent?.cwd ?? ".")
491
+
492
+ // ① 判序第一步:解析项目声明(缺 ⇒ `no project on this call`)——判据点不存在时,号对不对无从谈起。
493
+ if (claim.none) return { target: cwd, message: ledgerNoProjectRefusal(CASE_NO_PROJECT, LIST_UNKNOWN_NO_PROJECT) }
494
+
495
+ const cache = new Map()
496
+ if (claim.declared !== undefined) {
497
+ // ② 声明形式:界 = 相关性判据((b) 支上溯 ∥ (a) 支自身带 manifest);两支都取不到 ⇒ 拒。
498
+ const resolved = await resolveDeclaredRoot(claim.declared, cwd)
499
+ if (!resolved) {
500
+ return { target: claim.declared, message: ledgerNoProjectRefusal(caseNotAProject(claim.declared), LIST_UNKNOWN_NOT_A_PROJECT) }
501
+ }
502
+ const at = await ledgerRowAt(resolved.root, parsed.no ?? null, cache)
503
+ const message = ledgerRowRefusal(parsed, at)
504
+ return message ? { target: resolved.root, message } : null
505
+ }
506
+
507
+ // ③ 定位参数形式:每个目标按自己的祖先链取根(一笔写跨两个项目 ⇒ 号要在每个项目的账上各查到)。
508
+ // `init` 的取根豁免(决策 43 · 条件形):目标沿祖先链取不到项目根 ⇒ 不进本门(无账可问);
509
+ // 取得到根(含既有项目内的子目录自举)⇒ 照判要号,目标自身已有 manifest 时门过后由工具自身拒。
510
+ for (const p of claim.paths) {
511
+ const root = resolveProjectRoot(p)
512
+ if (!root) {
513
+ if (String(args?.action ?? "") === "init") return null
514
+ return { target: p, message: ledgerNoProjectRefusal(caseNotAProject(p), LIST_UNKNOWN_TARGET_NOT_A_PROJECT) }
515
+ }
516
+ const at = await ledgerRowAt(root, parsed.no ?? null, cache)
517
+ const message = ledgerRowRefusal(parsed, at)
518
+ if (message) return { target: p, message }
519
+ }
520
+ return null
521
+ }
@@ -8,6 +8,8 @@
8
8
  * blocking review); depth>0 (eng-coder self-review) stays synchronous always.
9
9
  */
10
10
  import { runAdvisorReview, advisorIncompleteMarker, ADVISOR_LAUNCH_REFUSAL_PREFIX } from "../advisor/run.mjs"
11
+ import { readFileSync } from "node:fs"
12
+ import { countRoundBlocks } from "./batch-skeleton.mjs"
11
13
  import { resolveBatchDocPath } from "./batch.mjs"
12
14
  // 设计评审的文档受理面单源 = review-facts.mjs 的 resolveReviewDocPaths(判据正文 =
13
15
  // docs/core/design/ADVISOR-GUARDS.md §12:相对形按会话项目根 / 绝对形照用 / 受理 = 项目内现存 `.md`)。
@@ -22,7 +24,10 @@ import {
22
24
  } from "./advisor-async.mjs"
23
25
  // F30/F31(2026-09-18 顾问面治理批):类型门判定 / 拒发串 / 对象标识行——单源 `advisor/notice.mjs`
24
26
  // (文案逐字 = ADVISOR-GUARDS.md §2.4 / §2.5);零计数载体(计数护栏随撤 cap 整体退场)。
25
- import { typeGateCriterion, buildTypeGateRefusal, scopeSummary, withIdentityLine } from "../advisor/notice.mjs"
27
+ import { typeGateCriterion, buildTypeGateRefusal, identityLine, scopeSummary, withIdentityLine } from "../advisor/notice.mjs"
28
+ // 账链门判据(设计单源 = `docs/core/design/ADVISOR-GUARDS.md` §2 第 7 条):批档之外还须有未决台账行指着它。
29
+ // 判据档零静态仓内依赖(台账链在档内动态 import——W8 契约②),故本档可静态引用。
30
+ import { resolveBatchLedgerLink } from "./batch-ledger-link.mjs"
26
31
  // #15 描述外置:描述文本单点 = tool-docs/advisor.md(DESC 单解析面,缺档抛错语义不变)
27
32
  import { DESC } from "../tools/shared.mjs"
28
33
 
@@ -31,6 +36,74 @@ import { DESC } from "../tools/shared.mjs"
31
36
  // here for the tests' import surface (implementation re-exported).
32
37
  export { validateDesignToken } from "./advisor-async.mjs"
33
38
 
39
+ /**
40
+ * §3 点火基线(`docs/core/design/ADVISOR-GUARDS.md` §1.1 判据 1):绑了批档 ⇒ 记下点火时该档 §3
41
+ * 已有的轮次块数(块计数单源 = `countRoundBlocks`);未绑 / 读不到 ⇒ `null`——读不到不等于
42
+ * 「没有记录要验」,结算面的 §3 落地门据此判 `batch-unreadable`。每轮发起刷新:同一实例的修轮
43
+ * 是新的一轮,基线随之推进(否则上一轮留下的块会让本轮一字未写也过门)。
44
+ */
45
+ function section3BlocksBaseline(batchDocAbs) {
46
+ if (typeof batchDocAbs !== "string" || !batchDocAbs) return null
47
+ try {
48
+ return countRoundBlocks(readFileSync(batchDocAbs, "utf8"), 3)
49
+ } catch {
50
+ return null
51
+ }
52
+ }
53
+
54
+ /** `batchDoc` 缺失形态(缺失 ∥ `null` ∥ 空串 ∥ 纯空白——非字符串不在此判,留给 `resolveBatchDocPath` 的既有文案)。 */
55
+ function missingBatchDoc(raw) {
56
+ return raw === undefined || raw === null || (typeof raw === "string" && raw.trim() === "")
57
+ }
58
+
59
+ /**
60
+ * 设计评审准入门拒文(**逐字**——单源 `docs/core/design/ADVISOR-GUARDS.md` §2 第 6 条):首行 = 稳定前缀
61
+ * `Advisor: launch refused` + 标识行(与类型门同族形态);其后 Why + 两个期望形 + 可执行的下一步 + 收尾句。
62
+ * 与类型门的唯一形态偏差 = 多一句「没有批档 ⇒ 先建档」:本门判的是「参数在不在」,用户知道怎么传不等于
63
+ * 知道没档时怎么办。
64
+ */
65
+ function buildBatchDocRefusal(identity) {
66
+ return [
67
+ `Advisor: launch refused — a design review must carry \`batchDoc\`: the batch record this review belongs to. ${identity}`,
68
+ "Why: the token a design review issues is closed by its batch — closing that record consumes it. A review with no batch mints a token nothing can close: it rides the 7-day TTL and nothing else. Refusing the review at launch is how that token never exists.",
69
+ "Expected form 1 — project-root-relative: docs/batches/<batch>-<topic>.md (resolved against the session project root).",
70
+ "Expected form 2 — absolute: D:\\repo\\docs\\batches\\<batch>-<topic>.md (used as-is).",
71
+ 'No batch record in flight: create one first — batch({action:"create", …}) — one command; the record is the premise of the review, not paperwork.',
72
+ "Nothing was sent: no review instance, no round consumed, no design token minted, no LLM call.",
73
+ ].join("\n")
74
+ }
75
+
76
+ /**
77
+ * 账链门拒文(**逐字**——单源 `docs/core/design/ADVISOR-GUARDS.md` §2 第 7 条):首行 = 稳定前缀
78
+ * `Advisor: launch refused` + 标识行(与准入门同族形态);其后 Why + `Record:` 读数 + 三条修法 + 收尾句。
79
+ *
80
+ * 偏差(相对准入门)= 判据从「参数在不在」进到「账在不在」,因此多一行 `Record:` 读数值:让下一步可判——
81
+ * 是行丢了(`db: open` ∧ `unsettled: 0`),还是库根本不在盘(`db: not on disk`,Fix 1 无行可改)。
82
+ */
83
+ function buildBatchNoLedgerRefusal({ batchDocAbs, chain, scope }) {
84
+ const dbText = chain.db === "open"
85
+ ? `open at ${chain.dbFile}`
86
+ : chain.db === "not-on-disk"
87
+ ? `not on disk at ${chain.dbFile}`
88
+ : chain.db === "unreadable"
89
+ ? `unreadable at ${chain.dbFile}`
90
+ : "—" // 无项目根:没有库位可点名(`ledger base: —` 同因)
91
+ return [
92
+ `Advisor: launch refused — a design review needs its batch record to carry an unsettled ledger row. ${identityLine({
93
+ type: "design",
94
+ scope,
95
+ round: "—", // 判定先于实例解析——本次发起无轮次(§2 第 5 条字段行)
96
+ criterion: "batchdoc-no-ledger",
97
+ })}`,
98
+ `Why: the record says the work is in flight, the ledger says whose work it is — and no unsettled row points at this record (reason=${chain.reason}).`,
99
+ `Record: ${batchDocAbs} — ledger base: ${chain.root ?? "—"} · db: ${dbText} · rows whose task_book resolves here: ${chain.rows} (unsettled: ${chain.unsettled}).`,
100
+ 'Fix 1 (the row was archived, still recoverable): ledger({action:"update", id:<N>, status:"在途"}) — 待核销 → 在途 是六态唯一的回退边; then ledger({action:"update", id:<N>, task_book:"<仓根相对指针>"}). Row is 已核销 / 已废弃 ⇒ that edge is gone, use Fix 2.',
101
+ 'Fix 2 (this record is the right home, no row can come back): start a new record — batch({action:"create", …}) opens its ledger row in the same call — and launch the review under it.',
102
+ "Fix 3 (this record is finished): close it, and launch the review under the record that owns the work.",
103
+ "Nothing was sent: no review instance, no round consumed, no design token minted, no LLM call.",
104
+ ].join("\n")
105
+ }
106
+
34
107
  export const advisorTool = {
35
108
  name: "advisor",
36
109
  description: DESC("advisor"),
@@ -65,7 +138,7 @@ export const advisorTool = {
65
138
  },
66
139
  batchDoc: {
67
140
  type: "string",
68
- 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).",
141
+ description: "Design review only, REQUIRED: the batch record this review belongs to — the reviewer then also gets the `batch` write channel to record its findings + verdict. A design review without it is refused at launch (a token with no batch has no closer); a path that does not resolve to a readable file is refused. Relative paths resolve against the session cwd first, then candidate project roots (project-root-relative e.g. <repo>/docs/batches/…, or absolute). Code reviews do not pass it.",
69
142
  },
70
143
  },
71
144
  required: ["type"],
@@ -95,6 +168,43 @@ export const advisorTool = {
95
168
  })
96
169
  }
97
170
  const reviewType = args.type
171
+ // 准入门:`type="design"` 且 `batchDoc` 缺失 / 空 ⇒ 拒发(判据与拒文 = `docs/core/design/ADVISOR-GUARDS.md`
172
+ // §2 第 6 条);「携了须可读」那半幅随账链门(下段)一并判——判据与文案仍单源 `resolveBatchDocPath`。
173
+ // 绑定落 `resolved.run`(评审实例键)。
174
+ // 理由:设计评审签出的 token 由关档收口,而关档只认批归属——批外签发的授权没有第二个关闭者
175
+ // (spawn 只校验不消费、关档消费对无归属槽不适用,只剩 7 天 TTL)⇒ 堵法是让这种授权根本发不出去。
176
+ // 位置 = 类型门之后、文档范围解析之前:类型诊断与「参数在不在」都先于范围诊断(真因不被次要报错掩盖),
177
+ // 且二者都先于实例解析 / 签发 / LLM(零实例 / 零 token / 零槽 / 零 LLM)。
178
+ if (reviewType === "design" && missingBatchDoc(args.batchDoc)) {
179
+ if (ctx._toolCallId !== undefined) (agent._advisorRefusals ??= new Set()).add(ctx._toolCallId)
180
+ return buildBatchDocRefusal(identityLine({
181
+ type: reviewType,
182
+ scope: scopeSummary(args.documents?.length ? args.documents : args.paths),
183
+ round: "—", // 判定先于实例解析——本次发起无轮次(§2 第 5 条字段行)
184
+ criterion: "batchdoc-missing",
185
+ }))
186
+ }
187
+ // 账链门(`docs/core/design/ADVISOR-GUARDS.md` §2 第 7 条):`type="design"` ∧ `batchDoc` 已解析为绝对路径且
188
+ // 可读 ⇒ 点火拍查一次账——本档还有没有 ≥1 行未决台账行(`PENDING_STATUSES` 四态)的 `task_book` 指着它;
189
+ // 查不出「有」⇒ 拒发(`criterion=batchdoc-no-ledger`,reason 三值),登记 `_advisorRefusals`。
190
+ // 理由:第 6 条只要求「参数在 + 路径可读」,账侧从头到尾不问——行被撤回 ∥ 被结 ∥ 被改挂之后,
191
+ // 评审照发、token 照签,收口时重锚不到任何行。
192
+ // 批档绝对路径在本门就要用 ⇒ `resolveBatchDocPath` 从绑定行提前到此处调一次(绑定行复用同值,不二次解析);
193
+ // 携了不可读 ⇒ 走其既有 throw(顺序表第 3 行:可读是账链的前提,前提坏先报前提)。
194
+ // 代码评审不走本门(绑定行对 `type="code"` 仍走原解析——行为零变)。
195
+ let designBatchDocAbs = null
196
+ if (reviewType === "design") {
197
+ designBatchDocAbs = resolveBatchDocPath(agent.cwd, args.batchDoc)
198
+ const chain = await resolveBatchLedgerLink({ batchDocAbs: designBatchDocAbs, anchor: agent.cwd })
199
+ if (!chain.ok) {
200
+ if (ctx._toolCallId !== undefined) (agent._advisorRefusals ??= new Set()).add(ctx._toolCallId)
201
+ return buildBatchNoLedgerRefusal({
202
+ batchDocAbs: designBatchDocAbs,
203
+ chain,
204
+ scope: scopeSummary(args.documents?.length ? args.documents : args.paths),
205
+ })
206
+ }
207
+ }
98
208
  const documents = args.documents || null
99
209
  // Scope fallback: the runtime mutation record (zero git) covers guard-triggered
100
210
  // reviews where the model did not pass explicit paths.
@@ -115,12 +225,14 @@ export const advisorTool = {
115
225
  const { invalid } = resolveReviewDocPaths(documents, agent.cwd)
116
226
  if (invalid.length > 0) {
117
227
  if (ctx._toolCallId !== undefined) (agent._advisorRefusals ??= new Set()).add(ctx._toolCallId)
118
- // 拒发文案:既有稳定前缀逐字(行首)+ 逐文档「输入 → 解析后绝对路径 ∥ 拒因」+ 锚态(仅无项目根)+ 期望形两句。
119
- const lines = invalid.map((v) => `- ${v.doc} → ${v.resolved ?? "(no base)"} ∥ reason=${v.reason}`)
120
- if (invalid.some((v) => v.reason === "no-project-root")) {
228
+ // 拒发文案:既有稳定前缀逐字(行首)+ 逐文档「输入 → 解析后绝对路径 ∥ 拒因」+ 锚态(仅无唯一基根)+ 候选全列 + 期望形两句。
229
+ const lines = invalid.map((v) => `- ${v.doc} → ${v.resolved ?? "(no base)"} ∥ reason=${v.reason}${Array.isArray(v.sameName) && v.sameName.length > 0 ? `(盘上同名:${v.sameName.join(" ∥ ")})` : ""}`)
230
+ if (invalid.some((v) => v.reason === "ambiguous-path" || v.reason === "dangling-path")) {
121
231
  const view = projectRootView(agent.cwd)
122
232
  const where = view.state === "workspace" ? `workspace (${view.candidates.join(", ")})` : "none (no PROJECT-MANIFEST.json here or above)"
123
- lines.push(`Session anchor is ${where} — no project root, so a relative path has no base to resolve against.`)
233
+ lines.push(`Session anchor is ${where} — no single project root, so a relative path has no base to resolve against.`)
234
+ const cands = [...new Set(invalid.flatMap((v) => v.candidates ?? []))]
235
+ if (cands.length > 0) lines.push(`Candidates: ${cands.join(" ∥ ")} — pass one explicitly (an absolute path is the surest form).`)
124
236
  }
125
237
  lines.push("Expected form 1 — project-root-relative: docs/core/design/ADVISOR-GUARDS.md (resolved against the session project root).", "Expected form 2 — absolute: D:\\repo\\docs\\core\\design\\ADVISOR-GUARDS.md (used as-is).")
126
238
  return withIdentityLine(
@@ -154,13 +266,17 @@ export const advisorTool = {
154
266
  // scopes agent._advisorRound/_lastAdvisorOutput so the message builder and
155
267
  // the run.mjs cap read THIS instance's round/prior (multi-review isolation).
156
268
  const resolved = resolveAdvisorLaunch(agent, reviewType, { documents })
157
- // BATCH-RECORD.md §4.2 评审侧批次档门禁 + 实例键绑定(评审实例的唯一路径来源):
158
- // 口径 = **「若传则须可读」**(空/不可读 → throw;不强制必传——无批次档的在途设计评审
159
- // 零回归,N5);绑定落在 resolved.run(评审实例键,与 reviewType/round/designId 同族)——
160
- // 并发设计评审各绑各档,不用单值会话态(BATCH-RECORD.md §4.7 #6)。
269
+ // BATCH-RECORD.md §4.2 评审侧批次档绑定(评审实例的唯一路径来源):准入面与账链门已保证设计评审的
270
+ // `batchDoc`(判据 = `docs/core/design/ADVISOR-GUARDS.md` §2 第 6 ∥ 7 条),本段只剩绑定落 `resolved.run`
271
+ // (评审实例键,与 reviewType/round/designId 同族)——并发设计评审各绑各档,不用单值会话态
272
+ // (BATCH-RECORD.md §4.7 #6)。
161
273
  if (args.batchDoc !== undefined && args.batchDoc !== null) {
162
- resolved.run.batchDoc = resolveBatchDocPath(agent.cwd, args.batchDoc)
274
+ // 设计评审的绝对路径由账链门那一段算好(`designBatchDocAbs`——不二次解析);代码评审仍走原解析。
275
+ resolved.run.batchDoc = designBatchDocAbs ?? resolveBatchDocPath(agent.cwd, args.batchDoc)
163
276
  }
277
+ // §3 点火基线(ADVISOR-GUARDS.md §1.1 判据 1):结算面判「§3 已被本轮写过」需要「本轮之前有几块」
278
+ // 这个点——只在点火拍可得(结算时再读只能得到「现在有几块」)。两轨同源(`run` 由池条目透传)。
279
+ resolved.run.section3BlocksAtStart = section3BlocksBaseline(resolved.run.batchDoc)
164
280
  // Design token minted for EVERY design round — the reviewer echoes it only on
165
281
  // a clean pass; on pass it is slotted under the instance's designId at settle
166
282
  // (sync: right here; async: the settle callback — fix #2). A NEW instance
@@ -139,7 +139,7 @@ export function parkAsyncPending(parent, entry) {
139
139
 
140
140
  /**
141
141
  * §6.31.5 升级账本写助手(消化账务批):`_unsettledDigests` 追加一行
142
- * (`{ id, role, failures, report, at }`)。载体吸收(父字段优先 / history 借用——形态同
142
+ * (`{ id, role, missRounds, report, at }`)。载体吸收(父字段优先 / history 借用——形态同
143
143
  * `parkAsyncPending`);行序 = 升级序(读面 = `digest-account.mjs` `unsettledRows`)。
144
144
  */
145
145
  export function appendUnsettledDigest(parent, row) {
@@ -187,7 +187,7 @@ export function releaseSettledEntry(entry) {
187
187
 
188
188
  /**
189
189
  * §6.31.4 投递面(消化账务批 · 2026-10-05 · 台账 #930):会话态注入后**仅**释放 `childAgent`——
190
- * `report` 保留至销账 / 升级(重投需原文;驻留增量以报告量级 ∥ 重投上限封顶)。fallback 面零调用
190
+ * `report` 保留至销账 / 升级(升级终局披露需原文;驻留增量以报告量级 ∥ 未签收条数封顶)。fallback 面零调用
191
191
  * (既有 `releaseSettledEntry` 逐字沿用——注入即释放)。
192
192
  */
193
193
  export function releaseChildHold(entry) {