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
@@ -2,29 +2,42 @@
2
2
  * worktree extension — Claude Code's EnterWorktree/ExitWorktree.
3
3
  *
4
4
  * enter_worktree creates (or switches into) a git worktree under
5
- * `.claude/worktrees/` and "moves" the session there. pi fixes a session's cwd
5
+ * `.claude/worktrees/` (`.onecode/worktrees/` in independent mode) and "moves"
6
+ * the session there. pi fixes a session's cwd
6
7
  * at creation, so the move is enforced on the tool_call hook: bash commands
7
8
  * are prefixed with `cd`, relative paths resolve against the worktree (see
8
9
  * rewrite.ts), and an every-turn reminder keeps the model oriented. State
9
- * rides in tool-result details so a resumed or branched session restores it.
10
+ * rides in tool-result details so a resumed or branched session restores it;
11
+ * a /clear or /new keeps the worktree, as Claude Code does, by recording it on
12
+ * the new branch.
10
13
  */
11
14
 
12
15
  import { execFile } from "node:child_process";
13
16
  import { randomBytes } from "node:crypto";
14
17
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
15
- import { join } from "node:path";
18
+ import { homedir } from "node:os";
19
+ import { join, resolve } from "node:path";
16
20
  import { promisify } from "node:util";
17
21
  import { StringEnum } from "@earendil-works/pi-ai";
18
22
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
19
23
  import { Type } from "typebox";
20
24
  import { restoreLatestDetails } from "../lib/branch-restore.ts";
25
+ import { checkoutGitRunsProgram } from "../auto-mode/git-checkout-programs.ts";
21
26
  import { DEFER_CHANNEL } from "../lib/deferred.ts";
22
27
  import { REMINDER_CHANNEL } from "../lib/reminders.ts";
28
+ import { absoluteFrom, comparablePath, tryRealpath } from "../lib/paths.ts";
23
29
  import { WORKTREE_CHANNEL, type WorktreeLocation } from "../lib/worktree-channel.ts";
24
- import { worktreeBashGuardReason } from "./guards.ts";
30
+ import { namesGit, unlistedWorktreesReason, worktreeBashGuardReason } from "./guards.ts";
31
+ import { worktreePowershellGuardReason } from "./powershell-guards.ts";
32
+ import { bashParserReady } from "../lib/bash-parser.ts";
25
33
  import { ORIGINAL_COMMAND_CHANNEL, type OriginalCommandRecord } from "../lib/original-command.ts";
34
+ import { enterWorktreeDescription, ENTER_WORKTREE_PARAMS, EXIT_WORKTREE_DESCRIPTION, EXIT_WORKTREE_PARAMS } from "./descriptions.ts";
26
35
  import { rewriteToolInput, validateWorktreeName } from "./rewrite.ts";
27
36
  import { ccToolRenderers } from "../lib/tui-render.ts";
37
+ import { findProjectRoot, HARNESS_GIT_CONFIG, HARNESS_GIT_NO_HOOKS, repositoryWorktrees } from "../lib/git.ts";
38
+ import { projectConfigDir, projectConfigDirName } from "../lib/config-mode.ts";
39
+ import { readState, readStringsState, recordState } from "../lib/ledger/state.ts";
40
+ import { processState } from "../lib/process-state.ts";
28
41
 
29
42
  const run = promisify(execFile);
30
43
  const REMINDER_KEY = "cc-worktree-session";
@@ -36,7 +49,7 @@ interface WorktreeState {
36
49
  baseCommit?: string;
37
50
  createdByUs: boolean;
38
51
  originalCwd: string;
39
- /** Root of the shared checkout (`git rev-parse --show-toplevel` at entry). */
52
+ /** Root of the main checkout, even when the session started in a linked worktree. */
40
53
  sharedRoot: string;
41
54
  }
42
55
 
@@ -46,7 +59,7 @@ interface WorktreeDetails {
46
59
  }
47
60
 
48
61
  async function git(args: string[], cwd: string): Promise<string> {
49
- const { stdout } = await run("git", args, { cwd, maxBuffer: 10 * 1024 * 1024 });
62
+ const { stdout } = await run("git", [...HARNESS_GIT_CONFIG, ...HARNESS_GIT_NO_HOOKS, ...args], { cwd, maxBuffer: 10 * 1024 * 1024 });
50
63
  return stdout.trim();
51
64
  }
52
65
 
@@ -58,6 +71,32 @@ async function listWorktreePaths(cwd: string): Promise<string[]> {
58
71
  .map((line) => line.slice("worktree ".length));
59
72
  }
60
73
 
