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
@@ -0,0 +1,439 @@
1
+ /**
2
+ * Replaying the session's last provider request with a few messages appended,
3
+ * so a side call (btw, recap, compaction) reads the prompt cache the session
4
+ * wrote. Claude Code sends these calls the same way: the main request's
5
+ * `system`, `tools` and `messages` unchanged, the final assistant reply, then
6
+ * the side prompt (working-docs/decisions/caching.md "Side calls replay the last
7
+ * request").
8
+ *
9
+ * The request cannot be rebuilt from pi's APIs: tool-search reshapes the wire
10
+ * `tools` array and context-management edits the body inside pi's request
11
+ * pipeline, which `completeSimple` bypasses, and one differing byte in `tools`
12
+ * or `system` misses the whole cache. So the compaction extension (the last to
13
+ * see `before_provider_request`) publishes the final payload on
14
+ * LAST_REQUEST_CHANNEL, and a side call sends it back with its tail spliced on
15
+ * through `completeSimple`'s `onPayload`. pi-ai converts the tail itself (the
16
+ * call's own context is just the tail), so its wire shape matches the rest by
17
+ * construction.
18
+ *
19
+ * Pure: no pi extension API; only pi-ai types and the vendored estimator.
20
+ */
21
+
22
+ import type { Message } from "@earendil-works/pi-ai";
23
+ import { estimateMessageTokens } from "./pi-ai-estimate.ts";
24
+
25
+ /** The compaction extension publishes a RequestCapture here, or undefined when none is valid. */
26
+ export const LAST_REQUEST_CHANNEL = "one-code:last-request";
27
+
28
+ /** The provider APIs whose request body can be extended by appending to one list. */
29
+ export type ReplayApi = "anthropic-messages" | "openai-completions" | "openai-responses";
30
+
31
+ const LIST_KEY: Record<ReplayApi, string> = {
32
+ "anthropic-messages": "messages",
33
+ "openai-completions": "messages",
34
+ "openai-responses": "input",
35
+ };
36
+
37
+ /** The output cap's field, first match in the captured body wins. */
38
+ const MAX_TOKEN_KEYS: Record<ReplayApi, readonly string[]> = {
39
+ "anthropic-messages": ["max_tokens"],
40
+ "openai-completions": ["max_completion_tokens", "max_tokens"],
41
+ "openai-responses": ["max_output_tokens"],
42
+ };
43
+
44
+ /** The session's last provider request body, and the model it went to. */
45
+ export interface RequestCapture {
46
+ api: ReplayApi;
47
+ provider: string;
48
+ modelId: string;
49
+ payload: Record<string, unknown>;
50
+ /** The last message of the context the body was built from: where a fork's own messages start. */
51
+ covers?: CoveredThrough;
52
+ /** The session the body went out for: its id is the routing affinity a fork must send too. */
53
+ sessionId?: string;
54
+ }
55
+
56
+ /** A transcript message named by what survives a session fork: its role and timestamp. */
57
+ export interface CoveredThrough {
58
+ role: string;
59
+ timestamp: number;
60
+ }
61
+
62
+ interface ModelRef {
63
+ api?: string;
64
+ provider?: string;
65
+ id?: string;
66
+ }
67
+
68
+ /** A capture of `payload` for `model`, or undefined when its API cannot be replayed. */
69
+ export function captureRequest(model: ModelRef | undefined, payload: unknown, covers?: CoveredThrough, sessionId?: string): RequestCapture | undefined {
70
+ const api = model?.api;
71
+ if (api !== "anthropic-messages" && api !== "openai-completions" && api !== "openai-responses") return undefined;
72
+ if (!model?.provider || !model.id || !isRecord(payload) || !Array.isArray(payload[LIST_KEY[api]])) return undefined;
73
+ return { api, provider: model.provider, modelId: model.id, payload, ...(covers ? { covers } : {}), ...(sessionId ? { sessionId } : {}) };
74
+ }
75
+
76
+ /** The model fields pi-ai reads to choose its session-affinity headers. */
77
+ export interface AffinityModel extends ModelRef {
78
+ baseUrl?: string;
79
+ compat?: { sendSessionAffinityHeaders?: boolean; sessionAffinityFormat?: string };
80
+ }
81
+
82
+ /**
83
+ * The session-affinity headers pi-ai sends with `sessionId` for `model`, so a
84
+ * fork can send its parent's: a gateway routes on them (OpenRouter spreads a
85
+ * model across hosts, each with its own cache), and the fork is a different
86
+ * session. Extension headers merge after pi-ai's, so these override. A copy
87
+ * of pi-ai's `createClient` rules for the three replayable APIs, unchanged
88
+ * from 0.87.1 to 0.99.2 (re-check at a pi bump:
89
+ * features/pi-adaptation/plan.md §3 step 9).
90
+ */
91
+ export function affinityHeaders(model: AffinityModel | undefined, sessionId: string): Record<string, string> {
92
+ if (!model) return {};
93
+ const openRouter = model.provider === "openrouter" || (model.baseUrl ?? "").includes("openrouter.ai");
94
+ const send = model.compat?.sendSessionAffinityHeaders ?? openRouter;
95
+ if (model.api === "anthropic-messages") {
96
+ if (!send) return {};
97
+ const format = model.compat?.sessionAffinityFormat ?? (openRouter ? "openrouter" : undefined);
98
+ return { [format === "openrouter" ? "x-session-id" : "x-session-affinity"]: sessionId };
99
+ }
100
+ if (model.api !== "openai-completions" && model.api !== "openai-responses") return {};
101
+ if (model.api === "openai-completions" && !send) return {};
102
+ const format = model.compat?.sessionAffinityFormat ?? (openRouter ? "openrouter" : "openai");
103
+ if (format === "openrouter") return { "x-session-id": sessionId };
104
+ return {
105
+ ...(format === "openai" ? { session_id: sessionId } : {}),
106
+ "x-client-request-id": sessionId,
107
+ ...(model.api === "openai-completions" ? { "x-session-affinity": sessionId } : {}),
108
+ };
109
+ }
110
+
111
+ /** The last non-system message of a context, as a fork will find it again; undefined without one. */
112
+ export function lastCovered(messages: readonly { role: string; timestamp?: number }[]): CoveredThrough | undefined {
113
+ for (let index = messages.length - 1; index >= 0; index--) {
114
+ const { role, timestamp } = messages[index];
115
+ if (role === "system") continue;
116
+ return typeof timestamp === "number" ? { role, timestamp } : undefined;
117
+ }
118
+ return undefined;
119
+ }
120
+
121
+ /** True when `model` is the one `capture` went to: a different model has a different cache. */
122
+ export function captureMatches(capture: RequestCapture, model: ModelRef | undefined): boolean {
123
+ return capture.api === model?.api && capture.provider === model?.provider && capture.modelId === model?.id;
124
+ }
125
+
126
+ interface MessageLike {
127
+ role: string;
128
+ content?: unknown;
129
+ stopReason?: string;
130
+ }
131
+
132
+ /**
133
+ * The last captured request plus the assistant reply that answered it, as one
134
+ * consumer extension sees them. The reply is kept only when it closes the
135
+ * exchange cleanly: an errored or aborted reply is omitted from pi's context,
136
+ * and one that called tools is still waiting on their results (appending it
137
+ * without them is invalid on every provider), so both leave the tail empty.
138
+ */
139
+ export class LastExchange<Reply extends MessageLike = MessageLike> {
140
+ private capture: RequestCapture | undefined;
141
+ private reply: Reply | undefined;
142
+
143
+ /** A new request (or undefined: none valid) replaces the old one and its reply. */
144
+ setCapture(capture: RequestCapture | undefined): void {
145
+ this.capture = capture;
146
+ this.reply = undefined;
147
+ }
148
+
149
+ /** Feed every main-session `message_end`; only assistant messages matter. */
150
+ noteMessage(message: Reply): void {
151
+ if (!this.capture || message.role !== "assistant") return;
152
+ const content = Array.isArray(message.content) ? (message.content as { type?: string }[]) : [];
153
+ const clean = message.stopReason !== "error" && message.stopReason !== "aborted" && !content.some((block) => block.type === "toolCall");
154
+ this.reply = clean ? message : undefined;
155
+ }
156
+
157
+ /** The capture and reply when the capture went to `model`; a switched model has its own cache. */
158
+ forModel(model: ModelRef | undefined): { capture: RequestCapture; reply: Reply | undefined } | undefined {
159
+ const capture = this.capture;
160
+ if (!capture || capture.provider !== model?.provider || capture.modelId !== model?.id) return undefined;
161
+ return { capture, reply: this.reply };
162
+ }
163
+ }
164
+
165
+ /**
166
+ * The captured body with `tail`'s message list (a body pi-ai built for the tail
167
+ * alone) appended, every other field kept byte for byte. `maxTokens` replaces
168
+ * the output cap when given.
169
+ *
170
+ * Anthropic cache breakpoints: the tail's own markers are dropped, and when the
171
+ * tail starts with the assistant reply, the markers on the captured request's
172
+ * last message move to the reply's last text block (Claude Code's placement).
173
+ * The marker count never grows (the API allows four), the prefix the session
174
+ * cached is read through the lookback, and the reply gets written for the
175
+ * user's next turn. A tail that starts with a user message is merged into a
176
+ * trailing user message, so the roles still alternate.
177
+ */
178
+ export function extendPayload(capture: RequestCapture, tail: Record<string, unknown>, maxTokens?: number): Record<string, unknown> {
179
+ const key = LIST_KEY[capture.api];
180
+ const base = capture.payload[key] as unknown[];
181
+ const extra = withoutCacheControl(Array.isArray(tail[key]) ? (tail[key] as unknown[]) : []) as unknown[];
182
+ const list = capture.api === "anthropic-messages" ? appendAnthropic(base, extra) : [...base, ...extra];
183
+ const out: Record<string, unknown> = { ...capture.payload, [key]: list };
184
+ if (maxTokens !== undefined) {
185
+ const keys = MAX_TOKEN_KEYS[capture.api];
186
+ out[keys.find((k) => k in capture.payload) ?? keys[0]] = maxTokens;
187
+ }
188
+ return out;
189
+ }
190
+
191
+ /** The output cap the captured body carries, if any. */
192
+ function capturedMaxTokens(capture: RequestCapture): number | undefined {
193
+ for (const key of MAX_TOKEN_KEYS[capture.api]) {
194
+ const value = capture.payload[key];
195
+ if (typeof value === "number") return value;
196
+ }
197
+ return undefined;
198
+ }
199
+
200
+ /** A replay that would leave less output room than this is not sent as a replay. */
201
+ export const MIN_REPLAY_OUTPUT_TOKENS = 1024;
202
+ /** The output cap when neither the captured body nor the caller names one. */
203
+ const DEFAULT_REPLAY_OUTPUT_TOKENS = 8192;
204
+
205
+ /**
206
+ * The messages a side call appends: the reply that closed the exchange, if
207
+ * any, then `before` (a side session's earlier exchanges), then its prompt.
208
+ */
209
+ export function replayTail(reply: Message | undefined, prompt: string, timestamp = Date.now(), before: readonly Message[] = []): Message[] {
210
+ const question: Message = { role: "user", content: [{ type: "text", text: prompt }], timestamp };
211
+ return [...(reply ? [reply] : []), ...before, question];
212
+ }
213
+
214
+ /**
215
+ * The output cap for a replay, or undefined when too little room is left. The
216
+ * captured cap was clamped to what that request left of the window, and the
217
+ * tail spends some of it; `limit` caps it further.
218
+ */
219
+ export function replayOutputCap(capture: RequestCapture, tail: readonly Message[], limit?: number): number | undefined {
220
+ const captured = capturedMaxTokens(capture);
221
+ const room = captured === undefined ? (limit ?? DEFAULT_REPLAY_OUTPUT_TOKENS) : captured - tail.reduce((sum, message) => sum + estimateMessageTokens(message), 0);
222
+ const cap = limit === undefined ? room : Math.min(limit, room);
223
+ return cap >= MIN_REPLAY_OUTPUT_TOKENS ? cap : undefined;
224
+ }
225
+
226
+ // ---------------------------------------------------------------------------
227
+ // Forks: every request of a child session that inherits the transcript
228
+ // ---------------------------------------------------------------------------
229
+
230
+ /**
231
+ * A fork's context with the messages its parent's captured request already
232
+ * carries removed: everything up to and including `covers`, system messages
233
+ * kept (pi requires the leading one). What is left is the fork's own tail: the
234
+ * reply that answered the capture, the fork's task, and its own turns. A
235
+ * session fork copies the entries with their timestamps, so the boundary is
236
+ * found by role and timestamp; undefined when it is not there (the fork
237
+ * compacted, or the capture belongs to another branch).
238
+ */
239
+ export function uncoveredMessages<M extends { role: string; timestamp?: number }>(messages: readonly M[], covers: CoveredThrough): M[] | undefined {
240
+ const boundary = messages.findLastIndex((message) => message.role === covers.role && message.timestamp === covers.timestamp);
241
+ if (boundary === -1) return undefined;
242
+ return [...messages.slice(0, boundary + 1).filter((message) => message.role === "system"), ...messages.slice(boundary + 1)];
243
+ }
244
+
245
+ /** Anthropic's limit on `cache_control` breakpoints in one request. */
246
+ export const MAX_CACHE_MARKERS = 4;
247
+
248
+ /**
249
+ * A fork's request: the parent's captured body byte for byte (system prompt,
250
+ * tools, messages, `prompt_cache_key`, every other field), then the fork's own
251
+ * tail from `child` (pi's body for the fork's uncovered messages), so the fork
252
+ * reads the prefix the parent cached (decisions/caching.md, "A call that
253
+ * inherits the session's context reads the session's cache").
254
+ *
255
+ * - Tools stay the parent's. A tool the fork gained after its first request
256
+ * (not in `baseline`, the first request's names), or one a tail
257
+ * `tool_reference` names, is appended so it stays callable; that breaks the
258
+ * cache from there on, as the same load does in the parent.
259
+ * - The output cap is the smaller of the fork's and the parent's cap less the
260
+ * tail, since pi clamped the fork's to a context without the prefix.
261
+ * - Cache markers: the parent's stay where the parent wrote its entry, the
262
+ * tail's mark the fork's own entry, and the earliest are dropped past four.
263
+ */
264
+ export function forkRequestPayload(capture: RequestCapture, child: Record<string, unknown>, baseline: ReadonlySet<string>): Record<string, unknown> {
265
+ const key = LIST_KEY[capture.api];
266
+ const base = capture.payload[key] as unknown[];
267
+ const tail = (Array.isArray(child[key]) ? (child[key] as WireMessage[]) : []).filter((entry) => entry.role !== "system" && entry.role !== "developer");
268
+ const out: Record<string, unknown> = { ...capture.payload, [key]: capture.api === "anthropic-messages" ? joinUserTurns(base, tail) : [...base, ...tail] };
269
+ const tools = forkTools(capture.payload.tools, child.tools, baseline, tail);
270
+ if (tools) out.tools = tools;
271
+ const capKey = MAX_TOKEN_KEYS[capture.api].find((k) => typeof capture.payload[k] === "number");
272
+ // Appended tool schemas spend room too; the parent's own tools are already in its cap.
273
+ const appended = Array.isArray(tools) && Array.isArray(capture.payload.tools) ? tools.slice(capture.payload.tools.length) : [];
274
+ const tailRoom = forkOutputRoom(capture, tail);
275
+ const room = tailRoom === undefined ? undefined : tailRoom - (appended.length > 0 ? Math.ceil(JSON.stringify(appended).length / 4) : 0);
276
+ if (capKey && room !== undefined) {
277
+ // Never above the room: the fork chose the fallback before this when too little was left.
278
+ const own = typeof child[capKey] === "number" ? (child[capKey] as number) : room;
279
+ out[capKey] = Math.max(1, Math.min(own, room));
280
+ }
281
+ capCacheMarkers(out, key, capture, tail);
282
+ return out;
283
+ }
284
+
285
+ /**
286
+ * The output room a fork request leaves: the captured cap (pi clamped it to
287
+ * what the parent's context left of the window) less a rough size of `tail`.
288
+ * Undefined when the capture names no cap. A fork whose room is below
289
+ * `MIN_REPLAY_OUTPUT_TOKENS` must not be spliced: it goes out whole instead.
290
+ */
291
+ export function forkOutputRoom(capture: RequestCapture, tail: unknown): number | undefined {
292
+ const capKey = MAX_TOKEN_KEYS[capture.api].find((k) => typeof capture.payload[k] === "number");
293
+ return capKey === undefined ? undefined : (capture.payload[capKey] as number) - Math.ceil(JSON.stringify(tail).length / 4);
294
+ }
295
+
296
+ /** The names a body's tool list declares (Anthropic, Chat Completions and Responses shapes). */
297
+ export function toolNames(tools: unknown): string[] {
298
+ return Array.isArray(tools) ? tools.map(toolName).filter((name): name is string => name !== undefined) : [];
299
+ }
300
+
301
+ function toolName(tool: unknown): string | undefined {
302
+ if (!isRecord(tool)) return undefined;
303
+ if (typeof tool.name === "string") return tool.name;
304
+ return isRecord(tool.function) && typeof tool.function.name === "string" ? tool.function.name : undefined;
305
+ }
306
+
307
+ function forkTools(parent: unknown, child: unknown, baseline: ReadonlySet<string>, tail: unknown[]): unknown[] | undefined {
308
+ if (!Array.isArray(parent)) return Array.isArray(child) ? (withoutCacheControl(child) as unknown[]) : undefined;
309
+ const declared = new Set(toolNames(parent));
310
+ const referenced = referencedTools(tail);
311
+ const extra = (Array.isArray(child) ? child : []).filter((tool) => {
312
+ const name = toolName(tool);
313
+ return name !== undefined && !declared.has(name) && (!baseline.has(name) || referenced.has(name));
314
+ });
315
+ return extra.length > 0 ? [...parent, ...(withoutCacheControl(extra) as unknown[])] : parent;
316
+ }
317
+
318
+ /** Tool names the tail's `tool_reference` blocks load (Anthropic tool search). */
319
+ function referencedTools(value: unknown, found = new Set<string>()): Set<string> {
320
+ if (Array.isArray(value)) for (const entry of value) referencedTools(entry, found);
321
+ else if (isRecord(value)) {
322
+ if (value.type === "tool_reference" && typeof value.tool_name === "string") found.add(value.tool_name);
323
+ for (const entry of Object.values(value)) referencedTools(entry, found);
324
+ }
325
+ return found;
326
+ }
327
+
328
+ /** Anthropic needs alternating roles: a tail that opens with a user turn joins the captured last one. */
329
+ function joinUserTurns(base: unknown[], tail: unknown[]): unknown[] {
330
+ const last = base.at(-1) as WireMessage | undefined;
331
+ const first = tail[0] as WireMessage | undefined;
332
+ if (last?.role === "user" && first?.role === "user") {
333
+ return [...base.slice(0, -1), { ...last, content: [...blocksOf(last.content), ...blocksOf(first.content)] }, ...tail.slice(1)];
334
+ }
335
+ return [...base, ...tail];
336
+ }
337
+
338
+ /**
339
+ * Drop the earliest `cache_control` markers (tools, then system, then
340
+ * messages) past the limit. Copy-on-write: the captured body is shared by
341
+ * every fork, so only the path to a dropped marker is copied.
342
+ */
343
+ function capCacheMarkers(payload: Record<string, unknown>, listKey: string, base: RequestCapture, tail: unknown[]): void {
344
+ const keys = ["tools", "system", listKey].filter((key) => key in payload);
345
+ // Appended tools carry no markers, so the body holds the capture's and the tail's.
346
+ const total = captureMarkers(base) + countMarkers(tail);
347
+ const budget = { drop: total - MAX_CACHE_MARKERS };
348
+ for (const key of keys) {
349
+ if (budget.drop <= 0) break;
350
+ payload[key] = dropFirstMarkers(payload[key], budget);
351
+ }
352
+ }
353
+
354
+ /** The captured body's marker count, walked once per capture: the list can hold a 1M-token transcript. */
355
+ const capturedMarkerCounts = new WeakMap<RequestCapture, number>();
356
+ function captureMarkers(capture: RequestCapture): number {
357
+ let count = capturedMarkerCounts.get(capture);
358
+ if (count === undefined) {
359
+ const payload = capture.payload;
360
+ count = countMarkers(payload.tools) + countMarkers(payload.system) + countMarkers(payload[LIST_KEY[capture.api]]);
361
+ capturedMarkerCounts.set(capture, count);
362
+ }
363
+ return count;
364
+ }
365
+
366
+ function countMarkers(value: unknown): number {
367
+ if (Array.isArray(value)) return value.reduce((sum: number, entry) => sum + countMarkers(entry), 0);
368
+ if (!isRecord(value)) return 0;
369
+ let count = "cache_control" in value ? 1 : 0;
370
+ for (const [key, entry] of Object.entries(value)) if (key !== "cache_control") count += countMarkers(entry);
371
+ return count;
372
+ }
373
+
374
+ function dropFirstMarkers(value: unknown, budget: { drop: number }): unknown {
375
+ if (budget.drop <= 0) return value;
376
+ if (Array.isArray(value)) {
377
+ let changed = false;
378
+ const out = value.map((entry) => {
379
+ const next = dropFirstMarkers(entry, budget);
380
+ if (next !== entry) changed = true;
381
+ return next;
382
+ });
383
+ return changed ? out : value;
384
+ }
385
+ if (!isRecord(value)) return value;
386
+ let out: Record<string, unknown> | undefined;
387
+ if ("cache_control" in value) {
388
+ const { cache_control: _, ...rest } = value;
389
+ out = rest;
390
+ budget.drop--;
391
+ }
392
+ for (const [key, entry] of Object.entries(out ?? value)) {
393
+ if (budget.drop <= 0) break;
394
+ const next = dropFirstMarkers(entry, budget);
395
+ if (next !== entry) out = { ...(out ?? value), [key]: next };
396
+ }
397
+ return out ?? value;
398
+ }
399
+
400
+ type Block = Record<string, unknown>;
401
+ type WireMessage = { role?: string; content?: unknown } & Record<string, unknown>;
402
+
403
+ function appendAnthropic(base: unknown[], extra: unknown[]): unknown[] {
404
+ if (extra.length === 0) return base;
405
+ const last = base.at(-1) as WireMessage | undefined;
406
+ const first = extra[0] as WireMessage;
407
+ if (last && first.role === "assistant") {
408
+ const markers = blocksOf(last.content).map((block) => block.cache_control).filter((marker) => marker !== undefined);
409
+ const replyBlocks = blocksOf(first.content);
410
+ const anchor = replyBlocks.findLastIndex((block) => block.type === "text");
411
+ if (markers.length === 0 || anchor === -1) return [...base, ...extra];
412
+ const lastUnmarked = { ...last, content: blocksOf(last.content).map(({ cache_control: _, ...rest }) => rest) };
413
+ const content = replyBlocks.map((block, index) => (index === anchor ? { ...block, cache_control: markers.at(-1) } : block));
414
+ return [...base.slice(0, -1), lastUnmarked, { ...first, content }, ...extra.slice(1)];
415
+ }
416
+ if (last && last.role === "user" && first.role === "user") {
417
+ return [...base.slice(0, -1), { ...last, content: [...blocksOf(last.content), ...blocksOf(first.content)] }, ...extra.slice(1)];
418
+ }
419
+ return [...base, ...extra];
420
+ }
421
+
422
+ function blocksOf(content: unknown): Block[] {
423
+ if (typeof content === "string") return [{ type: "text", text: content }];
424
+ return Array.isArray(content) ? (content as Block[]) : [];
425
+ }
426
+
427
+ function withoutCacheControl(value: unknown): unknown {
428
+ if (Array.isArray(value)) return value.map(withoutCacheControl);
429
+ if (!isRecord(value)) return value;
430
+ const out: Record<string, unknown> = {};
431
+ for (const [key, entry] of Object.entries(value)) {
432
+ if (key !== "cache_control") out[key] = withoutCacheControl(entry);
433
+ }
434
+ return out;
435
+ }
436
+
437
+ function isRecord(value: unknown): value is Record<string, unknown> {
438
+ return typeof value === "object" && value !== null && !Array.isArray(value);
439
+ }
@@ -14,8 +14,9 @@
14
14
  * module state — so each re-derives it through `sessionScratchpadDir`.
