thincoder 0.12.62 → 0.12.64
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 +27 -0
- package/README.md +46 -49
- package/bin/thincoder.mjs +68 -34
- package/package.json +7 -6
- 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 +257 -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 +60 -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 +35 -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-plan.mjs +9 -0
- 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 +18 -10
- package/src/tui/interaction.mjs +16 -7
- package/src/tui/key-modes.mjs +9 -4
- package/src/tui/ledger-surface.mjs +22 -59
- 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 +15 -6
- 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 +57 -23
- package/src/tui/tool-events.mjs +11 -8
- package/src/tui/tui-lifecycle.mjs +9 -0
- package/src/tui/wizard.mjs +3 -3
- package/src/tui/wrapped-spawn.mjs +6 -2
- 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
package/src/advisor/messages.mjs
DELETED
|
@@ -1,299 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* advisor/messages.mjs — advisor user-message building (buildAdvisorUserMessage).
|
|
3
|
-
* Split out of advisor.mjs to keep it under the 300-line advisory threshold
|
|
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.
|
|
7
|
-
*/
|
|
8
|
-
import { join, relative, sep } from "node:path"
|
|
9
|
-
import { findReviewRepos, collectRepoSnapshots, collectChangedFiles } from "./repos.mjs"
|
|
10
|
-
import { buildConvergenceBody, buildConvergenceInstructions } from "./convergence.mjs"
|
|
11
|
-
import { loadAdvisorMd, extractConversationBackground, extractAgentResponseTable } from "./history.mjs"
|
|
12
|
-
import { injectProjectGuide, injectDocumentMap, injectProjectStandards, NO_GIT_NOTICE } from "./project-context.mjs"
|
|
13
|
-
|
|
14
|
-
// Structural split (not an authority migration): the moved helpers stay reachable here.
|
|
15
|
-
export { findProjectRoot, injectProjectGuide } from "./project-context.mjs"
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Build the mechanical review-object declaration block (AGENT-LOOP.md §18.8
|
|
19
|
-
* D-OA2 — English anchored form). Injected at the START of the review user
|
|
20
|
-
* message every round: round 1 (design + code), the legacy convergence path,
|
|
21
|
-
* and the round-2+ follow-up (see buildAdvisorFollowUp) — the reviewer must
|
|
22
|
-
* not re-derive "who is being reviewed / why" from the documents (T-OA2:
|
|
23
|
-
* every round stays anchored). Absent object → "" (legacy calls degrade to
|
|
24
|
-
* the current behavior — T-OA3).
|
|
25
|
-
* @param {Object|null} [object] — { type, target, status, reason, exclude }
|
|
26
|
-
* (strings; `exclude` may also be a list — joined with ", ")
|
|
27
|
-
* @returns {string} the declaration block (empty when no object)
|
|
28
|
-
*/
|
|
29
|
-
export function buildObjectDeclarationBlock(object = null) {
|
|
30
|
-
if (!object || typeof object !== "object" || Array.isArray(object)) return ""
|
|
31
|
-
const field = (v) => (Array.isArray(v) ? v.join(", ") : v == null ? "" : String(v))
|
|
32
|
-
return [
|
|
33
|
-
"## Review-object declaration (mechanical — do not infer)",
|
|
34
|
-
`Review type: ${field(object.type)} | Target: ${field(object.target)} | Object state: ${field(object.status)} | Trigger: ${field(object.reason)}`,
|
|
35
|
-
`Excluded (not in this review): ${field(object.exclude)}`,
|
|
36
|
-
"Follow this declaration — do not infer the review target from the documents.",
|
|
37
|
-
"",
|
|
38
|
-
].join("\n")
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Approval-signal block for design reviews (round 1 and round 2+ — §11.2 D-24b:
|
|
43
|
-
* an async fix-round continuation must be able to re-approve, so the token is
|
|
44
|
-
* injected into EVERY design round; the reviewer echoes it only on a clean pass).
|
|
45
|
-
* §29.1 F2a (2026-09-07): BOTH values are injected — the token AND the designId
|
|
46
|
-
* (anchor sentence verbatim — Copy BOTH values). designId null (legacy direct
|
|
47
|
-
* callers without a resolved instance) degrades to the token-only form.
|
|
48
|
-
*/
|
|
49
|
-
export function buildDesignApprovalBlock(designToken, designId) {
|
|
50
|
-
const echo = designId
|
|
51
|
-
? `If — and ONLY if — your review finds NO 🔴 (Critical) issues, end your reply with this exact token: [DESIGN-TOKEN:${designToken}] and this exact designId: ${designId}. Copy BOTH values verbatim.`
|
|
52
|
-
: `If — and ONLY if — your review finds NO 🔴 (Critical) issues, end your reply with this exact token: [DESIGN-TOKEN:${designToken}]`
|
|
53
|
-
return [
|
|
54
|
-
"## Approval Signal",
|
|
55
|
-
echo,
|
|
56
|
-
"🟡 (Advisory) and 🔵 (Note) findings do NOT block approval — list them if present, but still include the token. If there are any 🔴 issues, do NOT include the token.",
|
|
57
|
-
].join("\n")
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Build the user message for an advisor review session.
|
|
62
|
-
* @param {Object} agent — the parent agent
|
|
63
|
-
* @param {Object|null} [prior] — prior issue table
|
|
64
|
-
* @param {string} [reviewType] — "design" or "code" (default)
|
|
65
|
-
* @param {string|null} [designToken] — token injected into the design-review prompt; the advisor echoes it only on approval
|
|
66
|
-
* @param {string[]|null} [documents] — design review only: explicit list of doc paths to review (requirements + design + referenced docs).
|
|
67
|
-
* When set, the review input is built from this list ONLY — no git-diff change-set collection.
|
|
68
|
-
* When absent, the legacy git-diff-based scope is kept (backward compatible).
|
|
69
|
-
* @param {string[]|null} [paths] — code review only: explicit list of file/dir paths to review (deduped; shown under Review Scope)
|
|
70
|
-
* @param {Object|null} [object] — review-object declaration (§18.8 D-OA1/D-OA3):
|
|
71
|
-
* { type, target, status, reason, exclude } — mechanically injected at the
|
|
72
|
-
* start of the user message; absent → no injection (legacy calls unchanged).
|
|
73
|
-
* @param {string|null} [designId] — §29.1 F2a: injected next to the token in the
|
|
74
|
-
* Approval Signal (both values — the reviewer copies both verbatim); null →
|
|
75
|
-
* token-only degradation (legacy direct callers).
|
|
76
|
-
* @returns {string} the user message
|
|
77
|
-
*/
|
|
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) {
|
|
90
|
-
// prior = the full prior review output (string) when a convergence round is
|
|
91
|
-
// being built (decision 2026-08-08 — verbatim injection, model understands it).
|
|
92
|
-
// Deterministic: only _advisorRound > 0 with stored output counts.
|
|
93
|
-
const p = prior ?? ((agent._advisorRound || 0) > 0 ? agent._lastAdvisorOutput : null)
|
|
94
|
-
|
|
95
|
-
const parts = []
|
|
96
|
-
// Review-object declaration FIRST — D-OA1: at the start of the user message
|
|
97
|
-
// (after the system prompt, before the review content). Covers round 1
|
|
98
|
-
// design/code and the legacy convergence path; the round-2+ normal path
|
|
99
|
-
// prepends it in buildAdvisorFollowUp (T-OA2 — every round stays anchored).
|
|
100
|
-
const declaration = buildObjectDeclarationBlock(object)
|
|
101
|
-
if (declaration) parts.push(declaration)
|
|
102
|
-
const docList = Array.isArray(documents) ? documents.filter((d) => typeof d === "string" && d.trim()) : []
|
|
103
|
-
const pathList = Array.isArray(paths) ? [...new Set(paths.filter((p) => typeof p === "string" && p.trim()))] : []
|
|
104
|
-
|
|
105
|
-
// Project guide FIRST in EVERY review path (code AND design round 0): the map
|
|
106
|
-
// to the requirements docs is needed for design reviews too (design must fit
|
|
107
|
-
// the requirements, not just the methodology). Design round 0 early-returns
|
|
108
|
-
// below — the guide must be injected before that return. Project root is
|
|
109
|
-
// discovered from the review scope (code paths AND design-doc paths, so a
|
|
110
|
-
// documents-only design review still finds the subproject guide).
|
|
111
|
-
const guideRoot = injectProjectGuide(agent, parts, [...pathList, ...docList])
|
|
112
|
-
|
|
113
|
-
// Design review: simplified message — focus on the design doc, not code
|
|
114
|
-
if (reviewType === "design" && (agent._advisorRound || 0) === 0) {
|
|
115
|
-
const repos = findReviewRepos(agent)
|
|
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
|
-
}
|
|
122
|
-
if (docList.length > 0) {
|
|
123
|
-
// Explicit review scope (engineering mode, FR2): the caller hands over the
|
|
124
|
-
// doc list — the advisor reviews ONLY these. No git-diff change-set
|
|
125
|
-
// collection: diff-based discovery reviewed unrelated files, and untracked
|
|
126
|
-
// design docs were invisible to git diff anyway (ENGINEERING-MODE.md §2.4).
|
|
127
|
-
parts.push("The documents below are the review scope. Review ONLY these files — do not scan git diff or read any other files.")
|
|
128
|
-
parts.push("")
|
|
129
|
-
parts.push("## Documents to Review")
|
|
130
|
-
parts.push(docList.map((d) => `- ${d} — Read this file in full`).join("\n"))
|
|
131
|
-
parts.push("")
|
|
132
|
-
} else {
|
|
133
|
-
// Backward-compatible fallback (no documents): discover docs via git status/diff.
|
|
134
|
-
parts.push("The following changes are a design document. Review it against the project's methodology.")
|
|
135
|
-
parts.push("")
|
|
136
|
-
|
|
137
|
-
// List changed file paths explicitly — new design docs are untracked,
|
|
138
|
-
// so git diff HEAD won't show their content; the advisor must read the file itself
|
|
139
|
-
const changedFiles = collectChangedFiles(repos, agent.cwd)
|
|
140
|
-
if (changedFiles.length > 0) {
|
|
141
|
-
parts.push("## Changed Files")
|
|
142
|
-
parts.push(changedFiles.map((f) => `- ${f}`).join("\n"))
|
|
143
|
-
parts.push("")
|
|
144
|
-
parts.push("Read each changed file in full — untracked files are not shown in the diff below.")
|
|
145
|
-
parts.push("")
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Pre-collected changes — the design doc diff.
|
|
149
|
-
const snapshots = collectRepoSnapshots(repos, agent.cwd)
|
|
150
|
-
if (snapshots.length > 0) {
|
|
151
|
-
parts.push("## Design Document (git diff)")
|
|
152
|
-
parts.push(...snapshots)
|
|
153
|
-
parts.push("")
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Engineering mode: the project's DECLARED standards document
|
|
158
|
-
// (advisor.standardsDoc) — undeclared degrades visibly (PO-2).
|
|
159
|
-
if (agent.config?.agent?.engineering) {
|
|
160
|
-
injectProjectStandards(agent, parts, guideRoot)
|
|
161
|
-
}
|
|
162
|
-
|
|
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)
|
|
167
|
-
|
|
168
|
-
parts.push("## Instructions")
|
|
169
|
-
if (docList.length > 0) {
|
|
170
|
-
parts.push("1. Read every document in the Documents to Review list in full — review ONLY those files.")
|
|
171
|
-
} else {
|
|
172
|
-
parts.push("1. Read the design document fully.")
|
|
173
|
-
}
|
|
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?).")
|
|
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?")
|
|
176
|
-
parts.push("4. Do NOT run git diff or look for code changes — there are none at this stage.")
|
|
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.")
|
|
178
|
-
if (designToken) {
|
|
179
|
-
parts.push("")
|
|
180
|
-
parts.push(buildDesignApprovalBlock(designToken, designId))
|
|
181
|
-
}
|
|
182
|
-
return parts.join("\n")
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Convergence data (round 2+). LEGACY COMPATIBILITY PATH: the normal advisor
|
|
186
|
-
// flow routes convergence rounds through buildAdvisorFollowUp (fresh session,
|
|
187
|
-
// decision d698434); this block only fires for direct external callers of
|
|
188
|
-
// buildAdvisorUserMessage with a stored prior review output. Kept to avoid
|
|
189
|
-
// breaking those. Same rule as buildAdvisorFollowUp: the FULL prior review
|
|
190
|
-
// output is injected verbatim (decision 2026-08-08 — the model understands it;
|
|
191
|
-
// no table/header/phrase parsing).
|
|
192
|
-
// NOTE: this legacy path does NOT apply escapeLiteralEscapes (that lives in
|
|
193
|
-
// advisor.mjs and importing it here would create a top-level module cycle).
|
|
194
|
-
// Direct callers must escape the injected output themselves if the parent
|
|
195
|
-
// conversation can quote literal "\x"/"\u" sequences (server 400 risk).
|
|
196
|
-
if (p && (agent._advisorRound || 0) > 0) {
|
|
197
|
-
const scopeFiles = resolveScopeFiles(agent, paths)
|
|
198
|
-
const response = extractAgentResponseTable(agent.history)
|
|
199
|
-
|| (scopeFiles?.length
|
|
200
|
-
? "(Agent did not provide a response table — perform a fresh review of: " + scopeFiles.slice(0, 10).join(", ") + ")"
|
|
201
|
-
: "(Agent did not provide a response table — perform a fresh review of the files named in the system prompt context)")
|
|
202
|
-
const round = (agent._advisorRound || 0) + 1
|
|
203
|
-
parts.push(buildConvergenceBody(p, response, round, scopeFiles))
|
|
204
|
-
parts.push("")
|
|
205
|
-
parts.push("---")
|
|
206
|
-
parts.push("")
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (pathList.length > 0 || docList.length > 0) {
|
|
210
|
-
parts.push("## Review Scope")
|
|
211
|
-
}
|
|
212
|
-
if (pathList.length > 0) {
|
|
213
|
-
parts.push("Review these code files/directories — read them in full for context:")
|
|
214
|
-
parts.push("")
|
|
215
|
-
parts.push(pathList.map((p) => `- ${p}`).join("\n"))
|
|
216
|
-
parts.push("")
|
|
217
|
-
}
|
|
218
|
-
if (docList.length > 0) {
|
|
219
|
-
if (reviewType === "design") {
|
|
220
|
-
parts.push("The documents below are the review scope. Review ONLY these files — do NOT scan git diff or read any other files.")
|
|
221
|
-
} else {
|
|
222
|
-
parts.push("The documents below define acceptance criteria and review context. Read them for context, then read the code files specified in the review scope. Judge the implementation against these documents.")
|
|
223
|
-
}
|
|
224
|
-
parts.push("")
|
|
225
|
-
parts.push("## Documents to Review")
|
|
226
|
-
parts.push(docList.map((d) => `- ${d} — Read this file in full`).join("\n"))
|
|
227
|
-
parts.push("")
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Conversation background — recent user↔assistant exchanges for intent context
|
|
231
|
-
const background = extractConversationBackground(agent.history)
|
|
232
|
-
if (background) {
|
|
233
|
-
parts.push("## Conversation Background (recent turns)")
|
|
234
|
-
parts.push(background)
|
|
235
|
-
parts.push("")
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Review criteria
|
|
239
|
-
const criteria = loadAdvisorMd(agent.cwd)
|
|
240
|
-
parts.push("## Review Criteria")
|
|
241
|
-
parts.push(criteria)
|
|
242
|
-
if (guideRoot) {
|
|
243
|
-
// Requirement-fit is a first-class dimension when the project guide was
|
|
244
|
-
// found — the criteria file (advisor.md) may not mention it (legacy).
|
|
245
|
-
parts.push("")
|
|
246
|
-
parts.push("Additional criterion: **requirement fit** — does the implementation match what the requirements documents (referenced by the Project Guide above) actually ask for?")
|
|
247
|
-
}
|
|
248
|
-
parts.push("")
|
|
249
|
-
|
|
250
|
-
// Engineering mode: project standards (declaration-only, same helper as the
|
|
251
|
-
// design path) — undeclared degrades visibly (PO-2).
|
|
252
|
-
if (agent.config?.agent?.engineering) {
|
|
253
|
-
injectProjectStandards(agent, parts, guideRoot)
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Instructions — round-aware: re-reviews skip convention discovery entirely.
|
|
257
|
-
// These are SUPPLEMENTARY reminders to the system prompt's numbered workflow —
|
|
258
|
-
// deliberately not renumbered as a competing sequence.
|
|
259
|
-
const isReReview = p && (agent._advisorRound || 0) > 0
|
|
260
|
-
parts.push("## Instructions")
|
|
261
|
-
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.")
|
|
262
|
-
if (isReReview) {
|
|
263
|
-
const round = (agent._advisorRound || 0) + 1
|
|
264
|
-
parts.push(...buildConvergenceInstructions(round, pathList))
|
|
265
|
-
} else {
|
|
266
|
-
parts.push("2. " + (guideRoot
|
|
267
|
-
? "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."
|
|
268
|
-
: "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."))
|
|
269
|
-
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.")
|
|
270
|
-
parts.push("4. Use `grep` or `lsp` to trace callers, imports, and dependencies — only where the diff leaves genuine doubt.")
|
|
271
|
-
parts.push("5. Produce your review table based on the review criteria above. Do not re-read content you already have.")
|
|
272
|
-
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.")
|
|
273
|
-
}
|
|
274
|
-
parts.push("")
|
|
275
|
-
parts.push("Return your review as a markdown table (or a clear statement that everything is fine).")
|
|
276
|
-
|
|
277
|
-
return parts.join("\n")
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Resolve the review surface for the convergence fallback: explicit `paths`
|
|
282
|
-
* win; otherwise the runtime mutation record (_touchedFiles, ABSOLUTE) is
|
|
283
|
-
* normalized to cwd-relative so the fallback list matches the relative-path
|
|
284
|
-
* norm the reviewer sees everywhere else. Paths outside cwd are relativized
|
|
285
|
-
* with path.relative — never a mixed absolute/relative list.
|
|
286
|
-
*/
|
|
287
|
-
export function resolveScopeFiles(agent, paths) {
|
|
288
|
-
const normalize = (p) => {
|
|
289
|
-
// sep-guarded prefix check — /proj vs /project-other must not collide
|
|
290
|
-
const abs = p === agent.cwd || p.startsWith(agent.cwd + sep) ? p : join(agent.cwd, p)
|
|
291
|
-
return relative(agent.cwd, abs)
|
|
292
|
-
}
|
|
293
|
-
if (Array.isArray(paths)) return [...new Set(paths.map(normalize))]
|
|
294
|
-
if (agent._touchedFiles?.length) {
|
|
295
|
-
return [...new Set(agent._touchedFiles.map(normalize))]
|
|
296
|
-
}
|
|
297
|
-
return null
|
|
298
|
-
}
|
|
299
|
-
|
|
@@ -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
|
-
}
|