@thincoder/core 0.10.12 → 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 (174) hide show
  1. package/CHANGELOG.md +42 -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 +74 -49
  14. package/agent/run-start.mjs +8 -8
  15. package/agent/setup.mjs +6 -11
  16. package/agent/suspension.mjs +1 -1
  17. package/agent/turn-loop.mjs +35 -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 -354
  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 +283 -80
  32. package/agent-tools/batch-unit-test-gate.mjs +107 -0
  33. package/agent-tools/batch.mjs +243 -50
  34. package/agent-tools/context.mjs +6 -3
  35. package/agent-tools/design-slots.mjs +24 -24
  36. package/agent-tools/design-token.mjs +96 -15
  37. package/agent-tools/doc-check.mjs +18 -2
  38. package/agent-tools/eng.mjs +1 -1
  39. package/agent-tools/escalate-async.mjs +3 -3
  40. package/agent-tools/manifest.mjs +8 -5
  41. package/agent-tools/read-history.mjs +5 -5
  42. package/agent-tools/review-facts.mjs +31 -13
  43. package/agent-tools/spawn-gates.mjs +10 -14
  44. package/agent-tools/ssh-session.mjs +120 -111
  45. package/agent-tools/ssh-transfer.mjs +210 -0
  46. package/agent-tools/ssh-transport.mjs +277 -46
  47. package/agent-tools/subagent-actions-query.mjs +1 -1
  48. package/agent-tools/subagent-actions.mjs +1 -1
  49. package/agent-tools/subagent-async.mjs +3 -2
  50. package/agent-tools/subagent-scheduler.mjs +43 -9
  51. package/agent-tools/subagent-spawn.mjs +68 -9
  52. package/agent-tools/subagent.mjs +16 -14
  53. package/agent.mjs +1 -0
  54. package/client-identity.mjs +70 -0
  55. package/config-presets.mjs +5 -3
  56. package/config.mjs +19 -13
  57. package/context.mjs +39 -8
  58. package/declaration.mjs +2 -2
  59. package/doc-check/anchors.mjs +128 -28
  60. package/doc-check/index.mjs +93 -27
  61. package/doc-check/line-refs.mjs +112 -0
  62. package/doc-check/paragraph.mjs +24 -8
  63. package/embedding.mjs +2 -0
  64. package/explore-distill.mjs +1 -1
  65. package/generate-title.mjs +3 -2
  66. package/i18n.mjs +1 -1
  67. package/index-discover.mjs +4 -1
  68. package/ledger-close-proof.mjs +62 -0
  69. package/ledger-cmd.mjs +79 -194
  70. package/ledger-db.mjs +29 -28
  71. package/ledger-migrate.mjs +4 -4
  72. package/ledger-read.mjs +10 -8
  73. package/ledger-surface.mjs +1 -1
  74. package/ledger-tools.mjs +10 -10
  75. package/ledger-variant-notice.mjs +1 -1
  76. package/ledger-write-ops.mjs +373 -0
  77. package/ledger.mjs +2 -2
  78. package/manifest-discovery.mjs +2 -2
  79. package/manifest-schema.mjs +4 -0
  80. package/memory/memory-tool.mjs +5 -2
  81. package/memory/schema.mjs +5 -26
  82. package/model-specs.mjs +6 -5
  83. package/package.json +1 -1
  84. package/path-resolve.mjs +217 -0
  85. package/permission.mjs +4 -0
  86. package/prompt-overlays.mjs +5 -0
  87. package/prompts/advisor-design.md +9 -6
  88. package/prompts/advisor-round1.md +4 -3
  89. package/prompts/advisor-round2.md +6 -5
  90. package/prompts/advisor-round3.md +6 -5
  91. package/prompts/common.md +23 -10
  92. package/prompts/consult-base.md +2 -1
  93. package/prompts/discipline-engineering.md +29 -48
  94. package/prompts/discipline-normal.md +10 -5
  95. package/prompts/persona-eng-coder.md +8 -0
  96. package/prompts/persona-eng-designer.md +22 -12
  97. package/prompts/persona-engineering.md +81 -66
  98. package/prompts/persona-plan.md +2 -1
  99. package/prompts/persona-tester.md +55 -0
  100. package/provider/anthropic.mjs +12 -7
  101. package/provider/core.mjs +21 -5
  102. package/provider/errors.mjs +41 -0
  103. package/provider/google.mjs +25 -9
  104. package/provider/list-models.mjs +4 -3
  105. package/provider/rate.mjs +11 -3
  106. package/provider/responses.mjs +17 -7
  107. package/provider/sse.mjs +15 -4
  108. package/session-identity.mjs +21 -0
  109. package/session-lifecycle.mjs +12 -4
  110. package/session-slots.mjs +9 -14
  111. package/session.mjs +5 -0
  112. package/skip-dirs.mjs +48 -0
  113. package/team-ledger.mjs +4 -4
  114. package/text-budget.mjs +4 -0
  115. package/token-window.mjs +107 -10
  116. package/tool-docs/advisor.md +2 -2
  117. package/tool-docs/apply_patch.md +2 -0
  118. package/tool-docs/bash.md +4 -0
  119. package/tool-docs/batch.md +85 -1
  120. package/tool-docs/browser.md +3 -1
  121. package/tool-docs/code_search.md +3 -1
  122. package/tool-docs/consult_start.md +6 -2
  123. package/tool-docs/consult_stop.md +3 -1
  124. package/tool-docs/delete.md +3 -0
  125. package/tool-docs/doc_check.md +43 -8
  126. package/tool-docs/doc_search.md +5 -1
  127. package/tool-docs/edit.md +22 -1
  128. package/tool-docs/execute.md +7 -1
  129. package/tool-docs/fetch.md +3 -1
  130. package/tool-docs/file_ops.md +3 -1
  131. package/tool-docs/git.md +9 -1
  132. package/tool-docs/goal.md +2 -0
  133. package/tool-docs/grep.md +1 -1
  134. package/tool-docs/hashline_edit.md +2 -0
  135. package/tool-docs/insert_after.md +3 -1
  136. package/tool-docs/ledger.md +5 -5
  137. package/tool-docs/lint.md +1 -0
  138. package/tool-docs/manifest-read.md +2 -1
  139. package/tool-docs/manifest.md +9 -2
  140. package/tool-docs/memory.md +11 -1
  141. package/tool-docs/notify_parent.md +1 -1
  142. package/tool-docs/plan.md +2 -0
  143. package/tool-docs/question.md +1 -1
  144. package/tool-docs/read_history.md +15 -3
  145. package/tool-docs/read_image.md +3 -1
  146. package/tool-docs/recent_changes.md +3 -1
  147. package/tool-docs/repo_outline.md +3 -1
  148. package/tool-docs/settings.md +10 -2
  149. package/tool-docs/skill.md +2 -0
  150. package/tool-docs/ssh.md +30 -17
  151. package/tool-docs/subagent.md +50 -13
  152. package/tool-docs/task.md +2 -0
  153. package/tool-docs/team_ledger.md +1 -1
  154. package/tool-docs/timer.md +7 -0
  155. package/tool-docs/verify.md +13 -1
  156. package/tool-docs/websearch.md +3 -1
  157. package/tool-docs/write.md +2 -0
  158. package/tools/bash.mjs +12 -2
  159. package/tools/edit-batch.mjs +40 -33
  160. package/tools/edit-diff.mjs +69 -1
  161. package/tools/execute.mjs +10 -4
  162. package/tools/file.mjs +17 -27
  163. package/tools/git.mjs +27 -1
  164. package/tools/ops.mjs +2 -1
  165. package/tools/patch.mjs +12 -3
  166. package/tools/process-doc-mark.mjs +139 -0
  167. package/tools/search.mjs +15 -6
  168. package/tools/shared.mjs +24 -1
  169. package/tools/ssh.mjs +48 -7
  170. package/tools/tree.mjs +4 -1
  171. package/tools/write-path.mjs +64 -3
  172. package/traces/trace-store.mjs +4 -3
  173. package/agent/light-round.mjs +0 -63
  174. package/agent/unbooked-pen.mjs +0 -62
