@thincoder/core 0.10.12 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/advisor/messages.mjs +4 -2
- package/advisor/notice.mjs +1 -1
- package/advisor/project-context.mjs +121 -11
- package/agent/completion.mjs +13 -1
- package/agent/digest-account.mjs +73 -46
- package/agent/dispatch-run.mjs +1 -1
- package/agent/dispatch.mjs +29 -21
- package/agent/family-tools.mjs +9 -5
- package/agent/helpers.mjs +1 -94
- package/agent/offload.mjs +88 -0
- package/agent/record-results.mjs +1 -6
- package/agent/run-stages.mjs +74 -49
- package/agent/run-start.mjs +8 -8
- package/agent/setup.mjs +6 -11
- package/agent/suspension.mjs +1 -1
- package/agent/turn-loop.mjs +35 -3
- package/agent/write-face.mjs +99 -0
- package/agent/write-gate.mjs +354 -5
- package/agent-tools/advisor.mjs +127 -11
- package/agent-tools/async-settle.mjs +2 -2
- package/agent-tools/bash-async.mjs +8 -2
- package/agent-tools/batch-closeout.mjs +206 -0
- package/agent-tools/batch-gate.mjs +287 -73
- package/agent-tools/batch-ledger-bind.mjs +183 -0
- package/agent-tools/batch-ledger-link.mjs +73 -0
- package/agent-tools/batch-lifecycle.mjs +113 -354
- package/agent-tools/batch-paths.mjs +45 -40
- package/agent-tools/batch-read.mjs +1 -1
- package/agent-tools/batch-reopen.mjs +142 -0
- package/agent-tools/batch-skeleton.mjs +283 -80
- package/agent-tools/batch-unit-test-gate.mjs +107 -0
- package/agent-tools/batch.mjs +243 -50
- package/agent-tools/context.mjs +6 -3
- package/agent-tools/design-slots.mjs +24 -24
- package/agent-tools/design-token.mjs +96 -15
- package/agent-tools/doc-check.mjs +18 -2
- package/agent-tools/eng.mjs +1 -1
- package/agent-tools/escalate-async.mjs +3 -3
- package/agent-tools/manifest.mjs +8 -5
- package/agent-tools/read-history.mjs +5 -5
- package/agent-tools/review-facts.mjs +31 -13
- package/agent-tools/spawn-gates.mjs +10 -14
- package/agent-tools/ssh-session.mjs +120 -111
- package/agent-tools/ssh-transfer.mjs +210 -0
- package/agent-tools/ssh-transport.mjs +277 -46
- package/agent-tools/subagent-actions-query.mjs +1 -1
- package/agent-tools/subagent-actions.mjs +1 -1
- package/agent-tools/subagent-async.mjs +3 -2
- package/agent-tools/subagent-scheduler.mjs +43 -9
- package/agent-tools/subagent-spawn.mjs +68 -9
- package/agent-tools/subagent.mjs +16 -14
- package/agent.mjs +1 -0
- package/client-identity.mjs +70 -0
- package/config-presets.mjs +5 -3
- package/config.mjs +19 -13
- package/context.mjs +39 -8
- package/declaration.mjs +2 -2
- package/doc-check/anchors.mjs +128 -28
- package/doc-check/index.mjs +93 -27
- package/doc-check/line-refs.mjs +112 -0
- package/doc-check/paragraph.mjs +24 -8
- package/embedding.mjs +2 -0
- package/explore-distill.mjs +1 -1
- package/generate-title.mjs +3 -2
- package/i18n.mjs +1 -1
- package/index-discover.mjs +4 -1
- package/ledger-close-proof.mjs +62 -0
- package/ledger-cmd.mjs +79 -194
- package/ledger-db.mjs +29 -28
- package/ledger-migrate.mjs +4 -4
- package/ledger-read.mjs +10 -8
- package/ledger-surface.mjs +1 -1
- package/ledger-tools.mjs +10 -10
- package/ledger-variant-notice.mjs +1 -1
- package/ledger-write-ops.mjs +373 -0
- package/ledger.mjs +2 -2
- package/manifest-discovery.mjs +2 -2
- package/manifest-schema.mjs +4 -0
- package/memory/memory-tool.mjs +5 -2
- package/memory/schema.mjs +5 -26
- package/model-specs.mjs +6 -5
- package/package.json +1 -1
- package/path-resolve.mjs +217 -0
- package/permission.mjs +4 -0
- package/prompt-overlays.mjs +5 -0
- package/prompts/advisor-design.md +9 -6
- package/prompts/advisor-round1.md +4 -3
- package/prompts/advisor-round2.md +6 -5
- package/prompts/advisor-round3.md +6 -5
- package/prompts/common.md +23 -10
- package/prompts/consult-base.md +2 -1
- package/prompts/discipline-engineering.md +29 -48
- package/prompts/discipline-normal.md +10 -5
- package/prompts/persona-eng-coder.md +8 -0
- package/prompts/persona-eng-designer.md +22 -12
- package/prompts/persona-engineering.md +81 -66
- package/prompts/persona-plan.md +2 -1
- package/prompts/persona-tester.md +55 -0
- package/provider/anthropic.mjs +12 -7
- package/provider/core.mjs +21 -5
- package/provider/errors.mjs +41 -0
- package/provider/google.mjs +25 -9
- package/provider/list-models.mjs +4 -3
- package/provider/rate.mjs +11 -3
- package/provider/responses.mjs +17 -7
- package/provider/sse.mjs +15 -4
- package/session-identity.mjs +21 -0
- package/session-lifecycle.mjs +12 -4
- package/session-slots.mjs +9 -14
- package/session.mjs +5 -0
- package/skip-dirs.mjs +48 -0
- package/team-ledger.mjs +4 -4
- package/text-budget.mjs +4 -0
- package/token-window.mjs +107 -10
- package/tool-docs/advisor.md +2 -2
- package/tool-docs/apply_patch.md +2 -0
- package/tool-docs/bash.md +4 -0
- package/tool-docs/batch.md +85 -1
- package/tool-docs/browser.md +3 -1
- package/tool-docs/code_search.md +3 -1
- package/tool-docs/consult_start.md +6 -2
- package/tool-docs/consult_stop.md +3 -1
- package/tool-docs/delete.md +3 -0
- package/tool-docs/doc_check.md +43 -8
- package/tool-docs/doc_search.md +5 -1
- package/tool-docs/edit.md +22 -1
- package/tool-docs/execute.md +7 -1
- package/tool-docs/fetch.md +3 -1
- package/tool-docs/file_ops.md +3 -1
- package/tool-docs/git.md +9 -1
- package/tool-docs/goal.md +2 -0
- package/tool-docs/grep.md +1 -1
- package/tool-docs/hashline_edit.md +2 -0
- package/tool-docs/insert_after.md +3 -1
- package/tool-docs/ledger.md +5 -5
- package/tool-docs/lint.md +1 -0
- package/tool-docs/manifest-read.md +2 -1
- package/tool-docs/manifest.md +9 -2
- package/tool-docs/memory.md +11 -1
- package/tool-docs/notify_parent.md +1 -1
- package/tool-docs/plan.md +2 -0
- package/tool-docs/question.md +1 -1
- package/tool-docs/read_history.md +15 -3
- package/tool-docs/read_image.md +3 -1
- package/tool-docs/recent_changes.md +3 -1
- package/tool-docs/repo_outline.md +3 -1
- package/tool-docs/settings.md +10 -2
- package/tool-docs/skill.md +2 -0
- package/tool-docs/ssh.md +30 -17
- package/tool-docs/subagent.md +50 -13
- package/tool-docs/task.md +2 -0
- package/tool-docs/team_ledger.md +1 -1
- package/tool-docs/timer.md +7 -0
- package/tool-docs/verify.md +13 -1
- package/tool-docs/websearch.md +3 -1
- package/tool-docs/write.md +2 -0
- package/tools/bash.mjs +12 -2
- package/tools/edit-batch.mjs +40 -33
- package/tools/edit-diff.mjs +69 -1
- package/tools/execute.mjs +10 -4
- package/tools/file.mjs +17 -27
- package/tools/git.mjs +27 -1
- package/tools/ops.mjs +2 -1
- package/tools/patch.mjs +12 -3
- package/tools/process-doc-mark.mjs +139 -0
- package/tools/search.mjs +15 -6
- package/tools/shared.mjs +24 -1
- package/tools/ssh.mjs +48 -7
- package/tools/tree.mjs +4 -1
- package/tools/write-path.mjs +64 -3
- package/traces/trace-store.mjs +4 -3
- package/agent/light-round.mjs +0 -63
- package/agent/unbooked-pen.mjs +0 -62
package/agent-tools/batch.mjs
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* 路径解析单源 + #84 记账缝)。
|
|
4
4
|
*
|
|
5
5
|
* 单工具 `batch`,action = create(建档,§4.11)/ append(段写入——原 batch-segment.mjs 语义
|
|
6
|
-
* 逐字迁移,§4.1 全表)/
|
|
6
|
+
* 逐字迁移,§4.1 全表)/ edit(段内就地改——锚定区间替换,§4.19)/ status(段属主状态行流转,
|
|
7
|
+
* §4.12)/ close(收口冻结,§4.13)。写入面**按段分权、不按段分处置**:六段各自只有一个作者,
|
|
8
|
+
* 作者对本段可 append 加内容 ∥ edit 就地改既有行(§4.19);状态行只有一个写者(status)。
|
|
7
9
|
* 权威规格 = `docs/core/design/BATCH-RECORD.md` §4(契约/挂载/用例 §4.8/骨架 §4.10/
|
|
8
10
|
* 参数面 §4.11–§4.13);骨架与判定字面单源 = `batch-skeleton.mjs`;lifecycle 面 =
|
|
9
11
|
* `batch-lifecycle.mjs`。**依赖单向(KD-4):skeleton ← lifecycle ← 主档**。
|
|
@@ -15,8 +17,11 @@
|
|
|
15
17
|
* spawn/实例注入,语法上写不到别处——运行期门拒)。append 段白名单(**模式感知**——§4.16):
|
|
16
18
|
* depth-0 → 工程 §1/§4/§6 ∥ 普通 §1/§2/§4/§5/§6 · eng-designer → §2 · 评审 → §3 · eng-coder → §5 ·
|
|
17
19
|
* coder(普通模式分支增表——SEGMENT_BY_ROLE_NORMAL)→ §5;status 写域 = 调用者自己段(模式感知
|
|
18
|
-
* ——见 batch-lifecycle.mjs
|
|
19
|
-
*
|
|
20
|
+
* ——见 batch-lifecycle.mjs)。**edit 的段域 = 同模式 append 白名单**(§4.19「段域」行——一张表两
|
|
21
|
+
* 个动作,不另立第二张):能 append 哪段就 edit 哪段,两动作只差写法(段尾加内容 ∥ 段内改既有字)。
|
|
22
|
+
* append 迁移面错误串**逐字保持
|
|
23
|
+
* "batch_segment:" 前缀**(§4.1 全表锚断言);"batch:" 前缀 = 新增错误面(create/close/status/
|
|
24
|
+
* edit 锚判据/findInFlight/path 门/dispatch)。
|
|
20
25
|
*
|
|
21
26
|
* 导出面:`batchTool`(主名)· `resolveBatchDocPath` · `batchDocBases` · `batchDocForReview` ·
|
|
22
27
|
* `configureBatchSegment` / `resetBatchSegment`(#84 记账缝——契约名不变,AC-11 零改)·
|
|
@@ -30,18 +35,20 @@ import { readFileSync, writeFileSync } from "node:fs"
|
|
|
30
35
|
|
|
31
36
|
// 描述面 = `tool-docs/batch.md`(#15 描述外置统一——工厂形描述经 DESC 单一解析面,工厂调用期取值)
|
|
32
37
|
import { DESC } from "../tools/shared.mjs"
|
|
33
|
-
import { SEGMENT_BY_ROLE, SEGMENT_BY_ROLE_NORMAL, DEPTH0_SEGMENTS, readBatchStatusLine, sectionHeaderRe, findPlaceholderResidue, placeholderResidueError } from "./batch-skeleton.mjs"
|
|
38
|
+
import { SEGMENT_BY_ROLE, SEGMENT_BY_ROLE_NORMAL, DEPTH0_SEGMENTS, readBatchStatusLine, sectionHeaderRe, STATUS_LINE_RE, findPlaceholderResidue, placeholderResidueError, ROUND_HEADING_RE, nextRoundNumber, statusResetPlan, statusPassPlan, updateSectionStatusLine, verdictGapOf } from "./batch-skeleton.mjs"
|
|
34
39
|
import { batchDocBases, resolveBatchDocPath } from "./batch-paths.mjs"
|
|
35
40
|
import { gateReceiptLine, gateRefusalMessage, runBatchGate } from "./batch-gate.mjs"
|
|
36
|
-
import {
|
|
41
|
+
import { assertSectionFiveGate } from "./batch-closeout.mjs"
|
|
42
|
+
import { createBatchRecord } from "./batch-ledger-bind.mjs"
|
|
43
|
+
import { closeBatchRecord, findInFlightBatch, statusBatchRecord } from "./batch-lifecycle.mjs"
|
|
44
|
+
// 日期戳单源(`todayLocal`——重开 §1 状态行与本节留痕行同一枚,日期口径只一处)。
|
|
45
|
+
import { todayLocal } from "./batch-reopen.mjs"
|
|
37
46
|
|
|
38
47
|
export { SEGMENT_BY_ROLE, batchDocBases, resolveBatchDocPath }
|
|
39
48
|
|
|
40
49
|
/** text 单次上限(BATCH-RECORD.md §4.1——超出引导分段追加,不承诺"不新盖戳")。append 迁移域私有区常量
|
|
41
50
|
* (随执行体自 batch-segment.mjs 迁入——骨架档只住骨架/状态行单源,lifecycle 档头枚举口径)。 */
|
|
42
51
|
export const MAX_TEXT_CHARS = 20000
|
|
43
|
-
/** 工具写入的轮次节标题形态(N 计数口径 = 该形态行;`### 轮次与发现(…)` 骨架行不匹配)。 */
|
|
44
|
-
const ROUND_HEADING_RE = /^### 轮次 \d+(评审子代理)/
|
|
45
52
|
/** 凭证形态(`BATCH-RECORD.md` §4.1——三形):`[DESIGN-TOKEN:…]` 闭括号形 ∥ `designId: …` 键形 ∥ 无标两段
|
|
46
53
|
* token 值形 `⟨uuid⟩:⟨epoch⟩`(与单源 `design-token.mjs` 判据对表);裸 uuid 非凭证不剥(KD-7)。两字面勿
|
|
47
54
|
* 合并:`CRED_RE` 走 `.replace()` 须 `/g` ∥ `CRED_TEST_RE` 逐行 `.test()` 须非 `/g`(lastIndex 有状态会漏剥)。 */
|
|
@@ -103,6 +110,13 @@ function allowedSegment(agent, review, engineering) {
|
|
|
103
110
|
return SEGMENT_BY_ROLE[role] ?? (engineering ? null : SEGMENT_BY_ROLE_NORMAL[role]) ?? null
|
|
104
111
|
}
|
|
105
112
|
|
|
113
|
+
/** 无写权身份拒串(`append` 与 `edit` 共用一条——两动作读同一张段白名单,同一件事不各写一份串)。 */
|
|
114
|
+
function noSegmentError(engineering) {
|
|
115
|
+
return engineering
|
|
116
|
+
? "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."
|
|
117
|
+
: "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); in normal mode the parent agent writes §1/§2/§4/§5/§6 through ordinary document writes."
|
|
118
|
+
}
|
|
119
|
+
|
|
106
120
|
/**
|
|
107
121
|
* 凭证剥除 + 伪造轮次标题丢弃(BATCH-RECORD.md §4.1 F6/AC30/AC34)。
|
|
108
122
|
* 逐行处理:含凭证形态的行 → 剥除该子串,剥后为空则整行丢弃(其余内容逐字保留)。
|
|
@@ -120,25 +134,22 @@ function sanitizeText(text, dropStamp) {
|
|
|
120
134
|
return kept.join("\n")
|
|
121
135
|
}
|
|
122
136
|
|
|
123
|
-
/**
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
/** 段尾定位 + 插入(append-only:只在段尾插入,既有字节不变;返回新全文 + 本轮 N)。 */
|
|
130
|
-
function insertIntoSection(src, seg, text) {
|
|
137
|
+
/** 段尾定位 + 插入(append-only:只在段尾插入,既有字节不变;返回新全文 + 本轮 N)。
|
|
138
|
+
* `blockLead`(仅 §3——打回重置的留痕行):落在**工具戳标题之后、评审正文之前**——与
|
|
139
|
+
* `### 轮次 N(评审子代理)` 同属工具戳面,append-only 不变。
|
|
140
|
+
*/
|
|
141
|
+
function insertIntoSection(src, seg, text, blockLead = "") {
|
|
131
142
|
const hdr = sectionHeaderRe(seg).exec(src)
|
|
132
|
-
if (!hdr)
|
|
133
|
-
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.`)
|
|
134
|
-
}
|
|
143
|
+
if (!hdr) throw new Error(missingSectionError(seg))
|
|
135
144
|
const nextRe = /^## §\d/gm
|
|
136
145
|
nextRe.lastIndex = hdr.index + hdr[0].length
|
|
137
146
|
const next = nextRe.exec(src)
|
|
138
147
|
const endIdx = next ? next.index : src.length
|
|
139
148
|
// 来源戳(仅 §3):工具生成标题 + N——调用方写不进自己的标题(sanitize 已丢弃同名行)。
|
|
140
|
-
|
|
141
|
-
const
|
|
149
|
+
// N 的单源 = `nextRoundNumber`(= `countRoundBlocks + 1`)——留痕行的「第 N 轮」同取这一枚。
|
|
150
|
+
const roundN = seg === 3 ? nextRoundNumber(src, 3) : 0
|
|
151
|
+
const stamp = seg === 3 ? `### 轮次 ${roundN}(评审子代理)\n\n` + (blockLead ? `${blockLead}\n\n` : "") : ""
|
|
152
|
+
const payload = stamp + text
|
|
142
153
|
const before = src.slice(0, endIdx)
|
|
143
154
|
const after = src.slice(endIdx)
|
|
144
155
|
const eol = src.includes("\r\n") ? "\r\n" : "\n"
|
|
@@ -156,20 +167,17 @@ function insertIntoSection(src, seg, text) {
|
|
|
156
167
|
* - 目标定位(D-BR21):depth-0 path 可选(缺省 = findInFlightBatch 唯一在飞批);子代理/评审
|
|
157
168
|
* 传 path ⇒ 拒,目标 = spawn/实例绑定;
|
|
158
169
|
* - 其余(gate / text 校验 / 剥凭证 / 骨架保护 / **死占位机检(F11-C)** / **文档机检门(§3.20 结构兜底 ①——
|
|
159
|
-
*
|
|
170
|
+
* **全部 append 同挂**(含 depth-0 与评审写 §3):本次写入文本的命中 ⇒ 拒写,ERROR ⇒ 写 + 回执明示;
|
|
171
|
+
* 批档已落段命中只入回执)** / 插入 / 记账 / 回执)逐字保持原语义。
|
|
160
172
|
*/
|
|
161
173
|
function appendBatchRecord({ args, ctx, review, batchDoc, onWritten }) {
|
|
162
174
|
const agent = ctx?.agent ?? {}
|
|
163
|
-
const cwd = agent.cwd
|
|
175
|
+
const cwd = agent.cwd
|
|
164
176
|
const depth0 = ctx?.depth === 0
|
|
165
177
|
// 模式位执行期实读(§4.16——同款先例 = task.mjs execute 门;翻转即时生效——D-BR26)
|
|
166
178
|
const engineering = !!agent?.config?.agent?.engineering
|
|
167
179
|
const seg = allowedSegment(agent, review, engineering)
|
|
168
|
-
if (!depth0 && seg === null)
|
|
169
|
-
throw new Error(engineering
|
|
170
|
-
? "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."
|
|
171
|
-
: "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); in normal mode the parent agent writes §1/§2/§4/§5/§6 through ordinary document writes.")
|
|
172
|
-
}
|
|
180
|
+
if (!depth0 && seg === null) throw new Error(noSegmentError(engineering))
|
|
173
181
|
const n = segmentNumber(args?.segment)
|
|
174
182
|
if (n === null) {
|
|
175
183
|
if (depth0) {
|
|
@@ -207,21 +215,15 @@ function appendBatchRecord({ args, ctx, review, batchDoc, onWritten }) {
|
|
|
207
215
|
}
|
|
208
216
|
const src = readFileSync(abs, "utf8")
|
|
209
217
|
const status = readBatchStatusLine(src)
|
|
210
|
-
if (status === "closed")
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
if (status === "unknown") {
|
|
214
|
-
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. Do NOT close the record after a refused append — nothing landed, and closing freezes the missing section for good (失败 ⇒ 勿 close).")
|
|
215
|
-
}
|
|
218
|
+
if (status === "closed") throw new Error(FROZEN_RECORD_ERROR)
|
|
219
|
+
if (status === "unknown") throw new Error(UNRESOLVED_STATUS_ERROR)
|
|
216
220
|
if (typeof args?.text !== "string") {
|
|
217
221
|
throw new Error("batch_segment: text must be a string (the markdown to append).")
|
|
218
222
|
}
|
|
219
223
|
if (args.text.length > MAX_TEXT_CHARS) {
|
|
220
224
|
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 顺延).`)
|
|
221
225
|
}
|
|
222
|
-
if (/^## §\d/m.test(args.text))
|
|
223
|
-
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. Do NOT close the record after a refused append — nothing landed, and closing freezes the missing section for good (失败 ⇒ 勿 close).")
|
|
224
|
-
}
|
|
226
|
+
if (/^## §\d/m.test(args.text)) throw new Error(SKELETON_GUARD_ERROR)
|
|
225
227
|
const body = sanitizeText(args.text, n === 3).replace(/^\n+/, "").replace(/\s+$/, "")
|
|
226
228
|
if (!body.trim()) {
|
|
227
229
|
throw new Error("batch_segment: nothing to append — the text is empty after credential stripping (credential values never reach the record).")
|
|
@@ -230,21 +232,198 @@ function appendBatchRecord({ args, ctx, review, batchDoc, onWritten }) {
|
|
|
230
232
|
// (段体引用豁免——判据可辨性收正,`TOOLS.md` §6.20;骨架枚举单源 = skeleton 档 TEMPLATE_PLACEHOLDERS)。
|
|
231
233
|
const residues = findPlaceholderResidue(src)
|
|
232
234
|
if (residues.length) throw new Error(placeholderResidueError(residues))
|
|
235
|
+
// §5 写入门(段序门 · 设计档 §2.6 `R-G2`):实现发生在评审通过之后,序在这里判得出真假;这一门没有旁路
|
|
236
|
+
// (`§1` 声明 §3 不适用已随声明机制退役)。
|
|
237
|
+
if (n === 5) assertSectionFiveGate(src)
|
|
238
|
+
// §3 写入闸(设计档 `BATCH-LEDGER-REVIEW-UNIFY.md` §2.6.1 `R-G3`——裁定行在写的时刻就要在):
|
|
239
|
+
// 能写 §3 的只有绑本档的评审实例,它结算后即离场 ⇒ 把错误退回**还有权补那一行的人在场的那一刻**。
|
|
240
|
+
// 判定输入 = 净化后正文 `body`(落档的就是它;每笔 §3 自成一块 ⇒ 「写前正文 ≡ 写后末块」只对 body 成立);
|
|
241
|
+
// 判据单源 = 骨架档 `verdictGapOf`(与 §5 门判据 2 ∥ 签发面同一个函数)。
|
|
242
|
+
// 放行面 = **只拒两格**(`无裁定行` ∥ `两者同携`):判打回的一笔(`VERDICT: changes-required` ∥ `结论:不通过`)
|
|
243
|
+
// 照写——§5 门判据 2 与打回重置都靠那一块自带打回裁定;拦它 = 把打回这条业务路堵死。
|
|
244
|
+
// 位次 = 既有拒因之后、文档机检门之前(拒 = 零字节落盘、轮次号不消耗——N 在 insertIntoSection 时才取)。
|
|
245
|
+
if (n === 3) {
|
|
246
|
+
const gap = verdictGapOf(body)
|
|
247
|
+
if (gap === "无裁定行" || gap === "两者同携") throw new Error(SECTION3_VERDICT_GAP_ERROR)
|
|
248
|
+
}
|
|
233
249
|
// 文档机检门(§3.20「结构兜底」①——门位 = 既有拒因全部之后、insertIntoSection / 写盘之前):
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
250
|
+
// **全部 append 同挂**(§1–§6,含 depth-0 与评审写 §3——规则一条,不按身份分支);
|
|
251
|
+
// 强制集 = 本次写入文本 ∪ 写者触达账,批档已落段命中只入回执(不阻本次写)。
|
|
252
|
+
// `pending` = 本次段号与正文:门据此落同目录片段档判「你手里这段」,判法与批档本体逐字同源。
|
|
253
|
+
// REFUSED ⇒ throw(零字节落盘);PASS ⇒ 照写;ERROR / 门自身异常 ⇒ 写 + 回执明示。
|
|
254
|
+
const gate = runBatchGate({ abs, cwd, agent, scope: "writer", pending: { section: n, body } })
|
|
238
255
|
if (gate?.state === "refused") throw new Error(gateRefusalMessage(gate, "append"))
|
|
239
|
-
|
|
256
|
+
// 裁定行 → 状态行(BLRU §2.5——**工具被动写**,两个方向共用同一处判定与同一条写入路径):
|
|
257
|
+
// 打回即重置(三合取:本笔写 §3 ∧ 本笔命中打回裁定行 ∧ 写前 §3 状态行 = 评审通过)⇒ 本轮块首留痕行
|
|
258
|
+
// (随 `blockLead` 进插入)+ 状态行重置;通过即置位(本笔写 §3 ∧ 本笔命中 pass 裁定行且不命中打回行 ∧
|
|
259
|
+
// 写前 §3 状态行 ≠ 评审通过)⇒ 状态行置成评审通过,无留痕行(块本身即记录)。
|
|
260
|
+
// 两条靠各自的三合取天然互斥;`??` 的左先右后与签发面 `verdictGateFailure` 同序(先打回、后 pass)。
|
|
261
|
+
// 状态行仍**只经 `updateSectionStatusLine` 这一个写入函数**改写(不新开第二条写路径);仍是
|
|
262
|
+
// 一次 `writeFileSync`——块与字进的是同一个字符串(无「块已落、字未到」的窗口)。
|
|
263
|
+
// 留痕行不经文档机检门——门位在上面(判集 = 本次评审正文 `pending.body`)。
|
|
264
|
+
const date = todayLocal()
|
|
265
|
+
const plan = statusResetPlan(src, n, body, date) ?? statusPassPlan(src, n, body, date)
|
|
266
|
+
const { written: inserted, roundN } = insertIntoSection(src, n, body, plan?.traceLine ?? "")
|
|
267
|
+
const written = plan ? updateSectionStatusLine(inserted, 3, plan.statusValue) : inserted
|
|
240
268
|
writeFileSync(abs, written)
|
|
241
269
|
// #84:记账面按端注入(缺省 no-op = CLI 语义零行为变——端装配经 configureBatchSegment 覆盖)。
|
|
242
270
|
onWritten?.(agent, abs)
|
|
243
271
|
return `batch_segment: appended ${body.length} characters to §${n} of the batch record${roundN ? ` (### 轮次 ${roundN}(评审子代理))` : ""}.${gateReceiptLine(gate)}`
|
|
244
272
|
}
|
|
245
273
|
|
|
274
|
+
// ─── 拒串单源(append / edit 共用——两动作只差写法,不差判据)───────────────
|
|
275
|
+
|
|
276
|
+
/** 拒串:`§3` 写入闸(`R-G3` 逐字 = `docs/core/design/BATCH-LEDGER-REVIEW-UNIFY.md` §2.6.1)——一笔 `§3` 追加读成
|
|
277
|
+
* 「无裁定行」∥「两者同携」⇒ 拒写(零字节落盘、轮次号不消耗)。两个裁定词**内联在同一句里,不各占独立一行**:
|
|
278
|
+
* 各占一行时这段拒文自己就读成「两者同携」,把拒文原样引用并另写一行裁定的一笔仍被拒——想解释这道闸的人恰被
|
|
279
|
+
* 它挡在门外;缩进 ∥ 列表符同样不取(照抄的行不命中整行锚 ⇒ 补的那行永不生效)。混形反例与「不消耗轮次号」照留。 */
|
|
280
|
+
const SECTION3_VERDICT_GAP_ERROR = "batch_segment: §3 append refused — this round block states no readable verdict (块内裁定 = 无裁定行 | 两者同携). A §3 block must carry exactly one verdict as a line of its own; put exactly one of `VERDICT: pass` ∥ `VERDICT: changes-required` on such a line in the text you append — copying this refusal verbatim copies the words, not a verdict line.\n(本地化整行形 `结论:通过` / `结论:不通过` 同认,中文形的尾句号可省可带(`结论:通过。` 同认)。不作独立整行的写法不认——例如 `**结论** = VERDICT: pass`。) Nothing was written — no bytes landed and no round number was consumed: append the same text again with the verdict line added."
|
|
281
|
+
/** 拒串:已收口档(§4.9 门 ②——append / edit / status 共用一条,串逐字含「已收口档不回改」)。 */
|
|
282
|
+
const FROZEN_RECORD_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."
|
|
283
|
+
/** 拒串:§1 状态行不可解析或缺失(fail-closed 视为冻结——与冻结门同一条解析器)。 */
|
|
284
|
+
const UNRESOLVED_STATUS_ERROR = "batch_segment: 状态行不可解析或缺失 — the bound batch record has no §1 `**状态行**:` line inside the section header zone whose value contains 已收口 or 进行中 (fail-closed: the write is refused as if frozen). If the record does carry such a line further down — in a round block or in the section body — move it up under `## §1`: a status line outside the header zone is never read. Nothing was written. Do NOT close the record after a refused append — nothing landed, and closing freezes the missing section for good (失败 ⇒ 勿 close)."
|
|
285
|
+
/** 拒串:段标题缺失(create 是骨架唯一权威入口,append / edit 都不补骨架)。 */
|
|
286
|
+
const missingSectionError = (seg) => `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.`
|
|
287
|
+
/** 拒串:骨架保护(写入文本 ∥ 替换文本含 `## §N` 段头形态 ⇒ 段定位失效——append / edit 共用)。
|
|
288
|
+
* 两个动作都不许把段头写进段体:段定位靠 `## §N` 标题,写进去就把定位面改掉(§4.1 fail-closed 行)。 */
|
|
289
|
+
const SKELETON_GUARD_ERROR = "batch_segment: the text contains a section header line matching `^## §N` — that would break section location. Rewrite it (escape the heading, or drop the leading `## §N`), then call again (骨架保护). Nothing was written. Do NOT close the record after a refused append — nothing landed, and closing freezes the missing section for good (失败 ⇒ 勿 close)."
|
|
290
|
+
/** 拒串:edit 的锚 / 替换触及或引入 `**状态行**:` 行(§4.19 锚判据;逐字 = `TOOLS.md` §6.24 ⑤ E5)。 */
|
|
291
|
+
const EDIT_STATUS_LINE_ERROR = "batch: edit would rewrite or introduce a **状态行** line — that line has exactly one writer: status, with a value from the section's word list. Nothing was written."
|
|
292
|
+
/** 拒串:edit 空锚(E1——空锚的命中计数不会 ≥2,按「恰一次」会放行并插在段首)。 */
|
|
293
|
+
const EDIT_BLANK_ANCHOR_ERROR = "batch: edit needs a non-empty oldText — a blank anchor cannot match exactly once. Nothing was written."
|
|
294
|
+
/** 拒串:edit 的 newText 缺参 / 非串(`type: string` 的 fail-closed 面——缺参若当空串处理就是静默删区间)。 */
|
|
295
|
+
const EDIT_NO_TEXT_ERROR = "batch: newText must be a string — pass an empty string to delete the anchored range. Nothing was written."
|
|
296
|
+
|
|
297
|
+
// ─── edit 面(段内就地改:`BATCH-RECORD.md` §4.19 的代码单源)─────────────────────
|
|
298
|
+
|
|
299
|
+
/** 段体**字符区间**(`## §N` 标题行的行尾之后 → 下一 `## §N` 段头之前;段头缺失 ⇒ null)。
|
|
300
|
+
* 段定位判据与 `batch-skeleton.mjs` 同源(`sectionHeaderRe` + `/^## §\d/gm`)——本档只取**偏移**。
|
|
301
|
+
* 区间函数住这里而不进骨架档:骨架档贴 500 软线,本批不带出助手函数(分批记录 2.4 贴线判)。 */
|
|
302
|
+
function sectionBodySpan(src, seg) {
|
|
303
|
+
const hdr = sectionHeaderRe(seg).exec(src)
|
|
304
|
+
if (!hdr) return null
|
|
305
|
+
const nextRe = /^## §\d/gm
|
|
306
|
+
nextRe.lastIndex = hdr.index + hdr[0].length
|
|
307
|
+
const next = nextRe.exec(src)
|
|
308
|
+
const end = next ? next.index : src.length
|
|
309
|
+
const nl = src.indexOf("\n", hdr.index) // 段头行余文(`## §N …` 的标题部分)不入段体
|
|
310
|
+
return { start: nl < 0 || nl + 1 > end ? end : nl + 1, end }
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** 段体内每条 `**状态行**:` 行的字符区间(相对段体起点)——「状态行只有一个写者」的判定面(E5)。 */
|
|
314
|
+
function statusLineSpans(body) {
|
|
315
|
+
const out = []
|
|
316
|
+
let at = 0
|
|
317
|
+
for (const line of body.split("\n")) {
|
|
318
|
+
if (STATUS_LINE_RE.test(line)) out.push([at, at + line.length])
|
|
319
|
+
at += line.length + 1
|
|
320
|
+
}
|
|
321
|
+
return out
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* edit 执行体(action=edit——作者对自己那一段的**受认可通道**,不是唯一通道;判据单源 = `docs/core/design/BATCH-RECORD.md` §4.19):
|
|
326
|
+
* 锚定区间替换——`oldText` 在**目标段段体**内恰一次命中(逐字节,含行尾)⇒ 换为 `newText`;`newText` 空串 = 删区间。
|
|
327
|
+
* 判定与写盘**全部复用既有面**(零新写盘路径):段域 = 同模式 append 白名单(§4.1 一处单源,一张表两个动作)· path 门(D-BR21)·
|
|
328
|
+
* 冻结拒写(§4.9 门 ②,串与 append 逐字同)· 剥凭证(同一条 `sanitizeText`;§3 另丢伪造轮次戳——§4.7 不变量 5)· 骨架保护 ·
|
|
329
|
+
* 文档机检门(**判集两类**:段长类判 `newText`、锚类判整档——§4.9 门 ④)· `onWrite` 记账 · 一次 `writeFileSync`。
|
|
330
|
+
* **不跑 §5 段序门**(§4.19「段序门」行:edit 须有既有锚 ⇒ 写不出原本不存在的内容,造不出「先实施」;
|
|
331
|
+
* 反过来在 §3 进行中时拒掉 §5 的旧行,正是本批要消的那种死锁)。
|
|
332
|
+
* **不做整段替换**(段体实测两成超 `MAX_TEXT_CHARS`——重发全文 = 静默丢内容)∥ **不做模糊匹配**(一次近似改写 = 一次
|
|
333
|
+
* 没人看见的错改;匹配 = 逐字节,`\r\n` 档上的 LF 锚零命中 ⇒ 拒,安全方向)。
|
|
334
|
+
* 拒面(全 fail-closed,零字节落盘):空锚 E1 · 零命中 E2 · ≥2 命中(列命中行号)E3 · 锚越段界 E4 · 锚或替换触及 / 引入
|
|
335
|
+
* `**状态行**:` 行 E5 · 骨架保护 ∥ 冻结 ∥ 段域 ∥ newText 缺参(E6 族)。
|
|
336
|
+
*/
|
|
337
|
+
function editBatchRecord({ args, ctx, review, batchDoc, onWritten }) {
|
|
338
|
+
const agent = ctx?.agent ?? {}
|
|
339
|
+
const cwd = agent.cwd
|
|
340
|
+
const depth0 = ctx?.depth === 0
|
|
341
|
+
const engineering = !!agent?.config?.agent?.engineering
|
|
342
|
+
// 段域(§4.19「段域」行):**= 同模式 append 白名单**——能 append 哪段就 edit 哪段,段域一处单源、一张表两个动作。
|
|
343
|
+
// 子代理侧取身份段(§4.1:身份 → 段);depth-0 侧取同模式 append 白名单(工程 §1/§4/§6 ∥ 普通 §1/§2/§4/§5/§6,§4.16)。
|
|
344
|
+
const own = allowedSegment(agent, review, engineering)
|
|
345
|
+
if (!depth0 && own === null) throw new Error(noSegmentError(engineering))
|
|
346
|
+
const n = segmentNumber(args?.segment)
|
|
347
|
+
if (n === null) {
|
|
348
|
+
throw new Error(depth0
|
|
349
|
+
? `batch: unknown segment ${JSON.stringify(args?.segment ?? null)} — depth-0 edit rewrites a section of the in-flight record that you author (${engineering ? "§1/§4/§6" : "§1/§2/§4/§5/§6"} — pass the section number you write, e.g. "§4"). Nothing was written.`
|
|
350
|
+
: `batch_segment: unknown segment ${JSON.stringify(args?.segment ?? null)} — pass the section number you write (e.g. "§${own}").`)
|
|
351
|
+
}
|
|
352
|
+
if (depth0) {
|
|
353
|
+
const domain = engineering ? DEPTH0_SEGMENTS.append.engineering : DEPTH0_SEGMENTS.append.normal
|
|
354
|
+
if (!domain.includes(n)) {
|
|
355
|
+
throw new Error(engineering
|
|
356
|
+
? `batch: §${n} is not yours to write — in engineering mode depth-0 edit rewrites §1/§4/§6 (一段一作者: §2 = eng-designer, §3 = design review, §5 = eng-coder). Nothing was written.`
|
|
357
|
+
: `batch: §${n} is not yours to write — in normal mode depth-0 edit rewrites §1/§2/§4/§5/§6 (一段一作者: §3 = design review only). Nothing was written.`)
|
|
358
|
+
}
|
|
359
|
+
} else if (n !== own) {
|
|
360
|
+
throw new Error(`batch_segment: §${n} is not yours to write — this caller writes §${own} only (一段一作者: eng-designer → §2, design review → §3, eng-coder → §5). Nothing was written.`)
|
|
361
|
+
}
|
|
362
|
+
// 目标定位(D-BR21)——与 append / status 同序:段域 → path 门 → 取值。
|
|
363
|
+
const givenPath = typeof args?.path === "string" && args.path.trim() ? args.path : null
|
|
364
|
+
let abs
|
|
365
|
+
if (givenPath) {
|
|
366
|
+
if (!depth0) {
|
|
367
|
+
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.")
|
|
368
|
+
}
|
|
369
|
+
abs = resolveBatchDocPath(cwd, givenPath)
|
|
370
|
+
} else if (depth0) {
|
|
371
|
+
abs = findInFlightBatch(cwd, batchDocBases(cwd)) // 歧义锚 ⇒ 基底 = 候选并集(#828——唯一在飞批照取 / ≥2 照列)
|
|
372
|
+
} else {
|
|
373
|
+
if (!batchDoc) {
|
|
374
|
+
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.")
|
|
375
|
+
}
|
|
376
|
+
abs = resolveBatchDocPath(cwd, batchDoc)
|
|
377
|
+
}
|
|
378
|
+
const src = readFileSync(abs, "utf8")
|
|
379
|
+
const status = readBatchStatusLine(src)
|
|
380
|
+
if (status === "closed") throw new Error(FROZEN_RECORD_ERROR)
|
|
381
|
+
if (status === "unknown") throw new Error(UNRESOLVED_STATUS_ERROR)
|
|
382
|
+
const oldText = args?.oldText
|
|
383
|
+
if (typeof oldText !== "string" || !oldText.trim()) throw new Error(EDIT_BLANK_ANCHOR_ERROR)
|
|
384
|
+
if (typeof args?.newText !== "string") throw new Error(EDIT_NO_TEXT_ERROR)
|
|
385
|
+
if (/^## §\d/m.test(args.newText)) throw new Error(SKELETON_GUARD_ERROR)
|
|
386
|
+
const span = sectionBodySpan(src, n)
|
|
387
|
+
if (!span) throw new Error(missingSectionError(n))
|
|
388
|
+
const body = src.slice(span.start, span.end)
|
|
389
|
+
const hits = []
|
|
390
|
+
for (let at = body.indexOf(oldText); at >= 0; at = body.indexOf(oldText, at + 1)) hits.push(at)
|
|
391
|
+
if (!hits.length) {
|
|
392
|
+
// 零命中 vs 越段界:全档别处有 ⇒ 锚越出目标段段体(E4);全档无 ⇒ 真零命中(E2)。两者都 fail-closed:
|
|
393
|
+
// 「删那行作废判据」若零命中放行,会报成功而一字未删——比不做更坏。
|
|
394
|
+
throw new Error(src.includes(oldText)
|
|
395
|
+
? `batch: edit anchor crosses the §${n} section boundary — it starts outside the section body or reaches the next §N header, and rewriting across it would break section location. Nothing was written.`
|
|
396
|
+
: `batch: edit anchor not found — oldText must match the §${n} body exactly once, byte for byte (line endings included). Nothing was written. Re-read the section and pass its current text verbatim.`)
|
|
397
|
+
}
|
|
398
|
+
if (hits.length > 1) {
|
|
399
|
+
const lines = hits.map((at) => src.slice(0, span.start + at).split("\n").length).join(", ")
|
|
400
|
+
throw new Error(`batch: edit anchor is not unique — it matches ${hits.length} places inside §${n} (lines ${lines}). Extend the anchor until it matches once. Nothing was written.`)
|
|
401
|
+
}
|
|
402
|
+
const hit = hits[0]
|
|
403
|
+
// 状态行只有一个写者(status):锚**触及**该行 ∥ 替换**引入**一条 ⇒ 拒(否则一次 edit 就能伪造 status 的生命周期流转)。
|
|
404
|
+
const spans = statusLineSpans(body)
|
|
405
|
+
if (spans.some(([s, e]) => hit < e && hit + oldText.length > s)) throw new Error(EDIT_STATUS_LINE_ERROR)
|
|
406
|
+
if (args.newText.split(/\r?\n/).some((line) => STATUS_LINE_RE.test(line))) throw new Error(EDIT_STATUS_LINE_ERROR)
|
|
407
|
+
// 替换文本落盘取目标档的 EOL 形(`sanitizeText` 逐行处理后以 LF 相连;同 append 的 `insertIntoSection` 口径)。
|
|
408
|
+
// 来源戳只有工具能盖(§4.7 不变量 5)⇒ `edit §3` 的 `newText` 同 append 丢伪造轮次戳行;其余段无戳可丢。
|
|
409
|
+
const eol = src.includes("\r\n") ? "\r\n" : "\n"
|
|
410
|
+
const replacement = sanitizeText(args.newText, n === 3).replaceAll("\n", eol)
|
|
411
|
+
const newBody = body.slice(0, hit) + replacement + body.slice(hit + oldText.length)
|
|
412
|
+
const postEdit = src.slice(0, span.start) + newBody + src.slice(span.end)
|
|
413
|
+
// 文档机检门(§4.9 门 ④——**判集按命中类分两套**):**段长类**(段落过长 · 行过长)判本次写入的文本 = `newText`
|
|
414
|
+
// (一条早先就在的超长段不再拦本段的无关改动);**锚类**(档缺 · 节缺 · 引缺)判**整档**——引用完整性是档级属性。
|
|
415
|
+
// 「整档」= **改写后的**档(`recordView`):#2021 的现场就是「那条死引文被这一次 edit 换掉」,拿改写前的盘上字节判,
|
|
416
|
+
// 那一行永远过不去(本批要消的就是那个死锁)。REFUSED ⇒ 拒(零字节落盘)∥ ERROR ⇒ 写 + 回执明示。
|
|
417
|
+
const gate = runBatchGate({ abs, cwd, agent, scope: "writer", pending: { section: n, body: replacement, recordView: postEdit } })
|
|
418
|
+
if (gate?.state === "refused") throw new Error(gateRefusalMessage(gate, "edit"))
|
|
419
|
+
writeFileSync(abs, postEdit)
|
|
420
|
+
// #84:记账面按端注入(与 append 同一条缝——缺省 no-op = CLI 语义零行为变)。
|
|
421
|
+
onWritten?.(agent, abs)
|
|
422
|
+
return `batch: edited §${n} of the batch record — replaced ${oldText.length} characters with ${replacement.length}.${gateReceiptLine(gate)}`
|
|
423
|
+
}
|
|
424
|
+
|
|
246
425
|
/**
|
|
247
|
-
* 批次档生命周期工具工厂(主名 `batch
|
|
426
|
+
* 批次档生命周期工具工厂(主名 `batch`——单工具五 action)。
|
|
248
427
|
* @param {string|null} batchDoc — 绑定的目标档(spawn:`child._batchDoc`;评审:实例键;
|
|
249
428
|
* depth-0 主 agent 挂载时 = null——目标走 D-BR21 可选 path / 在飞扫描)
|
|
250
429
|
* @param {{review?: boolean}} [opts] — review=true 表示"设计评审实例"形态(写 §3 + 工具盖戳)
|
|
@@ -258,8 +437,8 @@ export function batchTool(batchDoc = null, { review = false } = {}) {
|
|
|
258
437
|
properties: {
|
|
259
438
|
action: {
|
|
260
439
|
type: "string",
|
|
261
|
-
enum: ["create", "append", "status", "close"],
|
|
262
|
-
description: "Which lifecycle step to run: create (new record) / append (your section) / status (your section's status line) / close (freeze the record).",
|
|
440
|
+
enum: ["create", "append", "edit", "status", "close"],
|
|
441
|
+
description: "Which lifecycle step to run: create (new record) / append (add text to the END of your section) / edit — replace one anchored range inside YOUR OWN section (§1–§6 alike; your identity decides which section) / status (your section's status line) / close (freeze the record).",
|
|
263
442
|
},
|
|
264
443
|
path: {
|
|
265
444
|
type: "string",
|
|
@@ -267,15 +446,23 @@ export function batchTool(batchDoc = null, { review = false } = {}) {
|
|
|
267
446
|
},
|
|
268
447
|
segment: {
|
|
269
448
|
type: "string",
|
|
270
|
-
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 (engineering) / append §1/§2/§4/§5/§6 · status §1/§2/§5 (normal); eng-designer §2; eng-coder §5; normal-mode coder with a batchDoc binding §5; design review §3).",
|
|
449
|
+
description: "append/edit/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/edit §1/§4/§6 · status §1 (engineering) / append/edit §1/§2/§4/§5/§6 · status §1/§2/§5 (normal); eng-designer §2; eng-coder §5; normal-mode coder with a batchDoc binding §5; design review §3). edit reads the same whitelist as append — one table, two actions: the section you may append is the section you may rewrite in place.",
|
|
271
450
|
},
|
|
272
451
|
text: {
|
|
273
452
|
type: "string",
|
|
274
453
|
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).",
|
|
275
454
|
},
|
|
455
|
+
oldText: {
|
|
456
|
+
type: "string",
|
|
457
|
+
description: "edit only — oldText must match exactly once, byte for byte (line endings included), inside the section body; newText replaces that range, and an empty newText deletes it. Refused with nothing written: a blank oldText, zero hits, two or more hits (the matching lines are named), an anchor crossing the section boundary, and any anchor or replacement that touches or introduces a `**状态行**:` line (the status line has one writer: `status`).",
|
|
458
|
+
},
|
|
459
|
+
newText: {
|
|
460
|
+
type: "string",
|
|
461
|
+
description: "edit only — the replacement text for the anchored range; an empty string deletes the range. Credentials are stripped mechanically (same as append); a `## §N` heading line is refused (skeleton protection), and on §3 a `### 轮次 N(评审子代理)` line is dropped (the round stamp is tool-generated only).",
|
|
462
|
+
},
|
|
276
463
|
value: {
|
|
277
464
|
type: "string",
|
|
278
|
-
description: "status only — the new status-line value (single line; exactly one legal keyword of your section's STATUS_WORDS entry: §1 进行中/已收口 · §2 设计完成 · §3
|
|
465
|
+
description: "status only — the new status-line value (single line; exactly one legal keyword of your section's STATUS_WORDS entry: §1 进行中/已收口 · §2 设计完成 · §3 评审通过 · §5 实施完成 — plus decoration only: surrounding symbols / a trailing ISO date / whitespace; prose goes to `note`).",
|
|
279
466
|
},
|
|
280
467
|
note: {
|
|
281
468
|
type: "string",
|
|
@@ -291,11 +478,15 @@ export function batchTool(batchDoc = null, { review = false } = {}) {
|
|
|
291
478
|
},
|
|
292
479
|
ledger: {
|
|
293
480
|
type: "string",
|
|
294
|
-
description: "create only — optional:
|
|
481
|
+
description: "create only — optional: an existing ledger entry number to take over (e.g. \"#1830\"; a leading # is optional). Omit it and create opens a NEW ledger row in the same call (status 在途 · task_book = this record · trigger 归批) — but only when no unsettled row exists on the same face (board); otherwise create is refused and lists the candidates, and you pass either ledger:\"#<id>\" to take one over or newDebt:\"<why this is not one of them>\". Pass an archived / settled / foreign-owned row and create is refused. create and the ledger row are one action — there is no separate registration step.",
|
|
482
|
+
},
|
|
483
|
+
newDebt: {
|
|
484
|
+
type: "string",
|
|
485
|
+
description: "create only — optional: one line saying why this batch is a NEW debt rather than one of the unsettled rows already on the same face (board). Required only when you omit ledger while that face has unsettled rows; the reason lands in the new row's evidence and is quoted in this record's closeout receipt. Omit both ledger and newDebt while unsettled rows exist ⇒ create is refused with the candidate list.",
|
|
295
486
|
},
|
|
296
487
|
board: {
|
|
297
488
|
type: "string",
|
|
298
|
-
description: "create only — optional: the owning board for the header's 台账 line (e.g. \"core\").
|
|
489
|
+
description: "create only — optional: the owning board for the header's 台账 line (e.g. \"core\"). Given, the opened/taken-over row is a requirement and carries this board; omitted, the row is a tech_todo and the header 括注 shows 「—」.",
|
|
299
490
|
},
|
|
300
491
|
date: {
|
|
301
492
|
type: "string",
|
|
@@ -310,7 +501,7 @@ export function batchTool(batchDoc = null, { review = false } = {}) {
|
|
|
310
501
|
},
|
|
311
502
|
async execute(args, ctx) {
|
|
312
503
|
const agent = ctx?.agent ?? {}
|
|
313
|
-
const cwd = agent.cwd
|
|
504
|
+
const cwd = agent.cwd
|
|
314
505
|
const bases = batchDocBases(cwd) // 歧义锚 ⇒ 候选项目基底并集(#828——create / 在飞 / 写门基底集同源)
|
|
315
506
|
const onWritten = injectedOnWrite
|
|
316
507
|
/** depth-0 目标定位(D-BR21):path 传 ⇒ 可读性门;缺省 ⇒ 唯一在飞批(depth-0)/
|
|
@@ -332,12 +523,14 @@ export function batchTool(batchDoc = null, { review = false } = {}) {
|
|
|
332
523
|
return createBatchRecord({ args, ctx, review, cwd, bases, onWritten })
|
|
333
524
|
case "append":
|
|
334
525
|
return appendBatchRecord({ args, ctx, review, batchDoc, onWritten })
|
|
526
|
+
case "edit":
|
|
527
|
+
return editBatchRecord({ args, ctx, review, batchDoc, onWritten })
|
|
335
528
|
case "status":
|
|
336
529
|
return statusBatchRecord({ args, ctx, review, pickTarget, onWritten })
|
|
337
530
|
case "close":
|
|
338
531
|
return closeBatchRecord({ args, ctx, review, pickTarget, onWritten })
|
|
339
532
|
default:
|
|
340
|
-
throw new Error(`batch: unknown action ${JSON.stringify(args?.action ?? null)} — pass one of create / append / status / close.`)
|
|
533
|
+
throw new Error(`batch: unknown action ${JSON.stringify(args?.action ?? null)} — pass one of create / append / edit / status / close.`)
|
|
341
534
|
}
|
|
342
535
|
},
|
|
343
536
|
}
|
package/agent-tools/context.mjs
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
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` 不新增第三口径(首行主口径 = 窗口占用 total / window,压缩线以具名数值 +
|
|
6
|
+
* 机械面:`stats` 不新增第三口径(首行主口径 = 窗口占用 total / window,压缩线以具名数值 + 距线呈现;状态行行(第 6 行)走核 `historyReading`——与 CLI ∥ VSC 同源:一个实现点,端侧零自算式);`prune` = 合格集(`token-window.mjs` 单源)+「原位换内容」应用面(`context.mjs`);`compact` **当次零执行**(只登记 `agent._pendingCompact`,由下一安全点消费——§6.16.2)。
|
|
7
7
|
*/
|
|
8
|
-
import { contextUsage, estimateTokens, collectStaleToolOutputs,
|
|
8
|
+
import { contextUsage, estimateTokens, collectStaleToolOutputs, historyReading, tokenRatioOf, PRUNE_MIN_TOKENS } from "../token-window.mjs"
|
|
9
9
|
import { requestBudget } from "../config.mjs"
|
|
10
10
|
import { pruneStaleToolOutputs, COMPRESS_FAILURE_LIMIT } from "../context.mjs"
|
|
11
11
|
import { slotPath } from "../session-slots.mjs"
|
|
@@ -89,6 +89,9 @@ function statsReport(agent) {
|
|
|
89
89
|
const cacheLine = cacheTurns === 0
|
|
90
90
|
? "Cache: no provider usage readings yet (hit/miss arrive with the first response)"
|
|
91
91
|
: `Cache: hit ${cacheHit} · miss ${cacheMiss} tokens (${Math.round((cacheHit / cacheTotal) * 100)}% hit) across ${cacheTurns} response(s)`
|
|
92
|
+
// 第 6 行 = 核读数投影(§6.16.4):`pct` 与括号内的 `calibratedHistory` 是同一次调用的两个返回值——同屏两值同尺;
|
|
93
|
+
// 标记 `calibrated history estimate` 把它与 `Segments ≈` 行的裸估算 `history`(核 `estimateTokens`,未乘比)分名。
|
|
94
|
+
const statusReading = historyReading(agent?.history ?? [], agent?.provider, tokenRatioOf(agent))
|
|
92
95
|
return [
|
|
93
96
|
`Context ≈${usage.total} tokens (${pct}% of ${usage.window} window); compaction line ${usage.threshold} (${toGo} to the line)`,
|
|
94
97
|
// §6.16.4 字面单源(该行为唯一字面,逐字;预算面同源 requestBudget——不自算第二套):
|
|
@@ -105,7 +108,7 @@ function statsReport(agent) {
|
|
|
105
108
|
`Segments ≈: system ${usage.system} · tools ${usage.tools} · history ${usage.history} (basis: ${usage.basis})`,
|
|
106
109
|
`History: tool outputs ${toolOut} tokens in ${toolCount} msgs · prunable stale ${stale.tokens} tokens in ${stale.indexes.length} msgs · protected tail ${stale.tailCount} msgs`,
|
|
107
110
|
cacheLine,
|
|
108
|
-
`Status line: context ${
|
|
111
|
+
`Status line: context ${statusReading.pct}% (${statusReading.tokens} calibrated history estimate / ${statusReading.window} window)`,
|
|
109
112
|
`Compaction so far: ${compaction} · failures ${agent?._compressFailures ?? 0}/${COMPRESS_FAILURE_LIMIT}`,
|
|
110
113
|
].join("\n")
|
|
111
114
|
}
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* 数据源 = 槽文件权威表(`readEngTokensFromSlot`——无 TTL 过滤原样)∪ 内存 Map(并集去重,
|
|
8
8
|
* 冲突以槽为准——D-S3 同源);**零写**(不 reconcile 回写、不落盘)。列表面 = 逐槽一行
|
|
9
9
|
* (designId ∥ live/expired ∥ ageDays ∥ expiresAt ∥ 批 ∥ designToken(末位,恒携);龄序 = 最老在前)
|
|
10
|
-
* + 计数尾行;「批」= 批次归属旁列(§10——settle 时按 `
|
|
11
|
-
* 取 basename,旁列缺失 ⇒ `_advisorRuns` 按 designId 命中 run
|
|
12
|
-
*
|
|
10
|
+
* + 计数尾行;「批」= 批次归属旁列(§10——settle 时按 `batchAbsPathOfRun` 派生落盘的绝对路径)
|
|
11
|
+
* 取 basename,旁列缺失 ⇒ `_advisorRuns` 按 designId 命中 run 用**同一派生**兜底(`run.batchDoc`
|
|
12
|
+
* 优先、`docSetKey` 段档次之——KD-SL2);两者皆不可得 ⇒ `—`。
|
|
13
13
|
* `designId` 过滤单槽(trim 精确匹配;空串视同缺省;未知 ⇒ 拒 + 列在场 id);缺省 = 现行清点面逐字零变;零写同前。
|
|
14
14
|
* 面二 `executeConsumeDesignAction`(`subagent` action `consume-design` 消费核——自
|
|
15
15
|
* `subagent-spawn.mjs` 迁入):选择器恰一(`designId` ∥ `designIds` ∥ `expired` ∥
|
|
@@ -54,8 +54,9 @@ export function unionBatches(agent, { strict = false } = {}) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/** 「批」列派生(KD-SL2):批次归属旁列(§10)优先——取该绝对路径文件的 basename;旁列缺失
|
|
57
|
-
* (旁列启用前签发的旧槽)⇒ 会话内评审实例注册表 `_advisorRuns` 按 designId
|
|
58
|
-
*
|
|
57
|
+
* (旁列启用前签发的旧槽)⇒ 会话内评审实例注册表 `_advisorRuns` 按 designId 命中的 run 逆序取
|
|
58
|
+
* **最近一个能派生出批档的**(同一派生 `batchAbsPathOfRun`——`run.batchDoc` 优先、`docSetKey`
|
|
59
|
+
* 段档兜底;与 settle 写旁列同源:清点面不能拿另一个派生去读同一个事实);皆不可得(跨重启 /
|
|
59
60
|
* 模式重进 / 注册表回收)⇒ `—`(如实降级)。 */
|
|
60
61
|
function batchOf(agent, designId, batches) {
|
|
61
62
|
const fromSide = batches instanceof Map ? batches.get(designId) : undefined
|
|
@@ -64,8 +65,9 @@ function batchOf(agent, designId, batches) {
|
|
|
64
65
|
const runs = advisorRuns(agent)
|
|
65
66
|
if (!(runs instanceof Map)) return "—"
|
|
66
67
|
for (const run of [...runs.values()].reverse()) {
|
|
67
|
-
if (run?.designId !== designId
|
|
68
|
-
const
|
|
68
|
+
if (run?.designId !== designId) continue
|
|
69
|
+
const abs = batchAbsPathOfRun(run)
|
|
70
|
+
const name = abs ? basenameOfPath(abs) : null
|
|
69
71
|
if (name) return name
|
|
70
72
|
}
|
|
71
73
|
return "—"
|
|
@@ -77,23 +79,9 @@ function basenameOfPath(p) {
|
|
|
77
79
|
return parts.length ? parts[parts.length - 1] : null
|
|
78
80
|
}
|
|
79
81
|
|
|
80
|
-
/** docSetKey(JSON 串)→ `batches/`
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
try { list = JSON.parse(docSetKey) } catch { return null }
|
|
84
|
-
if (!Array.isArray(list)) return null
|
|
85
|
-
for (const p of list) {
|
|
86
|
-
if (typeof p !== "string") continue
|
|
87
|
-
const parts = p.split(/[\\/]/)
|
|
88
|
-
const i = parts.lastIndexOf("batches")
|
|
89
|
-
if (i >= 0 && i < parts.length - 1 && parts[i + 1]) return parts[i + 1]
|
|
90
|
-
}
|
|
91
|
-
return null
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** docSetKey(JSON 串)→ `batches/` 段档的**绝对路径**(与 `batchBasename` 同一 `lastIndexOf("batches")`
|
|
95
|
-
* 判据);不可解析 / 无命中 ⇒ null。settle 时刻的批归属派生单源(§10「批次标识的来源」——
|
|
96
|
-
* 零新输入:判据与清点面同源)。`resolve` = 归一(分隔符 ∥ `.`/`..`);落盘值即此形。
|
|
82
|
+
/** docSetKey(JSON 串)→ `batches/` 段档的**绝对路径**(判据 = `lastIndexOf("batches")` 段序);
|
|
83
|
+
* 不可解析 / 无命中 ⇒ null。批归属派生的**兜底源**(§10「批次标识的来源」第 2 源;上层
|
|
84
|
+
* = `batchAbsPathOfRun` 的两源定序)。`resolve` = 归一(分隔符 ∥ `.`/`..`);落盘值即此形。
|
|
97
85
|
* **归属不唯一 ⇒ 不猜**:文档集里出现**多份不同的**批档(收口消费以批为键,猜错 = 消费别批的
|
|
98
86
|
* 授权)⇒ 返回 null(归属 = 无,与「不含批档」同一降级层——§10 同段「比留下一个可解释的
|
|
99
87
|
* 滞留槽更坏」的排序)。同一档的两种写法(分隔符 / 大小写)去重,不算两份。 */
|
|
@@ -114,6 +102,18 @@ export function batchAbsPath(docSetKey) {
|
|
|
114
102
|
return hits.length === 1 ? hits[0] : null
|
|
115
103
|
}
|
|
116
104
|
|
|
105
|
+
/** 批归属派生(两源定序——单源,三处消费共用):① `run.batchDoc`(评审实例的显式批档绑定,
|
|
106
|
+
* `advisor.mjs` 落在实例上)优先;② 否则 `batchAbsPath(run.docSetKey)` 兜底(批档写进 `documents`
|
|
107
|
+
* 的调用形)。两源同时有值**不是取舍**——主源在手就不查兜底(派生不改写声明)。两者皆无 ⇒ `null`
|
|
108
|
+
* (归属 = 无,不猜)。设计单源 = `docs/core/design/DESIGN-TOKEN-SETTLEMENT.md` §10「批次标识的来源」;
|
|
109
|
+
* 兜底源可得归属、而 §3 落地门只认 `run.batchDoc` 这一残余,登记在
|
|
110
|
+
* `docs/core/design/ADVISOR-GUARDS.md` §1.2。 */
|
|
111
|
+
export function batchAbsPathOfRun(run) {
|
|
112
|
+
const bound = run?.batchDoc
|
|
113
|
+
if (typeof bound === "string" && bound.trim()) return resolve(bound)
|
|
114
|
+
return batchAbsPath(run?.docSetKey)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
117
|
/** 批档身份键(§10「匹配」):两侧(旁列值 ∥ 收口目标 `abs`)都是 `node:path` 解析后的绝对路径;
|
|
118
118
|
* 比较前分隔符归一、`win32` 下不区分大小写(宿主事实)。出处:`docSetKey` 的 `normAbs` 对绝对
|
|
119
119
|
* 输入原样保留、不归一分隔符,而 `documents` 同时受理项目根相对形与绝对形 ⇒ 同一批档在
|