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
@@ -2,23 +2,29 @@
2
2
  * skill extension — Claude Code's Skill tool.
3
3
  *
4
4
  * pi's own mechanism lists skills in the system prompt and expects the model to
5
- * `read` the SKILL.md path. Claude Code instead exposes a `skill` tool that
6
- * returns the skill's instructions as a tool result. This adds that tool, so a
7
- * skill can be invoked by name — including plugin skills as `<plugin>:<skill>`.
5
+ * `read` the SKILL.md path. Claude Code instead exposes a `skill` tool, which
6
+ * answers "Launching skill: <name>" with the skill's instructions beside it.
7
+ * This adds that tool, so a skill can be invoked by name — including plugin
8
+ * skills as `<plugin>:<skill>` — and expands a skill the user types into its
9
+ * instructions before the model sees the message, as Claude Code does.
8
10
  *
9
11
  * Skills discovered by pi (which includes `~/.claude/skills` and
10
12
  * `.claude/skills` thanks to the claude-compat extension) are read from
11
13
  * `before_agent_start`'s systemPromptOptions rather than rediscovered here.
12
14
  */
13
15
 
14
- import { readFileSync } from "node:fs";
16
+ import { readFileSync, statSync } from "node:fs";
15
17
  import os from "node:os";
18
+ import { dirname, join, resolve, sep } from "node:path";
19
+ import type { Api, Model } from "@earendil-works/pi-ai";
16
20
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
17
- import { getAgentDir, stripFrontmatter } from "@earendil-works/pi-coding-agent";
21
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
18
22
  import { Type } from "typebox";
23
+ import { canShowCustomUi, notifyOrPrint, notifyRpcReadOnly } from "../lib/headless-output.ts";
19
24
  import { pluginRoot } from "../lib/plugin-root.ts";
20
25
  import { defaultDiscoverRoots, discoverPlugins } from "../lib/plugins.ts";
21
- import { awaitOneShotTurn } from "../lib/notifications.ts";
26
+ import { awaitOneShotTurn, sessionOutlivesTurn } from "../lib/notifications.ts";
27
+ import { PROMPT_OPTIONS_CHANNEL, type PromptOptionsAnnouncement } from "../lib/prompt-options.ts";
22
28
  import { CONTEXT_ORDER, REMINDER_CHANNEL } from "../lib/reminders.ts";