@@ -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) {
@@ -38,6 +38,8 @@ import { cleanupOldToolResults, escapeXml } from "../agent/helpers.mjs"
38
38
  import { pushReal } from "../context.mjs"
39
39
  import { makeDecoder, sanitizeOutput } from "../tools/shared.mjs"
40
40
  import { killProcessTree } from "../tools/process-tree.mjs"
41
+ // 过程档标记(TOOLS.md §6.28):后台径的结算摘要也是输出行——解析基准 = entry 记下的 spawn 局部 `cwd`。
42
+ import { markProcessDocLines } from "../tools/process-doc-mark.mjs"
41
43
  import { buildBashEnv } from "../tools/bash.mjs"
42
44
  import {
43
45
  bindChildController, buildChildSignal, carrierField, getAsyncPool, parkAsyncPending,
@@ -119,7 +121,7 @@ export async function launchBgTask(parent, ctx, { command, timeout = null, shell
119
121
  consumeSubagentToken(parent, id, "bash async launch", "bash")
120
122
  assertPoolKeyFree(pool, id, "bash")
121
123
  const logPath = join(dir, `${Date.now()}-bash-${id}.log`)
122
- const cwd = ctx?.cwd ?? parent.cwd ?? process.cwd()
124
+ const cwd = ctx?.cwd ?? parent.cwd
123
125
  // UTF-8 码页前缀 = 同步径同款(win32 + 默认 shell);解码走 `makeDecoder`(同源)。
124
126
  const effectiveCommand = process.platform === "win32" && !shell ? `chcp 65001 >nul && ${command}` : command
125
127
  const child = spawn(effectiveCommand, [], {
@@ -132,6 +134,8 @@ export async function launchBgTask(parent, ctx, { command, timeout = null, shell
132
134
  })
133
135
  const entry = {
134
136
  id, role: "bg", command, child, logPath,
137
+ // 解析基准 = 传给 spawn 的那个局部值(`ctx.cwd` 缺省时它跟着 undefined,另取就错位)
138
+ cwd,
135
139
  status: "running", done: false, cancelled: false, discarded: false,
136
140
  startedAt: Date.now(), endedAt: null, exit: null, killed: null, error: null,
137
141
  report: null, tail: "", tailDropped: false, bytes: 0,
@@ -211,7 +215,7 @@ function finalizeBgEntry(parent, entry, { code = null, signal = null, background
211
215
  wakeAsyncWaiters(parent)
212
216
  }
213
217
 
214
- /** 注入面尾截(双帽 + 截尾判据:行帽/字符帽/环形缓冲丢头三径合一)。 */
218
+ /** 注入面尾截(双帽 + 截尾判据:行帽/字符帽/环形缓冲丢头三径合一),截帽完成后接过程档打标。 */
215
219
  function renderTail(entry) {
216
220
  let text = String(entry.tail ?? "")
217
221
  let truncated = entry.tailDropped === true
@@ -224,6 +228,8 @@ function renderTail(entry) {
224
228
  text = text.slice(-DIGEST_TAIL_MAX_CHARS)
225
229
  truncated = true
226
230
  }
231
+ // §6.28:两个消费点(`finalizeBgEntry` 的 `entry.report` ∥ `injectBgResult` 拼 body)同得标好的文本
232
+ text = markProcessDocLines(text, entry.cwd)
227
233
  return { text, truncated }
228
234
  }
229
235
 
@@ -0,0 +1,206 @@
1
+ /**
2
+ * agent-tools/batch-closeout.mjs — 收口面的四道判据与回执(从 `batch-lifecycle.mjs` 拆出,2026-10-27)。
3
+ *
4
+ * 权威规格 = `docs/core/design/BATCH-LEDGER-REVIEW-UNIFY.md` §2.6(四道门)∥ §2.11(收口前显式处置);
5
+ * 链门的原文契约 = `docs/core/design/BATCH-RECORD.md` §4.18-A。
6
+ *
7
+ * 本档承载三件(都不碰 fs 写、只读与判):
8
+ * ① `assertCloseoutGates` —— 收口两路(`close` ∥ `status` 写 `§1 已收口`)共调的判序:
9
+ * 链完整性门(§2/§3/§4/§6 各段须有内容——`不适用` 声明机制已退役)→ **`§3` 状态词臂**(无条件 = `评审通过`)
10
+ * → 本批批内单测档门(`TESTING.md` §3.5)→ 文档机检门(`DOC-DISCIPLINE.md` §3.20)。
11
+ * ② `assertSectionFiveGate` —— §5 写入门(段序门):两条合取——① `§3` 状态行 = `评审通过`;② `§3` 最近一轮
12
+ * 轮次块自己携 pass 裁定行(块 = `lastRoundBlock`;裁定读数 = 骨架档 `verdictGapOf`,`append §3` 写入闸与
13
+ * 签发面读同一个函数)。旧的第一条旁路(`§1` 声明 §3 不适用)已随本批删除:实现只发生在评审之后,没有例外段。
14
+ * ③ `reanchorReceiptLine` —— 收口重锚回执三行(§2.11 逐字)。
15
+ *
16
+ * 为什么 §3 内容臂留着(不换成只判状态词):内容臂守的是「段里真跑过」,状态词臂守的是「跑的结论是
17
+ * pass」——换掉内容臂 = 门被放宽(一段空着但状态词对就过),那正是本批要堵的洞。所以是**加**一条臂。
18
+ *
19
+ * 依赖单向(KD-4):`skeleton ← closeout ← bind ← reopen ← lifecycle`——本档不 import lifecycle。
20
+ */
21
+ import { readFileSync } from "node:fs"
22
+
23
+ import {
24
+ countRoundBlocks, inapplicableDeclarationError, lastRoundBlock, malformedInapplicableLine, readSectionStatusWord,
25
+ sectionContentState, STATUS_WORDS, verdictGapOf,
26
+ } from "./batch-skeleton.mjs"
27
+ // 裁定读数 = 骨架档 `verdictGapOf` 一个函数(设计档 §2.6.1「判据单源」):`§5` 门判据 2 与签发面判的是同一份
28
+ // 回执文本,组合序各写一遍必然一处变一处不变。本档不再 import 两条裁定锚——锚与序都在那个函数里。
29
+ import { assertUnitTestFilesCleared } from "./batch-unit-test-gate.mjs"
30
+ import { gateRefusalMessage, runBatchGate } from "./batch-gate.mjs"
31
+
32
+ /** 链判段集(§2/§3/§4/§6——§5 段号豁免恒不判;§1 主 agent 自述段不设前置)。 */
33
+ const CHAIN_SEGMENTS = [2, 3, 4, 6]
34
+
35
+ /** §6 空闸拒句(#422①——空 ⇒ 拒 + 提示先写 §6;防「append 被拒 ⇒ close 照常 ⇒ §6 永久为空」复发)。
36
+ * **§6 为唯一缺段时**拒串取本函数(§4.18-A J2——「非空门」文案逐字零回归);判据本体 = 单源谓词
37
+ * `sectionContentState(src, 6)`。 */
38
+ function emptySectionSixError() {
39
+ return "batch: §6 收口段为空(non-empty gate)— close refuses: the closeout section (§6) has no content, and closing would freeze the record with that section permanently empty. Write the section first (append §6: the closeout / verification summary), then call close again. Nothing was written."
40
+ }
41
+
42
+ /** 链门拒绝句(§4.18-A J1——缺段非仅 §6 时;`missing` = 点名串,
43
+ * 形如 `§2 / §3 / §4`——按段号升序列出)。不再提声明路:机制已退役,提它就等于又给一条可照抄的路。 */
44
+ function closeoutChainError(missing) {
45
+ return "batch: 收口链未跑完(closeout chain gate)— close refuses: the record's closeout chain has not run, and closing would freeze it that way. " +
46
+ `Missing: ${missing}. Each named section must carry its own content — the \`不适用\` declaration is retired (every batch needs its design, its review and its approval). ` +
47
+ "§6 must carry its verification. Nothing was written."
48
+ }
49
+
50
+ /** `§3` 状态词臂拒句(设计档 §2.6 `R-G1` 逐字形):链门已过、只剩评审结论没到时出这一条。
51
+ * `value` = 读到的状态词(缺失 ⇒ `(无状态行)`)。本臂无条件——没有「声明不适用」这条旁路。 */
52
+ function closeoutS3Error(value) {
53
+ return "batch: 收口链未跑完(closeout chain gate)— close refuses: " +
54
+ `§3 状态行 = ${value},须 = 评审通过(评审须真跑过并给 pass)。Missing: §3. Nothing was written.`
55
+ }
56
+
57
+ /**
58
+ * 链完整性门(§4.18-A——**收口前置**):判据 = §2 / §3 / §4 / §6 每段须 `sectionContentState(src, seg) !== "empty"`
59
+ * (有内容);不满足 ⇒ throw 并点名缺段(J1;§6 为唯一缺段时 = J2 旧串逐字)。门不判内容质量、
60
+ * 不做自动修复、不判 §1 / §5。声明行扫描先行(J3——那类行现在一律拒,不静默当作无声明)。
61
+ * @param {string} src — 档全文(写盘前只读)
62
+ */
63
+ export function assertCloseoutChainComplete(src) {
64
+ const badDecl = malformedInapplicableLine(src)
65
+ if (badDecl !== null) throw new Error(inapplicableDeclarationError(badDecl))
66
+ const missing = CHAIN_SEGMENTS.filter((n) => sectionContentState(src, n) === "empty")
67
+ if (missing.length === 0) return
68
+ if (missing.length === 1 && missing[0] === 6) throw new Error(emptySectionSixError()) // J2——零回归
69
+ throw new Error(closeoutChainError(missing.map((n) => `§${n}`).join(" / "))) // J1
70
+ }
71
+
72
+ /**
73
+ * 收口两路共调判序(设计档 §2.4–§2.6 · 2026-10-27 四道门):
74
+ * ① 链完整性门(§2 / §3 / §4 / §6 各段须有内容)
75
+ * ② **`§3` 状态词臂**:`§3` 状态行 = `评审通过`(无条件)
76
+ * ③ 本批批内单测档门(仍在盘 ⇒ 拒——零留存的结构兜底)
77
+ * ④ 文档机检门(REFUSED ∥ ERROR ⇒ 拒)
78
+ * 位次 = 既有拒因之后、写盘之前(任一被拒 ⇒ 档字节不变)。返回 `④` 的门结果供回执拼行(`null` = 未跑)。
79
+ * @param {{abs:string, cwd:string, agent:object, src:string, action:"close"|"status"}} o
80
+ * `src` = 调用方已读的写盘前全文(判据与写盘读同一份字节,不二次读盘)。
81
+ * @returns {object|null} 文档机检门结果
82
+ */
83
+ export function assertCloseoutGates({ abs, cwd, agent, src, action = "close" }) {
84
+ assertCloseoutChainComplete(src) // ① 链门(§2 / §3 / §4 / §6 各段须有内容)
85
+ // ② §3 状态词臂(无条件):评审须真跑过并给 pass。旧旁路「§1 声明 §3 不适用」已随声明机制退役删除。
86
+ const s3 = readSectionStatusWord(src, 3)
87
+ if (s3 !== STATUS_WORDS[3].done) throw new Error(closeoutS3Error(s3 ?? "(无状态行)"))
88
+ assertUnitTestFilesCleared(abs, action) // ③
89
+ const gate = runBatchGate({ abs, cwd, agent, scope: "close" }) // ④
90
+ if (gate.state === "refused" || gate.state === "error") throw new Error(gateRefusalMessage(gate, action))
91
+ return gate
92
+ }
93
+
94
+ /** 读档(本档只读一面——写盘归 lifecycle 与 bind;读错上抛不伪装)。 */
95
+ export function readCloseoutSource(abs) { return readFileSync(abs, "utf8") }
96
+
97
+ /**
98
+ * 判据 2 的块内裁定读数(设计档 §2.6「pass 谓词」):`null` = pass;否则 = 拒文括注里的那一格。
99
+ * 组合序不在本档——一律取骨架档 `verdictGapOf`(判据单源,设计档 §2.6.1):写面、`§5` 门与签发面三处同读
100
+ * 一个函数,锚与顺序都只有那一份。本函数只留名字:拒文(`R-G2b`)与设计档用的就是这个符号,读者按它对账。
101
+ * 块 = `null`(段内无块)⇒ `verdictGapOf(null)` = 「无裁定行」,与改前同值。
102
+ * @param {string|null} block — `lastRoundBlock(src, 3)` 的返回形
103
+ * @returns {string|null} 拒文括注值 ∥ `null`(块自己说了 pass)
104
+ */
105
+ function lastRoundVerdictGap(block) {
106
+ return verdictGapOf(block)
107
+ }
108
+
109
+ /** 判据 2 拒文(设计档 §2.6 `R-G2b` 逐字;`blocks` = 段内轮次块数——无块时取 0)。
110
+ * `gap` = `lastRoundVerdictGap` 的非空返回形,落进括注的裁定格。 */
111
+ function sectionFiveVerdictBlockError(src, gap) {
112
+ const blocks = countRoundBlocks(src, 3)
113
+ return "batch: §5 append refused — the last §3 round block carries no pass verdict " +
114
+ `(§3 最近一轮轮次块 = 第 ${blocks} 轮,块内裁定 = ${gap}). A pass means that block carries a \`VERDICT: pass\` line and no changes-required line — ` +
115
+ "a round that does not state a pass is not a pass: append the verdict line to that block (`VERDICT: pass`),或按裁定行重跑评审。Nothing was written."
116
+ }
117
+
118
+ /**
119
+ * §5 写入门(段序门 · 设计档 §2.6):实现发生在评审通过之后,序在这里才判得出真假。
120
+ * 判据 = 两条合取(**没有第一条的旁路**:`§1` 声明 §3 不适用已随声明机制退役——实现只发生在评审之后),
121
+ * 先判状态词、过了再判块(状态词不过时拒文已足以说明,不必再报块):
122
+ * ① `§3` 状态行 = `评审通过`(拒文 `R-G2`);
123
+ * ② `§3` **最近一轮轮次块自己携 pass 裁定行**(拒文 `R-G2b`)——评审写了一笔 `§3` 却不带任何裁定行时,
124
+ * 上一轮的 `评审通过` 仍是字面字,只判 ① 就会放行。② 只问一个可读的事实「门所依赖的块自己有没有
125
+ * 说结论」,不猜意图(补记 ∥ 隐性打回的处置是同一条);`§3` 无任何轮次块 ⇒ ② 不成立、照拒——
126
+ * 真跑过的 pass 必落本轮块,无块即无 pass 可依。
127
+ * 修正轮在 §3 已 pass 之后追加 §5,末块携 pass 裁定 ⇒ 两判据当场成立。`status` 单写不再足以开 `§5`(只置状态词、不落一块携 pass 裁定的
128
+ * 轮次块 ⇒ 照拒)。
129
+ * @param {string} src — 写盘前的档全文
130
+ */
131
+ export function assertSectionFiveGate(src) {
132
+ const s3 = readSectionStatusWord(src, 3)
133
+ if (s3 !== STATUS_WORDS[3].done) {
134
+ throw new Error("batch: §5 append refused — the implementation section opens only after the design review passed " +
135
+ `(§3 状态行 = ${s3 ?? "(无状态行)"},须 = 评审通过). Nothing was written.`)
136
+ }
137
+ const gap = lastRoundVerdictGap(lastRoundBlock(src, 3))
138
+ if (gap !== null) throw new Error(sectionFiveVerdictBlockError(src, gap))
139
+ }
140
+
141
+ /**
142
+ * 收口重锚回执段(设计档 §2.11 逐字三行;空行不出):
143
+ * `[ledger reanchor] 本批在途行 → 待核销 N 行:#12 「…」;#15 「…」 —— 认领行只有两种终局:…`
144
+ * `[ledger reanchor] 本批未动行 → 条件 M 行:#16 「…」 —— 这些行不是本批的活(未认领却被绑在本档 = 点火违规)…`
145
+ * `[ledger reanchor] 本批 N+M 行由重锚默认推进,父侧未显式处置:#12, #15, #16`
146
+ * **建议句贴在自己那一类行上**:第一行只装 `在途` 行、第二行只装 `待讨论` / `待设计` 行——两类的出路相反
147
+ * (认领行只有两种终局;未认领行本来就不该在本批),一句管两类正是要收的病句;汇总行因此只点名、不给建议。
148
+ * 第三行只在 N+M > 0 时出现(父侧全处置过 ⇒ 零该行,回执不噪)。失败 ⇒ 只出失败行(台账零写、冻结照常)。
149
+ * @param {{reanchored:Array, failed:string|null}|null} result — `ledgerReanchorBatch` 返回形
150
+ */
151
+ export function reanchorReceiptLine(result) {
152
+ if (!result) return ""
153
+ if (result.failed) return `\n[ledger reanchor] 未重锚:${result.failed}——台账零写;记录冻结照常完成`
154
+ const face = (r) => `#${r.id} 「${r.title}」`
155
+ const toSettle = (result.reanchored ?? []).filter((r) => r.to === "待核销")
156
+ const toCondition = (result.reanchored ?? []).filter((r) => r.to === "条件")
157
+ const lines = []
158
+ // 建议句各贴其行(§2.11):第一行只装认领行、第二行只装未认领行,第三行只点名。
159
+ if (toSettle.length) {
160
+ lines.push(`\n[ledger reanchor] 本批在途行 → 待核销 ${toSettle.length} 行:${toSettle.map(face).join(";")}` +
161
+ " —— 认领行只有两种终局:做完的 close 勾销(待核销 → 已核销),判定不做的 update → 已废弃 并写判据(认领即承诺,没有第三种)")
162
+ }
163
+ if (toCondition.length) {
164
+ lines.push(`\n[ledger reanchor] 本批未动行 → 条件 ${toCondition.length} 行:${toCondition.map(face).join(";")}` +
165
+ " —— 这些行不是本批的活(未认领却被绑在本档 = 点火违规);已落 trigger = 条件 + 复启句,由下一批接手,本批不再处置")
166
+ }
167
+ const all = result.reanchored ?? []
168
+ if (all.length) {
169
+ lines.push(`\n[ledger reanchor] 本批 ${all.length} 行由重锚默认推进,父侧未显式处置:${all.map((r) => `#${r.id}`).join(", ")}`)
170
+ }
171
+ return lines.join("")
172
+ }
173
+
174
+ /** 新事理由的 `evidence` 前缀(单源 = `docs/core/design/LEDGER.md` §6.3「参数面」;写入侧由
175
+ * `thincoder-core/ledger-write-ops.mjs` 的 `NEW_DEBT_EVIDENCE_PREFIX` 造,本档按同一字面取文)。 */
176
+ const NEW_DEBT_PREFIX = "新事理由:"
177
+ /** `evidence` 追加相接符(追加形 `旧|新` 零空格——§6.3「取文」)。 */
178
+ const EVIDENCE_JOINER = "|"
179
+
180
+ /**
181
+ * 取一行新事理由(设计档 §6.3「取文」):`evidence` 中 `新事理由:` 之后、至首个 `|`(追加相接符)
182
+ * 或串尾的一段。无该前缀 ∥ 取到空串 ⇒ `null`(负向锁:不是本机制写的行不引)。**只读**——不改入参。
183
+ * @param {string|null} evidence — 行 `evidence` 原值
184
+ * @returns {string|null} 理由原文 ∥ `null`
185
+ */
186
+ export function newDebtReasonOf(evidence) {
187
+ const s = String(evidence ?? "")
188
+ const at = s.indexOf(NEW_DEBT_PREFIX)
189
+ if (at < 0) return null
190
+ const rest = s.slice(at + NEW_DEBT_PREFIX.length)
191
+ const cut = rest.indexOf(EVIDENCE_JOINER)
192
+ const value = (cut < 0 ? rest : rest.slice(0, cut)).trim()
193
+ return value === "" ? null : value
194
+ }
195
+
196
+ /**
197
+ * 收口回执的新事理由引文段(逐字 = 设计档 §6.3「理由的读面与时点」:` 新事理由(#<id>):<理由原文>`)。
198
+ * 读者 = 本批收口那一笔的主 agent · 时点 = 本批收口。**零新清单、只引不写**:纯格式化、零库访问、
199
+ * 不改入参;一条理由一行。取不到本批行 ⇒ 入参无该键 ⇒ 不出该段(收口不被否决)。
200
+ * @param {{newDebtReasons?:{id:number, reason:string}[]}|null} result — `reanchorOnClose` 返回形
201
+ * @returns {string} 以换行起头;无引文 ⇒ 空串
202
+ */
203
+ export function newDebtReasonReceiptLine(result) {
204
+ const rows = result?.newDebtReasons ?? []
205
+ return rows.map((r) => `\n 新事理由(#${r.id}):${r.reason}`).join("")
206
+ }