one-code-extension 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (419) hide show
  1. package/README.md +16 -5
  2. package/agents/general-purpose.md +2 -2
  3. package/docs/guide/README.md +2 -1
  4. package/docs/guide/bring-your-claude-code-setup.md +60 -9
  5. package/docs/guide/configuration.md +48 -14
  6. package/docs/guide/differences-from-claude-code.md +32 -23
  7. package/docs/guide/doctor.md +20 -13
  8. package/docs/guide/hooks.md +18 -3
  9. package/docs/guide/installation.md +27 -4
  10. package/docs/guide/permissions-modes-and-auto-mode.md +225 -49
  11. package/docs/guide/providers-and-models.md +137 -54
  12. package/docs/guide/reference.md +27 -15
  13. package/docs/guide/sessions-and-context.md +69 -16
  14. package/docs/guide/skills-plugins-and-mcp.md +57 -20
  15. package/docs/guide/subagents-and-workflows.md +95 -29
  16. package/docs/guide/tasks-and-background-work.md +101 -26
  17. package/docs/guide/terminal-interface.md +24 -12
  18. package/docs/guide/tools.md +46 -5
  19. package/docs/guide/troubleshooting.md +13 -9
  20. package/docs/guide/windows.md +58 -31
  21. package/extensions/artifacts/gallery.ts +227 -0
  22. package/extensions/artifacts/index.ts +288 -0
  23. package/extensions/artifacts/store.ts +570 -0
  24. package/extensions/artifacts/viewer.ts +199 -0
  25. package/extensions/ask-user/description.ts +67 -0
  26. package/extensions/ask-user/dialogs.ts +80 -0
  27. package/extensions/ask-user/index.ts +37 -33
  28. package/extensions/ask-user/questions.ts +32 -7
  29. package/extensions/ask-user/widget.ts +45 -25
  30. package/extensions/auto-mode/actions.ts +6 -0
  31. package/extensions/auto-mode/cache.ts +88 -0
  32. package/extensions/auto-mode/classifier-prompt.ts +5 -4
  33. package/extensions/auto-mode/classifier.ts +290 -65
  34. package/extensions/auto-mode/config.ts +72 -20
  35. package/extensions/auto-mode/decision-log.ts +2 -0
  36. package/extensions/auto-mode/defaults.ts +1 -1
  37. package/extensions/auto-mode/denials.ts +123 -0
  38. package/extensions/auto-mode/git-checkout-programs.ts +187 -0
  39. package/extensions/auto-mode/git-status-meta.ts +159 -0
  40. package/extensions/auto-mode/history.ts +189 -0
  41. package/extensions/auto-mode/instructions.ts +58 -52
  42. package/extensions/auto-mode/model-select.ts +159 -134
  43. package/extensions/auto-mode/paths.ts +57 -5
  44. package/extensions/auto-mode/pause.ts +57 -2
  45. package/extensions/auto-mode/prompt.ts +37 -21
  46. package/extensions/auto-mode/read-only-options.ts +724 -0
  47. package/extensions/auto-mode/resolved-paths-meta.ts +133 -0
  48. package/extensions/auto-mode/rules.ts +18 -1
  49. package/extensions/auto-mode/safety-floor.ts +361 -18
  50. package/extensions/auto-mode/setup-run.ts +20 -14
  51. package/extensions/auto-mode/shell-analysis.ts +764 -490
  52. package/extensions/auto-mode/shell-parse.ts +885 -0
  53. package/extensions/auto-mode/system-note.ts +33 -0
  54. package/extensions/auto-mode/transcript.ts +80 -51
  55. package/extensions/background/cron.ts +515 -0
  56. package/extensions/background/index.ts +514 -236
  57. package/extensions/background/loop-fire.ts +136 -0
  58. package/extensions/background/loop-preamble.ts +59 -0
  59. package/extensions/background/loop-skill.ts +163 -0
  60. package/extensions/background/monitor-batch.ts +53 -0
  61. package/extensions/background/noop-fold.ts +128 -0
  62. package/extensions/background/switch-guard.ts +52 -0
  63. package/extensions/background/wakeup.ts +159 -55
  64. package/extensions/bash/background.ts +53 -17
  65. package/extensions/bash/child.ts +7 -0
  66. package/extensions/bash/description.ts +188 -0
  67. package/extensions/bash/guards.ts +15 -67
  68. package/extensions/bash/index.ts +88 -35
  69. package/extensions/branding/compaction-keep.ts +63 -0
  70. package/extensions/branding/declined.ts +22 -0
  71. package/extensions/branding/index.ts +133 -9
  72. package/extensions/branding/prompt-editor.ts +53 -6
  73. package/extensions/branding/queued-edit.ts +31 -0
  74. package/extensions/branding/replaced-builtins.ts +68 -0
  75. package/extensions/branding/startup.ts +33 -71
  76. package/extensions/btw/index.ts +229 -71
  77. package/extensions/btw/panel.ts +228 -100
  78. package/extensions/btw/prompt.ts +41 -4
  79. package/extensions/claude-compat/index.ts +7 -11
  80. package/extensions/claude-context/index.ts +446 -58
  81. package/extensions/compaction/cut.ts +157 -0
  82. package/extensions/compaction/index.ts +216 -75
  83. package/extensions/compaction/kept-thinking.ts +62 -0
  84. package/extensions/compaction/prompt.ts +15 -12
  85. package/extensions/context-budget/index.ts +24 -5
  86. package/extensions/context-management/index.ts +25 -0
  87. package/extensions/doctor/build.ts +59 -8
  88. package/extensions/doctor/builtins.ts +84 -0
  89. package/extensions/doctor/cli.ts +7 -1
  90. package/extensions/doctor/compat.ts +88 -23
  91. package/extensions/doctor/dependencies.ts +41 -5
  92. package/extensions/doctor/fix-prompt.ts +9 -2
  93. package/extensions/doctor/index.ts +102 -58
  94. package/extensions/doctor/models.ts +102 -63
  95. package/extensions/doctor/presets.ts +42 -27
  96. package/extensions/doctor/report.ts +19 -2
  97. package/extensions/doctor/viewer.ts +49 -27
  98. package/extensions/effort/index.ts +72 -19
  99. package/extensions/effort/settings.ts +21 -0
  100. package/extensions/effort/slider.ts +24 -20
  101. package/extensions/exit/index.ts +32 -1
  102. package/extensions/file-tracker/index.ts +337 -26
  103. package/extensions/file-tracker/replay.ts +96 -10
  104. package/extensions/file-tracker/restore.ts +63 -0
  105. package/extensions/file-tracker/results.ts +66 -0
  106. package/extensions/file-tracker/shell-reads.ts +211 -0
  107. package/extensions/file-tracker/tracker.ts +32 -15
  108. package/extensions/footer/footer-line.ts +45 -24
  109. package/extensions/footer/index.ts +47 -2
  110. package/extensions/footer/pr.ts +34 -5
  111. package/extensions/hooks/compaction.ts +7 -0
  112. package/extensions/hooks/executor.ts +32 -30
  113. package/extensions/hooks/index.ts +198 -31
  114. package/extensions/hooks/matcher.ts +3 -0
  115. package/extensions/hooks/protocol.ts +41 -3
  116. package/extensions/hooks/settings.ts +50 -19
  117. package/extensions/hooks/subagent-bridge.ts +2 -0
  118. package/extensions/hooks/trust.ts +13 -5
  119. package/extensions/idle-compact/index.ts +119 -0
  120. package/extensions/idle-compact/policy.ts +143 -0
  121. package/extensions/init/index.ts +11 -10
  122. package/extensions/init/prompt.ts +25 -32
  123. package/extensions/interrupted/index.ts +13 -1
  124. package/extensions/kept-thinking/index.ts +27 -0
  125. package/extensions/lib/agent-cron.ts +130 -0
  126. package/extensions/lib/agent-loader.ts +15 -1
  127. package/extensions/lib/agent-view.ts +85 -0
  128. package/extensions/lib/anthropic-payload.ts +82 -1
  129. package/extensions/lib/anthropic-server-call.ts +230 -0
  130. package/extensions/lib/anthropic-server-tools.ts +485 -0
  131. package/extensions/lib/app-launch.mjs +94 -0
  132. package/extensions/lib/argument-hints.ts +190 -0
  133. package/extensions/lib/atomic-write.ts +6 -2
  134. package/extensions/lib/auto-memory.ts +75 -0
  135. package/extensions/lib/bash-parser.ts +84 -0
  136. package/extensions/lib/btw-fork.ts +93 -0
  137. package/extensions/lib/child-extensions.ts +42 -0
  138. package/extensions/lib/child-writes.ts +45 -0
  139. package/extensions/lib/claude-context.ts +515 -100
  140. package/extensions/lib/claude-external-includes.ts +68 -0
  141. package/extensions/lib/claude-id.ts +22 -0
  142. package/extensions/lib/claude-rules.ts +323 -0
  143. package/extensions/lib/claude-settings.ts +76 -4
  144. package/extensions/lib/compaction-boundary.ts +36 -0
  145. package/extensions/lib/compaction-keep.mjs +83 -0
  146. package/extensions/lib/config-mode.ts +112 -0
  147. package/extensions/lib/consent-dialogs.ts +63 -0
  148. package/extensions/lib/consent-preview.ts +5 -1
  149. package/extensions/lib/consent-stores.ts +48 -0
  150. package/extensions/lib/context-facts.ts +78 -0
  151. package/extensions/lib/context-stack.ts +81 -0
  152. package/extensions/lib/deferred.ts +219 -25
  153. package/extensions/lib/dock-rows.ts +60 -0
  154. package/extensions/lib/empty-tool-result.ts +27 -0
  155. package/extensions/lib/environment-block.ts +76 -0
  156. package/extensions/lib/fetch-timeout.ts +24 -6
  157. package/extensions/{system-prompt → lib}/git-status.ts +51 -13
  158. package/extensions/lib/git.ts +75 -13
  159. package/extensions/lib/grammars/LICENSE-tree-sitter-bash +21 -0
  160. package/extensions/lib/grammars/README.md +2 -0
  161. package/extensions/lib/grammars/tree-sitter-bash.wasm +0 -0
  162. package/extensions/lib/guide-docs.ts +45 -0
  163. package/extensions/lib/headless-output.ts +46 -0
  164. package/extensions/lib/hook-gate.ts +7 -7
  165. package/extensions/lib/interrupt.ts +20 -4
  166. package/extensions/lib/key-input.ts +455 -0
  167. package/extensions/lib/keys.ts +14 -1
  168. package/extensions/lib/local-command.ts +47 -13
  169. package/extensions/lib/mcp-overrides.ts +1 -1
  170. package/extensions/lib/mcp-share.ts +46 -0
  171. package/extensions/lib/memory-content.ts +146 -0
  172. package/extensions/lib/memory-index.ts +18 -0
  173. package/extensions/lib/memory.ts +134 -123
  174. package/extensions/lib/model-catalog/huggingface.json +174 -0
  175. package/extensions/lib/model-catalog/models-dev.json +18297 -0
  176. package/extensions/lib/model-catalog/openrouter.json +5252 -0
  177. package/extensions/lib/model-catalog-data.ts +513 -0
  178. package/extensions/lib/model-catalog.ts +462 -0
  179. package/extensions/lib/model-choice-warnings.ts +137 -0
  180. package/extensions/lib/model-pairings.ts +152 -0
  181. package/extensions/{auto-mode → lib}/model-picker.ts +26 -43
  182. package/extensions/lib/model-policy.ts +112 -15
  183. package/extensions/lib/model-tier.ts +283 -355
  184. package/extensions/lib/model-unusable.ts +1 -1
  185. package/extensions/lib/newer-model.ts +100 -0
  186. package/extensions/lib/notices.ts +87 -0
  187. package/extensions/lib/notifications.ts +308 -64
  188. package/extensions/lib/one-code-settings.ts +74 -8
  189. package/extensions/lib/open-browser.ts +63 -0
  190. package/extensions/lib/openrouter-generation.ts +94 -0
  191. package/extensions/lib/operation-deadline.ts +30 -0
  192. package/extensions/lib/original-command.ts +4 -4
  193. package/extensions/lib/package-version.ts +1 -1
  194. package/extensions/lib/panel-focus.ts +134 -0
  195. package/extensions/lib/paths.ts +17 -2
  196. package/extensions/lib/permission-gate.ts +72 -6
  197. package/extensions/lib/persisted-output.ts +60 -8
  198. package/extensions/lib/pi-ai-estimate.ts +19 -3
  199. package/extensions/lib/pi-version.ts +21 -2
  200. package/extensions/lib/plan-mode-channels.ts +20 -5
  201. package/extensions/lib/plugins.ts +26 -17
  202. package/extensions/lib/powershell-parse-script.ts +208 -0
  203. package/extensions/lib/powershell-parser.ts +344 -0
  204. package/extensions/lib/process-tree.ts +131 -8
  205. package/extensions/lib/prompt-options.ts +82 -0
  206. package/extensions/lib/queued-delivery.ts +44 -0
  207. package/extensions/lib/reminders.ts +364 -58
  208. package/extensions/lib/replaced-builtins.mjs +199 -0
  209. package/extensions/lib/replay-call.ts +70 -0
  210. package/extensions/lib/request-replay.ts +439 -0
  211. package/extensions/lib/scratchpad.ts +81 -14
  212. package/extensions/lib/send-now-channel.ts +7 -0
  213. package/extensions/lib/session-model-tier.ts +51 -0
  214. package/extensions/lib/session-work.ts +64 -0
  215. package/extensions/lib/settings-channels.ts +4 -4
  216. package/extensions/lib/shell-exit-semantics.ts +170 -0
  217. package/extensions/lib/shell-quote.ts +11 -0
  218. package/extensions/lib/shell-result.ts +70 -0
  219. package/extensions/lib/shell-spawn.ts +70 -6
  220. package/extensions/lib/shell-tool.ts +75 -25
  221. package/extensions/lib/side-call-cache.ts +124 -0
  222. package/extensions/lib/side-call-run.ts +96 -0
  223. package/extensions/lib/side-call-usage.ts +38 -0
  224. package/extensions/lib/side-call.ts +13 -1
  225. package/extensions/lib/single-flight.ts +18 -0
  226. package/extensions/lib/skill-body.ts +42 -0
  227. package/extensions/lib/skill-scan.ts +63 -24
  228. package/extensions/lib/spill-file.ts +140 -0
  229. package/extensions/lib/subagent-handback.ts +113 -0
  230. package/extensions/lib/system-role.ts +156 -0
  231. package/extensions/lib/terminal-text.ts +114 -0
  232. package/extensions/lib/text-width.ts +47 -16
  233. package/extensions/lib/timer-ops.ts +20 -0
  234. package/extensions/lib/tool-additions.ts +149 -0
  235. package/extensions/lib/tool-call-corruption.ts +134 -0
  236. package/extensions/lib/tool-path.ts +66 -0
  237. package/extensions/lib/tool-variants.ts +69 -0
  238. package/extensions/lib/tui-render.ts +255 -27
  239. package/extensions/lib/turn-budget-layout.ts +143 -0
  240. package/extensions/lib/update-check.mjs +15 -2
  241. package/extensions/lib/usage-bus.ts +1 -1
  242. package/extensions/lib/user-denial.ts +38 -0
  243. package/extensions/lib/workspace-channel.ts +16 -0
  244. package/extensions/lib/worktree-channel.ts +31 -3
  245. package/extensions/lib/worktree-isolation.ts +40 -10
  246. package/extensions/lsp/client.ts +8 -9
  247. package/extensions/lsp/index.ts +70 -4
  248. package/extensions/lsp/keep-alive.ts +1 -1
  249. package/extensions/lsp/trust.ts +119 -0
  250. package/extensions/lsp/watcher.ts +1 -1
  251. package/extensions/mcp/announce.ts +87 -5
  252. package/extensions/mcp/client.ts +161 -48
  253. package/extensions/mcp/config.ts +132 -8
  254. package/extensions/mcp/discovery.ts +19 -0
  255. package/extensions/mcp/index.ts +209 -71
  256. package/extensions/mcp/oauth/browser.ts +38 -17
  257. package/extensions/mcp/oauth/flow.ts +25 -17
  258. package/extensions/mcp/oauth/provider.ts +14 -8
  259. package/extensions/mcp/oauth/store.ts +34 -16
  260. package/extensions/mcp/panel/keys.ts +12 -11
  261. package/extensions/mcp/panel/render.ts +9 -6
  262. package/extensions/mcp/schema.ts +104 -5
  263. package/extensions/mcp/trust.ts +151 -67
  264. package/extensions/memory/entries.ts +23 -14
  265. package/extensions/memory/index.ts +134 -28
  266. package/extensions/memory/open-external.ts +33 -18
  267. package/extensions/memory/panel.ts +135 -26
  268. package/extensions/newer-model/index.ts +31 -0
  269. package/extensions/notebook/description.ts +18 -0
  270. package/extensions/notebook/index.ts +6 -5
  271. package/extensions/notebook/notebook.ts +1 -1
  272. package/extensions/permissions/index.ts +1300 -381
  273. package/extensions/permissions/matcher.ts +425 -74
  274. package/extensions/permissions/modes.ts +6 -4
  275. package/extensions/permissions/panel/host.ts +130 -0
  276. package/extensions/permissions/panel/keys.ts +48 -0
  277. package/extensions/permissions/panel/render.ts +450 -0
  278. package/extensions/permissions/panel/state.ts +550 -0
  279. package/extensions/permissions/powershell-cmdlets.ts +198 -0
  280. package/extensions/permissions/powershell-paths.ts +197 -0
  281. package/extensions/permissions/powershell-rules.ts +207 -280
  282. package/extensions/permissions/powershell-tree.ts +557 -0
  283. package/extensions/permissions/project-trust.ts +27 -11
  284. package/extensions/permissions/protected-paths.ts +14 -2
  285. package/extensions/permissions/session-grant.ts +6 -0
  286. package/extensions/permissions/settings.ts +166 -31
  287. package/extensions/permissions/subagent-gate.ts +25 -0
  288. package/extensions/permissions/workspace.ts +78 -0
  289. package/extensions/plan-mode/index.ts +156 -69
  290. package/extensions/plan-mode/reminder.ts +89 -21
  291. package/extensions/plan-mode/texts.ts +177 -0
  292. package/extensions/plan-mode/viewer.ts +92 -27
  293. package/extensions/plugins/counts.ts +3 -3
  294. package/extensions/plugins/index.ts +61 -46
  295. package/extensions/plugins/install/install.ts +16 -0
  296. package/extensions/plugins/marketplace/sync.ts +9 -3
  297. package/extensions/plugins/marketplace/types.ts +3 -1
  298. package/extensions/plugins/panel/keys.ts +21 -24
  299. package/extensions/plugins/panel/render.ts +12 -6
  300. package/extensions/plugins/shell-expand.ts +107 -0
  301. package/extensions/plugins/template.ts +32 -15
  302. package/extensions/powershell/child.ts +7 -0
  303. package/extensions/powershell/description.ts +38 -6
  304. package/extensions/powershell/guards.ts +1 -1
  305. package/extensions/powershell/index.ts +12 -7
  306. package/extensions/recap/index.ts +65 -60
  307. package/extensions/recap/prompt.ts +2 -2
  308. package/extensions/recap/scheduler.ts +3 -5
  309. package/extensions/search-tools/grep-merge.ts +39 -0
  310. package/extensions/search-tools/index.ts +0 -0
  311. package/extensions/send-now/chord.ts +62 -0
  312. package/extensions/send-now/index.ts +133 -0
  313. package/extensions/session-title/index.ts +3 -0
  314. package/extensions/session-title/title.ts +4 -1
  315. package/extensions/skill/code-review.ts +97 -0
  316. package/extensions/skill/index.ts +421 -101
  317. package/extensions/skill/invoke.ts +128 -18
  318. package/extensions/skill/listing.ts +133 -9
  319. package/extensions/skill/panel/keys.ts +15 -16
  320. package/extensions/skill/panel/render.ts +5 -2
  321. package/extensions/skill/panel/state.ts +12 -1
  322. package/extensions/subagents/agent-args.ts +39 -0
  323. package/extensions/subagents/agent-description.ts +132 -0
  324. package/extensions/subagents/agents.ts +116 -23
  325. package/extensions/subagents/announce.ts +69 -0
  326. package/extensions/subagents/default-model.ts +2 -3
  327. package/extensions/subagents/delegation-steer.ts +4 -4
  328. package/extensions/subagents/fork-cache.ts +76 -0
  329. package/extensions/subagents/guide-agent.ts +189 -0
  330. package/extensions/subagents/hand-back-review.ts +3 -0
  331. package/extensions/subagents/index.ts +1493 -824
  332. package/extensions/subagents/live-runs.ts +26 -6
  333. package/extensions/subagents/model-select.ts +38 -25
  334. package/extensions/subagents/outcome.ts +2 -2
  335. package/extensions/subagents/panel-keys.ts +73 -31
  336. package/extensions/subagents/panel-render.ts +120 -96
  337. package/extensions/subagents/panel-widget.ts +60 -89
  338. package/extensions/subagents/runner.ts +108 -80
  339. package/extensions/subagents/runs.ts +39 -5
  340. package/extensions/subagents/send-to-main-tool.ts +1 -1
  341. package/extensions/subagents/session-turns.ts +32 -8
  342. package/extensions/subagents/shell-panel.ts +36 -178
  343. package/extensions/subagents/tasks-dialog.ts +170 -0
  344. package/extensions/subagents/viewport-mirror.ts +95 -0
  345. package/extensions/subagents/worktree.ts +57 -13
  346. package/extensions/system-prompt/environment.ts +19 -19
  347. package/extensions/system-prompt/idle-turn.ts +65 -0
  348. package/extensions/system-prompt/index.ts +131 -47
  349. package/extensions/system-prompt/template.ts +24 -50
  350. package/extensions/system-prompt/tiers/common.ts +23 -64
  351. package/extensions/system-prompt/tiers/frontier.ts +32 -11
  352. package/extensions/system-prompt/tiers/low.ts +67 -45
  353. package/extensions/system-prompt/tiers/mid.ts +96 -38
  354. package/extensions/system-reminder/index.ts +175 -13
  355. package/extensions/tasks/descriptions.ts +190 -0
  356. package/extensions/tasks/index.ts +120 -41
  357. package/extensions/tasks/store.ts +83 -16
  358. package/extensions/tool-call-corruption/child.ts +7 -0
  359. package/extensions/tool-call-corruption/index.ts +237 -0
  360. package/extensions/tool-search/announce.ts +16 -0
  361. package/extensions/tool-search/index.ts +189 -41
  362. package/extensions/tool-style/index.ts +68 -10
  363. package/extensions/tool-style/write-description.ts +40 -0
  364. package/extensions/tool-style/write-render.ts +237 -0
  365. package/extensions/turn-duration/index.ts +7 -3
  366. package/extensions/turn-duration/line.ts +47 -4
  367. package/extensions/web/backends.ts +6 -9
  368. package/extensions/web/description.ts +50 -0
  369. package/extensions/web/index.ts +118 -53
  370. package/extensions/web/native-call.ts +15 -0
  371. package/extensions/web/native-results.ts +41 -0
  372. package/extensions/web-fetch/description.ts +42 -0
  373. package/extensions/web-fetch/extract.ts +53 -6
  374. package/extensions/web-fetch/index.ts +125 -76
  375. package/extensions/web-fetch/page-content.ts +19 -0
  376. package/extensions/web-fetch/response.ts +49 -0
  377. package/extensions/web-fetch/summarize.ts +9 -10
  378. package/extensions/workflow/agent-session.ts +105 -18
  379. package/extensions/workflow/args.ts +24 -0
  380. package/extensions/workflow/description.ts +67 -0
  381. package/extensions/workflow/globals.ts +17 -3
  382. package/extensions/workflow/index.ts +145 -162
  383. package/extensions/workflow/records.ts +1 -0
  384. package/extensions/workflow/run-manager.ts +29 -6
  385. package/extensions/workflow/saved-workflows.ts +3 -2
  386. package/extensions/workflow/types.ts +22 -1
  387. package/extensions/workflow/viewer.ts +54 -24
  388. package/extensions/workflow/widget.ts +20 -0
  389. package/extensions/worktree/descriptions.ts +91 -0
  390. package/extensions/worktree/guards.ts +316 -54
  391. package/extensions/worktree/index.ts +121 -39
  392. package/extensions/worktree/powershell-guards.ts +167 -0
  393. package/extensions/worktree/rewrite.ts +14 -16
  394. package/package.json +18 -7
  395. package/skills/artifact-design/SKILL.md +85 -0
  396. package/skills/code-review/SKILL.md +20 -20
  397. package/skills/code-review/cells/arguments-and-flags.md +19 -0
  398. package/skills/code-review/cells/high-3x5-recall-verify.md +137 -0
  399. package/skills/code-review/cells/high-8-inline.md +122 -0
  400. package/skills/code-review/cells/low-1-pass-max4.md +27 -0
  401. package/skills/code-review/cells/low-1-pass-min-files-4.md +28 -0
  402. package/skills/code-review/cells/max-5x5-verify-sweep.md +162 -0
  403. package/skills/code-review/cells/medium-3x5-verify.md +132 -0
  404. package/skills/code-review/cells/minimal-prompt.md +32 -0
  405. package/skills/code-review/cells/xhigh-10-inline-sweep.md +149 -0
  406. package/skills/code-review/cells/xhigh-5x5-verify-sweep.md +162 -0
  407. package/skills/fewer-permission-prompts/SKILL.md +1 -1
  408. package/skills/loop/SKILL.md +11 -0
  409. package/skills/security-review/SKILL.md +2 -5
  410. package/skills/simplify/SKILL.md +7 -5
  411. package/skills/workflow-authoring/SKILL.md +143 -0
  412. package/themes/onecode-light.json +1 -1
  413. package/themes/onecode.json +1 -1
  414. package/types/app.d.ts +58 -0
  415. package/types/community-packages.d.ts +26 -1
  416. package/extensions/auto-mode/recoverability.ts +0 -229
  417. package/extensions/lib/capability-index.ts +0 -466
  418. package/extensions/lib/model-facts.generated.json +0 -2675
  419. package/extensions/lib/model-facts.ts +0 -162
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Cross-extension "a setting you own just changed on disk" channels (pure
3
3
  * constants). jiti isolates module state, so when one extension writes another
