@thincoder/core 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/abort-provenance.mjs +116 -0
  2. package/advisor/citations.mjs +139 -0
  3. package/advisor/compaction.mjs +174 -0
  4. package/advisor/convergence.mjs +80 -0
  5. package/advisor/history.mjs +77 -0
  6. package/advisor/loop.mjs +288 -0
  7. package/advisor/messages.mjs +299 -0
  8. package/advisor/notice.mjs +141 -0
  9. package/advisor/project-context.mjs +197 -0
  10. package/advisor/repos.mjs +150 -0
  11. package/advisor/run.mjs +190 -0
  12. package/advisor/truncate.mjs +57 -0
  13. package/advisor.mjs +281 -0
  14. package/agent/child-marks.mjs +24 -0
  15. package/agent/completion.mjs +145 -0
  16. package/agent/dispatch.mjs +493 -0
  17. package/agent/family-tools.mjs +174 -0
  18. package/agent/helpers.mjs +412 -0
  19. package/agent/post-turn.mjs +70 -0
  20. package/agent/record-results.mjs +174 -0
  21. package/agent/relay-prefix.mjs +39 -0
  22. package/agent/run-stages.mjs +244 -0
  23. package/agent/setup-reminders.mjs +199 -0
  24. package/agent/setup.mjs +234 -0
  25. package/agent/spawn-child.mjs +258 -0
  26. package/agent/suspension.mjs +240 -0
  27. package/agent/write-gate.mjs +87 -0
  28. package/agent-tools/advisor-async.mjs +481 -0
  29. package/agent-tools/advisor-settle.mjs +240 -0
  30. package/agent-tools/advisor.mjs +280 -0
  31. package/agent-tools/async-discard.mjs +143 -0
  32. package/agent-tools/async-settle.mjs +299 -0
  33. package/agent-tools/batch-segment.mjs +265 -0
  34. package/agent-tools/child-permission.mjs +45 -0
  35. package/agent-tools/consult.mjs +471 -0
  36. package/agent-tools/design-token.mjs +117 -0
  37. package/agent-tools/digest-budget.mjs +76 -0
  38. package/agent-tools/eng.mjs +102 -0
  39. package/agent-tools/escalate-async.mjs +302 -0
  40. package/agent-tools/goal.mjs +119 -0
  41. package/agent-tools/panel-blocks.mjs +24 -0
  42. package/agent-tools/parent-channel.mjs +231 -0
  43. package/agent-tools/plan.mjs +86 -0
  44. package/agent-tools/read-history.mjs +309 -0
  45. package/agent-tools/recent-changes.mjs +24 -0
  46. package/agent-tools/review-facts.mjs +31 -0
  47. package/agent-tools/settings.mjs +268 -0
  48. package/agent-tools/skill.mjs +63 -0
  49. package/agent-tools/spawn-gates.mjs +109 -0
  50. package/agent-tools/subagent-actions.mjs +496 -0
  51. package/agent-tools/subagent-async.mjs +456 -0
  52. package/agent-tools/subagent-panel.mjs +160 -0
  53. package/agent-tools/subagent-run.mjs +208 -0
  54. package/agent-tools/subagent-scheduler.mjs +446 -0
  55. package/agent-tools/subagent-spawn.mjs +478 -0
  56. package/agent-tools/subagent.mjs +419 -0
  57. package/agent-tools/task.mjs +87 -0
  58. package/agent-tools/timer.mjs +46 -0
  59. package/agent-tools/verify.mjs +295 -0
  60. package/agent-tools.mjs +23 -0
  61. package/agent.mjs +430 -0
  62. package/auto-think.mjs +115 -0
  63. package/compress-form.mjs +24 -0
  64. package/config-io.mjs +277 -0
  65. package/config-migrate.mjs +178 -0
  66. package/config-presets.mjs +49 -0
  67. package/config.mjs +419 -0
  68. package/context.mjs +495 -0
  69. package/conventions.mjs +223 -0
  70. package/embedding.mjs +120 -0
  71. package/escape.mjs +152 -0
  72. package/expand-home.mjs +16 -0
  73. package/explore-distill.mjs +152 -0
  74. package/generate-title.mjs +123 -0
  75. package/git/checkpoint.mjs +448 -0
  76. package/git/gitmem.mjs +100 -0
  77. package/history-window.mjs +179 -0
  78. package/hooks.mjs +108 -0
  79. package/i18n.mjs +106 -0
  80. package/index-bin.mjs +48 -0
  81. package/index-discover.mjs +176 -0
  82. package/ledger-cmd.mjs +209 -0
  83. package/ledger-db.mjs +86 -0
  84. package/ledger-surface.mjs +76 -0
  85. package/ledger.mjs +202 -0
  86. package/log.mjs +195 -0
  87. package/manifest.mjs +338 -0
  88. package/markdown.mjs +106 -0
  89. package/mcp/helpers.mjs +51 -0
  90. package/mcp/transport-http.mjs +248 -0
  91. package/mcp/transport-stdio.mjs +140 -0
  92. package/mcp/transport-ws.mjs +122 -0
  93. package/mcp.mjs +295 -0
  94. package/memory/code-index.mjs +219 -0
  95. package/memory/code-sync.mjs +427 -0
  96. package/memory/core.mjs +318 -0
  97. package/memory/delete.mjs +242 -0
  98. package/memory/docs.mjs +431 -0
  99. package/memory/file-walk.mjs +109 -0
  100. package/memory/origin.mjs +24 -0
  101. package/memory/scan.mjs +177 -0
  102. package/memory/schema.mjs +460 -0
  103. package/memory.mjs +21 -0
  104. package/model-ref.mjs +66 -0
  105. package/model-specs.mjs +277 -0
  106. package/package.json +32 -0
  107. package/peer-domains.mjs +265 -0
  108. package/peer-instances.mjs +178 -0
  109. package/permission.mjs +79 -0
  110. package/process-probe.mjs +315 -0
  111. package/prompt-files.mjs +113 -0
  112. package/prompt-overlays.mjs +78 -0
  113. package/prompts/advisor-design.md +43 -0
  114. package/prompts/advisor-round1.md +41 -0
  115. package/prompts/advisor-round2.md +46 -0
  116. package/prompts/advisor-round3.md +42 -0
  117. package/prompts/common.md +158 -0
  118. package/prompts/consult-base.md +19 -0
  119. package/prompts/discipline-engineering.md +123 -0
  120. package/prompts/discipline-normal.md +206 -0
  121. package/prompts/persona-coder.md +21 -0
  122. package/prompts/persona-eng-coder.md +41 -0
  123. package/prompts/persona-eng-designer.md +80 -0
  124. package/prompts/persona-engineering.md +160 -0
  125. package/prompts/persona-explore.md +15 -0
  126. package/prompts/persona-normal.md +35 -0
  127. package/prompts/persona-plan.md +27 -0
  128. package/provider/anthropic.mjs +225 -0
  129. package/provider/core.mjs +491 -0
  130. package/provider/errors.mjs +101 -0
  131. package/provider/google.mjs +257 -0
  132. package/provider/index.mjs +7 -0
  133. package/provider/list-models.mjs +163 -0
  134. package/provider/normalize.mjs +81 -0
  135. package/provider/rate.mjs +168 -0
  136. package/provider/responses.mjs +495 -0
  137. package/provider/retry.mjs +88 -0
  138. package/provider/sse.mjs +264 -0
  139. package/provider/wait-status.mjs +59 -0
  140. package/proxy.mjs +274 -0
  141. package/rules.mjs +53 -0
  142. package/session-gc.mjs +248 -0
  143. package/session-guard.mjs +59 -0
  144. package/session-lifecycle.mjs +305 -0
  145. package/session-migrate.mjs +48 -0
  146. package/session-rename.mjs +38 -0
  147. package/session-segments.mjs +99 -0
  148. package/session-slot-write.mjs +168 -0
  149. package/session-slots-manifest.mjs +264 -0
  150. package/session-slots.mjs +298 -0
  151. package/session-store.mjs +441 -0
  152. package/session.mjs +244 -0
  153. package/skills.mjs +234 -0
  154. package/text-budget.mjs +79 -0
  155. package/token-ttl.mjs +285 -0
  156. package/tool-docs/apply_patch.md +15 -0
  157. package/tool-docs/bash.md +38 -0
  158. package/tool-docs/delete.md +13 -0
  159. package/tool-docs/edit.md +30 -0
  160. package/tool-docs/execute.md +21 -0
  161. package/tool-docs/fetch.md +12 -0
  162. package/tool-docs/file_ops.md +17 -0
  163. package/tool-docs/get_current_time.md +8 -0
  164. package/tool-docs/git.md +54 -0
  165. package/tool-docs/glob.md +11 -0
  166. package/tool-docs/grep.md +19 -0
  167. package/tool-docs/hashline_edit.md +14 -0
  168. package/tool-docs/insert_after.md +15 -0
  169. package/tool-docs/lint.md +10 -0
  170. package/tool-docs/ls.md +12 -0
  171. package/tool-docs/lsp.md +10 -0
  172. package/tool-docs/process.md +10 -0
  173. package/tool-docs/question.md +16 -0
  174. package/tool-docs/read.md +20 -0
  175. package/tool-docs/read_image.md +8 -0
  176. package/tool-docs/tree.md +14 -0
  177. package/tool-docs/wait_for.md +22 -0
  178. package/tool-docs/websearch.md +16 -0
  179. package/tool-docs/write.md +11 -0
  180. package/tools/bash.mjs +276 -0
  181. package/tools/edit-batch.mjs +204 -0
  182. package/tools/edit-diff.mjs +388 -0
  183. package/tools/exec-run.mjs +43 -0
  184. package/tools/execute.mjs +243 -0
  185. package/tools/file.mjs +464 -0
  186. package/tools/git-checkpoint.mjs +143 -0
  187. package/tools/git-ext.mjs +173 -0
  188. package/tools/git.mjs +415 -0
  189. package/tools/glob-dialect.mjs +130 -0
  190. package/tools/index.mjs +76 -0
  191. package/tools/linter.mjs +120 -0
  192. package/tools/lsp.mjs +335 -0
  193. package/tools/ops.mjs +293 -0
  194. package/tools/patch.mjs +290 -0
  195. package/tools/question.mjs +26 -0
  196. package/tools/repomap.mjs +314 -0
  197. package/tools/search.mjs +248 -0
  198. package/tools/shared.mjs +467 -0
  199. package/tools/tree.mjs +81 -0
  200. package/tools/web.mjs +224 -0
  201. package/tools/write-path.mjs +191 -0
  202. package/traces/trace-store.mjs +303 -0
  203. package/undo-stack.mjs +47 -0
