one-code-extension 0.4.0 → 0.5.0

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 (419) hide show
  1. package/README.md +219 -219
  2. package/agents/general-purpose.md +2 -2
  3. package/docs/guide/README.md +2 -1
  4. package/docs/guide/bring-your-claude-code-setup.md +60 -9
  5. package/docs/guide/configuration.md +48 -14
  6. package/docs/guide/differences-from-claude-code.md +32 -23
  7. package/docs/guide/doctor.md +20 -13
  8. package/docs/guide/hooks.md +18 -3
  9. package/docs/guide/installation.md +27 -4
  10. package/docs/guide/permissions-modes-and-auto-mode.md +225 -49
  11. package/docs/guide/providers-and-models.md +137 -54
  12. package/docs/guide/reference.md +27 -15
  13. package/docs/guide/sessions-and-context.md +69 -16
  14. package/docs/guide/skills-plugins-and-mcp.md +57 -20
  15. package/docs/guide/subagents-and-workflows.md +95 -29
  16. package/docs/guide/tasks-and-background-work.md +101 -26
  17. package/docs/guide/terminal-interface.md +24 -12
  18. package/docs/guide/tools.md +46 -5
  19. package/docs/guide/troubleshooting.md +13 -9
  20. package/docs/guide/windows.md +66 -34
  21. package/extensions/artifacts/gallery.ts +227 -0
  22. package/extensions/artifacts/index.ts +288 -0
  23. package/extensions/artifacts/store.ts +570 -0
  24. package/extensions/artifacts/viewer.ts +199 -0
  25. package/extensions/ask-user/description.ts +67 -0
  26. package/extensions/ask-user/dialogs.ts +80 -0
  27. package/extensions/ask-user/index.ts +37 -33
  28. package/extensions/ask-user/questions.ts +32 -7
  29. package/extensions/ask-user/widget.ts +45 -25
  30. package/extensions/auto-mode/actions.ts +6 -0
  31. package/extensions/auto-mode/cache.ts +88 -0
  32. package/extensions/auto-mode/classifier-prompt.ts +5 -4
  33. package/extensions/auto-mode/classifier.ts +290 -65
  34. package/extensions/auto-mode/config.ts +72 -20
  35. package/extensions/auto-mode/decision-log.ts +2 -0
  36. package/extensions/auto-mode/defaults.ts +1 -1
  37. package/extensions/auto-mode/denials.ts +123 -0
  38. package/extensions/auto-mode/git-checkout-programs.ts +187 -0
  39. package/extensions/auto-mode/git-status-meta.ts +159 -0
  40. package/extensions/auto-mode/history.ts +189 -0
  41. package/extensions/auto-mode/instructions.ts +58 -52
  42. package/extensions/auto-mode/model-select.ts +159 -134
  43. package/extensions/auto-mode/paths.ts +57 -5
  44. package/extensions/auto-mode/pause.ts +57 -2
  45. package/extensions/auto-mode/prompt.ts +37 -21
  46. package/extensions/auto-mode/read-only-options.ts +724 -0
  47. package/extensions/auto-mode/resolved-paths-meta.ts +133 -0
  48. package/extensions/auto-mode/rules.ts +18 -1
  49. package/extensions/auto-mode/safety-floor.ts +361 -18
  50. package/extensions/auto-mode/setup-run.ts +20 -14
  51. package/extensions/auto-mode/shell-analysis.ts +764 -490
  52. package/extensions/auto-mode/shell-parse.ts +885 -0
  53. package/extensions/auto-mode/system-note.ts +33 -0
  54. package/extensions/auto-mode/transcript.ts +80 -51
  55. package/extensions/background/cron.ts +515 -0
  56. package/extensions/background/index.ts +514 -236
  57. package/extensions/background/loop-fire.ts +136 -0
  58. package/extensions/background/loop-preamble.ts +59 -0
  59. package/extensions/background/loop-skill.ts +163 -0
  60. package/extensions/background/monitor-batch.ts +53 -0
  61. package/extensions/background/noop-fold.ts +128 -0
  62. package/extensions/background/switch-guard.ts +52 -0
  63. package/extensions/background/wakeup.ts +159 -55
  64. package/extensions/bash/background.ts +53 -17
  65. package/extensions/bash/child.ts +7 -0
  66. package/extensions/bash/description.ts +188 -0
  67. package/extensions/bash/guards.ts +15 -67
  68. package/extensions/bash/index.ts +88 -35
  69. package/extensions/branding/compaction-keep.ts +63 -0
  70. package/extensions/branding/declined.ts +22 -0
  71. package/extensions/branding/index.ts +133 -9
  72. package/extensions/branding/prompt-editor.ts +53 -6
  73. package/extensions/branding/queued-edit.ts +31 -0
  74. package/extensions/branding/replaced-builtins.ts +68 -0
  75. package/extensions/branding/startup.ts +33 -71
  76. package/extensions/btw/index.ts +229 -71
  77. package/extensions/btw/panel.ts +228 -100
  78. package/extensions/btw/prompt.ts +41 -4
  79. package/extensions/claude-compat/index.ts +7 -11
  80. package/extensions/claude-context/index.ts +446 -58
  81. package/extensions/compaction/cut.ts +157 -0
  82. package/extensions/compaction/index.ts +216 -75
  83. package/extensions/compaction/kept-thinking.ts +62 -0
  84. package/extensions/compaction/prompt.ts +15 -12
  85. package/extensions/context-budget/index.ts +24 -5
  86. package/extensions/context-management/index.ts +25 -0
  87. package/extensions/doctor/build.ts +59 -8
  88. package/extensions/doctor/builtins.ts +84 -0
  89. package/extensions/doctor/cli.ts +7 -1
  90. package/extensions/doctor/compat.ts +88 -23
  91. package/extensions/doctor/dependencies.ts +41 -5
  92. package/extensions/doctor/fix-prompt.ts +9 -2
  93. package/extensions/doctor/index.ts +102 -58
  94. package/extensions/doctor/models.ts +102 -63
  95. package/extensions/doctor/presets.ts +42 -27
  96. package/extensions/doctor/report.ts +19 -2
  97. package/extensions/doctor/viewer.ts +49 -27
  98. package/extensions/effort/index.ts +72 -19
  99. package/extensions/effort/settings.ts +21 -0
  100. package/extensions/effort/slider.ts +24 -20
  101. package/extensions/exit/index.ts +32 -1
  102. package/extensions/file-tracker/index.ts +337 -26
  103. package/extensions/file-tracker/replay.ts +96 -10
  104. package/extensions/file-tracker/restore.ts +63 -0
  105. package/extensions/file-tracker/results.ts +66 -0
  106. package/extensions/file-tracker/shell-reads.ts +211 -0
  107. package/extensions/file-tracker/tracker.ts +32 -15
  108. package/extensions/footer/footer-line.ts +45 -24
  109. package/extensions/footer/index.ts +47 -2
  110. package/extensions/footer/pr.ts +34 -5
  111. package/extensions/hooks/compaction.ts +7 -0
  112. package/extensions/hooks/executor.ts +32 -30
  113. package/extensions/hooks/index.ts +198 -31
  114. package/extensions/hooks/matcher.ts +3 -0
  115. package/extensions/hooks/protocol.ts +41 -3
  116. package/extensions/hooks/settings.ts +50 -19
  117. package/extensions/hooks/subagent-bridge.ts +2 -0
  118. package/extensions/hooks/trust.ts +13 -5
  119. package/extensions/idle-compact/index.ts +119 -0
  120. package/extensions/idle-compact/policy.ts +143 -0
  121. package/extensions/init/index.ts +11 -10
  122. package/extensions/init/prompt.ts +25 -32
  123. package/extensions/interrupted/index.ts +13 -1
  124. package/extensions/kept-thinking/index.ts +27 -0
  125. package/extensions/lib/agent-cron.ts +130 -0
  126. package/extensions/lib/agent-loader.ts +15 -1
  127. package/extensions/lib/agent-view.ts +85 -0
  128. package/extensions/lib/anthropic-payload.ts +82 -1
  129. package/extensions/lib/anthropic-server-call.ts +230 -0
  130. package/extensions/lib/anthropic-server-tools.ts +485 -0
  131. package/extensions/lib/app-launch.mjs +94 -0
  132. package/extensions/lib/argument-hints.ts +190 -0
  133. package/extensions/lib/atomic-write.ts +6 -2
  134. package/extensions/lib/auto-memory.ts +75 -0
  135. package/extensions/lib/bash-parser.ts +84 -0
  136. package/extensions/lib/btw-fork.ts +93 -0
  137. package/extensions/lib/child-extensions.ts +42 -0
  138. package/extensions/lib/child-writes.ts +45 -0
  139. package/extensions/lib/claude-context.ts +515 -100
  140. package/extensions/lib/claude-external-includes.ts +68 -0
  141. package/extensions/lib/claude-id.ts +22 -0
  142. package/extensions/lib/claude-rules.ts +323 -0
  143. package/extensions/lib/claude-settings.ts +76 -4
  144. package/extensions/lib/compaction-boundary.ts +36 -0
  145. package/extensions/lib/compaction-keep.mjs +83 -0
  146. package/extensions/lib/config-mode.ts +112 -0
  147. package/extensions/lib/consent-dialogs.ts +63 -0
  148. package/extensions/lib/consent-preview.ts +5 -1
  149. package/extensions/lib/consent-stores.ts +48 -0
  150. package/extensions/lib/context-facts.ts +78 -0
  151. package/extensions/lib/context-stack.ts +81 -0
  152. package/extensions/lib/deferred.ts +219 -25
  153. package/extensions/lib/dock-rows.ts +60 -0
  154. package/extensions/lib/empty-tool-result.ts +27 -0
  155. package/extensions/lib/environment-block.ts +76 -0
  156. package/extensions/lib/fetch-timeout.ts +24 -6
  157. package/extensions/{system-prompt → lib}/git-status.ts +51 -13
  158. package/extensions/lib/git.ts +75 -13
  159. package/extensions/lib/grammars/LICENSE-tree-sitter-bash +21 -0
  160. package/extensions/lib/grammars/README.md +2 -0
  161. package/extensions/lib/grammars/tree-sitter-bash.wasm +0 -0
  162. package/extensions/lib/guide-docs.ts +45 -0
  163. package/extensions/lib/headless-output.ts +46 -0
  164. package/extensions/lib/hook-gate.ts +7 -7
  165. package/extensions/lib/interrupt.ts +20 -4
  166. package/extensions/lib/key-input.ts +455 -0
  167. package/extensions/lib/keys.ts +14 -1
  168. package/extensions/lib/local-command.ts +47 -13
  169. package/extensions/lib/mcp-overrides.ts +1 -1
  170. package/extensions/lib/mcp-share.ts +46 -0
  171. package/extensions/lib/memory-content.ts +146 -0
  172. package/extensions/lib/memory-index.ts +18 -0
  173. package/extensions/lib/memory.ts +134 -123
  174. package/extensions/lib/model-catalog/huggingface.json +174 -0
  175. package/extensions/lib/model-catalog/models-dev.json +18297 -0
  176. package/extensions/lib/model-catalog/openrouter.json +5252 -0
  177. package/extensions/lib/model-catalog-data.ts +513 -0
  178. package/extensions/lib/model-catalog.ts +462 -0
  179. package/extensions/lib/model-choice-warnings.ts +137 -0
  180. package/extensions/lib/model-pairings.ts +152 -0
  181. package/extensions/{auto-mode → lib}/model-picker.ts +26 -43
  182. package/extensions/lib/model-policy.ts +112 -15
  183. package/extensions/lib/model-tier.ts +283 -355
  184. package/extensions/lib/model-unusable.ts +1 -1
  185. package/extensions/lib/newer-model.ts +100 -0
  186. package/extensions/lib/notices.ts +87 -0
  187. package/extensions/lib/notifications.ts +308 -64
  188. package/extensions/lib/one-code-settings.ts +74 -8
  189. package/extensions/lib/open-browser.ts +63 -0
  190. package/extensions/lib/openrouter-generation.ts +94 -0
  191. package/extensions/lib/operation-deadline.ts +30 -0
  192. package/extensions/lib/original-command.ts +4 -4
  193. package/extensions/lib/package-version.ts +1 -1
  194. package/extensions/lib/panel-focus.ts +134 -0
  195. package/extensions/lib/paths.ts +17 -2
  196. package/extensions/lib/permission-gate.ts +72 -6
  197. package/extensions/lib/persisted-output.ts +60 -8
  198. package/extensions/lib/pi-ai-estimate.ts +19 -3
  199. package/extensions/lib/pi-version.ts +21 -2
  200. package/extensions/lib/plan-mode-channels.ts +20 -5
  201. package/extensions/lib/plugins.ts +26 -17
  202. package/extensions/lib/powershell-parse-script.ts +208 -0
  203. package/extensions/lib/powershell-parser.ts +344 -0
  204. package/extensions/lib/process-tree.ts +131 -8
  205. package/extensions/lib/prompt-options.ts +82 -0
  206. package/extensions/lib/queued-delivery.ts +44 -0
  207. package/extensions/lib/reminders.ts +364 -58
  208. package/extensions/lib/replaced-builtins.mjs +199 -0
  209. package/extensions/lib/replay-call.ts +70 -0
  210. package/extensions/lib/request-replay.ts +439 -0
  211. package/extensions/lib/scratchpad.ts +81 -14
  212. package/extensions/lib/send-now-channel.ts +7 -0
  213. package/extensions/lib/session-model-tier.ts +51 -0
  214. package/extensions/lib/session-work.ts +64 -0
  215. package/extensions/lib/settings-channels.ts +4 -4
  216. package/extensions/lib/shell-exit-semantics.ts +170 -0
  217. package/extensions/lib/shell-quote.ts +11 -0
  218. package/extensions/lib/shell-result.ts +70 -0
  219. package/extensions/lib/shell-spawn.ts +70 -6
  220. package/extensions/lib/shell-tool.ts +75 -25
  221. package/extensions/lib/side-call-cache.ts +124 -0
  222. package/extensions/lib/side-call-run.ts +96 -0
  223. package/extensions/lib/side-call-usage.ts +38 -0
  224. package/extensions/lib/side-call.ts +13 -1
  225. package/extensions/lib/single-flight.ts +18 -0
  226. package/extensions/lib/skill-body.ts +42 -0
  227. package/extensions/lib/skill-scan.ts +63 -24
  228. package/extensions/lib/spill-file.ts +140 -0
  229. package/extensions/lib/subagent-handback.ts +113 -0
  230. package/extensions/lib/system-role.ts +156 -0
  231. package/extensions/lib/terminal-text.ts +114 -0
  232. package/extensions/lib/text-width.ts +47 -16
  233. package/extensions/lib/timer-ops.ts +20 -0
  234. package/extensions/lib/tool-additions.ts +149 -0
  235. package/extensions/lib/tool-call-corruption.ts +134 -0
  236. package/extensions/lib/tool-path.ts +66 -0
  237. package/extensions/lib/tool-variants.ts +69 -0
  238. package/extensions/lib/tui-render.ts +255 -27
  239. package/extensions/lib/turn-budget-layout.ts +143 -0
  240. package/extensions/lib/update-check.mjs +15 -2
  241. package/extensions/lib/usage-bus.ts +1 -1
  242. package/extensions/lib/user-denial.ts +38 -0
  243. package/extensions/lib/workspace-channel.ts +16 -0
  244. package/extensions/lib/worktree-channel.ts +31 -3
  245. package/extensions/lib/worktree-isolation.ts +40 -10
  246. package/extensions/lsp/client.ts +8 -9
  247. package/extensions/lsp/index.ts +70 -4
  248. package/extensions/lsp/keep-alive.ts +1 -1
  249. package/extensions/lsp/trust.ts +119 -0
  250. package/extensions/lsp/watcher.ts +1 -1
  251. package/extensions/mcp/announce.ts +87 -5
  252. package/extensions/mcp/client.ts +161 -48
  253. package/extensions/mcp/config.ts +132 -8
  254. package/extensions/mcp/discovery.ts +19 -0
  255. package/extensions/mcp/index.ts +209 -71
  256. package/extensions/mcp/oauth/browser.ts +38 -17
  257. package/extensions/mcp/oauth/flow.ts +25 -17
  258. package/extensions/mcp/oauth/provider.ts +14 -8
  259. package/extensions/mcp/oauth/store.ts +34 -16
  260. package/extensions/mcp/panel/keys.ts +12 -11
  261. package/extensions/mcp/panel/render.ts +9 -6
  262. package/extensions/mcp/schema.ts +104 -5
  263. package/extensions/mcp/trust.ts +151 -67
  264. package/extensions/memory/entries.ts +23 -14
  265. package/extensions/memory/index.ts +134 -28
  266. package/extensions/memory/open-external.ts +33 -18
  267. package/extensions/memory/panel.ts +135 -26
  268. package/extensions/newer-model/index.ts +31 -0
  269. package/extensions/notebook/description.ts +18 -0
  270. package/extensions/notebook/index.ts +6 -5
  271. package/extensions/notebook/notebook.ts +1 -1
  272. package/extensions/permissions/index.ts +1300 -381
  273. package/extensions/permissions/matcher.ts +425 -74
  274. package/extensions/permissions/modes.ts +6 -4
  275. package/extensions/permissions/panel/host.ts +130 -0
  276. package/extensions/permissions/panel/keys.ts +48 -0
  277. package/extensions/permissions/panel/render.ts +450 -0
  278. package/extensions/permissions/panel/state.ts +550 -0
  279. package/extensions/permissions/powershell-cmdlets.ts +198 -0
  280. package/extensions/permissions/powershell-paths.ts +197 -0
  281. package/extensions/permissions/powershell-rules.ts +207 -280
  282. package/extensions/permissions/powershell-tree.ts +557 -0
  283. package/extensions/permissions/project-trust.ts +27 -11
  284. package/extensions/permissions/protected-paths.ts +14 -2
  285. package/extensions/permissions/session-grant.ts +6 -0
  286. package/extensions/permissions/settings.ts +166 -31
  287. package/extensions/permissions/subagent-gate.ts +25 -0
  288. package/extensions/permissions/workspace.ts +78 -0
  289. package/extensions/plan-mode/index.ts +156 -69
  290. package/extensions/plan-mode/reminder.ts +89 -21
  291. package/extensions/plan-mode/texts.ts +177 -0
  292. package/extensions/plan-mode/viewer.ts +92 -27
  293. package/extensions/plugins/counts.ts +3 -3
  294. package/extensions/plugins/index.ts +61 -46
  295. package/extensions/plugins/install/install.ts +16 -0
  296. package/extensions/plugins/marketplace/sync.ts +9 -3
  297. package/extensions/plugins/marketplace/types.ts +3 -1
  298. package/extensions/plugins/panel/keys.ts +21 -24
  299. package/extensions/plugins/panel/render.ts +12 -6
  300. package/extensions/plugins/shell-expand.ts +107 -0
  301. package/extensions/plugins/template.ts +32 -15
  302. package/extensions/powershell/child.ts +7 -0
  303. package/extensions/powershell/description.ts +38 -6
  304. package/extensions/powershell/guards.ts +1 -1
  305. package/extensions/powershell/index.ts +12 -7
  306. package/extensions/recap/index.ts +65 -60
  307. package/extensions/recap/prompt.ts +2 -2
  308. package/extensions/recap/scheduler.ts +3 -5
  309. package/extensions/search-tools/grep-merge.ts +39 -0
  310. package/extensions/search-tools/index.ts +0 -0
  311. package/extensions/send-now/chord.ts +62 -0
  312. package/extensions/send-now/index.ts +133 -0
  313. package/extensions/session-title/index.ts +3 -0
  314. package/extensions/session-title/title.ts +4 -1
  315. package/extensions/skill/code-review.ts +97 -0
  316. package/extensions/skill/index.ts +421 -101
  317. package/extensions/skill/invoke.ts +128 -18
  318. package/extensions/skill/listing.ts +133 -9
  319. package/extensions/skill/panel/keys.ts +15 -16
  320. package/extensions/skill/panel/render.ts +5 -2
  321. package/extensions/skill/panel/state.ts +12 -1
  322. package/extensions/subagents/agent-args.ts +39 -0
  323. package/extensions/subagents/agent-description.ts +132 -0
  324. package/extensions/subagents/agents.ts +116 -23
  325. package/extensions/subagents/announce.ts +69 -0
  326. package/extensions/subagents/default-model.ts +2 -3
  327. package/extensions/subagents/delegation-steer.ts +4 -4
  328. package/extensions/subagents/fork-cache.ts +76 -0
  329. package/extensions/subagents/guide-agent.ts +189 -0
  330. package/extensions/subagents/hand-back-review.ts +3 -0
  331. package/extensions/subagents/index.ts +1493 -824
  332. package/extensions/subagents/live-runs.ts +26 -6
  333. package/extensions/subagents/model-select.ts +38 -25
  334. package/extensions/subagents/outcome.ts +2 -2
  335. package/extensions/subagents/panel-keys.ts +73 -31
  336. package/extensions/subagents/panel-render.ts +120 -96
  337. package/extensions/subagents/panel-widget.ts +60 -89
  338. package/extensions/subagents/runner.ts +108 -80
  339. package/extensions/subagents/runs.ts +39 -5
  340. package/extensions/subagents/send-to-main-tool.ts +1 -1
  341. package/extensions/subagents/session-turns.ts +32 -8
  342. package/extensions/subagents/shell-panel.ts +36 -178
  343. package/extensions/subagents/tasks-dialog.ts +170 -0
  344. package/extensions/subagents/viewport-mirror.ts +95 -0
  345. package/extensions/subagents/worktree.ts +57 -13
  346. package/extensions/system-prompt/environment.ts +19 -19
  347. package/extensions/system-prompt/idle-turn.ts +65 -0
  348. package/extensions/system-prompt/index.ts +131 -47
  349. package/extensions/system-prompt/template.ts +24 -50
  350. package/extensions/system-prompt/tiers/common.ts +23 -64
  351. package/extensions/system-prompt/tiers/frontier.ts +32 -11
  352. package/extensions/system-prompt/tiers/low.ts +67 -45
  353. package/extensions/system-prompt/tiers/mid.ts +96 -38
  354. package/extensions/system-reminder/index.ts +175 -13
  355. package/extensions/tasks/descriptions.ts +190 -0
  356. package/extensions/tasks/index.ts +120 -41
  357. package/extensions/tasks/store.ts +83 -16
  358. package/extensions/tool-call-corruption/child.ts +7 -0
  359. package/extensions/tool-call-corruption/index.ts +237 -0
  360. package/extensions/tool-search/announce.ts +16 -0
  361. package/extensions/tool-search/index.ts +189 -41
  362. package/extensions/tool-style/index.ts +68 -10
  363. package/extensions/tool-style/write-description.ts +40 -0
  364. package/extensions/tool-style/write-render.ts +237 -0
  365. package/extensions/turn-duration/index.ts +7 -3
  366. package/extensions/turn-duration/line.ts +47 -4
  367. package/extensions/web/backends.ts +6 -9
  368. package/extensions/web/description.ts +50 -0
  369. package/extensions/web/index.ts +118 -53
  370. package/extensions/web/native-call.ts +15 -0
  371. package/extensions/web/native-results.ts +41 -0
  372. package/extensions/web-fetch/description.ts +42 -0
  373. package/extensions/web-fetch/extract.ts +53 -6
  374. package/extensions/web-fetch/index.ts +125 -76
  375. package/extensions/web-fetch/page-content.ts +19 -0
  376. package/extensions/web-fetch/response.ts +49 -0
  377. package/extensions/web-fetch/summarize.ts +9 -10
  378. package/extensions/workflow/agent-session.ts +105 -18
  379. package/extensions/workflow/args.ts +24 -0
  380. package/extensions/workflow/description.ts +67 -0
  381. package/extensions/workflow/globals.ts +17 -3
  382. package/extensions/workflow/index.ts +145 -162
  383. package/extensions/workflow/records.ts +1 -0
  384. package/extensions/workflow/run-manager.ts +29 -6
  385. package/extensions/workflow/saved-workflows.ts +3 -2
  386. package/extensions/workflow/types.ts +22 -1
  387. package/extensions/workflow/viewer.ts +54 -24
  388. package/extensions/workflow/widget.ts +20 -0
  389. package/extensions/worktree/descriptions.ts +91 -0
  390. package/extensions/worktree/guards.ts +316 -54
  391. package/extensions/worktree/index.ts +121 -39
  392. package/extensions/worktree/powershell-guards.ts +167 -0
  393. package/extensions/worktree/rewrite.ts +14 -16
  394. package/package.json +19 -7
  395. package/skills/artifact-design/SKILL.md +85 -0
  396. package/skills/code-review/SKILL.md +20 -20
  397. package/skills/code-review/cells/arguments-and-flags.md +19 -0
  398. package/skills/code-review/cells/high-3x5-recall-verify.md +137 -0
  399. package/skills/code-review/cells/high-8-inline.md +122 -0
  400. package/skills/code-review/cells/low-1-pass-max4.md +27 -0
  401. package/skills/code-review/cells/low-1-pass-min-files-4.md +28 -0
  402. package/skills/code-review/cells/max-5x5-verify-sweep.md +162 -0
  403. package/skills/code-review/cells/medium-3x5-verify.md +132 -0
  404. package/skills/code-review/cells/minimal-prompt.md +32 -0
  405. package/skills/code-review/cells/xhigh-10-inline-sweep.md +149 -0
  406. package/skills/code-review/cells/xhigh-5x5-verify-sweep.md +162 -0
  407. package/skills/fewer-permission-prompts/SKILL.md +1 -1
  408. package/skills/loop/SKILL.md +11 -0
  409. package/skills/security-review/SKILL.md +2 -5
  410. package/skills/simplify/SKILL.md +7 -5
  411. package/skills/workflow-authoring/SKILL.md +143 -0
  412. package/themes/onecode-light.json +1 -1
  413. package/themes/onecode.json +1 -1
  414. package/types/app.d.ts +58 -0
  415. package/types/community-packages.d.ts +26 -1
  416. package/extensions/auto-mode/recoverability.ts +0 -229
  417. package/extensions/lib/capability-index.ts +0 -466
  418. package/extensions/lib/model-facts.generated.json +0 -2675
  419. package/extensions/lib/model-facts.ts +0 -162
