one-code-extension 0.4.0 → 0.5.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 +219 -219
- 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 +32 -23
- package/docs/guide/doctor.md +20 -13
- package/docs/guide/hooks.md +18 -3
- package/docs/guide/installation.md +27 -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 +66 -34
- 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 +123 -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 +515 -0
- package/extensions/background/index.ts +514 -236
- package/extensions/background/loop-fire.ts +136 -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 +52 -0
- package/extensions/background/wakeup.ts +159 -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 +88 -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 +229 -71
- package/extensions/btw/panel.ts +228 -100
- package/extensions/btw/prompt.ts +41 -4
- package/extensions/claude-compat/index.ts +7 -11
- package/extensions/claude-context/index.ts +446 -58
- package/extensions/compaction/cut.ts +157 -0
- package/extensions/compaction/index.ts +216 -75
- package/extensions/compaction/kept-thinking.ts +62 -0
- package/extensions/compaction/prompt.ts +15 -12
- package/extensions/context-budget/index.ts +24 -5
- package/extensions/context-management/index.ts +25 -0
- 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 +72 -19
- 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 +337 -26
- package/extensions/file-tracker/replay.ts +96 -10
- package/extensions/file-tracker/restore.ts +63 -0
- package/extensions/file-tracker/results.ts +66 -0
- package/extensions/file-tracker/shell-reads.ts +211 -0
- package/extensions/file-tracker/tracker.ts +32 -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 +198 -31
- package/extensions/hooks/matcher.ts +3 -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 +13 -5
- 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/lib/agent-cron.ts +130 -0
- package/extensions/lib/agent-loader.ts +15 -1
- package/extensions/lib/agent-view.ts +85 -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 +42 -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 +78 -0
- package/extensions/lib/context-stack.ts +81 -0
- package/extensions/lib/deferred.ts +219 -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/local-command.ts +47 -13
- 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 +174 -0
- package/extensions/lib/model-catalog/models-dev.json +18297 -0
- package/extensions/lib/model-catalog/openrouter.json +5252 -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 +308 -64
- 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 +21 -2
- package/extensions/lib/plan-mode-channels.ts +20 -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/process-tree.ts +131 -8
- package/extensions/lib/prompt-options.ts +82 -0
- package/extensions/lib/queued-delivery.ts +44 -0
- package/extensions/lib/reminders.ts +364 -58
- package/extensions/lib/replaced-builtins.mjs +199 -0
- package/extensions/lib/replay-call.ts +70 -0
- package/extensions/lib/request-replay.ts +439 -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 +13 -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 +113 -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 +149 -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 +143 -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 +70 -4
- 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 +87 -5
- 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 +209 -71
- 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 +151 -67
- 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/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 +1300 -381
- package/extensions/permissions/matcher.ts +425 -74
- package/extensions/permissions/modes.ts +6 -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 +156 -69
- package/extensions/plan-mode/reminder.ts +89 -21
- 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 +61 -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 +12 -7
- package/extensions/recap/index.ts +65 -60
- 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/code-review.ts +97 -0
- package/extensions/skill/index.ts +421 -101
- package/extensions/skill/invoke.ts +128 -18
- 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 +116 -23
- package/extensions/subagents/announce.ts +69 -0
- package/extensions/subagents/default-model.ts +2 -3
- package/extensions/subagents/delegation-steer.ts +4 -4
- package/extensions/subagents/fork-cache.ts +76 -0
- package/extensions/subagents/guide-agent.ts +189 -0
- package/extensions/subagents/hand-back-review.ts +3 -0
- package/extensions/subagents/index.ts +1493 -824
- package/extensions/subagents/live-runs.ts +26 -6
- package/extensions/subagents/model-select.ts +38 -25
- package/extensions/subagents/outcome.ts +2 -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/runner.ts +108 -80
- package/extensions/subagents/runs.ts +39 -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 +131 -47
- package/extensions/system-prompt/template.ts +24 -50
- 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 +175 -13
- 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/announce.ts +16 -0
- package/extensions/tool-search/index.ts +189 -41
- 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 +105 -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 +145 -162
- package/extensions/workflow/records.ts +1 -0
- package/extensions/workflow/run-manager.ts +29 -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 +121 -39
- package/extensions/worktree/powershell-guards.ts +167 -0
- package/extensions/worktree/rewrite.ts +14 -16
- package/package.json +19 -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
|
@@ -2,7 +2,8 @@
|
|
|
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
|
|
@@ -12,19 +13,27 @@
|
|
|
12
13
|
import { execFile } from "node:child_process";
|
|
13
14
|
import { randomBytes } from "node:crypto";
|
|
14
15
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
15
|
-
import {
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
import { join, resolve } from "node:path";
|
|
16
18
|
import { promisify } from "node:util";
|
|
17
19
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
18
20
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
19
21
|
import { Type } from "typebox";
|
|
20
22
|
import { restoreLatestDetails } from "../lib/branch-restore.ts";
|
|
23
|
+
import { checkoutGitRunsProgram } from "../auto-mode/git-checkout-programs.ts";
|
|
21
24
|
import { DEFER_CHANNEL } from "../lib/deferred.ts";
|
|
22
25
|
import { REMINDER_CHANNEL } from "../lib/reminders.ts";
|
|
26
|
+
import { absoluteFrom, comparablePath, tryRealpath } from "../lib/paths.ts";
|
|
23
27
|
import { WORKTREE_CHANNEL, type WorktreeLocation } from "../lib/worktree-channel.ts";
|
|
24
|
-
import { worktreeBashGuardReason } from "./guards.ts";
|
|
28
|
+
import { namesGit, unlistedWorktreesReason, worktreeBashGuardReason } from "./guards.ts";
|
|
29
|
+
import { worktreePowershellGuardReason } from "./powershell-guards.ts";
|
|
30
|
+
import { bashParserReady } from "../lib/bash-parser.ts";
|
|
25
31
|
import { ORIGINAL_COMMAND_CHANNEL, type OriginalCommandRecord } from "../lib/original-command.ts";
|
|
32
|
+
import { enterWorktreeDescription, ENTER_WORKTREE_PARAMS, EXIT_WORKTREE_DESCRIPTION, EXIT_WORKTREE_PARAMS } from "./descriptions.ts";
|
|
26
33
|
import { rewriteToolInput, validateWorktreeName } from "./rewrite.ts";
|
|
27
34
|
import { ccToolRenderers } from "../lib/tui-render.ts";
|
|
35
|
+
import { findProjectRoot, HARNESS_GIT_CONFIG, HARNESS_GIT_NO_HOOKS, repositoryWorktrees } from "../lib/git.ts";
|
|
36
|
+
import { projectConfigDir, projectConfigDirName } from "../lib/config-mode.ts";
|
|
28
37
|
|
|
29
38
|
const run = promisify(execFile);
|
|
30
39
|
const REMINDER_KEY = "cc-worktree-session";
|
|
@@ -36,7 +45,7 @@ interface WorktreeState {
|
|
|
36
45
|
baseCommit?: string;
|
|
37
46
|
createdByUs: boolean;
|
|
38
47
|
originalCwd: string;
|
|
39
|
-
/** Root of the
|
|
48
|
+
/** Root of the main checkout, even when the session started in a linked worktree. */
|
|
40
49
|
sharedRoot: string;
|
|
41
50
|
}
|
|
42
51
|
|
|
@@ -46,7 +55,7 @@ interface WorktreeDetails {
|
|
|
46
55
|
}
|
|
47
56
|
|
|
48
57
|
async function git(args: string[], cwd: string): Promise<string> {
|
|
49
|
-
const { stdout } = await run("git", args, { cwd, maxBuffer: 10 * 1024 * 1024 });
|
|
58
|
+
const { stdout } = await run("git", [...HARNESS_GIT_CONFIG, ...HARNESS_GIT_NO_HOOKS, ...args], { cwd, maxBuffer: 10 * 1024 * 1024 });
|
|
50
59
|
return stdout.trim();
|
|
51
60
|
}
|
|
52
61
|
|
|
@@ -58,6 +67,20 @@ async function listWorktreePaths(cwd: string): Promise<string[]> {
|
|
|
58
67
|
.map((line) => line.slice("worktree ".length));
|
|
59
68
|
}
|
|
60
69
|
|
|
70
|
+
/**
|
|
71
|
+
* The worktree git-isolation guard, said when the session enters: before it,
|
|
72
|
+
* GPT-6 Sol and Astra both ran a read-only `git -C <main checkout> status`
|
|
73
|
+
* and met the refusal with no warning (2026-10-04 self-test).
|
|
74
|
+
*/
|
|
75
|
+
const ISOLATION_NOTE = "Git commands aimed at the main checkout or another worktree of this repository are refused until you exit.";
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Sessions already told their restored worktree is gone, as `<session id>\0<path>`.
|
|
79
|
+
* Module scope on purpose: jiti keeps this module across the factory re-run
|
|
80
|
+
* that `/reload` makes, and a reload restores the same stale state again.
|
|
81
|
+
*/
|
|
82
|
+
const reportedMissingWorktrees = new Set<string>();
|
|
83
|
+
|
|
61
84
|
export default function worktreeExtension(pi: ExtensionAPI) {
|
|
62
85
|
let state: WorktreeState | undefined;
|
|
63
86
|
|
|
@@ -66,9 +89,9 @@ export default function worktreeExtension(pi: ExtensionAPI) {
|
|
|
66
89
|
`${s.branch ? ` (branch ${s.branch})` : ""}, not in ${s.originalCwd}. ` +
|
|
67
90
|
"Relative paths and bash commands already run there. Use exit_worktree to leave when the user asks.";
|
|
68
91
|
|
|
69
|
-
const applyState = (next: WorktreeState | undefined) => {
|
|
92
|
+
const applyState = (next: WorktreeState | undefined, toolCallId?: string) => {
|
|
70
93
|
state = next;
|
|
71
|
-
const location: WorktreeLocation | null = next ? { path: next.path, branch: next.branch } : null;
|
|
94
|
+
const location: WorktreeLocation | null = next ? { path: next.path, branch: next.branch, sharedRoot: next.sharedRoot } : null;
|
|
72
95
|
pi.events.emit(WORKTREE_CHANNEL, location);
|
|
73
96
|
if (next) {
|
|
74
97
|
pi.events.emit(REMINDER_CHANNEL, {
|
|
@@ -76,6 +99,7 @@ export default function worktreeExtension(pi: ExtensionAPI) {
|
|
|
76
99
|
scope: "every-turn",
|
|
77
100
|
key: REMINDER_KEY,
|
|
78
101
|
placement: "sticky-append",
|
|
102
|
+
toolCallId,
|
|
79
103
|
});
|
|
80
104
|
} else {
|
|
81
105
|
pi.events.emit(REMINDER_CHANNEL, { key: REMINDER_KEY, remove: true });
|
|
@@ -85,7 +109,18 @@ export default function worktreeExtension(pi: ExtensionAPI) {
|
|
|
85
109
|
const reconstructState = (ctx: ExtensionContext) => {
|
|
86
110
|
const details = restoreLatestDetails<WorktreeDetails>(ctx.sessionManager.getBranch(), WORKTREE_TOOLS, (d) => d?.worktreeState !== undefined);
|
|
87
111
|
let restored = details?.worktreeState ?? undefined;
|
|
88
|
-
if (restored && !existsSync(restored.path))
|
|
112
|
+
if (restored && !existsSync(restored.path)) {
|
|
113
|
+
// Nothing records that the session left, so every later restore finds
|
|
114
|
+
// the same stale state; tell the model once per session.
|
|
115
|
+
const key = `${ctx.sessionManager.getSessionId()}\0${restored.path}`;
|
|
116
|
+
if (!reportedMissingWorktrees.has(key)) {
|
|
117
|
+
reportedMissingWorktrees.add(key);
|
|
118
|
+
pi.events.emit(REMINDER_CHANNEL, {
|
|
119
|
+
text: `Left worktree session; back in ${restored.originalCwd}. The worktree at ${restored.path} no longer exists.`,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
restored = undefined;
|
|
123
|
+
}
|
|
89
124
|
// Sessions persisted before sharedRoot existed: originalCwd is the best guess.
|
|
90
125
|
if (restored && !restored.sharedRoot) restored = { ...restored, sharedRoot: restored.originalCwd };
|
|
91
126
|
applyState(restored);
|
|
@@ -94,16 +129,38 @@ export default function worktreeExtension(pi: ExtensionAPI) {
|
|
|
94
129
|
pi.on("session_start", (_event, ctx) => reconstructState(ctx));
|
|
95
130
|
pi.on("session_tree", (_event, ctx) => reconstructState(ctx));
|
|
96
131
|
|
|
97
|
-
|
|
132
|
+
/** The repository's worktrees for a git command's target check; undefined when git cannot list them. */
|
|
133
|
+
const otherWorktrees = async (command: string, cwd: string): Promise<string[] | undefined> =>
|
|
134
|
+
namesGit(command) ? repositoryWorktrees(cwd) : [];
|
|
135
|
+
|
|
136
|
+
pi.on("tool_call", async (event) => {
|
|
98
137
|
if (!state) return;
|
|
99
|
-
if (
|
|
100
|
-
if (
|
|
138
|
+
if (WORKTREE_TOOLS.has(event.toolName)) return;
|
|
139
|
+
if (!existsSync(state.path)) {
|
|
140
|
+
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.` };
|
|
141
|
+
}
|
|
142
|
+
if (["Agent", "SendMessage", "workflow"].includes(event.toolName)) return;
|
|
143
|
+
if (event.toolName === "bash" || event.toolName === "monitor") {
|
|
101
144
|
// Guard before rewriting (and before the permission prompt — this
|
|
102
145
|
// extension loads ahead of permissions): git must verifiably target
|
|
103
146
|
// this worktree, and shared-stash footguns are refused with the recipe.
|
|
147
|
+
// A monitor's command runs through the same bash, so it is judged the same way.
|
|
148
|
+
const command = (event.input as Record<string, unknown>).command;
|
|
149
|
+
if (typeof command === "string") {
|
|
150
|
+
await bashParserReady();
|
|
151
|
+
const others = await otherWorktrees(command, state.path);
|
|
152
|
+
if (others === undefined) return { block: true, reason: unlistedWorktreesReason(state.path) };
|
|
153
|
+
const reason = worktreeBashGuardReason({ command, worktreePath: state.path, sharedRoot: state.sharedRoot, otherWorktrees: others });
|
|
154
|
+
if (reason) return { block: true, reason };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (event.toolName === "powershell") {
|
|
158
|
+
// The same invariants for PowerShell, the primary shell on Windows.
|
|
104
159
|
const command = (event.input as Record<string, unknown>).command;
|
|
105
160
|
if (typeof command === "string") {
|
|
106
|
-
const
|
|
161
|
+
const others = await otherWorktrees(command, state.path);
|
|
162
|
+
if (others === undefined) return { block: true, reason: unlistedWorktreesReason(state.path) };
|
|
163
|
+
const reason = worktreePowershellGuardReason({ command, worktreePath: state.path, sharedRoot: state.sharedRoot, otherWorktrees: others });
|
|
107
164
|
if (reason) return { block: true, reason };
|
|
108
165
|
}
|
|
109
166
|
}
|
|
@@ -118,15 +175,12 @@ export default function worktreeExtension(pi: ExtensionAPI) {
|
|
|
118
175
|
name: "enter_worktree",
|
|
119
176
|
label: "Enter Worktree",
|
|
120
177
|
...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.",
|
|
178
|
+
description: enterWorktreeDescription(projectConfigDirName()),
|
|
123
179
|
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" })),
|
|
180
|
+
name: Type.Optional(Type.String({ description: ENTER_WORKTREE_PARAMS.name })),
|
|
181
|
+
path: Type.Optional(Type.String({ description: ENTER_WORKTREE_PARAMS.path })),
|
|
128
182
|
}),
|
|
129
|
-
async execute(
|
|
183
|
+
async execute(toolCallId, params, _signal, _onUpdate, ctx) {
|
|
130
184
|
const fail = (text: string) => ({ content: [{ type: "text" as const, text }], details: {} as WorktreeDetails, isError: true });
|
|
131
185
|
|
|
132
186
|
if (params.name && params.path) return fail("Pass either `name` or `path`, not both.");
|
|
@@ -139,15 +193,22 @@ export default function worktreeExtension(pi: ExtensionAPI) {
|
|
|
139
193
|
|
|
140
194
|
let repoRoot: string;
|
|
141
195
|
try {
|
|
142
|
-
|
|
196
|
+
// git prints `C:/…` on Windows; resolve gives the native form every path built from it shares.
|
|
197
|
+
repoRoot = resolve(await git(["rev-parse", "--show-toplevel"], ctx.cwd));
|
|
143
198
|
} catch {
|
|
144
199
|
return fail("enter_worktree needs a git repository; this directory is not one.");
|
|
145
200
|
}
|
|
146
201
|
|
|
202
|
+
const sharedRoot = findProjectRoot(repoRoot) ?? repoRoot;
|
|
147
203
|
if (params.path) {
|
|
148
204
|
const known = await listWorktreePaths(ctx.cwd);
|
|
149
|
-
|
|
205
|
+
// git lists `C:/…` on Windows, while the model passes the `C:\…` we showed it.
|
|
206
|
+
const requested = absoluteFrom(ctx.cwd, params.path);
|
|
207
|
+
const wanted = comparablePath(tryRealpath(requested) ?? requested);
|
|
208
|
+
const listed = known.find((p) => comparablePath(tryRealpath(p) ?? p) === wanted);
|
|
209
|
+
const target = listed === undefined ? undefined : resolve(listed);
|
|
150
210
|
if (!target) return fail(`${params.path} is not a worktree of this repository. Known worktrees:\n${known.join("\n")}`);
|
|
211
|
+
if (!existsSync(target)) return fail(`Worktree ${target} no longer exists. Choose an existing worktree from \`git worktree list\`.`);
|
|
151
212
|
// The branch is display-only here (footer, reminder): a detached HEAD
|
|
152
213
|
// leaves it unset silently, a failing git is reported in the result text.
|
|
153
214
|
let branch: string | undefined;
|
|
@@ -158,10 +219,10 @@ export default function worktreeExtension(pi: ExtensionAPI) {
|
|
|
158
219
|
} catch (error) {
|
|
159
220
|
branchNote = ` Its branch could not be read (${(error as Error).message.split("\n")[0]}).`;
|
|
160
221
|
}
|
|
161
|
-
const next: WorktreeState = { path: target, branch, createdByUs: false, originalCwd: ctx.cwd, sharedRoot
|
|
162
|
-
applyState(next);
|
|
222
|
+
const next: WorktreeState = { path: target, branch, createdByUs: false, originalCwd: ctx.cwd, sharedRoot };
|
|
223
|
+
applyState(next, toolCallId);
|
|
163
224
|
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}
|
|
225
|
+
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
226
|
details: { worktreeState: next } satisfies WorktreeDetails,
|
|
166
227
|
};
|
|
167
228
|
}
|
|
@@ -177,7 +238,13 @@ export default function worktreeExtension(pi: ExtensionAPI) {
|
|
|
177
238
|
const nameError = validateWorktreeName(name);
|
|
178
239
|
if (nameError) return fail(`Invalid worktree name "${name}": ${nameError}`);
|
|
179
240
|
|
|
180
|
-
|
|
241
|
+
// Hooks are disabled by the git wrapper; checkout filters must go through
|
|
242
|
+
// the normal shell permission gate rather than execute implicitly here.
|
|
243
|
+
const program = checkoutGitRunsProgram(repoRoot, homedir(), []);
|
|
244
|
+
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\`.`);
|
|
245
|
+
|
|
246
|
+
// `.onecode/worktrees` in independent mode (lib/config-mode.ts).
|
|
247
|
+
const worktreesDir = join(projectConfigDir(repoRoot), "worktrees");
|
|
181
248
|
mkdirSync(worktreesDir, { recursive: true });
|
|
182
249
|
// Self-ignoring directory: worktrees never show up as untracked files.
|
|
183
250
|
const ignorePath = join(worktreesDir, ".gitignore");
|
|
@@ -195,13 +262,13 @@ export default function worktreeExtension(pi: ExtensionAPI) {
|
|
|
195
262
|
return fail(`Could not create worktree: ${(error as Error).message}`);
|
|
196
263
|
}
|
|
197
264
|
|
|
198
|
-
const next: WorktreeState = { path, branch, baseCommit, createdByUs: true, originalCwd: ctx.cwd, sharedRoot
|
|
199
|
-
applyState(next);
|
|
265
|
+
const next: WorktreeState = { path, branch, baseCommit, createdByUs: true, originalCwd: ctx.cwd, sharedRoot };
|
|
266
|
+
applyState(next, toolCallId);
|
|
200
267
|
return {
|
|
201
268
|
content: [
|
|
202
269
|
{
|
|
203
270
|
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}
|
|
271
|
+
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
272
|
},
|
|
206
273
|
],
|
|
207
274
|
details: { worktreeState: next } satisfies WorktreeDetails,
|
|
@@ -213,11 +280,10 @@ export default function worktreeExtension(pi: ExtensionAPI) {
|
|
|
213
280
|
name: "exit_worktree",
|
|
214
281
|
label: "Exit Worktree",
|
|
215
282
|
...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.',
|
|
283
|
+
description: EXIT_WORKTREE_DESCRIPTION,
|
|
218
284
|
parameters: Type.Object({
|
|
219
|
-
action: StringEnum(["keep", "remove"] as const, { description:
|
|
220
|
-
discard_changes: Type.Optional(Type.Boolean({ description:
|
|
285
|
+
action: StringEnum(["keep", "remove"] as const, { description: EXIT_WORKTREE_PARAMS.action }),
|
|
286
|
+
discard_changes: Type.Optional(Type.Boolean({ description: EXIT_WORKTREE_PARAMS.discard_changes })),
|
|
221
287
|
}),
|
|
222
288
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
223
289
|
if (!state) {
|
|
@@ -234,7 +300,9 @@ export default function worktreeExtension(pi: ExtensionAPI) {
|
|
|
234
300
|
content: [
|
|
235
301
|
{
|
|
236
302
|
type: "text",
|
|
237
|
-
text: `Left worktree session; back in ${current.originalCwd}.
|
|
303
|
+
text: `Left worktree session; back in ${current.originalCwd}. ` + (existsSync(current.path)
|
|
304
|
+
? `The worktree remains at ${current.path}${current.branch ? ` (branch ${current.branch})` : ""} — re-enter it with enter_worktree (path: "${current.path}").`
|
|
305
|
+
: `The worktree at ${current.path} no longer exists. No branch was removed.`),
|
|
238
306
|
},
|
|
239
307
|
],
|
|
240
308
|
details: { worktreeState: null } satisfies WorktreeDetails,
|
|
@@ -254,11 +322,13 @@ export default function worktreeExtension(pi: ExtensionAPI) {
|
|
|
254
322
|
if (!params.discard_changes) {
|
|
255
323
|
const blockers: string[] = [];
|
|
256
324
|
try {
|
|
257
|
-
const dirty = await git(["status", "--porcelain"], current.path);
|
|
258
|
-
if (dirty) blockers.push(`Uncommitted
|
|
325
|
+
const dirty = await git(["status", "--porcelain", "--untracked-files=all"], current.path);
|
|
326
|
+
if (dirty) blockers.push(`Uncommitted files:\n${dirty}`);
|
|
259
327
|
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.`);
|
|
328
|
+
const ahead = await git(["rev-list", "--count", "HEAD", `refs/heads/${current.branch}`, `^${current.baseCommit}`], current.path);
|
|
329
|
+
if (ahead !== "0") blockers.push(`${ahead} commit(s) on HEAD or ${current.branch} not on the original branch.`);
|
|
330
|
+
} else {
|
|
331
|
+
blockers.push("Could not verify commits without the original branch and base commit.");
|
|
262
332
|
}
|
|
263
333
|
} catch {
|
|
264
334
|
blockers.push("Could not verify the worktree is clean.");
|
|
@@ -278,8 +348,7 @@ export default function worktreeExtension(pi: ExtensionAPI) {
|
|
|
278
348
|
}
|
|
279
349
|
|
|
280
350
|
try {
|
|
281
|
-
await git(["worktree", "remove", "--force", current.path], current.originalCwd);
|
|
282
|
-
if (current.branch) await git(["branch", "-D", current.branch], current.originalCwd);
|
|
351
|
+
await git(["worktree", "remove", ...(params.discard_changes ? ["--force"] : []), current.path], current.originalCwd);
|
|
283
352
|
} catch (error) {
|
|
284
353
|
return {
|
|
285
354
|
content: [{ type: "text", text: `Could not remove the worktree: ${(error as Error).message}` }],
|
|
@@ -288,6 +357,19 @@ export default function worktreeExtension(pi: ExtensionAPI) {
|
|
|
288
357
|
};
|
|
289
358
|
}
|
|
290
359
|
applyState(undefined);
|
|
360
|
+
if (current.branch) {
|
|
361
|
+
try {
|
|
362
|
+
// The commit check above (or discard consent) already cleared the branch;
|
|
363
|
+
// -d would also refuse a tip the original checkout's current HEAD lacks.
|
|
364
|
+
await git(["branch", "-D", current.branch], current.originalCwd);
|
|
365
|
+
} catch (error) {
|
|
366
|
+
return {
|
|
367
|
+
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.` }],
|
|
368
|
+
details: { worktreeState: null } satisfies WorktreeDetails,
|
|
369
|
+
isError: true,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
}
|
|
291
373
|
return {
|
|
292
374
|
content: [{ type: "text", text: `Removed worktree ${current.path}${current.branch ? ` and branch ${current.branch}` : ""}; back in ${current.originalCwd}.` }],
|
|
293
375
|
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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "one-code-extension",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "One Code as a pi package — the Claude Code experience on your own pi install: ported system prompt, Claude-Code-format permissions, .claude/ compatibility, ToolSearch-style deferred tools, and system-reminder steering. For the bundled app, install `@one-ai/one-code` instead.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi",
|
|
@@ -28,9 +28,11 @@
|
|
|
28
28
|
"extensions": [
|
|
29
29
|
"extensions/branding/index.ts",
|
|
30
30
|
"extensions/claude-compat/index.ts",
|
|
31
|
+
"extensions/kept-thinking/index.ts",
|
|
31
32
|
"extensions/context-budget/index.ts",
|
|
32
33
|
"extensions/system-reminder/index.ts",
|
|
33
34
|
"extensions/claude-context/index.ts",
|
|
35
|
+
"extensions/tool-call-corruption/index.ts",
|
|
34
36
|
"extensions/hooks/index.ts",
|
|
35
37
|
"extensions/tool-search/index.ts",
|
|
36
38
|
"extensions/search-tools/index.ts",
|
|
@@ -50,9 +52,11 @@
|
|
|
50
52
|
"extensions/effort/index.ts",
|
|
51
53
|
"extensions/spinner/index.ts",
|
|
52
54
|
"extensions/model-default/index.ts",
|
|
55
|
+
"extensions/newer-model/index.ts",
|
|
53
56
|
"extensions/footer/index.ts",
|
|
54
57
|
"extensions/turn-duration/index.ts",
|
|
55
58
|
"extensions/interrupted/index.ts",
|
|
59
|
+
"extensions/send-now/index.ts",
|
|
56
60
|
"extensions/recap/index.ts",
|
|
57
61
|
"extensions/btw/index.ts",
|
|
58
62
|
"extensions/session-title/index.ts",
|
|
@@ -62,6 +66,7 @@
|
|
|
62
66
|
"extensions/tasks/index.ts",
|
|
63
67
|
"extensions/plan-mode/index.ts",
|
|
64
68
|
"extensions/notebook/index.ts",
|
|
69
|
+
"extensions/artifacts/index.ts",
|
|
65
70
|
"extensions/web/index.ts",
|
|
66
71
|
"extensions/web-fetch/index.ts",
|
|
67
72
|
"extensions/lsp/index.ts",
|
|
@@ -69,6 +74,7 @@
|
|
|
69
74
|
"extensions/doctor/index.ts",
|
|
70
75
|
"extensions/context-management/index.ts",
|
|
71
76
|
"extensions/compaction/index.ts",
|
|
77
|
+
"extensions/idle-compact/index.ts",
|
|
72
78
|
"extensions/system-prompt/index.ts"
|
|
73
79
|
],
|
|
74
80
|
"themes": [
|
|
@@ -88,27 +94,33 @@
|
|
|
88
94
|
"scripts": {
|
|
89
95
|
"typecheck": "tsc --noEmit",
|
|
90
96
|
"test": "vitest run",
|
|
91
|
-
"gen:model-
|
|
97
|
+
"gen:model-catalog": "node scripts/gen-model-catalog.mjs",
|
|
98
|
+
"sync:app-readme": "node scripts/app-readme.mjs"
|
|
92
99
|
},
|
|
93
100
|
"dependencies": {
|
|
94
101
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
95
102
|
"@mozilla/readability": "^0.6.0",
|
|
96
103
|
"acorn": "^8.18.0",
|
|
104
|
+
"ignore": "^7.0.8",
|
|
97
105
|
"linkedom": "^0.18.13",
|
|
106
|
+
"marked": "^18.0.11",
|
|
98
107
|
"pi-web-search": "^1.5.0",
|
|
99
108
|
"turndown": "^7.2.4",
|
|
100
|
-
"
|
|
109
|
+
"web-tree-sitter": "^0.27.0",
|
|
110
|
+
"yaml": "^2.9.0"
|
|
101
111
|
},
|
|
102
112
|
"devDependencies": {
|
|
103
|
-
"@earendil-works/pi-agent-core": "^0.
|
|
104
|
-
"@earendil-works/pi-ai": "^0.
|
|
105
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
113
|
+
"@earendil-works/pi-agent-core": "^1.0.0",
|
|
114
|
+
"@earendil-works/pi-ai": "^1.0.0",
|
|
115
|
+
"@earendil-works/pi-coding-agent": "^1.0.0",
|
|
106
116
|
"@types/node": "^26.1.2",
|
|
107
117
|
"@types/turndown": "^5.0.6",
|
|
118
|
+
"typebox": "^1.3.7",
|
|
108
119
|
"typescript": "^5.6.0",
|
|
109
120
|
"vitest": "^2.1.0"
|
|
110
121
|
},
|
|
111
122
|
"peerDependencies": {
|
|
112
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
123
|
+
"@earendil-works/pi-coding-agent": ">=0.84.3",
|
|
124
|
+
"typebox": "*"
|
|
113
125
|
}
|
|
114
126
|
}
|