one-code-extension 0.4.1 → 0.6.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 (439) 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 +40 -28
  7. package/docs/guide/doctor.md +20 -13
  8. package/docs/guide/hooks.md +18 -3
  9. package/docs/guide/installation.md +26 -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 +124 -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 +642 -0
  56. package/extensions/background/index.ts +614 -248
  57. package/extensions/background/loop-fire.ts +173 -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 +56 -0
  63. package/extensions/background/wakeup.ts +178 -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 +66 -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 +250 -78
  77. package/extensions/btw/panel.ts +228 -100
  78. package/extensions/btw/prompt.ts +41 -4
  79. package/extensions/cache-marks/index.ts +42 -0
  80. package/extensions/claude-compat/index.ts +7 -11
  81. package/extensions/claude-context/index.ts +436 -63
  82. package/extensions/compaction/cut.ts +157 -0
  83. package/extensions/compaction/index.ts +219 -75
  84. package/extensions/compaction/kept-thinking.ts +62 -0
  85. package/extensions/compaction/prompt.ts +15 -12
  86. package/extensions/context-budget/budget.ts +16 -2
  87. package/extensions/context-budget/index.ts +35 -15
  88. package/extensions/context-management/index.ts +36 -28
  89. package/extensions/doctor/build.ts +59 -8
  90. package/extensions/doctor/builtins.ts +84 -0
  91. package/extensions/doctor/cli.ts +7 -1
  92. package/extensions/doctor/compat.ts +88 -23
  93. package/extensions/doctor/dependencies.ts +41 -5
  94. package/extensions/doctor/fix-prompt.ts +9 -2
  95. package/extensions/doctor/index.ts +102 -58
  96. package/extensions/doctor/models.ts +102 -63
  97. package/extensions/doctor/presets.ts +42 -27
  98. package/extensions/doctor/report.ts +19 -2
  99. package/extensions/doctor/viewer.ts +49 -27
  100. package/extensions/effort/index.ts +141 -25
  101. package/extensions/effort/settings.ts +21 -0
  102. package/extensions/effort/slider.ts +24 -20
  103. package/extensions/exit/index.ts +32 -1
  104. package/extensions/file-tracker/index.ts +420 -42
  105. package/extensions/file-tracker/replay.ts +137 -24
  106. package/extensions/file-tracker/restore.ts +66 -0
  107. package/extensions/file-tracker/results.ts +66 -0
  108. package/extensions/file-tracker/shell-reads.ts +211 -0
  109. package/extensions/file-tracker/tracker.ts +45 -15
  110. package/extensions/footer/footer-line.ts +45 -24
  111. package/extensions/footer/index.ts +47 -2
  112. package/extensions/footer/pr.ts +34 -5
  113. package/extensions/hooks/compaction.ts +7 -0
  114. package/extensions/hooks/executor.ts +32 -30
  115. package/extensions/hooks/index.ts +209 -58
  116. package/extensions/hooks/matcher.ts +3 -0
  117. package/extensions/hooks/prompt-context.ts +59 -0
  118. package/extensions/hooks/protocol.ts +41 -3
  119. package/extensions/hooks/settings.ts +50 -19
  120. package/extensions/hooks/subagent-bridge.ts +2 -0
  121. package/extensions/hooks/trust.ts +26 -12
  122. package/extensions/idle-compact/index.ts +119 -0
  123. package/extensions/idle-compact/policy.ts +143 -0
  124. package/extensions/init/index.ts +11 -10
  125. package/extensions/init/prompt.ts +25 -32
  126. package/extensions/interrupted/index.ts +13 -1
  127. package/extensions/kept-thinking/index.ts +27 -0
  128. package/extensions/ledger/index.ts +46 -0
  129. package/extensions/ledger/wire.ts +154 -0
  130. package/extensions/lib/agent-cron.ts +146 -0
  131. package/extensions/lib/agent-loader.ts +18 -3
  132. package/extensions/lib/agent-view.ts +85 -0
  133. package/extensions/lib/anthropic-auth.ts +98 -0
  134. package/extensions/lib/anthropic-payload.ts +82 -1
  135. package/extensions/lib/anthropic-server-call.ts +230 -0
  136. package/extensions/lib/anthropic-server-tools.ts +485 -0
  137. package/extensions/lib/app-launch.mjs +94 -0
  138. package/extensions/lib/argument-hints.ts +190 -0
  139. package/extensions/lib/atomic-write.ts +6 -2
  140. package/extensions/lib/auto-memory.ts +75 -0
  141. package/extensions/lib/bash-parser.ts +84 -0
  142. package/extensions/lib/btw-fork.ts +93 -0
  143. package/extensions/lib/child-extensions.ts +52 -0
  144. package/extensions/lib/child-writes.ts +45 -0
  145. package/extensions/lib/claude-context.ts +515 -100
  146. package/extensions/lib/claude-external-includes.ts +68 -0
  147. package/extensions/lib/claude-id.ts +22 -0
  148. package/extensions/lib/claude-rules.ts +323 -0
  149. package/extensions/lib/claude-settings.ts +76 -4
  150. package/extensions/lib/compaction-boundary.ts +36 -0
  151. package/extensions/lib/compaction-keep.mjs +83 -0
  152. package/extensions/lib/config-mode.ts +112 -0
  153. package/extensions/lib/consent-dialogs.ts +63 -0
  154. package/extensions/lib/consent-preview.ts +5 -1
  155. package/extensions/lib/consent-stores.ts +48 -0
  156. package/extensions/lib/context-facts.ts +97 -0
  157. package/extensions/lib/covered-messages.ts +23 -0
  158. package/extensions/lib/deferred.ts +359 -25
  159. package/extensions/lib/dock-rows.ts +60 -0
  160. package/extensions/lib/empty-tool-result.ts +27 -0
  161. package/extensions/lib/environment-block.ts +76 -0
  162. package/extensions/lib/fetch-timeout.ts +24 -6
  163. package/extensions/{system-prompt → lib}/git-status.ts +51 -13
  164. package/extensions/lib/git.ts +75 -13
  165. package/extensions/lib/grammars/LICENSE-tree-sitter-bash +21 -0
  166. package/extensions/lib/grammars/README.md +2 -0
  167. package/extensions/lib/grammars/tree-sitter-bash.wasm +0 -0
  168. package/extensions/lib/guide-docs.ts +45 -0
  169. package/extensions/lib/headless-output.ts +46 -0
  170. package/extensions/lib/hook-gate.ts +7 -7
  171. package/extensions/lib/interrupt.ts +20 -4
  172. package/extensions/lib/key-input.ts +455 -0
  173. package/extensions/lib/keys.ts +14 -1
  174. package/extensions/lib/ledger/fold-memo.ts +49 -0
  175. package/extensions/lib/ledger/fold.ts +265 -0
  176. package/extensions/lib/ledger/layout.ts +72 -0
  177. package/extensions/lib/ledger/plan.ts +102 -0
  178. package/extensions/lib/ledger/records.ts +276 -0
  179. package/extensions/lib/ledger/state.ts +47 -0
  180. package/extensions/lib/ledger/validate.ts +141 -0
  181. package/extensions/lib/ledger/wire.ts +632 -0
  182. package/extensions/lib/local-command.ts +71 -19
  183. package/extensions/lib/mcp-overrides.ts +1 -1
  184. package/extensions/lib/mcp-share.ts +46 -0
  185. package/extensions/lib/memory-content.ts +146 -0
  186. package/extensions/lib/memory-index.ts +18 -0
  187. package/extensions/lib/memory.ts +134 -123
  188. package/extensions/lib/model-catalog/huggingface.json +178 -0
  189. package/extensions/lib/model-catalog/models-dev.json +18533 -0
  190. package/extensions/lib/model-catalog/openrouter.json +5050 -0
  191. package/extensions/lib/model-catalog-data.ts +513 -0
  192. package/extensions/lib/model-catalog.ts +462 -0
  193. package/extensions/lib/model-choice-warnings.ts +137 -0
  194. package/extensions/lib/model-pairings.ts +152 -0
  195. package/extensions/{auto-mode → lib}/model-picker.ts +26 -43
  196. package/extensions/lib/model-policy.ts +112 -15
  197. package/extensions/lib/model-tier.ts +283 -355
  198. package/extensions/lib/model-unusable.ts +1 -1
  199. package/extensions/lib/newer-model.ts +100 -0
  200. package/extensions/lib/notices.ts +87 -0
  201. package/extensions/lib/notifications.ts +357 -73
  202. package/extensions/lib/one-code-settings.ts +74 -8
  203. package/extensions/lib/open-browser.ts +63 -0
  204. package/extensions/lib/openrouter-generation.ts +94 -0
  205. package/extensions/lib/operation-deadline.ts +30 -0
  206. package/extensions/lib/original-command.ts +4 -4
  207. package/extensions/lib/package-version.ts +1 -1
  208. package/extensions/lib/panel-focus.ts +134 -0
  209. package/extensions/lib/paths.ts +17 -2
  210. package/extensions/lib/permission-gate.ts +72 -6
  211. package/extensions/lib/persisted-output.ts +60 -8
  212. package/extensions/lib/pi-ai-estimate.ts +19 -3
  213. package/extensions/lib/pi-version.ts +38 -2
  214. package/extensions/lib/plan-mode-channels.ts +42 -5
  215. package/extensions/lib/plugins.ts +26 -17
  216. package/extensions/lib/powershell-parse-script.ts +208 -0
  217. package/extensions/lib/powershell-parser.ts +344 -0
  218. package/extensions/lib/previous-mark.ts +101 -0
  219. package/extensions/lib/process-state.ts +48 -0
  220. package/extensions/lib/process-tree.ts +132 -8
  221. package/extensions/lib/prompt-options.ts +82 -0
  222. package/extensions/lib/queued-delivery.ts +44 -0
  223. package/extensions/lib/reminders.ts +370 -358
  224. package/extensions/lib/replaced-builtins.mjs +199 -0
  225. package/extensions/lib/replay-call.ts +70 -0
  226. package/extensions/lib/request-replay.ts +547 -0
  227. package/extensions/lib/scratchpad.ts +81 -14
  228. package/extensions/lib/send-now-channel.ts +7 -0
  229. package/extensions/lib/session-model-tier.ts +51 -0
  230. package/extensions/lib/session-work.ts +64 -0
  231. package/extensions/lib/settings-channels.ts +4 -4
  232. package/extensions/lib/shell-exit-semantics.ts +170 -0
  233. package/extensions/lib/shell-quote.ts +11 -0
  234. package/extensions/lib/shell-result.ts +70 -0
  235. package/extensions/lib/shell-spawn.ts +70 -6
  236. package/extensions/lib/shell-tool.ts +75 -25
  237. package/extensions/lib/side-call-cache.ts +124 -0
  238. package/extensions/lib/side-call-run.ts +96 -0
  239. package/extensions/lib/side-call-usage.ts +38 -0
  240. package/extensions/lib/side-call.ts +24 -1
  241. package/extensions/lib/single-flight.ts +18 -0
  242. package/extensions/lib/skill-body.ts +42 -0
  243. package/extensions/lib/skill-scan.ts +63 -24
  244. package/extensions/lib/spill-file.ts +140 -0
  245. package/extensions/lib/subagent-handback.ts +112 -0
  246. package/extensions/lib/system-role.ts +156 -0
  247. package/extensions/lib/terminal-text.ts +114 -0
  248. package/extensions/lib/text-width.ts +47 -16
  249. package/extensions/lib/timer-ops.ts +20 -0
  250. package/extensions/lib/tool-additions.ts +129 -0
  251. package/extensions/lib/tool-call-corruption.ts +134 -0
  252. package/extensions/lib/tool-path.ts +66 -0
  253. package/extensions/lib/tool-variants.ts +69 -0
  254. package/extensions/lib/tui-render.ts +255 -27
  255. package/extensions/lib/turn-budget-layout.ts +159 -0
  256. package/extensions/lib/update-check.mjs +15 -2
  257. package/extensions/lib/usage-bus.ts +1 -1
  258. package/extensions/lib/user-denial.ts +38 -0
  259. package/extensions/lib/workspace-channel.ts +16 -0
  260. package/extensions/lib/worktree-channel.ts +31 -3
  261. package/extensions/lib/worktree-isolation.ts +40 -10
  262. package/extensions/lsp/client.ts +8 -9
  263. package/extensions/lsp/index.ts +76 -12
  264. package/extensions/lsp/keep-alive.ts +1 -1
  265. package/extensions/lsp/trust.ts +119 -0
  266. package/extensions/lsp/watcher.ts +1 -1
  267. package/extensions/mcp/announce.ts +90 -43
  268. package/extensions/mcp/client.ts +161 -48
  269. package/extensions/mcp/config.ts +132 -8
  270. package/extensions/mcp/discovery.ts +19 -0
  271. package/extensions/mcp/index.ts +220 -121
  272. package/extensions/mcp/oauth/browser.ts +38 -17
  273. package/extensions/mcp/oauth/flow.ts +25 -17
  274. package/extensions/mcp/oauth/provider.ts +14 -8
  275. package/extensions/mcp/oauth/store.ts +34 -16
  276. package/extensions/mcp/panel/keys.ts +12 -11
  277. package/extensions/mcp/panel/render.ts +9 -6
  278. package/extensions/mcp/schema.ts +104 -5
  279. package/extensions/mcp/trust.ts +164 -73
  280. package/extensions/memory/entries.ts +23 -14
  281. package/extensions/memory/index.ts +134 -28
  282. package/extensions/memory/open-external.ts +33 -18
  283. package/extensions/memory/panel.ts +135 -26
  284. package/extensions/model-default/index.ts +86 -19
  285. package/extensions/newer-model/index.ts +31 -0
  286. package/extensions/notebook/description.ts +18 -0
  287. package/extensions/notebook/index.ts +6 -5
  288. package/extensions/notebook/notebook.ts +1 -1
  289. package/extensions/permissions/index.ts +1426 -398
  290. package/extensions/permissions/matcher.ts +425 -74
  291. package/extensions/permissions/modes.ts +11 -4
  292. package/extensions/permissions/panel/host.ts +130 -0
  293. package/extensions/permissions/panel/keys.ts +48 -0
  294. package/extensions/permissions/panel/render.ts +450 -0
  295. package/extensions/permissions/panel/state.ts +550 -0
  296. package/extensions/permissions/powershell-cmdlets.ts +198 -0
  297. package/extensions/permissions/powershell-paths.ts +197 -0
  298. package/extensions/permissions/powershell-rules.ts +207 -280
  299. package/extensions/permissions/powershell-tree.ts +557 -0
  300. package/extensions/permissions/project-trust.ts +27 -11
  301. package/extensions/permissions/protected-paths.ts +14 -2
  302. package/extensions/permissions/session-grant.ts +6 -0
  303. package/extensions/permissions/settings.ts +166 -31
  304. package/extensions/permissions/subagent-gate.ts +25 -0
  305. package/extensions/permissions/workspace.ts +78 -0
  306. package/extensions/plan-mode/index.ts +276 -92
  307. package/extensions/plan-mode/reminder.ts +89 -21
  308. package/extensions/plan-mode/run-state.ts +26 -0
  309. package/extensions/plan-mode/texts.ts +177 -0
  310. package/extensions/plan-mode/viewer.ts +92 -27
  311. package/extensions/plugins/counts.ts +3 -3
  312. package/extensions/plugins/index.ts +64 -46
  313. package/extensions/plugins/install/install.ts +16 -0
  314. package/extensions/plugins/marketplace/sync.ts +9 -3
  315. package/extensions/plugins/marketplace/types.ts +3 -1
  316. package/extensions/plugins/panel/keys.ts +21 -24
  317. package/extensions/plugins/panel/render.ts +12 -6
  318. package/extensions/plugins/shell-expand.ts +107 -0
  319. package/extensions/plugins/template.ts +32 -15
  320. package/extensions/powershell/child.ts +7 -0
  321. package/extensions/powershell/description.ts +38 -6
  322. package/extensions/powershell/guards.ts +1 -1
  323. package/extensions/powershell/index.ts +14 -7
  324. package/extensions/recap/index.ts +73 -71
  325. package/extensions/recap/prompt.ts +2 -2
  326. package/extensions/recap/scheduler.ts +3 -5
  327. package/extensions/search-tools/grep-merge.ts +39 -0
  328. package/extensions/search-tools/index.ts +0 -0
  329. package/extensions/send-now/chord.ts +62 -0
  330. package/extensions/send-now/index.ts +133 -0
  331. package/extensions/session-title/index.ts +3 -0
  332. package/extensions/session-title/title.ts +4 -1
  333. package/extensions/skill/announce.ts +30 -0
  334. package/extensions/skill/code-review.ts +97 -0
  335. package/extensions/skill/index.ts +469 -132
  336. package/extensions/skill/invoke.ts +118 -75
  337. package/extensions/skill/listing.ts +133 -9
  338. package/extensions/skill/panel/keys.ts +15 -16
  339. package/extensions/skill/panel/render.ts +5 -2
  340. package/extensions/skill/panel/state.ts +12 -1
  341. package/extensions/subagents/agent-args.ts +39 -0
  342. package/extensions/subagents/agent-description.ts +132 -0
  343. package/extensions/subagents/agents.ts +146 -31
  344. package/extensions/subagents/default-model.ts +2 -3
  345. package/extensions/subagents/delegation-steer.ts +4 -4
  346. package/extensions/subagents/fork-cache.ts +82 -0
  347. package/extensions/subagents/guide-agent.ts +189 -0
  348. package/extensions/subagents/hand-back-review.ts +3 -0
  349. package/extensions/subagents/index.ts +1594 -852
  350. package/extensions/subagents/live-runs.ts +26 -6
  351. package/extensions/subagents/model-select.ts +38 -25
  352. package/extensions/subagents/outcome.ts +5 -2
  353. package/extensions/subagents/panel-keys.ts +73 -31
  354. package/extensions/subagents/panel-render.ts +120 -96
  355. package/extensions/subagents/panel-widget.ts +60 -89
  356. package/extensions/subagents/parent-mode.ts +50 -0
  357. package/extensions/subagents/pending-claim.ts +18 -1
  358. package/extensions/subagents/runner.ts +135 -80
  359. package/extensions/subagents/runs.ts +44 -5
  360. package/extensions/subagents/send-to-main-tool.ts +1 -1
  361. package/extensions/subagents/session-turns.ts +32 -8
  362. package/extensions/subagents/shell-panel.ts +36 -178
  363. package/extensions/subagents/tasks-dialog.ts +170 -0
  364. package/extensions/subagents/viewport-mirror.ts +95 -0
  365. package/extensions/subagents/worktree.ts +57 -13
  366. package/extensions/system-prompt/environment.ts +19 -19
  367. package/extensions/system-prompt/idle-turn.ts +65 -0
  368. package/extensions/system-prompt/index.ts +165 -47
  369. package/extensions/system-prompt/template.ts +34 -51
  370. package/extensions/system-prompt/tiers/common.ts +23 -64
  371. package/extensions/system-prompt/tiers/frontier.ts +32 -11
  372. package/extensions/system-prompt/tiers/low.ts +67 -45
  373. package/extensions/system-prompt/tiers/mid.ts +96 -38
  374. package/extensions/system-reminder/index.ts +204 -70
  375. package/extensions/tasks/descriptions.ts +190 -0
  376. package/extensions/tasks/index.ts +120 -41
  377. package/extensions/tasks/store.ts +83 -16
  378. package/extensions/tool-call-corruption/child.ts +7 -0
  379. package/extensions/tool-call-corruption/index.ts +237 -0
  380. package/extensions/tool-search/index.ts +366 -144
  381. package/extensions/tool-style/index.ts +68 -10
  382. package/extensions/tool-style/write-description.ts +40 -0
  383. package/extensions/tool-style/write-render.ts +237 -0
  384. package/extensions/turn-duration/index.ts +7 -3
  385. package/extensions/turn-duration/line.ts +47 -4
  386. package/extensions/web/backends.ts +6 -9
  387. package/extensions/web/description.ts +50 -0
  388. package/extensions/web/index.ts +118 -53
  389. package/extensions/web/native-call.ts +15 -0
  390. package/extensions/web/native-results.ts +41 -0
  391. package/extensions/web-fetch/description.ts +42 -0
  392. package/extensions/web-fetch/extract.ts +53 -6
  393. package/extensions/web-fetch/index.ts +125 -76
  394. package/extensions/web-fetch/page-content.ts +19 -0
  395. package/extensions/web-fetch/response.ts +49 -0
  396. package/extensions/web-fetch/summarize.ts +9 -10
  397. package/extensions/workflow/agent-session.ts +108 -18
  398. package/extensions/workflow/args.ts +24 -0
  399. package/extensions/workflow/description.ts +67 -0
  400. package/extensions/workflow/globals.ts +17 -3
  401. package/extensions/workflow/index.ts +157 -162
  402. package/extensions/workflow/records.ts +1 -0
  403. package/extensions/workflow/run-manager.ts +33 -6
  404. package/extensions/workflow/saved-workflows.ts +3 -2
  405. package/extensions/workflow/types.ts +22 -1
  406. package/extensions/workflow/viewer.ts +54 -24
  407. package/extensions/workflow/widget.ts +20 -0
  408. package/extensions/worktree/descriptions.ts +91 -0
  409. package/extensions/worktree/guards.ts +316 -54
  410. package/extensions/worktree/index.ts +162 -44
  411. package/extensions/worktree/powershell-guards.ts +167 -0
  412. package/extensions/worktree/rewrite.ts +14 -16
  413. package/package.json +21 -7
  414. package/skills/artifact-design/SKILL.md +85 -0
  415. package/skills/code-review/SKILL.md +20 -20
  416. package/skills/code-review/cells/arguments-and-flags.md +19 -0
  417. package/skills/code-review/cells/high-3x5-recall-verify.md +137 -0
  418. package/skills/code-review/cells/high-8-inline.md +122 -0
  419. package/skills/code-review/cells/low-1-pass-max4.md +27 -0
  420. package/skills/code-review/cells/low-1-pass-min-files-4.md +28 -0
  421. package/skills/code-review/cells/max-5x5-verify-sweep.md +162 -0
  422. package/skills/code-review/cells/medium-3x5-verify.md +132 -0
  423. package/skills/code-review/cells/minimal-prompt.md +32 -0
  424. package/skills/code-review/cells/xhigh-10-inline-sweep.md +149 -0
  425. package/skills/code-review/cells/xhigh-5x5-verify-sweep.md +162 -0
  426. package/skills/fewer-permission-prompts/SKILL.md +1 -1
  427. package/skills/loop/SKILL.md +11 -0
  428. package/skills/security-review/SKILL.md +2 -5
  429. package/skills/simplify/SKILL.md +7 -5
  430. package/skills/workflow-authoring/SKILL.md +143 -0
  431. package/themes/onecode-light.json +1 -1
  432. package/themes/onecode.json +1 -1
  433. package/types/app.d.ts +58 -0
  434. package/types/community-packages.d.ts +26 -1
  435. package/extensions/auto-mode/recoverability.ts +0 -229
  436. package/extensions/lib/capability-index.ts +0 -466
  437. package/extensions/lib/model-facts.generated.json +0 -2675
  438. package/extensions/lib/model-facts.ts +0 -162
  439. package/extensions/tool-search/announce.ts +0 -42