@@ -1,63 +1,58 @@
1
1
  /**
2
2
  * Which system-prompt register + tool surface the active model gets. Claude Code
3
- * ships a capability-tiered prompt — a terse one for Opus, a much longer, more
4
- * explicit one for Sonnet/Haiku (Anthropic removed 80%+ of the prompt for
5
- * Claude-5-gen models with no measured loss). One Code serves every provider, so
3
+ * ships a capability-tiered prompt — a short one for Opus ≥ 4.8, Sonnet ≥ 5.5
4
+ * and Fable, a much longer, more explicit one for Haiku and the older Sonnets
5
+ * and Opuses (findings §56). One Code serves every provider, so
6
6
  * its non-frontier audience extends *below* Haiku; a single lean prompt
7
7
  * under-instructs it. This module maps a model to one of FOUR tiers:
8
8
  *
9
- * frontier Opus/Fable only — terse register (cc-opus), bash covers search
10
- * workhorse Sonnet-class + capable third-party — verbose register (cc-sonnet)
11
- * cheap Haiku-class — verbose register (cc-haiku), matches CC exactly
12
- * tiny sub-Haiku models — verbose + weak-model scaffolding + grep/find/ls
9
+ * frontier Claude Code's own short-prompt models, and OpenAI's GPT-6+ Astra and Sol
10
+ * workhorse a vendor's large current models — the long register
11
+ * cheap a vendor's smaller or older models — the long register
12
+ * tiny literally small models — verbose + weak-model scaffolding + grep/find/ls
13
13
  *
14
14
  * `extensions/system-prompt` picks the prompt text from the tier;
15
- * `extensions/search-tools` activates grep/find/ls only for `tiny`. Rationale +
16
- * rejected alternatives: `docs/decisions/model-tiers.md`.
17
- *
18
- * The classification obeys the repo convention against id-substring matching as
19
- * the LEAD signal (see `auto-mode/model-select.ts`): frontier is a version-gated
20
- * first-party Anthropic allowlist. Below it, a curated anchor map or the name
21
- * class sets the tier and the model's GENERATION (models.dev release dates,
22
- * `model-facts.ts`) demotes it — price is not consulted once facts are known, so
23
- * an unpriced but current flagship on a hosted catalog keeps the workhorse
24
- * register. Rows without facts fall back to the name-class cap, the "pro"-class
25
- * hint and the absolute price floor, where unpriced/opaque means maximum
26
- * scaffolding. The Artificial Analysis index never sets a register; it feeds the
27
- * optional measured SELECTION floor (`capability-index.ts`) — see
28
- * `classifyModelTier` and `docs/decisions/model-tiers.md`.
15
+ * `extensions/search-tools` activates grep/find/ls only for `tiny`. Frontier is
16
+ * a version-gated first-party allowlist (Claude Code's own rule); below it the
17
+ * tier comes from the public catalogs (`model-catalog.ts`: release date, price
18
+ * and size against the same vendor's current models), with no model names,
19
+ * dollar floors or benchmark scores in the code. Rationale + rejected
20
+ * alternatives: `working-docs/decisions/model-tiers.md`.
29
21
  */
