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
@@ -7,6 +7,9 @@
7
7
  * 不禁内容变更(.set/.delete/.push),调用方行为与同模块时代一致。
8
8
  */
9
9
 
10
+ // TUI-OOM-ROOTCAUSE(TUI.md §15.3.1):显示层额度常量/工具行定位(display-budget 叶子)。
11
+ import { capLines, TOOL_RESULT_MAX_CHARS } from "./display-budget.mjs"
12
+
10
13
  // Tool execution start timestamps (performance.now ms). Keyed by tool_call id
11
14
  // when available (parallel same-name tools each get their own tick — the
12
15
  // P0-3 fix, 2026-08-30), falling back to a per-name FIFO queue for callers
@@ -73,7 +76,9 @@ export function sweepToolBlocks(state) {
73
76
  * 1) Multimodal results (read_image) embed FULL base64 images in the JSON —
74
77
  * the human needs only the text part (model gets images via the multimodal
75
78
  * channel); 2) results beyond maxRows are truncated in the block (full
76
- * text always lives in history for the model). Returns row array. */
79
+ * text always lives in history for the model). Returns row array.
80
+ * TUI-OOM-ROOTCAUSE(§15.3.1 TOOL_RESULT_MAX_CHARS):总量额度与 400 行双维
81
+ * (单行任意大——无 `\n` 巨 chunk/JSON 尾行),尾截断 + 标记。 */
77
82
  export function slimToolResultForDisplay(result, maxRows = 400) {
78
83
  let displayResult = result
79
84
  try {
@@ -81,9 +86,18 @@ export function slimToolResultForDisplay(result, maxRows = 400) {
81
86
  if (parsed?.images?.length) displayResult = parsed.text ?? result
82
87
  } catch { /* not JSON — show as-is */ }
83
88
  const rows = String(displayResult).split("\n").filter((l) => l.trim())
84
- return rows.length > maxRows
89
+ const capped = rows.length > maxRows
85
90
  ? [...rows.slice(0, maxRows), `… (result truncated at ${maxRows} rows — full text in history)`]
86
91
  : rows
92
+ return capLines(capped, TOOL_RESULT_MAX_CHARS)
93
+ }
94
+
95
+ /** 按块定位其载体行(字符账 re-account 用——块对象唯一)。 */
96
+ export function findToolLine(state, name, toolId) {
97
+ const block = findToolBlock(state, name, toolId)
98
+ if (!block) return null
99
+ for (const l of state.lines ?? []) if (l._toolBlock === block) return l
100
+ return null
87
101
  }
88
102
  /** Mark the dispatch-level tool carrier done when its result is consumed by a
89
103
  * dedicated branch (subagent/escalate/advisor blocks) — without this the turn
@@ -19,29 +19,60 @@
19
19
  import { C } from "./ansi.mjs"
20
20
  import { formatToolSummary } from "./tool-summaries.mjs"
21
21
  import { describeToolArgs, toolArgsLines } from "./tool-args.mjs"
22
- import { ADVISOR_THINKING_PLACEHOLDER, resolveAdvisorProvider } from "../advisor/run.mjs"
22
+ import { ADVISOR_THINKING_PLACEHOLDER, resolveAdvisorProvider } from "@thincoder/core/advisor/run.mjs"
23
23
  import {
24
24
  SUBAGENT_ROLES, routeSubToken, routeSubReasoning, routeSubToolCall,
25
25
  routeSubToolOutput, finishSubTask, finishSubTaskKey, finishSubTasksByRole, freezeDoneSubTasks,
26
26
  ensureCompressPanel, markCompressFailed, markCompressDone, markCompressFallback,
27
+ shiftFreezeAnchors,
27
28
  } from "./subagent-blocks.mjs"
28
29
  // 第 27 批 §12.3②:前缀正则换名 + import 源改文法模块(纯换名——语义零改)。
29
- import { RELAY_PREFIX_RE } from "../agent/relay-prefix.mjs"
30
- import { TURN_CAP_MARK, STOPPED_MARK } from "../agent/spawn-child.mjs"
30
+ import { RELAY_PREFIX_RE } from "@thincoder/core/agent/relay-prefix.mjs"
31
+ import { TURN_CAP_MARK, STOPPED_MARK } from "@thincoder/core/agent/spawn-child.mjs"
32
+ // 批 1 CORE-DEFECT-FIXES B3:onWait 相位值域 + 文案单源(消费面禁各自枚举——PROVIDER.md §6.20)
33
+ import { waitStatusText } from "@thincoder/core/provider/wait-status.mjs"
31
34
  // 2026-09-05 module-split:ticks/maps/sweep/slim/settle/探测/find 族迁 tool-display.mjs——
32
35
  // buildToolCallbacks 内部引用用本地 import;sweepToolBlocks re-export(agent-turn 消费面)
33
36
  import {
34
37
  TOOL_OUTPUT_LINE_CAP, REMINDER_CAP, REMINDER_PERSIST_TURNS,
35
38
  _toolTicks, _subActions, _subActionQ,
36
39
  tickStart, tickTake, settleToolBlock, isAsyncSpawnResult, isSpawnErrorResult,
37
- findToolBlock, slimToolResultForDisplay, sweepToolBlocks,
40
+ findToolBlock, findToolLine, slimToolResultForDisplay, sweepToolBlocks,
38
41
  } from "./tool-display.mjs"
39
42
  export { sweepToolBlocks, slimToolResultForDisplay } from "./tool-display.mjs"
43
+ // TUI-OOM-ROOTCAUSE(TUI.md §15.3.1/§15.3.3):显示层额度(行/载体/输出环/评审/流式)。
44
+ import {
45
+ capText, capLine, capLines, appendCapped, capAdvisorText, accountLine, syncLineBudget,
46
+ LINE_TRUNC_MARKER, MIDDLE_TRUNC_MARKER, ADVISOR_CAP_OPTS, STREAM_CAP_OPTS,
47
+ TOOL_OUTPUT_ENTRY_MAX_CHARS, TOOL_OUTPUT_TOTAL_MAX_CHARS, ADVISOR_TEXT_MAX_CHARS,
48
+ } from "./display-budget.mjs"
49
+
50
+ /** 输出环字符维总量(§15.3.1 TOOL_OUTPUT_TOTAL_MAX_CHARS——与 200 条目环同款丢最旧)。 */
51
+ function trimOutputChars(block) {
52
+ let total = 0
53
+ for (const s of block.output) total += s.length
54
+ while (total > TOOL_OUTPUT_TOTAL_MAX_CHARS && block.output.length > 0) {
55
+ total -= block.output.shift().length
56
+ }
57
+ }
40
58
 
41
59
  /** Build the agent callbacks + the shared flushStream for one turn.
42
60
  * askPermission, askQuestion, saveSessionImpl } */
43
61
  export function buildToolCallbacks(deps) {
44
62
  const { agent, state, pushLine, render, scheduleRender, ensureAssistantLabel, askPermission, askBatchPermission, askQuestion, saveSessionImpl } = deps
63
+ // 行集总量对账便捷式(§15.3.3——冻结锚点平移交 subagent 族函数)
64
+ const budgetSync = () => syncLineBudget(state, { onTrim: (st, n) => shiftFreezeAnchors(st, n) })
65
+ /** Advisor 有序块累积 + 额度(§15.3.1 ADVISOR_TEXT_MAX_CHARS——头 32K/尾 96K/中段标记)。 */
66
+ const pushAdvisorChunk = (raw, kind) => {
67
+ const blocks = state._advisorBlocks ??= []
68
+ const last = blocks.at(-1)
69
+ if (last && last.kind === kind) last.text = appendCapped(last.text, raw, ADVISOR_CAP_OPTS)
70
+ else blocks.push({ kind, text: appendCapped("", raw, ADVISOR_CAP_OPTS) })
71
+ // 多块总量:丢最旧块直至 ≤ 额度(裁决尾部优先)
72
+ let total = 0
73
+ for (const b of blocks) total += b.text.length
74
+ while (total > ADVISOR_TEXT_MAX_CHARS && blocks.length > 1) total -= blocks.shift().text.length
75
+ }
45
76
  // NOTE: advisor buffers (_advisorThink/advisorStreaming) are cleared here too.
46
77
  // Timing safety: onToolResult flushes _advisorThink into history and empties
47
78
  // the buffers BEFORE onTurnEnd can call flushStream (tool result is
@@ -69,7 +100,7 @@ export function buildToolCallbacks(deps) {
69
100
  // block content, never the main stream (D1). Routing details in subagent-blocks.
70
101
  if (routeSubToken(state, t, scheduleRender)) return
71
102
  ensureAssistantLabel()
72
- state.streaming += t
103
+ state.streaming = appendCapped(state.streaming, t, STREAM_CAP_OPTS) // §15.3.1 STREAM_MAX_CHARS
73
104
  scheduleRender()
74
105
  },
75
106
  onReasoning: (t) => {
@@ -77,7 +108,7 @@ export function buildToolCallbacks(deps) {
77
108
  // buffer as kind=think (F2: same treatment as the main reasoning stream).
78
109
  if (routeSubReasoning(state, t, scheduleRender)) return
79
110
  ensureAssistantLabel()
80
- state.reasoning += t
111
+ state.reasoning = appendCapped(state.reasoning, t, STREAM_CAP_OPTS) // §15.3.1 STREAM_MAX_CHARS
81
112
  scheduleRender()
82
113
  },
83
114
  onToolCall: (name, args, toolId) => {
@@ -139,6 +170,8 @@ export function buildToolCallbacks(deps) {
139
170
  done: false,
140
171
  },
141
172
  })
173
+ accountLine(state, state.lines[state.lines.length - 1]) // 载体入总量账(§15.3.4)
174
+ budgetSync()
142
175
  tickStart(name, toolId)
143
176
  },
144
177
  // §7.2.3(方案 e):dispatch runOne 把工具 ctx 上的 _subagentKey(sync spawn/
@@ -236,11 +269,13 @@ export function buildToolCallbacks(deps) {
236
269
  // screen of garbage (user report 2026-08-30). The model gets the image
237
270
  // via the multimodal channel (agent.mjs), the human needs only the
238
271
  // text part: strip image parts from the displayed result.
239
- block.result = slimToolResultForDisplay(result)
272
+ block.result = slimToolResultForDisplay(result) // §15.3.1 RESULT 额度(行 400 + 字符 64K 双维)
240
273
  block.summary = formatToolSummary(name, result)
241
274
  block.done = true
242
275
  const started = tickTake(name, toolId)
243
276
  block.elapsed = started !== null ? Math.round(performance.now() - started) : null
277
+ accountLine(state, findToolLine(state, name, toolId)) // 载体原地变更 → 重新入账
278
+ budgetSync()
244
279
  }
245
280
  }
