one-code-extension 0.4.1 → 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 +16 -5
  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 +58 -31
  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 +18 -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,16 +1,17 @@
1
1
  /**
2
2
  * The classifier call itself — the one file here that talks to a provider.
3
3
  *
4
- * Claude Code's two-stage formula, reproduced (docs/decisions/auto-mode.md, P4):
4
+ * Claude Code's two-stage formula, reproduced (working-docs/decisions/auto-mode.md, P4):
5
5
  * Stage 1 grades HARM ONLY (maxTokens 64). severity < 50 → allow, no stage 2.
6
- * Stage 2 applies intent + ALLOW (maxTokens 8192, <thinking> CoT) → severity +
6
+ * Stage 2 applies intent + ALLOW (maxTokens 4096, <thinking> CoT) → severity +
7
7
  * <category> (+ our verified <intent>). Both stages share one system prompt and
8
- * transcript byte-for-byte. Only the system prompt (the ~30k-token ruleset) is
9
- * a cache hit across stages and calls: pi-ai marks a breakpoint on the last
10
- * user block only, so the <transcript> (up to ~15k tokens) is re-read uncached
11
- * by stage 2 and by every gated call on Anthropic-style providers. Known,
12
- * deferred — docs/decisions/caching.md "Classifier transcript" and
13
- * docs/upstream_prs.md #16 (fixable locally via pi-ai's `onPayload`).
8
+ * transcript byte-for-byte. Where the request carries explicit breakpoints
9
+ * (Anthropic Messages, OpenRouter's Anthropic-format Chat Completions,
10
+ * Bedrock Converse), onPayload splits the user text at stable history-entry
11
+ * boundaries and moves the user cache marker before the pending action and
12
+ * stage instruction (cache.ts). Both stages and later gated calls can read
13
+ * that prefix; the system markers stay intact. Other APIs keep the single
14
+ * string and their provider's prefix caching.
14
15
  *
15
16
  * A one-shot `completeSimple` per stage rather than an agent session: no tools,
16
17
  * no history beyond the transcript it is handed, nothing to be talked into. Every
@@ -19,11 +20,15 @@
19
20
  * has approved nothing.
20
21
  */
21
22
 
22
- import type { Model, Api, AssistantMessage, ThinkingLevel } from "@earendil-works/pi-ai";
23
+ import { appendFileSync } from "node:fs";
24
+ import { cleanupSessionResources, isContextOverflow, type Model, type Api, type AssistantMessage, type ThinkingLevel } from "@earendil-works/pi-ai";
23
25
  import { completeSimple } from "@earendil-works/pi-ai/compat";
24
26
  import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
25
- import { forcedReasoningLevel, isReasoningMandatoryError, reasoningRetryLevel } from "../lib/model-policy.ts";
27
+ import { costRatioText, isNotablyCostlier } from "../lib/model-choice-warnings.ts";
28
+ import { modeCycleKey } from "../lib/keys.ts";
29
+ import { classifierCostRatio, forcedReasoningLevel, isReasoningMandatoryError, modelSpec as spec, reasoningRetryLevel } from "../lib/model-policy.ts";
26
30
  import type { AutoModeConfig } from "./config.ts";