@@ -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,18 @@
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 type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
33
- import { RunOutcomeLatch } from "./interrupt.ts";
32
+ import { AsyncLocalStorage } from "node:async_hooks";
33
+ import { randomUUID } from "node:crypto";
34
+ import { type ExtensionAPI, type ExtensionContext, VERSION as PI_VERSION } from "@earendil-works/pi-coding-agent";
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 { piFiresBeforeAgentStartForCustomRuns } from "./pi-version.ts";
39
+ import { isUserRun, PROMPT_OPTIONS_CHANNEL } from "./prompt-options.ts";
35
40
  import { wrapReminder } from "./reminders.ts";
36
41
 
37
42
  /** pi's session-mode union, derived from the context (the package root does not export ExtensionMode itself). */
@@ -154,7 +159,7 @@ export type HandBackVerdict = { kind: "blocked"; reason: string } | { kind: "una
154
159
  * provider body, a stack), and this is the choke point where it enters the
155
160
  * model-facing warning, so it must not go in unbounded. The clip is a bound on
156
161
  * 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
162
+ * persisted (working-docs/decisions/subagents-workflows.md, 2026-09-21); the
158
163
  * `unavailable` variant marks a clipped reason with an ellipsis, the `blocked`
159
164
  * one stays byte-exact with CC.
