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
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* `~/.claude`: One Code used to write them there, and a stale value (a model the
|
|
23
23
|
* session cannot reach) must not keep biting. Every write goes to
|
|
24
24
|
* `~/.onecode/settings.json`; One Code never mutates Claude Code's files. See
|
|
25
|
-
* "Own state, borrowed config" in docs/decisions/memory-state.md.
|
|
25
|
+
* "Own state, borrowed config" in working-docs/decisions/memory-state.md.
|
|
26
26
|
*
|
|
27
27
|
* ## The customization surface (CC 2.1.233's own)
|
|
28
28
|
*
|
|
@@ -42,11 +42,12 @@
|
|
|
42
42
|
*
|
|
43
43
|
* (The rule lists were briefly retired between the ruleset adoption and CC
|
|
44
44
|
* 2.1.233 shipping `/auto-mode-setup`, which writes this exact schema — see
|
|
45
|
-
* docs/decisions/auto-mode.md.)
|
|
45
|
+
* working-docs/decisions/auto-mode.md.)
|
|
46
46
|
*/
|
|
47
47
|
|
|
48
48
|
import { existsSync, readFileSync } from "node:fs";
|
|
49
|
-
import { claudeUserSettingsPath } from "../lib/claude-settings.ts";
|
|
49
|
+
import { claudeUserSettingsPath, managedSettingsPaths } from "../lib/claude-settings.ts";
|
|
50
|
+
import { claudeSourcesOn } from "../lib/config-mode.ts";
|
|
50
51
|
import { oneCodeSettingsPath, readSettingsForWrite, writeSettings } from "../lib/one-code-settings.ts";
|
|
51
52
|
import { DEFAULT_ENVIRONMENT, slotName } from "./defaults.ts";
|
|
52
53
|
|
|
@@ -61,12 +62,12 @@ export interface AutoModeConfig {
|
|
|
61
62
|
allow: string[];
|
|
62
63
|
/** Suspend narrow Bash allow rules so every shell command reaches the classifier. */
|
|
63
64
|
classifyAllShell: boolean;
|
|
64
|
-
/** `provider/model-id`
|
|
65
|
+
/** `provider/model-id` the user chose for the classifier (`/auto-mode model`). */
|
|
65
66
|
classifierModel?: string;
|
|
66
67
|
/**
|
|
67
68
|
* Containment identity (`modelIdentity().containment`) the `classifierModel`
|
|
68
69
|
* was stamped for when set via `/auto-mode model`. A cross-provider setting
|
|
69
|
-
* whose stamp no longer matches the session is treated as stale and
|
|
70
|
+
* whose stamp no longer matches the session is treated as stale and replaced
|
|
70
71
|
* — see `classifierCandidates`. Undefined for a hand-edited setting.
|
|
71
72
|
*/
|
|
72
73
|
classifierModelSetFor?: string;
|
|
@@ -91,21 +92,16 @@ interface AutoModeSettingsFile {
|
|
|
91
92
|
|
|
92
93
|
const DEFAULTS_TOKEN = "$defaults";
|
|
93
94
|
|
|
94
|
-
/** Managed-settings locations, highest authority, matching Claude Code's paths. */
|
|
95
|
-
export function managedSettingsPaths(): string[] {
|
|
96
|
-
if (process.platform === "darwin") return ["/Library/Application Support/ClaudeCode/managed-settings.json"];
|
|
97
|
-
if (process.platform === "win32") return ["C:\\ProgramData\\ClaudeCode\\managed-settings.json"];
|
|
98
|
-
return ["/etc/claude-code/managed-settings.json"];
|
|
99
|
-
}
|
|
100
|
-
|
|
101
95
|
/**
|
|
102
96
|
* The files `autoMode` is read from, lowest precedence first. One Code's own
|
|
103
97
|
* auto-mode keys (`classifierModel`, `classifierModelSetFor`) are deliberately
|
|
104
|
-
* NOT collected from the borrowed `claudeUserSettingsPath`
|
|
105
|
-
*
|
|
106
|
-
* survive the move to `~/.onecode` (see the read guard in the loader).
|
|
98
|
+
* NOT collected from the borrowed `claudeUserSettingsPath` (see the read guard
|
|
99
|
+
* in the loader).
|
|
107
100
|
*/
|
|
108
101
|
export function autoModeSettingsPaths(home: string): string[] {
|
|
102
|
+
// Independent mode (lib/config-mode.ts) reads One Code's file alone; the
|
|
103
|
+
// built-in rules apply regardless, since settings only append to them.
|
|
104
|
+
if (!claudeSourcesOn()) return [oneCodeSettingsPath(home)];
|
|
109
105
|
return [claudeUserSettingsPath(home), oneCodeSettingsPath(home), ...managedSettingsPaths()];
|
|
110
106
|
}
|
|
111
107
|
|
|
@@ -285,7 +281,7 @@ export function loadAutoModeConfigWithDiagnostics(home: string): AutoModeConfigL
|
|
|
285
281
|
// `classifierModel` is One Code's own key, not Claude Code's. It is read from
|
|
286
282
|
// `~/.onecode` and managed settings only — never from `~/.claude`, where an
|
|
287
283
|
// old One Code build may have left a stale value. A leftover in `~/.claude` is
|
|
288
|
-
// surfaced as a diagnostic so the user knows why it
|
|
284
|
+
// surfaced as a diagnostic so the user knows why it does not apply.
|
|
289
285
|
if (typeof block.classifierModel === "string" && block.classifierModel.trim()) {
|
|
290
286
|
if (path === claudeUser) {
|
|
291
287
|
diagnostics.push(
|
|
@@ -331,9 +327,8 @@ export function loadAutoModeConfig(home: string): AutoModeConfig {
|
|
|
331
327
|
/**
|
|
332
328
|
* Persist `autoMode.classifierModel` in One Code's own settings file
|
|
333
329
|
* (`~/.onecode/settings.json`), preserving every other key. `undefined` removes
|
|
334
|
-
* the setting. Never touches Claude Code's files
|
|
335
|
-
* the knob that may move the classifier to another provider
|
|
336
|
-
* Code's own state (see the module comment).
|
|
330
|
+
* the setting. Never touches Claude Code's files: this is One Code's key, and
|
|
331
|
+
* the knob that may move the classifier to another provider.
|
|
337
332
|
*
|
|
338
333
|
* Unlike loading, this throws on a malformed file: a lenient read merely skips
|
|
339
334
|
* rules, but a lenient write would replace the whole settings file with only ours.
|
|
@@ -369,7 +364,7 @@ function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
|
369
364
|
* Persist a `/auto-mode setup` result to One Code's own settings file: replace
|
|
370
365
|
* the wizard-owned keys (environment + the three rule lists — an undefined list
|
|
371
366
|
* removes stale values, last-writer like CC's own wizard) while preserving every
|
|
372
|
-
* other settings key and
|
|
367
|
+
* other settings key and non-wizard autoMode keys.
|
|
373
368
|
* Never touches Claude Code's files; throws on a malformed file rather than
|
|
374
369
|
* replacing the settings with only ours.
|
|
375
370
|
*/
|
|
@@ -387,6 +382,63 @@ export function persistAutoModeSetup(patch: Record<string, string[] | undefined>
|
|
|
387
382
|
writeSettings(path, file);
|
|
388
383
|
}
|
|
389
384
|
|
|
385
|
+
/** The auto-mode keys the /permissions Auto mode tab lists and edits. */
|
|
386
|
+
export type AutoModeListKey = "allow" | "soft_deny" | "hard_deny" | "environment";
|
|
387
|
+
|
|
388
|
+
export interface SourcedAutoModeEntry {
|
|
389
|
+
key: AutoModeListKey;
|
|
390
|
+
text: string;
|
|
391
|
+
source: "claude-user" | "onecode-user" | "managed";
|
|
392
|
+
path: string;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Every auto-mode rule and environment entry with the file it came from, in
|
|
397
|
+
* load order. `"$defaults"` is left out: in the rule lists it means nothing
|
|
398
|
+
* here (the built-ins always apply), and in the environment it is the
|
|
399
|
+
* splice point, shown as the built-in default rather than as an entry.
|
|
400
|
+
*/
|
|
401
|
+
export function listAutoModeEntries(home: string): SourcedAutoModeEntry[] {
|
|
402
|
+
const claudeUser = claudeUserSettingsPath(home);
|
|
403
|
+
const oneCodeUser = oneCodeSettingsPath(home);
|
|
404
|
+
const entries: SourcedAutoModeEntry[] = [];
|
|
405
|
+
for (const path of autoModeSettingsPaths(home)) {
|
|
406
|
+
const block = asRecord(readFile(path, [])?.autoMode);
|
|
407
|
+
if (!block) continue;
|
|
408
|
+
const source = path === claudeUser ? "claude-user" : path === oneCodeUser ? "onecode-user" : "managed";
|
|
409
|
+
for (const key of ["allow", "soft_deny", "hard_deny", "environment"] as const) {
|
|
410
|
+
for (const text of stringArray(block[key]) ?? []) {
|
|
411
|
+
if (text.trim() !== DEFAULTS_TOKEN) entries.push({ key, text, source, path });
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return entries;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** Whether One Code's own `autoMode.environment` keeps the built-in default through `"$defaults"`. */
|
|
419
|
+
export function oneCodeEnvironmentExtendsDefault(home: string): boolean {
|
|
420
|
+
const block = asRecord(readFile(oneCodeSettingsPath(home), [])?.autoMode);
|
|
421
|
+
return (stringArray(block?.environment) ?? []).some((text) => text.trim() === DEFAULTS_TOKEN);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Rewrite one auto-mode list in One Code's own settings file. An emptied list
|
|
426
|
+
* is removed (for the environment, that restores the built-in default), and so
|
|
427
|
+
* is an emptied `autoMode` block. Throws on a malformed file, like the other
|
|
428
|
+
* writers.
|
|
429
|
+
*/
|
|
430
|
+
export function updateOneCodeAutoModeList(key: AutoModeListKey, update: (entries: string[]) => string[], home: string): void {
|
|
431
|
+
const path = oneCodeSettingsPath(home);
|
|
432
|
+
const file = readSettingsForWrite(path);
|
|
433
|
+
const block = asRecord(file.autoMode) ?? {};
|
|
434
|
+
const next = update(stringArray(block[key]) ?? []);
|
|
435
|
+
if (next.length === 0) delete block[key];
|
|
436
|
+
else block[key] = next;
|
|
437
|
+
if (Object.keys(block).length === 0) delete file.autoMode;
|
|
438
|
+
else file.autoMode = block;
|
|
439
|
+
writeSettings(path, file);
|
|
440
|
+
}
|
|
441
|
+
|
|
390
442
|
/**
|
|
391
443
|
* The `permissions.allow` entries in One Code's own settings file — what the
|
|
392
444
|
* setup audit can actually remove. Broad rules that live in Claude Code's files
|
|
@@ -38,6 +38,8 @@ export interface DecisionEntry {
|
|
|
38
38
|
raw?: string;
|
|
39
39
|
/** `provider/id` of the model that produced a classifier verdict. */
|
|
40
40
|
model?: string;
|
|
41
|
+
/** The provider rejected the full classifier transcript as too long, not unsafe. */
|
|
42
|
+
transcriptTooLong?: true;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
const SUBJECT_LIMIT = 300;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The ruleset itself is no longer configurable prose lists — the classifier
|
|
5
5
|
* ruleset is a fixed monolith matching Claude Code's classifier (see
|
|
6
|
-
* classifier-prompt.ts and docs/decisions/auto-mode.md). The one customization
|
|
6
|
+
* classifier-prompt.ts and working-docs/decisions/auto-mode.md). The one customization
|
|
7
7
|
* surface Claude Code exposes, and the only one we expose, is the `## Environment` section:
|
|
8
8
|
* `DEFAULT_ENVIRONMENT` is CC's default slot text, and a user's `autoMode.environment`
|
|
9
9
|
* entries replace it. Re-exported here so config.ts has a single stable import
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calls the auto-mode classifier denied this session, and the one-shot grants
|
|
3
|
+
* the user mints by approving them in `/permissions` (pure).
|
|
4
|
+
*
|
|
5
|
+
* Claude Code records every classifier denial (newest first, at most 20) for
|
|
6
|
+
* its "Recently denied" tab, keyed by tool and exact input. Approving one there
|
|
7
|
+
* writes no rule and bypasses nothing: the model is told it may retry, and the
|
|
8
|
+
* retry goes through the classifier again (findings §33). One Code's classifier
|
|
9
|
+
* takes user intent only from the user's own typed input, so a retry would
|
|
10
|
+
* most likely be blocked again. Here an approval instead mints a grant for that
|
|
11
|
+
* exact tool and input: the next call that matches it skips the classifier
|
|
12
|
+
* once, then the grant is spent. It carries the authority of answering "Yes"
|
|
13
|
+
* once on a permission prompt, and no more: a changed command, another
|
|
14
|
+
* directory or a second run is judged as usual, and deny rules and the safety
|
|
15
|
+
* floor are checked before any grant (working-docs/decisions/auto-mode.md,
|
|
16
|
+
* "Approving a denied call").
|
|
17
|
+
*
|
|
18
|
+
* The store lives in the permissions extension's process state for one
|
|
19
|
+
* session, which a /reload keeps. It is never persisted, and a new session
|
|
20
|
+
* starts empty.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** Claude Code's `MAX_DENIALS`. */
|
|
24
|
+
export const MAX_DENIALS = 20;
|
|
25
|
+
|
|
26
|
+
export interface AutoModeDenial {
|
|
27
|
+
/** Unique within the store, so a panel row outlives list changes. */
|
|
28
|
+
id: number;
|
|
29
|
+
/** The normalized tool name (`bash`, `write`, `mcp__server__tool`). */
|
|
30
|
+
toolName: string;
|
|
31
|
+
/** What the user sees for the call: `bash(rm -rf dist)`, its subject clipped at 200 characters for the panel. */
|
|
32
|
+
display: string;
|
|
33
|
+
/** The same call unclipped, for the text the model reads (the grant message, the breadcrumb). */
|
|
34
|
+
fullDisplay: string;
|
|
35
|
+
/** The exact call (`denialInputKey`). */
|
|
36
|
+
inputKey: string;
|
|
37
|
+
/** The classifier's reason (a grounded rule name, or the failure it explained). */
|
|
38
|
+
reason: string;
|
|
39
|
+
/** The grounded rule name, when the verdict cited one. */
|
|
40
|
+
rule?: string;
|
|
41
|
+
timestamp: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** JSON with object keys sorted at every level, so two spellings of one input agree. */
|
|
45
|
+
function stableJson(value: unknown): string {
|
|
46
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
47
|
+
if (value && typeof value === "object") {
|
|
48
|
+
const entries = Object.entries(value as Record<string, unknown>)
|
|
49
|
+
.filter(([, v]) => v !== undefined)
|
|
50
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
51
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${stableJson(v)}`).join(",")}}`;
|
|
52
|
+
}
|
|
53
|
+
return JSON.stringify(value) ?? "null";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The key a denial and its grant are matched by: the tool, the directory the
|
|
58
|
+
* call runs in, and its whole input. For a shell tool only the command counts,
|
|
59
|
+
* as in Claude Code (its `description` and `timeout` do not change what runs);
|
|
60
|
+
* for every other tool the whole input does. The directory is part of the key
|
|
61
|
+
* because the same command in another directory is another action.
|
|
62
|
+
*/
|
|
63
|
+
export function denialInputKey(toolName: string, input: Record<string, unknown>, cwd: string, shell: boolean): string {
|
|
64
|
+
const counted = shell ? { command: input.command } : input;
|
|
65
|
+
return `${toolName}\0${cwd}\0${stableJson(counted)}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class DenialStore {
|
|
69
|
+
private denials: AutoModeDenial[] = [];
|
|
70
|
+
/** Unspent grants per exact call: two approved denials of one call allow two retries. */
|
|
71
|
+
private readonly grants = new Map<string, number>();
|
|
72
|
+
private nextId = 1;
|
|
73
|
+
|
|
74
|
+
/** Record a classifier denial, newest first, keeping the last `MAX_DENIALS`. */
|
|
75
|
+
record(denial: Omit<AutoModeDenial, "id">): AutoModeDenial {
|
|
76
|
+
const entry = { ...denial, id: this.nextId++ };
|
|
77
|
+
this.denials = [entry, ...this.denials].slice(0, MAX_DENIALS);
|
|
78
|
+
return entry;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Newest first. */
|
|
82
|
+
list(): readonly AutoModeDenial[] {
|
|
83
|
+
return this.denials;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Approve denials by id: each leaves the list and mints a grant for its exact
|
|
88
|
+
* call. Returns the approved entries, in list order; unknown ids are skipped.
|
|
89
|
+
*/
|
|
90
|
+
approve(ids: ReadonlySet<number>): AutoModeDenial[] {
|
|
91
|
+
const approved = this.denials.filter((denial) => ids.has(denial.id));
|
|
92
|
+
for (const denial of approved) this.grants.set(denial.inputKey, (this.grants.get(denial.inputKey) ?? 0) + 1);
|
|
93
|
+
this.denials = this.denials.filter((denial) => !ids.has(denial.id));
|
|
94
|
+
return approved;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Spend one grant for this exact call. True once per approval. */
|
|
98
|
+
takeGrant(inputKey: string): boolean {
|
|
99
|
+
const left = this.grants.get(inputKey);
|
|
100
|
+
if (!left) return false;
|
|
101
|
+
if (left === 1) this.grants.delete(inputKey);
|
|
102
|
+
else this.grants.set(inputKey, left - 1);
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** A call that went through another way drops its earlier denials (Claude Code does the same). */
|
|
107
|
+
settle(inputKey: string): void {
|
|
108
|
+
this.denials = this.denials.filter((denial) => denial.inputKey !== inputKey);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** A new session starts with no denials and no grants. */
|
|
112
|
+
reset(): void {
|
|
113
|
+
this.denials = [];
|
|
114
|
+
this.grants.clear();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The message the model gets when the user approves denials in `/permissions`:
|
|
120
|
+
* Claude Code's wording, verbatim (findings §33).
|
|
121
|
+
*/
|
|
122
|
+
export function permissionGrantedMessage(displays: readonly string[]): string {
|
|
123
|
+
return `Permission granted for: ${displays.join(", ")}. You may now retry ${displays.length === 1 ? "this command" : "these commands"} if you would like.`;
|
|
124
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a checkout's own git configuration makes a read-only git command run
|
|
3
|
+
* a program (pure apart from fs reads).
|
|
4
|
+
*
|
|
5
|
+
* The pre-gate fast-paths `git status`, `diff`, `log` and the other read-only
|
|
6
|
+
* subcommands by their options (read-only-options.ts). git honours the
|
|
7
|
+
* repository's `.git/config`, and some keys there name a program that those
|
|
8
|
+
* reads run: `core.fsmonitor` on every index read, a `filter.<driver>.clean`
|
|
9
|
+
* when `status` re-hashes a file, `diff.external` and the diff and textconv
|
|
10
|
+
* drivers on `diff`/`log -p`/`show`, `gpg.program` on `log --show-signature`
|
|
11
|
+
* or a `%G?` format, a pager. `status` also rewrites the index opportunistically,
|
|
12
|
+
* which runs a `post-index-change` hook. A directory whose `.git` came from an
|
|
13
|
+
* archive, a synced folder or a copied tree can carry any of these, so a git
|
|
14
|
+
* read there is not provably read-only and goes to the classifier (PR #8
|
|
15
|
+
* review; SECURITY-REVIEW-2026-09-23's folder-trust residual).
|
|
16
|
+
*
|
|
17
|
+
* Only repository-scoped files are read: the checkout's `config`, its
|
|
18
|
+
* `config.worktree`, every submodule's config, and the hooks
|
|
19
|
+
* directories (`core.hooksPath` included). The user's own global and system
|
|
20
|
+
* configuration is theirs, and git-lfs's standard filter commands pass. The
|
|
21
|
+
* scan is textual and wide on purpose: an unneeded escalation costs one
|
|
22
|
+
* classifier call, a missed key would be code execution behind a "safe", so
|
|
23
|
+
* anything it cannot read counts as a program too.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { type Dirent, existsSync, readdirSync, readFileSync } from "node:fs";
|
|
27
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
28
|
+
import { findGitRoot, gitdirFileTarget } from "../lib/git.ts";
|
|
29
|
+
import { expandTilde } from "../lib/paths.ts";
|
|
30
|
+
|
|
31
|
+
const BOOLEAN = /^(true|false|yes|no|on|off|1|0)?$/i;
|
|
32
|
+
|
|
33
|
+
/** git-lfs's own filter commands (`git lfs install`), resolved on the user's PATH. */
|
|
34
|
+
const LFS_FILTERS = new Set(["git-lfs clean -- %f", "git-lfs smudge -- %f", "git-lfs filter-process"]);
|
|
35
|
+
|
|
36
|
+
/** Keys whose value git runs during a read, by section (subsections included). */
|
|
37
|
+
const PROGRAM_KEYS: Record<string, (key: string, value: string | undefined) => boolean> = {
|
|
38
|
+
// `core.fsmonitor=true` is git's own daemon; any other value is a hook command.
|
|
39
|
+
core: (key, value) => (key === "fsmonitor" && !BOOLEAN.test(value ?? "")) || key === "pager",
|
|
40
|
+
diff: (key) => key === "external" || key === "command" || key === "textconv",
|
|
41
|
+
filter: (key, value) => (key === "clean" || key === "smudge" || key === "process") && !LFS_FILTERS.has(value ?? ""),
|
|
42
|
+
pager: (_key, value) => !BOOLEAN.test(value ?? ""),
|
|
43
|
+
gpg: (key) => key === "program",
|
|
44
|
+
// An included file is not followed; it could hold any of the above.
|
|
45
|
+
include: (key) => key === "path",
|
|
46
|
+
includeif: (key) => key === "path",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** One git config file's entries, section names lowercased without their subsection. */
|
|
50
|
+
function configEntries(text: string): { section: string; key: string; value: string | undefined }[] {
|
|
51
|
+
const entries: { section: string; key: string; value: string | undefined }[] = [];
|
|
52
|
+
let section = "";
|
|
53
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
54
|
+
let line = raw.trim();
|
|
55
|
+
const header = /^\[\s*([A-Za-z0-9.-]+)(?:\s+"(?:[^"\\]|\\.)*")?\s*\]/.exec(line);
|
|
56
|
+
if (header) {
|
|
57
|
+
// `[diff "x"]` and the old `[diff.x]` spelling both name section `diff`.
|
|
58
|
+
section = header[1].split(".")[0].toLowerCase();
|
|
59
|
+
line = line.slice(header[0].length).trim();
|
|
60
|
+
}
|
|
61
|
+
if (!line || line.startsWith("#") || line.startsWith(";")) continue;
|
|
62
|
+
const entry = /^([A-Za-z][A-Za-z0-9-]*)\s*(?:=\s*(.*))?$/.exec(line);
|
|
63
|
+
if (entry) entries.push({ section, key: entry[1].toLowerCase(), value: entry[2]?.trim().replace(/^"(.*)"$/, "$1") });
|
|
64
|
+
}
|
|
65
|
+
return entries;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* What one git config file's text names: the first program-valued key, as
|
|
70
|
+
* `section.key`, and every `core.hooksPath`. A hooks path is not a program by
|
|
71
|
+
* itself (husky sets one); the caller looks there for the hook a read runs.
|
|
72
|
+
*/
|
|
73
|
+
export function configRunsProgram(text: string): { program?: string; hooksPaths: string[] } {
|
|
74
|
+
const hooksPaths: string[] = [];
|
|
75
|
+
for (const { section, key, value } of configEntries(text)) {
|
|
76
|
+
if (section === "core" && key === "hookspath" && value) hooksPaths.push(value);
|
|
77
|
+
else if (PROGRAM_KEYS[section]?.(key, value)) return { program: `${section}.${key}`, hooksPaths };
|
|
78
|
+
}
|
|
79
|
+
return { hooksPaths };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The checkout holding `dir`: its worktree root, git dir and common dir, or undefined outside one. */
|
|
83
|
+
function checkoutOf(dir: string): { root: string; gitDir: string; commonDir: string } | undefined {
|
|
84
|
+
const root = findGitRoot(dir);
|
|
85
|
+
if (!root) return undefined;
|
|
86
|
+
const gitDir = gitdirFileTarget(root) ?? join(root, ".git");
|
|
87
|
+
let commonDir = gitDir;
|
|
88
|
+
try {
|
|
89
|
+
const common = readFileSync(join(gitDir, "commondir"), "utf-8").trim();
|
|
90
|
+
if (common) commonDir = isAbsolute(common) ? common : resolve(gitDir, common);
|
|
91
|
+
} catch {
|
|
92
|
+
// Not a linked worktree: the git dir is the common dir.
|
|
93
|
+
}
|
|
94
|
+
return { root, gitDir, commonDir };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** A missing file or directory, as opposed to one this process cannot read. */
|
|
98
|
+
function isMissing(error: unknown): boolean {
|
|
99
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
100
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Every submodule git dir `status` can recurse into, or undefined when the
|
|
105
|
+
* search cannot finish (an unreadable directory, more than 200 repositories,
|
|
106
|
+
* nesting past 32 levels), which the caller treats as a program. Found two
|
|
107
|
+
* ways: the standard layout under `<commonDir>/modules` (a nested submodule's
|
|
108
|
+
* under its parent's own `modules/`; a slashed submodule name is a directory
|
|
109
|
+
* chain), and each `.gitmodules` path whose `.git` points somewhere else.
|
|
110
|
+
*/
|
|
111
|
+
function submoduleGitDirs(root: string, commonDir: string): string[] | undefined {
|
|
112
|
+
const found = new Set<string>();
|
|
113
|
+
const walk = (dir: string, depth: number): boolean => {
|
|
114
|
+
if (depth > 32) return false;
|
|
115
|
+
let entries: Dirent[];
|
|
116
|
+
try {
|
|
117
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
118
|
+
} catch (error) {
|
|
119
|
+
return isMissing(error);
|
|
120
|
+
}
|
|
121
|
+
if (entries.some((entry) => entry.name === "HEAD" || entry.name === "config")) {
|
|
122
|
+
found.add(dir);
|
|
123
|
+
return found.size <= 200 && walk(join(dir, "modules"), depth + 1);
|
|
124
|
+
}
|
|
125
|
+
return entries.every((entry) => !entry.isDirectory() || walk(join(dir, entry.name), depth + 1));
|
|
126
|
+
};
|
|
127
|
+
if (!walk(join(commonDir, "modules"), 0)) return undefined;
|
|
128
|
+
let gitmodules = "";
|
|
129
|
+
try {
|
|
130
|
+
gitmodules = readFileSync(join(root, ".gitmodules"), "utf-8");
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (!isMissing(error)) return undefined;
|
|
133
|
+
}
|
|
134
|
+
for (const { section, key, value } of configEntries(gitmodules)) {
|
|
135
|
+
if (section !== "submodule" || key !== "path" || !value) continue;
|
|
136
|
+
const worktree = resolve(root, value);
|
|
137
|
+
const dotGit = join(worktree, ".git");
|
|
138
|
+
const gitDir = gitdirFileTarget(worktree) ?? (existsSync(dotGit) ? dotGit : undefined);
|
|
139
|
+
if (gitDir) found.add(gitDir);
|
|
140
|
+
if (found.size > 200) return undefined;
|
|
141
|
+
}
|
|
142
|
+
return [...found];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The hooks a git read can run: `status` rewrites the index. */
|
|
146
|
+
export const READ_HOOKS = ["post-index-change"] as const;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Why a git command in `dir` could run a program the checkout names, or
|
|
150
|
+
* undefined when its repository-scoped configuration names none (or `dir` is
|
|
151
|
+
* in no checkout). `hooks` are the hook names the command can run; `home`
|
|
152
|
+
* expands a `~` hooks path.
|
|
153
|
+
*/
|
|
154
|
+
export function checkoutGitRunsProgram(dir: string, home: string, hooks: readonly string[] = READ_HOOKS): string | undefined {
|
|
155
|
+
const checkout = checkoutOf(dir);
|
|
156
|
+
if (!checkout) return undefined;
|
|
157
|
+
const { root, gitDir, commonDir } = checkout;
|
|
158
|
+
const modules = submoduleGitDirs(root, commonDir);
|
|
159
|
+
if (!modules) return "the checkout's submodules cannot all be read";
|
|
160
|
+
// `status` rewrites the index, which runs `post-index-change`; it recurses
|
|
161
|
+
// into submodules, each with its own hooks directory.
|
|
162
|
+
const hooksDirs = [join(commonDir, "hooks"), ...modules.map((module) => join(module, "hooks"))];
|
|
163
|
+
for (const file of new Set([join(commonDir, "config"), join(commonDir, "config.worktree"), join(gitDir, "config.worktree")])) {
|
|
164
|
+
const found = readConfig(file);
|
|
165
|
+
if (!found) return "the checkout's git config cannot be read";
|
|
166
|
+
if (found.program) return `the checkout's git config sets ${found.program}, which names a program git runs`;
|
|
167
|
+
for (const path of found.hooksPaths) hooksDirs.push(resolve(root, expandTilde(path, home)));
|
|
168
|
+
}
|
|
169
|
+
for (const module of modules) {
|
|
170
|
+
const found = readConfig(join(module, "config"));
|
|
171
|
+
// A submodule's relative hooks path is resolved against a worktree this check does not map.
|
|
172
|
+
if (!found || found.program || found.hooksPaths.length > 0) {
|
|
173
|
+
return `a submodule's git config ${found ? `sets ${found.program ?? "core.hookspath"}, which names a program git runs` : "cannot be read"}`;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const hook = hooks.find((name) => hooksDirs.some((dir) => existsSync(join(dir, name))));
|
|
177
|
+
return hook && `the checkout has a ${hook} hook, which git runs`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** What a config file names; empty when it does not exist, undefined when it cannot be read. */
|
|
181
|
+
function readConfig(file: string): { program?: string; hooksPaths: string[] } | undefined {
|
|
182
|
+
try {
|
|
183
|
+
return configRunsProgram(readFileSync(file, "utf-8"));
|
|
184
|
+
} catch (error) {
|
|
185
|
+
return isMissing(error) ? { hooksPaths: [] } : undefined;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The auto-mode transcript's `{"meta":{"gitStatus":…}}` line (pure), in the
|
|
3
|
+
* Claude Code-compatible shape the ruleset describes.
|
|
4
|
+
*
|
|
5
|
+
* Before a shell command that can destroy uncommitted work, the harness runs
|
|
6
|
+
* `git status` itself and puts the result directly above that command in the
|
|
7
|
+
* classifier's `<transcript>`, so the classifier judges the tree's real state
|
|
8
|
+
* instead of presuming it dirty. The ruleset says how to read it ("A
|
|
9
|
+
* `gitStatus` of `{"clean":true}` clears the Irreversible Local Destruction
|
|
10
|
+
* presume-dirty for that command; staged/modified/untracked counts … confirm
|
|
11
|
+
* it"). A command qualifies when any pattern of a qualifying category
|
|
12
|
+
* (`GIT_STATUS_CATEGORIES`) matches it, `monitor` commands included.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
type Pattern = { pattern: RegExp; category: string };
|
|
16
|
+
|
|
17
|
+
/** Bash command categories; `destructiveCategory` names a command by the first match. */
|
|
18
|
+
const BASH_PATTERNS: Pattern[] = [
|
|
19
|
+
{ pattern: /\bgit\s+reset\s+--hard\b/, category: "git_reset_hard" },
|
|
20
|
+
{ pattern: /\bgit\s+push\b[^;&|\n]*[ \t](--force|--force-with-lease|-f)\b/, category: "git_force_push" },
|
|
21
|
+
{ pattern: /\bgit\s+clean\b(?![^;&|\n]*(?:-[a-zA-Z]*n|--dry-run))[^;&|\n]*-[a-zA-Z]*f/, category: "git_clean_force" },
|
|
22
|
+
{ pattern: /\bgit\s+checkout\s+(--\s+)?\.[ \t]*($|[;&|\n])/, category: "git_checkout_dot" },
|
|
23
|
+
{ pattern: /\bgit\s+restore\s+(--\s+)?\.[ \t]*($|[;&|\n])/, category: "git_restore_dot" },
|
|
24
|
+
{ pattern: /\bgit\s+stash[ \t]+(drop|clear)\b/, category: "git_stash_drop" },
|
|
25
|
+
{ pattern: /\bgit\s+branch\s+(-D[ \t]|--delete\s+--force|--force\s+--delete)\b/, category: "git_branch_force_delete" },
|
|
26
|
+
{ pattern: /\bgit\s+(commit|push|merge)\b[^;&|\n]*--no-verify\b/, category: "git_no_verify" },
|
|
27
|
+
{ pattern: /\bgit\s+commit\b[^;&|\n]*--amend\b/, category: "git_commit_amend" },
|
|
28
|
+
{
|
|
29
|
+
pattern: /(^|[;&|\n][ \t]*)rm\s+-[a-zA-Z]*[rR][a-zA-Z]*f|(^|[;&|\n][ \t]*)rm\s+-[a-zA-Z]*f[a-zA-Z]*[rR]/,
|
|
30
|
+
category: "rm_recursive_force",
|
|
31
|
+
},
|
|
32
|
+
{ pattern: /(^|[;&|\n][ \t]*)rm\s+-[a-zA-Z]*[rR]/, category: "rm_recursive" },
|
|
33
|
+
{ pattern: /(^|[;&|\n][ \t]*)rm\s+-[a-zA-Z]*f/, category: "rm_force" },
|
|
34
|
+
{ pattern: /\b(DROP|TRUNCATE)\s+(TABLE|DATABASE|SCHEMA)\b/i, category: "sql_drop_truncate" },
|
|
35
|
+
{ pattern: /\bDELETE\s+FROM\s+\w+[ \t]*(;|"|'|\n|$)/i, category: "sql_delete_from" },
|
|
36
|
+
{ pattern: /\bkubectl\s+delete\b/, category: "kubectl_delete" },
|
|
37
|
+
{ pattern: /\bterraform\s+destroy\b/, category: "terraform_destroy" },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const REMOVE = "(?:^|[|;&\\n({])[ \\t]*(Remove-Item|rm|del|rd|rmdir|ri)\\b[^|;&\\n}]*";
|
|
41
|
+
|
|
42
|
+
/** PowerShell command categories, in the same first-match order. */
|
|
43
|
+
const POWERSHELL_PATTERNS: Pattern[] = [
|
|
44
|
+
{ pattern: new RegExp(`${REMOVE}-Recurse\\b[^|;&\\n}]*-Force\\b`, "i"), category: "remove_item_recursive_force" },
|
|
45
|
+
{ pattern: new RegExp(`${REMOVE}-Force\\b[^|;&\\n}]*-Recurse\\b`, "i"), category: "remove_item_recursive_force" },
|
|
46
|
+
{ pattern: new RegExp(`${REMOVE}-Recurse\\b`, "i"), category: "remove_item_recursive" },
|
|
47
|
+
{ pattern: new RegExp(`${REMOVE}-Force\\b`, "i"), category: "remove_item_force" },
|
|
48
|
+
{ pattern: /\bClear-Content\b[^|;&\n]*\*/i, category: "clear_content_glob" },
|
|
49
|
+
{ pattern: /\bFormat-Volume\b/i, category: "format_volume" },
|
|
50
|
+
{ pattern: /\bClear-Disk\b/i, category: "clear_disk" },
|
|
51
|
+
{ pattern: /\bgit\s+reset\s+--hard\b/i, category: "git_reset_hard" },
|
|
52
|
+
{ pattern: /\bgit\s+push\b[^|;&\n]*\s+(--force|--force-with-lease|-f)\b/i, category: "git_force_push" },
|
|
53
|
+
{ pattern: /\bgit\s+clean\b(?![^|;&\n]*(?:-[a-zA-Z]*n|--dry-run))[^|;&\n]*-[a-zA-Z]*f/i, category: "git_clean_force" },
|
|
54
|
+
{ pattern: /\bgit\s+stash\s+(drop|clear)\b/i, category: "git_stash_drop" },
|
|
55
|
+
{ pattern: /\b(DROP|TRUNCATE)\s+(TABLE|DATABASE|SCHEMA)\b/i, category: "sql_drop_truncate" },
|
|
56
|
+
{ pattern: /\bStop-Computer\b/i, category: "stop_computer" },
|
|
57
|
+
{ pattern: /\bRestart-Computer\b/i, category: "restart_computer" },
|
|
58
|
+
{ pattern: /\bClear-RecycleBin\b/i, category: "clear_recycle_bin" },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
/** The categories that get a gitStatus line: the ones that can destroy uncommitted work. */
|
|
62
|
+
export const GIT_STATUS_CATEGORIES: ReadonlySet<string> = new Set([
|
|
63
|
+
"git_reset_hard",
|
|
64
|
+
"git_checkout_dot",
|
|
65
|
+
"git_restore_dot",
|
|
66
|
+
"git_clean_force",
|
|
67
|
+
"rm_recursive_force",
|
|
68
|
+
"rm_recursive",
|
|
69
|
+
"rm_force",
|
|
70
|
+
"remove_item_recursive_force",
|
|
71
|
+
"remove_item_recursive",
|
|
72
|
+
"remove_item_force",
|
|
73
|
+
"clear_content_glob",
|
|
74
|
+
]);
|
|
75
|
+
|
|
76
|
+
/** The destructive category of a shell command (first match, first 10,000 characters), or undefined. */
|
|
77
|
+
export function destructiveCategory(tool: "bash" | "powershell", command: string): string | undefined {
|
|
78
|
+
const text = command.length > 10_000 ? command.slice(0, 10_000) : command;
|
|
79
|
+
const patterns = tool === "powershell" ? POWERSHELL_PATTERNS : BASH_PATTERNS;
|
|
80
|
+
return patterns.find(({ pattern }) => pattern.test(text))?.category;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The categories of every pattern this command matches (first 10,000 characters). */
|
|
84
|
+
function matchedCategories(tool: "bash" | "powershell", command: string): { text: string; categories: string[] } {
|
|
85
|
+
const text = command.length > 10_000 ? command.slice(0, 10_000) : command;
|
|
86
|
+
const patterns = tool === "powershell" ? POWERSHELL_PATTERNS : BASH_PATTERNS;
|
|
87
|
+
return { text, categories: patterns.filter(({ pattern }) => pattern.test(text)).map(({ category }) => category) };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Whether this shell command gets a gitStatus line above it in the transcript.
|
|
92
|
+
* Any qualifying pattern counts, not only the first match, so `git push
|
|
93
|
+
* --force x && rm -rf y` gets the line for its `rm -rf`: the line only ever
|
|
94
|
+
* adds ground truth.
|
|
95
|
+
*/
|
|
96
|
+
export function wantsGitStatusMeta(tool: "bash" | "powershell", command: string): boolean {
|
|
97
|
+
return matchedCategories(tool, command).categories.some((category) => GIT_STATUS_CATEGORIES.has(category));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** `git clean` deletes ignored files only with `-x`/`-X`. */
|
|
101
|
+
const GIT_CLEAN_IGNORED = /\bgit\s+clean\b[^;&|\n]*[ \t]-[a-zA-Z]*[xX]/i;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Whether this shell command can delete ignored files (`.env`, local builds),
|
|
105
|
+
* which `git status` hides and git cannot restore. Every non-git removal
|
|
106
|
+
* category can; of the git ones, only a `git clean -x`/`-X`.
|
|
107
|
+
*/
|
|
108
|
+
export function reachesIgnoredFiles(tool: "bash" | "powershell", command: string): boolean {
|
|
109
|
+
const { text, categories } = matchedCategories(tool, command);
|
|
110
|
+
return categories.some((category) =>
|
|
111
|
+
category === "git_clean_force" ? GIT_CLEAN_IGNORED.test(text) : GIT_STATUS_CATEGORIES.has(category) && !category.startsWith("git_"),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The `git status` arguments for the line (the harness adds its own `-c`
|
|
117
|
+
* hardening), wider than Claude Code's `--ignore-submodules=dirty` (PR #8
|
|
118
|
+
* review). A dirty submodule always counts as modified: `rm -rf` deletes its
|
|
119
|
+
* work, and `git reset --hard --recurse-submodules` (or `submodule.recurse`,
|
|
120
|
+
* which the command line does not show) discards it. Ignored entries (`!!`,
|
|
121
|
+
* one per ignored directory) are listed only for a command that deletes them
|
|
122
|
+
* (`reachesIgnoredFiles`), so the line can withhold `{"clean":true}` there
|
|
123
|
+
* and keep it for a `git reset --hard` in a repo with an ignored `.env`.
|
|
124
|
+
*/
|
|
125
|
+
export function gitStatusMetaArgs(reachesIgnored: boolean): readonly string[] {
|
|
126
|
+
const args = ["status", "--porcelain", "--ignore-submodules=none", "--untracked-files=normal"];
|
|
127
|
+
return reachesIgnored ? [...args, "--ignored=matching"] : args;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The line's gitStatus value: `{clean: true}`, or the tree's counts. */
|
|
131
|
+
export type GitStatusMeta = { clean: true } | { staged: number; modified: number; untracked: number };
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Summarize `git status --porcelain` output as clean, or staged/modified/untracked
|
|
135
|
+
* counts. Undefined (no line: the classifier keeps presuming dirty) when only
|
|
136
|
+
* ignored entries remain, which the wider `gitStatusMetaArgs` list.
|
|
137
|
+
*/
|
|
138
|
+
export function gitStatusMeta(porcelain: string): GitStatusMeta | undefined {
|
|
139
|
+
let staged = 0;
|
|
140
|
+
let modified = 0;
|
|
141
|
+
let untracked = 0;
|
|
142
|
+
let ignored = 0;
|
|
143
|
+
for (const line of porcelain.split("\n")) {
|
|
144
|
+
if (line.length < 2) continue;
|
|
145
|
+
const [index, worktree] = [line[0], line[1]];
|
|
146
|
+
if (index === "?" && worktree === "?") {
|
|
147
|
+
untracked++;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (index === "!" && worktree === "!") {
|
|
151
|
+
ignored++;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (index !== " " && index !== "?") staged++;
|
|
155
|
+
if (worktree !== " ") modified++;
|
|
156
|
+
}
|
|
157
|
+
if (staged !== 0 || modified !== 0 || untracked !== 0) return { staged, modified, untracked };
|
|
158
|
+
return ignored > 0 ? undefined : { clean: true };
|
|
159
|
+
}
|