@thincoder/core 0.9.1 → 0.9.2
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/LICENSE +21 -0
- package/README.md +89 -0
- package/advisor/loop.mjs +2 -2
- package/advisor/run.mjs +1 -1
- package/agent/family-tools.mjs +20 -10
- package/agent/setup-reminders.mjs +67 -11
- package/agent-tools/advisor.mjs +2 -2
- package/agent-tools/batch-lifecycle.mjs +245 -0
- package/agent-tools/batch-segment.mjs +16 -263
- package/agent-tools/batch-skeleton.mjs +94 -0
- package/agent-tools/batch.mjs +397 -0
- package/agent-tools/eng.mjs +4 -0
- package/agent-tools/parent-channel.mjs +17 -0
- package/agent-tools/plan.mjs +34 -0
- package/agent-tools.mjs +5 -1
- package/generate-title.mjs +6 -6
- package/i18n.mjs +4 -4
- package/ledger-cmd.mjs +30 -7
- package/ledger-db.mjs +22 -2
- package/ledger-executors.mjs +103 -0
- package/ledger-surface.mjs +15 -8
- package/ledger.mjs +15 -4
- package/manifest.mjs +172 -60
- package/package.json +18 -1
- package/prompts/advisor-design.md +1 -1
- package/prompts/advisor-round2.md +1 -1
- package/prompts/advisor-round3.md +1 -1
- package/prompts/common.md +1 -1
- package/prompts/persona-eng-coder.md +1 -1
- package/prompts/persona-eng-designer.md +1 -1
- package/prompts/persona-engineering.md +8 -5
- package/session-gc.mjs +118 -74
- package/session-lifecycle.mjs +18 -5
- package/session-slots-manifest.mjs +36 -3
- package/session-stale.mjs +247 -0
- package/tools/git.mjs +14 -9
- package/traces/trace-cleanup.mjs +109 -0
- package/traces/trace-store.mjs +32 -36
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-tools/batch.mjs — 批次档生命周期工具(KD-4 拆分主档:action 分发 + append 迁移面 +
|
|
3
|
+
* 路径解析单源 + #84 记账缝)。
|
|
4
|
+
*
|
|
5
|
+
* 单工具 `batch`,action = create(建档,§4.11)/ append(段写入——原 batch-segment.mjs 语义
|
|
6
|
+
* 逐字迁移,§4.1 全表)/ status(段属主状态行流转,§4.12)/ close(收口冻结,§4.13)。
|
|
7
|
+
* 权威规格 = `docs/core/design/BATCH-RECORD.md` §4(契约/挂载/用例 BR-1–26/骨架 §4.10/
|
|
8
|
+
* 参数面 §4.11–§4.13/别名 §4.14);骨架与判定字面单源 = `batch-skeleton.mjs`;lifecycle 面 =
|
|
9
|
+
* `batch-lifecycle.mjs`。**依赖单向(KD-4):skeleton ← lifecycle ← 主档**。
|
|
10
|
+
*
|
|
11
|
+
* 身份判据(D-BR17/D-BR18/D-BR21):主 agent 调用 ctx 带 `depth === 0`(dispatch 装配);
|
|
12
|
+
* eng 子代理 `depth > 0`(`agent._role` 定段);评审实例 ctx 无 depth 但工具以 `review: true`
|
|
13
|
+
* 绑定。create/close 仅 depth-0 放行;depth-0 的 append/status/close 可选 `path`(D-BR21:
|
|
14
|
+
* 缺省 = findInFlightBatch 取唯一在飞批,0/复数 ⇒ throw);子代理/评审传 path ⇒ 拒(目标 =
|
|
15
|
+
* spawn/实例注入,语法上写不到别处——运行期门拒)。append 段白名单:depth-0 → §1/§4/§6 ·
|
|
16
|
+
* eng-designer → §2 · 评审 → §3 · eng-coder → §5;status 写域 = 调用者自己段(主 agent 仅 §1
|
|
17
|
+
* ——轮 2 #3 裁定②)。append 迁移面错误串**逐字保持 "batch_segment:" 前缀**(§4.1 全表锚断言);
|
|
18
|
+
* "batch:" 前缀 = 本批新增错误面(create/close/status/findInFlight/path 门/dispatch)。
|
|
19
|
+
*
|
|
20
|
+
* 导出面:`batchTool`(主名)· `batchSegmentTool`(**过渡别名**——name 仍 "batch_segment",
|
|
21
|
+
* schema 仅 {segment,text}、描述逐字,execute ⇒ append 同一执行体;撤除判据 = BATCH-RECORD
|
|
22
|
+
* §4.14)· `resolveBatchDocPath` · `batchDocBases` · `batchDocForReview` ·
|
|
23
|
+
* `configureBatchSegment` / `resetBatchSegment`(#84 记账缝——契约名不变,AC-11 零改)·
|
|
24
|
+
* `MAX_TEXT_CHARS` · `SEGMENT_BY_ROLE`(re-export——shim 导出全超集需要)。
|
|
25
|
+
*
|
|
26
|
+
* #84 记账面(CORE-UNIFICATION §2.13.4):写入成功后的记账**按端注入**——核内缺省 no-op = CLI
|
|
27
|
+
* 语义零行为变;端装配经 `configureBatchSegment({ onWrite })` 覆盖(VSC 侧现形 = `_touchedFiles`
|
|
28
|
+
* 记账)。核内零端名分支(契约 5——只认 `onWrite` 函数名)。
|
|
29
|
+
*/
|
|
30
|
+
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs"
|
|
31
|
+
import { resolve } from "node:path"
|
|
32
|
+
|
|
33
|
+
import { docRootBase, docRootPaths, readManifest } from "../manifest.mjs"
|
|
34
|
+
import { SEGMENT_BY_ROLE, readBatchStatusLine, sectionHeaderRe } from "./batch-skeleton.mjs"
|
|
35
|
+
import { closeBatchRecord, createBatchRecord, findInFlightBatch, statusBatchRecord } from "./batch-lifecycle.mjs"
|
|
36
|
+
|
|
37
|
+
export { SEGMENT_BY_ROLE }
|
|
38
|
+
|
|
39
|
+
/** text 单次上限(§2.20.1——超出引导分段追加,不承诺"不新盖戳")。append 迁移域私有区常量
|
|
40
|
+
* (随执行体自 batch-segment.mjs 迁入——骨架档只住骨架/状态行单源,lifecycle 档头枚举口径);
|
|
41
|
+
* export = shim 导出全超集(旧 `batch-segment.mjs` 导出面等价保持)。 */
|
|
42
|
+
export const MAX_TEXT_CHARS = 20000
|
|
43
|
+
/** 工具写入的轮次节标题形态(N 计数口径 = 该形态行;`### 轮次与发现(…)` 骨架行不匹配)。 */
|
|
44
|
+
const ROUND_HEADING_RE = /^### 轮次 \d+(评审子代理)/
|
|
45
|
+
/** 凭证形态(§2.7 冒号态):`[DESIGN-TOKEN:…]` 与 `designId: …`——本节自有正则。 */
|
|
46
|
+
const CRED_RE = /\[DESIGN-TOKEN:[^\]]*\]|designId\s*:\s*\S+/g
|
|
47
|
+
const CRED_TEST_RE = /\[DESIGN-TOKEN:[^\]]*\]|designId\s*:\s*\S+/
|
|
48
|
+
|
|
49
|
+
// ─── #84 记账面注入缝(「记账面按端注入」,形态参 §2.13.5 注入缝)───────────────
|
|
50
|
+
/**
|
|
51
|
+
* 写入成功后的记账注入面(**缺省 no-op** = CLI 语义零行为变):端装配层可覆盖——把写成的
|
|
52
|
+
* 批次档记入调用者写域(VSC 侧现形 = `agent._touchedFiles` 记账,供子代理合入 / recent_changes /
|
|
53
|
+
* verify / advisor 默认范围消费)。核内零端名分支(契约 5——本档只认 `onWrite` 函数名)。
|
|
54
|
+
* 形态与 `tools/write-path.mjs` 的 `configureWritePath` 同款:模块级、缺省不覆盖、null 撤销。
|
|
55
|
+
*/
|
|
56
|
+
let injectedOnWrite = null
|
|
57
|
+
export function configureBatchSegment(impl) {
|
|
58
|
+
injectedOnWrite = impl && typeof impl.onWrite === "function" ? impl.onWrite : null
|
|
59
|
+
}
|
|
60
|
+
/** 撤销注入(测试与端装配生命周期用——缺省态 = 零记账)。 */
|
|
61
|
+
export function resetBatchSegment() { injectedOnWrite = null }
|
|
62
|
+
|
|
63
|
+
/** 可读文件判据(存在且为文件——目录/缺失同判不可读;与 lifecycle 档同名 helper 同型——
|
|
64
|
+
* KD-4 单向依赖下各自持有,三行谓词不构成第二权威源)。 */
|
|
65
|
+
function readableFile(abs) {
|
|
66
|
+
try { return existsSync(abs) && statSync(abs).isFile() } catch { return false }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 批次档路径门禁(评审侧 §2.20.2 口径 = **「若传则须可读」**):空/非字符串/不可读 → throw。
|
|
71
|
+
* 非空且可读 → 返回绝对路径(`\` 归一——照 `files`/`batchDoc` spawn 门先例)。
|
|
72
|
+
* v2:cwd 不可读 → manifest `docRoot.batches` 复判(M3 模块设计 §2.1#4——N3 可迁移;
|
|
73
|
+
* 值形态 = 串 | 多根数组——逐基底按序复判,首个可读者胜)。
|
|
74
|
+
*/
|
|
75
|
+
export function resolveBatchDocPath(cwd, given) {
|
|
76
|
+
const base = cwd ?? process.cwd()
|
|
77
|
+
const raw = typeof given === "string" ? given.trim() : ""
|
|
78
|
+
if (!raw) {
|
|
79
|
+
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.")
|
|
80
|
+
}
|
|
81
|
+
const abs = resolve(base, raw.replace(/\\/g, "/"))
|
|
82
|
+
if (readableFile(abs)) return abs
|
|
83
|
+
// 双基底(M3 模块设计 §2.1#4):cwd 不可读 → manifest docRoot.batches 复判(逐基底按序,
|
|
84
|
+
// 首个可读者胜);manifest 缺失 / 非法 / 读错 → 无第二基底(v1 单基底语义零变),
|
|
85
|
+
// 全不可读 → throw。
|
|
86
|
+
let man = { ok: false }
|
|
87
|
+
try { man = readManifest(base) } catch { /* 权限等读错——按无 manifest 处理(v1 语义) */ }
|
|
88
|
+
if (man.ok) {
|
|
89
|
+
for (const root of docRootPaths(man.manifest?.docRoot?.batches, base)) {
|
|
90
|
+
const alt = resolve(root, raw.replace(/\\/g, "/"))
|
|
91
|
+
if (readableFile(alt)) return alt
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
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.`)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 批次档基底根数组(**声明面单源**——轮 2 裁定:create 相对基底 / depth-0 在飞扫描共用,
|
|
99
|
+
* 不写死 `docs/batches`):manifest `docRoot.batches`(串 | 多根数组)→ `docRootPaths`
|
|
100
|
+
* 归一去重(相对基底按项目根解析);manifest 缺失/非法/读错 → 回退默认基底 `"docs/batches"`
|
|
101
|
+
* (v1 单基底语义零变)。恒返回非空数组(create 的 bases[0] / findInFlightBatch 扫描面消费)。
|
|
102
|
+
* @param {string} cwd
|
|
103
|
+
* @returns {string[]}
|
|
104
|
+
*/
|
|
105
|
+
export function batchDocBases(cwd) {
|
|
106
|
+
const base = cwd ?? process.cwd()
|
|
107
|
+
let man = { ok: false }
|
|
108
|
+
try { man = readManifest(base) } catch { /* 读错 → 按无 manifest 处理(回退默认基底) */ }
|
|
109
|
+
const roots = man.ok ? docRootPaths(man.manifest?.docRoot?.batches, base) : []
|
|
110
|
+
return roots.length ? roots : [resolve(docRootBase(base), "docs/batches")]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 设计评审的实例绑定解析(§2.20.2/§2.20.3——评审实例的唯一路径来源):
|
|
115
|
+
* - 同步路径:调用方(advisor 工具)把实例绑定显式放进 callbacks(**带 batchDoc 键**,
|
|
116
|
+
* 未绑定即 null)——以此为准,不再回看池条目(防同步/异步混跑时串档);
|
|
117
|
+
* - 异步路径:本评审所在**池条目**的 `run.batchDoc`(文档集 = 实例键,各评审各取各条)。
|
|
118
|
+
*/
|
|
119
|
+
export function batchDocForReview(agent, documents, callbacks = null) {
|
|
120
|
+
if (callbacks && "batchDoc" in callbacks) return callbacks.batchDoc ?? null
|
|
121
|
+
const key = JSON.stringify([...(documents ?? [])].sort())
|
|
122
|
+
for (const e of agent?._asyncAdvisors?.values?.() ?? []) {
|
|
123
|
+
if (e.status === "running" && e.reviewType === "design" && JSON.stringify([...(e.documents ?? [])].sort()) === key) {
|
|
124
|
+
return e.run?.batchDoc ?? null
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return null
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ─── append 迁移面(原 batch-segment.mjs :129-184 私有区——逐字迁移,错误串逐字保序)───
|
|
131
|
+
|
|
132
|
+
/** 段号解析:`§2` / `2` / `§2 批次任务` 均可;无法解析 → null。 */
|
|
133
|
+
function segmentNumber(raw) {
|
|
134
|
+
const m = /^§?\s*(\d+)/.exec(String(raw ?? "").trim())
|
|
135
|
+
return m ? Number(m[1]) : null
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** 身份 → 可写段号;无写权身份 → null。评审实例绑定优先(评审者的身份即"设计评审"。)
|
|
139
|
+
* depth-0 的段白名单(append = §1/§4/§6)由 appendBatchRecord 分支,不经本表。 */
|
|
140
|
+
function allowedSegment(agent, review) {
|
|
141
|
+
if (review) return 3
|
|
142
|
+
return SEGMENT_BY_ROLE[agent?._role] ?? null
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 凭证剥除 + 伪造轮次标题丢弃(§2.20.1 F6/AC30/AC34)。
|
|
147
|
+
* 逐行处理:含凭证形态的行 → 剥除该子串,剥后为空则整行丢弃(其余内容逐字保留)。
|
|
148
|
+
* `dropStamp`(仅 §3 目标段):调用方自带的 `### 轮次 N(评审子代理)` 行被丢弃——
|
|
149
|
+
* 工具生成才是唯一来源戳(否则伪造戳会污染 N 计数)。
|
|
150
|
+
*/
|
|
151
|
+
function sanitizeText(text, dropStamp) {
|
|
152
|
+
const kept = []
|
|
153
|
+
for (const line of text.replace(/\r\n?/g, "\n").split("\n")) {
|
|
154
|
+
if (dropStamp && ROUND_HEADING_RE.test(line)) continue
|
|
155
|
+
if (!CRED_TEST_RE.test(line)) { kept.push(line); continue }
|
|
156
|
+
const cleaned = line.replace(CRED_RE, "").replace(/[ \t]+$/, "")
|
|
157
|
+
if (cleaned.trim()) kept.push(cleaned)
|
|
158
|
+
}
|
|
159
|
+
return kept.join("\n")
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** §3 内工具写入的轮次行计数(N = 计数 + 1;收窄口径——只数工具戳形态)。 */
|
|
163
|
+
function roundCount(src, headerMatch, endIdx) {
|
|
164
|
+
const body = src.slice(headerMatch.index + headerMatch[0].length, endIdx)
|
|
165
|
+
return body.split("\n").filter((l) => ROUND_HEADING_RE.test(l)).length
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** 段尾定位 + 插入(append-only:只在段尾插入,既有字节不变;返回新全文 + 本轮 N)。 */
|
|
169
|
+
function insertIntoSection(src, seg, text) {
|
|
170
|
+
const hdr = sectionHeaderRe(seg).exec(src)
|
|
171
|
+
if (!hdr) {
|
|
172
|
+
throw new Error(`batch_segment: the bound batch record has no "## §${seg}" section header — the six-section skeleton is written by the record's creator before any segment write. Fix: ask the parent/creator to add the "## §${seg} …" heading (with the template's sub-headings) first, then call batch_segment again. Nothing was written.`)
|
|
173
|
+
}
|
|
174
|
+
const nextRe = /^## §\d/gm
|
|
175
|
+
nextRe.lastIndex = hdr.index + hdr[0].length
|
|
176
|
+
const next = nextRe.exec(src)
|
|
177
|
+
const endIdx = next ? next.index : src.length
|
|
178
|
+
// 来源戳(仅 §3):工具生成标题 + N——调用方写不进自己的标题(sanitize 已丢弃同名行)。
|
|
179
|
+
const roundN = seg === 3 ? roundCount(src, hdr, endIdx) + 1 : 0
|
|
180
|
+
const payload = (seg === 3 ? `### 轮次 ${roundN}(评审子代理)\n\n` : "") + text
|
|
181
|
+
const before = src.slice(0, endIdx)
|
|
182
|
+
const after = src.slice(endIdx)
|
|
183
|
+
const eol = src.includes("\r\n") ? "\r\n" : "\n"
|
|
184
|
+
const lead = before === "" ? "" : before.endsWith(eol + eol) ? "" : before.endsWith(eol) ? eol : eol + eol
|
|
185
|
+
// 尾随 eol 恒加(块自成行);后面还有段标题时再补一个空行(保持模板的空行分隔)。
|
|
186
|
+
const written = before + lead + payload.replaceAll("\n", eol) + eol + (after === "" ? "" : eol) + after
|
|
187
|
+
return { written, roundN }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* append 执行体(action=append——原 batch_segment execute 迁移 + depth-0 分支):
|
|
192
|
+
* - 段白名单:depth-0 → §1/§4/§6(新错误面,"batch:" 前缀);eng-designer → §2 · 评审 → §3 ·
|
|
193
|
+
* eng-coder → §5(迁移错误面,"batch_segment:" 前缀**逐字保持**);
|
|
194
|
+
* - 目标定位(D-BR21):depth-0 path 可选(缺省 = findInFlightBatch 唯一在飞批);子代理/评审
|
|
195
|
+
* 传 path ⇒ 拒,目标 = spawn/实例绑定;
|
|
196
|
+
* - 其余(gate / text 校验 / 剥凭证 / 骨架保护 / 插入 / 记账 / 回执)逐字保持原语义。
|
|
197
|
+
*/
|
|
198
|
+
function appendBatchRecord({ args, ctx, review, batchDoc, onWritten }) {
|
|
199
|
+
const agent = ctx?.agent ?? {}
|
|
200
|
+
const cwd = agent.cwd ?? process.cwd()
|
|
201
|
+
const depth0 = ctx?.depth === 0
|
|
202
|
+
const seg = allowedSegment(agent, review)
|
|
203
|
+
if (!depth0 && seg === null) {
|
|
204
|
+
throw new Error("batch_segment: no segment is writable by this caller — the channel exists for eng-designer (§2), eng-coder (§5) and design reviews bound to a batch record (§3); the parent agent writes §1/§4/§6 through ordinary document writes.")
|
|
205
|
+
}
|
|
206
|
+
const n = segmentNumber(args?.segment)
|
|
207
|
+
if (n === null) {
|
|
208
|
+
if (depth0) {
|
|
209
|
+
throw new Error(`batch: unknown segment ${JSON.stringify(args?.segment ?? null)} — depth-0 append writes §1/§4/§6 of the in-flight record (pass the section number you write, e.g. "§4").`)
|
|
210
|
+
}
|
|
211
|
+
throw new Error(`batch_segment: unknown segment ${JSON.stringify(args?.segment ?? null)} — pass the section number you write (e.g. "§${seg}").`)
|
|
212
|
+
}
|
|
213
|
+
if (depth0) {
|
|
214
|
+
if (n !== 1 && n !== 4 && n !== 6) {
|
|
215
|
+
throw new Error(`batch: §${n} is not yours to write — depth-0 append writes §1/§4/§6 only (一段一作者: §2 = eng-designer, §3 = design review, §5 = eng-coder).`)
|
|
216
|
+
}
|
|
217
|
+
} else if (n !== seg) {
|
|
218
|
+
throw new Error(`batch_segment: §${n} is not yours to write — this caller writes §${seg} only (一段一作者: eng-designer → §2, design review → §3, eng-coder → §5).`)
|
|
219
|
+
}
|
|
220
|
+
// 目标定位(D-BR21)——path 拒面先于绑定解析(与 status 同序:段白名单 → path 门 → 取值)。
|
|
221
|
+
const givenPath = typeof args?.path === "string" && args.path.trim() ? args.path : null
|
|
222
|
+
let abs
|
|
223
|
+
if (givenPath) {
|
|
224
|
+
if (!depth0) {
|
|
225
|
+
throw new Error("batch: path is a depth-0-only parameter (D-BR21) — your target record arrives via the spawn binding / the review instance key. Nothing was written.")
|
|
226
|
+
}
|
|
227
|
+
abs = resolveBatchDocPath(cwd, givenPath)
|
|
228
|
+
} else if (depth0) {
|
|
229
|
+
abs = findInFlightBatch(cwd, batchDocBases(cwd))
|
|
230
|
+
} else {
|
|
231
|
+
if (!batchDoc) {
|
|
232
|
+
throw new Error("batch_segment: no batch record is bound to this caller — there is no path parameter by design (the target arrives via the spawn binding / the review instance key). Report the section as not written.")
|
|
233
|
+
}
|
|
234
|
+
abs = resolveBatchDocPath(cwd, batchDoc)
|
|
235
|
+
}
|
|
236
|
+
const src = readFileSync(abs, "utf8")
|
|
237
|
+
const status = readBatchStatusLine(src)
|
|
238
|
+
if (status === "closed") {
|
|
239
|
+
throw new Error("batch_segment: 已收口档不回改 — the bound batch record's §1 status line contains 「已收口」, so the record is frozen: its body is never written to again (整档冻结;改 = 新批新档). Nothing was written. Report the section as not written.")
|
|
240
|
+
}
|
|
241
|
+
if (status === "unknown") {
|
|
242
|
+
throw new Error("batch_segment: 状态行不可解析或缺失 — the bound batch record has no §1 `**状态行**:` line whose value contains 已收口 or 进行中 (fail-closed: the write is refused as if frozen). Ask the record's creator to set the §1 status line, then call again. Nothing was written.")
|
|
243
|
+
}
|
|
244
|
+
if (typeof args?.text !== "string") {
|
|
245
|
+
throw new Error("batch_segment: text must be a string (the markdown to append).")
|
|
246
|
+
}
|
|
247
|
+
if (args.text.length > MAX_TEXT_CHARS) {
|
|
248
|
+
throw new Error(`batch_segment: text is ${args.text.length} characters — the limit is ${MAX_TEXT_CHARS} per call. Split it into multiple calls: each call becomes its own section and the round number N continues (分段追加——每次调用各成节、N 顺延).`)
|
|
249
|
+
}
|
|
250
|
+
if (/^## §\d/m.test(args.text)) {
|
|
251
|
+
throw new Error("batch_segment: the text contains a section header line matching `^## §N` — that would break section location and the append-only guarantee. Rewrite it (escape the heading, or drop the leading `## §N`), then call again (骨架保护). Nothing was written.")
|
|
252
|
+
}
|
|
253
|
+
const body = sanitizeText(args.text, n === 3).replace(/^\n+/, "").replace(/\s+$/, "")
|
|
254
|
+
if (!body.trim()) {
|
|
255
|
+
throw new Error("batch_segment: nothing to append — the text is empty after credential stripping (credential values never reach the record).")
|
|
256
|
+
}
|
|
257
|
+
const { written, roundN } = insertIntoSection(src, n, body)
|
|
258
|
+
writeFileSync(abs, written)
|
|
259
|
+
// #84:记账面按端注入(缺省 no-op = CLI 语义零行为变——端装配经 configureBatchSegment 覆盖)。
|
|
260
|
+
onWritten?.(agent, abs)
|
|
261
|
+
return `batch_segment: appended ${body.length} characters to §${n} of the batch record${roundN ? ` (### 轮次 ${roundN}(评审子代理))` : ""}.`
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* 批次档生命周期工具工厂(主名 `batch`——单工具四 action)。
|
|
266
|
+
* @param {string|null} batchDoc — 绑定的目标档(spawn:`child._batchDoc`;评审:实例键;
|
|
267
|
+
* depth-0 主 agent 挂载时 = null——目标走 D-BR21 可选 path / 在飞扫描)
|
|
268
|
+
* @param {{review?: boolean}} [opts] — review=true 表示"设计评审实例"形态(写 §3 + 工具盖戳)
|
|
269
|
+
*/
|
|
270
|
+
export function batchTool(batchDoc = null, { review = false } = {}) {
|
|
271
|
+
return {
|
|
272
|
+
name: "batch",
|
|
273
|
+
description:
|
|
274
|
+
"Batch-record lifecycle tool (一段一作者 — six append-only sections, one author each). " +
|
|
275
|
+
"actions: create (depth-0 only — write a new six-section skeleton record; pass path + topic) · " +
|
|
276
|
+
"append (add text to the END of YOUR section — existing lines are never rewritten or deleted) · " +
|
|
277
|
+
"status (update YOUR section's `**状态行**:` line — the value must carry exactly ONE legal keyword of your section's STATUS_WORDS entry) · " +
|
|
278
|
+
"close (depth-0 only — freeze the record: §1 → 「已收口 <date>」, every further write is refused). " +
|
|
279
|
+
"Identity fixes what you may write (段白名单): main agent (depth 0) — append §1/§4/§6, status §1 only, create/close; eng-designer — §2; eng-coder — §5; design review (review binding) — §3. A write outside your own section is refused. " +
|
|
280
|
+
"Target record: sub-agents and design reviews have NO path parameter in practice — the record arrives via the spawn binding / the review instance key (passing path is refused); depth-0 MAY pass path (若传则须可读), omitting it picks the unique in-flight record (0 or ≥2 in flight ⇒ refuse — pass path). " +
|
|
281
|
+
"A design review's append is stamped by the tool with a `### 轮次 N(评审子代理)` heading — N is tool-counted; do not write your own heading (it would be dropped). " +
|
|
282
|
+
"Credential values are stripped mechanically before writing (never write a token or designId value). " +
|
|
283
|
+
"Failures are hard and visible (no silent fallback): if a write is refused or fails, say so in your report — “§× 未写入”.",
|
|
284
|
+
parameters: {
|
|
285
|
+
type: "object",
|
|
286
|
+
properties: {
|
|
287
|
+
action: {
|
|
288
|
+
type: "string",
|
|
289
|
+
enum: ["create", "append", "status", "close"],
|
|
290
|
+
description: "Which lifecycle step to run: create (new record) / append (your section) / status (your section's status line) / close (freeze the record).",
|
|
291
|
+
},
|
|
292
|
+
path: {
|
|
293
|
+
type: "string",
|
|
294
|
+
description: "depth-0 only (D-BR21) — the batch record to operate on (若传则须可读). Omit it to target the unique in-flight record (0 or ≥2 in flight ⇒ refuse). Sub-agents and design reviews: do NOT pass path — your target record arrives via the spawn binding / the review instance key.",
|
|
295
|
+
},
|
|
296
|
+
segment: {
|
|
297
|
+
type: "string",
|
|
298
|
+
description: "append/status only — the batch-record section you are writing (e.g. \"§4\"). Declares the section number only; your identity decides what is actually writable (main agent: append §1/§4/§6 · status §1; eng-designer §2; design review §3; eng-coder §5).",
|
|
299
|
+
},
|
|
300
|
+
text: {
|
|
301
|
+
type: "string",
|
|
302
|
+
description: "append only — the markdown to append (verbatim — findings table + VERDICT + counts for a design review §3). Limit 20000 characters per call; longer content is refused — split it into multiple calls (each call becomes its own section, N continues).",
|
|
303
|
+
},
|
|
304
|
+
value: {
|
|
305
|
+
type: "string",
|
|
306
|
+
description: "status only — the new status-line value (single line; must contain exactly one legal keyword of your section's STATUS_WORDS entry: §1 进行中/已收口 · §2 设计完成 · §3 评审完成 · §5 实施完成).",
|
|
307
|
+
},
|
|
308
|
+
topic: {
|
|
309
|
+
type: "string",
|
|
310
|
+
description: "create only — the batch subject word shared by the record header (keep the file name aligned with it).",
|
|
311
|
+
},
|
|
312
|
+
date: {
|
|
313
|
+
type: "string",
|
|
314
|
+
description: "create only — the record date (YYYY-MM-DD); defaults to today.",
|
|
315
|
+
},
|
|
316
|
+
prev: {
|
|
317
|
+
type: "string",
|
|
318
|
+
description: "create only — the predecessor pointer line (前情); defaults to 无(独立批).",
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
required: ["action"],
|
|
322
|
+
},
|
|
323
|
+
async execute(args, ctx) {
|
|
324
|
+
const agent = ctx?.agent ?? {}
|
|
325
|
+
const cwd = agent.cwd ?? process.cwd()
|
|
326
|
+
const bases = batchDocBases(cwd)
|
|
327
|
+
const onWritten = injectedOnWrite
|
|
328
|
+
/** depth-0 目标定位(D-BR21):path 传 ⇒ 可读性门;缺省 ⇒ 唯一在飞批(depth-0)/
|
|
329
|
+
* 绑定解析(子代理/评审)。status/close 经此注入 lifecycle;append 自带同款分支。 */
|
|
330
|
+
const pickTarget = (raw) => {
|
|
331
|
+
const given = typeof raw === "string" && raw.trim() ? raw : null
|
|
332
|
+
if (given) {
|
|
333
|
+
if (review || ctx?.depth !== 0) {
|
|
334
|
+
throw new Error("batch: path is a depth-0-only parameter (D-BR21) — your target record arrives via the spawn binding / the review instance key. Nothing was written.")
|
|
335
|
+
}
|
|
336
|
+
return resolveBatchDocPath(cwd, given)
|
|
337
|
+
}
|
|
338
|
+
if (!review && ctx?.depth === 0) return findInFlightBatch(cwd, bases)
|
|
339
|
+
if (batchDoc) return resolveBatchDocPath(cwd, batchDoc)
|
|
340
|
+
throw new Error("batch: no batch record is bound to this caller — the target arrives via the spawn binding / the review instance key / the depth-0 in-flight scan, and none is present. Nothing was written.")
|
|
341
|
+
}
|
|
342
|
+
switch (args?.action) {
|
|
343
|
+
case "create":
|
|
344
|
+
return createBatchRecord({ args, ctx, review, cwd, bases, onWritten })
|
|
345
|
+
case "append":
|
|
346
|
+
return appendBatchRecord({ args, ctx, review, batchDoc, onWritten })
|
|
347
|
+
case "status":
|
|
348
|
+
return statusBatchRecord({ args, ctx, review, pickTarget, onWritten })
|
|
349
|
+
case "close":
|
|
350
|
+
return closeBatchRecord({ args, ctx, review, pickTarget, onWritten })
|
|
351
|
+
default:
|
|
352
|
+
throw new Error(`batch: unknown action ${JSON.stringify(args?.action ?? null)} — pass one of create / append / status / close.`)
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* 过渡别名(BATCH-RECORD §4.14——撤除判据见该节):name 仍 "batch_segment"、schema 仅
|
|
360
|
+
* {segment, text}、描述**逐字**保持原 batch-segment.mjs 基准;execute ⇒ append(同一执行体,
|
|
361
|
+
* 行为等价——AC-2/AC-9)。生产挂载面已全切主名 `batch`(BATCH-RECORD §4.3);本工厂仅供
|
|
362
|
+
* 过渡 shim 导出面(旧测试 / VSC 侧 import 消费)与撤除判据实跑使用。
|
|
363
|
+
* @param {string|null} batchDoc — 绑定的目标档(spawn:`child._batchDoc`;评审:实例键)
|
|
364
|
+
* @param {{review?: boolean}} [opts] — review=true 表示"设计评审实例"形态(写 §3 + 工具盖戳)
|
|
365
|
+
*/
|
|
366
|
+
export function batchSegmentTool(batchDoc = null, { review = false } = {}) {
|
|
367
|
+
const tool = batchTool(batchDoc, { review })
|
|
368
|
+
const own = review ? "§3" : null
|
|
369
|
+
return {
|
|
370
|
+
name: "batch_segment",
|
|
371
|
+
description:
|
|
372
|
+
"Append your own section of the batch record (一段一作者). " +
|
|
373
|
+
"There is NO path parameter: the target record is bound to you (at spawn for eng-designer/eng-coder, per review instance for a design review) and your identity fixes the section you may write " +
|
|
374
|
+
"(eng-designer → §2, design review → §3, eng-coder → §5) — a write outside your own section is refused. " +
|
|
375
|
+
"Append-only: the text lands at the end of your section; existing lines are never rewritten or deleted. " +
|
|
376
|
+
"Credential values are stripped mechanically before writing (never write a token or designId value). " +
|
|
377
|
+
"A design review's append is stamped by the tool with a `### 轮次 N(评审子代理)` heading — N is tool-counted; do not write your own heading (it would be dropped). " +
|
|
378
|
+
"Failures are hard and visible (no silent fallback): if the write is refused or fails, say so in your report — “§× 未写入”.",
|
|
379
|
+
parameters: {
|
|
380
|
+
type: "object",
|
|
381
|
+
properties: {
|
|
382
|
+
segment: {
|
|
383
|
+
type: "string",
|
|
384
|
+
description: `The batch-record section you are writing${own ? ` — yours is ${own}` : ""}. Declares the section number only; your identity decides what is actually writable.`,
|
|
385
|
+
},
|
|
386
|
+
text: {
|
|
387
|
+
type: "string",
|
|
388
|
+
description: "The markdown to append (verbatim — findings table + VERDICT + counts for a design review §3). Limit 20000 characters per call; longer content is refused — split it into multiple calls (each call becomes its own section, N continues).",
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
required: ["segment", "text"],
|
|
392
|
+
},
|
|
393
|
+
async execute(args, ctx) {
|
|
394
|
+
return tool.execute({ action: "append", segment: args?.segment, text: args?.text }, ctx)
|
|
395
|
+
},
|
|
396
|
+
}
|
|
397
|
+
}
|
package/agent-tools/eng.mjs
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* 拒翻 ⇒ 返回原因串 + **零副作用**(模式保持 OFF)。OFF 方向与幂等 enter 零改。
|
|
16
16
|
*/
|
|
17
17
|
import { ENG_ON_REMINDER, ENG_OFF_REMINDER } from "../agent.mjs"
|
|
18
|
+
import { clearPlanMode } from "./plan.mjs"
|
|
18
19
|
import { purgeExpiredDesignTokens } from "../token-ttl.mjs"
|
|
19
20
|
import { resolveEngineeringManifest } from "../manifest.mjs"
|
|
20
21
|
|
|
@@ -83,6 +84,9 @@ export const engTool = {
|
|
|
83
84
|
}
|
|
84
85
|
ctx.agent.manifest = r.manifest
|
|
85
86
|
ctx.agent.config.agent.engineering = true
|
|
87
|
+
// FR31 ③ / KD10(翻转点①):工程模式 ⇒ planMode 恒 false——中途开模式时清残留
|
|
88
|
+
//(planMode 位 + reminder 计数 + 未注入的 plan 提示语),半状态(工程纪律 × plan 只读)不产生。
|
|
89
|
+
clearPlanMode(ctx.agent)
|
|
86
90
|
// R16 (F-R16b ②): off→on 不重评——只清过期 token(用户裁定"打开工程模式时
|
|
87
91
|
// 应该清理"),有效 token 原样保留——遍历 Map 删过期,返回文案含清理个数。
|
|
88
92
|
const cleared = purgeExpiredDesignTokens(ctx.agent)
|
|
@@ -30,6 +30,9 @@
|
|
|
30
30
|
* ≤ `UPSTREAM_MSG_MAX` 且父队列 ≤ `UPSTREAM_QUEUE_MAX`(超限 = 工具**明确报错**,不静默丢)。
|
|
31
31
|
* 射程纪律(两问自检 + 正负清单)在提示词面(§6.27.8),机制面不新增语义判定。
|
|
32
32
|
*
|
|
33
|
+
* 显示面单点(F-UC8 · AGENT-LOOP-SUBAGENT.md §6.27.12.13 ②):`upstreamAskLabelVars(carrier)`——CLI / VSC
|
|
34
|
+
* 两端共用的提示行携参(队首 ask 的 `from` + 单行归一截断的 `message`);两端各持一份 = 漂移源(D2)。
|
|
35
|
+
*
|
|
33
36
|
* 模块图:静态 import 核单点 `async-settle.mjs`(`carrierField` / `getAsyncPool` /
|
|
34
37
|
* `tombstoneOf` / `wakeAsyncWaiters`——同层既有导出,同 `async-discard.mjs:35` 先例;唤醒走
|
|
35
38
|
* 既有静态边,零新增)+ `../context.mjs`(`pushReal`)
|
|
@@ -97,6 +100,20 @@ export function upstreamWaiting(carrier) {
|
|
|
97
100
|
return Array.isArray(q) && q.some((e) => e.kind === "ask")
|
|
98
101
|
}
|
|
99
102
|
|
|
103
|
+
/** ask 提示行显示串上限(字符——模块内常量,不导出)。 */
|
|
104
|
+
const ASK_LABEL_MSG_MAX = 120
|
|
105
|
+
|
|
106
|
+
/** ask 提示行携参(显示面单点——CLI / VSC 两端同源):队首未 drain ask ⇒ `{ from, msg }`;无 ⇒ null。
|
|
107
|
+
* 选择 = 插入序首个 `kind === "ask"`(与 drain 列示序同源);`msg` = 单行归一 + 截断 `ASK_LABEL_MSG_MAX`;
|
|
108
|
+
* 载体经 `carrierField` 吸收(同 `upstreamWaiting`)。 */
|
|
109
|
+
export function upstreamAskLabelVars(carrier) {
|
|
110
|
+
const q = carrierField(carrier, "_childUpstream")
|
|
111
|
+
const e = Array.isArray(q) ? q.find((x) => x.kind === "ask") : null
|
|
112
|
+
if (!e) return null
|
|
113
|
+
const one = String(e.message ?? "").replace(/\s+/g, " ").trim()
|
|
114
|
+
return { from: String(e.from ?? "?"), msg: one.length > ASK_LABEL_MSG_MAX ? one.slice(0, ASK_LABEL_MSG_MAX - 1) + "…" : (one || "…") }
|
|
115
|
+
}
|
|
116
|
+
|
|
100
117
|
/**
|
|
101
118
|
* 子 → 父入队(单点;**同步**——零等待、零 await,非阻塞结构保证;`ask` 另同步唤一次父侧
|
|
102
119
|
* 挂起驱动〔`wakeAsyncWaiters`——无等待 / 无轮询,返回即达〕)。父侧下一回合边界由
|
package/agent-tools/plan.mjs
CHANGED
|
@@ -26,6 +26,40 @@ const PLAN_EXIT_REMINDER =
|
|
|
26
26
|
const SPARSE_INTERVAL = 2
|
|
27
27
|
const FULL_INTERVAL = 5
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* ENG-PLAN-EXCLUSION(FR31 ② · 设计 `docs/core/design/ENGINEERING-MODE-V2.md` §2.3 E7):
|
|
31
|
+
* 工程模式下 plan 面的**拒绝文案**——TUI `/plan`(`thincoder-cli/src/tui/cmd-plan.mjs`)与
|
|
32
|
+
* ACP `session/set_mode` / `session/set_config_option`(`thincoder-cli/src/acp/handlers-session.mjs`)
|
|
33
|
+
* 两面**共用一条**(D2 单源;VSC 面板面提示 = webview i18n 键 `toolbar.planDisabled`)。
|
|
34
|
+
*/
|
|
35
|
+
export const PLAN_ENGINEERING_REFUSED =
|
|
36
|
+
"plan mode is disabled in engineering mode — engineering mode already runs design-before-code (design → review → approval → implementation)."
|
|
37
|
+
|
|
38
|
+
/** ENG-PLAN-EXCLUSION(FR31 ③ · KD10):未注入 plan 提示语判据 = 三条 reminder 常量逐字组成员。 */
|
|
39
|
+
const isPlanReminder = (r) => r === PLAN_FULL_REMINDER || r === PLAN_SPARSE_REMINDER || r === PLAN_EXIT_REMINDER
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* ENG-PLAN-EXCLUSION(FR31 ③ / KD10——单点复用):工程模式 ⇒ `planMode` 恒 false。
|
|
43
|
+
* 清 `planMode` + 两 reminder 计数 + **未注入的 plan 提示语**(`_pendingReminders` 中的三条
|
|
44
|
+
* plan 提醒——否则排队的 `PLAN_EXIT_REMINDER`「Start implementing your plan …」会在模式翻转后
|
|
45
|
+
* 落地,与工程链条打架)。五个挂点复用本函数:三翻转(核 `eng` 工具 / CLI `/eng` / VSC 面板开关)
|
|
46
|
+
* + 两恢复(CLI `applySession` / VSC `applySlotSessionState`)。
|
|
47
|
+
* @param {object} agent — 会话 agent(就地改写)
|
|
48
|
+
* @returns {boolean} 是否清掉了实况(planMode 曾为 true / 有排队 plan 提示语)——调用方按需提示
|
|
49
|
+
*/
|
|
50
|
+
export function clearPlanMode(agent) {
|
|
51
|
+
if (!agent) return false
|
|
52
|
+
const queued = Array.isArray(agent._pendingReminders) ? agent._pendingReminders.filter(isPlanReminder).length : 0
|
|
53
|
+
const had = agent.planMode === true || queued > 0
|
|
54
|
+
agent.planMode = false
|
|
55
|
+
agent._planTurnsSinceReminder = 0
|
|
56
|
+
agent._planTurnsSinceSparse = 0
|
|
57
|
+
if (Array.isArray(agent._pendingReminders)) {
|
|
58
|
+
agent._pendingReminders = agent._pendingReminders.filter((r) => !isPlanReminder(r))
|
|
59
|
+
}
|
|
60
|
+
return had
|
|
61
|
+
}
|
|
62
|
+
|
|
29
63
|
/**
|
|
30
64
|
* Decide which plan-mode reminder (if any) to inject this turn.
|
|
31
65
|
* @param {object} agent — the agent object (mutated: tracks reminder state)
|
package/agent-tools.mjs
CHANGED
|
@@ -14,7 +14,11 @@ export { timerTool } from "./agent-tools/timer.mjs"
|
|
|
14
14
|
export { advisorTool } from "./agent-tools/advisor.mjs"
|
|
15
15
|
export { engTool } from "./agent-tools/eng.mjs"
|
|
16
16
|
export { readHistoryTool } from "./agent-tools/read-history.mjs"
|
|
17
|
-
export {
|
|
17
|
+
export { batchTool } from "./agent-tools/batch.mjs"
|
|
18
|
+
// 批次档生命周期工具(BATCH-RECORD §4.3 挂载表——主名 `batch` 单工具四 action:depth-0 主 agent
|
|
19
|
+
// create/close + append §1/§4/§6 + status §1(轮 2 裁定②:§4/§6 状态面走普通文档写)(D-BR18);
|
|
20
|
+
// eng 子代理 eng 分支绑定段写入;评审 §3。
|
|
21
|
+
// 过渡别名 batchSegmentTool 不入登记册(§4.14——生产挂载面全切主名;别名仅供 shim 导出面)。
|
|
18
22
|
// CORE-UNIFICATION TOOLS #83(统一登记册——VSC `agent-tools/index.mjs:15` 含 consult 家族;
|
|
19
23
|
// CLI 原把 consult 另挂 `agent/setup.mjs:173` ⇒ 归位:登记册即单一来源,装配方只读本档)。
|
|
20
24
|
export { consultStartTool, consultStopTool } from "./agent-tools/consult.mjs"
|
package/generate-title.mjs
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { proxyFetch } from "./proxy.mjs"
|
|
12
|
+
// D7(2026-09-21 块标题行对齐批——标题链单源):首条真实 user 消息谓词收核单源
|
|
13
|
+
// (`isRealUserMsg`——纯函数零依赖,无环)。
|
|
14
|
+
import { isRealUserMsg } from "./history-window.mjs"
|
|
12
15
|
|
|
13
16
|
// Test seam (_-prefix, mirrors run.mjs seams): lets the proxy-branch regression
|
|
14
17
|
// test swap the proxy fetch. The branch it exercises used to carry a dynamic
|
|
@@ -105,13 +108,10 @@ export async function ensureSessionTitle(agent) {
|
|
|
105
108
|
if (agent.title) return agent.title
|
|
106
109
|
try {
|
|
107
110
|
// TUI-OOM-ROOTCAUSE 批(SESSION.md §6.14):绑定态首条 user 消息在记录存储(段 1)——
|
|
108
|
-
// 内存窗口可能已滑过它(store.firstUserMessage
|
|
111
|
+
// 内存窗口可能已滑过它(store.firstUserMessage 首扫一次并缓存);未绑定回退内存查找
|
|
112
|
+
// (谓词单源 = `isRealUserMsg`——与 VSC 端壳同判据)。
|
|
109
113
|
let firstUser = agent._recordStore?.firstUserMessage?.() ?? null
|
|
110
|
-
if (!firstUser)
|
|
111
|
-
firstUser = (agent._fullHistory ?? agent.history).find(
|
|
112
|
-
(m) => m.role === "user" && typeof m.content === "string" && !m.content.startsWith("[System reminder:"),
|
|
113
|
-
)
|
|
114
|
-
}
|
|
114
|
+
if (!firstUser) firstUser = (agent._fullHistory ?? agent.history).find(isRealUserMsg)
|
|
115
115
|
if (firstUser) {
|
|
116
116
|
const title = await generateTitle(firstUser.content, agent.provider)
|
|
117
117
|
if (title) agent.title = title
|
package/i18n.mjs
CHANGED
|
@@ -35,10 +35,10 @@ export const CORE_MESSAGES = Object.freeze({
|
|
|
35
35
|
"digest.done": { en: "Digested ${n} background report(s) (${seconds}s)", zh: "已消化 ${n} 份后台报告(${seconds}s)" },
|
|
36
36
|
"digest.aborted": { en: "Digestion interrupted (${seconds}s)", zh: "消化中断(${seconds}s)" },
|
|
37
37
|
"digest.turnLabel": { en: "[auto-turn: digesting finished subagent reports…]", zh: "自动回合:消化已完成的子代理报告…" },
|
|
38
|
-
// M4(2026-09-20
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
"digest.
|
|
38
|
+
// M4(2026-09-20 端差·显示面消差批)+ F-UC8(2026-09-21 信号提示行批 · §6.27.12.13 ①–②):起跑标签
|
|
39
|
+
// **两档**——ask 档携参「谁 + 啥」(`${from}` / `${msg}`——核单点 `upstreamAskLabelVars`)/ digest 档(下键);
|
|
40
|
+
// manual / AUTO **同判**(`auto` 泛句无生产者 ⇒ 键退场);VSC 本地档不重复定义(单一权威容器)。
|
|
41
|
+
"digest.turnLabelAsk": { en: "[auto-turn: answering ${from}: ${msg}]", zh: "自动回合:答复 ${from}:${msg}" },
|
|
42
42
|
"digest.capAuto": { en: "[auto-turn: continuing past turn cap…]", zh: "自动回合:越过轮次上限,继续推进…" },
|
|
43
43
|
"digest.capStop": { en: "[auto-turn stopped at ${turns} turns — partial digest; finished reports stay in history]", zh: "自动回合在 ${turns} 轮处停止——部分消化;已完成的报告保留在历史中" },
|
|
44
44
|
// ── 限流 / 供应商状态行 ──
|
package/ledger-cmd.mjs
CHANGED
|
@@ -37,7 +37,7 @@ export function ledgerCount({ cwd } = {}) {
|
|
|
37
37
|
/** 路径 = 存在的**档**?(目录 / 缺失 → false——写门存在性判据用)。 */
|
|
38
38
|
const isFile = (p) => { try { return statSync(p).isFile() } catch { return false } }
|
|
39
39
|
|
|
40
|
-
/** 写门·指针存在性(设计档 §6.1 · 台账 #38 · AC-M2-
|
|
40
|
+
/** 写门·指针存在性(设计档 §6.1 · 台账 #38 · AC-M2-9):写命令落盘前判**结果行**——`status ∈
|
|
41
41
|
* {在途, 待核销}` 且 `task_book` 非空 ⇒ 文件部分(首个 `§` 前子串,trim)须经基准 `resolve(base, …)`
|
|
42
42
|
* 指向存在的档;缺文件部分(`§2` / 全空白)⇒ 拒;不在册 / 非文件 ⇒ 拒(throw,行不变)。
|
|
43
43
|
* 判位与迁移表判同层(落盘前);文案前缀 = 调用函数名(与同函数既有两条文案同款)。
|
|
@@ -67,8 +67,22 @@ export function ledgerAdd({ cwd, row }) {
|
|
|
67
67
|
} finally { db.close() }
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
/**
|
|
71
|
-
|
|
70
|
+
/** executor 目标值计算(设计档 docs/core/design/LEDGER.md §3.1——判位在 ledgerUpdate 体内:迁移表判 → 本语义 → 写门 → UPDATE)。
|
|
71
|
+
* 优先级 = patch 显式 > 自动语义(进在途 = executorSessionId / 出在途 = NULL)> 行现值兜底 > NULL。 */
|
|
72
|
+
function resolveExecutorTarget(row, to, patch, executorSessionId) {
|
|
73
|
+
if (row.status === "待设计" && to === "在途") {
|
|
74
|
+
return patch.executor ?? executorSessionId ?? row.executor ?? null
|
|
75
|
+
}
|
|
76
|
+
if (row.status === "在途" && (to === "待核销" || to === "已废弃")) {
|
|
77
|
+
return null // 出边自动语义压 patch——显式传 executor 也不复活
|
|
78
|
+
}
|
|
79
|
+
return patch.executor ?? row.executor ?? null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 更新(写命令,仅主 agent):UPDATE——状态迁移前判允许迁移表(不在表内 → 拒,行不变)。
|
|
83
|
+
* executor 目标值随同一 UPDATE 落列(LEDGER.md §3.1);executorSessionId = 调用会话(函数参数注入——K-LX1
|
|
84
|
+
* 纯函数可测;工具层动态 import getSessionId() 供值,零新静态边)。 */
|
|
85
|
+
export function ledgerUpdate({ cwd, id, patch, executorSessionId }) {
|
|
72
86
|
const db = openLedger(cwd, { create: true })
|
|
73
87
|
try {
|
|
74
88
|
const row = db.prepare("SELECT * FROM items WHERE id = ?").get(id)
|
|
@@ -79,9 +93,10 @@ export function ledgerUpdate({ cwd, id, patch }) {
|
|
|
79
93
|
}
|
|
80
94
|
const nextTaskBook = patch.task_book ?? row.task_book
|
|
81
95
|
assertTaskBookGate(cwd, "ledgerUpdate", to, nextTaskBook) // 判序:迁移表判 → 本门 → UPDATE
|
|
96
|
+
const nextExecutor = resolveExecutorTarget(row, to, patch, executorSessionId)
|
|
82
97
|
const now = nowIso()
|
|
83
|
-
db.prepare(`UPDATE items SET status = ?, title = ?, board = ?, req_doc = ?, task_book = ?, evidence = ?, trigger = ?, updated_at = ? WHERE id = ?`)
|
|
84
|
-
.run(to, patch.title ?? row.title, patch.board ?? row.board, patch.req_doc ?? row.req_doc, nextTaskBook, patch.evidence ?? row.evidence, patch.trigger ?? row.trigger, now, id)
|
|
98
|
+
db.prepare(`UPDATE items SET status = ?, title = ?, board = ?, req_doc = ?, task_book = ?, evidence = ?, trigger = ?, executor = ?, updated_at = ? WHERE id = ?`)
|
|
99
|
+
.run(to, patch.title ?? row.title, patch.board ?? row.board, patch.req_doc ?? row.req_doc, nextTaskBook, patch.evidence ?? row.evidence, patch.trigger ?? row.trigger, nextExecutor, now, id)
|
|
85
100
|
return { id }
|
|
86
101
|
} finally { db.close() }
|
|
87
102
|
}
|
|
@@ -97,7 +112,8 @@ export function ledgerClose({ cwd, id, status }) {
|
|
|
97
112
|
if (!row) throw new Error(`ledgerClose:行 ${id} 不存在`)
|
|
98
113
|
if (status === "已核销" && row.status !== "待核销") throw new Error(`ledgerClose:勾销仅限待核销(现态 ${row.status})`)
|
|
99
114
|
const now = nowIso()
|
|
100
|
-
|
|
115
|
+
// 撤回(任意态 → 已废弃——含在途直撤)同步 executor = NULL(LEDGER.md §3.1 ④);勾销路径 executor 已在离场迁移清空,零触碰
|
|
116
|
+
db.prepare("UPDATE items SET status = ?, closed_at = ?, updated_at = ?, executor = ? WHERE id = ?").run(status, now, now, status === "已废弃" ? null : row.executor, id)
|
|
101
117
|
db.exec("COMMIT")
|
|
102
118
|
return { id, status }
|
|
103
119
|
} catch (e) { db.exec("ROLLBACK"); throw e }
|
|
@@ -181,12 +197,19 @@ export const ledgerUpdateTool = {
|
|
|
181
197
|
task_book: { type: "string", description: "任务书指针(缺省 = 不变)" },
|
|
182
198
|
evidence: { type: "string", description: "证据(缺省 = 不变)" },
|
|
183
199
|
trigger: { type: "string", enum: ["归批", "条件", "认账不排期"], description: "触发(缺省 = 不变)" },
|
|
200
|
+
executor: { type: "string", description: "执行者 sessionId(可选——接手改写归属用;缺省 = 按状态迁移语义:进在途自动写本会话 / 出在途自动清空 / 其余不变)" },
|
|
184
201
|
},
|
|
185
202
|
},
|
|
186
203
|
readonly: false,
|
|
187
204
|
async execute(args, ctx) {
|
|
188
205
|
const { cwd, id, ...patch } = args
|
|
189
|
-
|
|
206
|
+
let executorSessionId
|
|
207
|
+
if (patch.executor === undefined) {
|
|
208
|
+
// 动态 import——零新静态边(K-LX1);工具层供值,核心函数保持参数注入纯函数
|
|
209
|
+
const { getSessionId } = await import("./session-slots.mjs")
|
|
210
|
+
executorSessionId = getSessionId()
|
|
211
|
+
}
|
|
212
|
+
return JSON.stringify(ledgerUpdate({ cwd: cwd ?? cwdOf(ctx), id, patch, executorSessionId }))
|
|
190
213
|
},
|
|
191
214
|
}
|
|
192
215
|
|