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
@@ -1,92 +1,91 @@
1
1
  /**
2
- * System-reminder queue — the steering mechanism shared by all One Code
3
- * extensions. Reminders are injected transiently into the outgoing request via
4
- * pi's `context` event; they are never written to the session file.
2
+ * System reminders: the steering blocks One Code adds to a request, rendered
3
+ * once and then kept as sent (working-docs/features/lifecycle-grid-hunt/design.md
4
+ * section 5). The ledger (extensions/ledger) sends through pi only the
5
+ * messages after the last request's coverage, the tail, and takes everything
6
+ * above it from that request's record. So a reminder is rendered only onto the
7
+ * tail, rides the request that first carried it, and stays in the record
8
+ * byte for byte; nothing here is restored or re-rendered later.
5
9
  *
6
10
  * Cross-extension contract: emit on the event bus channel
7
- * `one-code:system-reminder` with `{ text, scope?, placement? }` to enqueue from
8
- * any extension (including third-party ones). The queue instance itself lives
9
- * in the system-reminder extension; do not import one from here (jiti gives
10
- * every extension its own module instance, so a shared export steers nothing).
11
+ * `one-code:system-reminder` with `{ text, placement? }`. The live
12
+ * state lives in the system-reminder extension; do not import one from here
13
+ * (jiti gives every extension its own module instance, so a shared export
14
+ * steers nothing).
11
15
  *
12
- * Three placements, chosen by what kind of fact the reminder is:
16
+ * Four placements, chosen by what kind of fact the reminder is:
13
17
  *
14
- * - `first-prepend` — SESSION CONTEXT that never changes mid-session (deferred
15
- * tools, agent catalog, MCP instructions, skills, `# claudeMd`). Front of the
16
- * FIRST user message, before the user's text, ordered by `order`, exactly
17
- * Claude Code's first-message stack. Byte-stable, so the cached prefix holds.
18
- * - `sticky-append` — SESSION STATE that switches on and off (auto mode, plan
19
- * mode, a worktree session, ultracode). Appended to the user message that
20
- * opened the turn during which the state switched on (`since`) and to EVERY
21
- * user message after it, so message N carries the block on every later
22
- * request too: the prefix stays byte-stable while the state holds, and the
23
- * model still sees the reminder on its latest turn. Switching off drops the
24
- * blocks (one cache miss, once). The old `last-append` + every-turn
25
- * combination re-cached the previous turn on every turn — permanently, in
26
- * auto mode.
27
- * - `last-append` — ONE-SHOT STEERING about what just happened (a deferred tool
28
- * miss, a file changed under the model, a mode change). Delivered where the
29
- * model reads next, and then KEPT there so the message never changes again:
30
- * a one-shot pending when a tool result is stored is written INTO that result
31
- * (`takeOneShots` from the `tool_result` hook — Claude Code persists its
32
- * mid-turn reminders the same way, so the transcript carries them and no
33
- * request ever re-caches the result); a one-shot still pending when a request
34
- * goes out is `pin`ned to the request's tail (the trailing tool result by call
35
- * id, else the last user message by timestamp) and re-attached to that same
36
- * message on every later request, byte-identical. Pins are process memory
37
- * (a `--resume` drops them: one miss, once).
38
- * - `user-prepend` — LOCAL-COMMAND BREADCRUMBS (`/clear`, `/model`, a panel
39
- * command): what Claude Code shows the model when the user ran a slash
40
- * command. Bare text blocks placed BEFORE the user's text on the next user
41
- * message that opens a request (after the `first-prepend` stack on message
42
- * one), then pinned there like a delivered one-shot. They never ride a tool
43
- * result — a command run mid-turn waits for the next prompt. The caveat
44
- * block is queued `once` per pending run (a second command joins the first
45
- * caveat); the command and stdout blocks are never collapsed.
46
- * `lib/local-command.ts` builds the blocks.
18
+ * - `first-prepend`: SESSION CONTEXT (environment, deferred tools, agent
19
+ * catalog, MCP instructions, skills, `# claudeMd`). Live state, kept current
20
+ * by its owner; rendered only on a request that opens an epoch, in front of
21
+ * the first user message and ordered by `order`: Claude Code's first-message
22
+ * stack. Inside an epoch message 1 is the record, so a change is not
23
+ * rendered; its owner announces the difference at render time
24
+ * (RENDER_CHANNEL: live minus what the epoch told).
25
+ * - `sticky-append`: SESSION STATE that switches on and off (auto mode, plan
26
+ * mode, a worktree session, ultracode). Appended to every user message of
27
+ * the tail stamped at or after the state switched on (`since`). Switched on
28
+ * mid-turn, it first rides the switching call's result (`toolCallId`), or
29
+ * the first result after the switch, never the already-sent turn opener.
30
+ * Removing it closes it: later messages stop carrying it, and the ones
31
+ * already sent keep it in the record.
32
+ * - `last-append`: ONE-SHOT steering about what just happened (a deferred
33
+ * tool miss, a file changed under the model, a mode change). Persisted when
34
+ * emitted as a `one-code:pending` entry, so it survives `/reload` and quit
35
+ * and is gone with an abandoned `/tree` branch; delivered on the tail of the
36
+ * next request (the call's own result when `toolCallId` names one in the
37
+ * tail, the prompt's own message when `promptTimestamp` names one, else
38
+ * the trailing tool result, else the last user message), and
39
+ * recorded as consumed by that request.
40
+ * - `user-prepend`: LOCAL-COMMAND BREADCRUMBS (`/clear`, `/model`, a panel
41
+ * command): pending like a one-shot, but placed only before the text of a
42
+ * prompt that opens a request; mid-turn it waits. The caveat block is
43
+ * queued `once` per run (a second command joins the first caveat); the
44
+ * command and stdout blocks are never collapsed. `lib/local-command.ts`
45
+ * builds them.
47
46
  *
48
- * Persisted or pinned is decided by WHEN the one-shot is emitted, which is a
49
- * load-order rule: system-reminder's `tool_result` hook takes the one-shots
50
- * pending at that moment, so only an extension whose own `tool_result` handler
51
- * runs BEFORE it (listed before `system-reminder` in `package.json`
52
- * `pi.extensions`, like `context-budget`) or that emits from `tool_call` gets
53
- * its block written into the result. Emitting from a later `tool_result`
54
- * handler, or from `tool_execution_end` (pi runs it AFTER the `tool_result`
55
- * hooks, findings §3), lands the block on the same result but as a pin. Same
56
- * wording on the wire, so this only matters for `--resume` and for ordering
57
- * after the `<total_tokens>` line.
58
- *
59
- * A compaction summary counts as a user message for anchoring (pi renders it as
60
- * one), so the context stack survives a compaction that left no user turn. So
61
- * does a `custom` harness message (task notification, wakeup, hook context):
62
- * pi sends it as a user message, and a turn it opens has no other user turn.
63
- *
64
- * Retry safety falls out of this: pi re-runs the `context` transform per LLM
65
- * attempt, and both a persisted and a pinned one-shot are present on every
66
- * attempt by construction.
47
+ * A compaction summary counts as a user message for the stack (pi renders it
48
+ * as one), and so does a `custom` harness message (task notification, wakeup,
49
+ * hook context): pi sends it as a user message, and a turn it opens has no
50
+ * other user turn.
67
51
  */
