thincoder 0.12.60 → 0.12.62

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 (171) hide show
  1. package/CHANGELOG.md +49 -2
  2. package/README.md +8 -6
  3. package/bin/thincoder.mjs +38 -124
  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 +19 -6
  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 +24 -7
  23. package/src/agent/setup-reminders.mjs +16 -9
  24. package/src/agent/setup.mjs +92 -128
  25. package/src/agent/spawn-child.mjs +43 -11
  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/async-settle.mjs +13 -0
  30. package/src/agent-tools/batch-segment.mjs +195 -0
  31. package/src/agent-tools/consult.mjs +28 -10
  32. package/src/agent-tools/design-token.mjs +14 -1
  33. package/src/agent-tools/digest-budget.mjs +76 -0
  34. package/src/agent-tools/eng.mjs +3 -3
  35. package/src/agent-tools/escalate-async.mjs +22 -13
  36. package/src/agent-tools/read-history.mjs +31 -6
  37. package/src/agent-tools/review-streak.mjs +93 -0
  38. package/src/agent-tools/settings.mjs +130 -17
  39. package/src/agent-tools/subagent-actions.mjs +18 -6
  40. package/src/agent-tools/subagent-async.mjs +66 -14
  41. package/src/agent-tools/subagent-panel.mjs +22 -15
  42. package/src/agent-tools/subagent-run.mjs +10 -7
  43. package/src/agent-tools/subagent-scheduler.mjs +57 -8
  44. package/src/agent-tools/subagent-spawn.mjs +69 -16
  45. package/src/agent-tools/subagent.mjs +175 -49
  46. package/src/agent-tools/verify.mjs +13 -34
  47. package/src/agent-tools.mjs +1 -0
  48. package/src/agent.mjs +42 -21
  49. package/src/cli/distill-command.mjs +2 -2
  50. package/src/cli/make-agent.mjs +23 -7
  51. package/src/cli/memory-command.mjs +2 -2
  52. package/src/cli/setup-wizard.mjs +29 -9
  53. package/src/completions.mjs +114 -0
  54. package/src/config-migrate.mjs +70 -0
  55. package/src/config.mjs +132 -63
  56. package/src/context.mjs +12 -1
  57. package/src/conventions.mjs +223 -0
  58. package/src/crash-reports.mjs +35 -8
  59. package/src/expand-home.mjs +16 -0
  60. package/src/generate-title.mjs +9 -4
  61. package/src/heap-watch.mjs +88 -0
  62. package/src/hooks.mjs +7 -3
  63. package/src/ledger.mjs +227 -0
  64. package/src/memory/code-index.mjs +9 -3
  65. package/src/memory/code-sync.mjs +77 -36
  66. package/src/memory/core.mjs +8 -9
  67. package/src/memory/delete.mjs +2 -0
  68. package/src/memory/docs.mjs +17 -11
  69. package/src/memory/file-walk.mjs +109 -0
  70. package/src/memory/scan.mjs +95 -0
  71. package/src/memory/schema.mjs +15 -3
  72. package/src/model-ref.mjs +66 -0
  73. package/src/model-specs.mjs +42 -8
  74. package/src/prompt-overlays.mjs +73 -16
  75. package/src/prompts/advisor-design.md +19 -9
  76. package/src/prompts/advisor-round1.md +8 -2
  77. package/src/prompts/advisor-round2.md +14 -3
  78. package/src/prompts/advisor-round3.md +14 -3
  79. package/src/prompts/common.md +115 -0
  80. package/src/prompts/consult-base.md +2 -0
  81. package/src/prompts/discipline-engineering.md +258 -0
  82. package/src/prompts/discipline-normal.md +185 -0
  83. package/src/prompts/persona-coder.md +21 -0
  84. package/src/prompts/persona-eng-coder.md +37 -0
  85. package/src/prompts/persona-eng-designer.md +60 -0
  86. package/src/prompts/persona-engineering.md +55 -0
  87. package/src/prompts/persona-explore.md +15 -0
  88. package/src/prompts/persona-normal.md +27 -0
  89. package/src/prompts/persona-plan.md +26 -0
  90. package/src/provider/anthropic.mjs +4 -4
  91. package/src/provider/core.mjs +13 -32
  92. package/src/provider/errors.mjs +26 -1
  93. package/src/provider/google.mjs +5 -6
  94. package/src/provider/index.mjs +2 -1
  95. package/src/provider/list-models.mjs +93 -0
  96. package/src/provider/rate.mjs +2 -1
  97. package/src/provider/responses.mjs +5 -3
  98. package/src/provider/sse.mjs +3 -4
  99. package/src/proxy.mjs +9 -14
  100. package/src/session-gc.mjs +9 -2
  101. package/src/session-guard.mjs +12 -0
  102. package/src/session-segments.mjs +100 -0
  103. package/src/session-slots.mjs +10 -2
  104. package/src/session-store.mjs +441 -0
  105. package/src/session.mjs +137 -99
  106. package/src/text-budget.mjs +46 -0
  107. package/src/token-ttl.mjs +2 -1
  108. package/src/tools/{system.mjs → bash.mjs} +6 -243
  109. package/src/tools/file.mjs +30 -10
  110. package/src/tools/git.md +1 -1
  111. package/src/tools/git.mjs +15 -34
  112. package/src/tools/index.mjs +4 -2
  113. package/src/tools/ops.mjs +20 -7
  114. package/src/tools/question.md +1 -0
  115. package/src/tools/question.mjs +26 -0
  116. package/src/tools/read.md +1 -1
  117. package/src/tools/read_image.md +1 -1
  118. package/src/tools/search.mjs +236 -0
  119. package/src/traces/trace-store.mjs +195 -64
  120. package/src/tui/agent-turn.mjs +32 -13
  121. package/src/tui/ansi.mjs +2 -0
  122. package/src/tui/clipboard.mjs +7 -1
  123. package/src/tui/cmd-advisor.mjs +3 -2
  124. package/src/tui/cmd-clear.mjs +2 -0
  125. package/src/tui/cmd-config.mjs +108 -37
  126. package/src/tui/cmd-eng.mjs +11 -27
  127. package/src/tui/cmd-exit.mjs +6 -8
  128. package/src/tui/cmd-model.mjs +14 -12
  129. package/src/tui/cmd-new.mjs +5 -1
  130. package/src/tui/cmd-reindex.mjs +7 -0
  131. package/src/tui/cmd-session.mjs +8 -4
  132. package/src/tui/cmd-submodel.mjs +8 -5
  133. package/src/tui/cmd-undo.mjs +4 -3
  134. package/src/tui/display-budget.mjs +184 -0
  135. package/src/tui/index.mjs +69 -40
  136. package/src/tui/key-handler-search.mjs +9 -1
  137. package/src/tui/key-handler.mjs +61 -17
  138. package/src/tui/key-modes.mjs +86 -8
  139. package/src/tui/layout.mjs +18 -10
  140. package/src/tui/ledger-surface.mjs +69 -0
  141. package/src/tui/model-catalog.mjs +89 -0
  142. package/src/tui/model-picker.mjs +498 -0
  143. package/src/tui/mouse.mjs +47 -10
  144. package/src/tui/pickers.mjs +28 -410
  145. package/src/tui/render-frame.mjs +38 -16
  146. package/src/tui/render-loop.mjs +2 -0
  147. package/src/tui/render-segments.mjs +5 -19
  148. package/src/tui/render.mjs +37 -5
  149. package/src/tui/slash-commands.mjs +2 -2
  150. package/src/tui/startup.mjs +45 -13
  151. package/src/tui/subagent-blocks.mjs +70 -90
  152. package/src/tui/subagent-children.mjs +125 -67
  153. package/src/tui/subagent-freeze.mjs +48 -45
  154. package/src/tui/subagent-panel.mjs +21 -66
  155. package/src/tui/suspension-drive.mjs +31 -83
  156. package/src/tui/tool-args.mjs +9 -4
  157. package/src/tui/tool-display.mjs +20 -5
  158. package/src/tui/tool-events.mjs +76 -30
  159. package/src/tui/tui-lifecycle.mjs +27 -7
  160. package/src/tui/wizard.mjs +52 -18
  161. package/src/tui/wrapped-spawn.mjs +54 -0
  162. package/src/prompts/coder.md +0 -13
  163. package/src/prompts/discipline.md +0 -84
  164. package/src/prompts/eng-coder.md +0 -19
  165. package/src/prompts/engineering-sub.md +0 -14
  166. package/src/prompts/engineering.md +0 -87
  167. package/src/prompts/explore.md +0 -12
  168. package/src/prompts/main.md +0 -34
  169. package/src/prompts/methodology-template.md +0 -38
  170. package/src/prompts/plan.md +0 -9
  171. package/src/prompts/system.md +0 -44