23
29
  import {
24
30
  nextSkillState,
@@ -30,22 +36,34 @@ import {
30
36
  skillStateFor,
31
37
  } from "../lib/skill-overrides.ts";
32
38
  import { BUNDLED_SKILLS_DIR, estimateSkillTokens, promptTemplateNames, scanSkills, scopeForPath } from "../lib/skill-scan.ts";
33
- import { recordUsage } from "../lib/usage-tracker.ts";
39
+ import { sessionModelTier } from "../lib/session-model-tier.ts";
40
+ import { readUsage, recordUsage, usageKey } from "../lib/usage-tracker.ts";
34
41
  import { boundedDockHeight, ccToolRenderers, safeThemeBold, safeThemePaint, truncateLine } from "../lib/tui-render.ts";
35
42
  import {
36
43
  bareSkillMatches,
37
- buildSkillBlock,
44
+ commandDisplayLine,
45
+ launchingSkill,
46
+ missingArgumentsNote,
38
47
  parseSkillCommand,
39
48
  redactOffSkillMessages,
40
49
  resolveSkill,
50
+ SKILL_INVOCATION_TYPE,
51
+ type SkillInvocationDetails,
41
52
  skillCommandCandidates,
53
+ skillPromptText,
54
+ typedSkillContent,
55
+ withoutDuplicateSkillCommands,
42
56
  } from "./invoke.ts";
57
+ import { codeReviewBody } from "./code-review.ts";
43
58
  import { decodeSkillsKey } from "./panel/keys.ts";
44
- import { skillListingText } from "./listing.ts";
59
+ import { frontmatterFlag, readableSkillListing, skillListingBudget, skillListingText, usageScore, withoutPiSkillsBlock } from "./listing.ts";
45
60
  import { renderSkillsPanel, type SkillsPaint } from "./panel/render.ts";
46
- import { applySkillsKey, initialSkillsState, type SkillsRow, visibleRows } from "./panel/state.ts";
61
+ import { applySkillsKey, followRow, initialSkillsState, type SkillsRow, visibleRows } from "./panel/state.ts";
62
+ import { announceArgumentHint, type CommandHint, frontmatterCommandHint } from "../lib/argument-hints.ts";
63
+ import { parseSlashCommand, SKILL_BODY_CHANNEL, type SkillBodyQuery, SLASH_EXPAND_CHANNEL, type SlashExpandQuery } from "../lib/skill-body.ts";
47
64
  import { parseFrontmatterLoosely } from "../lib/frontmatter.ts";
48
65
  import { registerLocalCommand } from "../lib/local-command.ts";
66
+ import { panelUi } from "../lib/panel-focus.ts";
49
67
 
50
68
  interface IndexedSkill {
51
69
  name: string;
@@ -55,22 +73,83 @@ interface IndexedSkill {
55
73
  scope: SkillScope;
56
74
  state: SkillState;
57
75
  pluginName?: string;
76
+ /** The `when_to_use` frontmatter, which Claude Code lists after the description. */
77
+ whenToUse?: string;
78
+ /**
79
+ * A plugin command (`<plugin>:<command>`): listed for the model and
80
+ * loadable through the tool, as in Claude Code; typed, it stays the
81
+ * plugins extension's command.
82
+ */
83
+ kind?: "command";
84
+ /**
85
+ * Frontmatter `disable-model-invocation: true`: only the user starts it (a
86
+ * typed `/<name>`); it is left out of the model's listing and the `skill`
87
+ * tool refuses it, as Claude Code's Skill tool does.
88
+ */
89
+ disableModelInvocation?: boolean;
58
90
  }
59
91
 
92
+
60
93
  /** Bounded dock like the /plugins panel — keeps the transcript visible above. */
61
94
  const SKILLS_PANEL_MAX_HEIGHT = 24;
62
95
 
96
+ const BUNDLED_ROOT = resolve(BUNDLED_SKILLS_DIR);
97
+ /** A skill in One Code's own catalog, like one built into Claude Code: listed last, kept whole over budget, no folder named. */
98
+ const isBundled = (path: string): boolean => resolve(path).startsWith(BUNDLED_ROOT + sep);
99
+ const CODE_REVIEW_PATH = join(BUNDLED_ROOT, "code-review", "SKILL.md");
100
+ const CODE_REVIEW_CELLS = join(BUNDLED_ROOT, "code-review", "cells");
101
+
63
102
  export default function skillExtension(pi: ExtensionAPI) {
103
+ const requestTier = sessionModelTier(pi);
104
+ // pi draws a user message as markdown, so a typed skill, `/init` and
105
+ // `/doctor` showed their whole expanded prompt; show the command line.
106
+ pi.registerMarkdownTransformer((markdown, context) =>
107
+ context.messageType === "user" ? (commandDisplayLine(markdown) ?? markdown) : markdown,
108
+ );
64
109
  /** pi resolves skills per turn; cache the latest list for the tool to use. */
65
110
  let piSkills: IndexedSkill[] = [];
66
111
  /** Session cwd, so project-level enabledPlugins settings apply to plugin skills. */
67
112
  let sessionCwd: string | undefined;
113
+ /** The session's model: its context window sizes the listing, its tier picks code-review's body. */
114
+ let sessionModel: Model<Api> | undefined;
68
115
 
69
- pi.on("before_agent_start", (event, ctx) => {
70
- sessionCwd = ctx.cwd;
71
- const skills = event.systemPromptOptions.skills ?? [];
116
+ /**
117
+ * True once a `prompt()` has run (the only path that emits
118
+ * before_agent_start). Until then an idle skill delivery goes out as a user
119
+ * message: pi's `sendMessage(…, {triggerTurn})` skips that preamble, so a
120
+ * session opening with `/loop …` or `/simplify` ran its first request
121
+ * without our system prompt or reminder stack (lib/notifications.ts,
122
+ * "First turn of a session"; upstream_prs.md #17). Later turns get the
123
+ * prompt from system-prompt's context_with_system handler (idle-turn.ts).
124
+ */
125
+ let prompted = false;
126
+ /**
127
+ * A one-shot run (`-p`, `--mode json`) ends when its prompt returns, so the
128
+ * bare `/<name>` skill forms are taken by the `input` hook there instead of
129
+ * registered as commands: a command can only start a second prompt, which
130
+ * the run does not wait for.
131
+ */
132
+ let oneShot = false;
133
+ /** The bare skill names a one-shot run takes in the `input` hook. */
134
+ const oneShotSkillNames = new Set<string>();
135
+
136
+ // A factory re-run builds each session's instance, so this is belt and braces
137
+ // (lib/notifications.ts resets its twin the same way).
138
+ pi.on("session_start", (_event, ctx) => {
139
+ prompted = false;
140
+ oneShot = !sessionOutlivesTurn(ctx.mode);
141
+ announcedSkills = false;
142
+ sessionUsage = undefined;
143
+ sessionModel = ctx.model;
144
+ });
145
+ pi.on("model_select", (event) => {
146
+ sessionModel = event.model;
147
+ });
148
+ /** Index the skills pi resolved for this turn and list them for the model. */
149
+ const adoptPiSkills = (skills: unknown[], cwd: string) => {
150
+ sessionCwd = cwd;
72
151
  piSkills = skills.map((skill) => {
73
- const record = skill as unknown as { name: string; description?: string; path?: string; filePath?: string };
152
+ const record = skill as unknown as { name: string; description?: string; path?: string; filePath?: string; disableModelInvocation?: boolean };
74
153
  const path = record.path ?? record.filePath ?? "";
75
154
  return {
76
155
  name: record.name,
@@ -79,10 +158,21 @@ export default function skillExtension(pi: ExtensionAPI) {
79
158
  source: "project" as const,
80
159
  scope: scopeForPath(path, os.homedir(), getAgentDir()),
81
160
  state: "on" as const, // resolved per index() call below
161
+ // pi reads only a YAML `true`; Claude Code also takes the string "true".
162
+ disableModelInvocation: record.disableModelInvocation === true || (path ? readModelInvocationDisabled(path) : false),
82
163
  };
83
164
  });
84
165
  // Claude Code lists skills as a <system-reminder> on the first user message
85
- // (its block 3), framed for the Skill tool — not in the system prompt.
166
+ // (its block 3), framed for the Skill tool — not in the system prompt. A
167
+ // session without that tool (an agent limited to read and bash, a main
168
+ // session started with fewer tools) gets the same block framed for reading
169
+ // the file, since pi's own section is dropped from every prompt below.
170
+ const tools = pi.getActiveTools();
171
+ if (!tools.includes("skill")) {
172
+ const readable = readableSkillListing(index(), tools.includes("read") ? "the read tool" : "the shell");
173
+ if (readable) pi.events.emit(REMINDER_CHANNEL, { text: readable, scope: "every-turn", key: "skills", placement: "first-prepend", order: CONTEXT_ORDER.skills });
174
+ return;
175
+ }
86
176
  const listing = describe();
87
177
  if (listing !== "(no skills available)") {
88
178
  pi.events.emit(REMINDER_CHANNEL, {
@@ -93,6 +183,28 @@ export default function skillExtension(pi: ExtensionAPI) {
93
183
  order: CONTEXT_ORDER.skills,
94
184
  });
95
185
  }
186
+ };
187
+ pi.on("before_agent_start", (event, ctx) => {
188
+ prompted = true;
189
+ sessionModel = ctx.model ?? sessionModel;
190
+ adoptPiSkills(event.systemPromptOptions.skills ?? [], ctx.cwd);
191
+ // The listing above is the one skills instruction: drop pi's own section
192
+ // from the prompt (a child agent's own prompt carries it; the main
193
+ // session's prompt is rebuilt without it).
194
+ if (typeof event.systemPrompt !== "string") return;
195
+ const stripped = withoutPiSkillsBlock(event.systemPrompt);
196
+ return stripped === event.systemPrompt ? undefined : { systemPrompt: stripped };
197
+ });
198
+ // The first turn a background completion opens (lib/prompt-options.ts)
199
+ // carries the same listing a typed first prompt would.
200
+ // The first announcement wins, as in system-prompt: both describe one turn.
201
+ let announcedSkills = false;
202
+ pi.events.on(PROMPT_OPTIONS_CHANNEL, (data) => {
203
+ const announced = data as PromptOptionsAnnouncement | undefined;
204
+ const skills = (announced?.options as { skills?: unknown[] } | undefined)?.skills;
205
+ if (prompted || announcedSkills || !announced?.cwd || !Array.isArray(skills)) return;
206
+ announcedSkills = true;
207
+ adoptPiSkills(skills, announced.cwd);
96
208
  });
97
209
 
98
210
  /**
@@ -102,24 +214,57 @@ export default function skillExtension(pi: ExtensionAPI) {
102
214
  * stable for the session anyway (the listing must stay byte-stable for the
103
215
  * cache prefix).
104
216
  */
105
- const descriptionCache = new Map<string, string | undefined>();
217
+ const frontmatterCache = new Map<string, Record<string, unknown> | undefined>();
218
+ /** A SKILL.md's frontmatter, read once per path: the description and the argument hint come from it. */
219
+ const readFrontmatter = (path: string): Record<string, unknown> | undefined => {
220
+ if (frontmatterCache.has(path)) return frontmatterCache.get(path);
221
+ let frontmatter: Record<string, unknown> | undefined;
222
+ try {
223
+ frontmatter = parseFrontmatterLoosely(readFileSync(path, "utf-8")).frontmatter;
224
+ } catch {
225
+ frontmatter = undefined;
226
+ }
227
+ frontmatterCache.set(path, frontmatter);
228
+ return frontmatter;
229
+ };
106
230
  const readDescription = (path: string): string | undefined => {
107
- if (descriptionCache.has(path)) return descriptionCache.get(path);
108
- const description = readDescriptionUncached(path);
109
- descriptionCache.set(path, description);
110
- return description;
231
+ const description = readFrontmatter(path)?.description;
232
+ return typeof description === "string" ? description : undefined;
233
+ };
234
+ const readWhenToUse = (path: string): string | undefined => {
235
+ const whenToUse = readFrontmatter(path)?.when_to_use;
236
+ return typeof whenToUse === "string" && whenToUse.trim() ? whenToUse : undefined;
111
237
  };
112
- const readDescriptionUncached = (path: string): string | undefined => {
238
+
239
+ /**
240
+ * A SKILL.md's `disable-model-invocation` flag, read apart from the
241
+ * description cache and again whenever the file changes: a flag added
242
+ * mid-session must stop the model loading the skill, while the listing's
243
+ * descriptions stay byte-stable.
244
+ */
245
+ const invocationFlagCache = new Map<string, { mtimeMs: number; disabled: boolean }>();
246
+ const readModelInvocationDisabled = (path: string): boolean => {
247
+ let mtimeMs = -1;
113
248
  try {
114
- const { frontmatter } = parseFrontmatterLoosely(readFileSync(path, "utf-8")) as {
115
- frontmatter?: { description?: unknown };
116
- };
117
- return typeof frontmatter?.description === "string" ? frontmatter.description : undefined;
249
+ mtimeMs = statSync(path).mtimeMs;
118
250
  } catch {
119
- return undefined;
251
+ // Unreadable: parsed (and failed) below, cached under -1.
120
252
  }
253
+ const cached = invocationFlagCache.get(path);
254
+ if (cached && cached.mtimeMs === mtimeMs) return cached.disabled;
255
+ let disabled = false;
256
+ try {
257
+ disabled = frontmatterFlag(parseFrontmatterLoosely(readFileSync(path, "utf-8")).frontmatter?.["disable-model-invocation"]);
258
+ } catch {
259
+ // No file, no flag.
260
+ }
261
+ invocationFlagCache.set(path, { mtimeMs, disabled });
262
+ return disabled;
121
263
  };
122
264
 
265
+ /** A SKILL.md's `argument-hint`, the prompt's placeholder after its command (lib/argument-hints.ts). */
266
+ const readArgumentHint = (path: string): CommandHint | undefined => frontmatterCommandHint(readFrontmatter(path));
267
+
123
268
  // Per-skill availability comes from the skill-overrides store (the /skills
124
269
  // panel cycles it, the /plugins panel manages plugin skills). The listing
125
270
  // and the tool both re-read it, so a change applies live.
@@ -130,7 +275,11 @@ export default function skillExtension(pi: ExtensionAPI) {
130
275
  // the /skills command/panel falls back to a disk scan (reading each
131
276
  // SKILL.md's frontmatter for its description) — that's the gap that made a
132
277
  // fresh session's /skills show only plugin skills.
133
- const index = (cwd = sessionCwd): IndexedSkill[] => {
278
+ //
279
+ // The order is Claude Code's: project and user skills, then plugin commands
280
+ // and plugin skills, then the bundled catalog.
281
+ /** Every listable entry; `withCommands: false` leaves out plugin commands (and their frontmatter reads). */
282
+ const index = (cwd = sessionCwd, withCommands = true): IndexedSkill[] => {
134
283
  const agentDir = getAgentDir();
135
284
  const home = os.homedir();
136
285
  const states = readSkillStates(pluginRoot(agentDir));
@@ -145,34 +294,78 @@ export default function skillExtension(pi: ExtensionAPI) {
145
294
  source: "project" as const,
146
295
  scope: skill.scope,
147
296
  state: "on" as const,
297
+ disableModelInvocation: readModelInvocationDisabled(skill.path),
148
298
  }));
149
299
  const project = base.map((skill) => ({
150
300
  ...skill,
301
+ whenToUse: readWhenToUse(skill.path),
151
302
  state: skillStateFor(states, skillOverrideKey(skill.scope, skill.name)),
152
303
  }));
153
304
  // Plugin skills from discoverPlugins are already filtered to enabled by
154
305
  // the same store; anything it returns is fully available (plugin skills
155
306
  // aren't governed by skillOverrides — managed via /plugins).
156
- const plugin = discoverPlugins(defaultDiscoverRoots(agentDir, cwd, home)).skills.map((skill) => ({
307
+ const discovered = discoverPlugins(defaultDiscoverRoots(agentDir, cwd, home));
308
+ const plugin = discovered.skills.map((skill) => ({
157
309
  name: skill.name,
158
310
  description: readDescription(skill.path),
311
+ whenToUse: readWhenToUse(skill.path),
159
312
  path: skill.path,
160
313
  source: "plugin" as const,
161
314
  scope: "plugin" as const,
162
315
  state: "on" as const,
163
316
  pluginName: skill.plugin,
317
+ disableModelInvocation: readModelInvocationDisabled(skill.path),
164
318
  }));
165
- return [...project, ...plugin];
319
+ // A plugin command is listed when it describes itself, as in Claude Code.
320
+ const commands = !withCommands ? [] : discovered.commands.flatMap((command) => {
321
+ const description = readDescription(command.path);
322
+ const whenToUse = readWhenToUse(command.path);
323
+ if (!description?.trim() && !whenToUse) return [];
324
+ return [
325
+ {
326
+ name: command.name,
327
+ description,
328
+ whenToUse,
329
+ path: command.path,
330
+ source: "plugin" as const,
331
+ scope: "plugin" as const,
332
+ state: "on" as const,
333
+ pluginName: command.plugin,
334
+ kind: "command" as const,
335
+ disableModelInvocation: readModelInvocationDisabled(command.path),
336
+ },
337
+ ];
338
+ });
339
+ return [...project.filter((skill) => !isBundled(skill.path)), ...commands, ...plugin, ...project.filter((skill) => isBundled(skill.path))];
166
340
  };
341
+ /** Skills proper: the /skills panel and the typed forms leave plugin commands to the plugins extension. */
342
+ const skillsOnly = (cwd = sessionCwd): IndexedSkill[] => index(cwd, false);
167
343
 
168
- const describe = () => skillListingText(index());
344
+ /**
345
+ * Usage as the session found it: read once, so the listing's ranking (and
346
+ * with it message 1 of the cached prefix) does not move when a skill is
347
+ * used mid-session or a day's decay crosses a threshold.
348
+ */
349
+ let sessionUsage: { usage: ReturnType<typeof readUsage>; now: Date } | undefined;
350
+
351
+ /** The listing, within Claude Code's budget for the session model's context window. */
352
+ const describe = () => {
353
+ sessionUsage ??= { usage: readUsage(pluginRoot(getAgentDir())), now: new Date() };
354
+ const { usage, now } = sessionUsage;
355
+ const skills = index().map((skill) => ({
356
+ ...skill,
357
+ bundled: isBundled(skill.path),
358
+ usage: usageScore(usage[usageKey(skill.kind ?? "skill", skill.name)], now),
359
+ }));
360
+ return skillListingText(skills, skillListingBudget(sessionModel?.contextWindow));
361
+ };
169
362
 
170
363
  pi.registerTool({
171
364
  name: "skill",
172
365
  label: "Skill",
173
366
  ...ccToolRenderers<{ skill?: string; args?: string }>("Skill", {
174
367
  title: (a) => (a ? [a.skill, a.args].filter(Boolean).join(" ") : undefined),
175
- // The full instruction text goes to the model; the transcript needs one line.
368
+ // The skill's text goes to the model; the transcript needs one line.
176
369
  result: (_r, a, isError) => (isError ? undefined : a?.skill ? `Loaded ${a.skill}` : undefined),
177
370
  }),
178
371
  description:
@@ -183,12 +376,12 @@ export default function skillExtension(pi: ExtensionAPI) {
183
376
  args: Type.Optional(Type.String({ description: "Arguments to pass through to the skill" })),
184
377
  list: Type.Optional(Type.Boolean({ description: "List available skills instead of invoking one" })),
185
378
  }),
186
- async execute(_toolCallId, params) {
379
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
187
380
  // `list`, or a bare call with no invoke signal, browses the catalog. A
188
381
  // call that passed `args` but no `skill` is an invocation that forgot to
189
382
  // name the skill — fail loudly rather than silently returning the
190
383
  // catalog, which a weak model reads as a non-sequitur (same archetype as
191
- // the subagent tool; see docs/decisions/subagents-workflows.md).
384
+ // the subagent tool; see working-docs/decisions/subagents-workflows.md).
192
385
  const all = index();
193
386
  if (params.list || (!params.skill && params.args == null)) {
194
387
  return {
@@ -241,10 +434,21 @@ export default function skillExtension(pi: ExtensionAPI) {
241
434
  };
242
435
  }
243
436
 
244
- let body: string;
437
+ // Only the user may start a skill marked `disable-model-invocation`
438
+ // (a typed `/<name>` runs through deliverSkill, not this tool).
439
+ if (found.disableModelInvocation || readModelInvocationDisabled(found.path)) {
440
+ return {
441
+ content: [{ type: "text", text: `Skill ${wanted} cannot be used with Skill tool due to disable-model-invocation` }],
442
+ details: { skill: found.name } as Record<string, unknown>,
443
+ isError: true,
444
+ };
445
+ }
446
+
447
+ let promptText: string;
245
448
  try {
246
449
  const parsed = parseFrontmatterLoosely(readFileSync(found.path, "utf-8")) as { body: string };
247
- body = parsed.body.trim();
450
+ const { body, args } = skillBody(found, parsed.body.trim(), params.args?.trim() ?? "", ctx.cwd);
451
+ promptText = skillPromptText(body, args, baseDirFor(found));
248
452
  } catch (error) {
249
453
  return {
250
454
  content: [{ type: "text", text: `Could not read skill "${found.name}": ${(error as Error).message}` }],
@@ -253,76 +457,130 @@ export default function skillExtension(pi: ExtensionAPI) {
253
457
  };
254
458
  }
255
459
 
256
- recordUsage(pluginRoot(getAgentDir()), "skill", found.name);
460
+ recordUsage(pluginRoot(getAgentDir()), found.kind ?? "skill", found.name);
257
461
 
258
- // Resource paths in a skill are relative to its own directory, so the
259
- // model needs to know where it lives to read references/ or scripts/.
260
- const header = [
261
- `Skill: ${found.name}`,
262
- `Location: ${found.path}`,
263
- params.args ? `Arguments: ${params.args}` : undefined,
264
- "Follow these instructions for the current task.",
265
- ]
266
- .filter(Boolean)
267
- .join("\n");
462
+ // A skill that takes arguments, called without any, returns its
463
+ // no-argument body; say so, since a model previewing the skill would
464
+ // otherwise follow it (invoke.ts missingArgumentsNote).
465
+ const hint = params.args?.trim() ? undefined : readArgumentHint(found.path);
466
+ const hintText = hint?.hint ?? hint?.argNames?.map((name) => `<${name}>`).join(" ");
467
+ const note = hintText ? missingArgumentsNote(hintText) : undefined;
268
468
 
469
+ // Claude Code's shape: the result says the skill is launching, and its
470
+ // text follows as a block of its own.
269
471
  return {
270
- content: [{ type: "text", text: `${header}\n\n---\n\n${body}` }],
472
+ content: [
473
+ { type: "text", text: launchingSkill(found.name) },
474
+ { type: "text", text: note ? `${note.before}\n\n${promptText}\n\n${note.after}` : promptText },
475
+ ],
271
476
  details: { skill: found.name, path: found.path } as Record<string, unknown>,
272
477
  };
273
478
  },
274
479
  });
275
480
 
276
481
  /**
277
- * Surface a user-facing skill message: a TUI/RPC toast where there is UI, or
278
- * stderr in a one-shot run (`-p`, `--mode json`), where `ctx.ui.notify` is a
279
- * no-op (pi wires no UI context there) so the message would otherwise vanish
280
- * and the command would exit silently. `console.error` writes to stderr, so it
281
- * never corrupts the `--mode json` event stream on stdout.
482
+ * A skill's body and the arguments still to append: a generated body
483
+ * (lib/skill-body.ts, `/loop`) carries its arguments, a file's does not.
484
+ * The bundled code-review picks its body by tier and effort (code-review.ts).
282
485
  */
283
- const notifyOrPrint = (
284
- ctx: Pick<ExtensionContext, "hasUI" | "ui">,
285
- message: string,
286
- level: "warning" | "error",
287
- ): void => {
288
- if (ctx.hasUI) ctx.ui.notify(message, level);
289
- else console.error(message);
486
+ const skillBody = (found: IndexedSkill, fileBody: string, args: string, cwd: string): { body: string; args: string } => {
487
+ if (found.kind === "command") return { body: fileBody, args };
488
+ if (resolve(found.path) === CODE_REVIEW_PATH) {
489
+ try {
490
+ const review = codeReviewBody(CODE_REVIEW_CELLS, requestTier(), pi.getThinkingLevel(), args);
491
+ return { body: review.body, args: review.args };
492
+ } catch {
493
+ // A cell file missing from an install: the SKILL.md body (the medium cell) still reviews.
494
+ }
495
+ }
496
+ const query: SkillBodyQuery = { skill: found.name, path: found.path, args, cwd };
497
+ pi.events.emit(SKILL_BODY_CHANNEL, query);
498
+ return query.body !== undefined ? { body: query.body, args: "" } : { body: fileBody, args };
290
499
  };
500
+ /** The folder Claude Code names before a skill's text; none for One Code's own catalog. */
501
+ const baseDirFor = (found: IndexedSkill): string | undefined => (found.kind === "command" || isBundled(found.path) ? undefined : dirname(found.path));
502
+
503
+ // A fired scheduled prompt that is a skill's slash command runs that skill,
504
+ // as Claude Code's queue runs a fired `/babysit-prs` (lib/skill-body.ts).
505
+ pi.events.on(SLASH_EXPAND_CHANNEL, (data) => {
506
+ const query = data as SlashExpandQuery;
507
+ const command = parseSlashCommand(query.text);
508
+ if (!command) return;
509
+ const found = resolveSkill(skillsOnly(query.cwd), command.name.replace(/^skill:/, ""));
510
+ if (!found || found.state === "off") return;
511
+ let fileBody: string;
512
+ try {
513
+ fileBody = parseFrontmatterLoosely(readFileSync(found.path, "utf-8")).body.trim();
514
+ } catch {
515
+ return;
516
+ }
517
+ recordUsage(pluginRoot(getAgentDir()), "skill", found.name);
518
+ const { body, args } = skillBody(found, fileBody, command.args, query.cwd);
519
+ query.expanded = typedSkillContent(found.name, command.args, skillPromptText(body, args, baseDirFor(found))).join("");
520
+ });
521
+
522
+ /** The `input` hook's answer that replaces the typed prompt with the skill's message. */
523
+ type SkillTransform = { action: "transform"; text: string; images?: Array<{ type: "image"; data: string; mimeType: string }> };
291
524
 
292
525
  /**
293
526
  * Run a resolved skill the way a user-typed command does: refuse an "off"
294
527
  * skill (in EVERY mode — falling through would hand `/skill:` to pi's native
295
528
  * expansion, which knows nothing of the overrides store and would run it; a
296
529
  * one-shot run gets the refusal on stderr instead of a UI notice, never as
297
- * silent execution), else re-deliver pi's exact `<skill>` block as a hidden
298
- * custom message. The model receives the same bytes pi's own expansion would
299
- * submit as a user turn (convertToLlm maps a custom message to a user message
300
- * regardless of `display`), but nothing renders. Shared by the `/skill:<name>`
301
- * interception and the bare `/<name>` commands.
530
+ * silent execution), else deliver Claude Code's typed-skill message (the
531
+ * command breadcrumb and the skill's text) as a hidden custom message: the
532
+ * model receives it as a user message (convertToLlm maps a custom message
533
+ * to one regardless of `display`), but nothing renders and no `skill` call
534
+ * is needed. Shared by the `/skill:<name>` interception, the typed
535
+ * `/<plugin>:<skill>` form and the bare `/<name>` commands.
302
536
  */
303
537
  const deliverSkill = async (
304
538
  found: IndexedSkill,
305
539
  args: string,
306
540
  ctx: ExtensionContext & { waitForIdle?: () => Promise<void> },
307
- extra: { images?: Array<{ type: "image"; data: string; mimeType: string }>; streamingBehavior?: "steer" | "followUp" } = {},
308
- ): Promise<"handled" | "unavailable"> => {
541
+ extra: {
542
+ images?: Array<{ type: "image"; data: string; mimeType: string }>;
543
+ streamingBehavior?: "steer" | "followUp";
544
+ input?: string;
545
+ /** Called from the `input` hook, which can hand the message back as the prompt's own text. */
546
+ fromInput?: boolean;
547
+ } = {},
548
+ ): Promise<"handled" | "unavailable" | SkillTransform> => {
309
549
  if (found.state === "off") {
310
550
  const where = found.source === "plugin" ? "/plugins" : "/skills";
311
551
  notifyOrPrint(ctx, `Skill "${found.name}" is turned off — enable it from ${where} to run it.`, "warning");
312
552
  return "handled";
313
553
  }
314
- let body: string;
554
+ let fileBody: string;
315
555
  try {
316
- body = stripFrontmatter(readFileSync(found.path, "utf-8")).trim();
556
+ fileBody = parseFrontmatterLoosely(readFileSync(found.path, "utf-8")).body.trim();
317
557
  } catch {
318
558
  return "unavailable";
319
559
  }
320
560
  recordUsage(pluginRoot(getAgentDir()), "skill", found.name);
321
- const block = buildSkillBlock({ name: found.name, filePath: found.path }, body, args);
322
- // Carry any attached images alongside the block, as pi's native path would.
323
- const content = extra.images?.length ? [{ type: "text" as const, text: block }, ...extra.images] : block;
561
+ const { body, args: bodyArgs } = skillBody(found, fileBody, args, ctx.cwd);
562
+ // Claude Code's typed-skill message: the command breadcrumb, then the
563
+ // skill's text; any attached images ride after them.
564
+ const [breadcrumb, text] = typedSkillContent(found.name, args, skillPromptText(body, bodyArgs, baseDirFor(found)));
565
+ const content = [{ type: "text" as const, text: breadcrumb }, { type: "text" as const, text }, ...(extra.images ?? [])];
566
+ if (!prompted && !extra.streamingBehavior) {
567
+ // From the input hook the message becomes the typed prompt's own text
568
+ // (the same bytes sendUserMessage would send: its text parts joined by a
569
+ // newline). sendUserMessage starts a second prompt that is still in its
570
+ // async preflight when this handler returns, so the session looks idle,
571
+ // and a one-shot run disposed the session before that turn ran.
572
+ if (extra.fromInput) return { action: "transform", text: `${breadcrumb}\n${text}`, images: extra.images };
573
+ pi.sendUserMessage(content, { deliverAs: "followUp" });
574
+ await awaitOneShotTurn(ctx);
575
+ return "handled";
576
+ }
324
577
  pi.sendMessage(
325
- { customType: "one-code:skill-invocation", content, display: false, details: { skill: found.name, args } },
578
+ {
579
+ customType: SKILL_INVOCATION_TYPE,
580
+ content,
581
+ display: false,
582
+ details: { skill: found.name, args, ...(extra.input !== undefined ? { input: extra.input } : {}) } satisfies SkillInvocationDetails,
583
+ },
326
584
  { triggerTurn: true, ...(extra.streamingBehavior ? { deliverAs: extra.streamingBehavior } : {}) },
327
585
  );
328
586
  // The turn we just triggered rides a fire-and-forget pi.sendMessage. In a
@@ -339,21 +597,52 @@ export default function skillExtension(pi: ExtensionAPI) {
339
597
  // the transcript as a new user turn. Intercept it here, suppress pi's
340
598
  // expansion (return "handled"), and deliver through deliverSkill. An unknown
341
599
  // name, an ambiguous plugin match, or an unreadable file falls through to
342
- // pi's native handling. This hook only covers prompt(); paths that expand
343
- // without an `input` event (steer/followUp) are caught by the context-hook
344
- // redaction below.
600
+ // pi's native handling. Since pi 0.86.0 (faa9863cb) this also runs for a
601
+ // message queued mid-turn and for RPC steer/followUp (`streamingBehavior` is
602
+ // set), so an off skill is refused before it enters the queue. On older pi
603
+ // those paths skip `input`; the context-hook redaction below covers them.
604
+ //
605
+ // A typed `/<plugin>:<skill>` has no command of its own (plugin skills get
606
+ // no bare alias), so it reached the model as text and the model called
607
+ // `skill`; it is expanded here too, as Claude Code expands it. Only an exact
608
+ // plugin-skill name is taken; anything else stays the user's text.
609
+ //
610
+ // In a one-shot run the bare `/<name>` forms are not commands (see
611
+ // registerSkillCommands) and are taken here too, so the first skill becomes
612
+ // the prompt's own text and the run waits for its turn.
345
613
  pi.on("input", async (event, ctx) => {
346
- const cmd = parseSkillCommand(event.text);
614
+ const typed = parseSkillCommand(event.text) ?? oneShotBareSkill(event.text);
615
+ const plugin = typed ? undefined : typedPluginSkill(event.text);
616
+ const cmd = typed ?? plugin?.command;
347
617
  if (!cmd) return { action: "continue" };
348
- const found = resolveSkill(index(), cmd.name);
618
+ const found = plugin?.skill ?? resolveSkill(skillsOnly(ctx.cwd), cmd.name);
349
619
  if (!found) return { action: "continue" };
350
620
  const outcome = await deliverSkill(found, cmd.args, ctx, {
351
621
  images: event.images as Array<{ type: "image"; data: string; mimeType: string }> | undefined,
352
622
  streamingBehavior: event.streamingBehavior,
623
+ input: event.text,
624
+ fromInput: true,
353
625
  });
626
+ if (typeof outcome === "object") return outcome;
354
627
  return { action: outcome === "handled" ? "handled" : "continue" };
355
628
  });
356
629
 
630
+ /** A typed bare `/<name> [args]` naming a skill whose command a one-shot run left unregistered. */
631
+ const oneShotBareSkill = (text: string): { name: string; args: string } | undefined => {
632
+ if (!oneShot || !text.startsWith("/")) return undefined;
633
+ const command = parseSlashCommand(text);
634
+ return command && oneShotSkillNames.has(command.name) ? command : undefined;
635
+ };
636
+
637
+ /** A typed `/<plugin>:<skill> [args]` naming a plugin skill exactly, and that skill. */
638
+ const typedPluginSkill = (text: string): { command: { name: string; args: string }; skill: IndexedSkill } | undefined => {
639
+ if (!text.startsWith("/")) return undefined;
640
+ const command = parseSlashCommand(text);
641
+ if (!command?.name.includes(":")) return undefined;
642
+ const skill = skillsOnly().find((candidate) => candidate.source === "plugin" && candidate.name === command.name);
643
+ return skill ? { command, skill } : undefined;
644
+ };
645
+
357
646
  // Claude Code invokes a user/project skill as a bare `/<name>` (only plugin
358
647
  // skills carry a `<plugin>:` prefix); pi registers `/skill:<name>`. Register
359
648
  // each project/user/bundled skill as a bare extension command too, so
@@ -370,7 +659,7 @@ export default function skillExtension(pi: ExtensionAPI) {
370
659
  // be unregistered, so the handler resolves the skill afresh at invocation:
371
660
  // a skill deleted or turned off since registration is refused, never run.
372
661
  const registeredSkillCommands = new Set<string>();
373
- const registerSkillCommands = (cwd: string | undefined) => {
662
+ const registerSkillCommands = (cwd: string | undefined, skills: IndexedSkill[] = skillsOnly(cwd)) => {
374
663
  let taken: string[];
375
664
  try {
376
665
  taken = pi.getCommands().map((command) => command.name);
@@ -378,12 +667,18 @@ export default function skillExtension(pi: ExtensionAPI) {
378
667
  return; // not bound yet (load time) — session_start retries
379
668
  }
380
669
  const templates = promptTemplateNames(cwd ?? process.cwd(), os.homedir(), getAgentDir());
381
- for (const skill of skillCommandCandidates(index(cwd), [...taken, ...templates, ...registeredSkillCommands])) {
670
+ for (const skill of skillCommandCandidates(skills, [...taken, ...templates, ...registeredSkillCommands, ...oneShotSkillNames])) {
671
+ if (oneShot) {
672
+ oneShotSkillNames.add(skill.name);
673
+ continue;
674
+ }
382
675
  registeredSkillCommands.add(skill.name);
676
+ const hint = readArgumentHint(skill.path);
677
+ if (hint) announceArgumentHint(pi, skill.name, hint);
383
678
  pi.registerCommand(skill.name, {
384
679
  description: skill.description ? `${skill.description} (skill)` : `Run the ${skill.name} skill`,
385
680
  handler: async (args, ctx) => {
386
- const found = resolveSkill(index(ctx.cwd), skill.name);
681
+ const found = resolveSkill(skillsOnly(ctx.cwd), skill.name);
387
682
  if (!found || (await deliverSkill(found, args.trim(), ctx)) === "unavailable") {
388
683
  notifyOrPrint(ctx, `Skill "${skill.name}" is no longer available (its SKILL.md was removed or is unreadable).`, "error");
389
684
  }
@@ -391,20 +686,46 @@ export default function skillExtension(pi: ExtensionAPI) {
391
686
  });
392
687
  }
393
688
  };
394
- pi.on("session_start", (_event, ctx) => registerSkillCommands(ctx.cwd));
689
+ pi.on("session_start", (_event, ctx) => {
690
+ const skills = skillsOnly(ctx.cwd);
691
+ registerSkillCommands(ctx.cwd, skills);
692
+ // pi lists every skill a second time as `/skill:<name>`; drop that entry
693
+ // where the bare command exists (withoutDuplicateSkillCommands).
694
+ if (ctx.hasUI && ctx.mode === "tui") {
695
+ ctx.ui.addAutocompleteProvider((current) => ({
696
+ ...current,
697
+ getSuggestions: async (lines, cursorLine, cursorCol, options) => {
698
+ const suggestions = await current.getSuggestions(lines, cursorLine, cursorCol, options);
699
+ if (!suggestions) return suggestions;
700
+ const items = withoutDuplicateSkillCommands(suggestions.items, suggestions.prefix, registeredSkillCommands);
701
+ return items.length > 0 ? { ...suggestions, items } : null;
702
+ },
703
+ applyCompletion: (...args) => current.applyCompletion(...args),
704
+ ...(current.shouldTriggerFileCompletion ? { shouldTriggerFileCompletion: (...args) => current.shouldTriggerFileCompletion!(...args) } : {}),
705
+ }));
706
+ }
707
+ // pi's own `/skill:<name>` form, which pi lists only after the first turn.
708
+ for (const skill of skills) {
709
+ const hint = skill.source === "plugin" ? undefined : readArgumentHint(skill.path);
710
+ if (hint) announceArgumentHint(pi, `skill:${skill.name}`, hint);
711
+ }
712
+ });
395
713
  // pi's per-turn resolution (frontmatter `name`, description required) can
396
714
  // surface skills the pre-turn disk scan missed; late registrations still
397
715
  // execute (autocomplete lists them after a /reload). Gated on an unseen
398
716
  // name so the steady state costs one Set lookup per skill per turn, not a
399
717
  // re-index (before_agent_start runs every turn).
400
718
  pi.on("before_agent_start", (_event, ctx) => {
401
- if (piSkills.some((skill) => !registeredSkillCommands.has(skill.name))) registerSkillCommands(ctx.cwd);
719
+ if (piSkills.some((skill) => !registeredSkillCommands.has(skill.name) && !oneShotSkillNames.has(skill.name))) registerSkillCommands(ctx.cwd);
402
720
  });
403
721
 
404
- // Fail-closed backstop for the `input` interception above: pi's steer() and
405
- // followUp() expand `/skill:<name>` natively WITHOUT firing an `input` event
406
- // (queued interactive messages after the first, RPC steer/followUp), so a
407
- // turned-off skill's instructions can land in the session history anyway.
722
+ // FALLBACK for pi < 0.86.0 (plan.md "Fallbacks for older pi"; delete it
723
+ // when the peer floor reaches 0.86.0). Before faa9863cb, pi's steer() and
724
+ // followUp() expanded `/skill:<name>` natively WITHOUT firing an `input`
725
+ // event (queued interactive messages after the first, RPC steer/followUp),
726
+ // so a turned-off skill's instructions could land in the session history.
727
+ // On 0.86+ the `input` handler above refuses them first, so this finds
728
+ // nothing to redact and returns undefined.
408
729
  // Strip them from every outgoing request instead — the wire copy carries the
409
730
  // refusal notice, the session file keeps the original bytes, and untouched
410
731
  // requests return undefined so the message array stays byte-identical
@@ -421,10 +742,11 @@ export default function skillExtension(pi: ExtensionAPI) {
421
742
  });
422
743
 
423
744
  const buildSkillsRows = (cwd: string | undefined): SkillsRow[] =>
424
- index(cwd).map((skill) => ({
745
+ skillsOnly(cwd).map((skill) => ({
425
746
  key: skillOverrideKey(skill.scope, skill.name),
426
747
  name: skill.name,
427
- scope: skill.scope,
748
+ // The override key keeps the path's scope; the row names the catalog.
749
+ scope: isBundled(skill.path) ? ("bundled" as const) : skill.scope,
428
750
  tokens: estimateSkillTokens(skill.path),
429
751
  state: skill.state,
430
752
  locked: skill.source === "plugin",
@@ -436,7 +758,7 @@ export default function skillExtension(pi: ExtensionAPI) {
436
758
  // the skill-overrides store live. Plugin skills show locked (managed via
437
759
  // /plugins). Pure state/render live in ./panel; this owns repaint + writes.
438
760
  const openSkillsPanel = async (ctx: ExtensionContext): Promise<void> => {
439
- await ctx.ui.custom<null>((tui, theme, _keybindings, done) => {
761
+ await panelUi(pi.events, ctx.ui).custom<null>((tui, theme, _keybindings, done) => {
440
762
  const paint: SkillsPaint = { fg: safeThemePaint(theme), bold: safeThemeBold(theme) };
441
763
  const state = initialSkillsState();
442
764
  let rows = buildSkillsRows(ctx.cwd);
@@ -462,7 +784,6 @@ export default function skillExtension(pi: ExtensionAPI) {
462
784
  if (!key) return;
463
785
  const before = visibleRows(rows, state);
464
786
  const anchorKey = before[state.cursor]?.key;
465
- const sortBefore = state.sort;
466
787
  const effect = applySkillsKey(state, key, before);
467
788
  notice = undefined;
468
789
  if (effect?.kind === "close") {
@@ -478,13 +799,8 @@ export default function skillExtension(pi: ExtensionAPI) {
478
799
  } else if (effect?.kind === "locked") {
479
800
  notice = `${effect.row.name} is a plugin skill — manage it from /plugins.`;
480
801
  }
481
- // A sort toggle reorders the list; keep the same skill selected so a
482
- // following Space acts on the row the user was looking at, not the one
483
- // that slid into its index.
484
- if (state.sort !== sortBefore && anchorKey) {
485
- const idx = visibleRows(rows, state).findIndex((row) => row.key === anchorKey);
486
- if (idx >= 0) state.cursor = idx;
487
- }
802
+ // A sort toggle or a cycle under the state sort reorders the list.
803
+ if ((key.kind === "text" || effect?.kind === "cycle") && anchorKey) followRow(state, visibleRows(rows, state), anchorKey);
488
804
  repaint();
489
805
  },
490
806
  invalidate: () => {
@@ -495,17 +811,20 @@ export default function skillExtension(pi: ExtensionAPI) {
495
811
  };
496
812
 
497
813
  registerLocalCommand(pi, "skills", {
814
+ reportsResult: true,
498
815
  description: "View and manage skills (on / name-only / user-only / off)",
499
816
  handler: async (args, ctx) => {
500
- if (ctx.hasUI) {
817
+ if (canShowCustomUi(ctx)) {
501
818
  await openSkillsPanel(ctx);
502
- return;
819
+ return "Skills dialog dismissed";
503
820
  }
821
+ const readOnly = notifyRpcReadOnly(ctx, "/skills", "change skill states");
822
+ const withNotice = (text: string) => (readOnly ? `${readOnly}\n${text}` : text);
504
823
  // Non-interactive fallback: a flat listing with each skill's state.
505
- const all = index(ctx.cwd);
824
+ const all = skillsOnly(ctx.cwd);
506
825
  if (all.length === 0) {
507
826
  ctx.ui.notify("No skills available.", "info");
508
- return;
827
+ return withNotice("No skills available.");
509
828
  }
510
829
  const lines = all.map((skill) => {
511
830
  const label = skill.source === "plugin" ? "plugin" : skill.state;
@@ -513,6 +832,7 @@ export default function skillExtension(pi: ExtensionAPI) {
513
832
  return `- ${skill.name} [${label}]${desc}`;
514
833
  });
515
834
  ctx.ui.notify(`Skills:\n${lines.join("\n")}`, "info");
835
+ return withNotice(`Skills:\n${lines.join("\n")}`);
516
836
  },
517
837
  });
518
838
  }