one-code-extension 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (419) hide show
  1. package/README.md +219 -219
  2. package/agents/general-purpose.md +2 -2
  3. package/docs/guide/README.md +2 -1
  4. package/docs/guide/bring-your-claude-code-setup.md +60 -9
  5. package/docs/guide/configuration.md +48 -14
  6. package/docs/guide/differences-from-claude-code.md +32 -23
  7. package/docs/guide/doctor.md +20 -13
  8. package/docs/guide/hooks.md +18 -3
  9. package/docs/guide/installation.md +27 -4
  10. package/docs/guide/permissions-modes-and-auto-mode.md +225 -49
  11. package/docs/guide/providers-and-models.md +137 -54
  12. package/docs/guide/reference.md +27 -15
  13. package/docs/guide/sessions-and-context.md +69 -16
  14. package/docs/guide/skills-plugins-and-mcp.md +57 -20
  15. package/docs/guide/subagents-and-workflows.md +95 -29
  16. package/docs/guide/tasks-and-background-work.md +101 -26
  17. package/docs/guide/terminal-interface.md +24 -12
  18. package/docs/guide/tools.md +46 -5
  19. package/docs/guide/troubleshooting.md +13 -9
  20. package/docs/guide/windows.md +66 -34
  21. package/extensions/artifacts/gallery.ts +227 -0
  22. package/extensions/artifacts/index.ts +288 -0
  23. package/extensions/artifacts/store.ts +570 -0
  24. package/extensions/artifacts/viewer.ts +199 -0
  25. package/extensions/ask-user/description.ts +67 -0
  26. package/extensions/ask-user/dialogs.ts +80 -0
  27. package/extensions/ask-user/index.ts +37 -33
  28. package/extensions/ask-user/questions.ts +32 -7
  29. package/extensions/ask-user/widget.ts +45 -25
  30. package/extensions/auto-mode/actions.ts +6 -0
  31. package/extensions/auto-mode/cache.ts +88 -0
  32. package/extensions/auto-mode/classifier-prompt.ts +5 -4
  33. package/extensions/auto-mode/classifier.ts +290 -65
  34. package/extensions/auto-mode/config.ts +72 -20
  35. package/extensions/auto-mode/decision-log.ts +2 -0
  36. package/extensions/auto-mode/defaults.ts +1 -1
  37. package/extensions/auto-mode/denials.ts +123 -0
  38. package/extensions/auto-mode/git-checkout-programs.ts +187 -0
  39. package/extensions/auto-mode/git-status-meta.ts +159 -0
  40. package/extensions/auto-mode/history.ts +189 -0
  41. package/extensions/auto-mode/instructions.ts +58 -52
  42. package/extensions/auto-mode/model-select.ts +159 -134
  43. package/extensions/auto-mode/paths.ts +57 -5
  44. package/extensions/auto-mode/pause.ts +57 -2
  45. package/extensions/auto-mode/prompt.ts +37 -21
  46. package/extensions/auto-mode/read-only-options.ts +724 -0
  47. package/extensions/auto-mode/resolved-paths-meta.ts +133 -0
  48. package/extensions/auto-mode/rules.ts +18 -1
  49. package/extensions/auto-mode/safety-floor.ts +361 -18
  50. package/extensions/auto-mode/setup-run.ts +20 -14
  51. package/extensions/auto-mode/shell-analysis.ts +764 -490
  52. package/extensions/auto-mode/shell-parse.ts +885 -0
  53. package/extensions/auto-mode/system-note.ts +33 -0
  54. package/extensions/auto-mode/transcript.ts +80 -51
  55. package/extensions/background/cron.ts +515 -0
  56. package/extensions/background/index.ts +514 -236
  57. package/extensions/background/loop-fire.ts +136 -0
  58. package/extensions/background/loop-preamble.ts +59 -0
  59. package/extensions/background/loop-skill.ts +163 -0
  60. package/extensions/background/monitor-batch.ts +53 -0
  61. package/extensions/background/noop-fold.ts +128 -0
  62. package/extensions/background/switch-guard.ts +52 -0
  63. package/extensions/background/wakeup.ts +159 -55
  64. package/extensions/bash/background.ts +53 -17
  65. package/extensions/bash/child.ts +7 -0
  66. package/extensions/bash/description.ts +188 -0
  67. package/extensions/bash/guards.ts +15 -67
  68. package/extensions/bash/index.ts +88 -35
  69. package/extensions/branding/compaction-keep.ts +63 -0
  70. package/extensions/branding/declined.ts +22 -0
  71. package/extensions/branding/index.ts +133 -9
  72. package/extensions/branding/prompt-editor.ts +53 -6
  73. package/extensions/branding/queued-edit.ts +31 -0
  74. package/extensions/branding/replaced-builtins.ts +68 -0
  75. package/extensions/branding/startup.ts +33 -71
  76. package/extensions/btw/index.ts +229 -71
  77. package/extensions/btw/panel.ts +228 -100
  78. package/extensions/btw/prompt.ts +41 -4
  79. package/extensions/claude-compat/index.ts +7 -11
  80. package/extensions/claude-context/index.ts +446 -58
  81. package/extensions/compaction/cut.ts +157 -0
  82. package/extensions/compaction/index.ts +216 -75
  83. package/extensions/compaction/kept-thinking.ts +62 -0
  84. package/extensions/compaction/prompt.ts +15 -12
  85. package/extensions/context-budget/index.ts +24 -5
  86. package/extensions/context-management/index.ts +25 -0
  87. package/extensions/doctor/build.ts +59 -8
  88. package/extensions/doctor/builtins.ts +84 -0
  89. package/extensions/doctor/cli.ts +7 -1
  90. package/extensions/doctor/compat.ts +88 -23
  91. package/extensions/doctor/dependencies.ts +41 -5
  92. package/extensions/doctor/fix-prompt.ts +9 -2
  93. package/extensions/doctor/index.ts +102 -58
  94. package/extensions/doctor/models.ts +102 -63
  95. package/extensions/doctor/presets.ts +42 -27
  96. package/extensions/doctor/report.ts +19 -2
  97. package/extensions/doctor/viewer.ts +49 -27
  98. package/extensions/effort/index.ts +72 -19
  99. package/extensions/effort/settings.ts +21 -0
  100. package/extensions/effort/slider.ts +24 -20
  101. package/extensions/exit/index.ts +32 -1
  102. package/extensions/file-tracker/index.ts +337 -26
  103. package/extensions/file-tracker/replay.ts +96 -10
  104. package/extensions/file-tracker/restore.ts +63 -0
  105. package/extensions/file-tracker/results.ts +66 -0
  106. package/extensions/file-tracker/shell-reads.ts +211 -0
  107. package/extensions/file-tracker/tracker.ts +32 -15
  108. package/extensions/footer/footer-line.ts +45 -24
  109. package/extensions/footer/index.ts +47 -2
  110. package/extensions/footer/pr.ts +34 -5
  111. package/extensions/hooks/compaction.ts +7 -0
  112. package/extensions/hooks/executor.ts +32 -30
  113. package/extensions/hooks/index.ts +198 -31
  114. package/extensions/hooks/matcher.ts +3 -0
  115. package/extensions/hooks/protocol.ts +41 -3
  116. package/extensions/hooks/settings.ts +50 -19
  117. package/extensions/hooks/subagent-bridge.ts +2 -0
  118. package/extensions/hooks/trust.ts +13 -5
  119. package/extensions/idle-compact/index.ts +119 -0
  120. package/extensions/idle-compact/policy.ts +143 -0
  121. package/extensions/init/index.ts +11 -10
  122. package/extensions/init/prompt.ts +25 -32
  123. package/extensions/interrupted/index.ts +13 -1
  124. package/extensions/kept-thinking/index.ts +27 -0
  125. package/extensions/lib/agent-cron.ts +130 -0
  126. package/extensions/lib/agent-loader.ts +15 -1
  127. package/extensions/lib/agent-view.ts +85 -0
  128. package/extensions/lib/anthropic-payload.ts +82 -1
  129. package/extensions/lib/anthropic-server-call.ts +230 -0
  130. package/extensions/lib/anthropic-server-tools.ts +485 -0
  131. package/extensions/lib/app-launch.mjs +94 -0
  132. package/extensions/lib/argument-hints.ts +190 -0
  133. package/extensions/lib/atomic-write.ts +6 -2
  134. package/extensions/lib/auto-memory.ts +75 -0
  135. package/extensions/lib/bash-parser.ts +84 -0
  136. package/extensions/lib/btw-fork.ts +93 -0
  137. package/extensions/lib/child-extensions.ts +42 -0
  138. package/extensions/lib/child-writes.ts +45 -0
  139. package/extensions/lib/claude-context.ts +515 -100
  140. package/extensions/lib/claude-external-includes.ts +68 -0
  141. package/extensions/lib/claude-id.ts +22 -0
  142. package/extensions/lib/claude-rules.ts +323 -0
  143. package/extensions/lib/claude-settings.ts +76 -4
  144. package/extensions/lib/compaction-boundary.ts +36 -0
  145. package/extensions/lib/compaction-keep.mjs +83 -0
  146. package/extensions/lib/config-mode.ts +112 -0
  147. package/extensions/lib/consent-dialogs.ts +63 -0
  148. package/extensions/lib/consent-preview.ts +5 -1
  149. package/extensions/lib/consent-stores.ts +48 -0
  150. package/extensions/lib/context-facts.ts +78 -0
  151. package/extensions/lib/context-stack.ts +81 -0
  152. package/extensions/lib/deferred.ts +219 -25
  153. package/extensions/lib/dock-rows.ts +60 -0
  154. package/extensions/lib/empty-tool-result.ts +27 -0
  155. package/extensions/lib/environment-block.ts +76 -0
  156. package/extensions/lib/fetch-timeout.ts +24 -6
  157. package/extensions/{system-prompt → lib}/git-status.ts +51 -13
  158. package/extensions/lib/git.ts +75 -13
  159. package/extensions/lib/grammars/LICENSE-tree-sitter-bash +21 -0
  160. package/extensions/lib/grammars/README.md +2 -0
  161. package/extensions/lib/grammars/tree-sitter-bash.wasm +0 -0
  162. package/extensions/lib/guide-docs.ts +45 -0
  163. package/extensions/lib/headless-output.ts +46 -0
  164. package/extensions/lib/hook-gate.ts +7 -7
  165. package/extensions/lib/interrupt.ts +20 -4
  166. package/extensions/lib/key-input.ts +455 -0
  167. package/extensions/lib/keys.ts +14 -1
  168. package/extensions/lib/local-command.ts +47 -13
  169. package/extensions/lib/mcp-overrides.ts +1 -1
  170. package/extensions/lib/mcp-share.ts +46 -0
  171. package/extensions/lib/memory-content.ts +146 -0
  172. package/extensions/lib/memory-index.ts +18 -0
  173. package/extensions/lib/memory.ts +134 -123
  174. package/extensions/lib/model-catalog/huggingface.json +174 -0
  175. package/extensions/lib/model-catalog/models-dev.json +18297 -0
  176. package/extensions/lib/model-catalog/openrouter.json +5252 -0
  177. package/extensions/lib/model-catalog-data.ts +513 -0
  178. package/extensions/lib/model-catalog.ts +462 -0
  179. package/extensions/lib/model-choice-warnings.ts +137 -0
  180. package/extensions/lib/model-pairings.ts +152 -0
  181. package/extensions/{auto-mode → lib}/model-picker.ts +26 -43
  182. package/extensions/lib/model-policy.ts +112 -15
  183. package/extensions/lib/model-tier.ts +283 -355
  184. package/extensions/lib/model-unusable.ts +1 -1
  185. package/extensions/lib/newer-model.ts +100 -0
  186. package/extensions/lib/notices.ts +87 -0
  187. package/extensions/lib/notifications.ts +308 -64
  188. package/extensions/lib/one-code-settings.ts +74 -8
  189. package/extensions/lib/open-browser.ts +63 -0
  190. package/extensions/lib/openrouter-generation.ts +94 -0
  191. package/extensions/lib/operation-deadline.ts +30 -0
  192. package/extensions/lib/original-command.ts +4 -4
  193. package/extensions/lib/package-version.ts +1 -1
  194. package/extensions/lib/panel-focus.ts +134 -0
  195. package/extensions/lib/paths.ts +17 -2
  196. package/extensions/lib/permission-gate.ts +72 -6
  197. package/extensions/lib/persisted-output.ts +60 -8
  198. package/extensions/lib/pi-ai-estimate.ts +19 -3
  199. package/extensions/lib/pi-version.ts +21 -2
  200. package/extensions/lib/plan-mode-channels.ts +20 -5
  201. package/extensions/lib/plugins.ts +26 -17
  202. package/extensions/lib/powershell-parse-script.ts +208 -0
  203. package/extensions/lib/powershell-parser.ts +344 -0
  204. package/extensions/lib/process-tree.ts +131 -8
  205. package/extensions/lib/prompt-options.ts +82 -0
  206. package/extensions/lib/queued-delivery.ts +44 -0
  207. package/extensions/lib/reminders.ts +364 -58
  208. package/extensions/lib/replaced-builtins.mjs +199 -0
  209. package/extensions/lib/replay-call.ts +70 -0
  210. package/extensions/lib/request-replay.ts +439 -0
  211. package/extensions/lib/scratchpad.ts +81 -14
  212. package/extensions/lib/send-now-channel.ts +7 -0
  213. package/extensions/lib/session-model-tier.ts +51 -0
  214. package/extensions/lib/session-work.ts +64 -0
  215. package/extensions/lib/settings-channels.ts +4 -4
  216. package/extensions/lib/shell-exit-semantics.ts +170 -0
  217. package/extensions/lib/shell-quote.ts +11 -0
  218. package/extensions/lib/shell-result.ts +70 -0
  219. package/extensions/lib/shell-spawn.ts +70 -6
  220. package/extensions/lib/shell-tool.ts +75 -25
  221. package/extensions/lib/side-call-cache.ts +124 -0
  222. package/extensions/lib/side-call-run.ts +96 -0
  223. package/extensions/lib/side-call-usage.ts +38 -0
  224. package/extensions/lib/side-call.ts +13 -1
  225. package/extensions/lib/single-flight.ts +18 -0
  226. package/extensions/lib/skill-body.ts +42 -0
  227. package/extensions/lib/skill-scan.ts +63 -24
  228. package/extensions/lib/spill-file.ts +140 -0
  229. package/extensions/lib/subagent-handback.ts +113 -0
  230. package/extensions/lib/system-role.ts +156 -0
  231. package/extensions/lib/terminal-text.ts +114 -0
  232. package/extensions/lib/text-width.ts +47 -16
  233. package/extensions/lib/timer-ops.ts +20 -0
  234. package/extensions/lib/tool-additions.ts +149 -0
  235. package/extensions/lib/tool-call-corruption.ts +134 -0
  236. package/extensions/lib/tool-path.ts +66 -0
  237. package/extensions/lib/tool-variants.ts +69 -0
  238. package/extensions/lib/tui-render.ts +255 -27
  239. package/extensions/lib/turn-budget-layout.ts +143 -0
  240. package/extensions/lib/update-check.mjs +15 -2
  241. package/extensions/lib/usage-bus.ts +1 -1
  242. package/extensions/lib/user-denial.ts +38 -0
  243. package/extensions/lib/workspace-channel.ts +16 -0
  244. package/extensions/lib/worktree-channel.ts +31 -3
  245. package/extensions/lib/worktree-isolation.ts +40 -10
  246. package/extensions/lsp/client.ts +8 -9
  247. package/extensions/lsp/index.ts +70 -4
  248. package/extensions/lsp/keep-alive.ts +1 -1
  249. package/extensions/lsp/trust.ts +119 -0
  250. package/extensions/lsp/watcher.ts +1 -1
  251. package/extensions/mcp/announce.ts +87 -5
  252. package/extensions/mcp/client.ts +161 -48
  253. package/extensions/mcp/config.ts +132 -8
  254. package/extensions/mcp/discovery.ts +19 -0
  255. package/extensions/mcp/index.ts +209 -71
  256. package/extensions/mcp/oauth/browser.ts +38 -17
  257. package/extensions/mcp/oauth/flow.ts +25 -17
  258. package/extensions/mcp/oauth/provider.ts +14 -8
  259. package/extensions/mcp/oauth/store.ts +34 -16
  260. package/extensions/mcp/panel/keys.ts +12 -11
  261. package/extensions/mcp/panel/render.ts +9 -6
  262. package/extensions/mcp/schema.ts +104 -5
  263. package/extensions/mcp/trust.ts +151 -67
  264. package/extensions/memory/entries.ts +23 -14
  265. package/extensions/memory/index.ts +134 -28
  266. package/extensions/memory/open-external.ts +33 -18
  267. package/extensions/memory/panel.ts +135 -26
  268. package/extensions/newer-model/index.ts +31 -0
  269. package/extensions/notebook/description.ts +18 -0
  270. package/extensions/notebook/index.ts +6 -5
  271. package/extensions/notebook/notebook.ts +1 -1
  272. package/extensions/permissions/index.ts +1300 -381
  273. package/extensions/permissions/matcher.ts +425 -74
  274. package/extensions/permissions/modes.ts +6 -4
  275. package/extensions/permissions/panel/host.ts +130 -0
  276. package/extensions/permissions/panel/keys.ts +48 -0
  277. package/extensions/permissions/panel/render.ts +450 -0
  278. package/extensions/permissions/panel/state.ts +550 -0
  279. package/extensions/permissions/powershell-cmdlets.ts +198 -0
  280. package/extensions/permissions/powershell-paths.ts +197 -0
  281. package/extensions/permissions/powershell-rules.ts +207 -280
  282. package/extensions/permissions/powershell-tree.ts +557 -0
  283. package/extensions/permissions/project-trust.ts +27 -11
  284. package/extensions/permissions/protected-paths.ts +14 -2
  285. package/extensions/permissions/session-grant.ts +6 -0
  286. package/extensions/permissions/settings.ts +166 -31
  287. package/extensions/permissions/subagent-gate.ts +25 -0
  288. package/extensions/permissions/workspace.ts +78 -0
  289. package/extensions/plan-mode/index.ts +156 -69
  290. package/extensions/plan-mode/reminder.ts +89 -21
  291. package/extensions/plan-mode/texts.ts +177 -0
  292. package/extensions/plan-mode/viewer.ts +92 -27
  293. package/extensions/plugins/counts.ts +3 -3
  294. package/extensions/plugins/index.ts +61 -46
  295. package/extensions/plugins/install/install.ts +16 -0
  296. package/extensions/plugins/marketplace/sync.ts +9 -3
  297. package/extensions/plugins/marketplace/types.ts +3 -1
  298. package/extensions/plugins/panel/keys.ts +21 -24
  299. package/extensions/plugins/panel/render.ts +12 -6
  300. package/extensions/plugins/shell-expand.ts +107 -0
  301. package/extensions/plugins/template.ts +32 -15
  302. package/extensions/powershell/child.ts +7 -0
  303. package/extensions/powershell/description.ts +38 -6
  304. package/extensions/powershell/guards.ts +1 -1
  305. package/extensions/powershell/index.ts +12 -7
  306. package/extensions/recap/index.ts +65 -60
  307. package/extensions/recap/prompt.ts +2 -2
  308. package/extensions/recap/scheduler.ts +3 -5
  309. package/extensions/search-tools/grep-merge.ts +39 -0
  310. package/extensions/search-tools/index.ts +0 -0
  311. package/extensions/send-now/chord.ts +62 -0
  312. package/extensions/send-now/index.ts +133 -0
  313. package/extensions/session-title/index.ts +3 -0
  314. package/extensions/session-title/title.ts +4 -1
  315. package/extensions/skill/code-review.ts +97 -0
  316. package/extensions/skill/index.ts +421 -101
  317. package/extensions/skill/invoke.ts +128 -18
  318. package/extensions/skill/listing.ts +133 -9
  319. package/extensions/skill/panel/keys.ts +15 -16
  320. package/extensions/skill/panel/render.ts +5 -2
  321. package/extensions/skill/panel/state.ts +12 -1
  322. package/extensions/subagents/agent-args.ts +39 -0
  323. package/extensions/subagents/agent-description.ts +132 -0
  324. package/extensions/subagents/agents.ts +116 -23
  325. package/extensions/subagents/announce.ts +69 -0
  326. package/extensions/subagents/default-model.ts +2 -3
  327. package/extensions/subagents/delegation-steer.ts +4 -4
  328. package/extensions/subagents/fork-cache.ts +76 -0
  329. package/extensions/subagents/guide-agent.ts +189 -0
  330. package/extensions/subagents/hand-back-review.ts +3 -0
  331. package/extensions/subagents/index.ts +1493 -824
  332. package/extensions/subagents/live-runs.ts +26 -6
  333. package/extensions/subagents/model-select.ts +38 -25
  334. package/extensions/subagents/outcome.ts +2 -2
  335. package/extensions/subagents/panel-keys.ts +73 -31
  336. package/extensions/subagents/panel-render.ts +120 -96
  337. package/extensions/subagents/panel-widget.ts +60 -89
  338. package/extensions/subagents/runner.ts +108 -80
  339. package/extensions/subagents/runs.ts +39 -5
  340. package/extensions/subagents/send-to-main-tool.ts +1 -1
  341. package/extensions/subagents/session-turns.ts +32 -8
  342. package/extensions/subagents/shell-panel.ts +36 -178
  343. package/extensions/subagents/tasks-dialog.ts +170 -0
  344. package/extensions/subagents/viewport-mirror.ts +95 -0
  345. package/extensions/subagents/worktree.ts +57 -13
  346. package/extensions/system-prompt/environment.ts +19 -19
  347. package/extensions/system-prompt/idle-turn.ts +65 -0
  348. package/extensions/system-prompt/index.ts +131 -47
  349. package/extensions/system-prompt/template.ts +24 -50
  350. package/extensions/system-prompt/tiers/common.ts +23 -64
  351. package/extensions/system-prompt/tiers/frontier.ts +32 -11
  352. package/extensions/system-prompt/tiers/low.ts +67 -45
  353. package/extensions/system-prompt/tiers/mid.ts +96 -38
  354. package/extensions/system-reminder/index.ts +175 -13
  355. package/extensions/tasks/descriptions.ts +190 -0
  356. package/extensions/tasks/index.ts +120 -41
  357. package/extensions/tasks/store.ts +83 -16
  358. package/extensions/tool-call-corruption/child.ts +7 -0
  359. package/extensions/tool-call-corruption/index.ts +237 -0
  360. package/extensions/tool-search/announce.ts +16 -0
  361. package/extensions/tool-search/index.ts +189 -41
  362. package/extensions/tool-style/index.ts +68 -10
  363. package/extensions/tool-style/write-description.ts +40 -0
  364. package/extensions/tool-style/write-render.ts +237 -0
  365. package/extensions/turn-duration/index.ts +7 -3
  366. package/extensions/turn-duration/line.ts +47 -4
  367. package/extensions/web/backends.ts +6 -9
  368. package/extensions/web/description.ts +50 -0
  369. package/extensions/web/index.ts +118 -53
  370. package/extensions/web/native-call.ts +15 -0
  371. package/extensions/web/native-results.ts +41 -0
  372. package/extensions/web-fetch/description.ts +42 -0
  373. package/extensions/web-fetch/extract.ts +53 -6
  374. package/extensions/web-fetch/index.ts +125 -76
  375. package/extensions/web-fetch/page-content.ts +19 -0
  376. package/extensions/web-fetch/response.ts +49 -0
  377. package/extensions/web-fetch/summarize.ts +9 -10
  378. package/extensions/workflow/agent-session.ts +105 -18
  379. package/extensions/workflow/args.ts +24 -0
  380. package/extensions/workflow/description.ts +67 -0
  381. package/extensions/workflow/globals.ts +17 -3
  382. package/extensions/workflow/index.ts +145 -162
  383. package/extensions/workflow/records.ts +1 -0
  384. package/extensions/workflow/run-manager.ts +29 -6
  385. package/extensions/workflow/saved-workflows.ts +3 -2
  386. package/extensions/workflow/types.ts +22 -1
  387. package/extensions/workflow/viewer.ts +54 -24
  388. package/extensions/workflow/widget.ts +20 -0
  389. package/extensions/worktree/descriptions.ts +91 -0
  390. package/extensions/worktree/guards.ts +316 -54
  391. package/extensions/worktree/index.ts +121 -39
  392. package/extensions/worktree/powershell-guards.ts +167 -0
  393. package/extensions/worktree/rewrite.ts +14 -16
  394. package/package.json +19 -7
  395. package/skills/artifact-design/SKILL.md +85 -0
  396. package/skills/code-review/SKILL.md +20 -20
  397. package/skills/code-review/cells/arguments-and-flags.md +19 -0
  398. package/skills/code-review/cells/high-3x5-recall-verify.md +137 -0
  399. package/skills/code-review/cells/high-8-inline.md +122 -0
  400. package/skills/code-review/cells/low-1-pass-max4.md +27 -0
  401. package/skills/code-review/cells/low-1-pass-min-files-4.md +28 -0
  402. package/skills/code-review/cells/max-5x5-verify-sweep.md +162 -0
  403. package/skills/code-review/cells/medium-3x5-verify.md +132 -0
  404. package/skills/code-review/cells/minimal-prompt.md +32 -0
  405. package/skills/code-review/cells/xhigh-10-inline-sweep.md +149 -0
  406. package/skills/code-review/cells/xhigh-5x5-verify-sweep.md +162 -0
  407. package/skills/fewer-permission-prompts/SKILL.md +1 -1
  408. package/skills/loop/SKILL.md +11 -0
  409. package/skills/security-review/SKILL.md +2 -5
  410. package/skills/simplify/SKILL.md +7 -5
  411. package/skills/workflow-authoring/SKILL.md +143 -0
  412. package/themes/onecode-light.json +1 -1
  413. package/themes/onecode.json +1 -1
  414. package/types/app.d.ts +58 -0
  415. package/types/community-packages.d.ts +26 -1
  416. package/extensions/auto-mode/recoverability.ts +0 -229
  417. package/extensions/lib/capability-index.ts +0 -466
  418. package/extensions/lib/model-facts.generated.json +0 -2675
  419. package/extensions/lib/model-facts.ts +0 -162
