thincoder 0.12.60 → 0.12.61
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 +24 -1
- package/README.md +8 -6
- package/bin/thincoder.mjs +27 -122
- package/package.json +3 -2
- package/src/abort-provenance.mjs +116 -0
- package/src/acp/bridge.mjs +38 -17
- package/src/acp.mjs +6 -1
- package/src/advisor/citations.mjs +83 -21
- package/src/advisor/compaction.mjs +174 -0
- package/src/advisor/loop.mjs +293 -0
- package/src/advisor/messages.mjs +36 -134
- package/src/advisor/project-context.mjs +194 -0
- package/src/advisor/repos.mjs +17 -40
- package/src/advisor/run.mjs +124 -329
- package/src/advisor/truncate.mjs +57 -0
- package/src/advisor.mjs +3 -2
- package/src/agent/completion.mjs +1 -1
- package/src/agent/dispatch.mjs +47 -12
- package/src/agent/helpers.mjs +71 -13
- package/src/agent/record-results.mjs +13 -5
- package/src/agent/relay-prefix.mjs +39 -0
- package/src/agent/run-stages.mjs +21 -6
- package/src/agent/setup-reminders.mjs +16 -9
- package/src/agent/setup.mjs +92 -128
- package/src/agent/spawn-child.mjs +26 -9
- package/src/agent-tools/advisor-async.mjs +70 -180
- package/src/agent-tools/advisor-settle.mjs +231 -0
- package/src/agent-tools/advisor.mjs +69 -20
- package/src/agent-tools/batch-segment.mjs +195 -0
- package/src/agent-tools/consult.mjs +23 -10
- package/src/agent-tools/design-token.mjs +14 -1
- package/src/agent-tools/digest-budget.mjs +76 -0
- package/src/agent-tools/eng.mjs +3 -3
- package/src/agent-tools/escalate-async.mjs +16 -13
- package/src/agent-tools/read-history.mjs +13 -3
- package/src/agent-tools/review-streak.mjs +93 -0
- package/src/agent-tools/settings.mjs +130 -17
- package/src/agent-tools/subagent-actions.mjs +15 -6
- package/src/agent-tools/subagent-async.mjs +66 -14
- package/src/agent-tools/subagent-panel.mjs +22 -15
- package/src/agent-tools/subagent-run.mjs +9 -6
- package/src/agent-tools/subagent-scheduler.mjs +57 -8
- package/src/agent-tools/subagent-spawn.mjs +63 -16
- package/src/agent-tools/subagent.mjs +175 -49
- package/src/agent-tools/verify.mjs +13 -34
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +38 -21
- package/src/cli/distill-command.mjs +2 -2
- package/src/cli/make-agent.mjs +23 -7
- package/src/cli/memory-command.mjs +2 -2
- package/src/cli/setup-wizard.mjs +29 -9
- package/src/completions.mjs +114 -0
- package/src/config-migrate.mjs +70 -0
- package/src/config.mjs +132 -63
- package/src/conventions.mjs +223 -0
- package/src/crash-reports.mjs +7 -2
- package/src/expand-home.mjs +16 -0
- package/src/generate-title.mjs +1 -1
- package/src/hooks.mjs +7 -3
- package/src/memory/code-index.mjs +9 -3
- package/src/memory/code-sync.mjs +70 -31
- package/src/memory/delete.mjs +2 -0
- package/src/memory/docs.mjs +10 -6
- package/src/memory/file-walk.mjs +109 -0
- package/src/memory/schema.mjs +15 -3
- package/src/model-ref.mjs +66 -0
- package/src/model-specs.mjs +42 -8
- package/src/prompt-overlays.mjs +73 -16
- package/src/prompts/advisor-design.md +18 -8
- package/src/prompts/advisor-round1.md +7 -1
- package/src/prompts/advisor-round2.md +13 -2
- package/src/prompts/advisor-round3.md +13 -2
- package/src/prompts/common.md +115 -0
- package/src/prompts/consult-base.md +2 -0
- package/src/prompts/discipline-engineering.md +217 -0
- package/src/prompts/discipline-normal.md +179 -0
- package/src/prompts/persona-coder.md +21 -0
- package/src/prompts/persona-eng-coder.md +37 -0
- package/src/prompts/persona-eng-designer.md +55 -0
- package/src/prompts/persona-engineering.md +54 -0
- package/src/prompts/persona-explore.md +15 -0
- package/src/prompts/persona-normal.md +27 -0
- package/src/prompts/persona-plan.md +26 -0
- package/src/provider/anthropic.mjs +4 -4
- package/src/provider/core.mjs +13 -32
- package/src/provider/errors.mjs +26 -1
- package/src/provider/google.mjs +5 -6
- package/src/provider/index.mjs +2 -1
- package/src/provider/list-models.mjs +93 -0
- package/src/provider/rate.mjs +2 -1
- package/src/provider/responses.mjs +5 -3
- package/src/provider/sse.mjs +3 -4
- package/src/proxy.mjs +9 -14
- package/src/session-slots.mjs +7 -2
- package/src/session.mjs +59 -38
- package/src/token-ttl.mjs +2 -1
- package/src/tools/{system.mjs → bash.mjs} +6 -243
- package/src/tools/file.mjs +30 -10
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +15 -34
- package/src/tools/index.mjs +4 -2
- package/src/tools/ops.mjs +20 -7
- package/src/tools/question.md +1 -0
- package/src/tools/question.mjs +26 -0
- package/src/tools/read.md +1 -1
- package/src/tools/read_image.md +1 -1
- package/src/tools/search.mjs +236 -0
- package/src/tui/agent-turn.mjs +32 -13
- package/src/tui/ansi.mjs +2 -0
- package/src/tui/clipboard.mjs +7 -1
- package/src/tui/cmd-advisor.mjs +3 -2
- package/src/tui/cmd-config.mjs +108 -37
- package/src/tui/cmd-eng.mjs +11 -27
- package/src/tui/cmd-exit.mjs +6 -8
- package/src/tui/cmd-model.mjs +14 -12
- package/src/tui/cmd-reindex.mjs +7 -0
- package/src/tui/cmd-submodel.mjs +8 -5
- package/src/tui/cmd-undo.mjs +4 -3
- package/src/tui/index.mjs +38 -37
- package/src/tui/key-handler.mjs +61 -17
- package/src/tui/key-modes.mjs +86 -8
- package/src/tui/layout.mjs +18 -10
- package/src/tui/model-catalog.mjs +89 -0
- package/src/tui/model-picker.mjs +498 -0
- package/src/tui/mouse.mjs +47 -10
- package/src/tui/pickers.mjs +28 -410
- package/src/tui/render-frame.mjs +32 -16
- package/src/tui/render-loop.mjs +2 -0
- package/src/tui/render-segments.mjs +5 -19
- package/src/tui/render.mjs +37 -5
- package/src/tui/slash-commands.mjs +2 -2
- package/src/tui/subagent-blocks.mjs +69 -90
- package/src/tui/subagent-children.mjs +50 -64
- package/src/tui/subagent-freeze.mjs +40 -43
- package/src/tui/subagent-panel.mjs +21 -66
- package/src/tui/suspension-drive.mjs +28 -82
- package/src/tui/tool-args.mjs +4 -2
- package/src/tui/tool-display.mjs +4 -3
- package/src/tui/tool-events.mjs +23 -16
- package/src/tui/tui-lifecycle.mjs +18 -5
- package/src/tui/wizard.mjs +52 -18
- package/src/tui/wrapped-spawn.mjs +38 -0
- package/src/prompts/coder.md +0 -13
- package/src/prompts/discipline.md +0 -84
- package/src/prompts/eng-coder.md +0 -19
- package/src/prompts/engineering-sub.md +0 -14
- package/src/prompts/engineering.md +0 -87
- package/src/prompts/explore.md +0 -12
- package/src/prompts/main.md +0 -34
- package/src/prompts/methodology-template.md +0 -38
- package/src/prompts/plan.md +0 -9
- package/src/prompts/system.md +0 -44
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
* agent-tools/advisor.mjs — advisor tool wrapper.
|
|
3
3
|
* The agent calls this explicitly to get an independent review.
|
|
4
4
|
* type="design" for design doc review, type="code" for code review (default).
|
|
5
|
-
* §
|
|
5
|
+
* §11.2 (R13 — async advisor): at depth 0 the review launches into the
|
|
6
6
|
* background pool by DEFAULT (async:true / omitted; async:false forces the
|
|
7
7
|
* blocking review); depth>0 (eng-coder self-review) stays synchronous always.
|
|
8
8
|
*/
|
|
9
|
-
import { runAdvisorReview, MAX_ADVISOR_ROUNDS, buildCapMessage } from "../advisor/run.mjs"
|
|
10
|
-
import {
|
|
9
|
+
import { runAdvisorReview, MAX_ADVISOR_ROUNDS, buildCapMessage, advisorIncompleteMarker, ADVISOR_LAUNCH_REFUSAL_PREFIX, buildDesignReviewGuardMessage } from "../advisor/run.mjs"
|
|
10
|
+
import { resolveBatchDocPath } from "./batch-segment.mjs"
|
|
11
|
+
import { isDocPath, loadConventions } from "../conventions.mjs"
|
|
11
12
|
import {
|
|
12
13
|
generateDesignToken,
|
|
13
14
|
settleDesignReview,
|
|
@@ -15,6 +16,10 @@ import {
|
|
|
15
16
|
launchAsyncAdvisor,
|
|
16
17
|
stripApprovedSuffix,
|
|
17
18
|
} from "./advisor-async.mjs"
|
|
19
|
+
// 第 33 批(§17.5):护栏消费面——工具层预检(检查点 1)+ 同步面计数(计数点 2)。
|
|
20
|
+
import {
|
|
21
|
+
designReviewOutcome, designReviewStreakRecord, designReviewStreakStopped, noteDesignReviewOutcome,
|
|
22
|
+
} from "./review-streak.mjs"
|
|
18
23
|
|
|
19
24
|
// Design-token utilities moved to advisor-async.mjs (the async settle shares
|
|
20
25
|
// them — no wrapper↔runner module cycle); validateDesignToken stays exported
|
|
@@ -38,11 +43,12 @@ export const advisorTool = {
|
|
|
38
43
|
"Optionally pass object={type,target,status,reason,exclude} to anchor the review target " +
|
|
39
44
|
"(AGENT-LOOP.md §18.8 — the review-object declaration is mechanically injected into the review message); " +
|
|
40
45
|
"absent → legacy behavior (no injection). " +
|
|
41
|
-
"ASYNC (AGENT-LOOP.md §
|
|
46
|
+
"ASYNC (AGENT-LOOP.md §11.2): at depth 0 the review runs in the BACKGROUND by default " +
|
|
42
47
|
"(async:true or omitted) — the call returns an ack immediately, the turn ends, and the report " +
|
|
43
|
-
"arrives automatically in a digest turn when the review finishes;
|
|
44
|
-
"
|
|
45
|
-
"
|
|
48
|
+
"arrives automatically in a digest turn when the review finishes; background reviews share one pool — " +
|
|
49
|
+
"at most agent.poolLimits.advisor concurrent reviews (default 4 — configurable via /config 并发池 or " +
|
|
50
|
+
"config.json; pool-full and same-scope refusals state the current limit) — launch reviews one at a time. " +
|
|
51
|
+
"Inside a child (depth>0 — eng-coder self-review) " +
|
|
46
52
|
"reviews are always synchronous; async:true is rejected there. " +
|
|
47
53
|
"Returns the review report — the advisor's findings verdict: all-clear (call verify) or a findings list to fix.",
|
|
48
54
|
parameters: {
|
|
@@ -51,7 +57,7 @@ export const advisorTool = {
|
|
|
51
57
|
type: { type: "string", enum: ["code", "design"], description: "Review type: 'design' for design doc review, 'code' for code review (default)" },
|
|
52
58
|
async: {
|
|
53
59
|
type: "boolean",
|
|
54
|
-
description: "Background review: default at depth 0 = true (async — ack now, report via digest); async:false forces the blocking review. depth>0 → always sync (async:true rejected).",
|
|
60
|
+
description: "Background review: default at depth 0 = true (async — ack now, report via digest); async:false forces the blocking review (mechanism parameter — top-level launches are async by default). depth>0 → always sync (async:true rejected).",
|
|
55
61
|
},
|
|
56
62
|
object: {
|
|
57
63
|
type: "object",
|
|
@@ -74,6 +80,10 @@ export const advisorTool = {
|
|
|
74
80
|
items: { type: "string" },
|
|
75
81
|
description: "Explicit list of doc paths to review (design docs, requirements docs, referenced docs). The advisor reviews ONLY these — it does NOT scan git diff. Use for both design review and code review to pass the task's Docs involved list.",
|
|
76
82
|
},
|
|
83
|
+
batchDoc: {
|
|
84
|
+
type: "string",
|
|
85
|
+
description: "Design review only: path to the batch record currently in flight. Validated WHENEVER passed (any review type) — a value that is not a readable file is refused with an error rather than ignored; for design reviews the reviewer then ALSO gets the batch_segment write channel to record its findings table + VERDICT + counts into §3 (ENGINEERING-MODE.md §2.20). Omit when no batch record is in flight — the review then runs unchanged with no write channel (zero regression).",
|
|
86
|
+
},
|
|
77
87
|
},
|
|
78
88
|
},
|
|
79
89
|
readonly: true,
|
|
@@ -100,20 +110,20 @@ export const advisorTool = {
|
|
|
100
110
|
return "Advisor: no review scope specified. Provide paths (files/directories to review) or documents (acceptance criteria context)."
|
|
101
111
|
}
|
|
102
112
|
|
|
103
|
-
// Design review:
|
|
113
|
+
// Design review: the review scope must be documentation files. Classification
|
|
114
|
+
// comes from the single authority (src/conventions.mjs) — the old `docs/`
|
|
115
|
+
// prefix test is retired with it (FR12: no directory-name hardcoding in the
|
|
116
|
+
// gate; a project whose docs live elsewhere just declares its code paths).
|
|
104
117
|
if (reviewType === "design" && documents) {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
if (doc.startsWith("docs/") || doc.startsWith("docs\\")) return false
|
|
108
|
-
return !isDocFile(doc)
|
|
109
|
-
})
|
|
118
|
+
const conv = loadConventions(agent.cwd)
|
|
119
|
+
const invalidDocs = documents.filter((doc) => !isDocPath(doc, conv))
|
|
110
120
|
if (invalidDocs.length > 0) {
|
|
111
121
|
if (ctx._toolCallId !== undefined) (agent._advisorRefusals ??= new Set()).add(ctx._toolCallId)
|
|
112
|
-
return `Advisor: design review documents must be
|
|
122
|
+
return `Advisor: design review documents must be documentation files (per the project's conventions). Invalid: ${invalidDocs.join(", ")}`
|
|
113
123
|
}
|
|
114
124
|
}
|
|
115
125
|
|
|
116
|
-
// §
|
|
126
|
+
// §11.2 (R13 — ruling ②-3 A): async gate. Depth-0 defaults to the
|
|
117
127
|
// background pool; depth>0 (eng-coder internal self-review) is ALWAYS sync —
|
|
118
128
|
// an explicit async:true there is rejected, the default never flips.
|
|
119
129
|
const depth = ctx?.depth
|
|
@@ -121,18 +131,25 @@ export const advisorTool = {
|
|
|
121
131
|
// 拒发登记(与 cap/池满拒同款):评审未跑——不置 called/不耗轮次(record-results
|
|
122
132
|
// 的 REFUSED 契约——advisor 评审发现 #1:拒发不得静默满足 guard)。
|
|
123
133
|
if (ctx._toolCallId !== undefined) (agent._advisorRefusals ??= new Set()).add(ctx._toolCallId)
|
|
124
|
-
return "Advisor: async reviews are only available at depth 0 — the top-level session owns the background pool (AGENT-LOOP.md §
|
|
134
|
+
return "Advisor: async reviews are only available at depth 0 — the top-level session owns the background pool (AGENT-LOOP.md §11.2); inside a child (eng-coder self-review) reviews run synchronously. Call advisor again without async:true (or with async:false)."
|
|
125
135
|
}
|
|
126
136
|
const isAsync = args.async === true || (depth === 0 && args.async !== false)
|
|
127
137
|
// (ctx.depth undefined = direct callers/tests without a dispatch context —
|
|
128
138
|
// legacy sync semantics.)
|
|
129
139
|
|
|
130
|
-
// Per-review instance resolution (§
|
|
140
|
+
// Per-review instance resolution (§11.2 ③ — ruling ②-5 A): fix rounds
|
|
131
141
|
// continue the same reviewId (design = the doc-set instance's designId —
|
|
132
142
|
// slot/spawn continuity; code = the newest OPEN instance). The resolution
|
|
133
143
|
// scopes agent._advisorRound/_lastAdvisorOutput so the message builder and
|
|
134
144
|
// the run.mjs cap read THIS instance's round/prior (multi-review isolation).
|
|
135
145
|
const resolved = resolveAdvisorLaunch(agent, reviewType, { documents })
|
|
146
|
+
// §2.20.2 评审侧批次档门禁 + 实例键绑定(batch_segment 的唯一路径来源):
|
|
147
|
+
// 口径 = **「若传则须可读」**(空/不可读 → throw;不强制必传——无批次档的在途设计评审
|
|
148
|
+
// 零回归,N5);绑定落在 resolved.run(评审实例键,与 reviewType/round/designId 同族)——
|
|
149
|
+
// 并发设计评审各绑各档,不用单值会话态(§2.20.8 #6)。
|
|
150
|
+
if (args.batchDoc !== undefined && args.batchDoc !== null) {
|
|
151
|
+
resolved.run.batchDoc = resolveBatchDocPath(agent.cwd, args.batchDoc)
|
|
152
|
+
}
|
|
136
153
|
// Design token minted for EVERY design round — the reviewer echoes it only on
|
|
137
154
|
// a clean pass; on pass it is slotted under the instance's designId at settle
|
|
138
155
|
// (sync: right here; async: the settle callback — fix #2). A NEW instance
|
|
@@ -154,6 +171,14 @@ export const advisorTool = {
|
|
|
154
171
|
return buildCapMessage(agent)
|
|
155
172
|
}
|
|
156
173
|
|
|
174
|
+
// 第 33 批(§17.5 检查点 1——工具层预检;cap 预检邻位 / sync / async 分叉前):同一 doc-set
|
|
175
|
+
// 连续 `MAX_DESIGN_REVIEW_STREAK` 次未产出可用结算 ⇒ 拒发(登记 `_advisorRefusals` 同 cap /
|
|
176
|
+
// 池满款——不置 called / 不耗轮次 / **零 LLM**),返回结论串。
|
|
177
|
+
if (reviewType === "design" && designReviewStreakStopped(agent, resolved.run.docSetKey)) {
|
|
178
|
+
if (ctx._toolCallId !== undefined) (agent._advisorRefusals ??= new Set()).add(ctx._toolCallId)
|
|
179
|
+
return buildDesignReviewGuardMessage(designReviewStreakRecord(agent, resolved.run.docSetKey), documents ?? [])
|
|
180
|
+
}
|
|
181
|
+
|
|
157
182
|
if (isAsync) {
|
|
158
183
|
const ack = launchAsyncAdvisor(agent, ctx, {
|
|
159
184
|
reviewType, documents, paths, object: reviewObject,
|
|
@@ -170,10 +195,15 @@ export const advisorTool = {
|
|
|
170
195
|
if (ctx._toolCallId !== undefined) {
|
|
171
196
|
(agent._advisorAsyncAcks ??= new Set()).add(ctx._toolCallId)
|
|
172
197
|
}
|
|
198
|
+
// E(F17/§14.14 E-3c):设计评审点火回执追加冻结句(代码评审 ack 零改)——窗口下界以
|
|
199
|
+
// 可观察信号表达:报告送达 / 取消前,被审文档(含批次档)零写入。
|
|
200
|
+
const freezeNote = reviewType === "design"
|
|
201
|
+
? ";D5 冻结窗口:被审文档(含批次档)在报告送达前零写入——在途写入会被拒绝,写入将使本轮结算为陈旧 (pass 不发 token)"
|
|
202
|
+
: ""
|
|
173
203
|
return JSON.stringify({
|
|
174
204
|
id: ack.id, kind: "advisor", status: "running",
|
|
175
205
|
reviewId: resolved.reviewId,
|
|
176
|
-
note: "评审已后台启动——完成自动回来 (review started in the background — the report arrives in a digest turn automatically; pass this id to cancel if needed)",
|
|
206
|
+
note: "评审已后台启动——完成自动回来 (review started in the background — the report arrives in a digest turn automatically; pass this id to cancel if needed)" + freezeNote,
|
|
177
207
|
})
|
|
178
208
|
}
|
|
179
209
|
|
|
@@ -187,8 +217,18 @@ export const advisorTool = {
|
|
|
187
217
|
const result = await runAdvisorReview(agent, reviewType, {
|
|
188
218
|
onOutput: ctx.onOutput,
|
|
189
219
|
signal: ctx.signal,
|
|
220
|
+
// 同步路径的实例绑定传递(异步路径由池条目 run.batchDoc 取——run.mjs 自行解析)。
|
|
221
|
+
batchDoc: resolved.run.batchDoc ?? null,
|
|
190
222
|
}, designToken, documents, paths, reviewObject, designId)
|
|
191
223
|
|
|
224
|
+
// B 启动拒绝(§14.4 #2——稳定前缀):拒发登记(同池满 / cap 款——不置 called、不耗轮次),
|
|
225
|
+
// 可见报错照常返回(record-results 的 REFUSED 契约)。第 33 批:判定单点——同时供设计
|
|
226
|
+
// 失败分类复用(launchRefused ⇒ neutral——无尝试发生,§17.3 #1)。
|
|
227
|
+
const launchRefused = String(result).startsWith(ADVISOR_LAUNCH_REFUSAL_PREFIX)
|
|
228
|
+
if (launchRefused && ctx._toolCallId !== undefined) {
|
|
229
|
+
(agent._advisorRefusals ??= new Set()).add(ctx._toolCallId)
|
|
230
|
+
}
|
|
231
|
+
|
|
192
232
|
if (reviewType === "design") {
|
|
193
233
|
// Design pass/fail settlement — token echo IS the verdict (prompt-enforced);
|
|
194
234
|
// no findings-table heuristics: a design with issues never carries the token.
|
|
@@ -198,11 +238,20 @@ export const advisorTool = {
|
|
|
198
238
|
// retired — no mirror write here; the dispatch/spawn gates read the authoritative
|
|
199
239
|
// multi-slot Map (+ slot-file re-read). Slotting moved into settleDesignReview
|
|
200
240
|
// (shared with the async settle — fix #2).
|
|
201
|
-
const
|
|
241
|
+
const incomplete = advisorIncompleteMarker(result)
|
|
242
|
+
const settled = settleDesignReview(agent, resolved.run, designToken, result, { incomplete })
|
|
243
|
+
// 第 33 批(§17.5 计数点 2——同步面):同分类单源落账(同步面无 stale / 无落盘步骤
|
|
244
|
+
// ⇒ persistFailed 恒 false)。
|
|
245
|
+
noteDesignReviewOutcome(agent, resolved.run.docSetKey, designReviewOutcome({
|
|
246
|
+
launchRefused, stale: false, hasResult: result != null, incomplete, persistFailed: false,
|
|
247
|
+
}))
|
|
202
248
|
// F2e (§29.1): the sync prior mirror must not carry the raw echo the runner
|
|
203
249
|
// stored — overwrite with the clean settled form (exact-suffix truncation).
|
|
204
250
|
if (settled.passed) {
|
|
205
251
|
agent._lastAdvisorOutput = stripApprovedSuffix(settled.output, resolved.run.approvedSuffix)
|
|
252
|
+
} else if (incomplete) {
|
|
253
|
+
// 未完成 ⇒ 同步 prior 镜像覆写为清洗后输出(防未注册 token 进 prior——§14.3)。
|
|
254
|
+
agent._lastAdvisorOutput = settled.output
|
|
206
255
|
}
|
|
207
256
|
return settled.output
|
|
208
257
|
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-tools/batch-segment.mjs — 批次档段写入工具(ENGINEERING-MODE.md §2.20 · FR22 F1-F7)。
|
|
3
|
+
*
|
|
4
|
+
* 规则、守卫与 fail-closed 清单的**权威正文在 §2.20.1**(本文件不重述——D2 单一权威源);
|
|
5
|
+
* 实现要点(正文未定死、由代码承载):
|
|
6
|
+
* - 身份判据 = 评审实例绑定(`review` 形态)/ `agent._role`;
|
|
7
|
+
* - append-only:在段尾(下一条 `## §N` 或 EOF)插入,既有行字节不变;
|
|
8
|
+
* - 来源戳 `### 轮次 N(评审子代理)`(仅 §3)——N = §3 内该形态行计数 + 1,**调用方自带**
|
|
9
|
+
* 的同名标题行被丢弃(否则伪造戳会污染 N 计数——§2.20.8 #5);
|
|
10
|
+
* - 凭证剥除:含凭证形态的行剥掉该子串,剥后为空则整行丢弃(「零命中」+「其余逐字保留」);
|
|
11
|
+
* - 路径门禁:`resolveBatchDocPath`(评审侧「若传则须可读」);工具内再查一次可读性。
|
|
12
|
+
*
|
|
13
|
+
* 导出面:`batchSegmentTool` · `resolveBatchDocPath` · `batchDocForReview`(异步评审实例键取绑定)。
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs"
|
|
16
|
+
import { resolve } from "node:path"
|
|
17
|
+
|
|
18
|
+
/** text 单次上限(§2.20.1——超出引导分段追加,不承诺"不新盖戳")。 */
|
|
19
|
+
export const MAX_TEXT_CHARS = 20000
|
|
20
|
+
/** 身份 → 可写段号(§2.20.1 段白名单)。 */
|
|
21
|
+
export const SEGMENT_BY_ROLE = { "eng-designer": 2, "eng-coder": 5 }
|
|
22
|
+
/** 工具写入的轮次节标题形态(N 计数口径 = 该形态行;`### 轮次与发现(…)` 骨架行不匹配)。 */
|
|
23
|
+
const ROUND_HEADING_RE = /^### 轮次 \d+(评审子代理)/
|
|
24
|
+
/** 段标题定位(`## §N` 独立标题——`## §20` 不误命中 §2)。 */
|
|
25
|
+
const sectionHeaderRe = (seg) => new RegExp(`^## §${seg}(?=\\s|$)`, "m")
|
|
26
|
+
/** 凭证形态(§2.7 冒号态):`[DESIGN-TOKEN:…]` 与 `designId: …`——本节自有正则。 */
|
|
27
|
+
const CRED_RE = /\[DESIGN-TOKEN:[^\]]*\]|designId\s*:\s*\S+/g
|
|
28
|
+
const CRED_TEST_RE = /\[DESIGN-TOKEN:[^\]]*\]|designId\s*:\s*\S+/
|
|
29
|
+
|
|
30
|
+
/** 可读文件判据(存在且为文件——目录/缺失同判不可读)。 */
|
|
31
|
+
function readableFile(abs) {
|
|
32
|
+
try { return existsSync(abs) && statSync(abs).isFile() } catch { return false }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 批次档路径门禁(评审侧 §2.20.2 口径 = **「若传则须可读」**):空/非字符串/不可读 → throw。
|
|
37
|
+
* 非空且可读 → 返回绝对路径(`\` 归一——照 `files`/`batchDoc` spawn 门先例)。
|
|
38
|
+
*/
|
|
39
|
+
export function resolveBatchDocPath(cwd, given) {
|
|
40
|
+
const raw = typeof given === "string" ? given.trim() : ""
|
|
41
|
+
if (!raw) {
|
|
42
|
+
throw new Error("batchDoc must be a non-empty path to the batch record (ENGINEERING-MODE.md §2.20.2) — pass the batch record currently in flight, or omit the parameter entirely when no batch record is in flight.")
|
|
43
|
+
}
|
|
44
|
+
const abs = resolve(cwd ?? process.cwd(), raw.replace(/\\/g, "/"))
|
|
45
|
+
if (!readableFile(abs)) {
|
|
46
|
+
throw new Error(`batchDoc is not a readable file: ${raw} — pass the path of the batch record currently in flight (a path that resolves to an existing file), or omit the parameter when no batch record is in flight.`)
|
|
47
|
+
}
|
|
48
|
+
return abs
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 设计评审的实例绑定解析(§2.20.2/§2.20.3——batch_segment 的唯一路径来源):
|
|
53
|
+
* - 同步路径:调用方(advisor 工具)把实例绑定显式放进 callbacks(**带 batchDoc 键**,
|
|
54
|
+
* 未绑定即 null)——以此为准,不再回看池条目(防同步/异步混跑时串档);
|
|
55
|
+
* - 异步路径:本评审所在**池条目**的 `run.batchDoc`(文档集 = 实例键,各评审各取各条)。
|
|
56
|
+
*/
|
|
57
|
+
export function batchDocForReview(agent, documents, callbacks = null) {
|
|
58
|
+
if (callbacks && "batchDoc" in callbacks) return callbacks.batchDoc ?? null
|
|
59
|
+
const key = JSON.stringify([...(documents ?? [])].sort())
|
|
60
|
+
for (const e of agent?._asyncAdvisors?.values?.() ?? []) {
|
|
61
|
+
if (e.status === "running" && e.reviewType === "design" && JSON.stringify([...(e.documents ?? [])].sort()) === key) {
|
|
62
|
+
return e.run?.batchDoc ?? null
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 段号解析:`§2` / `2` / `§2 批次任务` 均可;无法解析 → null。 */
|
|
69
|
+
function segmentNumber(raw) {
|
|
70
|
+
const m = /^§?\s*(\d+)/.exec(String(raw ?? "").trim())
|
|
71
|
+
return m ? Number(m[1]) : null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 身份 → 可写段号;无写权身份 → null。评审实例绑定优先(评审者的身份即"设计评审"。) */
|
|
75
|
+
function allowedSegment(agent, review) {
|
|
76
|
+
if (review) return 3
|
|
77
|
+
return SEGMENT_BY_ROLE[agent?._role] ?? null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 凭证剥除 + 伪造轮次标题丢弃(§2.20.1 F6/AC30/AC34)。
|
|
82
|
+
* 逐行处理:含凭证形态的行 → 剥除该子串,剥后为空则整行丢弃(其余内容逐字保留)。
|
|
83
|
+
* `dropStamp`(仅 §3 目标段):调用方自带的 `### 轮次 N(评审子代理)` 行被丢弃——
|
|
84
|
+
* 工具生成才是唯一来源戳(否则伪造戳会污染 N 计数)。
|
|
85
|
+
*/
|
|
86
|
+
function sanitizeText(text, dropStamp) {
|
|
87
|
+
const kept = []
|
|
88
|
+
for (const line of text.replace(/\r\n?/g, "\n").split("\n")) {
|
|
89
|
+
if (dropStamp && ROUND_HEADING_RE.test(line)) continue
|
|
90
|
+
if (!CRED_TEST_RE.test(line)) { kept.push(line); continue }
|
|
91
|
+
const cleaned = line.replace(CRED_RE, "").replace(/[ \t]+$/, "")
|
|
92
|
+
if (cleaned.trim()) kept.push(cleaned)
|
|
93
|
+
}
|
|
94
|
+
return kept.join("\n")
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** §3 内工具写入的轮次行计数(N = 计数 + 1;收窄口径——只数工具戳形态)。 */
|
|
98
|
+
function roundCount(src, headerMatch, endIdx) {
|
|
99
|
+
const body = src.slice(headerMatch.index + headerMatch[0].length, endIdx)
|
|
100
|
+
return body.split("\n").filter((l) => ROUND_HEADING_RE.test(l)).length
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 段尾定位 + 插入(append-only:只在段尾插入,既有字节不变;返回新全文 + 本轮 N)。 */
|
|
104
|
+
function insertIntoSection(src, seg, text) {
|
|
105
|
+
const hdr = sectionHeaderRe(seg).exec(src)
|
|
106
|
+
if (!hdr) {
|
|
107
|
+
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 (§1.12). 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.`)
|
|
108
|
+
}
|
|
109
|
+
const nextRe = /^## §\d/gm
|
|
110
|
+
nextRe.lastIndex = hdr.index + hdr[0].length
|
|
111
|
+
const next = nextRe.exec(src)
|
|
112
|
+
const endIdx = next ? next.index : src.length
|
|
113
|
+
// 来源戳(仅 §3):工具生成标题 + N——调用方写不进自己的标题(sanitize 已丢弃同名行)。
|
|
114
|
+
const roundN = seg === 3 ? roundCount(src, hdr, endIdx) + 1 : 0
|
|
115
|
+
const payload = (seg === 3 ? `### 轮次 ${roundN}(评审子代理)\n\n` : "") + text
|
|
116
|
+
const before = src.slice(0, endIdx)
|
|
117
|
+
const after = src.slice(endIdx)
|
|
118
|
+
const eol = src.includes("\r\n") ? "\r\n" : "\n"
|
|
119
|
+
const lead = before === "" ? "" : before.endsWith(eol + eol) ? "" : before.endsWith(eol) ? eol : eol + eol
|
|
120
|
+
// 尾随 eol 恒加(块自成行);后面还有段标题时再补一个空行(保持模板的空行分隔)。
|
|
121
|
+
const written = before + lead + payload.replaceAll("\n", eol) + eol + (after === "" ? "" : eol) + after
|
|
122
|
+
return { written, roundN }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 批次档段写入工具工厂。
|
|
127
|
+
* @param {string|null} batchDoc — 绑定的目标档(spawn:`child._batchDoc`;评审:实例键)
|
|
128
|
+
* @param {{review?: boolean}} [opts] — review=true 表示"设计评审实例"形态(写 §3 + 工具盖戳)
|
|
129
|
+
*/
|
|
130
|
+
export function batchSegmentTool(batchDoc = null, { review = false } = {}) {
|
|
131
|
+
const own = review ? "§3" : null
|
|
132
|
+
return {
|
|
133
|
+
name: "batch_segment",
|
|
134
|
+
description:
|
|
135
|
+
"Append your own section of the batch record (ENGINEERING-MODE.md §2.20 — 一段一作者). " +
|
|
136
|
+
"There is NO path parameter: the target record is bound to you (at spawn for eng-designer/eng-coder, per review instance for a design review) and your identity fixes the section you may write " +
|
|
137
|
+
"(eng-designer → §2, design review → §3, eng-coder → §5) — a write outside your own section is refused. " +
|
|
138
|
+
"Append-only: the text lands at the end of your section; existing lines are never rewritten or deleted. " +
|
|
139
|
+
"Credential values are stripped mechanically before writing (never write a token or designId value — §2.7). " +
|
|
140
|
+
"A design review's append is stamped by the tool with a `### 轮次 N(评审子代理)` heading — N is tool-counted; do not write your own heading (it would be dropped). " +
|
|
141
|
+
"Failures are hard and visible (no silent fallback): if the write is refused or fails, say so in your report — “§× 未写入”.",
|
|
142
|
+
parameters: {
|
|
143
|
+
type: "object",
|
|
144
|
+
properties: {
|
|
145
|
+
segment: {
|
|
146
|
+
type: "string",
|
|
147
|
+
description: `The batch-record section you are writing${own ? ` — yours is ${own}` : ""}. Declares the section number only; your identity decides what is actually writable (§2.20.1).`,
|
|
148
|
+
},
|
|
149
|
+
text: {
|
|
150
|
+
type: "string",
|
|
151
|
+
description: "The markdown to append (verbatim — findings table + VERDICT + counts for a design review §3). Limit 20000 characters per call; longer content is refused — split it into multiple calls (each call becomes its own section, N continues).",
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
required: ["segment", "text"],
|
|
155
|
+
},
|
|
156
|
+
async execute(args, ctx) {
|
|
157
|
+
const agent = ctx?.agent ?? {}
|
|
158
|
+
const seg = allowedSegment(agent, review)
|
|
159
|
+
if (seg === null) {
|
|
160
|
+
throw new Error("batch_segment: no segment is writable by this caller — the channel exists for eng-designer (§2), eng-coder (§5) and design reviews bound to a batch record (§3); the parent agent writes §1/§4/§6 through ordinary document writes (ENGINEERING-MODE.md §2.20.1/§2.20.3).")
|
|
161
|
+
}
|
|
162
|
+
const n = segmentNumber(args?.segment)
|
|
163
|
+
if (n === null) {
|
|
164
|
+
throw new Error(`batch_segment: unknown segment ${JSON.stringify(args?.segment ?? null)} — pass the section number you write (e.g. "§${seg}").`)
|
|
165
|
+
}
|
|
166
|
+
if (n !== seg) {
|
|
167
|
+
throw new Error(`batch_segment: §${n} is not yours to write — this caller writes §${seg} only (一段一作者: eng-designer → §2, design review → §3, eng-coder → §5; ENGINEERING-MODE.md §2.20.1).`)
|
|
168
|
+
}
|
|
169
|
+
if (!batchDoc) {
|
|
170
|
+
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, ENGINEERING-MODE.md §2.20.2). Report the section as not written.")
|
|
171
|
+
}
|
|
172
|
+
const abs = resolve(agent.cwd ?? process.cwd(), String(batchDoc).replace(/\\/g, "/"))
|
|
173
|
+
if (!readableFile(abs)) {
|
|
174
|
+
throw new Error(`batch_segment: the bound batch record is not a readable file: ${abs} — nothing was written (§2.20.1 fail-closed). Check the record still exists, then report the section as not written.`)
|
|
175
|
+
}
|
|
176
|
+
if (typeof args?.text !== "string") {
|
|
177
|
+
throw new Error("batch_segment: text must be a string (the markdown to append).")
|
|
178
|
+
}
|
|
179
|
+
if (args.text.length > MAX_TEXT_CHARS) {
|
|
180
|
+
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 顺延; ENGINEERING-MODE.md §2.20.1).`)
|
|
181
|
+
}
|
|
182
|
+
if (/^## §\d/m.test(args.text)) {
|
|
183
|
+
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 (§2.20.1 骨架保护). Nothing was written.")
|
|
184
|
+
}
|
|
185
|
+
const body = sanitizeText(args.text, seg === 3).replace(/^\n+/, "").replace(/\s+$/, "")
|
|
186
|
+
if (!body.trim()) {
|
|
187
|
+
throw new Error("batch_segment: nothing to append — the text is empty after credential stripping (credential values never reach the record; ENGINEERING-MODE.md §2.7/§2.20.1).")
|
|
188
|
+
}
|
|
189
|
+
const src = readFileSync(abs, "utf8")
|
|
190
|
+
const { written, roundN } = insertIntoSection(src, seg, body)
|
|
191
|
+
writeFileSync(abs, written)
|
|
192
|
+
return `batch_segment: appended ${body.length} characters to §${seg} of the batch record${roundN ? ` (### 轮次 ${roundN}(评审子代理))` : ""}.`
|
|
193
|
+
},
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -27,9 +27,11 @@ import { resolveChildProvider } from "./subagent.mjs"
|
|
|
27
27
|
import { pushReal } from "../context.mjs"
|
|
28
28
|
import { offloadToolResult, escapeXml } from "../agent/helpers.mjs"
|
|
29
29
|
import { logEvent, errText } from "../log.mjs"
|
|
30
|
+
import { deathLine } from "../abort-provenance.mjs"
|
|
30
31
|
import { makeRelay, wrapChildCallbacks, runWithContinue, ensureChildApiKey, clampEffort } from "../agent/spawn-child.mjs"
|
|
31
32
|
// ASYNC-RESULT-CONTAINER.md D2/D3/D6:pending 单容器停靠 + settle 公共收尾 + child signal 单点
|
|
32
33
|
import { buildChildSignal, settleAsyncEntry } from "./async-settle.mjs"
|
|
34
|
+
import { digestBudgetOver, persistOverflowReport } from "./digest-budget.mjs" // B5(群 B 批 §22 D-DG2):digest 注入预算单源
|
|
33
35
|
|
|
34
36
|
// Named consult defaults (consult P2, 2026-08-30).
|
|
35
37
|
const CONSULT_TIMEOUT_MS = 600_000 // default consult lifecycle timeout
|
|
@@ -178,8 +180,15 @@ export function composeConsultDigest(session) {
|
|
|
178
180
|
* (_pendingAsyncResults——ASYNC-RESULT-CONTAINER.md D2——role 分发注入)。
|
|
179
181
|
*/
|
|
180
182
|
export async function injectConsultResult(agent, entry) {
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
+
const full = String(entry?.report ?? "(no consultation result)")
|
|
184
|
+
// §22 D-DG3(群 B 批 B5):计入预算的 raw = 报告正文(标签行不计)——首行标签拆出;超限
|
|
185
|
+
// 路径保留族标签行(与 VSC 镜像同形);落盘内容 = raw(与其余三族同口径)。
|
|
186
|
+
const at = full.indexOf("\n")
|
|
187
|
+
const label = at === -1 ? "" : full.slice(0, at + 1)
|
|
188
|
+
const raw = at === -1 ? full : full.slice(at + 1)
|
|
189
|
+
const over = digestBudgetOver(agent, raw.length)
|
|
190
|
+
const saved = over ? await persistOverflowReport(raw, { tag: `consult-${entry?.id ?? "session"}` }) : null
|
|
191
|
+
const preview = saved ? `${label}${saved}` : await offloadToolResult(full, `consult-${entry?.id ?? "session"}`)
|
|
183
192
|
pushReal(agent, {
|
|
184
193
|
role: "user",
|
|
185
194
|
content: escapeXml(preview),
|
|
@@ -208,7 +217,8 @@ async function runConsultChild(ctx, session, id, m, problem, ctrl) {
|
|
|
208
217
|
const armWatchdog = () => {
|
|
209
218
|
const t = setTimeout(() => {
|
|
210
219
|
timedOut = true
|
|
211
|
-
|
|
220
|
+
// §20.3 站点 #12 信号面(第 24 批):watchdog 中止带 timeout reason
|
|
221
|
+
try { ctrl.abort({ abortTrigger: "timeout", abortDetail: "consult-watchdog" }) } catch { /* already settled */ }
|
|
212
222
|
}, timeoutMs)
|
|
213
223
|
t.unref?.()
|
|
214
224
|
return t
|
|
@@ -329,7 +339,7 @@ async function runConsultChild(ctx, session, id, m, problem, ctrl) {
|
|
|
329
339
|
} catch (e) {
|
|
330
340
|
// Runner errors (incl. the watchdog's abort) settle as a failed reply — the
|
|
331
341
|
// continue/declined paths are already handled inside runWithContinue.
|
|
332
|
-
const note = timedOut ? `consultation timed out after ${Math.round(timeoutMs / 60000)}min (agent.consultTimeoutMs)` : e?.
|
|
342
|
+
const note = timedOut ? `consultation timed out after ${Math.round(timeoutMs / 60000)}min (agent.consultTimeoutMs)` : deathLine(e, ctrl?.signal)
|
|
333
343
|
settle(false, note)
|
|
334
344
|
logSettle("error", note)
|
|
335
345
|
}
|
|
@@ -338,8 +348,9 @@ async function runConsultChild(ctx, session, id, m, problem, ctrl) {
|
|
|
338
348
|
// continue-prompt settle as failed replies — the runner's own errors are already
|
|
339
349
|
// handled inside the loop above. relayPrefix is null on these paths — no TUI
|
|
340
350
|
// block was ever opened, so no freeze event is emitted.
|
|
341
|
-
|
|
342
|
-
|
|
351
|
+
const line = deathLine(e, ctrl?.signal)
|
|
352
|
+
settle(false, line)
|
|
353
|
+
logSettle("error", line)
|
|
343
354
|
} finally {
|
|
344
355
|
clearTimeout(watchdog)
|
|
345
356
|
}
|
|
@@ -354,7 +365,7 @@ async function runConsultChild(ctx, session, id, m, problem, ctrl) {
|
|
|
354
365
|
export function cleanupConsultSessions(agent) {
|
|
355
366
|
for (const s of agent._consultSessions?.values() ?? []) {
|
|
356
367
|
s.stopped = true
|
|
357
|
-
for (const c of s.controllers ?? []) { try { c.abort() } catch { /* already settled */ } }
|
|
368
|
+
for (const c of s.controllers ?? []) { try { c.abort({ abortTrigger: "stop", abortDetail: "consult-cleanup" }) } catch { /* already settled */ } }
|
|
358
369
|
}
|
|
359
370
|
agent._consultSessions?.clear()
|
|
360
371
|
}
|
|
@@ -415,8 +426,9 @@ export const consultStartTool = {
|
|
|
415
426
|
// 兜底:挂起会话内的 consult children 持会话 signal,digest 自身 Ctrl+C 不误伤)。
|
|
416
427
|
const baseSignal = buildChildSignal(agent, ctx)
|
|
417
428
|
if (baseSignal) {
|
|
418
|
-
|
|
419
|
-
|
|
429
|
+
// §20.3 站点 #10(第 24 批):hop 逐跳保 reason
|
|
430
|
+
if (baseSignal.aborted) ctrl.abort(baseSignal.reason)
|
|
431
|
+
else baseSignal.addEventListener("abort", () => ctrl.abort(baseSignal.reason), { once: true })
|
|
420
432
|
}
|
|
421
433
|
// Fire and forget — each child settles itself into the session; the session
|
|
422
434
|
// routes to the pending single container when every child has settled (R17).
|
|
@@ -449,7 +461,8 @@ export const consultStopTool = {
|
|
|
449
461
|
if (!s) return JSON.stringify({ error: "unknown consult id" })
|
|
450
462
|
const abandoned = s.pending
|
|
451
463
|
s.stopped = true
|
|
452
|
-
|
|
464
|
+
// §20.3 站点 #11(第 24 批):会话级停 = stop
|
|
465
|
+
for (const c of s.controllers) { try { c.abort({ abortTrigger: "stop", abortDetail: "consult-stop" }) } catch { /* already settled */ } }
|
|
453
466
|
return JSON.stringify({ abandoned, cancelled: true })
|
|
454
467
|
},
|
|
455
468
|
}
|
|
@@ -73,12 +73,25 @@ export function makeDesignTokenRegex(token, flags = "") {
|
|
|
73
73
|
* later review of the same doc-set starts a fresh full review). On non-echo:
|
|
74
74
|
* strip every dead token occurrence and return the findings text — slots stay
|
|
75
75
|
* untouched (方案 ②: a failed re-review revokes nothing).
|
|
76
|
+
* @param {Object} [opts] — 第 11 批(A/F11):`opts.incomplete` = 宿主尾族判定 kind
|
|
77
|
+
* (单谓词 `advisorIncompleteMarker`——结算方透传)。非空 ⇒ **一律不签发**(无论评审文本
|
|
78
|
+
* 是否回显 token):剥除全部 token 回显 + 追加未签发提示(原因 + 恢复指引),不写槽、
|
|
79
|
+
* 不关实例(可重评)。
|
|
76
80
|
* @returns {{passed: boolean, output: string}}
|
|
77
81
|
*/
|
|
78
|
-
export function settleDesignReview(agent, run, designToken, rawResult) {
|
|
82
|
+
export function settleDesignReview(agent, run, designToken, rawResult, opts = {}) {
|
|
79
83
|
if (!designToken || typeof rawResult !== "string") {
|
|
80
84
|
return { passed: false, output: rawResult ?? "" }
|
|
81
85
|
}
|
|
86
|
+
// A(F11/§14.3 消费点 1):未完成即不签发——判据与 code 完成守卫同源(单谓词)。
|
|
87
|
+
const incomplete = opts?.incomplete ?? null
|
|
88
|
+
if (incomplete) {
|
|
89
|
+
const stripped = rawResult.replace(makeDesignTokenRegex(designToken, "g"), "").trim()
|
|
90
|
+
return {
|
|
91
|
+
passed: false,
|
|
92
|
+
output: `${stripped}\n\n评审未完成——token 未签发 (review incomplete — no design token issued; reason: ${incomplete})\n以更小范围重跑设计评审(逐档 / 逐节拆分,或拆到两次评审),或调大 agent.advisor.timeoutMs 后重试;补充检查未完成的部分不得按已核处理。`.trim(),
|
|
93
|
+
}
|
|
94
|
+
}
|
|
82
95
|
const tokenPattern = makeDesignTokenRegex(designToken)
|
|
83
96
|
if (!tokenPattern.test(rawResult)) {
|
|
84
97
|
const stripped = rawResult.replace(makeDesignTokenRegex(designToken, "g"), "").trim()
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* digest-budget.mjs — digest 注入批量预算单源(群 B 批 B5——AGENT-LOOP.md §22 语义源)。
|
|
3
|
+
* 原预算块居 subagent-async.mjs(只覆盖经 injectAsyncResult 的三族:subagent / advisor /
|
|
4
|
+
* escalate),consult 族注入器(injectConsultResult)绕过 ⇒ 多族条目同轮合并注入时可累计
|
|
5
|
+
* 超限(BATCH-3-STRUCTURE F-2 原始事故面 1.3MB 请求体)。本档把常量 / 判超 / 记账 / 落盘
|
|
6
|
+
* 四处合一(D2 单源)+ 四族全接线(D-DG2)。
|
|
7
|
+
*
|
|
8
|
+
* 契约(D-DG1 三导出——CLI 面):
|
|
9
|
+
* - `DIGEST_INJECT_BUDGET`(64×1024——与 offload 单条预览同量级);
|
|
10
|
+
* - `digestBudgetOver(agent, size)`——判超 + 记账:`used > 0 && used + size > BUDGET`
|
|
11
|
+
* 首条豁免保留(单条大报告 >64K 走 offload 预览照旧——预算只约束轮累计);轮界定
|
|
12
|
+
* `agent.history.length !== r.len + 1` 语义逐字迁入(相邻注入间 history 无其他落史 = 同轮;
|
|
13
|
+
* 每次注入 pushReal 恰 +1——他人落史 = 新请求窗口 → 预算复位);键 = agent(会话级复用
|
|
14
|
+
* ——Map 常驻一两项)。
|
|
15
|
+
* - `persistOverflowReport(raw, { tag })`——超限条目全量落盘(`configDir/tool-results` +
|
|
16
|
+
* `cleanupOldToolResults` 轮转;落盘名 `<ts>-<tag>.log`(tag = 溯源标签:写入族 + 条目 id
|
|
17
|
+
* ——非法字符归一 `_`))。返回清单行文本(inline = 仅此——不 inline 全文);落盘失败 →
|
|
18
|
+
* null(调用方回退常规 inline 路径——offload 同款「失败不吞报告」语义)。
|
|
19
|
+
*
|
|
20
|
+
* 计数口径(D-DG3):计入预算的 raw = 报告 / 错误正文(不含 `[System reminder: …]` 标签行
|
|
21
|
+
* ——与既有口径一致);错误条目同计同落盘。
|
|
22
|
+
*
|
|
23
|
+
* 兼容面(D-DG4):subagent-async.mjs 保留 `DIGEST_INJECT_BUDGET` +
|
|
24
|
+
* `_setDigestOffloadDirForTest` re-export(测试导入面零改);单条 offload 预览路径零改。
|
|
25
|
+
*
|
|
26
|
+
* 模块图:import node:fs/promises + node:path + ../agent/helpers.mjs(cleanupOldToolResults)+
|
|
27
|
+
* ../config.mjs(configDir)——叶子级;subagent-async / consult 单向 import 本档(无环)。
|
|
28
|
+
*/
|
|
29
|
+
import { mkdir, writeFile } from "node:fs/promises"
|
|
30
|
+
import { join } from "node:path"
|
|
31
|
+
import { cleanupOldToolResults } from "../agent/helpers.mjs"
|
|
32
|
+
import { configDir } from "../config.mjs"
|
|
33
|
+
|
|
34
|
+
export const DIGEST_INJECT_BUDGET = 64 * 1024 // 64K——与 offload 单条预览同量级
|
|
35
|
+
|
|
36
|
+
// 轮记账:agent → { len, used }(逐字自 subagent-async.mjs 迁入——语义零变)。
|
|
37
|
+
const digestRounds = new Map()
|
|
38
|
+
function digestRoundFor(agent) {
|
|
39
|
+
const h = agent.history
|
|
40
|
+
const r = digestRounds.get(agent)
|
|
41
|
+
if (!r || h.length !== r.len + 1) {
|
|
42
|
+
const fresh = { len: h.length, used: 0 }
|
|
43
|
+
digestRounds.set(agent, fresh)
|
|
44
|
+
return fresh
|
|
45
|
+
}
|
|
46
|
+
r.len = h.length
|
|
47
|
+
return r
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 判超 + 记账(size = 原始正文字符数——标签行不计;先判后记:首条恒豁免)。 */
|
|
51
|
+
export function digestBudgetOver(agent, size) {
|
|
52
|
+
const round = digestRoundFor(agent)
|
|
53
|
+
const over = round.used > 0 && round.used + size > DIGEST_INJECT_BUDGET
|
|
54
|
+
round.used += size
|
|
55
|
+
return over
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Test seam: 落盘目录重定向(单测沙箱——生产从不调用)。 */
|
|
59
|
+
let _digestOffloadDirOverride = null
|
|
60
|
+
export function _setDigestOffloadDirForTest(dir) { _digestOffloadDirOverride = dir }
|
|
61
|
+
|
|
62
|
+
/** 超限条目全量落盘(返回清单行文本;失败 null——调用方回退 inline——结果零丢失)。
|
|
63
|
+
* tag 入文件名(溯源:写入族 + 条目 id)。 */
|
|
64
|
+
export async function persistOverflowReport(raw, { tag }) {
|
|
65
|
+
try {
|
|
66
|
+
const dir = _digestOffloadDirOverride ?? join(configDir, "tool-results")
|
|
67
|
+
await cleanupOldToolResults(dir)
|
|
68
|
+
await mkdir(dir, { recursive: true })
|
|
69
|
+
const file = join(dir, `${Date.now()}-${String(tag).replace(/[^a-zA-Z0-9_-]/g, "_")}.log`)
|
|
70
|
+
await writeFile(file, raw, "utf8")
|
|
71
|
+
return `Report saved to disk (digest inject budget exceeded — full text not inlined): ${file}\nRead it with the read tool (offset/limit).`
|
|
72
|
+
} catch (e) {
|
|
73
|
+
console.warn(`[digest] report persist failed — falling back to inline (report not lost): ${e.message}`)
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
}
|
package/src/agent-tools/eng.mjs
CHANGED
|
@@ -34,7 +34,7 @@ export const engTool = {
|
|
|
34
34
|
// R16 (F-R16a): OFF 不清 token——有效 token 跨模式存活(设计评审过的产物不因
|
|
35
35
|
// 开关重复烧)。过期清理跑在另外三处(恢复过滤 / 开模式 / spawn 门禁拒)。
|
|
36
36
|
ctx.agent._advisorRound = 0 // reset convergence budget
|
|
37
|
-
ctx.agent._advisorRuns = new Map() // §
|
|
37
|
+
ctx.agent._advisorRuns = new Map() // §11.2 D-24b: per-review instances die with the mode (fresh cycles)
|
|
38
38
|
ctx.agent._touchedFiles = [] // clear mutation tracking
|
|
39
39
|
ctx.agent._lastEngState = false
|
|
40
40
|
ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
|
|
@@ -52,11 +52,11 @@ export const engTool = {
|
|
|
52
52
|
// R16 (F-R16b ②): off→on 不重评——只清过期 token(用户裁定"打开工程模式时
|
|
53
53
|
// 应该清理"),有效 token 原样保留——遍历 Map 删过期,返回文案含清理个数。
|
|
54
54
|
const cleared = purgeExpiredDesignTokens(ctx.agent)
|
|
55
|
-
ctx.agent._advisorRuns = new Map() // §
|
|
55
|
+
ctx.agent._advisorRuns = new Map() // §11.2 D-24b: per-review instances die with the mode (fresh cycles)
|
|
56
56
|
ctx.agent._lastEngState = true
|
|
57
57
|
ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
|
|
58
58
|
ctx.agent._pendingReminders.push(ENG_ON_REMINDER)
|
|
59
|
-
let msg = "Engineering mode activated. Design-before-code enforced: write a design document
|
|
59
|
+
let msg = "Engineering mode activated. Design-before-code enforced: write a design document first (location per your project's document conventions), run advisor with type='design', get user approval, then implement via eng-coder subagents."
|
|
60
60
|
if (cleared > 0) {
|
|
61
61
|
msg += ` Cleared ${cleared} expired design token${cleared === 1 ? "" : "s"} — 清 ${cleared} 个过期 token,有效 token 保留(TTL 内不重评)。`
|
|
62
62
|
}
|