thincoder 0.12.10 → 0.12.12

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/src/advisor.mjs CHANGED
@@ -1,12 +1,13 @@
1
1
  /**
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
- * resolution, review entry) in advisor/run.mjs; git discovery/collection in
5
- * advisor/repos.mjs; history extraction in advisor/history.mjs.
4
+ * resolution, review entry) in advisor/run.mjs; history extraction in advisor/history.mjs.
5
+ * repos.mjs still hosts the doc-file classifier (isDocFile) used by mutation tracking.
6
6
  *
7
7
  * The advisor runs as a read-only exploration sub-agent with tools
8
- * (read, glob, grep, ls, git, lsp, code_search). It discovers changes
9
- * via git diff, reads files for context, and traces callers via grep/lsp.
8
+ * (read, glob, grep, ls, lsp, code_search) — ZERO git, every round. The change
9
+ * surface comes from the review scope (paths / _touchedFiles), never from git;
10
+ * verification is `read`-only with quoted-line evidence (7d49a52, d3be613).
10
11
  *
11
12
  * Config:
12
13
  * { advisor: { enabled: true, provider: "deepseek", model: "deepseek-chat" } }
@@ -14,9 +15,16 @@
14
15
  *
15
16
  * Convergence protocol:
16
17
  * Round 1: full review → produces a numbered issue table.
17
- * Agent responds with a response table per issue.
18
- * Round 2: semi-convergence — verifies table + can flag obvious new issues.
18
+ * Agent responds with a response table per issue (fix claims).
19
+ * Round 2: semi-convergence — verifies the prior table + can flag obvious new issues.
19
20
  * Round 3+: strict convergence — only checks the prior issue table.
21
+ * The prior issue table IS injected into rounds 2+ (decision 2026-08-05,
22
+ * reversed) — it is the ONLY complete verification list: the agent response
23
+ * table covers only issues the agent chose to answer, so skipped issues would
24
+ * silently escape convergence without it. The fix-claim table travels as a
25
+ * focus reference only. Restatement risk is handled mechanically:
26
+ * host-verified citations reject references that do not match the current
27
+ * disk state, and fresh sessions exclude old read data.
20
28
  * Each round replaces the system prompt (ROUND1 → ROUND2 → ROUND3) so the
21
29
  * round-1 full-scope mandate can't bleed into later rounds, plus a mechanical
22
30
  * cap (MAX_ADVISOR_ROUNDS in run.mjs) refuses a 6th review call outright.
@@ -24,22 +32,20 @@
24
32
  * file:line evidence for any unfixed/new finding — see docs/design/ADVISOR-CONVERGENCE.md.
25
33
  *
26
34
  * Session memory (agent._advisorSession):
27
- * All advisor calls within one run share a single conversation — round 2+
28
- * just appends a follow-up (agent response + refreshed diff + round rules),
29
- * so the advisor keeps its exploration context instead of re-discovering
30
- * everything. The session is discarded when the run ends (runAgent resets it);
31
- * the next task starts a fresh advisor session. After an app restart the
32
- * in-memory session is gone falls back to a fresh session seeded from the
33
- * issue/response tables in the main history.
35
+ * RETAINED for initialization compatibility but NEVER read (decision d698434):
36
+ * every review round builds a fresh [system, user] session round 2+ must not
37
+ * reuse round 1's messages, because the old read outputs are the anchoring
38
+ * source of re-review false reports and a token sink. Convergence data (prior
39
+ * issue table + agent response table) travels via buildAdvisorFollowUp.
40
+ * The field is reset by runAgent; the write sites are harmless leftovers.
34
41
  *
35
42
  * Project customisation: .thincoder/advisor.md in the project root.
36
43
  */
37
44
  import { readFileSync } from "node:fs"
38
45
  import { join, dirname } from "node:path"
39
46
  import { fileURLToPath } from "node:url"
40
- import { findReviewRepos } from "./advisor/repos.mjs"
41
47
  import { extractPriorIssueTable, extractAgentResponseTable } from "./advisor/history.mjs"
42
- import { buildAdvisorUserMessage } from "./advisor/messages.mjs"
48
+ import { buildAdvisorUserMessage, buildConvergenceInstructions, resolveScopeFiles } from "./advisor/messages.mjs"
43
49
  // Re-export for run.mjs and tests (keeps their imports from "../advisor.mjs" stable)