@@ -8,7 +8,9 @@
8
8
  * a PR whose head lives in the base repo, so it misses the common case of a PR
9
9
  * opened from a fork (`isCrossRepository`), even though `list` on the base repo
10
10
  * knows about it. `--state open` keeps a merged/closed PR for the same branch
11
- * name from showing.
11
+ * name from showing. The footer repeats the lookup once a minute while the
12
+ * session is in use (`nextPrPollDelay`), so a PR merged, closed or opened
13
+ * mid-session shows within a minute.
12
14
  *
13
15
  * The exec lives here but the parse is split out so it unit-tests without a
14
16
  * subprocess.
@@ -35,19 +37,46 @@ export function parsePrNumber(stdout: string): number | undefined {
35
37
 
36
38
  const LOOKUP_TIMEOUT_MS = 4000;
37
39
 
40
+ /** A lookup's outcome: the open PR, if any, and whether `gh` is not installed at all. */
41
+ export interface PrLookup {
42
+ pr: number | undefined;
43
+ ghMissing: boolean;
44
+ }
45
+
38
46
  /**
39
- * Resolve the open PR number for `branch` in `cwd`, or `undefined`. Never
40
- * rejects: any failure (no gh, no PR, detached HEAD, timeout) is `undefined`.
47
+ * Resolve the open PR number for `branch` in `cwd`. Never rejects: any failure
48
+ * (no gh, no PR, detached HEAD, timeout) has `pr` undefined, and a `gh` that
49
+ * is not on PATH says so, so the footer stops polling for it.
41
50
  */
42
- export function fetchPrNumber(cwd: string, branch: string): Promise<number | undefined> {
51
+ export function fetchPrNumber(cwd: string, branch: string): Promise<PrLookup> {
43
52
  return new Promise((resolve) => {
44
53
  execFile(
45
54
  "gh",
46
55
  ["pr", "list", "--head", branch, "--state", "open", "--json", "number"],
47
56
  { cwd, timeout: LOOKUP_TIMEOUT_MS, encoding: "utf8" },
48
57
  (error, stdout) => {
49
- resolve(error ? undefined : parsePrNumber(stdout));
58
+ const ghMissing = (error as NodeJS.ErrnoException | null)?.code === "ENOENT";
59
+ resolve({ pr: error ? undefined : parsePrNumber(stdout), ghMissing });
50
60
  },
51
61
  );
52
62
  });
53
63
  }
64
+
65
+ /** Claude Code's PR status cadence (its `usePrStatus`): one lookup a minute while the session is in use. */
66
+ export const PR_POLL_INTERVAL_MS = 60_000;
67
+ /** Polling stops after this long without user input, and resumes on the next input. */
68
+ export const PR_IDLE_STOP_MS = 60 * 60_000;
69
+ /** A lookup slower than this turns polling off for the session (branch changes still look up). */
70
+ export const PR_SLOW_LOOKUP_MS = 4_000;
71
+
72
+ /**
73
+ * When the next PR lookup is due, relative to the last one: never sooner than
74
+ * PR_POLL_INTERVAL_MS after it, so a turn boundary asking for a refresh does
75
+ * not spawn `gh` more than once a minute. Undefined when polling should stop:
76
+ * turned off by a slow lookup, or no input for PR_IDLE_STOP_MS.
77
+ */
78
+ export function nextPrPollDelay(state: { lastLookupAt: number; lastInputAt: number; disabled: boolean }, now: number): number | undefined {
79
+ if (state.disabled) return undefined;
80
+ if (now - state.lastInputAt >= PR_IDLE_STOP_MS) return undefined;
81
+ return Math.max(0, state.lastLookupAt + PR_POLL_INTERVAL_MS - now);
82
+ }
@@ -0,0 +1,7 @@
1
+ /** Successful PreCompact hook output, scoped to the compaction that requested it. */
2
+ export const PRECOMPACT_INSTRUCTIONS_CHANNEL = "one-code:precompact-instructions";
3
+
4
+ export interface PreCompactInstructions {
5
+ signal: AbortSignal;
6
+ instructions: string;
7
+ }
@@ -7,7 +7,7 @@
7
7
  * pi.exec is deliberately not used: it hardcodes shell:false and gives the
8
8
  * child no stdin, while Claude Code hooks are shell command strings that read
9
9
  * a JSON payload from stdin. (Ported from pi-code's runHookCommand, MIT — see
10
- * docs/decisions.md.)
10
+ * working-docs/decisions.md.)
11
11
  *
12
12
  * The interpreter is Claude Code's: bash (Git Bash on Windows, honouring
13
13
  * `CLAUDE_CODE_GIT_BASH_PATH`) unless the hook says `shell: "powershell"`, or
