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
|
@@ -19,15 +19,77 @@
|
|
|
19
19
|
* → ./subagent-run.mjs——execute 只保留动作分流 + 装配调用 + 阻塞路径。
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import { gateEngCoderSpawn, TURN_CAP_MARK, emitNestedChildEvent } from "../agent/spawn-child.mjs"
|
|
22
|
+
import { gateEngCoderSpawn, TURN_CAP_MARK, STOPPED_MARK, emitNestedChildEvent } from "../agent/spawn-child.mjs"
|
|
23
23
|
import { logEvent, errText } from "../log.mjs"
|
|
24
|
+
import { abortError, deathLine } from "../abort-provenance.mjs"
|
|
24
25
|
import {
|
|
25
|
-
runChildPipeline, executeCancelAction,
|
|
26
|
+
runChildPipeline, executeCancelAction, enqueueAsk, mergeChildMutations,
|
|
26
27
|
} from "./subagent-async.mjs"
|
|
28
|
+
// SYNC-CANCEL(L52——2026-09-09):阻塞路径自属 AbortController 链到会话/回合基信号
|
|
29
|
+
// 的单点(async-settle.mjs D6——_sessionSignal ?? ctx.signal——与 async 条目 controller
|
|
30
|
+
// 链同一语义——挂起场景 base 命中而 ctx.signal 未 abort——R2)。
|
|
31
|
+
import { buildChildSignal } from "./async-settle.mjs"
|
|
27
32
|
import { executeStatusAction, executeEscalateAction, executePanelAction, executeObserveAction, executeSendAction } from "./subagent-actions.mjs"
|
|
28
33
|
import { prepareScheduling, buildSpawnChild, executeConsumeDesignAction } from "./subagent-spawn.mjs"
|
|
29
34
|
import { executeAsyncSpawn } from "./subagent-run.mjs"
|
|
30
35
|
|
|
36
|
+
// ─── SYNC-CANCEL 纯函数(可测——无 io)──────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* SYNC-CANCEL F2 catch 三分支分类(可测纯函数——R3 收紧):
|
|
40
|
+
* ① "base" 整回合停:ctx.signal 或 baseSignal(= parent._sessionSignal ?? ctx.signal——
|
|
41
|
+
* buildChildSignal——挂起会话场景 base 命中而 ctx.signal 未 abort——R2)aborted →
|
|
42
|
+
* 现状保留(emitNestedChildEvent stopped + rethrow);
|
|
43
|
+
* ② "targeted" 定向中止:err 是 AbortError 且自属 ctrl aborted(且非整回合停)→
|
|
44
|
+
* 折叠 stopped partial 报告(父回合继续——merge/STOPPED_MARK/警示);
|
|
45
|
+
* ③ "error" 其他错误 → 现状保留(child:error + rethrow)。
|
|
46
|
+
* ⚠ 查 baseSignal 非仅 ctx.signal——挂起 digest 场景 child 链 _sessionSignal(R2——
|
|
47
|
+
* digest 自身 Ctrl+I/Ctrl+C 不误伤;会话 Stop 逐链中止必须归 ①)。
|
|
48
|
+
*/
|
|
49
|
+
export function classifySyncAbort(ctxSignal, baseSignal, ctrlSignal, err) {
|
|
50
|
+
if (ctxSignal?.aborted || baseSignal?.aborted) return "base"
|
|
51
|
+
if (err?.name === "AbortError" && ctrlSignal?.aborted) return "targeted"
|
|
52
|
+
return "error"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* SYNC-CANCEL F1/F5 中止控制器装配(可测):sync 阻塞 spawn 建**自属** AbortController
|
|
57
|
+
* (childRunOpts.signal 覆写为 ctrl.signal——照抄 async 分支 subagent-run.mjs 覆写模式)
|
|
58
|
+
* ——ctrl 链到基信号:baseSignal aborted → ctrl.abort();否则 addEventListener("abort",
|
|
59
|
+
* → ctrl.abort(), { once:true })——Ctrl+C/I 整回合停语义不变(base abort 逐链传播——
|
|
60
|
+
* AC2);嵌套 sync spawn 递归可中止(内层链外层 ctrl.signal——逐层自属——AC4)。
|
|
61
|
+
* 注册 `parent._syncChildAborts`(key = relayPrefix 去尾——{ ctrl, stopped:false }——
|
|
62
|
+
* TUI ⏹ 门控 live 判据 + cancelSyncChild 定向中止目标——与 async 条目 controller 存池
|
|
63
|
+
* 分层一致)。返回 { ctrl, disarm }——disarm 注销 registry(调用方 try/finally 三路径
|
|
64
|
+
* 共用——R7 防跨回合残留)。
|
|
65
|
+
*/
|
|
66
|
+
export function armSyncChildAbort(parent, key, baseSignal) {
|
|
67
|
+
const ctrl = new AbortController()
|
|
68
|
+
if (baseSignal) {
|
|
69
|
+
// §20.3 站点 #10(第 24 批):hop 逐跳保 reason(下游可判定「谁杀的」)
|
|
70
|
+
if (baseSignal.aborted) ctrl.abort(baseSignal.reason)
|
|
71
|
+
else baseSignal.addEventListener("abort", () => ctrl.abort(baseSignal.reason), { once: true })
|
|
72
|
+
}
|
|
73
|
+
const registry = (parent._syncChildAborts ??= new Map())
|
|
74
|
+
registry.set(key, { ctrl, stopped: false })
|
|
75
|
+
const disarm = () => { registry.delete(key) }
|
|
76
|
+
return { ctrl, disarm }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* SYNC-CANCEL ② 折叠报告构建(可测纯函数——仿 runChildPipeline onDeclined partial 形态,
|
|
81
|
+
* subagent-async.mjs onDeclined:STOPPED_MARK + partial 警示 + 捕获输出 + eng-coder
|
|
82
|
+
* designId 后缀——AC3)。capturedOutput = child._capturedOutput(spawn-child.mjs
|
|
83
|
+
* runWithContinue capture 累积——子代理已流式输出的剥哨兵文本)。
|
|
84
|
+
*/
|
|
85
|
+
export function buildSyncStoppedReport(role, capturedOutput, designId) {
|
|
86
|
+
let report = `Subagent (${role}) ${STOPPED_MARK} — work may be partial; review recent_changes before deciding next steps.\nPartial output: ${capturedOutput || ""}`
|
|
87
|
+
if (role === "eng-coder") {
|
|
88
|
+
report += `\ndesignId: ${designId ?? "(single-design session — designId optional)"} — reuse it (with the same designToken) when re-spawning this eng-coder.`
|
|
89
|
+
}
|
|
90
|
+
return report
|
|
91
|
+
}
|
|
92
|
+
|
|
31
93
|
/**
|
|
32
94
|
* subagent tool — ONE tool, EIGHT actions (AGENT-LOOP.md §19/§19.5/§19.6/§19.8 +
|
|
33
95
|
* SUBAGENT-OBSERVE-SEND): spawn (default) / status (non-blocking pool query) / observe
|
|
@@ -41,7 +103,8 @@ import { executeAsyncSpawn } from "./subagent-run.mjs"
|
|
|
41
103
|
* - action:"spawn" roles: "explore" — read-only tools, search/read/analyze
|
|
42
104
|
* (suitable for codebase exploration); "coder" — full tool set, self-contained
|
|
43
105
|
* implementation tasks; "plan" — read-only planning; "eng-coder" —
|
|
44
|
-
* engineering-mode implementation (design-token gated)
|
|
106
|
+
* engineering-mode implementation (design-token gated); "eng-designer" —
|
|
107
|
+
* engineering-mode design writing (requirements + design docs, batchDoc gated).
|
|
45
108
|
* - no role specified — invalid by design since the 2026-08-25 fail-closed gate
|
|
46
109
|
* (role is mandatory; "no role → same tool set as parent" was removed with the
|
|
47
110
|
* coder-leak fix and the header text above predates it)
|
|
@@ -53,11 +116,11 @@ export const subagentTool = {
|
|
|
53
116
|
description:
|
|
54
117
|
"ONE tool, EIGHT actions (AGENT-LOOP.md §19/§19.5/§19.6/§19.8 + SUBAGENT-OBSERVE-SEND) — pick by what you need:\n" +
|
|
55
118
|
"- action:'spawn' (DEFAULT): spawn a sub-agent to handle an independent subtask in an isolated context; the sub-agent returns only its final report. Spawn MULTIPLE subagents in the SAME response for parallel work—they run concurrently.\n" +
|
|
56
|
-
"- action:'status': NON-BLOCKING progress query — returns immediately and consumes nothing. Give the spawn's id for one child ({id, role, status: running|queued|done, model, elapsedSec, turn, maxTurns, position?), or omit it for an overview of the whole pool ({overview: {running: [{id, role, model, elapsedSec, turn, maxTurns}], queued: [{id, role, position}], done: [{id, role}]}}). §19.5.6 touched-files summary: running entries also carry touchedFiles (first 5, relative to your cwd), touchedMore (count beyond 5) and, when nothing was touched yet, the placeholder touched (\"—(尚无改动)\"); queued (not yet started) entries carry the placeholder touched (\"—(未启动)\") — see what a running child has changed BEFORE deciding to cancel it. Use THIS to see progress — it never blocks and never consumes a result (async results are delivered to you automatically).\n" +
|
|
119
|
+
"- action:'status': NON-BLOCKING progress query — returns immediately and consumes nothing. Give the spawn's id for one child ({id, role, status: running|queued|done, model, elapsedSec, turn, maxTurns, position?), or omit it for an overview of the whole pool ({overview: {running: [{id, role, model, elapsedSec, turn, maxTurns}], queued: [{id, role, position}], done: [{id, role}]}}). §19.5.6 touched-files summary: running entries also carry touchedFiles (first 5, relative to your cwd), touchedMore (count beyond 5) and, when nothing was touched yet, the placeholder touched (\"—(尚无改动)\"); queued (not yet started) entries carry the placeholder touched (\"—(未启动)\") — see what a running child has changed BEFORE deciding to cancel it. Use THIS to see progress — it never blocks and never consumes a result (async results are delivered to you automatically). Background advisor reviews share this status surface (AGENT-LOOP.md §18): pass a review id and it answers in the same shape with role:'advisor' plus reviewType (design|code) / round / elapsedSec — and the overview lists reviews alongside subagents.\n" +
|
|
57
120
|
"- action:'observe': SEE what a running async subagent is DOING right now (progress vs stuck) — pass the spawn id. Returns {id, role, status, turn, maxTurns, touched…, currentTool? — array of tool name(s) currently executing (read from its dispatch state; omitted when none in flight — present when stuck in a long tool call), recentTurns: [last N one-line turn summaries, newest-first; default 5, parameterizable via recent]}. Readonly — observable on running/queued/done: running shows live activity, queued (not started) returns a placeholder, done (settled, report auto-delivered) returns the activity summary only — NOT the full report (that rides the auto channel; observe stays terse to keep your context lean). Use it to judge whether a long-running child is stuck vs progressing BEFORE deciding to cancel or steer it.\n" +
|
|
58
121
|
"- action:'send': STEER a running async subagent mid-flight — pass the spawn id + message (a direction like \"check X, don't fixate on Y\"). The message queues and the child consumes it at its next turn boundary as an ORDINARY user instruction (non-interrupting — its current tool finishes first; its convergence/audit discipline is unchanged — injection is guidance, not a deviation waiver). Returns {id, status:'delivered', queued}. Only a RUNNING async subagent is targetable — sync (you're waiting on it, no relay), queued (not started), settled or unknown ids error clearly. If the child settles before its next turn boundary, its settle report carries an 'undelivered' note so you don't assume the guidance landed.\n" +
|
|
59
|
-
"- action:'escalate' (飞刀 — a flown-in expert): hand an implementation task to a STRONGER model from your consult models (agent.consultModels). It gets WRITE access and does the work itself — reads, edits, runs tests — then returns a post-op report (what changed, why, verification). Use it when YOU judge the task calls for stronger hands (complex multi-file refactoring, an intractable bug, intricate algorithm work — or work beyond your comfortable ability); escalate EARLY, not after burning attempts. model: pick a candidate as 'provider:model' (default = the first consult model). Not available in engineering mode (implementation goes through eng-coder spawns there). DEFAULT-ASYNC at depth 0 (AGENT-LOOP.md §25 D-R17b): the launch returns an ack {id, role:'escalate', status:'running'} and the flight runs in the background (pooled with the other role-domain spawns) — its post-op report is delivered to you automatically with its mutations merged into your bookkeeping
|
|
60
|
-
"- action:'cancel': STOP one background subagent — pass the id from the async spawn return (REQUIRED — omitting it errors; a blanket cancel is unsupported, Ctrl+C stops everything). Running target aborts immediately ({id, status:'cancelled'}); a queued target is removed from the queue ({id, status:'cancelled', was:'queued'} and later queue positions shift forward). Other children and the session keep running — cancellation is targeted. Use it when a background child is going the wrong way (e.g. burning turns) and you must stop it before its report arrives. Cancel is a last resort: verify alarming signals with reliable checks (git/node — not guesses) first; prefer scoped recovery (restore a single affected file) over killing the child — a running child's in-flight work dies with it, partial changes stay unmerged and unaudited.\n" +
|
|
122
|
+
"- action:'escalate' (飞刀 — a flown-in expert): hand an implementation task to a STRONGER model from your consult models (agent.consultModels). It gets WRITE access and does the work itself — reads, edits, runs tests — then returns a post-op report (what changed, why, verification). Use it when YOU judge the task calls for stronger hands (complex multi-file refactoring, an intractable bug, intricate algorithm work — or work beyond your comfortable ability); escalate EARLY, not after burning attempts. model: pick a candidate as 'provider:model' (default = the first consult model). Not available in engineering mode (implementation goes through eng-coder spawns there). DEFAULT-ASYNC at depth 0 (AGENT-LOOP.md §25 D-R17b): the launch returns an ack {id, role:'escalate', status:'running'} and the flight runs in the background (pooled with the other role-domain spawns) — its post-op report is delivered to you automatically with its mutations merged into your bookkeeping.\n" +
|
|
123
|
+
"- action:'cancel': STOP one background subagent — pass the id from the async spawn return (REQUIRED — omitting it errors; a blanket cancel is unsupported, Ctrl+C stops everything). Running target aborts immediately ({id, status:'cancelled'}); a queued target is removed from the queue ({id, status:'cancelled', was:'queued'} and later queue positions shift forward). Other children and the session keep running — cancellation is targeted. Use it when a background child is going the wrong way (e.g. burning turns) and you must stop it before its report arrives. Cancel is a last resort: verify alarming signals with reliable checks (git/node — not guesses) first; prefer scoped recovery (restore a single affected file) over killing the child — a running child's in-flight work dies with it, partial changes stay unmerged and unaudited. Background advisor reviews are cancelable on this same action (AGENT-LOOP.md §18): pass the review id — its controller aborts, no token is issued for a cancelled review.\n" +
|
|
61
124
|
"- action:'consume-design' (engineering mode, parent side — chain-terminal token consumption): after the delivery is verified and the chain closes out, consume this design's token slot — pass the designId (optional for a single-design session). The slot is consumed; a further spawn for the same designId is mechanically rejected, and any new work (including deviation fixes) requires a fresh design review and token. Idempotent: an unknown designId / already-consumed slot is a no-op notice, never an error. Do NOT call it while the chain is still open — fix rounds reuse the same slot (same designId + designToken).\n" +
|
|
62
125
|
"- action:'panel': DIAGNOSE + fix the subagent panel — the collapsible blocks under the conversation the user sees (CLI TUI panel mirror; headless/VS Code degrade to a 'no panel' pool view). view (default — call it with no params or view:true): returns the live panel blocks [{key, role, status: running|done|awaitingDigest} — running entries also carry elapsedSec; awaitingDigest entries whose report is ALREADY digested carry digested:true (stuck blocks — the freezable ones — explain odd panel states here)] exactly as the user sees them. freeze: pass the block key of a digested-stuck block ({action:'panel', freeze:'role#N'}) to reclaim it into the conversation — the freeze ONLY passes for awaitingDigest blocks with no live pool entry and no pending report (gated); freezing a block whose report is still pending would break the digestion order and is refused with a clear error.\n\n" +
|
|
63
126
|
"Why delegate? A sub-agent runs in its own isolated context — its reads, searches, tool calls and edits never enter your history or pollute your window; only its final report comes back. Delegation keeps your working context lean (you see the whole session, not the child's noise) and the child single-mindedly focused on one task. Parallel children run concurrently, saving wall-clock time. Every coder/eng-coder child carries its own verify + advisor self-review discipline — handed-off work is already verified before you read a word of it.\n\n" +
|
|
@@ -65,9 +128,10 @@ export const subagentTool = {
|
|
|
65
128
|
"- explore — read-only search & analysis. Toolset: the read/search family (grep, read, glob, code_search, doc_search, repo_outline, lsp, tree...). No git context injected—evidence from read/glob/grep and the task book. Its report must list what it searched and what it did NOT find. Fast — specify thoroughness in the task: quick / medium / thorough (default medium).\n" +
|
|
66
129
|
"- plan — read-only implementation planning. Same read/search toolset; NEVER edits files. Returns a step-by-step plan for the parent to execute.\n" +
|
|
67
130
|
"- coder — full implementation. The parent's complete read/write/execute toolset plus verify and advisor for self-review. Its final report must include a delivery transparency table with one row per task requirement (Done / Simplified / Not done — no deferred column).\n" +
|
|
68
|
-
"- eng-coder — engineering-mode coder (available only in engineering mode, replacing coder). Same full toolset as coder plus the design-driven methodology overlay; REQUIRES a valid designToken arg obtained from a passed advisor(type='design') review. The advisor's Approved reply also echoes a designId — pass it as the designId arg: required to pick between designs when several approved reviews are active, optional for a single design. The delivery report echoes the designId back for the audit fix round.\n" +
|
|
69
|
-
"
|
|
70
|
-
"
|
|
131
|
+
"- eng-coder — engineering-mode coder (available only in engineering mode, replacing coder). Same full toolset as coder plus the design-driven methodology overlay; REQUIRES a valid designToken arg obtained from a passed advisor(type='design') review. The advisor's Approved reply also echoes a designId — pass it as the designId arg: required to pick between designs when several approved reviews are active, optional for a single design. The delivery report echoes the designId back for the audit fix round. ALSO REQUIRES a batchDoc arg — the batch record path (docs/batches/<batch>-<topic>.md), the batch §2 task book this spawn implements: a spawn without it, or with a path that does not resolve to a readable file, is mechanically refused.\n" +
|
|
132
|
+
"- eng-designer — engineering-mode design writer (available only in engineering mode): the SOLE author of the requirements + design documents and of the batch record §2 (the batch task book) — revisions included. Writes no implementation code, does not edit prompt files, does not fire reviews, and needs NO designToken (its authorization is the confirmed requirements). It surveys on its own, but may only spawn read-only 'explore' children (sync, ≤6 per batch). ALSO REQUIRES a batchDoc arg — the batch record path (docs/batches/<batch>-<topic>.md); the same mechanical gate as eng-coder: a spawn without it, or with a path that does not resolve to a readable file, is mechanically refused.\n" +
|
|
133
|
+
"Mode filtering: normal mode exposes explore/plan/coder; engineering mode exposes explore/plan/eng-designer/eng-coder. The schema enum reflects the active mode.\n\n" +
|
|
134
|
+
"Async spawn (AGENT-LOOP.md §15/§18/§11.1): pass async:true to spawn WITHOUT waiting — returns {id, role, status:\"running\"} immediately so you can keep working in your own turn (read/check files, run other tools) while the child runs in the background. The child's report is delivered to you automatically — there is no fetch action; use action:'status' only to see progress, never to wait for the result. Top-level spawns are ALWAYS async — never pass `async:false` at depth-0 (the report arrives automatically; if your next step needs it, end the turn and let the digest deliver it). Inside subagents (depth>0) spawns are always synchronous (platform rule). Eng-coder's delivery protocol runs fully inside the child (implementation → audit → self-fix → advisor re-review → converged delivery). Async spawns are pooled per role domain (AGENT-LOOP.md §11.1): at most 4 concurrent eng-coders and 4 concurrent other-role spawns by default (agent.poolLimits overrides both) — a full domain queues further spawns with a position while the other domain keeps starting (domains never block each other), and top-level only. After an async spawn the turn winds down normally — nothing expects you to wait for it: the child runs in the background and its report is delivered to you automatically — before your next turn, or digested in the suspension session — so end the turn; do not poll or wait for the result.\n\n" +
|
|
71
135
|
"Task scheduling (AGENT-LOOP.md §20): declare the scheduling metadata to let the SCHEDULER order your spawns — files: the file paths this task will modify, dependsOn: ids from prior async spawn returns whose outcome this task needs. Overlapping-file tasks are serialized and dependent tasks are started in order automatically: a spawn that would conflict, or whose dependencies have not settled, queues instead of running ({id, status:\"queued\", position, reason} — the waiting task auto-starts when the conflict clears / its dependency settles; cancel a queued task to drop it). A spawn whose dependency was cancelled or failed stays queued and marked \"dependency cancelled\" until you decide (cancel it) — in an AUTO session it starts by itself. Referencing an unknown id errors; an id already consumed (auto-delivered by the auto channel) counts as satisfied. Omit both parameters for the plain immediate spawn (no scheduler involvement).\n\n" +
|
|
72
136
|
"Writing the prompt:\n" +
|
|
73
137
|
"- The sub-agent starts with zero context — it has not seen this conversation. Brief it like a colleague who just walked into the room: state the goal, list what you already know, hand over the specifics.\n" +
|
|
@@ -82,11 +146,12 @@ export const subagentTool = {
|
|
|
82
146
|
freeze: { type: "string", description: "action:'panel' only: block key of a digested-stuck awaitingDigest block (e.g. \"eng-coder#9\") to reclaim into the conversation via the gated done-freeze event. Refused when the block is running/done/unknown or its report is still pending digestion (would break the digestion order). Requires the CLI TUI panel mirror — headless/VS Code report the freeze unavailable." },
|
|
83
147
|
task: { type: "string", description: "Required for action:'spawn' (the self-contained task brief) and action:'escalate' (goal, constraints, entry files, acceptance criteria). Not used by status." },
|
|
84
148
|
context: { type: "string", description: "Optional background the sub-agent needs (it cannot see this conversation); action:'spawn' only." },
|
|
85
|
-
role: { type: "string", enum: ["explore", "plan", "coder", "eng-coder"], description: "The sub-agent role — see the tool description for the role capability matrix. Exact spelling required. action:'spawn' only (escalate spawns its own expert internally)." },
|
|
149
|
+
role: { type: "string", enum: ["explore", "plan", "coder", "eng-coder", "eng-designer"], description: "The sub-agent role — see the tool description for the role capability matrix. Exact spelling required. action:'spawn' only (escalate spawns its own expert internally)." },
|
|
86
150
|
model: { type: "string", description: "action:'spawn': provider/model override for this sub-agent ('provider:model', a provider name, or a model name on the parent's provider — defaults to config.agent.subagentModels[role], then config.agent.subagentModel, then the parent's provider). pass \"default\" to explicitly inherit the default model — equivalent to omitting the parameter. action:'escalate': pick a consult candidate as 'provider:model' (default = the first consult model)." },
|
|
87
151
|
designToken: { type: "string", description: "Required when role='eng-coder': the token returned by advisor(type='design') after the design review passed. Without a valid token, eng-coder cannot modify files." },
|
|
88
152
|
designId: { type: "string", description: "Optional when role='eng-coder': the designId echoed with the approved token by advisor(type='design'). Required to pick between designs when several approved reviews are active in the session — each eng-coder carries its own designId+token pair so parallel implementations never overwrite each other. Optional for a single design. action:'consume-design': the design whose slot to close out — optional for a single-design session; required to pick when several approved designs are active (the consume gate refuses to guess)." },
|
|
89
|
-
|
|
153
|
+
batchDoc: { type: "string", description: "REQUIRED for role='eng-coder' and role='eng-designer': the batch record path (docs/batches/<batch>-<topic>.md) — the batch §2 task book this spawn implements (or writes). The spawn is mechanically refused without it, and also when the path does not resolve (cwd-relative or absolute) to a readable file; the CONTENT is never validated (the batch record owns that). explore/plan/coder spawns ignore it." },
|
|
154
|
+
async: { type: "boolean", description: "action:'spawn': true = spawn without waiting — returns {id, status:\"running\"} immediately; the report is delivered to you automatically (there is no fetch action). Default: depth-0 → true (async — every role, AGENT-LOOP.md §18 D-E1a); depth>0 → sync (forced). action:'escalate': same semantics (AGENT-LOOP.md §25 D-R17b) — default async at depth 0; async:false keeps the legacy synchronous flight (mechanism parameter — see the Async spawn section for top-level guidance)." },
|
|
90
155
|
files: { type: "array", items: { type: "string" }, description: "action:'spawn' only: the file write-domain this task declares (cwd-relative or absolute paths). files must be file-level paths (one per file you will modify). Directory declarations are NOT supported — they bypass the conflict detector and are rejected with an error. Tasks with overlapping files are serialized automatically — a conflicting spawn queues ({id, status:\"queued\", position, reason}) instead of running concurrently and starts when the conflict clears. Omit to skip conflict detection (plain immediate spawn)." },
|
|
91
156
|
dependsOn: { type: "array", items: { type: "string" }, description: "action:'spawn' only: ids from prior async spawn returns whose outcome this task needs — the task queues ({id, status:\"queued\", position, reason}) until every dependency settles, then starts automatically. Ids already consumed (auto-delivered to the model) count as satisfied; a dependency cancelled or failed leaves the task queued marked 'dependency cancelled' until you decide (cancel it — AUTO sessions auto-start). Unknown ids error." },
|
|
92
157
|
id: { type: "string", description: "action:'status'/'observe'/'send'/'cancel': the subagent id from the async spawn return. status: omit = overview of the whole pool; observe/send/cancel: REQUIRED (observe needs the child to inspect; send needs the target; never omit on cancel — a blanket cancel is unsupported)." },
|
|
@@ -105,12 +170,12 @@ export const subagentTool = {
|
|
|
105
170
|
? String(args.action)
|
|
106
171
|
: "spawn"
|
|
107
172
|
if (action !== "spawn") {
|
|
108
|
-
// §19 restricted-variant action gate (round2 #3): the
|
|
109
|
-
//
|
|
110
|
-
// coder+WRITE child (violates explore-only intent) and status/panel/
|
|
111
|
-
//
|
|
112
|
-
if ((ctx.depth ?? 0) > 0 && ctx.agent?._role === "eng-coder") {
|
|
113
|
-
throw new Error(`only action:'spawn' (sync explore
|
|
173
|
+
// §19 restricted-variant action gate (round2 #3): the engineering-child channel
|
|
174
|
+
// (depth>0, role eng-coder or eng-designer) is spawn-only — escalate spawns a
|
|
175
|
+
// coder+WRITE child (violates explore-only intent) and status/panel/observe/send
|
|
176
|
+
// have no async pool / panel mirror to query in a child context.
|
|
177
|
+
if ((ctx.depth ?? 0) > 0 && (ctx.agent?._role === "eng-coder" || ctx.agent?._role === "eng-designer")) {
|
|
178
|
+
throw new Error(`only action:'spawn' (sync explore children) is available inside an ${ctx.agent._role} — escalate/status/cancel/panel/consume-design/observe/send are not (AGENT-LOOP.md §19 D-M3)`)
|
|
114
179
|
}
|
|
115
180
|
// §17 N3/D-S6 spawn gate (manual tier): auto-turn digests may not spawn —
|
|
116
181
|
// async OR blocking — the digest must stay organize-only. The escalate
|
|
@@ -130,7 +195,10 @@ export const subagentTool = {
|
|
|
130
195
|
if (action === "consume-design") return executeConsumeDesignAction(args, ctx)
|
|
131
196
|
// §19.6 panel 动作:view(readonly 面——digest 内放行——自省类)与 freeze
|
|
132
197
|
// (控制类——同 cancel——digest 内放行)。深度/门控检查在 executePanelAction 内。
|
|
133
|
-
|
|
198
|
+
// CLI-ACTIVITY-DEBLOAT F-3(2026-09-10)接线:executePanelAction 经 ctx.state
|
|
199
|
+
// (= agent._tuiState——startTUI 反向挂载)读时现算面板块(computePanelBlocks)——
|
|
200
|
+
// 手工面板镜像已退役。headless/VSC 无挂载 → 现算返 null → 降级照旧。
|
|
201
|
+
if (action === "panel") return executePanelAction(args, { ...ctx, state: ctx.agent?._tuiState })
|
|
134
202
|
// SUBAGENT-OBSERVE-SEND:observe = readonly 查询(同 status——digest/planMode 放行);
|
|
135
203
|
// send = 控制类豁免(同 cancel——父回合内显式调用即授权)。深度门在各自执行器内。
|
|
136
204
|
if (action === "observe") return executeObserveAction(args, ctx)
|
|
@@ -157,9 +225,9 @@ export const subagentTool = {
|
|
|
157
225
|
// variant roles ("Coder", " coder") bypass BOTH mode gates and fall through to
|
|
158
226
|
// full tools / no overlay — a full-write coder without design review. Schema enums are
|
|
159
227
|
// advisory; providers don't enforce them. Fail closed on unknown roles.
|
|
160
|
-
const ROLES = new Set(["explore", "plan", "coder", "eng-coder"])
|
|
228
|
+
const ROLES = new Set(["explore", "plan", "coder", "eng-coder", "eng-designer"])
|
|
161
229
|
if (!ROLES.has(role)) {
|
|
162
|
-
throw new Error(`Unknown subagent role: ${JSON.stringify(role)}. Valid roles: explore, plan, coder, eng-coder (exact spelling).`)
|
|
230
|
+
throw new Error(`Unknown subagent role: ${JSON.stringify(role)}. Valid roles: explore, plan, coder, eng-coder, eng-designer (exact spelling).`)
|
|
163
231
|
}
|
|
164
232
|
// §18 D-E3 internal-spawn gate: an eng-coder sub-agent may only spawn sync
|
|
165
233
|
// explore (audit) children — non-explore roles and async are refused here
|
|
@@ -168,13 +236,19 @@ export const subagentTool = {
|
|
|
168
236
|
// augmentation below. Runs BEFORE the mode gates so the eng-coder-specific
|
|
169
237
|
// error (not the generic engineering-mode one) surfaces.
|
|
170
238
|
const engAuditAttempt = gateEngCoderSpawn(ctx.agent, ctx.depth, role, args.async)
|
|
171
|
-
// Role is mutually exclusive per mode: normal mode → "coder", engineering mode → "eng-coder"
|
|
239
|
+
// Role is mutually exclusive per mode: normal mode → "coder", engineering mode → "eng-coder"/"eng-designer"
|
|
172
240
|
if (parent.config?.agent?.engineering && role === "coder") {
|
|
173
|
-
throw new Error("Engineering mode: use role='eng-coder' for implementation tasks.")
|
|
241
|
+
throw new Error("Engineering mode: role='coder' is disabled — use role='eng-coder' for implementation tasks (or role='eng-designer' for design writing).")
|
|
174
242
|
}
|
|
175
243
|
if (!parent.config?.agent?.engineering && role === "eng-coder") {
|
|
176
244
|
throw new Error("Engineering mode is not active — use role='coder' for implementation tasks.")
|
|
177
245
|
}
|
|
246
|
+
// Third mode gate (ENGINEERING-MODE.md §2.15 A—— symmetric completion):
|
|
247
|
+
// eng-designer is engineering-mode-only, same family as eng-coder (both carry
|
|
248
|
+
// the engineering discipline overlay + the batchDoc gate).
|
|
249
|
+
if (!parent.config?.agent?.engineering && role === "eng-designer") {
|
|
250
|
+
throw new Error("Engineering mode is not active — role='eng-designer' is engineering-mode only (it writes the requirements/design documents inside the engineering workflow); use role='explore' or role='plan' for read-only work.")
|
|
251
|
+
}
|
|
178
252
|
|
|
179
253
|
// §17 N3/D-S6 spawn gate (manual tier): auto-turn digests may not spawn — async
|
|
180
254
|
// OR blocking — the digest must stay organize-only. AUTO tier (autoApprove) is
|
|
@@ -205,9 +279,25 @@ export const subagentTool = {
|
|
|
205
279
|
// turns them into Error tool results — unchanged behavior).
|
|
206
280
|
const askSubagentContinue = (e) => {
|
|
207
281
|
if (!ctx.onPermissionRequest) return Promise.resolve(false)
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
282
|
+
const key = relayPrefix.slice(0, -1)
|
|
283
|
+
const ask = async () => {
|
|
284
|
+
// SYNC-CANCEL v2(模态 deny——用户裁):⏹ 后 entry.stopped——不再弹模态。
|
|
285
|
+
// ⚠ 不能直接 resolve(false) 走 onDeclined 降级(TURN_CAP partial——child 已撞
|
|
286
|
+
// cap——runWithContinue 的 decline 是正常 return——永远到不了 abort 检出点——
|
|
287
|
+
// stopped 折叠语义丢失:无 ⟦ev⟧stopped/无 STOPPED_MARK——块冻结标 done 而非
|
|
288
|
+
// stopped——评审 🟡#2)。stopped 分支改抛 AbortError——runWithContinue 只捕
|
|
289
|
+
// ContinueError——原样上抛 → 阻塞 catch 三分支②折叠("child 随即在 abort 检出点
|
|
290
|
+
// 解绕折叠"——AGENT-LOOP §7.2 机制文)。abort 恒已在途(stopped 只由
|
|
291
|
+
// cancelSyncChild 与 ctrl.abort 同时置位)——信号语义真实。
|
|
292
|
+
if (parent._syncChildAborts?.get(key)?.stopped) throw abortError(ctrl.signal, "settle", "sync-stopped")
|
|
293
|
+
const go = await ctx.onPermissionRequest("continue", { turns: e.turn, agent: key })
|
|
294
|
+
// ⏹ deny(denyModalForOwner resolve(false))与用户按 n 同形——旗标区分:
|
|
295
|
+
// stopped → 同上抛(折叠——abort 先于 deny 已在途);普通 n → false 走 decline
|
|
296
|
+
// (现状——cap partial 报告)。
|
|
297
|
+
if (parent._syncChildAborts?.get(key)?.stopped) throw abortError(ctrl.signal, "settle", "sync-stopped")
|
|
298
|
+
return go
|
|
299
|
+
}
|
|
300
|
+
return enqueueAsk(parent, "_permQueue", ask)
|
|
211
301
|
}
|
|
212
302
|
|
|
213
303
|
// ── Async branch(2026-09-05 module-split——executeAsyncSpawn verbatim 迁
|
|
@@ -216,43 +306,79 @@ export const subagentTool = {
|
|
|
216
306
|
return executeAsyncSpawn(parent, ctx, role, args, child, input, childOpts, childRunOpts, relayPrefix, built.childProvider, prep.files, prep.dependsOn)
|
|
217
307
|
}
|
|
218
308
|
|
|
219
|
-
// ── Blocking path (unchanged semantics
|
|
220
|
-
//
|
|
221
|
-
//
|
|
309
|
+
// ── Blocking path (unchanged semantics + SYNC-CANCEL targeted stop) ──
|
|
310
|
+
// SYNC-CANCEL F1/F5(2026-09-09):自属 AbortController(armSyncChildAbort——
|
|
311
|
+
// childRunOpts.signal 覆写 ctrl.signal——照抄 async 分支 subagent-run.mjs 的
|
|
312
|
+
// 覆写模式——buildChildRunOpts 不改——escalate/consult 零触碰)——⏹ 定向中止
|
|
313
|
+
// (cancelSyncChild → ctrl.abort)与整回合停(base abort 逐链传播)解耦;registry
|
|
314
|
+
// 注册/注销(try/finally 三路径——R7 防跨回合残留)。LOGGING(LOGGING.md):
|
|
315
|
+
// child:*(阻塞 spawn——runChildPipeline 前后;declined partial 由 TURN_CAP_MARK
|
|
316
|
+
// 检出;⏹ 折叠由 STOPPED_MARK 检出——kind partial;错误原样上抛(dispatch 转
|
|
317
|
+
// tool:error))
|
|
222
318
|
const blockT0 = Date.now()
|
|
223
319
|
logEvent("child:spawn", { role, id: child._logId, kind: "blocking" })
|
|
320
|
+
const syncKey = relayPrefix.slice(0, -1)
|
|
321
|
+
// baseSignal 一次性快照(spawn 时刻)——catch 分类复用同一信号对象(会话收尾把
|
|
322
|
+
// _sessionSignal 置 null 的窗口内重读会漂移——快照防误判)
|
|
323
|
+
const baseSignal = buildChildSignal(parent, ctx)
|
|
324
|
+
const { ctrl, disarm } = armSyncChildAbort(parent, syncKey, baseSignal)
|
|
325
|
+
let pipelineReport
|
|
224
326
|
try {
|
|
225
|
-
|
|
327
|
+
pipelineReport = await runChildPipeline(child, input, childOpts, { ...childRunOpts, signal: ctrl.signal }, {
|
|
226
328
|
parent, role, args,
|
|
227
329
|
askContinue: askSubagentContinue,
|
|
228
330
|
})
|
|
229
|
-
// §27 R23 D-R23c1(评审 #1 🅰——生成侧补发射):sync spawn 同步收尾——若本 spawn
|
|
230
|
-
// 处于嵌套上下文(ctx.callbacks 已是嵌套 wrapper——eng-coder 内 explore 审计)→
|
|
231
|
-
// 发内层 ⟦ev⟧done(完整嵌套前缀——wrapper 链自动补外层)→ 主 TUI 路由子块定格
|
|
232
|
-
// (T-R23c.1)。非嵌套(depth-0)零变化——冻结仍由 dispatch subKey 精确冻承接。
|
|
233
|
-
emitNestedChildEvent(ctx, relayPrefix, "done")
|
|
234
|
-
logEvent("child:done", { role, id: child._logId, ms: Date.now() - blockT0, kind: String(pipelineReport).includes(TURN_CAP_MARK) ? "partial" : "ok" })
|
|
235
|
-
// §7.2.3 sync spawn 完成精确冻结(方案 e):execute 返回前 ctx 留子代理 key
|
|
236
|
-
// (relayPrefix 去尾 = `role#N`)——dispatch runOne 读它作 onToolResult 第 4 参 →
|
|
237
|
-
// TUI finishSubTaskKey 按 key 精确冻(async eng-coder 先启动时不再误冻其块——
|
|
238
|
-
// T-F2)。仅成功路径设置:async 分支不设(round2 #2——ack 带 status:running 由
|
|
239
|
-
// isAsyncSpawnResult 跳过冻结);错误/拒绝路径到此之前已 throw/return——ctx 未设
|
|
240
|
-
// ——错误路径不触发冻结(round1 #1——T-F5)。
|
|
241
|
-
ctx._subagentKey = relayPrefix.slice(0, -1)
|
|
242
|
-
return pipelineReport
|
|
243
331
|
} catch (e) {
|
|
244
|
-
|
|
332
|
+
// SYNC-CANCEL F2 三分支(classifySyncAbort 纯函数):
|
|
333
|
+
const cls = classifySyncAbort(ctx.signal, baseSignal, ctrl.signal, e)
|
|
334
|
+
if (cls === "base") {
|
|
335
|
+
// ① 整回合停(现状逐字保留——挂起场景 base 命中而 ctx.signal 未 abort——R2):
|
|
245
336
|
// §27 R23:外层 abort 传播的中断——内层开块随之外层冻结前先收尾定格
|
|
246
337
|
// (D-R23c1 stopped——T-R23c.2a 生成侧路径;TUI 冻结兜底仍在 freezeSubTaskLines)
|
|
247
338
|
emitNestedChildEvent(ctx, relayPrefix, "stopped")
|
|
248
339
|
throw e // 用户停——不落错误事件
|
|
249
340
|
}
|
|
250
|
-
|
|
251
|
-
|
|
341
|
+
if (cls === "error") {
|
|
342
|
+
// ③ 其他错误(现状逐字保留——:249-253):
|
|
343
|
+
// §27 R23 error-run 映射(实现批补一行):run 错误(非 abort)→ 同样发 stopped
|
|
344
|
+
// ——内层子块定格不悬空(T-R23a.3——工具错/运行错误路径)。
|
|
345
|
+
emitNestedChildEvent(ctx, relayPrefix, "stopped")
|
|
346
|
+
logEvent("child:error", { role, id: child._logId, ms: Date.now() - blockT0, err: errText(deathLine(e, ctrl?.signal), 200) })
|
|
347
|
+
throw e
|
|
348
|
+
}
|
|
349
|
+
// ② targeted 折叠(err AbortError && 自属 ctrl aborted && 非整回合停):merge +
|
|
350
|
+
// stopped partial 报告(父回合继续拿报告——AC1/AC3)。merge 镜像 escalate sync
|
|
351
|
+
// runner 先例(subagent-actions.mjs runner 包装层——guard 在 mergeChildMutations
|
|
352
|
+
// 内——见子代理已写文件才传播)。
|
|
353
|
+
if (role === "eng-coder" && child._mutatedThisRun) mergeChildMutations(parent, child)
|
|
354
|
+
pipelineReport = buildSyncStoppedReport(role, child._capturedOutput ?? "", args?.designId)
|
|
355
|
+
// 块冻结标 stopped(R6——非 done):⏹ 定向中止的 TUI 顶层块立即定格 stopped
|
|
356
|
+
// (async settle cancelled 分支同款直发——async-settle.mjs settleAsyncEntry);
|
|
357
|
+
// 嵌套(eng-coder 内 explore 审计)经 emitNestedChildEvent 定格子块——stopped
|
|
358
|
+
// 幂等无害(重复/迟到 done 由 §27.1 F2 done 子块定格丢弃兜底)。
|
|
359
|
+
ctx.callbacks?.onToken?.(`${relayPrefix}⟦ev⟧stopped\x1e0\x1e0\x1estopped\x1e`)
|
|
252
360
|
emitNestedChildEvent(ctx, relayPrefix, "stopped")
|
|
253
|
-
|
|
254
|
-
|
|
361
|
+
} finally {
|
|
362
|
+
// R7 防跨回合残留:成功/折叠②/整回合停①/错误③ 四出口统一注销(设计"三路径"
|
|
363
|
+
// 口径 = 成功/折叠/整回合停——错误③ 同样 rethrow 经 finally——同归本注销)
|
|
364
|
+
disarm()
|
|
255
365
|
}
|
|
366
|
+
// §27 R23 D-R23c1(评审 #1 🅰——生成侧补发射):sync spawn 同步收尾——若本 spawn
|
|
367
|
+
// 处于嵌套上下文(ctx.callbacks 已是嵌套 wrapper——eng-coder 内 explore 审计)→
|
|
368
|
+
// 发内层 ⟦ev⟧done(完整嵌套前缀——wrapper 链自动补外层)→ 主 TUI 路由子块定格
|
|
369
|
+
// (T-R23c.1)。非嵌套(depth-0)零变化——冻结仍由 dispatch subKey 精确冻承接。
|
|
370
|
+
// ② 折叠 = 正常 return(本共用出口:done 补发照设 + ctx._subagentKey 照设——成功
|
|
371
|
+
// 冻结管线复用——迟到 done 对已定格 stopped 块被丢弃——幂等无害)。
|
|
372
|
+
emitNestedChildEvent(ctx, relayPrefix, "done")
|
|
373
|
+
logEvent("child:done", { role, id: child._logId, ms: Date.now() - blockT0, kind: String(pipelineReport).includes(TURN_CAP_MARK) || String(pipelineReport).includes(STOPPED_MARK) ? "partial" : "ok" })
|
|
374
|
+
// §7.2.3 sync spawn 完成精确冻结(方案 e):execute 返回前 ctx 留子代理 key
|
|
375
|
+
// (relayPrefix 去尾 = `role#N`)——dispatch runOne 读它作 onToolResult 第 4 参 →
|
|
376
|
+
// TUI finishSubTaskKey 按 key 精确冻(async eng-coder 先启动时不再误冻其块——
|
|
377
|
+
// T-F2)。仅成功/折叠路径设置:async 分支不设(round2 #2——ack 带 status:running 由
|
|
378
|
+
// isAsyncSpawnResult 跳过冻结);base/error 路径到此之前已 throw——ctx 未设
|
|
379
|
+
// ——中止/错误路径不触发冻结(round1 #1——T-F5)。
|
|
380
|
+
ctx._subagentKey = syncKey
|
|
381
|
+
return pipelineReport
|
|
256
382
|
},
|
|
257
383
|
}
|
|
258
384
|
|
|
@@ -263,7 +389,7 @@ export const subagentTool = {
|
|
|
263
389
|
// (maybeRefillAsync——execute 不再直接使用池常量)。
|
|
264
390
|
// 2026-09-05 拆分轮: maybeRefillAsync 随 §20 调度器独立(./subagent-scheduler.mjs)——
|
|
265
391
|
// 再导出源改写,消费面(agent.mjs 动态 import 等)不变。
|
|
266
|
-
// 2026-09-06 §
|
|
392
|
+
// 2026-09-06 §11.1 拆分轮: ASYNC_SUBAGENT_LIMIT 导出 → ASYNC_POOL_LIMITS(分域常量——
|
|
267
393
|
// 定义在 subagent-async.mjs——re-export 面同步)。
|
|
268
394
|
export {
|
|
269
395
|
ASYNC_POOL_LIMITS,
|
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
* path (doc-only changes return early) — the task list and self-review checklist.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { isCodePath, isDocPath, loadConventions } from "../conventions.mjs"
|
|
15
15
|
import { execSync, spawnSync } from "node:child_process"
|
|
16
16
|
import { existsSync } from "node:fs"
|
|
17
|
-
import {
|
|
17
|
+
import { resolve } from "node:path"
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* §18.12 D-VR1 path normalization — mirrors the §20.5 file-domain handling:
|
|
@@ -31,32 +31,6 @@ function changedFileKey(p) {
|
|
|
31
31
|
return process.platform === "win32" ? p.toLowerCase() : p
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
/**
|
|
35
|
-
* Nearest ancestor of an absolute path holding package.json or .git — the
|
|
36
|
-
* project/repo root. Walk stops at the filesystem root; returns null when no anchor exists.
|
|
37
|
-
*/
|
|
38
|
-
function findProjectRoot(absPath) {
|
|
39
|
-
let dir = dirname(resolve(absPath))
|
|
40
|
-
for (;;) {
|
|
41
|
-
if (existsSync(join(dir, "package.json")) || existsSync(join(dir, ".git"))) return dir
|
|
42
|
-
const parent = dirname(dir)
|
|
43
|
-
if (parent === dir) return null
|
|
44
|
-
dir = parent
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* True when the path is under <projectRoot>/src/ (anchored via findProjectRoot —
|
|
50
|
-
* a parent-path /src/ segment outside the project must not classify a file as
|
|
51
|
-
* product code). Falls back to the loose /src/ pattern when no anchor exists.
|
|
52
|
-
*/
|
|
53
|
-
function isUnderSrc(absPath) {
|
|
54
|
-
const root = findProjectRoot(absPath)
|
|
55
|
-
if (!root) return /(?:^|[\\/])src[\\/]/.test(absPath)
|
|
56
|
-
const norm = normalizeChangedPath(absPath, root)
|
|
57
|
-
return norm.startsWith(normalizeChangedPath("src", root) + "/")
|
|
58
|
-
}
|
|
59
|
-
|
|
60
34
|
/**
|
|
61
35
|
* D-V3 block guidance — a block message must not just say "not verified": it lists
|
|
62
36
|
* the changed code files and points to the project's AGENTS.md natural-language
|
|
@@ -180,19 +154,24 @@ export const verifyTool = {
|
|
|
180
154
|
// 1b. Doc-only fast path: every changed file is documentation (docs/, *.md,
|
|
181
155
|
// LICENSE…) — syntax checks and a verification declaration are meaningless
|
|
182
156
|
// for doc changes, and the task list/self-review checklist add nothing either.
|
|
183
|
-
//
|
|
184
|
-
//
|
|
157
|
+
// Paths inside a declared code segment (default: src — incl. prompts/*.md) are
|
|
158
|
+
// product code — excluded from the fast path, consistent with the design gate.
|
|
159
|
+
// The project's own layout is declarable (.thincoder/conventions.json).
|
|
160
|
+
// Empty list (no changes / git unavailable)
|
|
185
161
|
// intentionally falls through to the normal path below.
|
|
186
|
-
|
|
162
|
+
const conv = loadConventions(cwd)
|
|
163
|
+
if (changedFiles.length > 0 && changedFiles.every((f) => isDocPath(f, conv))) {
|
|
187
164
|
lines.push("")
|
|
188
165
|
lines.push("Documentation-only changes — skipping syntax checks and tests.")
|
|
189
166
|
ctx.agent._verifyPassed = true
|
|
190
167
|
return lines.join("\n")
|
|
191
168
|
}
|
|
192
169
|
|
|
193
|
-
// Code files needing verification: anything
|
|
194
|
-
//
|
|
195
|
-
|
|
170
|
+
// Code files needing verification: anything the shared classifier calls product
|
|
171
|
+
// code (D-V1 — these require a verification declaration). Temp scratch files are
|
|
172
|
+
// neither code nor docs and fall out of this list (a throwaway diagnostic script
|
|
173
|
+
// is not a code change — no declaration is demanded for it).
|
|
174
|
+
const codeFiles = changedFiles.filter((f) => isCodePath(f, conv))
|
|
196
175
|
|
|
197
176
|
// 2. Advisory syntax hint (D-V5) — node --check on changed .js/.mjs when node
|
|
198
177
|
// exists. SOFT hint only — it does NOT gate done (no language-specific
|
package/src/agent-tools.mjs
CHANGED
|
@@ -14,3 +14,4 @@ export { timerTool } from "./agent-tools/timer.mjs"
|
|
|
14
14
|
export { advisorTool } from "./agent-tools/advisor.mjs"
|
|
15
15
|
export { engTool } from "./agent-tools/eng.mjs"
|
|
16
16
|
export { readHistoryTool } from "./agent-tools/read-history.mjs"
|
|
17
|
+
export { batchSegmentTool } from "./agent-tools/batch-segment.mjs"
|
package/src/agent.mjs
CHANGED
|
@@ -3,39 +3,43 @@
|
|
|
3
3
|
* LLM ↔ tool-call loop, until the task is done.
|
|
4
4
|
*/
|
|
5
5
|
import { chat } from "./provider/index.mjs"
|
|
6
|
+
import { abortError, annotateAbort } from "./abort-provenance.mjs"
|
|
6
7
|
import { pushReal, summarizeRunExplorations } from "./context.mjs"
|
|
7
8
|
import { specForModel } from "./config.mjs"
|
|
8
|
-
import {
|
|
9
|
-
import { join, dirname, resolve } from "node:path"
|
|
10
|
-
import { fileURLToPath } from "node:url"
|
|
9
|
+
import { resolve } from "node:path"
|
|
11
10
|
import { executeToolCalls } from "./agent/dispatch.mjs"
|
|
12
11
|
import { recordToolResults } from "./agent/record-results.mjs"
|
|
13
12
|
import { FILE_MUTATORS } from "./agent/helpers.mjs"
|
|
14
13
|
import { prepareRun } from "./agent/setup.mjs"
|
|
15
14
|
import { injectPostTurn } from "./agent/post-turn.mjs"
|
|
16
15
|
import { handleCompletion } from "./agent/completion.mjs"
|
|
16
|
+
// PROMPT-SYSTEM 施工② G1(2026-09-10):六件槽位常量装载收口 prompt-overlays.mjs
|
|
17
|
+
// (与子代理角色常量同源——单一权威锚 D1);旧三件文件读取随本批退役。
|
|
18
|
+
// 槽位常量通过下方 re-export 面(mod: rel — re-export 保 import 面);本文件自身零直接消费。
|
|
17
19
|
// 主循环阶段函数(压缩检查/注入组/回合收尾)2026-09-05 实践轮迁 agent/run-stages.mjs
|
|
18
20
|
import { runCompactionCheck, injectTurnReminders, finalizeAgentTurn, injectResponseReminders } from "./agent/run-stages.mjs"
|
|
19
21
|
import {
|
|
20
22
|
escapeXml, repairHistory, listWorkDir,
|
|
21
23
|
readonlyToolNames, collectGitContext, loadProjectInstructions,
|
|
22
24
|
ContinueError,
|
|
25
|
+
turnFrame, // 第 19 批(TURN-ACROSS-SEGMENTS):跨段累计编号帧(设计 §19.3)
|
|
23
26
|
DEFAULT_MAX_TURNS, DEFAULT_SUBAGENT_TURNS,
|
|
24
27
|
MIN_REPORT_CHARS, REPORT_CONTINUATION,
|
|
25
28
|
AUTO_TURN_DIGEST_DOMAIN,
|
|
26
29
|
} from "./agent/helpers.mjs"
|
|
27
30
|
// ENG 提醒族 + auto-turn domain 2026-09-05 迁 agent/helpers.mjs(agent.mjs 530 > 500 硬限)
|
|
28
|
-
//
|
|
31
|
+
// PROMPT-SYSTEM 施工② G1(2026-09-10):六件槽位常量装载收口 prompt-overlays.mjs
|
|
32
|
+
// (与子代理角色常量同源——单一权威锚 D1);本文件 re-export 保 import 面。
|
|
29
33
|
export {
|
|
30
|
-
|
|
34
|
+
PERSONA_ENGINEERING, PERSONA_NORMAL, COMMON,
|
|
35
|
+
DISCIPLINE_ENGINEERING, DISCIPLINE_NORMAL,
|
|
36
|
+
CONSULT_BASE,
|
|
31
37
|
} from "./prompt-overlays.mjs"
|
|
32
38
|
export { ENG_ON_REMINDER, ENG_OFF_REMINDER } from "./agent/helpers.mjs"
|
|
33
39
|
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const DISCIPLINE_RULES = readFileSync(join(__dirname, "prompts", "discipline.md"), "utf8")
|
|
38
|
-
const MAIN_OVERLAY = readFileSync(join(__dirname, "prompts", "main.md"), "utf8")
|
|
40
|
+
// Role → persona slot mapping note(施工② G3): 子代理人格由 assemblePrompt 场景表按
|
|
41
|
+
// role 承载(persona-explore/coder/plan/eng-coder.md——D1 表 = 蓝图 §3.2 1:1);
|
|
42
|
+
// spawn 侧 overlay 概念已退役(prompt-overlays.mjs 不再导出 OVERLAY 别名常量)。
|
|
39
43
|
|
|
40
44
|
// exported for consumption by agent-tools.mjs
|
|
41
45
|
export {
|
|
@@ -66,8 +70,8 @@ export function createAgent({
|
|
|
66
70
|
// (AC3 零写) — no field initializer; the multi-slot Map `_engDesignTokens` is the
|
|
67
71
|
// authoritative ledger (hydrated by restoreEngTokens / written by settle).
|
|
68
72
|
_touchedFiles: [], _verifyRetries: 0, _advisorRound: 0, _advisorSession: null,
|
|
69
|
-
_advisorRuns: new Map(), // §
|
|
70
|
-
_mutationSeq: 0, _mutLog: [], // §
|
|
73
|
+
_advisorRuns: new Map(), // §11.2 D-24b: per-review convergence instances (rounds/prior/designId)
|
|
74
|
+
_mutationSeq: 0, _mutLog: [], // §11.2 D-24b: mutation log (in-flight review staleness scan)
|
|
71
75
|
_lastAdvisorOutput: null, // full review output from the most recent advisor call (convergence rounds inject it verbatim)
|
|
72
76
|
_lastEngState: false,
|
|
73
77
|
_pendingReminders: [],
|
|
@@ -109,8 +113,10 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
109
113
|
}
|
|
110
114
|
agent._inAutoTurn = autoTurn // spawn gate for manual-tier digests (§17 D-S6/N3)
|
|
111
115
|
const { maxTurns, threshold, tools, toolSchemas, toolByName, systemPrompt } = await prepareRun(
|
|
116
|
+
// G1/G2(施工②):prompt 装配收口 prepareRun 内部(assemblePrompt——prompt-overlays.mjs
|
|
117
|
+
// 槽位常量,与子代理角色常量同源——单一权威锚 D1);本调用不再携带 prompt 常量。
|
|
112
118
|
agent, input, callbacks,
|
|
113
|
-
{ depth, signal, overrideTurns, resume: resume || autoTurn
|
|
119
|
+
{ depth, signal, overrideTurns, resume: resume || autoTurn },
|
|
114
120
|
)
|
|
115
121
|
|
|
116
122
|
// Exploration-distillation boundary (CONTEXT-COMPACTION §5): prepareRun already
|
|
@@ -120,6 +126,10 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
120
126
|
// Per-run bookkeeping reset — PRESERVED on `resume` (ContinueError continuation):
|
|
121
127
|
// mutation/guard continuity and the convergence budget must survive a continuation.
|
|
122
128
|
if (!resume) {
|
|
129
|
+
// 第 19 批(TURN-ACROSS-SEGMENTS——设计 TURN-CAP-CONTINUE.md §19.3):链内累计编号
|
|
130
|
+
// 只在链起点复位——续跑(resume:true)不重置、不回退(编号帧公式见 helpers.mjs
|
|
131
|
+
// turnFrame)。与下方 mutation/guard 复位同条件同点(全档唯一复位点)。
|
|
132
|
+
agent._turnSeq = 0
|
|
123
133
|
// §17 D-S6: an auto-turn's guard marks are inherited by the next USER run (not
|
|
124
134
|
// reset) so auto-turn changes never escape the guard silently.
|
|
125
135
|
const g = agent._inheritedGuard
|
|
@@ -172,14 +182,20 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
172
182
|
let thrownError = null
|
|
173
183
|
try {
|
|
174
184
|
for (let turn = 0; turn < maxTurns; turn++) {
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
|
|
185
|
+
// 第 19 批(TURN-ACROSS-SEGMENTS——设计 TURN-CAP-CONTINUE.md §19.3):编号帧——
|
|
186
|
+
// `_turnSeq` 每轮 +1(跨段累计,仅 `!resume` 链起点复位);面向消费面的两字段
|
|
187
|
+
// (状态行 + ⟦ev⟧turn / ⟦ev⟧approval 事件共用)在此同点赋值(编号唯一权威;帧
|
|
188
|
+
// 公式 = helpers.mjs turnFrame)。段内帽判定不读帧(下行循环条件只读段内
|
|
189
|
+
// turn / maxTurns——N6)。
|
|
190
|
+
const frame = turnFrame(++agent._turnSeq, turn, maxTurns)
|
|
191
|
+
agent._currentTurn = frame.turn
|
|
192
|
+
agent._maxTurns = frame.maxTurns
|
|
178
193
|
// D2 (AGENT-LOOP.md §7.2): depth>0 children emit a ⟦ev⟧turn progress token each turn —
|
|
179
194
|
// single emit point covering all three spawn tools; phase=llm (tool/done progress rides
|
|
180
|
-
// the onToolCall/onToolResult relay — no token for those).
|
|
195
|
+
// the onToolCall/onToolResult relay — no token for those). 第 19 批:载荷取上方帧值
|
|
196
|
+
// (agent._currentTurn / _maxTurns——字段形态 / 字段数 / phase 零变化,N5)。
|
|
181
197
|
if (depth > 0 && callbacks.onToken) {
|
|
182
|
-
callbacks.onToken(`⟦ev⟧turn\x1e${
|
|
198
|
+
callbacks.onToken(`⟦ev⟧turn\x1e${agent._currentTurn}\x1e${agent._maxTurns}\x1ellm\x1e`)
|
|
183
199
|
}
|
|
184
200
|
|
|
185
201
|
// SUBAGENT-OBSERVE-SEND D2: 子代理回合边界消费点——每轮开头把父侧经 subagent
|
|
@@ -286,7 +302,8 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
286
302
|
role: "user",
|
|
287
303
|
content: `[User interrupt: ${response.interruptMessage}]`,
|
|
288
304
|
})
|
|
289
|
-
|
|
305
|
+
// §20.3 站点 #8(第 24 批):错误对象已自带 name/message——只补来源标注(缺 abortInfo 才补)
|
|
306
|
+
throw annotateAbort(Object.assign(new Error("User interrupted"), { name: "AbortError" }), signal, "agent", "interrupted-response")
|
|
290
307
|
}
|
|
291
308
|
|
|
292
309
|
if (response.usage) {
|
|
@@ -318,7 +335,7 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
318
335
|
}
|
|
319
336
|
|
|
320
337
|
// abort after chat completes, before committing history: don't commit a half-finished turn
|
|
321
|
-
if (signal?.aborted) throw
|
|
338
|
+
if (signal?.aborted) throw abortError(signal, "agent", "post-chat")
|
|
322
339
|
|
|
323
340
|
pushReal(agent, {
|
|
324
341
|
role: "assistant",
|
|
@@ -391,6 +408,6 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
391
408
|
// 2026-09-05 实践轮:回合收尾(consult 清理/async 池分流/guard 继承——原 425-462
|
|
392
409
|
// 段 + collectSettledAsync 466-494 整体迁 agent/run-stages.mjs finalizeAgentTurn——
|
|
393
410
|
// CLI 对位 VS run-stages——finally 只剩一行调用 + 骨架注释)。
|
|
394
|
-
await finalizeAgentTurn(agent, { signal, autoTurn, suspDriven, thrownError })
|
|
411
|
+
await finalizeAgentTurn(agent, { signal, autoTurn, suspDriven, thrownError, depth })
|
|
395
412
|
}
|
|
396
413
|
}
|