@thincoder/core 0.9.1
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/abort-provenance.mjs +116 -0
- package/advisor/citations.mjs +139 -0
- package/advisor/compaction.mjs +174 -0
- package/advisor/convergence.mjs +80 -0
- package/advisor/history.mjs +77 -0
- package/advisor/loop.mjs +288 -0
- package/advisor/messages.mjs +299 -0
- package/advisor/notice.mjs +141 -0
- package/advisor/project-context.mjs +197 -0
- package/advisor/repos.mjs +150 -0
- package/advisor/run.mjs +190 -0
- package/advisor/truncate.mjs +57 -0
- package/advisor.mjs +281 -0
- package/agent/child-marks.mjs +24 -0
- package/agent/completion.mjs +145 -0
- package/agent/dispatch.mjs +493 -0
- package/agent/family-tools.mjs +174 -0
- package/agent/helpers.mjs +412 -0
- package/agent/post-turn.mjs +70 -0
- package/agent/record-results.mjs +174 -0
- package/agent/relay-prefix.mjs +39 -0
- package/agent/run-stages.mjs +244 -0
- package/agent/setup-reminders.mjs +199 -0
- package/agent/setup.mjs +234 -0
- package/agent/spawn-child.mjs +258 -0
- package/agent/suspension.mjs +240 -0
- package/agent/write-gate.mjs +87 -0
- package/agent-tools/advisor-async.mjs +481 -0
- package/agent-tools/advisor-settle.mjs +240 -0
- package/agent-tools/advisor.mjs +280 -0
- package/agent-tools/async-discard.mjs +143 -0
- package/agent-tools/async-settle.mjs +299 -0
- package/agent-tools/batch-segment.mjs +265 -0
- package/agent-tools/child-permission.mjs +45 -0
- package/agent-tools/consult.mjs +471 -0
- package/agent-tools/design-token.mjs +117 -0
- package/agent-tools/digest-budget.mjs +76 -0
- package/agent-tools/eng.mjs +102 -0
- package/agent-tools/escalate-async.mjs +302 -0
- package/agent-tools/goal.mjs +119 -0
- package/agent-tools/panel-blocks.mjs +24 -0
- package/agent-tools/parent-channel.mjs +231 -0
- package/agent-tools/plan.mjs +86 -0
- package/agent-tools/read-history.mjs +309 -0
- package/agent-tools/recent-changes.mjs +24 -0
- package/agent-tools/review-facts.mjs +31 -0
- package/agent-tools/settings.mjs +268 -0
- package/agent-tools/skill.mjs +63 -0
- package/agent-tools/spawn-gates.mjs +109 -0
- package/agent-tools/subagent-actions.mjs +496 -0
- package/agent-tools/subagent-async.mjs +456 -0
- package/agent-tools/subagent-panel.mjs +160 -0
- package/agent-tools/subagent-run.mjs +208 -0
- package/agent-tools/subagent-scheduler.mjs +446 -0
- package/agent-tools/subagent-spawn.mjs +478 -0
- package/agent-tools/subagent.mjs +419 -0
- package/agent-tools/task.mjs +87 -0
- package/agent-tools/timer.mjs +46 -0
- package/agent-tools/verify.mjs +295 -0
- package/agent-tools.mjs +23 -0
- package/agent.mjs +430 -0
- package/auto-think.mjs +115 -0
- package/compress-form.mjs +24 -0
- package/config-io.mjs +277 -0
- package/config-migrate.mjs +178 -0
- package/config-presets.mjs +49 -0
- package/config.mjs +419 -0
- package/context.mjs +495 -0
- package/conventions.mjs +223 -0
- package/embedding.mjs +120 -0
- package/escape.mjs +152 -0
- package/expand-home.mjs +16 -0
- package/explore-distill.mjs +152 -0
- package/generate-title.mjs +123 -0
- package/git/checkpoint.mjs +448 -0
- package/git/gitmem.mjs +100 -0
- package/history-window.mjs +179 -0
- package/hooks.mjs +108 -0
- package/i18n.mjs +106 -0
- package/index-bin.mjs +48 -0
- package/index-discover.mjs +176 -0
- package/ledger-cmd.mjs +209 -0
- package/ledger-db.mjs +86 -0
- package/ledger-surface.mjs +76 -0
- package/ledger.mjs +202 -0
- package/log.mjs +195 -0
- package/manifest.mjs +338 -0
- package/markdown.mjs +106 -0
- package/mcp/helpers.mjs +51 -0
- package/mcp/transport-http.mjs +248 -0
- package/mcp/transport-stdio.mjs +140 -0
- package/mcp/transport-ws.mjs +122 -0
- package/mcp.mjs +295 -0
- package/memory/code-index.mjs +219 -0
- package/memory/code-sync.mjs +427 -0
- package/memory/core.mjs +318 -0
- package/memory/delete.mjs +242 -0
- package/memory/docs.mjs +431 -0
- package/memory/file-walk.mjs +109 -0
- package/memory/origin.mjs +24 -0
- package/memory/scan.mjs +177 -0
- package/memory/schema.mjs +460 -0
- package/memory.mjs +21 -0
- package/model-ref.mjs +66 -0
- package/model-specs.mjs +277 -0
- package/package.json +32 -0
- package/peer-domains.mjs +265 -0
- package/peer-instances.mjs +178 -0
- package/permission.mjs +79 -0
- package/process-probe.mjs +315 -0
- package/prompt-files.mjs +113 -0
- package/prompt-overlays.mjs +78 -0
- package/prompts/advisor-design.md +43 -0
- package/prompts/advisor-round1.md +41 -0
- package/prompts/advisor-round2.md +46 -0
- package/prompts/advisor-round3.md +42 -0
- package/prompts/common.md +158 -0
- package/prompts/consult-base.md +19 -0
- package/prompts/discipline-engineering.md +123 -0
- package/prompts/discipline-normal.md +206 -0
- package/prompts/persona-coder.md +21 -0
- package/prompts/persona-eng-coder.md +41 -0
- package/prompts/persona-eng-designer.md +80 -0
- package/prompts/persona-engineering.md +160 -0
- package/prompts/persona-explore.md +15 -0
- package/prompts/persona-normal.md +35 -0
- package/prompts/persona-plan.md +27 -0
- package/provider/anthropic.mjs +225 -0
- package/provider/core.mjs +491 -0
- package/provider/errors.mjs +101 -0
- package/provider/google.mjs +257 -0
- package/provider/index.mjs +7 -0
- package/provider/list-models.mjs +163 -0
- package/provider/normalize.mjs +81 -0
- package/provider/rate.mjs +168 -0
- package/provider/responses.mjs +495 -0
- package/provider/retry.mjs +88 -0
- package/provider/sse.mjs +264 -0
- package/provider/wait-status.mjs +59 -0
- package/proxy.mjs +274 -0
- package/rules.mjs +53 -0
- package/session-gc.mjs +248 -0
- package/session-guard.mjs +59 -0
- package/session-lifecycle.mjs +305 -0
- package/session-migrate.mjs +48 -0
- package/session-rename.mjs +38 -0
- package/session-segments.mjs +99 -0
- package/session-slot-write.mjs +168 -0
- package/session-slots-manifest.mjs +264 -0
- package/session-slots.mjs +298 -0
- package/session-store.mjs +441 -0
- package/session.mjs +244 -0
- package/skills.mjs +234 -0
- package/text-budget.mjs +79 -0
- package/token-ttl.mjs +285 -0
- package/tool-docs/apply_patch.md +15 -0
- package/tool-docs/bash.md +38 -0
- package/tool-docs/delete.md +13 -0
- package/tool-docs/edit.md +30 -0
- package/tool-docs/execute.md +21 -0
- package/tool-docs/fetch.md +12 -0
- package/tool-docs/file_ops.md +17 -0
- package/tool-docs/get_current_time.md +8 -0
- package/tool-docs/git.md +54 -0
- package/tool-docs/glob.md +11 -0
- package/tool-docs/grep.md +19 -0
- package/tool-docs/hashline_edit.md +14 -0
- package/tool-docs/insert_after.md +15 -0
- package/tool-docs/lint.md +10 -0
- package/tool-docs/ls.md +12 -0
- package/tool-docs/lsp.md +10 -0
- package/tool-docs/process.md +10 -0
- package/tool-docs/question.md +16 -0
- package/tool-docs/read.md +20 -0
- package/tool-docs/read_image.md +8 -0
- package/tool-docs/tree.md +14 -0
- package/tool-docs/wait_for.md +22 -0
- package/tool-docs/websearch.md +16 -0
- package/tool-docs/write.md +11 -0
- package/tools/bash.mjs +276 -0
- package/tools/edit-batch.mjs +204 -0
- package/tools/edit-diff.mjs +388 -0
- package/tools/exec-run.mjs +43 -0
- package/tools/execute.mjs +243 -0
- package/tools/file.mjs +464 -0
- package/tools/git-checkpoint.mjs +143 -0
- package/tools/git-ext.mjs +173 -0
- package/tools/git.mjs +415 -0
- package/tools/glob-dialect.mjs +130 -0
- package/tools/index.mjs +76 -0
- package/tools/linter.mjs +120 -0
- package/tools/lsp.mjs +335 -0
- package/tools/ops.mjs +293 -0
- package/tools/patch.mjs +290 -0
- package/tools/question.mjs +26 -0
- package/tools/repomap.mjs +314 -0
- package/tools/search.mjs +248 -0
- package/tools/shared.mjs +467 -0
- package/tools/tree.mjs +81 -0
- package/tools/web.mjs +224 -0
- package/tools/write-path.mjs +191 -0
- package/traces/trace-store.mjs +303 -0
- package/undo-stack.mjs +47 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* advisor/notice.mjs — advisor 拒回 / 失败结论文案 + 对象标识行(单源;2026-09-18 顾问面治理批)。
|
|
3
|
+
*
|
|
4
|
+
* 三族文案共用本档(设计权威 = `docs/core/design/ADVISOR-GUARDS.md` §2.4 / §2.5 / §7):
|
|
5
|
+
* 1. **类型门拒回**(F30——顶层 `type` 缺 / 非法 / 与 `object` 声明冲突 ⇒ 拒发);
|
|
6
|
+
* 2. **范围拒回标识行**(F31——三处范围拒绝的既有稳定前缀逐字 + 标识块尾随);
|
|
7
|
+
* 3. **失败结算结论块**(F28/F29——code / design 两轨共用;判据名纯函数 + 人读说明表)。
|
|
8
|
+
* 本档为叶子模块(零仓内导入——纯函数、零状态、零 LLM 输出解析 = N20);文案逐字 = 设计档,
|
|
9
|
+
* 改动须同轮同步该档(D2 单一权威源)。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** 对象标识行(F31 单源):`[type=… · scope=… · round=… · criterion=…]`。 */
|
|
13
|
+
export function identityLine({ type, scope, round, criterion }) {
|
|
14
|
+
return `[type=${type} · scope=${scope} · round=${round} · criterion=${criterion}]`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** 范围摘要(首路径 + `+N more`;无范围 ⇒ `none`)。 */
|
|
18
|
+
export function scopeSummary(list) {
|
|
19
|
+
const items = (Array.isArray(list) ? list : []).filter((p) => typeof p === "string" && p.trim())
|
|
20
|
+
if (items.length === 0) return "none"
|
|
21
|
+
return items.length === 1 ? items[0] : `${items[0]} +${items.length - 1} more`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 拒回族统一形态(F31):既有稳定前缀逐字(行首)+ 标识块尾随——两句同时满足。 */
|
|
25
|
+
export function withIdentityLine(text, fields) {
|
|
26
|
+
return `${text} ${identityLine(fields)}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─── ① 类型门(F30——ADVISOR-GUARDS.md §2.4)───────────────────────────────────
|
|
30
|
+
|
|
31
|
+
const isLegalTrack = (v) => v === "code" || v === "design"
|
|
32
|
+
|
|
33
|
+
/** 实收值原样回显(非字符串经 JSON 化——不推断、不近似)。 */
|
|
34
|
+
function receivedText(rawType) {
|
|
35
|
+
if (typeof rawType === "string") return rawType
|
|
36
|
+
try {
|
|
37
|
+
return JSON.stringify(rawType) ?? String(rawType)
|
|
38
|
+
} catch {
|
|
39
|
+
return String(rawType)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 类型门判据名(纯函数——判定枚举闭合三值;`null` = 放行)。
|
|
45
|
+
* `type-missing` = 缺失 / `null` / 空串 / 纯空白串;`type-invalid` = 非空非法值 / 非字符串;
|
|
46
|
+
* `type-object-conflict` = 顶层显式合法值 ∧ `object.type` 声明为**另一**合法值(窄读法:非法
|
|
47
|
+
* 声明值不构成轨矛盾——`object` 只描述评审对象、不选轨)。
|
|
48
|
+
*/
|
|
49
|
+
export function typeGateCriterion(rawType, declaredType) {
|
|
50
|
+
if (rawType === undefined || rawType === null || (typeof rawType === "string" && rawType.trim() === "")) {
|
|
51
|
+
return "type-missing"
|
|
52
|
+
}
|
|
53
|
+
if (!isLegalTrack(rawType)) return "type-invalid"
|
|
54
|
+
return isLegalTrack(declaredType) && declaredType !== rawType ? "type-object-conflict" : null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 类型门拒发串(逐字——§2.4:前缀行 + `Why` 三形态 + 对象声明追加行 + 两个合法值行)。 */
|
|
58
|
+
export function buildTypeGateRefusal({ criterion, received = null, declared = null, scope = "none" }) {
|
|
59
|
+
const line = identityLine({
|
|
60
|
+
// 冲突拒回记顶层实收值(未定轨);未定轨两分支记 absent / invalid。
|
|
61
|
+
type: criterion === "type-object-conflict" ? String(received) : (criterion === "type-missing" ? "absent" : "invalid"),
|
|
62
|
+
scope,
|
|
63
|
+
round: "—", // 判定先于实例解析——本次发起无轮次(§2.5 字段行)
|
|
64
|
+
criterion,
|
|
65
|
+
})
|
|
66
|
+
const why = criterion === "type-missing"
|
|
67
|
+
? "Why: the call carried no type at the top level — an omitted type must not silently fall back to the code track (that is how a design review becomes a code review without anyone noticing)."
|
|
68
|
+
: criterion === "type-invalid"
|
|
69
|
+
? `Why: "${receivedText(received)}" is not one of the two legal values — there is no closest-match guessing and no silent fallback.`
|
|
70
|
+
: `Why: the top-level type="${received}" disagrees with the object declaration object.type="${declared}" — align them: set the top-level type to "${declared}", or change the object.type declaration to "${received}".`
|
|
71
|
+
const parts = [
|
|
72
|
+
`Advisor: launch refused — the tool call must carry an explicit type at the top level, one of the two legal values below. ${line}`,
|
|
73
|
+
why,
|
|
74
|
+
]
|
|
75
|
+
// 对象声明追加行(归位——附于未定轨两分支,仅当 object.type === "design" 声明在位)。
|
|
76
|
+
if (declared === "design" && criterion !== "type-object-conflict") {
|
|
77
|
+
parts.push('The object declaration says type="design"; the declaration describes the review target, it does not select the review track.')
|
|
78
|
+
}
|
|
79
|
+
parts.push(
|
|
80
|
+
' • type="code" — code review: reviews the code you changed; pass paths=[...] to scope files/directories (documents=[...] adds acceptance-criteria context).',
|
|
81
|
+
' • type="design" — design review: reviews design / requirement documents; pass documents=[...] (the explicit list) — plus batchDoc when a batch record is in flight.',
|
|
82
|
+
"Nothing was sent: no review instance, no round consumed, no design token minted, no LLM call.",
|
|
83
|
+
)
|
|
84
|
+
return parts.join("\n")
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ─── ② 失败结算(F28 / F29——ADVISOR-GUARDS.md §7)─────────────────────────────
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 失败结算判据名(纯函数单源、零状态——§7 契约一优先级,自上而下首个命中)。
|
|
91
|
+
* @returns {string|null} 判据名;`null` = 可用判决 / 无尝试发生(不产结论块)。
|
|
92
|
+
*/
|
|
93
|
+
export function settlementCriterion(input) {
|
|
94
|
+
const { launchRefused = false, stale = false, hasResult = true, incomplete = null, persistFailed = false } = input ?? {}
|
|
95
|
+
if (launchRefused) return null // 1. 未发起请求(无尝试发生)
|
|
96
|
+
if (stale) return "stale" // 2. 陈旧结算(未产出可用凭证)
|
|
97
|
+
if (!hasResult) return "no_report" // 3. 无报告(fail-closed)
|
|
98
|
+
if (incomplete && incomplete !== "interrupted") return String(incomplete) // 4. 宿主截断尾五 kind
|
|
99
|
+
if (incomplete === "interrupted") return null // 5. 用户 / 系统中断——被丢弃的尝试
|
|
100
|
+
if (persistFailed) return "no_credential" // 6. pass 但槽落盘失败(设计专属面)
|
|
101
|
+
return null // 7. 可用判决(pass 且落盘成功 / changes-required)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 判据名 → 人读说明(逐字——§7 表第三列;`no_credential` 设计专属、`stale` 两轨)。 */
|
|
105
|
+
const CRITERION_MEANINGS = {
|
|
106
|
+
timeout: "review exceeded the wall-clock budget (agent.advisor.timeoutMs)",
|
|
107
|
+
context_limit: "review exceeded the model context budget",
|
|
108
|
+
turn_cap: "review exceeded the tool-round limit",
|
|
109
|
+
empty: "the provider returned an empty response",
|
|
110
|
+
review_failed: "provider / transport error",
|
|
111
|
+
stale: "the reviewed target changed while the review was in flight",
|
|
112
|
+
no_credential: "the token could not be written to the session ledger",
|
|
113
|
+
no_report: "the review settled without a report",
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function criterionMeaning(name) {
|
|
117
|
+
return CRITERION_MEANINGS[name] ?? String(name ?? "")
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 失败结论块(逐字——§7 契约二;**两轨共用**):块首行 = 对象标识行,既有结算正文逐字下沉为
|
|
122
|
+
* 块体,尝试表 = **本次尝试单行**(`#` = 实例尝试序号;零跨次载体),选项三值不自动执行。
|
|
123
|
+
* @param {{type: string, scope: string, round: number, criterion: string, body: string|null}} p
|
|
124
|
+
*/
|
|
125
|
+
export function buildSettlementConclusion({ type, scope, round, criterion, body }) {
|
|
126
|
+
const lines = [
|
|
127
|
+
`Advisor: review failure — 本轮未产出可用结论 (no usable settlement) ${identityLine({
|
|
128
|
+
type, scope, round: `${round}/uncapped`, criterion,
|
|
129
|
+
})}`,
|
|
130
|
+
]
|
|
131
|
+
const text = String(body ?? "").trim()
|
|
132
|
+
if (text) lines.push(text)
|
|
133
|
+
lines.push(
|
|
134
|
+
"Failed attempts in this review instance (most recent last):",
|
|
135
|
+
"| # | outcome | meaning |",
|
|
136
|
+
"|---|---|---|",
|
|
137
|
+
`| ${round} | ${criterion} | ${criterionMeaning(criterion)} |`,
|
|
138
|
+
"Options: 1. proceed as-is (no usable conclusion — design: no token, implementation stays gated; code: the code face stays unapproved) · 2. narrow or change the scope and re-run · 3. stop and report to the user",
|
|
139
|
+
)
|
|
140
|
+
return lines.join("\n")
|
|
141
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
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
|
+
// 字段名归一:解析后的 provider 挂点两端其一(`provider` 本体字段 / `_provider` 运载字段)
|
|
114
|
+
// ——取在场者,两形态同语义(都指本回合生效的 provider 对象)。
|
|
115
|
+
const resolvedProvider = agent.provider ?? agent._provider
|
|
116
|
+
const ctx = providerSpec(resolvedProvider).context
|
|
117
|
+
const cap = Math.max(PROJECT_GUIDE_MIN, Math.floor(ctx * PROJECT_GUIDE_FRACTION))
|
|
118
|
+
const shown = text.length <= cap
|
|
119
|
+
? text
|
|
120
|
+
: [...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
|
|
121
|
+
parts.push(`<!-- Project root: ${relative(agent.cwd, path).split(sep).join("/")} (inferred from the review scope under ${agent.cwd}) -->`)
|
|
122
|
+
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.")
|
|
123
|
+
parts.push("")
|
|
124
|
+
parts.push(shown)
|
|
125
|
+
parts.push("")
|
|
126
|
+
return root // guide injected — requirement-fit criteria apply (truthy root)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Inject the project document map (PO-1). Resolution: the DECLARED path
|
|
131
|
+
* (`advisor.docMap`, project-root-relative) wins; the built-in probe
|
|
132
|
+
* (`docs/README.md` → `docs/design/README.md`) stays as a fallback for projects
|
|
133
|
+
* laid out that way. Neither usable → an explicit degradation sentence — never a
|
|
134
|
+
* silent skip: the reviewer must know the Document-ownership criterion is unchecked.
|
|
135
|
+
* @returns {string|null} the injected map path, or null when degraded
|
|
136
|
+
*/
|
|
137
|
+
export function injectDocumentMap(agent, parts, root) {
|
|
138
|
+
const base = root ?? agent.cwd
|
|
139
|
+
const conv = loadConventions(agent.cwd)
|
|
140
|
+
let mapPath = null
|
|
141
|
+
if (conv.advisor.docMap) {
|
|
142
|
+
const declared = resolve(base, conv.advisor.docMap)
|
|
143
|
+
if (existsSync(declared)) mapPath = declared
|
|
144
|
+
} else {
|
|
145
|
+
mapPath = [resolve(base, "docs", "README.md"), resolve(base, "docs", "design", "README.md")]
|
|
146
|
+
.find((p) => existsSync(p)) ?? null
|
|
147
|
+
}
|
|
148
|
+
parts.push("## Document Map")
|
|
149
|
+
let text = null
|
|
150
|
+
if (mapPath) {
|
|
151
|
+
try {
|
|
152
|
+
text = readFileSync(mapPath, "utf8")
|
|
153
|
+
} catch (e) {
|
|
154
|
+
console.warn(`[advisor] document map unreadable at ${mapPath}: ${e.message}`)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (text === null) {
|
|
158
|
+
parts.push(NO_DOC_MAP_NOTICE) // degraded — visible by contract
|
|
159
|
+
parts.push("")
|
|
160
|
+
return null
|
|
161
|
+
}
|
|
162
|
+
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.")
|
|
163
|
+
parts.push(text)
|
|
164
|
+
parts.push("")
|
|
165
|
+
return mapPath
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Inject the project standards document (PO-2). DECLARATION ONLY
|
|
170
|
+
* (`advisor.standardsDoc`, project-root-relative): the old hardcoded
|
|
171
|
+
* `METHODOLOGY.md` probe injected a file most projects do not have — and its
|
|
172
|
+
* absence was silent. Undeclared (or unreadable) → explicit degradation sentence.
|
|
173
|
+
* @returns {string|null} the injected standards path, or null when degraded
|
|
174
|
+
*/
|
|
175
|
+
export function injectProjectStandards(agent, parts, root) {
|
|
176
|
+
const base = root ?? agent.cwd
|
|
177
|
+
const conv = loadConventions(agent.cwd)
|
|
178
|
+
const declared = conv.advisor.standardsDoc ? resolve(base, conv.advisor.standardsDoc) : null
|
|
179
|
+
parts.push("## Project Standards")
|
|
180
|
+
let text = null
|
|
181
|
+
if (declared && existsSync(declared)) {
|
|
182
|
+
try {
|
|
183
|
+
text = readFileSync(declared, "utf8")
|
|
184
|
+
} catch (e) {
|
|
185
|
+
console.warn(`[advisor] project standards unreadable at ${declared}: ${e.message}`)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (text === null) {
|
|
189
|
+
parts.push(NO_STANDARDS_NOTICE) // degraded — visible by contract
|
|
190
|
+
parts.push("")
|
|
191
|
+
return null
|
|
192
|
+
}
|
|
193
|
+
parts.push("The project declares these standards for this review. Judge the design/changes against them:")
|
|
194
|
+
parts.push(text)
|
|
195
|
+
parts.push("")
|
|
196
|
+
return declared
|
|
197
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
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
|
+
}
|