thincoder 0.12.60 → 0.12.61

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/README.md +8 -6
  3. package/bin/thincoder.mjs +27 -122
  4. package/package.json +3 -2
  5. package/src/abort-provenance.mjs +116 -0
  6. package/src/acp/bridge.mjs +38 -17
  7. package/src/acp.mjs +6 -1
  8. package/src/advisor/citations.mjs +83 -21
  9. package/src/advisor/compaction.mjs +174 -0
  10. package/src/advisor/loop.mjs +293 -0
  11. package/src/advisor/messages.mjs +36 -134
  12. package/src/advisor/project-context.mjs +194 -0
  13. package/src/advisor/repos.mjs +17 -40
  14. package/src/advisor/run.mjs +124 -329
  15. package/src/advisor/truncate.mjs +57 -0
  16. package/src/advisor.mjs +3 -2
  17. package/src/agent/completion.mjs +1 -1
  18. package/src/agent/dispatch.mjs +47 -12
  19. package/src/agent/helpers.mjs +71 -13
  20. package/src/agent/record-results.mjs +13 -5
  21. package/src/agent/relay-prefix.mjs +39 -0
  22. package/src/agent/run-stages.mjs +21 -6
  23. package/src/agent/setup-reminders.mjs +16 -9
  24. package/src/agent/setup.mjs +92 -128
  25. package/src/agent/spawn-child.mjs +26 -9
  26. package/src/agent-tools/advisor-async.mjs +70 -180
  27. package/src/agent-tools/advisor-settle.mjs +231 -0
  28. package/src/agent-tools/advisor.mjs +69 -20
  29. package/src/agent-tools/batch-segment.mjs +195 -0
  30. package/src/agent-tools/consult.mjs +23 -10
  31. package/src/agent-tools/design-token.mjs +14 -1
  32. package/src/agent-tools/digest-budget.mjs +76 -0
  33. package/src/agent-tools/eng.mjs +3 -3
  34. package/src/agent-tools/escalate-async.mjs +16 -13
  35. package/src/agent-tools/read-history.mjs +13 -3
  36. package/src/agent-tools/review-streak.mjs +93 -0
  37. package/src/agent-tools/settings.mjs +130 -17
  38. package/src/agent-tools/subagent-actions.mjs +15 -6
  39. package/src/agent-tools/subagent-async.mjs +66 -14
  40. package/src/agent-tools/subagent-panel.mjs +22 -15
  41. package/src/agent-tools/subagent-run.mjs +9 -6
  42. package/src/agent-tools/subagent-scheduler.mjs +57 -8
  43. package/src/agent-tools/subagent-spawn.mjs +63 -16
  44. package/src/agent-tools/subagent.mjs +175 -49
  45. package/src/agent-tools/verify.mjs +13 -34
  46. package/src/agent-tools.mjs +1 -0
  47. package/src/agent.mjs +38 -21
  48. package/src/cli/distill-command.mjs +2 -2
  49. package/src/cli/make-agent.mjs +23 -7
  50. package/src/cli/memory-command.mjs +2 -2
  51. package/src/cli/setup-wizard.mjs +29 -9
  52. package/src/completions.mjs +114 -0
  53. package/src/config-migrate.mjs +70 -0
  54. package/src/config.mjs +132 -63
  55. package/src/conventions.mjs +223 -0
  56. package/src/crash-reports.mjs +7 -2
  57. package/src/expand-home.mjs +16 -0
  58. package/src/generate-title.mjs +1 -1
  59. package/src/hooks.mjs +7 -3
  60. package/src/memory/code-index.mjs +9 -3
  61. package/src/memory/code-sync.mjs +70 -31
  62. package/src/memory/delete.mjs +2 -0
  63. package/src/memory/docs.mjs +10 -6
  64. package/src/memory/file-walk.mjs +109 -0
  65. package/src/memory/schema.mjs +15 -3
  66. package/src/model-ref.mjs +66 -0
  67. package/src/model-specs.mjs +42 -8
  68. package/src/prompt-overlays.mjs +73 -16
  69. package/src/prompts/advisor-design.md +18 -8
  70. package/src/prompts/advisor-round1.md +7 -1
  71. package/src/prompts/advisor-round2.md +13 -2
  72. package/src/prompts/advisor-round3.md +13 -2
  73. package/src/prompts/common.md +115 -0
  74. package/src/prompts/consult-base.md +2 -0
  75. package/src/prompts/discipline-engineering.md +217 -0
  76. package/src/prompts/discipline-normal.md +179 -0
  77. package/src/prompts/persona-coder.md +21 -0
  78. package/src/prompts/persona-eng-coder.md +37 -0
  79. package/src/prompts/persona-eng-designer.md +55 -0
  80. package/src/prompts/persona-engineering.md +54 -0
  81. package/src/prompts/persona-explore.md +15 -0
  82. package/src/prompts/persona-normal.md +27 -0
  83. package/src/prompts/persona-plan.md +26 -0
  84. package/src/provider/anthropic.mjs +4 -4
  85. package/src/provider/core.mjs +13 -32
  86. package/src/provider/errors.mjs +26 -1
  87. package/src/provider/google.mjs +5 -6
  88. package/src/provider/index.mjs +2 -1
  89. package/src/provider/list-models.mjs +93 -0
  90. package/src/provider/rate.mjs +2 -1
  91. package/src/provider/responses.mjs +5 -3
  92. package/src/provider/sse.mjs +3 -4
  93. package/src/proxy.mjs +9 -14
  94. package/src/session-slots.mjs +7 -2
  95. package/src/session.mjs +59 -38
  96. package/src/token-ttl.mjs +2 -1
  97. package/src/tools/{system.mjs → bash.mjs} +6 -243
  98. package/src/tools/file.mjs +30 -10
  99. package/src/tools/git.md +1 -1
  100. package/src/tools/git.mjs +15 -34
  101. package/src/tools/index.mjs +4 -2
  102. package/src/tools/ops.mjs +20 -7
  103. package/src/tools/question.md +1 -0
  104. package/src/tools/question.mjs +26 -0
  105. package/src/tools/read.md +1 -1
  106. package/src/tools/read_image.md +1 -1
  107. package/src/tools/search.mjs +236 -0
  108. package/src/tui/agent-turn.mjs +32 -13
  109. package/src/tui/ansi.mjs +2 -0
  110. package/src/tui/clipboard.mjs +7 -1
  111. package/src/tui/cmd-advisor.mjs +3 -2
  112. package/src/tui/cmd-config.mjs +108 -37
  113. package/src/tui/cmd-eng.mjs +11 -27
  114. package/src/tui/cmd-exit.mjs +6 -8
  115. package/src/tui/cmd-model.mjs +14 -12
  116. package/src/tui/cmd-reindex.mjs +7 -0
  117. package/src/tui/cmd-submodel.mjs +8 -5
  118. package/src/tui/cmd-undo.mjs +4 -3
  119. package/src/tui/index.mjs +38 -37
  120. package/src/tui/key-handler.mjs +61 -17
  121. package/src/tui/key-modes.mjs +86 -8
  122. package/src/tui/layout.mjs +18 -10
  123. package/src/tui/model-catalog.mjs +89 -0
  124. package/src/tui/model-picker.mjs +498 -0
  125. package/src/tui/mouse.mjs +47 -10
  126. package/src/tui/pickers.mjs +28 -410
  127. package/src/tui/render-frame.mjs +32 -16
  128. package/src/tui/render-loop.mjs +2 -0
  129. package/src/tui/render-segments.mjs +5 -19
  130. package/src/tui/render.mjs +37 -5
  131. package/src/tui/slash-commands.mjs +2 -2
  132. package/src/tui/subagent-blocks.mjs +69 -90
  133. package/src/tui/subagent-children.mjs +50 -64
  134. package/src/tui/subagent-freeze.mjs +40 -43
  135. package/src/tui/subagent-panel.mjs +21 -66
  136. package/src/tui/suspension-drive.mjs +28 -82
  137. package/src/tui/tool-args.mjs +4 -2
  138. package/src/tui/tool-display.mjs +4 -3
  139. package/src/tui/tool-events.mjs +23 -16
  140. package/src/tui/tui-lifecycle.mjs +18 -5
  141. package/src/tui/wizard.mjs +52 -18
  142. package/src/tui/wrapped-spawn.mjs +38 -0
  143. package/src/prompts/coder.md +0 -13
  144. package/src/prompts/discipline.md +0 -84
  145. package/src/prompts/eng-coder.md +0 -19
  146. package/src/prompts/engineering-sub.md +0 -14
  147. package/src/prompts/engineering.md +0 -87
  148. package/src/prompts/explore.md +0 -12
  149. package/src/prompts/main.md +0 -34
  150. package/src/prompts/methodology-template.md +0 -38
  151. package/src/prompts/plan.md +0 -9
  152. package/src/prompts/system.md +0 -44
