thincoder 0.12.58 → 0.12.60
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 +78 -2
- package/README.md +3 -3
- package/bin/thincoder.mjs +88 -19
- package/package.json +4 -3
- package/src/acp/bridge.mjs +135 -26
- package/src/advisor/messages.mjs +57 -4
- package/src/advisor/run.mjs +119 -79
- package/src/advisor.mjs +34 -7
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +182 -22
- package/src/agent/helpers.mjs +71 -4
- package/src/agent/record-results.mjs +46 -10
- package/src/agent/run-stages.mjs +227 -0
- package/src/agent/setup-reminders.mjs +62 -0
- package/src/agent/setup.mjs +107 -20
- package/src/agent/spawn-child.mjs +54 -4
- package/src/agent-tools/advisor-async.mjs +456 -0
- package/src/agent-tools/advisor.mjs +133 -109
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/consult.mjs +154 -104
- package/src/agent-tools/design-token.mjs +104 -0
- package/src/agent-tools/eng.mjs +26 -30
- package/src/agent-tools/escalate-async.mjs +286 -0
- package/src/agent-tools/goal.mjs +11 -1
- package/src/agent-tools/read-history.mjs +284 -0
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/settings.mjs +152 -0
- package/src/agent-tools/skill.mjs +2 -1
- package/src/agent-tools/subagent-actions.mjs +470 -0
- package/src/agent-tools/subagent-async.mjs +382 -0
- package/src/agent-tools/subagent-panel.mjs +153 -0
- package/src/agent-tools/subagent-run.mjs +202 -0
- package/src/agent-tools/subagent-scheduler.mjs +343 -0
- package/src/agent-tools/subagent-spawn.mjs +406 -0
- package/src/agent-tools/subagent.mjs +203 -377
- package/src/agent-tools/task.mjs +4 -3
- package/src/agent-tools/timer.mjs +9 -4
- package/src/agent-tools/verify.mjs +198 -238
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +145 -242
- package/src/auto-think.mjs +14 -0
- package/src/cli/distill-command.mjs +10 -4
- package/src/cli/make-agent.mjs +4 -1
- package/src/cli/memory-command.mjs +2 -1
- package/src/cli/permission.mjs +8 -1
- package/src/cli/setup-wizard.mjs +17 -12
- package/src/config.mjs +61 -8
- package/src/context.mjs +81 -163
- package/src/crash-reports.mjs +123 -0
- package/src/distill.mjs +30 -12
- package/src/escape.mjs +6 -4
- package/src/explore-distill.mjs +155 -0
- package/src/log.mjs +195 -0
- package/src/memory/code-sync.mjs +2 -1
- package/src/memory/core.mjs +11 -72
- package/src/memory/delete.mjs +234 -0
- package/src/memory/docs.mjs +206 -87
- package/src/memory.mjs +3 -1
- package/src/model-specs.mjs +15 -1
- package/src/peer-domains.mjs +265 -0
- package/src/peer-instances.mjs +231 -0
- package/src/prompt-overlays.mjs +25 -0
- package/src/prompts/advisor-design.md +18 -39
- package/src/prompts/advisor-round1.md +20 -32
- package/src/prompts/advisor-round2.md +16 -16
- package/src/prompts/advisor-round3.md +16 -16
- package/src/prompts/coder.md +7 -28
- package/src/prompts/consult-base.md +4 -11
- package/src/prompts/discipline.md +31 -44
- package/src/prompts/eng-coder.md +9 -34
- package/src/prompts/engineering-sub.md +10 -8
- package/src/prompts/engineering.md +61 -264
- package/src/prompts/explore.md +4 -14
- package/src/prompts/main.md +18 -35
- package/src/prompts/methodology-template.md +32 -38
- package/src/prompts/plan.md +2 -9
- package/src/prompts/system.md +18 -35
- package/src/provider/core.mjs +62 -69
- package/src/provider/errors.mjs +76 -0
- package/src/provider/retry.mjs +8 -45
- package/src/session-gc.mjs +214 -0
- package/src/session-guard.mjs +47 -0
- package/src/session-rename.mjs +38 -0
- package/src/session-slots.mjs +181 -58
- package/src/session.mjs +48 -89
- package/src/token-ttl.mjs +273 -0
- package/src/tools/apply_patch.md +3 -1
- package/src/tools/bash.md +1 -1
- package/src/tools/checklist-sync.mjs +181 -0
- package/src/tools/checklist.mjs +52 -39
- package/src/tools/delete.md +1 -0
- package/src/tools/edit-batch.mjs +131 -44
- package/src/tools/edit-diff.mjs +348 -0
- package/src/tools/edit.md +20 -13
- package/src/tools/execute.md +7 -7
- package/src/tools/execute.mjs +55 -24
- package/src/tools/file.mjs +25 -70
- package/src/tools/file_ops.md +2 -1
- package/src/tools/get_current_time.md +3 -1
- package/src/tools/git.mjs +14 -6
- package/src/tools/glob-dialect.mjs +130 -0
- package/src/tools/glob.md +3 -3
- package/src/tools/grep.md +1 -1
- package/src/tools/hashline_edit.md +2 -0
- package/src/tools/index.mjs +3 -3
- package/src/tools/insert_after.md +2 -1
- package/src/tools/lint.md +2 -0
- package/src/tools/lsp.md +4 -1
- package/src/tools/ops.mjs +175 -3
- package/src/tools/patch.mjs +84 -13
- package/src/tools/question.md +5 -1
- package/src/tools/repomap.mjs +1 -1
- package/src/tools/shared.mjs +18 -25
- package/src/tools/system.mjs +50 -30
- package/src/tools/tree.md +2 -1
- package/src/tools/wait_for.md +22 -0
- package/src/tools/web.mjs +5 -3
- package/src/tools/websearch.md +2 -1
- package/src/tools/write.md +2 -0
- package/src/traces/trace-store.mjs +224 -0
- package/src/tui/agent-turn.mjs +179 -27
- package/src/tui/clipboard.mjs +15 -4
- package/src/tui/cmd-config.mjs +77 -16
- package/src/tui/cmd-eng.mjs +20 -16
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-mcp.mjs +17 -2
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-think.mjs +11 -11
- package/src/tui/cmd-upgrade.mjs +19 -4
- package/src/tui/config-helpers.mjs +28 -16
- package/src/tui/distill-cmd.mjs +1 -1
- package/src/tui/index.mjs +31 -96
- package/src/tui/interaction.mjs +13 -2
- package/src/tui/key-handler.mjs +105 -155
- package/src/tui/key-modes.mjs +215 -0
- package/src/tui/layout.mjs +22 -1
- package/src/tui/mouse.mjs +46 -0
- package/src/tui/pickers.mjs +51 -25
- package/src/tui/render-conversation.mjs +13 -161
- package/src/tui/render-frame.mjs +27 -10
- package/src/tui/render-loop.mjs +4 -1
- package/src/tui/render-segments.mjs +182 -0
- package/src/tui/startup.mjs +40 -0
- package/src/tui/subagent-blocks.mjs +272 -262
- package/src/tui/subagent-children.mjs +176 -0
- package/src/tui/subagent-freeze.mjs +172 -0
- package/src/tui/subagent-panel.mjs +125 -12
- package/src/tui/suspension-drive.mjs +351 -0
- package/src/tui/tool-args.mjs +10 -2
- package/src/tui/tool-display.mjs +142 -0
- package/src/tui/tool-events.mjs +127 -231
- package/src/tui/tui-lifecycle.mjs +29 -0
- package/src/tui/update-notice.mjs +76 -0
- package/src/tui/wizard.mjs +48 -12
- package/src/agent-tools/escalate.mjs +0 -179
- package/src/agent-tools/subagent-check.mjs +0 -107
- package/src/tools/exec-prelude.mjs +0 -84
|
@@ -1,19 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* consult.mjs — multi-model consultation ("会诊", docs/design/CONSULTATION.md). CLI port.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Two tools (AGENT-LOOP.md §25 D-R17a — R17, 2026-09-06): consult_start
|
|
5
|
+
* (non-blocking spawn) / consult_stop (cancel a running session). consult_check
|
|
6
|
+
* was RETIRED with the digest auto-injection: the mechanism does ZERO judging —
|
|
7
|
+
* when every model of a session settles (pending 0), the session moves to the
|
|
8
|
+
* pending single container (`_pendingAsyncResults` +role "consult"——
|
|
9
|
+
* ASYNC-RESULT-CONTAINER.md D2——升格完整 entry) and the NEXT run start (user turn
|
|
10
|
+
* or digest auto-turn) injects the full verdict text ("[System reminder:
|
|
11
|
+
* consultation #id finished — N replies …]" — per-model status annotations on
|
|
12
|
+
* partial/full failures) for the main agent to judge and act on in the digestion
|
|
13
|
+
* round. A cancelled session (consult_stop) never reaches the stream.
|
|
7
14
|
*
|
|
8
15
|
* CLI adaptation (vs the VS Code plugin): the child runner is CLI's runAgent
|
|
9
16
|
* (runAgent(child, input, callbacks, opts) — an agent object, not provider+cwd);
|
|
10
17
|
* children are built with createAgent({ role: "consult", readonly tools,
|
|
11
18
|
* CONSULT_BASE overlay }); activity streams to the parent TUI via the relay
|
|
12
|
-
* prefix `consult#<
|
|
19
|
+
* prefix `consult#<childRelayN>/` (one per consultant child — the shared subagent
|
|
20
|
+
* relay channel; the child relay number is NOT the session id — sessions key
|
|
21
|
+
* their own `_consultIdCounter`), not onSubagent/onToolPanel.
|
|
22
|
+
* Each child settles its own TUI block with a ⟦ev⟧done event at settle (R17 —
|
|
23
|
+
* the old in-turn check consumption is gone).
|
|
13
24
|
*/
|
|
14
25
|
import { createAgent, runAgent, readonlyToolNames } from "../agent.mjs"
|
|
15
26
|
import { resolveChildProvider } from "./subagent.mjs"
|
|
27
|
+
import { pushReal } from "../context.mjs"
|
|
28
|
+
import { offloadToolResult, escapeXml } from "../agent/helpers.mjs"
|
|
29
|
+
import { logEvent, errText } from "../log.mjs"
|
|
16
30
|
import { makeRelay, wrapChildCallbacks, runWithContinue, ensureChildApiKey, clampEffort } from "../agent/spawn-child.mjs"
|
|
31
|
+
// ASYNC-RESULT-CONTAINER.md D2/D3/D6:pending 单容器停靠 + settle 公共收尾 + child signal 单点
|
|
32
|
+
import { buildChildSignal, settleAsyncEntry } from "./async-settle.mjs"
|
|
17
33
|
|
|
18
34
|
// Named consult defaults (consult P2, 2026-08-30).
|
|
19
35
|
const CONSULT_TIMEOUT_MS = 600_000 // default consult lifecycle timeout
|
|
@@ -27,7 +43,7 @@ function consultLabel(m) {
|
|
|
27
43
|
/** Narrow the configured consultModels pool to a requested subset.
|
|
28
44
|
* Each selector is "provider:model", a bare provider name, or a bare model name
|
|
29
45
|
* (case-insensitive). A trailing " (effort)" suffix is tolerated (round2 复核
|
|
30
|
-
* 对齐 escalate
|
|
46
|
+
* 对齐 escalate 动作(subagent action:"escalate"):withPool 列表会带 " (high)" 后缀,模型照抄应可匹配).
|
|
31
47
|
* Returns { models, error } — error set when a selector matches
|
|
32
48
|
* nothing (surface the typo rather than silently dropping it). Absent/empty selectors
|
|
33
49
|
* → the full pool. */
|
|
@@ -111,13 +127,66 @@ export function makeMainHistoryTool(parentAgent) {
|
|
|
111
127
|
}
|
|
112
128
|
}
|
|
113
129
|
|
|
114
|
-
/**
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
130
|
+
/**
|
|
131
|
+
* Full-session settle routing (R17 — AGENT-LOOP.md §25 D-R17a): a session whose
|
|
132
|
+
* pending count reached 0 has no more replies coming — the session leaves
|
|
133
|
+
* `_consultSessions` and, unless it was cancelled (consult_stop / turn-end
|
|
134
|
+
* abort), moves into the pending single container (`_pendingAsyncResults` +role
|
|
135
|
+
* "consult"——ASYNC-RESULT-CONTAINER.md D2——升格完整 entry:同 subagent/advisor/
|
|
136
|
+
* escalate 的 `{id, role, report, done, ...}` 形态)whose report carries the full
|
|
137
|
+
* per-model verdict text (composed here — all settle states are known,
|
|
138
|
+
* partial/full failures annotated per model). The entry is injected at the next
|
|
139
|
+
* run start (user turn or digest auto-turn — agent.mjs); the suspension driver
|
|
140
|
+
* is woken (settle-event parity with the async pools) so an idle settle still
|
|
141
|
+
* triggers the digestion round (T-R17j).
|
|
142
|
+
* D3:公共收尾统一走 settleAsyncEntry 共享 helper(四族同机制)——consult 族参数:
|
|
143
|
+
* 无池(会话池无条目——settle 即出池)、无 ctx(无 TUI 冻结事件——子块各自 settle 时
|
|
144
|
+
* 已冻结)、无 onAccounting;helper 按 role "consult" 恒停靠 pending(非挂起期也停靠)。
|
|
145
|
+
* Cancelled sessions produce no digest (T-R17c).
|
|
146
|
+
*/
|
|
147
|
+
function sessionSettled(agent, session) {
|
|
148
|
+
agent?._consultSessions?.delete(String(session.id))
|
|
149
|
+
if (session.stopped) return // cancelled — no digest (T-R17c)
|
|
150
|
+
const entry = {
|
|
151
|
+
id: String(session.id),
|
|
152
|
+
role: "consult",
|
|
153
|
+
report: composeConsultDigest(session),
|
|
154
|
+
error: null, done: true, status: "done", cancelled: false,
|
|
155
|
+
relayPrefix: null, startedAt: null, _settle: null, _settleSeq: 0,
|
|
156
|
+
}
|
|
157
|
+
settleAsyncEntry(agent, entry, { pool: null, ctx: null })
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Digest body for a fully-settled session — title + one annotated line per
|
|
161
|
+
* reply (failed replies marked per-model — round2 #7; full text is injected
|
|
162
|
+
* verbatim and may be >64K → offloaded with a preview at injection). */
|
|
163
|
+
export function composeConsultDigest(session) {
|
|
164
|
+
const replies = session.replies ?? []
|
|
165
|
+
const parts = replies.map((r) =>
|
|
166
|
+
r.failed
|
|
167
|
+
? `- [${r.model}] (failed): ${r.reply}`
|
|
168
|
+
: `- [${r.model}]: ${r.reply}`)
|
|
169
|
+
const counts = `${replies.length} of ${session.total} models replied (${session.failed} failed)`
|
|
170
|
+
return `[System reminder: consultation #${session.id} finished — ${counts}]\n${parts.join("\n")}`
|
|
118
171
|
}
|
|
119
172
|
|
|
120
|
-
|
|
173
|
+
/**
|
|
174
|
+
* Inject one settled consult family entry into the parent history as a
|
|
175
|
+
* user-role reminder (run-start injection — agent.mjs; same shape rules as
|
|
176
|
+
* injectAsyncResult: XML-escaped, >64K offloaded with preview + path). Consumed
|
|
177
|
+
* = the caller splices the entry out of the pending single container
|
|
178
|
+
* (_pendingAsyncResults——ASYNC-RESULT-CONTAINER.md D2——role 分发注入)。
|
|
179
|
+
*/
|
|
180
|
+
export async function injectConsultResult(agent, entry) {
|
|
181
|
+
const body = entry?.report ?? "(no consultation result)"
|
|
182
|
+
const preview = await offloadToolResult(String(body), `consult-${entry.id ?? "session"}`)
|
|
183
|
+
pushReal(agent, {
|
|
184
|
+
role: "user",
|
|
185
|
+
content: escapeXml(preview),
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function settleChild(agent, session, id, label, ok, payload, emitDone) {
|
|
121
190
|
if (ok) {
|
|
122
191
|
session.received++
|
|
123
192
|
session.replies.push({ model: label, reply: payload })
|
|
@@ -128,7 +197,8 @@ function settleChild(session, id, label, ok, payload) {
|
|
|
128
197
|
session.replies.push({ model: label, reply: `(consultation failed: ${payload})`, failed: true })
|
|
129
198
|
}
|
|
130
199
|
session.pending--
|
|
131
|
-
|
|
200
|
+
emitDone?.() // per-child TUI block freeze at settle (R17 — child's activity card is done)
|
|
201
|
+
if (session.pending === 0) sessionSettled(agent, session)
|
|
132
202
|
}
|
|
133
203
|
|
|
134
204
|
async function runConsultChild(ctx, session, id, m, problem, ctrl) {
|
|
@@ -145,6 +215,27 @@ async function runConsultChild(ctx, session, id, m, problem, ctrl) {
|
|
|
145
215
|
}
|
|
146
216
|
let watchdog = armWatchdog()
|
|
147
217
|
const label = consultLabel(m)
|
|
218
|
+
// LOGGING(LOGGING.md):child:*(consult)——logSettle 在函数作用域声明(外层 catch
|
|
219
|
+
// 覆盖 spawn 前失败路径);spawn 事件在 relay 建立后发射(logArmed 翻转——provider/
|
|
220
|
+
// 创建失败 = 从未启动,不落子事件、错误仅经 settleChild 进会话)。
|
|
221
|
+
let childLogId = null
|
|
222
|
+
let logT0 = 0
|
|
223
|
+
let logArmed = false
|
|
224
|
+
let logDone = false
|
|
225
|
+
const logSettle = (kind, payload) => {
|
|
226
|
+
if (!logArmed || logDone || !childLogId) return
|
|
227
|
+
logDone = true
|
|
228
|
+
const ms = Date.now() - logT0
|
|
229
|
+
const base = { role: "consult", id: childLogId, ms }
|
|
230
|
+
if (kind === "ok" || kind === "partial") logEvent("child:done", { ...base, kind })
|
|
231
|
+
else logEvent("child:error", { ...base, err: errText(payload, 200) })
|
|
232
|
+
}
|
|
233
|
+
// R17: relay prefix assigned before the child runner arms — the per-child TUI
|
|
234
|
+
// block freeze emits only when a block actually exists (relay established).
|
|
235
|
+
let relayPrefix = null
|
|
236
|
+
const settle = (ok, payload) => settleChild(agent, session, id, label, ok, payload, relayPrefix
|
|
237
|
+
? () => ctx.callbacks?.onToken?.(`${relayPrefix}⟦ev⟧done\x1e0\x1e0\x1edone\x1e`)
|
|
238
|
+
: null)
|
|
148
239
|
try {
|
|
149
240
|
// Provider resolution: consultModels entries are { provider, model, effort? } — resolve
|
|
150
241
|
// via the subagent's provider resolver ("provider:model" handles cross-provider picks).
|
|
@@ -156,9 +247,10 @@ async function runConsultChild(ctx, session, id, m, problem, ctrl) {
|
|
|
156
247
|
}
|
|
157
248
|
// Clamp the pool's effort to the model's reasoningEffortEnum — an out-of-enum
|
|
158
249
|
// value makes provider/core throw on EVERY chat call (candidate dies on takeoff).
|
|
159
|
-
// Symmetric with escalate
|
|
160
|
-
// effort "high" (enum is xhigh/medium/low).
|
|
161
|
-
// (the provider preset default may ALSO be
|
|
250
|
+
// Symmetric with the escalate action (subagent action:"escalate"); 2026-08-16
|
|
251
|
+
// a real consult died on qwen3.8-max effort "high" (enum is xhigh/medium/low).
|
|
252
|
+
// Out-of-enum: DROP the effort entirely (the provider preset default may ALSO be
|
|
253
|
+
// out-of-enum for this override model).
|
|
162
254
|
clampEffort(provider, m.model, m.effort)
|
|
163
255
|
|
|
164
256
|
// Read-only consultant: filter the parent tool set down to readonly tools + main_history.
|
|
@@ -182,7 +274,13 @@ async function runConsultChild(ctx, session, id, m, problem, ctrl) {
|
|
|
182
274
|
// Activity relay via the unified spawn-child pipeline (§7.2 D3): `consult#<subId>/`
|
|
183
275
|
// prefix (same channel subagent uses — parallel consultants stay independent) +
|
|
184
276
|
// onToolOutput passthrough so the consultant's tool output lands in its TUI block.
|
|
185
|
-
|
|
277
|
+
relayPrefix = makeRelay(agent, "consult", ctx.callbacks?.onToken, provider.model ?? "")
|
|
278
|
+
// LOGGING:arm(spawn 事件——relay 建立后;子内事件归属 _logId)
|
|
279
|
+
childLogId = relayPrefix.slice(0, -1)
|
|
280
|
+
child._logId = childLogId
|
|
281
|
+
logT0 = Date.now()
|
|
282
|
+
logArmed = true
|
|
283
|
+
logEvent("child:spawn", { role: "consult", id: childLogId, kind: "consult" })
|
|
186
284
|
const childCallbacks = wrapChildCallbacks(relayPrefix, ctx.callbacks ?? {})
|
|
187
285
|
let declined = false // review #1: guard against double-settle when onDeclined fired
|
|
188
286
|
|
|
@@ -215,46 +313,50 @@ async function runConsultChild(ctx, session, id, m, problem, ctrl) {
|
|
|
215
313
|
},
|
|
216
314
|
onDeclined: (e) => {
|
|
217
315
|
declined = true
|
|
218
|
-
|
|
316
|
+
settle(false, `turn cap reached (${e.turn} turns) — stopped, diagnosis may be partial`)
|
|
317
|
+
logSettle("partial", null)
|
|
219
318
|
return undefined
|
|
220
319
|
},
|
|
221
320
|
},
|
|
222
321
|
)
|
|
223
322
|
// Review #1 fix: onDeclined already settled this child as a failed reply —
|
|
224
323
|
// settling again here would push a phantom empty success reply and decrement
|
|
225
|
-
// `pending` twice (negative pending
|
|
226
|
-
|
|
227
|
-
|
|
324
|
+
// `pending` twice (negative pending would re-enter the settle routing).
|
|
325
|
+
if (!declined) {
|
|
326
|
+
settle(true, String(result ?? ""))
|
|
327
|
+
logSettle("ok", null)
|
|
328
|
+
}
|
|
228
329
|
} catch (e) {
|
|
229
330
|
// Runner errors (incl. the watchdog's abort) settle as a failed reply — the
|
|
230
331
|
// continue/declined paths are already handled inside runWithContinue.
|
|
231
332
|
const note = timedOut ? `consultation timed out after ${Math.round(timeoutMs / 60000)}min (agent.consultTimeoutMs)` : e?.message ?? String(e)
|
|
232
|
-
|
|
333
|
+
settle(false, note)
|
|
334
|
+
logSettle("error", note)
|
|
233
335
|
}
|
|
234
336
|
} catch (e) {
|
|
235
337
|
// Errors BEFORE the runner (provider resolution, createAgent) or a throwing
|
|
236
338
|
// continue-prompt settle as failed replies — the runner's own errors are already
|
|
237
|
-
// handled inside the loop above.
|
|
238
|
-
|
|
339
|
+
// handled inside the loop above. relayPrefix is null on these paths — no TUI
|
|
340
|
+
// block was ever opened, so no freeze event is emitted.
|
|
341
|
+
settle(false, e?.message ?? String(e))
|
|
342
|
+
logSettle("error", e?.message ?? String(e))
|
|
239
343
|
} finally {
|
|
240
344
|
clearTimeout(watchdog)
|
|
241
345
|
}
|
|
242
346
|
}
|
|
243
347
|
|
|
244
|
-
/** Turn-end cleanup (
|
|
245
|
-
*
|
|
348
|
+
/** Turn-end / session-end abort cleanup (R17 — call sites: the Ctrl+C abort
|
|
349
|
+
* branches of finalizeAgentTurn and the suspension driver). Consultation
|
|
350
|
+
* sessions are now cross-turn background work (like async subagents): a NORMAL
|
|
351
|
+
* turn end keeps them alive — this runs only when the user stops everything:
|
|
352
|
+
* every leftover session is marked stopped (its settles never reach the digest
|
|
353
|
+
* stream — T-R17c) and its controllers aborted. */
|
|
246
354
|
export function cleanupConsultSessions(agent) {
|
|
247
355
|
for (const s of agent._consultSessions?.values() ?? []) {
|
|
248
356
|
s.stopped = true
|
|
249
357
|
for (const c of s.controllers ?? []) { try { c.abort() } catch { /* already settled */ } }
|
|
250
|
-
for (const w of s.waiters?.splice(0) ?? []) { try { w() } catch { /* noop */ } }
|
|
251
358
|
}
|
|
252
359
|
agent._consultSessions?.clear()
|
|
253
|
-
// NOTE: deliberately void (consult P3, 2026-08-30). The { stopped: true } marker
|
|
254
|
-
// only reaches the TUI via the consult_stop TOOL return (onToolResult freezes
|
|
255
|
-
// blocks on tool calls) — cleanup runs from the turn finally, where the block
|
|
256
|
-
// freeze is owned by freezeAllSubTasks + sweepToolBlocks, so a return here is
|
|
257
|
-
// dead weight. Blocks still get frozen on interrupt via that sweep.
|
|
258
360
|
}
|
|
259
361
|
|
|
260
362
|
export const consultStartTool = {
|
|
@@ -265,8 +367,11 @@ export const consultStartTool = {
|
|
|
265
367
|
"Start a parallel multi-model consultation (会诊) for a hard problem you are stuck on (repeated failures, no headway). " +
|
|
266
368
|
"Call it directly when the user asks for 会诊 / consult — an explicit user request applies even if you are not 'stuck'. " +
|
|
267
369
|
"Several configured models (agent.consultModels) analyze the same problem INDEPENDENTLY and in parallel. " +
|
|
268
|
-
"Non-blocking: returns immediately with a consult id
|
|
269
|
-
"
|
|
370
|
+
"Non-blocking: returns immediately with a consult id; the consultants keep running in the background across turns. " +
|
|
371
|
+
"When EVERY model has replied (or failed), the full verdict text is delivered to you automatically — as a system " +
|
|
372
|
+
"reminder at the next run start, or digested on its own while the session is idle — judge and adopt each opinion " +
|
|
373
|
+
"yourself with your own tools (opinions are suggestions, not gates). To stop a session early (user changed their " +
|
|
374
|
+
"mind / wants the tokens back), call consult_stop(id) — a stopped session delivers no digest.\n" +
|
|
270
375
|
"Parameters:\n" +
|
|
271
376
|
"- problem (required): a brief — the symptom, what you already tried (failure trail), and entry-point files. " +
|
|
272
377
|
"Do NOT paste raw error logs; consultants pull the main session history themselves via their main_history tool.\n" +
|
|
@@ -296,7 +401,7 @@ export const consultStartTool = {
|
|
|
296
401
|
agent._consultSessions ??= new Map()
|
|
297
402
|
const id = String((agent._consultIdCounter = (agent._consultIdCounter ?? 0) + 1))
|
|
298
403
|
const session = {
|
|
299
|
-
id, controllers: [], replies: [], pending: 0,
|
|
404
|
+
id, controllers: [], replies: [], pending: 0,
|
|
300
405
|
failed: 0, terminated: 0, stopped: false, received: 0, total: run.length,
|
|
301
406
|
models: run.map(consultLabel),
|
|
302
407
|
}
|
|
@@ -306,100 +411,45 @@ export const consultStartTool = {
|
|
|
306
411
|
session.pending++
|
|
307
412
|
const ctrl = new AbortController()
|
|
308
413
|
session.controllers.push(ctrl)
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
414
|
+
// D6 buildChildSignal 单点(ASYNC-RESULT-CONTAINER.md D5——consult 补 _sessionSignal
|
|
415
|
+
// 兜底:挂起会话内的 consult children 持会话 signal,digest 自身 Ctrl+C 不误伤)。
|
|
416
|
+
const baseSignal = buildChildSignal(agent, ctx)
|
|
417
|
+
if (baseSignal) {
|
|
418
|
+
if (baseSignal.aborted) ctrl.abort()
|
|
419
|
+
else baseSignal.addEventListener("abort", () => ctrl.abort(), { once: true })
|
|
312
420
|
}
|
|
313
|
-
// Fire and forget — each child settles itself into the session
|
|
421
|
+
// Fire and forget — each child settles itself into the session; the session
|
|
422
|
+
// routes to the pending single container when every child has settled (R17).
|
|
314
423
|
runConsultChild(ctx, session, id, m, problem, ctrl)
|
|
315
424
|
}
|
|
316
425
|
return JSON.stringify({ id, models: session.models })
|
|
317
426
|
},
|
|
318
427
|
}
|
|
319
428
|
|
|
320
|
-
export const consultCheckTool = {
|
|
321
|
-
name: "consult_check",
|
|
322
|
-
readonly: true,
|
|
323
|
-
description:
|
|
324
|
-
"Read the NEXT consultation reply (whichever model answered first). Blocks until a reply arrives or all models " +
|
|
325
|
-
"have settled. The reply is raw and unjudged — verify/adopt it with your own tools. When done is true, no more " +
|
|
326
|
-
"replies are coming.\n" +
|
|
327
|
-
"Call it ALONE in a turn — do NOT batch it with calls that depend on its reply (readonly tools run in parallel).\n" +
|
|
328
|
-
"Replies arrive in arrival order: call it repeatedly (n = 1, 2, 3, …) until done is true.\n" +
|
|
329
|
-
"Parameters:\n" +
|
|
330
|
-
"- id (required): the consult id from consult_start\n" +
|
|
331
|
-
"- n (required): the 1-based read number for this consult — pass 1 on the first check, 2 on the next, and so on. It exists so consecutive checks are distinct tool calls (loop detectors) and the transcript reads as a sequence.",
|
|
332
|
-
parameters: {
|
|
333
|
-
type: "object",
|
|
334
|
-
properties: {
|
|
335
|
-
id: { type: "string", description: "Consult id" },
|
|
336
|
-
n: { type: "number", description: "1-based read number: 1 for the first check, incrementing with each subsequent check of the same consult" },
|
|
337
|
-
},
|
|
338
|
-
required: ["id", "n"],
|
|
339
|
-
},
|
|
340
|
-
async execute({ id, n: _n }, ctx) {
|
|
341
|
-
const s = ctx.agent?._consultSessions?.get(String(id))
|
|
342
|
-
if (!s) return JSON.stringify({ error: "unknown consult id" })
|
|
343
|
-
const abortAll = () => { for (const c of s.controllers) { try { c.abort() } catch { /* noop */ } } }
|
|
344
|
-
if (ctx.signal?.aborted) abortAll()
|
|
345
|
-
|
|
346
|
-
for (;;) {
|
|
347
|
-
if (s.replies.length > 0) {
|
|
348
|
-
const r = s.replies.shift()
|
|
349
|
-
return JSON.stringify({
|
|
350
|
-
reply: r.reply, model: r.model, failedReply: r.failed === true,
|
|
351
|
-
received: s.received,
|
|
352
|
-
failed: s.failed,
|
|
353
|
-
terminated: s.terminated ?? 0, total: s.total,
|
|
354
|
-
done: s.replies.length === 0 && s.pending === 0,
|
|
355
|
-
})
|
|
356
|
-
}
|
|
357
|
-
if (s.pending === 0) {
|
|
358
|
-
return JSON.stringify({ done: true, received: s.received, failed: s.failed, total: s.total })
|
|
359
|
-
}
|
|
360
|
-
const stopped = await new Promise((resolve) => {
|
|
361
|
-
function cleanup() {
|
|
362
|
-
const i = s.waiters.indexOf(w)
|
|
363
|
-
if (i >= 0) s.waiters.splice(i, 1)
|
|
364
|
-
ctx.signal?.removeEventListener("abort", onAbort)
|
|
365
|
-
}
|
|
366
|
-
function w() { cleanup(); resolve(false) }
|
|
367
|
-
function onAbort() { cleanup(); abortAll(); resolve(true) }
|
|
368
|
-
s.waiters.push(w)
|
|
369
|
-
if (ctx.signal) {
|
|
370
|
-
if (ctx.signal.aborted) { onAbort(); return }
|
|
371
|
-
ctx.signal.addEventListener("abort", onAbort, { once: true })
|
|
372
|
-
}
|
|
373
|
-
})
|
|
374
|
-
if (stopped) return JSON.stringify({ done: true, stopped: true, received: s.received, failed: s.failed, total: s.total })
|
|
375
|
-
}
|
|
376
|
-
},
|
|
377
|
-
}
|
|
378
|
-
|
|
379
429
|
export const consultStopTool = {
|
|
380
430
|
name: "consult_stop",
|
|
381
431
|
readonly: false,
|
|
382
432
|
sideEffectExempt: true,
|
|
383
433
|
description:
|
|
384
|
-
"
|
|
385
|
-
"
|
|
434
|
+
"Cancel a still-running consultation session (会诊) — the user changed their mind, the problem resolved, or you want the tokens back. " +
|
|
435
|
+
"Aborts every consultant that is still running; a stopped session delivers NO digest (R17 — its already-collected partial replies are dropped). " +
|
|
436
|
+
"Sessions that finished on their own are no longer cancellable — their verdict text is delivered automatically.\n" +
|
|
437
|
+
"Returns JSON {abandoned: <pending count>, cancelled: true} — or {error: \"unknown consult id\"} (already finished/cancelled).\n" +
|
|
386
438
|
"Parameters:\n" +
|
|
387
|
-
"- id (required): the consult id from consult_start
|
|
388
|
-
"- n (required): incrementing call number for this consult (next value after the last consult_check/consult_stop) — keeps repeated calls distinct.",
|
|
439
|
+
"- id (required): the consult id from consult_start",
|
|
389
440
|
parameters: {
|
|
390
441
|
type: "object",
|
|
391
442
|
properties: {
|
|
392
443
|
id: { type: "string", description: "Consult id" },
|
|
393
|
-
n: { type: "number", description: "Incrementing call number for this consult (see consult_check)" },
|
|
394
444
|
},
|
|
395
|
-
required: ["id"
|
|
445
|
+
required: ["id"],
|
|
396
446
|
},
|
|
397
|
-
async execute({ id
|
|
447
|
+
async execute({ id }, ctx) {
|
|
398
448
|
const s = ctx.agent?._consultSessions?.get(String(id))
|
|
399
449
|
if (!s) return JSON.stringify({ error: "unknown consult id" })
|
|
400
450
|
const abandoned = s.pending
|
|
401
451
|
s.stopped = true
|
|
402
452
|
for (const c of s.controllers) { try { c.abort() } catch { /* already settled */ } }
|
|
403
|
-
return JSON.stringify({
|
|
453
|
+
return JSON.stringify({ abandoned, cancelled: true })
|
|
404
454
|
},
|
|
405
455
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design-token.mjs — design-token utilities(2026-09-08 自 advisor-async.mjs 提取——
|
|
3
|
+
* advisor-async 随 DESIGN-TOKEN-SETTLEMENT D1 落盘块再越 500 行硬限;本工具组 verbatim
|
|
4
|
+
* 迁入,零语义变)。
|
|
5
|
+
*
|
|
6
|
+
* 沿革:初由 agent-tools/advisor.mjs 迁至 advisor-async.mjs(sync wrapper 与 async
|
|
7
|
+
* settle 共享一套实现——无 wrapper↔runner 环),再由 advisor-async.mjs 迁入本文件。
|
|
8
|
+
* 既有 import 面不变:advisor-async.mjs re-export 全部 7 个导出(advisor.mjs 与测试
|
|
9
|
+
* 的 import 路径原样保留——advisor.mjs 再转发 validateDesignToken 给 spawn 门禁)。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { randomUUID } from "node:crypto"
|
|
13
|
+
import { tokenExpiryMs } from "../token-ttl.mjs"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* F2c/F2e (§29.1 2026-09-07): the engine-generated Approved suffix — ONE builder
|
|
17
|
+
* shared by the sync settle and the async settle (and the prior stores, which
|
|
18
|
+
* strip it with stripApprovedSuffix — exact-suffix truncation, never a regex
|
|
19
|
+
* guess, zero collateral). The slot count is a point-in-time snapshot taken at
|
|
20
|
+
* settle time — the situation at spawn time may differ (F2d backs that up).
|
|
21
|
+
*/
|
|
22
|
+
export function buildApprovedSuffix(designToken, designId, slotCount) {
|
|
23
|
+
return `Approved. Pass this exact token to eng-coder (designToken parameter): ${designToken}\ndesignId: ${designId} (pass as the designId parameter when spawning eng-coder — optional while this session holds a single design; ${slotCount} approved design slot(s) held as of this approval, and the count may have changed since — with several designs the spawn gate refuses a missing designId and lists the held ids)`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** F2e (§29.1): strip the engine-generated Approved suffix from a report before
|
|
27
|
+
* it becomes a prior — the suffix is deterministic (buildApprovedSuffix), so the
|
|
28
|
+
* truncation is exact; a text not ending in it passes through untouched. */
|
|
29
|
+
export function stripApprovedSuffix(text, suffix) {
|
|
30
|
+
if (typeof text !== "string" || !suffix) return text
|
|
31
|
+
return text.endsWith(suffix) ? text.slice(0, text.length - suffix.length).trim() : text
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const TOKEN_TTL_DEFAULT_MS = 7 * 24 * 3600 * 1000 // 7-day ceiling (v2 2026-08-25)
|
|
35
|
+
|
|
36
|
+
/** Effective token TTL: config override with runtime validation (timeoutMs precedent). */
|
|
37
|
+
export function effectiveTokenTtlMs(agent) {
|
|
38
|
+
const cfg = agent?.config?.agent?.engTokenTtlMs
|
|
39
|
+
return (Number.isFinite(cfg) && cfg > 0) ? cfg : TOKEN_TTL_DEFAULT_MS
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Mint an unsigned design token with expiration (2026-09-06: HMAC layer removed —
|
|
43
|
+
* the token is a FLOW credential: uuid:expiresAt, exact slot match + TTL only). */
|
|
44
|
+
export function generateDesignToken(agent) {
|
|
45
|
+
const uuid = randomUUID()
|
|
46
|
+
const expiresAt = Math.floor(Date.now() + effectiveTokenTtlMs(agent))
|
|
47
|
+
return `${uuid}:${expiresAt}`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Validate a design token: format + expiration — ALL fail-closed (v2 2026-08-25).
|
|
51
|
+
* R16: format/expiry semantics live in token-ttl.mjs (tokenExpiryMs — single source
|
|
52
|
+
* shared with restore filtering / enter cleanup / spawn-gate slot deletion — D-R16b). */
|
|
53
|
+
export function validateDesignToken(token) {
|
|
54
|
+
const expiry = tokenExpiryMs(token)
|
|
55
|
+
return expiry !== null && expiry >= Date.now()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Build a [DESIGN-TOKEN:...] regex matching the FULL token (uuid:expiresAt). */
|
|
59
|
+
export function makeDesignTokenRegex(token, flags = "") {
|
|
60
|
+
const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
61
|
+
return new RegExp(
|
|
62
|
+
`(?:^|\\s|\`|\\*)\\[DESIGN-TOKEN:\\s*${escaped}\\s*\\](?:\\s|$|\`|\\*)`,
|
|
63
|
+
flags + "ms"
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Shared design-review settlement (sync wrapper + async settle): the token echo
|
|
69
|
+
* IS the verdict — the advisor echoes it only on approval. On echo: slot the
|
|
70
|
+
* token under designId (+ eng-coder gate flag; single-value mirror retired per
|
|
71
|
+
* DESIGN-TOKEN-SETTLEMENT D3) and return
|
|
72
|
+
* the clean output with the Approved suffix; the review instance CLOSES (a
|
|
73
|
+
* later review of the same doc-set starts a fresh full review). On non-echo:
|
|
74
|
+
* strip every dead token occurrence and return the findings text — slots stay
|
|
75
|
+
* untouched (方案 ②: a failed re-review revokes nothing).
|
|
76
|
+
* @returns {{passed: boolean, output: string}}
|
|
77
|
+
*/
|
|
78
|
+
export function settleDesignReview(agent, run, designToken, rawResult) {
|
|
79
|
+
if (!designToken || typeof rawResult !== "string") {
|
|
80
|
+
return { passed: false, output: rawResult ?? "" }
|
|
81
|
+
}
|
|
82
|
+
const tokenPattern = makeDesignTokenRegex(designToken)
|
|
83
|
+
if (!tokenPattern.test(rawResult)) {
|
|
84
|
+
const stripped = rawResult.replace(makeDesignTokenRegex(designToken, "g"), "").trim()
|
|
85
|
+
return { passed: false, output: stripped || "Advisor: design review did not pass." }
|
|
86
|
+
}
|
|
87
|
+
// Echoed the token → review passed. Issue it to the parent for eng-coder.
|
|
88
|
+
agent._engDesignTokens ??= new Map()
|
|
89
|
+
agent._engDesignTokens.set(run.designId, designToken)
|
|
90
|
+
// DESIGN-TOKEN-SETTLEMENT D3(2026-09-08):单值镜像 `_engDesignToken` 退役——
|
|
91
|
+
// 只写多槽 Map(AC3 零镜像写);settle 落盘由调用方(settleAdvisorRun)当场做。
|
|
92
|
+
// Unlock the dispatch design gate for eng-coder SELF-review (defense-in-depth —
|
|
93
|
+
// see the sync wrapper's note: unreachable today, kept for parity).
|
|
94
|
+
if (agent._role === "eng-coder") agent._engDesignReviewed = true
|
|
95
|
+
run.open = false // approval closes this doc-set instance — next review is fresh
|
|
96
|
+
const clean = rawResult.replace(makeDesignTokenRegex(designToken, "g"), "").trim()
|
|
97
|
+
// F2c (§29.1): id echo + omission guidance + point-in-time slot snapshot — the
|
|
98
|
+
// same suffix the F2e prior stores strip with (stored for the exact truncation).
|
|
99
|
+
run.approvedSuffix = buildApprovedSuffix(designToken, run.designId, agent._engDesignTokens.size)
|
|
100
|
+
return {
|
|
101
|
+
passed: true,
|
|
102
|
+
output: `${clean}\n\n${run.approvedSuffix}`,
|
|
103
|
+
}
|
|
104
|
+
}
|
package/src/agent-tools/eng.mjs
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* eng tool: enter/exit engineering mode.
|
|
3
3
|
* In engineering mode the agent follows design-before-code methodology.
|
|
4
|
-
* Toggled here at session level;
|
|
4
|
+
* Toggled here at session level (in-memory flag; the session slot is the sole authority —
|
|
5
|
+
* saveSession round-trips it at turn end; /eng writes the slot directly). The old ctx
|
|
6
|
+
* state-persist hook was removed 2026-09-08 (ENG-SESSION-PROVIDER-CLEANUP D1.2): no
|
|
7
|
+
* provider existed and its legacy payload keys were never read by applySession.
|
|
8
|
+
* R16 (2026-09-06): design tokens are session-level flow credentials — mode toggles
|
|
9
|
+
* do NOT clear them (ON→OFF keeps, OFF→ON does not require a fresh review); only TTL
|
|
10
|
+
* expiry cleans tokens, at three points: restore filtering (session.mjs applySession) /
|
|
11
|
+
* eng enter expired cleanup (below + cmd-eng ON) / spawn-gate expired rejection
|
|
12
|
+
* (subagent-spawn.mjs). See docs/design/ENG-TOKEN-BINDING-TUNING.md §5/§5.1 (F-R16).
|
|
5
13
|
*/
|
|
6
14
|
import { ENG_ON_REMINDER, ENG_OFF_REMINDER } from "../agent.mjs"
|
|
15
|
+
import { purgeExpiredDesignTokens } from "../token-ttl.mjs"
|
|
7
16
|
|
|
8
17
|
export const engTool = {
|
|
9
18
|
name: "eng",
|
|
10
19
|
description:
|
|
11
|
-
"Enter or exit engineering mode. In engineering mode, follow design-before-code: write a design document, run advisor design review, get user approval, then implement via eng-coder subagents."
|
|
20
|
+
"Enter or exit engineering mode. In engineering mode, follow design-before-code: write a design document, run advisor design review, get user approval, then implement via eng-coder subagents. " +
|
|
21
|
+
"Returns the mode state — 'Engineering mode activated/exited' (an already-active state is acknowledged).",
|
|
12
22
|
parameters: {
|
|
13
23
|
type: "object",
|
|
14
24
|
properties: {
|
|
@@ -21,50 +31,36 @@ export const engTool = {
|
|
|
21
31
|
ctx.agent.config.agent ??= {}
|
|
22
32
|
if (args.action === "exit") {
|
|
23
33
|
ctx.agent.config.agent.engineering = false
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
ctx.agent._engDesignReviewed = false // reset gate state
|
|
34
|
+
// R16 (F-R16a): OFF 不清 token——有效 token 跨模式存活(设计评审过的产物不因
|
|
35
|
+
// 开关重复烧)。过期清理跑在另外三处(恢复过滤 / 开模式 / spawn 门禁拒)。
|
|
27
36
|
ctx.agent._advisorRound = 0 // reset convergence budget
|
|
37
|
+
ctx.agent._advisorRuns = new Map() // §24 D-24b: per-review instances die with the mode (fresh cycles)
|
|
28
38
|
ctx.agent._touchedFiles = [] // clear mutation tracking
|
|
29
39
|
ctx.agent._lastEngState = false
|
|
30
40
|
ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
|
|
31
41
|
ctx.agent._pendingReminders.push(ENG_OFF_REMINDER)
|
|
32
|
-
// 持久化工程模式状态到会话
|
|
33
|
-
if (ctx.persistState) {
|
|
34
|
-
await ctx.persistState({
|
|
35
|
-
engineering: false,
|
|
36
|
-
engDesignToken: null,
|
|
37
|
-
engDesignReviewed: false,
|
|
38
|
-
advisorRound: 0,
|
|
39
|
-
touchedFiles: []
|
|
40
|
-
})
|
|
41
|
-
}
|
|
42
42
|
return "Engineering mode exited. Standard discipline now applies. You may edit files directly."
|
|
43
43
|
}
|
|
44
44
|
if (args.action === "enter") {
|
|
45
|
-
// Idempotent enter (v2 2026-08-25): already in engineering mode → no-op
|
|
46
|
-
//
|
|
47
|
-
//
|
|
45
|
+
// Idempotent enter (v2 2026-08-25): already in engineering mode → pure no-op
|
|
46
|
+
// (cleanup only runs on a real off→on transition — T-R16c precondition: first
|
|
47
|
+
// exit/OFF, then enter; an already-on enter must not touch tokens at all).
|
|
48
48
|
if (ctx.agent.config.agent.engineering) {
|
|
49
49
|
return "Engineering mode already active. Existing design tokens stay valid."
|
|
50
50
|
}
|
|
51
51
|
ctx.agent.config.agent.engineering = true
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
// R16 (F-R16b ②): off→on 不重评——只清过期 token(用户裁定"打开工程模式时
|
|
53
|
+
// 应该清理"),有效 token 原样保留——遍历 Map 删过期,返回文案含清理个数。
|
|
54
|
+
const cleared = purgeExpiredDesignTokens(ctx.agent)
|
|
55
|
+
ctx.agent._advisorRuns = new Map() // §24 D-24b: per-review instances die with the mode (fresh cycles)
|
|
54
56
|
ctx.agent._lastEngState = true
|
|
55
57
|
ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
|
|
56
58
|
ctx.agent._pendingReminders.push(ENG_ON_REMINDER)
|
|
57
|
-
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
engineering: true,
|
|
61
|
-
engDesignToken: null,
|
|
62
|
-
engDesignReviewed: false,
|
|
63
|
-
advisorRound: 0,
|
|
64
|
-
touchedFiles: []
|
|
65
|
-
})
|
|
59
|
+
let msg = "Engineering mode activated. Design-before-code enforced: write a design document in docs/, run advisor with type='design', get user approval, then implement via eng-coder subagents."
|
|
60
|
+
if (cleared > 0) {
|
|
61
|
+
msg += ` Cleared ${cleared} expired design token${cleared === 1 ? "" : "s"} — 清 ${cleared} 个过期 token,有效 token 保留(TTL 内不重评)。`
|
|
66
62
|
}
|
|
67
|
-
return
|
|
63
|
+
return msg
|
|
68
64
|
}
|
|
69
65
|
return "Invalid action: expected 'enter' or 'exit'"
|
|
70
66
|
},
|