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
@@ -15,7 +15,7 @@
15
15
  *
16
16
  * A hook can block anything, loaders included (tool_search/skill/
17
17
  * structured_output) — full CC fidelity, the user's config is the user's
18
- * choice; see docs/findings.md for the foot-gun note. A hook can never
18
+ * choice; see working-docs/findings.md for the foot-gun note. A hook can never
19
19
  * pre-approve: "allow" is informational and the permission gate still runs.
20
20
  *
21
21
  * pi's emitToolCall has no try/catch around handlers, so every hook dispatch
@@ -26,11 +26,17 @@
26
26
  * never as a message of its own: PreToolUse context is queued as a one-shot on
27
27
  * the reminder channel, so it lands INSIDE the tool result it belongs to,
28
28
  * persisted, at no extra turn; PostToolUse context is appended to the result
29
- * the same way; prompt/session/compaction context is returned from
30
- * `before_agent_start` as a custom message right AFTER the user's prompt. Until
29
+ * the same way; prompt, session and compaction context is a one-shot too,
30
+ * persisted as a `one-code:pending` entry when the hook answers, so it rides
31
+ * the next request whatever opens it (a prompt, a notification, the
32
+ * continuation after a mid-turn compaction) and counts as delivered only when
33
+ * that request's reply stays on the branch (lib/ledger/fold.ts). Until
31
34
  * 2026-09-05 each was a custom steer (one extra LLM turn per hook per tool,
32
35
  * pi draining one steer per call) and prompt context landed BEFORE the prompt
33
- * (STEERING-REVIEW-2026-09-05 M6).
36
+ * (STEERING-REVIEW-2026-09-05 M6); until 2026-10-21 prompt, session and
37
+ * compaction context was a `before_agent_start` message, taken off its queue
38
+ * before the request went out and held until a run started (round 3,
39
+ * R3-M14).
34
40
  *
35
41
  * Subagents: the tool hooks also run for a child's tool calls, through the
36
42
  * bridge published on `SUBAGENT_HOOK_CHANNEL` (subagent-bridge.ts): the child
@@ -38,12 +44,16 @@
38
44
  * (settings, consent, logging) and a payload naming the child.
39
45
  */
40
46
 