160
165
  */
@@ -278,7 +283,7 @@ export function monitorEndedSummary(description: string, status: TaskStatus, pro
278
283
  * `Workflow "NAME" finished` — UNVERIFIED: the 2.1.278 binary holds no local
279
284
  * workflow completion summary (only remote/error variants) and no transcript
280
285
  * on this machine carries one, so this mirrors the agent shape
281
- * (docs/features/notifications/plan.md, Deviations).
286
+ * (working-docs/features/notifications/plan.md, Deviations).
282
287
  */
283
288
  export function workflowSummary(name: string, status: TaskStatus, reason?: string): string {
284
289
  const suffix = reason ? `: ${reason}` : "";
@@ -362,6 +367,8 @@ const TAG_RE = {
362
367
  summary: /<summary>([\s\S]*?)<\/summary>/,
363
368
  result: /<result>([\s\S]*?)<\/result>/,
364
369
  event: /<event>([\s\S]*?)<\/event>/,
370
+ status: /<status>([\s\S]*?)<\/status>/,
371
+ durationMs: /<duration_ms>(\d+)<\/duration_ms>/,
365
372
  };
366
373
 
367
374
  function tagBody(block: string, tag: keyof typeof TAG_RE): string | undefined {
@@ -371,7 +378,16 @@ function tagBody(block: string, tag: keyof typeof TAG_RE): string | undefined {
371
378
  /** One frame of a notification's wire text, for display. */
372
379
  export type NotificationFrame =
373
380
  | { kind: "agent-message"; from: string; body: string; handBack: boolean }
374
- | { kind: "task"; summary: string; body: string; pointer: boolean }
381
+ | {
382
+ kind: "task";
383
+ summary: string;
384
+ body: string;
385
+ pointer: boolean;
386
+ /** `<status>`, when the frame has one (an agent or shell completion). */
387
+ status?: string;
388
+ /** `<usage>`'s `<duration_ms>`: an agent frame's run time. */
389
+ durationMs?: number;
390
+ }
375
391
  | { kind: "text"; text: string };
376
392
 
377
393
  /**
@@ -393,11 +409,14 @@ export function parseNotificationFrames(wire: string): NotificationFrame[] {
393
409
  const block = match[1];
394
410
  const body = tagBody(block, "result") ?? tagBody(block, "event");
395
411
  const result = body === undefined ? "" : unescapeTaskText(body).trim();
412
+ const durationMs = tagBody(block, "durationMs");
396
413
  frames.push({
397
414
  kind: "task",
398
415
  summary: unescapeTaskText(tagBody(block, "summary") ?? ""),
399
416
  body: result,
400
417
  pointer: result.startsWith(HAND_BACK_POINTER_PREFIX),
418
+ status: tagBody(block, "status"),
419
+ durationMs: durationMs === undefined ? undefined : Number(durationMs),
401
420
  });
402
421
  } else {
403
422
  frames.push({ kind: "agent-message", from: match[2], body: match[3], handBack: match[3].includes(HAND_BACK_PREAMBLE) });
@@ -464,7 +483,16 @@ export const NOTIFICATION_ID_KEY = "notificationId";
464
483
  export const NOTIFICATION_BATCH_KEY = "notifications";
465
484
 
466
485
  /** 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;
486
+ export interface TaskNotifier {
487
+ (customType: string, text: string, details?: Record<string, unknown>): void;
488
+ /**
489
+ * True while notifications are held after an interrupted turn (see
490
+ * `createTaskNotifier`). A producer that emits a stream of batches (a
491
+ * monitor) keeps accumulating its own bounded batch while this is true,
492
+ * instead of adding one held message per flush.
493
+ */
494
+ holding(): boolean;
495
+ }
468
496
 
469
497
  /**
470
498
  * `task_output` announces here that it returned a FINISHED task's output in a
@@ -501,16 +529,35 @@ export interface TaskNotifierOptions {
501
529
  * report, so its hand-back would only repeat it.
502
530
  */
503
531
  withdrawOnDelivery?: boolean;
532
+ /**
533
+ * Whether pi fires `before_agent_start` for a run `sendMessage` opens from
534
+ * idle (pi 1.2.0 and later). Then such a run builds One Code's system
535
+ * prompt like a typed one, and no notification needs the user-message
536
+ * path (see "First turn of a session"). Defaults to the running pi's
537
+ * version; tests set it to model an older pi.
538
+ */
539
+ customRunsFireBeforeAgentStart?: boolean;
540
+ /**
541
+ * Runs once a notification this notifier sent reaches the conversation,
542
+ * with its customType and the details it was sent with: at its
543
+ * `message_end`, or at the send for one delivered as a user message (see
544
+ * "First turn of a session"), whose message carries no details for pi to
545
+ * hand back. A producer whose message makes something true for the model
546
+ * (a `/loop` fire's instructions) records that here, not when it calls the
547
+ * notifier: the notifier may still hold the message, and a `/reload`
548
+ * drops it.
549
+ */
550
+ onDelivered?: (customType: string, details: Record<string, unknown>) => void;
504
551
  }
505
552
 
506
553
  /**
507
554
  * 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
555
+ * wakeup or cron job delivers the model's prompt verbatim). Such a message is
509
556
  * never merged with frames — it goes out on its own, in arrival order — or the
510
557
  * prompt would abut a `<task-notification>` with nothing marking the switch,
511
558
  * and the wrong renderer would draw the pair.
512
559
  */
513
- export const REINVOCATION_TYPES: ReadonlySet<string> = new Set(["wakeup", "loop", "loop-start"]);
560
+ export const REINVOCATION_TYPES: ReadonlySet<string> = new Set(["wakeup", "cron"]);
514
561
 
515
562
  /** Default coalescing window (ms). */
516
563
  export const DEFAULT_COALESCE_MS = 250;
@@ -556,11 +603,27 @@ export const DEFAULT_COALESCE_MS = 250;
556
603
  * `triggerTurn` restarted, within milliseconds, the very work the user had just
557
604
  * interrupted (review M2). So once a turn has settled `aborted` or `error`
558
605
  * (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.
606
+ * notification either), every notification — the re-sends and any new one
607
+ * arriving while idle — is held by the notifier itself, not started as a turn.
608
+ * The hold is bounded and ends with the next turn, however it starts:
609
+ *
610
+ * - The user's next prompt (`before_agent_start`) takes everything held as
611
+ * `nextTurn` messages, which pi appends to that prompt
612
+ * (`_pendingNextTurnMessages`, filled synchronously before pi reads it),
613
+ * exactly Claude Code's behaviour after an interrupt. The held frames merge
614
+ * into one message, as a coalescing window would merge them, so a long
615
+ * absence costs one message, not one per arrival.
616
+ * - Any other turn start (`agent_start`: a typed skill such as `/loop`, which
617
+ * pi runs through `sendMessage` and never through `prompt()`, or a scheduled
618
+ * fire) ends the hold too, and what was held is steered into that turn. A
619
+ * turn that then settles cleanly leaves no hold behind; before, only a typed
620
+ * prompt ended it, so one Esc early in a session silenced every later
621
+ * `/loop` tick.
622
+ * - A fresh re-invocation (a cron or wakeup fire, `REINVOCATION_TYPES`) is
623
+ * never held: it is the turn's input, scheduled by the user or the loop, so
624
+ * it opens its turn as it would have without the interrupt. Only a re-send
625
+ * of one Esc discarded waits. Producers that stream (a monitor) ask
626
+ * `holding()` and keep their own bounded batch meanwhile.
564
627
  *
565
628
  * Dead sessions: a producer's callback can outlive the session it belongs to
566
629
  * (a background shell finishing after `/clear` replaced the session, or after
@@ -571,16 +634,33 @@ export const DEFAULT_COALESCE_MS = 250;
571
634
  * anything dispatched afterwards; a throwing `sendMessage` is swallowed as a
572
635
  * second line.
573
636
  *
574
- * First turn of a session: pi's idle `sendMessage(…, {triggerTurn: true})`
575
- * calls `_runAgentPrompt` directly and skips the `prompt()` preamble, so no
576
- * `before_agent_start` fires — and our system prompt is set from that hook.
637
+ * First turn of a session, on pi before 1.2.0: pi's idle
638
+ * `sendMessage(…, {triggerTurn: true})` calls `_runAgentPrompt` directly and
639
+ * skips the `prompt()` preamble, so no `before_agent_start` fires — and our
640
+ * system prompt is set from that hook.
577
641
  * A session whose first input was `/loop …` therefore ran its first request on
578
642
  * pi's stock system prompt with no reminder stack (review H1, measured). Until
579
643
  * no `prompt()` has run in this process, an idle notification is delivered as a
580
644
  * 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.
645
+ * prompt path; the cost is that this one notification renders as a user bubble
646
+ * (the background extension's markdown transformer shows only its headline).
647
+ * The usual first-turn case avoids it: the command that started the work (the
648
+ * `/btw` fork) announces pi's prompt options (lib/prompt-options.ts), which
649
+ * system-prompt builds from, and the notification goes the custom-message way
650
+ * with Claude Code's one-line render. Every later notification goes that way. Its turn skips
651
+ * `before_agent_start` too, and pi drops a run's prompt when the run settles,
652
+ * so the system-prompt extension installs the prompt for it from
653
+ * `context_with_system` (`system-prompt/idle-turn.ts`). Upstream ask:
654
+ * working-docs/upstream_prs.md #17.
655
+ *
656
+ * pi 1.2.0 fires `before_agent_start` for that run too, so there every
657
+ * notification goes the custom-message way from the start. Before, the
658
+ * fallback ran on every pi, and a `/reload` re-runs the factory, which starts
659
+ * over as if no prompt had run: a re-armed `/loop` wakeup went out as a plain
660
+ * user message and lost its customType and no-op fold (lifecycle grid
661
+ * R3-M17). On an older pi the fallback is still right after a reload: the
662
+ * system-prompt extension has lost the options too, and the user-message
663
+ * path is the only one that rebuilds the prompt.
584
664
  */
585
665
  export function createTaskNotifier(pi: TaskNotifierApi, options: TaskNotifierOptions = {}): TaskNotifier {
586
666
  interface Pending {
@@ -588,6 +668,8 @@ export function createTaskNotifier(pi: TaskNotifierApi, options: TaskNotifierOpt
588
668
  text: string;
589
669
  details: Record<string, unknown>;
590
670
  resent: boolean;
671
+ /** Unmerged arrivals so a held group can withdraw one task without losing its peers. */
672
+ items: Incoming[];
591
673
  }
592
674
  interface Incoming {
593
675
  customType: string;
@@ -595,16 +677,27 @@ export function createTaskNotifier(pi: TaskNotifierApi, options: TaskNotifierOpt
595
677
  details: Record<string, unknown>;
596
678
  }
597
679
  const coalesceMs = options.coalesceMs ?? DEFAULT_COALESCE_MS;
680
+ const customRunsPrompt = options.customRunsFireBeforeAgentStart ?? piFiresBeforeAgentStartForCustomRuns(PI_VERSION);
598
681
  const pending = new Map<string, Pending>();
682
+ // Every notifier hears every message_end; simultaneous producers must not share an outbox id.
683
+ const producerId = randomUUID();
599
684
  let seq = 0;
685
+ const nextId = () => `${producerId}-${++seq}`;
600
686
  /** False once the session this notifier belongs to has shut down. */
601
687
  let active = true;
602
- /** True once `prompt()` has run in this process (the only path that emits before_agent_start). */
603
- let prompted = false;
688
+ /**
689
+ * True once an idle turn can be built without `prompt()`: always on pi
690
+ * 1.2.0 and later; before it, once a prompt has run (the only path that
691
+ * emits before_agent_start there) or a command announced pi's prompt
692
+ * options.
693
+ */
694
+ let prompted = customRunsPrompt;
604
695
  /** True between agent_start and agent_settled. */
605
696
  let busy = false;
606
- /** True from a turn settling aborted/errored until the user's next prompt: hold, do not start turns. */
697
+ /** True from a turn settling aborted/errored until the next turn starts: hold, do not start turns. */
607
698
  let interrupted = false;
699
+ /** What the hold kept, in arrival order; released as one merged message (see the header). */
700
+ let held: Incoming[] = [];
608
701
  const outcome = new RunOutcomeLatch();
609
702
  /** Notifications waiting for the coalescing window to close. */
610
703
  let batch: Incoming[] = [];
@@ -615,29 +708,24 @@ export function createTaskNotifier(pi: TaskNotifierApi, options: TaskNotifierOpt
615
708
  pending.delete(id);
616
709
  return;
617
710
  }
711
+ // Held after an interrupt (see the header): kept here, out of reach of
712
+ // Esc's queue clearing, so it leaves the outbox now; a re-send at a
713
+ // later settle would duplicate it. A fresh re-invocation is the one
714
+ // exception: it opens its own turn.
715
+ if (interrupted && !busy && (entry.resent || !REINVOCATION_TYPES.has(entry.customType))) {
716
+ pending.delete(id);
717
+ held.push(...entry.items);
718
+ return;
719
+ }
618
720
  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
721
  if (!prompted && !busy) {
637
722
  // No confirmation possible for a user-role message (no details), and
638
723
  // prompt() cannot be cleared by Esc before it starts: count it delivered.
639
724
  pending.delete(id);
640
- pi.sendUserMessage(frameForDelivery(entry.text, "opens-turn"));
725
+ // followUp: pi 1.2.0 counts a starting run as busy and rejects a
726
+ // bare send there; when idle, the option changes nothing.
727
+ pi.sendUserMessage(frameForDelivery(entry.text, "opens-turn"), { deliverAs: "followUp" });
728
+ options.onDelivered?.(entry.customType, entry.details);
641
729
  return;
642
730
  }
643
731
  pi.sendMessage(
@@ -656,43 +744,56 @@ export function createTaskNotifier(pi: TaskNotifierApi, options: TaskNotifierOpt
656
744
  }
657
745
  };
658
746
 
659
- /** One outbox entry: a lone notification as it came, several merged under one text. */
747
+ /** One entry for a group: a lone notification as it came, several merged under one text. */
748
+ const merge = (group: Incoming[]): Incoming =>
749
+ group.length === 1
750
+ ? group[0]
751
+ : {
752
+ customType: group[0].customType,
753
+ text: mergeNotificationTexts(group.map((n) => n.text)),
754
+ // Keep any pre-batched metadata flat.
755
+ details: {
756
+ [NOTIFICATION_BATCH_KEY]: group.flatMap(
757
+ (n) => (n.details[NOTIFICATION_BATCH_KEY] as unknown[] | undefined) ?? [{ customType: n.customType, details: n.details }],
758
+ ),
759
+ },
760
+ };
761
+
762
+ /** One outbox entry per group. */
660
763
  const enqueue = (group: Incoming[]) => {
661
764
  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
- };
765
+ const id = nextId();
766
+ const entry: Pending = { ...merge(group), resent: false, items: group };
672
767
  pending.set(id, entry);
673
768
  dispatch(id, entry);
674
769
  };
675
770
 
676
771
  /**
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.
772
+ * Split arrivals into delivery groups: frames merge, a standalone
773
+ * (turn-input) message stands alone, arrival order is kept across the two.
680
774
  */
681
- const flush = () => {
682
- batchTimer = undefined;
683
- const incoming = batch;
684
- batch = [];
775
+ const groups = (incoming: Incoming[]): Incoming[][] => {
776
+ const out: Incoming[][] = [];
685
777
  let frames: Incoming[] = [];
686
778
  for (const item of incoming) {
687
779
  if (!REINVOCATION_TYPES.has(item.customType)) {
688
780
  frames.push(item);
689
781
  continue;
690
782
  }
691
- enqueue(frames);
783
+ if (frames.length > 0) out.push(frames);
692
784
  frames = [];
693
- enqueue([item]);
785
+ out.push([item]);
694
786
  }
695
- enqueue(frames);
787
+ if (frames.length > 0) out.push(frames);
788
+ return out;
789
+ };
790
+
791
+ /** Close the coalescing window. */
792
+ const flush = () => {
793
+ batchTimer = undefined;
794
+ const incoming = batch;
795
+ batch = [];
796
+ for (const group of groups(incoming)) enqueue(group);
696
797
  };
697
798
  const clearBatch = () => {
698
799
  if (batchTimer !== undefined) clearTimeout(batchTimer);
@@ -700,11 +801,46 @@ export function createTaskNotifier(pi: TaskNotifierApi, options: TaskNotifierOpt
700
801
  batch = [];
701
802
  };
702
803
 
804
+ /** End the hold. `withPrompt`: the user's prompt is being built, so ride it as nextTurn; else steer into the turn that started. */
805
+ const release = (withPrompt: boolean) => {
806
+ interrupted = false;
807
+ const released = held;
808
+ held = [];
809
+ if (!active || released.length === 0) return;
810
+ for (const group of groups(released)) {
811
+ if (!withPrompt) {
812
+ enqueue(group);
813
+ continue;
814
+ }
815
+ const entry = merge(group);
816
+ try {
817
+ // Synchronous up to pi's push onto `_pendingNextTurnMessages`, which
818
+ // the prompt being built reads right after this hook; pi keeps it out
819
+ // of reach of Esc's queue clearing, so no outbox entry.
820
+ pi.sendMessage(
821
+ {
822
+ customType: entry.customType,
823
+ content: [{ type: "text", text: frameForDelivery(entry.text, "with-user-prompt") }],
824
+ display: true,
825
+ details: { ...entry.details, [NOTIFICATION_ID_KEY]: nextId() },
826
+ },
827
+ { deliverAs: "nextTurn" },
828
+ );
829
+ } catch {
830
+ // Disposed under us (see dispatch).
831
+ }
832
+ }
833
+ };
834
+
703
835
  pi.on("message_end", (event) => {
704
- const message = event.message as { role?: string; details?: unknown };
836
+ const message = event.message as { role?: string; customType?: string; details?: unknown };
705
837
  if (message.role !== "custom") return;
706
- const id = (message.details as Record<string, unknown> | undefined)?.[NOTIFICATION_ID_KEY];
707
- if (typeof id === "string") pending.delete(id);
838
+ const details = message.details as Record<string, unknown> | undefined;
839
+ const id = details?.[NOTIFICATION_ID_KEY];
840
+ if (typeof id !== "string") return;
841
+ pending.delete(id);
842
+ // Every notifier hears every message_end: only its own ids are its deliveries.
843
+ if (details && id.startsWith(`${producerId}-`)) options.onDelivered?.(message.customType ?? "", details);
708
844
  });
709
845
  pi.on("agent_end", (event, ctx) => {
710
846
  outcome.record(event.messages, ctx.signal?.aborted);
@@ -724,33 +860,54 @@ export function createTaskNotifier(pi: TaskNotifierApi, options: TaskNotifierOpt
724
860
  });
725
861
  pi.on("agent_start", () => {
726
862
  busy = true;
863
+ // A turn the user did not type (a skill such as /loop, a scheduled fire):
864
+ // the session is working again, so what was held steers into it.
865
+ if (interrupted) release(false);
727
866
  });
728
- pi.on("before_agent_start", () => {
867
+ pi.on("before_agent_start", (event) => {
868
+ prompted = true;
869
+ // A run an extension opened (pi 1.2.0 fires this for it too) has no user
870
+ // message to ride; agent_start releases into it as before.
871
+ if (isUserRun(event)) release(true);
872
+ });
873
+ pi.events?.on(PROMPT_OPTIONS_CHANNEL, () => {
729
874
  prompted = true;
730
- interrupted = false;
731
875
  });
732
876
  // A replaced session (/clear, /new, resume) has no use for the old one's undelivered notices.
733
877
  pi.on("session_start", () => {
734
878
  pending.clear();
735
879
  clearBatch();
880
+ held = [];
736
881
  active = true;
737
- prompted = false;
882
+ prompted = customRunsPrompt;
738
883
  busy = false;
739
884
  interrupted = false;
740
885
  });
741
886
  pi.on("session_shutdown", () => {
742
887
  active = false;
743
888
  clearBatch();
889
+ held = [];
744
890
  });
745
891
  if (options.withdrawOnDelivery) {
746
892
  pi.events?.on(TASK_OUTPUT_DELIVERED_CHANNEL, (data) => {
747
893
  const taskId = (data as TaskOutputDelivered | undefined)?.taskId;
748
894
  if (taskId === undefined) return;
749
- batch = batch.filter((item) => item.details.taskId !== taskId);
895
+ const keep = (item: Incoming) => item.details.taskId !== taskId;
896
+ batch = batch.filter(keep);
897
+ held = held.filter(keep);
898
+ // A sent entry still awaiting confirmation is resent (or held) when an
899
+ // interrupt discards pi's queue: drop the task from it too, re-merging
900
+ // what remains, so the model is not told of output it already has.
901
+ for (const [id, entry] of [...pending]) {
902
+ const items = entry.items.filter(keep);
903
+ if (items.length === entry.items.length) continue;
904
+ if (items.length === 0) pending.delete(id);
905
+ else Object.assign(entry, merge(items), { items });
906
+ }
750
907
  });
751
908
  }
752
909
 
753
- return (customType, text, details = {}) => {
910
+ const notify = (customType: string, text: string, details: Record<string, unknown> = {}) => {
754
911
  if (!active) return;
755
912
  batch.push({ customType, text, details });
756
913
  if (coalesceMs <= 0) {
@@ -762,6 +919,7 @@ export function createTaskNotifier(pi: TaskNotifierApi, options: TaskNotifierOpt
762
919
  (batchTimer as { unref?: () => void }).unref?.();
763
920
  }
764
921
  };
922
+ return Object.assign(notify, { holding: () => active && interrupted && !busy });
765
923
  }
766
924
 
767
925
  /**
@@ -786,10 +944,136 @@ export function sessionOutlivesTurn(mode: ExtensionMode): boolean {
786
944
  }
787
945
  }
788
946
 
947
+ /**
948
+ * What a one-shot session (`-p`, `--mode json`) changes for the model, said
949
+ * before its first call. Without it the tool descriptions promise detached
950
+ * work, notifications and questions the session cannot deliver, and the model
951
+ * learns otherwise only from a result: in the 2026-10-04 Codex battery both
952
+ * GPT-6 models planned around background shells, agents, monitors, cron jobs
953
+ * and the question dialog, then had to retract. One Code's own text; Claude
954
+ * Code sends no such block.
955
+ */
956
+ export function oneShotSessionNote(): string {
957
+ return [
958
+ "This is a non-interactive one-shot session: it ends when this turn ends, and nobody can reply before then.",
959
+ "- 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.",
960
+ "- Scheduled and recurring jobs never fire.",
961
+ "- No user can answer a question or approve a plan. State your assumption and proceed, or ask in your final reply and stop.",
962
+ ].join("\n");
963
+ }
964
+
965
+ /**
966
+ * The notice that a session whose first message said it was one-shot runs
967
+ * interactively now (a `-p` session continued in the TUI). One Code's own text.
968
+ */
969
+ export function interactiveSessionNote(): string {
970
+ return "This session is interactive now, although its first message said it was one-shot: a user can reply, background work notifies you when it ends, scheduled jobs fire, and questions and plan approvals reach the user.";
971
+ }
972
+
789
973
  function assertNever(mode: never): never {
790
974
  throw new Error(`Unhandled session mode: ${String(mode)}`);
791
975
  }
792
976
 
977
+ /**
978
+ * Submit a user turn and keep a one-shot command alive through preflight AND
979
+ * settlement. Create once at extension factory scope, not once per command.
980
+ *
981
+ * Unlike sendMessage, sendUserMessage awaits input hooks/auth/before_agent_start
982
+ * before marking the run active. waitForIdle alone therefore returns too soon.
983
+ * Serialize one-shot commands across extensions and wait out any active run
984
+ * before sending: a queued message need not emit agent_start. Correlate that
985
+ * event with the sending async context, never the next unrelated start. Then
986
+ * waitForIdle covers tools, follow-ups and retries. TUI/RPC stay fire-and-forget.
987
+ *
988
+ * pi's void API reports preflight errors separately, without a completion event.
989
+ * Bound the start wait so an error or an input hook consuming the prompt cannot
990
+ * hang a print process forever. The timeout applies only to startup, not the run.
991
+ */
992
+ export function createUserMessageSender(pi: Pick<ExtensionAPI, "on" | "sendUserMessage" | "events">) {
993
+ type Waiter = { start(): void; stop(): void };
994
+ const pending = new Set<Waiter>();
995
+ const sending = new AsyncLocalStorage<Waiter>();
996
+ let active = true;
997
+ let previous = Promise.resolve();
998
+ // Each extension has its own module instance. Reserve the same position in
999
+ // all senders through the session bus, not shared module state.
1000
+ pi.events.on("one-code:user-turn-queue", (data) => {
1001
+ if (!active) return;
1002
+ const reservation = data as { before: Promise<void>[]; done: Promise<void> };
1003
+ reservation.before.push(previous);
1004
+ previous = reservation.done;
1005
+ });
1006
+ pi.on("agent_start", () => {
1007
+ // Async preflight preserves the sending call's context, including input
1008
+ // transformations. An unrelated turn must not release this command.
1009
+ sending.getStore()?.start();
1010
+ });
1011
+ pi.on("session_shutdown", () => {
1012
+ active = false;
1013
+ for (const waiter of pending) waiter.stop();
1014
+ pending.clear();
1015
+ sending.disable();
1016
+ });
1017
+ // A replacement session (/new, /resume) reuses this extension instance:
1018
+ // its commands report their failures again, and never queue behind a
1019
+ // reservation the old session left unreleased.
1020
+ pi.on("session_start", () => {
1021
+ active = true;
1022
+ previous = Promise.resolve();
1023
+ });
1024
+ return async (
1025
+ ctx: Parameters<typeof awaitOneShotTurn>[0],
1026
+ content: Parameters<ExtensionAPI["sendUserMessage"]>[0],
1027
+ options?: Parameters<ExtensionAPI["sendUserMessage"]>[1],
1028
+ ): Promise<void> => {
1029
+ if (!active) throw new Error("Session shut down before the command's user turn completed.");
1030
+ if (sessionOutlivesTurn(ctx.mode)) {
1031
+ // followUp by default: pi 1.2.0 counts a starting run as busy and
1032
+ // rejects a send without deliverAs there; when idle it changes nothing.
1033
+ pi.sendUserMessage(content, { ...options, deliverAs: options?.deliverAs ?? "followUp" });
1034
+ return;
1035
+ }
1036
+ let release!: () => void;
1037
+ const reservation = { before: [] as Promise<void>[], done: new Promise<void>((resolve) => { release = resolve; }) };
1038
+ pi.events.emit("one-code:user-turn-queue", reservation);
1039
+ await withKeepAlive(async () => {
1040
+ let start!: () => void;
1041
+ let fail!: (error: Error) => void;
1042
+ const started = new Promise<void>((resolve) => { start = resolve; });
1043
+ const stopped = new Promise<never>((_resolve, reject) => { fail = reject; });
1044
+ const waiter = { start, stop: () => fail(new Error("Session shut down before the command's user turn completed.")) };
1045
+ let timer: ReturnType<typeof setTimeout> | undefined;
1046
+ pending.add(waiter);
1047
+ try {
1048
+ await Promise.race([Promise.all(reservation.before), stopped]);
1049
+ // A queued user message can be consumed in an existing run without
1050
+ // another agent_start. One-shot commands run after that run settles.
1051
+ if (!ctx.isIdle()) await Promise.race([awaitOneShotTurn(ctx), stopped]);
1052
+ if (!active) throw new Error("Session shut down before the command's user turn completed.");
1053
+ 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);
1054
+ // A raw extension send can still start during preflight. Without a
1055
+ // per-send queue acknowledgement, reject that race rather than run
1056
+ // a queued command we would then incorrectly report as not started.
1057
+ sending.run(waiter, () => pi.sendUserMessage(content, options ? { ...options, deliverAs: undefined } : undefined));
1058
+ await Promise.race([started, stopped]);
1059
+ clearTimeout(timer);
1060
+ await Promise.race([awaitOneShotTurn(ctx), stopped]);
1061
+ } finally {
1062
+ clearTimeout(timer);
1063
+ pending.delete(waiter);
1064
+ release();
1065
+ }
1066
+ }).catch((error) => {
1067
+ // pi logs command-handler rejections but runPrintMode still returns
1068
+ // success. Tell the session's exit extension without touching the
1069
+ // process-wide code here (a later prompt may succeed, or this may be
1070
+ // a child). A shutdown rejection must not act on a disposed session.
1071
+ if (active) pi.events.emit(ONE_SHOT_COMMAND_FAILED_CHANNEL, {});
1072
+ throw error;
1073
+ });
1074
+ };
1075
+ }
1076
+
793
1077
  /**
794
1078
  * Block until the current agent run settles, but only in one-shot modes.
795
1079
  *
@@ -803,8 +1087,8 @@ function assertNever(mode: never): never {
803
1087
  *
804
1088
  * `waitForIdle()` exists on a command context; a plain event-handler context
805
1089
  * (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.
1090
+ * `pi.sendMessage` marks the run active synchronously. `sendUserMessage` does
1091
+ * not: use createUserMessageSender for that API so its preflight finishes first.
808
1092
  */
809
1093
  export async function awaitOneShotTurn(ctx: {
810
1094
  mode: ExtensionMode;