@thincoder/core 0.10.8 → 0.10.9
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 +6 -0
- package/agent/family-tools.mjs +2 -2
- package/agent/helpers.mjs +6 -6
- package/agent/post-turn.mjs +34 -0
- package/agent/run-stages.mjs +43 -9
- package/agent/setup.mjs +10 -2
- package/agent/turn-loop.mjs +6 -0
- package/agent-tools/audit-block.mjs +4 -2
- package/agent-tools/batch-gate.mjs +198 -0
- package/agent-tools/batch-lifecycle.mjs +216 -36
- package/agent-tools/batch-skeleton.mjs +87 -0
- package/agent-tools/batch.mjs +11 -3
- package/agent-tools/context.mjs +28 -5
- package/agent-tools/design-slots.mjs +7 -33
- package/agent-tools/doc-check.mjs +44 -6
- package/agent-tools/spawn-gates.mjs +100 -41
- package/agent-tools/ssh-session.mjs +108 -61
- package/agent-tools/ssh-transport.mjs +17 -4
- package/agent-tools/ssh-vault.mjs +291 -0
- package/agent-tools/subagent-spawn.mjs +18 -11
- package/agent-tools/subagent.mjs +13 -7
- package/agent-tools/timer.mjs +2 -2
- package/config-defaults.mjs +92 -0
- package/config-normalize.mjs +109 -0
- package/config.mjs +88 -197
- package/doc-check/anchors.mjs +54 -12
- package/doc-check/index.mjs +161 -12
- package/doc-check/version.mjs +84 -0
- package/doc-check/width.mjs +5 -4
- package/i18n.mjs +2 -0
- package/ledger-cmd.mjs +314 -61
- package/ledger-db.mjs +22 -10
- package/ledger-migrate.mjs +15 -9
- package/ledger-tools.mjs +115 -69
- package/ledger.mjs +13 -10
- package/log.mjs +2 -2
- package/manifest-schema.mjs +15 -1
- package/package.json +1 -1
- package/permission.mjs +7 -1
- package/prompts/advisor-design.md +21 -21
- package/prompts/advisor-round1.md +18 -18
- package/prompts/advisor-round2.md +22 -22
- package/prompts/advisor-round3.md +24 -24
- package/prompts/common.md +108 -93
- package/prompts/consult-base.md +8 -8
- package/prompts/discipline-engineering.md +68 -65
- package/prompts/discipline-normal.md +69 -68
- package/prompts/persona-coder.md +6 -6
- package/prompts/persona-eng-coder.md +19 -18
- package/prompts/persona-eng-designer.md +36 -32
- package/prompts/persona-engineering.md +114 -98
- package/prompts/persona-explore.md +2 -2
- package/prompts/persona-normal.md +16 -16
- package/prompts/persona-plan.md +7 -7
- package/prompts/subagent-base.md +4 -4
- package/provider/anthropic.mjs +4 -2
- package/provider/core.mjs +4 -2
- package/provider/google.mjs +5 -1
- package/provider/responses-request.mjs +3 -2
- package/session-lifecycle.mjs +6 -6
- package/team.mjs +2 -1
- package/token-window.mjs +1 -1
- package/tool-docs/batch.md +1 -1
- package/tool-docs/doc_check.md +30 -7
- package/tool-docs/fetch.md +3 -2
- package/tool-docs/ledger.md +7 -5
- package/tool-docs/ssh.md +23 -6
- package/tool-docs/subagent.md +6 -11
- package/tool-docs/websearch.md +3 -1
- package/tools/ssh.mjs +24 -11
- package/tools/web-deepseek.mjs +172 -0
- package/tools/web.mjs +29 -4
|
@@ -15,15 +15,30 @@
|
|
|
15
15
|
* (eng-designer → §2 · 评审 → §3 · eng-coder → §5 · coder → §5【普通模式分支增表】· 主 agent →
|
|
16
16
|
* 工程 §1 ∥ 普通 §1/§2/§5【取段——§4.16】——§4/§6 状态面走普通文档写);depth-0 的 status/path 面
|
|
17
17
|
* 按 D-BR21(可选 path,缺省 = 在飞批唯一时取)。
|
|
18
|
+
*
|
|
19
|
+
* 文档机检门(§3.20「结构兜底」②):`close` 与 `status` 值余核「已收口」两路写盘前跑
|
|
20
|
+
* `runBatchGate({ scope: "close" })`——`REFUSED`(K ≥ 1)∥ `ERROR` ⇒ throw(§1 状态行字节不变);
|
|
21
|
+
* `PASS` / 未跑(无声明面)⇒ 落盘 + 回执明示。门单源住 `batch-gate.mjs`(判据零改)。
|
|
22
|
+
*
|
|
23
|
+
* 链完整性门(§4.18-A):`close` ∥ `status` 写「已收口」两路在写盘前同过 `assertCloseoutChainComplete`
|
|
24
|
+
* ——§2/§3/§4/§6 各段有内容 ∥ §1 声明不适用(仅 §2/§3/§4 可声明;§6 不可声明 · §5 段号豁免);
|
|
25
|
+
* 缺段 ⇒ 点名拒(门位 = 冻结拒写之后、文档机检门之前)。重开径(§4.18-B):已收口档
|
|
26
|
+
* `status` §1 → 进行中 + `note` 非空 ⇒ 状态行改写 + §1 段尾追记行(只追加,其余行字节不变)。
|
|
27
|
+
*
|
|
28
|
+
* 收口重锚(§6.2 · C · AC-M2-30):收口有两条通路——`close` ∥ `status` 写 §1「已收口」——两路都冻结档、
|
|
29
|
+
* 都让批「收口了」,故**两路同调**单源 `reanchorOnClose`(本档下方),调点均在状态行写盘之后。
|
|
30
|
+
* 一次收口只出一份 `[ledger reanchor]` 回执段(判据 = 前态未冻结:已冻结档在两路都被冻结门早拒)。
|
|
18
31
|
*/
|
|
19
32
|
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"
|
|
20
|
-
import { dirname, resolve } from "node:path"
|
|
33
|
+
import { dirname, relative, resolve } from "node:path"
|
|
21
34
|
|
|
22
35
|
import {
|
|
23
36
|
SEGMENT_BY_ROLE, SEGMENT_BY_ROLE_NORMAL, DEPTH0_SEGMENTS, STATUS_WORDS, STATUS_LINE_RE, readBatchStatusLine, sectionHasStatusLine, sectionHeaderRe, batchSkeleton,
|
|
24
37
|
TEMPLATE_PLACEHOLDERS, findPlaceholderResidue, placeholderResidueError,
|
|
38
|
+
sectionContentState, malformedInapplicableLine, inapplicableDeclarationError,
|
|
25
39
|
} from "./batch-skeleton.mjs"
|
|
26
40
|
import { resolveBatchCreatePath } from "./batch-paths.mjs"
|
|
41
|
+
import { gateReceiptLine, gateRefusalMessage, runBatchGate } from "./batch-gate.mjs"
|
|
27
42
|
|
|
28
43
|
/** 可读文件判据(存在且为文件——目录/缺失同判不可读;与主档同名 helper 同型——KD-4 单向
|
|
29
44
|
* 依赖下各自持有,三行谓词不构成第二权威源)。 */
|
|
@@ -202,21 +217,10 @@ function updateSectionStatusLine(src, seg, value) {
|
|
|
202
217
|
return src.slice(0, hdr.index) + parts.join("") + src.slice(endIdx)
|
|
203
218
|
}
|
|
204
219
|
|
|
205
|
-
/** §6
|
|
206
|
-
*
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (!hdr) return false
|
|
210
|
-
const nextRe = /^## §\d/gm
|
|
211
|
-
nextRe.lastIndex = hdr.index + hdr[0].length
|
|
212
|
-
const next = nextRe.exec(src)
|
|
213
|
-
const body = src.slice(hdr.index + hdr[0].length, next ? next.index : src.length)
|
|
214
|
-
const nl = body.indexOf("\n") // 跳过段头行余文(`## §6 …` 的标题部分)
|
|
215
|
-
return (nl >= 0 ? body.slice(nl + 1) : "").trim() !== ""
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/** §6 空闸拒绝句(#422①——空 ⇒ 拒 + 提示先写 §6;防「append 被拒 ⇒ close 照常 ⇒ §6 永久为空」
|
|
219
|
-
* 复发——2026-09-26 dispatch-sizing 批实证)。 */
|
|
220
|
+
/** §6 空闸拒句(#422①——空 ⇒ 拒 + 提示先写 §6;防「append 被拒 ⇒ close 照常 ⇒ §6 永久为空」
|
|
221
|
+
* 复发——2026-09-26 dispatch-sizing 批实证):**§6 为唯一缺段时**拒串取本函数(§4.18-A J2——
|
|
222
|
+
* 「非空门」文案逐字零回归);判据本体 = 单源谓词 `sectionContentState(src, 6)`(`batch-skeleton.mjs`),
|
|
223
|
+
* 旧谓词函数已随判据单源退场(不留第二套)。 */
|
|
220
224
|
function emptySectionSixError() {
|
|
221
225
|
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."
|
|
222
226
|
}
|
|
@@ -233,6 +237,99 @@ function assertGateOpen(src) {
|
|
|
233
237
|
}
|
|
234
238
|
}
|
|
235
239
|
|
|
240
|
+
// ─── 链完整性门(§4.18-A——close ∥ status 写「已收口」两路收口前置;D-BR30)──────────
|
|
241
|
+
|
|
242
|
+
/** 链判段集(§2/§3/§4/§6——§5 段号豁免恒不判;§1 主 agent 自述段不设前置)。 */
|
|
243
|
+
const CHAIN_SEGMENTS = [2, 3, 4, 6]
|
|
244
|
+
|
|
245
|
+
/** 链门拒绝句(§4.18-A J1——缺段非仅 §6 时;逐字文本 = `TOOLS.md` §6.24 J1。`missing` = 点名串,
|
|
246
|
+
* 形如 `§2 / §3 / §4`——按段号升序列出)。 */
|
|
247
|
+
function closeoutChainError(missing) {
|
|
248
|
+
return "batch: 收口链未跑完(closeout chain gate)— close refuses: the record's closeout chain has not run, and closing would freeze it that way. " +
|
|
249
|
+
`Missing: ${missing}. Each named section must carry content (append it) or be declared inapplicable in §1 as ` +
|
|
250
|
+
"`> 不适用:§N —— <reason>` (§2 / §3 / §4 only). §6 must carry its verification (never declarable) and §5 is exempt (a light-channel round has no implementation section). " +
|
|
251
|
+
"Nothing was written."
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* 链完整性门(§4.18-A——**收口前置**):`close` ∥ `status` 写「已收口」两路在写盘前同过本门。
|
|
256
|
+
* 判据:§2 / §3 / §4 / §6 每段须 `sectionContentState(src, seg) !== "empty"`(有内容 ∥ 声明不适用);
|
|
257
|
+
* 不满足 ⇒ throw 并点名缺段(拒串 = J1——点名串按段号升序 `§2 / §3 / §4` 形)。**§6 为唯一缺段时
|
|
258
|
+
* 拒串 = 旧串逐字**(`emptySectionSixError()`——J2,零回归)。门位 = 既有拒因之后、文档机检门之前
|
|
259
|
+
* (§4.9 门清单第 ③ 道);门不判内容质量、不做自动修复、不判 §1 / §5。
|
|
260
|
+
* @param {string} src — 档全文(写盘前只读)
|
|
261
|
+
*/
|
|
262
|
+
function assertCloseoutChainComplete(src) {
|
|
263
|
+
// 声明形不合先行(J3——不静默当作无声明):§1 内「不适用」打头而完整形不合 ⇒ 拒
|
|
264
|
+
const badDecl = malformedInapplicableLine(src)
|
|
265
|
+
if (badDecl !== null) throw new Error(inapplicableDeclarationError(badDecl))
|
|
266
|
+
const missing = CHAIN_SEGMENTS.filter((n) => sectionContentState(src, n) === "empty")
|
|
267
|
+
if (missing.length === 0) return
|
|
268
|
+
if (missing.length === 1 && missing[0] === 6) throw new Error(emptySectionSixError()) // J2——零回归
|
|
269
|
+
throw new Error(closeoutChainError(missing.map((n) => `§${n}`).join(" / "))) // J1
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ─── 重开路径(§4.18-B——已收口档带审计的逆向流转;D-BR31)─────────────────────────
|
|
273
|
+
|
|
274
|
+
/** 重开缺理由拒绝句(§4.18-B J4——触发合取缺 note;逐字文本 = `TOOLS.md` §6.24 J4)。 */
|
|
275
|
+
function reopenReasonError() {
|
|
276
|
+
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."
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* 重开(§4.18-B——已收口档 status(进行中, note 非空)):一次写盘、两处落笔——
|
|
281
|
+
* ① §1 状态行 → `进行中(重开 <YYYY-MM-DD>)`(updateSectionStatusLine 既有单行豁免);
|
|
282
|
+
* ② §1 **段尾追加**追记行 `**重开记录**:<日期> · 原收口 <旧日期> · 理由 = <note> · 操作者 = 主 agent`
|
|
283
|
+
* (appendLineToSection——只追加;原收口 = 改写前状态行的收口日期字面,无日期字面 ⇒ `未标日期`)。
|
|
284
|
+
* 只追加判据:行数 +1、仅状态行原位替换、余行逐字节不变。EOL = §1 段头行自身行尾(CRLF 档 ⇒ CRLF)。
|
|
285
|
+
* @param {string} src — 档全文(closed 态)
|
|
286
|
+
* @param {string} note — 重开理由(调用面已验非空单行)
|
|
287
|
+
* @returns {string} 写盘用新全文
|
|
288
|
+
*/
|
|
289
|
+
function rewriteAsReopen(src, note) {
|
|
290
|
+
// 原收口 = 改写前 §1 状态行的收口日期字面(先取后改——重开后该行不再携带它)
|
|
291
|
+
const prevLine = (readBatchStatusLineRaw(src) ?? "").trim()
|
|
292
|
+
const prevDate = /已收口\s*(\d{4}-\d{2}-\d{2})/.exec(prevLine)?.[1] ?? "未标日期"
|
|
293
|
+
const date = todayLocal()
|
|
294
|
+
const withStatus = updateSectionStatusLine(src, 1, `进行中(重开 ${date})`)
|
|
295
|
+
return appendLineToSection(withStatus, 1, `**重开记录**:${date} · 原收口 ${prevDate} · 理由 = ${note} · 操作者 = 主 agent`)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** §1 状态行值原文(改写前的读取面——readBatchStatusLine 只给三态,日期字面须自己读)。 */
|
|
299
|
+
function readBatchStatusLineRaw(src) {
|
|
300
|
+
const s1 = /^## §1(?=\s|$)/m.exec(src)
|
|
301
|
+
if (!s1) return null
|
|
302
|
+
const nextRe = /^## §\d/gm
|
|
303
|
+
nextRe.lastIndex = s1.index + s1[0].length
|
|
304
|
+
const next = nextRe.exec(src)
|
|
305
|
+
const body = src.slice(s1.index + s1[0].length, next ? next.index : src.length)
|
|
306
|
+
return body.split(/\r?\n/).map((line) => STATUS_LINE_RE.exec(line)).find(Boolean)?.[1] ?? null
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* §N 段尾追加一行(§4.18-B——重开追记行载体;append-only:只追加,既有行字节零变)。
|
|
311
|
+
* 位置 = 段体尾部(下一 `## §` 段头之前 ∥ 文末);追加行 EOL = §1 段头行自身行尾
|
|
312
|
+
* (`\r\n` ⇒ CRLF ∥ 其余 ⇒ `\n`——探测窗 = 段头行整体〔含行尾〕,`/^## §N[^\r\n]*\r?\n/` 一次取形)。
|
|
313
|
+
* 段标题缺失 ⇒ throw(create 是骨架唯一权威入口——同 append 面 fail-closed)。
|
|
314
|
+
*/
|
|
315
|
+
function appendLineToSection(src, seg, line) {
|
|
316
|
+
const hdr = sectionHeaderRe(seg).exec(src)
|
|
317
|
+
if (!hdr) {
|
|
318
|
+
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.`)
|
|
319
|
+
}
|
|
320
|
+
const headerLine = /^## §\d[^\r\n]*\r?\n/m.exec(src.slice(hdr.index))
|
|
321
|
+
const eol = headerLine?.[0].endsWith("\r\n") ? "\r\n" : "\n"
|
|
322
|
+
const nextRe = /^## §\d/gm
|
|
323
|
+
nextRe.lastIndex = hdr.index + hdr[0].length
|
|
324
|
+
const next = nextRe.exec(src)
|
|
325
|
+
const endIdx = next ? next.index : src.length
|
|
326
|
+
let before = src.slice(0, endIdx)
|
|
327
|
+
const after = src.slice(endIdx)
|
|
328
|
+
// 只保证「行自成行」:段尾无换行 ⇒ 补一个段头行行尾;已有换行零补(不插空行——只追加判据 = 行数 +1)
|
|
329
|
+
if (!before.endsWith(eol)) before += eol
|
|
330
|
+
return before + line + eol + after
|
|
331
|
+
}
|
|
332
|
+
|
|
236
333
|
/** 全词表去重并集(各段项——value 命中判据与 note 零命判据共用同一口径)。 */
|
|
237
334
|
function statusVocabulary() {
|
|
238
335
|
return [...new Set(Object.values(STATUS_WORDS).flatMap((w) => Object.values(w)))]
|
|
@@ -268,13 +365,20 @@ function assertStatusValue(seg, value) {
|
|
|
268
365
|
}
|
|
269
366
|
}
|
|
270
367
|
|
|
368
|
+
/** note 单行判据(两径共用——非重开径落状态行括注、重开径落 §1 追记行,两者都是一行;
|
|
369
|
+
* 重开径不取 `assertStatusNote` 的另两条判据〔词表零命中 / 死占位〕——理由允许携词表关键字,
|
|
370
|
+
* 而追记行不是状态行形态、不参与 gate 判读(§4.18-B 理由校验)。 */
|
|
371
|
+
function assertSingleLineNote(note) {
|
|
372
|
+
if (/\r?\n/.test(note)) {
|
|
373
|
+
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.")
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
271
377
|
/** status `note` 字段校验(F11-A——落盘形态 = 状态行行内括注):单行 + **全词表零命中**
|
|
272
378
|
* (括注永不误触冻结门——§1 判定是子串包含;已收口优先误冻结防线的第二道)+ 死占位判据零命中
|
|
273
379
|
* (括注不得成为骨架占位的新驻留面)。 */
|
|
274
380
|
function assertStatusNote(note) {
|
|
275
|
-
|
|
276
|
-
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.")
|
|
277
|
-
}
|
|
381
|
+
assertSingleLineNote(note)
|
|
278
382
|
const hits = statusVocabulary().filter((w) => note.includes(w))
|
|
279
383
|
if (hits.length) {
|
|
280
384
|
throw new Error(`batch: status note must not contain any STATUS_WORDS keyword (${hits.join(" + ")}) — a parenthetical carrying a keyword would be read as gate truth (§1 substring predicate). Put the keyword in value, the prose in note. Nothing was written.`)
|
|
@@ -293,9 +397,11 @@ function assertStatusNote(note) {
|
|
|
293
397
|
* path 参数 = 仅 depth-0(D-BR21)——子代理/评审传 path ⇒ 拒(目标 = spawn/实例注入,语法上
|
|
294
398
|
* 写不到别处)。F11-C 挂点 = 写盘前(assertStatusValue 后):档头结构行含骨架死占位 ⇒ 拒
|
|
295
399
|
* (段体引用豁免;close 不拦——收口是主 agent 终态动作)。
|
|
296
|
-
*
|
|
400
|
+
* **收口第二通路**:值余核 = §1「已收口」时本 action 同样收口(链门 + 机检门 + 冻结与 close 同序)⇒
|
|
401
|
+
* 状态行写盘后同调 `reanchorOnClose`(单源——不复制 close 径逻辑),回执携同一 `[ledger reanchor]` 段。
|
|
402
|
+
* @returns {Promise<string>} 成功消息
|
|
297
403
|
*/
|
|
298
|
-
export function statusBatchRecord({ args, ctx, review, pickTarget, onWritten }) {
|
|
404
|
+
export async function statusBatchRecord({ args, ctx, review, pickTarget, onWritten }) {
|
|
299
405
|
const depth0 = isDepthZero(ctx)
|
|
300
406
|
// 模式位执行期实读(§4.16——同款先例 = task.mjs execute 门;翻转即时生效——D-BR26)。
|
|
301
407
|
// 写域:评审 → §3 · 子代理 → 身份段(普通模式分支合并 coder 增表——工程模式分支不查)·
|
|
@@ -332,44 +438,118 @@ export function statusBatchRecord({ args, ctx, review, pickTarget, onWritten })
|
|
|
332
438
|
if (/\r?\n/.test(value)) {
|
|
333
439
|
throw new Error("batch: status value must be a single line — a multi-line value would break the one-line status-line form. Nothing was written.")
|
|
334
440
|
}
|
|
335
|
-
// note(可选):散文说明的承载面——落状态行行内括注(value
|
|
441
|
+
// note(可选):散文说明的承载面——落状态行行内括注(value 内散文不再容忍);缺省 / 空串 = 无括注。
|
|
442
|
+
// **重开径例外(§4.18-B)**:值关键词 = 进行中 ∧ §1 门态 = closed ⇒ note = 重开理由(J4 缺则拒),
|
|
443
|
+
// 不进状态行括注(落 §1 追记行);`assertStatusNote` 不取——理由允许携词表关键字(常写「误标已收口」)。
|
|
336
444
|
const note = typeof args?.note === "string" ? args.note.trim() : ""
|
|
337
445
|
assertStatusValue(seg, value)
|
|
338
|
-
if (note) assertStatusNote(note)
|
|
339
|
-
const lineValue = note ? `${value}(${note})` : value
|
|
340
446
|
const abs = pickTarget(args?.path)
|
|
341
447
|
const src = readFileSync(abs, "utf8")
|
|
342
|
-
|
|
448
|
+
// 重开合取(§4.18-B):段 = §1 · depth-0(非评审)· 值关键词 = 进行中 · 门态 = closed——
|
|
449
|
+
// 先于冻结拒写判定(门②的唯一例外);note = 重开理由(J4 缺则拒)。
|
|
450
|
+
const reopen = seg === 1 && depth0 && !review && stripDecorations(value) === STATUS_WORDS[1].open && readBatchStatusLine(src) === "closed"
|
|
451
|
+
if (reopen && !note) throw new Error(reopenReasonError()) // J4——重开必须带理由
|
|
452
|
+
// 重开径理由校验(§4.18-B):单行 + 非空(非空已由 J4 判);**不取 `assertStatusNote`**——理由
|
|
453
|
+
// 允许携状态词表关键字(常写「误标已收口」:追记行不是状态行形态、不参与 gate 判读)。
|
|
454
|
+
if (reopen) assertSingleLineNote(note)
|
|
455
|
+
else if (note) assertStatusNote(note)
|
|
456
|
+
const lineValue = note && !reopen ? `${value}(${note})` : value
|
|
457
|
+
if (!reopen) assertGateOpen(src) // 冻结拒写——重开合取是唯一越过点(§4.9 门②例外;其余逐字零变)
|
|
343
458
|
// F11-C(写盘前):档头结构行死占位残留 ⇒ 拒(段体引用豁免——判据可辨性收正,`TOOLS.md` §6.20)
|
|
344
459
|
const residues = findPlaceholderResidue(src)
|
|
345
460
|
if (residues.length) throw new Error(placeholderResidueError(residues))
|
|
461
|
+
if (reopen) {
|
|
462
|
+
// 重开(§4.18-B):一次写盘、两处落笔(状态行改写 + §1 段尾追记行)——不跑链门 / 机检门
|
|
463
|
+
//(值非「已收口」;写后门态 = open,后续 append/status 恢复放行)。
|
|
464
|
+
writeFileSync(abs, rewriteAsReopen(src, note))
|
|
465
|
+
onWritten?.(ctx?.agent ?? {}, abs)
|
|
466
|
+
return `batch: §1 reopened — status line → 「进行中(重开 ${todayLocal()})」 + reopen record appended to §1 (reason lands verbatim in the record line). Append/status are writable again (BR-64); the closeout chain gate still applies at the next close.`
|
|
467
|
+
}
|
|
468
|
+
// ③ 链完整性门(§4.18-A——门位 = 冻结拒写之后、文档机检门之前):链未跑完先报结构缺口,不先跑机检
|
|
469
|
+
// 收口判据(本笔把 §1 写成「已收口」)单点——链门 ∥ 机检门 ∥ 重锚合取三处共消费,段号守卫只写这一次
|
|
470
|
+
// (`STATUS_WORDS[1].closed` 只属 §1 项,`assertStatusValue` 亦拒他段该值;显式 `seg === 1` 让不变量不靠巧合)。
|
|
471
|
+
const closingValue = seg === 1 && stripDecorations(value) === STATUS_WORDS[1].closed
|
|
472
|
+
if (closingValue) assertCloseoutChainComplete(src)
|
|
473
|
+
// ④ 文档机检门(§3.20「结构兜底」②——门位 = 链门之后、状态行改写之前):值余核 =「已收口」时同门;
|
|
474
|
+
// REFUSED(K ≥ 1)∥ ERROR ⇒ throw(§1 状态行字节不变);其余值零挂(回执逐字旧形)。
|
|
475
|
+
const gate = closingValue ? runBatchGate({ abs, cwd: ctx?.agent?.cwd ?? process.cwd(), agent: ctx?.agent ?? {}, scope: "close" }) : null
|
|
476
|
+
if (gate && (gate.state === "refused" || gate.state === "error")) throw new Error(gateRefusalMessage(gate, "status"))
|
|
346
477
|
const written = updateSectionStatusLine(src, seg, lineValue)
|
|
347
478
|
writeFileSync(abs, written)
|
|
348
479
|
onWritten?.(ctx?.agent ?? {}, abs)
|
|
349
|
-
|
|
480
|
+
// 收口重锚(§6.2 · C · AC-M2-30):值余核 = §1「已收口」⇒ 本 action 就是收口(上两道门与 close 同序、
|
|
481
|
+
// 同样冻结档)⇒ 同调单源 `reanchorOnClose`(不复制 close 径逻辑);次序位 = 状态行写盘之后(与 close 径一致)
|
|
482
|
+
// ——本笔被任一拒因拒 ⇒ 上面各门早 throw,台账零写(被拒的收口没有收口)。
|
|
483
|
+
// **一次收口只出一份清单**:触发 = `closingValue` ∧ 前态未冻结;`batch` 一次调用只分派一个 action
|
|
484
|
+
// (close ∥ status 不会同拍)⇒ 同一拍至多一次重锚、至多一份 `[ledger reanchor]`。前一合取项 =
|
|
485
|
+
// **defensive-only**:前态 closed 已被冻结门(`assertGateOpen`,本函数上方)早拒,正常路径跑不到这里。
|
|
486
|
+
const reanchor = closingValue && readBatchStatusLine(src) !== "closed" ? await reanchorOnClose(abs) : null
|
|
487
|
+
return `batch: §${seg} status line updated to ${JSON.stringify(lineValue)} — the freeze gate (§1) is a read-only domain for non-§1 writes (frozen truth stays = the §1 line).${gateReceiptLine(gate)}${reanchorReceiptLine(reanchor)}`
|
|
350
488
|
}
|
|
351
489
|
|
|
352
490
|
/**
|
|
353
491
|
* close——收口冻结(§4.13,仅 depth-0)。§1 状态行 →「已收口 <YYYY-MM-DD>」;此后该档
|
|
354
|
-
* append/status 全拒(冻结判据唯一真值 = §1 行——§4.9
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
492
|
+
* append/status 全拒(冻结判据唯一真值 = §1 行——§4.9 零语义变;唯一例外 = 重开合取 §4.18-B)。
|
|
493
|
+
* 对已收口档再 close ⇒ 拒(close 本身是对冻结档的写)。不代写 §6 内容、不替代台账核销事务。
|
|
494
|
+
* 前提规则(#422 + 本批 A 链门 §4.18-A):① 链完整性门(收口前置——§2/§3/§4/§6 各段有内容 ∥ 声明
|
|
495
|
+
* 不适用;§6 唯一缺段时拒串 = 旧 §6 串逐字零回归);② 拒句含「失败 ⇒ 勿 close」(append/status 面
|
|
496
|
+
* ——见 batch.mjs 与上方 assertGateOpen);③ 无状态行档 ⇒ 自建机读位(updateSectionStatusLine
|
|
497
|
+
* 缺失即插——同路收口;「有行但不可解析」仍 fail-closed)。
|
|
498
|
+
* @returns {Promise<string>} 成功消息
|
|
360
499
|
*/
|
|
361
|
-
export function closeBatchRecord({ args, ctx, review, pickTarget, onWritten }) {
|
|
500
|
+
export async function closeBatchRecord({ args, ctx, review, pickTarget, onWritten }) {
|
|
362
501
|
assertMainAgentOnly(ctx, "close", review)
|
|
363
502
|
const abs = pickTarget(args?.path)
|
|
364
503
|
const src = readFileSync(abs, "utf8")
|
|
365
504
|
// #422③:§1 无状态行档 ⇒ 自建机读位(下行走 updateSectionStatusLine 缺失即插——同路收口);
|
|
366
505
|
// 「有行但不可解析」维持 fail-closed(assertGateOpen 拒——防误冻结)。
|
|
367
506
|
if (readBatchStatusLine(src) !== "open" && sectionHasStatusLine(src, 1)) assertGateOpen(src)
|
|
368
|
-
//
|
|
369
|
-
|
|
507
|
+
// 链完整性门(§4.18-A——门位 = 既有拒因之后、文档机检门之前):§2/§3/§4/§6 各段有内容 ∥ 声明
|
|
508
|
+
// 不适用;缺段 ⇒ 拒并点名(§6 唯一缺段 = J2 旧串逐字)。含声明形不合判(J3——不静默当作无声明)。
|
|
509
|
+
assertCloseoutChainComplete(src)
|
|
510
|
+
// 文档机检门(§3.20「结构兜底」②——门位 = 链门之后、状态行改写之前):REFUSED(K ≥ 1)∥ ERROR
|
|
511
|
+
// ⇒ throw(fail-closed,不落「已收口」);PASS / 未跑(无声明面)⇒ 落盘 + 回执明示。
|
|
512
|
+
const gate = runBatchGate({ abs, cwd: ctx?.agent?.cwd ?? process.cwd(), agent: ctx?.agent ?? {}, scope: "close" })
|
|
513
|
+
if (gate.state === "refused" || gate.state === "error") throw new Error(gateRefusalMessage(gate, "close"))
|
|
370
514
|
const date = todayLocal()
|
|
371
515
|
const written = updateSectionStatusLine(src, 1, `已收口 ${date}`)
|
|
372
516
|
writeFileSync(abs, written)
|
|
373
517
|
onWritten?.(ctx?.agent ?? {}, abs)
|
|
374
|
-
|
|
518
|
+
// 收口重锚(§6.2 · C · AC-M2-30):顺序位 = 全部拒因通过、§1 状态行写盘**之后**(上两行)——close 被拒
|
|
519
|
+
//(身份 ∥ 冻结 ∥ 链完整性门 ∥ 文档机检门)⇒ 台账零写(被拒的 close 没有收口,「原批已收口」那时是假事实)。
|
|
520
|
+
const reanchor = await reanchorOnClose(abs)
|
|
521
|
+
return `batch: §1 status line → 「已收口 ${date}」 — the record is frozen: append is refused from now on, and status is refused except the reopen conjunction (§1 → 进行中 with a note) (整档冻结;改 = 新批新档; §6 内容与台账核销仍走既有通道).${gateReceiptLine(gate)}${reanchorReceiptLine(reanchor)}`
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** 收口重锚(设计档 §6.2 · C)——收口两条通路(`close` ∥ `status` 写 §1「已收口」)共调本函数(同一单源,
|
|
525
|
+
* 不复制逻辑);写入单源 = `ledgerReanchorBatch`(本档只供 `cwd` 基准与回执形;工具层零 SQL)。
|
|
526
|
+
* `cwd` = **批档所属项目根** = `resolveLedgerRoot(dirname(<batchDoc>))?.root`(§6.1 口径 2 同一子表达式)——
|
|
527
|
+
* **不用会话锚**:会话锚为工作区 ∥ 容器时可能收到别的项目根 ⇒ 选行零命中 ⇒ 唯一的机器写静默失效。
|
|
528
|
+
* 无根 ⇒ 传 `null`(核心不取行、不重锚,`failed` 点名——不落兜底基准)。
|
|
529
|
+
* `batch` = 项目根相对路径(复启句的 `<批名>` 即其文件部分;绝对路径同样可解析,但句内会带整条绝对路径)。
|
|
530
|
+
* 失败**不抛**(台账忙 ∥ 无根均不否决收口——档面写与台账库分属两存储,不做分布式事务)。 */
|
|
531
|
+
async function reanchorOnClose(abs) {
|
|
532
|
+
try {
|
|
533
|
+
const { ledgerReanchorBatch, resolveLedgerRoot } = await import("../ledger.mjs") // 动态 import:ledger-db 静态引 node:sqlite(W8 契约②)
|
|
534
|
+
const root = resolveLedgerRoot(dirname(abs))?.root ?? null
|
|
535
|
+
// 句内路径统一正斜杠(与 `task_book` 列值同形——`<批名>` 是给人读的事实句,不是平台路径)
|
|
536
|
+
return ledgerReanchorBatch({ cwd: root, batch: root ? relative(root, abs).replace(/\\/g, "/") : abs })
|
|
537
|
+
} catch (e) {
|
|
538
|
+
// 非预期异常(模块加载失败 ∥ 根推导内读声明抛)同样不得否决收口——降为同一返回形(台账零写)
|
|
539
|
+
return { batch: abs, reanchored: [], alreadyAnchored: [], failed: String(e?.message ?? e) }
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/** 重锚回执段(回执单源 = `ledgerReanchorBatch` 返回形——本档不另调选行再拼一次):无未落行 ⇒ 无该段;
|
|
544
|
+
* 失败 ⇒ 点名失败因(台账零写、冻结照常);非空不覆盖形(复启句入 `evidence`)逐行点名。 */
|
|
545
|
+
function reanchorReceiptLine(result) {
|
|
546
|
+
if (!result) return ""
|
|
547
|
+
if (result.failed) return `\n[ledger reanchor] 未重锚:${result.failed}——台账零写;记录冻结照常完成`
|
|
548
|
+
const line = (r) => `#${r.id} ${r.status} 「${r.title}」`
|
|
549
|
+
const anchored = result.reanchored.map((r) => `${line(r)}→ 条件${r.sentenceTo === "evidence" ? "(resume_when 现值非空——原值保留,复启句追加 evidence)" : ""}`)
|
|
550
|
+
const skipped = result.alreadyAnchored.map((r) => line(r))
|
|
551
|
+
return [
|
|
552
|
+
anchored.length ? `\n[ledger reanchor] 本批未落行 → 条件 ${anchored.length} 行:${anchored.join(";")}` : "",
|
|
553
|
+
skipped.length ? `\n[ledger reanchor] 已是 条件(幂等跳过,零写)${skipped.length} 行:${skipped.join(";")}` : "",
|
|
554
|
+
].join("")
|
|
375
555
|
}
|
|
@@ -162,6 +162,93 @@ export function readSectionStatusWord(src, seg) {
|
|
|
162
162
|
return null
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
/**
|
|
166
|
+
* 「不适用」声明行形态单源(§4.18-A——**一行一档**):`> 不适用:§N —— <理由>`,§N ∈ {2,3,4}、
|
|
167
|
+
* 理由非空(`\S.*` = 至少一个非空白字符起头)。识别域 = §1 段体内(声明住 §1——D-BR30:工程模式
|
|
168
|
+
* 主 agent 的 append 域 = §1/§4/§6,写不进 §2/§3,而声明恰发生在 designer / 评审本轮不跑时 ⇒
|
|
169
|
+
* 段内声明形在需要的场合不可写)。声明 = 链门(`sectionContentState`)的合法满足面。
|
|
170
|
+
*/
|
|
171
|
+
const INAPPLICABLE_RE = /^>\s*不适用[::]\s*§(\d+)\s*——\s*(\S.*)$/
|
|
172
|
+
/** 可声明的段号集(§5 段号豁免——免声明;§6 不可声明——收口段必须带验证内容)。 */
|
|
173
|
+
export const INAPPLICABLE_SEGMENTS = [2, 3, 4]
|
|
174
|
+
/** §1 声明行的识别前缀(形不合扫描的宽网——行首命中它而完整形不合 ⇒ 拒,不静默当作无声明)。 */
|
|
175
|
+
const INAPPLICABLE_PREFIX_RE = /^>\s*不适用/
|
|
176
|
+
|
|
177
|
+
/** §N 段体行集(段头缺失 ⇒ null——create 是骨架唯一权威入口;行切分 = /\r?\n/——EOL 形态无关)。 */
|
|
178
|
+
function sectionBodyLines(src, seg) {
|
|
179
|
+
const hdr = sectionHeaderRe(seg).exec(src)
|
|
180
|
+
if (!hdr) return null
|
|
181
|
+
const nextRe = /^## §\d/gm
|
|
182
|
+
nextRe.lastIndex = hdr.index + hdr[0].length
|
|
183
|
+
const next = nextRe.exec(src)
|
|
184
|
+
const body = src.slice(hdr.index + hdr[0].length, next ? next.index : src.length)
|
|
185
|
+
const nl = body.indexOf("\n") // 跳过段头行余文(`## §N …` 的标题部分)
|
|
186
|
+
return (nl >= 0 ? body.slice(nl + 1) : "").split(/\r?\n/)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 段内容判据**单源**(§4.18-A closeout chain gate 的判空口径;D-BR30 判据单源——**不许两套**;
|
|
191
|
+
* §4.13 的 §6 旧判据 `sectionSixHasContent` 改由本谓词承担——§6 为唯一缺段时拒串仍取旧函数)。
|
|
192
|
+
* 返回三态:`content`(段体至少一行非排除类)∥ `not-applicable`(§1 声明该段不适用且理由非空——
|
|
193
|
+
* 仅 §2/§3/§4)∥ `empty`。排除类(判空依据——「标题在、正文零行」判空):空行 ∥ 标题行
|
|
194
|
+
* (`^#{1,6}\s`)∥ `**状态行**:` 行 ∥ `<§N 模板占位:…>` 行 ∥ 「不适用」声明行。
|
|
195
|
+
* 口径收正注:旧 `sectionSixHasContent` 对「只有子标题(`### …`)」判非空;本谓词按 §4.18-A 判空
|
|
196
|
+
* (子标题属排除类标题行)——设计档 §4.18-A 明文收窄。
|
|
197
|
+
* @param {string} src — 档全文
|
|
198
|
+
* @param {number|string} seg — 段号(§1–§6;§1 恒 `content`——主 agent 自述段不设前置)
|
|
199
|
+
* @returns {"content"|"not-applicable"|"empty"} 段内容三态
|
|
200
|
+
*/
|
|
201
|
+
export function sectionContentState(src, seg) {
|
|
202
|
+
const n = Number(seg)
|
|
203
|
+
if (n === 1) return "content"
|
|
204
|
+
const lines = sectionBodyLines(src, n)
|
|
205
|
+
if (lines === null) return "empty" // 段头缺失 = 空(骨架由 create 一次预写;缺失即无内容)
|
|
206
|
+
if (lines.some((l) => NONEMPTY_RE.test(l) && !EXCLUDED_LINE_RE.test(l))) return "content"
|
|
207
|
+
// 段体无实质内容 ⇒ 查 §1 声明(仅 §2/§3/§4 可声明;理由非空——INAPPLICABLE_RE 的 (\S.*) 已保证)
|
|
208
|
+
if (INAPPLICABLE_SEGMENTS.includes(n)) {
|
|
209
|
+
for (const l of sectionBodyLines(src, 1) ?? []) {
|
|
210
|
+
const m = INAPPLICABLE_RE.exec(l)
|
|
211
|
+
if (m && Number(m[1]) === n) return "not-applicable"
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return "empty"
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** 非空白行谓词(空行排除)。 */
|
|
218
|
+
const NONEMPTY_RE = /\S/
|
|
219
|
+
/** 排除类行判据:标题行(`^#{1,6}\s`)∥ `**状态行**:` 行 ∥ `<§N 模板占位:…>` 行 ∥ 「不适用」声明行
|
|
220
|
+
* (空行由 NONEMPTY_RE 单独排除——四类合一次判定)。 */
|
|
221
|
+
const EXCLUDED_LINE_RE = /^#{1,6}\s|^\s*\*\*状态行\*\*[::]|^<§\d+ 模板占位[::]|^>\s*不适用/
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* 声明形不合拒绝句(§4.18-A J3——形不合 ⇒ 拒,**不静默当作无声明**):行首命中
|
|
225
|
+
* `^>\s*不适用` 却不满足完整形(缺 `——` ∥ 缺理由 ∥ § 号 ∉ {2,3,4})。
|
|
226
|
+
* 逐字文本 = `TOOLS.md` §6.24 J3。
|
|
227
|
+
* @param {string} line — 违规行原文
|
|
228
|
+
* @returns {string} 完整拒绝句(调用方 throw new Error(...))
|
|
229
|
+
*/
|
|
230
|
+
export function inapplicableDeclarationError(line) {
|
|
231
|
+
return "batch: 「不适用」声明形不合(malformed inapplicability declaration)— one line per section: " +
|
|
232
|
+
"`> 不适用:§N —— <reason>` with a non-empty reason, §N ∈ {2,3,4} (§5 needs no declaration; §6 is never declarable). " +
|
|
233
|
+
`Offending line: ${line}. Nothing was written.`
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* §1 声明行机械扫描(链门前置判据——形不合 ⇒ 拒,不静默):识别域 = §1 段体。行首命中
|
|
238
|
+
* `^>\s*不适用` 而完整形不合(INAPPLICABLE_RE 不中 ∥ § 号 ∉ {2,3,4})⇒ 返回违规行原文;
|
|
239
|
+
* § 号 ∈ {2,3,4} 且理由非空 ⇒ 合法。无违规 ⇒ null。
|
|
240
|
+
* @param {string} src — 档全文
|
|
241
|
+
* @returns {string|null} 首个形不合声明行原文 / null
|
|
242
|
+
*/
|
|
243
|
+
export function malformedInapplicableLine(src) {
|
|
244
|
+
for (const l of sectionBodyLines(src, 1) ?? []) {
|
|
245
|
+
if (!INAPPLICABLE_PREFIX_RE.test(l)) continue
|
|
246
|
+
const m = INAPPLICABLE_RE.exec(l)
|
|
247
|
+
if (!m || !INAPPLICABLE_SEGMENTS.includes(Number(m[1]))) return l
|
|
248
|
+
}
|
|
249
|
+
return null
|
|
250
|
+
}
|
|
251
|
+
|
|
165
252
|
/**
|
|
166
253
|
* §N 段内「`**状态行**:` 行存在性」判据(#422③——close 对无状态行档自建机读位的**采集面**:
|
|
167
254
|
* 与 readBatchStatusLine 的「值可解析」正交——无行 ⇒ 可自建;有行而值不可解析 ⇒ 仍 fail-closed)。
|
package/agent-tools/batch.mjs
CHANGED
|
@@ -32,6 +32,7 @@ import { readFileSync, writeFileSync } from "node:fs"
|
|
|
32
32
|
import { DESC } from "../tools/shared.mjs"
|
|
33
33
|
import { SEGMENT_BY_ROLE, SEGMENT_BY_ROLE_NORMAL, DEPTH0_SEGMENTS, readBatchStatusLine, sectionHeaderRe, findPlaceholderResidue, placeholderResidueError } from "./batch-skeleton.mjs"
|
|
34
34
|
import { batchDocBases, resolveBatchDocPath } from "./batch-paths.mjs"
|
|
35
|
+
import { gateReceiptLine, gateRefusalMessage, runBatchGate } from "./batch-gate.mjs"
|
|
35
36
|
import { closeBatchRecord, createBatchRecord, findInFlightBatch, statusBatchRecord } from "./batch-lifecycle.mjs"
|
|
36
37
|
|
|
37
38
|
export { SEGMENT_BY_ROLE, batchDocBases, resolveBatchDocPath }
|
|
@@ -154,7 +155,8 @@ function insertIntoSection(src, seg, text) {
|
|
|
154
155
|
* eng-coder → §5 · coder(普通模式分支增表)→ §5(迁移错误面,"batch_segment:" 前缀**逐字保持**);
|
|
155
156
|
* - 目标定位(D-BR21):depth-0 path 可选(缺省 = findInFlightBatch 唯一在飞批);子代理/评审
|
|
156
157
|
* 传 path ⇒ 拒,目标 = spawn/实例绑定;
|
|
157
|
-
* - 其余(gate / text 校验 / 剥凭证 / 骨架保护 / **死占位机检(F11-C)** /
|
|
158
|
+
* - 其余(gate / text 校验 / 剥凭证 / 骨架保护 / **死占位机检(F11-C)** / **文档机检门(§3.20 结构兜底 ①——
|
|
159
|
+
* 子代理写 §2 / §5:REFUSED ⇒ 拒写,ERROR ⇒ 写 + 回执明示)** / 插入 / 记账 / 回执)逐字保持原语义。
|
|
158
160
|
*/
|
|
159
161
|
function appendBatchRecord({ args, ctx, review, batchDoc, onWritten }) {
|
|
160
162
|
const agent = ctx?.agent ?? {}
|
|
@@ -228,11 +230,17 @@ function appendBatchRecord({ args, ctx, review, batchDoc, onWritten }) {
|
|
|
228
230
|
// (段体引用豁免——判据可辨性收正,`TOOLS.md` §6.20;骨架枚举单源 = skeleton 档 TEMPLATE_PLACEHOLDERS)。
|
|
229
231
|
const residues = findPlaceholderResidue(src)
|
|
230
232
|
if (residues.length) throw new Error(placeholderResidueError(residues))
|
|
233
|
+
// 文档机检门(§3.20「结构兜底」①——门位 = 既有拒因全部之后、insertIntoSection / 写盘之前):
|
|
234
|
+
// 只挂子代理写 §2 / §5(depth-0 与评审不检);REFUSED ⇒ throw;PASS ⇒ 照写;ERROR / 门自身异常 ⇒ 写 + 回执明示。
|
|
235
|
+
const gate = !depth0 && !review && (n === 2 || n === 5)
|
|
236
|
+
? runBatchGate({ abs, cwd, agent, scope: "writer" })
|
|
237
|
+
: null
|
|
238
|
+
if (gate?.state === "refused") throw new Error(gateRefusalMessage(gate, "append"))
|
|
231
239
|
const { written, roundN } = insertIntoSection(src, n, body)
|
|
232
240
|
writeFileSync(abs, written)
|
|
233
241
|
// #84:记账面按端注入(缺省 no-op = CLI 语义零行为变——端装配经 configureBatchSegment 覆盖)。
|
|
234
242
|
onWritten?.(agent, abs)
|
|
235
|
-
return `batch_segment: appended ${body.length} characters to §${n} of the batch record${roundN ? ` (### 轮次 ${roundN}(评审子代理))` : ""}
|
|
243
|
+
return `batch_segment: appended ${body.length} characters to §${n} of the batch record${roundN ? ` (### 轮次 ${roundN}(评审子代理))` : ""}.${gateReceiptLine(gate)}`
|
|
236
244
|
}
|
|
237
245
|
|
|
238
246
|
/**
|
|
@@ -271,7 +279,7 @@ export function batchTool(batchDoc = null, { review = false } = {}) {
|
|
|
271
279
|
},
|
|
272
280
|
note: {
|
|
273
281
|
type: "string",
|
|
274
|
-
description: "status only — prose that lands as a parenthetical on the status line (`**状态行**:进行中(note)`). Single line; must carry no STATUS_WORDS keyword (those go in `value`) and no skeleton placeholders. Omit for a plain value.",
|
|
282
|
+
description: "status only — prose that lands as a parenthetical on the status line (`**状态行**:进行中(note)`). Single line; must carry no STATUS_WORDS keyword (those go in `value`) and no skeleton placeholders. Omit for a plain value. On the reopen path (a frozen record, §1 → 进行中) `note` is REQUIRED and lands verbatim in the §1 reopen record instead of the status-line parenthetical (it may name the frozen keyword there).",
|
|
275
283
|
},
|
|
276
284
|
topic: {
|
|
277
285
|
type: "string",
|
package/agent-tools/context.mjs
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* 单一工具三操作(用户定形 F-CC5):`stats`(感知 F-CC1)· `prune`(噪声清理 F-CC3)· `compact`(主动压缩 F-CC2,携 focus + 自动附任务/目标 + 可回查锚——裁令 A/D)。
|
|
4
4
|
* 描述外置(tool-docs/context.md——#15 统一面:文本单点 = tool-docs,DESC 加载);装配面 = 登记册 `agent-tools.mjs`(#83)+ `agent/family-tools.mjs` 的 depth-0 段(`depthOnly`)——两端自动同表;**depth>0 不给**(§6.16.6)。
|
|
5
5
|
* 本档同时是**注记文案 / 轻推行 / 回执文案的单源**(核与 VSC 的 run-stages 消费点经动态 import 取用——零新增静态边)。
|
|
6
|
-
* 机械面:`stats`
|
|
6
|
+
* 机械面:`stats` 不新增第三口径(首行主口径 = 窗口占用 total / window,压缩线以具名数值 + 距线呈现;第 4 行走 `historyPercent` 与状态行同式);`prune` = 合格集(`token-window.mjs` 单源)+「原位换内容」应用面(`context.mjs`);`compact` **当次零执行**(只登记 `agent._pendingCompact`,由下一安全点消费——§6.16.2)。
|
|
7
7
|
*/
|
|
8
8
|
import { contextUsage, estimateTokens, collectStaleToolOutputs, historyPercent, PRUNE_MIN_TOKENS } from "../token-window.mjs"
|
|
9
|
+
import { requestBudget } from "../config.mjs"
|
|
9
10
|
import { pruneStaleToolOutputs, COMPRESS_FAILURE_LIMIT } from "../context.mjs"
|
|
10
11
|
import { slotPath } from "../session-slots.mjs"
|
|
11
12
|
// #15 描述外置:描述文本单点 = tool-docs/context.md(DESC 单解析面,缺档抛错语义不变)
|
|
@@ -72,16 +73,38 @@ function statsReport(agent) {
|
|
|
72
73
|
toolCount++
|
|
73
74
|
toolOut += estimateTokens([m])
|
|
74
75
|
}
|
|
75
|
-
const pct = Math.round((usage.total / usage.
|
|
76
|
+
const pct = Math.round((usage.total / usage.window) * 100) // 主口径 = 窗口占用(§6.16.4 首行定稿 / D-CC29)
|
|
76
77
|
const toGo = Math.max(0, usage.threshold - usage.total) // 已越阈值 ⇒ 0(自动压缩将在下一安全点接手)
|
|
77
78
|
const info = agent?._lastCompressInfo
|
|
78
79
|
const compaction = !info ? "none"
|
|
79
80
|
: info.mode === "summary" ? `last summary freed ${info.tokensFreed} tokens`
|
|
80
81
|
: "last fallback truncation"
|
|
82
|
+
// Cache line (batch 2026-10-18-context-cache-visibility): the provider reports
|
|
83
|
+
// prompt_cache_hit/miss per response; the core now tallies them so the question
|
|
84
|
+
// "is the prefix cache actually helping?" is answered by a reading, not a guess.
|
|
85
|
+
const cacheHit = agent?._cacheHitTokens ?? 0
|
|
86
|
+
const cacheMiss = agent?._cacheMissTokens ?? 0
|
|
87
|
+
const cacheTurns = agent?._usageTurns ?? 0
|
|
88
|
+
const cacheTotal = cacheHit + cacheMiss
|
|
89
|
+
const cacheLine = cacheTurns === 0
|
|
90
|
+
? "Cache: no provider usage readings yet (hit/miss arrive with the first response)"
|
|
91
|
+
: `Cache: hit ${cacheHit} · miss ${cacheMiss} tokens (${Math.round((cacheHit / cacheTotal) * 100)}% hit) across ${cacheTurns} response(s)`
|
|
81
92
|
return [
|
|
82
|
-
`Context ≈${usage.total} tokens
|
|
93
|
+
`Context ≈${usage.total} tokens (${pct}% of ${usage.window} window); compaction line ${usage.threshold} (${toGo} to the line)`,
|
|
94
|
+
// §6.16.4 字面单源(该行为唯一字面,逐字;预算面同源 requestBudget——不自算第二套):
|
|
95
|
+
// window {window} ({spec|provider-override}) · max_tokens {maxTokens|not sent} ({explicit|spec|absent})
|
|
96
|
+
// · prompt {total} · to compaction line {threshold − total} · to window limit {limit − total}
|
|
97
|
+
// 两距离带符号(负 = 已越);maxTokens 未发写 not sent、来源写 absent(该档守卫不覆盖——§6.17 射程)。
|
|
98
|
+
(() => {
|
|
99
|
+
const budget = requestBudget(agent?.provider)
|
|
100
|
+
const mt = budget.maxTokens === undefined ? "not sent" : String(budget.maxTokens)
|
|
101
|
+
return `Request budget: window ${budget.window} (${budget.windowSource}) · max_tokens ${mt} (${budget.maxTokensSource})`
|
|
102
|
+
+ ` · prompt ${usage.total} · to compaction line ${usage.threshold - usage.total}`
|
|
103
|
+
+ ` · to window limit ${budget.limit - usage.total}`
|
|
104
|
+
})(),
|
|
83
105
|
`Segments ≈: system ${usage.system} · tools ${usage.tools} · history ${usage.history} (basis: ${usage.basis})`,
|
|
84
106
|
`History: tool outputs ${toolOut} tokens in ${toolCount} msgs · prunable stale ${stale.tokens} tokens in ${stale.indexes.length} msgs · protected tail ${stale.tailCount} msgs`,
|
|
107
|
+
cacheLine,
|
|
85
108
|
`Status line: context ${historyPercent(agent?.history ?? [], agent?.provider)}% (${usage.history} history estimate / ${usage.window} window)`,
|
|
86
109
|
`Compaction so far: ${compaction} · failures ${agent?._compressFailures ?? 0}/${COMPRESS_FAILURE_LIMIT}`,
|
|
87
110
|
].join("\n")
|
|
@@ -100,8 +123,8 @@ function pruneReceipt(agent) {
|
|
|
100
123
|
|
|
101
124
|
/**
|
|
102
125
|
* 可回查锚(父侧裁令 A · §6.16.7 两分支逐字):回执尾段给出「压掉的可一步取回」的坐标与调用形。
|
|
103
|
-
* 取回面 = `read_history` 既有 `path
|
|
104
|
-
*
|
|
126
|
+
* 取回面 = `read_history` 既有 `path=`(**不新建通道**);未绑定槽(会话首次保存前——CLI;VSC 恒绑定、不落本支——§6.16.7)
|
|
127
|
+
* ⇒ `cwd:` 发现面(等效取回——`cwd` 缺省时字面 `cwd:undefined` 的降级形,工具内不抛)。
|
|
105
128
|
*/
|
|
106
129
|
function retrievabilityAnchor(agent) {
|
|
107
130
|
const slot = agent?._slot
|
|
@@ -5,12 +5,10 @@
|
|
|
5
5
|
* 面一 `executeDesignSlotsAction`(`subagent` action `design-slots`——只读):
|
|
6
6
|
* 数据源 = 槽文件权威表(`readEngTokensFromSlot`——无 TTL 过滤原样)∪ 内存 Map(并集去重,
|
|
7
7
|
* 冲突以槽为准——D-S3 同源);**零写**(不 reconcile 回写、不落盘)。列表面 = 逐槽一行
|
|
8
|
-
* (designId ∥ live/expired ∥ ageDays ∥ expiresAt ∥
|
|
9
|
-
*
|
|
8
|
+
* (designId ∥ live/expired ∥ ageDays ∥ expiresAt ∥ 批 ∥ designToken(末位,恒携);龄序 = 最老在前)
|
|
9
|
+
* + 计数尾行;「批」= `_advisorRuns` 按 designId 命中 run 的 `docSetKey` 中 `batches/` 段档取 basename
|
|
10
10
|
* (KD-SL2——零新存储;不可得 ⇒ `—`)。
|
|
11
|
-
*
|
|
12
|
-
* `designId` 过滤单槽 ∥ `reveal:true`(必携 designId)单槽揭值——掩蔽 = 只揭活槽;缺省两参 = 现行
|
|
13
|
-
* 清点面逐字零变;零写同前。
|
|
11
|
+
* `designId` 过滤单槽(trim 精确匹配;空串视同缺省;未知 ⇒ 拒 + 列在场 id);缺省 = 现行清点面逐字零变;零写同前。
|
|
14
12
|
* 面二 `executeConsumeDesignAction`(`subagent` action `consume-design` 消费核——自
|
|
15
13
|
* `subagent-spawn.mjs` 迁入):选择器恰一(`designId` ∥ `designIds` ∥ `expired` ∥
|
|
16
14
|
* `olderThanDays`——KD-SL3 fail-closed 不猜);目标集 = 并集视图按选择器解析 → 逐枚移出
|
|
@@ -83,21 +81,12 @@ function slotFacts(agent, designId, token, now, ttlMs) {
|
|
|
83
81
|
/** 龄序比较(最老在前——剩余有效期升序;不可判龄(畸形串)排尾——不冒充最老)。 */
|
|
84
82
|
const byOldest = (a, b) => (b.ageDays ?? -Infinity) - (a.ageDays ?? -Infinity)
|
|
85
83
|
|
|
86
|
-
/** 口面「活」判据(回收读口掩蔽口径——2026-10-14):格式合法 ∧ 未过期——与列表 `status` 判据
|
|
87
|
-
* (`tokenExpired` 单源)不同(畸形槽列表记 live、口面照掩蔽——设计档 §9 掩蔽口径)。 */
|
|
88
|
-
const liveSlot = (e) => e.expiresAt !== null && !e.expired
|
|
89
|
-
|
|
90
|
-
/** 回收行(reveal 命中活槽时随槽行输出——值原样入 spawn 门):门禁校验 = 严格相等。 */
|
|
91
|
-
const RECOVERY_LINE = "design-slots: pass designId + designToken verbatim to the eng-coder spawn — the gate checks exact equality."
|
|
92
|
-
|
|
93
84
|
/**
|
|
94
85
|
* `subagent` action `design-slots`(F-SL1——只读清点面):工程模式父侧限定(与 consume-design
|
|
95
86
|
* 同门)。输出 = 行文本(逐槽行 + 计数尾行);字段 / 序 / 降级标记 = 契约,行内措辞 = 实现面。
|
|
96
87
|
* 分类 = 只读(planMode 放行 / 免审批 / digest 放行——谓词在 `agent/dispatch-gates.mjs`)。
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* `reveal:true`(必携 designId)单槽揭值——掩蔽 = 只揭活槽(格式合法 ∧ 未过期);两参皆缺 =
|
|
100
|
-
* 现行清点面逐字零变;零写(不 reconcile / 不落盘)。
|
|
88
|
+
* `designId` 过滤单槽(trim 精确匹配;空串视同缺省;未知 ⇒ 拒 + 列在场 id);
|
|
89
|
+
* 缺省 = 全量清点(不改变行契约——令牌列恒携);零写(不 reconcile / 不落盘)。
|
|
101
90
|
*/
|
|
102
91
|
export function executeDesignSlotsAction(args, ctx) {
|
|
103
92
|
const parent = ctx?.agent
|
|
@@ -108,17 +97,9 @@ export function executeDesignSlotsAction(args, ctx) {
|
|
|
108
97
|
const ttlMs = effectiveTokenTtlMs(parent)
|
|
109
98
|
const entries = [...unionSlots(parent)].map(([id, t]) => slotFacts(parent, id, t, now, ttlMs))
|
|
110
99
|
entries.sort(byOldest)
|
|
111
|
-
//
|
|
112
|
-
// trim 后精确匹配、空串视同缺省;`reveal:true` 必携 designId(一次一槽——不吐批量凭据)。
|
|
113
|
-
const revealArg = args?.reveal
|
|
114
|
-
if (revealArg !== undefined && revealArg !== null && typeof revealArg !== "boolean") {
|
|
115
|
-
throw new Error(`design-slots: reveal must be a boolean (got ${JSON.stringify(revealArg)}) — only the literal true reveals a token value; no guessing.`)
|
|
116
|
-
}
|
|
100
|
+
// 参数面(fail-closed——不猜):`designId` trim 后精确匹配、空串视同缺省;未知 ⇒ 拒 + 列在场 id。
|
|
117
101
|
const rawId = args?.designId
|
|
118
102
|
const designId = rawId === undefined || rawId === null ? "" : String(rawId).trim()
|
|
119
|
-
if (revealArg === true && !designId) {
|
|
120
|
-
throw new Error("design-slots: reveal:true requires the designId parameter — one slot at a time (no bulk dump).")
|
|
121
|
-
}
|
|
122
103
|
let selected = entries
|
|
123
104
|
if (designId) {
|
|
124
105
|
selected = entries.filter((e) => e.designId === designId)
|
|
@@ -127,14 +108,7 @@ export function executeDesignSlotsAction(args, ctx) {
|
|
|
127
108
|
throw new Error(`design-slots: unknown designId ${JSON.stringify(designId)} — no slot holds this id (held design ids: ${held}).`)
|
|
128
109
|
}
|
|
129
110
|
}
|
|
130
|
-
const
|
|
131
|
-
const lines = selected.flatMap((e) => {
|
|
132
|
-
const base = `- designId=${e.designId} status=${e.expired ? "expired" : "live"} ageDays=${e.ageDays === null ? "—" : e.ageDays.toFixed(1)} expiresAt=${e.expiresAt === null ? "—" : new Date(e.expiresAt).toISOString()} batch=${e.batch}`
|
|
133
|
-
if (!reveal) return [base]
|
|
134
|
-
if (liveSlot(e)) return [`${base} designToken=${e.token}`, RECOVERY_LINE]
|
|
135
|
-
// 掩蔽(如实注因):过期 / 畸形槽在任何门禁都过不了——揭值零收益(设计档 §9 掩蔽口径)
|
|
136
|
-
return [`${base} designToken=— (${e.expiresAt === null ? "malformed" : "expired"} — masked; passes no gate)`]
|
|
137
|
-
})
|
|
111
|
+
const lines = selected.map((e) => `- designId=${e.designId} status=${e.expired ? "expired" : "live"} ageDays=${e.ageDays === null ? "—" : e.ageDays.toFixed(1)} expiresAt=${e.expiresAt === null ? "—" : new Date(e.expiresAt).toISOString()} batch=${e.batch} designToken=${e.expiresAt === null ? "—" : e.token}`)
|
|
138
112
|
const live = selected.filter((e) => !e.expired).length
|
|
139
113
|
lines.push(`design-slots: ${selected.length} slot(s) — ${live} live / ${selected.length - live} expired${designId ? " (filtered by designId)" : ""}`)
|
|
140
114
|
return lines.join("\n")
|