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
@@ -7,20 +7,43 @@
7
7
  * cannot overwrite someone else's change.
8
8
  * - Files that change out of band are reported in a `<system-reminder>` with the
9
9
  * new content around the change, line-numbered.
10
+ * - pi's read, write and edit results read as Claude Code's (`results.ts`).
11
+ * - After a compaction, the files read or written most recently come back as
12
+ * Claude Code brings them back: the contents of a small one, a note to read
13
+ * a large one again (`restore.ts`), and the read state is cleared.
10
14
  *
11
15
  * Tracking is by content, not by tool call, which is what makes it catch writes
12
16
  * that went through bash and never touched an intercepted tool.
13
17
  */
14
18
 
15
- import { existsSync, readFileSync, statSync } from "node:fs";
16
- import { isAbsolute, resolve } from "node:path";
17
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
19
+ import { CHILD_WROTE_CHANNEL, type ChildWrote } from "../lib/child-writes.ts";
20
+ import { createHash } from "node:crypto";
21
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
22
+ import { homedir } from "node:os";
23
+ import { join } from "node:path";
24
+ import { createReadToolDefinition, type ExtensionAPI, type ExtensionContext, type SessionCompactEvent } from "@earendil-works/pi-coding-agent";
18
25
  import { pathArgument } from "../auto-mode/paths.ts";
19
- import { REMINDER_CHANNEL } from "../lib/reminders.ts";
20
- import { pathsReadOnBranch } from "./replay.ts";
26
+ import { bashParserReady } from "../lib/bash-parser.ts";
27
+ import { discoverContextFiles, instructionRule } from "../lib/claude-context.ts";
28
+ import { readExternalIncludesApproval } from "../lib/claude-external-includes.ts";
29
+ import { inContextEntries, latestCompaction } from "../lib/compaction-boundary.ts";
30
+ import { autoMemoryEnabled } from "../lib/auto-memory.ts";
31
+ import { projectMemoryDir } from "../lib/memory.ts";
32
+ import { claudeConfigDir, comparablePath, isPathAtOrUnder, tryRealpath } from "../lib/paths.ts";
33
+ import { estimateTextTokens } from "../lib/pi-ai-estimate.ts";
34
+ import { planFileOnBranch } from "../lib/plan-mode-channels.ts";
35
+ import { REMINDER_CHANNEL, type ReminderPayload } from "../lib/reminders.ts";
36
+ import { resolveToolPath } from "../lib/tool-path.ts";
37
+ import { parseRules, ruleMatches } from "../permissions/matcher.ts";
38
+ import { loadPermissionSettings } from "../permissions/settings.ts";
39
+ import { type FileObservation, filesSeenOnBranch, keptReadPaths, keptShellReadPaths, shellCallsOnBranch } from "./replay.ts";
40
+ import { type RestoredRead, restoreBlocks, restoreCandidates } from "./restore.ts";
41
+ import { fileToolResultContent } from "./results.ts";
42
+ import { shellReadPaths, shownInFull } from "./shell-reads.ts";
43
+ import { sessionWorkCwd, WORKTREE_CHANNEL, type WorktreeLocation } from "../lib/worktree-channel.ts";
21
44
  import {
22
45
  describeChanges,
23
- EXTERNAL_CHANGE_REMINDER,
46
+ CHILD_CHANGE_REMINDER, EXTERNAL_CHANGE_REMINDER,
24
47
  type FileStamp,
25
48
  FileTracker,
26
49
  STALE_REASON,
@@ -46,6 +69,15 @@ function readIfPresent(path: string): string | undefined {
46
69
  }
47
70
  }
48
71
 
72
+ /** Whether `path` is a regular file (symlinks followed): never a device, FIFO or directory. */
73
+ function isRegularFile(path: string): boolean {
74
+ try {
75
+ return statSync(path).isFile();
76
+ } catch {
77
+ return false;
78
+ }
79
+ }
80
+
49
81
  /** The file's disk stamp, or undefined when it is gone (or not a plain file we could stat). */
