Merge remote-tracking branch 'upstream/main' into claude/model-agnostic-selection-rmDX3
# Conflicts: # packages/pi-coding-agent/src/core/model-resolver.ts # src/cli.ts
This commit is contained in:
commit
bafa4e483d
405 changed files with 28401 additions and 6656 deletions
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
15
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
|
@ -1,3 +1,16 @@
|
|||
## Linked issue
|
||||
|
||||
<!--
|
||||
PRs without a linked issue will be closed.
|
||||
Open or find an issue first: https://github.com/gsd-build/gsd-2/issues
|
||||
-->
|
||||
|
||||
Closes #<!-- issue number — required -->
|
||||
|
||||
- [ ] I have linked an issue above. I understand that PRs without a linked issue will be closed without review.
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
**What:** <!-- One sentence — what does this change? -->
|
||||
|
|
@ -10,7 +23,7 @@
|
|||
|
||||
## Why
|
||||
|
||||
<!-- The motivation. What problem does this solve? Link issues: Closes #123 -->
|
||||
<!-- The motivation. What problem does this solve? -->
|
||||
|
||||
## How
|
||||
|
||||
|
|
|
|||
58
.github/workflows/ci.yml
vendored
58
.github/workflows/ci.yml
vendored
|
|
@ -130,6 +130,15 @@ jobs:
|
|||
- name: Install web host dependencies
|
||||
run: npm --prefix web ci
|
||||
|
||||
- name: Cache Next.js build
|
||||
uses: useblacksmith/cache@v5
|
||||
with:
|
||||
path: web/.next/cache
|
||||
key: nextjs-${{ runner.os }}-${{ hashFiles('web/package-lock.json') }}-${{ hashFiles('web/app/**', 'web/components/**', 'web/lib/**', 'web/hooks/**') }}
|
||||
restore-keys: |
|
||||
nextjs-${{ runner.os }}-${{ hashFiles('web/package-lock.json') }}-
|
||||
nextjs-${{ runner.os }}-
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
|
|
@ -148,15 +157,45 @@ jobs:
|
|||
- name: Run package tests
|
||||
run: npm run test:packages
|
||||
|
||||
- name: Run integration tests
|
||||
run: npm run test:integration
|
||||
|
||||
- name: Check test coverage thresholds
|
||||
run: npm run test:coverage
|
||||
|
||||
windows-portability:
|
||||
integration-tests:
|
||||
timeout-minutes: 15
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.docs-only != 'true'
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '24'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
# Integration tests need the same compiled artifacts as the build job:
|
||||
# - dist/loader.js and packages/pi-coding-agent/dist/** from `npm run build`
|
||||
# - web/node_modules/.bin/next for tests that shell `build:web-host` at runtime
|
||||
# Duplicating the build here (instead of sharing artifacts via needs: build)
|
||||
# preserves wall-clock parallelism with the build job — see PR #4093.
|
||||
- name: Install web host dependencies
|
||||
run: npm --prefix web ci
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Run integration tests
|
||||
run: npm run test:integration
|
||||
|
||||
windows-portability:
|
||||
timeout-minutes: 25
|
||||
needs: detect-changes
|
||||
if: >-
|
||||
needs.detect-changes.outputs.docs-only != 'true'
|
||||
runs-on: blacksmith-4vcpu-windows-2025
|
||||
|
|
@ -180,12 +219,17 @@ jobs:
|
|||
- name: Typecheck extensions
|
||||
run: npm run typecheck:extensions
|
||||
|
||||
- name: Run unit tests
|
||||
run: npm run test:unit
|
||||
|
||||
- name: Run package tests
|
||||
run: npm run test:packages
|
||||
|
||||
- name: Run Windows portability tests
|
||||
run: >-
|
||||
node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs
|
||||
--experimental-strip-types --test
|
||||
src/tests/windows-portability.test.ts
|
||||
src/resources/extensions/gsd/tests/validate-directory.test.ts
|
||||
src/tests/integration/web-mode-windows-hide.test.ts
|
||||
|
||||
rtk-portability:
|
||||
timeout-minutes: 20
|
||||
needs: detect-changes
|
||||
|
|
|
|||
44
.github/workflows/pipeline.yml
vendored
44
.github/workflows/pipeline.yml
vendored
|
|
@ -24,6 +24,10 @@ jobs:
|
|||
credentials:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
env:
|
||||
BLACKSMITH_CACHE_TOKEN: ${{ env.BLACKSMITH_CACHE_TOKEN }}
|
||||
BLACKSMITH_CACHE_URL: ${{ env.BLACKSMITH_CACHE_URL }}
|
||||
GITHUB_REPO_NAME: ${{ github.repository }}
|
||||
outputs:
|
||||
dev-version: ${{ steps.stamp.outputs.version }}
|
||||
steps:
|
||||
|
|
@ -41,6 +45,15 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Cache Next.js build
|
||||
uses: useblacksmith/cache@v5
|
||||
with:
|
||||
path: web/.next/cache
|
||||
key: nextjs-${{ runner.os }}-${{ hashFiles('web/package-lock.json') }}-${{ hashFiles('web/app/**', 'web/components/**', 'web/lib/**', 'web/hooks/**') }}
|
||||
restore-keys: |
|
||||
nextjs-${{ runner.os }}-${{ hashFiles('web/package-lock.json') }}-
|
||||
nextjs-${{ runner.os }}-
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
|
|
@ -153,9 +166,18 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Cache Next.js build
|
||||
uses: useblacksmith/cache@v5
|
||||
with:
|
||||
path: web/.next/cache
|
||||
key: nextjs-${{ runner.os }}-${{ hashFiles('web/package-lock.json') }}-${{ hashFiles('web/app/**', 'web/components/**', 'web/lib/**', 'web/hooks/**') }}
|
||||
restore-keys: |
|
||||
nextjs-${{ runner.os }}-${{ hashFiles('web/package-lock.json') }}-
|
||||
nextjs-${{ runner.os }}-
|
||||
|
||||
- name: Run live LLM tests (optional)
|
||||
continue-on-error: true
|
||||
run: npm run test:live
|
||||
run: npm run test:live || echo "::warning::Live LLM tests failed — non-blocking, but worth investigating"
|
||||
env:
|
||||
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
|
|
@ -175,21 +197,26 @@ jobs:
|
|||
RELEASE_VERSION: ${{ steps.release.outputs.version }}
|
||||
run: node scripts/bump-version.mjs "$RELEASE_VERSION"
|
||||
|
||||
- name: Validate package files after version bump
|
||||
run: |
|
||||
node -e "require('./package.json')" && \
|
||||
node -e "require('./packages/pi-coding-agent/package.json')" && \
|
||||
node -e "require('./pkg/package.json')" && \
|
||||
echo "All package.json files are valid"
|
||||
|
||||
- name: Update CHANGELOG.md
|
||||
run: node scripts/update-changelog.mjs /tmp/changelog-entry.md
|
||||
|
||||
- name: Commit, tag, and push
|
||||
- name: Commit and tag release
|
||||
env:
|
||||
RELEASE_VERSION: ${{ steps.release.outputs.version }}
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add package.json package-lock.json CHANGELOG.md native/npm/*/package.json pkg/package.json packages/pi-coding-agent/package.json
|
||||
git add package.json package-lock.json web/package-lock.json CHANGELOG.md native/npm/*/package.json pkg/package.json packages/pi-coding-agent/package.json
|
||||
git commit -m "release: v${RELEASE_VERSION}"
|
||||
git tag "v${RELEASE_VERSION}"
|
||||
git pull --rebase origin main
|
||||
git push origin main
|
||||
git push origin "v${RELEASE_VERSION}"
|
||||
|
||||
- name: Build release
|
||||
run: npm run build
|
||||
|
|
@ -209,6 +236,13 @@ jobs:
|
|||
fi
|
||||
}
|
||||
|
||||
- name: Push release commit and tag
|
||||
env:
|
||||
RELEASE_VERSION: ${{ steps.release.outputs.version }}
|
||||
run: |
|
||||
git push origin main
|
||||
git push origin "v${RELEASE_VERSION}"
|
||||
|
||||
- name: Create GitHub Release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
|||
14
.mcp.json
14
.mcp.json
|
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"mcpServers": {
|
||||
"repowise": {
|
||||
"command": "repowise",
|
||||
"args": [
|
||||
"mcp",
|
||||
"/Users/jeremymcspadden/Github/gsd-2",
|
||||
"--transport",
|
||||
"stdio"
|
||||
],
|
||||
"description": "repowise: codebase intelligence \u2014 docs, graph, git signals, dead code, decisions"
|
||||
}
|
||||
}
|
||||
}
|
||||
191
CHANGELOG.md
191
CHANGELOG.md
|
|
@ -6,6 +6,188 @@ Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [2.73.0] - 2026-04-13
|
||||
|
||||
### Added
|
||||
- **pi-ai**: add Alibaba DashScope as standalone provider (#3891)
|
||||
- **gsd**: add layered depth enforcement to discuss.md (#4079)
|
||||
|
||||
### Fixed
|
||||
- **gsd**: reconcile stale slice rows and rebuild STATE.md before DB close (#3658)
|
||||
- **gsd**: block direct writes to gsd.db via hooks to prevent corruption (#3674)
|
||||
- **gsd**: break 3 circular dependencies in extension modules (#3730)
|
||||
- **claude-code**: default GSD subagents to bypassPermissions and pre-authorize safe built-ins (#4099 follow-up)
|
||||
- **gsd**: add memory pressure watchdog and persist stuck detection state (#3708)
|
||||
- **state**: prevent false degraded-mode warning when DB not yet initialized (#3922)
|
||||
- **async-jobs**: suppress stale follow-up for jobs consumed by await_job (#3787) (#3788)
|
||||
- **gsd**: rebuild STATE.md after unit completion (#3876)
|
||||
- **gsd**: let doctor heal dispatch fixable warnings (#3875)
|
||||
- **gsd**: preserve experimental preferences in merges (#3847)
|
||||
- **gsd**: heal legacy task arrays and evidence rows (#4027)
|
||||
- **gsd**: unlock depth verification outside guided flow (#4058)
|
||||
- **gsd**: preserve paused auto badge after provider pause (#4062)
|
||||
- **ollama**: add cloud auth support and resolve real context window via /api/show (#4017)
|
||||
- **security**: activate auth middleware and harden shutdown/update routes (#4023)
|
||||
- **gsd**: normalize workingDirectory prompt paths (#4057)
|
||||
- **claude-code**: pre-authorize workflow MCP tools so interactive acceptEdits mode stops blocking GSD commands
|
||||
- **cli**: resolve duplicate validateConfiguredModel and missing getPiDefaultModelAndProvider import
|
||||
- update GSD runtime ignore patterns for team mode (#2824)
|
||||
- **gsd**: prevent double frontmatter in task SUMMARY.md from projection re-render (#2818)
|
||||
- flush extension provider registrations before model resolution (#1923)
|
||||
- **gsd**: reset db-open attempted flag on close (#4024)
|
||||
- **gsd**: unblock mixed-dependency zero-dep slices (#4025)
|
||||
- **pi-tui**: filter kitty keypad private-use input (#4026)
|
||||
- **gsd**: disable db mmap on darwin (#4029)
|
||||
- **gsd**: reject empty roadmap stubs as milestone plans (#4063)
|
||||
- persist defaultProvider when user selects Claude Code CLI in onboarding (#4104)
|
||||
- **pi-ai**: filter unavailable github copilot models (#4031)
|
||||
- **claude-code**: wrap prompt history in XML tags to stop transcript fabrication
|
||||
- clean up MCP tool rendering in Claude Code CLI stream
|
||||
|
||||
### Changed
|
||||
- **pi-ai**: regenerate model registry from upstream APIs (#3887)
|
||||
- require linked issue in PR template (#4112)
|
||||
|
||||
## [2.72.0] - 2026-04-13
|
||||
|
||||
### Added
|
||||
- **agents**: add GSD phase guard to prevent subagent/phase conflicts
|
||||
- **agents**: add 8 specialist subagents and slim pro agents
|
||||
- **tui**: improve gsd overlays, shortcuts, and notification flows
|
||||
|
||||
### Fixed
|
||||
- **ci**: build artifacts in integration-tests job
|
||||
- **auto**: recover from OpenRouter credit affordability errors
|
||||
- **gsd**: cast unknown gate id in test to satisfy GateId type
|
||||
- **gsd**: route quality gates through a per-turn registry
|
||||
- **mcp**: expose every registered tool and fix SDK subpath resolution
|
||||
- **mcp**: resolve rebase regressions in stream-adapter
|
||||
- **mcp**: thread abort signals, restore tool fidelity, and fix subpath imports
|
||||
- **doctor**: skip key check for CLI-authenticated providers
|
||||
- **tui**: overlay subscription + Ctrl+Shift+P shortcut conflict
|
||||
- **models**: block unconfigured models from selection surfaces
|
||||
- **ollama**: clear footer status when provider unavailable
|
||||
- **gsd**: guard model override in minimal command contexts
|
||||
- **model**: require provider readiness for saved default selection
|
||||
- **gsd**: honor /gsd model as session override across dispatch
|
||||
- **gsd**: use milestone branch for merged worktree cleanup
|
||||
- **pi-coding-agent**: show full OAuth login URLs
|
||||
- **auto**: add structured cooldown error and bounded retry budget
|
||||
- **auto**: survive transient 429 credential cooldown in auto sessions
|
||||
- **pi-coding-agent**: match renderable tools case-insensitively
|
||||
- **headless**: keep idle timeout off during interactive tools
|
||||
- **claude-code-cli**: surface result text for success errors
|
||||
- **pi-ai**: use bearer auth for MiniMax Anthropic API
|
||||
- **gsd**: scope stuck-loop forensics to auto sessions
|
||||
- **gsd**: repair DB-only milestone unpark state
|
||||
- **gsd**: detach auto start from active turns
|
||||
- **cli**: include all internal node_modules entries in pnpm merged dir
|
||||
- **gsd**: enforce anti-fabrication turn-taking in discuss prompts
|
||||
- **cli**: address review findings for pnpm merged node_modules
|
||||
- **cli**: handle pnpm global installs by merging both node_modules roots
|
||||
- **gsd**: keep project db path after worktree enter
|
||||
- **gsd**: ignore prose inputs in pre-exec checks
|
||||
- **gsd**: read existing artifacts before write
|
||||
- **mcp-server**: use explicit sdk js subpaths
|
||||
- **cli**: preserve anthropic api provider
|
||||
- **gsd**: document flat task summary layout
|
||||
- **gsd**: require verification classes in validation prompts
|
||||
- **mcp-server**: open the DB for inline workflow tools
|
||||
- **gsd**: ignore pre-existing files in task ordering
|
||||
- **gsd**: detect property-value JSON invocation errors
|
||||
- **cli**: honor custom-provider defaults before onboarding
|
||||
- **gsd**: dedupe repeated notifications
|
||||
- **gsd**: open DB before bootstrap deriveState
|
||||
- **cli**: clean up stdin after sessions command readline interface closes
|
||||
- **gsd**: skip reverse dependents in dispatch fallback
|
||||
- **gsd**: classify plain connection-error as transient
|
||||
- **cli**: resolve hoisted node_modules for global installs
|
||||
- **pi-ai**: cast test tool fixtures to any for TSchema compatibility
|
||||
- **commands**: use specific validation reason in blocked-directory warning
|
||||
- **commands**: show friendly message when /gsd runs from $HOME instead of unhandled error
|
||||
|
||||
### Changed
|
||||
- **ci**: run integration tests in parallel with build
|
||||
- **ci**: cache Next.js build artifacts with Blacksmith cache
|
||||
- sync package-lock.json version fields to 2.68.0
|
||||
- **pi-ai**: add cache_control breakpoints to tool definitions
|
||||
|
||||
## [2.71.0] - 2026-04-11
|
||||
|
||||
### Added
|
||||
- **mcp-server**: add secure_env_collect tool via MCP form elicitation
|
||||
|
||||
### Fixed
|
||||
- **tui**: clear pinned output on message_end to prevent duplicate display
|
||||
- **tui**: clear pinned latest output on turn completion
|
||||
- **tui**: restore pinned output above editor during tool execution
|
||||
- TOCTOU file locking race conditions in event log and custom workflow graph
|
||||
- **tui**: mask secure extension input values in interactive mode
|
||||
- **claude-code**: harden MCP elicitation schema handling
|
||||
- **claude-code**: accept secure_env_collect MCP elicitation forms
|
||||
- **interactive**: keep MCP tool output ordered and restore secure prompt fallback
|
||||
- **interactive**: preserve MCP tool output stream ordering
|
||||
- **gsd**: resolve workflow MCP test typing regressions
|
||||
- **mcp**: return isError flag on workflow tool execution failures
|
||||
- **discuss**: add structuredQuestionsAvailable conditional to all gates
|
||||
- **discuss**: add multi-round questioning to new-project discuss phase
|
||||
- **gsd**: harden claude-code workflow MCP bootstrap
|
||||
- **web**: drop provisional pre-tool question text
|
||||
|
||||
### Changed
|
||||
- extract deriveStateFromDb logic into composable helpers
|
||||
- **pr**: drop web-layer changes from MCP stream-order fix
|
||||
|
||||
## [2.70.1] - 2026-04-11
|
||||
|
||||
### Fixed
|
||||
- **routing**: address codex review — complete interactive bypass and accurate banner
|
||||
- **routing**: skip dynamic routing for interactive dispatches, always show model changes (#3962)
|
||||
- **ci**: trim windows portability integration load
|
||||
- **ci**: narrow windows portability coverage
|
||||
- **ci**: skip validate-pack in windows portability job
|
||||
- **ci**: unblock windows portability follow-up
|
||||
- **windows**: harden portability across runtime and tooling
|
||||
- **auto**: use pathToFileURL for cross-platform import and reconcile regression test
|
||||
- **auto**: resolve resource-loader.js from GSD_PKG_ROOT on resume (#3949)
|
||||
- **mcp-server**: importLocalModule resolves src/ paths from dist/ context
|
||||
- **gsd**: surface scoped doctor health warnings
|
||||
- **gsd**: skip skipped slices in milestone prompts
|
||||
- **gsd**: handle doubled-backtick pre-exec paths
|
||||
- **update**: fetch latest version from registry
|
||||
|
||||
## [2.70.0] - 2026-04-10
|
||||
|
||||
### Added
|
||||
- **mcp-server**: expose ask_user_questions via elicitation
|
||||
|
||||
### Fixed
|
||||
- **pi-ai**: remove Anthropic OAuth flow for TOS compliance
|
||||
- **mcp-server**: hydrate model credentials into env
|
||||
- **mcp-server**: hydrate stored tool credentials on startup
|
||||
- **gsd**: auto-enable cmux when detected instead of prompting
|
||||
- **mcp-server**: URL scheme regex no longer matches Windows drive letters
|
||||
|
||||
## [2.69.0] - 2026-04-10
|
||||
|
||||
### Added
|
||||
- **gsd**: implement ADR-005 multi-model provider and tool strategy
|
||||
- **gsd**: complete ADR-004 capability-aware model routing implementation
|
||||
|
||||
### Fixed
|
||||
- **gsd**: add missing directories to codebase generator exclude list
|
||||
- **gsd**: wire ADR-005 infrastructure into live paths
|
||||
- **gsd**: replace empty catch with logWarning for CI compliance
|
||||
- **gsd**: merge enhanced context sections into standard template, clean up stale gate patterns
|
||||
- **gsd**: remove broken discuss-prepared template, inject briefs into discuss.md
|
||||
|
||||
## [2.68.1] - 2026-04-10
|
||||
|
||||
### Fixed
|
||||
- **ci**: update FILE-SYSTEM-MAP.md path after docs reorganization
|
||||
- **test**: update discord invite test path after docs reorganization
|
||||
- **gsd**: resolve resource-loader import for deployed extensions
|
||||
|
||||
## [2.68.0] - 2026-04-10
|
||||
|
||||
### Added
|
||||
|
|
@ -2664,7 +2846,14 @@ Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|||
### Changed
|
||||
- License updated to MIT
|
||||
|
||||
[Unreleased]: https://github.com/gsd-build/gsd-2/compare/v2.68.0...HEAD
|
||||
[Unreleased]: https://github.com/gsd-build/gsd-2/compare/v2.73.0...HEAD
|
||||
[2.73.0]: https://github.com/gsd-build/gsd-2/compare/v2.72.0...v2.73.0
|
||||
[2.72.0]: https://github.com/gsd-build/gsd-2/compare/v2.71.0...v2.72.0
|
||||
[2.71.0]: https://github.com/gsd-build/gsd-2/compare/v2.70.1...v2.71.0
|
||||
[2.70.1]: https://github.com/gsd-build/gsd-2/compare/v2.70.0...v2.70.1
|
||||
[2.70.0]: https://github.com/gsd-build/gsd-2/compare/v2.69.0...v2.70.0
|
||||
[2.69.0]: https://github.com/gsd-build/gsd-2/compare/v2.68.1...v2.69.0
|
||||
[2.68.1]: https://github.com/gsd-build/gsd-2/compare/v2.68.0...v2.68.1
|
||||
[2.68.0]: https://github.com/gsd-build/gsd-2/compare/v2.67.0...v2.68.0
|
||||
[2.67.0]: https://github.com/gsd-build/gsd-2/compare/v2.66.1...v2.67.0
|
||||
[2.66.1]: https://github.com/gsd-build/gsd-2/compare/v2.66.0...v2.66.1
|
||||
|
|
|
|||
165
README.md
165
README.md
|
|
@ -21,42 +21,74 @@ One command. Walk away. Come back to a built project with clean git history.
|
|||
|
||||
> GSD now provisions a managed [RTK](https://github.com/rtk-ai/rtk) binary on supported macOS, Linux, and Windows installs to compress shell-command output in `bash`, `async_bash`, `bg_shell`, and verification flows. GSD forces `RTK_TELEMETRY_DISABLED=1` for all managed invocations. Set `GSD_RTK_DISABLED=1` to disable the integration.
|
||||
|
||||
> **📋 NOTICE: New to Node on Mac?** If you installed Node.js via Homebrew, you may be running a development release instead of LTS. **[Read this guide](./docs/node-lts-macos.md)** to pin Node 24 LTS and avoid compatibility issues.
|
||||
> **📋 NOTICE: New to Node on Mac?** If you installed Node.js via Homebrew, you may be running a development release instead of LTS. **[Read this guide](./docs/user-docs/node-lts-macos.md)** to pin Node 24 LTS and avoid compatibility issues.
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## What's New in v2.68
|
||||
## What's New in v2.71
|
||||
|
||||
### MCP Workflow Tools
|
||||
### MCP Secure Env Collect
|
||||
|
||||
- **Full workflow over MCP** — slice replanning, milestone management, slice completion, task completion, and core planning tools are now exposed over MCP for external integrations.
|
||||
- **Transport-gated MCP** — workflow tool availability adapts to provider transport capabilities automatically.
|
||||
- **Write gate enforcement** — workflow MCP respects write gates, preventing unauthorized state mutations from external clients.
|
||||
- **Secure credential collection over MCP** — the new `secure_env_collect` tool uses MCP form elicitation to collect secrets (API keys, tokens) from external clients without exposing values in tool output. Masks input in interactive mode.
|
||||
- **Hardened elicitation schema** — MCP elicitation schema handling is stricter, with proper validation and fallback for providers that don't support forms.
|
||||
|
||||
### Reliability & Recovery
|
||||
### MCP Reliability
|
||||
|
||||
- **False degraded-mode fix** — eliminates spurious degraded-mode warnings when the DB hasn't been initialized yet.
|
||||
- **Stale session resume suppression** — prevents stale interrupted-session resume prompts from hijacking fresh sessions.
|
||||
- **Merge conflict recovery** — `autoCommitDirtyState` guarded with cwd restore on `MergeConflictError`.
|
||||
- **Auto-resume hardening** — `autoStartTime` restored on resume, managed resources resynced on auto resume.
|
||||
- **Stream ordering preserved** — MCP tool output now renders in the correct order, fixing interleaved output in Claude Code and other MCP clients.
|
||||
- **isError flag propagation** — workflow tool execution failures now correctly return `isError: true`, so MCP clients can distinguish success from failure.
|
||||
- **Multi-round discuss questions** — new-project discuss phase supports multi-round questioning with structured question gates.
|
||||
|
||||
### TUI & Developer Experience
|
||||
### Model Selection Hardening
|
||||
|
||||
- **Contextual tips system** — TUI and web terminal now surface contextual tips based on workflow state.
|
||||
- **Claude Code MCP streaming** — real-time streaming and tool output rendering for Claude Code MCP connections.
|
||||
- **Unconfigured models blocked** — models without a configured provider are filtered from selection surfaces, preventing dispatch failures.
|
||||
- **Provider readiness required** — saved default model selection now verifies the provider is ready before accepting it.
|
||||
- **Session override honored** — `/gsd model` selection persists as a session override across all dispatch phases.
|
||||
- **Minimal context guard** — model override logic is skipped in minimal command contexts where it doesn't apply.
|
||||
|
||||
### Infrastructure
|
||||
### Auto-Mode Resilience
|
||||
|
||||
- **Weekly model registry refresh** — CI workflow auto-regenerates the model registry on a weekly schedule.
|
||||
- **Codebase cache auto-refresh** — stale codebase cache is refreshed automatically without manual intervention.
|
||||
- **Credential cooldown recovery** — auto-mode survives transient 429 rate-limit responses with structured cooldown errors and a bounded retry budget.
|
||||
- **Fire-and-forget auto start** — auto start is detached from active turns to prevent blocking.
|
||||
- **Scoped forensics** — stuck-loop forensics are now scoped to auto sessions only, preventing false positives in interactive use.
|
||||
|
||||
### TUI Improvements
|
||||
|
||||
- **Overlay subscription fix** — resolved overlay subscription lifecycle and `Ctrl+Shift+P` shortcut conflict.
|
||||
- **Improved overlays and shortcuts** — GSD overlays, keyboard shortcuts, and notification flows redesigned for consistency.
|
||||
- **Pinned output restored** — pinned output bar displays above the editor during tool execution again.
|
||||
- **Turn completion cleanup** — pinned latest output is cleared on turn completion, preventing stale output from persisting.
|
||||
- **Secure input masking** — extension input values are masked in interactive mode when collecting secrets.
|
||||
|
||||
### Provider Fixes
|
||||
|
||||
- **Full OAuth login URLs** — OAuth login URLs are now displayed in full instead of being truncated.
|
||||
- **MiniMax bearer auth** — MiniMax Anthropic API requests use proper bearer authentication.
|
||||
- **Case-insensitive tool rendering** — renderable tool matching is now case-insensitive, fixing missed tool output.
|
||||
- **Headless idle timeout** — idle timeout is kept off during interactive tool execution in headless mode.
|
||||
|
||||
### Reliability & Internals
|
||||
|
||||
- **TOCTOU file locking** — race conditions in event log and custom workflow graph file locking are fixed with proper atomic lock acquisition.
|
||||
- **State derive refactor** — `deriveStateFromDb` god function extracted into composable, testable helpers.
|
||||
- **Windows portability** — hardened cross-platform portability across runtime, tooling, and CI.
|
||||
- **Model routing transparency** — dynamic routing is skipped for interactive dispatches; model changes are always shown in the banner.
|
||||
- **Capability-aware routing (ADR-004)** — full implementation of capability scoring, `before_model_select` hook, and task metadata extraction.
|
||||
- **Multi-model provider strategy (ADR-005)** — infrastructure for multi-provider model selection wired into live paths.
|
||||
- **Anti-fabrication guardrails** — discuss prompts enforce turn-taking to prevent fabricated user responses.
|
||||
- **Milestone worktree cleanup** — merged worktree cleanup uses the milestone branch instead of generic lookups.
|
||||
- **Tool cache control** — `cache_control` breakpoints added to tool definitions for improved prompt caching.
|
||||
|
||||
See the full [Changelog](./CHANGELOG.md) for details on every release.
|
||||
|
||||
<details>
|
||||
<summary>Previous highlights (v2.67 and earlier)</summary>
|
||||
<summary>Previous highlights (v2.70 and earlier)</summary>
|
||||
|
||||
- **Full workflow over MCP (v2.68)** — slice replanning, milestone management, slice completion, task completion, and core planning tools exposed over MCP
|
||||
- **Transport-gated MCP (v2.68)** — workflow tool availability adapts to provider transport capabilities automatically
|
||||
- **Contextual tips system (v2.68)** — TUI and web terminal surface contextual tips based on workflow state
|
||||
- **Ask user questions over MCP (v2.70)** — interactive questions exposed via elicitation for external integrations
|
||||
- **Tiered Context Injection (M005)** — relevance-scoped context with 65%+ token reduction
|
||||
- **Resilient transient error recovery** — defers to Core RetryHandler and fixes cmdCtx race conditions
|
||||
- **Anthropic subscription routing** — auto-routed through Claude Code CLI provider with proper display names
|
||||
|
|
@ -86,30 +118,35 @@ See the full [Changelog](./CHANGELOG.md) for details on every release.
|
|||
|
||||
## Documentation
|
||||
|
||||
Full documentation is available at **[gsd.build](https://gsd.build)** (powered by Mintlify) and in the [`docs/`](./docs/) directory:
|
||||
Full documentation is in the [`docs/`](./docs/) directory:
|
||||
|
||||
- **[Getting Started](./docs/getting-started.md)** — install, first run, basic usage
|
||||
- **[Auto Mode](./docs/auto-mode.md)** — autonomous execution deep-dive
|
||||
- **[Configuration](./docs/configuration.md)** — all preferences, models, git, and hooks
|
||||
- **[Custom Models](./docs/custom-models.md)** — add custom providers (Ollama, vLLM, LM Studio, proxies)
|
||||
- **[Token Optimization](./docs/token-optimization.md)** — profiles, context compression, complexity routing
|
||||
- **[Cost Management](./docs/cost-management.md)** — budgets, tracking, projections
|
||||
- **[Git Strategy](./docs/git-strategy.md)** — worktree isolation, branching, merge behavior
|
||||
- **[Parallel Orchestration](./docs/parallel-orchestration.md)** — run multiple milestones simultaneously
|
||||
- **[Working in Teams](./docs/working-in-teams.md)** — unique IDs, shared artifacts
|
||||
- **[Skills](./docs/skills.md)** — bundled skills, discovery, custom authoring
|
||||
- **[Commands Reference](./docs/commands.md)** — all commands and keyboard shortcuts
|
||||
- **[Architecture](./docs/architecture.md)** — system design and dispatch pipeline
|
||||
- **[Troubleshooting](./docs/troubleshooting.md)** — common issues, doctor, forensics, recovery
|
||||
- **[CI/CD Pipeline](./docs/ci-cd-pipeline.md)** — three-stage promotion pipeline (Dev → Test → Prod)
|
||||
- **[VS Code Extension](./vscode-extension/README.md)** — chat participant, sidebar dashboard, RPC integration
|
||||
- **[Visualizer](./docs/visualizer.md)** — workflow visualizer with stats and discussion status
|
||||
- **[Remote Questions](./docs/remote-questions.md)** — route decisions to Slack or Discord when human input is needed
|
||||
- **[Dynamic Model Routing](./docs/dynamic-model-routing.md)** — complexity-based model selection and budget pressure
|
||||
- **[Web Interface](./docs/web-interface.md)** — browser-based project management and real-time progress
|
||||
- **[Pipeline Simplification (ADR-003)](./docs/ADR-003-pipeline-simplification.md)** — merged research into planning, mechanical completion
|
||||
### User Guides
|
||||
|
||||
- **[Getting Started](./docs/user-docs/getting-started.md)** — install, first run, basic usage
|
||||
- **[Auto Mode](./docs/user-docs/auto-mode.md)** — autonomous execution deep-dive
|
||||
- **[Configuration](./docs/user-docs/configuration.md)** — all preferences, models, git, and hooks
|
||||
- **[Custom Models](./docs/user-docs/custom-models.md)** — add custom providers (Ollama, vLLM, LM Studio, proxies)
|
||||
- **[Token Optimization](./docs/user-docs/token-optimization.md)** — profiles, context compression, complexity routing
|
||||
- **[Cost Management](./docs/user-docs/cost-management.md)** — budgets, tracking, projections
|
||||
- **[Git Strategy](./docs/user-docs/git-strategy.md)** — worktree isolation, branching, merge behavior
|
||||
- **[Parallel Orchestration](./docs/user-docs/parallel-orchestration.md)** — run multiple milestones simultaneously
|
||||
- **[Working in Teams](./docs/user-docs/working-in-teams.md)** — unique IDs, shared artifacts
|
||||
- **[Skills](./docs/user-docs/skills.md)** — bundled skills, discovery, custom authoring
|
||||
- **[Commands Reference](./docs/user-docs/commands.md)** — all commands and keyboard shortcuts
|
||||
- **[Troubleshooting](./docs/user-docs/troubleshooting.md)** — common issues, doctor, forensics, recovery
|
||||
- **[Visualizer](./docs/user-docs/visualizer.md)** — workflow visualizer with stats and discussion status
|
||||
- **[Remote Questions](./docs/user-docs/remote-questions.md)** — route decisions to Slack or Discord when human input is needed
|
||||
- **[Dynamic Model Routing](./docs/user-docs/dynamic-model-routing.md)** — complexity-based model selection and budget pressure
|
||||
- **[Web Interface](./docs/user-docs/web-interface.md)** — browser-based project management and real-time progress
|
||||
- **[Migration from v1](./docs/user-docs/migration.md)** — `.planning` → `.gsd` migration
|
||||
- **[Docker Sandbox](./docker/README.md)** — run GSD auto mode in an isolated Docker container
|
||||
- **[Migration from v1](./docs/migration.md)** — `.planning` → `.gsd` migration
|
||||
|
||||
### Developer Docs
|
||||
|
||||
- **[Architecture](./docs/dev/architecture.md)** — system design and dispatch pipeline
|
||||
- **[CI/CD Pipeline](./docs/dev/ci-cd-pipeline.md)** — three-stage promotion pipeline (Dev → Test → Prod)
|
||||
- **[Pipeline Simplification (ADR-003)](./docs/dev/ADR-003-pipeline-simplification.md)** — merged research into planning, mechanical completion
|
||||
- **[VS Code Extension](./vscode-extension/README.md)** — chat participant, sidebar dashboard, RPC integration
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -325,7 +362,7 @@ gsd headless query
|
|||
gsd headless dispatch plan
|
||||
```
|
||||
|
||||
Headless auto-responds to interactive prompts, detects completion, and exits with structured codes: `0` complete, `1` error/timeout, `2` blocked. Auto-restarts on crash with exponential backoff. Use `gsd headless query` for instant, machine-readable state inspection — returns phase, next dispatch preview, and parallel worker costs as a single JSON object without spawning an LLM session. Pair with [remote questions](./docs/remote-questions.md) to route decisions to Slack or Discord when human input is needed.
|
||||
Headless auto-responds to interactive prompts, detects completion, and exits with structured codes: `0` complete, `1` error/timeout, `2` blocked. Auto-restarts on crash with exponential backoff. Use `gsd headless query` for instant, machine-readable state inspection — returns phase, next dispatch preview, and parallel worker costs as a single JSON object without spawning an LLM session. Pair with [remote questions](./docs/user-docs/remote-questions.md) to route decisions to Slack or Discord when human input is needed.
|
||||
|
||||
**Multi-session orchestration** — headless mode supports file-based IPC in `.gsd/parallel/` for coordinating multiple GSD workers across milestones. Build orchestrators that spawn, monitor, and budget-cap a fleet of GSD workers.
|
||||
|
||||
|
|
@ -498,9 +535,8 @@ auto_report: true
|
|||
| `verification_commands`| Array of shell commands to run after task execution (e.g., `["npm run lint", "npm run test"]`) |
|
||||
| `verification_auto_fix`| Auto-retry on verification failures (default: true) |
|
||||
| `verification_max_retries` | Max retries for verification failures (default: 2) |
|
||||
| `require_slice_discussion` | Pause auto-mode before each slice for human discussion review |
|
||||
| `phases.require_slice_discussion` | Pause auto-mode before each slice for human discussion review |
|
||||
| `auto_report` | Auto-generate HTML reports after milestone completion (default: true) |
|
||||
| `searchExcludeDirs` | Directories to exclude from `@` file autocomplete (e.g., `["node_modules", ".git", "dist"]`) |
|
||||
|
||||
### Agent Instructions
|
||||
|
||||
|
|
@ -530,7 +566,7 @@ token_profile: budget # or balanced (default), quality
|
|||
|
||||
**Budget pressure** graduates model downgrading as you approach your budget ceiling — 50%, 75%, and 90% thresholds progressively shift work to cheaper tiers.
|
||||
|
||||
See the full [Token Optimization Guide](./docs/token-optimization.md) for details.
|
||||
See the full [Token Optimization Guide](./docs/user-docs/token-optimization.md) for details.
|
||||
|
||||
### Bundled Tools
|
||||
|
||||
|
|
@ -565,13 +601,15 @@ GSD ships with 24 extensions, all loaded automatically:
|
|||
|
||||
### Bundled Agents
|
||||
|
||||
Three specialized subagents for delegated work:
|
||||
Five specialized subagents for delegated work:
|
||||
|
||||
| Agent | Role |
|
||||
| -------------- | ------------------------------------------------------------ |
|
||||
| **Scout** | Fast codebase recon — returns compressed context for handoff |
|
||||
| **Researcher** | Web research — finds and synthesizes current information |
|
||||
| **Worker** | General-purpose execution in an isolated context window |
|
||||
| Agent | Role |
|
||||
| ------------------- | ------------------------------------------------------------ |
|
||||
| **Scout** | Fast codebase recon — returns compressed context for handoff |
|
||||
| **Researcher** | Web research — finds and synthesizes current information |
|
||||
| **Worker** | General-purpose execution in an isolated context window |
|
||||
| **JavaScript Pro** | JavaScript-specialized execution and debugging |
|
||||
| **TypeScript Pro** | TypeScript-specialized execution and debugging |
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -585,8 +623,10 @@ The best practice for working in teams is to ensure unique milestone names acros
|
|||
# ── GSD: Runtime / Ephemeral (per-developer, per-session) ──────────────────
|
||||
# Crash detection sentinel — PID lock, written per auto-mode session
|
||||
.gsd/auto.lock
|
||||
# Auto-mode dispatch tracker — prevents re-running completed units
|
||||
.gsd/completed-units.json
|
||||
# Auto-mode dispatch tracker — prevents re-running completed units (includes archived per-milestone files)
|
||||
.gsd/completed-units*.json
|
||||
# State manifest — workflow state for recovery
|
||||
.gsd/state-manifest.json
|
||||
# Derived state cache — regenerated from plan/roadmap files on disk
|
||||
.gsd/STATE.md
|
||||
# Per-developer token/cost accumulator
|
||||
|
|
@ -599,6 +639,14 @@ The best practice for working in teams is to ensure unique milestone names acros
|
|||
.gsd/worktrees/
|
||||
# Parallel orchestration IPC and worker status
|
||||
.gsd/parallel/
|
||||
# SQLite database and WAL sidecars — checkpoint state, forensics data
|
||||
.gsd/gsd.db*
|
||||
# Daily-rotated event journal — structured event log for forensics
|
||||
.gsd/journal/
|
||||
# Doctor run history — diagnostic check results
|
||||
.gsd/doctor-history.jsonl
|
||||
# Workflow event log — structured event stream
|
||||
.gsd/event-log.jsonl
|
||||
# Generated HTML reports (regenerable via /gsd export --html)
|
||||
.gsd/reports/
|
||||
# Session-specific interrupted-work markers
|
||||
|
|
@ -646,9 +694,8 @@ gsd (CLI binary)
|
|||
├─ resource-loader.ts Syncs bundled extensions + agents to ~/.gsd/agent/
|
||||
└─ src/resources/
|
||||
├─ extensions/gsd/ Core GSD extension (auto, state, commands, ...)
|
||||
├─ extensions/... 23 supporting extensions
|
||||
├─ agents/ scout, researcher, worker
|
||||
├─ AGENTS.md Agent routing instructions
|
||||
├─ extensions/... 21 supporting extensions
|
||||
├─ agents/ scout, researcher, worker, javascript-pro, typescript-pro
|
||||
└─ GSD-WORKFLOW.md Manual bootstrap protocol
|
||||
```
|
||||
|
||||
|
|
@ -720,6 +767,14 @@ Use expensive models where quality matters (planning, complex execution) and che
|
|||
|
||||
---
|
||||
|
||||
## Ecosystem
|
||||
|
||||
| Project | Description |
|
||||
| ------- | ----------- |
|
||||
| [GSD2 Config Utility](https://github.com/jeremymcs/gsd2-config) | Standalone configuration tool for managing GSD preferences, providers, and API keys |
|
||||
|
||||
---
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://star-history.com/#gsd-build/gsd-2&Date">
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ Welcome to the GSD documentation. This covers everything from getting started to
|
|||
|
||||
Guides for installing, configuring, and using GSD day-to-day. Located in [`user-docs/`](./user-docs/).
|
||||
|
||||
Simplified Chinese translation: [`zh-CN/`](./zh-CN/).
|
||||
|
||||
| Guide | Description |
|
||||
|-------|-------------|
|
||||
| [Getting Started](./user-docs/getting-started.md) | Installation, first run, and basic usage |
|
||||
|
|
|
|||
67
docs/dev/ADR-005-multi-model-provider-tool-strategy.md
Normal file
67
docs/dev/ADR-005-multi-model-provider-tool-strategy.md
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# ADR-005: Multi-Model, Multi-Provider, and Tool Strategy
|
||||
|
||||
**Status:** Accepted
|
||||
**Date:** 2026-03-27
|
||||
**Deciders:** Jeremy McSpadden
|
||||
**Related:** ADR-004 (capability-aware model routing), ADR-003 (pipeline simplification), [Issue #2790](https://github.com/gsd-build/gsd-2/issues/2790)
|
||||
|
||||
## Context
|
||||
|
||||
PR #2755 lands capability-aware model routing (ADR-004), extending the router from a one-dimensional complexity-tier system to a two-dimensional system that scores models across 7 capability dimensions. GSD can now intelligently pick the best model for a task from a heterogeneous pool.
|
||||
|
||||
But model selection is only one piece of the multi-model puzzle. The system faces structural gaps as users configure diverse provider pools:
|
||||
|
||||
1. **Tool compatibility is assumed, not verified** — Every registered tool is sent to every model regardless of provider capabilities.
|
||||
2. **No tool-aware model routing** — ADR-004 scores 7 capability dimensions but none encode whether a model can actually use the tools a task requires.
|
||||
3. **Provider failover loses context fidelity** — Cross-provider switches silently degrade conversation quality (thinking blocks dropped, tool IDs remapped).
|
||||
4. **Tool availability is static across a session** — The same tools are presented regardless of the selected model's capabilities.
|
||||
5. **No provider capability registry** — Provider quirks are scattered across `*-shared.ts` files.
|
||||
|
||||
## Decision
|
||||
|
||||
Introduce a provider capability registry and tool compatibility layer that integrates with ADR-004's capability-aware model router.
|
||||
|
||||
### Design Principles
|
||||
|
||||
1. **Layered on ADR-004, not replacing it.** Capability scoring remains primary. This adds tool compatibility as a hard constraint.
|
||||
2. **Hard constraints filter; soft scores rank.** Tool support is binary — it filters the eligible set before scoring.
|
||||
3. **Provider knowledge is declarative, not scattered.** Provider capabilities move to an explicit registry.
|
||||
4. **Tool sets adapt to model capabilities.** Active tool set adjusts when the router selects a different model.
|
||||
5. **Graceful degradation preserved.** Unknown providers get full tool access — same as today.
|
||||
|
||||
### Implementation Phases
|
||||
|
||||
1. **Phase 1:** Provider Capabilities Registry (`packages/pi-ai/src/providers/provider-capabilities.ts`)
|
||||
2. **Phase 2:** Tool Compatibility Metadata (extend `ToolDefinition` with `compatibility` field)
|
||||
3. **Phase 3:** Tool-compatibility filter in routing pipeline + `ProviderSwitchReport` in `transform-messages.ts`
|
||||
4. **Phase 4:** `adjustToolSet` extension hook
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Eliminates silent tool failures when routing to incompatible providers
|
||||
- Makes cross-provider routing safe by default
|
||||
- Provider knowledge becomes queryable (registry vs scattered code)
|
||||
- Cross-provider context loss becomes visible via `ProviderSwitchReport`
|
||||
|
||||
### Negative
|
||||
- More metadata to maintain (provider capabilities, tool compatibility)
|
||||
- Tool filtering adds a pipeline step (sub-millisecond, O(models × tools))
|
||||
- Risk of over-filtering (mitigated: opt-in per tool, permissive defaults)
|
||||
|
||||
### Neutral
|
||||
- Existing behavior unchanged without metadata
|
||||
- ADR-004 scoring is unmodified
|
||||
- Provider implementations simplify over time as registry replaces scattered workarounds
|
||||
|
||||
## Appendix: Architecture Reference
|
||||
|
||||
| File | Role |
|
||||
|------|------|
|
||||
| `packages/pi-ai/src/providers/register-builtins.ts` | Provider registration |
|
||||
| `packages/pi-ai/src/providers/*-shared.ts` | Provider-specific handling |
|
||||
| `packages/pi-ai/src/providers/transform-messages.ts` | Cross-provider normalization |
|
||||
| `packages/pi-ai/src/types.ts` | Core types |
|
||||
| `packages/pi-coding-agent/src/core/extensions/types.ts` | ToolDefinition, ExtensionAPI |
|
||||
| `src/resources/extensions/gsd/model-router.ts` | Capability scoring (ADR-004) |
|
||||
| `src/resources/extensions/gsd/auto-model-selection.ts` | Model selection orchestration |
|
||||
|
|
@ -86,18 +86,15 @@ Implication for GSD2:
|
|||
|
||||
These are directionally correct because GSD is using the user's own local Claude Code installation as the authenticated Anthropic surface.
|
||||
|
||||
### Medium/high-risk pieces
|
||||
### Medium/high-risk pieces — RESOLVED
|
||||
|
||||
- `packages/pi-ai/src/utils/oauth/anthropic.ts`
|
||||
Still implements a first-party-looking Anthropic OAuth flow for GSD itself using `claude.ai/oauth/authorize` and `platform.claude.com/v1/oauth/token`.
|
||||
- `packages/pi-ai/src/utils/oauth/index.ts`
|
||||
Still registers `anthropicOAuthProvider` as a built-in OAuth provider.
|
||||
- `src/web/onboarding-service.ts`
|
||||
Still advertises Anthropic as `supportsOAuth: true`, which keeps the web onboarding surface inconsistent with the TUI stance.
|
||||
- `packages/daemon/src/orchestrator.ts`
|
||||
Reads Anthropic OAuth credentials from `~/.gsd/agent/auth.json`, refreshes them, and then uses the access token for Anthropic API calls.
|
||||
All Anthropic OAuth code paths have been removed:
|
||||
|
||||
The key risk is not just stale UI. The repo still contains code paths where GSD can behave as a third-party Anthropic OAuth client and then convert that credential into direct API access.
|
||||
- `packages/pi-ai/src/utils/oauth/anthropic.ts` — **Deleted.** No longer implements Anthropic OAuth flow.
|
||||
- `packages/pi-ai/src/utils/oauth/index.ts` — **Updated.** `anthropicOAuthProvider` removed from built-in registry.
|
||||
- `src/web/onboarding-service.ts` — **Updated.** Anthropic set to `supportsOAuth: false`.
|
||||
- `packages/daemon/src/orchestrator.ts` — **Updated.** OAuth token refresh removed; requires `ANTHROPIC_API_KEY` env var.
|
||||
- `packages/pi-ai/src/providers/anthropic.ts` — **Updated.** OAuth client branch removed; `isOAuthToken` always returns false.
|
||||
|
||||
## Recommended Policy For GSD2
|
||||
|
||||
|
|
@ -149,14 +146,14 @@ This is the best long-term UX because it separates:
|
|||
- API-billed usage
|
||||
- cloud-routed usage
|
||||
|
||||
## Concrete Repo Follow-ups
|
||||
## Concrete Repo Follow-ups — COMPLETED
|
||||
|
||||
1. Delete or disable `packages/pi-ai/src/utils/oauth/anthropic.ts`.
|
||||
2. Remove `anthropicOAuthProvider` from `packages/pi-ai/src/utils/oauth/index.ts`.
|
||||
3. Change `src/web/onboarding-service.ts` so Anthropic does not claim OAuth support.
|
||||
4. Audit `packages/daemon/src/orchestrator.ts` and any other callers that treat Anthropic OAuth access tokens as API credentials.
|
||||
5. Update docs/UI labels to prefer `anthropic-api` for direct API usage and `claude-code` for subscription usage.
|
||||
6. Add tests that fail if Anthropic subscription OAuth is reintroduced through the onboarding/provider registry.
|
||||
1. ~~Delete or disable `packages/pi-ai/src/utils/oauth/anthropic.ts`.~~ **Done** — file deleted.
|
||||
2. ~~Remove `anthropicOAuthProvider` from `packages/pi-ai/src/utils/oauth/index.ts`.~~ **Done.**
|
||||
3. ~~Change `src/web/onboarding-service.ts` so Anthropic does not claim OAuth support.~~ **Done.**
|
||||
4. ~~Audit `packages/daemon/src/orchestrator.ts` and any other callers that treat Anthropic OAuth access tokens as API credentials.~~ **Done** — daemon now requires `ANTHROPIC_API_KEY`.
|
||||
5. ~~Update docs/UI labels to prefer `anthropic-api` for direct API usage and `claude-code` for subscription usage.~~ **Done** — providers.md and getting-started.md updated.
|
||||
6. Add tests that fail if Anthropic subscription OAuth is reintroduced through the onboarding/provider registry. — **TODO.**
|
||||
|
||||
## Decision Rule
|
||||
|
||||
|
|
|
|||
|
|
@ -148,6 +148,7 @@ Recommended verification order:
|
|||
|
||||
- Use absolute paths for local executables and scripts when possible.
|
||||
- For `stdio` servers, prefer setting required environment variables directly in the MCP config instead of relying on an interactive shell profile.
|
||||
- GSD and `gsd-mcp-server` both hydrate supported model and tool keys saved in `~/.gsd/agent/auth.json`, so MCP configs can safely reference them through `${ENV_VAR}` placeholders without committing raw credentials.
|
||||
- If a server is team-shared and safe to commit, `.mcp.json` is usually the better home.
|
||||
- If a server depends on machine-local paths, personal services, or local-only secrets, prefer `.gsd/mcp.json`.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,74 +1,311 @@
|
|||
# Getting Started
|
||||
# Getting Started with GSD
|
||||
|
||||
## Install
|
||||
GSD is an AI coding agent that handles planning, execution, verification, and shipping so you can focus on what to build. This guide walks you through installation on macOS, Windows, and Linux, then gets you running your first session.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
| Requirement | Minimum | Recommended |
|
||||
|-------------|---------|-------------|
|
||||
| **[Node.js](https://nodejs.org/)** | 22.0.0 | 24 LTS |
|
||||
| **[Git](https://git-scm.com/)** | 2.20+ | Latest |
|
||||
| **LLM API key** | Any supported provider | Anthropic (Claude) |
|
||||
|
||||
Don't have Node.js or Git yet? Follow the OS-specific instructions below.
|
||||
|
||||
---
|
||||
|
||||
## Install by Operating System
|
||||
|
||||
### macOS
|
||||
|
||||
> **Downloads:** [Node.js](https://nodejs.org/) | [Git](https://git-scm.com/download/mac) | [Homebrew](https://brew.sh/)
|
||||
|
||||
**Step 1 — Install Homebrew** (skip if you already have it):
|
||||
|
||||
```bash
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
```
|
||||
|
||||
**Step 2 — Install Node.js and Git:**
|
||||
|
||||
```bash
|
||||
brew install node git
|
||||
```
|
||||
|
||||
**Step 3 — Verify dependencies are installed:**
|
||||
|
||||
```bash
|
||||
node --version # should print v22.x or higher
|
||||
git --version # should print 2.20+
|
||||
```
|
||||
|
||||
**Step 4 — Install GSD:**
|
||||
|
||||
```bash
|
||||
npm install -g gsd-pi
|
||||
```
|
||||
|
||||
Requires Node.js ≥ 22.0.0 (24 LTS recommended) and Git.
|
||||
|
||||
> **`command not found: gsd`?** Your shell may not have npm's global bin directory in `$PATH`. Run `npm prefix -g` to find it, then add `$(npm prefix -g)/bin` to your PATH. See [Troubleshooting](./troubleshooting.md#command-not-found-gsd-after-install) for details.
|
||||
|
||||
GSD checks for updates once every 24 hours. When a new version is available, you'll see an interactive prompt at startup with the option to update immediately or skip. You can also update from within a session with `/gsd update`.
|
||||
|
||||
### Set up API keys
|
||||
|
||||
If you use a non-Anthropic model, you'll need a search API key for web search. Run `/gsd config` to set keys globally — they're saved to `~/.gsd/agent/auth.json` and apply to all projects:
|
||||
**Step 5 — Set up your LLM provider:**
|
||||
|
||||
```bash
|
||||
# Inside any GSD session:
|
||||
/gsd config
|
||||
```
|
||||
# Option A: Set an environment variable (Anthropic recommended)
|
||||
export ANTHROPIC_API_KEY="sk-ant-..."
|
||||
|
||||
See [Global API Keys](./configuration.md#global-api-keys-gsd-config) for details on supported keys.
|
||||
|
||||
### Set up custom MCP servers
|
||||
|
||||
If you want GSD to call local or external MCP servers, add project-local config in `.mcp.json` or `.gsd/mcp.json`.
|
||||
|
||||
See [Configuration → MCP Servers](./configuration.md#mcp-servers) for examples and verification steps.
|
||||
|
||||
### VS Code Extension
|
||||
|
||||
GSD is also available as a VS Code extension. Install from the marketplace (publisher: FluxLabs) or search for "GSD" in VS Code extensions. The extension provides:
|
||||
|
||||
- **`@gsd` chat participant** — talk to the agent in VS Code Chat
|
||||
- **Sidebar dashboard** — connection status, model info, token usage, quick actions
|
||||
- **Full command palette** — start/stop agent, switch models, export sessions
|
||||
|
||||
The CLI (`gsd-pi`) must be installed first — the extension connects to it via RPC.
|
||||
|
||||
### Web Interface
|
||||
|
||||
GSD also has a browser-based interface. Run `gsd --web` to start a local web server with a visual dashboard, real-time progress, and multi-project support. See [Web Interface](./web-interface.md) for details.
|
||||
|
||||
## First Launch
|
||||
|
||||
Run `gsd` in any directory:
|
||||
|
||||
```bash
|
||||
gsd
|
||||
```
|
||||
|
||||
GSD displays a welcome screen showing your version, active model, and available tool keys. Then on first launch, it runs a setup wizard:
|
||||
|
||||
1. **LLM Provider** — select from 20+ providers (Anthropic, OpenAI, Google, OpenRouter, GitHub Copilot, Amazon Bedrock, Azure, and more). OAuth flows handle Claude Max and Copilot subscriptions automatically; otherwise paste an API key.
|
||||
2. **Tool API Keys** (optional) — Brave Search, Context7, Jina, Slack, Discord. Press Enter to skip any.
|
||||
|
||||
If you have an existing Pi installation, provider credentials are imported automatically.
|
||||
|
||||
For detailed setup instructions for specific providers (OpenRouter, Ollama, LM Studio, vLLM, and more), see the [Provider Setup Guide](./providers.md).
|
||||
|
||||
Re-run the wizard anytime with:
|
||||
|
||||
```bash
|
||||
# Option B: Use the built-in config wizard
|
||||
gsd config
|
||||
```
|
||||
|
||||
## Choose a Model
|
||||
To persist the key, add the export line to `~/.zshrc`:
|
||||
|
||||
GSD auto-selects a default model after login. Switch later with:
|
||||
```bash
|
||||
echo 'export ANTHROPIC_API_KEY="sk-ant-..."' >> ~/.zshrc
|
||||
source ~/.zshrc
|
||||
```
|
||||
|
||||
See [Provider Setup Guide](./providers.md) for all 20+ supported providers.
|
||||
|
||||
**Step 6 — Launch GSD:**
|
||||
|
||||
```bash
|
||||
cd ~/my-project # navigate to any project
|
||||
gsd # start a session
|
||||
```
|
||||
|
||||
**Step 7 — Verify everything works:**
|
||||
|
||||
```bash
|
||||
gsd --version # prints the installed version
|
||||
```
|
||||
|
||||
Inside the session, type `/model` to confirm your LLM is connected.
|
||||
|
||||
> **Apple Silicon PATH fix:** If `gsd` isn't found after install, npm's global bin may not be in your PATH:
|
||||
> ```bash
|
||||
> echo 'export PATH="$(npm prefix -g)/bin:$PATH"' >> ~/.zshrc
|
||||
> source ~/.zshrc
|
||||
> ```
|
||||
|
||||
> **oh-my-zsh conflict:** The oh-my-zsh git plugin defines `alias gsd='git svn dcommit'`. Fix with `unalias gsd 2>/dev/null` in `~/.zshrc`, or use `gsd-cli` instead.
|
||||
|
||||
---
|
||||
|
||||
### Windows
|
||||
|
||||
> **Downloads:** [Node.js](https://nodejs.org/) | [Git for Windows](https://git-scm.com/download/win) | [Windows Terminal](https://aka.ms/terminal)
|
||||
|
||||
#### Option A: winget (recommended for Windows 10/11)
|
||||
|
||||
**Step 1 — Install Node.js and Git:**
|
||||
|
||||
```powershell
|
||||
winget install OpenJS.NodeJS.LTS
|
||||
winget install Git.Git
|
||||
```
|
||||
|
||||
**Step 2 — Restart your terminal** (close and reopen PowerShell or Windows Terminal).
|
||||
|
||||
**Step 3 — Verify dependencies are installed:**
|
||||
|
||||
```powershell
|
||||
node --version # should print v22.x or higher
|
||||
git --version # should print 2.20+
|
||||
```
|
||||
|
||||
**Step 4 — Install GSD:**
|
||||
|
||||
```powershell
|
||||
npm install -g gsd-pi
|
||||
```
|
||||
|
||||
**Step 5 — Set up your LLM provider:**
|
||||
|
||||
```powershell
|
||||
# Option A: Set an environment variable (current session)
|
||||
$env:ANTHROPIC_API_KEY = "sk-ant-..."
|
||||
|
||||
# Option B: Use the built-in config wizard
|
||||
gsd config
|
||||
```
|
||||
|
||||
To persist the key permanently, add it via System Settings > Environment Variables, or run:
|
||||
|
||||
```powershell
|
||||
[System.Environment]::SetEnvironmentVariable("ANTHROPIC_API_KEY", "sk-ant-...", "User")
|
||||
```
|
||||
|
||||
See [Provider Setup Guide](./providers.md) for all 20+ supported providers.
|
||||
|
||||
**Step 6 — Launch GSD:**
|
||||
|
||||
```powershell
|
||||
cd C:\Users\you\my-project # navigate to any project
|
||||
gsd # start a session
|
||||
```
|
||||
|
||||
**Step 7 — Verify everything works:**
|
||||
|
||||
```powershell
|
||||
gsd --version # prints the installed version
|
||||
```
|
||||
|
||||
Inside the session, type `/model` to confirm your LLM is connected.
|
||||
|
||||
#### Option B: Manual install
|
||||
|
||||
1. Download and install [Node.js LTS](https://nodejs.org/) — check **"Add to PATH"** during setup
|
||||
2. Download and install [Git for Windows](https://git-scm.com/download/win) — use default options
|
||||
3. Open a **new** terminal, then follow Steps 3-7 above
|
||||
|
||||
> **Windows tips:**
|
||||
> - Use **Windows Terminal** or **PowerShell** for the best experience. Command Prompt works but has limited color support.
|
||||
> - If `gsd` isn't recognized, restart your terminal. Windows needs a fresh terminal to pick up new PATH entries.
|
||||
> - **WSL2** also works — install WSL, then follow the Linux instructions inside your distro.
|
||||
|
||||
---
|
||||
|
||||
### Linux
|
||||
|
||||
> **Downloads:** [Node.js](https://nodejs.org/) | [Git](https://git-scm.com/download/linux) | [nvm](https://github.com/nvm-sh/nvm)
|
||||
|
||||
Pick your distro, then follow the steps.
|
||||
|
||||
#### Ubuntu / Debian
|
||||
|
||||
**Step 1 — Install Node.js and Git:**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs git
|
||||
```
|
||||
|
||||
#### Fedora / RHEL / CentOS
|
||||
|
||||
**Step 1 — Install Node.js and Git:**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://rpm.nodesource.com/setup_24.x | sudo bash -
|
||||
sudo dnf install -y nodejs git
|
||||
```
|
||||
|
||||
#### Arch Linux
|
||||
|
||||
**Step 1 — Install Node.js and Git:**
|
||||
|
||||
```bash
|
||||
sudo pacman -S nodejs npm git
|
||||
```
|
||||
|
||||
#### Using nvm (any distro)
|
||||
|
||||
**Step 1 — Install nvm, then Node.js:**
|
||||
|
||||
```bash
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash
|
||||
source ~/.bashrc # or ~/.zshrc
|
||||
nvm install 24
|
||||
nvm use 24
|
||||
```
|
||||
|
||||
#### All distros: Steps 2-7
|
||||
|
||||
**Step 2 — Verify dependencies are installed:**
|
||||
|
||||
```bash
|
||||
node --version # should print v22.x or higher
|
||||
git --version # should print 2.20+
|
||||
```
|
||||
|
||||
**Step 3 — Install GSD:**
|
||||
|
||||
```bash
|
||||
npm install -g gsd-pi
|
||||
```
|
||||
|
||||
**Step 4 — Set up your LLM provider:**
|
||||
|
||||
```bash
|
||||
# Option A: Set an environment variable (Anthropic recommended)
|
||||
export ANTHROPIC_API_KEY="sk-ant-..."
|
||||
|
||||
# Option B: Use the built-in config wizard
|
||||
gsd config
|
||||
```
|
||||
|
||||
To persist the key, add the export line to `~/.bashrc` (or `~/.zshrc`):
|
||||
|
||||
```bash
|
||||
echo 'export ANTHROPIC_API_KEY="sk-ant-..."' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
See [Provider Setup Guide](./providers.md) for all 20+ supported providers.
|
||||
|
||||
**Step 5 — Launch GSD:**
|
||||
|
||||
```bash
|
||||
cd ~/my-project # navigate to any project
|
||||
gsd # start a session
|
||||
```
|
||||
|
||||
**Step 6 — Verify everything works:**
|
||||
|
||||
```bash
|
||||
gsd --version # prints the installed version
|
||||
```
|
||||
|
||||
Inside the session, type `/model` to confirm your LLM is connected.
|
||||
|
||||
> **Permission errors on `npm install -g`?** Don't use `sudo npm`. Fix npm's global directory instead:
|
||||
> ```bash
|
||||
> mkdir -p ~/.npm-global
|
||||
> npm config set prefix '~/.npm-global'
|
||||
> echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> ~/.bashrc
|
||||
> source ~/.bashrc
|
||||
> npm install -g gsd-pi
|
||||
> ```
|
||||
|
||||
---
|
||||
|
||||
### Docker (any OS)
|
||||
|
||||
> **Downloads:** [Docker Desktop](https://www.docker.com/products/docker-desktop/)
|
||||
|
||||
Run GSD in an isolated sandbox without installing Node.js on your host.
|
||||
|
||||
**Step 1 — Install Docker Desktop** (4.58+ required).
|
||||
|
||||
**Step 2 — Clone the GSD repo:**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/gsd-build/gsd-2.git
|
||||
cd gsd-2/docker
|
||||
```
|
||||
|
||||
**Step 3 — Create and enter a sandbox:**
|
||||
|
||||
```bash
|
||||
docker sandbox create --template . --name gsd-sandbox
|
||||
docker sandbox exec -it gsd-sandbox bash
|
||||
```
|
||||
|
||||
**Step 4 — Set your API key and run GSD:**
|
||||
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY="sk-ant-..."
|
||||
gsd auto "implement the feature described in issue #42"
|
||||
```
|
||||
|
||||
See [Docker Sandbox docs](../../docker/README.md) for full configuration, resource limits, and compose files.
|
||||
|
||||
---
|
||||
|
||||
## After Installation
|
||||
|
||||
### Choose a Model
|
||||
|
||||
GSD auto-selects a default model after provider setup. Switch anytime inside a session:
|
||||
|
||||
```
|
||||
/model
|
||||
|
|
@ -76,18 +313,20 @@ GSD auto-selects a default model after login. Switch later with:
|
|||
|
||||
Or configure per-phase models in preferences — see [Configuration](./configuration.md).
|
||||
|
||||
---
|
||||
|
||||
## Two Ways to Work
|
||||
|
||||
### Step Mode — `/gsd`
|
||||
|
||||
Type `/gsd` inside a session. GSD executes one unit of work at a time, pausing between each with a wizard showing what completed and what's next.
|
||||
|
||||
- **No `.gsd/` directory** → starts a discussion flow to capture your project vision
|
||||
- **Milestone exists, no roadmap** → discuss or research the milestone
|
||||
- **Roadmap exists, slices pending** → plan the next slice or execute a task
|
||||
- **Mid-task** → resume where you left off
|
||||
- **No `.gsd/` directory** — starts a discussion flow to capture your project vision
|
||||
- **Milestone exists, no roadmap** — discuss or research the milestone
|
||||
- **Roadmap exists, slices pending** — plan the next slice or execute a task
|
||||
- **Mid-task** — resume where you left off
|
||||
|
||||
Step mode is the on-ramp. You stay in the loop, reviewing output between each step.
|
||||
Step mode keeps you in the loop, reviewing output between each step.
|
||||
|
||||
### Auto Mode — `/gsd auto`
|
||||
|
||||
|
|
@ -99,9 +338,11 @@ Type `/gsd auto` and walk away. GSD autonomously researches, plans, executes, ve
|
|||
|
||||
See [Auto Mode](./auto-mode.md) for full details.
|
||||
|
||||
## Two Terminals, One Project
|
||||
---
|
||||
|
||||
The recommended workflow: auto mode in one terminal, steering from another.
|
||||
## Recommended Workflow: Two Terminals
|
||||
|
||||
Run auto mode in one terminal, steer from another.
|
||||
|
||||
**Terminal 1 — let it build:**
|
||||
|
||||
|
|
@ -121,9 +362,9 @@ gsd
|
|||
|
||||
Both terminals read and write the same `.gsd/` files. Decisions in terminal 2 are picked up at the next phase boundary automatically.
|
||||
|
||||
## Project Structure
|
||||
---
|
||||
|
||||
GSD organizes work into a hierarchy:
|
||||
## How GSD Organizes Work
|
||||
|
||||
```
|
||||
Milestone → a shippable version (4-10 slices)
|
||||
|
|
@ -138,25 +379,45 @@ All state lives on disk in `.gsd/`:
|
|||
```
|
||||
.gsd/
|
||||
PROJECT.md — what the project is right now
|
||||
REQUIREMENTS.md — requirement contract (active/validated/deferred)
|
||||
REQUIREMENTS.md — requirement contract
|
||||
DECISIONS.md — append-only architectural decisions
|
||||
KNOWLEDGE.md — cross-session rules, patterns, and lessons
|
||||
RUNTIME.md — runtime context: API endpoints, env vars, services (v2.39)
|
||||
KNOWLEDGE.md — cross-session rules and patterns
|
||||
STATE.md — quick-glance status
|
||||
milestones/
|
||||
M001/
|
||||
M001-ROADMAP.md — slice plan with risk levels and dependencies
|
||||
M001-CONTEXT.md — scope and goals from discussion
|
||||
M001-ROADMAP.md — slice plan with dependencies
|
||||
slices/
|
||||
S01/
|
||||
S01-PLAN.md — task decomposition
|
||||
S01-SUMMARY.md — what happened
|
||||
S01-UAT.md — human test script
|
||||
tasks/
|
||||
T01-PLAN.md
|
||||
T01-SUMMARY.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## VS Code Extension
|
||||
|
||||
GSD is also available as a VS Code extension. Install from the marketplace (publisher: FluxLabs) or search for "GSD" in VS Code extensions:
|
||||
|
||||
- **`@gsd` chat participant** — talk to the agent in VS Code Chat
|
||||
- **Sidebar dashboard** — connection status, model info, token usage
|
||||
- **Full command palette** — start/stop agent, switch models, export sessions
|
||||
|
||||
The CLI (`gsd-pi`) must be installed first — the extension connects to it via RPC.
|
||||
|
||||
---
|
||||
|
||||
## Web Interface
|
||||
|
||||
GSD has a browser-based interface for visual project management:
|
||||
|
||||
```bash
|
||||
gsd --web
|
||||
```
|
||||
|
||||
See [Web Interface](./web-interface.md) for details.
|
||||
|
||||
---
|
||||
|
||||
## Resume a Session
|
||||
|
||||
```bash
|
||||
|
|
@ -165,36 +426,48 @@ gsd --continue # or gsd -c
|
|||
|
||||
Resumes the most recent session for the current directory.
|
||||
|
||||
To browse and pick from all saved sessions:
|
||||
Browse all saved sessions:
|
||||
|
||||
```bash
|
||||
gsd sessions
|
||||
```
|
||||
|
||||
Shows each session's date, message count, and first-message preview so you can choose which one to resume.
|
||||
---
|
||||
|
||||
## Updating GSD
|
||||
|
||||
GSD checks for updates every 24 hours and prompts at startup. You can also update manually:
|
||||
|
||||
```bash
|
||||
npm update -g gsd-pi
|
||||
```
|
||||
|
||||
Or from within a session:
|
||||
|
||||
```
|
||||
/gsd update
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Troubleshooting
|
||||
|
||||
| Problem | Fix |
|
||||
|---------|-----|
|
||||
| `command not found: gsd` | Add npm global bin to PATH (see OS-specific notes above) |
|
||||
| `gsd` runs `git svn dcommit` | oh-my-zsh conflict — `unalias gsd` or use `gsd-cli` |
|
||||
| Permission errors on `npm install -g` | Fix npm prefix (see Linux notes) or use nvm |
|
||||
| Can't connect to LLM | Check API key with `gsd config`, verify network access |
|
||||
| `gsd` hangs on start | Check Node.js version: `node --version` (need 22+) |
|
||||
|
||||
For more, see [Troubleshooting](./troubleshooting.md).
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Auto Mode](./auto-mode.md) — deep dive into autonomous execution
|
||||
- [Configuration](./configuration.md) — model selection, timeouts, budgets
|
||||
- [Commands Reference](./commands.md) — all commands and shortcuts
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### `gsd` command runs `git svn dcommit` instead of GSD
|
||||
|
||||
The [oh-my-zsh git plugin](https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/git) defines `alias gsd='git svn dcommit'`, which shadows the GSD binary.
|
||||
|
||||
**Option 1** — Remove the alias in your `~/.zshrc` (add after the `source $ZSH/oh-my-zsh.sh` line):
|
||||
|
||||
```bash
|
||||
unalias gsd 2>/dev/null
|
||||
```
|
||||
|
||||
**Option 2** — Use the alternative binary name:
|
||||
|
||||
```bash
|
||||
gsd-cli
|
||||
```
|
||||
|
||||
Both `gsd` and `gsd-cli` point to the same binary.
|
||||
- [Provider Setup](./providers.md) — detailed setup for every provider
|
||||
- [Working in Teams](./working-in-teams.md) — multi-developer workflows
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ Step-by-step setup instructions for every LLM provider GSD supports. If you ran
|
|||
|
||||
| Provider | Auth Method | Env Variable | Config File |
|
||||
|----------|-------------|-------------|-------------|
|
||||
| Anthropic | OAuth or API key | `ANTHROPIC_API_KEY` | — |
|
||||
| Anthropic | API key | `ANTHROPIC_API_KEY` | — |
|
||||
| OpenAI | API key | `OPENAI_API_KEY` | — |
|
||||
| Google Gemini | API key | `GEMINI_API_KEY` | — |
|
||||
| OpenRouter | API key | `OPENROUTER_API_KEY` | Optional `models.json` |
|
||||
|
|
@ -55,25 +55,91 @@ Built-in providers have models pre-registered in GSD. You only need to supply cr
|
|||
|
||||
**Recommended.** Anthropic models have the deepest integration: built-in web search, extended thinking, and prompt caching.
|
||||
|
||||
**Option A — Browser sign-in (recommended):**
|
||||
|
||||
```bash
|
||||
gsd config
|
||||
# Choose "Sign in with your browser" → "Anthropic (Claude)"
|
||||
```
|
||||
|
||||
Or inside a session: `/login`
|
||||
|
||||
**Option B — API key:**
|
||||
**Option A — API key (recommended):**
|
||||
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY="sk-ant-..."
|
||||
```
|
||||
|
||||
Or paste it during `gsd config` when prompted.
|
||||
Or run `gsd config` and paste your key when prompted.
|
||||
|
||||
**Get a key:** [console.anthropic.com/settings/keys](https://console.anthropic.com/settings/keys)
|
||||
|
||||
**Option B — Claude Code CLI:**
|
||||
|
||||
If you have a Claude Pro or Max subscription, you can authenticate through Anthropic's official Claude Code CLI. Install it, sign in with `claude`, then GSD will detect and route through it automatically:
|
||||
|
||||
```bash
|
||||
# Install Claude Code CLI (see https://docs.anthropic.com/en/docs/claude-code)
|
||||
claude
|
||||
# Sign in when prompted, then start GSD
|
||||
gsd
|
||||
```
|
||||
|
||||
GSD detects your local Claude Code installation and uses it as the authenticated Anthropic surface. This is the TOS-compliant path for subscription users — GSD never handles your subscription credentials directly.
|
||||
|
||||
> **Note:** GSD does not support browser-based OAuth sign-in for Anthropic. Use an API key or the Claude Code CLI instead.
|
||||
|
||||
**Option C — Use your Claude Pro/Max plan with GSD inside Claude Code:**
|
||||
|
||||
If you already have a Claude Pro or Max subscription and want to use GSD's planning, execution, and milestone orchestration directly from Claude Code — without switching to a separate terminal — you can connect GSD as an MCP server. This gives Claude Code access to GSD's full workflow toolset via the [Model Context Protocol](https://modelcontextprotocol.io), so you get GSD's structured project management powered by your existing Claude plan.
|
||||
|
||||
**Automatic setup (recommended):**
|
||||
|
||||
When GSD detects a Claude Code model during startup, it automatically writes a `.mcp.json` file in your project root with the GSD workflow MCP server configured. No manual steps needed — just start GSD once with Claude Code as the provider and the config is created for you.
|
||||
|
||||
You can also trigger this manually from inside a GSD session:
|
||||
|
||||
```bash
|
||||
/gsd mcp init
|
||||
```
|
||||
|
||||
This writes (or updates) the `gsd-workflow` entry in your project's `.mcp.json`. Claude Code discovers this file automatically on its next session start.
|
||||
|
||||
**Manual setup:**
|
||||
|
||||
If you prefer to configure it yourself, add GSD to your project's `.mcp.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"gsd": {
|
||||
"command": "npx",
|
||||
"args": ["gsd-mcp-server"],
|
||||
"env": {
|
||||
"GSD_CLI_PATH": "/path/to/gsd"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or if `gsd-mcp-server` is installed globally:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"gsd": {
|
||||
"command": "gsd-mcp-server"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can also add this to `~/.claude/settings.json` under `mcpServers` to make GSD available across all projects.
|
||||
|
||||
**What's exposed:**
|
||||
|
||||
The MCP server provides GSD's full workflow tool surface — milestone planning, task completion, slice management, roadmap reassessment, journal queries, and more. Session management tools (`gsd_execute`, `gsd_status`, `gsd_result`, `gsd_cancel`) let Claude Code start and monitor GSD auto-mode sessions. See [Commands → MCP Server Mode](./commands.md#mcp-server-mode) for the full tool list.
|
||||
|
||||
**Verify the connection:**
|
||||
|
||||
From inside a GSD session, check that the MCP server is reachable:
|
||||
|
||||
```bash
|
||||
/gsd mcp status
|
||||
```
|
||||
|
||||
### OpenAI
|
||||
|
||||
```bash
|
||||
|
|
|
|||
32
docs/zh-CN/README.md
Normal file
32
docs/zh-CN/README.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# GSD 文档
|
||||
|
||||
欢迎使用 GSD 文档。这里涵盖了从快速开始到高级配置、自动模式内部机制,以及如何基于 Pi SDK 扩展 GSD 的内容。
|
||||
|
||||
> 本目录是主文档的简体中文翻译。目前优先覆盖 `docs/user-docs/` 这套用户手册;如中英文内容有差异,请以英文原文为准。
|
||||
|
||||
## 用户文档
|
||||
|
||||
用于安装、配置和日常使用 GSD 的指南。文件位于 [`user-docs/`](./user-docs/)。
|
||||
|
||||
| 指南 | 说明 |
|
||||
|------|------|
|
||||
| [快速开始](./user-docs/getting-started.md) | 安装、首次运行和基础使用 |
|
||||
| [自动模式](./user-docs/auto-mode.md) | 自主执行如何工作,包括状态机、崩溃恢复和引导控制 |
|
||||
| [命令参考](./user-docs/commands.md) | 所有命令、键盘快捷键和 CLI 参数 |
|
||||
| [远程提问](./user-docs/remote-questions.md) | 用于无头自动模式的 Discord、Slack 和 Telegram 集成 |
|
||||
| [配置](./user-docs/configuration.md) | 偏好设置、模型选择、Git 设置和 Token 配置 |
|
||||
| [提供商设置](./user-docs/providers.md) | OpenRouter、Ollama、LM Studio、vLLM 以及所有受支持提供商的分步配置 |
|
||||
| [自定义模型](./user-docs/custom-models.md) | 高级模型配置,包括 `models.json` 结构、兼容标志和覆盖项 |
|
||||
| [Token 优化](./user-docs/token-optimization.md) | Token 配置、上下文压缩、复杂度路由和自适应学习 |
|
||||
| [动态模型路由](./user-docs/dynamic-model-routing.md) | 基于复杂度的模型选择、成本表、升级策略和预算压力 |
|
||||
| [捕获与分流](./user-docs/captures-triage.md) | 自动模式中的随手记录,以及自动分流处理 |
|
||||
| [工作流可视化器](./user-docs/visualizer.md) | 用于查看进度、依赖、指标和时间线的交互式 TUI 叠层 |
|
||||
| [成本管理](./user-docs/cost-management.md) | 预算上限、成本跟踪、成本预测和执行策略 |
|
||||
| [Git 策略](./user-docs/git-strategy.md) | 工作树隔离、分支模型和合并行为 |
|
||||
| [并行编排](./user-docs/parallel-orchestration.md) | 通过隔离的工作线程和协调机制同时运行多个 milestones |
|
||||
| [团队协作](./user-docs/working-in-teams.md) | 唯一 milestone ID、`.gitignore` 设置和共享规划产物 |
|
||||
| [技能](./user-docs/skills.md) | 内置技能、技能发现和自定义技能编写 |
|
||||
| [从 v1 迁移](./user-docs/migration.md) | 将 `.planning` 目录迁移到新的 `.gsd` 格式 |
|
||||
| [故障排查](./user-docs/troubleshooting.md) | 常见问题、`/gsd doctor`、`/gsd forensics` 和恢复流程 |
|
||||
| [Web 界面](./user-docs/web-interface.md) | 通过 `gsd --web` 使用基于浏览器的项目管理界面 |
|
||||
| [VS Code 扩展](../../vscode-extension/README.md) | 聊天参与者、侧边栏仪表板以及 VS Code 的 RPC 集成 |
|
||||
301
docs/zh-CN/user-docs/auto-mode.md
Normal file
301
docs/zh-CN/user-docs/auto-mode.md
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
# 自动模式
|
||||
|
||||
自动模式是 GSD 的自主执行引擎。运行 `/gsd auto`,然后离开;回来时你会看到已经构建好的软件,以及干净的 git 历史。
|
||||
|
||||
## 工作原理
|
||||
|
||||
自动模式本质上是一个**由磁盘文件驱动的状态机**。它会读取 `.gsd/STATE.md`,确定下一个工作单元,创建一个新的 agent 会话,把所有相关上下文预先内联到一个聚焦 prompt 中,再让 LLM 执行。LLM 完成后,自动模式会再次读取磁盘状态,并派发下一个工作单元。
|
||||
|
||||
### 执行循环
|
||||
|
||||
每个 slice 都会自动经历以下阶段:
|
||||
|
||||
```
|
||||
Plan (with integrated research) → Execute (per task) → Complete → Reassess Roadmap → Next Slice
|
||||
↓ (all slices done)
|
||||
Validate Milestone → Complete Milestone
|
||||
```
|
||||
|
||||
- **Plan**:巡检代码库、研究相关文档,把 slice 分解成带 must-have 的 task
|
||||
- **Execute**:在新的上下文窗口中逐个执行 task
|
||||
- **Complete**:写 summary、UAT 脚本、标记 roadmap、提交代码
|
||||
- **Reassess**:检查 roadmap 是否仍然合理
|
||||
- **Validate Milestone**:在所有 slices 完成后做一致性校验,把 roadmap 的成功标准与实际结果对照,避免在封板前漏掉关键缺口
|
||||
|
||||
## 关键特性
|
||||
|
||||
### 每个单元都用全新会话
|
||||
|
||||
每个 task、research 阶段和 planning 步骤都会得到一个干净的上下文窗口。没有历史垃圾堆积,也不会因为上下文膨胀导致质量下降。派发 prompt 中已经包含 task plan、历史 summary、依赖上下文、决策记录等必要信息,因此 LLM 一开始就能对齐,而不必先花工具调用去读文件。
|
||||
|
||||
### 预加载上下文
|
||||
|
||||
派发 prompt 会精心组装以下内容:
|
||||
|
||||
| 内联产物 | 用途 |
|
||||
|----------|------|
|
||||
| Task plan | 告诉 agent 要构建什么 |
|
||||
| Slice plan | 说明当前 task 在整体中的位置 |
|
||||
| 历史 task summaries | 告诉 agent 已经完成了什么 |
|
||||
| 依赖 summary | 提供跨 slice 上下文 |
|
||||
| Roadmap 摘要 | 说明整体方向 |
|
||||
| Decisions register | 提供架构上下文 |
|
||||
|
||||
具体内联多少内容由你的 [token profile](./token-optimization.md) 控制。`budget` 模式只内联最少上下文,`quality` 模式则把所有内容都内联进去。
|
||||
|
||||
### Git 隔离
|
||||
|
||||
GSD 支持三种 milestone 隔离模式(通过偏好设置中的 `git.isolation` 配置):
|
||||
|
||||
- **`worktree`**(默认):每个 milestone 都运行在 `.gsd/worktrees/<MID>/` 下自己的 git worktree 中,分支名为 `milestone/<MID>`。所有 slice 工作都顺序提交,不需要切分支,也不会在 milestone 内部产生合并冲突。milestone 完成后,再整体 squash merge 回主分支,形成一个干净提交。
|
||||
- **`branch`**:工作发生在项目根目录下的 `milestone/<MID>` 分支上。适合子模块较多、worktree 表现不佳的仓库。
|
||||
- **`none`**:直接在当前分支工作。没有 worktree,也没有 milestone 分支。适合文件隔离会破坏开发工具的热重载场景。
|
||||
|
||||
详见 [Git 策略](./git-strategy.md)。
|
||||
|
||||
### 并行执行
|
||||
|
||||
当项目里存在彼此独立的 milestones 时,可以同时运行它们。每个 milestone 都拥有自己的 worker 进程和 worktree。配置与用法见 [并行编排](./parallel-orchestration.md)。
|
||||
|
||||
### 崩溃恢复
|
||||
|
||||
自动模式会用锁文件跟踪当前工作单元。如果会话中途退出,下一次执行 `/gsd auto` 时,会读取残留的会话文件,从所有已经落盘的工具调用中综合生成一份恢复简报,然后带着完整上下文继续执行。
|
||||
|
||||
**Headless 自动重启(v2.26):** 当运行 `gsd headless auto` 时,崩溃会触发带指数退避的自动重启(5s → 10s → 30s 上限,默认最多 3 次)。通过 `--max-restarts N` 配置。SIGINT/SIGTERM 不会触发重启。结合崩溃恢复机制,这让真正的“跑一夜直到完成”成为可能。
|
||||
|
||||
### Provider 错误恢复
|
||||
|
||||
GSD 会对 provider 错误分类,并在安全时自动恢复:
|
||||
|
||||
| 错误类型 | 示例 | 动作 |
|
||||
|----------|------|------|
|
||||
| **限流** | 429、`too many requests` | 按 `retry-after` 头等待,或默认 60 秒后自动恢复 |
|
||||
| **服务端错误** | 500、502、503、`overloaded`、`api_error` | 30 秒后自动恢复 |
|
||||
| **永久错误** | `unauthorized`、`invalid key`、`billing` | 无限期暂停,等待人工恢复 |
|
||||
|
||||
对临时性错误通常不需要人工介入,系统会短暂暂停后自动继续。
|
||||
|
||||
### 增量记忆(v2.26)
|
||||
|
||||
GSD 会维护一个 `KNOWLEDGE.md` 文件,作为项目特有规则、模式和经验的追加式记录。agent 在每个工作单元开始时都会读取它;当发现反复出现的问题、非显而易见的模式或未来会话需要遵循的规则时,也会把内容追加进去。这样一来,自动模式就有了跨会话、跨上下文窗口的持久记忆。
|
||||
|
||||
### 上下文压力监视器(v2.26)
|
||||
|
||||
当上下文使用达到 70% 时,GSD 会向 agent 发送收尾信号,提醒它优先完成可持久化的输出(例如提交、写 summary),避免在 task 中途因为上下文打满而什么都没来得及落盘。
|
||||
|
||||
### 有意义的提交信息(v2.26)
|
||||
|
||||
提交信息不是通用的 “complete task”,而是从 task summary 生成的。每条提交消息都反映了真正完成了什么,因此 `git log` 看起来更像一份高质量的变更日志。
|
||||
|
||||
### 卡死检测(v2.39)
|
||||
|
||||
GSD 使用滑动窗口分析来检测卡死循环。它不只是简单地统计“同一单元是否重复派发两次”,而是会分析近期派发历史中的重复模式,因此既能发现单点重复,也能发现 A→B→A→B 这样的循环。一旦检测到,GSD 会先带着更深的诊断 prompt 重试一次;如果仍然失败,自动模式就会停止,并指出它原本期待的具体文件,便于你介入。
|
||||
|
||||
这种滑动窗口方法能降低合法重试场景(例如可自动修复的 verification 失败)的误报,同时更快抓到真正的卡死循环。
|
||||
|
||||
### 事后取证(v2.40)
|
||||
|
||||
`/gsd forensics` 是一个面向自动模式失败分析的全访问 GSD 调试器,提供:
|
||||
|
||||
- **异常检测**:对卡死循环、成本尖峰、超时、产物缺失和崩溃做结构化识别,并标注严重级别
|
||||
- **单元追踪**:最近 10 次单元执行,包含错误细节和执行时长
|
||||
- **指标分析**:成本、token 数量和执行时间拆分
|
||||
- **Doctor 集成**:把 `/gsd doctor` 中的结构性健康问题一起纳入
|
||||
- **LLM 引导调查**:启动一个拥有完整工具访问权限的 agent 会话来调查根因
|
||||
|
||||
```
|
||||
/gsd forensics [optional problem description]
|
||||
```
|
||||
|
||||
更多诊断方式见 [故障排查](./troubleshooting.md)。
|
||||
|
||||
### 超时监管
|
||||
|
||||
三层超时机制可以防止会话失控:
|
||||
|
||||
| 超时类型 | 默认值 | 行为 |
|
||||
|----------|--------|------|
|
||||
| Soft | 20 分钟 | 警告 LLM 应该开始收尾 |
|
||||
| Idle | 10 分钟 | 检测停滞并介入 |
|
||||
| Hard | 30 分钟 | 暂停自动模式 |
|
||||
|
||||
恢复引导会提醒 LLM 在真正超时前尽量完成可持久化输出。配置方式如下:
|
||||
|
||||
```yaml
|
||||
auto_supervisor:
|
||||
soft_timeout_minutes: 20
|
||||
idle_timeout_minutes: 10
|
||||
hard_timeout_minutes: 30
|
||||
```
|
||||
|
||||
### 成本跟踪
|
||||
|
||||
每个工作单元的 token 使用量和成本都会被记录,并按阶段、slice 和模型拆分。仪表板会显示运行总量和预测值。预算上限可以在超支前主动暂停自动模式。
|
||||
|
||||
详见 [成本管理](./cost-management.md)。
|
||||
|
||||
### 自适应重规划
|
||||
|
||||
每完成一个 slice,roadmap 都会重新评估。如果最新工作暴露出会改变计划的新信息,后续 slices 就会在继续前被重新排序、添加或删除。`balanced` 和 `budget` token profile 可以跳过这一阶段。
|
||||
|
||||
### 验证强制执行(v2.26)
|
||||
|
||||
你可以配置 shell 命令,让它们在每个 task 执行后自动运行:
|
||||
|
||||
```yaml
|
||||
verification_commands:
|
||||
- npm run lint
|
||||
- npm run test
|
||||
verification_auto_fix: true # 默认开启自动重试修复
|
||||
verification_max_retries: 2 # 最大重试次数(默认 2)
|
||||
```
|
||||
|
||||
一旦失败,agent 会看到 verification 输出并尝试自动修复后重试,再决定是否继续。这意味着代码质量门禁是靠机制强制执行,而不是靠 LLM“自觉遵守”。
|
||||
|
||||
### Slice 讨论门(v2.26)
|
||||
|
||||
如果你希望每个 slice 开始前都先经过人工确认:
|
||||
|
||||
```yaml
|
||||
require_slice_discussion: true
|
||||
```
|
||||
|
||||
自动模式会在每个 slice 开始前暂停,并把 slice 上下文展示出来供你讨论。确认后才继续执行。适用于高风险项目,尤其是你希望 agent 开始构建前先复核计划的时候。
|
||||
|
||||
### HTML 报告(v2.26)
|
||||
|
||||
每当 milestone 完成后,GSD 都会在 `.gsd/reports/` 中自动生成一个自包含的 HTML 报告。报告包括项目摘要、进度树、slice 依赖图(SVG DAG)、成本 / Token 柱状图、执行时间线、变更日志和知识库。没有外部依赖,所有 CSS 和 JS 都会内联。
|
||||
|
||||
```yaml
|
||||
auto_report: true # 默认开启
|
||||
```
|
||||
|
||||
你也可以随时手动执行 `/gsd export --html` 生成报告,或通过 `/gsd export --html --all`(v2.28)为所有 milestones 一次性生成报告。
|
||||
|
||||
### 故障恢复强化(v2.28)
|
||||
|
||||
v2.28 通过多项机制强化了自动模式的可靠性:原子文件写入可避免崩溃时损坏文件;OAuth 拉取超时(30 秒)避免无限挂起;RPC 子进程退出能被检测并报告;blob 垃圾回收可防止磁盘无限增长。结合已有的崩溃恢复和 headless 自动重启,自动模式可以真正支持“扔在那里跑一晚上”的场景。
|
||||
|
||||
### 流水线架构(v2.40)
|
||||
|
||||
自动循环采用的是线性阶段流水线,而非递归派发。每轮迭代都经过明确的阶段:
|
||||
|
||||
1. **Pre-Dispatch**:校验状态、检查守卫、解析模型偏好
|
||||
2. **Dispatch**:使用聚焦 prompt 执行当前单元
|
||||
3. **Post-Unit**:关闭该单元、更新缓存、执行清理
|
||||
4. **Verification**:可选验证门(lint、test 等)
|
||||
5. **Stuck Detection**:滑动窗口模式分析
|
||||
|
||||
这种线性流程更容易调试,占用更少内存(没有递归调用栈),也使错误恢复更清晰,因为每个阶段都有明确的入口和出口条件。
|
||||
|
||||
### 实时健康可见性(v2.40)
|
||||
|
||||
`/gsd doctor` 发现的问题现在会实时出现在三个地方:
|
||||
|
||||
- **Dashboard widget**:健康指示器,显示问题数量和严重级别
|
||||
- **Workflow visualizer**:状态面板中展示问题
|
||||
- **HTML reports**:生成报告时带出完整健康信息
|
||||
|
||||
问题按严重程度分为:`error`(阻塞自动模式)、`warning`(不阻塞)和 `info`(提示性质)。自动模式会在派发时检查健康状态,并可在关键问题出现时主动暂停。
|
||||
|
||||
### Prompt 中的技能激活(v2.39)
|
||||
|
||||
配置好的技能会被自动解析并注入派发 prompt。agent 会收到一个 “Available Skills” 区块,列出当前上下文匹配的技能,来源包括:
|
||||
|
||||
- `always_use_skills`:始终注入
|
||||
- `prefer_skills`:以偏好形式注入
|
||||
- `skill_rules`:根据 `when` 条件做条件激活
|
||||
|
||||
技能路由偏好详见 [配置](./configuration.md)。
|
||||
|
||||
## 控制自动模式
|
||||
|
||||
### 启动
|
||||
|
||||
```
|
||||
/gsd auto
|
||||
```
|
||||
|
||||
### 暂停
|
||||
|
||||
按 **Escape**。对话会被保留,你可以继续和 agent 交互、查看状态,或者稍后恢复。
|
||||
|
||||
### 恢复
|
||||
|
||||
```
|
||||
/gsd auto
|
||||
```
|
||||
|
||||
自动模式会读取磁盘状态,并从中断处继续。
|
||||
|
||||
### 停止
|
||||
|
||||
```
|
||||
/gsd stop
|
||||
```
|
||||
|
||||
优雅地停止自动模式。这个命令也可以从另一个终端执行。
|
||||
|
||||
### 引导
|
||||
|
||||
```
|
||||
/gsd steer
|
||||
```
|
||||
|
||||
在不中断流水线的情况下,强制修改计划文档。修改会在下一个阶段边界生效。
|
||||
|
||||
### 捕获
|
||||
|
||||
```
|
||||
/gsd capture "add rate limiting to API endpoints"
|
||||
```
|
||||
|
||||
随手记录想法,不打断当前执行。Captures 会在 tasks 之间自动 triage。详见 [捕获与分流](./captures-triage.md)。
|
||||
|
||||
### 可视化
|
||||
|
||||
```
|
||||
/gsd visualize
|
||||
```
|
||||
|
||||
打开工作流可视化器,交互式查看进度、依赖、指标和时间线。详见 [工作流可视化器](./visualizer.md)。
|
||||
|
||||
## 仪表板
|
||||
|
||||
`Ctrl+Alt+G` 或 `/gsd status` 会显示实时进度:
|
||||
|
||||
- 当前 milestone、slice 和 task
|
||||
- 自动模式的已运行时间和当前阶段
|
||||
- 每个单元的成本与 token 拆分
|
||||
- 成本预测
|
||||
- 已完成和进行中的单元
|
||||
- 待 triage 的 capture 数量(如果存在)
|
||||
- 并行 worker 状态(运行并行 milestones 时显示,也包含 80% 预算预警)
|
||||
|
||||
## 跳过阶段
|
||||
|
||||
Token profile 可以通过跳过某些阶段来降低成本:
|
||||
|
||||
| 阶段 | `budget` | `balanced` | `quality` |
|
||||
|------|----------|------------|-----------|
|
||||
| Milestone Research | 跳过 | 执行 | 执行 |
|
||||
| Slice Research | 跳过 | 跳过 | 执行 |
|
||||
| Reassess Roadmap | 跳过 | 执行 | 执行 |
|
||||
|
||||
更多细节见 [Token 优化](./token-optimization.md)。
|
||||
|
||||
## 动态模型路由
|
||||
|
||||
启用后,自动模式会为简单工作单元(例如 slice completion、UAT)自动选择更便宜的模型,并把昂贵模型保留给复杂工作(例如重规划或架构 task)。详见 [动态模型路由](./dynamic-model-routing.md)。
|
||||
|
||||
## 响应式 Task 执行(v2.38)
|
||||
|
||||
当在偏好中设置 `reactive_execution: true` 时,GSD 会从 task plan 中的 IO 注解推导依赖图。互不冲突的 tasks(没有共享文件读写)会通过 subagents 并行派发,而存在依赖的 tasks 则等待前驱完成。
|
||||
|
||||
```yaml
|
||||
reactive_execution: true # 默认关闭
|
||||
```
|
||||
|
||||
依赖图推导是纯函数且确定性的:它会解析 ready-set、检测冲突和死锁,并做相应防护。并行批次中的 verification 结果会被沿用,因此某些 tasks 如果已经通过验证,后续同一 slice 中其他 tasks 完成时就不需要再次验证。
|
||||
|
||||
这套实现位于 `reactive-graph.ts`(负责图推导、ready-set 解析、冲突 / 死锁检测),并集成到了 `auto-dispatch.ts` 和 `auto-prompts.ts`。
|
||||
84
docs/zh-CN/user-docs/captures-triage.md
Normal file
84
docs/zh-CN/user-docs/captures-triage.md
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# 捕获与分流
|
||||
|
||||
*引入于 v2.19.0*
|
||||
|
||||
Captures 允许你在自动模式执行过程中随手记录想法,而不必打断当前流程。你可以把新想法、bug 或范围变更记录下来,让 GSD 在 tasks 之间的自然间隙中进行分流处理。
|
||||
|
||||
## 快速开始
|
||||
|
||||
在自动模式运行期间(或任何时候):
|
||||
|
||||
```
|
||||
/gsd capture "add rate limiting to the API endpoints"
|
||||
/gsd capture "the auth flow should support OAuth, not just JWT"
|
||||
```
|
||||
|
||||
这些 capture 会追加到 `.gsd/CAPTURES.md`,并在 tasks 之间自动参与 triage。
|
||||
|
||||
## 工作原理
|
||||
|
||||
### 流程
|
||||
|
||||
```
|
||||
capture → triage → confirm → resolve → resume
|
||||
```
|
||||
|
||||
1. **Capture**:`/gsd capture "thought"` 会带着时间戳和唯一 ID 追加到 `.gsd/CAPTURES.md`
|
||||
2. **Triage**:在 tasks 之间的自然衔接点(`handleAgentEnd` 中),GSD 会检测待处理 capture 并进行分类
|
||||
3. **Confirm**:向用户展示建议的处理方式,由用户确认或调整
|
||||
4. **Resolve**:应用该处理方案(插入 task、触发重规划、延期等)
|
||||
5. **Resume**:自动模式继续运行
|
||||
|
||||
### 分类类型
|
||||
|
||||
每条 capture 都会被分类到以下五种类型之一:
|
||||
|
||||
| 类型 | 含义 | 处理方式 |
|
||||
|------|------|----------|
|
||||
| `quick-task` | 小型、可独立完成的修复 | 立即以内联 quick task 执行 |
|
||||
| `inject` | 当前 slice 需要新增 task | 将 task 注入当前活跃的 slice plan |
|
||||
| `defer` | 重要但不紧急 | 延后到 roadmap reassessment 时处理 |
|
||||
| `replan` | 改变当前实现路径 | 带着 capture 上下文触发 slice replan |
|
||||
| `note` | 仅供记录,不需要动作 | 记录并确认,不修改计划 |
|
||||
|
||||
### 自动分流
|
||||
|
||||
在自动模式下,triage 会在 tasks 之间自动触发。triage prompt 会收到:
|
||||
|
||||
- 所有待处理 captures
|
||||
- 当前 slice plan
|
||||
- 当前活跃 roadmap
|
||||
|
||||
LLM 会对每条 capture 进行分类并给出建议处理方案。会修改计划的处理方式(`inject`、`replan`)需要用户确认。
|
||||
|
||||
### 手动分流
|
||||
|
||||
你也可以随时手动触发 triage:
|
||||
|
||||
```
|
||||
/gsd triage
|
||||
```
|
||||
|
||||
这在你积累了多条 capture,并希望在下一个自然间隙之前先处理掉它们时很有用。
|
||||
|
||||
## 仪表板集成
|
||||
|
||||
当有待 triage 的 capture 时,进度组件会显示一个待处理数量徽标。无论是在 `Ctrl+Alt+G` 仪表板里,还是自动模式进度组件里,都能看到这个提示。
|
||||
|
||||
## 上下文注入
|
||||
|
||||
Capture 上下文会自动注入到:
|
||||
|
||||
- **Replan-slice prompts**:让重规划知道是什么触发了它
|
||||
- **Reassess-roadmap prompts**:让被延后的 capture 也会影响 roadmap 决策
|
||||
|
||||
## Worktree 感知
|
||||
|
||||
Captures 总是写回**原始项目根目录**下的 `.gsd/CAPTURES.md`,而不是 worktree 的本地副本。这样从 steering 终端记录的内容,也能被运行在 worktree 里的自动模式会话看到。
|
||||
|
||||
## 命令
|
||||
|
||||
| 命令 | 说明 |
|
||||
|------|------|
|
||||
| `/gsd capture "text"` | 记录一个想法(单词时引号可省略) |
|
||||
| `/gsd triage` | 手动触发待处理 captures 的 triage |
|
||||
177
docs/zh-CN/user-docs/claude-code-auth-compliance.md
Normal file
177
docs/zh-CN/user-docs/claude-code-auth-compliance.md
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
# Claude Code 认证合规性研究
|
||||
|
||||
日期:2026-04-10
|
||||
|
||||
## 执行摘要
|
||||
|
||||
Anthropic 当前公开的指导原则边界非常清晰:
|
||||
|
||||
- Anthropic 自家的原生应用,包括 Claude Code,可以使用 Claude 订阅认证。
|
||||
- 第三方工具应优先通过 Claude Console 或受支持云 provider 的 API key 进行认证。
|
||||
- 任何伪装身份、绕过订阅限制转发第三方流量、或以其他方式违反 Anthropic 条款的应用,都被明确禁止。
|
||||
|
||||
对于 GSD2,安全路径应当是:
|
||||
|
||||
1. 把本地 Claude Code 视为一个外部、已认证的运行时。
|
||||
2. 永远不要让 GSD 用户通过 GSD 托管的 Anthropic OAuth 去登录 Claude 订阅。
|
||||
3. 永远不要把 Claude.ai 的订阅 OAuth 凭据交换成 bearer token,然后冒充 Claude Code 直接调用 Anthropic API。
|
||||
4. 如果 GSD 需要直接访问 Anthropic API,则必须要求使用 Claude Console API key、Bedrock、Vertex 或其他被明确支持的 provider 路径。
|
||||
|
||||
## Anthropic 明确允许的内容
|
||||
|
||||
### 1. Claude Code 本身可以使用 Claude 订阅认证
|
||||
|
||||
Anthropic 帮助中心说明:Claude Pro / Max 用户应安装 Claude Code,运行 `claude`,并“使用与你登录 Claude 相同的凭据”完成登录。文档还指出,这样会把订阅直接连接到 Claude Code,并且 `/login` 是切换账户类型的方式。Team / Enterprise 文章对组织账号也给出了同样流程。
|
||||
|
||||
对 GSD2 的含义:
|
||||
|
||||
- 允许用户在真正的 `claude` CLI 内部完成认证,是符合 Anthropic 文档流程的
|
||||
- 检测 `claude auth status`,然后通过本地 CLI 或官方 Claude Code SDK 路由工作,是风险最低的方案
|
||||
|
||||
### 2. Claude Code 同时支持订阅 OAuth 和 API 凭据
|
||||
|
||||
Anthropic 的 Claude Code 文档说明,支持的认证类型包括 Claude.ai 凭据、Claude API 凭据、Azure Auth、Bedrock Auth 和 Vertex Auth。文档还定义了认证优先级:
|
||||
|
||||
1. cloud provider 凭据
|
||||
2. `ANTHROPIC_AUTH_TOKEN`
|
||||
3. `ANTHROPIC_API_KEY`
|
||||
4. `apiKeyHelper`
|
||||
5. 来自 `/login` 的订阅 OAuth
|
||||
|
||||
对 GSD2 的含义:
|
||||
|
||||
- 如果 GSD2 是通过 shell 调用或嵌入 Claude Code,那么它应尊重 Claude Code 自己的凭据选择逻辑,而不是再发明一套平行的 Anthropic OAuth 流程
|
||||
- 对需要动态短期凭据、但又不希望把原始 API key 交给工具的组织来说,`apiKeyHelper` 是一个干净的企业级出口
|
||||
|
||||
### 3. Anthropic 的商业使用可通过 API keys 和受支持的云 provider 实现
|
||||
|
||||
Anthropic 的商业条款约束的是 API keys 及其相关 Anthropic 服务,包括供客户构建给终端用户使用的产品。面向团队的认证文档推荐使用 Claude for Teams / Enterprise、Claude Console、Bedrock、Vertex 或 Microsoft Foundry。
|
||||
|
||||
对 GSD2 的含义:
|
||||
|
||||
- 如果 GSD2 作为一个产品面向用户提供 Anthropic 能力,那么任何直接 Anthropic 访问都应走商业认证路径,而不是复用订阅 token
|
||||
|
||||
## Anthropic 明确警告的内容
|
||||
|
||||
Anthropic 当前的 “Logging in to your Claude account” 文章给出了最清晰的表述:
|
||||
|
||||
- 订阅计划仅适用于 Anthropic 原生应用的日常使用,包括 Claude Web、桌面端、移动端和 Claude Code
|
||||
- 对第三方工具(包括开源项目),“首选方式”是通过 Claude Console 或受支持云 provider 的 API key 认证
|
||||
- 如果你正在为他人构建产品、应用或工具,应使用 Claude Console API key 或受支持云 provider 的认证方式
|
||||
- 任何伪装身份、绕过订阅限制转发第三方流量、或以其他方式违反条款的工具,都被禁止
|
||||
|
||||
Anthropic 的消费条款还额外加入两项限制:
|
||||
|
||||
- 用户不得把账户登录信息、API keys 或账户凭据分享给他人
|
||||
- 除非是通过 Anthropic API key 访问服务,或者 Anthropic 明确允许,否则用户不得通过自动化或非人工方式访问这些服务
|
||||
|
||||
对 GSD2 的含义:
|
||||
|
||||
- 由 GSD 托管的 Anthropic 订阅 OAuth 流程属于高风险
|
||||
- 在 GSD 自己的 API client 中复用用户 Claude 订阅凭据属于高风险
|
||||
- 任何会让 Anthropic 误以为请求来自 Claude Code、但实际上来自 GSD 基础设施的流程,都越界了
|
||||
|
||||
## 当前 GSD2 发现
|
||||
|
||||
### 低风险 / 已对齐的部分
|
||||
|
||||
- `src/resources/extensions/claude-code-cli/index.ts`
|
||||
把 `claude-code` 注册成 `externalCli` provider,并通过 Anthropic 官方的 `@anthropic-ai/claude-agent-sdk` 路由
|
||||
- `src/resources/extensions/claude-code-cli/readiness.ts`
|
||||
只通过 `claude --version` 和 `claude auth status` 检查本地 CLI 是否存在以及认证状态
|
||||
- `src/onboarding.ts`
|
||||
TUI onboarding 已移除 Anthropic 浏览器 OAuth,并把本地 Claude Code 路由标记为符合 TOS 的路径
|
||||
- `src/cli.ts`
|
||||
当检测到本地 CLI 可用时,会把用户从 `anthropic` 迁移到 `claude-code`
|
||||
|
||||
这些方向是正确的,因为此时 GSD 使用的是用户自己本地安装的 Claude Code,作为已认证的 Anthropic surface。
|
||||
|
||||
### 中高风险部分 —— 已解决
|
||||
|
||||
所有 Anthropic OAuth 代码路径都已被移除:
|
||||
|
||||
- `packages/pi-ai/src/utils/oauth/anthropic.ts` —— **已删除**,不再实现 Anthropic OAuth 流程
|
||||
- `packages/pi-ai/src/utils/oauth/index.ts` —— **已更新**,内置注册表中移除了 `anthropicOAuthProvider`
|
||||
- `src/web/onboarding-service.ts` —— **已更新**,将 Anthropic 标记为 `supportsOAuth: false`
|
||||
- `packages/daemon/src/orchestrator.ts` —— **已更新**,去掉 OAuth token refresh,改为要求 `ANTHROPIC_API_KEY` 环境变量
|
||||
- `packages/pi-ai/src/providers/anthropic.ts` —— **已更新**,移除 OAuth client 分支,`isOAuthToken` 始终返回 false
|
||||
|
||||
## 针对 GSD2 的建议策略
|
||||
|
||||
将下面内容作为仓库规则:
|
||||
|
||||
- Claude 订阅认证只允许存在于 Anthropic 自有 surface 中:
|
||||
- `claude` CLI
|
||||
- 基于本地已认证 Claude Code 安装的 Claude Code SDK
|
||||
- 其他 Anthropic 文档明确支持的原生流程
|
||||
- GSD2 不得为终端用户实现自己的 Anthropic 订阅 OAuth 流程
|
||||
- GSD2 不得持久化 Anthropic 订阅 OAuth token,供后续 API 调用使用
|
||||
- GSD2 不得使用由 GSD 获取的订阅 OAuth tokens 来发送 Anthropic API 流量
|
||||
- GSD2 可以支持 Anthropic 直接访问,但仅限以下方式:
|
||||
- `ANTHROPIC_API_KEY`
|
||||
- 保存在 auth storage 中的 Claude Console API keys
|
||||
- `apiKeyHelper`
|
||||
- Bedrock / Vertex / Foundry
|
||||
- 本地 Claude Code provider
|
||||
|
||||
## 推荐实现方案
|
||||
|
||||
### 方案 A:安全的最小合规清理
|
||||
|
||||
1. 从内置 OAuth provider 注册表中移除 Anthropic
|
||||
2. 把 Web onboarding 中的 Anthropic 改为只支持 API key
|
||||
3. 当 `claude auth status` 成功时,继续保留 `claude-code` 作为推荐路径
|
||||
4. 增加明确的 UI 文案:
|
||||
- “Claude 订阅用户:请登录本地 Claude Code app / CLI,而不是 GSD。”
|
||||
5. 阻止任何把 Anthropic OAuth 凭据转换成 GSD 托管请求 API 认证的迁移或代码路径
|
||||
|
||||
这是让仓库与 Anthropic 当前公开指导对齐的最快路径。
|
||||
|
||||
### 方案 B:企业级安全的 Anthropic 支持
|
||||
|
||||
把 Anthropic 支持拆分成三种清晰模式:
|
||||
|
||||
- `claude-code`
|
||||
只使用本地已认证的 `claude` 运行时
|
||||
- `anthropic-api`
|
||||
使用 Console API keys 或 `apiKeyHelper`
|
||||
- `anthropic-cloud`
|
||||
使用 Bedrock、Vertex 或 Foundry
|
||||
|
||||
然后彻底移除任何模糊的 `anthropic` 浏览器登录路径。
|
||||
|
||||
这是长期最好的 UX,因为它清晰地区分了:
|
||||
|
||||
- 基于订阅的原生使用
|
||||
- 基于 API 计费的使用
|
||||
- 通过云路由的使用
|
||||
|
||||
## 具体仓库后续动作 —— 已完成
|
||||
|
||||
1. ~~删除或禁用 `packages/pi-ai/src/utils/oauth/anthropic.ts`。~~ **已完成** —— 文件已删除
|
||||
2. ~~从 `packages/pi-ai/src/utils/oauth/index.ts` 中移除 `anthropicOAuthProvider`。~~ **已完成**
|
||||
3. ~~修改 `src/web/onboarding-service.ts`,让 Anthropic 不再声称支持 OAuth。~~ **已完成**
|
||||
4. ~~审查 `packages/daemon/src/orchestrator.ts` 以及其他把 Anthropic OAuth access token 当作 API 凭据使用的调用方。~~ **已完成** —— daemon 现在要求 `ANTHROPIC_API_KEY`
|
||||
5. ~~更新文档 / UI 文案:直接 API 使用优先 `anthropic-api`,订阅使用优先 `claude-code`。~~ **已完成** —— `providers.md` 和 `getting-started.md` 已更新
|
||||
6. 添加测试,防止 Anthropic 订阅 OAuth 通过 onboarding / provider registry 被重新引入 —— **TODO**
|
||||
|
||||
## 决策规则
|
||||
|
||||
如果某个拟议中的 GSD2 特性需要访问 Anthropic,先问一个问题:
|
||||
|
||||
“GSD 是以 GSD 的身份调用 Anthropic,还是 GSD 只是把工作委派给用户本地已认证的 Claude Code 运行时?”
|
||||
|
||||
- 如果 GSD 是以 GSD 的身份调用 Anthropic:必须要求 API key 或受支持的云认证
|
||||
- 如果 GSD 只是委派给本地 Claude Code:可以接受,前提是 GSD 自身不会拦截、生成或重放订阅凭据
|
||||
|
||||
## 审查过的来源
|
||||
|
||||
- Anthropic Help Center: “Logging in to your Claude account”
|
||||
- Anthropic Help Center: “Using Claude Code with your Pro or Max plan”
|
||||
- Anthropic Help Center: “Use Claude Code with your Team or Enterprise plan”
|
||||
- Anthropic Help Center: “Managing API key environment variables in Claude Code”
|
||||
- Anthropic Help Center: “API Key Best Practices: Keeping Your Keys Safe and Secure”
|
||||
- Claude Code Docs:getting started / authentication / team / settings / IAM
|
||||
- Anthropic Commercial Terms of Service
|
||||
- Anthropic Consumer Terms of Service
|
||||
- Anthropic Usage Policy
|
||||
308
docs/zh-CN/user-docs/commands.md
Normal file
308
docs/zh-CN/user-docs/commands.md
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
# 命令参考
|
||||
|
||||
## 会话命令
|
||||
|
||||
| 命令 | 说明 |
|
||||
|------|------|
|
||||
| `/gsd` | Step mode:一次执行一个工作单元,并在每步之间暂停 |
|
||||
| `/gsd next` | 显式 Step mode(与 `/gsd` 相同) |
|
||||
| `/gsd auto` | 自动模式:research、plan、execute、commit,然后重复 |
|
||||
| `/gsd quick` | 在不经过完整 planning 开销的情况下,执行一个带 GSD 保证的 quick task(原子提交、状态跟踪) |
|
||||
| `/gsd stop` | 优雅地停止自动模式 |
|
||||
| `/gsd pause` | 暂停自动模式(保留状态,可用 `/gsd auto` 恢复) |
|
||||
| `/gsd steer` | 在执行过程中强制修改 plan 文档 |
|
||||
| `/gsd discuss` | 讨论架构和决策(可与自动模式并行使用) |
|
||||
| `/gsd status` | 进度仪表板 |
|
||||
| `/gsd widget` | 循环切换仪表板组件:full / small / min / off |
|
||||
| `/gsd queue` | 给未来 milestones 排队和重排(自动模式中也安全) |
|
||||
| `/gsd capture` | 随手记录一个想法,不打断当前流程(自动模式中可用) |
|
||||
| `/gsd triage` | 手动触发待处理 captures 的 triage |
|
||||
| `/gsd dispatch` | 直接派发一个指定阶段(research、plan、execute、complete、reassess、uat、replan) |
|
||||
| `/gsd history` | 查看执行历史(支持 `--cost`、`--phase`、`--model` 过滤) |
|
||||
| `/gsd forensics` | 全访问 GSD 调试器:用于分析自动模式失败,支持结构化异常检测、单元追踪和 LLM 引导的根因分析 |
|
||||
| `/gsd cleanup` | 清理 GSD 状态文件和过期 worktrees |
|
||||
| `/gsd visualize` | 打开工作流可视化器(进度、依赖、指标、时间线) |
|
||||
| `/gsd export --html` | 为当前或已完成的 milestone 生成自包含 HTML 报告 |
|
||||
| `/gsd export --html --all` | 一次性为所有 milestones 生成回顾报告 |
|
||||
| `/gsd update` | 在会话内更新到最新版本 |
|
||||
| `/gsd knowledge` | 添加持久化项目知识(规则、模式或经验) |
|
||||
| `/gsd fast` | 为支持的模型切换 service tier(优先级 API 路由) |
|
||||
| `/gsd rate` | 评价上一个单元所用模型层级(over / ok / under),帮助改进自适应路由 |
|
||||
| `/gsd changelog` | 查看分类后的发行说明 |
|
||||
| `/gsd logs` | 浏览活动日志、调试日志和指标 |
|
||||
| `/gsd remote` | 控制远程自动模式 |
|
||||
| `/gsd help` | 查看所有 GSD 子命令的分类参考及说明 |
|
||||
|
||||
## 配置与诊断
|
||||
|
||||
| 命令 | 说明 |
|
||||
|------|------|
|
||||
| `/gsd prefs` | 模型选择、超时和预算上限 |
|
||||
| `/gsd mode` | 切换工作流模式(solo / team),同时应用与 milestone ID、git 提交行为和文档相关的协调默认值 |
|
||||
| `/gsd config` | 重新运行 provider 配置向导(LLM provider + 工具 key) |
|
||||
| `/gsd keys` | API key 管理器:列出、添加、移除、测试、轮换、doctor |
|
||||
| `/gsd doctor` | 运行时健康检查与自动修复;问题会实时显示在 widget、visualizer 和 HTML reports 中(v2.40) |
|
||||
| `/gsd inspect` | 查看 SQLite DB 诊断信息 |
|
||||
| `/gsd init` | 项目初始化向导:检测、配置并 bootstrap `.gsd/` |
|
||||
| `/gsd setup` | 查看全局 setup 状态和配置 |
|
||||
| `/gsd skill-health` | 技能生命周期仪表板:使用统计、成功率、token 趋势、过期告警 |
|
||||
| `/gsd skill-health <name>` | 查看某个 skill 的详细信息 |
|
||||
| `/gsd skill-health --declining` | 只显示被标记为表现下降的 skills |
|
||||
| `/gsd skill-health --stale N` | 显示 N 天以上未使用的 skills |
|
||||
| `/gsd hooks` | 查看已配置的 post-unit 和 pre-dispatch hooks |
|
||||
| `/gsd run-hook` | 手动触发一个指定 hook |
|
||||
| `/gsd migrate` | 将 v1 的 `.planning` 目录迁移到 `.gsd` 格式 |
|
||||
|
||||
## Milestone 管理
|
||||
|
||||
| 命令 | 说明 |
|
||||
|------|------|
|
||||
| `/gsd new-milestone` | 创建一个新的 milestone |
|
||||
| `/gsd skip` | 阻止某个工作单元被自动模式派发 |
|
||||
| `/gsd undo` | 回退上一个已完成单元 |
|
||||
| `/gsd undo-task` | 重置某个特定 task 的完成状态(DB + markdown) |
|
||||
| `/gsd reset-slice` | 重置某个 slice 及其所有 tasks(DB + markdown) |
|
||||
| `/gsd park` | Park 一个 milestone,不删除,只跳过 |
|
||||
| `/gsd unpark` | 重新激活一个已 park 的 milestone |
|
||||
| Discard milestone | 在 `/gsd` 向导的 “Milestone actions” → “Discard” 中可用 |
|
||||
|
||||
## 并行编排
|
||||
|
||||
| 命令 | 说明 |
|
||||
|------|------|
|
||||
| `/gsd parallel start` | 分析可并行性、确认后启动 workers |
|
||||
| `/gsd parallel status` | 显示所有 workers 的状态、进度和成本 |
|
||||
| `/gsd parallel stop [MID]` | 停止所有 workers,或停止某个指定 milestone 的 worker |
|
||||
| `/gsd parallel pause [MID]` | 暂停所有 workers,或暂停某个指定 worker |
|
||||
| `/gsd parallel resume [MID]` | 恢复已暂停的 workers |
|
||||
| `/gsd parallel merge [MID]` | 把已完成的 milestones 合并回 main |
|
||||
|
||||
完整文档见 [并行编排](./parallel-orchestration.md)。
|
||||
|
||||
## Workflow Templates(v2.42)
|
||||
|
||||
| 命令 | 说明 |
|
||||
|------|------|
|
||||
| `/gsd start` | 启动一个 workflow template(bugfix、spike、feature、hotfix、refactor、security-audit、dep-upgrade、full-project) |
|
||||
| `/gsd start resume` | 恢复一个进行中的 workflow |
|
||||
| `/gsd templates` | 列出可用 workflow templates |
|
||||
| `/gsd templates info <name>` | 查看某个 template 的详细信息 |
|
||||
|
||||
## 自定义 Workflows(v2.42)
|
||||
|
||||
| 命令 | 说明 |
|
||||
|------|------|
|
||||
| `/gsd workflow new` | 创建一个新的 workflow definition(通过 skill) |
|
||||
| `/gsd workflow run <name>` | 创建一个 run 并启动自动模式 |
|
||||
| `/gsd workflow list` | 列出 workflow runs |
|
||||
| `/gsd workflow validate <name>` | 校验一个 workflow YAML definition |
|
||||
| `/gsd workflow pause` | 暂停自定义 workflow 的自动模式 |
|
||||
| `/gsd workflow resume` | 恢复已暂停的自定义 workflow 自动模式 |
|
||||
|
||||
## 扩展
|
||||
|
||||
| 命令 | 说明 |
|
||||
|------|------|
|
||||
| `/gsd extensions list` | 列出所有扩展及其状态 |
|
||||
| `/gsd extensions enable <id>` | 启用一个被禁用的扩展 |
|
||||
| `/gsd extensions disable <id>` | 禁用一个扩展 |
|
||||
| `/gsd extensions info <id>` | 查看扩展详情 |
|
||||
|
||||
## cmux 集成
|
||||
|
||||
| 命令 | 说明 |
|
||||
|------|------|
|
||||
| `/gsd cmux status` | 显示 cmux 检测结果、prefs 和能力 |
|
||||
| `/gsd cmux on` | 启用 cmux 集成 |
|
||||
| `/gsd cmux off` | 禁用 cmux 集成 |
|
||||
| `/gsd cmux notifications on/off` | 切换 cmux 桌面通知 |
|
||||
| `/gsd cmux sidebar on/off` | 切换 cmux 侧边栏元数据 |
|
||||
| `/gsd cmux splits on/off` | 切换 cmux subagent 可视化分屏 |
|
||||
|
||||
## GitHub Sync(v2.39)
|
||||
|
||||
| 命令 | 说明 |
|
||||
|------|------|
|
||||
| `/github-sync bootstrap` | 初始配置:根据当前 `.gsd/` 状态创建 GitHub Milestones、Issues 和 draft PRs |
|
||||
| `/github-sync status` | 显示同步映射数量(milestones、slices、tasks) |
|
||||
|
||||
在偏好设置里启用 `github.enabled: true`。要求已安装并认证 `gh` CLI。同步映射会保存在 `.gsd/.github-sync.json`。
|
||||
|
||||
## Git 命令
|
||||
|
||||
| 命令 | 说明 |
|
||||
|------|------|
|
||||
| `/worktree`(`/wt`) | Git worktree 生命周期管理:create、switch、merge、remove |
|
||||
|
||||
## 会话管理
|
||||
|
||||
| 命令 | 说明 |
|
||||
|------|------|
|
||||
| `/clear` | 启动一个新会话(`/new` 的别名) |
|
||||
| `/exit` | 优雅退出,会在退出前保存会话状态 |
|
||||
| `/kill` | 立即终止 GSD 进程 |
|
||||
| `/model` | 切换当前 active model |
|
||||
| `/login` | 登录一个 LLM provider |
|
||||
| `/thinking` | 在会话中切换 thinking level |
|
||||
| `/voice` | 切换实时语音转文字(macOS、Linux) |
|
||||
|
||||
## 键盘快捷键
|
||||
|
||||
| 快捷键 | 动作 |
|
||||
|--------|------|
|
||||
| `Ctrl+Alt+G` | 切换 dashboard overlay |
|
||||
| `Ctrl+Alt+V` | 切换语音转录 |
|
||||
| `Ctrl+Alt+B` | 显示后台 shell 进程 |
|
||||
| `Ctrl+V` / `Alt+V` | 从剪贴板粘贴图片(截图 → vision 输入) |
|
||||
| `Escape` | 暂停自动模式(保留对话) |
|
||||
|
||||
> **注意:** 在不支持 Kitty keyboard protocol 的终端中(如 macOS Terminal.app、JetBrains IDEs),界面会显示 slash-command 形式的回退命令,而不是 `Ctrl+Alt` 快捷键。
|
||||
>
|
||||
> **提示:** 如果 `Ctrl+V` 被终端拦截(例如 Warp),可改用 `Alt+V` 粘贴剪贴板图片。
|
||||
|
||||
## CLI 参数
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `gsd` | 启动新的交互式会话 |
|
||||
| `gsd --continue`(`-c`) | 恢复当前目录最近一次会话 |
|
||||
| `gsd --model <id>` | 为当前会话覆盖默认模型 |
|
||||
| `gsd --print "msg"`(`-p`) | 单次 prompt 模式(无 TUI) |
|
||||
| `gsd --mode <text\|json\|rpc\|mcp>` | 非交互使用时的输出模式 |
|
||||
| `gsd --list-models [search]` | 列出可用模型并退出 |
|
||||
| `gsd --web [path]` | 启动基于浏览器的 Web 界面(可选项目路径) |
|
||||
| `gsd --worktree`(`-w`)[name] | 在 git worktree 中启动会话(未指定时自动生成名称) |
|
||||
| `gsd --no-session` | 禁用会话持久化 |
|
||||
| `gsd --extension <path>` | 加载一个额外扩展(可重复) |
|
||||
| `gsd --append-system-prompt <text>` | 向 system prompt 末尾追加文本 |
|
||||
| `gsd --tools <list>` | 启用的工具列表,逗号分隔 |
|
||||
| `gsd --version`(`-v`) | 输出版本并退出 |
|
||||
| `gsd --help`(`-h`) | 输出帮助并退出 |
|
||||
| `gsd sessions` | 交互式会话选择器:列出当前目录所有保存的会话并选择一个恢复 |
|
||||
| `gsd --debug` | 启用结构化 JSONL 诊断日志,用于排查 dispatch 和 state 问题 |
|
||||
| `gsd config` | 配置搜索和文档工具所需的全局 API keys(保存到 `~/.gsd/agent/auth.json`,对所有项目生效)。见 [Global API Keys](./configuration.md#global-api-keys-gsd-config)。 |
|
||||
| `gsd update` | 更新到最新版本 |
|
||||
| `gsd headless new-milestone` | 根据上下文文件创建新的 milestone(headless,无需 TUI) |
|
||||
|
||||
## Headless 模式
|
||||
|
||||
`gsd headless` 可在无 TUI 的情况下运行 `/gsd` 命令,适合 CI、cron job 和脚本自动化。它会在 RPC 模式下启动一个子进程,自动回应交互式提示、检测完成状态,并用有意义的退出码退出。
|
||||
|
||||
```bash
|
||||
# 运行自动模式(默认)
|
||||
gsd headless
|
||||
|
||||
# 运行一个单元
|
||||
gsd headless next
|
||||
|
||||
# 即时 JSON 快照,无需 LLM,约 50ms
|
||||
gsd headless query
|
||||
|
||||
# 用于 CI 的超时参数
|
||||
gsd headless --timeout 600000 auto
|
||||
|
||||
# 强制指定一个 phase
|
||||
gsd headless dispatch plan
|
||||
|
||||
# 根据上下文文件创建新 milestone,并启动自动模式
|
||||
gsd headless new-milestone --context brief.md --auto
|
||||
|
||||
# 用内联文本创建 milestone
|
||||
gsd headless new-milestone --context-text "Build a REST API with auth"
|
||||
|
||||
# 从 stdin 管道输入上下文
|
||||
echo "Build a CLI tool" | gsd headless new-milestone --context -
|
||||
```
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `--timeout N` | 总超时(毫秒),默认 `300000` / 5 分钟 |
|
||||
| `--max-restarts N` | 崩溃时自动重启并指数退避(默认 3)。设为 0 可关闭 |
|
||||
| `--json` | 以 JSONL 形式把所有事件流式输出到 stdout |
|
||||
| `--model ID` | 覆盖 headless 会话使用的模型 |
|
||||
| `--context <file>` | 给 `new-milestone` 提供上下文文件(用 `-` 表示 stdin) |
|
||||
| `--context-text <text>` | 给 `new-milestone` 提供内联上下文文本 |
|
||||
| `--auto` | 在创建 milestone 后直接接续自动模式 |
|
||||
|
||||
**退出码:** `0` 表示完成,`1` 表示错误或超时,`2` 表示被阻塞。
|
||||
|
||||
任何 `/gsd` 子命令都可以作为位置参数使用,例如:`gsd headless status`、`gsd headless doctor`、`gsd headless dispatch execute` 等。
|
||||
|
||||
### `gsd headless query`
|
||||
|
||||
它会返回单个 JSON 对象,包含完整项目快照,无需 LLM 会话,也无需 RPC 子进程,响应几乎即时(约 50ms)。这是 orchestration 工具和脚本检查 GSD 状态的推荐方式。
|
||||
|
||||
```bash
|
||||
gsd headless query | jq '.state.phase'
|
||||
# "executing"
|
||||
|
||||
gsd headless query | jq '.next'
|
||||
# {"action":"dispatch","unitType":"execute-task","unitId":"M001/S01/T03"}
|
||||
|
||||
gsd headless query | jq '.cost.total'
|
||||
# 4.25
|
||||
```
|
||||
|
||||
**输出结构:**
|
||||
|
||||
```json
|
||||
{
|
||||
"state": {
|
||||
"phase": "executing",
|
||||
"activeMilestone": { "id": "M001", "title": "..." },
|
||||
"activeSlice": { "id": "S01", "title": "..." },
|
||||
"activeTask": { "id": "T01", "title": "..." },
|
||||
"registry": [{ "id": "M001", "status": "active" }, ...],
|
||||
"progress": { "milestones": { "done": 0, "total": 2 }, "slices": { "done": 1, "total": 3 } },
|
||||
"blockers": []
|
||||
},
|
||||
"next": {
|
||||
"action": "dispatch",
|
||||
"unitType": "execute-task",
|
||||
"unitId": "M001/S01/T01"
|
||||
},
|
||||
"cost": {
|
||||
"workers": [{ "milestoneId": "M001", "cost": 1.50, "state": "running", ... }],
|
||||
"total": 1.50
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<a id="mcp-server-mode"></a>
|
||||
## MCP Server 模式
|
||||
|
||||
`gsd --mode mcp` 会通过 stdin/stdout 将 GSD 作为一个 [Model Context Protocol](https://modelcontextprotocol.io) server 运行。这会把所有 GSD 工具(read、write、edit、bash 等)暴露给外部 AI 客户端,例如 Claude Desktop、VS Code Copilot,以及任何兼容 MCP 的宿主。
|
||||
|
||||
```bash
|
||||
# 以 MCP server 模式启动 GSD
|
||||
gsd --mode mcp
|
||||
```
|
||||
|
||||
服务会注册 agent 会话中的全部工具,并把 MCP 的 `tools/list` 与 `tools/call` 请求映射到 GSD 的工具定义上。连接会一直保持,直到底层 transport 关闭。
|
||||
|
||||
## 会话内更新
|
||||
|
||||
`/gsd update` 会检查 npm 上是否有更新版本,并在不离开当前会话的情况下完成安装。
|
||||
|
||||
```bash
|
||||
/gsd update
|
||||
# Current version: v2.36.0
|
||||
# Checking npm registry...
|
||||
# Updated to v2.37.0. Restart GSD to use the new version.
|
||||
```
|
||||
|
||||
如果已经是最新版本,它会给出提示且不做任何操作。
|
||||
|
||||
## 导出
|
||||
|
||||
`/gsd export` 用于导出 milestone 工作报告。
|
||||
|
||||
```bash
|
||||
# 为当前 active milestone 生成 HTML 报告
|
||||
/gsd export --html
|
||||
|
||||
# 一次性为所有 milestones 生成回顾报告
|
||||
/gsd export --html --all
|
||||
```
|
||||
|
||||
报告会保存到 `.gsd/reports/`,并生成一个可浏览的 `index.html`,链接到所有已生成的快照。
|
||||
852
docs/zh-CN/user-docs/configuration.md
Normal file
852
docs/zh-CN/user-docs/configuration.md
Normal file
|
|
@ -0,0 +1,852 @@
|
|||
# 配置
|
||||
|
||||
GSD 偏好设置保存在 `~/.gsd/PREFERENCES.md`(全局)或 `.gsd/PREFERENCES.md`(项目级)中。可以通过 `/gsd prefs` 进行交互式管理。
|
||||
|
||||
## `/gsd prefs` 命令
|
||||
|
||||
| 命令 | 说明 |
|
||||
|------|------|
|
||||
| `/gsd prefs` | 打开全局偏好设置向导(默认) |
|
||||
| `/gsd prefs global` | 全局偏好设置交互向导(`~/.gsd/PREFERENCES.md`) |
|
||||
| `/gsd prefs project` | 项目偏好设置交互向导(`.gsd/PREFERENCES.md`) |
|
||||
| `/gsd prefs status` | 显示当前偏好文件、合并后的值以及 skill 解析状态 |
|
||||
| `/gsd prefs wizard` | `/gsd prefs global` 的别名 |
|
||||
| `/gsd prefs setup` | `/gsd prefs wizard` 的别名;若偏好文件不存在会自动创建 |
|
||||
| `/gsd prefs import-claude` | 将 Claude marketplace plugins 和 skills 以命名空间化的 GSD 组件形式导入 |
|
||||
| `/gsd prefs import-claude global` | 导入到全局作用域 |
|
||||
| `/gsd prefs import-claude project` | 导入到项目作用域 |
|
||||
|
||||
## 偏好文件格式
|
||||
|
||||
偏好设置使用 markdown 文件中的 YAML frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
version: 1
|
||||
models:
|
||||
research: claude-sonnet-4-6
|
||||
planning: claude-opus-4-6
|
||||
execution: claude-sonnet-4-6
|
||||
completion: claude-sonnet-4-6
|
||||
skill_discovery: suggest
|
||||
auto_supervisor:
|
||||
soft_timeout_minutes: 20
|
||||
idle_timeout_minutes: 10
|
||||
hard_timeout_minutes: 30
|
||||
budget_ceiling: 50.00
|
||||
token_profile: balanced
|
||||
---
|
||||
```
|
||||
|
||||
## 全局与项目偏好
|
||||
|
||||
| 作用域 | 路径 | 适用范围 |
|
||||
|--------|------|----------|
|
||||
| 全局 | `~/.gsd/PREFERENCES.md` | 所有项目 |
|
||||
| 项目 | `.gsd/PREFERENCES.md` | 仅当前项目 |
|
||||
|
||||
**合并规则:**
|
||||
|
||||
- **标量字段**(`skill_discovery`、`budget_ceiling`):如果项目级定义了,则项目级优先
|
||||
- **数组字段**(`always_use_skills` 等):拼接,顺序为全局在前、项目在后
|
||||
- **对象字段**(`models`、`git`、`auto_supervisor`):浅合并,项目级按 key 覆盖
|
||||
|
||||
<a id="global-api-keys-gsd-config"></a>
|
||||
## 全局 API Keys(`/gsd config`)
|
||||
|
||||
工具 API keys 会全局保存在 `~/.gsd/agent/auth.json` 中,并自动应用到所有项目。只需用 `/gsd config` 配置一次,无需在每个项目里维护 `.env`。
|
||||
|
||||
```bash
|
||||
/gsd config
|
||||
```
|
||||
|
||||
这会打开一个交互式向导,显示哪些 key 已配置、哪些仍缺失。你可以选择一个工具并输入相应的 key。
|
||||
|
||||
### 支持的 keys
|
||||
|
||||
| 工具 | 环境变量 | 用途 | 获取地址 |
|
||||
|------|----------|------|----------|
|
||||
| Tavily Search | `TAVILY_API_KEY` | 为非 Anthropic models 提供 Web 搜索 | [tavily.com/app/api-keys](https://tavily.com/app/api-keys) |
|
||||
| Brave Search | `BRAVE_API_KEY` | 为非 Anthropic models 提供 Web 搜索 | [brave.com/search/api](https://brave.com/search/api) |
|
||||
| Context7 Docs | `CONTEXT7_API_KEY` | 库文档检索 | [context7.com/dashboard](https://context7.com/dashboard) |
|
||||
|
||||
### 工作方式
|
||||
|
||||
1. `/gsd config` 会把 keys 保存到 `~/.gsd/agent/auth.json`
|
||||
2. 每次会话启动时,`loadToolApiKeys()` 都会读取该文件并设置环境变量
|
||||
3. 这些 keys 对所有项目生效,无需单独配置
|
||||
4. 环境变量(例如 `export BRAVE_API_KEY=...`)优先级高于保存下来的 keys
|
||||
5. Anthropic models 不需要 Brave/Tavily,因为它们自带 Web 搜索
|
||||
|
||||
## MCP Servers
|
||||
|
||||
GSD 可以连接配置在项目文件中的外部 MCP servers。这适合接入本地工具、内部 API、自托管服务,或者那些未作为 GSD 原生扩展内置的集成。
|
||||
|
||||
### 配置文件位置
|
||||
|
||||
GSD 会从以下项目本地路径读取 MCP client 配置:
|
||||
|
||||
- `.mcp.json`
|
||||
- `.gsd/mcp.json`
|
||||
|
||||
如果两个文件都存在,会按 server 名称做合并,先找到的定义优先。通常建议:
|
||||
|
||||
- 把你愿意提交到仓库的共享 MCP 配置放在 `.mcp.json`
|
||||
- 把仅本机使用、不希望共享的 MCP 配置放在 `.gsd/mcp.json`
|
||||
|
||||
### 支持的 transport
|
||||
|
||||
| Transport | 配置形状 | 适用场景 |
|
||||
|-----------|----------|----------|
|
||||
| `stdio` | `command` + 可选 `args`、`env`、`cwd` | 启动本地 MCP server 进程 |
|
||||
| `http` | `url` | 连接到已经运行中的 MCP server |
|
||||
|
||||
### 示例:stdio server
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"my-server": {
|
||||
"type": "stdio",
|
||||
"command": "/absolute/path/to/python3",
|
||||
"args": ["/absolute/path/to/server.py"],
|
||||
"env": {
|
||||
"API_URL": "http://localhost:8000"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 示例:HTTP server
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"my-http-server": {
|
||||
"url": "http://localhost:8080/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 验证一个 server
|
||||
|
||||
添加配置后,可以在 GSD 会话中这样验证:
|
||||
|
||||
```text
|
||||
mcp_servers
|
||||
mcp_discover(server="my-server")
|
||||
mcp_call(server="my-server", tool="<tool_name>", args={...})
|
||||
```
|
||||
|
||||
推荐验证顺序:
|
||||
|
||||
1. `mcp_servers`:确认 GSD 能看到配置文件并正确解析 server 条目
|
||||
2. `mcp_discover`:确认 server 进程能启动,并能响应 `tools/list`
|
||||
3. `mcp_call`:确认至少有一个真实 tool 可以成功调用
|
||||
|
||||
### 说明
|
||||
|
||||
- 尽量为本地可执行文件和脚本使用绝对路径
|
||||
- 对于 `stdio` servers,优先在 MCP 配置里显式设置需要的环境变量,而不是依赖交互式 shell profile
|
||||
- GSD 和 `gsd-mcp-server` 都会自动加载保存在 `~/.gsd/agent/auth.json` 中的 model / tool keys,因此 MCP 配置可以安全地通过 `${ENV_VAR}` 占位符引用这些值,而不必提交原始凭据
|
||||
- 如果某个 server 是团队共享且适合提交到仓库,通常更适合放在 `.mcp.json`
|
||||
- 如果某个 server 依赖本机路径、个人服务或本地 secrets,更适合放在 `.gsd/mcp.json`
|
||||
|
||||
## 环境变量
|
||||
|
||||
| 变量 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `GSD_HOME` | `~/.gsd` | 全局 GSD 目录。除非单独覆盖,否则其它路径都从这里派生。影响偏好、skills、sessions 以及项目状态。(v2.39) |
|
||||
| `GSD_PROJECT_ID` | (自动哈希) | 覆盖自动生成的项目身份哈希。这样项目状态会写入 `$GSD_HOME/projects/<GSD_PROJECT_ID>/`,而不是计算出的哈希目录。适用于 CI/CD 或多个克隆共享状态。(v2.39) |
|
||||
| `GSD_STATE_DIR` | `$GSD_HOME` | 项目状态根目录。控制 `projects/<repo-hash>/` 的创建位置。对项目状态的优先级高于 `GSD_HOME`。 |
|
||||
| `GSD_CODING_AGENT_DIR` | `$GSD_HOME/agent` | agent 目录,包含托管资源、扩展和 auth。对 agent 相关路径的优先级高于 `GSD_HOME`。 |
|
||||
| `GSD_ALLOWED_COMMAND_PREFIXES` | (内置列表) | 允许用于 `!command` 值解析的命令前缀,逗号分隔。会覆盖 settings.json 中的 `allowedCommandPrefixes`。见 [自定义模型:命令允许列表](custom-models.md#command-allowlist)。 |
|
||||
| `GSD_FETCH_ALLOWED_URLS` | (无) | 对 `fetch_page` URL block 免检的 hostnames,逗号分隔。会覆盖 settings.json 中的 `fetchAllowedUrls`。见 [URL Blocking](#url-blocking-fetch_page)。 |
|
||||
|
||||
## 全部设置
|
||||
|
||||
### `models`
|
||||
|
||||
按阶段选择 model。每个 key 都可以是一个 model 字符串,或者是带 fallbacks 的对象。
|
||||
|
||||
```yaml
|
||||
models:
|
||||
research: claude-sonnet-4-6
|
||||
planning:
|
||||
model: claude-opus-4-6
|
||||
fallbacks:
|
||||
- openrouter/z-ai/glm-5
|
||||
execution: claude-sonnet-4-6
|
||||
execution_simple: claude-haiku-4-5-20250414
|
||||
completion: claude-sonnet-4-6
|
||||
subagent: claude-sonnet-4-6
|
||||
```
|
||||
|
||||
**阶段键:** `research`、`planning`、`execution`、`execution_simple`、`completion`、`subagent`
|
||||
|
||||
- `execution_simple`:用于被 [complexity router](./token-optimization.md#complexity-based-task-routing) 判断为 “simple” 的 task
|
||||
- `subagent`:委派给 subagent 的 task 所使用的 model(scout、researcher、worker)
|
||||
- 指定 provider:使用 `provider/model` 格式(例如 `bedrock/claude-sonnet-4-6`),或者在对象格式里额外写 `provider` 字段
|
||||
- 省略某个 key 时,会使用当前 active model
|
||||
|
||||
### 自定义 Model 定义(`models.json`)
|
||||
|
||||
你可以在 `~/.gsd/agent/models.json` 里定义自定义 models 和 providers。这允许你添加默认注册表里没有的 models,适合自托管 endpoints(Ollama、vLLM、LM Studio)、微调模型、代理,或者刚发布的新 provider。
|
||||
|
||||
GSD 读取 `models.json` 的顺序如下:
|
||||
|
||||
1. `~/.gsd/agent/models.json`:主位置(GSD)
|
||||
2. `~/.pi/agent/models.json`:回退位置(Pi)
|
||||
3. 如果两者都不存在,则创建 `~/.gsd/agent/models.json`
|
||||
|
||||
**本地 models(Ollama)的快速示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"ollama": {
|
||||
"baseUrl": "http://localhost:11434/v1",
|
||||
"api": "openai-completions",
|
||||
"apiKey": "ollama",
|
||||
"models": [
|
||||
{ "id": "llama3.1:8b" },
|
||||
{ "id": "qwen2.5-coder:7b" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
每次打开 `/model` 时,这个文件都会重新加载,无需重启。
|
||||
|
||||
关于 provider 配置、model overrides、OpenAI compatibility 和更多高级示例,见 [自定义模型指南](./custom-models.md)。
|
||||
|
||||
**带 fallbacks 的示例:**
|
||||
|
||||
```yaml
|
||||
models:
|
||||
planning:
|
||||
model: claude-opus-4-6
|
||||
fallbacks:
|
||||
- openrouter/z-ai/glm-5
|
||||
- openrouter/moonshotai/kimi-k2.5
|
||||
provider: bedrock # 可选:固定到某个 provider
|
||||
```
|
||||
|
||||
当某个 model 切换失败(provider 不可用、被限流、额度耗尽)时,GSD 会自动尝试 `fallbacks` 列表中的下一个 model。
|
||||
|
||||
### Community Provider Extensions
|
||||
|
||||
对于 GSD 未内置的 providers,社区扩展可以添加完整 provider 支持,包括正确的 model 定义、thinking format 配置以及交互式 API key 设置。
|
||||
|
||||
| 扩展 | Provider | Models | 安装命令 |
|
||||
|------|----------|--------|----------|
|
||||
| [`pi-dashscope`](https://www.npmjs.com/package/pi-dashscope) | Alibaba DashScope(ModelStudio) | Qwen3、GLM-5、MiniMax M2.5、Kimi K2.5 | `gsd install npm:pi-dashscope` |
|
||||
|
||||
对于 DashScope models,更推荐使用社区扩展而不是内置的 `alibaba-coding-plan` provider,因为前者会走正确的 OpenAI-compatible endpoint,并包含适配 thinking mode 的 per-model compatibility flags。
|
||||
|
||||
### `token_profile`
|
||||
|
||||
负责协调 model 选择、阶段跳过和上下文压缩。详见 [Token 优化](./token-optimization.md)。
|
||||
|
||||
可选值:`budget`、`balanced`(默认)、`quality`
|
||||
|
||||
| 配置 | 行为 |
|
||||
|------|------|
|
||||
| `budget` | 跳过 research + reassessment 阶段,优先使用便宜模型 |
|
||||
| `balanced` | 默认行为:所有阶段运行,使用标准模型选择 |
|
||||
| `quality` | 所有阶段运行,优先更高质量模型 |
|
||||
|
||||
### `phases`
|
||||
|
||||
对自动模式中哪些阶段运行做细粒度控制:
|
||||
|
||||
```yaml
|
||||
phases:
|
||||
skip_research: false # 跳过 milestone 级 research
|
||||
skip_reassess: false # 在每个 slice 后跳过 roadmap reassessment
|
||||
skip_slice_research: true # 跳过每个 slice 的 research
|
||||
reassess_after_slice: true # 每个 slice 后执行 roadmap reassessment(reassessment 的前提)
|
||||
require_slice_discussion: false # 每个 slice 前暂停,等待讨论
|
||||
```
|
||||
|
||||
这些值通常由 `token_profile` 自动设置,但也可以显式覆盖。
|
||||
|
||||
> **注意:** Roadmap reassessment 需要显式设置 `reassess_after_slice: true`。如果没有它,无论 `skip_reassess` 怎么配,reassessment 都不会运行。
|
||||
|
||||
### `skill_discovery`
|
||||
|
||||
控制 GSD 在自动模式中如何发现并应用 skills。
|
||||
|
||||
| 值 | 行为 |
|
||||
|----|------|
|
||||
| `auto` | 自动查找并应用 skills |
|
||||
| `suggest` | 在 research 阶段识别到 skills,但不自动安装(默认) |
|
||||
| `off` | 关闭 skill discovery |
|
||||
|
||||
### `auto_supervisor`
|
||||
|
||||
自动模式监督器使用的超时阈值:
|
||||
|
||||
```yaml
|
||||
auto_supervisor:
|
||||
model: claude-sonnet-4-6 # 可选:supervisor 使用的 model(默认当前 active model)
|
||||
soft_timeout_minutes: 20 # 提醒 LLM 收尾
|
||||
idle_timeout_minutes: 10 # 检测停滞
|
||||
hard_timeout_minutes: 30 # 暂停自动模式
|
||||
```
|
||||
|
||||
### `budget_ceiling`
|
||||
|
||||
自动模式期间允许消耗的最大美元金额。不需要 `$`,直接填数字:
|
||||
|
||||
```yaml
|
||||
budget_ceiling: 50.00
|
||||
```
|
||||
|
||||
### `budget_enforcement`
|
||||
|
||||
预算上限的执行方式:
|
||||
|
||||
| 值 | 行为 |
|
||||
|----|------|
|
||||
| `warn` | 记录警告,但继续运行 |
|
||||
| `pause` | 暂停自动模式(设置 ceiling 时的默认值) |
|
||||
| `halt` | 彻底停止自动模式 |
|
||||
|
||||
### `context_pause_threshold`
|
||||
|
||||
上下文窗口使用率达到多少(0-100)时,自动模式会暂停并进行 checkpoint。设为 `0` 可关闭。
|
||||
|
||||
```yaml
|
||||
context_pause_threshold: 80 # 在上下文使用达到 80% 时暂停
|
||||
```
|
||||
|
||||
默认值:`0`(关闭)
|
||||
|
||||
### `uat_dispatch`
|
||||
|
||||
在 slice 完成后自动运行 UAT(User Acceptance Test):
|
||||
|
||||
```yaml
|
||||
uat_dispatch: true
|
||||
```
|
||||
|
||||
### Verification(v2.26)
|
||||
|
||||
配置在每次 task 执行后自动运行的 shell 命令。若失败,会先尝试自动修复重试,再决定是否继续。
|
||||
|
||||
```yaml
|
||||
verification_commands:
|
||||
- npm run lint
|
||||
- npm run test
|
||||
verification_auto_fix: true # 失败时自动重试修复(默认:true)
|
||||
verification_max_retries: 2 # 最大重试次数(默认:2)
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `verification_commands` | string[] | `[]` | task 执行后要运行的 shell 命令 |
|
||||
| `verification_auto_fix` | boolean | `true` | verification 失败时是否自动重试 |
|
||||
| `verification_max_retries` | number | `2` | 自动修复重试的最大次数 |
|
||||
|
||||
<a id="url-blocking-fetch_page"></a>
|
||||
### URL Blocking(`fetch_page`)
|
||||
|
||||
`fetch_page` 工具默认会阻止访问私有网络和内部网络地址,以防 SSRF(server-side request forgery)。这能防止 agent 被诱导去访问内部服务、云 metadata endpoint 或本地文件。
|
||||
|
||||
**默认会被拦截:**
|
||||
|
||||
| 类别 | 示例 |
|
||||
|------|------|
|
||||
| 私有 IP 段 | `10.x.x.x`、`172.16-31.x.x`、`192.168.x.x`、`127.x.x.x` |
|
||||
| Link-local / 云 metadata | `169.254.x.x`(AWS/GCP instance metadata) |
|
||||
| 云 metadata hostname | `metadata.google.internal`、`instance-data` |
|
||||
| Localhost | `localhost`(任意端口) |
|
||||
| 非 HTTP 协议 | `file://`、`ftp://` |
|
||||
| IPv6 私有地址段 | `::1`、`fc00:`、`fd`、`fe80:` |
|
||||
|
||||
公共 URL(例如 `https://example.com`、`http://8.8.8.8`)不受影响。
|
||||
|
||||
**允许特定内部主机:**
|
||||
|
||||
如果你确实需要 agent 访问内网 URL(例如自托管文档、VPN 后的内部 API),可以在全局设置 `~/.gsd/agent/settings.json` 中添加 `fetchAllowedUrls`:
|
||||
|
||||
```json
|
||||
{
|
||||
"fetchAllowedUrls": ["internal-docs.company.com", "192.168.1.50"]
|
||||
}
|
||||
```
|
||||
|
||||
或者设置 `GSD_FETCH_ALLOWED_URLS` 环境变量(逗号分隔)。环境变量优先级高于 settings.json:
|
||||
|
||||
```bash
|
||||
export GSD_FETCH_ALLOWED_URLS="internal-docs.company.com,192.168.1.50"
|
||||
```
|
||||
|
||||
被允许的 hostname 会绕过 blocklist 检查。但协议限制依然有效,也就是说 `file://` 和 `ftp://` 仍然不能加入 allowlist。
|
||||
|
||||
> **注意:** 这是一个仅全局生效的设置。项目级 settings.json 不能覆盖 URL allowlist,以防克隆下来的仓库把 `fetch_page` 指向内部基础设施。
|
||||
|
||||
### `auto_report`(v2.26)
|
||||
|
||||
在 milestone 完成后自动生成 HTML 报告:
|
||||
|
||||
```yaml
|
||||
auto_report: true # 默认:true
|
||||
```
|
||||
|
||||
报告会以自包含 HTML 文件的形式写入 `.gsd/reports/`,所有 CSS / JS 都内嵌。
|
||||
|
||||
### `unique_milestone_ids`
|
||||
|
||||
为 milestone IDs 添加随机后缀,以避免团队协作中的 ID 冲突:
|
||||
|
||||
```yaml
|
||||
unique_milestone_ids: true
|
||||
# 输出示例:M001-eh88as,而不是 M001
|
||||
```
|
||||
|
||||
### `git`
|
||||
|
||||
Git 行为配置。所有字段都是可选的:
|
||||
|
||||
```yaml
|
||||
git:
|
||||
auto_push: false # 提交后推送到远程
|
||||
push_branches: false # 推送 milestone 分支到远程
|
||||
remote: origin # git remote 名称
|
||||
snapshots: true # 长 task 执行期间做 WIP snapshot commits
|
||||
pre_merge_check: auto # worktree merge 前执行检查(true / false / "auto")
|
||||
commit_type: feat # 覆盖 conventional commit 前缀
|
||||
main_branch: main # 主分支名称
|
||||
merge_strategy: squash # worktree 分支合并方式:"squash" 或 "merge"
|
||||
isolation: worktree # git isolation:"worktree"、"branch" 或 "none"
|
||||
commit_docs: true # 是否把 .gsd/ 产物提交到 git(设为 false 时仅保留本地)
|
||||
manage_gitignore: true # 设为 false 时,GSD 不再修改 .gitignore
|
||||
worktree_post_create: .gsd/hooks/post-worktree-create # worktree 创建后执行的脚本
|
||||
auto_pr: false # milestone 完成时自动创建 PR(要求 push_branches)
|
||||
pr_target_branch: develop # 自动创建 PR 的目标分支(默认:main branch)
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `auto_push` | boolean | `false` | 提交后推送到远程 |
|
||||
| `push_branches` | boolean | `false` | 把 milestone 分支推送到远程 |
|
||||
| `remote` | string | `"origin"` | Git remote 名称 |
|
||||
| `snapshots` | boolean | `true` | 长 task 期间做 WIP snapshot commits |
|
||||
| `pre_merge_check` | bool/string | `"auto"` | merge 前是否执行检查(`true` / `false` / `"auto"`) |
|
||||
| `commit_type` | string | (自动推断) | 覆盖 conventional commit 前缀(`feat`、`fix`、`refactor`、`docs`、`test`、`chore`、`perf`、`ci`、`build`、`style`) |
|
||||
| `main_branch` | string | `"main"` | 主分支名称 |
|
||||
| `merge_strategy` | string | `"squash"` | worktree 分支合并方式:`"squash"`(合并为单个提交)或 `"merge"`(保留单独提交) |
|
||||
| `isolation` | string | `"worktree"` | 自动模式隔离方式:`"worktree"`(独立目录)、`"branch"`(直接在项目根目录工作,适合子模块多的仓库)、`"none"`(无隔离,直接提交到当前分支) |
|
||||
| `commit_docs` | boolean | `true` | 是否把 `.gsd/` planning 产物提交到 git。设为 `false` 则仅保留本地 |
|
||||
| `manage_gitignore` | boolean | `true` | 设为 `false` 后,GSD 将完全不修改 `.gitignore`,不会添加基础规则,也不会做自愈 |
|
||||
| `worktree_post_create` | string | (无) | worktree 创建后执行的脚本。环境变量中会传入 `SOURCE_DIR` 和 `WORKTREE_DIR` |
|
||||
| `auto_pr` | boolean | `false` | milestone 完成时自动创建 pull request。要求 `auto_push: true` 且已安装认证 `gh` CLI |
|
||||
| `pr_target_branch` | string | (main branch) | 自动创建 PR 的目标分支,例如 `develop`、`qa`。未设置时默认回退到 `main_branch` |
|
||||
|
||||
#### `git.worktree_post_create`
|
||||
|
||||
在 worktree 创建后执行脚本(自动模式和手动 `/worktree` 都适用)。适合复制 `.env`、建立资源目录软链,或者执行那些 worktree 不会继承的 setup 步骤。
|
||||
|
||||
```yaml
|
||||
git:
|
||||
worktree_post_create: .gsd/hooks/post-worktree-create
|
||||
```
|
||||
|
||||
脚本会收到两个环境变量:
|
||||
|
||||
- `SOURCE_DIR`:原始项目根目录
|
||||
- `WORKTREE_DIR`:新创建的 worktree 路径
|
||||
|
||||
示例 hook(`.gsd/hooks/post-worktree-create`):
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Copy environment files and symlink assets into the new worktree
|
||||
cp "$SOURCE_DIR/.env" "$WORKTREE_DIR/.env"
|
||||
cp "$SOURCE_DIR/.env.local" "$WORKTREE_DIR/.env.local" 2>/dev/null || true
|
||||
ln -sf "$SOURCE_DIR/assets" "$WORKTREE_DIR/assets"
|
||||
```
|
||||
|
||||
路径既可以是绝对路径,也可以相对项目根目录。脚本有 30 秒超时限制。失败不会中断流程,GSD 会记录告警后继续。
|
||||
|
||||
<a id="gitauto_pr"></a>
|
||||
#### `git.auto_pr`
|
||||
|
||||
在 milestone 完成时自动创建 pull request。适用于 Gitflow 或分支工作流团队,在合并到目标分支前通过 PR 做审查。
|
||||
|
||||
```yaml
|
||||
git:
|
||||
auto_push: true
|
||||
auto_pr: true
|
||||
pr_target_branch: develop # 或 qa、staging 等
|
||||
```
|
||||
|
||||
**要求:**
|
||||
|
||||
- `auto_push: true`:创建 PR 前必须先把 milestone 分支推送到远程
|
||||
- 已安装并认证 [`gh` CLI](https://cli.github.com/)(`gh auth login`)
|
||||
|
||||
**工作方式:**
|
||||
|
||||
1. milestone 完成后,GSD 先把 worktree squash merge 回主分支
|
||||
2. 如果 `auto_push: true`,把主分支推送到远程
|
||||
3. 把 milestone 分支推送到远程
|
||||
4. 通过 `gh pr create` 从 milestone 分支向 `pr_target_branch` 创建 PR
|
||||
|
||||
如果没有设置 `pr_target_branch`,PR 会默认指向 `main_branch`(或者自动检测出的主分支)。PR 创建失败不会中断流程,GSD 会记录日志后继续。
|
||||
|
||||
### `github`(v2.39)
|
||||
|
||||
GitHub 同步配置。启用后,GSD 会自动把 milestones、slices 和 tasks 同步到 GitHub Issues、PRs 和 Milestones。
|
||||
|
||||
```yaml
|
||||
github:
|
||||
enabled: true
|
||||
repo: "owner/repo" # 省略时从 git remote 自动检测
|
||||
labels: [gsd, auto-generated] # 应用到创建出的 issues / PRs 的标签
|
||||
project: "Project ID" # 可选的 GitHub Project board
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `enabled` | boolean | `false` | 是否启用 GitHub 同步 |
|
||||
| `repo` | string | (自动检测) | `owner/repo` 格式的 GitHub 仓库名 |
|
||||
| `labels` | string[] | `[]` | 创建的 issues / PRs 要附加的标签 |
|
||||
| `project` | string | (无) | GitHub Project ID,用于接入 Project board |
|
||||
|
||||
**要求:**
|
||||
|
||||
- 已安装并认证 `gh` CLI(`gh auth login`)
|
||||
- 同步映射会保存在 `.gsd/.github-sync.json`
|
||||
- 具备速率限制感知:当 GitHub API rate limit 偏低时会跳过同步
|
||||
|
||||
**命令:**
|
||||
|
||||
- `/github-sync bootstrap`:初始化配置并执行同步
|
||||
- `/github-sync status`:显示同步映射数量
|
||||
|
||||
### `notifications`
|
||||
|
||||
控制 GSD 在自动模式中发出哪些通知:
|
||||
|
||||
```yaml
|
||||
notifications:
|
||||
enabled: true
|
||||
on_complete: true # 单元完成时通知
|
||||
on_error: true # 出错时通知
|
||||
on_budget: true # 预算阈值通知
|
||||
on_milestone: true # milestone 完成时通知
|
||||
on_attention: true # 需要人工介入时通知
|
||||
```
|
||||
|
||||
**macOS 通知方式:** GSD 会优先使用 [`terminal-notifier`](https://github.com/julienXX/terminal-notifier),不可用时回退到 `osascript`。建议安装 `terminal-notifier`,获得更稳定的通知体验:
|
||||
|
||||
```bash
|
||||
brew install terminal-notifier
|
||||
```
|
||||
|
||||
原因:`osascript display notification` 的通知权限是算在你的终端应用(Ghostty、iTerm2 等)上的,而这些应用在 System Settings → Notifications 中未必被允许。`terminal-notifier` 会注册成独立 App,并在首次使用时主动请求通知权限。如果通知异常,见 [故障排查:macOS 上通知不显示](troubleshooting.md#notifications-not-appearing-on-macos)。
|
||||
|
||||
### `remote_questions`
|
||||
|
||||
把交互式问题路由到 Slack 或 Discord,以支持 headless 自动模式:
|
||||
|
||||
```yaml
|
||||
remote_questions:
|
||||
channel: slack # 或 discord
|
||||
channel_id: "C1234567890"
|
||||
timeout_minutes: 15 # 问题超时(1-30 分钟)
|
||||
poll_interval_seconds: 10 # 轮询间隔(2-30 秒)
|
||||
```
|
||||
|
||||
### `post_unit_hooks`
|
||||
|
||||
在特定单元完成后触发的自定义 hooks:
|
||||
|
||||
```yaml
|
||||
post_unit_hooks:
|
||||
- name: code-review
|
||||
after: [execute-task]
|
||||
prompt: "Review the code changes for quality and security issues."
|
||||
model: claude-opus-4-6 # 可选:覆盖 model
|
||||
max_cycles: 1 # 每次触发最多执行几轮(1-10,默认 1)
|
||||
artifact: REVIEW.md # 可选:若该文件已存在则跳过
|
||||
retry_on: NEEDS-REWORK.md # 可选:若生成该文件,则回退并重跑触发单元
|
||||
agent: review-agent # 可选:指定使用哪个 agent 定义
|
||||
enabled: true # 可选:保留配置但临时禁用
|
||||
```
|
||||
|
||||
`after` 可识别的 unit types 包括:`research-milestone`、`plan-milestone`、`research-slice`、`plan-slice`、`execute-task`、`complete-slice`、`replan-slice`、`reassess-roadmap`、`run-uat`
|
||||
|
||||
**Prompt 占位符:** `{milestoneId}`、`{sliceId}`、`{taskId}` 会自动替换成当前上下文值。
|
||||
|
||||
### `pre_dispatch_hooks`
|
||||
|
||||
在 dispatch 前拦截某个单元。支持三种动作:
|
||||
|
||||
**Modify**:在单元 prompt 前后拼接文本
|
||||
|
||||
```yaml
|
||||
pre_dispatch_hooks:
|
||||
- name: add-standards
|
||||
before: [execute-task]
|
||||
action: modify
|
||||
prepend: "Follow our coding standards document."
|
||||
append: "Run linting after changes."
|
||||
```
|
||||
|
||||
**Skip**:完全跳过该单元
|
||||
|
||||
```yaml
|
||||
pre_dispatch_hooks:
|
||||
- name: skip-research
|
||||
before: [research-slice]
|
||||
action: skip
|
||||
skip_if: RESEARCH.md # 可选:仅当该文件存在时才跳过
|
||||
```
|
||||
|
||||
**Replace**:完全替换该单元 prompt
|
||||
|
||||
```yaml
|
||||
pre_dispatch_hooks:
|
||||
- name: custom-execute
|
||||
before: [execute-task]
|
||||
action: replace
|
||||
prompt: "Execute the task using TDD methodology."
|
||||
unit_type: execute-task-tdd # 可选:覆盖 unit type 标签
|
||||
model: claude-opus-4-6 # 可选:覆盖 model
|
||||
```
|
||||
|
||||
所有 pre-dispatch hooks 都支持 `enabled: true/false`,用于开关而不删除配置。
|
||||
|
||||
### `always_use_skills` / `prefer_skills` / `avoid_skills`
|
||||
|
||||
Skill 路由偏好:
|
||||
|
||||
```yaml
|
||||
always_use_skills:
|
||||
- debug-like-expert
|
||||
prefer_skills:
|
||||
- frontend-design
|
||||
avoid_skills: []
|
||||
```
|
||||
|
||||
Skills 既可以写裸名称(去 `~/.agents/skills/` 和 `.agents/skills/` 查找),也可以写绝对路径。
|
||||
|
||||
### `skill_rules`
|
||||
|
||||
基于人类可读触发条件的情景化 skill 路由:
|
||||
|
||||
```yaml
|
||||
skill_rules:
|
||||
- when: task involves authentication
|
||||
use: [clerk]
|
||||
- when: frontend styling work
|
||||
prefer: [frontend-design]
|
||||
- when: working with legacy code
|
||||
avoid: [aggressive-refactor]
|
||||
```
|
||||
|
||||
### `custom_instructions`
|
||||
|
||||
附加到每个会话上的持久指令:
|
||||
|
||||
```yaml
|
||||
custom_instructions:
|
||||
- "Always use TypeScript strict mode"
|
||||
- "Prefer functional patterns over classes"
|
||||
```
|
||||
|
||||
如果是项目特有知识(模式、坑点、经验),请优先放到 `.gsd/KNOWLEDGE.md` 中,因为它会自动注入每个 agent prompt。你也可以通过 `/gsd knowledge rule|pattern|lesson <description>` 添加。
|
||||
|
||||
### `RUNTIME.md`:运行时上下文(v2.39)
|
||||
|
||||
你可以在 `.gsd/RUNTIME.md` 中声明项目级运行时上下文。这个文件会内联进 task execution prompt,让 agent 能准确知道运行环境,而不必靠猜测路径或 URL。
|
||||
|
||||
**位置:** `.gsd/RUNTIME.md`
|
||||
|
||||
**示例:**
|
||||
|
||||
```markdown
|
||||
# Runtime Context
|
||||
|
||||
## API Endpoints
|
||||
- Main API: https://api.example.com
|
||||
- Cache: redis://localhost:6379
|
||||
|
||||
## Environment Variables
|
||||
- DEPLOYMENT_ENV: staging
|
||||
- DB_POOL_SIZE: 20
|
||||
|
||||
## Local Services
|
||||
- PostgreSQL: localhost:5432
|
||||
- Redis: localhost:6379
|
||||
```
|
||||
|
||||
适合放在这里的信息,是那些执行时需要知道、但又不属于 `DECISIONS.md`(架构)或 `KNOWLEDGE.md`(规则 / 模式)的内容。典型例子包括:API base URL、服务端口、部署目标,以及环境特有配置。
|
||||
|
||||
### `dynamic_routing`
|
||||
|
||||
基于复杂度的 model 路由。详见 [动态模型路由](./dynamic-model-routing.md)。
|
||||
|
||||
```yaml
|
||||
dynamic_routing:
|
||||
enabled: true
|
||||
capability_routing: true # 按 task capability 评分 models(v2.59)
|
||||
tier_models:
|
||||
light: claude-haiku-4-5
|
||||
standard: claude-sonnet-4-6
|
||||
heavy: claude-opus-4-6
|
||||
escalate_on_failure: true
|
||||
budget_pressure: true
|
||||
cross_provider: true
|
||||
```
|
||||
|
||||
### `context_management`(v2.59)
|
||||
|
||||
控制自动模式会话中的 observation masking 和 tool result truncation。可在不增加 LLM 开销的前提下,减少 compaction 之间的上下文膨胀。
|
||||
|
||||
```yaml
|
||||
context_management:
|
||||
observation_masking: true # 用占位符替换旧 tool result(默认:true)
|
||||
observation_mask_turns: 8 # 保留最近 N 个 user turn 的结果(1-50,默认:8)
|
||||
compaction_threshold_percent: 0.70 # 在 70% 上下文使用率处触发 compaction(0.5-0.95,默认:0.70)
|
||||
tool_result_max_chars: 800 # 单个 tool result 的最大字符数(200-10000,默认:800)
|
||||
```
|
||||
|
||||
### `service_tier`(v2.42)
|
||||
|
||||
OpenAI 支持模型的 service tier 偏好。可通过 `/gsd fast` 切换。
|
||||
|
||||
| 值 | 行为 |
|
||||
|----|------|
|
||||
| `"priority"` | Priority tier:2 倍成本,更快响应 |
|
||||
| `"flex"` | Flex tier:0.5 倍成本,更慢响应 |
|
||||
| (未设置) | 默认 tier |
|
||||
|
||||
```yaml
|
||||
service_tier: priority
|
||||
```
|
||||
|
||||
### `forensics_dedup`(v2.43)
|
||||
|
||||
可选启用:在 `/gsd forensics` 提交 issue 之前,先搜索现有 issues 和 PRs。会额外消耗一些 AI tokens。
|
||||
|
||||
```yaml
|
||||
forensics_dedup: true # 默认:false
|
||||
```
|
||||
|
||||
### `show_token_cost`(v2.44)
|
||||
|
||||
可选启用:在 footer 中显示每次 prompt 和累计会话的 token 成本。
|
||||
|
||||
```yaml
|
||||
show_token_cost: true # 默认:false
|
||||
```
|
||||
|
||||
### `auto_visualize`
|
||||
|
||||
在 milestone 完成后自动显示工作流可视化器:
|
||||
|
||||
```yaml
|
||||
auto_visualize: true
|
||||
```
|
||||
|
||||
详见 [工作流可视化器](./visualizer.md)。
|
||||
|
||||
### `parallel`
|
||||
|
||||
同时运行多个 milestones。默认关闭。
|
||||
|
||||
```yaml
|
||||
parallel:
|
||||
enabled: false # 总开关
|
||||
max_workers: 2 # 并发 workers 数(1-4)
|
||||
budget_ceiling: 50.00 # 聚合成本上限(美元)
|
||||
merge_strategy: "per-milestone" # "per-slice" 或 "per-milestone"
|
||||
auto_merge: "confirm" # "auto"、"confirm" 或 "manual"
|
||||
```
|
||||
|
||||
完整细节见 [并行编排](./parallel-orchestration.md)。
|
||||
|
||||
## 完整示例
|
||||
|
||||
```yaml
|
||||
---
|
||||
version: 1
|
||||
|
||||
# Model selection
|
||||
models:
|
||||
research: openrouter/deepseek/deepseek-r1
|
||||
planning:
|
||||
model: claude-opus-4-6
|
||||
fallbacks:
|
||||
- openrouter/z-ai/glm-5
|
||||
execution: claude-sonnet-4-6
|
||||
execution_simple: claude-haiku-4-5-20250414
|
||||
completion: claude-sonnet-4-6
|
||||
|
||||
# Token optimization
|
||||
token_profile: balanced
|
||||
|
||||
# Dynamic model routing
|
||||
dynamic_routing:
|
||||
enabled: true
|
||||
escalate_on_failure: true
|
||||
budget_pressure: true
|
||||
|
||||
# Budget
|
||||
budget_ceiling: 25.00
|
||||
budget_enforcement: pause
|
||||
context_pause_threshold: 80
|
||||
|
||||
# Supervision
|
||||
auto_supervisor:
|
||||
soft_timeout_minutes: 15
|
||||
hard_timeout_minutes: 25
|
||||
|
||||
# Git
|
||||
git:
|
||||
auto_push: true
|
||||
merge_strategy: squash
|
||||
isolation: worktree # "worktree", "branch", or "none"
|
||||
commit_docs: true
|
||||
|
||||
# Skills
|
||||
skill_discovery: suggest
|
||||
skill_staleness_days: 60 # Skills unused for N days get deprioritized (0 = disabled)
|
||||
always_use_skills:
|
||||
- debug-like-expert
|
||||
skill_rules:
|
||||
- when: task involves authentication
|
||||
use: [clerk]
|
||||
|
||||
# Notifications
|
||||
notifications:
|
||||
on_complete: false
|
||||
on_milestone: true
|
||||
on_attention: true
|
||||
|
||||
# Visualizer
|
||||
auto_visualize: true
|
||||
|
||||
# Service tier
|
||||
service_tier: priority # "priority" or "flex" (for /gsd fast)
|
||||
|
||||
# Diagnostics
|
||||
forensics_dedup: true # deduplicate before filing forensics issues
|
||||
show_token_cost: true # show per-prompt cost in footer
|
||||
|
||||
# Hooks
|
||||
post_unit_hooks:
|
||||
- name: code-review
|
||||
after: [execute-task]
|
||||
prompt: "Review {sliceId}/{taskId} for quality and security."
|
||||
artifact: REVIEW.md
|
||||
---
|
||||
```
|
||||
94
docs/zh-CN/user-docs/cost-management.md
Normal file
94
docs/zh-CN/user-docs/cost-management.md
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# 成本管理
|
||||
|
||||
GSD 会跟踪自动模式中每个派发工作单元的 Token 使用量和成本。这些数据会驱动仪表板、预算约束以及成本预测。
|
||||
|
||||
## 成本跟踪
|
||||
|
||||
每个工作单元的指标都会被自动记录:
|
||||
|
||||
- **Token 数量**:input、output、cache read、cache write、total
|
||||
- **成本**:每个单元的美元成本
|
||||
- **耗时**:真实墙钟时间
|
||||
- **工具调用数**:工具调用次数
|
||||
- **消息数量**:assistant 与 user 消息数
|
||||
|
||||
数据保存在 `.gsd/metrics.json` 中,并且可跨会话持续存在。
|
||||
|
||||
### 查看成本
|
||||
|
||||
**仪表板**:按 `Ctrl+Alt+G` 或执行 `/gsd status` 可查看实时成本拆分。
|
||||
|
||||
**可用聚合维度:**
|
||||
|
||||
- 按阶段(research、planning、execution、completion、reassessment)
|
||||
- 按 slice(M001/S01、M001/S02 等)
|
||||
- 按模型(哪些模型最耗预算)
|
||||
- 项目总计
|
||||
|
||||
## 预算上限
|
||||
|
||||
可以为单个项目设置最大支出:
|
||||
|
||||
```yaml
|
||||
---
|
||||
version: 1
|
||||
budget_ceiling: 50.00
|
||||
---
|
||||
```
|
||||
|
||||
### 执行模式
|
||||
|
||||
控制触达预算上限后会发生什么:
|
||||
|
||||
```yaml
|
||||
budget_enforcement: pause # 设置 ceiling 后的默认值
|
||||
```
|
||||
|
||||
| 模式 | 行为 |
|
||||
|------|------|
|
||||
| `warn` | 记录警告,但继续执行 |
|
||||
| `pause` | 暂停自动模式,等待用户动作 |
|
||||
| `halt` | 直接停止自动模式 |
|
||||
|
||||
## 成本预测
|
||||
|
||||
当至少完成两个 slices 后,GSD 会预测剩余成本:
|
||||
|
||||
```
|
||||
Projected remaining: $12.40 ($6.20/slice avg × 2 remaining)
|
||||
```
|
||||
|
||||
预测基于已完成工作的每-slice 平均成本。如果预算上限已触达,结果中还会附带一条警告。
|
||||
|
||||
## 预算压力与模型降级
|
||||
|
||||
当预算接近上限时,[复杂度路由器](./token-optimization.md#budget-pressure)会自动把模型分配降到更便宜的层级。这是一个渐进过程:
|
||||
|
||||
- **已使用 < 50%**:不调整
|
||||
- **已使用 50-75%**:standard task 降为 light
|
||||
- **已使用 75-90%**:同样降级,但更激进
|
||||
- **已使用 > 90%**:几乎所有 task 都降级,只有 heavy task 仍保留在 standard
|
||||
|
||||
这样可以把预算尽量均匀地分摊到剩余工作中,而不是过早在几个复杂 task 上耗尽。
|
||||
|
||||
## Token 配置与成本
|
||||
|
||||
`token_profile` 偏好会直接影响成本:
|
||||
|
||||
| 配置 | 常见节省幅度 | 方式 |
|
||||
|------|--------------|------|
|
||||
| `budget` | 40-60% | 更便宜的模型、跳过部分阶段、最小上下文 |
|
||||
| `balanced` | 10-20% | 默认模型、跳过 slice research、标准上下文 |
|
||||
| `quality` | 0%(基线) | 完整模型、完整阶段、完整上下文 |
|
||||
|
||||
更多细节见 [Token 优化](./token-optimization.md)。
|
||||
|
||||
## 建议
|
||||
|
||||
- 先用 `balanced` 配置,并设置一个较宽松的 `budget_ceiling` 来建立成本基线
|
||||
- 完成几个 slices 后查看 `/gsd status`,确认每个 slice 的平均成本
|
||||
- 对于已知流程、重复性高的工作,切换到 `budget` 配置
|
||||
- 只有在做架构决策时才建议使用 `quality`
|
||||
- 可以通过按阶段选模型,只在 planning 使用 Opus,而在 execution 保持 Sonnet
|
||||
- 开启 `dynamic_routing`,让简单 task 自动下沉到更便宜的模型,详见 [动态模型路由](./dynamic-model-routing.md)
|
||||
- 使用 `/gsd visualize` 的 Metrics 标签页查看预算具体花在了哪里
|
||||
378
docs/zh-CN/user-docs/custom-models.md
Normal file
378
docs/zh-CN/user-docs/custom-models.md
Normal file
|
|
@ -0,0 +1,378 @@
|
|||
# 自定义模型
|
||||
|
||||
通过 `~/.gsd/agent/models.json` 添加自定义 providers 和 models(Ollama、vLLM、LM Studio、代理等)。
|
||||
|
||||
## 目录
|
||||
|
||||
- [最小示例](#minimal-example)
|
||||
- [完整示例](#full-example)
|
||||
- [支持的 API](#supported-apis)
|
||||
- [Provider 配置](#provider-configuration)
|
||||
- [Model 配置](#model-configuration)
|
||||
- [覆盖内置 Providers](#overriding-built-in-providers)
|
||||
- [按 model 覆盖](#per-model-overrides)
|
||||
- [OpenAI 兼容性](#openai-compatibility)
|
||||
|
||||
<a id="minimal-example"></a>
|
||||
## 最小示例
|
||||
|
||||
对于本地 models(Ollama、LM Studio、vLLM),每个 model 只要求提供 `id`:
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"ollama": {
|
||||
"baseUrl": "http://localhost:11434/v1",
|
||||
"api": "openai-completions",
|
||||
"apiKey": "ollama",
|
||||
"models": [
|
||||
{ "id": "llama3.1:8b" },
|
||||
{ "id": "qwen2.5-coder:7b" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`apiKey` 在 schema 中是必填,但 Ollama 会忽略它,因此任意值都可以。
|
||||
|
||||
有些 OpenAI-compatible server 不支持推理模型使用的 `developer` role。对于这类 provider,需要把 `compat.supportsDeveloperRole` 设为 `false`,这样 GSD 会改用 `system` message 发送 system prompt。如果该 server 同时也不支持 `reasoning_effort`,还应把 `compat.supportsReasoningEffort` 也设为 `false`。
|
||||
|
||||
你可以在 provider 级别设置 `compat`,让它应用到该 provider 下的所有 models;也可以在 model 级别单独覆盖某个 model。这个设置常见于 Ollama、vLLM、SGLang 以及类似的 OpenAI-compatible server。
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"ollama": {
|
||||
"baseUrl": "http://localhost:11434/v1",
|
||||
"api": "openai-completions",
|
||||
"apiKey": "ollama",
|
||||
"compat": {
|
||||
"supportsDeveloperRole": false,
|
||||
"supportsReasoningEffort": false
|
||||
},
|
||||
"models": [
|
||||
{
|
||||
"id": "gpt-oss:20b",
|
||||
"reasoning": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<a id="full-example"></a>
|
||||
## 完整示例
|
||||
|
||||
当你需要显式覆盖默认值时,可以写成更完整的配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"ollama": {
|
||||
"baseUrl": "http://localhost:11434/v1",
|
||||
"api": "openai-completions",
|
||||
"apiKey": "ollama",
|
||||
"models": [
|
||||
{
|
||||
"id": "llama3.1:8b",
|
||||
"name": "Llama 3.1 8B (Local)",
|
||||
"reasoning": false,
|
||||
"input": ["text"],
|
||||
"contextWindow": 128000,
|
||||
"maxTokens": 32000,
|
||||
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
每次打开 `/model` 时,这个文件都会重新加载。可以在会话过程中直接编辑,无需重启。
|
||||
|
||||
<a id="supported-apis"></a>
|
||||
## 支持的 API
|
||||
|
||||
| API | 说明 |
|
||||
|-----|------|
|
||||
| `openai-completions` | OpenAI Chat Completions(兼容性最好) |
|
||||
| `openai-responses` | OpenAI Responses API |
|
||||
| `anthropic-messages` | Anthropic Messages API |
|
||||
| `google-generative-ai` | Google Generative AI |
|
||||
|
||||
`api` 可以设置在 provider 级别(作为该 provider 下所有 models 的默认值),也可以设置在 model 级别(覆盖单个 model)。
|
||||
|
||||
<a id="provider-configuration"></a>
|
||||
## Provider 配置
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `baseUrl` | API endpoint URL |
|
||||
| `api` | API 类型(见上) |
|
||||
| `apiKey` | API key(见下方值解析) |
|
||||
| `headers` | 自定义请求头(见下方值解析) |
|
||||
| `authHeader` | 设为 `true` 时,自动添加 `Authorization: Bearer <apiKey>` |
|
||||
| `models` | model 配置数组 |
|
||||
| `modelOverrides` | 针对该 provider 的内置 models 做按 model 覆盖 |
|
||||
|
||||
<a id="value-resolution"></a>
|
||||
### 值解析
|
||||
|
||||
`apiKey` 和 `headers` 支持三种写法:
|
||||
|
||||
- **Shell 命令:** `"!command"`,执行后读取 stdout
|
||||
```json
|
||||
"apiKey": "!security find-generic-password -ws 'anthropic'"
|
||||
"apiKey": "!op read 'op://vault/item/credential'"
|
||||
```
|
||||
- **环境变量:** 取对应环境变量的值
|
||||
```json
|
||||
"apiKey": "MY_API_KEY"
|
||||
```
|
||||
- **字面量:** 直接使用
|
||||
```json
|
||||
"apiKey": "sk-..."
|
||||
```
|
||||
|
||||
<a id="command-allowlist"></a>
|
||||
#### 命令允许列表
|
||||
|
||||
Shell 命令(`!command`)只能执行一组已知的凭据工具。只有以下前缀开头的命令才会被允许:
|
||||
|
||||
`pass`、`op`、`aws`、`gcloud`、`vault`、`security`、`gpg`、`bw`、`gopass`、`lpass`
|
||||
|
||||
不在列表中的命令会被阻止,最终该值会解析为 `undefined`。同时会向 stderr 输出一条警告。
|
||||
|
||||
为了防止注入,命令参数中的 shell 操作符(`;`、`|`、`&`、`` ` ``、`$`、`>`、`<`)同样会被阻止。
|
||||
|
||||
**自定义允许列表:**
|
||||
|
||||
如果你使用的凭据工具不在默认列表中,可以在全局设置(`~/.gsd/agent/settings.json`)里覆盖:
|
||||
|
||||
```json
|
||||
{
|
||||
"allowedCommandPrefixes": ["pass", "op", "sops", "doppler", "mycli"]
|
||||
}
|
||||
```
|
||||
|
||||
这会完全替换默认列表,因此如果你还想保留默认命令,需要一起写进去。
|
||||
|
||||
你也可以设置 `GSD_ALLOWED_COMMAND_PREFIXES` 环境变量(逗号分隔)。环境变量优先级高于 settings.json:
|
||||
|
||||
```bash
|
||||
export GSD_ALLOWED_COMMAND_PREFIXES="pass,op,sops,doppler"
|
||||
```
|
||||
|
||||
> **注意:** 这是一个仅全局生效的设置。项目级 settings.json(`<project>/.gsd/settings.json`)不能覆盖命令 allowlist,以防克隆下来的仓库提升命令执行权限。
|
||||
|
||||
### 自定义 Headers
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"custom-proxy": {
|
||||
"baseUrl": "https://proxy.example.com/v1",
|
||||
"apiKey": "MY_API_KEY",
|
||||
"api": "anthropic-messages",
|
||||
"headers": {
|
||||
"x-portkey-api-key": "PORTKEY_API_KEY",
|
||||
"x-secret": "!op read 'op://vault/item/secret'"
|
||||
},
|
||||
"models": [...]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<a id="model-configuration"></a>
|
||||
## Model 配置
|
||||
|
||||
| 字段 | 必填 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `id` | 是 | — | Model 标识符(会原样传给 API) |
|
||||
| `name` | 否 | `id` | 可读的 model 标签,用于匹配(例如 `--model` 模糊匹配)并显示在详情 / 状态文字里 |
|
||||
| `api` | 否 | provider 的 `api` | 为这个 model 覆盖 provider 的 API 类型 |
|
||||
| `reasoning` | 否 | `false` | 是否支持扩展 thinking |
|
||||
| `input` | 否 | `["text"]` | 输入类型:`["text"]` 或 `["text", "image"]` |
|
||||
| `contextWindow` | 否 | `128000` | 上下文窗口大小(tokens) |
|
||||
| `maxTokens` | 否 | `16384` | 最大输出 tokens |
|
||||
| `cost` | 否 | 全为 0 | `{"input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0}`(每百万 tokens) |
|
||||
| `compat` | 否 | provider 的 `compat` | OpenAI 兼容性覆盖项。如果 provider 和 model 两边都配置了,会合并 |
|
||||
|
||||
当前行为:
|
||||
|
||||
- `/model` 与 `--list-models` 都是按 model `id` 列出条目
|
||||
- 配置里的 `name` 会用于 model 匹配,以及详情 / 状态文本展示
|
||||
|
||||
<a id="overriding-built-in-providers"></a>
|
||||
## 覆盖内置 Providers
|
||||
|
||||
如果你想把某个内置 provider 经由代理路由出去,但又不想重新定义全部 models,可以这样写:
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"anthropic": {
|
||||
"baseUrl": "https://my-proxy.example.com/v1"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这样所有内置 Anthropic models 仍然可用。已有的 OAuth 或 API key 认证也会继续生效。
|
||||
|
||||
如果你想把自定义 models 合并进某个内置 provider,就同时提供 `models` 数组:
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"anthropic": {
|
||||
"baseUrl": "https://my-proxy.example.com/v1",
|
||||
"apiKey": "ANTHROPIC_API_KEY",
|
||||
"api": "anthropic-messages",
|
||||
"models": [...]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
合并规则如下:
|
||||
|
||||
- 内置 models 会保留
|
||||
- 自定义 models 会按 `id` 在该 provider 下执行 upsert
|
||||
- 如果某个自定义 model 的 `id` 与内置 model 相同,自定义 model 会替换那个内置 model
|
||||
- 如果某个自定义 model 的 `id` 是新的,它会作为新增条目并列出现
|
||||
|
||||
<a id="per-model-overrides"></a>
|
||||
## 按 model 覆盖
|
||||
|
||||
如果你只想修改某些特定的内置 model,而不想替换整个 provider 的 model 列表,可以使用 `modelOverrides`。
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"openrouter": {
|
||||
"modelOverrides": {
|
||||
"anthropic/claude-sonnet-4": {
|
||||
"name": "Claude Sonnet 4 (Bedrock Route)",
|
||||
"compat": {
|
||||
"openRouterRouting": {
|
||||
"only": ["amazon-bedrock"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`modelOverrides` 支持的字段包括:`name`、`reasoning`、`input`、`cost`(可部分覆盖)、`contextWindow`、`maxTokens`、`headers`、`compat`。
|
||||
|
||||
行为说明:
|
||||
|
||||
- `modelOverrides` 只会应用到内置 provider 的 models 上
|
||||
- 未知的 model ID 会被忽略
|
||||
- 可以把 provider 级别的 `baseUrl` / `headers` 与 `modelOverrides` 组合使用
|
||||
- 如果某个 provider 同时定义了 `models`,那么自定义 models 会在应用完内置覆盖后再合并;如果它的 `id` 与已覆盖的内置 model 相同,最终会以自定义 model 为准
|
||||
|
||||
<a id="openai-compatibility"></a>
|
||||
## OpenAI 兼容性
|
||||
|
||||
对于只部分兼容 OpenAI 的 providers,可通过 `compat` 字段修正行为。
|
||||
|
||||
- provider 级别的 `compat` 会作为该 provider 下所有 models 的默认值
|
||||
- model 级别的 `compat` 会覆盖该 model 的 provider 级别设置
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"local-llm": {
|
||||
"baseUrl": "http://localhost:8080/v1",
|
||||
"api": "openai-completions",
|
||||
"compat": {
|
||||
"supportsUsageInStreaming": false,
|
||||
"maxTokensField": "max_tokens"
|
||||
},
|
||||
"models": [...]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `supportsStore` | Provider 是否支持 `store` 字段 |
|
||||
| `supportsDeveloperRole` | 是否使用 `developer` 而非 `system` role |
|
||||
| `supportsReasoningEffort` | 是否支持 `reasoning_effort` 参数 |
|
||||
| `reasoningEffortMap` | 把 GSD 的 thinking levels 映射到 provider 专属 `reasoning_effort` 值 |
|
||||
| `supportsUsageInStreaming` | 是否支持 `stream_options: { include_usage: true }`(默认 `true`) |
|
||||
| `maxTokensField` | 使用 `max_completion_tokens` 还是 `max_tokens` |
|
||||
| `requiresToolResultName` | tool result message 中是否必须包含 `name` |
|
||||
| `requiresAssistantAfterToolResult` | tool result 之后、user message 之前是否需要插入 assistant message |
|
||||
| `requiresThinkingAsText` | 是否把 thinking block 转成纯文本 |
|
||||
| `thinkingFormat` | 使用 `reasoning_effort`、`zai`、`qwen` 或 `qwen-chat-template` 的 thinking 参数格式 |
|
||||
| `supportsStrictMode` | 是否在 tool definitions 中包含 `strict` 字段 |
|
||||
| `openRouterRouting` | 传给 OpenRouter 的路由配置,用于 model/provider 选择 |
|
||||
| `vercelGatewayRouting` | Vercel AI Gateway 的路由配置,用于 provider 选择(`only`、`order`) |
|
||||
|
||||
`qwen` 使用顶层 `enable_thinking`。对于要求 `chat_template_kwargs.enable_thinking` 的本地 Qwen-compatible server,请使用 `qwen-chat-template`。
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"openrouter": {
|
||||
"baseUrl": "https://openrouter.ai/api/v1",
|
||||
"apiKey": "OPENROUTER_API_KEY",
|
||||
"api": "openai-completions",
|
||||
"models": [
|
||||
{
|
||||
"id": "openrouter/anthropic/claude-3.5-sonnet",
|
||||
"name": "OpenRouter Claude 3.5 Sonnet",
|
||||
"compat": {
|
||||
"openRouterRouting": {
|
||||
"order": ["anthropic"],
|
||||
"fallbacks": ["openai"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Vercel AI Gateway 示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"vercel-ai-gateway": {
|
||||
"baseUrl": "https://ai-gateway.vercel.sh/v1",
|
||||
"apiKey": "AI_GATEWAY_API_KEY",
|
||||
"api": "openai-completions",
|
||||
"models": [
|
||||
{
|
||||
"id": "moonshotai/kimi-k2.5",
|
||||
"name": "Kimi K2.5 (Fireworks via Vercel)",
|
||||
"reasoning": true,
|
||||
"input": ["text", "image"],
|
||||
"cost": { "input": 0.6, "output": 3, "cacheRead": 0, "cacheWrite": 0 },
|
||||
"contextWindow": 262144,
|
||||
"maxTokens": 262144,
|
||||
"compat": {
|
||||
"vercelGatewayRouting": {
|
||||
"only": ["fireworks", "novita"],
|
||||
"order": ["fireworks", "novita"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
287
docs/zh-CN/user-docs/dynamic-model-routing.md
Normal file
287
docs/zh-CN/user-docs/dynamic-model-routing.md
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
# 动态模型路由
|
||||
|
||||
*引入于 v2.19.0。Capability scoring 引入于 v2.52.0。*
|
||||
|
||||
动态模型路由会为简单工作自动选择更便宜的模型,并把昂贵模型留给复杂 task。这样在有成本上限的套餐下,通常可以减少 20-50% 的 token 消耗,同时在关键位置保持质量。
|
||||
|
||||
从 v2.52.0 开始,router 使用 **capability-aware scoring**,为每个 task 选择最合适的 model,而不只是简单挑选该 tier 里最便宜的。
|
||||
|
||||
## 工作原理
|
||||
|
||||
自动模式派发的每个工作单元都会经过一个两阶段流水线:
|
||||
|
||||
**阶段 1:复杂度分类**:先把工作划分到某个 tier(light / standard / heavy)。
|
||||
|
||||
**阶段 2:能力评分**:在符合该 tier 的候选 models 里,根据它们的能力和 task 需求的匹配程度进行排序。
|
||||
|
||||
核心规则是:**只允许降级,不允许升级**。用户在偏好设置中配置的 model 始终是上限,router 不会把它升级到比你配置更强的 model。
|
||||
|
||||
| Tier | 典型工作 | 默认模型级别 |
|
||||
|------|----------|--------------|
|
||||
| **Light** | slice completion、UAT、hooks | Haiku 级 |
|
||||
| **Standard** | research、planning、execution、milestone completion | Sonnet 级 |
|
||||
| **Heavy** | replan、roadmap reassessment、复杂 execution | Opus 级 |
|
||||
|
||||
## 启用方式
|
||||
|
||||
动态路由默认关闭。可在偏好设置中开启:
|
||||
|
||||
```yaml
|
||||
---
|
||||
version: 1
|
||||
dynamic_routing:
|
||||
enabled: true
|
||||
---
|
||||
```
|
||||
|
||||
## 配置
|
||||
|
||||
```yaml
|
||||
dynamic_routing:
|
||||
enabled: true
|
||||
tier_models: # 可选:为每个 tier 显式指定 model
|
||||
light: claude-haiku-4-5
|
||||
standard: claude-sonnet-4-6
|
||||
heavy: claude-opus-4-6
|
||||
escalate_on_failure: true # task 失败时提升 tier(默认:true)
|
||||
budget_pressure: true # 接近预算上限时自动降级(默认:true)
|
||||
cross_provider: true # 可跨 provider 选择 model(默认:true)
|
||||
hooks: true # 是否对 post-unit hooks 也应用路由(默认:true)
|
||||
capability_routing: true # 在 tier 内启用 capability scoring(默认:true)
|
||||
```
|
||||
|
||||
### `tier_models`
|
||||
|
||||
覆盖每个 tier 默认使用的 model。如果省略,router 会使用内置 capability mapping,它已经知道一些常见 model 家族的大致定位:
|
||||
|
||||
- **Light:** `claude-haiku-4-5`、`gpt-4o-mini`、`gemini-2.0-flash`
|
||||
- **Standard:** `claude-sonnet-4-6`、`gpt-4o`、`gemini-2.5-pro`
|
||||
- **Heavy:** `claude-opus-4-6`、`gpt-4.5-preview`、`gemini-2.5-pro`
|
||||
|
||||
### `escalate_on_failure`
|
||||
|
||||
当 task 在某个 tier 上失败时,router 会在重试时提升到下一层:Light → Standard → Heavy。这样可以避免便宜模型在其实需要更强推理能力的工作上浪费重试次数。
|
||||
|
||||
### `budget_pressure`
|
||||
|
||||
当预算接近上限时,router 会逐步降低 tier:
|
||||
|
||||
| 已使用预算 | 影响 |
|
||||
|------------|------|
|
||||
| < 50% | 不调整 |
|
||||
| 50-75% | Standard → Light |
|
||||
| 75-90% | 更激进地降级 |
|
||||
| > 90% | 几乎所有工作都 → Light;只有 Heavy 保持在 Standard |
|
||||
|
||||
### `cross_provider`
|
||||
|
||||
开启后,router 可以从你的主 provider 之外选择 model。它会使用内置成本表,在每个 tier 里找到最便宜的 model。要求目标 provider 已经正确配置。
|
||||
|
||||
### `capability_routing`
|
||||
|
||||
开启后(默认:true),router 会通过 capability scoring 在某个 tier 内选出“最适合”的 model,而不是永远只选最便宜的那个。设为 `false` 可恢复到纯 cheapest-in-tier 行为:
|
||||
|
||||
```yaml
|
||||
dynamic_routing:
|
||||
enabled: true
|
||||
capability_routing: false # 关闭评分,改用 tier 内最便宜的 model
|
||||
```
|
||||
|
||||
## Capability Profiles
|
||||
|
||||
每个 model 都有一个内置的 **capability profile**,它是一个 7 维评分(0-100),表示该 model 在不同 task 类型下的能力强弱:
|
||||
|
||||
| 维度 | 含义 |
|
||||
|------|------|
|
||||
| `coding` | 代码生成和实现准确性 |
|
||||
| `debugging` | 诊断与修复错误的能力 |
|
||||
| `research` | 信息综合与主题探索能力 |
|
||||
| `reasoning` | 多步逻辑推理能力 |
|
||||
| `speed` | 延迟与吞吐(可视为能力深度的反向维度) |
|
||||
| `longContext` | 处理大代码库和长文档的能力 |
|
||||
| `instruction` | 精确遵循结构化指令的能力 |
|
||||
|
||||
目前 9 个 models 带有内置 profile:`claude-opus-4-6`、`claude-sonnet-4-6`、`claude-haiku-4-5`、`gpt-4o`、`gpt-4o-mini`、`gemini-2.5-pro`、`gemini-2.0-flash`、`deepseek-chat`、`o3`。
|
||||
|
||||
没有内置 profile 的 models 会收到**全维度均为 50** 的默认分数。这是一个冷启动策略:未知模型可以参与竞争,但不会凭空占优。从用户角度看,这类模型的路由行为和 capability scoring 引入前保持一致。
|
||||
|
||||
**这些 profiles 是启发式排序,不是 benchmark。** 它们表达的是大致的相对优势,而不是经过严格验证的 benchmark 结果。如果你很了解某个 model,可通过用户覆盖项(见下文)修正这些分值。
|
||||
|
||||
## 评分方式
|
||||
|
||||
tier 内的路由流程如下:
|
||||
|
||||
```
|
||||
classify complexity tier
|
||||
↓
|
||||
filter eligible models for tier
|
||||
↓
|
||||
fire before_model_select hook (optional override)
|
||||
↓
|
||||
capability score eligible models
|
||||
↓
|
||||
select winner (or first eligible if scoring is disabled)
|
||||
```
|
||||
|
||||
**评分公式:** 各能力维度的加权平均
|
||||
|
||||
```
|
||||
score = Σ(weight × capability) / Σ(weights)
|
||||
```
|
||||
|
||||
**Task requirements** 是动态的,不同 unit types 对维度的权重不同:
|
||||
|
||||
| Unit Type | 核心维度 |
|
||||
|-----------|----------|
|
||||
| `execute-task` | coding (0.9)、instruction (0.7)、speed (0.3) |
|
||||
| `research-*` | research (0.9)、longContext (0.7)、reasoning (0.5) |
|
||||
| `plan-*` | reasoning (0.9)、coding (0.5) |
|
||||
| `replan-slice` | reasoning (0.9)、debugging (0.6)、coding (0.5) |
|
||||
| `complete-slice`、`run-uat` | instruction (0.8)、speed (0.7) |
|
||||
|
||||
对于 `execute-task`,router 还会进一步根据 task metadata 微调需求:
|
||||
|
||||
- 带有 `docs`、`config`、`readme` 等 tag:提高 instruction 权重
|
||||
- 包含 `concurrency`、`compatibility` 等关键词:提高 debugging 和 reasoning 权重
|
||||
- 包含 `migration`、`architecture` 等关键词:提高 reasoning 和 coding 权重
|
||||
- 文件数较多(≥6)或估计行数较大(≥500):提高 coding 和 reasoning 权重
|
||||
|
||||
**平分时的决策:** 当两个 models 的得分相差不超过 2 分时,优先选择更便宜的那个。如果成本也相同,则按 model ID 字典序打破平局(确定性结果)。
|
||||
|
||||
## 用户覆盖
|
||||
|
||||
如果你对某个 model 的能力认知比内置 profile 更准确,可以通过 `models` 配置里的 `modelOverrides` 修正:
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"anthropic": {
|
||||
"modelOverrides": {
|
||||
"claude-sonnet-4-6": {
|
||||
"capabilities": {
|
||||
"debugging": 90,
|
||||
"research": 85
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
这些覆盖会与内置默认值进行**深度合并**:你只需覆盖指定维度,未指定的维度仍保留内置值。
|
||||
|
||||
**典型用法:** 如果你发现某个 model 在某一类工作上持续优于内置 profile,就覆盖对应维度,把 router 更积极地引导到该 model。
|
||||
|
||||
## 详细输出
|
||||
|
||||
开启 verbose mode 时,router 会把自己的路由决策打印出来。如果使用了 capability scoring,日志会包含完整评分拆分:
|
||||
|
||||
```
|
||||
Dynamic routing [S]: claude-sonnet-4-6 (capability-scored) — claude-sonnet-4-6: 82.3, gpt-4o: 78.1, deepseek-chat: 72.0
|
||||
```
|
||||
|
||||
如果只使用了 tier 级路由(例如评分被禁用、只有一个符合条件的 model,或命中了路由守卫):
|
||||
|
||||
```
|
||||
Dynamic routing [S]: claude-sonnet-4-6 (standard complexity, multiple steps)
|
||||
```
|
||||
|
||||
路由决策中的 `selectionMethod` 字段会说明采用了哪种路径:
|
||||
|
||||
- `"capability-scored"`:使用 capability scoring 选出了最终 model
|
||||
- `"tier-only"`:使用了 tier 内最便宜的 model(或显式固定值)
|
||||
|
||||
## 扩展 Hook
|
||||
|
||||
扩展可以通过 `before_model_select` hook 拦截并覆盖 model 选择。
|
||||
|
||||
Hook 触发时机在 **tier 过滤之后**(已知符合条件的 models),但在 **capability scoring 之前**(尚未计算分数)。Hook 可以完全接管选择,也可以返回 `undefined`,让 scoring 按默认逻辑继续。
|
||||
|
||||
**注册处理器:**
|
||||
|
||||
```typescript
|
||||
pi.on("before_model_select", async (event) => {
|
||||
const { unitType, unitId, classification, taskMetadata, eligibleModels, phaseConfig } = event;
|
||||
|
||||
// 自定义路由策略:research 一律优先用 gemini
|
||||
if (unitType.startsWith("research-")) {
|
||||
const gemini = eligibleModels.find(id => id.includes("gemini"));
|
||||
if (gemini) return { modelId: gemini };
|
||||
}
|
||||
|
||||
// 返回 undefined,让 capability scoring 继续
|
||||
return undefined;
|
||||
});
|
||||
```
|
||||
|
||||
**事件负载:**
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `unitType` | `string` | 当前派发单元类型(例如 `"execute-task"`) |
|
||||
| `unitId` | `string` | 此次单元派发的唯一标识符 |
|
||||
| `classification` | `{ tier, reason, downgraded }` | 复杂度分类结果 |
|
||||
| `taskMetadata` | `Record<string, unknown> \| undefined` | 从单元 plan 中提取出的 task 元数据 |
|
||||
| `eligibleModels` | `string[]` | 符合该 tier 的 models |
|
||||
| `phaseConfig` | `{ primary, fallbacks } \| undefined` | 用户为该 phase 配置的 model |
|
||||
|
||||
**返回值:** `{ modelId: string }` 表示覆盖默认选择;返回 `undefined` 表示交给 capability scoring。
|
||||
|
||||
**第一个覆盖者生效:** 如果多个扩展都注册了处理器,第一个返回非 `undefined` 的处理器获胜,后续处理器不会再被调用。
|
||||
|
||||
## 复杂度分类
|
||||
|
||||
工作单元通过纯启发式规则分类,不涉及 LLM 调用,耗时通常低于 1ms。
|
||||
|
||||
### Unit Type 默认值
|
||||
|
||||
| Unit Type | 默认 Tier |
|
||||
|-----------|-----------|
|
||||
| `complete-slice`、`run-uat` | Light |
|
||||
| `research-*`、`plan-*`、`complete-milestone` | Standard |
|
||||
| `execute-task` | Standard(可被 task 分析升级) |
|
||||
| `replan-slice`、`reassess-roadmap` | Heavy |
|
||||
| `hook/*` | Light |
|
||||
|
||||
### Task Plan 分析
|
||||
|
||||
对于 `execute-task` 单元,分类器会分析 task plan:
|
||||
|
||||
| 信号 | 简单 → Light | 复杂 → Heavy |
|
||||
|------|--------------|--------------|
|
||||
| Step 数量 | ≤ 3 | ≥ 8 |
|
||||
| 文件数 | ≤ 3 | ≥ 8 |
|
||||
| 描述长度 | < 500 chars | > 2000 chars |
|
||||
| 代码块数 | — | ≥ 5 |
|
||||
| 复杂度关键词 | 无 | 有 |
|
||||
|
||||
**复杂度关键词:** `research`、`investigate`、`refactor`、`migrate`、`integrate`、`complex`、`architect`、`redesign`、`security`、`performance`、`concurrent`、`parallel`、`distributed`、`backward compat`
|
||||
|
||||
### 自适应学习
|
||||
|
||||
路由历史(`.gsd/routing-history.json`)会按 unit type 和 tier 记录成功 / 失败情况。如果某种模式下某个 tier 的失败率超过 20%,未来相似分类会自动上调一个 tier。用户反馈(`over` / `under` / `ok`)的权重是自动结果的 2 倍。
|
||||
|
||||
## 与 Token Profile 的关系
|
||||
|
||||
动态路由和 token profile 是互补的:
|
||||
|
||||
- **Token profiles**(`budget` / `balanced` / `quality`)控制阶段跳过和上下文压缩
|
||||
- **Dynamic routing** 控制每个工作单元在对应 phase 内的 model 选择
|
||||
|
||||
两者同时开启时,token profile 负责给出基础模型集,dynamic routing 再在这些基础之上做进一步优化。`budget` token profile + dynamic routing 组合能带来最大的成本节省。
|
||||
|
||||
## 成本表
|
||||
|
||||
Router 内置了一张常见 models 的成本表,用于跨 provider 成本比较。成本单位都是每百万 tokens(input / output):
|
||||
|
||||
| Model | Input | Output |
|
||||
|-------|-------|--------|
|
||||
| claude-haiku-4-5 | $0.80 | $4.00 |
|
||||
| claude-sonnet-4-6 | $3.00 | $15.00 |
|
||||
| claude-opus-4-6 | $15.00 | $75.00 |
|
||||
| gpt-4o-mini | $0.15 | $0.60 |
|
||||
| gpt-4o | $2.50 | $10.00 |
|
||||
| gemini-2.0-flash | $0.10 | $0.40 |
|
||||
|
||||
这张成本表仅用于比较,实际计费仍然来自你所使用的 provider。
|
||||
473
docs/zh-CN/user-docs/getting-started.md
Normal file
473
docs/zh-CN/user-docs/getting-started.md
Normal file
|
|
@ -0,0 +1,473 @@
|
|||
# GSD 快速开始
|
||||
|
||||
GSD 是一个 AI 编程代理,负责规划、执行、验证和交付,让你可以把注意力放在“要构建什么”上。本指南会带你完成 macOS、Windows 和 Linux 的安装,并启动你的第一个会话。
|
||||
|
||||
---
|
||||
|
||||
## 前置条件
|
||||
|
||||
| 要求 | 最低版本 | 推荐版本 |
|
||||
|------|----------|----------|
|
||||
| **[Node.js](https://nodejs.org/)** | 22.0.0 | 24 LTS |
|
||||
| **[Git](https://git-scm.com/)** | 2.20+ | 最新版 |
|
||||
| **LLM API key** | 任意受支持提供商 | Anthropic(Claude) |
|
||||
|
||||
如果你还没有安装 Node.js 或 Git,请按下面对应操作系统的步骤进行。
|
||||
|
||||
---
|
||||
|
||||
## 按操作系统安装
|
||||
|
||||
### macOS
|
||||
|
||||
> **下载链接:** [Node.js](https://nodejs.org/) | [Git](https://git-scm.com/download/mac) | [Homebrew](https://brew.sh/)
|
||||
|
||||
**第 1 步:安装 Homebrew**(如果已安装可跳过):
|
||||
|
||||
```bash
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
```
|
||||
|
||||
**第 2 步:安装 Node.js 和 Git:**
|
||||
|
||||
```bash
|
||||
brew install node git
|
||||
```
|
||||
|
||||
**第 3 步:验证依赖已安装:**
|
||||
|
||||
```bash
|
||||
node --version # 应输出 v22.x 或更高
|
||||
git --version # 应输出 2.20+
|
||||
```
|
||||
|
||||
**第 4 步:安装 GSD:**
|
||||
|
||||
```bash
|
||||
npm install -g gsd-pi
|
||||
```
|
||||
|
||||
**第 5 步:设置你的 LLM provider:**
|
||||
|
||||
```bash
|
||||
# 选项 A:设置环境变量(推荐 Anthropic)
|
||||
export ANTHROPIC_API_KEY="sk-ant-..."
|
||||
|
||||
# 选项 B:使用内置配置向导
|
||||
gsd config
|
||||
```
|
||||
|
||||
如果想永久保存这个 key,把 export 语句写入 `~/.zshrc`:
|
||||
|
||||
```bash
|
||||
echo 'export ANTHROPIC_API_KEY="sk-ant-..."' >> ~/.zshrc
|
||||
source ~/.zshrc
|
||||
```
|
||||
|
||||
所有 20+ provider 的完整配置方式请见 [提供商设置指南](./providers.md)。
|
||||
|
||||
**第 6 步:启动 GSD:**
|
||||
|
||||
```bash
|
||||
cd ~/my-project # 进入任意项目目录
|
||||
gsd # 启动一个会话
|
||||
```
|
||||
|
||||
**第 7 步:确认一切正常:**
|
||||
|
||||
```bash
|
||||
gsd --version # 输出已安装版本
|
||||
```
|
||||
|
||||
进入会话后,输入 `/model` 以确认你的 LLM 已成功连接。
|
||||
|
||||
> **Apple Silicon PATH 修复:** 如果安装后找不到 `gsd`,可能是 npm 的全局 bin 目录没有加入 PATH:
|
||||
> ```bash
|
||||
> echo 'export PATH="$(npm prefix -g)/bin:$PATH"' >> ~/.zshrc
|
||||
> source ~/.zshrc
|
||||
> ```
|
||||
|
||||
> **oh-my-zsh 冲突:** oh-my-zsh 的 git 插件定义了 `alias gsd='git svn dcommit'`。可在 `~/.zshrc` 中加入 `unalias gsd 2>/dev/null`,或者改用 `gsd-cli`。
|
||||
|
||||
---
|
||||
|
||||
### Windows
|
||||
|
||||
> **下载链接:** [Node.js](https://nodejs.org/) | [Git for Windows](https://git-scm.com/download/win) | [Windows Terminal](https://aka.ms/terminal)
|
||||
|
||||
#### 选项 A:使用 winget(推荐 Windows 10/11)
|
||||
|
||||
**第 1 步:安装 Node.js 和 Git:**
|
||||
|
||||
```powershell
|
||||
winget install OpenJS.NodeJS.LTS
|
||||
winget install Git.Git
|
||||
```
|
||||
|
||||
**第 2 步:重启终端**(关闭并重新打开 PowerShell 或 Windows Terminal)。
|
||||
|
||||
**第 3 步:验证依赖已安装:**
|
||||
|
||||
```powershell
|
||||
node --version # 应输出 v22.x 或更高
|
||||
git --version # 应输出 2.20+
|
||||
```
|
||||
|
||||
**第 4 步:安装 GSD:**
|
||||
|
||||
```powershell
|
||||
npm install -g gsd-pi
|
||||
```
|
||||
|
||||
**第 5 步:设置你的 LLM provider:**
|
||||
|
||||
```powershell
|
||||
# 选项 A:设置环境变量(仅当前会话)
|
||||
$env:ANTHROPIC_API_KEY = "sk-ant-..."
|
||||
|
||||
# 选项 B:使用内置配置向导
|
||||
gsd config
|
||||
```
|
||||
|
||||
如果要永久保存该 key,可在系统设置的环境变量中添加,或者执行:
|
||||
|
||||
```powershell
|
||||
[System.Environment]::SetEnvironmentVariable("ANTHROPIC_API_KEY", "sk-ant-...", "User")
|
||||
```
|
||||
|
||||
所有 20+ provider 的完整配置方式请见 [提供商设置指南](./providers.md)。
|
||||
|
||||
**第 6 步:启动 GSD:**
|
||||
|
||||
```powershell
|
||||
cd C:\Users\you\my-project # 进入任意项目目录
|
||||
gsd # 启动一个会话
|
||||
```
|
||||
|
||||
**第 7 步:确认一切正常:**
|
||||
|
||||
```powershell
|
||||
gsd --version # 输出已安装版本
|
||||
```
|
||||
|
||||
进入会话后,输入 `/model` 以确认你的 LLM 已成功连接。
|
||||
|
||||
#### 选项 B:手动安装
|
||||
|
||||
1. 下载并安装 [Node.js LTS](https://nodejs.org/),安装时勾选 **“Add to PATH”**
|
||||
2. 下载并安装 [Git for Windows](https://git-scm.com/download/win),使用默认选项
|
||||
3. 打开一个**新的**终端,然后继续执行上面的第 3-7 步
|
||||
|
||||
> **Windows 提示:**
|
||||
> - 建议使用 **Windows Terminal** 或 **PowerShell**,体验最佳。Command Prompt 也能用,但颜色支持较弱。
|
||||
> - 如果 `gsd` 无法识别,先重启终端。Windows 需要新开终端才能读取更新后的 PATH。
|
||||
> - **WSL2** 也可用,安装 WSL 后,在发行版内部按 Linux 说明继续。
|
||||
|
||||
---
|
||||
|
||||
### Linux
|
||||
|
||||
> **下载链接:** [Node.js](https://nodejs.org/) | [Git](https://git-scm.com/download/linux) | [nvm](https://github.com/nvm-sh/nvm)
|
||||
|
||||
先确认你的发行版,然后按对应步骤安装。
|
||||
|
||||
#### Ubuntu / Debian
|
||||
|
||||
**第 1 步:安装 Node.js 和 Git:**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs git
|
||||
```
|
||||
|
||||
#### Fedora / RHEL / CentOS
|
||||
|
||||
**第 1 步:安装 Node.js 和 Git:**
|
||||
|
||||
```bash
|
||||
curl -fsSL https://rpm.nodesource.com/setup_24.x | sudo bash -
|
||||
sudo dnf install -y nodejs git
|
||||
```
|
||||
|
||||
#### Arch Linux
|
||||
|
||||
**第 1 步:安装 Node.js 和 Git:**
|
||||
|
||||
```bash
|
||||
sudo pacman -S nodejs npm git
|
||||
```
|
||||
|
||||
#### 使用 nvm(任意发行版)
|
||||
|
||||
**第 1 步:先安装 nvm,再安装 Node.js:**
|
||||
|
||||
```bash
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash
|
||||
source ~/.bashrc # 或 ~/.zshrc
|
||||
nvm install 24
|
||||
nvm use 24
|
||||
```
|
||||
|
||||
#### 所有发行版:第 2-7 步
|
||||
|
||||
**第 2 步:验证依赖已安装:**
|
||||
|
||||
```bash
|
||||
node --version # 应输出 v22.x 或更高
|
||||
git --version # 应输出 2.20+
|
||||
```
|
||||
|
||||
**第 3 步:安装 GSD:**
|
||||
|
||||
```bash
|
||||
npm install -g gsd-pi
|
||||
```
|
||||
|
||||
**第 4 步:设置你的 LLM provider:**
|
||||
|
||||
```bash
|
||||
# 选项 A:设置环境变量(推荐 Anthropic)
|
||||
export ANTHROPIC_API_KEY="sk-ant-..."
|
||||
|
||||
# 选项 B:使用内置配置向导
|
||||
gsd config
|
||||
```
|
||||
|
||||
如果想永久保存这个 key,把 export 语句写到 `~/.bashrc`(或 `~/.zshrc`)中:
|
||||
|
||||
```bash
|
||||
echo 'export ANTHROPIC_API_KEY="sk-ant-..."' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
所有 20+ provider 的完整配置方式请见 [提供商设置指南](./providers.md)。
|
||||
|
||||
**第 5 步:启动 GSD:**
|
||||
|
||||
```bash
|
||||
cd ~/my-project # 进入任意项目目录
|
||||
gsd # 启动一个会话
|
||||
```
|
||||
|
||||
**第 6 步:确认一切正常:**
|
||||
|
||||
```bash
|
||||
gsd --version # 输出已安装版本
|
||||
```
|
||||
|
||||
进入会话后,输入 `/model` 以确认你的 LLM 已成功连接。
|
||||
|
||||
> **`npm install -g` 遇到权限错误?** 不要用 `sudo npm`。应改为修复 npm 的全局目录:
|
||||
> ```bash
|
||||
> mkdir -p ~/.npm-global
|
||||
> npm config set prefix '~/.npm-global'
|
||||
> echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> ~/.bashrc
|
||||
> source ~/.bashrc
|
||||
> npm install -g gsd-pi
|
||||
> ```
|
||||
|
||||
---
|
||||
|
||||
### Docker(任意操作系统)
|
||||
|
||||
> **下载链接:** [Docker Desktop](https://www.docker.com/products/docker-desktop/)
|
||||
|
||||
如果你不想在宿主机安装 Node.js,可以在隔离沙箱中运行 GSD。
|
||||
|
||||
**第 1 步:安装 Docker Desktop**(要求 4.58+)。
|
||||
|
||||
**第 2 步:克隆 GSD 仓库:**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/gsd-build/gsd-2.git
|
||||
cd gsd-2/docker
|
||||
```
|
||||
|
||||
**第 3 步:创建并进入沙箱:**
|
||||
|
||||
```bash
|
||||
docker sandbox create --template . --name gsd-sandbox
|
||||
docker sandbox exec -it gsd-sandbox bash
|
||||
```
|
||||
|
||||
**第 4 步:设置 API key 并运行 GSD:**
|
||||
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY="sk-ant-..."
|
||||
gsd auto "implement the feature described in issue #42"
|
||||
```
|
||||
|
||||
完整的配置、资源限制和 compose 文件请见 [Docker Sandbox 文档](../../../docker/README.md)。
|
||||
|
||||
---
|
||||
|
||||
## 安装之后
|
||||
|
||||
### 选择模型
|
||||
|
||||
完成 provider 设置后,GSD 会自动选择一个默认模型。你可以在会话中随时切换:
|
||||
|
||||
```
|
||||
/model
|
||||
```
|
||||
|
||||
也可以在偏好设置中按阶段配置模型,详见 [配置](./configuration.md)。
|
||||
|
||||
---
|
||||
|
||||
## 两种工作方式
|
||||
|
||||
### 步骤模式 — `/gsd`
|
||||
|
||||
在会话内输入 `/gsd`。GSD 会一次执行一个工作单元,并在每一步之间暂停,通过向导展示刚完成了什么、下一步是什么。
|
||||
|
||||
- **没有 `.gsd/` 目录**:启动讨论流程,先收集你的项目愿景
|
||||
- **已有 milestone,但没有 roadmap**:讨论或研究该 milestone
|
||||
- **roadmap 已存在,仍有待完成的 slices**:规划下一个 slice 或执行一个 task
|
||||
- **进行到一半的 task**:从上次停下的地方继续
|
||||
|
||||
步骤模式会让你始终留在回路中,在每一步之间查看和确认输出。
|
||||
|
||||
### 自动模式 — `/gsd auto`
|
||||
|
||||
输入 `/gsd auto` 后就可以离开。GSD 会自主完成 research、planning、execution、verification、commit,并持续推进每个 slice,直到 milestone 完成。
|
||||
|
||||
```
|
||||
/gsd auto
|
||||
```
|
||||
|
||||
完整细节请见 [自动模式](./auto-mode.md)。
|
||||
|
||||
---
|
||||
|
||||
## 推荐工作流:两个终端
|
||||
|
||||
一个终端跑自动模式,另一个终端负责引导和干预。
|
||||
|
||||
**终端 1:让它构建**
|
||||
|
||||
```bash
|
||||
gsd
|
||||
/gsd auto
|
||||
```
|
||||
|
||||
**终端 2:在它工作时进行引导**
|
||||
|
||||
```bash
|
||||
gsd
|
||||
/gsd discuss # 讨论架构决策
|
||||
/gsd status # 查看进度
|
||||
/gsd queue # 排队下一个 milestone
|
||||
```
|
||||
|
||||
两个终端都会读写同一套 `.gsd/` 文件。你在终端 2 里做出的决策,会在下一个阶段边界被自动拾取。
|
||||
|
||||
---
|
||||
|
||||
## GSD 如何组织工作
|
||||
|
||||
```
|
||||
Milestone → 一个可交付版本(4-10 个 slice)
|
||||
Slice → 一个可演示的垂直能力(1-7 个 task)
|
||||
Task → 一个适合单个上下文窗口的工作单元
|
||||
```
|
||||
|
||||
铁律是:**一个 task 必须能装进一个上下文窗口。** 装不下,就说明它应该拆成两个 task。
|
||||
|
||||
所有状态都保存在 `.gsd/` 中:
|
||||
|
||||
```
|
||||
.gsd/
|
||||
PROJECT.md — 项目当前是什么
|
||||
REQUIREMENTS.md — 需求契约
|
||||
DECISIONS.md — 追加式架构决策记录
|
||||
KNOWLEDGE.md — 跨会话规则与模式
|
||||
STATE.md — 一眼可见的状态摘要
|
||||
milestones/
|
||||
M001/
|
||||
M001-ROADMAP.md — 带依赖关系的 slice 计划
|
||||
slices/
|
||||
S01/
|
||||
S01-PLAN.md — task 拆解
|
||||
S01-SUMMARY.md — 实际发生了什么
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## VS Code 扩展
|
||||
|
||||
GSD 也提供 VS Code 扩展。你可以从扩展市场安装(publisher: FluxLabs),或者在 VS Code 扩展面板中直接搜索 “GSD”:
|
||||
|
||||
- **`@gsd` 聊天参与者**:在 VS Code Chat 中直接与 agent 对话
|
||||
- **侧边栏仪表板**:显示连接状态、模型信息、Token 使用量
|
||||
- **完整命令面板**:启动 / 停止 agent、切换模型、导出会话
|
||||
|
||||
CLI(`gsd-pi`)需要先安装好,扩展会通过 RPC 与其连接。
|
||||
|
||||
---
|
||||
|
||||
## Web 界面
|
||||
|
||||
GSD 也提供一个基于浏览器的可视化项目管理界面:
|
||||
|
||||
```bash
|
||||
gsd --web
|
||||
```
|
||||
|
||||
详见 [Web 界面](./web-interface.md)。
|
||||
|
||||
---
|
||||
|
||||
## 恢复会话
|
||||
|
||||
```bash
|
||||
gsd --continue # 或 gsd -c
|
||||
```
|
||||
|
||||
会恢复当前目录最近一次会话。
|
||||
|
||||
浏览所有保存过的会话:
|
||||
|
||||
```bash
|
||||
gsd sessions
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 更新 GSD
|
||||
|
||||
GSD 每 24 小时检查一次更新,并在启动时提示。你也可以手动更新:
|
||||
|
||||
```bash
|
||||
npm update -g gsd-pi
|
||||
```
|
||||
|
||||
或者在会话中执行:
|
||||
|
||||
```
|
||||
/gsd update
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 快速排障
|
||||
|
||||
| 问题 | 解决方式 |
|
||||
|------|----------|
|
||||
| `command not found: gsd` | 把 npm 全局 bin 目录加入 PATH(见上面的系统说明) |
|
||||
| `gsd` 实际执行了 `git svn dcommit` | oh-my-zsh 冲突,执行 `unalias gsd` 或改用 `gsd-cli` |
|
||||
| `npm install -g gsd-pi` 权限错误 | 修复 npm prefix(见 Linux 说明)或改用 nvm |
|
||||
| 无法连接到 LLM | 用 `gsd config` 检查 API key,并确认网络可用 |
|
||||
| `gsd` 启动时卡住 | 检查 Node.js 版本:`node --version`(需要 22+) |
|
||||
|
||||
更多问题见 [故障排查](./troubleshooting.md)。
|
||||
|
||||
---
|
||||
|
||||
## 下一步
|
||||
|
||||
- [自动模式](./auto-mode.md):深入理解自主执行
|
||||
- [配置](./configuration.md):模型选择、超时和预算
|
||||
- [命令参考](./commands.md):所有命令和快捷键
|
||||
- [提供商设置](./providers.md):每个 provider 的详细配置
|
||||
- [团队协作](./working-in-teams.md):多开发者工作流
|
||||
186
docs/zh-CN/user-docs/git-strategy.md
Normal file
186
docs/zh-CN/user-docs/git-strategy.md
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
# Git 策略
|
||||
|
||||
GSD 使用 git 来实现 milestone 隔离,以及每个 milestone 内部的顺序提交。你可以通过 **isolation mode** 控制工作发生在哪里。整个策略是自动化的,你不需要手工管理分支。
|
||||
|
||||
## 隔离模式
|
||||
|
||||
GSD 支持三种隔离模式,通过 `git.isolation` 偏好设置:
|
||||
|
||||
| 模式 | 工作目录 | 分支 | 适用场景 |
|
||||
|------|----------|------|----------|
|
||||
| `worktree`(默认) | `.gsd/worktrees/<MID>/` | `milestone/<MID>` | 大多数项目,milestones 之间文件完全隔离 |
|
||||
| `branch` | 项目根目录 | `milestone/<MID>` | 子模块较多、worktree 表现不佳的仓库 |
|
||||
| `none` | 项目根目录 | 当前分支(不建 milestone 分支) | 热重载工作流中,文件隔离会破坏开发工具的场景 |
|
||||
|
||||
### `worktree` 模式(默认)
|
||||
|
||||
每个 milestone 都会在 `.gsd/worktrees/<MID>/` 下拥有自己的 git worktree,对应一个 `milestone/<MID>` 分支。所有执行都发生在该 worktree 中。完成后,worktree 会被 squash merge 回主分支,形成一个干净的提交,然后清理对应 worktree 和分支。
|
||||
|
||||
这提供了完整的文件隔离,某个 milestone 的变更不会干扰你的主工作副本。
|
||||
|
||||
### `branch` 模式
|
||||
|
||||
工作直接在项目根目录中的 `milestone/<MID>` 分支上进行,不会创建 worktree。完成后,该分支会被合并回主分支(是 squash merge 还是普通 merge 由 `merge_strategy` 控制)。
|
||||
|
||||
当 worktree 会带来问题时使用它,例如:子模块较多的仓库、包含硬编码路径的仓库、或者 worktree symlink 表现异常的环境。
|
||||
|
||||
### `none` 模式
|
||||
|
||||
工作直接发生在当前分支。没有 worktree,也没有 milestone 分支。GSD 依然会按顺序提交,并使用 conventional commit message,但不会提供分支级隔离。
|
||||
|
||||
适用于热重载工作流中“文件隔离会破坏开发工具”的情况(例如只能监视项目根目录的文件监听器),或者很小的项目里不值得承担分支开销的情况。
|
||||
|
||||
## 分支模型(worktree 模式)
|
||||
|
||||
```
|
||||
main ─────────────────────────────────────────────────────────
|
||||
│ ↑
|
||||
└── milestone/M001 (worktree) ────────────────────────┘
|
||||
commit: feat: core types
|
||||
commit: feat: markdown parser
|
||||
commit: feat: file writer
|
||||
commit: docs: workflow docs
|
||||
...
|
||||
→ squash-merged to main as single commit
|
||||
```
|
||||
|
||||
在 **branch 模式** 下,流程相同,只是工作发生在项目根目录而不是独立的 worktree 目录。
|
||||
|
||||
在 **none 模式** 下,提交直接落到当前分支,不会创建 milestone 分支,也不需要合并步骤。
|
||||
|
||||
### 并行 worktrees
|
||||
|
||||
如果启用了 [并行编排](./parallel-orchestration.md),多个 milestones 可以同时运行在各自独立的 worktree 中:
|
||||
|
||||
```
|
||||
main ──────────────────────────────────────────────────────────
|
||||
│ ↑ ↑
|
||||
├── milestone/M002 (worktree) ─────────┘ │
|
||||
│ commit: feat: auth types │
|
||||
│ commit: feat: JWT middleware │
|
||||
│ → squash-merged first │
|
||||
│ │
|
||||
└── milestone/M003 (worktree) ────────────────────────┘
|
||||
commit: feat: dashboard layout
|
||||
commit: feat: chart components
|
||||
→ squash-merged second
|
||||
```
|
||||
|
||||
每个 worktree 都工作在自己的分支和自己的提交历史上。为了避免冲突,合并会顺序进行。
|
||||
|
||||
### 关键特性
|
||||
|
||||
- **单分支顺序提交**:没有按 slice 单独分支,也不会在单个 milestone 内产生合并冲突
|
||||
- **Squash merge 到主分支**:在 worktree 和 branch 模式下,所有提交最终都会以一个干净的提交压缩到主分支(可通过 `merge_strategy` 配置)
|
||||
|
||||
### 提交格式
|
||||
|
||||
提交使用 conventional commit 格式,并在 trailer 中带上 GSD 元数据:
|
||||
|
||||
```
|
||||
feat: core type definitions
|
||||
|
||||
GSD-Task: M001/S01/T01
|
||||
|
||||
feat: markdown parser for plan files
|
||||
|
||||
GSD-Task: M001/S01/T02
|
||||
```
|
||||
|
||||
## Worktree 管理
|
||||
|
||||
以下特性仅适用于 **worktree 模式**。
|
||||
|
||||
### 自动(自动模式)
|
||||
|
||||
自动模式会自动创建并管理 worktrees:
|
||||
|
||||
1. milestone 启动时,在 `.gsd/worktrees/<MID>/` 创建 worktree,并切到 `milestone/<MID>` 分支
|
||||
2. 将 `.gsd/milestones/` 下的规划产物复制到该 worktree
|
||||
3. 所有执行都发生在 worktree 内部
|
||||
4. milestone 完成后,把该 worktree squash merge 回集成分支
|
||||
5. 删除 worktree 和对应分支
|
||||
|
||||
### 手动
|
||||
|
||||
使用 `/worktree`(或 `/wt`)命令手动管理 worktree:
|
||||
|
||||
```
|
||||
/worktree create
|
||||
/worktree switch
|
||||
/worktree merge
|
||||
/worktree remove
|
||||
```
|
||||
|
||||
## 工作流模式
|
||||
|
||||
如果不想逐个配置 git 设置,可以通过 `mode` 获得一组更合理的默认值:
|
||||
|
||||
```yaml
|
||||
mode: solo # 个人项目:自动推送、squash、简单 ID
|
||||
mode: team # 共享仓库:唯一 ID、推送分支、预合并检查
|
||||
```
|
||||
|
||||
| 设置 | `solo` | `team` |
|
||||
|---|---|---|
|
||||
| `git.auto_push` | `true` | `false` |
|
||||
| `git.push_branches` | `false` | `true` |
|
||||
| `git.pre_merge_check` | `false` | `true` |
|
||||
| `git.merge_strategy` | `"squash"` | `"squash"` |
|
||||
| `git.isolation` | `"worktree"` | `"worktree"` |
|
||||
| `git.commit_docs` | `true` | `true` |
|
||||
| `unique_milestone_ids` | `false` | `true` |
|
||||
|
||||
Mode 默认值的优先级最低,任何显式偏好设置都会覆盖它们。例如,`mode: solo` 配合 `git.auto_push: false`,就表示除了自动推送以外,其它行为都沿用 solo 的默认配置。
|
||||
|
||||
已有但未设置 `mode` 的配置会保持原样,不会被自动注入新默认值。
|
||||
|
||||
## Git 偏好设置
|
||||
|
||||
可以在偏好设置中配置 git 行为:
|
||||
|
||||
```yaml
|
||||
git:
|
||||
auto_push: false # 提交后推送
|
||||
push_branches: false # 推送 milestone 分支
|
||||
remote: origin
|
||||
snapshots: false # WIP 快照提交
|
||||
pre_merge_check: false # 合并前校验
|
||||
commit_type: feat # 覆盖提交类型前缀
|
||||
main_branch: main # 主分支名称
|
||||
commit_docs: true # 将 .gsd/ 提交到 git
|
||||
isolation: worktree # "worktree"、"branch" 或 "none"
|
||||
auto_pr: false # milestone 完成时自动创建 PR
|
||||
pr_target_branch: develop # PR 目标分支(默认 main)
|
||||
```
|
||||
|
||||
### 自动创建 Pull Request
|
||||
|
||||
对于使用 Gitflow 或分支工作流的团队,GSD 可以在 milestone 完成时自动创建 pull request:
|
||||
|
||||
```yaml
|
||||
git:
|
||||
auto_push: true
|
||||
auto_pr: true
|
||||
pr_target_branch: develop
|
||||
```
|
||||
|
||||
这样会把 milestone 分支推送到远程,并创建一个目标分支为 `develop`(或你指定的其它分支)的 PR。要求已安装并认证 `gh` CLI。详见 [git.auto_pr](./configuration.md#gitauto_pr)。
|
||||
|
||||
### `commit_docs: false`
|
||||
|
||||
当设置为 `false` 时,GSD 会把 `.gsd/` 添加到 `.gitignore`,所有规划产物只保留在本地。适合只有部分成员使用 GSD 的团队,或者公司要求仓库保持干净的场景。
|
||||
|
||||
## 自愈能力
|
||||
|
||||
GSD 内置了对常见 git 问题的自动恢复:
|
||||
|
||||
- **Detached HEAD**:自动重新附着到正确分支
|
||||
- **过期锁文件**:移除崩溃进程残留的 `index.lock`
|
||||
- **孤儿 worktree**:检测并提供清理废弃 worktree 的选项(仅 worktree 模式)
|
||||
|
||||
可通过 `/gsd doctor` 手动检查 git 健康状态。
|
||||
|
||||
## 原生 Git 操作
|
||||
|
||||
从 v2.16 起,GSD 在派发热路径中的读密集 git 操作改用 libgit2 原生绑定。这消除了每次派发周期中约 70 次进程拉起,从而提升了自动模式吞吐量。
|
||||
48
docs/zh-CN/user-docs/migration.md
Normal file
48
docs/zh-CN/user-docs/migration.md
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# 从 v1 迁移
|
||||
|
||||
如果你有仍在使用原始 Get Shit Done(v1)`.planning` 目录结构的项目,可以把它们迁移到 GSD-2 的 `.gsd` 格式。
|
||||
|
||||
## 运行迁移
|
||||
|
||||
```bash
|
||||
# 在项目目录内执行
|
||||
/gsd migrate
|
||||
|
||||
# 或者显式指定路径
|
||||
/gsd migrate ~/projects/my-old-project
|
||||
```
|
||||
|
||||
## 会迁移什么
|
||||
|
||||
迁移工具会:
|
||||
|
||||
- 解析旧版的 `PROJECT.md`、`ROADMAP.md`、`REQUIREMENTS.md`、phase 目录、计划、总结和研究文档
|
||||
- 将 phases 映射为 slices、plans 映射为 tasks、milestones 映射为 milestones
|
||||
- 保留完成状态(`[x]` 阶段保持已完成,原有 summary 会被带过来)
|
||||
- 将研究文件整合进新的目录结构
|
||||
- 在真正写入前先展示预览
|
||||
- 可选运行一次由 agent 驱动的结果审查,以做质量保证
|
||||
|
||||
## 支持的格式
|
||||
|
||||
迁移器可处理多种 v1 文档变体:
|
||||
|
||||
- 按 milestone 分段、带 `<details>` 块的 roadmap
|
||||
- 粗体 phase 条目
|
||||
- 列表格式的 requirements
|
||||
- 十进制 phase 编号
|
||||
- 跨不同 milestones 重复的 phase 编号
|
||||
|
||||
## 前提条件
|
||||
|
||||
如果项目有 `ROADMAP.md` 来描述 milestone 结构,迁移效果最好。没有的话,系统会根据 `phases/` 目录推断 milestones。
|
||||
|
||||
## 迁移后
|
||||
|
||||
迁移完成后,用下面的命令检查输出结果:
|
||||
|
||||
```bash
|
||||
/gsd doctor
|
||||
```
|
||||
|
||||
它会检查 `.gsd/` 的完整性,并标出任何结构性问题。
|
||||
75
docs/zh-CN/user-docs/node-lts-macos.md
Normal file
75
docs/zh-CN/user-docs/node-lts-macos.md
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# 在 macOS 上通过 Homebrew 固定 Node.js LTS 版本
|
||||
|
||||
如果你是通过 Homebrew 安装 Node.js(`brew install node`),那你跟踪的是**当前最新正式版本**,其中可能包含奇数版本的开发分支(例如 23.x、25.x)。这些版本并不是 LTS,可能带来破坏性变更或稳定性问题。
|
||||
|
||||
GSD 要求 Node.js **v22 或更高版本**,并且在 **LTS(偶数版本)** 上运行效果最好。本指南展示如何用 Homebrew 固定到 Node 24 LTS。
|
||||
|
||||
## 检查当前版本
|
||||
|
||||
```bash
|
||||
node --version
|
||||
```
|
||||
|
||||
如果输出的是奇数主版本号(例如 `v23.x`、`v25.x`),说明你当前使用的是开发版。
|
||||
|
||||
## 安装 Node 24 LTS
|
||||
|
||||
Homebrew 为 LTS 版本提供了带版本号的 formula:
|
||||
|
||||
```bash
|
||||
# 取消当前版本(可能不是 LTS)的链接
|
||||
brew unlink node
|
||||
|
||||
# 安装 Node 24 LTS
|
||||
brew install node@24
|
||||
|
||||
# 将它设为默认版本
|
||||
brew link --overwrite node@24
|
||||
```
|
||||
|
||||
验证:
|
||||
|
||||
```bash
|
||||
node --version
|
||||
# 应显示 v24.x.x
|
||||
```
|
||||
|
||||
## 为什么要固定到 LTS?
|
||||
|
||||
- **稳定性**:LTS 版本会在 30 个月内持续收到 bug 修复和安全更新
|
||||
- **兼容性**:包括 GSD 在内的 npm 包通常都会优先测试 LTS 版本
|
||||
- **可预期**:`brew upgrade` 不会把你突然升级到不稳定的开发版
|
||||
|
||||
## 防止误升级
|
||||
|
||||
默认情况下,`brew upgrade` 会升级所有包,这可能让你离开固定版本。可以把对应 formula pin 住:
|
||||
|
||||
```bash
|
||||
brew pin node@24
|
||||
```
|
||||
|
||||
如果以后想取消固定:
|
||||
|
||||
```bash
|
||||
brew unpin node@24
|
||||
```
|
||||
|
||||
## 在多个版本之间切换
|
||||
|
||||
如果你需要同时使用多个 Node 版本(例如 22 和 24),更推荐使用版本管理器:
|
||||
|
||||
- **[nvm](https://github.com/nvm-sh/nvm)**:`nvm install 24 && nvm use 24`
|
||||
- **[fnm](https://github.com/Schniz/fnm)**:`fnm install 24 && fnm use 24`(更快,基于 Rust)
|
||||
- **[mise](https://mise.jdx.dev/)**:`mise use node@24`(多语言版本管理器)
|
||||
|
||||
这些工具允许你通过 `.node-version` 或 `.nvmrc` 为不同项目设置独立的 Node 版本。
|
||||
|
||||
## 验证 GSD 是否正常工作
|
||||
|
||||
固定版本后,执行:
|
||||
|
||||
```bash
|
||||
node --version # v24.x.x
|
||||
npm install -g gsd-pi
|
||||
gsd --version
|
||||
```
|
||||
310
docs/zh-CN/user-docs/parallel-orchestration.md
Normal file
310
docs/zh-CN/user-docs/parallel-orchestration.md
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
# 并行 Milestone 编排
|
||||
|
||||
在隔离的 git worktrees 中同时运行多个 milestones。每个 milestone 都拥有自己的 worker 进程、自己的分支和自己的上下文窗口;同时还会有一个 coordinator 跟踪进度、执行预算限制并保持整体同步。
|
||||
|
||||
> **状态:** 该功能默认处于 `parallel.enabled: false`。属于显式 opt-in,对现有用户零影响。
|
||||
|
||||
## 快速开始
|
||||
|
||||
1. 在偏好设置中开启并行模式:
|
||||
|
||||
```yaml
|
||||
---
|
||||
parallel:
|
||||
enabled: true
|
||||
max_workers: 2
|
||||
---
|
||||
```
|
||||
|
||||
2. 启动并行执行:
|
||||
|
||||
```
|
||||
/gsd parallel start
|
||||
```
|
||||
|
||||
GSD 会扫描所有 milestones,检查依赖与文件重叠,给出一份可并行性报告,并为符合条件的 milestones 启动 workers。
|
||||
|
||||
3. 监控进度:
|
||||
|
||||
```
|
||||
/gsd parallel status
|
||||
```
|
||||
|
||||
4. 完成后停止:
|
||||
|
||||
```
|
||||
/gsd parallel stop
|
||||
```
|
||||
|
||||
## 工作原理
|
||||
|
||||
### 架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Coordinator(你的 GSD 会话) │
|
||||
│ │
|
||||
│ 职责: │
|
||||
│ - 可并行性分析(依赖 + 文件重叠) │
|
||||
│ - Worker 启动与生命周期管理 │
|
||||
│ - 全部 workers 的预算跟踪 │
|
||||
│ - 派发控制信号(pause / resume / stop) │
|
||||
│ - 会话状态监控 │
|
||||
│ - Merge 对账 │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ Worker 1 │ │ Worker 2 │ │ Worker 3 │ ... │
|
||||
│ │ M001 │ │ M003 │ │ M005 │ │
|
||||
│ └──────────┘ └──────────┘ └──────────┘ │
|
||||
│ │ │ │ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ .gsd/worktrees/ .gsd/worktrees/ .gsd/worktrees/ │
|
||||
│ M001/ M003/ M005/ │
|
||||
│ (milestone/ (milestone/ (milestone/ │
|
||||
│ M001 branch) M003 branch) M005 branch) │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Worker 隔离
|
||||
|
||||
每个 worker 都是一个完全隔离的独立 `gsd` 进程:
|
||||
|
||||
| 资源 | 隔离方式 |
|
||||
|------|----------|
|
||||
| **文件系统** | Git worktree:每个 worker 都有自己的 checkout |
|
||||
| **Git 分支** | `milestone/<MID>`:每个 milestone 一条分支 |
|
||||
| **状态推导** | 通过 `GSD_MILESTONE_LOCK` 环境变量,让 `deriveState()` 只看到被分配的 milestone |
|
||||
| **上下文窗口** | 独立进程:每个 worker 都有自己的 agent sessions |
|
||||
| **指标** | 每个 worktree 都有自己的 `.gsd/metrics.json` |
|
||||
| **崩溃恢复** | 每个 worktree 都有自己的 `.gsd/auto.lock` |
|
||||
|
||||
### 协调方式
|
||||
|
||||
Workers 和 coordinator 通过基于文件的 IPC 通信:
|
||||
|
||||
- **会话状态文件**(`.gsd/parallel/<MID>.status.json`):worker 写入 heartbeat,coordinator 读取
|
||||
- **信号文件**(`.gsd/parallel/<MID>.signal.json`):coordinator 写信号,worker 消费
|
||||
- **原子写入**:使用写临时文件再 rename 的方式,避免读到半成品
|
||||
|
||||
## 可并行性分析
|
||||
|
||||
在真正启动并行执行之前,GSD 会先检查哪些 milestones 可以安全并发运行。
|
||||
|
||||
### 规则
|
||||
|
||||
1. **未完成**:已完成的 milestones 会被跳过
|
||||
2. **依赖满足**:所有 `dependsOn` 指向的 milestones 都必须已处于 `complete`
|
||||
3. **文件重叠检查**:如果多个 milestones 会触碰同一批文件,会给出警告(但仍可执行)
|
||||
|
||||
### 示例报告
|
||||
|
||||
```
|
||||
# Parallel Eligibility Report
|
||||
|
||||
## Eligible for Parallel Execution (2)
|
||||
|
||||
- **M002** — Auth System
|
||||
All dependencies satisfied.
|
||||
- **M003** — Dashboard UI
|
||||
All dependencies satisfied.
|
||||
|
||||
## Ineligible (2)
|
||||
|
||||
- **M001** — Core Types
|
||||
Already complete.
|
||||
- **M004** — API Integration
|
||||
Blocked by incomplete dependencies: M002.
|
||||
|
||||
## File Overlap Warnings (1)
|
||||
|
||||
- **M002** <-> **M003** — 2 shared file(s):
|
||||
- `src/types.ts`
|
||||
- `src/middleware.ts`
|
||||
```
|
||||
|
||||
文件重叠只是警告,不是阻断条件。因为两个 milestones 会运行在各自独立的 worktree 中,它们不会在文件系统层面互相干扰。真正的冲突会在 merge 阶段被检测和处理。
|
||||
|
||||
## 配置
|
||||
|
||||
把下面内容加到 `~/.gsd/PREFERENCES.md` 或 `.gsd/PREFERENCES.md`:
|
||||
|
||||
```yaml
|
||||
---
|
||||
parallel:
|
||||
enabled: false # 总开关(默认:false)
|
||||
max_workers: 2 # 并发 workers 数(1-4,默认:2)
|
||||
budget_ceiling: 50.00 # 聚合成本上限(美元,可选)
|
||||
merge_strategy: "per-milestone" # 何时 merge:"per-slice" 或 "per-milestone"
|
||||
auto_merge: "confirm" # "auto"、"confirm" 或 "manual"
|
||||
---
|
||||
```
|
||||
|
||||
### 配置参考
|
||||
|
||||
| Key | 类型 | 默认值 | 说明 |
|
||||
|-----|------|--------|------|
|
||||
| `enabled` | boolean | `false` | 总开关。只有设为 `true`,`/gsd parallel` 命令才可用。 |
|
||||
| `max_workers` | number(1-4) | `2` | 最大并发 worker 进程数。值越高,内存与 API 预算消耗也越高。 |
|
||||
| `budget_ceiling` | number | 无 | 所有 workers 的聚合美元预算上限。达到后不会再派发新单元。 |
|
||||
| `merge_strategy` | `"per-slice"` 或 `"per-milestone"` | `"per-milestone"` | worktree 变更何时回合并到主分支。Per-milestone 会等整个 milestone 完成后再合并。 |
|
||||
| `auto_merge` | `"auto"`、`"confirm"`、`"manual"` | `"confirm"` | merge-back 策略。`confirm` 会在合并前询问;`manual` 要求显式执行 `/gsd parallel merge`。 |
|
||||
|
||||
## 命令
|
||||
|
||||
| 命令 | 说明 |
|
||||
|------|------|
|
||||
| `/gsd parallel start` | 分析可并行性、确认并启动 workers |
|
||||
| `/gsd parallel status` | 显示所有 workers 的状态、已完成单元和成本 |
|
||||
| `/gsd parallel stop` | 停止所有 workers(发送 SIGTERM) |
|
||||
| `/gsd parallel stop M002` | 停止某个指定 milestone 的 worker |
|
||||
| `/gsd parallel pause` | 暂停所有 workers(完成当前单元后等待) |
|
||||
| `/gsd parallel pause M002` | 暂停某个指定 worker |
|
||||
| `/gsd parallel resume` | 恢复所有已暂停 workers |
|
||||
| `/gsd parallel resume M002` | 恢复某个指定 worker |
|
||||
| `/gsd parallel merge` | 把所有已完成 milestones 合并回 main |
|
||||
| `/gsd parallel merge M002` | 只把某个指定 milestone 合并回 main |
|
||||
|
||||
## 信号生命周期
|
||||
|
||||
Coordinator 通过信号和 workers 通信:
|
||||
|
||||
```
|
||||
Coordinator Worker
|
||||
│ │
|
||||
├── sendSignal("pause") ──→ │
|
||||
│ ├── consumeSignal()
|
||||
│ ├── pauseAuto()
|
||||
│ │ (完成当前单元后等待)
|
||||
│ │
|
||||
├── sendSignal("resume") ─→ │
|
||||
│ ├── consumeSignal()
|
||||
│ ├── 继续 dispatch loop
|
||||
│ │
|
||||
├── sendSignal("stop") ───→ │
|
||||
│ + SIGTERM ────────────→ │
|
||||
│ ├── consumeSignal() or SIGTERM handler
|
||||
│ ├── stopAuto()
|
||||
│ └── 进程退出
|
||||
```
|
||||
|
||||
Workers 会在单元之间检查信号(位于 `handleAgentEnd`)。在 stop 场景下,coordinator 还会额外发送 `SIGTERM` 来提高响应速度。
|
||||
|
||||
## Merge 对账
|
||||
|
||||
当 milestones 完成后,它们在 worktree 中的改动需要 merge 回主分支。
|
||||
|
||||
### Merge 顺序
|
||||
|
||||
- **顺序合并**(默认):按 milestone ID 顺序合并(M001 在 M002 之前)
|
||||
- **按完成顺序合并**:按照 milestones 实际完成的先后顺序合并
|
||||
|
||||
### 冲突处理
|
||||
|
||||
1. `.gsd/` 状态文件(如 `STATE.md`、`metrics.json`)会**自动解决**,默认接受 milestone 分支版本
|
||||
2. 代码冲突则会**停止并报告**。合并会暂停,并显示哪些文件冲突。你需要手动解决后,再执行 `/gsd parallel merge <MID>` 重试
|
||||
|
||||
### 示例
|
||||
|
||||
```
|
||||
/gsd parallel merge
|
||||
|
||||
# Merge Results
|
||||
|
||||
- **M002** — merged successfully (pushed)
|
||||
- **M003** — CONFLICT (2 file(s)):
|
||||
- `src/types.ts`
|
||||
- `src/middleware.ts`
|
||||
Resolve conflicts manually and run `/gsd parallel merge M003` to retry.
|
||||
```
|
||||
|
||||
## 预算管理
|
||||
|
||||
当设置了 `budget_ceiling` 时,coordinator 会跟踪所有 workers 的聚合成本:
|
||||
|
||||
- 成本会从每个 worker 的 session status 中汇总
|
||||
- 达到上限后,coordinator 会向 workers 发出停止信号
|
||||
- 每个 worker 仍会独立遵守项目级 `budget_ceiling` 偏好
|
||||
|
||||
## 健康监控
|
||||
|
||||
### Doctor 集成
|
||||
|
||||
`/gsd doctor` 能检测并行会话相关问题:
|
||||
|
||||
- **过期的并行会话**:worker 进程已经死亡,但没有清理干净。Doctor 会检查 `.gsd/parallel/*.status.json` 中记录的 PID 和 heartbeat,发现失效后自动清理。
|
||||
|
||||
可以执行 `/gsd doctor --fix` 自动清理。
|
||||
|
||||
### 过期检测
|
||||
|
||||
满足以下任一条件时,会话会被视为 stale:
|
||||
|
||||
- Worker PID 已经不存在(通过 `process.kill(pid, 0)` 检查)
|
||||
- 最近一次 heartbeat 超过 30 秒
|
||||
|
||||
Coordinator 会在 `refreshWorkerStatuses()` 中执行 stale detection,并自动移除已经死亡的会话。
|
||||
|
||||
## 安全模型
|
||||
|
||||
| 安全层 | 保护内容 |
|
||||
|--------|----------|
|
||||
| **Feature flag** | 默认 `parallel.enabled: false`,不影响现有用户 |
|
||||
| **可并行性分析** | 启动前检查依赖和文件重叠 |
|
||||
| **Worker 隔离** | 独立进程、worktrees、分支、上下文窗口 |
|
||||
| **`GSD_MILESTONE_LOCK`** | 每个 worker 在状态推导时只能看到自己的 milestone |
|
||||
| **`GSD_PARALLEL_WORKER`** | Worker 不能再嵌套启动新的并行会话 |
|
||||
| **预算上限** | 跨所有 workers 执行聚合成本限制 |
|
||||
| **信号式关闭** | 通过文件信号 + SIGTERM 优雅停止 |
|
||||
| **Doctor 集成** | 检测并清理孤儿会话 |
|
||||
| **冲突感知 merge** | 遇到代码冲突时停止;`.gsd/` 状态冲突自动解决 |
|
||||
|
||||
## 文件布局
|
||||
|
||||
```
|
||||
.gsd/
|
||||
├── parallel/ # Coordinator ↔ worker IPC
|
||||
│ ├── M002.status.json # Worker heartbeat + progress
|
||||
│ ├── M002.signal.json # Coordinator → worker signals
|
||||
│ ├── M003.status.json
|
||||
│ └── M003.signal.json
|
||||
├── worktrees/ # Git worktrees(每个 milestone 一个)
|
||||
│ ├── M002/ # M002 的隔离 checkout
|
||||
│ │ ├── .gsd/ # M002 自己的状态文件
|
||||
│ │ │ ├── auto.lock
|
||||
│ │ │ ├── metrics.json
|
||||
│ │ │ └── milestones/
|
||||
│ │ └── src/ # M002 的工作副本
|
||||
│ └── M003/
|
||||
│ └── ...
|
||||
└── ...
|
||||
```
|
||||
|
||||
`.gsd/parallel/` 和 `.gsd/worktrees/` 都会被 gitignore,因为它们只是运行时协调文件,永远不会提交。
|
||||
|
||||
## 故障排查
|
||||
|
||||
### “Parallel mode is not enabled”
|
||||
|
||||
在偏好设置里加入 `parallel.enabled: true`。
|
||||
|
||||
### “No milestones are eligible for parallel execution”
|
||||
|
||||
说明所有 milestones 要么已完成,要么被依赖阻塞。可通过 `/gsd queue` 查看 milestone 状态和依赖链。
|
||||
|
||||
### Worker 崩溃后如何恢复
|
||||
|
||||
Workers 会自动把状态持久化到磁盘。如果某个 worker 进程死亡,coordinator 会通过 heartbeat 超时检测到死掉的 PID,并把该 worker 标记为 crashed。重启后,worker 会从磁盘状态继续:崩溃恢复、worktree 重入和 completed-unit 跟踪都会延续之前的状态。
|
||||
|
||||
1. 执行 `/gsd doctor --fix` 清理 stale sessions
|
||||
2. 执行 `/gsd parallel status` 查看当前状态
|
||||
3. 重新执行 `/gsd parallel start`,为剩余 milestones 启动新的 workers
|
||||
|
||||
### 并行执行完成后发生 merge 冲突
|
||||
|
||||
1. 执行 `/gsd parallel merge` 查看哪些 milestones 存在冲突
|
||||
2. 在 `.gsd/worktrees/<MID>/` 对应的 worktree 中手动解决冲突
|
||||
3. 执行 `/gsd parallel merge <MID>` 重试
|
||||
|
||||
### Workers 看起来卡住了
|
||||
|
||||
先检查是否触达了预算上限:`/gsd parallel status` 会显示每个 worker 的成本。继续执行的话,提升 `parallel.budget_ceiling` 或直接移除它。
|
||||
677
docs/zh-CN/user-docs/providers.md
Normal file
677
docs/zh-CN/user-docs/providers.md
Normal file
|
|
@ -0,0 +1,677 @@
|
|||
# Provider 设置指南
|
||||
|
||||
这是一份覆盖 GSD 所有受支持 LLM providers 的分步配置指南。如果你已经运行过 onboarding 向导(`gsd config`)并选择了 provider,很可能已经配置完成,可以在会话中用 `/model` 检查。
|
||||
|
||||
## 目录
|
||||
|
||||
- [快速参考](#quick-reference)
|
||||
- [内置 Providers](#built-in-providers)
|
||||
- [Anthropic(Claude)](#anthropic-claude)
|
||||
- [OpenAI](#openai)
|
||||
- [Google Gemini](#google-gemini)
|
||||
- [OpenRouter](#openrouter)
|
||||
- [Groq](#groq)
|
||||
- [xAI(Grok)](#xai-grok)
|
||||
- [Mistral](#mistral)
|
||||
- [GitHub Copilot](#github-copilot)
|
||||
- [Amazon Bedrock](#amazon-bedrock)
|
||||
- [Vertex AI 上的 Anthropic](#anthropic-on-vertex-ai)
|
||||
- [Azure OpenAI](#azure-openai)
|
||||
- [本地 Providers](#local-providers)
|
||||
- [Ollama](#ollama)
|
||||
- [LM Studio](#lm-studio)
|
||||
- [vLLM](#vllm)
|
||||
- [SGLang](#sglang)
|
||||
- [自定义 OpenAI-Compatible Endpoints](#custom-openai-compatible-endpoints)
|
||||
- [常见坑点](#common-pitfalls)
|
||||
- [验证你的配置](#verifying-your-setup)
|
||||
|
||||
<a id="quick-reference"></a>
|
||||
## 快速参考
|
||||
|
||||
| Provider | 认证方式 | 环境变量 | 配置文件 |
|
||||
|----------|----------|----------|----------|
|
||||
| Anthropic | API key | `ANTHROPIC_API_KEY` | — |
|
||||
| OpenAI | API key | `OPENAI_API_KEY` | — |
|
||||
| Google Gemini | API key | `GEMINI_API_KEY` | — |
|
||||
| OpenRouter | API key | `OPENROUTER_API_KEY` | 可选 `models.json` |
|
||||
| Groq | API key | `GROQ_API_KEY` | — |
|
||||
| xAI | API key | `XAI_API_KEY` | — |
|
||||
| Mistral | API key | `MISTRAL_API_KEY` | — |
|
||||
| GitHub Copilot | OAuth | `GH_TOKEN` | — |
|
||||
| Amazon Bedrock | IAM credentials | `AWS_PROFILE` 或 `AWS_ACCESS_KEY_ID` | — |
|
||||
| Vertex AI | ADC | `GOOGLE_APPLICATION_CREDENTIALS` | — |
|
||||
| Azure OpenAI | API key | `AZURE_OPENAI_API_KEY` | — |
|
||||
| Ollama | 无(本地) | — | 需要 `models.json` |
|
||||
| LM Studio | 无(本地) | — | 需要 `models.json` |
|
||||
| vLLM / SGLang | 无(本地) | — | 需要 `models.json` |
|
||||
|
||||
---
|
||||
|
||||
<a id="built-in-providers"></a>
|
||||
## 内置 Providers
|
||||
|
||||
内置 providers 的 models 已经预注册在 GSD 里。你只需要提供认证信息。
|
||||
|
||||
<a id="anthropic-claude"></a>
|
||||
### Anthropic(Claude)
|
||||
|
||||
**推荐。** Anthropic models 集成最深,支持内置 Web 搜索、extended thinking 和 prompt caching。
|
||||
|
||||
**选项 A:API key(推荐)**
|
||||
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY="sk-ant-..."
|
||||
```
|
||||
|
||||
或者运行 `gsd config`,在提示时粘贴 key。
|
||||
|
||||
**获取 key:** [console.anthropic.com/settings/keys](https://console.anthropic.com/settings/keys)
|
||||
|
||||
**选项 B:Claude Code CLI**
|
||||
|
||||
如果你有 Claude Pro 或 Max 订阅,可以通过 Anthropic 官方的 Claude Code CLI 完成认证。安装后执行 `claude` 登录,随后 GSD 会自动检测并经由该通道路由:
|
||||
|
||||
```bash
|
||||
# 安装 Claude Code CLI(见 https://docs.anthropic.com/en/docs/claude-code)
|
||||
claude
|
||||
# 按提示登录,然后启动 GSD
|
||||
gsd
|
||||
```
|
||||
|
||||
GSD 会检测你本地的 Claude Code 安装,并把它作为已认证的 Anthropic surface 使用。这是 Anthropic 订阅用户符合 TOS 的方式,GSD 不会直接处理你的订阅凭据。
|
||||
|
||||
> **注意:** GSD 不支持 Anthropic 的浏览器 OAuth 登录。请改用 API key 或 Claude Code CLI。
|
||||
|
||||
**选项 C:在 Claude Code 里直接用 Claude Pro / Max 订阅跑 GSD**
|
||||
|
||||
如果你已经有 Claude Pro / Max 订阅,并希望直接在 Claude Code 里使用 GSD 的 planning、execution 和 milestone orchestration,而不是切到单独终端,那么可以把 GSD 接成一个 MCP server。这样 Claude Code 就能通过 [Model Context Protocol](https://modelcontextprotocol.io) 使用 GSD 的完整 workflow 工具集,在你现有 Claude plan 的驱动下获得 GSD 的结构化项目管理能力。
|
||||
|
||||
**自动配置(推荐)**
|
||||
|
||||
当 GSD 在启动时检测到 Claude Code model,它会自动在项目根目录写入一个带有 GSD workflow MCP server 配置的 `.mcp.json` 文件。无需手动步骤,只要以 Claude Code 作为 provider 启动一次 GSD,配置就会自动生成。
|
||||
|
||||
你也可以在 GSD 会话中手动触发:
|
||||
|
||||
```bash
|
||||
/gsd mcp init
|
||||
```
|
||||
|
||||
这会在项目的 `.mcp.json` 中写入(或更新)`gsd-workflow` 条目。Claude Code 会在下一次启动会话时自动发现这个文件。
|
||||
|
||||
**手动配置**
|
||||
|
||||
如果你更希望自己配置,可以把 GSD 加到项目的 `.mcp.json` 中:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"gsd": {
|
||||
"command": "npx",
|
||||
"args": ["gsd-mcp-server"],
|
||||
"env": {
|
||||
"GSD_CLI_PATH": "/path/to/gsd"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
如果 `gsd-mcp-server` 已经全局安装:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"gsd": {
|
||||
"command": "gsd-mcp-server"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
你也可以把这段配置写到 `~/.claude/settings.json` 的 `mcpServers` 中,让 GSD 在所有项目中都可用。
|
||||
|
||||
**暴露了什么**
|
||||
|
||||
MCP server 会暴露 GSD 的完整 workflow 工具面:milestone planning、task completion、slice 管理、roadmap reassessment、journal 查询等。会话管理工具(`gsd_execute`、`gsd_status`、`gsd_result`、`gsd_cancel`)允许 Claude Code 启动并监控 GSD 自动模式会话。完整工具列表见 [命令 → MCP Server 模式](./commands.md#mcp-server-mode)。
|
||||
|
||||
**验证连接**
|
||||
|
||||
在 GSD 会话里检查 MCP server 是否可达:
|
||||
|
||||
```bash
|
||||
/gsd mcp status
|
||||
```
|
||||
|
||||
<a id="openai"></a>
|
||||
### OpenAI
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY="sk-..."
|
||||
```
|
||||
|
||||
或者运行 `gsd config`,选择 “Paste an API key” 然后选择 “OpenAI”。
|
||||
|
||||
**获取 key:** [platform.openai.com/api-keys](https://platform.openai.com/api-keys)
|
||||
|
||||
<a id="google-gemini"></a>
|
||||
### Google Gemini
|
||||
|
||||
```bash
|
||||
export GEMINI_API_KEY="..."
|
||||
```
|
||||
|
||||
**获取 key:** [aistudio.google.com/app/apikey](https://aistudio.google.com/app/apikey)
|
||||
|
||||
<a id="openrouter"></a>
|
||||
### OpenRouter
|
||||
|
||||
OpenRouter 通过单个 API key 聚合了多个 providers 的 200+ models。
|
||||
|
||||
**第 1 步:获取 API key**
|
||||
|
||||
访问 [openrouter.ai/keys](https://openrouter.ai/keys) 创建一个 key。
|
||||
|
||||
**第 2 步:设置 key**
|
||||
|
||||
```bash
|
||||
export OPENROUTER_API_KEY="sk-or-..."
|
||||
```
|
||||
|
||||
或者运行 `gsd config`,选择 “Paste an API key” 然后选择 “OpenRouter”。
|
||||
|
||||
**第 3 步:切换到 OpenRouter model**
|
||||
|
||||
在 GSD 会话中输入 `/model` 并选择一个 OpenRouter model。OpenRouter models 都以 `openrouter/` 为前缀(例如 `openrouter/anthropic/claude-sonnet-4`)。
|
||||
|
||||
**可选:通过 `models.json` 添加自定义 OpenRouter models**
|
||||
|
||||
如果你想使用不在内置列表中的 model,可把它写进 `~/.gsd/agent/models.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"openrouter": {
|
||||
"baseUrl": "https://openrouter.ai/api/v1",
|
||||
"apiKey": "OPENROUTER_API_KEY",
|
||||
"api": "openai-completions",
|
||||
"models": [
|
||||
{
|
||||
"id": "meta-llama/llama-3.3-70b",
|
||||
"name": "Llama 3.3 70B (OpenRouter)",
|
||||
"reasoning": false,
|
||||
"input": ["text"],
|
||||
"contextWindow": 131072,
|
||||
"maxTokens": 32768,
|
||||
"cost": { "input": 0.3, "output": 0.3, "cacheRead": 0, "cacheWrite": 0 }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
注意:这里的 `apiKey` 字段写的是**环境变量名**,不是字面 key。GSD 会自动解析它。你也可以改用字面值或 shell 命令(见 [值解析](./custom-models.md#value-resolution))。
|
||||
|
||||
**可选:路由到指定上游 provider**
|
||||
|
||||
你可以通过 `modelOverrides` 控制 OpenRouter 实际选用哪个上游 provider:
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"openrouter": {
|
||||
"modelOverrides": {
|
||||
"anthropic/claude-sonnet-4": {
|
||||
"compat": {
|
||||
"openRouterRouting": {
|
||||
"only": ["amazon-bedrock"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<a id="groq"></a>
|
||||
### Groq
|
||||
|
||||
```bash
|
||||
export GROQ_API_KEY="gsk_..."
|
||||
```
|
||||
|
||||
**获取 key:** [console.groq.com/keys](https://console.groq.com/keys)
|
||||
|
||||
<a id="xai-grok"></a>
|
||||
### xAI(Grok)
|
||||
|
||||
```bash
|
||||
export XAI_API_KEY="xai-..."
|
||||
```
|
||||
|
||||
**获取 key:** [console.x.ai](https://console.x.ai)
|
||||
|
||||
<a id="mistral"></a>
|
||||
### Mistral
|
||||
|
||||
```bash
|
||||
export MISTRAL_API_KEY="..."
|
||||
```
|
||||
|
||||
**获取 key:** [console.mistral.ai/api-keys](https://console.mistral.ai/api-keys)
|
||||
|
||||
<a id="github-copilot"></a>
|
||||
### GitHub Copilot
|
||||
|
||||
使用 OAuth,通过浏览器登录:
|
||||
|
||||
```bash
|
||||
gsd config
|
||||
# 选择 "Sign in with your browser" → "GitHub Copilot"
|
||||
```
|
||||
|
||||
要求你拥有有效的 GitHub Copilot 订阅。
|
||||
|
||||
<a id="amazon-bedrock"></a>
|
||||
### Amazon Bedrock
|
||||
|
||||
Bedrock 使用 AWS IAM 凭据,而不是 API key。下面任意一种都可以:
|
||||
|
||||
```bash
|
||||
# 选项 1:命名 profile
|
||||
export AWS_PROFILE="my-profile"
|
||||
|
||||
# 选项 2:IAM keys
|
||||
export AWS_ACCESS_KEY_ID="AKIA..."
|
||||
export AWS_SECRET_ACCESS_KEY="..."
|
||||
export AWS_REGION="us-east-1"
|
||||
|
||||
# 选项 3:Bedrock API key(bearer token)
|
||||
export AWS_BEARER_TOKEN_BEDROCK="..."
|
||||
```
|
||||
|
||||
ECS task roles 和 IRSA(Kubernetes)也会被自动检测。
|
||||
|
||||
<a id="anthropic-on-vertex-ai"></a>
|
||||
### Vertex AI 上的 Anthropic
|
||||
|
||||
使用 Google Cloud Application Default Credentials:
|
||||
|
||||
```bash
|
||||
gcloud auth application-default login
|
||||
export ANTHROPIC_VERTEX_PROJECT_ID="my-project-id"
|
||||
```
|
||||
|
||||
或者设置 `GOOGLE_CLOUD_PROJECT`,并确保 ADC 凭据存在于 `~/.config/gcloud/application_default_credentials.json`。
|
||||
|
||||
<a id="azure-openai"></a>
|
||||
### Azure OpenAI
|
||||
|
||||
```bash
|
||||
export AZURE_OPENAI_API_KEY="..."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<a id="local-providers"></a>
|
||||
## 本地 Providers
|
||||
|
||||
本地 providers 运行在你的机器上。因为 GSD 需要知道 endpoint URL 和可用 models,所以它们都要求配置 `models.json`。
|
||||
|
||||
**配置文件位置:** `~/.gsd/agent/models.json`
|
||||
|
||||
每次打开 `/model` 时,这个文件都会自动重新加载,无需重启。
|
||||
|
||||
<a id="ollama"></a>
|
||||
### Ollama
|
||||
|
||||
**第 1 步:安装并启动 Ollama**
|
||||
|
||||
```bash
|
||||
# macOS
|
||||
brew install ollama
|
||||
ollama serve
|
||||
|
||||
# 或前往 https://ollama.com 下载
|
||||
```
|
||||
|
||||
**第 2 步:拉取一个 model**
|
||||
|
||||
```bash
|
||||
ollama pull llama3.1:8b
|
||||
ollama pull qwen2.5-coder:7b
|
||||
```
|
||||
|
||||
**第 3 步:创建 `~/.gsd/agent/models.json`**
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"ollama": {
|
||||
"baseUrl": "http://localhost:11434/v1",
|
||||
"api": "openai-completions",
|
||||
"apiKey": "ollama",
|
||||
"compat": {
|
||||
"supportsDeveloperRole": false,
|
||||
"supportsReasoningEffort": false
|
||||
},
|
||||
"models": [
|
||||
{ "id": "llama3.1:8b" },
|
||||
{ "id": "qwen2.5-coder:7b" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`apiKey` 是 schema 的必填字段,但 Ollama 会忽略它,因此任意值都可以。
|
||||
|
||||
**第 4 步:选择 model**
|
||||
|
||||
在 GSD 里输入 `/model`,然后选择你的 Ollama model。
|
||||
|
||||
**Ollama 提示:**
|
||||
|
||||
- Ollama 不支持 `developer` role,也不支持 `reasoning_effort`,因此请始终设置 `compat.supportsDeveloperRole: false` 和 `compat.supportsReasoningEffort: false`
|
||||
- 如果得到空响应,先检查 `ollama serve` 是否正在运行,以及 model 是否已经 pull 下来
|
||||
- 如果未显式指定,`contextWindow` 和 `maxTokens` 默认分别为 128K / 16K。若模型能力不同,请手动覆盖
|
||||
|
||||
<a id="lm-studio"></a>
|
||||
### LM Studio
|
||||
|
||||
**第 1 步:安装 LM Studio**
|
||||
|
||||
访问 [lmstudio.ai](https://lmstudio.ai) 下载。
|
||||
|
||||
**第 2 步:启动本地 server**
|
||||
|
||||
在 LM Studio 中进入 “Local Server” 标签页,加载一个 model,然后点击 “Start Server”。默认端口为 1234。
|
||||
|
||||
**第 3 步:创建 `~/.gsd/agent/models.json`**
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"lm-studio": {
|
||||
"baseUrl": "http://localhost:1234/v1",
|
||||
"api": "openai-completions",
|
||||
"apiKey": "lm-studio",
|
||||
"compat": {
|
||||
"supportsDeveloperRole": false,
|
||||
"supportsReasoningEffort": false
|
||||
},
|
||||
"models": [
|
||||
{
|
||||
"id": "your-model-name",
|
||||
"name": "My Local Model",
|
||||
"contextWindow": 32768,
|
||||
"maxTokens": 4096
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
把 `your-model-name` 替换成 LM Studio server 标签页中显示的 model 标识符。
|
||||
|
||||
**LM Studio 提示:**
|
||||
|
||||
- `models.json` 里的 model `id` 必须与 LM Studio server API 返回的值完全一致
|
||||
- LM Studio 默认端口是 1234;如果你改了端口,也要同步修改 `baseUrl`
|
||||
- 如果模型支持更大的上下文,记得上调 `contextWindow` 和 `maxTokens`
|
||||
|
||||
<a id="vllm"></a>
|
||||
### vLLM
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"vllm": {
|
||||
"baseUrl": "http://localhost:8000/v1",
|
||||
"api": "openai-completions",
|
||||
"apiKey": "vllm",
|
||||
"compat": {
|
||||
"supportsDeveloperRole": false,
|
||||
"supportsReasoningEffort": false,
|
||||
"supportsUsageInStreaming": false
|
||||
},
|
||||
"models": [
|
||||
{
|
||||
"id": "meta-llama/Llama-3.1-8B-Instruct",
|
||||
"contextWindow": 128000,
|
||||
"maxTokens": 16384
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
model `id` 必须与 `vllm serve` 启动时传入的 `--model` 参数完全一致。
|
||||
|
||||
<a id="sglang"></a>
|
||||
### SGLang
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"sglang": {
|
||||
"baseUrl": "http://localhost:30000/v1",
|
||||
"api": "openai-completions",
|
||||
"apiKey": "sglang",
|
||||
"compat": {
|
||||
"supportsDeveloperRole": false,
|
||||
"supportsReasoningEffort": false
|
||||
},
|
||||
"models": [
|
||||
{
|
||||
"id": "meta-llama/Llama-3.1-8B-Instruct"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<a id="custom-openai-compatible-endpoints"></a>
|
||||
## 自定义 OpenAI-Compatible Endpoints
|
||||
|
||||
任何实现了 OpenAI Chat Completions API 的 server 都可以和 GSD 配合使用。这包括代理(LiteLLM、Portkey、Helicone)、自托管推理服务,以及新出现的 providers。
|
||||
|
||||
**最快路径:使用 onboarding 向导**
|
||||
|
||||
```bash
|
||||
gsd config
|
||||
# 选择 "Paste an API key" → "Custom (OpenAI-compatible)"
|
||||
# 输入:base URL、API key、model ID
|
||||
```
|
||||
|
||||
这会自动帮你写好 `~/.gsd/agent/models.json`。
|
||||
|
||||
**手动配置:**
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"my-provider": {
|
||||
"baseUrl": "https://my-endpoint.example.com/v1",
|
||||
"apiKey": "MY_PROVIDER_API_KEY",
|
||||
"api": "openai-completions",
|
||||
"models": [
|
||||
{
|
||||
"id": "model-id-here",
|
||||
"name": "Friendly Model Name",
|
||||
"reasoning": false,
|
||||
"input": ["text"],
|
||||
"contextWindow": 128000,
|
||||
"maxTokens": 16384,
|
||||
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**添加自定义 headers(常见于代理)**
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"litellm-proxy": {
|
||||
"baseUrl": "https://litellm.example.com/v1",
|
||||
"apiKey": "MY_API_KEY",
|
||||
"api": "openai-completions",
|
||||
"headers": {
|
||||
"x-custom-header": "value"
|
||||
},
|
||||
"models": [...]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**支持 thinking mode 的 Qwen models**
|
||||
|
||||
对于 Qwen-compatible servers,可用 `thinkingFormat` 打开 thinking mode:
|
||||
|
||||
```json
|
||||
{
|
||||
"compat": {
|
||||
"thinkingFormat": "qwen",
|
||||
"supportsDeveloperRole": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
如果该 server 要求 `chat_template_kwargs.enable_thinking`,请改用 `"qwen-chat-template"`。
|
||||
|
||||
关于 `compat` 字段、`modelOverrides`、值解析和高级配置的完整说明,见 [自定义模型](./custom-models.md)。
|
||||
|
||||
---
|
||||
|
||||
<a id="common-pitfalls"></a>
|
||||
## 常见坑点
|
||||
|
||||
### 使用有效 key 仍提示 “Authentication failed”
|
||||
|
||||
**原因:** key 虽然设在 shell 中,但 GSD 看不到。
|
||||
|
||||
**解决:** 确认你是在同一个终端里 `export` 了该环境变量并运行 `gsd`。或者直接用 `gsd config` 把 key 保存进 `~/.gsd/agent/auth.json`,这样就能跨会话持久化。
|
||||
|
||||
### OpenRouter models 没出现在 `/model`
|
||||
|
||||
**原因:** 没有设置 `OPENROUTER_API_KEY`,因此 GSD 会隐藏 OpenRouter models。
|
||||
|
||||
**解决:** 设置 key 并重启 GSD:
|
||||
|
||||
```bash
|
||||
export OPENROUTER_API_KEY="sk-or-..."
|
||||
gsd
|
||||
```
|
||||
|
||||
### Ollama 返回空响应
|
||||
|
||||
**原因:** Ollama server 没有运行,或者对应 model 尚未 pull。
|
||||
|
||||
**解决:**
|
||||
|
||||
```bash
|
||||
# 确认 server 正在运行
|
||||
curl http://localhost:11434/v1/models
|
||||
|
||||
# 如果 model 缺失则先 pull
|
||||
ollama pull llama3.1:8b
|
||||
```
|
||||
|
||||
### LM Studio model ID 不匹配
|
||||
|
||||
**原因:** `models.json` 中的 `id` 和 LM Studio 实际通过 API 暴露的值不一致。
|
||||
|
||||
**解决:** 去 LM Studio 的 server 标签页查看精确的 model 标识符。它通常会包含文件名或量化后缀(例如 `lmstudio-community/Meta-Llama-3.1-8B-Instruct-GGUF`)。
|
||||
|
||||
### 本地 models 报 `developer` role 错误
|
||||
|
||||
**原因:** 大多数本地推理 server 不支持 OpenAI 的 `developer` message role。
|
||||
|
||||
**解决:** 在 provider 配置里添加 `compat.supportsDeveloperRole: false`。这样 GSD 会改用 `system` message:
|
||||
|
||||
```json
|
||||
{
|
||||
"compat": {
|
||||
"supportsDeveloperRole": false,
|
||||
"supportsReasoningEffort": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 本地 models 报 `stream_options` 错误
|
||||
|
||||
**原因:** 部分 server 不支持 `stream_options: { include_usage: true }`。
|
||||
|
||||
**解决:** 添加 `compat.supportsUsageInStreaming: false`:
|
||||
|
||||
```json
|
||||
{
|
||||
"compat": {
|
||||
"supportsUsageInStreaming": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 报 “apiKey is required” 校验错误
|
||||
|
||||
**原因:** `models.json` schema 规定:只要定义了 `models`,就必须存在 `apiKey`。
|
||||
|
||||
**解决:** 对于不需要认证的本地 server,填一个占位值即可:
|
||||
|
||||
```json
|
||||
"apiKey": "not-needed"
|
||||
```
|
||||
|
||||
### 自定义 models 的成本显示为 `$0.00`
|
||||
|
||||
这是**预期行为**。GSD 对自定义 models 的默认成本就是 0。如果你想获得准确的成本跟踪,需要自己填写 `cost` 字段:
|
||||
|
||||
```json
|
||||
"cost": { "input": 0.15, "output": 0.60, "cacheRead": 0.015, "cacheWrite": 0.19 }
|
||||
```
|
||||
|
||||
这些值的单位都是每百万 tokens。
|
||||
|
||||
---
|
||||
|
||||
<a id="verifying-your-setup"></a>
|
||||
## 验证你的配置
|
||||
|
||||
完成 provider 配置后:
|
||||
|
||||
1. **启动 GSD:**
|
||||
```bash
|
||||
gsd
|
||||
```
|
||||
|
||||
2. **检查可用 models:**
|
||||
```
|
||||
/model
|
||||
```
|
||||
列表里应该能看到该 provider 的 models。
|
||||
|
||||
3. **切换到对应 model:**
|
||||
在 `/model` 选择器中选中它。
|
||||
|
||||
4. **发送一条测试消息:**
|
||||
输入任意内容,确认 model 可以正常响应。
|
||||
|
||||
如果 model 没有出现,请检查:
|
||||
|
||||
- 当前 shell 中是否设置了对应环境变量
|
||||
- `models.json` 是否是合法 JSON(可执行 `cat ~/.gsd/agent/models.json | python3 -m json.tool`)
|
||||
- 本地 providers 的 server 是否已经运行
|
||||
|
||||
如果还需要更多帮助,请查看 [故障排查](./troubleshooting.md),或者在会话中运行 `/gsd doctor`。
|
||||
161
docs/zh-CN/user-docs/remote-questions.md
Normal file
161
docs/zh-CN/user-docs/remote-questions.md
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
# 远程提问
|
||||
|
||||
在无头自动模式下运行时,远程提问允许 GSD 通过 Slack、Discord 或 Telegram 请求用户输入。当 GSD 遇到需要人工判断的决策点时,它会把问题发到你配置好的频道,并轮询等待响应。
|
||||
|
||||
## 设置
|
||||
|
||||
### Discord
|
||||
|
||||
```
|
||||
/gsd remote discord
|
||||
```
|
||||
|
||||
配置向导会:
|
||||
|
||||
1. 询问你的 Discord bot token
|
||||
2. 通过 Discord API 验证该 token
|
||||
3. 列出 bot 当前加入的服务器(或让你选择)
|
||||
4. 列出所选服务器中的文本频道
|
||||
5. 发送一条测试消息以确认权限
|
||||
6. 把配置保存到 `~/.gsd/PREFERENCES.md`
|
||||
|
||||
**Bot 要求:**
|
||||
|
||||
- 需要一个带 token 的 Discord bot application(来自 [Discord Developer Portal](https://discord.com/developers/applications))
|
||||
- Bot 必须以以下权限加入目标服务器:
|
||||
- Send Messages
|
||||
- Read Message History
|
||||
- Add Reactions
|
||||
- View Channel
|
||||
- 必须设置 `DISCORD_BOT_TOKEN` 环境变量(配置向导会帮你处理)
|
||||
|
||||
### Slack
|
||||
|
||||
```
|
||||
/gsd remote slack
|
||||
```
|
||||
|
||||
配置向导会:
|
||||
|
||||
1. 询问你的 Slack bot token(`xoxb-...`)
|
||||
2. 验证该 token
|
||||
3. 列出 bot 可访问的频道(也支持手动输入 ID)
|
||||
4. 发送一条测试消息确认权限
|
||||
5. 保存配置
|
||||
|
||||
**Bot 要求:**
|
||||
|
||||
- 需要一个带 bot token 的 Slack app(来自 [Slack API](https://api.slack.com/apps))
|
||||
- Bot 必须已加入目标频道
|
||||
- 公共 / 私有频道常见需要的 scope:`chat:write`、`reactions:read`、`reactions:write`、`channels:read`、`groups:read`、`channels:history`、`groups:history`
|
||||
|
||||
### Telegram
|
||||
|
||||
```
|
||||
/gsd remote telegram
|
||||
```
|
||||
|
||||
配置向导会:
|
||||
|
||||
1. 询问你的 Telegram bot token(来自 [@BotFather](https://t.me/BotFather))
|
||||
2. 通过 Telegram API 验证该 token
|
||||
3. 询问 chat ID(群聊或私聊)
|
||||
4. 发送测试消息以确认权限
|
||||
5. 保存配置
|
||||
|
||||
**Bot 要求:**
|
||||
|
||||
- 需要一个来自 [@BotFather](https://t.me/BotFather) 的 Telegram bot token
|
||||
- Bot 必须已加入目标群聊(或者直接与 bot 私聊)
|
||||
- 必须设置 `TELEGRAM_BOT_TOKEN` 环境变量
|
||||
|
||||
## 配置
|
||||
|
||||
远程提问配置保存在 `~/.gsd/PREFERENCES.md`:
|
||||
|
||||
```yaml
|
||||
remote_questions:
|
||||
channel: discord # 或 slack 或 telegram
|
||||
channel_id: "1234567890123456789"
|
||||
timeout_minutes: 5 # 1-30,默认 5
|
||||
poll_interval_seconds: 5 # 2-30,默认 5
|
||||
```
|
||||
|
||||
## 工作原理
|
||||
|
||||
1. GSD 在自动模式过程中遇到一个决策点
|
||||
2. 问题会以富文本 embed(Discord)或 Block Kit 消息(Slack)的形式发送到你配置的频道
|
||||
3. GSD 按设定的间隔轮询响应
|
||||
4. 你可以通过以下方式回复:
|
||||
- **添加数字表情回应**(1️⃣、2️⃣ 等),适用于单问题提示
|
||||
- **回复消息内容**,可以是数字(`1`)、逗号分隔数字(`1,3`)或自由文本
|
||||
5. GSD 读取到响应后继续执行
|
||||
6. 提示消息上会追加一个 ✅ 反应,表示已收到
|
||||
|
||||
### 响应格式
|
||||
|
||||
**单个问题:**
|
||||
|
||||
- 用数字表情回应(适用于单问题提示)
|
||||
- 回复一个数字:`2`
|
||||
- 回复自由文本(会作为用户备注记录)
|
||||
|
||||
**多个问题:**
|
||||
|
||||
- 用分号回复:`1;2;custom text`
|
||||
- 用换行回复(每行一个答案)
|
||||
|
||||
### 超时
|
||||
|
||||
如果在 `timeout_minutes` 内没有收到响应,提示会超时,GSD 将带着超时结果继续执行。LLM 会根据当前上下文处理超时,通常是做一个保守默认选择,或者暂停自动模式。
|
||||
|
||||
## 命令
|
||||
|
||||
| 命令 | 说明 |
|
||||
|------|------|
|
||||
| `/gsd remote` | 显示远程提问菜单和当前状态 |
|
||||
| `/gsd remote slack` | 配置 Slack 集成 |
|
||||
| `/gsd remote discord` | 配置 Discord 集成 |
|
||||
| `/gsd remote status` | 显示当前配置和最近一次提示状态 |
|
||||
| `/gsd remote disconnect` | 移除远程提问配置 |
|
||||
|
||||
## Discord 与 Slack 功能对比
|
||||
|
||||
| 功能 | Discord | Slack |
|
||||
|------|---------|-------|
|
||||
| 富文本消息格式 | Embeds with fields | Block Kit |
|
||||
| 用 reaction 回答 | ✅(单问题) | ✅(单问题) |
|
||||
| 线程式回复 | Message replies | Thread replies |
|
||||
| 日志中的消息 URL | ✅ | ✅ |
|
||||
| 已收到应答的确认 | ✅ 收到后加 reaction | ✅ 收到后加 reaction |
|
||||
| 多问题支持 | 文本回复(分号 / 换行) | 文本回复(分号 / 换行) |
|
||||
| 提示中的上下文来源 | ✅(footer) | ✅(context block) |
|
||||
| 服务器 / 频道选择器 | ✅(交互式) | ✅(交互式 + 手动兜底) |
|
||||
| Token 验证 | ✅ | ✅ |
|
||||
| 配置阶段测试消息 | ✅ | ✅ |
|
||||
|
||||
## 故障排查
|
||||
|
||||
### “Remote auth failed”
|
||||
|
||||
- 确认 bot token 正确且未过期
|
||||
- 对 Discord:确认 bot 仍然在目标服务器内
|
||||
- 对 Slack:确认 bot token 以 `xoxb-` 开头
|
||||
|
||||
### “Could not send to channel”
|
||||
|
||||
- 确认 bot 在目标频道拥有 Send Messages 权限
|
||||
- 对 Discord:检查 Server Settings 中 bot 对应角色的权限
|
||||
- 对 Slack:确认 bot 已加入频道(`/invite @botname`)
|
||||
|
||||
### 未检测到响应
|
||||
|
||||
- 确认你是在**回复该提示消息**,而不是单独发了一条新消息
|
||||
- 对 reactions:只有单问题提示上的数字表情(1️⃣-5️⃣)会被识别
|
||||
- 检查 `timeout_minutes` 是否足够长,能覆盖你的响应时间
|
||||
|
||||
### 频道 ID 格式
|
||||
|
||||
- **Slack**:9-12 位大写字母数字字符(例如 `C0123456789`)
|
||||
- **Discord**:17-20 位纯数字 snowflake ID(例如 `1234567890123456789`)
|
||||
- 在 Discord 中开启 Developer Mode(Settings → Advanced)后可以复制频道 ID
|
||||
195
docs/zh-CN/user-docs/skills.md
Normal file
195
docs/zh-CN/user-docs/skills.md
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
# 技能
|
||||
|
||||
技能(Skills)是当当前 task 匹配时由 GSD 加载的专用指令集。它们为 LLM 提供领域化指导,例如编码模式、框架惯用法、测试策略和工具使用方式。
|
||||
|
||||
Skills 遵循开放的 [Agent Skills 标准](https://agentskills.io/),并且**不是 GSD 专属格式**。它们同样适用于 Claude Code、OpenAI Codex、Cursor、GitHub Copilot、Windsurf 以及其他 40+ agent。
|
||||
|
||||
## 技能目录
|
||||
|
||||
GSD 会按优先级顺序从两个位置读取技能:
|
||||
|
||||
| 位置 | 范围 | 说明 |
|
||||
|------|------|------|
|
||||
| `~/.agents/skills/` | 全局 | 对所有项目和所有兼容 agent 共享 |
|
||||
| `.agents/skills/`(项目根目录) | 项目级 | 项目专用技能,可提交到版本控制 |
|
||||
|
||||
如果出现同名技能,全局技能优先于项目技能。
|
||||
|
||||
> **从 `~/.gsd/agent/skills/` 迁移:** 升级后首次启动时,GSD 会自动把旧版 `~/.gsd/agent/skills/` 中的技能复制到 `~/.agents/skills/`。旧目录会保留,以兼容旧流程。
|
||||
|
||||
## 安装技能
|
||||
|
||||
技能通过 [skills.sh CLI](https://skills.sh) 安装:
|
||||
|
||||
```bash
|
||||
# 交互式:选择要安装的技能以及目标 agent
|
||||
npx skills add dpearson2699/swift-ios-skills
|
||||
|
||||
# 非交互方式安装指定技能
|
||||
npx skills add dpearson2699/swift-ios-skills --skill swift-concurrency --skill swiftui-patterns -y
|
||||
|
||||
# 安装仓库中的全部技能
|
||||
npx skills add dpearson2699/swift-ios-skills --all
|
||||
|
||||
# 检查更新
|
||||
npx skills check
|
||||
|
||||
# 更新已安装技能
|
||||
npx skills update
|
||||
```
|
||||
|
||||
### 入门技能目录
|
||||
|
||||
在执行 `gsd init` 时,GSD 会检测项目技术栈并推荐合适的技能包。对于 brownfield 项目,检测是自动的;对于 greenfield 项目,则由用户选择技术栈。
|
||||
|
||||
这个精选目录维护在 `src/resources/extensions/gsd/skill-catalog.ts`。每一条目都会把一个技术栈映射到一个 skills.sh 仓库,以及其中的具体技能名称。
|
||||
|
||||
#### 可用技能包
|
||||
|
||||
**Swift(检测到任意 Swift 项目,例如 `Package.swift` 或 `.xcodeproj`):**
|
||||
|
||||
- **SwiftUI**:布局、导航、动画、手势、Liquid Glass
|
||||
- **Swift Core**:Swift 语言、并发、Codable、Charts、Testing、SwiftData
|
||||
|
||||
**iOS(仅当 `.xcodeproj` 目标通过 `SDKROOT` 指向 `iphoneos` 时):**
|
||||
|
||||
- **iOS App Frameworks**:App Intents、Widgets、StoreKit、MapKit、Live Activities
|
||||
- **iOS Data Frameworks**:CloudKit、HealthKit、MusicKit、WeatherKit、Contacts
|
||||
- **iOS AI & ML**:Core ML、Vision、端侧 AI、语音识别
|
||||
- **iOS Engineering**:网络、安全、可访问性、本地化、Instruments
|
||||
- **iOS Hardware**:Bluetooth、CoreMotion、NFC、PencilKit、RealityKit
|
||||
- **iOS Platform**:CallKit、EnergyKit、HomeKit、SharePlay、PermissionKit
|
||||
|
||||
**Web:**
|
||||
|
||||
- **React & Web Frontend**:React 最佳实践、Web 设计、组合模式
|
||||
- **React Native**:跨平台移动开发模式
|
||||
- **Frontend Design & UX**:前端设计与可访问性
|
||||
|
||||
**语言:**
|
||||
|
||||
- **Rust**:Rust 模式与最佳实践
|
||||
- **Python**:Python 模式与最佳实践
|
||||
- **Go**:Go 模式与最佳实践
|
||||
|
||||
**通用:**
|
||||
|
||||
- **Document Handling**:PDF、DOCX、XLSX、PPTX 的创建和处理
|
||||
|
||||
### 维护目录
|
||||
|
||||
技能目录定义位于 [`src/resources/extensions/gsd/skill-catalog.ts`](../../../src/resources/extensions/gsd/skill-catalog.ts)。新增或更新一个技能包时:
|
||||
|
||||
1. 在 `SKILL_CATALOG` 数组中新增一个 `SkillPack` 条目,包含 `repo`、`skills` 和匹配条件
|
||||
2. 基于语言检测做匹配时,使用 `matchLanguages`(取值来自 `detection.ts` 中的 `LANGUAGE_MAP`)
|
||||
3. 基于 Xcode 平台做匹配时,使用 `matchXcodePlatforms`(例如 `["iphoneos"]`,取自 `project.pbxproj` 中的 `SDKROOT`)
|
||||
4. 基于文件存在与否做匹配时,使用 `matchFiles`(对照 `detection.ts` 中的 `PROJECT_FILES`)
|
||||
5. 如果这个技能包需要在 greenfield 选项中出现,把它加入 `GREENFIELD_STACKS`
|
||||
6. 如果多个技能包共享同一个 `repo`,它们会被合并为一次 `npx skills add` 调用
|
||||
|
||||
## 技能发现
|
||||
|
||||
`skill_discovery` 偏好控制 GSD 在自动模式中如何发现技能:
|
||||
|
||||
| 模式 | 行为 |
|
||||
|------|------|
|
||||
| `auto` | 自动查找并应用技能 |
|
||||
| `suggest` | 识别技能,但需要确认(默认) |
|
||||
| `off` | 关闭技能发现 |
|
||||
|
||||
## 技能偏好
|
||||
|
||||
你可以通过偏好设置控制使用哪些技能:
|
||||
|
||||
```yaml
|
||||
---
|
||||
version: 1
|
||||
always_use_skills:
|
||||
- debug-like-expert
|
||||
prefer_skills:
|
||||
- frontend-design
|
||||
avoid_skills:
|
||||
- security-docker
|
||||
skill_rules:
|
||||
- when: task involves Clerk authentication
|
||||
use: [clerk]
|
||||
- when: frontend styling work
|
||||
prefer: [frontend-design]
|
||||
---
|
||||
```
|
||||
|
||||
### 解析顺序
|
||||
|
||||
技能可以通过以下几种方式引用:
|
||||
|
||||
1. **裸名称**:例如 `frontend-design`,会扫描 `~/.agents/skills/` 和项目内的 `.agents/skills/`
|
||||
2. **绝对路径**:例如 `/Users/you/.agents/skills/my-skill/SKILL.md`
|
||||
3. **目录路径**:例如 `~/custom-skills/my-skill`,会在其中查找 `SKILL.md`
|
||||
|
||||
全局技能(`~/.agents/skills/`)优先于项目技能(`.agents/skills/`)。
|
||||
|
||||
## 自定义技能
|
||||
|
||||
你可以通过新增一个包含 `SKILL.md` 的目录来创建自己的技能:
|
||||
|
||||
```
|
||||
~/.agents/skills/my-skill/
|
||||
SKILL.md — 给 LLM 的指令
|
||||
references/ — 可选参考文件
|
||||
```
|
||||
|
||||
`SKILL.md` 中写的是当技能启用时,LLM 应遵循的指令。参考文件可由技能按需加载。
|
||||
|
||||
### 项目本地技能
|
||||
|
||||
如果想为某个项目提供专用指导,可以把技能放在项目里:
|
||||
|
||||
```
|
||||
.agents/skills/my-project-skill/
|
||||
SKILL.md
|
||||
```
|
||||
|
||||
项目本地技能可以提交到版本控制中,让团队成员共享同一套技能。
|
||||
|
||||
## 技能生命周期管理
|
||||
|
||||
GSD 会跨自动模式会话跟踪技能表现,并提供健康度数据,帮助你持续维护技能质量。
|
||||
|
||||
### 技能遥测
|
||||
|
||||
每个自动模式工作单元都会记录哪些技能可用、哪些技能实际加载。这些数据和现有的 token / 成本数据一起存入 `metrics.json`。
|
||||
|
||||
### 技能健康度面板
|
||||
|
||||
通过 `/gsd skill-health` 查看技能表现:
|
||||
|
||||
```
|
||||
/gsd skill-health # 总览表:名称、使用次数、成功率、token、趋势、最近使用时间
|
||||
/gsd skill-health rust-core # 查看单个技能的详细信息
|
||||
/gsd skill-health --stale 30 # 查看 30+ 天未使用的技能
|
||||
/gsd skill-health --declining # 查看成功率在下降的技能
|
||||
```
|
||||
|
||||
该面板会标出可能需要关注的技能:
|
||||
|
||||
- **最近 10 次使用的成功率低于 70%**
|
||||
- **Token 使用量比上一个窗口上升 20% 以上**
|
||||
- **过期技能**:超过设定阈值未使用
|
||||
|
||||
### 过期检测
|
||||
|
||||
长时间未使用的技能会被标记为 stale,并可自动降低优先级:
|
||||
|
||||
```yaml
|
||||
---
|
||||
skill_staleness_days: 60 # 默认 60;设为 0 表示关闭
|
||||
---
|
||||
```
|
||||
|
||||
过期技能会被排除在自动匹配之外,但仍然可以通过 `read` 显式调用。
|
||||
|
||||
### Heal-Skill(单元后分析)
|
||||
|
||||
如果把它配置为 post-unit hook,GSD 可以分析 agent 在执行中是否偏离了某个技能的指令。如果检测到明显漂移(例如 API 模式过时、指导错误),它会把建议修复写到 `.gsd/skill-review-queue.md`,供人工审核。
|
||||
|
||||
一个关键设计原则是:技能**永远不会被自动修改**。研究表明,人工策展的技能明显优于自动生成技能,因此保留人工审核是必要的。
|
||||
373
docs/zh-CN/user-docs/token-optimization.md
Normal file
373
docs/zh-CN/user-docs/token-optimization.md
Normal file
|
|
@ -0,0 +1,373 @@
|
|||
# Token 优化
|
||||
|
||||
*引入于 v2.17.0*
|
||||
|
||||
GSD 2.17 引入了一套协同工作的 token 优化系统,在大多数工作负载下可以在不牺牲输出质量的前提下,将 token 使用降低 40-60%。这套系统由三部分构成:**token profiles**、**context compression** 和 **基于复杂度的 task 路由**。
|
||||
|
||||
## Token Profiles
|
||||
|
||||
Token profile 是一个单一偏好项,用来统一协调 model 选择、阶段跳过和上下文压缩级别。在偏好设置中这样配置:
|
||||
|
||||
```yaml
|
||||
---
|
||||
version: 1
|
||||
token_profile: balanced
|
||||
---
|
||||
```
|
||||
|
||||
可用的 profile 有三个:
|
||||
|
||||
### `budget`:最大节省(降低 40-60%)
|
||||
|
||||
面向成本敏感型工作流。它会使用更便宜的 models,跳过可选阶段,并把 dispatch 上下文压缩到最低必要程度。
|
||||
|
||||
| 维度 | 设置 |
|
||||
|------|------|
|
||||
| Planning model | Sonnet |
|
||||
| Execution model | Sonnet |
|
||||
| Simple task model | Haiku |
|
||||
| Completion model | Haiku |
|
||||
| Subagent model | Haiku |
|
||||
| Milestone research | **跳过** |
|
||||
| Slice research | **跳过** |
|
||||
| Roadmap reassessment | **跳过** |
|
||||
| Context inline level | **Minimal**:丢弃 decisions、requirements、额外 templates |
|
||||
|
||||
适合:原型开发、小项目、已充分理解的代码库、强调成本控制的迭代。
|
||||
|
||||
### `balanced`:智能默认值(默认)
|
||||
|
||||
默认 profile。保留关键阶段,跳过那些对大多数项目边际收益不高的阶段,并采用标准级别的上下文压缩。
|
||||
|
||||
| 维度 | 设置 |
|
||||
|------|------|
|
||||
| Planning model | 用户默认值 |
|
||||
| Execution model | 用户默认值 |
|
||||
| Simple task model | 用户默认值 |
|
||||
| Completion model | 用户默认值 |
|
||||
| Subagent model | Sonnet |
|
||||
| Milestone research | 执行 |
|
||||
| Slice research | **跳过** |
|
||||
| Roadmap reassessment | 执行 |
|
||||
| Context inline level | **Standard**:保留关键上下文,丢弃低信号附加内容 |
|
||||
|
||||
适合:大多数项目、日常开发。
|
||||
|
||||
### `quality`:完整上下文(不压缩)
|
||||
|
||||
所有阶段都会运行。所有上下文产物都会被内联。没有捷径。
|
||||
|
||||
| 维度 | 设置 |
|
||||
|------|------|
|
||||
| 所有 models | 用户配置的默认值 |
|
||||
| 所有阶段 | 执行 |
|
||||
| Context inline level | **Full**:全部内联 |
|
||||
|
||||
适合:复杂架构、需要深度 research 的 greenfield 项目、关键生产环境工作。
|
||||
|
||||
## Context Compression
|
||||
|
||||
每个 token profile 都会映射到一个 **inline level**,它控制在 dispatch prompt 里预加载多少上下文:
|
||||
|
||||
| Profile | Inline Level | 包含内容 |
|
||||
|---------|--------------|----------|
|
||||
| `budget` | `minimal` | Task plan、关键历史 summaries(截断)。不包含 decisions register、requirements、UAT template、secrets manifest。 |
|
||||
| `balanced` | `standard` | Task plan、历史 summaries、slice plan、roadmap 摘要。不包含部分辅助 templates。 |
|
||||
| `quality` | `full` | 全部内容:所有 plans、summaries、decisions、requirements、templates 和根文件。 |
|
||||
|
||||
### 压缩如何工作
|
||||
|
||||
Dispatch prompt builder 接受一个 `inlineLevel` 参数。在不同级别下,特定产物会被按规则裁剪:
|
||||
|
||||
**Minimal 级别的裁剪:**
|
||||
|
||||
- `buildExecuteTaskPrompt`:丢弃 decisions template,并把历史 summaries 截断到只保留最近一个
|
||||
- `buildPlanMilestonePrompt`:丢弃 `PROJECT.md`、`REQUIREMENTS.md`、decisions 以及 `secrets-manifest` 等补充 templates
|
||||
- `buildCompleteSlicePrompt`:丢弃 requirements 和 UAT template 的内联
|
||||
- `buildCompleteMilestonePrompt`:丢弃根级 GSD 文件内联
|
||||
- `buildReassessRoadmapPrompt`:丢弃 project、requirements 和 decisions 文件
|
||||
|
||||
这些裁剪是累积式的:`standard` 会丢掉一部分,`minimal` 会丢掉更多;`full` 则保留全部上下文(也就是 v2.17 之前的行为)。
|
||||
|
||||
### 覆盖 Inline Level
|
||||
|
||||
Inline level 由 `token_profile` 推导而来。如果你想独立于 profile 控制阶段行为,请使用 `phases` 偏好设置:
|
||||
|
||||
```yaml
|
||||
---
|
||||
version: 1
|
||||
token_profile: budget
|
||||
phases:
|
||||
skip_research: false # 覆盖:即使是 budget,也执行 research
|
||||
---
|
||||
```
|
||||
|
||||
显式设置的 `phases` 总是优先于 profile 默认值。
|
||||
|
||||
<a id="complexity-based-task-routing"></a>
|
||||
## 基于复杂度的 Task 路由
|
||||
|
||||
当启用 dynamic routing 时,GSD 会根据复杂度对每个 task 做分类,并将其路由到合适的 model tier。简单的文档修复会使用更便宜的模型,而复杂的架构工作会获得所需的推理能力。
|
||||
|
||||
> **前提条件:** Dynamic routing 需要在偏好设置里显式配置 `models`。如果没有 `models` 段,routing 会被跳过,所有 phases 都会使用会话启动时的 model。Token profiles 会自动设置 `models`。
|
||||
|
||||
> **上限行为:** 当 dynamic routing 启用时,每个 phase 中配置的 model 充当的是**上限**,而不是固定绑定。Router 可以为更简单的工作降级到更便宜的 model,但绝不会超过你配置的 model。
|
||||
|
||||
### 分类如何工作
|
||||
|
||||
Tasks 会通过分析 task plan 来分类:
|
||||
|
||||
| 信号 | Simple | Standard | Complex |
|
||||
|------|--------|----------|---------|
|
||||
| Step 数量 | ≤ 3 | 4-7 | ≥ 8 |
|
||||
| 文件数 | ≤ 3 | 4-7 | ≥ 8 |
|
||||
| 描述长度 | < 500 chars | 500-2000 | > 2000 chars |
|
||||
| 代码块数 | — | — | ≥ 5 |
|
||||
| 信号词 | 无 | 任意出现 | — |
|
||||
|
||||
**会阻止判定为 simple 的信号词:** `research`、`investigate`、`refactor`、`migrate`、`integrate`、`complex`、`architect`、`redesign`、`security`、`performance`、`concurrent`、`parallel`、`distributed`、`backward compat`、`migration`、`architecture`、`concurrency`、`compatibility`。
|
||||
|
||||
空 plan 或格式错误的 plan 会默认归类到 `standard`(偏保守的选择)。
|
||||
|
||||
### Unit Type 默认值
|
||||
|
||||
非 task 单元也有内置的 tier 分配:
|
||||
|
||||
| Unit Type | 默认 Tier |
|
||||
|-----------|-----------|
|
||||
| `complete-slice`、`run-uat` | Light |
|
||||
| `research-*`、`plan-*`、`execute-task`、`complete-milestone` | Standard |
|
||||
| `replan-slice`、`reassess-roadmap` | Heavy |
|
||||
| `hook/*` | Light |
|
||||
|
||||
### Model 路由
|
||||
|
||||
每个 tier 会映射到某类 model 配置:
|
||||
|
||||
| Tier | 对应 Model Phase Key | 常见 Model |
|
||||
|------|----------------------|------------|
|
||||
| Light | `completion` | Haiku(budget)/ 用户默认值 |
|
||||
| Standard | `execution` | Sonnet / 用户默认值 |
|
||||
| Heavy | `execution` | Opus / 用户默认值 |
|
||||
|
||||
如果配置了 `execution_simple`,simple tasks 会优先使用它。`budget` profile 会自动把该键设为 Haiku。
|
||||
|
||||
<a id="budget-pressure"></a>
|
||||
### 预算压力
|
||||
|
||||
当接近预算上限时,分类器会自动降低 tier:
|
||||
|
||||
| 已使用预算 | 影响 |
|
||||
|------------|------|
|
||||
| < 50% | 不调整 |
|
||||
| 50-75% | Standard → Light |
|
||||
| 75-90% | Standard → Light |
|
||||
| > 90% | 除 Heavy 之外全部 → Light;Heavy → Standard |
|
||||
|
||||
这种逐步降级方式能尽量把最复杂工作的模型质量保留下来,同时随着预算逼近上限逐步降低成本。
|
||||
|
||||
## 自适应学习(Routing History)
|
||||
|
||||
GSD 会随着时间推移记录每个 tier 分配的成功 / 失败情况,并据此调整未来的分类。它默认自动生效,并持久化在 `.gsd/routing-history.json` 中。
|
||||
|
||||
### 工作方式
|
||||
|
||||
1. 每个工作单元完成后,系统会把结果(成功 / 失败)记录到对应的 unit type 和 tier 上
|
||||
2. 结果会按 pattern 跟踪,例如 `execute-task` 或 `execute-task:docs`,并维护最近 50 条的滚动窗口
|
||||
3. 如果某个 pattern 下某个 tier 的失败率超过 20%,未来相同 pattern 的分类会自动上调一个 tier
|
||||
4. 系统也支持更细粒度的 tag pattern,例如 `execute-task:test` 和 `execute-task:frontend`
|
||||
|
||||
### 用户反馈
|
||||
|
||||
你可以通过 `/gsd rate` 为最近完成的工作单元提交反馈:
|
||||
|
||||
```
|
||||
/gsd rate over # model 太强了,下次更倾向便宜一点
|
||||
/gsd rate ok # model 选得合适,不调整
|
||||
/gsd rate under # model 太弱了,下次更倾向强一点
|
||||
```
|
||||
|
||||
这些反馈的权重是自动结果的 2 倍。要求 dynamic routing 已启用(最近完成的单元必须带有 tier 数据)。
|
||||
|
||||
### 数据管理
|
||||
|
||||
```bash
|
||||
# Routing history 按项目存储
|
||||
.gsd/routing-history.json
|
||||
|
||||
# 清空历史以重置自适应学习
|
||||
# (通过 routing-history 模块 API 完成)
|
||||
```
|
||||
|
||||
反馈数组最多保留 200 条。每个 pattern 的结果统计使用 50 条滚动窗口,以防陈旧数据长期主导判断。
|
||||
|
||||
## 配置示例
|
||||
|
||||
### 成本优先配置
|
||||
|
||||
```yaml
|
||||
---
|
||||
version: 1
|
||||
token_profile: budget
|
||||
budget_ceiling: 25.00
|
||||
models:
|
||||
execution_simple: claude-haiku-4-5-20250414
|
||||
---
|
||||
```
|
||||
|
||||
### 使用自定义 Models 的平衡配置
|
||||
|
||||
```yaml
|
||||
---
|
||||
version: 1
|
||||
token_profile: balanced
|
||||
models:
|
||||
planning:
|
||||
model: claude-opus-4-6
|
||||
fallbacks:
|
||||
- openrouter/z-ai/glm-5
|
||||
execution: claude-sonnet-4-6
|
||||
---
|
||||
```
|
||||
|
||||
### 面向关键工作的高质量配置
|
||||
|
||||
```yaml
|
||||
---
|
||||
version: 1
|
||||
token_profile: quality
|
||||
models:
|
||||
planning: claude-opus-4-6
|
||||
execution: claude-opus-4-6
|
||||
---
|
||||
```
|
||||
|
||||
### 按阶段覆盖
|
||||
|
||||
`token_profile` 会设置默认值,但显式偏好始终优先:
|
||||
|
||||
```yaml
|
||||
---
|
||||
version: 1
|
||||
token_profile: budget
|
||||
phases:
|
||||
skip_research: false # 覆盖:保留 milestone research
|
||||
models:
|
||||
planning: claude-opus-4-6 # 覆盖:即使是 budget profile,planning 也用 Opus
|
||||
---
|
||||
```
|
||||
|
||||
## 这些机制如何协同
|
||||
|
||||
```
|
||||
PREFERENCES.md
|
||||
└─ token_profile: balanced
|
||||
├─ resolveProfileDefaults() → model 默认值 + phase 跳过默认值
|
||||
├─ resolveInlineLevel() → standard
|
||||
│ └─ prompt builders 根据 level 决定纳入哪些上下文
|
||||
├─ classifyUnitComplexity() → 路由到 execution / execution_simple model
|
||||
│ ├─ task plan 分析(steps、files、signals)
|
||||
│ ├─ unit type 默认值
|
||||
│ ├─ budget pressure 调整
|
||||
│ ├─ 从 routing-history.json 做自适应学习
|
||||
│ └─ capability scoring(当 `capability_routing: true` 时)
|
||||
│ └─ 7 维 model profile × task requirement vectors
|
||||
└─ context_management
|
||||
├─ observation masking(before_provider_request hook)
|
||||
├─ tool result truncation(tool_result_max_chars)
|
||||
└─ phase handoff anchors(注入 prompt builders)
|
||||
```
|
||||
|
||||
Profile 会在 dispatch pipeline 的起点解析一次,并一路向下流动。每一层上,显式偏好都优先于 profile 默认值。
|
||||
|
||||
## Observation Masking
|
||||
|
||||
*引入于 v2.59.0*
|
||||
|
||||
在自动模式会话中,tool results 会不断堆积在会话历史里并占用上下文窗口。Observation masking 会在每次 LLM 调用前,把早于最近 N 个 user turns 的 tool result 内容替换成轻量占位符。这样可以在**不增加任何 LLM 开销**的前提下减少 token 使用:不需要额外总结调用,也不会带来额外延迟。
|
||||
|
||||
Observation masking 在自动模式中默认开启。可通过偏好设置控制:
|
||||
|
||||
```yaml
|
||||
context_management:
|
||||
observation_masking: true # 默认:true(设为 false 可关闭)
|
||||
observation_mask_turns: 8 # 保留最近 8 个 user turns 内的结果(范围:1-50)
|
||||
tool_result_max_chars: 800 # 单个 tool result 超过该长度时进行截断
|
||||
```
|
||||
|
||||
### 工作方式
|
||||
|
||||
1. 每次 provider request 之前,`before_provider_request` hook 会检查 messages 数组
|
||||
2. 早于阈值的 tool results(`toolResult`、`bashExecution`)会被替换成 `[result masked — within summarized history]`
|
||||
3. 最近的 tool results(仍在保留窗口内)会完整保留
|
||||
4. 所有 assistant 和 user messages 始终保留,只有 tool result 内容会被 masking
|
||||
|
||||
它与现有的 compaction 系统配套:masking 负责减少两次 compaction 之间的上下文压力,而 compaction 负责在窗口填满时执行完整上下文重置。
|
||||
|
||||
### Tool Result Truncation
|
||||
|
||||
单个 tool result 如果超过 `tool_result_max_chars`(默认 800),会被加上 `…[truncated]` 标记后截断。这可以防止某一次特别大的工具输出独占上下文窗口。
|
||||
|
||||
## Phase Handoff Anchors
|
||||
|
||||
*引入于 v2.59.0*
|
||||
|
||||
当自动模式在 phases 之间切换(research → planning → execution)时,系统会把结构化 JSON anchors 写到 `.gsd/milestones/<mid>/anchors/<phase>.json`。下游 prompt builders 会自动注入这些 anchors,让下一阶段继承前一阶段的意图、决策、阻塞点和下一步,而不必重新从 artifact 文件里推断。
|
||||
|
||||
这能减少上下文漂移,也就是企业级 agent 失败案例中最常见的一类问题:agent 在 phase 边界上丢失了之前的决策脉络。
|
||||
|
||||
Anchors 会在 `research-milestone`、`research-slice`、`plan-milestone` 和 `plan-slice` 成功完成后自动写入,不需要任何配置。
|
||||
|
||||
## Prompt Compression
|
||||
|
||||
*引入于 v2.29.0*
|
||||
|
||||
GSD 可以在退回到 section-boundary truncation 之前,先做确定性的 prompt compression。这样在上下文超预算时,可以保留更多信息。
|
||||
|
||||
### 压缩策略
|
||||
|
||||
在偏好设置中配置:
|
||||
|
||||
```yaml
|
||||
---
|
||||
version: 1
|
||||
compression_strategy: compress
|
||||
---
|
||||
```
|
||||
|
||||
可用策略有两个:
|
||||
|
||||
| 策略 | 行为 | 默认适用对象 |
|
||||
|------|------|--------------|
|
||||
| `truncate` | 在边界处整段丢弃 section(v2.29 之前的行为) | `quality` profile |
|
||||
| `compress` | 先做启发式文本压缩,如果仍超预算,再截断 | `budget` 和 `balanced` profiles |
|
||||
|
||||
Compression 会确定性地去掉冗余空白、缩短啰嗦表达、去重重复内容并删除低信息量样板文本,不涉及任何 LLM 调用。
|
||||
|
||||
### 上下文选择
|
||||
|
||||
控制文件如何内联进 prompt:
|
||||
|
||||
```yaml
|
||||
---
|
||||
version: 1
|
||||
context_selection: smart
|
||||
---
|
||||
```
|
||||
|
||||
| 模式 | 行为 | 默认适用对象 |
|
||||
|------|------|--------------|
|
||||
| `full` | 内联完整文件 | `balanced` 和 `quality` profiles |
|
||||
| `smart` | 对大文件(>3KB)使用 TF-IDF 语义分块,只纳入相关部分 | `budget` profile |
|
||||
|
||||
### 结构化数据压缩
|
||||
|
||||
在 `budget` 和 `balanced` 的 inline level 下,decisions 和 requirements 会被格式化成更紧凑的表示方式,相比完整 markdown tables 可节省 30-50% tokens。
|
||||
|
||||
### Summary Distillation
|
||||
|
||||
如果某个 slice 有 3 个以上依赖 summary,且总量超过 summary 预算,GSD 会先提取结构化核心数据(`provides`、`requires`、`key_files`、`key_decisions`),丢弃冗长 prose 段落,然后才会退回到 section-boundary truncation。
|
||||
|
||||
### Cache Hit Rate Tracking
|
||||
|
||||
指标账本现在会为每个工作单元记录 `cacheHitRate`(输入 tokens 中来自缓存的比例),并提供 `aggregateCacheHitRate()` 用于统计整场会话的缓存表现。
|
||||
434
docs/zh-CN/user-docs/troubleshooting.md
Normal file
434
docs/zh-CN/user-docs/troubleshooting.md
Normal file
|
|
@ -0,0 +1,434 @@
|
|||
# 故障排查
|
||||
|
||||
## `/gsd doctor`
|
||||
|
||||
内置诊断工具会校验 `.gsd/` 的完整性:
|
||||
|
||||
```
|
||||
/gsd doctor
|
||||
```
|
||||
|
||||
它会检查:
|
||||
|
||||
- 文件结构和命名约定
|
||||
- roadmap ↔ slice ↔ task 的引用完整性
|
||||
- 完成状态是否一致
|
||||
- Git worktree 健康状态(仅 worktree 和 branch 模式;none 模式跳过)
|
||||
- 过期锁文件和孤儿运行时记录
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 自动模式在同一个单元上循环
|
||||
|
||||
**症状:** 同一个工作单元(例如 `research-slice` 或 `plan-slice`)被反复派发,直到触发 dispatch 上限。
|
||||
|
||||
**原因:**
|
||||
|
||||
- 崩溃后的缓存过期:内存中的文件列表没有反映新产物
|
||||
- LLM 没有生成预期的 artifact 文件
|
||||
|
||||
**解决:** 先运行 `/gsd doctor` 修复状态,然后执行 `/gsd auto` 恢复。如果问题持续存在,检查预期 artifact 文件是否确实已经写到磁盘。
|
||||
|
||||
### 自动模式因 “Loop detected” 停止
|
||||
|
||||
**原因:** 同一个单元连续两次没有生成预期 artifact。
|
||||
|
||||
**解决:** 检查 task plan 是否足够清晰。如果 plan 存在歧义,先手动澄清,再执行 `/gsd auto` 恢复。
|
||||
|
||||
### Worktree 中出现了错误文件
|
||||
|
||||
**症状:** Planning 产物或代码被写到了错误目录。
|
||||
|
||||
**原因:** LLM 把内容写回了主仓库,而不是 worktree。
|
||||
|
||||
**解决:** 该问题已在 v2.14+ 修复。如果你仍在旧版本,请更新。现在 dispatch prompt 已包含明确的工作目录指令。
|
||||
|
||||
### 安装后出现 `command not found: gsd`
|
||||
|
||||
**症状:** `npm install -g gsd-pi` 成功,但系统找不到 `gsd`。
|
||||
|
||||
**原因:** npm 的全局 bin 目录没有加入 shell 的 `$PATH`。
|
||||
|
||||
**解决:**
|
||||
|
||||
```bash
|
||||
# 找出 npm 安装二进制的目录
|
||||
npm prefix -g
|
||||
# 输出:/opt/homebrew(Apple Silicon)或 /usr/local(Intel Mac)
|
||||
|
||||
# 如果缺失,把 bin 目录加入 PATH
|
||||
echo 'export PATH="$(npm prefix -g)/bin:$PATH"' >> ~/.zshrc
|
||||
source ~/.zshrc
|
||||
```
|
||||
|
||||
**临时方案:** 直接执行 `npx gsd-pi`,或使用 `$(npm prefix -g)/bin/gsd`。
|
||||
|
||||
**常见原因:**
|
||||
|
||||
- **Homebrew Node**:理论上 `/opt/homebrew/bin` 应该在 PATH 里,但如果 shell profile 没有初始化 Homebrew,就可能缺失
|
||||
- **版本管理器(nvm、fnm、mise)**:全局 bin 路径是按版本区分的,需确保版本管理器正确初始化
|
||||
- **oh-my-zsh**:`gitfast` 插件会把 `gsd` alias 到 `git svn dcommit`。可通过 `alias gsd` 检查,并在需要时取消 alias
|
||||
|
||||
### `npm install -g gsd-pi` 失败
|
||||
|
||||
**常见原因:**
|
||||
|
||||
- 缺少 workspace packages:已在 v2.10.4+ 修复
|
||||
- Linux 上 `postinstall` 卡住(Playwright `--with-deps` 触发 sudo):已在 v2.3.6+ 修复
|
||||
- Node.js 版本过低:要求 ≥ 22.0.0
|
||||
|
||||
### 自动模式中的 provider 错误
|
||||
|
||||
**症状:** 自动模式因为 provider 错误暂停(限流、服务端错误、认证失败)。
|
||||
|
||||
**GSD 的处理方式(v2.26):**
|
||||
|
||||
| 错误类型 | 自动恢复? | 延迟 |
|
||||
|----------|------------|------|
|
||||
| Rate limit(429、`too many requests`) | ✅ 是 | `retry-after` 头或默认 60 秒 |
|
||||
| Server error(500、502、503、`overloaded`) | ✅ 是 | 30 秒 |
|
||||
| Auth / billing(`unauthorized`、`invalid key`) | ❌ 否 | 需要手动恢复 |
|
||||
|
||||
对于瞬时错误,GSD 会短暂停顿后自动继续。对于永久性错误,建议配置 fallback models:
|
||||
|
||||
```yaml
|
||||
models:
|
||||
execution:
|
||||
model: claude-sonnet-4-6
|
||||
fallbacks:
|
||||
- openrouter/minimax/minimax-m2.5
|
||||
```
|
||||
|
||||
**Headless 模式:** `gsd headless auto` 在进程崩溃时会自动重启整个进程(默认 3 次,带指数退避)。与 provider 错误自动恢复配合后,能支持真正的夜间无人值守运行。
|
||||
|
||||
常见的 provider 配置问题(role 错误、streaming 错误、model ID 不匹配)见 [Provider 设置指南:常见坑点](./providers.md#common-pitfalls)。
|
||||
|
||||
### 达到预算上限
|
||||
|
||||
**症状:** 自动模式因 “Budget ceiling reached” 暂停。
|
||||
|
||||
**解决:** 提高偏好设置中的 `budget_ceiling`,或者切换到 `budget` token profile 降低每个工作单元成本,然后再执行 `/gsd auto` 恢复。
|
||||
|
||||
### 过期锁文件
|
||||
|
||||
**症状:** 自动模式无法启动,提示另一个会话正在运行。
|
||||
|
||||
**解决:** GSD 会自动检测过期锁:如果持有锁的 PID 已死亡,则在下次 `/gsd auto` 时清理并重新获取锁。它也会处理 `proper-lockfile` 崩溃后遗留的 `.gsd.lock/` 目录。如果自动恢复失败,可手动删除 `.gsd/auto.lock` 和 `.gsd.lock/`:
|
||||
|
||||
```bash
|
||||
rm -f .gsd/auto.lock
|
||||
rm -rf "$(dirname .gsd)/.gsd.lock"
|
||||
```
|
||||
|
||||
### Git merge 冲突
|
||||
|
||||
**症状:** Worktree merge 在 `.gsd/` 文件上失败。
|
||||
|
||||
**解决:** GSD 会自动解决 `.gsd/` 运行时文件上的冲突。对于代码文件的内容冲突,LLM 会先获得一次 fix-merge 会话进行自动修复;若失败,则需要手动解决。
|
||||
|
||||
### Pre-dispatch 提示 milestone integration branch 已不存在
|
||||
|
||||
**症状:** 自动模式或 `/gsd doctor` 报告某个 milestone 记录的 integration branch 已经不在 git 中。
|
||||
|
||||
**这意味着什么:** 该 milestone 的 `.gsd/milestones/<MID>/<MID>-META.json` 里仍然记录着启动时的 branch,但该 branch 之后被重命名或删除了。
|
||||
|
||||
**当前行为:**
|
||||
|
||||
- 如果 GSD 能确定性地恢复到一个安全 branch,就不会再直接 hard-stop 自动模式
|
||||
- 安全回退的顺序是:
|
||||
- 显式配置且存在的 `git.main_branch`
|
||||
- 仓库自动检测到的默认 integration branch(例如 `main` 或 `master`)
|
||||
- 在这种情况下,`/gsd doctor` 会给出 warning,而 `/gsd doctor fix` 会把过期的 metadata 改写为当前有效 branch
|
||||
- 如果无法确定安全回退 branch,GSD 仍会阻止继续运行
|
||||
|
||||
**解决:**
|
||||
|
||||
- 先执行 `/gsd doctor fix`,在安全回退很明显时自动改写过期 metadata
|
||||
- 如果 GSD 仍然阻塞,则请重新创建缺失 branch,或更新 git 偏好设置,让 `git.main_branch` 指向一个真实存在的 branch
|
||||
|
||||
### 写 `.gsd/` 文件时出现瞬时 `EBUSY` / `EPERM` / `EACCES`
|
||||
|
||||
**症状:** 在 Windows 上,自动模式或 doctor 在更新 `.gsd/` 文件时偶发 `EBUSY`、`EPERM` 或 `EACCES`。
|
||||
|
||||
**原因:** 杀毒软件、索引器、编辑器或文件监视器可能会在 GSD 执行原子 rename 的瞬间,短暂锁住目标文件或临时文件。
|
||||
|
||||
**当前行为:** GSD 现在会对这类瞬时 rename 失败做短时、有上界的退避重试;这样既能覆盖短暂锁竞争,也不会因为真正的文件系统问题而无限挂起。
|
||||
|
||||
**解决:**
|
||||
|
||||
- 重新执行操作;大多数瞬时锁竞争会很快自行解除
|
||||
- 如果错误持续,关闭可能占用该文件的工具后再试
|
||||
- 如果反复失败,运行 `/gsd doctor`,确认仓库状态依旧健康,并记录具体路径与错误码
|
||||
|
||||
### Node v24 Web 启动失败
|
||||
|
||||
**症状:** 在 Node v24 上执行 `gsd --web` 时,报 `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`。
|
||||
|
||||
**原因:** Node v24 修改了对 `node_modules` 的 type stripping 行为,导致 Next.js Web 构建失败。
|
||||
|
||||
**解决:** 已在 v2.42.0+ 修复(#1864)。升级到最新版本。
|
||||
|
||||
### 孤儿 Web server 进程
|
||||
|
||||
**症状:** `gsd --web` 因端口 3000 已被占用而失败,但实际上并没有运行中的 GSD 会话。
|
||||
|
||||
**原因:** 上一次 Web server 退出时未能清理进程。
|
||||
|
||||
**解决:** 已在 v2.42.0+ 修复。现在 GSD 会自动清理过期的 Web server 进程。如果你还在旧版本,可手动终止孤儿进程:`lsof -ti:3000 | xargs kill`。
|
||||
|
||||
### 非 JS 项目被 worktree health check 阻挡
|
||||
|
||||
**症状:** 在不使用 Node.js 的项目(例如 Rust、Go、Python)中,worktree health check 失败或阻塞自动模式。
|
||||
|
||||
**原因:** 在 v2.42.0 之前,worktree health check 只识别 JavaScript 生态。
|
||||
|
||||
**解决:** 已在 v2.42.0+ 修复(#1860)。现在 health check 已支持 17+ 生态。升级到最新版本。
|
||||
|
||||
### 德语 / 非英语 locale 下的 git 错误
|
||||
|
||||
**症状:** 当系统 locale 不是英语(例如德语)时,Git 命令失败或输出异常。
|
||||
|
||||
**原因:** GSD 之前假设 git 输出永远是英文。
|
||||
|
||||
**解决:** 已在 v2.42.0+ 修复。现在所有 git 命令都会强制 `LC_ALL=C`,从而无论系统 locale 如何,都保证 git 输出一致为英文。
|
||||
|
||||
## MCP Client 问题
|
||||
|
||||
### `mcp_servers` 显示没有已配置 servers
|
||||
|
||||
**症状:** `mcp_servers` 报告没有配置任何 server。
|
||||
|
||||
**常见原因:**
|
||||
|
||||
- 当前项目里不存在 `.mcp.json` 或 `.gsd/mcp.json`
|
||||
- 配置文件不是合法 JSON
|
||||
- 你是在另一个项目目录中配置的 server,但当前启动 GSD 的目录不同
|
||||
|
||||
**解决:**
|
||||
|
||||
- 把 server 配置加到 `.mcp.json` 或 `.gsd/mcp.json`
|
||||
- 确认文件能被正常解析为 JSON
|
||||
- 重新执行 `mcp_servers(refresh=true)`
|
||||
|
||||
### `mcp_discover` 超时
|
||||
|
||||
**症状:** `mcp_discover` 因超时失败。
|
||||
|
||||
**常见原因:**
|
||||
|
||||
- Server 进程启动了,但没有完成 MCP 握手
|
||||
- 配置的命令指向一个启动时会卡住的脚本
|
||||
- Server 正在等待某个不可用依赖或后端服务
|
||||
|
||||
**解决:**
|
||||
|
||||
- 在 GSD 外部直接运行该命令,确认 server 能真正启动
|
||||
- 检查后端 URL 或依赖服务是否可达
|
||||
- 如果是本地自定义 server,确认它使用的是 MCP SDK 或正确的 stdio 协议实现
|
||||
|
||||
### `mcp_discover` 报 connection closed
|
||||
|
||||
**症状:** `mcp_discover` 立即失败,并提示连接被关闭。
|
||||
|
||||
**常见原因:**
|
||||
|
||||
- 可执行文件路径错误
|
||||
- 脚本路径错误
|
||||
- 缺失运行时依赖
|
||||
- Server 在响应前就崩溃了
|
||||
|
||||
**解决:**
|
||||
|
||||
- 确认 `command` 和 `args` 路径正确且尽量使用绝对路径
|
||||
- 手动运行命令,查看导入 / 运行时错误
|
||||
- 检查配置中的解释器或运行时在当前机器上是否存在
|
||||
|
||||
### `mcp_call` 因缺少必填参数失败
|
||||
|
||||
**症状:** MCP tool 已成功发现,但调用时因缺少必填字段而校验失败。
|
||||
|
||||
**常见原因:**
|
||||
|
||||
- 调用形状写错了
|
||||
- 目标 server 的 tool schema 已更新
|
||||
- 你调用的是旧 server 定义或旧分支构建
|
||||
|
||||
**解决:**
|
||||
|
||||
- 重新执行 `mcp_discover(server="name")`,确认实际要求的参数名
|
||||
- 按 `mcp_call(server="name", tool="tool_name", args={...})` 的形式调用
|
||||
- 如果你正在开发 GSD 本身,在 schema 变更后重新执行 `npm run build`
|
||||
|
||||
### 本地 stdio server 手动可用,但在 GSD 中不可用
|
||||
|
||||
**症状:** 手动执行 server 命令没有问题,但 GSD 连接不上。
|
||||
|
||||
**常见原因:**
|
||||
|
||||
- Server 依赖某些 GSD 不会继承的 shell 状态
|
||||
- 相对路径只有在另一个 working directory 中才成立
|
||||
- 需要的环境变量存在于你的 shell 中,但没有写进 MCP 配置
|
||||
|
||||
**解决:**
|
||||
|
||||
- 对 `command` 和脚本参数都使用绝对路径
|
||||
- 把所需环境变量写进 MCP 配置的 `env` 块
|
||||
- 有必要时,在 server 定义里显式设置 `cwd`
|
||||
|
||||
### Session lock 被另一个终端中的 `/gsd` 抢走
|
||||
|
||||
**症状:** 在第二个终端运行 `/gsd`(step mode)时,正在运行的自动模式会话失去了锁。
|
||||
|
||||
**解决:** 已在 v2.36.0 修复。现在裸 `/gsd` 不会再从运行中的自动模式会话手里抢 session lock。升级到最新版本。
|
||||
|
||||
### Worktree 中的提交落到了 main,而不是 `milestone/<MID>` 分支
|
||||
|
||||
**症状:** 自动模式在 worktree 中提交时,最终落在了 `main`,而不是 `milestone/<MID>`。
|
||||
|
||||
**解决:** 已在 v2.37.1 修复。现在 dispatch 前会重新校正 CWD,并在失败时清理过期 merge 状态。升级到最新版本。
|
||||
|
||||
### Extension loader 因 subpath export 错误而失败
|
||||
|
||||
**症状:** 扩展加载时报 `Cannot find module`,并且错误信息引用了 npm subpath exports。
|
||||
|
||||
**原因:** Extension loader 中的动态导入过去无法解析 npm subpath exports(例如 `@pkg/foo/bar`)。
|
||||
|
||||
**解决:** 已在 v2.38+ 修复。现在 extension loader 会自动解析 npm subpath exports,并为动态导入创建 `node_modules` symlink。升级到最新版本。
|
||||
|
||||
## 恢复流程
|
||||
|
||||
### 重置自动模式状态
|
||||
|
||||
```bash
|
||||
rm .gsd/auto.lock
|
||||
rm .gsd/completed-units.json
|
||||
```
|
||||
|
||||
然后执行 `/gsd auto`,从当前磁盘状态重新开始。
|
||||
|
||||
### 重置路由历史
|
||||
|
||||
如果自适应模型路由给出了糟糕的结果,可以清空路由历史:
|
||||
|
||||
```bash
|
||||
rm .gsd/routing-history.json
|
||||
```
|
||||
|
||||
### 完整重建状态
|
||||
|
||||
```
|
||||
/gsd doctor
|
||||
```
|
||||
|
||||
Doctor 会从磁盘上的 plan 和 roadmap 文件重建 `STATE.md`,并修复检测到的不一致项。
|
||||
|
||||
## 获取帮助
|
||||
|
||||
- **GitHub Issues:** [github.com/gsd-build/GSD-2/issues](https://github.com/gsd-build/GSD-2/issues)
|
||||
- **Dashboard:** `Ctrl+Alt+G` 或 `/gsd status`,查看实时诊断信息
|
||||
- **Forensics:** `/gsd forensics`,用于对自动模式失败做结构化事后分析
|
||||
- **Session logs:** `.gsd/activity/` 中包含用于崩溃取证的 JSONL 会话转储
|
||||
|
||||
## iTerm2 专属问题
|
||||
|
||||
### Ctrl+Alt 快捷键触发了错误动作(例如 Ctrl+Alt+G 打开了外部编辑器,而不是 GSD dashboard)
|
||||
|
||||
**症状:** 按下 Ctrl+Alt+G 后,会触发外部编辑器提示(Ctrl+G),而不是 GSD dashboard。其它 Ctrl+Alt 快捷键也表现得像它们对应的 Ctrl-only 快捷键。
|
||||
|
||||
**原因:** iTerm2 默认的 Left Option Key 设置是 “Normal”,这会吞掉 Ctrl+Alt 组合中的 Alt 修饰键。终端实际只收到了 Ctrl,所以 Ctrl+Alt+G 最终变成 Ctrl+G。
|
||||
|
||||
**解决:** 在 iTerm2 中进入 **Profiles → Keys → General**,把 **Left Option Key** 改成 **Esc+**。这样 Alt / Option 会发送 escape 前缀,终端应用就能正确识别 Ctrl+Alt 快捷键。
|
||||
|
||||
## Windows 专属问题
|
||||
|
||||
### Windows 上 LSP 返回 ENOENT(MSYS2 / Git Bash)
|
||||
|
||||
**症状:** LSP 初始化因 `ENOENT` 失败,或者把 `/c/Users/...` 这类 POSIX 路径错误地解析为 `C:\Users\...`。
|
||||
|
||||
**原因:** MSYS2 / Git Bash 中的 `which` 命令返回的是 POSIX 风格路径,而 Node.js 的 `spawn()` 无法正确解析。
|
||||
|
||||
**解决:** 已在 v2.29+ 修复,Windows 现在改用 `where.exe`。升级到最新版本。
|
||||
|
||||
### 构建 WXT / 浏览器扩展时出现 EBUSY
|
||||
|
||||
**症状:** 构建浏览器扩展时出现 `EBUSY: resource busy or locked, rmdir .output/chrome-mv3`。
|
||||
|
||||
**原因:** Chromium 浏览器仍然从构建输出目录加载着该扩展,导致目录无法删除。
|
||||
|
||||
**解决:** 关闭浏览器中的该扩展,或者在 WXT 配置里使用不同的 `outDirTemplate`,避开被锁住的目录。
|
||||
|
||||
## 数据库问题
|
||||
|
||||
### “GSD database is not available”
|
||||
|
||||
**症状:** `gsd_decision_save`(及其别名 `gsd_save_decision`)、`gsd_requirement_update`(及其别名 `gsd_update_requirement`)或 `gsd_summary_save`(及其别名 `gsd_save_summary`)报这个错误。
|
||||
|
||||
**原因:** SQLite 数据库未初始化。这个问题会出现在 v2.29 之前的手动 `/gsd` 会话(非自动模式)中。
|
||||
|
||||
**解决:** 已在 v2.29+ 修复。现在数据库会在第一次 tool call 时自动初始化。升级到最新版本。
|
||||
|
||||
## Verification 问题
|
||||
|
||||
### Verification gate 因 shell 语法错误失败
|
||||
|
||||
**症状:** 在 verification 阶段出现 `stderr: /bin/sh: 1: Syntax error: "(" unexpected`。
|
||||
|
||||
**原因:** 某个描述性字符串(例如 `All 10 checks pass (build, lint)`)被误当成 shell 命令执行。这通常发生在 task plans 的 `verify:` 字段里写了 prose,而不是实际命令。
|
||||
|
||||
**解决:** 已在 v2.29+ 修复,现在偏好命令会先通过 `isLikelyCommand()` 过滤。请确保偏好中的 `verification_commands` 只包含合法 shell 命令,而不是文字描述。
|
||||
|
||||
## LSP(Language Server Protocol)
|
||||
|
||||
### “LSP isn't available in this workspace”
|
||||
|
||||
GSD 会根据项目文件自动检测 language servers(例如 `package.json` → TypeScript、`Cargo.toml` → Rust、`go.mod` → Go)。如果没有检测到 server,agent 会跳过 LSP 功能。
|
||||
|
||||
**查看状态:**
|
||||
|
||||
```
|
||||
lsp status
|
||||
```
|
||||
|
||||
它会显示哪些 servers 已经激活;如果一个都没找到,也会说明原因,包括发现了哪些项目标记、但缺失了哪些 server 命令。
|
||||
|
||||
**常见修复方式:**
|
||||
|
||||
| 项目类型 | 安装命令 |
|
||||
|----------|----------|
|
||||
| TypeScript / JavaScript | `npm install -g typescript-language-server typescript` |
|
||||
| Python | `pip install pyright` 或 `pip install python-lsp-server` |
|
||||
| Rust | `rustup component add rust-analyzer` |
|
||||
| Go | `go install golang.org/x/tools/gopls@latest` |
|
||||
|
||||
安装完成后,执行 `lsp reload` 即可重新检测,无需重启 GSD。
|
||||
|
||||
## Notifications
|
||||
|
||||
<a id="notifications-not-appearing-on-macos"></a>
|
||||
### macOS 上通知不显示
|
||||
|
||||
**症状:** 偏好中已设置 `notifications.enabled: true`,但自动模式期间没有任何桌面通知(没有 milestone 完成提示、预算预警或错误通知),同时日志里也没有报错。
|
||||
|
||||
**原因:** GSD 在 macOS 上会把 `osascript display notification` 作为回退方案。这个命令的通知归属你的终端应用(Ghostty、iTerm2、Alacritty、Kitty、Warp 等)。如果该终端应用在 System Settings → Notifications 中没有权限,macOS 会静默丢弃通知,而 `osascript` 仍然返回 0,不会报错。
|
||||
|
||||
很多终端应用只有在成功送出过至少一条通知后,才会出现在通知设置面板里,这就形成了“先能通知,系统才给你配置通知”的鸡生蛋蛋生鸡问题。
|
||||
|
||||
**推荐修复方式:** 安装 `terminal-notifier`,它会注册为独立的 Notification Center 应用:
|
||||
|
||||
```bash
|
||||
brew install terminal-notifier
|
||||
```
|
||||
|
||||
GSD 在检测到 `terminal-notifier` 可用时会自动优先使用它。首次使用时,macOS 会弹出通知权限请求,这是预期行为。
|
||||
|
||||
**替代修复方式:** 进入 **System Settings → Notifications**,为你的终端应用启用通知。如果终端应用不在列表中,可以先在 Terminal.app 中手动发送一条测试通知,注册出 “Script Editor”:
|
||||
|
||||
```bash
|
||||
osascript -e 'display notification "test" with title "GSD"'
|
||||
```
|
||||
|
||||
**验证:** 完成任一修复后,用下面命令测试:
|
||||
|
||||
```bash
|
||||
terminal-notifier -title "GSD" -message "working!" -sound Glass
|
||||
```
|
||||
104
docs/zh-CN/user-docs/visualizer.md
Normal file
104
docs/zh-CN/user-docs/visualizer.md
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# 工作流可视化器
|
||||
|
||||
*引入于 v2.19.0*
|
||||
|
||||
工作流可视化器是一个全屏 TUI 叠层视图,以交互式四标签页的形式展示项目进度、依赖关系、成本指标和执行时间线。
|
||||
|
||||
## 打开可视化器
|
||||
|
||||
```
|
||||
/gsd visualize
|
||||
```
|
||||
|
||||
或者配置为在 milestone 完成后自动显示:
|
||||
|
||||
```yaml
|
||||
auto_visualize: true
|
||||
```
|
||||
|
||||
## 标签页
|
||||
|
||||
可通过 `Tab`、`1`-`4` 或方向键切换标签页。
|
||||
|
||||
### 1. 进度
|
||||
|
||||
以树状视图展示 milestones、slices 和 tasks 的完成状态:
|
||||
|
||||
```
|
||||
M001: User Management 3/6 tasks ⏳
|
||||
✅ S01: Auth module 3/3 tasks
|
||||
✅ T01: Core types
|
||||
✅ T02: JWT middleware
|
||||
✅ T03: Login flow
|
||||
⏳ S02: User dashboard 1/2 tasks
|
||||
✅ T01: Layout component
|
||||
⬜ T02: Profile page
|
||||
⬜ S03: Admin panel 0/1 tasks
|
||||
```
|
||||
|
||||
已完成项显示勾选,进行中项显示转圈,待处理项显示空框。每一层级也会显示 task 数量和完成百分比。
|
||||
|
||||
如果某个 milestone 经过 discussion 阶段,还会显示**讨论状态**,用于表明需求是否已经记录,以及讨论停留在哪个状态。
|
||||
|
||||
### 2. 依赖
|
||||
|
||||
用 ASCII 依赖图展示 slices 之间的关系:
|
||||
|
||||
```
|
||||
S01 ──→ S02 ──→ S04
|
||||
└───→ S03 ──↗
|
||||
```
|
||||
|
||||
它会把 roadmap 中的 `depends:` 字段可视化出来,便于快速判断哪些 slices 被阻塞、哪些可以继续推进。
|
||||
|
||||
### 3. 指标
|
||||
|
||||
通过柱状图展示成本和 Token 使用情况:
|
||||
|
||||
- **按阶段**:research、planning、execution、completion、reassessment
|
||||
- **按 slice**:每个 slice 的成本以及累计总额
|
||||
- **按模型**:哪些模型消耗了最多预算
|
||||
|
||||
数据来自 `.gsd/metrics.json`。
|
||||
|
||||
### 4. 时间线
|
||||
|
||||
按时间顺序展示执行历史,包括:
|
||||
|
||||
- 单元类型和 ID
|
||||
- 开始 / 结束时间戳
|
||||
- 持续时间
|
||||
- 使用的模型
|
||||
- Token 数量
|
||||
|
||||
条目按执行时间排序,因此可以看到自动模式的完整派发历史。
|
||||
|
||||
## 控制
|
||||
|
||||
| 按键 | 动作 |
|
||||
|------|------|
|
||||
| `Tab` | 下一个标签页 |
|
||||
| `Shift+Tab` | 上一个标签页 |
|
||||
| `1`-`4` | 直接跳转到标签页 |
|
||||
| `↑` / `↓` | 在当前标签页内滚动 |
|
||||
| `Escape` / `q` | 关闭可视化器 |
|
||||
|
||||
## 自动刷新
|
||||
|
||||
可视化器每 2 秒从磁盘刷新一次数据,因此即使它和自动模式会话同时打开,也能保持最新状态。
|
||||
|
||||
## HTML 导出(v2.26)
|
||||
|
||||
如果需要在终端外部分享报告,可以使用 `/gsd export --html`。它会在 `.gsd/reports/` 中生成一个自包含的 HTML 文件,包含与 TUI 可视化器相同的数据:进度树、依赖图(SVG DAG)、成本 / Token 柱状图、执行时间线、变更日志和知识库。所有 CSS 和 JS 都会内联,无外部依赖,也可以在任意浏览器中打印为 PDF。
|
||||
|
||||
自动生成的 `index.html` 会集中列出所有报告,并显示跨 milestones 的推进指标。
|
||||
|
||||
```yaml
|
||||
auto_report: true # 在 milestone 完成后自动生成(默认开启)
|
||||
```
|
||||
|
||||
## 配置
|
||||
|
||||
```yaml
|
||||
auto_visualize: true # 在 milestone 完成后显示可视化器
|
||||
```
|
||||
67
docs/zh-CN/user-docs/web-interface.md
Normal file
67
docs/zh-CN/user-docs/web-interface.md
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# Web 界面
|
||||
|
||||
> 新增于 v2.41.0
|
||||
|
||||
GSD 提供了基于浏览器的 Web 界面,用于项目管理、实时进度监控以及多项目支持。
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
gsd --web
|
||||
```
|
||||
|
||||
这会启动一个本地 Web 服务器,并在默认浏览器中打开 GSD 仪表板。
|
||||
|
||||
### CLI 参数(v2.42.0)
|
||||
|
||||
```bash
|
||||
gsd --web --host 0.0.0.0 --port 8080 --allowed-origins "https://example.com"
|
||||
```
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `--host` | `localhost` | Web 服务器监听地址 |
|
||||
| `--port` | `3000` | Web 服务器端口 |
|
||||
| `--allowed-origins` | (无) | 允许的 CORS 来源列表,逗号分隔 |
|
||||
|
||||
## 功能
|
||||
|
||||
- **项目管理**:在可视化仪表板中查看 milestones、slices 和 tasks
|
||||
- **实时进度**:通过 server-sent events 在自动模式执行期间推送状态更新
|
||||
- **多项目支持**:通过 `?project=` URL 参数,在单个浏览器标签页中管理多个项目
|
||||
- **切换项目根目录**:无需重启服务器即可在 Web UI 中切换项目目录(v2.44)
|
||||
- **首次引导流程**:可在浏览器中完成 API key 设置和 provider 配置
|
||||
- **模型选择**:直接从 Web UI 切换模型和 provider
|
||||
|
||||
## 架构
|
||||
|
||||
Web 界面基于 Next.js 构建,并通过桥接服务与 GSD 后端通信。每个项目都会拥有自己的 bridge 实例,以便在并发会话中保持隔离。
|
||||
|
||||
关键组件:
|
||||
|
||||
- `ProjectBridgeService`:按项目分配的命令路由和 SSE 订阅服务
|
||||
- `getProjectBridgeServiceForCwd()`:根据项目路径返回独立实例的注册表
|
||||
- `resolveProjectCwd()`:从请求 URL 中读取 `?project=`,若不存在则回退到 `GSD_WEB_PROJECT_CWD`
|
||||
|
||||
## 配置
|
||||
|
||||
默认情况下,Web 服务器监听在 `localhost:3000`。如需覆盖,可使用 `--host`、`--port` 和 `--allowed-origins`(见上面的 CLI 参数)。
|
||||
|
||||
### 环境变量
|
||||
|
||||
| 变量 | 说明 |
|
||||
|------|------|
|
||||
| `GSD_WEB_PROJECT_CWD` | 当未指定 `?project=` 时使用的默认项目路径 |
|
||||
|
||||
## Node v24 兼容性
|
||||
|
||||
Node v24 对类型剥离(type stripping)做了破坏性改动,曾导致 Web 启动时报 `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`。该问题已在 v2.42.0+ 中修复(#1864)。如果你仍然遇到这个错误,请升级 GSD。
|
||||
|
||||
## 认证令牌持久化
|
||||
|
||||
从 v2.42.0 起,Web UI 会把认证令牌持久化到 `sessionStorage`,因此页面刷新后不会丢失登录态(#1877)。在此之前,每次刷新都需要重新认证。
|
||||
|
||||
## 平台说明
|
||||
|
||||
- **Windows**:由于 Next.js webpack 在系统目录上会触发 EPERM 问题,Windows 下会跳过 Web 构建。CLI 仍然可完整使用。
|
||||
- **macOS / Linux**:完整支持。
|
||||
103
docs/zh-CN/user-docs/working-in-teams.md
Normal file
103
docs/zh-CN/user-docs/working-in-teams.md
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
# 团队协作
|
||||
|
||||
GSD 支持多人并行工作流,让多个开发者可以同时在同一个仓库中工作。
|
||||
|
||||
## 设置
|
||||
|
||||
### 1. 启用 Team Mode
|
||||
|
||||
为团队使用配置 GSD 的最简单方法,是在项目偏好中设置 `mode: team`。这会一次性开启唯一 milestone ID、推送分支和预合并检查:
|
||||
|
||||
```yaml
|
||||
# .gsd/PREFERENCES.md(项目级,提交到 git)
|
||||
---
|
||||
version: 1
|
||||
mode: team
|
||||
---
|
||||
```
|
||||
|
||||
这相当于手动设置 `unique_milestone_ids: true`、`git.push_branches: true`、`git.pre_merge_check: true` 以及其他适合团队协作的默认值。你仍然可以覆盖单个选项,例如如果团队偏好自动推送,也可以在 `mode: team` 基础上再加 `git.auto_push: true`。
|
||||
|
||||
你也可以不使用 mode,而是单独配置每一项设置(详见 [Git 策略](git-strategy.md))。
|
||||
|
||||
### 2. 配置 `.gitignore`
|
||||
|
||||
共享规划产物(milestones、roadmaps、decisions),同时把运行时文件保留在本地:
|
||||
|
||||
```bash
|
||||
# ── GSD:运行时 / 临时文件(按开发者、按会话隔离)──────
|
||||
.gsd/auto.lock
|
||||
.gsd/completed-units.json
|
||||
.gsd/STATE.md
|
||||
.gsd/metrics.json
|
||||
.gsd/activity/
|
||||
.gsd/runtime/
|
||||
.gsd/worktrees/
|
||||
.gsd/milestones/**/continue.md
|
||||
.gsd/milestones/**/*-CONTINUE.md
|
||||
```
|
||||
|
||||
**会共享的内容**(提交到 git):
|
||||
|
||||
- `.gsd/PREFERENCES.md`:项目偏好
|
||||
- `.gsd/PROJECT.md`:持续维护的项目描述
|
||||
- `.gsd/REQUIREMENTS.md`:需求契约
|
||||
- `.gsd/DECISIONS.md`:架构决策
|
||||
- `.gsd/milestones/`:roadmaps、plans、summaries 和 research
|
||||
|
||||
**仅保留本地的内容**(gitignore):
|
||||
|
||||
- 锁文件、指标、状态缓存、运行时记录、worktrees、活动日志
|
||||
|
||||
### 3. 提交偏好设置
|
||||
|
||||
```bash
|
||||
git add .gsd/PREFERENCES.md
|
||||
git commit -m "chore: enable GSD team workflow"
|
||||
```
|
||||
|
||||
## `commit_docs: false`
|
||||
|
||||
如果团队里只有部分成员使用 GSD,或者公司策略要求仓库保持干净:
|
||||
|
||||
```yaml
|
||||
git:
|
||||
commit_docs: false
|
||||
```
|
||||
|
||||
这会把整个 `.gsd/` 加入 `.gitignore`,让所有产物都保留在本地。这样使用 GSD 的开发者仍然能获得结构化规划的好处,而不会影响不使用 GSD 的同事。
|
||||
|
||||
## 迁移现有项目
|
||||
|
||||
如果你当前项目里对 `.gsd/` 做了整目录忽略:
|
||||
|
||||
1. 确保当前没有进行中的 milestones(工作区状态干净)
|
||||
2. 按上面的选择性规则更新 `.gitignore`
|
||||
3. 在 `.gsd/PREFERENCES.md` 中添加 `unique_milestone_ids: true`
|
||||
4. 如有需要,重命名现有 milestones 以使用唯一 ID:
|
||||
```
|
||||
I have turned on unique milestone ids, please update all old milestone
|
||||
ids to use this new format e.g. M001-abc123 where abc123 is a random
|
||||
6 char lowercase alpha numeric string. Update all references in all
|
||||
.gsd file contents, file names and directory names. Validate your work
|
||||
once done to ensure referential integrity.
|
||||
```
|
||||
5. 提交修改
|
||||
|
||||
## 并行开发
|
||||
|
||||
多个开发者可以同时对不同 milestones 运行自动模式。每个开发者都会:
|
||||
|
||||
- 获得自己的 worktree(`.gsd/worktrees/<MID>/`,已加入 gitignore)
|
||||
- 在独立的 `milestone/<MID>` 分支上工作
|
||||
- 独立地 squash merge 回主分支
|
||||
|
||||
milestone 依赖可以通过 `M00X-CONTEXT.md` frontmatter 声明:
|
||||
|
||||
```yaml
|
||||
---
|
||||
depends_on: [M001-eh88as]
|
||||
---
|
||||
```
|
||||
|
||||
GSD 会强制要求上游依赖 milestone 先完成,之后才会启动下游工作。
|
||||
65
gitbook/README.md
Normal file
65
gitbook/README.md
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# What is GSD?
|
||||
|
||||
GSD is an AI-powered development agent that turns project ideas into working software. Describe what you want to build, and GSD researches, plans, codes, tests, and commits — with clean git history and full cost tracking.
|
||||
|
||||
## How It Works
|
||||
|
||||
GSD breaks your project into manageable pieces and works through them systematically:
|
||||
|
||||
```
|
||||
You describe your project
|
||||
↓
|
||||
GSD creates a milestone with slices (features)
|
||||
↓
|
||||
Each slice is decomposed into tasks
|
||||
↓
|
||||
Tasks are executed one at a time in fresh AI sessions
|
||||
↓
|
||||
Code is committed, verified, and the next task begins
|
||||
```
|
||||
|
||||
You can stay hands-on with **step mode** (reviewing each step) or let GSD run autonomously with **auto mode** while you grab coffee.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Autonomous execution** — `/gsd auto` runs research, planning, coding, testing, and committing without intervention
|
||||
- **20+ LLM providers** — Anthropic, OpenAI, Google, OpenRouter, GitHub Copilot, Amazon Bedrock, local models, and more
|
||||
- **Git isolation** — Each milestone works in its own worktree branch, merged cleanly when done
|
||||
- **Cost tracking** — Real-time token usage, budget ceilings, and automatic model downgrading
|
||||
- **Crash recovery** — Sessions resume automatically after interruptions
|
||||
- **Skills system** — Domain-specific instruction sets for frameworks, languages, and tools
|
||||
- **Parallel milestones** — Run multiple milestones simultaneously in isolated worktrees
|
||||
- **Remote questions** — Get Discord, Slack, or Telegram notifications when GSD needs input
|
||||
- **Web interface** — Browser-based dashboard with real-time progress
|
||||
- **VS Code extension** — Chat participant, sidebar dashboard, and full command palette
|
||||
- **Headless mode** — Run in CI pipelines, cron jobs, and scripted automation
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Install
|
||||
npm install -g gsd-pi
|
||||
|
||||
# Launch
|
||||
gsd
|
||||
|
||||
# Start autonomous mode
|
||||
/gsd auto
|
||||
```
|
||||
|
||||
See [Installation](getting-started/installation.md) for detailed setup instructions.
|
||||
|
||||
## Two Ways to Work
|
||||
|
||||
| Mode | Command | Best For |
|
||||
|------|---------|----------|
|
||||
| **Step** | `/gsd` | Staying in the loop, reviewing each step |
|
||||
| **Auto** | `/gsd auto` | Walking away, overnight builds, batch work |
|
||||
|
||||
The recommended workflow: run auto mode in one terminal, steer from another. See [Step Mode](core-concepts/step-mode.md) and [Auto Mode](core-concepts/auto-mode.md).
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Node.js** 22.0.0 or later (24 LTS recommended)
|
||||
- **Git** installed and configured
|
||||
- An API key for at least one LLM provider (or use browser sign-in for Anthropic/GitHub Copilot)
|
||||
49
gitbook/SUMMARY.md
Normal file
49
gitbook/SUMMARY.md
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# Table of contents
|
||||
|
||||
* [What is GSD?](README.md)
|
||||
|
||||
## Getting Started
|
||||
|
||||
* [Installation](getting-started/installation.md)
|
||||
* [Your First Project](getting-started/first-project.md)
|
||||
* [Choosing a Model](getting-started/choosing-a-model.md)
|
||||
|
||||
## Core Concepts
|
||||
|
||||
* [How GSD Organizes Work](core-concepts/project-structure.md)
|
||||
* [Step Mode](core-concepts/step-mode.md)
|
||||
* [Auto Mode](core-concepts/auto-mode.md)
|
||||
|
||||
## Configuration
|
||||
|
||||
* [Preferences](configuration/preferences.md)
|
||||
* [Provider Setup](configuration/providers.md)
|
||||
* [Custom Models](configuration/custom-models.md)
|
||||
* [Git & Worktrees](configuration/git-settings.md)
|
||||
* [Notifications](configuration/notifications.md)
|
||||
* [MCP Servers](configuration/mcp-servers.md)
|
||||
|
||||
## Features
|
||||
|
||||
* [Cost Management](features/cost-management.md)
|
||||
* [Token Optimization](features/token-optimization.md)
|
||||
* [Dynamic Model Routing](features/dynamic-model-routing.md)
|
||||
* [Skills](features/skills.md)
|
||||
* [Captures & Triage](features/captures.md)
|
||||
* [Workflow Visualizer](features/visualizer.md)
|
||||
* [Workflow Templates](features/workflow-templates.md)
|
||||
* [Web Interface](features/web-interface.md)
|
||||
* [Remote Questions](features/remote-questions.md)
|
||||
* [Working in Teams](features/teams.md)
|
||||
* [Parallel Orchestration](features/parallel.md)
|
||||
* [Headless & CI Mode](features/headless.md)
|
||||
* [GitHub Sync](features/github-sync.md)
|
||||
|
||||
## Reference
|
||||
|
||||
* [Commands](reference/commands.md)
|
||||
* [Keyboard Shortcuts](reference/keyboard-shortcuts.md)
|
||||
* [CLI Flags](reference/cli-flags.md)
|
||||
* [Environment Variables](reference/environment-variables.md)
|
||||
* [Troubleshooting](reference/troubleshooting.md)
|
||||
* [Migration from v1](reference/migration.md)
|
||||
131
gitbook/configuration/custom-models.md
Normal file
131
gitbook/configuration/custom-models.md
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
# Custom Models
|
||||
|
||||
Define custom models and providers in `~/.gsd/agent/models.json`. This lets you add models not in the default registry — self-hosted endpoints, fine-tuned models, proxies, or new provider releases.
|
||||
|
||||
## File Location
|
||||
|
||||
GSD looks for models.json at:
|
||||
1. `~/.gsd/agent/models.json` (primary)
|
||||
2. `~/.pi/agent/models.json` (fallback)
|
||||
|
||||
The file reloads each time you open `/model` — no restart needed.
|
||||
|
||||
## Basic Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"my-provider": {
|
||||
"baseUrl": "https://my-endpoint.example.com/v1",
|
||||
"apiKey": "MY_PROVIDER_API_KEY",
|
||||
"api": "openai-completions",
|
||||
"models": [
|
||||
{
|
||||
"id": "model-id-here",
|
||||
"name": "Friendly Model Name",
|
||||
"reasoning": false,
|
||||
"input": ["text"],
|
||||
"contextWindow": 128000,
|
||||
"maxTokens": 16384,
|
||||
"cost": { "input": 0.15, "output": 0.60, "cacheRead": 0.015, "cacheWrite": 0.19 }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Key Resolution
|
||||
|
||||
The `apiKey` field can be:
|
||||
|
||||
- **An environment variable name**: `"OPENROUTER_API_KEY"` — GSD resolves it automatically
|
||||
- **A literal value**: `"sk-abc123..."` — used directly
|
||||
- **A dummy value**: `"not-needed"` — for local servers that don't require auth
|
||||
|
||||
## Compatibility Flags
|
||||
|
||||
Local and non-standard servers often need compatibility adjustments:
|
||||
|
||||
```json
|
||||
{
|
||||
"compat": {
|
||||
"supportsDeveloperRole": false,
|
||||
"supportsReasoningEffort": false,
|
||||
"supportsUsageInStreaming": false,
|
||||
"thinkingFormat": "qwen"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Flag | Default | Purpose |
|
||||
|------|---------|---------|
|
||||
| `supportsDeveloperRole` | `true` | Set `false` if the server doesn't support the `developer` message role |
|
||||
| `supportsReasoningEffort` | `true` | Set `false` if the server doesn't support reasoning effort parameters |
|
||||
| `supportsUsageInStreaming` | `true` | Set `false` if streaming responses don't include token usage |
|
||||
| `thinkingFormat` | — | Set `"qwen"` for Qwen thinking mode, `"qwen-chat-template"` for chat template variant |
|
||||
|
||||
## Custom Headers
|
||||
|
||||
For proxies that need extra headers:
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"litellm-proxy": {
|
||||
"baseUrl": "https://litellm.example.com/v1",
|
||||
"apiKey": "MY_API_KEY",
|
||||
"api": "openai-completions",
|
||||
"headers": {
|
||||
"x-custom-header": "value"
|
||||
},
|
||||
"models": [...]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Model Overrides
|
||||
|
||||
Override specific model settings without redefining the entire model:
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"openrouter": {
|
||||
"modelOverrides": {
|
||||
"anthropic/claude-sonnet-4": {
|
||||
"compat": {
|
||||
"openRouterRouting": {
|
||||
"only": ["amazon-bedrock"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Cost Tracking
|
||||
|
||||
For accurate cost tracking with custom models, add the `cost` field (per million tokens):
|
||||
|
||||
```json
|
||||
"cost": {
|
||||
"input": 0.15,
|
||||
"output": 0.60,
|
||||
"cacheRead": 0.015,
|
||||
"cacheWrite": 0.19
|
||||
}
|
||||
```
|
||||
|
||||
Without this, cost shows $0.00 — which is the expected default for custom models.
|
||||
|
||||
## Community Extensions
|
||||
|
||||
For providers not built into GSD, community extensions add full provider support:
|
||||
|
||||
| Extension | Provider | Install |
|
||||
|-----------|----------|---------|
|
||||
| `pi-dashscope` | Alibaba DashScope (Qwen3, GLM-5, etc.) | `gsd install npm:pi-dashscope` |
|
||||
148
gitbook/configuration/git-settings.md
Normal file
148
gitbook/configuration/git-settings.md
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
# Git & Worktrees
|
||||
|
||||
GSD uses git for milestone isolation and sequential commits. The strategy is fully automated — you don't need to manage branches manually.
|
||||
|
||||
## Isolation Modes
|
||||
|
||||
GSD supports three isolation modes, configured via `git.isolation` in preferences:
|
||||
|
||||
| Mode | Working Directory | Branch | Best For |
|
||||
|------|-------------------|--------|----------|
|
||||
| `worktree` (default) | `.gsd/worktrees/<MID>/` | `milestone/<MID>` | Most projects — full isolation |
|
||||
| `branch` | Project root | `milestone/<MID>` | Submodule-heavy repos |
|
||||
| `none` | Project root | Current branch | Hot-reload workflows |
|
||||
|
||||
### Worktree Mode (Default)
|
||||
|
||||
Each milestone gets its own git worktree and branch. All execution happens inside the worktree. On completion, everything is squash-merged to main as one clean commit. The worktree and branch are then cleaned up.
|
||||
|
||||
Changes in a milestone can't interfere with your main working copy.
|
||||
|
||||
### Branch Mode
|
||||
|
||||
Work happens in the project root on a `milestone/<MID>` branch. No worktree directory is created. Useful when worktrees cause problems with submodules or hardcoded paths.
|
||||
|
||||
### None Mode
|
||||
|
||||
Work happens directly on your current branch. No worktree, no milestone branch. GSD still commits with conventional commit messages. Use this when file isolation breaks dev tooling (file watchers, hot-reload, etc.).
|
||||
|
||||
## Branching Model
|
||||
|
||||
```
|
||||
main ────────────────────────────────────────────
|
||||
│ ↑
|
||||
└── milestone/M001 (worktree) ─────────────┘
|
||||
commit: feat: core types
|
||||
commit: feat: markdown parser
|
||||
commit: feat: file writer
|
||||
→ squash-merged to main
|
||||
```
|
||||
|
||||
## Workflow Modes
|
||||
|
||||
Set `mode` for sensible defaults instead of configuring each setting individually:
|
||||
|
||||
```yaml
|
||||
mode: solo # personal projects
|
||||
mode: team # shared repos
|
||||
```
|
||||
|
||||
| Setting | `solo` | `team` |
|
||||
|---------|--------|--------|
|
||||
| `git.auto_push` | `true` | `false` |
|
||||
| `git.push_branches` | `false` | `true` |
|
||||
| `git.pre_merge_check` | `false` | `true` |
|
||||
| `unique_milestone_ids` | `false` | `true` |
|
||||
|
||||
Mode defaults are the lowest priority — any explicit preference overrides them.
|
||||
|
||||
## Git Preferences
|
||||
|
||||
```yaml
|
||||
git:
|
||||
auto_push: false # push after commits
|
||||
push_branches: false # push milestone branch to remote
|
||||
remote: origin # git remote name
|
||||
snapshots: true # WIP snapshot commits during long tasks
|
||||
pre_merge_check: auto # validation before merge
|
||||
commit_type: feat # override conventional commit prefix
|
||||
main_branch: main # primary branch name
|
||||
merge_strategy: squash # "squash" or "merge"
|
||||
isolation: worktree # "worktree", "branch", or "none"
|
||||
commit_docs: true # commit .gsd/ artifacts to git
|
||||
manage_gitignore: true # let GSD manage .gitignore
|
||||
auto_pr: false # create PR on milestone completion
|
||||
pr_target_branch: develop # PR target branch
|
||||
```
|
||||
|
||||
## Automatic Pull Requests
|
||||
|
||||
For teams using Gitflow or branch-based workflows:
|
||||
|
||||
```yaml
|
||||
git:
|
||||
auto_push: true
|
||||
auto_pr: true
|
||||
pr_target_branch: develop
|
||||
```
|
||||
|
||||
When a milestone completes, GSD pushes the branch and creates a PR targeting your specified branch. Requires `gh` CLI installed and authenticated.
|
||||
|
||||
## Post-Worktree Hook
|
||||
|
||||
Run a script after worktree creation (copy `.env` files, symlink assets, etc.):
|
||||
|
||||
```yaml
|
||||
git:
|
||||
worktree_post_create: .gsd/hooks/post-worktree-create
|
||||
```
|
||||
|
||||
Example hook:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
cp "$SOURCE_DIR/.env" "$WORKTREE_DIR/.env"
|
||||
ln -sf "$SOURCE_DIR/assets" "$WORKTREE_DIR/assets"
|
||||
```
|
||||
|
||||
## Keeping `.gsd/` Local
|
||||
|
||||
For teams where only some members use GSD:
|
||||
|
||||
```yaml
|
||||
git:
|
||||
commit_docs: false
|
||||
```
|
||||
|
||||
This adds `.gsd/` to `.gitignore` entirely. You get structured planning without affecting teammates who don't use GSD.
|
||||
|
||||
## Commit Format
|
||||
|
||||
Commits use conventional commit format with GSD metadata:
|
||||
|
||||
```
|
||||
feat: core type definitions
|
||||
|
||||
GSD-Task: M001/S01/T01
|
||||
```
|
||||
|
||||
## Manual Worktree Management
|
||||
|
||||
Use `/worktree` (or `/wt`) for manual worktree operations:
|
||||
|
||||
```
|
||||
/worktree create
|
||||
/worktree switch
|
||||
/worktree merge
|
||||
/worktree remove
|
||||
```
|
||||
|
||||
## Self-Healing
|
||||
|
||||
GSD automatically recovers from common git issues:
|
||||
|
||||
- **Detached HEAD** — reattaches to the correct branch
|
||||
- **Stale lock files** — removes `index.lock` from crashed processes
|
||||
- **Orphaned worktrees** — detects and cleans up abandoned worktrees
|
||||
|
||||
Run `/gsd doctor` to check git health manually.
|
||||
65
gitbook/configuration/mcp-servers.md
Normal file
65
gitbook/configuration/mcp-servers.md
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# MCP Servers
|
||||
|
||||
GSD can connect to external MCP (Model Context Protocol) servers for local tools, internal APIs, self-hosted services, or integrations not built in as native extensions.
|
||||
|
||||
## Configuration Files
|
||||
|
||||
GSD reads MCP config from these project-local paths:
|
||||
|
||||
- `.mcp.json` — repo-shared config (safe to commit)
|
||||
- `.gsd/mcp.json` — local-only config (not shared)
|
||||
|
||||
If both exist, server names are merged and the first definition found wins.
|
||||
|
||||
## Supported Transports
|
||||
|
||||
| Transport | Config Shape | Use When |
|
||||
|-----------|-------------|----------|
|
||||
| `stdio` | `command` + optional `args`, `env`, `cwd` | Launching a local MCP server |
|
||||
| `http` | `url` | Connecting to an already-running server |
|
||||
|
||||
## Examples
|
||||
|
||||
### stdio Server
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"my-server": {
|
||||
"type": "stdio",
|
||||
"command": "/absolute/path/to/python3",
|
||||
"args": ["/absolute/path/to/server.py"],
|
||||
"env": {
|
||||
"API_URL": "http://localhost:8000"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### HTTP Server
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"my-http-server": {
|
||||
"url": "http://localhost:8080/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Verifying a Server
|
||||
|
||||
After adding config, verify from a GSD session:
|
||||
|
||||
1. `mcp_servers` — confirms GSD sees the config
|
||||
2. `mcp_discover(server="my-server")` — confirms the server starts and responds
|
||||
3. `mcp_call(server="my-server", tool="<tool>", args={...})` — confirms a real tool call works
|
||||
|
||||
## Tips
|
||||
|
||||
- Use **absolute paths** for executables and scripts
|
||||
- Set required **environment variables** directly in the MCP config's `env` block
|
||||
- Use `.mcp.json` for team-shared servers; `.gsd/mcp.json` for machine-local ones
|
||||
- If a server depends on local paths or personal secrets, keep it in `.gsd/mcp.json`
|
||||
38
gitbook/configuration/notifications.md
Normal file
38
gitbook/configuration/notifications.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# Notifications
|
||||
|
||||
GSD sends desktop notifications during auto mode to keep you informed without watching the terminal.
|
||||
|
||||
## Configuration
|
||||
|
||||
```yaml
|
||||
notifications:
|
||||
enabled: true
|
||||
on_complete: true # notify on unit completion
|
||||
on_error: true # notify on errors
|
||||
on_budget: true # notify on budget thresholds
|
||||
on_milestone: true # notify when milestone finishes
|
||||
on_attention: true # notify when manual attention needed
|
||||
```
|
||||
|
||||
## macOS Setup
|
||||
|
||||
GSD uses `terminal-notifier` when available, falling back to `osascript`.
|
||||
|
||||
**Recommended:** Install `terminal-notifier` for reliable delivery:
|
||||
|
||||
```bash
|
||||
brew install terminal-notifier
|
||||
```
|
||||
|
||||
**Why?** The `osascript` fallback attributes notifications to your terminal app (Ghostty, iTerm2, etc.), which may not have notification permissions. `terminal-notifier` registers as its own app and prompts for permission on first use.
|
||||
|
||||
### Notifications Not Appearing?
|
||||
|
||||
1. Check **System Settings → Notifications** for your terminal app
|
||||
2. Install `terminal-notifier` (recommended)
|
||||
3. Test with:
|
||||
```bash
|
||||
terminal-notifier -title "GSD" -message "working!" -sound Glass
|
||||
```
|
||||
|
||||
If your terminal app doesn't appear in Notification settings, it may need to send at least one notification first to register. See [Troubleshooting](../reference/troubleshooting.md) for more details.
|
||||
238
gitbook/configuration/preferences.md
Normal file
238
gitbook/configuration/preferences.md
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
# Preferences
|
||||
|
||||
GSD preferences live in YAML frontmatter markdown files. You can configure them globally or per-project.
|
||||
|
||||
## Managing Preferences
|
||||
|
||||
```
|
||||
/gsd prefs # open the global preferences wizard
|
||||
/gsd prefs project # open the project preferences wizard
|
||||
/gsd prefs status # show current values and where they come from
|
||||
```
|
||||
|
||||
## Preference Files
|
||||
|
||||
| Scope | Path | Applies To |
|
||||
|-------|------|-----------|
|
||||
| Global | `~/.gsd/PREFERENCES.md` | All projects |
|
||||
| Project | `.gsd/PREFERENCES.md` | Current project only |
|
||||
|
||||
**How they merge:**
|
||||
- **Scalar fields** (`budget_ceiling`, `token_profile`): project wins if defined
|
||||
- **Array fields** (`always_use_skills`, etc.): concatenated (global first, then project)
|
||||
- **Object fields** (`models`, `git`, `auto_supervisor`): shallow-merged, project overrides per-key
|
||||
|
||||
## Quick Example
|
||||
|
||||
```yaml
|
||||
---
|
||||
version: 1
|
||||
|
||||
# Model selection
|
||||
models:
|
||||
research: claude-sonnet-4-6
|
||||
planning: claude-opus-4-6
|
||||
execution: claude-sonnet-4-6
|
||||
completion: claude-sonnet-4-6
|
||||
|
||||
# Token optimization
|
||||
token_profile: balanced
|
||||
|
||||
# Budget
|
||||
budget_ceiling: 25.00
|
||||
budget_enforcement: pause
|
||||
|
||||
# Supervision
|
||||
auto_supervisor:
|
||||
soft_timeout_minutes: 15
|
||||
hard_timeout_minutes: 25
|
||||
|
||||
# Git
|
||||
git:
|
||||
auto_push: true
|
||||
merge_strategy: squash
|
||||
isolation: worktree
|
||||
|
||||
# Verification
|
||||
verification_commands:
|
||||
- npm run lint
|
||||
- npm run test
|
||||
|
||||
# Notifications
|
||||
notifications:
|
||||
on_milestone: true
|
||||
on_attention: true
|
||||
---
|
||||
```
|
||||
|
||||
## All Settings
|
||||
|
||||
### `models`
|
||||
|
||||
Per-phase model selection. See [Choosing a Model](../getting-started/choosing-a-model.md).
|
||||
|
||||
```yaml
|
||||
models:
|
||||
research: claude-sonnet-4-6
|
||||
planning:
|
||||
model: claude-opus-4-6
|
||||
fallbacks:
|
||||
- openrouter/z-ai/glm-5
|
||||
execution: claude-sonnet-4-6
|
||||
execution_simple: claude-haiku-4-5
|
||||
completion: claude-sonnet-4-6
|
||||
subagent: claude-sonnet-4-6
|
||||
```
|
||||
|
||||
### `token_profile`
|
||||
|
||||
Coordinates model selection, phase skipping, and context compression. Values: `budget`, `balanced` (default), `quality`. See [Token Optimization](../features/token-optimization.md).
|
||||
|
||||
### `budget_ceiling`
|
||||
|
||||
Maximum USD to spend during auto mode:
|
||||
|
||||
```yaml
|
||||
budget_ceiling: 50.00
|
||||
```
|
||||
|
||||
### `budget_enforcement`
|
||||
|
||||
What happens when the ceiling is reached:
|
||||
|
||||
| Value | Behavior |
|
||||
|-------|----------|
|
||||
| `warn` | Log a warning, continue |
|
||||
| `pause` | Pause auto mode (default) |
|
||||
| `halt` | Stop auto mode entirely |
|
||||
|
||||
### `auto_supervisor`
|
||||
|
||||
Timeout thresholds for auto mode:
|
||||
|
||||
```yaml
|
||||
auto_supervisor:
|
||||
soft_timeout_minutes: 20 # warn AI to wrap up
|
||||
idle_timeout_minutes: 10 # detect stalls
|
||||
hard_timeout_minutes: 30 # pause auto mode
|
||||
```
|
||||
|
||||
### `verification_commands`
|
||||
|
||||
Shell commands that run after every task execution:
|
||||
|
||||
```yaml
|
||||
verification_commands:
|
||||
- npm run lint
|
||||
- npm run test
|
||||
verification_auto_fix: true # auto-retry on failure (default)
|
||||
verification_max_retries: 2 # max attempts (default: 2)
|
||||
```
|
||||
|
||||
### `phases`
|
||||
|
||||
Fine-grained control over which phases run:
|
||||
|
||||
```yaml
|
||||
phases:
|
||||
skip_research: false
|
||||
skip_reassess: false
|
||||
skip_slice_research: true
|
||||
reassess_after_slice: true
|
||||
require_slice_discussion: false
|
||||
```
|
||||
|
||||
### `skill_discovery`
|
||||
|
||||
| Value | Behavior |
|
||||
|-------|----------|
|
||||
| `auto` | Skills found and applied automatically |
|
||||
| `suggest` | Skills identified but not auto-applied (default) |
|
||||
| `off` | Skill discovery disabled |
|
||||
|
||||
### `dynamic_routing`
|
||||
|
||||
Automatic model selection by task complexity. See [Dynamic Model Routing](../features/dynamic-model-routing.md).
|
||||
|
||||
```yaml
|
||||
dynamic_routing:
|
||||
enabled: true
|
||||
escalate_on_failure: true
|
||||
budget_pressure: true
|
||||
```
|
||||
|
||||
### `git`
|
||||
|
||||
Git behavior. See [Git & Worktrees](git-settings.md).
|
||||
|
||||
```yaml
|
||||
git:
|
||||
auto_push: false
|
||||
merge_strategy: squash
|
||||
isolation: worktree
|
||||
commit_docs: true
|
||||
auto_pr: false
|
||||
```
|
||||
|
||||
### `notifications`
|
||||
|
||||
See [Notifications](notifications.md).
|
||||
|
||||
```yaml
|
||||
notifications:
|
||||
enabled: true
|
||||
on_complete: true
|
||||
on_error: true
|
||||
on_milestone: true
|
||||
on_attention: true
|
||||
```
|
||||
|
||||
### `remote_questions`
|
||||
|
||||
Route questions to Slack, Discord, or Telegram. See [Remote Questions](../features/remote-questions.md).
|
||||
|
||||
```yaml
|
||||
remote_questions:
|
||||
channel: discord
|
||||
channel_id: "1234567890123456789"
|
||||
timeout_minutes: 5
|
||||
```
|
||||
|
||||
### `parallel`
|
||||
|
||||
Run multiple milestones simultaneously. See [Parallel Orchestration](../features/parallel.md).
|
||||
|
||||
```yaml
|
||||
parallel:
|
||||
enabled: false
|
||||
max_workers: 2
|
||||
budget_ceiling: 50.00
|
||||
```
|
||||
|
||||
### `custom_instructions`
|
||||
|
||||
Durable instructions appended to every session:
|
||||
|
||||
```yaml
|
||||
custom_instructions:
|
||||
- "Always use TypeScript strict mode"
|
||||
- "Prefer functional patterns over classes"
|
||||
```
|
||||
|
||||
For project-specific patterns, use `.gsd/KNOWLEDGE.md` instead — it's injected into every agent prompt automatically.
|
||||
|
||||
### `context_pause_threshold`
|
||||
|
||||
Context window usage percentage at which auto mode pauses:
|
||||
|
||||
```yaml
|
||||
context_pause_threshold: 80 # pause at 80%
|
||||
```
|
||||
|
||||
### `show_token_cost`
|
||||
|
||||
Show per-prompt and cumulative session token cost in the footer:
|
||||
|
||||
```yaml
|
||||
show_token_cost: true
|
||||
```
|
||||
277
gitbook/configuration/providers.md
Normal file
277
gitbook/configuration/providers.md
Normal file
|
|
@ -0,0 +1,277 @@
|
|||
# Provider Setup
|
||||
|
||||
Step-by-step setup instructions for every LLM provider GSD supports. If you ran the onboarding wizard (`gsd config`) and picked a provider, you may already be configured — check with `/model` inside a session.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Provider | Auth Method | Environment Variable |
|
||||
|----------|-------------|---------------------|
|
||||
| Anthropic | OAuth or API key | `ANTHROPIC_API_KEY` |
|
||||
| OpenAI | API key | `OPENAI_API_KEY` |
|
||||
| Google Gemini | API key | `GEMINI_API_KEY` |
|
||||
| OpenRouter | API key | `OPENROUTER_API_KEY` |
|
||||
| Groq | API key | `GROQ_API_KEY` |
|
||||
| xAI (Grok) | API key | `XAI_API_KEY` |
|
||||
| Mistral | API key | `MISTRAL_API_KEY` |
|
||||
| GitHub Copilot | OAuth | `GH_TOKEN` |
|
||||
| Amazon Bedrock | IAM credentials | `AWS_PROFILE` or `AWS_ACCESS_KEY_ID` |
|
||||
| Vertex AI | ADC | `GOOGLE_APPLICATION_CREDENTIALS` |
|
||||
| Azure OpenAI | API key | `AZURE_OPENAI_API_KEY` |
|
||||
| Ollama | None (local) | — |
|
||||
| LM Studio | None (local) | — |
|
||||
| vLLM / SGLang | None (local) | — |
|
||||
|
||||
## Built-in Providers
|
||||
|
||||
### Anthropic (Claude)
|
||||
|
||||
**Recommended.** Anthropic models have the deepest integration: built-in web search, extended thinking, and prompt caching.
|
||||
|
||||
**Option A — Browser sign-in (recommended):**
|
||||
|
||||
```bash
|
||||
gsd config
|
||||
# Choose "Sign in with your browser" → "Anthropic (Claude)"
|
||||
```
|
||||
|
||||
Or inside a session: `/login`
|
||||
|
||||
**Option B — API key:**
|
||||
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY="sk-ant-..."
|
||||
```
|
||||
|
||||
### OpenAI
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY="sk-..."
|
||||
```
|
||||
|
||||
Or run `gsd config` and choose "Paste an API key" then "OpenAI".
|
||||
|
||||
### Google Gemini
|
||||
|
||||
```bash
|
||||
export GEMINI_API_KEY="..."
|
||||
```
|
||||
|
||||
### OpenRouter
|
||||
|
||||
OpenRouter aggregates 200+ models from multiple providers behind a single API key.
|
||||
|
||||
1. Get a key at [openrouter.ai/keys](https://openrouter.ai/keys)
|
||||
2. Set it:
|
||||
```bash
|
||||
export OPENROUTER_API_KEY="sk-or-..."
|
||||
```
|
||||
3. In GSD, type `/model` to select an OpenRouter model (prefixed with `openrouter/`)
|
||||
|
||||
To add models not in the built-in list, add them to `~/.gsd/agent/models.json`. See [Custom Models](custom-models.md).
|
||||
|
||||
### Groq
|
||||
|
||||
```bash
|
||||
export GROQ_API_KEY="gsk_..."
|
||||
```
|
||||
|
||||
### xAI (Grok)
|
||||
|
||||
```bash
|
||||
export XAI_API_KEY="xai-..."
|
||||
```
|
||||
|
||||
### Mistral
|
||||
|
||||
```bash
|
||||
export MISTRAL_API_KEY="..."
|
||||
```
|
||||
|
||||
### GitHub Copilot
|
||||
|
||||
Uses OAuth — sign in through the browser:
|
||||
|
||||
```bash
|
||||
gsd config
|
||||
# Choose "Sign in with your browser" → "GitHub Copilot"
|
||||
```
|
||||
|
||||
Requires an active GitHub Copilot subscription.
|
||||
|
||||
### Amazon Bedrock
|
||||
|
||||
Bedrock uses AWS IAM credentials:
|
||||
|
||||
```bash
|
||||
# Named profile
|
||||
export AWS_PROFILE="my-profile"
|
||||
|
||||
# Or IAM keys
|
||||
export AWS_ACCESS_KEY_ID="AKIA..."
|
||||
export AWS_SECRET_ACCESS_KEY="..."
|
||||
export AWS_REGION="us-east-1"
|
||||
|
||||
# Or bearer token
|
||||
export AWS_BEARER_TOKEN_BEDROCK="..."
|
||||
```
|
||||
|
||||
ECS task roles and IRSA (Kubernetes) are also detected automatically.
|
||||
|
||||
### Anthropic on Vertex AI
|
||||
|
||||
```bash
|
||||
gcloud auth application-default login
|
||||
export ANTHROPIC_VERTEX_PROJECT_ID="my-project-id"
|
||||
```
|
||||
|
||||
### Azure OpenAI
|
||||
|
||||
```bash
|
||||
export AZURE_OPENAI_API_KEY="..."
|
||||
```
|
||||
|
||||
## Local Providers
|
||||
|
||||
Local providers run on your machine. They require a `models.json` configuration file at `~/.gsd/agent/models.json` because GSD needs to know the endpoint URL and available models.
|
||||
|
||||
The file reloads each time you open `/model` — no restart needed.
|
||||
|
||||
### Ollama
|
||||
|
||||
1. Install and start Ollama:
|
||||
```bash
|
||||
brew install ollama
|
||||
ollama serve
|
||||
```
|
||||
|
||||
2. Pull a model:
|
||||
```bash
|
||||
ollama pull llama3.1:8b
|
||||
```
|
||||
|
||||
3. Create `~/.gsd/agent/models.json`:
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"ollama": {
|
||||
"baseUrl": "http://localhost:11434/v1",
|
||||
"api": "openai-completions",
|
||||
"apiKey": "ollama",
|
||||
"compat": {
|
||||
"supportsDeveloperRole": false,
|
||||
"supportsReasoningEffort": false
|
||||
},
|
||||
"models": [
|
||||
{ "id": "llama3.1:8b" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. In GSD, type `/model` and select your Ollama model.
|
||||
|
||||
### LM Studio
|
||||
|
||||
1. Install [LM Studio](https://lmstudio.ai)
|
||||
2. Go to "Local Server" tab, load a model, click "Start Server" (default port 1234)
|
||||
3. Create `~/.gsd/agent/models.json`:
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"lm-studio": {
|
||||
"baseUrl": "http://localhost:1234/v1",
|
||||
"api": "openai-completions",
|
||||
"apiKey": "lm-studio",
|
||||
"compat": {
|
||||
"supportsDeveloperRole": false,
|
||||
"supportsReasoningEffort": false
|
||||
},
|
||||
"models": [
|
||||
{ "id": "your-model-name" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### vLLM
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"vllm": {
|
||||
"baseUrl": "http://localhost:8000/v1",
|
||||
"api": "openai-completions",
|
||||
"apiKey": "vllm",
|
||||
"compat": {
|
||||
"supportsDeveloperRole": false,
|
||||
"supportsReasoningEffort": false,
|
||||
"supportsUsageInStreaming": false
|
||||
},
|
||||
"models": [
|
||||
{ "id": "meta-llama/Llama-3.1-8B-Instruct" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### SGLang
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": {
|
||||
"sglang": {
|
||||
"baseUrl": "http://localhost:30000/v1",
|
||||
"api": "openai-completions",
|
||||
"apiKey": "sglang",
|
||||
"compat": {
|
||||
"supportsDeveloperRole": false,
|
||||
"supportsReasoningEffort": false
|
||||
},
|
||||
"models": [
|
||||
{ "id": "meta-llama/Llama-3.1-8B-Instruct" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Custom OpenAI-Compatible Endpoints
|
||||
|
||||
Any server that implements the OpenAI Chat Completions API can work with GSD — proxies (LiteLLM, Portkey, Helicone), self-hosted inference, new providers.
|
||||
|
||||
**Quickest path:**
|
||||
|
||||
```bash
|
||||
gsd config
|
||||
# Choose "Paste an API key" → "Custom (OpenAI-compatible)"
|
||||
# Enter: base URL, API key, model ID
|
||||
```
|
||||
|
||||
This writes `~/.gsd/agent/models.json` for you. See [Custom Models](custom-models.md) for manual setup.
|
||||
|
||||
## Verifying Your Setup
|
||||
|
||||
1. Launch GSD: `gsd`
|
||||
2. Check available models: `/model`
|
||||
3. Select your model from the picker
|
||||
4. Send a test message to confirm it responds
|
||||
|
||||
If the model doesn't appear, check:
|
||||
- The environment variable is set in the current shell
|
||||
- `models.json` is valid JSON
|
||||
- The server is running (for local providers)
|
||||
|
||||
## Common Issues
|
||||
|
||||
| Problem | Cause | Fix |
|
||||
|---------|-------|-----|
|
||||
| "Authentication failed" with valid key | Key not visible to GSD | Export in the same terminal, or save via `gsd config` |
|
||||
| OpenRouter models not in `/model` | No API key set | Set `OPENROUTER_API_KEY` and restart |
|
||||
| Ollama returns empty responses | Server not running or model not pulled | Run `ollama serve` and `ollama pull <model>` |
|
||||
| LM Studio model ID mismatch | ID doesn't match server | Check LM Studio's server tab for the exact identifier |
|
||||
| `developer` role error | Local server doesn't support it | Set `compat.supportsDeveloperRole: false` |
|
||||
| `stream_options` error | Server doesn't support streaming usage | Set `compat.supportsUsageInStreaming: false` |
|
||||
| Cost shows $0.00 | Default for custom models | Add `cost` field to model definition |
|
||||
183
gitbook/core-concepts/auto-mode.md
Normal file
183
gitbook/core-concepts/auto-mode.md
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
# Auto Mode
|
||||
|
||||
Auto mode is GSD's autonomous execution engine. Run `/gsd auto`, walk away, come back to built software with clean git history.
|
||||
|
||||
## Starting Auto Mode
|
||||
|
||||
```
|
||||
/gsd auto
|
||||
```
|
||||
|
||||
GSD reads `.gsd/STATE.md`, determines the next unit of work, creates a fresh AI session with all relevant context, and lets the AI execute. When it finishes, GSD reads disk state again and dispatches the next unit. This continues until the milestone is complete.
|
||||
|
||||
## The Execution Loop
|
||||
|
||||
Each slice flows through phases automatically:
|
||||
|
||||
```
|
||||
Plan → Execute (per task) → Complete → Reassess Roadmap → Next Slice
|
||||
↓ (all done)
|
||||
Validate Milestone
|
||||
```
|
||||
|
||||
- **Plan** — scouts the codebase, researches docs, decomposes the slice into tasks
|
||||
- **Execute** — runs each task in a fresh context window
|
||||
- **Complete** — writes summary, UAT script, marks roadmap, commits
|
||||
- **Reassess** — checks if the roadmap still makes sense after what was learned
|
||||
- **Validate** — after all slices, verifies success criteria were actually met
|
||||
|
||||
## Controlling Auto Mode
|
||||
|
||||
### Pause
|
||||
|
||||
Press **Escape**. The conversation is preserved. You can interact with the agent, inspect state, or resume.
|
||||
|
||||
### Resume
|
||||
|
||||
```
|
||||
/gsd auto
|
||||
```
|
||||
|
||||
Auto mode reads disk state and picks up where it left off.
|
||||
|
||||
### Stop
|
||||
|
||||
```
|
||||
/gsd stop
|
||||
```
|
||||
|
||||
Stops auto mode gracefully. Can be run from a different terminal.
|
||||
|
||||
### Steer
|
||||
|
||||
```
|
||||
/gsd steer
|
||||
```
|
||||
|
||||
Modify plan documents during execution without stopping. Changes are picked up at the next phase boundary.
|
||||
|
||||
### Capture Thoughts
|
||||
|
||||
```
|
||||
/gsd capture "add rate limiting to API endpoints"
|
||||
```
|
||||
|
||||
Fire-and-forget thought capture. Captures are triaged automatically between tasks without pausing execution. See [Captures & Triage](../features/captures.md).
|
||||
|
||||
## Fresh Session Per Unit
|
||||
|
||||
Every task gets a clean AI context window. No accumulated garbage, no quality degradation from context bloat. The dispatch prompt includes everything needed — task plans, prior summaries, decisions, dependency context — so the AI starts oriented.
|
||||
|
||||
## Git Isolation
|
||||
|
||||
GSD isolates milestone work using one of three modes:
|
||||
|
||||
| Mode | How It Works | Best For |
|
||||
|------|-------------|----------|
|
||||
| `worktree` (default) | Each milestone gets its own directory and branch | Most projects |
|
||||
| `branch` | Work happens in the project root on a milestone branch | Submodule-heavy repos |
|
||||
| `none` | Work happens directly on your current branch | Hot-reload workflows |
|
||||
|
||||
In worktree mode, all commits are squash-merged to main as one clean commit when the milestone completes. See [Git & Worktrees](../configuration/git-settings.md).
|
||||
|
||||
## Crash Recovery
|
||||
|
||||
If a session dies, the next `/gsd auto` reads the surviving session file, synthesizes a recovery briefing from every tool call that made it to disk, and resumes with full context.
|
||||
|
||||
In headless mode (`gsd headless auto`), crashes trigger automatic restart with exponential backoff (5s → 10s → 30s, up to 3 attempts). Combined with crash recovery, this enables true overnight "fire and forget" execution.
|
||||
|
||||
## Provider Error Recovery
|
||||
|
||||
GSD handles provider errors automatically:
|
||||
|
||||
| Error Type | Examples | What Happens |
|
||||
|-----------|----------|-------------|
|
||||
| Rate limit | 429, "too many requests" | Auto-resumes after cooldown (60s or retry-after header) |
|
||||
| Server error | 500, 502, 503, "overloaded" | Auto-resumes after 30s |
|
||||
| Permanent | "unauthorized", "invalid key" | Pauses — requires manual resume |
|
||||
|
||||
No manual intervention needed for transient errors.
|
||||
|
||||
## Timeout Supervision
|
||||
|
||||
Three timeout tiers prevent runaway sessions:
|
||||
|
||||
| Timeout | Default | What Happens |
|
||||
|---------|---------|-------------|
|
||||
| Soft | 20 min | Warns the AI to wrap up |
|
||||
| Idle | 10 min | Detects stalls, intervenes |
|
||||
| Hard | 30 min | Pauses auto mode |
|
||||
|
||||
Configure in preferences:
|
||||
|
||||
```yaml
|
||||
auto_supervisor:
|
||||
soft_timeout_minutes: 20
|
||||
idle_timeout_minutes: 10
|
||||
hard_timeout_minutes: 30
|
||||
```
|
||||
|
||||
## Verification Gates
|
||||
|
||||
Configure shell commands that run automatically after every task:
|
||||
|
||||
```yaml
|
||||
verification_commands:
|
||||
- npm run lint
|
||||
- npm run test
|
||||
verification_auto_fix: true # auto-retry on failure
|
||||
verification_max_retries: 2 # max retry attempts
|
||||
```
|
||||
|
||||
If verification fails, the AI sees the output and attempts to fix the issues before advancing. This ensures quality gates are enforced mechanically.
|
||||
|
||||
## Slice Discussion Gate
|
||||
|
||||
For projects requiring human review before each slice:
|
||||
|
||||
```yaml
|
||||
require_slice_discussion: true
|
||||
```
|
||||
|
||||
Auto mode pauses before each slice, showing the plan for your approval before building.
|
||||
|
||||
## Stuck Detection
|
||||
|
||||
GSD uses sliding-window analysis to detect stuck loops — not just "same unit dispatched twice" but also cycles like A→B→A→B. On detection, GSD retries once with a diagnostic prompt. If it fails again, auto mode stops with details so you can intervene.
|
||||
|
||||
## Cost Tracking
|
||||
|
||||
Every unit's token usage and cost is captured, broken down by phase, slice, and model. The dashboard shows running totals and projections. Budget ceilings can pause auto mode before overspending. See [Cost Management](../features/cost-management.md).
|
||||
|
||||
## Dashboard
|
||||
|
||||
`Ctrl+Alt+G` or `/gsd status` shows real-time progress:
|
||||
|
||||
- Current milestone, slice, and task
|
||||
- Auto mode elapsed time and phase
|
||||
- Per-unit cost and token breakdown
|
||||
- Cost projections
|
||||
- Completed and in-progress units
|
||||
- Pending capture count
|
||||
- Parallel worker status (when running parallel milestones)
|
||||
|
||||
## HTML Reports
|
||||
|
||||
After a milestone completes, GSD generates a self-contained HTML report in `.gsd/reports/` with project summary, progress tree, dependency graph, cost metrics, timeline, and changelog. Generate manually with:
|
||||
|
||||
```
|
||||
/gsd export --html
|
||||
/gsd export --html --all # all milestones
|
||||
```
|
||||
|
||||
## Diagnostic Tools
|
||||
|
||||
If auto mode has issues, GSD provides two diagnostic tools:
|
||||
|
||||
- **`/gsd doctor`** — validates `.gsd/` integrity, checks referential consistency, fixes structural issues
|
||||
- **`/gsd forensics`** — full post-mortem debugger with anomaly detection, unit traces, metrics analysis, and AI-guided investigation
|
||||
|
||||
```
|
||||
/gsd doctor
|
||||
/gsd forensics [optional problem description]
|
||||
```
|
||||
104
gitbook/core-concepts/project-structure.md
Normal file
104
gitbook/core-concepts/project-structure.md
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# How GSD Organizes Work
|
||||
|
||||
GSD uses a three-level hierarchy to break projects into manageable pieces that an AI can execute reliably.
|
||||
|
||||
## The Hierarchy
|
||||
|
||||
```
|
||||
Milestone → a shippable version (4-10 slices)
|
||||
Slice → one demoable vertical feature (1-7 tasks)
|
||||
Task → one context-window-sized unit of work
|
||||
```
|
||||
|
||||
### Milestones
|
||||
|
||||
A milestone is a shippable version of your project — an MVP, a major release, or a feature set that delivers standalone value. Milestones typically contain 4-10 slices.
|
||||
|
||||
Examples:
|
||||
- "MVP with user auth, dashboard, and settings"
|
||||
- "v2.0 with real-time collaboration and API v2"
|
||||
- "Security hardening milestone"
|
||||
|
||||
### Slices
|
||||
|
||||
A slice is one demoable, vertical capability within a milestone. It cuts across layers (database, backend, frontend) to deliver something you could show to a user. Slices contain 1-7 tasks.
|
||||
|
||||
Examples:
|
||||
- "User authentication with JWT"
|
||||
- "Dashboard layout with charts"
|
||||
- "API rate limiting"
|
||||
|
||||
### Tasks
|
||||
|
||||
A task is the smallest unit of work — something that fits in one AI context window. If a task can't be completed in a single AI session, it's broken into smaller tasks.
|
||||
|
||||
Examples:
|
||||
- "Create the User model and migration"
|
||||
- "Implement JWT middleware"
|
||||
- "Build the login form component"
|
||||
|
||||
## The `.gsd/` Directory
|
||||
|
||||
All project state lives on disk in a `.gsd/` directory at your project root:
|
||||
|
||||
```
|
||||
.gsd/
|
||||
PROJECT.md — living description of what the project is
|
||||
REQUIREMENTS.md — requirement contract (active/validated/deferred)
|
||||
DECISIONS.md — append-only architectural decisions log
|
||||
KNOWLEDGE.md — cross-session rules, patterns, and lessons
|
||||
RUNTIME.md — runtime context: API endpoints, env vars, services
|
||||
STATE.md — quick-glance status of current work
|
||||
PREFERENCES.md — project-level preferences (optional)
|
||||
milestones/
|
||||
M001/
|
||||
M001-ROADMAP.md — slice plan with risk levels and dependencies
|
||||
M001-CONTEXT.md — scope and goals from discussion phase
|
||||
slices/
|
||||
S01/
|
||||
S01-PLAN.md — task decomposition for this slice
|
||||
S01-SUMMARY.md — what was built and what changed
|
||||
S01-UAT.md — human test script
|
||||
tasks/
|
||||
T01-PLAN.md — detailed plan for this task
|
||||
T01-SUMMARY.md — what the task accomplished
|
||||
```
|
||||
|
||||
### Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `PROJECT.md` | High-level project description, updated as the project evolves |
|
||||
| `REQUIREMENTS.md` | Formal requirement contract — tracks what's active, validated, and deferred |
|
||||
| `DECISIONS.md` | Append-only log of architectural decisions with rationale |
|
||||
| `KNOWLEDGE.md` | Rules, patterns, and lessons learned across sessions — GSD reads this at the start of every task |
|
||||
| `RUNTIME.md` | Runtime context like API URLs, ports, and environment variables |
|
||||
| `STATE.md` | Current status at a glance — auto-generated, don't edit manually |
|
||||
|
||||
## How Work Flows
|
||||
|
||||
Each slice flows through phases:
|
||||
|
||||
```
|
||||
Plan → Execute (per task) → Complete → Reassess Roadmap → Next Slice
|
||||
```
|
||||
|
||||
1. **Plan** — GSD scouts the codebase, researches relevant docs, and decomposes the slice into tasks with clear requirements
|
||||
2. **Execute** — Each task runs in a fresh AI session with focused context
|
||||
3. **Complete** — GSD writes summaries, generates a UAT script, and commits
|
||||
4. **Reassess** — The roadmap is checked against reality — slices may be reordered, added, or removed
|
||||
5. **Next Slice** — The loop continues until all slices are done
|
||||
|
||||
After all slices complete, a **milestone validation** gate checks that success criteria were actually met before sealing the milestone.
|
||||
|
||||
## Adding Knowledge
|
||||
|
||||
GSD maintains a knowledge base that persists across sessions. Add rules, patterns, or lessons:
|
||||
|
||||
```
|
||||
/gsd knowledge rule "Always use parameterized queries for database access"
|
||||
/gsd knowledge pattern "Service classes go in src/services/"
|
||||
/gsd knowledge lesson "The OAuth flow requires the redirect URL to match exactly"
|
||||
```
|
||||
|
||||
This knowledge is injected into every task prompt automatically.
|
||||
54
gitbook/core-concepts/step-mode.md
Normal file
54
gitbook/core-concepts/step-mode.md
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# Step Mode
|
||||
|
||||
Step mode is GSD's interactive, one-step-at-a-time workflow. You stay in the loop, reviewing output between each step.
|
||||
|
||||
## Starting Step Mode
|
||||
|
||||
```
|
||||
/gsd
|
||||
```
|
||||
|
||||
GSD reads the state of your `.gsd/` directory and presents a wizard showing what's completed and what's next. It then executes one unit of work and pauses.
|
||||
|
||||
## How It Works
|
||||
|
||||
Step mode adapts to your project's current state:
|
||||
|
||||
| State | What Happens |
|
||||
|-------|-------------|
|
||||
| No `.gsd/` directory | Starts a discussion flow to capture your project vision |
|
||||
| Milestone exists, no roadmap | Opens a discussion or research phase for the milestone |
|
||||
| Roadmap exists, slices pending | Plans the next slice or executes the next task |
|
||||
| Mid-task | Resumes where you left off |
|
||||
|
||||
After each unit completes, you see results and decide what to do next. This is ideal for:
|
||||
|
||||
- New projects where you want to shape the architecture
|
||||
- Critical work where you want to review each step
|
||||
- Learning how GSD works before trusting auto mode
|
||||
|
||||
## Steering During Step Mode
|
||||
|
||||
Between steps, you can:
|
||||
|
||||
- **Discuss** — `/gsd discuss` to talk through architecture decisions
|
||||
- **Skip** — `/gsd skip` to prevent a unit from being dispatched
|
||||
- **Undo** — `/gsd undo` to revert the last completed unit
|
||||
- **Switch to auto** — `/gsd auto` to let GSD continue autonomously
|
||||
|
||||
## When to Use Step Mode
|
||||
|
||||
- **First milestone** — Review GSD's work before trusting it to run solo
|
||||
- **Architectural decisions** — When you want to guide the approach
|
||||
- **Unfamiliar codebases** — When you want to ensure GSD understands the project
|
||||
- **High-stakes changes** — When mistakes would be costly
|
||||
|
||||
## Transitioning to Auto Mode
|
||||
|
||||
Once you're comfortable with GSD's approach, switch to auto mode:
|
||||
|
||||
```
|
||||
/gsd auto
|
||||
```
|
||||
|
||||
You can always press **Escape** to pause auto mode and return to step-by-step control.
|
||||
54
gitbook/features/captures.md
Normal file
54
gitbook/features/captures.md
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# Captures & Triage
|
||||
|
||||
Captures let you fire-and-forget thoughts during auto-mode execution. Instead of pausing auto mode to steer, capture ideas, bugs, or scope changes and let GSD triage them at natural seams between tasks.
|
||||
|
||||
## Quick Start
|
||||
|
||||
While auto mode is running (or any time):
|
||||
|
||||
```
|
||||
/gsd capture "add rate limiting to the API endpoints"
|
||||
/gsd capture "the auth flow should support OAuth, not just JWT"
|
||||
```
|
||||
|
||||
Captures are appended to `.gsd/CAPTURES.md` and triaged automatically between tasks.
|
||||
|
||||
## How It Works
|
||||
|
||||
```
|
||||
Capture → Triage → Confirm → Resolve → Resume
|
||||
```
|
||||
|
||||
1. **Capture** — your thought is saved with a timestamp
|
||||
2. **Triage** — between tasks, GSD classifies each capture
|
||||
3. **Confirm** — you see the proposed resolution and approve or adjust
|
||||
4. **Resolve** — the resolution is applied
|
||||
5. **Resume** — auto mode continues
|
||||
|
||||
## Classification Types
|
||||
|
||||
Each capture is classified into one of five types:
|
||||
|
||||
| Type | Meaning | What Happens |
|
||||
|------|---------|-------------|
|
||||
| `quick-task` | Small, self-contained fix | Executed immediately |
|
||||
| `inject` | New task needed in current slice | Task added to active slice |
|
||||
| `defer` | Important but not urgent | Deferred to roadmap reassessment |
|
||||
| `replan` | Changes the current approach | Triggers slice replan |
|
||||
| `note` | Informational, no action needed | Acknowledged, no changes |
|
||||
|
||||
Plan-modifying resolutions (inject, replan) require your confirmation.
|
||||
|
||||
## Manual Triage
|
||||
|
||||
Trigger triage manually at any time:
|
||||
|
||||
```
|
||||
/gsd triage
|
||||
```
|
||||
|
||||
Useful when you've accumulated several captures and want to process them before the next natural seam.
|
||||
|
||||
## Dashboard Integration
|
||||
|
||||
The progress widget shows a pending capture count badge when captures are waiting for triage.
|
||||
74
gitbook/features/cost-management.md
Normal file
74
gitbook/features/cost-management.md
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# Cost Management
|
||||
|
||||
GSD tracks token usage and cost for every unit of work during auto mode. This data powers the dashboard, budget enforcement, and cost projections.
|
||||
|
||||
## Viewing Costs
|
||||
|
||||
**Dashboard:** Press `Ctrl+Alt+G` or type `/gsd status` for real-time cost breakdown.
|
||||
|
||||
**Visualizer:** `/gsd visualize` → Metrics tab for detailed charts.
|
||||
|
||||
**Aggregations:**
|
||||
- By phase (research, planning, execution, completion, reassessment)
|
||||
- By slice
|
||||
- By model
|
||||
- Project totals
|
||||
|
||||
## Budget Ceiling
|
||||
|
||||
Set a maximum spend:
|
||||
|
||||
```yaml
|
||||
budget_ceiling: 50.00
|
||||
```
|
||||
|
||||
### Enforcement Modes
|
||||
|
||||
```yaml
|
||||
budget_enforcement: pause # default when ceiling is set
|
||||
```
|
||||
|
||||
| Mode | What Happens |
|
||||
|------|-------------|
|
||||
| `warn` | Log a warning, keep going |
|
||||
| `pause` | Pause auto mode, wait for you |
|
||||
| `halt` | Stop auto mode entirely |
|
||||
|
||||
## Cost Projections
|
||||
|
||||
Once at least two slices have completed, GSD projects the remaining cost:
|
||||
|
||||
```
|
||||
Projected remaining: $12.40 ($6.20/slice avg × 2 remaining)
|
||||
```
|
||||
|
||||
## Budget Pressure
|
||||
|
||||
When approaching the budget ceiling, GSD automatically uses cheaper models:
|
||||
|
||||
| Budget Used | Effect |
|
||||
|------------|--------|
|
||||
| < 50% | No adjustment |
|
||||
| 50-75% | Standard tasks downgrade to lighter models |
|
||||
| 75-90% | More aggressive downgrading |
|
||||
| > 90% | Nearly everything downgrades; only complex tasks stay at standard |
|
||||
|
||||
This spreads your budget across remaining work instead of exhausting it early.
|
||||
|
||||
## Token Profiles & Cost
|
||||
|
||||
| Profile | Typical Savings | How |
|
||||
|---------|----------------|-----|
|
||||
| `budget` | 40-60% | Cheaper models, phase skipping, minimal context |
|
||||
| `balanced` | 10-20% | Default models, standard context |
|
||||
| `quality` | 0% (baseline) | All phases, full context |
|
||||
|
||||
## Tips
|
||||
|
||||
- Start with `balanced` profile and a generous `budget_ceiling` to establish baseline costs
|
||||
- Check `/gsd status` after a few slices to see per-slice cost averages
|
||||
- Switch to `budget` for well-understood, repetitive work
|
||||
- Use `quality` only when architectural decisions are being made
|
||||
- Use per-phase model selection to save: Opus for planning, Sonnet for execution
|
||||
- Enable `dynamic_routing` for automatic model downgrading on simple tasks
|
||||
- Use `/gsd visualize` → Metrics tab to see where your budget is going
|
||||
88
gitbook/features/dynamic-model-routing.md
Normal file
88
gitbook/features/dynamic-model-routing.md
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# Dynamic Model Routing
|
||||
|
||||
Dynamic model routing automatically selects cheaper models for simple work and reserves expensive models for complex tasks. This reduces cost by 20-50% without sacrificing quality where it matters.
|
||||
|
||||
## Enabling
|
||||
|
||||
```yaml
|
||||
dynamic_routing:
|
||||
enabled: true
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
Each unit passes through two stages:
|
||||
|
||||
1. **Complexity classification** — classifies work as light, standard, or heavy
|
||||
2. **Capability scoring** — within the tier, ranks models by how well they match the task
|
||||
|
||||
**Key rule:** Your configured model is always the ceiling — routing never upgrades beyond what you've set.
|
||||
|
||||
| Tier | Typical Work | Model Level |
|
||||
|------|-------------|-------------|
|
||||
| Light | Slice completion, UAT, hooks | Haiku-class |
|
||||
| Standard | Research, planning, execution | Sonnet-class |
|
||||
| Heavy | Replanning, roadmap reassessment | Opus-class |
|
||||
|
||||
## Configuration
|
||||
|
||||
```yaml
|
||||
dynamic_routing:
|
||||
enabled: true
|
||||
tier_models: # optional: explicit model per tier
|
||||
light: claude-haiku-4-5
|
||||
standard: claude-sonnet-4-6
|
||||
heavy: claude-opus-4-6
|
||||
escalate_on_failure: true # bump tier on failure (default)
|
||||
budget_pressure: true # auto-downgrade near budget ceiling (default)
|
||||
cross_provider: true # consider models from other providers (default)
|
||||
capability_routing: true # score models by task fit (default)
|
||||
```
|
||||
|
||||
### Escalate on Failure
|
||||
|
||||
When a task fails at a given tier, the router escalates to the next tier on retry: Light → Standard → Heavy. This prevents cheap models from burning retries on work that needs more reasoning.
|
||||
|
||||
### Budget Pressure
|
||||
|
||||
When approaching the budget ceiling, the router progressively downgrades:
|
||||
|
||||
| Budget Used | Effect |
|
||||
|------------|--------|
|
||||
| < 50% | No adjustment |
|
||||
| 50-75% | Standard → Light |
|
||||
| 75-90% | More aggressive |
|
||||
| > 90% | Nearly everything → Light |
|
||||
|
||||
### Cross-Provider
|
||||
|
||||
When enabled, the router may select models from providers other than your primary, using the built-in cost table to find the cheapest model at each tier.
|
||||
|
||||
### Capability Routing
|
||||
|
||||
Models are scored across 7 dimensions: coding, debugging, research, reasoning, speed, long context handling, and instruction following. Different task types weight these dimensions differently — a research task prioritizes research and reasoning, while an execution task prioritizes coding and instruction following.
|
||||
|
||||
Set `capability_routing: false` to revert to simple cheapest-in-tier selection.
|
||||
|
||||
## Interaction with Token Profiles
|
||||
|
||||
Dynamic routing and token profiles work together:
|
||||
|
||||
- **Token profiles** control phase skipping and context compression
|
||||
- **Dynamic routing** controls per-unit model selection
|
||||
|
||||
The `budget` profile + dynamic routing provides maximum cost savings.
|
||||
|
||||
## Adaptive Learning
|
||||
|
||||
GSD tracks routing outcomes in `.gsd/routing-history.json`. If a tier's failure rate exceeds 20% for a given task type, future classifications are bumped up.
|
||||
|
||||
Use `/gsd rate` to submit feedback:
|
||||
|
||||
```
|
||||
/gsd rate over # too powerful — use cheaper next time
|
||||
/gsd rate ok # just right
|
||||
/gsd rate under # too weak — use stronger next time
|
||||
```
|
||||
|
||||
Feedback is weighted 2x compared to automatic outcomes.
|
||||
44
gitbook/features/github-sync.md
Normal file
44
gitbook/features/github-sync.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# GitHub Sync
|
||||
|
||||
GSD can auto-sync milestones, slices, and tasks to GitHub Issues, PRs, and Milestones.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Install and authenticate the `gh` CLI:
|
||||
```bash
|
||||
gh auth login
|
||||
```
|
||||
|
||||
2. Enable in preferences:
|
||||
```yaml
|
||||
github:
|
||||
enabled: true
|
||||
repo: "owner/repo" # auto-detected from git remote if omitted
|
||||
labels: [gsd, auto-generated] # labels for created items
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/github-sync bootstrap` | Initial setup — creates GitHub Milestones, Issues, and draft PRs from current `.gsd/` state |
|
||||
| `/github-sync status` | Show sync mapping counts (milestones, slices, tasks) |
|
||||
|
||||
## How It Works
|
||||
|
||||
- Milestones → GitHub Milestones
|
||||
- Slices → GitHub Issues (linked to milestone)
|
||||
- Tasks → GitHub Issue checklists
|
||||
- Completed slices → Draft PRs
|
||||
|
||||
Sync mapping is persisted in `.gsd/.github-sync.json`. The sync is rate-limit aware — it skips when the GitHub API rate limit is low.
|
||||
|
||||
## Configuration
|
||||
|
||||
```yaml
|
||||
github:
|
||||
enabled: true
|
||||
repo: "owner/repo"
|
||||
labels: [gsd, auto-generated]
|
||||
project: "Project ID" # optional: GitHub Project board
|
||||
```
|
||||
86
gitbook/features/headless.md
Normal file
86
gitbook/features/headless.md
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
# Headless & CI Mode
|
||||
|
||||
`gsd headless` runs GSD commands without a terminal UI — designed for CI pipelines, cron jobs, and scripted automation.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```bash
|
||||
# Run auto mode
|
||||
gsd headless
|
||||
|
||||
# Run a single unit
|
||||
gsd headless next
|
||||
|
||||
# With timeout for CI
|
||||
gsd headless --timeout 600000 auto
|
||||
|
||||
# Force a specific phase
|
||||
gsd headless dispatch plan
|
||||
|
||||
# Stream all events as JSONL
|
||||
gsd headless --json auto
|
||||
```
|
||||
|
||||
## Creating Milestones Headlessly
|
||||
|
||||
```bash
|
||||
# From a context file
|
||||
gsd headless new-milestone --context brief.md --auto
|
||||
|
||||
# From inline text
|
||||
gsd headless new-milestone --context-text "Build a REST API with auth"
|
||||
|
||||
# Pipe from stdin
|
||||
echo "Build a CLI tool" | gsd headless new-milestone --context -
|
||||
```
|
||||
|
||||
## CLI Flags
|
||||
|
||||
| Flag | Default | Description |
|
||||
|------|---------|-------------|
|
||||
| `--timeout N` | 300000 (5 min) | Overall timeout in milliseconds |
|
||||
| `--max-restarts N` | 3 | Auto-restart on crash (0 to disable) |
|
||||
| `--json` | — | Stream events as JSONL to stdout |
|
||||
| `--model ID` | — | Override model for this session |
|
||||
| `--context <file>` | — | Context file for `new-milestone` (use `-` for stdin) |
|
||||
| `--context-text <text>` | — | Inline context for `new-milestone` |
|
||||
| `--auto` | — | Chain into auto mode after milestone creation |
|
||||
|
||||
## Exit Codes
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| `0` | Complete |
|
||||
| `1` | Error or timeout |
|
||||
| `2` | Blocked |
|
||||
|
||||
## Instant State Query
|
||||
|
||||
`gsd headless query` returns a JSON snapshot of project state — no AI session, instant response (~50ms):
|
||||
|
||||
```bash
|
||||
gsd headless query | jq '.state.phase'
|
||||
# "executing"
|
||||
|
||||
gsd headless query | jq '.next'
|
||||
# {"action":"dispatch","unitType":"execute-task","unitId":"M001/S01/T03"}
|
||||
|
||||
gsd headless query | jq '.cost.total'
|
||||
# 4.25
|
||||
```
|
||||
|
||||
Any `/gsd` subcommand works as a positional argument: `gsd headless status`, `gsd headless doctor`, etc.
|
||||
|
||||
## MCP Server Mode
|
||||
|
||||
`gsd --mode mcp` runs GSD as a Model Context Protocol server over stdin/stdout, exposing all GSD tools to external AI clients:
|
||||
|
||||
```bash
|
||||
gsd --mode mcp
|
||||
```
|
||||
|
||||
Compatible with Claude Desktop, VS Code Copilot, and any MCP host.
|
||||
|
||||
## Auto-Restart
|
||||
|
||||
In headless mode, crashes trigger automatic restart with exponential backoff (5s → 10s → 30s cap, default 3 attempts). SIGINT/SIGTERM bypasses restart. Combined with crash recovery, this enables true overnight unattended execution.
|
||||
97
gitbook/features/parallel.md
Normal file
97
gitbook/features/parallel.md
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
# Parallel Orchestration
|
||||
|
||||
Run multiple milestones simultaneously in isolated git worktrees. Each milestone gets its own worker process, branch, and context window.
|
||||
|
||||
{% hint style="info" %}
|
||||
Parallel mode is off by default. Enable it in preferences to use `/gsd parallel` commands.
|
||||
{% endhint %}
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. Enable parallel mode:
|
||||
```yaml
|
||||
parallel:
|
||||
enabled: true
|
||||
max_workers: 2
|
||||
```
|
||||
|
||||
2. Start parallel execution:
|
||||
```
|
||||
/gsd parallel start
|
||||
```
|
||||
GSD scans milestones, checks dependencies and file overlap, shows an eligibility report, and spawns workers.
|
||||
|
||||
3. Monitor:
|
||||
```
|
||||
/gsd parallel status
|
||||
```
|
||||
|
||||
4. Stop:
|
||||
```
|
||||
/gsd parallel stop
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
Each worker is a separate GSD process with complete isolation:
|
||||
|
||||
| Resource | Isolation |
|
||||
|----------|----------|
|
||||
| Filesystem | Own git worktree |
|
||||
| Git branch | `milestone/<MID>` |
|
||||
| Context window | Separate process |
|
||||
| Metrics | Own `metrics.json` |
|
||||
| Crash recovery | Own `auto.lock` |
|
||||
|
||||
Workers communicate with the coordinator through file-based IPC — heartbeat files and signal files in `.gsd/parallel/`.
|
||||
|
||||
## Eligibility
|
||||
|
||||
Before starting, GSD checks which milestones can run concurrently:
|
||||
|
||||
1. **Not complete** — finished milestones are skipped
|
||||
2. **Dependencies satisfied** — all `dependsOn` entries must be complete
|
||||
3. **File overlap** — milestones touching the same files get a warning (but are still eligible since they run in separate worktrees)
|
||||
|
||||
## Configuration
|
||||
|
||||
```yaml
|
||||
parallel:
|
||||
enabled: false # master toggle (default: false)
|
||||
max_workers: 2 # concurrent workers (1-4)
|
||||
budget_ceiling: 50.00 # aggregate cost limit
|
||||
merge_strategy: "per-milestone" # when to merge back
|
||||
auto_merge: "confirm" # "auto", "confirm", or "manual"
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/gsd parallel start` | Analyze and start workers |
|
||||
| `/gsd parallel status` | Show all workers with progress and cost |
|
||||
| `/gsd parallel stop [MID]` | Stop all or a specific worker |
|
||||
| `/gsd parallel pause [MID]` | Pause all or a specific worker |
|
||||
| `/gsd parallel resume [MID]` | Resume paused workers |
|
||||
| `/gsd parallel merge [MID]` | Merge completed milestones to main |
|
||||
|
||||
## Merge Reconciliation
|
||||
|
||||
When milestones complete, their changes merge back to main:
|
||||
|
||||
- `.gsd/` state files are auto-resolved
|
||||
- Code conflicts halt the merge — resolve manually and retry with `/gsd parallel merge <MID>`
|
||||
|
||||
## Budget Management
|
||||
|
||||
When `budget_ceiling` is set, aggregate cost across all workers is tracked. When the ceiling is reached, workers are signaled to stop.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Fix |
|
||||
|---------|-----|
|
||||
| "Parallel mode is not enabled" | Set `parallel.enabled: true` |
|
||||
| "No eligible milestones" | All milestones are complete or blocked; check `/gsd queue` |
|
||||
| Worker crashed | Run `/gsd doctor --fix`, then `/gsd parallel start` |
|
||||
| Merge conflicts | Resolve in `.gsd/worktrees/<MID>/`, then `/gsd parallel merge <MID>` |
|
||||
| Workers seem stuck | Check if budget ceiling was reached via `/gsd parallel status` |
|
||||
90
gitbook/features/remote-questions.md
Normal file
90
gitbook/features/remote-questions.md
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
# Remote Questions
|
||||
|
||||
Remote questions let GSD ask for your input via Slack, Discord, or Telegram when running in headless auto mode. When GSD needs a decision, it posts the question to your configured channel and polls for a response.
|
||||
|
||||
## Setup
|
||||
|
||||
### Discord
|
||||
|
||||
```
|
||||
/gsd remote discord
|
||||
```
|
||||
|
||||
The wizard prompts for your bot token, validates it, lets you pick a server and channel, sends a test message, and saves the config.
|
||||
|
||||
**Bot requirements:**
|
||||
- A bot application with a token from the [Discord Developer Portal](https://discord.com/developers/applications)
|
||||
- Bot invited to the server with: Send Messages, Read Message History, Add Reactions, View Channel
|
||||
- `DISCORD_BOT_TOKEN` environment variable set
|
||||
|
||||
### Slack
|
||||
|
||||
```
|
||||
/gsd remote slack
|
||||
```
|
||||
|
||||
**Bot requirements:**
|
||||
- A Slack app with a bot token (`xoxb-...`) from [Slack API](https://api.slack.com/apps)
|
||||
- Bot invited to the target channel
|
||||
- Scopes: `chat:write`, `reactions:read`, `reactions:write`, `channels:read`, `groups:read`, `channels:history`, `groups:history`
|
||||
|
||||
### Telegram
|
||||
|
||||
```
|
||||
/gsd remote telegram
|
||||
```
|
||||
|
||||
**Bot requirements:**
|
||||
- A bot token from [@BotFather](https://t.me/BotFather)
|
||||
- Bot added to the target group chat
|
||||
- `TELEGRAM_BOT_TOKEN` environment variable set
|
||||
|
||||
## Configuration
|
||||
|
||||
```yaml
|
||||
remote_questions:
|
||||
channel: discord # or slack or telegram
|
||||
channel_id: "1234567890123456789"
|
||||
timeout_minutes: 5 # 1-30, default 5
|
||||
poll_interval_seconds: 5 # 2-30, default 5
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. GSD encounters a decision point during auto mode
|
||||
2. The question is posted to your channel as a rich message
|
||||
3. GSD polls for a response at the configured interval
|
||||
4. You respond by:
|
||||
- **Reacting** with a number emoji (1️⃣, 2️⃣, etc.) for single-question prompts
|
||||
- **Replying** with a number, comma-separated numbers, or free text
|
||||
5. GSD picks up the response and continues
|
||||
6. A ✅ reaction confirms receipt
|
||||
|
||||
### Response Formats
|
||||
|
||||
**Single question:** React with a number emoji, reply with a number, or reply with free text.
|
||||
|
||||
**Multiple questions:** Reply with semicolons (`1;2;custom text`) or newlines (one answer per line).
|
||||
|
||||
### Timeouts
|
||||
|
||||
If no response arrives within `timeout_minutes`, GSD continues with a timeout result — typically making a conservative default choice.
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/gsd remote` | Show menu and current status |
|
||||
| `/gsd remote slack` | Set up Slack |
|
||||
| `/gsd remote discord` | Set up Discord |
|
||||
| `/gsd remote telegram` | Set up Telegram |
|
||||
| `/gsd remote status` | Show current config |
|
||||
| `/gsd remote disconnect` | Remove configuration |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Problem | Fix |
|
||||
|---------|-----|
|
||||
| "Remote auth failed" | Verify bot token is correct and not expired |
|
||||
| "Could not send to channel" | Check bot has Send Messages permission; invite bot to channel |
|
||||
| No response detected | Make sure you're replying to the prompt message, not posting a new one |
|
||||
120
gitbook/features/skills.md
Normal file
120
gitbook/features/skills.md
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
# Skills
|
||||
|
||||
Skills are specialized instruction sets that GSD loads when the task matches. They provide domain-specific guidance — coding patterns, framework idioms, testing strategies, and tool usage.
|
||||
|
||||
Skills follow the open [Agent Skills standard](https://agentskills.io/) and work across multiple AI agents, not just GSD.
|
||||
|
||||
## Skill Directories
|
||||
|
||||
| Location | Scope | Description |
|
||||
|----------|-------|------------|
|
||||
| `~/.agents/skills/` | Global | Shared across all projects |
|
||||
| `.agents/skills/` (project root) | Project | Project-specific, committable to git |
|
||||
|
||||
Global skills take precedence when names collide.
|
||||
|
||||
## Installing Skills
|
||||
|
||||
Skills are installed via the [skills.sh CLI](https://skills.sh):
|
||||
|
||||
```bash
|
||||
# Interactive — choose skills and target agents
|
||||
npx skills add dpearson2699/swift-ios-skills
|
||||
|
||||
# Install specific skills
|
||||
npx skills add dpearson2699/swift-ios-skills --skill swift-concurrency --skill swiftui-patterns -y
|
||||
|
||||
# Install all from a repo
|
||||
npx skills add dpearson2699/swift-ios-skills --all
|
||||
|
||||
# Check for updates
|
||||
npx skills check
|
||||
|
||||
# Update installed skills
|
||||
npx skills update
|
||||
```
|
||||
|
||||
## Onboarding Catalog
|
||||
|
||||
During `gsd init`, GSD detects your project's tech stack and recommends relevant skill packs:
|
||||
|
||||
- **Swift** — SwiftUI, Swift Core, concurrency, Charts, Testing
|
||||
- **iOS** — App Intents, Widgets, StoreKit, MapKit, Core ML, Vision, accessibility
|
||||
- **Web** — React, React Native, frontend design, accessibility
|
||||
- **Languages** — Rust, Python, Go patterns and best practices
|
||||
- **General** — Document handling (PDF, DOCX, XLSX)
|
||||
|
||||
## Skill Discovery
|
||||
|
||||
The `skill_discovery` preference controls how GSD finds skills during auto mode:
|
||||
|
||||
| Mode | Behavior |
|
||||
|------|----------|
|
||||
| `auto` | Skills found and applied automatically |
|
||||
| `suggest` | Skills identified but require confirmation (default) |
|
||||
| `off` | No skill discovery |
|
||||
|
||||
## Skill Preferences
|
||||
|
||||
Control which skills are used:
|
||||
|
||||
```yaml
|
||||
always_use_skills:
|
||||
- debug-like-expert
|
||||
prefer_skills:
|
||||
- frontend-design
|
||||
avoid_skills:
|
||||
- security-docker
|
||||
skill_rules:
|
||||
- when: task involves authentication
|
||||
use: [clerk]
|
||||
- when: frontend styling work
|
||||
prefer: [frontend-design]
|
||||
```
|
||||
|
||||
## Creating Custom Skills
|
||||
|
||||
Create your own skill by adding a directory with a `SKILL.md` file:
|
||||
|
||||
```
|
||||
~/.agents/skills/my-skill/
|
||||
SKILL.md — instructions for the AI
|
||||
references/ — optional reference files
|
||||
```
|
||||
|
||||
The `SKILL.md` contains instructions the AI follows when the skill is active.
|
||||
|
||||
### Project-Local Skills
|
||||
|
||||
Place skills in your project root for project-specific guidance:
|
||||
|
||||
```
|
||||
.agents/skills/my-project-skill/
|
||||
SKILL.md
|
||||
```
|
||||
|
||||
Project-local skills can be committed to git so team members share the same skill set.
|
||||
|
||||
## Skill Health Dashboard
|
||||
|
||||
Track skill performance:
|
||||
|
||||
```
|
||||
/gsd skill-health # overview table
|
||||
/gsd skill-health rust-core # detailed view for one skill
|
||||
/gsd skill-health --stale 30 # skills unused for 30+ days
|
||||
/gsd skill-health --declining # skills with falling success rates
|
||||
```
|
||||
|
||||
The dashboard flags:
|
||||
- Success rate below 70% over the last 10 uses
|
||||
- Token usage rising 20%+ compared to previous window
|
||||
- Skills unused beyond the configured threshold
|
||||
|
||||
### Staleness Detection
|
||||
|
||||
```yaml
|
||||
skill_staleness_days: 60 # flag skills unused for 60+ days (0 to disable)
|
||||
```
|
||||
|
||||
Stale skills are excluded from automatic matching but remain available for explicit use.
|
||||
91
gitbook/features/teams.md
Normal file
91
gitbook/features/teams.md
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
# Working in Teams
|
||||
|
||||
GSD supports multi-user workflows where several developers work on the same repository concurrently.
|
||||
|
||||
## Quick Setup
|
||||
|
||||
The simplest way: set team mode in your project preferences.
|
||||
|
||||
```yaml
|
||||
# .gsd/PREFERENCES.md (committed to git)
|
||||
---
|
||||
version: 1
|
||||
mode: team
|
||||
---
|
||||
```
|
||||
|
||||
This enables unique milestone IDs, push branches, pre-merge checks, and other team-appropriate defaults in one setting.
|
||||
|
||||
## What Team Mode Does
|
||||
|
||||
| Setting | Effect |
|
||||
|---------|--------|
|
||||
| `unique_milestone_ids` | IDs like `M001-eh88as` instead of `M001` — no collisions |
|
||||
| `git.push_branches` | Milestone branches are pushed to remote |
|
||||
| `git.pre_merge_check` | Validation runs before merging |
|
||||
|
||||
You can override individual settings on top of `mode: team`.
|
||||
|
||||
## Configure `.gitignore`
|
||||
|
||||
Share planning artifacts while keeping runtime files local:
|
||||
|
||||
```bash
|
||||
# Runtime files (per-developer, gitignore these)
|
||||
.gsd/auto.lock
|
||||
.gsd/completed-units.json
|
||||
.gsd/STATE.md
|
||||
.gsd/metrics.json
|
||||
.gsd/activity/
|
||||
.gsd/runtime/
|
||||
.gsd/worktrees/
|
||||
.gsd/milestones/**/continue.md
|
||||
.gsd/milestones/**/*-CONTINUE.md
|
||||
```
|
||||
|
||||
**What gets shared** (committed to git):
|
||||
- `.gsd/PREFERENCES.md` — project preferences
|
||||
- `.gsd/PROJECT.md` — living project description
|
||||
- `.gsd/REQUIREMENTS.md` — requirement contract
|
||||
- `.gsd/DECISIONS.md` — architectural decisions
|
||||
- `.gsd/milestones/` — roadmaps, plans, summaries, research
|
||||
|
||||
**What stays local** (gitignored):
|
||||
- Lock files, metrics, state, activity logs, worktrees
|
||||
|
||||
## Commit the Config
|
||||
|
||||
```bash
|
||||
git add .gsd/PREFERENCES.md
|
||||
git commit -m "chore: enable GSD team workflow"
|
||||
```
|
||||
|
||||
## Keeping `.gsd/` Local
|
||||
|
||||
For teams where only some members use GSD:
|
||||
|
||||
```yaml
|
||||
git:
|
||||
commit_docs: false
|
||||
```
|
||||
|
||||
This gitignores `.gsd/` entirely. You get structured planning without affecting teammates.
|
||||
|
||||
## Parallel Development
|
||||
|
||||
Multiple developers can run auto mode simultaneously on different milestones. Each developer:
|
||||
|
||||
- Gets their own worktree (`.gsd/worktrees/<MID>/`)
|
||||
- Works on a unique `milestone/<MID>` branch
|
||||
- Squash-merges to main independently
|
||||
|
||||
Milestone dependencies can be declared:
|
||||
|
||||
```yaml
|
||||
# In M00X-CONTEXT.md frontmatter
|
||||
---
|
||||
depends_on: [M001-eh88as]
|
||||
---
|
||||
```
|
||||
|
||||
GSD enforces that dependent milestones complete before starting downstream work.
|
||||
108
gitbook/features/token-optimization.md
Normal file
108
gitbook/features/token-optimization.md
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
# Token Optimization
|
||||
|
||||
GSD's token optimization system can reduce token usage by 40-60% without sacrificing output quality. It has three pillars: **token profiles**, **context compression**, and **complexity-based task routing**.
|
||||
|
||||
## Token Profiles
|
||||
|
||||
A token profile coordinates model selection, phase skipping, and context compression with a single setting:
|
||||
|
||||
```yaml
|
||||
token_profile: balanced
|
||||
```
|
||||
|
||||
### `budget` — Maximum Savings (40-60%)
|
||||
|
||||
| Setting | Value |
|
||||
|---------|-------|
|
||||
| Planning model | Sonnet |
|
||||
| Execution model | Sonnet |
|
||||
| Simple task model | Haiku |
|
||||
| Milestone research | Skipped |
|
||||
| Slice research | Skipped |
|
||||
| Roadmap reassessment | Skipped |
|
||||
| Context level | Minimal |
|
||||
|
||||
Best for: prototyping, small projects, well-understood codebases.
|
||||
|
||||
### `balanced` — Smart Defaults (default)
|
||||
|
||||
| Setting | Value |
|
||||
|---------|-------|
|
||||
| All models | User's default |
|
||||
| Milestone research | Runs |
|
||||
| Slice research | Skipped |
|
||||
| Roadmap reassessment | Runs |
|
||||
| Context level | Standard |
|
||||
|
||||
Best for: most projects, day-to-day development.
|
||||
|
||||
### `quality` — Full Context
|
||||
|
||||
| Setting | Value |
|
||||
|---------|-------|
|
||||
| All models | User's configured defaults |
|
||||
| All phases | Run |
|
||||
| Context level | Full |
|
||||
|
||||
Best for: complex architectures, greenfield projects, critical work.
|
||||
|
||||
## Context Compression
|
||||
|
||||
Each profile controls how much context is pre-loaded into AI prompts:
|
||||
|
||||
| Profile | What's Included |
|
||||
|---------|----------------|
|
||||
| `budget` | Task plan and essential prior summaries only |
|
||||
| `balanced` | Task plan, summaries, slice plan, roadmap excerpt |
|
||||
| `quality` | Everything — all plans, summaries, decisions, requirements |
|
||||
|
||||
## Complexity-Based Task Routing
|
||||
|
||||
GSD classifies each task by complexity and routes it to an appropriate model:
|
||||
|
||||
| Complexity | Indicators | Model Level |
|
||||
|-----------|------------|-------------|
|
||||
| Simple | ≤3 steps, ≤3 files, short description | Haiku-class |
|
||||
| Standard | 4-7 steps, 4-7 files | Sonnet-class |
|
||||
| Complex | ≥8 steps, ≥8 files, complexity keywords | Opus-class |
|
||||
|
||||
**Complexity keywords** that prevent simple classification: `refactor`, `migrate`, `integrate`, `architect`, `security`, `performance`, `concurrent`, `distributed`, and others.
|
||||
|
||||
{% hint style="info" %}
|
||||
Dynamic routing requires `models` configured in your preferences and `dynamic_routing.enabled: true`. See [Dynamic Model Routing](dynamic-model-routing.md).
|
||||
{% endhint %}
|
||||
|
||||
## Overriding Profile Defaults
|
||||
|
||||
The `token_profile` sets defaults, but explicit preferences always win:
|
||||
|
||||
```yaml
|
||||
token_profile: budget
|
||||
phases:
|
||||
skip_research: false # override: keep research
|
||||
models:
|
||||
planning: claude-opus-4-6 # override: use Opus for planning
|
||||
```
|
||||
|
||||
## Adaptive Learning
|
||||
|
||||
GSD tracks success and failure of tier assignments over time. If a model tier's failure rate exceeds 20% for a given task type, future tasks of that type are bumped to a higher tier.
|
||||
|
||||
Submit manual feedback with:
|
||||
|
||||
```
|
||||
/gsd rate over # model was overpowered — use cheaper next time
|
||||
/gsd rate ok # model was appropriate
|
||||
/gsd rate under # model was too weak — use stronger next time
|
||||
```
|
||||
|
||||
## Observation Masking
|
||||
|
||||
During auto mode, old tool results are replaced with lightweight placeholders before each AI call. This reduces token usage between compactions with zero overhead.
|
||||
|
||||
```yaml
|
||||
context_management:
|
||||
observation_masking: true # default: true
|
||||
observation_mask_turns: 8 # keep results from last 8 turns
|
||||
tool_result_max_chars: 800 # truncate large tool outputs
|
||||
```
|
||||
82
gitbook/features/visualizer.md
Normal file
82
gitbook/features/visualizer.md
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
# Workflow Visualizer
|
||||
|
||||
The workflow visualizer is a full-screen terminal overlay showing project progress, dependencies, cost metrics, and execution timeline.
|
||||
|
||||
## Opening
|
||||
|
||||
```
|
||||
/gsd visualize
|
||||
```
|
||||
|
||||
Or configure automatic display after milestone completion:
|
||||
|
||||
```yaml
|
||||
auto_visualize: true
|
||||
```
|
||||
|
||||
## Tabs
|
||||
|
||||
Switch tabs with `Tab`, `1`-`4`, or arrow keys.
|
||||
|
||||
### 1. Progress
|
||||
|
||||
A tree view of milestones, slices, and tasks with completion status:
|
||||
|
||||
```
|
||||
M001: User Management 3/6 tasks
|
||||
✅ S01: Auth module 3/3 tasks
|
||||
✅ T01: Core types
|
||||
✅ T02: JWT middleware
|
||||
✅ T03: Login flow
|
||||
⏳ S02: User dashboard 1/2 tasks
|
||||
✅ T01: Layout component
|
||||
⬜ T02: Profile page
|
||||
```
|
||||
|
||||
### 2. Dependencies
|
||||
|
||||
An ASCII dependency graph showing slice relationships:
|
||||
|
||||
```
|
||||
S01 ──→ S02 ──→ S04
|
||||
└───→ S03 ──↗
|
||||
```
|
||||
|
||||
### 3. Metrics
|
||||
|
||||
Bar charts showing cost and token usage:
|
||||
|
||||
- By phase (research, planning, execution, completion)
|
||||
- By slice (with running totals)
|
||||
- By model (which models consumed the most budget)
|
||||
|
||||
### 4. Timeline
|
||||
|
||||
Chronological execution history: unit type, timestamps, duration, model, and token counts.
|
||||
|
||||
## Controls
|
||||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `Tab` | Next tab |
|
||||
| `Shift+Tab` | Previous tab |
|
||||
| `1`-`4` | Jump to tab |
|
||||
| `↑`/`↓` | Scroll |
|
||||
| `Escape` / `q` | Close |
|
||||
|
||||
The visualizer auto-refreshes every 2 seconds, staying current alongside running auto mode.
|
||||
|
||||
## HTML Reports
|
||||
|
||||
For shareable reports outside the terminal:
|
||||
|
||||
```
|
||||
/gsd export --html # current milestone
|
||||
/gsd export --html --all # all milestones
|
||||
```
|
||||
|
||||
Generates self-contained HTML files in `.gsd/reports/` with progress tree, dependency graph, cost charts, timeline, and changelog. All CSS and JS are inlined — no external dependencies. Printable to PDF from any browser.
|
||||
|
||||
```yaml
|
||||
auto_report: true # auto-generate after milestone completion (default)
|
||||
```
|
||||
37
gitbook/features/web-interface.md
Normal file
37
gitbook/features/web-interface.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Web Interface
|
||||
|
||||
GSD includes a browser-based interface for project management and real-time progress monitoring.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
gsd --web
|
||||
```
|
||||
|
||||
This starts a local web server and opens the dashboard in your default browser.
|
||||
|
||||
## CLI Flags
|
||||
|
||||
```bash
|
||||
gsd --web --host 0.0.0.0 --port 8080 --allowed-origins "https://example.com"
|
||||
```
|
||||
|
||||
| Flag | Default | Description |
|
||||
|------|---------|-------------|
|
||||
| `--host` | `localhost` | Bind address |
|
||||
| `--port` | `3000` | Port |
|
||||
| `--allowed-origins` | (none) | Comma-separated CORS origins |
|
||||
|
||||
## Features
|
||||
|
||||
- **Project management** — view milestones, slices, and tasks in a visual dashboard
|
||||
- **Real-time progress** — live updates as auto mode executes
|
||||
- **Multi-project support** — manage multiple projects from one browser tab via `?project=` URL parameter
|
||||
- **Change project root** — switch directories from the web UI without restarting
|
||||
- **Onboarding flow** — API key setup and provider configuration in the browser
|
||||
- **Model selection** — switch models and providers from the web UI
|
||||
|
||||
## Platform Notes
|
||||
|
||||
- **macOS/Linux** — Full support
|
||||
- **Windows** — Web build is skipped due to Next.js compatibility issues; CLI remains fully functional
|
||||
45
gitbook/features/workflow-templates.md
Normal file
45
gitbook/features/workflow-templates.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# Workflow Templates
|
||||
|
||||
Workflow templates are pre-built patterns for common development tasks. Instead of setting up a full milestone for a quick bugfix or spike, use a template to get started immediately.
|
||||
|
||||
## Using Templates
|
||||
|
||||
```
|
||||
/gsd start # pick from available templates
|
||||
/gsd start resume # resume an in-progress workflow
|
||||
```
|
||||
|
||||
## Available Templates
|
||||
|
||||
| Template | Purpose |
|
||||
|----------|---------|
|
||||
| `bugfix` | Fix a specific bug with diagnosis and verification |
|
||||
| `spike` | Time-boxed investigation or prototype |
|
||||
| `feature` | Standard feature development |
|
||||
| `hotfix` | Urgent production fix |
|
||||
| `refactor` | Code restructuring and cleanup |
|
||||
| `security-audit` | Security review and remediation |
|
||||
| `dep-upgrade` | Dependency update and migration |
|
||||
| `full-project` | Complete project from scratch |
|
||||
|
||||
## Listing and Inspecting
|
||||
|
||||
```
|
||||
/gsd templates # list all available templates
|
||||
/gsd templates info <name> # show details for a template
|
||||
```
|
||||
|
||||
## Custom Workflows
|
||||
|
||||
Create your own workflow definitions:
|
||||
|
||||
```
|
||||
/gsd workflow new # create a new workflow YAML
|
||||
/gsd workflow run <name> # start a workflow run
|
||||
/gsd workflow list # list active runs
|
||||
/gsd workflow validate <name> # validate definition
|
||||
/gsd workflow pause # pause running workflow
|
||||
/gsd workflow resume # resume paused workflow
|
||||
```
|
||||
|
||||
Custom workflows are defined in YAML and can specify phases, dependencies, and configuration for each step.
|
||||
94
gitbook/getting-started/choosing-a-model.md
Normal file
94
gitbook/getting-started/choosing-a-model.md
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# Choosing a Model
|
||||
|
||||
GSD auto-selects a default model after you log in to a provider. You can switch models at any time.
|
||||
|
||||
## Switch Models
|
||||
|
||||
Inside a GSD session, type:
|
||||
|
||||
```
|
||||
/model
|
||||
```
|
||||
|
||||
This opens an interactive picker showing all available models from your configured providers.
|
||||
|
||||
## Per-Phase Models
|
||||
|
||||
Different phases of work have different requirements. You can assign specific models to each phase in your preferences:
|
||||
|
||||
```yaml
|
||||
models:
|
||||
research: claude-sonnet-4-6 # scouting and research
|
||||
planning: claude-opus-4-6 # architectural decisions
|
||||
execution: claude-sonnet-4-6 # writing code
|
||||
execution_simple: claude-haiku-4-5 # simple tasks (docs, config)
|
||||
completion: claude-sonnet-4-6 # summaries and wrap-up
|
||||
subagent: claude-sonnet-4-6 # delegated sub-tasks
|
||||
```
|
||||
|
||||
Omit a key to use whatever model is currently active for that phase.
|
||||
|
||||
## Model Fallbacks
|
||||
|
||||
If a model is unavailable (provider down, rate limited, credits exhausted), GSD can automatically fall back to another:
|
||||
|
||||
```yaml
|
||||
models:
|
||||
planning:
|
||||
model: claude-opus-4-6
|
||||
fallbacks:
|
||||
- openrouter/z-ai/glm-5
|
||||
- openrouter/moonshotai/kimi-k2.5
|
||||
```
|
||||
|
||||
Fallbacks are tried in order until one works.
|
||||
|
||||
## Token Profiles
|
||||
|
||||
Token profiles coordinate model selection, phase skipping, and context compression with a single setting:
|
||||
|
||||
| Profile | Cost Savings | Best For |
|
||||
|---------|-------------|----------|
|
||||
| `budget` | 40-60% | Prototyping, small projects, well-understood codebases |
|
||||
| `balanced` | 10-20% | Most projects, day-to-day development (default) |
|
||||
| `quality` | 0% (baseline) | Complex architectures, greenfield projects, critical work |
|
||||
|
||||
```yaml
|
||||
token_profile: balanced
|
||||
```
|
||||
|
||||
See [Token Optimization](../features/token-optimization.md) for details.
|
||||
|
||||
## Dynamic Model Routing
|
||||
|
||||
When enabled, GSD automatically picks cheaper models for simple tasks and reserves expensive ones for complex work:
|
||||
|
||||
```yaml
|
||||
dynamic_routing:
|
||||
enabled: true
|
||||
```
|
||||
|
||||
A documentation fix gets Haiku. An architectural refactor gets Opus. Your configured model is always the ceiling — routing never upgrades beyond what you've set.
|
||||
|
||||
See [Dynamic Model Routing](../features/dynamic-model-routing.md) for the full guide.
|
||||
|
||||
## Supported Providers
|
||||
|
||||
GSD supports 20+ providers out of the box. See [Provider Setup](../configuration/providers.md) for setup instructions:
|
||||
|
||||
| Provider | Auth Method |
|
||||
|----------|-------------|
|
||||
| Anthropic (Claude) | OAuth or API key |
|
||||
| OpenAI | API key |
|
||||
| Google Gemini | API key |
|
||||
| OpenRouter | API key |
|
||||
| Groq | API key |
|
||||
| xAI (Grok) | API key |
|
||||
| Mistral | API key |
|
||||
| GitHub Copilot | OAuth |
|
||||
| Amazon Bedrock | IAM credentials |
|
||||
| Vertex AI | ADC |
|
||||
| Azure OpenAI | API key |
|
||||
| Ollama | Local (no auth) |
|
||||
| LM Studio | Local (no auth) |
|
||||
| vLLM / SGLang | Local (no auth) |
|
||||
128
gitbook/getting-started/first-project.md
Normal file
128
gitbook/getting-started/first-project.md
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
# Your First Project
|
||||
|
||||
## Launch GSD
|
||||
|
||||
Open a terminal in any project directory (or an empty one) and run:
|
||||
|
||||
```bash
|
||||
gsd
|
||||
```
|
||||
|
||||
GSD shows a welcome screen with your version, active model, and available tool keys.
|
||||
|
||||
## Start a Discussion
|
||||
|
||||
Type `/gsd` to enter step mode. GSD reads the state of your project directory and determines the next logical action:
|
||||
|
||||
- **No `.gsd/` directory** — starts a discussion flow to capture your project vision
|
||||
- **Milestone exists, no roadmap** — discuss or research the milestone
|
||||
- **Roadmap exists, slices pending** — plan the next slice or execute a task
|
||||
- **Mid-task** — resume where you left off
|
||||
|
||||
For a new project, GSD will ask you to describe what you want to build. Talk through your vision — GSD captures requirements, architectural decisions, and scope.
|
||||
|
||||
## The Project Hierarchy
|
||||
|
||||
After discussion, GSD organizes your work into:
|
||||
|
||||
```
|
||||
Milestone → a shippable version (4-10 slices)
|
||||
Slice → one demoable feature (1-7 tasks)
|
||||
Task → one context-window-sized unit of work
|
||||
```
|
||||
|
||||
The key rule: **a task must fit in one AI context window.** If it can't, it becomes two tasks.
|
||||
|
||||
## Run Auto Mode
|
||||
|
||||
Once you have a milestone and roadmap, let GSD take the wheel:
|
||||
|
||||
```
|
||||
/gsd auto
|
||||
```
|
||||
|
||||
GSD autonomously:
|
||||
1. **Plans** each slice — scouts the codebase, researches docs, decomposes into tasks
|
||||
2. **Executes** each task — writes code in a fresh AI session
|
||||
3. **Completes** the slice — writes summaries, commits with meaningful messages
|
||||
4. **Reassesses** the roadmap — checks if the plan still makes sense
|
||||
5. **Repeats** until the milestone is done
|
||||
|
||||
## The Two-Terminal Workflow
|
||||
|
||||
The recommended approach: auto mode in one terminal, steering from another.
|
||||
|
||||
**Terminal 1 — let it build:**
|
||||
|
||||
```bash
|
||||
gsd
|
||||
/gsd auto
|
||||
```
|
||||
|
||||
**Terminal 2 — steer while it works:**
|
||||
|
||||
```bash
|
||||
gsd
|
||||
/gsd discuss # talk through architecture decisions
|
||||
/gsd status # check progress
|
||||
/gsd queue # queue the next milestone
|
||||
/gsd capture "add rate limiting to the API" # fire-and-forget thought
|
||||
```
|
||||
|
||||
Both terminals read and write the same `.gsd/` files. Decisions in terminal 2 are picked up at the next phase boundary automatically.
|
||||
|
||||
## Check Progress
|
||||
|
||||
Press `Ctrl+Alt+G` or type `/gsd status` to see the dashboard:
|
||||
|
||||
- Current milestone, slice, and task
|
||||
- Elapsed time and phase
|
||||
- Per-unit cost and token breakdown
|
||||
- Completed and in-progress work
|
||||
|
||||
## Resume a Session
|
||||
|
||||
```bash
|
||||
gsd --continue # or gsd -c
|
||||
```
|
||||
|
||||
Resumes the most recent session for the current directory.
|
||||
|
||||
To browse and pick from all saved sessions:
|
||||
|
||||
```bash
|
||||
gsd sessions
|
||||
```
|
||||
|
||||
Shows each session's date, message count, and preview so you can choose which to resume.
|
||||
|
||||
## What's on Disk
|
||||
|
||||
All state lives in `.gsd/` inside your project:
|
||||
|
||||
```
|
||||
.gsd/
|
||||
PROJECT.md — what the project is
|
||||
REQUIREMENTS.md — requirement contract
|
||||
DECISIONS.md — architectural decisions
|
||||
KNOWLEDGE.md — cross-session rules and patterns
|
||||
STATE.md — quick-glance status
|
||||
milestones/
|
||||
M001/
|
||||
M001-ROADMAP.md — slice plan with dependencies
|
||||
M001-CONTEXT.md — scope and goals
|
||||
slices/
|
||||
S01/
|
||||
S01-PLAN.md — task decomposition
|
||||
S01-SUMMARY.md — what happened
|
||||
S01-UAT.md — test script
|
||||
tasks/
|
||||
T01-PLAN.md
|
||||
T01-SUMMARY.md
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Auto Mode](../core-concepts/auto-mode.md) — deep dive into autonomous execution
|
||||
- [Preferences](../configuration/preferences.md) — model selection, timeouts, budgets
|
||||
- [Commands](../reference/commands.md) — all commands and shortcuts
|
||||
84
gitbook/getting-started/installation.md
Normal file
84
gitbook/getting-started/installation.md
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# Installation
|
||||
|
||||
## Install GSD
|
||||
|
||||
```bash
|
||||
npm install -g gsd-pi
|
||||
```
|
||||
|
||||
Requires **Node.js 22.0.0 or later** (24 LTS recommended) and **Git**.
|
||||
|
||||
{% hint style="info" %}
|
||||
**`command not found: gsd`?** Your shell may not have npm's global bin directory in `$PATH`. Run `npm prefix -g` to find it, then add `$(npm prefix -g)/bin` to your PATH. See [Troubleshooting](../reference/troubleshooting.md) for details.
|
||||
{% endhint %}
|
||||
|
||||
GSD checks for updates once every 24 hours. When a new version is available, you'll see a prompt at startup with the option to update immediately or skip. You can also update from within a session with `/gsd update`.
|
||||
|
||||
## Set Up Your LLM Provider
|
||||
|
||||
Launch GSD for the first time:
|
||||
|
||||
```bash
|
||||
gsd
|
||||
```
|
||||
|
||||
The setup wizard walks you through:
|
||||
|
||||
1. **LLM Provider** — choose from 20+ providers (Anthropic, OpenAI, Google, OpenRouter, GitHub Copilot, Amazon Bedrock, Azure, and more). OAuth flows handle Claude Max and Copilot subscriptions automatically; otherwise paste an API key.
|
||||
2. **Tool API Keys** (optional) — Brave Search, Context7, Jina, Slack, Discord. Press Enter to skip any.
|
||||
|
||||
Re-run the wizard anytime with:
|
||||
|
||||
```bash
|
||||
gsd config
|
||||
```
|
||||
|
||||
For detailed provider setup, see [Provider Setup](../configuration/providers.md).
|
||||
|
||||
## Set Up API Keys for Tools
|
||||
|
||||
If you use a non-Anthropic model, you may need a search API key for web search. Run `/gsd config` inside any GSD session to set keys globally — they're saved to `~/.gsd/agent/auth.json` and apply to all projects.
|
||||
|
||||
| Tool | Purpose | Get a Key |
|
||||
|------|---------|-----------|
|
||||
| Tavily Search | Web search for non-Anthropic models | [tavily.com](https://tavily.com/app/api-keys) |
|
||||
| Brave Search | Web search for non-Anthropic models | [brave.com](https://brave.com/search/api) |
|
||||
| Context7 Docs | Library documentation lookup | [context7.com](https://context7.com/dashboard) |
|
||||
|
||||
Anthropic models have built-in web search and don't need these keys.
|
||||
|
||||
## VS Code Extension
|
||||
|
||||
GSD is also available as a VS Code extension. Install from the marketplace (publisher: FluxLabs) or search for "GSD" in VS Code extensions.
|
||||
|
||||
The extension provides:
|
||||
|
||||
- **`@gsd` chat participant** — talk to the agent in VS Code Chat
|
||||
- **Sidebar dashboard** — connection status, model info, token usage, quick actions
|
||||
- **Full command palette** — start/stop agent, switch models, export sessions
|
||||
|
||||
The CLI (`gsd-pi`) must be installed first — the extension connects to it via RPC.
|
||||
|
||||
## Web Interface
|
||||
|
||||
GSD also has a browser-based interface:
|
||||
|
||||
```bash
|
||||
gsd --web
|
||||
```
|
||||
|
||||
This starts a local web server with a visual dashboard, real-time progress, and multi-project support. See [Web Interface](../features/web-interface.md) for details.
|
||||
|
||||
## Alternative Binary Name
|
||||
|
||||
If the `gsd` command conflicts with another tool (e.g., the oh-my-zsh git plugin aliases `gsd` to `git svn dcommit`), use the alternative:
|
||||
|
||||
```bash
|
||||
gsd-cli
|
||||
```
|
||||
|
||||
Both `gsd` and `gsd-cli` point to the same binary. To remove the conflict permanently, add this to your `~/.zshrc`:
|
||||
|
||||
```bash
|
||||
unalias gsd 2>/dev/null
|
||||
```
|
||||
61
gitbook/reference/cli-flags.md
Normal file
61
gitbook/reference/cli-flags.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# CLI Flags
|
||||
|
||||
## Starting GSD
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `gsd` | Start a new interactive session |
|
||||
| `gsd --continue` (`-c`) | Resume the most recent session |
|
||||
| `gsd --model <id>` | Override the default model for this session |
|
||||
| `gsd --web [path]` | Start browser-based web interface |
|
||||
| `gsd --worktree` (`-w`) [name] | Start in a git worktree |
|
||||
| `gsd --no-session` | Disable session persistence |
|
||||
| `gsd --extension <path>` | Load an additional extension (repeatable) |
|
||||
| `gsd --append-system-prompt <text>` | Append text to the system prompt |
|
||||
| `gsd --tools <list>` | Comma-separated tools to enable |
|
||||
| `gsd --version` (`-v`) | Print version and exit |
|
||||
| `gsd --help` (`-h`) | Print help and exit |
|
||||
| `gsd --debug` | Enable diagnostic logging |
|
||||
|
||||
## Non-Interactive Modes
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `gsd --print "msg"` (`-p`) | Single-shot prompt mode (no TUI) |
|
||||
| `gsd --mode <text\|json\|rpc\|mcp>` | Output mode for non-interactive use |
|
||||
|
||||
## Session Management
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `gsd sessions` | Interactive session picker — list and resume saved sessions |
|
||||
| `gsd --list-models [search]` | List available models and exit |
|
||||
|
||||
## Configuration
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `gsd config` | Set up global API keys |
|
||||
| `gsd update` | Update to the latest version |
|
||||
|
||||
## Headless Mode
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `gsd headless` | Run without TUI |
|
||||
| `gsd headless --timeout N` | Timeout in ms (default: 300000) |
|
||||
| `gsd headless --max-restarts N` | Auto-restart on crash (default: 3) |
|
||||
| `gsd headless --json` | Stream events as JSONL |
|
||||
| `gsd headless --model ID` | Override model |
|
||||
| `gsd headless --context <file>` | Context file for `new-milestone` |
|
||||
| `gsd headless --context-text <text>` | Inline context for `new-milestone` |
|
||||
| `gsd headless --auto` | Chain into auto mode after milestone creation |
|
||||
| `gsd headless query` | Instant JSON state snapshot (~50ms) |
|
||||
|
||||
## Web Interface
|
||||
|
||||
| Flag | Default | Description |
|
||||
|------|---------|-------------|
|
||||
| `--host` | `localhost` | Bind address |
|
||||
| `--port` | `3000` | Port |
|
||||
| `--allowed-origins` | (none) | CORS origins |
|
||||
128
gitbook/reference/commands.md
Normal file
128
gitbook/reference/commands.md
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
# Commands
|
||||
|
||||
## Session Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/gsd` | Step mode — execute one unit at a time |
|
||||
| `/gsd auto` | Autonomous mode — research, plan, execute, commit, repeat |
|
||||
| `/gsd quick` | Quick task with GSD guarantees but no full planning |
|
||||
| `/gsd stop` | Stop auto mode gracefully |
|
||||
| `/gsd pause` | Pause auto mode (preserves state) |
|
||||
| `/gsd steer` | Modify plan documents during execution |
|
||||
| `/gsd discuss` | Discuss architecture and decisions |
|
||||
| `/gsd status` | Progress dashboard |
|
||||
| `/gsd widget` | Cycle dashboard widget: full / small / min / off |
|
||||
| `/gsd queue` | Queue and reorder future milestones |
|
||||
| `/gsd capture` | Fire-and-forget thought capture |
|
||||
| `/gsd triage` | Manually trigger capture triage |
|
||||
| `/gsd dispatch` | Dispatch a specific phase directly |
|
||||
| `/gsd history` | View execution history (supports `--cost`, `--phase`, `--model` filters) |
|
||||
| `/gsd forensics` | Full debugger for auto-mode failures |
|
||||
| `/gsd cleanup` | Clean up state files and stale worktrees |
|
||||
| `/gsd visualize` | Open workflow visualizer |
|
||||
| `/gsd export --html` | Generate HTML report for current milestone |
|
||||
| `/gsd export --html --all` | Generate reports for all milestones |
|
||||
| `/gsd update` | Update GSD to the latest version |
|
||||
| `/gsd knowledge` | Add persistent project knowledge |
|
||||
| `/gsd fast` | Toggle service tier for supported models |
|
||||
| `/gsd rate` | Rate last unit's model tier (over/ok/under) |
|
||||
| `/gsd changelog` | Show release notes |
|
||||
| `/gsd logs` | Browse activity and debug logs |
|
||||
| `/gsd remote` | Control remote auto-mode |
|
||||
| `/gsd help` | Show all available commands |
|
||||
|
||||
## Configuration & Diagnostics
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/gsd prefs` | Preferences wizard |
|
||||
| `/gsd mode` | Switch workflow mode (solo/team) |
|
||||
| `/gsd config` | Re-run provider setup wizard |
|
||||
| `/gsd keys` | API key manager |
|
||||
| `/gsd doctor` | Runtime health checks with auto-fix |
|
||||
| `/gsd inspect` | Show database diagnostics |
|
||||
| `/gsd init` | Project init wizard |
|
||||
| `/gsd setup` | Global setup status |
|
||||
| `/gsd skill-health` | Skill lifecycle dashboard |
|
||||
| `/gsd hooks` | Show configured hooks |
|
||||
| `/gsd migrate` | Migrate v1 `.planning` to `.gsd` format |
|
||||
|
||||
## Milestone Management
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/gsd new-milestone` | Create a new milestone |
|
||||
| `/gsd skip` | Prevent a unit from auto-mode dispatch |
|
||||
| `/gsd undo` | Revert last completed unit |
|
||||
| `/gsd undo-task` | Reset a specific task's completion state |
|
||||
| `/gsd reset-slice` | Reset a slice and all its tasks |
|
||||
| `/gsd park` | Park a milestone (skip without deleting) |
|
||||
| `/gsd unpark` | Reactivate a parked milestone |
|
||||
|
||||
## Parallel Orchestration
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/gsd parallel start` | Analyze and start parallel workers |
|
||||
| `/gsd parallel status` | Show worker state and progress |
|
||||
| `/gsd parallel stop [MID]` | Stop workers |
|
||||
| `/gsd parallel pause [MID]` | Pause workers |
|
||||
| `/gsd parallel resume [MID]` | Resume workers |
|
||||
| `/gsd parallel merge [MID]` | Merge completed milestones |
|
||||
|
||||
## Workflow Templates
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/gsd start` | Start a workflow template |
|
||||
| `/gsd start resume` | Resume an in-progress workflow |
|
||||
| `/gsd templates` | List available templates |
|
||||
| `/gsd templates info <name>` | Show template details |
|
||||
|
||||
## Custom Workflows
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/gsd workflow new` | Create a workflow definition |
|
||||
| `/gsd workflow run <name>` | Start a workflow run |
|
||||
| `/gsd workflow list` | List workflow runs |
|
||||
| `/gsd workflow validate <name>` | Validate a workflow YAML |
|
||||
| `/gsd workflow pause` | Pause workflow auto-mode |
|
||||
| `/gsd workflow resume` | Resume paused workflow |
|
||||
|
||||
## Extensions
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/gsd extensions list` | List all extensions |
|
||||
| `/gsd extensions enable <id>` | Enable an extension |
|
||||
| `/gsd extensions disable <id>` | Disable an extension |
|
||||
| `/gsd extensions info <id>` | Show extension details |
|
||||
|
||||
## GitHub Sync
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/github-sync bootstrap` | Initial GitHub sync setup |
|
||||
| `/github-sync status` | Show sync mapping counts |
|
||||
|
||||
## Session Management
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/clear` | Start a new session |
|
||||
| `/exit` | Graceful shutdown |
|
||||
| `/model` | Switch the active model |
|
||||
| `/login` | Log in to an LLM provider |
|
||||
| `/thinking` | Toggle thinking level |
|
||||
| `/voice` | Toggle speech-to-text |
|
||||
| `/worktree` (`/wt`) | Git worktree management |
|
||||
|
||||
## In-Session Update
|
||||
|
||||
```
|
||||
/gsd update
|
||||
```
|
||||
|
||||
Checks npm for a newer version and installs it without leaving the session.
|
||||
56
gitbook/reference/environment-variables.md
Normal file
56
gitbook/reference/environment-variables.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
# Environment Variables
|
||||
|
||||
## GSD Configuration
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `GSD_HOME` | `~/.gsd` | Global GSD directory. All paths derive from this unless individually overridden. |
|
||||
| `GSD_PROJECT_ID` | (auto-hash) | Override automatic project identity hash. Useful for CI/CD or sharing state across repo clones. |
|
||||
| `GSD_STATE_DIR` | `$GSD_HOME` | Per-project state root. Controls where `projects/<repo-hash>/` directories are created. |
|
||||
| `GSD_CODING_AGENT_DIR` | `$GSD_HOME/agent` | Agent directory for extensions, auth, and managed resources. |
|
||||
| `GSD_FETCH_ALLOWED_URLS` | (none) | Comma-separated hostnames exempt from internal URL blocking. |
|
||||
| `GSD_ALLOWED_COMMAND_PREFIXES` | (built-in) | Comma-separated command prefixes allowed for value resolution. |
|
||||
| `GSD_WEB_PROJECT_CWD` | — | Default project path for `gsd --web` when `?project=` is not specified. |
|
||||
|
||||
## LLM Provider Keys
|
||||
|
||||
| Variable | Provider |
|
||||
|----------|----------|
|
||||
| `ANTHROPIC_API_KEY` | Anthropic (Claude) |
|
||||
| `OPENAI_API_KEY` | OpenAI |
|
||||
| `GEMINI_API_KEY` | Google Gemini |
|
||||
| `OPENROUTER_API_KEY` | OpenRouter |
|
||||
| `GROQ_API_KEY` | Groq |
|
||||
| `XAI_API_KEY` | xAI (Grok) |
|
||||
| `MISTRAL_API_KEY` | Mistral |
|
||||
| `GH_TOKEN` | GitHub Copilot |
|
||||
| `AWS_PROFILE` | Amazon Bedrock (named profile) |
|
||||
| `AWS_ACCESS_KEY_ID` | Amazon Bedrock (IAM keys) |
|
||||
| `AWS_SECRET_ACCESS_KEY` | Amazon Bedrock (IAM keys) |
|
||||
| `AWS_REGION` | Amazon Bedrock (region) |
|
||||
| `AWS_BEARER_TOKEN_BEDROCK` | Amazon Bedrock (bearer token) |
|
||||
| `ANTHROPIC_VERTEX_PROJECT_ID` | Vertex AI |
|
||||
| `GOOGLE_APPLICATION_CREDENTIALS` | Vertex AI (ADC) |
|
||||
| `AZURE_OPENAI_API_KEY` | Azure OpenAI |
|
||||
|
||||
## Tool API Keys
|
||||
|
||||
| Variable | Purpose |
|
||||
|----------|---------|
|
||||
| `TAVILY_API_KEY` | Tavily web search |
|
||||
| `BRAVE_API_KEY` | Brave web search |
|
||||
| `CONTEXT7_API_KEY` | Context7 documentation lookup |
|
||||
| `DISCORD_BOT_TOKEN` | Discord remote questions |
|
||||
| `TELEGRAM_BOT_TOKEN` | Telegram remote questions |
|
||||
|
||||
## URL Blocking
|
||||
|
||||
The `fetch_page` tool blocks requests to private/internal networks by default (SSRF protection). To allow specific internal hosts:
|
||||
|
||||
```bash
|
||||
export GSD_FETCH_ALLOWED_URLS="internal-docs.company.com,192.168.1.50"
|
||||
```
|
||||
|
||||
Or set `fetchAllowedUrls` in `~/.gsd/agent/settings.json`.
|
||||
|
||||
Blocked by default: private IP ranges, cloud metadata endpoints, localhost, non-HTTP protocols, IPv6 private ranges.
|
||||
33
gitbook/reference/keyboard-shortcuts.md
Normal file
33
gitbook/reference/keyboard-shortcuts.md
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# Keyboard Shortcuts
|
||||
|
||||
| Shortcut | Action |
|
||||
|----------|--------|
|
||||
| `Ctrl+Alt+G` | Toggle dashboard overlay |
|
||||
| `Ctrl+Alt+V` | Toggle voice transcription |
|
||||
| `Ctrl+Alt+B` | Show background shell processes |
|
||||
| `Ctrl+V` / `Alt+V` | Paste image from clipboard (screenshot → vision input) |
|
||||
| `Escape` | Pause auto mode (preserves conversation) |
|
||||
|
||||
## Terminal Compatibility
|
||||
|
||||
In terminals without Kitty keyboard protocol support (macOS Terminal.app, JetBrains IDEs), slash-command fallbacks are shown instead of `Ctrl+Alt` shortcuts.
|
||||
|
||||
{% hint style="tip" %}
|
||||
If `Ctrl+V` is intercepted by your terminal (e.g. Warp), use `Alt+V` instead for clipboard image paste.
|
||||
{% endhint %}
|
||||
|
||||
## iTerm2 Note
|
||||
|
||||
If `Ctrl+Alt` shortcuts trigger the wrong action (e.g., `Ctrl+Alt+G` opens external editor instead of the dashboard), go to **Profiles → Keys → General** and set **Left Option Key** to **Esc+**. This makes Alt/Option work correctly with Ctrl combinations.
|
||||
|
||||
## cmux Integration
|
||||
|
||||
If you use cmux (terminal multiplexer), GSD can integrate with it:
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `/gsd cmux status` | Show cmux detection and capabilities |
|
||||
| `/gsd cmux on` / `off` | Enable/disable integration |
|
||||
| `/gsd cmux notifications on/off` | Toggle desktop notifications |
|
||||
| `/gsd cmux sidebar on/off` | Toggle sidebar metadata |
|
||||
| `/gsd cmux splits on/off` | Toggle visual subagent splits |
|
||||
48
gitbook/reference/migration.md
Normal file
48
gitbook/reference/migration.md
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# Migration from v1
|
||||
|
||||
If you have projects with `.planning` directories from the original Get Shit Done (v1), you can migrate them to GSD-2's `.gsd` format.
|
||||
|
||||
## Running the Migration
|
||||
|
||||
```bash
|
||||
# From within the project directory
|
||||
/gsd migrate
|
||||
|
||||
# Or specify a path
|
||||
/gsd migrate ~/projects/my-old-project
|
||||
```
|
||||
|
||||
## What Gets Migrated
|
||||
|
||||
The migration tool:
|
||||
|
||||
- Parses your old `PROJECT.md`, `ROADMAP.md`, `REQUIREMENTS.md`, phase directories, plans, summaries, and research
|
||||
- Maps phases → slices, plans → tasks, milestones → milestones
|
||||
- Preserves completion state (`[x]` phases stay done, summaries carry over)
|
||||
- Consolidates research files into the new structure
|
||||
- Shows a preview before writing anything
|
||||
- Optionally runs an AI-driven review for quality assurance
|
||||
|
||||
## Supported Formats
|
||||
|
||||
The migration handles various v1 format variations:
|
||||
|
||||
- Milestone-sectioned roadmaps with `<details>` blocks
|
||||
- Bold phase entries
|
||||
- Bullet-format requirements
|
||||
- Decimal phase numbering
|
||||
- Duplicate phase numbers across milestones
|
||||
|
||||
## Requirements
|
||||
|
||||
Migration works best with a `ROADMAP.md` file for milestone structure. Without one, milestones are inferred from the `phases/` directory.
|
||||
|
||||
## Post-Migration
|
||||
|
||||
After migrating, verify the output:
|
||||
|
||||
```
|
||||
/gsd doctor
|
||||
```
|
||||
|
||||
This checks `.gsd/` integrity and flags any structural issues.
|
||||
151
gitbook/reference/troubleshooting.md
Normal file
151
gitbook/reference/troubleshooting.md
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
# Troubleshooting
|
||||
|
||||
## `/gsd doctor`
|
||||
|
||||
The built-in diagnostic tool validates `.gsd/` integrity:
|
||||
|
||||
```
|
||||
/gsd doctor
|
||||
```
|
||||
|
||||
It checks file structure, roadmap ↔ slice ↔ task consistency, completion state, git health, stale locks, and orphaned records.
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Auto mode loops on the same unit
|
||||
|
||||
The same unit dispatches repeatedly.
|
||||
|
||||
**Fix:** Run `/gsd doctor` to repair state, then `/gsd auto`. If it persists, check that the expected artifact file exists on disk.
|
||||
|
||||
### Auto mode stops with "Loop detected"
|
||||
|
||||
A unit failed to produce its expected artifact twice.
|
||||
|
||||
**Fix:** Check the task plan for clarity. Refine it manually, then `/gsd auto`.
|
||||
|
||||
### `command not found: gsd` after install
|
||||
|
||||
npm's global bin directory isn't in `$PATH`.
|
||||
|
||||
**Fix:**
|
||||
```bash
|
||||
npm prefix -g
|
||||
# Add the bin dir to PATH:
|
||||
echo 'export PATH="$(npm prefix -g)/bin:$PATH"' >> ~/.zshrc
|
||||
source ~/.zshrc
|
||||
```
|
||||
|
||||
**Common causes:**
|
||||
- **Homebrew Node** — `/opt/homebrew/bin` missing from PATH
|
||||
- **Version manager (nvm, fnm, mise)** — global bin is version-specific
|
||||
- **oh-my-zsh** — `gitfast` plugin aliases `gsd` to `git svn dcommit`; check with `alias gsd`
|
||||
|
||||
### Provider errors during auto mode
|
||||
|
||||
| Error Type | Auto-Resume? | Delay |
|
||||
|-----------|-------------|-------|
|
||||
| Rate limit (429) | Yes | 60s or retry-after header |
|
||||
| Server error (500, 502, 503) | Yes | 30s |
|
||||
| Auth/billing ("unauthorized") | No | Manual resume required |
|
||||
|
||||
For permanent errors, configure fallback models:
|
||||
|
||||
```yaml
|
||||
models:
|
||||
execution:
|
||||
model: claude-sonnet-4-6
|
||||
fallbacks:
|
||||
- openrouter/minimax/minimax-m2.5
|
||||
```
|
||||
|
||||
### Budget ceiling reached
|
||||
|
||||
Auto mode pauses with "Budget ceiling reached."
|
||||
|
||||
**Fix:** Increase `budget_ceiling` in preferences, or switch to `budget` token profile, then `/gsd auto`.
|
||||
|
||||
### Stale lock file
|
||||
|
||||
Auto mode won't start, says another session is running.
|
||||
|
||||
**Fix:** GSD auto-detects stale locks (dead PID = auto cleanup). If automatic recovery fails:
|
||||
|
||||
```bash
|
||||
rm -f .gsd/auto.lock
|
||||
rm -rf "$(dirname .gsd)/.gsd.lock"
|
||||
```
|
||||
|
||||
### Git merge conflicts
|
||||
|
||||
Worktree merge fails on `.gsd/` files.
|
||||
|
||||
**Fix:** `.gsd/` conflicts are auto-resolved. Code conflicts get an AI fix attempt; if that fails, resolve manually.
|
||||
|
||||
### Notifications not appearing on macOS
|
||||
|
||||
**Fix:** Install `terminal-notifier`:
|
||||
|
||||
```bash
|
||||
brew install terminal-notifier
|
||||
```
|
||||
|
||||
See [Notifications](../configuration/notifications.md) for details.
|
||||
|
||||
## MCP Issues
|
||||
|
||||
### No servers configured
|
||||
|
||||
**Fix:** Add server to `.mcp.json` or `.gsd/mcp.json`, verify JSON is valid, run `mcp_servers(refresh=true)`.
|
||||
|
||||
### Server discovery times out
|
||||
|
||||
**Fix:** Run the configured command outside GSD to confirm it starts. Check that backend services are reachable.
|
||||
|
||||
### Server connection closed immediately
|
||||
|
||||
**Fix:** Verify `command` and `args` paths are correct and absolute. Run the command manually to catch errors.
|
||||
|
||||
## Recovery Procedures
|
||||
|
||||
### Reset auto mode state
|
||||
|
||||
```bash
|
||||
rm .gsd/auto.lock
|
||||
rm .gsd/completed-units.json
|
||||
```
|
||||
|
||||
Then `/gsd auto` to restart from current state.
|
||||
|
||||
### Reset routing history
|
||||
|
||||
```bash
|
||||
rm .gsd/routing-history.json
|
||||
```
|
||||
|
||||
### Full state rebuild
|
||||
|
||||
```
|
||||
/gsd doctor
|
||||
```
|
||||
|
||||
Rebuilds `STATE.md` from plan and roadmap files and fixes inconsistencies.
|
||||
|
||||
## Getting Help
|
||||
|
||||
- **GitHub Issues:** [github.com/gsd-build/GSD-2/issues](https://github.com/gsd-build/GSD-2/issues)
|
||||
- **Dashboard:** `Ctrl+Alt+G` or `/gsd status`
|
||||
- **Forensics:** `/gsd forensics` for post-mortem analysis
|
||||
- **Session logs:** `.gsd/activity/` contains JSONL session dumps
|
||||
|
||||
## Platform-Specific Issues
|
||||
|
||||
### iTerm2
|
||||
|
||||
`Ctrl+Alt` shortcuts trigger wrong actions → Set **Profiles → Keys → General → Left Option Key** to **Esc+**.
|
||||
|
||||
### Windows
|
||||
|
||||
- LSP ENOENT on MSYS2/Git Bash → Fixed in v2.29+, upgrade
|
||||
- EBUSY errors during builds → Close browser extension, or change output directory
|
||||
- Transient EBUSY/EPERM on `.gsd/` files → Retry; close file-locking tools if persistent
|
||||
|
|
@ -77,6 +77,7 @@
|
|||
{
|
||||
"group": "Features",
|
||||
"pages": [
|
||||
"guides/change-management",
|
||||
"guides/captures-triage",
|
||||
"guides/parallel-orchestration",
|
||||
"guides/remote-questions",
|
||||
|
|
|
|||
211
mintlify-docs/guides/change-management.mdx
Normal file
211
mintlify-docs/guides/change-management.mdx
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
---
|
||||
title: "Change management"
|
||||
description: "How to handle bugs, new features, and roadmap reshuffling after milestones are underway."
|
||||
---
|
||||
|
||||
Reality diverges from plans. This guide covers every situation where you need to act on something discovered after work is already in flight — from a single bug fix to inserting a whole new milestone before the next planned one.
|
||||
|
||||
## Mental model
|
||||
|
||||
GSD's pipeline looks like this:
|
||||
|
||||
```
|
||||
M001 complete → M002 complete → M003 → M004 → ...
|
||||
```
|
||||
|
||||
The key invariant: **completed units are sealed.** A finished task, slice, or milestone can't be un-completed. What you can always do is add new work ahead of or after any remaining unit.
|
||||
|
||||
Between milestones you have the most freedom. Inside a running milestone you have real steering options without breaking the state machine.
|
||||
|
||||
---
|
||||
|
||||
## Small bug or quick fix
|
||||
|
||||
**A self-contained fix that can be described in a sentence.**
|
||||
|
||||
```
|
||||
/gsd quick "fix the date formatting bug in the invoice renderer"
|
||||
```
|
||||
|
||||
`/gsd quick` executes immediately with full GSD guarantees (atomic commit, state tracking) but skips milestone ceremony. It doesn't touch the milestone pipeline.
|
||||
|
||||
---
|
||||
|
||||
## Bug or idea discovered while auto-mode is running
|
||||
|
||||
**You spot something mid-execution but don't want to interrupt the run.**
|
||||
|
||||
```
|
||||
/gsd capture "the login redirect is broken on mobile viewports"
|
||||
/gsd capture "add a loading spinner to the data table"
|
||||
```
|
||||
|
||||
Captures are appended to `.gsd/CAPTURES.md` and triaged automatically at natural seams between tasks. See [captures and triage](/guides/captures-triage) for the full classification system.
|
||||
|
||||
To force processing immediately:
|
||||
|
||||
```
|
||||
/gsd triage
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Current slice plan is wrong
|
||||
|
||||
**You're mid-slice and the plan no longer makes sense — wrong approach, missing step, or a blocker.**
|
||||
|
||||
```
|
||||
/gsd steer
|
||||
```
|
||||
|
||||
This opens an interactive session to hard-edit plan documents. Changes are picked up at the next phase boundary without stopping auto-mode.
|
||||
|
||||
For structural changes (adding tasks, removing tasks), the agent triggers a slice replan internally when it discovers a blocker. Completed tasks are protected — only pending tasks can be mutated.
|
||||
|
||||
---
|
||||
|
||||
## Bugs and features need to land before the next milestone
|
||||
|
||||
**M001 and M002 are done. You've found bugs and have new features that must ship before M003 can proceed.**
|
||||
|
||||
<Steps>
|
||||
<Step title="Triage your list">
|
||||
Separate into: bugs vs. features, and must-before-M003 vs. can-wait.
|
||||
</Step>
|
||||
<Step title="Create a new milestone for the pre-M003 work">
|
||||
```
|
||||
/gsd new-milestone
|
||||
```
|
||||
Describe the bugs and features. GSD creates a milestone — the title is what matters, not the number.
|
||||
</Step>
|
||||
<Step title="Check and reorder the queue">
|
||||
```
|
||||
/gsd queue
|
||||
```
|
||||
Confirm the new milestone is queued before M003. Reorder if needed.
|
||||
</Step>
|
||||
<Step title="Park M003 if it shouldn't run yet">
|
||||
```
|
||||
/gsd park M003
|
||||
```
|
||||
Parking skips M003 without deleting it. Unpark when ready:
|
||||
```
|
||||
/gsd unpark M003
|
||||
```
|
||||
</Step>
|
||||
<Step title="Run auto-mode">
|
||||
```
|
||||
/gsd auto
|
||||
```
|
||||
Auto-mode dispatches the next active milestone in queue order.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
---
|
||||
|
||||
## Modifying a not-yet-started milestone
|
||||
|
||||
**You want to change M003's scope — add slices, remove slices, change the approach — before it starts.**
|
||||
|
||||
Since M003 hasn't started, its plan files can be edited directly. Use `/gsd discuss` to talk through the changes and let GSD rewrite the artifacts:
|
||||
|
||||
```
|
||||
/gsd discuss
|
||||
```
|
||||
|
||||
> "M003 needs to include the new auth flow we discovered. Can we add a slice for that and remove the old token refresh slice?"
|
||||
|
||||
Or use `/gsd steer` to edit plan files directly.
|
||||
|
||||
If M003 is partially done (some slices complete), auto-mode calls `reassess-roadmap` automatically after each slice. You can also discuss changes during a pause — GSD can add, modify, or remove pending slices without touching the completed ones.
|
||||
|
||||
---
|
||||
|
||||
## A milestone needs to step back one position
|
||||
|
||||
**Your "Milestone 3" is effectively now "Milestone 4" because new work must insert before it.**
|
||||
|
||||
GSD milestone numbers are labels, not positions. Execution order is controlled by the queue, not the ID. The procedure is the same as above: create the new milestone, confirm queue order with `/gsd queue`, park M003 if needed.
|
||||
|
||||
The milestone IDs stay as-is — M003 just executes later. No renumbering needed.
|
||||
|
||||
---
|
||||
|
||||
## Many bugs — worth a dedicated bugfix milestone
|
||||
|
||||
**After M002 you have 10+ bugs across multiple systems. Too scattered for individual quick tasks.**
|
||||
|
||||
```
|
||||
/gsd new-milestone
|
||||
```
|
||||
|
||||
Describe the full bug list. GSD creates a milestone with slices organized by system or severity. Run it in auto-mode like any other milestone. When done, all bugs land as clean commits with a formal milestone summary — readable as a bugfix release.
|
||||
|
||||
---
|
||||
|
||||
## Feature ideas that can wait
|
||||
|
||||
**Real ideas, but nothing that blocks the current plan.**
|
||||
|
||||
```
|
||||
/gsd capture "dark mode toggle on the dashboard"
|
||||
```
|
||||
|
||||
Deferred captures surface during roadmap reassessment. GSD can fold them into a later milestone when the timing makes sense. Or queue a dedicated features milestone directly:
|
||||
|
||||
```
|
||||
/gsd queue
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Bug in a completed slice
|
||||
|
||||
**A shipped slice has a bug but it's already sealed.**
|
||||
|
||||
You cannot un-complete the slice. Options:
|
||||
|
||||
- `/gsd quick` for small fixes
|
||||
- A new slice in the next milestone that explicitly patches the bug — reference the original slice in the description
|
||||
- `/gsd steer` to add a fix task to the current active milestone if you're still inside it
|
||||
|
||||
The completed slice record is preserved as-is. The fix lands as new work with its own commit and summary.
|
||||
|
||||
---
|
||||
|
||||
## An entire milestone went in the wrong direction
|
||||
|
||||
**M002 is done but the approach was wrong, and M003 builds on it.**
|
||||
|
||||
<Steps>
|
||||
<Step title="Discuss the situation">
|
||||
```
|
||||
/gsd discuss
|
||||
```
|
||||
Work through what's wrong and what the correction looks like before touching anything.
|
||||
</Step>
|
||||
<Step title="Create a correction milestone">
|
||||
A focused "M002b" or "M002-fix" that refactors or replaces what went wrong. Scope it precisely to the broken parts.
|
||||
</Step>
|
||||
<Step title="Revise M003 if needed">
|
||||
If M003 doesn't depend on the broken parts, trim its scope so it doesn't compound the problem. Fix fully in the correction milestone first.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
---
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Situation | Command |
|
||||
|---|---|
|
||||
| Small self-contained fix | `/gsd quick` |
|
||||
| Thought during auto-mode | `/gsd capture` |
|
||||
| Force-process captures now | `/gsd triage` |
|
||||
| Current slice plan is wrong | `/gsd steer` |
|
||||
| New work must land before next milestone | `/gsd new-milestone` + `/gsd queue` |
|
||||
| Delay a future milestone | `/gsd park <MID>` / `/gsd unpark <MID>` |
|
||||
| Modify a not-yet-started milestone | `/gsd discuss` or `/gsd steer` |
|
||||
| Many bugs → dedicated milestone | `/gsd new-milestone` (bugfix scope) |
|
||||
| Ideas that can wait | `/gsd capture` or `/gsd queue` |
|
||||
| Check/reorder pipeline | `/gsd queue` |
|
||||
| Architecture discussion | `/gsd discuss` |
|
||||
|
|
@ -26,19 +26,24 @@ Share planning artifacts while keeping runtime files local:
|
|||
```bash
|
||||
# Runtime / ephemeral (per-developer)
|
||||
.gsd/auto.lock
|
||||
.gsd/completed-units.json
|
||||
.gsd/completed-units*.json
|
||||
.gsd/state-manifest.json
|
||||
.gsd/STATE.md
|
||||
.gsd/metrics.json
|
||||
.gsd/activity/
|
||||
.gsd/runtime/
|
||||
.gsd/worktrees/
|
||||
.gsd/gsd.db*
|
||||
.gsd/journal/
|
||||
.gsd/doctor-history.jsonl
|
||||
.gsd/event-log.jsonl
|
||||
.gsd/milestones/**/continue.md
|
||||
.gsd/milestones/**/*-CONTINUE.md
|
||||
```
|
||||
|
||||
**Shared** (committed): preferences, PROJECT.md, REQUIREMENTS.md, DECISIONS.md, milestones.
|
||||
|
||||
**Local** (gitignored): lock files, metrics, state cache, worktrees, activity logs.
|
||||
**Local** (gitignored): lock files, metrics, state cache, worktrees, activity logs, database files, journals.
|
||||
|
||||
### 3. Commit
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@gsd-build/engine-darwin-arm64",
|
||||
"version": "2.68.0",
|
||||
"version": "2.73.0",
|
||||
"description": "GSD native engine binary for macOS ARM64",
|
||||
"os": [
|
||||
"darwin"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@gsd-build/engine-darwin-x64",
|
||||
"version": "2.68.0",
|
||||
"version": "2.73.0",
|
||||
"description": "GSD native engine binary for macOS Intel",
|
||||
"os": [
|
||||
"darwin"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@gsd-build/engine-linux-arm64-gnu",
|
||||
"version": "2.68.0",
|
||||
"version": "2.73.0",
|
||||
"description": "GSD native engine binary for Linux ARM64 (glibc)",
|
||||
"os": [
|
||||
"linux"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@gsd-build/engine-linux-x64-gnu",
|
||||
"version": "2.68.0",
|
||||
"version": "2.73.0",
|
||||
"description": "GSD native engine binary for Linux x64 (glibc)",
|
||||
"os": [
|
||||
"linux"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@gsd-build/engine-win32-x64-msvc",
|
||||
"version": "2.68.0",
|
||||
"version": "2.73.0",
|
||||
"description": "GSD native engine binary for Windows x64 (MSVC)",
|
||||
"os": [
|
||||
"win32"
|
||||
|
|
|
|||
6
package-lock.json
generated
6
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "gsd-pi",
|
||||
"version": "2.66.1",
|
||||
"version": "2.72.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "gsd-pi",
|
||||
"version": "2.66.1",
|
||||
"version": "2.72.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
|
|
@ -9535,7 +9535,7 @@
|
|||
},
|
||||
"packages/pi-coding-agent": {
|
||||
"name": "@gsd/pi-coding-agent",
|
||||
"version": "2.66.1",
|
||||
"version": "2.72.0",
|
||||
"dependencies": {
|
||||
"@mariozechner/jiti": "^2.6.2",
|
||||
"@silvia-odwyer/photon-node": "^0.3.4",
|
||||
|
|
|
|||
22
package.json
22
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "gsd-pi",
|
||||
"version": "2.68.0",
|
||||
"version": "2.73.0",
|
||||
"description": "GSD — Get Shit Done coding agent",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
|
@ -56,22 +56,22 @@
|
|||
"copy-themes": "node scripts/copy-themes.cjs",
|
||||
"copy-export-html": "node scripts/copy-export-html.cjs",
|
||||
"test:compile": "node scripts/compile-tests.mjs",
|
||||
"test:unit": "npm run test:compile && node --import ./scripts/dist-test-resolve.mjs --experimental-test-isolation=process --test-reporter=./scripts/test-reporter-compact.mjs --test 'dist-test/src/tests/*.test.js' 'dist-test/src/resources/extensions/gsd/tests/*.test.js' 'dist-test/src/resources/extensions/gsd/tests/*.test.mjs' 'dist-test/src/resources/extensions/shared/tests/*.test.js' 'dist-test/src/resources/extensions/claude-code-cli/tests/*.test.js' 'dist-test/src/resources/extensions/github-sync/tests/*.test.js' 'dist-test/src/resources/extensions/universal-config/tests/*.test.js' 'dist-test/src/resources/extensions/voice/tests/*.test.js' 'dist-test/src/resources/extensions/mcp-client/tests/*.test.js'",
|
||||
"test:packages": "node --test packages/pi-coding-agent/dist/core/*.test.js",
|
||||
"test:marketplace": "GSD_TEST_CLONE_MARKETPLACES=1 node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/claude-import-tui.test.ts src/resources/extensions/gsd/tests/plugin-importer-live.test.ts src/tests/marketplace-discovery.test.ts",
|
||||
"test:coverage": "c8 --reporter=text --reporter=lcov --exclude='src/resources/extensions/gsd/tests/**' --exclude='src/tests/**' --exclude='scripts/**' --exclude='native/**' --exclude='node_modules/**' --check-coverage --statements=40 --lines=40 --branches=20 --functions=20 node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --experimental-test-isolation=process --test src/resources/extensions/gsd/tests/*.test.ts src/resources/extensions/gsd/tests/*.test.mjs src/tests/*.test.ts src/resources/extensions/shared/tests/*.test.ts",
|
||||
"test:integration": "node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test 'src/tests/integration/*.test.ts' 'src/resources/extensions/gsd/tests/integration/*.test.ts' 'src/resources/extensions/async-jobs/*.test.ts' 'src/resources/extensions/browser-tools/tests/*.test.mjs'",
|
||||
"test:unit": "npm run test:compile && node --import ./scripts/dist-test-resolve.mjs --experimental-test-isolation=process --test-reporter=./scripts/test-reporter-compact.mjs --test \"dist-test/src/tests/*.test.js\" \"dist-test/src/resources/extensions/gsd/tests/*.test.js\" \"dist-test/src/resources/extensions/gsd/tests/*.test.mjs\" \"dist-test/src/resources/extensions/shared/tests/*.test.js\" \"dist-test/src/resources/extensions/claude-code-cli/tests/*.test.js\" \"dist-test/src/resources/extensions/github-sync/tests/*.test.js\" \"dist-test/src/resources/extensions/universal-config/tests/*.test.js\" \"dist-test/src/resources/extensions/voice/tests/*.test.js\" \"dist-test/src/resources/extensions/mcp-client/tests/*.test.js\"",
|
||||
"test:packages": "node --test packages/pi-coding-agent/dist/core/*.test.js packages/pi-coding-agent/dist/core/tools/spawn-shell-windows.test.js",
|
||||
"test:marketplace": "node scripts/with-env.mjs GSD_TEST_CLONE_MARKETPLACES=1 -- node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/claude-import-tui.test.ts src/resources/extensions/gsd/tests/plugin-importer-live.test.ts src/tests/marketplace-discovery.test.ts",
|
||||
"test:coverage": "c8 --reporter=text --reporter=lcov --exclude=\"src/resources/extensions/gsd/tests/**\" --exclude=\"src/tests/**\" --exclude=\"scripts/**\" --exclude=\"native/**\" --exclude=\"node_modules/**\" --check-coverage --statements=40 --lines=40 --branches=20 --functions=20 node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --experimental-test-isolation=process --test src/resources/extensions/gsd/tests/*.test.ts src/resources/extensions/gsd/tests/*.test.mjs src/tests/*.test.ts src/resources/extensions/shared/tests/*.test.ts",
|
||||
"test:integration": "node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test \"src/tests/integration/*.test.ts\" \"src/resources/extensions/gsd/tests/integration/*.test.ts\" \"src/resources/extensions/async-jobs/*.test.ts\" \"src/resources/extensions/browser-tools/tests/*.test.mjs\"",
|
||||
"pretest": "npm run typecheck:extensions",
|
||||
"test": "npm run test:unit && npm run test:integration",
|
||||
"test:smoke": "node --experimental-strip-types tests/smoke/run.ts",
|
||||
"test:fixtures": "node --experimental-strip-types tests/fixtures/run.ts",
|
||||
"test:fixtures:record": "GSD_FIXTURE_MODE=record node --experimental-strip-types tests/fixtures/record.ts",
|
||||
"test:live": "GSD_LIVE_TESTS=1 node --experimental-strip-types tests/live/run.ts",
|
||||
"test:fixtures:record": "node scripts/with-env.mjs GSD_FIXTURE_MODE=record -- node --experimental-strip-types tests/fixtures/record.ts",
|
||||
"test:live": "node scripts/with-env.mjs GSD_LIVE_TESTS=1 -- node --experimental-strip-types tests/live/run.ts",
|
||||
"test:browser-tools": "node --test src/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs src/resources/extensions/browser-tools/tests/browser-tools-integration.test.mjs",
|
||||
"test:native": "node --test packages/native/src/__tests__/grep.test.mjs",
|
||||
"test:secret-scan": "node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/tests/secret-scan.test.ts",
|
||||
"secret-scan": "bash scripts/secret-scan.sh",
|
||||
"secret-scan:install-hook": "bash scripts/install-hooks.sh",
|
||||
"secret-scan": "node scripts/secret-scan.mjs",
|
||||
"secret-scan:install-hook": "node scripts/install-hooks.mjs",
|
||||
"build:native": "node native/scripts/build.js",
|
||||
"build:native:dev": "node native/scripts/build.js --dev",
|
||||
"dev": "node scripts/dev.js",
|
||||
|
|
@ -92,7 +92,7 @@
|
|||
"release:update-changelog": "node scripts/update-changelog.mjs",
|
||||
"docker:build-runtime": "docker build --target runtime -t ghcr.io/gsd-build/gsd-pi .",
|
||||
"docker:build-builder": "docker build --target builder -t ghcr.io/gsd-build/gsd-ci-builder .",
|
||||
"prepublishOnly": "npm run sync-pkg-version && npm run sync-platform-versions && ([ \"$CI\" = 'true' ] || git diff --exit-code || (echo 'ERROR: version sync changed files — commit them before publishing' && exit 1)) && npm run build && npm run typecheck:extensions && npm run validate-pack",
|
||||
"prepublishOnly": "npm run sync-pkg-version && npm run sync-platform-versions && node scripts/prepublish-check.mjs && npm run build && npm run typecheck:extensions && npm run validate-pack",
|
||||
"test:live-regression": "node --experimental-strip-types tests/live-regression/run.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -12,9 +12,6 @@
|
|||
*/
|
||||
|
||||
import { z } from 'zod';
|
||||
import { readFileSync, writeFileSync, chmodSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
import type Anthropic from '@anthropic-ai/sdk';
|
||||
import type {
|
||||
MessageParam,
|
||||
|
|
@ -30,90 +27,18 @@ import type { ProjectInfo, ManagedSession } from './types.js';
|
|||
import type { Logger } from './logger.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// OAuth token resolution — reads GSD's auth.json, refreshes if expired
|
||||
// API key resolution — requires ANTHROPIC_API_KEY env var
|
||||
// Anthropic OAuth removed per TOS compliance (see docs/user-docs/claude-code-auth-compliance.md)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface OAuthCredentials {
|
||||
type: 'oauth';
|
||||
refresh: string;
|
||||
access: string;
|
||||
expires: number;
|
||||
}
|
||||
|
||||
const TOKEN_URL = 'https://platform.claude.com/v1/oauth/token';
|
||||
const CLIENT_ID = atob('OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl');
|
||||
|
||||
/**
|
||||
* Read the Anthropic OAuth access token from GSD's auth.json.
|
||||
* If expired, refresh it and write the new credentials back.
|
||||
* Falls back to ANTHROPIC_API_KEY env var if no OAuth credential exists.
|
||||
*/
|
||||
async function resolveAnthropicApiKey(logger?: Logger): Promise<string> {
|
||||
// Try env var first (explicit override)
|
||||
if (process.env.ANTHROPIC_API_KEY) {
|
||||
return process.env.ANTHROPIC_API_KEY;
|
||||
}
|
||||
|
||||
const authPath = join(homedir(), '.gsd', 'agent', 'auth.json');
|
||||
let authData: Record<string, unknown>;
|
||||
try {
|
||||
authData = JSON.parse(readFileSync(authPath, 'utf-8'));
|
||||
} catch {
|
||||
function resolveAnthropicApiKey(): string {
|
||||
const apiKey = process.env.ANTHROPIC_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error(
|
||||
'No Anthropic auth found. Run `gsd login` to authenticate, or set ANTHROPIC_API_KEY.',
|
||||
'ANTHROPIC_API_KEY is required. Set it in your environment or run `gsd config`.',
|
||||
);
|
||||
}
|
||||
|
||||
const cred = authData.anthropic as OAuthCredentials | undefined;
|
||||
if (!cred || cred.type !== 'oauth' || !cred.access) {
|
||||
throw new Error(
|
||||
'No Anthropic OAuth credential in auth.json. Run `gsd login` to authenticate.',
|
||||
);
|
||||
}
|
||||
|
||||
// If token is still valid, use it
|
||||
if (Date.now() < cred.expires) {
|
||||
return cred.access;
|
||||
}
|
||||
|
||||
// Token expired — refresh it
|
||||
logger?.info('orchestrator: refreshing Anthropic OAuth token');
|
||||
const response = await fetch(TOKEN_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
grant_type: 'refresh_token',
|
||||
client_id: CLIENT_ID,
|
||||
refresh_token: cred.refresh,
|
||||
}),
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`Anthropic token refresh failed: ${error}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
expires_in: number;
|
||||
};
|
||||
|
||||
const newCred: OAuthCredentials = {
|
||||
type: 'oauth',
|
||||
refresh: data.refresh_token,
|
||||
access: data.access_token,
|
||||
expires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,
|
||||
};
|
||||
|
||||
// Write back to auth.json
|
||||
authData.anthropic = newCred;
|
||||
writeFileSync(authPath, JSON.stringify(authData, null, 2), 'utf-8');
|
||||
chmodSync(authPath, 0o600);
|
||||
logger?.info('orchestrator: Anthropic OAuth token refreshed');
|
||||
|
||||
return newCred.access;
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -254,11 +179,11 @@ export class Orchestrator {
|
|||
|
||||
/**
|
||||
* Lazily initialise the Anthropic client. Dynamic import handles K007 module resolution.
|
||||
* Resolves auth from GSD's OAuth credentials (auth.json), refreshing if needed.
|
||||
* Requires ANTHROPIC_API_KEY environment variable.
|
||||
*/
|
||||
private async getClient(): Promise<Anthropic> {
|
||||
if (this.client) return this.client;
|
||||
const apiKey = await resolveAnthropicApiKey(this.deps.logger);
|
||||
const apiKey = resolveAnthropicApiKey();
|
||||
const { default: AnthropicSDK } = await import('@anthropic-ai/sdk');
|
||||
this.client = new AnthropicSDK({ apiKey });
|
||||
return this.client;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ Start GSD auto-mode sessions, poll progress, resolve blockers, and retrieve resu
|
|||
This package now exposes two tool surfaces:
|
||||
|
||||
- session/read tools for starting and inspecting GSD sessions
|
||||
- workflow mutation tools for planning, completion, validation, reassessment, and gate persistence
|
||||
- MCP-native interactive tools for structured user input
|
||||
- headless-safe workflow tools for planning, completion, validation, reassessment, metadata persistence, and journal reads
|
||||
|
||||
## Installation
|
||||
|
||||
|
|
@ -74,18 +75,29 @@ Add to `.cursor/mcp.json`:
|
|||
|
||||
## Tools
|
||||
|
||||
### Workflow mutation tools
|
||||
### Workflow tools
|
||||
|
||||
The workflow MCP surface includes:
|
||||
|
||||
- `gsd_decision_save`
|
||||
- `gsd_save_decision`
|
||||
- `gsd_requirement_update`
|
||||
- `gsd_update_requirement`
|
||||
- `gsd_requirement_save`
|
||||
- `gsd_save_requirement`
|
||||
- `gsd_milestone_generate_id`
|
||||
- `gsd_generate_milestone_id`
|
||||
- `gsd_plan_milestone`
|
||||
- `gsd_plan_slice`
|
||||
- `gsd_plan_task`
|
||||
- `gsd_task_plan`
|
||||
- `gsd_replan_slice`
|
||||
- `gsd_slice_replan`
|
||||
- `gsd_task_complete`
|
||||
- `gsd_complete_task`
|
||||
- `gsd_slice_complete`
|
||||
- `gsd_complete_slice`
|
||||
- `gsd_skip_slice`
|
||||
- `gsd_validate_milestone`
|
||||
- `gsd_milestone_validate`
|
||||
- `gsd_complete_milestone`
|
||||
|
|
@ -95,13 +107,21 @@ The workflow MCP surface includes:
|
|||
- `gsd_save_gate_result`
|
||||
- `gsd_summary_save`
|
||||
- `gsd_milestone_status`
|
||||
- `gsd_journal_query`
|
||||
|
||||
These mutation tools use the same GSD workflow handlers as the native in-process tool path.
|
||||
These tools use the same GSD workflow handlers as the native in-process tool path wherever a shared handler exists.
|
||||
|
||||
### Interactive tools
|
||||
|
||||
The packaged server now exposes `ask_user_questions` through MCP form elicitation. This keeps the existing GSD answer payload shape while allowing Claude Code CLI and other elicitation-capable clients to surface structured user choices.
|
||||
|
||||
`secure_env_collect` is still not exposed by this package. That path needs MCP URL elicitation or an equivalent secure bridge because secrets should not flow through form elicitation.
|
||||
|
||||
Current support boundary:
|
||||
|
||||
- when running inside the GSD monorepo checkout, the MCP server auto-discovers the shared workflow executor module
|
||||
- outside the monorepo, set `GSD_WORKFLOW_EXECUTORS_MODULE` to an importable `workflow-tool-executors` module path if you want the mutation tools enabled
|
||||
- `ask_user_questions` requires an MCP client that supports form elicitation
|
||||
- session/read tools do not depend on this bridge
|
||||
|
||||
If the executor bridge cannot be loaded, workflow mutation calls will fail with a precise configuration error instead of silently degrading.
|
||||
|
|
@ -214,6 +234,8 @@ Resolve a pending blocker in a session by sending a response to the blocked UI r
|
|||
| `GSD_CLI_PATH` | Absolute path to the GSD CLI binary. If not set, the server resolves `gsd` via `which`. |
|
||||
| `GSD_WORKFLOW_EXECUTORS_MODULE` | Optional absolute path or `file:` URL for the shared GSD workflow executor module used by workflow mutation tools. |
|
||||
|
||||
The server also hydrates supported model-provider and tool credentials from `~/.gsd/agent/auth.json` on startup. Keys saved through `/gsd config` or `/gsd keys` become available to the MCP server process automatically, and any explicitly-set environment variable still wins.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* @gsd-build/mcp-server CLI — stdio transport entry point.
|
||||
*
|
||||
|
|
@ -9,13 +7,17 @@
|
|||
|
||||
import { SessionManager } from './session-manager.js';
|
||||
import { createMcpServer } from './server.js';
|
||||
import { loadStoredCredentialEnvKeys } from './tool-credentials.js';
|
||||
|
||||
const MCP_PKG = '@modelcontextprotocol/sdk';
|
||||
|
||||
async function main(): Promise<void> {
|
||||
loadStoredCredentialEnvKeys();
|
||||
|
||||
const sessionManager = new SessionManager();
|
||||
|
||||
// Create the configured MCP server with all 12 tools (6 session + 6 read-only)
|
||||
// Create the configured MCP server with session, interactive, read-only,
|
||||
// and workflow tools.
|
||||
const { server } = await createMcpServer(sessionManager);
|
||||
|
||||
// Dynamic import for StdioServerTransport (same TS subpath workaround)
|
||||
|
|
|
|||
280
packages/mcp-server/src/env-writer.test.ts
Normal file
280
packages/mcp-server/src/env-writer.test.ts
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
// @gsd-build/mcp-server — Tests for env-writer utilities
|
||||
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
||||
|
||||
import { describe, it, afterEach } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import {
|
||||
checkExistingEnvKeys,
|
||||
detectDestination,
|
||||
writeEnvKey,
|
||||
applySecrets,
|
||||
isSafeEnvVarKey,
|
||||
isSupportedDeploymentEnvironment,
|
||||
shellEscapeSingle,
|
||||
} from './env-writer.js';
|
||||
|
||||
function makeTempDir(prefix: string): string {
|
||||
return mkdtempSync(join(tmpdir(), `${prefix}-`));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// checkExistingEnvKeys
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('checkExistingEnvKeys', () => {
|
||||
it('finds key in .env file', async () => {
|
||||
const tmp = makeTempDir('env-check');
|
||||
try {
|
||||
const envPath = join(tmp, '.env');
|
||||
writeFileSync(envPath, 'API_KEY=secret123\nOTHER=val\n');
|
||||
const result = await checkExistingEnvKeys(['API_KEY'], envPath);
|
||||
assert.deepStrictEqual(result, ['API_KEY']);
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('finds key in process.env', async () => {
|
||||
const tmp = makeTempDir('env-check');
|
||||
const saved = process.env.GSD_MCP_TEST_KEY_1;
|
||||
try {
|
||||
process.env.GSD_MCP_TEST_KEY_1 = 'some-value';
|
||||
const envPath = join(tmp, '.env');
|
||||
const result = await checkExistingEnvKeys(['GSD_MCP_TEST_KEY_1'], envPath);
|
||||
assert.deepStrictEqual(result, ['GSD_MCP_TEST_KEY_1']);
|
||||
} finally {
|
||||
delete process.env.GSD_MCP_TEST_KEY_1;
|
||||
if (saved !== undefined) process.env.GSD_MCP_TEST_KEY_1 = saved;
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('returns empty for missing keys', async () => {
|
||||
const tmp = makeTempDir('env-check');
|
||||
try {
|
||||
const envPath = join(tmp, '.env');
|
||||
writeFileSync(envPath, 'OTHER=val\n');
|
||||
delete process.env.DEFINITELY_NOT_SET_MCP_XYZ;
|
||||
const result = await checkExistingEnvKeys(['DEFINITELY_NOT_SET_MCP_XYZ'], envPath);
|
||||
assert.deepStrictEqual(result, []);
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('handles missing .env file gracefully', async () => {
|
||||
const tmp = makeTempDir('env-check');
|
||||
try {
|
||||
const envPath = join(tmp, 'nonexistent.env');
|
||||
delete process.env.DEFINITELY_NOT_SET_MCP_XYZ;
|
||||
const result = await checkExistingEnvKeys(['DEFINITELY_NOT_SET_MCP_XYZ'], envPath);
|
||||
assert.deepStrictEqual(result, []);
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// detectDestination
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('detectDestination', () => {
|
||||
it('returns vercel when vercel.json exists', () => {
|
||||
const tmp = makeTempDir('dest');
|
||||
try {
|
||||
writeFileSync(join(tmp, 'vercel.json'), '{}');
|
||||
assert.equal(detectDestination(tmp), 'vercel');
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('returns convex when convex/ dir exists', () => {
|
||||
const tmp = makeTempDir('dest');
|
||||
try {
|
||||
mkdirSync(join(tmp, 'convex'));
|
||||
assert.equal(detectDestination(tmp), 'convex');
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('returns dotenv when neither exists', () => {
|
||||
const tmp = makeTempDir('dest');
|
||||
try {
|
||||
assert.equal(detectDestination(tmp), 'dotenv');
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('vercel takes priority over convex', () => {
|
||||
const tmp = makeTempDir('dest');
|
||||
try {
|
||||
writeFileSync(join(tmp, 'vercel.json'), '{}');
|
||||
mkdirSync(join(tmp, 'convex'));
|
||||
assert.equal(detectDestination(tmp), 'vercel');
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// writeEnvKey
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('writeEnvKey', () => {
|
||||
it('creates .env file with new key', async () => {
|
||||
const tmp = makeTempDir('write');
|
||||
try {
|
||||
const envPath = join(tmp, '.env');
|
||||
await writeEnvKey(envPath, 'NEW_KEY', 'new-value');
|
||||
const content = readFileSync(envPath, 'utf8');
|
||||
assert.ok(content.includes('NEW_KEY=new-value'));
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('updates existing key in-place', async () => {
|
||||
const tmp = makeTempDir('write');
|
||||
try {
|
||||
const envPath = join(tmp, '.env');
|
||||
writeFileSync(envPath, 'EXISTING=old\nOTHER=keep\n');
|
||||
await writeEnvKey(envPath, 'EXISTING', 'new');
|
||||
const content = readFileSync(envPath, 'utf8');
|
||||
assert.ok(content.includes('EXISTING=new'));
|
||||
assert.ok(content.includes('OTHER=keep'));
|
||||
assert.ok(!content.includes('old'));
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('escapes newlines in values', async () => {
|
||||
const tmp = makeTempDir('write');
|
||||
try {
|
||||
const envPath = join(tmp, '.env');
|
||||
await writeEnvKey(envPath, 'MULTI', 'line1\nline2');
|
||||
const content = readFileSync(envPath, 'utf8');
|
||||
assert.ok(content.includes('MULTI=line1\\nline2'));
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects non-string values', async () => {
|
||||
const tmp = makeTempDir('write');
|
||||
try {
|
||||
const envPath = join(tmp, '.env');
|
||||
await assert.rejects(
|
||||
() => writeEnvKey(envPath, 'KEY', undefined as unknown as string),
|
||||
/expects a string value/,
|
||||
);
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// applySecrets (dotenv)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('applySecrets', () => {
|
||||
const savedKeys: Record<string, string | undefined> = {};
|
||||
|
||||
afterEach(() => {
|
||||
for (const [k, v] of Object.entries(savedKeys)) {
|
||||
if (v === undefined) delete process.env[k];
|
||||
else process.env[k] = v;
|
||||
}
|
||||
});
|
||||
|
||||
it('writes keys to .env and hydrates process.env', async () => {
|
||||
const tmp = makeTempDir('apply');
|
||||
const envPath = join(tmp, '.env');
|
||||
savedKeys.GSD_APPLY_TEST_A = process.env.GSD_APPLY_TEST_A;
|
||||
try {
|
||||
const { applied, errors } = await applySecrets(
|
||||
[{ key: 'GSD_APPLY_TEST_A', value: 'val-a' }],
|
||||
'dotenv',
|
||||
{ envFilePath: envPath },
|
||||
);
|
||||
assert.deepStrictEqual(applied, ['GSD_APPLY_TEST_A']);
|
||||
assert.deepStrictEqual(errors, []);
|
||||
assert.equal(process.env.GSD_APPLY_TEST_A, 'val-a');
|
||||
const content = readFileSync(envPath, 'utf8');
|
||||
assert.ok(content.includes('GSD_APPLY_TEST_A=val-a'));
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('returns errors for invalid vercel environment', async () => {
|
||||
const tmp = makeTempDir('apply');
|
||||
try {
|
||||
const { applied, errors } = await applySecrets(
|
||||
[{ key: 'KEY', value: 'val' }],
|
||||
'vercel',
|
||||
{
|
||||
envFilePath: join(tmp, '.env'),
|
||||
environment: 'staging' as 'development',
|
||||
execFn: async () => ({ code: 0, stderr: '' }),
|
||||
},
|
||||
);
|
||||
assert.deepStrictEqual(applied, []);
|
||||
assert.ok(errors[0]?.includes('unsupported'));
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Validation helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('isSafeEnvVarKey', () => {
|
||||
it('accepts valid keys', () => {
|
||||
assert.ok(isSafeEnvVarKey('API_KEY'));
|
||||
assert.ok(isSafeEnvVarKey('_PRIVATE'));
|
||||
assert.ok(isSafeEnvVarKey('key123'));
|
||||
});
|
||||
|
||||
it('rejects invalid keys', () => {
|
||||
assert.ok(!isSafeEnvVarKey('123BAD'));
|
||||
assert.ok(!isSafeEnvVarKey('has-dash'));
|
||||
assert.ok(!isSafeEnvVarKey('has space'));
|
||||
assert.ok(!isSafeEnvVarKey(''));
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSupportedDeploymentEnvironment', () => {
|
||||
it('accepts valid environments', () => {
|
||||
assert.ok(isSupportedDeploymentEnvironment('development'));
|
||||
assert.ok(isSupportedDeploymentEnvironment('preview'));
|
||||
assert.ok(isSupportedDeploymentEnvironment('production'));
|
||||
});
|
||||
|
||||
it('rejects invalid environments', () => {
|
||||
assert.ok(!isSupportedDeploymentEnvironment('staging'));
|
||||
assert.ok(!isSupportedDeploymentEnvironment('test'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('shellEscapeSingle', () => {
|
||||
it('wraps in single quotes', () => {
|
||||
assert.equal(shellEscapeSingle('hello'), "'hello'");
|
||||
});
|
||||
|
||||
it('escapes embedded single quotes', () => {
|
||||
assert.equal(shellEscapeSingle("it's"), "'it'\\''s'");
|
||||
});
|
||||
});
|
||||
183
packages/mcp-server/src/env-writer.ts
Normal file
183
packages/mcp-server/src/env-writer.ts
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
// @gsd-build/mcp-server — Environment variable write utilities
|
||||
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
||||
//
|
||||
// Shared helpers for writing env vars to .env files, detecting project
|
||||
// destinations, and checking existing keys. Used by secure_env_collect
|
||||
// MCP tool. No TUI dependencies — pure filesystem + process.env operations.
|
||||
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { existsSync, statSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// checkExistingEnvKeys
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Check which keys already exist in a .env file or process.env.
|
||||
* Returns the subset of `keys` that are already set.
|
||||
*/
|
||||
export async function checkExistingEnvKeys(keys: string[], envFilePath: string): Promise<string[]> {
|
||||
let fileContent = "";
|
||||
try {
|
||||
fileContent = await readFile(envFilePath, "utf8");
|
||||
} catch {
|
||||
// ENOENT or other read error — proceed with empty content
|
||||
}
|
||||
|
||||
const existing: string[] = [];
|
||||
for (const key of keys) {
|
||||
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const regex = new RegExp(`^${escaped}\\s*=`, "m");
|
||||
if (regex.test(fileContent) || key in process.env) {
|
||||
existing.push(key);
|
||||
}
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// detectDestination
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Detect the write destination based on project files in basePath.
|
||||
* Priority: vercel.json → convex/ dir → fallback "dotenv".
|
||||
*/
|
||||
export function detectDestination(basePath: string): "dotenv" | "vercel" | "convex" {
|
||||
if (existsSync(resolve(basePath, "vercel.json"))) {
|
||||
return "vercel";
|
||||
}
|
||||
const convexPath = resolve(basePath, "convex");
|
||||
try {
|
||||
if (existsSync(convexPath) && statSync(convexPath).isDirectory()) {
|
||||
return "convex";
|
||||
}
|
||||
} catch {
|
||||
// stat error — treat as not found
|
||||
}
|
||||
return "dotenv";
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// writeEnvKey
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Write a single key=value pair to a .env file.
|
||||
* Updates existing keys in-place, appends new ones at the end.
|
||||
*/
|
||||
export async function writeEnvKey(filePath: string, key: string, value: string): Promise<void> {
|
||||
if (typeof value !== "string") {
|
||||
throw new TypeError(`writeEnvKey expects a string value for key "${key}", got ${typeof value}`);
|
||||
}
|
||||
let content = "";
|
||||
try {
|
||||
content = await readFile(filePath, "utf8");
|
||||
} catch {
|
||||
content = "";
|
||||
}
|
||||
const escaped = value.replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/\r/g, "");
|
||||
const line = `${key}=${escaped}`;
|
||||
const regex = new RegExp(`^${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*=.*$`, "m");
|
||||
if (regex.test(content)) {
|
||||
content = content.replace(regex, line);
|
||||
} else {
|
||||
if (content.length > 0 && !content.endsWith("\n")) content += "\n";
|
||||
content += `${line}\n`;
|
||||
}
|
||||
await writeFile(filePath, content, "utf8");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Validation helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function isSafeEnvVarKey(key: string): boolean {
|
||||
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(key);
|
||||
}
|
||||
|
||||
export function isSupportedDeploymentEnvironment(env: string): boolean {
|
||||
return env === "development" || env === "preview" || env === "production";
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shell helpers (for vercel/convex CLI)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function shellEscapeSingle(value: string): string {
|
||||
return `'${value.replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// applySecrets
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface ApplyResult {
|
||||
applied: string[];
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply collected secrets to the target destination.
|
||||
* Dotenv writes are handled directly; vercel/convex shell out via execFn.
|
||||
*/
|
||||
export async function applySecrets(
|
||||
provided: Array<{ key: string; value: string }>,
|
||||
destination: "dotenv" | "vercel" | "convex",
|
||||
opts: {
|
||||
envFilePath: string;
|
||||
environment?: string;
|
||||
execFn?: (cmd: string, args: string[]) => Promise<{ code: number; stderr: string }>;
|
||||
},
|
||||
): Promise<ApplyResult> {
|
||||
const applied: string[] = [];
|
||||
const errors: string[] = [];
|
||||
|
||||
if (destination === "dotenv") {
|
||||
for (const { key, value } of provided) {
|
||||
try {
|
||||
await writeEnvKey(opts.envFilePath, key, value);
|
||||
applied.push(key);
|
||||
// Hydrate process.env so the current session sees the new value
|
||||
process.env[key] = value;
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
errors.push(`${key}: ${msg}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ((destination === "vercel" || destination === "convex") && opts.execFn) {
|
||||
const env = opts.environment ?? "development";
|
||||
if (!isSupportedDeploymentEnvironment(env)) {
|
||||
errors.push(`environment: unsupported target environment "${env}"`);
|
||||
return { applied, errors };
|
||||
}
|
||||
for (const { key, value } of provided) {
|
||||
if (!isSafeEnvVarKey(key)) {
|
||||
errors.push(`${key}: invalid environment variable name`);
|
||||
continue;
|
||||
}
|
||||
const cmd = destination === "vercel"
|
||||
? `printf %s ${shellEscapeSingle(value)} | vercel env add ${key} ${env}`
|
||||
: "";
|
||||
try {
|
||||
const result = destination === "vercel"
|
||||
? await opts.execFn("sh", ["-c", cmd])
|
||||
: await opts.execFn("npx", ["convex", "env", "set", key, value]);
|
||||
if (result.code !== 0) {
|
||||
errors.push(`${key}: ${result.stderr.slice(0, 200)}`);
|
||||
} else {
|
||||
applied.push(key);
|
||||
process.env[key] = value;
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
errors.push(`${key}: ${msg}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { applied, errors };
|
||||
}
|
||||
48
packages/mcp-server/src/import-candidates.test.ts
Normal file
48
packages/mcp-server/src/import-candidates.test.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// GSD-2 — Regression tests for importLocalModule candidate resolution (#3954)
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { _buildImportCandidates } from "./workflow-tools.js";
|
||||
|
||||
describe("_buildImportCandidates", () => {
|
||||
it("includes dist/ fallback for src/ paths", () => {
|
||||
const candidates = _buildImportCandidates("../../../src/resources/extensions/gsd/db-writer.js");
|
||||
assert.ok(
|
||||
candidates.some((c) => c.includes("/dist/resources/extensions/gsd/db-writer.js")),
|
||||
"should include dist/ swapped candidate",
|
||||
);
|
||||
});
|
||||
|
||||
it("includes src/ fallback for dist/ paths", () => {
|
||||
const candidates = _buildImportCandidates("../../../dist/resources/extensions/gsd/db-writer.js");
|
||||
assert.ok(
|
||||
candidates.some((c) => c.includes("/src/resources/extensions/gsd/db-writer.js")),
|
||||
"should include src/ swapped candidate",
|
||||
);
|
||||
});
|
||||
|
||||
it("includes .ts variants for .js paths", () => {
|
||||
const candidates = _buildImportCandidates("../../../src/resources/extensions/gsd/db-writer.js");
|
||||
assert.ok(
|
||||
candidates.some((c) => c.endsWith("db-writer.ts") && c.includes("/src/")),
|
||||
"should include .ts variant for original src/ path",
|
||||
);
|
||||
assert.ok(
|
||||
candidates.some((c) => c.endsWith("db-writer.ts") && c.includes("/dist/")),
|
||||
"should include .ts variant for swapped dist/ path",
|
||||
);
|
||||
});
|
||||
|
||||
it("returns original path first", () => {
|
||||
const input = "../../../src/resources/extensions/gsd/db-writer.js";
|
||||
const candidates = _buildImportCandidates(input);
|
||||
assert.equal(candidates[0], input, "first candidate should be the original path");
|
||||
});
|
||||
|
||||
it("handles paths without src/ or dist/ gracefully", () => {
|
||||
const candidates = _buildImportCandidates("./local-module.js");
|
||||
assert.equal(candidates.length, 2, "should have original + .ts variant only");
|
||||
assert.equal(candidates[0], "./local-module.js");
|
||||
assert.equal(candidates[1], "./local-module.ts");
|
||||
});
|
||||
});
|
||||
|
|
@ -16,7 +16,11 @@ import { resolve } from 'node:path';
|
|||
import { EventEmitter } from 'node:events';
|
||||
|
||||
import { SessionManager } from './session-manager.js';
|
||||
import { createMcpServer } from './server.js';
|
||||
import {
|
||||
buildAskUserQuestionsElicitRequest,
|
||||
createMcpServer,
|
||||
formatAskUserQuestionsElicitResult,
|
||||
} from './server.js';
|
||||
import { MAX_EVENTS } from './types.js';
|
||||
import type { ManagedSession, CostAccumulator, PendingBlocker } from './types.js';
|
||||
|
||||
|
|
@ -574,6 +578,8 @@ describe('createMcpServer tool registration', () => {
|
|||
it('creates server successfully with all required methods', async () => {
|
||||
const { server } = await createMcpServer(sm);
|
||||
assert.ok(server);
|
||||
assert.ok(server.server);
|
||||
assert.equal(typeof server.server.elicitInput, 'function');
|
||||
assert.ok(typeof server.connect === 'function');
|
||||
assert.ok(typeof server.close === 'function');
|
||||
});
|
||||
|
|
@ -625,4 +631,82 @@ describe('createMcpServer tool registration', () => {
|
|||
const session = sm.getSession(sessionId)!;
|
||||
assert.equal(session.status, 'cancelled');
|
||||
});
|
||||
|
||||
it('buildAskUserQuestionsElicitRequest adds None of the above note field for single-select questions', () => {
|
||||
const request = buildAskUserQuestionsElicitRequest([
|
||||
{
|
||||
id: 'depth_verification_M001',
|
||||
header: 'Depth Check',
|
||||
question: 'Did I capture the depth right?',
|
||||
options: [
|
||||
{ label: 'Yes, you got it (Recommended)', description: 'Continue with the current summary.' },
|
||||
{ label: 'Not quite', description: 'I need to clarify the depth further.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'focus_areas',
|
||||
header: 'Focus',
|
||||
question: 'Which areas matter most?',
|
||||
allowMultiple: true,
|
||||
options: [
|
||||
{ label: 'Frontend', description: 'Prioritize the UI.' },
|
||||
{ label: 'Backend', description: 'Prioritize server logic.' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
assert.equal(request.mode, 'form');
|
||||
assert.deepEqual(request.requestedSchema.required, ['depth_verification_M001', 'focus_areas']);
|
||||
assert.ok(request.requestedSchema.properties['depth_verification_M001']);
|
||||
assert.ok(request.requestedSchema.properties['depth_verification_M001__note']);
|
||||
assert.ok(!request.requestedSchema.properties['focus_areas__note']);
|
||||
});
|
||||
|
||||
it('formatAskUserQuestionsElicitResult preserves the existing answers JSON shape', () => {
|
||||
const result = formatAskUserQuestionsElicitResult(
|
||||
[
|
||||
{
|
||||
id: 'depth_verification_M001',
|
||||
header: 'Depth Check',
|
||||
question: 'Did I capture the depth right?',
|
||||
options: [
|
||||
{ label: 'Yes, you got it (Recommended)', description: 'Continue with the current summary.' },
|
||||
{ label: 'Not quite', description: 'I need to clarify the depth further.' },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'focus_areas',
|
||||
header: 'Focus',
|
||||
question: 'Which areas matter most?',
|
||||
allowMultiple: true,
|
||||
options: [
|
||||
{ label: 'Frontend', description: 'Prioritize the UI.' },
|
||||
{ label: 'Backend', description: 'Prioritize server logic.' },
|
||||
],
|
||||
},
|
||||
],
|
||||
{
|
||||
action: 'accept',
|
||||
content: {
|
||||
depth_verification_M001: 'None of the above',
|
||||
depth_verification_M001__note: 'Need more implementation detail.',
|
||||
focus_areas: ['Frontend', 'Backend'],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
result,
|
||||
JSON.stringify({
|
||||
answers: {
|
||||
depth_verification_M001: {
|
||||
answers: ['None of the above', 'user_note: Need more implementation detail.'],
|
||||
},
|
||||
focus_areas: {
|
||||
answers: ['Frontend', 'Backend'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
265
packages/mcp-server/src/secure-env-collect.test.ts
Normal file
265
packages/mcp-server/src/secure-env-collect.test.ts
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
// @gsd-build/mcp-server — Tests for secure_env_collect MCP tool
|
||||
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
||||
//
|
||||
// Tests the secure_env_collect tool registered in createMcpServer.
|
||||
// Uses a mock MCP server to intercept tool registration and elicitInput calls.
|
||||
|
||||
import { describe, it, beforeEach } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
import { createMcpServer } from './server.js';
|
||||
import { SessionManager } from './session-manager.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock infrastructure
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* We intercept McpServer construction by monkey-patching the dynamic import.
|
||||
* Instead, we'll test the tool handler indirectly through the exported
|
||||
* createMcpServer function — capturing the registered tool handlers.
|
||||
*
|
||||
* Since createMcpServer dynamically imports McpServer, we need to test at
|
||||
* a level that exercises the tool handler logic. We do this by extracting
|
||||
* the tool handler through the server.tool() calls.
|
||||
*/
|
||||
|
||||
interface RegisteredTool {
|
||||
name: string;
|
||||
description: string;
|
||||
params: Record<string, unknown>;
|
||||
handler: (args: Record<string, unknown>) => Promise<unknown>;
|
||||
}
|
||||
|
||||
interface ToolResult {
|
||||
content?: Array<{ type: string; text: string }>;
|
||||
isError?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock McpServer that captures tool registrations and provides
|
||||
* a controllable elicitInput response.
|
||||
*/
|
||||
class MockMcpServer {
|
||||
registeredTools: RegisteredTool[] = [];
|
||||
elicitResponse: { action: string; content?: Record<string, unknown> } = { action: 'accept', content: {} };
|
||||
|
||||
server = {
|
||||
elicitInput: async (_params: unknown) => {
|
||||
return this.elicitResponse;
|
||||
},
|
||||
};
|
||||
|
||||
tool(name: string, description: string, params: Record<string, unknown>, handler: (args: Record<string, unknown>) => Promise<unknown>) {
|
||||
this.registeredTools.push({ name, description, params, handler });
|
||||
}
|
||||
|
||||
async connect(_transport: unknown) { /* no-op */ }
|
||||
async close() { /* no-op */ }
|
||||
|
||||
getToolHandler(name: string): ((args: Record<string, unknown>) => Promise<unknown>) | undefined {
|
||||
return this.registeredTools.find((t) => t.name === name)?.handler;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper to create a mock MCP server with secure_env_collect registered
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Since createMcpServer uses dynamic import for McpServer, we can't easily
|
||||
* mock it. Instead, we test the env-writer utilities directly (in env-writer.test.ts)
|
||||
* and test the tool integration by verifying:
|
||||
* 1. The tool exists in the registered tools list
|
||||
* 2. The handler produces correct results with mock data
|
||||
*
|
||||
* For handler-level testing, we create a standalone test that replicates
|
||||
* the tool handler logic with a controllable mock.
|
||||
*/
|
||||
|
||||
function makeTempDir(prefix: string): string {
|
||||
return mkdtempSync(join(tmpdir(), `${prefix}-`));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Integration test — verify tool is registered
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('secure_env_collect tool registration', () => {
|
||||
it('createMcpServer registers secure_env_collect tool', async () => {
|
||||
// This test verifies the tool exists — createMcpServer internally calls
|
||||
// server.tool('secure_env_collect', ...) which we can't intercept without
|
||||
// module mocking, but we can verify the server creates successfully
|
||||
const sm = new SessionManager();
|
||||
try {
|
||||
const { server } = await createMcpServer(sm);
|
||||
assert.ok(server, 'server should be created');
|
||||
// The McpServer internally tracks registered tools — we verify no error
|
||||
} finally {
|
||||
await sm.cleanup();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Handler logic tests — using env-writer directly to test the flow
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('secure_env_collect handler logic', () => {
|
||||
it('skips keys that already exist in .env', async () => {
|
||||
const tmp = makeTempDir('sec-collect');
|
||||
try {
|
||||
const envPath = join(tmp, '.env');
|
||||
writeFileSync(envPath, 'ALREADY_SET=existing-value\n');
|
||||
|
||||
// Import the utility directly to test the pre-check logic
|
||||
const { checkExistingEnvKeys } = await import('./env-writer.js');
|
||||
const existing = await checkExistingEnvKeys(['ALREADY_SET', 'NEW_KEY'], envPath);
|
||||
assert.deepStrictEqual(existing, ['ALREADY_SET']);
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('writes collected values to .env without returning secret values', async () => {
|
||||
const tmp = makeTempDir('sec-collect');
|
||||
try {
|
||||
const envPath = join(tmp, '.env');
|
||||
const savedKey = process.env.SEC_COLLECT_TEST_KEY;
|
||||
|
||||
const { applySecrets } = await import('./env-writer.js');
|
||||
const { applied, errors } = await applySecrets(
|
||||
[{ key: 'SEC_COLLECT_TEST_KEY', value: 'super-secret-value' }],
|
||||
'dotenv',
|
||||
{ envFilePath: envPath },
|
||||
);
|
||||
|
||||
assert.deepStrictEqual(applied, ['SEC_COLLECT_TEST_KEY']);
|
||||
assert.deepStrictEqual(errors, []);
|
||||
|
||||
// Verify the value was written
|
||||
const content = readFileSync(envPath, 'utf8');
|
||||
assert.ok(content.includes('SEC_COLLECT_TEST_KEY=super-secret-value'));
|
||||
|
||||
// Verify process.env was hydrated
|
||||
assert.equal(process.env.SEC_COLLECT_TEST_KEY, 'super-secret-value');
|
||||
|
||||
// Cleanup
|
||||
if (savedKey === undefined) delete process.env.SEC_COLLECT_TEST_KEY;
|
||||
else process.env.SEC_COLLECT_TEST_KEY = savedKey;
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('auto-detects vercel destination from vercel.json', async () => {
|
||||
const tmp = makeTempDir('sec-collect');
|
||||
try {
|
||||
writeFileSync(join(tmp, 'vercel.json'), '{}');
|
||||
const { detectDestination } = await import('./env-writer.js');
|
||||
assert.equal(detectDestination(tmp), 'vercel');
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('handles empty form values as skipped', async () => {
|
||||
// Simulate what happens when user leaves a field empty in the form
|
||||
const formContent: Record<string, string> = {
|
||||
'API_KEY': 'provided-value',
|
||||
'OPTIONAL_KEY': '', // empty = skip
|
||||
};
|
||||
|
||||
const provided: Array<{ key: string; value: string }> = [];
|
||||
const skipped: string[] = [];
|
||||
|
||||
for (const [key, raw] of Object.entries(formContent)) {
|
||||
const value = typeof raw === 'string' ? raw.trim() : '';
|
||||
if (value.length > 0) {
|
||||
provided.push({ key, value });
|
||||
} else {
|
||||
skipped.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
assert.deepStrictEqual(provided, [{ key: 'API_KEY', value: 'provided-value' }]);
|
||||
assert.deepStrictEqual(skipped, ['OPTIONAL_KEY']);
|
||||
});
|
||||
|
||||
it('result text never contains secret values', async () => {
|
||||
const tmp = makeTempDir('sec-collect');
|
||||
try {
|
||||
const envPath = join(tmp, '.env');
|
||||
const savedKey = process.env.RESULT_TEXT_TEST;
|
||||
|
||||
const { applySecrets } = await import('./env-writer.js');
|
||||
const { applied } = await applySecrets(
|
||||
[{ key: 'RESULT_TEXT_TEST', value: 'sk-super-secret-abc123' }],
|
||||
'dotenv',
|
||||
{ envFilePath: envPath },
|
||||
);
|
||||
|
||||
// Simulate building result text (same logic as the tool handler)
|
||||
const lines: string[] = [
|
||||
'destination: dotenv (auto-detected)',
|
||||
...applied.map((k) => `✓ ${k}: applied`),
|
||||
];
|
||||
const resultText = lines.join('\n');
|
||||
|
||||
// The result MUST NOT contain the secret value
|
||||
assert.ok(!resultText.includes('sk-super-secret-abc123'), 'result text must not contain secret value');
|
||||
assert.ok(resultText.includes('RESULT_TEXT_TEST'), 'result text should contain key name');
|
||||
|
||||
// Cleanup
|
||||
if (savedKey === undefined) delete process.env.RESULT_TEXT_TEST;
|
||||
else process.env.RESULT_TEXT_TEST = savedKey;
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('handles multiple keys with mixed existing/new/skipped', async () => {
|
||||
const tmp = makeTempDir('sec-collect');
|
||||
try {
|
||||
const envPath = join(tmp, '.env');
|
||||
writeFileSync(envPath, 'EXISTING_A=already-here\n');
|
||||
const savedB = process.env.NEW_B;
|
||||
const savedC = process.env.SKIP_C;
|
||||
|
||||
const { checkExistingEnvKeys, applySecrets } = await import('./env-writer.js');
|
||||
|
||||
const allKeys = ['EXISTING_A', 'NEW_B', 'SKIP_C'];
|
||||
const existing = await checkExistingEnvKeys(allKeys, envPath);
|
||||
assert.deepStrictEqual(existing, ['EXISTING_A']);
|
||||
|
||||
// Simulate form response: NEW_B has value, SKIP_C is empty
|
||||
const formContent = { NEW_B: 'new-value', SKIP_C: '' };
|
||||
const provided: Array<{ key: string; value: string }> = [];
|
||||
const skipped: string[] = [];
|
||||
|
||||
for (const key of allKeys.filter((k) => !existing.includes(k))) {
|
||||
const raw = formContent[key as keyof typeof formContent] ?? '';
|
||||
if (raw.trim().length > 0) provided.push({ key, value: raw.trim() });
|
||||
else skipped.push(key);
|
||||
}
|
||||
|
||||
const { applied, errors } = await applySecrets(provided, 'dotenv', { envFilePath: envPath });
|
||||
|
||||
assert.deepStrictEqual(applied, ['NEW_B']);
|
||||
assert.deepStrictEqual(skipped, ['SKIP_C']);
|
||||
assert.deepStrictEqual(errors, []);
|
||||
assert.deepStrictEqual(existing, ['EXISTING_A']);
|
||||
|
||||
// Cleanup
|
||||
if (savedB === undefined) delete process.env.NEW_B;
|
||||
else process.env.NEW_B = savedB;
|
||||
if (savedC === undefined) delete process.env.SKIP_C;
|
||||
else process.env.SKIP_C = savedC;
|
||||
} finally {
|
||||
rmSync(tmp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -2,8 +2,9 @@
|
|||
* MCP Server — registers GSD orchestration, project-state, and workflow tools.
|
||||
*
|
||||
* Session tools (6): gsd_execute, gsd_status, gsd_result, gsd_cancel, gsd_query, gsd_resolve_blocker
|
||||
* Interactive tools (2): ask_user_questions, secure_env_collect via MCP form elicitation
|
||||
* Read-only tools (6): gsd_progress, gsd_roadmap, gsd_history, gsd_doctor, gsd_captures, gsd_knowledge
|
||||
* Workflow tools (17): planning, replanning, completion, validation, reassessment, gate result, and milestone status tools
|
||||
* Workflow tools (29): headless-safe planning, metadata persistence, replanning, completion, validation, reassessment, gate result, status, and journal tools
|
||||
*
|
||||
* Uses dynamic imports for @modelcontextprotocol/sdk because TS Node16
|
||||
* cannot resolve the SDK's subpath exports statically (same pattern as
|
||||
|
|
@ -21,6 +22,7 @@ import { readCaptures } from './readers/captures.js';
|
|||
import { readKnowledge } from './readers/knowledge.js';
|
||||
import { runDoctorLite } from './readers/doctor-lite.js';
|
||||
import { registerWorkflowTools } from './workflow-tools.js';
|
||||
import { applySecrets, checkExistingEnvKeys, detectDestination } from './env-writer.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants
|
||||
|
|
@ -44,50 +46,89 @@ function errorContent(message: string): { isError: true; content: Array<{ type:
|
|||
return { isError: true, content: [{ type: 'text' as const, text: message }] };
|
||||
}
|
||||
|
||||
/** Return raw text content without JSON wrapping. */
|
||||
function textContent(text: string): { content: Array<{ type: 'text'; text: string }> } {
|
||||
return { content: [{ type: 'text' as const, text }] };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// gsd_query filesystem reader
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function readProjectState(projectDir: string, _query: string): Promise<Record<string, unknown>> {
|
||||
/**
|
||||
* Normalized query categories for {@link readProjectState}.
|
||||
*
|
||||
* Maps user-supplied query strings (or empty) to the set of fields we return.
|
||||
* Accepts common synonyms so the MCP client can pass intuitive values.
|
||||
*/
|
||||
const QUERY_FIELDS = {
|
||||
all: ['state', 'project', 'requirements', 'milestones'] as const,
|
||||
state: ['state'] as const,
|
||||
status: ['state'] as const,
|
||||
project: ['project'] as const,
|
||||
requirements: ['requirements'] as const,
|
||||
milestones: ['milestones'] as const,
|
||||
} as const;
|
||||
|
||||
type QueryCategory = keyof typeof QUERY_FIELDS;
|
||||
type ProjectStateField = (typeof QUERY_FIELDS)[QueryCategory][number];
|
||||
|
||||
function normalizeQuery(query: string | undefined): QueryCategory {
|
||||
const key = (query ?? 'all').trim().toLowerCase();
|
||||
if (key in QUERY_FIELDS) return key as QueryCategory;
|
||||
return 'all';
|
||||
}
|
||||
|
||||
async function readProjectState(projectDir: string, query: string | undefined): Promise<Record<string, unknown>> {
|
||||
const gsdDir = join(resolve(projectDir), '.gsd');
|
||||
const result: Record<string, unknown> = { projectDir: resolve(projectDir) };
|
||||
const category = normalizeQuery(query);
|
||||
const wanted = new Set<ProjectStateField>(QUERY_FIELDS[category]);
|
||||
|
||||
// STATE.md — current execution state
|
||||
try {
|
||||
result.state = await readFile(join(gsdDir, 'STATE.md'), 'utf-8');
|
||||
} catch {
|
||||
result.state = null;
|
||||
}
|
||||
const result: Record<string, unknown> = {
|
||||
projectDir: resolve(projectDir),
|
||||
query: category,
|
||||
};
|
||||
|
||||
// PROJECT.md — project description
|
||||
try {
|
||||
result.project = await readFile(join(gsdDir, 'PROJECT.md'), 'utf-8');
|
||||
} catch {
|
||||
result.project = null;
|
||||
}
|
||||
|
||||
// REQUIREMENTS.md — requirement contract
|
||||
try {
|
||||
result.requirements = await readFile(join(gsdDir, 'REQUIREMENTS.md'), 'utf-8');
|
||||
} catch {
|
||||
result.requirements = null;
|
||||
}
|
||||
|
||||
// List milestones with basic metadata
|
||||
const milestonesDir = join(gsdDir, 'milestones');
|
||||
try {
|
||||
const entries = await readdir(milestonesDir, { withFileTypes: true });
|
||||
const milestones: Array<{ id: string; hasRoadmap: boolean; hasSummary: boolean }> = [];
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const mDir = join(milestonesDir, entry.name);
|
||||
const hasRoadmap = await fileExists(join(mDir, `${entry.name}-ROADMAP.md`));
|
||||
const hasSummary = await fileExists(join(mDir, `${entry.name}-SUMMARY.md`));
|
||||
milestones.push({ id: entry.name, hasRoadmap, hasSummary });
|
||||
if (wanted.has('state')) {
|
||||
try {
|
||||
result.state = await readFile(join(gsdDir, 'STATE.md'), 'utf-8');
|
||||
} catch {
|
||||
result.state = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (wanted.has('project')) {
|
||||
try {
|
||||
result.project = await readFile(join(gsdDir, 'PROJECT.md'), 'utf-8');
|
||||
} catch {
|
||||
result.project = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (wanted.has('requirements')) {
|
||||
try {
|
||||
result.requirements = await readFile(join(gsdDir, 'REQUIREMENTS.md'), 'utf-8');
|
||||
} catch {
|
||||
result.requirements = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (wanted.has('milestones')) {
|
||||
const milestonesDir = join(gsdDir, 'milestones');
|
||||
try {
|
||||
const entries = await readdir(milestonesDir, { withFileTypes: true });
|
||||
const milestones: Array<{ id: string; hasRoadmap: boolean; hasSummary: boolean }> = [];
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const mDir = join(milestonesDir, entry.name);
|
||||
const hasRoadmap = await fileExists(join(mDir, `${entry.name}-ROADMAP.md`));
|
||||
const hasSummary = await fileExists(join(mDir, `${entry.name}-SUMMARY.md`));
|
||||
milestones.push({ id: entry.name, hasRoadmap, hasSummary });
|
||||
}
|
||||
result.milestones = milestones;
|
||||
} catch {
|
||||
result.milestones = [];
|
||||
}
|
||||
result.milestones = milestones;
|
||||
} catch {
|
||||
result.milestones = [];
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
@ -106,12 +147,189 @@ async function fileExists(path: string): Promise<boolean> {
|
|||
// MCP Server type — minimal interface for the dynamically-imported McpServer
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface ElicitResult {
|
||||
action: 'accept' | 'decline' | 'cancel';
|
||||
content?: Record<string, string | number | boolean | string[]>;
|
||||
}
|
||||
|
||||
interface ElicitRequestFormParams {
|
||||
mode?: 'form';
|
||||
message: string;
|
||||
requestedSchema: {
|
||||
type: 'object';
|
||||
properties: Record<string, Record<string, unknown>>;
|
||||
required?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler extra — the second argument passed by McpServer.tool handlers.
|
||||
* Contains an AbortSignal scoped to the JSON-RPC request (cancelled when
|
||||
* the client cancels the `tools/call`) plus other per-request metadata.
|
||||
* Tools that can actually be stopped mid-flight should honour `signal`.
|
||||
*/
|
||||
export interface McpToolExtra {
|
||||
signal?: AbortSignal;
|
||||
requestId?: string | number;
|
||||
sendNotification?: (notification: unknown) => void | Promise<void>;
|
||||
}
|
||||
|
||||
interface McpServerInstance {
|
||||
tool(name: string, description: string, params: Record<string, unknown>, handler: (args: Record<string, unknown>) => Promise<unknown>): unknown;
|
||||
tool(
|
||||
name: string,
|
||||
description: string,
|
||||
params: Record<string, unknown>,
|
||||
handler: (args: Record<string, unknown>, extra?: McpToolExtra) => Promise<unknown>,
|
||||
): unknown;
|
||||
server: {
|
||||
elicitInput(
|
||||
params: AskUserQuestionsElicitRequest | ElicitRequestFormParams,
|
||||
options?: unknown,
|
||||
): Promise<AskUserQuestionsElicitResult>;
|
||||
};
|
||||
connect(transport: unknown): Promise<void>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
interface AskUserQuestionOption {
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface AskUserQuestion {
|
||||
id: string;
|
||||
header: string;
|
||||
question: string;
|
||||
options: AskUserQuestionOption[];
|
||||
allowMultiple?: boolean;
|
||||
}
|
||||
|
||||
interface AskUserQuestionsParams {
|
||||
questions: AskUserQuestion[];
|
||||
}
|
||||
|
||||
type AskUserQuestionsContentValue = string | number | boolean | string[];
|
||||
|
||||
interface AskUserQuestionsElicitResult {
|
||||
action: 'accept' | 'decline' | 'cancel';
|
||||
content?: Record<string, AskUserQuestionsContentValue>;
|
||||
}
|
||||
|
||||
interface AskUserQuestionsElicitRequest {
|
||||
mode: 'form';
|
||||
message: string;
|
||||
requestedSchema: {
|
||||
type: 'object';
|
||||
properties: Record<string, Record<string, unknown>>;
|
||||
required?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
const OTHER_OPTION_LABEL = 'None of the above';
|
||||
|
||||
function normalizeAskUserQuestionsNote(value: AskUserQuestionsContentValue | undefined): string {
|
||||
return typeof value === 'string' ? value.trim() : '';
|
||||
}
|
||||
|
||||
function normalizeAskUserQuestionsAnswers(
|
||||
value: AskUserQuestionsContentValue | undefined,
|
||||
allowMultiple: boolean,
|
||||
): string[] {
|
||||
if (allowMultiple) {
|
||||
return Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : [];
|
||||
}
|
||||
|
||||
return typeof value === 'string' && value.length > 0 ? [value] : [];
|
||||
}
|
||||
|
||||
function validateAskUserQuestionsPayload(questions: AskUserQuestion[]): string | null {
|
||||
if (questions.length === 0 || questions.length > 3) {
|
||||
return 'Error: questions must contain 1-3 items';
|
||||
}
|
||||
|
||||
for (const question of questions) {
|
||||
if (!question.options || question.options.length === 0) {
|
||||
return `Error: ask_user_questions requires non-empty options for every question (question "${question.id}" has none)`;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function buildAskUserQuestionsElicitRequest(questions: AskUserQuestion[]): AskUserQuestionsElicitRequest {
|
||||
const properties: Record<string, Record<string, unknown>> = {};
|
||||
const required = questions.map((question) => question.id);
|
||||
|
||||
for (const question of questions) {
|
||||
if (question.allowMultiple) {
|
||||
properties[question.id] = {
|
||||
type: 'array',
|
||||
title: question.header,
|
||||
description: question.question,
|
||||
minItems: 1,
|
||||
maxItems: question.options.length,
|
||||
items: {
|
||||
anyOf: question.options.map((option) => ({
|
||||
const: option.label,
|
||||
title: option.label,
|
||||
})),
|
||||
},
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
properties[question.id] = {
|
||||
type: 'string',
|
||||
title: question.header,
|
||||
description: question.question,
|
||||
oneOf: [...question.options, { label: OTHER_OPTION_LABEL, description: 'Choose this when the listed options do not fit.' }].map((option) => ({
|
||||
const: option.label,
|
||||
title: option.label,
|
||||
})),
|
||||
};
|
||||
|
||||
properties[`${question.id}__note`] = {
|
||||
type: 'string',
|
||||
title: `${question.header} Note`,
|
||||
description: `Optional note for "${OTHER_OPTION_LABEL}".`,
|
||||
maxLength: 500,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
mode: 'form',
|
||||
message: 'Please answer the following question(s). For single-select questions, choose "None of the above" and add a note if the provided options do not fit.',
|
||||
requestedSchema: {
|
||||
type: 'object',
|
||||
properties,
|
||||
required,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function formatAskUserQuestionsElicitResult(
|
||||
questions: AskUserQuestion[],
|
||||
result: AskUserQuestionsElicitResult,
|
||||
): string {
|
||||
const answers: Record<string, { answers: string[] }> = {};
|
||||
const content = result.content ?? {};
|
||||
|
||||
for (const question of questions) {
|
||||
const answerList = normalizeAskUserQuestionsAnswers(content[question.id], !!question.allowMultiple);
|
||||
|
||||
if (!question.allowMultiple && answerList[0] === OTHER_OPTION_LABEL) {
|
||||
const note = normalizeAskUserQuestionsNote(content[`${question.id}__note`]);
|
||||
if (note) {
|
||||
answerList.push(`user_note: ${note}`);
|
||||
}
|
||||
}
|
||||
|
||||
answers[question.id] = { answers: answerList };
|
||||
}
|
||||
|
||||
return JSON.stringify({ answers });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// createMcpServer
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -131,11 +349,16 @@ export async function createMcpServer(sessionManager: SessionManager): Promise<{
|
|||
|
||||
const server: McpServerInstance = new McpServer(
|
||||
{ name: SERVER_NAME, version: SERVER_VERSION },
|
||||
{ capabilities: { tools: {} } },
|
||||
{ capabilities: { tools: {}, elicitation: {} } },
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// gsd_execute — start a new GSD auto-mode session
|
||||
// gsd_execute — start a new GSD auto-mode session.
|
||||
//
|
||||
// If the JSON-RPC request is aborted while the session is starting (or
|
||||
// immediately after), we cancel the session so we don't leak a background
|
||||
// RpcClient process. Once the session is running the caller should use
|
||||
// `gsd_cancel` to stop it via sessionId.
|
||||
// -----------------------------------------------------------------------
|
||||
server.tool(
|
||||
'gsd_execute',
|
||||
|
|
@ -146,12 +369,20 @@ export async function createMcpServer(sessionManager: SessionManager): Promise<{
|
|||
model: z.string().optional().describe('Model ID override'),
|
||||
bare: z.boolean().optional().describe('Run in bare mode (skip user config)'),
|
||||
},
|
||||
async (args: Record<string, unknown>) => {
|
||||
async (args: Record<string, unknown>, extra?: McpToolExtra) => {
|
||||
const { projectDir, command, model, bare } = args as {
|
||||
projectDir: string; command?: string; model?: string; bare?: boolean;
|
||||
};
|
||||
try {
|
||||
const sessionId = await sessionManager.startSession(projectDir, { command, model, bare });
|
||||
|
||||
// If the client aborted while startSession was running, cancel the
|
||||
// newly-created session rather than leaving an orphaned process.
|
||||
if (extra?.signal?.aborted) {
|
||||
await sessionManager.cancelSession(sessionId).catch(() => { /* swallow */ });
|
||||
return errorContent('gsd_execute aborted by client before returning');
|
||||
}
|
||||
|
||||
return jsonContent({ sessionId, status: 'started' });
|
||||
} catch (err) {
|
||||
return errorContent(err instanceof Error ? err.message : String(err));
|
||||
|
|
@ -244,17 +475,25 @@ export async function createMcpServer(sessionManager: SessionManager): Promise<{
|
|||
);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// gsd_query — read project state from filesystem (no session needed)
|
||||
// gsd_query — read project state from filesystem (no session needed).
|
||||
//
|
||||
// `query` is optional: when omitted the tool returns all fields (STATE.md,
|
||||
// PROJECT.md, requirements, milestone listing). Accepted narrow values:
|
||||
// "state" / "status", "project", "requirements", "milestones", "all".
|
||||
// Unknown values fall back to "all" for forward-compatibility.
|
||||
// -----------------------------------------------------------------------
|
||||
server.tool(
|
||||
'gsd_query',
|
||||
'Query GSD project state from the filesystem. Returns STATE.md, PROJECT.md, requirements, and milestone listing. Does not require an active session.',
|
||||
'Query GSD project state from the filesystem. By default returns STATE.md, PROJECT.md, requirements, and milestone listing. Pass `query` to narrow the response (accepted: "state"/"status", "project", "requirements", "milestones", "all"). Does not require an active session.',
|
||||
{
|
||||
projectDir: z.string().describe('Absolute path to the project directory'),
|
||||
query: z.string().describe('What to query (e.g. "status", "milestones", "requirements")'),
|
||||
query: z
|
||||
.enum(['all', 'state', 'status', 'project', 'requirements', 'milestones'])
|
||||
.optional()
|
||||
.describe('Narrow the response to a single field (default: "all")'),
|
||||
},
|
||||
async (args: Record<string, unknown>) => {
|
||||
const { projectDir, query } = args as { projectDir: string; query: string };
|
||||
const { projectDir, query } = args as { projectDir: string; query?: string };
|
||||
try {
|
||||
const state = await readProjectState(projectDir, query);
|
||||
return jsonContent(state);
|
||||
|
|
@ -285,6 +524,160 @@ export async function createMcpServer(sessionManager: SessionManager): Promise<{
|
|||
},
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// ask_user_questions — structured user input via MCP form elicitation
|
||||
// -----------------------------------------------------------------------
|
||||
server.tool(
|
||||
'ask_user_questions',
|
||||
'Request user input for one to three short questions and wait for the response. Single-select questions include a free-form "None of the above" path. Multi-select questions allow multiple choices.',
|
||||
{
|
||||
questions: z.array(z.object({
|
||||
id: z.string().describe('Stable identifier for mapping answers (snake_case)'),
|
||||
header: z.string().describe('Short header label shown in the UI (12 or fewer chars)'),
|
||||
question: z.string().describe('Single-sentence prompt shown to the user'),
|
||||
options: z.array(z.object({
|
||||
label: z.string().describe('User-facing label (1-5 words)'),
|
||||
description: z.string().describe('One short sentence explaining impact/tradeoff if selected'),
|
||||
})).describe('Provide 2-3 mutually exclusive choices. Put the recommended option first and suffix its label with "(Recommended)". Do not include an "Other" option for single-select questions.'),
|
||||
allowMultiple: z.boolean().optional().describe('If true, the user can select multiple options. No "None of the above" option is added.'),
|
||||
})).describe('Questions to show the user. Prefer 1 and do not exceed 3.'),
|
||||
},
|
||||
async (args: Record<string, unknown>) => {
|
||||
const { questions } = args as unknown as AskUserQuestionsParams;
|
||||
try {
|
||||
const validationError = validateAskUserQuestionsPayload(questions);
|
||||
if (validationError) return errorContent(validationError);
|
||||
|
||||
const elicitation = await server.server.elicitInput(buildAskUserQuestionsElicitRequest(questions));
|
||||
if (elicitation.action !== 'accept' || !elicitation.content) {
|
||||
return textContent('ask_user_questions was cancelled before receiving a response');
|
||||
}
|
||||
|
||||
return textContent(formatAskUserQuestionsElicitResult(questions, elicitation));
|
||||
} catch (err) {
|
||||
return errorContent(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// secure_env_collect — collect secrets via MCP form elicitation
|
||||
// -----------------------------------------------------------------------
|
||||
server.tool(
|
||||
'secure_env_collect',
|
||||
'Collect environment variables securely via form input. Values are written directly to .env (or Vercel/Convex) and NEVER appear in tool output — only key names and applied/skipped status are returned. Use this instead of asking users to manually edit .env files or paste secrets into chat.',
|
||||
{
|
||||
projectDir: z.string().describe('Absolute path to the project directory'),
|
||||
keys: z.array(z.object({
|
||||
key: z.string().describe('Env var name, e.g. OPENAI_API_KEY'),
|
||||
hint: z.string().optional().describe('Format hint shown to user, e.g. "starts with sk-"'),
|
||||
guidance: z.array(z.string()).optional().describe('Step-by-step instructions for obtaining this key'),
|
||||
})).min(1).describe('Environment variables to collect'),
|
||||
destination: z.enum(['dotenv', 'vercel', 'convex']).optional().describe('Where to write secrets. Auto-detected from project files if omitted.'),
|
||||
envFilePath: z.string().optional().describe('Path to .env file (dotenv only). Defaults to .env in projectDir.'),
|
||||
environment: z.enum(['development', 'preview', 'production']).optional().describe('Target environment (vercel/convex only)'),
|
||||
},
|
||||
async (args: Record<string, unknown>) => {
|
||||
const { projectDir, keys, destination, envFilePath, environment } = args as {
|
||||
projectDir: string;
|
||||
keys: Array<{ key: string; hint?: string; guidance?: string[] }>;
|
||||
destination?: 'dotenv' | 'vercel' | 'convex';
|
||||
envFilePath?: string;
|
||||
environment?: 'development' | 'preview' | 'production';
|
||||
};
|
||||
|
||||
try {
|
||||
const resolvedProjectDir = resolve(projectDir);
|
||||
const resolvedEnvPath = resolve(resolvedProjectDir, envFilePath ?? '.env');
|
||||
|
||||
// (1) Check which keys already exist
|
||||
const allKeyNames = keys.map((k) => k.key);
|
||||
const existingKeys = await checkExistingEnvKeys(allKeyNames, resolvedEnvPath);
|
||||
const existingSet = new Set(existingKeys);
|
||||
const pendingKeys = keys.filter((k) => !existingSet.has(k.key));
|
||||
|
||||
// If all keys already exist, return immediately
|
||||
if (pendingKeys.length === 0) {
|
||||
const lines = existingKeys.map((k) => `• ${k}: already set`);
|
||||
return textContent(`All ${existingKeys.length} key(s) already set.\n${lines.join('\n')}`);
|
||||
}
|
||||
|
||||
// (2) Build elicitation form — one string field per pending key
|
||||
const properties: Record<string, Record<string, unknown>> = {};
|
||||
const required: string[] = [];
|
||||
|
||||
for (const item of pendingKeys) {
|
||||
const descParts: string[] = [];
|
||||
if (item.hint) descParts.push(`Format: ${item.hint}`);
|
||||
if (item.guidance && item.guidance.length > 0) {
|
||||
descParts.push('How to get this:');
|
||||
item.guidance.forEach((step, i) => descParts.push(`${i + 1}. ${step}`));
|
||||
}
|
||||
descParts.push('Leave empty to skip.');
|
||||
|
||||
properties[item.key] = {
|
||||
type: 'string',
|
||||
title: item.key,
|
||||
description: descParts.join('\n'),
|
||||
};
|
||||
// Don't mark as required — empty string = skip
|
||||
}
|
||||
|
||||
// (3) Elicit input from the MCP client
|
||||
const elicitation = await server.server.elicitInput({
|
||||
message: `Enter values for ${pendingKeys.length} environment variable(s). Values are written directly to the project and never shown to the AI.`,
|
||||
requestedSchema: {
|
||||
type: 'object',
|
||||
properties,
|
||||
required,
|
||||
},
|
||||
});
|
||||
|
||||
if (elicitation.action !== 'accept' || !elicitation.content) {
|
||||
return textContent('secure_env_collect was cancelled by user.');
|
||||
}
|
||||
|
||||
// (4) Separate provided vs skipped from form response
|
||||
const provided: Array<{ key: string; value: string }> = [];
|
||||
const skipped: string[] = [];
|
||||
|
||||
for (const item of pendingKeys) {
|
||||
const raw = elicitation.content[item.key];
|
||||
const value = typeof raw === 'string' ? raw.trim() : '';
|
||||
if (value.length > 0) {
|
||||
provided.push({ key: item.key, value });
|
||||
} else {
|
||||
skipped.push(item.key);
|
||||
}
|
||||
}
|
||||
|
||||
// (5) Auto-detect destination if not specified
|
||||
const resolvedDestination = destination ?? detectDestination(resolvedProjectDir);
|
||||
|
||||
// (6) Write secrets to destination
|
||||
const { applied, errors } = await applySecrets(provided, resolvedDestination, {
|
||||
envFilePath: resolvedEnvPath,
|
||||
environment,
|
||||
});
|
||||
|
||||
// (7) Build result — NEVER include secret values
|
||||
const lines: string[] = [
|
||||
`destination: ${resolvedDestination}${!destination ? ' (auto-detected)' : ''}${environment ? ` (${environment})` : ''}`,
|
||||
];
|
||||
for (const k of applied) lines.push(`✓ ${k}: applied`);
|
||||
for (const k of skipped) lines.push(`• ${k}: skipped`);
|
||||
for (const k of existingKeys) lines.push(`• ${k}: already set`);
|
||||
for (const e of errors) lines.push(`✗ ${e}`);
|
||||
|
||||
return errors.length > 0 && applied.length === 0
|
||||
? errorContent(lines.join('\n'))
|
||||
: textContent(lines.join('\n'));
|
||||
} catch (err) {
|
||||
return errorContent(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// =======================================================================
|
||||
// READ-ONLY TOOLS — no session required, pure filesystem reads
|
||||
// =======================================================================
|
||||
|
|
|
|||
95
packages/mcp-server/src/tool-credentials.test.ts
Normal file
95
packages/mcp-server/src/tool-credentials.test.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { loadStoredCredentialEnvKeys, resolveAuthPath } from "./tool-credentials.js";
|
||||
|
||||
describe("tool credentials", () => {
|
||||
it("hydrates supported model and tool keys from auth.json", () => {
|
||||
const tempRoot = mkdtempSync(join(tmpdir(), "gsd-mcp-auth-"));
|
||||
const authPath = join(tempRoot, "auth.json");
|
||||
const env: NodeJS.ProcessEnv = {};
|
||||
|
||||
try {
|
||||
writeFileSync(authPath, JSON.stringify({
|
||||
anthropic: { type: "api_key", key: "sk-ant-secret" },
|
||||
openai: { type: "api_key", key: "sk-openai-secret" },
|
||||
tavily: { type: "api_key", key: "tvly-secret" },
|
||||
context7: [{ type: "api_key", key: "ctx7-secret" }],
|
||||
}));
|
||||
|
||||
const loaded = loadStoredCredentialEnvKeys({ authPath, env });
|
||||
assert.deepEqual(loaded.sort(), [
|
||||
"ANTHROPIC_API_KEY",
|
||||
"CONTEXT7_API_KEY",
|
||||
"OPENAI_API_KEY",
|
||||
"TAVILY_API_KEY",
|
||||
]);
|
||||
assert.equal(env.ANTHROPIC_API_KEY, "sk-ant-secret");
|
||||
assert.equal(env.OPENAI_API_KEY, "sk-openai-secret");
|
||||
assert.equal(env.TAVILY_API_KEY, "tvly-secret");
|
||||
assert.equal(env.CONTEXT7_API_KEY, "ctx7-secret");
|
||||
} finally {
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("does not overwrite explicit environment variables", () => {
|
||||
const tempRoot = mkdtempSync(join(tmpdir(), "gsd-mcp-auth-"));
|
||||
const authPath = join(tempRoot, "auth.json");
|
||||
const env: NodeJS.ProcessEnv = {
|
||||
BRAVE_API_KEY: "already-set",
|
||||
};
|
||||
|
||||
try {
|
||||
writeFileSync(authPath, JSON.stringify({
|
||||
brave: { type: "api_key", key: "from-auth-json" },
|
||||
anthropic: { type: "api_key", key: "sk-ant-from-auth-json" },
|
||||
}));
|
||||
|
||||
const loaded = loadStoredCredentialEnvKeys({ authPath, env });
|
||||
assert.deepEqual(loaded, ["ANTHROPIC_API_KEY"]);
|
||||
assert.equal(env.BRAVE_API_KEY, "already-set");
|
||||
assert.equal(env.ANTHROPIC_API_KEY, "sk-ant-from-auth-json");
|
||||
} finally {
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("ignores oauth credentials because they are resolved through auth storage, not env hydration", () => {
|
||||
const tempRoot = mkdtempSync(join(tmpdir(), "gsd-mcp-auth-"));
|
||||
const authPath = join(tempRoot, "auth.json");
|
||||
const env: NodeJS.ProcessEnv = {};
|
||||
|
||||
try {
|
||||
writeFileSync(authPath, JSON.stringify({
|
||||
openai: { type: "oauth", access: "oauth-access-token" },
|
||||
"google-gemini-cli": { type: "oauth", token: "ya29.oauth-token" },
|
||||
}));
|
||||
|
||||
const loaded = loadStoredCredentialEnvKeys({ authPath, env });
|
||||
assert.deepEqual(loaded, []);
|
||||
assert.equal(env.OPENAI_API_KEY, undefined);
|
||||
assert.equal(env.GEMINI_API_KEY, undefined);
|
||||
} finally {
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("resolves auth.json from GSD_CODING_AGENT_DIR", () => {
|
||||
const tempRoot = mkdtempSync(join(tmpdir(), "gsd-mcp-agent-dir-"));
|
||||
const agentDir = join(tempRoot, "agent");
|
||||
mkdirSync(agentDir, { recursive: true });
|
||||
|
||||
try {
|
||||
assert.equal(
|
||||
resolveAuthPath({ GSD_CODING_AGENT_DIR: agentDir }),
|
||||
join(agentDir, "auth.json"),
|
||||
);
|
||||
} finally {
|
||||
rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
97
packages/mcp-server/src/tool-credentials.ts
Normal file
97
packages/mcp-server/src/tool-credentials.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
type AuthCredential =
|
||||
| { type?: unknown; key?: unknown }
|
||||
| Array<{ type?: unknown; key?: unknown }>;
|
||||
|
||||
type AuthStorageData = Record<string, AuthCredential>;
|
||||
|
||||
const AUTH_ENV_KEYS = [
|
||||
["anthropic", "ANTHROPIC_API_KEY"],
|
||||
["openai", "OPENAI_API_KEY"],
|
||||
["github-copilot", "GITHUB_TOKEN"],
|
||||
["google", "GEMINI_API_KEY"],
|
||||
["groq", "GROQ_API_KEY"],
|
||||
["xai", "XAI_API_KEY"],
|
||||
["openrouter", "OPENROUTER_API_KEY"],
|
||||
["mistral", "MISTRAL_API_KEY"],
|
||||
["ollama-cloud", "OLLAMA_API_KEY"],
|
||||
["custom-openai", "CUSTOM_OPENAI_API_KEY"],
|
||||
["cerebras", "CEREBRAS_API_KEY"],
|
||||
["azure-openai-responses", "AZURE_OPENAI_API_KEY"],
|
||||
["vercel-ai-gateway", "AI_GATEWAY_API_KEY"],
|
||||
["zai", "ZAI_API_KEY"],
|
||||
["minimax", "MINIMAX_API_KEY"],
|
||||
["minimax-cn", "MINIMAX_CN_API_KEY"],
|
||||
["huggingface", "HF_TOKEN"],
|
||||
["opencode", "OPENCODE_API_KEY"],
|
||||
["opencode-go", "OPENCODE_API_KEY"],
|
||||
["kimi-coding", "KIMI_API_KEY"],
|
||||
["alibaba-coding-plan", "ALIBABA_API_KEY"],
|
||||
["brave", "BRAVE_API_KEY"],
|
||||
["brave_answers", "BRAVE_ANSWERS_KEY"],
|
||||
["context7", "CONTEXT7_API_KEY"],
|
||||
["jina", "JINA_API_KEY"],
|
||||
["tavily", "TAVILY_API_KEY"],
|
||||
["slack_bot", "SLACK_BOT_TOKEN"],
|
||||
["discord_bot", "DISCORD_BOT_TOKEN"],
|
||||
["telegram_bot", "TELEGRAM_BOT_TOKEN"],
|
||||
] as const;
|
||||
|
||||
function expandHome(pathValue: string): string {
|
||||
if (pathValue === "~") return homedir();
|
||||
if (pathValue.startsWith("~/")) return join(homedir(), pathValue.slice(2));
|
||||
return pathValue;
|
||||
}
|
||||
|
||||
function getStoredApiKey(data: AuthStorageData, providerId: string): string | undefined {
|
||||
const raw = data[providerId];
|
||||
const credentials = Array.isArray(raw) ? raw : raw ? [raw] : [];
|
||||
|
||||
for (const credential of credentials) {
|
||||
if (credential?.type !== "api_key") continue;
|
||||
if (typeof credential.key !== "string") continue;
|
||||
if (credential.key.trim().length === 0) continue;
|
||||
return credential.key;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function resolveAuthPath(env: NodeJS.ProcessEnv = process.env): string {
|
||||
const agentDir = env.GSD_CODING_AGENT_DIR?.trim();
|
||||
if (agentDir) return join(expandHome(agentDir), "auth.json");
|
||||
return join(homedir(), ".gsd", "agent", "auth.json");
|
||||
}
|
||||
|
||||
export function loadStoredCredentialEnvKeys(options: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
authPath?: string;
|
||||
} = {}): string[] {
|
||||
const env = options.env ?? process.env;
|
||||
const authPath = options.authPath ?? resolveAuthPath(env);
|
||||
if (!existsSync(authPath)) return [];
|
||||
|
||||
let parsed: AuthStorageData;
|
||||
try {
|
||||
const raw = readFileSync(authPath, "utf-8");
|
||||
const data = JSON.parse(raw) as unknown;
|
||||
if (!data || typeof data !== "object" || Array.isArray(data)) return [];
|
||||
parsed = data as AuthStorageData;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
const loaded: string[] = [];
|
||||
for (const [providerId, envVar] of AUTH_ENV_KEYS) {
|
||||
if (env[envVar]) continue;
|
||||
const key = getStoredApiKey(parsed, providerId);
|
||||
if (!key) continue;
|
||||
env[envVar] = key;
|
||||
loaded.push(envVar);
|
||||
}
|
||||
|
||||
return loaded;
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ import { tmpdir } from "node:os";
|
|||
import { randomUUID } from "node:crypto";
|
||||
|
||||
import { _getAdapter, closeDatabase } from "../../../src/resources/extensions/gsd/gsd-db.ts";
|
||||
import { registerWorkflowTools } from "./workflow-tools.ts";
|
||||
import { registerWorkflowTools, WORKFLOW_TOOL_NAMES } from "./workflow-tools.ts";
|
||||
|
||||
function makeTmpBase(): string {
|
||||
const base = join(tmpdir(), `gsd-mcp-workflow-${randomUUID()}`);
|
||||
|
|
@ -68,33 +68,12 @@ function makeMockServer() {
|
|||
}
|
||||
|
||||
describe("workflow MCP tools", () => {
|
||||
it("registers the seventeen workflow tools", () => {
|
||||
it("registers the full headless-safe workflow tool surface", () => {
|
||||
const server = makeMockServer();
|
||||
registerWorkflowTools(server as any);
|
||||
|
||||
assert.equal(server.tools.length, 17);
|
||||
assert.deepEqual(
|
||||
server.tools.map((t) => t.name),
|
||||
[
|
||||
"gsd_plan_milestone",
|
||||
"gsd_plan_slice",
|
||||
"gsd_replan_slice",
|
||||
"gsd_slice_replan",
|
||||
"gsd_slice_complete",
|
||||
"gsd_complete_slice",
|
||||
"gsd_complete_milestone",
|
||||
"gsd_milestone_complete",
|
||||
"gsd_validate_milestone",
|
||||
"gsd_milestone_validate",
|
||||
"gsd_reassess_roadmap",
|
||||
"gsd_roadmap_reassess",
|
||||
"gsd_save_gate_result",
|
||||
"gsd_summary_save",
|
||||
"gsd_task_complete",
|
||||
"gsd_complete_task",
|
||||
"gsd_milestone_status",
|
||||
],
|
||||
);
|
||||
assert.equal(server.tools.length, WORKFLOW_TOOL_NAMES.length);
|
||||
assert.deepEqual(server.tools.map((t) => t.name), [...WORKFLOW_TOOL_NAMES]);
|
||||
});
|
||||
|
||||
it("gsd_summary_save writes artifact through the shared executor", async () => {
|
||||
|
|
@ -405,6 +384,116 @@ describe("workflow MCP tools", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("gsd_requirement_save opens the DB before inline requirement writes", async () => {
|
||||
const base = makeTmpBase();
|
||||
try {
|
||||
const server = makeMockServer();
|
||||
registerWorkflowTools(server as any);
|
||||
const requirementTool = server.tools.find((t) => t.name === "gsd_requirement_save");
|
||||
assert.ok(requirementTool, "requirement tool should be registered");
|
||||
|
||||
closeDatabase();
|
||||
|
||||
const result = await requirementTool!.handler({
|
||||
projectDir: base,
|
||||
class: "operability",
|
||||
description: "Inline MCP requirement save regression",
|
||||
why: "Reproduce missing ensureDbOpen in workflow-tools",
|
||||
source: "user",
|
||||
status: "active",
|
||||
primary_owner: "M010/S10",
|
||||
validation: "n/a",
|
||||
});
|
||||
|
||||
assert.match((result as any).content[0].text as string, /Saved requirement R\d+/);
|
||||
assert.ok(existsSync(join(base, ".gsd", "REQUIREMENTS.md")), "REQUIREMENTS.md should be written to disk");
|
||||
const row = _getAdapter()!
|
||||
.prepare("SELECT id, class, description FROM requirements WHERE description = ?")
|
||||
.get("Inline MCP requirement save regression") as Record<string, unknown> | undefined;
|
||||
assert.ok(row, "requirement should be written to the database");
|
||||
assert.equal(row["class"], "operability");
|
||||
} finally {
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
|
||||
it("gsd_plan_task reopens the DB before inline task planning writes", async () => {
|
||||
const base = makeTmpBase();
|
||||
try {
|
||||
const server = makeMockServer();
|
||||
registerWorkflowTools(server as any);
|
||||
const milestoneTool = server.tools.find((t) => t.name === "gsd_plan_milestone");
|
||||
const sliceTool = server.tools.find((t) => t.name === "gsd_plan_slice");
|
||||
const taskTool = server.tools.find((t) => t.name === "gsd_plan_task");
|
||||
assert.ok(milestoneTool, "milestone planning tool should be registered");
|
||||
assert.ok(sliceTool, "slice planning tool should be registered");
|
||||
assert.ok(taskTool, "task planning tool should be registered");
|
||||
|
||||
await milestoneTool!.handler({
|
||||
projectDir: base,
|
||||
milestoneId: "M010",
|
||||
title: "Inline task planning DB reopen",
|
||||
vision: "Seed a slice, close the DB, then plan another task inline.",
|
||||
slices: [
|
||||
{
|
||||
sliceId: "S10",
|
||||
title: "Inline task planning",
|
||||
risk: "medium",
|
||||
depends: [],
|
||||
demo: "Inline gsd_plan_task reopens the DB after it was closed.",
|
||||
goal: "Preserve MCP task planning after the DB adapter is closed.",
|
||||
successCriteria: "The second task plan persists after a closed DB is reopened.",
|
||||
proofLevel: "integration",
|
||||
integrationClosure: "The inline MCP handler reopens the DB before planning.",
|
||||
observabilityImpact: "workflow-tools MCP tests cover the inline reopen path.",
|
||||
},
|
||||
],
|
||||
});
|
||||
await sliceTool!.handler({
|
||||
projectDir: base,
|
||||
milestoneId: "M010",
|
||||
sliceId: "S10",
|
||||
goal: "Create the initial slice plan before closing the DB.",
|
||||
tasks: [
|
||||
{
|
||||
taskId: "T10",
|
||||
title: "Seed existing task",
|
||||
description: "Create the initial task plan before closing the DB.",
|
||||
estimate: "5m",
|
||||
files: ["packages/mcp-server/src/workflow-tools.ts"],
|
||||
verify: "node --test",
|
||||
inputs: ["M010-ROADMAP.md"],
|
||||
expectedOutput: ["T10-PLAN.md"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
closeDatabase();
|
||||
|
||||
const result = await taskTool!.handler({
|
||||
projectDir: base,
|
||||
milestoneId: "M010",
|
||||
sliceId: "S10",
|
||||
taskId: "T11",
|
||||
title: "Reopen and plan",
|
||||
description: "Exercise the inline plan-task path after the DB was closed.",
|
||||
estimate: "5m",
|
||||
files: ["packages/mcp-server/src/workflow-tools.ts"],
|
||||
verify: "node --test",
|
||||
inputs: ["M010-ROADMAP.md", "S10-PLAN.md"],
|
||||
expectedOutput: ["T11-PLAN.md"],
|
||||
});
|
||||
|
||||
assert.match((result as any).content[0].text as string, /Planned task T11/);
|
||||
assert.ok(
|
||||
existsSync(join(base, ".gsd", "milestones", "M010", "slices", "S10", "tasks", "T11-PLAN.md")),
|
||||
"T11 plan should be written after reopening the DB",
|
||||
);
|
||||
} finally {
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
|
||||
it("gsd_replan_slice and gsd_slice_replan work end-to-end", async () => {
|
||||
const base = makeTmpBase();
|
||||
try {
|
||||
|
|
@ -974,3 +1063,31 @@ describe("workflow MCP tools", () => {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("URL scheme regex — Windows drive letter safety", () => {
|
||||
// This is the regex used in getWriteGateModuleCandidates() and
|
||||
// getWorkflowExecutorModuleCandidates() to reject non-file URL schemes.
|
||||
// It must NOT match single-letter Windows drive prefixes (C:, D:, etc.).
|
||||
const urlSchemeRegex = /^[a-z]{2,}:/i;
|
||||
|
||||
it("rejects multi-letter URL schemes", () => {
|
||||
assert.ok(urlSchemeRegex.test("http://example.com"), "http: should match");
|
||||
assert.ok(urlSchemeRegex.test("https://example.com"), "https: should match");
|
||||
assert.ok(urlSchemeRegex.test("ftp://files.example.com"), "ftp: should match");
|
||||
assert.ok(urlSchemeRegex.test("file:///C:/Users"), "file: should match");
|
||||
assert.ok(urlSchemeRegex.test("node:fs"), "node: should match");
|
||||
});
|
||||
|
||||
it("allows single-letter Windows drive prefixes", () => {
|
||||
assert.ok(!urlSchemeRegex.test("C:\\Users\\user\\project"), "C:\\ should not match");
|
||||
assert.ok(!urlSchemeRegex.test("D:\\other\\path"), "D:\\ should not match");
|
||||
assert.ok(!urlSchemeRegex.test("c:\\lowercase\\drive"), "c:\\ should not match");
|
||||
assert.ok(!urlSchemeRegex.test("E:/forward/slash/path"), "E:/ should not match");
|
||||
});
|
||||
|
||||
it("allows bare filesystem paths", () => {
|
||||
assert.ok(!urlSchemeRegex.test("/usr/local/lib/module.js"), "unix absolute path should not match");
|
||||
assert.ok(!urlSchemeRegex.test("./relative/path.js"), "relative path should not match");
|
||||
assert.ok(!urlSchemeRegex.test("../parent/path.js"), "parent relative path should not match");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -244,6 +244,10 @@ type WorkflowWriteGateModule = {
|
|||
) => { block: boolean; reason?: string };
|
||||
};
|
||||
|
||||
type WorkflowDbBootstrapModule = {
|
||||
ensureDbOpen: (basePath?: string) => Promise<boolean>;
|
||||
};
|
||||
|
||||
let workflowToolExecutorsPromise: Promise<WorkflowToolExecutors> | null = null;
|
||||
let workflowExecutionQueue: Promise<void> = Promise.resolve();
|
||||
let workflowWriteGatePromise: Promise<WorkflowWriteGateModule> | null = null;
|
||||
|
|
@ -318,7 +322,7 @@ function getWriteGateModuleCandidates(): string[] {
|
|||
const candidates: string[] = [];
|
||||
const explicitModule = process.env.GSD_WORKFLOW_WRITE_GATE_MODULE?.trim();
|
||||
if (explicitModule) {
|
||||
if (/^[a-z]+:/i.test(explicitModule) && !explicitModule.startsWith("file:")) {
|
||||
if (/^[a-z]{2,}:/i.test(explicitModule) && !explicitModule.startsWith("file:")) {
|
||||
throw new Error("GSD_WORKFLOW_WRITE_GATE_MODULE only supports file: URLs or filesystem paths.");
|
||||
}
|
||||
candidates.push(explicitModule.startsWith("file:") ? explicitModule : toFileUrl(explicitModule));
|
||||
|
|
@ -326,6 +330,7 @@ function getWriteGateModuleCandidates(): string[] {
|
|||
|
||||
candidates.push(
|
||||
new URL("../../../src/resources/extensions/gsd/bootstrap/write-gate.js", import.meta.url).href,
|
||||
new URL("../../../dist/resources/extensions/gsd/bootstrap/write-gate.js", import.meta.url).href,
|
||||
new URL("../../../src/resources/extensions/gsd/bootstrap/write-gate.ts", import.meta.url).href,
|
||||
);
|
||||
|
||||
|
|
@ -336,11 +341,46 @@ function toFileUrl(modulePath: string): string {
|
|||
return pathToFileURL(resolve(modulePath)).href;
|
||||
}
|
||||
|
||||
/** @internal — exported for testing only */
|
||||
export function _buildImportCandidates(relativePath: string): string[] {
|
||||
// Build candidate paths: try the given path first, then swap src/<->dist/
|
||||
// and try .ts extension. This handles both dev (tsx from src/) and prod
|
||||
// (compiled from dist/) execution contexts.
|
||||
const candidates: string[] = [relativePath];
|
||||
const swapped = relativePath.includes("/src/")
|
||||
? relativePath.replace("/src/", "/dist/")
|
||||
: relativePath.includes("/dist/")
|
||||
? relativePath.replace("/dist/", "/src/")
|
||||
: null;
|
||||
if (swapped) candidates.push(swapped);
|
||||
// Also try .ts variants for dev-mode tsx execution
|
||||
if (relativePath.endsWith(".js")) {
|
||||
candidates.push(relativePath.replace(/\.js$/, ".ts"));
|
||||
if (swapped) candidates.push(swapped.replace(/\.js$/, ".ts"));
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
async function importLocalModule<T>(relativePath: string): Promise<T> {
|
||||
const candidates = _buildImportCandidates(relativePath)
|
||||
.map((p) => new URL(p, import.meta.url).href);
|
||||
|
||||
let lastErr: unknown;
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
return await import(candidate) as T;
|
||||
} catch (err) {
|
||||
lastErr = err;
|
||||
}
|
||||
}
|
||||
throw lastErr;
|
||||
}
|
||||
|
||||
function getWorkflowExecutorModuleCandidates(env: NodeJS.ProcessEnv = process.env): string[] {
|
||||
const candidates: string[] = [];
|
||||
const explicitModule = env.GSD_WORKFLOW_EXECUTORS_MODULE?.trim();
|
||||
if (explicitModule) {
|
||||
if (/^[a-z]+:/i.test(explicitModule) && !explicitModule.startsWith("file:")) {
|
||||
if (/^[a-z]{2,}:/i.test(explicitModule) && !explicitModule.startsWith("file:")) {
|
||||
throw new Error("GSD_WORKFLOW_EXECUTORS_MODULE only supports file: URLs or filesystem paths.");
|
||||
}
|
||||
candidates.push(explicitModule.startsWith("file:") ? explicitModule : toFileUrl(explicitModule));
|
||||
|
|
@ -348,6 +388,7 @@ function getWorkflowExecutorModuleCandidates(env: NodeJS.ProcessEnv = process.en
|
|||
|
||||
candidates.push(
|
||||
new URL("../../../src/resources/extensions/gsd/tools/workflow-tool-executors.js", import.meta.url).href,
|
||||
new URL("../../../dist/resources/extensions/gsd/tools/workflow-tool-executors.js", import.meta.url).href,
|
||||
new URL("../../../src/resources/extensions/gsd/tools/workflow-tool-executors.ts", import.meta.url).href,
|
||||
);
|
||||
|
||||
|
|
@ -420,6 +461,38 @@ interface McpToolServer {
|
|||
): unknown;
|
||||
}
|
||||
|
||||
export const WORKFLOW_TOOL_NAMES = [
|
||||
"gsd_decision_save",
|
||||
"gsd_save_decision",
|
||||
"gsd_requirement_update",
|
||||
"gsd_update_requirement",
|
||||
"gsd_requirement_save",
|
||||
"gsd_save_requirement",
|
||||
"gsd_milestone_generate_id",
|
||||
"gsd_generate_milestone_id",
|
||||
"gsd_plan_milestone",
|
||||
"gsd_plan_slice",
|
||||
"gsd_plan_task",
|
||||
"gsd_task_plan",
|
||||
"gsd_replan_slice",
|
||||
"gsd_slice_replan",
|
||||
"gsd_slice_complete",
|
||||
"gsd_complete_slice",
|
||||
"gsd_skip_slice",
|
||||
"gsd_complete_milestone",
|
||||
"gsd_milestone_complete",
|
||||
"gsd_validate_milestone",
|
||||
"gsd_milestone_validate",
|
||||
"gsd_reassess_roadmap",
|
||||
"gsd_roadmap_reassess",
|
||||
"gsd_save_gate_result",
|
||||
"gsd_summary_save",
|
||||
"gsd_task_complete",
|
||||
"gsd_complete_task",
|
||||
"gsd_milestone_status",
|
||||
"gsd_journal_query",
|
||||
] as const;
|
||||
|
||||
async function runSerializedWorkflowOperation<T>(fn: () => Promise<T>): Promise<T> {
|
||||
// The shared DB adapter and workflow log base path are process-global, so
|
||||
// workflow MCP mutations must not overlap within a single server process.
|
||||
|
|
@ -437,6 +510,22 @@ async function runSerializedWorkflowOperation<T>(fn: () => Promise<T>): Promise<
|
|||
}
|
||||
}
|
||||
|
||||
async function runSerializedWorkflowDbOperation<T>(
|
||||
projectDir: string,
|
||||
fn: () => Promise<T>,
|
||||
): Promise<T> {
|
||||
return runSerializedWorkflowOperation(async () => {
|
||||
const { ensureDbOpen } = await importLocalModule<WorkflowDbBootstrapModule>(
|
||||
"../../../src/resources/extensions/gsd/bootstrap/dynamic-tools.js",
|
||||
);
|
||||
const dbAvailable = await ensureDbOpen(projectDir);
|
||||
if (!dbAvailable) {
|
||||
throw new Error("GSD database is not available");
|
||||
}
|
||||
return fn();
|
||||
});
|
||||
}
|
||||
|
||||
async function enforceWorkflowWriteGate(
|
||||
toolName: string,
|
||||
projectDir: string,
|
||||
|
|
@ -566,6 +655,15 @@ async function handleSaveGateResult(
|
|||
return runSerializedWorkflowOperation(() => executeSaveGateResult(params, projectDir));
|
||||
}
|
||||
|
||||
async function ensureMilestoneDbRow(milestoneId: string): Promise<void> {
|
||||
try {
|
||||
const { insertMilestone } = await importLocalModule<any>("../../../src/resources/extensions/gsd/gsd-db.js");
|
||||
insertMilestone({ id: milestoneId, status: "queued" });
|
||||
} catch {
|
||||
// Ignore pre-existing rows or transient DB availability issues.
|
||||
}
|
||||
}
|
||||
|
||||
const projectDirParam = z.string().describe("Absolute path to the project directory within the configured workflow root");
|
||||
|
||||
const planMilestoneParams = {
|
||||
|
|
@ -689,7 +787,7 @@ const saveGateResultParams = {
|
|||
projectDir: projectDirParam,
|
||||
milestoneId: z.string().describe("Milestone ID (e.g. M001)"),
|
||||
sliceId: z.string().describe("Slice ID (e.g. S01)"),
|
||||
gateId: z.enum(["Q3", "Q4", "Q5", "Q6", "Q7", "Q8"]).describe("Gate ID"),
|
||||
gateId: z.enum(["Q3", "Q4", "Q5", "Q6", "Q7", "Q8", "MV01", "MV02", "MV03", "MV04"]).describe("Gate ID"),
|
||||
taskId: z.string().optional().describe("Task ID for task-scoped gates"),
|
||||
verdict: z.enum(["pass", "flag", "omitted"]).describe("Gate verdict"),
|
||||
rationale: z.string().describe("One-sentence justification"),
|
||||
|
|
@ -772,6 +870,73 @@ const summarySaveParams = {
|
|||
};
|
||||
const summarySaveSchema = z.object(summarySaveParams);
|
||||
|
||||
const decisionSaveParams = {
|
||||
projectDir: projectDirParam,
|
||||
scope: z.string().describe("Scope of the decision (e.g. architecture, library, observability)"),
|
||||
decision: z.string().describe("What is being decided"),
|
||||
choice: z.string().describe("The choice made"),
|
||||
rationale: z.string().describe("Why this choice was made"),
|
||||
revisable: z.string().optional().describe("Whether this can be revisited"),
|
||||
when_context: z.string().optional().describe("When/context for the decision"),
|
||||
made_by: z.enum(["human", "agent", "collaborative"]).optional().describe("Who made the decision"),
|
||||
};
|
||||
const decisionSaveSchema = z.object(decisionSaveParams);
|
||||
|
||||
const requirementUpdateParams = {
|
||||
projectDir: projectDirParam,
|
||||
id: z.string().describe("Requirement ID (e.g. R001)"),
|
||||
status: z.string().optional().describe("New status"),
|
||||
validation: z.string().optional().describe("Validation criteria or proof"),
|
||||
notes: z.string().optional().describe("Additional notes"),
|
||||
description: z.string().optional().describe("Updated description"),
|
||||
primary_owner: z.string().optional().describe("Primary owning slice"),
|
||||
supporting_slices: z.string().optional().describe("Supporting slices"),
|
||||
};
|
||||
const requirementUpdateSchema = z.object(requirementUpdateParams);
|
||||
|
||||
const requirementSaveParams = {
|
||||
projectDir: projectDirParam,
|
||||
class: z.string().describe("Requirement class"),
|
||||
description: z.string().describe("Short description of the requirement"),
|
||||
why: z.string().describe("Why this requirement matters"),
|
||||
source: z.string().describe("Origin of the requirement"),
|
||||
status: z.string().optional().describe("Requirement status"),
|
||||
primary_owner: z.string().optional().describe("Primary owning slice"),
|
||||
supporting_slices: z.string().optional().describe("Supporting slices"),
|
||||
validation: z.string().optional().describe("Validation criteria"),
|
||||
notes: z.string().optional().describe("Additional notes"),
|
||||
};
|
||||
const requirementSaveSchema = z.object(requirementSaveParams);
|
||||
|
||||
const milestoneGenerateIdParams = {
|
||||
projectDir: projectDirParam,
|
||||
};
|
||||
const milestoneGenerateIdSchema = z.object(milestoneGenerateIdParams);
|
||||
|
||||
const planTaskParams = {
|
||||
projectDir: projectDirParam,
|
||||
milestoneId: z.string().describe("Milestone ID (e.g. M001)"),
|
||||
sliceId: z.string().describe("Slice ID (e.g. S01)"),
|
||||
taskId: z.string().describe("Task ID (e.g. T01)"),
|
||||
title: z.string().describe("Task title"),
|
||||
description: z.string().describe("Task description / steps block"),
|
||||
estimate: z.string().describe("Task estimate"),
|
||||
files: z.array(z.string()).describe("Files likely touched"),
|
||||
verify: z.string().describe("Verification command or block"),
|
||||
inputs: z.array(z.string()).describe("Input files or references"),
|
||||
expectedOutput: z.array(z.string()).describe("Expected output files or artifacts"),
|
||||
observabilityImpact: z.string().optional().describe("Task observability impact"),
|
||||
};
|
||||
const planTaskSchema = z.object(planTaskParams);
|
||||
|
||||
const skipSliceParams = {
|
||||
projectDir: projectDirParam,
|
||||
sliceId: z.string().describe("Slice ID (e.g. S02)"),
|
||||
milestoneId: z.string().describe("Milestone ID (e.g. M003)"),
|
||||
reason: z.string().optional().describe("Reason for skipping this slice"),
|
||||
};
|
||||
const skipSliceSchema = z.object(skipSliceParams);
|
||||
|
||||
const taskCompleteParams = {
|
||||
projectDir: projectDirParam,
|
||||
taskId: z.string().describe("Task ID (e.g. T01)"),
|
||||
|
|
@ -803,7 +968,171 @@ const milestoneStatusParams = {
|
|||
};
|
||||
const milestoneStatusSchema = z.object(milestoneStatusParams);
|
||||
|
||||
const journalQueryParams = {
|
||||
projectDir: projectDirParam,
|
||||
flowId: z.string().optional().describe("Filter by flow ID"),
|
||||
unitId: z.string().optional().describe("Filter by unit ID"),
|
||||
rule: z.string().optional().describe("Filter by rule name"),
|
||||
eventType: z.string().optional().describe("Filter by event type"),
|
||||
after: z.string().optional().describe("ISO-8601 lower bound (inclusive)"),
|
||||
before: z.string().optional().describe("ISO-8601 upper bound (inclusive)"),
|
||||
limit: z.number().optional().describe("Maximum entries to return"),
|
||||
};
|
||||
const journalQuerySchema = z.object(journalQueryParams);
|
||||
|
||||
export function registerWorkflowTools(server: McpToolServer): void {
|
||||
server.tool(
|
||||
"gsd_decision_save",
|
||||
"Record a project decision to the GSD database and regenerate DECISIONS.md.",
|
||||
decisionSaveParams,
|
||||
async (args: Record<string, unknown>) => {
|
||||
const parsed = parseWorkflowArgs(decisionSaveSchema, args);
|
||||
const { projectDir, ...params } = parsed;
|
||||
await enforceWorkflowWriteGate("gsd_decision_save", projectDir);
|
||||
const result = await runSerializedWorkflowDbOperation(projectDir, async () => {
|
||||
const { saveDecisionToDb } = await importLocalModule<any>("../../../src/resources/extensions/gsd/db-writer.js");
|
||||
return saveDecisionToDb(params, projectDir);
|
||||
});
|
||||
return { content: [{ type: "text" as const, text: `Saved decision ${result.id}` }] };
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"gsd_save_decision",
|
||||
"Alias for gsd_decision_save. Record a project decision to the GSD database and regenerate DECISIONS.md.",
|
||||
decisionSaveParams,
|
||||
async (args: Record<string, unknown>) => {
|
||||
const parsed = parseWorkflowArgs(decisionSaveSchema, args);
|
||||
const { projectDir, ...params } = parsed;
|
||||
await enforceWorkflowWriteGate("gsd_decision_save", projectDir);
|
||||
const result = await runSerializedWorkflowDbOperation(projectDir, async () => {
|
||||
const { saveDecisionToDb } = await importLocalModule<any>("../../../src/resources/extensions/gsd/db-writer.js");
|
||||
return saveDecisionToDb(params, projectDir);
|
||||
});
|
||||
return { content: [{ type: "text" as const, text: `Saved decision ${result.id}` }] };
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"gsd_requirement_update",
|
||||
"Update an existing requirement in the GSD database and regenerate REQUIREMENTS.md.",
|
||||
requirementUpdateParams,
|
||||
async (args: Record<string, unknown>) => {
|
||||
const parsed = parseWorkflowArgs(requirementUpdateSchema, args);
|
||||
const { projectDir, id, ...updates } = parsed;
|
||||
await enforceWorkflowWriteGate("gsd_requirement_update", projectDir);
|
||||
await runSerializedWorkflowDbOperation(projectDir, async () => {
|
||||
const { updateRequirementInDb } = await importLocalModule<any>("../../../src/resources/extensions/gsd/db-writer.js");
|
||||
return updateRequirementInDb(id, updates, projectDir);
|
||||
});
|
||||
return { content: [{ type: "text" as const, text: `Updated requirement ${id}` }] };
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"gsd_update_requirement",
|
||||
"Alias for gsd_requirement_update. Update an existing requirement in the GSD database and regenerate REQUIREMENTS.md.",
|
||||
requirementUpdateParams,
|
||||
async (args: Record<string, unknown>) => {
|
||||
const parsed = parseWorkflowArgs(requirementUpdateSchema, args);
|
||||
const { projectDir, id, ...updates } = parsed;
|
||||
await enforceWorkflowWriteGate("gsd_requirement_update", projectDir);
|
||||
await runSerializedWorkflowDbOperation(projectDir, async () => {
|
||||
const { updateRequirementInDb } = await importLocalModule<any>("../../../src/resources/extensions/gsd/db-writer.js");
|
||||
return updateRequirementInDb(id, updates, projectDir);
|
||||
});
|
||||
return { content: [{ type: "text" as const, text: `Updated requirement ${id}` }] };
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"gsd_requirement_save",
|
||||
"Record a new requirement to the GSD database and regenerate REQUIREMENTS.md.",
|
||||
requirementSaveParams,
|
||||
async (args: Record<string, unknown>) => {
|
||||
const parsed = parseWorkflowArgs(requirementSaveSchema, args);
|
||||
const { projectDir, ...params } = parsed;
|
||||
await enforceWorkflowWriteGate("gsd_requirement_save", projectDir);
|
||||
const result = await runSerializedWorkflowDbOperation(projectDir, async () => {
|
||||
const { saveRequirementToDb } = await importLocalModule<any>("../../../src/resources/extensions/gsd/db-writer.js");
|
||||
return saveRequirementToDb(params, projectDir);
|
||||
});
|
||||
return { content: [{ type: "text" as const, text: `Saved requirement ${result.id}` }] };
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"gsd_save_requirement",
|
||||
"Alias for gsd_requirement_save. Record a new requirement to the GSD database and regenerate REQUIREMENTS.md.",
|
||||
requirementSaveParams,
|
||||
async (args: Record<string, unknown>) => {
|
||||
const parsed = parseWorkflowArgs(requirementSaveSchema, args);
|
||||
const { projectDir, ...params } = parsed;
|
||||
await enforceWorkflowWriteGate("gsd_requirement_save", projectDir);
|
||||
const result = await runSerializedWorkflowDbOperation(projectDir, async () => {
|
||||
const { saveRequirementToDb } = await importLocalModule<any>("../../../src/resources/extensions/gsd/db-writer.js");
|
||||
return saveRequirementToDb(params, projectDir);
|
||||
});
|
||||
return { content: [{ type: "text" as const, text: `Saved requirement ${result.id}` }] };
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"gsd_milestone_generate_id",
|
||||
"Generate the next milestone ID for a new GSD milestone.",
|
||||
milestoneGenerateIdParams,
|
||||
async (args: Record<string, unknown>) => {
|
||||
const { projectDir } = parseWorkflowArgs(milestoneGenerateIdSchema, args);
|
||||
await enforceWorkflowWriteGate("gsd_milestone_generate_id", projectDir);
|
||||
const id = await runSerializedWorkflowDbOperation(projectDir, async () => {
|
||||
const {
|
||||
claimReservedId,
|
||||
findMilestoneIds,
|
||||
getReservedMilestoneIds,
|
||||
nextMilestoneId,
|
||||
} = await importLocalModule<any>("../../../src/resources/extensions/gsd/milestone-ids.js");
|
||||
const reserved = claimReservedId();
|
||||
if (reserved) {
|
||||
await ensureMilestoneDbRow(reserved);
|
||||
return reserved;
|
||||
}
|
||||
const allIds = [...new Set([...findMilestoneIds(projectDir), ...getReservedMilestoneIds()])];
|
||||
const nextId = nextMilestoneId(allIds);
|
||||
await ensureMilestoneDbRow(nextId);
|
||||
return nextId;
|
||||
});
|
||||
return { content: [{ type: "text" as const, text: id }] };
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"gsd_generate_milestone_id",
|
||||
"Alias for gsd_milestone_generate_id. Generate the next milestone ID for a new GSD milestone.",
|
||||
milestoneGenerateIdParams,
|
||||
async (args: Record<string, unknown>) => {
|
||||
const { projectDir } = parseWorkflowArgs(milestoneGenerateIdSchema, args);
|
||||
await enforceWorkflowWriteGate("gsd_milestone_generate_id", projectDir);
|
||||
const id = await runSerializedWorkflowDbOperation(projectDir, async () => {
|
||||
const {
|
||||
claimReservedId,
|
||||
findMilestoneIds,
|
||||
getReservedMilestoneIds,
|
||||
nextMilestoneId,
|
||||
} = await importLocalModule<any>("../../../src/resources/extensions/gsd/milestone-ids.js");
|
||||
const reserved = claimReservedId();
|
||||
if (reserved) {
|
||||
await ensureMilestoneDbRow(reserved);
|
||||
return reserved;
|
||||
}
|
||||
const allIds = [...new Set([...findMilestoneIds(projectDir), ...getReservedMilestoneIds()])];
|
||||
const nextId = nextMilestoneId(allIds);
|
||||
await ensureMilestoneDbRow(nextId);
|
||||
return nextId;
|
||||
});
|
||||
return { content: [{ type: "text" as const, text: id }] };
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"gsd_plan_milestone",
|
||||
"Write milestone planning state to the GSD database and render ROADMAP.md from DB.",
|
||||
|
|
@ -830,6 +1159,48 @@ export function registerWorkflowTools(server: McpToolServer): void {
|
|||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"gsd_plan_task",
|
||||
"Write task planning state to the GSD database and render tasks/T##-PLAN.md from DB.",
|
||||
planTaskParams,
|
||||
async (args: Record<string, unknown>) => {
|
||||
const parsed = parseWorkflowArgs(planTaskSchema, args);
|
||||
const { projectDir, ...params } = parsed;
|
||||
await enforceWorkflowWriteGate("gsd_plan_task", projectDir, params.milestoneId);
|
||||
const result = await runSerializedWorkflowDbOperation(projectDir, async () => {
|
||||
const { handlePlanTask } = await importLocalModule<any>("../../../src/resources/extensions/gsd/tools/plan-task.js");
|
||||
return handlePlanTask(params, projectDir);
|
||||
});
|
||||
if ("error" in result) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
return {
|
||||
content: [{ type: "text" as const, text: `Planned task ${result.taskId} (${result.sliceId}/${result.milestoneId})` }],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"gsd_task_plan",
|
||||
"Alias for gsd_plan_task. Write task planning state to the GSD database and render tasks/T##-PLAN.md from DB.",
|
||||
planTaskParams,
|
||||
async (args: Record<string, unknown>) => {
|
||||
const parsed = parseWorkflowArgs(planTaskSchema, args);
|
||||
const { projectDir, ...params } = parsed;
|
||||
await enforceWorkflowWriteGate("gsd_plan_task", projectDir, params.milestoneId);
|
||||
const result = await runSerializedWorkflowDbOperation(projectDir, async () => {
|
||||
const { handlePlanTask } = await importLocalModule<any>("../../../src/resources/extensions/gsd/tools/plan-task.js");
|
||||
return handlePlanTask(params, projectDir);
|
||||
});
|
||||
if ("error" in result) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
return {
|
||||
content: [{ type: "text" as const, text: `Planned task ${result.taskId} (${result.sliceId}/${result.milestoneId})` }],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"gsd_replan_slice",
|
||||
"Replan a slice after a blocker is discovered, preserving completed tasks and re-rendering PLAN.md + REPLAN.md.",
|
||||
|
|
@ -870,6 +1241,36 @@ export function registerWorkflowTools(server: McpToolServer): void {
|
|||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"gsd_skip_slice",
|
||||
"Mark a slice as skipped so auto-mode advances past it without executing.",
|
||||
skipSliceParams,
|
||||
async (args: Record<string, unknown>) => {
|
||||
const { projectDir, milestoneId, sliceId, reason } = parseWorkflowArgs(skipSliceSchema, args);
|
||||
await enforceWorkflowWriteGate("gsd_skip_slice", projectDir, milestoneId);
|
||||
await runSerializedWorkflowDbOperation(projectDir, async () => {
|
||||
const { getSlice, updateSliceStatus } = await importLocalModule<any>("../../../src/resources/extensions/gsd/gsd-db.js");
|
||||
const { invalidateStateCache } = await importLocalModule<any>("../../../src/resources/extensions/gsd/state.js");
|
||||
const { rebuildState } = await importLocalModule<any>("../../../src/resources/extensions/gsd/doctor.js");
|
||||
const slice = getSlice(milestoneId, sliceId);
|
||||
if (!slice) {
|
||||
throw new Error(`Slice ${sliceId} not found in milestone ${milestoneId}`);
|
||||
}
|
||||
if (slice.status === "complete" || slice.status === "done") {
|
||||
throw new Error(`Slice ${sliceId} is already complete and cannot be skipped`);
|
||||
}
|
||||
if (slice.status !== "skipped") {
|
||||
updateSliceStatus(milestoneId, sliceId, "skipped");
|
||||
invalidateStateCache();
|
||||
await rebuildState(projectDir);
|
||||
}
|
||||
});
|
||||
return {
|
||||
content: [{ type: "text" as const, text: `Skipped slice ${sliceId} (${milestoneId}). Reason: ${reason ?? "User-directed skip"}.` }],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"gsd_complete_milestone",
|
||||
"Record a completed milestone to the GSD database and render its SUMMARY.md.",
|
||||
|
|
@ -994,4 +1395,19 @@ export function registerWorkflowTools(server: McpToolServer): void {
|
|||
return runSerializedWorkflowOperation(() => executeMilestoneStatus({ milestoneId }, projectDir));
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"gsd_journal_query",
|
||||
"Query the structured event journal for auto-mode iterations.",
|
||||
journalQueryParams,
|
||||
async (args: Record<string, unknown>) => {
|
||||
const { projectDir, limit, ...filters } = parseWorkflowArgs(journalQuerySchema, args);
|
||||
const { queryJournal } = await importLocalModule<any>("../../../src/resources/extensions/gsd/journal.js");
|
||||
const entries = queryJournal(projectDir, filters).slice(0, limit ?? 100);
|
||||
if (entries.length === 0) {
|
||||
return { content: [{ type: "text" as const, text: "No matching journal entries found." }] };
|
||||
}
|
||||
return { content: [{ type: "text" as const, text: JSON.stringify(entries, null, 2) }] };
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import assert from "node:assert/strict";
|
|||
import { readFileSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { Agent } from "./agent.ts";
|
||||
import { getModel, type AssistantMessageEventStream } from "@gsd/pi-ai";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
|
|
@ -50,4 +52,84 @@ describe("Agent — activeInferenceModel (#1844 Bug 2)", () => {
|
|||
assert.ok(setLine < abortLine,
|
||||
"activeInferenceModel must be set before streaming infrastructure is created");
|
||||
});
|
||||
|
||||
it("getProviderOptions are forwarded into the provider stream call", async () => {
|
||||
let capturedOptions: Record<string, unknown> | undefined;
|
||||
const agent = new Agent({
|
||||
initialState: {
|
||||
model: getModel("anthropic", "claude-3-5-sonnet-20241022"),
|
||||
systemPrompt: "test",
|
||||
tools: [],
|
||||
},
|
||||
getProviderOptions: async () => ({ customRuntimeOption: "present" }),
|
||||
streamFn: (_model, _context, options): AssistantMessageEventStream => {
|
||||
capturedOptions = options as Record<string, unknown> | undefined;
|
||||
return {
|
||||
async *[Symbol.asyncIterator]() {
|
||||
yield {
|
||||
type: "start",
|
||||
partial: {
|
||||
role: "assistant",
|
||||
content: [],
|
||||
api: "anthropic-messages",
|
||||
provider: "anthropic",
|
||||
model: "claude-3-5-sonnet-20241022",
|
||||
usage: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 0,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
},
|
||||
stopReason: "stop",
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
};
|
||||
yield {
|
||||
type: "done",
|
||||
message: {
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "ok" }],
|
||||
api: "anthropic-messages",
|
||||
provider: "anthropic",
|
||||
model: "claude-3-5-sonnet-20241022",
|
||||
usage: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 0,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
},
|
||||
stopReason: "stop",
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
};
|
||||
},
|
||||
result: async () => ({
|
||||
role: "assistant",
|
||||
content: [{ type: "text", text: "ok" }],
|
||||
api: "anthropic-messages",
|
||||
provider: "anthropic",
|
||||
model: "claude-3-5-sonnet-20241022",
|
||||
usage: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
totalTokens: 0,
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
},
|
||||
stopReason: "stop",
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
[Symbol.asyncDispose]: async () => {},
|
||||
} as AssistantMessageEventStream;
|
||||
},
|
||||
});
|
||||
|
||||
await agent.prompt("hello");
|
||||
assert.equal(capturedOptions?.customRuntimeOption, "present");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -108,6 +108,14 @@ export interface AgentOptions {
|
|||
* switches mid-session are handled correctly.
|
||||
*/
|
||||
externalToolExecution?: (model: Model<any>) => boolean;
|
||||
|
||||
/**
|
||||
* Optional provider-specific options to merge into the next stream call.
|
||||
*
|
||||
* Use this for runtime-only callbacks or handles that should not live in
|
||||
* shared agent state, such as UI bridges for external CLI providers.
|
||||
*/
|
||||
getProviderOptions?: (model: Model<any>) => Record<string, unknown> | undefined | Promise<Record<string, unknown> | undefined>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -152,6 +160,7 @@ export class Agent {
|
|||
private _beforeToolCall?: AgentLoopConfig["beforeToolCall"];
|
||||
private _afterToolCall?: AgentLoopConfig["afterToolCall"];
|
||||
private _externalToolExecution?: (model: Model<any>) => boolean;
|
||||
private _getProviderOptions?: AgentOptions["getProviderOptions"];
|
||||
|
||||
constructor(opts: AgentOptions = {}) {
|
||||
this._state = { ...this._state, ...opts.initialState };
|
||||
|
|
@ -167,6 +176,7 @@ export class Agent {
|
|||
this._transport = opts.transport ?? "sse";
|
||||
this._maxRetryDelayMs = opts.maxRetryDelayMs;
|
||||
this._externalToolExecution = opts.externalToolExecution;
|
||||
this._getProviderOptions = opts.getProviderOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -486,8 +496,10 @@ export class Agent {
|
|||
};
|
||||
|
||||
let skipInitialSteeringPoll = options?.skipInitialSteeringPoll === true;
|
||||
const providerOptions = await this._getProviderOptions?.(model);
|
||||
|
||||
const config: AgentLoopConfig = {
|
||||
...(providerOptions ?? {}),
|
||||
model,
|
||||
reasoning,
|
||||
sessionId: this._sessionId,
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@ export function getEnvApiKey(provider: any): string | undefined {
|
|||
"opencode-go": "OPENCODE_API_KEY",
|
||||
"kimi-coding": "KIMI_API_KEY",
|
||||
"alibaba-coding-plan": "ALIBABA_API_KEY",
|
||||
"alibaba-dashscope": "DASHSCOPE_API_KEY",
|
||||
ollama: "OLLAMA_API_KEY",
|
||||
"ollama-cloud": "OLLAMA_API_KEY",
|
||||
"custom-openai": "CUSTOM_OPENAI_API_KEY",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,10 @@ export * from "./providers/google-vertex.js";
|
|||
export * from "./providers/mistral.js";
|
||||
export * from "./providers/openai-completions.js";
|
||||
export * from "./providers/openai-responses.js";
|
||||
export * from "./providers/provider-capabilities.js";
|
||||
export * from "./providers/register-builtins.js";
|
||||
export type { ProviderSwitchReport } from "./providers/transform-messages.js";
|
||||
export { createEmptyReport, hasTransformations, transformMessagesWithReport } from "./providers/transform-messages.js";
|
||||
export * from "./stream.js";
|
||||
export * from "./types.js";
|
||||
export * from "./utils/event-stream.js";
|
||||
|
|
|
|||
|
|
@ -170,6 +170,104 @@ export const CUSTOM_MODELS = {
|
|||
} satisfies Model<"openai-completions">,
|
||||
},
|
||||
|
||||
// ─── Alibaba DashScope ───────────────────────────────────────────────
|
||||
// Regular DashScope API for users without the Coding Plan.
|
||||
// Uses the international OpenAI-compatible endpoint.
|
||||
// Requires DASHSCOPE_API_KEY from: dashscope.console.aliyun.com
|
||||
// Pricing: https://www.alibabacloud.com/help/en/model-studio/model-pricing
|
||||
"alibaba-dashscope": {
|
||||
"qwen3-max": {
|
||||
id: "qwen3-max",
|
||||
name: "Qwen3 Max",
|
||||
api: "openai-completions",
|
||||
provider: "alibaba-dashscope",
|
||||
baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 1.2,
|
||||
output: 6,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 1000000,
|
||||
maxTokens: 32768,
|
||||
compat: { thinkingFormat: "qwen", supportsDeveloperRole: false },
|
||||
} satisfies Model<"openai-completions">,
|
||||
"qwen3.5-plus": {
|
||||
id: "qwen3.5-plus",
|
||||
name: "Qwen3.5 Plus",
|
||||
api: "openai-completions",
|
||||
provider: "alibaba-dashscope",
|
||||
baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.4,
|
||||
output: 1.2,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 1000000,
|
||||
maxTokens: 65536,
|
||||
compat: { thinkingFormat: "qwen", supportsDeveloperRole: false },
|
||||
} satisfies Model<"openai-completions">,
|
||||
"qwen3.5-flash": {
|
||||
id: "qwen3.5-flash",
|
||||
name: "Qwen3.5 Flash",
|
||||
api: "openai-completions",
|
||||
provider: "alibaba-dashscope",
|
||||
baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.1,
|
||||
output: 0.4,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 1000000,
|
||||
maxTokens: 32768,
|
||||
compat: { supportsDeveloperRole: false },
|
||||
} satisfies Model<"openai-completions">,
|
||||
"qwen3-coder-plus": {
|
||||
id: "qwen3-coder-plus",
|
||||
name: "Qwen3 Coder Plus",
|
||||
api: "openai-completions",
|
||||
provider: "alibaba-dashscope",
|
||||
baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 1.0,
|
||||
output: 5.0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 1000000,
|
||||
maxTokens: 65536,
|
||||
compat: { supportsDeveloperRole: false },
|
||||
} satisfies Model<"openai-completions">,
|
||||
"qwen3.6-plus": {
|
||||
id: "qwen3.6-plus",
|
||||
name: "Qwen3.6 Plus",
|
||||
api: "openai-completions",
|
||||
provider: "alibaba-dashscope",
|
||||
baseUrl: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: {
|
||||
input: 0.5,
|
||||
output: 3.0,
|
||||
cacheRead: 0,
|
||||
cacheWrite: 0,
|
||||
},
|
||||
contextWindow: 1000000,
|
||||
maxTokens: 65536,
|
||||
compat: { thinkingFormat: "qwen", supportsDeveloperRole: false },
|
||||
} satisfies Model<"openai-completions">,
|
||||
},
|
||||
|
||||
// ─── Z.AI (GLM-5.1) ────────────────────────────────────────────────
|
||||
// GLM-5.1 is the latest GLM model from Zhipu AI, not yet in models.dev.
|
||||
// Uses the Z.AI Coding Plan endpoint (OpenAI-compatible).
|
||||
|
|
|
|||
373
packages/pi-ai/src/models.generated.test.ts
Normal file
373
packages/pi-ai/src/models.generated.test.ts
Normal file
|
|
@ -0,0 +1,373 @@
|
|||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { MODELS } from "./models.generated.js";
|
||||
import { getModel, getModels, getProviders } from "./models.js";
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Regression: qwen/qwen3.6-plus missing from OpenRouter (issue #3582)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
describe("regression #3582 — qwen/qwen3.6-plus available via openrouter", () => {
|
||||
it("qwen/qwen3.6-plus exists in MODELS['openrouter']", () => {
|
||||
const model = MODELS["openrouter"]["qwen/qwen3.6-plus" as keyof (typeof MODELS)["openrouter"]];
|
||||
assert.ok(model, "qwen/qwen3.6-plus must be present in MODELS.openrouter");
|
||||
});
|
||||
|
||||
it("qwen/qwen3.6-plus is accessible via getModel()", () => {
|
||||
const model = getModel("openrouter", "qwen/qwen3.6-plus" as any);
|
||||
assert.ok(model, "getModel('openrouter', 'qwen/qwen3.6-plus') must return a model");
|
||||
});
|
||||
|
||||
it("qwen/qwen3.6-plus has id matching its registry key", () => {
|
||||
const model = getModel("openrouter", "qwen/qwen3.6-plus" as any);
|
||||
assert.equal(model.id, "qwen/qwen3.6-plus");
|
||||
});
|
||||
|
||||
it("qwen/qwen3.6-plus has provider set to openrouter", () => {
|
||||
const model = getModel("openrouter", "qwen/qwen3.6-plus" as any);
|
||||
assert.equal(model.provider, "openrouter");
|
||||
});
|
||||
|
||||
it("qwen/qwen3.6-plus has reasoning enabled", () => {
|
||||
const model = getModel("openrouter", "qwen/qwen3.6-plus" as any);
|
||||
assert.equal(model.reasoning, true, "Qwen3.6 Plus is a reasoning model");
|
||||
});
|
||||
|
||||
it("qwen/qwen3.6-plus has 1M context window", () => {
|
||||
const model = getModel("openrouter", "qwen/qwen3.6-plus" as any);
|
||||
assert.equal(model.contextWindow, 1_000_000);
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Regression: z-ai/glm-5.1 missing from OpenRouter (issue #4069)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
describe("regression #4069 — z-ai/glm-5.1 available via openrouter", () => {
|
||||
it("z-ai/glm-5.1 exists in MODELS['openrouter']", () => {
|
||||
const model = MODELS["openrouter"]["z-ai/glm-5.1" as keyof (typeof MODELS)["openrouter"]];
|
||||
assert.ok(model, "z-ai/glm-5.1 must be present in MODELS.openrouter");
|
||||
});
|
||||
|
||||
it("z-ai/glm-5.1 is accessible via getModel()", () => {
|
||||
const model = getModel("openrouter", "z-ai/glm-5.1" as any);
|
||||
assert.ok(model, "getModel('openrouter', 'z-ai/glm-5.1') must return a model");
|
||||
});
|
||||
|
||||
it("z-ai/glm-5.1 has id matching its registry key", () => {
|
||||
const model = getModel("openrouter", "z-ai/glm-5.1" as any);
|
||||
assert.equal(model.id, "z-ai/glm-5.1");
|
||||
});
|
||||
|
||||
it("z-ai/glm-5.1 has provider set to openrouter", () => {
|
||||
const model = getModel("openrouter", "z-ai/glm-5.1" as any);
|
||||
assert.equal(model.provider, "openrouter");
|
||||
});
|
||||
|
||||
it("z-ai/glm-5.1 has a positive context window", () => {
|
||||
const model = getModel("openrouter", "z-ai/glm-5.1" as any);
|
||||
assert.ok(model.contextWindow > 0);
|
||||
});
|
||||
|
||||
it("z-ai/glm-5.1 uses the OpenRouter base URL", () => {
|
||||
const model = getModel("openrouter", "z-ai/glm-5.1" as any);
|
||||
assert.equal(model.baseUrl, "https://openrouter.ai/api/v1");
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Structural invariants — every model in MODELS must be well-formed
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
describe("MODELS structural invariants", () => {
|
||||
type ModelEntry = { providerKey: string; modelKey: string; model: Record<string, unknown> };
|
||||
|
||||
function allModels(): ModelEntry[] {
|
||||
const entries: ModelEntry[] = [];
|
||||
for (const [providerKey, providerModels] of Object.entries(MODELS)) {
|
||||
for (const [modelKey, model] of Object.entries(providerModels)) {
|
||||
entries.push({ providerKey, modelKey, model: model as Record<string, unknown> });
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
it("every model's id field matches its key in MODELS", () => {
|
||||
const mismatches: string[] = [];
|
||||
for (const { providerKey, modelKey, model } of allModels()) {
|
||||
if (model["id"] !== modelKey) {
|
||||
mismatches.push(`${providerKey}/${modelKey}: id="${model["id"]}"`);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(mismatches, [], `Models where 'id' doesn't match registry key:\n ${mismatches.join("\n ")}`);
|
||||
});
|
||||
|
||||
it("every model's provider field matches its parent provider key", () => {
|
||||
const mismatches: string[] = [];
|
||||
for (const { providerKey, modelKey, model } of allModels()) {
|
||||
if (model["provider"] !== providerKey) {
|
||||
mismatches.push(`${providerKey}/${modelKey}: provider="${model["provider"]}"`);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(mismatches, [], `Models where 'provider' doesn't match parent key:\n ${mismatches.join("\n ")}`);
|
||||
});
|
||||
|
||||
it("every model has a non-empty string name", () => {
|
||||
const invalid: string[] = [];
|
||||
for (const { providerKey, modelKey, model } of allModels()) {
|
||||
if (typeof model["name"] !== "string" || model["name"].trim() === "") {
|
||||
invalid.push(`${providerKey}/${modelKey}`);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(invalid, [], `Models with missing or empty name:\n ${invalid.join("\n ")}`);
|
||||
});
|
||||
|
||||
it("every model has a non-empty string api", () => {
|
||||
const invalid: string[] = [];
|
||||
for (const { providerKey, modelKey, model } of allModels()) {
|
||||
if (typeof model["api"] !== "string" || model["api"].trim() === "") {
|
||||
invalid.push(`${providerKey}/${modelKey}`);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(invalid, [], `Models with missing or empty api:\n ${invalid.join("\n ")}`);
|
||||
});
|
||||
|
||||
it("every model's baseUrl starts with https:// (or is empty for azure-openai-responses)", () => {
|
||||
const invalid: string[] = [];
|
||||
for (const { providerKey, modelKey, model } of allModels()) {
|
||||
if (providerKey === "azure-openai-responses") continue;
|
||||
const url = model["baseUrl"];
|
||||
if (typeof url !== "string" || !url.startsWith("https://")) {
|
||||
invalid.push(`${providerKey}/${modelKey}: baseUrl="${url}"`);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(invalid, [], `Models with missing or non-HTTPS baseUrl:\n ${invalid.join("\n ")}`);
|
||||
});
|
||||
|
||||
it("azure-openai-responses models have an empty baseUrl (runtime-configured)", () => {
|
||||
const models = getModels("azure-openai-responses");
|
||||
assert.ok(models.length > 0, "azure-openai-responses must have at least one model");
|
||||
for (const model of models) {
|
||||
assert.equal(model.baseUrl, "", `azure-openai-responses/${model.id} should have empty baseUrl`);
|
||||
}
|
||||
});
|
||||
|
||||
it("every model has a boolean reasoning field", () => {
|
||||
const invalid: string[] = [];
|
||||
for (const { providerKey, modelKey, model } of allModels()) {
|
||||
if (typeof model["reasoning"] !== "boolean") {
|
||||
invalid.push(`${providerKey}/${modelKey}: reasoning=${model["reasoning"]}`);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(invalid, [], `Models with non-boolean reasoning:\n ${invalid.join("\n ")}`);
|
||||
});
|
||||
|
||||
it("every model has a non-empty input array", () => {
|
||||
const invalid: string[] = [];
|
||||
for (const { providerKey, modelKey, model } of allModels()) {
|
||||
const input = model["input"];
|
||||
if (!Array.isArray(input) || input.length === 0) {
|
||||
invalid.push(`${providerKey}/${modelKey}`);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(invalid, [], `Models with missing or empty input array:\n ${invalid.join("\n ")}`);
|
||||
});
|
||||
|
||||
it("every model has a positive contextWindow", () => {
|
||||
const invalid: string[] = [];
|
||||
for (const { providerKey, modelKey, model } of allModels()) {
|
||||
const cw = model["contextWindow"];
|
||||
if (typeof cw !== "number" || cw <= 0 || !Number.isFinite(cw)) {
|
||||
invalid.push(`${providerKey}/${modelKey}: contextWindow=${cw}`);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(invalid, [], `Models with invalid contextWindow:\n ${invalid.join("\n ")}`);
|
||||
});
|
||||
|
||||
it("every model has a positive maxTokens", () => {
|
||||
const invalid: string[] = [];
|
||||
for (const { providerKey, modelKey, model } of allModels()) {
|
||||
const mt = model["maxTokens"];
|
||||
if (typeof mt !== "number" || mt <= 0 || !Number.isFinite(mt)) {
|
||||
invalid.push(`${providerKey}/${modelKey}: maxTokens=${mt}`);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(invalid, [], `Models with invalid maxTokens:\n ${invalid.join("\n ")}`);
|
||||
});
|
||||
|
||||
it("every model's maxTokens does not exceed contextWindow", () => {
|
||||
const knownExceptions = new Set([
|
||||
"openrouter/meta-llama/llama-3-8b-instruct",
|
||||
"openrouter/nex-agi/deepseek-v3.1-nex-n1",
|
||||
"openrouter/openai/gpt-3.5-turbo-0613",
|
||||
"openrouter/z-ai/glm-5",
|
||||
]);
|
||||
|
||||
const invalid: string[] = [];
|
||||
for (const { providerKey, modelKey, model } of allModels()) {
|
||||
if (knownExceptions.has(`${providerKey}/${modelKey}`)) continue;
|
||||
const cw = model["contextWindow"] as number;
|
||||
const mt = model["maxTokens"] as number;
|
||||
if (typeof cw === "number" && typeof mt === "number" && mt > cw) {
|
||||
invalid.push(`${providerKey}/${modelKey}: maxTokens(${mt}) > contextWindow(${cw})`);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(invalid, [], `Models where maxTokens exceeds contextWindow:\n ${invalid.join("\n ")}`);
|
||||
});
|
||||
|
||||
it("every model has a cost object with non-negative numeric fields", () => {
|
||||
const knownNegativeCostModels = new Set([
|
||||
"openrouter/openrouter/auto",
|
||||
]);
|
||||
|
||||
const invalid: string[] = [];
|
||||
for (const { providerKey, modelKey, model } of allModels()) {
|
||||
if (knownNegativeCostModels.has(`${providerKey}/${modelKey}`)) continue;
|
||||
const cost = model["cost"] as Record<string, unknown> | undefined;
|
||||
if (!cost || typeof cost !== "object") {
|
||||
invalid.push(`${providerKey}/${modelKey}: missing cost object`);
|
||||
continue;
|
||||
}
|
||||
for (const field of ["input", "output", "cacheRead", "cacheWrite"] as const) {
|
||||
const val = cost[field];
|
||||
if (typeof val !== "number" || val < 0 || !Number.isFinite(val)) {
|
||||
invalid.push(`${providerKey}/${modelKey}: cost.${field}=${val}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.deepEqual(invalid, [], `Models with invalid cost fields:\n ${invalid.join("\n ")}`);
|
||||
});
|
||||
|
||||
it("no provider has duplicate model IDs", () => {
|
||||
const duplicates: string[] = [];
|
||||
for (const [providerKey, providerModels] of Object.entries(MODELS)) {
|
||||
const ids = Object.values(providerModels).map((m) => (m as Record<string, unknown>)["id"] as string);
|
||||
const seen = new Set<string>();
|
||||
for (const id of ids) {
|
||||
if (seen.has(id)) duplicates.push(`${providerKey}/${id}`);
|
||||
seen.add(id);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(duplicates, [], `Duplicate model IDs within a provider:\n ${duplicates.join("\n ")}`);
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Registry shape
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
describe("MODELS registry shape", () => {
|
||||
it("has exactly 23 providers", () => {
|
||||
const count = Object.keys(MODELS).length;
|
||||
assert.equal(count, 23, `Expected 23 providers, got ${count}: ${Object.keys(MODELS).join(", ")}`);
|
||||
});
|
||||
|
||||
it("has at least 200 models in total (sanity check)", () => {
|
||||
let total = 0;
|
||||
for (const providerModels of Object.values(MODELS)) {
|
||||
total += Object.keys(providerModels).length;
|
||||
}
|
||||
assert.ok(total >= 200, `Registry has only ${total} models — unexpectedly small`);
|
||||
});
|
||||
|
||||
it("all 23 expected providers are present", () => {
|
||||
const expected = [
|
||||
"amazon-bedrock",
|
||||
"anthropic",
|
||||
"azure-openai-responses",
|
||||
"cerebras",
|
||||
"github-copilot",
|
||||
"google",
|
||||
"google-antigravity",
|
||||
"google-gemini-cli",
|
||||
"google-vertex",
|
||||
"groq",
|
||||
"huggingface",
|
||||
"kimi-coding",
|
||||
"minimax",
|
||||
"minimax-cn",
|
||||
"mistral",
|
||||
"openai",
|
||||
"openai-codex",
|
||||
"opencode",
|
||||
"opencode-go",
|
||||
"openrouter",
|
||||
"vercel-ai-gateway",
|
||||
"xai",
|
||||
"zai",
|
||||
];
|
||||
const actual = Object.keys(MODELS).sort();
|
||||
assert.deepEqual(actual, expected.sort());
|
||||
});
|
||||
|
||||
it("getProviders() returns all generated providers", () => {
|
||||
const providers = getProviders();
|
||||
for (const p of Object.keys(MODELS)) {
|
||||
assert.ok(providers.includes(p as any), `getProviders() missing generated provider: ${p}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Removed models must not exist
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
describe("removed models are absent from the registry", () => {
|
||||
const removedModels: Array<{ provider: string; id: string }> = [
|
||||
{ provider: "openrouter", id: "anthropic/claude-3.5-sonnet" },
|
||||
{ provider: "openrouter", id: "anthropic/claude-3.5-sonnet-20240620" },
|
||||
{ provider: "openrouter", id: "mistralai/mistral-small-24b-instruct-2501" },
|
||||
{ provider: "openrouter", id: "mistralai/mistral-small-3.1-24b-instruct:free" },
|
||||
{ provider: "openrouter", id: "qwen/qwen3-4b:free" },
|
||||
{ provider: "openrouter", id: "stepfun/step-3.5-flash:free" },
|
||||
{ provider: "openrouter", id: "x-ai/grok-4.20-beta" },
|
||||
{ provider: "openrouter", id: "arcee-ai/trinity-mini:free" },
|
||||
{ provider: "openrouter", id: "google/gemini-3-pro-preview" },
|
||||
{ provider: "openrouter", id: "kwaipilot/kat-coder-pro" },
|
||||
{ provider: "openrouter", id: "meituan/longcat-flash-thinking" },
|
||||
{ provider: "vercel-ai-gateway", id: "xai/grok-2-vision" },
|
||||
{ provider: "anthropic", id: "claude-3-7-sonnet-latest" },
|
||||
];
|
||||
|
||||
for (const { provider, id } of removedModels) {
|
||||
it(`${provider}/${id} has been removed`, () => {
|
||||
const model = getModel(provider as any, id as any);
|
||||
assert.equal(model, undefined, `${provider}/${id} should be removed but is still present`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Spot-checks for notable models added in this regeneration
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
describe("spot-checks for models added in this regeneration", () => {
|
||||
const newModels: Array<{ provider: string; id: string; reasoning?: boolean }> = [
|
||||
{ provider: "openrouter", id: "z-ai/glm-5.1" },
|
||||
{ provider: "openrouter", id: "z-ai/glm-5v-turbo" },
|
||||
{ provider: "openrouter", id: "google/gemma-4-31b-it" },
|
||||
{ provider: "openrouter", id: "google/gemma-4-26b-a4b-it" },
|
||||
{ provider: "openrouter", id: "arcee-ai/trinity-large-thinking", reasoning: true },
|
||||
{ provider: "openrouter", id: "openai/gpt-audio" },
|
||||
{ provider: "openrouter", id: "anthropic/claude-opus-4.6-fast" },
|
||||
{ provider: "openrouter", id: "qwen/qwen3.6-plus" },
|
||||
{ provider: "groq", id: "groq/compound" },
|
||||
{ provider: "groq", id: "groq/compound-mini" },
|
||||
{ provider: "huggingface", id: "zai-org/GLM-5.1" },
|
||||
{ provider: "openai", id: "gpt-5.3-chat-latest" },
|
||||
{ provider: "mistral", id: "mistral-small-2603" },
|
||||
{ provider: "zai", id: "glm-5.1" },
|
||||
];
|
||||
|
||||
for (const { provider, id, reasoning } of newModels) {
|
||||
it(`${provider}/${id} is present in the registry`, () => {
|
||||
const model = getModel(provider as any, id as any);
|
||||
assert.ok(model, `Expected ${provider}/${id} to be present after regeneration`);
|
||||
assert.equal(model.id, id);
|
||||
assert.equal(model.provider, provider);
|
||||
if (reasoning !== undefined) {
|
||||
assert.equal(model.reasoning, reasoning, `${id} reasoning should be ${reasoning}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -109,6 +109,141 @@ describe("model registry — custom zai provider (GLM-5.1)", () => {
|
|||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// New provider: alibaba-dashscope (feat: #3891)
|
||||
//
|
||||
// Regular DashScope API for users without the Coding Plan.
|
||||
// Separate from alibaba-coding-plan — different endpoint, auth, and pricing.
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
describe("model registry — alibaba-dashscope provider", () => {
|
||||
it("alibaba-dashscope is a registered provider", () => {
|
||||
const providers = getProviders();
|
||||
assert.ok(
|
||||
providers.includes("alibaba-dashscope"),
|
||||
`Expected "alibaba-dashscope" in providers, got: ${providers.join(", ")}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("alibaba-dashscope has all expected models", () => {
|
||||
const models = getModels("alibaba-dashscope");
|
||||
const ids = models.map((m) => m.id).sort();
|
||||
const expected = [
|
||||
"qwen3-coder-plus",
|
||||
"qwen3-max",
|
||||
"qwen3.5-flash",
|
||||
"qwen3.5-plus",
|
||||
"qwen3.6-plus",
|
||||
];
|
||||
assert.deepEqual(ids, expected);
|
||||
});
|
||||
|
||||
it("alibaba-dashscope models use the international DashScope base URL", () => {
|
||||
const models = getModels("alibaba-dashscope");
|
||||
for (const model of models) {
|
||||
assert.equal(
|
||||
model.baseUrl,
|
||||
"https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
|
||||
`Model ${model.id} has wrong baseUrl: ${model.baseUrl}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("alibaba-dashscope models use openai-completions API", () => {
|
||||
const models = getModels("alibaba-dashscope");
|
||||
for (const model of models) {
|
||||
assert.equal(model.api, "openai-completions", `Model ${model.id} has wrong api: ${model.api}`);
|
||||
}
|
||||
});
|
||||
|
||||
it("alibaba-dashscope models have provider set correctly", () => {
|
||||
const models = getModels("alibaba-dashscope");
|
||||
for (const model of models) {
|
||||
assert.equal(
|
||||
model.provider,
|
||||
"alibaba-dashscope",
|
||||
`Model ${model.id} has wrong provider: ${model.provider}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("alibaba-dashscope models all have 1M context window", () => {
|
||||
const models = getModels("alibaba-dashscope");
|
||||
for (const model of models) {
|
||||
assert.equal(model.contextWindow, 1_000_000, `Model ${model.id} has wrong contextWindow: ${model.contextWindow}`);
|
||||
}
|
||||
});
|
||||
|
||||
it("alibaba-dashscope models have positive paid costs (not free-tier)", () => {
|
||||
const models = getModels("alibaba-dashscope");
|
||||
for (const model of models) {
|
||||
assert.ok(model.cost.input > 0, `${model.id}: input cost should be > 0 (paid tier)`);
|
||||
assert.ok(model.cost.output > 0, `${model.id}: output cost should be > 0 (paid tier)`);
|
||||
}
|
||||
});
|
||||
|
||||
it("qwen3-max is a reasoning model with correct pricing", () => {
|
||||
const model = getModel("alibaba-dashscope" as any, "qwen3-max" as any);
|
||||
assert.ok(model, "Expected getModel to return qwen3-max for alibaba-dashscope");
|
||||
assert.equal(model.reasoning, true);
|
||||
assert.equal(model.cost.input, 1.2);
|
||||
assert.equal(model.cost.output, 6);
|
||||
assert.equal(model.maxTokens, 32768);
|
||||
});
|
||||
|
||||
it("qwen3.5-plus is a reasoning model with correct pricing", () => {
|
||||
const model = getModel("alibaba-dashscope" as any, "qwen3.5-plus" as any);
|
||||
assert.ok(model, "Expected getModel to return qwen3.5-plus for alibaba-dashscope");
|
||||
assert.equal(model.reasoning, true);
|
||||
assert.equal(model.cost.input, 0.4);
|
||||
assert.equal(model.cost.output, 1.2);
|
||||
assert.equal(model.maxTokens, 65536);
|
||||
});
|
||||
|
||||
it("qwen3.5-flash is not a reasoning model", () => {
|
||||
const model = getModel("alibaba-dashscope" as any, "qwen3.5-flash" as any);
|
||||
assert.ok(model, "Expected getModel to return qwen3.5-flash for alibaba-dashscope");
|
||||
assert.equal(model.reasoning, false);
|
||||
assert.equal(model.cost.input, 0.1);
|
||||
assert.equal(model.cost.output, 0.4);
|
||||
});
|
||||
|
||||
it("qwen3-coder-plus is not a reasoning model", () => {
|
||||
const model = getModel("alibaba-dashscope" as any, "qwen3-coder-plus" as any);
|
||||
assert.ok(model, "Expected getModel to return qwen3-coder-plus for alibaba-dashscope");
|
||||
assert.equal(model.reasoning, false);
|
||||
assert.equal(model.cost.input, 1.0);
|
||||
assert.equal(model.cost.output, 5.0);
|
||||
});
|
||||
|
||||
it("qwen3.6-plus is a reasoning model", () => {
|
||||
const model = getModel("alibaba-dashscope" as any, "qwen3.6-plus" as any);
|
||||
assert.ok(model, "Expected getModel to return qwen3.6-plus for alibaba-dashscope");
|
||||
assert.equal(model.reasoning, true);
|
||||
assert.equal(model.cost.input, 0.5);
|
||||
assert.equal(model.cost.output, 3.0);
|
||||
});
|
||||
|
||||
it("alibaba-dashscope is independent of alibaba-coding-plan (different endpoint)", () => {
|
||||
const dashscope = getModels("alibaba-dashscope");
|
||||
const codingPlan = getModels("alibaba-coding-plan");
|
||||
for (const m of dashscope) {
|
||||
assert.notEqual(
|
||||
m.baseUrl,
|
||||
"https://coding-intl.dashscope.aliyuncs.com/v1",
|
||||
`${m.id} must not use the Coding Plan endpoint`,
|
||||
);
|
||||
}
|
||||
// Both providers must coexist — coding-plan must not have been overwritten
|
||||
assert.ok(codingPlan.length > 0, "alibaba-coding-plan must still have models");
|
||||
});
|
||||
|
||||
it("getModel returns undefined for unknown model in alibaba-dashscope (failure path)", () => {
|
||||
const model = getModel("alibaba-dashscope" as any, "does-not-exist" as any);
|
||||
assert.equal(model, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("model registry — custom models do not collide with generated models", () => {
|
||||
it("generated providers still exist alongside custom providers", () => {
|
||||
const providers = getProviders();
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ import { AssistantMessageEventStream } from "../utils/event-stream.js";
|
|||
import { parseStreamingJson } from "../utils/json-parse.js";
|
||||
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
|
||||
import { adjustMaxTokensForThinking, buildBaseOptions, clampReasoning } from "./simple-options.js";
|
||||
import { transformMessages } from "./transform-messages.js";
|
||||
import { transformMessagesWithReport } from "./transform-messages.js";
|
||||
|
||||
export interface BedrockOptions extends StreamOptions {
|
||||
region?: string;
|
||||
|
|
@ -151,7 +151,7 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream", BedrockOpt
|
|||
messages: convertMessages(context, model, cacheRetention),
|
||||
system: buildSystemPrompt(context.systemPrompt, model, cacheRetention),
|
||||
inferenceConfig: { maxTokens: options.maxTokens, temperature: options.temperature },
|
||||
toolConfig: convertToolConfig(context.tools, options.toolChoice),
|
||||
toolConfig: convertToolConfig(context.tools, options.toolChoice, model, cacheRetention),
|
||||
additionalModelRequestFields: buildAdditionalModelRequestFields(model, options),
|
||||
};
|
||||
const nextCommandInput = await options?.onPayload?.(commandInput, model);
|
||||
|
|
@ -487,7 +487,7 @@ function convertMessages(
|
|||
cacheRetention: CacheRetention,
|
||||
): Message[] {
|
||||
const result: Message[] = [];
|
||||
const transformedMessages = transformMessages(context.messages, model, normalizeToolCallId);
|
||||
const transformedMessages = transformMessagesWithReport(context.messages, model, normalizeToolCallId, "bedrock-converse-stream");
|
||||
|
||||
for (let i = 0; i < transformedMessages.length; i++) {
|
||||
const m = transformedMessages[i];
|
||||
|
|
@ -633,6 +633,8 @@ function convertMessages(
|
|||
function convertToolConfig(
|
||||
tools: Tool[] | undefined,
|
||||
toolChoice: BedrockOptions["toolChoice"],
|
||||
model: Model<"bedrock-converse-stream">,
|
||||
cacheRetention: CacheRetention,
|
||||
): ToolConfiguration | undefined {
|
||||
if (!tools?.length || toolChoice === "none") return undefined;
|
||||
|
||||
|
|
@ -644,6 +646,16 @@ function convertToolConfig(
|
|||
},
|
||||
}));
|
||||
|
||||
// Add cachePoint after last tool for supported models
|
||||
if (cacheRetention !== "none" && supportsPromptCaching(model)) {
|
||||
bedrockTools.push({
|
||||
cachePoint: {
|
||||
type: CachePointType.DEFAULT,
|
||||
...(cacheRetention === "long" ? { ttl: CacheTTL.ONE_HOUR } : {}),
|
||||
},
|
||||
} as any);
|
||||
}
|
||||
|
||||
let bedrockToolChoice: ToolChoice | undefined;
|
||||
switch (toolChoice) {
|
||||
case "auto":
|
||||
|
|
|
|||
32
packages/pi-ai/src/providers/anthropic-auth.test.ts
Normal file
32
packages/pi-ai/src/providers/anthropic-auth.test.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { usesAnthropicBearerAuth } from "./anthropic.js";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
test("usesAnthropicBearerAuth covers Bearer-only Anthropic-compatible providers (#3783)", () => {
|
||||
assert.equal(usesAnthropicBearerAuth("alibaba-coding-plan"), true);
|
||||
assert.equal(usesAnthropicBearerAuth("minimax"), true);
|
||||
assert.equal(usesAnthropicBearerAuth("minimax-cn"), true);
|
||||
assert.equal(usesAnthropicBearerAuth("anthropic"), false);
|
||||
});
|
||||
|
||||
test("createClient routes Bearer-auth providers through authToken (#3783)", () => {
|
||||
const source = readFileSync(join(__dirname, "..", "..", "src", "providers", "anthropic.ts"), "utf-8");
|
||||
assert.ok(
|
||||
source.includes("const usesBearerAuth = usesAnthropicBearerAuth(model.provider);"),
|
||||
"createClient should derive auth mode from usesAnthropicBearerAuth",
|
||||
);
|
||||
assert.ok(
|
||||
source.includes("apiKey: usesBearerAuth ? null : apiKey"),
|
||||
"Bearer-auth providers should skip x-api-key auth",
|
||||
);
|
||||
assert.ok(
|
||||
source.includes("authToken: usesBearerAuth ? apiKey : undefined"),
|
||||
"Bearer-auth providers should send authToken instead",
|
||||
);
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue