@thincoder/core 0.10.2 → 0.10.4
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 +65 -0
- package/advisor/citations.mjs +40 -4
- package/advisor/compaction.mjs +21 -2
- package/advisor/convergence.mjs +1 -1
- package/advisor/history.mjs +8 -6
- package/advisor/loop.mjs +29 -27
- package/advisor/messages.mjs +5 -4
- package/advisor/run.mjs +5 -1
- package/advisor/timeline.mjs +26 -0
- package/advisor.mjs +4 -2
- package/agent/completion.mjs +3 -1
- package/agent/digest-account.mjs +188 -0
- package/agent/dispatch-run.mjs +29 -12
- package/agent/family-tools.mjs +11 -11
- package/agent/helpers.mjs +8 -0
- package/agent/record-results.mjs +10 -1
- package/agent/run-stages.mjs +27 -4
- package/agent/run-start.mjs +49 -3
- package/agent/spawn-child.mjs +2 -1
- package/agent/subagent-event.mjs +71 -0
- package/agent/suspension.mjs +60 -12
- package/agent/turn-loop.mjs +31 -2
- package/agent/write-gate.mjs +13 -25
- package/agent-tools/advisor-async.mjs +25 -7
- package/agent-tools/advisor-settle.mjs +7 -9
- package/agent-tools/advisor.mjs +34 -33
- package/agent-tools/async-settle.mjs +29 -5
- package/agent-tools/batch-lifecycle.mjs +29 -7
- package/agent-tools/batch-paths.mjs +22 -2
- package/agent-tools/batch-read.mjs +36 -0
- package/agent-tools/batch-skeleton.mjs +59 -33
- package/agent-tools/batch.mjs +16 -6
- package/agent-tools/design-token.mjs +76 -16
- package/agent-tools/escalate-async.mjs +4 -2
- package/agent-tools/review-facts.mjs +143 -16
- package/agent-tools/subagent-actions-query.mjs +13 -6
- package/agent-tools/subagent-async.mjs +16 -4
- package/agent-tools/subagent-panel.mjs +121 -37
- package/agent-tools/subagent-spawn.mjs +1 -1
- package/agent-tools/subagent.mjs +5 -3
- package/agent-tools/verify.mjs +5 -0
- package/agent-tools/zero-write-watch.mjs +77 -0
- package/agent.mjs +1 -1
- package/auto-think.mjs +26 -2
- package/config-presets.mjs +8 -0
- package/config.mjs +30 -0
- package/conventions.mjs +26 -6
- package/declaration.mjs +26 -2
- package/embedding.mjs +35 -3
- package/escape.mjs +1 -1
- package/i18n.mjs +5 -0
- package/ledger-cmd.mjs +13 -21
- package/ledger-db.mjs +34 -1
- package/ledger-migrate.mjs +4 -3
- package/ledger-read.mjs +94 -0
- package/ledger-surface.mjs +15 -46
- package/ledger-tools.mjs +148 -85
- package/ledger-variant-notice.mjs +47 -0
- package/ledger.mjs +38 -64
- package/manifest-schema.mjs +19 -7
- package/manifest.mjs +32 -11
- package/mcp/transport-http.mjs +52 -4
- package/mcp/transport-stdio.mjs +33 -8
- package/mcp.mjs +13 -5
- package/memory/code-search.mjs +5 -3
- package/memory/core.mjs +6 -4
- package/memory/docs.mjs +5 -3
- package/memory/schema.mjs +2 -0
- package/package.json +1 -1
- package/prompts/advisor-design.md +3 -2
- package/prompts/advisor-round1.md +1 -0
- package/prompts/advisor-round2.md +8 -3
- package/prompts/advisor-round3.md +7 -2
- package/prompts/common.md +2 -2
- package/prompts/discipline-normal.md +2 -1
- package/prompts/persona-engineering.md +7 -4
- package/provider/anthropic.mjs +14 -2
- package/provider/core.mjs +10 -4
- package/provider/errors.mjs +23 -0
- package/provider/google.mjs +31 -4
- package/provider/normalize.mjs +57 -0
- package/provider/responses-request.mjs +3 -1
- package/provider/responses.mjs +38 -4
- package/provider/sse.mjs +45 -13
- package/proxy.mjs +13 -3
- package/session.mjs +13 -2
- package/shell-identity.mjs +107 -0
- package/stream-destroy.mjs +62 -0
- package/tool-docs/advisor.md +1 -1
- package/tool-docs/bash.md +1 -1
- package/tool-docs/batch.md +1 -1
- package/tool-docs/delete.md +7 -5
- package/tool-docs/ledger.md +6 -0
- package/tool-docs/subagent.md +10 -0
- package/tool-docs/verify.md +1 -1
- package/tools/bash.mjs +9 -4
- package/tools/edit-batch.mjs +7 -2
- package/tools/index.mjs +1 -4
- package/tools/patch.mjs +8 -2
- package/tools/write-path.mjs +6 -4
- package/undo-stack.mjs +23 -9
- package/tool-docs/ledger_add.md +0 -1
- package/tool-docs/ledger_close.md +0 -1
- package/tool-docs/ledger_count.md +0 -1
- package/tool-docs/ledger_query.md +0 -1
- package/tool-docs/ledger_update.md +0 -1
package/agent/turn-loop.mjs
CHANGED
|
@@ -49,14 +49,22 @@ export async function runTurnLoop(agent, {
|
|
|
49
49
|
|
|
50
50
|
// SUBAGENT-UPSTREAM-CHANNEL(AGENT-LOOP-UPSTREAM.md §6.27.4 消费点):子 → 父在飞消息的
|
|
51
51
|
// 回合边界注入单点取用一次(模块缓存 ⇒ 每 run 一次代价);动态 import = 零新增静态边
|
|
52
|
-
//
|
|
52
|
+
// (同款先例 = run-start.mjs:43 ∥ :66——injectAsyncResult 动态 import)。
|
|
53
53
|
const { drainChildUpstream } = await import("../agent-tools/parent-channel.mjs")
|
|
54
|
+
// #934(零落笔看门狗——AGENT-LOOP-UPSTREAM.md §6.32):连续零写越阈 ⇒ 自动上行提醒(只推——
|
|
55
|
+
// 不杀 / 不转向);单源叶 = zero-write-watch.mjs,动态 import 沿上条同款(零新增静态边)。
|
|
56
|
+
const { maybeZeroWriteAlert } = await import("../agent-tools/zero-write-watch.mjs")
|
|
57
|
+
// §6.31.5 落痕写点门(#939):载体吸收经 `carrierField` 单点(§6.31.4 同式);动态 import = 零新增静态边(沿上方两条同款)。
|
|
58
|
+
const { carrierField } = await import("../agent-tools/async-settle.mjs")
|
|
54
59
|
|
|
55
60
|
// #417(撞帽载荷「本段零落盘轮数」——只报数:零阈值 / 零自动动作):计数基线——回合环逐轮
|
|
56
61
|
// 比对 `_touchedFiles` 增量(轮顶对上一轮结账;撞帽收尾轮在环外单结)。
|
|
57
62
|
agent._turnFilesMark = agent._touchedFiles.length
|
|
58
63
|
|
|
59
64
|
for (let turn = 0; turn < maxTurns; turn++) {
|
|
65
|
+
// #793(核面小修批——AGENT-LOOP.md §6.2 环边界中止前置 ∥ 需求 F5):环头检查点——中止恒以
|
|
66
|
+
// AbortError 收束、不开启新轮(环头注入族 / 压缩 / 模型调用零触)。
|
|
67
|
+
if (signal?.aborted) throw abortError(signal, "agent", "turn-head")
|
|
60
68
|
// 第 19 批(TURN-ACROSS-SEGMENTS——设计 TURN-CAP-CONTINUE.md §4):编号帧——
|
|
61
69
|
// `_turnSeq` 每轮 +1(跨段累计,仅 `!resume` 链起点复位);面向消费面的两字段
|
|
62
70
|
// (状态行 + ⟦ev⟧turn / ⟦ev⟧approval 事件共用)在此同点赋值(编号唯一权威;帧
|
|
@@ -67,7 +75,18 @@ export async function runTurnLoop(agent, {
|
|
|
67
75
|
agent._maxTurns = frame.maxTurns
|
|
68
76
|
// #417(撞帽载荷「本段零落盘轮数」——采集点与 `_turnSeq` 同源面):上一轮至今 `_touchedFiles`
|
|
69
77
|
// 零增 ⇒ 上一轮零落盘,计数 +1;mark 前移(收尾轮在环外结账——见下方 ContinueError 前)。
|
|
70
|
-
|
|
78
|
+
// #934(零落笔看门狗——§6.32.2 ① 单点采集):streak 与越阈判定同点——上轮零写 ⇒ +1 并判越阈;
|
|
79
|
+
// 有写 ⇒ streak 归零 + 清闩(写后重臂——每连续零写段恰一条)。#417 计数语义逐字保持。
|
|
80
|
+
if (turn > 0) {
|
|
81
|
+
if (agent._touchedFiles.length === agent._turnFilesMark) {
|
|
82
|
+
agent._zeroWriteTurns += 1
|
|
83
|
+
agent._zeroWriteStreak += 1
|
|
84
|
+
maybeZeroWriteAlert(agent) // #934:连续零写越阈 ⇒ 自动上行提醒(只推——§6.32)
|
|
85
|
+
} else {
|
|
86
|
+
agent._zeroWriteStreak = 0
|
|
87
|
+
agent._zeroWriteAlerted = false // 写后重臂
|
|
88
|
+
}
|
|
89
|
+
}
|
|
71
90
|
agent._turnFilesMark = agent._touchedFiles.length
|
|
72
91
|
// D2 (AGENT-LOOP-SUBAGENT.md §6.7.2): depth>0 children emit a ⟦ev⟧turn progress token each turn —
|
|
73
92
|
// single emit point covering all three spawn tools; phase=llm (tool/done progress rides
|
|
@@ -106,6 +125,13 @@ export async function runTurnLoop(agent, {
|
|
|
106
125
|
systemPrompt, toolSchemas, streamOutput, callbacks, signal, streamRuleFired, autoTurn, depth, turn,
|
|
107
126
|
})
|
|
108
127
|
|
|
128
|
+
// §6.31.5 落痕(消化账务批 · 2026-10-05 · 台账 #930 ∥ #939 收正):本 run 内**逐轮追加**——
|
|
129
|
+
// 每模型轮返回即写(会话态);首轮签收不为收口轮覆盖(见账取件读累积痕)。
|
|
130
|
+
// 落父字段(抗 `history` 整体替换——`context.mjs` `applyCompression`);非会话零动作。
|
|
131
|
+
if (carrierField(agent, "_daSession") === true && response.content) {
|
|
132
|
+
agent._lastRunOutput = `${agent._lastRunOutput ?? ""}${response.content}\n`
|
|
133
|
+
}
|
|
134
|
+
|
|
109
135
|
// 内置工具(Responses web_search)结果本地化:服务端已执行——入历史为 tool 消息;
|
|
110
136
|
// 服务端 item id 是 msg_xxx 非 web_search_call_ 前缀——必须合成前缀(toItems 识别锚点),
|
|
111
137
|
// 原始 id 存入 content(真机冒烟 2026-08-31 验证)。
|
|
@@ -240,5 +266,8 @@ export async function runTurnLoop(agent, {
|
|
|
240
266
|
// #417(撞帽载荷「本段零落盘轮数」):收尾轮(撞帽轮)若零落盘同样计入——轮顶计数只
|
|
241
267
|
// 覆盖到倒数第二轮(maxTurns ≤ 0 时环未跑,不虚计)。
|
|
242
268
|
if (maxTurns > 0 && agent._touchedFiles.length === agent._turnFilesMark) agent._zeroWriteTurns += 1
|
|
269
|
+
// #793(核面小修批——AGENT-LOOP.md §6.2 环边界中止前置 ∥ 需求 F5):环尾检查点——中止恒以
|
|
270
|
+
// AbortError 收束(不落 ContinueError——用户 Stop 优先于继续提示;消费契约 = 外层按 reason 判定)。
|
|
271
|
+
if (signal?.aborted) throw abortError(signal, "agent", "turn-tail")
|
|
243
272
|
throw new ContinueError(maxTurns)
|
|
244
273
|
}
|
package/agent/write-gate.mjs
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
* (KD-M4-3)——本档承载三个单点:
|
|
7
7
|
*
|
|
8
8
|
* 1. `resolveReviewTargetPaths(agent)` —— 评审目标解析单点:读 manifest `docRoot`
|
|
9
|
-
* 产出评审对象 / 被审文件的绝对路径集合(目录级;子键值形态 = 串 |
|
|
10
|
-
*
|
|
9
|
+
* 产出评审对象 / 被审文件的绝对路径集合(目录级;子键值形态 = 串 | 多根数组)。
|
|
10
|
+
* 判定本体(`REVIEW_ROOT_KEYS` 键集 + `resolveReviewRootsFor(dir)`——2026-10-02 收口 ·
|
|
11
|
+
* 台账 #828 按用点形)自本档迁 `review-facts.mjs`(2026-10-04 · #921 · KD-2——本档同名
|
|
12
|
+
* 再出口保既有 import 面);缺 `docRoot` 键 → `readManifest`
|
|
11
13
|
* 的 fillDefaults 已补默认(架构 §2.3 E2 便利 fallback 落点);整档缺失 / 非法 JSON →
|
|
12
14
|
* `{ok:false}` → 回退 DEFAULT_MANIFEST.docRoot(本函数是壳门之下的便利层——E2 的
|
|
13
15
|
* 「整档缺失」由壳面 requireManifest 按用点处置(判 + 报明 / 建档,**不抛**——
|
|
@@ -15,8 +17,6 @@
|
|
|
15
17
|
* KD-M1-12):CLI make-agent.mjs / VSC setup.mjs 同判);读错(权限等)→ 上抛不伪装成缺失(manifest.mjs 契约)。
|
|
16
18
|
* M6 的 `advisor.mjs` design-review 分类分支消费本导出
|
|
17
19
|
* (KD-M6-1——同源不重复实现;落 dispatch.mjs 会让 advisor 反向 import 门禁簇成环)。
|
|
18
|
-
* **按用点形(2026-10-02 收口 · 台账 #828)**:`resolveReviewRootsFor(dir)`——同单点
|
|
19
|
-
* 非第二实现(按目标目录 ∥ 所属项目根解析;多档锚下各文档按各自项目判定)。
|
|
20
20
|
* 2. `freezeWindowConflict(agent, absPaths)` —— D5 冻结窗口判据组装:被审文件集 =
|
|
21
21
|
* 声明文档集(`inflightDesignReviewConflict` 同源 docAbs 腿)+ 批次档(`run.batchDoc`
|
|
22
22
|
* 腿——评审绑定批档在途时,写批档同样致 stale)。dispatch.mjs
|
|
@@ -30,7 +30,6 @@
|
|
|
30
30
|
* 代码路径判定(conventions.mjs 单一权威分类——KD-M4-1 保留为代码路径判定用)。
|
|
31
31
|
*/
|
|
32
32
|
import { basename, dirname, resolve } from "node:path"
|
|
33
|
-
import { DEFAULT_MANIFEST, docRootPaths, readManifest } from "../manifest.mjs"
|
|
34
33
|
// advisor-settle 无上游依赖本簇(dispatch → write-gate → advisor-settle 单向)。normAbs
|
|
35
34
|
// 权威本体 = review-facts.mjs(第 33 批迁入),advisor-settle re-export;本档再 re-export
|
|
36
35
|
// (指针链非副本),M6 经本档一行 import 取齐。
|
|
@@ -38,13 +37,17 @@ import { inflightDesignReviewConflict, normAbs } from "../agent-tools/advisor-se
|
|
|
38
37
|
// 批次档路径单源(叶档——`BATCH-RECORD.md` §4.15):批次档写门基底集取 `batchDocBases`。
|
|
39
38
|
import { batchDocBases } from "../agent-tools/batch-paths.mjs"
|
|
40
39
|
export { normAbs }
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
// 2026-10-04(工具路径基面根治批 · #921 · KD-2 同名再出口):REVIEW_ROOT_KEYS +
|
|
41
|
+
// resolveReviewRootsFor 判定本体迁 `review-facts.mjs`(评审事实面——advisor-async 既有
|
|
42
|
+
// import 面零新模块边);本档同名再出口(指针非副本——normAbs 同款先例)——既有 import 面
|
|
43
|
+
// (advisor.mjs / 测试)零改;本档消费点 `resolveReviewTargetPaths` 薄委托同名函数。
|
|
44
|
+
export { REVIEW_ROOT_KEYS, resolveReviewRootsFor } from "../agent-tools/review-facts.mjs"
|
|
45
|
+
import { resolveReviewRootsFor } from "../agent-tools/review-facts.mjs"
|
|
44
46
|
|
|
45
47
|
/**
|
|
46
|
-
* 评审目标解析单点(M4 §2.1#1
|
|
47
|
-
*
|
|
48
|
+
* 评审目标解析单点(M4 §2.1#1):评审对象 / 被审文件的绝对路径集合(目录级)——
|
|
49
|
+
* 判定本体(manifest `docRoot` 读面 / 缺档回退 / 读错上抛语义)2026-10-04 迁
|
|
50
|
+
* `review-facts.mjs` 单源(KD-2——本档薄委托;详述见该档 resolveReviewRootsFor JSDoc)。
|
|
48
51
|
* 缺 `docRoot` 键 → readManifest 已补默认;整档缺失 / 非法 →
|
|
49
52
|
* `{ok:false}` → 回退 DEFAULT_MANIFEST.docRoot(便利层——E2 的「整档缺失」由壳面
|
|
50
53
|
* requireManifest 按用点处置(判 + 报明 / 建档,**不抛**——启动侧恒不拒,KD-M1-25;**工程模式会话**口径——KD-M1-12),本函数不重复拦);
|
|
@@ -56,21 +59,6 @@ export function resolveReviewTargetPaths(agent) {
|
|
|
56
59
|
return resolveReviewRootsFor(agent?.cwd ?? process.cwd())
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
/** 同单点之**按用点解析**增量(2026-09-21「参数按用点解析」之裁收口 · 台账 #828):按**目标目录**
|
|
60
|
-
* (其所属项目根——最近带档祖先)解析评审目标根集——多档锚下各文档按各自项目判定。
|
|
61
|
-
* @param {string} dir 目标目录(项目根——最近带档祖先)
|
|
62
|
-
* @returns {string[]} 评审目标目录绝对路径(去重)
|
|
63
|
-
*/
|
|
64
|
-
export function resolveReviewRootsFor(dir) {
|
|
65
|
-
const man = readManifest(dir)
|
|
66
|
-
const root = man.ok && man.manifest?.docRoot && typeof man.manifest.docRoot === "object"
|
|
67
|
-
? man.manifest.docRoot
|
|
68
|
-
: DEFAULT_MANIFEST.docRoot
|
|
69
|
-
const out = []
|
|
70
|
-
for (const key of REVIEW_ROOT_KEYS) out.push(...docRootPaths(root?.[key], dir))
|
|
71
|
-
return [...new Set(out)]
|
|
72
|
-
}
|
|
73
|
-
|
|
74
62
|
/**
|
|
75
63
|
* D5 冻结窗口判据组装(M4 §2.1#2):评审在途(点火 → 结算)期间父侧对被审文件集
|
|
76
64
|
* (声明文档集 + 批次档)的写入 → 返回冲突 { id, path };无冲突 → null。
|
|
@@ -47,13 +47,13 @@ export {
|
|
|
47
47
|
// 第 11 批拆分(2026-09-11):settle 记账 + 变更日志(noteMutations)+ 陈旧判定
|
|
48
48
|
// (reviewIsStale)+ 冻结冲突(inflightDesignReviewConflict)迁 advisor-settle.mjs——
|
|
49
49
|
// 既有 import 面(dispatch / subagent-async / escalate-async / 测试)经此 re-export 不变。
|
|
50
|
-
import {
|
|
50
|
+
import { mutationSeqOf, settleAdvisorRun } from "./advisor-settle.mjs"
|
|
51
51
|
export {
|
|
52
52
|
mutationSeqOf, noteMutations, reviewIsStale, settleAdvisorRun, inflightDesignReviewConflict,
|
|
53
53
|
} from "./advisor-settle.mjs"
|
|
54
54
|
// 第 33 批(评审失败护栏):doc-set 键迁事实面档(实例续跑与陈旧判定同锚单源;原为私有——零 import 面)。
|
|
55
55
|
// 本文件继续在实例解析 / 池 entries 上消费它。
|
|
56
|
-
import { docSetKey } from "./review-facts.mjs"
|
|
56
|
+
import { docSetKey, resolveReviewDocPaths, scopeAdvisorMirror } from "./review-facts.mjs"
|
|
57
57
|
// F31(2026-09-18 顾问面治理批):拒回文案对象标识行单源。
|
|
58
58
|
import { withIdentityLine, scopeSummary } from "../advisor/notice.mjs"
|
|
59
59
|
import { runAdvisorReview, resolveAdvisorProvider, ADVISOR_THINKING_PLACEHOLDER } from "../advisor/run.mjs"
|
|
@@ -65,6 +65,8 @@ import { deathLine } from "../abort-provenance.mjs"
|
|
|
65
65
|
// ASYNC-RESULT-CONTAINER.md D3/D6:settle 公共收尾单点 + child signal 构建单点
|
|
66
66
|
import { bindChildController, buildChildSignal, carrierField, getAsyncPool, settleAsyncEntry, tombstoneOf, writeTombstone } from "./async-settle.mjs"
|
|
67
67
|
import { nextSubagentId, consumeSubagentToken, assertPoolKeyFree, entryTerminal } from "./subagent-scheduler.mjs"
|
|
68
|
+
// #934(零落笔看门狗——AGENT-LOOP-UPSTREAM.md §6.32.6 触发接线②):评审族推送单源(叶档——恒零抛)。
|
|
69
|
+
import { pushAdvisorAlert } from "./zero-write-watch.mjs"
|
|
68
70
|
|
|
69
71
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
70
72
|
// Review-instance registry (_advisorRuns — per-review rounds/prior/cap)
|
|
@@ -135,6 +137,11 @@ export function resolveAdvisorLaunch(agent, reviewType, { documents = null } = {
|
|
|
135
137
|
// the same id (no slot residue — T14: the old token is then rejected at
|
|
136
138
|
// the gate); old slots die at TTL only, kept as the fail-safe fallback.
|
|
137
139
|
const prior = [...runs.values()].reverse().find((r) => r.reviewType === "design" && r.docSetKey === key)
|
|
140
|
+
// #863 实例回收(§6.10):同 docSetKey 保最新一条 closed(= prior——F2h 只取 newest,语义零变);
|
|
141
|
+
// 余者去重回收(实例随删释放——closed 零消费面)。
|
|
142
|
+
for (const r of [...runs.values()]) {
|
|
143
|
+
if (r !== prior && r.reviewType === "design" && !r.open && r.docSetKey === key) runs.delete(r.reviewId)
|
|
144
|
+
}
|
|
138
145
|
run = {
|
|
139
146
|
reviewId, reviewType, designId: prior?.designId ?? reviewId,
|
|
140
147
|
round: 0, priorOutput: null, stale: false, open: true, docSetKey: key,
|
|
@@ -144,6 +151,10 @@ export function resolveAdvisorLaunch(agent, reviewType, { documents = null } = {
|
|
|
144
151
|
} else {
|
|
145
152
|
run = openCodeRun(agent)
|
|
146
153
|
if (!run) {
|
|
154
|
+
// #863 实例回收(§6.10):新 code 实例创建 ⇒ closed 实例清除(openCodeRun 仅取 open——零消费)。
|
|
155
|
+
for (const r of [...runs.values()]) {
|
|
156
|
+
if (r.reviewType === "code" && !r.open) runs.delete(r.reviewId)
|
|
157
|
+
}
|
|
147
158
|
const reviewId = randomUUID()
|
|
148
159
|
run = {
|
|
149
160
|
reviewId, reviewType, designId: null,
|
|
@@ -152,9 +163,9 @@ export function resolveAdvisorLaunch(agent, reviewType, { documents = null } = {
|
|
|
152
163
|
runs.set(reviewId, run)
|
|
153
164
|
}
|
|
154
165
|
}
|
|
155
|
-
// Scope the
|
|
156
|
-
|
|
157
|
-
agent
|
|
166
|
+
// Scope the mirror fields to THIS instance (message builder + run.mjs cap +
|
|
167
|
+
// displays; #949 ① — the queued-launch window is re-secured at entry.start).
|
|
168
|
+
scopeAdvisorMirror(agent, run)
|
|
158
169
|
return { run, isNew: run.round === 0 && !run.priorOutput, reviewId: run.reviewId, designId: run.designId }
|
|
159
170
|
}
|
|
160
171
|
|
|
@@ -381,8 +392,11 @@ export function launchAsyncAdvisor(parent, ctx, launch) {
|
|
|
381
392
|
id, role: "advisor", reviewType, run,
|
|
382
393
|
reviewId: run.reviewId, designId, designToken, documents, paths, object,
|
|
383
394
|
launchSeq: mutationSeqOf(parent),
|
|
395
|
+
// docAbs 产出面同源归一(2026-10-04 · #921 · A 随件):四腿单源 resolved(裸形/前缀形
|
|
396
|
+
// 命中候选项目根即归一为绝对路径)——冻结窗/陈旧判定对**裸形受理档**照护成立(否则 A
|
|
397
|
+
// 开冻结窗保护洞);绝对形与旧 normAbs 面逐字等价。
|
|
384
398
|
docAbs: reviewType === "design" && Array.isArray(documents)
|
|
385
|
-
? documents
|
|
399
|
+
? [...resolveReviewDocPaths(documents, parent.cwd).resolved.values()]
|
|
386
400
|
: [],
|
|
387
401
|
relayPrefix: `advisor#${id}/`,
|
|
388
402
|
status: queued ? "queued" : "running", position: undefined,
|
|
@@ -411,9 +425,12 @@ export function launchAsyncAdvisor(parent, ctx, launch) {
|
|
|
411
425
|
// Async mark + [model] — the TUI block opens at ACTUAL start (⏹ gating reads it).
|
|
412
426
|
ctx?.callbacks?.onToken?.(entry.relayPrefix + "⟦ev⟧async\x1e")
|
|
413
427
|
ctx?.callbacks?.onToken?.(entry.relayPrefix + "[model]" + (entry.model ?? ""))
|
|
428
|
+
// #949 ①:排队 / 延迟窗口封死——构建前按本评审实例复核镜像(消费点定域;幂等)。
|
|
429
|
+
scopeAdvisorMirror(parent, run)
|
|
414
430
|
runAdvisorReview(parent, reviewType, {
|
|
415
431
|
onOutput: (chunk) => relayAdvisorOutput(ctx?.callbacks, entry.relayPrefix, chunk),
|
|
416
432
|
signal: entry.controller.signal,
|
|
433
|
+
onStall: (rounds) => pushAdvisorAlert(parent, `advisor#${id}`, rounds), // #934:连续无产出越阈 ⇒ 自动上行提醒(只推——§6.32)
|
|
417
434
|
}, designToken, documents, paths, object, designId)
|
|
418
435
|
.then((report) => { entry.report = report })
|
|
419
436
|
// §6.12 第 3 条合成器(第 24 批):原 message 前缀逐字保留 + 来源后缀
|
|
@@ -474,7 +491,8 @@ export function closeOpenCodeAdvisorRuns(agent) {
|
|
|
474
491
|
const runs = advisorRunsRead(agent)
|
|
475
492
|
if (runs instanceof Map) {
|
|
476
493
|
for (const r of runs.values()) {
|
|
477
|
-
|
|
494
|
+
// #863 关闭点轻量化:闭合同拍释放 priorOutput(closed 实例零消费面——续跑仅取 open)。
|
|
495
|
+
if (r.reviewType === "code" && r.open) { r.open = false; r.priorOutput = null; closed = true }
|
|
478
496
|
}
|
|
479
497
|
}
|
|
480
498
|
return closed
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* ADVISOR-GUARDS.md §7)。记账本体 / 陈旧判定 / 轮次 / token 落盘零改。
|
|
22
22
|
*/
|
|
23
23
|
import { persistEngTokens } from "../token-ttl.mjs"
|
|
24
|
-
import { settleDesignReview,
|
|
24
|
+
import { settleDesignReview, stripDesignTokenEcho, stripApprovedSuffix } from "./design-token.mjs"
|
|
25
25
|
import { looksLikeReviewOutput, advisorIncompleteMarker, ADVISOR_LAUNCH_REFUSAL_PREFIX } from "../advisor/run.mjs"
|
|
26
26
|
import { isCodePath, loadProjectDeclaration } from "../conventions.mjs"
|
|
27
27
|
import { logEvent } from "../log.mjs"
|
|
@@ -176,9 +176,7 @@ export function settleAdvisorRun(agent, entry) {
|
|
|
176
176
|
run.approvedSuffix = null
|
|
177
177
|
persistFailed = true
|
|
178
178
|
logEvent("advisor:error", { id: `advisor#${entry.id}`, err: "engDesignTokens slot persist failed — settle failed (re-review)" })
|
|
179
|
-
const stripped = String(result)
|
|
180
|
-
.replace(makeDesignTokenRegex(entry.designToken, "g"), "")
|
|
181
|
-
.trim()
|
|
179
|
+
const stripped = stripDesignTokenEcho(String(result), entry.designToken).trim()
|
|
182
180
|
report = `${stripped}\n\nD1: the design review passed but the token could NOT be durably written to the session ledger (slot persist failed) — re-run advisor(type='design') to re-issue; no eng-coder spawn is authorized for this review (评审通过但 token 未能持久化——需重评).`.trim()
|
|
183
181
|
}
|
|
184
182
|
} else {
|
|
@@ -210,17 +208,17 @@ export function settleAdvisorRun(agent, entry) {
|
|
|
210
208
|
}
|
|
211
209
|
} else if (run.reviewType === "design" && entry.designToken && result != null) {
|
|
212
210
|
// fix B(stale 分支):陈旧评审不签发——digest 不得展示未注册 token——先剥
|
|
213
|
-
//
|
|
214
|
-
const stripped = String(result)
|
|
215
|
-
.replace(makeDesignTokenRegex(entry.designToken, "g"), "")
|
|
216
|
-
.trim()
|
|
211
|
+
// token 回显(单源三形)+ 前置 "评审目标已变更——token 未签发"(不变式——两分支都清洗)。
|
|
212
|
+
const stripped = stripDesignTokenEcho(String(result), entry.designToken).trim()
|
|
217
213
|
report = `评审目标已变更——token 未签发 (review target changed after launch — this review judged a stale state; no design token was issued — re-run the review on the current state)\n\n${stripped}`.trim()
|
|
218
214
|
}
|
|
219
215
|
// Prior of round 2+ = the last REVIEW-LOOKING output (mirror of run.mjs's guard).
|
|
220
216
|
// F2e: strip the engine-approved suffix FIRST — the prior must never
|
|
221
217
|
// carry the raw token / designId (exact truncation — zero collateral).
|
|
222
218
|
// 结论块(下方)刻意晚于 prior 归一:prior 只承载评审正文,不携带机制文案。
|
|
223
|
-
|
|
219
|
+
// #863 关闭点轻量化(父侧批·五裁定——§2.3 表外追加):closed 实例零消费面(续跑仅取 open)——
|
|
220
|
+
// 关闭后不再写回 priorOutput(否则 settleDesignReview 关闭点的释放被覆盖)。
|
|
221
|
+
if (run.open && report && looksLikeReviewOutput(report)) {
|
|
224
222
|
run.priorOutput = stripApprovedSuffix(report, run.approvedSuffix)
|
|
225
223
|
}
|
|
226
224
|
// F28/F29 失败结论(2026-09-18 顾问面治理批——**两轨共用**,ADVISOR-GUARDS.md §7 契约二):
|
package/agent-tools/advisor.mjs
CHANGED
|
@@ -9,12 +9,11 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { runAdvisorReview, advisorIncompleteMarker, ADVISOR_LAUNCH_REFUSAL_PREFIX } from "../advisor/run.mjs"
|
|
11
11
|
import { resolveBatchDocPath } from "./batch.mjs"
|
|
12
|
-
// M6(模块设计 §2.1 F3):评审对象来源读 manifest docRoot
|
|
13
|
-
//
|
|
14
|
-
// normAbs
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { sep } from "node:path"
|
|
12
|
+
// M6(模块设计 §2.1 F3):评审对象来源读 manifest docRoot(声明面)——单一权威源 =
|
|
13
|
+
// review-facts.mjs 的 resolveReviewDocPaths(2026-10-04 · #921 · KD-2——分类四腿 + B 文案
|
|
14
|
+
// 诊断数据单源;write-gate 同名再出口保 normAbs 面)。不 import dispatch.mjs(簇间回边,环风险)。
|
|
15
|
+
import { normAbs } from "../agent/write-gate.mjs"
|
|
16
|
+
import { resolveReviewDocPaths } from "./review-facts.mjs"
|
|
18
17
|
import {
|
|
19
18
|
generateDesignToken,
|
|
20
19
|
settleDesignReview,
|
|
@@ -63,11 +62,11 @@ export const advisorTool = {
|
|
|
63
62
|
documents: {
|
|
64
63
|
type: "array",
|
|
65
64
|
items: { type: "string" },
|
|
66
|
-
description: "Doc paths to review (design / requirements / referenced docs). The advisor reviews ONLY these — no git diff scan. Use for both review types to pass the task's docs list.",
|
|
65
|
+
description: "Doc paths to review (design / requirements / referenced docs). Relative paths resolve against the session cwd first, then candidate project roots (project-root-relative e.g. <repo>/docs/…, or absolute). The advisor reviews ONLY these — no git diff scan. Use for both review types to pass the task's docs list.",
|
|
67
66
|
},
|
|
68
67
|
batchDoc: {
|
|
69
68
|
type: "string",
|
|
70
|
-
description: "Design review only: the in-flight batch record path — validated whenever passed (unreadable ⇒ refused, not ignored); the reviewer then also gets the `batch` write channel to record its findings + verdict. Omit when no batch record is in flight (no write channel — zero regression).",
|
|
69
|
+
description: "Design review only: the in-flight batch record path — validated whenever passed (unreadable ⇒ refused, not ignored); the reviewer then also gets the `batch` write channel to record its findings + verdict. Relative paths resolve against the session cwd first, then candidate project roots (project-root-relative e.g. <repo>/docs/batches/…, or absolute). Omit when no batch record is in flight (no write channel — zero regression).",
|
|
71
70
|
},
|
|
72
71
|
},
|
|
73
72
|
required: ["type"],
|
|
@@ -112,34 +111,36 @@ export const advisorTool = {
|
|
|
112
111
|
)
|
|
113
112
|
}
|
|
114
113
|
|
|
115
|
-
// Design review: the review scope must be documentation files. Classification
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
114
|
+
// Design review: the review scope must be documentation files. Classification =
|
|
115
|
+
// resolveReviewDocPaths (2026-10-04 · #921 · the four-leg single authority in
|
|
116
|
+
// review-facts.mjs): legs ① session-root set / ② owning-project root set keep the
|
|
117
|
+
// legacy behavior verbatim; leg ③ probes candidate project roots for bare
|
|
118
|
+
// project-root-relative forms; leg ④ disambiguates by readable-file uniqueness
|
|
119
|
+
// (fail-closed — criterion=scope-doc-ambiguous when ≥2 candidates are readable).
|
|
119
120
|
if (reviewType === "design" && documents) {
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
// 的 docRoot 判定;无主档文档回退会话根集判定(原行为零变)。
|
|
123
|
-
const ownerRootsCache = new Map()
|
|
124
|
-
const ownRoots = (abs) => {
|
|
125
|
-
const owner = owningProject(abs)
|
|
126
|
-
if (!owner) return []
|
|
127
|
-
let r = ownerRootsCache.get(owner)
|
|
128
|
-
if (!r) {
|
|
129
|
-
r = resolveReviewRootsFor(owner).map((x) => x.replace(/[\\/]/g, sep))
|
|
130
|
-
ownerRootsCache.set(owner, r)
|
|
131
|
-
}
|
|
132
|
-
return r
|
|
133
|
-
}
|
|
134
|
-
const invalidDocs = documents.filter((doc) => {
|
|
135
|
-
const n = normAbs(doc, agent.cwd).replace(/[\\/]/g, sep)
|
|
136
|
-
const under = (r) => n === r || n.startsWith(r + sep)
|
|
137
|
-
return !roots.some(under) && !ownRoots(n).some(under)
|
|
138
|
-
})
|
|
139
|
-
if (invalidDocs.length > 0) {
|
|
121
|
+
const { resolved, invalid, ambiguous, candidates } = resolveReviewDocPaths(documents, agent.cwd)
|
|
122
|
+
if (invalid.length > 0 || ambiguous) {
|
|
140
123
|
if (ctx._toolCallId !== undefined) (agent._advisorRefusals ??= new Set()).add(ctx._toolCallId)
|
|
124
|
+
if (ambiguous) {
|
|
125
|
+
// 歧义拒(KD-4 独立判据——不混入 not-doc;列全部可读命中——fail-closed 不静默挑一)。
|
|
126
|
+
return withIdentityLine(
|
|
127
|
+
`Advisor: design review document is ambiguous — "${ambiguous.doc}" matches several candidate projects: ${ambiguous.hits.join(", ")}. Pass a project-root-relative or absolute path.`,
|
|
128
|
+
{ type: reviewType, scope: scopeSummary(documents), round: "—", criterion: "scope-doc-ambiguous" },
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
// not-doc 拒:既有稳定前缀逐字(行首)+ 解析诊断尾(B 面根治——逐文档「解析后绝对
|
|
132
|
+
// 路径 ← 基面 = 会话 cwd」+ 候选项目根提示 cap 5 + 重试指引——纠正「误报因」缺陷)。
|
|
133
|
+
const invalidList = invalid.map((v) => v.doc).join(", ")
|
|
134
|
+
const attemptedList = invalid
|
|
135
|
+
.map((v) => `${v.doc} → ${v.attempted.length > 0 ? v.attempted.join(" | ") : normAbs(v.doc, agent.cwd)}`)
|
|
136
|
+
.join("; ")
|
|
137
|
+
const shown = candidates.slice(0, 5)
|
|
138
|
+
const more = candidates.length - shown.length
|
|
139
|
+
const candidateNote = shown.length > 0
|
|
140
|
+
? `(candidates: ${shown.join(", ")}${more > 0 ? ` +${more} more` : ""}) — pass project-root-relative (e.g. ${shown[0]}/docs/…) or absolute paths`
|
|
141
|
+
: "— pass project-root-relative or absolute paths"
|
|
141
142
|
return withIdentityLine(
|
|
142
|
-
`Advisor: design review documents must be documentation files (per the project's conventions). Invalid: ${
|
|
143
|
+
`Advisor: design review documents must be documentation files (per the project's conventions). Invalid: ${invalidList}\nDiagnosis: resolved against session cwd ${agent.cwd} → ${attemptedList}; no candidate project docRoot contains it ${candidateNote}.`,
|
|
143
144
|
{ type: reviewType, scope: scopeSummary(documents), round: "—", criterion: "scope-not-doc" },
|
|
144
145
|
)
|
|
145
146
|
}
|
|
@@ -134,6 +134,17 @@ export function parkAsyncPending(parent, entry) {
|
|
|
134
134
|
pend.push(entry)
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/**
|
|
138
|
+
* §6.31.5 升级账本写助手(消化账务批):`_unsettledDigests` 追加一行
|
|
139
|
+
* (`{ id, role, failures, report, at }`)。载体吸收(父字段优先 / history 借用——形态同
|
|
140
|
+
* `parkAsyncPending`);行序 = 升级序(读面 = `digest-account.mjs` `unsettledRows`)。
|
|
141
|
+
*/
|
|
142
|
+
export function appendUnsettledDigest(parent, row) {
|
|
143
|
+
const existing = carrierField(parent, "_unsettledDigests")
|
|
144
|
+
const list = (parent._unsettledDigests ??= Array.isArray(existing) ? existing : [])
|
|
145
|
+
list.push(row)
|
|
146
|
+
}
|
|
147
|
+
|
|
137
148
|
// ─── D4 守卫统一(严格版)+ D6 信号兜底单点 ──────────────────────────────────
|
|
138
149
|
|
|
139
150
|
/** D4 settle 守卫统一(escalate 严格版):父侧中止 = 回合 signal aborted(#98 VSC 侧
|
|
@@ -161,7 +172,9 @@ export function buildChildSignal(parent, ctx) {
|
|
|
161
172
|
* 对子代理对象的引用(`childAgent`/`report`——条目前此从不显式释放,挂起期 = 分钟级驻留)。
|
|
162
173
|
* 幂等(重入不抛);池内/挂起未消化窗口语义零变(settle 时刻不释放——表 2 候选 2 否决:
|
|
163
174
|
* status/observe 在窗口内仍读 child 摘要)。三消费点(回合尾收集 / run 起始 pending 注入 /
|
|
164
|
-
*
|
|
175
|
+
* 挂起残差注入)——fallback 面(无 `_daSession`)注入完成后调本 helper(逐字沿用);会话态
|
|
176
|
+
* (`_daSession`)注入后改调 `releaseChildHold`(仅释放 `childAgent`——`report` 保留至销账 /
|
|
177
|
+
* 升级;§6.31.4 ∥ KD-DA7)。
|
|
165
178
|
*/
|
|
166
179
|
export function releaseSettledEntry(entry) {
|
|
167
180
|
if (!entry || typeof entry !== "object") return
|
|
@@ -169,6 +182,16 @@ export function releaseSettledEntry(entry) {
|
|
|
169
182
|
entry.report = null
|
|
170
183
|
}
|
|
171
184
|
|
|
185
|
+
/**
|
|
186
|
+
* §6.31.4 投递面(消化账务批 · 2026-10-05 · 台账 #930):会话态注入后**仅**释放 `childAgent`——
|
|
187
|
+
* `report` 保留至销账 / 升级(重投需原文;驻留增量以报告量级 ∥ 重投上限封顶)。fallback 面零调用
|
|
188
|
+
* (既有 `releaseSettledEntry` 逐字沿用——注入即释放)。
|
|
189
|
+
*/
|
|
190
|
+
export function releaseChildHold(entry) {
|
|
191
|
+
if (!entry || typeof entry !== "object") return
|
|
192
|
+
entry.childAgent = null
|
|
193
|
+
}
|
|
194
|
+
|
|
172
195
|
/**
|
|
173
196
|
* D3 settle 共享 helper——四族 settle 回调公共收尾单点(ASYNC-RESULT-CONTAINER.md
|
|
174
197
|
* AC1:settle 记账单点,无逐字重复)。流程:
|
|
@@ -273,10 +296,11 @@ export function settleAsyncEntry(parent, entry, opts = {}) {
|
|
|
273
296
|
parkAsyncPending(parent, entry)
|
|
274
297
|
pool?.delete(String(entry.id))
|
|
275
298
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
299
|
+
// 发射单点(块到达时点归位批 · #746|§6.8「settle 延迟冻结(两态统一)」):settle 一律 ⟦ev⟧settled
|
|
300
|
+
// (区块头恒中间态「done · awaiting digestion」驻留面板);⟦ev⟧done = 消费面补发(消费窗清单单源
|
|
301
|
+
// = §6.8)⇒ 冻结 ∥ 归档恒落投递时点(消化账务批 · 2026-10-05——归档 ≠ 销账;销账后移见账 = §6.31);
|
|
302
|
+
// 回合运行中零归档触发器(「S 不夹运行中回合」结构不变式)。
|
|
303
|
+
ctx?.callbacks?.onToken?.(`${entry.relayPrefix}⟦ev⟧settled\x1e0\x1e0\x1esettled\x1e`)
|
|
280
304
|
}
|
|
281
305
|
// ④ 公共尾部:settleSeq 递增 + _settle 唤醒(never rejects)+ 唤醒挂起驱动 waiter
|
|
282
306
|
entry._settleSeq = (parent._asyncSettleSeq = (parent._asyncSettleSeq ?? 0) + 1)
|
|
@@ -49,6 +49,18 @@ function segmentNumber(raw) {
|
|
|
49
49
|
return m ? Number(m[1]) : null
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/** 档头两可选参归一(`BATCH-RECORD.md` §4.11 契约——逐字文本面 = `TOOLS.md` §6.20):trim ∥
|
|
53
|
+
* 单行(拒句逐字)∥ 空串 ≡ 未给(null);`ledger` 前导 `#` 补齐(board 无补齐)。给参 ⇒ 建即填
|
|
54
|
+
* 台账行(batchSkeleton 实参面);未给(任一)⇒ 占位留存 + 回执填法句。 */
|
|
55
|
+
function normalizeHeaderParam(name, raw) {
|
|
56
|
+
const v = typeof raw === "string" ? raw.trim() : ""
|
|
57
|
+
if (!v) return null
|
|
58
|
+
if (/\r?\n/.test(v)) {
|
|
59
|
+
throw new Error("batch: create ledger / board must be single lines — a multi-line value would break the header's 台账 line. Nothing was written.")
|
|
60
|
+
}
|
|
61
|
+
return name === "ledger" && !v.startsWith("#") ? `#${v}` : v
|
|
62
|
+
}
|
|
63
|
+
|
|
52
64
|
/** main-agent-only 门(BR-19/BR-24):create/close 仅 depth-0 放行(eng 子代理 / 评审皆拒)。 */
|
|
53
65
|
function assertMainAgentOnly(ctx, action, review) {
|
|
54
66
|
if (review || !isDepthZero(ctx)) {
|
|
@@ -58,8 +70,9 @@ function assertMainAgentOnly(ctx, action, review) {
|
|
|
58
70
|
|
|
59
71
|
/** 递归收集目录下全部 `*.md`(#369 · KD-4:显式嵌套路径为合法 create 面 ⇒ 顶层平扫会欠计
|
|
60
72
|
* ⇒ 复数情形漏判、默认定位可落非预期档)。只认真目录(`dirent.isDirectory()`——symlink 不入,
|
|
61
|
-
*
|
|
62
|
-
|
|
73
|
+
* 防环);单目录读错按无候选处理(尽力面,同旧平扫)。**转导出**(read-data-interface 批——
|
|
74
|
+
* `batch/list` 读面文件集同源复用;零行为变)。 */
|
|
75
|
+
export function collectMarkdownFiles(dir, out = []) {
|
|
63
76
|
let entries = []
|
|
64
77
|
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return out }
|
|
65
78
|
for (const e of entries) {
|
|
@@ -107,6 +120,8 @@ export function findInFlightBatch(cwd, bases) {
|
|
|
107
120
|
* 路径解析 = `resolveBatchCreatePath`(§4.15 单源:候选序首个落基底内者 / 锚定串只解析项目根形)。
|
|
108
121
|
* F11-B:`source` **必填**(topic 同款空拒——骨架编制行实参化;不传 = 死占位残留必被 F11-C 拒
|
|
109
122
|
* ⇒ create 即拒,不留死锁);`prev` 传入值**幂等剥**「前情 = 」前缀(值规范化;默认值零变)。
|
|
123
|
+
* 2026-10-05(A-2):`ledger` / `board` 可选参**建即填**台账行(归一契约 = §4.11;回执两态 =
|
|
124
|
+
* 两参皆给 ⇒ 标注已填 ∥ 未给(任一)⇒ 填法句)。
|
|
110
125
|
* @returns {string} 成功消息(含落盘绝对路径)
|
|
111
126
|
*/
|
|
112
127
|
export function createBatchRecord({ args, ctx, review, cwd, bases, onWritten }) {
|
|
@@ -137,15 +152,22 @@ export function createBatchRecord({ args, ctx, review, cwd, bases, onWritten })
|
|
|
137
152
|
if (/\r?\n/.test(source)) {
|
|
138
153
|
throw new Error("batch: create source must be a single line — a multi-line source would break the header's 编制 line. Nothing was written.")
|
|
139
154
|
}
|
|
155
|
+
// 档头两可选参(§4.11 契约——给则建即填台账行;未给任一 ⇒ 占位留存 + 回执填法句)
|
|
156
|
+
const ledger = normalizeHeaderParam("ledger", args?.ledger)
|
|
157
|
+
const board = normalizeHeaderParam("board", args?.board)
|
|
140
158
|
const date = typeof args?.date === "string" && args.date.trim() ? args.date.trim() : todayLocal()
|
|
141
159
|
// F11-B:prev 幂等 strip——调用方自带「前情 = 」前缀(单或重复)一并剥净(`+` 量词;归一化目的
|
|
142
160
|
// = 值规范化,无前缀值原样通过)。默认值形态「无(独立批)」零变。
|
|
143
161
|
const prevRaw = typeof args?.prev === "string" ? args.prev.trim() : ""
|
|
144
162
|
const prev = prevRaw ? (prevRaw.replace(/^(?:前情\s*[=::]\s*)+/g, "").trim() || "无(独立批)") : "无(独立批)"
|
|
145
163
|
mkdirSync(dirname(abs), { recursive: true })
|
|
146
|
-
writeFileSync(abs, batchSkeleton({ date, topic, source, prev }))
|
|
164
|
+
writeFileSync(abs, batchSkeleton({ date, topic, source, prev, ledger, board }))
|
|
147
165
|
onWritten?.(ctx?.agent ?? {}, abs)
|
|
148
|
-
|
|
166
|
+
// 回执两态(`TOOLS.md` §6.20 逐字):两参皆给 ⇒ 标注已填;未给(任一)⇒ 填法句(填法 = 文件编辑)
|
|
167
|
+
const headerNote = ledger !== null && board !== null
|
|
168
|
+
? `header 台账 line filled (${ledger} · ${board})`
|
|
169
|
+
: "header 台账 placeholder(s) remain — fill them by file edit before the record's first append/status opens"
|
|
170
|
+
return `batch: created ${abs} — six-section skeleton written (§1 status line carries the gate-legal 「进行中」 placeholder; register the ledger entry yourself — create does not). ${headerNote}`
|
|
149
171
|
}
|
|
150
172
|
|
|
151
173
|
/**
|
|
@@ -264,7 +286,7 @@ function assertStatusNote(note) {
|
|
|
264
286
|
* 恰一词(**余核 = 关键词**——F11-A 谓词收紧),散文说明走独立 `note` 字段(落状态行括注);
|
|
265
287
|
* 冻结真值不变(gate 只读 §1)。path 参数 = 仅 depth-0(D-BR21)——子代理/评审传 path
|
|
266
288
|
* ⇒ 拒(目标 = spawn/实例注入,语法上写不到别处)。F11-C 挂点 = 写盘前(assertStatusValue 后):
|
|
267
|
-
*
|
|
289
|
+
* 档头结构行含骨架死占位 ⇒ 拒(段体引用豁免;close 不拦——收口是主 agent 终态动作)。
|
|
268
290
|
* @returns {string} 成功消息
|
|
269
291
|
*/
|
|
270
292
|
export function statusBatchRecord({ args, ctx, review, pickTarget, onWritten }) {
|
|
@@ -297,8 +319,8 @@ export function statusBatchRecord({ args, ctx, review, pickTarget, onWritten })
|
|
|
297
319
|
const abs = pickTarget(args?.path)
|
|
298
320
|
const src = readFileSync(abs, "utf8")
|
|
299
321
|
assertGateOpen(src)
|
|
300
|
-
// F11-C
|
|
301
|
-
const residues = findPlaceholderResidue(src
|
|
322
|
+
// F11-C(写盘前):档头结构行死占位残留 ⇒ 拒(段体引用豁免——判据可辨性收正,`TOOLS.md` §6.20)
|
|
323
|
+
const residues = findPlaceholderResidue(src)
|
|
302
324
|
if (residues.length) throw new Error(placeholderResidueError(residues))
|
|
303
325
|
const written = updateSectionStatusLine(src, seg, lineValue)
|
|
304
326
|
writeFileSync(abs, written)
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* **歧义锚**(条 5 · 2026-10-02 · #828)= create 按目标所属项目落基底 ∥ 无所属显式拒;读面候选腿追加;
|
|
11
11
|
* 在飞 ∥ 写门基底集 = 候选并集(`batchDocBases`);**零变面** = 绝对路径照旧、三个错误文案逐字、
|
|
12
12
|
* 「参数在 + 路径可读」判据句、`ok` / `none` 两态。
|
|
13
|
+
* **基底自有项目根腿 + 基底相对单段收窄**(条 6 · 2026-10-05 · #942)= 逐基底 `owningProject` 恰一落点收 ∥ 多落点显式拒(`ambiguousLandingError`);基底相对腿限单段(多段串不二次拼接)。
|
|
13
14
|
*
|
|
14
15
|
* 依赖(KD-4 单向):本档为**叶档**——只 import `node:*` 与 `../manifest.mjs`;主档 re-export 保既有面。
|
|
15
16
|
*/
|
|
@@ -98,6 +99,7 @@ const outsideBasesError = (raw) => new Error(`batch: create path resolves outsid
|
|
|
98
99
|
const anchoredEscapeError = (raw) => new Error(`batch: create path is anchored at a batch base root but does not resolve under the declared docRoot.batches root(s) — refusing to nest it (fail-closed). Path: ${raw}`)
|
|
99
100
|
/** 歧义锚 create 拒面(**文案逐字** = `BATCH-RECORD.md` §4.15 条 5——机检锚 `ambiguous session anchor`)。 */
|
|
100
101
|
const ambiguousCreateError = (raw, candidates) => new Error(`batch: ambiguous session anchor — create target "${raw}" belongs to no candidate project; pass an explicit project path (e.g. "${candidates[0]}/docs/batches/<file>.md"). Candidates: ${candidates.join(", ")}`)
|
|
102
|
+
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(", ")}`)
|
|
101
103
|
|
|
102
104
|
/** 歧义锚读面候选腿(条 5):逐候选项目——项目根形(`resolve(candidate, p)`)∥ 声明基底形
|
|
103
105
|
* (候选按名排序、并集保序——「首个可读」在此即为确定判读)。 */
|
|
@@ -149,7 +151,7 @@ export function resolveBatchDocPath(cwd, given) {
|
|
|
149
151
|
}
|
|
150
152
|
|
|
151
153
|
/**
|
|
152
|
-
* **create 面**解析(§4.15 条 2/3):绝对路径照旧取用(仍过越基底判据);相对路径 = 候选序中首个**落基底
|
|
154
|
+
* **create 面**解析(§4.15 条 2/3/6):绝对路径照旧取用(仍过越基底判据);相对路径 = 候选序中首个**落基底
|
|
153
155
|
* 根内**者(新档不存在——可读性无判别力);锚定串只解析项目根形,不落基底内 ⇒ fail-closed 新文案。
|
|
154
156
|
* **歧义锚**(条 5):目标所属项目(`owningProject`)∈ 锚候选集 ⇒ 基底 = 该项目声明面(落其基底内;
|
|
155
157
|
* 越出 ⇒ 既有 fail-closed);无所属 ⇒ **显式拒**(列候选 + 显式路径指引——不静默落锚)。
|
|
@@ -179,8 +181,26 @@ export function resolveBatchCreatePath(cwd, given, bases) {
|
|
|
179
181
|
if (!insideBases(abs, roots)) throw anchoredEscapeError(raw)
|
|
180
182
|
return abs
|
|
181
183
|
}
|
|
182
|
-
for (const abs of [resolve(base, p), resolve(batchProjectRoot(base), p)
|
|
184
|
+
for (const abs of [resolve(base, p), resolve(batchProjectRoot(base), p)]) {
|
|
183
185
|
if (insideBases(abs, roots)) return abs
|
|
184
186
|
}
|
|
187
|
+
// ③ 基底自有项目根形(#942 条 6):owningProject 可判者逐基底取值;distinct 落点 ≥2 ⇒ 拒
|
|
188
|
+
const owned = []
|
|
189
|
+
for (const b of roots) {
|
|
190
|
+
const ownRoot = owningProject(b)
|
|
191
|
+
if (!ownRoot) continue
|
|
192
|
+
const abs = resolve(ownRoot, p)
|
|
193
|
+
if (insideBases(abs, roots) && !owned.some((x) => samePath(x, abs))) owned.push(abs)
|
|
194
|
+
}
|
|
195
|
+
if (owned.length > 1) throw ambiguousLandingError(raw, owned)
|
|
196
|
+
if (owned.length === 1) return owned[0]
|
|
197
|
+
// ④ 基底相对形收窄单段(归一后无 `/` 才走原基底腿——多段串不二次拼接)
|
|
198
|
+
const leafRel = roots.length ? relative(roots[0], resolve(roots[0], p)) : ""
|
|
199
|
+
if (leafRel && !leafRel.startsWith("..") && !leafRel.includes(sep)) {
|
|
200
|
+
for (const b of roots) {
|
|
201
|
+
const abs = resolve(b, p)
|
|
202
|
+
if (insideBases(abs, roots)) return abs
|
|
203
|
+
}
|
|
204
|
+
}
|
|
185
205
|
throw outsideBasesError(raw)
|
|
186
206
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-tools/batch-read.mjs — 批次档只读读面(read-data-interface 批 · 设计档
|
|
3
|
+
* `docs/cli/design/READ-DATA-INTERFACE.md` §2.5 / §3.3 · 台账 #887)。
|
|
4
|
+
*
|
|
5
|
+
* `listBatchRecords({ cwd, bases })` —— ACP `batch/list` 与变更观察器共用的**文件集 + 段状态单源**:
|
|
6
|
+
* 文件集 = 基底根(`batchDocBases`——声明面单源)下全部 `*.md`(递归;收集器 `collectMarkdownFiles`
|
|
7
|
+
* 转导出复用);路径升序稳定;单档读错跳过(尽力面)。各段状态 = `readSectionStatusWord`(段头 /
|
|
8
|
+
* 状态行正则 / 词表全消费 batch-skeleton 单源——与冻结门字面零漂移;`§4` / `§6` = 字面「无状态词」)。
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync } from "node:fs"
|
|
11
|
+
import { basename } from "node:path"
|
|
12
|
+
|
|
13
|
+
import { collectMarkdownFiles } from "./batch-lifecycle.mjs"
|
|
14
|
+
import { batchDocBases } from "./batch-paths.mjs"
|
|
15
|
+
import { readSectionStatusWord } from "./batch-skeleton.mjs"
|
|
16
|
+
|
|
17
|
+
/** 段号表(1–6——输出映射键序固定;`readSectionStatusWord` 消费数形段号)。 */
|
|
18
|
+
const SEGMENTS = [1, 2, 3, 4, 5, 6]
|
|
19
|
+
|
|
20
|
+
/** 批次档列举(只读):`{ batches: [{ file, path, sections }] }`——`path` = 绝对路径(`/` 归一)、
|
|
21
|
+
* `file` = basename、`batches` 按路径升序;单档读错跳过(尽力面)。`bases` 缺省 = `batchDocBases(cwd)`。 */
|
|
22
|
+
export function listBatchRecords({ cwd, bases } = {}) {
|
|
23
|
+
const base = cwd ?? process.cwd()
|
|
24
|
+
const roots = bases ?? batchDocBases(base)
|
|
25
|
+
const files = new Set()
|
|
26
|
+
for (const root of roots) for (const abs of collectMarkdownFiles(root)) files.add(abs.replace(/\\/g, "/"))
|
|
27
|
+
const batches = []
|
|
28
|
+
for (const p of [...files].sort()) {
|
|
29
|
+
let src
|
|
30
|
+
try { src = readFileSync(p, "utf8") } catch { continue } // 单档读错跳过(尽力面)
|
|
31
|
+
const sections = {}
|
|
32
|
+
for (const seg of SEGMENTS) sections[`§${seg}`] = readSectionStatusWord(src, seg)
|
|
33
|
+
batches.push({ file: basename(p), path: p, sections })
|
|
34
|
+
}
|
|
35
|
+
return { batches }
|
|
36
|
+
}
|