package/src/agent.mjs CHANGED
@@ -3,39 +3,43 @@
3
3
  * LLM ↔ tool-call loop, until the task is done.
4
4
  */
5
5
  import { chat } from "./provider/index.mjs"
6
+ import { abortError, annotateAbort } from "./abort-provenance.mjs"
6
7
  import { pushReal, summarizeRunExplorations } from "./context.mjs"
7
8
  import { specForModel } from "./config.mjs"
8
- import { readFileSync } from "node:fs"
9
- import { join, dirname, resolve } from "node:path"
10
- import { fileURLToPath } from "node:url"
9
+ import { resolve } from "node:path"
11
10
  import { executeToolCalls } from "./agent/dispatch.mjs"
12
11
  import { recordToolResults } from "./agent/record-results.mjs"
13
12
  import { FILE_MUTATORS } from "./agent/helpers.mjs"
14
13
  import { prepareRun } from "./agent/setup.mjs"
15
14
  import { injectPostTurn } from "./agent/post-turn.mjs"
16
15
  import { handleCompletion } from "./agent/completion.mjs"
16
+ // PROMPT-SYSTEM 施工② G1(2026-09-10):六件槽位常量装载收口 prompt-overlays.mjs
17
+ // (与子代理角色常量同源——单一权威锚 D1);旧三件文件读取随本批退役。
18
+ // 槽位常量通过下方 re-export 面(mod: rel — re-export 保 import 面);本文件自身零直接消费。
17
19
  // 主循环阶段函数(压缩检查/注入组/回合收尾)2026-09-05 实践轮迁 agent/run-stages.mjs
18
20
  import { runCompactionCheck, injectTurnReminders, finalizeAgentTurn, injectResponseReminders } from "./agent/run-stages.mjs"
19
21
  import {
20
22
  escapeXml, repairHistory, listWorkDir,
21
23
  readonlyToolNames, collectGitContext, loadProjectInstructions,
22
24
  ContinueError,
25
+ turnFrame, // 第 19 批(TURN-ACROSS-SEGMENTS):跨段累计编号帧(设计 §19.3)
23
26
  DEFAULT_MAX_TURNS, DEFAULT_SUBAGENT_TURNS,
24
27
  MIN_REPORT_CHARS, REPORT_CONTINUATION,
25
28
  AUTO_TURN_DIGEST_DOMAIN,
26
29
  } from "./agent/helpers.mjs"
