thincoder 0.12.60 → 0.12.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -1
- package/README.md +8 -6
- package/bin/thincoder.mjs +27 -122
- package/package.json +3 -2
- package/src/abort-provenance.mjs +116 -0
- package/src/acp/bridge.mjs +38 -17
- package/src/acp.mjs +6 -1
- package/src/advisor/citations.mjs +83 -21
- package/src/advisor/compaction.mjs +174 -0
- package/src/advisor/loop.mjs +293 -0
- package/src/advisor/messages.mjs +36 -134
- package/src/advisor/project-context.mjs +194 -0
- package/src/advisor/repos.mjs +17 -40
- package/src/advisor/run.mjs +124 -329
- package/src/advisor/truncate.mjs +57 -0
- package/src/advisor.mjs +3 -2
- package/src/agent/completion.mjs +1 -1
- package/src/agent/dispatch.mjs +47 -12
- package/src/agent/helpers.mjs +71 -13
- package/src/agent/record-results.mjs +13 -5
- package/src/agent/relay-prefix.mjs +39 -0
- package/src/agent/run-stages.mjs +21 -6
- package/src/agent/setup-reminders.mjs +16 -9
- package/src/agent/setup.mjs +92 -128
- package/src/agent/spawn-child.mjs +26 -9
- package/src/agent-tools/advisor-async.mjs +70 -180
- package/src/agent-tools/advisor-settle.mjs +231 -0
- package/src/agent-tools/advisor.mjs +69 -20
- package/src/agent-tools/batch-segment.mjs +195 -0
- package/src/agent-tools/consult.mjs +23 -10
- package/src/agent-tools/design-token.mjs +14 -1
- package/src/agent-tools/digest-budget.mjs +76 -0
- package/src/agent-tools/eng.mjs +3 -3
- package/src/agent-tools/escalate-async.mjs +16 -13
- package/src/agent-tools/read-history.mjs +13 -3
- package/src/agent-tools/review-streak.mjs +93 -0
- package/src/agent-tools/settings.mjs +130 -17
- package/src/agent-tools/subagent-actions.mjs +15 -6
- package/src/agent-tools/subagent-async.mjs +66 -14
- package/src/agent-tools/subagent-panel.mjs +22 -15
- package/src/agent-tools/subagent-run.mjs +9 -6
- package/src/agent-tools/subagent-scheduler.mjs +57 -8
- package/src/agent-tools/subagent-spawn.mjs +63 -16
- package/src/agent-tools/subagent.mjs +175 -49
- package/src/agent-tools/verify.mjs +13 -34
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +38 -21
- package/src/cli/distill-command.mjs +2 -2
- package/src/cli/make-agent.mjs +23 -7
- package/src/cli/memory-command.mjs +2 -2
- package/src/cli/setup-wizard.mjs +29 -9
- package/src/completions.mjs +114 -0
- package/src/config-migrate.mjs +70 -0
- package/src/config.mjs +132 -63
- package/src/conventions.mjs +223 -0
- package/src/crash-reports.mjs +7 -2
- package/src/expand-home.mjs +16 -0
- package/src/generate-title.mjs +1 -1
- package/src/hooks.mjs +7 -3
- package/src/memory/code-index.mjs +9 -3
- package/src/memory/code-sync.mjs +70 -31
- package/src/memory/delete.mjs +2 -0
- package/src/memory/docs.mjs +10 -6
- package/src/memory/file-walk.mjs +109 -0
- package/src/memory/schema.mjs +15 -3
- package/src/model-ref.mjs +66 -0
- package/src/model-specs.mjs +42 -8
- package/src/prompt-overlays.mjs +73 -16
- package/src/prompts/advisor-design.md +18 -8
- package/src/prompts/advisor-round1.md +7 -1
- package/src/prompts/advisor-round2.md +13 -2
- package/src/prompts/advisor-round3.md +13 -2
- package/src/prompts/common.md +115 -0
- package/src/prompts/consult-base.md +2 -0
- package/src/prompts/discipline-engineering.md +217 -0
- package/src/prompts/discipline-normal.md +179 -0
- package/src/prompts/persona-coder.md +21 -0
- package/src/prompts/persona-eng-coder.md +37 -0
- package/src/prompts/persona-eng-designer.md +55 -0
- package/src/prompts/persona-engineering.md +54 -0
- package/src/prompts/persona-explore.md +15 -0
- package/src/prompts/persona-normal.md +27 -0
- package/src/prompts/persona-plan.md +26 -0
- package/src/provider/anthropic.mjs +4 -4
- package/src/provider/core.mjs +13 -32
- package/src/provider/errors.mjs +26 -1
- package/src/provider/google.mjs +5 -6
- package/src/provider/index.mjs +2 -1
- package/src/provider/list-models.mjs +93 -0
- package/src/provider/rate.mjs +2 -1
- package/src/provider/responses.mjs +5 -3
- package/src/provider/sse.mjs +3 -4
- package/src/proxy.mjs +9 -14
- package/src/session-slots.mjs +7 -2
- package/src/session.mjs +59 -38
- package/src/token-ttl.mjs +2 -1
- package/src/tools/{system.mjs → bash.mjs} +6 -243
- package/src/tools/file.mjs +30 -10
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +15 -34
- package/src/tools/index.mjs +4 -2
- package/src/tools/ops.mjs +20 -7
- package/src/tools/question.md +1 -0
- package/src/tools/question.mjs +26 -0
- package/src/tools/read.md +1 -1
- package/src/tools/read_image.md +1 -1
- package/src/tools/search.mjs +236 -0
- package/src/tui/agent-turn.mjs +32 -13
- package/src/tui/ansi.mjs +2 -0
- package/src/tui/clipboard.mjs +7 -1
- package/src/tui/cmd-advisor.mjs +3 -2
- package/src/tui/cmd-config.mjs +108 -37
- package/src/tui/cmd-eng.mjs +11 -27
- package/src/tui/cmd-exit.mjs +6 -8
- package/src/tui/cmd-model.mjs +14 -12
- package/src/tui/cmd-reindex.mjs +7 -0
- package/src/tui/cmd-submodel.mjs +8 -5
- package/src/tui/cmd-undo.mjs +4 -3
- package/src/tui/index.mjs +38 -37
- package/src/tui/key-handler.mjs +61 -17
- package/src/tui/key-modes.mjs +86 -8
- package/src/tui/layout.mjs +18 -10
- package/src/tui/model-catalog.mjs +89 -0
- package/src/tui/model-picker.mjs +498 -0
- package/src/tui/mouse.mjs +47 -10
- package/src/tui/pickers.mjs +28 -410
- package/src/tui/render-frame.mjs +32 -16
- package/src/tui/render-loop.mjs +2 -0
- package/src/tui/render-segments.mjs +5 -19
- package/src/tui/render.mjs +37 -5
- package/src/tui/slash-commands.mjs +2 -2
- package/src/tui/subagent-blocks.mjs +69 -90
- package/src/tui/subagent-children.mjs +50 -64
- package/src/tui/subagent-freeze.mjs +40 -43
- package/src/tui/subagent-panel.mjs +21 -66
- package/src/tui/suspension-drive.mjs +28 -82
- package/src/tui/tool-args.mjs +4 -2
- package/src/tui/tool-display.mjs +4 -3
- package/src/tui/tool-events.mjs +23 -16
- package/src/tui/tui-lifecycle.mjs +18 -5
- package/src/tui/wizard.mjs +52 -18
- package/src/tui/wrapped-spawn.mjs +38 -0
- package/src/prompts/coder.md +0 -13
- package/src/prompts/discipline.md +0 -84
- package/src/prompts/eng-coder.md +0 -19
- package/src/prompts/engineering-sub.md +0 -14
- package/src/prompts/engineering.md +0 -87
- package/src/prompts/explore.md +0 -12
- package/src/prompts/main.md +0 -34
- package/src/prompts/methodology-template.md +0 -38
- package/src/prompts/plan.md +0 -9
- package/src/prompts/system.md +0 -44
package/src/advisor/messages.mjs
CHANGED
|
@@ -2,108 +2,17 @@
|
|
|
2
2
|
* advisor/messages.mjs — advisor user-message building (buildAdvisorUserMessage).
|
|
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
|
+
* Project-context discovery/injection lives in project-context.mjs (the D-1 registration,
|
|
6
|
+
* ENGINEERING-MODE.md §2.26.3); the pre-split surface is re-exported below.
|
|
5
7
|
*/
|
|
6
|
-
import {
|
|
7
|
-
import { resolve, join, relative, dirname, sep } from "node:path"
|
|
8
|
-
import { providerSpec } from "../config.mjs"
|
|
8
|
+
import { join, relative, sep } from "node:path"
|
|
9
9
|
import { findReviewRepos, collectRepoSnapshots, collectChangedFiles } from "./repos.mjs"
|
|
10
10
|
import { buildConvergenceBody, buildConvergenceInstructions } from "./convergence.mjs"
|
|
11
11
|
import { loadAdvisorMd, extractConversationBackground, extractAgentResponseTable } from "./history.mjs"
|
|
12
|
+
import { injectProjectGuide, injectDocumentMap, injectProjectStandards, NO_GIT_NOTICE } from "./project-context.mjs"
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
// providerSpec: the project-guide budget follows the provider-level context
|
|
94
|
-
// override (PROVIDER.md §15 — advisor messages budget is context-based).
|
|
95
|
-
const ctx = providerSpec(agent.provider).context
|
|
96
|
-
const cap = Math.max(PROJECT_GUIDE_MIN, Math.floor(ctx * PROJECT_GUIDE_FRACTION))
|
|
97
|
-
const shown = text.length <= cap
|
|
98
|
-
? text
|
|
99
|
-
: [...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
|
|
100
|
-
parts.push(`<!-- Project root: ${relative(agent.cwd, path).split(sep).join("/")} (inferred from the review scope under ${agent.cwd}) -->`)
|
|
101
|
-
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.")
|
|
102
|
-
parts.push("")
|
|
103
|
-
parts.push(shown)
|
|
104
|
-
parts.push("")
|
|
105
|
-
return root // guide injected — requirement-fit criteria apply (truthy root)
|
|
106
|
-
}
|
|
14
|
+
// Structural split (not an authority migration): the moved helpers stay reachable here.
|
|
15
|
+
export { findProjectRoot, injectProjectGuide } from "./project-context.mjs"
|
|
107
16
|
|
|
108
17
|
/**
|
|
109
18
|
* Build the mechanical review-object declaration block (AGENT-LOOP.md §18.8
|
|
@@ -130,7 +39,7 @@ export function buildObjectDeclarationBlock(object = null) {
|
|
|
130
39
|
}
|
|
131
40
|
|
|
132
41
|
/**
|
|
133
|
-
* Approval-signal block for design reviews (round 1 and round 2+ — §
|
|
42
|
+
* Approval-signal block for design reviews (round 1 and round 2+ — §11.2 D-24b:
|
|
134
43
|
* an async fix-round continuation must be able to re-approve, so the token is
|
|
135
44
|
* injected into EVERY design round; the reviewer echoes it only on a clean pass).
|
|
136
45
|
* §29.1 F2a (2026-09-07): BOTH values are injected — the token AND the designId
|
|
@@ -167,6 +76,17 @@ export function buildDesignApprovalBlock(designToken, designId) {
|
|
|
167
76
|
* @returns {string} the user message
|
|
168
77
|
*/
|
|
169
78
|
export function buildAdvisorUserMessage(agent, prior, reviewType, designToken = null, documents = null, paths = null, object = null, designId = null) {
|
|
79
|
+
const body = buildAdvisorUserMessageInner(agent, prior, reviewType, designToken, documents, paths, object, designId)
|
|
80
|
+
// B 构建自愈(F12/§14.4 #1):design + token 且输出不含逐字信号 ⇒ 尾包补齐 Approval Signal。
|
|
81
|
+
// 覆盖所有出口(含 code 形态分支降级态与 legacy 收敛分支)——既有分支语义零改:已在分支内
|
|
82
|
+
// 注入过的路径因 `[DESIGN-TOKEN:{token}` 逐字在场而不重复追加(幂等)。
|
|
83
|
+
if (reviewType !== "design" || !designToken) return body
|
|
84
|
+
if (body.includes(`[DESIGN-TOKEN:${designToken}`)) return body
|
|
85
|
+
return `${body}\n\n${buildDesignApprovalBlock(designToken, designId)}`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** 内层构建(无自愈尾包)——出口、分支与消息形态与拆分前逐字一致。 */
|
|
89
|
+
function buildAdvisorUserMessageInner(agent, prior, reviewType, designToken = null, documents = null, paths = null, object = null, designId = null) {
|
|
170
90
|
// prior = the full prior review output (string) when a convergence round is
|
|
171
91
|
// being built (decision 2026-08-08 — verbatim injection, model understands it).
|
|
172
92
|
// Deterministic: only _advisorRound > 0 with stored output counts.
|
|
@@ -194,6 +114,11 @@ export function buildAdvisorUserMessage(agent, prior, reviewType, designToken =
|
|
|
194
114
|
if (reviewType === "design" && (agent._advisorRound || 0) === 0) {
|
|
195
115
|
const repos = findReviewRepos(agent)
|
|
196
116
|
parts.push("## Design Review")
|
|
117
|
+
// FR15/P8: no git → change-set context is unavailable. Say it (never silent).
|
|
118
|
+
if (repos.length === 0) {
|
|
119
|
+
parts.push(NO_GIT_NOTICE)
|
|
120
|
+
parts.push("")
|
|
121
|
+
}
|
|
197
122
|
if (docList.length > 0) {
|
|
198
123
|
// Explicit review scope (engineering mode, FR2): the caller hands over the
|
|
199
124
|
// doc list — the advisor reviews ONLY these. No git-diff change-set
|
|
@@ -229,40 +154,24 @@ export function buildAdvisorUserMessage(agent, prior, reviewType, designToken =
|
|
|
229
154
|
}
|
|
230
155
|
}
|
|
231
156
|
|
|
232
|
-
// Engineering mode:
|
|
233
|
-
//
|
|
157
|
+
// Engineering mode: the project's DECLARED standards document
|
|
158
|
+
// (advisor.standardsDoc) — undeclared degrades visibly (PO-2).
|
|
234
159
|
if (agent.config?.agent?.engineering) {
|
|
235
|
-
|
|
236
|
-
const mpath = resolve(guideRoot ?? agent.cwd, "METHODOLOGY.md")
|
|
237
|
-
const methodology = readFileSync(mpath, "utf8")
|
|
238
|
-
parts.push("## Project Methodology")
|
|
239
|
-
parts.push("Evaluate the design against this methodology:")
|
|
240
|
-
parts.push(methodology)
|
|
241
|
-
parts.push("")
|
|
242
|
-
} catch { /* file doesn't exist — skip */ }
|
|
160
|
+
injectProjectStandards(agent, parts, guideRoot)
|
|
243
161
|
}
|
|
244
162
|
|
|
245
|
-
// Document map (docs/
|
|
246
|
-
//
|
|
247
|
-
// (
|
|
248
|
-
|
|
249
|
-
try {
|
|
250
|
-
const mapPath = resolve(guideRoot ?? agent.cwd, "docs", "design", "README.md")
|
|
251
|
-
if (existsSync(mapPath)) {
|
|
252
|
-
parts.push("## Document Map")
|
|
253
|
-
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.")
|
|
254
|
-
parts.push(readFileSync(mapPath, "utf8"))
|
|
255
|
-
parts.push("")
|
|
256
|
-
}
|
|
257
|
-
} catch { /* file doesn't exist or is unreadable — skip */ }
|
|
163
|
+
// Document map: declared path wins, the built-in fallback probe (docs/README.md
|
|
164
|
+
// → docs/design/README.md) stays; neither → explicit degradation sentence
|
|
165
|
+
// (never the old silent skip).
|
|
166
|
+
injectDocumentMap(agent, parts, guideRoot ?? agent.cwd)
|
|
258
167
|
|
|
259
168
|
parts.push("## Instructions")
|
|
260
169
|
if (docList.length > 0) {
|
|
261
|
-
parts.push("1. Read every document in the Documents to Review list in full — review ONLY those files.
|
|
170
|
+
parts.push("1. Read every document in the Documents to Review list in full — review ONLY those files.")
|
|
262
171
|
} else {
|
|
263
|
-
parts.push("1. Read the design document fully.
|
|
172
|
+
parts.push("1. Read the design document fully.")
|
|
264
173
|
}
|
|
265
|
-
parts.push("2. Review against: completeness (all requirements covered?), feasibility (can this be built?), methodology compliance (does it follow the project's
|
|
174
|
+
parts.push("2. Review against: completeness (all requirements covered?), feasibility (can this be built?), methodology compliance (does it follow the project's standards as provided?), clarity (specific enough?), acceptance criteria (verifiable?), scope (appropriate?).")
|
|
266
175
|
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?")
|
|
267
176
|
parts.push("4. Do NOT run git diff or look for code changes — there are none at this stage.")
|
|
268
177
|
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.")
|
|
@@ -338,17 +247,10 @@ export function buildAdvisorUserMessage(agent, prior, reviewType, designToken =
|
|
|
338
247
|
}
|
|
339
248
|
parts.push("")
|
|
340
249
|
|
|
341
|
-
// Engineering mode:
|
|
342
|
-
//
|
|
250
|
+
// Engineering mode: project standards (declaration-only, same helper as the
|
|
251
|
+
// design path) — undeclared degrades visibly (PO-2).
|
|
343
252
|
if (agent.config?.agent?.engineering) {
|
|
344
|
-
|
|
345
|
-
const mpath = resolve(guideRoot ?? agent.cwd, "METHODOLOGY.md")
|
|
346
|
-
const methodology = readFileSync(mpath, "utf8")
|
|
347
|
-
parts.push("## Project Methodology (Engineering Mode)")
|
|
348
|
-
parts.push("The project follows this methodology. Evaluate the changes against it:")
|
|
349
|
-
parts.push(methodology)
|
|
350
|
-
parts.push("")
|
|
351
|
-
} catch { /* file doesn't exist — skip */ }
|
|
253
|
+
injectProjectStandards(agent, parts, guideRoot)
|
|
352
254
|
}
|
|
353
255
|
|
|
354
256
|
// Instructions — round-aware: re-reviews skip convention discovery entirely.
|
|
@@ -0,0 +1,194 @@
|
|
|
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
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { execFileSync } from "node:child_process"
|
|
6
6
|
import { dirname, basename, resolve } from "node:path"
|
|
7
|
+
import { loadConventions, isCodePath } from "../conventions.mjs"
|
|
7
8
|
|
|
8
9
|
const GIT_TIMEOUT = 5_000
|
|
9
10
|
const MAX_EMBEDDED_DIFF = 50_000
|
|
@@ -97,32 +98,10 @@ export function collectChangedFiles(repos, cwd) {
|
|
|
97
98
|
return files
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
111
|
-
/** True when a path matches the doc/license pattern by extension or name.
|
|
112
|
-
* NOTE: this is extension-based only — it does NOT exclude src/ paths.
|
|
113
|
-
* Callers must separately check the src/ prefix for product-code semantics
|
|
114
|
-
* (e.g. src/prompts/*.md IS product code despite matching DOC_FILE).
|
|
115
|
-
* See isDocOnlyChange for the combined check. */
|
|
116
|
-
export function isDocFile(p) {
|
|
117
|
-
return DOC_FILE.test(p ?? "")
|
|
118
|
-
}
|
|
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
|
-
}
|
|
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).
|
|
126
105
|
|
|
127
106
|
/**
|
|
128
107
|
* True when this run mutated at least one CODE file. Shared single source of
|
|
@@ -132,24 +111,22 @@ export function isTempFile(p) {
|
|
|
132
111
|
* files (tmp-*, .tmp, .temp) are excluded too — a throwaway diagnostic script
|
|
133
112
|
* is not a code change. Mutations without a known path (tools outside
|
|
134
113
|
* FILE_MUTATORS) are treated as code — cannot tell, so guard conservatively.
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
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.
|
|
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.
|
|
140
117
|
*/
|
|
141
|
-
export function hasCodeMutations({ _touchedFiles, _mutatedThisRun }) {
|
|
118
|
+
export function hasCodeMutations({ _touchedFiles, _mutatedThisRun, cwd }) {
|
|
142
119
|
const files = _touchedFiles ?? []
|
|
143
120
|
if (files.length === 0) return _mutatedThisRun
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return files.some((p) => /(?:^|[\\/])src[\\/]/.test(p) || (!isTempFile(p) && !isDocFile(p)))
|
|
121
|
+
const conv = loadConventions(cwd)
|
|
122
|
+
return files.some((p) => isCodePath(p, conv))
|
|
147
123
|
}
|
|
148
124
|
|
|
149
125
|
/** True when all changed files across repos are documentation (md/txt/LICENSE etc.).
|
|
150
|
-
* Anything
|
|
151
|
-
*
|
|
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. */
|
|
152
128
|
export function isDocOnlyChange(repos, cwd) {
|
|
129
|
+
const conv = loadConventions(cwd)
|
|
153
130
|
const targets = repos.length > 0 ? repos : [cwd]
|
|
154
131
|
let sawChanges = false
|
|
155
132
|
for (const repo of targets) {
|
|
@@ -164,9 +141,9 @@ export function isDocOnlyChange(repos, cwd) {
|
|
|
164
141
|
for (const line of status.split("\n")) {
|
|
165
142
|
// porcelain: "XY path" or "XY old -> new" (rename)
|
|
166
143
|
const filePath = line.slice(3).split(" -> ").pop().replace(/^"|"$/g, "")
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
if (
|
|
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
|
|
170
147
|
}
|
|
171
148
|
}
|
|
172
149
|
return sawChanges
|