4
- * extension's setting — `/doctor preset` writing the subagent default and the
5
- * auto-mode classifier model — it announces the write here and the owner
6
- * re-reads its file and republishes its live status. No payload: the owner's
7
- * own loader is the source of truth.
4
+ * extension's setting — `/doctor preset` writing the subagent default and
5
+ * clearing the auto-mode classifier model — it announces the write here and the
6
+ * owner re-reads its file and republishes its live status. No payload: the
7
+ * owner's own loader is the source of truth.
8
8
  */
9
9
 
10
10
  /** `subagentModel` in ~/.onecode/settings.json changed; the subagents extension re-resolves and re-emits its status. */
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Claude Code's semantic exit codes (pure): a nonzero exit that is not an
3
+ * error, such as grep's "no matches" or diff's "files differ", which Claude
4
+ * Code reports as plain output with no `Exit code N` line. Bash and
5
+ * PowerShell keep separate tables and separate last-command rules.
6
+ */
7
+ import { parseBash } from "./bash-parser.ts";
8
+
9
+ type BashNode = NonNullable<ReturnType<typeof parseBash>>["rootNode"];
10
+ const CONTAINERS = new Set(["program", "list", "pipeline"]);
11
+ const OPERATORS = new Set(["&&", "||", "|", ";", "&", "|&", "\n"]);
12
+ const EXIT_ONE_SUCCESSES = new Set(["grep", "rg", "egrep", "fgrep", "find", "diff", "test", "["]);
13
+
14
+ /**
15
+ * The rightmost command's text, or undefined when an `&&` precedes it: a failed
16
+ * earlier step can explain the exit, not the final search. Redirects and
17
+ * comments are skipped, as Claude Code's last-command walk does.
18
+ */
19
+ function lastCommand(root: BashNode): string | undefined {
20
+ let node: BashNode | undefined = root;
21
+ while (node && (CONTAINERS.has(node.type) || node.type === "redirected_statement")) {
22
+ const children: BashNode[] = node.children;
23
+ const last = children.findLastIndex((child) => !OPERATORS.has(child.type) && child.type !== "comment" && !child.type.endsWith("_redirect"));
24
+ if (node.type === "list" && children[last - 1]?.type === "&&") return undefined;
25
+ node = children[last];
26
+ }
27
+ return node?.text;
28
+ }
29
+
30
+ /** git's global options that take their value as the next word (`--git-dir=x` is one word). */
31
+ const GIT_VALUE_OPTIONS = new Set(["-C", "-c", "--git-dir", "--work-tree", "--namespace", "--super-prefix", "--config-env", "--attr-source"]);
32
+
33
+ /** git's subcommand among the words after `git`: the first that is no option or an option's value. */
34
+ function gitSubcommandOf(args: readonly string[]): string | undefined {
35
+ for (let i = 0; i < args.length; i++) {
36
+ if (!args[i].startsWith("-")) return args[i];
37
+ if (GIT_VALUE_OPTIONS.has(args[i])) i++;
38
+ }
39
+ return undefined;
40
+ }
41
+
42
+ /** Bash: exit 1 from a final grep, rg, egrep, fgrep, find, diff, test, `[` or git grep/diff. */
43
+ export function bashExitOneIsSuccess(command: string): boolean {
44
+ if (command.length > 10_000) return false;
45
+ const tree = parseBash(command);
46
+ if (!tree) return false;
47
+ try {
48
+ if (tree.rootNode.type !== "program") return false;
49
+ const last = lastCommand(tree.rootNode);
50
+ if (last === undefined) return false;
51
+ // Claude Code matches the literal first word: no unwrapping of env, assignments or quotes.
52
+ const [head, ...args] = last.trim().split(/\s+/);
53
+ if (head !== "git") return EXIT_ONE_SUCCESSES.has(head);
54
+ const sub = gitSubcommandOf(args);
55
+ return sub === "grep" || sub === "diff";
56
+ } finally {
57
+ tree.delete();
58
+ }
59
+ }
60
+
61
+ // PowerShell. The last segment splits on `;`, `|`, newlines, `&&` and a
62
+ // spaced `&`, outside quotes and comments; `&&` before it means an earlier
63
+ // step can explain the exit.
64
+ type Segment = { text: string; andGated: boolean };
65
+
66
+ function lastSegment(command: string): Segment {
67
+ const segments: Segment[] = [];
68
+ let start = 0;
69
+ let andGated = false;
70
+ let inSingle = false;
71
+ let inDouble = false;
72
+ for (let i = 0; i < command.length; i++) {
73
+ const c = command[i];
74
+ if (inSingle) {
75
+ if (c === "'") inSingle = false;
76
+ continue;
77
+ }
78
+ if (inDouble) {
79
+ if (c === "`") i++;
80
+ else if (c === '"') inDouble = false;
81
+ continue;
82
+ }
83
+ if (c === "#" && (i === 0 || " \t\n\r".includes(command[i - 1]))) {
84
+ segments.push({ text: command.slice(start, i), andGated });
85
+ while (i + 1 < command.length && command[i + 1] !== "\n" && command[i + 1] !== "\r") i++;
86
+ start = i + 1;
87
+ continue;
88
+ }
89
+ if (c === "'") {
90
+ inSingle = true;
91
+ continue;
92
+ }
93
+ if (c === '"') {
94
+ inDouble = true;
95
+ continue;
96
+ }
97
+ if (c === ";" || c === "|" || c === "\n" || c === "\r") {
98
+ segments.push({ text: command.slice(start, i), andGated });
99
+ start = i + 1;
100
+ if (c !== "|") andGated = false;
101
+ continue;
102
+ }
103
+ if (c === "&") {
104
+ if (command[i + 1] === "&") {
105
+ segments.push({ text: command.slice(start, i), andGated });
106
+ i++;
107
+ start = i + 1;
108
+ andGated = true;
109
+ continue;
110
+ }
111
+ const before = command[i - 1];
112
+ if ((before === " " || before === "\t") && command.slice(start, i).trim() !== "") {
113
+ segments.push({ text: command.slice(start, i), andGated });
114
+ start = i + 1;
115
+ andGated = false;
116
+ }
117
+ }
118
+ }
119
+ segments.push({ text: command.slice(start), andGated });
120
+ return segments.findLast((segment) => segment.text.trim()) ?? { text: command, andGated: false };
121
+ }
122
+
123
+ /** A segment's first word, unquoted, and the text after it; a leading `&` or `.` call operator is dropped. */
124
+ function splitFirstWord(segment: string): { word: string; rest: string } {
125
+ const line = segment.trim().replace(/^[&.]\s+/, "");
126
+ const quoted = /^"([^"]*)"|^'([^']*)'/.exec(line);
127
+ if (quoted) return { word: quoted[1] ?? quoted[2], rest: line.slice(quoted[0].length) };
128
+ const raw = line.split(/\s+/)[0] ?? "";
129
+ return { word: raw.replace(/^["']|["']$/g, ""), rest: line.slice(raw.length) };
130
+ }
131
+
132
+ /** The command's file name, lowercased, without a native .exe/.cmd/.bat extension. */
133
+ function baseCommand(word: string): { base: string; nativeExt: string | undefined } {
134
+ const file = (word.split(/[\\/]/).pop() || word).toLowerCase();
135
+ const nativeExt = ["exe", "cmd", "bat"].find((ext) => file.endsWith(`.${ext}`));
136
+ return { base: nativeExt ? file.slice(0, -(nativeExt.length + 1)) : file, nativeExt };
137
+ }
138
+
139
+ const exitOneOk = (code: number) => code === 0 || code === 1;
140
+ const POWERSHELL_SEMANTICS = new Map<string, (code: number) => boolean>([
141
+ ["grep", exitOneOk],
142
+ ["rg", exitOneOk],
143
+ ["egrep", exitOneOk],
144
+ ["fgrep", exitOneOk],
145
+ ["findstr", exitOneOk],
146
+ ["robocopy", (code) => code >= 0 && code < 8],
147
+ ]);
148
+ // Only as `<name>.exe` with output: bare `where` is Where-Object, bare `fc` Format-Custom.
149
+ const POWERSHELL_EXE_WITH_OUTPUT = new Map<string, (code: number) => boolean>([
150
+ ["where", exitOneOk],
151
+ ["fc", exitOneOk],
152
+ ["diff", exitOneOk],
153
+ ]);
154
+ const NOT_RECOGNIZED = /is not recognized as (a name of a cmdlet|the name of a cmdlet|an? internal)/i;
155
+
156
+ /** PowerShell: a nonzero exit the final command's table calls success. `output` is stdout and stderr merged. */
157
+ export function powershellExitIsSuccess(command: string, exitCode: number, output: string): boolean {
158
+ if (exitCode === 0) return true;
159
+ const { text, andGated } = lastSegment(command);
160
+ const { word, rest } = splitFirstWord(text);
161
+ const { base, nativeExt } = baseCommand(word);
162
+ const sub = base === "git" ? gitSubcommandOf(rest.trim().split(/\s+/).filter(Boolean)) : undefined;
163
+ const ok =
164
+ sub === "grep" || sub === "diff"
165
+ ? exitOneOk
166
+ : ((nativeExt === "exe" && output.trim() !== "" ? POWERSHELL_EXE_WITH_OUTPUT.get(base) : undefined) ?? POWERSHELL_SEMANTICS.get(base));
167
+ if (!ok?.(exitCode)) return false;
168
+ // A command that never ran, or findstr's own error, is a failure after all.
169
+ return !(andGated || NOT_RECOGNIZED.test(output) || /CommandNotFoundException/.test(output) || /^FINDSTR: /m.test(output));
170
+ }
@@ -0,0 +1,11 @@
1
+ /** Quoting one word for a shell command line. Pure. */
2
+
3
+ /** A POSIX shell single-quoted word: the text exactly, whatever it holds. */
4
+ export function shellQuote(word: string): string {
5
+ return `'${word.replace(/'/g, "'\\''")}'`;
6
+ }
7
+
8
+ /** PowerShell single-quoted literal: only `'` needs escaping, as `''`. */
9
+ export function powershellQuote(word: string): string {
10
+ return `'${word.replace(/'/g, "''")}'`;
11
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Claude Code's shell result text, made from pi's (pure). pi ends a failing
3
+ * command's output with `\n\nCommand exited with code N` and keeps the
4
+ * output's trailing newline; Claude Code starts a failure with `Exit code N`
5
+ * on its own line and trims the output's leading blank lines and trailing
6
+ * whitespace, and an empty success reads `(<tool> completed with no output)`.
7
+ * A nonzero exit Claude Code's semantics call success (grep's no match, a
8
+ * diff that found differences) reads as a success. pi's truncation notes
9
+ * stay where they are, at the end of the output.
10
+ */
11
+
12
+ import { emptyToolResultText } from "./empty-tool-result.ts";
13
+ import { bashExitOneIsSuccess, powershellExitIsSuccess } from "./shell-exit-semantics.ts";
14
+
15
+ type Block = { type: string; text?: string };
16
+ type ShellResult = { content?: Block[]; structuredContent?: unknown; isError?: boolean };
17
+
18
+ /** pi's text for an empty output. */
19
+ const PI_NO_OUTPUT = "(no output)";
20
+
21
+ function successText(output: string, toolName: string): string {
22
+ if (output === PI_NO_OUTPUT) return emptyToolResultText(toolName);
23
+ return output.replace(/^(\s*\n)+/, "").trimEnd() || emptyToolResultText(toolName);
24
+ }
25
+
26
+ /** The output before pi's `Command exited with code N` trailer, or undefined when the text is not pi's failure shape. */
27
+ function failureOutput(text: string, exitCode: number): string | undefined {
28
+ const status = `Command exited with code ${exitCode}`;
29
+ if (text !== status && !text.endsWith(`\n\n${status}`)) return undefined;
30
+ return text.slice(0, Math.max(0, text.length - status.length - 2));
31
+ }
32
+
33
+ /** Claude Code's failure text: `Exit code N`, then the output on its own lines. */
34
+ function failureText(output: string, exitCode: number): string {
35
+ return (output === PI_NO_OUTPUT || output === "" ? `Exit code ${exitCode}` : `Exit code ${exitCode}\n${output}`).trimEnd();
36
+ }
37
+
38
+ /** Claude Code's text for one foreground result, or undefined to keep pi's. */
39
+ export function claudeCodeShellText(text: string, exitCode: unknown, toolName: string): string | undefined {
40
+ if (typeof exitCode !== "number") return undefined;
41
+ if (exitCode === 0) return successText(text, toolName);
42
+ const output = failureOutput(text, exitCode);
43
+ return output === undefined ? undefined : failureText(output, exitCode);
44
+ }
45
+
46
+ function semanticSuccess(toolName: string, command: string, exitCode: number, output: string): boolean {
47
+ if (toolName === "bash") return exitCode === 1 && bashExitOneIsSuccess(command);
48
+ if (toolName === "powershell") return powershellExitIsSuccess(command, exitCode, output === PI_NO_OUTPUT ? "" : output);
49
+ return false;
50
+ }
51
+
52
+ /** A foreground result with its first text block in Claude Code's words; other results pass through. */
53
+ export function withClaudeCodeShellText<R extends ShellResult>(result: R, toolName: string, command: string): R {
54
+ const first = result?.content?.[0];
55
+ if (first?.type !== "text" || typeof first.text !== "string") return result;
56
+ const exitCode = (result.structuredContent as { exit_code?: unknown } | undefined)?.exit_code;
57
+ const rest = (result.content ?? []).slice(1);
58
+ let text: string | undefined;
59
+ if (typeof exitCode === "number" && exitCode !== 0) {
60
+ const output = failureOutput(first.text, exitCode);
61
+ if (output !== undefined && semanticSuccess(toolName, command, exitCode, output)) {
62
+ return { ...result, content: [{ ...first, text: successText(output, toolName) }, ...rest], isError: false };
63
+ }
64
+ text = output === undefined ? undefined : failureText(output, exitCode);
65
+ } else {
66
+ text = claudeCodeShellText(first.text, exitCode, toolName);
67
+ }
68
+ if (text === undefined || text === first.text) return result;
69
+ return { ...result, content: [{ ...first, text }, ...rest] };
70
+ }
@@ -2,7 +2,7 @@
2
2
  * Which shell One Code spawns, and how — the one place that knows about Git
3
3
  * Bash on Windows and about PowerShell.
4
4
  *
5
- * Claude Code's shape (findings §22, docs/decisions/windows.md): Git for
5
+ * Claude Code's shape (findings §22, working-docs/decisions/windows.md): Git for
6
6
  * Windows is optional. When a bash is available it drives the `bash` tool,
7
7
  * hooks and background shells; `CLAUDE_CODE_GIT_BASH_PATH` (process env, or
8
8
  * Claude Code's user-settings `env` block) names a custom bash and is ignored
@@ -29,9 +29,11 @@ import { type ChildProcess, spawn, type SpawnOptions } from "node:child_process"
29
29
  import { constants, existsSync } from "node:fs";
30
30
  import { access } from "node:fs/promises";
31
31
  import { homedir } from "node:os";
32
- import { basename } from "node:path";
33
- import { getPowerShellConfig, getShellConfig } from "@earendil-works/pi-coding-agent";
32
+ import { basename, delimiter, join } from "node:path";
33
+ import { getAgentDir, getPowerShellConfig, getShellConfig } from "@earendil-works/pi-coding-agent";
34
+ import { childProcessEnv } from "./app-launch.mjs";
34
35
  import { readSettingsEnv } from "./claude-settings.ts";
36
+ import { PowerShellParser, sharedPowerShellParser } from "./powershell-parser.ts";
35
37
  import { detachedSpawnOptions, killProcessTree, waitForChildExit } from "./process-tree.ts";
36
38
  import { whichOnPath } from "./which.ts";
37
39
 
@@ -52,6 +54,20 @@ export const POWERSHELL_ARGS = ["-NoProfile", "-NonInteractive", "-ExecutionPoli
52
54
  /** pi's UTF-8 output prefix for PowerShell (core/tools/powershell.ts), so Windows PowerShell 5.1 does not emit UTF-16. */
53
55
  export const POWERSHELL_UTF8_PREFIX = "try { [Console]::OutputEncoding=[System.Text.Encoding]::UTF8 } catch {}\n";
54
56
 
57
+ /**
58
+ * What the PowerShell tool prepends to every command, foreground and
59
+ * background: pi's UTF-8 line, then UTF-8 for `Out-File` and `$OutputEncoding`
60
+ * and plain-text rendering, as Claude Code's PowerShell tool runs commands.
61
+ * `$PSStyle.OutputRendering = 'PlainText'` keeps pwsh 7's formatter from
62
+ * colouring table headers, which otherwise reached the model as raw
63
+ * `\x1b[32;1m` escapes.
64
+ */
65
+ export const POWERSHELL_TOOL_PREFIX =
66
+ // One line, so an error's "At line:N" stays one off the command's own line, as with pi's prefix alone.
67
+ `${POWERSHELL_UTF8_PREFIX.trimEnd()}; try { $PSDefaultParameterValues['Out-File:Encoding'] = 'utf8' } catch {}; ` +
68
+ "if ($ExecutionContext.SessionState.LanguageMode -eq 'FullLanguage') { try { $OutputEncoding = [System.Text.UTF8Encoding]::new() } catch {}; " +
69
+ "if ($null -ne $PSStyle) { try { $PSStyle.OutputRendering = 'PlainText' } catch {} } };\n";
70
+
55
71
  export const GIT_BASH_PATH_VAR = "CLAUDE_CODE_GIT_BASH_PATH";
56
72
 
57
73
  // ---------------------------------------------------------------------------
@@ -187,6 +203,27 @@ export function powerShellSpawn(): ShellSpawn | undefined {
187
203
  return cachedPowerShell.spawn;
188
204
  }
189
205
 
206
+ /**
207
+ * PowerShell's own parser, served by the same executable and arguments the
208
+ * PowerShell tool runs (lib/powershell-parser.ts), one server per process.
209
+ * Undefined when no PowerShell resolves. It resolves command names on the
210
+ * foreground tool's PATH, pi's managed-tools directory first (piShellEnv):
211
+ * a `findstr.exe` there shadows Windows' own where the command runs. The
212
+ * background spawn's PATH lacks that directory, so a name resolved to
213
+ * System32 here resolves there too.
214
+ */
215
+ export function powerShellParser(spec: ShellSpawn | undefined = powerShellSpawn()): PowerShellParser | undefined {
216
+ if (!spec) return undefined;
217
+ return sharedPowerShellParser(
218
+ spec.shell,
219
+ () =>
220
+ new PowerShellParser({
221
+ spawnServer: (bootstrap) =>
222
+ spawnShellCommand(spec, bootstrap, { env: piShellEnv(join(getAgentDir(), "bin")), stdio: ["pipe", "pipe", "pipe"] }),
223
+ }),
224
+ );
225
+ }
226
+
190
227
  /** Whether the resolved PowerShell is Windows PowerShell 5.1 (`powershell.exe`) or PowerShell 7+ (`pwsh`). */
191
228
  export function powerShellEdition(spawnSpec: ShellSpawn | undefined): "core" | "desktop" | "unknown" {
192
229
  if (!spawnSpec) return "unknown";
@@ -204,16 +241,20 @@ export function powerShellEdition(spawnSpec: ShellSpawn | undefined): "core" | "
204
241
  * shell's arguments (`bash -c <cmd>`, `pwsh … -Command <cmd>`); stdin transport
205
242
  * writes it to the child's stdin (pi's legacy-WSL bash), which needs stdin
206
243
  * piped — callers that must feed their own stdin (hooks) reject that form.
244
+ * The child's environment is `options.env` (default `process.env`) with the
245
+ * bundled app's launcher variables put back to the user's own values
246
+ * (app-launch.mjs childProcessEnv), so a `pi` run from a command is the user's pi.
207
247
  */
208
248
  export function spawnShellCommand(spec: ShellSpawn, command: string, options: SpawnOptions): ChildProcess {
249
+ const env = childProcessEnv(options.env ?? process.env);
209
250
  if (spec.commandTransport === "stdin") {
210
251
  const stdio = Array.isArray(options.stdio) ? ["pipe", ...options.stdio.slice(1)] : ["pipe", "pipe", "pipe"];
211
- const child = spawn(spec.shell, spec.args, { ...options, stdio: stdio as SpawnOptions["stdio"], windowsHide: true });
252
+ const child = spawn(spec.shell, spec.args, { ...options, env, stdio: stdio as SpawnOptions["stdio"], windowsHide: true });
212
253
  child.stdin?.on("error", () => {});
213
254
  child.stdin?.end(command);
214
255
  return child;
215
256
  }
216
- return spawn(spec.shell, [...spec.args, command], { ...options, windowsHide: true });
257
+ return spawn(spec.shell, [...spec.args, command], { ...options, env, windowsHide: true });
217
258
  }
218
259
 
219
260
  // ---------------------------------------------------------------------------
@@ -228,6 +269,29 @@ export interface ShellOperations {
228
269
  ) => Promise<{ exitCode: number | null }>;
229
270
  }
230
271
 
272
+ /**
273
+ * pi's shell environment (utils/shell.ts `getShellEnv`, not exported): `env`
274
+ * with pi's managed-tools directory (`<agentDir>/bin`) first on PATH, the PATH
275
+ * key found in any case.
276
+ */
277
+ export function piShellEnv(binDir: string, env: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
278
+ const pathKey = Object.keys(env).find((key) => key.toLowerCase() === "path") ?? "PATH";
279
+ const current = env[pathKey] ?? "";
280
+ if (current.split(delimiter).includes(binDir)) return { ...env };
281
+ return { ...env, [pathKey]: [binDir, current].filter(Boolean).join(delimiter) };
282
+ }
283
+
284
+ /**
285
+ * `operations` with every command's environment passed through
286
+ * childProcessEnv; `defaultEnv` stands in when the caller passes none (pi's
287
+ * user `!` commands never do, and its local operations then use its shell env).
288
+ */
289
+ export function withChildProcessEnv(operations: ShellOperations, defaultEnv: () => NodeJS.ProcessEnv): ShellOperations {
290
+ return {
291
+ exec: (command, cwd, options) => operations.exec(command, cwd, { ...options, env: childProcessEnv(options.env ?? defaultEnv()) }),
292
+ };
293
+ }
294
+
231
295
  const MAX_TIMEOUT_MS = 2_147_483_647;
232
296
 
233
297
  /** pi's timeout validation (seconds → ms), same messages. */
@@ -263,7 +327,7 @@ export function createPowerShellOperations(resolve: () => ShellSpawn | undefined
263
327
  } catch {
264
328
  throw new Error(`Working directory does not exist: ${cwd}\nCannot execute PowerShell commands.`);
265
329
  }
266
- const child = spawnShellCommand(spec, `${POWERSHELL_UTF8_PREFIX}${command}`, {
330
+ const child = spawnShellCommand(spec, `${POWERSHELL_TOOL_PREFIX}${command}`, {
267
331
  cwd,
268
332
  ...detachedSpawnOptions(),
269
333
  env: env ?? process.env,
@@ -13,24 +13,34 @@
13
13
 
14
14
  import { mkdirSync } from "node:fs";
15
15
  import { join } from "node:path";
16
- import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
16
+ import type { ExtensionAPI, ExtensionContext, ExtensionToolContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
17
17
  import type { Static, TObject } from "typebox";
18
18
  import { generateTaskId, TASK_REGISTER_CHANNEL } from "../background/registry.ts";
19
19
  import { type BashFinishSummary, runBackgroundBashBlocking, startBackgroundBash, tailCap } from "../bash/background.ts";
20
20
  import { createTaskNotifier, oneShotNote, sessionOutlivesTurn, shellSummary, type TaskStatus, taskNotification } from "./notifications.ts";
21
21
  import { commandToEvaluate, trackOriginalCommands } from "./original-command.ts";
22
- import { persistIfLarge, sessionResultsDir } from "./persisted-output.ts";
22
+ import { persistedFilePreview, persistIfLarge, sessionResultsDir } from "./persisted-output.ts";
23
+ import { withClaudeCodeShellText } from "./shell-result.ts";
24
+ import { bashParserReady } from "./bash-parser.ts";
23
25
  import type { ShellSpawn } from "./shell-spawn.ts";
26
+ import { keepSpillReadable } from "./spill-file.ts";
24
27
  import { ccWrapBuiltinRenderers, linesComponent, resultLines } from "./tui-render.ts";
28
+ import { registerVariantTool } from "./tool-variants.ts";
25
29
 
26
30
  // The completion notification carries status + exit code + where the output is,
27
31
  // plus only a short tail: a finished build used to push 30 KB (~8k tokens) into
28
32
  // context unasked (review T7). The full tail stays behind task_output / the log.
29
33
  export const NOTIFY_OUTPUT_CAP = 2_000;
30
- /** One-shot modes return the whole output in the result; past this it is persisted (file + preview), never cut. */
31
- export const ONE_SHOT_OUTPUT_CAP = 30_000;
34
+ /** Claude Code's inline cap for a shell's output; past it the output is persisted (file + head preview), never cut. */
35
+ export const INLINE_OUTPUT_CAP = 30_000;
32
36
  /** Claude Code's cap; the tools' `timeout` is milliseconds, as CC's is. */
33
37
  export const MAX_TIMEOUT_MS = 600_000;
38
+ /**
39
+ * Claude Code's default for a command that names no `timeout`, which both
40
+ * tool descriptions promise. pi's executor has no default, so without this a
41
+ * foreground `npm run dev` ran until Esc, and forever in a `-p` run.
42
+ */
43
+ export const DEFAULT_TIMEOUT_MS = 120_000;
34
44
 
35
45
  /** The parameter shape both shell tools share (descriptions differ per tool). */
36
46
  export interface ShellToolParams {
@@ -53,7 +63,7 @@ export interface ShellToolBase {
53
63
 
54
64
  /** pi's shell tool executor: `execute(toolCallId, { command, timeout: seconds }, signal, onUpdate, ctx)`. */
55
65
  export interface ShellToolExecutor {
56
- execute: (toolCallId: string, params: { command: string; timeout?: number }, signal: AbortSignal | undefined, onUpdate: any, ctx: ExtensionContext) => Promise<any>;
66
+ execute: (toolCallId: string, params: { command: string; timeout?: number }, signal: AbortSignal | undefined, onUpdate: any, ctx: ExtensionToolContext) => Promise<any>;
57
67
  }
58
68
 
59
69
  /** What one shell tool's definition supplies; everything else is shared. */
@@ -98,10 +108,20 @@ export function finishLine(summary: BashFinishSummary, timeoutSeconds?: number):
98
108
  return `failed (${detail ?? `exit code ${summary.exitCode}`})`;
99
109
  }
100
110
 
111
+ /**
112
+ * Why a one-shot `run_in_background` call was stopped: a one-shot session
113
+ * cannot keep a command running past the run, so the call is held to the
114
+ * foreground deadline.
115
+ */
116
+ export function oneShotDeadlineNote(timeoutMs: number, explicit: boolean): string {
117
+ const which = explicit ? `its ${timeoutMs} ms timeout` : `the default ${timeoutMs} ms timeout (no \`timeout\` was given)`;
118
+ return `It was stopped at ${which}: a one-shot session cannot keep a background command running after the run ends, so a server or watcher started this way stops at the deadline. Pass a longer \`timeout\` (max ${MAX_TIMEOUT_MS}) for a long build.`;
119
+ }
120
+
101
121
  /** `<sessionDir>/bash/<taskId>/output.log` — one spool location for every shell tool (the shell panel reads it). */
102
122
  export function taskLogPath(ctx: ExtensionContext, taskId: string): string | undefined {
103
123
  try {
104
- const dir = join(ctx.sessionManager.getSessionDir(), "bash", taskId);
124
+ const dir = join(sessionResultsDir(ctx), "bash", taskId);
105
125
  mkdirSync(dir, { recursive: true });
106
126
  return join(dir, "output.log");
107
127
  } catch {
@@ -109,7 +129,13 @@ export function taskLogPath(ctx: ExtensionContext, taskId: string): string | und
109
129
  }
110
130
  }
111
131
 
112
- export function registerShellTool<P extends TObject>(pi: ExtensionAPI, spec: ShellToolSpec<P>): void {
132
+ /**
133
+ * Register the shell tool; the returned setter registers it again with a new
134
+ * description (a no-op when the text is unchanged), for a description that
135
+ * follows the session (lib/tool-variants.ts). Everything else — the notifier,
136
+ * the original-command tracker, execute — is built once.
137
+ */
138
+ export function registerShellTool<P extends TObject>(pi: ExtensionAPI, spec: ShellToolSpec<P>): (description: string) => void {
113
139
  // A completion whose output task_output just returned is withdrawn (CC's
114
140
  // delivered_as_tool_result) — the model already has it.
115
141
  const notifyTask = createTaskNotifier(pi, { withdrawOnDelivery: true });
@@ -124,7 +150,7 @@ export function registerShellTool<P extends TObject>(pi: ExtensionAPI, spec: She
124
150
  // "Took Ns" that counted permission-prompt wait disappears.
125
151
  const wrapped = ccWrapBuiltinRenderers<{ command?: string }>(spec.ccLabel, spec.base, { title: (a) => a?.command });
126
152
 
127
- pi.registerTool({
153
+ const definition = {
128
154
  name: spec.name,
129
155
  label: spec.base.label,
130
156
  description: spec.description,
@@ -162,14 +188,24 @@ export function registerShellTool<P extends TObject>(pi: ExtensionAPI, spec: She
162
188
  if (guardReason) return { content: [{ type: "text" as const, text: guardReason }], isError: true, details: {} };
163
189
  // pi's executor takes seconds; the model-facing unit is milliseconds
164
190
  // (Claude Code's), so a habitual `timeout: 120000` is 2 minutes, not
165
- // 33 hours (review T11).
166
- const timeoutSeconds =
167
- params.timeout !== undefined && Number.isFinite(params.timeout) && params.timeout > 0
168
- ? Math.min(params.timeout, MAX_TIMEOUT_MS) / 1000
169
- : undefined;
191
+ // 33 hours (review T11). A command that names none gets Claude Code's
192
+ // default, which is what the descriptions promise.
193
+ const explicitTimeout = params.timeout !== undefined && Number.isFinite(params.timeout) && params.timeout > 0;
194
+ const timeoutSeconds = (explicitTimeout ? Math.min(params.timeout!, MAX_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS) / 1000;
170
195
 
171
196
  if (!params.run_in_background) {
172
- return spec.foreground(ctx.cwd).execute(toolCallId, { command: params.command, timeout: timeoutSeconds }, signal, onUpdate, ctx);
197
+ // pi spills a long output to os.tmpdir(), outside every readable root;
198
+ // move it where the model's read of it is a working-space read, and
199
+ // cut the output Claude Code's way (its head, not pi's tail). Then
200
+ // Claude Code's result text (`Exit code N` first, trimmed output).
201
+ const result = await keepSpillReadable(
202
+ () => spec.foreground(ctx.cwd).execute(toolCallId, { command: params.command, timeout: timeoutSeconds }, signal, onUpdate, ctx),
203
+ sessionResultsDir(ctx),
204
+ { inlineMax: INLINE_OUTPUT_CAP, id: `${spec.name}-${toolCallId}` },
205
+ );
206
+ // The no-match check parses the command; only bash results need the grammar.
207
+ if (spec.name === "bash") await bashParserReady();
208
+ return withClaudeCodeShellText(result, spec.name, originalCommand);
173
209
  }
174
210
 
175
211
  const shell = spec.backgroundShell();
@@ -189,48 +225,61 @@ export function registerShellTool<P extends TObject>(pi: ExtensionAPI, spec: She
189
225
  // detached task would be orphaned, and its completion callback would
190
226
  // call sendMessage on the disposed session and crash pi (measured,
191
227
  // STEERING-REVIEW-2026-09-05 H3). Run blocking there — the same rule the
192
- // Agent tool applies — and return the output in the result.
228
+ // Agent tool applies — and return the output in the result. The call
229
+ // holds the turn like a foreground command, so it gets the foreground
230
+ // deadline (the model's `timeout`, else the default): with none, a
231
+ // server started this way hung the run forever. A detached shell below
232
+ // keeps having no deadline (findings §31).
193
233
  if (!sessionOutlivesTurn(ctx.mode)) {
194
234
  const summary = await runBackgroundBashBlocking({ id, command, description, cwd: ctx.cwd, timeoutSeconds, logPath, shell }, signal);
195
- const output = persistIfLarge(summary.output, { dir: sessionResultsDir(ctx), id: `${spec.name}-${id}`, maxBytes: ONE_SHOT_OUTPUT_CAP });
235
+ const spool = summary.logPath && Buffer.byteLength(summary.output, "utf8") > INLINE_OUTPUT_CAP
236
+ ? persistedFilePreview(summary.logPath)
237
+ : undefined;
238
+ const output = spool ?? persistIfLarge(summary.output, { dir: sessionResultsDir(ctx), id: `${spec.name}-${id}`, maxBytes: INLINE_OUTPUT_CAP });
239
+ const deadline = summary.timedOut ? ` ${oneShotDeadlineNote(timeoutSeconds * 1000, explicitTimeout)}` : "";
196
240
  return {
197
241
  content: [
198
242
  {
199
243
  type: "text" as const,
200
- text: `${spec.ccLabel} task ${id} (${description}) ${finishLine(summary, timeoutSeconds)}. ${oneShotNote("command")}${logPath ? ` Log: ${logPath}.` : ""}\n\n${output}`,
244
+ text: `${spec.ccLabel} task ${id} (${description}) ${finishLine(summary, timeoutSeconds)}. ${oneShotNote("command")}${deadline}${summary.logPath ? ` Log: ${summary.logPath}.` : ""}\n\n${output}`,
201
245
  },
202
246
  ],
203
247
  // No taskId: nothing was registered behind task_output/task_stop, and
204
248
  // its presence is what renderResult reads as "running in the background".
205
- details: { logPath },
206
- isError: summary.exitCode !== 0 && !summary.stopped,
249
+ details: { logPath: summary.logPath },
250
+ isError: shellFinish(summary, timeoutSeconds).status === "failed",
207
251
  };
208
252
  }
209
253
 
254
+ // No timeout here: Claude Code clears a command's timeout the
255
+ // moment it goes to the background, so a detached shell has no
256
+ // deadline (findings §31). Models habitually send `timeout: 600000`
257
+ // next to `run_in_background`, which used to kill a dev server at
258
+ // exactly 10 minutes. The one-shot path above keeps the timeout
259
+ // because there the command holds the turn.
210
260
  const task = startBackgroundBash({
211
261
  id,
212
262
  command,
213
263
  description,
214
264
  cwd: ctx.cwd,
215
- timeoutSeconds,
216
265
  logPath,
217
266
  shell,
218
267
  onFinished: (finishedTask, summary) => {
219
268
  // Claude Code's shell completion names the output file and nothing
220
269
  // else — the model reads it (or task_output) when it needs the text.
221
270
  // Without a spool file the tail rides `<result>` so nothing is lost.
222
- const { status, detail } = shellFinish(summary, timeoutSeconds);
271
+ const { status, detail } = shellFinish(summary);
223
272
  notify(
224
273
  taskNotification({
225
274
  kind: "shell",
226
275
  taskId: id,
227
276
  toolUseId: toolCallId,
228
- outputFile: logPath,
277
+ outputFile: finishedTask.logPath,
229
278
  status,
230
279
  summary: shellSummary(description, status, summary.exitCode, detail),
231
- result: logPath ? undefined : tailCap(summary.output, NOTIFY_OUTPUT_CAP).trim() || undefined,
280
+ result: finishedTask.logPath ? undefined : tailCap(summary.output, NOTIFY_OUTPUT_CAP).trim() || undefined,
232
281
  }),
233
- { taskId: id, status: finishedTask.status, exitCode: summary.exitCode, logPath },
282
+ { taskId: id, status: finishedTask.status, exitCode: summary.exitCode, logPath: finishedTask.logPath },
234
283
  );
235
284
  },
236
285
  });
@@ -248,5 +297,6 @@ export function registerShellTool<P extends TObject>(pi: ExtensionAPI, spec: She
248
297
  details: { taskId: id, logPath },
249
298
  };
250
299
  },
251
- } as ToolDefinition<P>);
300
+ } as ToolDefinition<P>;
301
+ return registerVariantTool(pi, spec.description, (description) => ({ ...definition, description }) as unknown as ToolDefinition);
252
302
  }