@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,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* abort-provenance.mjs — abort 来源标注词汇表(单一权威源——AGENT-LOOP.md §20.3 第 1/2 条)
|
|
3
|
+
*
|
|
4
|
+
* 子代理 / 评审 / 会诊死亡的可诊断性:产生点标注结构化来源(`err.abortInfo`)→
|
|
5
|
+
* controller 链 hop 逐跳保 reason(§20.3 第 4 条 #10)→ 结算 / 报告面用 `deathLine`
|
|
6
|
+
* 合成死亡行(原 message 前缀逐字保留 + 来源后缀)。
|
|
7
|
+
*
|
|
8
|
+
* 纯函数、零 import——任意层可引、无环(provider / proxy / agent / settle 族共用)。
|
|
9
|
+
* 词汇表:
|
|
10
|
+
* - trigger(5 值):`user`(reason.interrupt)· `timeout`(TimeoutError / 定时器面)·
|
|
11
|
+
* `cancel`(定向中止)· `stop`(整批 / 会话 / 回合级停)· `unknown`(残留 / 未标注——告警态)。
|
|
12
|
+
* - layer(3 值):`provider` / `agent` / `settle`;未标注错误回落 `unrecorded`(合成器 token)。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** trigger 枚举权威(测试锁计数 5——AGENT-LOOP.md §20.3 第 1 条表)。 */
|
|
16
|
+
export const TRIGGERS = ["user", "timeout", "cancel", "stop", "unknown"]
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* trigger 判定(判据序 = §20.3 第 1 条表;signal.reason 四形态:
|
|
20
|
+
* ① `{interrupt:true(,message)}`(既有 user 面)② `TimeoutError`(原生——timeout 面)
|
|
21
|
+
* ③ `{abortTrigger:"cancel"|"stop"|"timeout"(,abortDetail)}`(新增——程序性中止)
|
|
22
|
+
* ④ 缺失 / 无已知键(→ unknown——诊断告警态,不得静默)。
|
|
23
|
+
*/
|
|
24
|
+
export function triggerOf(signal) {
|
|
25
|
+
const reason = signal?.reason
|
|
26
|
+
if (reason == null) return "unknown"
|
|
27
|
+
if (reason.interrupt === true) return "user"
|
|
28
|
+
if (reason.name === "TimeoutError" || reason.abortTrigger === "timeout") return "timeout"
|
|
29
|
+
if (reason.abortTrigger === "cancel") return "cancel"
|
|
30
|
+
if (reason.abortTrigger === "stop") return "stop"
|
|
31
|
+
return "unknown"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 产生点错误(abort 面):AbortError + `reason` 透传 + `abortInfo` 标注。
|
|
36
|
+
* message 取既有通用文案(既有前缀断言零回归——死亡行前缀逐字保留)。
|
|
37
|
+
*/
|
|
38
|
+
export function abortError(signal, layer, detail) {
|
|
39
|
+
const e = new DOMException("The operation was aborted", "AbortError")
|
|
40
|
+
e.reason = signal?.reason
|
|
41
|
+
e.abortInfo = { trigger: triggerOf(signal), layer, detail }
|
|
42
|
+
return e
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 产生点错误(定时器面):自有 message + `abortInfo{trigger:"timeout"}`。
|
|
47
|
+
* name 保持 "Error"(既有分类谓词——`AbortError` 分支——零变化)。
|
|
48
|
+
*/
|
|
49
|
+
export function timeoutError(message, layer, detail) {
|
|
50
|
+
const e = new Error(message)
|
|
51
|
+
e.abortInfo = { trigger: "timeout", layer, detail }
|
|
52
|
+
return e
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 外部错误补标(undici / fetch 拒否等):缺 `abortInfo` 才补(不覆盖产生点标注);
|
|
57
|
+
* 不改 `name` / `message`(消费面零回归)。
|
|
58
|
+
* 第 4 参 detail 为站点名(站点表逐站点传——自由短串)。
|
|
59
|
+
*/
|
|
60
|
+
export function annotateAbort(err, signal, layer, detail) {
|
|
61
|
+
if (err?.abortInfo) return err
|
|
62
|
+
try {
|
|
63
|
+
err.abortInfo = { trigger: triggerOf(signal), layer, detail: detail ?? "unrecorded" }
|
|
64
|
+
} catch { /* frozen error——保持未标注(合成器 unknown 告警兜底) */ }
|
|
65
|
+
return err
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 死亡行长度上限(AGENT-LOOP.md §20.3 第 3 条——超长优先截 detail)。 */
|
|
69
|
+
const MAX_LEN = 300
|
|
70
|
+
/** unknown 形态 detail token(§20.3 第 3 条逐字)。 */
|
|
71
|
+
const NO_REASON = "no reason on signal"
|
|
72
|
+
/** 未标注回落 token(layer / detail 两面兜底)。 */
|
|
73
|
+
const UNRECORDED = "unrecorded"
|
|
74
|
+
|
|
75
|
+
/** 信号域 detail 回落(§20.3 第 1 条形态③——`abortDetail` 载站点名;错误未标注时仍可辨发起动作)。 */
|
|
76
|
+
const detailOf = (signal) => (typeof signal?.reason?.abortDetail === "string" ? signal.reason.abortDetail : null)
|
|
77
|
+
|
|
78
|
+
/** 求值链(§20.3 第 1 条:err.abortInfo → triggerOf(signal) → err.name 兜底)。 */
|
|
79
|
+
function resolveAbort(err, signal) {
|
|
80
|
+
const info = err?.abortInfo
|
|
81
|
+
if (info?.trigger) return { trigger: info.trigger, layer: info.layer ?? UNRECORDED, detail: info.detail || null }
|
|
82
|
+
const fromSignal = triggerOf(signal)
|
|
83
|
+
if (fromSignal !== "unknown") return { trigger: fromSignal, layer: UNRECORDED, detail: detailOf(signal) }
|
|
84
|
+
if (err?.name === "TimeoutError") return { trigger: "timeout", layer: UNRECORDED, detail: detailOf(signal) }
|
|
85
|
+
// AbortError / 无名——告警态(unknown 显式,空后缀即违规)
|
|
86
|
+
return { trigger: "unknown", layer: UNRECORDED, detail: NO_REASON }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** detail 裁剪:优先截 detail(budget = 上限内剩余字符)。 */
|
|
90
|
+
function clipDetail(detail, budget) {
|
|
91
|
+
if (budget <= 0) return ""
|
|
92
|
+
return detail.length <= budget ? detail : detail.slice(0, Math.max(1, budget - 1)) + "…"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 死亡行合成器(§20.3 第 3 条——报告面单点):
|
|
97
|
+
* `<原 message>[ ← cause: <cause.message>][ · abort(<trigger>@<layer>:<detail>)]`
|
|
98
|
+
* - 原 message 前缀逐字保留(既有前缀 / 包含断言零回归);
|
|
99
|
+
* - 后缀出现条件 = `err.abortInfo` 存在 ∨ `signal?.aborted` ∨ `err.name ∈ {AbortError, TimeoutError}`;
|
|
100
|
+
* - cause 段(P1)独立于后缀("fetch failed" 类网络死亡真因同判);
|
|
101
|
+
* - 总长 ≤300 字符(超长先截 detail,仍超再截尾)。
|
|
102
|
+
*/
|
|
103
|
+
export function deathLine(err, signal) {
|
|
104
|
+
let line = err?.message ?? String(err ?? "")
|
|
105
|
+
if (err?.cause?.message) line += ` ← cause: ${err.cause.message}`
|
|
106
|
+
const abortish = Boolean(err?.abortInfo) || Boolean(signal?.aborted) ||
|
|
107
|
+
err?.name === "AbortError" || err?.name === "TimeoutError"
|
|
108
|
+
if (abortish) {
|
|
109
|
+
const { trigger, layer, detail } = resolveAbort(err, signal)
|
|
110
|
+
const head = `${line} · abort(${trigger}@${layer}:`
|
|
111
|
+
// unknown 形态逐字用告警 token(§20.3 第 3 条——站点 detail 不进告警行);非串 detail 归一化
|
|
112
|
+
const text = trigger === "unknown" ? NO_REASON : (detail == null ? UNRECORDED : String(detail))
|
|
113
|
+
line = `${head}${clipDetail(text, MAX_LEN - head.length - 1)})`
|
|
114
|
+
}
|
|
115
|
+
return line.length > MAX_LEN ? line.slice(0, MAX_LEN - 1) + "…" : line
|
|
116
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* citations.mjs — host-verified citation checking (decision d698434).
|
|
3
|
+
* Extracted from advisor/run.mjs (file-size split). The evidence rule becomes
|
|
4
|
+
* a host fact: every `file:line: content` reference in a review is mechanically
|
|
5
|
+
* checked against the CURRENT disk state; mismatches mark the finding
|
|
6
|
+
* unverified and cannot support a push-back.
|
|
7
|
+
*
|
|
8
|
+
* 第 11 批(C / F14 / §14.5):解析候选 = cwd + **评审对象声明范围派生根**(声明仓根 /
|
|
9
|
+
* 声明文件目录 / 声明目录——纯路径派生,零扫描、零 git);命中判据三条件全中(围栏内 ∧
|
|
10
|
+
* 可读 ∧ 目标行含引文内容)⇒ 零新增假命中。失败原因三分(file unreadable /
|
|
11
|
+
* content mismatch @ path / path traversal)——父侧不再人肉复核。
|
|
12
|
+
*/
|
|
13
|
+
import { readFileSync, realpathSync } from "node:fs"
|
|
14
|
+
import { resolve, relative, dirname, extname, sep, isAbsolute, join } from "node:path"
|
|
15
|
+
|
|
16
|
+
// `file:line: content` citations — the file group is narrowed to source/config
|
|
17
|
+
// extensions so URLs (`example.com:8080: …`) don't become false-positive
|
|
18
|
+
// citations that fail as "file unreadable" in the verification report.
|
|
19
|
+
// Single-letter extensions (c/h) are kept — false positives (e.g. "a.c:1: x")
|
|
20
|
+
// are rare and only add a failed-citation line to the report; the report is
|
|
21
|
+
// advisory for the parent agent, never a crash path.
|
|
22
|
+
const CITATION_RE = /([\w./\\-]+\.(?:mjs|cjs|js|ts|jsx|tsx|mts|cts|py|rs|go|c|h|cpp|hpp|java|rb|php|sh|bash|json|md|markdown|mdx|yaml|yml|toml|css|html)):(\d+):\s*([^`\n]{4,})/g
|
|
23
|
+
|
|
24
|
+
/** Extract `file:line: content` citations from a review text. */
|
|
25
|
+
export function extractCitations(text) {
|
|
26
|
+
const out = []
|
|
27
|
+
for (const m of text.matchAll(CITATION_RE)) {
|
|
28
|
+
out.push({ file: m[1], line: Number(m[2]), content: m[3].trim() })
|
|
29
|
+
}
|
|
30
|
+
return out
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 候选解析根(§14.5——纯路径派生,候选顺序 = cwd → 各声明路径的派生根):
|
|
34
|
+
* 声明仓根(`cwd/<segs[0]>`)与 声明文件目录 / 声明目录本身。声明在 cwd 之外 → 不派生
|
|
35
|
+
* (relative 越出 cwd 或跨盘符——候选与声明脱节即假命中面)。 */
|
|
36
|
+
function citationRoots(cwd, scope) {
|
|
37
|
+
const base = resolve(cwd)
|
|
38
|
+
const roots = [base]
|
|
39
|
+
for (const s of Array.isArray(scope) ? scope : []) {
|
|
40
|
+
if (typeof s !== "string" || !s.trim()) continue
|
|
41
|
+
const abs = resolve(base, s)
|
|
42
|
+
const rel = relative(base, abs)
|
|
43
|
+
if (!rel || rel.startsWith("..") || isAbsolute(rel)) continue
|
|
44
|
+
const segs = rel.split(/[\\/]/).filter(Boolean)
|
|
45
|
+
if (segs.length === 0) continue
|
|
46
|
+
const repoRoot = join(base, segs[0])
|
|
47
|
+
const declaredDir = extname(abs) ? dirname(abs) : abs
|
|
48
|
+
for (const r of [repoRoot, declaredDir]) if (!roots.includes(r)) roots.push(r)
|
|
49
|
+
}
|
|
50
|
+
return roots
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 单引文解析(§14.5):按候选顺序试 `resolve(root, file)`;三条件全中才算命中。
|
|
54
|
+
* @returns {{matched: true, root: string, resolved: string}|{matched: false, reason: string}} */
|
|
55
|
+
function resolveCitation(citation, roots, base) {
|
|
56
|
+
const fence = base + sep
|
|
57
|
+
let mismatch = null
|
|
58
|
+
let traversal = false
|
|
59
|
+
for (const root of roots) {
|
|
60
|
+
let real
|
|
61
|
+
try {
|
|
62
|
+
// Path confinement: citation paths are LLM-generated — never trust them.
|
|
63
|
+
// A hallucinated "../config.json" would otherwise read (and leak via the
|
|
64
|
+
// report) files outside the project, including API-key configs.
|
|
65
|
+
// realpathSync resolves symlinks too — a link inside the project that
|
|
66
|
+
// points outside must not pass the prefix check.
|
|
67
|
+
real = realpathSync(resolve(root, citation.file))
|
|
68
|
+
} catch {
|
|
69
|
+
continue // 该候选无此文件(或不可解析)——试下一候选
|
|
70
|
+
}
|
|
71
|
+
if (!real.startsWith(fence)) { traversal = true; continue }
|
|
72
|
+
let line = ""
|
|
73
|
+
try {
|
|
74
|
+
line = readFileSync(real, "utf8").split("\n")[citation.line - 1] ?? ""
|
|
75
|
+
} catch {
|
|
76
|
+
continue // 存在但读不了(目录等)——按未命中处理,试下一候选
|
|
77
|
+
}
|
|
78
|
+
if (line.includes(citation.content)) {
|
|
79
|
+
return { matched: true, root, resolved: relative(base, real).split(sep).join("/") }
|
|
80
|
+
}
|
|
81
|
+
mismatch ??= real
|
|
82
|
+
}
|
|
83
|
+
if (mismatch) return { matched: false, reason: `content mismatch @ ${relative(base, mismatch).split(sep).join("/")}` }
|
|
84
|
+
if (traversal) return { matched: false, reason: "path traversal" }
|
|
85
|
+
return { matched: false, reason: "file unreadable" }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Mechanically verify citations against the CURRENT file state: read the file,
|
|
90
|
+
* take the exact line, check it CONTAINS the quoted content. Reports
|
|
91
|
+
* N/M matched + the mismatches. Unverified citations cannot support a
|
|
92
|
+
* push-back — the evidence rule becomes a host fact, not a prompt wish.
|
|
93
|
+
* @param {string} text — the review text
|
|
94
|
+
* @param {string} cwd — the agent's working directory (workspace root)
|
|
95
|
+
* @param {{scope?: string[]}} [opts] — scope = 评审对象声明路径(documents + paths);
|
|
96
|
+
* 省略 ⇒ 旧行为(仅 cwd 候选——签名向后兼容)。
|
|
97
|
+
*/
|
|
98
|
+
export function verifyCitations(text, cwd, opts = {}) {
|
|
99
|
+
const citations = extractCitations(text)
|
|
100
|
+
const matched = []
|
|
101
|
+
const failed = []
|
|
102
|
+
const base = resolve(cwd)
|
|
103
|
+
const roots = citationRoots(cwd, opts?.scope)
|
|
104
|
+
for (const c of citations) {
|
|
105
|
+
const r = resolveCitation(c, roots, base)
|
|
106
|
+
if (r.matched) {
|
|
107
|
+
// 命中根记录(§14.5):经派生根解析(非 cwd 直解)的命中在报告中注明解析路径。
|
|
108
|
+
matched.push(r.root === base ? c : { ...c, root: r.root, resolved: r.resolved })
|
|
109
|
+
} else {
|
|
110
|
+
failed.push({ ...c, reason: r.reason })
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return { total: citations.length, matched, failed }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Append the verification report to the review text (visible to the parent agent). */
|
|
117
|
+
export function appendCitationReport(text, cwd, opts = {}) {
|
|
118
|
+
const { total, matched, failed } = verifyCitations(text, cwd, opts)
|
|
119
|
+
if (total === 0) return text // no citations — nothing to verify
|
|
120
|
+
const lines = [
|
|
121
|
+
"",
|
|
122
|
+
"---",
|
|
123
|
+
`[host-verified] ${matched.length}/${total} citations match current file state.`,
|
|
124
|
+
]
|
|
125
|
+
// 命中根透明(F14/§14.5):经声明范围派生根解析的命中逐条注明解析路径(cwd 直解的不列
|
|
126
|
+
// ——零噪音;列的正是修复前会被误报为 unreadable 的裸路径引用)。
|
|
127
|
+
const derived = matched.filter((c) => c.resolved)
|
|
128
|
+
if (derived.length > 0) {
|
|
129
|
+
lines.push("Citations resolved via the declared review scope (bare path — resolved root noted):")
|
|
130
|
+
for (const c of derived.slice(0, 10)) lines.push(`- ${c.file}:${c.line} → ${c.resolved}`)
|
|
131
|
+
}
|
|
132
|
+
if (failed.length > 0) {
|
|
133
|
+
lines.push("Citations that do NOT match the current file state (treat their claims as unverified):")
|
|
134
|
+
for (const f of failed.slice(0, 10)) {
|
|
135
|
+
lines.push(`- ${f.file}:${f.line}: ${f.content.slice(0, 80)}${f.reason ? ` (${f.reason})` : ""}`)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return text + lines.join("\n")
|
|
139
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* advisor/compaction.mjs — advisor review support (split out of advisor/run.mjs,
|
|
3
|
+
* 第 11 批 — run.mjs was 498/500 硬帽): context trimming + the review's resource
|
|
4
|
+
* limits + the terminal-state guards + the review-text assembler.
|
|
5
|
+
*
|
|
6
|
+
* estimateTokens / compactMessages moved VERBATIM (the only edit is the `pinned`
|
|
7
|
+
* re-attach — F13/§14.4 #3); renderTimeline moved verbatim too, so the tail
|
|
8
|
+
* GENERATOR and the tail CLASSIFIER stay in one file with the assembler they
|
|
9
|
+
* feed (§14.3 谓词 ↔ §14.6 结构化尾——同族单源)。拆分线 = 行数硬帽实测(见批次档 §5)。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { providerSpec } from "../config.mjs" // 第 25 批:预算派生(与 loop.mjs:11 同源导入)
|
|
13
|
+
// B4(群 B 批,CLI §18.3——F32):CJK 加权单源(provider/rate.mjs 叶子向无环)
|
|
14
|
+
import { estimateText } from "../provider/rate.mjs"
|
|
15
|
+
|
|
16
|
+
export const MAX_ADVISOR_TURNS = 100
|
|
17
|
+
// NOTE: prompts/advisor-round{1,2,3}.md encourage the model to finish within
|
|
18
|
+
// ~30 tool turns — a prompt-level efficiency target, DISTINCT from the
|
|
19
|
+
// 100-turn mechanical hard cap (MAX_ADVISOR_TURNS above; pure runaway-loop
|
|
20
|
+
// guard). They serve different purposes; do NOT synchronize them.
|
|
21
|
+
|
|
22
|
+
// Context window limits
|
|
23
|
+
// 上下文预算(第 25 批——120K 硬编码退场):预算跟随评审模型窗口(providerSpec:
|
|
24
|
+
// 模型规格表 × provider 级 context 覆盖)。头寸用途 = chars/4 估算误差 + 响应/协议开销
|
|
25
|
+
// (内存不构成约束——设计 §16.4);判死线仍是宿主机自限线,服务端窗口约束不变。
|
|
26
|
+
export const CONTEXT_LIMIT_RATIO = 0.8 // 判死线 = 窗口 × 0.8
|
|
27
|
+
const COMPACT_TRIGGER_RATIO = 0.8 // 压缩触发 = 判死线 × 0.8(既有关系零改)
|
|
28
|
+
|
|
29
|
+
/** 评审上下文预算(纯函数——两档阈值可机测;provider 为 null 时退化默认规格)。 */
|
|
30
|
+
export function advisorContextBudget(provider) {
|
|
31
|
+
const limit = Math.floor(providerSpec(provider).context * CONTEXT_LIMIT_RATIO)
|
|
32
|
+
return { limit, compactAt: Math.floor(limit * COMPACT_TRIGGER_RATIO) }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const TOOL_TIMEOUT_MS = 30_000 // single tool timeout
|
|
36
|
+
export const REVIEW_TIMEOUT_MS = 600_000 // whole review timeout (10 minutes)
|
|
37
|
+
export const MAX_RESULT_CHARS = 64 * 1024 // tool result truncation (line-aware; 64K, aligned with main offload limit)
|
|
38
|
+
const MAX_KEY_FILES_IN_COMPACTION = 5 // files named in the compaction summary
|
|
39
|
+
|
|
40
|
+
/** Estimate token count from messages(B4——群 B 批 CLI §18.3:扁平 chars/4 改 `estimateText`
|
|
41
|
+
* 加权式——ASCII/4 + 非 ASCII/1;纯 ASCII 与旧式逐值相等;CJK 低估 ~3-4× 修正;
|
|
42
|
+
* walker(content / tool_calls 两源)与计数口径零改)。 */
|
|
43
|
+
export function estimateTokens(messages) {
|
|
44
|
+
return messages.reduce((sum, msg) => {
|
|
45
|
+
const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content || "")
|
|
46
|
+
const toolCalls = msg.tool_calls ? JSON.stringify(msg.tool_calls) : ""
|
|
47
|
+
return sum + estimateText(content + toolCalls)
|
|
48
|
+
}, 0)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Compact early messages when context grows too large — LOCAL trimming only
|
|
52
|
+
* (no LLM summarization). MUTATES in place (splice) so the caller's array
|
|
53
|
+
* reference stays valid — a reassignment would leave the caller's logging
|
|
54
|
+
* (tool-call count, token estimate) reading a stale array. */
|
|
55
|
+
export function compactMessages(messages, pinned = null) {
|
|
56
|
+
// Keep: system prompt, last 20 messages (≈ 10 assistant+tool exchanges),
|
|
57
|
+
// user message — the rest is summarized.
|
|
58
|
+
if (messages.length <= 20) return
|
|
59
|
+
|
|
60
|
+
const system = messages[0]
|
|
61
|
+
const recent = messages.slice(-20)
|
|
62
|
+
const old = messages.slice(1, -20)
|
|
63
|
+
|
|
64
|
+
// Count actual tool messages (old.length counts user/assistant rows too)
|
|
65
|
+
const toolCount = old.filter((m) => m.role === "tool").length
|
|
66
|
+
const keyFiles = old
|
|
67
|
+
.filter((m) => m.role === "tool")
|
|
68
|
+
.map((m) => m.content?.split("\n")[0]?.slice(0, 50)) // first line of tool results typically names the file that was read/grepped
|
|
69
|
+
.filter(Boolean)
|
|
70
|
+
.slice(0, MAX_KEY_FILES_IN_COMPACTION)
|
|
71
|
+
const filesPart = keyFiles.length > 0 ? ` Key files examined: ${keyFiles.join(", ")}` : ""
|
|
72
|
+
const summary = `Earlier exploration: ${toolCount} tool calls completed.${filesPart}`
|
|
73
|
+
|
|
74
|
+
// F13(第 11 批 §14.4 #3):压缩丢掉的正是**首条 user 消息**(评审简报,含 token)——
|
|
75
|
+
// pinned 由评审参数构建(非模型输出),在本次压缩动作内作为一条 user 消息重挂(幂等可读:
|
|
76
|
+
// 重复压缩允许重复挂回,不做存在性判定)。
|
|
77
|
+
const pin = pinned ? [{ role: "user", content: pinned }] : []
|
|
78
|
+
messages.splice(0, messages.length,
|
|
79
|
+
system,
|
|
80
|
+
{ role: "user", content: `[Context compacted] ${summary}` },
|
|
81
|
+
...pin,
|
|
82
|
+
...recent)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
86
|
+
// 不完整判定族(A / F16 共用单谓词——§14.3;六 kind = 宿主尾族)
|
|
87
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
/** 宿主尾族六 kind 的块首行逐字前缀(§14.3 表)。变量段(token 数 / 秒数 / 工具轮数)
|
|
90
|
+
* 不入前缀——取各尾的固定字面部分;`review_failed` = run.mjs catch 的字符串 resolve
|
|
91
|
+
* 形态(不 throw),其余五条 = renderTimeline 尾(loop.mjs)。 */
|
|
92
|
+
const ADVISOR_INCOMPLETE_PREFIXES = [
|
|
93
|
+
["context_limit", "Advisor: context window limit"],
|
|
94
|
+
["turn_cap", "Advisor: stopped after"],
|
|
95
|
+
["timeout", "Advisor: review timeout"],
|
|
96
|
+
["empty", "Advisor: empty response"],
|
|
97
|
+
["interrupted", "Advisor: interrupted."],
|
|
98
|
+
["review_failed", "Advisor: review failed"],
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
/** 单谓词(三消费点同源:design 结算 / code 完成守卫 / 报告提示)——**块首行扫描**(按空行
|
|
102
|
+
* 分块,逐块取首行 trim 后测前缀;时间线与尾以空行相接,六条尾均以块首行形态落地)。
|
|
103
|
+
* 负向精度(§14.3 修正轮):引文中同串的**非块首形态**(围栏内行 / 表格行 / 引用行)不判
|
|
104
|
+
* incomplete;块首裸行引用同串的残余误报方向安全(fail-closed——多付一轮重跑,如实登记)。
|
|
105
|
+
* @returns {string|null} kind 或 null */
|
|
106
|
+
export function advisorIncompleteMarker(text) {
|
|
107
|
+
for (const block of String(text ?? "").split(/\n\s*\n/)) {
|
|
108
|
+
const first = block.split("\n").find((l) => l.trim() !== "")
|
|
109
|
+
if (!first) continue
|
|
110
|
+
const line = first.trim()
|
|
111
|
+
for (const [kind, prefix] of ADVISOR_INCOMPLETE_PREFIXES) {
|
|
112
|
+
if (line.startsWith(prefix)) return kind
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
119
|
+
// 预算提示 + 结构化超时尾(D / F15——§14.6 #2/#3)
|
|
120
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
/** 0.75 一次性预算提示判定(纯函数——阈值两侧可机测;每场评审至多一次)。 */
|
|
123
|
+
export function shouldBudgetNudge(elapsedMs, budgetMs, nudged) {
|
|
124
|
+
return !nudged && Number.isFinite(budgetMs) && budgetMs > 0 && elapsedMs >= budgetMs * 0.75
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** 预算提示文案(逐字——§14.6 #2;由循环注入一条 user 消息)。 */
|
|
128
|
+
export function budgetNudgeText(elapsedMs, budgetMs) {
|
|
129
|
+
const secs = (ms) => Math.round(ms / 100) / 10
|
|
130
|
+
const pct = Math.round((elapsedMs / budgetMs) * 100)
|
|
131
|
+
return `⏳ review budget: ~${pct}% consumed (${secs(elapsedMs)}s of ${secs(budgetMs)}s). Converge now: emit your findings table for the evidence you have verified, mark anything you could not verify explicitly as \`unverified\` (unverified evidence must not support a pass), and emit your verdict line.`
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** 结构化超时尾(§14.6 #3):族前缀 `Advisor: review timeout after {S}s.` 逐字保持
|
|
135
|
+
* (判定族字面依赖);其后 = 机读统计(rounds / tool calls / review text produced)
|
|
136
|
+
* + 可执行恢复指引(narrower scope / 调预算)。 */
|
|
137
|
+
export function timeoutTail(timeoutMs, rounds, toolCalls, producedText) {
|
|
138
|
+
const s = Math.round(timeoutMs / 1000)
|
|
139
|
+
return [
|
|
140
|
+
`Advisor: review timeout after ${s}s. Review incomplete — the wall-clock budget was exhausted; partial findings (if any) are above.`,
|
|
141
|
+
`- rounds: ${rounds} · tool calls: ${toolCalls} · review text produced: ${producedText ? "yes" : "no"}`,
|
|
142
|
+
`- budget: ${s}s (agent.advisor.timeoutMs) — re-run with a narrower scope (split the review across fewer documents) or raise the budget.`,
|
|
143
|
+
].join("\n")
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
147
|
+
// 评审文本装配(loop 的尾经此与时间线合流——与尾族同文件:生成 / 判定 / 装配单源)
|
|
148
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
// The live "[thinking…]" wait indicator shares its exact text with the TUI
|
|
151
|
+
// cleanup regex (agent-turn.mjs strips it before flushing to history) — keep
|
|
152
|
+
// them in lockstep.
|
|
153
|
+
export const ADVISOR_THINKING_PLACEHOLDER = "\n[thinking…]\n"
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Tool-call progress line summary delegates to the single source describeToolArgs
|
|
157
|
+
* (../tui/tool-args.mjs) — the same function main-agent tool blocks and subagent
|
|
158
|
+
* blocks use. 2026-08-31: replaced the local picker (action/path/pattern/command-only)
|
|
159
|
+
* so advisor progress lines show the quoted-path forms everywhere else.
|
|
160
|
+
*/
|
|
161
|
+
/**
|
|
162
|
+
* Render the ordered review timeline — thinking / tool progress / final text
|
|
163
|
+
* interleaved EXACTLY as emitted, so the persisted record shows the review
|
|
164
|
+
* process at its real positions. A summary appended at the end would lose the
|
|
165
|
+
* order (the user-visible "no tool calls in the advisor record" gap). The
|
|
166
|
+
* live "[thinking…]" placeholder is stripped (wait indicator, not content).
|
|
167
|
+
*/
|
|
168
|
+
export function renderTimeline(timeline, tail = "") {
|
|
169
|
+
const body = timeline
|
|
170
|
+
.map((b) => b.text.replaceAll(ADVISOR_THINKING_PLACEHOLDER, "").trim())
|
|
171
|
+
.filter(Boolean)
|
|
172
|
+
.join("\n\n")
|
|
173
|
+
return [body, tail].filter(Boolean).join("\n\n")
|
|
174
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* advisor/convergence.mjs — shared convergence-round message building (round 2+).
|
|
3
|
+
* SINGLE source for the round-2+ sections used by BOTH the normal advisor flow
|
|
4
|
+
* (buildAdvisorFollowUp) and the legacy path (buildAdvisorUserMessage) — fixes must
|
|
5
|
+
* not be replicated in two places. Lives in its own module to avoid the advisor-entry
|
|
6
|
+
* ↔ message-building import cycle.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Shared convergence-round instructions — single source for BOTH paths
|
|
11
|
+
* (buildAdvisorUserMessage's legacy convergence block and
|
|
12
|
+
* buildAdvisorFollowUp), so the wording cannot diverge.
|
|
13
|
+
* Round 2 may flag obvious new issues; round 3+ is strict verification.
|
|
14
|
+
* @param {number} round — convergence round number (2+)
|
|
15
|
+
* @param {string[]|null} scopeFiles — optional file list for the no-response fallback
|
|
16
|
+
* @returns {string[]} the numbered instruction lines (callers spread them)
|
|
17
|
+
*/
|
|
18
|
+
export function buildConvergenceInstructions(round, scopeFiles = null) {
|
|
19
|
+
const fileList = scopeFiles?.length
|
|
20
|
+
? ` The review surface is: ${scopeFiles.slice(0, 10).join(", ")}.`
|
|
21
|
+
: ""
|
|
22
|
+
return [
|
|
23
|
+
`1. IMPORTANT: verify EVERY item of the prior review output against the CURRENT FILE STATE with \`read\` — never decide based on earlier snapshots alone.${fileList}`,
|
|
24
|
+
"2. STALE-CONTEXT WARNING: any diff or file content from earlier messages is a historical snapshot — treat it as expired. Only fresh `read` results describe the current state.",
|
|
25
|
+
"3. You have no git tool; git output in earlier messages is historical and untrustworthy (committed fixes never show in a diff).",
|
|
26
|
+
"4. `read` the files named in the prior review output (or the review surface above) in full — ALWAYS. Batch reads/greps in a single reply.",
|
|
27
|
+
"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 be stale or fabricated. Findings without a fresh quoted line are treated as unverified and will not be accepted.",
|
|
28
|
+
"6. Produce your verification table. Do not re-read content you already have.",
|
|
29
|
+
round === 2
|
|
30
|
+
? "7. You may flag obvious NEW issues introduced by the fixes (crashes, data loss, logic errors — not style)."
|
|
31
|
+
: "7. Do NOT look for new issues.",
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Shared convergence body — SINGLE source for the round-2+ message sections
|
|
37
|
+
* (decision 2026-08-08): the FULL verbatim prior review output is the only
|
|
38
|
+
* complete verification list; the agent response table is a focus aid only.
|
|
39
|
+
* Used by buildAdvisorFollowUp (the normal flow) and the legacy path in
|
|
40
|
+
* messages.mjs (direct external callers of buildAdvisorUserMessage) — fixes
|
|
41
|
+
* must not be replicated in two places.
|
|
42
|
+
* @param {string} p — full prior review output (verbatim)
|
|
43
|
+
* @param {string} response — agent fix-claims table (or fallback text)
|
|
44
|
+
* @param {number} round — next round number (>= 2)
|
|
45
|
+
* @param {string[]|null} [scopeFiles] — review surface for instructions
|
|
46
|
+
* @returns {string} the convergence message
|
|
47
|
+
*/
|
|
48
|
+
export function buildConvergenceBody(p, response, round, scopeFiles) {
|
|
49
|
+
const label = round === 2 ? "Verify Prior Table + Flag New Issues" : "Strict Verification"
|
|
50
|
+
const reminder = round === 2
|
|
51
|
+
? "verify every item in the prior review output and flag only obvious new issues introduced by the fixes"
|
|
52
|
+
: "strictly verify only the prior review output — do NOT look for new issues"
|
|
53
|
+
const parts = [
|
|
54
|
+
`## Round ${round} — ${label}`,
|
|
55
|
+
"",
|
|
56
|
+
`[System reminder: this is round ${round} of the convergence protocol. ` +
|
|
57
|
+
`The system prompt for this round has already narrowed the review scope — follow it: ${reminder}.]`,
|
|
58
|
+
"",
|
|
59
|
+
// Prior review output IS in the context (decision 2026-08-08): the FULL
|
|
60
|
+
// verbatim output of the last review — the only complete verification list.
|
|
61
|
+
// The agent response table covers only issues the agent chose to answer,
|
|
62
|
+
// so issues the agent skipped would silently escape convergence without
|
|
63
|
+
// the prior output. The model understands the review output directly —
|
|
64
|
+
// no table/header/phrase parsing. Restatement risk is handled
|
|
65
|
+
// mechanically: host-verified citations reject references that do not
|
|
66
|
+
// match the CURRENT disk state, and fresh sessions exclude old read data.
|
|
67
|
+
// The agent response table stays as a focus aid ("I fixed X"), not as the
|
|
68
|
+
// to-verify list.
|
|
69
|
+
"## Prior Review Output (verify every item it raises)",
|
|
70
|
+
p,
|
|
71
|
+
"",
|
|
72
|
+
"## Agent Response (fix claims — reference only)",
|
|
73
|
+
response,
|
|
74
|
+
"",
|
|
75
|
+
"## Instructions",
|
|
76
|
+
...buildConvergenceInstructions(round, scopeFiles),
|
|
77
|
+
"",
|
|
78
|
+
]
|
|
79
|
+
return parts.join("\n")
|
|
80
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* advisor/history.mjs — advisor history extraction: agent response table and conversation background.
|
|
3
|
+
*/
|
|
4
|
+
import { readFileSync } from "node:fs"
|
|
5
|
+
import { join } from "node:path"
|
|
6
|
+
|
|
7
|
+
export const ADVISOR_MD_PATH = ".thincoder/advisor.md"
|
|
8
|
+
const AGENT_RESPONSE_HEADER = "| # | Action | Detail |"
|
|
9
|
+
|
|
10
|
+
const DEFAULT_CRITERIA = `Review the code changes, focusing on:
|
|
11
|
+
1. Correctness: logic errors, edge cases, off-by-one, incomplete modifications
|
|
12
|
+
2. Security: unhandled exceptions, null references, resource leaks, race conditions
|
|
13
|
+
3. Consistency: alignment with existing project patterns and conventions
|
|
14
|
+
4. Completeness: missing callers, imports, or follow-up changes
|
|
15
|
+
5. Maintainability: vague naming, missing comments, overly complex logic`
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Extract the agent's response table (| # | Action | Detail |) — the fix-claims
|
|
19
|
+
* reference for convergence rounds.
|
|
20
|
+
* Semantics (decision 2026-08-08): without sinceIdx, scan BACKWARD for the
|
|
21
|
+
* MOST RECENT response table (no prior-table index is carried anymore — the
|
|
22
|
+
* agent response is a focus aid only; format drift falls back to the
|
|
23
|
+
* no-response text and never drives control flow). With sinceIdx, scan
|
|
24
|
+
* FORWARD from it (legacy callers/tests).
|
|
25
|
+
* @param {Array} history — message history
|
|
26
|
+
* @param {number} [sinceIdx] — legacy: start scanning forward from this index
|
|
27
|
+
* @returns {string|null} the response table content, or null
|
|
28
|
+
*/
|
|
29
|
+
export function extractAgentResponseTable(history, sinceIdx) {
|
|
30
|
+
const entries = Array.isArray(history) ? history : []
|
|
31
|
+
if (sinceIdx !== undefined) {
|
|
32
|
+
for (let i = sinceIdx; i < entries.length; i++) {
|
|
33
|
+
const m = entries[i]
|
|
34
|
+
if (m.role !== "assistant" || typeof m.content !== "string") continue
|
|
35
|
+
if (m.content.includes(AGENT_RESPONSE_HEADER)) return m.content
|
|
36
|
+
}
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
40
|
+
const m = entries[i]
|
|
41
|
+
if (m.role !== "assistant" || typeof m.content !== "string") continue
|
|
42
|
+
if (m.content.includes(AGENT_RESPONSE_HEADER)) return m.content
|
|
43
|
+
}
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Load review criteria: project .thincoder/advisor.md if present,
|
|
49
|
+
* otherwise the built-in defaults.
|
|
50
|
+
*/
|
|
51
|
+
export function loadAdvisorMd(cwd) {
|
|
52
|
+
try {
|
|
53
|
+
return readFileSync(join(cwd, ADVISOR_MD_PATH), "utf8")
|
|
54
|
+
} catch {
|
|
55
|
+
return DEFAULT_CRITERIA
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Extract recent user↔assistant exchanges (up to maxTurns) for intent context. */
|
|
60
|
+
export function extractConversationBackground(history, maxTurns = 3) {
|
|
61
|
+
const entries = Array.isArray(history) ? history : []
|
|
62
|
+
const lines = []
|
|
63
|
+
let turns = 0
|
|
64
|
+
for (let i = entries.length - 1; i >= 0 && turns < maxTurns; i--) {
|
|
65
|
+
const m = entries[i]
|
|
66
|
+
if (m.role === "tool" || m.role === "system") continue
|
|
67
|
+
if (typeof m.content !== "string") continue
|
|
68
|
+
if (m.content.startsWith("[System reminder:") || m.content.startsWith("[System mode:") || m.content.startsWith("[Relevant memories")) continue
|
|
69
|
+
// ^ [System mode:] is not currently generated anywhere — kept as forward-looking
|
|
70
|
+
// defensive filtering in case convergence messages ever adopt a different prefix.
|
|
71
|
+
if (m.role === "user" || m.role === "assistant") {
|
|
72
|
+
lines.unshift(`${m.role === "user" ? "User" : "Assistant"}: ${m.content.slice(0, 400)}`)
|
|
73
|
+
if (m.role === "user") turns++
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return lines.length > 0 ? lines.join("\n\n") : null
|
|
77
|
+
}
|