thincoder 0.12.61 → 0.12.63

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.
Files changed (230) hide show
  1. package/CHANGELOG.md +34 -1
  2. package/README.md +13 -12
  3. package/bin/thincoder.mjs +63 -28
  4. package/package.json +6 -5
  5. package/src/acp/bridge.mjs +35 -15
  6. package/src/acp/client-caps.mjs +86 -0
  7. package/src/acp/ext.mjs +86 -0
  8. package/src/acp/handlers-session.mjs +240 -0
  9. package/src/acp/handlers-slots.mjs +196 -0
  10. package/src/acp/login.mjs +48 -0
  11. package/src/acp/session.mjs +6 -4
  12. package/src/acp.mjs +67 -371
  13. package/src/cli/distill-command.mjs +3 -3
  14. package/src/cli/make-agent.mjs +59 -17
  15. package/src/cli/memory-command.mjs +3 -3
  16. package/src/cli/permission.mjs +4 -48
  17. package/src/cli/setup-wizard.mjs +1 -1
  18. package/src/completions.mjs +3 -1
  19. package/src/crash-reports.mjs +32 -10
  20. package/src/distill.mjs +4 -4
  21. package/src/heap-watch.mjs +88 -0
  22. package/src/prompt-injections.mjs +20 -0
  23. package/src/tui/agent-turn.mjs +40 -9
  24. package/src/tui/cmd-advisor.mjs +5 -5
  25. package/src/tui/cmd-clear.mjs +2 -0
  26. package/src/tui/cmd-config.mjs +8 -8
  27. package/src/tui/cmd-eng.mjs +25 -9
  28. package/src/tui/cmd-mcp.mjs +9 -8
  29. package/src/tui/cmd-model.mjs +1 -1
  30. package/src/tui/cmd-new.mjs +10 -5
  31. package/src/tui/cmd-reindex.mjs +1 -1
  32. package/src/tui/cmd-restore.mjs +2 -2
  33. package/src/tui/cmd-session.mjs +31 -4
  34. package/src/tui/cmd-skills.mjs +1 -1
  35. package/src/tui/cmd-think.mjs +22 -9
  36. package/src/tui/config-helpers.mjs +1 -1
  37. package/src/tui/display-budget.mjs +206 -0
  38. package/src/tui/index.mjs +40 -12
  39. package/src/tui/interaction.mjs +16 -7
  40. package/src/tui/key-handler-search.mjs +9 -1
  41. package/src/tui/key-modes.mjs +9 -4
  42. package/src/tui/ledger-surface.mjs +85 -0
  43. package/src/tui/model-catalog.mjs +4 -4
  44. package/src/tui/model-picker.mjs +8 -7
  45. package/src/tui/mouse.mjs +11 -6
  46. package/src/tui/pickers.mjs +15 -2
  47. package/src/tui/render-conversation.mjs +1 -1
  48. package/src/tui/render-frame.mjs +17 -6
  49. package/src/tui/render-loop.mjs +1 -1
  50. package/src/tui/render-segments.mjs +3 -1
  51. package/src/tui/slash-commands.mjs +1 -1
  52. package/src/tui/startup.mjs +49 -17
  53. package/src/tui/subagent-blocks.mjs +21 -3
  54. package/src/tui/subagent-children.mjs +86 -14
  55. package/src/tui/subagent-freeze.mjs +80 -3
  56. package/src/tui/suspension-drive.mjs +48 -22
  57. package/src/tui/tool-args.mjs +5 -2
  58. package/src/tui/tool-display.mjs +16 -2
  59. package/src/tui/tool-events.mjs +64 -22
  60. package/src/tui/tui-lifecycle.mjs +9 -2
  61. package/src/tui/wizard.mjs +3 -3
  62. package/src/tui/wrapped-spawn.mjs +21 -5
  63. package/src/abort-provenance.mjs +0 -116
  64. package/src/advisor/citations.mjs +0 -139
  65. package/src/advisor/compaction.mjs +0 -174
  66. package/src/advisor/convergence.mjs +0 -80
  67. package/src/advisor/history.mjs +0 -77
  68. package/src/advisor/loop.mjs +0 -293
  69. package/src/advisor/messages.mjs +0 -299
  70. package/src/advisor/project-context.mjs +0 -194
  71. package/src/advisor/repos.mjs +0 -150
  72. package/src/advisor/run.mjs +0 -293
  73. package/src/advisor/truncate.mjs +0 -57
  74. package/src/advisor.mjs +0 -290
  75. package/src/agent/completion.mjs +0 -146
  76. package/src/agent/dispatch.mjs +0 -489
  77. package/src/agent/helpers.mjs +0 -384
  78. package/src/agent/post-turn.mjs +0 -70
  79. package/src/agent/record-results.mjs +0 -174
  80. package/src/agent/relay-prefix.mjs +0 -39
  81. package/src/agent/run-stages.mjs +0 -242
  82. package/src/agent/setup-reminders.mjs +0 -69
  83. package/src/agent/setup.mjs +0 -354
  84. package/src/agent/spawn-child.mjs +0 -228
  85. package/src/agent-tools/advisor-async.mjs +0 -346
  86. package/src/agent-tools/advisor-settle.mjs +0 -231
  87. package/src/agent-tools/advisor.mjs +0 -260
  88. package/src/agent-tools/async-settle.mjs +0 -191
  89. package/src/agent-tools/batch-segment.mjs +0 -195
  90. package/src/agent-tools/consult.mjs +0 -468
  91. package/src/agent-tools/design-token.mjs +0 -117
  92. package/src/agent-tools/digest-budget.mjs +0 -76
  93. package/src/agent-tools/eng.mjs +0 -67
  94. package/src/agent-tools/escalate-async.mjs +0 -289
  95. package/src/agent-tools/goal.mjs +0 -119
  96. package/src/agent-tools/plan.mjs +0 -81
  97. package/src/agent-tools/read-history.mjs +0 -294
  98. package/src/agent-tools/recent-changes.mjs +0 -24
  99. package/src/agent-tools/review-streak.mjs +0 -93
  100. package/src/agent-tools/settings.mjs +0 -265
  101. package/src/agent-tools/skill.mjs +0 -47
  102. package/src/agent-tools/subagent-actions.mjs +0 -479
  103. package/src/agent-tools/subagent-async.mjs +0 -434
  104. package/src/agent-tools/subagent-panel.mjs +0 -160
  105. package/src/agent-tools/subagent-run.mjs +0 -205
  106. package/src/agent-tools/subagent-scheduler.mjs +0 -392
  107. package/src/agent-tools/subagent-spawn.mjs +0 -453
  108. package/src/agent-tools/subagent.mjs +0 -404
  109. package/src/agent-tools/task.mjs +0 -87
  110. package/src/agent-tools/timer.mjs +0 -46
  111. package/src/agent-tools/verify.mjs +0 -271
  112. package/src/agent-tools.mjs +0 -17
  113. package/src/agent.mjs +0 -413
  114. package/src/auto-think.mjs +0 -115
  115. package/src/config-migrate.mjs +0 -70
  116. package/src/config.mjs +0 -496
  117. package/src/context.mjs +0 -381
  118. package/src/conventions.mjs +0 -223
  119. package/src/embedding.mjs +0 -120
  120. package/src/escape.mjs +0 -152
  121. package/src/expand-home.mjs +0 -16
  122. package/src/explore-distill.mjs +0 -155
  123. package/src/generate-title.mjs +0 -83
  124. package/src/git/checkpoint.mjs +0 -448
  125. package/src/git/gitmem.mjs +0 -100
  126. package/src/hooks.mjs +0 -97
  127. package/src/log.mjs +0 -195
  128. package/src/markdown.mjs +0 -106
  129. package/src/mcp/helpers.mjs +0 -51
  130. package/src/mcp/transport-http.mjs +0 -248
  131. package/src/mcp/transport-stdio.mjs +0 -140
  132. package/src/mcp/transport-ws.mjs +0 -122
  133. package/src/mcp.mjs +0 -295
  134. package/src/memory/code-index.mjs +0 -219
  135. package/src/memory/code-sync.mjs +0 -413
  136. package/src/memory/core.mjs +0 -300
  137. package/src/memory/delete.mjs +0 -236
  138. package/src/memory/docs.mjs +0 -417
  139. package/src/memory/file-walk.mjs +0 -109
  140. package/src/memory/schema.mjs +0 -452
  141. package/src/memory.mjs +0 -21
  142. package/src/model-ref.mjs +0 -66
  143. package/src/model-specs.mjs +0 -179
  144. package/src/peer-domains.mjs +0 -265
  145. package/src/peer-instances.mjs +0 -231
  146. package/src/prompt-overlays.mjs +0 -82
  147. package/src/prompts/advisor-design.md +0 -41
  148. package/src/prompts/advisor-round1.md +0 -41
  149. package/src/prompts/advisor-round2.md +0 -46
  150. package/src/prompts/advisor-round3.md +0 -42
  151. package/src/prompts/common.md +0 -115
  152. package/src/prompts/consult-base.md +0 -19
  153. package/src/prompts/discipline-engineering.md +0 -217
  154. package/src/prompts/discipline-normal.md +0 -179
  155. package/src/prompts/persona-coder.md +0 -21
  156. package/src/prompts/persona-eng-coder.md +0 -37
  157. package/src/prompts/persona-eng-designer.md +0 -55
  158. package/src/prompts/persona-engineering.md +0 -54
  159. package/src/prompts/persona-explore.md +0 -15
  160. package/src/prompts/persona-normal.md +0 -27
  161. package/src/prompts/persona-plan.md +0 -26
  162. package/src/provider/anthropic.mjs +0 -225
  163. package/src/provider/core.mjs +0 -476
  164. package/src/provider/errors.mjs +0 -101
  165. package/src/provider/google.mjs +0 -257
  166. package/src/provider/index.mjs +0 -7
  167. package/src/provider/list-models.mjs +0 -93
  168. package/src/provider/normalize.mjs +0 -81
  169. package/src/provider/rate.mjs +0 -108
  170. package/src/provider/responses.mjs +0 -495
  171. package/src/provider/retry.mjs +0 -88
  172. package/src/provider/sse.mjs +0 -264
  173. package/src/proxy.mjs +0 -261
  174. package/src/rules.mjs +0 -53
  175. package/src/session-gc.mjs +0 -214
  176. package/src/session-guard.mjs +0 -47
  177. package/src/session-migrate.mjs +0 -48
  178. package/src/session-rename.mjs +0 -38
  179. package/src/session-slots.mjs +0 -489
  180. package/src/session.mjs +0 -475
  181. package/src/skills.mjs +0 -153
  182. package/src/token-ttl.mjs +0 -274
  183. package/src/tools/apply_patch.md +0 -15
  184. package/src/tools/bash.md +0 -37
  185. package/src/tools/bash.mjs +0 -268
  186. package/src/tools/checklist-sync.mjs +0 -181
  187. package/src/tools/checklist.md +0 -13
  188. package/src/tools/checklist.mjs +0 -299
  189. package/src/tools/delete.md +0 -13
  190. package/src/tools/edit-batch.mjs +0 -191
  191. package/src/tools/edit-diff.mjs +0 -348
  192. package/src/tools/edit.md +0 -30
  193. package/src/tools/execute.md +0 -21
  194. package/src/tools/execute.mjs +0 -228
  195. package/src/tools/fetch.md +0 -12
  196. package/src/tools/file.mjs +0 -469
  197. package/src/tools/file_ops.md +0 -17
  198. package/src/tools/get_current_time.md +0 -8
  199. package/src/tools/git-checkpoint.mjs +0 -143
  200. package/src/tools/git-ext.mjs +0 -173
  201. package/src/tools/git.md +0 -54
  202. package/src/tools/git.mjs +0 -356
  203. package/src/tools/glob-dialect.mjs +0 -130
  204. package/src/tools/glob.md +0 -11
  205. package/src/tools/grep.md +0 -19
  206. package/src/tools/hashline_edit.md +0 -14
  207. package/src/tools/index.mjs +0 -36
  208. package/src/tools/insert_after.md +0 -15
  209. package/src/tools/lint.md +0 -10
  210. package/src/tools/linter.mjs +0 -128
  211. package/src/tools/ls.md +0 -12
  212. package/src/tools/lsp.md +0 -10
  213. package/src/tools/lsp.mjs +0 -316
  214. package/src/tools/ops.mjs +0 -299
  215. package/src/tools/patch.mjs +0 -282
  216. package/src/tools/process.md +0 -10
  217. package/src/tools/question.md +0 -16
  218. package/src/tools/question.mjs +0 -26
  219. package/src/tools/read.md +0 -20
  220. package/src/tools/read_image.md +0 -8
  221. package/src/tools/repomap.mjs +0 -314
  222. package/src/tools/search.mjs +0 -236
  223. package/src/tools/shared.mjs +0 -446
  224. package/src/tools/tree.md +0 -14
  225. package/src/tools/tree.mjs +0 -66
  226. package/src/tools/wait_for.md +0 -22
  227. package/src/tools/web.mjs +0 -224
  228. package/src/tools/websearch.md +0 -16
  229. package/src/tools/write.md +0 -11
  230. package/src/traces/trace-store.mjs +0 -224