30
22
 
23
+ import { statSync } from "node:fs";
24
+ import { homedir } from "node:os";
31
25
  import type { Api, Model } from "@earendil-works/pi-ai";
26
+ import { readJsonFile } from "./atomic-write.ts";
27
+ import { autoSelectable, catalogModelFor, sizeFromName, smallByName, TINY_PARAMS } from "./model-catalog.ts";
32
28
  import {
33
- capabilityFloor,
34
- type CapabilitySnapshot,
35
- type FloorRole,
36
- loadCapabilitySnapshot,
37
- referenceScore,
38
- REGISTER_CHEAP_RATIO,
39
- REGISTER_TINY_RATIO,
40
- scoreFor,
41
- } from "./capability-index.ts";
42
- import { isPriorGeneration, lacksToolCalls, modelGeneration } from "./model-facts.ts";
43
- import { baseModelId, isDatedDuplicate, modelIdentity, modelsContainedToSession, modelSpec, pricedInput, supportsImageInput } from "./model-policy.ts";
44
- import { oneCodeStateDir } from "./paths.ts";
29
+ BUILTIN_PROVIDER_POLICIES,
30
+ baseModelId,
31
+ isDatedDuplicate,
32
+ isExperimentalBuild,
33
+ modelIdentity,
34
+ modelsContainedToSession,
35
+ modelSpec,
36
+ pricedInput,
37
+ providerPolicy,
38
+ supportsImageInput,
39
+ } from "./model-policy.ts";
40
+ import { oneCodeSettingsPath } from "./one-code-settings.ts";
45
41
 
