thincoder 0.12.12 → 0.12.13
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 +1 -1
- package/src/advisor/convergence.mjs +80 -0
- package/src/advisor/history.mjs +19 -73
- package/src/advisor/messages.mjs +142 -50
- package/src/advisor/repos.mjs +40 -0
- package/src/advisor/run.mjs +16 -4
- package/src/advisor.mjs +31 -60
- package/src/agent/completion.mjs +1 -8
- package/src/agent.mjs +20 -27
- package/src/config.mjs +13 -5
- package/src/context.mjs +32 -9
- package/src/prompts/advisor-round1.md +3 -3
- package/src/prompts/advisor-round2.md +6 -6
- package/src/prompts/advisor-round3.md +11 -13
package/package.json
CHANGED
|
@@ -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
|
+
}
|
package/src/advisor/history.mjs
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* advisor/history.mjs — advisor history extraction:
|
|
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
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/src/advisor/messages.mjs
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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.
|
|
89
|
-
parts.push("4.
|
|
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
|
|
103
|
-
// Same rule as buildAdvisorFollowUp:
|
|
104
|
-
// 2026-08-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
package/src/advisor/repos.mjs
CHANGED
|
@@ -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
|
package/src/advisor/run.mjs
CHANGED
|
@@ -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
|
-
|
|
381
|
-
const
|
|
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
|
package/src/advisor.mjs
CHANGED
|
@@ -44,10 +44,11 @@
|
|
|
44
44
|
import { readFileSync } from "node:fs"
|
|
45
45
|
import { join, dirname } from "node:path"
|
|
46
46
|
import { fileURLToPath } from "node:url"
|
|
47
|
-
import {
|
|
48
|
-
import { buildAdvisorUserMessage,
|
|
47
|
+
import { extractAgentResponseTable } from "./advisor/history.mjs"
|
|
48
|
+
import { buildAdvisorUserMessage, resolveScopeFiles } from "./advisor/messages.mjs"
|
|
49
|
+
import { buildConvergenceBody } from "./advisor/convergence.mjs"
|
|
49
50
|
// Re-export for run.mjs and tests (keeps their imports from "../advisor.mjs" stable)
|
|
50
|
-
export { ADVISOR_MD_PATH,
|
|
51
|
+
export { ADVISOR_MD_PATH, extractAgentResponseTable, extractConversationBackground } from "./advisor/history.mjs"
|
|
51
52
|
export { buildAdvisorUserMessage } from "./advisor/messages.mjs"
|
|
52
53
|
|
|
53
54
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
@@ -72,8 +73,7 @@ const ADVISOR_ROUND1 = loadPrompt("advisor-round1.md", "advisor-round1.md")
|
|
|
72
73
|
const ADVISOR_ROUND2 = loadPrompt("advisor-round2.md", "advisor-round2.md")
|
|
73
74
|
const ADVISOR_ROUND3 = loadPrompt("advisor-round3.md", "advisor-round3.md")
|
|
74
75
|
// Fallback when advisor-design.md is missing — keep in sync with the real
|
|
75
|
-
// file (table format + workflow steps
|
|
76
|
-
// extractPriorIssueTable's DESIGN_TABLE_HEADER matching).
|
|
76
|
+
// file (table format + workflow steps).
|
|
77
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.`
|
|
78
78
|
let ADVISOR_DESIGN = ""
|
|
79
79
|
// Design review is OPTIONAL (engineering mode only) — silent fallback to the
|
|
@@ -88,24 +88,28 @@ try { ADVISOR_DESIGN = readFileSync(join(__dirname, "prompts", "advisor-design.m
|
|
|
88
88
|
/**
|
|
89
89
|
* Build the system prompt for an advisor review session.
|
|
90
90
|
* @param {Object} agent — the parent agent
|
|
91
|
-
* @param {Object|null} [prior] — prior
|
|
91
|
+
* @param {Object|null} [prior] — prior review output (full text; decision 2026-08-08)
|
|
92
92
|
* @param {string} [reviewType] — "design" for design review, undefined/"code" for code review
|
|
93
93
|
* @returns {string} the system prompt
|
|
94
94
|
*/
|
|
95
95
|
export function buildAdvisorSystemPrompt(agent, prior, reviewType) {
|
|
96
|
+
// Round decision is DETERMINISTIC (decision 2026-08-08): _advisorRound > 0
|
|
97
|
+
// with a stored review output means convergence (round 2+); 0 means round 1.
|
|
98
|
+
// No prior-table parsing, no all-clear phrase matching — the round counter
|
|
99
|
+
// and the stored output are the only inputs. A restarted process has
|
|
100
|
+
// _advisorRound 0 → conservative full re-review.
|
|
101
|
+
const hasPrior = (agent._advisorRound || 0) > 0 && (prior ?? agent._lastAdvisorOutput)
|
|
96
102
|
// Design review: round 1 uses the dedicated design-review prompt (full scope +
|
|
97
103
|
// approval token); rounds 2+ converge like code reviews (verify agent fix claims).
|
|
98
104
|
if (reviewType === "design") {
|
|
99
|
-
|
|
100
|
-
if (!p || (agent._advisorRound || 0) === 0) {
|
|
105
|
+
if (!hasPrior) {
|
|
101
106
|
return ADVISOR_DESIGN || ADVISOR_DESIGN_FALLBACK
|
|
102
107
|
}
|
|
103
108
|
const round = (agent._advisorRound || 0) + 1
|
|
104
109
|
if (round === 2) return ADVISOR_ROUND2
|
|
105
110
|
return ADVISOR_ROUND3
|
|
106
111
|
}
|
|
107
|
-
|
|
108
|
-
if (!p || (agent._advisorRound || 0) === 0) return ADVISOR_ROUND1
|
|
112
|
+
if (!hasPrior) return ADVISOR_ROUND1
|
|
109
113
|
const round = (agent._advisorRound || 0) + 1
|
|
110
114
|
if (round === 2) return ADVISOR_ROUND2
|
|
111
115
|
return ADVISOR_ROUND3
|
|
@@ -134,12 +138,11 @@ export function buildAdvisorSystemPrompt(agent, prior, reviewType) {
|
|
|
134
138
|
* otherwise scan history from index 0 and could match an unrelated stale table)
|
|
135
139
|
*/
|
|
136
140
|
export function buildAdvisorFollowUp(agent, prior, scopeFiles = null) {
|
|
137
|
-
// Convergence follow-up REQUIRES a prior review record
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
const p = prior ?? extractPriorIssueTable(agent.history)
|
|
141
|
+
// Convergence follow-up REQUIRES a prior review record — the full output of
|
|
142
|
+
// the last review, injected VERBATIM (decision 2026-08-08: the model
|
|
143
|
+
// understands the review output; no table/header/phrase parsing). The caller
|
|
144
|
+
// usually passes it; fall back to the stored agent._lastAdvisorOutput.
|
|
145
|
+
const p = prior ?? agent._lastAdvisorOutput
|
|
143
146
|
if (!p) {
|
|
144
147
|
// Plain "System reminder:" prefix (no brackets) — same convention as the
|
|
145
148
|
// round-1 path (some OpenAI-compatible servers parse '['-prefixed content
|
|
@@ -155,38 +158,9 @@ export function buildAdvisorFollowUp(agent, prior, scopeFiles = null) {
|
|
|
155
158
|
const noResponseFallback = scopeFiles?.length
|
|
156
159
|
? "(Agent did not provide a response table — perform a fresh review of: " + scopeFiles.slice(0, 10).join(", ") + ")"
|
|
157
160
|
: "(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
|
|
161
|
+
const response = extractAgentResponseTable(agent.history) || noResponseFallback
|
|
159
162
|
const round = (agent._advisorRound || 0) + 1
|
|
160
|
-
|
|
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"
|
|
165
|
-
const parts = [
|
|
166
|
-
`## Round ${round} — ${label}`,
|
|
167
|
-
"",
|
|
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}.]`,
|
|
170
|
-
"",
|
|
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,
|
|
181
|
-
"",
|
|
182
|
-
"## Agent Response (fix claims — reference only)",
|
|
183
|
-
response,
|
|
184
|
-
"",
|
|
185
|
-
"## Instructions",
|
|
186
|
-
...buildConvergenceInstructions(round, scopeFiles),
|
|
187
|
-
"",
|
|
188
|
-
]
|
|
189
|
-
return parts.join("\n")
|
|
163
|
+
return buildConvergenceBody(p, response, round, scopeFiles)
|
|
190
164
|
}
|
|
191
165
|
|
|
192
166
|
/**
|
|
@@ -229,11 +203,15 @@ export function escapeLiteralEscapes(text) {
|
|
|
229
203
|
* @param {string[]|null} [documents] — design review only: explicit list of doc paths to review (passed through to buildAdvisorUserMessage)
|
|
230
204
|
* @param {string[]|null} [paths] — code review only: explicit list of file/dir paths to review
|
|
231
205
|
*/
|
|
232
|
-
export function prepareAdvisorMessages(agent, reviewType, designToken = null, documents = null, paths = null) {
|
|
233
|
-
|
|
206
|
+
export function prepareAdvisorMessages(agent, reviewType, designToken = null, documents = null, paths = null, priorParam = null) {
|
|
207
|
+
// Deterministic convergence state (decision 2026-08-08): round 2+ requires
|
|
208
|
+
// _advisorRound > 0 AND a stored prior review output. No history parsing.
|
|
209
|
+
// priorParam (direct callers) wins over the stored output — same derivation
|
|
210
|
+
// as buildAdvisorSystemPrompt (single source of truth for round semantics).
|
|
211
|
+
const prior = (agent._advisorRound || 0) > 0 ? (priorParam ?? agent._lastAdvisorOutput) : null
|
|
234
212
|
|
|
235
213
|
// Design review round 1: the dedicated full-scope review with the approval
|
|
236
|
-
// token (an independent gate — it runs even when a prior
|
|
214
|
+
// token (an independent gate — it runs even when a prior review exists, e.g.
|
|
237
215
|
// after a failed design review). Fresh session.
|
|
238
216
|
if (reviewType === "design" && (agent._advisorRound || 0) === 0) {
|
|
239
217
|
return [
|
|
@@ -248,21 +226,14 @@ export function prepareAdvisorMessages(agent, reviewType, designToken = null, do
|
|
|
248
226
|
// re-reading) and a token sink. The agent response table (fix claims) is
|
|
249
227
|
// injected through buildAdvisorFollowUp instead; the system prompt carries
|
|
250
228
|
// the round (ROUND2/ROUND3) via buildAdvisorSystemPrompt.
|
|
251
|
-
//
|
|
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
|
|
229
|
+
// No prior review output: reset ONLY when this run made no code changes (user
|
|
259
230
|
// decision 2026-08-05: any loop that modified code must NOT reset — the
|
|
260
231
|
// advisor guard WILL push back, so the convergence round must keep advancing
|
|
261
232
|
// toward the cap; a run with no mutations has no push-back risk and a reset
|
|
262
233
|
// is safe). Deterministic runtime state (`_mutatedThisRun`) decides — never
|
|
263
234
|
// model output (phrases/table headers drift; three rounds of false reports
|
|
264
|
-
// proved it). Either way the message is a fresh full review (no
|
|
265
|
-
// exists without a
|
|
235
|
+
// proved it). Either way the message is a fresh full review (no prior output
|
|
236
|
+
// exists without a completed review) — only the round counter differs.
|
|
266
237
|
if (!prior || (agent._advisorRound || 0) === 0) {
|
|
267
238
|
if (!(agent._mutatedThisRun ?? false)) {
|
|
268
239
|
// New review cycle (first review, all-clear, or no code changes): reset
|
package/src/agent/completion.mjs
CHANGED
|
@@ -4,17 +4,10 @@
|
|
|
4
4
|
* Checks: pending tasks, verify guard, advisor guard.
|
|
5
5
|
* Returns { action: 'continue' | 'done', content?, guardPushbacks, honestReminderInjected, advisorPushbacks }
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
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
10
|
|
|
11
|
-
/** True when this run mutated at least one CODE file. Mirrors agent.mjs:hasCodeMutations. */
|
|
12
|
-
function hasCodeMutations(agent) {
|
|
13
|
-
const files = agent._touchedFiles ?? []
|
|
14
|
-
if (files.length === 0) return agent._mutatedThisRun
|
|
15
|
-
return files.some((p) => /(?:^|[\\/])src[\\/]/.test(p) || !isDocFile(p))
|
|
16
|
-
}
|
|
17
|
-
|
|
18
11
|
const MAX_VERIFY_PUSHBACKS = 2
|
|
19
12
|
const MAX_VERIFY_RETRIES = 3
|
|
20
13
|
const MAX_ADVISOR_PUSHBACKS = 3
|
package/src/agent.mjs
CHANGED
|
@@ -13,7 +13,6 @@ import { executeToolCalls } from "./agent/dispatch.mjs"
|
|
|
13
13
|
import { prepareRun } from "./agent/setup.mjs"
|
|
14
14
|
import { injectPostTurn, STALL_WINDOW_SIZE, STALL_THRESHOLD, GOAL_BUDGET_WARN_RATIO } from "./agent/post-turn.mjs"
|
|
15
15
|
import { handleCompletion } from "./agent/completion.mjs"
|
|
16
|
-
import { isDocFile } from "./advisor/repos.mjs"
|
|
17
16
|
import {
|
|
18
17
|
escapeXml, tryCanonicalize, repairHistory, listWorkDir,
|
|
19
18
|
readonlyToolNames, collectGitContext, loadProjectInstructions,
|
|
@@ -56,23 +55,8 @@ export const ENG_ON_REMINDER =
|
|
|
56
55
|
"subagents only. Advisor calls are NOT per-turn-mandatory — call only at " +
|
|
57
56
|
"flow nodes or when the user asks.]"
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
* (docs/, *.md, LICENSE…) must NOT trigger the advisor/verify guards — the
|
|
62
|
-
* design phase edits docs/ and must not be pushed to a code review.
|
|
63
|
-
* Mutations without a known path (tools outside FILE_MUTATORS) are treated as
|
|
64
|
-
* code — cannot tell, so guard conservatively.
|
|
65
|
-
* Product-code semantics match isProductCode: anything under src/ (incl.
|
|
66
|
-
* src/prompts/*.md) is code; anything else that isn't a doc file is code.
|
|
67
|
-
* NOTE: _touchedFiles stores ABSOLUTE paths (join(cwd, p)), so the src/ check
|
|
68
|
-
* matches a path component (works for "src/..." and "D:\...\src\..." alike),
|
|
69
|
-
* not a bare ^src prefix — the literal ^src[\\/] form would be dead code here.
|
|
70
|
-
*/
|
|
71
|
-
export function hasCodeMutations(agent) {
|
|
72
|
-
const files = agent._touchedFiles ?? []
|
|
73
|
-
if (files.length === 0) return agent._mutatedThisRun
|
|
74
|
-
return files.some((p) => /(?:^|[\\/])src[\\/]/.test(p) || !isDocFile(p))
|
|
75
|
-
}
|
|
58
|
+
// Re-exported for API compatibility (single source of truth: advisor/repos.mjs)
|
|
59
|
+
export { hasCodeMutations } from "./advisor/repos.mjs"
|
|
76
60
|
|
|
77
61
|
/** Engineering-mode status injection — one reminder when engineering mode is ON. */
|
|
78
62
|
function injectEngineeringReminder(agent) {
|
|
@@ -101,6 +85,7 @@ export function createAgent({
|
|
|
101
85
|
_engDesignReviewed: false, // eng-coder: design review gate passed (hard gate in dispatch.mjs)
|
|
102
86
|
_engDesignToken: null, // issued by advisor(type="design"); required to spawn eng-coder
|
|
103
87
|
_touchedFiles: [], _verifyRetries: 0, _advisorRound: 0, _advisorSession: null,
|
|
88
|
+
_lastAdvisorOutput: null, // full review output from the most recent advisor call (convergence rounds inject it verbatim)
|
|
104
89
|
_lastEngState: false,
|
|
105
90
|
_pendingReminders: [],
|
|
106
91
|
_pendingTimers: [],
|
|
@@ -165,7 +150,7 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
165
150
|
const lastRole = agent.history.at(-1)?.role
|
|
166
151
|
if (lastRole === "user" || lastRole === "tool") {
|
|
167
152
|
try {
|
|
168
|
-
if (await compressIfNeeded(agent, threshold, callbacks, compactionOverhead)) {
|
|
153
|
+
if (await compressIfNeeded(agent, threshold, callbacks, compactionOverhead, signal)) {
|
|
169
154
|
agent._compressFailures = 0
|
|
170
155
|
agent._planReminderAtLen = 0 // After compression history shrinks, reset cadence so reminders resume
|
|
171
156
|
recentCallSigs.length = 0 // After compression history is rebuilt, reset stall detection counter
|
|
@@ -220,8 +205,8 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
220
205
|
await classifyAndApply(agent, turn).catch(() => {})
|
|
221
206
|
}
|
|
222
207
|
|
|
223
|
-
|
|
224
|
-
response = await chat(agent.provider, {
|
|
208
|
+
if (process.env.ADVISOR_DEBUG) console.error("[chat-call]", JSON.stringify({ turn, histLen: agent.history.length, lastRole: agent.history.at(-1)?.role }))
|
|
209
|
+
try { response = await chat(agent.provider, {
|
|
225
210
|
messages, tools: toolSchemas,
|
|
226
211
|
onToken: callbacks.onToken,
|
|
227
212
|
onReasoning: callbacks.onReasoning,
|
|
@@ -389,13 +374,21 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
|
|
|
389
374
|
pushReal(agent, { role: "tool", tool_call_id: toolCall.id, content: result })
|
|
390
375
|
if (tool && ok) {
|
|
391
376
|
if (FILE_MUTATORS.has(toolCall.name)) {
|
|
392
|
-
// Direct file edit — code was changed.
|
|
377
|
+
// Direct file edit — code was changed. The prior advisor review and
|
|
378
|
+
// verify are stale: a review that ran before the edit no longer
|
|
379
|
+
// covers the current file state.
|
|
393
380
|
agent._mutatedThisRun = true
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
381
|
+
agent._calledAdvisorThisRun = false
|
|
382
|
+
agent._verifiedThisRun = false
|
|
383
|
+
agent._verifyPassed = undefined
|
|
384
|
+
} else if (!tool.readonly && !tool.sideEffectExempt) {
|
|
385
|
+
// Non-mutating side-effect tools (bash, git): do NOT invalidate the
|
|
386
|
+
// advisor review — a review is triggered by CODE MUTATIONS only
|
|
387
|
+
// (user decision 2026-08-08: the guard rule is "review after code
|
|
388
|
+
// changes", not "review after any environment change"; bash is
|
|
389
|
+
// barred from writing files, so it cannot change the reviewed code).
|
|
390
|
+
// Verify IS invalidated: its state snapshot (git diff, file list)
|
|
391
|
+
// may be stale after git/shell operations.
|
|
399
392
|
if (agent._verifiedThisRun) {
|
|
400
393
|
agent._verifiedThisRun = false
|
|
401
394
|
agent._verifyPassed = undefined
|
package/src/config.mjs
CHANGED
|
@@ -139,9 +139,12 @@ const COMPACT_RATIO = 0.6
|
|
|
139
139
|
|
|
140
140
|
/** Look up spec by model name prefix (case-insensitive), conservative default for unknown models */
|
|
141
141
|
const warnedModels = new Set() // warn once per model name — specForModel is a hot path (every request)
|
|
142
|
+
// Pre-sorted once at module scope — specForModel runs on every request (agent, provider core,
|
|
143
|
+
// context, auto-think, TUI rendering); re-sorting per call was wasteful.
|
|
144
|
+
const SORTED_SPECS = [...MODEL_SPECS].sort((a, b) => b[0].length - a[0].length)
|
|
142
145
|
export function specForModel(model) {
|
|
143
146
|
const m = (model ?? "").toLowerCase()
|
|
144
|
-
for (const [prefix, spec] of
|
|
147
|
+
for (const [prefix, spec] of SORTED_SPECS) {
|
|
145
148
|
if (m.startsWith(prefix.toLowerCase())) return spec
|
|
146
149
|
}
|
|
147
150
|
// Unknown model: warn ONCE (not per request) so a typo'd ID or a missing alias surfaces
|
|
@@ -189,6 +192,8 @@ export function normalizeProxy(proxy) {
|
|
|
189
192
|
* Load configuration.
|
|
190
193
|
* Env var priority: THINCODER_ACTIVE_PROVIDER > config file activeProvider
|
|
191
194
|
* THINCODER_API_KEY / THINCODER_BASE_URL / THINCODER_MODEL override the current active provider's corresponding fields
|
|
195
|
+
* THINCODER_ACTIVE_MODEL overrides the active model (wins over THINCODER_MODEL — see loadConfig)
|
|
196
|
+
* Provider-specific key fallbacks (when providers[] lacks a key): DEEPSEEK_API_KEY / OPENAI_API_KEY
|
|
192
197
|
*/
|
|
193
198
|
export function loadConfig() {
|
|
194
199
|
let config = {}
|
|
@@ -203,7 +208,7 @@ export function loadConfig() {
|
|
|
203
208
|
const merged = {
|
|
204
209
|
...DEFAULTS,
|
|
205
210
|
...config,
|
|
206
|
-
providers: config.providers
|
|
211
|
+
providers: Array.isArray(config.providers) && config.providers.length ? config.providers.map((p) => ({ ...p })) : DEFAULTS.providers.map((p) => ({ ...p })),
|
|
207
212
|
activeProvider: config.activeProvider ?? DEFAULTS.activeProvider,
|
|
208
213
|
agent: { ...DEFAULTS.agent, ...config.agent },
|
|
209
214
|
memory: { ...DEFAULTS.memory, ...config.memory },
|
|
@@ -247,6 +252,8 @@ export function loadConfig() {
|
|
|
247
252
|
|
|
248
253
|
// apiKey also falls back to env vars (when providers doesn't include a key)
|
|
249
254
|
// Provider-specific env vars only apply to the matching provider name, preventing keys from leaking to wrong endpoints
|
|
255
|
+
// NOTE: only deepseek/openai have provider-specific fallbacks by design — the other presets
|
|
256
|
+
// intentionally rely on THINCODER_API_KEY or keys stored in config.json (no silent env pickup).
|
|
250
257
|
if (!runtimeProvider.apiKey?.trim()) {
|
|
251
258
|
const envMap = { deepseek: "DEEPSEEK_API_KEY", openai: "OPENAI_API_KEY" }
|
|
252
259
|
const keyVar = envMap[merged.activeProvider]
|
|
@@ -278,9 +285,10 @@ export function loadConfig() {
|
|
|
278
285
|
*/
|
|
279
286
|
export function saveConfig(config) {
|
|
280
287
|
mkdirSync(configDir, { recursive: true })
|
|
281
|
-
// Inject $schema for editor autocompletion/validation (strip on load)
|
|
282
|
-
|
|
288
|
+
// Inject $schema for editor autocompletion/validation (strip on load) — write a copy,
|
|
289
|
+
// never mutate the caller's object.
|
|
290
|
+
const out = { ...config, $schema: "https://thincoder.dev/schemas/config.json" }
|
|
283
291
|
// 0600: config.json contains API keys, must not be world-readable (POSIX; chmod is best-effort on Windows)
|
|
284
|
-
writeFileSync(configPath, JSON.stringify(
|
|
292
|
+
writeFileSync(configPath, JSON.stringify(out, null, 2) + "\n", { encoding: "utf8", mode: 0o600 })
|
|
285
293
|
try { chmodSync(configPath, 0o600) } catch { /* may fail on Windows, ignore */ }
|
|
286
294
|
}
|
package/src/context.mjs
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
* context.mjs — Context management and compaction
|
|
3
3
|
* When no measured token count is available, use estimation as fallback (ASCII/4 + non-ASCII/1, no tokenizer dependency).
|
|
4
4
|
* When a measured value exists (response usage.prompt_tokens), trust it — estimation underestimates CJK by 3-4x and relying solely on it may never trigger compaction.
|
|
5
|
-
* Compaction strategy:
|
|
5
|
+
* Compaction strategy: summarize everything before the tail into one LLM note, keep the latest N messages verbatim.
|
|
6
|
+
* NOTE: no dedicated head is kept (KEEP_HEAD = 0) — in multi-task sessions the earliest messages are
|
|
7
|
+
* typically a COMPLETED earlier task; preserving them verbatim anchored the model's attention on stale
|
|
8
|
+
* work after compaction. The earliest messages now go into the summary (which distinguishes completed
|
|
9
|
+
* vs in-progress work), so the post-compaction context anchors on the current task (recent tail) only.
|
|
6
10
|
*/
|
|
7
11
|
|
|
8
12
|
import { chat } from "./provider/index.mjs"
|
|
@@ -30,11 +34,16 @@ export function estimateTokens(messages) {
|
|
|
30
34
|
return tokens
|
|
31
35
|
}
|
|
32
36
|
|
|
33
|
-
const KEEP_HEAD =
|
|
37
|
+
const KEEP_HEAD = 0 // No dedicated head: earliest messages may be a COMPLETED earlier task in multi-task
|
|
38
|
+
// sessions — keeping them verbatim anchored attention on stale work. Everything before the tail is
|
|
39
|
+
// summarized (the summary itself distinguishes completed vs in-progress work; see SUMMARIZE_PROMPT).
|
|
34
40
|
// Tail size scales with the model context window (~30 messages per 100K tokens),
|
|
35
41
|
// capped at 40% of history so small histories don't over-reserve. Window-adaptive
|
|
36
42
|
// replaces the old fixed 10: on a 1M window, 10 messages is too thin for recent work.
|
|
37
43
|
function keepTailSize(provider, historyLen) {
|
|
44
|
+
// provider is guaranteed at every call site (runAgent always builds one); specForModel
|
|
45
|
+
// degrades to DEFAULT_SPEC (128K) only if provider/model is somehow absent — acceptable
|
|
46
|
+
// because the 40% history cap still bounds the tail.
|
|
38
47
|
const ctxWindow = specForModel(provider?.model ?? "").context
|
|
39
48
|
return Math.min(Math.max(10, Math.floor((ctxWindow / 100_000) * 30)), Math.floor(historyLen * 0.4))
|
|
40
49
|
}
|
|
@@ -43,7 +52,9 @@ const SUMMARIZE_PROMPT = `You are a conversation compressor. Summarize the follo
|
|
|
43
52
|
Requirements:
|
|
44
53
|
- Write in first person, present tense — these are "my" handover notes, continuing my own train of thought
|
|
45
54
|
- Most important: preserve design decisions and their reasons — architecture choices, API contracts, naming conventions, trade-off rationale. These are the anchors the subsequent code must not deviate from
|
|
46
|
-
-
|
|
55
|
+
- Distinguish COMPLETED vs IN-PROGRESS work: completed tasks get a ONE-LINE recap each (what was done, key outcome); spend the detail budget on unresolved issues, next steps, and the CURRENT task
|
|
56
|
+
- The user's most recent request defines the current task — anchor on it. Earlier requests are likely already completed and only need the one-line recap; do NOT preserve them at full fidelity
|
|
57
|
+
- Keep: files modified and why, unresolved issues, next steps
|
|
47
58
|
- Drop: pleasantries, repetition, fine-grained tool output details
|
|
48
59
|
- Honestly mark uncertain items: anything not actually verified must say "unverified"; do not present guesses as facts
|
|
49
60
|
- Use bullet-point output; aim for information completeness, not a hard word limit (old 500-char cap is deprecated; in a 1M-context era, err on the long side)
|
|
@@ -71,8 +82,8 @@ const FALLBACK_NOTE =
|
|
|
71
82
|
|
|
72
83
|
/**
|
|
73
84
|
* Split history into head / middle (to be summarized) / tail; return null if no middle to compress.
|
|
74
|
-
*
|
|
75
|
-
*
|
|
85
|
+
* head is normally empty (KEEP_HEAD = 0 — earliest messages go into the summary); the
|
|
86
|
+
* tool_calls-extension logic below is defensive for future KEEP_HEAD > 0.
|
|
76
87
|
* The tail boundary must include any assistant whose tool results are in the tail — if the assistant is in the middle,
|
|
77
88
|
* the summary swallows it, leaving orphan tool results → protocol 400.
|
|
78
89
|
*/
|
|
@@ -101,7 +112,10 @@ function splitHistory(history, keepTail) {
|
|
|
101
112
|
}
|
|
102
113
|
|
|
103
114
|
// skip orphan tool messages at the new tail boundary (tool whose assistant was pulled in above)
|
|
104
|
-
|
|
115
|
+
// NOTE: single-assistant assumption — the backwards scan pulls the nearest owner only; in
|
|
116
|
+
// practice a tail spans at most one assistant→tools cycle (parallel calls share one assistant).
|
|
117
|
+
// Bounds-guarded so an all-tool tail cannot push tailStart past history.length.
|
|
118
|
+
while (tailStart < history.length && tailStart > headEnd && history[tailStart].role === "tool") {
|
|
105
119
|
tailStart++
|
|
106
120
|
}
|
|
107
121
|
if (tailStart <= headEnd) return null
|
|
@@ -127,6 +141,9 @@ export function pushReal(agent, msg) {
|
|
|
127
141
|
function applyCompression(agent, headEnd, tailStart, note) {
|
|
128
142
|
// _fullHistory already holds every real message (written at the source via pushReal),
|
|
129
143
|
// so compaction only shrinks the machine line — nothing to preserve here.
|
|
144
|
+
// head is normally empty (KEEP_HEAD = 0) — the summary note becomes the first message,
|
|
145
|
+
// which is exactly the intent: post-compaction context anchors on the current task, not on
|
|
146
|
+
// possibly-completed earlier requests.
|
|
130
147
|
const head = agent.history.slice(0, headEnd)
|
|
131
148
|
const tail = agent.history.slice(tailStart)
|
|
132
149
|
agent.history = [
|
|
@@ -173,7 +190,7 @@ function applyCompression(agent, headEnd, tailStart, note) {
|
|
|
173
190
|
* @param {object} extras - { systemPrompt?, tools? } — estimated overhead for the pure-estimation
|
|
174
191
|
* path (no measured baseline); the measured path already includes system+tools in prompt_tokens.
|
|
175
192
|
*/
|
|
176
|
-
export async function compressIfNeeded(agent, threshold, callbacks, extras = {}) {
|
|
193
|
+
export async function compressIfNeeded(agent, threshold, callbacks, extras = {}, signal) {
|
|
177
194
|
const history = agent.history
|
|
178
195
|
// Prefer the real baseline: the last response's prompt_tokens is the measured value for the full context (system+tools+history).
|
|
179
196
|
// Subsequent appended messages use estimation as increment; when no measured value exists (first turn / after restore / right after compaction), fall back to pure estimation
|
|
@@ -200,15 +217,21 @@ export async function compressIfNeeded(agent, threshold, callbacks, extras = {})
|
|
|
200
217
|
const toolNote = m.tool_calls ? ` [called tools: ${m.tool_calls.map((t) => t.function.name).join(", ")}]` : ""
|
|
201
218
|
// user messages get a wider cap (8000): cutting off a long user-pasted requirement loses original intent; tool/assistant capped at 2000 is enough
|
|
202
219
|
const cap = m.role === "user" ? 8000 : 2000
|
|
203
|
-
|
|
204
|
-
|
|
220
|
+
// Multimodal messages (array content): extract the TEXT parts — the image itself can't be
|
|
221
|
+
// summarized, but any accompanying text (e.g. "看这张图" + image) must not be silently lost
|
|
222
|
+
let text = ""
|
|
223
|
+
if (typeof m.content === "string") text = m.content
|
|
224
|
+
else if (Array.isArray(m.content)) text = m.content.filter((p) => p?.type === "text").map((p) => p.text ?? "").join(" ")
|
|
225
|
+
return `[${m.role}]${toolNote} ${text.slice(0, cap)}`
|
|
205
226
|
})
|
|
206
227
|
.join("\n")
|
|
207
228
|
|
|
208
229
|
// The summary is a plain-text task, no reasoning needed — passing thinking to the compaction provider wastes tokens.
|
|
209
230
|
// Silent by design (D11): no onToken/onReasoning — the compaction process must not stream to the frontend.
|
|
231
|
+
// signal propagates user cancellation (Ctrl+C) to the in-flight summary call.
|
|
210
232
|
const summary = await chat({ ...agent.provider, thinking: null, reasoningEffort: null }, {
|
|
211
233
|
messages: [{ role: "user", content: SUMMARIZE_PROMPT + serialized }],
|
|
234
|
+
signal,
|
|
212
235
|
})
|
|
213
236
|
|
|
214
237
|
applyCompression(agent, split.headEnd, split.tailStart, COMPACTION_PREFIX + summary.content)
|
|
@@ -5,7 +5,7 @@ You have a budget of 30 tool rounds (chat turns) — plan your exploration accor
|
|
|
5
5
|
|
|
6
6
|
Review workflow:
|
|
7
7
|
1. The files to review are listed in the review scope. Read them in full. The review scope defines exactly which files to inspect.
|
|
8
|
-
2.
|
|
8
|
+
2. **READ THE PROJECT GUIDE FIRST** — the `## Project Guide (AGENTS.md)` section in the review context maps the project's structure and tells you where its requirements/design documents live. Read the requirements documents it points to (whatever the guide names — no fixed file names are assumed). **The user's requirements live in those documents; the conversation background is only a supplement.** If the guide says none exist, judge from the conversation background and say so explicitly if requirements are unclear.
|
|
9
9
|
3. Read the specified files for full context. **Batch independent `read` calls in a SINGLE reply** — do not read files one at a time. Each round-trip counts against your limit.
|
|
10
10
|
4. Use grep or lsp to trace callers, imports, and dependencies — only where genuinely needed.
|
|
11
11
|
5. Produce your review table.
|
|
@@ -20,8 +20,8 @@ Rules:
|
|
|
20
20
|
- First judge the task from the conversation background: if the changes are clearly non-code and cannot affect runtime behavior, reply immediately with the all-clear phrase — `"All clear — no code changes to review."` (the host recognizes it via the "all clear" / "no 🔴" / "review passed" / "no issues found" markers, matched case-insensitively) — do NOT spend tool calls exploring. This applies to static docs, README, and CHANGELOG files. Prompts and configs that shape behaviour are NOT exempt — review them normally.
|
|
21
21
|
- **Requirement fit**: check the implementation against what the user actually asked for — a review is not only about "is the code correct" but also "is this what the user wanted". Two comparisons:
|
|
22
22
|
- (a) **Claim vs implementation**: the implementer's stated intent (conversation background / response table / commit message) vs what the implementation actually does — claiming X but delivering Y is a gap.
|
|
23
|
-
- (b) **Expectation vs shape**:
|
|
24
|
-
- **Known limit**: the conversation background only includes the last 3 user–assistant exchanges — older user expectations may not be visible. (a) is the primary check (needs only recent context); (b) is best-effort — check what the background
|
|
23
|
+
- (b) **Expectation vs shape**: the requirements documents named by the Project Guide (AGENTS.md) and explicit user expectations vs the delivered shape — "asked for A, got B" (e.g. "the record must keep the real order" vs a summary appended at the end) is a gap. **The requirements documents are the primary reference — read them (workflow step 2) before judging fit; do not judge against expectations you cannot see.**
|
|
24
|
+
- **Known limit**: the conversation background only includes the last 3 user–assistant exchanges — older user expectations may not be visible, which is why the requirements documents are the primary reference. (a) is the primary check (needs only recent context); (b) is best-effort — check what the docs/background show, do NOT treat an invisible expectation as a gap.
|
|
25
25
|
- **Severity**: 🔴 = the user's explicit request was not fulfilled; 🟡 = fulfilled but in a suboptimal or misleading way. Flag gaps by impact and state in the Issue: what the user asked for, what was delivered, and where they diverge. Claims must cite evidence (the user's own words or the implementation lines) — a "requirement gap" without evidence is 🔵 at most.
|
|
26
26
|
- Reply in the same language as the conversation background.
|
|
27
27
|
- Respect the project's stated platform requirements — do not flag features as errors if they are valid under the project's target environment.
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
You are an independent review advisor.
|
|
2
|
-
Verify the prior
|
|
2
|
+
Verify the prior review output (provided in the review context).
|
|
3
3
|
You may note obvious new issues introduced by the fixes.
|
|
4
4
|
You have read-only tools to explore the codebase.
|
|
5
5
|
You have a budget of 30 tool rounds (chat turns). Hard mechanical cap: 100 rounds.
|
|
6
6
|
|
|
7
7
|
Review workflow:
|
|
8
|
-
1. The affected files are named in
|
|
8
|
+
1. The prior review output above is the COMPLETE output of the last review — read it and understand every issue it raises. The affected files are named in it — read them in full. The prior review output is HISTORY from a previous review, not current state.
|
|
9
9
|
2. STALE-CONTEXT WARNING: any content from earlier messages is a historical snapshot — treat it as expired. Only fresh `read` results describe the current state.
|
|
10
|
-
3. Project conventions were established in round 1 — do NOT re-read AGENTS.md / design docs unless a prior-
|
|
11
|
-
4. **ALWAYS verify current file content with `read` before judging
|
|
10
|
+
3. Project conventions were established in round 1 — do NOT re-read AGENTS.md / design docs unless a prior-review item names them or a fix appears to contradict the task itself.
|
|
11
|
+
4. **ALWAYS verify current file content with `read` before judging an item as fixed or unfixed — never decide based on the prior review output alone.** Fixes may already be committed — `read` the files named there regardless. (Note: you have NO git tool this round; any git output in earlier messages is historical and untrustworthy.) Batch independent tool calls in one reply.
|
|
12
12
|
5. Use grep or lsp to trace callers, imports, and dependencies — only where genuinely needed.
|
|
13
13
|
6. Produce your review table.
|
|
14
14
|
|
|
15
|
-
Budget: read only the files named in the prior-
|
|
15
|
+
Budget: read only the files named in the prior-review items. If at 15 rounds you have not yet verified all items, wrap up.
|
|
16
16
|
|
|
17
17
|
Rules:
|
|
18
18
|
- Respect the project's stated platform requirements — do not flag features as errors if they are valid under the project's target environment.
|
|
19
|
-
- Primarily check fix status of items in the prior
|
|
19
|
+
- Primarily check fix status of items in the prior review output.
|
|
20
20
|
- For items marked "fixed": verify they were actually fixed.
|
|
21
21
|
- For items marked "not an issue": evaluate whether the reasoning is sound.
|
|
22
22
|
- Every "Unfixed" or "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 be fabricated or stale. Findings without a fresh quoted line are treated as unverified and will not be accepted.
|
|
@@ -1,31 +1,29 @@
|
|
|
1
1
|
You are an independent review advisor.
|
|
2
|
-
Strictly verify only the prior
|
|
3
|
-
Do NOT look for new issues.
|
|
2
|
+
Strictly verify only the prior review output (provided in the review context).
|
|
4
3
|
You have read-only tools to explore the codebase.
|
|
5
4
|
You have a budget of 30 tool rounds (chat turns). Hard mechanical cap: 100 rounds.
|
|
6
5
|
|
|
7
6
|
Review workflow:
|
|
8
|
-
1. The affected files are named in
|
|
7
|
+
1. The prior review output above is the COMPLETE output of the last review — read it and understand every issue it raises. The affected files are named in it — read them in full. The prior review output is HISTORY from a previous review, not current state.
|
|
9
8
|
2. STALE-CONTEXT WARNING: any content from earlier messages is a historical snapshot — treat it as expired. Only fresh `read` results describe the current state.
|
|
10
|
-
3. Project conventions were established in round 1 — do NOT re-read AGENTS.md / design docs unless a prior-
|
|
11
|
-
4. **ALWAYS verify current file content with `read` before judging
|
|
12
|
-
5.
|
|
9
|
+
3. Project conventions were established in round 1 — do NOT re-read AGENTS.md / design docs unless a prior-review item names them.
|
|
10
|
+
4. **ALWAYS verify current file content with `read` before judging an item as fixed or unfixed — never decide based on the prior review output alone.** Fixes may already be committed — `read` the files named there regardless. (Note: you have NO git tool this round; any git output in earlier messages is historical and untrustworthy.) Batch independent tool calls in one reply.
|
|
11
|
+
5. Use grep or lsp to trace callers, imports, and dependencies — only where genuinely needed.
|
|
13
12
|
6. Produce your review table.
|
|
14
13
|
|
|
15
|
-
Budget: read only the files named in the prior-
|
|
14
|
+
Budget: read only the files named in the prior-review items. If at 15 rounds you have not yet verified all items, wrap up.
|
|
16
15
|
|
|
17
16
|
Rules:
|
|
18
17
|
- Respect the project's stated platform requirements — do not flag features as errors if they are valid under the project's target environment.
|
|
19
|
-
- Only check fix status of items in the prior
|
|
20
|
-
-
|
|
21
|
-
- For items marked "not an issue": evaluate whether the reasoning is sound.
|
|
22
|
-
- Every "Unfixed" 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 be fabricated or stale. Findings without a fresh quoted line are treated as unverified and will not be accepted.
|
|
18
|
+
- Only check fix status of items in the prior review output.
|
|
19
|
+
- Every "Unfixed" or "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 be fabricated or stale. Findings without a fresh quoted line are treated as unverified and will not be accepted.
|
|
23
20
|
- **Host verification**: your `file:line: content` citations are mechanically checked against the CURRENT file state — quote exactly what `read` returned; a mismatch marks the finding unverified.
|
|
24
21
|
- **Fresh context**: this round's conversation contains NO read output from earlier rounds — every file must be re-read this round.
|
|
25
|
-
-
|
|
22
|
+
- Do NOT look for new issues. This round exists ONLY to verify that the items from the prior review output are resolved.
|
|
23
|
+
- Do NOT nitpick style or naming.
|
|
24
|
+
- Output a Markdown table listing all remaining problems:
|
|
26
25
|
| # | Orig# | File | Severity | Status | Notes |
|
|
27
26
|
|---|-------|------|----------|--------|-------|
|
|
28
27
|
| 1 | 3 | src/x.mjs | 🔴 | Unfixed | ... |
|
|
29
|
-
| 2 | 5 | src/y.mjs | 🟡 | Reasoning invalid | ... |
|
|
30
28
|
- If all 🔴 issues are resolved and remaining items are only 🟡/🔵, the review passes (🟡/🔵 do not block approval). If any 🔴 issue persists, do not claim it passed.
|
|
31
29
|
- Stop calling tools once you are ready to produce the review table.
|