44
50
  export { ADVISOR_MD_PATH, extractPriorIssueTable, extractAgentResponseTable, extractConversationBackground } from "./advisor/history.mjs"
45
51
  export { buildAdvisorUserMessage } from "./advisor/messages.mjs"
@@ -50,15 +56,30 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
50
56
  // Prompt files — loaded at module init
51
57
  // ────────────────────────────────────────
52
58
 
53
- const ADVISOR_ROUND1 = readFileSync(join(__dirname, "prompts", "advisor-round1.md"), "utf8")
59
+ function loadPrompt(file, name) {
60
+ try {
61
+ return readFileSync(join(__dirname, "prompts", file), "utf8")
62
+ } catch {
63
+ throw new Error(`${name} missing from the installation (prompts/${file}) — reinstall thincoder or restore the file`)
64
+ }
65
+ }
66
+
67
+ const ADVISOR_ROUND1 = loadPrompt("advisor-round1.md", "advisor-round1.md")
54
68
  // ROUND2/3 are used whenever a convergence round (round 2+) is being built:
55
69
  // in-run session continuation replaces the system prompt with them, and a
56
70
  // rebuilt fresh session (e.g. after a failed review) also selects them via
57
71
  // buildAdvisorSystemPrompt when _advisorRound > 0.
58
- const ADVISOR_ROUND2 = readFileSync(join(__dirname, "prompts", "advisor-round2.md"), "utf8")
59
- const ADVISOR_ROUND3 = readFileSync(join(__dirname, "prompts", "advisor-round3.md"), "utf8")
72
+ const ADVISOR_ROUND2 = loadPrompt("advisor-round2.md", "advisor-round2.md")
73
+ const ADVISOR_ROUND3 = loadPrompt("advisor-round3.md", "advisor-round3.md")
74
+ // Fallback when advisor-design.md is missing — keep in sync with the real
75
+ // file (table format + workflow steps; a drifted fallback would also break
76
+ // extractPriorIssueTable's DESIGN_TABLE_HEADER matching).
77
+ const ADVISOR_DESIGN_FALLBACK = `You are an independent design reviewer for an engineering-mode project. Review the design document in the changes below. Evaluate: completeness, feasibility, clarity, scope, acceptance criteria. Read METHODOLOGY.md if provided. Produce a review table with | # | Category | Severity | Issue | Suggestion | format.`
60
78
  let ADVISOR_DESIGN = ""
61
- try { ADVISOR_DESIGN = readFileSync(join(__dirname, "prompts", "advisor-design.md"), "utf8") } catch { /* design review unavailable */ }
79
+ // Design review is OPTIONAL (engineering mode only) silent fallback to the
80
+ // in-code constant is intentional, unlike the mandatory round prompts which
81
+ // must exist for every review (loadPrompt throws a descriptive error there).
82
+ try { ADVISOR_DESIGN = readFileSync(join(__dirname, "prompts", "advisor-design.md"), "utf8") } catch { /* fallback below */ }
62
83
 
63
84
  // ────────────────────────────────────────
64
85
  // System prompt building
