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
@@ -12,7 +12,7 @@
12
12
  * preamble, then every report line indented two spaces (a frame-like line at
13
13
  * column zero inside the report is thereby forged). Its completion is a
14
14
  * separate kind=agent `<task-notification>` whose `<result>` only points at
15
- * that message — CC's two-message split (docs/decisions/subagents-workflows.md).
15
+ * that message — CC's two-message split (working-docs/decisions/subagents-workflows.md).
16
16
  *
17
17
  * Wakeups are neither: CC re-invokes the session with the scheduled prompt
18
18
  * verbatim (findings §21), so `background/wakeup.ts` sends the prompt as-is.
@@ -25,13 +25,17 @@
25
25
  * applies it at dispatch (`frameForDelivery`), never the producers.
26
26
  *
27
27
  * Every literal below was read out of real CC transcripts and the shipped
28
- * 2.1.278 binary (docs/findings/24-claude-code-notifications.md) and is locked
28
+ * 2.1.278 binary (working-docs/findings/24-claude-code-notifications.md) and is locked
29
29
  * by test/unit/notification-fidelity.test.ts. The delivery engine
30
30
  * (`createTaskNotifier`, further down) is unchanged by the frame shape.
31
31
  */
32
+ import { AsyncLocalStorage } from "node:async_hooks";
33
+ import { randomUUID } from "node:crypto";
32
34
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
33
- import { RunOutcomeLatch } from "./interrupt.ts";
35
+ import { withKeepAlive } from "../lsp/keep-alive.ts";
36
+ import { ONE_SHOT_COMMAND_FAILED_CHANNEL, RunOutcomeLatch } from "./interrupt.ts";
34
37
  import { escapeXml } from "./local-command.ts";
38
+ import { isUserRun, PROMPT_OPTIONS_CHANNEL } from "./prompt-options.ts";
35
39
  import { wrapReminder } from "./reminders.ts";
36
40
 
37
41
  /** pi's session-mode union, derived from the context (the package root does not export ExtensionMode itself). */
@@ -154,7 +158,7 @@ export type HandBackVerdict = { kind: "blocked"; reason: string } | { kind: "una
154
158
  * provider body, a stack), and this is the choke point where it enters the
155
159
  * model-facing warning, so it must not go in unbounded. The clip is a bound on
156
160
  * a diagnostic clause, not on output the model works from, so it is not
157
- * persisted (docs/decisions/subagents-workflows.md, 2026-09-21); the
161
+ * persisted (working-docs/decisions/subagents-workflows.md, 2026-09-21); the
158
162
  * `unavailable` variant marks a clipped reason with an ellipsis, the `blocked`
159
163
  * one stays byte-exact with CC.
160
164
  */