@@ -1,351 +1,95 @@
1
1
  /**
2
- * advisor/run.mjs — advisor execution: tool loop, provider resolution, and the review entry point.
3
- * Message building lives in advisor.mjs.
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 保持不变)。
4
6
  */
5
- import { chat } from "../provider/core.mjs"
6
- import { findProvider, providerSpec } from "../config.mjs"
7
- import { toOpenAISchema } from "../tools/index.mjs"
7
+ import { findProvider } from "../config.mjs"
8
8
  import { prepareAdvisorMessages } from "../advisor.mjs"
9
+ import { buildObjectDeclarationBlock, buildDesignApprovalBlock } from "./messages.mjs"
9
10
  import { appendCitationReport } from "./citations.mjs"
10
- import { describeToolArgs } from "../tui/tool-args.mjs"
11
+ import { runAdvisorToolLoop } from "./loop.mjs"
12
+ import { advisorIncompleteMarker, estimateTokens } from "./compaction.mjs"
13
+ import { batchDocForReview } from "../agent-tools/batch-segment.mjs"
14
+ // 第 33 批(§17.5 模块图):护栏常量 / doc-set 键 / 记录读取——单向导入(review-streak 无回指)。
15
+ import {
16
+ MAX_DESIGN_REVIEW_STREAK, docSetKey, designReviewStreakRecord, designReviewStreakStopped,
17
+ } from "../agent-tools/review-streak.mjs"
18
+
19
+ // 拆分后 import 面(既有导出名逐一保面——re-export;谓词为本批新增)。
20
+ export { ADVISOR_THINKING_PLACEHOLDER, MAX_RESULT_CHARS, renderTimeline as _renderTimeline } from "./compaction.mjs"
21
+ export { advisorToolsFor, advisorToolsFor as _advisorToolsFor } from "./loop.mjs"
22
+ export { runAdvisorToolLoop as _runAdvisorToolLoop } from "./loop.mjs"
23
+ export { advisorIncompleteMarker } from "./compaction.mjs"
11
24
 
12
- const MAX_ADVISOR_TURNS = 100
13
25
  // Mechanical convergence cap: up to 5 rounds suffice; a 6th call means the model
14
- // is looping — refuse it instead of burning tokens. §24 D-24b (2026-09-06): PER
26
+ // is looping — refuse it instead of burning tokens. §11.2 D-24b (2026-09-06): PER
15
27
  // REVIEW INSTANCE (agent._advisorRuns); CODE REVIEWS ONLY (2026-09-07 §8 ruling)
16
28
  // — design reviews are EXEMPT: their rounds keep advancing, the cap never refuses.
17
29
  export const MAX_ADVISOR_ROUNDS = 5
18
30
 