50
82
  function statIfPresent(path: string): FileStamp | undefined {
51
83
  try {
@@ -56,12 +88,95 @@ function statIfPresent(path: string): FileStamp | undefined {
56
88
  }
57
89
  }
58
90
 
59
- /** Observe a file's current content together with the stamp it was read at. */
60
- function observeFromDisk(tracker: FileTracker, path: string): void {
91
+ function commandOf(input: unknown): string {
92
+ const command = (input as { command?: unknown } | undefined)?.command;
93
+ return typeof command === "string" ? command : "";
94
+ }
95
+
96
+ function textOf(content: unknown): string {
97
+ if (!Array.isArray(content)) return "";
98
+ return content.map((block) => (block as { type?: string; text?: string })?.type === "text" ? ((block as { text?: string }).text ?? "") : "").join("\n");
99
+ }
100
+
101
+ /**
102
+ * A file's identity and version, from metadata only: `ctimeMs` changes on any
103
+ * write, rename or metadata change, and cannot be set back by the writer.
104
+ */
105
+ function versionOf(path: string): string | undefined {
106
+ try {
107
+ const stat = statSync(path);
108
+ return stat.isFile() ? `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}` : undefined;
109
+ } catch {
110
+ return undefined;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * The files a shell command may print, with each one's version as the command
116
+ * starts (`shell-reads.ts` paths, resolved as the shell resolves them, from
117
+ * the directory a literal `cd` earlier in the command moved to). Taken before
118
+ * the command runs, so a file the command itself rewrites (`cat x; cp y x`)
119
+ * is not certified with its new content (found by a GPT-6 Sol review,
120
+ * 2026-10-04). Metadata only: this runs before the permission gate decides
121
+ * the call, so a file the user then denies is never opened.
122
+ */
123
+ function shellReadSnapshot(command: string, cwd: string): Map<string, string> {
124
+ const snapshot = new Map<string, string>();
125
+ if (!command) return snapshot;
126
+ for (const path of shellReadPaths(command, cwd, homedir(), (dir) => readdirSync(dir))) {
127
+ // Regular files only: a device or FIFO (`head -c 1 /dev/zero`) reports size 0
128
+ // and a read of it never ends (found by a GPT-6 Astra review).
129
+ if (!isRegularFile(path)) continue;
130
+ const stamp = statIfPresent(path);
131
+ if (!stamp || stamp.size > MAX_SHELL_READ_BYTES) continue;
132
+ const version = versionOf(path);
133
+ if (version !== undefined) snapshot.set(path, version);
134
+ }
135
+ return snapshot;
136
+ }
137
+
138
+ /**
139
+ * Count as read each file the command printed in full and did not change:
140
+ * its version before and after the command is the same, and the output holds
141
+ * all of it. Observed, not touched: the post-compaction restore follows the
142
+ * file tools, as Claude Code's does. Returns the files it observed.
143
+ */
144
+ function observeShellReads(tracker: FileTracker, before: Map<string, string>, output: string): string[] {
145
+ const observed: string[] = [];
146
+ if (!output) return observed;
147
+ for (const [path, version] of before) {
148
+ const stamp = statIfPresent(path);
149
+ if (!stamp || !isRegularFile(path) || versionOf(path) !== version) continue;
150
+ const content = readIfPresent(path);
151
+ if (content === undefined || versionOf(path) !== version) continue;
152
+ if (!shownInFull(output, content)) continue;
153
+ tracker.observe(path, content, Date.now(), stamp);
154
+ observed.push(path);
155
+ }
156
+ return observed;
157
+ }
158
+
159
+ /** Above this a shell read's output is persisted, not shown, so the file cannot have been seen whole. */
160
+ const MAX_SHELL_READ_BYTES = 256 * 1024;
161
+
162
+ /**
163
+ * Observe a file's current content together with the stamp it was read at.
164
+ * Returns that stamp, and what was seen for the result that read it
165
+ * (undefined when the file is gone).
166
+ */
167
+ function observeFromDisk(tracker: FileTracker, path: string): { stamp?: FileStamp; seen?: FileObservation } {
61
168
  const stamp = statIfPresent(path);
62
169
  const current = readIfPresent(path);
63
- if (current === undefined) tracker.forget(path);
64
- else tracker.observe(path, current, Date.now(), stamp);
170
+ if (current === undefined) {
171
+ tracker.forget(path);
172
+ return stamp ? { stamp } : {};
173
+ }
174
+ tracker.observe(path, current, Date.now(), stamp);
175
+ return stamp ? { stamp, seen: { path, size: stamp.size, mtimeMs: stamp.mtimeMs, sha256: sha256(current) } } : {};
176
+ }
177
+
178
+ function sha256(text: string): string {
179
+ return createHash("sha256").update(text).digest("hex");
65
180
  }
66
181
 
67
182
  function pathOf(input: unknown, cwd: string): string | undefined {
@@ -71,40 +186,191 @@ function pathOf(input: unknown, cwd: string): string | undefined {
71
186
  // read-before-write guard (code-review).
72
187
  const raw = pathArgument(input as Record<string, unknown> | undefined);
73
188
  if (typeof raw !== "string" || !raw.trim()) return undefined;
74
- return isAbsolute(raw) ? raw : resolve(cwd, raw);
189
+ // pi's own resolution (`~`, `@`, `file://`, …): the guard must check the
190
+ // file the tool will actually touch (lib/tool-path.ts).
191
+ return resolveToolPath(raw, cwd);
192
+ }
193
+
194
+ /**
195
+ * The files the context still shows after a compaction, as comparable paths:
196
+ * the context stack (the CLAUDE.md family as claude-context discovers it, what
197
+ * they `@`-import, and the auto-memory index), the plan file, the files a
198
+ * `read` in the kept turns returned (`kept`) and the files a kept bash call
199
+ * showed in full (`keptShell`), both absolute as recorded. Claude Code's
200
+ * restore skips them; they stay read.
201
+ */
202
+ function inContextFiles(ctx: ExtensionContext, branch: readonly { type: string; customType?: string; data?: unknown }[], kept: string[], keptShell: string[]): Set<string> {
203
+ const home = homedir();
204
+ // Use the context loader's import confinement and approval: a literal
205
+ // unapproved @import is not evidence that its file is still in context.
206
+ const stack = discoverContextFiles({ cwd: ctx.cwd, home, homeClaudeDir: claudeConfigDir(), rule: instructionRule(home),
207
+ includeExternal: readExternalIncludesApproval(ctx.cwd, home).approved,
208
+ }).flatMap((file) => [file.path, ...(file.imported ?? [])]);
209
+ return new Set(
210
+ [...stack, autoMemoryEnabled(ctx.cwd) ? join(projectMemoryDir(ctx.cwd, home), "MEMORY.md") : undefined, planFileOnBranch(branch), ...kept, ...keptShell]
211
+ .filter((path): path is string => typeof path === "string")
212
+ .map(comparablePath),
213
+ );
214
+ }
215
+
216
+ /**
217
+ * Whether a file's contents may come back without the permission gate that a
218
+ * `read` call passes: only a file that resolves (symlinks followed) inside the
219
+ * working directory and that no read deny or ask rule covers now. Any other
220
+ * file, one read outside the project after a question, say, is not restored;
221
+ * the model reads it again through the gate if it needs it. Nothing passes
222
+ * when the settings cannot be read (the gate reports them).
223
+ */
224
+ function restoreAllowed(ctx: ExtensionContext): (path: string) => boolean {
225
+ try {
226
+ const settings = loadPermissionSettings(ctx.cwd, homedir());
227
+ const rules = parseRules([...settings.deny, ...settings.ask]);
228
+ const root = tryRealpath(ctx.cwd) ?? ctx.cwd;
229
+ return (path) => {
230
+ const real = tryRealpath(path);
231
+ if (!real || !isPathAtOrUnder(real, root)) return false;
232
+ return !rules.some((rule) => ruleMatches(rule, "read", path, ctx.cwd) || ruleMatches(rule, "read", real, ctx.cwd));
233
+ };
234
+ } catch {
235
+ return () => false;
236
+ }
237
+ }
238
+
239
+ /** A file read again through pi's own read tool, as the model would see it; undefined when gone, unreadable or an image. */
240
+ async function readAgain(read: ReturnType<typeof createReadToolDefinition>, path: string, ctx: ExtensionContext): Promise<string | undefined> {
241
+ try {
242
+ const result = await read.execute("restore", { path }, undefined, undefined, ctx as never);
243
+ if (result.content.some((block) => block.type !== "text")) return undefined;
244
+ return result.content.map((block) => (block as { text: string }).text).join("\n");
245
+ } catch {
246
+ return undefined;
247
+ }
75
248
  }
76
249
 
77
250
  export default function fileTrackerExtension(pi: ExtensionAPI) {
78
251
  let tracker = new FileTracker();
252
+ /**
253
+ * The files each bash call showed in full, by call id, decided where the
254
+ * call ran: a compaction keeps the reads of the calls it keeps from this,
255
+ * since re-parsing a command later resolves it from today's directory.
256
+ */
257
+ const shellReadsByCall = new Map<string, string[]>();
258
+ const recordShellReads = (callId: string, paths: string[]) => {
259
+ if (paths.length > 0) shellReadsByCall.set(callId, paths);
260
+ };
261
+ /** The entered worktree, where the shell runs while one is active (lib/worktree-channel.ts). */
262
+ let entered: WorktreeLocation | undefined;
263
+ pi.events.on(WORKTREE_CHANNEL, (data) => {
264
+ const location = data as WorktreeLocation | null | undefined;
265
+ entered = location?.path ? location : undefined;
266
+ });
79
267
  let sessionId: string | undefined;
268
+ /**
269
+ * Every file a read or write touched on this branch, with its modification
270
+ * time as stat'ed then (as the result recorded it, for one replayed after a
271
+ * resume): the post-compaction restore's order.
272
+ */
273
+ let touched = new Map<string, number>();
274
+ /**
275
+ * Files this session's own child agents changed since the parent last saw
276
+ * them (lib/child-writes.ts). Every way the parent sees a file clears its
277
+ * entry: a file tool, a shell read, a rebuild from disk, the change scan.
278
+ */
279
+ const childWrites = new Map<string, string | undefined>();
280
+ pi.events.on(CHILD_WROTE_CHANNEL, (data) => {
281
+ const wrote = data as ChildWrote | undefined;
282
+ if (wrote?.path) childWrites.set(wrote.path, wrote.agent);
283
+ });
284
+ /** Observe a file the model read or wrote, and record it as touched at its current modification time. */
285
+ const touch = (path: string) => {
286
+ childWrites.delete(path);
287
+ const { stamp, seen } = observeFromDisk(tracker, path);
288
+ if (stamp) touched.set(path, stamp.mtimeMs);
289
+ return seen;
290
+ };
80
291
 
81
- // A resumed session (or a session_tree switch) carries reads the model did
82
- // earlier in the transcript; rebuild the tracker from the branch so post-
83
- // resume edits are not refused as "never read" (replay.ts). A new session id
84
- // starts from an empty tracker.
85
- const reconstruct = (ctx: ExtensionContext) => {
292
+ // A resumed session (or a /reload, a fork, a session_tree switch) carries
293
+ // reads the model did earlier in the transcript; rebuild the tracker from
294
+ // the part of the branch still in context (since the latest compaction's
295
+ // kept tail) so later edits are not refused as "never read" (replay.ts).
296
+ // Everything comes from the branch, as the results and pending entries
297
+ // recorded it, never from this instance: pi runs the factory again on each
298
+ // of those. A new session id starts from an empty tracker.
299
+ const reconstruct = (ctx: ExtensionContext, fresh = false) => {
86
300
  const id = ctx.sessionManager.getSessionId?.() ?? undefined;
87
- if (id !== sessionId) {
301
+ if (fresh || id !== sessionId) {
88
302
  sessionId = id;
89
303
  tracker = new FileTracker();
90
304
  }
305
+ // Tracked files are re-read from disk below: what a child wrote is now the baseline.
306
+ childWrites.clear();
91
307
  let entries: unknown[] = [];
92
308
  try {
93
- entries = ctx.sessionManager.getBranch() as unknown[];
309
+ entries = inContextEntries(ctx.sessionManager.getBranch() as unknown[]);
94
310
  } catch {
95
311
  return;
96
312
  }
97
- for (const raw of pathsReadOnBranch(entries)) {
98
- observeFromDisk(tracker, isAbsolute(raw) ? raw : resolve(ctx.cwd, raw));
313
+ // What each result or restore saw, recorded at the time and by the path
314
+ // the tool resolved: a file that changed since is stale, never taken as
315
+ // read in its current form (round-2 review H1), and a gone one untracked.
316
+ const files = filesSeenOnBranch(entries);
317
+ const changed: Pick<FileObservation, "path" | "sha256">[] = [];
318
+ for (const [path, { seen, notified }] of files) {
319
+ const stamp = statIfPresent(path);
320
+ const current = readIfPresent(path);
321
+ if (current === undefined) continue;
322
+ const hash = sha256(current);
323
+ if (hash === seen.sha256) {
324
+ tracker.observe(path, current, Date.now(), stamp);
325
+ continue;
326
+ }
327
+ tracker.observeChanged(path, Date.now());
328
+ // A version a notice already named is not announced again (round-3 review L9).
329
+ if (!notified.has(hash)) changed.push({ path, sha256: hash });
330
+ }
331
+ if (changed.length > 0) {
332
+ // Unkeyed: a key would let this notice replace an undelivered earlier one
333
+ // whose versions the branch already counts as told.
334
+ pi.events.emit(REMINDER_CHANNEL, {
335
+ text: `These files changed on disk since you last read them in this conversation: ${changed.map((file) => file.path).join(", ")}. Read them again before editing them.`,
336
+ notified: changed,
337
+ } satisfies ReminderPayload);
338
+ }
339
+ // Shell reads need the bash grammar, which may still be loading this early.
340
+ const target = tracker;
341
+ shellReadsByCall.clear();
342
+ const shellCalls = [...shellCallsOnBranch(entries)];
343
+ if (shellCalls.length > 0) {
344
+ void bashParserReady().then(() => {
345
+ if (target !== tracker) return;
346
+ // A replayed result has no before-state: the files as they are now stand in for it.
347
+ for (const call of shellCalls) recordShellReads(call.id, observeShellReads(target, shellReadSnapshot(call.command, sessionWorkCwd(entered, ctx.cwd)), call.output));
348
+ });
99
349
  }
350
+ touched = new Map([...files].map(([path, { seen }]) => [path, seen.mtimeMs]));
100
351
  };
101
352
  pi.on("session_start", (_event, ctx) => reconstruct(ctx));
102
- pi.on("session_tree", (_event, ctx) => reconstruct(ctx));
353
+ // A branch switch keeps the session id, but the reads of the branch left
354
+ // behind are not in context any more: start from an empty tracker, as for a
355
+ // new session (found by an adversarial GPT-6 Astra run, 2026-10-04).
356
+ pi.on("session_tree", (_event, ctx) => reconstruct(ctx, true));
357
+
358
+ /** Whether a write's file existed when the call was made, by call id: its result says "created" or "updated". */
359
+ const writeTargetExisted = new Map<string, boolean>();
103
360
 
104
- pi.on("tool_call", (event, ctx) => {
361
+ /** Each running bash call's candidate files as they were when it started (`shellReadSnapshot`). */
362
+ const shellSnapshots = new Map<string, Map<string, string>>();
363
+ pi.on("tool_call", async (event, ctx) => {
364
+ if (event.toolName === "bash") {
365
+ await bashParserReady();
366
+ // Relative paths in the command resolve where the shell runs: the entered worktree, if any.
367
+ shellSnapshots.set(event.toolCallId, shellReadSnapshot(commandOf(event.input), sessionWorkCwd(entered, ctx.cwd)));
368
+ return undefined;
369
+ }
105
370
  if (!GUARDED_TOOLS.has(event.toolName)) return undefined;
106
371
  const path = pathOf(event.input, ctx.cwd);
107
372
  if (!path) return undefined;
373
+ if (event.toolName === "write") writeTargetExisted.set(event.toolCallId, existsSync(path));
108
374
 
109
375
  const current = readIfPresent(path);
110
376
  const status = tracker.status(path, current);
@@ -115,8 +381,19 @@ export default function fileTrackerExtension(pi: ExtensionAPI) {
115
381
  return { block: true, reason: STALE_REASON(path) };
116
382
  });
117
383
 
118
- pi.on("tool_result", (event, ctx) => {
119
- if (event.isError) return undefined;
384
+ pi.on("tool_result", async (event, ctx) => {
385
+ const existedBefore = writeTargetExisted.get(event.toolCallId);
386
+ writeTargetExisted.delete(event.toolCallId);
387
+ if (event.toolName === "bash") {
388
+ const before = shellSnapshots.get(event.toolCallId);
389
+ shellSnapshots.delete(event.toolCallId);
390
+ if (before) {
391
+ const observed = observeShellReads(tracker, before, textOf(event.content));
392
+ recordShellReads(event.toolCallId, observed);
393
+ for (const path of observed) childWrites.delete(path);
394
+ }
395
+ return undefined;
396
+ }
120
397
  if (!READ_TOOLS.has(event.toolName) && !GUARDED_TOOLS.has(event.toolName)) return undefined;
121
398
 
122
399
  const path = pathOf(event.input, ctx.cwd);
@@ -124,8 +401,96 @@ export default function fileTrackerExtension(pi: ExtensionAPI) {
124
401
 
125
402
  // After a read we know the file; after our own write we know it again, so a
126
403
  // successful edit does not make the file look stale to the next edit.
127
- observeFromDisk(tracker, path);
128
- return undefined;
404
+ // What the model saw, on the result: a resume compares it with the disk.
405
+ const observation = event.isError ? undefined : touch(path);
406
+ const content = fileToolResultContent({
407
+ toolName: event.toolName,
408
+ isError: event.isError,
409
+ content: event.content,
410
+ path,
411
+ cwd: ctx.cwd,
412
+ existedBefore,
413
+ isEmptyFile: () => statIfPresent(path)?.size === 0,
414
+ });
415
+ if (!content && !observation) return undefined;
416
+ return {
417
+ ...(content ? { content } : {}),
418
+ ...(observation ? { details: { ...((event.details as Record<string, unknown> | undefined) ?? {}), oneCodeFile: observation } } : {}),
419
+ };
420
+ });
421
+
422
+ /**
423
+ * Claude Code's post-compaction file restore (`restore.ts`). The
424
+ * blocks open the next user message (`user-prepend`); a request that opens
425
+ * with none (the rest of a run after a mid-turn compaction, an overflow
426
+ * retry) carries them on its tail (`orTail`), so the model sees a restored
427
+ * file before it can edit it. A later compaction drops an undelivered one
428
+ * (`untilCompaction`): it restores for the epoch the one before it opened.
429
+ *
430
+ * Like Claude Code, the compaction also clears what the session has read:
431
+ * the summary no longer holds those files, so an edit needs a fresh read.
432
+ * A file the context still shows stays read (a kept turn's read or whole-file
433
+ * shell read, the context stack, the plan file), and a restored file counts
434
+ * as read and as touched for the next compaction's restore, here and after
435
+ * a reload or a resume (its block records what it showed); a note never
436
+ * marks its file read. Contents come back only for a file the gate would
437
+ * let through without a question (`restoreAllowed`).
438
+ */
439
+ pi.on("session_compact", async (event: SessionCompactEvent, ctx) => {
440
+ const before = touched;
441
+ try {
442
+ const branch = ctx.sessionManager.getBranch();
443
+ const compaction = latestCompaction(branch);
444
+ const keptTurns = compaction ? branch.slice(compaction.keptStart, compaction.index) : [];
445
+ // The kept shell reads: what each kept bash call certified as it ran.
446
+ const keptShell = keptShellReadPaths(keptTurns, (id) => shellReadsByCall.get(id), (path) => tracker.lastSeen(path));
447
+ // A call the summary took never comes back: only the kept ones need their record.
448
+ const keptCalls = new Set([...shellCallsOnBranch(keptTurns)].map((call) => call.id));
449
+ for (const id of shellReadsByCall.keys()) if (!keptCalls.has(id)) shellReadsByCall.delete(id);
450
+ const inContext = inContextFiles(ctx, branch, keptReadPaths(keptTurns), keptShell);
451
+ // The loader lists imports by real path; a tracked path may be spelled through a symlink.
452
+ const shown = (path: string) => inContext.has(comparablePath(path)) || inContext.has(comparablePath(tryRealpath(path) ?? path));
453
+ // A file the context still shows keeps its read state (what the model
454
+ // saw, not what is on disk now, so the stale-edit guard still holds)
455
+ // and its touch time, for the next compaction's restore.
456
+ for (const path of tracker.tracked) if (!shown(path)) tracker.forget(path);
457
+ touched = new Map([...before].filter(([path]) => shown(path)));
458
+ // Opting out of One Code's restore does not disable pi's compaction:
459
+ // reads discarded by either summarizer still need a fresh read.
460
+ if (process.env.CC_COMPACTION === "0" || before.size === 0) return;
461
+ const allowed = restoreAllowed(ctx);
462
+ const candidates = restoreCandidates(before, (path) => shown(path) || !allowed(path));
463
+ const read = createReadToolDefinition(ctx.cwd);
464
+ const reads: RestoredRead[] = await Promise.all(candidates.map(async (path) => ({ path, text: await readAgain(read, path, ctx) })));
465
+ const shownText = new Map(reads.map(({ path, text }) => [path, text]));
466
+ const { blocks, restored } = restoreBlocks(reads, estimateTextTokens);
467
+ // The block that shows a file records what it showed, so a reload or a
468
+ // resume counts the file read exactly where the branch holds the block
469
+ // (replay.ts filesSeenOnBranch). The file counts as read only when the
470
+ // disk now holds exactly what the block shows: one that changed while
471
+ // it was read back (even back to where it started) fails safe, and a
472
+ // fresh read is asked for instead of passing the stale-edit guard.
473
+ const seen = new Map(
474
+ restored.map((path) => {
475
+ const observed = touch(path);
476
+ const text = shownText.get(path);
477
+ if (observed && (text === undefined || sha256(text) !== observed.sha256)) {
478
+ tracker.forget(path);
479
+ return [path, undefined] as const;
480
+ }
481
+ return [path, observed] as const;
482
+ }),
483
+ );
484
+ for (const { text, restores } of blocks) {
485
+ const observation = restores ? seen.get(restores) : undefined;
486
+ pi.events.emit(REMINDER_CHANNEL, { text, placement: "user-prepend", orTail: true, untilCompaction: true, ...(observation ? { observations: [observation] } : {}) } satisfies ReminderPayload);
487
+ }
488
+ } catch {
489
+ // The restore is best effort: nothing comes back, and an edit needs a fresh read.
490
+ tracker = new FileTracker();
491
+ touched = new Map();
492
+ shellReadsByCall.clear();
493
+ }
129
494
  });
130
495
 
131
496
  /**
@@ -147,12 +512,10 @@ export default function fileTrackerExtension(pi: ExtensionAPI) {
147
512
  * (one blocked call plus a re-read per edit for anyone with an asynchronous
148
513
  * formatter). Claude Code computes its `edited_text_file` attachments on
149
514
  * every tool round, so the model reads the change before its next edit
150
- * (review M5). The one-shot is pending when the next request goes out and
151
- * the injector pins it to that tool result — pi runs `tool_result` hooks
152
- * BEFORE it emits `tool_execution_end` (agent-loop.js
153
- * finalizeExecutedToolCall → emitToolExecutionEnd), so it cannot be written
154
- * into the stored result; pinned is the same mechanism tool-search's
155
- * deferred-tool miss uses. Our own edit/write/read of a file was observed by
515
+ * (review M5). The one-shot is a pending entry when the next request goes
516
+ * out, and the renderer places it on that request's trailing tool result
517
+ * (lib/reminders.ts), the same mechanism tool-search's deferred-tool miss
518
+ * uses; reminders never enter a stored result. Our own edit/write/read of a file was observed by
156
519
  * the `tool_result` handler above before this fires, so it is fresh here
157
520
  * and never reported as external — but only for a call that ran alone. pi
158
521
  * executes a batch of parallel tool calls CONCURRENTLY, so the first edit's
@@ -163,17 +526,27 @@ export default function fileTrackerExtension(pi: ExtensionAPI) {
163
526
  * runs only once the batch has fully drained.
164
527
  */
165
528
  const reportExternalChanges = () => {
166
- const detailed: string[] = [];
167
- const overflow: string[] = [];
529
+ // Each notice records the versions it names on its pending entry, so a
530
+ // reload or a resume does not announce them again (replay.ts).
531
+ const detailed: { text: string; notified?: Pick<FileObservation, "path" | "sha256"> }[] = [];
532
+ const overflow: Pick<FileObservation, "path" | "sha256">[] = [];
168
533
  for (const path of tracker.tracked) {
534
+ // Undefined for a file tracked without content (oversized); an empty
535
+ // file is compared like any other.
169
536
  const previous = tracker.lastSeen(path);
170
- if (previous === undefined || previous === "") continue;
537
+ if (previous === undefined) continue;
171
538
  const stamp = statIfPresent(path);
172
539
  if (stamp && tracker.unchangedOnDisk(path, stamp)) continue;
540
+ // This scan accounts for the child's write whatever it finds (same
541
+ // content, already notified, overflow), so a later change by someone
542
+ // else is never credited to that agent.
543
+ const byChild = childWrites.has(path);
544
+ const childAgent = childWrites.get(path);
545
+ childWrites.delete(path);
173
546
  const current = stamp ? readIfPresent(path) : undefined;
174
547
  if (stamp === undefined || current === undefined) {
175
548
  tracker.forget(path);
176
- detailed.push(`${path} no longer exists; it was deleted or moved after you read it.`);
549
+ detailed.push({ text: `${path} no longer exists; it was deleted or moved after you last read or wrote it.` });
177
550
  continue;
178
551
  }
179
552
  tracker.recordStamp(path, stamp);
@@ -181,22 +554,24 @@ export default function fileTrackerExtension(pi: ExtensionAPI) {
181
554
  // Don't repeat the same warning every turn…
182
555
  if (tracker.alreadyNotified(path, current)) continue;
183
556
 
557
+ const version = { path, sha256: sha256(current) };
184
558
  if (detailed.length < DETAILED_CHANGE_REMINDERS_PER_TURN) {
185
559
  const excerpt = describeChanges(previous, current);
186
- if (excerpt) detailed.push(EXTERNAL_CHANGE_REMINDER(path, excerpt));
560
+ if (excerpt) detailed.push({ text: byChild ? CHILD_CHANGE_REMINDER(path, childAgent, excerpt) : EXTERNAL_CHANGE_REMINDER(path, excerpt), notified: version });
187
561
  } else {
188
- overflow.push(path);
562
+ overflow.push(version);
189
563
  }
190
564
  // …but deliberately do NOT record the new content as seen: the file must
191
565
  // stay stale so the edit guard still forces a re-read. Marking it read
192
566
  // here would announce the change and then permit the clobbering edit.
193
567
  tracker.markNotified(path, current);
194
568
  }
195
- for (const text of detailed) pi.events.emit(REMINDER_CHANNEL, { text });
569
+ for (const { text, notified } of detailed) pi.events.emit(REMINDER_CHANNEL, { text, ...(notified ? { notified: [notified] } : {}) } satisfies ReminderPayload);
196
570
  if (overflow.length > 0) {
197
571
  pi.events.emit(REMINDER_CHANNEL, {
198
- text: `${overflow.length} more file(s) you read earlier changed on disk since: ${overflow.join(", ")}. Re-read any of them before editing it.`,
199
- });
572
+ text: `${overflow.length} more file(s) you read earlier changed on disk since: ${overflow.map((file) => file.path).join(", ")}. Re-read any of them before editing it.`,
573
+ notified: overflow,
574
+ } satisfies ReminderPayload);
200
575
  }
201
576
  };
202
577
  /**
@@ -212,8 +587,11 @@ export default function fileTrackerExtension(pi: ExtensionAPI) {
212
587
  pi.on("agent_start", () => {
213
588
  // A turn aborted mid-batch can leave ids behind; each turn starts from a
214
589
  // clean set so the mid-turn scan cannot be wedged off for the rest of the
215
- // session.
590
+ // session. A write blocked before it ran left its existence entry behind,
591
+ // and a blocked or aborted bash call its snapshot of file versions.
216
592
  executing.clear();
593
+ writeTargetExisted.clear();
594
+ shellSnapshots.clear();
217
595
  reportExternalChanges();
218
596
  return undefined;
219
597
  });