thincoder 0.12.62 → 0.12.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +13 -12
- package/bin/thincoder.mjs +53 -27
- package/package.json +6 -5
- package/src/acp/bridge.mjs +35 -15
- package/src/acp/client-caps.mjs +86 -0
- package/src/acp/ext.mjs +86 -0
- package/src/acp/handlers-session.mjs +240 -0
- package/src/acp/handlers-slots.mjs +196 -0
- package/src/acp/login.mjs +48 -0
- package/src/acp/session.mjs +6 -4
- package/src/acp.mjs +67 -379
- package/src/cli/distill-command.mjs +3 -3
- package/src/cli/make-agent.mjs +59 -17
- package/src/cli/memory-command.mjs +3 -3
- package/src/cli/permission.mjs +4 -48
- package/src/cli/setup-wizard.mjs +1 -1
- package/src/completions.mjs +3 -1
- package/src/crash-reports.mjs +1 -1
- package/src/distill.mjs +4 -4
- package/src/heap-watch.mjs +1 -1
- package/src/prompt-injections.mjs +20 -0
- package/src/tui/agent-turn.mjs +40 -9
- package/src/tui/cmd-advisor.mjs +5 -5
- package/src/tui/cmd-config.mjs +8 -8
- package/src/tui/cmd-eng.mjs +25 -9
- package/src/tui/cmd-mcp.mjs +9 -8
- package/src/tui/cmd-model.mjs +1 -1
- package/src/tui/cmd-new.mjs +6 -5
- package/src/tui/cmd-reindex.mjs +1 -1
- package/src/tui/cmd-restore.mjs +2 -2
- package/src/tui/cmd-session.mjs +24 -1
- package/src/tui/cmd-skills.mjs +1 -1
- package/src/tui/cmd-think.mjs +22 -9
- package/src/tui/config-helpers.mjs +1 -1
- package/src/tui/display-budget.mjs +33 -11
- package/src/tui/index.mjs +9 -9
- package/src/tui/interaction.mjs +16 -7
- package/src/tui/key-modes.mjs +9 -4
- package/src/tui/ledger-surface.mjs +26 -10
- package/src/tui/model-catalog.mjs +4 -4
- package/src/tui/model-picker.mjs +8 -7
- package/src/tui/mouse.mjs +11 -6
- package/src/tui/pickers.mjs +15 -2
- package/src/tui/render-conversation.mjs +1 -1
- package/src/tui/render-frame.mjs +10 -5
- package/src/tui/render-loop.mjs +1 -1
- package/src/tui/render-segments.mjs +3 -1
- package/src/tui/slash-commands.mjs +1 -1
- package/src/tui/startup.mjs +14 -14
- package/src/tui/subagent-blocks.mjs +20 -3
- package/src/tui/subagent-freeze.mjs +73 -2
- package/src/tui/suspension-drive.mjs +46 -22
- package/src/tui/tool-events.mjs +11 -8
- package/src/tui/wizard.mjs +3 -3
- package/src/abort-provenance.mjs +0 -116
- package/src/advisor/citations.mjs +0 -139
- package/src/advisor/compaction.mjs +0 -174
- package/src/advisor/convergence.mjs +0 -80
- package/src/advisor/history.mjs +0 -77
- package/src/advisor/loop.mjs +0 -293
- package/src/advisor/messages.mjs +0 -299
- package/src/advisor/project-context.mjs +0 -194
- package/src/advisor/repos.mjs +0 -150
- package/src/advisor/run.mjs +0 -293
- package/src/advisor/truncate.mjs +0 -57
- package/src/advisor.mjs +0 -290
- package/src/agent/completion.mjs +0 -146
- package/src/agent/dispatch.mjs +0 -489
- package/src/agent/helpers.mjs +0 -384
- package/src/agent/post-turn.mjs +0 -70
- package/src/agent/record-results.mjs +0 -174
- package/src/agent/relay-prefix.mjs +0 -39
- package/src/agent/run-stages.mjs +0 -244
- package/src/agent/setup-reminders.mjs +0 -69
- package/src/agent/setup.mjs +0 -354
- package/src/agent/spawn-child.mjs +0 -243
- package/src/agent-tools/advisor-async.mjs +0 -346
- package/src/agent-tools/advisor-settle.mjs +0 -231
- package/src/agent-tools/advisor.mjs +0 -260
- package/src/agent-tools/async-settle.mjs +0 -204
- package/src/agent-tools/batch-segment.mjs +0 -195
- package/src/agent-tools/consult.mjs +0 -473
- package/src/agent-tools/design-token.mjs +0 -117
- package/src/agent-tools/digest-budget.mjs +0 -76
- package/src/agent-tools/eng.mjs +0 -67
- package/src/agent-tools/escalate-async.mjs +0 -295
- package/src/agent-tools/goal.mjs +0 -119
- package/src/agent-tools/plan.mjs +0 -81
- package/src/agent-tools/read-history.mjs +0 -309
- package/src/agent-tools/recent-changes.mjs +0 -24
- package/src/agent-tools/review-streak.mjs +0 -93
- package/src/agent-tools/settings.mjs +0 -265
- package/src/agent-tools/skill.mjs +0 -47
- package/src/agent-tools/subagent-actions.mjs +0 -482
- package/src/agent-tools/subagent-async.mjs +0 -434
- package/src/agent-tools/subagent-panel.mjs +0 -160
- package/src/agent-tools/subagent-run.mjs +0 -205
- package/src/agent-tools/subagent-scheduler.mjs +0 -392
- package/src/agent-tools/subagent-spawn.mjs +0 -459
- package/src/agent-tools/subagent.mjs +0 -404
- package/src/agent-tools/task.mjs +0 -87
- package/src/agent-tools/timer.mjs +0 -46
- package/src/agent-tools/verify.mjs +0 -271
- package/src/agent-tools.mjs +0 -17
- package/src/agent.mjs +0 -417
- package/src/auto-think.mjs +0 -115
- package/src/config-migrate.mjs +0 -70
- package/src/config.mjs +0 -496
- package/src/context.mjs +0 -392
- package/src/conventions.mjs +0 -223
- package/src/embedding.mjs +0 -120
- package/src/escape.mjs +0 -152
- package/src/expand-home.mjs +0 -16
- package/src/explore-distill.mjs +0 -155
- package/src/generate-title.mjs +0 -88
- package/src/git/checkpoint.mjs +0 -448
- package/src/git/gitmem.mjs +0 -100
- package/src/hooks.mjs +0 -97
- package/src/ledger.mjs +0 -227
- package/src/log.mjs +0 -195
- package/src/markdown.mjs +0 -106
- package/src/mcp/helpers.mjs +0 -51
- package/src/mcp/transport-http.mjs +0 -248
- package/src/mcp/transport-stdio.mjs +0 -140
- package/src/mcp/transport-ws.mjs +0 -122
- package/src/mcp.mjs +0 -295
- package/src/memory/code-index.mjs +0 -219
- package/src/memory/code-sync.mjs +0 -415
- package/src/memory/core.mjs +0 -299
- package/src/memory/delete.mjs +0 -236
- package/src/memory/docs.mjs +0 -419
- package/src/memory/file-walk.mjs +0 -109
- package/src/memory/scan.mjs +0 -95
- package/src/memory/schema.mjs +0 -452
- package/src/memory.mjs +0 -21
- package/src/model-ref.mjs +0 -66
- package/src/model-specs.mjs +0 -179
- package/src/peer-domains.mjs +0 -265
- package/src/peer-instances.mjs +0 -231
- package/src/prompt-overlays.mjs +0 -82
- package/src/prompts/advisor-design.md +0 -41
- package/src/prompts/advisor-round1.md +0 -41
- package/src/prompts/advisor-round2.md +0 -46
- package/src/prompts/advisor-round3.md +0 -42
- package/src/prompts/common.md +0 -115
- package/src/prompts/consult-base.md +0 -19
- package/src/prompts/discipline-engineering.md +0 -258
- package/src/prompts/discipline-normal.md +0 -185
- package/src/prompts/persona-coder.md +0 -21
- package/src/prompts/persona-eng-coder.md +0 -37
- package/src/prompts/persona-eng-designer.md +0 -60
- package/src/prompts/persona-engineering.md +0 -55
- package/src/prompts/persona-explore.md +0 -15
- package/src/prompts/persona-normal.md +0 -27
- package/src/prompts/persona-plan.md +0 -26
- package/src/provider/anthropic.mjs +0 -225
- package/src/provider/core.mjs +0 -476
- package/src/provider/errors.mjs +0 -101
- package/src/provider/google.mjs +0 -257
- package/src/provider/index.mjs +0 -7
- package/src/provider/list-models.mjs +0 -93
- package/src/provider/normalize.mjs +0 -81
- package/src/provider/rate.mjs +0 -108
- package/src/provider/responses.mjs +0 -495
- package/src/provider/retry.mjs +0 -88
- package/src/provider/sse.mjs +0 -264
- package/src/proxy.mjs +0 -261
- package/src/rules.mjs +0 -53
- package/src/session-gc.mjs +0 -221
- package/src/session-guard.mjs +0 -59
- package/src/session-migrate.mjs +0 -48
- package/src/session-rename.mjs +0 -38
- package/src/session-segments.mjs +0 -100
- package/src/session-slots.mjs +0 -492
- package/src/session-store.mjs +0 -441
- package/src/session.mjs +0 -492
- package/src/skills.mjs +0 -153
- package/src/text-budget.mjs +0 -46
- package/src/token-ttl.mjs +0 -274
- package/src/tools/apply_patch.md +0 -15
- package/src/tools/bash.md +0 -37
- package/src/tools/bash.mjs +0 -268
- package/src/tools/checklist-sync.mjs +0 -181
- package/src/tools/checklist.md +0 -13
- package/src/tools/checklist.mjs +0 -299
- package/src/tools/delete.md +0 -13
- package/src/tools/edit-batch.mjs +0 -191
- package/src/tools/edit-diff.mjs +0 -348
- package/src/tools/edit.md +0 -30
- package/src/tools/execute.md +0 -21
- package/src/tools/execute.mjs +0 -228
- package/src/tools/fetch.md +0 -12
- package/src/tools/file.mjs +0 -469
- package/src/tools/file_ops.md +0 -17
- package/src/tools/get_current_time.md +0 -8
- package/src/tools/git-checkpoint.mjs +0 -143
- package/src/tools/git-ext.mjs +0 -173
- package/src/tools/git.md +0 -54
- package/src/tools/git.mjs +0 -356
- package/src/tools/glob-dialect.mjs +0 -130
- package/src/tools/glob.md +0 -11
- package/src/tools/grep.md +0 -19
- package/src/tools/hashline_edit.md +0 -14
- package/src/tools/index.mjs +0 -36
- package/src/tools/insert_after.md +0 -15
- package/src/tools/lint.md +0 -10
- package/src/tools/linter.mjs +0 -128
- package/src/tools/ls.md +0 -12
- package/src/tools/lsp.md +0 -10
- package/src/tools/lsp.mjs +0 -316
- package/src/tools/ops.mjs +0 -299
- package/src/tools/patch.mjs +0 -282
- package/src/tools/process.md +0 -10
- package/src/tools/question.md +0 -16
- package/src/tools/question.mjs +0 -26
- package/src/tools/read.md +0 -20
- package/src/tools/read_image.md +0 -8
- package/src/tools/repomap.mjs +0 -314
- package/src/tools/search.mjs +0 -236
- package/src/tools/shared.mjs +0 -446
- package/src/tools/tree.md +0 -14
- package/src/tools/tree.mjs +0 -66
- package/src/tools/wait_for.md +0 -22
- package/src/tools/web.mjs +0 -224
- package/src/tools/websearch.md +0 -16
- package/src/tools/write.md +0 -11
- package/src/traces/trace-store.mjs +0 -355
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* advisor/project-context.mjs — project-context discovery and injection for review
|
|
3
|
-
* messages (Project Guide, document map, project standards, change-set context).
|
|
4
|
-
*
|
|
5
|
-
* Split out of messages.mjs (the split registered as D-1 in ENGINEERING-MODE.md
|
|
6
|
-
* §2.26.3, trigger "thicken again" — met by this batch). messages.mjs keeps its
|
|
7
|
-
* pre-split surface via re-export; this module is consumed by it only.
|
|
8
|
-
*
|
|
9
|
-
* Portability contract (PORTABILITY FR10/FR11 · PO-1/PO-2 · P8): every discovery
|
|
10
|
-
* step here is either satisfied or DEGRADED VISIBLY. The old code probed a fixed
|
|
11
|
-
* `docs/README.md` and a fixed `METHODOLOGY.md` and skipped injection silently when
|
|
12
|
-
* absent — in a project with a different layout the reviewer still scored the
|
|
13
|
-
* dimension it could no longer check (silent failure, the worst class: invisible
|
|
14
|
-
* and unfixable by the user). Missing pieces now say so, and the reviewer is told
|
|
15
|
-
* to state the limitation in its findings. Projects can point at their own files
|
|
16
|
-
* through `.thincoder/conventions.json` (advisor.docMap / advisor.standardsDoc).
|
|
17
|
-
*/
|
|
18
|
-
import { readFileSync, existsSync } from "node:fs"
|
|
19
|
-
import { resolve, join, relative, dirname, sep } from "node:path"
|
|
20
|
-
import { providerSpec } from "../config.mjs"
|
|
21
|
-
import { loadConventions } from "../conventions.mjs"
|
|
22
|
-
|
|
23
|
-
/** Project guide (AGENTS.md) injection budget — decision 2026-08-08:
|
|
24
|
-
* NO fixed truncation; long-context models (1M+) get up to 5% of their context
|
|
25
|
-
* window for the doc map, small windows still get a floor so the map is always
|
|
26
|
-
* visible. The map is what tells the reviewer WHERE the requirements docs live
|
|
27
|
-
* (requirement-fit is judged against those docs, not the conversation only). */
|
|
28
|
-
const PROJECT_GUIDE_MIN = 8192 // chars — floor for small-window models
|
|
29
|
-
const PROJECT_GUIDE_FRACTION = 0.05 // 5% of the reviewer model's context window
|
|
30
|
-
|
|
31
|
-
/** Absent-AGENTS.md notice (guide is optional: the conversation carries intent). */
|
|
32
|
-
export const NO_GUIDE_NOTICE = "(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.)"
|
|
33
|
-
|
|
34
|
-
/** Explicit degradation sentences (PORTABILITY design §4.4 — verbatim). */
|
|
35
|
-
export const NO_DOC_MAP_NOTICE = "(No document map found under the project root, and none is declared — the Document ownership criterion is degraded: check placement against the Project Guide where present, and state the limitation in your findings.)"
|
|
36
|
-
export const NO_STANDARDS_NOTICE = "(No project standards document was declared — judge methodology compliance from the Project Guide (when present) and the review criteria above; state the limitation in your findings.)"
|
|
37
|
-
export const NO_GIT_NOTICE = "(No git repository detected — change-set context is unavailable; read the review-scope files directly.)"
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Discover the project root for the review — user decision 2026-08-08:
|
|
41
|
-
* the project root is a SUBDIRECTORY of the working directory, never an
|
|
42
|
-
* ancestor above it. Priority:
|
|
43
|
-
* 1. Walk UP from each review-scope file's directory, bounded by cwd —
|
|
44
|
-
* the NEAREST AGENTS.md inside the workspace wins. In a monorepo this is
|
|
45
|
-
* the subproject's own doc map even when cwd itself has an AGENTS.md
|
|
46
|
-
* (a workspace-level meta map must not shadow the subproject guide).
|
|
47
|
-
* 2. No scope files / nothing found → cwd (single project; the walk's
|
|
48
|
-
* last step naturally lands on cwd's own AGENTS.md when it exists).
|
|
49
|
-
* @param {string} cwd — the agent's working directory (workspace root)
|
|
50
|
-
* @param {string[]} scopeFiles — cwd-relative review-scope paths (may be empty)
|
|
51
|
-
* @returns {string|null} absolute project root with an AGENTS.md, or null
|
|
52
|
-
*/
|
|
53
|
-
export function findProjectRoot(cwd, scopeFiles) {
|
|
54
|
-
// Normalize separators before comparing: input paths may use either
|
|
55
|
-
// convention (join() → "\\" on Windows; tool args / tests → "/"). Mixed
|
|
56
|
-
// styles made isInside(cwd + sep) miss legitimately nested paths.
|
|
57
|
-
const norm = (p) => p.replaceAll("\\", "/")
|
|
58
|
-
const isInside = (dir) => {
|
|
59
|
-
const d = norm(dir)
|
|
60
|
-
const c = norm(cwd)
|
|
61
|
-
return d === c || d.startsWith(c + "/")
|
|
62
|
-
}
|
|
63
|
-
for (const f of scopeFiles) {
|
|
64
|
-
let dir = dirname(resolve(cwd, f))
|
|
65
|
-
while (isInside(dir) && dir !== dirname(dir)) {
|
|
66
|
-
if (existsSync(join(dir, "AGENTS.md"))) return dir
|
|
67
|
-
dir = dirname(dir)
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
// No scope files, or none found in the walk — cwd itself (its AGENTS.md is
|
|
71
|
-
// checked as the walk's final step for scope files; for empty scopes, check
|
|
72
|
-
// it explicitly so a bare cwd project still gets its guide).
|
|
73
|
-
if (existsSync(join(cwd, "AGENTS.md"))) return cwd
|
|
74
|
-
return null
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Inject the project guide (AGENTS.md) into the review message. AGENTS.md is the
|
|
79
|
-
* project's doc map — it defines the structure and where requirements/design
|
|
80
|
-
* documents live. The reviewer must see it FIRST: requirement-fit is judged
|
|
81
|
-
* against the documents it points to, with the conversation background as a
|
|
82
|
-
* supplement. Absent AGENTS.md degrades honestly (no pretending there is a map).
|
|
83
|
-
* @param {Object} agent — the parent agent
|
|
84
|
-
* @param {string[]} parts — message parts (mutated)
|
|
85
|
-
* @param {string[]} [scopeFiles] — cwd-relative review-scope paths for project-root discovery
|
|
86
|
-
* @returns {string|null} the discovered project root (abs), or null when no guide
|
|
87
|
-
*/
|
|
88
|
-
export function injectProjectGuide(agent, parts, scopeFiles = []) {
|
|
89
|
-
parts.push("## Project Guide (AGENTS.md)")
|
|
90
|
-
const root = findProjectRoot(agent.cwd, scopeFiles)
|
|
91
|
-
const path = root ? join(root, "AGENTS.md") : null
|
|
92
|
-
let text
|
|
93
|
-
if (!path) {
|
|
94
|
-
parts.push(NO_GUIDE_NOTICE)
|
|
95
|
-
parts.push("")
|
|
96
|
-
return null // no guide — requirement-fit falls back to the conversation
|
|
97
|
-
}
|
|
98
|
-
try {
|
|
99
|
-
text = readFileSync(path, "utf8")
|
|
100
|
-
} catch (e) {
|
|
101
|
-
if (e.code !== "ENOENT") {
|
|
102
|
-
// File exists but is unreadable (EACCES etc.) — log, don't masquerade as "not found".
|
|
103
|
-
console.warn(`[advisor] AGENTS.md unreadable at ${path}: ${e.message}`)
|
|
104
|
-
}
|
|
105
|
-
parts.push(NO_GUIDE_NOTICE)
|
|
106
|
-
parts.push("")
|
|
107
|
-
return null // no guide — requirement-fit falls back to the conversation
|
|
108
|
-
}
|
|
109
|
-
// readFileSync succeeded — compute the budget OUTSIDE the try so a spec
|
|
110
|
-
// lookup failure can never masquerade as "no AGENTS.md".
|
|
111
|
-
// providerSpec: the project-guide budget follows the provider-level context
|
|
112
|
-
// override (PROVIDER.md §15 — advisor messages budget is context-based).
|
|
113
|
-
const ctx = providerSpec(agent.provider).context
|
|
114
|
-
const cap = Math.max(PROJECT_GUIDE_MIN, Math.floor(ctx * PROJECT_GUIDE_FRACTION))
|
|
115
|
-
const shown = text.length <= cap
|
|
116
|
-
? text
|
|
117
|
-
: [...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
|
|
118
|
-
parts.push(`<!-- Project root: ${relative(agent.cwd, path).split(sep).join("/")} (inferred from the review scope under ${agent.cwd}) -->`)
|
|
119
|
-
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.")
|
|
120
|
-
parts.push("")
|
|
121
|
-
parts.push(shown)
|
|
122
|
-
parts.push("")
|
|
123
|
-
return root // guide injected — requirement-fit criteria apply (truthy root)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Inject the project document map (PO-1). Resolution: the DECLARED path
|
|
128
|
-
* (`advisor.docMap`, project-root-relative) wins; the built-in probe
|
|
129
|
-
* (`docs/README.md` → `docs/design/README.md`) stays as a fallback for projects
|
|
130
|
-
* laid out that way. Neither usable → an explicit degradation sentence — never a
|
|
131
|
-
* silent skip: the reviewer must know the Document-ownership criterion is unchecked.
|
|
132
|
-
* @returns {string|null} the injected map path, or null when degraded
|
|
133
|
-
*/
|
|
134
|
-
export function injectDocumentMap(agent, parts, root) {
|
|
135
|
-
const base = root ?? agent.cwd
|
|
136
|
-
const conv = loadConventions(agent.cwd)
|
|
137
|
-
let mapPath = null
|
|
138
|
-
if (conv.advisor.docMap) {
|
|
139
|
-
const declared = resolve(base, conv.advisor.docMap)
|
|
140
|
-
if (existsSync(declared)) mapPath = declared
|
|
141
|
-
} else {
|
|
142
|
-
mapPath = [resolve(base, "docs", "README.md"), resolve(base, "docs", "design", "README.md")]
|
|
143
|
-
.find((p) => existsSync(p)) ?? null
|
|
144
|
-
}
|
|
145
|
-
parts.push("## Document Map")
|
|
146
|
-
let text = null
|
|
147
|
-
if (mapPath) {
|
|
148
|
-
try {
|
|
149
|
-
text = readFileSync(mapPath, "utf8")
|
|
150
|
-
} catch (e) {
|
|
151
|
-
console.warn(`[advisor] document map unreadable at ${mapPath}: ${e.message}`)
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
if (text === null) {
|
|
155
|
-
parts.push(NO_DOC_MAP_NOTICE) // degraded — visible by contract
|
|
156
|
-
parts.push("")
|
|
157
|
-
return null
|
|
158
|
-
}
|
|
159
|
-
parts.push("The document map below registers which document files exist per section. Use it for the Document ownership criterion: a change for an existing section must amend that section's document, not create a new file.")
|
|
160
|
-
parts.push(text)
|
|
161
|
-
parts.push("")
|
|
162
|
-
return mapPath
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Inject the project standards document (PO-2). DECLARATION ONLY
|
|
167
|
-
* (`advisor.standardsDoc`, project-root-relative): the old hardcoded
|
|
168
|
-
* `METHODOLOGY.md` probe injected a file most projects do not have — and its
|
|
169
|
-
* absence was silent. Undeclared (or unreadable) → explicit degradation sentence.
|
|
170
|
-
* @returns {string|null} the injected standards path, or null when degraded
|
|
171
|
-
*/
|
|
172
|
-
export function injectProjectStandards(agent, parts, root) {
|
|
173
|
-
const base = root ?? agent.cwd
|
|
174
|
-
const conv = loadConventions(agent.cwd)
|
|
175
|
-
const declared = conv.advisor.standardsDoc ? resolve(base, conv.advisor.standardsDoc) : null
|
|
176
|
-
parts.push("## Project Standards")
|
|
177
|
-
let text = null
|
|
178
|
-
if (declared && existsSync(declared)) {
|
|
179
|
-
try {
|
|
180
|
-
text = readFileSync(declared, "utf8")
|
|
181
|
-
} catch (e) {
|
|
182
|
-
console.warn(`[advisor] project standards unreadable at ${declared}: ${e.message}`)
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
if (text === null) {
|
|
186
|
-
parts.push(NO_STANDARDS_NOTICE) // degraded — visible by contract
|
|
187
|
-
parts.push("")
|
|
188
|
-
return null
|
|
189
|
-
}
|
|
190
|
-
parts.push("The project declares these standards for this review. Judge the design/changes against them:")
|
|
191
|
-
parts.push(text)
|
|
192
|
-
parts.push("")
|
|
193
|
-
return declared
|
|
194
|
-
}
|
package/src/advisor/repos.mjs
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* advisor/repos.mjs — git repository discovery and change collection for advisor reviews.
|
|
3
|
-
* Shared by message building (advisor.mjs) and the review runner (advisor/run.mjs).
|
|
4
|
-
*/
|
|
5
|
-
import { execFileSync } from "node:child_process"
|
|
6
|
-
import { dirname, basename, resolve } from "node:path"
|
|
7
|
-
import { loadConventions, isCodePath } from "../conventions.mjs"
|
|
8
|
-
|
|
9
|
-
const GIT_TIMEOUT = 5_000
|
|
10
|
-
const MAX_EMBEDDED_DIFF = 50_000
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Find the git repository roots that contain the agent's touched files.
|
|
14
|
-
* Falls back to cwd if no repos found.
|
|
15
|
-
*/
|
|
16
|
-
export function findReviewRepos(agent, paths = null) {
|
|
17
|
-
const touched = agent._touchedFiles ?? []
|
|
18
|
-
const sources = paths ? [...touched, ...paths.map((p) => resolve(agent.cwd, p))] : touched
|
|
19
|
-
const repos = []
|
|
20
|
-
|
|
21
|
-
for (const abs of sources) {
|
|
22
|
-
try {
|
|
23
|
-
const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
24
|
-
cwd: dirname(abs), encoding: "utf8", timeout: GIT_TIMEOUT,
|
|
25
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
26
|
-
}).trim()
|
|
27
|
-
if (root && !repos.includes(root)) repos.push(root)
|
|
28
|
-
} catch { /* not a git repo */ }
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (repos.length > 0) return repos
|
|
32
|
-
|
|
33
|
-
// Fallback: cwd itself
|
|
34
|
-
try {
|
|
35
|
-
const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
36
|
-
cwd: agent.cwd, encoding: "utf8", timeout: GIT_TIMEOUT,
|
|
37
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
38
|
-
}).trim()
|
|
39
|
-
if (root) return [root]
|
|
40
|
-
} catch { /* not a git repo */ }
|
|
41
|
-
|
|
42
|
-
return []
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Collect git status + diff for each repo, embedded into the review context so
|
|
47
|
-
* the advisor doesn't need to spend its first tool calls discovering changes.
|
|
48
|
-
*/
|
|
49
|
-
export function collectRepoSnapshots(repos, cwd) {
|
|
50
|
-
const targets = repos.length > 0 ? repos : [cwd]
|
|
51
|
-
const parts = []
|
|
52
|
-
for (const repo of targets) {
|
|
53
|
-
let status, diff
|
|
54
|
-
try {
|
|
55
|
-
status = execFileSync("git", ["status", "--porcelain"], {
|
|
56
|
-
cwd: repo, encoding: "utf8", timeout: GIT_TIMEOUT, stdio: ["ignore", "pipe", "pipe"],
|
|
57
|
-
}).trim()
|
|
58
|
-
diff = execFileSync("git", ["diff", "HEAD"], {
|
|
59
|
-
cwd: repo, encoding: "utf8", timeout: GIT_TIMEOUT, stdio: ["ignore", "pipe", "pipe"],
|
|
60
|
-
maxBuffer: 8 * 1024 * 1024,
|
|
61
|
-
})
|
|
62
|
-
} catch { continue /* not a git repo or git failed */ }
|
|
63
|
-
if (!status && !diff.trim()) continue
|
|
64
|
-
parts.push(`### ${repo}`)
|
|
65
|
-
if (status) parts.push("```", status, "```")
|
|
66
|
-
if (diff.trim()) {
|
|
67
|
-
const truncated = diff.length > MAX_EMBEDDED_DIFF
|
|
68
|
-
// Prepend a blockquote explaining diff notation to the LLM so it doesn't
|
|
69
|
-
// treat deleted lines (-) as still-present content (phantom-issue fix).
|
|
70
|
-
parts.push("**⚠️ IMPORTANT:** In the diff below, `-` lines are **REMOVED** (no longer in the file); `+` lines are **ADDED**. Always `read` the actual file for its current state — never treat a `-` line as still-present content.")
|
|
71
|
-
parts.push("```diff", truncated ? diff.slice(0, MAX_EMBEDDED_DIFF) : diff.trimEnd(), "```")
|
|
72
|
-
if (truncated) parts.push(`(diff truncated at ${MAX_EMBEDDED_DIFF} chars — use the git tool to see the rest)`)
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return parts
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** List changed file paths (including untracked) across repos — used by design review
|
|
79
|
-
* so the advisor knows which files to read even when git diff HEAD can't show them.
|
|
80
|
-
* Multi-repo: each path is annotated with its repo basename so the advisor can resolve it. */
|
|
81
|
-
export function collectChangedFiles(repos, cwd) {
|
|
82
|
-
const targets = repos.length > 0 ? repos : [cwd]
|
|
83
|
-
const files = []
|
|
84
|
-
for (const repo of targets) {
|
|
85
|
-
try {
|
|
86
|
-
const status = execFileSync("git", ["status", "--porcelain"], {
|
|
87
|
-
cwd: repo, encoding: "utf8", timeout: GIT_TIMEOUT, stdio: ["ignore", "pipe", "pipe"],
|
|
88
|
-
}).trim()
|
|
89
|
-
const repoLabel = targets.length > 1 ? `[${basename(repo)}] ` : ""
|
|
90
|
-
for (const line of status.split("\n")) {
|
|
91
|
-
// "XY path" or "XY old -> new" (rename) — take the final path, strip quotes
|
|
92
|
-
const pathParts = line.slice(3).split(" -> ")
|
|
93
|
-
const p = pathParts[pathParts.length - 1].trim().replace(/^"|"$/g, "")
|
|
94
|
-
if (p) files.push(repoLabel + p)
|
|
95
|
-
}
|
|
96
|
-
} catch { /* not a git repo — skip */ }
|
|
97
|
-
}
|
|
98
|
-
return files
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Path classification (code / doc / temp) lives in ../conventions.mjs — the single
|
|
102
|
-
// authority shared by every gate and guard (PORTABILITY FR12 / PO-10). This module
|
|
103
|
-
// consumes it: `isDocFile` / `isTempFile` were retired here (no re-export — a second
|
|
104
|
-
// import path would recreate the drift the authority exists to remove).
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* True when this run mutated at least one CODE file. Shared single source of
|
|
108
|
-
* truth for the advisor/verify guards (agent.mjs + agent/completion.mjs).
|
|
109
|
-
* Doc-only changes (docs/, *.md, LICENSE…) must NOT trigger the guards — the
|
|
110
|
-
* design phase edits docs/ and must not be pushed to a code review. Temp/scratch
|
|
111
|
-
* files (tmp-*, .tmp, .temp) are excluded too — a throwaway diagnostic script
|
|
112
|
-
* is not a code change. Mutations without a known path (tools outside
|
|
113
|
-
* FILE_MUTATORS) are treated as code — cannot tell, so guard conservatively.
|
|
114
|
-
* Classification semantics (incl. "a declared code segment wins over temp/doc")
|
|
115
|
-
* live in conventions.mjs; _touchedFiles stores ABSOLUTE paths, which the
|
|
116
|
-
* segment matcher handles at any depth.
|
|
117
|
-
*/
|
|
118
|
-
export function hasCodeMutations({ _touchedFiles, _mutatedThisRun, cwd }) {
|
|
119
|
-
const files = _touchedFiles ?? []
|
|
120
|
-
if (files.length === 0) return _mutatedThisRun
|
|
121
|
-
const conv = loadConventions(cwd)
|
|
122
|
-
return files.some((p) => isCodePath(p, conv))
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/** True when all changed files across repos are documentation (md/txt/LICENSE etc.).
|
|
126
|
-
* Anything inside a code segment (incl. src/prompts/*.md) counts as product code —
|
|
127
|
-
* the same authority the design gate uses. Paths here are repo-relative. */
|
|
128
|
-
export function isDocOnlyChange(repos, cwd) {
|
|
129
|
-
const conv = loadConventions(cwd)
|
|
130
|
-
const targets = repos.length > 0 ? repos : [cwd]
|
|
131
|
-
let sawChanges = false
|
|
132
|
-
for (const repo of targets) {
|
|
133
|
-
let status
|
|
134
|
-
try {
|
|
135
|
-
status = execFileSync("git", ["status", "--porcelain"], {
|
|
136
|
-
cwd: repo, encoding: "utf8", timeout: GIT_TIMEOUT, stdio: ["ignore", "pipe", "pipe"],
|
|
137
|
-
}).trim()
|
|
138
|
-
} catch { continue /* repo inaccessible — check the rest */ }
|
|
139
|
-
if (!status) continue
|
|
140
|
-
sawChanges = true
|
|
141
|
-
for (const line of status.split("\n")) {
|
|
142
|
-
// porcelain: "XY path" or "XY old -> new" (rename)
|
|
143
|
-
const filePath = line.slice(3).split(" -> ").pop().replace(/^"|"$/g, "")
|
|
144
|
-
// Code (a declared code segment is unconditional, even for tmp-*.mjs) →
|
|
145
|
-
// not a doc-only change; docs and temp scratch files fall through.
|
|
146
|
-
if (isCodePath(filePath, conv)) return false
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return sawChanges
|
|
150
|
-
}
|
package/src/advisor/run.mjs
DELETED
|
@@ -1,293 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* advisor/run.mjs — advisor execution: provider resolution and the review entry
|
|
3
|
-
* point. Message building lives in advisor.mjs; the tool loop lives in loop.mjs
|
|
4
|
-
* and the context/limit/tail guards in compaction.mjs(第 11 批硬帽拆分——
|
|
5
|
-
* run.mjs 原 498/500;既有 import 面经 re-export 保持不变)。
|
|
6
|
-
*/
|
|
7
|
-
import { findProvider } from "../config.mjs"
|
|
8
|
-
import { prepareAdvisorMessages } from "../advisor.mjs"
|
|
9
|
-
import { buildObjectDeclarationBlock, buildDesignApprovalBlock } from "./messages.mjs"
|
|
10
|
-
import { appendCitationReport } from "./citations.mjs"
|
|
11
|
-
import { runAdvisorToolLoop } from "./loop.mjs"
|
|
12
|
-
import { advisorIncompleteMarker, estimateTokens } from "./compaction.mjs"
|
|
13
|
-
import { batchDocForReview } from "../agent-tools/batch-segment.mjs"
|
|
14
|
-
// 第 33 批(§17.5 模块图):护栏常量 / doc-set 键 / 记录读取——单向导入(review-streak 无回指)。
|
|
15
|
-
import {
|
|
16
|
-
MAX_DESIGN_REVIEW_STREAK, docSetKey, designReviewStreakRecord, designReviewStreakStopped,
|
|
17
|
-
} from "../agent-tools/review-streak.mjs"
|
|
18
|
-
|
|
19
|
-
// 拆分后 import 面(既有导出名逐一保面——re-export;谓词为本批新增)。
|
|
20
|
-
export { ADVISOR_THINKING_PLACEHOLDER, MAX_RESULT_CHARS, renderTimeline as _renderTimeline } from "./compaction.mjs"
|
|
21
|
-
export { advisorToolsFor, advisorToolsFor as _advisorToolsFor } from "./loop.mjs"
|
|
22
|
-
export { runAdvisorToolLoop as _runAdvisorToolLoop } from "./loop.mjs"
|
|
23
|
-
export { advisorIncompleteMarker } from "./compaction.mjs"
|
|
24
|
-
|
|
25
|
-
// Mechanical convergence cap: up to 5 rounds suffice; a 6th call means the model
|
|
26
|
-
// is looping — refuse it instead of burning tokens. §11.2 D-24b (2026-09-06): PER
|
|
27
|
-
// REVIEW INSTANCE (agent._advisorRuns); CODE REVIEWS ONLY (2026-09-07 §8 ruling)
|
|
28
|
-
// — design reviews are EXEMPT: their rounds keep advancing, the cap never refuses.
|
|
29
|
-
export const MAX_ADVISOR_ROUNDS = 5
|
|
30
|
-
|
|
31
|
-
/** B 启动拒绝前缀(§14.4 #2)——稳定契约单源(三消费点同串):run.mjs 生成;同步工具面
|
|
32
|
-
* 据此登记 `_advisorRefusals`;异步结算面据此不置 `_calledAdvisorThisRun`。 */
|
|
33
|
-
export const ADVISOR_LAUNCH_REFUSAL_PREFIX = "Advisor: design review launch refused"
|
|
34
|
-
|
|
35
|
-
/** 评审失败护栏稳定前缀(第 33 批 §17.4——与 `ADVISOR_LAUNCH_REFUSAL_PREFIX` 同族;
|
|
36
|
-
* 实现 grep / 用例断言锚)。凭证卫生:串内零 token / designId 值。 */
|
|
37
|
-
export const ADVISOR_DESIGN_STREAK_STOP_PREFIX = "Advisor: design review stopped"
|
|
38
|
-
|
|
39
|
-
/** kind → 人读说明(第 33 批 §17.4 逐字——结论表第三列;八类 = 五 kind + stale +
|
|
40
|
-
* no_credential + no_report,与 review-streak.mjs 分类输出同集)。 */
|
|
41
|
-
const DESIGN_REVIEW_OUTCOME_MEANINGS = {
|
|
42
|
-
timeout: "review exceeded the wall-clock budget (agent.advisor.timeoutMs)",
|
|
43
|
-
context_limit: "review exceeded the model context budget",
|
|
44
|
-
turn_cap: "review exceeded the tool-round limit",
|
|
45
|
-
empty: "the provider returned an empty response",
|
|
46
|
-
review_failed: "provider / transport error",
|
|
47
|
-
stale: "the reviewed documents changed while the review was in flight",
|
|
48
|
-
no_credential: "the token could not be written to the session ledger",
|
|
49
|
-
no_report: "the review settled without a report",
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* 停止结论串(第 33 批 §17.4 逐字;F29):稳定前缀 + 停止的 doc-set 清单 + 失败尝试表
|
|
54
|
-
* (记录逐条——kind + 人读说明)+ 三选项(接受现状 / 改变或缩小范围后重跑 / /new 重置)。
|
|
55
|
-
* 全文零凭证值;不自动执行任何恢复动作(发起权在父代理 / 用户)。
|
|
56
|
-
* @param {{count: number, log: string[]}|null} record — 护栏记录(`designReviewStreakRecord`)
|
|
57
|
-
* @param {string[]|null} documents — 被停的文档集(结论清单数据源)
|
|
58
|
-
*/
|
|
59
|
-
export function buildDesignReviewGuardMessage(record, documents) {
|
|
60
|
-
const log = Array.isArray(record?.log) ? record.log : []
|
|
61
|
-
const docList = Array.isArray(documents) ? documents.filter((d) => typeof d === "string" && d.trim()) : []
|
|
62
|
-
return [
|
|
63
|
-
`${ADVISOR_DESIGN_STREAK_STOP_PREFIX} — ${MAX_DESIGN_REVIEW_STREAK} consecutive attempts on this document set produced no design token (repeated failed settlements; no further reviews will start for this set in this session).`,
|
|
64
|
-
"Document set (1 design instance — no token issued):",
|
|
65
|
-
...docList.map((d) => `- ${d}`),
|
|
66
|
-
"Attempts (most recent last):",
|
|
67
|
-
"| # | outcome | meaning |",
|
|
68
|
-
"|---|---|---|",
|
|
69
|
-
...log.map((kind, i) => `| ${i + 1} | ${kind} | ${DESIGN_REVIEW_OUTCOME_MEANINGS[kind] ?? kind} |`),
|
|
70
|
-
"Options:",
|
|
71
|
-
"1. Accept the current state and proceed — implementation for this document set stays gated (no design token).",
|
|
72
|
-
"2. Narrow or change the scope: a different document set starts a fresh budget — fix the cause first (agent.advisor.timeoutMs / advisor model / provider).",
|
|
73
|
-
"3. Start a new session (/new) to reset the guard.",
|
|
74
|
-
].join("\n")
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const MAX_UNFIXED_DISPLAY = 10 // unfixed issues shown in the cap message
|
|
78
|
-
|
|
79
|
-
/** Resolve the advisor's provider: cfg.provider/model when set, otherwise the main agent's provider */
|
|
80
|
-
export function resolveAdvisorProvider(agent) {
|
|
81
|
-
const cfg = agent.config?.advisor
|
|
82
|
-
if (cfg?.provider) {
|
|
83
|
-
try {
|
|
84
|
-
// F-1 (ISSUE-FIX-BATCH): children carry no agent.providers (spawn childConfig copies the
|
|
85
|
-
// parent config) — fall back to config.providersList (.length: [] must not skip the list).
|
|
86
|
-
const provider = findProvider(agent.providers?.length ? agent.providers : agent.config?.providersList ?? [agent.provider], cfg.provider)
|
|
87
|
-
// F-2a (MODEL-400-FIX):无 cfg.model 时渠道克隆须重派生 model——渠道裸克隆会丢 model 键
|
|
88
|
-
// → 无 model 请求 → serde 400。
|
|
89
|
-
// MODEL-SELECTION v2(M3④):无 cfg.model → 命中渠道自己的默认模型(`provider.model`
|
|
90
|
-
// 单值);渠道无默认模型 → 父 provider 兜底(与 subagent F-2c 同构)——绝不产出静默
|
|
91
|
-
// undefined-model 请求(最极端两者皆无 → chat 前 assertProviderModel fail-fast)。
|
|
92
|
-
const result = cfg.model ? { ...provider, model: cfg.model } : { ...provider, model: provider.model ?? agent.provider?.model }
|
|
93
|
-
if (cfg.thinking === null || cfg.thinking === false) result.thinking = undefined // explicitly off
|
|
94
|
-
else if (cfg.thinking !== undefined) result.thinking = cfg.thinking
|
|
95
|
-
if (cfg.reasoningEffort !== undefined) result.reasoningEffort = cfg.reasoningEffort
|
|
96
|
-
return result
|
|
97
|
-
} catch (e) {
|
|
98
|
-
// Provider not found or lookup failed — fall back to main provider, but surface the reason
|
|
99
|
-
console.warn(`[advisor] resolveAdvisorProvider: ${e.message}`)
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
const provider = { ...agent.provider }
|
|
103
|
-
if (cfg?.model) provider.model = cfg.model
|
|
104
|
-
// thinking off: null AND false both mean "explicitly off" — a raw `false`
|
|
105
|
-
// value is invalid for providers that expect undefined or an object.
|
|
106
|
-
if (cfg?.thinking === null || cfg?.thinking === false) provider.thinking = undefined
|
|
107
|
-
else if (cfg?.thinking !== undefined) provider.thinking = cfg.thinking
|
|
108
|
-
if (cfg?.reasoningEffort !== undefined) provider.reasoningEffort = cfg.reasoningEffort
|
|
109
|
-
return provider
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Extract unfixed issues from prior review text (for the cap message).
|
|
114
|
-
* Input: an advisor review markdown table (`| # | … |` rows). A row counts as
|
|
115
|
-
* unfixed unless its line carries a resolved-status word (fixed/resolved/done/
|
|
116
|
-
* addressed/corrected, ✓/✔). Returns at most MAX_UNFIXED_DISPLAY plain
|
|
117
|
-
* (pipe-stripped) row strings.
|
|
118
|
-
*/
|
|
119
|
-
function extractUnfixedIssues(priorText) {
|
|
120
|
-
if (!priorText) return []
|
|
121
|
-
const lines = priorText.split("\n")
|
|
122
|
-
// Resolved-status words: fixed/resolved/done/addressed/corrected (+ ✓/✔).
|
|
123
|
-
// \b prevents "unfixed"/"prefixed" from matching "fixed".
|
|
124
|
-
const resolvedRe = /\b(?:fixed|resolved|done|addressed|corrected)\b|✓|✔/i
|
|
125
|
-
return lines
|
|
126
|
-
.filter((line) => /\|\s*\d+\s*\|/.test(line)) // 匹配表格行
|
|
127
|
-
.filter((line) => !resolvedRe.test(line))
|
|
128
|
-
// Strip only the leading/trailing table pipes — inner pipes (escaped or
|
|
129
|
-
// in-cell content) stay intact instead of garbling the cap message.
|
|
130
|
-
.map((line) => line.trim().replace(/^\|/, "").replace(/\|$/, "").trim())
|
|
131
|
-
.filter(Boolean)
|
|
132
|
-
.slice(0, MAX_UNFIXED_DISPLAY)
|
|
133
|
-
}
|
|
134
|
-
/** Review-looking guard (async settle parity): a markdown table row or ≥200 chars of prose counts as a prior. */
|
|
135
|
-
export function looksLikeReviewOutput(text) {
|
|
136
|
-
const trimmed = String(text ?? "").trim()
|
|
137
|
-
return /\|.*\|.*\|/.test(trimmed) || trimmed.length >= 200
|
|
138
|
-
}
|
|
139
|
-
/** Cap message (shared by runAdvisorReview and the async pre-check — per-review refusal). */
|
|
140
|
-
export function buildCapMessage(agent) {
|
|
141
|
-
const prior = agent._lastAdvisorOutput
|
|
142
|
-
const unfixed = prior ? extractUnfixedIssues(prior) : []
|
|
143
|
-
let message = `Advisor: convergence cap reached after ${MAX_ADVISOR_ROUNDS} rounds.\n`
|
|
144
|
-
if (unfixed.length > 0) {
|
|
145
|
-
message += `\nUnresolved issues from prior rounds:\n${unfixed.map((i) => `- ${i}`).join("\n")}\n`
|
|
146
|
-
} else {
|
|
147
|
-
message += "\nAll prior issues appear resolved.\n"
|
|
148
|
-
}
|
|
149
|
-
message += "\nOptions:\n1. Accept current state and proceed\n2. Manually review specific concerns with read/grep\n3. Start a new session (/new) to reset the advisor"
|
|
150
|
-
return message
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* 压缩定锚简报(F13/§14.4 #3)——**由评审参数构建**(非模型输出):对象声明 / 文档清单 /
|
|
155
|
-
* Approval Signal 三锚;重内容(项目指南 / 方法论 / 文档地图)不入 pin(压缩的意义所在)。
|
|
156
|
-
* 形态逐字见设计 §14.4 #3(首行为机械化重挂说明)。
|
|
157
|
-
*/
|
|
158
|
-
function buildPinnedBrief(reviewType, documents, object, designToken, designId) {
|
|
159
|
-
const docList = Array.isArray(documents) ? documents.filter((d) => typeof d === "string" && d.trim()) : []
|
|
160
|
-
const parts = ["[review brief — re-attached after context compaction; the original review request is no longer in the context]"]
|
|
161
|
-
const declaration = buildObjectDeclarationBlock(object)
|
|
162
|
-
if (declaration) parts.push(declaration.trimEnd())
|
|
163
|
-
if (docList.length > 0) {
|
|
164
|
-
parts.push("## Documents to Review")
|
|
165
|
-
parts.push(docList.map((d) => `- ${d} — Read this file in full`).join("\n"))
|
|
166
|
-
}
|
|
167
|
-
if (reviewType === "design" && designToken) parts.push(buildDesignApprovalBlock(designToken, designId))
|
|
168
|
-
return parts.join("\n\n")
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Run an advisor review. reviewType: "code" (default) or "design". Returns review text or null when skipped.
|
|
173
|
-
* @param {string|null} [designToken] — injected into the design-review prompt; the advisor echoes it only on approval.
|
|
174
|
-
* @param {string[]|null} [documents] — design review only: explicit list of doc paths to review; passed through to the message builder.
|
|
175
|
-
* @param {string[]|null} [paths] — code review only: explicit list of file/dir paths to review.
|
|
176
|
-
* @param {Object|null} [object] — review-object declaration (§18.8 D-OA1/D-OA3): { type, target, status, reason, exclude }; absent → legacy behavior (no injection).
|
|
177
|
-
* @param {string|null} [designId] — §29.1 F2a: injected next to the design token in
|
|
178
|
-
* the Approval Signal (passed through to the message builders).
|
|
179
|
-
*/
|
|
180
|
-
export async function runAdvisorReview(agent, reviewType, callbacks, designToken = null, documents = null, paths = null, object = null, designId = null) {
|
|
181
|
-
const onOutput = callbacks?.onOutput
|
|
182
|
-
const signal = callbacks?.signal
|
|
183
|
-
const startTime = Date.now()
|
|
184
|
-
|
|
185
|
-
// Advisor reviews are ALWAYS available (2026-08-21 semantic refactor): the
|
|
186
|
-
// former advisor.enabled gate is removed — review capability has no off
|
|
187
|
-
// switch; only the guard (completion pushback) is opt-in via advisor.guard.
|
|
188
|
-
|
|
189
|
-
// Mechanical convergence cap — CODE REVIEWS ONLY (2026-09-07 §8 ruling: design
|
|
190
|
-
// reviews are exempt). _advisorRound is scoped to the current review instance
|
|
191
|
-
// (§11.2 D-24b ③), so >= MAX_ADVISOR_ROUNDS blocks the next call of THIS instance.
|
|
192
|
-
// 5 rounds max; after that the review is never pushed back.
|
|
193
|
-
if (reviewType !== "design" && (agent._advisorRound || 0) >= MAX_ADVISOR_ROUNDS) {
|
|
194
|
-
return buildCapMessage(agent)
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// 第 33 批(§17.5 检查点 2——内防线):同一 doc-set 连续未产出可用结算达阈值 ⇒ 直接拒绝
|
|
198
|
-
// (不建消息、不发起、零 LLM)——防直接调用方绕过工具层预检(正常工具链在预检已拒)。
|
|
199
|
-
if (reviewType === "design" && Array.isArray(documents) && documents.length > 0) {
|
|
200
|
-
const streakKey = docSetKey(documents, agent.cwd)
|
|
201
|
-
if (designReviewStreakStopped(agent, streakKey)) {
|
|
202
|
-
return buildDesignReviewGuardMessage(designReviewStreakRecord(agent, streakKey), documents)
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const provider = resolveAdvisorProvider(agent)
|
|
207
|
-
// Advisor always works in the agent's cwd — scope is defined by paths/documents.
|
|
208
|
-
const advisorCwd = agent.cwd
|
|
209
|
-
|
|
210
|
-
const messages = prepareAdvisorMessages(agent, reviewType, designToken, documents, paths, null, object, designId)
|
|
211
|
-
|
|
212
|
-
// B 启动断言(fail-closed——§14.4 #2):设计评审请求内**必须**携带与本次签发 token 精确
|
|
213
|
-
// 对应的 Approval Signal——构建面补不上就拒绝启动(不发"请回显一个不存在的 token"的请求)。
|
|
214
|
-
// 拒绝报告前缀 `Advisor: design review launch refused` = 稳定契约(同步工具面据此登记
|
|
215
|
-
// _advisorRefusals;异步结算面据此不置 _calledAdvisorThisRun)。工具路径恒签发 token ⇒
|
|
216
|
-
// 该拒绝为直接调用方兜底(防御纵深——正常链不可达,如实注)。
|
|
217
|
-
if (reviewType === "design") {
|
|
218
|
-
const missing = !designToken
|
|
219
|
-
? "no design token was minted"
|
|
220
|
-
: (messages.some((m) => m.role === "user" && String(m.content ?? "").includes(`[DESIGN-TOKEN:${designToken}`))
|
|
221
|
-
? null
|
|
222
|
-
: "the request does not carry the approval signal")
|
|
223
|
-
if (missing) {
|
|
224
|
-
return `${ADVISOR_LAUNCH_REFUSAL_PREFIX} — ${missing}. Nothing was sent: a request that asks the reviewer to echo a token it cannot see would break the credential chain. Re-run advisor(type='design') to mint a fresh token.`
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// §2.20.3 批次档写通道的绑定(仅设计评审):同步路径 = 调用方(advisor 工具)传入的
|
|
229
|
-
// callbacks.batchDoc(即 resolved.run 的实例绑定);异步路径 = 本实例在跑池条目的
|
|
230
|
-
// run.batchDoc(同文档集实例键——各评审各取各档,不用单值会话态)。
|
|
231
|
-
const boundBatchDoc = reviewType === "design" ? batchDocForReview(agent, documents, callbacks) : null
|
|
232
|
-
// F13/§14.4 #3:压缩定锚简报(评审参数构建——压缩触发时由 compaction 重挂)。
|
|
233
|
-
const pinned = buildPinnedBrief(reviewType, documents, object, designToken, designId)
|
|
234
|
-
|
|
235
|
-
try {
|
|
236
|
-
const result = await runAdvisorToolLoop(provider, messages, onOutput, signal, agent, advisorCwd, null, reviewType, boundBatchDoc, pinned)
|
|
237
|
-
|
|
238
|
-
// Host-verified citations (decision d698434): mechanically check every
|
|
239
|
-
// `file:line: content` reference in the review against the CURRENT file
|
|
240
|
-
// state. LLMs cannot self-enforce the evidence rule — the model may quote
|
|
241
|
-
// the prior table instead of re-reading (three consecutive false reports
|
|
242
|
-
// cited pre-fix line content). Unverified citations must not support a
|
|
243
|
-
// push-back; the parent agent sees the verification report.
|
|
244
|
-
// F14/§14.5:解析候选 = cwd + 评审对象声明范围派生根(scope = documents + paths)。
|
|
245
|
-
let final = result
|
|
246
|
-
if (!result.trimStart().startsWith("Advisor:")) {
|
|
247
|
-
final = appendCitationReport(result, advisorCwd, { scope: [...(documents ?? []), ...(paths ?? [])] })
|
|
248
|
-
// Success path: keep the FULL review output for convergence rounds —
|
|
249
|
-
// round 2+ injects this verbatim and the model understands it (decision
|
|
250
|
-
// 2026-08-08: prior-table hard parsing removed; no phrase/header matching).
|
|
251
|
-
// Guard: only store outputs that actually carry a review — a markdown
|
|
252
|
-
// table row (`| a | b | c |`) or substantial prose (>200 chars). An
|
|
253
|
-
// empty or tool-progress-only reply must not become the "prior review"
|
|
254
|
-
// of round 2+.
|
|
255
|
-
const looksLikeReview = looksLikeReviewOutput(final)
|
|
256
|
-
if (looksLikeReview) {
|
|
257
|
-
agent._lastAdvisorOutput = final
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Log review statistics for observability
|
|
262
|
-
const elapsed = Math.round((Date.now() - startTime) / 1000)
|
|
263
|
-
const toolCallCount = messages.filter((m) => m.role === "tool").length
|
|
264
|
-
const tokensUsed = estimateTokens(messages)
|
|
265
|
-
onOutput?.({
|
|
266
|
-
kind: "text",
|
|
267
|
-
text: `\n[advisor] Review completed: ${elapsed}s, ${toolCallCount} tool calls, ~${Math.round(tokensUsed / 1000)}k tokens\n`,
|
|
268
|
-
})
|
|
269
|
-
return final
|
|
270
|
-
} catch (e) {
|
|
271
|
-
if (e.name === "AbortError" && signal?.reason?.interrupt) throw e
|
|
272
|
-
|
|
273
|
-
// 细化错误类型
|
|
274
|
-
const errorType = e.message.includes("rate limit") || e.message.includes("429") ? "rate limit"
|
|
275
|
-
: e.message.includes("timeout") ? "timeout"
|
|
276
|
-
: e.message.includes("network") || e.message.includes("ECONNREFUSED") ? "network"
|
|
277
|
-
: e.message.includes("context length") ? "context_too_long"
|
|
278
|
-
: "unknown"
|
|
279
|
-
|
|
280
|
-
const retryAdvice = errorType === "rate limit"
|
|
281
|
-
? "Wait a moment and retry. Consider using a cheaper model for advisor."
|
|
282
|
-
: errorType === "timeout"
|
|
283
|
-
? "The model took too long. Try with a narrower scope."
|
|
284
|
-
: errorType === "context_too_long"
|
|
285
|
-
? "Reduce the scope (fewer files/paths) or use a model with larger context window."
|
|
286
|
-
: "You may retry or proceed to verify manually."
|
|
287
|
-
|
|
288
|
-
return `Advisor: review failed (${errorType}) — ${e.message || "unknown error"}. ${retryAdvice}`
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
// Host-verified citations — moved to citations.mjs (kept re-exported here for
|
|
292
|
-
// import compatibility: tests and callers import from run.mjs).
|
|
293
|
-
export { extractCitations, verifyCitations, appendCitationReport } from "./citations.mjs"
|