47
+ import { randomUUID } from "node:crypto";
48
+ import { contentText } from "@earendil-works/pi-ai";
41
49
  import { getAgentDir, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
42
50
  import { RunOutcomeLatch } from "../lib/interrupt.ts";
51
+ import { SESSION_WORK_CHANNEL, type SessionWorkQuery } from "../lib/session-work.ts";
43
52
  import { claudeConfigDir } from "../lib/paths.ts";
44
53
  import { defaultDiscoverRoots } from "../lib/plugins.ts";
45
54
  import { appendHookLog, formatDebugLine, hooksDebugEnabled, hooksLogPath } from "./debug.ts";
46
55
  import { runHookCommand } from "./executor.ts";
56
+ import { PRECOMPACT_INSTRUCTIONS_CHANNEL, type PreCompactInstructions } from "./compaction.ts";
47
57
  import { changedSince, FORMATTER_NOTICE, type FileSnapshot, fileToolTarget, snapshotFile } from "./formatter-notice.ts";
48
58
  import { matcherApplies, ccToolName, toolMatchCandidates, ccToolInput, nativeToolInput } from "./matcher.ts";
49
59
  import { loadPluginHooks } from "./plugin-hooks.ts";
@@ -52,13 +62,18 @@ import {
52
62
  type CcHookEvent,
53
63
  type HookOutcome,
54
64
  type HookStdinPayload,
65
+ cancelledOutcome,
55
66
  interpretHookResult,
56
67
  } from "./protocol.ts";
57
68
  import { type HookCommand, type HooksSource, loadHookSettings } from "./settings.ts";
58
69
  import { persistIfLarge, sessionResultsDir } from "../lib/persisted-output.ts";
59
- import { REMINDER_CHANNEL, wrapReminder } from "../lib/reminders.ts";
70
+ import { REMINDER_CHANNEL, type ReminderPayload, wrapReminder } from "../lib/reminders.ts";
60
71
  import { type ChildHookCall, type ChildHookResult, type HookBridge, SUBAGENT_HOOK_CHANNEL, type SubagentHookPayload } from "./subagent-bridge.ts";
61
72
  import { projectHooksApproved } from "./trust.ts";
73
+ import { PromptContextRouter } from "./prompt-context.ts";
74
+ import { SKILL_INVOCATION_TYPE, type SkillInvocationDetails } from "../skill/invoke.ts";
75
+ import { consentDialog } from "../lib/consent-dialogs.ts";
76
+ import { followWorkCwd } from "../lib/worktree-channel.ts";
62
77
 
63
78
  /** Claude Code's `hook_additional_context` attachment text (utils/messages.ts). */
64
79
  export function hookContextText(event: CcHookEvent, text: string): string {
@@ -74,11 +89,13 @@ export function hookContextText(event: CcHookEvent, text: string): string {
74
89
  * convention) and the model gets a preview plus the path — nothing is cut away.
75
90
  */
76
91
  const HOOK_MODEL_TEXT_CAP = 20_000;
77
- // A monotonic counter, not Date.now(), so two hooks finishing in the same
78
- // millisecond do not persist to the same file and clobber each other (review M2).
79
- let hookPersistSeq = 0;
92
+ // A random id, never a counter: the results directory is shared by every
93
+ // session of the project, and a counter restarts with each process and each
94
+ // /reload, so a later hook overwrote a file an earlier transcript points to
95
+ // (design.md section 14). Two hooks finishing in the same millisecond still get
96
+ // two files (review M2).
80
97
  function capHookText(text: string, ctx: HookDispatchCtx, label: string): string {
81
- return persistIfLarge(text, { dir: sessionResultsDir(ctx), id: `hook-${label}-${hookPersistSeq++}`, maxBytes: HOOK_MODEL_TEXT_CAP });
98
+ return persistIfLarge(text, { dir: sessionResultsDir(ctx), id: `hook-${label}-${randomUUID()}`, maxBytes: HOOK_MODEL_TEXT_CAP });
82
99
  }
83
100
 
84
101
  interface MatchedHook {
@@ -95,15 +112,31 @@ interface HookDispatchCtx {
95
112
  cwd: string;
96
113
  hasUI: boolean;
97
114
  sessionManager: { getSessionId(): string; getSessionFile(): string | undefined; getSessionDir?(): string | undefined };
98
- ui: { confirm(title: string, message: string): Promise<boolean | undefined>; notify(message: string, type: "info"): void };
115
+ ui: { confirm(title: string, message: string, options?: { signal?: AbortSignal }): Promise<boolean | undefined>; notify(message: string, type: "info"): void };
116
+ signal?: AbortSignal;
99
117
  /** Set on the shutdown snapshot: no consent prompt, no notices — the session is gone. */
100
118
  sessionEnded?: true;
101
119
  }
102
120
 
121
+ /** Claude Code's default cap on consecutive Stop-hook continuations in one user turn. */
122
+ const MAX_STOP_CONTINUATIONS = 8;
123
+
124
+ /** The customType of the Stop hook's continuation message. */
125
+ const STOP_CONTINUATION_TYPE = "one-code:hook-stop";
126
+
127
+ /** Whether a `before_agent_start` opens the Stop hook's own continuation run (pi 1.2.0 fires it for custom runs). */
128
+ function isStopContinuation(event: unknown): boolean {
129
+ const run = event as { messageType?: string; message?: { customType?: string } };
130
+ return run.messageType === "custom" && run.message?.customType === STOP_CONTINUATION_TYPE;
131
+ }
132
+
103
133
  export default function hooksExtension(pi: ExtensionAPI) {
104
- let stopHookActive = false;
105
- /** Context from UserPromptSubmit / SessionStart / PostCompact hooks, delivered with the next prompt. */
106
- let pendingPromptContext: Array<{ event: CcHookEvent; text: string }> = [];
134
+ const workCwd = followWorkCwd(pi.events);
135
+ let stopHookBlocks = 0;
136
+ /** Prompt, session and compaction hook context, as a one-shot the next request carries (lib/reminders.ts). */
137
+ const deliverContext = (event: CcHookEvent, text: string) => pi.events.emit(REMINDER_CHANNEL, { text: hookContextText(event, text), placement: "last-append" });
138
+ /** UserPromptSubmit context, held until pi delivers its prompt (prompt-context.ts). */
139
+ const promptContext = new PromptContextRouter();
107
140
  /** The last context seen, for bridged child calls (dispatched parent-side). */
108
141
  let lastCtx: ExtensionContext | undefined;
109
142
  /**
@@ -116,6 +149,8 @@ export default function hooksExtension(pi: ExtensionAPI) {
116
149
  let sessionStartPending: Promise<void> | undefined;
117
150
  /** Bumped on every session_start; a superseded dispatch checks it before writing context. */
118
151
  let sessionGen = 0;
152
+ /** Covers startup hooks and other dispatches that have no active turn signal. */
153
+ let sessionAbort = new AbortController();
119
154
 
120
155
  const notify = (ctx: HookDispatchCtx, message: string) => {
121
156
  if (ctx.sessionEnded) return; // session gone; nowhere to show it
@@ -131,7 +166,7 @@ export default function hooksExtension(pi: ExtensionAPI) {
131
166
  ): Promise<MatchedHook[]> => {
132
167
  const claudeDir = claudeConfigDir();
133
168
  const loaded = loadHookSettings(claudeDir, ctx.cwd);
134
- const pluginSources = loadPluginHooks(defaultDiscoverRoots(getAgentDir(), ctx.cwd), loaded.diagnostics);
169
+ const pluginSources = loaded.disabled ? [] : loadPluginHooks(defaultDiscoverRoots(getAgentDir(), ctx.cwd), loaded.diagnostics);
135
170
  if (hooksDebugEnabled()) {
136
171
  for (const diagnostic of loaded.diagnostics) process.stderr.write(`[hooks] ${diagnostic}\n`);
137
172
  }
@@ -143,7 +178,8 @@ export default function hooksExtension(pi: ExtensionAPI) {
143
178
  projectAllowed = await projectHooksApproved(ctx.cwd, projectSources, {
144
179
  hasUI: ctx.hasUI,
145
180
  noPrompt: ctx.sessionEnded,
146
- confirm: (title, message) => ctx.ui.confirm(title, message),
181
+ signal: ctx.signal,
182
+ confirm: (title, message) => consentDialog(pi.events, (signal) => ctx.ui.confirm(title, message, { signal: signal && ctx.signal ? AbortSignal.any([signal, ctx.signal]) : signal ?? ctx.signal })),
147
183
  notify: (message) => notify(ctx, message),
148
184
  });
149
185
  }
@@ -168,7 +204,7 @@ export default function hooksExtension(pi: ExtensionAPI) {
168
204
  const basePayload = (ctx: HookDispatchCtx, event: CcHookEvent): HookStdinPayload => ({
169
205
  session_id: ctx.sessionManager.getSessionId(),
170
206
  transcript_path: ctx.sessionManager.getSessionFile() ?? "",
171
- cwd: ctx.cwd,
207
+ cwd: workCwd(ctx.cwd),
172
208
  hook_event_name: event,
173
209
  });
174
210
 
@@ -181,20 +217,27 @@ export default function hooksExtension(pi: ExtensionAPI) {
181
217
  ctx: HookDispatchCtx,
182
218
  event: CcHookEvent,
183
219
  matchValue: { candidates?: string[]; ignoreMatcher?: boolean },
184
- payload: HookStdinPayload,
220
+ payloadOrFactory: HookStdinPayload | (() => Promise<HookStdinPayload | undefined>),
185
221
  /** Runs once, only when hooks will actually run — the point where a caller can snapshot state the hooks may change. */
186
222
  willRun?: () => void,
187
223
  ): Promise<HookOutcome> => {
188
224
  const merged: HookOutcome = {};
225
+ const signal = ctx.sessionEnded ? ctx.signal : ctx.signal ? AbortSignal.any([ctx.signal, sessionAbort.signal]) : sessionAbort.signal;
189
226
  try {
190
- const hooks = await collectHooks(ctx, event, matchValue);
191
- if (hooks.length === 0) return merged;
227
+ const hooks = await collectHooks({ ...ctx, signal }, event, matchValue);
228
+ const payload = typeof payloadOrFactory === "function" ? await payloadOrFactory() : payloadOrFactory;
229
+ if (!payload || hooks.length === 0) return merged;
230
+ // Cancelled before the hooks ran: the gating events block, as a mid-run cancel does.
231
+ if (signal?.aborted) return cancelledOutcome(event);
192
232
  willRun?.();
193
233
  const stdin = JSON.stringify(payload);
194
- const outcomes = await Promise.all(
234
+ const outcomes = await Promise.allSettled(
195
235
  hooks.map(async ({ source, hook }) => {
196
236
  const run = await runHookCommand(hook.command, stdin, {
197
- cwd: ctx.cwd,
237
+ // The calling session's worktree or child's directory; settings,
238
+ // consent and CLAUDE_PROJECT_DIR still belong to the parent project.
239
+ cwd: payload.cwd,
240
+ signal,
198
241
  timeoutSeconds: hook.timeout,
199
242
  shell: hook.shell,
200
243
  projectDir: ctx.cwd,
@@ -221,7 +264,16 @@ export default function hooksExtension(pi: ExtensionAPI) {
221
264
  return outcome;
222
265
  }),
223
266
  );
224
- for (const outcome of outcomes) {
267
+ if (signal?.aborted) return cancelledOutcome(event);
268
+ for (const result of outcomes) {
269
+ // One broken response must not erase another hook's deny or let the
270
+ // tool run while the other hooks are still deciding.
271
+ if (result.status === "rejected") {
272
+ if (hooksDebugEnabled()) process.stderr.write(`[hooks] hook error (${event}): ${String(result.reason)}\n`);
273
+ continue;
274
+ }
275
+ const outcome = result.value;
276
+ if (outcome.preventContinuation) merged.preventContinuation = true;
225
277
  // A block reason becomes model-facing text (the tool result, or a
226
278
  // Stop follow-up), so it is bounded like the other hook texts —
227
279
  // stderr on an exit-2 hook is easily a full stack trace (review M2).
@@ -291,7 +343,7 @@ export default function hooksExtension(pi: ExtensionAPI) {
291
343
  // to it: this extension awaits the hook and loads before file-tracker, so
292
344
  // the tracker records the hook's version as the model's own write
293
345
  // (formatter-notice.ts, review M1). Snapshot only when hooks will run.
294
- const target = event.isError ? undefined : fileToolTarget(event.toolName, event.input, ctx.cwd);
346
+ const target = event.isError ? undefined : fileToolTarget(event.toolName, event.input, payload.cwd);
295
347
  let before: FileSnapshot | undefined;
296
348
  const outcome = await dispatch(ctx, "PostToolUse", { candidates: toolMatchCandidates(event.toolName) }, payload, () => {
297
349
  if (target) before = snapshotFile(target);
@@ -305,47 +357,98 @@ export default function hooksExtension(pi: ExtensionAPI) {
305
357
  [outcome.additionalContext && wrapReminder(hookContextText("PostToolUse", outcome.additionalContext)), formatterNotice]
306
358
  .filter(Boolean)
307
359
  .join("\n") || undefined,
308
- });
360
+ }, event.toolName);
309
361
  if (!content) return undefined;
310
362
  return { content, isError: event.isError };
311
363
  });
312
364
 
313
- /** Await the backgrounded SessionStart dispatch once, if one is pending. */
365
+ /**
366
+ * Await the backgrounded SessionStart dispatch, if one is pending. Every
367
+ * caller awaits the same dispatch, and it is forgotten only once it settles:
368
+ * a notification opening the first turn and a prompt typed meanwhile both
369
+ * wait for it, rather than the second returning at once.
370
+ */
314
371
  const drainSessionStart = async () => {
315
- if (!sessionStartPending) return;
316
372
  const pending = sessionStartPending;
317
- sessionStartPending = undefined;
373
+ if (!pending) return;
318
374
  try {
319
375
  await pending;
320
376
  } catch {
321
377
  // dispatch already reports its own failures; never block the turn on one.
322
378
  }
379
+ if (sessionStartPending === pending) sessionStartPending = undefined;
323
380
  };
324
381
 
325
382
  // ---- UserPromptSubmit ---------------------------------------------------
326
383
  pi.on("input", async (event, ctx) => {
384
+ // A new idle prompt means an earlier idle one was delivered or never will
385
+ // be (an extension after this one handled it): its context is stale. An
386
+ // extension's prompt (a command's sendUserMessage) counts too, or its
387
+ // message would take that context as its own.
388
+ if (!event.streamingBehavior) promptContext.dropIdle();
327
389
  if (event.source === "extension") return undefined;
328
- stopHookActive = false;
390
+ // Set for a message queued mid-turn (pi 0.86+ fires `input` for
391
+ // steer/followUp). The Stop latch is reset in before_agent_start, where a
392
+ // turn really starts: pi re-checks streaming after these handlers, so a
393
+ // queued message whose turn ended during the hook opens a new turn.
394
+ const gen = sessionGen;
329
395
  // Drain the backgrounded SessionStart dispatch before this prompt's own
330
396
  // UserPromptSubmit context, so their order in the first turn is unchanged
331
397
  // (SessionStart context precedes UserPromptSubmit). before_agent_start is
332
398
  // the backstop for turns with no input event (one-shot, programmatic).
333
399
  await drainSessionStart();
400
+ if (gen !== sessionGen) return { action: "handled" as const };
334
401
  const payload: HookStdinPayload = { ...basePayload(ctx, "UserPromptSubmit"), prompt: event.text };
335
402
  const outcome = await dispatch(ctx, "UserPromptSubmit", { ignoreMatcher: true }, payload);
403
+ if (gen !== sessionGen) return { action: "handled" as const };
336
404
  if (outcome.block) {
337
405
  notify(ctx, `Prompt blocked by UserPromptSubmit hook: ${outcome.block.reason}`);
338
406
  return { action: "handled" as const };
339
407
  }
340
- if (outcome.additionalContext) pendingPromptContext.push({ event: "UserPromptSubmit", text: outcome.additionalContext });
408
+ // The context waits for pi to deliver this prompt (message_start below),
409
+ // queued mid-turn or not, so it rides the prompt's own request and never a
410
+ // run a notification opens first. A queued message is held without
411
+ // context too, so its delivery never takes an idle prompt's.
412
+ const text = outcome.additionalContext ? hookContextText("UserPromptSubmit", outcome.additionalContext) : undefined;
413
+ if (event.streamingBehavior) promptContext.holdQueued(event.text, text);
414
+ else if (text) promptContext.holdIdle(event.text, text);
341
415
  return undefined;
342
416
  });
343
417
 
344
- // Prompt/session/compaction hook context rides the turn it belongs to as a
345
- // custom message pi appends right AFTER the user's prompt (the
346
- // before_agent_start `message` return). An idle `sendMessage` from here
347
- // landed it BEFORE the prompt (pi appends first, then adds the user message).
348
- pi.on("before_agent_start", async (_event, ctx) => {
418
+ // pi awaits this before the request that carries the delivered message, so a
419
+ // one-shot emitted here is pinned to that message: after the prompt, as the
420
+ // before_agent_start message is for a prompt that opens a turn.
421
+ pi.on("message_start", async (event) => {
422
+ const gen = sessionGen;
423
+ const message = event.message;
424
+ // Pinned to its prompt: when that prompt's request fails, the next prompt's
425
+ // request resends it with its own context, not with this one's.
426
+ const emit = (text: string) => pi.events.emit(REMINDER_CHANNEL, { text, placement: "last-append", promptTimestamp: message.timestamp } satisfies ReminderPayload);
427
+ if (message.role === "user") {
428
+ if (promptContext.isEmpty) return;
429
+ // A queued message delivers as typed, so an exact match is its own; the
430
+ // idle prompt's message may carry its expansion instead.
431
+ const text = promptContext.release(contentText(message.content, ""));
432
+ if (text) emit(text);
433
+ return;
434
+ }
435
+ if (message.role !== "custom") return;
436
+ // A background completion that opens the first turn skips
437
+ // before_agent_start: the SessionStart context must be pending before its
438
+ // request is rendered.
439
+ await drainSessionStart();
440
+ // A `/skill:` command the skill extension took over arrives as its hidden
441
+ // message, carrying the typed text: that prompt's context rides it.
442
+ if (message.customType !== SKILL_INVOCATION_TYPE || promptContext.isEmpty) return;
443
+ const input = (message.details as Partial<SkillInvocationDetails> | undefined)?.input;
444
+ const queued = typeof input === "string" ? promptContext.release(input, true) : undefined;
445
+ if (gen === sessionGen && queued) emit(queued);
446
+ });
447
+
448
+ // The first turn waits for the backgrounded SessionStart dispatch, so its
449
+ // context is pending before the request is rendered.
450
+ pi.on("before_agent_start", async (event, ctx) => {
451
+ const gen = sessionGen;
349
452
  lastCtx = ctx;
350
453
  // The SessionStart dispatch runs in the background from session_start; the
351
454
  // first turn must wait for it so its additionalContext is present (and, on
@@ -353,35 +456,37 @@ export default function hooksExtension(pi: ExtensionAPI) {
353
456
  // The `input` handler usually drained it already; this is the backstop for
354
457
  // turns with no input event.
355
458
  await drainSessionStart();
356
- if (pendingPromptContext.length === 0) return;
357
- const texts = pendingPromptContext;
358
- pendingPromptContext = [];
359
- return {
360
- message: {
361
- customType: "one-code:hook-context",
362
- content: texts.map(({ event, text }) => wrapReminder(hookContextText(event, text))).join("\n"),
363
- display: false,
364
- },
365
- };
459
+ if (gen !== sessionGen) return;
460
+ // Every run that opens lands here (on pi 1.2.0 also one an extension
461
+ // opened; a queued delivery does not). Only the Stop hook's own
462
+ // continuation keeps the cap counting: a notification or scheduled run
463
+ // starts a fresh turn, as a prompt does.
464
+ if (!isStopContinuation(event)) stopHookBlocks = 0;
366
465
  });
367
466
 
368
467
  // ---- SessionStart / SessionEnd -----------------------------------------
369
468
  const dispatchSessionStart = async (ctx: ExtensionContext, source: string, gen: number) => {
370
- const payload: HookStdinPayload = { ...basePayload(ctx, "SessionStart"), source };
371
- const outcome = await dispatch(ctx, "SessionStart", { candidates: [source] }, payload);
469
+ // Reserve project-hook consent synchronously, before MCP and external
470
+ // imports enqueue their dialogs. Only defer the payload: worktree's later
471
+ // synchronous session_start handler must restore cwd before we capture it.
472
+ const outcome = await dispatch(ctx, "SessionStart", { candidates: [source] }, async () => {
473
+ await new Promise<void>((resolve) => setImmediate(resolve));
474
+ return gen === sessionGen ? { ...basePayload(ctx, "SessionStart"), source } : undefined;
475
+ });
372
476
  // A later session_start (rapid /clear, resume) may have superseded this one
373
477
  // while its hook ran; discard the stale result instead of pushing it into
374
- // the new session's context (`pendingPromptContext` is shared and reset).
375
- if (gen === sessionGen && outcome.additionalContext) {
376
- pendingPromptContext.push({ event: "SessionStart", text: outcome.additionalContext });
377
- }
478
+ // the new session's context.
479
+ if (gen === sessionGen && outcome.additionalContext) deliverContext("SessionStart", outcome.additionalContext);
378
480
  };
379
481
 
380
482
  pi.on("session_start", (event, ctx) => {
483
+ sessionAbort.abort();
484
+ sessionAbort = new AbortController();
381
485
  lastCtx = ctx;
382
486
  const gen = ++sessionGen;
383
- pendingPromptContext = [];
487
+ promptContext.clear();
384
488
  sessionStartPending = undefined;
489
+ stopHookBlocks = 0;
385
490
  // Publish the child hook bridge (subagent-bridge.ts). The closures read
386
491
  // live parent state per call, so once per session start is enough.
387
492
  pi.events.emit(SUBAGENT_HOOK_CHANNEL, { bridge: childHookBridge } satisfies SubagentHookPayload);
@@ -410,9 +515,21 @@ export default function hooksExtension(pi: ExtensionAPI) {
410
515
  agent_id: call.sessionId,
411
516
  agent_type: call.agentType,
412
517
  });
518
+ /**
519
+ * The parent's settings/consent context with the CHILD's run signal. A
520
+ * background child outlives the parent's turn, so Esc in the parent must
521
+ * not skip a child's deny hook; stopping the child must cancel its hooks.
522
+ */
523
+ const childDispatchCtx = (ctx: ExtensionContext, call: ChildHookCall): HookDispatchCtx => ({
524
+ cwd: ctx.cwd,
525
+ hasUI: ctx.hasUI,
526
+ sessionManager: ctx.sessionManager,
527
+ ui: ctx.ui,
528
+ signal: call.signal,
529
+ });
413
530
  const childHookBridge: HookBridge = {
414
531
  async preToolUse(call) {
415
- const ctx = lastCtx;
532
+ const ctx = lastCtx && childDispatchCtx(lastCtx, call);
416
533
  if (!ctx) return {};
417
534
  const outcome = await dispatch(ctx, "PreToolUse", { candidates: toolMatchCandidates(call.toolName) }, childPayload(call, "PreToolUse"));
418
535
  return {
@@ -422,7 +539,7 @@ export default function hooksExtension(pi: ExtensionAPI) {
422
539
  };
423
540
  },
424
541
  async postToolUse(result: ChildHookResult) {
425
- const ctx = lastCtx;
542
+ const ctx = lastCtx && childDispatchCtx(lastCtx, result);
426
543
  if (!ctx) return {};
427
544
  const payload: HookStdinPayload = {
428
545
  ...childPayload(result, "PostToolUse"),
@@ -449,6 +566,9 @@ export default function hooksExtension(pi: ExtensionAPI) {
449
566
  };
450
567
 
451
568
  pi.on("session_shutdown", (event, ctx) => {
569
+ // Also cancels a queued SessionStart before its deferred dispatch touches a disposed context.
570
+ sessionGen += 1;
571
+ sessionAbort.abort();
452
572
  // A reload re-runs the extensions but keeps the conversation (pi's
453
573
  // AgentSession.reload): not a session end, Claude Code has no such event.
454
574
  if (event.reason === "reload") return;
@@ -492,19 +612,40 @@ export default function hooksExtension(pi: ExtensionAPI) {
492
612
  });
493
613
 
494
614
  pi.on("agent_settled", async (_event, ctx) => {
615
+ const gen = sessionGen;
616
+ // A queued message pi never delivered as typed (expanded, or taken over by
617
+ // an extension) keeps no context for a later, unrelated prompt; an idle
618
+ // prompt that opened no run of its own (one handled after this extension)
619
+ // keeps none either.
620
+ promptContext.clear();
495
621
  // CC skips Stop when the turn ended on an API error (a blocking hook would
496
622
  // spiral: error → block → retry → error) or on a user interrupt (its query
497
623
  // loop returns on the abort signal before the stop-hook step). An empty latch
498
624
  // means no run ended in this turn, which is not a response to stop on either.
499
625
  if (stopOutcome.take() !== "ok") return;
500
- const payload: HookStdinPayload = { ...basePayload(ctx, "Stop"), stop_hook_active: stopHookActive };
626
+ // Claude Code's two lists of what will wake the session later, filled by the
627
+ // background extension (lib/session-work.ts); empty arrays when none.
628
+ const work: SessionWorkQuery = { crons: [], tasks: [] };
629
+ pi.events.emit(SESSION_WORK_CHANNEL, work);
630
+ const payload: HookStdinPayload = {
631
+ ...basePayload(ctx, "Stop"),
632
+ stop_hook_active: stopHookBlocks > 0,
633
+ background_tasks: work.tasks,
634
+ session_crons: work.crons,
635
+ };
501
636
  const outcome = await dispatch(ctx, "Stop", { ignoreMatcher: true }, payload);
502
- if (!outcome.block) return;
503
- stopHookActive = true;
637
+ if (gen !== sessionGen || outcome.preventContinuation || !outcome.block) return;
638
+ // Claude Code limits repeated Stop blocks to eight continuations. A hook
639
+ // that ignores stop_hook_active must not run up an unbounded model loop.
640
+ if (stopHookBlocks >= MAX_STOP_CONTINUATIONS) {
641
+ notify(ctx, `Stop hook continuation limit reached (${MAX_STOP_CONTINUATIONS}); ending the turn.`);
642
+ return;
643
+ }
644
+ stopHookBlocks += 1;
504
645
  try {
505
646
  pi.sendMessage(
506
647
  {
507
- customType: "one-code:hook-stop",
648
+ customType: STOP_CONTINUATION_TYPE,
508
649
  content: `Stop hook blocked stopping: ${outcome.block.reason}`,
509
650
  display: false,
510
651
  },
@@ -533,10 +674,19 @@ export default function hooksExtension(pi: ExtensionAPI) {
533
674
  notify(ctx, `Compaction cancelled by PreCompact hook: ${outcome.block.reason}`);
534
675
  return { cancel: true };
535
676
  }
677
+ // These instructions belong to the summarizer, not the next user turn.
678
+ // The signal keeps them scoped to this attempt if compaction is cancelled.
679
+ if (outcome.additionalContext) {
680
+ pi.events.emit(PRECOMPACT_INSTRUCTIONS_CHANNEL, {
681
+ signal: event.signal,
682
+ instructions: outcome.additionalContext,
683
+ } satisfies PreCompactInstructions);
684
+ }
536
685
  return undefined;
537
686
  });
538
687
 
539
688
  pi.on("session_compact", async (event, ctx) => {
689
+ const gen = sessionGen;
540
690
  const trigger = compactTrigger(event);
541
691
  const payload: HookStdinPayload = {
542
692
  ...basePayload(ctx, "PostCompact"),
@@ -544,7 +694,8 @@ export default function hooksExtension(pi: ExtensionAPI) {
544
694
  compact_summary: event.compactionEntry.summary,
545
695
  };
546
696
  const outcome = await dispatch(ctx, "PostCompact", { candidates: [trigger] }, payload);
547
- if (outcome.additionalContext) pendingPromptContext.push({ event: "PostCompact", text: outcome.additionalContext });
697
+ if (gen !== sessionGen) return;
698
+ if (outcome.additionalContext) deliverContext("PostCompact", outcome.additionalContext);
548
699
  // CC fires SessionStart(source: "compact") after compaction. Awaited inline
549
700
  // (not backgrounded), so the current generation is still in force.
550
701
  await dispatchSessionStart(ctx, "compact", sessionGen);
@@ -38,6 +38,9 @@ const CC_CANONICAL: Record<string, string> = {
38
38
  task_stop: "TaskStop",
39
39
  monitor: "Monitor",
40
40
  schedule_wakeup: "ScheduleWakeup",
41
+ cron_create: "CronCreate",
42
+ cron_list: "CronList",
43
+ cron_delete: "CronDelete",
41
44
  send_message: "SendMessage",
42
45
  enter_worktree: "EnterWorktree",
43
46
  exit_worktree: "ExitWorktree",
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Which delivered message takes a UserPromptSubmit hook's context. Pure (no pi
3
+ * imports).
4
+ *
5
+ * A message queued mid-turn delivers as typed, so the user message with its
6
+ * text is its own (lib/queued-delivery.ts). A prompt typed while idle is
7
+ * expanded by pi after the input handlers (a prompt template, a `/skill:` no
8
+ * extension took), and a later handler may transform it, so its message
9
+ * rarely carries the typed text. It is the next prompt pi delivers, though:
10
+ * the first user message no queued message claims takes it.
11
+ *
12
+ * A queued message is held even when its hook added no context, so its
13
+ * delivery claims nothing instead of taking an idle prompt's context.
14
+ */
15
+
16
+ import { QueuedDelivery } from "../lib/queued-delivery.ts";
17
+
18
+ export class PromptContextRouter {
19
+ /** Context per queued message; null marks one whose hook added none. */
20
+ private readonly queued = new QueuedDelivery<string | null>();
21
+ private idle: Array<{ text: string; payload: string }> = [];
22
+
23
+ /** A message queued mid-turn, with its hook's context or none. */
24
+ holdQueued(text: string, payload: string | undefined): void {
25
+ this.queued.hold(text, payload ?? null);
26
+ }
27
+
28
+ /** A prompt typed while idle and its hook's context. */
29
+ holdIdle(text: string, payload: string): void {
30
+ this.idle.push({ text, payload });
31
+ }
32
+
33
+ /** A new idle prompt: an earlier idle one was delivered or never will be, so its context is stale. */
34
+ dropIdle(): void {
35
+ this.idle = [];
36
+ }
37
+
38
+ get isEmpty(): boolean {
39
+ return this.queued.isEmpty && this.idle.length === 0;
40
+ }
41
+
42
+ /**
43
+ * The context for a delivered user message with this text: a queued message
44
+ * typed so claims its own (none, when its hook added none); else the idle
45
+ * prompt typed so; else, unless `exact`, the oldest idle prompt. Released once.
46
+ */
47
+ release(text: string, exact = false): string | undefined {
48
+ const queued = this.queued.release(text);
49
+ if (queued !== undefined) return queued ?? undefined;
50
+ const at = this.idle.findIndex((entry) => entry.text === text);
51
+ const index = at === -1 && !exact && this.idle.length > 0 ? 0 : at;
52
+ return index === -1 ? undefined : this.idle.splice(index, 1)[0].payload;
53
+ }
54
+
55
+ clear(): void {
56
+ this.queued.clear();
57
+ this.idle = [];
58
+ }
59
+ }