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
@@ -0,0 +1,237 @@
1
+ /** Reject damaged OpenRouter streams before tool execution, without changing the request prefix. */
2
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import { notifyOrPrint } from "../lib/headless-output.ts";
4
+ import { TURN_FAILED_CHANNEL, type TurnFailedEvent } from "../lib/interrupt.ts";
5
+ import { sessionOutlivesTurn } from "../lib/notifications.ts";
6
+ import { isOpenRouter, openRouterProviderName, toolCallCorruptionNotice, type UpstreamFailure } from "../lib/openrouter-generation.ts";
7
+ import { sessionAlive } from "../lib/session-lifecycle.ts";
8
+ import {
9
+ CONTENT_MARKUP_LIMIT,
10
+ RUNAWAY_PADDING_LIMIT,
11
+ scanToolCallMarkup,
12
+ TOOL_CALL_MARKUP_LIMIT,
13
+ type ToolCallDamage,
14
+ type TrailingPadding,
15
+ toolCallCorruptionReason,
16
+ trailingPadding,
17
+ } from "../lib/tool-call-corruption.ts";
18
+ import { withKeepAlive } from "../lsp/keep-alive.ts";
19
+
20
+ // Silence allowed inside a stream. Every raw chunk restarts the clock, but
21
+ // OpenRouter's keepalive comments never reach us, so a model thinking
22
+ // server-side before its first token looks silent: that wait is long. After
23
+ // the first content it is Claude Code's 90 s watchdog, and 60 s while a tool
24
+ // call's arguments stream (arguments never pause for thinking).
25
+ const FIRST_CONTENT_IDLE_MS = 300_000;
26
+ const STREAM_IDLE_MS = 90_000;
27
+ const TOOL_CALL_IDLE_MS = 60_000;
28
+
29
+ export default function toolCallCorruptionExtension(pi: ExtensionAPI) {
30
+ toolCallCorruptionGuard(pi, { announce: true });
31
+ }
32
+
33
+ /**
34
+ * The guard itself. `announce: false` is the in-process child's shape
35
+ * (`child.ts`): it blocks and stops the same way, but a child has no terminal
36
+ * of its own (stderr is the parent's TUI) and is disposed without
37
+ * `session_shutdown`, so it prints nothing and starts no provider lookup that
38
+ * could outlive it. Its parent sees the child's turn end terminated.
39
+ */
40
+ export function toolCallCorruptionGuard(pi: ExtensionAPI, { announce }: { announce: boolean }) {
41
+ const alive = sessionAlive(pi);
42
+ const rawCalls = new Map<number, { raw: string; markup: number; contentMarkup: number; next: number }>();
43
+ const flagged = new Map<string, ToolCallDamage>();
44
+ // Any provider: a call streaming blank padding without end never reaches
45
+ // toolcall_end, and nothing else stops it (gpt-6.1-sol, battery round 7:
46
+ // 26 MB of tabs and newlines over 25 minutes in a one-shot run).
47
+ const padding = new Map<number, TrailingPadding>();
48
+ const pending = new Map<AbortController, Promise<void>>();
49
+ let responseId: string | undefined;
50
+ // OpenRouter names the serving provider on every chunk; a cancelled
51
+ // generation is never recorded, so the lookup cannot name it after a stop.
52
+ let streamProvider: string | undefined;
53
+ let stopped = false;
54
+ // A suspect call goes back to the model once per run; the next one is corrupt.
55
+ let suspectReturned = false;
56
+ let contentStarted = false;
57
+ let idleTimer: ReturnType<typeof setTimeout> | undefined;
58
+ let idleMs = 0;
59
+
60
+ const clearIdle = () => {
61
+ clearTimeout(idleTimer);
62
+ idleTimer = undefined;
63
+ };
64
+ const resetMessage = () => {
65
+ rawCalls.clear();
66
+ flagged.clear();
67
+ padding.clear();
68
+ responseId = undefined;
69
+ streamProvider = undefined;
70
+ contentStarted = false;
71
+ clearIdle();
72
+ };
73
+ const resetRun = () => {
74
+ resetMessage();
75
+ stopped = false;
76
+ suspectReturned = false;
77
+ };
78
+ const cancelLookups = () => {
79
+ for (const controller of pending.keys()) controller.abort();
80
+ pending.clear();
81
+ };
82
+
83
+ /** Abort the turn once and tell the user which upstream provider failed. */
84
+ const stopTurn = (ctx: ExtensionContext, failure: UpstreamFailure) => {
85
+ if (stopped || !alive()) return;
86
+ stopped = true;
87
+ clearIdle();
88
+ const what = typeof failure === "object" ? `stalled ${failure.stalledSeconds} s` : failure;
89
+ pi.events.emit(TURN_FAILED_CHANNEL, { reason: `OpenRouter upstream failure: ${what}` } satisfies TurnFailedEvent);
90
+ ctx.abort();
91
+ if (!announce) return;
92
+ const model = ctx.model!;
93
+ const registry = ctx.modelRegistry;
94
+ const output = { hasUI: ctx.hasUI, ui: ctx.ui };
95
+ const controller = new AbortController();
96
+ const named = streamProvider;
97
+ const notice = withKeepAlive(async () => {
98
+ const provider = named ?? await openRouterProviderName(responseId, async () => {
99
+ const auth = await registry.getApiKeyAndHeaders(model);
100
+ return auth.ok ? auth.apiKey : undefined;
101
+ }, controller.signal);
102
+ if (alive() && !controller.signal.aborted) notifyOrPrint(output, toolCallCorruptionNotice(model.id, provider, failure), "error");
103
+ }).finally(() => pending.delete(controller));
104
+ pending.set(controller, notice);
105
+ };
106
+ /** (Re)start the silence clock; a call still streaming arguments keeps the short window. */
107
+ const armIdle = (ctx: ExtensionContext) => {
108
+ if (stopped) return clearIdle();
109
+ const ms = rawCalls.size > 0 ? TOOL_CALL_IDLE_MS : contentStarted ? STREAM_IDLE_MS : FIRST_CONTENT_IDLE_MS;
110
+ if (idleTimer && ms === idleMs) {
111
+ idleTimer.refresh();
112
+ return;
113
+ }
114
+ clearIdle();
115
+ idleMs = ms;
116
+ idleTimer = setTimeout(() => {
117
+ idleTimer = undefined;
118
+ try {
119
+ stopTurn(ctx, { stalledSeconds: ms / 1000 });
120
+ } catch {
121
+ // A child session disposed mid-stream (no session_shutdown): its ctx throws, and there is nothing left to stop.
122
+ }
123
+ }, ms);
124
+ idleTimer.unref?.();
125
+ };
126
+
127
+ pi.on("session_start", () => {
128
+ cancelLookups();
129
+ resetRun();
130
+ });
131
+ pi.on("session_shutdown", () => {
132
+ cancelLookups();
133
+ resetRun();
134
+ });
135
+ pi.on("agent_start", resetRun);
136
+ pi.on("message_start", (event, ctx) => {
137
+ if (event.message.role !== "assistant") return;
138
+ resetMessage();
139
+ if (alive() && isOpenRouter(ctx.model)) armIdle(ctx);
140
+ });
141
+
142
+ pi.on("provider_stream_event", (event, ctx) => {
143
+ if (!alive() || !isOpenRouter(ctx.model)) return;
144
+ const provider = (event.data as { provider?: unknown } | null)?.provider;
145
+ if (typeof provider === "string" && provider.trim()) streamProvider = provider.trim();
146
+ // Any chunk is a sign of life, content or not (reasoning details, usage, an empty delta).
147
+ if (idleTimer) armIdle(ctx);
148
+ });
149
+
150
+ pi.on("message_update", (event, ctx) => {
151
+ const update = event.assistantMessageEvent;
152
+ if (!alive() || stopped || update.type !== "toolcall_delta") return;
153
+ const state = trailingPadding(padding.get(update.contentIndex), update.delta);
154
+ padding.set(update.contentIndex, state);
155
+ if (!state.structural || state.run < RUNAWAY_PADDING_LIMIT) return;
156
+ stopped = true;
157
+ clearIdle();
158
+ const reason = `The model streamed over ${RUNAWAY_PADDING_LIMIT.toLocaleString("en-US")} characters of blank space into a tool call's arguments without finishing it, so the turn was stopped.`;
159
+ pi.events.emit(TURN_FAILED_CHANNEL, { reason } satisfies TurnFailedEvent);
160
+ ctx.abort();
161
+ if (announce) notifyOrPrint({ hasUI: ctx.hasUI, ui: ctx.ui }, `${reason} Send the request again, or switch models if it repeats.`, "error");
162
+ });
163
+
164
+ pi.on("message_update", (event, ctx) => {
165
+ if (!alive() || !isOpenRouter(ctx.model)) return;
166
+ const update = event.assistantMessageEvent;
167
+ if (!("partial" in update)) return;
168
+ responseId = update.partial.responseId || responseId;
169
+ contentStarted = true;
170
+ switch (update.type) {
171
+ case "toolcall_start":
172
+ rawCalls.set(update.contentIndex, { raw: "", markup: 0, contentMarkup: 0, next: 0 });
173
+ break;
174
+ case "toolcall_delta": {
175
+ const call = rawCalls.get(update.contentIndex) ?? { raw: "", markup: 0, contentMarkup: 0, next: 0 };
176
+ call.raw += update.delta;
177
+ const scan = scanToolCallMarkup(call.raw, call.next);
178
+ call.markup += scan.count;
179
+ call.contentMarkup += scan.contentCount;
180
+ call.next = scan.next;
181
+ rawCalls.set(update.contentIndex, call);
182
+ // A leaked call can stream markup without end and never reach toolcall_end.
183
+ if (call.markup >= TOOL_CALL_MARKUP_LIMIT || call.contentMarkup >= CONTENT_MARKUP_LIMIT) return stopTurn(ctx, "corrupt-stream");
184
+ break;
185
+ }
186
+ case "toolcall_end": {
187
+ const raw = rawCalls.get(update.contentIndex)?.raw;
188
+ rawCalls.delete(update.contentIndex);
189
+ if (raw === undefined) break;
190
+ const damage = toolCallCorruptionReason(update.toolCall.name, raw);
191
+ if (damage) flagged.set(update.toolCall.id, damage);
192
+ break;
193
+ }
194
+ }
195
+ armIdle(ctx);
196
+ });
197
+ pi.on("message_end", (event, ctx) => {
198
+ if (event.message.role === "assistant") clearIdle();
199
+ if (alive() && isOpenRouter(ctx.model) && event.message.role === "assistant") {
200
+ responseId = event.message.responseId || responseId;
201
+ // A reply cut off at the token limit ends mid-call: the damage to its
202
+ // last block is the limit's, not the provider's. A call completed
203
+ // before it was judged whole and stays blocked.
204
+ const last = event.message.stopReason === "length" ? event.message.content.at(-1) : undefined;
205
+ if (last?.type === "toolCall") flagged.delete(last.id);
206
+ }
207
+ });
208
+ pi.on("tool_call", (event, ctx) => {
209
+ if (!alive() || !isOpenRouter(ctx.model)) return;
210
+ const damage = flagged.get(event.toolCallId);
211
+ if (!damage) return;
212
+ if (damage.kind === "suspect" && !suspectReturned) {
213
+ suspectReturned = true;
214
+ flagged.delete(event.toolCallId);
215
+ return { block: true, reason: `${damage.reason} It was not run. Send the complete command again; if it really ends there, drop the trailing blank or the empty option.` };
216
+ }
217
+ const repeat = damage.kind === "suspect" ? " An earlier command in this run came through cut the same way." : "";
218
+ flagged.set(event.toolCallId, { kind: "corrupt", reason: damage.reason });
219
+ return {
220
+ block: true,
221
+ reason: `OpenRouter tool-call corruption: ${damage.reason}${repeat} Do not retry this call; switch models or exclude the upstream provider.`,
222
+ terminate: true,
223
+ };
224
+ });
225
+ pi.on("tool_execution_end", (event, ctx) => {
226
+ if (!alive() || stopped || !isOpenRouter(ctx.model) || flagged.get(event.toolCallId)?.kind !== "corrupt") return;
227
+ // Aborting inside tool_call would replace our reason with "Operation aborted".
228
+ // This also catches schema-validation failures, which bypass tool_call entirely.
229
+ stopTurn(ctx, "blocked-call");
230
+ });
231
+ pi.on("agent_end", async (_event, ctx) => {
232
+ clearIdle();
233
+ // Print/JSON dispose the session immediately after agent_end. The turn is
234
+ // already aborted; flush its bounded diagnostic before one-shot teardown.
235
+ if (!sessionOutlivesTurn(ctx.mode)) await Promise.all(pending.values());
236
+ });
237
+ }
@@ -31,6 +31,22 @@ export function planAnnouncement(input: {
31
31
  return added.length === 0 ? { kind: "none" } : { kind: "addendum", added };
32
32
  }
33
33
 
34
+ /** JSON-safe persisted baseline for the names already described to the model. */
35
+ export function deferredToolsBaseline(value: unknown): string[] | undefined {
36
+ if (!Array.isArray(value) || !value.every((name) => typeof name === "string")) return undefined;
37
+ return [...new Set(value)];
38
+ }
39
+
40
+ /** Compatibility fallback for a v1 stack written before structured baselines. */
41
+ export function deferredToolsFromReminder(text: string | undefined): string[] | undefined {
42
+ if (!text) return undefined;
43
+ const marker = "\n";
44
+ const lines = text.split(marker);
45
+ return lines.length > 1 && lines[0].startsWith("The following deferred tools are available via tool_search")
46
+ ? lines.slice(1).filter(Boolean)
47
+ : undefined;
48
+ }
49
+
34
50
  /** Bring the announced set in line with what the plan told the model. */
35
51
  export function applyAnnouncement(announced: Set<string>, plan: AnnouncementPlan): void {
36
52
  if (plan.kind === "rewrite") {
@@ -33,23 +33,32 @@ import {
33
33
  deferredAddendumText,
34
34
  deferredMissReminderText,
35
35
  DeferredRegistry,
36
+ deferralEnabled,
36
37
  deferredReminderText,
38
+ loaderUnavailableReminderText,
37
39
  type DeferRequest,
38
40
  resultText,
39
41
  searchTools,
40
- selectedNames,
42
+ selectedRequests,
43
+ type ResponsesToolLoadModel,
41
44
  stabilizeDeferredTools,
45
+ stabilizeResponsesToolLoads,
46
+ responsesToolLoadCompat,
42
47
  supportsToolReferences,
43
48
  toolNotFoundName,
44
49
  toolSearchLoads,
50
+ WITHHOLD_CHANNEL,
51
+ withheldMissReminderText,
45
52
  } from "../lib/deferred.ts";
46
- import { looksLikeAnthropicRequest } from "../lib/anthropic-payload.ts";
53
+ import { looksLikeAnthropicRequest, MID_CONVERSATION_SYSTEM_BETA, withBetas } from "../lib/anthropic-payload.ts";
54
+ import { addendumNamesOnBranch, liftAddenda, supportsToolAdditions, TOOL_ADDITION_BETAS, withToolAdditions } from "../lib/tool-additions.ts";
47
55
  import { MCP_TOOLS_CHANNEL, type McpToolsPayload } from "../lib/mcp-share.ts";
48
56
  import { CONTEXT_ORDER, REMINDER_CHANNEL } from "../lib/reminders.ts";
49
57
  import { sessionAlive } from "../lib/session-lifecycle.ts";
50
58
  import { ccToolRenderers } from "../lib/tui-render.ts";
51
59
  import { normalizeToolName } from "../permissions/matcher.ts";
52
- import { applyAnnouncement, planAnnouncement } from "./announce.ts";
60
+ import { applyAnnouncement, deferredToolsBaseline, deferredToolsFromReminder, planAnnouncement } from "./announce.ts";
61
+ import { CONTEXT_BASELINE_CHANNEL, restoredContext } from "../lib/context-stack.ts";
53
62
  import { registerLocalCommand } from "../lib/local-command.ts";
54
63
 
55
64
  /** Coalesces the burst of per-tool defers one server emits into one listing update. */
@@ -58,6 +67,12 @@ const ANNOUNCE_DEBOUNCE_MS = 100;
58
67
  export default function toolSearchExtension(pi: ExtensionAPI) {
59
68
  // Owned here (see lib/deferred.ts): the only instance, fed over DEFER_CHANNEL.
60
69
  const deferredRegistry = new DeferredRegistry();
70
+ // Names WITHHOLD_CHANNEL removed from the registry, kept only so a direct
71
+ // call to one still gets an explanation (tool_execution_end below) instead
72
+ // of a bare "not found": the registry itself can't answer that, since
73
+ // removal is what makes the name stop being deferred (searchable/loadable).
74
+ // A re-defer (a model change bringing it back) clears the name from here.
75
+ const withheldNames = new Set<string>();
61
76
  let sessionStarted = false;
62
77
  // The deferred-tools listing is a first-prepend reminder on message 1, part of
63
78
  // the cached prefix of every later request. Before the first request nothing
@@ -77,21 +92,36 @@ export default function toolSearchExtension(pi: ExtensionAPI) {
77
92
  const alive = sessionAlive(pi);
78
93
  /** Names the model has been told about: the frozen listing plus every addendum. */
79
94
  const announced = new Set<string>();
95
+ /** The session's model as last seen, for an announcement made outside any hook. */
96
+ let latestModel: { provider?: string; api?: string; compat?: unknown } | undefined;
80
97
 
81
- const searchableTools = () =>
82
- pi
83
- .getAllTools()
98
+ /** Whether tool_search is registered; a `--tools` list without it turns deferral off (lib/deferred.ts). */
99
+ const deferring = () => deferralEnabled(pi.getAllTools());
100
+ const searchableTools = () => {
101
+ const all = pi.getAllTools();
102
+ if (!deferralEnabled(all)) return [];
103
+ return all
84
104
  .filter((tool) => deferredRegistry.has(tool.name))
85
105
  .map((tool) => ({
86
106
  name: tool.name,
87
107
  description: tool.description ?? "",
88
108
  keywords: deferredRegistry.keywordsFor(tool.name),
89
109
  }));
110
+ };
90
111
 
112
+ const publishBaseline = (names: ReadonlySet<string>) => {
113
+ pi.events.emit(CONTEXT_BASELINE_CHANNEL, { key: "tool-search", value: [...names] });
114
+ };
91
115
  const announce = () => {
92
116
  if (!alive()) return;
93
117
  const available = searchableTools();
94
118
  const plan = planAnnouncement({ requestSent, announced, available: available.map((t) => t.name) });
119
+ // Store the post-announcement state before the next context pass can place it.
120
+ if (plan.kind !== "none") {
121
+ const next = new Set(announced);
122
+ applyAnnouncement(next, plan);
123
+ publishBaseline(next);
124
+ }
95
125
  if (plan.kind === "rewrite") {
96
126
  pi.events.emit(REMINDER_CHANNEL, {
97
127
  scope: "every-turn",
@@ -104,6 +134,12 @@ export default function toolSearchExtension(pi: ExtensionAPI) {
104
134
  // Unkeyed on purpose: a keyed next-turn reminder replaces its
105
135
  // predecessor, and an addendum replaced before delivery would lose names.
106
136
  pi.events.emit(REMINDER_CHANNEL, { text: deferredAddendumText(plan.added) });
137
+ // Where the request carries it as Claude Code's tool_addition, the model
138
+ // calls the tool directly, so pi's dispatcher must already accept it.
139
+ if (supportsToolAdditions(latestModel)) {
140
+ for (const name of plan.added) loadedBefore.add(name);
141
+ pi.setActiveTools([...new Set([...pi.getActiveTools(), ...plan.added])]);
142
+ }
107
143
  }
108
144
  applyAnnouncement(announced, plan);
109
145
  };
@@ -137,8 +173,15 @@ export default function toolSearchExtension(pi: ExtensionAPI) {
137
173
  }
138
174
  });
139
175
 
140
- pi.on("context", () => {
176
+ /** The addenda the last `context` pass lifted off tool results, by call id (lib/tool-additions.ts). */
177
+ let addendaByCall = new Map<string, string[]>();
178
+ pi.on("context", (event, ctx) => {
141
179
  requestSent = true;
180
+ addendaByCall = new Map();
181
+ if (!supportsToolAdditions(ctx?.model) || !Array.isArray(event?.messages)) return undefined;
182
+ const lifted = liftAddenda(event.messages);
183
+ addendaByCall = lifted.byCall;
184
+ return lifted.messages === event.messages ? undefined : { messages: lifted.messages };
142
185
  });
143
186
 
144
187
  // Every tool_search load of this session (call id → names), seeded from the
@@ -162,18 +205,44 @@ export default function toolSearchExtension(pi: ExtensionAPI) {
162
205
  // the eager list, re-caching everything from the tools block down — is
163
206
  // demoted back to deferred and loaded through `tool_reference` blocks in the
164
207
  // tool_search result that activated it (lib/deferred.ts stabilizeDeferredTools
165
- // has the rules; docs/decisions/caching.md the measurements).
208
+ // has the rules; working-docs/decisions/caching.md the measurements).
209
+ // On OpenAI Responses models with native tool loading (GPT-5.4 and newer on
210
+ // openai and openai-codex) a loaded tool leaves `tools` for pi-ai's own
211
+ // load item at the tool_search result instead (stabilizeResponsesToolLoads).
166
212
  pi.on("before_provider_request", (event, ctx) => {
167
- if (!supportsToolReferences(ctx.model as { provider?: string; id?: string } | undefined)) return undefined;
168
- if (!looksLikeAnthropicRequest(event.payload)) return undefined;
169
- return stabilizeDeferredTools(event.payload as Record<string, unknown>, pi.getAllTools(), (name) => deferredRegistry.has(name), loads);
213
+ latestModel = ctx.model;
214
+ const allTools = pi.getAllTools();
215
+ const enabled = deferralEnabled(allTools);
216
+ const toolLoadCompat = responsesToolLoadCompat(ctx.model as ResponsesToolLoadModel | undefined);
217
+ if (toolLoadCompat) {
218
+ if (!enabled) return undefined;
219
+ return stabilizeResponsesToolLoads(event.payload as Record<string, unknown>, (name) => deferredRegistry.has(name), loads, toolLoadCompat);
220
+ }
221
+ const original = event.payload as Record<string, unknown>;
222
+ const additions = supportsToolAdditions(ctx.model) && looksLikeAnthropicRequest(original);
223
+ // The context step lifted this request's addenda off their tool results;
224
+ // whatever happens below, they go out (lib/tool-additions.ts).
225
+ const announceAsText = () => {
226
+ const text = additions ? withToolAdditions(original, addendaByCall, false) : undefined;
227
+ return text ? withBetas(text.payload, [MID_CONVERSATION_SYSTEM_BETA]) : undefined;
228
+ };
229
+ if (!enabled || !supportsToolReferences(ctx.model as { provider?: string; id?: string } | undefined) || !looksLikeAnthropicRequest(original)) return announceAsText();
230
+ const isDeferred = (name: string) => deferredRegistry.has(name);
231
+ // A tool that arrived mid-session goes out as Claude Code's tool_addition
232
+ // where the model takes one, declared deferred.
233
+ const lifted = additions ? withToolAdditions(original, addendaByCall) : undefined;
234
+ const stable = stabilizeDeferredTools(lifted?.payload ?? original, allTools, isDeferred, loads, new Set(lifted?.names ?? []));
235
+ // Renamed (OAuth) tools cannot be referenced: the active tools stay as pi
236
+ // rendered them and the addition goes as text.
237
+ if (!stable) return announceAsText();
238
+ return lifted ? withBetas(stable, TOOL_ADDITION_BETAS) : stable;
170
239
  });
171
240
 
172
241
 
173
242
  /** Deactivate every deferred-registry tool the transcript has not loaded, and announce the loadable set. */
174
243
  const deferAll = () => {
175
244
  const deferred = new Set(deferredRegistry.names);
176
- if (deferred.size === 0) return;
245
+ if (deferred.size === 0 || !deferring()) return;
177
246
  const active = pi.getActiveTools();
178
247
  const next = active.filter((name) => !deferred.has(name) || loadedBefore.has(name));
179
248
  if (next.length !== active.length) pi.setActiveTools(next);
@@ -183,11 +252,12 @@ export default function toolSearchExtension(pi: ExtensionAPI) {
183
252
  pi.events.on(DEFER_CHANNEL, (data) => {
184
253
  const request = data as DeferRequest;
185
254
  deferredRegistry.add(request);
255
+ if (request?.name) withheldNames.delete(request.name);
186
256
  // A defer arriving after the session_start pass (MCP servers connect
187
257
  // asynchronously and register their tools then) would otherwise leave the
188
258
  // tool eager AND unlisted in the reminder. Deactivate it (unless the
189
259
  // transcript loaded it) and refresh the keyed reminder.
190
- if (sessionStarted && request?.name) {
260
+ if (sessionStarted && request?.name && deferring()) {
191
261
  const active = pi.getActiveTools();
192
262
  if (active.includes(request.name) && !loadedBefore.has(request.name)) {
193
263
  pi.setActiveTools(active.filter((name) => name !== request.name));
@@ -198,19 +268,51 @@ export default function toolSearchExtension(pi: ExtensionAPI) {
198
268
  }
199
269
  });
200
270
 
271
+ pi.events.on(WITHHOLD_CHANNEL, (data) => {
272
+ const name = (data as { name?: string } | undefined)?.name;
273
+ if (!name || !deferredRegistry.remove(name)) return;
274
+ withheldNames.add(name);
275
+ if (!alive()) return;
276
+ const active = pi.getActiveTools();
277
+ if (active.includes(name)) pi.setActiveTools(active.filter((n) => n !== name));
278
+ // Before the first request the listing is still free to change: rewrite it
279
+ // now, synchronously, so a request going out right after session_start
280
+ // never names the withheld tool. After it the frozen listing stands.
281
+ if (sessionStarted && !requestSent) announce();
282
+ });
283
+
201
284
  pi.on("session_start", (_event, ctx) => {
202
285
  sessionStarted = true;
203
286
  // A pending debounce belongs to the previous session's listing (RPC's
204
287
  // new_session emits session_start twice, findings §3); deferAll below
205
288
  // announces the fresh one.
206
289
  clearAnnounce();
207
- loads = toolSearchLoads(ctx?.sessionManager?.getBranch?.() ?? []);
290
+ latestModel = ctx?.model;
291
+ const branch = ctx?.sessionManager?.getBranch?.() ?? [];
292
+ loads = toolSearchLoads(branch);
208
293
  loadedBefore = new Set([...loads.values()].flat());
209
- // A new session (/clear, or a resume in a new process) has no cached prefix
210
- // yet: its first request gets a freshly written listing.
211
- requestSent = false;
294
+ // Tools added mid-session by tool_addition stay callable after a resume.
295
+ if (supportsToolAdditions(latestModel)) for (const name of addendumNamesOnBranch(branch)) loadedBefore.add(name);
296
+ const restored = restoredContext(pi.events);
297
+ // A restored stack is already message 1. Seed its announced names so a live
298
+ // capability change becomes one addendum, not a rewrite or duplicate notice.
299
+ requestSent = restored !== undefined;
212
300
  announced.clear();
301
+ const baseline = deferredToolsBaseline(restored?.baselines["tool-search"]);
302
+ const legacy = deferredToolsFromReminder(restored?.stack.find((entry) => (entry as { key?: string }).key === "deferred-tools")?.text);
303
+ for (const name of baseline ?? legacy ?? []) announced.add(name);
304
+ // Native tool_addition entries can outlive an older baseline snapshot; they
305
+ // already told the model about these names, so merge them before planning.
306
+ for (const name of addendumNamesOnBranch(branch)) announced.add(name);
213
307
  deferAll();
308
+ // A restored message 1 keeps its listing (its bytes are locked), so when
309
+ // this run's tool list leaves tool_search out, say once that it is gone.
310
+ if (restored && announced.size > 0 && !deferring()) {
311
+ pi.events.emit(REMINDER_CHANNEL, { scope: "next-turn", key: "deferred-unavailable", text: loaderUnavailableReminderText() });
312
+ }
313
+ // Even an absent listing is meaningful on resume: preserve that baseline so
314
+ // the first later deferred tool is a tail notice, not a message-1 rewrite.
315
+ if (!restored) publishBaseline(announced);
214
316
  });
215
317
 
216
318
  pi.on("session_shutdown", clearAnnounce);
@@ -228,15 +330,28 @@ export default function toolSearchExtension(pi: ExtensionAPI) {
228
330
  // Map a Claude Code spelling (a direct `NotebookEdit` call) to our name so
229
331
  // the miss is recognised and the steer names the loadable tool (M1).
230
332
  const name = raw ? normalizeToolName(raw) : undefined;
231
- if (!name || !deferredRegistry.has(name)) return;
232
- pi.events.emit(REMINDER_CHANNEL, {
233
- scope: "next-turn",
234
- key: `deferred-miss-${name}`,
235
- text: deferredMissReminderText(name),
236
- });
333
+ if (!name) return;
334
+ // Without tool_search there is nothing to steer a deferred-tool miss to.
335
+ if (deferredRegistry.has(name) && deferring()) {
336
+ pi.events.emit(REMINDER_CHANNEL, {
337
+ scope: "next-turn",
338
+ key: `deferred-miss-${name}`,
339
+ text: deferredMissReminderText(name),
340
+ });
341
+ } else if (withheldNames.has(name)) {
342
+ // A withheld tool is gone from the registry too (WITHHOLD_CHANNEL), so
343
+ // the ordinary steer above would send the model to tool_search for a
344
+ // name that will not be found there either.
345
+ pi.events.emit(REMINDER_CHANNEL, {
346
+ scope: "next-turn",
347
+ key: `deferred-miss-${name}`,
348
+ text: withheldMissReminderText(name),
349
+ });
350
+ }
237
351
  });
238
352
 
239
353
  pi.registerTool({
354
+ // A literal, like every registration: agent-view-tool-labels.test.ts reads them from source.
240
355
  name: "tool_search",
241
356
  label: "Tool Search",
242
357
  ...ccToolRenderers("Tool Search"),
@@ -257,14 +372,34 @@ export default function toolSearchExtension(pi: ExtensionAPI) {
257
372
  // silently dropped before — the model was told the rest "Loaded" and only
258
373
  // discovered the miss later as an opaque InputValidationError. Always
259
374
  // surface the unmatched names.
260
- const requested = selectedNames(params.query);
261
- const notFound = requested
262
- ? requested.filter((n) => !matches.some((m) => m.name.toLowerCase() === n))
375
+ const requested = selectedRequests(params.query);
376
+ // A requested tool that is already callable (an eager tool, or one loaded
377
+ // earlier) is a harmless no-op, as in Claude Code: the model proceeds
378
+ // instead of retrying a name it was told does not exist.
379
+ const active = pi.getActiveTools();
380
+ const alreadyActive = (name: string) => active.find((tool) => tool.toLowerCase() === name);
381
+ const unmatched = requested
382
+ ? requested.filter((request) => !matches.some((m) => m.name.toLowerCase() === request.name))
263
383
  : [];
384
+ const eager = [...new Set(unmatched.flatMap((request) => alreadyActive(request.name) ?? []))];
385
+ const notFound = unmatched.filter((request) => !alreadyActive(request.name));
386
+ // A withheld name can still sit in the frozen listing (withdrawn by a model
387
+ // change after the first request), so it gets "do not retry", not a
388
+ // spelling hint. Both notes quote the model's own spelling.
389
+ const withdrawn = [...new Set(notFound.filter((n) => withheldNames.has(n.name)).map((n) => n.asWritten))];
390
+ const missing = [...new Set(notFound.filter((n) => !withheldNames.has(n.name)).map((n) => n.asWritten))];
264
391
  const notFoundNote =
265
- notFound.length > 0
266
- ? ` Not found (not deferred tool names — check spelling, or search by keyword instead of \`select:\`): ${notFound.join(", ")}.`
267
- : "";
392
+ (missing.length > 0
393
+ ? ` Not found (not deferred tool names — check spelling, or search by keyword instead of \`select:\`): ${missing.join(", ")}.`
394
+ : "") +
395
+ (withdrawn.length > 0 ? ` Withdrawn for the current model (do not retry): ${withdrawn.join(", ")}.` : "");
396
+
397
+ if (matches.length === 0 && eager.length > 0) {
398
+ return {
399
+ content: [{ type: "text", text: `Already loaded: ${eager.join(", ")}.${notFoundNote}` }],
400
+ details: { matches: eager, added: [] as string[], notFound, refused: [] as string[] },
401
+ };
402
+ }
268
403
 
269
404
  if (matches.length === 0) {
270
405
  const names = available.map((t) => t.name).join(", ") || "(none)";
@@ -272,30 +407,39 @@ export default function toolSearchExtension(pi: ExtensionAPI) {
272
407
  content: [
273
408
  { type: "text", text: `No tools matched "${params.query}".${notFoundNote} Tools that can be loaded: ${names}` },
274
409
  ],
275
- details: { matches: [] as string[], added: [] as string[], notFound },
410
+ details: { matches: [] as string[], added: [] as string[], notFound, refused: [] as string[] },
276
411
  isError: true,
277
412
  };
278
413
  }
279
414
 
280
- const active = pi.getActiveTools();
281
- const added = matches.map((m) => m.name).filter((name) => !active.includes(name));
282
- if (added.length > 0) {
283
- pi.setActiveTools([...new Set([...active, ...added])]);
284
- loads.set(toolCallId, added);
285
- }
415
+ const matchNames = matches.map((m) => m.name);
416
+ const toActivate = matchNames.filter((name) => !active.includes(name));
417
+ if (toActivate.length > 0) pi.setActiveTools([...new Set([...active, ...toActivate])]);
418
+ // pi can decline to activate a tool: since 1.0.4 a `--tools` list that
419
+ // names no `mcp__*` entry keeps the MCP tools registered but not
420
+ // activatable. Report what pi actually activated, never a plausible load.
421
+ const now = new Set(pi.getActiveTools());
422
+ const added = toActivate.filter((name) => now.has(name));
423
+ const refused = toActivate.filter((name) => !now.has(name));
424
+ if (added.length > 0) loads.set(toolCallId, added);
286
425
 
287
- const loaded = matches.map((m) => m.name);
426
+ const loaded = [...new Set([...matchNames, ...eager])].filter((name) => now.has(name));
427
+ const already = loaded.filter((name) => !added.includes(name));
428
+ const refusedNote =
429
+ refused.length > 0 ? ` Could not load ${refused.join(", ")}: this session's tool list (pi's --tools) leaves them out, so they cannot be called.` : "";
288
430
  return {
289
431
  content: [
290
432
  {
291
433
  type: "text",
292
434
  text:
293
- (added.length > 0
294
- ? `Loaded ${added.join(", ")}. These tools are now callable.`
295
- : `Already loaded: ${loaded.join(", ")}.`) + notFoundNote,
435
+ ([
436
+ added.length > 0 ? `Loaded ${added.join(", ")}. These tools are now callable.` : "",
437
+ already.length > 0 ? `Already loaded: ${already.join(", ")}.` : "",
438
+ ].filter(Boolean).join(" ") + refusedNote + notFoundNote).trim(),
296
439
  },
297
440
  ],
298
- details: { matches: loaded, added, notFound },
441
+ details: { matches: loaded, added, notFound, refused },
442
+ isError: loaded.length === 0,
299
443
  };
300
444
  },
301
445
  });
@@ -303,6 +447,10 @@ export default function toolSearchExtension(pi: ExtensionAPI) {
303
447
  registerLocalCommand(pi, "tools-deferred", {
304
448
  description: "Show which tools are deferred (loadable via tool_search)",
305
449
  handler: async (args, ctx) => {
450
+ if (!deferring()) {
451
+ ctx.ui.notify("tool_search is not registered (a --tools list leaves it out), so no tool is deferred.", "info");
452
+ return;
453
+ }
306
454
  const available = searchableTools();
307
455
  const activeSet = new Set(pi.getActiveTools());
308
456
  const lines = available.map((t) => `${activeSet.has(t.name) ? "loaded " : "deferred"} ${t.name}`);