27
30
  // ENG 提醒族 + auto-turn domain 2026-09-05 迁 agent/helpers.mjs(agent.mjs 530 > 500 硬限)
28
- // overlay 载荷(explore/coder/plan/eng-coder/consult)迁 prompt-overlays.mjs——re-export 保面
31
+ // PROMPT-SYSTEM 施工② G1(2026-09-10):六件槽位常量装载收口 prompt-overlays.mjs
32
+ // (与子代理角色常量同源——单一权威锚 D1);本文件 re-export 保 import 面。
29
33
  export {
30
- EXPLORE_OVERLAY, CODER_OVERLAY, PLAN_OVERLAY, ENG_CODER_OVERLAY, CONSULT_BASE,
34
+ PERSONA_ENGINEERING, PERSONA_NORMAL, COMMON,
35
+ DISCIPLINE_ENGINEERING, DISCIPLINE_NORMAL,
36
+ CONSULT_BASE,
31
37
  } from "./prompt-overlays.mjs"
32
38
  export { ENG_ON_REMINDER, ENG_OFF_REMINDER } from "./agent/helpers.mjs"
33
39
 
34
- // Prompt files (byte-stable, loaded once)
35
- const __dirname = dirname(fileURLToPath(import.meta.url))
36
- const SYSTEM_PROMPT = readFileSync(join(__dirname, "prompts", "system.md"), "utf8")
37
- const DISCIPLINE_RULES = readFileSync(join(__dirname, "prompts", "discipline.md"), "utf8")
38
- const MAIN_OVERLAY = readFileSync(join(__dirname, "prompts", "main.md"), "utf8")
40
+ // Role → persona slot mapping note(施工② G3): 子代理人格由 assemblePrompt 场景表按
41
+ // role 承载(persona-explore/coder/plan/eng-coder.md——D1 表 = 蓝图 §3.2 1:1);
42
+ // spawn 侧 overlay 概念已退役(prompt-overlays.mjs 不再导出 OVERLAY 别名常量)。
39
43
 
40
44
  // exported for consumption by agent-tools.mjs
