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
@@ -0,0 +1,344 @@
1
+ /**
2
+ * PowerShell's own parser as a service: one long-lived parse server per
3
+ * process, run on the PowerShell the tool itself runs, so the gate reads a
4
+ * line exactly as it will execute (decisions/windows.md "The PowerShell
5
+ * pre-gate reads PowerShell's own parse"; the tree-sitter grammars it replaces
6
+ * are measured in findings §41).
7
+ *
8
+ * {@link PowerShellParser.parse} never rejects. Anything short of a clean
9
+ * answer, whether the server could not start, timed out, crashed, runs in
10
+ * Constrained Language Mode or stopped for good after too many crashes,
11
+ * resolves to `{ ok: false, reason }`, and the caller treats the line as
12
+ * unparsed (the classifier decides). A parse with errors is still `ok: true`:
13
+ * the tree and its `errors` are PowerShell's answer; judging them is the
14
+ * caller's job.
15
+ *
16
+ * Lifecycle. The server starts on the first request and stops after
17
+ * `idleMs` without one; the next request starts it again. A crash, or a
18
+ * request that times out (the server is then killed), counts toward
19
+ * `maxRestarts`; past it the parser stays unavailable for the life of the
20
+ * process. The child, its pipes and every timer are unref'd, so an idle
21
+ * server never holds a finished one-shot run open; a caller that awaits a
22
+ * parse in one holds its own ref (`withKeepAlive`). The server also exits on
23
+ * its own when this process ends, because its stdin closes.
24
+ *
25
+ * Pure: no pi imports. The caller supplies `spawnServer`, which starts the
26
+ * right executable with the bootstrap after `-Command`, so the BI fork can use
27
+ * its own launcher.
28
+ */
29
+
30
+ import type { ChildProcess } from "node:child_process";
31
+ import { POWERSHELL_PARSE_BOOTSTRAP, POWERSHELL_PARSE_WALKER } from "./powershell-parse-script.ts";
32
+ import { unrefHandle } from "./process-tree.ts";
33
+
34
+ /** One syntax-tree node, as the walker reports it (powershell-parse-script.ts). */
35
+ export interface PowerShellAstNode {
36
+ /** The .NET type name: `CommandAst`, `StringConstantExpressionAst`, … */
37
+ type: string;
38
+ /** Index of the parent node in `nodes`; -1 for the root. */
39
+ parent: number;
40
+ /** Extent as UTF-16 offsets into the command (JS string indices). */
41
+ start: number;
42
+ end: number;
43
+ /** CommandAst: the static command name (null when not static). CommandParameterAst: the parameter. VariableExpressionAst: the variable path. Type nodes: the type's full name. */
44
+ name?: string | null;
45
+ /** String and constant nodes: the value PowerShell resolved (quotes and escapes applied). */
46
+ value?: string | null;
47
+ /** String nodes: `BareWord`, `SingleQuoted`, `DoubleQuoted`, `SingleQuotedHereString`, `DoubleQuotedHereString`. */
48
+ kind?: string;
49
+ /** CommandAst: the invocation operator (`Unknown`, `Ampersand`, `Dot`). Operator nodes: the operator. */
50
+ operator?: string;
51
+ /** VariableExpressionAst: `@name` splatting. */
52
+ splatted?: boolean;
53
+ /** Redirections: the stream redirected (`Output`, `Error`, `All`, …) and, for a merge, where to. */
54
+ from?: string;
55
+ to?: string;
56
+ /** FileRedirectionAst: `>>`. */
57
+ append?: boolean;
58
+ /** Pipelines and pipeline chains: a trailing `&` (PowerShell 7). */
59
+ background?: boolean;
60
+ /** Member access: `::`. */
61
+ static?: boolean;
62
+ /** CommandAst with a static name that resolves in this PowerShell: the resolved command's type (`Cmdlet`, `Application`, `Function`, …), after an alias. */
63
+ commandType?: string;
64
+ /** CommandAst: the name was an alias. */
65
+ alias?: boolean;
66
+ /** CommandAst: the resolved cmdlet's name, or an application's full path. */
67
+ resolvedName?: string;
68
+ /** CommandAst: the resolved command's module (empty for an application). */
69
+ module?: string;
70
+ /**
71
+ * CommandAst of a cmdlet: PowerShell's static parameter binding. `value` is
72
+ * the bound argument's node index, -1 for a switch given alone, and -2 for a
73
+ * value the binder built itself: the arguments a remaining-arguments
74
+ * parameter collects (`Write-Output a b`), whose node indices are
75
+ * `elements` (-1 for one that matched no argument).
76
+ */
77
+ bindings?: Array<{ parameter: string; value: number; elements?: number[] }>;
78
+ /** CommandAst of a cmdlet: the arguments or parameters the binder could not bind (an unknown or ambiguous name, a surplus positional, a repeat). */
79
+ bindingErrors?: string[];
80
+ /** CommandAst of a cmdlet: the static binder is missing or threw. */
81
+ bindingUnavailable?: boolean;
82
+ }
83
+
84
+ export interface PowerShellParse {
85
+ /** The tree in pre-order, root first. */
86
+ nodes: PowerShellAstNode[];
87
+ /** Each parse error's ErrorId. PowerShell runs nothing from a line that has one. */
88
+ errors: string[];
89
+ }
90
+
91
+ export type PowerShellParseOutcome = { ok: true; parse: PowerShellParse } | { ok: false; reason: string };
92
+
93
+ export interface PowerShellParserOptions {
94
+ /** Start the server: the PowerShell executable with `bootstrap` as the argument after `-Command`, stdio all piped. */
95
+ spawnServer: (bootstrap: string) => ChildProcess;
96
+ /** How long the first request waits while the server starts. */
97
+ startTimeoutMs?: number;
98
+ /** How long a request waits once the server is ready. */
99
+ requestTimeoutMs?: number;
100
+ /** Crashes and timeouts tolerated before the parser stays unavailable. */
101
+ maxRestarts?: number;
102
+ /** Stop the server after this long without a request. */
103
+ idleMs?: number;
104
+ }
105
+
106
+ export const POWERSHELL_PARSE_START_TIMEOUT_MS = 5_000;
107
+ export const POWERSHELL_PARSE_REQUEST_TIMEOUT_MS = 2_000;
108
+ export const POWERSHELL_PARSE_MAX_RESTARTS = 3;
109
+ export const POWERSHELL_PARSE_IDLE_MS = 10 * 60_000;
110
+
111
+ interface Pending {
112
+ resolve: (outcome: PowerShellParseOutcome) => void;
113
+ timer: ReturnType<typeof setTimeout>;
114
+ }
115
+
116
+ interface Server {
117
+ child: ChildProcess;
118
+ ready: boolean;
119
+ /** Set once the server is gone or being stopped; its late output and exit are ignored. */
120
+ stopped: boolean;
121
+ /** Why it stopped. */
122
+ reason?: string;
123
+ buffer: string;
124
+ pending: Map<number, Pending>;
125
+ }
126
+
127
+ const encode = (text: string) => Buffer.from(text, "utf8").toString("base64");
128
+
129
+ export class PowerShellParser {
130
+ private readonly options: Required<PowerShellParserOptions>;
131
+ private server: Server | undefined;
132
+ private nextId = 1;
133
+ private failures = 0;
134
+ /** Why the parser is unavailable for good; set past `maxRestarts`, or when the server runs in a restricted language mode. */
135
+ private disabled: string | undefined;
136
+ private idleTimer: ReturnType<typeof setTimeout> | undefined;
137
+
138
+ constructor(options: PowerShellParserOptions) {
139
+ this.options = {
140
+ startTimeoutMs: POWERSHELL_PARSE_START_TIMEOUT_MS,
141
+ requestTimeoutMs: POWERSHELL_PARSE_REQUEST_TIMEOUT_MS,
142
+ maxRestarts: POWERSHELL_PARSE_MAX_RESTARTS,
143
+ idleMs: POWERSHELL_PARSE_IDLE_MS,
144
+ ...options,
145
+ };
146
+ }
147
+
148
+ /** Why the parser cannot answer at all, or undefined while it can (or can still start). */
149
+ unavailable(): string | undefined {
150
+ return this.disabled;
151
+ }
152
+
153
+ /** Parse `command` with PowerShell's own parser. Never rejects. */
154
+ parse(command: string): Promise<PowerShellParseOutcome> {
155
+ if (this.disabled) return Promise.resolve({ ok: false, reason: this.disabled });
156
+ let server: Server;
157
+ try {
158
+ server = this.server ?? this.start();
159
+ } catch (error) {
160
+ return Promise.resolve({ ok: false, reason: this.fail(`PowerShell could not start: ${message(error)}`) });
161
+ }
162
+ // A server can fail while it starts (a spawn error, a restricted language
163
+ // mode) before this request is registered with it.
164
+ if (server.stopped) return Promise.resolve({ ok: false, reason: this.disabled ?? server.reason ?? "the PowerShell parser stopped" });
165
+ this.touch();
166
+ const id = this.nextId++;
167
+ return new Promise((resolve) => {
168
+ const timeoutMs = server.ready ? this.options.requestTimeoutMs : this.options.startTimeoutMs;
169
+ const timer = setTimeout(() => {
170
+ server.pending.delete(id);
171
+ resolve({ ok: false, reason: `the PowerShell parser did not answer within ${timeoutMs} ms` });
172
+ this.crashed(server, `the PowerShell parser timed out after ${timeoutMs} ms`);
173
+ }, timeoutMs);
174
+ unrefHandle(timer);
175
+ server.pending.set(id, { resolve, timer });
176
+ server.child.stdin?.write(`${id} ${encode(command)}\n`);
177
+ });
178
+ }
179
+
180
+ /** Stop the server. The next request starts a new one. */
181
+ stop(): void {
182
+ const server = this.server;
183
+ if (!server) return;
184
+ this.server = undefined;
185
+ this.clearIdle();
186
+ this.settleAll(server, "the PowerShell parser was stopped");
187
+ server.stopped = true;
188
+ server.child.stdin?.end();
189
+ server.child.kill();
190
+ }
191
+
192
+ private start(): Server {
193
+ const child = this.options.spawnServer(POWERSHELL_PARSE_BOOTSTRAP);
194
+ const server: Server = { child, ready: false, stopped: false, buffer: "", pending: new Map() };
195
+ this.server = server;
196
+ child.stdin?.on("error", () => {});
197
+ child.stdout?.setEncoding("ascii");
198
+ child.stdout?.on("data", (chunk: string) => this.receive(server, chunk));
199
+ // PowerShell's own error output is not part of the protocol; drain it so
200
+ // the pipe never fills.
201
+ child.stderr?.on("data", () => {});
202
+ child.on("error", (error) => this.crashed(server, `PowerShell could not start: ${message(error)}`));
203
+ child.on("exit", (code, signal) => this.crashed(server, `the PowerShell parser exited (${signal ?? `code ${code}`})`));
204
+ unrefHandle(child);
205
+ unrefHandle(child.stdin);
206
+ unrefHandle(child.stdout);
207
+ unrefHandle(child.stderr);
208
+ child.stdin?.write(`${encode(POWERSHELL_PARSE_WALKER)}\n`);
209
+ return server;
210
+ }
211
+
212
+ private receive(server: Server, chunk: string): void {
213
+ if (server.stopped) return;
214
+ server.buffer += chunk;
215
+ for (let newline = server.buffer.indexOf("\n"); newline >= 0; newline = server.buffer.indexOf("\n")) {
216
+ const line = server.buffer.slice(0, newline).trim();
217
+ server.buffer = server.buffer.slice(newline + 1);
218
+ if (line) this.record(server, line);
219
+ if (server.stopped) return;
220
+ }
221
+ }
222
+
223
+ private record(server: Server, line: string): void {
224
+ let record: Record<string, unknown>;
225
+ try {
226
+ record = JSON.parse(Buffer.from(line, "base64").toString("utf8")) as Record<string, unknown>;
227
+ } catch {
228
+ this.crashed(server, "the PowerShell parser wrote a line that is not part of its protocol");
229
+ return;
230
+ }
231
+ if (record.id === 0) {
232
+ if (record.language !== "FullLanguage") {
233
+ // Constrained Language Mode (or stricter) is a machine policy, so a
234
+ // restart cannot change it.
235
+ this.disabled = `PowerShell runs in ${String(record.language)} mode, where the parser is not available`;
236
+ this.crashed(server, this.disabled);
237
+ return;
238
+ }
239
+ // Failures are not reset here: a line that hangs the walker would
240
+ // otherwise restart it forever.
241
+ server.ready = true;
242
+ return;
243
+ }
244
+ const pending = typeof record.id === "number" ? server.pending.get(record.id) : undefined;
245
+ if (!pending) return;
246
+ server.pending.delete(record.id as number);
247
+ clearTimeout(pending.timer);
248
+ if (typeof record.failure === "string") {
249
+ pending.resolve({ ok: false, reason: `the PowerShell parser failed: ${record.failure}` });
250
+ return;
251
+ }
252
+ pending.resolve({ ok: true, parse: { nodes: asArray(record.nodes).map(normalizeNode), errors: asArray(record.errors).map(String) } });
253
+ }
254
+
255
+ /** The server is gone or unusable: settle what waits on it and count the failure. */
256
+ private crashed(server: Server, reason: string): void {
257
+ if (server.stopped) return;
258
+ server.stopped = true;
259
+ server.reason = reason;
260
+ if (this.server === server) this.server = undefined;
261
+ this.clearIdle();
262
+ this.settleAll(server, reason);
263
+ server.child.stdin?.end();
264
+ server.child.kill();
265
+ this.fail(reason);
266
+ }
267
+
268
+ private fail(reason: string): string {
269
+ this.failures++;
270
+ if (!this.disabled && this.failures > this.options.maxRestarts) {
271
+ this.disabled = `the PowerShell parser is off after ${this.failures} failures (last: ${reason})`;
272
+ }
273
+ return reason;
274
+ }
275
+
276
+ private settleAll(server: Server, reason: string): void {
277
+ for (const pending of server.pending.values()) {
278
+ clearTimeout(pending.timer);
279
+ pending.resolve({ ok: false, reason });
280
+ }
281
+ server.pending.clear();
282
+ }
283
+
284
+ private touch(): void {
285
+ this.clearIdle();
286
+ this.idleTimer = setTimeout(() => this.stop(), this.options.idleMs);
287
+ unrefHandle(this.idleTimer);
288
+ }
289
+
290
+ private clearIdle(): void {
291
+ if (this.idleTimer) clearTimeout(this.idleTimer);
292
+ this.idleTimer = undefined;
293
+ }
294
+ }
295
+
296
+ /**
297
+ * A node with its list fields made lists: ConvertTo-Json may write a
298
+ * one-element list as the element. A binding list that is not one after
299
+ * that marks the binding unavailable, which the gate refuses.
300
+ */
301
+ function normalizeNode(value: unknown): PowerShellAstNode {
302
+ const node = { ...(value as PowerShellAstNode) };
303
+ if (node.bindingErrors !== undefined) node.bindingErrors = asArray(node.bindingErrors).map(String);
304
+ if (node.bindings !== undefined) {
305
+ const bindings = asArray(node.bindings) as Array<{ parameter?: unknown; value?: unknown; elements?: unknown }>;
306
+ if (bindings.every((b) => b !== null && typeof b === "object" && typeof b.parameter === "string" && typeof b.value === "number")) {
307
+ node.bindings = bindings.map((b) => ({ parameter: b.parameter as string, value: b.value as number, ...(b.elements === undefined ? {} : { elements: asArray(b.elements).map(Number) }) }));
308
+ } else {
309
+ delete node.bindings;
310
+ node.bindingUnavailable = true;
311
+ }
312
+ }
313
+ return node;
314
+ }
315
+
316
+ function asArray(value: unknown): unknown[] {
317
+ if (Array.isArray(value)) return value;
318
+ // ConvertTo-Json writes a one-element array as that element on some
319
+ // PowerShell versions, and an empty one as null.
320
+ return value === undefined || value === null ? [] : [value];
321
+ }
322
+
323
+ function message(error: unknown): string {
324
+ return error instanceof Error ? error.message : String(error);
325
+ }
326
+
327
+ const STATE_KEY = Symbol.for("one-code.powershell-parser");
328
+
329
+ /**
330
+ * The process-wide parser for one PowerShell executable. It lives on
331
+ * `globalThis`, not in module state: jiti gives each extension, and each
332
+ * in-process child session, its own instance of this module (findings §3), and
333
+ * one server per process is the point.
334
+ */
335
+ export function sharedPowerShellParser(executable: string, create: () => PowerShellParser): PowerShellParser {
336
+ const store = globalThis as { [STATE_KEY]?: Map<string, PowerShellParser> };
337
+ const parsers = (store[STATE_KEY] ??= new Map());
338
+ let parser = parsers.get(executable);
339
+ if (!parser) {
340
+ parser = create();
341
+ parsers.set(executable, parser);
342
+ }
343
+ return parser;
344
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Keep the previous request's conversation breakpoint (pure).
3
+ *
4
+ * Anthropic finds an earlier cache entry only a limited way back from a
5
+ * breakpoint, about 20 content blocks, and pi moves the one conversation
6
+ * breakpoint to each request's tail. A request that adds many blocks since the
7
+ * last one (a large parallel tool batch), or a long run of `tool_addition`
8
+ * blocks after the session-facts system message, then cannot reach the entry
9
+ * the previous request wrote and re-caches the whole conversation (findings
10
+ * §72: a live pair read 11,707 of 24,837 tokens, and 24,837 with this mark).
11
+ * Marking the block the previous request marked again lets the server match
12
+ * that entry directly. It costs nothing: the entry exists, so the mark reads.
13
+ */
14
+
15
+ import { appendPoint } from "./ledger/wire.ts";
16
+ import { MAX_CACHE_MARKERS } from "./request-replay.ts";
17
+
18
+ type Block = Record<string, unknown> & { cache_control?: unknown };
19
+ type Message = { role?: unknown; content?: unknown };
20
+
21
+ /** Where a request's last conversation breakpoint sat, and the block it marked. */
22
+ export interface MessageMark {
23
+ message: number;
24
+ block: number;
25
+ marker: unknown;
26
+ /** The marked block itself: pi builds each request afresh but reuses large strings (an image's data), so a structural compare short-circuits on them. */
27
+ content: Block;
28
+ }
29
+
30
+ const blocksOf = (message: unknown): Block[] =>
31
+ Array.isArray((message as Message | null)?.content) ? ((message as Message).content as Block[]) : [];
32
+
33
+ /** Equal JSON values, ignoring a top-level `cache_control`. */
34
+ function sameBlock(a: Block, b: Block): boolean {
35
+ const keys = (block: Block) => Object.keys(block).filter((key) => key !== "cache_control");
36
+ const left = keys(a);
37
+ return left.length === keys(b).length && left.every((key) => sameValue(a[key], b[key]));
38
+ }
39
+ function sameValue(a: unknown, b: unknown): boolean {
40
+ if (a === b) return true;
41
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null || Array.isArray(a) !== Array.isArray(b)) return false;
42
+ const left = Object.keys(a);
43
+ return left.length === Object.keys(b).length && left.every((key) => sameValue((a as Record<string, unknown>)[key], (b as Record<string, unknown>)[key]));
44
+ }
45
+
46
+ /** The last breakpoint in `messages`, or undefined when none carries one. */
47
+ export function lastMessageMark(messages: unknown): MessageMark | undefined {
48
+ if (!Array.isArray(messages)) return undefined;
49
+ for (let message = messages.length - 1; message >= 0; message--) {
50
+ const blocks = blocksOf(messages[message]);
51
+ for (let block = blocks.length - 1; block >= 0; block--) {
52
+ const marker = blocks[block]?.cache_control;
53
+ if (marker !== undefined) return { message, block, marker, content: blocks[block] };
54
+ }
55
+ }
56
+ return undefined;
57
+ }
58
+
59
+ function marksIn(value: unknown): number {
60
+ if (!Array.isArray(value)) return 0;
61
+ return value.reduce((sum: number, entry) => {
62
+ const own = (entry as Block | null)?.cache_control !== undefined ? 1 : 0;
63
+ return sum + own + blocksOf(entry).filter((block) => block?.cache_control !== undefined).length;
64
+ }, 0);
65
+ }
66
+
67
+ /**
68
+ * The payload with `previous` marked again: only when that block is still
69
+ * there with the same bytes, no later block already took its place as the
70
+ * tail mark, and a breakpoint slot is free. Otherwise the payload unchanged.
71
+ */
72
+ export function withPreviousMark<T extends Record<string, unknown>>(payload: T, previous: MessageMark | undefined): T {
73
+ if (!previous || !Array.isArray(payload.messages)) return payload;
74
+ const messages = payload.messages as Message[];
75
+ const blocks = blocksOf(messages[previous.message]);
76
+ const target = blocks[previous.block];
77
+ if (!target || target.cache_control !== undefined || !sameBlock(target, previous.content)) return payload;
78
+ if (marksIn(payload.tools) + marksIn(payload.system) + marksIn(messages) >= MAX_CACHE_MARKERS) return payload;
79
+ const copy = [...messages];
80
+ const content = [...blocks];
81
+ content[previous.block] = { ...target, cache_control: previous.marker };
82
+ copy[previous.message] = { ...messages[previous.message], content };
83
+ return { ...payload, messages: copy };
84
+ }
85
+
86
+ /**
87
+ * The breakpoint the base request carried, located in `messages` (the request
88
+ * that extends it, spliced): the last block of the base's last element before
89
+ * its trailing directives, with the marker this request's own tail mark uses.
90
+ * Undefined when the request has no tail mark or the base is empty.
91
+ */
92
+ export function baseMark(messages: unknown, baseList: readonly unknown[], api: string): MessageMark | undefined {
93
+ if (!Array.isArray(messages)) return undefined;
94
+ const end = appendPoint(baseList, api);
95
+ const tail = lastMessageMark(messages);
96
+ if (end === 0 || !tail || tail.message < end) return undefined;
97
+ const blocks = blocksOf(messages[end - 1]);
98
+ const block = blocks.length - 1;
99
+ if (block < 0) return undefined;
100
+ return { message: end - 1, block, marker: tail.marker, content: blocks[block] };
101
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Process state: what lives as long as the One Code process and no longer
3
+ * (lifecycle-grid-hunt plan, part D). Every piece of state an extension holds
4
+ * is one of three kinds:
5
+ *
6
+ * - branch state follows the conversation: a resume, `/reload`, `/fork` and
7
+ * `/tree` each read it back from the branch (a `one-code:state` entry,
8
+ * lib/ledger/state.ts, or a fold of the branch's own entries);
9
+ * - process state survives `/reload` and the session swaps of `/clear`,
10
+ * `/new`, `/resume` and `/fork`, and ends with the process (a grant the
11
+ * user gave "for this session", a notice owed to the next session): this
12
+ * module;
13
+ * - resources (a timer, a child process, a connection) end with the instance
14
+ * that opened them, in `session_shutdown`.
15
+ *
16
+ * Module scope is none of them: pi re-evaluates every extension module at a
17
+ * `/reload` and at a `/resume` into another directory (findings §8), so a
18
+ * module-level value is lost there and kept everywhere else. The store here
19
+ * is a `globalThis` slot, which no re-evaluation resets.
20
+ *
21
+ * Pure: no pi imports.
22
+ */
23
+
24
+ const SLOT = Symbol.for("one-code:process-state");
25
+
26
+ type Store = Map<string, unknown>;
27
+
28
+ function store(): Store {
29
+ const slot = globalThis as { [SLOT]?: Store };
30
+ return (slot[SLOT] ??= new Map());
31
+ }
32
+
33
+ /**
34
+ * The process's value for `key`, created by `init` on first use. Keys are
35
+ * namespaced by their owner (`"permissions:grants"`); the value is shared by
36
+ * every instance of that owner in the process, so it must be mutated, not
37
+ * replaced.
38
+ */
39
+ export function processState<T>(key: string, init: () => T): T {
40
+ const values = store();
41
+ if (!values.has(key)) values.set(key, init());
42
+ return values.get(key) as T;
43
+ }
44
+
45
+ /** Forget every process value: a test's fresh process. */
46
+ export function resetProcessStateForTest(): void {
47
+ store().clear();
48
+ }
@@ -14,9 +14,22 @@
14
14
  * The producer spawns `detached` (own process group; `detachedSpawnOptions`)
15
15
  * and stops it with `killProcessTree`, which signals the negative pid — the
16
16
  * whole group — falling back to the child alone when the group is already gone.
17
- * `stopProcessTree` adds the grace period: SIGTERM now, SIGKILL if the leader
18
- * has not exited by then (a command that ignores SIGTERM still closes late,
19
- * which is exactly the late callback the shutdown paths guard against).
17
+ * `stopProcessTree` adds the grace period: SIGTERM now, SIGKILL to the group
18
+ * when it ends, whether or not the leader is still alive. The leader is often
19
+ * only the `bash -c` wrapper, which dies on TERM at once while the command it
20
+ * forked (a dev server in its graceful-shutdown handler) keeps the group and
21
+ * its port; a leader-only check skipped the SIGKILL exactly then
22
+ * (LIFECYCLE-BACKGROUND-REVIEW-2026-09-26 M1, measured). A group whose SIGKILL
23
+ * is still pending when the process exits (pi's quit calls `process.exit`,
24
+ * and the grace timer is unref'd) is SIGKILLed from a `process.on("exit")`
25
+ * hook, so nothing One Code stopped outlives it.
26
+ *
27
+ * `rememberProcessGroup` owns a background shell's group for the lifetime of
28
+ * this process, including descendants left by a successfully exited leader.
29
+ * Session replacement must not kill those intentionally launched daemons.
30
+ * Each module instance keeps its own groups through process listeners (never
31
+ * through a session context or cross-extension module state), reaps empty
32
+ * groups, and sends TERM synchronously on process exit or termination signals.
20
33
  *
21
34
  * `waitForChildExit` is the counterpart on the waiting side: it settles on
22
35
  * `exit` plus a short stdio grace, never on `close` alone — a descendant the
@@ -37,6 +50,16 @@ export const EXIT_STDIO_GRACE_MS = 200;
37
50
  /** ...and the most it may keep arriving in total: a straggler writing continuously must not keep re-arming the grace. */
38
51
  export const EXIT_STDIO_MAX_MS = 2_000;
39
52
 
53
+ /**
54
+ * Unref a child process, one of its stdio streams or a timer, so it never
55
+ * holds the event loop open on its own (a finished one-shot run can exit);
56
+ * a caller awaiting it holds its own ref. Child pipes are sockets at run
57
+ * time and carry `unref()` even though their stream types do not declare it.
58
+ */
59
+ export function unrefHandle(handle: unknown): void {
60
+ (handle as { unref?: () => void } | null | undefined)?.unref?.();
61
+ }
62
+
40
63
  /**
41
64
  * `detached` everywhere but Windows, where process groups do not exist — except
42
65
  * for a child that must outlive an exiting parent (a fire-and-forget hook),
@@ -95,20 +118,121 @@ export function killProcessTree(child: ChildProcess, signal: NodeJS.Signals = "S
95
118
  }
96
119
  }
97
120
 
121
+ /** Groups owned by this producer, retained by process listeners across session replacement. */
122
+ const rememberedGroups = new Set<ChildProcess>();
123
+ let groupReaper: NodeJS.Timeout | undefined;
124
+ const terminationSignals = ["SIGINT", "SIGTERM", "SIGHUP"] as const;
125
+ const groupSignalHandlers = new Map<NodeJS.Signals, () => void>();
126
+
127
+ /**
128
+ * Whether a remembered tree still needs killing at exit. Windows has no
129
+ * process groups, so only a live leader is worth a `taskkill /T`; it is judged
130
+ * by the child's own exit state, because once the leader exits Windows may
131
+ * hand its pid to an unrelated process that a pid probe would then find. On
132
+ * POSIX a group id stays reserved while any member lives, and the reaper
133
+ * forgets an emptied group within a second, far sooner than sequential pid
134
+ * allocation wraps around to it.
135
+ */
136
+ export function rememberedGroupAlive(child: ChildProcess): boolean {
137
+ if (child.pid == null) return false;
138
+ if (process.platform === "win32") return child.exitCode === null && child.signalCode === null;
139
+ try {
140
+ process.kill(-child.pid, 0);
141
+ return true;
142
+ } catch (error) {
143
+ return (error as NodeJS.ErrnoException).code !== "ESRCH";
144
+ }
145
+ }
146
+
147
+ function releaseGroupListeners(): void {
148
+ if (groupReaper) clearInterval(groupReaper);
149
+ groupReaper = undefined;
150
+ process.removeListener("exit", terminateRememberedGroups);
151
+ for (const [signal, handler] of groupSignalHandlers) process.removeListener(signal, handler);
152
+ groupSignalHandlers.clear();
153
+ }
154
+
155
+ /** Exit handlers cannot await a grace period. Existing explicit stops retain their TERM/KILL escalation below. */
156
+ function terminateRememberedGroups(): void {
157
+ for (const child of rememberedGroups) {
158
+ if (rememberedGroupAlive(child)) killProcessTree(child, "SIGTERM");
159
+ }
160
+ rememberedGroups.clear();
161
+ releaseGroupListeners();
162
+ }
163
+
98
164
  /**
99
- * SIGTERM the tree, then SIGKILL it after `graceMs` unless the leader exited.
100
- * The timer is unref'd: it must never be what keeps a one-shot process alive.
165
+ * Remember a detached background tree from spawn until its group becomes empty.
166
+ * Register before awaiting its leader, so even immediate process exit covers
167
+ * running work. The unref'd reaper cannot keep a print run alive. No session
168
+ * shutdown handler touches this set; the process listeners keep this module
169
+ * instance alive, and reach its set, even after pi evaluates the module again
170
+ * (a /reload, a /resume into another directory: findings §8).
171
+ */
172
+ export function rememberProcessGroup(child: ChildProcess): void {
173
+ if (child.pid == null || rememberedGroups.has(child)) return;
174
+ rememberedGroups.add(child);
175
+ if (groupReaper) return;
176
+ process.on("exit", terminateRememberedGroups);
177
+ for (const signal of terminationSignals) {
178
+ const handler = () => {
179
+ terminateRememberedGroups();
180
+ // Do not swallow Node's default signal termination when there is no
181
+ // pi handler. With isolated producers, the last listener re-raises it.
182
+ if (process.listenerCount(signal) === 0) process.kill(process.pid, signal);
183
+ };
184
+ groupSignalHandlers.set(signal, handler);
185
+ // Run before signal-exit observers: after we remove ourselves they must
186
+ // still see the original listener count and preserve default termination.
187
+ process.prependOnceListener(signal, handler);
188
+ }
189
+ groupReaper = setInterval(() => {
190
+ for (const child of rememberedGroups) {
191
+ if (!rememberedGroupAlive(child)) rememberedGroups.delete(child);
192
+ }
193
+ if (rememberedGroups.size === 0) releaseGroupListeners();
194
+ }, 1_000);
195
+ groupReaper.unref?.();
196
+ }
197
+
198
+ /**
199
+ * Trees sent SIGTERM whose SIGKILL has not run yet. Keyed by the child, not
200
+ * its pid, so a pid the OS hands to a new child during the grace does not
201
+ * make that child look already scheduled. Per module instance on purpose
202
+ * (each extension force-kills its own children); one `exit` listener per
203
+ * instance, installed on first use.
204
+ */
205
+ const pendingKills = new Set<ChildProcess>();
206
+ let exitHookInstalled = false;
207
+
208
+ /** The process is exiting: the grace timers will never fire, so force-kill every tree still pending. */
209
+ function killPendingAtExit(): void {
210
+ for (const child of pendingKills) killProcessTree(child, "SIGKILL");
211
+ pendingKills.clear();
212
+ }
213
+
214
+ /**
215
+ * SIGTERM the tree, then SIGKILL the whole group after `graceMs`, whatever
216
+ * the leader's state: a group already gone makes that an ESRCH, and a live
217
+ * group keeps its id reserved, so the id cannot name anyone else. The timer
218
+ * is unref'd (it must never be what keeps a one-shot process alive); a
219
+ * process exit before it fires runs the SIGKILL from the `exit` hook.
101
220
  */
102
221
  export function stopProcessTree(child: ChildProcess, graceMs: number): void {
103
222
  killProcessTree(child, "SIGTERM");
104
223
  // taskkill /F is already forceful; there is no gentler first signal to grace.
105
224
  if (process.platform === "win32") return;
106
- if (child.exitCode !== null || child.signalCode !== null) return;
225
+ if (child.pid == null || pendingKills.has(child)) return;
226
+ pendingKills.add(child);
227
+ if (!exitHookInstalled) {
228
+ exitHookInstalled = true;
229
+ process.on("exit", killPendingAtExit);
230
+ }
107
231
  const timer = setTimeout(() => {
108
- if (child.exitCode === null && child.signalCode === null) killProcessTree(child, "SIGKILL");
232
+ if (!pendingKills.delete(child)) return;
233
+ killProcessTree(child, "SIGKILL");
109
234
  }, graceMs);
110
235
  timer.unref?.();
111
- child.once("exit", () => clearTimeout(timer));
112
236
  }
113
237
 
114
238
  export interface ChildExit {