@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,190 @@
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
+
15
+ // 拆分后 import 面(既有导出名逐一保面——re-export;谓词为本批新增)。
16
+ export { ADVISOR_THINKING_PLACEHOLDER, MAX_RESULT_CHARS, renderTimeline as _renderTimeline } from "./compaction.mjs"
17
+ export { advisorToolsFor, advisorToolsFor as _advisorToolsFor } from "./loop.mjs"
18
+ export { runAdvisorToolLoop as _runAdvisorToolLoop } from "./loop.mjs"
19
+ export { advisorIncompleteMarker } from "./compaction.mjs"
20
+
21
+ /** B 启动拒绝前缀(§14.4 #2)——稳定契约单源(三消费点同串):run.mjs 生成;同步工具面
22
+ * 据此登记 `_advisorRefusals`;异步结算面据此不置 `_calledAdvisorThisRun`。 */
23
+ export const ADVISOR_LAUNCH_REFUSAL_PREFIX = "Advisor: design review launch refused"
24
+
25
+ /** Resolve the advisor's provider: cfg.provider/model when set, otherwise the main agent's provider */
26
+ export function resolveAdvisorProvider(agent) {
27
+ const cfg = agent.config?.advisor
28
+ if (cfg?.provider) {
29
+ try {
30
+ // F-1 (ISSUE-FIX-BATCH): children carry no agent.providers (spawn childConfig copies the
31
+ // parent config) — fall back to config.providersList (.length: [] must not skip the list).
32
+ const provider = findProvider(agent.providers?.length ? agent.providers : agent.config?.providersList ?? [agent.provider], cfg.provider)
33
+ // F-2a (MODEL-400-FIX):无 cfg.model 时渠道克隆须重派生 model——渠道裸克隆会丢 model 键
34
+ // → 无 model 请求 → serde 400。
35
+ // MODEL-SELECTION v2(M3④):无 cfg.model → 命中渠道自己的默认模型(`provider.model`
36
+ // 单值);渠道无默认模型 → 父 provider 兜底(与 subagent F-2c 同构)——绝不产出静默
37
+ // undefined-model 请求(最极端两者皆无 → chat 前 assertProviderModel fail-fast)。
38
+ const result = cfg.model ? { ...provider, model: cfg.model } : { ...provider, model: provider.model ?? agent.provider?.model }
39
+ if (cfg.thinking === null || cfg.thinking === false) result.thinking = undefined // explicitly off
40
+ else if (cfg.thinking !== undefined) result.thinking = cfg.thinking
41
+ if (cfg.reasoningEffort !== undefined) result.reasoningEffort = cfg.reasoningEffort
42
+ return result
43
+ } catch (e) {
44
+ // Provider not found or lookup failed — fall back to main provider, but surface the reason
45
+ console.warn(`[advisor] resolveAdvisorProvider: ${e.message}`)
46
+ }
47
+ }
48
+ const provider = { ...agent.provider }
49
+ if (cfg?.model) provider.model = cfg.model
50
+ // thinking off: null AND false both mean "explicitly off" — a raw `false`
51
+ // value is invalid for providers that expect undefined or an object.
52
+ if (cfg?.thinking === null || cfg?.thinking === false) provider.thinking = undefined
53
+ else if (cfg?.thinking !== undefined) provider.thinking = cfg.thinking
54
+ if (cfg?.reasoningEffort !== undefined) provider.reasoningEffort = cfg.reasoningEffort
55
+ return provider
56
+ }
57
+
58
+ /** Review-looking guard (async settle parity): a markdown table row or ≥200 chars of prose counts as a prior. */
59
+ export function looksLikeReviewOutput(text) {
60
+ const trimmed = String(text ?? "").trim()
61
+ return /\|.*\|.*\|/.test(trimmed) || trimmed.length >= 200
62
+ }
63
+
64
+ /**
65
+ * 压缩定锚简报(F13/§14.4 #3)——**由评审参数构建**(非模型输出):对象声明 / 文档清单 /
66
+ * Approval Signal 三锚;重内容(项目指南 / 方法论 / 文档地图)不入 pin(压缩的意义所在)。
67
+ * 形态逐字见设计 §14.4 #3(首行为机械化重挂说明)。
68
+ */
69
+ function buildPinnedBrief(reviewType, documents, object, designToken, designId) {
70
+ const docList = Array.isArray(documents) ? documents.filter((d) => typeof d === "string" && d.trim()) : []
71
+ const parts = ["[review brief — re-attached after context compaction; the original review request is no longer in the context]"]
72
+ const declaration = buildObjectDeclarationBlock(object)
73
+ if (declaration) parts.push(declaration.trimEnd())
74
+ if (docList.length > 0) {
75
+ parts.push("## Documents to Review")
76
+ parts.push(docList.map((d) => `- ${d} — Read this file in full`).join("\n"))
77
+ }
78
+ if (reviewType === "design" && designToken) parts.push(buildDesignApprovalBlock(designToken, designId))
79
+ return parts.join("\n\n")
80
+ }
81
+
82
+ /**
83
+ * Run an advisor review. reviewType: "code" or "design" (the top-level type is validated at the tool gate — F30; this entry point consumes the already-decided value). Returns review text or null when skipped.
84
+ * @param {string|null} [designToken] — injected into the design-review prompt; the advisor echoes it only on approval.
85
+ * @param {string[]|null} [documents] — design review only: explicit list of doc paths to review; passed through to the message builder.
86
+ * @param {string[]|null} [paths] — code review only: explicit list of file/dir paths to review.
87
+ * @param {Object|null} [object] — review-object declaration (§18.8 D-OA1/D-OA3): { type, target, status, reason, exclude }; absent → legacy behavior (no injection).
88
+ * @param {string|null} [designId] — §29.1 F2a: injected next to the design token in
89
+ * the Approval Signal (passed through to the message builders).
90
+ */
91
+ export async function runAdvisorReview(agent, reviewType, callbacks, designToken = null, documents = null, paths = null, object = null, designId = null) {
92
+ const onOutput = callbacks?.onOutput
93
+ const signal = callbacks?.signal
94
+ const startTime = Date.now()
95
+
96
+ // Advisor reviews are ALWAYS available (2026-08-21 semantic refactor): the
97
+ // former advisor.enabled gate is removed — review capability has no off
98
+ // switch; only the guard (completion pushback) is opt-in via advisor.guard.
99
+
100
+ // 撤 cap / 撤计数据(2026-09-18 用户裁定——ADVISOR-CONVERGENCE.md §3.1):本执行体**无任何按计数
101
+ // 拒发预检**(原 cap 内防线 + 同 doc-set 连败停止内防线整体退场;轮次仅作提示词衰减与显示)。
102
+ // 失败路径的出口 = 失败结论块(`advisor-settle.mjs` 结算出口,两轨共用——ADVISOR-GUARDS.md §7)。
103
+ const provider = resolveAdvisorProvider(agent)
104
+ // Advisor always works in the agent's cwd — scope is defined by paths/documents.
105
+ const advisorCwd = agent.cwd
106
+
107
+ const messages = prepareAdvisorMessages(agent, reviewType, designToken, documents, paths, null, object, designId)
108
+
109
+ // B 启动断言(fail-closed——§14.4 #2):设计评审请求内**必须**携带与本次签发 token 精确
110
+ // 对应的 Approval Signal——构建面补不上就拒绝启动(不发"请回显一个不存在的 token"的请求)。
111
+ // 拒绝报告前缀 `Advisor: design review launch refused` = 稳定契约(同步工具面据此登记
112
+ // _advisorRefusals;异步结算面据此不置 _calledAdvisorThisRun)。工具路径恒签发 token ⇒
113
+ // 该拒绝为直接调用方兜底(防御纵深——正常链不可达,如实注)。
114
+ if (reviewType === "design") {
115
+ const missing = !designToken
116
+ ? "no design token was minted"
117
+ : (messages.some((m) => m.role === "user" && String(m.content ?? "").includes(`[DESIGN-TOKEN:${designToken}`))
118
+ ? null
119
+ : "the request does not carry the approval signal")
120
+ if (missing) {
121
+ 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.`
122
+ }
123
+ }
124
+
125
+ // §2.20.3 批次档写通道的绑定(仅设计评审):同步路径 = 调用方(advisor 工具)传入的
126
+ // callbacks.batchDoc(即 resolved.run 的实例绑定);异步路径 = 本实例在跑池条目的
127
+ // run.batchDoc(同文档集实例键——各评审各取各档,不用单值会话态)。
128
+ const boundBatchDoc = reviewType === "design" ? batchDocForReview(agent, documents, callbacks) : null
129
+ // F13/§14.4 #3:压缩定锚简报(评审参数构建——压缩触发时由 compaction 重挂)。
130
+ const pinned = buildPinnedBrief(reviewType, documents, object, designToken, designId)
131
+
132
+ try {
133
+ const result = await runAdvisorToolLoop(provider, messages, onOutput, signal, agent, advisorCwd, null, reviewType, boundBatchDoc, pinned)
134
+
135
+ // Host-verified citations (decision d698434): mechanically check every
136
+ // `file:line: content` reference in the review against the CURRENT file
137
+ // state. LLMs cannot self-enforce the evidence rule — the model may quote
138
+ // the prior table instead of re-reading (three consecutive false reports
139
+ // cited pre-fix line content). Unverified citations must not support a
140
+ // push-back; the parent agent sees the verification report.
141
+ // F14/§14.5:解析候选 = cwd + 评审对象声明范围派生根(scope = documents + paths)。
142
+ let final = result
143
+ if (!result.trimStart().startsWith("Advisor:")) {
144
+ final = appendCitationReport(result, advisorCwd, { scope: [...(documents ?? []), ...(paths ?? [])] })
145
+ // Success path: keep the FULL review output for convergence rounds —
146
+ // round 2+ injects this verbatim and the model understands it (decision
147
+ // 2026-08-08: prior-table hard parsing removed; no phrase/header matching).
148
+ // Guard: only store outputs that actually carry a review — a markdown
149
+ // table row (`| a | b | c |`) or substantial prose (>200 chars). An
150
+ // empty or tool-progress-only reply must not become the "prior review"
151
+ // of round 2+.
152
+ const looksLikeReview = looksLikeReviewOutput(final)
153
+ if (looksLikeReview) {
154
+ agent._lastAdvisorOutput = final
155
+ }
156
+ }
157
+
158
+ // Log review statistics for observability
159
+ const elapsed = Math.round((Date.now() - startTime) / 1000)
160
+ const toolCallCount = messages.filter((m) => m.role === "tool").length
161
+ const tokensUsed = estimateTokens(messages)
162
+ onOutput?.({
163
+ kind: "text",
164
+ text: `\n[advisor] Review completed: ${elapsed}s, ${toolCallCount} tool calls, ~${Math.round(tokensUsed / 1000)}k tokens\n`,
165
+ })
166
+ return final
167
+ } catch (e) {
168
+ if (e.name === "AbortError" && signal?.reason?.interrupt) throw e
169
+
170
+ // 细化错误类型
171
+ const errorType = e.message.includes("rate limit") || e.message.includes("429") ? "rate limit"
172
+ : e.message.includes("timeout") ? "timeout"
173
+ : e.message.includes("network") || e.message.includes("ECONNREFUSED") ? "network"
174
+ : e.message.includes("context length") ? "context_too_long"
175
+ : "unknown"
176
+
177
+ const retryAdvice = errorType === "rate limit"
178
+ ? "Wait a moment and retry. Consider using a cheaper model for advisor."
179
+ : errorType === "timeout"
180
+ ? "The model took too long. Try with a narrower scope."
181
+ : errorType === "context_too_long"
182
+ ? "Reduce the scope (fewer files/paths) or use a model with larger context window."
183
+ : "You may retry or proceed to verify manually."
184
+
185
+ return `Advisor: review failed (${errorType}) — ${e.message || "unknown error"}. ${retryAdvice}`
186
+ }
187
+ }
188
+ // Host-verified citations — moved to citations.mjs (kept re-exported here for
189
+ // import compatibility: tests and callers import from run.mjs).
190
+ export { extractCitations, verifyCitations, appendCitationReport } from "./citations.mjs"
@@ -0,0 +1,57 @@
1
+ /**
2
+ * truncate.mjs — advisor 工具结果的行感知双端截断(DUAL-END-TRUNCATION F-2,
3
+ * thincoder-cli/docs/design/_archive/DUAL-END-TRUNCATION.md,2026-09-09)。
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/advisor.mjs ADDED
@@ -0,0 +1,281 @@
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. Rounds have NO
31
+ * mechanical cap (2026-09-18 ruling — ADVISOR-CONVERGENCE.md §3.1): the round
32
+ * counter only drives prompt decay and display; failure paths end in a
33
+ * settlement conclusion block, and repeated failures are reported to the user.
34
+ * Rounds 2+ also declare all earlier diffs STALE and require read-verified
35
+ * file:line evidence for any unfixed/new finding — see docs/design/ADVISOR-CONVERGENCE.md.
36
+ *
37
+ * Session memory (agent._advisorSession):
38
+ * RETAINED for initialization compatibility but NEVER read (decision d698434):
39
+ * every review round builds a fresh [system, user] session — round 2+ must not
40
+ * reuse round 1's messages, because the old read outputs are the anchoring
41
+ * source of re-review false reports and a token sink. Convergence data (prior
42
+ * issue table + agent response table) travels via buildAdvisorFollowUp.
43
+ * The field is reset by runAgent; the write sites are harmless leftovers.
44
+ *
45
+ * Project customisation: .thincoder/advisor.md in the project root.
46
+ */
47
+ import { extractAgentResponseTable } from "./advisor/history.mjs"
48
+ import { buildAdvisorUserMessage, resolveScopeFiles, buildObjectDeclarationBlock, buildDesignApprovalBlock } from "./advisor/messages.mjs"
49
+ import { buildConvergenceBody } from "./advisor/convergence.mjs"
50
+ import { escapeLiteralEscapes } from "./escape.mjs"
51
+ import { applyPromptInjections, loadAdvisorPrompt } from "./prompt-files.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
+ // ────────────────────────────────────────
58
+ // Prompt files — loaded at module init
59
+ // ────────────────────────────────────────
60
+ // U2 核内笔(CORE-UNIFICATION §2.6.3(六)):私持 `loadPrompt`/自持路径运算已删——四常量
61
+ // 改经核内单一解析面 `loadAdvisorPrompt`(契约 8 / D-C13);缺档语义不变 = 抛错(契约 9)。
62
+
63
+ const ADVISOR_ROUND1 = loadAdvisorPrompt("advisor-round1.md")
64
+ // ROUND2/3 are used whenever a convergence round (round 2+) is being built:
65
+ // in-run session continuation replaces the system prompt with them, and a
66
+ // rebuilt fresh session (e.g. after a failed review) also selects them via
67
+ // buildAdvisorSystemPrompt when _advisorRound > 0.
68
+ const ADVISOR_ROUND2 = loadAdvisorPrompt("advisor-round2.md")
69
+ const ADVISOR_ROUND3 = loadAdvisorPrompt("advisor-round3.md")
70
+ // Design-review prompt — hard-loaded like the round prompts (decision
71
+ // 2026-08-21): a missing file means a broken installation, and silently
72
+ // degrading to a lesser in-code prompt would quietly strip the approval-signal
73
+ // and citation rules, disabling design approval entirely. loadAdvisorPrompt throws.
74
+ const ADVISOR_DESIGN = loadAdvisorPrompt("advisor-design.md")
75
+
76
+ // ────────────────────────────────────────
77
+ // System prompt building
78
+ // ────────────────────────────────────────
79
+
80
+ /**
81
+ /** Build the system prompt for an advisor review session.
82
+ * @param {Object} agent — the parent agent
83
+ * @param {Object|null} [prior] — prior review output (full text; decision 2026-08-08)
84
+ * @param {string} [reviewType] — "design" for design review, undefined/"code" for code review
85
+ * @returns {string} the system prompt
86
+ */
87
+
88
+ // 锚替换缝(CORE-UNIFICATION §2.13.8——U0):本函数**六个 return 逐条**经 `applyPromptInjections`
89
+ // (未配置 = 恒等,现行行为零变);读取径(模块级常量缓存)不动。结构机检
90
+ // (`test/prompt-injections.test.mjs`)按 return 分支逐条守——新增分支未挂即红。
91
+ export function buildAdvisorSystemPrompt(agent, prior, reviewType) {
92
+ // Round decision is DETERMINISTIC (decision 2026-08-08): _advisorRound > 0
93
+ // with a stored review output means convergence (round 2+); 0 means round 1.
94
+ // No prior-table parsing, no all-clear phrase matching — the round counter
95
+ // and the stored output are the only inputs. A restarted process has
96
+ // _advisorRound 0 → conservative full re-review.
97
+ const hasPrior = (agent._advisorRound || 0) > 0 && (prior ?? agent._lastAdvisorOutput)
98
+ // Design review: round 1 uses the dedicated design-review prompt (full scope +
99
+ // approval token); rounds 2+ converge like code reviews (verify agent fix claims).
100
+ if (reviewType === "design") {
101
+ if (!hasPrior) {
102
+ return applyPromptInjections(ADVISOR_DESIGN)
103
+ }
104
+ const round = (agent._advisorRound || 0) + 1
105
+ if (round === 2) return applyPromptInjections(ADVISOR_ROUND2)
106
+ return applyPromptInjections(ADVISOR_ROUND3)
107
+ }
108
+ if (!hasPrior) return applyPromptInjections(ADVISOR_ROUND1)
109
+ const round = (agent._advisorRound || 0) + 1
110
+ if (round === 2) return applyPromptInjections(ADVISOR_ROUND2)
111
+ return applyPromptInjections(ADVISOR_ROUND3)
112
+ }
113
+
114
+ // ────────────────────────────────────────
115
+ // Follow-up building (round 2+)
116
+ // ────────────────────────────────────────
117
+
118
+ /**
119
+ * Build a follow-up user message for round 2+ — the agent's response table +
120
+ * round-aware instructions, without re-sending the full round-1 context.
121
+ * Deliberately NO git information injected (no diff snapshot, no git context):
122
+ * git output misled re-reviews — committed fixes never show in `git diff HEAD`,
123
+ * so the model read "no changes" as "no fixes". Verification is `read`-only.
124
+ * NOTE: the caller (prepareAdvisorMessages) applies escapeLiteralEscapes to
125
+ * the return value — direct callers must do the same (the prior table and
126
+ * agent response can quote literal "\x"/"\u" sequences).
127
+ * @param {Object} agent — the parent agent (history used for the response table)
128
+ * @param {Object|null} prior — prior issue table (extracted from history when null)
129
+ * @param {string[]|null} [scopeFiles] — review surface for the no-response fallback (cwd-relative)
130
+ * @param {Object|null} [object] — review-object declaration (§18.8): mechanically
131
+ * prepended to the round-2+ follow-up so every round stays anchored (T-OA2).
132
+ * @returns {string} the follow-up user message — or a plain "System reminder: …"
133
+ * fresh-review fallback (NO brackets — some OpenAI-compatible servers parse
134
+ * '['-prefixed content as structured data / expand escapes) when no prior
135
+ * review exists at all (caller misuse; the response-table extraction would
136
+ * otherwise scan history from index 0 and could match an unrelated stale table)
137
+ */
138
+ export function buildAdvisorFollowUp(agent, prior, scopeFiles = null, object = null) {
139
+ // Convergence follow-up REQUIRES a prior review record — the full output of
140
+ // the last review, injected VERBATIM (decision 2026-08-08: the model
141
+ // understands the review output; no table/header/phrase parsing). The caller
142
+ // usually passes it; fall back to the stored agent._lastAdvisorOutput.
143
+ const p = prior ?? agent._lastAdvisorOutput
144
+ if (!p) {
145
+ // Plain "System reminder:" prefix (no brackets) — same convention as the
146
+ // round-1 path (some OpenAI-compatible servers parse '['-prefixed content
147
+ // as structured data / expand escapes; see prepareAdvisorMessages).
148
+ return "System reminder: convergence follow-up requested without a prior review — perform a fresh full review."
149
+ }
150
+ // Convergence semantics require round >= 2 (round 1 is the full review, not
151
+ // verification). A direct caller with _advisorRound 0 would otherwise get a
152
+ // meaningless "Round 1 — Strict Verification".
153
+ if ((agent._advisorRound || 0) < 1) {
154
+ return "System reminder: convergence follow-up requested at round 1 — a full review is already in progress; no prior verification exists yet."
155
+ }
156
+ const noResponseFallback = scopeFiles?.length
157
+ ? "(Agent did not provide a response table — perform a fresh review of: " + scopeFiles.slice(0, 10).join(", ") + ")"
158
+ : "(Agent did not provide a response table — perform a fresh full review; the review surface is unknown, ask the user for the file list)"
159
+ const response = extractAgentResponseTable(agent.history) || noResponseFallback
160
+ const round = (agent._advisorRound || 0) + 1
161
+ // Review-object declaration FIRST (T-OA2 — round 2+ stays anchored, no re-archaeology).
162
+ const declaration = buildObjectDeclarationBlock(object)
163
+ return (declaration ? declaration + "\n" : "") + buildConvergenceBody(p, response, round, scopeFiles)
164
+ }
165
+
166
+ /**
167
+ * Resolve the review surface for the convergence fallback — moved to
168
+ * messages.mjs so the legacy path shares it (see there).
169
+ */
170
+
171
+ // escapeLiteralEscapes 已抽到 ./escape.mjs(advisor 与主 agent 发送路径共用),
172
+ // 这里 re-export 保持既有 import 稳定。
173
+ export { escapeLiteralEscapes }
174
+
175
+
176
+ /**
177
+ * Build the advisor conversation for this run.
178
+ * EVERY call builds a fresh [system, user] session (decision d698434) — no
179
+ * session reuse across rounds: round 1 = full scope (ROUND1 prompt), rounds
180
+ * 2+ = convergence (ROUND2/ROUND3 prompt + fix-claims follow-up).
181
+ * @param {Object} agent — the parent agent
182
+ * @param {string} [reviewType] — "design" or "code" (已由工具层类型门定轨——此处无缺省;F30)
183
+ * @param {string|null} [designToken] — design-review approval token (design only)
184
+ * @param {string[]|null} [documents] — design review only: explicit list of doc paths to review (passed through to buildAdvisorUserMessage)
185
+ * @param {string[]|null} [paths] — code review only: explicit list of file/dir paths to review
186
+ * @param {Object|null} [object] — review-object declaration (§18.8 D-OA1): passed through
187
+ * to the user-message builders; mechanically injected at the start of every review
188
+ * round (round 1 design/code + round 2+ follow-up). Absent → legacy behavior.
189
+ * @param {string|null} [designId] — §29.1 F2a: injected next to the design token in
190
+ * the Approval Signal (round 1 + round 2+ — both values, verbatim anchor).
191
+ */
192
+ export function prepareAdvisorMessages(agent, reviewType, designToken = null, documents = null, paths = null, priorParam = null, object = null, designId = null) {
193
+ // Deterministic convergence state (decision 2026-08-08): round 2+ requires
194
+ // _advisorRound > 0 AND a stored prior review output. No history parsing.
195
+ // priorParam (direct callers) wins over the stored output — same derivation
196
+ // as buildAdvisorSystemPrompt (single source of truth for round semantics).
197
+ const prior = (agent._advisorRound || 0) > 0 ? (priorParam ?? agent._lastAdvisorOutput) : null
198
+
199
+ // Design review round 1: the dedicated full-scope review with the approval
200
+ // token (an independent gate — it runs even when a prior review exists, e.g.
201
+ // after a failed design review). Fresh session.
202
+ if (reviewType === "design" && (agent._advisorRound || 0) === 0) {
203
+ return [
204
+ { role: "system", content: buildAdvisorSystemPrompt(agent, prior, reviewType) },
205
+ { role: "user", content: escapeLiteralEscapes(buildAdvisorUserMessage(agent, prior, reviewType, designToken, documents, paths, object, designId)) },
206
+ ]
207
+ }
208
+
209
+ // Every round is a FRESH session (decision d698434): round 2+ must NOT reuse
210
+ // round 1's messages — the old read outputs are the top anchoring source of
211
+ // re-review false reports (the model quoted pre-fix file content instead of
212
+ // re-reading) and a token sink. The agent response table (fix claims) is
213
+ // injected through buildAdvisorFollowUp instead; the system prompt carries
214
+ // the round (ROUND2/ROUND3) via buildAdvisorSystemPrompt.
215
+ // No prior review output: reset ONLY when this run made no code changes (user
216
+ // decision 2026-08-05: any loop that modified code must NOT reset — the
217
+ // advisor guard WILL push back, so the convergence round must keep advancing
218
+ // through retries (round = prompt decay + display only; no mechanical cap —
219
+ // ADVISOR-CONVERGENCE.md §3.1); a run with no mutations has no push-back risk
220
+ // and a reset is safe). Deterministic runtime state (`_mutatedThisRun`) decides — never
221
+ // model output (phrases/table headers drift; three rounds of false reports
222
+ // proved it). Either way the message is a fresh full review (no prior output
223
+ // exists without a completed review) — only the round counter differs.
224
+ if (!prior || (agent._advisorRound || 0) === 0) {
225
+ if (!(agent._mutatedThisRun ?? false)) {
226
+ // New review cycle (first review, all-clear, or no code changes): reset
227
+ // the round so the cycle starts from ROUND1 (round = prompt decay/display).
228
+ agent._advisorRound = 0
229
+ }
230
+ // Mutations exist → KEEP the round (attempts keep advancing through retries).
231
+ const user = buildAdvisorUserMessage(agent, prior, reviewType, designToken, documents, paths, object, designId)
232
+ return [
233
+ { role: "system", content: buildAdvisorSystemPrompt(agent, prior, reviewType) },
234
+ {
235
+ role: "user",
236
+ // NOTE (2026-08-06): the leading prefix is a PLAIN "System reminder:",
237
+ // NOT "[System reminder: ...]" — some OpenAI-compatible servers try to
238
+ // parse content that STARTS with '[' as structured content (or expand
239
+ // escape sequences in it). A literal "\x" inside the conversation
240
+ // background (e.g. the parent agent quoting escape sequences) then
241
+ // fails server-side as "unexpected end of hex escape" → 400. Plain
242
+ // prefix keeps the review message a plain string everywhere.
243
+ // The whole content also passes through escapeLiteralEscapes (below)
244
+ // so literal "\x"/"\u" quoted by the parent agent can never form an
245
+ // invalid escape when the server expands them.
246
+ 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}`),
247
+ },
248
+ ]
249
+ }
250
+
251
+ // Convergence rounds (2+): fresh [system(ROUND2/3), user(prior table + fix
252
+ // claims)]. buildAdvisorFollowUp carries BOTH the prior issue table (the
253
+ // only complete verification list — decision 2026-08-05, reversed) and the
254
+ // agent's fix-claim table (focus reference). buildAdvisorSystemPrompt
255
+ // selects ROUND2 for round 2, ROUND3 for rounds 3+ — a failed review retry
256
+ // keeps _advisorRound so the convergence prompt matches the attempt count.
257
+ // scopeFiles gives the fallback (agent gave no response table) a concrete
258
+ // review surface.
259
+ const scopeFiles = resolveScopeFiles(agent, paths)
260
+ const followUp = buildAdvisorFollowUp(agent, prior, scopeFiles, object)
261
+ // §11.2 D-24b (design round 2+ — async fix-round continuations must be able to
262
+ // re-approve): re-anchor the review scope (the convergence follow-up carries no
263
+ // document list) and inject the round's design token with the approval signal.
264
+ if (reviewType === "design") {
265
+ const docList = Array.isArray(documents)
266
+ ? documents.filter((d) => typeof d === "string" && d.trim())
267
+ : []
268
+ const scopeBlock = docList.length > 0
269
+ ? `\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")}`
270
+ : ""
271
+ const tokenBlock = designToken ? `\n\n${buildDesignApprovalBlock(designToken, designId)}` : ""
272
+ return [
273
+ { role: "system", content: buildAdvisorSystemPrompt(agent, prior, reviewType) },
274
+ { role: "user", content: escapeLiteralEscapes(followUp + scopeBlock + tokenBlock) },
275
+ ]
276
+ }
277
+ return [
278
+ { role: "system", content: buildAdvisorSystemPrompt(agent, prior, reviewType) },
279
+ { role: "user", content: escapeLiteralEscapes(followUp) },
280
+ ]
281
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * child-marks.mjs — 子代理报告文本锚点(机器检出子串——单一权威源)。
3
+ *
4
+ * 收编自 `spawn-child.mjs`(2026-09-20 端差·显示面消差批 §2.2 X6 实现轮):端壳侧需**静态**
5
+ * 导入锚点做块头注记判定,而 `spawn-child.mjs` 静态链经核 agent 栈可达 `node:sqlite`
6
+ * ⇒ 端壳静态闭包禁达(W8 契约②;`thincoder-vscode/test/engine-floor-guard.test.mjs`
7
+ * fail-closed 红)⇒ 锚点下沉**零依赖叶**(先例 = `relay-prefix.mjs` 同因下沉)。`spawn-child.mjs`
8
+ * 原样再导出 ⇒ 既有 import 面(核 agent-tools 族 / CLI `tool-events.mjs`)零改。
9
+ *
10
+ * 语义:消费端用 `includes()` 检出锚点 ⇒ 合成块头注记("work may be partial")——两端各一处:
11
+ * CLI = `thincoder-cli/src/tui/tool-events.mjs:217`;端壳 = `thincoder-vscode/src/extension/
12
+ * panel-callbacks.mjs`(onToolResult)。文案演进只改这里(与检测判据单源——消除文案/判据漂移面)。
13
+ *
14
+ * 零 import——任意层可引、无环。
15
+ */
16
+
17
+ /** turn-cap 降级文案的公共锚点:subagent/escalate 的 onDeclined 文案必含此子串
18
+ * (`subagent-async.mjs` 折叠 partial / `escalate-async.mjs` 同族)。 */
19
+ export const TURN_CAP_MARK = "stopped: turn cap reached"
20
+
21
+ /** SYNC-CANCEL(L52):sync spawn ⏹ 定向中止折叠报告的公共锚点(`buildSyncStoppedReport`);
22
+ * 消费端用 `includes()` 检测"用户定向中止——工作可能不完整"语义(块冻结标 stopped 而非
23
+ * done——R6)。与 TURN_CAP_MARK 同族单源纪律。 */
24
+ export const STOPPED_MARK = "stopped by user"