19
- // NOTE: prompts/advisor-round{1,2,3}.md encourage the model to finish within
20
- // ~30 tool turns — a prompt-level efficiency target, DISTINCT from the
21
- // 100-turn mechanical hard cap (MAX_ADVISOR_TURNS above; pure runaway-loop
22
- // guard). They serve different purposes; do NOT synchronize them.
23
-
24
- // The live "[thinking…]" wait indicator shares its exact text with the TUI
25
- // cleanup regex (agent-turn.mjs strips it before flushing to history) — keep
26
- // them in lockstep.
27
- export const ADVISOR_THINKING_PLACEHOLDER = "\n[thinking…]\n"
28
-
29
- // Context window limits
30
- const MAX_CONTEXT_TOKENS = 120_000 // Reserve headroom to avoid OOM
31
- const TOOL_TIMEOUT_MS = 30_000 // single tool timeout
32
- const REVIEW_TIMEOUT_MS = 600_000 // whole review timeout (10 minutes)
33
- export const MAX_RESULT_CHARS = 64 * 1024 // tool result truncation (line-aware; 64K, aligned with main offload limit)
34
- const MAX_UNFIXED_DISPLAY = 10 // unfixed issues shown in the cap message
35
- const MAX_KEY_FILES_IN_COMPACTION = 5 // files named in the compaction summary
36
-
37
- /** Estimate token count from messages (rough: 1 token 4 chars) */
38
- function estimateTokens(messages) {
39
- return messages.reduce((sum, msg) => {
40
- const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content || "")
41
- const toolCalls = msg.tool_calls ? JSON.stringify(msg.tool_calls) : ""
42
- return sum + Math.ceil((content.length + toolCalls.length) / 4)
43
- }, 0)
44
- }
45
-
46
- /** Compact early messages when context grows too large — LOCAL trimming only
47
- * (no LLM summarization). MUTATES in place (splice) so the caller's array
48
- * reference stays valid — a reassignment would leave the caller's logging
49
- * (tool-call count, token estimate) reading a stale array. */
50
- function compactMessages(messages) {
51
- // Keep: system prompt, last 20 messages (≈ 10 assistant+tool exchanges),
52
- // user message — the rest is summarized.
53
- if (messages.length <= 20) return
54
-
55
- const system = messages[0]
56
- const recent = messages.slice(-20)
57
- const old = messages.slice(1, -20)
58
-
59
- // Count actual tool messages (old.length counts user/assistant rows too)
60
- const toolCount = old.filter((m) => m.role === "tool").length
61
- const keyFiles = old
62
- .filter((m) => m.role === "tool")
63
- .map((m) => m.content?.split("\n")[0]?.slice(0, 50)) // first line of tool results typically names the file that was read/grepped
64
- .filter(Boolean)
65
- .slice(0, MAX_KEY_FILES_IN_COMPACTION)
66
- const filesPart = keyFiles.length > 0 ? ` Key files examined: ${keyFiles.join(", ")}` : ""
67
- const summary = `Earlier exploration: ${toolCount} tool calls completed.${filesPart}`
68
-
69
- messages.splice(0, messages.length,
70
- system,
71
- { role: "user", content: `[Context compacted] ${summary}` },
72
- ...recent)
73
- }
74
-
75
- const { readTool, globTool, grepTool, lsTool } = await import("../tools/index.mjs")
76
- const { lspTool } = await import("../tools/lsp.mjs")
77
- const { codeSearchTool } = await import("../memory/code-sync.mjs")
78
-
79
- /**
80
- * Advisor tool set — ZERO git, read-only ONLY, every round. The change surface
81
- * comes from the review scope (paths / _touchedFiles injected by the caller),
82
- * never from git: git output misled reviews (committed fixes never show in
83
- * `git diff HEAD`, so "no changes" was read as "not fixed") and the user
84
- * mandate is full decoupling (7d49a52 + d3be613). The reviewer reads files
85
- * and searches code; it never touches git and never writes.
86
- * No round parameter — the set is constant across all rounds.
87
- * @param {Object} agent — only used for the code index (agent.memory); the
88
- * semantic code_search tool needs it. Without a memory, the set is 5 tools.
89
- */
90
- function advisorToolsFor(agent) {
91
- const search = agent?.memory ? codeSearchTool(agent.memory) : null
92
- const tools = search
93
- ? [readTool, globTool, grepTool, lsTool, lspTool, search]
94
- : [readTool, globTool, grepTool, lsTool, lspTool]
95
- return { schemas: tools.map(toOpenAISchema), byName: new Map(tools.map((t) => [t.name, t])) }
31
+ /** B 启动拒绝前缀(§14.4 #2)——稳定契约单源(三消费点同串):run.mjs 生成;同步工具面
32
+ * 据此登记 `_advisorRefusals`;异步结算面据此不置 `_calledAdvisorThisRun`。 */
33
+ export const ADVISOR_LAUNCH_REFUSAL_PREFIX = "Advisor: design review launch refused"
34
+
35
+ /** 评审失败护栏稳定前缀(第 33 批 §17.4——与 `ADVISOR_LAUNCH_REFUSAL_PREFIX` 同族;
36
+ * 实现 grep / 用例断言锚)。凭证卫生:串内零 token / designId 值。 */
37
+ export const ADVISOR_DESIGN_STREAK_STOP_PREFIX = "Advisor: design review stopped"
38
+
39
+ /** kind 人读说明(第 33 批 §17.4 逐字——结论表第三列;八类 = 五 kind + stale +
40
+ * no_credential + no_report,与 review-streak.mjs 分类输出同集)。 */
41
+ const DESIGN_REVIEW_OUTCOME_MEANINGS = {
42
+ timeout: "review exceeded the wall-clock budget (agent.advisor.timeoutMs)",
43
+ context_limit: "review exceeded the model context budget",
44
+ turn_cap: "review exceeded the tool-round limit",
45
+ empty: "the provider returned an empty response",
46
+ review_failed: "provider / transport error",
47
+ stale: "the reviewed documents changed while the review was in flight",
48
+ no_credential: "the token could not be written to the session ledger",
49
+ no_report: "the review settled without a report",
96
50
  }
97
- // Test seam: the tool set is pure (agent.memory → code_search inclusion).
98
- export { advisorToolsFor as _advisorToolsFor }
99
51
 
100
52
  /**
101
- * Tool-call progress line summary delegates to the single source describeToolArgs
102
- * (../tui/tool-args.mjs) the same function main-agent tool blocks and subagent
103
- * blocks use. 2026-08-31: replaced the local picker (action/path/pattern/command-only)
104
- * so advisor progress lines show the quoted-path forms everywhere else.
105
- */
106
- /**
107
- * Render the ordered review timeline — thinking / tool progress / final text
108
- * interleaved EXACTLY as emitted, so the persisted record shows the review
109
- * process at its real positions. A summary appended at the end would lose the
110
- * order (the user-visible "no tool calls in the advisor record" gap). The
111
- * live "[thinking…]" placeholder is stripped (wait indicator, not content).
53
+ * 停止结论串(第 33 §17.4 逐字;F29):稳定前缀 + 停止的 doc-set 清单 + 失败尝试表
54
+ * (记录逐条——kind + 人读说明)+ 三选项(接受现状 / 改变或缩小范围后重跑 / /new 重置)。
55
+ * 全文零凭证值;不自动执行任何恢复动作(发起权在父代理 / 用户)。
56
+ * @param {{count: number, log: string[]}|null} record 护栏记录(`designReviewStreakRecord`)
57
+ * @param {string[]|null} documents — 被停的文档集(结论清单数据源)
112
58
  */
113
- function renderTimeline(timeline, tail = "") {
114
- const body = timeline
115
- .map((b) => b.text.replaceAll(ADVISOR_THINKING_PLACEHOLDER, "").trim())
116
- .filter(Boolean)
117
- .join("\n\n")
118
- return [body, tail].filter(Boolean).join("\n\n")
59
+ export function buildDesignReviewGuardMessage(record, documents) {
60
+ const log = Array.isArray(record?.log) ? record.log : []
61
+ const docList = Array.isArray(documents) ? documents.filter((d) => typeof d === "string" && d.trim()) : []
62
+ return [
63
+ `${ADVISOR_DESIGN_STREAK_STOP_PREFIX} — ${MAX_DESIGN_REVIEW_STREAK} consecutive attempts on this document set produced no design token (repeated failed settlements; no further reviews will start for this set in this session).`,
64
+ "Document set (1 design instance — no token issued):",
65
+ ...docList.map((d) => `- ${d}`),
66
+ "Attempts (most recent last):",
67
+ "| # | outcome | meaning |",
68
+ "|---|---|---|",
69
+ ...log.map((kind, i) => `| ${i + 1} | ${kind} | ${DESIGN_REVIEW_OUTCOME_MEANINGS[kind] ?? kind} |`),
70
+ "Options:",
71
+ "1. Accept the current state and proceed — implementation for this document set stays gated (no design token).",
72
+ "2. Narrow or change the scope: a different document set starts a fresh budget — fix the cause first (agent.advisor.timeoutMs / advisor model / provider).",
73
+ "3. Start a new session (/new) to reset the guard.",
74
+ ].join("\n")
119
75
  }
120
- // Test seam (mirrors _advisorToolsFor).
121
- export { renderTimeline as _renderTimeline }
122
- // Test seam (T-TS8/9): the tool loop itself — toolsOverride injects a mock tool
123
- // set with controllable timing/errors (the real set comes from advisorToolsFor).
124
- export { runAdvisorToolLoop as _runAdvisorToolLoop }
125
-
126
- /**
127
- * Run the advisor's tool loop: chat → execute tools → repeat.
128
- * Stops when the model produces text without tool calls.
129
- *
130
- * Progress lines (→ tool args) are emitted via onOutput between model bursts so
131
- * the panel keeps moving while the advisor explores — otherwise the panel sits
132
- * frozen through every tool-call phase and the review appears to have stalled.
133
- */
134
- async function runAdvisorToolLoop(provider, messages, onOutput, signal, agent, cwd, toolsOverride = null) {
135
- // Kind-tagged wrappers: the TUI panel colors reasoning / answer / tool progress differently.
136
- // Every chunk is ALSO recorded into an ordered timeline — the persisted record
137
- // must show the review process (thinking ↔ tool progress ↔ final text) at its
138
- // real positions, not a summary appended at the end. Same-kind consecutive
139
- // chunks merge (token streams); kind flips start a new entry.
140
- const timeline = []
141
- const record = (kind, text) => {
142
- const last = timeline.at(-1)
143
- if (last && last.kind === kind) last.text += text
144
- else timeline.push({ kind, text })
145
- }
146
- const emit = (kind) => (text) => { record(kind, text); onOutput?.({ kind, text }) }
147
- const onThink = emit("think")
148
- const onText = emit("text")
149
- const onTool = emit("tool")
150
- // toolsOverride = test seam (T-TS8/9): the real advisor tool set, or a mock
151
- // set with controllable timing/errors.
152
- const { schemas: toolSchemas, byName: toolByName } = toolsOverride ?? advisorToolsFor(agent)
153
- let turns = 0
154
- const startTime = Date.now()
155
-
156
- while (true) {
157
- // Interrupted (Ctrl+I) — stop immediately instead of spinning a fresh uncancellable signal
158
- if (signal?.aborted) return renderTimeline(timeline, "Advisor: interrupted.")
159
-
160
- // Check review timeout (10 minutes by default; agent.advisor.timeoutMs overrides)
161
- // 运行时校验(设计评审 #1,2026-08-24):手写 config.json 的非法值(0/负数/字符串)
162
- // 不得静默禁用或立即触发超时——非法一律回退默认。
163
- const cfg = agent.config?.advisor?.timeoutMs
164
- const timeoutMs = (Number.isFinite(cfg) && cfg > 0) ? cfg : REVIEW_TIMEOUT_MS
165
- if (Date.now() - startTime > timeoutMs) {
166
- return renderTimeline(timeline, `Advisor: review timeout after ${Math.round(timeoutMs / 1000)}s. Partial results may be available. Try again with a narrower scope.`)
167
- }
168
-
169
- if (++turns > MAX_ADVISOR_TURNS) {
170
- return renderTimeline(timeline, "Advisor: stopped after " + MAX_ADVISOR_TURNS + " tool rounds — the review appears to be looping. You may retry with a narrower scope.")
171
- }
172
-
173
- // Check context window and compact if needed
174
- const currentTokens = estimateTokens(messages)
175
- if (currentTokens > MAX_CONTEXT_TOKENS * 0.8) {
176
- onText(`\n[Context compacted: ${currentTokens} tokens → reducing to fit window]\n`)
177
- compactMessages(messages)
178
- if (estimateTokens(messages) > MAX_CONTEXT_TOKENS) {
179
- // Report the POST-compaction count — the pre-compaction currentTokens
180
- // is stale by the time compaction has run.
181
- return renderTimeline(timeline, `Advisor: context window limit reached (${estimateTokens(messages)} tokens). Review incomplete — too many tool calls. Try a narrower scope.`)
182
- }
183
- }
184
-
185
- // LLM generation silence: the reasoning phase produces no SSE bytes for
186
- // seconds to tens of seconds (server-side prefill on large contexts, per
187
- // tool-round LLM return). A placeholder keeps the panel visibly working.
188
- // kind "think" (NOT "text"): the placeholder must land in the SAME buffer
189
- // and position as the upcoming reasoning — a "text"-kind placeholder
190
- // rendered BELOW the think block, and the reasoning stream appeared ABOVE
191
- // it ("the stream runs back to the front"). Same buffer = same spot; the
192
- // reasoning continues right where the placeholder sits.
193
- onOutput?.({ kind: "think", text: ADVISOR_THINKING_PLACEHOLDER })
194
76
 
195
- const response = await chat(provider, {
196
- messages,
197
- tools: toolSchemas,
198
- // Pass the signal UNCONDITIONALLY: a signal aborted between the check
199
- // above and here must still cancel the fetch. core.mjs composes
200
- // AbortSignal.any([signal, timeout]) — an already-aborted signal makes
201
- // the request fail immediately instead of ignoring the interrupt.
202
- signal: signal ?? null,
203
- onToken: onText,
204
- onReasoning: onThink,
205
- // LOGGING(vscode advisor/run.mjs parity——按 stage 可 grep)
206
- // §18.6 D-TR4:轨迹元数据增补——kind=advisor(评审独立于子代理——T-TR2);role
207
- // 透出调用方角色(eng-coder 内嵌评审时为 "eng-coder");session/cwd 供轨迹对回;
208
- // traces 开关沿 agent.config(D-TR6)。
209
- logCtx: {
210
- stage: "advisor",
211
- role: agent?._role ?? null,
212
- kind: "advisor",
213
- session: agent?._sessionStart ?? null,
214
- cwd,
215
- traces: agent?.config?.traces?.enabled !== false,
216
- },
217
- })
218
-
219
- // No tool calls — this is the final review text. The final answer was
220
- // already streamed into the timeline via onText; fall back to
221
- // response.content only if nothing was recorded.
222
- if (!response.toolCalls?.length) {
223
- if (!response.content?.trim()) return renderTimeline(timeline) || "Advisor: empty response — review was inconclusive"
224
- return renderTimeline(timeline) || response.content.trim()
225
- }
226
-
227
- // Push assistant message with tool calls. reasoning_content ECHO is
228
- // mandatory for reasoningEcho:"required" providers (deepseek/kimi): the
229
- // server stops returning reasoning_content on later rounds when the
230
- // tool-call assistant history lacks it — the observed "reasoning stops
231
- // after the first tool call, returns only at the final answer" symptom.
232
- // Mirrors the main agent's push (agent.mjs).
233
- messages.push({
234
- role: "assistant",
235
- content: response.content || null,
236
- tool_calls: response.toolCalls.map((tc) => ({
237
- id: tc.id, type: "function",
238
- function: { name: tc.name, arguments: tc.arguments },
239
- })),
240
- ...(response.reasoning && providerSpec(provider).reasoningEcho === "required"
241
- ? { reasoning_content: response.reasoning }
242
- : {}),
243
- })
244
-
245
- // B1 (AGENT-LOOP.md §18.7 D-TS7): the SAME LLM reply's multiple read-only
246
- // tool calls run in PARALLEL (Promise.all) — results are backfilled in
247
- // toolCalls order (Promise.all preserves the input order → tool_call_id
248
- // never mismatches); each tool's timeout/error is captured independently
249
- // (the existing TOOL_TIMEOUT stays — one failing tool does not block the
250
- // others); progress lines are emitted in toolCalls order. The read-only
251
- // tool set has no side effects — no sequencing/serialization needed.
252
- // Scope note (round1 review #10): B1 is ONLY in-loop tool parallelism — it
253
- // does NOT solve the TODO "platform execution: advisor parallel calls are
254
- // actually serial" mystery (docs/TODO.md — LOGGING evidence item), which
255
- // concerns multiple advisor CALLS observed as serial, not one reply's
256
- // tool calls.
257
- const parsed = response.toolCalls.map((tc) => {
258
- const tool = toolByName.get(tc.name)
259
- let args = {}
260
- let parseError = null
261
- try {
262
- args = JSON.parse(tc.arguments || "{}")
263
- } catch (e) {
264
- parseError = `Error: invalid JSON in tool arguments: ${e.message}\nRaw arguments: ${(tc.arguments || "").slice(0, 200)}`
265
- }
266
- return { tc, tool, args, parseError }
267
- })
268
- // Progress lines first, in toolCalls order (emitted before the parallel
269
- // run — display order is independent of completion order).
270
- for (const p of parsed) {
271
- if (p.parseError) continue // parse-error tools get no progress line (legacy behavior)
272
- const argsLine = describeToolArgs(p.tc.name, p.args)
273
- onTool(`\n→ ${p.tc.name}${argsLine ? " " + argsLine : ""}\n`)
274
- }
275
- // Every tool runs CONCURRENTLY; each result/error lands in its own slot —
276
- // Promise.all preserves input order, so index i always matches parsed[i].
277
- const executed = await Promise.all(parsed.map(async (p) => {
278
- // Parse failure → error to model immediately (no execution)
279
- if (p.parseError) return p.parseError
280
- if (!p.tool) return `Error: unknown tool "${p.tc.name}". Available: ${[...toolByName.keys()].join(", ")}`
281
- // Execute with timeout (clear the timer when the tool wins the race —
282
- // otherwise up to MAX_ADVISOR_TURNS dangling timers accumulate)
283
- try {
284
- let timeoutId
285
- const timeoutPromise = new Promise((_, reject) => {
286
- timeoutId = setTimeout(() => reject(new Error(`tool timeout after ${TOOL_TIMEOUT_MS}ms`)), TOOL_TIMEOUT_MS)
287
- })
288
- let toolPromise
289
- try {
290
- toolPromise = p.tool.execute(p.args, { cwd, agent, onOutput, signal })
291
- return await Promise.race([toolPromise, timeoutPromise])
292
- } finally {
293
- clearTimeout(timeoutId)
294
- // Timeout won → toolPromise is still pending; a later rejection
295
- // would surface as an unhandled rejection. The race already
296
- // consumed the result/error in the normal path, so this no-op
297
- // catch only fires for the abandoned-tool case.
298
- toolPromise?.catch(() => {})
299
- }
300
- } catch (e) {
301
- const errorType = e.message.includes("timeout") ? "timeout"
302
- : e.message.includes("ENOENT") ? "file_not_found"
303
- : e.message.includes("permission") ? "permission_denied"
304
- : "execution_error"
305
- return `Error (${errorType}): ${e.message}`
306
- }
307
- }))
308
-
309
- // Backfill in toolCalls order (executed[i] ↔ parsed[i]); per-result
310
- // non-string serialization + line-aware truncation stay per-tool.
311
- for (let i = 0; i < parsed.length; i++) {
312
- let result = executed[i]
313
- if (typeof result !== "string") result = JSON.stringify(result)
314
-
315
- if (result.length > MAX_RESULT_CHARS) {
316
- const lines = result.split("\n")
317
- let truncated = ""
318
- let charCount = 0
319
- let keptLines = 0
320
-
321
- for (let j = 0; j < lines.length; j++) {
322
- const line = lines[j]
323
- if (charCount + line.length + 1 > MAX_RESULT_CHARS) break
324
- truncated += line + "\n"
325
- charCount += line.length + 1
326
- keptLines++
327
- }
328
-
329
- const remainingLines = lines.length - keptLines
330
- result = (
331
- truncated +
332
- `\n… (truncated: ${remainingLines} more lines, ${result.length} chars total)\n` +
333
- `To see more content, use: read(path, offset=${keptLines + 1}, limit=200)`
334
- )
335
- }
336
-
337
- messages.push({ role: "tool", tool_call_id: parsed[i].tc.id, content: result })
338
- }
339
- }
340
- }
77
+ const MAX_UNFIXED_DISPLAY = 10 // unfixed issues shown in the cap message
341
78
 
342
79
  /** Resolve the advisor's provider: cfg.provider/model when set, otherwise the main agent's provider */
343
80
  export function resolveAdvisorProvider(agent) {
344
81
  const cfg = agent.config?.advisor
345
82
  if (cfg?.provider) {
346
83
  try {
347
- const provider = findProvider(agent.providers ?? [agent.provider], cfg.provider)
348
- const result = cfg.model ? { ...provider, model: cfg.model } : { ...provider }
84
+ // F-1 (ISSUE-FIX-BATCH): children carry no agent.providers (spawn childConfig copies the
85
+ // parent config) fall back to config.providersList (.length: [] must not skip the list).
86
+ const provider = findProvider(agent.providers?.length ? agent.providers : agent.config?.providersList ?? [agent.provider], cfg.provider)
87
+ // F-2a (MODEL-400-FIX):无 cfg.model 时渠道克隆须重派生 model——渠道裸克隆会丢 model 键
88
+ // → 无 model 请求 → serde 400。
89
+ // MODEL-SELECTION v2(M3④):无 cfg.model → 命中渠道自己的默认模型(`provider.model`
90
+ // 单值);渠道无默认模型 → 父 provider 兜底(与 subagent F-2c 同构)——绝不产出静默
91
+ // undefined-model 请求(最极端两者皆无 → chat 前 assertProviderModel fail-fast)。
92
+ const result = cfg.model ? { ...provider, model: cfg.model } : { ...provider, model: provider.model ?? agent.provider?.model }
349
93
  if (cfg.thinking === null || cfg.thinking === false) result.thinking = undefined // explicitly off
350
94
  else if (cfg.thinking !== undefined) result.thinking = cfg.thinking
351
95
  if (cfg.reasoningEffort !== undefined) result.reasoningEffort = cfg.reasoningEffort
@@ -406,6 +150,24 @@ export function buildCapMessage(agent) {
406
150
  return message
407
151
  }
408
152
 
153
+ /**
154
+ * 压缩定锚简报(F13/§14.4 #3)——**由评审参数构建**(非模型输出):对象声明 / 文档清单 /
155
+ * Approval Signal 三锚;重内容(项目指南 / 方法论 / 文档地图)不入 pin(压缩的意义所在)。
156
+ * 形态逐字见设计 §14.4 #3(首行为机械化重挂说明)。
157
+ */
158
+ function buildPinnedBrief(reviewType, documents, object, designToken, designId) {
159
+ const docList = Array.isArray(documents) ? documents.filter((d) => typeof d === "string" && d.trim()) : []
160
+ const parts = ["[review brief — re-attached after context compaction; the original review request is no longer in the context]"]
161
+ const declaration = buildObjectDeclarationBlock(object)
162
+ if (declaration) parts.push(declaration.trimEnd())
163
+ if (docList.length > 0) {
164
+ parts.push("## Documents to Review")
165
+ parts.push(docList.map((d) => `- ${d} — Read this file in full`).join("\n"))
166
+ }
167
+ if (reviewType === "design" && designToken) parts.push(buildDesignApprovalBlock(designToken, designId))
168
+ return parts.join("\n\n")
169
+ }
170
+
409
171
  /**
410
172
  * Run an advisor review. reviewType: "code" (default) or "design". Returns review text or null when skipped.
411
173
  * @param {string|null} [designToken] — injected into the design-review prompt; the advisor echoes it only on approval.
@@ -426,20 +188,52 @@ export async function runAdvisorReview(agent, reviewType, callbacks, designToken
426
188
 
427
189
  // Mechanical convergence cap — CODE REVIEWS ONLY (2026-09-07 §8 ruling: design
428
190
  // reviews are exempt). _advisorRound is scoped to the current review instance
429
- // (§24 D-24b ③), so >= MAX_ADVISOR_ROUNDS blocks the next call of THIS instance.
191
+ // (§11.2 D-24b ③), so >= MAX_ADVISOR_ROUNDS blocks the next call of THIS instance.
430
192
  // 5 rounds max; after that the review is never pushed back.
431
193
  if (reviewType !== "design" && (agent._advisorRound || 0) >= MAX_ADVISOR_ROUNDS) {
432
194
  return buildCapMessage(agent)
433
195
  }
434
196
 
197
+ // 第 33 批(§17.5 检查点 2——内防线):同一 doc-set 连续未产出可用结算达阈值 ⇒ 直接拒绝
198
+ // (不建消息、不发起、零 LLM)——防直接调用方绕过工具层预检(正常工具链在预检已拒)。
199
+ if (reviewType === "design" && Array.isArray(documents) && documents.length > 0) {
200
+ const streakKey = docSetKey(documents, agent.cwd)
201
+ if (designReviewStreakStopped(agent, streakKey)) {
202
+ return buildDesignReviewGuardMessage(designReviewStreakRecord(agent, streakKey), documents)
203
+ }
204
+ }
205
+
435
206
  const provider = resolveAdvisorProvider(agent)
436
207
  // Advisor always works in the agent's cwd — scope is defined by paths/documents.
437
208
  const advisorCwd = agent.cwd
438
209
 
439
210
  const messages = prepareAdvisorMessages(agent, reviewType, designToken, documents, paths, null, object, designId)
440
211
 
212
+ // B 启动断言(fail-closed——§14.4 #2):设计评审请求内**必须**携带与本次签发 token 精确
213
+ // 对应的 Approval Signal——构建面补不上就拒绝启动(不发"请回显一个不存在的 token"的请求)。
214
+ // 拒绝报告前缀 `Advisor: design review launch refused` = 稳定契约(同步工具面据此登记
215
+ // _advisorRefusals;异步结算面据此不置 _calledAdvisorThisRun)。工具路径恒签发 token ⇒
216
+ // 该拒绝为直接调用方兜底(防御纵深——正常链不可达,如实注)。
217
+ if (reviewType === "design") {
218
+ const missing = !designToken
219
+ ? "no design token was minted"
220
+ : (messages.some((m) => m.role === "user" && String(m.content ?? "").includes(`[DESIGN-TOKEN:${designToken}`))
221
+ ? null
222
+ : "the request does not carry the approval signal")
223
+ if (missing) {
224
+ return `${ADVISOR_LAUNCH_REFUSAL_PREFIX} — ${missing}. Nothing was sent: a request that asks the reviewer to echo a token it cannot see would break the credential chain. Re-run advisor(type='design') to mint a fresh token.`
225
+ }
226
+ }
227
+
228
+ // §2.20.3 批次档写通道的绑定(仅设计评审):同步路径 = 调用方(advisor 工具)传入的
229
+ // callbacks.batchDoc(即 resolved.run 的实例绑定);异步路径 = 本实例在跑池条目的
230
+ // run.batchDoc(同文档集实例键——各评审各取各档,不用单值会话态)。
231
+ const boundBatchDoc = reviewType === "design" ? batchDocForReview(agent, documents, callbacks) : null
232
+ // F13/§14.4 #3:压缩定锚简报(评审参数构建——压缩触发时由 compaction 重挂)。
233
+ const pinned = buildPinnedBrief(reviewType, documents, object, designToken, designId)
234
+
441
235
  try {
442
- const result = await runAdvisorToolLoop(provider, messages, onOutput, signal, agent, advisorCwd)
236
+ const result = await runAdvisorToolLoop(provider, messages, onOutput, signal, agent, advisorCwd, null, reviewType, boundBatchDoc, pinned)
443
237
 
444
238
  // Host-verified citations (decision d698434): mechanically check every
445
239
  // `file:line: content` reference in the review against the CURRENT file
@@ -447,9 +241,10 @@ export async function runAdvisorReview(agent, reviewType, callbacks, designToken
447
241
  // the prior table instead of re-reading (three consecutive false reports
448
242
  // cited pre-fix line content). Unverified citations must not support a
449
243
  // push-back; the parent agent sees the verification report.
244
+ // F14/§14.5:解析候选 = cwd + 评审对象声明范围派生根(scope = documents + paths)。
450
245
  let final = result
451
246
  if (!result.trimStart().startsWith("Advisor:")) {
452
- final = appendCitationReport(result, advisorCwd)
247
+ final = appendCitationReport(result, advisorCwd, { scope: [...(documents ?? []), ...(paths ?? [])] })
453
248
  // Success path: keep the FULL review output for convergence rounds —
454
249
  // round 2+ injects this verbatim and the model understands it (decision
455
250
  // 2026-08-08: prior-table hard parsing removed; no phrase/header matching).
@@ -0,0 +1,57 @@
1
+ /**
2
+ * truncate.mjs — advisor 工具结果的行感知双端截断(DUAL-END-TRUNCATION F-2,
3
+ * DUAL-END-TRUNCATION.md,2026-09-09——CLI/VSC 双端逐字同构镜像)。
4
+ *
5
+ * 原 run.mjs 内联截断为纯头向(line-aware 从首行累加至 64K break)——评审尾部
6
+ * 结论/裁决被切。现改头尾双保:头行累加至预算 ~60% → 中段省略注 → 尾行累加至
7
+ * 剩余 ~40%(保尾结论——与 read 工具 C 方案 / offload 双端预览同构的
8
+ * "头+尾保留、中段截断"策略——共享设计语言、分别实现)。
9
+ *
10
+ * 纯函数无导入:maxChars 由调用方传入(run.mjs 传 MAX_RESULT_CHARS)。
11
+ */
12
+
13
+ /** advisor 截断头占比——头行累加至此份额后切中段,尾自动取剩余预算(设计 §4 ≈0.6)。 */
14
+ export const ADVISOR_HEAD_RATIO = 0.6
15
+
16
+ /** 尾部预算预留(字符):中段省略注 + offset 续读提示的保守预留——注的位数只减不增
17
+ * (middle ≤ totalChars——超大结果位数可达 ~10 位——200 字符仍宽松覆盖注+提示 ≤ ~140;
18
+ * 差额留给尾行)。 */
19
+ const NOTE_RESERVE_CHARS = 200
20
+
21
+ /**
22
+ * 行感知双端截断:result > maxChars → 头 + `… (truncated: K more lines, TOTAL chars
23
+ * total)` + 尾 + offset 续读提示;≤ maxChars 原样返回(无假截断注)。K = 头尾之间
24
+ * 的中段行数——头(≤60%) + 尾(≤余预算) 均行级累加、绝不半行切开;中段存在时 K ≥ 1
25
+ * (防御:K ≤ 0 时静默返回头尾拼合——不谎报截断)。
26
+ */
27
+ export function truncateAdvisorResult(result, maxChars) {
28
+ if (result.length <= maxChars) return result
29
+ const totalChars = result.length
30
+ const lines = result.split("\n")
31
+ const headBudget = Math.floor(maxChars * ADVISOR_HEAD_RATIO)
32
+ let head = ""
33
+ let headLines = 0
34
+ let chars = 0
35
+ while (headLines < lines.length && chars + lines[headLines].length + 1 <= headBudget) {
36
+ chars += lines[headLines].length + 1
37
+ head += lines[headLines] + "\n"
38
+ headLines++
39
+ }
40
+ const tailBudget = Math.max(0, maxChars - chars - NOTE_RESERVE_CHARS)
41
+ let tail = ""
42
+ let tailChars = 0
43
+ let tailStart = lines.length
44
+ while (tailStart > headLines && tailChars + lines[tailStart - 1].length + 1 <= tailBudget) {
45
+ tailChars += lines[tailStart - 1].length + 1
46
+ tailStart--
47
+ tail = lines[tailStart] + "\n" + tail
48
+ }
49
+ const middle = tailStart - headLines
50
+ if (middle <= 0) return head + tail // 头尾已覆盖全部行——绝不产生假截断注
51
+ return (
52
+ head +
53
+ `\n… (truncated: ${middle} more lines, ${totalChars} chars total)\n` +
54
+ tail +
55
+ `To see more content, use: read(path, offset=${headLines + 1}, limit=200)`
56
+ )
57
+ }
package/src/advisor.mjs CHANGED
@@ -2,7 +2,8 @@
2
2
  * advisor.mjs — advisor system-prompt selection, follow-up building, session assembly.
3
3
  * User-message building lives in advisor/messages.mjs; execution (tool loop, provider
4
4
  * resolution, review entry) in advisor/run.mjs; history extraction in advisor/history.mjs.
5
- * repos.mjs still hosts the doc-file classifier (isDocFile) used by mutation tracking.
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).
6
7
  *
7
8
  * The advisor runs as a read-only exploration sub-agent with tools
8
9
  * (read, glob, grep, ls, lsp, code_search) — ZERO git, every round. The change
@@ -266,7 +267,7 @@ export function prepareAdvisorMessages(agent, reviewType, designToken = null, do
266
267
  // review surface.
267
268
  const scopeFiles = resolveScopeFiles(agent, paths)
268
269
  const followUp = buildAdvisorFollowUp(agent, prior, scopeFiles, object)
269
- // §24 D-24b (design round 2+ — async fix-round continuations must be able to
270
+ // §11.2 D-24b (design round 2+ — async fix-round continuations must be able to
270
271
  // re-approve): re-anchor the review scope (the convergence follow-up carries no
271
272
  // document list) and inject the round's design token with the approval signal.
272
273
  if (reviewType === "design") {
@@ -120,7 +120,7 @@ export function handleCompletion(agent, response, depth, turn, guardPushbacks, h
120
120
  const cfg = agent.config?.advisor
121
121
  const advisorReview = cfg?.guard === true
122
122
  if (depth === 0 && advisorReview && !agent.config?.agent?.engineering) {
123
- // §24 D-24b (T-24b4 — guard timing): an async review that is still in flight
123
+ // §11.2 D-24b (T-24b4 — guard timing): an async review that is still in flight
124
124
  // (or queued in the advisor pool) means the review was launched — the guard
125
125
  // does NOT push back while it is pending (未决不算未评审); once it settles
126
126
  // non-stale it marks _calledAdvisorThisRun, and a STALE settle leaves the