thincoder 0.12.59 → 0.12.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -4
- package/README.md +10 -8
- package/bin/thincoder.mjs +99 -133
- package/package.json +6 -4
- package/src/abort-provenance.mjs +116 -0
- package/src/acp/bridge.mjs +45 -21
- package/src/acp.mjs +6 -1
- package/src/advisor/citations.mjs +83 -21
- package/src/advisor/compaction.mjs +174 -0
- package/src/advisor/loop.mjs +293 -0
- package/src/advisor/messages.mjs +59 -137
- package/src/advisor/project-context.mjs +194 -0
- package/src/advisor/repos.mjs +17 -40
- package/src/advisor/run.mjs +156 -359
- package/src/advisor/truncate.mjs +57 -0
- package/src/advisor.mjs +27 -7
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +145 -27
- package/src/agent/helpers.mjs +107 -13
- package/src/agent/record-results.mjs +55 -11
- package/src/agent/relay-prefix.mjs +39 -0
- package/src/agent/run-stages.mjs +242 -0
- package/src/agent/setup-reminders.mjs +69 -0
- package/src/agent/setup.mjs +107 -127
- package/src/agent/spawn-child.mjs +55 -13
- package/src/agent-tools/advisor-async.mjs +346 -0
- package/src/agent-tools/advisor-settle.mjs +231 -0
- package/src/agent-tools/advisor.mjs +167 -116
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/batch-segment.mjs +195 -0
- package/src/agent-tools/consult.mjs +139 -107
- package/src/agent-tools/design-token.mjs +117 -0
- package/src/agent-tools/digest-budget.mjs +76 -0
- package/src/agent-tools/eng.mjs +24 -29
- package/src/agent-tools/escalate-async.mjs +289 -0
- package/src/agent-tools/read-history.mjs +166 -32
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/review-streak.mjs +93 -0
- package/src/agent-tools/settings.mjs +137 -34
- package/src/agent-tools/subagent-actions.mjs +180 -133
- package/src/agent-tools/subagent-async.mjs +184 -177
- package/src/agent-tools/subagent-panel.mjs +160 -0
- package/src/agent-tools/subagent-run.mjs +205 -0
- package/src/agent-tools/subagent-scheduler.mjs +100 -27
- package/src/agent-tools/subagent-spawn.mjs +453 -0
- package/src/agent-tools/subagent.mjs +256 -578
- package/src/agent-tools/verify.mjs +119 -292
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +89 -205
- package/src/cli/distill-command.mjs +12 -6
- package/src/cli/make-agent.mjs +26 -8
- package/src/cli/memory-command.mjs +4 -3
- package/src/cli/permission.mjs +2 -2
- package/src/cli/setup-wizard.mjs +42 -17
- package/src/completions.mjs +114 -0
- package/src/config-migrate.mjs +70 -0
- package/src/config.mjs +180 -63
- package/src/context.mjs +5 -147
- package/src/conventions.mjs +223 -0
- package/src/crash-reports.mjs +128 -0
- package/src/distill.mjs +11 -11
- package/src/expand-home.mjs +16 -0
- package/src/explore-distill.mjs +155 -0
- package/src/generate-title.mjs +1 -1
- package/src/hooks.mjs +7 -3
- package/src/memory/code-index.mjs +9 -3
- package/src/memory/code-sync.mjs +72 -32
- package/src/memory/core.mjs +6 -193
- package/src/memory/delete.mjs +236 -0
- package/src/memory/docs.mjs +68 -54
- package/src/memory/file-walk.mjs +109 -0
- package/src/memory/schema.mjs +15 -3
- package/src/memory.mjs +3 -1
- package/src/model-ref.mjs +66 -0
- package/src/model-specs.mjs +42 -8
- package/src/peer-domains.mjs +265 -0
- package/src/peer-instances.mjs +231 -0
- package/src/prompt-overlays.mjs +82 -0
- package/src/prompts/advisor-design.md +18 -75
- package/src/prompts/advisor-round1.md +14 -67
- package/src/prompts/advisor-round2.md +15 -51
- package/src/prompts/advisor-round3.md +15 -51
- package/src/prompts/common.md +115 -0
- package/src/prompts/consult-base.md +5 -23
- package/src/prompts/discipline-engineering.md +217 -0
- package/src/prompts/discipline-normal.md +179 -0
- package/src/prompts/persona-coder.md +21 -0
- package/src/prompts/persona-eng-coder.md +37 -0
- package/src/prompts/persona-eng-designer.md +55 -0
- package/src/prompts/persona-engineering.md +54 -0
- package/src/prompts/persona-explore.md +15 -0
- package/src/prompts/persona-normal.md +27 -0
- package/src/prompts/persona-plan.md +26 -0
- package/src/provider/anthropic.mjs +4 -4
- package/src/provider/core.mjs +18 -98
- package/src/provider/errors.mjs +101 -0
- package/src/provider/google.mjs +5 -6
- package/src/provider/index.mjs +2 -1
- package/src/provider/list-models.mjs +93 -0
- package/src/provider/rate.mjs +2 -1
- package/src/provider/responses.mjs +5 -3
- package/src/provider/retry.mjs +8 -45
- package/src/provider/sse.mjs +3 -4
- package/src/proxy.mjs +9 -14
- package/src/session-gc.mjs +214 -0
- package/src/session-guard.mjs +47 -0
- package/src/session-rename.mjs +38 -0
- package/src/session-slots.mjs +188 -60
- package/src/session.mjs +104 -124
- package/src/token-ttl.mjs +274 -0
- package/src/tools/{system.mjs → bash.mjs} +19 -221
- package/src/tools/checklist-sync.mjs +181 -0
- package/src/tools/checklist.mjs +52 -39
- package/src/tools/edit-batch.mjs +109 -10
- package/src/tools/edit-diff.mjs +110 -27
- package/src/tools/edit.md +17 -12
- package/src/tools/execute.mjs +31 -4
- package/src/tools/file.mjs +41 -16
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +23 -34
- package/src/tools/glob-dialect.mjs +130 -0
- package/src/tools/glob.md +3 -3
- package/src/tools/grep.md +1 -1
- package/src/tools/index.mjs +9 -8
- package/src/tools/ops.mjs +188 -3
- package/src/tools/patch.mjs +3 -3
- package/src/tools/question.md +4 -0
- package/src/tools/question.mjs +26 -0
- package/src/tools/read.md +1 -2
- package/src/tools/read_image.md +1 -1
- package/src/tools/search.mjs +236 -0
- package/src/tools/shared.mjs +14 -13
- package/src/tools/wait_for.md +22 -0
- package/src/tui/agent-turn.mjs +36 -228
- package/src/tui/ansi.mjs +2 -0
- package/src/tui/clipboard.mjs +7 -1
- package/src/tui/cmd-advisor.mjs +3 -2
- package/src/tui/cmd-config.mjs +142 -30
- package/src/tui/cmd-eng.mjs +28 -40
- package/src/tui/cmd-exit.mjs +6 -8
- package/src/tui/cmd-mcp.mjs +8 -2
- package/src/tui/cmd-model.mjs +14 -12
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-reindex.mjs +7 -0
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-submodel.mjs +8 -5
- package/src/tui/cmd-think.mjs +10 -10
- package/src/tui/cmd-undo.mjs +4 -3
- package/src/tui/cmd-upgrade.mjs +19 -4
- package/src/tui/config-helpers.mjs +28 -16
- package/src/tui/distill-cmd.mjs +1 -1
- package/src/tui/index.mjs +40 -38
- package/src/tui/interaction.mjs +3 -3
- package/src/tui/key-handler.mjs +61 -17
- package/src/tui/key-modes.mjs +86 -8
- package/src/tui/layout.mjs +18 -10
- package/src/tui/model-catalog.mjs +89 -0
- package/src/tui/model-picker.mjs +498 -0
- package/src/tui/mouse.mjs +52 -9
- package/src/tui/pickers.mjs +28 -392
- package/src/tui/render-frame.mjs +32 -16
- package/src/tui/render-loop.mjs +2 -0
- package/src/tui/render-segments.mjs +12 -9
- package/src/tui/render.mjs +37 -5
- package/src/tui/slash-commands.mjs +2 -2
- package/src/tui/startup.mjs +4 -0
- package/src/tui/subagent-blocks.mjs +106 -295
- package/src/tui/subagent-children.mjs +162 -0
- package/src/tui/subagent-freeze.mjs +169 -0
- package/src/tui/subagent-panel.mjs +24 -31
- package/src/tui/suspension-drive.mjs +297 -0
- package/src/tui/tool-args.mjs +7 -5
- package/src/tui/tool-display.mjs +143 -0
- package/src/tui/tool-events.mjs +56 -185
- package/src/tui/tui-lifecycle.mjs +46 -4
- package/src/tui/update-notice.mjs +4 -0
- package/src/tui/wizard.mjs +61 -21
- package/src/tui/wrapped-spawn.mjs +38 -0
- package/src/prompts/coder.md +0 -56
- package/src/prompts/discipline.md +0 -102
- package/src/prompts/eng-coder.md +0 -44
- package/src/prompts/engineering-sub.md +0 -34
- package/src/prompts/engineering.md +0 -340
- package/src/prompts/explore.md +0 -21
- package/src/prompts/main.md +0 -56
- package/src/prompts/methodology-template.md +0 -58
- package/src/prompts/plan.md +0 -16
- package/src/prompts/system.md +0 -63
- package/src/tools/pdf-parse-text.mjs +0 -497
- package/src/tools/pdf-parse-xref.mjs +0 -499
- package/src/tools/pdf.mjs +0 -155
- package/src/tools/read_pdf.md +0 -21
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* advisor/loop.mjs — advisor tool loop: chat → execute tools → repeat, plus the
|
|
3
|
+
* review timeline (split out of advisor/run.mjs, 第 11 批 — run.mjs was 498/500
|
|
4
|
+
* 硬帽;拆分保持既有 import 面:run.mjs 继续 re-export 本文件导出)。
|
|
5
|
+
*
|
|
6
|
+
* 第 11 批(F15/§14.6):每次 chat 调用携带硬墙信号(`AbortSignal.any([signal,
|
|
7
|
+
* AbortSignal.timeout(remaining)])`;墙判定绑信号状态——抛错 / partial 两形态同判),
|
|
8
|
+
* 并按 0.75 一次性预算提示 + 结构化超时尾收尾;守卫与限额函数在 compaction.mjs。
|
|
9
|
+
*/
|
|
10
|
+
import { chat } from "../provider/core.mjs"
|
|
11
|
+
import { providerSpec } from "../config.mjs"
|
|
12
|
+
import { toOpenAISchema } from "../tools/index.mjs"
|
|
13
|
+
import { describeToolArgs } from "../tui/tool-args.mjs"
|
|
14
|
+
import { truncateAdvisorResult } from "./truncate.mjs"
|
|
15
|
+
import { batchSegmentTool } from "../agent-tools/batch-segment.mjs"
|
|
16
|
+
import {
|
|
17
|
+
estimateTokens, compactMessages, shouldBudgetNudge, budgetNudgeText, timeoutTail, renderTimeline,
|
|
18
|
+
MAX_ADVISOR_TURNS, advisorContextBudget, TOOL_TIMEOUT_MS, REVIEW_TIMEOUT_MS, MAX_RESULT_CHARS,
|
|
19
|
+
ADVISOR_THINKING_PLACEHOLDER,
|
|
20
|
+
} from "./compaction.mjs"
|
|
21
|
+
|
|
22
|
+
const { readTool, globTool, grepTool, lsTool } = await import("../tools/index.mjs")
|
|
23
|
+
const { lspTool } = await import("../tools/lsp.mjs")
|
|
24
|
+
const { codeSearchTool } = await import("../memory/code-sync.mjs")
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Advisor tool set — ZERO git, read-only ONLY, every round. The change surface
|
|
28
|
+
* comes from the review scope (paths / _touchedFiles injected by the caller),
|
|
29
|
+
* never from git: git output misled reviews (committed fixes never show in
|
|
30
|
+
* `git diff HEAD`, so "no changes" was read as "not fixed") and the user
|
|
31
|
+
* mandate is full decoupling (7d49a52 + d3be613). The reviewer reads files
|
|
32
|
+
* and searches code; it never touches git and never writes.
|
|
33
|
+
* No round parameter — the set is constant across all rounds.
|
|
34
|
+
* @param {Object} agent — only used for the code index (agent.memory); the
|
|
35
|
+
* semantic code_search tool needs it. Without a memory, the set is 5 tools.
|
|
36
|
+
*/
|
|
37
|
+
function advisorToolsFor(agent, reviewType = "code", batchDoc = null) {
|
|
38
|
+
const search = agent?.memory ? codeSearchTool(agent.memory) : null
|
|
39
|
+
const tools = search
|
|
40
|
+
? [readTool, globTool, grepTool, lsTool, lspTool, search]
|
|
41
|
+
: [readTool, globTool, grepTool, lsTool, lspTool]
|
|
42
|
+
// §2.20.3(第 4 批):**只有绑定了批次档的设计评审**额外拿到写通道——代码评审工具集
|
|
43
|
+
// 逐字节不变(零 git + 只读不变量,§2.20.8 #1);未绑定 → 不挂载(fail-closed)。
|
|
44
|
+
if (reviewType === "design" && batchDoc) tools.push(batchSegmentTool(batchDoc, { review: true }))
|
|
45
|
+
return { schemas: tools.map(toOpenAISchema), byName: new Map(tools.map((t) => [t.name, t])) }
|
|
46
|
+
}
|
|
47
|
+
// Test seam: the tool set is pure (agent.memory → code_search inclusion).
|
|
48
|
+
export { advisorToolsFor, advisorToolsFor as _advisorToolsFor }
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Run the advisor's tool loop: chat → execute tools → repeat.
|
|
52
|
+
* Stops when the model produces text without tool calls.
|
|
53
|
+
*
|
|
54
|
+
* Progress lines (→ tool args) are emitted via onOutput between model bursts so
|
|
55
|
+
* the panel keeps moving while the advisor explores — otherwise the panel sits
|
|
56
|
+
* frozen through every tool-call phase and the review appears to have stalled.
|
|
57
|
+
*
|
|
58
|
+
* @param {string|null} [pinned] — 第 11 批:压缩定锚简报(评审参数构建——F13/§14.4 #3)。
|
|
59
|
+
* @param {{now?: Function, chat?: Function}} [seams] — 测试缝(默认 Date.now / chat——
|
|
60
|
+
* 生产调用不传,默认回退零行为变)。
|
|
61
|
+
*/
|
|
62
|
+
async function runAdvisorToolLoop(provider, messages, onOutput, signal, agent, cwd, toolsOverride = null, reviewType = "code", batchDoc = null, pinned = null, seams = {}) {
|
|
63
|
+
const now = seams.now ?? Date.now
|
|
64
|
+
const chatCall = seams.chat ?? chat
|
|
65
|
+
// 第 11 批硬墙 / 预算 / 尾:实现注解见下方各点;守卫函数与 renderTimeline 在 compaction.mjs。
|
|
66
|
+
// Kind-tagged wrappers: the TUI panel colors reasoning / answer / tool progress differently.
|
|
67
|
+
// Every chunk is ALSO recorded into an ordered timeline — the persisted record
|
|
68
|
+
// must show the review process (thinking ↔ tool progress ↔ final text) at its
|
|
69
|
+
// real positions, not a summary appended at the end. Same-kind consecutive
|
|
70
|
+
// chunks merge (token streams); kind flips start a new entry.
|
|
71
|
+
const timeline = []
|
|
72
|
+
const record = (kind, text) => {
|
|
73
|
+
const last = timeline.at(-1)
|
|
74
|
+
if (last && last.kind === kind) last.text += text
|
|
75
|
+
else timeline.push({ kind, text })
|
|
76
|
+
}
|
|
77
|
+
const emit = (kind) => (text) => { record(kind, text); onOutput?.({ kind, text }) }
|
|
78
|
+
const onThink = emit("think")
|
|
79
|
+
const onText = emit("text")
|
|
80
|
+
const onTool = emit("tool")
|
|
81
|
+
// toolsOverride = test seam (T-TS8/9): the real advisor tool set, or a mock
|
|
82
|
+
// set with controllable timing/errors.
|
|
83
|
+
const { schemas: toolSchemas, byName: toolByName } = toolsOverride ?? advisorToolsFor(agent, reviewType, batchDoc)
|
|
84
|
+
let turns = 0
|
|
85
|
+
let toolCallCount = 0
|
|
86
|
+
let reviewTextProduced = false
|
|
87
|
+
let budgetNudged = false
|
|
88
|
+
const startTime = now()
|
|
89
|
+
// 第 25 批(§16.3):上下文预算跟随评审模型窗口——`providerSpec`(模型规格表 × provider 级
|
|
90
|
+
// context 覆盖)派生;函数体内、while 轮次外一次性(provider 全场不变),两档消费见下守卫。
|
|
91
|
+
const budget = advisorContextBudget(provider)
|
|
92
|
+
|
|
93
|
+
while (true) {
|
|
94
|
+
// Interrupted (Ctrl+I) — stop immediately instead of spinning a fresh uncancellable signal
|
|
95
|
+
if (signal?.aborted) return renderTimeline(timeline, "Advisor: interrupted.")
|
|
96
|
+
|
|
97
|
+
// Check review timeout (10 minutes by default; agent.advisor.timeoutMs overrides)
|
|
98
|
+
// 运行时校验(设计评审 #1,2026-08-24):手写 config.json 的非法值(0/负数/字符串)
|
|
99
|
+
// 不得静默禁用或立即触发超时——非法一律回退默认。
|
|
100
|
+
const cfg = agent.config?.advisor?.timeoutMs
|
|
101
|
+
const timeoutMs = (Number.isFinite(cfg) && cfg > 0) ? cfg : REVIEW_TIMEOUT_MS
|
|
102
|
+
const elapsed = now() - startTime
|
|
103
|
+
const remaining = timeoutMs - elapsed
|
|
104
|
+
// 硬墙(§14.6 #1):预算用尽 → 结构化超时尾(首行 = 判定族 timeout 前缀)。
|
|
105
|
+
if (remaining <= 0) {
|
|
106
|
+
return renderTimeline(timeline, timeoutTail(timeoutMs, turns, toolCallCount, reviewTextProduced))
|
|
107
|
+
}
|
|
108
|
+
// 0.75 一次性预算提示(§14.6 #2——同一检查点、每场评审至多一次):注入一条 user 消息
|
|
109
|
+
// 促模型在墙前收敛产出(不改语义判据、不碰提示词面)。
|
|
110
|
+
if (shouldBudgetNudge(elapsed, timeoutMs, budgetNudged)) {
|
|
111
|
+
budgetNudged = true
|
|
112
|
+
messages.push({ role: "user", content: budgetNudgeText(elapsed, timeoutMs) })
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (++turns > MAX_ADVISOR_TURNS) {
|
|
116
|
+
return renderTimeline(timeline, "Advisor: stopped after " + MAX_ADVISOR_TURNS + " tool rounds — the review appears to be looping. You may retry with a narrower scope.")
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Check context window and compact if needed
|
|
120
|
+
const currentTokens = estimateTokens(messages)
|
|
121
|
+
if (currentTokens > budget.compactAt) {
|
|
122
|
+
onText(`\n[Context compacted: ${currentTokens} tokens → reducing to fit window]\n`)
|
|
123
|
+
compactMessages(messages, pinned)
|
|
124
|
+
if (estimateTokens(messages) > budget.limit) {
|
|
125
|
+
// Report the POST-compaction count — the pre-compaction currentTokens
|
|
126
|
+
// is stale by the time compaction has run.
|
|
127
|
+
return renderTimeline(timeline, `Advisor: context window limit reached (${estimateTokens(messages)} tokens). Review incomplete — too many tool calls. Try a narrower scope.`)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// LLM generation silence: the reasoning phase produces no SSE bytes for
|
|
132
|
+
// seconds to tens of seconds (server-side prefill on large contexts, per
|
|
133
|
+
// tool-round LLM return). A placeholder keeps the panel visibly working.
|
|
134
|
+
// kind "think" (NOT "text"): the placeholder must land in the SAME buffer
|
|
135
|
+
// and position as the upcoming reasoning — a "text"-kind placeholder
|
|
136
|
+
// rendered BELOW the think block, and the reasoning stream appeared ABOVE
|
|
137
|
+
// it ("the stream runs back to the front"). Same buffer = same spot; the
|
|
138
|
+
// reasoning continues right where the placeholder sits.
|
|
139
|
+
onOutput?.({ kind: "think", text: ADVISOR_THINKING_PLACEHOLDER })
|
|
140
|
+
|
|
141
|
+
// 硬墙(§14.6 #1):单次请求信号 = 用户信号 × 本调用 deadline(remaining)。复合信号
|
|
142
|
+
// 无条件传入(上层检查与本调用之间的中止仍必须取消请求——已 aborted 的 composite 使请求
|
|
143
|
+
// 立即失败);此处改正了原指向 provider/core.mjs 组合 AbortSignal 的陈旧注释(§14.10 #3)。
|
|
144
|
+
const callSignal = signal
|
|
145
|
+
? AbortSignal.any([signal, AbortSignal.timeout(remaining)])
|
|
146
|
+
: AbortSignal.timeout(remaining)
|
|
147
|
+
let response
|
|
148
|
+
try {
|
|
149
|
+
response = await chatCall(provider, {
|
|
150
|
+
messages,
|
|
151
|
+
tools: toolSchemas,
|
|
152
|
+
signal: callSignal,
|
|
153
|
+
onToken: (t) => { if (String(t ?? "").trim()) reviewTextProduced = true; onText(t) },
|
|
154
|
+
onReasoning: onThink,
|
|
155
|
+
// LOGGING(vscode advisor/run.mjs parity——按 stage 可 grep)
|
|
156
|
+
// §18.6 D-TR4:轨迹元数据增补——kind=advisor(评审独立于子代理——T-TR2);role
|
|
157
|
+
// 透出调用方角色(eng-coder 内嵌评审时为 "eng-coder");session/cwd 供轨迹对回;
|
|
158
|
+
// traces 开关沿 agent.config(D-TR6)。
|
|
159
|
+
logCtx: {
|
|
160
|
+
stage: "advisor",
|
|
161
|
+
role: agent?._role ?? null,
|
|
162
|
+
kind: "advisor",
|
|
163
|
+
session: agent?._sessionStart ?? null,
|
|
164
|
+
cwd,
|
|
165
|
+
traces: agent?.config?.traces?.enabled !== false,
|
|
166
|
+
},
|
|
167
|
+
})
|
|
168
|
+
} catch (e) {
|
|
169
|
+
// 墙判定绑信号状态(§14.6 #1——非异常名):① 用户信号已中止 ⇒ 原样上抛(中断语义
|
|
170
|
+
// 零变);② 复合信号已中止(墙触发)而用户信号未中止 ⇒ 结构化超时尾(形态①:抛错;
|
|
171
|
+
// AbortError / TimeoutError 两名兜底——AbortSignal.timeout 的 reason 是 TimeoutError
|
|
172
|
+
// DOMException);③ 其余错误原样上抛(runAdvisorReview 的失败分类不变)。
|
|
173
|
+
if (signal?.aborted) throw e
|
|
174
|
+
if (callSignal.aborted || e?.name === "AbortError" || e?.name === "TimeoutError") {
|
|
175
|
+
return renderTimeline(timeline, timeoutTail(timeoutMs, turns, toolCallCount, reviewTextProduced))
|
|
176
|
+
}
|
|
177
|
+
throw e
|
|
178
|
+
}
|
|
179
|
+
if (signal?.aborted) return renderTimeline(timeline, "Advisor: interrupted.")
|
|
180
|
+
// 形态②(§14.6 #1):不抛错而返回 partial(流已有内容时中断以 partial:true 透传)——
|
|
181
|
+
// 不得按普通结果收尾:墙触发(复合信号已中止)同判。
|
|
182
|
+
if (callSignal.aborted && response?.partial) {
|
|
183
|
+
return renderTimeline(timeline, timeoutTail(timeoutMs, turns, toolCallCount, reviewTextProduced))
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// No tool calls — this is the final review text. The final answer was
|
|
187
|
+
// already streamed into the timeline via onText; fall back to
|
|
188
|
+
// response.content only if nothing was recorded.
|
|
189
|
+
if (!response.toolCalls?.length) {
|
|
190
|
+
if (!response.content?.trim()) return renderTimeline(timeline) || "Advisor: empty response — review was inconclusive"
|
|
191
|
+
return renderTimeline(timeline) || response.content.trim()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Push assistant message with tool calls. reasoning_content ECHO is
|
|
195
|
+
// mandatory for reasoningEcho:"required" providers (deepseek/kimi): the
|
|
196
|
+
// server stops returning reasoning_content on later rounds when the
|
|
197
|
+
// tool-call assistant history lacks it — the observed "reasoning stops
|
|
198
|
+
// after the first tool call, returns only at the final answer" symptom.
|
|
199
|
+
// Mirrors the main agent's push (agent.mjs).
|
|
200
|
+
messages.push({
|
|
201
|
+
role: "assistant",
|
|
202
|
+
content: response.content || null,
|
|
203
|
+
tool_calls: response.toolCalls.map((tc) => ({
|
|
204
|
+
id: tc.id, type: "function",
|
|
205
|
+
function: { name: tc.name, arguments: tc.arguments },
|
|
206
|
+
})),
|
|
207
|
+
...(response.reasoning && providerSpec(provider).reasoningEcho === "required"
|
|
208
|
+
? { reasoning_content: response.reasoning }
|
|
209
|
+
: {}),
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
// B1 (AGENT-LOOP.md §18.7 D-TS7): the SAME LLM reply's multiple read-only
|
|
213
|
+
// tool calls run in PARALLEL (Promise.all) — results are backfilled in
|
|
214
|
+
// toolCalls order (Promise.all preserves the input order → tool_call_id
|
|
215
|
+
// never mismatches); each tool's timeout/error is captured independently
|
|
216
|
+
// (the existing TOOL_TIMEOUT stays — one failing tool does not block the
|
|
217
|
+
// others); progress lines are emitted in toolCalls order. The read-only
|
|
218
|
+
// tool set has no side effects — no sequencing/serialization needed.
|
|
219
|
+
// Scope note (round1 review #10): B1 is ONLY in-loop tool parallelism — it
|
|
220
|
+
// does NOT solve the TODO "platform execution: advisor parallel calls are
|
|
221
|
+
// actually serial" mystery (docs/TODO.md — LOGGING evidence item), which
|
|
222
|
+
// concerns multiple advisor CALLS observed as serial, not one reply's
|
|
223
|
+
// tool calls.
|
|
224
|
+
const parsed = response.toolCalls.map((tc) => {
|
|
225
|
+
const tool = toolByName.get(tc.name)
|
|
226
|
+
let args = {}
|
|
227
|
+
let parseError = null
|
|
228
|
+
try {
|
|
229
|
+
args = JSON.parse(tc.arguments || "{}")
|
|
230
|
+
} catch (e) {
|
|
231
|
+
parseError = `Error: invalid JSON in tool arguments: ${e.message}\nRaw arguments: ${(tc.arguments || "").slice(0, 200)}`
|
|
232
|
+
}
|
|
233
|
+
return { tc, tool, args, parseError }
|
|
234
|
+
})
|
|
235
|
+
toolCallCount += parsed.length
|
|
236
|
+
// Progress lines first, in toolCalls order (emitted before the parallel
|
|
237
|
+
// run — display order is independent of completion order).
|
|
238
|
+
for (const p of parsed) {
|
|
239
|
+
if (p.parseError) continue // parse-error tools get no progress line (legacy behavior)
|
|
240
|
+
const argsLine = describeToolArgs(p.tc.name, p.args)
|
|
241
|
+
onTool(`\n→ ${p.tc.name}${argsLine ? " " + argsLine : ""}\n`)
|
|
242
|
+
}
|
|
243
|
+
// Every tool runs CONCURRENTLY; each result/error lands in its own slot —
|
|
244
|
+
// Promise.all preserves input order, so index i always matches parsed[i].
|
|
245
|
+
const executed = await Promise.all(parsed.map(async (p) => {
|
|
246
|
+
// Parse failure → error to model immediately (no execution)
|
|
247
|
+
if (p.parseError) return p.parseError
|
|
248
|
+
if (!p.tool) return `Error: unknown tool "${p.tc.name}". Available: ${[...toolByName.keys()].join(", ")}`
|
|
249
|
+
// Execute with timeout (clear the timer when the tool wins the race —
|
|
250
|
+
// otherwise up to MAX_ADVISOR_TURNS dangling timers accumulate)
|
|
251
|
+
try {
|
|
252
|
+
let timeoutId
|
|
253
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
254
|
+
timeoutId = setTimeout(() => reject(new Error(`tool timeout after ${TOOL_TIMEOUT_MS}ms`)), TOOL_TIMEOUT_MS)
|
|
255
|
+
})
|
|
256
|
+
let toolPromise
|
|
257
|
+
try {
|
|
258
|
+
toolPromise = p.tool.execute(p.args, { cwd, agent, onOutput, signal })
|
|
259
|
+
return await Promise.race([toolPromise, timeoutPromise])
|
|
260
|
+
} finally {
|
|
261
|
+
clearTimeout(timeoutId)
|
|
262
|
+
// Timeout won → toolPromise is still pending; a later rejection
|
|
263
|
+
// would surface as an unhandled rejection. The race already
|
|
264
|
+
// consumed the result/error in the normal path, so this no-op
|
|
265
|
+
// catch only fires for the abandoned-tool case.
|
|
266
|
+
toolPromise?.catch(() => {})
|
|
267
|
+
}
|
|
268
|
+
} catch (e) {
|
|
269
|
+
const errorType = e.message.includes("timeout") ? "timeout"
|
|
270
|
+
: e.message.includes("ENOENT") ? "file_not_found"
|
|
271
|
+
: e.message.includes("permission") ? "permission_denied"
|
|
272
|
+
: "execution_error"
|
|
273
|
+
return `Error (${errorType}): ${e.message}`
|
|
274
|
+
}
|
|
275
|
+
}))
|
|
276
|
+
|
|
277
|
+
// Backfill in toolCalls order (executed[i] ↔ parsed[i]); per-result
|
|
278
|
+
// non-string serialization + dual-end line-aware truncation stay per-tool
|
|
279
|
+
// (DUAL-END-TRUNCATION F-2 — truncate.mjs: head ≈60% + tail ≈40% — keep the
|
|
280
|
+
// tail verdicts; ≤ MAX_RESULT_CHARS results pass through untouched).
|
|
281
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
282
|
+
let result = executed[i]
|
|
283
|
+
if (typeof result !== "string") result = JSON.stringify(result)
|
|
284
|
+
|
|
285
|
+
result = truncateAdvisorResult(result, MAX_RESULT_CHARS)
|
|
286
|
+
|
|
287
|
+
messages.push({ role: "tool", tool_call_id: parsed[i].tc.id, content: result })
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// Test seam (T-TS8/9): the tool loop itself — toolsOverride injects a mock tool
|
|
292
|
+
// set with controllable timing/errors (the real set comes from advisorToolsFor).
|
|
293
|
+
export { runAdvisorToolLoop, runAdvisorToolLoop as _runAdvisorToolLoop }
|
package/src/advisor/messages.mjs
CHANGED
|
@@ -2,108 +2,17 @@
|
|
|
2
2
|
* advisor/messages.mjs — advisor user-message building (buildAdvisorUserMessage).
|
|
3
3
|
* Split out of advisor.mjs to keep it under the 300-line advisory threshold
|
|
4
4
|
* (.thincoder/advisor.md). System prompts live in advisor.mjs / prompts/.
|
|
5
|
+
* Project-context discovery/injection lives in project-context.mjs (the D-1 registration,
|
|
6
|
+
* ENGINEERING-MODE.md §2.26.3); the pre-split surface is re-exported below.
|
|
5
7
|
*/
|
|
6
|
-
import {
|
|
7
|
-
import { resolve, join, relative, dirname, sep } from "node:path"
|
|
8
|
-
import { providerSpec } from "../config.mjs"
|
|
8
|
+
import { join, relative, sep } from "node:path"
|
|
9
9
|
import { findReviewRepos, collectRepoSnapshots, collectChangedFiles } from "./repos.mjs"
|
|
10
10
|
import { buildConvergenceBody, buildConvergenceInstructions } from "./convergence.mjs"
|
|
11
11
|
import { loadAdvisorMd, extractConversationBackground, extractAgentResponseTable } from "./history.mjs"
|
|
12
|
+
import { injectProjectGuide, injectDocumentMap, injectProjectStandards, NO_GIT_NOTICE } from "./project-context.mjs"
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
* window for the doc map, small windows still get a floor so the map is always
|
|
16
|
-
* visible. The map is what tells the reviewer WHERE the requirements docs live
|
|
17
|
-
* (requirement-fit is judged against those docs, not the conversation only). */
|
|
18
|
-
const PROJECT_GUIDE_MIN = 8192 // chars — floor for small-window models
|
|
19
|
-
const PROJECT_GUIDE_FRACTION = 0.05 // 5% of the reviewer model's context window
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Discover the project root for the review — user decision 2026-08-08:
|
|
23
|
-
* the project root is a SUBDIRECTORY of the working directory, never an
|
|
24
|
-
* ancestor above it. Priority:
|
|
25
|
-
* 1. Walk UP from each review-scope file's directory, bounded by cwd —
|
|
26
|
-
* the NEAREST AGENTS.md inside the workspace wins. In a monorepo this is
|
|
27
|
-
* the subproject's own doc map even when cwd itself has an AGENTS.md
|
|
28
|
-
* (a workspace-level meta map must not shadow the subproject guide).
|
|
29
|
-
* 2. No scope files / nothing found → cwd (single project; the walk's
|
|
30
|
-
* last step naturally lands on cwd's own AGENTS.md when it exists).
|
|
31
|
-
* @param {string} cwd — the agent's working directory (workspace root)
|
|
32
|
-
* @param {string[]} scopeFiles — cwd-relative review-scope paths (may be empty)
|
|
33
|
-
* @returns {string|null} absolute project root with an AGENTS.md, or null
|
|
34
|
-
*/
|
|
35
|
-
function findProjectRoot(cwd, scopeFiles) {
|
|
36
|
-
// Normalize separators before comparing: input paths may use either
|
|
37
|
-
// convention (join() → "\\" on Windows; tool args / tests → "/"). Mixed
|
|
38
|
-
// styles made isInside(cwd + sep) miss legitimately nested paths.
|
|
39
|
-
const norm = (p) => p.replaceAll("\\", "/")
|
|
40
|
-
const isInside = (dir) => {
|
|
41
|
-
const d = norm(dir)
|
|
42
|
-
const c = norm(cwd)
|
|
43
|
-
return d === c || d.startsWith(c + "/")
|
|
44
|
-
}
|
|
45
|
-
for (const f of scopeFiles) {
|
|
46
|
-
let dir = dirname(resolve(cwd, f))
|
|
47
|
-
while (isInside(dir) && dir !== dirname(dir)) {
|
|
48
|
-
if (existsSync(join(dir, "AGENTS.md"))) return dir
|
|
49
|
-
dir = dirname(dir)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
// No scope files, or none found in the walk — cwd itself (its AGENTS.md is
|
|
53
|
-
// checked as the walk's final step for scope files; for empty scopes, check
|
|
54
|
-
// it explicitly so a bare cwd project still gets its guide).
|
|
55
|
-
if (existsSync(join(cwd, "AGENTS.md"))) return cwd
|
|
56
|
-
return null
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Inject the project guide (AGENTS.md) into the review message. AGENTS.md is the
|
|
61
|
-
* project's doc map — it defines the structure and where requirements/design
|
|
62
|
-
* documents live. The reviewer must see it FIRST: requirement-fit is judged
|
|
63
|
-
* against the documents it points to, with the conversation background as a
|
|
64
|
-
* supplement. Absent AGENTS.md degrades honestly (no pretending there is a map).
|
|
65
|
-
* @param {Object} agent — the parent agent
|
|
66
|
-
* @param {string[]} parts — message parts (mutated)
|
|
67
|
-
* @param {string[]} [scopeFiles] — cwd-relative review-scope paths for project-root discovery
|
|
68
|
-
* @returns {string|null} the discovered project root (abs), or null when no guide
|
|
69
|
-
*/
|
|
70
|
-
function injectProjectGuide(agent, parts, scopeFiles = []) {
|
|
71
|
-
parts.push("## Project Guide (AGENTS.md)")
|
|
72
|
-
const root = findProjectRoot(agent.cwd, scopeFiles)
|
|
73
|
-
const path = root ? join(root, "AGENTS.md") : null
|
|
74
|
-
let text
|
|
75
|
-
if (!path) {
|
|
76
|
-
parts.push("(No AGENTS.md found — neither at the working directory root nor in any review-scope subdirectory. Judge the user's requirements from the conversation background, and say so explicitly if the requirements are unclear.)")
|
|
77
|
-
parts.push("")
|
|
78
|
-
return null // no guide — requirement-fit falls back to the conversation
|
|
79
|
-
}
|
|
80
|
-
try {
|
|
81
|
-
text = readFileSync(path, "utf8")
|
|
82
|
-
} catch (e) {
|
|
83
|
-
if (e.code !== "ENOENT") {
|
|
84
|
-
// File exists but is unreadable (EACCES etc.) — log, don't masquerade as "not found".
|
|
85
|
-
console.warn(`[advisor] AGENTS.md unreadable at ${path}: ${e.message}`)
|
|
86
|
-
}
|
|
87
|
-
parts.push("(No AGENTS.md found — neither at the working directory root nor in any review-scope subdirectory. Judge the user's requirements from the conversation background, and say so explicitly if the requirements are unclear.)")
|
|
88
|
-
parts.push("")
|
|
89
|
-
return null // no guide — requirement-fit falls back to the conversation
|
|
90
|
-
}
|
|
91
|
-
// readFileSync succeeded — compute the budget OUTSIDE the try so a spec
|
|
92
|
-
// lookup failure can never masquerade as "no AGENTS.md".
|
|
93
|
-
// providerSpec: the project-guide budget follows the provider-level context
|
|
94
|
-
// override (PROVIDER.md §15 — advisor messages budget is context-based).
|
|
95
|
-
const ctx = providerSpec(agent.provider).context
|
|
96
|
-
const cap = Math.max(PROJECT_GUIDE_MIN, Math.floor(ctx * PROJECT_GUIDE_FRACTION))
|
|
97
|
-
const shown = text.length <= cap
|
|
98
|
-
? text
|
|
99
|
-
: [...text].slice(0, cap).join("") + `\n\n…(truncated at ${cap} chars — read the full file if you need more)` // codepoint-safe slice: no broken surrogate pairs at the boundary
|
|
100
|
-
parts.push(`<!-- Project root: ${relative(agent.cwd, path).split(sep).join("/")} (inferred from the review scope under ${agent.cwd}) -->`)
|
|
101
|
-
parts.push("This file defines the project's structure and where its requirements/design documents live. Read the documents it points to — the user's requirements live THERE, not only in the conversation background.")
|
|
102
|
-
parts.push("")
|
|
103
|
-
parts.push(shown)
|
|
104
|
-
parts.push("")
|
|
105
|
-
return root // guide injected — requirement-fit criteria apply (truthy root)
|
|
106
|
-
}
|
|
14
|
+
// Structural split (not an authority migration): the moved helpers stay reachable here.
|
|
15
|
+
export { findProjectRoot, injectProjectGuide } from "./project-context.mjs"
|
|
107
16
|
|
|
108
17
|
/**
|
|
109
18
|
* Build the mechanical review-object declaration block (AGENT-LOOP.md §18.8
|
|
@@ -129,6 +38,25 @@ export function buildObjectDeclarationBlock(object = null) {
|
|
|
129
38
|
].join("\n")
|
|
130
39
|
}
|
|
131
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
|
+
|
|
132
60
|
/**
|
|
133
61
|
* Build the user message for an advisor review session.
|
|
134
62
|
* @param {Object} agent — the parent agent
|
|
@@ -142,9 +70,23 @@ export function buildObjectDeclarationBlock(object = null) {
|
|
|
142
70
|
* @param {Object|null} [object] — review-object declaration (§18.8 D-OA1/D-OA3):
|
|
143
71
|
* { type, target, status, reason, exclude } — mechanically injected at the
|
|
144
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).
|
|
145
76
|
* @returns {string} the user message
|
|
146
77
|
*/
|
|
147
|
-
export function buildAdvisorUserMessage(agent, prior, reviewType, designToken = null, documents = null, paths = null, object = null) {
|
|
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) {
|
|
148
90
|
// prior = the full prior review output (string) when a convergence round is
|
|
149
91
|
// being built (decision 2026-08-08 — verbatim injection, model understands it).
|
|
150
92
|
// Deterministic: only _advisorRound > 0 with stored output counts.
|
|
@@ -172,6 +114,11 @@ export function buildAdvisorUserMessage(agent, prior, reviewType, designToken =
|
|
|
172
114
|
if (reviewType === "design" && (agent._advisorRound || 0) === 0) {
|
|
173
115
|
const repos = findReviewRepos(agent)
|
|
174
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
|
+
}
|
|
175
122
|
if (docList.length > 0) {
|
|
176
123
|
// Explicit review scope (engineering mode, FR2): the caller hands over the
|
|
177
124
|
// doc list — the advisor reviews ONLY these. No git-diff change-set
|
|
@@ -207,48 +154,30 @@ export function buildAdvisorUserMessage(agent, prior, reviewType, designToken =
|
|
|
207
154
|
}
|
|
208
155
|
}
|
|
209
156
|
|
|
210
|
-
// Engineering mode:
|
|
211
|
-
//
|
|
157
|
+
// Engineering mode: the project's DECLARED standards document
|
|
158
|
+
// (advisor.standardsDoc) — undeclared degrades visibly (PO-2).
|
|
212
159
|
if (agent.config?.agent?.engineering) {
|
|
213
|
-
|
|
214
|
-
const mpath = resolve(guideRoot ?? agent.cwd, "METHODOLOGY.md")
|
|
215
|
-
const methodology = readFileSync(mpath, "utf8")
|
|
216
|
-
parts.push("## Project Methodology")
|
|
217
|
-
parts.push("Evaluate the design against this methodology:")
|
|
218
|
-
parts.push(methodology)
|
|
219
|
-
parts.push("")
|
|
220
|
-
} catch { /* file doesn't exist — skip */ }
|
|
160
|
+
injectProjectStandards(agent, parts, guideRoot)
|
|
221
161
|
}
|
|
222
162
|
|
|
223
|
-
// Document map (docs/
|
|
224
|
-
//
|
|
225
|
-
// (
|
|
226
|
-
|
|
227
|
-
try {
|
|
228
|
-
const mapPath = resolve(guideRoot ?? agent.cwd, "docs", "design", "README.md")
|
|
229
|
-
if (existsSync(mapPath)) {
|
|
230
|
-
parts.push("## Document Map")
|
|
231
|
-
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.")
|
|
232
|
-
parts.push(readFileSync(mapPath, "utf8"))
|
|
233
|
-
parts.push("")
|
|
234
|
-
}
|
|
235
|
-
} catch { /* file doesn't exist or is unreadable — skip */ }
|
|
163
|
+
// Document map: declared path wins, the built-in fallback probe (docs/README.md
|
|
164
|
+
// → docs/design/README.md) stays; neither → explicit degradation sentence
|
|
165
|
+
// (never the old silent skip).
|
|
166
|
+
injectDocumentMap(agent, parts, guideRoot ?? agent.cwd)
|
|
236
167
|
|
|
237
168
|
parts.push("## Instructions")
|
|
238
169
|
if (docList.length > 0) {
|
|
239
|
-
parts.push("1. Read every document in the Documents to Review list in full — review ONLY those files.
|
|
170
|
+
parts.push("1. Read every document in the Documents to Review list in full — review ONLY those files.")
|
|
240
171
|
} else {
|
|
241
|
-
parts.push("1. Read the design document fully.
|
|
172
|
+
parts.push("1. Read the design document fully.")
|
|
242
173
|
}
|
|
243
|
-
parts.push("2. Review against: completeness (all requirements covered?), feasibility (can this be built?), methodology compliance (does it follow the project's
|
|
174
|
+
parts.push("2. Review against: completeness (all requirements covered?), feasibility (can this be built?), methodology compliance (does it follow the project's standards as provided?), clarity (specific enough?), acceptance criteria (verifiable?), scope (appropriate?).")
|
|
244
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?")
|
|
245
176
|
parts.push("4. Do NOT run git diff or look for code changes — there are none at this stage.")
|
|
246
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.")
|
|
247
178
|
if (designToken) {
|
|
248
179
|
parts.push("")
|
|
249
|
-
parts.push(
|
|
250
|
-
parts.push(`If — and ONLY if — your review finds NO 🔴 (Critical) issues, end your reply with this exact token: [DESIGN-TOKEN:${designToken}]`)
|
|
251
|
-
parts.push("🟡 (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.")
|
|
180
|
+
parts.push(buildDesignApprovalBlock(designToken, designId))
|
|
252
181
|
}
|
|
253
182
|
return parts.join("\n")
|
|
254
183
|
}
|
|
@@ -318,17 +247,10 @@ export function buildAdvisorUserMessage(agent, prior, reviewType, designToken =
|
|
|
318
247
|
}
|
|
319
248
|
parts.push("")
|
|
320
249
|
|
|
321
|
-
// Engineering mode:
|
|
322
|
-
//
|
|
250
|
+
// Engineering mode: project standards (declaration-only, same helper as the
|
|
251
|
+
// design path) — undeclared degrades visibly (PO-2).
|
|
323
252
|
if (agent.config?.agent?.engineering) {
|
|
324
|
-
|
|
325
|
-
const mpath = resolve(guideRoot ?? agent.cwd, "METHODOLOGY.md")
|
|
326
|
-
const methodology = readFileSync(mpath, "utf8")
|
|
327
|
-
parts.push("## Project Methodology (Engineering Mode)")
|
|
328
|
-
parts.push("The project follows this methodology. Evaluate the changes against it:")
|
|
329
|
-
parts.push(methodology)
|
|
330
|
-
parts.push("")
|
|
331
|
-
} catch { /* file doesn't exist — skip */ }
|
|
253
|
+
injectProjectStandards(agent, parts, guideRoot)
|
|
332
254
|
}
|
|
333
255
|
|
|
334
256
|
// Instructions — round-aware: re-reviews skip convention discovery entirely.
|