246
281
  if (name === "advisor") {
@@ -255,17 +290,22 @@ export function buildToolCallbacks(deps) {
255
290
  // _advisorBlocks keep rendering the running view until cleared at turn end.
256
291
  const blocks = state._advisorBlocks ?? []
257
292
  if (blocks.length > 0) {
258
- const text = blocks
293
+ // 冻结文本额度(§15.3.1:头 32K + 尾 96K 保裁决尾部 + 中段标记——非静默截断,
294
+ // 代码评审 #3:capLines(...)[0] 会丢标记且截尾)
295
+ const text = capAdvisorText(blocks
259
296
  .map((b) => b.text.replaceAll(ADVISOR_THINKING_PLACEHOLDER, ""))
260
297
  .join("")
261
298
  .replace(/\n{3,}/g, "\n\n")
262
- .trim()
299
+ .trim())
263
300
  if (text) {
264
- state.lines.push({
301
+ const line = {
265
302
  text: "advisor review",
266
303
  color: C.dim,
267
304
  _frozenAdvisor: text,
268
- })
305
+ }
306
+ state.lines.push(line)
307
+ accountLine(state, line)
308
+ budgetSync()
269
309
  }
270
310
  }
271
311
  }
@@ -298,25 +338,26 @@ export function buildToolCallbacks(deps) {
298
338
  const isString = typeof chunk === "string"
299
339
  const raw = isString ? chunk : String(chunk?.text ?? "")
300
340
  const kind = isString ? "text" : (chunk?.kind ?? "text")
301
- const blocks = state._advisorBlocks ??= []
302
- const last = blocks.at(-1)
303
- if (last && last.kind === kind) last.text += raw
304
- else blocks.push({ kind, text: raw })
341
+ pushAdvisorChunk(raw, kind) // §15.3.1 累积额度(多块丢最旧)
305
342
  scheduleRender()
306
343
  return
307
344
  }
308
345
  // Append into the CURRENT tool block's output buffer (the block is the
309
346
  // display; no _live scroll lines anymore). N2-style cap keeps memory
310
- // bounded: keep the LAST 200 output lines per call.
347
+ // bounded: keep the LAST 200 output lines per call; 字符维双维
348
+ // (§15.3.1:单条目 ≤ TOOL_OUTPUT_ENTRY_MAX_CHARS、总量 ≤ TOOL_OUTPUT_TOTAL_MAX_CHARS 丢最旧)。
311
349
  const block = findToolBlock(state, name, toolId)
312
350
  if (block) {
313
351
  for (const line of part.text.split("\n")) {
314
352
  const trimmed = line.trimEnd()
315
- if (trimmed) block.output.push(trimmed)
353
+ if (trimmed) block.output.push(capText(trimmed, { max: TOOL_OUTPUT_ENTRY_MAX_CHARS, keepHead: 6_000, keepTail: 2_000, marker: MIDDLE_TRUNC_MARKER }))
316
354
  }
317
355
  if (block.output.length > TOOL_OUTPUT_LINE_CAP) {
318
356
  block.output.splice(0, block.output.length - TOOL_OUTPUT_LINE_CAP)
319
357
  }
358
+ trimOutputChars(block)
359
+ accountLine(state, findToolLine(state, name, toolId))
360
+ budgetSync()
320
361
  }
321
362
  scheduleRender()
322
363
  },