74
+ /**
75
+ * The worktree git-isolation guard, said when the session enters: before it,
76
+ * GPT-6 Sol and Astra both ran a read-only `git -C <main checkout> status`
77
+ * and met the refusal with no warning (2026-10-04 self-test).
78
+ */
79
+ const ISOLATION_NOTE = "Git commands aimed at the main checkout or another worktree of this repository are refused until you exit.";
80
+
81
+ /** The `one-code:state` key of the worktree paths the branch was already told are gone. */
82
+ const MISSING_REPORTED_STATE = "worktree:missing-reported";
83
+
84
+ /** The `one-code:state` key of the worktree a /clear or /new carried onto its new branch. */
85
+ const CARRIED_STATE = "worktree:carried";
86
+
87
+ /**
88
+ * The worktree the session is in, as process state (lib/process-state.ts): a
89
+ * /clear or /new starts an empty branch, and Claude Code keeps the session in
90
+ * its worktree across that (decisions/tools.md). Children never load this
91
+ * extension, so only the main session writes it.
92
+ */
93
+ const liveWorktree = () => processState<{ state?: WorktreeState }>("worktree:live", () => ({}));
94
+
95
+ function isWorktreeState(value: unknown): value is WorktreeState {
96
+ const s = value as Partial<WorktreeState> | null | undefined;
97
+ return typeof s?.path === "string" && typeof s.originalCwd === "string" && typeof s.sharedRoot === "string" && typeof s.createdByUs === "boolean";
98
+ }
99
+
61
100
  export default function worktreeExtension(pi: ExtensionAPI) {
62
101
  let state: WorktreeState | undefined;
63
102
 
@@ -66,44 +105,98 @@ export default function worktreeExtension(pi: ExtensionAPI) {
66
105
  `${s.branch ? ` (branch ${s.branch})` : ""}, not in ${s.originalCwd}. ` +
67
106
  "Relative paths and bash commands already run there. Use exit_worktree to leave when the user asks.";
68
107
 
69
- const applyState = (next: WorktreeState | undefined) => {
108
+ const applyState = (next: WorktreeState | undefined, toolCallId?: string) => {
70
109
  state = next;
71
- const location: WorktreeLocation | null = next ? { path: next.path, branch: next.branch } : null;
110
+ liveWorktree().state = next;
111
+ const location: WorktreeLocation | null = next ? { path: next.path, branch: next.branch, sharedRoot: next.sharedRoot } : null;
72
112
  pi.events.emit(WORKTREE_CHANNEL, location);
73
113
  if (next) {
74
114
  pi.events.emit(REMINDER_CHANNEL, {
75
115
  text: reminderFor(next),
76
- scope: "every-turn",
77
116
  key: REMINDER_KEY,
78
117
  placement: "sticky-append",
118
+ toolCallId,
79
119
  });
80
120
  } else {
81
121
  pi.events.emit(REMINDER_CHANNEL, { key: REMINDER_KEY, remove: true });
82
122
  }
83
123
  };
84
124
 
85
- const reconstructState = (ctx: ExtensionContext) => {
125
+ /**
126
+ * The worktree a /clear or /new keeps: the one this process was in, when it
127
+ * belongs to this directory and still exists, recorded on the new branch so
128
+ * a resume of it reads it back. A branch with its own worktree tool results
129
+ * follows them instead (they come after the carry).
130
+ */
131
+ const carriedState = (ctx: ExtensionContext, reason: string | undefined): WorktreeState | undefined => {
132
+ // A recorded carry holds only in the directory it was entered from, as
133
+ // the live one does: a branch forked into another project, or continued
134
+ // after its directory went away, starts outside the worktree.
135
+ const enteredHere = (s: WorktreeState) => comparablePath(tryRealpath(s.originalCwd) ?? s.originalCwd) === comparablePath(tryRealpath(ctx.cwd) ?? ctx.cwd);
136
+ const recorded = readState(ctx, CARRIED_STATE);
137
+ if (isWorktreeState(recorded)) return enteredHere(recorded) ? recorded : undefined;
138
+ const live = liveWorktree().state;
139
+ if (reason !== "new" || !live || !existsSync(live.path) || !enteredHere(live)) return undefined;
140
+ recordState(pi, CARRIED_STATE, live);
141
+ return live;
142
+ };
143
+
144
+ const reconstructState = (ctx: ExtensionContext, reason?: string) => {
86
145
  const details = restoreLatestDetails<WorktreeDetails>(ctx.sessionManager.getBranch(), WORKTREE_TOOLS, (d) => d?.worktreeState !== undefined);
87
- let restored = details?.worktreeState ?? undefined;
88
- if (restored && !existsSync(restored.path)) restored = undefined;
146
+ let restored = details ? (details.worktreeState ?? undefined) : carriedState(ctx, reason);
147
+ if (restored && !existsSync(restored.path)) {
148
+ // Nothing in the tool details records that the session left, so every
149
+ // later restore finds the same stale state; tell the branch once, and
150
+ // record that it was told (a resume, /reload or /tree reads it back).
151
+ const reported = readStringsState(ctx, MISSING_REPORTED_STATE);
152
+ if (!reported.includes(restored.path)) {
153
+ recordState(pi, MISSING_REPORTED_STATE, [...reported, restored.path]);
154
+ pi.events.emit(REMINDER_CHANNEL, {
155
+ text: `Left worktree session; back in ${restored.originalCwd}. The worktree at ${restored.path} no longer exists.`,
156
+ });
157
+ }
158
+ restored = undefined;
159
+ }
89
160
  // Sessions persisted before sharedRoot existed: originalCwd is the best guess.
90
161
  if (restored && !restored.sharedRoot) restored = { ...restored, sharedRoot: restored.originalCwd };
91
162
  applyState(restored);
92
163
  };
93
164
 
94
- pi.on("session_start", (_event, ctx) => reconstructState(ctx));
165
+ pi.on("session_start", (event, ctx) => reconstructState(ctx, event.reason));
95
166
  pi.on("session_tree", (_event, ctx) => reconstructState(ctx));
96
167
 
97
- pi.on("tool_call", (event) => {
168
+ /** The repository's worktrees for a git command's target check; undefined when git cannot list them. */
169
+ const otherWorktrees = async (command: string, cwd: string): Promise<string[] | undefined> =>
170
+ namesGit(command) ? repositoryWorktrees(cwd) : [];
171
+
172
+ pi.on("tool_call", async (event) => {
98
173
  if (!state) return;
99
- if (["enter_worktree", "exit_worktree", "Agent", "SendMessage", "workflow"].includes(event.toolName)) return;
100
- if (event.toolName === "bash") {
174
+ if (WORKTREE_TOOLS.has(event.toolName)) return;
175
+ if (!existsSync(state.path)) {
176
+ return { block: true, reason: `The active worktree at ${state.path} no longer exists. Use exit_worktree with action: "keep" to return to ${state.originalCwd}, or enter_worktree with path to switch to an existing worktree. No tool has run in the original checkout.` };
177
+ }
178
+ if (["Agent", "SendMessage", "workflow"].includes(event.toolName)) return;
179
+ if (event.toolName === "bash" || event.toolName === "monitor") {
101
180
  // Guard before rewriting (and before the permission prompt — this
102
181
  // extension loads ahead of permissions): git must verifiably target
103
182
  // this worktree, and shared-stash footguns are refused with the recipe.
183
+ // A monitor's command runs through the same bash, so it is judged the same way.
184
+ const command = (event.input as Record<string, unknown>).command;
185
+ if (typeof command === "string") {
186
+ await bashParserReady();
187
+ const others = await otherWorktrees(command, state.path);
188
+ if (others === undefined) return { block: true, reason: unlistedWorktreesReason(state.path) };
189
+ const reason = worktreeBashGuardReason({ command, worktreePath: state.path, sharedRoot: state.sharedRoot, otherWorktrees: others });
190
+ if (reason) return { block: true, reason };
191
+ }
192
+ }
193
+ if (event.toolName === "powershell") {
194
+ // The same invariants for PowerShell, the primary shell on Windows.
104
195
  const command = (event.input as Record<string, unknown>).command;
105
196
  if (typeof command === "string") {
106
- const reason = worktreeBashGuardReason({ command, worktreePath: state.path, sharedRoot: state.sharedRoot });
197
+ const others = await otherWorktrees(command, state.path);
198
+ if (others === undefined) return { block: true, reason: unlistedWorktreesReason(state.path) };
199
+ const reason = worktreePowershellGuardReason({ command, worktreePath: state.path, sharedRoot: state.sharedRoot, otherWorktrees: others });
107
200
  if (reason) return { block: true, reason };
108
201
  }
109
202
  }
@@ -118,15 +211,12 @@ export default function worktreeExtension(pi: ExtensionAPI) {
118
211
  name: "enter_worktree",
119
212
  label: "Enter Worktree",
120
213
  ...ccToolRenderers("Enter Worktree"),
121
- description:
122
- "Create an isolated git worktree under .claude/worktrees/ (branched from the current HEAD) and switch this session into it — subsequent commands and relative paths run there. Use ONLY when the user or project instructions explicitly ask for a worktree. Pass `name` to name the new worktree, or `path` to switch into an existing worktree instead (mutually exclusive). Leave with exit_worktree.",
214
+ description: enterWorktreeDescription(projectConfigDirName()),
123
215
  parameters: Type.Object({
124
- name: Type.Optional(
125
- Type.String({ description: "Name for a new worktree (segments of letters/digits/._-, max 64 chars). Random if omitted" }),
126
- ),
127
- path: Type.Optional(Type.String({ description: "Existing worktree (from `git worktree list`) to switch into instead of creating one" })),
216
+ name: Type.Optional(Type.String({ description: ENTER_WORKTREE_PARAMS.name })),
217
+ path: Type.Optional(Type.String({ description: ENTER_WORKTREE_PARAMS.path })),
128
218
  }),
129
- async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
219
+ async execute(toolCallId, params, _signal, _onUpdate, ctx) {
130
220
  const fail = (text: string) => ({ content: [{ type: "text" as const, text }], details: {} as WorktreeDetails, isError: true });
131
221
 
132
222
  if (params.name && params.path) return fail("Pass either `name` or `path`, not both.");
@@ -139,15 +229,22 @@ export default function worktreeExtension(pi: ExtensionAPI) {
139
229
 
140
230
  let repoRoot: string;
141
231
  try {
142
- repoRoot = await git(["rev-parse", "--show-toplevel"], ctx.cwd);
232
+ // git prints `C:/…` on Windows; resolve gives the native form every path built from it shares.
233
+ repoRoot = resolve(await git(["rev-parse", "--show-toplevel"], ctx.cwd));
143
234
  } catch {
144
235
  return fail("enter_worktree needs a git repository; this directory is not one.");
145
236
  }
146
237
 
238
+ const sharedRoot = findProjectRoot(repoRoot) ?? repoRoot;
147
239
  if (params.path) {
148
240
  const known = await listWorktreePaths(ctx.cwd);
149
- const target = known.find((p) => p === params.path);
241
+ // git lists `C:/…` on Windows, while the model passes the `C:\…` we showed it.
242
+ const requested = absoluteFrom(ctx.cwd, params.path);
243
+ const wanted = comparablePath(tryRealpath(requested) ?? requested);
244
+ const listed = known.find((p) => comparablePath(tryRealpath(p) ?? p) === wanted);
245
+ const target = listed === undefined ? undefined : resolve(listed);
150
246
  if (!target) return fail(`${params.path} is not a worktree of this repository. Known worktrees:\n${known.join("\n")}`);
247
+ if (!existsSync(target)) return fail(`Worktree ${target} no longer exists. Choose an existing worktree from \`git worktree list\`.`);
151
248
  // The branch is display-only here (footer, reminder): a detached HEAD
152
249
  // leaves it unset silently, a failing git is reported in the result text.
153
250
  let branch: string | undefined;
@@ -158,10 +255,10 @@ export default function worktreeExtension(pi: ExtensionAPI) {
158
255
  } catch (error) {
159
256
  branchNote = ` Its branch could not be read (${(error as Error).message.split("\n")[0]}).`;
160
257
  }
161
- const next: WorktreeState = { path: target, branch, createdByUs: false, originalCwd: ctx.cwd, sharedRoot: repoRoot };
162
- applyState(next);
258
+ const next: WorktreeState = { path: target, branch, createdByUs: false, originalCwd: ctx.cwd, sharedRoot };
259
+ applyState(next, toolCallId);
163
260
  return {
164
- content: [{ type: "text", text: `Switched into existing worktree ${target}${branch ? ` (branch ${branch})` : ""}.${branchNote} All work now happens there; exit_worktree returns to ${ctx.cwd}.` }],
261
+ content: [{ type: "text", text: `Switched into existing worktree ${target}${branch ? ` (branch ${branch})` : ""}.${branchNote} All work now happens there; exit_worktree returns to ${ctx.cwd}. ${ISOLATION_NOTE}` }],
165
262
  details: { worktreeState: next } satisfies WorktreeDetails,
166
263
  };
167
264
  }
@@ -177,7 +274,13 @@ export default function worktreeExtension(pi: ExtensionAPI) {
177
274
  const nameError = validateWorktreeName(name);
178
275
  if (nameError) return fail(`Invalid worktree name "${name}": ${nameError}`);
179
276
 
180
- const worktreesDir = join(repoRoot, ".claude", "worktrees");
277
+ // Hooks are disabled by the git wrapper; checkout filters must go through
278
+ // the normal shell permission gate rather than execute implicitly here.
279
+ const program = checkoutGitRunsProgram(repoRoot, homedir(), []);
280
+ if (program) return fail(`Could not create worktree safely: ${program}. Create it with a permission-reviewed \`git worktree add\` command, then enter it with \`path\`.`);
281
+
282
+ // `.onecode/worktrees` in independent mode (lib/config-mode.ts).
283
+ const worktreesDir = join(projectConfigDir(repoRoot), "worktrees");
181
284
  mkdirSync(worktreesDir, { recursive: true });
182
285
  // Self-ignoring directory: worktrees never show up as untracked files.
183
286
  const ignorePath = join(worktreesDir, ".gitignore");
@@ -195,13 +298,13 @@ export default function worktreeExtension(pi: ExtensionAPI) {
195
298
  return fail(`Could not create worktree: ${(error as Error).message}`);
196
299
  }
197
300
 
198
- const next: WorktreeState = { path, branch, baseCommit, createdByUs: true, originalCwd: ctx.cwd, sharedRoot: repoRoot };
199
- applyState(next);
301
+ const next: WorktreeState = { path, branch, baseCommit, createdByUs: true, originalCwd: ctx.cwd, sharedRoot };
302
+ applyState(next, toolCallId);
200
303
  return {
201
304
  content: [
202
305
  {
203
306
  type: "text",
204
- text: `Created worktree ${path} on branch ${branch} (from HEAD ${baseCommit.slice(0, 8)}). All commands and relative paths now run there; exit_worktree returns to ${ctx.cwd}.`,
307
+ text: `Created worktree ${path} on branch ${branch} (from HEAD ${baseCommit.slice(0, 8)}). All commands and relative paths now run there; exit_worktree returns to ${ctx.cwd}. ${ISOLATION_NOTE}`,
205
308
  },
206
309
  ],
207
310
  details: { worktreeState: next } satisfies WorktreeDetails,
@@ -213,11 +316,10 @@ export default function worktreeExtension(pi: ExtensionAPI) {
213
316
  name: "exit_worktree",
214
317
  label: "Exit Worktree",
215
318
  ...ccToolRenderers("Exit Worktree"),
216
- description:
217
- 'End the worktree session started by enter_worktree and return to the original directory. action: "keep" leaves the worktree and branch on disk; "remove" deletes both — refused (listing the changes) if there are uncommitted files or commits not on the original branch, unless discard_changes is true. No-op when no worktree session is active.',
319
+ description: EXIT_WORKTREE_DESCRIPTION,
218
320
  parameters: Type.Object({
219
- action: StringEnum(["keep", "remove"] as const, { description: '"keep" preserves the worktree on disk; "remove" deletes it and its branch' }),
220
- discard_changes: Type.Optional(Type.Boolean({ description: "Required true to remove a worktree with uncommitted files or unmerged commits" })),
321
+ action: StringEnum(["keep", "remove"] as const, { description: EXIT_WORKTREE_PARAMS.action }),
322
+ discard_changes: Type.Optional(Type.Boolean({ description: EXIT_WORKTREE_PARAMS.discard_changes })),
221
323
  }),
222
324
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
223
325
  if (!state) {
@@ -234,7 +336,9 @@ export default function worktreeExtension(pi: ExtensionAPI) {
234
336
  content: [
235
337
  {
236
338
  type: "text",
237
- text: `Left worktree session; back in ${current.originalCwd}. The worktree remains at ${current.path}${current.branch ? ` (branch ${current.branch})` : ""} — re-enter it with enter_worktree {path}.`,
339
+ text: `Left worktree session; back in ${current.originalCwd}. ` + (existsSync(current.path)
340
+ ? `The worktree remains at ${current.path}${current.branch ? ` (branch ${current.branch})` : ""} — re-enter it with enter_worktree (path: "${current.path}").`
341
+ : `The worktree at ${current.path} no longer exists. No branch was removed.`),
238
342
  },
239
343
  ],
240
344
  details: { worktreeState: null } satisfies WorktreeDetails,
@@ -254,11 +358,13 @@ export default function worktreeExtension(pi: ExtensionAPI) {
254
358
  if (!params.discard_changes) {
255
359
  const blockers: string[] = [];
256
360
  try {
257
- const dirty = await git(["status", "--porcelain"], current.path);
258
- if (dirty) blockers.push(`Uncommitted changes:\n${dirty}`);
361
+ const dirty = await git(["status", "--porcelain", "--untracked-files=all"], current.path);
362
+ if (dirty) blockers.push(`Uncommitted files:\n${dirty}`);
259
363
  if (current.baseCommit && current.branch) {
260
- const ahead = await git(["rev-list", "--count", `${current.baseCommit}..${current.branch}`], current.path);
261
- if (ahead !== "0") blockers.push(`${ahead} commit(s) on ${current.branch} not on the original branch.`);
364
+ const ahead = await git(["rev-list", "--count", "HEAD", `refs/heads/${current.branch}`, `^${current.baseCommit}`], current.path);
365
+ if (ahead !== "0") blockers.push(`${ahead} commit(s) on HEAD or ${current.branch} not on the original branch.`);
366
+ } else {
367
+ blockers.push("Could not verify commits without the original branch and base commit.");
262
368
  }
263
369
  } catch {
264
370
  blockers.push("Could not verify the worktree is clean.");
@@ -278,8 +384,7 @@ export default function worktreeExtension(pi: ExtensionAPI) {
278
384
  }
279
385
 
280
386
  try {
281
- await git(["worktree", "remove", "--force", current.path], current.originalCwd);
282
- if (current.branch) await git(["branch", "-D", current.branch], current.originalCwd);
387
+ await git(["worktree", "remove", ...(params.discard_changes ? ["--force"] : []), current.path], current.originalCwd);
283
388
  } catch (error) {
284
389
  return {
285
390
  content: [{ type: "text", text: `Could not remove the worktree: ${(error as Error).message}` }],
@@ -288,6 +393,19 @@ export default function worktreeExtension(pi: ExtensionAPI) {
288
393
  };
289
394
  }
290
395
  applyState(undefined);
396
+ if (current.branch) {
397
+ try {
398
+ // The commit check above (or discard consent) already cleared the branch;
399
+ // -d would also refuse a tip the original checkout's current HEAD lacks.
400
+ await git(["branch", "-D", current.branch], current.originalCwd);
401
+ } catch (error) {
402
+ return {
403
+ content: [{ type: "text", text: `Removed worktree ${current.path}; back in ${current.originalCwd}, but could not delete branch ${current.branch}: ${(error as Error).message}. Inspect the branch before removing it manually.` }],
404
+ details: { worktreeState: null } satisfies WorktreeDetails,
405
+ isError: true,
406
+ };
407
+ }
408
+ }
291
409
  return {
292
410
  content: [{ type: "text", text: `Removed worktree ${current.path}${current.branch ? ` and branch ${current.branch}` : ""}; back in ${current.originalCwd}.` }],
293
411
  details: { worktreeState: null } satisfies WorktreeDetails,
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Worktree-session guards for the `powershell` tool (pure): the same two
3
+ * invariants guards.ts enforces for bash (git must verifiably target this
4
+ * session's worktree; shared-stash forms that collide with a concurrent
5
+ * session are refused), judged on PowerShell's statement split
6
+ * (permissions/powershell-rules.ts). On Windows PowerShell is the primary
7
+ * shell, so without this the invariant did not hold where it matters most.
8
+ *
9
+ * Conservative in the one direction that matters: git in a line the split
10
+ * cannot follow (a script block, `$(…)`, a call operator, `Invoke-Expression`,
11
+ * a nested shell, backtick escapes), after a directory change it cannot
12
+ * resolve, or next to a GIT_DIR / GIT_WORK_TREE / GIT_COMMON_DIR setting is
13
+ * refused, naming the plain form that passes. A line without git passes.
14
+ * Steering, not security: the permission gate still judges every command.
15
+ */
16
+
17
+ import { homedir } from "node:os";
18
+ import { isAbsolute, resolve } from "node:path";
19
+ import { toAbsolute } from "../auto-mode/paths.ts";
20
+ import { gitSubcommand } from "../auto-mode/shell-analysis.ts";
21
+ import { powershellInjectionSyntax, powershellStatements, statementCommand } from "../permissions/powershell-rules.ts";
22
+ import { containsPath, isolated, sharedRepositoryRoots, stashReason, type WorktreeGuardContext } from "./guards.ts";
23
+
24
+ /** git as a word (`git`, `GIT`, `git.exe`); PowerShell resolves commands case-insensitively. */
25
+ const mentionsGit = (text: string) => /\bgit(?:\.exe)?\b/i.test(text);
26
+
27
+ /** The repository variables, in any spelling (`$env:GIT_DIR = …`, `Set-Item env:GIT_WORK_TREE`, `[Environment]::SetEnvironmentVariable('GIT_DIR', …)`). */
28
+ const REPOSITORY_ENV = /\bGIT_(?:DIR|WORK_TREE|COMMON_DIR)\b/i;
29
+
30
+ /** The words of one statement, quotes removed (`'…'` literal, `"…"` with doubled quotes). */
31
+ function statementWords(statement: string): string[] {
32
+ const words: string[] = [];
33
+ const pattern = /'((?:[^']|'')*)'|"((?:[^"]|"")*)"|([^\s'"]+)/g;
34
+ let end = 0;
35
+ for (const match of statement.matchAll(pattern)) {
36
+ const value = match[1] !== undefined ? match[1].replace(/''/g, "'") : match[2] !== undefined ? match[2].replace(/""/g, '"') : match[3];
37
+ // Quotes can start mid-word: --git-dir="C:\\my repo\\.git" is one argument.
38
+ if (words.length > 0 && match.index === end) words[words.length - 1] += value;
39
+ else words.push(value);
40
+ end = match.index + match[0].length;
41
+ }
42
+ return words;
43
+ }
44
+
45
+ /** A PowerShell path word resolved against `dir`: `~` and `~\` expand, a drive or UNC path stands alone. */
46
+ function resolvePowershellPath(dir: string, word: string): string {
47
+ const home = homedir();
48
+ const tilde = word === "~" || word.startsWith("~/") || word.startsWith("~\\") ? `~/${word.slice(2)}` : word;
49
+ if (/^[A-Za-z]:[\\/]/.test(tilde) || tilde.startsWith("\\\\") || isAbsolute(tilde)) return resolve(tilde);
50
+ return toAbsolute(dir, tilde === "~/" ? "~" : tilde, home);
51
+ }
52
+
53
+ /** Values PowerShell expands at runtime (a variable, a subexpression) or a wildcard `-Path` matches. */
54
+ const dynamicPath = (word: string) => /[$`*?[]/.test(word);
55
+
56
+ /** Location parameters that take a value, besides the path itself. */
57
+ const LOCATION_VALUE_PARAMETERS = /^-(?:erroraction|ea|warningaction|wa|informationaction|infa|stackname|errorvariable|ev|warningvariable|wv|informationvariable|iv|outvariable|ov)$/i;
58
+ const PATH_PARAMETER = /^-(?:literalpath|path|lp|pspath)(?::(.+))?$/i;
59
+
60
+ /** The directory a `Set-Location` / `Push-Location` statement moves to, as written; undefined when none is named. */
61
+ function locationTarget(args: string[]): string | undefined {
62
+ let positional: string | undefined;
63
+ for (let j = 0; j < args.length; j++) {
64
+ const word = args[j];
65
+ const named = PATH_PARAMETER.exec(word);
66
+ if (named) return named[1] ?? args[j + 1] ?? "";
67
+ if (LOCATION_VALUE_PARAMETERS.test(word)) j++;
68
+ else if (!word.startsWith("-") || word === "-") positional ??= word;
69
+ }
70
+ return positional;
71
+ }
72
+
73
+ export function worktreePowershellGuardReason(context: WorktreeGuardContext): string | undefined {
74
+ const { command, worktreePath } = context;
75
+ const sharedRoots = sharedRepositoryRoots(context);
76
+ if (!mentionsGit(command)) return undefined;
77
+ const plainGit = `Run git as its own plain statement, with literal paths inside ${worktreePath} (no script blocks, call operators or nested shells).`;
78
+
79
+ const statements = powershellStatements(command);
80
+ if (statements === undefined) {
81
+ return isolated(worktreePath, "this command's quoting could not be followed, so the repository its git commands target cannot be verified", plainGit);
82
+ }
83
+ const syntax = powershellInjectionSyntax(command);
84
+ if (syntax) {
85
+ return isolated(worktreePath, `this command runs git next to ${syntax}, so the repository it targets cannot be verified`, plainGit);
86
+ }
87
+ if (REPOSITORY_ENV.test(command)) {
88
+ return isolated(
89
+ worktreePath,
90
+ "this command sets GIT_DIR, GIT_WORK_TREE or GIT_COMMON_DIR, which can point git at another repository",
91
+ `Drop the variable and run git from ${worktreePath}.`,
92
+ );
93
+ }
94
+
95
+ /** Directory the current statement runs in; undefined = not statically known. */
96
+ let dir: string | undefined = worktreePath;
97
+ for (const statement of statements) {
98
+ const cmd = statementCommand(statement);
99
+ const [, ...args] = statementWords(statement);
100
+
101
+ if (cmd === "set-location" || cmd === "push-location") {
102
+ const target = locationTarget(args);
103
+ const absolute = target !== undefined && (/^[A-Za-z]:[\\/]/.test(target) || target.startsWith("\\\\") || isAbsolute(target));
104
+ // No argument: pwsh 7 goes home. `-`/`+` walk the location history, which is not known here.
105
+ if (target === undefined) dir = args.length === 0 ? homedir() : undefined;
106
+ else if (!target || target === "-" || target === "+" || dynamicPath(target)) dir = undefined;
107
+ // A relative move from an unknown directory stays unknown; an absolute one re-anchors.
108
+ else if (dir !== undefined || absolute) dir = resolvePowershellPath(dir ?? worktreePath, target);
109
+ continue;
110
+ }
111
+ if (cmd === "pop-location") {
112
+ dir = undefined;
113
+ continue;
114
+ }
115
+ if (cmd !== "git") continue;
116
+
117
+ if (dir === undefined) {
118
+ return isolated(worktreePath, "a preceding directory change makes the repository this git command targets unverifiable", `Use literal paths inside ${worktreePath} instead.`);
119
+ }
120
+
121
+ let effective = dir;
122
+ const extraTargets: string[] = [];
123
+ let i = 0;
124
+ while (i < args.length && args[i].startsWith("-")) {
125
+ const word = args[i];
126
+ const lower = word.toLowerCase();
127
+ const pathFlag = word === "-C" || lower === "--git-dir" || lower === "--work-tree";
128
+ const inlined = lower.startsWith("--git-dir=") || lower.startsWith("--work-tree=");
129
+ const configWorktree = word === "-c" && /^core\.worktree=/i.test(args[i + 1] ?? "");
130
+ if (lower === "--config-env" || lower.startsWith("--config-env=")) {
131
+ return isolated(worktreePath, "`git --config-env` reads its configuration at runtime, so the repository it targets cannot be verified", `Use literal paths inside ${worktreePath} instead.`);
132
+ }
133
+ if (pathFlag || inlined || configWorktree) {
134
+ const raw = inlined ? word.slice(word.indexOf("=") + 1) : configWorktree ? args[i + 1].slice(args[i + 1].indexOf("=") + 1) : args[i + 1];
135
+ if (!raw || dynamicPath(raw)) {
136
+ return isolated(worktreePath, `\`git ${word}\` computes its repository target at runtime, so it cannot be verified`, `Use literal paths inside ${worktreePath} instead.`);
137
+ }
138
+ const target = resolvePowershellPath(effective, raw);
139
+ if (word === "-C") effective = target;
140
+ else extraTargets.push(target);
141
+ i += inlined ? 1 : 2;
142
+ continue;
143
+ }
144
+ i += word === "-c" || lower === "--namespace" || lower === "--exec-path" ? 2 : 1;
145
+ }
146
+
147
+ const targets = [effective, ...extraTargets];
148
+ for (const target of targets) {
149
+ if (containsPath(worktreePath, target)) continue;
150
+ if (sharedRoots.some((root) => containsPath(root, target))) {
151
+ return isolated(
152
+ worktreePath,
153
+ `this git command targets ${target}, which is the shared checkout or another worktree of the same repository`,
154
+ `Run the equivalent against ${worktreePath}.`,
155
+ );
156
+ }
157
+ }
158
+ if (targets.some((target) => containsPath(worktreePath, target))) {
159
+ const { sub, rest } = gitSubcommand(args.map((value) => ({ value })));
160
+ if (sub === "stash") {
161
+ const reason = stashReason(rest);
162
+ if (reason) return reason;
163
+ }
164
+ }
165
+ }
166
+ return undefined;
167
+ }
@@ -10,31 +10,23 @@
10
10
  */
11
11
 
12
12
  import { isAbsolute, resolve } from "node:path";
13
+ import { powershellQuote, shellQuote } from "../lib/shell-quote.ts";
14
+ import { normalizeToolPath } from "../lib/tool-path.ts";
13
15
 
14
16
  /** Tools whose `path` argument is relative to the session cwd. */
15
17
  const PATH_TOOLS = new Set(["read", "edit", "write", "notebook_edit", "grep", "find", "ls", "lsp_diagnostics"]);
16
18
  /** Of those, the ones where a missing path means "the cwd itself". */
17
19
  const DEFAULTS_TO_CWD = new Set(["grep", "find", "ls"]);
18
20
 
19
- export function shellQuote(path: string): string {
20
- return `'${path.replace(/'/g, "'\\''")}'`;
21
- }
22
-
23
- /** PowerShell single-quoted literal: only `'` needs escaping, as `''`. */
24
- export function powershellQuote(path: string): string {
25
- return `'${path.replace(/'/g, "''")}'`;
26
- }
27
-
28
-
29
21
  /**
30
- * Mutates `input` in place so the call runs inside the worktree. For bash,
31
- * returns the model's ORIGINAL command (pre-`cd`-wrapper) so the caller can
22
+ * Mutates `input` in place so the call runs inside the worktree. For bash
23
+ * (and `monitor`, which runs a bash command), returns the model's ORIGINAL command (pre-`cd`-wrapper) so the caller can
32
24
  * publish it over `ORIGINAL_COMMAND_CHANNEL` keyed by the call id: the
33
25
  * permission matcher evaluates rules against what the model asked for, not
34
26
  * the wrapper (which starts with `cd` and matches no Bash rule), while the
35
27
  * classifier and safety floor keep reading the wrapped `input.command`, whose
36
28
  * `cd` makes containment resolve inside the worktree —
37
- * docs/decisions/code-review-remediation.md ("Worktree vs. permission rules").
29
+ * working-docs/decisions/code-review-remediation.md ("Worktree vs. permission rules").
38
30
  * The original is deliberately NOT stored in `input`: a key there is
39
31
  * model-writable (lib/original-command.ts).
40
32
  */
@@ -43,7 +35,10 @@ export function rewriteToolInput(
43
35
  input: Record<string, unknown>,
44
36
  worktreePath: string,
45
37
  ): { originalCommand?: string } {
46
- if (toolName === "bash") {
38
+ // `monitor` runs its command through the same bash as the `bash` tool
39
+ // (background/index.ts), so it gets the same `cd` wrapper; a `ws` monitor
40
+ // has no command and nothing to move.
41
+ if (toolName === "bash" || toolName === "monitor") {
47
42
  if (typeof input.command === "string") {
48
43
  const originalCommand = input.command;
49
44
  input.command = `cd ${shellQuote(worktreePath)} && (${originalCommand}\n)`;
@@ -73,8 +68,11 @@ export function rewriteToolInput(
73
68
  (field) => typeof input[field] === "string" && (input[field] as string).length > 0,
74
69
  );
75
70
  if (pathField) {
76
- const value = input[pathField] as string;
77
- if (!isAbsolute(value)) input[pathField] = resolve(worktreePath, value);
71
+ // Read the value the way pi's tools will (`~`, `@`, `file://`): judged
72
+ // raw, `~/x` and `@src/x` looked relative and became literal `~` and `@src`
73
+ // directories inside the worktree.
74
+ const normalized = normalizeToolPath(input[pathField] as string);
75
+ if (!isAbsolute(normalized)) input[pathField] = resolve(worktreePath, normalized);
78
76
  } else if (DEFAULTS_TO_CWD.has(toolName)) {
79
77
  input.path = worktreePath;
80
78
  }