31
+ import { CLASSIFIER_CACHE_APIS, cacheClassifierHistory } from "./cache.ts";
27
32
  import {
28
33
  buildPayload,
29
34
  type ClassifyRequest,
@@ -37,6 +42,7 @@ import {
37
42
  } from "./prompt.ts";
38
43
  import {
39
44
  type Candidate,
45
+ type ClassifierFallback,
40
46
  type ClassifierNotice,
41
47
  classifierCandidates,
42
48
  describeCandidate,
@@ -44,10 +50,25 @@ import {
44
50
  replyText,
45
51
  withAuthBaseUrl,
46
52
  } from "./model-select.ts";
53
+ import { actionLength, MAX_ACTION_CHARS } from "./transcript.ts";
47
54
 
48
55
  /** A slow classifier stalls every tool call, so the wait is capped per stage. */
49
56
  export const CLASSIFIER_TIMEOUT_MS = 30_000;
50
57
 
58
+ /**
59
+ * Claude Code's default retry count for a rate limit or server error (its
60
+ * classifier's side query uses it). pi-ai's default is none, so one 429 left
61
+ * the call unjudged; the stage timeout above still bounds the retries.
62
+ */
63
+ export const CLASSIFIER_MAX_RETRIES = 10;
64
+
65
+ /**
66
+ * Output headroom for a model that cannot turn thinking off, added to both
67
+ * stages' caps as Claude Code pads its always-on-thinking models: the
68
+ * reasoning otherwise spends the budget before the verdict is written.
69
+ */
70
+ export const FORCED_THINKING_PADDING = 2048;
71
+
51
72
  /**
52
73
  * A pinned model that times out this many calls in a row is unpinned, so a model
53
74
  * that was fast when first chosen but has since fallen behind demand does not
@@ -55,6 +76,19 @@ export const CLASSIFIER_TIMEOUT_MS = 30_000;
55
76
  */
56
77
  export const ROTATE_AFTER_TIMEOUTS = 2;
57
78
 
79
+ /**
80
+ * The provider refused the classifier's prompt as a possible usage-policy
81
+ * violation ("Invalid prompt: your prompt was flagged as potentially violating
82
+ * our usage policy"). GPT-6 Luna on Codex did this to about one classifier
83
+ * prompt in ten, at random, and passed the same prompt on a retry (findings
84
+ * §59). A refused call is retried once on the same model; a second refusal
85
+ * drops the model for the rest of the session (decisions/auto-mode.md,
86
+ * "A classifier that refuses its prompt").
87
+ */
88
+ export function isPromptPolicyRefusal(message: string): boolean {
89
+ return /invalid prompt\b[^.\n]{0,80}\bflagged\b[^.\n]{0,80}usage policy/i.test(message);
90
+ }
91
+
58
92
  /**
59
93
  * Whether a failure is our own classifier timeout (or an equivalent abort/stall)
60
94
  * rather than a substantive provider error. A timeout is transient — worth a
@@ -66,6 +100,42 @@ export function isClassifierTimeout(message: string): boolean {
66
100
  return /\babort|\btimed?\s*out\b|\btimeout\b|\betimedout\b|\bdeadline\b/i.test(message);
67
101
  }
68
102
 
103
+ /**
104
+ * Whether a failure happened below HTTP: the request never got an answer
105
+ * (`fetch failed`, a reset or refused connection, a DNS miss). Like a timeout it
106
+ * says nothing about the call or the model, so it takes the timeout path: one
107
+ * retry, then a "not judged" block. A provider's own error reply (a 500, a 429)
108
+ * is not one; it stays a substantive error. Observed 2026-10-04: a few minutes
109
+ * of `fetch failed` on openai-codex blocked read-only calls as "denied".
110
+ */
111
+ export function isTransportFailure(message: string): boolean {
112
+ return /\bfetch failed\b|\beconn(reset|refused|aborted)\b|\benotfound\b|\beai_again\b|\bepipe\b|socket hang up|other side closed|\bund_err_socket\b|network (error|is unreachable)|\bwebsocket (closed|error)\b/i.test(
113
+ message,
114
+ );
115
+ }
116
+
117
+ /**
118
+ * A provider's error without its advice to the user ("Please try again with a
119
+ * different prompt", "try rephrasing your request", a link): handed to the
120
+ * model in a block, that advice invites rephrasing the call past the gate.
121
+ */
122
+ export function providerErrorSummary(message: string): string {
123
+ // Advice follows a sentence end, a colon or semicolon, or a line break.
124
+ const cut = message.search(/(?:[.!:;]\s+|\n\s*)(please\b|if this seems|try\b|you can\b|learn more|see\b)/i);
125
+ const kept = cut >= 0 ? message.slice(0, cut + 1) : message;
126
+ return kept.replace(/:?\s*https?:\/\/\S+/g, "").trim().replace(/[:;]$/, "");
127
+ }
128
+
129
+ /** A provider rejected the input window, not the action's safety or output limit. */
130
+ export function isClassifierTranscriptTooLong(message: string): boolean {
131
+ // Only pi-ai's error-pattern branch: no catalog window means this never
132
+ // infers overflow from an estimate or a model's reported usage.
133
+ return isContextOverflow({ stopReason: "error", errorMessage: message } as AssistantMessage);
134
+ }
135
+
136
+ /** Pause before retrying after a transport failure, so a brief network drop can clear. */
137
+ export const TRANSPORT_RETRY_DELAY_MS = 1500;
138
+
69
139
  /**
70
140
  * Which model the classifier settled on, and which candidates turned out to be
71
141
  * unusable. Owned by the caller so this module stays stateless, and so the
@@ -76,6 +146,12 @@ export interface ClassifierState {
76
146
  pinned?: Model<Api>;
77
147
  /** `provider/id` of candidates that failed as unusable. */
78
148
  rejected: Set<string>;
149
+ /**
150
+ * The subset of `rejected` that refused the classifier's prompt twice. Unlike
151
+ * another rejection, such a model is not retried when every candidate is
152
+ * gone: it would refuse again, so the gate stops and says how to go on.
153
+ */
154
+ refused: Set<string>;
79
155
  /** Notices already delivered, so a per-call warning is not repeated per call. */
80
156
  notified: Set<string>;
81
157
  /**
@@ -84,15 +160,15 @@ export interface ClassifierState {
84
160
  */
85
161
  timeoutStreak: number;
86
162
  /**
87
- * The candidate chain cached for the current (session model, classifier config)
88
- * signature, so the O(catalog) ranking is not rebuilt on every gated tool call.
89
- * Invalidated only when the session model or the `classifierModel` setting
90
- * changes — catalog churn is deliberately ignored, matching the pin, which
163
+ * The candidate chain cached for the current session-model/window signature,
164
+ * so the O(catalog) ranking is not rebuilt on every gated tool call.
165
+ * Invalidated when the session model or its catalog context window changes —
166
+ * catalog churn is otherwise deliberately ignored, matching the pin, which
91
167
  * already commits the session to one classifier. The live `rejected` filter is
92
168
  * still applied to the cached chain per call, so a model that dies mid-session
93
169
  * is still stepped over without a rebuild.
94
170
  */
95
- chainCache?: { signature: string; candidates: Candidate[]; notices: ClassifierNotice[] };
171
+ chainCache?: { signature: string; candidates: Candidate[]; notices: ClassifierNotice[]; fallback?: ClassifierFallback };
96
172
  /**
97
173
  * `provider/id` -> the thinking level a model turned out to REQUIRE (its
98
174
  * provider rejected the thinking-off request with "reasoning is mandatory").
@@ -102,10 +178,28 @@ export interface ClassifierState {
102
178
  * proactively, without an error).
103
179
  */
104
180
  forcedReasoning: Map<string, ThinkingLevel>;
181
+ /** Previous history boundary on a breakpoint API, a cache lookup hint only (cache.ts). */
182
+ cacheHistoryEnd?: number;
105
183
  }
106
184
 
107
185
  export function createClassifierState(): ClassifierState {
108
- return { rejected: new Set(), notified: new Set(), timeoutStreak: 0, forcedReasoning: new Map() };
186
+ return { rejected: new Set(), refused: new Set(), notified: new Set(), timeoutStreak: 0, forcedReasoning: new Map() };
187
+ }
188
+
189
+ /** The `cacheKey` a session's classifier calls pass (`ClassifierDeps.cacheKey`). */
190
+ export function classifierCacheKey(sessionId: string): string {
191
+ return `${sessionId}:auto-mode`;
192
+ }
193
+
194
+ /**
195
+ * Close what pi-ai holds open under the session's classifier key. The key
196
+ * doubles as pi-ai's `sessionId`, and on openai-codex that keeps one WebSocket
197
+ * per id with a 5-minute idle timer holding the process; pi's own dispose
198
+ * closes only the session's id, so without this a `pi -p` run waits out the
199
+ * timer after its answer (findings §59).
200
+ */
201
+ export function releaseClassifierResources(sessionId: string): void {
202
+ cleanupSessionResources(classifierCacheKey(sessionId));
109
203
  }
110
204
 
111
205
  export interface ClassifierDeps {
@@ -114,8 +208,15 @@ export interface ClassifierDeps {
114
208
  config: AutoModeConfig;
115
209
  signal?: AbortSignal;
116
210
  state: ClassifierState;
211
+ /**
212
+ * A stable key for this session's classifier calls. pi-ai sends it as
213
+ * OpenAI's prompt_cache_key (Codex and the OpenAI APIs), which routes the
214
+ * calls to the cache that holds the previous call's prefix; without it
215
+ * consecutive calls landed on different cache machines and missed.
216
+ */
217
+ cacheKey?: string;
117
218
  /** Tell the user something once — which model is in use, or that theirs is dead. */
118
- onNotice?: (message: string, level: "info" | "warning") => void;
219
+ onNotice?: (message: string, level: "info" | "warning", notice?: ClassifierNotice) => void;
119
220
  /** Report each classifier reply's usage, for the all-in footer cost. Observer
120
221
  * only — it never affects the payload sent or the verdict parsed. */
121
222
  onUsage?: (usage: unknown) => void;
@@ -135,7 +236,7 @@ export interface ClassifierDeps {
135
236
  }
136
237
 
137
238
  /** How a single candidate's attempt failed, so the outer loop can react. */
138
- type StepKind = "timeout" | "cancelled" | "unavailable" | "error" | "truncated";
239
+ type StepKind = "timeout" | "cancelled" | "unavailable" | "refused" | "error" | "truncated" | "transcript-too-long";
139
240
  class StepError extends Error {
140
241
  constructor(
141
242
  readonly kind: StepKind,
@@ -146,42 +247,80 @@ class StepError extends Error {
146
247
  }
147
248
 
148
249
  /** Signature that must stay equal for the cached chain to be reused (see ClassifierState.chainCache). */
149
- function selectionSignature(deps: ClassifierDeps): string {
150
- const session = deps.sessionModel ? `${deps.sessionModel.provider}/${deps.sessionModel.id}` : "(none)";
151
- return `${session}|${deps.config.classifierModel ?? ""}|${deps.config.classifierModelSetFor ?? ""}`;
250
+ function selectionSignature(sessionModel: Model<Api> | undefined, config: Pick<AutoModeConfig, "classifierModel" | "classifierModelSetFor">): string {
251
+ const configured = `${config.classifierModel ?? ""}|${config.classifierModelSetFor ?? ""}`;
252
+ return sessionModel ? `${sessionModel.provider}/${sessionModel.id}|${sessionModel.contextWindow ?? "unknown"}|${configured}` : `(none)|${configured}`;
253
+ }
254
+
255
+ /**
256
+ * The selection chain for this session model and setting, built once per
257
+ * signature and shared by every reader: the gate, the badge and banner that
258
+ * name the next classifier, and the announcement. The newer-model warnings
259
+ * are always in it; a reader that honours `suggestNewerModels` drops them.
260
+ */
261
+ export function classifierChain(
262
+ state: ClassifierState,
263
+ input: { available: () => Model<Api>[]; sessionModel: Model<Api> | undefined; config: Pick<AutoModeConfig, "classifierModel" | "classifierModelSetFor"> },
264
+ ): NonNullable<ClassifierState["chainCache"]> {
265
+ const signature = selectionSignature(input.sessionModel, input.config);
266
+ const cached = state.chainCache;
267
+ if (cached && cached.signature === signature) return cached;
268
+ const built = classifierCandidates({
269
+ available: input.available(),
270
+ sessionModel: input.sessionModel,
271
+ configured: input.config.classifierModel,
272
+ configuredSetForContainment: input.config.classifierModelSetFor,
273
+ });
274
+ const chain = { signature, candidates: built.candidates, notices: built.notices, ...(built.fallback ? { fallback: built.fallback } : {}) };
275
+ // Don't poison the cache with an empty chain (e.g. a not-yet-populated
276
+ // registry) — that would permanently block the gate; recompute next call.
277
+ if (built.candidates.length > 0) state.chainCache = chain;
278
+ return chain;
152
279
  }
153
280
 
154
281
  /** The candidate chain (minus anything already unusable this session) and its notices. */
155
282
  function remainingCandidates(deps: ClassifierDeps): { candidates: Candidate[]; notices: ClassifierNotice[] } {
156
- const signature = selectionSignature(deps);
157
- let cached = deps.state.chainCache;
158
- if (!cached || cached.signature !== signature) {
159
- const built = classifierCandidates({
160
- available: deps.registry.getAvailable(),
161
- sessionModel: deps.sessionModel,
162
- configured: deps.config.classifierModel,
163
- configuredSetForContainment: deps.config.classifierModelSetFor,
164
- });
165
- cached = { signature, candidates: built.candidates, notices: built.notices };
166
- // Don't poison the cache with an empty chain (e.g. a not-yet-populated
167
- // registry) — that would permanently block the gate; recompute next call.
168
- if (built.candidates.length > 0) deps.state.chainCache = cached;
169
- }
170
- const { candidates: all, notices } = cached;
283
+ const { candidates: all, notices } = classifierChain(deps.state, { available: () => deps.registry.getAvailable(), sessionModel: deps.sessionModel, config: deps.config });
171
284
  const usable = all.filter((entry) => !deps.state.rejected.has(`${entry.model.provider}/${entry.model.id}`));
172
285
  if (usable.length > 0) return { candidates: usable, notices };
173
286
  // If everything has been rejected, the session model is still worth one more
174
287
  // attempt: a gate that stops asking has stopped gating. It has to be the
175
288
  // session model specifically — the *last* chain entry is the cost-ranked pick,
176
- // which nothing price-chosen should lead, let alone be the sole retry.
289
+ // which nothing price-chosen should lead, let alone be the sole retry. A model
290
+ // that refused the prompt is not retried: it would refuse again
291
+ // (`refusalStop`).
177
292
  const session = all.filter((entry) => entry.source === "session");
178
- return { candidates: session.length > 0 ? session : all.slice(-1), notices };
293
+ const retry = session.length > 0 ? session : all.slice(-1);
294
+ return { candidates: retry.filter((entry) => !deps.state.refused.has(`${entry.model.provider}/${entry.model.id}`)), notices };
179
295
  }
180
296
 
181
- function notifyOnce(deps: ClassifierDeps, key: string, message: string, level: "info" | "warning") {
297
+ /**
298
+ * Every candidate is gone and one refused the classifier's prompt: the call is
299
+ * not judged, and the user is told once how to go on.
300
+ */
301
+ function refusalStop(deps: ClassifierDeps, unusable: readonly string[] = []): ClassifyVerdict {
302
+ const refused = [...deps.state.refused].join(", ");
303
+ // A model that failed this call for another cause has its own fix: name it too.
304
+ const others = unusable.length > 0 ? ` ${unusable.join("; ")}.` : "";
305
+ notifyOnce(
306
+ deps,
307
+ "refusal-stop",
308
+ `Auto mode has no classifier left: ${refused} refused the classifier's prompt as a possible usage-policy violation, and no other eligible model can screen calls.${others} ` +
309
+ `Calls are not judged and do not run. Switch to manual approval (${modeCycleKey()} cycles permission modes), or choose another classifier with /auto-mode model <provider/id>.`,
310
+ "warning",
311
+ );
312
+ return {
313
+ decision: "block",
314
+ reason: `Auto mode has no classifier: ${refused} refused the classifier's prompt as a possible usage-policy violation.${others}`,
315
+ tier: "unmatched",
316
+ noVerdict: true,
317
+ };
318
+ }
319
+
320
+ function notifyOnce(deps: ClassifierDeps, key: string, message: string, level: "info" | "warning", notice?: ClassifierNotice) {
182
321
  if (deps.state.notified.has(key)) return;
183
322
  deps.state.notified.add(key);
184
- deps.onNotice?.(message, level);
323
+ deps.onNotice?.(message, level, notice);
185
324
  }
186
325
 
187
326
  /**
@@ -191,17 +330,31 @@ function notifyOnce(deps: ClassifierDeps, key: string, message: string, level: "
191
330
  export async function classify(request: ClassifyRequest, deps: ClassifierDeps): Promise<ClassifyVerdict> {
192
331
  const { candidates, notices } = remainingCandidates(deps);
193
332
  if (candidates.length === 0) {
194
- return { decision: "block", reason: "No model is available to run the auto-mode classifier.", tier: "unmatched" };
333
+ if (deps.state.refused.size > 0) return refusalStop(deps);
334
+ return { decision: "block", reason: "No model is available to run the auto-mode classifier.", tier: "unmatched", noVerdict: true };
195
335
  }
196
336
 
197
- // Selection notices (a configured model unavailable or overridden as stale, a
198
- // cross-provider setting honored) — surfaced once each at their own level, keyed
199
- // by their text so an informational "honored" line is not shown as a warning.
200
- for (const notice of notices) notifyOnce(deps, notice.text, notice.text, notice.level);
337
+ // Automatic-selection fallback explanations are surfaced once per distinct message.
338
+ // A newer-model hint about a chosen classifier is the session-start
339
+ // announcement's to make (it honours suggestNewerModels), not a gated call's.
340
+ for (const notice of notices) if (notice.choiceWarning !== "newer") notifyOnce(deps, notice.text, notice.text, notice.level, notice);
341
+
342
+ // The action is sent whole (transcript.ts MAX_ACTION_CHARS); one too large for
343
+ // that is refused with its size, never judged from a prefix.
344
+ const actionChars = deps.reviewOnly ? 0 : actionLength(request.transcript);
345
+ if (actionChars > MAX_ACTION_CHARS) {
346
+ return {
347
+ decision: "block",
348
+ reason: `This call is ${actionChars} characters, more than the ${MAX_ACTION_CHARS} the auto-mode classifier reviews in full. Split it into smaller calls.`,
349
+ tier: "unmatched",
350
+ noVerdict: true,
351
+ };
352
+ }
201
353
 
202
354
  // buildPayload builds the ~110KB ruleset once and returns the grounding index
203
355
  // derived from it, so the ruleset is not rebuilt/re-parsed a second time here.
204
- const { system, userPrefix, index } = buildPayload(request);
356
+ const { system, userPrefix, history, tail, index } = buildPayload(request);
357
+ const previousHistoryEnd = deps.state.cacheHistoryEnd;
205
358
  const stage1Text = stage1User(userPrefix);
206
359
  // A permission rule refused something this turn, so stage 2 gets the extra
207
360
  // instruction that the user's intent does not excuse an equivalent-effect
@@ -209,6 +362,10 @@ export async function classify(request: ClassifyRequest, deps: ClassifierDeps):
209
362
  // left byte-identical to CC's.
210
363
  const afterRuleDenial = request.transcript.some((entry) => entry.kind === "denied");
211
364
  const debug = process.env.CC_AUTO_MODE_DEBUG;
365
+ // Probe hook (test/e2e/cache-probe.sh): one JSON line per classifier reply with
366
+ // its text and pi-ai's normalized usage, whatever the transport (Codex runs
367
+ // over WebSocket, where a fetch wrapper sees nothing). Opt-in, never read back.
368
+ const usageLog = process.env.CC_AUTO_MODE_LOG;
212
369
 
213
370
  // A model already pinned this session is tried first, so the classifier does
214
371
  // not change under the session while the pin is healthy. The rest of the chain
@@ -234,7 +391,11 @@ export async function classify(request: ClassifyRequest, deps: ClassifierDeps):
234
391
 
235
392
  let lastError = "";
236
393
  let sawTimeout = false;
394
+ /** Candidates rejected this call as unusable, with their errors. */
395
+ const unusable: string[] = [];
237
396
  let timedOutKey: string | undefined;
397
+ /** The timed-out candidate's own error: a later candidate's failure must not relabel it. */
398
+ let timedOutError = "";
238
399
  // Walks `attempts` in order; a candidate that turns out to require thinking is
239
400
  // re-queued at the front for exactly one retry (see the reasoning-mandatory
240
401
  // branch below). The `!== undefined` guard narrows `candidate`, so no cast.
@@ -257,7 +418,7 @@ export async function classify(request: ClassifyRequest, deps: ClassifierDeps):
257
418
  // One provider call. `reasoning`/`temperature` are deliberately omitted: pi
258
419
  // turns thinking off entirely when `reasoning` is absent (what a classifier
259
420
  // wants), and `temperature` is deprecated/unsupported on several models and
260
- // fails closed. See docs/decisions/auto-mode.md. The one exception: a model
421
+ // fails closed. See working-docs/decisions/auto-mode.md. The one exception: a model
261
422
  // that CANNOT disable thinking (catalog-marked, or learned via the
262
423
  // reasoning-mandatory retry below) gets its lowest supported level — the
263
424
  // off-request would 400 and the gate would fail closed on every call.
@@ -279,7 +440,15 @@ export async function classify(request: ClassifyRequest, deps: ClassifierDeps):
279
440
  env: auth.env,
280
441
  signal: deps.signal ? AbortSignal.any([deps.signal, timeout]) : timeout,
281
442
  maxTokens,
443
+ maxRetries: CLASSIFIER_MAX_RETRIES,
282
444
  cacheRetention: "long",
445
+ ...(deps.cacheKey ? { sessionId: deps.cacheKey } : {}),
446
+ ...(CLASSIFIER_CACHE_APIS.has(model.api) ? {
447
+ onPayload: (payload: unknown) => {
448
+ cacheClassifierHistory(payload, [...history, tail, userText.slice(userPrefix.length)], history.length - 1, previousHistoryEnd);
449
+ deps.state.cacheHistoryEnd = history.length - 1;
450
+ },
451
+ } : {}),
283
452
  ...(reasoning ? { reasoning } : {}),
284
453
  },
285
454
  );
@@ -288,13 +457,25 @@ export async function classify(request: ClassifyRequest, deps: ClassifierDeps):
288
457
  // Turn one reply into text, or throw a StepError the loop knows how to route.
289
458
  // `big` is the retry cap for a length-truncated reply; a still-truncated reply
290
459
  // at `big` is its own outcome (not a candidate to step past).
460
+ const padding = reasoning ? FORCED_THINKING_PADDING : 0;
291
461
  const call = async (userText: string, base: number, big: number, stage: number): Promise<string> => {
462
+ base += padding;
463
+ big += padding;
292
464
  const inspect = (reply: AssistantMessage): string | "length" => {
293
465
  deps.onUsage?.(reply.usage);
466
+ if (usageLog) {
467
+ try {
468
+ appendFileSync(usageLog, `${JSON.stringify({ model: key, api: model.api, stage, system, user: userText, usage: reply.usage, stopReason: reply.stopReason })}\n`);
469
+ } catch {
470
+ // A probe log that cannot be written must not fail the gate.
471
+ }
472
+ }
294
473
  if (deps.signal?.aborted) throw new StepError("cancelled", "cancelled");
295
474
  if (reply.stopReason === "error" || reply.stopReason === "aborted") {
296
475
  const msg = reply.errorMessage ?? reply.stopReason ?? "provider error";
297
- if (isClassifierTimeout(msg)) throw new StepError("timeout", msg);
476
+ if (isContextOverflow(reply)) throw new StepError("transcript-too-long", msg);
477
+ if (isClassifierTimeout(msg) || isTransportFailure(msg)) throw new StepError("timeout", msg);
478
+ if (isPromptPolicyRefusal(msg)) throw new StepError("refused", msg);
298
479
  throw new StepError(isModelUnavailableError(msg) ? "unavailable" : "error", msg);
299
480
  }
300
481
  if (reply.stopReason === "length") return "length";
@@ -306,10 +487,13 @@ export async function classify(request: ClassifyRequest, deps: ClassifierDeps):
306
487
  try {
307
488
  seen = inspect(reply);
308
489
  } catch (error) {
309
- // One retry on a timeout, before giving up on this model — a stall under
310
- // load usually clears on a second try. A user cancel or a substantive
311
- // error is not retried.
312
- if (error instanceof StepError && error.kind === "timeout") {
490
+ // One retry on a timeout or a usage-policy refusal, before giving up on
491
+ // this model: a stall under load usually clears on a second try, and a
492
+ // refused prompt usually passes. The identical request still reads the
493
+ // session's cached prefix. A user cancel or a substantive error is not
494
+ // retried.
495
+ if (error instanceof StepError && (error.kind === "timeout" || error.kind === "refused")) {
496
+ if (error.kind === "timeout" && isTransportFailure(error.message)) await new Promise((resolve) => setTimeout(resolve, TRANSPORT_RETRY_DELAY_MS));
313
497
  reply = await runCall(userText, base, stage);
314
498
  seen = inspect(reply);
315
499
  } else {
@@ -331,12 +515,13 @@ export async function classify(request: ClassifyRequest, deps: ClassifierDeps):
331
515
  if (deps.reviewOnly) {
332
516
  // A finished run has no harm floor to short-circuit — go straight to the
333
517
  // full stage-2 evaluation, reframed to judge the whole sequence.
334
- const text = await call(reviewUser(userPrefix), 1024, 4096, 2);
518
+ const text = await call(reviewUser(userPrefix), 4096, 8192, 2);
335
519
  verdict = parseStage2(text, index, request.userMessages);
336
520
  stageInfo = `review sev=${parseSeverity(text) ?? "?"}`;
337
521
  } else {
338
522
  // STAGE 1 — harm only. maxTokens 64 like CC; if the reply overruns 64
339
523
  // without a readable severity, one retry with headroom recovers it.
524
+ // Stage 2's 4096 is CC's too, with one retry at 8192.
340
525
  const s1 = await call(stage1Text, 64, 1024, 1);
341
526
  const sev1 = parseSeverity(s1);
342
527
  if (sev1 !== null && sev1 < SEVERITY_THRESHOLD) {
@@ -346,7 +531,7 @@ export async function classify(request: ClassifyRequest, deps: ClassifierDeps):
346
531
  // sev1 >= 50, or unparseable → fall to the fuller stage-2 evaluation.
347
532
  // stage2User is built here (not eagerly) so the common allow case
348
533
  // does not concatenate the transcript prefix a second time.
349
- const s2 = await call(stage2User(userPrefix, afterRuleDenial), 1024, 4096, 2);
534
+ const s2 = await call(stage2User(userPrefix, afterRuleDenial), 4096, 8192, 2);
350
535
  verdict = parseStage2(s2, index, request.userMessages);
351
536
  stageInfo = `s1=${sev1 ?? "?"} s2=${parseSeverity(s2) ?? "?"}`;
352
537
  // Stage 1 flagged the call and stage 2 cleared it without quoting the
@@ -385,15 +570,27 @@ export async function classify(request: ClassifyRequest, deps: ClassifierDeps):
385
570
  lastError = error instanceof Error ? error.message : String(error);
386
571
 
387
572
  if (kind === "cancelled") {
388
- return { decision: "block", reason: "Auto-mode classification was cancelled.", tier: "unmatched" };
573
+ return { decision: "block", reason: "Auto-mode classification was cancelled.", tier: "unmatched", noVerdict: true };
574
+ }
575
+ // The provider is authoritative about its token window. Do not clip the
576
+ // history or retry another model: the gate must offer manual approval
577
+ // interactively, or stop a headless run, rather than invent a verdict.
578
+ if (kind === "transcript-too-long" || (kind === "error" && isClassifierTranscriptTooLong(lastError))) {
579
+ return {
580
+ decision: "block",
581
+ reason: `Classifier transcript exceeded context window (${key}). The action was not judged. Compact the session (/compact) and continue.`,
582
+ tier: "unmatched",
583
+ noVerdict: true,
584
+ transcriptTooLong: true,
585
+ };
389
586
  }
390
587
  if (kind === "truncated") {
391
588
  if (debug) process.stderr.write(`[auto-mode] ${key} ${request.toolName} → verdict truncated at maxTokens\n`);
392
589
  return {
393
590
  decision: "block",
394
- reason:
395
- "The approval classifier's reply was cut off by its output limit before the verdict completed. If this keeps happening, pin a stronger classifier model with /auto-mode model.",
591
+ reason: "The approval classifier's reply was cut off by its output limit before the verdict completed.",
396
592
  tier: "unmatched",
593
+ noVerdict: true,
397
594
  };
398
595
  }
399
596
  if (kind === "error") {
@@ -414,29 +611,53 @@ export async function classify(request: ClassifyRequest, deps: ClassifierDeps):
414
611
  }
415
612
  // A substantive, non-transient failure that is not "model unusable here":
416
613
  // surface it rather than papering over something about to clear.
417
- return { decision: "block", reason: `Auto-mode classifier could not be reached (${lastError}).`, tier: "unmatched" };
614
+ return { decision: "block", reason: `The auto-mode classifier returned no verdict (${providerErrorSummary(lastError)}).`, tier: "unmatched", noVerdict: true };
418
615
  }
419
616
  if (kind === "timeout") {
420
617
  // Transient — do NOT reject the model, it may be fine next call. Step to
421
618
  // the next candidate; a pinned model that keeps timing out is unpinned.
422
619
  sawTimeout = true;
423
620
  timedOutKey = key;
621
+ timedOutError = lastError;
424
622
  if (deps.state.pinned && `${deps.state.pinned.provider}/${deps.state.pinned.id}` === key) {
425
623
  deps.state.timeoutStreak += 1;
426
624
  if (deps.state.timeoutStreak >= ROTATE_AFTER_TIMEOUTS) deps.state.pinned = undefined;
427
625
  }
428
626
  continue;
429
627
  }
628
+ if (kind === "refused") {
629
+ // Refused twice in a row: drop the model for the session and pin the next
630
+ // one, rather than pay a fallback's uncached prefix on every refusal. The
631
+ // refusal is about the classifier's prompt, not the account, so the
632
+ // subagent selector is not told.
633
+ reject(key);
634
+ deps.state.refused.add(key);
635
+ const next = queue[0]?.model;
636
+ // A candidate that timed out on this call is still eligible: end the
637
+ // chain as a timeout (after the loop), not as "no classifier left".
638
+ if (!next) {
639
+ if (sawTimeout) continue;
640
+ return refusalStop(deps, unusable);
641
+ }
642
+ const ratio = classifierCostRatio(next, model);
643
+ notifyOnce(
644
+ deps,
645
+ `refused:${key}`,
646
+ `${key} refused the auto-mode classifier's prompt twice as a possible usage-policy violation, so auto mode screens calls with ` +
647
+ `${spec(next)} for the rest of this session${isNotablyCostlier(ratio) ? `, at about ${costRatioText(ratio)} the cost per call` : ""}. ` +
648
+ "Choose another classifier with /auto-mode model <provider/id>.",
649
+ "warning",
650
+ );
651
+ continue;
652
+ }
430
653
  // unavailable: this model is not usable here — step over it and record it.
431
654
  reject(key);
432
655
  deps.onModelUnusable?.(key, lastError);
433
- const isConfigured = candidate.source === "configured";
656
+ unusable.push(`${key} cannot be used (${lastError})`);
434
657
  notifyOnce(
435
658
  deps,
436
659
  `rejected:${key}`,
437
- isConfigured
438
- ? `Auto mode cannot use ${key} from autoMode.classifierModel (${lastError}). Set a different model in ~/.onecode/settings.json.`
439
- : `Auto mode cannot use ${key} as its classifier (${lastError}); trying another model. Set autoMode.classifierModel in ~/.onecode/settings.json to choose one.`,
660
+ `Auto mode cannot use ${key} as its classifier (${lastError}); trying another eligible model.`,
440
661
  "warning",
441
662
  );
442
663
  }
@@ -448,19 +669,23 @@ export async function classify(request: ClassifyRequest, deps: ClassifierDeps):
448
669
  // where it can, and only blocks outright when running non-interactively.
449
670
  if (sawTimeout) {
450
671
  const model = timedOutKey ? timedOutKey.split("/").pop() : undefined;
672
+ const network = isTransportFailure(timedOutError);
451
673
  return {
452
674
  decision: "block",
453
675
  tier: "timeout",
676
+ noVerdict: true,
454
677
  reason:
455
- `Auto mode could not screen this ${request.toolName} call in time — the approval classifier${model ? ` (${model})` : ""} ` +
456
- "is temporarily unavailable (timed out), so the call was not judged either way.",
678
+ `Auto mode could not screen this ${request.toolName} call${network ? "" : " in time"} — the approval classifier${model ? ` (${model})` : ""} ` +
679
+ `is temporarily unavailable (${network ? `network error: ${timedOutError}` : "timed out"}), so the call was not judged either way.`,
457
680
  };
458
681
  }
459
682
 
683
+ if (deps.state.refused.size > 0) return refusalStop(deps, unusable);
460
684
  // Every candidate was unusable — say which knob fixes it.
461
685
  return {
462
686
  decision: "block",
463
- reason: `No usable auto-mode classifier model (last error: ${lastError}). Set autoMode.classifierModel in ~/.onecode/settings.json.`,
687
+ reason: `No usable auto-mode classifier model (last error: ${lastError}).`,
464
688
  tier: "unmatched",
689
+ noVerdict: true,
465
690
  };
466
691
  }