gsd-pi 2.71.0 → 2.72.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 +57 -17
- package/dist/cli.js +29 -3
- package/dist/headless-events.d.ts +2 -0
- package/dist/headless-events.js +7 -0
- package/dist/headless.js +16 -3
- package/dist/mcp-server.js +40 -17
- package/dist/provider-migrations.d.ts +10 -0
- package/dist/provider-migrations.js +12 -0
- package/dist/resource-loader.js +139 -13
- package/dist/resources/GSD-WORKFLOW.md +1 -1
- package/dist/resources/agents/debugger.md +58 -0
- package/dist/resources/agents/doc-writer.md +43 -0
- package/dist/resources/agents/git-ops.md +56 -0
- package/dist/resources/agents/javascript-pro.md +46 -271
- package/dist/resources/agents/planner.md +55 -0
- package/dist/resources/agents/refactorer.md +47 -0
- package/dist/resources/agents/reviewer.md +48 -0
- package/dist/resources/agents/security.md +59 -0
- package/dist/resources/agents/tester.md +50 -0
- package/dist/resources/agents/typescript-pro.md +41 -235
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +113 -10
- package/dist/resources/extensions/gsd/auto/infra-errors.js +34 -0
- package/dist/resources/extensions/gsd/auto/loop.js +32 -1
- package/dist/resources/extensions/gsd/auto/phases.js +5 -1
- package/dist/resources/extensions/gsd/auto/session.js +11 -0
- package/dist/resources/extensions/gsd/auto-dashboard.js +22 -16
- package/dist/resources/extensions/gsd/auto-model-selection.js +10 -2
- package/dist/resources/extensions/gsd/auto-prompts.js +88 -33
- package/dist/resources/extensions/gsd/auto-start.js +34 -7
- package/dist/resources/extensions/gsd/auto-tool-tracking.js +1 -1
- package/dist/resources/extensions/gsd/auto-worktree.js +1 -1
- package/dist/resources/extensions/gsd/auto.js +56 -0
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +3 -3
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +2 -0
- package/dist/resources/extensions/gsd/bootstrap/register-shortcuts.js +63 -51
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +6 -0
- package/dist/resources/extensions/gsd/commands/context.js +15 -6
- package/dist/resources/extensions/gsd/commands/dispatcher.js +12 -2
- package/dist/resources/extensions/gsd/commands/handlers/auto.js +10 -33
- package/dist/resources/extensions/gsd/commands/handlers/core.js +56 -11
- package/dist/resources/extensions/gsd/commands/handlers/notifications-handler.js +15 -6
- package/dist/resources/extensions/gsd/commands/handlers/workflow.js +4 -10
- package/dist/resources/extensions/gsd/dashboard-overlay.js +8 -3
- package/dist/resources/extensions/gsd/dispatch-guard.js +18 -1
- package/dist/resources/extensions/gsd/doctor-providers.js +23 -0
- package/dist/resources/extensions/gsd/error-classifier.js +5 -2
- package/dist/resources/extensions/gsd/forensics.js +19 -6
- package/dist/resources/extensions/gsd/gate-registry.js +208 -0
- package/dist/resources/extensions/gsd/gsd-db.js +41 -0
- package/dist/resources/extensions/gsd/guided-flow.js +5 -10
- package/dist/resources/extensions/gsd/metrics.js +1 -0
- package/dist/resources/extensions/gsd/milestone-actions.js +10 -4
- package/dist/resources/extensions/gsd/milestone-validation-gates.js +11 -12
- package/dist/resources/extensions/gsd/notification-overlay.js +42 -13
- package/dist/resources/extensions/gsd/notification-store.js +56 -5
- package/dist/resources/extensions/gsd/notification-widget.js +5 -13
- package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +8 -3
- package/dist/resources/extensions/gsd/pre-execution-checks.js +35 -2
- package/dist/resources/extensions/gsd/prompt-validation.js +126 -0
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +5 -3
- package/dist/resources/extensions/gsd/prompts/discuss.md +2 -0
- package/dist/resources/extensions/gsd/prompts/execute-task.md +22 -19
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +2 -0
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +2 -0
- package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/queue.md +3 -2
- package/dist/resources/extensions/gsd/prompts/system.md +1 -0
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +4 -1
- package/dist/resources/extensions/gsd/session-model-override.js +25 -0
- package/dist/resources/extensions/gsd/shortcut-defs.js +40 -0
- package/dist/resources/extensions/gsd/state.js +9 -2
- package/dist/resources/extensions/gsd/tools/complete-slice.js +52 -1
- package/dist/resources/extensions/gsd/tools/complete-task.js +51 -1
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +4 -1
- package/dist/resources/extensions/ollama/index.js +13 -5
- package/dist/resources/extensions/shared/gsd-phase-state.js +35 -0
- package/dist/resources/extensions/subagent/agents.js +8 -0
- package/dist/resources/extensions/subagent/index.js +17 -0
- package/dist/resources/skills/create-skill/SKILL.md +2 -0
- package/dist/startup-model-validation.d.ts +0 -1
- package/dist/startup-model-validation.js +6 -2
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +8 -8
- 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/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 +8 -8
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
- package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
- 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 +1 -1
- package/packages/mcp-server/dist/server.d.ts +12 -1
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +90 -42
- 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 +22 -12
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/server.ts +110 -38
- package/packages/mcp-server/src/workflow-tools.test.ts +110 -0
- package/packages/mcp-server/src/workflow-tools.ts +32 -12
- package/packages/pi-ai/dist/providers/amazon-bedrock.js +11 -2
- package/packages/pi-ai/dist/providers/amazon-bedrock.js.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-auth.test.d.ts +2 -0
- package/packages/pi-ai/dist/providers/anthropic-auth.test.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/anthropic-auth.test.js +20 -0
- package/packages/pi-ai/dist/providers/anthropic-auth.test.js.map +1 -0
- package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +4 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.js +8 -3
- package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.test.js +44 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.test.js.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.d.ts +2 -1
- package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.js +7 -4
- package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js +11 -0
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/src/providers/amazon-bedrock.ts +13 -1
- package/packages/pi-ai/src/providers/anthropic-auth.test.ts +32 -0
- package/packages/pi-ai/src/providers/anthropic-shared.test.ts +55 -1
- package/packages/pi-ai/src/providers/anthropic-shared.ts +14 -3
- package/packages/pi-ai/src/providers/anthropic.ts +8 -4
- package/packages/pi-ai/src/providers/openai-completions.ts +14 -0
- package/packages/pi-coding-agent/dist/core/agent-session-renderable-tools.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/agent-session-renderable-tools.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/agent-session-renderable-tools.test.js +61 -0
- package/packages/pi-coding-agent/dist/core/agent-session-renderable-tools.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +2 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +10 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.js +27 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.test.js +85 -0
- package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver-initial-model-auth.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/model-resolver-initial-model-auth.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-resolver-initial-model-auth.test.js +64 -0
- package/packages/pi-coding-agent/dist/core/model-resolver-initial-model-auth.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.js +22 -18
- package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.test.d.ts +8 -0
- package/packages/pi-coding-agent/dist/core/model-resolver.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-resolver.test.js +75 -0
- package/packages/pi-coding-agent/dist/core/model-resolver.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/retry-handler.d.ts +5 -0
- package/packages/pi-coding-agent/dist/core/retry-handler.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.js +55 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.test.js +57 -0
- package/packages/pi-coding-agent/dist/core/retry-handler.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.d.ts +11 -0
- package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.js +38 -5
- package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/sdk.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/sdk.test.js +71 -0
- package/packages/pi-coding-agent/dist/core/sdk.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +1 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/login-dialog.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/login-dialog.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/login-dialog.test.js +13 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/login-dialog.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.d.ts +4 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.js +24 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +9 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +4 -0
- 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 +43 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +7 -2
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.js +6 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.js.map +1 -1
- 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 +4 -3
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js +4 -2
- package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/agent-session-renderable-tools.test.ts +70 -0
- package/packages/pi-coding-agent/src/core/agent-session.ts +2 -1
- package/packages/pi-coding-agent/src/core/auth-storage.test.ts +108 -0
- package/packages/pi-coding-agent/src/core/auth-storage.ts +30 -0
- package/packages/pi-coding-agent/src/core/model-resolver-initial-model-auth.test.ts +78 -0
- package/packages/pi-coding-agent/src/core/model-resolver.test.ts +85 -0
- package/packages/pi-coding-agent/src/core/model-resolver.ts +22 -18
- package/packages/pi-coding-agent/src/core/retry-handler.test.ts +83 -0
- package/packages/pi-coding-agent/src/core/retry-handler.ts +60 -1
- package/packages/pi-coding-agent/src/core/sdk.test.ts +89 -0
- package/packages/pi-coding-agent/src/core/sdk.ts +45 -9
- package/packages/pi-coding-agent/src/index.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/login-dialog.test.ts +24 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/login-dialog.ts +30 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +15 -6
- package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +47 -0
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +7 -2
- package/packages/pi-coding-agent/src/modes/interactive/controllers/model-controller.ts +6 -1
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +4 -3
- package/packages/pi-coding-agent/src/modes/interactive/slash-command-handlers.ts +4 -2
- package/pkg/package.json +1 -1
- package/src/resources/GSD-WORKFLOW.md +1 -1
- package/src/resources/agents/debugger.md +58 -0
- package/src/resources/agents/doc-writer.md +43 -0
- package/src/resources/agents/git-ops.md +56 -0
- package/src/resources/agents/javascript-pro.md +46 -271
- package/src/resources/agents/planner.md +55 -0
- package/src/resources/agents/refactorer.md +47 -0
- package/src/resources/agents/reviewer.md +48 -0
- package/src/resources/agents/security.md +59 -0
- package/src/resources/agents/tester.md +50 -0
- package/src/resources/agents/typescript-pro.md +41 -235
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +122 -8
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +189 -6
- package/src/resources/extensions/gsd/auto/infra-errors.ts +38 -0
- package/src/resources/extensions/gsd/auto/loop-deps.ts +2 -0
- package/src/resources/extensions/gsd/auto/loop.ts +45 -1
- package/src/resources/extensions/gsd/auto/phases.ts +6 -0
- package/src/resources/extensions/gsd/auto/session.ts +11 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +29 -18
- package/src/resources/extensions/gsd/auto-model-selection.ts +9 -1
- package/src/resources/extensions/gsd/auto-prompts.ts +111 -33
- package/src/resources/extensions/gsd/auto-start.ts +41 -7
- package/src/resources/extensions/gsd/auto-tool-tracking.ts +1 -1
- package/src/resources/extensions/gsd/auto-worktree.ts +1 -1
- package/src/resources/extensions/gsd/auto.ts +72 -0
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +3 -3
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +2 -0
- package/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts +79 -60
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +7 -0
- package/src/resources/extensions/gsd/commands/context.ts +16 -5
- package/src/resources/extensions/gsd/commands/dispatcher.ts +14 -2
- package/src/resources/extensions/gsd/commands/handlers/auto.ts +10 -36
- package/src/resources/extensions/gsd/commands/handlers/core.ts +58 -11
- package/src/resources/extensions/gsd/commands/handlers/notifications-handler.ts +17 -7
- package/src/resources/extensions/gsd/commands/handlers/workflow.ts +4 -10
- package/src/resources/extensions/gsd/dashboard-overlay.ts +10 -3
- package/src/resources/extensions/gsd/dispatch-guard.ts +18 -1
- package/src/resources/extensions/gsd/doctor-providers.ts +24 -0
- package/src/resources/extensions/gsd/error-classifier.ts +5 -2
- package/src/resources/extensions/gsd/forensics.ts +23 -7
- package/src/resources/extensions/gsd/gate-registry.ts +251 -0
- package/src/resources/extensions/gsd/gsd-db.ts +51 -0
- package/src/resources/extensions/gsd/guided-flow.ts +5 -10
- package/src/resources/extensions/gsd/interrupted-session.ts +1 -0
- package/src/resources/extensions/gsd/metrics.ts +12 -1
- package/src/resources/extensions/gsd/milestone-actions.ts +10 -3
- package/src/resources/extensions/gsd/milestone-validation-gates.ts +11 -13
- package/src/resources/extensions/gsd/notification-overlay.ts +47 -14
- package/src/resources/extensions/gsd/notification-store.ts +54 -5
- package/src/resources/extensions/gsd/notification-widget.ts +5 -14
- package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +10 -3
- package/src/resources/extensions/gsd/pre-execution-checks.ts +39 -2
- package/src/resources/extensions/gsd/prompt-validation.ts +157 -0
- package/src/resources/extensions/gsd/prompts/complete-slice.md +5 -3
- package/src/resources/extensions/gsd/prompts/discuss.md +2 -0
- package/src/resources/extensions/gsd/prompts/execute-task.md +22 -19
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +2 -0
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +2 -0
- package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/queue.md +3 -2
- package/src/resources/extensions/gsd/prompts/system.md +1 -0
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +4 -1
- package/src/resources/extensions/gsd/session-model-override.ts +36 -0
- package/src/resources/extensions/gsd/shortcut-defs.ts +56 -0
- package/src/resources/extensions/gsd/state.ts +13 -2
- package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +25 -9
- package/src/resources/extensions/gsd/tests/auto-start-worktree-db-path.test.ts +28 -0
- package/src/resources/extensions/gsd/tests/bootstrap-derive-state-db-open.test.ts +39 -0
- package/src/resources/extensions/gsd/tests/complete-slice-gate-closure.test.ts +167 -0
- package/src/resources/extensions/gsd/tests/complete-slice-prompt-task-summary-layout.test.ts +18 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +36 -0
- package/src/resources/extensions/gsd/tests/execute-task-prompt-existing-artifact-guard.test.ts +33 -0
- package/src/resources/extensions/gsd/tests/forensics-stuck-loops.test.ts +62 -0
- package/src/resources/extensions/gsd/tests/format-shortcut.test.ts +31 -0
- package/src/resources/extensions/gsd/tests/gate-dispatch.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/gate-registry.test.ts +140 -0
- package/src/resources/extensions/gsd/tests/gsd-no-project-error.test.ts +73 -0
- package/src/resources/extensions/gsd/tests/infra-errors-cooldown.test.ts +180 -0
- package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +66 -1
- package/src/resources/extensions/gsd/tests/model-isolation.test.ts +36 -51
- package/src/resources/extensions/gsd/tests/notification-store.test.ts +35 -0
- package/src/resources/extensions/gsd/tests/notification-widget.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/notifications-handler.test.ts +90 -0
- package/src/resources/extensions/gsd/tests/parallel-monitor-overlay.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/park-db-sync.test.ts +18 -0
- package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +49 -0
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +19 -0
- package/src/resources/extensions/gsd/tests/prompt-system-gate-coverage.test.ts +208 -0
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +16 -0
- package/src/resources/extensions/gsd/tests/register-shortcuts.test.ts +63 -5
- package/src/resources/extensions/gsd/tests/session-model-override.test.ts +35 -0
- package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +90 -0
- package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +7 -0
- package/src/resources/extensions/gsd/tests/validate-milestone-prompt-verification-classes.test.ts +18 -0
- package/src/resources/extensions/gsd/tools/complete-slice.ts +63 -0
- package/src/resources/extensions/gsd/tools/complete-task.ts +63 -0
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +4 -1
- package/src/resources/extensions/gsd/types.ts +26 -0
- package/src/resources/extensions/ollama/index.ts +13 -3
- package/src/resources/extensions/ollama/ollama-status-indicator.test.ts +28 -0
- package/src/resources/extensions/shared/gsd-phase-state.ts +42 -0
- package/src/resources/extensions/shared/tests/gsd-phase-state.test.ts +48 -0
- package/src/resources/extensions/subagent/agents.ts +10 -0
- package/src/resources/extensions/subagent/index.ts +18 -0
- package/src/resources/extensions/subagent/tests/agents-conflicts.test.ts +33 -0
- package/src/resources/skills/create-skill/SKILL.md +2 -0
- /package/dist/web/standalone/.next/static/{nPky_WQC28aBD77eZsRAB → Y0I7CjXJl-tWoV__KidV4}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{nPky_WQC28aBD77eZsRAB → Y0I7CjXJl-tWoV__KidV4}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for the #unconfigured-models fix: findInitialModel() must
|
|
3
|
+
* skip the saved default when its provider has no working auth, rather than
|
|
4
|
+
* returning an unusable model that every selector surface would display as
|
|
5
|
+
* "current".
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import test from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { findInitialModel } from "./model-resolver.js";
|
|
11
|
+
|
|
12
|
+
function fakeRegistry(options: {
|
|
13
|
+
models: Array<{ provider: string; id: string }>;
|
|
14
|
+
readyProviders: Set<string>;
|
|
15
|
+
}) {
|
|
16
|
+
const fullModels = options.models.map((m) => ({
|
|
17
|
+
...m,
|
|
18
|
+
name: m.id,
|
|
19
|
+
api: "anthropic-messages",
|
|
20
|
+
baseUrl: "",
|
|
21
|
+
reasoning: false,
|
|
22
|
+
input: ["text"],
|
|
23
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
24
|
+
contextWindow: 128_000,
|
|
25
|
+
maxTokens: 4096,
|
|
26
|
+
}));
|
|
27
|
+
const available = fullModels.filter((m) => options.readyProviders.has(m.provider));
|
|
28
|
+
return {
|
|
29
|
+
find(provider: string, id: string) {
|
|
30
|
+
return fullModels.find((m) => m.provider === provider && m.id === id);
|
|
31
|
+
},
|
|
32
|
+
getAvailable() {
|
|
33
|
+
return available;
|
|
34
|
+
},
|
|
35
|
+
isProviderRequestReady(provider: string) {
|
|
36
|
+
return options.readyProviders.has(provider);
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test("findInitialModel skips saved default when provider has no auth", async () => {
|
|
42
|
+
// User saved xai/grok-4 as default, but XAI_API_KEY is unset so xai is
|
|
43
|
+
// in the registry but not ready. Previously findInitialModel() step 3
|
|
44
|
+
// returned xai anyway — now it must fall through to step 4 and pick
|
|
45
|
+
// an available model.
|
|
46
|
+
const registry = fakeRegistry({
|
|
47
|
+
models: [
|
|
48
|
+
{ provider: "xai", id: "grok-4-fast-non-reasoning" },
|
|
49
|
+
{ provider: "anthropic", id: "claude-opus-4-6" },
|
|
50
|
+
],
|
|
51
|
+
readyProviders: new Set(["anthropic"]),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const result = await findInitialModel({
|
|
55
|
+
scopedModels: [],
|
|
56
|
+
isContinuing: false,
|
|
57
|
+
defaultProvider: "xai",
|
|
58
|
+
defaultModelId: "grok-4-fast-non-reasoning",
|
|
59
|
+
modelRegistry: registry as any,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
assert.ok(result.model, "a model must be returned");
|
|
63
|
+
assert.equal(result.model!.provider, "anthropic", "unauth'd saved default must be skipped");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("findInitialModel keeps saved default when provider has auth", async () => {
|
|
67
|
+
const registry = fakeRegistry({
|
|
68
|
+
models: [
|
|
69
|
+
{ provider: "anthropic", id: "claude-opus-4-6" },
|
|
70
|
+
{ provider: "openai", id: "gpt-5.4" },
|
|
71
|
+
],
|
|
72
|
+
readyProviders: new Set(["anthropic", "openai"]),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const result = await findInitialModel({
|
|
76
|
+
scopedModels: [],
|
|
77
|
+
isContinuing: false,
|
|
78
|
+
defaultProvider: "openai",
|
|
79
|
+
defaultModelId: "gpt-5.4",
|
|
80
|
+
modelRegistry: registry as any,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
assert.equal(result.model?.provider, "openai");
|
|
84
|
+
assert.equal(result.model?.id, "gpt-5.4");
|
|
85
|
+
});
|
|
@@ -504,27 +504,31 @@ export async function findInitialModel(options: {
|
|
|
504
504
|
|
|
505
505
|
// 3. Try saved default from settings
|
|
506
506
|
if (defaultProvider && defaultModelId) {
|
|
507
|
-
|
|
508
|
-
if (
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
507
|
+
// Guard against stale settings defaults: only use the saved provider/model
|
|
508
|
+
// if the provider is actually request-ready (auth/OAuth/CLI ready).
|
|
509
|
+
if (modelRegistry.isProviderRequestReady(defaultProvider)) {
|
|
510
|
+
const found = modelRegistry.find(defaultProvider, defaultModelId);
|
|
511
|
+
if (found) {
|
|
512
|
+
// Check if the provider's recommended default is a higher-capability variant
|
|
513
|
+
// of the saved model (e.g. saved "claude-opus-4-6" vs recommended "claude-opus-4-6-extended").
|
|
514
|
+
// If so, prefer the recommended variant to avoid using a smaller context window (#1125).
|
|
515
|
+
const recommendedId = defaultModelPerProvider[defaultProvider as KnownProvider];
|
|
516
|
+
if (recommendedId && recommendedId !== defaultModelId && recommendedId.startsWith(defaultModelId)) {
|
|
517
|
+
const recommended = modelRegistry.find(defaultProvider, recommendedId);
|
|
518
|
+
if (recommended) {
|
|
519
|
+
model = recommended;
|
|
520
|
+
if (defaultThinkingLevel) {
|
|
521
|
+
thinkingLevel = defaultThinkingLevel;
|
|
522
|
+
}
|
|
523
|
+
return { model, thinkingLevel, fallbackMessage: undefined };
|
|
519
524
|
}
|
|
520
|
-
return { model, thinkingLevel, fallbackMessage: undefined };
|
|
521
525
|
}
|
|
526
|
+
model = found;
|
|
527
|
+
if (defaultThinkingLevel) {
|
|
528
|
+
thinkingLevel = defaultThinkingLevel;
|
|
529
|
+
}
|
|
530
|
+
return { model, thinkingLevel, fallbackMessage: undefined };
|
|
522
531
|
}
|
|
523
|
-
model = found;
|
|
524
|
-
if (defaultThinkingLevel) {
|
|
525
|
-
thinkingLevel = defaultThinkingLevel;
|
|
526
|
-
}
|
|
527
|
-
return { model, thinkingLevel, fallbackMessage: undefined };
|
|
528
532
|
}
|
|
529
533
|
}
|
|
530
534
|
|
|
@@ -171,6 +171,25 @@ describe("RetryHandler — long-context entitlement 429 (#2803)", () => {
|
|
|
171
171
|
const retryStart = emittedEvents.find((e) => e.type === "auto_retry_start");
|
|
172
172
|
assert.ok(retryStart, "Regular 429 should enter backoff retry");
|
|
173
173
|
});
|
|
174
|
+
|
|
175
|
+
it("classifies OpenRouter credit affordability errors as quota_exhausted", async () => {
|
|
176
|
+
const { deps, emittedEvents } = createMockDeps({
|
|
177
|
+
model: createMockModel("openrouter", "openai/gpt-5-pro"),
|
|
178
|
+
markUsageLimitReachedResult: false,
|
|
179
|
+
fallbackResult: null,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const handler = new RetryHandler(deps);
|
|
183
|
+
const msg = errorMessage(
|
|
184
|
+
"402 This request requires more credits, or fewer max_tokens. You requested up to 32000 tokens, but can only afford 329.",
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const result = await handler.handleRetryableError(msg);
|
|
188
|
+
|
|
189
|
+
assert.equal(result, true, "affordability error should trigger credit-aware retry");
|
|
190
|
+
const retryStart = emittedEvents.find((e) => e.type === "auto_retry_start");
|
|
191
|
+
assert.ok(retryStart, "Expected immediate retry after reducing max tokens");
|
|
192
|
+
});
|
|
174
193
|
});
|
|
175
194
|
|
|
176
195
|
describe("long-context model downgrade", () => {
|
|
@@ -271,6 +290,61 @@ describe("RetryHandler — long-context entitlement 429 (#2803)", () => {
|
|
|
271
290
|
});
|
|
272
291
|
});
|
|
273
292
|
|
|
293
|
+
describe("credit-aware maxTokens retry", () => {
|
|
294
|
+
it("reduces maxTokens on same model when provider reports affordable cap", async () => {
|
|
295
|
+
const expensiveModel = createMockModel("openrouter", "openai/gpt-5-pro");
|
|
296
|
+
expensiveModel.maxTokens = 128000;
|
|
297
|
+
|
|
298
|
+
const { deps, emittedEvents, onModelChangeFn } = createMockDeps({
|
|
299
|
+
model: expensiveModel,
|
|
300
|
+
markUsageLimitReachedResult: false,
|
|
301
|
+
fallbackResult: null,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const handler = new RetryHandler(deps);
|
|
305
|
+
const msg = errorMessage(
|
|
306
|
+
"402 This request requires more credits, or fewer max_tokens. You requested up to 32000 tokens, but can only afford 329.",
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
const result = await handler.handleRetryableError(msg);
|
|
310
|
+
assert.equal(result, true, "should retry after reducing maxTokens");
|
|
311
|
+
|
|
312
|
+
const setModelCalls = (deps.agent.setModel as any).mock.calls;
|
|
313
|
+
assert.equal(setModelCalls.length, 1, "should apply one model downgrade");
|
|
314
|
+
const downgraded = setModelCalls[0].arguments[0] as Model<Api>;
|
|
315
|
+
assert.equal(downgraded.provider, "openrouter");
|
|
316
|
+
assert.equal(downgraded.id, "openai/gpt-5-pro");
|
|
317
|
+
assert.equal(downgraded.maxTokens, 297, "expected affordability cap with safety buffer");
|
|
318
|
+
|
|
319
|
+
assert.equal(onModelChangeFn.mock.calls.length, 1, "should notify about model update");
|
|
320
|
+
const switchEvent = emittedEvents.find((e) => e.type === "fallback_provider_switch");
|
|
321
|
+
assert.ok(switchEvent, "should emit model-adjustment event");
|
|
322
|
+
assert.ok(
|
|
323
|
+
String(switchEvent?.reason || "").includes("credit-aware retry"),
|
|
324
|
+
"switch reason should mention credit-aware retry",
|
|
325
|
+
);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("does not mark credentials in cooldown for affordability quota errors", async () => {
|
|
329
|
+
const expensiveModel = createMockModel("openrouter", "openai/gpt-5-pro");
|
|
330
|
+
expensiveModel.maxTokens = 128000;
|
|
331
|
+
|
|
332
|
+
const { deps, markUsageLimitReached } = createMockDeps({
|
|
333
|
+
model: expensiveModel,
|
|
334
|
+
markUsageLimitReachedResult: false,
|
|
335
|
+
fallbackResult: null,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const handler = new RetryHandler(deps);
|
|
339
|
+
const msg = errorMessage(
|
|
340
|
+
"402 This request requires more credits, or fewer max_tokens. You requested up to 32000 tokens, but can only afford 329.",
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
await handler.handleRetryableError(msg);
|
|
344
|
+
assert.equal(markUsageLimitReached.mock.calls.length, 0, "quota error should skip credential cooldown");
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
274
348
|
describe("isRetryableError", () => {
|
|
275
349
|
it("considers long-context entitlement error as retryable", () => {
|
|
276
350
|
const { deps } = createMockDeps();
|
|
@@ -291,6 +365,15 @@ describe("RetryHandler — long-context entitlement 429 (#2803)", () => {
|
|
|
291
365
|
);
|
|
292
366
|
assert.equal(handler.isRetryableError(msg), false);
|
|
293
367
|
});
|
|
368
|
+
|
|
369
|
+
it("considers OpenRouter affordability credit errors as retryable", () => {
|
|
370
|
+
const { deps } = createMockDeps();
|
|
371
|
+
const handler = new RetryHandler(deps);
|
|
372
|
+
const msg = errorMessage(
|
|
373
|
+
"402 This request requires more credits, or fewer max_tokens. You requested up to 32000 tokens, but can only afford 329.",
|
|
374
|
+
);
|
|
375
|
+
assert.equal(handler.isRetryableError(msg), true);
|
|
376
|
+
});
|
|
294
377
|
});
|
|
295
378
|
|
|
296
379
|
describe("third-party block claude-code fallback (#3772)", () => {
|
|
@@ -116,7 +116,7 @@ export class RetryHandler {
|
|
|
116
116
|
// generated error from getApiKey() when credentials are in a backoff window.
|
|
117
117
|
// Re-entering the retry handler for that message creates a cascade of empty
|
|
118
118
|
// error entries in the session file, breaking resume (#3429).
|
|
119
|
-
return /overloaded|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|reset before headers|terminated|retry delay|network.?(?:is\s+)?unavailable|credentials.*expired|extra usage is required|(?:out of|no) extra usage|third.party.*draw from extra|third.party.*not.*available/i.test(
|
|
119
|
+
return /overloaded|rate.?limit|too many requests|402|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|connection.?error|connection.?refused|other side closed|fetch failed|upstream.?connect|reset before headers|terminated|retry delay|network.?(?:is\s+)?unavailable|credentials.*expired|requires more credits|can only afford|insufficient credits|not enough credits|extra usage is required|(?:out of|no) extra usage|third.party.*draw from extra|third.party.*not.*available/i.test(
|
|
120
120
|
err,
|
|
121
121
|
);
|
|
122
122
|
}
|
|
@@ -158,6 +158,14 @@ export class RetryHandler {
|
|
|
158
158
|
const isRateLimit = errorType === "rate_limit";
|
|
159
159
|
const isQuotaError = errorType === "quota_exhausted";
|
|
160
160
|
|
|
161
|
+
// Credit-aware retry (OpenRouter-style 402 affordability errors):
|
|
162
|
+
// when provider reports "can only afford N", lower maxTokens and retry
|
|
163
|
+
// on the same model before rotating credentials/providers.
|
|
164
|
+
if (isQuotaError) {
|
|
165
|
+
const adjusted = this._tryAffordableMaxTokensRetry(message, retryGeneration);
|
|
166
|
+
if (adjusted) return true;
|
|
167
|
+
}
|
|
168
|
+
|
|
161
169
|
// Credential rotation — only for transient rate limits (#3430).
|
|
162
170
|
// Quota errors ("Extra usage is required") are account-level billing
|
|
163
171
|
// gates; rotating to another credential on the same account won't help
|
|
@@ -409,12 +417,63 @@ export class RetryHandler {
|
|
|
409
417
|
// Long-context entitlement errors are billing gates, not transient rate limits.
|
|
410
418
|
// Must be checked before the generic 429/rate_limit regex.
|
|
411
419
|
if (/extra usage is required|long context required/i.test(err)) return "quota_exhausted";
|
|
420
|
+
if (/requires more credits|can only afford|insufficient credits|not enough credits|credit balance/i.test(err))
|
|
421
|
+
return "quota_exhausted";
|
|
412
422
|
if (/quota|billing|exceeded.*limit|usage.*limit/i.test(err)) return "quota_exhausted";
|
|
413
423
|
if (/rate.?limit|too many requests|429/i.test(err)) return "rate_limit";
|
|
414
424
|
if (/500|502|503|504|server.?error|internal.?error|service.?unavailable/i.test(err)) return "server_error";
|
|
415
425
|
return "unknown";
|
|
416
426
|
}
|
|
417
427
|
|
|
428
|
+
/**
|
|
429
|
+
* Attempt a same-model retry by reducing maxTokens when provider reports
|
|
430
|
+
* an affordability cap (e.g., "can only afford 329").
|
|
431
|
+
*/
|
|
432
|
+
private _tryAffordableMaxTokensRetry(message: AssistantMessage, retryGeneration: number): boolean {
|
|
433
|
+
const currentModel = this._deps.getModel();
|
|
434
|
+
if (!currentModel || !message.errorMessage) return false;
|
|
435
|
+
|
|
436
|
+
// Example: "can only afford 329"
|
|
437
|
+
const match = message.errorMessage.match(/can only afford\s+([\d,]+)/i);
|
|
438
|
+
if (!match?.[1]) return false;
|
|
439
|
+
|
|
440
|
+
const affordable = Number.parseInt(match[1].replace(/,/g, ""), 10);
|
|
441
|
+
if (!Number.isFinite(affordable) || affordable <= 0) return false;
|
|
442
|
+
|
|
443
|
+
// Leave a small buffer so slight input variance doesn't immediately re-fail.
|
|
444
|
+
const safetyBuffer = Math.min(64, Math.max(16, Math.floor(affordable * 0.1)));
|
|
445
|
+
const targetMaxTokens = Math.max(64, affordable - safetyBuffer);
|
|
446
|
+
const downgradedMaxTokens = Math.min(currentModel.maxTokens, targetMaxTokens);
|
|
447
|
+
if (downgradedMaxTokens >= currentModel.maxTokens) return false;
|
|
448
|
+
|
|
449
|
+
const downgradedModel = {
|
|
450
|
+
...currentModel,
|
|
451
|
+
maxTokens: downgradedMaxTokens,
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
this._deps.agent.setModel(downgradedModel);
|
|
455
|
+
this._deps.onModelChange(downgradedModel);
|
|
456
|
+
this._removeLastAssistantError();
|
|
457
|
+
|
|
458
|
+
this._deps.emit({
|
|
459
|
+
type: "fallback_provider_switch",
|
|
460
|
+
from: `${currentModel.provider}/${currentModel.id} (maxTokens=${currentModel.maxTokens})`,
|
|
461
|
+
to: `${downgradedModel.provider}/${downgradedModel.id} (maxTokens=${downgradedModel.maxTokens})`,
|
|
462
|
+
reason: `credit-aware retry: provider affordable cap ${affordable} tokens`,
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
this._deps.emit({
|
|
466
|
+
type: "auto_retry_start",
|
|
467
|
+
attempt: this._retryAttempt + 1,
|
|
468
|
+
maxAttempts: this._deps.settingsManager.getRetrySettings().maxRetries,
|
|
469
|
+
delayMs: 0,
|
|
470
|
+
errorMessage: `${message.errorMessage} (reducing max tokens)`,
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
this._scheduleContinue(retryGeneration);
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
|
|
418
477
|
/**
|
|
419
478
|
* Attempt to downgrade a long-context model (e.g. claude-opus-4-6[1m]) to its
|
|
420
479
|
* base model (claude-opus-4-6) when the account lacks the long-context billing
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// pi-coding-agent / CredentialCooldownError unit tests
|
|
2
|
+
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
|
3
|
+
|
|
4
|
+
import { describe, it } from "node:test";
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { CredentialCooldownError } from "./sdk.js";
|
|
7
|
+
|
|
8
|
+
// ─── CredentialCooldownError ──────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
describe("CredentialCooldownError", () => {
|
|
11
|
+
it("is an instance of Error", () => {
|
|
12
|
+
const err = new CredentialCooldownError("anthropic");
|
|
13
|
+
assert.ok(err instanceof Error);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("has name set to CredentialCooldownError", () => {
|
|
17
|
+
const err = new CredentialCooldownError("anthropic");
|
|
18
|
+
assert.equal(err.name, "CredentialCooldownError");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("has code set to AUTH_COOLDOWN", () => {
|
|
22
|
+
const err = new CredentialCooldownError("anthropic");
|
|
23
|
+
assert.equal(err.code, "AUTH_COOLDOWN");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("message includes the provider name", () => {
|
|
27
|
+
const err = new CredentialCooldownError("openai");
|
|
28
|
+
assert.ok(
|
|
29
|
+
err.message.includes("openai"),
|
|
30
|
+
`Expected message to include provider "openai", got: ${err.message}`,
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("message mentions cooldown window", () => {
|
|
35
|
+
const err = new CredentialCooldownError("anthropic");
|
|
36
|
+
assert.ok(
|
|
37
|
+
/cooldown window/i.test(err.message),
|
|
38
|
+
`Expected message to mention "cooldown window", got: ${err.message}`,
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("retryAfterMs is undefined when not provided", () => {
|
|
43
|
+
const err = new CredentialCooldownError("anthropic");
|
|
44
|
+
assert.equal(err.retryAfterMs, undefined);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("retryAfterMs holds the provided value when specified", () => {
|
|
48
|
+
const err = new CredentialCooldownError("anthropic", 30_000);
|
|
49
|
+
assert.equal(err.retryAfterMs, 30_000);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("retryAfterMs is 0 when explicitly passed as 0", () => {
|
|
53
|
+
const err = new CredentialCooldownError("anthropic", 0);
|
|
54
|
+
assert.equal(err.retryAfterMs, 0);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("code property is readonly and always AUTH_COOLDOWN regardless of provider", () => {
|
|
58
|
+
for (const provider of ["anthropic", "openai", "google", "openrouter"]) {
|
|
59
|
+
const err = new CredentialCooldownError(provider);
|
|
60
|
+
assert.equal(err.code, "AUTH_COOLDOWN", `code should be AUTH_COOLDOWN for provider "${provider}"`);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("different providers produce different messages", () => {
|
|
65
|
+
const err1 = new CredentialCooldownError("anthropic");
|
|
66
|
+
const err2 = new CredentialCooldownError("openai");
|
|
67
|
+
assert.notEqual(err1.message, err2.message);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("can be caught as an Error in a try/catch", () => {
|
|
71
|
+
let caught: unknown;
|
|
72
|
+
try {
|
|
73
|
+
throw new CredentialCooldownError("anthropic", 5_000);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
caught = e;
|
|
76
|
+
}
|
|
77
|
+
assert.ok(caught instanceof Error);
|
|
78
|
+
assert.ok(caught instanceof CredentialCooldownError);
|
|
79
|
+
assert.equal((caught as CredentialCooldownError).retryAfterMs, 5_000);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("code property is detectable via plain object check (cross-process pattern)", () => {
|
|
83
|
+
const err = new CredentialCooldownError("anthropic", 15_000);
|
|
84
|
+
// Simulate cross-process serialization: only plain properties survive JSON round-trip
|
|
85
|
+
const plain = { code: err.code, retryAfterMs: err.retryAfterMs, message: err.message };
|
|
86
|
+
assert.equal(plain.code, "AUTH_COOLDOWN");
|
|
87
|
+
assert.equal(plain.retryAfterMs, 15_000);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Structured error thrown when all credentials for a provider are in a
|
|
5
|
+
* backoff window. Carries typed metadata so callers (e.g. the auto-loop)
|
|
6
|
+
* can make informed retry decisions instead of string-matching the message.
|
|
7
|
+
*/
|
|
8
|
+
export class CredentialCooldownError extends Error {
|
|
9
|
+
readonly code = "AUTH_COOLDOWN" as const;
|
|
10
|
+
/** Milliseconds until the earliest credential becomes available, or undefined if unknown. */
|
|
11
|
+
readonly retryAfterMs: number | undefined;
|
|
12
|
+
|
|
13
|
+
constructor(provider: string, retryAfterMs?: number) {
|
|
14
|
+
super(
|
|
15
|
+
`All credentials for "${provider}" are in a cooldown window. ` +
|
|
16
|
+
`Please wait a moment and try again, or switch to a different provider.`,
|
|
17
|
+
);
|
|
18
|
+
this.name = "CredentialCooldownError";
|
|
19
|
+
this.retryAfterMs = retryAfterMs;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
2
22
|
import { Agent, type AgentMessage, type ThinkingLevel } from "@gsd/pi-agent-core";
|
|
3
23
|
import type { Message, Model } from "@gsd/pi-ai";
|
|
4
24
|
import { getAgentDir, getDocsPath } from "../config.js";
|
|
@@ -363,8 +383,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
363
383
|
|
|
364
384
|
// Retry key resolution with backoff to handle transient network failures
|
|
365
385
|
// (e.g., OAuth token refresh failing due to brief connectivity loss).
|
|
386
|
+
// When credentials are in a cooldown window (e.g., after a 429), wait
|
|
387
|
+
// for the backoff to expire instead of using fixed delays that are
|
|
388
|
+
// shorter than the cooldown duration.
|
|
366
389
|
const maxAttempts = 3;
|
|
367
390
|
const baseDelayMs = 2000;
|
|
391
|
+
const maxCooldownWaitMs = 60_000; // Don't wait longer than 60s (skip quota-exhausted 30min backoffs)
|
|
368
392
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
369
393
|
const key = await modelRegistry.getApiKeyForProvider(resolvedProvider);
|
|
370
394
|
if (key) return key;
|
|
@@ -379,7 +403,21 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
379
403
|
const isOAuth = model && modelRegistry.isUsingOAuth(model);
|
|
380
404
|
if (!hasAuth && !isOAuth) break;
|
|
381
405
|
|
|
382
|
-
//
|
|
406
|
+
// If credentials are in a cooldown window, wait for the earliest
|
|
407
|
+
// one to expire rather than using a fixed delay that's too short.
|
|
408
|
+
const backoffExpiry = modelRegistry.authStorage.getEarliestBackoffExpiry(resolvedProvider);
|
|
409
|
+
if (backoffExpiry !== undefined) {
|
|
410
|
+
const waitMs = backoffExpiry - Date.now() + 500; // 500ms buffer
|
|
411
|
+
if (waitMs > 0 && waitMs <= maxCooldownWaitMs) {
|
|
412
|
+
await new Promise(resolve => setTimeout(resolve, waitMs));
|
|
413
|
+
continue; // Retry immediately after cooldown clears
|
|
414
|
+
}
|
|
415
|
+
if (waitMs > maxCooldownWaitMs) {
|
|
416
|
+
break; // Quota-exhausted or very long backoff — don't block
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Standard exponential backoff for non-cooldown transient failures
|
|
383
421
|
await new Promise(resolve => setTimeout(resolve, baseDelayMs * attempt));
|
|
384
422
|
}
|
|
385
423
|
|
|
@@ -390,10 +428,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
390
428
|
// the retry handler and creating cascading error entries (#3429).
|
|
391
429
|
const hasAuth = modelRegistry.authStorage.hasAuth(resolvedProvider);
|
|
392
430
|
if (hasAuth) {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
);
|
|
431
|
+
const expiry = modelRegistry.authStorage.getEarliestBackoffExpiry(resolvedProvider);
|
|
432
|
+
const retryAfterMs = expiry !== undefined ? Math.max(0, expiry - Date.now()) : undefined;
|
|
433
|
+
throw new CredentialCooldownError(resolvedProvider, retryAfterMs);
|
|
397
434
|
}
|
|
398
435
|
const model = agent.state.model;
|
|
399
436
|
const isOAuth = model && modelRegistry.isUsingOAuth(model);
|
|
@@ -401,10 +438,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
401
438
|
// If credentials exist but are all in a backoff window (quota / rate-limit),
|
|
402
439
|
// surface a specific message instead of the misleading "Authentication failed".
|
|
403
440
|
if (modelRegistry.authStorage.areAllCredentialsBackedOff(resolvedProvider)) {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
);
|
|
441
|
+
const expiry = modelRegistry.authStorage.getEarliestBackoffExpiry(resolvedProvider);
|
|
442
|
+
const retryAfterMs = expiry !== undefined ? Math.max(0, expiry - Date.now()) : undefined;
|
|
443
|
+
throw new CredentialCooldownError(resolvedProvider, retryAfterMs);
|
|
408
444
|
}
|
|
409
445
|
throw new Error(
|
|
410
446
|
`Authentication failed for "${resolvedProvider}". ` +
|
package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/login-dialog.test.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { buildAuthUrlPresentation } from "../login-dialog.js";
|
|
4
|
+
|
|
5
|
+
describe("LoginDialogComponent", () => {
|
|
6
|
+
test("shows the full OAuth URL when the hyperlink label is truncated", () => {
|
|
7
|
+
const presentation = buildAuthUrlPresentation(
|
|
8
|
+
"https://auth.example.com/device?code=ABCD-1234&callback=oauth&state=needs-full-visibility",
|
|
9
|
+
52,
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
assert.notEqual(
|
|
13
|
+
presentation.displayUrl,
|
|
14
|
+
"https://auth.example.com/device?code=ABCD-1234&callback=oauth&state=needs-full-visibility",
|
|
15
|
+
"narrow terminals should still truncate the hyperlink label",
|
|
16
|
+
);
|
|
17
|
+
assert.ok(presentation.fullUrlLines.length > 1, "truncated URLs should expose wrapped full-url lines");
|
|
18
|
+
assert.match(presentation.fullUrlLines[0] ?? "", /https:\/\/auth\.example\.com\/device\?code=ABCD-1234&/);
|
|
19
|
+
assert.match(
|
|
20
|
+
presentation.fullUrlLines[presentation.fullUrlLines.length - 1] ?? "",
|
|
21
|
+
/state=needs-full-visibility/,
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -7,6 +7,27 @@ import { theme } from "../theme/theme.js";
|
|
|
7
7
|
import { DynamicBorder } from "./dynamic-border.js";
|
|
8
8
|
import { keyHint } from "./keybinding-hints.js";
|
|
9
9
|
|
|
10
|
+
function wrapPlainText(text: string, width: number): string[] {
|
|
11
|
+
const lines: string[] = [];
|
|
12
|
+
const safeWidth = Math.max(1, width);
|
|
13
|
+
for (let idx = 0; idx < text.length; idx += safeWidth) {
|
|
14
|
+
lines.push(text.slice(idx, idx + safeWidth));
|
|
15
|
+
}
|
|
16
|
+
return lines.length > 0 ? lines : [""];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function buildAuthUrlPresentation(url: string, terminalColumns: number): {
|
|
20
|
+
displayUrl: string;
|
|
21
|
+
fullUrlLines: string[];
|
|
22
|
+
} {
|
|
23
|
+
const maxUrlWidth = Math.max(20, terminalColumns - 4);
|
|
24
|
+
const displayUrl = truncateToWidth(url, maxUrlWidth);
|
|
25
|
+
return {
|
|
26
|
+
displayUrl,
|
|
27
|
+
fullUrlLines: displayUrl === url ? [] : wrapPlainText(url, maxUrlWidth),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
10
31
|
/**
|
|
11
32
|
* Login dialog component - replaces editor during OAuth login flow.
|
|
12
33
|
*
|
|
@@ -124,14 +145,21 @@ export class LoginDialogComponent extends Container implements Focusable {
|
|
|
124
145
|
|
|
125
146
|
// Truncate the visible URL text so it never wraps (which would break
|
|
126
147
|
// the OSC 8 hyperlink). The full URL is still the link target.
|
|
127
|
-
const
|
|
128
|
-
const displayUrl = truncateToWidth(url, maxUrlWidth);
|
|
148
|
+
const { displayUrl, fullUrlLines } = buildAuthUrlPresentation(url, this.tui.terminal.columns);
|
|
129
149
|
const urlLink = `\x1b]8;;${url}\x07${theme.fg("accent", displayUrl)}\x1b]8;;\x07`;
|
|
130
150
|
this.contentContainer.addChild(new Text(urlLink, 1, 0));
|
|
131
151
|
|
|
132
152
|
const clickHint = process.platform === "darwin" ? "Cmd+click to open" : "Ctrl+click to open";
|
|
133
153
|
this.contentContainer.addChild(new Text(theme.fg("dim", clickHint), 1, 0));
|
|
134
154
|
|
|
155
|
+
if (fullUrlLines.length > 0) {
|
|
156
|
+
this.contentContainer.addChild(new Spacer(1));
|
|
157
|
+
this.contentContainer.addChild(new Text(theme.fg("dim", "Full URL:"), 1, 0));
|
|
158
|
+
for (const line of fullUrlLines) {
|
|
159
|
+
this.contentContainer.addChild(new Text(theme.fg("dim", line), 1, 0));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
135
163
|
if (instructions) {
|
|
136
164
|
this.contentContainer.addChild(new Spacer(1));
|
|
137
165
|
this.contentContainer.addChild(new Text(theme.fg("warning", instructions), 1, 0));
|
|
@@ -120,7 +120,12 @@ export class ModelSelectorComponent extends Container implements Focusable {
|
|
|
120
120
|
this.settingsManager = settingsManager;
|
|
121
121
|
this.modelRegistry = modelRegistry;
|
|
122
122
|
this.scopedModels = scopedModels;
|
|
123
|
-
|
|
123
|
+
// Only land in "scoped" view when at least one scoped model has working
|
|
124
|
+
// auth — otherwise the user would see an empty picker (#unconfigured-models).
|
|
125
|
+
const hasReadyScopedModel = scopedModels.some((scoped) =>
|
|
126
|
+
modelRegistry.isProviderRequestReady(scoped.model.provider),
|
|
127
|
+
);
|
|
128
|
+
this.scope = hasReadyScopedModel ? "scoped" : "all";
|
|
124
129
|
this.onSelectCallback = onSelect;
|
|
125
130
|
this.onCancelCallback = onCancel;
|
|
126
131
|
|
|
@@ -215,12 +220,16 @@ export class ModelSelectorComponent extends Container implements Focusable {
|
|
|
215
220
|
}
|
|
216
221
|
|
|
217
222
|
this.allModels = this.sortModelsWithinProvider(models);
|
|
223
|
+
// Scoped models must also be filtered by provider readiness so users
|
|
224
|
+
// can't pick a scoped model whose provider has no API key / OAuth.
|
|
218
225
|
this.scopedModelItems = this.sortModelsWithinProvider(
|
|
219
|
-
this.scopedModels
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
226
|
+
this.scopedModels
|
|
227
|
+
.filter((scoped) => this.modelRegistry.isProviderRequestReady(scoped.model.provider))
|
|
228
|
+
.map((scoped) => ({
|
|
229
|
+
provider: scoped.model.provider,
|
|
230
|
+
id: scoped.model.id,
|
|
231
|
+
model: scoped.model,
|
|
232
|
+
})),
|
|
224
233
|
);
|
|
225
234
|
this.activeModels = this.scope === "scoped" ? this.scopedModelItems : this.allModels;
|
|
226
235
|
this.filteredModels = this.activeModels;
|