68
52
 
69
53
  import type { AgentMessage } from "@earendil-works/pi-agent-core";
70
54
  import type { ImageContent, TextContent } from "@earendil-works/pi-ai";
55
+ import type { BlockFact, FileVersion, ToldFact } from "./ledger/records.ts";
56
+ import { contentHash } from "./ledger/wire.ts";
57
+ import type { MovedBlock } from "./system-role.ts";
71
58
 
72
59
  export const REMINDER_CHANNEL = "one-code:system-reminder";
73
60
 
74
- /** "next-turn" fires once on the next LLM call; "every-turn" fires on every call until removed. */
75
- export type ReminderScope = "next-turn" | "every-turn";
61
+ /**
62
+ * Emitted by the system-reminder extension once per request, while it renders
63
+ * the tail (a `RenderRequest`). An owner of a capability block subscribes and
64
+ * adds the blocks this request needs from live state and what the epoch has
65
+ * told: on a request inside an epoch, the names live now that the epoch has
66
+ * not told yet. Computed when the request goes out, never when a change is
67
+ * queued, so a capability that came and went before the request is never
68
+ * announced.
69
+ */
70
+ export const RENDER_CHANNEL = "one-code:render";
76
71
 
77
72
  /** See the module header for which placement fits which kind of reminder. */
78
73
  export type ReminderPlacement = "first-prepend" | "sticky-append" | "last-append" | "user-prepend";
79
74
 
80
- /** Where a delivered one-shot stays: a tool result (by call id) or a user turn (by timestamp). */
81
- export type PinAnchor = { kind: "toolResult"; toolCallId: string } | { kind: "user"; timestamp: number };
82
-
83
75
  /**
84
76
  * `order` values for the `first-prepend` context stack, matching Claude Code's
85
- * fixed sequence on the first user message: deferred tools → agent catalog → MCP
86
- * instructions → skills → the `# claudeMd` block (last, just before the user's
87
- * text). Gaps leave room for One Code-specific reminders (e.g. subagent models).
77
+ * fixed sequence on the first user message: environment → model line → deferred
78
+ * tools → agent catalog → MCP instructions → skills → the instructions block →
79
+ * the context block → the date (just before the user's text). Gaps leave room
80
+ * for One Code-specific reminders (e.g. subagent models). On a model that takes
81
+ * a mid-conversation system message, everything below `claudeMd`, and the date,
82
+ * moves into that message instead (`movesToSystemRole`, lib/system-role.ts).
88
83
  */
