@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
@@ -15,9 +15,11 @@
15
15
  * 依赖(KD-4 单向):本档为**叶档**——只 import `node:*` 与 `../manifest.mjs`;主档 re-export 保既有面。
16
16
  */
17
17
  import { existsSync, statSync } from "node:fs"
18
- import { isAbsolute, relative, resolve, sep } from "node:path"
18
+ import { isAbsolute, parse, relative, resolve, sep } from "node:path"
19
19
 
20
20
  import { docRootBase, docRootPaths, owningProject, projectRootView, readManifest, resolveProjectRoot } from "../manifest.mjs"
21
+ // 候选序单源 = 统一解析层(`docs/core/design/PATH-RESOLUTION.md` §2.2);本档只留本面参数与落位判据。
22
+ import { danglingSameNameHint, resolvePathTarget } from "../path-resolve.mjs"
21
23
 
22
24
  /** 可读文件判据(存在且为文件——目录 / 缺失同判不可读)。 */
23
25
  function readableFile(abs) {
@@ -26,7 +28,7 @@ function readableFile(abs) {
26
28
 
27
29
  /** 工作区锚候选(判定单点 = `projectRootView`——`workspace` 态 ⇒ 候选全列**按名排序**;余者 null)。 */
28
30
  function workspaceCandidates(cwd) {
29
- const view = projectRootView(cwd ?? process.cwd())
31
+ const view = projectRootView(cwd)
30
32
  return view.state === "workspace" ? view.candidates : null
31
33
  }
32
34
 
@@ -44,7 +46,7 @@ export function batchProjectRoot(cwd) {
44
46
 
45
47
  /** manifest **声明基底**(声明面;manifest 缺失 / 非法 / 读错 ⇒ 空集——v1 单基底语义零变)。 */
46
48
  function declaredBases(cwd) {
47
- const base = cwd ?? process.cwd()
49
+ const base = cwd
48
50
  let man = { ok: false }
49
51
  try { man = readManifest(base) } catch { /* 读错 → 按无 manifest 处理(回退默认基底) */ }
50
52
  return man.ok ? docRootPaths(man.manifest?.docRoot?.batches, base) : []
@@ -103,53 +105,56 @@ const anchoredEscapeError = (raw) => new Error(`batch: create path is anchored a
103
105
  const workspaceCreateError = (raw, candidates) => new Error(`batch: create target "${raw}" belongs to no project — the session anchor holds ${candidates.length} project(s) below it: ${candidates.join(", ")}. Pass an explicit path under a project (e.g. "${candidates[0]}/docs/batches/<file>.md").`)
104
106
  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(", ")}`)
105
107
 
106
- /** 工作区锚读面候选分支(条 5):逐候选项目——项目根形(`resolve(candidate, p)`)∥ 声明基底形
107
- * (候选按名排序、并集保序——「首个可读」在此即为确定判读)。 */
108
- function workspaceReadLegs(cwd, p) {
109
- const ws = workspaceCandidates(cwd)
110
- if (!ws) return []
111
- const out = []
112
- for (const c of ws) {
113
- out.push(resolve(c, p))
114
- for (const b of declaredBases(c)) out.push(resolve(b, p))
108
+ /**
109
+ * **读面**解析的四态本体(候选序 + 判态全部经统一解析层——本面只声明自己的参数:
110
+ * `order:"near-first"`(近基根 = cwd / 会话锚 先)+ `kind:"file"` + `crossRepo:"dangling"`,
111
+ * `bases` = `docRoot.batches` 声明面(读面无 manifest ⇒ 无这一级))。锚定串只解析项目根形
112
+ * (不二次拼接——§4.15 判据,见 `resolveBatchReadPath`)。
113
+ * 导出面供 spawn 门拼「追加同名候选」(§2.6:既有文案逐字在前,新信息追加在后)。
114
+ * @returns {object} 统一解析层的四态结果(`docs/core/design/PATH-RESOLUTION.md` §2.1)
115
+ */
116
+ export function resolveBatchReadResult(cwd, given) {
117
+ const base = cwd
118
+ const raw = typeof given === "string" ? given.trim() : ""
119
+ if (!raw) return { state: "dangling", missing: String(given ?? ""), sameName: [], level: "path", tried: [] }
120
+ const p = raw.replace(/\\/g, "/")
121
+ if (anchoredPrefix(p, basePrefixes(base, batchDocBases(base)))) {
122
+ // 锚定形:只解析项目根形(已含基底相对前缀者不再二次拼接)
123
+ const root = batchProjectRoot(base)
124
+ return resolvePathTarget(root, p, { kind: "file", order: "root-first", bases: [root] })
115
125
  }
116
- return out
126
+ return resolvePathTarget(base, raw, { kind: "file", order: "near-first", bases: declaredBases(base) })
117
127
  }
118
128
 
119
129
  /**
120
- * **读面**解析(append / status / close 与评审门 / spawn 门共用):首个**可读**候选胜;无 ⇒ null
121
- * (调用方各自抛既有文案)。锚定串只解析项目根形(跳过 ①③——已含基底相对前缀者不再二次拼接)。
122
- * @param {string} cwd 会话锚(缺省 → 进程 cwd)
130
+ * **读面**解析(append / status / close 与评审门 / spawn 门共用):解析出唯一落点且可读 ⇒ 该绝对路径;
131
+ * 其余(无果 / 判不出唯一)⇒ null(调用方各自抛既有文案)。
132
+ * @param {string} cwd 会话锚
123
133
  * @param {string} given 参数原值(相对 / 绝对、`\` 与 `/` 混写均可)
124
134
  * @returns {string|null} 可读文件的绝对路径 / null
125
135
  */
126
136
  export function resolveBatchReadPath(cwd, given) {
127
- const base = cwd ?? process.cwd()
128
- const raw = typeof given === "string" ? given.trim() : ""
129
- if (!raw) return null
130
- const p = raw.replace(/\\/g, "/")
131
- if (anchoredPrefix(p, basePrefixes(base, batchDocBases(base)))) {
132
- const abs = resolve(batchProjectRoot(base), p)
133
- return readableFile(abs) ? abs : null
134
- }
135
- const candidates = [resolve(base, p), resolve(batchProjectRoot(base), p), ...declaredBases(base).map((b) => resolve(b, p)), ...workspaceReadLegs(base, p)]
136
- for (const abs of candidates) if (readableFile(abs)) return abs
137
- return null
137
+ const r = resolveBatchReadResult(cwd, given)
138
+ return r.state === "resolved" && readableFile(r.abs) ? r.abs : null
138
139
  }
139
140
 
140
141
  /**
141
142
  * 批次档路径门禁(评审侧 `BATCH-RECORD.md` §4.2 口径 = **「若传则须可读」**):空 / 非字符串 / 不可读 → throw。
142
- * 非空且可读 → 返回绝对路径(`\` 归一——照 `files` / `batchDoc` spawn 门先例)。错误文案逐字零变(零回归硬约束)。
143
+ * 非空且可读 → 返回绝对路径(`\` 归一——照 `files` / `batchDoc` spawn 门先例)。既有文案逐字在前,
144
+ * 层的同名候选 / 候选全列**追加在后**(§2.6:新信息只追加)。
143
145
  */
144
146
  export function resolveBatchDocPath(cwd, given) {
145
- const base = cwd ?? process.cwd()
147
+ const base = cwd
146
148
  const raw = typeof given === "string" ? given.trim() : ""
147
149
  if (!raw) {
148
150
  throw new Error("batchDoc must be a non-empty path to the batch record — pass the batch record currently in flight, or omit the parameter entirely when no batch record is in flight.")
149
151
  }
150
- const abs = resolveBatchReadPath(base, raw)
151
- if (abs) return abs
152
- throw new Error(`batchDoc is not a readable file: ${raw} — pass the path of the batch record currently in flight (a path that resolves to an existing file), or omit the parameter when no batch record is in flight.`)
152
+ const r = resolveBatchReadResult(base, raw)
153
+ if (r.state === "resolved" && readableFile(r.abs)) return r.abs
154
+ const hint = r.state === "dangling"
155
+ ? (danglingSameNameHint(r) ? ` ${danglingSameNameHint(r)}` : "")
156
+ : r.state === "ambiguous" ? ` Candidates: ${r.candidates.join(" ∥ ")}` : ""
157
+ throw new Error(`batchDoc is not a readable file: ${raw} — pass the path of the batch record currently in flight (a path that resolves to an existing file), or omit the parameter when no batch record is in flight.${hint ? `;${hint.trim()}` : ""}`)
153
158
  }
154
159
 
155
160
  /**
@@ -163,7 +168,7 @@ export function resolveBatchDocPath(cwd, given) {
163
168
  * @returns {string} 落位绝对路径
164
169
  */
165
170
  export function resolveBatchCreatePath(cwd, given, bases) {
166
- const base = cwd ?? process.cwd()
171
+ const base = cwd
167
172
  const raw = String(given ?? "").trim()
168
173
  const p = raw.replace(/\\/g, "/")
169
174
  let roots = bases
@@ -183,13 +188,15 @@ export function resolveBatchCreatePath(cwd, given, bases) {
183
188
  if (!insideBases(abs, roots)) throw anchoredEscapeError(raw)
184
189
  return abs
185
190
  }
186
- for (const abs of [resolve(base, p), resolve(batchProjectRoot(base), p)]) {
187
- if (insideBases(abs, roots)) return abs
191
+ // 候选序由统一解析层给(① 近基根 cwd/会话锚 → ② 项目根);落位判据(落基底内)在原地判
192
+ for (const anchor of [base, batchProjectRoot(base)]) {
193
+ const r = resolvePathTarget(anchor, p, { kind: "none", order: "near-first", bases: roots })
194
+ if (r.state === "resolved" && insideBases(r.abs, roots)) return r.abs
188
195
  }
189
196
  // ③ 基底自有项目根形(#942 条 6):owningProject 可判者逐基底取值;distinct 落点 ≥2 ⇒ 拒
190
197
  const owned = []
191
198
  for (const b of roots) {
192
- const ownRoot = owningProject(b)
199
+ const ownRoot = owningProject(b, parse(b).root)
193
200
  if (!ownRoot) continue
194
201
  const abs = resolve(ownRoot, p)
195
202
  if (insideBases(abs, roots) && !owned.some((x) => samePath(x, abs))) owned.push(abs)
@@ -199,10 +206,8 @@ export function resolveBatchCreatePath(cwd, given, bases) {
199
206
  // ④ 基底相对形收窄单段(归一后无 `/` 才走原基底分支——多段串不二次拼接)
200
207
  const leafRel = roots.length ? relative(roots[0], resolve(roots[0], p)) : ""
201
208
  if (leafRel && !leafRel.startsWith("..") && !leafRel.includes(sep)) {
202
- for (const b of roots) {
203
- const abs = resolve(b, p)
204
- if (insideBases(abs, roots)) return abs
205
- }
209
+ const r = resolvePathTarget(roots[0], p, { kind: "none", order: "near-first", bases: roots })
210
+ if (r.state === "resolved" && insideBases(r.abs, roots)) return r.abs
206
211
  }
207
212
  throw outsideBasesError(raw)
208
213
  }
@@ -20,7 +20,7 @@ const SEGMENTS = [1, 2, 3, 4, 5, 6]
20
20
  /** 批次档列举(只读):`{ batches: [{ file, path, sections }] }`——`path` = 绝对路径(`/` 归一)、
21
21
  * `file` = basename、`batches` 按路径升序;单档读错跳过(尽力面)。`bases` 缺省 = `batchDocBases(cwd)`。 */
22
22
  export function listBatchRecords({ cwd, bases } = {}) {
23
- const base = cwd ?? process.cwd()
23
+ const base = cwd
24
24
  const roots = bases ?? batchDocBases(base)
25
25
  const files = new Set()
26
26
  for (const root of roots) for (const abs of collectMarkdownFiles(root)) files.add(abs.replace(/\\/g, "/"))
@@ -0,0 +1,142 @@
1
+ /**
2
+ * agent-tools/batch-reopen.mjs — 重开径(`§1` 已收口 → 进行中)与台账回收调点(从 `batch-lifecycle.mjs`
3
+ * 拆出,2026-10-27)。
4
+ *
5
+ * 权威规格 = `docs/core/design/BATCH-RECORD.md` §4.18-B(重开的档面语义)+ `docs/core/design/BATCH-LEDGER-REVIEW-UNIFY.md`
6
+ * §2.13(重开回收:收口的反面——批档开 ⇒ 台账行回工作位)。
7
+ *
8
+ * 两件事:
9
+ * ① `rewriteAsReopen` —— 一次写盘、两处落笔:状态行原位改写 + §1 段尾追记行(append-only:行数 +1、
10
+ * 仅状态行原位替换、余行逐字节不变)。
11
+ * ② `reclaimOnReopen` —— 台账写点(`ledgerReclaimBatch`):`待核销 → 在途`(行回工作位);`已核销` 不动
12
+ * (终态)+ 回执点名;其余状态零触。**失败不否决重开**(重开是从误冻结里出来的唯一通路,台账库忙不该
13
+ * 把档锁死)——失败 ⇒ 重开照成、回执点名失败原文 + 后果 + 出路。
14
+ *
15
+ * 触发判据**不在这里**:只由 `batch-lifecycle.mjs` 的 `reopen` 合取触发(`seg === 1 ∧ depth0 ∧ ¬review ∧
16
+ * 值 = 进行中 ∧ 前态 = closed`)——两处各推一次必然漂移,本档只承接「已判定要重开」之后的动作。
17
+ */
18
+ import { dirname, relative } from "node:path"
19
+
20
+ import { STATUS_LINE_RE, sectionHeaderRe, updateSectionStatusLine } from "./batch-skeleton.mjs"
21
+
22
+ /** 本地今天(YYYY-MM-DD)——重开日期戳(close 侧同一枚,经本档导出复用——日期口径只此一处)。 */
23
+ export function todayLocal() {
24
+ const d = new Date()
25
+ const p = (n) => String(n).padStart(2, "0")
26
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`
27
+ }
28
+
29
+ /** note 单行判据(两径共用——非重开径落状态行括注、重开径落 §1 追记行,两者都是一行;重开径不取
30
+ * `assertStatusNote` 的另两条判据〔词表零命中 / 死占位〕——理由允许携状态词表关键字,而追记行不是
31
+ * 状态行形态、不参与 gate 判读)。 */
32
+ export function assertSingleLineNote(note) {
33
+ if (/\r?\n/.test(note)) {
34
+ throw new Error("batch: status note must be a single line — a multi-line note would break the one-line status-line form. Nothing was written.")
35
+ }
36
+ }
37
+
38
+ /** 重开缺理由拒绝句(§4.18-B J4——触发合取缺 note;逐字文本 = `TOOLS.md` §6.24 J4)。 */
39
+ export function reopenReasonError() {
40
+ return "batch: 重开需理由(reopen requires a reason)— reopening a frozen record reverses a recorded decision, so the reason must be stated: pass it as `note` (it lands verbatim in the §1 reopen record). Nothing was written."
41
+ }
42
+
43
+ /** §1 状态行值原文(改写前的读取面——readBatchStatusLine 只给三态,日期字面须自己读)。
44
+ * **取值区 = §1 段头区**(`## §1` 标题之后、下一 `^#{2,}` 起行之前——与写面同一条定位面):
45
+ * 回执要的是真段级那一行,段体 / 轮次块里引述的状态行不算。 */
46
+ export function readBatchStatusLineRaw(src) {
47
+ const s1 = /^## §1(?=\s|$)/m.exec(src)
48
+ if (!s1) return null
49
+ const zone = src.slice(s1.index + s1[0].length)
50
+ const cut = zone.search(/^#{2,}/m) // 区界判据与写面同一条:任意 `##` / `###` 起行都结束段头区
51
+ const body = cut < 0 ? zone : zone.slice(0, cut)
52
+ return body.split(/\r?\n/).map((line) => STATUS_LINE_RE.exec(line)).find(Boolean)?.[1] ?? null
53
+ }
54
+
55
+ /**
56
+ * §N 段尾追加一行(§4.18-B——重开追记行载体;append-only:只追加,既有行字节零变)。
57
+ * 位置 = 段体尾部(下一 `## §` 段头之前 ∥ 文末);追加行 EOL = §N 段头行自身行尾
58
+ * (`\r\n` ⇒ CRLF ∥ 其余 ⇒ `\n`——探测窗 = 段头行整体〔含行尾〕)。段标题缺失 ⇒ throw。
59
+ */
60
+ export function appendLineToSection(src, seg, line) {
61
+ const hdr = sectionHeaderRe(seg).exec(src)
62
+ if (!hdr) {
63
+ throw new Error(`batch: the batch record has no "## §${seg}" section header — cannot append to its tail (create is the skeleton's only authoritative entry). Nothing was written.`)
64
+ }
65
+ const headerLine = /^## §\d[^\r\n]*\r?\n/m.exec(src.slice(hdr.index))
66
+ const eol = headerLine?.[0].endsWith("\r\n") ? "\r\n" : "\n"
67
+ const nextRe = /^## §\d/gm
68
+ nextRe.lastIndex = hdr.index + hdr[0].length
69
+ const next = nextRe.exec(src)
70
+ const endIdx = next ? next.index : src.length
71
+ let before = src.slice(0, endIdx)
72
+ const after = src.slice(endIdx)
73
+ // 只保证「行自成行」:段尾无换行 ⇒ 补一个段头行行尾;已有换行零补(不插空行——只追加判据 = 行数 +1)
74
+ if (!before.endsWith(eol)) before += eol
75
+ return before + line + eol + after
76
+ }
77
+
78
+ /**
79
+ * 重开写盘全文(§4.18-B——已收口档 status(进行中, note 非空)):一次写盘、两处落笔——
80
+ * ① §1 状态行 → `进行中(重开 <YYYY-MM-DD>)`;② §1 **段尾追加**追记行
81
+ * `**重开记录**:<日期> · 原收口 <旧日期> · 理由 = <note> · 操作者 = 主 agent`
82
+ * (原收口 = 改写前状态行的收口日期字面,无日期字面 ⇒ `未标日期`)。
83
+ * EOL = §1 段头行自身行尾(CRLF 档 ⇒ CRLF)。
84
+ * @param {string} src — 档全文(closed 态)
85
+ * @param {string} note — 重开理由(调用面已验非空单行)
86
+ * @returns {string} 写盘用新全文
87
+ */
88
+ export function rewriteAsReopen(src, note) {
89
+ const prevLine = (readBatchStatusLineRaw(src) ?? "").trim()
90
+ const prevDate = /已收口\s*(\d{4}-\d{2}-\d{2})/.exec(prevLine)?.[1] ?? "未标日期"
91
+ const date = todayLocal()
92
+ const withStatus = updateSectionStatusLine(src, 1, `进行中(重开 ${date})`)
93
+ return appendLineToSection(withStatus, 1, `**重开记录**:${date} · 原收口 ${prevDate} · 理由 = ${note} · 操作者 = 主 agent`)
94
+ }
95
+
96
+ /**
97
+ * 重开回收调点(设计档 §2.13):写入单源 = `ledgerReclaimBatch`(本档只供 `cwd` 基准、会话 id 与回执形;
98
+ * 工具层零 SQL)。`cwd` = **批档所属项目根**(`resolveLedgerRoot(dirname(<batchDoc>), boundary)?.root`——
99
+ * 不用会话锚:多根工作区锚会收到别的项目根 ⇒ 选行零命中 ⇒ 唯一的机器写静默失效);`boundary` = 会话锚。
100
+ * 无根 ⇒ 传 `null`(核心不取行,`failed` 点名——不落兜底基准)。
101
+ * **失败不抛**(台账忙 ∥ 无根均不否决重开——档面写与台账库分属两存储)。
102
+ * @param {string} abs — 本批批档绝对路径
103
+ * @param {string} boundary — 会话锚(上溯上界)
104
+ * @returns {Promise<{batch:string, reclaimed:Array, unreclaimable:Array, failed:string|null}>}
105
+ */
106
+ export async function reclaimOnReopen(abs, boundary) {
107
+ try {
108
+ const { ledgerReclaimBatch, resolveLedgerRoot } = await import("../ledger.mjs") // 动态 import:ledger-db 静态引 node:sqlite(W8 契约②)
109
+ const { getSessionId } = await import("../session-slots.mjs") // 同一动态 import 面:executor = 本会话(AC-M2-7)
110
+ const root = resolveLedgerRoot(dirname(abs), boundary)?.root ?? null
111
+ // 句内路径统一正斜杠(与 `task_book` 列值同形——`<批名>` 是给人读的事实句,不是平台路径)
112
+ return ledgerReclaimBatch({
113
+ cwd: root,
114
+ batch: root ? relative(root, abs).replace(/\\/g, "/") : abs,
115
+ boundary,
116
+ executorSessionId: getSessionId(),
117
+ })
118
+ } catch (e) {
119
+ // 非预期异常(模块加载失败 ∥ 根推导内读声明抛)同样不得否决重开——降为同一返回形(台账零改)
120
+ return { batch: abs, reclaimed: [], unreclaimable: [], failed: String(e?.message ?? e) }
121
+ }
122
+ }
123
+
124
+ /**
125
+ * 重开回收回执(设计档 §2.13 逐字;空行不出):`failed` 非空 ⇒ 只出第三行(替代前两行)。
126
+ * 第三行 = 失败原文 + 后果 + 出路——「行仍 `待核销` ⇒ 台账里没有该档的在途行(写锁会拒每一笔写盘),
127
+ * 库恢复后 update → 在途」。
128
+ * @param {{reclaimed:Array, unreclaimable:Array, failed:string|null}|null} result
129
+ */
130
+ export function reopenReceiptLine(result) {
131
+ if (!result) return ""
132
+ const face = (r) => `#${r.id} 「${r.title}」`
133
+ if (result.failed) {
134
+ return `\n[ledger reopen] 回收失败:${result.failed}——台账零改;行仍 待核销 的,台账里没有该档的在途行(写锁会拒每一笔写盘),库恢复后 update → 在途`
135
+ }
136
+ const lines = []
137
+ if ((result.reclaimed ?? []).length) lines.push(`\n[ledger reopen] 本批待核销行 → 在途 ${result.reclaimed.length} 行:${result.reclaimed.map(face).join(";")}`)
138
+ if ((result.unreclaimable ?? []).length) {
139
+ lines.push(`\n[ledger reopen] 本批已核销行不可回收 ${result.unreclaimable.length} 行:${result.unreclaimable.map(face).join(";")}——已核销为终态,重开不回收;要继续做须另立新行(新档 §1「前情」指回本档)`)
140
+ }
141
+ return lines.join("")
142
+ }