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,18 +1,26 @@
1
1
  /**
2
2
  * Rebuild read-before-write state from a session branch (pure): which files
3
- * the model has read or written this conversation, so a `--resume`d session
4
- * does not block every edit with "has not been read in this conversation" —
5
- * a claim the model can see is false in its own transcript (review T9).
3
+ * the model has seen this conversation, so a `--resume`d session does not
4
+ * block every edit with "has not been read in this conversation", a claim
5
+ * the model can see is false in its own transcript (review T9).
6
6
  *
7
- * The content the model saw is not in the transcript, so a replayed file is
8
- * observed with its CURRENT content: an edit after resume is allowed, and a
9
- * change made while the session was closed is not flagged. That is Claude
10
- * Code's behaviour too (its read state persists across resume); the stale-edit
11
- * guard still catches anything that changes from this point on.
7
+ * The content the model saw is not in the transcript, but each read and write
8
+ * result records what it saw (`details.oneCodeFile`, `FileObservation`) under
9
+ * the absolute path the tool resolved. The replay keys on that path, never on
10
+ * the call's own spelling: a worktree rewrites a relative path only in the
11
+ * call it runs, so the transcript's `a.ts` would resolve to the main
12
+ * checkout's twin (round-3 review M15). A post-compaction restore records the
13
+ * same on its pending entry, and a changed-on-disk notice records the
14
+ * versions it named. The caller compares each observation with the disk: a
15
+ * file that still matches is read, one that changed since is stale. A result
16
+ * that recorded nothing (a session from before the record existed) leaves its
17
+ * file unread: the disk as it is now is never taken as what the model saw
18
+ * (round-2 review H1).
12
19
  */
13
20
 
14
- const READ_TOOLS = new Set(["read", "notebook_edit"]);
15
- const WRITE_TOOLS = new Set(["edit", "write", "notebook_edit"]);
21
+ import { latestCompaction } from "../lib/compaction-boundary.ts";
22
+ import { type FileVersion, isFileVersion, readRecord } from "../lib/ledger/records.ts";
23
+ import { shownInFull } from "./shell-reads.ts";
16
24
 
