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.
- package/README.md +16 -5
- package/agents/general-purpose.md +2 -2
- package/docs/guide/README.md +2 -1
- package/docs/guide/bring-your-claude-code-setup.md +60 -9
- package/docs/guide/configuration.md +48 -14
- package/docs/guide/differences-from-claude-code.md +40 -28
- package/docs/guide/doctor.md +20 -13
- package/docs/guide/hooks.md +18 -3
- package/docs/guide/installation.md +26 -4
- package/docs/guide/permissions-modes-and-auto-mode.md +225 -49
- package/docs/guide/providers-and-models.md +137 -54
- package/docs/guide/reference.md +27 -15
- package/docs/guide/sessions-and-context.md +69 -16
- package/docs/guide/skills-plugins-and-mcp.md +57 -20
- package/docs/guide/subagents-and-workflows.md +95 -29
- package/docs/guide/tasks-and-background-work.md +101 -26
- package/docs/guide/terminal-interface.md +24 -12
- package/docs/guide/tools.md +46 -5
- package/docs/guide/troubleshooting.md +13 -9
- package/docs/guide/windows.md +58 -31
- package/extensions/artifacts/gallery.ts +227 -0
- package/extensions/artifacts/index.ts +288 -0
- package/extensions/artifacts/store.ts +570 -0
- package/extensions/artifacts/viewer.ts +199 -0
- package/extensions/ask-user/description.ts +67 -0
- package/extensions/ask-user/dialogs.ts +80 -0
- package/extensions/ask-user/index.ts +37 -33
- package/extensions/ask-user/questions.ts +32 -7
- package/extensions/ask-user/widget.ts +45 -25
- package/extensions/auto-mode/actions.ts +6 -0
- package/extensions/auto-mode/cache.ts +88 -0
- package/extensions/auto-mode/classifier-prompt.ts +5 -4
- package/extensions/auto-mode/classifier.ts +290 -65
- package/extensions/auto-mode/config.ts +72 -20
- package/extensions/auto-mode/decision-log.ts +2 -0
- package/extensions/auto-mode/defaults.ts +1 -1
- package/extensions/auto-mode/denials.ts +124 -0
- package/extensions/auto-mode/git-checkout-programs.ts +187 -0
- package/extensions/auto-mode/git-status-meta.ts +159 -0
- package/extensions/auto-mode/history.ts +189 -0
- package/extensions/auto-mode/instructions.ts +58 -52
- package/extensions/auto-mode/model-select.ts +159 -134
- package/extensions/auto-mode/paths.ts +57 -5
- package/extensions/auto-mode/pause.ts +57 -2
- package/extensions/auto-mode/prompt.ts +37 -21
- package/extensions/auto-mode/read-only-options.ts +724 -0
- package/extensions/auto-mode/resolved-paths-meta.ts +133 -0
- package/extensions/auto-mode/rules.ts +18 -1
- package/extensions/auto-mode/safety-floor.ts +361 -18
- package/extensions/auto-mode/setup-run.ts +20 -14
- package/extensions/auto-mode/shell-analysis.ts +764 -490
- package/extensions/auto-mode/shell-parse.ts +885 -0
- package/extensions/auto-mode/system-note.ts +33 -0
- package/extensions/auto-mode/transcript.ts +80 -51
- package/extensions/background/cron.ts +642 -0
- package/extensions/background/index.ts +614 -248
- package/extensions/background/loop-fire.ts +173 -0
- package/extensions/background/loop-preamble.ts +59 -0
- package/extensions/background/loop-skill.ts +163 -0
- package/extensions/background/monitor-batch.ts +53 -0
- package/extensions/background/noop-fold.ts +128 -0
- package/extensions/background/switch-guard.ts +56 -0
- package/extensions/background/wakeup.ts +178 -55
- package/extensions/bash/background.ts +53 -17
- package/extensions/bash/child.ts +7 -0
- package/extensions/bash/description.ts +188 -0
- package/extensions/bash/guards.ts +15 -67
- package/extensions/bash/index.ts +66 -35
- package/extensions/branding/compaction-keep.ts +63 -0
- package/extensions/branding/declined.ts +22 -0
- package/extensions/branding/index.ts +133 -9
- package/extensions/branding/prompt-editor.ts +53 -6
- package/extensions/branding/queued-edit.ts +31 -0
- package/extensions/branding/replaced-builtins.ts +68 -0
- package/extensions/branding/startup.ts +33 -71
- package/extensions/btw/index.ts +250 -78
- package/extensions/btw/panel.ts +228 -100
- package/extensions/btw/prompt.ts +41 -4
- package/extensions/cache-marks/index.ts +42 -0
- package/extensions/claude-compat/index.ts +7 -11
- package/extensions/claude-context/index.ts +436 -63
- package/extensions/compaction/cut.ts +157 -0
- package/extensions/compaction/index.ts +219 -75
- package/extensions/compaction/kept-thinking.ts +62 -0
- package/extensions/compaction/prompt.ts +15 -12
- package/extensions/context-budget/budget.ts +16 -2
- package/extensions/context-budget/index.ts +35 -15
- package/extensions/context-management/index.ts +36 -28
- package/extensions/doctor/build.ts +59 -8
- package/extensions/doctor/builtins.ts +84 -0
- package/extensions/doctor/cli.ts +7 -1
- package/extensions/doctor/compat.ts +88 -23
- package/extensions/doctor/dependencies.ts +41 -5
- package/extensions/doctor/fix-prompt.ts +9 -2
- package/extensions/doctor/index.ts +102 -58
- package/extensions/doctor/models.ts +102 -63
- package/extensions/doctor/presets.ts +42 -27
- package/extensions/doctor/report.ts +19 -2
- package/extensions/doctor/viewer.ts +49 -27
- package/extensions/effort/index.ts +141 -25
- package/extensions/effort/settings.ts +21 -0
- package/extensions/effort/slider.ts +24 -20
- package/extensions/exit/index.ts +32 -1
- package/extensions/file-tracker/index.ts +420 -42
- package/extensions/file-tracker/replay.ts +137 -24
- package/extensions/file-tracker/restore.ts +66 -0
- package/extensions/file-tracker/results.ts +66 -0
- package/extensions/file-tracker/shell-reads.ts +211 -0
- package/extensions/file-tracker/tracker.ts +45 -15
- package/extensions/footer/footer-line.ts +45 -24
- package/extensions/footer/index.ts +47 -2
- package/extensions/footer/pr.ts +34 -5
- package/extensions/hooks/compaction.ts +7 -0
- package/extensions/hooks/executor.ts +32 -30
- package/extensions/hooks/index.ts +209 -58
- package/extensions/hooks/matcher.ts +3 -0
- package/extensions/hooks/prompt-context.ts +59 -0
- package/extensions/hooks/protocol.ts +41 -3
- package/extensions/hooks/settings.ts +50 -19
- package/extensions/hooks/subagent-bridge.ts +2 -0
- package/extensions/hooks/trust.ts +26 -12
- package/extensions/idle-compact/index.ts +119 -0
- package/extensions/idle-compact/policy.ts +143 -0
- package/extensions/init/index.ts +11 -10
- package/extensions/init/prompt.ts +25 -32
- package/extensions/interrupted/index.ts +13 -1
- package/extensions/kept-thinking/index.ts +27 -0
- package/extensions/ledger/index.ts +46 -0
- package/extensions/ledger/wire.ts +154 -0
- package/extensions/lib/agent-cron.ts +146 -0
- package/extensions/lib/agent-loader.ts +18 -3
- package/extensions/lib/agent-view.ts +85 -0
- package/extensions/lib/anthropic-auth.ts +98 -0
- package/extensions/lib/anthropic-payload.ts +82 -1
- package/extensions/lib/anthropic-server-call.ts +230 -0
- package/extensions/lib/anthropic-server-tools.ts +485 -0
- package/extensions/lib/app-launch.mjs +94 -0
- package/extensions/lib/argument-hints.ts +190 -0
- package/extensions/lib/atomic-write.ts +6 -2
- package/extensions/lib/auto-memory.ts +75 -0
- package/extensions/lib/bash-parser.ts +84 -0
- package/extensions/lib/btw-fork.ts +93 -0
- package/extensions/lib/child-extensions.ts +52 -0
- package/extensions/lib/child-writes.ts +45 -0
- package/extensions/lib/claude-context.ts +515 -100
- package/extensions/lib/claude-external-includes.ts +68 -0
- package/extensions/lib/claude-id.ts +22 -0
- package/extensions/lib/claude-rules.ts +323 -0
- package/extensions/lib/claude-settings.ts +76 -4
- package/extensions/lib/compaction-boundary.ts +36 -0
- package/extensions/lib/compaction-keep.mjs +83 -0
- package/extensions/lib/config-mode.ts +112 -0
- package/extensions/lib/consent-dialogs.ts +63 -0
- package/extensions/lib/consent-preview.ts +5 -1
- package/extensions/lib/consent-stores.ts +48 -0
- package/extensions/lib/context-facts.ts +97 -0
- package/extensions/lib/covered-messages.ts +23 -0
- package/extensions/lib/deferred.ts +359 -25
- package/extensions/lib/dock-rows.ts +60 -0
- package/extensions/lib/empty-tool-result.ts +27 -0
- package/extensions/lib/environment-block.ts +76 -0
- package/extensions/lib/fetch-timeout.ts +24 -6
- package/extensions/{system-prompt → lib}/git-status.ts +51 -13
- package/extensions/lib/git.ts +75 -13
- package/extensions/lib/grammars/LICENSE-tree-sitter-bash +21 -0
- package/extensions/lib/grammars/README.md +2 -0
- package/extensions/lib/grammars/tree-sitter-bash.wasm +0 -0
- package/extensions/lib/guide-docs.ts +45 -0
- package/extensions/lib/headless-output.ts +46 -0
- package/extensions/lib/hook-gate.ts +7 -7
- package/extensions/lib/interrupt.ts +20 -4
- package/extensions/lib/key-input.ts +455 -0
- package/extensions/lib/keys.ts +14 -1
- package/extensions/lib/ledger/fold-memo.ts +49 -0
- package/extensions/lib/ledger/fold.ts +265 -0
- package/extensions/lib/ledger/layout.ts +72 -0
- package/extensions/lib/ledger/plan.ts +102 -0
- package/extensions/lib/ledger/records.ts +276 -0
- package/extensions/lib/ledger/state.ts +47 -0
- package/extensions/lib/ledger/validate.ts +141 -0
- package/extensions/lib/ledger/wire.ts +632 -0
- package/extensions/lib/local-command.ts +71 -19
- package/extensions/lib/mcp-overrides.ts +1 -1
- package/extensions/lib/mcp-share.ts +46 -0
- package/extensions/lib/memory-content.ts +146 -0
- package/extensions/lib/memory-index.ts +18 -0
- package/extensions/lib/memory.ts +134 -123
- package/extensions/lib/model-catalog/huggingface.json +178 -0
- package/extensions/lib/model-catalog/models-dev.json +18533 -0
- package/extensions/lib/model-catalog/openrouter.json +5050 -0
- package/extensions/lib/model-catalog-data.ts +513 -0
- package/extensions/lib/model-catalog.ts +462 -0
- package/extensions/lib/model-choice-warnings.ts +137 -0
- package/extensions/lib/model-pairings.ts +152 -0
- package/extensions/{auto-mode → lib}/model-picker.ts +26 -43
- package/extensions/lib/model-policy.ts +112 -15
- package/extensions/lib/model-tier.ts +283 -355
- package/extensions/lib/model-unusable.ts +1 -1
- package/extensions/lib/newer-model.ts +100 -0
- package/extensions/lib/notices.ts +87 -0
- package/extensions/lib/notifications.ts +357 -73
- package/extensions/lib/one-code-settings.ts +74 -8
- package/extensions/lib/open-browser.ts +63 -0
- package/extensions/lib/openrouter-generation.ts +94 -0
- package/extensions/lib/operation-deadline.ts +30 -0
- package/extensions/lib/original-command.ts +4 -4
- package/extensions/lib/package-version.ts +1 -1
- package/extensions/lib/panel-focus.ts +134 -0
- package/extensions/lib/paths.ts +17 -2
- package/extensions/lib/permission-gate.ts +72 -6
- package/extensions/lib/persisted-output.ts +60 -8
- package/extensions/lib/pi-ai-estimate.ts +19 -3
- package/extensions/lib/pi-version.ts +38 -2
- package/extensions/lib/plan-mode-channels.ts +42 -5
- package/extensions/lib/plugins.ts +26 -17
- package/extensions/lib/powershell-parse-script.ts +208 -0
- package/extensions/lib/powershell-parser.ts +344 -0
- package/extensions/lib/previous-mark.ts +101 -0
- package/extensions/lib/process-state.ts +48 -0
- package/extensions/lib/process-tree.ts +132 -8
- package/extensions/lib/prompt-options.ts +82 -0
- package/extensions/lib/queued-delivery.ts +44 -0
- package/extensions/lib/reminders.ts +370 -358
- package/extensions/lib/replaced-builtins.mjs +199 -0
- package/extensions/lib/replay-call.ts +70 -0
- package/extensions/lib/request-replay.ts +547 -0
- package/extensions/lib/scratchpad.ts +81 -14
- package/extensions/lib/send-now-channel.ts +7 -0
- package/extensions/lib/session-model-tier.ts +51 -0
- package/extensions/lib/session-work.ts +64 -0
- package/extensions/lib/settings-channels.ts +4 -4
- package/extensions/lib/shell-exit-semantics.ts +170 -0
- package/extensions/lib/shell-quote.ts +11 -0
- package/extensions/lib/shell-result.ts +70 -0
- package/extensions/lib/shell-spawn.ts +70 -6
- package/extensions/lib/shell-tool.ts +75 -25
- package/extensions/lib/side-call-cache.ts +124 -0
- package/extensions/lib/side-call-run.ts +96 -0
- package/extensions/lib/side-call-usage.ts +38 -0
- package/extensions/lib/side-call.ts +24 -1
- package/extensions/lib/single-flight.ts +18 -0
- package/extensions/lib/skill-body.ts +42 -0
- package/extensions/lib/skill-scan.ts +63 -24
- package/extensions/lib/spill-file.ts +140 -0
- package/extensions/lib/subagent-handback.ts +112 -0
- package/extensions/lib/system-role.ts +156 -0
- package/extensions/lib/terminal-text.ts +114 -0
- package/extensions/lib/text-width.ts +47 -16
- package/extensions/lib/timer-ops.ts +20 -0
- package/extensions/lib/tool-additions.ts +129 -0
- package/extensions/lib/tool-call-corruption.ts +134 -0
- package/extensions/lib/tool-path.ts +66 -0
- package/extensions/lib/tool-variants.ts +69 -0
- package/extensions/lib/tui-render.ts +255 -27
- package/extensions/lib/turn-budget-layout.ts +159 -0
- package/extensions/lib/update-check.mjs +15 -2
- package/extensions/lib/usage-bus.ts +1 -1
- package/extensions/lib/user-denial.ts +38 -0
- package/extensions/lib/workspace-channel.ts +16 -0
- package/extensions/lib/worktree-channel.ts +31 -3
- package/extensions/lib/worktree-isolation.ts +40 -10
- package/extensions/lsp/client.ts +8 -9
- package/extensions/lsp/index.ts +76 -12
- package/extensions/lsp/keep-alive.ts +1 -1
- package/extensions/lsp/trust.ts +119 -0
- package/extensions/lsp/watcher.ts +1 -1
- package/extensions/mcp/announce.ts +90 -43
- package/extensions/mcp/client.ts +161 -48
- package/extensions/mcp/config.ts +132 -8
- package/extensions/mcp/discovery.ts +19 -0
- package/extensions/mcp/index.ts +220 -121
- package/extensions/mcp/oauth/browser.ts +38 -17
- package/extensions/mcp/oauth/flow.ts +25 -17
- package/extensions/mcp/oauth/provider.ts +14 -8
- package/extensions/mcp/oauth/store.ts +34 -16
- package/extensions/mcp/panel/keys.ts +12 -11
- package/extensions/mcp/panel/render.ts +9 -6
- package/extensions/mcp/schema.ts +104 -5
- package/extensions/mcp/trust.ts +164 -73
- package/extensions/memory/entries.ts +23 -14
- package/extensions/memory/index.ts +134 -28
- package/extensions/memory/open-external.ts +33 -18
- package/extensions/memory/panel.ts +135 -26
- package/extensions/model-default/index.ts +86 -19
- package/extensions/newer-model/index.ts +31 -0
- package/extensions/notebook/description.ts +18 -0
- package/extensions/notebook/index.ts +6 -5
- package/extensions/notebook/notebook.ts +1 -1
- package/extensions/permissions/index.ts +1426 -398
- package/extensions/permissions/matcher.ts +425 -74
- package/extensions/permissions/modes.ts +11 -4
- package/extensions/permissions/panel/host.ts +130 -0
- package/extensions/permissions/panel/keys.ts +48 -0
- package/extensions/permissions/panel/render.ts +450 -0
- package/extensions/permissions/panel/state.ts +550 -0
- package/extensions/permissions/powershell-cmdlets.ts +198 -0
- package/extensions/permissions/powershell-paths.ts +197 -0
- package/extensions/permissions/powershell-rules.ts +207 -280
- package/extensions/permissions/powershell-tree.ts +557 -0
- package/extensions/permissions/project-trust.ts +27 -11
- package/extensions/permissions/protected-paths.ts +14 -2
- package/extensions/permissions/session-grant.ts +6 -0
- package/extensions/permissions/settings.ts +166 -31
- package/extensions/permissions/subagent-gate.ts +25 -0
- package/extensions/permissions/workspace.ts +78 -0
- package/extensions/plan-mode/index.ts +276 -92
- package/extensions/plan-mode/reminder.ts +89 -21
- package/extensions/plan-mode/run-state.ts +26 -0
- package/extensions/plan-mode/texts.ts +177 -0
- package/extensions/plan-mode/viewer.ts +92 -27
- package/extensions/plugins/counts.ts +3 -3
- package/extensions/plugins/index.ts +64 -46
- package/extensions/plugins/install/install.ts +16 -0
- package/extensions/plugins/marketplace/sync.ts +9 -3
- package/extensions/plugins/marketplace/types.ts +3 -1
- package/extensions/plugins/panel/keys.ts +21 -24
- package/extensions/plugins/panel/render.ts +12 -6
- package/extensions/plugins/shell-expand.ts +107 -0
- package/extensions/plugins/template.ts +32 -15
- package/extensions/powershell/child.ts +7 -0
- package/extensions/powershell/description.ts +38 -6
- package/extensions/powershell/guards.ts +1 -1
- package/extensions/powershell/index.ts +14 -7
- package/extensions/recap/index.ts +73 -71
- package/extensions/recap/prompt.ts +2 -2
- package/extensions/recap/scheduler.ts +3 -5
- package/extensions/search-tools/grep-merge.ts +39 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/send-now/chord.ts +62 -0
- package/extensions/send-now/index.ts +133 -0
- package/extensions/session-title/index.ts +3 -0
- package/extensions/session-title/title.ts +4 -1
- package/extensions/skill/announce.ts +30 -0
- package/extensions/skill/code-review.ts +97 -0
- package/extensions/skill/index.ts +469 -132
- package/extensions/skill/invoke.ts +118 -75
- package/extensions/skill/listing.ts +133 -9
- package/extensions/skill/panel/keys.ts +15 -16
- package/extensions/skill/panel/render.ts +5 -2
- package/extensions/skill/panel/state.ts +12 -1
- package/extensions/subagents/agent-args.ts +39 -0
- package/extensions/subagents/agent-description.ts +132 -0
- package/extensions/subagents/agents.ts +146 -31
- package/extensions/subagents/default-model.ts +2 -3
- package/extensions/subagents/delegation-steer.ts +4 -4
- package/extensions/subagents/fork-cache.ts +82 -0
- package/extensions/subagents/guide-agent.ts +189 -0
- package/extensions/subagents/hand-back-review.ts +3 -0
- package/extensions/subagents/index.ts +1594 -852
- package/extensions/subagents/live-runs.ts +26 -6
- package/extensions/subagents/model-select.ts +38 -25
- package/extensions/subagents/outcome.ts +5 -2
- package/extensions/subagents/panel-keys.ts +73 -31
- package/extensions/subagents/panel-render.ts +120 -96
- package/extensions/subagents/panel-widget.ts +60 -89
- package/extensions/subagents/parent-mode.ts +50 -0
- package/extensions/subagents/pending-claim.ts +18 -1
- package/extensions/subagents/runner.ts +135 -80
- package/extensions/subagents/runs.ts +44 -5
- package/extensions/subagents/send-to-main-tool.ts +1 -1
- package/extensions/subagents/session-turns.ts +32 -8
- package/extensions/subagents/shell-panel.ts +36 -178
- package/extensions/subagents/tasks-dialog.ts +170 -0
- package/extensions/subagents/viewport-mirror.ts +95 -0
- package/extensions/subagents/worktree.ts +57 -13
- package/extensions/system-prompt/environment.ts +19 -19
- package/extensions/system-prompt/idle-turn.ts +65 -0
- package/extensions/system-prompt/index.ts +165 -47
- package/extensions/system-prompt/template.ts +34 -51
- package/extensions/system-prompt/tiers/common.ts +23 -64
- package/extensions/system-prompt/tiers/frontier.ts +32 -11
- package/extensions/system-prompt/tiers/low.ts +67 -45
- package/extensions/system-prompt/tiers/mid.ts +96 -38
- package/extensions/system-reminder/index.ts +204 -70
- package/extensions/tasks/descriptions.ts +190 -0
- package/extensions/tasks/index.ts +120 -41
- package/extensions/tasks/store.ts +83 -16
- package/extensions/tool-call-corruption/child.ts +7 -0
- package/extensions/tool-call-corruption/index.ts +237 -0
- package/extensions/tool-search/index.ts +366 -144
- package/extensions/tool-style/index.ts +68 -10
- package/extensions/tool-style/write-description.ts +40 -0
- package/extensions/tool-style/write-render.ts +237 -0
- package/extensions/turn-duration/index.ts +7 -3
- package/extensions/turn-duration/line.ts +47 -4
- package/extensions/web/backends.ts +6 -9
- package/extensions/web/description.ts +50 -0
- package/extensions/web/index.ts +118 -53
- package/extensions/web/native-call.ts +15 -0
- package/extensions/web/native-results.ts +41 -0
- package/extensions/web-fetch/description.ts +42 -0
- package/extensions/web-fetch/extract.ts +53 -6
- package/extensions/web-fetch/index.ts +125 -76
- package/extensions/web-fetch/page-content.ts +19 -0
- package/extensions/web-fetch/response.ts +49 -0
- package/extensions/web-fetch/summarize.ts +9 -10
- package/extensions/workflow/agent-session.ts +108 -18
- package/extensions/workflow/args.ts +24 -0
- package/extensions/workflow/description.ts +67 -0
- package/extensions/workflow/globals.ts +17 -3
- package/extensions/workflow/index.ts +157 -162
- package/extensions/workflow/records.ts +1 -0
- package/extensions/workflow/run-manager.ts +33 -6
- package/extensions/workflow/saved-workflows.ts +3 -2
- package/extensions/workflow/types.ts +22 -1
- package/extensions/workflow/viewer.ts +54 -24
- package/extensions/workflow/widget.ts +20 -0
- package/extensions/worktree/descriptions.ts +91 -0
- package/extensions/worktree/guards.ts +316 -54
- package/extensions/worktree/index.ts +162 -44
- package/extensions/worktree/powershell-guards.ts +167 -0
- package/extensions/worktree/rewrite.ts +14 -16
- package/package.json +21 -7
- package/skills/artifact-design/SKILL.md +85 -0
- package/skills/code-review/SKILL.md +20 -20
- package/skills/code-review/cells/arguments-and-flags.md +19 -0
- package/skills/code-review/cells/high-3x5-recall-verify.md +137 -0
- package/skills/code-review/cells/high-8-inline.md +122 -0
- package/skills/code-review/cells/low-1-pass-max4.md +27 -0
- package/skills/code-review/cells/low-1-pass-min-files-4.md +28 -0
- package/skills/code-review/cells/max-5x5-verify-sweep.md +162 -0
- package/skills/code-review/cells/medium-3x5-verify.md +132 -0
- package/skills/code-review/cells/minimal-prompt.md +32 -0
- package/skills/code-review/cells/xhigh-10-inline-sweep.md +149 -0
- package/skills/code-review/cells/xhigh-5x5-verify-sweep.md +162 -0
- package/skills/fewer-permission-prompts/SKILL.md +1 -1
- package/skills/loop/SKILL.md +11 -0
- package/skills/security-review/SKILL.md +2 -5
- package/skills/simplify/SKILL.md +7 -5
- package/skills/workflow-authoring/SKILL.md +143 -0
- package/themes/onecode-light.json +1 -1
- package/themes/onecode.json +1 -1
- package/types/app.d.ts +58 -0
- package/types/community-packages.d.ts +26 -1
- package/extensions/auto-mode/recoverability.ts +0 -229
- package/extensions/lib/capability-index.ts +0 -466
- package/extensions/lib/model-facts.generated.json +0 -2675
- package/extensions/lib/model-facts.ts +0 -162
- 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"
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
88
|
-
if (restored && !existsSync(restored.path))
|
|
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", (
|
|
165
|
+
pi.on("session_start", (event, ctx) => reconstructState(ctx, event.reason));
|
|
95
166
|
pi.on("session_tree", (_event, ctx) => reconstructState(ctx));
|
|
96
167
|
|
|
97
|
-
|
|
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 (
|
|
100
|
-
if (
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
220
|
-
discard_changes: Type.Optional(Type.Boolean({ description:
|
|
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}.
|
|
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
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
}
|