15
15
  */
16
16
 
17
+ import { chmodSync, lstatSync, mkdirSync } from "node:fs";
17
18
  import os from "node:os";
18
- import { join } from "node:path";
19
+ import { dirname, join } from "node:path";
19
20
  import { findGitRoot } from "./git.ts";
20
21
  import { projectSlug } from "./memory.ts";
21
22
  import { tryRealpath } from "./paths.ts";
@@ -52,21 +53,87 @@ export function sessionScratchpadDir(cwd: string, sessionId: string): string {
52
53
  return scratchpadDir(resolveTmpRoot(), uid, findGitRoot(cwd) ?? cwd, sessionId);
53
54
  }
54
55
 
55
- /** Claude Code's Scratchpad Directory prompt section, verbatim (see payload.json). */
56
- export function scratchpadPromptSection(dir: string): string {
57
- return `# Scratchpad Directory
56
+ /** The owner-level directory of a scratchpad path (`<tmp>/onecode-<uid>`). */
57
+ function ownerDirOf(scratchpad: string): string {
58
+ return dirname(dirname(dirname(scratchpad)));
59
+ }
60
+
61
+ /** What `ownerDirProblem` needs from an `lstat`. */
62
+ export interface OwnerDirStat {
63
+ isSymbolicLink: boolean;
64
+ isDirectory: boolean;
65
+ uid: number;
66
+ mode: number;
67
+ }
68
+
69
+ /**
70
+ * Why the owner directory cannot hold this user's scratchpads, or undefined.
71
+ * On a shared `/tmp` the name is predictable from the uid, so another user
72
+ * can create it first: a symlink, a directory they own, or one anyone may
73
+ * write lets them read, rename or replace every session's files under it
74
+ * (the sticky bit on `/tmp` does not protect entries of their directory).
75
+ * Pure.
76
+ */
77
+ export function ownerDirProblem(stat: OwnerDirStat, uid: number): string | undefined {
78
+ if (stat.isSymbolicLink) return "it is a symlink";
79
+ if (!stat.isDirectory) return "it is not a directory";
80
+ if (stat.uid !== uid) return `it is owned by uid ${stat.uid}`;
81
+ if (stat.mode & 0o022) return "others may write to it";
82
+ return undefined;
83
+ }
58
84
 
59
- IMPORTANT: Always use this scratchpad directory for temporary files instead of \`/tmp\` or other system temp directories:
60
- \`${dir}\`
85
+ /**
86
+ * Create the scratchpad private to this user, or return false. The owner
87
+ * directory is created 0700 and must then be a real directory this user owns
88
+ * that nobody else may write (`ownerDirProblem`); one created before this
89
+ * check with a looser mode is tightened to 0700, so other users can neither
90
+ * list the project paths in it nor read what the model writes. Every level
91
+ * below is created 0700 too. Windows has no shared temp root (`%TEMP%` is per
92
+ * user), so there it only creates the directory.
93
+ */
94
+ export function ensurePrivateScratchpad(dir: string, uid: number | undefined = typeof process.getuid === "function" ? process.getuid() : undefined): boolean {
95
+ try {
96
+ if (uid !== undefined) {
97
+ const owner = ownerDirOf(dir);
98
+ mkdirSync(owner, { recursive: true, mode: 0o700 });
99
+ const stat = lstatSync(owner);
100
+ if (ownerDirProblem({ isSymbolicLink: stat.isSymbolicLink(), isDirectory: stat.isDirectory(), uid: stat.uid, mode: stat.mode }, uid)) return false;
101
+ if (stat.mode & 0o077) chmodSync(owner, 0o700);
102
+ }
103
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
104
+ return true;
105
+ } catch {
106
+ return false;
107
+ }
108
+ }
61
109
 
62
- Use this directory for ALL temporary file needs:
63
- - Storing intermediate results or data during multi-step tasks
64
- - Writing temporary scripts or configuration files
65
- - Saving outputs that don't belong in the user's project
66
- - Creating working files during analysis or processing
67
- - Any file that would otherwise go to \`/tmp\`
110
+ /**
111
+ * Whether an existing scratchpad sits under a private owner directory, with
112
+ * no side effects: for a gate that trusts a scratchpad another extension
113
+ * created.
114
+ */
115
+ export function isPrivateScratchpad(dir: string, uid: number | undefined = typeof process.getuid === "function" ? process.getuid() : undefined): boolean {
116
+ if (uid === undefined) return true;
117
+ try {
118
+ const stat = lstatSync(ownerDirOf(dir));
119
+ return ownerDirProblem({ isSymbolicLink: stat.isSymbolicLink(), isDirectory: stat.isDirectory(), uid: stat.uid, mode: stat.mode }, uid) === undefined && (stat.mode & 0o077) === 0;
120
+ } catch {
121
+ return false;
122
+ }
123
+ }
68
124
 
69
- Only use \`/tmp\` if the user explicitly requests it.
125
+ /** The session's scratchpad, created private to this user, or undefined when it cannot be. */
126
+ export function privateSessionScratchpadDir(cwd: string, sessionId: string): string | undefined {
127
+ const dir = sessionScratchpadDir(cwd, sessionId);
128
+ return ensurePrivateScratchpad(dir) ? dir : undefined;
129
+ }
70
130
 
71
- The scratchpad directory is session-specific, isolated from the user's project, and can generally be used without permission prompts.`;
131
+ /**
132
+ * A session's own private temp directory, the scratchpad's parent
133
+ * (`<tmp>/onecode-<uid>/<project-slug>/<session-id>`), created private to
134
+ * this user, or undefined when it cannot be.
135
+ */
136
+ export function privateSessionTempDir(cwd: string, sessionId: string): string | undefined {
137
+ const scratchpad = sessionScratchpadDir(cwd, sessionId);
138
+ return ensurePrivateScratchpad(scratchpad) ? dirname(scratchpad) : undefined;
72
139
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Emitted by the send-now extension just before it aborts a turn to deliver
3
+ * the queued messages, so the interrupted extension skips its "Interrupted ·
4
+ * What should One Code do instead?" note for that abort: the user has already
5
+ * said what to do instead.
6
+ */
7
+ export const SEND_NOW_CHANNEL = "one-code:send-now";
@@ -0,0 +1,51 @@
1
+ /**
2
+ * One request-surface tier per session/model boundary. The first consumer owns
3
+ * the lifecycle handlers; every consumer reads that owner's value over the
4
+ * event bus. No module state is shared (each extension has its own jiti copy),
5
+ * and an extension loaded in isolation still installs its own owner.
6
+ *
7
+ * Only session_start and model_select classify. A catalog refresh can affect
8
+ * automatic model selection immediately, but prompt text, tool descriptions,
9
+ * and tier-dependent steering keep the same tier until the next boundary.
10
+ */
11
+
12
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
13
+ import { type PromptTier, resolveModelTier } from "./model-tier.ts";
14
+ import { invalidateCatalogCache } from "./model-catalog-data.ts";
15
+
16
+ const SESSION_TIER_REQUEST_CHANNEL = "one-code:session-tier-request";
17
+
18
+ interface TierRequest {
19
+ owned?: boolean;
20
+ tier?: PromptTier;
21
+ }
22
+
23
+ /** Install the owner once on this bus, and return a reader of its frozen tier. */
24
+ export function sessionModelTier(pi: Pick<ExtensionAPI, "events" | "on">): () => PromptTier {
25
+ const request = (): TierRequest => {
26
+ const query: TierRequest = {};
27
+ pi.events.emit(SESSION_TIER_REQUEST_CHANNEL, query);
28
+ return query;
29
+ };
30
+ if (!request().owned) {
31
+ let tier: PromptTier | undefined;
32
+ pi.events.on(SESSION_TIER_REQUEST_CHANNEL, (data) => {
33
+ Object.assign(data as TierRequest, { owned: true, tier });
34
+ });
35
+ const resolve = (model: Parameters<typeof resolveModelTier>[0]) => {
36
+ // Doctor's jiti copy cannot clear our memo. Do not freeze a stale
37
+ // catalog if the boundary lands inside the disk cache's stat throttle.
38
+ invalidateCatalogCache();
39
+ tier = resolveModelTier(model);
40
+ };
41
+ // Registered before the first consumer's hooks. Later consumers preserve
42
+ // their own lifecycle ordering (for example, restored bash mode variants).
43
+ pi.on("session_start", (_event, ctx) => resolve(ctx.model));
44
+ pi.on("model_select", (event) => resolve(event.model));
45
+ }
46
+ return () => {
47
+ const tier = request().tier;
48
+ if (tier === undefined) throw new Error("Request tier is unavailable: emit session_start before building the request surface.");
49
+ return tier;
50
+ };
51
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * What will wake the session later, for Claude Code's Stop hook payload: its
3
+ * `session_crons` (cron_create jobs, /loop, schedule_wakeup) and
4
+ * `background_tasks` (in-flight background work) fields, findings §21.
5
+ *
6
+ * The hooks extension emits a `SessionWorkQuery` on SESSION_WORK_CHANNEL and
7
+ * reads it back once the synchronous emit returns; the background extension,
8
+ * which owns both lists, fills it (jiti isolation: no shared module state).
9
+ *
10
+ * Pure: no pi imports.
11
+ */
12
+
13
+ export const SESSION_WORK_CHANNEL = "one-code:session-work";
14
+
15
+ /** Claude Code's `session_crons` item. */
16
+ export interface SessionCron {
17
+ id: string;
18
+ /** The cron expression. */
19
+ schedule: string;
20
+ /** False for one-shots, a wakeup's single fire time included. */
21
+ recurring: boolean;
22
+ prompt: string;
23
+ }
24
+
25
+ /** Claude Code's `background_tasks` item. */
26
+ export interface SessionBackgroundTask {
27
+ id: string;
28
+ /** Friendly type label: shell, subagent, monitor, workflow; else the raw kind. */
29
+ type: string;
30
+ status: string;
31
+ description: string;
32
+ /** Only for shell tasks. */
33
+ command?: string;
34
+ }
35
+
36
+ export interface SessionWorkQuery {
37
+ crons: SessionCron[];
38
+ tasks: SessionBackgroundTask[];
39
+ }
40
+
41
+ /** The hook payload's field cap (Claude Code: 1000 characters, then an in-string marker). */
42
+ const FIELD_CAP = 1000;
43
+
44
+ /** Claude Code's clip for these fields: the first 1000 characters, then `… [+N chars]`. */
45
+ export function capField(text: string): string {
46
+ return text.length <= FIELD_CAP ? text : `${text.slice(0, FIELD_CAP)}… [+${text.length - FIELD_CAP} chars]`;
47
+ }
48
+
49
+ const TYPE_LABELS: Record<string, string> = { bash: "shell", shell: "shell", agent: "subagent", subagent: "subagent", monitor: "monitor", workflow: "workflow" };
50
+
51
+ export function sessionCron(job: { id: string; cron: string; recurring: boolean; prompt: string }): SessionCron {
52
+ return { id: job.id, schedule: job.cron, recurring: job.recurring, prompt: capField(job.prompt) };
53
+ }
54
+
55
+ export function sessionBackgroundTask(task: { id: string; kind: string; status: string; description: string; command?: string }): SessionBackgroundTask {
56
+ const type = TYPE_LABELS[task.kind] ?? task.kind;
57
+ return {
58
+ id: task.id,
59
+ type,
60
+ status: task.status,
61
+ description: capField(task.description),
62
+ ...(type === "shell" && task.command !== undefined && { command: capField(task.command) }),
63
+ };
64
+ }