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
@@ -3,22 +3,26 @@
3
3
  * command registration (pure fs). (The /plugins Installed tab builds its rows
4
4
  * from discoverPlugins directly and no longer calls this.)
5
5
  *
6
- * Scans the SAME directories claude-compat feeds to pi, in pi's order —
7
- * `~/.claude/skills`, `~/.agents/skills`, `<cwd>/.claude/skills`,
8
- * `<cwd>/.agents/skills`, `<agentDir>/skills`, then the bundled catalog last
9
- * (so it loses a name collision, as it does in pi) — and merges the plugin
10
- * skills the caller resolved. Rows carry the SKILL.md path (for the ~token
6
+ * Scans the SAME directories pi loads, in pi's order — `<agentDir>/skills`
7
+ * (pi's own), then the ones claude-compat feeds it: `~/.claude/skills`,
8
+ * `~/.agents/skills`, `<cwd>/.claude/skills`, `<cwd>/.agents/skills`, and the
9
+ * bundled catalog last — and merges the plugin skills the caller resolved.
10
+ * As in pi, a name loads once (the first wins, so the bundled catalog loses a
11
+ * collision) and a file reached twice through a link counts once, so the
12
+ * banner, `/skills` and `/doctor` count the skills pi actually loads. Rows carry the SKILL.md path (for the ~token
11
13
  * size estimate) and the scope used by skill-overrides keys. Before the first
12
14
  * turn this scan is the only skill list One Code has (pi resolves skills per
13
15
  * turn), so a root missing here is a skill with no `/name` command at startup.
14
16
  */
15
17
 
16
- import { existsSync, readdirSync, statSync } from "node:fs";
18
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
17
19
  import { dirname, join } from "node:path";
18
20
  import { fileURLToPath } from "node:url";
19
21
  import type { PluginSkill } from "./plugins.ts";
20
22
  import type { SkillScope } from "./skill-overrides.ts";
21
- import { claudeUserDir } from "./paths.ts";
23
+ import { claudeSourcesOn, projectConfigDir, userConfigDir } from "./config-mode.ts";
24
+ import { claudeUserDir, isPathAtOrUnder, tryRealpath } from "./paths.ts";
25
+ import { parseFrontmatterLoosely } from "./frontmatter.ts";
22
26
 
23
27
  /** The skill catalog shipped in this package: `<package>/skills` (Claude Code's self-contained built-in skills). */
24
28
  export const BUNDLED_SKILLS_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "skills");
@@ -31,16 +35,38 @@ export interface ScannedSkill {
31
35
 
32
36
  /**
33
37
  * The one scope-classification rule for a skill path — user skills live under
34
- * `~/.claude/skills` AND `<agentDir>/skills`; everything else is project
38
+ * `~/.claude/skills`, `~/.agents/skills` and `<agentDir>/skills`; everything else is project
35
39
  * scope. Both the scanner and the skill extension classify through this, so a
36
40
  * skill never shows different scopes in /plugins vs the skill tool.
37
41
  */
38
42
  export function scopeForPath(path: string, home: string, agentDir: string): SkillScope {
39
- if (path.startsWith(join(claudeUserDir(home), "skills")) || path.startsWith(join(agentDir, "skills"))) return "user";
40
- return "project";
43
+ const roots = [join(claudeUserDir(home), "skills"), join(home, ".agents", "skills"), join(agentDir, "skills")];
44
+ return roots.some((root) => isPathAtOrUnder(path, root)) ? "user" : "project";
41
45
  }
42
46
 
43
- function scanDir(dir: string, scope: SkillScope, into: Map<string, ScannedSkill>): void {
47
+ /**
48
+ * The skill folders One Code adds to pi's own (`<agentDir>/skills`, which pi
49
+ * scans itself), in load order: Claude Code's two only in Claude-compatible
50
+ * mode (lib/config-mode.ts), the cross-tool `.agents` pair in both. `claudeDir`
51
+ * defaults to `~/.claude` (`CLAUDE_CONFIG_DIR`); `~/.agents` is never moved.
52
+ */
53
+ export function skillSourceDirs(cwd: string, home: string, claudeDir: string = claudeUserDir(home)): Array<{ dir: string; scope: SkillScope }> {
54
+ const claude = claudeSourcesOn();
55
+ const dirs: Array<{ dir: string; scope: SkillScope } | false> = [
56
+ claude && { dir: join(claudeDir, "skills"), scope: "user" },
57
+ { dir: join(home, ".agents", "skills"), scope: "user" },
58
+ claude && { dir: join(cwd, ".claude", "skills"), scope: "project" },
59
+ { dir: join(cwd, ".agents", "skills"), scope: "project" },
60
+ ];
61
+ return dirs.filter((d): d is { dir: string; scope: SkillScope } => d !== false);
62
+ }
63
+
64
+ /** The prompt-template folders: `~/.claude/commands` and `.claude/commands`, or their `.onecode` twins in independent mode. */
65
+ export function commandDirs(cwd: string, home: string, claudeDir: string = claudeUserDir(home)): string[] {
66
+ return [join(claudeSourcesOn() ? claudeDir : userConfigDir(home), "commands"), join(projectConfigDir(cwd), "commands")];
67
+ }
68
+
69
+ function scanDir(dir: string, scope: SkillScope, into: Map<string, ScannedSkill>, seenFiles: Set<string>): void {
44
70
  if (!existsSync(dir)) return;
45
71
  let entries: string[];
46
72
  try {
@@ -51,9 +77,18 @@ function scanDir(dir: string, scope: SkillScope, into: Map<string, ScannedSkill>
51
77
  for (const entry of entries) {
52
78
  const skillFile = join(dir, entry, "SKILL.md");
53
79
  // existsSync follows symlinked skill directories, which isDirectory() would miss.
54
- if (existsSync(skillFile) && !into.has(`${scope}:${entry}`)) {
55
- into.set(`${scope}:${entry}`, { name: entry, path: skillFile, scope });
80
+ if (!existsSync(skillFile)) continue;
81
+ let name = entry;
82
+ try {
83
+ const declared = parseFrontmatterLoosely(readFileSync(skillFile, "utf-8")).frontmatter.name;
84
+ if (typeof declared === "string" && declared) name = declared;
85
+ } catch {
86
+ continue;
56
87
  }
88
+ const real = tryRealpath(skillFile) ?? skillFile;
89
+ if (into.has(name) || seenFiles.has(real)) continue;
90
+ seenFiles.add(real);
91
+ into.set(name, { name, path: skillFile, scope });
57
92
  }
58
93
  }
59
94
 
@@ -65,12 +100,10 @@ export function scanSkills(
65
100
  bundledSkillsDir?: string,
66
101
  ): ScannedSkill[] {
67
102
  const skills = new Map<string, ScannedSkill>();
68
- scanDir(join(claudeUserDir(home), "skills"), "user", skills);
69
- scanDir(join(home, ".agents", "skills"), "user", skills);
70
- scanDir(join(cwd, ".claude", "skills"), "project", skills);
71
- scanDir(join(cwd, ".agents", "skills"), "project", skills);
72
- scanDir(join(agentDir, "skills"), "user", skills);
73
- if (bundledSkillsDir) scanDir(bundledSkillsDir, scopeForPath(bundledSkillsDir, home, agentDir), skills);
103
+ const seenFiles = new Set<string>();
104
+ scanDir(join(agentDir, "skills"), "user", skills, seenFiles);
105
+ for (const { dir, scope } of skillSourceDirs(cwd, home)) scanDir(dir, scope, skills, seenFiles);
106
+ if (bundledSkillsDir) scanDir(bundledSkillsDir, scopeForPath(bundledSkillsDir, home, agentDir), skills, seenFiles);
74
107
  for (const skill of pluginSkills) {
75
108
  skills.set(`plugin:${skill.name}`, { name: skill.name, path: skill.path, scope: "plugin" });
76
109
  }
@@ -80,14 +113,20 @@ export function scanSkills(
80
113
  /**
81
114
  * Names of the `.claude/commands` prompt templates (`<name>.md`) pi will expose
82
115
  * as `/<name>` — the same dirs claude-compat feeds pi (`~/.claude/commands`,
83
- * `<cwd>/.claude/commands`) plus pi's own `<agentDir>/prompts`. pi resolves
116
+ * `<cwd>/.claude/commands`, or their `.onecode` twins in independent mode)
117
+ * plus pi's own `<agentDir>/prompts`. pi resolves
84
118
  * templates per turn, after `session_start`, so a bare skill command registered
85
119
  * then would silently shadow a same-named template for the whole session;
86
120
  * this pre-scan lets the registration skip those names.
87
121
  */
88
122
  export function promptTemplateNames(cwd: string, home: string, agentDir: string): string[] {
89
- const names = new Set<string>();
90
- for (const dir of [join(claudeUserDir(home), "commands"), join(cwd, ".claude", "commands"), join(agentDir, "prompts")]) {
123
+ return [...new Set(promptTemplateFiles(cwd, home, agentDir).map((file) => file.name))];
124
+ }
125
+
126
+ /** Every prompt-template file pi will resolve, with the command name each gives. */
127
+ export function promptTemplateFiles(cwd: string, home: string, agentDir: string): { name: string; path: string }[] {
128
+ const files: { name: string; path: string }[] = [];
129
+ for (const dir of [...commandDirs(cwd, home), join(agentDir, "prompts")]) {
91
130
  if (!existsSync(dir)) continue;
92
131
  let entries: string[];
93
132
  try {
@@ -95,9 +134,9 @@ export function promptTemplateNames(cwd: string, home: string, agentDir: string)
95
134
  } catch {
96
135
  continue;
97
136
  }
98
- for (const entry of entries) if (entry.endsWith(".md")) names.add(entry.slice(0, -3));
137
+ for (const entry of entries) if (entry.endsWith(".md")) files.push({ name: entry.slice(0, -3), path: join(dir, entry) });
99
138
  }
100
- return [...names];
139
+ return files;
101
140
  }
102
141
 
103
142
  /** Rough token estimate from the SKILL.md byte size (~4 bytes/token). */
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Keep pi's foreground shell spill file readable.
3
+ *
4
+ * When a foreground `bash` or `powershell` run prints more than pi keeps, pi
5
+ * writes the whole output to `<os.tmpdir()>/pi-<shell>-<16 hex>.log` and ends
6
+ * the text with `[… Full output: <path>]`, on success and on a failure: a
7
+ * non-zero exit is an error result from pi 0.99 on and a thrown error before
8
+ * it, and a timeout or an abort is always thrown. `os.tmpdir()` is outside
9
+ * every root the permission gate treats as readable, so the "full output is
10
+ * one read away" promise held only where outside reads skip the gate. So the
11
+ * file is moved into the session's persisted-results dir (a readable root)
12
+ * and the path in the text is rewritten to match.
13
+ *
14
+ * Only a file pi produced is moved: the path must sit directly in the temp
15
+ * dir and carry pi's name shape, so a command that prints a look-alike line
16
+ * cannot make the harness move some other file.
17
+ *
18
+ * The output is also cut Claude Code's way instead of pi's. pi
19
+ * keeps the last 2,000 lines or 50 KB and drops the head, so a batched
20
+ * command lost its first commands' output. Claude Code keeps the head: the
21
+ * whole output up to its inline cap, past it the persisted block naming the
22
+ * file, with the first 2 KB as the preview. pi's spill file holds the whole
23
+ * output, so the block names it and nothing is copied.
24
+ */
25
+
26
+ import { copyFileSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync } from "node:fs";
27
+ import { tmpdir } from "node:os";
28
+ import { basename, dirname, join, resolve } from "node:path";
29
+ import { persistedFilePreview, persistIfLarge } from "./persisted-output.ts";
30
+
31
+ const MARKER = "Full output: ";
32
+ const PI_SPILL_NAME = /^pi-[a-z]+-[0-9a-f]{16}\.log$/;
33
+ /** The note pi ends a cut output with: `\n\n[Showing lines …` or `\n\n[Showing last …`. */
34
+ const PI_NOTE = "\n\n[Showing ";
35
+ /** pi's status after the output of a run that did not exit 0. */
36
+ const PI_STATUS = /(?:^|\n\n)Command (?:exited with code -?\d+|timed out after \S+ seconds|aborted|terminated without an exit code)$/;
37
+
38
+ /** How a shell's output is cut: whole up to `inlineMax` bytes, past it persisted as `<id>.txt` when pi did not spill it. */
39
+ export interface OutputBound {
40
+ inlineMax: number;
41
+ id: string;
42
+ }
43
+
44
+ /** The spill path pi named at the end of `text`, when it is one pi produced in `tmp`. Pure. */
45
+ export function piSpillPath(text: string, tmp: string = tmpdir()): string | undefined {
46
+ const at = text.lastIndexOf(MARKER);
47
+ if (at === -1) return undefined;
48
+ const close = text.indexOf("]", at + MARKER.length);
49
+ if (close === -1) return undefined;
50
+ const path = text.slice(at + MARKER.length, close);
51
+ if (!PI_SPILL_NAME.test(basename(path))) return undefined;
52
+ if (resolve(dirname(path)) !== resolve(tmp)) return undefined;
53
+ return path;
54
+ }
55
+
56
+ /** Move `from` into `dir`, keeping its name; the new path, or undefined when it could not move. */
57
+ function moveInto(from: string, dir: string): string | undefined {
58
+ const to = join(dir, basename(from));
59
+ try {
60
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
61
+ try {
62
+ renameSync(from, to);
63
+ } catch {
64
+ // A different filesystem: copy, then drop the original.
65
+ copyFileSync(from, to);
66
+ unlinkSync(from);
67
+ }
68
+ return to;
69
+ } catch {
70
+ return undefined;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * `text` with its spill file moved and pi's tail cut replaced by Claude
76
+ * Code's head-first one. A spill file that cannot be moved keeps pi's text;
77
+ * one that cannot be read, or a text not in pi's shape, keeps pi's text with
78
+ * only the path rewritten.
79
+ */
80
+ export function boundSpillIn(text: string, resultsDir: string, bound: OutputBound, failed = true, tmp: string = tmpdir()): string {
81
+ // pi appends a status only to a run that did not exit 0; a successful
82
+ // command's own last line can look like one.
83
+ const status = failed ? PI_STATUS.exec(text) : null;
84
+ const end = status ? status.index : text.length;
85
+ const output = text.slice(0, end);
86
+ const from = piSpillPath(output, tmp);
87
+ if (!from) return persistIfLarge(output, { dir: resultsDir, id: bound.id, maxBytes: bound.inlineMax }) + text.slice(end);
88
+ const to = moveInto(from, resultsDir);
89
+ if (!to) return text;
90
+ // pi's note ends the output; anything else keeps pi's text.
91
+ const whole = output.includes(PI_NOTE) && output.endsWith(`${from}]`) ? wholeOutput(to, bound.inlineMax) : undefined;
92
+ return whole === undefined ? text.split(from).join(to) : whole + text.slice(end);
93
+ }
94
+
95
+ /** The spill file inline while it fits `inlineMax`, past it the persisted block; undefined when unreadable. */
96
+ function wholeOutput(path: string, inlineMax: number): string | undefined {
97
+ try {
98
+ return statSync(path).size <= inlineMax ? readFileSync(path, "utf8") : persistedFilePreview(path);
99
+ } catch {
100
+ return undefined;
101
+ }
102
+ }
103
+
104
+ type TextResult = { content: Array<{ type: string; text?: string }>; details?: unknown };
105
+
106
+ /**
107
+ * Run a foreground shell execute and keep its spill file readable: on a
108
+ * returned result the text and `details.fullOutputPath` are rewritten, and a
109
+ * thrown error is rethrown with its message rewritten, the output cut
110
+ * Claude Code's way (`bound`).
111
+ */
112
+ export async function keepSpillReadable<R extends TextResult>(run: () => Promise<R>, resultsDir: string, bound: OutputBound): Promise<R> {
113
+ const rewrite = (text: string, failed: boolean) => boundSpillIn(text, resultsDir, bound, failed);
114
+ let result: R;
115
+ try {
116
+ result = await run();
117
+ } catch (error) {
118
+ if (error instanceof Error) {
119
+ const message = rewrite(error.message, true);
120
+ if (message !== error.message) {
121
+ const moved = new Error(message);
122
+ moved.stack = error.stack;
123
+ throw moved;
124
+ }
125
+ }
126
+ throw error;
127
+ }
128
+ const details = result.details as { fullOutputPath?: unknown } | undefined;
129
+ const text = result.content.find((block) => block.type === "text" && typeof block.text === "string");
130
+ if (!text?.text) return result;
131
+ const rewritten = rewrite(text.text, (result as { isError?: boolean }).isError === true);
132
+ if (rewritten === text.text) return result;
133
+ const from = piSpillPath(text.text);
134
+ const to = from ? join(resultsDir, basename(from)) : undefined;
135
+ return {
136
+ ...result,
137
+ content: result.content.map((block) => (block === text ? { ...block, text: rewritten } : block)),
138
+ details: details && typeof details === "object" && to ? { ...details, fullOutputPath: to } : result.details,
139
+ };
140
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Claude Code's `SubagentHandback`: the tool every child session (an Agent
3
+ * run, a fork, a workflow agent) returns its result through, and the closing
4
+ * reminder that tells it so (decisions/subagents-workflows.md, "Every child
5
+ * hands back through SubagentHandback"). Claude Code gives it to Opus and
6
+ * Sonnet children only; One Code gives it to every child on every model.
7
+ *
8
+ * The tool records the report in the run's `HandbackSlot` and ends the run
9
+ * (`terminate`, so no further request goes out after a batch of hand-backs).
10
+ * The runner then reports the recorded text as the child's result, and falls
11
+ * back to the child's final message when the tool was never called.
12
+ *
13
+ * The tool keeps Claude Code's exact name, like `Agent` and `SendMessage`,
14
+ * because the harness frames that carry its report already name it ("its
15
+ * SubagentHandback call", lib/notifications.ts). It is harness plumbing: the
16
+ * child permission and hook gates never gate it, and an agent file's `tools`
17
+ * allowlist keeps it.
18
+ *
19
+ * In a fork the tool is deferred: a fork sends the parent's tool list so it
20
+ * reads the parent's prompt cache (subagents/fork-cache.ts), and an eager tool
21
+ * of its own would be dropped from that list. A deferred one reaches the
22
+ * request through a `tool_search` load, as the fork's `SendMessage` does.
23
+ */
24
+
25
+ import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
26
+ import { Type } from "typebox";
27
+ import { DEFER_CHANNEL } from "./deferred.ts";
28
+ import { REMINDER_CHANNEL, type ReminderPayload } from "./reminders.ts";
29
+
30
+ export const SUBAGENT_HANDBACK = "SubagentHandback";
31
+
32
+ /** Claude Code's description of the tool. */
33
+ export const SUBAGENT_HANDBACK_DESCRIPTION =
34
+ "Deliver your final report to the agent that spawned you (your caller). Use it once, for that hand-off only: when your work is complete, call SubagentHandback({message: <your full report>}). The call ends your run, so do everything else first and put everything your caller needs in that one report. It is not a messaging channel: do not use it for progress updates or questions.\n\nOnly a report delivered through SubagentHandback reaches your caller; plain text you write at the end of your run is NOT delivered. There is no recipient parameter: the report can only go to your caller.";
35
+
36
+ /** Claude Code's closing reminder on the child's task message. */
37
+ export const SUBAGENT_HANDBACK_REMINDER =
38
+ "Your final report is delivered through SubagentHandback: when your work is complete, call SubagentHandback({message: <your full report>}). The call ends your run, so make it your last step. Only a SubagentHandback call reaches your caller as your result; plain text you write at the end is not delivered.";
39
+
40
+ /**
41
+ * One Code's line before the reminder in a fork, where the tool is deferred
42
+ * (the fork sends its parent's tool list): it names the load step, which the
43
+ * frozen deferred listing it inherits does not.
44
+ */
45
+ export const SUBAGENT_HANDBACK_LOAD_FIRST =
46
+ 'SubagentHandback is deferred here: load it first with tool_search (query "select:SubagentHandback").';
47
+
48
+ /** Claude Code's result texts for the call. */
49
+ export const HANDBACK_DELIVERED = "Report delivered to your caller.";
50
+ export const HANDBACK_ALREADY_DELIVERED =
51
+ "Nothing was sent: your report was already delivered (SubagentHandback delivers one report). Use SendMessage for anything further, then stop.";
52
+ export const HANDBACK_EMPTY = "message must not be empty: pass your full report as `message`.";
53
+
54
+ /** Where a run keeps the report its child handed back. */
55
+ export interface HandbackSlot {
56
+ /** Record the report; false when this turn already delivered one. */
57
+ recordHandback(message: string): boolean;
58
+ }
59
+
60
+ /** A slot for a one-turn run (a workflow agent): the first report wins. */
61
+ export function oneTurnHandbackSlot(): HandbackSlot & { report?: string } {
62
+ const slot: HandbackSlot & { report?: string } = {
63
+ report: undefined,
64
+ recordHandback(message) {
65
+ if (slot.report !== undefined) return false;
66
+ slot.report = message;
67
+ return true;
68
+ },
69
+ };
70
+ return slot;
71
+ }
72
+
73
+ export function subagentHandbackTool(slot: HandbackSlot): ToolDefinition {
74
+ return {
75
+ name: SUBAGENT_HANDBACK,
76
+ label: "Hand back",
77
+ description: SUBAGENT_HANDBACK_DESCRIPTION,
78
+ parameters: Type.Object({ message: Type.String({ description: "Your full report for your caller" }) }, { additionalProperties: false }) as never,
79
+ async execute(_toolCallId: string, params: unknown) {
80
+ const message = (params as { message?: unknown } | undefined)?.message;
81
+ // Fail loud (decisions/tools.md): an empty report would end the run with nothing for the caller.
82
+ if (typeof message !== "string" || !message.trim()) {
83
+ return { content: [{ type: "text" as const, text: HANDBACK_EMPTY }], details: {}, isError: true };
84
+ }
85
+ if (!slot.recordHandback(message)) {
86
+ return { content: [{ type: "text" as const, text: HANDBACK_ALREADY_DELIVERED }], details: {}, isError: true };
87
+ }
88
+ return { content: [{ type: "text" as const, text: HANDBACK_DELIVERED }], details: { handback: true }, terminate: true };
89
+ },
90
+ } as ToolDefinition;
91
+ }
92
+
93
+ /**
94
+ * The inline extension a child session loads for the hand-back: the tool,
95
+ * deferred in a fork, and the closing reminder. The reminder is a standing
96
+ * block (`sticky-append`) switched on at session start, so it rides the
97
+ * child's task message and every later message of the run (a resident's
98
+ * next turns hand back too), and never an inherited message of a fork.
99
+ */
100
+ export function subagentHandbackExtension(slot: HandbackSlot, options: { deferred?: boolean } = {}) {
101
+ return (pi: ExtensionAPI) => {
102
+ pi.registerTool(subagentHandbackTool(slot));
103
+ if (options.deferred) pi.events.emit(DEFER_CHANNEL, { name: SUBAGENT_HANDBACK, keywords: ["handback", "report", "result", "final", "caller"] });
104
+ pi.on("session_start", () => {
105
+ pi.events.emit(REMINDER_CHANNEL, {
106
+ text: options.deferred ? `${SUBAGENT_HANDBACK_LOAD_FIRST}\n${SUBAGENT_HANDBACK_REMINDER}` : SUBAGENT_HANDBACK_REMINDER,
107
+ key: "subagent-handback",
108
+ placement: "sticky-append",
109
+ } satisfies ReminderPayload);
110
+ });
111
+ };
112
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Which models get Claude Code's mid-conversation `role: "system"` message,
3
+ * and in which wire shape (pure). One function for the system-prompt builder,
4
+ * the reminder hook and the tool hook, so all three agree on every request.
5
+ *
6
+ * The gate is pi's model catalog flag `compat.supportsMidConvoSystemMessages`
7
+ * alone, no list of our own: pi owns the per-model facts and adds models as it
8
+ * confirms them (`working-docs/decisions/tools.md`, "Claude Code's request
9
+ * shape, on every model pi flags for it"). Haiku before 5 is never in it:
10
+ * Anthropic rejects the role there (findings §52); Haiku 5.5 takes it
11
+ * (findings §67). Every unflagged model keeps the context in the first user
12
+ * message.
13
+ */
14
+
15
+ import { parseClaudeId } from "./claude-id.ts";
16
+
17
+ /** The wire shape a system message takes: Anthropic `system`, OpenAI Responses `input`, or Chat Completions `messages`. */
18
+ export type SystemRoleLayout = "anthropic" | "responses" | "completions";
19
+
20
+ /** The fields of a pi `Model` the gate reads. */
21
+ export interface SystemRoleModel {
22
+ id: string;
23
+ api: string;
24
+ compat?: unknown;
25
+ }
26
+
27
+ /** pi-ai's Responses APIs: requests carry `input` items and flat `{ type, name }` tools. */
28
+ export const RESPONSES_APIS: ReadonlySet<string> = new Set(["openai-responses", "azure-openai-responses", "openai-codex-responses"]);
29
+
30
+ /** The wire shape of a pi API, for the three shapes One Code rewrites; undefined for the rest (Google, Bedrock). */
31
+ export function wireShape(api: string): SystemRoleLayout | undefined {
32
+ if (api === "anthropic-messages") return "anthropic";
33
+ if (RESPONSES_APIS.has(api)) return "responses";
34
+ if (api === "openai-completions") return "completions";
35
+ return undefined;
36
+ }
37
+
38
+ /** The payload field that holds the conversation for a wire shape. */
39
+ export function messagesKey(shape: SystemRoleLayout): "input" | "messages" {
40
+ return shape === "responses" ? "input" : "messages";
41
+ }
42
+
43
+ export function systemRoleLayout(model: SystemRoleModel | undefined): SystemRoleLayout | undefined {
44
+ if (!model) return undefined;
45
+ const compat = model.compat as { supportsMidConvoSystemMessages?: unknown } | undefined;
46
+ if (compat?.supportsMidConvoSystemMessages !== true) return undefined;
47
+ if (model.api === "anthropic-messages" && isHaikuBeforeFive(model.id)) return undefined;
48
+ return wireShape(model.api);
49
+ }
50
+
51
+ /** A Haiku older than Haiku 5 in any provider's spelling; the old `claude-3-5-haiku` order parses as none. */
52
+ function isHaikuBeforeFive(id: string): boolean {
53
+ const claude = parseClaudeId(id);
54
+ return claude ? claude.family === "haiku" && claude.major < 5 : id.includes("haiku");
55
+ }
56
+
57
+ /** One context block that moves: its text as injected on the user message, and its text inside the system message. */
58
+ export interface MovedBlock {
59
+ /** The block exactly as the user message carries it (`<system-reminder>` frame and suffix included). */
60
+ framed: string;
61
+ /** The unwrapped text, as Claude Code's system message joins it. */
62
+ inner: string;
63
+ }
64
+
65
+ /**
66
+ * The role a system message takes on the OpenAI APIs: pi's own rule per API
67
+ * (`developer` for a reasoning model that accepts it, else `system`).
68
+ */
69
+ export function instructionRole(layout: SystemRoleLayout, model: { reasoning?: boolean; compat?: unknown }): "system" | "developer" {
70
+ if (layout === "anthropic") return "system";
71
+ const developer = (model.compat as { supportsDeveloperRole?: boolean } | undefined)?.supportsDeveloperRole;
72
+ const accepts = layout === "responses" ? developer !== false : developer === true;
73
+ return model.reasoning && accepts ? "developer" : "system";
74
+ }
75
+
76
+ /** A wire message and content part, as far as the payload rewrites read them. */
77
+ export type WireMessage = { role?: unknown; type?: unknown; content?: unknown; output?: unknown; call_id?: unknown; tool_call_id?: unknown };
78
+ export type WirePart = { type?: unknown; text?: unknown; content?: unknown; tool_use_id?: unknown; cache_control?: unknown };
79
+
80
+ /**
81
+ * The parts with those `drop` accepts removed, keeping pi's cache mark: when a
82
+ * removed part carried it, it moves to the new last part. Undefined when every
83
+ * part would go (a message cannot be left empty) or nothing is removed.
84
+ */
85
+ export function withoutParts(parts: readonly WirePart[], drop: (part: WirePart, index: number) => boolean): WirePart[] | undefined {
86
+ let mark: unknown;
87
+ const kept = parts.filter((part, index) => {
88
+ if (!drop(part, index)) return true;
89
+ if (part?.cache_control !== undefined) mark = part.cache_control;
90
+ return false;
91
+ });
92
+ if (kept.length === parts.length || kept.length === 0) return undefined;
93
+ if (mark === undefined || kept[kept.length - 1].cache_control !== undefined) return kept;
94
+ return [...kept.slice(0, -1), { ...kept[kept.length - 1], cache_control: mark }];
95
+ }
96
+
97
+ /**
98
+ * The value stored under a pi call id for the id a request carries: the same
99
+ * id, the Responses call id before pi's `|item` suffix, or the id pi-ai's
100
+ * Anthropic conversion rewrote (any other character to `_`, at most 64), as it
101
+ * does for a call made on another provider before a model switch.
102
+ */
103
+ export function byWireCallId<V>(stored: ReadonlyMap<string, V>, wireId: string): V | undefined {
104
+ if (stored.has(wireId)) return stored.get(wireId);
105
+ for (const [id, value] of stored) {
106
+ if (id.split("|")[0] === wireId || id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 64) === wireId) return value;
107
+ }
108
+ return undefined;
109
+ }
110
+
111
+ /** A system message with `text` in the API's own shape. */
112
+ export function systemMessage(layout: SystemRoleLayout, role: "system" | "developer", text: string): WireMessage {
113
+ return layout === "anthropic" ? { role: "system", content: [{ type: "text", text }] } : { role, content: text };
114
+ }
115
+
116
+ /**
117
+ * The payload with the moved context blocks lifted off the user message that
118
+ * carries them and joined, unwrapped and a blank line apart, into one system
119
+ * message placed right after it, in the API's own shape (Claude Code's layout,
120
+ * `decisions/tools.md`). Blocks are found by their exact text, never by
121
+ * position: a fork's request drops the parent's first message, and then nothing
122
+ * moves. The cache mark is reseated afterwards (anthropic-payload.ts
123
+ * reseatMessageMark). Undefined when nothing changes.
124
+ */
125
+ export function withSystemRoleContext(
126
+ payload: Record<string, unknown>,
127
+ layout: SystemRoleLayout,
128
+ moved: readonly MovedBlock[],
129
+ role: "system" | "developer",
130
+ ): Record<string, unknown> | undefined {
131
+ if (moved.length === 0) return undefined;
132
+ const key = messagesKey(layout);
133
+ const messages = payload[key];
134
+ if (!Array.isArray(messages)) return undefined;
135
+ const wanted = new Set(moved.map((block) => block.framed));
136
+ const index = messages.findIndex(
137
+ (message: WireMessage) => message?.role === "user" && Array.isArray(message.content) && (message.content as WirePart[]).some((part) => wanted.has(part?.text as string)),
138
+ );
139
+ if (index === -1) return undefined;
140
+
141
+ const carrier = messages[index] as WireMessage & { content: WirePart[] };
142
+ const found = new Set<string>();
143
+ const kept = carrier.content.filter((part) => {
144
+ const text = part?.text;
145
+ if (typeof text !== "string" || !wanted.has(text) || found.has(text)) return true;
146
+ found.add(text);
147
+ return false;
148
+ });
149
+ const text = moved
150
+ .filter((block) => found.has(block.framed))
151
+ .map((block) => block.inner)
152
+ .join("\n\n");
153
+
154
+ const user: WireMessage = { ...carrier, content: kept };
155
+ return { ...payload, [key]: [...messages.slice(0, index), user, systemMessage(layout, role, text), ...messages.slice(index + 1)] };
156
+ }