46
42
  export type PromptTier = "frontier" | "workhorse" | "cheap" | "tiny";
47
43
 
48
- /** More-scaffolded = higher rank. Used to combine the price floor with the name cap. */
44
+ /** More-scaffolded = higher rank. */
49
45
  const TIER_RANK: Record<PromptTier, number> = { frontier: 0, workhorse: 1, cheap: 2, tiny: 3 };
50
- function moreScaffolded(a: PromptTier, b: PromptTier): PromptTier {
51
- return TIER_RANK[a] >= TIER_RANK[b] ? a : b;
46
+
47
+ /** Whether `value` names one of the four tiers. */
48
+ export function isPromptTier(value: unknown): value is PromptTier {
49
+ return typeof value === "string" && Object.hasOwn(TIER_RANK, value);
52
50
  }
53
51
 
54
- /**
55
- * Cost-preference order for automatic secondary-model selection: cheapest
56
- * *capable* tier first, stepping UP only when a tier is empty. `tiny` is absent
57
- * on purpose — automatic selection never lands there (see
58
- * `economicalContainedCandidates`).
59
- */
60
- const COST_PREFERENCE: Record<Exclude<PromptTier, "tiny">, number> = { cheap: 0, workhorse: 1, frontier: 2 };
52
+ /** How many tiers `tier` sits below `reference` (negative when above). */
53
+ export function tiersBelow(tier: PromptTier, reference: PromptTier): number {
54
+ return TIER_RANK[tier] - TIER_RANK[reference];
55
+ }
61
56
 
62
57
  /**
63
58
  * `CC_PROMPT_TIER=frontier|workhorse|cheap|tiny` forces a register; `auto`/unset
@@ -66,14 +61,15 @@ const COST_PREFERENCE: Record<Exclude<PromptTier, "tiny">, number> = { cheap: 0,
66
61
  */
67
62
  export function tierOverride(env: NodeJS.ProcessEnv = process.env): PromptTier | undefined {
68
63
  const raw = env.CC_PROMPT_TIER?.trim().toLowerCase();
69
- return raw === "frontier" || raw === "workhorse" || raw === "cheap" || raw === "tiny" ? raw : undefined;
64
+ return isPromptTier(raw) ? raw : undefined;
70
65
  }
71
66
 
72
67
  /**
73
- * Anthropic first-party frontier gate: Opus/Fable ≥ 4.7 ONLY (Sonnet is
74
- * deliberately excluded — CC gives only Opus the terse register, and the
75
- * intelligence index would wrongly promote flash models to frontier at max
76
- * effort; see `docs/decisions/model-tiers.md`). Adapted from pi-ai's
68
+ * Anthropic first-party frontier gate: Opus ≥ 4.8, Sonnet ≥ 5.5 and every
69
+ * Fable — the models Claude Code gives its short prompt and tool forms (Opus
70
+ * 4.7 and Sonnet 5 get the long ones; findings §56). Haiku 5 and later get the
71
+ * short forms too, but only for the prompt (`isHaikuWithFrontierForms`).
72
+ * Claude Code decides this by version, so One Code does too. Adapted from pi-ai's
77
73
  * `defaultSupportsToolReferences` — never Haiku, and the `length < 8` guard stops
78
74
  * a dated suffix (`claude-opus-4-8-20251101`) being read as the minor version.
79
75
  * Version parse, not price: `claude-opus-4-1` ($15/M) costs more than
@@ -83,8 +79,39 @@ function isAnthropicFrontier(model: Model<Api>): boolean {
83
79
  if (model.provider !== "anthropic" || model.id.includes("haiku")) return false;
84
80
  const version = parseClaudeVersion(model.id);
85
81
  if (!version) return false;
86
- if (version.family === "sonnet") return false; // Sonnet is workhorse, never frontier
87
- return version.major > 4 || (version.major === 4 && version.minor >= 7);
82
+ if (version.family === "fable") return true;
83
+ if (version.family === "sonnet") return version.major > 5 || (version.major === 5 && version.minor >= 5);
84
+ return version.major > 4 || (version.major === 4 && version.minor >= 8);
85
+ }
86
+
87
+ /**
88
+ * First-party Haiku 5 and later take the frontier prompt and tool forms
89
+ * (findings §68), but nothing else that reads the tier: model selection, the
90
+ * permission gate's fast paths and the doctor judge a Haiku by its catalog
91
+ * tier, so the cost algorithm never offers one as an Opus-class substitute.
92
+ */
93
+ function isHaikuWithFrontierForms(model: Model<Api>): boolean {
94
+ if (model.provider !== "anthropic") return false;
95
+ const major = model.id.match(/^claude-haiku-(\d+)(?:-|$)/)?.[1];
96
+ return major !== undefined && Number(major) >= 5;
97
+ }
98
+
99
+ /** What a tier is for: the session's prompt and tool forms, or judging a model's capability. */
100
+ export type TierPurpose = "prompt" | "capability";
101
+
102
+ /**
103
+ * OpenAI first-party frontier gate: the GPT-6 Astra and Sol lines and later
104
+ * majors (`gpt-6-astra`, `gpt-6-sol`, `gpt-6.1-sol-pro`), served by OpenAI
105
+ * itself (the `openai`, `openai-codex` and `azure` providers; `azure-openai-responses`
106
+ * before pi 1.0.3).
107
+ * GPT-5.x Sol and Terra, and every Luna, stay below it (decided 2026-09-25:
108
+ * Astra and Sol 6 are the OpenAI models comparable to Fable and Opus 5). A
109
+ * gateway-proxied copy can't be verified, the same rule as Claude above.
110
+ */
111
+ function isOpenAIFrontier(model: Model<Api>): boolean {
112
+ if (providerPolicy(model.provider).kind !== "direct" || modelIdentity(model).profile !== "openai") return false;
113
+ const match = model.id.match(/^gpt-(\d+)(?:\.\d+)?-(?:astra|sol)(?:-|$)/);
114
+ return match !== null && Number(match[1]) >= 6;
88
115
  }
89
116
 
90
117
  /**
@@ -92,7 +119,7 @@ function isAnthropicFrontier(model: Model<Api>): boolean {
92
119
  * `claude-sonnet-5`, `claude-opus-4-5-20251101`): family, major, minor. A
93
120
  * dated suffix is never read as the minor version (the `length < 8` guard).
94
121
  * Undefined for anything else. Shared by the frontier gate here and the
95
- * tool-reference gate in `deferred.ts`.
122
+ * server-tools gate in `anthropic-server-tools.ts`.
96
123
  */
97
124
  export function parseClaudeVersion(id: string): { family: "opus" | "sonnet" | "fable"; major: number; minor: number } | undefined {
98
125
  const version = id.match(/^claude-(opus|sonnet|fable)-(\d+)(?:-(\d+))?(?:-|$)/);
@@ -104,103 +131,7 @@ export function parseClaudeVersion(id: string): { family: "opus" | "sonnet" | "f
104
131
  };
105
132
  }
106
133
 
107
- /**
108
- * Curated anchor map for well-known third-party families, checked in order
109
- * (specific before general). A vetted allowlist — distinct from heuristic
110
- * substring matching — that fixes cases where price/name alone misroute a
111
- * flagship, and encodes the ratified overrides (GPT-5-mini→cheap,
112
- * GPT-5.6-Luna→cheap, GPT-5.x-codex-spark→cheap; the GPT-5-full→workhorse one
113
- * was retired 2026-09-11 once the coding index contradicted it). A match here is authoritative for the
114
- * name class (the human already accounted for the name and its generation), so
115
- * it wins outright — only the two-generations-behind rule still demotes it to
116
- * tiny (`classifyModelTier`). Shrink it whenever the catalog-wide snapshot
117
- * (`test/unit/model-tier-catalog.test.ts`) shows the generic rules reproduce an
118
- * entry; refresh the reference table with `tools/model-tiers/model_tiers.py`.
119
- */
120
- const ANCHOR_MAP: Array<[RegExp, PromptTier]> = [
121
- // OpenAI GPT-5 family — order matters: variant suffixes before the generic.
122
- [/(?:^|[-/.])gpt-5[.\d]*-?nano/i, "tiny"],
123
- [/(?:^|[-/.])gpt-5[.\d]*-?mini/i, "cheap"], // override: price would say tiny
124
- [/(?:^|[-/.])gpt-5[.\d]*-?luna/i, "cheap"], // override: OpenAI's cheap line despite a high benchmark
125
- // Spark: OpenAI's fast/small Codex line; no Artificial Analysis row either way, so it
126
- // is placed with the other lean lines rather than inheriting the family's workhorse
127
- // name class (2026-09-11 — it had been the cheapest "workhorse" on every Codex session).
128
- [/(?:^|[-/.])gpt-5[.\d]*-codex-spark/i, "cheap"],
129
- // No blanket `gpt-5 → workhorse` anchor any more (2026-09-11): it existed to guard
130
- // the base GPT-5 variant against a benchmark collision, but the coding index now
131
- // reads base gpt-5 at 37.8 and gpt-5.1 at 49.4 — the anchor was the lie, not the
132
- // score. Unsuffixed gpt-5.x rows take the name class (workhorse) and let their
133
- // generation and measured score demote them.
134
- [/(?:^|[-/.])o[34](?:[-/.]|$)/i, "cheap"], // o3, o3-pro, o4-mini
135
- [/(?:^|[-/.])gpt-4/i, "tiny"], // gpt-4o, gpt-4.1, gpt-4-turbo — prior generation
136
- // Anthropic reaching this path is gateway-proxied (first-party handled above);
137
- // a proxied model can't be version-verified for frontier, so Opus/Sonnet → workhorse.
138
- [/claude[-/.].*haiku/i, "cheap"],
139
- [/claude[-/.].*(?:sonnet|opus)/i, "workhorse"],
140
- // Google Gemini needs no anchor: `lite` is a tiny name, `pro` a capable one, and
141
- // bare flash is cheap by name class (generation decides 2.5 vs 3.x) — removed
142
- // 2026-09-10 once the catalog-wide snapshot showed the generic rules reproduce it.
143
- // Other capable flagships whose price may dip below the workhorse floor.
144
- [/(?:^|[-/.])grok-4/i, "workhorse"],
145
- // DeepSeek — priced ~10x below the vendors the price floors were calibrated
146
- // on, so on the no-facts path the floors alone put every V4 Flash row in tiny
147
- // and let R1-0528 ($0.50, on the cheap boundary) win automatic selection
148
- // (2026-09-10). With release-date facts the generic rules reproduce the V4
149
- // rows; the block stays for rows models.dev does not know yet (V4.1 Flash on
150
- // its release day) and for R1/V3.x/`deepseek-chat`, which a one-generation
151
- // demotion would leave at cheap rather than tiny.
152
- [/(?:^|[-/.])deepseek[-.]v4[.\d]*-flash/i, "cheap"],
153
- [/(?:^|[-/.])deepseek[-.]v4[.\d]*-pro/i, "workhorse"],
154
- [/(?:^|[-/.])deepseek[-.](?:r1|chat|v3)(?:[-.:]|$)/i, "tiny"], // `deepseek.v3` is Bedrock's spelling
155
- ];
156
-
157
- function anchorTier(id: string): PromptTier | undefined {
158
- const base = baseModelId(id);
159
- for (const [pattern, tier] of ANCHOR_MAP) if (pattern.test(base)) return tier;
160
- return undefined;
161
- }
162
-
163
- /**
164
- * Name-class CAP: a lean/fast model class raises scaffolding (never lowers it).
165
- * Delimited tokens — a bare `/mini/` matches inside "geMINI", dragging every
166
- * Gemini model to cheap. `\d+b` catches size tags (8b, 70b) as the strongest
167
- * (tiny) signal. Returns the ceiling tier the name justifies.
168
- */
169
- const TINY_NAME_HINT = /(?:^|[-/.])(?:nano|micro|lite|tiny|xs|distill|instant|\d+b)(?:[-/.]|$)/i;
170
- const CHEAP_NAME_HINT = /(?:^|[-/.])(?:flash|mini|small|air)(?:[-/.]|$)/i;
171
- function nameClassCap(id: string): PromptTier {
172
- const base = baseModelId(id); // `flash:batch` must still read as flash
173
- if (TINY_NAME_HINT.test(base)) return "tiny";
174
- if (CHEAP_NAME_HINT.test(base)) return "cheap";
175
- return "workhorse";
176
- }
177
-
178
- /**
179
- * A capable-class name marks the flagship of a family (deepseek-v4-pro,
180
- * qwen3-max) — keeps a cheap or unpriced flagship at workhorse rather than being
181
- * dragged down by the price floor. Delimited so it fires on `-pro`/`max-` but not
182
- * `prometheus`/`improved`. Only applies when no lean-name cap contradicts it.
183
- */
184
- const CAPABLE_NAME_HINT = /(?:^|[-/.])(?:pro|max|ultra|large)(?:[-/.]|$)/i;
185
-
186
- /** Input price (USD/1M) floors: capable flagships sit ≥ $1/M; mini/flash under $0.5. */
187
- const WORKHORSE_MIN_INPUT_COST = 1.0;
188
- const CHEAP_MIN_INPUT_COST = 0.5;
189
-
190
- /** Price/containment floor for an unanchored third-party model, biased low. */
191
- function priceFloorTier(model: Model<Api>): PromptTier {
192
- if (modelIdentity(model).confidence === "opaque") return "tiny"; // local/self-hosted → max scaffolding
193
- const price = pricedInput(model);
194
- if (price === undefined) return "tiny"; // unpriced/unknown → maximum scaffolding
195
- if (price >= WORKHORSE_MIN_INPUT_COST) return "workhorse";
196
- if (price >= CHEAP_MIN_INPUT_COST) return "cheap";
197
- return "tiny";
198
- }
199
-
200
- /** One step more scaffolded: workhorse → cheap → tiny (frontier never enters). */
201
- function demote(tier: PromptTier): PromptTier {
202
- return tier === "workhorse" ? "cheap" : "tiny";
203
- }
134
+ export { claudeFamily, parseClaudeId } from "./claude-id.ts";
204
135
 
205
136
  export interface TierClassification {
206
137
  tier: PromptTier;
@@ -210,110 +141,128 @@ export interface TierClassification {
210
141
 
211
142
  /**
212
143
  * The tier and the rule that produced it. `resolveModelTier` is the plain form.
213
- *
214
- * Non-frontier, non-first-party models are classified in this order:
215
- * 1. the curated anchor map (verifiable providers only) sets the name-class tier;
216
- * 2. otherwise the name class does (pro/max/unmarked → workhorse, flash/mini →
217
- * cheap, nano/lite/size-tag → tiny) — when the model has release-date facts.
218
- * Then its GENERATION demotes: one tier when it trails its vendor family's
219
- * newest release by more than a year (name-class path only — an anchor is
220
- * human-reviewed), to tiny beyond two for both. Price is not
221
- * consulted: it encodes neither capability nor generation (an old model
222
- * keeps its old price), and absolute dollar floors misread every vendor
223
- * priced below the US majors — `model-facts.ts`.
224
- * 3. without facts, the pre-facts heuristics: name-class cap, the capable-name
225
- * hint (verifiable providers only), and the absolute price floor, combined
226
- * as the more scaffolded of the two.
144
+ * In order:
145
+ * 1. `CC_PROMPT_TIER` (the session's register only; selection ignores it);
146
+ * 2. the user's `modelTiers` setting for this model;
147
+ * 3. the Claude Code parity gate: first-party Opus ≥ 4.8, Sonnet ≥ 5.5,
148
+ * Fable, and OpenAI's GPT-6+ Astra and Sol are frontier; for the prompt
149
+ * (`purpose: "prompt"`, the default) first-party Haiku ≥ 5 is too;
150
+ * 4. the catalog tier (`model-catalog.ts`): tiny by size, workhorse or cheap
151
+ * by size or price against the same vendor's current models, demoted one
152
+ * step by age;
153
+ * 5. a model no catalog knows: tiny when its id names a small size or it is
154
+ * unpriced or on a custom provider, cheap otherwise.
227
155
  */
228
- export function classifyModelTier(model: Model<Api> | undefined, env: NodeJS.ProcessEnv = process.env): TierClassification {
229
- const named = classifyByName(model, env);
230
- // The measured cap: a name can only lie UPWARD about capability that a score
231
- // then corrects (gpt-5 at 37.8 is not a Sonnet-class model whatever its
232
- // anchor said), and downward it is already the safer register, so the score
233
- // only ever adds scaffolding. Frontier (a first-party allowlist), Anthropic
234
- // first-party (the scale's own anchors), a forced tier and an already-tiny
235
- // row are left alone.
236
- if (!model || named.reason === "CC_PROMPT_TIER" || named.tier === "frontier" || named.tier === "tiny" || model.provider === "anthropic") return named;
237
- const measured = measuredRegisterCap(model);
238
- if (!measured) return named;
239
- const tier = moreScaffolded(named.tier, measured.cap);
240
- if (tier === named.tier) return named;
241
- return { tier, reason: `${named.reason} · measured coding ${measured.coding} below the ${measured.cap} line (${measured.threshold.toFixed(1)})` };
156
+ export function classifyModelTier(
157
+ model: Model<Api> | undefined,
158
+ env: NodeJS.ProcessEnv = process.env,
159
+ purpose: TierPurpose = "prompt",
160
+ ): TierClassification {
161
+ const forced = tierOverride(env);
162
+ if (forced) return { tier: forced, reason: "CC_PROMPT_TIER" };
163
+ if (!model) return { tier: "tiny", reason: "no model" }; // unknown model → maximum scaffolding
164
+ const configured = configuredModelTier(model);
165
+ if (configured) return { tier: configured, reason: "modelTiers setting" };
166
+ if (isAnthropicFrontier(model)) return { tier: "frontier", reason: "anthropic frontier allowlist" };
167
+ if (purpose === "prompt" && isHaikuWithFrontierForms(model)) return { tier: "frontier", reason: "anthropic frontier allowlist (prompt and tool forms)" };
168
+ if (isOpenAIFrontier(model)) return { tier: "frontier", reason: "openai frontier allowlist" };
169
+ const entry = catalogModelFor(model);
170
+ if (entry) return { tier: entry.tier, reason: `catalog ${entry.id}: ${entry.reason}` };
171
+ const id = baseModelId(model.id);
172
+ const size = sizeFromName(id);
173
+ if (size !== undefined && size < TINY_PARAMS) return { tier: "tiny", reason: "not in the catalogs; a small size in its id" };
174
+ if (size === undefined && smallByName(id)) return { tier: "tiny", reason: "not in the catalogs; a small-model name" };
175
+ if (modelIdentity(model).confidence === "opaque" && !(model.provider in BUILTIN_PROVIDER_POLICIES)) {
176
+ return { tier: "tiny", reason: "custom provider, not in the catalogs" };
177
+ }
178
+ if (pricedInput(model) === undefined) return { tier: "tiny", reason: "unpriced, not in the catalogs" };
179
+ return { tier: "cheap", reason: "not in the catalogs" };
242
180
  }
243
181
 
244
182
  /**
245
- * The register cap the Artificial Analysis snapshot supports for a model, if
246
- * any: `cheap` below REGISTER_CHEAP_RATIO × Sonnet 5, `tiny` below
247
- * REGISTER_TINY_RATIO × Sonnet 5, judged on the default (thinking-on) variant.
248
- * Undefined without a snapshot, a confirmed score, or a reference score — the
249
- * name-led tier then stands, as before the snapshot existed.
183
+ * The user's own tier for a model from `modelTiers` in `~/.onecode/settings.json`,
184
+ * keyed by `provider/id` or by bare id (`{ "openrouter/qwen/qwen3.8-max": "workhorse" }`).
185
+ * User settings only: a project cannot loosen its own permission gate.
250
186
  */
251
- export function measuredRegisterCap(model: Model<Api>): { cap: "cheap" | "tiny"; coding: number; threshold: number } | undefined {
252
- const snapshot = currentCapabilitySnapshot();
253
- if (!snapshot) return undefined;
254
- const score = scoreFor(snapshot, model, "default");
255
- const reference = referenceScore(snapshot, "default");
256
- if (!score || !reference) return undefined;
257
- const tinyLine = reference.coding * REGISTER_TINY_RATIO;
258
- if (score.coding < tinyLine) return { cap: "tiny", coding: score.coding, threshold: tinyLine };
259
- const cheapLine = reference.coding * REGISTER_CHEAP_RATIO;
260
- if (score.coding < cheapLine) return { cap: "cheap", coding: score.coding, threshold: cheapLine };
261
- return undefined;
187
+ function configuredModelTier(model: { provider: string; id: string }): PromptTier | undefined {
188
+ const tiers = modelTierOverrides();
189
+ return tiers[`${model.provider}/${model.id}`] ?? tiers[model.id];
262
190
  }
263
191
 
264
- /** The name-led classification (anchors, name class, generation, price) before the measured cap. */
265
- function classifyByName(model: Model<Api> | undefined, env: NodeJS.ProcessEnv): TierClassification {
266
- const forced = tierOverride(env);
267
- if (forced) return { tier: forced, reason: "CC_PROMPT_TIER" };
268
- if (!model) return { tier: "tiny", reason: "no model" }; // unknown model → maximum scaffolding
192
+ let overrideMemo: { path: string; at: number; mtimeMs: number; tiers: Record<string, PromptTier> } | undefined;
193
+ let overridesPinned: Record<string, PromptTier> | undefined;
269
194
 
270
- if (isAnthropicFrontier(model)) return { tier: "frontier", reason: "anthropic frontier allowlist" };
271
- if (model.provider === "anthropic") {
272
- // First-party non-frontier: Haiku → cheap; Sonnet / Opus 4.1–4.6 / other →
273
- // workhorse. Version-named ids already encode generation, and the tiny
274
- // register would be wrong for a still-sold Opus, so no date demotion here;
275
- // automatic selection still skips prior-generation rows.
276
- return model.id.includes("haiku") ? { tier: "cheap", reason: "anthropic haiku" } : { tier: "workhorse", reason: "anthropic first-party" };
277
- }
195
+ /** Test seam: pin the `modelTiers` map (`undefined` reads the settings file again). */
196
+ export function setModelTierOverridesForTest(tiers: Record<string, PromptTier> | undefined): void {
197
+ overridesPinned = tiers;
198
+ overrideMemo = undefined;
199
+ }
278
200
 
279
- // A curated anchor is authoritative for the NAME CLASS — but only for a
280
- // verifiable provider. A local/self-hosted (opaque) provider's id could be
281
- // anything, so it must NOT reach the anchor map (a model aliased
282
- // "claude-sonnet-5" on ollama is not the real thing) nor the capable-name
283
- // hint; it falls through to the generic heuristics, where the opaque check
284
- // biases it to maximum scaffolding.
285
- const opaque = modelIdentity(model).confidence === "opaque";
286
- const anchor = opaque ? undefined : anchorTier(model.id);
287
-
288
- const generation = opaque ? undefined : modelGeneration(model);
289
- if (generation !== undefined) {
290
- const nameTier = anchor ?? nameClassCap(model.id); // never frontier: anchors and name classes stop at workhorse
291
- if (generation === "ancient") return { tier: "tiny", reason: `${anchor ? "anchor" : "name class"} · two generations behind` };
292
- // A curated anchor already weighed the model's generation (it is reviewed
293
- // against the catalog diff), so only the name-class path is demoted here.
294
- if (generation === "prior" && !anchor) return { tier: demote(nameTier), reason: "name class · one generation behind" };
295
- return { tier: nameTier, reason: anchor ? "anchor" : "name class" };
201
+ /**
202
+ * The validated `modelTiers` map. Stat-checked at most once a second and
203
+ * re-parsed only when the settings file's mtime moved: the permission gate asks
204
+ * on every tool call.
205
+ */
206
+ export function modelTierOverrides(): Record<string, PromptTier> {
207
+ if (overridesPinned) return overridesPinned;
208
+ const path = oneCodeSettingsPath(homedir());
209
+ const now = Date.now();
210
+ if (overrideMemo && overrideMemo.path === path && now - overrideMemo.at < 1000) return overrideMemo.tiers;
211
+ let mtimeMs = -1;
212
+ try {
213
+ mtimeMs = statSync(path).mtimeMs;
214
+ } catch {
215
+ // No settings file: no overrides.
216
+ }
217
+ if (overrideMemo && overrideMemo.path === path && overrideMemo.mtimeMs === mtimeMs) {
218
+ overrideMemo.at = now;
219
+ return overrideMemo.tiers;
296
220
  }
297
- if (anchor) return { tier: anchor, reason: "anchor" };
298
-
299
- const cap = nameClassCap(model.id);
300
- // A "pro"/"max"-class name keeps a cheap/unpriced flagship at workhorse; a lean
301
- // name (cap below workhorse) always wins, so only consult it when cap allows.
302
- if (!opaque && cap === "workhorse" && CAPABLE_NAME_HINT.test(baseModelId(model.id))) return { tier: "workhorse", reason: "capable name, no facts" };
303
- const floor = priceFloorTier(model);
304
- const tier = moreScaffolded(floor, cap);
305
- return { tier, reason: opaque ? "opaque provider" : pricedInput(model) === undefined ? "unpriced, no facts" : "price floor, no facts" };
221
+ const raw = mtimeMs < 0 ? undefined : readJsonFile<{ modelTiers?: unknown }>(path)?.modelTiers;
222
+ const tiers: Record<string, PromptTier> = {};
223
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) {
224
+ for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
225
+ if (isPromptTier(value)) tiers[key] = value;
226
+ }
227
+ }
228
+ overrideMemo = { path, at: now, mtimeMs, tiers };
229
+ return tiers;
306
230
  }
307
231
 
308
232
  export function resolveModelTier(model: Model<Api> | undefined, env: NodeJS.ProcessEnv = process.env): PromptTier {
309
233
  return classifyModelTier(model, env).tier;
310
234
  }
311
235
 
236
+ const TRUTHY_ENV = new Set(["1", "true", "yes", "on"]);
237
+
238
+ /**
239
+ * Whether the session model gets the task tools (`task_create` and family),
240
+ * Claude Code's model gate for a foreground session: its current models run
241
+ * without a task list, which `CLAUDE_CODE_ENABLE_TODO_TOOLS` turns back on
242
+ * (findings §31). Off for the frontier tier and first-party Sonnet 5 or later;
243
+ * on for everything else, including an unknown model (Claude Code keeps the
244
+ * tools for a model it does not recognise). Claude Code also turns them on in
245
+ * a backgrounded session, which One Code does not have.
246
+ */
247
+ export function taskToolsEnabled(
248
+ model: Model<Api> | undefined,
249
+ env: NodeJS.ProcessEnv = process.env,
250
+ /** Request-surface callers pass the session's frozen tier (session-model-tier.ts). */
251
+ tier?: PromptTier,
252
+ ): boolean {
253
+ if (TRUTHY_ENV.has(env.CLAUDE_CODE_ENABLE_TODO_TOOLS?.trim().toLowerCase() ?? "")) return true;
254
+ if (!model) return true;
255
+ if ((tier ?? resolveModelTier(model, env)) === "frontier") return false;
256
+ if (model.provider !== "anthropic") return true;
257
+ const version = parseClaudeVersion(model.id);
258
+ return !(version?.family === "sonnet" && version.major >= 5);
259
+ }
260
+
312
261
 
313
262
  /**
314
263
  * The ordered candidate chain for an automatic *secondary* model — the same
315
- * mechanism the auto-mode classifier and subagents both consume, so a session
316
- * screens and delegates on one economical same-provider model.
264
+ * mechanism the auto-mode classifier, subagents and readers consume before
265
+ * their own budget and tier rules decide which models qualify.
317
266
  *
318
267
  * The chain is:
319
268
  * - **contained** to the session's provider (and route/family on gateways):
@@ -321,32 +270,31 @@ export function resolveModelTier(model: Model<Api> | undefined, env: NodeJS.Proc
321
270
  * the parent transcript, so leaving the session's provider silently is a
322
271
  * privacy leak — only an explicit setting may cross (see the two model-select
323
272
  * modules);
324
- * - never `tiny`: a sub-Haiku model is a weak security boundary and a weak
325
- * coding worker, so automatic selection stops at `cheap` and steps UP
326
- * (cheap → workhorse → frontier), never down. This is the capability floor
327
- * `docs/decisions/auto-mode.md` recorded as still-missing;
328
- * - current generation only, and able to call tools: with models.dev facts
329
- * known (`model-facts.ts`), a row more than a year behind its vendor family's
330
- * newest release is excluded whatever its price or name, as is a row marked
331
- * as lacking tool calls. Rows without facts pass this gate unchanged;
332
- * - priced only: an unpriced/opaque row is treated as `tiny` by
333
- * `resolveModelTier`, so this excludes it too. On a provider with no usable
334
- * prices the chain is empty and callers degrade to the session model
335
- * (correct, merely not cheap).
273
+ * - never `tiny`: a small model is a weak security boundary and a weak coding
274
+ * worker, so automatic selection stops at `cheap`;
275
+ * - known to the catalogs and selectable there (`model-catalog.ts`): not
276
+ * superseded by a newer model of its vendor in the same price band that
277
+ * this pool also offers, not two years behind its vendor's newest, not
278
+ * deprecated, able to call tools, a text model. A row no catalog knows is
279
+ * never picked automatically;
280
+ * - priced only: on a provider with no usable prices the chain is empty and
281
+ * callers degrade to the session model (correct, merely not cheap).
336
282
  *
337
- * Sorted by cost preference (cheapest capable *tier* first) then input price, so
338
- * the head is "the cheapest capable model this provider offers." The session
339
- * model may appear in the chain; the budget-gated `cheaperContainedCandidates`
340
- * drops it. Budget ceilings and strictly-cheaper-than-session are caller policy —
341
- * this function ranks, it does not gate. A caller that already computed the
342
- * containment set may pass it as `contained` to skip the O(catalog) recompute.
283
+ * Sorted by input price, the more capable tier first at equal price, so the
284
+ * head is "the cheapest usable model this provider offers." Price leads, not
285
+ * tier: a frontier model cheaper than every cheap one (Haiku 5.5 against Haiku
286
+ * 4.5) is the better pick on both counts. The session model
287
+ * may appear in the chain; the budget-gated `cheaperContainedCandidates` drops
288
+ * it. Budget ceilings and tier floors are caller policy — this function ranks,
289
+ * it does not gate. A caller that already computed the containment set may
290
+ * pass it as `contained` to skip the O(catalog) recompute.
343
291
  *
344
292
  * `requireImageInput` (default false) drops every candidate that cannot take
345
293
  * image input — the modality gate a subagent selection sets when the session
346
294
  * model is image-capable, so a delegated worker can still read an image or PDF
347
295
  * the session may feed it. The classifier and the readers leave it false: the
348
296
  * classifier renders the transcript to text and the readers strip images, so
349
- * neither ever sends one. See `docs/decisions/model-policy.md`.
297
+ * neither ever sends one. See `working-docs/decisions/model-policy.md`.
350
298
  */
351
299
  export function economicalContainedCandidates(
352
300
  available: Model<Api>[],
@@ -355,37 +303,66 @@ export function economicalContainedCandidates(
355
303
  requireImageInput = false,
356
304
  ): Model<Api>[] {
357
305
  const pool = contained ?? modelsContainedToSession(available, sessionModel);
358
- return pool
359
- // Modality gate: when the session works with images/PDFs a subagent may
360
- // need to read, a text-only worker cannot serve — drop it before ranking.
361
- .filter((model) => !requireImageInput || supportsImageInput(model))
306
+ // Modality gate: when the session works with images/PDFs a subagent may
307
+ // need to read, a text-only worker cannot serve — drop it before ranking.
308
+ const usable = requireImageInput ? pool.filter(supportsImageInput) : pool;
309
+ // A superseded model is skipped only for a successor this pool can pick.
310
+ const served = servedCatalogIds(usable);
311
+ return usable
362
312
  // A dated snapshot whose undated alias is also listed is the same model
363
313
  // twice; rank the alias so every automatic pick (classifier, subagent,
364
314
  // reader, presets) names the model the way the user sees it in /model.
365
315
  .filter((model) => !isDatedDuplicate(model, pool))
366
- // Facts, when known: a model one generation behind its family is never
367
- // picked automatically however it prices (the R1-over-V4-Flash incident),
368
- // and a subagent or screener must be able to call tools at all.
369
- .filter((model) => !isPriorGeneration(model) && !lacksToolCalls(model))
316
+ .filter((model) => isAutoSelectable(model, served))
370
317
  // Classify by the model's INTRINSIC tier — never `process.env`: CC_PROMPT_TIER
371
318
  // forces the *session's* prompt-scaffolding register, and honoring it here
372
319
  // would collapse every candidate to one tier and let a `tiny` model through
373
320
  // the security floor. Selection must judge each model on its own merits.
374
- .map((model) => ({ model, tier: resolveModelTier(model, INTRINSIC_TIER_ENV), price: pricedInput(model) }))
321
+ .map((model) => ({ model, tier: intrinsicTier(model), price: pricedInput(model) }))
375
322
  .filter(
376
- (entry): entry is { model: Model<Api>; tier: Exclude<PromptTier, "tiny">; price: number } =>
323
+ (entry): entry is { model: Model<Api>; tier: PromptTier; price: number } =>
377
324
  entry.tier !== "tiny" && entry.price !== undefined,
378
325
  )
379
- .sort((a, b) => COST_PREFERENCE[a.tier] - COST_PREFERENCE[b.tier] || a.price - b.price)
326
+ .sort((a, b) => a.price - b.price || TIER_RANK[a.tier] - TIER_RANK[b.tier])
380
327
  .map((entry) => entry.model);
381
328
  }
382
329
 
330
+ /**
331
+ * Whether automatic selection may pick `model` at all: the catalogs know it and
332
+ * do not mark it legacy, deprecated, tool-less or non-text, it is not an
333
+ * experimental build (`isExperimentalBuild`), and it is not superseded by a
334
+ * model `served` holds (`model-catalog.ts autoSelectable`; `served` is
335
+ * `servedCatalogIds` of the session's contained pool). A model the user tiered
336
+ * by hand in `modelTiers` still needs a catalog entry: the setting speaks to
337
+ * its tier, not to whether it is current.
338
+ */
339
+ export function isAutoSelectable(model: Model<Api>, served: ReadonlySet<string>): boolean {
340
+ // The pi id too: a row can name an experimental build its catalog entry does not.
341
+ if (isExperimentalBuild(model.id)) return false;
342
+ const entry = catalogModelFor(model);
343
+ return entry !== undefined && autoSelectable(entry, served);
344
+ }
345
+
346
+ /** The catalog models a pool of pi rows serves: where a successor must be for supersession to skip a model. */
347
+ export function servedCatalogIds(pool: readonly Model<Api>[]): Set<string> {
348
+ const ids = new Set<string>();
349
+ for (const model of pool) {
350
+ const entry = catalogModelFor(model);
351
+ if (entry) ids.add(entry.id);
352
+ }
353
+ return ids;
354
+ }
355
+
383
356
  /** No CC_PROMPT_TIER override: automatic model *selection* always uses intrinsic tiers. */
384
357
  const INTRINSIC_TIER_ENV: NodeJS.ProcessEnv = Object.freeze({}) as NodeJS.ProcessEnv;
385
358
 
386
- /** A model's own tier, CC_PROMPT_TIER ignored — what selection judges a candidate by. */
359
+ /**
360
+ * A model's own capability tier, CC_PROMPT_TIER ignored — what selection, the
361
+ * permission gate and the doctor judge a model by. It differs from the prompt
362
+ * tier only for Haiku 5 and later (`isHaikuWithFrontierForms`).
363
+ */
387
364
  export function intrinsicTier(model: Model<Api>): PromptTier {
388
- return resolveModelTier(model, INTRINSIC_TIER_ENV);
365
+ return classifyModelTier(model, INTRINSIC_TIER_ENV, "capability").tier;
389
366
  }
390
367
 
391
368
  /** Whether `tier` is at least as capable as `floor` (frontier ≥ workhorse ≥ cheap ≥ tiny). */
@@ -393,114 +370,65 @@ export function atLeastTier(tier: PromptTier, floor: PromptTier): boolean {
393
370
  return TIER_RANK[tier] <= TIER_RANK[floor];
394
371
  }
395
372
 
373
+ /**
374
+ * Whether the permission gate gives `model`'s calls Claude Code's fast paths.
375
+ * Frontier and workhorse do; cheap and tiny keep One Code's stricter gate
376
+ * (working-docs/decisions/auto-mode.md, "Two gates by model tier"). Judged on
377
+ * the intrinsic tier, so `CC_PROMPT_TIER` can never loosen the gate; no model
378
+ * gets the stricter gate.
379
+ */
380
+ export function usesClaudeCodeFastPaths(model: Model<Api> | undefined): boolean {
381
+ return !!model && atLeastTier(intrinsicTier(model), "workhorse");
382
+ }
383
+
384
+ /** A model's price for comparing candidates, or undefined when it has none. */
385
+ export type ModelPrice = (model: Model<Api>) => number | undefined;
386
+
396
387
  /**
397
388
  * The budget-gated form the automatic secondary-model pickers all share: same as
398
389
  * `economicalContainedCandidates`, minus the session model itself and anything
399
- * dearer than it. `strict` requires *strictly* cheaper (subagents never upgrade a
400
- * cheap session); non-strict allows equal price (the classifier and reader
401
- * tolerate a same-price screener). With the session price unknown there is no
402
- * demonstrable saving, so `strict` yields nothing while non-strict keeps the
403
- * tier-ranked list. `contained` is forwarded to skip the containment recompute.
390
+ * dearer than it. `strict` requires *strictly* cheaper (the subagent default and
391
+ * the classifier: a same-price model saves nothing); non-strict allows equal
392
+ * price (the reader). With the session price unknown there is no demonstrable
393
+ * saving, so `strict` yields nothing while non-strict keeps the tier-ranked
394
+ * list. `contained` is forwarded to skip the containment recompute. `price`
395
+ * replaces the list input price (the classifier judges per-call cost).
404
396
  */
405
397
  export function cheaperContainedCandidates(
406
398
  available: Model<Api>[],
407
399
  sessionModel: Model<Api>,
408
- opts: { strict?: boolean; contained?: Model<Api>[]; role?: FloorRole; requireImageInput?: boolean } = {},
400
+ opts: { strict?: boolean; contained?: Model<Api>[]; requireImageInput?: boolean; price?: ModelPrice } = {},
409
401
  ): Model<Api>[] {
402
+ const priceOf = opts.price ?? pricedInput;
410
403
  const sessionSpec = modelSpec(sessionModel);
411
- const sessionPrice = pricedInput(sessionModel);
412
- const cheaper = economicalContainedCandidates(available, sessionModel, opts.contained, opts.requireImageInput).filter((model) => {
404
+ const sessionPrice = priceOf(sessionModel);
405
+ return economicalContainedCandidates(available, sessionModel, opts.contained, opts.requireImageInput).filter((model) => {
413
406
  if (modelSpec(model) === sessionSpec) return false;
414
407
  if (sessionPrice === undefined) return !opts.strict;
415
- const price = pricedInput(model);
408
+ const price = priceOf(model);
416
409
  return price !== undefined && (opts.strict ? price < sessionPrice : price <= sessionPrice);
417
410
  });
418
- return opts.role ? rankByCapability(cheaper, sessionModel, opts.role).map((entry) => entry.model) : cheaper;
419
411
  }
420
412
 
421
413
  /**
422
- * The weakest tier an AUTOMATIC pick may run on for a session: workhorse when
423
- * the session itself is workhorse or frontier (Claude Code's `min(main, sonnet)`
424
- * classifier rule), cheap otherwise (a cheap session has nothing cheaper and
425
- * capable; `tiny` is excluded upstream regardless). Shared by the auto-mode
426
- * classifier and the subagent default since 2026-09-11: a delegated worker
427
- * writes code and calls tools for many turns, and a weak one spends the saving
428
- * on retries — so both roles are held to one floor and, on most providers, one
429
- * model (docs/decisions/model-policy.md).
414
+ * The subagent default and the classifier's automatic pick: the strictly
415
+ * cheaper contained candidates (`cheaperContainedCandidates`) in the session's
416
+ * own tier or above, cheapest first. A workhorse session delegates to the cheapest workhorse-or-better
417
+ * model, a frontier session to the cheapest frontier one; a tiny session, whose
418
+ * tier automatic selection never picks, to the cheapest cheap-or-better one.
419
+ * The head is "the cheapest model this provider offers that is not weaker than
420
+ * the session's".
430
421
  */
431
- export function automaticTierFloor(sessionModel: Model<Api>): PromptTier {
432
- const tier = intrinsicTier(sessionModel);
433
- return tier === "frontier" || tier === "workhorse" ? "workhorse" : "cheap";
434
- }
435
-
436
- /**
437
- * The floor-gated form the classifier and the subagent default share: the
438
- * cheaper contained candidates (`cheaperContainedCandidates`) that either
439
- * measurably reach the role's capability floor (an Artificial Analysis
440
- * snapshot, when one exists — `capability-index.ts`) or, unscored, sit at or
441
- * above `automaticTierFloor(sessionModel)` by name class. Measured failures are
442
- * already dropped upstream. The head is "the cheapest model this provider
443
- * offers that is capable enough for the session".
444
- */
445
- export function capableContainedCandidates(
422
+ export function sameTierContainedCandidates(
446
423
  available: Model<Api>[],
447
424
  sessionModel: Model<Api>,
448
- role: FloorRole,
449
- opts: { strict?: boolean; contained?: Model<Api>[]; requireImageInput?: boolean } = {},
425
+ opts: { contained?: Model<Api>[]; requireImageInput?: boolean; price?: ModelPrice } = {},
450
426
  ): Model<Api>[] {
451
- const floor = automaticTierFloor(sessionModel);
452
- return rankedContainedCandidates(available, sessionModel, role, opts)
453
- .filter(({ model, measured }) => measured === "pass" || atLeastTier(intrinsicTier(model), floor))
454
- .map((entry) => entry.model);
455
- }
456
-
457
- interface RankedCandidate {
458
- model: Model<Api>;
459
- /** "pass": measurably reaches the floor; "unscored": no snapshot or no confirmed score (the caller's name-class rule decides). */
460
- measured: "pass" | "unscored";
461
- }
462
-
463
- /**
464
- * `cheaperContainedCandidates` with the measured verdict kept on each entry, so a
465
- * caller that must tell a measured pass from an unscored candidate (the
466
- * classifier's name-class fallback) reads it instead of recomputing it.
467
- */
468
- function rankedContainedCandidates(
469
- available: Model<Api>[],
470
- sessionModel: Model<Api>,
471
- role: FloorRole,
472
- opts: { strict?: boolean; contained?: Model<Api>[]; requireImageInput?: boolean } = {},
473
- ): RankedCandidate[] {
474
- return rankByCapability(cheaperContainedCandidates(available, sessionModel, opts), sessionModel, role);
475
- }
476
-
477
- /**
478
- * The measured capability floor (`capability-index.ts`) over a tier-ranked
479
- * candidate list: candidates the Artificial Analysis snapshot shows reaching
480
- * min(session, Sonnet 5) come first, cheapest first — capability per dollar is
481
- * the question here, and a flash-class model that measurably matches the
482
- * session model should beat a dearer "workhorse"-named one. Measured failures
483
- * are dropped. Unscored candidates (no key, no confirmed match) follow in their
484
- * tier order, for the caller's name-class rule to judge. The score never admits
485
- * anything the tier gate already refused (tiny, prior generation, no tools).
486
- */
487
- function rankByCapability(candidates: Model<Api>[], sessionModel: Model<Api>, role: FloorRole): RankedCandidate[] {
488
- const snapshot = currentCapabilitySnapshot();
489
- if (!snapshot) return candidates.map((model) => ({ model, measured: "unscored" }));
490
- const measured: RankedCandidate[] = [];
491
- const unscored: RankedCandidate[] = [];
492
- for (const model of candidates) {
493
- const verdict = capabilityFloor(snapshot, model, sessionModel, role).verdict;
494
- if (verdict === "pass") measured.push({ model, measured: "pass" });
495
- else if (verdict === "unscored") unscored.push({ model, measured: "unscored" });
496
- }
497
- const price = (entry: RankedCandidate) => pricedInput(entry.model) ?? Number.POSITIVE_INFINITY;
498
- return [...measured.sort((a, b) => price(a) - price(b)), ...unscored];
499
- }
500
-
501
- /** The cached Artificial Analysis snapshot, if a key has ever produced one (`capability-index.ts`). */
502
- export function currentCapabilitySnapshot(): CapabilitySnapshot | undefined {
503
- return loadCapabilitySnapshot(oneCodeStateDir());
427
+ const priceOf = opts.price ?? pricedInput;
428
+ const floor = intrinsicTier(sessionModel);
429
+ return cheaperContainedCandidates(available, sessionModel, { ...opts, strict: true })
430
+ .filter((model) => atLeastTier(intrinsicTier(model), floor))
431
+ .sort((a, b) => priceOf(a)! - priceOf(b)!);
504
432
  }
505
433
 
506
434
  export interface EconomicalModelChoice {
@@ -509,7 +437,7 @@ export interface EconomicalModelChoice {
509
437
  }
510
438
 
511
439
  /**
512
- * The cheapest capable same-provider model, else the session model itself. Used
440
+ * The cheapest usable same-provider model, else the session model itself. Used
513
441
  * for low-stakes one-shot jobs over untrusted content (the web_fetch reader,
514
442
  * the recap) — the content goes to the model, so containment applies. Never
515
443
  * dearer than the session model, and never a `tiny`-tier model
@@ -521,6 +449,6 @@ export function pickEconomicalContainedModel(
521
449
  sessionModel: Model<Api> | undefined,
522
450
  ): EconomicalModelChoice | undefined {
523
451
  if (!sessionModel) return undefined;
524
- const cheaper = cheaperContainedCandidates(available, sessionModel, { role: "reader" })[0];
452
+ const cheaper = cheaperContainedCandidates(available, sessionModel)[0];
525
453
  return cheaper ? { model: cheaper, via: "tier" } : { model: sessionModel, via: "session" };
526
454
  }