thincoder 0.12.12 → 0.12.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thincoder",
3
- "version": "0.12.12",
3
+ "version": "0.12.14",
4
4
  "description": "Thin coding agent - zero dependencies, no build step, Node.js native. Sharp code, zero bloat.",
5
5
  "keywords": [
6
6
  "ai",
@@ -0,0 +1,80 @@
1
+ /**
2
+ * advisor/convergence.mjs — shared convergence-round message building (round 2+).
3
+ * SINGLE source for the round-2+ sections used by BOTH the normal flow
4
+ * (buildAdvisorFollowUp in advisor.mjs) and the legacy path (buildAdvisorUserMessage
5
+ * in messages.mjs) — fixes must not be replicated in two places. Lives in its
6
+ * own module to avoid the messages.mjs ↔ advisor.mjs import cycle.
7
+ */
8
+
9
+ /**
10
+ * Shared convergence-round instructions — single source for BOTH paths
11
+ * (buildAdvisorUserMessage's legacy convergence block and
12
+ * buildAdvisorFollowUp), so the wording cannot diverge.
13
+ * Round 2 may flag obvious new issues; round 3+ is strict verification.
14
+ * @param {number} round — convergence round number (2+)
15
+ * @param {string[]|null} scopeFiles — optional file list for the no-response fallback
16
+ * @returns {string[]} the numbered instruction lines (callers spread them)
17
+ */
18
+ export function buildConvergenceInstructions(round, scopeFiles = null) {
19
+ const fileList = scopeFiles?.length
20
+ ? ` The review surface is: ${scopeFiles.slice(0, 10).join(", ")}.`
21
+ : ""
22
+ return [
23
+ `1. IMPORTANT: verify EVERY item of the prior review output against the CURRENT FILE STATE with \`read\` — never decide based on earlier snapshots alone.${fileList}`,
24
+ "2. STALE-CONTEXT WARNING: any diff or file content from earlier messages is a historical snapshot — treat it as expired. Only fresh `read` results describe the current state.",
25
+ "3. You have no git tool; git output in earlier messages is historical and untrustworthy (committed fixes never show in a diff).",
26
+ "4. `read` the files named in the prior review output (or the review surface above) in full — ALWAYS. Batch reads/greps in a single reply.",
27
+ "5. Evidence rule: every 'Unfixed'/'New' finding 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 be stale or fabricated. Findings without a fresh quoted line are treated as unverified and will not be accepted.",
28
+ "6. Produce your verification table. Do not re-read content you already have.",
29
+ round === 2
30
+ ? "7. You may flag obvious NEW issues introduced by the fixes (crashes, data loss, logic errors — not style)."
31
+ : "7. Do NOT look for new issues.",
32
+ ]
33
+ }
34
+
35
+ /**
36
+ * Shared convergence body — SINGLE source for the round-2+ message sections
37
+ * (decision 2026-08-08): the FULL verbatim prior review output is the only
38
+ * complete verification list; the agent response table is a focus aid only.
39
+ * Used by buildAdvisorFollowUp (the normal flow) and the legacy path in
40
+ * messages.mjs (direct external callers of buildAdvisorUserMessage) — fixes
41
+ * must not be replicated in two places.
42
+ * @param {string} p — full prior review output (verbatim)
43
+ * @param {string} response — agent fix-claims table (or fallback text)
44
+ * @param {number} round — next round number (>= 2)
45
+ * @param {string[]|null} [scopeFiles] — review surface for instructions
46
+ * @returns {string} the convergence message
47
+ */
48
+ export function buildConvergenceBody(p, response, round, scopeFiles) {
49
+ const label = round === 2 ? "Verify Prior Table + Flag New Issues" : "Strict Verification"
50
+ const reminder = round === 2
51
+ ? "verify every item in the prior review output and flag only obvious new issues introduced by the fixes"
52
+ : "strictly verify only the prior review output — do NOT look for new issues"
53
+ const parts = [
54
+ `## Round ${round} — ${label}`,
55
+ "",
56
+ `[System reminder: this is round ${round} of the convergence protocol. ` +
57
+ `The system prompt for this round has already narrowed the review scope — follow it: ${reminder}.]`,
58
+ "",
59
+ // Prior review output IS in the context (decision 2026-08-08): the FULL
60
+ // verbatim output of the last review — the only complete verification list.
61
+ // The agent response table covers only issues the agent chose to answer,
62
+ // so issues the agent skipped would silently escape convergence without
63
+ // the prior output. The model understands the review output directly —
64
+ // no table/header/phrase parsing. Restatement risk is handled
65
+ // mechanically: host-verified citations reject references that do not
66
+ // match the CURRENT disk state, and fresh sessions exclude old read data.
67
+ // The agent response table stays as a focus aid ("I fixed X"), not as the
68
+ // to-verify list.
69
+ "## Prior Review Output (verify every item it raises)",
70
+ p,
71
+ "",
72
+ "## Agent Response (fix claims — reference only)",
73
+ response,
74
+ "",
75
+ "## Instructions",
76
+ ...buildConvergenceInstructions(round, scopeFiles),
77
+ "",
78
+ ]
79
+ return parts.join("\n")
80
+ }
@@ -1,16 +1,11 @@
1
1
  /**
2
- * advisor/history.mjs — advisor history extraction: issue/response tables and conversation background.
2
+ * advisor/history.mjs — advisor history extraction: agent response table and conversation background.
3
3
  */
4
4
  import { readFileSync } from "node:fs"
5
5
  import { join } from "node:path"
6
6
 
7
7
  export const ADVISOR_MD_PATH = ".thincoder/advisor.md"
8
- export const ADVISOR_TABLE_HEADER = "| # | File | Severity | Issue | Suggestion |"
9
- // Design-review table header (advisor-design.md round 1): | # | Category | Severity | Issue | Suggestion |
10
- const DESIGN_TABLE_HEADER = "| # | Category | Severity | Issue | Suggestion |"
11
- const CONVERGENCE_TABLE_HEADER = "| # | Orig# | File | Severity | Status | Notes |"
12
8
  const AGENT_RESPONSE_HEADER = "| # | Action | Detail |"
13
- export const LEGACY_ADVISOR_HEADER = "| # | 文件 | 严重程度 | 问题描述 | 建议修复 |"
14
9
 
15
10
  const DEFAULT_CRITERIA = `Review the code changes, focusing on:
16
11
  1. Correctness: logic errors, edge cases, off-by-one, incomplete modifications
@@ -20,77 +15,28 @@ const DEFAULT_CRITERIA = `Review the code changes, focusing on:
20
15
  5. Maintainability: vague naming, missing comments, overly complex logic`
21
16
 
22
17
  /**
23
- * Extract the most recent advisor review table from history.
24
- * Returns { text, sinceIdx } where sinceIdx is the index of the advisor call's
25
- * own history entry extractAgentResponseTable skips it (role is "tool", not
26
- * "assistant") and scans forward for the agent's response table.
27
- * Returns null when: no advisor call, empty output, or the last review is
28
- * all-clear (nothing to follow up on).
18
+ * Extract the agent's response table (| # | Action | Detail |) — the fix-claims
19
+ * reference for convergence rounds.
20
+ * Semantics (decision 2026-08-08): without sinceIdx, scan BACKWARD for the
21
+ * MOST RECENT response table (no prior-table index is carried anymore — the
22
+ * agent response is a focus aid only; format drift falls back to the
23
+ * no-response text and never drives control flow). With sinceIdx, scan
24
+ * FORWARD from it (legacy callers/tests).
25
+ * @param {Array} history — message history
26
+ * @param {number} [sinceIdx] — legacy: start scanning forward from this index
27
+ * @returns {string|null} the response table content, or null
29
28
  */
30
- // All-clear phrases the prompts instruct the advisor to use on a clean review.
31
- // Used ONLY by extractPriorIssueTable (issue-table verdict — a phrase-free
32
- // issue table with rows is never all-clear). The round-reset guard no longer
33
- // depends on model output at all: prepareAdvisorMessages decides by the
34
- // deterministic _mutatedThisRun flag (user decision 2026-08-05). "no new
35
- // issues" is DELIBERATELY absent — verification-table outputs (round 2+)
36
- // commonly conclude with it (round 3+ instructions even SAY "do not look for
37
- // new issues"); treating it as all-clear would reset the convergence budget
38
- // after every round-2 review (the observed "always round 2" bug: prior → null
39
- // → _advisorRound reset → 1→2→1…).
40
- const ALL_CLEAR_PHRASES = ["no 🔴", "all clear", "全部通过", "review passed", "no issues found", "everything is fine"]
41
-
42
- export function extractPriorIssueTable(history) {
43
- // Negative signals: a table listing SOME items as unfixed is NOT all-clear.
44
- // Applied when the message carries a Status column (English or Chinese convergence
45
- // format) — "failed"/"❌" in a round-1 Issue description must NOT trigger it.
46
- const partiallyFixedRe = /\bunfixed\b|未修复|❌|\bfailed\b/i
29
+ export function extractAgentResponseTable(history, sinceIdx) {
47
30
  const entries = Array.isArray(history) ? history : []
48
-
49
- for (let i = entries.length - 1; i >= 0; i--) {
50
- const m = entries[i]
51
- if (m.role !== "tool" || typeof m.content !== "string") continue
52
- // Only review outputs carry one of these table headers. Matched at LINE START:
53
- // an `includes()` match would also fire on advisor output that quotes the
54
- // header constants' own source code (e.g. history.mjs), producing a phantom
55
- // "prior issue table" and re-opening convergence rounds against stale data.
56
- if (!lineHasHeader(m.content, ADVISOR_TABLE_HEADER)
57
- && !lineHasHeader(m.content, DESIGN_TABLE_HEADER)
58
- && !lineHasHeader(m.content, CONVERGENCE_TABLE_HEADER)
59
- && !lineHasHeader(m.content, LEGACY_ADVISOR_HEADER)) continue
60
- const text = m.content
61
- const lower = text.toLowerCase()
62
- // Has a Status column? (English convergence format or any table with a Status-like
63
- // column) — Chinese legacy tables lack it, but they are issue tables, not convergence.
64
- const hasStatusColumn = lineHasHeader(text, CONVERGENCE_TABLE_HEADER)
65
- || /Status|状态/.test(text.slice(0, text.indexOf("\n") + 1))
66
- if (hasStatusColumn) {
67
- // Convergence/verification table: pass/fail is row-level — free-text
68
- // phrases like "no new issues found" / "review passed" are partial or
69
- // boilerplate statements in verification outputs. Some row unfixed →
70
- // convergence continues; every row fixed → all clear (fresh cycle).
71
- if (partiallyFixedRe.test(lower)) return { text, sinceIdx: i }
72
- return null
31
+ if (sinceIdx !== undefined) {
32
+ for (let i = sinceIdx; i < entries.length; i++) {
33
+ const m = entries[i]
34
+ if (m.role !== "assistant" || typeof m.content !== "string") continue
35
+ if (m.content.includes(AGENT_RESPONSE_HEADER)) return m.content
73
36
  }
74
- // Issue table (round 1 / design): phrase-based all-clear detection.
75
- if (ALL_CLEAR_PHRASES.some((s) => lower.includes(s))) return null
76
- // sinceIdx = the advisor call's own index; extractAgentResponseTable skips it (role !== assistant)
77
- return { text, sinceIdx: i }
37
+ return null
78
38
  }
79
- return null
80
- }
81
-
82
- /** True when some line of `text` starts with `header` — table headers always sit at line start. */
83
- function lineHasHeader(text, header) {
84
- return text.split("\n").some((l) => l.trimStart().startsWith(header))
85
- }
86
-
87
- /**
88
- * Extract the agent's response table (| # | Action | Detail |) that follows
89
- * the advisor review. Returns null when missing or no advisor review precedes.
90
- */
91
- export function extractAgentResponseTable(history, sinceIdx) {
92
- const entries = Array.isArray(history) ? history : []
93
- for (let i = sinceIdx ?? 0; i < entries.length; i++) {
39
+ for (let i = entries.length - 1; i >= 0; i--) {
94
40
  const m = entries[i]
95
41
  if (m.role !== "assistant" || typeof m.content !== "string") continue
96
42
  if (m.content.includes(AGENT_RESPONSE_HEADER)) return m.content
@@ -3,10 +3,105 @@
3
3
  * Split out of advisor.mjs to keep it under the 300-line advisory threshold
4
4
  * (.thincoder/advisor.md). System prompts live in advisor.mjs / prompts/.
5
5
  */
6
- import { readFileSync } from "node:fs"
7
- import { resolve, join, relative } from "node:path"
6
+ import { readFileSync, existsSync } from "node:fs"
7
+ import { resolve, join, relative, dirname, sep } from "node:path"
8
+ import { specForModel } from "../config.mjs"
8
9
  import { findReviewRepos, collectRepoSnapshots, collectChangedFiles } from "./repos.mjs"
9
- import { loadAdvisorMd, extractConversationBackground, extractAgentResponseTable, extractPriorIssueTable } from "./history.mjs"
10
+ import { buildConvergenceBody, buildConvergenceInstructions } from "./convergence.mjs"
11
+ import { loadAdvisorMd, extractConversationBackground, extractAgentResponseTable } from "./history.mjs"
12
+
13
+ /** Project guide (AGENTS.md) injection budget — decision 2026-08-08:
14
+ * NO fixed truncation; long-context models (1M+) get up to 5% of their context
15
+ * window for the doc map, small windows still get a floor so the map is always
16
+ * visible. The map is what tells the reviewer WHERE the requirements docs live
17
+ * (requirement-fit is judged against those docs, not the conversation only). */
18
+ const PROJECT_GUIDE_MIN = 8192 // chars — floor for small-window models
19
+ const PROJECT_GUIDE_FRACTION = 0.05 // 5% of the reviewer model's context window
20
+
21
+ /**
22
+ * Discover the project root for the review — user decision 2026-08-08:
23
+ * the project root is a SUBDIRECTORY of the working directory, never an
24
+ * ancestor above it. Priority:
25
+ * 1. Walk UP from each review-scope file's directory, bounded by cwd —
26
+ * the NEAREST AGENTS.md inside the workspace wins. In a monorepo this is
27
+ * the subproject's own doc map even when cwd itself has an AGENTS.md
28
+ * (a workspace-level meta map must not shadow the subproject guide).
29
+ * 2. No scope files / nothing found → cwd (single project; the walk's
30
+ * last step naturally lands on cwd's own AGENTS.md when it exists).
31
+ * @param {string} cwd — the agent's working directory (workspace root)
32
+ * @param {string[]} scopeFiles — cwd-relative review-scope paths (may be empty)
33
+ * @returns {string|null} absolute project root with an AGENTS.md, or null
34
+ */
35
+ function findProjectRoot(cwd, scopeFiles) {
36
+ // Normalize separators before comparing: input paths may use either
37
+ // convention (join() → "\\" on Windows; tool args / tests → "/"). Mixed
38
+ // styles made isInside(cwd + sep) miss legitimately nested paths.
39
+ const norm = (p) => p.replaceAll("\\", "/")
40
+ const isInside = (dir) => {
41
+ const d = norm(dir)
42
+ const c = norm(cwd)
43
+ return d === c || d.startsWith(c + "/")
44
+ }
45
+ for (const f of scopeFiles) {
46
+ let dir = dirname(resolve(cwd, f))
47
+ while (isInside(dir) && dir !== dirname(dir)) {
48
+ if (existsSync(join(dir, "AGENTS.md"))) return dir
49
+ dir = dirname(dir)
50
+ }
51
+ }
52
+ // No scope files, or none found in the walk — cwd itself (its AGENTS.md is
53
+ // checked as the walk's final step for scope files; for empty scopes, check
54
+ // it explicitly so a bare cwd project still gets its guide).
55
+ if (existsSync(join(cwd, "AGENTS.md"))) return cwd
56
+ return null
57
+ }
58
+
59
+ /**
60
+ * Inject the project guide (AGENTS.md) into the review message. AGENTS.md is the
61
+ * project's doc map — it defines the structure and where requirements/design
62
+ * documents live. The reviewer must see it FIRST: requirement-fit is judged
63
+ * against the documents it points to, with the conversation background as a
64
+ * supplement. Absent AGENTS.md degrades honestly (no pretending there is a map).
65
+ * @param {Object} agent — the parent agent
66
+ * @param {string[]} parts — message parts (mutated)
67
+ * @param {string[]} [scopeFiles] — cwd-relative review-scope paths for project-root discovery
68
+ * @returns {string|null} the discovered project root (abs), or null when no guide
69
+ */
70
+ function injectProjectGuide(agent, parts, scopeFiles = []) {
71
+ parts.push("## Project Guide (AGENTS.md)")
72
+ const root = findProjectRoot(agent.cwd, scopeFiles)
73
+ const path = root ? join(root, "AGENTS.md") : null
74
+ let text
75
+ if (!path) {
76
+ parts.push("(No AGENTS.md found — neither at the working directory root nor in any review-scope subdirectory. Judge the user's requirements from the conversation background, and say so explicitly if the requirements are unclear.)")
77
+ parts.push("")
78
+ return null // no guide — requirement-fit falls back to the conversation
79
+ }
80
+ try {
81
+ text = readFileSync(path, "utf8")
82
+ } catch (e) {
83
+ if (e.code !== "ENOENT") {
84
+ // File exists but is unreadable (EACCES etc.) — log, don't masquerade as "not found".
85
+ console.warn(`[advisor] AGENTS.md unreadable at ${path}: ${e.message}`)
86
+ }
87
+ parts.push("(No AGENTS.md found — neither at the working directory root nor in any review-scope subdirectory. Judge the user's requirements from the conversation background, and say so explicitly if the requirements are unclear.)")
88
+ parts.push("")
89
+ return null // no guide — requirement-fit falls back to the conversation
90
+ }
91
+ // readFileSync succeeded — compute the budget OUTSIDE the try so a spec
92
+ // lookup failure can never masquerade as "no AGENTS.md".
93
+ const ctx = specForModel(agent.provider?.model ?? "").context
94
+ const cap = Math.max(PROJECT_GUIDE_MIN, Math.floor(ctx * PROJECT_GUIDE_FRACTION))
95
+ const shown = text.length <= cap
96
+ ? text
97
+ : [...text].slice(0, cap).join("") + `\n\n…(truncated at ${cap} chars — read the full file if you need more)` // codepoint-safe slice: no broken surrogate pairs at the boundary
98
+ parts.push(`<!-- Project root: ${relative(agent.cwd, path).split(sep).join("/")} (inferred from the review scope under ${agent.cwd}) -->`)
99
+ parts.push("This file defines the project's structure and where its requirements/design documents live. Read the documents it points to — the user's requirements live THERE, not only in the conversation background.")
100
+ parts.push("")
101
+ parts.push(shown)
102
+ parts.push("")
103
+ return root // guide injected — requirement-fit criteria apply (truthy root)
104
+ }
10
105
 
11
106
  /**
12
107
  * Build the user message for an advisor review session.
@@ -21,12 +116,23 @@ import { loadAdvisorMd, extractConversationBackground, extractAgentResponseTable
21
116
  * @returns {string} the user message
22
117
  */
23
118
  export function buildAdvisorUserMessage(agent, prior, reviewType, designToken = null, documents = null, paths = null) {
24
- const p = prior ?? extractPriorIssueTable(agent.history)
119
+ // prior = the full prior review output (string) when a convergence round is
120
+ // being built (decision 2026-08-08 — verbatim injection, model understands it).
121
+ // Deterministic: only _advisorRound > 0 with stored output counts.
122
+ const p = prior ?? ((agent._advisorRound || 0) > 0 ? agent._lastAdvisorOutput : null)
25
123
 
26
124
  const parts = []
27
125
  const docList = Array.isArray(documents) ? documents.filter((d) => typeof d === "string" && d.trim()) : []
28
126
  const pathList = Array.isArray(paths) ? [...new Set(paths.filter((p) => typeof p === "string" && p.trim()))] : []
29
127
 
128
+ // Project guide FIRST in EVERY review path (code AND design round 0): the map
129
+ // to the requirements docs is needed for design reviews too (design must fit
130
+ // the requirements, not just the methodology). Design round 0 early-returns
131
+ // below — the guide must be injected before that return. Project root is
132
+ // discovered from the review scope (code paths AND design-doc paths, so a
133
+ // documents-only design review still finds the subproject guide).
134
+ const guideRoot = injectProjectGuide(agent, parts, [...pathList, ...docList])
135
+
30
136
  // Design review: simplified message — focus on the design doc, not code
31
137
  if (reviewType === "design" && (agent._advisorRound || 0) === 0) {
32
138
  const repos = findReviewRepos(agent)
@@ -66,10 +172,11 @@ export function buildAdvisorUserMessage(agent, prior, reviewType, designToken =
66
172
  }
67
173
  }
68
174
 
69
- // Engineering mode: inject project methodology
175
+ // Engineering mode: inject project methodology (resolved from the
176
+ // DISCOVERED project root — in a monorepo that is the subproject, not cwd)
70
177
  if (agent.config?.agent?.engineering) {
71
178
  try {
72
- const mpath = resolve(agent.cwd, "METHODOLOGY.md")
179
+ const mpath = resolve(guideRoot ?? agent.cwd, "METHODOLOGY.md")
73
180
  const methodology = readFileSync(mpath, "utf8")
74
181
  parts.push("## Project Methodology")
75
182
  parts.push("Evaluate the design against this methodology:")
@@ -85,8 +192,9 @@ export function buildAdvisorUserMessage(agent, prior, reviewType, designToken =
85
192
  parts.push("1. Read the design document fully. Read METHODOLOGY.md to understand the project's standards.")
86
193
  }
87
194
  parts.push("2. Review against: completeness (all requirements covered?), feasibility (can this be built?), clarity (specific enough?), acceptance criteria (verifiable?), scope (appropriate?).")
88
- parts.push("3. Do NOT run git diff or look for code changes there are none at this stage.")
89
- parts.push("4. If you find issues, produce your review table with the format: | # | Category | Severity | Issue | Suggestion |. If the design passes, no table is needed.")
195
+ parts.push("3. If the ## Project Guide (AGENTS.md) section above is present, also check requirement fit: does the design match what the requirements documents it points to actually ask for?")
196
+ parts.push("4. Do NOT run git diff or look for code changes there are none at this stage.")
197
+ parts.push("5. If you find issues, produce your review table with the format: | # | Category | Severity | Issue | Suggestion |. If the design passes, no table is needed.")
90
198
  if (designToken) {
91
199
  parts.push("")
92
200
  parts.push("## Approval Signal")
@@ -99,24 +207,22 @@ export function buildAdvisorUserMessage(agent, prior, reviewType, designToken =
99
207
  // Convergence data (round 2+). LEGACY COMPATIBILITY PATH: the normal advisor
100
208
  // flow routes convergence rounds through buildAdvisorFollowUp (fresh session,
101
209
  // decision d698434); this block only fires for direct external callers of
102
- // buildAdvisorUserMessage with a prior table. Kept to avoid breaking those.
103
- // Same rule as buildAdvisorFollowUp: prior table IS injected (decision
104
- // 2026-08-05, reversed) it is the only complete verification list.
210
+ // buildAdvisorUserMessage with a stored prior review output. Kept to avoid
211
+ // breaking those. Same rule as buildAdvisorFollowUp: the FULL prior review
212
+ // output is injected verbatim (decision 2026-08-08 — the model understands it;
213
+ // no table/header/phrase parsing).
214
+ // NOTE: this legacy path does NOT apply escapeLiteralEscapes (that lives in
215
+ // advisor.mjs and importing it here would create a top-level module cycle).
216
+ // Direct callers must escape the injected output themselves if the parent
217
+ // conversation can quote literal "\x"/"\u" sequences (server 400 risk).
105
218
  if (p && (agent._advisorRound || 0) > 0) {
106
219
  const scopeFiles = resolveScopeFiles(agent, paths)
107
- const response = extractAgentResponseTable(agent.history, p.sinceIdx)
220
+ const response = extractAgentResponseTable(agent.history)
108
221
  || (scopeFiles?.length
109
222
  ? "(Agent did not provide a response table — perform a fresh review of: " + scopeFiles.slice(0, 10).join(", ") + ")"
110
223
  : "(Agent did not provide a response table — perform a fresh review of the files named in the system prompt context)")
111
224
  const round = (agent._advisorRound || 0) + 1
112
- const label = round === 2 ? "Verify Prior Table + Flag New Issues" : "Strict Verification"
113
- parts.push(`## Round ${round} — ${label}`)
114
- parts.push("")
115
- parts.push("## Prior Issue Table (verify every item)")
116
- parts.push(p.text)
117
- parts.push("")
118
- parts.push("## Agent Response (fix claims — reference only)")
119
- parts.push(response)
225
+ parts.push(buildConvergenceBody(p, response, round, scopeFiles))
120
226
  parts.push("")
121
227
  parts.push("---")
122
228
  parts.push("")
@@ -155,12 +261,19 @@ export function buildAdvisorUserMessage(agent, prior, reviewType, designToken =
155
261
  const criteria = loadAdvisorMd(agent.cwd)
156
262
  parts.push("## Review Criteria")
157
263
  parts.push(criteria)
264
+ if (guideRoot) {
265
+ // Requirement-fit is a first-class dimension when the project guide was
266
+ // found — the criteria file (advisor.md) may not mention it (legacy).
267
+ parts.push("")
268
+ parts.push("Additional criterion: **requirement fit** — does the implementation match what the requirements documents (referenced by the Project Guide above) actually ask for?")
269
+ }
158
270
  parts.push("")
159
271
 
160
272
  // Engineering mode: inject project methodology so advisor knows the rules
273
+ // (resolved from the DISCOVERED project root — subproject in a monorepo)
161
274
  if (agent.config?.agent?.engineering) {
162
275
  try {
163
- const mpath = resolve(agent.cwd, "METHODOLOGY.md")
276
+ const mpath = resolve(guideRoot ?? agent.cwd, "METHODOLOGY.md")
164
277
  const methodology = readFileSync(mpath, "utf8")
165
278
  parts.push("## Project Methodology (Engineering Mode)")
166
279
  parts.push("The project follows this methodology. Evaluate the changes against it:")
@@ -169,7 +282,9 @@ export function buildAdvisorUserMessage(agent, prior, reviewType, designToken =
169
282
  } catch { /* file doesn't exist — skip */ }
170
283
  }
171
284
 
172
- // Instructions — round-aware: re-reviews skip convention discovery entirely
285
+ // Instructions — round-aware: re-reviews skip convention discovery entirely.
286
+ // These are SUPPLEMENTARY reminders to the system prompt's numbered workflow —
287
+ // deliberately not renumbered as a competing sequence.
173
288
  const isReReview = p && (agent._advisorRound || 0) > 0
174
289
  parts.push("## Instructions")
175
290
  parts.push("1. IMPORTANT: the review scope lists the files under review — always verify current file state with `read` before judging. Never decide based on earlier snapshots alone.")
@@ -177,7 +292,9 @@ export function buildAdvisorUserMessage(agent, prior, reviewType, designToken =
177
292
  const round = (agent._advisorRound || 0) + 1
178
293
  parts.push(...buildConvergenceInstructions(round, pathList))
179
294
  } else {
180
- parts.push("2. Read `AGENTS.md` / design docs only if they exist (check once; do not re-probe with multiple patterns).")
295
+ parts.push("2. " + (guideRoot
296
+ ? "The `## Project Guide (AGENTS.md)` section above maps the project — read the requirements/design documents it points to (they are the primary reference for requirement-fit). Use `read` to load those documents."
297
+ : "No AGENTS.md was found at the project root — rely on the conversation background for the user's requirements. If the requirements are unclear, state so explicitly."))
181
298
  parts.push("3. `read` the files in the Review Scope in full — they define exactly what to inspect. Batch independent reads/greps in a single reply instead of one call per round-trip.")
182
299
  parts.push("4. Use `grep` or `lsp` to trace callers, imports, and dependencies — only where the diff leaves genuine doubt.")
183
300
  parts.push("5. Produce your review table based on the review criteria above. Do not re-read content you already have.")
@@ -198,7 +315,8 @@ export function buildAdvisorUserMessage(agent, prior, reviewType, designToken =
198
315
  */
199
316
  export function resolveScopeFiles(agent, paths) {
200
317
  const normalize = (p) => {
201
- const abs = p.startsWith(agent.cwd) ? p : join(agent.cwd, p)
318
+ // sep-guarded prefix check /proj vs /project-other must not collide
319
+ const abs = p === agent.cwd || p.startsWith(agent.cwd + sep) ? p : join(agent.cwd, p)
202
320
  return relative(agent.cwd, abs)
203
321
  }
204
322
  if (Array.isArray(paths)) return [...new Set(paths.map(normalize))]
@@ -208,29 +326,3 @@ export function resolveScopeFiles(agent, paths) {
208
326
  return null
209
327
  }
210
328
 
211
- /**
212
- * Shared convergence-round instructions — single source for BOTH paths
213
- * (buildAdvisorUserMessage's legacy convergence block and
214
- * buildAdvisorFollowUp), so the wording cannot diverge.
215
- * Round 2 may flag obvious new issues; round 3+ is strict verification.
216
- * @param {number} round — convergence round number (2+)
217
- * @param {string[]|null} scopeFiles — optional file list for the no-response fallback
218
- * @returns {string[]} the numbered instruction lines (callers spread them)
219
- */
220
- export function buildConvergenceInstructions(round, scopeFiles = null) {
221
- const fileList = scopeFiles?.length
222
- ? ` The review surface is: ${scopeFiles.slice(0, 10).join(", ")}.`
223
- : ""
224
- return [
225
- `1. IMPORTANT: verify EVERY item of the prior issue table against the CURRENT FILE STATE with \`read\` — never decide based on earlier snapshots alone.${fileList}`,
226
- "2. STALE-CONTEXT WARNING: any diff or file content from earlier messages is a historical snapshot — treat it as expired. Only fresh `read` results describe the current state.",
227
- "3. You have no git tool; git output in earlier messages is historical and untrustworthy (committed fixes never show in a diff).",
228
- "4. `read` the files named in the prior table (or the review surface above) in full — ALWAYS. Batch reads/greps in a single reply.",
229
- "5. Evidence rule: every 'Unfixed'/'New' finding 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 be stale or fabricated. Findings without a fresh quoted line are treated as unverified and will not be accepted.",
230
- "6. Produce your verification table. Do not re-read content you already have.",
231
- round === 2
232
- ? "7. You may flag obvious NEW issues introduced by the fixes (crashes, data loss, logic errors — not style)."
233
- : "7. Do NOT look for new issues.",
234
- ]
235
- }
236
-
@@ -99,6 +99,15 @@ export function collectChangedFiles(repos, cwd) {
99
99
 
100
100
  const DOC_FILE = /(?:^|[/\\])(?:LICENSE|NOTICE|CHANGELOG|AUTHORS)(?:\.\w+)?$|\.(?:md|markdown|mdx|txt|rst|adoc)$/i
101
101
 
102
+ /** Temporary/scratch files that must NOT count as code mutations: tmp-* named
103
+ * scratch scripts (tmp-c1.mjs, tmp-check.mjs…) and .tmp/.temp extensions.
104
+ * The advisor/verify guards skip these — a throwaway diagnostic script is not
105
+ * a code change, and writing one must not push the agent into a review loop.
106
+ * NOTE: matches the tmp-* basename at ANY directory depth, not just root —
107
+ * intentional: _touchedFiles stores absolute paths, so the pattern must work
108
+ * for "D:/proj/tmp-check.mjs" as well as a bare "tmp-check.mjs". */
109
+ const TEMP_FILE = /(?:^|[/\\])tmp-[^/\\]+$|\.(?:tmp|temp)$/i
110
+
102
111
  /** True when a path matches the doc/license pattern by extension or name.
103
112
  * NOTE: this is extension-based only — it does NOT exclude src/ paths.
104
113
  * Callers must separately check the src/ prefix for product-code semantics
@@ -108,6 +117,35 @@ export function isDocFile(p) {
108
117
  return DOC_FILE.test(p ?? "")
109
118
  }
110
119
 
120
+ /** True when a path is a throwaway temp file (tmp-* name or .tmp/.temp ext).
121
+ * Excluded from code-mutation detection so scratch scripts don't trigger
122
+ * advisor/verify guards. */
123
+ export function isTempFile(p) {
124
+ return TEMP_FILE.test(p ?? "")
125
+ }
126
+
127
+ /**
128
+ * True when this run mutated at least one CODE file. Shared single source of
129
+ * truth for the advisor/verify guards (agent.mjs + agent/completion.mjs).
130
+ * Doc-only changes (docs/, *.md, LICENSE…) must NOT trigger the guards — the
131
+ * design phase edits docs/ and must not be pushed to a code review. Temp/scratch
132
+ * files (tmp-*, .tmp, .temp) are excluded too — a throwaway diagnostic script
133
+ * is not a code change. Mutations without a known path (tools outside
134
+ * FILE_MUTATORS) are treated as code — cannot tell, so guard conservatively.
135
+ * Product-code semantics: anything under src/ (incl. src/prompts/*.md) is code;
136
+ * anything else that isn't a doc or temp file is code.
137
+ * NOTE: _touchedFiles stores ABSOLUTE paths (join(cwd, p)), so the src/ check
138
+ * matches a path component (works for "src/..." and "D:\...\src\..." alike),
139
+ * not a bare ^src prefix — the literal ^src[\\/] form would be dead code here.
140
+ */
141
+ export function hasCodeMutations({ _touchedFiles, _mutatedThisRun }) {
142
+ const files = _touchedFiles ?? []
143
+ if (files.length === 0) return _mutatedThisRun
144
+ // src/ is unconditional — anything under src/ is code regardless of its name
145
+ // (incl. src/tmp-*.mjs). Temp/doc exclusions apply only outside src/.
146
+ return files.some((p) => /(?:^|[\\/])src[\\/]/.test(p) || (!isTempFile(p) && !isDocFile(p)))
147
+ }
148
+
111
149
  /** True when all changed files across repos are documentation (md/txt/LICENSE etc.).
112
150
  * Anything under src/ (incl. src/prompts/*.md) counts as product code —
113
151
  * isProductCode semantics, consistent with the design gate. */
@@ -126,7 +164,9 @@ export function isDocOnlyChange(repos, cwd) {
126
164
  for (const line of status.split("\n")) {
127
165
  // porcelain: "XY path" or "XY old -> new" (rename)
128
166
  const filePath = line.slice(3).split(" -> ").pop().replace(/^"|"$/g, "")
167
+ // src/ is unconditional product code (even src/tmp-*.mjs) — check before the temp skip
129
168
  if (/^src[\\/]/.test(filePath) || !DOC_FILE.test(filePath)) return false
169
+ if (isTempFile(filePath)) continue
130
170
  }
131
171
  }
132
172
  return sawChanges
@@ -6,7 +6,6 @@ import { chat } from "../provider/core.mjs"
6
6
  import { findProvider, specForModel } from "../config.mjs"
7
7
  import { toOpenAISchema } from "../tools/index.mjs"
8
8
  import { prepareAdvisorMessages } from "../advisor.mjs"
9
- import { extractPriorIssueTable } from "../advisor/history.mjs"
10
9
  import { appendCitationReport } from "./citations.mjs"
11
10
 
12
11
  const MAX_ADVISOR_TURNS = 100
@@ -376,9 +375,10 @@ export async function runAdvisorReview(agent, reviewType, callbacks, designToken
376
375
  // blocks the next call. 5 rounds max; after that the review is never pushed back
377
376
  // (the caller decides: accept, manual re-check, or /new to reset).
378
377
  if ((agent._advisorRound || 0) >= MAX_ADVISOR_ROUNDS) {
379
- // 提取未解决的问题,给出更具体的指导
380
- const prior = extractPriorIssueTable(agent.history)
381
- const unfixed = prior ? extractUnfixedIssues(prior.text) : []
378
+ // Summarize unresolved items from the last review output for guidance
379
+ // (line-level status-word scan — no table-header parsing, decision 2026-08-08).
380
+ const prior = agent._lastAdvisorOutput
381
+ const unfixed = prior ? extractUnfixedIssues(prior) : []
382
382
 
383
383
  let message = `Advisor: convergence cap reached after ${MAX_ADVISOR_ROUNDS} rounds.\n`
384
384
  if (unfixed.length > 0) {
@@ -409,6 +409,18 @@ export async function runAdvisorReview(agent, reviewType, callbacks, designToken
409
409
  let final = result
410
410
  if (!result.trimStart().startsWith("Advisor:")) {
411
411
  final = appendCitationReport(result, advisorCwd)
412
+ // Success path: keep the FULL review output for convergence rounds —
413
+ // round 2+ injects this verbatim and the model understands it (decision
414
+ // 2026-08-08: prior-table hard parsing removed; no phrase/header matching).
415
+ // Guard: only store outputs that actually carry a review — a markdown
416
+ // table row (`| a | b | c |`) or substantial prose (>200 chars). An
417
+ // empty or tool-progress-only reply must not become the "prior review"
418
+ // of round 2+.
419
+ const trimmed = final.trim()
420
+ const looksLikeReview = /\|.*\|.*\|/.test(trimmed) || trimmed.length >= 200
421
+ if (looksLikeReview) {
422
+ agent._lastAdvisorOutput = final
423
+ }
412
424
  }
413
425
 
414
426
  // Log review statistics for observability