thincoder 0.12.11 → 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/README.md +8 -0
- package/bin/thincoder.mjs +11 -1
- package/package.json +1 -1
- package/src/acp/bridge.mjs +229 -0
- package/src/acp/session.mjs +46 -0
- package/src/acp/transport.mjs +155 -0
- package/src/acp.mjs +335 -0
- package/src/advisor/citations.mjs +77 -0
- package/src/advisor/convergence.mjs +80 -0
- package/src/advisor/history.mjs +20 -55
- package/src/advisor/messages.mjs +182 -36
- package/src/advisor/repos.mjs +40 -0
- package/src/advisor/run.mjs +201 -95
- package/src/advisor.mjs +192 -99
- package/src/agent/completion.mjs +10 -10
- package/src/agent/dispatch.mjs +14 -0
- package/src/agent-tools/advisor.mjs +11 -10
- package/src/agent-tools/subagent.mjs +26 -8
- package/src/agent.mjs +25 -32
- package/src/config.mjs +13 -5
- package/src/context.mjs +32 -9
- package/src/prompts/advisor-round1.md +9 -3
- package/src/prompts/advisor-round2.md +10 -8
- package/src/prompts/advisor-round3.md +14 -14
- package/src/prompts/discipline.md +1 -1
- package/src/session.mjs +19 -0
- package/src/tools/file.mjs +30 -0
- package/src/tools/insert_after.md +1 -0
- package/src/tools/patch.mjs +4 -0
- package/src/tools/shared.mjs +68 -57
- package/src/tui/agent-turn.mjs +73 -121
- package/src/tui/index.mjs +1 -1
- package/src/tui/markdown.mjs +26 -8
- package/src/tui/render-conversation.mjs +90 -39
- package/src/tui/tool-summaries.mjs +113 -0
package/src/advisor/messages.mjs
CHANGED
|
@@ -3,31 +3,138 @@
|
|
|
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 } 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.
|
|
13
108
|
* @param {Object} agent — the parent agent
|
|
14
|
-
* @param {Object|null} [
|
|
109
|
+
* @param {Object|null} [prior] — prior issue table
|
|
15
110
|
* @param {string} [reviewType] — "design" or "code" (default)
|
|
16
111
|
* @param {string|null} [designToken] — token injected into the design-review prompt; the advisor echoes it only on approval
|
|
17
112
|
* @param {string[]|null} [documents] — design review only: explicit list of doc paths to review (requirements + design + referenced docs).
|
|
18
113
|
* When set, the review input is built from this list ONLY — no git-diff change-set collection.
|
|
19
114
|
* When absent, the legacy git-diff-based scope is kept (backward compatible).
|
|
115
|
+
* @param {string[]|null} [paths] — code review only: explicit list of file/dir paths to review (deduped; shown under Review Scope)
|
|
20
116
|
* @returns {string} the user message
|
|
21
117
|
*/
|
|
22
|
-
export function buildAdvisorUserMessage(agent,
|
|
23
|
-
|
|
118
|
+
export function buildAdvisorUserMessage(agent, prior, reviewType, designToken = null, documents = null, paths = null) {
|
|
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)
|
|
24
123
|
|
|
25
124
|
const parts = []
|
|
26
125
|
const docList = Array.isArray(documents) ? documents.filter((d) => typeof d === "string" && d.trim()) : []
|
|
27
|
-
const pathList = Array.isArray(paths) ? paths.filter((p) => typeof p === "string" && p.trim()) : []
|
|
126
|
+
const pathList = Array.isArray(paths) ? [...new Set(paths.filter((p) => typeof p === "string" && p.trim()))] : []
|
|
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])
|
|
28
135
|
|
|
29
136
|
// Design review: simplified message — focus on the design doc, not code
|
|
30
|
-
if (reviewType === "design") {
|
|
137
|
+
if (reviewType === "design" && (agent._advisorRound || 0) === 0) {
|
|
31
138
|
const repos = findReviewRepos(agent)
|
|
32
139
|
parts.push("## Design Review")
|
|
33
140
|
if (docList.length > 0) {
|
|
@@ -65,10 +172,11 @@ export function buildAdvisorUserMessage(agent, _prior, reviewType, designToken =
|
|
|
65
172
|
}
|
|
66
173
|
}
|
|
67
174
|
|
|
68
|
-
// 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)
|
|
69
177
|
if (agent.config?.agent?.engineering) {
|
|
70
178
|
try {
|
|
71
|
-
const mpath = resolve(agent.cwd, "METHODOLOGY.md")
|
|
179
|
+
const mpath = resolve(guideRoot ?? agent.cwd, "METHODOLOGY.md")
|
|
72
180
|
const methodology = readFileSync(mpath, "utf8")
|
|
73
181
|
parts.push("## Project Methodology")
|
|
74
182
|
parts.push("Evaluate the design against this methodology:")
|
|
@@ -84,8 +192,9 @@ export function buildAdvisorUserMessage(agent, _prior, reviewType, designToken =
|
|
|
84
192
|
parts.push("1. Read the design document fully. Read METHODOLOGY.md to understand the project's standards.")
|
|
85
193
|
}
|
|
86
194
|
parts.push("2. Review against: completeness (all requirements covered?), feasibility (can this be built?), clarity (specific enough?), acceptance criteria (verifiable?), scope (appropriate?).")
|
|
87
|
-
parts.push("3.
|
|
88
|
-
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.")
|
|
89
198
|
if (designToken) {
|
|
90
199
|
parts.push("")
|
|
91
200
|
parts.push("## Approval Signal")
|
|
@@ -95,25 +204,33 @@ export function buildAdvisorUserMessage(agent, _prior, reviewType, designToken =
|
|
|
95
204
|
return parts.join("\n")
|
|
96
205
|
}
|
|
97
206
|
|
|
98
|
-
// Convergence data (round 2+).
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
207
|
+
// Convergence data (round 2+). LEGACY COMPATIBILITY PATH: the normal advisor
|
|
208
|
+
// flow routes convergence rounds through buildAdvisorFollowUp (fresh session,
|
|
209
|
+
// decision d698434); this block only fires for direct external callers of
|
|
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).
|
|
218
|
+
if (p && (agent._advisorRound || 0) > 0) {
|
|
219
|
+
const scopeFiles = resolveScopeFiles(agent, paths)
|
|
220
|
+
const response = extractAgentResponseTable(agent.history)
|
|
221
|
+
|| (scopeFiles?.length
|
|
222
|
+
? "(Agent did not provide a response table — perform a fresh review of: " + scopeFiles.slice(0, 10).join(", ") + ")"
|
|
223
|
+
: "(Agent did not provide a response table — perform a fresh review of the files named in the system prompt context)")
|
|
102
224
|
const round = (agent._advisorRound || 0) + 1
|
|
103
|
-
|
|
104
|
-
parts.push(`## Round ${round} — ${label}`)
|
|
105
|
-
parts.push("")
|
|
106
|
-
parts.push("## Prior Issue Table")
|
|
107
|
-
parts.push(prior.text)
|
|
108
|
-
parts.push("")
|
|
109
|
-
parts.push("## Agent Response")
|
|
110
|
-
parts.push(response)
|
|
225
|
+
parts.push(buildConvergenceBody(p, response, round, scopeFiles))
|
|
111
226
|
parts.push("")
|
|
112
227
|
parts.push("---")
|
|
113
228
|
parts.push("")
|
|
114
229
|
}
|
|
115
230
|
|
|
116
|
-
|
|
231
|
+
if (pathList.length > 0 || docList.length > 0) {
|
|
232
|
+
parts.push("## Review Scope")
|
|
233
|
+
}
|
|
117
234
|
if (pathList.length > 0) {
|
|
118
235
|
parts.push("Review these code files/directories — read them in full for context:")
|
|
119
236
|
parts.push("")
|
|
@@ -144,12 +261,19 @@ export function buildAdvisorUserMessage(agent, _prior, reviewType, designToken =
|
|
|
144
261
|
const criteria = loadAdvisorMd(agent.cwd)
|
|
145
262
|
parts.push("## Review Criteria")
|
|
146
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
|
+
}
|
|
147
270
|
parts.push("")
|
|
148
271
|
|
|
149
272
|
// Engineering mode: inject project methodology so advisor knows the rules
|
|
273
|
+
// (resolved from the DISCOVERED project root — subproject in a monorepo)
|
|
150
274
|
if (agent.config?.agent?.engineering) {
|
|
151
275
|
try {
|
|
152
|
-
const mpath = resolve(agent.cwd, "METHODOLOGY.md")
|
|
276
|
+
const mpath = resolve(guideRoot ?? agent.cwd, "METHODOLOGY.md")
|
|
153
277
|
const methodology = readFileSync(mpath, "utf8")
|
|
154
278
|
parts.push("## Project Methodology (Engineering Mode)")
|
|
155
279
|
parts.push("The project follows this methodology. Evaluate the changes against it:")
|
|
@@ -158,19 +282,20 @@ export function buildAdvisorUserMessage(agent, _prior, reviewType, designToken =
|
|
|
158
282
|
} catch { /* file doesn't exist — skip */ }
|
|
159
283
|
}
|
|
160
284
|
|
|
161
|
-
// Instructions — round-aware: re-reviews skip convention discovery entirely
|
|
162
|
-
|
|
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.
|
|
288
|
+
const isReReview = p && (agent._advisorRound || 0) > 0
|
|
163
289
|
parts.push("## Instructions")
|
|
164
|
-
parts.push("1. IMPORTANT:
|
|
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.")
|
|
165
291
|
if (isReReview) {
|
|
166
|
-
|
|
167
|
-
parts.push(
|
|
168
|
-
parts.push("4. `read` the files in the Review Scope in full — ALWAYS, regardless of what `git diff` shows. Batch reads/greps in a single reply.")
|
|
169
|
-
parts.push("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 come from the stale prior table. Findings without a fresh quoted line are treated as unverified and will not be accepted.")
|
|
170
|
-
parts.push("6. Produce your verification table. Do not re-read content you already have.")
|
|
292
|
+
const round = (agent._advisorRound || 0) + 1
|
|
293
|
+
parts.push(...buildConvergenceInstructions(round, pathList))
|
|
171
294
|
} else {
|
|
172
|
-
parts.push("2.
|
|
173
|
-
|
|
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."))
|
|
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.")
|
|
174
299
|
parts.push("4. Use `grep` or `lsp` to trace callers, imports, and dependencies — only where the diff leaves genuine doubt.")
|
|
175
300
|
parts.push("5. Produce your review table based on the review criteria above. Do not re-read content you already have.")
|
|
176
301
|
parts.push("6. You may also flag other issues: crashes, data loss, logic errors — anything obvious. This is the convergence protocol: round 1 is the full review, later rounds only re-verify.")
|
|
@@ -180,3 +305,24 @@ export function buildAdvisorUserMessage(agent, _prior, reviewType, designToken =
|
|
|
180
305
|
|
|
181
306
|
return parts.join("\n")
|
|
182
307
|
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Resolve the review surface for the convergence fallback: explicit `paths`
|
|
311
|
+
* win; otherwise the runtime mutation record (_touchedFiles, ABSOLUTE) is
|
|
312
|
+
* normalized to cwd-relative so the fallback list matches the relative-path
|
|
313
|
+
* norm the reviewer sees everywhere else. Paths outside cwd are relativized
|
|
314
|
+
* with path.relative — never a mixed absolute/relative list.
|
|
315
|
+
*/
|
|
316
|
+
export function resolveScopeFiles(agent, paths) {
|
|
317
|
+
const normalize = (p) => {
|
|
318
|
+
// sep-guarded prefix check — /proj vs /project-other must not collide
|
|
319
|
+
const abs = p === agent.cwd || p.startsWith(agent.cwd + sep) ? p : join(agent.cwd, p)
|
|
320
|
+
return relative(agent.cwd, abs)
|
|
321
|
+
}
|
|
322
|
+
if (Array.isArray(paths)) return [...new Set(paths.map(normalize))]
|
|
323
|
+
if (agent._touchedFiles?.length) {
|
|
324
|
+
return [...new Set(agent._touchedFiles.map(normalize))]
|
|
325
|
+
}
|
|
326
|
+
return null
|
|
327
|
+
}
|
|
328
|
+
|
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
|