@@ -1,293 +0,0 @@
1
- /**
2
- * advisor/run.mjs — advisor execution: provider resolution and the review entry
3
- * point. Message building lives in advisor.mjs; the tool loop lives in loop.mjs
4
- * and the context/limit/tail guards in compaction.mjs(第 11 批硬帽拆分——
5
- * run.mjs 原 498/500;既有 import 面经 re-export 保持不变)。
6
- */
7
- import { findProvider } from "../config.mjs"
8
- import { prepareAdvisorMessages } from "../advisor.mjs"
9
- import { buildObjectDeclarationBlock, buildDesignApprovalBlock } from "./messages.mjs"
10
- import { appendCitationReport } from "./citations.mjs"
11
- import { runAdvisorToolLoop } from "./loop.mjs"
12
- import { advisorIncompleteMarker, estimateTokens } from "./compaction.mjs"
13
- import { batchDocForReview } from "../agent-tools/batch-segment.mjs"
14
- // 第 33 批(§17.5 模块图):护栏常量 / doc-set 键 / 记录读取——单向导入(review-streak 无回指)。
15
- import {
16
- MAX_DESIGN_REVIEW_STREAK, docSetKey, designReviewStreakRecord, designReviewStreakStopped,
17
- } from "../agent-tools/review-streak.mjs"
18
-
19
- // 拆分后 import 面(既有导出名逐一保面——re-export;谓词为本批新增)。
20
- export { ADVISOR_THINKING_PLACEHOLDER, MAX_RESULT_CHARS, renderTimeline as _renderTimeline } from "./compaction.mjs"
21
- export { advisorToolsFor, advisorToolsFor as _advisorToolsFor } from "./loop.mjs"
22
- export { runAdvisorToolLoop as _runAdvisorToolLoop } from "./loop.mjs"
23
- export { advisorIncompleteMarker } from "./compaction.mjs"
24
-
25
- // Mechanical convergence cap: up to 5 rounds suffice; a 6th call means the model
26
- // is looping — refuse it instead of burning tokens. §11.2 D-24b (2026-09-06): PER
27
- // REVIEW INSTANCE (agent._advisorRuns); CODE REVIEWS ONLY (2026-09-07 §8 ruling)
28
- // — design reviews are EXEMPT: their rounds keep advancing, the cap never refuses.
29
- export const MAX_ADVISOR_ROUNDS = 5
30
-
31
- /** B 启动拒绝前缀(§14.4 #2)——稳定契约单源(三消费点同串):run.mjs 生成;同步工具面
32
- * 据此登记 `_advisorRefusals`;异步结算面据此不置 `_calledAdvisorThisRun`。 */
33
- export const ADVISOR_LAUNCH_REFUSAL_PREFIX = "Advisor: design review launch refused"
34
-
35
- /** 评审失败护栏稳定前缀(第 33 批 §17.4——与 `ADVISOR_LAUNCH_REFUSAL_PREFIX` 同族;
36
- * 实现 grep / 用例断言锚)。凭证卫生:串内零 token / designId 值。 */
37
- export const ADVISOR_DESIGN_STREAK_STOP_PREFIX = "Advisor: design review stopped"
38
-
39
- /** kind → 人读说明(第 33 批 §17.4 逐字——结论表第三列;八类 = 五 kind + stale +
40
- * no_credential + no_report,与 review-streak.mjs 分类输出同集)。 */
41
- const DESIGN_REVIEW_OUTCOME_MEANINGS = {
42
- timeout: "review exceeded the wall-clock budget (agent.advisor.timeoutMs)",
43
- context_limit: "review exceeded the model context budget",
44
- turn_cap: "review exceeded the tool-round limit",
45
- empty: "the provider returned an empty response",
46
- review_failed: "provider / transport error",
47
- stale: "the reviewed documents changed while the review was in flight",
48
- no_credential: "the token could not be written to the session ledger",
49
- no_report: "the review settled without a report",
50
- }
51
-
52
- /**
53
- * 停止结论串(第 33 批 §17.4 逐字;F29):稳定前缀 + 停止的 doc-set 清单 + 失败尝试表
54
- * (记录逐条——kind + 人读说明)+ 三选项(接受现状 / 改变或缩小范围后重跑 / /new 重置)。
55
- * 全文零凭证值;不自动执行任何恢复动作(发起权在父代理 / 用户)。
56
- * @param {{count: number, log: string[]}|null} record — 护栏记录(`designReviewStreakRecord`)
57
- * @param {string[]|null} documents — 被停的文档集(结论清单数据源)
58
- */
59
- export function buildDesignReviewGuardMessage(record, documents) {
60
- const log = Array.isArray(record?.log) ? record.log : []
61
- const docList = Array.isArray(documents) ? documents.filter((d) => typeof d === "string" && d.trim()) : []
62
- return [
63
- `${ADVISOR_DESIGN_STREAK_STOP_PREFIX} — ${MAX_DESIGN_REVIEW_STREAK} consecutive attempts on this document set produced no design token (repeated failed settlements; no further reviews will start for this set in this session).`,
64
- "Document set (1 design instance — no token issued):",
65
- ...docList.map((d) => `- ${d}`),
66
- "Attempts (most recent last):",
67
- "| # | outcome | meaning |",
68
- "|---|---|---|",
69
- ...log.map((kind, i) => `| ${i + 1} | ${kind} | ${DESIGN_REVIEW_OUTCOME_MEANINGS[kind] ?? kind} |`),
70
- "Options:",
71
- "1. Accept the current state and proceed — implementation for this document set stays gated (no design token).",
72
- "2. Narrow or change the scope: a different document set starts a fresh budget — fix the cause first (agent.advisor.timeoutMs / advisor model / provider).",
73
- "3. Start a new session (/new) to reset the guard.",
74
- ].join("\n")
75
- }
76
-
77
- const MAX_UNFIXED_DISPLAY = 10 // unfixed issues shown in the cap message
78
-
79
- /** Resolve the advisor's provider: cfg.provider/model when set, otherwise the main agent's provider */
80
- export function resolveAdvisorProvider(agent) {
81
- const cfg = agent.config?.advisor
82
- if (cfg?.provider) {
83
- try {
84
- // F-1 (ISSUE-FIX-BATCH): children carry no agent.providers (spawn childConfig copies the
85
- // parent config) — fall back to config.providersList (.length: [] must not skip the list).
86
- const provider = findProvider(agent.providers?.length ? agent.providers : agent.config?.providersList ?? [agent.provider], cfg.provider)
87
- // F-2a (MODEL-400-FIX):无 cfg.model 时渠道克隆须重派生 model——渠道裸克隆会丢 model 键
88
- // → 无 model 请求 → serde 400。
89
- // MODEL-SELECTION v2(M3④):无 cfg.model → 命中渠道自己的默认模型(`provider.model`
90
- // 单值);渠道无默认模型 → 父 provider 兜底(与 subagent F-2c 同构)——绝不产出静默
91
- // undefined-model 请求(最极端两者皆无 → chat 前 assertProviderModel fail-fast)。
92
- const result = cfg.model ? { ...provider, model: cfg.model } : { ...provider, model: provider.model ?? agent.provider?.model }
93
- if (cfg.thinking === null || cfg.thinking === false) result.thinking = undefined // explicitly off
94
- else if (cfg.thinking !== undefined) result.thinking = cfg.thinking
95
- if (cfg.reasoningEffort !== undefined) result.reasoningEffort = cfg.reasoningEffort
96
- return result
97
- } catch (e) {
98
- // Provider not found or lookup failed — fall back to main provider, but surface the reason
99
- console.warn(`[advisor] resolveAdvisorProvider: ${e.message}`)
100
- }
101
- }
102
- const provider = { ...agent.provider }
103
- if (cfg?.model) provider.model = cfg.model
104
- // thinking off: null AND false both mean "explicitly off" — a raw `false`
105
- // value is invalid for providers that expect undefined or an object.
106
- if (cfg?.thinking === null || cfg?.thinking === false) provider.thinking = undefined
107
- else if (cfg?.thinking !== undefined) provider.thinking = cfg.thinking
108
- if (cfg?.reasoningEffort !== undefined) provider.reasoningEffort = cfg.reasoningEffort
109
- return provider
110
- }
111
-
112
- /**
113
- * Extract unfixed issues from prior review text (for the cap message).
114
- * Input: an advisor review markdown table (`| # | … |` rows). A row counts as
115
- * unfixed unless its line carries a resolved-status word (fixed/resolved/done/
116
- * addressed/corrected, ✓/✔). Returns at most MAX_UNFIXED_DISPLAY plain
117
- * (pipe-stripped) row strings.
118
- */
119
- function extractUnfixedIssues(priorText) {
120
- if (!priorText) return []
121
- const lines = priorText.split("\n")
122
- // Resolved-status words: fixed/resolved/done/addressed/corrected (+ ✓/✔).
123
- // \b prevents "unfixed"/"prefixed" from matching "fixed".
124
- const resolvedRe = /\b(?:fixed|resolved|done|addressed|corrected)\b|✓|✔/i
125
- return lines
126
- .filter((line) => /\|\s*\d+\s*\|/.test(line)) // 匹配表格行
127
- .filter((line) => !resolvedRe.test(line))
128
- // Strip only the leading/trailing table pipes — inner pipes (escaped or
129
- // in-cell content) stay intact instead of garbling the cap message.
130
- .map((line) => line.trim().replace(/^\|/, "").replace(/\|$/, "").trim())
131
- .filter(Boolean)
132
- .slice(0, MAX_UNFIXED_DISPLAY)
133
- }
134
- /** Review-looking guard (async settle parity): a markdown table row or ≥200 chars of prose counts as a prior. */
135
- export function looksLikeReviewOutput(text) {
136
- const trimmed = String(text ?? "").trim()
137
- return /\|.*\|.*\|/.test(trimmed) || trimmed.length >= 200
138
- }
139
- /** Cap message (shared by runAdvisorReview and the async pre-check — per-review refusal). */
140
- export function buildCapMessage(agent) {
141
- const prior = agent._lastAdvisorOutput
142
- const unfixed = prior ? extractUnfixedIssues(prior) : []
143
- let message = `Advisor: convergence cap reached after ${MAX_ADVISOR_ROUNDS} rounds.\n`
144
- if (unfixed.length > 0) {
145
- message += `\nUnresolved issues from prior rounds:\n${unfixed.map((i) => `- ${i}`).join("\n")}\n`
146
- } else {
147
- message += "\nAll prior issues appear resolved.\n"
148
- }
149
- message += "\nOptions:\n1. Accept current state and proceed\n2. Manually review specific concerns with read/grep\n3. Start a new session (/new) to reset the advisor"
150
- return message
151
- }
152
-
153
- /**
154
- * 压缩定锚简报(F13/§14.4 #3)——**由评审参数构建**(非模型输出):对象声明 / 文档清单 /
155
- * Approval Signal 三锚;重内容(项目指南 / 方法论 / 文档地图)不入 pin(压缩的意义所在)。
156
- * 形态逐字见设计 §14.4 #3(首行为机械化重挂说明)。
157
- */
158
- function buildPinnedBrief(reviewType, documents, object, designToken, designId) {
159
- const docList = Array.isArray(documents) ? documents.filter((d) => typeof d === "string" && d.trim()) : []
160
- const parts = ["[review brief — re-attached after context compaction; the original review request is no longer in the context]"]
161
- const declaration = buildObjectDeclarationBlock(object)
162
- if (declaration) parts.push(declaration.trimEnd())
163
- if (docList.length > 0) {
164
- parts.push("## Documents to Review")
165
- parts.push(docList.map((d) => `- ${d} — Read this file in full`).join("\n"))
166
- }
167
- if (reviewType === "design" && designToken) parts.push(buildDesignApprovalBlock(designToken, designId))
168
- return parts.join("\n\n")
169
- }
170
-
171
- /**
172
- * Run an advisor review. reviewType: "code" (default) or "design". Returns review text or null when skipped.
173
- * @param {string|null} [designToken] — injected into the design-review prompt; the advisor echoes it only on approval.
174
- * @param {string[]|null} [documents] — design review only: explicit list of doc paths to review; passed through to the message builder.
175
- * @param {string[]|null} [paths] — code review only: explicit list of file/dir paths to review.
176
- * @param {Object|null} [object] — review-object declaration (§18.8 D-OA1/D-OA3): { type, target, status, reason, exclude }; absent → legacy behavior (no injection).
177
- * @param {string|null} [designId] — §29.1 F2a: injected next to the design token in
178
- * the Approval Signal (passed through to the message builders).
179
- */
180
- export async function runAdvisorReview(agent, reviewType, callbacks, designToken = null, documents = null, paths = null, object = null, designId = null) {
181
- const onOutput = callbacks?.onOutput
182
- const signal = callbacks?.signal
183
- const startTime = Date.now()
184
-
185
- // Advisor reviews are ALWAYS available (2026-08-21 semantic refactor): the
186
- // former advisor.enabled gate is removed — review capability has no off
187
- // switch; only the guard (completion pushback) is opt-in via advisor.guard.
188
-
189
- // Mechanical convergence cap — CODE REVIEWS ONLY (2026-09-07 §8 ruling: design
190
- // reviews are exempt). _advisorRound is scoped to the current review instance
191
- // (§11.2 D-24b ③), so >= MAX_ADVISOR_ROUNDS blocks the next call of THIS instance.
192
- // 5 rounds max; after that the review is never pushed back.
193
- if (reviewType !== "design" && (agent._advisorRound || 0) >= MAX_ADVISOR_ROUNDS) {
194
- return buildCapMessage(agent)
195
- }
196
-
197
- // 第 33 批(§17.5 检查点 2——内防线):同一 doc-set 连续未产出可用结算达阈值 ⇒ 直接拒绝
198
- // (不建消息、不发起、零 LLM)——防直接调用方绕过工具层预检(正常工具链在预检已拒)。
199
- if (reviewType === "design" && Array.isArray(documents) && documents.length > 0) {
200
- const streakKey = docSetKey(documents, agent.cwd)
201
- if (designReviewStreakStopped(agent, streakKey)) {
202
- return buildDesignReviewGuardMessage(designReviewStreakRecord(agent, streakKey), documents)
203
- }
204
- }
205
-
206
- const provider = resolveAdvisorProvider(agent)
207
- // Advisor always works in the agent's cwd — scope is defined by paths/documents.
208
- const advisorCwd = agent.cwd
209
-
210
- const messages = prepareAdvisorMessages(agent, reviewType, designToken, documents, paths, null, object, designId)
211
-
212
- // B 启动断言(fail-closed——§14.4 #2):设计评审请求内**必须**携带与本次签发 token 精确
213
- // 对应的 Approval Signal——构建面补不上就拒绝启动(不发"请回显一个不存在的 token"的请求)。
214
- // 拒绝报告前缀 `Advisor: design review launch refused` = 稳定契约(同步工具面据此登记
215
- // _advisorRefusals;异步结算面据此不置 _calledAdvisorThisRun)。工具路径恒签发 token ⇒
216
- // 该拒绝为直接调用方兜底(防御纵深——正常链不可达,如实注)。
217
- if (reviewType === "design") {
218
- const missing = !designToken
219
- ? "no design token was minted"
220
- : (messages.some((m) => m.role === "user" && String(m.content ?? "").includes(`[DESIGN-TOKEN:${designToken}`))
221
- ? null
222
- : "the request does not carry the approval signal")
223
- if (missing) {
224
- return `${ADVISOR_LAUNCH_REFUSAL_PREFIX} — ${missing}. Nothing was sent: a request that asks the reviewer to echo a token it cannot see would break the credential chain. Re-run advisor(type='design') to mint a fresh token.`
225
- }
226
- }
227
-
228
- // §2.20.3 批次档写通道的绑定(仅设计评审):同步路径 = 调用方(advisor 工具)传入的
229
- // callbacks.batchDoc(即 resolved.run 的实例绑定);异步路径 = 本实例在跑池条目的
230
- // run.batchDoc(同文档集实例键——各评审各取各档,不用单值会话态)。
231
- const boundBatchDoc = reviewType === "design" ? batchDocForReview(agent, documents, callbacks) : null
232
- // F13/§14.4 #3:压缩定锚简报(评审参数构建——压缩触发时由 compaction 重挂)。
233
- const pinned = buildPinnedBrief(reviewType, documents, object, designToken, designId)
234
-
235
- try {
236
- const result = await runAdvisorToolLoop(provider, messages, onOutput, signal, agent, advisorCwd, null, reviewType, boundBatchDoc, pinned)
237
-
238
- // Host-verified citations (decision d698434): mechanically check every
239
- // `file:line: content` reference in the review against the CURRENT file
240
- // state. LLMs cannot self-enforce the evidence rule — the model may quote
241
- // the prior table instead of re-reading (three consecutive false reports
242
- // cited pre-fix line content). Unverified citations must not support a
243
- // push-back; the parent agent sees the verification report.
244
- // F14/§14.5:解析候选 = cwd + 评审对象声明范围派生根(scope = documents + paths)。
245
- let final = result
246
- if (!result.trimStart().startsWith("Advisor:")) {
247
- final = appendCitationReport(result, advisorCwd, { scope: [...(documents ?? []), ...(paths ?? [])] })
248
- // Success path: keep the FULL review output for convergence rounds —
249
- // round 2+ injects this verbatim and the model understands it (decision
250
- // 2026-08-08: prior-table hard parsing removed; no phrase/header matching).
251
- // Guard: only store outputs that actually carry a review — a markdown
252
- // table row (`| a | b | c |`) or substantial prose (>200 chars). An
253
- // empty or tool-progress-only reply must not become the "prior review"
254
- // of round 2+.
255
- const looksLikeReview = looksLikeReviewOutput(final)
256
- if (looksLikeReview) {
257
- agent._lastAdvisorOutput = final
258
- }
259
- }
260
-
261
- // Log review statistics for observability
262
- const elapsed = Math.round((Date.now() - startTime) / 1000)
263
- const toolCallCount = messages.filter((m) => m.role === "tool").length
264
- const tokensUsed = estimateTokens(messages)
265
- onOutput?.({
266
- kind: "text",
267
- text: `\n[advisor] Review completed: ${elapsed}s, ${toolCallCount} tool calls, ~${Math.round(tokensUsed / 1000)}k tokens\n`,
268
- })
269
- return final
270
- } catch (e) {
271
- if (e.name === "AbortError" && signal?.reason?.interrupt) throw e
272
-
273
- // 细化错误类型
274
- const errorType = e.message.includes("rate limit") || e.message.includes("429") ? "rate limit"
275
- : e.message.includes("timeout") ? "timeout"
276
- : e.message.includes("network") || e.message.includes("ECONNREFUSED") ? "network"
277
- : e.message.includes("context length") ? "context_too_long"
278
- : "unknown"
279
-
280
- const retryAdvice = errorType === "rate limit"
281
- ? "Wait a moment and retry. Consider using a cheaper model for advisor."
282
- : errorType === "timeout"
283
- ? "The model took too long. Try with a narrower scope."
284
- : errorType === "context_too_long"
285
- ? "Reduce the scope (fewer files/paths) or use a model with larger context window."
286
- : "You may retry or proceed to verify manually."
287
-
288
- return `Advisor: review failed (${errorType}) — ${e.message || "unknown error"}. ${retryAdvice}`
289
- }
290
- }
291
- // Host-verified citations — moved to citations.mjs (kept re-exported here for
292
- // import compatibility: tests and callers import from run.mjs).
293
- export { extractCitations, verifyCitations, appendCitationReport } from "./citations.mjs"
@@ -1,57 +0,0 @@
1
- /**
2
- * truncate.mjs — advisor 工具结果的行感知双端截断(DUAL-END-TRUNCATION F-2,
3
- * DUAL-END-TRUNCATION.md,2026-09-09——CLI/VSC 双端逐字同构镜像)。
4
- *
5
- * 原 run.mjs 内联截断为纯头向(line-aware 从首行累加至 64K break)——评审尾部
6
- * 结论/裁决被切。现改头尾双保:头行累加至预算 ~60% → 中段省略注 → 尾行累加至
7
- * 剩余 ~40%(保尾结论——与 read 工具 C 方案 / offload 双端预览同构的
8
- * "头+尾保留、中段截断"策略——共享设计语言、分别实现)。
9
- *
10
- * 纯函数无导入:maxChars 由调用方传入(run.mjs 传 MAX_RESULT_CHARS)。
11
- */
12
-
13
- /** advisor 截断头占比——头行累加至此份额后切中段,尾自动取剩余预算(设计 §4 ≈0.6)。 */
14
- export const ADVISOR_HEAD_RATIO = 0.6
15
-
16
- /** 尾部预算预留(字符):中段省略注 + offset 续读提示的保守预留——注的位数只减不增
17
- * (middle ≤ totalChars——超大结果位数可达 ~10 位——200 字符仍宽松覆盖注+提示 ≤ ~140;
18
- * 差额留给尾行)。 */
19
- const NOTE_RESERVE_CHARS = 200
20
-
21
- /**
22
- * 行感知双端截断:result > maxChars → 头 + `… (truncated: K more lines, TOTAL chars
23
- * total)` + 尾 + offset 续读提示;≤ maxChars 原样返回(无假截断注)。K = 头尾之间
24
- * 的中段行数——头(≤60%) + 尾(≤余预算) 均行级累加、绝不半行切开;中段存在时 K ≥ 1
25
- * (防御:K ≤ 0 时静默返回头尾拼合——不谎报截断)。
26
- */
27
- export function truncateAdvisorResult(result, maxChars) {
28
- if (result.length <= maxChars) return result
29
- const totalChars = result.length
30
- const lines = result.split("\n")
31
- const headBudget = Math.floor(maxChars * ADVISOR_HEAD_RATIO)
32
- let head = ""
33
- let headLines = 0
34
- let chars = 0
35
- while (headLines < lines.length && chars + lines[headLines].length + 1 <= headBudget) {
36
- chars += lines[headLines].length + 1
37
- head += lines[headLines] + "\n"
38
- headLines++
39
- }
40
- const tailBudget = Math.max(0, maxChars - chars - NOTE_RESERVE_CHARS)
41
- let tail = ""
42
- let tailChars = 0
43
- let tailStart = lines.length
44
- while (tailStart > headLines && tailChars + lines[tailStart - 1].length + 1 <= tailBudget) {
45
- tailChars += lines[tailStart - 1].length + 1
46
- tailStart--
47
- tail = lines[tailStart] + "\n" + tail
48
- }
49
- const middle = tailStart - headLines
50
- if (middle <= 0) return head + tail // 头尾已覆盖全部行——绝不产生假截断注
51
- return (
52
- head +
53
- `\n… (truncated: ${middle} more lines, ${totalChars} chars total)\n` +
54
- tail +
55
- `To see more content, use: read(path, offset=${headLines + 1}, limit=200)`
56
- )
57
- }
package/src/advisor.mjs DELETED
@@ -1,290 +0,0 @@
1
- /**
2
- * advisor.mjs — advisor system-prompt selection, follow-up building, session assembly.
3
- * User-message building lives in advisor/messages.mjs; execution (tool loop, provider
4
- * resolution, review entry) in advisor/run.mjs; history extraction in advisor/history.mjs.
5
- * Path classification (code / doc / temp) and the project declaration surface live in
6
- * ../conventions.mjs — the single authority every gate consumes (repos.mjs included).
7
- *
8
- * The advisor runs as a read-only exploration sub-agent with tools
9
- * (read, glob, grep, ls, lsp, code_search) — ZERO git, every round. The change
10
- * surface comes from the review scope (paths / _touchedFiles), never from git;
11
- * verification is `read`-only with quoted-line evidence (7d49a52, d3be613).
12
- *
13
- * Config:
14
- * { advisor: { enabled: true, provider: "deepseek", model: "deepseek-chat" } }
15
- * provider + model are optional — defaults to the main agent's provider/model.
16
- *
17
- * Convergence protocol:
18
- * Round 1: full review → produces a numbered issue table.
19
- * Agent responds with a response table per issue (fix claims).
20
- * Round 2: semi-convergence — verifies the prior table + can flag obvious new issues.
21
- * Round 3+: strict convergence — only checks the prior issue table.
22
- * The prior issue table IS injected into rounds 2+ (decision 2026-08-05,
23
- * reversed) — it is the ONLY complete verification list: the agent response
24
- * table covers only issues the agent chose to answer, so skipped issues would
25
- * silently escape convergence without it. The fix-claim table travels as a
26
- * focus reference only. Restatement risk is handled mechanically:
27
- * host-verified citations reject references that do not match the current
28
- * disk state, and fresh sessions exclude old read data.
29
- * Each round replaces the system prompt (ROUND1 → ROUND2 → ROUND3) so the
30
- * round-1 full-scope mandate can't bleed into later rounds, plus a mechanical
31
- * cap (MAX_ADVISOR_ROUNDS in run.mjs) refuses a 6th review call outright.
32
- * Rounds 2+ also declare all earlier diffs STALE and require read-verified
33
- * file:line evidence for any unfixed/new finding — see docs/design/ADVISOR-CONVERGENCE.md.
34
- *
35
- * Session memory (agent._advisorSession):
36
- * RETAINED for initialization compatibility but NEVER read (decision d698434):
37
- * every review round builds a fresh [system, user] session — round 2+ must not
38
- * reuse round 1's messages, because the old read outputs are the anchoring
39
- * source of re-review false reports and a token sink. Convergence data (prior
40
- * issue table + agent response table) travels via buildAdvisorFollowUp.
41
- * The field is reset by runAgent; the write sites are harmless leftovers.
42
- *
43
- * Project customisation: .thincoder/advisor.md in the project root.
44
- */
45
- import { readFileSync } from "node:fs"
46
- import { join, dirname } from "node:path"
47
- import { fileURLToPath } from "node:url"
48
- import { extractAgentResponseTable } from "./advisor/history.mjs"
49
- import { buildAdvisorUserMessage, resolveScopeFiles, buildObjectDeclarationBlock, buildDesignApprovalBlock } from "./advisor/messages.mjs"
50
- import { buildConvergenceBody } from "./advisor/convergence.mjs"
51
- import { escapeLiteralEscapes } from "./escape.mjs"
52
- // Re-export for run.mjs and tests (keeps their imports from "../advisor.mjs" stable)
53
- export { ADVISOR_MD_PATH, extractAgentResponseTable, extractConversationBackground } from "./advisor/history.mjs"
54
- export { buildAdvisorUserMessage } from "./advisor/messages.mjs"
55
- export { buildObjectDeclarationBlock, buildDesignApprovalBlock } from "./advisor/messages.mjs"
56
-
57
- const __dirname = dirname(fileURLToPath(import.meta.url))
58
-
59
- // ────────────────────────────────────────
60
- // Prompt files — loaded at module init
61
- // ────────────────────────────────────────
62
-
63
- function loadPrompt(file, name) {
64
- try {
65
- return readFileSync(join(__dirname, "prompts", file), "utf8")
66
- } catch {
67
- throw new Error(`${name} missing from the installation (prompts/${file}) — reinstall thincoder or restore the file`)
68
- }
69
- }
70
-
71
- const ADVISOR_ROUND1 = loadPrompt("advisor-round1.md", "advisor-round1.md")
72
- // ROUND2/3 are used whenever a convergence round (round 2+) is being built:
73
- // in-run session continuation replaces the system prompt with them, and a
74
- // rebuilt fresh session (e.g. after a failed review) also selects them via
75
- // buildAdvisorSystemPrompt when _advisorRound > 0.
76
- const ADVISOR_ROUND2 = loadPrompt("advisor-round2.md", "advisor-round2.md")
77
- const ADVISOR_ROUND3 = loadPrompt("advisor-round3.md", "advisor-round3.md")
78
- // Design-review prompt — hard-loaded like the round prompts (decision
79
- // 2026-08-21): a missing file means a broken installation, and silently
80
- // degrading to a lesser in-code prompt would quietly strip the approval-signal
81
- // and citation rules, disabling design approval entirely. loadPrompt throws.
82
- const ADVISOR_DESIGN = loadPrompt("advisor-design.md", "advisor-design.md")
83
-
84
- // ────────────────────────────────────────
85
- // System prompt building
86
- // ────────────────────────────────────────
87
-
88
- /**
89
- * Build the system prompt for an advisor review session.
90
- * @param {Object} agent — the parent agent
91
- * @param {Object|null} [prior] — prior review output (full text; decision 2026-08-08)
92
- * @param {string} [reviewType] — "design" for design review, undefined/"code" for code review
93
- * @returns {string} the system prompt
94
- */
95
- /** Append local time so the reviewer knows "now" (same grounding as the agent loop). */
96
- function withTime(prompt) {
97
- const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || "local"
98
- return prompt + `\n\nCurrent time: ${new Date().toLocaleString("sv-SE")} (${timeZone}).`
99
- }
100
-
101
- export function buildAdvisorSystemPrompt(agent, prior, reviewType) {
102
- // Round decision is DETERMINISTIC (decision 2026-08-08): _advisorRound > 0
103
- // with a stored review output means convergence (round 2+); 0 means round 1.
104
- // No prior-table parsing, no all-clear phrase matching — the round counter
105
- // and the stored output are the only inputs. A restarted process has
106
- // _advisorRound 0 → conservative full re-review.
107
- const hasPrior = (agent._advisorRound || 0) > 0 && (prior ?? agent._lastAdvisorOutput)
108
- // Design review: round 1 uses the dedicated design-review prompt (full scope +
109
- // approval token); rounds 2+ converge like code reviews (verify agent fix claims).
110
- if (reviewType === "design") {
111
- if (!hasPrior) {
112
- return ADVISOR_DESIGN
113
- }
114
- const round = (agent._advisorRound || 0) + 1
115
- if (round === 2) return ADVISOR_ROUND2
116
- return ADVISOR_ROUND3
117
- }
118
- if (!hasPrior) return ADVISOR_ROUND1
119
- const round = (agent._advisorRound || 0) + 1
120
- if (round === 2) return ADVISOR_ROUND2
121
- return ADVISOR_ROUND3
122
- }
123
-
124
- // ────────────────────────────────────────
125
- // Follow-up building (round 2+)
126
- // ────────────────────────────────────────
127
-
128
- /**
129
- * Build a follow-up user message for round 2+ — the agent's response table +
130
- * round-aware instructions, without re-sending the full round-1 context.
131
- * Deliberately NO git information injected (no diff snapshot, no git context):
132
- * git output misled re-reviews — committed fixes never show in `git diff HEAD`,
133
- * so the model read "no changes" as "no fixes". Verification is `read`-only.
134
- * NOTE: the caller (prepareAdvisorMessages) applies escapeLiteralEscapes to
135
- * the return value — direct callers must do the same (the prior table and
136
- * agent response can quote literal "\x"/"\u" sequences).
137
- * @param {Object} agent — the parent agent (history used for the response table)
138
- * @param {Object|null} prior — prior issue table (extracted from history when null)
139
- * @param {string[]|null} [scopeFiles] — review surface for the no-response fallback (cwd-relative)
140
- * @param {Object|null} [object] — review-object declaration (§18.8): mechanically
141
- * prepended to the round-2+ follow-up so every round stays anchored (T-OA2).
142
- * @returns {string} the follow-up user message — or a plain "System reminder: …"
143
- * fresh-review fallback (NO brackets — some OpenAI-compatible servers parse
144
- * '['-prefixed content as structured data / expand escapes) when no prior
145
- * review exists at all (caller misuse; the response-table extraction would
146
- * otherwise scan history from index 0 and could match an unrelated stale table)
147
- */
148
- export function buildAdvisorFollowUp(agent, prior, scopeFiles = null, object = null) {
149
- // Convergence follow-up REQUIRES a prior review record — the full output of
150
- // the last review, injected VERBATIM (decision 2026-08-08: the model
151
- // understands the review output; no table/header/phrase parsing). The caller
152
- // usually passes it; fall back to the stored agent._lastAdvisorOutput.
153
- const p = prior ?? agent._lastAdvisorOutput
154
- if (!p) {
155
- // Plain "System reminder:" prefix (no brackets) — same convention as the
156
- // round-1 path (some OpenAI-compatible servers parse '['-prefixed content
157
- // as structured data / expand escapes; see prepareAdvisorMessages).
158
- return "System reminder: convergence follow-up requested without a prior review — perform a fresh full review."
159
- }
160
- // Convergence semantics require round >= 2 (round 1 is the full review, not
161
- // verification). A direct caller with _advisorRound 0 would otherwise get a
162
- // meaningless "Round 1 — Strict Verification".
163
- if ((agent._advisorRound || 0) < 1) {
164
- return "System reminder: convergence follow-up requested at round 1 — a full review is already in progress; no prior verification exists yet."
165
- }
166
- const noResponseFallback = scopeFiles?.length
167
- ? "(Agent did not provide a response table — perform a fresh review of: " + scopeFiles.slice(0, 10).join(", ") + ")"
168
- : "(Agent did not provide a response table — perform a fresh full review; the review surface is unknown, ask the user for the file list)"
169
- const response = extractAgentResponseTable(agent.history) || noResponseFallback
170
- const round = (agent._advisorRound || 0) + 1
171
- // Review-object declaration FIRST (T-OA2 — round 2+ stays anchored, no re-archaeology).
172
- const declaration = buildObjectDeclarationBlock(object)
173
- return (declaration ? declaration + "\n" : "") + buildConvergenceBody(p, response, round, scopeFiles)
174
- }
175
-
176
- /**
177
- * Resolve the review surface for the convergence fallback — moved to
178
- * messages.mjs so the legacy path shares it (see there).
179
- */
180
-
181
- // escapeLiteralEscapes 已抽到 ./escape.mjs(advisor 与主 agent 发送路径共用),
182
- // 这里 re-export 保持既有 import 稳定。
183
- export { escapeLiteralEscapes }
184
-
185
-
186
- /**
187
- * Build the advisor conversation for this run.
188
- * EVERY call builds a fresh [system, user] session (decision d698434) — no
189
- * session reuse across rounds: round 1 = full scope (ROUND1 prompt), rounds
190
- * 2+ = convergence (ROUND2/ROUND3 prompt + fix-claims follow-up).
191
- * @param {Object} agent — the parent agent
192
- * @param {string} [reviewType] — "design" or "code" (default)
193
- * @param {string|null} [designToken] — design-review approval token (design only)
194
- * @param {string[]|null} [documents] — design review only: explicit list of doc paths to review (passed through to buildAdvisorUserMessage)
195
- * @param {string[]|null} [paths] — code review only: explicit list of file/dir paths to review
196
- * @param {Object|null} [object] — review-object declaration (§18.8 D-OA1): passed through
197
- * to the user-message builders; mechanically injected at the start of every review
198
- * round (round 1 design/code + round 2+ follow-up). Absent → legacy behavior.
199
- * @param {string|null} [designId] — §29.1 F2a: injected next to the design token in
200
- * the Approval Signal (round 1 + round 2+ — both values, verbatim anchor).
201
- */
202
- export function prepareAdvisorMessages(agent, reviewType, designToken = null, documents = null, paths = null, priorParam = null, object = null, designId = null) {
203
- // Deterministic convergence state (decision 2026-08-08): round 2+ requires
204
- // _advisorRound > 0 AND a stored prior review output. No history parsing.
205
- // priorParam (direct callers) wins over the stored output — same derivation
206
- // as buildAdvisorSystemPrompt (single source of truth for round semantics).
207
- const prior = (agent._advisorRound || 0) > 0 ? (priorParam ?? agent._lastAdvisorOutput) : null
208
-
209
- // Design review round 1: the dedicated full-scope review with the approval
210
- // token (an independent gate — it runs even when a prior review exists, e.g.
211
- // after a failed design review). Fresh session.
212
- if (reviewType === "design" && (agent._advisorRound || 0) === 0) {
213
- return [
214
- { role: "system", content: withTime(buildAdvisorSystemPrompt(agent, prior, reviewType)) },
215
- { role: "user", content: escapeLiteralEscapes(buildAdvisorUserMessage(agent, prior, reviewType, designToken, documents, paths, object, designId)) },
216
- ]
217
- }
218
-
219
- // Every round is a FRESH session (decision d698434): round 2+ must NOT reuse
220
- // round 1's messages — the old read outputs are the top anchoring source of
221
- // re-review false reports (the model quoted pre-fix file content instead of
222
- // re-reading) and a token sink. The agent response table (fix claims) is
223
- // injected through buildAdvisorFollowUp instead; the system prompt carries
224
- // the round (ROUND2/ROUND3) via buildAdvisorSystemPrompt.
225
- // No prior review output: reset ONLY when this run made no code changes (user
226
- // decision 2026-08-05: any loop that modified code must NOT reset — the
227
- // advisor guard WILL push back, so the convergence round must keep advancing
228
- // toward the cap; a run with no mutations has no push-back risk and a reset
229
- // is safe). Deterministic runtime state (`_mutatedThisRun`) decides — never
230
- // model output (phrases/table headers drift; three rounds of false reports
231
- // proved it). Either way the message is a fresh full review (no prior output
232
- // exists without a completed review) — only the round counter differs.
233
- if (!prior || (agent._advisorRound || 0) === 0) {
234
- if (!(agent._mutatedThisRun ?? false)) {
235
- // New review cycle (first review, all-clear, or no code changes): reset
236
- // the round so the cycle gets its own 5-round budget.
237
- agent._advisorRound = 0
238
- }
239
- // Mutations exist → KEEP the round (cap keeps advancing through retries).
240
- const user = buildAdvisorUserMessage(agent, prior, reviewType, designToken, documents, paths, object, designId)
241
- return [
242
- { role: "system", content: withTime(buildAdvisorSystemPrompt(agent, prior, reviewType)) },
243
- {
244
- role: "user",
245
- // NOTE (2026-08-06): the leading prefix is a PLAIN "System reminder:",
246
- // NOT "[System reminder: ...]" — some OpenAI-compatible servers try to
247
- // parse content that STARTS with '[' as structured content (or expand
248
- // escape sequences in it). A literal "\x" inside the conversation
249
- // background (e.g. the parent agent quoting escape sequences) then
250
- // fails server-side as "unexpected end of hex escape" → 400. Plain
251
- // prefix keeps the review message a plain string everywhere.
252
- // The whole content also passes through escapeLiteralEscapes (below)
253
- // so literal "\x"/"\u" quoted by the parent agent can never form an
254
- // invalid escape when the server expands them.
255
- content: escapeLiteralEscapes(`System reminder: no prior issue table is being carried into this review (first review, app restart, or session clear) — start with a fresh full review.\n\n${user}`),
256
- },
257
- ]
258
- }
259
-
260
- // Convergence rounds (2+): fresh [system(ROUND2/3), user(prior table + fix
261
- // claims)]. buildAdvisorFollowUp carries BOTH the prior issue table (the
262
- // only complete verification list — decision 2026-08-05, reversed) and the
263
- // agent's fix-claim table (focus reference). buildAdvisorSystemPrompt
264
- // selects ROUND2 for round 2, ROUND3 for rounds 3+ — a failed review retry
265
- // keeps _advisorRound so the convergence prompt matches the attempt count.
266
- // scopeFiles gives the fallback (agent gave no response table) a concrete
267
- // review surface.
268
- const scopeFiles = resolveScopeFiles(agent, paths)
269
- const followUp = buildAdvisorFollowUp(agent, prior, scopeFiles, object)
270
- // §11.2 D-24b (design round 2+ — async fix-round continuations must be able to
271
- // re-approve): re-anchor the review scope (the convergence follow-up carries no
272
- // document list) and inject the round's design token with the approval signal.
273
- if (reviewType === "design") {
274
- const docList = Array.isArray(documents)
275
- ? documents.filter((d) => typeof d === "string" && d.trim())
276
- : []
277
- const scopeBlock = docList.length > 0
278
- ? `\n\n## Documents to Review\nThe documents below are the review scope. Review ONLY these files — do not scan git diff or read any other files.\n${docList.map((d) => `- ${d} — Read this file in full`).join("\n")}`
279
- : ""
280
- const tokenBlock = designToken ? `\n\n${buildDesignApprovalBlock(designToken, designId)}` : ""
281
- return [
282
- { role: "system", content: withTime(buildAdvisorSystemPrompt(agent, prior, reviewType)) },
283
- { role: "user", content: escapeLiteralEscapes(followUp + scopeBlock + tokenBlock) },
284
- ]
285
- }
286
- return [
287
- { role: "system", content: withTime(buildAdvisorSystemPrompt(agent, prior, reviewType)) },
288
- { role: "user", content: escapeLiteralEscapes(followUp) },
289
- ]
290
- }