17
25
  interface ToolCallBlock {
18
26
  type: "toolCall";
@@ -21,26 +29,131 @@ interface ToolCallBlock {
21
29
  arguments?: Record<string, unknown>;
22
30
  }
23
31
 
24
- /** Paths (as written in the tool input) of every successful read/edit/write on the branch, in order, deduplicated. */
25
- export function pathsReadOnBranch(entries: unknown[]): string[] {
26
- const calls = new Map<string, { name: string; path: string }>();
27
- const seen: string[] = [];
32
+ type BranchMessage = { role?: string; content?: unknown; toolCallId?: string; toolName?: string; isError?: boolean; details?: { oneCodeFile?: unknown } };
33
+
34
+ /** What a read or write result saw of its file, recorded in its details as `oneCodeFile`: a version with its size and modification time. */
35
+ export type FileObservation = Required<FileVersion>;
36
+
37
+ const isObservation = (value: unknown): value is FileObservation => isFileVersion(value) && value.size !== undefined && value.mtimeMs !== undefined;
38
+
39
+ /**
40
+ * Every successful tool result on the branch that recorded what it saw of a
41
+ * file, in branch order: its position, the tool that ran (from its call, else
42
+ * the result's own `toolName`) and the observation.
43
+ */
44
+ function* fileResults(entries: readonly unknown[]): Generator<{ index: number; name: string | undefined; seen: FileObservation }> {
45
+ const names = new Map<string, string>();
46
+ for (const [index, entry] of entries.entries()) {
47
+ const e = entry as { type?: string; message?: BranchMessage };
48
+ if (e?.type !== "message" || !e.message) continue;
49
+ const m = e.message;
50
+ if (m.role === "assistant" && Array.isArray(m.content)) {
51
+ for (const block of m.content as ToolCallBlock[]) if (block?.type === "toolCall" && block.id) names.set(block.id, block.name);
52
+ } else if (m.role === "toolResult" && !m.isError) {
53
+ const seen = m.details?.oneCodeFile;
54
+ if (isObservation(seen)) yield { index, name: (m.toolCallId ? names.get(m.toolCallId) : undefined) ?? m.toolName, seen };
55
+ }
56
+ }
57
+ }
58
+
59
+ /** What the branch shows the model saw of a file, and what it was told about the file since. */
60
+ export interface SeenFile {
61
+ /** The version the model last saw, as the result or restore that showed it recorded it. */
62
+ seen: FileObservation;
63
+ /** Content hashes of the changed versions a notice named after that. */
64
+ notified: Set<string>;
65
+ }
66
+
67
+ /**
68
+ * Each file the model saw in the entries still in context (`inContextEntries`),
69
+ * by absolute path: the last result or post-compaction restore that showed
70
+ * it, and the changed versions a notice named since.
71
+ *
72
+ * In the kept turns before the latest compaction a `read` result counts, as
73
+ * at the compaction itself: a kept edit or write does not show the whole
74
+ * file, so it counts only for a file a kept read showed before it, where it
75
+ * records the version the edit left (the live tracker observes the file
76
+ * again after its own write, and the compaction keeps that). A restore counts only after the latest compaction, and there it
77
+ * counts whether or not a request delivered it yet. A later compaction is
78
+ * the only thing that drops a restore undelivered (lib/ledger/fold.ts), so
79
+ * one recorded since the latest is either delivered or rides the next
80
+ * request, and the model sees the file before it can call a tool on it; the
81
+ * live session counts it read from the compaction on, too. A notice counts
82
+ * wherever it lies after the file's last observation: it is delivered or
83
+ * pending, and announcing the same version again would repeat it.
84
+ */
85
+ export function filesSeenOnBranch(entries: readonly unknown[]): Map<string, SeenFile> {
86
+ const compaction = latestCompaction(entries)?.index ?? -1;
87
+ const results = new Map([...fileResults(entries)].map((result) => [result.index, result]));
88
+ const files = new Map<string, SeenFile>();
89
+ const see = (seen: FileObservation) => files.set(seen.path, { seen, notified: new Set() });
90
+ entries.forEach((entry, index) => {
91
+ const result = results.get(index);
92
+ if (result) {
93
+ if (index > compaction || result.name === "read" || files.has(result.seen.path)) see(result.seen);
94
+ return;
95
+ }
96
+ const record = readRecord(entry as never);
97
+ if (record?.kind !== "pending") return;
98
+ if (index > compaction) for (const seen of record.data.observations ?? []) if (isObservation(seen)) see(seen);
99
+ for (const told of record.data.notified ?? []) files.get(told.path)?.notified.add(told.sha256);
100
+ });
101
+ return files;
102
+ }
103
+
104
+ /**
105
+ * The files (absolute, as their results recorded them) that a successful
106
+ * `read` in the kept turns returned: those files are in context already, so
107
+ * the post-compaction restore skips them.
108
+ */
109
+ export function keptReadPaths(kept: readonly unknown[]): string[] {
110
+ return [...fileResults(kept)].filter((result) => result.name === "read").map((result) => result.seen.path);
111
+ }
112
+
113
+ /**
114
+ * The tracked files that a bash call in the kept turns showed in full: those
115
+ * the call certified when it ran (`readsOf`, by call id: the directory it ran
116
+ * in decided, never today's) whose content as the tracker last saw it (`seen`)
117
+ * is still all in its kept output. Like a kept `read`, they are in context
118
+ * still, so they stay read; a file changed since is then stale, not unread. A
119
+ * call needs its result in the kept turns too, and one with no record
120
+ * certifies nothing (a miss costs one re-read).
121
+ */
122
+ export function keptShellReadPaths(kept: readonly unknown[], readsOf: (callId: string) => readonly string[] | undefined, seen: (path: string) => string | undefined): string[] {
123
+ const shown = new Set<string>();
124
+ for (const { id, output } of shellCallsOnBranch(kept)) {
125
+ for (const path of readsOf(id) ?? []) {
126
+ if (shown.has(path)) continue;
127
+ const content = seen(path);
128
+ if (content !== undefined && shownInFull(output, content)) shown.add(path);
129
+ }
130
+ }
131
+ return [...shown];
132
+ }
133
+
134
+ /**
135
+ * Every bash call on the branch with its command and the text of its result,
136
+ * in result order: a resumed session re-checks which files those outputs
137
+ * showed in full (`shell-reads.ts`), against the files' current content.
138
+ */
139
+ export function* shellCallsOnBranch(entries: readonly unknown[]): Generator<{ id: string; command: string; output: string }> {
140
+ const commands = new Map<string, string>();
28
141
  for (const entry of entries) {
29
- const e = entry as { type?: string; message?: { role?: string; content?: unknown; toolCallId?: string; toolName?: string; isError?: boolean } };
142
+ const e = entry as { type?: string; message?: BranchMessage };
30
143
  if (e?.type !== "message" || !e.message) continue;
31
144
  const m = e.message;
32
145
  if (m.role === "assistant" && Array.isArray(m.content)) {
33
146
  for (const block of m.content as ToolCallBlock[]) {
34
- if (block?.type !== "toolCall" || !block.id) continue;
35
- const path = block.arguments?.path;
36
- if (typeof path === "string" && path.trim()) calls.set(block.id, { name: block.name, path });
147
+ const command = block?.type === "toolCall" && block.name === "bash" ? block.arguments?.command : undefined;
148
+ if (typeof command === "string" && block.id) commands.set(block.id, command);
37
149
  }
38
- } else if (m.role === "toolResult" && m.toolCallId && !m.isError) {
39
- const call = calls.get(m.toolCallId);
40
- if (!call) continue;
41
- if (!READ_TOOLS.has(call.name) && !WRITE_TOOLS.has(call.name)) continue;
42
- if (!seen.includes(call.path)) seen.push(call.path);
150
+ } else if (m.role === "toolResult" && m.toolCallId) {
151
+ const command = commands.get(m.toolCallId);
152
+ if (command === undefined || !Array.isArray(m.content)) continue;
153
+ const output = (m.content as { type?: string; text?: string }[])
154
+ .map((block) => (block?.type === "text" ? (block.text ?? "") : ""))
155
+ .join("\n");
156
+ yield { id: m.toolCallId, command, output };
43
157
  }
44
158
  }
45
- return seen;
46
159
  }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The files that come back after a compaction, and how. Pure: no pi imports,
3
+ * no I/O.
4
+ *
5
+ * As in Claude Code: the five files the session read, edited or wrote with
6
+ * the newest modification time (as stat'ed at that read or write), minus the
7
+ * files the context still shows, are read again. A file within 5,000 tokens
8
+ * comes back as a read call and its result, a larger one as a note to read it
9
+ * again, while the total stays within 50,000 tokens (findings §47). Each is
10
+ * its own `<system-reminder>` block at the start of the next user message,
11
+ * worded as Claude Code words them with One Code's tool name and argument
12
+ * (`read`, `path`).
13
+ */
14
+
15
+ export const RESTORE_MAX_FILES = 5;
16
+ export const RESTORE_FILE_MAX_TOKENS = 5_000;
17
+ export const RESTORE_TOTAL_MAX_TOKENS = 50_000;
18
+
19
+ /**
20
+ * The files to restore, newest first: the touched files (path to modification
21
+ * time) minus `skip`, at most five. A file that cannot be read again is
22
+ * dropped afterwards, not replaced, as in Claude Code.
23
+ */
24
+ export function restoreCandidates(touched: ReadonlyMap<string, number>, skip: (path: string) => boolean): string[] {
25
+ const candidates: string[] = [];
26
+ for (const [path] of [...touched].sort((a, b) => b[1] - a[1])) {
27
+ if (candidates.length === RESTORE_MAX_FILES) break;
28
+ if (!skip(path)) candidates.push(path);
29
+ }
30
+ return candidates;
31
+ }
32
+
33
+ /** What reading a candidate again gave: its text as the read tool returns it, or nothing (gone, unreadable, an image). */
34
+ export type RestoredRead = { path: string; text: string | undefined };
35
+
36
+ /** One `<system-reminder>` block of a restore; `restores` names the file whose contents it puts back in context. */
37
+ export type RestoreBlock = { text: string; restores?: string };
38
+
39
+ /**
40
+ * The reminder blocks for the reads, in order: two blocks for a file within
41
+ * the per-file cap (the result block `restores` it), one note for a larger
42
+ * one, nothing for a file that could not be read. A file's blocks are kept
43
+ * only while the running total stays within the overall cap. `restored` lists
44
+ * the files whose contents went back into context.
45
+ */
46
+ export function restoreBlocks(reads: RestoredRead[], estimateTokens: (text: string) => number): { blocks: RestoreBlock[]; restored: string[] } {
47
+ const blocks: RestoreBlock[] = [];
48
+ const restored: string[] = [];
49
+ let total = 0;
50
+ for (const { path, text } of reads) {
51
+ if (text === undefined) continue;
52
+ const fits = estimateTokens(text) <= RESTORE_FILE_MAX_TOKENS;
53
+ const fileBlocks: RestoreBlock[] = fits ? [{ text: readCallBlock(path) }, { text: readResultBlock(text), restores: path }] : [{ text: tooLargeNote(path) }];
54
+ const tokens = fileBlocks.reduce((sum, block) => sum + estimateTokens(block.text), 0);
55
+ if (total + tokens > RESTORE_TOTAL_MAX_TOKENS) continue;
56
+ total += tokens;
57
+ blocks.push(...fileBlocks);
58
+ if (fits) restored.push(path);
59
+ }
60
+ return { blocks, restored };
61
+ }
62
+
63
+ export const readCallBlock = (path: string) => `Called the read tool with the following input: ${JSON.stringify({ path })}`;
64
+ export const readResultBlock = (text: string) => `Result of calling the read tool:\n${text}`;
65
+ export const tooLargeNote = (path: string) =>
66
+ `Note: ${path} was read before the last conversation was summarized, but the contents are too large to include. Use read tool if you need to access it.`;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Claude Code's result and error texts for pi's built-in read, write and edit
3
+ * (pure). pi's tools keep their call shapes (`path`, `edits[]`) and their own
4
+ * behavior; only what the model reads back changes, by rewriting the first
5
+ * text block of a result whose text is pi's exact wording. Anything else
6
+ * (pi's truncation notes, an image, a block another hook appended) passes
7
+ * through untouched.
8
+ *
9
+ * The success texts say the file state is current, which is true here: the
10
+ * file-tracker observes the file after every successful write or edit, so the
11
+ * next edit is not refused as unread or stale.
12
+ */
13
+
14
+ type Block = { type: string; text?: string };
15
+
16
+ export const FILE_STATE_CURRENT = " (file state is current in your context — no need to read it back)";
17
+
18
+ /** Claude Code's missing-file error for read and edit. */
19
+ export function fileDoesNotExist(cwd: string): string {
20
+ return `File does not exist. Note: your current working directory is ${cwd}.`;
21
+ }
22
+
23
+ /** Claude Code's empty-file read result. */
24
+ export const EMPTY_FILE_WARNING = "<system-reminder>Warning: the file exists but the contents are empty.</system-reminder>";
25
+
26
+ export interface FileToolResult {
27
+ toolName: string;
28
+ isError: boolean;
29
+ content: Block[];
30
+ /** The absolute path the tool touched. */
31
+ path: string;
32
+ cwd: string;
33
+ /** For a write: whether the file existed before the call (Claude Code says "updated" then, "created" otherwise). */
34
+ existedBefore?: boolean;
35
+ /** For a read: whether the file is empty on disk (asked only for an empty result). */
36
+ isEmptyFile?: () => boolean;
37
+ }
38
+
39
+ /** The first block's replacement text, or undefined to leave the result as pi wrote it. */
40
+ function rewrite(result: FileToolResult, text: string): string | undefined {
41
+ const { toolName, isError, path, cwd } = result;
42
+ if (toolName === "write" && !isError && text.startsWith("Successfully wrote to ")) {
43
+ return result.existedBefore
44
+ ? `The file ${path} has been updated successfully.${FILE_STATE_CURRENT}`
45
+ : `File created successfully at: ${path}${FILE_STATE_CURRENT}`;
46
+ }
47
+ if (toolName === "edit" && !isError && /^Successfully replaced \d+ block\(s\) in /.test(text)) {
48
+ return `The file ${path} has been updated successfully.${FILE_STATE_CURRENT}`;
49
+ }
50
+ if (toolName === "edit" && isError && /^Could not edit file: .*\. Error code: ENOENT\.$/s.test(text)) return fileDoesNotExist(cwd);
51
+ if (toolName === "read" && isError) {
52
+ if (/^ENOENT: no such file or directory, \w+ '/.test(text)) return fileDoesNotExist(cwd);
53
+ if (text === "EISDIR: illegal operation on a directory, read") return `${text} '${path}'`;
54
+ }
55
+ if (toolName === "read" && !isError && text === "" && result.isEmptyFile?.()) return EMPTY_FILE_WARNING;
56
+ return undefined;
57
+ }
58
+
59
+ /** The result's content with Claude Code's text in place of pi's, or undefined when nothing changes. */
60
+ export function fileToolResultContent<B extends Block>(result: FileToolResult & { content: B[] }): B[] | undefined {
61
+ if (!Array.isArray(result.content)) return undefined;
62
+ const [first, ...rest] = result.content;
63
+ if (first?.type !== "text" || typeof first.text !== "string") return undefined;
64
+ const text = rewrite(result, first.text);
65
+ return text === undefined ? undefined : [{ ...first, text }, ...rest];
66
+ }
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Files a shell command showed the model in full (pure apart from the bash
3
+ * grammar, which the caller awaits).
4
+ *
5
+ * The auto-mode note tells the model to read files with cat, head or sed, and
6
+ * GPT-6 models do. Before this, the edit that followed was refused as
7
+ * "not read in this conversation" although the whole file was in the
8
+ * transcript: 16 refused edits in one 72-run battery (2026-10-04). A file now
9
+ * counts as read when a line that runs a reader names it and the output holds
10
+ * the file's entire current content. That one check covers every way the
11
+ * model could have seen less: a pipe into `head`, `cat -n`, a slice, an output
12
+ * persisted or clipped for size, a write later in the same command. The
13
+ * stale-edit guard is unchanged, since the file is observed with that content.
14
+ */
15
+
16
+ import { isAbsolute } from "node:path";
17
+ import { toAbsoluteBash } from "../auto-mode/paths.ts";
18
+ import { globComponentRegex, isUnknownTilde, movesDirectory, resolvePayload } from "../auto-mode/shell-analysis.ts";
19
+ import { LOOPS, parseCommand, type ParseResult, scopedTracker, type Segment, type Token } from "../auto-mode/shell-parse.ts";
20
+
21
+ /** Commands whose file arguments may print a whole file. */
22
+ const READERS = new Set(["cat", "head", "tail", "sed"]);
23
+
24
+ const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
25
+
26
+ /** A word a reader may have printed, with the absolute directory the shell resolves it in. */
27
+ export interface ShellReadCandidate {
28
+ word: string;
29
+ dir: string;
30
+ }
31
+
32
+ /**
33
+ * The files `command` may have printed, as written: the plain arguments of
34
+ * its reader commands, plus every path-like word of a line that runs a reader
35
+ * at all. Candidates only, each still to be checked with {@link shownInFull}.
36
+ * Each comes with the directory its reader runs in, starting from `cwd` and
37
+ * following a literal `cd` ({@link segmentDirectories}); a reader whose
38
+ * directory cannot be known offers nothing. Empty when no reader runs or the
39
+ * command does not parse (the grammar not loaded included).
40
+ */
41
+ export function shellReadCandidates(command: string, cwd: string, home: string): ShellReadCandidate[] {
42
+ const parsed = parseCommand(command);
43
+ if (parsed.parseFailed) return [];
44
+ const readers = parsed.segments.flatMap((segment, index) => {
45
+ const first = segment.tokens.findIndex((token) => !ASSIGNMENT.test(token.value));
46
+ if (first === -1) return [];
47
+ const [head, ...args] = segment.tokens.slice(first);
48
+ return head && !head.dynamic && READERS.has(head.value.split("/").pop() ?? "") ? [{ segment, index, args }] : [];
49
+ });
50
+ // Most commands run no reader: skip the directory walk for them.
51
+ if (readers.length === 0) return [];
52
+ const dirs = segmentDirectories(parsed, cwd, home);
53
+ const candidates = new Map<string, ShellReadCandidate>();
54
+ const add = (word: string, dir: string) => candidates.set(`${dir}\0${word}`, { word, dir });
55
+ const readerDirs = new Set<string>();
56
+ for (const { segment, index, args } of readers) {
57
+ const dir = dirs[index];
58
+ if (dir === undefined) continue;
59
+ readerDirs.add(dir);
60
+ if (segment.substitution !== undefined) continue;
61
+ for (const arg of args) if (isPlainPath(arg)) add(arg.value, dir);
62
+ }
63
+ // A reader fed from a variable (`for f in a.py b.py; do cat "$f"; done`)
64
+ // names its files elsewhere in the line. Every plain path-like word is a
65
+ // candidate then, in each directory a reader ran in: the full-content
66
+ // check, not the parse, decides.
67
+ if (readerDirs.size > 0) {
68
+ const words = new Set<string>();
69
+ for (const word of [...(command.match(PATH_WORD) ?? []), ...(command.match(GLOB_WORD) ?? [])]) if (words.size < MAX_CANDIDATES) words.add(word);
70
+ for (const dir of [...readerDirs].slice(0, MAX_READER_DIRS)) for (const word of words) add(word, dir);
71
+ }
72
+ return [...candidates.values()];
73
+ }
74
+
75
+ /**
76
+ * The absolute paths `command` may have printed: its candidates
77
+ * ({@link shellReadCandidates}) resolved as the shell resolves them, from
78
+ * the directory each one's reader runs in, with a glob expanded through
79
+ * `readdir` (given an absolute directory). The shell's own resolution
80
+ * (`toAbsoluteBash`), not the file tools' (`resolveToolPath` strips a leading
81
+ * `@`, which `cat @x` keeps): `~/` and the shell's directory only.
82
+ */
83
+ export function shellReadPaths(command: string, cwd: string, home: string, readdir: (dir: string) => string[]): string[] {
84
+ const paths = new Set<string>();
85
+ for (const { word, dir } of shellReadCandidates(command, cwd, home)) {
86
+ for (const name of expandCandidate(word, (sub) => readdir(toAbsoluteBash(dir, sub, home)))) paths.add(toAbsoluteBash(dir, name, home));
87
+ }
88
+ return [...paths];
89
+ }
90
+
91
+ /**
92
+ * The directory each segment runs in, or undefined where it cannot be known.
93
+ * Only a plain `cd <literal>` (or `cd -- <literal>`) that surely ran moves
94
+ * the commands after it in its own subshell scope (`scopedTracker`). One that
95
+ * may not run moves only the later members of its own `&&` list
96
+ * (`Segment.andList`), which run only once it succeeded (`cd src && cat x`
97
+ * inside the worktree's `cd <wt> && ( … )`). Anything else that may change
98
+ * the directory makes the commands after it unknown, never guessed: a
99
+ * dynamic, glob, `-` or `~user` target, an option, `pushd` and `popd`, a
100
+ * wrapper, assignment or `!` in front, a `cd` that may not run (an `if`
101
+ * branch, after `||`) for everything outside its `&&` list, a `cd` last in a
102
+ * pipeline (`lastpipe` runs it in the shell around), a backgrounded line,
103
+ * `eval` and `source`, and a line that defines a function (which may replace
104
+ * `cd`). A loop body runs more than once, so after a move in any loop every
105
+ * command in a loop is unknown too. A miss costs one re-read; a wrong
106
+ * directory would certify a read that never happened.
107
+ */
108
+ function segmentDirectories(parsed: ParseResult, cwd: string, home: string): (string | undefined)[] {
109
+ const { segments } = parsed;
110
+ const dirs = scopedTracker<string | undefined>(cwd);
111
+ /** Where a `cd` that may not have run moved the later members of its `&&` list, by list. */
112
+ const listDirs = new Map<number, string | undefined>();
113
+ const inLoop = (segment: Segment) => segment.enclosing.some((construct) => LOOPS.has(construct));
114
+ const moves = segments.map(mayMove);
115
+ const loopMoves = segments.some((segment, index) => moves[index] && inLoop(segment));
116
+ return segments.map((segment, index) => {
117
+ const inList = segment.andList !== undefined && listDirs.has(segment.andList);
118
+ const dir = loopMoves && inLoop(segment) ? undefined : inList ? listDirs.get(segment.andList!) : dirs.get(segment);
119
+ if (!moves[index]) return dir;
120
+ const certain = !parsed.background && !parsed.definesFunction && !segment.pipelineShell && !segment.enclosing.includes("negated_command");
121
+ const target = certain ? literalCdTarget(segment) : undefined;
122
+ // A relative target from an unknown directory stays unknown; an absolute one re-anchors.
123
+ const next = target === undefined ? undefined : dir === undefined && !isAbsoluteTarget(target) ? undefined : toAbsoluteBash(dir ?? cwd, target, home);
124
+ if (segment.pipelineShell) dirs.set({ scopes: segment.pipelineShell }, undefined);
125
+ if (segment.conditional) {
126
+ dirs.set(segment, undefined);
127
+ if (segment.andList !== undefined) listDirs.set(segment.andList, next);
128
+ } else {
129
+ dirs.set(segment, next);
130
+ }
131
+ return dir;
132
+ });
133
+ }
134
+
135
+ /** Whether a segment may change the shell's directory: a `cd`/`pushd`/`popd` in any spelling, or a script run in this shell. */
136
+ function mayMove(segment: Segment): boolean {
137
+ return movesDirectory(segment) || ["eval", "source", "."].includes(resolvePayload(segment.tokens).command);
138
+ }
139
+
140
+ /** The target of `cd <word>` or `cd -- <word>` written plainly, or undefined for any other form. */
141
+ function literalCdTarget(segment: Segment): string | undefined {
142
+ const [head, ...args] = segment.tokens;
143
+ if (head?.value !== "cd" || head.dynamic) return undefined;
144
+ const rest = args[0]?.value === "--" ? args.slice(1) : args;
145
+ if (rest.length > 1) return undefined;
146
+ const target = rest[0];
147
+ // A bare `cd` goes home.
148
+ if (!target) return "~";
149
+ if (target.dynamic || target.glob || target.value === "" || target.value.startsWith("-") || isUnknownTilde(target.value)) return undefined;
150
+ return target.value;
151
+ }
152
+
153
+ /** Whether a `cd` target lands in the same place from any directory. */
154
+ function isAbsoluteTarget(target: string): boolean {
155
+ return isAbsolute(target) || target === "~" || target.startsWith("~/");
156
+ }
157
+
158
+ /**
159
+ * The files a candidate names: itself, or for a glob in its last component
160
+ * (`src/*.py`, a loop's word list), the matching entries of that directory,
161
+ * listed through `readdir` (no shell). Globs in a directory component are not
162
+ * expanded.
163
+ */
164
+ export function expandCandidate(word: string, readdir: (dir: string) => string[]): string[] {
165
+ if (!/[*?[]/.test(word)) return [word];
166
+ const slash = word.lastIndexOf("/");
167
+ const dir = slash === -1 ? "" : word.slice(0, slash);
168
+ if (/[*?[]/.test(dir)) return [];
169
+ const pattern = globComponentRegex(word.slice(slash + 1));
170
+ if (!pattern) return [];
171
+ let names: string[];
172
+ try {
173
+ names = readdir(dir || ".");
174
+ } catch {
175
+ return [];
176
+ }
177
+ return names.filter((name) => !name.startsWith(".") && pattern.test(name)).map((name) => (dir ? `${dir}/${name}` : name)).slice(0, MAX_CANDIDATES);
178
+ }
179
+
180
+ /** A word that could be a file path as written, absolute or relative: no quoting, expansion or glob characters. */
181
+ const PATH_WORD = /(?<![\w$\-/])\/?[\w.@+~][\w.@+~/,-]*/g;
182
+ /** A word with a glob character, which the shell would expand (`src/*.py`). */
183
+ const GLOB_WORD = /(?<![\w$\-])[\w.@+~/,-]*[*?[][\w.@+~/,*?[\]-]*/g;
184
+ const MAX_CANDIDATES = 50;
185
+ /** The path-like words of a line are tried in at most this many reader directories. */
186
+ const MAX_READER_DIRS = 4;
187
+
188
+ function isPlainPath(token: Token): boolean {
189
+ return !token.dynamic && !token.glob && token.value !== "" && !token.value.startsWith("-");
190
+ }
191
+
192
+ /**
193
+ * Whether `output` shows the whole of `content`. Trailing whitespace is
194
+ * ignored because the shell tool trims it. A tiny one-line file is never
195
+ * counted: its text turns up in outputs by chance (an adversarial GPT-6 Astra
196
+ * run matched `completed` to "(bash completed with no output)", `1` to
197
+ * "Exit code 1", an error line, a persisted-output tag), so the match would
198
+ * not show that the file was printed. What the guard protects still holds for
199
+ * a larger match under another file's name: every byte of the file was in
200
+ * front of the model, so an edit cannot discard content it has not seen.
201
+ */
202
+ export function shownInFull(output: string, content: string): boolean {
203
+ const body = content.trimEnd();
204
+ if (body.length < MIN_SHOWN_CHARS && !(body.includes("\n") && body.length >= MIN_SHOWN_LINES_CHARS)) return false;
205
+ return output.includes(body);
206
+ }
207
+
208
+ /** A one-line file shorter than this is never counted as shown. */
209
+ const MIN_SHOWN_CHARS = 40;
210
+ /** A multi-line file shorter than this is never counted as shown. */
211
+ const MIN_SHOWN_LINES_CHARS = 16;
@@ -13,9 +13,18 @@
13
13
  export type FreshnessStatus = "fresh" | "unread" | "stale" | "absent";
14
14
 
15
15
  export interface TrackedFile {
16
- /** Content as of the last read or write we performed. */
16
+ /** Content as of the last read or write we performed ("" when `oversized`). */
17
17
  content: string;
18
+ /**
19
+ * Too large to keep: we saw the file but hold no content to compare. An
20
+ * explicit flag, never the empty string, so a file read while EMPTY
21
+ * compares like any other and cannot be overwritten after someone fills
22
+ * it (TOOLS-REVIEW-2026-09-26 M2).
23
+ */
24
+ oversized: boolean;
18
25
  at: number;
26
+ /** Seen, but changed on disk since (a resumed read whose file moved on): stale until read again. */
27
+ changed?: boolean;
19
28
  }
20
29
 
21
30
  /** What a stat says about a file on disk; equal stamps mean "no need to re-read". */
@@ -54,9 +63,9 @@ export class FileTracker {
54
63
  this.notified.delete(path);
55
64
  if (content.length > MAX_TRACKED_BYTES) {
56
65
  // Too large to diff usefully; remember that we saw it, not its content.
57
- this.files.set(path, { content: "", at });
66
+ this.files.set(path, { content: "", oversized: true, at });
58
67
  } else {
59
- this.files.set(path, { content, at });
68
+ this.files.set(path, { content, oversized: false, at });
60
69
  }
61
70
  if (stamp) this.stamps.set(path, stamp);
62
71
  else this.stamps.delete(path);
@@ -66,6 +75,16 @@ export class FileTracker {
66
75
  }
67
76
  }
68
77
 
78
+ /**
79
+ * Record a file the model saw in a form that is gone: it changed on disk
80
+ * since, and its old content is not kept. It counts as read, but stale.
81
+ */
82
+ observeChanged(path: string, at: number): void {
83
+ this.notified.delete(path);
84
+ this.stamps.delete(path);
85
+ this.files.set(path, { content: "", oversized: false, at, changed: true });
86
+ }
87
+
69
88
  forget(path: string): void {
70
89
  this.files.delete(path);
71
90
  this.notified.delete(path);
@@ -103,8 +122,10 @@ export class FileTracker {
103
122
  return this.files.has(path);
104
123
  }
105
124
 
125
+ /** The content last seen, or undefined when untracked or tracked without content (oversized). */
106
126
  lastSeen(path: string): string | undefined {
107
- return this.files.get(path)?.content;
127
+ const known = this.files.get(path);
128
+ return known && !known.oversized && !known.changed ? known.content : undefined;
108
129
  }
109
130
 
110
131
  get tracked(): string[] {
@@ -121,9 +142,10 @@ export class FileTracker {
121
142
  if (currentContent === undefined) return "absent";
122
143
  const known = this.files.get(path);
123
144
  if (!known) return "unread";
145
+ if (known.changed) return "stale";
124
146
  // Oversized files are tracked without content; treat them as fresh rather
125
147
  // than blocking edits we cannot reason about.
126
- if (known.content === "" && currentContent !== "") return "fresh";
148
+ if (known.oversized) return "fresh";
127
149
  return known.content === currentContent ? "fresh" : "stale";
128
150
  }
129
151
  }
@@ -173,20 +195,28 @@ export function describeChanges(
173
195
  }
174
196
 
175
197
  export const UNREAD_REASON = (path: string, tool: string) =>
176
- `Read ${path} before using ${tool} on it. The file exists and has not been read in this conversation, so an edit could silently discard content you have not seen.`;
198
+ `Read ${path} before using ${tool} on it. The file exists and has not been read in this conversation, so an edit could silently discard content you have not seen. A shell read counts only when it printed the whole file as it is on disk (a plain \`cat\`); after a partial one (head, sed -n, a pipe, or output cut off at the size limit) or another version of it (such as \`git show HEAD:<file>\`), use the read tool.`;
199
+
200
+ /**
201
+ * The same notice when one of this session's subagents or workflow agents
202
+ * changed the file (lib/child-writes.ts): saying "someone else, a formatter or
203
+ * a command" there sent the model looking for an outside cause.
204
+ */
205
+ export const CHILD_CHANGE_REMINDER = (path: string, agent: string | undefined, excerpt: ChangeExcerpt) =>
206
+ `Note: ${path} was changed by ${agent ? `the agent "${agent}"` : "an agent"} you started in this session since you last read it. Take it as the current state. Here are the relevant changes (shown with line numbers); re-read the file before editing it:\n${excerpt.text}`;
177
207
 
178
208
  export const STALE_REASON = (path: string) =>
179
209
  `${path} has changed on disk since you last read it — someone else, a formatter, or a command may have modified it. Read it again before editing, or your change would overwrite theirs.`;
180
210
 
181
211
  /**
182
- * Claude Code's `edited_text_file` attachment text, with two additions: the
183
- * cause list also names a command (our tracker sees bash writes too), and the
184
- * closing clause tells the model to re-read before editing — CC marks the file
185
- * as read when it attaches the snippet, we deliberately do not (the stale-edit
186
- * guard keeps firing), so without that clause the model's next edit is blocked
187
- * and costs a round trip. The "intentional / don't revert / don't tell the
188
- * user" steer is CC's: without it a model mid-edit tends to restore its own
189
- * version and narrate linter noise (STEERING-REVIEW-2026-09-05 M5).
212
+ * Claude Code's `edited_text_file` attachment text, plus One Code's closing
213
+ * clause telling the model to re-read before editing: Claude Code marks the
214
+ * file as read when it attaches the snippet, we deliberately do not (the
215
+ * stale-edit guard keeps firing), so without that clause the model's next
216
+ * edit is blocked and costs a round trip. The "take it as the current state
217
+ * rather than reverting it" steer is Claude Code's: without it a model
218
+ * mid-edit tends to restore its own version and narrate linter noise
219
+ * (STEERING-REVIEW-2026-09-05 M5).
190
220
  */
191
221
  export const EXTERNAL_CHANGE_REMINDER = (path: string, excerpt: ChangeExcerpt) =>
192
- `Note: ${path} was modified, either by the user, a linter, or a command, after you last read it. This change was intentional, so make sure to take it into account as you proceed (ie. don't revert it unless the user asks you to). Don't tell the user this, since they are already aware. Here are the relevant changes (shown with line numbers); re-read the file before editing it:\n${excerpt.text}`;
222
+ `Note: ${path} changed on disk since you last read it. That's usually deliberate, so take it as the current state rather than reverting it; if the change looks wrong, say so rather than undoing it yourself — otherwise no need to call it out. Here are the relevant changes (shown with line numbers); re-read the file before editing it:\n${excerpt.text}`;