@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.
- package/CHANGELOG.md +29 -0
- package/advisor/messages.mjs +4 -2
- package/advisor/notice.mjs +1 -1
- package/advisor/project-context.mjs +121 -11
- package/agent/completion.mjs +13 -1
- package/agent/digest-account.mjs +73 -46
- package/agent/dispatch-run.mjs +1 -1
- package/agent/dispatch.mjs +29 -21
- package/agent/family-tools.mjs +9 -5
- package/agent/helpers.mjs +1 -94
- package/agent/offload.mjs +88 -0
- package/agent/record-results.mjs +1 -6
- package/agent/run-stages.mjs +69 -48
- package/agent/run-start.mjs +8 -8
- package/agent/setup.mjs +3 -11
- package/agent/suspension.mjs +1 -1
- package/agent/turn-loop.mjs +32 -3
- package/agent/write-face.mjs +99 -0
- package/agent/write-gate.mjs +354 -5
- package/agent-tools/advisor.mjs +127 -11
- package/agent-tools/async-settle.mjs +2 -2
- package/agent-tools/bash-async.mjs +8 -2
- package/agent-tools/batch-closeout.mjs +206 -0
- package/agent-tools/batch-gate.mjs +287 -73
- package/agent-tools/batch-ledger-bind.mjs +183 -0
- package/agent-tools/batch-ledger-link.mjs +73 -0
- package/agent-tools/batch-lifecycle.mjs +113 -367
- package/agent-tools/batch-paths.mjs +45 -40
- package/agent-tools/batch-read.mjs +1 -1
- package/agent-tools/batch-reopen.mjs +142 -0
- package/agent-tools/batch-skeleton.mjs +275 -80
- package/agent-tools/batch.mjs +243 -50
- package/agent-tools/context.mjs +6 -3
- package/agent-tools/design-slots.mjs +24 -24
- package/agent-tools/design-token.mjs +96 -15
- package/agent-tools/doc-check.mjs +18 -2
- package/agent-tools/eng.mjs +1 -1
- package/agent-tools/escalate-async.mjs +3 -3
- package/agent-tools/manifest.mjs +8 -5
- package/agent-tools/read-history.mjs +5 -5
- package/agent-tools/review-facts.mjs +31 -13
- package/agent-tools/spawn-gates.mjs +10 -14
- package/agent-tools/ssh-session.mjs +120 -111
- package/agent-tools/ssh-transfer.mjs +210 -0
- package/agent-tools/ssh-transport.mjs +277 -46
- package/agent-tools/subagent-actions-query.mjs +1 -1
- package/agent-tools/subagent-actions.mjs +1 -1
- package/agent-tools/subagent-async.mjs +3 -2
- package/agent-tools/subagent-scheduler.mjs +43 -9
- package/agent-tools/subagent-spawn.mjs +68 -9
- package/agent-tools/subagent.mjs +16 -14
- package/client-identity.mjs +22 -6
- package/config-presets.mjs +5 -3
- package/context.mjs +34 -3
- package/declaration.mjs +2 -2
- package/doc-check/anchors.mjs +128 -28
- package/doc-check/index.mjs +68 -15
- package/doc-check/line-refs.mjs +112 -0
- package/doc-check/paragraph.mjs +18 -5
- package/explore-distill.mjs +1 -1
- package/i18n.mjs +1 -1
- package/index-discover.mjs +4 -1
- package/ledger-close-proof.mjs +19 -37
- package/ledger-cmd.mjs +71 -195
- package/ledger-db.mjs +29 -28
- package/ledger-migrate.mjs +4 -4
- package/ledger-read.mjs +10 -8
- package/ledger-surface.mjs +1 -1
- package/ledger-tools.mjs +6 -6
- package/ledger-variant-notice.mjs +1 -1
- package/ledger-write-ops.mjs +373 -0
- package/ledger.mjs +2 -2
- package/manifest-discovery.mjs +2 -2
- package/manifest-schema.mjs +4 -0
- package/memory/memory-tool.mjs +5 -2
- package/memory/schema.mjs +5 -26
- package/package.json +1 -1
- package/path-resolve.mjs +217 -0
- package/permission.mjs +4 -0
- package/prompt-overlays.mjs +5 -0
- package/prompts/advisor-design.md +9 -6
- package/prompts/advisor-round1.md +4 -3
- package/prompts/advisor-round2.md +6 -5
- package/prompts/advisor-round3.md +6 -5
- package/prompts/common.md +23 -10
- package/prompts/consult-base.md +2 -1
- package/prompts/discipline-engineering.md +29 -48
- package/prompts/discipline-normal.md +10 -5
- package/prompts/persona-eng-coder.md +8 -0
- package/prompts/persona-eng-designer.md +22 -12
- package/prompts/persona-engineering.md +81 -68
- package/prompts/persona-plan.md +2 -1
- package/prompts/persona-tester.md +55 -0
- package/provider/core.mjs +2 -2
- package/provider/google.mjs +15 -5
- package/provider/rate.mjs +11 -3
- package/provider/sse.mjs +15 -4
- package/session-identity.mjs +21 -0
- package/session-lifecycle.mjs +9 -4
- package/session-slots.mjs +9 -14
- package/session.mjs +5 -0
- package/skip-dirs.mjs +48 -0
- package/team-ledger.mjs +4 -4
- package/text-budget.mjs +4 -0
- package/token-window.mjs +90 -14
- package/tool-docs/advisor.md +2 -2
- package/tool-docs/apply_patch.md +2 -0
- package/tool-docs/bash.md +4 -0
- package/tool-docs/batch.md +85 -1
- package/tool-docs/browser.md +3 -1
- package/tool-docs/code_search.md +3 -1
- package/tool-docs/consult_start.md +6 -2
- package/tool-docs/consult_stop.md +3 -1
- package/tool-docs/delete.md +3 -0
- package/tool-docs/doc_check.md +43 -8
- package/tool-docs/doc_search.md +5 -1
- package/tool-docs/edit.md +22 -1
- package/tool-docs/execute.md +7 -1
- package/tool-docs/fetch.md +3 -1
- package/tool-docs/file_ops.md +3 -1
- package/tool-docs/git.md +9 -1
- package/tool-docs/goal.md +2 -0
- package/tool-docs/grep.md +1 -1
- package/tool-docs/hashline_edit.md +2 -0
- package/tool-docs/insert_after.md +3 -1
- package/tool-docs/ledger.md +5 -5
- package/tool-docs/lint.md +1 -0
- package/tool-docs/manifest-read.md +2 -1
- package/tool-docs/manifest.md +9 -2
- package/tool-docs/memory.md +11 -1
- package/tool-docs/notify_parent.md +1 -1
- package/tool-docs/plan.md +2 -0
- package/tool-docs/question.md +1 -1
- package/tool-docs/read_history.md +15 -3
- package/tool-docs/read_image.md +3 -1
- package/tool-docs/recent_changes.md +3 -1
- package/tool-docs/repo_outline.md +3 -1
- package/tool-docs/settings.md +10 -2
- package/tool-docs/skill.md +2 -0
- package/tool-docs/ssh.md +30 -17
- package/tool-docs/subagent.md +50 -13
- package/tool-docs/task.md +2 -0
- package/tool-docs/team_ledger.md +1 -1
- package/tool-docs/timer.md +7 -0
- package/tool-docs/verify.md +13 -1
- package/tool-docs/websearch.md +3 -1
- package/tool-docs/write.md +2 -0
- package/tools/bash.mjs +12 -2
- package/tools/edit-batch.mjs +40 -33
- package/tools/edit-diff.mjs +69 -1
- package/tools/execute.mjs +10 -4
- package/tools/file.mjs +17 -27
- package/tools/git.mjs +27 -1
- package/tools/ops.mjs +2 -1
- package/tools/patch.mjs +12 -3
- package/tools/process-doc-mark.mjs +139 -0
- package/tools/search.mjs +15 -6
- package/tools/shared.mjs +24 -1
- package/tools/ssh.mjs +48 -7
- package/tools/tree.mjs +4 -1
- package/tools/write-path.mjs +64 -3
- package/traces/trace-store.mjs +4 -3
- package/agent/light-round.mjs +0 -63
- 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
|
|
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
|
|
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
|
-
/**
|
|
107
|
-
*
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
126
|
+
return resolvePathTarget(base, raw, { kind: "file", order: "near-first", bases: declaredBases(base) })
|
|
117
127
|
}
|
|
118
128
|
|
|
119
129
|
/**
|
|
120
|
-
* **读面**解析(append / status / close 与评审门 / spawn
|
|
121
|
-
*
|
|
122
|
-
* @param {string} 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
|
|
128
|
-
|
|
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
|
|
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
|
|
151
|
-
if (abs) return abs
|
|
152
|
-
|
|
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
|
|
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
|
-
|
|
187
|
-
|
|
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
|
-
|
|
203
|
-
|
|
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
|
|
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
|
+
}
|