@@ -278,7 +282,7 @@ export function monitorEndedSummary(description: string, status: TaskStatus, pro
278
282
  * `Workflow "NAME" finished` — UNVERIFIED: the 2.1.278 binary holds no local
279
283
  * workflow completion summary (only remote/error variants) and no transcript
280
284
  * on this machine carries one, so this mirrors the agent shape
281
- * (docs/features/notifications/plan.md, Deviations).
285
+ * (working-docs/features/notifications/plan.md, Deviations).
282
286
  */
283
287
  export function workflowSummary(name: string, status: TaskStatus, reason?: string): string {
284
288
  const suffix = reason ? `: ${reason}` : "";
@@ -362,6 +366,8 @@ const TAG_RE = {
362
366
  summary: /<summary>([\s\S]*?)<\/summary>/,
363
367
  result: /<result>([\s\S]*?)<\/result>/,
364
368
  event: /<event>([\s\S]*?)<\/event>/,
369
+ status: /<status>([\s\S]*?)<\/status>/,
370
+ durationMs: /<duration_ms>(\d+)<\/duration_ms>/,
365
371
  };
366
372
 
367
373
  function tagBody(block: string, tag: keyof typeof TAG_RE): string | undefined {
@@ -371,7 +377,16 @@ function tagBody(block: string, tag: keyof typeof TAG_RE): string | undefined {
371
377
  /** One frame of a notification's wire text, for display. */
372
378
  export type NotificationFrame =
373
379
  | { kind: "agent-message"; from: string; body: string; handBack: boolean }
374
- | { kind: "task"; summary: string; body: string; pointer: boolean }
380
+ | {
381
+ kind: "task";
382
+ summary: string;
383
+ body: string;
384
+ pointer: boolean;
385
+ /** `<status>`, when the frame has one (an agent or shell completion). */
386
+ status?: string;
387
+ /** `<usage>`'s `<duration_ms>`: an agent frame's run time. */
388
+ durationMs?: number;
389
+ }
375
390
  | { kind: "text"; text: string };
376
391
 
377
392
  /**
@@ -393,11 +408,14 @@ export function parseNotificationFrames(wire: string): NotificationFrame[] {
393
408
  const block = match[1];
394
409
  const body = tagBody(block, "result") ?? tagBody(block, "event");
395
410
  const result = body === undefined ? "" : unescapeTaskText(body).trim();
411
+ const durationMs = tagBody(block, "durationMs");
396
412
  frames.push({
397
413
  kind: "task",
398
414
  summary: unescapeTaskText(tagBody(block, "summary") ?? ""),
399
415
  body: result,
400
416
  pointer: result.startsWith(HAND_BACK_POINTER_PREFIX),
417
+ status: tagBody(block, "status"),
418
+ durationMs: durationMs === undefined ? undefined : Number(durationMs),
401
419
  });
402
420
  } else {
403
421
  frames.push({ kind: "agent-message", from: match[2], body: match[3], handBack: match[3].includes(HAND_BACK_PREAMBLE) });
@@ -463,8 +481,22 @@ export const NOTIFICATION_ID_KEY = "notificationId";
463
481
  /** `details` key on a coalesced message: the `{customType, details}` of each notification it carries, in arrival order. */
464
482
  export const NOTIFICATION_BATCH_KEY = "notifications";
465
483
 
484
+ /** True for the `details` of a message the notifier sent (one notification or a coalesced batch). */
485
+ export function isNotificationDetails(details: unknown): boolean {
486
+ return typeof details === "object" && details !== null && (NOTIFICATION_ID_KEY in details || NOTIFICATION_BATCH_KEY in details);
487
+ }
488
+
466
489
  /** Inject a harness notification (a task/agent completion, a monitor event, a review note) into the conversation. */
467
- export type TaskNotifier = (customType: string, text: string, details?: Record<string, unknown>) => void;
490
+ export interface TaskNotifier {
491
+ (customType: string, text: string, details?: Record<string, unknown>): void;
492
+ /**
493
+ * True while notifications are held after an interrupted turn (see
494
+ * `createTaskNotifier`). A producer that emits a stream of batches (a
495
+ * monitor) keeps accumulating its own bounded batch while this is true,
496
+ * instead of adding one held message per flush.
497
+ */
498
+ holding(): boolean;
499
+ }
468
500
 
469
501
  /**
470
502
  * `task_output` announces here that it returned a FINISHED task's output in a
@@ -505,12 +537,12 @@ export interface TaskNotifierOptions {
505
537
 
506
538
  /**
507
539
  * Custom types that are a turn's INPUT rather than a harness frame (a fired
508
- * wakeup or loop tick delivers the model's prompt verbatim). Such a message is
540
+ * wakeup or cron job delivers the model's prompt verbatim). Such a message is
509
541
  * never merged with frames — it goes out on its own, in arrival order — or the
510
542
  * prompt would abut a `<task-notification>` with nothing marking the switch,
511
543
  * and the wrong renderer would draw the pair.
512
544
  */
513
- export const REINVOCATION_TYPES: ReadonlySet<string> = new Set(["wakeup", "loop", "loop-start"]);
545
+ export const REINVOCATION_TYPES: ReadonlySet<string> = new Set(["wakeup", "cron"]);
514
546
 
515
547
  /** Default coalescing window (ms). */
516
548
  export const DEFAULT_COALESCE_MS = 250;
@@ -556,11 +588,27 @@ export const DEFAULT_COALESCE_MS = 250;
556
588
  * `triggerTurn` restarted, within milliseconds, the very work the user had just
557
589
  * interrupted (review M2). So once a turn has settled `aborted` or `error`
558
590
  * (a turn that died on a provider error should not be restarted by a
559
- * notification either), every notification — the re-sends and
560
- * any new one arriving while idle — is delivered as `nextTurn`: pi holds it
561
- * and appends it to the user's next prompt (`_pendingNextTurnMessages`),
562
- * exactly Claude Code's behaviour after an interrupt. The hold ends at the
563
- * next `before_agent_start`, i.e. the next prompt the user actually sends.
591
+ * notification either), every notification — the re-sends and any new one
592
+ * arriving while idle — is held by the notifier itself, not started as a turn.
593
+ * The hold is bounded and ends with the next turn, however it starts:
594
+ *
595
+ * - The user's next prompt (`before_agent_start`) takes everything held as
596
+ * `nextTurn` messages, which pi appends to that prompt
597
+ * (`_pendingNextTurnMessages`, filled synchronously before pi reads it),
598
+ * exactly Claude Code's behaviour after an interrupt. The held frames merge
599
+ * into one message, as a coalescing window would merge them, so a long
600
+ * absence costs one message, not one per arrival.
601
+ * - Any other turn start (`agent_start`: a typed skill such as `/loop`, which
602
+ * pi runs through `sendMessage` and never through `prompt()`, or a scheduled
603
+ * fire) ends the hold too, and what was held is steered into that turn. A
604
+ * turn that then settles cleanly leaves no hold behind; before, only a typed
605
+ * prompt ended it, so one Esc early in a session silenced every later
606
+ * `/loop` tick.
607
+ * - A fresh re-invocation (a cron or wakeup fire, `REINVOCATION_TYPES`) is
608
+ * never held: it is the turn's input, scheduled by the user or the loop, so
609
+ * it opens its turn as it would have without the interrupt. Only a re-send
610
+ * of one Esc discarded waits. Producers that stream (a monitor) ask
611
+ * `holding()` and keep their own bounded batch meanwhile.
564
612
  *
565
613
  * Dead sessions: a producer's callback can outlive the session it belongs to
566
614
  * (a background shell finishing after `/clear` replaced the session, or after
@@ -578,9 +626,16 @@ export const DEFAULT_COALESCE_MS = 250;
578
626
  * pi's stock system prompt with no reminder stack (review H1, measured). Until
579
627
  * no `prompt()` has run in this process, an idle notification is delivered as a
580
628
  * user message (`sendUserMessage`, source "extension"), which takes the full
581
- * prompt path; the cost is that this one notification renders as a user bubble.
582
- * Every later notification goes the custom-message way. Upstream ask:
583
- * docs/upstream_prs.md #17.
629
+ * prompt path; the cost is that this one notification renders as a user bubble
630
+ * (the background extension's markdown transformer shows only its headline).
631
+ * The usual first-turn case avoids it: the command that started the work (the
632
+ * `/btw` fork) announces pi's prompt options (lib/prompt-options.ts), which
633
+ * system-prompt builds from, and the notification goes the custom-message way
634
+ * with Claude Code's one-line render. Every later notification goes that way. Its turn skips
635
+ * `before_agent_start` too, and pi drops a run's prompt when the run settles,
636
+ * so the system-prompt extension installs the prompt for it from
637
+ * `context_with_system` (`system-prompt/idle-turn.ts`). Upstream ask:
638
+ * working-docs/upstream_prs.md #17.
584
639
  */
585
640
  export function createTaskNotifier(pi: TaskNotifierApi, options: TaskNotifierOptions = {}): TaskNotifier {
586
641
  interface Pending {
@@ -588,6 +643,8 @@ export function createTaskNotifier(pi: TaskNotifierApi, options: TaskNotifierOpt
588
643
  text: string;
589
644
  details: Record<string, unknown>;
590
645
  resent: boolean;
646
+ /** Unmerged arrivals so a held group can withdraw one task without losing its peers. */
647
+ items: Incoming[];
591
648
  }
592
649
  interface Incoming {
593
650
  customType: string;
@@ -596,15 +653,24 @@ export function createTaskNotifier(pi: TaskNotifierApi, options: TaskNotifierOpt
596
653
  }
597
654
  const coalesceMs = options.coalesceMs ?? DEFAULT_COALESCE_MS;
598
655
  const pending = new Map<string, Pending>();
656
+ // Every notifier hears every message_end; simultaneous producers must not share an outbox id.
657
+ const producerId = randomUUID();
599
658
  let seq = 0;
659
+ const nextId = () => `${producerId}-${++seq}`;
600
660
  /** False once the session this notifier belongs to has shut down. */
601
661
  let active = true;
602
- /** True once `prompt()` has run in this process (the only path that emits before_agent_start). */
662
+ /**
663
+ * True once an idle turn can be built without `prompt()`: a prompt has run
664
+ * (the only path that emits before_agent_start), or a command announced
665
+ * pi's prompt options.
666
+ */
603
667
  let prompted = false;
604
668
  /** True between agent_start and agent_settled. */
605
669
  let busy = false;
606
- /** True from a turn settling aborted/errored until the user's next prompt: hold, do not start turns. */
670
+ /** True from a turn settling aborted/errored until the next turn starts: hold, do not start turns. */
607
671
  let interrupted = false;
672
+ /** What the hold kept, in arrival order; released as one merged message (see the header). */
673
+ let held: Incoming[] = [];
608
674
  const outcome = new RunOutcomeLatch();
609
675
  /** Notifications waiting for the coalescing window to close. */
610
676
  let batch: Incoming[] = [];
@@ -615,29 +681,23 @@ export function createTaskNotifier(pi: TaskNotifierApi, options: TaskNotifierOpt
615
681
  pending.delete(id);
616
682
  return;
617
683
  }
684
+ // Held after an interrupt (see the header): kept here, out of reach of
685
+ // Esc's queue clearing, so it leaves the outbox now; a re-send at a
686
+ // later settle would duplicate it. A fresh re-invocation is the one
687
+ // exception: it opens its own turn.
688
+ if (interrupted && !busy && (entry.resent || !REINVOCATION_TYPES.has(entry.customType))) {
689
+ pending.delete(id);
690
+ held.push(...entry.items);
691
+ return;
692
+ }
618
693
  try {
619
- if (interrupted && !busy) {
620
- // Held for the user's next prompt (see the header). pi keeps a nextTurn
621
- // message on the session itself, out of reach of Esc's queue clearing,
622
- // so it counts as delivered now: a re-send at a later settle would
623
- // duplicate it.
624
- pending.delete(id);
625
- pi.sendMessage(
626
- {
627
- customType: entry.customType,
628
- content: [{ type: "text", text: frameForDelivery(entry.text, "with-user-prompt") }],
629
- display: true,
630
- details: { ...entry.details, [NOTIFICATION_ID_KEY]: id },
631
- },
632
- { deliverAs: "nextTurn" },
633
- );
634
- return;
635
- }
636
694
  if (!prompted && !busy) {
637
695
  // No confirmation possible for a user-role message (no details), and
638
696
  // prompt() cannot be cleared by Esc before it starts: count it delivered.
639
697
  pending.delete(id);
640
- pi.sendUserMessage(frameForDelivery(entry.text, "opens-turn"));
698
+ // followUp: pi 1.2.0 counts a starting run as busy and rejects a
699
+ // bare send there; when idle, the option changes nothing.
700
+ pi.sendUserMessage(frameForDelivery(entry.text, "opens-turn"), { deliverAs: "followUp" });
641
701
  return;
642
702
  }
643
703
  pi.sendMessage(
@@ -656,43 +716,56 @@ export function createTaskNotifier(pi: TaskNotifierApi, options: TaskNotifierOpt
656
716
  }
657
717
  };
658
718
 
659
- /** One outbox entry: a lone notification as it came, several merged under one text. */
719
+ /** One entry for a group: a lone notification as it came, several merged under one text. */
720
+ const merge = (group: Incoming[]): Incoming =>
721
+ group.length === 1
722
+ ? group[0]
723
+ : {
724
+ customType: group[0].customType,
725
+ text: mergeNotificationTexts(group.map((n) => n.text)),
726
+ // Keep any pre-batched metadata flat.
727
+ details: {
728
+ [NOTIFICATION_BATCH_KEY]: group.flatMap(
729
+ (n) => (n.details[NOTIFICATION_BATCH_KEY] as unknown[] | undefined) ?? [{ customType: n.customType, details: n.details }],
730
+ ),
731
+ },
732
+ };
733
+
734
+ /** One outbox entry per group. */
660
735
  const enqueue = (group: Incoming[]) => {
661
736
  if (group.length === 0) return;
662
- const id = `${++seq}-${Date.now().toString(36)}`;
663
- const entry: Pending =
664
- group.length === 1
665
- ? { ...group[0], resent: false }
666
- : {
667
- customType: group[0].customType,
668
- text: mergeNotificationTexts(group.map((n) => n.text)),
669
- details: { [NOTIFICATION_BATCH_KEY]: group.map((n) => ({ customType: n.customType, details: n.details })) },
670
- resent: false,
671
- };
737
+ const id = nextId();
738
+ const entry: Pending = { ...merge(group), resent: false, items: group };
672
739
  pending.set(id, entry);
673
740
  dispatch(id, entry);
674
741
  };
675
742
 
676
743
  /**
677
- * Close the coalescing window: frames that arrived in it merge into one
678
- * entry, a standalone (turn-input) message goes out on its own, and arrival
679
- * order is kept across the two.
744
+ * Split arrivals into delivery groups: frames merge, a standalone
745
+ * (turn-input) message stands alone, arrival order is kept across the two.
680
746
  */
681
- const flush = () => {
682
- batchTimer = undefined;
683
- const incoming = batch;
684
- batch = [];
747
+ const groups = (incoming: Incoming[]): Incoming[][] => {
748
+ const out: Incoming[][] = [];
685
749
  let frames: Incoming[] = [];
686
750
  for (const item of incoming) {
687
751
  if (!REINVOCATION_TYPES.has(item.customType)) {
688
752
  frames.push(item);
689
753
  continue;
690
754
  }
691
- enqueue(frames);
755
+ if (frames.length > 0) out.push(frames);
692
756
  frames = [];
693
- enqueue([item]);
757
+ out.push([item]);
694
758
  }
695
- enqueue(frames);
759
+ if (frames.length > 0) out.push(frames);
760
+ return out;
761
+ };
762
+
763
+ /** Close the coalescing window. */
764
+ const flush = () => {
765
+ batchTimer = undefined;
766
+ const incoming = batch;
767
+ batch = [];
768
+ for (const group of groups(incoming)) enqueue(group);
696
769
  };
697
770
  const clearBatch = () => {
698
771
  if (batchTimer !== undefined) clearTimeout(batchTimer);
@@ -700,6 +773,37 @@ export function createTaskNotifier(pi: TaskNotifierApi, options: TaskNotifierOpt
700
773
  batch = [];
701
774
  };
702
775
 
776
+ /** End the hold. `withPrompt`: the user's prompt is being built, so ride it as nextTurn; else steer into the turn that started. */
777
+ const release = (withPrompt: boolean) => {
778
+ interrupted = false;
779
+ const released = held;
780
+ held = [];
781
+ if (!active || released.length === 0) return;
782
+ for (const group of groups(released)) {
783
+ if (!withPrompt) {
784
+ enqueue(group);
785
+ continue;
786
+ }
787
+ const entry = merge(group);
788
+ try {
789
+ // Synchronous up to pi's push onto `_pendingNextTurnMessages`, which
790
+ // the prompt being built reads right after this hook; pi keeps it out
791
+ // of reach of Esc's queue clearing, so no outbox entry.
792
+ pi.sendMessage(
793
+ {
794
+ customType: entry.customType,
795
+ content: [{ type: "text", text: frameForDelivery(entry.text, "with-user-prompt") }],
796
+ display: true,
797
+ details: { ...entry.details, [NOTIFICATION_ID_KEY]: nextId() },
798
+ },
799
+ { deliverAs: "nextTurn" },
800
+ );
801
+ } catch {
802
+ // Disposed under us (see dispatch).
803
+ }
804
+ }
805
+ };
806
+
703
807
  pi.on("message_end", (event) => {
704
808
  const message = event.message as { role?: string; details?: unknown };
705
809
  if (message.role !== "custom") return;
@@ -724,15 +828,24 @@ export function createTaskNotifier(pi: TaskNotifierApi, options: TaskNotifierOpt
724
828
  });
725
829
  pi.on("agent_start", () => {
726
830
  busy = true;
831
+ // A turn the user did not type (a skill such as /loop, a scheduled fire):
832
+ // the session is working again, so what was held steers into it.
833
+ if (interrupted) release(false);
727
834
  });
728
- pi.on("before_agent_start", () => {
835
+ pi.on("before_agent_start", (event) => {
836
+ prompted = true;
837
+ // A run an extension opened (pi 1.2.0 fires this for it too) has no user
838
+ // message to ride; agent_start releases into it as before.
839
+ if (isUserRun(event)) release(true);
840
+ });
841
+ pi.events?.on(PROMPT_OPTIONS_CHANNEL, () => {
729
842
  prompted = true;
730
- interrupted = false;
731
843
  });
732
844
  // A replaced session (/clear, /new, resume) has no use for the old one's undelivered notices.
733
845
  pi.on("session_start", () => {
734
846
  pending.clear();
735
847
  clearBatch();
848
+ held = [];
736
849
  active = true;
737
850
  prompted = false;
738
851
  busy = false;
@@ -741,16 +854,28 @@ export function createTaskNotifier(pi: TaskNotifierApi, options: TaskNotifierOpt
741
854
  pi.on("session_shutdown", () => {
742
855
  active = false;
743
856
  clearBatch();
857
+ held = [];
744
858
  });
745
859
  if (options.withdrawOnDelivery) {
746
860
  pi.events?.on(TASK_OUTPUT_DELIVERED_CHANNEL, (data) => {
747
861
  const taskId = (data as TaskOutputDelivered | undefined)?.taskId;
748
862
  if (taskId === undefined) return;
749
- batch = batch.filter((item) => item.details.taskId !== taskId);
863
+ const keep = (item: Incoming) => item.details.taskId !== taskId;
864
+ batch = batch.filter(keep);
865
+ held = held.filter(keep);
866
+ // A sent entry still awaiting confirmation is resent (or held) when an
867
+ // interrupt discards pi's queue: drop the task from it too, re-merging
868
+ // what remains, so the model is not told of output it already has.
869
+ for (const [id, entry] of [...pending]) {
870
+ const items = entry.items.filter(keep);
871
+ if (items.length === entry.items.length) continue;
872
+ if (items.length === 0) pending.delete(id);
873
+ else Object.assign(entry, merge(items), { items });
874
+ }
750
875
  });
751
876
  }
752
877
 
753
- return (customType, text, details = {}) => {
878
+ const notify = (customType: string, text: string, details: Record<string, unknown> = {}) => {
754
879
  if (!active) return;
755
880
  batch.push({ customType, text, details });
756
881
  if (coalesceMs <= 0) {
@@ -762,6 +887,7 @@ export function createTaskNotifier(pi: TaskNotifierApi, options: TaskNotifierOpt
762
887
  (batchTimer as { unref?: () => void }).unref?.();
763
888
  }
764
889
  };
890
+ return Object.assign(notify, { holding: () => active && interrupted && !busy });
765
891
  }
766
892
 
767
893
  /**
@@ -786,10 +912,128 @@ export function sessionOutlivesTurn(mode: ExtensionMode): boolean {
786
912
  }
787
913
  }
788
914
 
915
+ /**
916
+ * What a one-shot session (`-p`, `--mode json`) changes for the model, said
917
+ * before its first call. Without it the tool descriptions promise detached
918
+ * work, notifications and questions the session cannot deliver, and the model
919
+ * learns otherwise only from a result: in the 2026-10-04 Codex battery both
920
+ * GPT-6 models planned around background shells, agents, monitors, cron jobs
921
+ * and the question dialog, then had to retract. One Code's own text; Claude
922
+ * Code sends no such block.
923
+ */
924
+ export function oneShotSessionNote(): string {
925
+ return [
926
+ "This is a non-interactive one-shot session: it ends when this turn ends, and nobody can reply before then.",
927
+ "- Work you start in the background (a shell command, an agent, a monitor) runs to completion before its call returns, and no notification follows. To overlap independent work, issue those calls together in one batch.",
928
+ "- Scheduled and recurring jobs never fire.",
929
+ "- No user can answer a question or approve a plan. State your assumption and proceed, or ask in your final reply and stop.",
930
+ ].join("\n");
931
+ }
932
+
789
933
  function assertNever(mode: never): never {
790
934
  throw new Error(`Unhandled session mode: ${String(mode)}`);
791
935
  }
792
936
 
937
+ /**
938
+ * Submit a user turn and keep a one-shot command alive through preflight AND
939
+ * settlement. Create once at extension factory scope, not once per command.
940
+ *
941
+ * Unlike sendMessage, sendUserMessage awaits input hooks/auth/before_agent_start
942
+ * before marking the run active. waitForIdle alone therefore returns too soon.
943
+ * Serialize one-shot commands across extensions and wait out any active run
944
+ * before sending: a queued message need not emit agent_start. Correlate that
945
+ * event with the sending async context, never the next unrelated start. Then
946
+ * waitForIdle covers tools, follow-ups and retries. TUI/RPC stay fire-and-forget.
947
+ *
948
+ * pi's void API reports preflight errors separately, without a completion event.
949
+ * Bound the start wait so an error or an input hook consuming the prompt cannot
950
+ * hang a print process forever. The timeout applies only to startup, not the run.
951
+ */
952
+ export function createUserMessageSender(pi: Pick<ExtensionAPI, "on" | "sendUserMessage" | "events">) {
953
+ type Waiter = { start(): void; stop(): void };
954
+ const pending = new Set<Waiter>();
955
+ const sending = new AsyncLocalStorage<Waiter>();
956
+ let active = true;
957
+ let previous = Promise.resolve();
958
+ // Each extension has its own module instance. Reserve the same position in
959
+ // all senders through the session bus, not shared module state.
960
+ pi.events.on("one-code:user-turn-queue", (data) => {
961
+ if (!active) return;
962
+ const reservation = data as { before: Promise<void>[]; done: Promise<void> };
963
+ reservation.before.push(previous);
964
+ previous = reservation.done;
965
+ });
966
+ pi.on("agent_start", () => {
967
+ // Async preflight preserves the sending call's context, including input
968
+ // transformations. An unrelated turn must not release this command.
969
+ sending.getStore()?.start();
970
+ });
971
+ pi.on("session_shutdown", () => {
972
+ active = false;
973
+ for (const waiter of pending) waiter.stop();
974
+ pending.clear();
975
+ sending.disable();
976
+ });
977
+ // A replacement session (/new, /resume) reuses this extension instance:
978
+ // its commands report their failures again, and never queue behind a
979
+ // reservation the old session left unreleased.
980
+ pi.on("session_start", () => {
981
+ active = true;
982
+ previous = Promise.resolve();
983
+ });
984
+ return async (
985
+ ctx: Parameters<typeof awaitOneShotTurn>[0],
986
+ content: Parameters<ExtensionAPI["sendUserMessage"]>[0],
987
+ options?: Parameters<ExtensionAPI["sendUserMessage"]>[1],
988
+ ): Promise<void> => {
989
+ if (!active) throw new Error("Session shut down before the command's user turn completed.");
990
+ if (sessionOutlivesTurn(ctx.mode)) {
991
+ // followUp by default: pi 1.2.0 counts a starting run as busy and
992
+ // rejects a send without deliverAs there; when idle it changes nothing.
993
+ pi.sendUserMessage(content, { ...options, deliverAs: options?.deliverAs ?? "followUp" });
994
+ return;
995
+ }
996
+ let release!: () => void;
997
+ const reservation = { before: [] as Promise<void>[], done: new Promise<void>((resolve) => { release = resolve; }) };
998
+ pi.events.emit("one-code:user-turn-queue", reservation);
999
+ await withKeepAlive(async () => {
1000
+ let start!: () => void;
1001
+ let fail!: (error: Error) => void;
1002
+ const started = new Promise<void>((resolve) => { start = resolve; });
1003
+ const stopped = new Promise<never>((_resolve, reject) => { fail = reject; });
1004
+ const waiter = { start, stop: () => fail(new Error("Session shut down before the command's user turn completed.")) };
1005
+ let timer: ReturnType<typeof setTimeout> | undefined;
1006
+ pending.add(waiter);
1007
+ try {
1008
+ await Promise.race([Promise.all(reservation.before), stopped]);
1009
+ // A queued user message can be consumed in an existing run without
1010
+ // another agent_start. One-shot commands run after that run settles.
1011
+ if (!ctx.isIdle()) await Promise.race([awaitOneShotTurn(ctx), stopped]);
1012
+ if (!active) throw new Error("Session shut down before the command's user turn completed.");
1013
+ timer = setTimeout(() => fail(new Error("Command's user turn did not start within 30 seconds. Check model authentication and extension input hooks, then retry.")), 30_000);
1014
+ // A raw extension send can still start during preflight. Without a
1015
+ // per-send queue acknowledgement, reject that race rather than run
1016
+ // a queued command we would then incorrectly report as not started.
1017
+ sending.run(waiter, () => pi.sendUserMessage(content, options ? { ...options, deliverAs: undefined } : undefined));
1018
+ await Promise.race([started, stopped]);
1019
+ clearTimeout(timer);
1020
+ await Promise.race([awaitOneShotTurn(ctx), stopped]);
1021
+ } finally {
1022
+ clearTimeout(timer);
1023
+ pending.delete(waiter);
1024
+ release();
1025
+ }
1026
+ }).catch((error) => {
1027
+ // pi logs command-handler rejections but runPrintMode still returns
1028
+ // success. Tell the session's exit extension without touching the
1029
+ // process-wide code here (a later prompt may succeed, or this may be
1030
+ // a child). A shutdown rejection must not act on a disposed session.
1031
+ if (active) pi.events.emit(ONE_SHOT_COMMAND_FAILED_CHANNEL, {});
1032
+ throw error;
1033
+ });
1034
+ };
1035
+ }
1036
+
793
1037
  /**
794
1038
  * Block until the current agent run settles, but only in one-shot modes.
795
1039
  *
@@ -803,8 +1047,8 @@ function assertNever(mode: never): never {
803
1047
  *
804
1048
  * `waitForIdle()` exists on a command context; a plain event-handler context
805
1049
  * (e.g. the `input` hook) only exposes `isIdle()`, so poll that as the fallback.
806
- * `pi.sendMessage` marks the run active synchronously, so `isIdle()` already
807
- * reads false by the time a caller reaches here.
1050
+ * `pi.sendMessage` marks the run active synchronously. `sendUserMessage` does
1051
+ * not: use createUserMessageSender for that API so its preflight finishes first.
808
1052
  */
809
1053
  export async function awaitOneShotTurn(ctx: {
810
1054
  mode: ExtensionMode;