thincoder 0.12.59 → 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 +62 -4
- package/README.md +10 -8
- package/bin/thincoder.mjs +99 -133
- package/package.json +6 -4
- package/src/abort-provenance.mjs +116 -0
- package/src/acp/bridge.mjs +45 -21
- 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 +59 -137
- package/src/advisor/project-context.mjs +194 -0
- package/src/advisor/repos.mjs +17 -40
- package/src/advisor/run.mjs +156 -359
- package/src/advisor/truncate.mjs +57 -0
- package/src/advisor.mjs +27 -7
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +145 -27
- package/src/agent/helpers.mjs +107 -13
- package/src/agent/record-results.mjs +55 -11
- package/src/agent/relay-prefix.mjs +39 -0
- package/src/agent/run-stages.mjs +242 -0
- package/src/agent/setup-reminders.mjs +69 -0
- package/src/agent/setup.mjs +107 -127
- package/src/agent/spawn-child.mjs +55 -13
- package/src/agent-tools/advisor-async.mjs +346 -0
- package/src/agent-tools/advisor-settle.mjs +231 -0
- package/src/agent-tools/advisor.mjs +167 -116
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/batch-segment.mjs +195 -0
- package/src/agent-tools/consult.mjs +139 -107
- package/src/agent-tools/design-token.mjs +117 -0
- package/src/agent-tools/digest-budget.mjs +76 -0
- package/src/agent-tools/eng.mjs +24 -29
- package/src/agent-tools/escalate-async.mjs +289 -0
- package/src/agent-tools/read-history.mjs +166 -32
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/review-streak.mjs +93 -0
- package/src/agent-tools/settings.mjs +137 -34
- package/src/agent-tools/subagent-actions.mjs +180 -133
- package/src/agent-tools/subagent-async.mjs +184 -177
- package/src/agent-tools/subagent-panel.mjs +160 -0
- package/src/agent-tools/subagent-run.mjs +205 -0
- package/src/agent-tools/subagent-scheduler.mjs +100 -27
- package/src/agent-tools/subagent-spawn.mjs +453 -0
- package/src/agent-tools/subagent.mjs +256 -578
- package/src/agent-tools/verify.mjs +119 -292
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +89 -205
- package/src/cli/distill-command.mjs +12 -6
- package/src/cli/make-agent.mjs +26 -8
- package/src/cli/memory-command.mjs +4 -3
- package/src/cli/permission.mjs +2 -2
- package/src/cli/setup-wizard.mjs +42 -17
- package/src/completions.mjs +114 -0
- package/src/config-migrate.mjs +70 -0
- package/src/config.mjs +180 -63
- package/src/context.mjs +5 -147
- package/src/conventions.mjs +223 -0
- package/src/crash-reports.mjs +128 -0
- package/src/distill.mjs +11 -11
- package/src/expand-home.mjs +16 -0
- package/src/explore-distill.mjs +155 -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 +72 -32
- package/src/memory/core.mjs +6 -193
- package/src/memory/delete.mjs +236 -0
- package/src/memory/docs.mjs +68 -54
- package/src/memory/file-walk.mjs +109 -0
- package/src/memory/schema.mjs +15 -3
- package/src/memory.mjs +3 -1
- package/src/model-ref.mjs +66 -0
- package/src/model-specs.mjs +42 -8
- package/src/peer-domains.mjs +265 -0
- package/src/peer-instances.mjs +231 -0
- package/src/prompt-overlays.mjs +82 -0
- package/src/prompts/advisor-design.md +18 -75
- package/src/prompts/advisor-round1.md +14 -67
- package/src/prompts/advisor-round2.md +15 -51
- package/src/prompts/advisor-round3.md +15 -51
- package/src/prompts/common.md +115 -0
- package/src/prompts/consult-base.md +5 -23
- 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 +18 -98
- package/src/provider/errors.mjs +101 -0
- 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/retry.mjs +8 -45
- package/src/provider/sse.mjs +3 -4
- package/src/proxy.mjs +9 -14
- 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 +188 -60
- package/src/session.mjs +104 -124
- package/src/token-ttl.mjs +274 -0
- package/src/tools/{system.mjs → bash.mjs} +19 -221
- package/src/tools/checklist-sync.mjs +181 -0
- package/src/tools/checklist.mjs +52 -39
- package/src/tools/edit-batch.mjs +109 -10
- package/src/tools/edit-diff.mjs +110 -27
- package/src/tools/edit.md +17 -12
- package/src/tools/execute.mjs +31 -4
- package/src/tools/file.mjs +41 -16
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +23 -34
- 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/index.mjs +9 -8
- package/src/tools/ops.mjs +188 -3
- package/src/tools/patch.mjs +3 -3
- package/src/tools/question.md +4 -0
- package/src/tools/question.mjs +26 -0
- package/src/tools/read.md +1 -2
- package/src/tools/read_image.md +1 -1
- package/src/tools/search.mjs +236 -0
- package/src/tools/shared.mjs +14 -13
- package/src/tools/wait_for.md +22 -0
- package/src/tui/agent-turn.mjs +36 -228
- 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 +142 -30
- package/src/tui/cmd-eng.mjs +28 -40
- package/src/tui/cmd-exit.mjs +6 -8
- package/src/tui/cmd-mcp.mjs +8 -2
- package/src/tui/cmd-model.mjs +14 -12
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-reindex.mjs +7 -0
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-submodel.mjs +8 -5
- package/src/tui/cmd-think.mjs +10 -10
- package/src/tui/cmd-undo.mjs +4 -3
- 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 +40 -38
- package/src/tui/interaction.mjs +3 -3
- 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 +52 -9
- package/src/tui/pickers.mjs +28 -392
- package/src/tui/render-frame.mjs +32 -16
- package/src/tui/render-loop.mjs +2 -0
- package/src/tui/render-segments.mjs +12 -9
- package/src/tui/render.mjs +37 -5
- package/src/tui/slash-commands.mjs +2 -2
- package/src/tui/startup.mjs +4 -0
- package/src/tui/subagent-blocks.mjs +106 -295
- package/src/tui/subagent-children.mjs +162 -0
- package/src/tui/subagent-freeze.mjs +169 -0
- package/src/tui/subagent-panel.mjs +24 -31
- package/src/tui/suspension-drive.mjs +297 -0
- package/src/tui/tool-args.mjs +7 -5
- package/src/tui/tool-display.mjs +143 -0
- package/src/tui/tool-events.mjs +56 -185
- package/src/tui/tui-lifecycle.mjs +46 -4
- package/src/tui/update-notice.mjs +4 -0
- package/src/tui/wizard.mjs +61 -21
- package/src/tui/wrapped-spawn.mjs +38 -0
- package/src/prompts/coder.md +0 -56
- package/src/prompts/discipline.md +0 -102
- package/src/prompts/eng-coder.md +0 -44
- package/src/prompts/engineering-sub.md +0 -34
- package/src/prompts/engineering.md +0 -340
- package/src/prompts/explore.md +0 -21
- package/src/prompts/main.md +0 -56
- package/src/prompts/methodology-template.md +0 -58
- package/src/prompts/plan.md +0 -16
- package/src/prompts/system.md +0 -63
- package/src/tools/pdf-parse-text.mjs +0 -497
- package/src/tools/pdf-parse-xref.mjs +0 -499
- package/src/tools/pdf.mjs +0 -155
- package/src/tools/read_pdf.md +0 -21
package/src/advisor.mjs
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* advisor.mjs — advisor system-prompt selection, follow-up building, session assembly.
|
|
3
3
|
* User-message building lives in advisor/messages.mjs; execution (tool loop, provider
|
|
4
4
|
* resolution, review entry) in advisor/run.mjs; history extraction in advisor/history.mjs.
|
|
5
|
-
*
|
|
5
|
+
* Path classification (code / doc / temp) and the project declaration surface live in
|
|
6
|
+
* ../conventions.mjs — the single authority every gate consumes (repos.mjs included).
|
|
6
7
|
*
|
|
7
8
|
* The advisor runs as a read-only exploration sub-agent with tools
|
|
8
9
|
* (read, glob, grep, ls, lsp, code_search) — ZERO git, every round. The change
|
|
@@ -45,13 +46,13 @@ import { readFileSync } from "node:fs"
|
|
|
45
46
|
import { join, dirname } from "node:path"
|
|
46
47
|
import { fileURLToPath } from "node:url"
|
|
47
48
|
import { extractAgentResponseTable } from "./advisor/history.mjs"
|
|
48
|
-
import { buildAdvisorUserMessage, resolveScopeFiles, buildObjectDeclarationBlock } from "./advisor/messages.mjs"
|
|
49
|
+
import { buildAdvisorUserMessage, resolveScopeFiles, buildObjectDeclarationBlock, buildDesignApprovalBlock } from "./advisor/messages.mjs"
|
|
49
50
|
import { buildConvergenceBody } from "./advisor/convergence.mjs"
|
|
50
51
|
import { escapeLiteralEscapes } from "./escape.mjs"
|
|
51
52
|
// Re-export for run.mjs and tests (keeps their imports from "../advisor.mjs" stable)
|
|
52
53
|
export { ADVISOR_MD_PATH, extractAgentResponseTable, extractConversationBackground } from "./advisor/history.mjs"
|
|
53
54
|
export { buildAdvisorUserMessage } from "./advisor/messages.mjs"
|
|
54
|
-
export { buildObjectDeclarationBlock } from "./advisor/messages.mjs"
|
|
55
|
+
export { buildObjectDeclarationBlock, buildDesignApprovalBlock } from "./advisor/messages.mjs"
|
|
55
56
|
|
|
56
57
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
57
58
|
|
|
@@ -195,8 +196,10 @@ export { escapeLiteralEscapes }
|
|
|
195
196
|
* @param {Object|null} [object] — review-object declaration (§18.8 D-OA1): passed through
|
|
196
197
|
* to the user-message builders; mechanically injected at the start of every review
|
|
197
198
|
* round (round 1 design/code + round 2+ follow-up). Absent → legacy behavior.
|
|
199
|
+
* @param {string|null} [designId] — §29.1 F2a: injected next to the design token in
|
|
200
|
+
* the Approval Signal (round 1 + round 2+ — both values, verbatim anchor).
|
|
198
201
|
*/
|
|
199
|
-
export function prepareAdvisorMessages(agent, reviewType, designToken = null, documents = null, paths = null, priorParam = null, object = null) {
|
|
202
|
+
export function prepareAdvisorMessages(agent, reviewType, designToken = null, documents = null, paths = null, priorParam = null, object = null, designId = null) {
|
|
200
203
|
// Deterministic convergence state (decision 2026-08-08): round 2+ requires
|
|
201
204
|
// _advisorRound > 0 AND a stored prior review output. No history parsing.
|
|
202
205
|
// priorParam (direct callers) wins over the stored output — same derivation
|
|
@@ -209,7 +212,7 @@ export function prepareAdvisorMessages(agent, reviewType, designToken = null, do
|
|
|
209
212
|
if (reviewType === "design" && (agent._advisorRound || 0) === 0) {
|
|
210
213
|
return [
|
|
211
214
|
{ role: "system", content: withTime(buildAdvisorSystemPrompt(agent, prior, reviewType)) },
|
|
212
|
-
{ role: "user", content: escapeLiteralEscapes(buildAdvisorUserMessage(agent, prior, reviewType, designToken, documents, paths, object)) },
|
|
215
|
+
{ role: "user", content: escapeLiteralEscapes(buildAdvisorUserMessage(agent, prior, reviewType, designToken, documents, paths, object, designId)) },
|
|
213
216
|
]
|
|
214
217
|
}
|
|
215
218
|
|
|
@@ -234,7 +237,7 @@ export function prepareAdvisorMessages(agent, reviewType, designToken = null, do
|
|
|
234
237
|
agent._advisorRound = 0
|
|
235
238
|
}
|
|
236
239
|
// Mutations exist → KEEP the round (cap keeps advancing through retries).
|
|
237
|
-
const user = buildAdvisorUserMessage(agent, prior, reviewType, designToken, documents, paths, object)
|
|
240
|
+
const user = buildAdvisorUserMessage(agent, prior, reviewType, designToken, documents, paths, object, designId)
|
|
238
241
|
return [
|
|
239
242
|
{ role: "system", content: withTime(buildAdvisorSystemPrompt(agent, prior, reviewType)) },
|
|
240
243
|
{
|
|
@@ -263,8 +266,25 @@ export function prepareAdvisorMessages(agent, reviewType, designToken = null, do
|
|
|
263
266
|
// scopeFiles gives the fallback (agent gave no response table) a concrete
|
|
264
267
|
// review surface.
|
|
265
268
|
const scopeFiles = resolveScopeFiles(agent, paths)
|
|
269
|
+
const followUp = buildAdvisorFollowUp(agent, prior, scopeFiles, object)
|
|
270
|
+
// §11.2 D-24b (design round 2+ — async fix-round continuations must be able to
|
|
271
|
+
// re-approve): re-anchor the review scope (the convergence follow-up carries no
|
|
272
|
+
// document list) and inject the round's design token with the approval signal.
|
|
273
|
+
if (reviewType === "design") {
|
|
274
|
+
const docList = Array.isArray(documents)
|
|
275
|
+
? documents.filter((d) => typeof d === "string" && d.trim())
|
|
276
|
+
: []
|
|
277
|
+
const scopeBlock = docList.length > 0
|
|
278
|
+
? `\n\n## Documents to Review\nThe documents below are the review scope. Review ONLY these files — do not scan git diff or read any other files.\n${docList.map((d) => `- ${d} — Read this file in full`).join("\n")}`
|
|
279
|
+
: ""
|
|
280
|
+
const tokenBlock = designToken ? `\n\n${buildDesignApprovalBlock(designToken, designId)}` : ""
|
|
281
|
+
return [
|
|
282
|
+
{ role: "system", content: withTime(buildAdvisorSystemPrompt(agent, prior, reviewType)) },
|
|
283
|
+
{ role: "user", content: escapeLiteralEscapes(followUp + scopeBlock + tokenBlock) },
|
|
284
|
+
]
|
|
285
|
+
}
|
|
266
286
|
return [
|
|
267
287
|
{ role: "system", content: withTime(buildAdvisorSystemPrompt(agent, prior, reviewType)) },
|
|
268
|
-
{ role: "user", content: escapeLiteralEscapes(
|
|
288
|
+
{ role: "user", content: escapeLiteralEscapes(followUp) },
|
|
269
289
|
]
|
|
270
290
|
}
|
package/src/agent/completion.mjs
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { hasCodeMutations } from "../advisor/repos.mjs"
|
|
8
8
|
import { pushReal } from "../context.mjs"
|
|
9
9
|
import { MAX_ADVISOR_ROUNDS } from "../advisor/run.mjs"
|
|
10
|
+
import { advisorReviewPending, effectiveAdvisorRound } from "../agent-tools/advisor-async.mjs"
|
|
10
11
|
|
|
11
12
|
const MAX_VERIFY_PUSHBACKS = 2
|
|
12
13
|
const MAX_VERIFY_RETRIES = 3
|
|
@@ -77,18 +78,20 @@ export function handleCompletion(agent, response, depth, turn, guardPushbacks, h
|
|
|
77
78
|
pushReal(agent, { role: "assistant", content: response.content })
|
|
78
79
|
agent.history.push({
|
|
79
80
|
role: "user",
|
|
80
|
-
content: "[System reminder: you modified files in this run but have not verified the changes. Before finishing:
|
|
81
|
+
content: "[System reminder: you modified files in this run but have not verified the changes. Before finishing: run the project's verification yourself (per its AGENTS.md test method), then call verify declaring the outcome via verification.status. verify mechanically gates on your declaration. If verification is genuinely impossible here, say so explicitly in your reply.]",
|
|
81
82
|
})
|
|
82
83
|
callbacks.onTurnEnd?.(agent, turn)
|
|
83
84
|
return { action: "continue", guardPushbacks, honestReminderInjected, advisorPushbacks }
|
|
84
85
|
}
|
|
85
|
-
// Verified but
|
|
86
|
+
// Verified but not passed → pushback to fix/complete (up to MAX_VERIFY_RETRIES).
|
|
87
|
+
// _verifyPassed === false means the declaration was failed, skipped without a
|
|
88
|
+
// reason, or not declared — not necessarily that a test failed.
|
|
86
89
|
if (agent._verifiedThisRun && agent._verifyPassed === false && agent._verifyRetries < MAX_VERIFY_RETRIES) {
|
|
87
90
|
agent._verifyRetries++
|
|
88
91
|
pushReal(agent, { role: "assistant", content: response.content })
|
|
89
92
|
agent.history.push({
|
|
90
93
|
role: "user",
|
|
91
|
-
content: `[System reminder:
|
|
94
|
+
content: `[System reminder: (retry ${agent._verifyRetries}/${MAX_VERIFY_RETRIES}) verify was not passed — either your verification declared failed, was skipped without a reason, or was not declared. Fix or complete your verification, then call verify again declaring the outcome. If you cannot fix after ${MAX_VERIFY_RETRIES} attempts, explain honestly what's blocking you.]`,
|
|
92
95
|
})
|
|
93
96
|
callbacks.onTurnEnd?.(agent, turn)
|
|
94
97
|
return { action: "continue", guardPushbacks, honestReminderInjected, advisorPushbacks }
|
|
@@ -103,7 +106,7 @@ export function handleCompletion(agent, response, depth, turn, guardPushbacks, h
|
|
|
103
106
|
pushReal(agent, { role: "assistant", content: response.content })
|
|
104
107
|
agent.history.push({
|
|
105
108
|
role: "user",
|
|
106
|
-
content: `[System reminder: ${MAX_VERIFY_RETRIES} verify attempts exhausted
|
|
109
|
+
content: `[System reminder: ${MAX_VERIFY_RETRIES} verify attempts exhausted. You have not passed verification. Either state explicitly that your verification could not be completed, or run verify again once it is. If your verification could not be completed, say so explicitly in your reply to the user — state what you tried and what you believe is blocking you, and do not present the work as complete; the user needs to know it is unfinished.]`,
|
|
107
110
|
})
|
|
108
111
|
callbacks.onTurnEnd?.(agent, turn)
|
|
109
112
|
return { action: "continue", guardPushbacks, honestReminderInjected, advisorPushbacks }
|
|
@@ -117,18 +120,21 @@ export function handleCompletion(agent, response, depth, turn, guardPushbacks, h
|
|
|
117
120
|
const cfg = agent.config?.advisor
|
|
118
121
|
const advisorReview = cfg?.guard === true
|
|
119
122
|
if (depth === 0 && advisorReview && !agent.config?.agent?.engineering) {
|
|
120
|
-
//
|
|
121
|
-
// (
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
|
|
123
|
+
// §11.2 D-24b (T-24b4 — guard timing): an async review that is still in flight
|
|
124
|
+
// (or queued in the advisor pool) means the review was launched — the guard
|
|
125
|
+
// does NOT push back while it is pending (未决不算未评审); once it settles
|
|
126
|
+
// non-stale it marks _calledAdvisorThisRun, and a STALE settle leaves the
|
|
127
|
+
// mark unset so the guard pushes back here again (fix #2 — no silent skip).
|
|
128
|
+
const pending = advisorReviewPending(agent)
|
|
129
|
+
const rounds = effectiveAdvisorRound(agent)
|
|
130
|
+
if (!pending && agent._mutatedThisRun && !agent._calledAdvisorThisRun && hasCodeMutations(agent)
|
|
125
131
|
&& advisorPushbacks < MAX_ADVISOR_PUSHBACKS
|
|
126
|
-
&&
|
|
132
|
+
&& rounds < MAX_ADVISOR_ROUNDS) {
|
|
127
133
|
advisorPushbacks++
|
|
128
134
|
pushReal(agent, { role: "assistant", content: response.content })
|
|
129
135
|
agent.history.push({
|
|
130
136
|
role: "user",
|
|
131
|
-
content: `[System reminder: you changed code in this run and MUST get an advisor review before finishing (round ${
|
|
137
|
+
content: `[System reminder: you changed code in this run and MUST get an advisor review before finishing (round ${rounds + 1}). Call the \`advisor\` tool now. This is required, not optional — do not skip it even if you believe the changes are trivial — the review will be quick either way. After the review, produce a response table for every issue found (see discipline rules for format).]`,
|
|
132
138
|
})
|
|
133
139
|
callbacks.onTurnEnd?.(agent, turn)
|
|
134
140
|
return { action: "continue", guardPushbacks, honestReminderInjected, advisorPushbacks }
|
package/src/agent/dispatch.mjs
CHANGED
|
@@ -5,10 +5,19 @@ import { logEvent, errText, headText } from "../log.mjs"
|
|
|
5
5
|
import { offloadToolResult, FILE_MUTATORS } from "./helpers.mjs"
|
|
6
6
|
import { runHooks } from "../hooks.mjs"
|
|
7
7
|
import { snapshotForUndo } from "../tui/cmd-undo.mjs"
|
|
8
|
-
import {
|
|
8
|
+
import { isCodePath, loadConventions } from "../conventions.mjs"
|
|
9
|
+
// R10 L3 (MULTI-INSTANCE-COLLAB §2a.5 D-L3b):写工具钩子——peerCollabNote(执行前冲突
|
|
10
|
+
// 检测——软提示不阻止)+ recordPeerWrites(成功后累积本回合写足迹——回合末 flush)。
|
|
11
|
+
import { PEER_WRITE_TOOLS, peerCollabNote, recordPeerWrites } from "../peer-domains.mjs"
|
|
9
12
|
import { writeFileSync, mkdirSync, existsSync } from "node:fs"
|
|
10
|
-
import { join } from "node:path"
|
|
13
|
+
import { join, resolve, relative } from "node:path"
|
|
11
14
|
import { homedir } from "node:os"
|
|
15
|
+
// §29 fix A(AGENT-LOOP.md §29——2026-09-07):FILE_MUTATORS 的 mutation-seq 记账从
|
|
16
|
+
// 批后提交(record-results noteMutations)移到执行成功即刻——唯一记账点(取代批后段
|
|
17
|
+
// + agent.mjs 中断分支记账——不双计)——同消息 [写 + async advisor launch] 时 launch 前
|
|
18
|
+
// 完成的写在 launchSeq 之前落地 → settle 不再误判 stale(§29 症状根因)。
|
|
19
|
+
import { noteMutations, inflightDesignReviewConflict } from "../agent-tools/advisor-async.mjs"
|
|
20
|
+
import { anyLiveDesignSlot } from "../token-ttl.mjs"
|
|
12
21
|
|
|
13
22
|
const ERRORS_DIR = join(homedir(), ".thincoder", "tool-errors")
|
|
14
23
|
|
|
@@ -40,9 +49,9 @@ function logToolError(toolName, args, error) {
|
|
|
40
49
|
|
|
41
50
|
/**
|
|
42
51
|
* §19 action-level classification (AGENT-LOOP.md §19 D-M1): the merged subagent
|
|
43
|
-
* tool expresses spawn (side effect) and
|
|
52
|
+
* tool expresses spawn (side effect) and status (read-only query) through
|
|
44
53
|
* its `action` parameter — the tool-level readonly flag can no longer express both.
|
|
45
|
-
* dispatch Phase-1/Phase-2 classifies per action:
|
|
54
|
+
* dispatch Phase-1/Phase-2 classifies per action: status behaves as readonly
|
|
46
55
|
* (planMode pass / no permission ask / batchable), spawn keeps its non-readonly
|
|
47
56
|
* gates, escalate runs non-readonly AND serially (the retired escalate tool had no
|
|
48
57
|
* parallel flag — zero behavior change under the merged surface).
|
|
@@ -50,13 +59,13 @@ function logToolError(toolName, args, error) {
|
|
|
50
59
|
* stops, never starts. isSubagentControlAction feeds the SAME two gate sites as
|
|
51
60
|
* readonly (planMode pass / no permission ask — never joins a batch approval
|
|
52
61
|
* group / no handler → not denied — digest 内 cancel 放行).
|
|
53
|
-
* §19.6 panel (round1 #5): view 面归只读类(同
|
|
62
|
+
* §19.6 panel (round1 #5): view 面归只读类(同 status——planMode 放行、免
|
|
54
63
|
* 审批、可批并行);freeze 面归控制类(同 cancel——planMode 放行、免权限审批、
|
|
55
64
|
* 批审批不入组、digest 内放行)。freeze 存在(非空 key)即控制类——否则只读类。
|
|
56
65
|
*/
|
|
57
66
|
function isSubagentReadonlyAction(toolName, args) {
|
|
58
67
|
// §6 memory 工具面重构(MEMORY.md §6 D-M5):memory search/list 是只读动作——与
|
|
59
|
-
// subagent
|
|
68
|
+
// subagent status 同分类(planMode 放行/免审批——Phase-2 批并行只认工具级
|
|
60
69
|
// readonly/parallel,memory 无 parallel → 按非只读串行,见 MEMORY.md §6.4 实现注)。
|
|
61
70
|
// 动作级判定——不能按工具名(同一 memory 工具的 put/delete/clear 保持侧效门)。
|
|
62
71
|
if (toolName === "memory") {
|
|
@@ -71,7 +80,10 @@ function isSubagentReadonlyAction(toolName, args) {
|
|
|
71
80
|
}
|
|
72
81
|
if (toolName !== "subagent" || !args || typeof args !== "object") return false
|
|
73
82
|
const action = args.action
|
|
74
|
-
|
|
83
|
+
// §19.8: check 动作已删除——只读面仅剩 status(planMode 放行/免权限审批/可批并行)
|
|
84
|
+
if (action === "status") return true
|
|
85
|
+
// SUBAGENT-OBSERVE-SEND:observe = readonly 查询(同 status——digest/planMode 放行)
|
|
86
|
+
if (action === "observe") return true
|
|
75
87
|
// §19.6 panel view 面(freeze 缺省/空 = 视图请求——readonly;非空 freeze 归控制类)
|
|
76
88
|
if (action === "panel" && (args.freeze === undefined || args.freeze === null || String(args.freeze) === "")) return true
|
|
77
89
|
return false
|
|
@@ -79,14 +91,44 @@ function isSubagentReadonlyAction(toolName, args) {
|
|
|
79
91
|
function isSubagentControlAction(toolName, args) {
|
|
80
92
|
if (toolName !== "subagent") return false
|
|
81
93
|
if (args?.action === "cancel") return true
|
|
94
|
+
// SUBAGENT-OBSERVE-SEND:send = 控制类豁免(同 cancel——父回合内显式调用即授权——
|
|
95
|
+
// 写子输入队列属父对子轻量引导,非产品代码写——免审批、planMode 放行、digest 内放行)
|
|
96
|
+
if (args?.action === "send") return true
|
|
82
97
|
// §19.6 panel freeze 面(D-P3 门控在 executor——只读/控制分类在此)
|
|
83
98
|
if (args?.action === "panel" && args.freeze !== undefined && args.freeze !== null && String(args.freeze) !== "") return true
|
|
84
99
|
return false
|
|
85
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* §2.6 token 链终消费制(2026-09-07——评审 #7d dispatch 分类):consume-design =
|
|
103
|
+
* 非只读控制动作——planMode 拒绝(不入 readonly/control 豁免——与其他非只读动作同门)、
|
|
104
|
+
* 免权限审批、不入批审批分组(无文件写——控制类直行——只停既有状态不起新副作用)。
|
|
105
|
+
* 与 cancel 的不同:cancel 是控制类豁免(planMode 放行),consume-design 按设计
|
|
106
|
+
* planMode 拒绝——故不并入 isSubagentControlAction,单独谓词只接权限豁免位。
|
|
107
|
+
*/
|
|
108
|
+
function isSubagentConsumeDesignAction(toolName, args) {
|
|
109
|
+
return toolName === "subagent" && args?.action === "consume-design"
|
|
110
|
+
}
|
|
86
111
|
function isSubagentEscalateAction(toolName, args) {
|
|
87
112
|
return toolName === "subagent" && args?.action === "escalate"
|
|
88
113
|
}
|
|
89
114
|
|
|
115
|
+
/**
|
|
116
|
+
* §29 fix A — 唯一记账点:FILE_MUTATORS 工具执行成功即刻记 mutation seq(abs 路径)。
|
|
117
|
+
* 取代 record-results 批后段 + agent.mjs 中断分支的 noteMutations(不双计——中断+同批
|
|
118
|
+
* launch 场景 seq 单计,AGENT-LOOP.md §29 T-A1i)。调用时机 = 写执行成功(非 Error 前缀
|
|
119
|
+
* 结果——recordPeerWrites 同款门);routed(M2 ACP 客户端执行)成功同样记账。
|
|
120
|
+
*/
|
|
121
|
+
function noteExecutedMutation(agent, tool, args) {
|
|
122
|
+
let paths
|
|
123
|
+
try {
|
|
124
|
+
paths = tool.touchedPaths ? tool.touchedPaths(args ?? {}) : [args?.path]
|
|
125
|
+
} catch { return }
|
|
126
|
+
const abs = (paths ?? [])
|
|
127
|
+
.filter((p) => typeof p === "string" && p)
|
|
128
|
+
.map((p) => resolve(agent.cwd, p))
|
|
129
|
+
if (abs.length > 0) noteMutations(agent, abs)
|
|
130
|
+
}
|
|
131
|
+
|
|
90
132
|
/**
|
|
91
133
|
* Two-phase execution:
|
|
92
134
|
* Phase 1 (serial): parse args one by one + planMode check + permission confirmation (side-effecting tools)
|
|
@@ -138,24 +180,61 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
138
180
|
}
|
|
139
181
|
|
|
140
182
|
// Engineering mode PARENT gate: the parent agent must not touch code files
|
|
141
|
-
// before the design review passed. Signaled by
|
|
142
|
-
//
|
|
143
|
-
// eng-coder-only and reset per run). Exemptions
|
|
144
|
-
// (
|
|
145
|
-
//
|
|
146
|
-
// src
|
|
147
|
-
//
|
|
148
|
-
|
|
183
|
+
// before the design review passed. Signaled by a live design slot (design-review
|
|
184
|
+
// approval — persists in the session slot, survives across turns; _engDesignReviewed
|
|
185
|
+
// is eng-coder-only and reset per run). Exemptions = the non-code classes of
|
|
186
|
+
// src/conventions.mjs (documentation and temp scratch files) — writing a design
|
|
187
|
+
// document IS the design step. Anything inside a declared code segment (default:
|
|
188
|
+
// src — incl. src/prompts/*.md) is product code, not documentation, and needs a
|
|
189
|
+
// design token. The project can declare its own code paths (.thincoder/
|
|
190
|
+
// conventions.json) so a non-src layout is not silently exempted. Mechanically
|
|
191
|
+
// blocks "talk then code".
|
|
192
|
+
// DESIGN-TOKEN-SETTLEMENT D3(2026-09-08):资格判据 = 权威槽"任一活槽存在"
|
|
193
|
+
// (anyLiveDesignSlot——查内存 Map,miss 回读槽文件——单值镜像 `_engDesignToken`
|
|
194
|
+
// 已退役,门禁不再读镜像——AC4)。
|
|
195
|
+
if (agent.config?.agent?.engineering && depth === 0
|
|
196
|
+
&& !anyLiveDesignSlot(agent)
|
|
149
197
|
&& FILE_MUTATORS.has(toolCall.name)) {
|
|
150
198
|
const paths = tool.touchedPaths ? tool.touchedPaths(args) : [args.path]
|
|
199
|
+
const conv = loadConventions(agent.cwd)
|
|
151
200
|
// Unknown/missing paths (non-string, e.g. no path argument) are treated
|
|
152
|
-
// as code — cannot tell what they touch, so block conservatively.
|
|
153
|
-
|
|
201
|
+
// as code — cannot tell what they touch, so block conservatively. Known
|
|
202
|
+
// paths go through the single shared classifier: a declared code segment
|
|
203
|
+
// (default: "src") at ANY depth, else anything that is not documentation.
|
|
204
|
+
const touchesCode = paths.some((p) => typeof p !== "string" || isCodePath(p, conv))
|
|
154
205
|
if (touchesCode) {
|
|
206
|
+
// Undeclared project → point at the declaration file (§4.3 降级可见契约).
|
|
207
|
+
const convNote = conv.declared
|
|
208
|
+
? ""
|
|
209
|
+
: ` — this path was classified as product code by the default conventions (code paths: ${conv.codePaths.join(", ")}); declare project conventions in .thincoder/conventions.json to adjust.`
|
|
155
210
|
prepared.push({
|
|
156
211
|
toolCall, tool, denied: true,
|
|
157
212
|
reason: "engineering design gate",
|
|
158
|
-
hint:
|
|
213
|
+
hint: `Engineering mode: write the design document first(location per your project's document conventions), then call advisor with type='design' to review it, and wait for user approval. Implementation is done by eng-coder subagents.${convNote}`,
|
|
214
|
+
})
|
|
215
|
+
continue
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// E(第 11 批·F17/§14.14 E-3d):D5 冻结窗口写前拦截——设计评审在途(点火 → 结算)期间,
|
|
220
|
+
// 父侧对被审文件集(声明文档集 + 批次档)的写入会被拒绝:在途写使本轮结算 stale——pass 轮
|
|
221
|
+
// = token 直接丢失(实证:第 10 批 id=20 整轮作废)。工具面 = FILE_MUTATORS(与变更记账
|
|
222
|
+
// 同集——不记入日志的写面既不判 stale 也不拦);判据与 reviewIsStale 同源
|
|
223
|
+
// (inflightDesignReviewConflict——同 docAbs / 同 normAbs;仅扫 running 未取消的设计条目)。
|
|
224
|
+
// 位置:只读 / autoApprove 短路之前——审批不得绕过冻结;拒绝 = 可见 denied + 逃生门
|
|
225
|
+
// (先 cancel → 改 → 重发)。
|
|
226
|
+
if (FILE_MUTATORS.has(toolCall.name)) {
|
|
227
|
+
let touched = []
|
|
228
|
+
try { touched = tool.touchedPaths ? tool.touchedPaths(args) : [args.path] } catch { touched = [] }
|
|
229
|
+
const absPaths = (touched ?? [])
|
|
230
|
+
.filter((p) => typeof p === "string" && p)
|
|
231
|
+
.map((p) => resolve(agent.cwd, p))
|
|
232
|
+
const conflict = inflightDesignReviewConflict(agent, absPaths)
|
|
233
|
+
if (conflict) {
|
|
234
|
+
prepared.push({
|
|
235
|
+
toolCall, tool, denied: true,
|
|
236
|
+
reason: "d5 freeze window",
|
|
237
|
+
hint: `write refused — design review #${conflict.id} is in flight over ${relative(agent.cwd, conflict.path)} (D5 freeze window). A write now would settle it stale — no token for a pass (the round is lost). Wait for the report, or cancel the review first (subagent action:'cancel' id:'${conflict.id}') and re-launch after the change.`,
|
|
159
238
|
})
|
|
160
239
|
continue
|
|
161
240
|
}
|
|
@@ -171,7 +250,7 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
171
250
|
// — the exemption never widens what reaches this stage (round4 #3, T-E14).
|
|
172
251
|
// PreToolUse hooks still run below. Non-eng-coder children keep the manual
|
|
173
252
|
// parent ask (human in the loop).
|
|
174
|
-
if (tool.readonly || isSubagentReadonlyAction(toolCall.name, args) || isSubagentControlAction(toolCall.name, args) || agent.autoApprove || agent._engTaskAuthorized) {
|
|
253
|
+
if (tool.readonly || isSubagentReadonlyAction(toolCall.name, args) || isSubagentControlAction(toolCall.name, args) || isSubagentConsumeDesignAction(toolCall.name, args) || agent.autoApprove || agent._engTaskAuthorized) {
|
|
175
254
|
if (!(await runHooks("PreToolUse", { agent, toolName: toolCall.name, toolArgs: args }))) {
|
|
176
255
|
prepared.push({ toolCall, tool, denied: true, reason: "blocked by PreToolUse hook" })
|
|
177
256
|
continue
|
|
@@ -247,6 +326,8 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
247
326
|
? "Error: plan mode is active — only read-only tools are allowed. Exit plan mode first."
|
|
248
327
|
: item.reason === "engineering design gate"
|
|
249
328
|
? `Error: design review required before any file modification. ${item.hint}`
|
|
329
|
+
: item.reason === "d5 freeze window"
|
|
330
|
+
? `Error: ${item.hint}`
|
|
250
331
|
: item.reason === "denied by user"
|
|
251
332
|
? "Error: permission denied by user"
|
|
252
333
|
: item.reason === "blocked by PreToolUse hook"
|
|
@@ -267,6 +348,11 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
267
348
|
const toolName = item.toolCall.name
|
|
268
349
|
logEvent("tool:call", { tool: toolName, child: agent?._logId })
|
|
269
350
|
try {
|
|
351
|
+
// R10 L3 (MULTI-INSTANCE-COLLAB §2a.5 D-L3b):结构化写工具执行前查 conflicts
|
|
352
|
+
// (命中他实例 hot 域 → 结果附软提示——决策⑥ A 不阻止;一次目录 stat——N3 度量);
|
|
353
|
+
// 足迹累积(D-L3a——"检测+记录一次完成")延后到执行成功(实际写过的文件)。
|
|
354
|
+
const isPeerWriteTool = PEER_WRITE_TOOLS.has(toolName)
|
|
355
|
+
const peerNote = isPeerWriteTool ? peerCollabNote(agent.cwd, item.tool, item.args) : null
|
|
270
356
|
// Snapshot for undo before side-effect tools (setupOutputPanel already fired in Phase 1)
|
|
271
357
|
if (!item.tool?.readonly && item.args) {
|
|
272
358
|
snapshotForUndo(agent, item.toolCall.name, item.args, agent.cwd)
|
|
@@ -276,9 +362,14 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
276
362
|
if (callbacks.toolRouter) {
|
|
277
363
|
const routed = await callbacks.toolRouter(item.toolCall.name, item.args)
|
|
278
364
|
if (routed?.handled) {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
365
|
+
const routedOk = !String(routed.result).startsWith("Error:")
|
|
366
|
+
const routedResult = peerNote && routedOk ? `${routed.result}\n${peerNote}` : routed.result
|
|
367
|
+
if (isPeerWriteTool && routedOk) recordPeerWrites(agent, item.tool, item.args)
|
|
368
|
+
// §29 fix A:routed 写成功(客户端执行)同样执行期即刻记账(唯一记账点)
|
|
369
|
+
if (routedOk && FILE_MUTATORS.has(toolName)) noteExecutedMutation(agent, item.tool, item.args)
|
|
370
|
+
callbacks.onToolResult?.(item.toolCall.name, routedResult, item.toolCall.id)
|
|
371
|
+
logEvent("tool:done", { tool: toolName, ms: Date.now() - toolT0, head: headText(routedResult, 200), child: agent?._logId })
|
|
372
|
+
return { ...item, result: routedResult, ok: true }
|
|
282
373
|
}
|
|
283
374
|
}
|
|
284
375
|
const origConsoleLog = console.log
|
|
@@ -296,18 +387,41 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
296
387
|
depth,
|
|
297
388
|
signal,
|
|
298
389
|
callbacks,
|
|
390
|
+
// §11.2 D-24b: per-call id — the advisor tool marker-keys its launch so
|
|
391
|
+
// recordToolResults can split async-ack accounting from sync settles.
|
|
392
|
+
_toolCallId: item.toolCall.id,
|
|
299
393
|
onOutput: (chunk) => callbacks.onToolOutput?.(item.toolCall.name, chunk, item.toolCall.id),
|
|
300
394
|
onQuestion: callbacks.onQuestion,
|
|
301
395
|
onPermissionRequest: callbacks.onPermissionRequest,
|
|
302
396
|
}
|
|
303
397
|
try {
|
|
304
|
-
|
|
398
|
+
// SUBAGENT-OBSERVE-SEND D1(评审 #1):in-flight 当前工具记账——工具执行期间在
|
|
399
|
+
// agent 上留 _inflightTools Set(子代理 observe 从 dispatch 状态读——卡在长工具
|
|
400
|
+
// 调用时 history 无新回合、恰需此信号);finally 清除。批并行工具同入 Set(observe
|
|
401
|
+
// 如实返回多个在跑工具)。主会话同样记账——无害(无人读)。
|
|
402
|
+
const inflight = agent._inflightTools ?? (agent._inflightTools = new Set())
|
|
403
|
+
inflight.add(toolName)
|
|
404
|
+
try {
|
|
405
|
+
rawResult = await item.tool.execute(item.args, toolCtx)
|
|
406
|
+
} finally {
|
|
407
|
+
inflight.delete(toolName)
|
|
408
|
+
}
|
|
305
409
|
} finally {
|
|
306
410
|
console.log = origConsoleLog
|
|
307
411
|
console.error = origConsoleErr
|
|
308
412
|
}
|
|
309
413
|
if (rawResult === undefined) throw new Error(`Tool "${item.toolCall.name}" returned undefined — all tools must return a string value`)
|
|
310
414
|
const raw = String(rawResult)
|
|
415
|
+
// 写成功(非 "Error:" 字符串结果)→ 足迹计入本回合集合(flush 在 finalizeAgentTurn)
|
|
416
|
+
if (isPeerWriteTool && !raw.startsWith("Error:")) {
|
|
417
|
+
recordPeerWrites(agent, item.tool, item.args)
|
|
418
|
+
}
|
|
419
|
+
// §29 fix A:FILE_MUTATORS 执行成功即刻记账(唯一记账点——取代 record-results 批后
|
|
420
|
+
// 段 + agent.mjs 中断分支——不双计)——同批 launch 前的写在 launchSeq 之前落地 →
|
|
421
|
+
// async advisor settle 不误判 stale(同批 launch 后写仍保守 stale——T-A2/T-24b9)。
|
|
422
|
+
if (FILE_MUTATORS.has(toolName) && !raw.startsWith("Error:")) {
|
|
423
|
+
noteExecutedMutation(agent, item.tool, item.args)
|
|
424
|
+
}
|
|
311
425
|
// Multimodal tools keep the raw result (base64 images ride the multimodal
|
|
312
426
|
// channel); everything else offloads oversized text to disk. Flag-driven, not
|
|
313
427
|
// name-driven (consult P3, 2026-08-30).
|
|
@@ -316,11 +430,15 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
316
430
|
const resultWithConsole = capturedConsole.length > 0
|
|
317
431
|
? `${result}\n[console during ${item.toolCall.name}]\n${capturedConsole.join("\n")}`
|
|
318
432
|
: result
|
|
319
|
-
|
|
433
|
+
// R10 L3:冲突软提示附在工具结果末尾(模型可见——不阻止写)
|
|
434
|
+
const resultForModel = peerNote && !raw.startsWith("Error:")
|
|
435
|
+
? `${resultWithConsole}\n${peerNote}`
|
|
436
|
+
: resultWithConsole
|
|
437
|
+
callbacks.onToolResult?.(item.toolCall.name, resultForModel, item.toolCall.id, toolCtx._subagentKey)
|
|
320
438
|
// PostToolUse hooks: fire-and-forget (result not awaited on hook failure)
|
|
321
439
|
runHooks("PostToolUse", { agent, toolName: item.toolCall.name, toolArgs: item.args, result: raw }).catch(() => {})
|
|
322
|
-
logEvent("tool:done", { tool: toolName, ms: Date.now() - toolT0, head: headText(
|
|
323
|
-
return { ...item, result:
|
|
440
|
+
logEvent("tool:done", { tool: toolName, ms: Date.now() - toolT0, head: headText(resultForModel, 200), child: agent?._logId })
|
|
441
|
+
return { ...item, result: resultForModel, ok: true }
|
|
324
442
|
} catch (error) {
|
|
325
443
|
// Persist to ~/.thincoder/tool-errors/ for post-mortem; only pass message to the model (stack traces confuse LLMs and may leak paths)
|
|
326
444
|
logToolError(item.toolCall.name, item.args, error)
|
|
@@ -357,7 +475,7 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
|
|
|
357
475
|
for (const item of prepared) {
|
|
358
476
|
// escalate action keeps the retired escalate tool's serial placement (no
|
|
359
477
|
// parallel flag): it flushes the batch and runs alone in call order (§19 —
|
|
360
|
-
// spawn stays parallel;
|
|
478
|
+
// spawn stays parallel; status classifies as readonly and batch freely).
|
|
361
479
|
if (item.tool && !item.tool.readonly
|
|
362
480
|
&& (!item.tool.parallel || isSubagentEscalateAction(item.tool.name, item.args))) {
|
|
363
481
|
await flush()
|
package/src/agent/helpers.mjs
CHANGED
|
@@ -6,7 +6,8 @@ import { readFileSync, readdirSync } from "node:fs"
|
|
|
6
6
|
import { homedir } from "node:os"
|
|
7
7
|
import { writeFile, mkdir, readdir, stat, unlink } from "node:fs/promises"
|
|
8
8
|
import { join } from "node:path"
|
|
9
|
-
import {
|
|
9
|
+
import { execFile } from "node:child_process"
|
|
10
|
+
import { promisify } from "node:util"
|
|
10
11
|
|
|
11
12
|
/** Single source for the AUTO-mode reminder (was duplicated in agent.mjs +
|
|
12
13
|
* setup.mjs — consult P2, 2026-08-30). */
|
|
@@ -138,20 +139,65 @@ export async function offloadToolResult(text, callId, dir = join(configDir, "too
|
|
|
138
139
|
}
|
|
139
140
|
}
|
|
140
141
|
|
|
141
|
-
|
|
142
|
-
|
|
142
|
+
// 失败冷却 v1(用户裁——GIT-ASYNC L21):真失败/超时后 30s 内跳过该 cwd 的收集——
|
|
143
|
+
// 病态 repo 周期拖慢变一次性。VSC setup-reminders.mjs 同构镜像。
|
|
144
|
+
const GIT_FAILURE_COOLDOWN_MS = 30_000
|
|
145
|
+
/** cwd → 最近一次 git 收集失败的 ts。评审 #2:访问时惰性清 >30s 旧条目(防无界累积)。 */
|
|
146
|
+
const gitFailureCooldowns = new Map()
|
|
147
|
+
// maxBuffer 沿用 execSync 默认现值 1 MiB(评审 #1——防大输出仓 ENOBUFS 翻转 → "" 破字节 parity)
|
|
148
|
+
const GIT_MAX_BUFFER = 1024 * 1024
|
|
149
|
+
|
|
150
|
+
const execFileAsync = promisify(execFile)
|
|
151
|
+
|
|
152
|
+
/** 冷却检查 + 惰性清扫:冷却期内的 cwd 返回 true(调用方直接 "" 跳过)。 */
|
|
153
|
+
function gitCooldownActive(cwd) {
|
|
154
|
+
const now = Date.now()
|
|
155
|
+
const lastFailure = gitFailureCooldowns.get(cwd)
|
|
156
|
+
if (lastFailure === undefined) return false
|
|
157
|
+
if (now - lastFailure >= GIT_FAILURE_COOLDOWN_MS) {
|
|
158
|
+
gitFailureCooldowns.delete(cwd) // 惰性清过期条目
|
|
159
|
+
return false
|
|
160
|
+
}
|
|
161
|
+
return true
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Test seams — cooldown Map 状态控制(生产从不调用):
|
|
165
|
+
* `_gitFailureCooldownForTests(cwd, ts)` 读(ts 省略)或写 ts;`_clear…` 删条目。 */
|
|
166
|
+
export function _gitFailureCooldownForTests(cwd, ts) {
|
|
167
|
+
if (ts !== undefined) gitFailureCooldowns.set(cwd, ts)
|
|
168
|
+
return gitFailureCooldowns.get(cwd)
|
|
169
|
+
}
|
|
170
|
+
export function _clearGitFailureCooldownForTests(cwd) {
|
|
171
|
+
gitFailureCooldowns.delete(cwd)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Git context 纯格式化(branch/log/status = trim 后原串)——独立导出供单测锁字节
|
|
175
|
+
* parity(现拼装逐字节保留——detached/dirty>20 截断/clean 三形态——VSC 镜像)。 */
|
|
176
|
+
export function composeGitContext({ branch, log, status }) {
|
|
177
|
+
const dirty = status ? status.split("\n").length : 0
|
|
178
|
+
return [
|
|
179
|
+
`Git context: on branch \`${branch || "(detached)"}\`${dirty ? `, ${dirty} uncommitted change(s)` : ", working tree clean"}.`,
|
|
180
|
+
log ? `Recent commits:\n${log}` : "",
|
|
181
|
+
status ? `Uncommitted:\n${status.split("\n").slice(0, MAX_GIT_CHANGES_DISPLAY).join("\n")}${dirty > MAX_GIT_CHANGES_DISPLAY ? `\n… (${dirty - MAX_GIT_CHANGES_DISPLAY} more)` : ""}` : "",
|
|
182
|
+
].filter(Boolean).join("\n")
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Collect git branch, recent commits, and working tree status as context text.
|
|
186
|
+
* GIT-ASYNC L21:3×execSync 串行(最坏 15s 阻塞)→ 3×execFile 并行(Promise.all——
|
|
187
|
+
* 最坏 = 单次 5s 超时——事件循环不冻结)。单 catch → "" 保持 all-or-nothing(任一
|
|
188
|
+
* 失败/超时 → 整段不注入——非 git 快失败同路径);catch 记冷却 ts——30s 内跳过。 */
|
|
189
|
+
export async function collectGitContext(cwd) {
|
|
190
|
+
if (gitCooldownActive(cwd)) return ""
|
|
143
191
|
try {
|
|
144
|
-
const opts = { cwd, encoding: "utf8",
|
|
145
|
-
const branch
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
log ? `Recent commits:\n${log}` : "",
|
|
152
|
-
status ? `Uncommitted:\n${status.split("\n").slice(0, MAX_GIT_CHANGES_DISPLAY).join("\n")}${dirty > MAX_GIT_CHANGES_DISPLAY ? `\n… (${dirty - MAX_GIT_CHANGES_DISPLAY} more)` : ""}` : "",
|
|
153
|
-
].filter(Boolean).join("\n")
|
|
192
|
+
const opts = { cwd, encoding: "utf8", timeout: GIT_TIMEOUT_MS, windowsHide: true, maxBuffer: GIT_MAX_BUFFER }
|
|
193
|
+
const [branch, log, status] = await Promise.all([
|
|
194
|
+
execFileAsync("git", ["branch", "--show-current"], opts).then((r) => r.stdout.trim()),
|
|
195
|
+
execFileAsync("git", ["--no-pager", "log", "--oneline", "-5"], opts).then((r) => r.stdout.trim()),
|
|
196
|
+
execFileAsync("git", ["status", "--short"], opts).then((r) => r.stdout.trim()),
|
|
197
|
+
])
|
|
198
|
+
return composeGitContext({ branch, log, status })
|
|
154
199
|
} catch {
|
|
200
|
+
gitFailureCooldowns.set(cwd, Date.now()) // 真失败/超时——冷却 30s
|
|
155
201
|
return ""
|
|
156
202
|
}
|
|
157
203
|
}
|
|
@@ -164,6 +210,18 @@ export class ContinueError extends Error {
|
|
|
164
210
|
}
|
|
165
211
|
}
|
|
166
212
|
|
|
213
|
+
/** 跨段累计编号帧(TURN-CAP-CONTINUE.md §19.3——第 19 批 TURN-ACROSS-SEGMENTS):
|
|
214
|
+
* 唯一计算点(纯函数)——把链内累计序数换算成面向消费面的编号载荷。
|
|
215
|
+
* - seq = 该轮链内累计序数(1 起——`agent._turnSeq` 每轮 +1,续跑不重置)
|
|
216
|
+
* - turn = 段内轮号(0 起——段内帽判定的循环变量)
|
|
217
|
+
* - maxTurns = 本段预算(段内帽判定值——原样传入)
|
|
218
|
+
* 返回 { turn, maxTurns }:turn = seq(累计已跑轮数);maxTurns = 段前累计 + 本段预算
|
|
219
|
+
* = seq - turn - 1 + maxTurns(差额项 = 本段开始前的链内累计)。
|
|
220
|
+
* 段内帽判定不读本帧(只读段内 turn / maxTurns——N6 零机制改动)。 */
|
|
221
|
+
export function turnFrame(seq, turn, maxTurns) {
|
|
222
|
+
return { turn: seq, maxTurns: seq - turn - 1 + maxTurns }
|
|
223
|
+
}
|
|
224
|
+
|
|
167
225
|
/** Repair malformed conversation history: remove orphan tool messages and fill missing tool results */
|
|
168
226
|
export function repairHistory(history) {
|
|
169
227
|
const out = []
|
|
@@ -288,3 +346,39 @@ export async function loadProjectInstructions(cwd) {
|
|
|
288
346
|
merged
|
|
289
347
|
)
|
|
290
348
|
}
|
|
349
|
+
|
|
350
|
+
// Engineering mode reminders + auto-turn digest domain + mode injector
|
|
351
|
+
// (2026-09-05 module-split:自 agent.mjs 迁入——agent.mjs 530 > 500 硬限——agent.mjs
|
|
352
|
+
// re-export 保 import 面:eng.mjs / cmd-eng.mjs / 测试从 agent.mjs import)
|
|
353
|
+
|
|
354
|
+
/** Engineering mode reminder — shared with eng.mjs tool. */
|
|
355
|
+
export const ENG_ON_REMINDER =
|
|
356
|
+
"[System reminder: engineering mode is ON — design-before-code enforced. " +
|
|
357
|
+
"Workflow: Requirements doc → Design doc → advisor(type='design') → " +
|
|
358
|
+
"user approval → eng-coder implementation. Code changes go through eng-coder " +
|
|
359
|
+
"subagents only. Advisor calls are NOT per-turn-mandatory — call only at " +
|
|
360
|
+
"flow nodes or when the user asks.]"
|
|
361
|
+
|
|
362
|
+
/** Engineering mode OFF reminder — shared with the eng tool and the injector. */
|
|
363
|
+
export const ENG_OFF_REMINDER =
|
|
364
|
+
"[System reminder: engineering mode is now OFF — standard discipline applies. " +
|
|
365
|
+
"Changes go through the normal workflow: you may edit files directly, advisor/verify " +
|
|
366
|
+
"guards apply per config.]"
|
|
367
|
+
|
|
368
|
+
/** Manual-tier auto-turn digest domain (AGENT-LOOP.md §17 D-S6): organize-only.
|
|
369
|
+
* Injected per manual auto-turn run — writes/execute/spawns/questions are also
|
|
370
|
+
* mechanically denied (no permission handler + spawn gate); this steers first. */
|
|
371
|
+
export const AUTO_TURN_DIGEST_DOMAIN =
|
|
372
|
+
"[System reminder: auto-turn — background async subagents finished while there was no user message, and this turn runs automatically to digest their reports (the finished-report reminders above). No one is waiting for this reply, so organize only: 1) summarize each finished report's key points into this conversation for the user to read later; 2) update the task list with the task tool (allowed) to mark finished work done; 3) write decision points with a suggested next step as text — do not execute it. FORBIDDEN this turn (mechanically enforced): modifying files, bash/execute/verify, spawning subagents, asking questions — those need a real user message. End the turn once the summaries are written.]"
|
|
373
|
+
|
|
374
|
+
/** Engineering-mode status injection — one reminder on EVERY transition (2026-08-25:
|
|
375
|
+
* OFF is announced too — the model must know the gates lifted; silence after /eng-off
|
|
376
|
+
* left it guessing. Covers TUI /eng, resume, and any path bypassing the eng tool.) */
|
|
377
|
+
export function injectEngineeringReminder(agent) {
|
|
378
|
+
const eng = agent.config?.agent?.engineering ?? false
|
|
379
|
+
if (eng !== agent._lastEngState) {
|
|
380
|
+
agent.history.push({ role: "user", content: eng ? ENG_ON_REMINDER : ENG_OFF_REMINDER, transient: true })
|
|
381
|
+
}
|
|
382
|
+
agent._lastEngState = eng
|
|
383
|
+
}
|
|
384
|
+
|