41
45
  export {
@@ -66,8 +70,8 @@ export function createAgent({
66
70
  // (AC3 零写) — no field initializer; the multi-slot Map `_engDesignTokens` is the
67
71
  // authoritative ledger (hydrated by restoreEngTokens / written by settle).
68
72
  _touchedFiles: [], _verifyRetries: 0, _advisorRound: 0, _advisorSession: null,
69
- _advisorRuns: new Map(), // §24 D-24b: per-review convergence instances (rounds/prior/designId)
70
- _mutationSeq: 0, _mutLog: [], // §24 D-24b: mutation log (in-flight review staleness scan)
73
+ _advisorRuns: new Map(), // §11.2 D-24b: per-review convergence instances (rounds/prior/designId)
74
+ _mutationSeq: 0, _mutLog: [], // §11.2 D-24b: mutation log (in-flight review staleness scan)
71
75
  _lastAdvisorOutput: null, // full review output from the most recent advisor call (convergence rounds inject it verbatim)
72
76
  _lastEngState: false,
73
77
  _pendingReminders: [],
@@ -102,15 +106,21 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
102
106
  if (pendingAsync?.length) {
103
107
  const { injectAsyncResult } = await import("./agent-tools/subagent.mjs")
104
108
  const { injectConsultResult } = await import("./agent-tools/consult.mjs")
109
+ // TUI-OOM-ROOTCAUSE(AGENT-LOOP.md §23.3.1 消费点②——run 起始 pending 注入):
110
+ // 注入完成后释放条目对子代理对象的持有(childAgent/report 置空——幂等 helper)。
111
+ const { releaseSettledEntry } = await import("./agent-tools/async-settle.mjs")
105
112
  for (const e of pendingAsync.splice(0)) {
106
113
  if (e.role === "consult") await injectConsultResult(agent, e)
107
114
  else await injectAsyncResult(agent, e)
115
+ releaseSettledEntry(e)
108
116
  }
109
117
  }
110
118
  agent._inAutoTurn = autoTurn // spawn gate for manual-tier digests (§17 D-S6/N3)
111
119
  const { maxTurns, threshold, tools, toolSchemas, toolByName, systemPrompt } = await prepareRun(
120
+ // G1/G2(施工②):prompt 装配收口 prepareRun 内部(assemblePrompt——prompt-overlays.mjs
121
+ // 槽位常量,与子代理角色常量同源——单一权威锚 D1);本调用不再携带 prompt 常量。
112
122
  agent, input, callbacks,
113
- { depth, signal, overrideTurns, resume: resume || autoTurn, systemPrompt: SYSTEM_PROMPT, disciplineRules: DISCIPLINE_RULES, mainOverlay: MAIN_OVERLAY },
123
+ { depth, signal, overrideTurns, resume: resume || autoTurn },
114
124
  )
115
125
 
116
126
  // Exploration-distillation boundary (CONTEXT-COMPACTION §5): prepareRun already
@@ -120,6 +130,10 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
120
130
  // Per-run bookkeeping reset — PRESERVED on `resume` (ContinueError continuation):
121
131
  // mutation/guard continuity and the convergence budget must survive a continuation.
122
132
  if (!resume) {
133
+ // 第 19 批(TURN-ACROSS-SEGMENTS——设计 TURN-CAP-CONTINUE.md §19.3):链内累计编号
134
+ // 只在链起点复位——续跑(resume:true)不重置、不回退(编号帧公式见 helpers.mjs
135
+ // turnFrame)。与下方 mutation/guard 复位同条件同点(全档唯一复位点)。
136
+ agent._turnSeq = 0
123
137
  // §17 D-S6: an auto-turn's guard marks are inherited by the next USER run (not
124
138
  // reset) so auto-turn changes never escape the guard silently.
125
139
  const g = agent._inheritedGuard
@@ -172,14 +186,20 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
172
186
  let thrownError = null
173
187
  try {
174
188
  for (let turn = 0; turn < maxTurns; turn++) {
175
- // Update turn counter for status bar display
176
- agent._currentTurn = turn + 1
177
- agent._maxTurns = maxTurns
189
+ // 第 19 批(TURN-ACROSS-SEGMENTS——设计 TURN-CAP-CONTINUE.md §19.3):编号帧——
190
+ // `_turnSeq` 每轮 +1(跨段累计,仅 `!resume` 链起点复位);面向消费面的两字段
191
+ // (状态行 + ⟦ev⟧turn / ⟦ev⟧approval 事件共用)在此同点赋值(编号唯一权威;帧
192
+ // 公式 = helpers.mjs turnFrame)。段内帽判定不读帧(下行循环条件只读段内
193
+ // turn / maxTurns——N6)。
194
+ const frame = turnFrame(++agent._turnSeq, turn, maxTurns)
195
+ agent._currentTurn = frame.turn
196
+ agent._maxTurns = frame.maxTurns
178
197
  // D2 (AGENT-LOOP.md §7.2): depth>0 children emit a ⟦ev⟧turn progress token each turn —
179
198
  // single emit point covering all three spawn tools; phase=llm (tool/done progress rides
180
- // the onToolCall/onToolResult relay — no token for those).
199
+ // the onToolCall/onToolResult relay — no token for those). 第 19 批:载荷取上方帧值
200
+ // (agent._currentTurn / _maxTurns——字段形态 / 字段数 / phase 零变化,N5)。
181
201
  if (depth > 0 && callbacks.onToken) {
182
- callbacks.onToken(`⟦ev⟧turn\x1e${turn + 1}\x1e${maxTurns}\x1ellm\x1e`)
202
+ callbacks.onToken(`⟦ev⟧turn\x1e${agent._currentTurn}\x1e${agent._maxTurns}\x1ellm\x1e`)
183
203
  }
184
204
 
185
205
  // SUBAGENT-OBSERVE-SEND D2: 子代理回合边界消费点——每轮开头把父侧经 subagent
@@ -286,7 +306,8 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
286
306
  role: "user",
287
307
  content: `[User interrupt: ${response.interruptMessage}]`,
288
308
  })
289
- throw Object.assign(new Error("User interrupted"), { name: "AbortError" })
309
+ // §20.3 站点 #8(第 24 批):错误对象已自带 name/message——只补来源标注(缺 abortInfo 才补)
310
+ throw annotateAbort(Object.assign(new Error("User interrupted"), { name: "AbortError" }), signal, "agent", "interrupted-response")
290
311
  }
291
312
 
292
313
  if (response.usage) {
@@ -318,7 +339,7 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
318
339
  }
319
340
 
320
341
  // abort after chat completes, before committing history: don't commit a half-finished turn
321
- if (signal?.aborted) throw new DOMException("Aborted", "AbortError")
342
+ if (signal?.aborted) throw abortError(signal, "agent", "post-chat")
322
343
 
323
344
  pushReal(agent, {
324
345
  role: "assistant",
@@ -391,6 +412,6 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
391
412
  // 2026-09-05 实践轮:回合收尾(consult 清理/async 池分流/guard 继承——原 425-462
392
413
  // 段 + collectSettledAsync 466-494 整体迁 agent/run-stages.mjs finalizeAgentTurn——
393
414
  // CLI 对位 VS run-stages——finally 只剩一行调用 + 骨架注释)。
394
- await finalizeAgentTurn(agent, { signal, autoTurn, suspDriven, thrownError })
415
+ await finalizeAgentTurn(agent, { signal, autoTurn, suspDriven, thrownError, depth })
395
416
  }
396
417
  }
@@ -1,4 +1,4 @@
1
- import { join } from "node:path"
1
+ import { isAbsolute, join } from "node:path"
2
2
  import { loadConfig, configPath } from "../config.mjs"
3
3
  import { createMemory } from "../memory.mjs"
4
4
  import { teamConfig, gitAuthor } from "./make-agent.mjs"
@@ -64,7 +64,7 @@ export async function distillCommand(args, exitSoon) {
64
64
  }
65
65
 
66
66
  const opts = {
67
- projectDir: config.memory.projectDir ? join(process.cwd(), config.memory.projectDir) : null,
67
+ projectDir: config.memory.projectDir ? (isAbsolute(config.memory.projectDir) ? config.memory.projectDir : join(process.cwd(), config.memory.projectDir)) : null,
68
68
  team,
69
69
  author: gitAuthor(),
70
70
  }
@@ -1,5 +1,5 @@
1
1
  import { execSync } from "node:child_process"
2
- import { join } from "node:path"
2
+ import { isAbsolute, join } from "node:path"
3
3
  import { createAgent } from "../agent.mjs"
4
4
  import { loadConfig, configDir } from "../config.mjs"
5
5
  import { createMemory, memoryTools, syncDir, codeSearchTool, docSearchTool } from "../memory.mjs"
@@ -10,8 +10,18 @@ import { discoverRules } from "../rules.mjs"
10
10
  // R10 L2(MULTI-INSTANCE-COLLAB §2a.4 D-L2b):peer_instances 只读工具——挂感知模块导出
11
11
  import { peerInstancesTool } from "../peer-instances.mjs"
12
12
 
13
+ /** 第 27 批 §12.3⑤(R-A1.1):装配期工具剔除——按 `name` 过滤的纯函数(机验锚)。
14
+ * 恒等语义:空列表 / 零命中 → 原数组原样返回(零意外剔除——T15)。
15
+ * ACP 通道经 `assembleAgent({ excludeTools })` 传入(acp.mjs `ACP_EXCLUDED_TOOLS`)。 */
16
+ export function applyToolExclusions(tools, excludeTools = []) {
17
+ const excluded = new Set(excludeTools ?? [])
18
+ if (excluded.size === 0) return tools
19
+ const kept = tools.filter((t) => !excluded.has(t?.name))
20
+ return kept.length === tools.length ? tools : kept
21
+ }
22
+
13
23
  /** Assemble an agent with memory, MCP tools, and code/doc indices attached (sync all layers, then return) */
14
- export async function assembleAgent() {
24
+ export async function assembleAgent({ excludeTools = [] } = {}) {
15
25
  const config = loadConfig()
16
26
  const provider = config.provider
17
27
  const providers = config.providersList
@@ -20,7 +30,7 @@ export async function assembleAgent() {
20
30
  const { injectProxy } = await import("../proxy.mjs")
21
31
  injectProxy(providers, config)
22
32
  // config.provider 是 loadConfig 里的独立拷贝,同步注入结果
23
- provider.proxyUri = providers.find((p) => p.name === config.activeProvider)?.proxyUri
33
+ if (provider?.name) provider.proxyUri = providers.find((p) => p.name === provider.name)?.proxyUri
24
34
 
25
35
  const memory = createMemory({ dbPath: config.memory.dbPath })
26
36
  // Vector retrieval: enabled if embedding is configured (lazy vector generation, computed on first search)
@@ -41,7 +51,7 @@ export async function assembleAgent() {
41
51
  memory.codeOrigin = cwd
42
52
  // Project layer: sync .thincoder/memory/ dir to index on startup (sync if present, skip otherwise)
43
53
  if (config.memory.projectDir) {
44
- memory.projectOrigin = join(cwd, config.memory.projectDir)
54
+ memory.projectOrigin = isAbsolute(config.memory.projectDir) ? config.memory.projectDir : join(cwd, config.memory.projectDir)
45
55
  await syncDir(memory, { layer: "project", dir: memory.projectOrigin })
46
56
  }
47
57
  // Team layer (optional): auto-clone on first use; startup only indexes local dir, remote pull via explicit thincoder sync
@@ -101,18 +111,24 @@ export async function assembleAgent() {
101
111
 
102
112
  const agent = createAgent({
103
113
  provider,
104
- tools: [...baseTools, ...mcpTools],
114
+ tools: applyToolExclusions([...baseTools, ...mcpTools], excludeTools),
105
115
  config,
106
116
  cwd,
107
117
  memory,
108
118
  })
109
119
  agent.providers = providers
110
- agent.activeProvider = config.activeProvider
111
- agent.activeModel = config.activeModel ?? null
120
+ // MODEL-MERGE-SESSION:config 无 active*——会话运行时起点 = defaultModel 复合解析
121
+ // (provider 对象带解析后 model——API/spec 消费点零改);无效 → {} + 原因走 D-S1 标记
122
+ agent.activeProvider = provider.name ?? ""
123
+ agent.activeModel = provider.model ?? null
112
124
  agent._mcpWarnings = mcpWarnings
113
125
  // SESSION.md §8 D-S1:assembleAgent 后唯一校验点(TUI/chat 两路径同源)——不抛错不退出,
114
126
  // 标记由调用侧消费(TUI 弹重选 / headless 报错)。空 provider 由 TUI 路径在 startTUI 前清空。
115
127
  validateProvider(agent)
128
+ // defaultModel 无效/未设的具体原因覆盖通用判据文案(providers 存在时更有指导性)
129
+ if (agent._providerInvalid && config.providerInvalidReason) {
130
+ agent._providerInvalidReason = config.providerInvalidReason
131
+ }
116
132
  return agent
117
133
  }
118
134
 
@@ -1,4 +1,4 @@
1
- import { join } from "node:path"
1
+ import { isAbsolute, join } from "node:path"
2
2
  import { loadConfig } from "../config.mjs"
3
3
  import { teamConfig } from "./make-agent.mjs"
4
4
  import { put, search, list } from "../memory/core.mjs"
@@ -67,7 +67,7 @@ export async function memoryCommand(memory, args, opts = {}) {
67
67
  function cliDirs() {
68
68
  const config = loadConfig()
69
69
  const dirs = { project: null, team: null }
70
- if (config.memory?.projectDir) dirs.project = join(process.cwd(), config.memory.projectDir)
70
+ if (config.memory?.projectDir) dirs.project = isAbsolute(config.memory.projectDir) ? config.memory.projectDir : join(process.cwd(), config.memory.projectDir)
71
71
  const team = teamConfig(config)
72
72
  if (team) dirs.team = team.dir
73
73
  return dirs
@@ -1,5 +1,6 @@
1
1
  import { createInterface } from "node:readline"
2
2
  import { configPath, writeConfigAtomic, PROVIDER_PRESETS } from "../config.mjs"
3
+ import { probeChannelModels } from "../tui/model-catalog.mjs"
3
4
 
4
5
  /** First-time setup (TTY chat / distill): ask a few questions to configure a provider, save to disk, return runtime provider. Cancel returns null. */
5
6
  export async function setupWizard() {
@@ -28,7 +29,7 @@ export async function setupWizard() {
28
29
  presets.forEach(([n, p], i) => console.error(` ${i + 1}. ${n.padEnd(10)} ${p.desc}`))
29
30
  console.error(` ${presets.length + 1}. Custom endpoint`)
30
31
  const choice = Number((await ask(`Pick [1-${presets.length + 1}]: `)).trim())
31
- let name, baseURL, model
32
+ let name, baseURL, model, presetFields = null
32
33
  if (choice === presets.length + 1) {
33
34
  name = (await ask("Name (e.g. my-openai): ")).trim()
34
35
  baseURL = (await ask("baseURL (e.g. https://api.openai.com/v1): ")).trim().replace(/\/+$/, "")
@@ -38,9 +39,11 @@ export async function setupWizard() {
38
39
  return null
39
40
  }
40
41
  } else if (choice >= 1 && choice <= presets.length) {
42
+ // 预设全量字段随行(format/thinking/maxTokens…)——探针分派与落盘同源(M9 需要 format)
43
+ presetFields = presets[choice - 1][1]
41
44
  name = presets[choice - 1][0]
42
- baseURL = presets[choice - 1][1].baseURL
43
- model = presets[choice - 1][1].model
45
+ baseURL = presetFields.baseURL
46
+ model = presetFields.model
44
47
  } else {
45
48
  console.error("Invalid choice — cancelled")
46
49
  return null
@@ -51,25 +54,42 @@ export async function setupWizard() {
51
54
  return null
52
55
  }
53
56
  const embedKey = (await ask("Optional: embedding API key (SiliconFlow, for vector search; press Enter to skip): ")).trim()
57
+ // M9 配置阶段准入(加渠道 = 配置写入面):探一次 `/models`——探通/探不通都不阻断保存,
58
+ // 仅明示结果(失败文案 = M8 消息本体;无绕过指引)。
59
+ // 探针输入带预设扩展字段(format 决定 M1 分派——claude/gemini 预设必须走自己的拉取分支)。
60
+ const probe = await probeChannelModels(presetFields ? { ...presetFields, name, apiKey } : { name, baseURL, apiKey })
61
+ console.error(probe.ok
62
+ ? `✓ ${name}: /models 可用(${probe.list.length} 个模型可候选)`
63
+ : `⚠ ${probe.message}`)
54
64
  // D-F5b:磁盘新鲜读 → mutate → mtime 门控写(writeConfigAtomic 收口);冲突 = 放弃
55
65
  // + 提示重试(首配场景另有实例同时写盘——极低概率;不自动合并——决策点① A)
66
+ // MODEL-SELECTION v2:渠道默认模型 = 单值 `model` + defaultModel 顶层复合;
67
+ // 预设扩展字段(format/thinking/maxTokens…)随行落盘——与 TUI wizard 近似同构(探针/聊天字段同源),
68
+ // 但 `thinking` 复制语义不同:本文件用 `!== undefined` → kimi/kimi-code 预设的 `thinking: null`
69
+ // **被落盘**;TUI 路径用真值过滤(model-picker.mjs / wizard.mjs)不落该键。
70
+ // `thinking: null`(NF1 = 显式 off)落盘差异已登记待口径统一(父侧)。
56
71
  const r = writeConfigAtomic(configPath, (raw) => {
57
72
  const providers = raw.providers?.length ? raw.providers : []
58
73
  const existing = providers.find((p) => p.name === name)
59
- if (existing) Object.assign(existing, { baseURL, model, apiKey })
60
- else providers.push({ name, baseURL, model, apiKey })
74
+ const rec = { name, baseURL, model, apiKey }
75
+ for (const k of ["format", "thinking", "reasoningEffort", "maxTokens", "chatPath"]) {
76
+ if (presetFields?.[k] !== undefined) rec[k] = presetFields[k]
77
+ }
78
+ if (existing) Object.assign(existing, rec) // 渠道老字段(models 候选清单)由 config-migrate 在下次 load 统一清理
79
+ else providers.push(rec)
61
80
  raw.providers = providers
62
- raw.activeProvider = name
63
- delete raw.activeModel // reset to default model
81
+ raw.defaultModel = `${name}:${model}`
82
+ delete raw.activeProvider
83
+ delete raw.activeModel
64
84
  if (embedKey) raw.embedding = { ...(raw.embedding ?? {}), apiKey: embedKey }
65
85
  })
66
86
  if (!r.ok) {
67
87
  console.error("config changed on disk concurrently — retry")
68
88
  return null
69
89
  }
70
- console.error(`Configured: ${name} / ${model} (saved to ${configPath})`)
90
+ console.error(`Configured: ${name} / ${model} (defaultModel = ${name}:${model} — saved to ${configPath})`)
71
91
  console.error(embedKey ? "Vector search enabled\n" : "(No embedding key configured: memory search will use text-only FTS. Add embedding.apiKey to config.json to enable vector search later.)\n")
72
- return { name, baseURL, model, apiKey }
92
+ return { name, baseURL, model, apiKey, defaultModel: `${name}:${model}` }
73
93
  } finally {
74
94
  rl.close()
75
95
  }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * completions.mjs — `thincoder completion <shell>` shell 补全脚本发射(bash/zsh/fish)。
3
+ * bin/thincoder.mjs case "completion" 直写段拆分(2026-09-08——500 行硬限触碰执行:
4
+ * 段逐字迁出,脚本字节与既有输出一致)。
5
+ */
6
+ export function printCompletion(shell) {
7
+ if (shell === "bash") {
8
+ process.stdout.write(`_thincoder() {
9
+ local cur prev words cword
10
+ _init_completion 2>/dev/null || { COMPREPLY=(); return; }
11
+ cur="\${COMP_WORDS[COMP_CWORD]}"
12
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
13
+ case "\${COMP_WORDS[1]}" in
14
+ chat) COMPREPLY=( \\$(compgen -W "--auto" -- "\\$cur") ) ;;
15
+ memory)
16
+ case "\\$prev" in
17
+ memory) COMPREPLY=( \\$(compgen -W "list search put remove" -- "\\$cur") ) ;;
18
+ list) COMPREPLY=( \\$(compgen -W "--type=rule --type=knowledge --type=decision --type=pattern" -- "\\$cur") ) ;;
19
+ put) COMPREPLY=( \\$(compgen -W "--type= --title= --content= --tags=" -- "\\$cur") ) ;;
20
+ esac ;;
21
+ distill) COMPREPLY=( \\$(compgen -W "--yes --layer=" -- "\\$cur") ) ;;
22
+ completion) COMPREPLY=( \\$(compgen -W "bash zsh fish" -- "\\$cur") ) ;;
23
+ *)
24
+ COMPREPLY=( \\$(compgen -W "chat acp memory sync reindex distill upgrade completion session -v --version -h --help" -- "\\$cur") ) ;;
25
+ esac
26
+ }
27
+ complete -F _thincoder thincoder
28
+ `)
29
+ } else if (shell === "zsh") {
30
+ process.stdout.write(`#compdef thincoder
31
+
32
+ _thincoder() {
33
+ local context state state_descr line
34
+ typeset -A opt_args
35
+ _arguments -C \\
36
+ '1: :->cmd' \\
37
+ '*:: :->args'
38
+
39
+ case "\\$state" in
40
+ cmd)
41
+ _values 'command' \\
42
+ 'chat[One-shot agent run with tools]' \\
43
+ 'acp[Agent Client Protocol server for IDEs]' \\
44
+ 'memory[Manage long-term memory]' \\
45
+ 'sync[Sync team memory repo]' \\
46
+ 'reindex[Rebuild local index from markdown]' \\
47
+ 'distill[Extract knowledge from session transcript]' \\
48
+ 'upgrade[Update to latest version from npm]' \\
49
+ 'completion[Generate shell completion script]' \\
50
+ 'session[Session dir GC: session gc --dry-run|--confirm]'
51
+ ;;
52
+ args)
53
+ case "\\$words[1]" in
54
+ chat) _arguments '--auto[Auto-approve all tool calls]' ;;
55
+ memory)
56
+ case "\\$words[2]" in
57
+ list) _arguments '--type=[Filter by type]' ;;
58
+ put) _arguments '--type=[Entry type]' '--title=[Title]' '--content=[Content]' '--tags=[Space-separated tags]' ;;
59
+ esac ;;
60
+ distill) _arguments '--yes[Skip confirmation]' '--layer=[Layer filter]' ;;
61
+ completion) _values 'shell' 'bash' 'zsh' 'fish' ;;
62
+ esac ;;
63
+ esac
64
+ }
65
+ _thincoder
66
+ `)
67
+ } else if (shell === "fish") {
68
+ process.stdout.write(`# thincoder completions for fish shell
69
+ complete -c thincoder -f
70
+
71
+ # Subcommands
72
+ complete -c thincoder -a chat -d 'One-shot agent run with tools'
73
+ complete -c thincoder -a memory -d 'Manage long-term memory'
74
+ complete -c thincoder -a sync -d 'Sync team memory repo'
75
+ complete -c thincoder -a reindex -d 'Rebuild local index from markdown'
76
+ complete -c thincoder -a distill -d 'Extract knowledge from session'
77
+ complete -c thincoder -a upgrade -d 'Update to latest version'
78
+ complete -c thincoder -a completion -d 'Shell completion'
79
+ complete -c thincoder -a session -d 'Session dir GC (session gc --dry-run|--confirm)'
80
+ complete -c thincoder -a acp -d 'Agent Client Protocol server for IDEs'
81
+
82
+ # Flags
83
+ complete -c thincoder -s v -l version -d 'Print version'
84
+ complete -c thincoder -s h -l help -d 'Print help'
85
+
86
+ # chat flags
87
+ complete -c thincoder -n '__fish_seen_subcommand_from chat' -l auto -d 'Auto-approve tool calls'
88
+
89
+ # memory subcommands
90
+ complete -c thincoder -n '__fish_seen_subcommand_from memory' -a list -d 'List entries'
91
+ complete -c thincoder -n '__fish_seen_subcommand_from memory' -a search -d 'Search memory'
92
+ complete -c thincoder -n '__fish_seen_subcommand_from memory' -a put -d 'Add entry'
93
+ complete -c thincoder -n '__fish_seen_subcommand_from memory' -a remove -d 'Remove entry'
94
+
95
+ # memory list flags
96
+ complete -c thincoder -n '__fish_seen_subcommand_from memory; and __fish_seen_subcommand_from list' -l type -d 'Filter by type' -xa 'rule knowledge decision pattern'
97
+
98
+ # memory put flags
99
+ complete -c thincoder -n '__fish_seen_subcommand_from memory; and __fish_seen_subcommand_from put' -l type -d 'Entry type'
100
+ complete -c thincoder -n '__fish_seen_subcommand_from memory; and __fish_seen_subcommand_from put' -l title -d 'Title'
101
+ complete -c thincoder -n '__fish_seen_subcommand_from memory; and __fish_seen_subcommand_from put' -l content -d 'Content'
102
+ complete -c thincoder -n '__fish_seen_subcommand_from memory; and __fish_seen_subcommand_from put' -l tags -d 'Space-separated tags'
103
+
104
+ # distill flags
105
+ complete -c thincoder -n '__fish_seen_subcommand_from distill' -l yes -d 'Skip confirmation'
106
+ complete -c thincoder -n '__fish_seen_subcommand_from distill' -l layer -d 'Layer filter'
107
+
108
+ # completion shells
109
+ complete -c thincoder -n '__fish_seen_subcommand_from completion' -a bash -d 'Bash completions'
110
+ complete -c thincoder -n '__fish_seen_subcommand_from completion' -a zsh -d 'Zsh completions'
111
+ complete -c thincoder -n '__fish_seen_subcommand_from completion' -a fish -d 'Fish completions'
112
+ `)
113
+ }
114
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * config-migrate.mjs — model-schema legacy-shape migration core (MODEL-SELECTION v2, §16.2 M7).
3
+ * Split out of config.mjs (500-line hard limit). Pure function, zero imports — the
4
+ * write-back orchestration (writeConfigAtomic / persistRaw, failure tolerance) stays in
5
+ * the caller (CLI: config.mjs loadConfig; VSC: config-io.mjs loadRaw — same rule, 双端各自
6
+ * 独立实现——不做同步依赖).
7
+ *
8
+ * 目标形态 C:每渠道恰好一个默认模型(`providers[].model` 单值)+ 顶层 `defaultModel` 复合。
9
+ * 老形态迁移(v2):
10
+ * - A:`providers[].model` + `activeProvider/activeModel` → `p.model` **保留**(不再搬入 models);
11
+ * `activeProvider/activeModel` → `defaultModel` 复合(`activeModel` 优先;AP 不存在/AM 空 → 首渠道回退)
12
+ * - B:`providers[].models[]` → `p.model = defaultModel 属本渠道的模型段 ?? 现有 p.model(非空字符串)
13
+ * ?? models[] 首个非空字符串`,然后 `delete p.models`
14
+ * - 混合/垃圾:非数组 `p.models` / 非字符串(空串)`p.model` → 删除(清理)
15
+ * - 顺序:先构造/读取有效 `defaultModel` 值(含老 active* 转换),再按它给各渠道播种 `p.model`
16
+ * - 幂等:无老字段(无 `models` / 无 active* / 无垃圾值)→ 返回 false 不动;slot 旧字段
17
+ * (会话文件内)不迁移——读侧容忍
18
+ * - 空结果合法:渠道无模型来源 → `p.model` 不设(模型选择经 `/models` 拉取候选——M8/M9)
19
+ */
20
+ export function migrateLegacyModelFields(raw) {
21
+ if (!raw || typeof raw !== "object") return false
22
+ const providers = Array.isArray(raw.providers) ? raw.providers : []
23
+ let changed = false
24
+
25
+ // ── ① 有效 defaultModel 值(含老 active* 转换)──
26
+ if (raw.activeProvider !== undefined || raw.activeModel !== undefined) {
27
+ const rawAP = typeof raw.activeProvider === "string" && raw.activeProvider.trim() ? raw.activeProvider : ""
28
+ const rawAM = typeof raw.activeModel === "string" && raw.activeModel.trim() ? raw.activeModel : ""
29
+ const ap = rawAP ? providers.find((p) => p?.name === rawAP) : null
30
+ if (ap && rawAM) {
31
+ if (raw.defaultModel == null) { raw.defaultModel = `${rawAP}:${rawAM}`; changed = true }
32
+ } else {
33
+ // AP 已不存在 / AM 无法归属 → 首渠道回退(该渠道有模型来源才可用——迁移产物必可用)
34
+ const first = providers.find((p) => seedModel(p) != null)
35
+ if (first && raw.defaultModel == null) { raw.defaultModel = `${first.name}:${seedModel(first)}`; changed = true }
36
+ }
37
+ delete raw.activeProvider
38
+ delete raw.activeModel
39
+ changed = true
40
+ }
41
+
42
+ // ── ② 渠道播种单值 `p.model` + 清 models ──
43
+ const dm = typeof raw.defaultModel === "string" && raw.defaultModel.trim() ? raw.defaultModel : null
44
+ const dmSep = dm ? dm.indexOf(":") : -1
45
+ const dmProvider = dmSep > 0 ? dm.slice(0, dmSep) : null
46
+ const dmModel = dmSep > 0 && dmSep < dm.length - 1 ? dm.slice(dmSep + 1) : null
47
+ for (const p of providers) {
48
+ if (!p || typeof p !== "object") continue
49
+ if (Array.isArray(p.models)) {
50
+ const next = (p.name === dmProvider && dmModel)
51
+ ? dmModel
52
+ : (typeof p.model === "string" && p.model.trim() ? p.model : seedModel(p))
53
+ if (next == null) delete p.model
54
+ else p.model = next
55
+ delete p.models
56
+ changed = true
57
+ continue
58
+ }
59
+ if (p.models !== undefined) { delete p.models; changed = true } // 非数组 models —— 垃圾清理
60
+ if (p.model !== undefined && !(typeof p.model === "string" && p.model.trim())) { delete p.model; changed = true }
61
+ }
62
+ return changed
63
+ }
64
+
65
+ /** 渠道级模型来源读数(迁移用):现有 `p.model`(非空字符串优先)或 `models[]` 首个非空字符串。 */
66
+ function seedModel(p) {
67
+ if (typeof p?.model === "string" && p.model.trim()) return p.model
68
+ const models = Array.isArray(p?.models) ? p.models : []
69
+ return models.find((m) => typeof m === "string" && m.trim()) ?? null
70
+ }