@@ -67,15 +88,24 @@ try { ADVISOR_DESIGN = readFileSync(join(__dirname, "prompts", "advisor-design.m
67
88
  /**
68
89
  * Build the system prompt for an advisor review session.
69
90
  * @param {Object} agent — the parent agent
70
- * @param {Object|null} [_prior] — prior issue table (from extractPriorIssueTable)
91
+ * @param {Object|null} [prior] — prior issue table (from extractPriorIssueTable)
71
92
  * @param {string} [reviewType] — "design" for design review, undefined/"code" for code review
72
93
  * @returns {string} the system prompt
73
94
  */
74
- export function buildAdvisorSystemPrompt(agent, _prior, reviewType) {
75
- // Design review: dedicated prompt, no convergence rounds
76
- if (reviewType === "design") return ADVISOR_DESIGN || `You are an independent design reviewer for an engineering-mode project. Review the design document in the changes below. Evaluate: completeness, feasibility, clarity, scope, acceptance criteria. Read METHODOLOGY.md if provided. Produce a review table with | # | Category | Severity | Issue | Suggestion | format.`
77
- const prior = _prior ?? extractPriorIssueTable(agent.history)
78
- if (!prior || (agent._advisorRound || 0) === 0) return ADVISOR_ROUND1
95
+ export function buildAdvisorSystemPrompt(agent, prior, reviewType) {
96
+ // Design review: round 1 uses the dedicated design-review prompt (full scope +
97
+ // approval token); rounds 2+ converge like code reviews (verify agent fix claims).
98
+ if (reviewType === "design") {
99
+ const p = prior ?? extractPriorIssueTable(agent.history)
100
+ if (!p || (agent._advisorRound || 0) === 0) {
101
+ return ADVISOR_DESIGN || ADVISOR_DESIGN_FALLBACK
102
+ }
103
+ const round = (agent._advisorRound || 0) + 1
104
+ if (round === 2) return ADVISOR_ROUND2
105
+ return ADVISOR_ROUND3
106
+ }
107
+ const p = prior ?? extractPriorIssueTable(agent.history)
108
+ if (!p || (agent._advisorRound || 0) === 0) return ADVISOR_ROUND1
79
109
  const round = (agent._advisorRound || 0) + 1
80
110
  if (round === 2) return ADVISOR_ROUND2
81
111
  return ADVISOR_ROUND3
@@ -87,98 +117,190 @@ export function buildAdvisorSystemPrompt(agent, _prior, reviewType) {
87
117
 
88
118
  /**
89
119
  * Build a follow-up user message for round 2+ — the agent's response table +
90
- * the refreshed diff, without re-sending the full round-1 context.
120
+ * round-aware instructions, without re-sending the full round-1 context.
121
+ * Deliberately NO git information injected (no diff snapshot, no git context):
122
+ * git output misled re-reviews — committed fixes never show in `git diff HEAD`,
123
+ * so the model read "no changes" as "no fixes". Verification is `read`-only.
124
+ * NOTE: the caller (prepareAdvisorMessages) applies escapeLiteralEscapes to
125
+ * the return value — direct callers must do the same (the prior table and
126
+ * agent response can quote literal "\x"/"\u" sequences).
127
+ * @param {Object} agent — the parent agent (history used for the response table)
128
+ * @param {Object|null} prior — prior issue table (extracted from history when null)
129
+ * @param {string[]|null} [scopeFiles] — review surface for the no-response fallback (cwd-relative)
130
+ * @returns {string} the follow-up user message — or a plain "System reminder: …"
131
+ * fresh-review fallback (NO brackets — some OpenAI-compatible servers parse
132
+ * '['-prefixed content as structured data / expand escapes) when no prior
133
+ * review exists at all (caller misuse; the response-table extraction would
134
+ * otherwise scan history from index 0 and could match an unrelated stale table)
91
135
  */
92
- export function buildAdvisorFollowUp(agent, _prior) {
93
- const prior = _prior ?? extractPriorIssueTable(agent.history)
94
- const response = extractAgentResponseTable(agent.history, prior?.sinceIdx ?? 0)
95
- || "(Agent did not provide a response table re-evaluate each issue)"
136
+ export function buildAdvisorFollowUp(agent, prior, scopeFiles = null) {
137
+ // Convergence follow-up REQUIRES a prior review record. The caller usually
138
+ // passes it; fall back to extracting it from history (compat for direct
139
+ // callers/tests). If there is genuinely no prior review, a nullish table
140
+ // would make the response-table extraction scan from index 0 and possibly
141
+ // match an unrelated stale table — return a round-1-style message instead.
142
+ const p = prior ?? extractPriorIssueTable(agent.history)
143
+ if (!p) {
144
+ // Plain "System reminder:" prefix (no brackets) — same convention as the
145
+ // round-1 path (some OpenAI-compatible servers parse '['-prefixed content
146
+ // as structured data / expand escapes; see prepareAdvisorMessages).
147
+ return "System reminder: convergence follow-up requested without a prior review — perform a fresh full review."
148
+ }
149
+ // Convergence semantics require round >= 2 (round 1 is the full review, not
150
+ // verification). A direct caller with _advisorRound 0 would otherwise get a
151
+ // meaningless "Round 1 — Strict Verification".
152
+ if ((agent._advisorRound || 0) < 1) {
153
+ return "System reminder: convergence follow-up requested at round 1 — a full review is already in progress; no prior verification exists yet."
154
+ }
155
+ const noResponseFallback = scopeFiles?.length
156
+ ? "(Agent did not provide a response table — perform a fresh review of: " + scopeFiles.slice(0, 10).join(", ") + ")"
157
+ : "(Agent did not provide a response table — perform a fresh full review; the review surface is unknown, ask the user for the file list)"
158
+ const response = extractAgentResponseTable(agent.history, p.sinceIdx) || noResponseFallback
96
159
  const round = (agent._advisorRound || 0) + 1
97
160
  const label = round === 2 ? "Verify Prior Table + Flag New Issues" : "Strict Verification"
98
161
 
162
+ const reminder = round === 2
163
+ ? "verify every item in the prior issue table and flag only obvious new issues introduced by the fixes"
164
+ : "strictly verify only the prior issue table — do NOT look for new issues"
99
165
  const parts = [
100
166
  `## Round ${round} — ${label}`,
101
167
  "",
102
- `[System reminder: this is round ${round} of the convergence protocol. The system prompt for this round has already narrowed the review scope — follow it: ${round === 2 ? "verify the prior table and flag only obvious new issues introduced by the fixes" : "strictly verify only the prior table — do NOT look for new issues"}.]`,
168
+ `[System reminder: this is round ${round} of the convergence protocol. ` +
169
+ `The system prompt for this round has already narrowed the review scope — follow it: ${reminder}.]`,
103
170
  "",
104
- "## Prior Issue Table",
105
- prior?.text ?? "(no prior tablereview from scratch)",
171
+ // Prior issue table IS in the context (decision 2026-08-05, reversed):
172
+ // it is the ONLY complete verification list the agent response table
173
+ // covers only issues the agent chose to answer, so issues the agent
174
+ // skipped would silently escape convergence. Restatement risk is handled
175
+ // mechanically: host-verified citations reject references that do not
176
+ // match the CURRENT disk state, and fresh sessions exclude old read data.
177
+ // The agent response table stays as a focus aid ("I fixed X"), not as the
178
+ // to-verify list.
179
+ "## Prior Issue Table (verify every item)",
180
+ p.text,
106
181
  "",
107
- "## Agent Response",
182
+ "## Agent Response (fix claims — reference only)",
108
183
  response,
109
184
  "",
110
185
  "## Instructions",
111
- round === 2
112
- ? "Verify each item in the prior table. Flag any obvious NEW issues introduced by the fixes (crashes, data loss, logic errors — not style). Produce a verification table."
113
- : "Strictly verify ONLY the items in the prior table against the CURRENT FILE STATE (use `read` — an empty diff does not mean the fixes are absent). Do NOT look for new issues.",
114
- "",
115
- "IMPORTANT: the prior issue table is HISTORY — always verify current file state with `read` before judging an item as fixed or unfixed.",
116
- // Round-aware evidence rule: "New" entries only exist in round 2 (round 3+ forbids them).
117
- `STALE-CONTEXT WARNING: only fresh \`read\` results describe the current state — never judge from earlier snapshots or from \`git diff\` (committed fixes never show in \`git diff HEAD\`). Read the files to verify. Any "Unfixed" entry${round === 2 ? ' (and any "New" entry)' : ""} MUST quote the exact line content from THIS round's \`read\` output (e.g. \`run.mjs:180: timeoutId = setTimeout(...)\`); line numbers alone are NOT evidence (they may come from the stale prior table). Uncited findings are unverified and will be ignored.`,
118
- "",
119
- "Do NOT re-read AGENTS.md / design docs. Verify fix status with \`read\` only — do not rely on git output: a clean working tree does not mean fixes are absent (they may be committed).",
186
+ ...buildConvergenceInstructions(round, scopeFiles),
120
187
  "",
121
188
  ]
122
- // Deliberately NO git information injected here (no diff snapshot, no git context):
123
- // git output misled re-reviews — committed fixes never show in `git diff HEAD`, so
124
- // the model read "no changes" as "no fixes". Verification is `read`-only by design.
125
189
  return parts.join("\n")
126
190
  }
127
191
 
128
192
  /**
129
- * Build or continue the advisor conversation for this run.
130
- * First call in a run: fresh [system, user] session. Later calls: append a
131
- * follow-up to the existing session so the advisor keeps its context.
132
- * After an app restart (session lost), starts a fresh round-1 full review —
133
- * exploration context is gone, so prior tables from history are not injected.
134
- * @param {string[]|null} [paths] code review only: explicit list of file/dir paths to review
193
+ * Resolve the review surface for the convergence fallback moved to
194
+ * messages.mjs so the legacy path shares it (see there).
195
+ */
196
+
197
+ /**
198
+ * Neutralize literal backslash escape sequences ("\x", "\u") that some
199
+ * OpenAI-compatible servers interpret inside message content ("unexpected end
200
+ * of hex escape" → 400 — observed 2026-08-06 when the conversation background
201
+ * quoted "\x" literals). Only sequences that would be INVALID when expanded
202
+ * are doubled ("\\x" → literal "\x" after server expansion); well-formed
203
+ * "\xNN" / "\uNNNN" pass through untouched (they expand to a byte/codepoint).
204
+ */
205
+ export function escapeLiteralEscapes(text) {
206
+ // (?<!\\) — only a SINGLE backslash counts ("\\x" already doubles the
207
+ // escape and must pass through untouched); lookbehind is fine on Node 24.
208
+ // Known limitation (documented, accepted): an ODD backslash run of 3+ (e.g.
209
+ // "\\\x") leaves the trailing "\x" un-doubled — vanishingly rare in real
210
+ // conversation text, and the sequence is still valid JSON either way.
211
+ // The lookahead treats "\x" followed by AT LEAST 2 hex as valid (servers
212
+ // expand only the first two: "\x1b3" → ESC + "3"); only truncated runs
213
+ // ("\x" + <2 hex) are doubled.
214
+ text = String(text ?? "")
215
+ return text
216
+ .replace(/(?<!\\)\\(x)(?![0-9a-fA-F]{2})/g, "\\\\$1")
217
+ .replace(/(?<!\\)\\(u)(?![0-9a-fA-F]{4})/g, "\\\\$1")
218
+ }
219
+
220
+
221
+ /**
222
+ * Build the advisor conversation for this run.
223
+ * EVERY call builds a fresh [system, user] session (decision d698434) — no
224
+ * session reuse across rounds: round 1 = full scope (ROUND1 prompt), rounds
225
+ * 2+ = convergence (ROUND2/ROUND3 prompt + fix-claims follow-up).
226
+ * @param {Object} agent — the parent agent
135
227
  * @param {string} [reviewType] — "design" or "code" (default)
136
228
  * @param {string|null} [designToken] — design-review approval token (design only)
137
229
  * @param {string[]|null} [documents] — design review only: explicit list of doc paths to review (passed through to buildAdvisorUserMessage)
230
+ * @param {string[]|null} [paths] — code review only: explicit list of file/dir paths to review
138
231
  */
139
232
  export function prepareAdvisorMessages(agent, reviewType, designToken = null, documents = null, paths = null) {
140
233
  const prior = extractPriorIssueTable(agent.history)
141
- // Design review: always fresh session, no convergence
142
- if (reviewType === "design") {
234
+
235
+ // Design review round 1: the dedicated full-scope review with the approval
236
+ // token (an independent gate — it runs even when a prior table exists, e.g.
237
+ // after a failed design review). Fresh session.
238
+ if (reviewType === "design" && (agent._advisorRound || 0) === 0) {
143
239
  return [
144
240
  { role: "system", content: buildAdvisorSystemPrompt(agent, prior, reviewType) },
145
- { role: "user", content: buildAdvisorUserMessage(agent, prior, reviewType, designToken, documents, paths) },
241
+ { role: "user", content: escapeLiteralEscapes(buildAdvisorUserMessage(agent, prior, reviewType, designToken, documents, paths)) },
146
242
  ]
147
243
  }
148
- let session = agent._advisorSession
149
- if (session) {
150
- // Session exists but no prior table (last review was all-clear or none)
151
- // a follow-up "Verify Prior Table" would be meaningless; start a fresh full review
152
- if (!prior) {
153
- agent._advisorSession = null
154
- session = null
155
- // Only reset the round counter on a truly fresh start (no prior reviews at all).
156
- // If _advisorRound > 0, there WAS a prior review it just passed (all-clear).
157
- if (!agent._advisorRound) agent._advisorRound = 0
158
- } else {
159
- // Convergence rounds (2+): replace the system prompt so the round-1
160
- // "full-scope review" mandate cannot override the follow-up's narrowed scope.
161
- // Without this the model re-runs a full review every round, finds new issues
162
- // each time, and the protocol never converges. buildAdvisorSystemPrompt
163
- // returns ROUND2 for round 2 and ROUND3 for round 3+.
164
- session[0] = { role: "system", content: buildAdvisorSystemPrompt(agent, prior, reviewType) }
165
- session.push({ role: "user", content: buildAdvisorFollowUp(agent, prior) })
166
- return session
244
+
245
+ // Every round is a FRESH session (decision d698434): round 2+ must NOT reuse
246
+ // round 1's messages the old read outputs are the top anchoring source of
247
+ // re-review false reports (the model quoted pre-fix file content instead of
248
+ // re-reading) and a token sink. The agent response table (fix claims) is
249
+ // injected through buildAdvisorFollowUp instead; the system prompt carries
250
+ // the round (ROUND2/ROUND3) via buildAdvisorSystemPrompt.
251
+ // Guard matches buildAdvisorSystemPrompt's ROUND1 condition
252
+ // (`!prior || _advisorRound === 0`): a stale prior table with _advisorRound 0
253
+ // (history persists across runAgent calls) must yield a fresh round-1 review —
254
+ // ROUND1 system prompt + full-scope user message, never the convergence
255
+ // follow-up (which would contradict the ROUND1 system prompt). The
256
+ // _advisorRound===0 half was lost in the _mutatedThisRun refactor and is
257
+ // restored here (regression 67ac851 6e15a6b window).
258
+ // No prior table: reset ONLY when this run made no code changes (user
259
+ // decision 2026-08-05: any loop that modified code must NOT reset — the
260
+ // advisor guard WILL push back, so the convergence round must keep advancing
261
+ // toward the cap; a run with no mutations has no push-back risk and a reset
262
+ // is safe). Deterministic runtime state (`_mutatedThisRun`) decides — never
263
+ // model output (phrases/table headers drift; three rounds of false reports
264
+ // proved it). Either way the message is a fresh full review (no issue list
265
+ // exists without a prior table) — only the round counter differs.
266
+ if (!prior || (agent._advisorRound || 0) === 0) {
267
+ if (!(agent._mutatedThisRun ?? false)) {
268
+ // New review cycle (first review, all-clear, or no code changes): reset
269
+ // the round so the cycle gets its own 5-round budget.
270
+ agent._advisorRound = 0
167
271
  }
272
+ // Mutations exist → KEEP the round (cap keeps advancing through retries).
273
+ const user = buildAdvisorUserMessage(agent, prior, reviewType, designToken, documents, paths)
274
+ return [
275
+ { role: "system", content: buildAdvisorSystemPrompt(agent, prior, reviewType) },
276
+ {
277
+ role: "user",
278
+ // NOTE (2026-08-06): the leading prefix is a PLAIN "System reminder:",
279
+ // NOT "[System reminder: ...]" — some OpenAI-compatible servers try to
280
+ // parse content that STARTS with '[' as structured content (or expand
281
+ // escape sequences in it). A literal "\x" inside the conversation
282
+ // background (e.g. the parent agent quoting escape sequences) then
283
+ // fails server-side as "unexpected end of hex escape" → 400. Plain
284
+ // prefix keeps the review message a plain string everywhere.
285
+ // The whole content also passes through escapeLiteralEscapes (below)
286
+ // so literal "\x"/"\u" quoted by the parent agent can never form an
287
+ // invalid escape when the server expands them.
288
+ content: escapeLiteralEscapes(`System reminder: no prior issue table is being carried into this review (first review, app restart, or session clear) — start with a fresh full review.\n\n${user}`),
289
+ },
290
+ ]
168
291
  }
169
- session = [
292
+
293
+ // Convergence rounds (2+): fresh [system(ROUND2/3), user(prior table + fix
294
+ // claims)]. buildAdvisorFollowUp carries BOTH the prior issue table (the
295
+ // only complete verification list — decision 2026-08-05, reversed) and the
296
+ // agent's fix-claim table (focus reference). buildAdvisorSystemPrompt
297
+ // selects ROUND2 for round 2, ROUND3 for rounds 3+ — a failed review retry
298
+ // keeps _advisorRound so the convergence prompt matches the attempt count.
299
+ // scopeFiles gives the fallback (agent gave no response table) a concrete
300
+ // review surface.
301
+ const scopeFiles = resolveScopeFiles(agent, paths)
302
+ return [
170
303
  { role: "system", content: buildAdvisorSystemPrompt(agent, prior, reviewType) },
171
- { role: "user", content: buildAdvisorUserMessage(agent, prior, reviewType, designToken, documents, paths) },
304
+ { role: "user", content: escapeLiteralEscapes(buildAdvisorFollowUp(agent, prior, scopeFiles)) },
172
305
  ]
173
- // Fresh session. Only reset round if this is truly the first review.
174
- // If _advisorRound > 0, there was a prior review that passed (all-clear).
175
- if (!agent._advisorRound) agent._advisorRound = 0
176
- if (!prior) {
177
- // Tell the advisor why no prior issue table is present
178
- session[1] = {
179
- role: "user",
180
- content: `[System reminder: no prior issue table is being carried into this review (first review, app restart, or session clear) — start with a fresh full review.]\n\n${session[1].content}`,
181
- }
182
- }
183
- return session
184
306
  }
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { isDocFile } from "../advisor/repos.mjs"
8
8
  import { pushReal } from "../context.mjs"
9
+ import { MAX_ADVISOR_ROUNDS } from "../advisor/run.mjs"
9
10
 
10
11
  /** True when this run mutated at least one CODE file. Mirrors agent.mjs:hasCodeMutations. */
11
12
  function hasCodeMutations(agent) {
@@ -117,12 +118,18 @@ export function handleCompletion(agent, response, depth, turn, guardPushbacks, h
117
118
  }
118
119
 
119
120
  // --- advisor guard: review of mutated files before completion ---
120
- // OPT-IN via advisor.enabled + guard!==false, and NEVER in engineering mode.
121
+ // Active by default when advisor.enabled is set (opt-out via guard: false),
122
+ // and NEVER in engineering mode.
121
123
  const cfg = agent.config?.advisor
122
124
  const advisorReview = cfg?.enabled && cfg?.guard !== false
123
125
  if (depth === 0 && advisorReview && !agent.config?.agent?.engineering) {
126
+ // Cap sync: beyond MAX_ADVISOR_ROUNDS the advisor tool refuses to review
127
+ // (run.mjs convergence cap) — pushing back further would loop forever
128
+ // (fix → pushback → cap-refused call → fix …). The cap message from the
129
+ // last accepted review stands; the user decides manually.
124
130
  if (agent._mutatedThisRun && !agent._calledAdvisorThisRun && hasCodeMutations(agent)
125
- && advisorPushbacks < MAX_ADVISOR_PUSHBACKS) {
131
+ && advisorPushbacks < MAX_ADVISOR_PUSHBACKS
132
+ && (agent._advisorRound || 0) < MAX_ADVISOR_ROUNDS) {
126
133
  advisorPushbacks++
127
134
  pushReal(agent, { role: "assistant", content: response.content })
128
135
  agent.history.push({
@@ -152,6 +152,15 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
152
152
  if (!item.tool?.readonly && item.args) {
153
153
  snapshotForUndo(agent, item.toolCall.name, item.args, agent.cwd)
154
154
  }
155
+ // M2 ACP: route fs tools through the client (IDE buffer / diff review).
156
+ // toolRouter returns { handled: true, result } to short-circuit execution.
157
+ if (callbacks.toolRouter) {
158
+ const routed = await callbacks.toolRouter(item.toolCall.name, item.args)
159
+ if (routed?.handled) {
160
+ callbacks.onToolResult?.(item.toolCall.name, routed.result)
161
+ return { ...item, result: routed.result, ok: true }
162
+ }
163
+ }
155
164
  const rawResult = await item.tool.execute(item.args, {
156
165
  cwd: agent.cwd,
157
166
  agent,
@@ -172,6 +181,11 @@ export async function executeToolCalls(agent, toolByName, toolCalls, callbacks,
172
181
  } catch (error) {
173
182
  // Persist to ~/.thincoder/tool-errors/ for post-mortem; only pass message to the model (stack traces confuse LLMs and may leak paths)
174
183
  logToolError(item.toolCall.name, item.args, error)
184
+ // User interrupt (Ctrl+C / Ctrl+I) must propagate, not become a tool error:
185
+ // swallowing it here would make the parent keep looping while the user
186
+ // asked to stop — worst case with subagents, where the child runs its
187
+ // whole turn budget and the interrupt appears to do nothing.
188
+ if (signal?.aborted) throw error
175
189
  runHooks("PostToolUseFailure", { agent, toolName: item.toolCall.name, toolArgs: item.args, error }).catch(() => {})
176
190
  // Build contextual error: tool name + key args so the model can reason about what went wrong
177
191
  const ctxParts = []
@@ -79,10 +79,10 @@ export const advisorTool = {
79
79
  "Run an independent review on your work. " +
80
80
  "Use type='design' to review design documents before implementation — pass documents=[...] with the explicit list of doc paths to review; use documents in code review too (the task's Docs involved list). " +
81
81
  "Use type='code' (default) to review code changes after implementation — pass paths=[...] to specify which files or directories to review, or documents=[...] for acceptance criteria context. " +
82
- "The advisor is an independent read-only sub-agent that explores the codebase, runs git diff, " +
82
+ "The advisor is an independent read-only sub-agent that explores the codebase, " +
83
83
  "reads files, and traces callers via grep/lsp. " +
84
- "For code review: round 1 does a full review, round 2 verifies the prior table, " +
85
- "round 3+ strictly checks only the prior table — convergence, not divergence. " +
84
+ "For code review: round 1 does a full review, round 2 verifies the agent's fix claims, " +
85
+ "round 3+ strictly checks only the fix claims — convergence, not divergence. " +
86
86
  "For design review: single-pass review against methodology and requirements. " +
87
87
  "Review criteria come from .thincoder/advisor.md (if present) or sensible defaults. " +
88
88
  "After the review, you MUST produce a response table (see discipline rules for format). " +
@@ -94,7 +94,7 @@ export const advisorTool = {
94
94
  paths: {
95
95
  type: "array",
96
96
  items: { type: "string" },
97
- description: "Code files or directories to review (for code review). Required unless documents is provided. The advisor reviews git diff filtered to these paths.",
97
+ description: "Code files or directories to review (for code review). Required unless documents is provided. The advisor reads the files/directories listed here — it has no git tool and never inspects diffs.",
98
98
  },
99
99
  documents: {
100
100
  type: "array",
@@ -110,11 +110,13 @@ export const advisorTool = {
110
110
  const agent = ctx.agent
111
111
  const reviewType = args.type || "code"
112
112
  const documents = args.documents || null
113
- const paths = args.paths || null
113
+ // Scope fallback: the runtime mutation record (zero git) covers guard-triggered
114
+ // reviews where the model did not pass explicit paths.
115
+ const paths = args.paths || (agent._touchedFiles?.length ? [...agent._touchedFiles] : null)
114
116
 
115
117
  // Code review must have a scope — no implicit fallback.
116
118
  if (reviewType !== "design" && !paths && !documents) {
117
- return "Advisor: no review scope specified. Provide paths (files/directories to review) or documents (acceptance criteria — code diff is still used)."
119
+ return "Advisor: no review scope specified. Provide paths (files/directories to review) or documents (acceptance criteria context)."
118
120
  }
119
121
 
120
122
  // Design review: validate that documents are in docs/ or are recognized doc files
@@ -129,11 +131,10 @@ export const advisorTool = {
129
131
  }
130
132
  }
131
133
 
132
- // Design review: always starts from round 1 (no convergence)
133
- if (reviewType === "design") {
134
- agent._advisorRound = 0
135
- agent._advisorSession = null
136
- }
134
+ // Design review: NO round reset design reviews share the 5-round convergence
135
+ // budget with code reviews (round advances in agent.mjs; cap in run.mjs).
136
+ // Session reset also removed: design rounds 2+ continue the advisor session
137
+ // like code reviews (fix claims + round-aware prompts).
137
138
 
138
139
  // Generate the design token BEFORE the review and inject it into the advisor's prompt.
139
140
  // The advisor (LLM) decides pass/fail itself and echoes the token only on approval —