@@ -22,7 +22,8 @@
22
22
  * otherwise `close` never fires and the promise hangs past the timeout
23
23
  * (Windows: `taskkill /T /F` on the leader's pid does the same)
24
24
  * - setEncoding("utf8") so multi-byte characters can't be split across chunks
25
- * - output capped at 1MB per stream
25
+ * - a hook exceeding 1MB per stream is killed and reported as an output-limit
26
+ * failure, never parsed as a silently truncated JSON verdict
26
27
  * - stdin errors ignored (a hook that exits without reading stdin — `exit 2`
27
28
  * — would otherwise EPIPE-crash the write)
28
29
  * - stdin JSON is newline-terminated: without a trailing "\n" a hook doing
@@ -41,13 +42,14 @@
41
42
  * SIGCHLD-driven exit callback; with the process handle unref'd the loop
42
43
  * counted itself empty and node exited 0 mid-tool, silently, before the
43
44
  * hook's `close` ever fired — measured at ~55% of runs with a trivial
44
- * `exit 0` hook (docs/one-shot-lsp-event-loop-drain.md has the same
45
+ * `exit 0` hook (working-docs/one-shot-lsp-event-loop-drain.md has the same
45
46
  * mechanism for the LSP client). A ref'd child holds the loop until `close`.
46
47
  */
47
48
 
48
49
  import type { ChildProcess } from "node:child_process";
49
50
  import { detachedSpawnOptions, killProcessTree, waitForChildExit } from "../lib/process-tree.ts";
50
51
  import { bashSpawn, POWERSHELL_UTF8_PREFIX, powerShellSpawn, type ShellSpawn, spawnShellCommand } from "../lib/shell-spawn.ts";
52
+ import type { FinishedRun } from "./protocol.ts";
51
53
  import type { HookShell } from "./settings.ts";
52
54
 
53
55
  /**
@@ -76,29 +78,12 @@ export function hookShellSpawn(shell: HookShell | undefined): { spec?: ShellSpaw
76
78
  return { spec: resolved.spawn, shell: want };
77
79
  }
78
80
 
79
- export interface HookRunResult {
80
- /**
81
- * null when the process was killed (timeout) or never spawned. Normalized on
82
- * the timeout path rather than taken from `waitpid`: a group SIGKILL is not
83
- * atomic, so the shell can be scheduled after its foreground child is killed
84
- * and before its own signal lands, reap the child and exit(128+9) itself —
85
- * `close` then reports a normal exit of 137 instead of death by signal
86
- * (roughly 1% of timeouts under load, and never when the shell had exec'd
87
- * away, leaving no shell to reap; findings §10.20). Callers should still
88
- * prefer `timedOut`, which says what happened rather than what it looked
89
- * like, but they no longer have to.
90
- */
91
- exitCode: number | null;
92
- timedOut: boolean;
93
- /** Set when the child could not be spawned at all. */
94
- spawnError?: string;
95
- stdout: string;
96
- stderr: string;
97
- durationMs: number;
98
- }
81
+ /** A finished run (`protocol.ts FinishedRun`, field docs there) and how long it took. */
82
+ export type HookRunResult = FinishedRun & { durationMs: number };
99
83
 
100
84
  export interface HookRunOptions {
101
85
  cwd: string;
86
+ signal?: AbortSignal;
102
87
  /** Seconds, Claude Code convention. Clamped to [1, MAX_TIMEOUT_S]. */
103
88
  timeoutSeconds?: number;
104
89
  /** Exposed to the hook as CLAUDE_PROJECT_DIR; defaults to cwd. */
@@ -129,6 +114,10 @@ export function runHookCommand(command: string, stdinJson: string, opts: HookRun
129
114
  const started = Date.now();
130
115
 
131
116
  return new Promise((resolve) => {
117
+ if (opts.signal?.aborted) {
118
+ resolve({ exitCode: null, timedOut: false, aborted: true, stdout: "", stderr: "", durationMs: Date.now() - started });
119
+ return;
120
+ }
132
121
  let child: ChildProcess;
133
122
  const failed = (spawnError: string) =>
134
123
  resolve({ exitCode: null, timedOut: false, spawnError, stdout: "", stderr: "", durationMs: Date.now() - started });
@@ -175,14 +164,26 @@ export function runHookCommand(command: string, stdinJson: string, opts: HookRun
175
164
  let stdout = "";
176
165
  let stderr = "";
177
166
  let timedOut = false;
167
+ let aborted = false;
168
+ let outputLimitExceeded = false;
169
+ const outputBytes = { stdout: 0, stderr: 0 };
178
170
  let settled = false;
171
+ const onAbort = () => {
172
+ aborted = true;
173
+ killProcessTree(child, "SIGKILL");
174
+ };
175
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
179
176
 
180
177
  const capture = (sink: "stdout" | "stderr") => (chunk: string) => {
181
- const current = sink === "stdout" ? stdout : stderr;
182
- if (current.length >= MAX_OUTPUT_BYTES) return;
183
- const next = current + chunk.slice(0, MAX_OUTPUT_BYTES - current.length);
184
- if (sink === "stdout") stdout = next;
185
- else stderr = next;
178
+ if (outputLimitExceeded) return;
179
+ outputBytes[sink] += Buffer.byteLength(chunk, "utf8");
180
+ if (outputBytes[sink] > MAX_OUTPUT_BYTES) {
181
+ outputLimitExceeded = true;
182
+ killProcessTree(child, "SIGKILL");
183
+ return;
184
+ }
185
+ if (sink === "stdout") stdout += chunk;
186
+ else stderr += chunk;
186
187
  };
187
188
  child.stdout?.setEncoding("utf8");
188
189
  child.stderr?.setEncoding("utf8");
@@ -200,7 +201,8 @@ export function runHookCommand(command: string, stdinJson: string, opts: HookRun
200
201
  if (settled) return;
201
202
  settled = true;
202
203
  clearTimeout(timer);
203
- resolve({ ...result, durationMs: Date.now() - started });
204
+ opts.signal?.removeEventListener("abort", onAbort);
205
+ resolve({ ...result, ...(aborted ? { aborted: true } : {}), ...(outputLimitExceeded ? { outputLimitExceeded: true } : {}), durationMs: Date.now() - started });
204
206
  };
205
207
 
206
208
  // Settles on exit plus a short stdio grace, not on `close`: a grandchild
@@ -211,7 +213,7 @@ export function runHookCommand(command: string, stdinJson: string, opts: HookRun
211
213
  // keeps "we killed it" from ever looking like an ordinary non-zero exit,
212
214
  // which fails OPEN downstream.
213
215
  waitForChildExit(child).then(
214
- ({ code }) => finish({ exitCode: timedOut ? null : code, timedOut, stdout, stderr }),
216
+ ({ code }) => finish({ exitCode: timedOut || aborted || outputLimitExceeded ? null : code, timedOut, stdout, stderr }),
215
217
  (error: Error) => finish({ exitCode: null, timedOut, spawnError: error.message, stdout, stderr }),
216
218
  );
217
219
 
@@ -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
@@ -38,12 +38,15 @@
38
38
  * (settings, consent, logging) and a payload naming the child.
39
39
  */
40
40
 
41
+ import { contentText } from "@earendil-works/pi-ai";
41
42
  import { getAgentDir, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
42
43
  import { RunOutcomeLatch } from "../lib/interrupt.ts";
44
+ import { SESSION_WORK_CHANNEL, type SessionWorkQuery } from "../lib/session-work.ts";
43
45
  import { claudeConfigDir } from "../lib/paths.ts";
44
46
  import { defaultDiscoverRoots } from "../lib/plugins.ts";
45
47
  import { appendHookLog, formatDebugLine, hooksDebugEnabled, hooksLogPath } from "./debug.ts";
46
48
  import { runHookCommand } from "./executor.ts";
49
+ import { PRECOMPACT_INSTRUCTIONS_CHANNEL, type PreCompactInstructions } from "./compaction.ts";
47
50
  import { changedSince, FORMATTER_NOTICE, type FileSnapshot, fileToolTarget, snapshotFile } from "./formatter-notice.ts";
48
51
  import { matcherApplies, ccToolName, toolMatchCandidates, ccToolInput, nativeToolInput } from "./matcher.ts";
49
52
  import { loadPluginHooks } from "./plugin-hooks.ts";
@@ -52,6 +55,7 @@ import {
52
55
  type CcHookEvent,
53
56
  type HookOutcome,
54
57
  type HookStdinPayload,
58
+ cancelledOutcome,
55
59
  interpretHookResult,
56
60
  } from "./protocol.ts";
57
61
  import { type HookCommand, type HooksSource, loadHookSettings } from "./settings.ts";
@@ -59,6 +63,12 @@ import { persistIfLarge, sessionResultsDir } from "../lib/persisted-output.ts";
59
63
  import { REMINDER_CHANNEL, wrapReminder } from "../lib/reminders.ts";
60
64
  import { type ChildHookCall, type ChildHookResult, type HookBridge, SUBAGENT_HOOK_CHANNEL, type SubagentHookPayload } from "./subagent-bridge.ts";
61
65
  import { projectHooksApproved } from "./trust.ts";
66
+ import { QueuedDelivery } from "../lib/queued-delivery.ts";
67
+ import { SKILL_INVOCATION_TYPE, type SkillInvocationDetails } from "../skill/invoke.ts";
68
+ import { isNotificationDetails } from "../lib/notifications.ts";
69
+ import { consentDialog } from "../lib/consent-dialogs.ts";
70
+ import { isUserRun } from "../lib/prompt-options.ts";
71
+ import { followWorkCwd } from "../lib/worktree-channel.ts";
62
72
 
63
73
  /** Claude Code's `hook_additional_context` attachment text (utils/messages.ts). */
64
74
  export function hookContextText(event: CcHookEvent, text: string): string {
@@ -95,17 +105,39 @@ interface HookDispatchCtx {
95
105
  cwd: string;
96
106
  hasUI: boolean;
97
107
  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 };
108
+ ui: { confirm(title: string, message: string, options?: { signal?: AbortSignal }): Promise<boolean | undefined>; notify(message: string, type: "info"): void };
109
+ signal?: AbortSignal;
99
110
  /** Set on the shutdown snapshot: no consent prompt, no notices — the session is gone. */
100
111
  sessionEnded?: true;
101
112
  }
102
113
 
114
+ /** Claude Code's default cap on consecutive Stop-hook continuations in one user turn. */
115
+ const MAX_STOP_CONTINUATIONS = 8;
116
+
117
+ /** The customType of the Stop hook's continuation message. */
118
+ const STOP_CONTINUATION_TYPE = "one-code:hook-stop";
119
+
120
+ /** Whether a `before_agent_start` opens the Stop hook's own continuation run (pi 1.2.0 fires it for custom runs). */
121
+ function isStopContinuation(event: unknown): boolean {
122
+ const run = event as { messageType?: string; message?: { customType?: string } };
123
+ return run.messageType === "custom" && run.message?.customType === STOP_CONTINUATION_TYPE;
124
+ }
125
+
103
126
  export default function hooksExtension(pi: ExtensionAPI) {
104
- let stopHookActive = false;
127
+ const workCwd = followWorkCwd(pi.events);
128
+ let stopHookBlocks = 0;
105
129
  /** Context from UserPromptSubmit / SessionStart / PostCompact hooks, delivered with the next prompt. */
106
130
  let pendingPromptContext: Array<{ event: CcHookEvent; text: string }> = [];
131
+ /**
132
+ * UserPromptSubmit context for messages queued mid-turn, held until pi
133
+ * delivers the message (lib/queued-delivery.ts): such a message never
134
+ * reaches before_agent_start.
135
+ */
136
+ const queuedPromptContext = new QueuedDelivery<string>();
107
137
  /** The last context seen, for bridged child calls (dispatched parent-side). */
108
138
  let lastCtx: ExtensionContext | undefined;
139
+ /** True once a prompt opened a turn in this session (before_agent_start). */
140
+ let prompted = false;
109
141
  /**
110
142
  * The in-flight SessionStart hook dispatch. It runs OFF the `session_start`
111
143
  * path so the editor opens immediately (a slow hook used to hold the prompt
@@ -116,6 +148,8 @@ export default function hooksExtension(pi: ExtensionAPI) {
116
148
  let sessionStartPending: Promise<void> | undefined;
117
149
  /** Bumped on every session_start; a superseded dispatch checks it before writing context. */
118
150
  let sessionGen = 0;
151
+ /** Covers startup hooks and other dispatches that have no active turn signal. */
152
+ let sessionAbort = new AbortController();
119
153
 
120
154
  const notify = (ctx: HookDispatchCtx, message: string) => {
121
155
  if (ctx.sessionEnded) return; // session gone; nowhere to show it
@@ -131,7 +165,7 @@ export default function hooksExtension(pi: ExtensionAPI) {
131
165
  ): Promise<MatchedHook[]> => {
132
166
  const claudeDir = claudeConfigDir();
133
167
  const loaded = loadHookSettings(claudeDir, ctx.cwd);
134
- const pluginSources = loadPluginHooks(defaultDiscoverRoots(getAgentDir(), ctx.cwd), loaded.diagnostics);
168
+ const pluginSources = loaded.disabled ? [] : loadPluginHooks(defaultDiscoverRoots(getAgentDir(), ctx.cwd), loaded.diagnostics);
135
169
  if (hooksDebugEnabled()) {
136
170
  for (const diagnostic of loaded.diagnostics) process.stderr.write(`[hooks] ${diagnostic}\n`);
137
171
  }
@@ -143,7 +177,8 @@ export default function hooksExtension(pi: ExtensionAPI) {
143
177
  projectAllowed = await projectHooksApproved(ctx.cwd, projectSources, {
144
178
  hasUI: ctx.hasUI,
145
179
  noPrompt: ctx.sessionEnded,
146
- confirm: (title, message) => ctx.ui.confirm(title, message),
180
+ signal: ctx.signal,
181
+ 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
182
  notify: (message) => notify(ctx, message),
148
183
  });
149
184
  }
@@ -168,7 +203,7 @@ export default function hooksExtension(pi: ExtensionAPI) {
168
203
  const basePayload = (ctx: HookDispatchCtx, event: CcHookEvent): HookStdinPayload => ({
169
204
  session_id: ctx.sessionManager.getSessionId(),
170
205
  transcript_path: ctx.sessionManager.getSessionFile() ?? "",
171
- cwd: ctx.cwd,
206
+ cwd: workCwd(ctx.cwd),
172
207
  hook_event_name: event,
173
208
  });
174
209
 
@@ -181,20 +216,27 @@ export default function hooksExtension(pi: ExtensionAPI) {
181
216
  ctx: HookDispatchCtx,
182
217
  event: CcHookEvent,
183
218
  matchValue: { candidates?: string[]; ignoreMatcher?: boolean },
184
- payload: HookStdinPayload,
219
+ payloadOrFactory: HookStdinPayload | (() => Promise<HookStdinPayload | undefined>),
185
220
  /** Runs once, only when hooks will actually run — the point where a caller can snapshot state the hooks may change. */
186
221
  willRun?: () => void,
187
222
  ): Promise<HookOutcome> => {
188
223
  const merged: HookOutcome = {};
224
+ const signal = ctx.sessionEnded ? ctx.signal : ctx.signal ? AbortSignal.any([ctx.signal, sessionAbort.signal]) : sessionAbort.signal;
189
225
  try {
190
- const hooks = await collectHooks(ctx, event, matchValue);
191
- if (hooks.length === 0) return merged;
226
+ const hooks = await collectHooks({ ...ctx, signal }, event, matchValue);
227
+ const payload = typeof payloadOrFactory === "function" ? await payloadOrFactory() : payloadOrFactory;
228
+ if (!payload || hooks.length === 0) return merged;
229
+ // Cancelled before the hooks ran: the gating events block, as a mid-run cancel does.
230
+ if (signal?.aborted) return cancelledOutcome(event);
192
231
  willRun?.();
193
232
  const stdin = JSON.stringify(payload);
194
- const outcomes = await Promise.all(
233
+ const outcomes = await Promise.allSettled(
195
234
  hooks.map(async ({ source, hook }) => {
196
235
  const run = await runHookCommand(hook.command, stdin, {
197
- cwd: ctx.cwd,
236
+ // The calling session's worktree or child's directory; settings,
237
+ // consent and CLAUDE_PROJECT_DIR still belong to the parent project.
238
+ cwd: payload.cwd,
239
+ signal,
198
240
  timeoutSeconds: hook.timeout,
199
241
  shell: hook.shell,
200
242
  projectDir: ctx.cwd,
@@ -221,7 +263,16 @@ export default function hooksExtension(pi: ExtensionAPI) {
221
263
  return outcome;
222
264
  }),
223
265
  );
224
- for (const outcome of outcomes) {
266
+ if (signal?.aborted) return cancelledOutcome(event);
267
+ for (const result of outcomes) {
268
+ // One broken response must not erase another hook's deny or let the
269
+ // tool run while the other hooks are still deciding.
270
+ if (result.status === "rejected") {
271
+ if (hooksDebugEnabled()) process.stderr.write(`[hooks] hook error (${event}): ${String(result.reason)}\n`);
272
+ continue;
273
+ }
274
+ const outcome = result.value;
275
+ if (outcome.preventContinuation) merged.preventContinuation = true;
225
276
  // A block reason becomes model-facing text (the tool result, or a
226
277
  // Stop follow-up), so it is bounded like the other hook texts —
227
278
  // stderr on an exit-2 hook is easily a full stack trace (review M2).
@@ -291,7 +342,7 @@ export default function hooksExtension(pi: ExtensionAPI) {
291
342
  // to it: this extension awaits the hook and loads before file-tracker, so
292
343
  // the tracker records the hook's version as the model's own write
293
344
  // (formatter-notice.ts, review M1). Snapshot only when hooks will run.
294
- const target = event.isError ? undefined : fileToolTarget(event.toolName, event.input, ctx.cwd);
345
+ const target = event.isError ? undefined : fileToolTarget(event.toolName, event.input, payload.cwd);
295
346
  let before: FileSnapshot | undefined;
296
347
  const outcome = await dispatch(ctx, "PostToolUse", { candidates: toolMatchCandidates(event.toolName) }, payload, () => {
297
348
  if (target) before = snapshotFile(target);
@@ -305,57 +356,119 @@ export default function hooksExtension(pi: ExtensionAPI) {
305
356
  [outcome.additionalContext && wrapReminder(hookContextText("PostToolUse", outcome.additionalContext)), formatterNotice]
306
357
  .filter(Boolean)
307
358
  .join("\n") || undefined,
308
- });
359
+ }, event.toolName);
309
360
  if (!content) return undefined;
310
361
  return { content, isError: event.isError };
311
362
  });
312
363
 
313
- /** Await the backgrounded SessionStart dispatch once, if one is pending. */
364
+ /**
365
+ * Await the backgrounded SessionStart dispatch, if one is pending. Every
366
+ * caller awaits the same dispatch, and it is forgotten only once it settles:
367
+ * a notification opening the first turn and a prompt typed meanwhile both
368
+ * wait for it, rather than the second returning at once.
369
+ */
314
370
  const drainSessionStart = async () => {
315
- if (!sessionStartPending) return;
316
371
  const pending = sessionStartPending;
317
- sessionStartPending = undefined;
372
+ if (!pending) return;
318
373
  try {
319
374
  await pending;
320
375
  } catch {
321
376
  // dispatch already reports its own failures; never block the turn on one.
322
377
  }
378
+ if (sessionStartPending === pending) sessionStartPending = undefined;
323
379
  };
324
380
 
325
381
  // ---- UserPromptSubmit ---------------------------------------------------
326
382
  pi.on("input", async (event, ctx) => {
327
383
  if (event.source === "extension") return undefined;
328
- stopHookActive = false;
384
+ // Set for a message queued mid-turn (pi 0.86+ fires `input` for
385
+ // steer/followUp). The Stop latch is reset in before_agent_start, where a
386
+ // turn really starts: pi re-checks streaming after these handlers, so a
387
+ // queued message whose turn ended during the hook opens a new turn.
388
+ const queued = event.streamingBehavior;
389
+ const gen = sessionGen;
329
390
  // Drain the backgrounded SessionStart dispatch before this prompt's own
330
391
  // UserPromptSubmit context, so their order in the first turn is unchanged
331
392
  // (SessionStart context precedes UserPromptSubmit). before_agent_start is
332
393
  // the backstop for turns with no input event (one-shot, programmatic).
333
394
  await drainSessionStart();
395
+ if (gen !== sessionGen) return { action: "handled" as const };
334
396
  const payload: HookStdinPayload = { ...basePayload(ctx, "UserPromptSubmit"), prompt: event.text };
335
397
  const outcome = await dispatch(ctx, "UserPromptSubmit", { ignoreMatcher: true }, payload);
398
+ if (gen !== sessionGen) return { action: "handled" as const };
336
399
  if (outcome.block) {
337
400
  notify(ctx, `Prompt blocked by UserPromptSubmit hook: ${outcome.block.reason}`);
338
401
  return { action: "handled" as const };
339
402
  }
340
- if (outcome.additionalContext) pendingPromptContext.push({ event: "UserPromptSubmit", text: outcome.additionalContext });
403
+ if (!outcome.additionalContext) return undefined;
404
+ // A queued message's context waits for pi to deliver that message
405
+ // (message_start below), so it rides the same request.
406
+ if (queued) queuedPromptContext.hold(event.text, hookContextText("UserPromptSubmit", outcome.additionalContext));
407
+ else pendingPromptContext.push({ event: "UserPromptSubmit", text: outcome.additionalContext });
341
408
  return undefined;
342
409
  });
343
410
 
411
+ // pi awaits this before the request that carries the delivered message, so a
412
+ // one-shot emitted here is pinned to that message: after the prompt, as the
413
+ // before_agent_start message is for a prompt that opens a turn.
414
+ pi.on("message_start", async (event) => {
415
+ const gen = sessionGen;
416
+ const message = event.message;
417
+ const emit = (text: string) => pi.events.emit(REMINDER_CHANNEL, { text, placement: "last-append" });
418
+ if (message.role === "user") {
419
+ if (queuedPromptContext.isEmpty) return;
420
+ const text = queuedPromptContext.release(contentText(message.content, ""));
421
+ if (text) emit(text);
422
+ return;
423
+ }
424
+ // A `/skill:` command the skill extension took over arrives as its hidden
425
+ // message, carrying the typed text. It opens the turn when the command
426
+ // was idle, through a sendMessage that never reaches before_agent_start,
427
+ // so the prompt context waiting there rides this message too.
428
+ if (message.role !== "custom") return;
429
+ // A background completion that opens the session's first turn
430
+ // (lib/prompt-options.ts) skips before_agent_start as well, so the
431
+ // SessionStart context a typed first prompt would carry rides it.
432
+ const opensFirstTurn = !prompted && isNotificationDetails(message.details);
433
+ if (message.customType !== SKILL_INVOCATION_TYPE && !opensFirstTurn) return;
434
+ if (opensFirstTurn) await drainSessionStart();
435
+ if (gen !== sessionGen) return;
436
+ const input = (message.details as Partial<SkillInvocationDetails> | undefined)?.input;
437
+ const queued = typeof input === "string" ? queuedPromptContext.release(input) : undefined;
438
+ if (queued) emit(queued);
439
+ const waiting = pendingPromptContext;
440
+ pendingPromptContext = [];
441
+ for (const { event: hookEvent, text } of waiting) emit(hookContextText(hookEvent, text));
442
+ });
443
+
344
444
  // Prompt/session/compaction hook context rides the turn it belongs to as a
345
445
  // custom message pi appends right AFTER the user's prompt (the
346
446
  // before_agent_start `message` return). An idle `sendMessage` from here
347
447
  // landed it BEFORE the prompt (pi appends first, then adds the user message).
348
- pi.on("before_agent_start", async (_event, ctx) => {
448
+ pi.on("before_agent_start", async (event, ctx) => {
449
+ const gen = sessionGen;
349
450
  lastCtx = ctx;
451
+ prompted = true;
350
452
  // The SessionStart dispatch runs in the background from session_start; the
351
453
  // first turn must wait for it so its additionalContext is present (and, on
352
454
  // first run, so its approval dialog gates execution before the model runs).
353
455
  // The `input` handler usually drained it already; this is the backstop for
354
456
  // turns with no input event.
355
457
  await drainSessionStart();
356
- if (pendingPromptContext.length === 0) return;
357
- const texts = pendingPromptContext;
358
- pendingPromptContext = [];
458
+ if (gen !== sessionGen) return;
459
+ // Every run that opens lands here (on pi 1.2.0 also one an extension
460
+ // opened; a queued delivery does not). Only the Stop hook's own
461
+ // continuation keeps the cap counting: a notification or scheduled run
462
+ // starts a fresh turn, as a prompt does.
463
+ if (!isStopContinuation(event)) stopHookBlocks = 0;
464
+ // UserPromptSubmit context belongs to the user's prompt; SessionStart and
465
+ // PostCompact context belong to whichever run opens next, which on pi
466
+ // 1.2.0 can be one a notification or scheduled fire opened. Its
467
+ // message_start can no longer deliver them: `prompted` is set above.
468
+ const userRun = isUserRun(event);
469
+ const texts = pendingPromptContext.filter((entry) => userRun || entry.event !== "UserPromptSubmit");
470
+ if (texts.length === 0) return;
471
+ pendingPromptContext = pendingPromptContext.filter((entry) => !texts.includes(entry));
359
472
  return {
360
473
  message: {
361
474
  customType: "one-code:hook-context",
@@ -367,8 +480,13 @@ export default function hooksExtension(pi: ExtensionAPI) {
367
480
 
368
481
  // ---- SessionStart / SessionEnd -----------------------------------------
369
482
  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);
483
+ // Reserve project-hook consent synchronously, before MCP and external
484
+ // imports enqueue their dialogs. Only defer the payload: worktree's later
485
+ // synchronous session_start handler must restore cwd before we capture it.
486
+ const outcome = await dispatch(ctx, "SessionStart", { candidates: [source] }, async () => {
487
+ await new Promise<void>((resolve) => setImmediate(resolve));
488
+ return gen === sessionGen ? { ...basePayload(ctx, "SessionStart"), source } : undefined;
489
+ });
372
490
  // A later session_start (rapid /clear, resume) may have superseded this one
373
491
  // while its hook ran; discard the stale result instead of pushing it into
374
492
  // the new session's context (`pendingPromptContext` is shared and reset).
@@ -378,10 +496,15 @@ export default function hooksExtension(pi: ExtensionAPI) {
378
496
  };
379
497
 
380
498
  pi.on("session_start", (event, ctx) => {
499
+ sessionAbort.abort();
500
+ sessionAbort = new AbortController();
381
501
  lastCtx = ctx;
382
502
  const gen = ++sessionGen;
383
503
  pendingPromptContext = [];
504
+ queuedPromptContext.clear();
384
505
  sessionStartPending = undefined;
506
+ prompted = false;
507
+ stopHookBlocks = 0;
385
508
  // Publish the child hook bridge (subagent-bridge.ts). The closures read
386
509
  // live parent state per call, so once per session start is enough.
387
510
  pi.events.emit(SUBAGENT_HOOK_CHANNEL, { bridge: childHookBridge } satisfies SubagentHookPayload);
@@ -410,9 +533,21 @@ export default function hooksExtension(pi: ExtensionAPI) {
410
533
  agent_id: call.sessionId,
411
534
  agent_type: call.agentType,
412
535
  });
536
+ /**
537
+ * The parent's settings/consent context with the CHILD's run signal. A
538
+ * background child outlives the parent's turn, so Esc in the parent must
539
+ * not skip a child's deny hook; stopping the child must cancel its hooks.
540
+ */
541
+ const childDispatchCtx = (ctx: ExtensionContext, call: ChildHookCall): HookDispatchCtx => ({
542
+ cwd: ctx.cwd,
543
+ hasUI: ctx.hasUI,
544
+ sessionManager: ctx.sessionManager,
545
+ ui: ctx.ui,
546
+ signal: call.signal,
547
+ });
413
548
  const childHookBridge: HookBridge = {
414
549
  async preToolUse(call) {
415
- const ctx = lastCtx;
550
+ const ctx = lastCtx && childDispatchCtx(lastCtx, call);
416
551
  if (!ctx) return {};
417
552
  const outcome = await dispatch(ctx, "PreToolUse", { candidates: toolMatchCandidates(call.toolName) }, childPayload(call, "PreToolUse"));
418
553
  return {
@@ -422,7 +557,7 @@ export default function hooksExtension(pi: ExtensionAPI) {
422
557
  };
423
558
  },
424
559
  async postToolUse(result: ChildHookResult) {
425
- const ctx = lastCtx;
560
+ const ctx = lastCtx && childDispatchCtx(lastCtx, result);
426
561
  if (!ctx) return {};
427
562
  const payload: HookStdinPayload = {
428
563
  ...childPayload(result, "PostToolUse"),
@@ -449,6 +584,9 @@ export default function hooksExtension(pi: ExtensionAPI) {
449
584
  };
450
585
 
451
586
  pi.on("session_shutdown", (event, ctx) => {
587
+ // Also cancels a queued SessionStart before its deferred dispatch touches a disposed context.
588
+ sessionGen += 1;
589
+ sessionAbort.abort();
452
590
  // A reload re-runs the extensions but keeps the conversation (pi's
453
591
  // AgentSession.reload): not a session end, Claude Code has no such event.
454
592
  if (event.reason === "reload") return;
@@ -492,19 +630,38 @@ export default function hooksExtension(pi: ExtensionAPI) {
492
630
  });
493
631
 
494
632
  pi.on("agent_settled", async (_event, ctx) => {
633
+ const gen = sessionGen;
634
+ // A queued message pi never delivered as typed (expanded, or taken over by
635
+ // an extension) keeps no context for a later, unrelated prompt.
636
+ queuedPromptContext.clear();
495
637
  // CC skips Stop when the turn ended on an API error (a blocking hook would
496
638
  // spiral: error → block → retry → error) or on a user interrupt (its query
497
639
  // loop returns on the abort signal before the stop-hook step). An empty latch
498
640
  // means no run ended in this turn, which is not a response to stop on either.
499
641
  if (stopOutcome.take() !== "ok") return;
500
- const payload: HookStdinPayload = { ...basePayload(ctx, "Stop"), stop_hook_active: stopHookActive };
642
+ // Claude Code's two lists of what will wake the session later, filled by the
643
+ // background extension (lib/session-work.ts); empty arrays when none.
644
+ const work: SessionWorkQuery = { crons: [], tasks: [] };
645
+ pi.events.emit(SESSION_WORK_CHANNEL, work);
646
+ const payload: HookStdinPayload = {
647
+ ...basePayload(ctx, "Stop"),
648
+ stop_hook_active: stopHookBlocks > 0,
649
+ background_tasks: work.tasks,
650
+ session_crons: work.crons,
651
+ };
501
652
  const outcome = await dispatch(ctx, "Stop", { ignoreMatcher: true }, payload);
502
- if (!outcome.block) return;
503
- stopHookActive = true;
653
+ if (gen !== sessionGen || outcome.preventContinuation || !outcome.block) return;
654
+ // Claude Code limits repeated Stop blocks to eight continuations. A hook
655
+ // that ignores stop_hook_active must not run up an unbounded model loop.
656
+ if (stopHookBlocks >= MAX_STOP_CONTINUATIONS) {
657
+ notify(ctx, `Stop hook continuation limit reached (${MAX_STOP_CONTINUATIONS}); ending the turn.`);
658
+ return;
659
+ }
660
+ stopHookBlocks += 1;
504
661
  try {
505
662
  pi.sendMessage(
506
663
  {
507
- customType: "one-code:hook-stop",
664
+ customType: STOP_CONTINUATION_TYPE,
508
665
  content: `Stop hook blocked stopping: ${outcome.block.reason}`,
509
666
  display: false,
510
667
  },
@@ -533,10 +690,19 @@ export default function hooksExtension(pi: ExtensionAPI) {
533
690
  notify(ctx, `Compaction cancelled by PreCompact hook: ${outcome.block.reason}`);
534
691
  return { cancel: true };
535
692
  }
693
+ // These instructions belong to the summarizer, not the next user turn.
694
+ // The signal keeps them scoped to this attempt if compaction is cancelled.
695
+ if (outcome.additionalContext) {
696
+ pi.events.emit(PRECOMPACT_INSTRUCTIONS_CHANNEL, {
697
+ signal: event.signal,
698
+ instructions: outcome.additionalContext,
699
+ } satisfies PreCompactInstructions);
700
+ }
536
701
  return undefined;
537
702
  });
538
703
 
539
704
  pi.on("session_compact", async (event, ctx) => {
705
+ const gen = sessionGen;
540
706
  const trigger = compactTrigger(event);
541
707
  const payload: HookStdinPayload = {
542
708
  ...basePayload(ctx, "PostCompact"),
@@ -544,6 +710,7 @@ export default function hooksExtension(pi: ExtensionAPI) {
544
710
  compact_summary: event.compactionEntry.summary,
545
711
  };
546
712
  const outcome = await dispatch(ctx, "PostCompact", { candidates: [trigger] }, payload);
713
+ if (gen !== sessionGen) return;
547
714
  if (outcome.additionalContext) pendingPromptContext.push({ event: "PostCompact", text: outcome.additionalContext });
548
715
  // CC fires SessionStart(source: "compact") after compaction. Awaited inline
549
716
  // (not backgrounded), so the current generation is still in force.
@@ -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",