@@ -0,0 +1,288 @@
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, assistantToolCallMessage } from "../config.mjs"
12
+ import { toOpenAISchema } from "../tools/index.mjs"
13
+ import { truncateAdvisorResult } from "./truncate.mjs"
14
+ import { batchSegmentTool } from "../agent-tools/batch-segment.mjs"
15
+ import {
16
+ estimateTokens, compactMessages, shouldBudgetNudge, budgetNudgeText, timeoutTail, renderTimeline,
17
+ MAX_ADVISOR_TURNS, advisorContextBudget, TOOL_TIMEOUT_MS, REVIEW_TIMEOUT_MS, MAX_RESULT_CHARS,
18
+ ADVISOR_THINKING_PLACEHOLDER,
19
+ } from "./compaction.mjs"
20
+
21
+ const { readTool, globTool, grepTool, lsTool } = await import("../tools/index.mjs")
22
+ const { lspTool } = await import("../tools/lsp.mjs")
23
+ const { codeSearchTool } = await import("../memory/code-sync.mjs")
24
+
25
+ /**
26
+ * Advisor tool set — ZERO git, read-only ONLY, every round. The change surface comes from the
27
+ * review scope (paths / _touchedFiles injected by the caller), never from git: git output misled
28
+ * reviews (committed fixes never show in `git diff HEAD`, so "no changes" was read as "not
29
+ * fixed") and the user mandate is full decoupling (7d49a52 + d3be613). The reviewer reads files
30
+ * and searches code; it never touches git and never writes. No round parameter — the set is
31
+ * constant across all rounds. 检索面**恒在**(与索引绑定解耦):`code_search` 永远在集内,
32
+ * 未绑定索引的会话在**执行面**降级为一句端中立说明(不崩、不伪造结果)——工具清单恒定。
33
+ * @param {Object} agent — 索引面载体(`agent.memory`);缺省时执行面降级。
34
+ */
35
+ function advisorCodeSearchTool(agent) {
36
+ return agent?.memory
37
+ ? codeSearchTool(agent.memory)
38
+ : { ...codeSearchTool(undefined), async execute() { return "(code index not available in this session)" } }
39
+ }
40
+
41
+ function advisorToolsFor(agent, reviewType = "code", batchDoc = null) {
42
+ const tools = [readTool, globTool, grepTool, lsTool, lspTool, advisorCodeSearchTool(agent)]
43
+ // §2.20.3(第 4 批):**只有绑定了批次档的设计评审**额外拿到写通道——代码评审工具集
44
+ // 逐字节不变(零 git + 只读不变量,§2.20.8 #1);未绑定 → 不挂载(fail-closed)。
45
+ if (reviewType === "design" && batchDoc) tools.push(batchSegmentTool(batchDoc, { review: true }))
46
+ return { schemas: tools.map(toOpenAISchema), byName: new Map(tools.map((t) => [t.name, t])) }
47
+ }
48
+ // Test seam: the tool set is pure(恒在六工具 + 批次档绑定條件)。
49
+ export { advisorToolsFor, advisorToolsFor as _advisorToolsFor }
50
+
51
+ /**
52
+ * Run the advisor's tool loop: chat → execute tools → repeat.
53
+ * Stops when the model produces text without tool calls.
54
+ *
55
+ * Progress lines (→ tool args) are emitted via onOutput between model bursts so
56
+ * the panel keeps moving while the advisor explores — otherwise the panel sits
57
+ * frozen through every tool-call phase and the review appears to have stalled.
58
+ *
59
+ * @param {string|null} [pinned] — 第 11 批:压缩定锚简报(评审参数构建——F13/§14.4 #3)。
60
+ * @param {{now?: Function, chat?: Function, describeArgs?: Function}} [seams] — 测试缝 + ④ 端差注入点
61
+ * (默认 Date.now / chat / 无进度行——生产调用不传时进度行由端装配层补齐)。
62
+ */
63
+ async function runAdvisorToolLoop(provider, messages, onOutput, signal, agent, cwd, toolsOverride = null, reviewType = "code", batchDoc = null, pinned = null, seams = {}) {
64
+ const now = seams.now ?? Date.now
65
+ const chatCall = seams.chat ?? chat
66
+ // ④ 端差注入点(CORE-UNIFICATION §2.5 #109「进度行按端注入」):工具进度行的参数摘要
67
+ // 是**展示面**(CLI = `tui/tool-args.mjs` 的 `describeToolArgs`;VSC = 卡片头),不进核。
68
+ // 未注入 ⇒ 不发进度行(核内零端名分支——契约 5 / 10)。端在装配层传入自己的格式化器。
69
+ const describeArgs = seams.describeArgs ?? (() => "")
70
+ // 第 11 批硬墙 / 预算 / 尾:实现注解见下方各点;守卫函数与 renderTimeline 在 compaction.mjs。
71
+ // Kind-tagged wrappers: the TUI panel colors reasoning / answer / tool progress differently.
72
+ // Every chunk is ALSO recorded into an ordered timeline — the persisted record
73
+ // must show the review process (thinking ↔ tool progress ↔ final text) at its
74
+ // real positions, not a summary appended at the end. Same-kind consecutive
75
+ // chunks merge (token streams); kind flips start a new entry.
76
+ const timeline = []
77
+ const record = (kind, text) => {
78
+ const last = timeline.at(-1)
79
+ if (last && last.kind === kind) last.text += text
80
+ else timeline.push({ kind, text })
81
+ }
82
+ const emit = (kind) => (text) => { record(kind, text); onOutput?.({ kind, text }) }
83
+ const onThink = emit("think")
84
+ const onText = emit("text")
85
+ const onTool = emit("tool")
86
+ // toolsOverride = test seam (T-TS8/9): the real advisor tool set, or a mock
87
+ // set with controllable timing/errors.
88
+ const { schemas: toolSchemas, byName: toolByName } = toolsOverride ?? advisorToolsFor(agent, reviewType, batchDoc)
89
+ let turns = 0
90
+ let toolCallCount = 0
91
+ let reviewTextProduced = false
92
+ let budgetNudged = false
93
+ const startTime = now()
94
+ // 第 25 批(§16.3):上下文预算跟随评审模型窗口——`providerSpec`(模型规格表 × provider 级
95
+ // context 覆盖)派生;函数体内、while 轮次外一次性(provider 全场不变),两档消费见下守卫。
96
+ const budget = advisorContextBudget(provider)
97
+
98
+ while (true) {
99
+ // Interrupted (Ctrl+I) — stop immediately instead of spinning a fresh uncancellable signal
100
+ if (signal?.aborted) return renderTimeline(timeline, "Advisor: interrupted.")
101
+
102
+ // Check review timeout (10 minutes by default; agent.advisor.timeoutMs overrides)
103
+ // 运行时校验(设计评审 #1,2026-08-24):手写 config.json 的非法值(0/负数/字符串)
104
+ // 不得静默禁用或立即触发超时——非法一律回退默认。
105
+ const cfg = agent.config?.advisor?.timeoutMs
106
+ const timeoutMs = (Number.isFinite(cfg) && cfg > 0) ? cfg : REVIEW_TIMEOUT_MS
107
+ const elapsed = now() - startTime
108
+ const remaining = timeoutMs - elapsed
109
+ // 硬墙(§14.6 #1):预算用尽 → 结构化超时尾(首行 = 判定族 timeout 前缀)。
110
+ if (remaining <= 0) {
111
+ return renderTimeline(timeline, timeoutTail(timeoutMs, turns, toolCallCount, reviewTextProduced))
112
+ }
113
+ // 0.75 一次性预算提示(§14.6 #2——同一检查点、每场评审至多一次):注入一条 user 消息
114
+ // 促模型在墙前收敛产出(不改语义判据、不碰提示词面)。
115
+ if (shouldBudgetNudge(elapsed, timeoutMs, budgetNudged)) {
116
+ budgetNudged = true
117
+ messages.push({ role: "user", content: budgetNudgeText(elapsed, timeoutMs) })
118
+ }
119
+
120
+ if (++turns > MAX_ADVISOR_TURNS) {
121
+ return renderTimeline(timeline, "Advisor: stopped after " + MAX_ADVISOR_TURNS + " tool rounds — the review appears to be looping. You may retry with a narrower scope.")
122
+ }
123
+
124
+ // Check context window and compact if needed
125
+ const currentTokens = estimateTokens(messages)
126
+ if (currentTokens > budget.compactAt) {
127
+ onText(`\n[Context compacted: ${currentTokens} tokens → reducing to fit window]\n`)
128
+ compactMessages(messages, pinned)
129
+ if (estimateTokens(messages) > budget.limit) {
130
+ // Report the POST-compaction count — the pre-compaction currentTokens
131
+ // is stale by the time compaction has run.
132
+ return renderTimeline(timeline, `Advisor: context window limit reached (${estimateTokens(messages)} tokens). Review incomplete — too many tool calls. Try a narrower scope.`)
133
+ }
134
+ }
135
+
136
+ // LLM generation silence: the reasoning phase produces no SSE bytes for
137
+ // seconds to tens of seconds (server-side prefill on large contexts, per
138
+ // tool-round LLM return). A placeholder keeps the panel visibly working.
139
+ // kind "think" (NOT "text"): the placeholder must land in the SAME buffer
140
+ // and position as the upcoming reasoning — a "text"-kind placeholder
141
+ // rendered BELOW the think block, and the reasoning stream appeared ABOVE
142
+ // it ("the stream runs back to the front"). Same buffer = same spot; the
143
+ // reasoning continues right where the placeholder sits.
144
+ onOutput?.({ kind: "think", text: ADVISOR_THINKING_PLACEHOLDER })
145
+
146
+ // 硬墙(§14.6 #1):单次请求信号 = 用户信号 × 本调用 deadline(remaining)。复合信号
147
+ // 无条件传入(上层检查与本调用之间的中止仍必须取消请求——已 aborted 的 composite 使请求
148
+ // 立即失败);此处改正了原指向 provider/core.mjs 组合 AbortSignal 的陈旧注释(§14.10 #3)。
149
+ const callSignal = signal
150
+ ? AbortSignal.any([signal, AbortSignal.timeout(remaining)])
151
+ : AbortSignal.timeout(remaining)
152
+ let response
153
+ try {
154
+ response = await chatCall(provider, {
155
+ messages,
156
+ tools: toolSchemas,
157
+ signal: callSignal,
158
+ onToken: (t) => { if (String(t ?? "").trim()) reviewTextProduced = true; onText(t) },
159
+ onReasoning: onThink,
160
+ // LOGGING(vscode advisor/run.mjs parity——按 stage 可 grep)
161
+ // §18.6 D-TR4:轨迹元数据增补——kind=advisor(评审独立于子代理——T-TR2);role
162
+ // 透出调用方角色(eng-coder 内嵌评审时为 "eng-coder");session/cwd 供轨迹对回;
163
+ // traces 开关沿 agent.config(D-TR6)。
164
+ logCtx: {
165
+ stage: "advisor",
166
+ role: agent?._role ?? null,
167
+ kind: "advisor",
168
+ session: agent?._sessionStart ?? null,
169
+ cwd,
170
+ traces: agent?.config?.traces?.enabled !== false,
171
+ },
172
+ })
173
+ } catch (e) {
174
+ // 墙判定绑信号状态(§14.6 #1——非异常名):① 用户信号已中止 ⇒ 原样上抛(中断语义
175
+ // 零变);② 复合信号已中止(墙触发)而用户信号未中止 ⇒ 结构化超时尾(形态①:抛错;
176
+ // AbortError / TimeoutError 两名兜底——AbortSignal.timeout 的 reason 是 TimeoutError
177
+ // DOMException);③ 其余错误原样上抛(runAdvisorReview 的失败分类不变)。
178
+ if (signal?.aborted) throw e
179
+ if (callSignal.aborted || e?.name === "AbortError" || e?.name === "TimeoutError") {
180
+ return renderTimeline(timeline, timeoutTail(timeoutMs, turns, toolCallCount, reviewTextProduced))
181
+ }
182
+ throw e
183
+ }
184
+ if (signal?.aborted) return renderTimeline(timeline, "Advisor: interrupted.")
185
+ // 形态②(§14.6 #1):不抛错而返回 partial(流已有内容时中断以 partial:true 透传)——
186
+ // 不得按普通结果收尾:墙触发(复合信号已中止)同判。
187
+ if (callSignal.aborted && response?.partial) {
188
+ return renderTimeline(timeline, timeoutTail(timeoutMs, turns, toolCallCount, reviewTextProduced))
189
+ }
190
+
191
+ // No tool calls — this is the final review text. The final answer was
192
+ // already streamed into the timeline via onText; fall back to
193
+ // response.content only if nothing was recorded.
194
+ if (!response.toolCalls?.length) {
195
+ if (!response.content?.trim()) return renderTimeline(timeline) || "Advisor: empty response — review was inconclusive"
196
+ return renderTimeline(timeline) || response.content.trim()
197
+ }
198
+
199
+ // Push assistant message with tool calls. reasoning_content ECHO is
200
+ // mandatory for reasoningEcho:"required" providers (deepseek/kimi): the
201
+ // server stops returning reasoning_content on later rounds when the
202
+ // tool-call assistant history lacks it — the observed "reasoning stops
203
+ // after the first tool call, returns only at the final answer" symptom.
204
+ // Mirrors the main agent's push (agent.mjs).
205
+ messages.push(assistantToolCallMessage(response, providerSpec(provider)))
206
+
207
+ // B1 (AGENT-LOOP.md §18.7 D-TS7): the SAME LLM reply's multiple read-only
208
+ // tool calls run in PARALLEL (Promise.all) — results are backfilled in
209
+ // toolCalls order (Promise.all preserves the input order → tool_call_id
210
+ // never mismatches); each tool's timeout/error is captured independently
211
+ // (the existing TOOL_TIMEOUT stays — one failing tool does not block the
212
+ // others); progress lines are emitted in toolCalls order. The read-only
213
+ // tool set has no side effects — no sequencing/serialization needed.
214
+ // Scope note (round1 review #10): B1 is ONLY in-loop tool parallelism — it
215
+ // does NOT solve the TODO "platform execution: advisor parallel calls are
216
+ // actually serial" mystery (docs/TODO.md — LOGGING evidence item), which
217
+ // concerns multiple advisor CALLS observed as serial, not one reply's
218
+ // tool calls.
219
+ const parsed = response.toolCalls.map((tc) => {
220
+ const tool = toolByName.get(tc.name)
221
+ let args = {}
222
+ let parseError = null
223
+ try {
224
+ args = JSON.parse(tc.arguments || "{}")
225
+ } catch (e) {
226
+ parseError = `Error: invalid JSON in tool arguments: ${e.message}\nRaw arguments: ${(tc.arguments || "").slice(0, 200)}`
227
+ }
228
+ return { tc, tool, args, parseError }
229
+ })
230
+ toolCallCount += parsed.length
231
+ // Progress lines first, in toolCalls order (emitted before the parallel
232
+ // run — display order is independent of completion order).
233
+ for (const p of parsed) {
234
+ if (p.parseError) continue // parse-error tools get no progress line (legacy behavior)
235
+ const argsLine = describeArgs(p.tc.name, p.args)
236
+ onTool(`\n→ ${p.tc.name}${argsLine ? " " + argsLine : ""}\n`)
237
+ }
238
+ // Every tool runs CONCURRENTLY; each result/error lands in its own slot —
239
+ // Promise.all preserves input order, so index i always matches parsed[i].
240
+ const executed = await Promise.all(parsed.map(async (p) => {
241
+ // Parse failure → error to model immediately (no execution)
242
+ if (p.parseError) return p.parseError
243
+ if (!p.tool) return `Error: unknown tool "${p.tc.name}". Available: ${[...toolByName.keys()].join(", ")}`
244
+ // Execute with timeout (clear the timer when the tool wins the race —
245
+ // otherwise up to MAX_ADVISOR_TURNS dangling timers accumulate)
246
+ try {
247
+ let timeoutId
248
+ const timeoutPromise = new Promise((_, reject) => {
249
+ timeoutId = setTimeout(() => reject(new Error(`tool timeout after ${TOOL_TIMEOUT_MS}ms`)), TOOL_TIMEOUT_MS)
250
+ })
251
+ let toolPromise
252
+ try {
253
+ toolPromise = p.tool.execute(p.args, { cwd, agent, onOutput, signal })
254
+ return await Promise.race([toolPromise, timeoutPromise])
255
+ } finally {
256
+ clearTimeout(timeoutId)
257
+ // Timeout won → toolPromise is still pending; a later rejection
258
+ // would surface as an unhandled rejection. The race already
259
+ // consumed the result/error in the normal path, so this no-op
260
+ // catch only fires for the abandoned-tool case.
261
+ toolPromise?.catch(() => {})
262
+ }
263
+ } catch (e) {
264
+ const errorType = e.message.includes("timeout") ? "timeout"
265
+ : e.message.includes("ENOENT") ? "file_not_found"
266
+ : e.message.includes("permission") ? "permission_denied"
267
+ : "execution_error"
268
+ return `Error (${errorType}): ${e.message}`
269
+ }
270
+ }))
271
+
272
+ // Backfill in toolCalls order (executed[i] ↔ parsed[i]); per-result
273
+ // non-string serialization + dual-end line-aware truncation stay per-tool
274
+ // (DUAL-END-TRUNCATION F-2 — truncate.mjs: head ≈60% + tail ≈40% — keep the
275
+ // tail verdicts; ≤ MAX_RESULT_CHARS results pass through untouched).
276
+ for (let i = 0; i < parsed.length; i++) {
277
+ let result = executed[i]
278
+ if (typeof result !== "string") result = JSON.stringify(result)
279
+
280
+ result = truncateAdvisorResult(result, MAX_RESULT_CHARS)
281
+
282
+ messages.push({ role: "tool", tool_call_id: parsed[i].tc.id, content: result })
283
+ }
284
+ }
285
+ }
286
+ // Test seam (T-TS8/9): the tool loop itself — toolsOverride injects a mock tool
287
+ // set with controllable timing/errors (the real set comes from advisorToolsFor).
288
+ export { runAdvisorToolLoop, runAdvisorToolLoop as _runAdvisorToolLoop }
@@ -0,0 +1,299 @@
1
+ /**
2
+ * advisor/messages.mjs — advisor user-message building (buildAdvisorUserMessage).
3
+ * Split out of advisor.mjs to keep it under the 300-line advisory threshold
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.
7
+ */
8
+ import { join, relative, sep } from "node:path"
9
+ import { findReviewRepos, collectRepoSnapshots, collectChangedFiles } from "./repos.mjs"
10
+ import { buildConvergenceBody, buildConvergenceInstructions } from "./convergence.mjs"
11
+ import { loadAdvisorMd, extractConversationBackground, extractAgentResponseTable } from "./history.mjs"
12
+ import { injectProjectGuide, injectDocumentMap, injectProjectStandards, NO_GIT_NOTICE } from "./project-context.mjs"
13
+
14
+ // Structural split (not an authority migration): the moved helpers stay reachable here.
15
+ export { findProjectRoot, injectProjectGuide } from "./project-context.mjs"
16
+
17
+ /**
18
+ * Build the mechanical review-object declaration block (AGENT-LOOP.md §18.8
19
+ * D-OA2 — English anchored form). Injected at the START of the review user
20
+ * message every round: round 1 (design + code), the legacy convergence path,
21
+ * and the round-2+ follow-up (see buildAdvisorFollowUp) — the reviewer must
22
+ * not re-derive "who is being reviewed / why" from the documents (T-OA2:
23
+ * every round stays anchored). Absent object → "" (legacy calls degrade to
24
+ * the current behavior — T-OA3).
25
+ * @param {Object|null} [object] — { type, target, status, reason, exclude }
26
+ * (strings; `exclude` may also be a list — joined with ", ")
27
+ * @returns {string} the declaration block (empty when no object)
28
+ */
29
+ export function buildObjectDeclarationBlock(object = null) {
30
+ if (!object || typeof object !== "object" || Array.isArray(object)) return ""
31
+ const field = (v) => (Array.isArray(v) ? v.join(", ") : v == null ? "" : String(v))
32
+ return [
33
+ "## Review-object declaration (mechanical — do not infer)",
34
+ `Review type: ${field(object.type)} | Target: ${field(object.target)} | Object state: ${field(object.status)} | Trigger: ${field(object.reason)}`,
35
+ `Excluded (not in this review): ${field(object.exclude)}`,
36
+ "Follow this declaration — do not infer the review target from the documents.",
37
+ "",
38
+ ].join("\n")
39
+ }
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
+
60
+ /**
61
+ * Build the user message for an advisor review session.
62
+ * @param {Object} agent — the parent agent
63
+ * @param {Object|null} [prior] — prior issue table
64
+ * @param {string} [reviewType] — "design" or "code" (已由工具层类型门定轨——此处无缺省;F30)
65
+ * @param {string|null} [designToken] — token injected into the design-review prompt; the advisor echoes it only on approval
66
+ * @param {string[]|null} [documents] — design review only: explicit list of doc paths to review (requirements + design + referenced docs).
67
+ * When set, the review input is built from this list ONLY — no git-diff change-set collection.
68
+ * When absent, the legacy git-diff-based scope is kept (backward compatible).
69
+ * @param {string[]|null} [paths] — code review only: explicit list of file/dir paths to review (deduped; shown under Review Scope)
70
+ * @param {Object|null} [object] — review-object declaration (§18.8 D-OA1/D-OA3):
71
+ * { type, target, status, reason, exclude } — mechanically injected at the
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).
76
+ * @returns {string} the user message
77
+ */
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) {
90
+ // prior = the full prior review output (string) when a convergence round is
91
+ // being built (decision 2026-08-08 — verbatim injection, model understands it).
92
+ // Deterministic: only _advisorRound > 0 with stored output counts.
93
+ const p = prior ?? ((agent._advisorRound || 0) > 0 ? agent._lastAdvisorOutput : null)
94
+
95
+ const parts = []
96
+ // Review-object declaration FIRST — D-OA1: at the start of the user message
97
+ // (after the system prompt, before the review content). Covers round 1
98
+ // design/code and the legacy convergence path; the round-2+ normal path
99
+ // prepends it in buildAdvisorFollowUp (T-OA2 — every round stays anchored).
100
+ const declaration = buildObjectDeclarationBlock(object)
101
+ if (declaration) parts.push(declaration)
102
+ const docList = Array.isArray(documents) ? documents.filter((d) => typeof d === "string" && d.trim()) : []
103
+ const pathList = Array.isArray(paths) ? [...new Set(paths.filter((p) => typeof p === "string" && p.trim()))] : []
104
+
105
+ // Project guide FIRST in EVERY review path (code AND design round 0): the map
106
+ // to the requirements docs is needed for design reviews too (design must fit
107
+ // the requirements, not just the methodology). Design round 0 early-returns
108
+ // below — the guide must be injected before that return. Project root is
109
+ // discovered from the review scope (code paths AND design-doc paths, so a
110
+ // documents-only design review still finds the subproject guide).
111
+ const guideRoot = injectProjectGuide(agent, parts, [...pathList, ...docList])
112
+
113
+ // Design review: simplified message — focus on the design doc, not code
114
+ if (reviewType === "design" && (agent._advisorRound || 0) === 0) {
115
+ const repos = findReviewRepos(agent)
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
+ }
122
+ if (docList.length > 0) {
123
+ // Explicit review scope (engineering mode, FR2): the caller hands over the
124
+ // doc list — the advisor reviews ONLY these. No git-diff change-set
125
+ // collection: diff-based discovery reviewed unrelated files, and untracked
126
+ // design docs were invisible to git diff anyway (ENGINEERING-MODE.md §2.4).
127
+ parts.push("The documents below are the review scope. Review ONLY these files — do not scan git diff or read any other files.")
128
+ parts.push("")
129
+ parts.push("## Documents to Review")
130
+ parts.push(docList.map((d) => `- ${d} — Read this file in full`).join("\n"))
131
+ parts.push("")
132
+ } else {
133
+ // Backward-compatible fallback (no documents): discover docs via git status/diff.
134
+ parts.push("The following changes are a design document. Review it against the project's methodology.")
135
+ parts.push("")
136
+
137
+ // List changed file paths explicitly — new design docs are untracked,
138
+ // so git diff HEAD won't show their content; the advisor must read the file itself
139
+ const changedFiles = collectChangedFiles(repos, agent.cwd)
140
+ if (changedFiles.length > 0) {
141
+ parts.push("## Changed Files")
142
+ parts.push(changedFiles.map((f) => `- ${f}`).join("\n"))
143
+ parts.push("")
144
+ parts.push("Read each changed file in full — untracked files are not shown in the diff below.")
145
+ parts.push("")
146
+ }
147
+
148
+ // Pre-collected changes — the design doc diff.
149
+ const snapshots = collectRepoSnapshots(repos, agent.cwd)
150
+ if (snapshots.length > 0) {
151
+ parts.push("## Design Document (git diff)")
152
+ parts.push(...snapshots)
153
+ parts.push("")
154
+ }
155
+ }
156
+
157
+ // Engineering mode: the project's DECLARED standards document
158
+ // (advisor.standardsDoc) — undeclared degrades visibly (PO-2).
159
+ if (agent.config?.agent?.engineering) {
160
+ injectProjectStandards(agent, parts, guideRoot)
161
+ }
162
+
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)
167
+
168
+ parts.push("## Instructions")
169
+ if (docList.length > 0) {
170
+ parts.push("1. Read every document in the Documents to Review list in full — review ONLY those files.")
171
+ } else {
172
+ parts.push("1. Read the design document fully.")
173
+ }
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?).")
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?")
176
+ parts.push("4. Do NOT run git diff or look for code changes — there are none at this stage.")
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.")
178
+ if (designToken) {
179
+ parts.push("")
180
+ parts.push(buildDesignApprovalBlock(designToken, designId))
181
+ }
182
+ return parts.join("\n")
183
+ }
184
+
185
+ // Convergence data (round 2+). LEGACY COMPATIBILITY PATH: the normal advisor
186
+ // flow routes convergence rounds through buildAdvisorFollowUp (fresh session,
187
+ // decision d698434); this block only fires for direct external callers of
188
+ // buildAdvisorUserMessage with a stored prior review output. Kept to avoid
189
+ // breaking those. Same rule as buildAdvisorFollowUp: the FULL prior review
190
+ // output is injected verbatim (decision 2026-08-08 — the model understands it;
191
+ // no table/header/phrase parsing).
192
+ // NOTE: this legacy path does NOT apply escapeLiteralEscapes (that lives in
193
+ // advisor.mjs and importing it here would create a top-level module cycle).
194
+ // Direct callers must escape the injected output themselves if the parent
195
+ // conversation can quote literal "\x"/"\u" sequences (server 400 risk).
196
+ if (p && (agent._advisorRound || 0) > 0) {
197
+ const scopeFiles = resolveScopeFiles(agent, paths)
198
+ const response = extractAgentResponseTable(agent.history)
199
+ || (scopeFiles?.length
200
+ ? "(Agent did not provide a response table — perform a fresh review of: " + scopeFiles.slice(0, 10).join(", ") + ")"
201
+ : "(Agent did not provide a response table — perform a fresh review of the files named in the system prompt context)")
202
+ const round = (agent._advisorRound || 0) + 1
203
+ parts.push(buildConvergenceBody(p, response, round, scopeFiles))
204
+ parts.push("")
205
+ parts.push("---")
206
+ parts.push("")
207
+ }
208
+
209
+ if (pathList.length > 0 || docList.length > 0) {
210
+ parts.push("## Review Scope")
211
+ }
212
+ if (pathList.length > 0) {
213
+ parts.push("Review these code files/directories — read them in full for context:")
214
+ parts.push("")
215
+ parts.push(pathList.map((p) => `- ${p}`).join("\n"))
216
+ parts.push("")
217
+ }
218
+ if (docList.length > 0) {
219
+ if (reviewType === "design") {
220
+ parts.push("The documents below are the review scope. Review ONLY these files — do NOT scan git diff or read any other files.")
221
+ } else {
222
+ parts.push("The documents below define acceptance criteria and review context. Read them for context, then read the code files specified in the review scope. Judge the implementation against these documents.")
223
+ }
224
+ parts.push("")
225
+ parts.push("## Documents to Review")
226
+ parts.push(docList.map((d) => `- ${d} — Read this file in full`).join("\n"))
227
+ parts.push("")
228
+ }
229
+
230
+ // Conversation background — recent user↔assistant exchanges for intent context
231
+ const background = extractConversationBackground(agent.history)
232
+ if (background) {
233
+ parts.push("## Conversation Background (recent turns)")
234
+ parts.push(background)
235
+ parts.push("")
236
+ }
237
+
238
+ // Review criteria
239
+ const criteria = loadAdvisorMd(agent.cwd)
240
+ parts.push("## Review Criteria")
241
+ parts.push(criteria)
242
+ if (guideRoot) {
243
+ // Requirement-fit is a first-class dimension when the project guide was
244
+ // found — the criteria file (advisor.md) may not mention it (legacy).
245
+ parts.push("")
246
+ parts.push("Additional criterion: **requirement fit** — does the implementation match what the requirements documents (referenced by the Project Guide above) actually ask for?")
247
+ }
248
+ parts.push("")
249
+
250
+ // Engineering mode: project standards (declaration-only, same helper as the
251
+ // design path) — undeclared degrades visibly (PO-2).
252
+ if (agent.config?.agent?.engineering) {
253
+ injectProjectStandards(agent, parts, guideRoot)
254
+ }
255
+
256
+ // Instructions — round-aware: re-reviews skip convention discovery entirely.
257
+ // These are SUPPLEMENTARY reminders to the system prompt's numbered workflow —
258
+ // deliberately not renumbered as a competing sequence.
259
+ const isReReview = p && (agent._advisorRound || 0) > 0
260
+ parts.push("## Instructions")
261
+ parts.push("1. IMPORTANT: the review scope lists the files under review — always verify current file state with `read` before judging. Never decide based on earlier snapshots alone.")
262
+ if (isReReview) {
263
+ const round = (agent._advisorRound || 0) + 1
264
+ parts.push(...buildConvergenceInstructions(round, pathList))
265
+ } else {
266
+ parts.push("2. " + (guideRoot
267
+ ? "The `## Project Guide (AGENTS.md)` section above maps the project — read the requirements/design documents it points to (they are the primary reference for requirement-fit). Use `read` to load those documents."
268
+ : "No AGENTS.md was found at the project root — rely on the conversation background for the user's requirements. If the requirements are unclear, state so explicitly."))
269
+ parts.push("3. `read` the files in the Review Scope in full — they define exactly what to inspect. Batch independent reads/greps in a single reply instead of one call per round-trip.")
270
+ parts.push("4. Use `grep` or `lsp` to trace callers, imports, and dependencies — only where the diff leaves genuine doubt.")
271
+ parts.push("5. Produce your review table based on the review criteria above. Do not re-read content you already have.")
272
+ parts.push("6. You may also flag other issues: crashes, data loss, logic errors — anything obvious. This is the convergence protocol: round 1 is the full review, later rounds only re-verify.")
273
+ }
274
+ parts.push("")
275
+ parts.push("Return your review as a markdown table (or a clear statement that everything is fine).")
276
+
277
+ return parts.join("\n")
278
+ }
279
+
280
+ /**
281
+ * Resolve the review surface for the convergence fallback: explicit `paths`
282
+ * win; otherwise the runtime mutation record (_touchedFiles, ABSOLUTE) is
283
+ * normalized to cwd-relative so the fallback list matches the relative-path
284
+ * norm the reviewer sees everywhere else. Paths outside cwd are relativized
285
+ * with path.relative — never a mixed absolute/relative list.
286
+ */
287
+ export function resolveScopeFiles(agent, paths) {
288
+ const normalize = (p) => {
289
+ // sep-guarded prefix check — /proj vs /project-other must not collide
290
+ const abs = p === agent.cwd || p.startsWith(agent.cwd + sep) ? p : join(agent.cwd, p)
291
+ return relative(agent.cwd, abs)
292
+ }
293
+ if (Array.isArray(paths)) return [...new Set(paths.map(normalize))]
294
+ if (agent._touchedFiles?.length) {
295
+ return [...new Set(agent._touchedFiles.map(normalize))]
296
+ }
297
+ return null
298
+ }
299
+