89
84
  export const CONTEXT_ORDER = {
85
+ environment: 1,
86
+ modelLine: 2,
87
+ /** One Code's note that a `-p`/json run ends with its turn (`lib/notifications.ts oneShotSessionNote`); not Claude Code's. */
88
+ oneShot: 3,
90
89
  deferredTools: 10,
91
90
  subagentModels: 20,
92
91
  agents: 21,
@@ -94,263 +93,364 @@ export const CONTEXT_ORDER = {
94
93
  delegation: 22,
95
94
  mcp: 30,
96
95
  skills: 40,
96
+ /** Claude Code's auto-mode note; only in the system message (`systemRoleOnly`). */
97
+ autoModeNote: 45,
98
+ /** The `<total_tokens>` line the first prompt carries (later prompts: lib/turn-budget-layout.ts). */
99
+ totalTokens: 46,
97
100
  // CLAUDE.md-family (with AGENTS.md as a per-directory fallback when a directory
98
101
  // has no CLAUDE.md) — CLAUDE.md > AGENTS.md.
99
102
  claudeMd: 50,
100
- // One Code's own instructions ride in their own block AFTER the # claudeMd
103
+ // One Code's own instructions ride in their own block AFTER the instructions
101
104
  // block (higher `order` = closer to the user text = higher precedence), so
102
105
  // ONECODE.md takes precedence over CLAUDE.md/AGENTS.md. Not part of CC.
103
- oneCodeMd: 60,
106
+ oneCodeMd: 52,
107
+ /** The user's email and the git snapshot. */
108
+ context: 54,
109
+ date: 56,
104
110
  } as const;
105
111
 
106
- /** A drained reminder with everything the injector needs to place it. */
107
- export interface ReminderEntry {
108
- text: string;
109
- placement: ReminderPlacement;
110
- order: number;
111
- /**
112
- * Literal text appended AFTER the closing `</system-reminder>` tag. Claude
113
- * Code's `# claudeMd` block ends `</system-reminder>\n\n` on the wire; this
114
- * reproduces that byte-for-byte. Defaults to "".
115
- */
116
- suffix?: string;
117
- /**
118
- * `sticky-append` only: when the state switched on. The user message that
119
- * opened the turn in progress at that moment, and every user message after
120
- * it, carry the block.
121
- */
122
- since?: number;
123
- /** Set on a pinned one-shot: the exact message it rides on every request. */
124
- pin?: PinAnchor;
125
- /**
126
- * Emit `text` as-is instead of inside a `<system-reminder>` frame — for the
127
- * few Claude Code blocks that ride bare, like `<total_tokens>` after a tool
128
- * result. Defaults to false.
129
- */
130
- raw?: boolean;
112
+ /**
113
+ * Whether a `first-prepend` block of this order leaves the first user message
114
+ * for the mid-conversation system message on a model that takes one: the
115
+ * session facts below the instructions block, and the date, which Claude Code
116
+ * puts last in that message. The instructions, One Code's own block and the
117
+ * context block stay in the user message.
118
+ */
119
+ export function movesToSystemRole(order: number): boolean {
120
+ return order < CONTEXT_ORDER.claudeMd || order === CONTEXT_ORDER.date;
131
121
  }
132
122
 
133
123
  export interface ReminderPayload {
134
124
  text?: string;
135
- scope?: ReminderScope;
136
- /** Key so a reminder can be replaced (next-turn) or replaced/removed (every-turn). */
125
+ /** Key so a reminder can be replaced (a pending one-shot, a live block) or removed (a live block). */
137
126
  key?: string;
138
- /** Remove the every-turn reminder registered under `key` instead of enqueueing. */
127
+ /** Remove the live block or close the sticky state under `key`. */
139
128
  remove?: boolean;
140
129
  /** Where in the message stack this reminder lands. Defaults to `last-append`. */
141
130
  placement?: ReminderPlacement;
142
131
  /** Sort key among `first-prepend` reminders (Claude Code order). Defaults to 0. */
143
132
  order?: number;
144
- /** Literal text appended after the closing `</system-reminder>` tag. Defaults to "". */
145
- suffix?: string;
146
133
  /** Emit the text bare, with no `<system-reminder>` frame. */
147
134
  raw?: boolean;
148
- /** `user-prepend` only: skip when the same text is already pending (the shared caveat). */
135
+ /** `first-prepend` only: sent only inside the mid-conversation system message, never on a user message. */
136
+ systemRoleOnly?: boolean;
137
+ /** `sticky-append` only: never on the message that carries the context stack, which already says the same. */
138
+ skipStackCarrier?: boolean;
139
+ /** `user-prepend` only: placed once per request, however many are pending (the shared caveat). */
149
140
  once?: boolean;
141
+ /** `user-prepend` only: on a request that does not open with a prompt (mid-turn, an overflow retry), the tail carries it instead. */
142
+ orTail?: boolean;
143
+ /** A one-shot dropped, undelivered, at a later compaction (a post-compaction restore). */
144
+ untilCompaction?: boolean;
145
+ /** A one-shot only: the files whose whole content it shows, recorded on its pending entry (`PendingRecordData.observations`). */
146
+ observations?: FileVersion[];
147
+ /** A one-shot only: the changed file versions it names, recorded on its pending entry (`PendingRecordData.notified`). */
148
+ notified?: FileVersion[];
150
149
  /**
151
- * `sticky-append` only: anchor the block from this timestamp instead of now —
152
- * `0` puts it on every user message in the session, including ones a resume
153
- * brought back (the `<total_tokens>` line rides every user message in CC).
150
+ * `sticky-append` only: the state is on from this timestamp instead of now.
151
+ * `0` puts it on every user message (the `<total_tokens>` line rides every
152
+ * user message in Claude Code).
154
153
  */
155
154
  since?: number;
155
+ /**
156
+ * The tool call whose result this reminder belongs to. A one-shot rides that
157
+ * result when the request carries it; a sticky state rides it first. Both
158
+ * avoid attaching a switch or countdown to another call in a parallel batch.
159
+ */
160
+ toolCallId?: string;
161
+ /**
162
+ * A one-shot only: the timestamp of the user message it belongs to (a
163
+ * prompt's hook context). It rides that message when the request carries
164
+ * it, so a prompt whose request failed keeps its own context when the
165
+ * next prompt resends it.
166
+ */
167
+ promptTimestamp?: number;
168
+ /** `first-prepend` only: what the block tells the model, recorded when an epoch sends it. */
169
+ facts?: ToldFact[];
156
170
  }
157
171
 
158
- interface StoredReminder extends ReminderEntry {
159
- key?: string;
172
+ /** The fact that the whole-text block `key` told `value` (a text, or a snapshot such as a git status). */
173
+ export function toldBlockFact(key: string, value: unknown): BlockFact {
174
+ return { kind: "block", key, hash: contentHash(value) };
160
175
  }
161
176
 
162
- type EnqueueOptions = {
163
- scope?: ReminderScope;
164
- key?: string;
165
- placement?: ReminderPlacement;
166
- order?: number;
167
- suffix?: string;
177
+ /** A live `first-prepend` block. */
178
+ export interface StackEntry {
179
+ key: string;
180
+ text: string;
181
+ order: number;
168
182
  raw?: boolean;
169
- /** Test seam / explicit anchor for `sticky-append`; defaults to now. */
170
- since?: number;
171
- /** `user-prepend` only: skip when the same text is already pending. */
172
- once?: boolean;
173
- };
183
+ systemRoleOnly?: boolean;
184
+ facts?: ToldFact[];
185
+ }
174
186
 
175
- export class ReminderQueue {
176
- private nextTurn: StoredReminder[] = [];
177
- /** Delivered one-shots, each fixed to the message it first rode (see `pin`). */
178
- private pinned: StoredReminder[] = [];
179
- private everyTurn = new Map<string, StoredReminder>();
180
- private readonly now: () => number;
187
+ /** A live `sticky-append` state. */
188
+ export interface StickyEntry {
189
+ key: string;
190
+ text: string;
191
+ raw?: boolean;
192
+ /** When it switched on: user messages stamped at or after it carry it. */
193
+ since: number;
194
+ toolCallId?: string;
195
+ skipStackCarrier?: boolean;
196
+ }
181
197
 
182
- constructor(now: () => number = Date.now) {
183
- this.now = now;
184
- }
198
+ /** A sticky state as a render places it: `delivered` once an answered request in the epoch carried it (its `stickyFact` is told), so a tool-result pin is only for its first delivery. */
199
+ export type StickyToPlace = StickyEntry & { delivered: boolean };
185
200
 
186
- enqueue(text: string, opts?: EnqueueOptions): void {
187
- if (!text.trim()) return;
188
- const placement = opts?.placement ?? "last-append";
189
- const entry: StoredReminder = {
190
- text,
191
- placement,
192
- order: opts?.order ?? 0,
193
- suffix: opts?.suffix,
194
- key: opts?.key,
195
- raw: opts?.raw,
196
- };
197
- if (placement === "sticky-append") {
198
- // A standing reminder re-emitted with the SAME text (plan mode re-emits
199
- // every turn) keeps its anchor, so the blocks on earlier messages do not
200
- // move. Different text under the same key is a new fact (plan → auto
201
- // under the shared "permission-mode" key) and anchors from now.
202
- const previous = opts?.key !== undefined ? this.everyTurn.get(opts.key) : undefined;
203
- entry.since =
204
- opts?.since ??
205
- (previous?.placement === "sticky-append" && previous.text === text ? previous.since : undefined) ??
206
- this.now();
207
- }
208
- if (opts?.scope === "every-turn") {
209
- this.everyTurn.set(opts.key ?? text, entry);
210
- return;
211
- }
212
- // A breadcrumb run shares one caveat block (`once`): the same text pending
213
- // twice would show the model two identical blocks in a row. Only the
214
- // announcer's caveat asks for this — two commands with the same (empty)
215
- // stdout are two commands, and both stay.
216
- if (placement === "user-prepend" && opts?.once && this.nextTurn.some((r) => r.placement === placement && r.text === text)) return;
217
- // A keyed next-turn reminder replaces its predecessor, so a rapidly
218
- // re-emitted state change (cycling permission modes) announces only
219
- // where it settled.
220
- if (opts?.key) this.nextTurn = this.nextTurn.filter((r) => r.key !== opts.key);
221
- this.nextTurn.push(entry);
222
- }
201
+ /** A one-shot to place on this request: a pending entry, or a block an owner rendered for it. */
202
+ export interface OneShot {
203
+ /** The pending entry's id; absent on a render-time block. */
204
+ id?: string;
205
+ text: string;
206
+ raw?: boolean;
207
+ placement: "last-append" | "user-prepend";
208
+ toolCallId?: string;
209
+ promptTimestamp?: number;
210
+ once?: boolean;
211
+ orTail?: boolean;
212
+ facts?: ToldFact[];
213
+ }
223
214
 
224
- remove(key: string): void {
225
- this.everyTurn.delete(key);
226
- }
215
+ /** What a capability owner adds on RENDER_CHANNEL. */
216
+ export interface RenderedBlock {
217
+ text: string;
218
+ /** What the block tells the model, recorded with the request that carries it. */
219
+ facts?: ToldFact[];
220
+ }
227
221
 
228
- /**
229
- * Take the pending `last-append` one-shots out of the queue — for the
230
- * `tool_result` hook, which writes them into the stored result. Other
231
- * placements stay queued for the next request.
232
- */
233
- takeOneShots(): ReminderEntry[] {
234
- const { matching, rest } = partition(this.nextTurn, (r) => r.placement === "last-append");
235
- this.nextTurn = rest;
236
- return matching.map(strip);
237
- }
222
+ /** The RENDER_CHANNEL payload. */
223
+ export interface RenderRequest {
224
+ /** True when the request opens an epoch: the live stack is rendered, so owners add nothing. */
225
+ epoch: boolean;
226
+ /** What the epoch has told the model so far, by fact kind. */
227
+ told: ReadonlyMap<string, ReadonlySet<string>>;
228
+ /** Modes the epoch has told, by key: on or off as last told. */
229
+ modes: ReadonlyMap<string, boolean>;
230
+ /** Whole-text blocks the epoch has told, by key: the content hash of the text last told (`toldBlockFact`). */
231
+ toldBlocks: ReadonlyMap<string, string>;
232
+ /** The request's model. */
233
+ model: { provider?: string; api?: string; id?: string; compat?: unknown } | undefined;
234
+ /** Blocks for the tail of this request. */
235
+ blocks: RenderedBlock[];
236
+ }
238
237
 
239
- /**
240
- * Fix the pending one-shots of `placement` to `anchor` — the message they
241
- * ride on this request — so every later request re-attaches them there
242
- * unchanged. Called by the owner at `context` time, after it has decided the
243
- * tail: `last-append` goes on the tail (`tailAnchor`), `user-prepend` only
244
- * on a request that ends in a user-like message (`openingUserAnchor`) —
245
- * mid-turn, breadcrumbs stay queued.
246
- */
247
- pin(anchor: PinAnchor, placement: "last-append" | "user-prepend" = "last-append"): void {
248
- const { matching, rest } = partition(this.nextTurn, (r) => r.placement === placement);
249
- for (const entry of matching) this.pinned.push({ ...entry, pin: anchor });
250
- this.nextTurn = rest;
238
+ /**
239
+ * The live reminder state: the `first-prepend` stack and the open sticky
240
+ * states, as their owners last emitted them. One-shots are not held here:
241
+ * they are persisted as pending entries when emitted.
242
+ */
243
+ export class LiveReminders {
244
+ private stack = new Map<string, StackEntry>();
245
+ /** Open states in the order they switched on. */
246
+ private sticky = new Map<string, StickyEntry>();
247
+ private readonly now: () => number;
248
+
249
+ constructor(now: () => number = Date.now) {
250
+ this.now = now;
251
251
  }
252
252
 
253
253
  /**
254
- * Everything to inject on this request: every-turn state, pinned one-shots,
255
- * then whatever is still pending. A `user-prepend` breadcrumb leaves the
256
- * queue only through `pin(anchor, "user-prepend")` (it must land on a user
257
- * message, not a tool result), so an unpinned one stays for the next request. `messages` is the request being built:
258
- * pins whose anchor is no longer in it (compacted away, or left behind on
259
- * another branch) are dropped here, as part of draining, so no caller can
260
- * forget the step. Nothing else is ever evicted: a pin is one small object,
261
- * and removing a pin whose message is still in context would change that
262
- * message and re-cache everything after it — the old "oldest past 400" cap
263
- * did exactly that (CACHE-REVIEW-2026-09-04 M3).
254
+ * Apply one reminder payload. Returns "one-shot" for a `last-append` or
255
+ * `user-prepend` reminder, which the caller persists as a pending entry.
264
256
  */
265
- drain(messages: AgentMessage[]): ReminderEntry[] {
266
- if (this.pinned.length > 0) {
267
- const locate = pinLocator(messages);
268
- this.pinned = this.pinned.filter((entry) => locate(entry.pin as PinAnchor) !== -1);
257
+ apply(payload: ReminderPayload): "live" | "one-shot" | "ignored" {
258
+ const placement = payload.placement ?? "last-append";
259
+ if (payload.remove) {
260
+ if (!payload.key) return "ignored";
261
+ this.stack.delete(payload.key);
262
+ this.sticky.delete(payload.key);
263
+ return "live";
269
264
  }
270
- const { matching: held, rest: pending } = partition(this.nextTurn, (r) => r.placement === "user-prepend");
271
- this.nextTurn = held;
272
- return [...[...this.everyTurn.values()].map(strip), ...this.pinned.map(strip), ...pending.map(strip)];
265
+ const text = payload.text;
266
+ if (typeof text !== "string" || !text.trim()) return "ignored";
267
+ if (placement === "last-append" || placement === "user-prepend") return "one-shot";
268
+ const key = payload.key ?? text;
269
+ if (placement === "first-prepend") {
270
+ const entry: StackEntry = { key, text, order: payload.order ?? 0 };
271
+ if (payload.raw) entry.raw = true;
272
+ if (payload.systemRoleOnly) entry.systemRoleOnly = true;
273
+ if (payload.facts) entry.facts = payload.facts;
274
+ this.stack.set(key, entry);
275
+ return "live";
276
+ }
277
+ const previous = this.sticky.get(key);
278
+ const same = previous && previous.text === text && !!previous.raw === !!payload.raw && !!previous.skipStackCarrier === !!payload.skipStackCarrier;
279
+ // Re-emitting an unchanged standing state keeps its start and delivery; a
280
+ // changed one is a new state from now.
281
+ if (same && (payload.since === undefined || payload.since === previous.since)) return "live";
282
+ const entry: StickyEntry = { key, text, since: payload.since ?? this.now() };
283
+ if (payload.raw) entry.raw = true;
284
+ if (payload.skipStackCarrier) entry.skipStackCarrier = true;
285
+ if (payload.toolCallId) entry.toolCallId = payload.toolCallId;
286
+ this.sticky.delete(key);
287
+ this.sticky.set(key, entry);
288
+ return "live";
273
289
  }
274
290
 
275
- get size(): number {
276
- return this.everyTurn.size + this.nextTurn.length + this.pinned.length;
291
+ /** The stack in Claude Code's order (`order`, then emission order). */
292
+ stackEntries(): StackEntry[] {
293
+ return [...this.stack.values()].map((entry, i) => ({ entry, i })).sort((a, b) => a.entry.order - b.entry.order || a.i - b.i).map((x) => x.entry);
277
294
  }
278
295
 
279
- /** True when a one-shot of `placement` is waiting to be delivered (the owner decides where). */
280
- hasPending(placement: "last-append" | "user-prepend"): boolean {
281
- return this.nextTurn.some((r) => r.placement === placement);
296
+ stickyEntries(): StickyEntry[] {
297
+ return [...this.sticky.values()];
282
298
  }
299
+ }
283
300
 
284
- /** `hasPending("last-append")` — kept for the existing callers and tests. */
285
- get hasPendingOneShots(): boolean {
286
- return this.hasPending("last-append");
287
- }
301
+ /** The told fact of a sticky state's first delivery: it follows the branch, and a failed request's is revoked. */
302
+ export function stickyFact(entry: Pick<StickyEntry, "key" | "text" | "since">): BlockFact {
303
+ return toldBlockFact(`sticky:${entry.key}`, { since: entry.since, text: entry.text });
288
304
  }
289
305
 
290
- /** One pass: the entries `test` accepts and the rest, in order. */
291
- function partition<T>(items: T[], test: (item: T) => boolean): { matching: T[]; rest: T[] } {
292
- const matching: T[] = [];
293
- const rest: T[] = [];
294
- for (const item of items) (test(item) ? matching : rest).push(item);
295
- return { matching, rest };
306
+ export interface RenderInput {
307
+ /** The request opens an epoch: the stack goes on the first user message. */
308
+ epoch: boolean;
309
+ /** The model takes a mid-conversation system message (`systemRoleOnly` blocks render, `moved` is filled). */
310
+ layout: boolean;
311
+ stack: readonly StackEntry[];
312
+ sticky: readonly StickyToPlace[];
313
+ oneShots: readonly OneShot[];
296
314
  }
297
315
 
298
- function strip(r: StoredReminder): ReminderEntry {
299
- const entry: ReminderEntry = { text: r.text, placement: r.placement, order: r.order, suffix: r.suffix };
300
- if (r.raw) entry.raw = true;
301
- if (r.since !== undefined) entry.since = r.since;
302
- if (r.pin !== undefined) entry.pin = r.pin;
303
- return entry;
316
+ export interface RenderResult {
317
+ messages: AgentMessage[];
318
+ /** The stack blocks that move to the mid-conversation system message (epoch and layout only). */
319
+ moved: MovedBlock[];
320
+ /** True when the stack was placed. */
321
+ stackPlaced: boolean;
322
+ /** The sticky states this request carried. */
323
+ stickyPlaced: string[];
324
+ /** The one-shots this request carried. */
325
+ placed: OneShot[];
304
326
  }
305
327
 
328
+ type ContentBlock = TextContent | ImageContent;
329
+
306
330
  /**
307
- * One pass over `messages` → a lookup from a pin to the index of the message it
308
- * rides (-1 when that message left the context). Built once per request by the
309
- * queue's drain and once by injectReminders, so pin count never multiplies the
310
- * message count on the per-request path.
331
+ * The request's messages (its tail, or all of them on an epoch) with the
332
+ * reminders placed, and what was placed. The input is not mutated. A one-shot
333
+ * that finds no anchor is not placed and stays pending.
311
334
  */
312
- function pinLocator(messages: AgentMessage[]): (pin: PinAnchor) => number {
313
- const byToolCall = new Map<string, number>();
314
- const byTimestamp = new Map<number, number>();
315
- messages.forEach((m, index) => {
316
- if (m.role === "toolResult") {
317
- const id = (m as { toolCallId?: string }).toolCallId;
318
- if (typeof id === "string" && !byToolCall.has(id)) byToolCall.set(id, index);
319
- } else if (isUserLike(m)) {
320
- const stamp = (m as { timestamp?: number }).timestamp;
321
- if (typeof stamp === "number" && !byTimestamp.has(stamp)) byTimestamp.set(stamp, index);
335
+ export function renderReminders(messages: readonly AgentMessage[], input: RenderInput): RenderResult {
336
+ const before = new Map<number, TextContent[]>();
337
+ const after = new Map<number, TextContent[]>();
338
+ const push = (map: Map<number, TextContent[]>, index: number, blocks: TextContent[]) => {
339
+ if (index < 0 || blocks.length === 0) return;
340
+ map.set(index, [...(map.get(index) ?? []), ...blocks]);
341
+ };
342
+ const firstUser = messages.findIndex(isUserLike);
343
+ const lastUser = messages.findLastIndex(isUserLike);
344
+ const last = messages.length - 1;
345
+ // Mid-turn, the request ends in the tool results the model is about to read:
346
+ // a one-shot belongs there. Otherwise on the last user turn.
347
+ const tail = last >= 0 && messages[last].role === "toolResult" ? last : lastUser;
348
+ const stackAt = firstUser === -1 ? tail : firstUser;
349
+
350
+ let stackPlaced = false;
351
+ let moved: MovedBlock[] = [];
352
+ if (input.epoch && stackAt !== -1) {
353
+ const stack = input.stack.filter((entry) => input.layout || !entry.systemRoleOnly);
354
+ push(before, stackAt, stack.map(block));
355
+ stackPlaced = stack.length > 0;
356
+ if (input.layout) {
357
+ moved = stack.filter((entry) => movesToSystemRole(entry.order)).map((entry) => ({ framed: framedReminderText(entry), inner: entry.text }));
322
358
  }
323
- });
324
- return (pin) => (pin.kind === "toolResult" ? byToolCall.get(pin.toolCallId) : byTimestamp.get(pin.timestamp)) ?? -1;
325
- }
359
+ }
326
360
 
327
- /** The anchor a one-shot lands on for this request: the trailing tool result, else the last user-like message. */
328
- export function tailAnchor(messages: AgentMessage[]): PinAnchor | undefined {
329
- const last = messages[messages.length - 1] as (AgentMessage & { toolCallId?: string }) | undefined;
330
- if (last?.role === "toolResult" && typeof last.toolCallId === "string") return { kind: "toolResult", toolCallId: last.toolCallId };
331
- for (let i = messages.length - 1; i >= 0; i--) {
332
- const m = messages[i] as AgentMessage & { timestamp?: number };
333
- if (isUserLike(m) && typeof m.timestamp === "number") {
334
- return { kind: "user", timestamp: m.timestamp };
361
+ const stickyPlaced: string[] = [];
362
+ for (const state of input.sticky) {
363
+ const carriers: number[] = [];
364
+ messages.forEach((message, index) => {
365
+ if (!isStickyCarrier(message) || stamp(message) < state.since) return;
366
+ if (state.skipStackCarrier && input.epoch && index === firstUser) return;
367
+ carriers.push(index);
368
+ });
369
+ // The pin is for a state switched on mid-turn; one on from the start
370
+ // (`since: 0`, the constant budget line) rides user messages only.
371
+ if (!state.delivered && state.since > 0) {
372
+ const first = firstResultAfter(messages, state);
373
+ if (first !== -1 && !carriers.includes(first)) carriers.unshift(first);
374
+ }
375
+ if (carriers.length === 0) continue;
376
+ stickyPlaced.push(state.key);
377
+ for (const index of carriers.sort((a, b) => a - b)) push(after, index, [block(state)]);
378
+ }
379
+
380
+ const placed: OneShot[] = [];
381
+ const opening = last >= 0 && isUserLike(messages[last]) ? last : -1;
382
+ const placedOnce = new Set<string>();
383
+ for (const shot of input.oneShots) {
384
+ if (shot.placement === "user-prepend" && (opening !== -1 || !shot.orTail)) {
385
+ if (opening === -1) continue;
386
+ placed.push(shot);
387
+ if (shot.once && placedOnce.has(shot.text)) continue;
388
+ if (shot.once) placedOnce.add(shot.text);
389
+ push(before, opening, [block(shot)]);
390
+ continue;
335
391
  }
392
+ const own = shot.toolCallId
393
+ ? messages.findIndex((m) => m.role === "toolResult" && (m as { toolCallId?: string }).toolCallId === shot.toolCallId)
394
+ : shot.promptTimestamp !== undefined
395
+ ? promptAt(messages, shot.promptTimestamp)
396
+ : -1;
397
+ const at = own !== -1 ? own : tail;
398
+ if (at === -1) continue;
399
+ placed.push(shot);
400
+ push(after, at, [block(shot)]);
336
401
  }
337
- return undefined;
402
+
403
+ const changed = before.size > 0 || after.size > 0;
404
+ return {
405
+ messages: changed ? messages.map((m, index) => withBlocks(m, before.get(index) ?? [], after.get(index) ?? [])) : [...messages],
406
+ moved,
407
+ stackPlaced,
408
+ stickyPlaced,
409
+ placed,
410
+ };
411
+ }
412
+
413
+ function stamp(message: AgentMessage): number {
414
+ return (message as { timestamp?: number }).timestamp ?? 0;
338
415
  }
339
416
 
417
+ /** How much later pi may stamp a custom message it rebuilt from the session than the one it delivered. */
418
+ const RESTAMP_WINDOW_MS = 1000;
419
+
340
420
  /**
341
- * The anchor for a `user-prepend` breadcrumb on this request: the trailing
342
- * message when it is a user-like turn (the prompt that opens the request), else
343
- * undefined — mid-turn (a trailing tool result) the breadcrumbs keep waiting.
421
+ * The prompt a one-shot's `promptTimestamp` names, or -1 when the request does
422
+ * not carry it. pi stores a custom message (a `/skill:` invocation) with the
423
+ * time it persisted it, at `message_end`, so after `/reload` or resume the
424
+ * rebuilt message is stamped a little after the delivered one the pin took;
425
+ * a user message keeps its own stamp.
344
426
  */
345
- export function openingUserAnchor(messages: AgentMessage[]): PinAnchor | undefined {
346
- const last = messages[messages.length - 1] as (AgentMessage & { timestamp?: number }) | undefined;
347
- if (last && isUserLike(last) && typeof last.timestamp === "number") return { kind: "user", timestamp: last.timestamp };
348
- return undefined;
427
+ function promptAt(messages: readonly AgentMessage[], timestamp: number): number {
428
+ const exact = messages.findIndex((m) => isStickyCarrier(m) && stamp(m) === timestamp);
429
+ if (exact !== -1) return exact;
430
+ return messages.findIndex((m) => m.role === "custom" && stamp(m) > timestamp && stamp(m) - timestamp < RESTAMP_WINDOW_MS);
349
431
  }
350
432
 
351
- /** A tool result's content with reminder blocks appended (for the `tool_result` hook). */
352
- export function appendReminderBlocks(content: ContentBlock[], entries: ReminderEntry[]): ContentBlock[] {
353
- return [...content, ...entries.map(reminderBlock)];
433
+ /**
434
+ * Where a state switched on mid-turn first shows: its switching call's result
435
+ * when the request carries it, else (that result compacted away, or no call
436
+ * named) the first tool result stamped after the switch, but never one after a
437
+ * user message that already carries it. With no carrier at all (an overflow
438
+ * compaction kept only a reply and its results), the request's tail.
439
+ */
440
+ function firstResultAfter(messages: readonly AgentMessage[], state: StickyEntry): number {
441
+ if (state.toolCallId !== undefined) {
442
+ const own = messages.findIndex((m) => m.role === "toolResult" && (m as { toolCallId?: string }).toolCallId === state.toolCallId);
443
+ if (own !== -1) return own;
444
+ }
445
+ for (let i = 0; i < messages.length; i++) {
446
+ const message = messages[i];
447
+ if (stamp(message) < state.since) continue;
448
+ if (isStickyCarrier(message)) return -1;
449
+ if (message.role === "toolResult") return i;
450
+ }
451
+ if (messages.some(isStickyCarrier)) return -1;
452
+ const last = messages.length - 1;
453
+ return last >= 0 && messages[last].role === "toolResult" ? last : -1;
354
454
  }
355
455
 
356
456
  /**
@@ -381,14 +481,17 @@ export const COMPACTION_SUMMARY_PREFIX =
381
481
  "The conversation history before this point was compacted into the following summary:\n\n<summary>\n";
382
482
  export const COMPACTION_SUMMARY_SUFFIX = "\n</summary>";
383
483
 
384
- type ContentBlock = TextContent | ImageContent;
385
-
386
484
  function toBlocks(content: string | ContentBlock[]): ContentBlock[] {
387
485
  return typeof content === "string" ? [{ type: "text", text: content }] : [...content];
388
486
  }
389
487
 
390
- function reminderBlock(entry: ReminderEntry): TextContent {
391
- return { type: "text", text: (entry.raw ? entry.text : wrapReminder(entry.text)) + (entry.suffix ?? "") };
488
+ /** A reminder's text exactly as it rides a message: framed, unless raw. */
489
+ export function framedReminderText(entry: { text: string; raw?: boolean }): string {
490
+ return entry.raw ? entry.text : wrapReminder(entry.text);
491
+ }
492
+
493
+ function block(entry: { text: string; raw?: boolean }): TextContent {
494
+ return { type: "text", text: framedReminderText(entry) };
392
495
  }
393
496
 
394
497
  /**
@@ -396,9 +499,7 @@ function reminderBlock(entry: ReminderEntry): TextContent {
396
499
  * (task notification, wakeup, hook context, `<new-diagnostics>`); pi's
397
500
  * `convertToLlm` sends it to the model as `role: "user"`, so on the wire it IS
398
501
  * a user turn — and a turn opened by one (a `/loop` tick, an agent report
399
- * arriving while idle) has no `user` message of its own. Before 2026-09-05 such
400
- * a request got no context stack at all, and its pending one-shots pinned to
401
- * the previous user message (STEERING-REVIEW-2026-09-05 H1).
502
+ * arriving while idle) has no `user` message of its own (STEERING-REVIEW-2026-09-05 H1).
402
503
  */
403
504
  function isUserLike(m: AgentMessage): boolean {
404
505
  return isStickyCarrier(m) || m.role === "compactionSummary";
@@ -427,92 +528,3 @@ function withBlocks(message: AgentMessage, before: TextContent[], after: TextCon
427
528
  const target = message as AgentMessage & { content: string | ContentBlock[] };
428
529
  return { ...target, content: [...before, ...toBlocks(target.content), ...after] } as AgentMessage;
429
530
  }
430
-
431
- /**
432
- * Returns a copy of `messages` with reminder blocks injected (Claude Code
433
- * convention: reminders ride inside message content blocks, never as messages
434
- * of their own). See the module header for the three placements. A bare string
435
- * is `last-append` (back-compat). The input array and its messages are not
436
- * mutated. With nothing to anchor to (no user, tool-result, or compaction
437
- * summary message), messages are returned unchanged and the caller keeps the
438
- * reminders queued.
439
- */
440
- export function injectReminders(messages: AgentMessage[], reminders: Array<string | ReminderEntry>): AgentMessage[] {
441
- if (reminders.length === 0) return messages;
442
-
443
- const entries: ReminderEntry[] = reminders.map((r) =>
444
- typeof r === "string" ? { text: r, placement: "last-append", order: 0 } : r,
445
- );
446
- const firstPrepend = entries
447
- .filter((e) => e.placement === "first-prepend")
448
- .map((e, i) => ({ e, i }))
449
- .sort((a, b) => a.e.order - b.e.order || a.i - b.i)
450
- .map((x) => x.e);
451
- const sticky = entries.filter((e) => e.placement === "sticky-append");
452
- const pinnedEntries = entries.filter((e) => e.pin !== undefined);
453
- const lastAppend = entries.filter((e) => e.placement === "last-append" && e.pin === undefined);
454
- // An unpinned `user-prepend` never reaches here through the queue (drain
455
- // holds it back); a caller passing one directly gets it on the opening user
456
- // message, before the text, like a pinned one.
457
- const looseUserPrepend = entries.filter((e) => e.placement === "user-prepend" && e.pin === undefined);
458
-
459
- const firstUserIndex = messages.findIndex(isUserLike);
460
- const lastUserIndex = messages.findLastIndex(isUserLike);
461
- const lastIndex = messages.length - 1;
462
- // Mid-turn, the request ends in the tool result(s) the model is about to
463
- // read — that is where a one-shot reminder belongs. Otherwise the last user turn.
464
- const tailIndex = lastIndex >= 0 && messages[lastIndex].role === "toolResult" ? lastIndex : lastUserIndex;
465
- if (firstUserIndex === -1 && tailIndex === -1) return messages;
466
-
467
- const before = new Map<number, TextContent[]>();
468
- const after = new Map<number, TextContent[]>();
469
- const push = (map: Map<number, TextContent[]>, index: number, blocks: TextContent[]) => {
470
- if (index < 0 || blocks.length === 0) return;
471
- map.set(index, [...(map.get(index) ?? []), ...blocks]);
472
- };
473
-
474
- push(before, firstUserIndex === -1 ? tailIndex : firstUserIndex, firstPrepend.map(reminderBlock));
475
-
476
- for (const entry of sticky) {
477
- const since = entry.since ?? 0;
478
- // The block rides the user message that opened the turn during which the
479
- // state switched on (the latest user message stamped before `since` — a
480
- // standing reminder emitted on before_agent_start is always stamped after
481
- // the turn's user message) and every user message after it. The set only
482
- // grows while the state holds, so earlier messages never change.
483
- let opener = -1;
484
- const carriers: number[] = [];
485
- messages.forEach((m, index) => {
486
- if (!isStickyCarrier(m)) return;
487
- const stamp = (m as { timestamp?: number }).timestamp ?? 0;
488
- if (stamp >= since) carriers.push(index);
489
- else opener = index;
490
- });
491
- if (opener !== -1) carriers.unshift(opener);
492
- if (carriers.length === 0) {
493
- // No user turn at all (overflow compaction mid-turn): ride the tail.
494
- push(after, tailIndex === -1 ? firstUserIndex : tailIndex, [reminderBlock(entry)]);
495
- continue;
496
- }
497
- for (const index of carriers) push(after, index, [reminderBlock(entry)]);
498
- }
499
-
500
- push(after, tailIndex === -1 ? firstUserIndex : tailIndex, lastAppend.map(reminderBlock));
501
- if (lastUserIndex !== -1) push(before, lastUserIndex, looseUserPrepend.map(reminderBlock));
502
-
503
- // Pinned one-shots ride the exact message they first landed on; a message
504
- // compacted away simply no longer carries its pin. A pinned breadcrumb sits
505
- // BEFORE that message's text (after the first-prepend stack, which was
506
- // pushed first); every other pin sits after it.
507
- if (pinnedEntries.length > 0) {
508
- const locate = pinLocator(messages);
509
- for (const entry of pinnedEntries) {
510
- const index = locate(entry.pin as PinAnchor);
511
- if (index === -1) continue;
512
- push(entry.placement === "user-prepend" ? before : after, index, [reminderBlock(entry)]);
513
- }
514
- }
515
-
516
- if (before.size === 0 && after.size === 0) return messages;
517
- return messages.map((m, index) => withBlocks(m, before.get(index) ?? [], after.get(index) ?? []));
518
- }