gsd-pi 2.77.0-dev.eaa4973bc → 2.78.0-dev.aeeb2ca00
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 +53 -17
- package/dist/claude-cli-check.js +46 -10
- package/dist/headless.js +49 -4
- package/dist/resource-loader.d.ts +40 -0
- package/dist/resource-loader.js +32 -13
- package/dist/resources/extensions/browser-tools/capture.js +9 -0
- package/dist/resources/extensions/browser-tools/tests/browser-tools-integration.test.mjs +8 -59
- package/dist/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +36 -24
- package/dist/resources/extensions/browser-tools/tests/capture-sharp-optional.test.cjs +69 -71
- package/dist/resources/extensions/browser-tools/tools/forms.js +5 -1
- package/dist/resources/extensions/browser-tools/tools/intent.js +5 -1
- package/dist/resources/extensions/claude-code-cli/readiness.js +72 -16
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +481 -17
- package/dist/resources/extensions/github-sync/templates.js +103 -0
- package/dist/resources/extensions/google-search/index.js +3 -2
- package/dist/resources/extensions/gsd/auto/loop.js +124 -2
- package/dist/resources/extensions/gsd/auto/phases.js +57 -39
- package/dist/resources/extensions/gsd/auto/session.js +6 -2
- package/dist/resources/extensions/gsd/auto-dispatch.js +142 -29
- package/dist/resources/extensions/gsd/auto-model-selection.js +124 -4
- package/dist/resources/extensions/gsd/auto-post-unit.js +150 -64
- package/dist/resources/extensions/gsd/auto-prompts.js +372 -104
- package/dist/resources/extensions/gsd/auto-recovery.js +197 -48
- package/dist/resources/extensions/gsd/auto-start.js +107 -29
- package/dist/resources/extensions/gsd/auto-tool-tracking.js +47 -7
- package/dist/resources/extensions/gsd/auto-worktree.js +122 -26
- package/dist/resources/extensions/gsd/auto.js +76 -21
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +19 -1
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +209 -0
- package/dist/resources/extensions/gsd/bootstrap/provider-error-resume.js +3 -6
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +7 -3
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +127 -9
- package/dist/resources/extensions/gsd/component-loader.js +447 -0
- package/dist/resources/extensions/gsd/component-types.js +69 -0
- package/dist/resources/extensions/gsd/context-store.js +23 -7
- package/dist/resources/extensions/gsd/detection.js +49 -1
- package/dist/resources/extensions/gsd/dispatch-guard.js +2 -17
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +1 -1
- package/dist/resources/extensions/gsd/forensics.js +106 -0
- package/dist/resources/extensions/gsd/gate-registry.js +2 -2
- package/dist/resources/extensions/gsd/git-constants.js +28 -1
- package/dist/resources/extensions/gsd/git-self-heal.js +27 -0
- package/dist/resources/extensions/gsd/git-service.js +126 -2
- package/dist/resources/extensions/gsd/gsd-db.js +6 -3
- package/dist/resources/extensions/gsd/guided-flow.js +39 -13
- package/dist/resources/extensions/gsd/memory-extractor.js +7 -1
- package/dist/resources/extensions/gsd/milestone-scope-classifier.js +299 -0
- package/dist/resources/extensions/gsd/milestone-summary-classifier.js +37 -0
- package/dist/resources/extensions/gsd/model-cost-table.js +3 -0
- package/dist/resources/extensions/gsd/model-router.js +6 -0
- package/dist/resources/extensions/gsd/native-git-bridge.js +34 -4
- package/dist/resources/extensions/gsd/preferences-validation.js +23 -0
- package/dist/resources/extensions/gsd/prompt-cache-optimizer.js +4 -0
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +6 -2
- package/dist/resources/extensions/gsd/prompts/discuss-headless.md +23 -4
- package/dist/resources/extensions/gsd/prompts/doctor-heal.md +5 -4
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +15 -2
- package/dist/resources/extensions/gsd/safety/git-checkpoint.js +11 -0
- package/dist/resources/extensions/gsd/service-tier.js +5 -2
- package/dist/resources/extensions/gsd/session-lock.js +19 -10
- package/dist/resources/extensions/gsd/skill-manifest.js +168 -0
- package/dist/resources/extensions/gsd/slice-cadence.js +238 -0
- package/dist/resources/extensions/gsd/slice-parallel-orchestrator.js +278 -8
- package/dist/resources/extensions/gsd/state-transition-matrix.js +118 -0
- package/dist/resources/extensions/gsd/state.js +69 -58
- package/dist/resources/extensions/gsd/sync-lock.js +98 -42
- package/dist/resources/extensions/gsd/tools/validate-milestone.js +7 -2
- package/dist/resources/extensions/gsd/unit-context-composer.js +147 -0
- package/dist/resources/extensions/gsd/unit-context-manifest.js +370 -0
- package/dist/resources/extensions/gsd/uok/dispatch-envelope.js +33 -0
- package/dist/resources/extensions/gsd/uok/execution-graph.js +10 -0
- package/dist/resources/extensions/gsd/uok/gate-runner.js +53 -5
- package/dist/resources/extensions/gsd/uok/gitops.js +2 -1
- package/dist/resources/extensions/gsd/uok/loop-adapter.js +37 -10
- package/dist/resources/extensions/gsd/uok/parity-report.js +58 -0
- package/dist/resources/extensions/gsd/uok/plan-v2.js +10 -4
- package/dist/resources/extensions/gsd/uok/writer.js +82 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +6 -0
- package/dist/resources/extensions/gsd/worktree-manager.js +85 -8
- package/dist/resources/extensions/gsd/worktree-resolver.js +86 -7
- package/dist/resources/extensions/gsd/worktree-telemetry.js +198 -0
- package/dist/resources/extensions/mcp-client/index.js +3 -1
- package/dist/resources/extensions/ollama/index.js +5 -1
- package/dist/resources/extensions/remote-questions/manager.js +11 -5
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
- package/dist/web/standalone/.next/server/chunks/1926.js +1 -1
- package/dist/web/standalone/.next/server/chunks/6897.js +1 -1
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +2 -3
- package/packages/daemon/package.json +2 -2
- package/packages/daemon/src/logger.ts +4 -3
- package/packages/mcp-server/dist/server.d.ts +24 -0
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +88 -87
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +15 -6
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/package.json +2 -2
- package/packages/mcp-server/src/mcp-server.test.ts +25 -3
- package/packages/mcp-server/src/readers/graph.test.ts +87 -15
- package/packages/mcp-server/src/secure-env-collect.test.ts +232 -237
- package/packages/mcp-server/src/server.ts +131 -105
- package/packages/mcp-server/src/workflow-tools.test.ts +85 -0
- package/packages/mcp-server/src/workflow-tools.ts +19 -6
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/packages/native/package.json +2 -2
- package/packages/native/src/__tests__/_test-coverage-guard.test.mjs +98 -0
- package/packages/native/src/__tests__/module-compat.test.mjs +59 -27
- package/packages/native/src/__tests__/ps.test.mjs +14 -8
- package/packages/native/src/__tests__/stream-process.test.mjs +23 -2
- package/packages/native/src/__tests__/truncate.test.mjs +17 -2
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-agent-core/src/agent-loop.test.ts +5 -15
- package/packages/pi-agent-core/src/agent.test.ts +96 -102
- package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-ai/dist/models/capability-patches.d.ts.map +1 -1
- package/packages/pi-ai/dist/models/capability-patches.js +9 -2
- package/packages/pi-ai/dist/models/capability-patches.js.map +1 -1
- package/packages/pi-ai/dist/models/generated/index.d.ts +34 -0
- package/packages/pi-ai/dist/models/generated/index.d.ts.map +1 -1
- package/packages/pi-ai/dist/models/generated/openai-codex.d.ts +17 -0
- package/packages/pi-ai/dist/models/generated/openai-codex.d.ts.map +1 -1
- package/packages/pi-ai/dist/models/generated/openai-codex.js +17 -0
- package/packages/pi-ai/dist/models/generated/openai-codex.js.map +1 -1
- package/packages/pi-ai/dist/models/generated/openai.d.ts +17 -0
- package/packages/pi-ai/dist/models/generated/openai.d.ts.map +1 -1
- package/packages/pi-ai/dist/models/generated/openai.js +17 -0
- package/packages/pi-ai/dist/models/generated/openai.js.map +1 -1
- package/packages/pi-ai/dist/models.generated.test.js +43 -70
- package/packages/pi-ai/dist/models.generated.test.js.map +1 -1
- package/packages/pi-ai/dist/models.test.js +36 -11
- package/packages/pi-ai/dist/models.test.js.map +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-ai/scripts/generate-models.ts +44 -0
- package/packages/pi-ai/src/models/capability-patches.ts +10 -2
- package/packages/pi-ai/src/models/generated/openai-codex.ts +17 -0
- package/packages/pi-ai/src/models/generated/openai.ts +17 -0
- package/packages/pi-ai/src/models.generated.test.ts +46 -73
- package/packages/pi-ai/src/models.test.ts +48 -11
- package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js +96 -32
- package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.js +75 -12
- package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js +99 -31
- package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts +5 -0
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +61 -0
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.js +30 -4
- package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry-auth-mode.test.js +17 -0
- package/packages/pi-coding-agent/dist/core/model-registry-auth-mode.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/resource-loader-cache-reset.test.js +76 -18
- package/packages/pi-coding-agent/dist/core/resource-loader-cache-reset.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.js +2 -6
- package/packages/pi-coding-agent/dist/core/retry-handler.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.test.js +5 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/retryable-error-regex.d.ts +18 -0
- package/packages/pi-coding-agent/dist/core/retryable-error-regex.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/retryable-error-regex.js +18 -0
- package/packages/pi-coding-agent/dist/core/retryable-error-regex.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/system-prompt.d.ts +20 -0
- package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js +16 -2
- package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +1 -0
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +1 -0
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js +36 -5
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-execution.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.js +20 -13
- package/packages/pi-coding-agent/dist/modes/interactive/components/dynamic-border.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +30 -12
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +18 -3
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.test.js +125 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +4 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +105 -13
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/tests/system-prompt-skill-filter.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/tests/system-prompt-skill-filter.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/tests/system-prompt-skill-filter.test.js +130 -0
- package/packages/pi-coding-agent/dist/tests/system-prompt-skill-filter.test.js.map +1 -0
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/agent-session-abort-order.test.ts +113 -37
- package/packages/pi-coding-agent/src/core/agent-session-model-switch.test.ts +89 -17
- package/packages/pi-coding-agent/src/core/agent-session-tool-refresh.test.ts +112 -43
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +58 -0
- package/packages/pi-coding-agent/src/core/lsp/lsp-integration.test.ts +35 -4
- package/packages/pi-coding-agent/src/core/model-registry-auth-mode.test.ts +20 -0
- package/packages/pi-coding-agent/src/core/resource-loader-cache-reset.test.ts +93 -28
- package/packages/pi-coding-agent/src/core/retry-handler.test.ts +5 -1
- package/packages/pi-coding-agent/src/core/retry-handler.ts +2 -8
- package/packages/pi-coding-agent/src/core/retryable-error-regex.ts +18 -0
- package/packages/pi-coding-agent/src/core/system-prompt.ts +35 -1
- package/packages/pi-coding-agent/src/index.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-execution.test.ts +49 -3
- package/packages/pi-coding-agent/src/modes/interactive/components/dynamic-border.test.ts +26 -20
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +48 -9
- package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.test.ts +146 -1
- package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +20 -3
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +2 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +119 -13
- package/packages/pi-coding-agent/src/tests/system-prompt-skill-filter.test.ts +157 -0
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-tui/dist/__tests__/autocomplete.test.js +18 -8
- package/packages/pi-tui/dist/__tests__/autocomplete.test.js.map +1 -1
- package/packages/pi-tui/dist/__tests__/overlay-layout.test.js +128 -17
- package/packages/pi-tui/dist/__tests__/overlay-layout.test.js.map +1 -1
- package/packages/pi-tui/dist/__tests__/stdin-buffer.test.js +37 -11
- package/packages/pi-tui/dist/__tests__/stdin-buffer.test.js.map +1 -1
- package/packages/pi-tui/dist/__tests__/tui.test.js +18 -30
- package/packages/pi-tui/dist/__tests__/tui.test.js.map +1 -1
- package/packages/pi-tui/dist/components/__tests__/input.test.js +10 -3
- package/packages/pi-tui/dist/components/__tests__/input.test.js.map +1 -1
- package/packages/pi-tui/dist/components/__tests__/loader.test.js +53 -9
- package/packages/pi-tui/dist/components/__tests__/loader.test.js.map +1 -1
- package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.js +6 -2
- package/packages/pi-tui/dist/components/__tests__/markdown-maxlines.test.js.map +1 -1
- package/packages/pi-tui/dist/components/editor.d.ts +14 -0
- package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/editor.js +19 -0
- package/packages/pi-tui/dist/components/editor.js.map +1 -1
- package/packages/pi-tui/dist/components/image.test.js +6 -5
- package/packages/pi-tui/dist/components/image.test.js.map +1 -1
- package/packages/pi-tui/dist/editor-component.d.ts +2 -0
- package/packages/pi-tui/dist/editor-component.d.ts.map +1 -1
- package/packages/pi-tui/dist/editor-component.js.map +1 -1
- package/packages/pi-tui/package.json +1 -1
- package/packages/pi-tui/src/__tests__/autocomplete.test.ts +24 -8
- package/packages/pi-tui/src/__tests__/overlay-layout.test.ts +140 -17
- package/packages/pi-tui/src/__tests__/stdin-buffer.test.ts +42 -11
- package/packages/pi-tui/src/__tests__/tui.test.ts +18 -37
- package/packages/pi-tui/src/components/__tests__/input.test.ts +19 -3
- package/packages/pi-tui/src/components/__tests__/loader.test.ts +112 -35
- package/packages/pi-tui/src/components/__tests__/markdown-maxlines.test.ts +9 -2
- package/packages/pi-tui/src/components/editor.ts +22 -0
- package/packages/pi-tui/src/components/image.test.ts +10 -5
- package/packages/pi-tui/src/editor-component.ts +3 -0
- package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
- package/packages/rpc-client/dist/rpc-client.test.js +101 -51
- package/packages/rpc-client/dist/rpc-client.test.js.map +1 -1
- package/packages/rpc-client/package.json +1 -1
- package/packages/rpc-client/src/rpc-client.test.ts +109 -52
- package/packages/rpc-client/tsconfig.tsbuildinfo +1 -1
- package/pkg/package.json +1 -1
- package/scripts/install.js +15 -1
- package/src/resources/extensions/browser-tools/capture.ts +12 -0
- package/src/resources/extensions/browser-tools/tests/browser-tools-integration.test.mjs +8 -59
- package/src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +36 -24
- package/src/resources/extensions/browser-tools/tests/capture-sharp-optional.test.cjs +69 -71
- package/src/resources/extensions/browser-tools/tools/forms.ts +5 -1
- package/src/resources/extensions/browser-tools/tools/intent.ts +5 -1
- package/src/resources/extensions/claude-code-cli/readiness.ts +75 -16
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +518 -19
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +919 -75
- package/src/resources/extensions/github-sync/templates.ts +151 -0
- package/src/resources/extensions/github-sync/tests/cli.test.ts +76 -7
- package/src/resources/extensions/github-sync/tests/templates.test.ts +92 -1
- package/src/resources/extensions/google-search/index.ts +3 -2
- package/src/resources/extensions/gsd/auto/loop.ts +142 -2
- package/src/resources/extensions/gsd/auto/phases.ts +62 -38
- package/src/resources/extensions/gsd/auto/session.ts +7 -2
- package/src/resources/extensions/gsd/auto-dispatch.ts +156 -29
- package/src/resources/extensions/gsd/auto-model-selection.ts +131 -4
- package/src/resources/extensions/gsd/auto-post-unit.ts +163 -73
- package/src/resources/extensions/gsd/auto-prompts.ts +385 -93
- package/src/resources/extensions/gsd/auto-recovery.ts +230 -51
- package/src/resources/extensions/gsd/auto-start.ts +127 -9
- package/src/resources/extensions/gsd/auto-tool-tracking.ts +51 -7
- package/src/resources/extensions/gsd/auto-worktree.ts +130 -26
- package/src/resources/extensions/gsd/auto.ts +90 -23
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +20 -1
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +221 -0
- package/src/resources/extensions/gsd/bootstrap/provider-error-resume.ts +3 -7
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +7 -3
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +158 -9
- package/src/resources/extensions/gsd/component-loader.ts +598 -0
- package/src/resources/extensions/gsd/component-types.ts +362 -0
- package/src/resources/extensions/gsd/context-store.ts +25 -8
- package/src/resources/extensions/gsd/detection.ts +58 -1
- package/src/resources/extensions/gsd/dispatch-guard.ts +2 -20
- package/src/resources/extensions/gsd/docs/preferences-reference.md +1 -1
- package/src/resources/extensions/gsd/forensics.ts +118 -1
- package/src/resources/extensions/gsd/gate-registry.ts +2 -2
- package/src/resources/extensions/gsd/git-constants.ts +30 -1
- package/src/resources/extensions/gsd/git-self-heal.ts +31 -0
- package/src/resources/extensions/gsd/git-service.ts +149 -2
- package/src/resources/extensions/gsd/gsd-db.ts +6 -3
- package/src/resources/extensions/gsd/guided-flow.ts +57 -14
- package/src/resources/extensions/gsd/journal.ts +11 -1
- package/src/resources/extensions/gsd/memory-extractor.ts +11 -3
- package/src/resources/extensions/gsd/milestone-scope-classifier.ts +366 -0
- package/src/resources/extensions/gsd/milestone-summary-classifier.ts +42 -0
- package/src/resources/extensions/gsd/model-cost-table.ts +3 -0
- package/src/resources/extensions/gsd/model-router.ts +6 -0
- package/src/resources/extensions/gsd/native-git-bridge.ts +34 -4
- package/src/resources/extensions/gsd/preferences-validation.ts +21 -0
- package/src/resources/extensions/gsd/prompt-cache-optimizer.ts +4 -0
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +6 -2
- package/src/resources/extensions/gsd/prompts/discuss-headless.md +23 -4
- package/src/resources/extensions/gsd/prompts/doctor-heal.md +5 -4
- package/src/resources/extensions/gsd/prompts/plan-slice.md +15 -2
- package/src/resources/extensions/gsd/safety/git-checkpoint.ts +15 -0
- package/src/resources/extensions/gsd/service-tier.ts +5 -2
- package/src/resources/extensions/gsd/session-lock.ts +20 -10
- package/src/resources/extensions/gsd/skill-manifest.ts +175 -0
- package/src/resources/extensions/gsd/slice-cadence.ts +299 -0
- package/src/resources/extensions/gsd/slice-parallel-orchestrator.ts +309 -8
- package/src/resources/extensions/gsd/state-transition-matrix.ts +152 -0
- package/src/resources/extensions/gsd/state.ts +76 -66
- package/src/resources/extensions/gsd/sync-lock.ts +97 -39
- package/src/resources/extensions/gsd/tests/artifact-retry-cap.test.ts +270 -0
- package/src/resources/extensions/gsd/tests/artifacts-table-preserved-on-cache-invalidate.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/auto-deterministic-error-classification-4973.test.ts +341 -0
- package/src/resources/extensions/gsd/tests/auto-discuss-milestone-deadlock-4973.test.ts +264 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +133 -292
- package/src/resources/extensions/gsd/tests/auto-model-selection-tool-poisoning.test.ts +742 -0
- package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +78 -0
- package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +61 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +93 -0
- package/src/resources/extensions/gsd/tests/auto-remediate-slice-status.test.ts +4 -1
- package/src/resources/extensions/gsd/tests/auto-retry-mcp-churn-fixes.test.ts +8 -194
- package/src/resources/extensions/gsd/tests/auto-start-clean-runtime-db-gated.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/auto-start-cold-db-bootstrap.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/auto-start-needs-discussion.test.ts +15 -58
- package/src/resources/extensions/gsd/tests/auto-start-worktree-db-path.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/auto-thinking-restore.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/auto-warning-noise-regression.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/bootstrap-derive-state-db-open.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/cache-staleness-regression.test.ts +17 -21
- package/src/resources/extensions/gsd/tests/canonical-milestone-root.test.ts +108 -0
- package/src/resources/extensions/gsd/tests/complete-milestone-excerpt.test.ts +263 -0
- package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +25 -0
- package/src/resources/extensions/gsd/tests/complete-slice-composer.test.ts +192 -0
- package/src/resources/extensions/gsd/tests/complete-slice-verification-gate.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/complete-task.test.ts +16 -8
- package/src/resources/extensions/gsd/tests/component-loader.test.ts +589 -0
- package/src/resources/extensions/gsd/tests/component-types.test.ts +127 -0
- package/src/resources/extensions/gsd/tests/context-store.test.ts +79 -0
- package/src/resources/extensions/gsd/tests/copy-planning-artifacts-samepath.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +50 -1
- package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +159 -0
- package/src/resources/extensions/gsd/tests/db-access-guardrails.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/derive-state-crossval.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/derive-state-db-disk-reconcile.test.ts +40 -0
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +91 -3
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/discuss-slice-structured-questions.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/discuss-tool-scope-leak.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +5 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +25 -0
- package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/dispatcher-stuck-planning.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/double-merge-guard.test.ts +4 -3
- package/src/resources/extensions/gsd/tests/empty-content-abort-loop.test.ts +4 -3
- package/src/resources/extensions/gsd/tests/execution-entry-missing-context-4671.test.ts +173 -0
- package/src/resources/extensions/gsd/tests/extension-bootstrap-isolation.test.ts +139 -129
- package/src/resources/extensions/gsd/tests/finalize-timeout-guard.test.ts +8 -104
- package/src/resources/extensions/gsd/tests/gate-state-canonicalization.test.ts +102 -0
- package/src/resources/extensions/gsd/tests/gate-storage.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/google-search-stub.test.ts +14 -4
- package/src/resources/extensions/gsd/tests/headless-milestone-parity.test.ts +117 -0
- package/src/resources/extensions/gsd/tests/hook-key-parsing.test.ts +4 -55
- package/src/resources/extensions/gsd/tests/integration/all-milestones-complete-merge.test.ts +7 -56
- package/src/resources/extensions/gsd/tests/integration/auto-recovery.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/integration/doctor-proactive.test.ts +18 -2
- package/src/resources/extensions/gsd/tests/integration/queue-completed-milestone-perf.test.ts +10 -4
- package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +144 -7
- package/src/resources/extensions/gsd/tests/integration/state-machine-live-validation.test.ts +4 -0
- package/src/resources/extensions/gsd/tests/integration/state-machine-runtime-failures.test.ts +2 -16
- package/src/resources/extensions/gsd/tests/interactive-routing-bypass.test.ts +9 -3
- package/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts +6 -9
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +64 -0
- package/src/resources/extensions/gsd/tests/knowledge.test.ts +93 -1
- package/src/resources/extensions/gsd/tests/mcp-client-security.test.ts +8 -37
- package/src/resources/extensions/gsd/tests/memory-extractor.test.ts +5 -15
- package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +227 -55
- package/src/resources/extensions/gsd/tests/milestone-scope-classifier.test.ts +187 -0
- package/src/resources/extensions/gsd/tests/milestone-summary-classifier.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +9 -1
- package/src/resources/extensions/gsd/tests/model-router.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/native-git-bridge-exec-fallback.test.ts +6 -48
- package/src/resources/extensions/gsd/tests/notification-widget.test.ts +6 -3
- package/src/resources/extensions/gsd/tests/orphaned-worktree-audit.test.ts +59 -2
- package/src/resources/extensions/gsd/tests/parallel-research-dispatch.test.ts +273 -130
- package/src/resources/extensions/gsd/tests/pipeline-variant-dispatch.test.ts +301 -0
- package/src/resources/extensions/gsd/tests/pre-execution-pause-wiring.test.ts +32 -1
- package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/prompt-cache-optimizer.test.ts +12 -0
- package/src/resources/extensions/gsd/tests/prompt-step-ordering.test.ts +15 -4
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +23 -24
- package/src/resources/extensions/gsd/tests/queue-auto-guard.test.ts +32 -0
- package/src/resources/extensions/gsd/tests/queue-draft-detection.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/queued-discuss-fast-path.test.ts +4 -5
- package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +75 -2
- package/src/resources/extensions/gsd/tests/reassess-default-optin.test.ts +132 -0
- package/src/resources/extensions/gsd/tests/recovery-attempts-reset.test.ts +8 -40
- package/src/resources/extensions/gsd/tests/regex-hardening.test.ts +136 -256
- package/src/resources/extensions/gsd/tests/research-milestone-composer.test.ts +114 -0
- package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +6 -3
- package/src/resources/extensions/gsd/tests/run-uat-composer.test.ts +148 -0
- package/src/resources/extensions/gsd/tests/service-tier.test.ts +4 -0
- package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +29 -0
- package/src/resources/extensions/gsd/tests/sidecar-queue.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/silent-catch-diagnostics.test.ts +55 -95
- package/src/resources/extensions/gsd/tests/single-writer-v3-tool-surface.test.ts +158 -0
- package/src/resources/extensions/gsd/tests/skill-activation.test.ts +120 -1
- package/src/resources/extensions/gsd/tests/skill-manifest.test.ts +112 -0
- package/src/resources/extensions/gsd/tests/slice-cadence.test.ts +242 -0
- package/src/resources/extensions/gsd/tests/slice-context-injection.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/slice-parallel-orchestrator.test.ts +164 -1
- package/src/resources/extensions/gsd/tests/smart-entry-draft.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/stale-dirlistcache-4648.test.ts +112 -0
- package/src/resources/extensions/gsd/tests/state-machine-full-walkthrough.test.ts +29 -5
- package/src/resources/extensions/gsd/tests/state-transition-matrix.test.ts +44 -0
- package/src/resources/extensions/gsd/tests/stop-auto-race-null-unit.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/structured-data-formatter.test.ts +11 -92
- package/src/resources/extensions/gsd/tests/subagent-model-dispatch.test.ts +7 -6
- package/src/resources/extensions/gsd/tests/survivor-branch-complete.test.ts +102 -101
- package/src/resources/extensions/gsd/tests/sync-lock.test.ts +31 -0
- package/src/resources/extensions/gsd/tests/sync-worktree-skip-current.test.ts +4 -3
- package/src/resources/extensions/gsd/tests/test-helpers.test.ts +98 -0
- package/src/resources/extensions/gsd/tests/test-helpers.ts +153 -0
- package/src/resources/extensions/gsd/tests/token-profile.test.ts +8 -1
- package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +61 -1
- package/src/resources/extensions/gsd/tests/tool-naming.test.ts +8 -1
- package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +355 -0
- package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +258 -0
- package/src/resources/extensions/gsd/tests/uok-contracts.test.ts +51 -0
- package/src/resources/extensions/gsd/tests/uok-execution-graph.test.ts +16 -0
- package/src/resources/extensions/gsd/tests/uok-gate-runner.test.ts +75 -0
- package/src/resources/extensions/gsd/tests/uok-gitops-wiring.test.ts +49 -26
- package/src/resources/extensions/gsd/tests/uok-loop-adapter-writer.test.ts +65 -0
- package/src/resources/extensions/gsd/tests/uok-parity-report.test.ts +42 -0
- package/src/resources/extensions/gsd/tests/uok-plan-v2-wiring.test.ts +19 -2
- package/src/resources/extensions/gsd/tests/uok-writer.test.ts +75 -0
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +12 -0
- package/src/resources/extensions/gsd/tests/verify-artifact-tightened.test.ts +144 -80
- package/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +20 -54
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +342 -277
- package/src/resources/extensions/gsd/tests/worker-model-override.test.ts +37 -29
- package/src/resources/extensions/gsd/tests/worktree-db.test.ts +226 -266
- package/src/resources/extensions/gsd/tests/worktree-health-monorepo.test.ts +103 -67
- package/src/resources/extensions/gsd/tests/worktree-nested-git-safety.test.ts +92 -90
- package/src/resources/extensions/gsd/tests/worktree-submodule-safety.test.ts +238 -59
- package/src/resources/extensions/gsd/tests/worktree-sync-overwrite-loop.test.ts +113 -161
- package/src/resources/extensions/gsd/tests/worktree-telemetry.test.ts +210 -0
- package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +262 -0
- package/src/resources/extensions/gsd/tests/write-gate-predicates.test.ts +186 -0
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +7 -5
- package/src/resources/extensions/gsd/tests/zombie-gsd-state.test.ts +80 -96
- package/src/resources/extensions/gsd/tools/validate-milestone.ts +8 -2
- package/src/resources/extensions/gsd/types.ts +3 -3
- package/src/resources/extensions/gsd/unit-context-composer.ts +218 -0
- package/src/resources/extensions/gsd/unit-context-manifest.ts +574 -0
- package/src/resources/extensions/gsd/uok/contracts.ts +65 -0
- package/src/resources/extensions/gsd/uok/dispatch-envelope.ts +56 -0
- package/src/resources/extensions/gsd/uok/execution-graph.ts +22 -0
- package/src/resources/extensions/gsd/uok/gate-runner.ts +65 -5
- package/src/resources/extensions/gsd/uok/gitops.ts +6 -1
- package/src/resources/extensions/gsd/uok/loop-adapter.ts +45 -10
- package/src/resources/extensions/gsd/uok/parity-report.ts +84 -0
- package/src/resources/extensions/gsd/uok/plan-v2.ts +13 -5
- package/src/resources/extensions/gsd/uok/writer.ts +113 -0
- package/src/resources/extensions/gsd/workflow-mcp.ts +6 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +108 -7
- package/src/resources/extensions/gsd/worktree-resolver.ts +96 -9
- package/src/resources/extensions/gsd/worktree-telemetry.ts +322 -0
- package/src/resources/extensions/mcp-client/index.ts +3 -1
- package/src/resources/extensions/mcp-client/tests/server-name-spaces.test.ts +70 -36
- package/src/resources/extensions/ollama/index.ts +5 -1
- package/src/resources/extensions/ollama/ollama-auth-mode.test.ts +123 -15
- package/src/resources/extensions/ollama/ollama-status-indicator.test.ts +206 -19
- package/src/resources/extensions/remote-questions/manager.ts +36 -4
- package/src/resources/extensions/remote-questions/tests/command-polling.test.ts +200 -190
- package/src/resources/extensions/shared/tests/interview-preview.test.ts +11 -3
- package/src/resources/extensions/voice/tests/linux-ready.test.ts +129 -113
- package/packages/pi-ai/dist/utils/oauth/oauth-providers.test.d.ts +0 -2
- package/packages/pi-ai/dist/utils/oauth/oauth-providers.test.d.ts.map +0 -1
- package/packages/pi-ai/dist/utils/oauth/oauth-providers.test.js +0 -289
- package/packages/pi-ai/dist/utils/oauth/oauth-providers.test.js.map +0 -1
- package/packages/pi-ai/src/utils/oauth/oauth-providers.test.ts +0 -363
- package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +0 -143
- package/src/resources/extensions/gsd/tests/complete-milestone-false-merge.test.ts +0 -157
- package/src/resources/extensions/gsd/tests/dashboard-model-label-ordering.test.ts +0 -107
- package/src/resources/extensions/gsd/tests/find-missing-summaries-closed.test.ts +0 -48
- package/src/resources/extensions/gsd/tests/forensics-context-persist.test.ts +0 -159
- package/src/resources/extensions/gsd/tests/forensics-db-completion.test.ts +0 -96
- package/src/resources/extensions/gsd/tests/forensics-dedup.test.ts +0 -79
- package/src/resources/extensions/gsd/tests/forensics-hook-key-parse.test.ts +0 -74
- package/src/resources/extensions/gsd/tests/forensics-journal.test.ts +0 -162
- package/src/resources/extensions/gsd/tests/gitignore-bg-shell.test.ts +0 -38
- package/src/resources/extensions/gsd/tests/gsd-no-project-error.test.ts +0 -73
- package/src/resources/extensions/gsd/tests/idle-watchdog-stall-override.test.ts +0 -125
- package/src/resources/extensions/gsd/tests/import-done-milestones.test.ts +0 -42
- /package/dist/web/standalone/.next/static/{5wbu35_C2_MQ3Jj1lEVDx → cAJH99yNS1UPbeSEiNRrV}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{5wbu35_C2_MQ3Jj1lEVDx → cAJH99yNS1UPbeSEiNRrV}/_ssgManifest.js +0 -0
|
@@ -13,6 +13,10 @@ import {
|
|
|
13
13
|
buildPromptFromContext,
|
|
14
14
|
buildSdkQueryPrompt,
|
|
15
15
|
buildSdkOptions,
|
|
16
|
+
createClaudeCodeCanUseToolHandler,
|
|
17
|
+
buildBashPermissionPattern,
|
|
18
|
+
buildBashPermissionPatternOptions,
|
|
19
|
+
bashCommandMatchesSavedRules,
|
|
16
20
|
createClaudeCodeElicitationHandler,
|
|
17
21
|
extractImageBlocksFromContext,
|
|
18
22
|
extractToolResultsFromSdkUserMessage,
|
|
@@ -20,11 +24,58 @@ import {
|
|
|
20
24
|
parseAskUserQuestionsElicitation,
|
|
21
25
|
parseTextInputElicitation,
|
|
22
26
|
parseClaudeLookupOutput,
|
|
27
|
+
resolveBundledClaudeCliPath,
|
|
28
|
+
normalizeClaudePathForSdk,
|
|
23
29
|
roundResultToElicitationContent,
|
|
24
30
|
} from "../stream-adapter.ts";
|
|
25
31
|
import type { AssistantMessage, Context, Message } from "@gsd/pi-ai";
|
|
26
32
|
import type { SDKUserMessage } from "../sdk-types.ts";
|
|
27
33
|
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Env helpers — `GSD_WORKFLOW_MCP_*` save/restore
|
|
36
|
+
//
|
|
37
|
+
// The naive pattern `process.env.X = prev.X` breaks when `prev.X` is
|
|
38
|
+
// undefined: Node coerces the assignment to the literal string
|
|
39
|
+
// "undefined", which then pollutes subsequent tests that read the var
|
|
40
|
+
// and assume it's absent. Issue #4808 documents the resulting bleed.
|
|
41
|
+
//
|
|
42
|
+
// `setWorkflowMcpEnv` returns a `restore()` closure that either
|
|
43
|
+
// re-assigns the previous string value OR `delete`s the key when the
|
|
44
|
+
// original was absent. Call in a try/finally; restore in the finally.
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
const WORKFLOW_MCP_ENV_KEYS = [
|
|
48
|
+
"GSD_WORKFLOW_MCP_COMMAND",
|
|
49
|
+
"GSD_WORKFLOW_MCP_NAME",
|
|
50
|
+
"GSD_WORKFLOW_MCP_ARGS",
|
|
51
|
+
"GSD_WORKFLOW_MCP_ENV",
|
|
52
|
+
"GSD_WORKFLOW_MCP_CWD",
|
|
53
|
+
] as const;
|
|
54
|
+
|
|
55
|
+
type WorkflowMcpEnvKey = (typeof WORKFLOW_MCP_ENV_KEYS)[number];
|
|
56
|
+
|
|
57
|
+
function setWorkflowMcpEnv(
|
|
58
|
+
values: Partial<Record<WorkflowMcpEnvKey, string>>,
|
|
59
|
+
): () => void {
|
|
60
|
+
const prev: Partial<Record<WorkflowMcpEnvKey, string | undefined>> = {};
|
|
61
|
+
for (const key of WORKFLOW_MCP_ENV_KEYS) {
|
|
62
|
+
prev[key] = process.env[key];
|
|
63
|
+
}
|
|
64
|
+
for (const [key, value] of Object.entries(values)) {
|
|
65
|
+
process.env[key] = value;
|
|
66
|
+
}
|
|
67
|
+
return function restore() {
|
|
68
|
+
for (const key of WORKFLOW_MCP_ENV_KEYS) {
|
|
69
|
+
const previous = prev[key];
|
|
70
|
+
if (previous === undefined) {
|
|
71
|
+
delete process.env[key];
|
|
72
|
+
} else {
|
|
73
|
+
process.env[key] = previous;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
28
79
|
// ---------------------------------------------------------------------------
|
|
29
80
|
// Existing tests — exhausted stream fallback (#2575)
|
|
30
81
|
// ---------------------------------------------------------------------------
|
|
@@ -737,19 +788,14 @@ describe("stream-adapter — session persistence (#2859)", () => {
|
|
|
737
788
|
});
|
|
738
789
|
|
|
739
790
|
test("buildSdkOptions includes workflow MCP server config when env is set", () => {
|
|
740
|
-
const
|
|
741
|
-
GSD_WORKFLOW_MCP_COMMAND:
|
|
742
|
-
GSD_WORKFLOW_MCP_NAME:
|
|
743
|
-
GSD_WORKFLOW_MCP_ARGS:
|
|
744
|
-
GSD_WORKFLOW_MCP_ENV:
|
|
745
|
-
GSD_WORKFLOW_MCP_CWD:
|
|
746
|
-
};
|
|
791
|
+
const restore = setWorkflowMcpEnv({
|
|
792
|
+
GSD_WORKFLOW_MCP_COMMAND: "node",
|
|
793
|
+
GSD_WORKFLOW_MCP_NAME: "gsd-workflow",
|
|
794
|
+
GSD_WORKFLOW_MCP_ARGS: JSON.stringify(["packages/mcp-server/dist/cli.js"]),
|
|
795
|
+
GSD_WORKFLOW_MCP_ENV: JSON.stringify({ GSD_CLI_PATH: "/tmp/gsd" }),
|
|
796
|
+
GSD_WORKFLOW_MCP_CWD: "/tmp/project",
|
|
797
|
+
});
|
|
747
798
|
try {
|
|
748
|
-
process.env.GSD_WORKFLOW_MCP_COMMAND = "node";
|
|
749
|
-
process.env.GSD_WORKFLOW_MCP_NAME = "gsd-workflow";
|
|
750
|
-
process.env.GSD_WORKFLOW_MCP_ARGS = JSON.stringify(["packages/mcp-server/dist/cli.js"]);
|
|
751
|
-
process.env.GSD_WORKFLOW_MCP_ENV = JSON.stringify({ GSD_CLI_PATH: "/tmp/gsd" });
|
|
752
|
-
process.env.GSD_WORKFLOW_MCP_CWD = "/tmp/project";
|
|
753
799
|
|
|
754
800
|
const options = buildSdkOptions("claude-sonnet-4-20250514", "test");
|
|
755
801
|
const mcpServers = options.mcpServers as Record<string, any>;
|
|
@@ -776,28 +822,19 @@ describe("stream-adapter — session persistence (#2859)", () => {
|
|
|
776
822
|
"mcp__gsd-workflow__*",
|
|
777
823
|
]);
|
|
778
824
|
} finally {
|
|
779
|
-
|
|
780
|
-
process.env.GSD_WORKFLOW_MCP_NAME = prev.GSD_WORKFLOW_MCP_NAME;
|
|
781
|
-
process.env.GSD_WORKFLOW_MCP_ARGS = prev.GSD_WORKFLOW_MCP_ARGS;
|
|
782
|
-
process.env.GSD_WORKFLOW_MCP_ENV = prev.GSD_WORKFLOW_MCP_ENV;
|
|
783
|
-
process.env.GSD_WORKFLOW_MCP_CWD = prev.GSD_WORKFLOW_MCP_CWD;
|
|
825
|
+
restore();
|
|
784
826
|
}
|
|
785
827
|
});
|
|
786
828
|
|
|
787
829
|
test("buildSdkOptions auto-approves every tool for custom workflow MCP server names", () => {
|
|
788
|
-
const
|
|
789
|
-
GSD_WORKFLOW_MCP_COMMAND:
|
|
790
|
-
GSD_WORKFLOW_MCP_NAME:
|
|
791
|
-
GSD_WORKFLOW_MCP_ARGS:
|
|
792
|
-
GSD_WORKFLOW_MCP_ENV:
|
|
793
|
-
GSD_WORKFLOW_MCP_CWD:
|
|
794
|
-
};
|
|
830
|
+
const restore = setWorkflowMcpEnv({
|
|
831
|
+
GSD_WORKFLOW_MCP_COMMAND: "node",
|
|
832
|
+
GSD_WORKFLOW_MCP_NAME: "custom-workflow",
|
|
833
|
+
GSD_WORKFLOW_MCP_ARGS: JSON.stringify(["packages/mcp-server/dist/cli.js"]),
|
|
834
|
+
GSD_WORKFLOW_MCP_ENV: JSON.stringify({ GSD_CLI_PATH: "/tmp/gsd" }),
|
|
835
|
+
GSD_WORKFLOW_MCP_CWD: "/tmp/project",
|
|
836
|
+
});
|
|
795
837
|
try {
|
|
796
|
-
process.env.GSD_WORKFLOW_MCP_COMMAND = "node";
|
|
797
|
-
process.env.GSD_WORKFLOW_MCP_NAME = "custom-workflow";
|
|
798
|
-
process.env.GSD_WORKFLOW_MCP_ARGS = JSON.stringify(["packages/mcp-server/dist/cli.js"]);
|
|
799
|
-
process.env.GSD_WORKFLOW_MCP_ENV = JSON.stringify({ GSD_CLI_PATH: "/tmp/gsd" });
|
|
800
|
-
process.env.GSD_WORKFLOW_MCP_CWD = "/tmp/project";
|
|
801
838
|
|
|
802
839
|
const options = buildSdkOptions("claude-sonnet-4-20250514", "test");
|
|
803
840
|
const mcpServers = options.mcpServers as Record<string, any>;
|
|
@@ -817,22 +854,16 @@ describe("stream-adapter — session persistence (#2859)", () => {
|
|
|
817
854
|
"mcp__custom-workflow__*",
|
|
818
855
|
]);
|
|
819
856
|
} finally {
|
|
820
|
-
|
|
821
|
-
process.env.GSD_WORKFLOW_MCP_NAME = prev.GSD_WORKFLOW_MCP_NAME;
|
|
822
|
-
process.env.GSD_WORKFLOW_MCP_ARGS = prev.GSD_WORKFLOW_MCP_ARGS;
|
|
823
|
-
process.env.GSD_WORKFLOW_MCP_ENV = prev.GSD_WORKFLOW_MCP_ENV;
|
|
824
|
-
process.env.GSD_WORKFLOW_MCP_CWD = prev.GSD_WORKFLOW_MCP_CWD;
|
|
857
|
+
restore();
|
|
825
858
|
}
|
|
826
859
|
});
|
|
827
860
|
|
|
828
861
|
test("buildSdkOptions auto-discovers bundled MCP server even without env hints", () => {
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
GSD_WORKFLOW_MCP_CWD: process.env.GSD_WORKFLOW_MCP_CWD,
|
|
835
|
-
};
|
|
862
|
+
// Use setWorkflowMcpEnv with no values to save current state;
|
|
863
|
+
// restore() in finally will put it back correctly (including
|
|
864
|
+
// deleting any keys that started as undefined — the #4808 bug
|
|
865
|
+
// the naive `process.env.X = prev.X` pattern introduced).
|
|
866
|
+
const restore = setWorkflowMcpEnv({});
|
|
836
867
|
try {
|
|
837
868
|
delete process.env.GSD_WORKFLOW_MCP_COMMAND;
|
|
838
869
|
delete process.env.GSD_WORKFLOW_MCP_NAME;
|
|
@@ -857,23 +888,15 @@ describe("stream-adapter — session persistence (#2859)", () => {
|
|
|
857
888
|
}
|
|
858
889
|
rmSync(emptyDir, { recursive: true, force: true });
|
|
859
890
|
} finally {
|
|
860
|
-
|
|
861
|
-
process.env.GSD_WORKFLOW_MCP_NAME = prev.GSD_WORKFLOW_MCP_NAME;
|
|
862
|
-
process.env.GSD_WORKFLOW_MCP_ARGS = prev.GSD_WORKFLOW_MCP_ARGS;
|
|
863
|
-
process.env.GSD_WORKFLOW_MCP_ENV = prev.GSD_WORKFLOW_MCP_ENV;
|
|
864
|
-
process.env.GSD_WORKFLOW_MCP_CWD = prev.GSD_WORKFLOW_MCP_CWD;
|
|
891
|
+
restore();
|
|
865
892
|
}
|
|
866
893
|
});
|
|
867
894
|
|
|
868
895
|
test("buildSdkOptions auto-detects local workflow MCP dist CLI when present", () => {
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
GSD_WORKFLOW_MCP_ENV: process.env.GSD_WORKFLOW_MCP_ENV,
|
|
874
|
-
GSD_WORKFLOW_MCP_CWD: process.env.GSD_WORKFLOW_MCP_CWD,
|
|
875
|
-
GSD_CLI_PATH: process.env.GSD_CLI_PATH,
|
|
876
|
-
};
|
|
896
|
+
// GSD_CLI_PATH isn't in WORKFLOW_MCP_ENV_KEYS, so save+restore it
|
|
897
|
+
// manually around setWorkflowMcpEnv which handles the MCP keys.
|
|
898
|
+
const prevCliPath = process.env.GSD_CLI_PATH;
|
|
899
|
+
const restore = setWorkflowMcpEnv({});
|
|
877
900
|
const originalCwd = process.cwd();
|
|
878
901
|
const repoDir = mkdtempSync(join(tmpdir(), "claude-mcp-detect-"));
|
|
879
902
|
try {
|
|
@@ -904,23 +927,18 @@ describe("stream-adapter — session persistence (#2859)", () => {
|
|
|
904
927
|
} finally {
|
|
905
928
|
process.chdir(originalCwd);
|
|
906
929
|
rmSync(repoDir, { recursive: true, force: true });
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
930
|
+
restore();
|
|
931
|
+
// GSD_CLI_PATH isn't in setWorkflowMcpEnv's scope — restore it here.
|
|
932
|
+
if (prevCliPath === undefined) {
|
|
933
|
+
delete process.env.GSD_CLI_PATH;
|
|
934
|
+
} else {
|
|
935
|
+
process.env.GSD_CLI_PATH = prevCliPath;
|
|
936
|
+
}
|
|
913
937
|
}
|
|
914
938
|
});
|
|
915
939
|
|
|
916
940
|
test("buildSdkOptions preserves runtime callbacks such as onElicitation", () => {
|
|
917
|
-
const
|
|
918
|
-
GSD_WORKFLOW_MCP_COMMAND: process.env.GSD_WORKFLOW_MCP_COMMAND,
|
|
919
|
-
GSD_WORKFLOW_MCP_NAME: process.env.GSD_WORKFLOW_MCP_NAME,
|
|
920
|
-
GSD_WORKFLOW_MCP_ARGS: process.env.GSD_WORKFLOW_MCP_ARGS,
|
|
921
|
-
GSD_WORKFLOW_MCP_ENV: process.env.GSD_WORKFLOW_MCP_ENV,
|
|
922
|
-
GSD_WORKFLOW_MCP_CWD: process.env.GSD_WORKFLOW_MCP_CWD,
|
|
923
|
-
};
|
|
941
|
+
const restore = setWorkflowMcpEnv({});
|
|
924
942
|
const onElicitation = async () => ({ action: "decline" as const });
|
|
925
943
|
try {
|
|
926
944
|
delete process.env.GSD_WORKFLOW_MCP_COMMAND;
|
|
@@ -931,11 +949,7 @@ describe("stream-adapter — session persistence (#2859)", () => {
|
|
|
931
949
|
const options = buildSdkOptions("claude-sonnet-4-20250514", "test", undefined, { onElicitation });
|
|
932
950
|
assert.equal(options.onElicitation, onElicitation);
|
|
933
951
|
} finally {
|
|
934
|
-
|
|
935
|
-
process.env.GSD_WORKFLOW_MCP_NAME = prev.GSD_WORKFLOW_MCP_NAME;
|
|
936
|
-
process.env.GSD_WORKFLOW_MCP_ARGS = prev.GSD_WORKFLOW_MCP_ARGS;
|
|
937
|
-
process.env.GSD_WORKFLOW_MCP_ENV = prev.GSD_WORKFLOW_MCP_ENV;
|
|
938
|
-
process.env.GSD_WORKFLOW_MCP_CWD = prev.GSD_WORKFLOW_MCP_CWD;
|
|
952
|
+
restore();
|
|
939
953
|
}
|
|
940
954
|
});
|
|
941
955
|
});
|
|
@@ -1363,8 +1377,838 @@ describe("stream-adapter — Windows Claude path lookup (#3770)", () => {
|
|
|
1363
1377
|
assert.equal(getClaudeLookupCommand("linux"), "which claude");
|
|
1364
1378
|
});
|
|
1365
1379
|
|
|
1366
|
-
test("parseClaudeLookupOutput
|
|
1367
|
-
const output =
|
|
1368
|
-
|
|
1380
|
+
test("parseClaudeLookupOutput prefers .exe on win32 when where output includes shims", () => {
|
|
1381
|
+
const output = [
|
|
1382
|
+
"C:\\Users\\djeff\\AppData\\Roaming\\npm\\claude",
|
|
1383
|
+
"C:\\Users\\djeff\\AppData\\Roaming\\npm\\claude.cmd",
|
|
1384
|
+
"C:\\Program Files\\Claude\\claude.exe",
|
|
1385
|
+
].join("\r\n");
|
|
1386
|
+
assert.equal(parseClaudeLookupOutput(output, "win32"), "C:\\Program Files\\Claude\\claude.exe");
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
test("parseClaudeLookupOutput keeps first line on non-win32 platforms", () => {
|
|
1390
|
+
const output = "/usr/local/bin/claude\n/opt/homebrew/bin/claude\n";
|
|
1391
|
+
assert.equal(parseClaudeLookupOutput(output, "darwin"), "/usr/local/bin/claude");
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
test("normalizeClaudePathForSdk swaps Windows shim paths to bundled cli.js", () => {
|
|
1395
|
+
const shimPath = "C:\\Users\\djeff\\AppData\\Roaming\\npm\\claude";
|
|
1396
|
+
const bundled = "C:\\repo\\node_modules\\@anthropic-ai\\claude-agent-sdk\\cli.js";
|
|
1397
|
+
assert.equal(normalizeClaudePathForSdk(shimPath, "win32", bundled), bundled);
|
|
1398
|
+
assert.equal(normalizeClaudePathForSdk("C:\\Program Files\\Claude\\claude.exe", "win32", bundled), "C:\\Program Files\\Claude\\claude.exe");
|
|
1399
|
+
});
|
|
1400
|
+
|
|
1401
|
+
test("resolveBundledClaudeCliPath returns a .js path when SDK package is present", () => {
|
|
1402
|
+
const resolved = resolveBundledClaudeCliPath();
|
|
1403
|
+
assert.ok(resolved, "expected sdk cli.js to be resolvable in test workspace");
|
|
1404
|
+
assert.match(resolved!, /[\\/]@anthropic-ai[\\/]claude-agent-sdk[\\/]cli\.js$/);
|
|
1405
|
+
});
|
|
1406
|
+
});
|
|
1407
|
+
|
|
1408
|
+
// ---------------------------------------------------------------------------
|
|
1409
|
+
// canUseTool handler (#4383)
|
|
1410
|
+
// ---------------------------------------------------------------------------
|
|
1411
|
+
|
|
1412
|
+
describe("stream-adapter — canUseTool handler", () => {
|
|
1413
|
+
function makeOptions(overrides: Partial<{ signal: AbortSignal; suggestions: Array<Record<string, unknown>>; title: string; description: string; toolUseID: string }> = {}) {
|
|
1414
|
+
return {
|
|
1415
|
+
signal: overrides.signal ?? new AbortController().signal,
|
|
1416
|
+
toolUseID: overrides.toolUseID ?? "toolu_test123",
|
|
1417
|
+
...(overrides.title !== undefined ? { title: overrides.title } : {}),
|
|
1418
|
+
...(overrides.description !== undefined ? { description: overrides.description } : {}),
|
|
1419
|
+
...(overrides.suggestions !== undefined ? { suggestions: overrides.suggestions } : {}),
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// Point process.cwd() at an empty temp dir so the real repo's
|
|
1424
|
+
// .claude/settings.local.json (which may already contain rules like
|
|
1425
|
+
// "Bash(gh pr list:*)") does not short-circuit the permission flow.
|
|
1426
|
+
// Returns a cleanup function that restores cwd and removes the temp dir.
|
|
1427
|
+
// biome-ignore lint/suspicious/noExplicitAny: test-only monkey-patch
|
|
1428
|
+
function withIsolatedCwd(): () => void {
|
|
1429
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-canusetool-")));
|
|
1430
|
+
const orig = process.cwd;
|
|
1431
|
+
process.cwd = () => dir;
|
|
1432
|
+
return () => {
|
|
1433
|
+
process.cwd = orig;
|
|
1434
|
+
rmSync(dir, { recursive: true, force: true });
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
test("returns undefined when no UI context is provided", () => {
|
|
1439
|
+
const handler = createClaudeCodeCanUseToolHandler(undefined);
|
|
1440
|
+
assert.equal(handler, undefined);
|
|
1441
|
+
});
|
|
1442
|
+
|
|
1443
|
+
test("shows select dialog with Allow/Always Allow/Deny and returns allow", async () => {
|
|
1444
|
+
let selectPrompt = "";
|
|
1445
|
+
let selectOptions: string[] = [];
|
|
1446
|
+
const ui = {
|
|
1447
|
+
select: async (prompt: string, options: string[]) => {
|
|
1448
|
+
selectPrompt = prompt;
|
|
1449
|
+
selectOptions = options;
|
|
1450
|
+
return "Allow";
|
|
1451
|
+
},
|
|
1452
|
+
};
|
|
1453
|
+
|
|
1454
|
+
const handler = createClaudeCodeCanUseToolHandler(ui as any);
|
|
1455
|
+
assert.ok(handler);
|
|
1456
|
+
|
|
1457
|
+
const input = { command: "ls -la" };
|
|
1458
|
+
const result = await handler!("Bash", input, makeOptions({
|
|
1459
|
+
title: "Claude wants to run: ls -la",
|
|
1460
|
+
description: "List directory contents",
|
|
1461
|
+
}));
|
|
1462
|
+
|
|
1463
|
+
assert.equal(result.behavior, "allow");
|
|
1464
|
+
assert.deepEqual((result as any).updatedInput, input);
|
|
1465
|
+
assert.equal((result as any).toolUseID, "toolu_test123");
|
|
1466
|
+
// Allow (one-time) should NOT include updatedPermissions
|
|
1467
|
+
assert.equal((result as any).updatedPermissions, undefined);
|
|
1468
|
+
assert.deepEqual(selectOptions, ["Allow", "Always Allow", "Deny"]);
|
|
1469
|
+
// Prompt includes title and input summary
|
|
1470
|
+
assert.ok(selectPrompt.includes("Claude wants to run: ls -la"));
|
|
1471
|
+
assert.ok(selectPrompt.includes("ls -la"));
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
test("returns deny when user selects Deny", async () => {
|
|
1475
|
+
const ui = {
|
|
1476
|
+
select: async () => "Deny",
|
|
1477
|
+
};
|
|
1478
|
+
|
|
1479
|
+
const handler = createClaudeCodeCanUseToolHandler(ui as any);
|
|
1480
|
+
const result = await handler!("Bash", { command: "rm -rf /" }, makeOptions());
|
|
1481
|
+
|
|
1482
|
+
assert.equal(result.behavior, "deny");
|
|
1483
|
+
assert.equal((result as any).message, "User denied");
|
|
1484
|
+
assert.equal((result as any).toolUseID, "toolu_test123");
|
|
1485
|
+
});
|
|
1486
|
+
|
|
1487
|
+
test("returns deny when user dismisses dialog (undefined)", async () => {
|
|
1488
|
+
const ui = {
|
|
1489
|
+
select: async () => undefined,
|
|
1490
|
+
};
|
|
1491
|
+
|
|
1492
|
+
const handler = createClaudeCodeCanUseToolHandler(ui as any);
|
|
1493
|
+
const result = await handler!("Bash", { command: "echo hi" }, makeOptions());
|
|
1494
|
+
|
|
1495
|
+
assert.equal(result.behavior, "deny");
|
|
1496
|
+
assert.equal((result as any).message, "User denied");
|
|
1497
|
+
});
|
|
1498
|
+
|
|
1499
|
+
test("Always Allow for Bash patches SDK suggestions with smart ruleContent", async () => {
|
|
1500
|
+
const notified: string[] = [];
|
|
1501
|
+
const ui = { select: async (_p: string, opts: string[]) => opts.find((o) => o.startsWith("Always Allow"))!, notify: (msg: string) => notified.push(msg) };
|
|
1502
|
+
const suggestions = [{
|
|
1503
|
+
type: "addRules",
|
|
1504
|
+
rules: [{ toolName: "Bash", ruleContent: "ls -la /tmp" }],
|
|
1505
|
+
behavior: "allow",
|
|
1506
|
+
destination: "localSettings",
|
|
1507
|
+
}];
|
|
1508
|
+
|
|
1509
|
+
const handler = createClaudeCodeCanUseToolHandler(ui as any);
|
|
1510
|
+
const result = await handler!("Bash", { command: "ls -la /tmp" }, makeOptions({ suggestions }));
|
|
1511
|
+
|
|
1512
|
+
assert.equal(result.behavior, "allow");
|
|
1513
|
+
// Should patch ruleContent with our smart pattern, preserving SDK structure
|
|
1514
|
+
assert.deepEqual((result as any).updatedPermissions, [{
|
|
1515
|
+
type: "addRules",
|
|
1516
|
+
rules: [{ toolName: "Bash", ruleContent: "ls:*" }],
|
|
1517
|
+
behavior: "allow",
|
|
1518
|
+
destination: "localSettings",
|
|
1519
|
+
}]);
|
|
1520
|
+
assert.equal(notified.length, 1);
|
|
1521
|
+
assert.ok(notified[0].includes("Saved:") && notified[0].includes("Bash(ls:*)"));
|
|
1522
|
+
});
|
|
1523
|
+
|
|
1524
|
+
test("Always Allow for Bash with subcommand-sensitive CLI captures verb", async () => {
|
|
1525
|
+
const cleanup = withIsolatedCwd();
|
|
1526
|
+
try {
|
|
1527
|
+
const notified: string[] = [];
|
|
1528
|
+
// First select call: pick "Always Allow ..."; second call (level
|
|
1529
|
+
// picker): pick the "git push" granularity explicitly.
|
|
1530
|
+
let selectCall = 0;
|
|
1531
|
+
const ui = {
|
|
1532
|
+
select: async (_p: string, opts: string[]) => {
|
|
1533
|
+
selectCall++;
|
|
1534
|
+
if (selectCall === 1) return opts.find((o) => o.startsWith("Always Allow"))!;
|
|
1535
|
+
return "Bash(git push:*)";
|
|
1536
|
+
},
|
|
1537
|
+
notify: (msg: string) => notified.push(msg),
|
|
1538
|
+
};
|
|
1539
|
+
const suggestions = [{
|
|
1540
|
+
type: "addRules",
|
|
1541
|
+
rules: [{ toolName: "Bash", ruleContent: "git push origin main" }],
|
|
1542
|
+
behavior: "allow",
|
|
1543
|
+
destination: "localSettings",
|
|
1544
|
+
}];
|
|
1545
|
+
|
|
1546
|
+
const handler = createClaudeCodeCanUseToolHandler(ui as any);
|
|
1547
|
+
const result = await handler!("Bash", { command: "git push origin main" }, makeOptions({ suggestions }));
|
|
1548
|
+
|
|
1549
|
+
assert.equal(result.behavior, "allow");
|
|
1550
|
+
assert.deepEqual((result as any).updatedPermissions, [{
|
|
1551
|
+
type: "addRules",
|
|
1552
|
+
rules: [{ toolName: "Bash", ruleContent: "git push:*" }],
|
|
1553
|
+
behavior: "allow",
|
|
1554
|
+
destination: "localSettings",
|
|
1555
|
+
}]);
|
|
1556
|
+
assert.ok(notified[0].includes("Saved:") && notified[0].includes("Bash(git push:*)"));
|
|
1557
|
+
} finally {
|
|
1558
|
+
cleanup();
|
|
1559
|
+
}
|
|
1560
|
+
});
|
|
1561
|
+
|
|
1562
|
+
test("Always Allow for Bash without suggestions builds proper PermissionUpdate", async () => {
|
|
1563
|
+
const cleanup = withIsolatedCwd();
|
|
1564
|
+
try {
|
|
1565
|
+
const notified: string[] = [];
|
|
1566
|
+
let selectCall = 0;
|
|
1567
|
+
const ui = {
|
|
1568
|
+
select: async (_p: string, opts: string[]) => {
|
|
1569
|
+
selectCall++;
|
|
1570
|
+
if (selectCall === 1) return opts.find((o) => o.startsWith("Always Allow"))!;
|
|
1571
|
+
return "Bash(gh pr list:*)";
|
|
1572
|
+
},
|
|
1573
|
+
notify: (msg: string) => notified.push(msg),
|
|
1574
|
+
};
|
|
1575
|
+
|
|
1576
|
+
const handler = createClaudeCodeCanUseToolHandler(ui as any);
|
|
1577
|
+
const result = await handler!("Bash", { command: "gh pr list" }, makeOptions());
|
|
1578
|
+
|
|
1579
|
+
assert.equal(result.behavior, "allow");
|
|
1580
|
+
// No SDK suggestions → builds PermissionUpdate from scratch
|
|
1581
|
+
assert.deepEqual((result as any).updatedPermissions, [{
|
|
1582
|
+
type: "addRules",
|
|
1583
|
+
rules: [{ toolName: "Bash", ruleContent: "gh pr list:*" }],
|
|
1584
|
+
behavior: "allow",
|
|
1585
|
+
destination: "localSettings",
|
|
1586
|
+
}]);
|
|
1587
|
+
assert.ok(notified[0].includes("Saved:") && notified[0].includes("Bash(gh pr list:*)"));
|
|
1588
|
+
} finally {
|
|
1589
|
+
cleanup();
|
|
1590
|
+
}
|
|
1591
|
+
});
|
|
1592
|
+
|
|
1593
|
+
test("Always Allow for non-Bash tools passes SDK suggestions through", async () => {
|
|
1594
|
+
const notified: string[] = [];
|
|
1595
|
+
const ui = { select: async (_p: string, opts: string[]) => opts.find((o) => o.startsWith("Always Allow"))!, notify: (msg: string) => notified.push(msg) };
|
|
1596
|
+
const suggestions = [{
|
|
1597
|
+
type: "addRules",
|
|
1598
|
+
rules: [{ toolName: "Write" }],
|
|
1599
|
+
behavior: "allow",
|
|
1600
|
+
destination: "localSettings",
|
|
1601
|
+
}];
|
|
1602
|
+
|
|
1603
|
+
const handler = createClaudeCodeCanUseToolHandler(ui as any);
|
|
1604
|
+
const result = await handler!("Write", { file_path: "/tmp/test.txt" }, makeOptions({ suggestions }));
|
|
1605
|
+
|
|
1606
|
+
assert.equal(result.behavior, "allow");
|
|
1607
|
+
assert.deepEqual((result as any).updatedPermissions, suggestions);
|
|
1608
|
+
// Non-Bash tools don't emit a post-selection notification (only Bash runs the level picker)
|
|
1609
|
+
assert.equal(notified.length, 0);
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1612
|
+
test("Always Allow for non-Bash without suggestions omits updatedPermissions", async () => {
|
|
1613
|
+
const notified: string[] = [];
|
|
1614
|
+
const ui = { select: async (_p: string, opts: string[]) => opts.find((o) => o.startsWith("Always Allow"))!, notify: (msg: string) => notified.push(msg) };
|
|
1615
|
+
|
|
1616
|
+
const handler = createClaudeCodeCanUseToolHandler(ui as any);
|
|
1617
|
+
const result = await handler!("Write", { file_path: "/tmp/test.txt" }, makeOptions());
|
|
1618
|
+
|
|
1619
|
+
assert.equal(result.behavior, "allow");
|
|
1620
|
+
assert.equal((result as any).updatedPermissions, undefined);
|
|
1621
|
+
// No suggestions → no notification
|
|
1622
|
+
assert.equal(notified.length, 0);
|
|
1623
|
+
});
|
|
1624
|
+
|
|
1625
|
+
test("prompt includes command text for Bash tools", async () => {
|
|
1626
|
+
let selectPrompt = "";
|
|
1627
|
+
const ui = {
|
|
1628
|
+
select: async (prompt: string) => {
|
|
1629
|
+
selectPrompt = prompt;
|
|
1630
|
+
return "Allow";
|
|
1631
|
+
},
|
|
1632
|
+
};
|
|
1633
|
+
|
|
1634
|
+
const handler = createClaudeCodeCanUseToolHandler(ui as any);
|
|
1635
|
+
await handler!("Bash", { command: "git status" }, makeOptions());
|
|
1636
|
+
assert.ok(selectPrompt.includes("git status"), `prompt should include command: ${selectPrompt}`);
|
|
1637
|
+
});
|
|
1638
|
+
|
|
1639
|
+
test("prompt includes file_path for file tools", async () => {
|
|
1640
|
+
let selectPrompt = "";
|
|
1641
|
+
const ui = {
|
|
1642
|
+
select: async (prompt: string) => {
|
|
1643
|
+
selectPrompt = prompt;
|
|
1644
|
+
return "Allow";
|
|
1645
|
+
},
|
|
1646
|
+
};
|
|
1647
|
+
|
|
1648
|
+
const handler = createClaudeCodeCanUseToolHandler(ui as any);
|
|
1649
|
+
await handler!("Write", { file_path: "/tmp/test.txt", content: "hello" }, makeOptions());
|
|
1650
|
+
assert.ok(selectPrompt.includes("/tmp/test.txt"), `prompt should include file path: ${selectPrompt}`);
|
|
1651
|
+
});
|
|
1652
|
+
|
|
1653
|
+
test("uses title from options when available", async () => {
|
|
1654
|
+
let selectPrompt = "";
|
|
1655
|
+
const ui = {
|
|
1656
|
+
select: async (prompt: string) => {
|
|
1657
|
+
selectPrompt = prompt;
|
|
1658
|
+
return "Allow";
|
|
1659
|
+
},
|
|
1660
|
+
};
|
|
1661
|
+
|
|
1662
|
+
const handler = createClaudeCodeCanUseToolHandler(ui as any);
|
|
1663
|
+
await handler!("WebFetch", {}, makeOptions({ title: "Claude wants to fetch: https://example.com" }));
|
|
1664
|
+
assert.ok(selectPrompt.includes("Claude wants to fetch: https://example.com"));
|
|
1665
|
+
});
|
|
1666
|
+
|
|
1667
|
+
test("falls back to default title when options.title is missing", async () => {
|
|
1668
|
+
let selectPrompt = "";
|
|
1669
|
+
const ui = {
|
|
1670
|
+
select: async (prompt: string) => {
|
|
1671
|
+
selectPrompt = prompt;
|
|
1672
|
+
return "Allow";
|
|
1673
|
+
},
|
|
1674
|
+
};
|
|
1675
|
+
|
|
1676
|
+
const handler = createClaudeCodeCanUseToolHandler(ui as any);
|
|
1677
|
+
await handler!("WebFetch", { url: "https://example.com" }, makeOptions());
|
|
1678
|
+
assert.ok(selectPrompt.includes("Allow Claude Code to use: WebFetch?"));
|
|
1679
|
+
});
|
|
1680
|
+
|
|
1681
|
+
test("returns deny when signal is already aborted", async () => {
|
|
1682
|
+
const ui = {
|
|
1683
|
+
select: async () => { throw new Error("should not be called"); },
|
|
1684
|
+
};
|
|
1685
|
+
|
|
1686
|
+
const controller = new AbortController();
|
|
1687
|
+
controller.abort();
|
|
1688
|
+
|
|
1689
|
+
const handler = createClaudeCodeCanUseToolHandler(ui as any);
|
|
1690
|
+
const result = await handler!("Bash", {}, makeOptions({ signal: controller.signal }));
|
|
1691
|
+
|
|
1692
|
+
assert.equal(result.behavior, "deny");
|
|
1693
|
+
assert.equal((result as any).message, "Aborted");
|
|
1694
|
+
});
|
|
1695
|
+
|
|
1696
|
+
test("returns deny when ui.select throws", async () => {
|
|
1697
|
+
const ui = {
|
|
1698
|
+
select: async () => { throw new Error("dialog crashed"); },
|
|
1699
|
+
};
|
|
1700
|
+
|
|
1701
|
+
const handler = createClaudeCodeCanUseToolHandler(ui as any);
|
|
1702
|
+
const result = await handler!("Bash", {}, makeOptions());
|
|
1703
|
+
|
|
1704
|
+
assert.equal(result.behavior, "deny");
|
|
1705
|
+
assert.equal((result as any).message, "Aborted");
|
|
1706
|
+
});
|
|
1707
|
+
|
|
1708
|
+
test("buildSdkOptions passes canUseTool through extraOptions", () => {
|
|
1709
|
+
const canUseTool = async () => ({ behavior: "allow" as const, updatedInput: {}, toolUseID: "test" });
|
|
1710
|
+
const opts = buildSdkOptions("claude-sonnet-4-6", "test", undefined, { canUseTool });
|
|
1711
|
+
assert.equal(opts.canUseTool, canUseTool);
|
|
1712
|
+
});
|
|
1713
|
+
|
|
1714
|
+
test("Always Allow shows level picker and user broadens to base command", async () => {
|
|
1715
|
+
const cleanup = withIsolatedCwd();
|
|
1716
|
+
try {
|
|
1717
|
+
const prompts: string[] = [];
|
|
1718
|
+
const levelOpts: string[][] = [];
|
|
1719
|
+
let selectCall = 0;
|
|
1720
|
+
const ui = {
|
|
1721
|
+
select: async (prompt: string, opts: string[]) => {
|
|
1722
|
+
prompts.push(prompt);
|
|
1723
|
+
selectCall++;
|
|
1724
|
+
if (selectCall === 1) return opts.find((o) => o.startsWith("Always Allow"))!;
|
|
1725
|
+
levelOpts.push(opts);
|
|
1726
|
+
return "Bash(gh:*)";
|
|
1727
|
+
},
|
|
1728
|
+
notify: () => {},
|
|
1729
|
+
};
|
|
1730
|
+
|
|
1731
|
+
const handler = createClaudeCodeCanUseToolHandler(ui as any);
|
|
1732
|
+
const result = await handler!("Bash", { command: "gh pr list" }, makeOptions());
|
|
1733
|
+
|
|
1734
|
+
assert.equal(result.behavior, "allow");
|
|
1735
|
+
assert.deepEqual((result as any).updatedPermissions, [{
|
|
1736
|
+
type: "addRules",
|
|
1737
|
+
rules: [{ toolName: "Bash", ruleContent: "gh:*" }],
|
|
1738
|
+
behavior: "allow",
|
|
1739
|
+
destination: "localSettings",
|
|
1740
|
+
}]);
|
|
1741
|
+
// Second dialog offered every granularity level
|
|
1742
|
+
assert.deepEqual(levelOpts[0], [
|
|
1743
|
+
"Bash(gh:*)",
|
|
1744
|
+
"Bash(gh pr:*)",
|
|
1745
|
+
"Bash(gh pr list:*)",
|
|
1746
|
+
]);
|
|
1747
|
+
assert.ok(prompts[1].includes("Save permission at which level?"));
|
|
1748
|
+
} finally {
|
|
1749
|
+
cleanup();
|
|
1750
|
+
}
|
|
1751
|
+
});
|
|
1752
|
+
|
|
1753
|
+
test("Always Allow narrows to mid-level pattern when user picks Bash(gh pr:*)", async () => {
|
|
1754
|
+
const cleanup = withIsolatedCwd();
|
|
1755
|
+
try {
|
|
1756
|
+
let selectCall = 0;
|
|
1757
|
+
const ui = {
|
|
1758
|
+
select: async (_p: string, opts: string[]) => {
|
|
1759
|
+
selectCall++;
|
|
1760
|
+
if (selectCall === 1) return opts.find((o) => o.startsWith("Always Allow"))!;
|
|
1761
|
+
return "Bash(gh pr:*)";
|
|
1762
|
+
},
|
|
1763
|
+
notify: () => {},
|
|
1764
|
+
};
|
|
1765
|
+
|
|
1766
|
+
const handler = createClaudeCodeCanUseToolHandler(ui as any);
|
|
1767
|
+
const result = await handler!("Bash", { command: "gh pr list --limit 5" }, makeOptions());
|
|
1768
|
+
|
|
1769
|
+
assert.equal(result.behavior, "allow");
|
|
1770
|
+
assert.deepEqual((result as any).updatedPermissions, [{
|
|
1771
|
+
type: "addRules",
|
|
1772
|
+
rules: [{ toolName: "Bash", ruleContent: "gh pr:*" }],
|
|
1773
|
+
behavior: "allow",
|
|
1774
|
+
destination: "localSettings",
|
|
1775
|
+
}]);
|
|
1776
|
+
} finally {
|
|
1777
|
+
cleanup();
|
|
1778
|
+
}
|
|
1779
|
+
});
|
|
1780
|
+
|
|
1781
|
+
test("Always Allow skips level picker when only one pattern is available", async () => {
|
|
1782
|
+
const cleanup = withIsolatedCwd();
|
|
1783
|
+
try {
|
|
1784
|
+
const prompts: string[] = [];
|
|
1785
|
+
const ui = {
|
|
1786
|
+
select: async (prompt: string, opts: string[]) => {
|
|
1787
|
+
prompts.push(prompt);
|
|
1788
|
+
return opts.find((o) => o.startsWith("Always Allow"))!;
|
|
1789
|
+
},
|
|
1790
|
+
notify: () => {},
|
|
1791
|
+
};
|
|
1792
|
+
|
|
1793
|
+
const handler = createClaudeCodeCanUseToolHandler(ui as any);
|
|
1794
|
+
const result = await handler!("Bash", { command: "ls -la /tmp" }, makeOptions());
|
|
1795
|
+
|
|
1796
|
+
assert.equal(result.behavior, "allow");
|
|
1797
|
+
// "ls" has no subcommand tokens before the flag → single-option path
|
|
1798
|
+
assert.equal(prompts.length, 1, "should not show a second dialog");
|
|
1799
|
+
assert.deepEqual((result as any).updatedPermissions, [{
|
|
1800
|
+
type: "addRules",
|
|
1801
|
+
rules: [{ toolName: "Bash", ruleContent: "ls:*" }],
|
|
1802
|
+
behavior: "allow",
|
|
1803
|
+
destination: "localSettings",
|
|
1804
|
+
}]);
|
|
1805
|
+
} finally {
|
|
1806
|
+
cleanup();
|
|
1807
|
+
}
|
|
1808
|
+
});
|
|
1809
|
+
|
|
1810
|
+
test("Always Allow denies the tool when level picker is dismissed", async () => {
|
|
1811
|
+
const cleanup = withIsolatedCwd();
|
|
1812
|
+
try {
|
|
1813
|
+
const notified: string[] = [];
|
|
1814
|
+
let selectCall = 0;
|
|
1815
|
+
const ui = {
|
|
1816
|
+
select: async (_p: string, opts: string[]) => {
|
|
1817
|
+
selectCall++;
|
|
1818
|
+
if (selectCall === 1) return opts.find((o) => o.startsWith("Always Allow"))!;
|
|
1819
|
+
return undefined; // user dismissed level picker
|
|
1820
|
+
},
|
|
1821
|
+
notify: (msg: string) => notified.push(msg),
|
|
1822
|
+
};
|
|
1823
|
+
|
|
1824
|
+
const handler = createClaudeCodeCanUseToolHandler(ui as any);
|
|
1825
|
+
const result = await handler!("Bash", { command: "gh pr list" }, makeOptions());
|
|
1826
|
+
|
|
1827
|
+
// Dismissing the level picker cancels the tool use — a one-time allow
|
|
1828
|
+
// would leave the spawned agent running even though the user bailed.
|
|
1829
|
+
assert.equal(result.behavior, "deny");
|
|
1830
|
+
assert.equal((result as any).updatedPermissions, undefined);
|
|
1831
|
+
assert.equal(notified.length, 0, "no 'Saved:' notification when nothing was saved");
|
|
1832
|
+
} finally {
|
|
1833
|
+
cleanup();
|
|
1834
|
+
}
|
|
1835
|
+
});
|
|
1836
|
+
});
|
|
1837
|
+
|
|
1838
|
+
// ---------------------------------------------------------------------------
|
|
1839
|
+
// buildBashPermissionPattern — smart permission granularity
|
|
1840
|
+
// ---------------------------------------------------------------------------
|
|
1841
|
+
|
|
1842
|
+
describe("buildBashPermissionPattern", () => {
|
|
1843
|
+
test("simple command wildcards all args", () => {
|
|
1844
|
+
assert.equal(buildBashPermissionPattern("ping -n 4 localhost"), "Bash(ping:*)");
|
|
1845
|
+
assert.equal(buildBashPermissionPattern("echo hello world"), "Bash(echo:*)");
|
|
1846
|
+
assert.equal(buildBashPermissionPattern("ls -la /tmp"), "Bash(ls:*)");
|
|
1847
|
+
assert.equal(buildBashPermissionPattern("node server.js"), "Bash(node:*)");
|
|
1848
|
+
});
|
|
1849
|
+
|
|
1850
|
+
test("git captures one subcommand", () => {
|
|
1851
|
+
assert.equal(buildBashPermissionPattern("git push origin main"), "Bash(git push:*)");
|
|
1852
|
+
assert.equal(buildBashPermissionPattern("git log --oneline"), "Bash(git log:*)");
|
|
1853
|
+
assert.equal(buildBashPermissionPattern("git status"), "Bash(git status:*)");
|
|
1854
|
+
});
|
|
1855
|
+
|
|
1856
|
+
test("gh captures two subcommands", () => {
|
|
1857
|
+
assert.equal(buildBashPermissionPattern("gh pr list"), "Bash(gh pr list:*)");
|
|
1858
|
+
assert.equal(buildBashPermissionPattern("gh pr create --title foo"), "Bash(gh pr create:*)");
|
|
1859
|
+
assert.equal(buildBashPermissionPattern("gh issue view 123"), "Bash(gh issue view:*)");
|
|
1860
|
+
});
|
|
1861
|
+
|
|
1862
|
+
test("npm captures one subcommand", () => {
|
|
1863
|
+
assert.equal(buildBashPermissionPattern("npm install lodash"), "Bash(npm install:*)");
|
|
1864
|
+
assert.equal(buildBashPermissionPattern("npm publish"), "Bash(npm publish:*)");
|
|
1865
|
+
assert.equal(buildBashPermissionPattern("npm run test"), "Bash(npm run:*)");
|
|
1866
|
+
});
|
|
1867
|
+
|
|
1868
|
+
test("npx captures package name", () => {
|
|
1869
|
+
assert.equal(buildBashPermissionPattern("npx vitest run"), "Bash(npx vitest:*)");
|
|
1870
|
+
assert.equal(buildBashPermissionPattern("npx --version"), "Bash(npx --version:*)");
|
|
1871
|
+
});
|
|
1872
|
+
|
|
1873
|
+
test("docker captures one subcommand", () => {
|
|
1874
|
+
assert.equal(buildBashPermissionPattern("docker ps -a"), "Bash(docker ps:*)");
|
|
1875
|
+
assert.equal(buildBashPermissionPattern("docker rm container1"), "Bash(docker rm:*)");
|
|
1876
|
+
});
|
|
1877
|
+
|
|
1878
|
+
test("aws captures two subcommands", () => {
|
|
1879
|
+
assert.equal(buildBashPermissionPattern("aws s3 cp file.txt s3://bucket/"), "Bash(aws s3 cp:*)");
|
|
1880
|
+
assert.equal(buildBashPermissionPattern("aws ec2 describe-instances"), "Bash(aws ec2 describe-instances:*)");
|
|
1881
|
+
});
|
|
1882
|
+
|
|
1883
|
+
test("skips sudo wrapper", () => {
|
|
1884
|
+
assert.equal(buildBashPermissionPattern("sudo ping localhost"), "Bash(ping:*)");
|
|
1885
|
+
assert.equal(buildBashPermissionPattern("sudo git push"), "Bash(git push:*)");
|
|
1886
|
+
});
|
|
1887
|
+
|
|
1888
|
+
test("skips env wrapper and VAR=val assignments", () => {
|
|
1889
|
+
assert.equal(buildBashPermissionPattern("env NODE_ENV=prod node server.js"), "Bash(node:*)");
|
|
1890
|
+
assert.equal(buildBashPermissionPattern("NODE_ENV=prod node server.js"), "Bash(node:*)");
|
|
1891
|
+
assert.equal(buildBashPermissionPattern("FOO=bar BAZ=qux git push"), "Bash(git push:*)");
|
|
1892
|
+
});
|
|
1893
|
+
|
|
1894
|
+
test("strips path from executable", () => {
|
|
1895
|
+
assert.equal(buildBashPermissionPattern("/usr/bin/git push"), "Bash(git push:*)");
|
|
1896
|
+
assert.equal(buildBashPermissionPattern("C:\\Windows\\ping.exe localhost"), "Bash(ping:*)");
|
|
1897
|
+
});
|
|
1898
|
+
|
|
1899
|
+
test("empty or whitespace-only command", () => {
|
|
1900
|
+
assert.equal(buildBashPermissionPattern(""), "Bash(*)");
|
|
1901
|
+
assert.equal(buildBashPermissionPattern(" "), "Bash(*)");
|
|
1902
|
+
});
|
|
1903
|
+
|
|
1904
|
+
test("chained commands — extracts pattern from the meaningful segment", () => {
|
|
1905
|
+
assert.equal(buildBashPermissionPattern("cd /foo && gh pr list --limit 5"), "Bash(gh pr list:*)");
|
|
1906
|
+
assert.equal(buildBashPermissionPattern("cd C:/Users/djeff/repos/gsd-2 && gh pr list --limit 5"), "Bash(gh pr list:*)");
|
|
1907
|
+
assert.equal(buildBashPermissionPattern("cd /tmp && git push origin main"), "Bash(git push:*)");
|
|
1908
|
+
assert.equal(buildBashPermissionPattern("export FOO=1 && npm install lodash"), "Bash(npm install:*)");
|
|
1909
|
+
assert.equal(buildBashPermissionPattern("mkdir -p out; docker ps -a"), "Bash(docker ps:*)");
|
|
1910
|
+
assert.equal(buildBashPermissionPattern("echo start || ping localhost"), "Bash(ping:*)");
|
|
1911
|
+
});
|
|
1912
|
+
|
|
1913
|
+
test("skips trailing || true / || : error suppressors", () => {
|
|
1914
|
+
assert.equal(
|
|
1915
|
+
buildBashPermissionPattern("cd C:/Users/djeff/repos/gsd-2 && gh pr create --dry-run --title \"test\" --body \"test\" 2>&1 || true"),
|
|
1916
|
+
"Bash(gh pr create:*)",
|
|
1917
|
+
);
|
|
1918
|
+
assert.equal(buildBashPermissionPattern("gh pr list || true"), "Bash(gh pr list:*)");
|
|
1919
|
+
assert.equal(buildBashPermissionPattern("git push || :"), "Bash(git push:*)");
|
|
1920
|
+
assert.equal(buildBashPermissionPattern("cd /tmp && npm install || echo failed"), "Bash(npm install:*)");
|
|
1921
|
+
});
|
|
1922
|
+
|
|
1923
|
+
test("single command is unaffected by chain extraction", () => {
|
|
1924
|
+
assert.equal(buildBashPermissionPattern("gh pr list"), "Bash(gh pr list:*)");
|
|
1925
|
+
assert.equal(buildBashPermissionPattern("git push origin main"), "Bash(git push:*)");
|
|
1926
|
+
});
|
|
1927
|
+
});
|
|
1928
|
+
|
|
1929
|
+
// ---------------------------------------------------------------------------
|
|
1930
|
+
// buildBashPermissionPatternOptions — granularity level menu
|
|
1931
|
+
// ---------------------------------------------------------------------------
|
|
1932
|
+
|
|
1933
|
+
describe("buildBashPermissionPatternOptions", () => {
|
|
1934
|
+
test("offers every prefix from base to full subcommand chain", () => {
|
|
1935
|
+
assert.deepEqual(buildBashPermissionPatternOptions("gh pr list"), [
|
|
1936
|
+
"Bash(gh:*)",
|
|
1937
|
+
"Bash(gh pr:*)",
|
|
1938
|
+
"Bash(gh pr list:*)",
|
|
1939
|
+
]);
|
|
1940
|
+
assert.deepEqual(buildBashPermissionPatternOptions("git push origin main"), [
|
|
1941
|
+
"Bash(git:*)",
|
|
1942
|
+
"Bash(git push:*)",
|
|
1943
|
+
"Bash(git push origin:*)",
|
|
1944
|
+
"Bash(git push origin main:*)",
|
|
1945
|
+
]);
|
|
1946
|
+
});
|
|
1947
|
+
|
|
1948
|
+
test("stops at first flag — flags are args, not verbs", () => {
|
|
1949
|
+
assert.deepEqual(buildBashPermissionPatternOptions("gh pr create --title foo"), [
|
|
1950
|
+
"Bash(gh:*)",
|
|
1951
|
+
"Bash(gh pr:*)",
|
|
1952
|
+
"Bash(gh pr create:*)",
|
|
1953
|
+
]);
|
|
1954
|
+
assert.deepEqual(buildBashPermissionPatternOptions("git log --oneline"), [
|
|
1955
|
+
"Bash(git:*)",
|
|
1956
|
+
"Bash(git log:*)",
|
|
1957
|
+
]);
|
|
1958
|
+
});
|
|
1959
|
+
|
|
1960
|
+
test("single-option when there is no subcommand to choose from", () => {
|
|
1961
|
+
assert.deepEqual(buildBashPermissionPatternOptions("ls -la /tmp"), ["Bash(ls:*)"]);
|
|
1962
|
+
assert.deepEqual(buildBashPermissionPatternOptions("ping -n 4 localhost"), ["Bash(ping:*)"]);
|
|
1963
|
+
assert.deepEqual(buildBashPermissionPatternOptions("node"), ["Bash(node:*)"]);
|
|
1964
|
+
});
|
|
1965
|
+
|
|
1966
|
+
test("extracts meaningful segment from compound commands", () => {
|
|
1967
|
+
assert.deepEqual(buildBashPermissionPatternOptions("cd /foo && gh pr list"), [
|
|
1968
|
+
"Bash(gh:*)",
|
|
1969
|
+
"Bash(gh pr:*)",
|
|
1970
|
+
"Bash(gh pr list:*)",
|
|
1971
|
+
]);
|
|
1972
|
+
assert.deepEqual(buildBashPermissionPatternOptions("gh pr create --dry-run || true"), [
|
|
1973
|
+
"Bash(gh:*)",
|
|
1974
|
+
"Bash(gh pr:*)",
|
|
1975
|
+
"Bash(gh pr create:*)",
|
|
1976
|
+
]);
|
|
1977
|
+
});
|
|
1978
|
+
|
|
1979
|
+
test("caps at three subcommand tokens to keep the menu short", () => {
|
|
1980
|
+
const result = buildBashPermissionPatternOptions("foo bar baz qux quux corge");
|
|
1981
|
+
// base + 3 sub tokens = 4 patterns max
|
|
1982
|
+
assert.equal(result.length, 4);
|
|
1983
|
+
assert.deepEqual(result, [
|
|
1984
|
+
"Bash(foo:*)",
|
|
1985
|
+
"Bash(foo bar:*)",
|
|
1986
|
+
"Bash(foo bar baz:*)",
|
|
1987
|
+
"Bash(foo bar baz qux:*)",
|
|
1988
|
+
]);
|
|
1989
|
+
});
|
|
1990
|
+
|
|
1991
|
+
test("skips sudo/env wrappers like the single-pattern variant", () => {
|
|
1992
|
+
assert.deepEqual(buildBashPermissionPatternOptions("sudo git push origin"), [
|
|
1993
|
+
"Bash(git:*)",
|
|
1994
|
+
"Bash(git push:*)",
|
|
1995
|
+
"Bash(git push origin:*)",
|
|
1996
|
+
]);
|
|
1997
|
+
assert.deepEqual(buildBashPermissionPatternOptions("NODE_ENV=prod node server.js"), [
|
|
1998
|
+
"Bash(node:*)",
|
|
1999
|
+
"Bash(node server.js:*)",
|
|
2000
|
+
]);
|
|
2001
|
+
});
|
|
2002
|
+
|
|
2003
|
+
test("empty command returns the catch-all pattern", () => {
|
|
2004
|
+
assert.deepEqual(buildBashPermissionPatternOptions(""), ["Bash(*)"]);
|
|
2005
|
+
assert.deepEqual(buildBashPermissionPatternOptions(" "), ["Bash(*)"]);
|
|
2006
|
+
});
|
|
2007
|
+
});
|
|
2008
|
+
|
|
2009
|
+
// ---------------------------------------------------------------------------
|
|
2010
|
+
// bashCommandMatchesSavedRules — compound command bypass for saved rules
|
|
2011
|
+
// ---------------------------------------------------------------------------
|
|
2012
|
+
|
|
2013
|
+
describe("bashCommandMatchesSavedRules — compound command bypass", () => {
|
|
2014
|
+
let tempDir: string;
|
|
2015
|
+
let originalCwd: string;
|
|
2016
|
+
|
|
2017
|
+
// Create a temp project directory with .claude/settings.local.json
|
|
2018
|
+
function setupSettings(allow: string[]): void {
|
|
2019
|
+
const claudeDir = join(tempDir, ".claude");
|
|
2020
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
2021
|
+
writeFileSync(
|
|
2022
|
+
join(claudeDir, "settings.local.json"),
|
|
2023
|
+
JSON.stringify({ permissions: { allow } }),
|
|
2024
|
+
);
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
// biome-ignore lint/suspicious/noExplicitAny: test-only monkey-patch
|
|
2028
|
+
let origCwd: any;
|
|
2029
|
+
|
|
2030
|
+
// Monkey-patch process.cwd() to point at our temp dir
|
|
2031
|
+
function setCwd(dir: string): void {
|
|
2032
|
+
origCwd = process.cwd;
|
|
2033
|
+
process.cwd = () => dir;
|
|
2034
|
+
}
|
|
2035
|
+
function restoreCwd(): void {
|
|
2036
|
+
if (origCwd) process.cwd = origCwd;
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
test("matches cd-prefixed compound command against saved prefix rule", () => {
|
|
2040
|
+
tempDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-rules-")));
|
|
2041
|
+
try {
|
|
2042
|
+
setupSettings(["Bash(gh pr list:*)"]);
|
|
2043
|
+
setCwd(tempDir);
|
|
2044
|
+
assert.equal(
|
|
2045
|
+
bashCommandMatchesSavedRules("cd /some/path && gh pr list --limit 5"),
|
|
2046
|
+
true,
|
|
2047
|
+
);
|
|
2048
|
+
} finally {
|
|
2049
|
+
restoreCwd();
|
|
2050
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
2051
|
+
}
|
|
2052
|
+
});
|
|
2053
|
+
|
|
2054
|
+
test("matches cd-prefixed compound command with exact subcommand", () => {
|
|
2055
|
+
tempDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-rules-")));
|
|
2056
|
+
try {
|
|
2057
|
+
setupSettings(["Bash(gh pr list:*)"]);
|
|
2058
|
+
setCwd(tempDir);
|
|
2059
|
+
assert.equal(
|
|
2060
|
+
bashCommandMatchesSavedRules("cd C:/Users/foo/repos/bar && gh pr list"),
|
|
2061
|
+
true,
|
|
2062
|
+
);
|
|
2063
|
+
} finally {
|
|
2064
|
+
restoreCwd();
|
|
2065
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
2066
|
+
}
|
|
2067
|
+
});
|
|
2068
|
+
|
|
2069
|
+
test("rejects when leading segment is not cd", () => {
|
|
2070
|
+
tempDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-rules-")));
|
|
2071
|
+
try {
|
|
2072
|
+
setupSettings(["Bash(gh pr list:*)"]);
|
|
2073
|
+
setCwd(tempDir);
|
|
2074
|
+
// "rm -rf /tmp" is not a cd command — should not auto-approve
|
|
2075
|
+
assert.equal(
|
|
2076
|
+
bashCommandMatchesSavedRules("rm -rf /tmp && gh pr list"),
|
|
2077
|
+
false,
|
|
2078
|
+
);
|
|
2079
|
+
} finally {
|
|
2080
|
+
restoreCwd();
|
|
2081
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
2082
|
+
}
|
|
2083
|
+
});
|
|
2084
|
+
|
|
2085
|
+
test("rejects when meaningful segment does not match any rule", () => {
|
|
2086
|
+
tempDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-rules-")));
|
|
2087
|
+
try {
|
|
2088
|
+
setupSettings(["Bash(gh pr list:*)"]);
|
|
2089
|
+
setCwd(tempDir);
|
|
2090
|
+
assert.equal(
|
|
2091
|
+
bashCommandMatchesSavedRules("cd /path && gh issue create --title foo"),
|
|
2092
|
+
false,
|
|
2093
|
+
);
|
|
2094
|
+
} finally {
|
|
2095
|
+
restoreCwd();
|
|
2096
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
2097
|
+
}
|
|
2098
|
+
});
|
|
2099
|
+
|
|
2100
|
+
test("matches simple (non-compound) commands against on-disk rules", () => {
|
|
2101
|
+
tempDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-rules-")));
|
|
2102
|
+
try {
|
|
2103
|
+
setupSettings(["Bash(gh pr list:*)"]);
|
|
2104
|
+
setCwd(tempDir);
|
|
2105
|
+
// Simple commands must also be checked — the SDK's in-memory cache
|
|
2106
|
+
// may be stale if the rule was added mid-session via "Always Allow"
|
|
2107
|
+
assert.equal(bashCommandMatchesSavedRules("gh pr list --limit 5"), true);
|
|
2108
|
+
assert.equal(bashCommandMatchesSavedRules("gh pr list"), true);
|
|
2109
|
+
} finally {
|
|
2110
|
+
restoreCwd();
|
|
2111
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
2112
|
+
}
|
|
2113
|
+
});
|
|
2114
|
+
|
|
2115
|
+
test("returns false for simple commands with no matching rule", () => {
|
|
2116
|
+
tempDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-rules-")));
|
|
2117
|
+
try {
|
|
2118
|
+
setupSettings(["Bash(gh pr list:*)"]);
|
|
2119
|
+
setCwd(tempDir);
|
|
2120
|
+
assert.equal(bashCommandMatchesSavedRules("gh issue list --limit 5"), false);
|
|
2121
|
+
} finally {
|
|
2122
|
+
restoreCwd();
|
|
2123
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
2124
|
+
}
|
|
2125
|
+
});
|
|
2126
|
+
|
|
2127
|
+
test("returns false when no settings file exists", () => {
|
|
2128
|
+
tempDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-rules-")));
|
|
2129
|
+
try {
|
|
2130
|
+
// No .claude/settings.local.json created
|
|
2131
|
+
setCwd(tempDir);
|
|
2132
|
+
assert.equal(
|
|
2133
|
+
bashCommandMatchesSavedRules("cd /path && gh pr list"),
|
|
2134
|
+
false,
|
|
2135
|
+
);
|
|
2136
|
+
} finally {
|
|
2137
|
+
restoreCwd();
|
|
2138
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
2139
|
+
}
|
|
2140
|
+
});
|
|
2141
|
+
|
|
2142
|
+
test("matches exact rule (non-prefix)", () => {
|
|
2143
|
+
tempDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-rules-")));
|
|
2144
|
+
try {
|
|
2145
|
+
setupSettings(["Bash(ping -n 4 localhost)"]);
|
|
2146
|
+
setCwd(tempDir);
|
|
2147
|
+
assert.equal(
|
|
2148
|
+
bashCommandMatchesSavedRules("cd /path && ping -n 4 localhost"),
|
|
2149
|
+
true,
|
|
2150
|
+
);
|
|
2151
|
+
} finally {
|
|
2152
|
+
restoreCwd();
|
|
2153
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
2154
|
+
}
|
|
2155
|
+
});
|
|
2156
|
+
|
|
2157
|
+
test("handles multiple cd segments before the meaningful command", () => {
|
|
2158
|
+
tempDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-rules-")));
|
|
2159
|
+
try {
|
|
2160
|
+
setupSettings(["Bash(npm install:*)"]);
|
|
2161
|
+
setCwd(tempDir);
|
|
2162
|
+
assert.equal(
|
|
2163
|
+
bashCommandMatchesSavedRules("cd /home && cd project && npm install lodash"),
|
|
2164
|
+
true,
|
|
2165
|
+
);
|
|
2166
|
+
} finally {
|
|
2167
|
+
restoreCwd();
|
|
2168
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
2169
|
+
}
|
|
2170
|
+
});
|
|
2171
|
+
|
|
2172
|
+
test("matches compound command with trailing || true suppressor", () => {
|
|
2173
|
+
tempDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-rules-")));
|
|
2174
|
+
try {
|
|
2175
|
+
setupSettings(["Bash(gh pr create:*)"]);
|
|
2176
|
+
setCwd(tempDir);
|
|
2177
|
+
assert.equal(
|
|
2178
|
+
bashCommandMatchesSavedRules('cd C:/Users/djeff/repos/gsd-2 && gh pr create --dry-run --title "test" --body "test" 2>&1 || true'),
|
|
2179
|
+
true,
|
|
2180
|
+
);
|
|
2181
|
+
assert.equal(
|
|
2182
|
+
bashCommandMatchesSavedRules("gh pr create --dry-run || true"),
|
|
2183
|
+
true,
|
|
2184
|
+
);
|
|
2185
|
+
assert.equal(
|
|
2186
|
+
bashCommandMatchesSavedRules("cd /tmp && git push || :"),
|
|
2187
|
+
false, // rule is for gh pr create, not git push
|
|
2188
|
+
);
|
|
2189
|
+
} finally {
|
|
2190
|
+
restoreCwd();
|
|
2191
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
2192
|
+
}
|
|
2193
|
+
});
|
|
2194
|
+
|
|
2195
|
+
test("reads rules from settings.json as well as settings.local.json", () => {
|
|
2196
|
+
tempDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-rules-")));
|
|
2197
|
+
try {
|
|
2198
|
+
const claudeDir = join(tempDir, ".claude");
|
|
2199
|
+
mkdirSync(claudeDir, { recursive: true });
|
|
2200
|
+
writeFileSync(
|
|
2201
|
+
join(claudeDir, "settings.json"),
|
|
2202
|
+
JSON.stringify({ permissions: { allow: ["Bash(git push:*)"] } }),
|
|
2203
|
+
);
|
|
2204
|
+
setCwd(tempDir);
|
|
2205
|
+
assert.equal(
|
|
2206
|
+
bashCommandMatchesSavedRules("cd /repo && git push origin main"),
|
|
2207
|
+
true,
|
|
2208
|
+
);
|
|
2209
|
+
} finally {
|
|
2210
|
+
restoreCwd();
|
|
2211
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
2212
|
+
}
|
|
1369
2213
|
});
|
|
1370
2214
|
});
|