@@ -363,11 +404,12 @@ export function buildToolCallbacks(deps) {
363
404
  state.tokens.cacheMiss += usage.prompt_cache_miss_tokens ?? 0
364
405
  state.tokens.reasoningTokens += usage.completion_tokens_details?.reasoning_tokens ?? 0
365
406
  },
366
- // Throttle wait (active gate / 429 backoff): show in status bar so user knows it's not frozen
367
- onWait: ({ phase, seconds }) => {
368
- if (phase === "gate") state.status = `TPM throttle wait ~${seconds}s`
369
- else if (phase === "overloaded") state.status = `Server overloaded, retrying in ${seconds}s`
370
- else state.status = `Rate-limited 429, retry in ${seconds}s`
407
+ // Throttle wait (active gate / 429 backoff): show in status bar so user knows it's not frozen.
408
+ // 相位值域/文案取自核单源(wait-status.mjs)——未知相位与 warn 前置告警不显示(不落兜底误标)。
409
+ onWait: (ev) => {
410
+ const s = waitStatusText(ev)
411
+ if (!s) return
412
+ state.status = s
371
413
  render()
372
414
  },
373
415
  onTaskUpdate: (items) => {
@@ -15,9 +15,16 @@ export function writeStartupSequence(write = (s) => process.stdout.write(s)) {
15
15
  write(ansi.altBuffer + ansi.hideCursor + ansi.mouseOn + ansi.bracketedPasteOn + ansi.keyboardPush + ansi.modifyOtherKeysOn + ansi.wrapOff)
16
16
  }
17
17
 
18
- /** TUI 清理序列:清屏 + 关闭鼠标/粘贴/键盘增强 + 退出 alt buffer + 显示光标 + 恢复环绕。 */
18
+ /** TUI 清理序列 = 异常退出恢复序列(单一来源——§9.2 表 2 候选 1):清屏 + 关闭鼠标/
19
+ * 粘贴/键盘增强 + 退出 alt buffer + 显示光标 + 恢复环绕。
20
+ * TUI-OOM-ROOTCAUSE(CRASH-REPORTS.md §9.3):提取为导出常量 `RECOVERY_SEQUENCE`——
21
+ * 包装父在子异常退出(V8 fatal——不走 JS 钩子)时补发同一序列(单一来源,禁第三份
22
+ * 副本);writeCleanupSequence 引用同常量(行为逐字不变——既有字节锁测试保持,AC-RT3)。 */
23
+ export const RECOVERY_SEQUENCE =
24
+ ansi.clearScreen + ansi.mouseOff + ansi.bracketedPasteOff + ansi.keyboardPop + ansi.modifyOtherKeysOff + ansi.mainBuffer + ansi.showCursor + ansi.reset + ansi.wrapOn
25
+
19
26
  export function writeCleanupSequence(write = (s) => process.stdout.write(s)) {
20
- write(ansi.clearScreen + ansi.mouseOff + ansi.bracketedPasteOff + ansi.keyboardPop + ansi.modifyOtherKeysOff + ansi.mainBuffer + ansi.showCursor + ansi.reset + ansi.wrapOn)
27
+ write(RECOVERY_SEQUENCE)
21
28
  }
22
29
 
23
30
  // R25(ARCHITECTURE.md §R25 F-R25a——复审 #1):TUI 活动态标志——index.mjs 在终端接管
@@ -6,7 +6,7 @@
6
6
  * ctx: { agent, state, pushLine, pushLabel, render, persistRaw, openModelPicker }
7
7
  */
8
8
 
9
- import { PROVIDER_PRESETS as PRESETS } from "../config.mjs"
9
+ import { PROVIDER_PRESETS as PRESETS } from "@thincoder/core/config.mjs"
10
10
  import { ansi, C } from "./ansi.mjs"
11
11
  import { computeLayout } from "./layout.mjs"
12
12
  import { probeChannelModels } from "./model-catalog.mjs"
@@ -202,7 +202,7 @@ export function createWizard(ctx) {
202
202
  agent.provider = { ...agent.providers.find((p) => p.name === f.name) }
203
203
  agent.provider.model = agent.activeModel
204
204
  if (agent.config?.agent?.compactThresholdAuto) {
205
- const { resolveCompactThreshold } = await import("../config.mjs")
205
+ const { resolveCompactThreshold } = await import("@thincoder/core/config.mjs")
206
206
  agent.config.agent.compactThreshold = resolveCompactThreshold(null, agent.provider).value
207
207
  }
208
208
  // agent.config 是 loadConfig merged——无 active* 键可写——defaultModel 随内存 merged 更新
@@ -226,7 +226,7 @@ export function createWizard(ctx) {
226
226
  agent.config.embedding ??= {}
227
227
  agent.config.embedding.apiKey = f.embedkey
228
228
  if (agent.memory && !agent.memory.embedder) {
229
- const { createEmbedder } = await import("../embedding.mjs")
229
+ const { createEmbedder } = await import("@thincoder/core/embedding.mjs")
230
230
  agent.memory.embedder = createEmbedder(agent.config.embedding)
231
231
  }
232
232
  pushLine(`Vector search enabled (${agent.config.embedding.model ?? "BAAI/bge-m3"})`, C.tool)
@@ -6,12 +6,13 @@ import { appendFileSync, mkdirSync, writeFileSync } from "node:fs"
6
6
  import { spawn } from "node:child_process"
7
7
  import { fileURLToPath } from "node:url"
8
8
  import { crashReportsDir } from "../crash-reports.mjs"
9
+ import { RECOVERY_SEQUENCE } from "./tui-lifecycle.mjs"
9
10
 
10
11
  // F-3 信号语义(raw mode 既有 key-handler 双按语义——Ctrl+C = stdin 字节不产生信号 → 子正常退 → 父收
11
12
  // exit 同码退):父忽略 SIGINT/SIGTERM——tee 不被打断。2026-09-09 实测:Windows process.kill(SIGINT)
12
13
  // = 硬杀 ≠ 控制台 Ctrl+C 事件(handler 不触发)——信号面测试走 mock(真控制台端到端留发布前手动 QA)。
13
14
  export const ignoreSignal = () => {} // no-op 单一引用——父信号 + stderr-error 监听共用——测试可精确复原
14
- export function spawnTuiWrapped({ dir = crashReportsDir(), script = fileURLToPath(new URL("../../bin/thincoder.mjs", import.meta.url)), spawnImpl = spawn, exitImpl = (code) => process.exit(code) } = {}) {
15
+ export function spawnTuiWrapped({ dir = crashReportsDir(), script = fileURLToPath(new URL("../../bin/thincoder.mjs", import.meta.url)), spawnImpl = spawn, exitImpl = (code) => process.exit(code), writeImpl = (s) => process.stdout.write(s) } = {}) {
15
16
  // ① mkdir 前置(评审 #2——prepareCrashReporting 只在子内跑——首启目录缺失会静默不包装)+ 开日志
16
17
  //(文件头元信息行:时间/pid/argv——F-2——0600 append)——失败 → false:不包装直接跑现逻辑(尽力面)
17
18
  let logPath = null
@@ -25,14 +26,29 @@ export function spawnTuiWrapped({ dir = crashReportsDir(), script = fileURLToPat
25
26
  const note = (s) => { try { appendFileSync(logPath, s) } catch { /* 落盘尽力面——不阻断 tee */ } }
26
27
  let exitCode = null, exitSignal = null, settled = false
27
28
  const finish = (code) => { if (!settled) { settled = true; exitImpl(code) } }
28
- const child = spawnImpl(process.execPath, [script, ...process.argv.slice(2)], {
29
+ // TUI-OOM-ROOTCAUSE(CRASH-REPORTS.md §9.3):子异常退出(V8 fatal——不走 JS 钩子)→
30
+ // 包装父补发恢复序列(唯一存活方;序列单一来源 = tui-lifecycle.RECOVERY_SEQUENCE)。
31
+ // 守卫落点 = exit/close 处置内(**不在共享 finish**——spawn error 路径经 error→finish(1),
32
+ // 字面挂 finish 会在子未启动时补发 clearScreen 序列,违 F5③);按 exitCode/exitSignal
33
+ // 判定(code !== 0 || signal != null);异常退出零动作;正常退出(code 0)零干预;
34
+ // 每进程恰一次(recovered 守卫,exit/close 双路 + 30s 兜底路径同守);写失败不阻断收尾。
35
+ let recovered = false
36
+ const recoverTerminal = (code, signal) => {
37
+ if (recovered) return
38
+ if (!(code !== 0 || signal != null)) return
39
+ recovered = true
40
+ try { writeImpl(RECOVERY_SEQUENCE) } catch { /* 尽力面:终端已关/管道已断——不阻断收尾 */ }
41
+ }
42
+ // F3③(CRASH-REPORTS):近堆上限快照落点定向——--diagnostic-dir 为 Node 选项,须在脚本路径前;
43
+ // 无条件注入(gate 单点在 crash-reports.mjs——包装器不判 env);未触发快照时零副作用。
44
+ const child = spawnImpl(process.execPath, [`--diagnostic-dir=${dir}`, script, ...process.argv.slice(2)], {
29
45
  stdio: ["inherit", "inherit", "pipe"], // 子 stderr pipe → tee;stdin/stdout 继承(TTY 原样)
30
46
  env: { ...process.env, THINCODER_TUI_WRAPPED: "1" }, // env 门——子内判定不包装——纯现逻辑(红线)
31
47
  windowsHide: false,
32
48
  })
33
49
  child.stderr.on("data", (chunk) => { try { process.stderr.write(chunk) } catch { /* 终端已关——日志仍落 */ } note(chunk) }) // ② tee 双写:终端实时 + 日志
34
- child.on("exit", (code, signal) => { exitCode = code; exitSignal = signal; setTimeout(() => finish(exitCode ?? (exitSignal ? 1 : 0)), 30_000).unref() }) // ③ exit 记码;兜底 30s 强退
35
- child.on("close", () => finish(exitCode ?? (exitSignal ? 1 : 0))) // ④ close = stderr 尾数据全收(AC-2)→ 同码退
36
- child.on("error", (err) => { note(`# spawn error: ${err.message}\n`); finish(1) }) // ⑤ spawn 失败不发 exit 只发 error
50
+ child.on("exit", (code, signal) => { exitCode = code; exitSignal = signal; recoverTerminal(code, signal); setTimeout(() => { recoverTerminal(exitCode, exitSignal); finish(exitCode ?? (exitSignal ? 1 : 0)) }, 30_000).unref() }) // ③ exit 记码;异常退出→补发恢复;兜底 30s 强退
51
+ child.on("close", () => { recoverTerminal(exitCode, exitSignal); finish(exitCode ?? (exitSignal ? 1 : 0)) }) // ④ close = stderr 尾数据全收(AC-2)→ 同码退
52
+ child.on("error", (err) => { note(`# spawn error: ${err.message}\n`); finish(1) }) // ⑤ spawn 失败不发 exit 只发 error(零序列动作——F5③)
37
53
  return true
38
54
  }
@@ -1,116 +0,0 @@
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
- }
@@ -1,139 +0,0 @@
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
- }