mirror of
https://github.com/glittercowboy/get-shit-done
synced 2026-04-26 01:35:29 +02:00
Compare commits
39 Commits
fix/2598-i
...
b1a670e662
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1a670e662 | ||
|
|
7c6f8005f3 | ||
|
|
cd05725576 | ||
|
|
c811792967 | ||
|
|
34b39f0a37 | ||
|
|
b1278f6fc3 | ||
|
|
303fd26b45 | ||
|
|
7b470f2625 | ||
|
|
c8ae6b3b4f | ||
|
|
7ed05c8811 | ||
|
|
0f8f7537da | ||
|
|
709f0382bf | ||
|
|
a6e692f789 | ||
|
|
b67ab38098 | ||
|
|
06463860e4 | ||
|
|
259c1d07d3 | ||
|
|
387c8a1f9c | ||
|
|
e973ff4cb6 | ||
|
|
8caa7d4c3a | ||
|
|
a72bebb379 | ||
|
|
31569c8cc8 | ||
|
|
eba0c99698 | ||
|
|
5a8a6fb511 | ||
|
|
bdba40cc3d | ||
|
|
df0ab0c0c9 | ||
|
|
807db75d55 | ||
|
|
74da61fb4a | ||
|
|
0a049149e1 | ||
|
|
a56707a07b | ||
|
|
f30da8326a | ||
|
|
1a3d953767 | ||
|
|
cc17886c51 | ||
|
|
41dc475c46 | ||
|
|
220da8e487 | ||
|
|
c90081176d | ||
|
|
1a694fcac3 | ||
|
|
9c0a153a5f | ||
|
|
86c5863afb | ||
|
|
1f2850c1a8 |
172
.github/workflows/install-smoke.yml
vendored
172
.github/workflows/install-smoke.yml
vendored
@@ -1,10 +1,13 @@
|
||||
name: Install Smoke
|
||||
|
||||
# Exercises the real install path: `npm pack` → `npm install -g <tarball>`
|
||||
# → run `bin/install.js` → assert `gsd-sdk` is on PATH.
|
||||
# Exercises the real install paths:
|
||||
# tarball: `npm pack` → `npm install -g <tarball>` → assert gsd-sdk on PATH
|
||||
# unpacked: `npm install -g <dir>` (no pack) → assert gsd-sdk on PATH + executable
|
||||
#
|
||||
# Closes the CI gap that let #2439 ship: the rest of the suite only reads
|
||||
# `bin/install.js` as a string and never executes it.
|
||||
# The tarball path is the canonical ship path. The unpacked path reproduces the
|
||||
# mode-644 failure class (issue #2453): npm does NOT chmod bin targets when
|
||||
# installing from an unpacked local directory, so any stale tsc output lacking
|
||||
# execute bits will be caught by the unpacked job before release.
|
||||
#
|
||||
# - PRs: path-filtered, minimal runner (ubuntu + Node LTS) for fast signal.
|
||||
# - Push to release branches / main: full matrix.
|
||||
@@ -16,6 +19,7 @@ on:
|
||||
- main
|
||||
paths:
|
||||
- 'bin/install.js'
|
||||
- 'bin/gsd-sdk.js'
|
||||
- 'sdk/**'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
@@ -40,6 +44,9 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# ---------------------------------------------------------------------------
|
||||
# Job 1: tarball install (existing canonical path)
|
||||
# ---------------------------------------------------------------------------
|
||||
smoke:
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 12
|
||||
@@ -78,6 +85,31 @@ jobs:
|
||||
if: steps.skip.outputs.skip != 'true'
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
# Need enough history to merge origin/main for stale-base detection.
|
||||
fetch-depth: 0
|
||||
|
||||
# The default `refs/pull/N/merge` ref GitHub produces for PRs is cached
|
||||
# against the recorded merge-base, not current main. When main advances
|
||||
# after the PR was opened, the merge ref stays stale and CI can fail on
|
||||
# issues that were already fixed upstream. Explicitly merge current
|
||||
# origin/main into the PR head so smoke always tests the PR against the
|
||||
# latest trunk. If the merge conflicts, emit a clear "rebase onto main"
|
||||
# diagnostic instead of a downstream build error that looks unrelated.
|
||||
- name: Rebase check — merge origin/main into PR head
|
||||
if: steps.skip.outputs.skip != 'true' && github.event_name == 'pull_request'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config user.email "ci@gsd-build"
|
||||
git config user.name "CI Rebase Check"
|
||||
git fetch origin main
|
||||
if ! git merge --no-edit --no-ff origin/main; then
|
||||
echo "::error::This PR cannot cleanly merge origin/main. Rebase your branch onto current main and push again."
|
||||
echo "::error::Conflicting files:"
|
||||
git diff --name-only --diff-filter=U
|
||||
git merge --abort
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Set up Node.js ${{ matrix.node-version }}
|
||||
if: steps.skip.outputs.skip != 'true'
|
||||
@@ -90,6 +122,23 @@ jobs:
|
||||
if: steps.skip.outputs.skip != 'true'
|
||||
run: npm ci
|
||||
|
||||
# Isolated SDK typecheck — if the build fails, emit a clear "stale base
|
||||
# or real type error" diagnostic instead of letting the failure cascade
|
||||
# into the tarball install step, where the downstream PATH assertion
|
||||
# misreports it as "gsd-sdk not on PATH — installSdkIfNeeded regression".
|
||||
- name: SDK typecheck (fails fast on type regressions)
|
||||
if: steps.skip.outputs.skip != 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if ! npm run build:sdk; then
|
||||
echo "::error::SDK build (npm run build:sdk) failed."
|
||||
echo "::error::Common cause: your PR base is behind main and picks up intermediate type errors that are already fixed on trunk."
|
||||
echo "::error::Fix: git fetch origin main && git rebase origin/main && git push --force-with-lease"
|
||||
echo "::error::If the error persists on a fresh rebase, the type error is real — fix it in sdk/src/ and push."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Pack root tarball
|
||||
if: steps.skip.outputs.skip != 'true'
|
||||
id: pack
|
||||
@@ -109,7 +158,7 @@ jobs:
|
||||
echo "$NPM_BIN" >> "$GITHUB_PATH"
|
||||
echo "npm global bin: $NPM_BIN"
|
||||
|
||||
- name: Install tarball globally (runs bin/install.js → installSdkIfNeeded)
|
||||
- name: Install tarball globally
|
||||
if: steps.skip.outputs.skip != 'true'
|
||||
shell: bash
|
||||
env:
|
||||
@@ -121,13 +170,14 @@ jobs:
|
||||
cd "$TMPDIR_ROOT"
|
||||
npm install -g "$WORKSPACE/$TARBALL"
|
||||
command -v get-shit-done-cc
|
||||
# `--claude --local` is the non-interactive code path (see
|
||||
# install.js main block: when both a runtime and location are set,
|
||||
# installAllRuntimes runs with isInteractive=false, no prompts).
|
||||
# We tolerate non-zero here because the authoritative assertion is
|
||||
# the next step: gsd-sdk must land on PATH. Some runtime targets
|
||||
# may exit before the SDK step for unrelated reasons on CI.
|
||||
get-shit-done-cc --claude --local || true
|
||||
# `--claude --local` is the non-interactive code path. Don't swallow
|
||||
# non-zero exit — if the installer fails, that IS the CI failure, and
|
||||
# its own error message is more useful than the downstream "shim
|
||||
# regression" assertion masking the real cause.
|
||||
if ! get-shit-done-cc --claude --local; then
|
||||
echo "::error::get-shit-done-cc --claude --local failed. See the install.js output above for the real error (SDK build, PATH resolution, chmod, etc.)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Assert gsd-sdk resolves on PATH
|
||||
if: steps.skip.outputs.skip != 'true'
|
||||
@@ -135,7 +185,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if ! command -v gsd-sdk >/dev/null 2>&1; then
|
||||
echo "::error::gsd-sdk is not on PATH after install — installSdkIfNeeded() regression"
|
||||
echo "::error::gsd-sdk is not on PATH after tarball install — shim regression"
|
||||
NPM_BIN="$(npm config get prefix)/bin"
|
||||
echo "npm global bin: $NPM_BIN"
|
||||
ls -la "$NPM_BIN" | grep -i gsd || true
|
||||
@@ -150,3 +200,99 @@ jobs:
|
||||
set -euo pipefail
|
||||
gsd-sdk --version || gsd-sdk --help
|
||||
echo "✓ gsd-sdk is executable"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Job 2: unpacked-dir install — reproduces the mode-644 failure class (#2453)
|
||||
#
|
||||
# `npm install -g <directory>` does NOT chmod bin targets when the source
|
||||
# file was produced by a build script (tsc emits 0o644). This job catches
|
||||
# regressions where sdk/dist/cli.js loses its execute bit before publish.
|
||||
# ---------------------------------------------------------------------------
|
||||
smoke-unpacked:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
# See the `smoke` job above for rationale — refs/pull/N/merge is cached
|
||||
# against the recorded merge-base, not current main. Explicitly merge
|
||||
# origin/main so smoke-unpacked also runs against the latest trunk.
|
||||
- name: Rebase check — merge origin/main into PR head
|
||||
if: github.event_name == 'pull_request'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config user.email "ci@gsd-build"
|
||||
git config user.name "CI Rebase Check"
|
||||
git fetch origin main
|
||||
if ! git merge --no-edit --no-ff origin/main; then
|
||||
echo "::error::This PR cannot cleanly merge origin/main. Rebase your branch onto current main and push again."
|
||||
echo "::error::Conflicting files:"
|
||||
git diff --name-only --diff-filter=U
|
||||
git merge --abort
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Set up Node.js 22
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install root deps
|
||||
run: npm ci
|
||||
|
||||
- name: Build SDK dist (sdk/dist is gitignored — must build for unpacked install)
|
||||
run: npm run build:sdk
|
||||
|
||||
- name: Ensure npm global bin is on PATH
|
||||
shell: bash
|
||||
run: |
|
||||
NPM_BIN="$(npm config get prefix)/bin"
|
||||
echo "$NPM_BIN" >> "$GITHUB_PATH"
|
||||
echo "npm global bin: $NPM_BIN"
|
||||
|
||||
- name: Strip execute bit from sdk/dist/cli.js to simulate tsc-fresh output
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Simulate the exact state tsc produces: cli.js at mode 644.
|
||||
chmod 644 sdk/dist/cli.js
|
||||
echo "Stripped execute bit: $(stat -c '%a' sdk/dist/cli.js 2>/dev/null || stat -f '%p' sdk/dist/cli.js)"
|
||||
|
||||
- name: Install from unpacked directory (no npm pack)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TMPDIR_ROOT=$(mktemp -d)
|
||||
cd "$TMPDIR_ROOT"
|
||||
npm install -g "$GITHUB_WORKSPACE"
|
||||
command -v get-shit-done-cc
|
||||
get-shit-done-cc --claude --local || true
|
||||
|
||||
- name: Assert gsd-sdk resolves on PATH after unpacked install
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if ! command -v gsd-sdk >/dev/null 2>&1; then
|
||||
echo "::error::gsd-sdk is not on PATH after unpacked install — #2453 regression"
|
||||
NPM_BIN="$(npm config get prefix)/bin"
|
||||
ls -la "$NPM_BIN" | grep -i gsd || true
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ gsd-sdk resolves at: $(command -v gsd-sdk)"
|
||||
|
||||
- name: Assert gsd-sdk is executable after unpacked install (#2453)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# This is the exact check that would have caught #2453 before release.
|
||||
# The shim (bin/gsd-sdk.js) invokes sdk/dist/cli.js via `node`, so
|
||||
# the execute bit on cli.js is not needed for the shim path. However
|
||||
# installSdkIfNeeded() also chmods cli.js in-place as a safety net.
|
||||
gsd-sdk --version || gsd-sdk --help
|
||||
echo "✓ gsd-sdk is executable after unpacked install"
|
||||
|
||||
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@@ -189,8 +189,11 @@ jobs:
|
||||
git add package.json package-lock.json sdk/package.json
|
||||
git commit -m "chore: bump to ${PRE_VERSION}"
|
||||
|
||||
- name: Build SDK
|
||||
run: cd sdk && npm ci && npm run build
|
||||
- name: Build SDK dist for tarball
|
||||
run: npm run build:sdk
|
||||
|
||||
- name: Verify tarball ships sdk/dist/cli.js (bug #2647)
|
||||
run: bash scripts/verify-tarball-sdk-dist.sh
|
||||
|
||||
- name: Dry-run publish validation
|
||||
run: |
|
||||
@@ -330,8 +333,11 @@ jobs:
|
||||
npm ci
|
||||
npm run test:coverage
|
||||
|
||||
- name: Build SDK
|
||||
run: cd sdk && npm ci && npm run build
|
||||
- name: Build SDK dist for tarball
|
||||
run: npm run build:sdk
|
||||
|
||||
- name: Verify tarball ships sdk/dist/cli.js (bug #2647)
|
||||
run: bash scripts/verify-tarball-sdk-dist.sh
|
||||
|
||||
- name: Dry-run publish validation
|
||||
run: |
|
||||
|
||||
28
.github/workflows/test.yml
vendored
28
.github/workflows/test.yml
vendored
@@ -35,6 +35,31 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
# Fetch full history so we can merge origin/main for stale-base detection.
|
||||
fetch-depth: 0
|
||||
|
||||
# GitHub's `refs/pull/N/merge` is cached against the recorded merge-base.
|
||||
# When main advances after a PR is opened, the cache stays stale and CI
|
||||
# runs against the pre-advance state — hiding bugs that are already fixed
|
||||
# on trunk and surfacing type errors that were introduced and then patched
|
||||
# on main in between. Explicitly merge current origin/main here so tests
|
||||
# always run against the latest trunk.
|
||||
- name: Rebase check — merge origin/main into PR head
|
||||
if: github.event_name == 'pull_request'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config user.email "ci@gsd-build"
|
||||
git config user.name "CI Rebase Check"
|
||||
git fetch origin main
|
||||
if ! git merge --no-edit --no-ff origin/main; then
|
||||
echo "::error::This PR cannot cleanly merge origin/main. Rebase your branch onto current main and push again."
|
||||
echo "::error::Conflicting files:"
|
||||
git diff --name-only --diff-filter=U
|
||||
git merge --abort
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Set up Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
@@ -45,6 +70,9 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build SDK dist (required by installer)
|
||||
run: npm run build:sdk
|
||||
|
||||
- name: Run tests with coverage
|
||||
shell: bash
|
||||
run: npm run test:coverage
|
||||
|
||||
12
CHANGELOG.md
12
CHANGELOG.md
@@ -28,8 +28,17 @@ If you use GSD **as a workflow**—milestones, phases, `.planning/` artifacts, b
|
||||
|
||||
### Fixed
|
||||
|
||||
- **End-of-phase routing suggestions now use `/gsd-<cmd>` (not the retired `/gsd:<cmd>`)** — All user-visible command suggestions in workflows (`execute-phase.md`, `transition.md`), tool output (`profile-output.cjs`, `init.cjs`), references, and templates have been updated from `/gsd:<cmd>` to `/gsd-<cmd>`, matching the Claude Code skill directory name and the user-typed slash-command format. Internal `Skill(skill="gsd:<cmd>")` calls (no leading slash) are preserved unchanged — those resolve by frontmatter `name:` not directory name. The namespace test (`bug-2543-gsd-slash-namespace.test.cjs`) has been updated to enforce the current invariant. Closes #2697.
|
||||
|
||||
- **`gsd-sdk query` now resolves parent `.planning/` root in multi-repo (`sub_repos`) workspaces** — when invoked from inside a `sub_repos`-listed child repo (e.g. `workspace/app/`), the SDK now walks up to the parent workspace that owns `.planning/`, matching the legacy `gsd-tools.cjs` `findProjectRoot` behavior. Previously `gsd-sdk query init.new-milestone` reported `project_exists: false` from the sub-repo, while `gsd-tools.cjs` resolved the parent root correctly. Resolution happens once in `cli.ts` before dispatch; if `projectDir` already owns `.planning/` (including explicit `--project-dir`), the walk is a no-op. Ported as `findProjectRoot` in `sdk/src/query/helpers.ts` with the same detection order (own `.planning/` wins, then parent `sub_repos` match, then legacy `multiRepo: true`, then `.git` heuristic), capped at 10 parent levels and never crossing `$HOME`. Closes #2623.
|
||||
- **Shell hooks falsely flagged as stale on every session** — `gsd-phase-boundary.sh`, `gsd-session-state.sh`, and `gsd-validate-commit.sh` now ship with a `# gsd-hook-version: {{GSD_VERSION}}` header; the installer substitutes `{{GSD_VERSION}}` in `.sh` hooks the same way it does for `.js` hooks; and the stale-hook detector in `gsd-check-update.js` now matches bash `#` comment syntax in addition to JS `//` syntax. All three changes are required together — neither the regex fix alone nor the install fix alone is sufficient to resolve the false positive (#2136, #2206, #2209, #2210, #2212)
|
||||
|
||||
## [1.38.2] - 2026-04-19
|
||||
|
||||
### Fixed
|
||||
- **SDK decoupled from build-from-source install** — replaces the fragile `tsc` + `npm install -g ./sdk` dance on user machines with a prebuilt `sdk/dist/` shipped inside the parent `get-shit-done-cc` tarball. The `gsd-sdk` CLI is now a `bin/gsd-sdk.js` shim in the parent package that resolves `sdk/dist/cli.js` and invokes it via `node`, so npm chmods the bin entry from the tarball (not from a secondary local install) and PATH/exec-bit issues cannot occur. Repurposes `installSdkIfNeeded()` in `bin/install.js` to only verify `sdk/dist/cli.js` exists and fix its execute bit (non-fatal); deletes `resolveGsdSdk()`, `detectShellRc()`, `emitSdkFatal()` and the source-build/global-install logic (162 lines removed). `release.yml` now runs `npm run build:sdk` before publish in both rc and finalize jobs, so every published tarball contains fresh SDK dist. `sdk/package.json` `prepublishOnly` is the final safety net (`rm -rf dist && tsc && chmod +x dist/cli.js`). `install-smoke.yml` adds an `smoke-unpacked` variant that installs from the unpacked dir with the exec bit stripped, so this class of regression cannot ship again. Closes #2441 and #2453.
|
||||
- **`--sdk` flag semantics changed** — previously forced a rebuild of the SDK from source; now verifies the bundled `sdk/dist/` is resolvable. Users who were invoking `get-shit-done-cc --sdk` as a "force rebuild" no longer need it — the SDK ships prebuilt.
|
||||
|
||||
### Added
|
||||
- **`/gsd-ingest-docs` command** — Scan a repo containing mixed ADRs, PRDs, SPECs, and DOCs and bootstrap or merge the full `.planning/` setup from them in a single pass. Parallel classification (`gsd-doc-classifier`), synthesis with precedence rules and cycle detection (`gsd-doc-synthesizer`), three-bucket conflicts report (`INGEST-CONFLICTS.md`: auto-resolved, competing-variants, unresolved-blockers), and hard-block on LOCKED-vs-LOCKED ADR contradictions in both new and merge modes. Supports directory-convention discovery and `--manifest <file>` YAML override with per-doc precedence. v1 caps at 50 docs per invocation; `--resolve interactive` is reserved. Extracts shared conflict-detection contract into `references/doc-conflict-engine.md` which `/gsd-import` now also consumes (#2387)
|
||||
- **`/gsd-plan-review-convergence` command** — Cross-AI plan convergence loop that automates `plan-phase → review → replan → re-review` cycles. Spawns isolated agents for `gsd-plan-phase` and `gsd-review`; orchestrator only does loop control, HIGH concern counting, stall detection, and escalation. Supports `--codex`, `--gemini`, `--claude`, `--opencode`, `--all` reviewers and `--max-cycles N` (default 3). Loop exits when no HIGH concerns remain; stall detection warns when count isn't decreasing; escalation gate asks user to proceed or review manually when max cycles reached (#2306)
|
||||
@@ -2368,7 +2377,8 @@ Technical implementation details for Phase 2 appear in the **Changed** section b
|
||||
- YOLO mode for autonomous execution
|
||||
- Interactive mode with checkpoints
|
||||
|
||||
[Unreleased]: https://github.com/gsd-build/get-shit-done/compare/v1.37.1...HEAD
|
||||
[Unreleased]: https://github.com/gsd-build/get-shit-done/compare/v1.38.2...HEAD
|
||||
[1.38.2]: https://github.com/gsd-build/get-shit-done/compare/v1.37.1...v1.38.2
|
||||
[1.37.1]: https://github.com/gsd-build/get-shit-done/compare/v1.37.0...v1.37.1
|
||||
[1.37.0]: https://github.com/gsd-build/get-shit-done/compare/v1.36.0...v1.37.0
|
||||
[1.36.0]: https://github.com/gsd-build/get-shit-done/releases/tag/v1.36.0
|
||||
|
||||
@@ -314,6 +314,15 @@ bin/install.js — Installer (multi-runtime)
|
||||
get-shit-done/
|
||||
bin/lib/ — Core library modules (.cjs)
|
||||
workflows/ — Workflow definitions (.md)
|
||||
Large workflows split per progressive-disclosure
|
||||
pattern: workflows/<name>/modes/*.md +
|
||||
workflows/<name>/templates/*. Parent dispatches
|
||||
to mode files. See workflows/discuss-phase/ as
|
||||
the canonical example (#2551). New modes for
|
||||
discuss-phase land in
|
||||
workflows/discuss-phase/modes/<mode>.md.
|
||||
Per-file budgets enforced by
|
||||
tests/workflow-size-budget.test.cjs.
|
||||
references/ — Reference documentation (.md)
|
||||
templates/ — File templates
|
||||
agents/ — Agent definitions (.md) — CANONICAL SOURCE
|
||||
|
||||
@@ -94,6 +94,19 @@ Based on focus, determine which documents you'll write:
|
||||
- `arch` → ARCHITECTURE.md, STRUCTURE.md
|
||||
- `quality` → CONVENTIONS.md, TESTING.md
|
||||
- `concerns` → CONCERNS.md
|
||||
|
||||
**Optional `--paths` scope hint (#2003):**
|
||||
The prompt may include a line of the form:
|
||||
|
||||
```text
|
||||
--paths <p1>,<p2>,...
|
||||
```
|
||||
|
||||
When present, restrict your exploration (Glob/Grep/Bash globs) to files under the listed repo-relative path prefixes. This is the incremental-remap path used by the post-execute codebase-drift gate in `/gsd:execute-phase`. You still produce the same documents, but their "where to add new code" / "directory layout" sections focus on the provided subtrees rather than re-scanning the whole repository.
|
||||
|
||||
**Path validation:** Reject any `--paths` value containing `..`, starting with `/`, or containing shell metacharacters (`;`, `` ` ``, `$`, `&`, `|`, `<`, `>`). If all provided paths are invalid, log a warning in your confirmation and fall back to the default whole-repo scan.
|
||||
|
||||
If no `--paths` hint is provided, behave exactly as before.
|
||||
</step>
|
||||
|
||||
<step name="explore_codebase">
|
||||
|
||||
32
bin/gsd-sdk.js
Executable file
32
bin/gsd-sdk.js
Executable file
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* bin/gsd-sdk.js — back-compat shim for external callers of `gsd-sdk`.
|
||||
*
|
||||
* When the parent package is installed globally (`npm install -g get-shit-done-cc`
|
||||
* or `npx get-shit-done-cc`), npm creates a `gsd-sdk` symlink in the global bin
|
||||
* directory pointing at this file. npm correctly chmods bin entries from a tarball,
|
||||
* so the execute-bit problem that afflicted the sub-install approach (issue #2453)
|
||||
* cannot occur here.
|
||||
*
|
||||
* This shim resolves sdk/dist/cli.js relative to its own location and delegates
|
||||
* to it via `node`, so `gsd-sdk <args>` behaves identically to
|
||||
* `node <packageDir>/sdk/dist/cli.js <args>`.
|
||||
*
|
||||
* Call sites (slash commands, agent prompts, hook scripts) continue to work without
|
||||
* changes because `gsd-sdk` still resolves on PATH — it just comes from this shim
|
||||
* in the parent package rather than from a separately installed @gsd-build/sdk.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const { spawnSync } = require('child_process');
|
||||
|
||||
const cliPath = path.resolve(__dirname, '..', 'sdk', 'dist', 'cli.js');
|
||||
|
||||
const result = spawnSync(process.execPath, [cliPath, ...process.argv.slice(2)], {
|
||||
stdio: 'inherit',
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
process.exit(result.status ?? 1);
|
||||
814
bin/install.js
814
bin/install.js
@@ -57,6 +57,20 @@ const claudeToCopilotTools = {
|
||||
// Get version from package.json
|
||||
const pkg = require('../package.json');
|
||||
|
||||
// #2517 — runtime-aware tier resolution shared with core.cjs.
|
||||
// Hoisted to top with absolute __dirname-based paths so `gsd install codex` works
|
||||
// when invoked via npm global install (cwd is the user's project, not the gsd repo
|
||||
// root). Inline `require('../get-shit-done/...')` from inside install functions
|
||||
// works only because Node resolves it relative to the install.js file regardless
|
||||
// of cwd, but keeping the require at the top makes the dependency explicit and
|
||||
// surfaces resolution failures at process start instead of at first install call.
|
||||
const _gsdLibDir = path.join(__dirname, '..', 'get-shit-done', 'bin', 'lib');
|
||||
const { MODEL_PROFILES: GSD_MODEL_PROFILES } = require(path.join(_gsdLibDir, 'model-profiles.cjs'));
|
||||
const {
|
||||
RUNTIME_PROFILE_MAP: GSD_RUNTIME_PROFILE_MAP,
|
||||
resolveTierEntry: gsdResolveTierEntry,
|
||||
} = require(path.join(_gsdLibDir, 'core.cjs'));
|
||||
|
||||
// Parse args
|
||||
const args = process.argv.slice(2);
|
||||
const hasGlobal = args.includes('--global') || args.includes('-g');
|
||||
@@ -620,6 +634,172 @@ function readGsdGlobalModelOverrides() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Effective per-agent model_overrides for the Codex / OpenCode install paths.
|
||||
*
|
||||
* Merges `~/.gsd/defaults.json` (global) with per-project
|
||||
* `<project>/.planning/config.json`. Per-project keys win on conflict so a
|
||||
* user can tune a single agent's model in one repo without re-setting the
|
||||
* global defaults for every other repo. Non-conflicting keys from both
|
||||
* sources are preserved.
|
||||
*
|
||||
* This is the fix for #2256: both adapters previously read only the global
|
||||
* file, so a per-project `model_overrides` (the common case the reporter
|
||||
* described — a per-project override for `gsd-codebase-mapper` in
|
||||
* `.planning/config.json`) was silently dropped and child agents inherited
|
||||
* the session default.
|
||||
*
|
||||
* `targetDir` is the consuming runtime's install root (e.g. `~/.codex` for
|
||||
* a global install, or `<project>/.codex` for a local install). We walk up
|
||||
* from there looking for `.planning/` so both cases resolve the correct
|
||||
* project root. When `targetDir` is null/undefined only the global file is
|
||||
* consulted (matches prior behavior for code paths that have no project
|
||||
* context).
|
||||
*
|
||||
* Returns a plain `{ agentName: modelId }` object, or `null` when neither
|
||||
* source defines `model_overrides`.
|
||||
*/
|
||||
function readGsdEffectiveModelOverrides(targetDir = null) {
|
||||
const global = readGsdGlobalModelOverrides();
|
||||
|
||||
let projectOverrides = null;
|
||||
if (targetDir) {
|
||||
let probeDir = path.resolve(targetDir);
|
||||
for (let depth = 0; depth < 8; depth += 1) {
|
||||
const candidate = path.join(probeDir, '.planning', 'config.json');
|
||||
if (fs.existsSync(candidate)) {
|
||||
try {
|
||||
const parsed = JSON.parse(fs.readFileSync(candidate, 'utf-8'));
|
||||
if (parsed && typeof parsed === 'object' && parsed.model_overrides
|
||||
&& typeof parsed.model_overrides === 'object') {
|
||||
projectOverrides = parsed.model_overrides;
|
||||
}
|
||||
} catch {
|
||||
// Malformed config.json — fall back to global; readGsdRuntimeProfileResolver
|
||||
// surfaces a parse warning via _readGsdConfigFile already.
|
||||
}
|
||||
break;
|
||||
}
|
||||
const parent = path.dirname(probeDir);
|
||||
if (parent === probeDir) break;
|
||||
probeDir = parent;
|
||||
}
|
||||
}
|
||||
|
||||
if (!global && !projectOverrides) return null;
|
||||
// Per-project wins on conflict; preserve non-conflicting global keys.
|
||||
return { ...(global || {}), ...(projectOverrides || {}) };
|
||||
}
|
||||
|
||||
/**
|
||||
* #2517 — Read a single GSD config file (defaults.json or per-project
|
||||
* config.json) into a plain object, returning null on missing/empty files
|
||||
* and warning to stderr on JSON parse failures so silent corruption can't
|
||||
* mask broken configs (review finding #5).
|
||||
*/
|
||||
function _readGsdConfigFile(absPath, label) {
|
||||
if (!fs.existsSync(absPath)) return null;
|
||||
let raw;
|
||||
try {
|
||||
raw = fs.readFileSync(absPath, 'utf-8');
|
||||
} catch (err) {
|
||||
process.stderr.write(`gsd: warning — could not read ${label} (${absPath}): ${err.message}\n`);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(raw);
|
||||
} catch (err) {
|
||||
process.stderr.write(`gsd: warning — invalid JSON in ${label} (${absPath}): ${err.message}\n`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* #2517 — Build a runtime-aware tier resolver for the install path.
|
||||
*
|
||||
* Probes BOTH per-project `<targetDir>/.planning/config.json` AND
|
||||
* `~/.gsd/defaults.json`, with per-project keys winning over global. This
|
||||
* matches `loadConfig`'s precedence and is the only way the PR's headline claim
|
||||
* — "set runtime in .planning/config.json and the Codex TOML emit picks it up"
|
||||
* — actually holds end-to-end (review finding #1).
|
||||
*
|
||||
* `targetDir` should be the consuming runtime's install root — install code
|
||||
* passes `path.dirname(<runtime root>)` so `.planning/config.json` resolves
|
||||
* relative to the user's project. When `targetDir` is null/undefined, only the
|
||||
* global defaults are consulted.
|
||||
*
|
||||
* Returns null if no `runtime` is configured (preserves prior behavior — only
|
||||
* model_overrides is embedded, no tier/reasoning-effort inference). Returns
|
||||
* null when `model_profile` is `inherit` so the literal alias passes through
|
||||
* unchanged.
|
||||
*
|
||||
* Returns { runtime, resolve(agentName) -> { model, reasoning_effort? } | null }
|
||||
*/
|
||||
function readGsdRuntimeProfileResolver(targetDir = null) {
|
||||
const homeDefaults = _readGsdConfigFile(
|
||||
path.join(os.homedir(), '.gsd', 'defaults.json'),
|
||||
'~/.gsd/defaults.json'
|
||||
);
|
||||
|
||||
// Per-project config probe. Resolve the project root by walking up from
|
||||
// targetDir until we hit a `.planning/` directory; this covers both the
|
||||
// common case (caller passes the project root) and the case where caller
|
||||
// passes a nested install dir like `<root>/.codex/`.
|
||||
let projectConfig = null;
|
||||
if (targetDir) {
|
||||
let probeDir = path.resolve(targetDir);
|
||||
for (let depth = 0; depth < 8; depth += 1) {
|
||||
const candidate = path.join(probeDir, '.planning', 'config.json');
|
||||
if (fs.existsSync(candidate)) {
|
||||
projectConfig = _readGsdConfigFile(candidate, '.planning/config.json');
|
||||
break;
|
||||
}
|
||||
const parent = path.dirname(probeDir);
|
||||
if (parent === probeDir) break;
|
||||
probeDir = parent;
|
||||
}
|
||||
}
|
||||
|
||||
// Per-project wins. Only fall back to ~/.gsd/defaults.json when the project
|
||||
// didn't set the field. Field-level merge (not whole-object replace) so a
|
||||
// user can keep `runtime` global while overriding only `model_profile` per
|
||||
// project, and vice versa.
|
||||
const merged = {
|
||||
runtime:
|
||||
(projectConfig && projectConfig.runtime) ||
|
||||
(homeDefaults && homeDefaults.runtime) ||
|
||||
null,
|
||||
model_profile:
|
||||
(projectConfig && projectConfig.model_profile) ||
|
||||
(homeDefaults && homeDefaults.model_profile) ||
|
||||
'balanced',
|
||||
model_profile_overrides:
|
||||
(projectConfig && projectConfig.model_profile_overrides) ||
|
||||
(homeDefaults && homeDefaults.model_profile_overrides) ||
|
||||
null,
|
||||
};
|
||||
|
||||
if (!merged.runtime) return null;
|
||||
|
||||
const profile = String(merged.model_profile).toLowerCase();
|
||||
if (profile === 'inherit') return null;
|
||||
|
||||
return {
|
||||
runtime: merged.runtime,
|
||||
resolve(agentName) {
|
||||
const agentModels = GSD_MODEL_PROFILES[agentName];
|
||||
if (!agentModels) return null;
|
||||
const tier = agentModels[profile] || agentModels.balanced;
|
||||
if (!tier) return null;
|
||||
return gsdResolveTierEntry({
|
||||
runtime: merged.runtime,
|
||||
tier,
|
||||
overrides: merged.model_profile_overrides,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Cache for attribution settings (populated once per runtime during install)
|
||||
const attributionCache = new Map();
|
||||
|
||||
@@ -933,11 +1113,31 @@ function convertClaudeCommandToCopilotSkill(content, skillName, isGlobal = false
|
||||
return `${fm}\n${body}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a skill directory name (gsd-<cmd>) to the frontmatter `name:` used
|
||||
* by Claude Code as the skill identity. Workflows emit `Skill(skill="gsd:<cmd>")`
|
||||
* (colon form) and Claude Code resolves skills by frontmatter `name:`, not
|
||||
* directory name — so emit colon form here. Directory stays hyphenated for
|
||||
* Windows path safety. See #2643.
|
||||
*
|
||||
* Codex must NOT use this helper: its adapter invokes skills as `$gsd-<cmd>`
|
||||
* (shell-var syntax) and a colon would terminate the variable name. Codex
|
||||
* keeps the hyphen form via `yamlQuote(skillName)` directly.
|
||||
*/
|
||||
function skillFrontmatterName(skillDirName) {
|
||||
if (typeof skillDirName !== 'string') return skillDirName;
|
||||
// Idempotent on already-colon form.
|
||||
if (skillDirName.includes(':')) return skillDirName;
|
||||
// Only rewrite the first hyphen after the `gsd` prefix.
|
||||
return skillDirName.replace(/^gsd-/, 'gsd:');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a Claude command (.md) to a Claude skill (SKILL.md).
|
||||
* Claude Code is the native format, so minimal conversion needed —
|
||||
* preserve allowed-tools as YAML multiline list, preserve argument-hint,
|
||||
* convert name from gsd:xxx to gsd-xxx format.
|
||||
* preserve allowed-tools as YAML multiline list, preserve argument-hint.
|
||||
* Emits `name: gsd:<cmd>` (colon) so Skill(skill="gsd:<cmd>") calls in
|
||||
* workflows resolve on flat-skills installs — see #2643.
|
||||
*/
|
||||
function convertClaudeCommandToClaudeSkill(content, skillName) {
|
||||
const { frontmatter, body } = extractFrontmatterAndBody(content);
|
||||
@@ -957,7 +1157,8 @@ function convertClaudeCommandToClaudeSkill(content, skillName) {
|
||||
}
|
||||
|
||||
// Reconstruct frontmatter in Claude skill format
|
||||
let fm = `---\nname: ${skillName}\ndescription: ${yamlQuote(description)}\n`;
|
||||
const frontmatterName = skillFrontmatterName(skillName);
|
||||
let fm = `---\nname: ${frontmatterName}\ndescription: ${yamlQuote(description)}\n`;
|
||||
if (argumentHint) fm += `argument-hint: ${yamlQuote(argumentHint)}\n`;
|
||||
if (agent) fm += `agent: ${agent}\n`;
|
||||
if (toolsBlock) fm += toolsBlock;
|
||||
@@ -1693,6 +1894,14 @@ function convertClaudeToCodexMarkdown(content) {
|
||||
converted = converted.replace(/\$HOME\/\.claude\//g, '$HOME/.codex/');
|
||||
converted = converted.replace(/~\/\.claude\//g, '~/.codex/');
|
||||
converted = converted.replace(/\.\/\.claude\//g, './.codex/');
|
||||
// Bare/project-relative .claude/... references (#2639). Covers strings like
|
||||
// "check `.claude/skills/`" where there is no ~/, $HOME/, or ./ anchor.
|
||||
// Negative lookbehind prevents double-replacing already-anchored forms and
|
||||
// avoids matching inside URLs or other slash-prefixed paths.
|
||||
converted = converted.replace(/(?<![A-Za-z0-9_\-./~$])\.claude\//g, '.codex/');
|
||||
// `.claudeignore` → `.codexignore` (#2639). Codex honors its own ignore
|
||||
// file; leaving the Claude-specific name is misleading in agent prompts.
|
||||
converted = converted.replace(/\.claudeignore\b/g, '.codexignore');
|
||||
// Runtime-neutral agent name replacement (#766)
|
||||
converted = neutralizeAgentReferences(converted, 'AGENTS.md');
|
||||
return converted;
|
||||
@@ -1729,9 +1938,17 @@ GSD workflows use \`Task(...)\` (Claude Code syntax). Translate to Codex collabo
|
||||
|
||||
Direct mapping:
|
||||
- \`Task(subagent_type="X", prompt="Y")\` → \`spawn_agent(agent_type="X", message="Y")\`
|
||||
- \`Task(model="...")\` → omit (Codex uses per-role config, not inline model selection)
|
||||
- \`Task(model="...")\` → omit. \`spawn_agent\` has no inline \`model\` parameter;
|
||||
GSD embeds the resolved per-agent model directly into each agent's \`.toml\`
|
||||
at install time so \`model_overrides\` from \`.planning/config.json\` and
|
||||
\`~/.gsd/defaults.json\` are honored automatically by Codex's agent router.
|
||||
- \`fork_context: false\` by default — GSD agents load their own context via \`<files_to_read>\` blocks
|
||||
|
||||
Spawn restriction:
|
||||
- Codex restricts \`spawn_agent\` to cases where the user has explicitly
|
||||
requested sub-agents. When automatic spawning is not permitted, do the
|
||||
work inline in the current agent rather than attempting to force a spawn.
|
||||
|
||||
Parallel fan-out:
|
||||
- Spawn multiple agents → collect agent IDs → \`wait(ids)\` for all to complete
|
||||
|
||||
@@ -1789,7 +2006,7 @@ purpose: ${toSingleLine(description)}
|
||||
* Sets required agent metadata, sandbox_mode, and developer_instructions
|
||||
* from the agent markdown content.
|
||||
*/
|
||||
function generateCodexAgentToml(agentName, agentContent, modelOverrides = null) {
|
||||
function generateCodexAgentToml(agentName, agentContent, modelOverrides = null, runtimeResolver = null) {
|
||||
const sandboxMode = CODEX_AGENT_SANDBOX[agentName] || 'read-only';
|
||||
const { frontmatter, body } = extractFrontmatterAndBody(agentContent);
|
||||
const frontmatterText = frontmatter || '';
|
||||
@@ -1808,9 +2025,20 @@ function generateCodexAgentToml(agentName, agentContent, modelOverrides = null)
|
||||
// Embed model override when configured in ~/.gsd/defaults.json so that
|
||||
// model_overrides is respected on Codex (which uses static TOML, not inline
|
||||
// Task() model parameters). See #2256.
|
||||
// Precedence: per-agent model_overrides > runtime-aware tier resolution (#2517).
|
||||
const modelOverride = modelOverrides?.[resolvedName] || modelOverrides?.[agentName];
|
||||
if (modelOverride) {
|
||||
lines.push(`model = ${JSON.stringify(modelOverride)}`);
|
||||
} else if (runtimeResolver) {
|
||||
// #2517 — runtime-aware tier resolution. Embeds Codex-native model + reasoning_effort
|
||||
// from RUNTIME_PROFILE_MAP / model_profile_overrides for the configured tier.
|
||||
const entry = runtimeResolver.resolve(resolvedName) || runtimeResolver.resolve(agentName);
|
||||
if (entry?.model) {
|
||||
lines.push(`model = ${JSON.stringify(entry.model)}`);
|
||||
if (entry.reasoning_effort) {
|
||||
lines.push(`model_reasoning_effort = ${JSON.stringify(entry.reasoning_effort)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Agent prompts contain raw backslashes in regexes and shell snippets.
|
||||
@@ -1838,7 +2066,10 @@ function generateCodexConfigBlock(agents, targetDir) {
|
||||
];
|
||||
|
||||
for (const { name, description } of agents) {
|
||||
lines.push(`[agents.${name}]`);
|
||||
// #2645 — Codex schema requires [[agents]] array-of-tables, not [agents.<name>] maps.
|
||||
// Emitting [agents.<name>] produces `invalid type: map, expected a sequence` on load.
|
||||
lines.push(`[[agents]]`);
|
||||
lines.push(`name = ${JSON.stringify(name)}`);
|
||||
lines.push(`description = ${JSON.stringify(description)}`);
|
||||
lines.push(`config_file = "${agentsPrefix}/${name}.toml"`);
|
||||
lines.push('');
|
||||
@@ -1847,8 +2078,39 @@ function generateCodexConfigBlock(agents, targetDir) {
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip any managed GSD agent sections from a TOML string.
|
||||
*
|
||||
* Handles BOTH shapes so reinstall self-heals broken legacy configs:
|
||||
* - Legacy: `[agents.gsd-*]` single-keyed map tables (pre-#2645).
|
||||
* - Current: `[[agents]]` array-of-tables whose `name = "gsd-*"`.
|
||||
*
|
||||
* A section runs from its header to the next `[` header or EOF.
|
||||
*/
|
||||
function stripCodexGsdAgentSections(content) {
|
||||
return content.replace(/^\[agents\.gsd-[^\]]+\]\n(?:(?!\[)[^\n]*\n?)*/gm, '');
|
||||
// Use the TOML-aware section parser so we never absorb adjacent user-authored
|
||||
// tables — even if their headers are indented or otherwise oddly placed.
|
||||
const sections = getTomlTableSections(content).filter((section) => {
|
||||
// Legacy `[agents.gsd-<name>]` map tables (pre-#2645).
|
||||
if (!section.array && /^agents\.gsd-/.test(section.path)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Current `[[agents]]` array-of-tables — only strip blocks whose
|
||||
// `name = "gsd-..."`, preserving user-authored [[agents]] entries.
|
||||
if (section.array && section.path === 'agents') {
|
||||
const body = content.slice(section.headerEnd, section.end);
|
||||
const nameMatch = body.match(/^[ \t]*name[ \t]*=[ \t]*["']([^"']+)["']/m);
|
||||
return Boolean(nameMatch && /^gsd-/.test(nameMatch[1]));
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return removeContentRanges(
|
||||
content,
|
||||
sections.map(({ start, end }) => ({ start, end })),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2546,13 +2808,27 @@ function isLegacyGsdAgentsSection(body) {
|
||||
|
||||
function stripLeakedGsdCodexSections(content) {
|
||||
const leakedSections = getTomlTableSections(content)
|
||||
.filter((section) =>
|
||||
section.path.startsWith('agents.gsd-') ||
|
||||
(
|
||||
.filter((section) => {
|
||||
// Legacy [agents.gsd-<name>] map tables (pre-#2645).
|
||||
if (!section.array && section.path.startsWith('agents.gsd-')) return true;
|
||||
|
||||
// Legacy bare [agents] table with only the old max_threads/max_depth keys.
|
||||
if (
|
||||
!section.array &&
|
||||
section.path === 'agents' &&
|
||||
isLegacyGsdAgentsSection(content.slice(section.headerEnd, section.end))
|
||||
)
|
||||
);
|
||||
) return true;
|
||||
|
||||
// Current [[agents]] array-of-tables whose name is gsd-*. Preserve
|
||||
// user-authored [[agents]] entries (other names) untouched.
|
||||
if (section.array && section.path === 'agents') {
|
||||
const body = content.slice(section.headerEnd, section.end);
|
||||
const nameMatch = body.match(/^[ \t]*name[ \t]*=[ \t]*["']([^"']+)["']/m);
|
||||
if (nameMatch && /^gsd-/.test(nameMatch[1])) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (leakedSections.length === 0) {
|
||||
return content;
|
||||
@@ -3033,25 +3309,37 @@ function installCodexConfig(targetDir, agentsSrc) {
|
||||
|
||||
for (const file of agentEntries) {
|
||||
let content = fs.readFileSync(path.join(agentsSrc, file), 'utf8');
|
||||
// Replace full .claude/get-shit-done prefix so path resolves to codex GSD install
|
||||
// Replace full .claude/get-shit-done prefix so path resolves to the Codex
|
||||
// GSD install before generic .claude → .codex conversion rewrites it.
|
||||
content = content.replace(/~\/\.claude\/get-shit-done\//g, codexGsdPath);
|
||||
content = content.replace(/\$HOME\/\.claude\/get-shit-done\//g, codexGsdPath);
|
||||
// Replace remaining .claude paths with .codex equivalents (#2320).
|
||||
// Capture group handles both trailing-slash form (~/.claude/) and
|
||||
// bare end-of-string form (~/.claude) in a single pass.
|
||||
content = content.replace(/\$HOME\/\.claude(\/|$)/g, '$HOME/.codex$1');
|
||||
content = content.replace(/~\/\.claude(\/|$)/g, '~/.codex$1');
|
||||
content = content.replace(/\.\/\.claude(\/|$)/g, './.codex$1');
|
||||
// Route TOML emit through the same full Claude→Codex conversion pipeline
|
||||
// used on the `.md` emit path (#2639). Covers: slash-command rewrites,
|
||||
// $ARGUMENTS → {{GSD_ARGS}}, /clear removal, anchored and bare .claude/
|
||||
// paths, .claudeignore → .codexignore, and standalone "Claude" /
|
||||
// CLAUDE.md neutralization via neutralizeAgentReferences(..., 'AGENTS.md').
|
||||
content = convertClaudeToCodexMarkdown(content);
|
||||
const { frontmatter } = extractFrontmatterAndBody(content);
|
||||
const name = extractFrontmatterField(frontmatter, 'name') || file.replace('.md', '');
|
||||
const description = extractFrontmatterField(frontmatter, 'description') || '';
|
||||
|
||||
agents.push({ name, description: toSingleLine(description) });
|
||||
|
||||
// Pass model overrides from ~/.gsd/defaults.json so Codex TOML files
|
||||
// Pass model overrides from both per-project `.planning/config.json` and
|
||||
// `~/.gsd/defaults.json` (project wins on conflict) so Codex TOML files
|
||||
// embed the configured model — Codex cannot receive model inline (#2256).
|
||||
const modelOverrides = readGsdGlobalModelOverrides();
|
||||
const tomlContent = generateCodexAgentToml(name, content, modelOverrides);
|
||||
// Previously only the global file was read, which silently dropped the
|
||||
// per-project override the reporter had set for gsd-codebase-mapper.
|
||||
// #2517 — also pass the runtime-aware tier resolver so profile tiers can
|
||||
// resolve to Codex-native model IDs + reasoning_effort when `runtime: "codex"`
|
||||
// is set in defaults.json.
|
||||
const modelOverrides = readGsdEffectiveModelOverrides(targetDir);
|
||||
// Pass `targetDir` so per-project .planning/config.json wins over global
|
||||
// ~/.gsd/defaults.json — without this, the PR's headline claim that
|
||||
// setting runtime in the project config reaches the Codex emit path is
|
||||
// false (review finding #1).
|
||||
const runtimeResolver = readGsdRuntimeProfileResolver(targetDir);
|
||||
const tomlContent = generateCodexAgentToml(name, content, modelOverrides, runtimeResolver);
|
||||
fs.writeFileSync(path.join(agentsTomlDir, `${name}.toml`), tomlContent);
|
||||
}
|
||||
|
||||
@@ -5751,9 +6039,13 @@ function install(isGlobal, runtime = 'claude') {
|
||||
content = processAttribution(content, getCommitAttribution(runtime));
|
||||
// Convert frontmatter for runtime compatibility (agents need different handling)
|
||||
if (isOpencode) {
|
||||
// Resolve per-agent model override from ~/.gsd/defaults.json (#2256)
|
||||
// Resolve per-agent model override from BOTH per-project
|
||||
// `.planning/config.json` and `~/.gsd/defaults.json`, with
|
||||
// per-project winning on conflict (#2256). Without the per-project
|
||||
// probe, an override set in `.planning/config.json` was silently
|
||||
// ignored and the child inherited OpenCode's default model.
|
||||
const _ocAgentName = entry.name.replace(/\.md$/, '');
|
||||
const _ocModelOverrides = readGsdGlobalModelOverrides();
|
||||
const _ocModelOverrides = readGsdEffectiveModelOverrides(targetDir);
|
||||
const _ocModelOverride = _ocModelOverrides?.[_ocAgentName] || null;
|
||||
content = convertClaudeToOpencodeFrontmatter(content, { isAgent: true, modelOverride: _ocModelOverride });
|
||||
} else if (isKilo) {
|
||||
@@ -6656,196 +6948,306 @@ function promptLocation(runtimes) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Build `@gsd-build/sdk` from the in-repo `sdk/` source tree and install the
|
||||
* resulting `gsd-sdk` binary globally so workflow commands that shell out to
|
||||
* `gsd-sdk query …` succeed.
|
||||
* Check whether any common shell rc file already contains a `PATH=` line
|
||||
* whose HOME-expanded value places `globalBin` on PATH (#2620).
|
||||
*
|
||||
* We build from source rather than `npm install -g @gsd-build/sdk` because the
|
||||
* npm-published package lags the source tree and shipping a stale SDK breaks
|
||||
* every /gsd-* command that depends on newer query handlers.
|
||||
* Parses `~/.zshrc`, `~/.bashrc`, `~/.bash_profile`, `~/.profile` (or the
|
||||
* override list in `rcFileNames`), matches `export PATH=` / bare `PATH=`
|
||||
* lines, and substitutes the common HOME forms (`$HOME`, `${HOME}`, `~`)
|
||||
* with `homeDir` before comparing each PATH segment against `globalBin`.
|
||||
*
|
||||
* Skip if --no-sdk. Skip if already on PATH (unless --sdk was explicit).
|
||||
* Failures are FATAL — we exit non-zero so install does not complete with a
|
||||
* silently broken SDK (issue #2439). Set GSD_ALLOW_OFF_PATH=1 to downgrade the
|
||||
* post-install PATH verification to a warning (exit code 2) for users with an
|
||||
* intentionally restricted PATH who will wire things up manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Resolve `gsd-sdk` on PATH. Uses `command -v` via `sh -c` on POSIX (portable
|
||||
* across sh/bash/zsh) and `where` on Windows. Returns trimmed path or null.
|
||||
*/
|
||||
function resolveGsdSdk() {
|
||||
const { spawnSync } = require('child_process');
|
||||
if (process.platform === 'win32') {
|
||||
const r = spawnSync('where', ['gsd-sdk'], { encoding: 'utf-8' });
|
||||
if (r.status === 0 && r.stdout && r.stdout.trim()) {
|
||||
return r.stdout.trim().split('\n')[0].trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const r = spawnSync('sh', ['-c', 'command -v gsd-sdk'], { encoding: 'utf-8' });
|
||||
if (r.status === 0 && r.stdout && r.stdout.trim()) {
|
||||
return r.stdout.trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort detection of the user's shell rc file for PATH remediation hints.
|
||||
*/
|
||||
function detectShellRc() {
|
||||
const path = require('path');
|
||||
const shell = process.env.SHELL || '';
|
||||
const home = process.env.HOME || '~';
|
||||
if (/\/zsh$/.test(shell)) return { shell: 'zsh', rc: path.join(home, '.zshrc') };
|
||||
if (/\/bash$/.test(shell)) return { shell: 'bash', rc: path.join(home, '.bashrc') };
|
||||
if (/\/fish$/.test(shell)) return { shell: 'fish', rc: path.join(home, '.config', 'fish', 'config.fish') };
|
||||
return { shell: 'sh', rc: path.join(home, '.profile') };
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a red fatal banner and exit. Prints actionable PATH remediation when
|
||||
* the global install succeeded but the bin dir is not on PATH.
|
||||
* Best-effort: any unreadable / malformed / non-existent rc file is ignored
|
||||
* and the fallback is the caller's existing absolute-path suggestion. Only
|
||||
* the `$HOME/…`, `${HOME}/…`, and `~/…` forms are handled — we do not try
|
||||
* to fully parse bash syntax.
|
||||
*
|
||||
* If exitCode is 2, this is the "off-PATH" case and GSD_ALLOW_OFF_PATH respect
|
||||
* is applied by the caller; we only print.
|
||||
* @param {string} globalBin Absolute path to npm's global bin directory.
|
||||
* @param {string} homeDir Absolute path used to substitute HOME / ~.
|
||||
* @param {string[]} [rcFileNames] Override the default rc file list.
|
||||
* @returns {boolean} true iff any rc file adds globalBin to PATH.
|
||||
*/
|
||||
function emitSdkFatal(reason, { globalBin, exitCode }) {
|
||||
const { shell, rc } = detectShellRc();
|
||||
const bar = '━'.repeat(72);
|
||||
const redBold = `${red}${bold}`;
|
||||
|
||||
console.error('');
|
||||
console.error(`${redBold}${bar}${reset}`);
|
||||
console.error(`${redBold} ✗ GSD SDK install failed — /gsd-* commands will not work${reset}`);
|
||||
console.error(`${redBold}${bar}${reset}`);
|
||||
console.error(` ${red}Reason:${reset} ${reason}`);
|
||||
|
||||
if (globalBin) {
|
||||
console.error('');
|
||||
console.error(` ${yellow}gsd-sdk was installed to:${reset}`);
|
||||
console.error(` ${cyan}${globalBin}${reset}`);
|
||||
console.error('');
|
||||
console.error(` ${yellow}Your shell's PATH does not include this directory.${reset}`);
|
||||
console.error(` Add it by running:`);
|
||||
if (shell === 'fish') {
|
||||
console.error(` ${cyan}fish_add_path "${globalBin}"${reset}`);
|
||||
console.error(` (or append to ${rc})`);
|
||||
} else {
|
||||
console.error(` ${cyan}echo 'export PATH="${globalBin}:$PATH"' >> ${rc}${reset}`);
|
||||
console.error(` ${cyan}source ${rc}${reset}`);
|
||||
}
|
||||
console.error('');
|
||||
console.error(` Then verify: ${cyan}command -v gsd-sdk${reset}`);
|
||||
if (exitCode === 2) {
|
||||
console.error('');
|
||||
console.error(` ${dim}(GSD_ALLOW_OFF_PATH=1 set → exit ${exitCode} instead of hard failure)${reset}`);
|
||||
}
|
||||
} else {
|
||||
console.error('');
|
||||
console.error(` Build manually to retry:`);
|
||||
console.error(` ${cyan}cd <install-dir>/sdk && npm install && npm run build && npm install -g .${reset}`);
|
||||
}
|
||||
|
||||
console.error(`${redBold}${bar}${reset}`);
|
||||
console.error('');
|
||||
process.exit(exitCode);
|
||||
}
|
||||
|
||||
function installSdkIfNeeded() {
|
||||
if (hasNoSdk) {
|
||||
console.log(`\n ${dim}Skipping GSD SDK install (--no-sdk)${reset}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const { spawnSync } = require('child_process');
|
||||
function homePathCoveredByRc(globalBin, homeDir, rcFileNames) {
|
||||
if (!globalBin || !homeDir) return false;
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
if (!hasSdk) {
|
||||
const resolved = resolveGsdSdk();
|
||||
if (resolved) {
|
||||
console.log(` ${green}✓${reset} GSD SDK already installed (gsd-sdk on PATH at ${resolved})`);
|
||||
return;
|
||||
const normalise = (p) => {
|
||||
if (!p) return '';
|
||||
let n = p.replace(/[\\/]+$/g, '');
|
||||
if (n === '') n = p.startsWith('/') ? '/' : p;
|
||||
return n;
|
||||
};
|
||||
|
||||
const targetAbs = normalise(path.resolve(globalBin));
|
||||
const homeAbs = path.resolve(homeDir);
|
||||
const files = rcFileNames || ['.zshrc', '.bashrc', '.bash_profile', '.profile'];
|
||||
|
||||
const expandHome = (segment) => {
|
||||
let s = segment;
|
||||
s = s.replace(/\$\{HOME\}/g, homeAbs);
|
||||
s = s.replace(/\$HOME/g, homeAbs);
|
||||
if (s.startsWith('~/') || s === '~') {
|
||||
s = s === '~' ? homeAbs : path.join(homeAbs, s.slice(2));
|
||||
}
|
||||
return s;
|
||||
};
|
||||
|
||||
// Match `PATH=…` (optionally prefixed with `export `). The RHS captures
|
||||
// through end-of-line; surrounding quotes are stripped before splitting.
|
||||
const assignRe = /^\s*(?:export\s+)?PATH\s*=\s*(.+?)\s*$/;
|
||||
|
||||
for (const name of files) {
|
||||
const rcPath = path.join(homeAbs, name);
|
||||
let content;
|
||||
try {
|
||||
content = fs.readFileSync(rcPath, 'utf8');
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const rawLine of content.split(/\r?\n/)) {
|
||||
const line = rawLine.replace(/^\s+/, '');
|
||||
if (line.startsWith('#')) continue;
|
||||
|
||||
const m = assignRe.exec(rawLine);
|
||||
if (!m) continue;
|
||||
|
||||
let rhs = m[1];
|
||||
if ((rhs.startsWith('"') && rhs.endsWith('"')) ||
|
||||
(rhs.startsWith("'") && rhs.endsWith("'"))) {
|
||||
rhs = rhs.slice(1, -1);
|
||||
}
|
||||
|
||||
for (const segment of rhs.split(':')) {
|
||||
if (!segment) continue;
|
||||
const trimmed = segment.trim();
|
||||
const expanded = expandHome(trimmed);
|
||||
if (expanded.includes('$')) continue;
|
||||
// Skip segments that are still relative after HOME expansion. A bare
|
||||
// `bin` entry (or `./bin`, `node_modules/.bin`, etc.) depends on the
|
||||
// shell's cwd at lookup time — it is NOT equivalent to `$HOME/bin`,
|
||||
// so resolving against homeAbs would produce false positives.
|
||||
if (!path.isAbsolute(expanded)) continue;
|
||||
try {
|
||||
const abs = normalise(path.resolve(expanded));
|
||||
if (abs === targetAbs) return true;
|
||||
} catch {
|
||||
// ignore unresolvable segments
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Locate the in-repo sdk/ directory relative to this installer file.
|
||||
// For global npm installs this resolves inside the published package dir;
|
||||
// for git-based installs (npx github:..., local clone) it resolves to the
|
||||
// repo's sdk/ tree. Both contain the source tree because root package.json
|
||||
// includes "sdk" in its `files` array.
|
||||
const sdkDir = path.resolve(__dirname, '..', 'sdk');
|
||||
const sdkPackageJson = path.join(sdkDir, 'package.json');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(sdkPackageJson)) {
|
||||
emitSdkFatal(`SDK source tree not found at ${sdkDir}.`, { globalBin: null, exitCode: 1 });
|
||||
}
|
||||
/**
|
||||
* Emit a PATH-export suggestion if globalBin is not already on PATH AND
|
||||
* the user's shell rc files do not already cover it via a HOME-relative
|
||||
* entry (#2620).
|
||||
*
|
||||
* Prints one of:
|
||||
* - nothing, if `globalBin` is already present on `process.env.PATH`
|
||||
* - a diagnostic "already covered via rc file" note, if an rc file has
|
||||
* `export PATH="$HOME/…/bin:$PATH"` (or equivalent) and the user just
|
||||
* needs to reopen their shell
|
||||
* - the absolute `echo 'export PATH="…:$PATH"' >> ~/.zshrc` suggestion,
|
||||
* if neither PATH nor any rc file covers globalBin
|
||||
*
|
||||
* Exported for tests; the installer calls this from finishInstall.
|
||||
*
|
||||
* @param {string} globalBin Absolute path to npm's global bin directory.
|
||||
* @param {string} homeDir Absolute HOME path.
|
||||
*/
|
||||
function maybeSuggestPathExport(globalBin, homeDir) {
|
||||
if (!globalBin || !homeDir) return;
|
||||
const path = require('path');
|
||||
|
||||
console.log(`\n ${cyan}Building GSD SDK from source (${sdkDir})…${reset}`);
|
||||
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
||||
const pathEnv = process.env.PATH || '';
|
||||
const targetAbs = path.resolve(globalBin).replace(/[\\/]+$/g, '') || globalBin;
|
||||
const onPath = pathEnv.split(path.delimiter).some((seg) => {
|
||||
if (!seg) return false;
|
||||
const abs = path.resolve(seg).replace(/[\\/]+$/g, '') || seg;
|
||||
return abs === targetAbs;
|
||||
});
|
||||
if (onPath) return;
|
||||
|
||||
// 1. Install sdk build-time dependencies (tsc, etc.)
|
||||
const installResult = spawnSync(npmCmd, ['install'], { cwd: sdkDir, stdio: 'inherit' });
|
||||
if (installResult.status !== 0) {
|
||||
emitSdkFatal('Failed to `npm install` in sdk/.', { globalBin: null, exitCode: 1 });
|
||||
}
|
||||
|
||||
// 2. Compile TypeScript → sdk/dist/
|
||||
const buildResult = spawnSync(npmCmd, ['run', 'build'], { cwd: sdkDir, stdio: 'inherit' });
|
||||
if (buildResult.status !== 0) {
|
||||
emitSdkFatal('Failed to `npm run build` in sdk/.', { globalBin: null, exitCode: 1 });
|
||||
}
|
||||
|
||||
// 3. Install the built package globally so `gsd-sdk` lands on PATH.
|
||||
const globalResult = spawnSync(npmCmd, ['install', '-g', '.'], { cwd: sdkDir, stdio: 'inherit' });
|
||||
if (globalResult.status !== 0) {
|
||||
emitSdkFatal('Failed to `npm install -g .` from sdk/.', { globalBin: null, exitCode: 1 });
|
||||
}
|
||||
|
||||
// 3a. Explicitly chmod dist/cli.js to 0o755 in the global install location.
|
||||
// `tsc` emits files at process umask (typically 0o644 — non-executable), and
|
||||
// `npm install -g` from a local directory does NOT chmod bin-script targets the
|
||||
// way tarball extraction does. Without this, the `gsd-sdk` bin symlink points at
|
||||
// a non-executable file and `command -v gsd-sdk` fails on every first install
|
||||
// (root cause of #2453). Mirrors the pattern used for hook files in this installer.
|
||||
try {
|
||||
const prefixRes = spawnSync(npmCmd, ['config', 'get', 'prefix'], { encoding: 'utf-8' });
|
||||
if (prefixRes.status === 0) {
|
||||
const npmPrefix = (prefixRes.stdout || '').trim();
|
||||
const sdkPkg = JSON.parse(fs.readFileSync(path.join(sdkDir, 'package.json'), 'utf-8'));
|
||||
const sdkName = sdkPkg.name; // '@gsd-build/sdk'
|
||||
const globalModulesDir = process.platform === 'win32'
|
||||
? path.join(npmPrefix, 'node_modules')
|
||||
: path.join(npmPrefix, 'lib', 'node_modules');
|
||||
const cliPath = path.join(globalModulesDir, sdkName, 'dist', 'cli.js');
|
||||
try { fs.chmodSync(cliPath, 0o755); } catch (e) { if (process.platform !== 'win32') throw e; }
|
||||
}
|
||||
} catch (e) { /* Non-fatal: PATH verification in step 4 will catch any real failure */ }
|
||||
|
||||
// 4. Verify gsd-sdk is actually resolvable on PATH. npm's global bin dir is
|
||||
// not always on the current shell's PATH (Homebrew prefixes, nvm setups,
|
||||
// unconfigured npm prefix), so a zero exit status from `npm install -g`
|
||||
// alone is not proof of a working binary (issue #2439 root cause).
|
||||
const resolved = resolveGsdSdk();
|
||||
if (resolved) {
|
||||
console.log(` ${green}✓${reset} Built and installed GSD SDK from source (gsd-sdk resolved at ${resolved})`);
|
||||
if (homePathCoveredByRc(globalBin, homeDir)) {
|
||||
console.log(` ${yellow}⚠${reset} ${bold}gsd-sdk${reset}'s directory is already on your PATH via an rc file entry — try reopening your shell (or ${cyan}source ~/.zshrc${reset}).`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Off-PATH: resolve npm global bin dir for actionable remediation.
|
||||
const prefixResult = spawnSync(npmCmd, ['config', 'get', 'prefix'], { encoding: 'utf-8' });
|
||||
const prefix = prefixResult.status === 0 ? (prefixResult.stdout || '').trim() : null;
|
||||
const globalBin = prefix
|
||||
? (process.platform === 'win32' ? prefix : path.join(prefix, 'bin'))
|
||||
: null;
|
||||
console.log('');
|
||||
console.log(` ${yellow}⚠${reset} ${bold}${globalBin}${reset} is not on your PATH.`);
|
||||
console.log(` Add it with one of:`);
|
||||
console.log(` ${cyan}echo 'export PATH="${globalBin}:$PATH"' >> ~/.zshrc${reset}`);
|
||||
console.log(` ${cyan}echo 'export PATH="${globalBin}:$PATH"' >> ~/.bashrc${reset}`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
const allowOffPath = process.env.GSD_ALLOW_OFF_PATH === '1';
|
||||
emitSdkFatal(
|
||||
'Built and installed GSD SDK, but `gsd-sdk` is not on your PATH.',
|
||||
{ globalBin, exitCode: allowOffPath ? 2 : 1 },
|
||||
);
|
||||
/**
|
||||
* Verify the prebuilt SDK dist is present and the gsd-sdk shim is wired up.
|
||||
*
|
||||
* As of fix/2441-sdk-decouple, sdk/dist/ is shipped prebuilt inside the
|
||||
* get-shit-done-cc npm tarball. The parent package declares a bin entry
|
||||
* "gsd-sdk": "bin/gsd-sdk.js" so npm chmods the shim correctly when
|
||||
* installing from a packed tarball — eliminating the mode-644 failure
|
||||
* (issue #2453) and the build-from-source failure modes (#2439, #2441).
|
||||
*
|
||||
* This function verifies the invariant: sdk/dist/cli.js exists and is
|
||||
* executable. If the execute bit is missing (possible in dev/clone setups
|
||||
* where sdk/dist was committed without +x), we fix it in-place.
|
||||
*
|
||||
* --no-sdk skips the check entirely (back-compat).
|
||||
* --sdk forces the check even if it would otherwise be skipped.
|
||||
*/
|
||||
/**
|
||||
* Classify the install context for the SDK directory.
|
||||
*
|
||||
* Distinguishes three shapes the installer must handle differently when
|
||||
* `sdk/dist/` is missing:
|
||||
*
|
||||
* - `tarball` + `npxCache: true`
|
||||
* User ran `npx get-shit-done-cc@latest`. sdk/ lives under
|
||||
* `<npm-cache>/_npx/<hash>/node_modules/get-shit-done-cc/sdk` which
|
||||
* is treated as read-only by npm/npx on Windows (#2649). We MUST
|
||||
* NOT attempt a nested `npm install` there — it will fail with
|
||||
* EACCES/EPERM and produce the misleading "Failed to npm install
|
||||
* in sdk/" error the user reported. Point at the global upgrade.
|
||||
*
|
||||
* - `tarball` + `npxCache: false`
|
||||
* User ran a global install (`npm i -g get-shit-done-cc`). sdk/dist
|
||||
* ships in the published tarball; if it's missing, the published
|
||||
* artifact itself is broken (see #2647). Same user-facing fix:
|
||||
* upgrade to latest.
|
||||
*
|
||||
* - `dev-clone`
|
||||
* Developer running from a git clone. Keep the existing "cd sdk &&
|
||||
* npm install && npm run build" hint — the user is expected to run
|
||||
* that themselves. The installer itself never shells out to npm.
|
||||
*
|
||||
* Detection heuristics are path-based and side-effect-free: we look for
|
||||
* `_npx` and `node_modules` segments that indicate a packaged install,
|
||||
* and for a `.git` directory nearby that indicates a clone. A best-effort
|
||||
* write probe detects read-only filesystems (tmpfile create + unlink);
|
||||
* probe failures are treated as read-only.
|
||||
*/
|
||||
function classifySdkInstall(sdkDir) {
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const segments = sdkDir.split(/[\\/]+/);
|
||||
const npxCache = segments.includes('_npx');
|
||||
const inNodeModules = segments.includes('node_modules');
|
||||
const parent = path.dirname(sdkDir);
|
||||
const hasGitNearby = fs.existsSync(path.join(parent, '.git'));
|
||||
|
||||
let mode;
|
||||
if (hasGitNearby && !npxCache && !inNodeModules) {
|
||||
mode = 'dev-clone';
|
||||
} else if (npxCache || inNodeModules) {
|
||||
mode = 'tarball';
|
||||
} else {
|
||||
mode = 'dev-clone';
|
||||
}
|
||||
|
||||
let readOnly = npxCache; // assume true for npx cache
|
||||
if (!readOnly) {
|
||||
try {
|
||||
const probe = path.join(sdkDir, `.gsd-write-probe-${process.pid}`);
|
||||
fs.writeFileSync(probe, '');
|
||||
fs.unlinkSync(probe);
|
||||
} catch {
|
||||
readOnly = true;
|
||||
}
|
||||
}
|
||||
|
||||
return { mode, npxCache, readOnly };
|
||||
}
|
||||
|
||||
function installSdkIfNeeded(opts) {
|
||||
opts = opts || {};
|
||||
if (hasNoSdk && !opts.sdkDir) {
|
||||
console.log(`\n ${dim}Skipping GSD SDK check (--no-sdk)${reset}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const sdkDir = opts.sdkDir || path.resolve(__dirname, '..', 'sdk');
|
||||
const sdkCliPath = path.join(sdkDir, 'dist', 'cli.js');
|
||||
|
||||
if (!fs.existsSync(sdkCliPath)) {
|
||||
const ctx = classifySdkInstall(sdkDir);
|
||||
const bar = '━'.repeat(72);
|
||||
const redBold = `${red}${bold}`;
|
||||
console.error('');
|
||||
console.error(`${redBold}${bar}${reset}`);
|
||||
console.error(`${redBold} ✗ GSD SDK dist not found — /gsd-* commands will not work${reset}`);
|
||||
console.error(`${redBold}${bar}${reset}`);
|
||||
console.error(` ${red}Reason:${reset} sdk/dist/cli.js not found at ${sdkCliPath}`);
|
||||
console.error('');
|
||||
|
||||
if (ctx.mode === 'tarball') {
|
||||
// User install (including `npx get-shit-done-cc@latest`, which stages
|
||||
// a read-only tarball under the npx cache). The sdk/dist/ artifact
|
||||
// should ship in the published tarball. If it's missing, the only
|
||||
// sane fix from the user's side is a fresh global install of a
|
||||
// version that includes dist/. Do NOT attempt a nested `npm install`
|
||||
// inside the (read-only) npx cache — that's the #2649 failure mode.
|
||||
if (ctx.npxCache) {
|
||||
console.error(` Detected read-only npx cache install (${dim}${sdkDir}${reset}).`);
|
||||
console.error(` The installer will ${bold}not${reset} attempt \`npm install\` inside the npx cache.`);
|
||||
console.error('');
|
||||
} else {
|
||||
console.error(` The published tarball appears to be missing sdk/dist/ (see #2647).`);
|
||||
console.error('');
|
||||
}
|
||||
console.error(` Fix: install a version that ships sdk/dist/ globally:`);
|
||||
console.error(` ${cyan}npm install -g get-shit-done-cc@latest${reset}`);
|
||||
console.error(` Or, if you prefer a one-shot run, clear the npx cache first:`);
|
||||
console.error(` ${cyan}npx --yes get-shit-done-cc@latest${reset}`);
|
||||
console.error(` Or build from source (git clone):`);
|
||||
console.error(` ${cyan}git clone https://github.com/gsd-build/get-shit-done && cd get-shit-done/sdk && npm install && npm run build${reset}`);
|
||||
} else {
|
||||
// Dev clone: keep the existing build-from-source hint.
|
||||
console.error(` Running from a git clone — build the SDK first:`);
|
||||
console.error(` ${cyan}cd sdk && npm install && npm run build${reset}`);
|
||||
}
|
||||
console.error(`${redBold}${bar}${reset}`);
|
||||
console.error('');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Ensure execute bit is set. tsc emits files at 0o644; git clone preserves
|
||||
// whatever mode was committed. Fix in-place so node-invoked paths work too.
|
||||
try {
|
||||
const stat = fs.statSync(sdkCliPath);
|
||||
const isExecutable = !!(stat.mode & 0o111);
|
||||
if (!isExecutable) {
|
||||
fs.chmodSync(sdkCliPath, stat.mode | 0o111);
|
||||
}
|
||||
} catch {
|
||||
// Non-fatal: if chmod fails (e.g. read-only fs) the shim still works via
|
||||
// `node sdkCliPath` invocation in bin/gsd-sdk.js.
|
||||
}
|
||||
|
||||
console.log(` ${green}✓${reset} GSD SDK ready (sdk/dist/cli.js)`);
|
||||
|
||||
// #2620: warn if npm's global bin is not on PATH, suppressing the
|
||||
// absolute-path suggestion when the user's rc already covers it via
|
||||
// a HOME-relative entry (e.g. `export PATH="$HOME/.npm-global/bin:$PATH"`).
|
||||
try {
|
||||
const { execSync } = require('child_process');
|
||||
const npmPrefix = execSync('npm prefix -g', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
||||
if (npmPrefix) {
|
||||
// On Windows npm prefix IS the bin dir; on POSIX it's `${prefix}/bin`.
|
||||
const globalBin = process.platform === 'win32' ? npmPrefix : path.join(npmPrefix, 'bin');
|
||||
maybeSuggestPathExport(globalBin, os.homedir());
|
||||
}
|
||||
} catch {
|
||||
// npm not available / exec failed — silently skip the PATH advice.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -6863,13 +7265,10 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
|
||||
const primaryStatuslineResult = results.find(r => statuslineRuntimes.includes(r.runtime));
|
||||
|
||||
const finalize = (shouldInstallStatusline) => {
|
||||
// Build @gsd-build/sdk from the in-repo sdk/ source and install it globally
|
||||
// so `gsd-sdk` lands on PATH. Every /gsd-* command shells out to
|
||||
// `gsd-sdk query …`; without this, commands fail with "command not found:
|
||||
// gsd-sdk". The npm-published @gsd-build/sdk is kept intentionally frozen
|
||||
// at an older version; we always build from source so users get the SDK
|
||||
// that matches the installed GSD version.
|
||||
// Runs by default; skip with --no-sdk. Idempotent when already present.
|
||||
// Verify sdk/dist/cli.js is present and executable. The dist is shipped
|
||||
// prebuilt in the tarball (fix/2441-sdk-decouple); gsd-sdk reaches users via
|
||||
// the parent package's bin/gsd-sdk.js shim, so no sub-install is needed.
|
||||
// Skip with --no-sdk.
|
||||
installSdkIfNeeded();
|
||||
|
||||
const printSummaries = () => {
|
||||
@@ -6911,8 +7310,12 @@ if (process.env.GSD_TEST_MODE) {
|
||||
stripGsdFromCodexConfig,
|
||||
mergeCodexConfig,
|
||||
installCodexConfig,
|
||||
readGsdRuntimeProfileResolver,
|
||||
readGsdEffectiveModelOverrides,
|
||||
install,
|
||||
uninstall,
|
||||
installSdkIfNeeded,
|
||||
classifySdkInstall,
|
||||
convertClaudeCommandToCodexSkill,
|
||||
convertClaudeToOpencodeFrontmatter,
|
||||
convertClaudeToKiloFrontmatter,
|
||||
@@ -6940,6 +7343,7 @@ if (process.env.GSD_TEST_MODE) {
|
||||
convertClaudeAgentToAntigravityAgent,
|
||||
copyCommandsAsAntigravitySkills,
|
||||
convertClaudeCommandToClaudeSkill,
|
||||
skillFrontmatterName,
|
||||
copyCommandsAsClaudeSkills,
|
||||
convertClaudeToWindsurfMarkdown,
|
||||
convertClaudeCommandToWindsurfSkill,
|
||||
@@ -6965,6 +7369,8 @@ if (process.env.GSD_TEST_MODE) {
|
||||
preserveUserArtifacts,
|
||||
restoreUserArtifacts,
|
||||
finishInstall,
|
||||
homePathCoveredByRc,
|
||||
maybeSuggestPathExport,
|
||||
};
|
||||
} else {
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ the normal phase sequence and accumulate context over time.
|
||||
**Plans:** 0 plans
|
||||
|
||||
Plans:
|
||||
- [ ] TBD (promote with /gsd:review-backlog when ready)
|
||||
- [ ] TBD (promote with /gsd-review-backlog when ready)
|
||||
```
|
||||
|
||||
4. **Create the phase directory:**
|
||||
@@ -65,15 +65,15 @@ the normal phase sequence and accumulate context over time.
|
||||
Directory: .planning/phases/{NEXT}-{slug}/
|
||||
|
||||
This item lives in the backlog parking lot.
|
||||
Use /gsd:discuss-phase {NEXT} to explore it further.
|
||||
Use /gsd:review-backlog to promote items to active milestone.
|
||||
Use /gsd-discuss-phase {NEXT} to explore it further.
|
||||
Use /gsd-review-backlog to promote items to active milestone.
|
||||
```
|
||||
|
||||
</process>
|
||||
|
||||
<notes>
|
||||
- 999.x numbering keeps backlog items out of the active phase sequence
|
||||
- Phase directories are created immediately, so /gsd:discuss-phase and /gsd:plan-phase work on them
|
||||
- Phase directories are created immediately, so /gsd-discuss-phase and /gsd-plan-phase work on them
|
||||
- No `Depends on:` field — backlog items are unsequenced by definition
|
||||
- Sparse numbering is fine (999.1, 999.3) — always uses next-decimal
|
||||
</notes>
|
||||
|
||||
@@ -13,8 +13,8 @@ allowed-tools:
|
||||
- AskUserQuestion
|
||||
argument-instructions: |
|
||||
Parse the argument as a phase number (integer, decimal, or letter-suffix), plus optional free-text instructions.
|
||||
Example: /gsd:add-tests 12
|
||||
Example: /gsd:add-tests 12 focus on edge cases in the pricing module
|
||||
Example: /gsd-add-tests 12
|
||||
Example: /gsd-add-tests 12 focus on edge cases in the pricing module
|
||||
---
|
||||
<objective>
|
||||
Generate unit and E2E tests for a completed phase, using its SUMMARY.md, CONTEXT.md, and VERIFICATION.md as specifications.
|
||||
|
||||
@@ -25,7 +25,7 @@ Then suggest `Depends on` updates to ROADMAP.md.
|
||||
<context>
|
||||
No arguments required. Requires an active milestone with ROADMAP.md.
|
||||
|
||||
Run this command BEFORE `/gsd:manager` to fill in missing `Depends on` fields and prevent merge conflicts from unordered parallel execution.
|
||||
Run this command BEFORE `/gsd-manager` to fill in missing `Depends on` fields and prevent merge conflicts from unordered parallel execution.
|
||||
</context>
|
||||
|
||||
<process>
|
||||
|
||||
@@ -42,19 +42,19 @@ Output: Milestone archived (roadmap + requirements), PROJECT.md evolved, git tag
|
||||
0. **Check for audit:**
|
||||
|
||||
- Look for `.planning/v{{version}}-MILESTONE-AUDIT.md`
|
||||
- If missing or stale: recommend `/gsd:audit-milestone` first
|
||||
- If audit status is `gaps_found`: recommend `/gsd:plan-milestone-gaps` first
|
||||
- If missing or stale: recommend `/gsd-audit-milestone` first
|
||||
- If audit status is `gaps_found`: recommend `/gsd-plan-milestone-gaps` first
|
||||
- If audit status is `passed`: proceed to step 1
|
||||
|
||||
```markdown
|
||||
## Pre-flight Check
|
||||
|
||||
{If no v{{version}}-MILESTONE-AUDIT.md:}
|
||||
⚠ No milestone audit found. Run `/gsd:audit-milestone` first to verify
|
||||
⚠ No milestone audit found. Run `/gsd-audit-milestone` first to verify
|
||||
requirements coverage, cross-phase integration, and E2E flows.
|
||||
|
||||
{If audit has gaps:}
|
||||
⚠ Milestone audit found gaps. Run `/gsd:plan-milestone-gaps` to create
|
||||
⚠ Milestone audit found gaps. Run `/gsd-plan-milestone-gaps` to create
|
||||
phases that close the gaps, or proceed anyway to accept as tech debt.
|
||||
|
||||
{If audit passed:}
|
||||
@@ -108,7 +108,7 @@ Output: Milestone archived (roadmap + requirements), PROJECT.md evolved, git tag
|
||||
- Ask about pushing tag
|
||||
|
||||
8. **Offer next steps:**
|
||||
- `/gsd:new-milestone` — start next milestone (questioning → research → requirements → roadmap)
|
||||
- `/gsd-new-milestone` — start next milestone (questioning → research → requirements → roadmap)
|
||||
|
||||
</process>
|
||||
|
||||
@@ -132,5 +132,5 @@ Output: Milestone archived (roadmap + requirements), PROJECT.md evolved, git tag
|
||||
- **Archive before deleting:** Always create archive files before updating/deleting originals
|
||||
- **One-line summary:** Collapsed milestone in ROADMAP.md should be single line with link
|
||||
- **Context efficiency:** Archive keeps ROADMAP.md and REQUIREMENTS.md constant size per milestone
|
||||
- **Fresh requirements:** Next milestone starts with `/gsd:new-milestone` which includes requirements definition
|
||||
- **Fresh requirements:** Next milestone starts with `/gsd-new-milestone` which includes requirements definition
|
||||
</critical_rules>
|
||||
|
||||
@@ -88,11 +88,11 @@ Active Debug Sessions
|
||||
hypothesis: Missing null check on req.body.user
|
||||
next: Verify fix passes regression test
|
||||
─────────────────────────────────────────────
|
||||
Run `/gsd:debug continue <slug>` to resume a session.
|
||||
No sessions? `/gsd:debug <description>` to start.
|
||||
Run `/gsd-debug continue <slug>` to resume a session.
|
||||
No sessions? `/gsd-debug <description>` to start.
|
||||
```
|
||||
|
||||
If no files exist or the glob returns nothing: print "No active debug sessions. Run `/gsd:debug <issue description>` to start one."
|
||||
If no files exist or the glob returns nothing: print "No active debug sessions. Run `/gsd-debug <issue description>` to start one."
|
||||
|
||||
STOP after displaying list. Do NOT proceed to further steps.
|
||||
|
||||
@@ -117,7 +117,7 @@ No agent spawn. Just information display. STOP after printing.
|
||||
|
||||
When SUBCMD=continue and SLUG is set:
|
||||
|
||||
Check `.planning/debug/{SLUG}.md` exists. If not, print "No active debug session found with slug: {SLUG}. Check `/gsd:debug list` for active sessions." and stop.
|
||||
Check `.planning/debug/{SLUG}.md` exists. If not, print "No active debug session found with slug: {SLUG}. Check `/gsd-debug list` for active sessions." and stop.
|
||||
|
||||
Read file and print Current Focus block to console:
|
||||
|
||||
@@ -247,7 +247,7 @@ specialist_dispatch_enabled: true
|
||||
Display the compact summary returned by the session manager.
|
||||
|
||||
If summary shows `DEBUG SESSION COMPLETE`: done.
|
||||
If summary shows `ABANDONED`: note session saved at `.planning/debug/{slug}.md` for later `/gsd:debug continue {slug}`.
|
||||
If summary shows `ABANDONED`: note session saved at `.planning/debug/{slug}.md` for later `/gsd-debug continue {slug}`.
|
||||
|
||||
</process>
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ Open-ended Socratic ideation session. Guides the developer through exploring an
|
||||
probing questions, optionally spawns research, then routes outputs to the appropriate GSD
|
||||
artifacts (notes, todos, seeds, research questions, requirements, or new phases).
|
||||
|
||||
Accepts an optional topic argument: `/gsd:explore authentication strategy`
|
||||
Accepts an optional topic argument: `/gsd-explore authentication strategy`
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
|
||||
@@ -16,8 +16,8 @@ Execute a trivial task directly in the current context without spawning subagent
|
||||
or generating PLAN.md files. For tasks too small to justify planning overhead:
|
||||
typo fixes, config changes, small refactors, forgotten commits, simple additions.
|
||||
|
||||
This is NOT a replacement for /gsd:quick — use /gsd:quick for anything that
|
||||
needs research, multi-step planning, or verification. /gsd:fast is for tasks
|
||||
This is NOT a replacement for /gsd-quick — use /gsd-quick for anything that
|
||||
needs research, multi-step planning, or verification. /gsd-fast is for tasks
|
||||
you could describe in one sentence and execute in under 2 minutes.
|
||||
</objective>
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ Knowledge graph is disabled. To activate:
|
||||
|
||||
node $HOME/.claude/get-shit-done/bin/gsd-tools.cjs config-set graphify.enabled true
|
||||
|
||||
Then run /gsd:graphify build to create the initial graph.
|
||||
Then run /gsd-graphify build to create the initial graph.
|
||||
```
|
||||
|
||||
---
|
||||
@@ -65,7 +65,7 @@ Parse `$ARGUMENTS` to determine the operation mode:
|
||||
```
|
||||
GSD > GRAPHIFY
|
||||
|
||||
Usage: /gsd:graphify <mode>
|
||||
Usage: /gsd-graphify <mode>
|
||||
|
||||
Modes:
|
||||
build Build or rebuild the knowledge graph
|
||||
@@ -85,7 +85,7 @@ node $HOME/.claude/get-shit-done/bin/gsd-tools.cjs graphify query <term>
|
||||
Parse the JSON output and display results:
|
||||
- If the output contains `"disabled": true`, display the disabled message from Step 1 and **STOP**
|
||||
- If the output contains `"error"` field, display the error message and **STOP**
|
||||
- If no nodes found, display: `No graph matches for '<term>'. Try /gsd:graphify build to create or rebuild the graph.`
|
||||
- If no nodes found, display: `No graph matches for '<term>'. Try /gsd-graphify build to create or rebuild the graph.`
|
||||
- Otherwise, display matched nodes grouped by type, with edge relationships and confidence tiers (EXTRACTED/INFERRED/AMBIGUOUS)
|
||||
|
||||
**STOP** after displaying results. Do not spawn an agent.
|
||||
|
||||
@@ -4,7 +4,6 @@ description: Insert urgent work as decimal phase (e.g., 72.1) between existing p
|
||||
argument-hint: <after> <description>
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Write
|
||||
- Bash
|
||||
---
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ Intel system is disabled. To activate:
|
||||
|
||||
gsd-sdk query config-set intel.enabled true
|
||||
|
||||
Then run /gsd:intel refresh to build the initial index.
|
||||
Then run /gsd-intel refresh to build the initial index.
|
||||
```
|
||||
|
||||
---
|
||||
@@ -63,7 +63,7 @@ Parse `$ARGUMENTS` to determine the operation mode:
|
||||
```
|
||||
GSD > INTEL
|
||||
|
||||
Usage: /gsd:intel <mode>
|
||||
Usage: /gsd-intel <mode>
|
||||
|
||||
Modes:
|
||||
query <term> Search intel files for a term
|
||||
@@ -82,7 +82,7 @@ gsd-sdk query intel.query <term>
|
||||
|
||||
Parse the JSON output and display results:
|
||||
- If the output contains `"disabled": true`, display the disabled message from Step 1 and **STOP**
|
||||
- If no matches found, display: `No intel matches for '<term>'. Try /gsd:intel refresh to build the index.`
|
||||
- If no matches found, display: `No intel matches for '<term>'. Try /gsd-intel refresh to build the index.`
|
||||
- Otherwise, display matching entries grouped by intel file
|
||||
|
||||
**STOP** after displaying results. Do not spawn an agent.
|
||||
|
||||
@@ -30,8 +30,8 @@ Focus area: $ARGUMENTS (optional - if provided, tells agents to focus on specifi
|
||||
Check for .planning/STATE.md - loads context if project already initialized
|
||||
|
||||
**This command can run:**
|
||||
- Before /gsd:new-project (brownfield codebases) - creates codebase map first
|
||||
- After /gsd:new-project (greenfield codebases) - updates codebase map as code evolves
|
||||
- Before /gsd-new-project (brownfield codebases) - creates codebase map first
|
||||
- After /gsd-new-project (greenfield codebases) - updates codebase map as code evolves
|
||||
- Anytime to refresh codebase understanding
|
||||
</context>
|
||||
|
||||
@@ -59,7 +59,7 @@ Check for .planning/STATE.md - loads context if project already initialized
|
||||
4. Wait for agents to complete, collect confirmations (NOT document contents)
|
||||
5. Verify all 7 documents exist with line counts
|
||||
6. Commit codebase map
|
||||
7. Offer next steps (typically: /gsd:new-project or /gsd:plan-phase)
|
||||
7. Offer next steps (typically: /gsd-new-project or /gsd-plan-phase)
|
||||
</process>
|
||||
|
||||
<success_criteria>
|
||||
|
||||
@@ -21,7 +21,7 @@ Brownfield equivalent of new-project. Project exists, PROJECT.md has history. Ga
|
||||
- `.planning/ROADMAP.md` — phase structure (continues numbering)
|
||||
- `.planning/STATE.md` — reset for new milestone
|
||||
|
||||
**After:** `/gsd:plan-phase [N]` to start execution.
|
||||
**After:** `/gsd-plan-phase [N]` to start execution.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
|
||||
@@ -29,7 +29,7 @@ Initialize a new project through unified flow: questioning → research (optiona
|
||||
- `.planning/ROADMAP.md` — phase structure
|
||||
- `.planning/STATE.md` — project memory
|
||||
|
||||
**After this command:** Run `/gsd:plan-phase 1` to start execution.
|
||||
**After this command:** Run `/gsd-plan-phase 1` to start execution.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
|
||||
@@ -30,7 +30,7 @@ Create a physical workspace directory containing copies of specified git repos (
|
||||
- `<path>/.planning/` — independent planning directory
|
||||
- `<path>/<repo>/` — git worktree or clone for each specified repo
|
||||
|
||||
**After this command:** `cd` into the workspace and run `/gsd:new-project` to initialize GSD.
|
||||
**After this command:** `cd` into the workspace and run `/gsd-new-project` to initialize GSD.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
|
||||
@@ -10,11 +10,11 @@ allowed-tools:
|
||||
- AskUserQuestion
|
||||
---
|
||||
<objective>
|
||||
Create all phases necessary to close gaps identified by `/gsd:audit-milestone`.
|
||||
Create all phases necessary to close gaps identified by `/gsd-audit-milestone`.
|
||||
|
||||
Reads MILESTONE-AUDIT.md, groups gaps into logical phases, creates phase entries in ROADMAP.md, and offers to plan each phase.
|
||||
|
||||
One command creates all fix phases — no manual `/gsd:add-phase` per gap.
|
||||
One command creates all fix phases — no manual `/gsd-add-phase` per gap.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
|
||||
@@ -40,7 +40,7 @@ Phase number: $ARGUMENTS (optional — auto-detects next unplanned phase if omit
|
||||
- `--gaps` — Gap closure mode (reads VERIFICATION.md, skips research)
|
||||
- `--skip-verify` — Skip verification loop
|
||||
- `--prd <file>` — Use a PRD/acceptance criteria file instead of discuss-phase. Parses requirements into CONTEXT.md automatically. Skips discuss-phase entirely.
|
||||
- `--reviews` — Replan incorporating cross-AI review feedback from REVIEWS.md (produced by `/gsd:review`)
|
||||
- `--reviews` — Replan incorporating cross-AI review feedback from REVIEWS.md (produced by `/gsd-review`)
|
||||
- `--text` — Use plain-text numbered lists instead of TUI menus (required for `/rc` remote sessions)
|
||||
|
||||
Normalize phase input in step 2 before any directory lookups.
|
||||
|
||||
@@ -16,7 +16,7 @@ milestone arrives. Seeds solve context rot: instead of a one-liner in Deferred t
|
||||
reads, a seed preserves the full WHY, WHEN to surface, and breadcrumbs to details.
|
||||
|
||||
Creates: .planning/seeds/SEED-NNN-slug.md
|
||||
Consumed by: /gsd:new-milestone (scans seeds and presents matches)
|
||||
Consumed by: /gsd-new-milestone (scans seeds and presents matches)
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
|
||||
@@ -71,7 +71,7 @@ For each directory found:
|
||||
- Check if PLAN.md exists
|
||||
- Check if SUMMARY.md exists; if so, read `status` from its frontmatter via:
|
||||
```bash
|
||||
gsd-sdk query frontmatter.get .planning/quick/{dir}/SUMMARY.md status 2>/dev/null
|
||||
gsd-sdk query frontmatter.get .planning/quick/{dir}/SUMMARY.md status
|
||||
```
|
||||
- Determine directory creation date: `stat -f "%SB" -t "%Y-%m-%d"` (macOS) or `stat -c "%w"` (Linux); fall back to the date prefix in the directory name (format: `YYYYMMDD-` prefix)
|
||||
- Derive display status:
|
||||
@@ -118,7 +118,7 @@ Status: {status from SUMMARY.md frontmatter, or "no summary yet"}
|
||||
Description: {first non-empty line from PLAN.md after frontmatter}
|
||||
Last action: {last meaningful line of SUMMARY.md, or "none"}
|
||||
─────────────────────────────────────
|
||||
Resume with: /gsd:quick resume {slug}
|
||||
Resume with: /gsd-quick resume {slug}
|
||||
```
|
||||
|
||||
No agent spawn. STOP after printing.
|
||||
|
||||
@@ -115,7 +115,7 @@ Read `backup-meta.json` from the patches directory.
|
||||
```
|
||||
No local patches found. Nothing to reapply.
|
||||
|
||||
Local patches are automatically saved when you run /gsd:update
|
||||
Local patches are automatically saved when you run /gsd-update
|
||||
after modifying any GSD workflow, command, or agent files.
|
||||
```
|
||||
Exit.
|
||||
@@ -278,7 +278,7 @@ Before proceeding to cleanup, evaluate the Hunk Verification Table produced in S
|
||||
**If the Hunk Verification Table is absent** (Step 4 did not produce it), STOP immediately and report to the user:
|
||||
```
|
||||
ERROR: Hunk Verification Table is missing. Post-merge verification was not completed.
|
||||
Rerun /gsd:reapply-patches to retry with full verification.
|
||||
Rerun /gsd-reapply-patches to retry with full verification.
|
||||
```
|
||||
|
||||
**If any row in the Hunk Verification Table shows `verified: no`**, STOP and report to the user:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: gsd:research-phase
|
||||
description: Research how to implement a phase (standalone - usually use /gsd:plan-phase instead)
|
||||
description: Research how to implement a phase (standalone - usually use /gsd-plan-phase instead)
|
||||
argument-hint: "[phase]"
|
||||
allowed-tools:
|
||||
- Read
|
||||
@@ -11,7 +11,7 @@ allowed-tools:
|
||||
<objective>
|
||||
Research how to implement a phase. Spawns gsd-phase-researcher agent with phase context.
|
||||
|
||||
**Note:** This is a standalone research command. For most workflows, use `/gsd:plan-phase` which integrates research automatically.
|
||||
**Note:** This is a standalone research command. For most workflows, use `/gsd-plan-phase` which integrates research automatically.
|
||||
|
||||
**Use this command when:**
|
||||
- You want to research without planning yet
|
||||
@@ -115,7 +115,7 @@ Mode: ecosystem
|
||||
</additional_context>
|
||||
|
||||
<downstream_consumer>
|
||||
Your RESEARCH.md will be loaded by `/gsd:plan-phase` which uses specific sections:
|
||||
Your RESEARCH.md will be loaded by `/gsd-plan-phase` which uses specific sections:
|
||||
- `## Standard Stack` → Plans use these libraries
|
||||
- `## Architecture Patterns` → Task structure follows these
|
||||
- `## Don't Hand-Roll` → Tasks NEVER build custom solutions for listed problems
|
||||
|
||||
@@ -13,7 +13,7 @@ allowed-tools:
|
||||
<objective>
|
||||
Invoke external AI CLIs (Gemini, Claude, Codex, OpenCode, Qwen Code, Cursor) to independently review phase plans.
|
||||
Produces a structured REVIEWS.md with per-reviewer feedback that can be fed back into
|
||||
planning via /gsd:plan-phase --reviews.
|
||||
planning via /gsd-plan-phase --reviews.
|
||||
|
||||
**Flow:** Detect CLIs → Build review prompt → Invoke each CLI → Collect responses → Write REVIEWS.md
|
||||
</objective>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: gsd:scan
|
||||
description: Rapid codebase assessment — lightweight alternative to /gsd:map-codebase
|
||||
description: Rapid codebase assessment — lightweight alternative to /gsd-map-codebase
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Write
|
||||
@@ -14,7 +14,7 @@ allowed-tools:
|
||||
Run a focused codebase scan for a single area, producing targeted documents in `.planning/codebase/`.
|
||||
Accepts an optional `--focus` flag: `tech`, `arch`, `quality`, `concerns`, or `tech+arch` (default).
|
||||
|
||||
Lightweight alternative to `/gsd:map-codebase` — spawns one mapper agent instead of four parallel ones.
|
||||
Lightweight alternative to `/gsd-map-codebase` — spawns one mapper agent instead of four parallel ones.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
|
||||
@@ -9,4 +9,4 @@ allowed-tools:
|
||||
|
||||
Show the following output to the user verbatim, with no extra commentary:
|
||||
|
||||
!`if ! command -v gsd-sdk >/dev/null 2>&1; then printf '⚠ gsd-sdk not found in PATH — /gsd:set-profile requires it.\n\nInstall the GSD SDK:\n npm install -g @gsd-build/sdk\n\nOr update GSD to get the latest packages:\n /gsd:update\n'; exit 1; fi; gsd-sdk query config-set-model-profile $ARGUMENTS --raw`
|
||||
!`if ! command -v gsd-sdk >/dev/null 2>&1; then printf '⚠ gsd-sdk not found in PATH — /gsd-set-profile requires it.\n\nInstall the GSD SDK:\n npm install -g @gsd-build/sdk\n\nOr update GSD to get the latest packages:\n /gsd-update\n'; exit 1; fi; gsd-sdk query config-set-model-profile $ARGUMENTS --raw`
|
||||
|
||||
39
commands/gsd/settings-advanced.md
Normal file
39
commands/gsd/settings-advanced.md
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: gsd:settings-advanced
|
||||
description: Power-user configuration — plan bounce, timeouts, branch templates, cross-AI execution, runtime knobs
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Write
|
||||
- Bash
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
<objective>
|
||||
Interactive configuration of GSD power-user knobs that don't belong in the common-case `/gsd-settings` prompt.
|
||||
|
||||
Routes to the settings-advanced workflow which handles:
|
||||
- Config existence ensuring (workstream-aware path resolution)
|
||||
- Current settings reading and parsing
|
||||
- Sectioned prompts: Planning Tuning, Execution Tuning, Discussion Tuning, Cross-AI Execution, Git Customization, Runtime / Output
|
||||
- Config merging that preserves every unrelated key
|
||||
- Confirmation table display
|
||||
|
||||
Use `/gsd-settings` for the common-case toggles (model profile, research/plan_check/verifier, branching strategy, context warnings). Use `/gsd-settings-advanced` once those are set and you want to tune the internals.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/get-shit-done/workflows/settings-advanced.md
|
||||
</execution_context>
|
||||
|
||||
<process>
|
||||
**Follow the settings-advanced workflow** from `@~/.claude/get-shit-done/workflows/settings-advanced.md`.
|
||||
|
||||
The workflow handles all logic including:
|
||||
1. Config file creation with defaults if missing (via `gsd-sdk query config-ensure-section`)
|
||||
2. Current config reading
|
||||
3. Six sectioned AskUserQuestion batches with current values pre-selected
|
||||
4. Numeric-input validation (non-numeric rejected, empty input keeps current)
|
||||
5. Answer parsing and config merging (preserves unrelated keys)
|
||||
6. File writing (atomic)
|
||||
7. Confirmation table display
|
||||
</process>
|
||||
44
commands/gsd/settings-integrations.md
Normal file
44
commands/gsd/settings-integrations.md
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
name: gsd:settings-integrations
|
||||
description: Configure third-party API keys, code-review CLI routing, and agent-skill injection
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Write
|
||||
- Bash
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
<objective>
|
||||
Interactive configuration of GSD's third-party integration surface:
|
||||
- Search API keys: `brave_search`, `firecrawl`, `exa_search`, and
|
||||
the `search_gitignored` toggle
|
||||
- Code-review CLI routing: `review.models.{claude,codex,gemini,opencode}`
|
||||
- Agent-skill injection: `agent_skills.<agent-type>`
|
||||
|
||||
API keys are stored plaintext in `.planning/config.json` but are masked
|
||||
(`****<last-4>`) in every piece of interactive output. The workflow never
|
||||
echoes plaintext to stdout, stderr, or any log.
|
||||
|
||||
This command is deliberately distinct from `/gsd-settings` (workflow toggles)
|
||||
and any `/gsd-settings-advanced` tuning surface. It handles *connectivity*,
|
||||
not pipeline shape.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/get-shit-done/workflows/settings-integrations.md
|
||||
</execution_context>
|
||||
|
||||
<process>
|
||||
**Follow the settings-integrations workflow** from
|
||||
`@~/.claude/get-shit-done/workflows/settings-integrations.md`.
|
||||
|
||||
The workflow handles:
|
||||
1. Resolving `$GSD_CONFIG_PATH` (flat vs workstream)
|
||||
2. Reading current integration values (masked for display)
|
||||
3. Section 1 — Search Integrations: Brave / Firecrawl / Exa / search_gitignored
|
||||
4. Section 2 — Review CLI Routing: review.models.{claude,codex,gemini,opencode}
|
||||
5. Section 3 — Agent Skills Injection: agent_skills.<agent-type>
|
||||
6. Writing values via `gsd-sdk query config-set` (which merges, preserving
|
||||
unrelated keys)
|
||||
7. Masked confirmation display
|
||||
</process>
|
||||
@@ -11,7 +11,7 @@ allowed-tools:
|
||||
- AskUserQuestion
|
||||
---
|
||||
<objective>
|
||||
Bridge local completion → merged PR. After /gsd:verify-work passes, ship the work: push branch, create PR with auto-generated body, optionally trigger review, and track the merge.
|
||||
Bridge local completion → merged PR. After /gsd-verify-work passes, ship the work: push branch, create PR with auto-generated body, optionally trigger review, and track the merge.
|
||||
|
||||
Closes the plan → execute → verify → ship loop.
|
||||
</objective>
|
||||
|
||||
@@ -25,7 +25,7 @@ Two modes:
|
||||
- **Idea mode** (default) — describe a design idea to sketch
|
||||
- **Frontier mode** (no argument or "frontier") — analyzes existing sketch landscape and proposes consistency and frontier sketches
|
||||
|
||||
Does not require `/gsd:new-project` — auto-creates `.planning/sketches/` if needed.
|
||||
Does not require `/gsd-new-project` — auto-creates `.planning/sketches/` if needed.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
|
||||
@@ -58,5 +58,5 @@ Execute the spec-phase workflow from @~/.claude/get-shit-done/workflows/spec-pha
|
||||
- Gate passed: ambiguity ≤ 0.20 AND all dimension minimums met
|
||||
- SPEC.md written with falsifiable requirements, explicit boundaries, and acceptance criteria
|
||||
- SPEC.md committed atomically
|
||||
- User knows they can now run /gsd:discuss-phase which will load SPEC.md automatically
|
||||
- User knows they can now run /gsd-discuss-phase which will load SPEC.md automatically
|
||||
</success_criteria>
|
||||
|
||||
@@ -25,7 +25,7 @@ Two modes:
|
||||
- **Idea mode** (default) — describe an idea to spike
|
||||
- **Frontier mode** (no argument or "frontier") — analyzes existing spike landscape and proposes integration and frontier spikes
|
||||
|
||||
Does not require `/gsd:new-project` — auto-creates `.planning/spikes/` if needed.
|
||||
Does not require `/gsd-new-project` — auto-creates `.planning/spikes/` if needed.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
|
||||
@@ -38,7 +38,7 @@ ls .planning/threads/*.md 2>/dev/null
|
||||
For each thread file found:
|
||||
- Read frontmatter `status` field via:
|
||||
```bash
|
||||
gsd-sdk query frontmatter.get .planning/threads/{file} status 2>/dev/null
|
||||
gsd-sdk query frontmatter.get .planning/threads/{file} status
|
||||
```
|
||||
- If frontmatter `status` field is missing, fall back to reading markdown heading `## Status: OPEN` (or IN PROGRESS / RESOLVED) from the file body
|
||||
- Read frontmatter `updated` field for the last-updated date
|
||||
@@ -62,7 +62,7 @@ frontend-build-tools resolved 2026-04-01 Vite vs webpack
|
||||
|
||||
If no threads exist (or none match the filter):
|
||||
```
|
||||
No threads found. Create one with: /gsd:thread <description>
|
||||
No threads found. Create one with: /gsd-thread <description>
|
||||
```
|
||||
|
||||
STOP after displaying. Do NOT proceed to further steps.
|
||||
@@ -117,8 +117,8 @@ When SUBCMD=status and SLUG is set (already sanitized):
|
||||
Next Steps:
|
||||
{content of ## Next Steps section}
|
||||
─────────────────────────────────────
|
||||
Resume with: /gsd:thread {SLUG}
|
||||
Close with: /gsd:thread close {SLUG}
|
||||
Resume with: /gsd-thread {SLUG}
|
||||
Close with: /gsd-thread close {SLUG}
|
||||
```
|
||||
|
||||
No agent spawn. STOP after printing.
|
||||
@@ -201,8 +201,8 @@ updated: {today ISO date}
|
||||
Thread: {slug}
|
||||
File: .planning/threads/{slug}.md
|
||||
|
||||
Resume anytime with: /gsd:thread {slug}
|
||||
Close when done with: /gsd:thread close {slug}
|
||||
Resume anytime with: /gsd-thread {slug}
|
||||
Close when done with: /gsd-thread close {slug}
|
||||
```
|
||||
</mode_create>
|
||||
|
||||
@@ -210,10 +210,10 @@ updated: {today ISO date}
|
||||
|
||||
<notes>
|
||||
- Threads are NOT phase-scoped — they exist independently of the roadmap
|
||||
- Lighter weight than /gsd:pause-work — no phase state, no plan context
|
||||
- Lighter weight than /gsd-pause-work — no phase state, no plan context
|
||||
- The value is in Context and Next Steps — a cold-start session can pick up immediately
|
||||
- Threads can be promoted to phases or backlog items when they mature:
|
||||
/gsd:add-phase or /gsd:add-backlog with context from the thread
|
||||
/gsd-add-phase or /gsd-add-backlog with context from the thread
|
||||
- Thread files live in .planning/threads/ — no collision with phases or other GSD structures
|
||||
- Thread status values: `open`, `in_progress`, `resolved`
|
||||
</notes>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: gsd:ultraplan-phase
|
||||
description: "[BETA] Offload plan phase to Claude Code's ultraplan cloud — drafts remotely while terminal stays free, review in browser with inline comments, import back via /gsd:import. Claude Code only."
|
||||
description: "[BETA] Offload plan phase to Claude Code's ultraplan cloud — drafts remotely while terminal stays free, review in browser with inline comments, import back via /gsd-import. Claude Code only."
|
||||
argument-hint: "[phase-number]"
|
||||
allowed-tools:
|
||||
- Read
|
||||
@@ -13,9 +13,9 @@ allowed-tools:
|
||||
Offload GSD's plan phase to Claude Code's ultraplan cloud infrastructure.
|
||||
|
||||
Ultraplan drafts the plan in a remote cloud session while your terminal stays free.
|
||||
Review and comment on the plan in your browser, then import it back via /gsd:import --from.
|
||||
Review and comment on the plan in your browser, then import it back via /gsd-import --from.
|
||||
|
||||
⚠ BETA: ultraplan is in research preview. Use /gsd:plan-phase for stable local planning.
|
||||
⚠ BETA: ultraplan is in research preview. Use /gsd-plan-phase for stable local planning.
|
||||
Requirements: Claude Code v2.1.91+, claude.ai account, GitHub repository.
|
||||
</objective>
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ Validate built features through conversational testing with persistent state.
|
||||
|
||||
Purpose: Confirm what Claude built actually works from user's perspective. One test at a time, plain text responses, no interrogation. When issues are found, automatically diagnose, plan fixes, and prepare for execution.
|
||||
|
||||
Output: {phase_num}-UAT.md tracking all test results. If issues found: diagnosed gaps, verified fix plans ready for /gsd:execute-phase
|
||||
Output: {phase_num}-UAT.md tracking all test results. If issues found: diagnosed gaps, verified fix plans ready for /gsd-execute-phase
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
|
||||
@@ -6,13 +6,13 @@ allowed-tools:
|
||||
- Bash
|
||||
---
|
||||
|
||||
# /gsd:workstreams
|
||||
# /gsd-workstreams
|
||||
|
||||
Manage parallel workstreams for concurrent milestone work.
|
||||
|
||||
## Usage
|
||||
|
||||
`/gsd:workstreams [subcommand] [args]`
|
||||
`/gsd-workstreams [subcommand] [args]`
|
||||
|
||||
### Subcommands
|
||||
|
||||
@@ -40,7 +40,7 @@ Display the workstreams in a table format showing name, status, current phase, a
|
||||
### create
|
||||
Run: `gsd-sdk query workstream.create <name> --raw --cwd "$CWD"`
|
||||
After creation, display the new workstream path and suggest next steps:
|
||||
- `/gsd:new-milestone --ws <name>` to set up the milestone
|
||||
- `/gsd-new-milestone --ws <name>` to set up the milestone
|
||||
|
||||
### status
|
||||
Run: `gsd-sdk query workstream.status <name> --raw --cwd "$CWD"`
|
||||
@@ -61,7 +61,7 @@ Run: `gsd-sdk query workstream.complete <name> --raw --cwd "$CWD"`
|
||||
Archive the workstream to milestones/.
|
||||
|
||||
### resume
|
||||
Set the workstream as active and suggest `/gsd:resume-work --ws <name>`.
|
||||
Set the workstream as active and suggest `/gsd-resume-work --ws <name>`.
|
||||
|
||||
## Step 3: Display Results
|
||||
|
||||
|
||||
@@ -343,18 +343,26 @@ GSD uses a multi-agent architecture where thin orchestrators (workflow files) sp
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Spawned by** | `/gsd-map-codebase` |
|
||||
| **Spawned by** | `/gsd-map-codebase`, post-execute drift gate in `/gsd:execute-phase` |
|
||||
| **Parallelism** | 4 instances (tech, architecture, quality, concerns) |
|
||||
| **Tools** | Read, Bash, Grep, Glob, Write |
|
||||
| **Model (balanced)** | Haiku |
|
||||
| **Color** | Cyan |
|
||||
| **Produces** | `.planning/codebase/*.md` (7 documents) |
|
||||
| **Produces** | `.planning/codebase/*.md` (7 documents, with `last_mapped_commit` frontmatter) |
|
||||
|
||||
**Key behaviors:**
|
||||
- Read-only exploration + structured output
|
||||
- Writes documents directly to disk
|
||||
- No reasoning required — pattern extraction from file contents
|
||||
|
||||
**`--paths <p1,p2,...>` scope hint (#2003):**
|
||||
Accepts an optional `--paths` directive in its prompt. When present, the
|
||||
mapper restricts Glob/Grep/Bash exploration to the listed repo-relative path
|
||||
prefixes — this is the incremental-remap path used by the post-execute
|
||||
codebase-drift gate. Path values that contain `..`, start with `/`, or
|
||||
include shell metacharacters are rejected. Without the hint, the mapper
|
||||
runs its default whole-repo scan.
|
||||
|
||||
---
|
||||
|
||||
### gsd-debugger
|
||||
|
||||
@@ -131,6 +131,33 @@ Orchestration logic that commands reference. Contains the step-by-step process i
|
||||
|
||||
**Total workflows:** see [`docs/INVENTORY.md`](INVENTORY.md#workflows) for the authoritative count and full roster.
|
||||
|
||||
#### Progressive disclosure for workflows
|
||||
|
||||
Workflow files are loaded verbatim into Claude's context every time the
|
||||
corresponding `/gsd:*` command is invoked. To keep that cost bounded, the
|
||||
workflow size budget enforced by `tests/workflow-size-budget.test.cjs`
|
||||
mirrors the agent budget from #2361:
|
||||
|
||||
| Tier | Per-file line limit |
|
||||
|-----------|--------------------|
|
||||
| `XL` | 1700 — top-level orchestrators (`execute-phase`, `plan-phase`, `new-project`) |
|
||||
| `LARGE` | 1500 — multi-step planners and large feature workflows |
|
||||
| `DEFAULT` | 1000 — focused single-purpose workflows (the target tier) |
|
||||
|
||||
`workflows/discuss-phase.md` is held to a stricter <500-line ceiling per
|
||||
issue #2551. When a workflow grows beyond its tier, extract per-mode bodies
|
||||
into `workflows/<workflow>/modes/<mode>.md`, templates into
|
||||
`workflows/<workflow>/templates/`, and shared knowledge into
|
||||
`get-shit-done/references/`. The parent file becomes a thin dispatcher that
|
||||
Reads only the mode and template files needed for the current invocation.
|
||||
|
||||
`workflows/discuss-phase/` is the canonical example of this pattern —
|
||||
parent dispatches, modes/ holds per-flag behavior (`power.md`, `all.md`,
|
||||
`auto.md`, `chain.md`, `text.md`, `batch.md`, `analyze.md`, `default.md`,
|
||||
`advisor.md`), and templates/ holds CONTEXT.md, DISCUSSION-LOG.md, and
|
||||
checkpoint.json schemas that are read only when the corresponding output
|
||||
file is being written.
|
||||
|
||||
### Agents (`agents/*.md`)
|
||||
|
||||
Specialized agent definitions with frontmatter specifying:
|
||||
@@ -384,7 +411,9 @@ plan-phase
|
||||
├── Research gate (blocks if RESEARCH.md has unresolved open questions)
|
||||
├── Phase Researcher → RESEARCH.md
|
||||
├── Planner (with reachability check) → PLAN.md files
|
||||
└── Plan Checker → Verify loop (max 3x)
|
||||
├── Plan Checker → Verify loop (max 3x)
|
||||
├── Requirements coverage gate (REQ-IDs → plans)
|
||||
└── Decision coverage gate (CONTEXT.md `<decisions>` → plans, BLOCKING — #2492)
|
||||
│
|
||||
▼
|
||||
state planned-phase → STATE.md (Planned/Ready to execute)
|
||||
@@ -395,6 +424,7 @@ execute-phase (context reduction: truncated prompts, cache-friendly ordering)
|
||||
├── Executor per plan → code + atomic commits
|
||||
├── SUMMARY.md per plan
|
||||
└── Verifier → VERIFICATION.md
|
||||
└── Decision coverage gate (CONTEXT.md decisions → shipped artifacts, NON-BLOCKING — #2492)
|
||||
│
|
||||
▼
|
||||
verify-work → UAT.md (user acceptance testing)
|
||||
@@ -467,8 +497,8 @@ Equivalent paths for other runtimes:
|
||||
│ ├── ARCHITECTURE.md
|
||||
│ └── PITFALLS.md
|
||||
├── codebase/ # Brownfield mapping (from /gsd-map-codebase)
|
||||
│ ├── STACK.md
|
||||
│ ├── ARCHITECTURE.md
|
||||
│ ├── STACK.md # YAML frontmatter carries `last_mapped_commit`
|
||||
│ ├── ARCHITECTURE.md # for the post-execute drift gate (#2003)
|
||||
│ ├── CONVENTIONS.md
|
||||
│ ├── CONCERNS.md
|
||||
│ ├── STRUCTURE.md
|
||||
@@ -502,6 +532,30 @@ Equivalent paths for other runtimes:
|
||||
└── continue-here.md # Context handoff (from pause-work)
|
||||
```
|
||||
|
||||
### Post-Execute Codebase Drift Gate (#2003)
|
||||
|
||||
After the last wave of `/gsd:execute-phase` commits, the workflow runs a
|
||||
non-blocking `codebase_drift_gate` step (between `schema_drift_gate` and
|
||||
`verify_phase_goal`). It compares the diff `last_mapped_commit..HEAD`
|
||||
against `.planning/codebase/STRUCTURE.md` and counts four kinds of
|
||||
structural elements:
|
||||
|
||||
1. New directories outside mapped paths
|
||||
2. New barrel exports at `(packages|apps)/<name>/src/index.*`
|
||||
3. New migration files
|
||||
4. New route modules under `routes/` or `api/`
|
||||
|
||||
If the count meets `workflow.drift_threshold` (default 3), the gate either
|
||||
**warns** (default) with the suggested `/gsd:map-codebase --paths …` command,
|
||||
or **auto-remaps** (`workflow.drift_action = auto-remap`) by spawning
|
||||
`gsd-codebase-mapper` scoped to the affected paths. Any error in detection
|
||||
or remap is logged and the phase continues — drift detection cannot fail
|
||||
verification.
|
||||
|
||||
`last_mapped_commit` lives in YAML frontmatter at the top of each
|
||||
`.planning/codebase/*.md` file; `bin/lib/drift.cjs` provides
|
||||
`readMappedCommit` and `writeMappedCommit` round-trip helpers.
|
||||
|
||||
---
|
||||
|
||||
## Installer Architecture
|
||||
|
||||
@@ -475,6 +475,25 @@ User-facing entry point: `/gsd-graphify` (see [Command Reference](COMMANDS.md#gs
|
||||
|
||||
---
|
||||
|
||||
## Reviewer CLI Routing
|
||||
|
||||
`review.models.<cli>` maps a reviewer flavor to a shell command invoked by the code-review workflow. Set via [`/gsd-settings-integrations`](COMMANDS.md#gsd-settings-integrations) or directly:
|
||||
|
||||
```bash
|
||||
gsd-sdk query config-set review.models.codex "codex exec --model gpt-5"
|
||||
gsd-sdk query config-set review.models.gemini "gemini -m gemini-2.5-pro"
|
||||
gsd-sdk query config-set review.models.opencode "opencode run --model claude-sonnet-4"
|
||||
gsd-sdk query config-set review.models.claude "" # clear — fall back to session model
|
||||
```
|
||||
|
||||
Slugs are validated against `[a-zA-Z0-9_-]+`; empty or path-containing slugs are rejected. See [`docs/CONFIGURATION.md`](CONFIGURATION.md#code-review-cli-routing) for the full field reference.
|
||||
|
||||
## Secret Handling
|
||||
|
||||
API keys configured via `/gsd-settings-integrations` (`brave_search`, `firecrawl`, `exa_search`) are written plaintext to `.planning/config.json` but are masked (`****<last-4>`) in every `config-set` / `config-get` output, confirmation table, and interactive prompt. See `get-shit-done/bin/lib/secrets.cjs` for the masking implementation. The `config.json` file itself is the security boundary — protect it with filesystem permissions and keep it out of git (`.planning/` is gitignored by default).
|
||||
|
||||
---
|
||||
|
||||
## See also
|
||||
|
||||
- [sdk/src/query/QUERY-HANDLERS.md](../sdk/src/query/QUERY-HANDLERS.md) — registry matrix, routing, golden parity, intentional CJS differences
|
||||
|
||||
@@ -562,6 +562,24 @@ Interactive command center for managing multiple phases from one terminal.
|
||||
/gsd-manager # Open command center dashboard
|
||||
```
|
||||
|
||||
**Checkpoint Heartbeats (#2410):**
|
||||
|
||||
Background `execute-phase` runs emit `[checkpoint]` markers at every wave and plan
|
||||
boundary so the Claude API SSE stream never idles long enough to trigger
|
||||
`Stream idle timeout - partial response received` on multi-plan phases. The
|
||||
format is:
|
||||
|
||||
```
|
||||
[checkpoint] phase {N} wave {W}/{M} starting, {count} plan(s), {P}/{Q} plans done
|
||||
[checkpoint] phase {N} wave {W}/{M} plan {plan_id} starting ({P}/{Q} plans done)
|
||||
[checkpoint] phase {N} wave {W}/{M} plan {plan_id} complete ({P}/{Q} plans done)
|
||||
[checkpoint] phase {N} wave {W}/{M} complete, {P}/{Q} plans done ({ok}/{count} ok)
|
||||
```
|
||||
|
||||
If a background phase fails partway through, grep the transcript for `[checkpoint]`
|
||||
to see the last confirmed boundary. The manager's background-completion handler
|
||||
uses these markers to report partial progress when an agent errors out.
|
||||
|
||||
**Manager Passthrough Flags:**
|
||||
|
||||
Configure per-step flags in `.planning/config.json` under `manager.flags`. These flags are appended to each dispatched command:
|
||||
@@ -1037,12 +1055,73 @@ Manage parallel workstreams for concurrent work on different milestone areas.
|
||||
|
||||
### `/gsd-settings`
|
||||
|
||||
Interactive configuration of workflow toggles and model profile.
|
||||
Interactive configuration of workflow toggles and model profile. Questions are grouped into six visual sections:
|
||||
|
||||
- **Planning** — Research, Plan Checker, Pattern Mapper, Nyquist, UI Phase, UI Gate, AI Phase
|
||||
- **Execution** — Verifier, TDD Mode, Code Review, Code Review Depth _(conditional — only when Code Review is on)_, UI Review
|
||||
- **Docs & Output** — Commit Docs, Skip Discuss, Worktrees
|
||||
- **Features** — Intel, Graphify
|
||||
- **Model & Pipeline** — Model Profile, Auto-Advance, Branching
|
||||
- **Misc** — Context Warnings, Research Qs
|
||||
|
||||
All answers are merged via `gsd-sdk query config-set` into the resolved project config path (`.planning/config.json` for a standard install, or `.planning/workstreams/<active>/config.json` when a workstream is active), preserving unrelated keys. After confirmation, the user may save the full settings object to `~/.gsd/defaults.json` so future `/gsd-new-project` runs start from the same baseline.
|
||||
|
||||
```bash
|
||||
/gsd-settings # Interactive config
|
||||
```
|
||||
|
||||
### `/gsd-settings-advanced`
|
||||
|
||||
Interactive configuration of power-user knobs — plan bounce, subagent timeouts, branch templates, cross-AI delegation, context window, and runtime output. Use after `/gsd-settings` once the common-case toggles are dialed in.
|
||||
|
||||
Six sections, each a focused prompt batch:
|
||||
|
||||
| Section | Keys |
|
||||
|---------|------|
|
||||
| Planning Tuning | `workflow.plan_bounce`, `workflow.plan_bounce_passes`, `workflow.plan_bounce_script`, `workflow.subagent_timeout`, `workflow.inline_plan_threshold` |
|
||||
| Execution Tuning | `workflow.node_repair`, `workflow.node_repair_budget`, `workflow.auto_prune_state` |
|
||||
| Discussion Tuning | `workflow.max_discuss_passes` |
|
||||
| Cross-AI Execution | `workflow.cross_ai_execution`, `workflow.cross_ai_command`, `workflow.cross_ai_timeout` |
|
||||
| Git Customization | `git.base_branch`, `git.phase_branch_template`, `git.milestone_branch_template` |
|
||||
| Runtime / Output | `response_language`, `context_window`, `search_gitignored`, `graphify.build_timeout` |
|
||||
|
||||
Current values are pre-selected; an empty input keeps the existing value. Numeric fields reject non-numeric input and re-prompt. Null-allowed fields (`plan_bounce_script`, `cross_ai_command`, `response_language`) accept an empty input as a clear. Writes route through `gsd-sdk query config-set`, which preserves every unrelated key.
|
||||
|
||||
```bash
|
||||
/gsd-settings-advanced # Six-section interactive config
|
||||
```
|
||||
|
||||
See [CONFIGURATION.md](CONFIGURATION.md) for the full schema and defaults.
|
||||
|
||||
### `/gsd-settings-integrations`
|
||||
|
||||
Interactive configuration of third-party integrations and cross-tool routing.
|
||||
Distinct from `/gsd-settings` (workflow toggles) — this command handles
|
||||
connectivity: API keys, reviewer CLI routing, and agent-skill injection.
|
||||
|
||||
Covers:
|
||||
|
||||
- **Search integrations:** `brave_search`, `firecrawl`, `exa_search` API keys,
|
||||
and the `search_gitignored` toggle.
|
||||
- **Code-review CLI routing:** `review.models.{claude,codex,gemini,opencode}`
|
||||
— a shell command per reviewer flavor.
|
||||
- **Agent-skill injection:** `agent_skills.<agent-type>` — skill names
|
||||
injected into an agent's spawn frontmatter. Agent-type slugs are validated
|
||||
against `[a-zA-Z0-9_-]+` so path separators and shell metacharacters are
|
||||
rejected.
|
||||
|
||||
API keys are stored plaintext in `.planning/config.json` but displayed masked
|
||||
(`****<last-4>`) in every interactive output, confirmation table, and
|
||||
`config-set` stdout/stderr line. Plaintext is never echoed, never logged,
|
||||
and never written to any file outside `config.json` by this workflow.
|
||||
|
||||
```bash
|
||||
/gsd-settings-integrations # Interactive config (three sections)
|
||||
```
|
||||
|
||||
See [`docs/CONFIGURATION.md`](CONFIGURATION.md) for the per-field reference and
|
||||
[`docs/CLI-TOOLS.md`](CLI-TOOLS.md) for the reviewer-CLI routing contract.
|
||||
|
||||
### `/gsd-set-profile`
|
||||
|
||||
Quick profile switch.
|
||||
|
||||
@@ -52,7 +52,8 @@ GSD stores project settings in `.planning/config.json`. Created during `/gsd-new
|
||||
"cross_ai_timeout": 300,
|
||||
"security_enforcement": true,
|
||||
"security_asvs_level": 1,
|
||||
"security_block_on": "high"
|
||||
"security_block_on": "high",
|
||||
"post_planning_gaps": true
|
||||
},
|
||||
"hooks": {
|
||||
"context_warnings": true,
|
||||
@@ -111,9 +112,12 @@ GSD stores project settings in `.planning/config.json`. Created during `/gsd-new
|
||||
|---------|------|---------|---------|-------------|
|
||||
| `mode` | enum | `interactive`, `yolo` | `interactive` | `yolo` auto-approves decisions; `interactive` confirms at each step |
|
||||
| `granularity` | enum | `coarse`, `standard`, `fine` | `standard` | Controls phase count: `coarse` (3-5), `standard` (5-8), `fine` (8-12) |
|
||||
| `model_profile` | enum | `quality`, `balanced`, `budget`, `inherit` | `balanced` | Model tier for each agent (see [Model Profiles](#model-profiles)) |
|
||||
| `model_profile` | enum | `quality`, `balanced`, `budget`, `adaptive`, `inherit` | `balanced` | Model tier for each agent (see [Model Profiles](#model-profiles)). `adaptive` was added per [#1713](https://github.com/gsd-build/get-shit-done/issues/1713) / [#1806](https://github.com/gsd-build/get-shit-done/issues/1806) and resolves the same way as the other tiers under runtime-aware profiles. |
|
||||
| `runtime` | string | `claude`, `codex`, or any string | (none) | Active runtime for [runtime-aware profile resolution](#runtime-aware-profiles-2517). When set, profile tiers (opus/sonnet/haiku) resolve to runtime-native model IDs. Today only the Codex install path emits per-agent model IDs from this resolver; other runtimes (`opencode`, `gemini`, `qwen`, `copilot`, …) consume the resolver at spawn time and gain dedicated install-path support in [#2612](https://github.com/gsd-build/get-shit-done/issues/2612). When unset (default), behavior is unchanged from prior versions. Added in v1.39 |
|
||||
| `model_profile_overrides.<runtime>.<tier>` | string \| object | per-runtime tier override | (none) | Override the runtime-aware tier mapping for a specific `(runtime, tier)`. Tier is one of `opus`, `sonnet`, `haiku`. Value is either a model ID string (e.g. `"gpt-5-pro"`) or `{ model, reasoning_effort }`. See [Runtime-Aware Profiles](#runtime-aware-profiles-2517). Added in v1.39 |
|
||||
| `project_code` | string | any short string | (none) | Prefix for phase directory names (e.g., `"ABC"` produces `ABC-01-setup/`). Added in v1.31 |
|
||||
| `response_language` | string | language code | (none) | Language for agent responses (e.g., `"pt"`, `"ko"`, `"ja"`). Propagates to all spawned agents for cross-phase language consistency. Added in v1.32 |
|
||||
| `context_window` | number | any integer | `200000` | Context window size in tokens. Set `1000000` for 1M-context models (e.g., `claude-opus-4-7[1m]`). Values `>= 500000` enable adaptive context enrichment (full-body reads of prior SUMMARY.md, deeper anti-pattern reads). Configured via `/gsd-settings-advanced`. |
|
||||
| `context_profile` | string | `dev`, `research`, `review` | (none) | Execution context preset that applies a pre-configured bundle of mode, model, and workflow settings for the current type of work. Added in v1.34 |
|
||||
| `claude_md_path` | string | any file path | `./CLAUDE.md` | Custom output path for the generated CLAUDE.md file. Useful for monorepos or projects that need CLAUDE.md in a non-root location. Defaults to `./CLAUDE.md` at the project root. Added in v1.36 |
|
||||
| `claude_md_assembly.mode` | enum | `embed`, `link` | `embed` | Controls how managed sections are written into CLAUDE.md. `embed` (default) inlines content between GSD markers. `link` writes `@.planning/<source-path>` instead — Claude Code expands the reference at runtime, reducing CLAUDE.md size by ~65% on typical projects. `link` only applies to sections that have a real source file; `workflow` and fallback sections always embed. Per-block overrides: `claude_md_assembly.blocks.<section>` (e.g. `claude_md_assembly.blocks.architecture: link`). Added in v1.38 |
|
||||
@@ -128,6 +132,41 @@ GSD stores project settings in `.planning/config.json`. Created during `/gsd-new
|
||||
|
||||
---
|
||||
|
||||
## Integration Settings
|
||||
|
||||
Configured interactively via [`/gsd-settings-integrations`](COMMANDS.md#gsd-settings-integrations). These are *connectivity* settings — API keys and cross-tool routing — and are intentionally kept separate from `/gsd-settings` (workflow toggles).
|
||||
|
||||
### Search API keys
|
||||
|
||||
API key fields accept a string value (the key itself). They can also be set to the sentinels `true`/`false`/`null` to override auto-detection from env vars / `~/.gsd/*_api_key` files (legacy behavior, see rows above).
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
|---------|------|---------|-------------|
|
||||
| `brave_search` | string \| boolean \| null | `null` | Brave Search API key used for web research. Displayed as `****<last-4>` in all UI / `config-set` output; never echoed plaintext |
|
||||
| `firecrawl` | string \| boolean \| null | `null` | Firecrawl API key for deep-crawl scraping. Masked in display |
|
||||
| `exa_search` | string \| boolean \| null | `null` | Exa Search API key for semantic search. Masked in display |
|
||||
|
||||
**Masking convention (`get-shit-done/bin/lib/secrets.cjs`):** keys 8+ characters render as `****<last-4>`; shorter keys render as `****`; `null`/empty renders as `(unset)`. Plaintext is written as-is to `.planning/config.json` — that file is the security boundary — but the CLI, confirmation tables, logs, and `AskUserQuestion` descriptions never display the plaintext. This applies to the `config-set` command output itself: `config-set brave_search <key>` returns a JSON payload with the value masked.
|
||||
|
||||
### Code-review CLI routing
|
||||
|
||||
`review.models.<cli>` maps a reviewer flavor to a shell command. The code-review workflow shells out using this command when a matching flavor is requested.
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
|---------|------|---------|-------------|
|
||||
| `review.models.claude` | string | (session model) | Command for Claude-flavored review. Defaults to the session model when unset |
|
||||
| `review.models.codex` | string | `null` | Command for Codex review, e.g. `"codex exec --model gpt-5"` |
|
||||
| `review.models.gemini` | string | `null` | Command for Gemini review, e.g. `"gemini -m gemini-2.5-pro"` |
|
||||
| `review.models.opencode` | string | `null` | Command for OpenCode review, e.g. `"opencode run --model claude-sonnet-4"` |
|
||||
|
||||
The `<cli>` slug is validated against `[a-zA-Z0-9_-]+`. Empty or path-containing slugs are rejected by `config-set`.
|
||||
|
||||
### Agent-skill injection (dynamic)
|
||||
|
||||
`agent_skills.<agent-type>` extends the `agent_skills` map documented below. Slug is validated against `[a-zA-Z0-9_-]+` — no path separators, no whitespace, no shell metacharacters. Configured interactively via `/gsd-settings-integrations`.
|
||||
|
||||
---
|
||||
|
||||
## Workflow Toggles
|
||||
|
||||
All workflow toggles follow the **absent = enabled** pattern. If a key is missing from config, it defaults to `true`.
|
||||
@@ -155,6 +194,7 @@ All workflow toggles follow the **absent = enabled** pattern. If a key is missin
|
||||
| `workflow.plan_bounce` | boolean | `false` | Run external validation script against generated plans. When enabled, the plan-phase orchestrator pipes each PLAN.md through the script specified by `plan_bounce_script` and blocks on non-zero exit. Added in v1.36 |
|
||||
| `workflow.plan_bounce_script` | string | (none) | Path to the external script invoked for plan bounce validation. Receives the PLAN.md path as its first argument. Required when `plan_bounce` is `true`. Added in v1.36 |
|
||||
| `workflow.plan_bounce_passes` | number | `2` | Number of sequential bounce passes to run. Each pass feeds the previous pass's output back into the validator. Higher values increase rigor at the cost of latency. Added in v1.36 |
|
||||
| `workflow.post_planning_gaps` | boolean | `true` | Unified post-planning gap report (#2493). After all plans are generated and committed, scans REQUIREMENTS.md and CONTEXT.md `<decisions>` against every PLAN.md in the phase directory, then prints one `Source \| Item \| Status` table. Word-boundary matching (REQ-1 vs REQ-10) and natural sort (REQ-02 before REQ-10). Non-blocking — informational report only. Set to `false` to skip Step 13e of plan-phase. |
|
||||
| `workflow.plan_chunked` | boolean | `false` | Enable chunked planning mode. When `true` (or when `--chunked` flag is passed to `/gsd-plan-phase`), the orchestrator splits the single long-lived planner Task into a short outline Task followed by N short per-plan Tasks (~3-5 min each). Each plan is committed individually for crash resilience. If a Task hangs and the terminal is force-killed, rerunning with `--chunked` resumes from the last completed plan. Particularly useful on Windows where long-lived Tasks may hang on stdio. Added in v1.38 |
|
||||
| `workflow.code_review_command` | string | (none) | Shell command for external code review integration in `/gsd-ship`. Receives changed file paths via stdin. Non-zero exit blocks the ship workflow. Added in v1.36 |
|
||||
| `workflow.tdd_mode` | boolean | `false` | Enable TDD pipeline as a first-class execution mode. When `true`, the planner aggressively applies `type: tdd` to eligible tasks (business logic, APIs, validations, algorithms) and the executor enforces RED/GREEN/REFACTOR gate sequence. An end-of-phase collaborative review checkpoint verifies gate compliance. Added in v1.36 |
|
||||
@@ -166,6 +206,8 @@ All workflow toggles follow the **absent = enabled** pattern. If a key is missin
|
||||
| `workflow.pattern_mapper` | boolean | `true` | Run the `gsd-pattern-mapper` agent between research and planning to map new files to existing codebase analogs |
|
||||
| `workflow.subagent_timeout` | number | `600` | Timeout in seconds for individual subagent invocations. Increase for long-running research or execution phases |
|
||||
| `workflow.inline_plan_threshold` | number | `3` | Maximum number of tasks in a phase before the planner generates a separate PLAN.md file instead of inlining tasks in the prompt |
|
||||
| `workflow.drift_threshold` | number | `3` | Minimum number of new structural elements (new directories, barrel exports, migrations, route modules) introduced during a phase before the post-execute codebase-drift gate takes action. See [#2003](https://github.com/gsd-build/get-shit-done/issues/2003). Added in v1.39 |
|
||||
| `workflow.drift_action` | string | `warn` | What to do when `workflow.drift_threshold` is exceeded after `/gsd-execute-phase`. `warn` prints a message suggesting `/gsd-map-codebase --paths …`; `auto-remap` spawns `gsd-codebase-mapper` scoped to the affected paths. Added in v1.39 |
|
||||
|
||||
### Recommended Presets
|
||||
|
||||
@@ -185,6 +227,17 @@ All workflow toggles follow the **absent = enabled** pattern. If a key is missin
|
||||
| `planning.search_gitignored` | boolean | `false` | Add `--no-ignore` to broad searches to include `.planning/` |
|
||||
| `planning.sub_repos` | array of strings | `[]` | Paths of nested sub-repos relative to the project root. When set, GSD-aware tooling scopes phase-lookup, path-resolution, and commit operations per sub-repo instead of treating the outer repo as a monorepo |
|
||||
|
||||
### Project-Root Resolution in Multi-Repo Workspaces
|
||||
|
||||
When `sub_repos` is set and `gsd-tools.cjs` or `gsd-sdk query` is invoked from inside a listed child repo, both CLIs walk up to the parent workspace that owns `.planning/` before dispatching handlers. Resolution order (checked at each ancestor up to 10 levels, never above `$HOME`):
|
||||
|
||||
1. If the starting directory already has its own `.planning/`, it is the project root (no walk-up).
|
||||
2. Parent has `.planning/config.json` listing the starting directory's top-level segment in `sub_repos` (or the legacy `planning.sub_repos` shape).
|
||||
3. Parent has `.planning/config.json` with legacy `multiRepo: true` and the starting directory is inside a git repo.
|
||||
4. Parent has `.planning/` and an ancestor up to the candidate parent contains `.git` (heuristic fallback).
|
||||
|
||||
If none match, the starting directory is returned unchanged. Explicit `--project-dir /path/to/workspace` is idempotent under this resolution.
|
||||
|
||||
### Auto-Detection
|
||||
|
||||
If `.planning/` is in `.gitignore`, `commit_docs` is automatically `false` regardless of config.json. This prevents git errors.
|
||||
@@ -412,6 +465,60 @@ These keys live under `workflow.*` — that is where the workflows and installer
|
||||
|
||||
---
|
||||
|
||||
## Decision Coverage Gates (`workflow.context_coverage_gate`)
|
||||
|
||||
When `discuss-phase` writes implementation decisions into CONTEXT.md
|
||||
`<decisions>`, two gates ensure those decisions survive the trip into
|
||||
plans and shipped code (issue #2492).
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
|---------|------|---------|-------------|
|
||||
| `workflow.context_coverage_gate` | boolean | `true` | Toggle for both decision-coverage gates. When `false`, both the plan-phase translation gate and the verify-phase validation gate skip silently. |
|
||||
|
||||
### What the gates do
|
||||
|
||||
**Plan-phase translation gate (BLOCKING).** Runs immediately after the
|
||||
existing requirements coverage gate, before plans are committed. For each
|
||||
trackable decision in `<decisions>`, it checks that the decision id
|
||||
(`D-NN`) or its text appears in at least one plan's `must_haves`,
|
||||
`truths`, or body. A miss surfaces the missing decision by id and refuses
|
||||
to mark the phase planned.
|
||||
|
||||
**Verify-phase validation gate (NON-BLOCKING).** Runs alongside the other
|
||||
verify steps. Searches every shipped artifact (PLAN.md, SUMMARY.md, files
|
||||
modified, recent commit subjects) for each trackable decision. Misses are
|
||||
written to VERIFICATION.md as a warning section but do **not** flip the
|
||||
overall verification status. The asymmetry is deliberate — by verify time
|
||||
the work is done, and a fuzzy substring miss should not fail an otherwise
|
||||
green phase.
|
||||
|
||||
### How to write decisions the gates accept
|
||||
|
||||
The discuss-phase template already produces `D-NN`-numbered decisions.
|
||||
The gate is happiest when:
|
||||
|
||||
1. Every plan that implements a decision **cites the id** somewhere —
|
||||
`must_haves.truths: ["D-12: bit offsets exposed"]` or a `D-12:` mention
|
||||
in the plan body. Strict id match is the cheapest, deterministic path.
|
||||
2. Soft phrase matching is a fallback for paraphrases — if a 6+-word slice
|
||||
of the decision text appears verbatim in a plan/summary, it counts.
|
||||
|
||||
### Opt-outs
|
||||
|
||||
A decision is **not** subject to the gates when any of the following
|
||||
apply:
|
||||
|
||||
- It lives under the `### Claude's Discretion` heading inside `<decisions>`.
|
||||
- It is tagged `[informational]`, `[folded]`, or `[deferred]` in its
|
||||
bullet (e.g., `- **D-08 [informational]:** Naming style for internal
|
||||
helpers`).
|
||||
|
||||
Use these escape hatches when a decision genuinely doesn't need plan
|
||||
coverage — implementation discretion, future ideas captured for the
|
||||
record, or items already deferred to a later phase.
|
||||
|
||||
---
|
||||
|
||||
## Review Settings
|
||||
|
||||
Configure per-CLI model selection for `/gsd-review`. When set, overrides the CLI's default model for that reviewer.
|
||||
@@ -513,6 +620,17 @@ Override specific agents without changing the entire profile:
|
||||
|
||||
Valid override values: `opus`, `sonnet`, `haiku`, `inherit`, or any fully-qualified model ID (e.g., `"openai/o3"`, `"google/gemini-2.5-pro"`).
|
||||
|
||||
`model_overrides` can be set in either `.planning/config.json` (per-project)
|
||||
or `~/.gsd/defaults.json` (global). Per-project entries win on conflict and
|
||||
non-conflicting global entries are preserved, so you can tune a single
|
||||
agent's model in one repo without re-setting global defaults. This applies
|
||||
uniformly across Claude Code, Codex, OpenCode, Kilo, and the other
|
||||
supported runtimes. On Codex and OpenCode, the resolved model is embedded
|
||||
into each agent's static config at install time — `spawn_agent` and
|
||||
OpenCode's `task` interface do not accept an inline `model` parameter, so
|
||||
running `gsd install <runtime>` after editing `model_overrides` is required
|
||||
for the change to take effect. See issue #2256.
|
||||
|
||||
### Non-Claude Runtimes (Codex, OpenCode, Gemini CLI, Kilo)
|
||||
|
||||
When GSD is installed for a non-Claude runtime, the installer automatically sets `resolve_model_ids: "omit"` in `~/.gsd/defaults.json`. This causes GSD to return an empty model parameter for all agents, so each agent uses whatever model the runtime is configured with. No additional setup is needed for the default case.
|
||||
@@ -550,6 +668,64 @@ The intent is the same as the Claude profile tiers -- use a stronger model for p
|
||||
| `true` | Maps aliases to full Claude model IDs (`claude-opus-4-6`) | Claude Code with API that requires full IDs |
|
||||
| `"omit"` | Returns empty string (runtime picks its default) | Non-Claude runtimes (Codex, OpenCode, Gemini CLI, Kilo) |
|
||||
|
||||
### Runtime-Aware Profiles (#2517)
|
||||
|
||||
When `runtime` is set, profile tiers (`opus`/`sonnet`/`haiku`) resolve to runtime-native model IDs instead of Claude aliases. This lets a single shared `.planning/config.json` work cleanly across Claude and Codex.
|
||||
|
||||
**Built-in tier maps:**
|
||||
|
||||
| Runtime | `opus` | `sonnet` | `haiku` | reasoning_effort |
|
||||
|---------|--------|----------|---------|------------------|
|
||||
| `claude` | `claude-opus-4-6` | `claude-sonnet-4-6` | `claude-haiku-4-5` | (not used) |
|
||||
| `codex` | `gpt-5.4` | `gpt-5.3-codex` | `gpt-5.4-mini` | `xhigh` / `medium` / `medium` |
|
||||
|
||||
**Codex example** — one config, tiered models, no large `model_overrides` block:
|
||||
|
||||
```json
|
||||
{
|
||||
"runtime": "codex",
|
||||
"model_profile": "balanced"
|
||||
}
|
||||
```
|
||||
|
||||
This resolves `gsd-planner` → `gpt-5.4` (xhigh), `gsd-executor` → `gpt-5.3-codex` (medium), `gsd-codebase-mapper` → `gpt-5.4-mini` (medium). The Codex installer embeds `model = "..."` and `model_reasoning_effort = "..."` in each generated agent TOML.
|
||||
|
||||
**Claude example** — explicit opt-in resolves to full Claude IDs (no `resolve_model_ids: true` needed):
|
||||
|
||||
```json
|
||||
{
|
||||
"runtime": "claude",
|
||||
"model_profile": "quality"
|
||||
}
|
||||
```
|
||||
|
||||
**Per-runtime overrides** — replace one or more tier defaults:
|
||||
|
||||
```json
|
||||
{
|
||||
"runtime": "codex",
|
||||
"model_profile": "quality",
|
||||
"model_profile_overrides": {
|
||||
"codex": {
|
||||
"opus": "gpt-5-pro",
|
||||
"haiku": { "model": "gpt-5-nano", "reasoning_effort": "low" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Precedence (highest to lowest):**
|
||||
|
||||
1. `model_overrides[<agent>]` — explicit per-agent ID always wins.
|
||||
2. **Runtime-aware tier resolution** (this section) — when `runtime` is set and profile is not `inherit`.
|
||||
3. `resolve_model_ids: "omit"` — returns empty string when no `runtime` is set.
|
||||
4. Claude-native default — `model_profile` tier as alias (current default).
|
||||
5. `inherit` — propagates literal `inherit` for `Task(model="inherit")` semantics.
|
||||
|
||||
**Backwards compatibility.** Setups without `runtime` set see zero behavior change — every existing config continues to work identically. Codex installs that auto-set `resolve_model_ids: "omit"` continue to omit the model field unless the user opts in by setting `runtime: "codex"`.
|
||||
|
||||
**Unknown runtimes.** If `runtime` is set to a value with no built-in tier map and no `model_profile_overrides[<runtime>]`, GSD falls back to the Claude-alias safe default rather than emit a model ID the runtime cannot accept. To support a new runtime, populate `model_profile_overrides.<runtime>.{opus,sonnet,haiku}` with valid IDs.
|
||||
|
||||
### Profile Philosophy
|
||||
|
||||
| Profile | Philosophy | When to Use |
|
||||
|
||||
@@ -802,6 +802,45 @@
|
||||
| `TESTING.md` | Test infrastructure, coverage, patterns |
|
||||
| `INTEGRATIONS.md` | External services, APIs, third-party dependencies |
|
||||
|
||||
**Incremental remap — `--paths` (#2003):** The mapper accepts an optional
|
||||
`--paths <p1,p2,...>` scope hint. When provided, it restricts exploration
|
||||
to the listed repo-relative prefixes instead of scanning the whole tree.
|
||||
This is the pathway used by the post-execute codebase-drift gate to refresh
|
||||
only the subtrees the phase actually changed. Each produced document carries
|
||||
`last_mapped_commit` in its YAML frontmatter so drift can be measured
|
||||
against the mapping point, not HEAD.
|
||||
|
||||
### 27a. Post-Execute Codebase Drift Detection
|
||||
|
||||
**Introduced by:** #2003
|
||||
**Trigger:** Runs automatically at the end of every `/gsd:execute-phase`
|
||||
**Configuration:**
|
||||
- `workflow.drift_threshold` (integer, default `3`) — minimum new
|
||||
structural elements before the gate acts.
|
||||
- `workflow.drift_action` (`warn` | `auto-remap`, default `warn`) —
|
||||
warn-only or spawn `gsd-codebase-mapper` with `--paths` scoped to
|
||||
affected subtrees.
|
||||
|
||||
**What counts as drift:**
|
||||
- New directory outside mapped paths
|
||||
- New barrel export at `(packages|apps)/*/src/index.*`
|
||||
- New migration file (supabase/prisma/drizzle/src/migrations/…)
|
||||
- New route module under `routes/` or `api/`
|
||||
|
||||
**Non-blocking guarantee:** any internal failure (missing STRUCTURE.md,
|
||||
git errors, mapper spawn failure) logs a single line and the phase
|
||||
continues. Drift detection cannot fail verification.
|
||||
|
||||
**Requirements:**
|
||||
- REQ-DRIFT-01: System MUST detect the four drift categories from `git diff
|
||||
--name-status last_mapped_commit..HEAD`
|
||||
- REQ-DRIFT-02: Action fires only when element count ≥ `workflow.drift_threshold`
|
||||
- REQ-DRIFT-03: `warn` action MUST NOT spawn any agent
|
||||
- REQ-DRIFT-04: `auto-remap` action MUST pass sanitized `--paths` to the mapper
|
||||
- REQ-DRIFT-05: Detection/remap failure MUST be non-blocking for `/gsd:execute-phase`
|
||||
- REQ-DRIFT-06: `last_mapped_commit` round-trip through YAML frontmatter
|
||||
on each `.planning/codebase/*.md` file
|
||||
|
||||
---
|
||||
|
||||
## Utility Features
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"generated": "2026-04-20",
|
||||
"generated": "2026-04-23",
|
||||
"families": {
|
||||
"agents": [
|
||||
"gsd-advisor-researcher",
|
||||
@@ -103,6 +103,8 @@
|
||||
"/gsd-session-report",
|
||||
"/gsd-set-profile",
|
||||
"/gsd-settings",
|
||||
"/gsd-settings-advanced",
|
||||
"/gsd-settings-integrations",
|
||||
"/gsd-ship",
|
||||
"/gsd-sketch",
|
||||
"/gsd-sketch-wrap-up",
|
||||
@@ -110,11 +112,11 @@
|
||||
"/gsd-spike",
|
||||
"/gsd-spike-wrap-up",
|
||||
"/gsd-stats",
|
||||
"/gsd-sync-skills",
|
||||
"/gsd-thread",
|
||||
"/gsd-ui-phase",
|
||||
"/gsd-ui-review",
|
||||
"/gsd-ultraplan-phase",
|
||||
"/gsd-sync-skills",
|
||||
"/gsd-undo",
|
||||
"/gsd-update",
|
||||
"/gsd-validate-phase",
|
||||
@@ -185,6 +187,8 @@
|
||||
"scan.md",
|
||||
"secure-phase.md",
|
||||
"session-report.md",
|
||||
"settings-advanced.md",
|
||||
"settings-integrations.md",
|
||||
"settings.md",
|
||||
"ship.md",
|
||||
"sketch-wrap-up.md",
|
||||
@@ -238,6 +242,7 @@
|
||||
"project-skills-discovery.md",
|
||||
"questioning.md",
|
||||
"revision-loop.md",
|
||||
"scout-codebase.md",
|
||||
"sketch-interactivity.md",
|
||||
"sketch-theme-system.md",
|
||||
"sketch-tooling.md",
|
||||
@@ -263,8 +268,11 @@
|
||||
"config-schema.cjs",
|
||||
"config.cjs",
|
||||
"core.cjs",
|
||||
"decisions.cjs",
|
||||
"docs.cjs",
|
||||
"drift.cjs",
|
||||
"frontmatter.cjs",
|
||||
"gap-checker.cjs",
|
||||
"graphify.cjs",
|
||||
"gsd2-import.cjs",
|
||||
"init.cjs",
|
||||
@@ -277,6 +285,7 @@
|
||||
"profile-pipeline.cjs",
|
||||
"roadmap.cjs",
|
||||
"schema-detect.cjs",
|
||||
"secrets.cjs",
|
||||
"security.cjs",
|
||||
"state.cjs",
|
||||
"template.cjs",
|
||||
|
||||
@@ -54,7 +54,7 @@ Full roster at `agents/gsd-*.md`. The "Primary doc" column flags whether [`docs/
|
||||
|
||||
---
|
||||
|
||||
## Commands (83 shipped)
|
||||
## Commands (85 shipped)
|
||||
|
||||
Full roster at `commands/gsd/*.md`. The groupings below mirror `docs/COMMANDS.md` section order; each row carries the command name, a one-line role derived from the command's frontmatter `description:`, and a link to the source file. `tests/command-count-sync.test.cjs` locks the count against the filesystem.
|
||||
|
||||
@@ -163,6 +163,8 @@ Full roster at `commands/gsd/*.md`. The groupings below mirror `docs/COMMANDS.md
|
||||
| `/gsd-sketch-wrap-up` | Package sketch design findings into a persistent project skill for future build conversations. | [commands/gsd/sketch-wrap-up.md](../commands/gsd/sketch-wrap-up.md) |
|
||||
| `/gsd-profile-user` | Generate developer behavioral profile and Claude-discoverable artifacts. | [commands/gsd/profile-user.md](../commands/gsd/profile-user.md) |
|
||||
| `/gsd-settings` | Configure GSD workflow toggles and model profile. | [commands/gsd/settings.md](../commands/gsd/settings.md) |
|
||||
| `/gsd-settings-advanced` | Power-user configuration — plan bounce, timeouts, branch templates, cross-AI execution, runtime knobs. | [commands/gsd/settings-advanced.md](../commands/gsd/settings-advanced.md) |
|
||||
| `/gsd-settings-integrations` | Configure third-party API keys, code-review CLI routing, and agent-skill injection. | [commands/gsd/settings-integrations.md](../commands/gsd/settings-integrations.md) |
|
||||
| `/gsd-set-profile` | Switch model profile for GSD agents (quality/balanced/budget/inherit). | [commands/gsd/set-profile.md](../commands/gsd/set-profile.md) |
|
||||
| `/gsd-pr-branch` | Create a clean PR branch by filtering out `.planning/` commits. | [commands/gsd/pr-branch.md](../commands/gsd/pr-branch.md) |
|
||||
| `/gsd-sync-skills` | Sync managed GSD skill directories across runtime roots for multi-runtime users. | [commands/gsd/sync-skills.md](../commands/gsd/sync-skills.md) |
|
||||
@@ -173,7 +175,7 @@ Full roster at `commands/gsd/*.md`. The groupings below mirror `docs/COMMANDS.md
|
||||
|
||||
---
|
||||
|
||||
## Workflows (81 shipped)
|
||||
## Workflows (83 shipped)
|
||||
|
||||
Full roster at `get-shit-done/workflows/*.md`. Workflows are thin orchestrators that commands reference internally; most are not read directly by end users. Rows below map each workflow file to its role (derived from the `<purpose>` block) and, where applicable, to the command that invokes it.
|
||||
|
||||
@@ -243,6 +245,8 @@ Full roster at `get-shit-done/workflows/*.md`. Workflows are thin orchestrators
|
||||
| `secure-phase.md` | Retroactive threat-mitigation audit for a completed phase. | `/gsd-secure-phase` |
|
||||
| `session-report.md` | Session report — token usage, work summary, outcomes. | `/gsd-session-report` |
|
||||
| `settings.md` | Configure GSD workflow toggles and model profile. | `/gsd-settings`, `/gsd-set-profile` |
|
||||
| `settings-advanced.md` | Configure GSD power-user knobs — plan bounce, timeouts, branch templates, cross-AI execution, runtime knobs. | `/gsd-settings-advanced` |
|
||||
| `settings-integrations.md` | Configure third-party API keys (Brave/Firecrawl/Exa), `review.models.<cli>` CLI routing, and `agent_skills.<agent-type>` injection with masked (`****<last-4>`) display. | `/gsd-settings-integrations` |
|
||||
| `ship.md` | Create PR, run review, and prepare for merge after verification. | `/gsd-ship` |
|
||||
| `sketch.md` | Explore design directions through throwaway HTML mockups with 2-3 variants per sketch. | `/gsd-sketch` |
|
||||
| `sketch-wrap-up.md` | Curate sketch findings and package them as a persistent `sketch-findings-[project]` skill. | `/gsd-sketch-wrap-up` |
|
||||
@@ -265,7 +269,7 @@ Full roster at `get-shit-done/workflows/*.md`. Workflows are thin orchestrators
|
||||
|
||||
---
|
||||
|
||||
## References (50 shipped)
|
||||
## References (51 shipped)
|
||||
|
||||
Full roster at `get-shit-done/references/*.md`. References are shared knowledge documents that workflows and agents `@-reference`. The groupings below match [`docs/ARCHITECTURE.md`](ARCHITECTURE.md#references-get-shit-donereferencesmd) — core, workflow, thinking-model clusters, and the modular planner decomposition.
|
||||
|
||||
@@ -299,6 +303,7 @@ Full roster at `get-shit-done/references/*.md`. References are shared knowledge
|
||||
| `continuation-format.md` | Session continuation/resume format. |
|
||||
| `domain-probes.md` | Domain-specific probing questions for discuss-phase. |
|
||||
| `gate-prompts.md` | Gate/checkpoint prompt templates. |
|
||||
| `scout-codebase.md` | Phase-type→codebase-map selection table for discuss-phase scout step (extracted via #2551). |
|
||||
| `revision-loop.md` | Plan revision iteration patterns. |
|
||||
| `universal-anti-patterns.md` | Universal anti-patterns to detect and avoid. |
|
||||
| `artifact-types.md` | Planning artifact type definitions. |
|
||||
@@ -350,11 +355,11 @@ The `gsd-planner` agent is decomposed into a core agent plus reference modules t
|
||||
| `planner-revision.md` | Plan revision patterns for iterative refinement. |
|
||||
| `planner-source-audit.md` | Planner source-audit and authority-limit rules. |
|
||||
|
||||
> **Subdirectory:** `get-shit-done/references/few-shot-examples/` contains additional few-shot examples (`plan-checker.md`, `verifier.md`) that are referenced from specific agents. These are not counted in the 50 top-level references.
|
||||
> **Subdirectory:** `get-shit-done/references/few-shot-examples/` contains additional few-shot examples (`plan-checker.md`, `verifier.md`) that are referenced from specific agents. These are not counted in the 51 top-level references.
|
||||
|
||||
---
|
||||
|
||||
## CLI Modules (26 shipped)
|
||||
## CLI Modules (30 shipped)
|
||||
|
||||
Full listing: `get-shit-done/bin/lib/*.cjs`.
|
||||
|
||||
@@ -366,8 +371,11 @@ Full listing: `get-shit-done/bin/lib/*.cjs`.
|
||||
| `config-schema.cjs` | Single source of truth for `VALID_CONFIG_KEYS` and dynamic key patterns; imported by both the validator and the config-schema-docs parity test |
|
||||
| `config.cjs` | `config.json` read/write, section initialization; imports validator from `config-schema.cjs` |
|
||||
| `core.cjs` | Error handling, output formatting, shared utilities, runtime fallbacks |
|
||||
| `decisions.cjs` | Shared parser for CONTEXT.md `<decisions>` blocks (D-NN entries); used by `gap-checker.cjs` and intended for #2492 plan/verify decision gates |
|
||||
| `docs.cjs` | Docs-update workflow init, Markdown scanning, monorepo detection |
|
||||
| `drift.cjs` | Post-execute codebase structural drift detector (#2003): classifies file changes into new-dir/barrel/migration/route categories and round-trips `last_mapped_commit` frontmatter |
|
||||
| `frontmatter.cjs` | YAML frontmatter CRUD operations |
|
||||
| `gap-checker.cjs` | Post-planning gap analysis (#2493): unified REQUIREMENTS.md + CONTEXT.md decisions vs PLAN.md coverage report (`gsd-tools gap-analysis`) |
|
||||
| `graphify.cjs` | Knowledge-graph build/query/status/diff for `/gsd-graphify` |
|
||||
| `gsd2-import.cjs` | External-plan ingest for `/gsd-from-gsd2` |
|
||||
| `init.cjs` | Compound context loading for each workflow type |
|
||||
@@ -380,6 +388,7 @@ Full listing: `get-shit-done/bin/lib/*.cjs`.
|
||||
| `profile-pipeline.cjs` | User behavioral profiling data pipeline, session file scanning |
|
||||
| `roadmap.cjs` | ROADMAP.md parsing, phase extraction, plan progress |
|
||||
| `schema-detect.cjs` | Schema-drift detection for ORM patterns (Prisma, Drizzle, etc.) |
|
||||
| `secrets.cjs` | Secret-config masking convention (`****<last-4>`) for integration keys managed by `/gsd-settings-integrations` — keeps plaintext out of `config-set` output |
|
||||
| `security.cjs` | Path traversal prevention, prompt injection detection, safe JSON/shell helpers |
|
||||
| `state.cjs` | STATE.md parsing, updating, progression, metrics |
|
||||
| `template.cjs` | Template selection and filling with variable substitution |
|
||||
|
||||
@@ -179,6 +179,47 @@ By default, `/gsd-discuss-phase` asks open-ended questions about your implementa
|
||||
|
||||
See [docs/workflow-discuss-mode.md](workflow-discuss-mode.md) for the full discuss-mode reference.
|
||||
|
||||
### Decision Coverage Gates
|
||||
|
||||
The discuss-phase captures implementation decisions in CONTEXT.md under a
|
||||
`<decisions>` block as numbered bullets (`- **D-01:** …`). Two gates — added
|
||||
for issue #2492 — ensure those decisions survive into plans and shipped
|
||||
code.
|
||||
|
||||
**Plan-phase translation gate (blocking).** After planning, GSD refuses to
|
||||
mark the phase planned until every trackable decision appears in at least
|
||||
one plan's `must_haves`, `truths`, or body. The gate names each missed
|
||||
decision by id (`D-07: …`) so you know exactly what to add, move, or
|
||||
reclassify.
|
||||
|
||||
**Verify-phase validation gate (non-blocking).** During verification, GSD
|
||||
searches plans, SUMMARY.md, modified files, and recent commit messages for
|
||||
each trackable decision. Misses are logged to VERIFICATION.md as a warning
|
||||
section; verification status is unchanged. The asymmetry is deliberate —
|
||||
the blocking gate is cheap at plan time but hostile at verify time.
|
||||
|
||||
**Writing decisions the gate can match.** Two match modes:
|
||||
|
||||
1. **Strict id match (recommended).** Cite the decision id anywhere in a
|
||||
plan that implements it — `must_haves.truths: ["D-12: bit offsets
|
||||
exposed"]`, a bullet in the plan body, a frontmatter comment. This is
|
||||
deterministic and unambiguous.
|
||||
2. **Soft phrase match (fallback).** If a 6+-word slice of the decision
|
||||
text appears verbatim in any plan or shipped artifact, it counts. This
|
||||
forgives paraphrasing but is less reliable.
|
||||
|
||||
**Opting a decision out.** If a decision genuinely should not be tracked —
|
||||
an implementation-discretion note, an informational capture, a decision
|
||||
already deferred — mark it one of these ways:
|
||||
|
||||
- Move it under the `### Claude's Discretion` heading inside `<decisions>`.
|
||||
- Tag it in its bullet: `- **D-08 [informational]:** …`,
|
||||
`- **D-09 [folded]:** …`, `- **D-10 [deferred]:** …`.
|
||||
|
||||
**Disabling the gates.** Set
|
||||
`workflow.context_coverage_gate: false` in `.planning/config.json` (or via
|
||||
`/gsd-settings`) to skip both gates silently. Default is `true`.
|
||||
|
||||
---
|
||||
|
||||
## UI Design Contract
|
||||
@@ -585,6 +626,20 @@ claude --dangerously-skip-permissions
|
||||
# (normal phase workflow from here)
|
||||
```
|
||||
|
||||
**Post-execute drift detection (#2003).** After every `/gsd:execute-phase`,
|
||||
GSD checks whether the phase introduced enough structural change
|
||||
(new directories, barrel exports, migrations, or route modules) to make
|
||||
`.planning/codebase/STRUCTURE.md` stale. If it did, the default behavior is
|
||||
to print a one-shot warning suggesting the exact `/gsd:map-codebase --paths …`
|
||||
invocation to refresh just the affected subtrees. Flip the behavior with:
|
||||
|
||||
```bash
|
||||
/gsd:settings workflow.drift_action auto-remap # remap automatically
|
||||
/gsd:settings workflow.drift_threshold 5 # tune sensitivity
|
||||
```
|
||||
|
||||
The gate is non-blocking: any internal failure logs and the phase continues.
|
||||
|
||||
### Quick Bug Fix
|
||||
|
||||
```bash
|
||||
@@ -740,6 +795,19 @@ To assign different models to different agents on a non-Claude runtime, add `mod
|
||||
|
||||
The installer auto-configures `resolve_model_ids: "omit"` for Gemini CLI, OpenCode, Kilo, and Codex. If you're manually setting up a non-Claude runtime, add it to `.planning/config.json` yourself.
|
||||
|
||||
#### Switching from Claude to Codex with one config change (#2517)
|
||||
|
||||
If you want tiered models on Codex without writing a large `model_overrides` block, set `runtime: "codex"` and pick a profile:
|
||||
|
||||
```json
|
||||
{
|
||||
"runtime": "codex",
|
||||
"model_profile": "balanced"
|
||||
}
|
||||
```
|
||||
|
||||
GSD will resolve each agent's tier (`opus`/`sonnet`/`haiku`) to the Codex-native model and reasoning effort defined in the runtime tier map (`gpt-5.4` xhigh / `gpt-5.3-codex` medium / `gpt-5.4-mini` medium). The Codex installer embeds both `model` and `model_reasoning_effort` into each agent's TOML automatically. To override a single tier, add `model_profile_overrides.codex.<tier>`. See [Runtime-Aware Profiles](CONFIGURATION.md#runtime-aware-profiles-2517).
|
||||
|
||||
See the [Configuration Reference](CONFIGURATION.md#non-claude-runtimes-codex-opencode-gemini-cli-kilo) for the full explanation.
|
||||
|
||||
### Installing for Cline
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
* verify artifacts <plan-file> Check must_haves.artifacts
|
||||
* verify key-links <plan-file> Check must_haves.key_links
|
||||
* verify schema-drift <phase> [--skip] Detect schema file changes without push
|
||||
* verify codebase-drift Detect structural drift since last codebase map (#2003)
|
||||
*
|
||||
* Template Fill:
|
||||
* template fill summary --phase N Create pre-filled SUMMARY.md
|
||||
@@ -187,6 +188,7 @@ const profileOutput = require('./lib/profile-output.cjs');
|
||||
const workstream = require('./lib/workstream.cjs');
|
||||
const docs = require('./lib/docs.cjs');
|
||||
const learnings = require('./lib/learnings.cjs');
|
||||
const gapChecker = require('./lib/gap-checker.cjs');
|
||||
|
||||
// ─── Arg parsing helpers ──────────────────────────────────────────────────────
|
||||
|
||||
@@ -481,6 +483,12 @@ async function runCommand(command, args, cwd, raw, defaultValue) {
|
||||
} else if (subcommand === 'prune') {
|
||||
const { 'keep-recent': keepRecent, 'dry-run': dryRun } = parseNamedArgs(args, ['keep-recent'], ['dry-run']);
|
||||
state.cmdStatePrune(cwd, { keepRecent: keepRecent || '3', dryRun: !!dryRun }, raw);
|
||||
} else if (subcommand === 'milestone-switch') {
|
||||
// Bug #2630: reset STATE.md frontmatter + Current Position for new milestone.
|
||||
// NB: the flag is `--milestone`, not `--version` — gsd-tools reserves
|
||||
// `--version` as a globally-invalid help flag (see NEVER_VALID_FLAGS above).
|
||||
const { milestone, name } = parseNamedArgs(args, ['milestone', 'name']);
|
||||
state.cmdStateMilestoneSwitch(cwd, milestone, name, raw);
|
||||
} else {
|
||||
state.cmdStateLoad(cwd, raw);
|
||||
}
|
||||
@@ -593,8 +601,10 @@ async function runCommand(command, args, cwd, raw, defaultValue) {
|
||||
} else if (subcommand === 'schema-drift') {
|
||||
const skipFlag = args.includes('--skip');
|
||||
verify.cmdVerifySchemaDrift(cwd, args[2], skipFlag, raw);
|
||||
} else if (subcommand === 'codebase-drift') {
|
||||
verify.cmdVerifyCodebaseDrift(cwd, raw);
|
||||
} else {
|
||||
error('Unknown verify subcommand. Available: plan-structure, phase-completeness, references, commits, artifacts, key-links, schema-drift');
|
||||
error('Unknown verify subcommand. Available: plan-structure, phase-completeness, references, commits, artifacts, key-links, schema-drift, codebase-drift');
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -709,6 +719,13 @@ async function runCommand(command, args, cwd, raw, defaultValue) {
|
||||
break;
|
||||
}
|
||||
|
||||
case 'gap-analysis': {
|
||||
// Post-planning gap checker (#2493) — unified REQUIREMENTS.md +
|
||||
// CONTEXT.md <decisions> coverage report against PLAN.md files.
|
||||
gapChecker.cmdGapAnalysis(cwd, args.slice(1), raw);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'phase': {
|
||||
const subcommand = args[1];
|
||||
if (subcommand === 'next-decimal') {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Scans all .planning/ artifact categories for items with open/unresolved state.
|
||||
* Returns structured JSON for workflow consumption.
|
||||
* Called by: gsd-tools.cjs audit-open
|
||||
* Used by: /gsd:complete-milestone pre-close gate
|
||||
* Used by: /gsd-complete-milestone pre-close gate
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
@@ -773,7 +773,7 @@ function cmdScaffold(cwd, type, options, raw) {
|
||||
switch (type) {
|
||||
case 'context': {
|
||||
filePath = path.join(phaseDir, `${padded}-CONTEXT.md`);
|
||||
content = `---\nphase: "${padded}"\nname: "${name || phaseInfo?.phase_name || 'Unnamed'}"\ncreated: ${today}\n---\n\n# Phase ${phase}: ${name || phaseInfo?.phase_name || 'Unnamed'} — Context\n\n## Decisions\n\n_Decisions will be captured during /gsd:discuss-phase ${phase}_\n\n## Discretion Areas\n\n_Areas where the executor can use judgment_\n\n## Deferred Ideas\n\n_Ideas to consider later_\n`;
|
||||
content = `---\nphase: "${padded}"\nname: "${name || phaseInfo?.phase_name || 'Unnamed'}"\ncreated: ${today}\n---\n\n# Phase ${phase}: ${name || phaseInfo?.phase_name || 'Unnamed'} — Context\n\n## Decisions\n\n_Decisions will be captured during /gsd-discuss-phase ${phase}_\n\n## Discretion Areas\n\n_Areas where the executor can use judgment_\n\n## Deferred Ideas\n\n_Ideas to consider later_\n`;
|
||||
break;
|
||||
}
|
||||
case 'uat': {
|
||||
|
||||
@@ -34,9 +34,12 @@ const VALID_CONFIG_KEYS = new Set([
|
||||
'workflow.plan_bounce_script',
|
||||
'workflow.plan_bounce_passes',
|
||||
'workflow.plan_chunked',
|
||||
'workflow.post_planning_gaps',
|
||||
'workflow.security_enforcement',
|
||||
'workflow.security_asvs_level',
|
||||
'workflow.security_block_on',
|
||||
'workflow.drift_threshold',
|
||||
'workflow.drift_action',
|
||||
'git.branching_strategy', 'git.base_branch', 'git.phase_branch_template', 'git.milestone_branch_template', 'git.quick_branch_template',
|
||||
'planning.commit_docs', 'planning.search_gitignored', 'planning.sub_repos',
|
||||
'workflow.cross_ai_execution', 'workflow.cross_ai_command', 'workflow.cross_ai_timeout',
|
||||
@@ -44,6 +47,7 @@ const VALID_CONFIG_KEYS = new Set([
|
||||
'workflow.inline_plan_threshold',
|
||||
'hooks.context_warnings',
|
||||
'hooks.workflow_guard',
|
||||
'workflow.context_coverage_gate',
|
||||
'statusline.show_last_command',
|
||||
'workflow.ui_review',
|
||||
'workflow.max_discuss_passes',
|
||||
@@ -54,11 +58,14 @@ const VALID_CONFIG_KEYS = new Set([
|
||||
'project_code', 'phase_naming',
|
||||
'manager.flags.discuss', 'manager.flags.plan', 'manager.flags.execute',
|
||||
'response_language',
|
||||
'context_window',
|
||||
'intel.enabled',
|
||||
'graphify.enabled',
|
||||
'graphify.build_timeout',
|
||||
'claude_md_path',
|
||||
'claude_md_assembly.mode',
|
||||
// #2517 — runtime-aware model profiles
|
||||
'runtime',
|
||||
]);
|
||||
|
||||
/**
|
||||
@@ -70,6 +77,10 @@ const DYNAMIC_KEY_PATTERNS = [
|
||||
{ test: (k) => /^review\.models\.[a-zA-Z0-9_-]+$/.test(k), description: 'review.models.<cli-name>' },
|
||||
{ test: (k) => /^features\.[a-zA-Z0-9_]+$/.test(k), description: 'features.<feature_name>' },
|
||||
{ test: (k) => /^claude_md_assembly\.blocks\.[a-zA-Z0-9_]+$/.test(k), description: 'claude_md_assembly.blocks.<section>' },
|
||||
// #2517 — runtime-aware model profile overrides: model_profile_overrides.<runtime>.<tier>
|
||||
// <runtime> is a free string (so users can map non-built-in runtimes); <tier> is enum-restricted.
|
||||
{ test: (k) => /^model_profile_overrides\.[a-zA-Z0-9_-]+\.(opus|sonnet|haiku)$/.test(k),
|
||||
description: 'model_profile_overrides.<runtime>.<opus|sonnet|haiku>' },
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,6 +11,7 @@ const {
|
||||
formatAgentToModelMapAsTable,
|
||||
} = require('./model-profiles.cjs');
|
||||
const { VALID_CONFIG_KEYS, isValidConfigKey } = require('./config-schema.cjs');
|
||||
const { isSecretKey, maskSecret } = require('./secrets.cjs');
|
||||
|
||||
const CONFIG_KEY_SUGGESTIONS = {
|
||||
'workflow.nyquist_validation_enabled': 'workflow.nyquist_validation',
|
||||
@@ -41,7 +42,7 @@ function validateKnownConfigKeyPath(keyPath) {
|
||||
* Merges (increasing priority):
|
||||
* 1. Hardcoded defaults — every key that loadConfig() resolves, plus mode/granularity
|
||||
* 2. User-level defaults from ~/.gsd/defaults.json (if present)
|
||||
* 3. userChoices — the settings the user explicitly selected during /gsd:new-project
|
||||
* 3. userChoices — the settings the user explicitly selected during /gsd-new-project
|
||||
*
|
||||
* Uses the canonical `git` namespace for branching keys (consistent with VALID_CONFIG_KEYS
|
||||
* and the settings workflow). loadConfig() handles both flat and nested formats, so this
|
||||
@@ -119,6 +120,7 @@ function buildNewProjectConfig(userChoices) {
|
||||
plan_bounce_script: null,
|
||||
plan_bounce_passes: 2,
|
||||
auto_prune_state: false,
|
||||
post_planning_gaps: CONFIG_DEFAULTS.post_planning_gaps,
|
||||
security_enforcement: CONFIG_DEFAULTS.security_enforcement,
|
||||
security_asvs_level: CONFIG_DEFAULTS.security_asvs_level,
|
||||
security_block_on: CONFIG_DEFAULTS.security_block_on,
|
||||
@@ -164,7 +166,7 @@ function buildNewProjectConfig(userChoices) {
|
||||
* Command: create a fully-materialized .planning/config.json for a new project.
|
||||
*
|
||||
* Accepts user-chosen settings as a JSON string (the keys the user explicitly
|
||||
* configured during /gsd:new-project). All remaining keys are filled from
|
||||
* configured during /gsd-new-project). All remaining keys are filled from
|
||||
* hardcoded defaults and optional ~/.gsd/defaults.json.
|
||||
*
|
||||
* Idempotent: if config.json already exists, returns { created: false }.
|
||||
@@ -333,7 +335,44 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
|
||||
error(`Invalid context value '${value}'. Valid values: ${VALID_CONTEXT_VALUES.join(', ')}`);
|
||||
}
|
||||
|
||||
// Codebase drift detector (#2003)
|
||||
const VALID_DRIFT_ACTIONS = ['warn', 'auto-remap'];
|
||||
if (keyPath === 'workflow.drift_action' && !VALID_DRIFT_ACTIONS.includes(String(parsedValue))) {
|
||||
error(`Invalid workflow.drift_action '${value}'. Valid values: ${VALID_DRIFT_ACTIONS.join(', ')}`);
|
||||
}
|
||||
if (keyPath === 'workflow.drift_threshold') {
|
||||
if (typeof parsedValue !== 'number' || !Number.isInteger(parsedValue) || parsedValue < 1) {
|
||||
error(`Invalid workflow.drift_threshold '${value}'. Must be a positive integer.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Post-planning gap checker (#2493)
|
||||
if (keyPath === 'workflow.post_planning_gaps') {
|
||||
if (typeof parsedValue !== 'boolean') {
|
||||
error(`Invalid workflow.post_planning_gaps '${value}'. Must be a boolean (true or false).`);
|
||||
}
|
||||
}
|
||||
|
||||
const setConfigValueResult = setConfigValue(cwd, keyPath, parsedValue);
|
||||
|
||||
// Mask secrets in both JSON and text output. The plaintext is written
|
||||
// to config.json (that's where secrets live on disk); the CLI output
|
||||
// must never echo it. See lib/secrets.cjs.
|
||||
if (isSecretKey(keyPath)) {
|
||||
const masked = maskSecret(parsedValue);
|
||||
const maskedPrev = setConfigValueResult.previousValue === undefined
|
||||
? undefined
|
||||
: maskSecret(setConfigValueResult.previousValue);
|
||||
const maskedResult = {
|
||||
...setConfigValueResult,
|
||||
value: masked,
|
||||
previousValue: maskedPrev,
|
||||
masked: true,
|
||||
};
|
||||
output(maskedResult, raw, `${keyPath}=${masked}`);
|
||||
return;
|
||||
}
|
||||
|
||||
output(setConfigValueResult, raw, `${keyPath}=${parsedValue}`);
|
||||
}
|
||||
|
||||
@@ -376,6 +415,14 @@ function cmdConfigGet(cwd, keyPath, raw, defaultValue) {
|
||||
error(`Key not found: ${keyPath}`);
|
||||
}
|
||||
|
||||
// Never echo plaintext for sensitive keys via config-get. Plaintext lives
|
||||
// in config.json on disk; the CLI surface always shows the masked form.
|
||||
if (isSecretKey(keyPath)) {
|
||||
const masked = maskSecret(current);
|
||||
output(masked, raw, masked);
|
||||
return;
|
||||
}
|
||||
|
||||
output(current, raw, String(current));
|
||||
}
|
||||
|
||||
|
||||
@@ -263,9 +263,10 @@ const CONFIG_DEFAULTS = {
|
||||
phase_naming: 'sequential', // 'sequential' (default, auto-increment) or 'custom' (arbitrary string IDs)
|
||||
project_code: null, // optional short prefix for phase dirs (e.g., 'CK' → 'CK-01-foundation')
|
||||
subagent_timeout: 300000, // 5 min default; increase for large codebases or slower models (ms)
|
||||
security_enforcement: true, // workflow.security_enforcement — threat-model-anchored security verification via /gsd:secure-phase
|
||||
security_enforcement: true, // workflow.security_enforcement — threat-model-anchored security verification via /gsd-secure-phase
|
||||
security_asvs_level: 1, // workflow.security_asvs_level — OWASP ASVS verification level (1=opportunistic, 2=standard, 3=comprehensive)
|
||||
security_block_on: 'high', // workflow.security_block_on — minimum severity that blocks phase advancement ('high' | 'medium' | 'low')
|
||||
post_planning_gaps: true, // workflow.post_planning_gaps — unified post-planning gap report (#2493): scan REQUIREMENTS.md + CONTEXT.md decisions vs all PLAN.md files
|
||||
};
|
||||
|
||||
function loadConfig(cwd) {
|
||||
@@ -287,26 +288,40 @@ function loadConfig(cwd) {
|
||||
// Auto-detect and sync sub_repos: scan for child directories with .git
|
||||
let configDirty = false;
|
||||
|
||||
// Migrate legacy "multiRepo: true" boolean → sub_repos array
|
||||
// Migrate legacy "multiRepo: true" boolean → planning.sub_repos array.
|
||||
// Canonical location is planning.sub_repos (#2561); writing to top-level
|
||||
// would be flagged as unknown by the validator below (#2638).
|
||||
if (parsed.multiRepo === true && !parsed.sub_repos && !parsed.planning?.sub_repos) {
|
||||
const detected = detectSubRepos(cwd);
|
||||
if (detected.length > 0) {
|
||||
parsed.sub_repos = detected;
|
||||
if (!parsed.planning) parsed.planning = {};
|
||||
parsed.planning.sub_repos = detected;
|
||||
parsed.planning.commit_docs = false;
|
||||
delete parsed.multiRepo;
|
||||
configDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Keep sub_repos in sync with actual filesystem
|
||||
const currentSubRepos = parsed.sub_repos || parsed.planning?.sub_repos || [];
|
||||
// Self-heal legacy/buggy installs: strip any stale top-level sub_repos,
|
||||
// preserving its value as the planning.sub_repos seed if that slot is empty.
|
||||
if (Object.prototype.hasOwnProperty.call(parsed, 'sub_repos')) {
|
||||
if (!parsed.planning) parsed.planning = {};
|
||||
if (!parsed.planning.sub_repos) {
|
||||
parsed.planning.sub_repos = parsed.sub_repos;
|
||||
}
|
||||
delete parsed.sub_repos;
|
||||
configDirty = true;
|
||||
}
|
||||
|
||||
// Keep planning.sub_repos in sync with actual filesystem
|
||||
const currentSubRepos = parsed.planning?.sub_repos || [];
|
||||
if (Array.isArray(currentSubRepos) && currentSubRepos.length > 0) {
|
||||
const detected = detectSubRepos(cwd);
|
||||
if (detected.length > 0) {
|
||||
const sorted = [...currentSubRepos].sort();
|
||||
if (JSON.stringify(sorted) !== JSON.stringify(detected)) {
|
||||
parsed.sub_repos = detected;
|
||||
if (!parsed.planning) parsed.planning = {};
|
||||
parsed.planning.sub_repos = detected;
|
||||
configDirty = true;
|
||||
}
|
||||
}
|
||||
@@ -339,6 +354,13 @@ function loadConfig(cwd) {
|
||||
);
|
||||
}
|
||||
|
||||
// #2517 — Validate runtime/tier values for keys that loadConfig handles but
|
||||
// can be edited directly into config.json (bypassing config-set's enum check).
|
||||
// This catches typos like `runtime: "codx"` and `model_profile_overrides.codex.banana`
|
||||
// at read time without rejecting back-compat values from new runtimes
|
||||
// (review findings #10, #13).
|
||||
_warnUnknownProfileOverrides(parsed, '.planning/config.json');
|
||||
|
||||
const get = (key, nested) => {
|
||||
if (parsed[key] !== undefined) return parsed[key];
|
||||
if (nested && parsed[nested.section] && parsed[nested.section][nested.field] !== undefined) {
|
||||
@@ -374,6 +396,7 @@ function loadConfig(cwd) {
|
||||
plan_checker: get('plan_checker', { section: 'workflow', field: 'plan_check' }) ?? defaults.plan_checker,
|
||||
verifier: get('verifier', { section: 'workflow', field: 'verifier' }) ?? defaults.verifier,
|
||||
nyquist_validation: get('nyquist_validation', { section: 'workflow', field: 'nyquist_validation' }) ?? defaults.nyquist_validation,
|
||||
post_planning_gaps: get('post_planning_gaps', { section: 'workflow', field: 'post_planning_gaps' }) ?? defaults.post_planning_gaps,
|
||||
parallelization,
|
||||
brave_search: get('brave_search') ?? defaults.brave_search,
|
||||
firecrawl: get('firecrawl') ?? defaults.firecrawl,
|
||||
@@ -390,6 +413,18 @@ function loadConfig(cwd) {
|
||||
project_code: get('project_code') ?? defaults.project_code,
|
||||
subagent_timeout: get('subagent_timeout', { section: 'workflow', field: 'subagent_timeout' }) ?? defaults.subagent_timeout,
|
||||
model_overrides: parsed.model_overrides || null,
|
||||
// #2517 — runtime-aware profiles. `runtime` defaults to null (back-compat).
|
||||
// When null, resolveModelInternal preserves today's Claude-native behavior.
|
||||
// NOTE: `runtime` and `model_profile_overrides` are intentionally read
|
||||
// flat-only (not via `get()` with a workflow.X fallback) — they are
|
||||
// top-level keys per docs/CONFIGURATION.md. The lighter-touch decision
|
||||
// here was to document the constraint rather than introduce nested
|
||||
// resolution edge cases for two new keys (review finding #9). The
|
||||
// schema validation in `_warnUnknownProfileOverrides` runs against the
|
||||
// raw `parsed` blob, so direct `.planning/config.json` edits surface
|
||||
// unknown runtime/tier names at load time, not silently (review finding #10).
|
||||
runtime: parsed.runtime || null,
|
||||
model_profile_overrides: parsed.model_profile_overrides || null,
|
||||
agent_skills: parsed.agent_skills || {},
|
||||
manager: parsed.manager || {},
|
||||
response_language: get('response_language') || null,
|
||||
@@ -415,6 +450,9 @@ function loadConfig(cwd) {
|
||||
plan_checker: globalDefaults.plan_checker ?? defaults.plan_checker,
|
||||
verifier: globalDefaults.verifier ?? defaults.verifier,
|
||||
nyquist_validation: globalDefaults.nyquist_validation ?? defaults.nyquist_validation,
|
||||
post_planning_gaps: globalDefaults.post_planning_gaps
|
||||
?? globalDefaults.workflow?.post_planning_gaps
|
||||
?? defaults.post_planning_gaps,
|
||||
parallelization: globalDefaults.parallelization ?? defaults.parallelization,
|
||||
text_mode: globalDefaults.text_mode ?? defaults.text_mode,
|
||||
resolve_model_ids: globalDefaults.resolve_model_ids ?? defaults.resolve_model_ids,
|
||||
@@ -1285,8 +1323,11 @@ function extractCurrentMilestone(content, cwd) {
|
||||
// Milestone headings look like: ## v2.0, ## Roadmap v2.0, ## ✅ v1.0, etc.
|
||||
const headingLevel = sectionMatch[1].match(/^(#{1,3})\s/)[1].length;
|
||||
const restContent = content.slice(sectionStart + sectionMatch[0].length);
|
||||
// Exclude phase headings (e.g. "### Phase 12: v1.0 Tech-Debt Closure") from
|
||||
// being treated as milestone boundaries just because they mention vX.Y in
|
||||
// the title. Phase headings always start with the literal `Phase `. See #2619.
|
||||
const nextMilestonePattern = new RegExp(
|
||||
`^#{1,${headingLevel}}\\s+(?:.*v\\d+\\.\\d+|✅|📋|🚧)`,
|
||||
`^#{1,${headingLevel}}\\s+(?!Phase\\s+\\S)(?:.*v\\d+\\.\\d+|✅|📋|🚧)`,
|
||||
'mi'
|
||||
);
|
||||
const nextMatch = restContent.match(nextMilestonePattern);
|
||||
@@ -1449,32 +1490,220 @@ const MODEL_ALIAS_MAP = {
|
||||
'haiku': 'claude-haiku-4-5',
|
||||
};
|
||||
|
||||
/**
|
||||
* #2517 — runtime-aware tier resolution.
|
||||
* Maps `model_profile` tiers (opus/sonnet/haiku) to runtime-native model IDs and
|
||||
* (where supported) reasoning_effort settings.
|
||||
*
|
||||
* Each entry: { model: <id>, reasoning_effort?: <level> }
|
||||
*
|
||||
* `claude` mirrors MODEL_ALIAS_MAP — present for symmetry so `runtime: "claude"`
|
||||
* resolves through the same code path. `codex` defaults are taken from the spec
|
||||
* in #2517. Unknown runtimes fall back to the Claude alias to avoid emitting
|
||||
* provider-specific IDs the runtime cannot accept.
|
||||
*/
|
||||
const RUNTIME_PROFILE_MAP = {
|
||||
claude: {
|
||||
opus: { model: 'claude-opus-4-6' },
|
||||
sonnet: { model: 'claude-sonnet-4-6' },
|
||||
haiku: { model: 'claude-haiku-4-5' },
|
||||
},
|
||||
codex: {
|
||||
opus: { model: 'gpt-5.4', reasoning_effort: 'xhigh' },
|
||||
sonnet: { model: 'gpt-5.3-codex', reasoning_effort: 'medium' },
|
||||
haiku: { model: 'gpt-5.4-mini', reasoning_effort: 'medium' },
|
||||
},
|
||||
};
|
||||
|
||||
const RUNTIMES_WITH_REASONING_EFFORT = new Set(['codex']);
|
||||
|
||||
/**
|
||||
* Tier enum allowed under `model_profile_overrides[runtime][tier]`. Mirrors the
|
||||
* regex in `config-schema.cjs` (DYNAMIC_KEY_PATTERNS) so loadConfig surfaces the
|
||||
* same constraint at read time, not only at config-set time (review finding #10).
|
||||
*/
|
||||
const RUNTIME_OVERRIDE_TIERS = new Set(['opus', 'sonnet', 'haiku']);
|
||||
|
||||
/**
|
||||
* Allowlist of runtime names the install pipeline currently knows how to emit
|
||||
* native model IDs for. Synced with `getDirName` in `bin/install.js` and the
|
||||
* runtime list in `docs/CONFIGURATION.md`. Free-string runtimes outside this
|
||||
* set are still accepted (#2517 deliberately leaves the runtime field open) —
|
||||
* a warning fires once at loadConfig so a typo like `runtime: "codx"` does not
|
||||
* silently fall back to Claude defaults (review findings #10, #13).
|
||||
*/
|
||||
const KNOWN_RUNTIMES = new Set([
|
||||
'claude', 'codex', 'opencode', 'kilo', 'gemini', 'qwen',
|
||||
'copilot', 'cursor', 'windsurf', 'augment', 'trae', 'codebuddy',
|
||||
'antigravity', 'cline',
|
||||
]);
|
||||
|
||||
const _warnedConfigKeys = new Set();
|
||||
/**
|
||||
* Emit a one-time stderr warning for unknown runtime/tier keys in a parsed
|
||||
* config blob. Idempotent across calls — the same (file, key) pair only warns
|
||||
* once per process so loadConfig can be called repeatedly without spamming.
|
||||
*
|
||||
* Does NOT reject — preserves back-compat for users on a runtime not yet in the
|
||||
* allowlist (the new-runtime case must always be possible without code changes).
|
||||
*/
|
||||
function _warnUnknownProfileOverrides(parsed, configLabel) {
|
||||
if (!parsed || typeof parsed !== 'object') return;
|
||||
|
||||
const runtime = parsed.runtime;
|
||||
if (runtime && typeof runtime === 'string' && !KNOWN_RUNTIMES.has(runtime)) {
|
||||
const key = `${configLabel}::runtime::${runtime}`;
|
||||
if (!_warnedConfigKeys.has(key)) {
|
||||
_warnedConfigKeys.add(key);
|
||||
try {
|
||||
process.stderr.write(
|
||||
`gsd: warning — config key "runtime" has unknown value "${runtime}". ` +
|
||||
`Known runtimes: ${[...KNOWN_RUNTIMES].sort().join(', ')}. ` +
|
||||
`Resolution will fall back to safe defaults. (#2517)\n`
|
||||
);
|
||||
} catch { /* stderr might be closed in some test harnesses */ }
|
||||
}
|
||||
}
|
||||
|
||||
const overrides = parsed.model_profile_overrides;
|
||||
if (!overrides || typeof overrides !== 'object') return;
|
||||
for (const [overrideRuntime, tierMap] of Object.entries(overrides)) {
|
||||
if (!KNOWN_RUNTIMES.has(overrideRuntime)) {
|
||||
const key = `${configLabel}::override-runtime::${overrideRuntime}`;
|
||||
if (!_warnedConfigKeys.has(key)) {
|
||||
_warnedConfigKeys.add(key);
|
||||
try {
|
||||
process.stderr.write(
|
||||
`gsd: warning — model_profile_overrides.${overrideRuntime}.* uses ` +
|
||||
`unknown runtime "${overrideRuntime}". Known runtimes: ` +
|
||||
`${[...KNOWN_RUNTIMES].sort().join(', ')}. (#2517)\n`
|
||||
);
|
||||
} catch { /* ok */ }
|
||||
}
|
||||
}
|
||||
if (!tierMap || typeof tierMap !== 'object') continue;
|
||||
for (const tierName of Object.keys(tierMap)) {
|
||||
if (!RUNTIME_OVERRIDE_TIERS.has(tierName)) {
|
||||
const key = `${configLabel}::override-tier::${overrideRuntime}.${tierName}`;
|
||||
if (!_warnedConfigKeys.has(key)) {
|
||||
_warnedConfigKeys.add(key);
|
||||
try {
|
||||
process.stderr.write(
|
||||
`gsd: warning — model_profile_overrides.${overrideRuntime}.${tierName} ` +
|
||||
`uses unknown tier "${tierName}". Allowed tiers: opus, sonnet, haiku. (#2517)\n`
|
||||
);
|
||||
} catch { /* ok */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Internal helper exposed for tests so per-process warning state can be reset
|
||||
// between cases that intentionally exercise the warning path repeatedly.
|
||||
function _resetRuntimeWarningCacheForTests() {
|
||||
_warnedConfigKeys.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* #2517 — Resolve the runtime-aware tier entry for (runtime, tier).
|
||||
*
|
||||
* Single source of truth shared by core.cjs (resolveModelInternal /
|
||||
* resolveReasoningEffortInternal) and bin/install.js (Codex/OpenCode TOML emit
|
||||
* paths). Always merges built-in defaults with user overrides at the field
|
||||
* level so partial overrides keep the unspecified fields:
|
||||
*
|
||||
* `{ codex: { opus: "gpt-5-pro" } }` keeps reasoning_effort: 'xhigh'
|
||||
* `{ codex: { opus: { reasoning_effort: 'low' } } }` keeps model: 'gpt-5.4'
|
||||
*
|
||||
* Without this field-merge, the documented string-shorthand example silently
|
||||
* dropped reasoning_effort and a partial-object override silently dropped the
|
||||
* model — both reported as critical findings in the #2609 review.
|
||||
*
|
||||
* Inputs:
|
||||
* - runtime: string (e.g. 'codex', 'claude', 'opencode')
|
||||
* - tier: 'opus' | 'sonnet' | 'haiku'
|
||||
* - overrides: optional `model_profile_overrides` blob (may be null/undefined)
|
||||
*
|
||||
* Returns `{ model: string, reasoning_effort?: string } | null`.
|
||||
*/
|
||||
function resolveTierEntry({ runtime, tier, overrides }) {
|
||||
if (!runtime || !tier) return null;
|
||||
|
||||
const builtin = RUNTIME_PROFILE_MAP[runtime]?.[tier] || null;
|
||||
const userRaw = overrides?.[runtime]?.[tier];
|
||||
|
||||
// String shorthand from CONFIGURATION.md examples — `{ codex: { opus: "gpt-5-pro" } }`.
|
||||
// Treat as `{ model: "gpt-5-pro" }` so the field-merge below still preserves
|
||||
// reasoning_effort from the built-in defaults.
|
||||
let userEntry = null;
|
||||
if (userRaw) {
|
||||
userEntry = typeof userRaw === 'string' ? { model: userRaw } : userRaw;
|
||||
}
|
||||
|
||||
if (!builtin && !userEntry) return null;
|
||||
// Field-merge: user fields win, built-in fills the gaps.
|
||||
return { ...(builtin || {}), ...(userEntry || {}) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience wrapper used by resolveModelInternal / resolveReasoningEffortInternal.
|
||||
* Pulls runtime + overrides out of a loaded config and delegates to resolveTierEntry.
|
||||
*/
|
||||
function _resolveRuntimeTier(config, tier) {
|
||||
return resolveTierEntry({
|
||||
runtime: config.runtime,
|
||||
tier,
|
||||
overrides: config.model_profile_overrides,
|
||||
});
|
||||
}
|
||||
|
||||
function resolveModelInternal(cwd, agentType) {
|
||||
const config = loadConfig(cwd);
|
||||
|
||||
// Check per-agent override first — always respected regardless of resolve_model_ids.
|
||||
// 1. Per-agent override — always respected; highest precedence.
|
||||
// Users who set fully-qualified model IDs (e.g., "openai/gpt-5.4") get exactly that.
|
||||
const override = config.model_overrides?.[agentType];
|
||||
if (override) {
|
||||
return override;
|
||||
}
|
||||
|
||||
// resolve_model_ids: "omit" — return empty string so the runtime uses its configured
|
||||
// default model. For non-Claude runtimes (OpenCode, Codex, etc.) that don't recognize
|
||||
// Claude aliases (opus/sonnet/haiku/inherit). Set automatically during install. See #1156.
|
||||
// 2. Compute the tier (opus/sonnet/haiku) for this agent under the active profile.
|
||||
const profile = String(config.model_profile || 'balanced').toLowerCase();
|
||||
const agentModels = MODEL_PROFILES[agentType];
|
||||
const tier = agentModels ? (agentModels[profile] || agentModels['balanced']) : null;
|
||||
|
||||
// 3. Runtime-aware resolution (#2517) — only when `runtime` is explicitly set
|
||||
// to a non-Claude runtime. `runtime: "claude"` is the implicit default and is
|
||||
// treated as a no-op here so it does not silently override `resolve_model_ids:
|
||||
// "omit"` (review finding #4). Deliberate ordering for non-Claude runtimes:
|
||||
// explicit opt-in beats `resolve_model_ids: "omit"` so users on Codex installs
|
||||
// that auto-set "omit" can still flip on tiered behavior by setting runtime
|
||||
// alone. inherit profile is preserved verbatim.
|
||||
if (config.runtime && config.runtime !== 'claude' && profile !== 'inherit' && tier) {
|
||||
const entry = _resolveRuntimeTier(config, tier);
|
||||
if (entry?.model) return entry.model;
|
||||
// Unknown runtime with no user-supplied overrides — fall through to Claude-safe
|
||||
// default rather than emit an ID the runtime can't accept.
|
||||
}
|
||||
|
||||
// 4. resolve_model_ids: "omit" — return empty string so the runtime uses its
|
||||
// configured default model. For non-Claude runtimes (OpenCode, Codex, etc.) that
|
||||
// don't recognize Claude aliases. Set automatically during install. See #1156.
|
||||
if (config.resolve_model_ids === 'omit') {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Fall back to profile lookup
|
||||
const profile = String(config.model_profile || 'balanced').toLowerCase();
|
||||
const agentModels = MODEL_PROFILES[agentType];
|
||||
// 5. Profile lookup (Claude-native default).
|
||||
if (!agentModels) return 'sonnet';
|
||||
if (profile === 'inherit') return 'inherit';
|
||||
const alias = agentModels[profile] || agentModels['balanced'] || 'sonnet';
|
||||
// `tier` is guaranteed truthy here: agentModels exists, and MODEL_PROFILES
|
||||
// entries always define `balanced`, so `agentModels[profile] || agentModels.balanced`
|
||||
// resolves to a string. Keep the local for readability — no defensive fallback.
|
||||
const alias = tier;
|
||||
|
||||
// resolve_model_ids: true — map alias to full Claude model ID
|
||||
// Prevents 404s when the Task tool passes aliases directly to the API
|
||||
// resolve_model_ids: true — map alias to full Claude model ID.
|
||||
// Prevents 404s when the Task tool passes aliases directly to the API.
|
||||
if (config.resolve_model_ids) {
|
||||
return MODEL_ALIAS_MAP[alias] || alias;
|
||||
}
|
||||
@@ -1482,6 +1711,41 @@ function resolveModelInternal(cwd, agentType) {
|
||||
return alias;
|
||||
}
|
||||
|
||||
/**
|
||||
* #2517 — Resolve runtime-specific reasoning_effort for an agent.
|
||||
* Returns null unless:
|
||||
* - `runtime` is explicitly set in config,
|
||||
* - the runtime supports reasoning_effort (currently: codex),
|
||||
* - profile is not 'inherit',
|
||||
* - the resolved tier entry has a `reasoning_effort` value.
|
||||
*
|
||||
* Never returns a value for Claude — keeps reasoning_effort out of Claude spawn paths.
|
||||
*/
|
||||
function resolveReasoningEffortInternal(cwd, agentType) {
|
||||
const config = loadConfig(cwd);
|
||||
if (!config.runtime) return null;
|
||||
// Strict allowlist: reasoning_effort only propagates for runtimes whose
|
||||
// install path actually accepts it. Adding a new runtime here is the only
|
||||
// way to enable effort propagation — overrides cannot bypass the gate.
|
||||
// Without this, a typo in `runtime` (e.g. `"codx"`) plus a user override
|
||||
// for that typo would leak `xhigh` into a Claude or unknown install
|
||||
// (review finding #3).
|
||||
if (!RUNTIMES_WITH_REASONING_EFFORT.has(config.runtime)) return null;
|
||||
// Per-agent override means user supplied a fully-qualified ID; reasoning_effort
|
||||
// for that case must be set via per-agent mechanism, not tier inference.
|
||||
if (config.model_overrides?.[agentType]) return null;
|
||||
|
||||
const profile = String(config.model_profile || 'balanced').toLowerCase();
|
||||
if (profile === 'inherit') return null;
|
||||
const agentModels = MODEL_PROFILES[agentType];
|
||||
if (!agentModels) return null;
|
||||
const tier = agentModels[profile] || agentModels['balanced'];
|
||||
if (!tier) return null;
|
||||
|
||||
const entry = _resolveRuntimeTier(config, tier);
|
||||
return entry?.reasoning_effort || null;
|
||||
}
|
||||
|
||||
// ─── Summary body helpers ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -1492,11 +1756,28 @@ function resolveModelInternal(cwd, agentType) {
|
||||
*/
|
||||
function extractOneLinerFromBody(content) {
|
||||
if (!content) return null;
|
||||
// Normalize EOLs so matching works for LF and CRLF files.
|
||||
const normalized = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||
// Strip frontmatter first
|
||||
const body = content.replace(/^---\n[\s\S]*?\n---\n*/, '');
|
||||
// Find the first **...** line after a # heading
|
||||
const match = body.match(/^#[^\n]*\n+\*\*([^*]+)\*\*/m);
|
||||
return match ? match[1].trim() : null;
|
||||
const body = normalized.replace(/^---\n[\s\S]*?\n---\n*/, '');
|
||||
// Find the first **...** span on a line after a # heading.
|
||||
// Two supported template forms:
|
||||
// 1) Labeled: **One-liner:** Real prose here. (bug #2660 — new template)
|
||||
// 2) Bare: **Real prose here.** (legacy template)
|
||||
// For (1), the first bold span ends in a colon and the prose that follows
|
||||
// on the same line is the one-liner. For (2), the bold span itself is the
|
||||
// one-liner.
|
||||
const match = body.match(/^#[^\n]*\n+\*\*([^*\n]+)\*\*([^\n]*)/m);
|
||||
if (!match) return null;
|
||||
const boldInner = match[1].trim();
|
||||
const afterBold = match[2];
|
||||
// Labeled form: bold span is a "Label:" prefix — capture prose after it.
|
||||
if (/:\s*$/.test(boldInner)) {
|
||||
const prose = afterBold.trim();
|
||||
return prose.length > 0 ? prose : null;
|
||||
}
|
||||
// Bare form: the bold content itself is the one-liner.
|
||||
return boldInner.length > 0 ? boldInner : null;
|
||||
}
|
||||
|
||||
// ─── Misc utilities ───────────────────────────────────────────────────────────
|
||||
@@ -1760,6 +2041,13 @@ module.exports = {
|
||||
getArchivedPhaseDirs,
|
||||
getRoadmapPhaseInternal,
|
||||
resolveModelInternal,
|
||||
resolveReasoningEffortInternal,
|
||||
RUNTIME_PROFILE_MAP,
|
||||
RUNTIMES_WITH_REASONING_EFFORT,
|
||||
KNOWN_RUNTIMES,
|
||||
RUNTIME_OVERRIDE_TIERS,
|
||||
resolveTierEntry,
|
||||
_resetRuntimeWarningCacheForTests,
|
||||
pathExistsInternal,
|
||||
generateSlugInternal,
|
||||
getMilestoneInfo,
|
||||
|
||||
48
get-shit-done/bin/lib/decisions.cjs
Normal file
48
get-shit-done/bin/lib/decisions.cjs
Normal file
@@ -0,0 +1,48 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Shared parser for CONTEXT.md `<decisions>` blocks.
|
||||
*
|
||||
* Used by:
|
||||
* - gap-checker.cjs (#2493 post-planning gap analysis)
|
||||
* - intended for #2492 (plan-phase decision gate, verify-phase decision validator)
|
||||
*
|
||||
* Format produced by discuss-phase.md:
|
||||
*
|
||||
* <decisions>
|
||||
* ## Implementation Decisions
|
||||
*
|
||||
* ### Category
|
||||
* - **D-01:** Decision text
|
||||
* - **D-02:** Another decision
|
||||
* </decisions>
|
||||
*
|
||||
* D-IDs outside the <decisions> block are ignored. Missing block returns [].
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parse the <decisions> section of a CONTEXT.md string.
|
||||
*
|
||||
* @param {string|null|undefined} contextMd - File contents, may be empty/missing.
|
||||
* @returns {Array<{id: string, text: string}>}
|
||||
*/
|
||||
function parseDecisions(contextMd) {
|
||||
if (!contextMd || typeof contextMd !== 'string') return [];
|
||||
const blockMatch = contextMd.match(/<decisions>([\s\S]*?)<\/decisions>/);
|
||||
if (!blockMatch) return [];
|
||||
const block = blockMatch[1];
|
||||
|
||||
const decisionRe = /^\s*-\s*\*\*(D-[A-Za-z0-9_-]+):\*\*\s*(.+?)\s*$/gm;
|
||||
const out = [];
|
||||
const seen = new Set();
|
||||
let m;
|
||||
while ((m = decisionRe.exec(block)) !== null) {
|
||||
const id = m[1];
|
||||
if (seen.has(id)) continue;
|
||||
seen.add(id);
|
||||
out.push({ id, text: m[2] });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
module.exports = { parseDecisions };
|
||||
378
get-shit-done/bin/lib/drift.cjs
Normal file
378
get-shit-done/bin/lib/drift.cjs
Normal file
@@ -0,0 +1,378 @@
|
||||
/**
|
||||
* Codebase Drift Detection (#2003)
|
||||
*
|
||||
* Detects structural drift between a committed codebase and the
|
||||
* `.planning/codebase/STRUCTURE.md` map produced by `gsd-codebase-mapper`.
|
||||
*
|
||||
* Four categories of drift element:
|
||||
* - new_dir → a newly-added file whose directory prefix does not appear
|
||||
* in STRUCTURE.md
|
||||
* - barrel → a newly-added barrel export at
|
||||
* (packages|apps)/<name>/src/index.(ts|tsx|js|mjs|cjs)
|
||||
* - migration → a newly-added migration file under one of the recognized
|
||||
* migration directories (supabase, prisma, drizzle, src/migrations, …)
|
||||
* - route → a newly-added route module under a `routes/` or `api/` dir
|
||||
*
|
||||
* Each file is counted at most once; when a file matches multiple categories
|
||||
* the most specific category wins (migration > route > barrel > new_dir).
|
||||
*
|
||||
* Design decisions (see PR for full rubber-duck):
|
||||
* - The library is pure. It takes parsed git diff output and returns a
|
||||
* structured result. The CLI/workflow layer is responsible for running
|
||||
* git and for spawning mappers.
|
||||
* - `last_mapped_commit` is stored as YAML-style frontmatter at the top of
|
||||
* each `.planning/codebase/*.md` file. This keeps the baseline attached
|
||||
* to the file, survives git moves, and avoids a sidecar JSON.
|
||||
* - The detector NEVER throws on malformed input — it returns a
|
||||
* `{ skipped: true }` result. The phase workflow depends on this
|
||||
* non-blocking guarantee.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('node:fs');
|
||||
|
||||
// ─── Constants ───────────────────────────────────────────────────────────────
|
||||
|
||||
const DRIFT_CATEGORIES = Object.freeze(['new_dir', 'barrel', 'migration', 'route']);
|
||||
|
||||
// Category priority when a single file matches multiple rules.
|
||||
// Higher index = more specific = wins.
|
||||
const CATEGORY_PRIORITY = { new_dir: 0, barrel: 1, route: 2, migration: 3 };
|
||||
|
||||
const BARREL_RE = /^(packages|apps)\/[^/]+\/src\/index\.(ts|tsx|js|mjs|cjs)$/;
|
||||
|
||||
const MIGRATION_RES = [
|
||||
/^supabase\/migrations\/.+\.sql$/,
|
||||
/^prisma\/migrations\/.+/,
|
||||
/^drizzle\/meta\/.+/,
|
||||
/^drizzle\/migrations\/.+/,
|
||||
/^src\/migrations\/.+\.(ts|js|sql)$/,
|
||||
/^db\/migrations\/.+\.(sql|ts|js)$/,
|
||||
/^migrations\/.+\.(sql|ts|js)$/,
|
||||
];
|
||||
|
||||
const ROUTE_RES = [
|
||||
/^(apps|packages)\/[^/]+\/src\/routes\/.+\.(ts|tsx|js|jsx|mjs|cjs)$/,
|
||||
/^src\/routes\/.+\.(ts|tsx|js|jsx|mjs|cjs)$/,
|
||||
/^src\/api\/.+\.(ts|tsx|js|jsx|mjs|cjs)$/,
|
||||
/^(apps|packages)\/[^/]+\/src\/api\/.+\.(ts|tsx|js|jsx|mjs|cjs)$/,
|
||||
];
|
||||
|
||||
// A conservative allowlist for `--paths` arguments passed to the mapper:
|
||||
// repo-relative path components separated by /, containing only
|
||||
// alphanumerics, dash, underscore, and dot (no `..`, no `/..`).
|
||||
const SAFE_PATH_RE = /^(?!.*\.\.)(?:[A-Za-z0-9_.][A-Za-z0-9_.\-]*)(?:\/[A-Za-z0-9_.][A-Za-z0-9_.\-]*)*$/;
|
||||
|
||||
// ─── Classification ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Classify a single file path into a drift category or null.
|
||||
*
|
||||
* @param {string} file - repo-relative path, forward slashes.
|
||||
* @returns {'barrel'|'migration'|'route'|null}
|
||||
*/
|
||||
function classifyFile(file) {
|
||||
if (typeof file !== 'string' || !file) return null;
|
||||
const norm = file.replace(/\\/g, '/');
|
||||
if (MIGRATION_RES.some((r) => r.test(norm))) return 'migration';
|
||||
if (ROUTE_RES.some((r) => r.test(norm))) return 'route';
|
||||
if (BARREL_RE.test(norm)) return 'barrel';
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* True iff any prefix of `file` (dir1, dir1/dir2, …) appears as a substring
|
||||
* of `structureMd`. Used to decide whether a file is in "mapped territory".
|
||||
*
|
||||
* Matching is deliberately substring-based — STRUCTURE.md is free-form
|
||||
* markdown, not a structured manifest. If the map mentions `src/lib/` the
|
||||
* check `structureMd.includes('src/lib')` holds.
|
||||
*/
|
||||
function isPathMapped(file, structureMd) {
|
||||
const norm = file.replace(/\\/g, '/');
|
||||
const parts = norm.split('/');
|
||||
// Check prefixes from longest to shortest; any hit means "mapped".
|
||||
for (let i = parts.length - 1; i >= 1; i--) {
|
||||
const prefix = parts.slice(0, i).join('/');
|
||||
if (structureMd.includes(prefix)) return true;
|
||||
}
|
||||
// Finally, if even the top-level dir is mentioned, count as mapped.
|
||||
if (parts.length > 0 && structureMd.includes(parts[0] + '/')) return true;
|
||||
if (parts.length > 0 && structureMd.includes('`' + parts[0] + '`')) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// ─── Main detection ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Detect codebase drift.
|
||||
*
|
||||
* @param {object} input
|
||||
* @param {string[]} input.addedFiles - files with git status A (new)
|
||||
* @param {string[]} input.modifiedFiles - files with git status M
|
||||
* @param {string[]} input.deletedFiles - files with git status D
|
||||
* @param {string|null|undefined} input.structureMd - contents of STRUCTURE.md
|
||||
* @param {number} [input.threshold=3] - min number of drift elements that triggers action
|
||||
* @param {'warn'|'auto-remap'} [input.action='warn']
|
||||
* @returns {object} result
|
||||
*/
|
||||
function detectDrift(input) {
|
||||
try {
|
||||
if (!input || typeof input !== 'object') {
|
||||
return skipped('invalid-input');
|
||||
}
|
||||
const {
|
||||
addedFiles,
|
||||
modifiedFiles,
|
||||
deletedFiles,
|
||||
structureMd,
|
||||
} = input;
|
||||
const threshold = Number.isInteger(input.threshold) && input.threshold >= 1
|
||||
? input.threshold
|
||||
: 3;
|
||||
const action = input.action === 'auto-remap' ? 'auto-remap' : 'warn';
|
||||
|
||||
if (structureMd === null || structureMd === undefined) {
|
||||
return skipped('missing-structure-md');
|
||||
}
|
||||
if (typeof structureMd !== 'string') {
|
||||
return skipped('invalid-structure-md');
|
||||
}
|
||||
|
||||
const added = Array.isArray(addedFiles) ? addedFiles.filter((x) => typeof x === 'string') : [];
|
||||
const modified = Array.isArray(modifiedFiles) ? modifiedFiles : [];
|
||||
const deleted = Array.isArray(deletedFiles) ? deletedFiles : [];
|
||||
|
||||
// Build elements. One element per file, highest-priority category wins.
|
||||
/** @type {{category: string, path: string}[]} */
|
||||
const elements = [];
|
||||
const seen = new Map();
|
||||
|
||||
for (const rawFile of added) {
|
||||
const file = rawFile.replace(/\\/g, '/');
|
||||
const specific = classifyFile(file);
|
||||
let category = specific;
|
||||
if (!category) {
|
||||
if (!isPathMapped(file, structureMd)) {
|
||||
category = 'new_dir';
|
||||
} else {
|
||||
continue; // mapped, known, ordinary file — not drift
|
||||
}
|
||||
}
|
||||
// Dedup: if we've already counted this path at higher-or-equal priority, skip
|
||||
const prior = seen.get(file);
|
||||
if (prior && CATEGORY_PRIORITY[prior] >= CATEGORY_PRIORITY[category]) continue;
|
||||
seen.set(file, category);
|
||||
}
|
||||
|
||||
for (const [file, category] of seen.entries()) {
|
||||
elements.push({ category, path: file });
|
||||
}
|
||||
|
||||
// Sort for stable output.
|
||||
elements.sort((a, b) =>
|
||||
a.category === b.category
|
||||
? a.path.localeCompare(b.path)
|
||||
: a.category.localeCompare(b.category),
|
||||
);
|
||||
|
||||
const actionRequired = elements.length >= threshold;
|
||||
let directive = 'none';
|
||||
let spawnMapper = false;
|
||||
let affectedPaths = [];
|
||||
let message = '';
|
||||
|
||||
if (actionRequired) {
|
||||
directive = action;
|
||||
affectedPaths = chooseAffectedPaths(elements.map((e) => e.path));
|
||||
if (action === 'auto-remap') {
|
||||
spawnMapper = true;
|
||||
}
|
||||
message = buildMessage(elements, affectedPaths, action);
|
||||
}
|
||||
|
||||
return {
|
||||
skipped: false,
|
||||
elements,
|
||||
actionRequired,
|
||||
directive,
|
||||
spawnMapper,
|
||||
affectedPaths,
|
||||
threshold,
|
||||
action,
|
||||
message,
|
||||
counts: {
|
||||
added: added.length,
|
||||
modified: modified.length,
|
||||
deleted: deleted.length,
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
// Non-blocking: never throw from this function.
|
||||
return skipped('exception:' + (err && err.message ? err.message : String(err)));
|
||||
}
|
||||
}
|
||||
|
||||
function skipped(reason) {
|
||||
return {
|
||||
skipped: true,
|
||||
reason,
|
||||
elements: [],
|
||||
actionRequired: false,
|
||||
directive: 'none',
|
||||
spawnMapper: false,
|
||||
affectedPaths: [],
|
||||
message: '',
|
||||
};
|
||||
}
|
||||
|
||||
function buildMessage(elements, affectedPaths, action) {
|
||||
const byCat = {};
|
||||
for (const e of elements) {
|
||||
(byCat[e.category] ||= []).push(e.path);
|
||||
}
|
||||
const lines = [
|
||||
`Codebase drift detected: ${elements.length} structural element(s) since last mapping.`,
|
||||
'',
|
||||
];
|
||||
const labels = {
|
||||
new_dir: 'New directories',
|
||||
barrel: 'New barrel exports',
|
||||
migration: 'New migrations',
|
||||
route: 'New route modules',
|
||||
};
|
||||
for (const cat of ['new_dir', 'barrel', 'migration', 'route']) {
|
||||
if (byCat[cat]) {
|
||||
lines.push(`${labels[cat]}:`);
|
||||
for (const p of byCat[cat]) lines.push(` - ${p}`);
|
||||
}
|
||||
}
|
||||
lines.push('');
|
||||
if (action === 'auto-remap') {
|
||||
lines.push(`Auto-remap scheduled for paths: ${affectedPaths.join(', ')}`);
|
||||
} else {
|
||||
lines.push(
|
||||
`Run /gsd-map-codebase --paths ${affectedPaths.join(',')} to refresh planning context.`,
|
||||
);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
// ─── Affected paths ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Collapse a list of drifted file paths into a sorted, deduplicated list of
|
||||
* the top-level directory prefixes (depth 2 when the repo uses an
|
||||
* `<apps|packages>/<name>/…` layout; depth 1 otherwise).
|
||||
*/
|
||||
function chooseAffectedPaths(paths) {
|
||||
const out = new Set();
|
||||
for (const raw of paths || []) {
|
||||
if (typeof raw !== 'string' || !raw) continue;
|
||||
const file = raw.replace(/\\/g, '/');
|
||||
const parts = file.split('/');
|
||||
if (parts.length === 0) continue;
|
||||
const top = parts[0];
|
||||
if ((top === 'apps' || top === 'packages') && parts.length >= 2) {
|
||||
out.add(`${top}/${parts[1]}`);
|
||||
} else {
|
||||
out.add(top);
|
||||
}
|
||||
}
|
||||
return [...out].sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter `paths` to only those that are safe to splice into a mapper prompt.
|
||||
* Any path that is absolute, contains traversal, or includes shell
|
||||
* metacharacters is dropped.
|
||||
*/
|
||||
function sanitizePaths(paths) {
|
||||
if (!Array.isArray(paths)) return [];
|
||||
const out = [];
|
||||
for (const p of paths) {
|
||||
if (typeof p !== 'string') continue;
|
||||
if (p.startsWith('/')) continue;
|
||||
if (!SAFE_PATH_RE.test(p)) continue;
|
||||
out.push(p);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ─── Frontmatter helpers ─────────────────────────────────────────────────────
|
||||
|
||||
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
|
||||
|
||||
function parseFrontmatter(content) {
|
||||
if (typeof content !== 'string') return { data: {}, body: '' };
|
||||
const m = content.match(FRONTMATTER_RE);
|
||||
if (!m) return { data: {}, body: content };
|
||||
const data = {};
|
||||
for (const line of m[1].split(/\r?\n/)) {
|
||||
const kv = line.match(/^([A-Za-z0-9_][A-Za-z0-9_-]*):\s*(.*)$/);
|
||||
if (!kv) continue;
|
||||
data[kv[1]] = kv[2];
|
||||
}
|
||||
return { data, body: content.slice(m[0].length) };
|
||||
}
|
||||
|
||||
function serializeFrontmatter(data, body) {
|
||||
const keys = Object.keys(data);
|
||||
if (keys.length === 0) return body;
|
||||
const lines = ['---'];
|
||||
for (const k of keys) lines.push(`${k}: ${data[k]}`);
|
||||
lines.push('---');
|
||||
return lines.join('\n') + '\n' + body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read `last_mapped_commit` from the frontmatter of a `.planning/codebase/*.md`
|
||||
* file. Returns null if the file does not exist or has no frontmatter.
|
||||
*/
|
||||
function readMappedCommit(filePath) {
|
||||
let content;
|
||||
try {
|
||||
content = fs.readFileSync(filePath, 'utf8');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const { data } = parseFrontmatter(content);
|
||||
const sha = data.last_mapped_commit;
|
||||
return typeof sha === 'string' && sha.length > 0 ? sha : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upsert `last_mapped_commit` and `last_mapped_at` into the frontmatter of
|
||||
* the given file, preserving any other frontmatter keys and the body.
|
||||
*/
|
||||
function writeMappedCommit(filePath, commitSha, isoDate) {
|
||||
// Symmetric with readMappedCommit (which returns null on missing files):
|
||||
// tolerate a missing target by creating a minimal frontmatter-only file
|
||||
// rather than throwing ENOENT. This matters when a mapper produces a new
|
||||
// doc and the caller stamps it before any prior content existed.
|
||||
let content = '';
|
||||
try {
|
||||
content = fs.readFileSync(filePath, 'utf8');
|
||||
} catch (err) {
|
||||
if (err.code !== 'ENOENT') throw err;
|
||||
}
|
||||
const { data, body } = parseFrontmatter(content);
|
||||
data.last_mapped_commit = commitSha;
|
||||
if (isoDate) data.last_mapped_at = isoDate;
|
||||
fs.writeFileSync(filePath, serializeFrontmatter(data, body));
|
||||
}
|
||||
|
||||
// ─── Exports ─────────────────────────────────────────────────────────────────
|
||||
|
||||
module.exports = {
|
||||
DRIFT_CATEGORIES,
|
||||
classifyFile,
|
||||
detectDrift,
|
||||
chooseAffectedPaths,
|
||||
sanitizePaths,
|
||||
readMappedCommit,
|
||||
writeMappedCommit,
|
||||
// Exposed for the CLI layer to reuse the same parser.
|
||||
parseFrontmatter,
|
||||
};
|
||||
183
get-shit-done/bin/lib/gap-checker.cjs
Normal file
183
get-shit-done/bin/lib/gap-checker.cjs
Normal file
@@ -0,0 +1,183 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Post-planning gap analysis (#2493).
|
||||
*
|
||||
* Reads REQUIREMENTS.md (planning-root) and CONTEXT.md (per-phase) and compares
|
||||
* each REQ-ID and D-ID against the concatenated text of all PLAN.md files in
|
||||
* the phase directory. Emits a unified `Source | Item | Status` report.
|
||||
*
|
||||
* Gated on workflow.post_planning_gaps (default true). When false, returns
|
||||
* { enabled: false } and does not scan.
|
||||
*
|
||||
* Coverage detection uses word-boundary regex matching to avoid false positives
|
||||
* (REQ-1 must not match REQ-10).
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { planningPaths, planningDir, escapeRegex, output, error } = require('./core.cjs');
|
||||
const { parseDecisions } = require('./decisions.cjs');
|
||||
|
||||
/**
|
||||
* Parse REQ-IDs from REQUIREMENTS.md content.
|
||||
*
|
||||
* Supports both checkbox (`- [ ] **REQ-NN** ...`) and traceability table
|
||||
* (`| REQ-NN | ... |`) formats.
|
||||
*/
|
||||
function parseRequirements(reqMd) {
|
||||
if (!reqMd || typeof reqMd !== 'string') return [];
|
||||
const out = [];
|
||||
const seen = new Set();
|
||||
|
||||
const checkboxRe = /^\s*-\s*\[[x ]\]\s*\*\*(REQ-[A-Za-z0-9_-]+)\*\*\s*(.*)$/gm;
|
||||
let cm = checkboxRe.exec(reqMd);
|
||||
while (cm !== null) {
|
||||
const id = cm[1];
|
||||
if (!seen.has(id)) {
|
||||
seen.add(id);
|
||||
out.push({ id, text: (cm[2] || '').trim() });
|
||||
}
|
||||
cm = checkboxRe.exec(reqMd);
|
||||
}
|
||||
|
||||
const tableRe = /\|\s*(REQ-[A-Za-z0-9_-]+)\s*\|/g;
|
||||
let tm = tableRe.exec(reqMd);
|
||||
while (tm !== null) {
|
||||
const id = tm[1];
|
||||
if (!seen.has(id)) {
|
||||
seen.add(id);
|
||||
out.push({ id, text: '' });
|
||||
}
|
||||
tm = tableRe.exec(reqMd);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function detectCoverage(items, planText) {
|
||||
return items.map(it => {
|
||||
const re = new RegExp('\\b' + escapeRegex(it.id) + '\\b');
|
||||
return {
|
||||
source: it.source,
|
||||
item: it.id,
|
||||
status: re.test(planText) ? 'Covered' : 'Not covered',
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function naturalKey(s) {
|
||||
return String(s).replace(/(\d+)/g, (_, n) => n.padStart(8, '0'));
|
||||
}
|
||||
|
||||
function sortRows(rows) {
|
||||
const sourceOrder = { 'REQUIREMENTS.md': 0, 'CONTEXT.md': 1 };
|
||||
return rows.slice().sort((a, b) => {
|
||||
const so = (sourceOrder[a.source] ?? 99) - (sourceOrder[b.source] ?? 99);
|
||||
if (so !== 0) return so;
|
||||
return naturalKey(a.item).localeCompare(naturalKey(b.item));
|
||||
});
|
||||
}
|
||||
|
||||
function formatGapTable(rows) {
|
||||
if (rows.length === 0) {
|
||||
return '## Post-Planning Gap Analysis\n\nNo requirements or decisions to check.\n';
|
||||
}
|
||||
const header = '| Source | Item | Status |\n|--------|------|--------|';
|
||||
const body = rows.map(r => {
|
||||
const tick = r.status === 'Covered' ? '\u2713 Covered' : '\u2717 Not covered';
|
||||
return `| ${r.source} | ${r.item} | ${tick} |`;
|
||||
}).join('\n');
|
||||
return `## Post-Planning Gap Analysis\n\n${header}\n${body}\n`;
|
||||
}
|
||||
|
||||
function readGate(cwd) {
|
||||
const cfgPath = path.join(planningDir(cwd), 'config.json');
|
||||
try {
|
||||
const raw = JSON.parse(fs.readFileSync(cfgPath, 'utf-8'));
|
||||
if (raw && raw.workflow && typeof raw.workflow.post_planning_gaps === 'boolean') {
|
||||
return raw.workflow.post_planning_gaps;
|
||||
}
|
||||
} catch { /* fall through */ }
|
||||
return true;
|
||||
}
|
||||
|
||||
function runGapAnalysis(cwd, phaseDir) {
|
||||
if (!readGate(cwd)) {
|
||||
return {
|
||||
enabled: false,
|
||||
rows: [],
|
||||
table: '',
|
||||
summary: 'workflow.post_planning_gaps disabled — skipping post-planning gap analysis',
|
||||
counts: { total: 0, covered: 0, uncovered: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
const absPhaseDir = path.isAbsolute(phaseDir) ? phaseDir : path.join(cwd, phaseDir);
|
||||
|
||||
const reqPath = planningPaths(cwd).requirements;
|
||||
const reqMd = fs.existsSync(reqPath) ? fs.readFileSync(reqPath, 'utf-8') : '';
|
||||
const reqItems = parseRequirements(reqMd).map(r => ({ ...r, source: 'REQUIREMENTS.md' }));
|
||||
|
||||
const ctxPath = path.join(absPhaseDir, 'CONTEXT.md');
|
||||
const ctxMd = fs.existsSync(ctxPath) ? fs.readFileSync(ctxPath, 'utf-8') : '';
|
||||
const dItems = parseDecisions(ctxMd).map(d => ({ ...d, source: 'CONTEXT.md' }));
|
||||
|
||||
const items = [...reqItems, ...dItems];
|
||||
|
||||
let planText = '';
|
||||
try {
|
||||
if (fs.existsSync(absPhaseDir)) {
|
||||
const files = fs.readdirSync(absPhaseDir).filter(f => /-PLAN\.md$/.test(f));
|
||||
planText = files.map(f => {
|
||||
try { return fs.readFileSync(path.join(absPhaseDir, f), 'utf-8'); }
|
||||
catch { return ''; }
|
||||
}).join('\n');
|
||||
}
|
||||
} catch { /* unreadable */ }
|
||||
|
||||
if (items.length === 0) {
|
||||
return {
|
||||
enabled: true,
|
||||
rows: [],
|
||||
table: '## Post-Planning Gap Analysis\n\nNo requirements or decisions to check.\n',
|
||||
summary: 'no requirements or decisions to check',
|
||||
counts: { total: 0, covered: 0, uncovered: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
const rows = sortRows(detectCoverage(items, planText));
|
||||
const uncovered = rows.filter(r => r.status === 'Not covered').length;
|
||||
const covered = rows.length - uncovered;
|
||||
|
||||
const summary = uncovered === 0
|
||||
? `\u2713 All ${rows.length} items covered by plans`
|
||||
: `\u26A0 ${uncovered} of ${rows.length} items not covered by any plan`;
|
||||
|
||||
return {
|
||||
enabled: true,
|
||||
rows,
|
||||
table: formatGapTable(rows) + '\n' + summary + '\n',
|
||||
summary,
|
||||
counts: { total: rows.length, covered, uncovered },
|
||||
};
|
||||
}
|
||||
|
||||
function cmdGapAnalysis(cwd, args, raw) {
|
||||
const idx = args.indexOf('--phase-dir');
|
||||
if (idx === -1 || !args[idx + 1]) {
|
||||
error('Usage: gap-analysis --phase-dir <path-to-phase-directory>');
|
||||
}
|
||||
const phaseDir = args[idx + 1];
|
||||
const result = runGapAnalysis(cwd, phaseDir);
|
||||
output(result, raw, result.table || result.summary);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
parseRequirements,
|
||||
detectCoverage,
|
||||
formatGapTable,
|
||||
sortRows,
|
||||
runGapAnalysis,
|
||||
cmdGapAnalysis,
|
||||
};
|
||||
@@ -420,7 +420,7 @@ function buildPreview(gsd2Data, artifacts) {
|
||||
lines.push('');
|
||||
lines.push('Cannot migrate automatically:');
|
||||
lines.push(' - GSD-2 cost/token ledger (no v1 equivalent)');
|
||||
lines.push(' - GSD-2 database state (rebuilt from files on first /gsd:health)');
|
||||
lines.push(' - GSD-2 database state (rebuilt from files on first /gsd-health)');
|
||||
lines.push(' - VS Code extension state');
|
||||
|
||||
return lines.join('\n');
|
||||
|
||||
@@ -827,20 +827,70 @@ function cmdInitMilestoneOp(cwd, raw) {
|
||||
let phaseCount = 0;
|
||||
let completedPhases = 0;
|
||||
const phasesDir = path.join(planningDir(cwd), 'phases');
|
||||
|
||||
// Bug #2633 — ROADMAP.md (current milestone section) is the authority for
|
||||
// phase counts, NOT the on-disk `.planning/phases/` directory. After
|
||||
// `phases clear` between milestones, on-disk dirs will be a subset of the
|
||||
// roadmap until each phase is materialized; reading from disk causes
|
||||
// `all_phases_complete: true` to fire prematurely.
|
||||
let roadmapPhaseNumbers = [];
|
||||
try {
|
||||
const roadmapPath = path.join(planningDir(cwd), 'ROADMAP.md');
|
||||
const roadmapRaw = fs.readFileSync(roadmapPath, 'utf-8');
|
||||
const currentSection = extractCurrentMilestone(roadmapRaw, cwd);
|
||||
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi;
|
||||
let m;
|
||||
while ((m = phasePattern.exec(currentSection)) !== null) {
|
||||
roadmapPhaseNumbers.push(m[1]);
|
||||
}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
// Canonicalize a phase token by stripping leading zeros from the integer
|
||||
// head while preserving any [A-Z]? suffix and dotted segments. So "03" →
|
||||
// "3", "03A" → "3A", "03.1" → "3.1", "3A" → "3A". Disk dirs that pad
|
||||
// ("03-alpha") then match roadmap tokens ("Phase 3") without ever
|
||||
// collapsing distinct tokens like "3" / "3A" / "3.1" into the same bucket.
|
||||
const canonicalizePhase = (tok) => {
|
||||
const m = tok.match(/^(\d+)([A-Z]?(?:\.\d+)*)$/);
|
||||
return m ? String(parseInt(m[1], 10)) + m[2] : tok;
|
||||
};
|
||||
const diskPhaseDirs = new Map();
|
||||
try {
|
||||
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
||||
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
||||
phaseCount = dirs.length;
|
||||
for (const e of entries) {
|
||||
if (!e.isDirectory()) continue;
|
||||
const m = e.name.match(/^(\d+[A-Z]?(?:\.\d+)*)/);
|
||||
if (!m) continue;
|
||||
diskPhaseDirs.set(canonicalizePhase(m[1]), e.name);
|
||||
}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
// Count phases with summaries (completed)
|
||||
for (const dir of dirs) {
|
||||
if (roadmapPhaseNumbers.length > 0) {
|
||||
phaseCount = roadmapPhaseNumbers.length;
|
||||
for (const num of roadmapPhaseNumbers) {
|
||||
const dirName = diskPhaseDirs.get(canonicalizePhase(num));
|
||||
if (!dirName) continue;
|
||||
try {
|
||||
const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
|
||||
const phaseFiles = fs.readdirSync(path.join(phasesDir, dirName));
|
||||
const hasSummary = phaseFiles.some(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
||||
if (hasSummary) completedPhases++;
|
||||
} catch { /* intentionally empty */ }
|
||||
}
|
||||
} catch { /* intentionally empty */ }
|
||||
} else {
|
||||
// Fallback: no parseable ROADMAP — preserve legacy on-disk behavior.
|
||||
try {
|
||||
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
||||
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
|
||||
phaseCount = dirs.length;
|
||||
for (const dir of dirs) {
|
||||
try {
|
||||
const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
|
||||
const hasSummary = phaseFiles.some(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
||||
if (hasSummary) completedPhases++;
|
||||
} catch { /* intentionally empty */ }
|
||||
}
|
||||
} catch { /* intentionally empty */ }
|
||||
}
|
||||
|
||||
// Check archive
|
||||
const archiveDir = path.join(planningRoot(cwd), 'archive');
|
||||
@@ -929,10 +979,10 @@ function cmdInitManager(cwd, raw) {
|
||||
|
||||
// Validate prerequisites
|
||||
if (!fs.existsSync(paths.roadmap)) {
|
||||
error('No ROADMAP.md found. Run /gsd:new-milestone first.');
|
||||
error('No ROADMAP.md found. Run /gsd-new-milestone first.');
|
||||
}
|
||||
if (!fs.existsSync(paths.state)) {
|
||||
error('No STATE.md found. Run /gsd:new-milestone first.');
|
||||
error('No STATE.md found. Run /gsd-new-milestone first.');
|
||||
}
|
||||
const rawContent = fs.readFileSync(paths.roadmap, 'utf-8');
|
||||
const content = extractCurrentMilestone(rawContent, cwd);
|
||||
@@ -1111,7 +1161,7 @@ function cmdInitManager(cwd, raw) {
|
||||
phase_name: phase.name,
|
||||
action: 'execute',
|
||||
reason: `${phase.plan_count} plans ready, dependencies met`,
|
||||
command: `/gsd:execute-phase ${phase.number}`,
|
||||
command: `/gsd-execute-phase ${phase.number}`,
|
||||
});
|
||||
} else if (phase.disk_status === 'discussed' || phase.disk_status === 'researched') {
|
||||
recommendedActions.push({
|
||||
@@ -1119,7 +1169,7 @@ function cmdInitManager(cwd, raw) {
|
||||
phase_name: phase.name,
|
||||
action: 'plan',
|
||||
reason: 'Context gathered, ready for planning',
|
||||
command: `/gsd:plan-phase ${phase.number}`,
|
||||
command: `/gsd-plan-phase ${phase.number}`,
|
||||
});
|
||||
} else if ((phase.disk_status === 'empty' || phase.disk_status === 'no_directory') && phase.is_next_to_discuss) {
|
||||
recommendedActions.push({
|
||||
@@ -1127,7 +1177,7 @@ function cmdInitManager(cwd, raw) {
|
||||
phase_name: phase.name,
|
||||
action: 'discuss',
|
||||
reason: 'Unblocked, ready to gather context',
|
||||
command: `/gsd:discuss-phase ${phase.number}`,
|
||||
command: `/gsd-discuss-phase ${phase.number}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1230,6 +1280,7 @@ function cmdInitProgress(cwd, raw) {
|
||||
// Build set of phases defined in ROADMAP for the current milestone
|
||||
const roadmapPhaseNums = new Set();
|
||||
const roadmapPhaseNames = new Map();
|
||||
const roadmapCheckboxStates = new Map();
|
||||
try {
|
||||
const roadmapContent = extractCurrentMilestone(
|
||||
fs.readFileSync(path.join(planningDir(cwd), 'ROADMAP.md'), 'utf-8'), cwd
|
||||
@@ -1240,6 +1291,13 @@ function cmdInitProgress(cwd, raw) {
|
||||
roadmapPhaseNums.add(hm[1]);
|
||||
roadmapPhaseNames.set(hm[1], hm[2].replace(/\(INSERTED\)/i, '').trim());
|
||||
}
|
||||
// #2646: parse `- [x] Phase N` checkbox states so ROADMAP-only phases
|
||||
// inherit completion from the ROADMAP when no phase directory exists.
|
||||
const cbPattern = /-\s*\[(x| )\]\s*.*Phase\s+(\d+[A-Z]?(?:\.\d+)*)[:\s]/gi;
|
||||
let cbm;
|
||||
while ((cbm = cbPattern.exec(roadmapContent)) !== null) {
|
||||
roadmapCheckboxStates.set(cbm[2], cbm[1].toLowerCase() === 'x');
|
||||
}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
||||
@@ -1295,21 +1353,27 @@ function cmdInitProgress(cwd, raw) {
|
||||
}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
// Add phases defined in ROADMAP but not yet scaffolded to disk
|
||||
// Add phases defined in ROADMAP but not yet scaffolded to disk. When the
|
||||
// ROADMAP has a `- [x] Phase N` checkbox, honor it as 'complete' so
|
||||
// completed_count and status reflect the ROADMAP source of truth (#2646).
|
||||
for (const [num, name] of roadmapPhaseNames) {
|
||||
const stripped = num.replace(/^0+/, '') || '0';
|
||||
if (!seenPhaseNums.has(stripped)) {
|
||||
const checkboxComplete =
|
||||
roadmapCheckboxStates.get(num) === true ||
|
||||
roadmapCheckboxStates.get(stripped) === true;
|
||||
const status = checkboxComplete ? 'complete' : 'not_started';
|
||||
const phaseInfo = {
|
||||
number: num,
|
||||
name: name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''),
|
||||
directory: null,
|
||||
status: 'not_started',
|
||||
status,
|
||||
plan_count: 0,
|
||||
summary_count: 0,
|
||||
has_research: false,
|
||||
};
|
||||
phases.push(phaseInfo);
|
||||
if (!nextPhase && !currentPhase) {
|
||||
if (!nextPhase && !currentPhase && status !== 'complete') {
|
||||
nextPhase = phaseInfo;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,7 +381,7 @@ function cmdPhaseAdd(cwd, description, raw, customId) {
|
||||
|
||||
// Build phase entry
|
||||
const dependsOn = config.phase_naming === 'custom' ? '' : `\n**Depends on:** Phase ${typeof _newPhaseId === 'number' ? _newPhaseId - 1 : 'TBD'}`;
|
||||
const phaseEntry = `\n### Phase ${_newPhaseId}: ${description}\n\n**Goal:** [To be planned]\n**Requirements**: TBD${dependsOn}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd:plan-phase ${_newPhaseId} to break down)\n`;
|
||||
const phaseEntry = `\n### Phase ${_newPhaseId}: ${description}\n\n**Goal:** [To be planned]\n**Requirements**: TBD${dependsOn}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd-plan-phase ${_newPhaseId} to break down)\n`;
|
||||
|
||||
// Find insertion point: before last "---" or at end
|
||||
let updatedContent;
|
||||
@@ -458,7 +458,7 @@ function cmdPhaseAddBatch(cwd, descriptions, raw) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
fs.writeFileSync(path.join(dirPath, '.gitkeep'), '');
|
||||
const dependsOn = config.phase_naming === 'custom' ? '' : `\n**Depends on:** Phase ${typeof newPhaseId === 'number' ? newPhaseId - 1 : 'TBD'}`;
|
||||
const phaseEntry = `\n### Phase ${newPhaseId}: ${description}\n\n**Goal:** [To be planned]\n**Requirements**: TBD${dependsOn}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd:plan-phase ${newPhaseId} to break down)\n`;
|
||||
const phaseEntry = `\n### Phase ${newPhaseId}: ${description}\n\n**Goal:** [To be planned]\n**Requirements**: TBD${dependsOn}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd-plan-phase ${newPhaseId} to break down)\n`;
|
||||
const lastSeparator = rawContent.lastIndexOf('\n---');
|
||||
rawContent = lastSeparator > 0
|
||||
? rawContent.slice(0, lastSeparator) + phaseEntry + rawContent.slice(lastSeparator)
|
||||
@@ -542,7 +542,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
||||
fs.writeFileSync(path.join(dirPath, '.gitkeep'), '');
|
||||
|
||||
// Build phase entry
|
||||
const phaseEntry = `\n### Phase ${_decimalPhase}: ${description} (INSERTED)\n\n**Goal:** [Urgent work - to be planned]\n**Requirements**: TBD\n**Depends on:** Phase ${afterPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd:plan-phase ${_decimalPhase} to break down)\n`;
|
||||
const phaseEntry = `\n### Phase ${_decimalPhase}: ${description} (INSERTED)\n\n**Goal:** [Urgent work - to be planned]\n**Requirements**: TBD\n**Depends on:** Phase ${afterPhase}\n**Plans:** 0 plans\n\nPlans:\n- [ ] TBD (run /gsd-plan-phase ${_decimalPhase} to break down)\n`;
|
||||
|
||||
// Insert after the target phase section
|
||||
const headerPattern = new RegExp(`(#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:[^\\n]*\\n)`, 'i');
|
||||
@@ -828,7 +828,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
||||
// Update plan count in phase section.
|
||||
// Use direct .replace() rather than replaceInCurrentMilestone() so this
|
||||
// works when the current milestone section is itself inside a <details>
|
||||
// block (the standard /gsd:new-project layout). replaceInCurrentMilestone
|
||||
// block (the standard /gsd-new-project layout). replaceInCurrentMilestone
|
||||
// scopes to content after the last </details>, which misses content inside
|
||||
// the current milestone's own <details> wrapper (#2005).
|
||||
// The phase-scoped heading pattern is specific enough to avoid matching
|
||||
|
||||
@@ -173,7 +173,7 @@ const CLAUDE_INSTRUCTIONS = {
|
||||
};
|
||||
|
||||
const CLAUDE_MD_FALLBACKS = {
|
||||
project: 'Project not yet initialized. Run /gsd:new-project to set up.',
|
||||
project: 'Project not yet initialized. Run /gsd-new-project to set up.',
|
||||
stack: 'Technology stack not yet documented. Will populate after codebase mapping or first phase.',
|
||||
conventions: 'Conventions not yet established. Will populate as patterns emerge during development.',
|
||||
architecture: 'Architecture not yet mapped. Follow existing patterns found in the codebase.',
|
||||
@@ -187,9 +187,9 @@ const CLAUDE_MD_WORKFLOW_ENFORCEMENT = [
|
||||
'Before using Edit, Write, or other file-changing tools, start work through a GSD command so planning artifacts and execution context stay in sync.',
|
||||
'',
|
||||
'Use these entry points:',
|
||||
'- `/gsd:quick` for small fixes, doc updates, and ad-hoc tasks',
|
||||
'- `/gsd:debug` for investigation and bug fixing',
|
||||
'- `/gsd:execute-phase` for planned phase work',
|
||||
'- `/gsd-quick` for small fixes, doc updates, and ad-hoc tasks',
|
||||
'- `/gsd-debug` for investigation and bug fixing',
|
||||
'- `/gsd-execute-phase` for planned phase work',
|
||||
'',
|
||||
'Do not make direct repo edits outside a GSD workflow unless the user explicitly asks to bypass it.',
|
||||
].join('\n');
|
||||
@@ -198,7 +198,7 @@ const CLAUDE_MD_PROFILE_PLACEHOLDER = [
|
||||
'<!-- GSD:profile-start -->',
|
||||
'## Developer Profile',
|
||||
'',
|
||||
'> Profile not yet configured. Run `/gsd:profile-user` to generate your developer profile.',
|
||||
'> Profile not yet configured. Run `/gsd-profile-user` to generate your developer profile.',
|
||||
'> This section is managed by `generate-claude-profile` -- do not edit manually.',
|
||||
'<!-- GSD:profile-end -->',
|
||||
].join('\n');
|
||||
@@ -768,7 +768,7 @@ function cmdGenerateDevPreferences(cwd, options, raw) {
|
||||
|
||||
let stackBlock;
|
||||
if (analysis.data_source === 'questionnaire') {
|
||||
stackBlock = 'Stack preferences not available (questionnaire-only profile). Run `/gsd:profile-user --refresh` with session data to populate.';
|
||||
stackBlock = 'Stack preferences not available (questionnaire-only profile). Run `/gsd-profile-user --refresh` with session data to populate.';
|
||||
} else if (options.stack) {
|
||||
stackBlock = options.stack;
|
||||
} else {
|
||||
@@ -854,7 +854,7 @@ function cmdGenerateClaudeProfile(cwd, options, raw) {
|
||||
'<!-- GSD:profile-start -->',
|
||||
'## Developer Profile',
|
||||
'',
|
||||
`> Generated by GSD from ${dataSource}. Run \`/gsd:profile-user --refresh\` to update.`,
|
||||
`> Generated by GSD from ${dataSource}. Run \`/gsd-profile-user --refresh\` to update.`,
|
||||
'',
|
||||
'| Dimension | Rating | Confidence |',
|
||||
'|-----------|--------|------------|',
|
||||
@@ -1053,7 +1053,7 @@ function cmdGenerateClaudeMd(cwd, options, raw) {
|
||||
let message = `Generated ${genCount}/${totalManaged} sections.`;
|
||||
if (sectionsFallback.length > 0) message += ` Fallback: ${sectionsFallback.join(', ')}.`;
|
||||
if (sectionsSkipped.length > 0) message += ` Skipped (manually edited): ${sectionsSkipped.join(', ')}.`;
|
||||
if (profileStatus === 'placeholder_added') message += ' Run /gsd:profile-user to unlock Developer Profile.';
|
||||
if (profileStatus === 'placeholder_added') message += ' Run /gsd-profile-user to unlock Developer Profile.';
|
||||
|
||||
const result = {
|
||||
claude_md_path: outputPath,
|
||||
|
||||
33
get-shit-done/bin/lib/secrets.cjs
Normal file
33
get-shit-done/bin/lib/secrets.cjs
Normal file
@@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Secrets handling — masking convention for API keys and other
|
||||
* credentials managed via /gsd-settings-integrations.
|
||||
*
|
||||
* Convention: strings 8+ chars long render as `****<last-4>`; shorter
|
||||
* strings render as `****` with no tail (to avoid leaking a meaningful
|
||||
* fraction of a short secret). null/empty renders as `(unset)`.
|
||||
*
|
||||
* Keys considered sensitive are listed in SECRET_CONFIG_KEYS and matched
|
||||
* at the exact key-path level. The list is intentionally narrow — these
|
||||
* are the fields documented as secrets in docs/CONFIGURATION.md.
|
||||
*/
|
||||
|
||||
const SECRET_CONFIG_KEYS = new Set([
|
||||
'brave_search',
|
||||
'firecrawl',
|
||||
'exa_search',
|
||||
]);
|
||||
|
||||
function isSecretKey(keyPath) {
|
||||
return SECRET_CONFIG_KEYS.has(keyPath);
|
||||
}
|
||||
|
||||
function maskSecret(value) {
|
||||
if (value === null || value === undefined || value === '') return '(unset)';
|
||||
const s = String(value);
|
||||
if (s.length < 8) return '****';
|
||||
return '****' + s.slice(-4);
|
||||
}
|
||||
|
||||
module.exports = { SECRET_CONFIG_KEYS, isSecretKey, maskSecret };
|
||||
@@ -1253,6 +1253,70 @@ function cmdStatePlannedPhase(cwd, phaseNumber, planCount, raw) {
|
||||
output({ updated, phase: phaseNumber, plan_count: planCount }, raw, updated.length > 0 ? 'true' : 'false');
|
||||
}
|
||||
|
||||
/**
|
||||
* Bug #2630: reset STATE.md for a new milestone cycle.
|
||||
* Stomps frontmatter milestone/milestone_name/status/progress AND rewrites
|
||||
* the Current Position body. Preserves Accumulated Context.
|
||||
* Symmetric with the SDK `stateMilestoneSwitch` handler.
|
||||
*/
|
||||
function cmdStateMilestoneSwitch(cwd, version, name, raw) {
|
||||
if (!version || !String(version).trim()) {
|
||||
output({ error: 'milestone required (--milestone <vX.Y>)' }, raw);
|
||||
return;
|
||||
}
|
||||
const resolvedName = (name && String(name).trim()) || 'milestone';
|
||||
const statePath = planningPaths(cwd).state;
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
const lockPath = acquireStateLock(statePath);
|
||||
try {
|
||||
const content = fs.existsSync(statePath) ? fs.readFileSync(statePath, 'utf-8') : '';
|
||||
const existingFm = extractFrontmatter(content);
|
||||
const body = stripFrontmatter(content);
|
||||
|
||||
const positionPattern = /(##\s*Current Position\s*\n)([\s\S]*?)(?=\n##|$)/i;
|
||||
const resetPositionBody =
|
||||
`\nPhase: Not started (defining requirements)\n` +
|
||||
`Plan: —\n` +
|
||||
`Status: Defining requirements\n` +
|
||||
`Last activity: ${today} — Milestone ${version} started\n\n`;
|
||||
let newBody;
|
||||
if (positionPattern.test(body)) {
|
||||
newBody = body.replace(positionPattern, (_m, header) => `${header}${resetPositionBody}`);
|
||||
} else {
|
||||
const preface = body.trim().length > 0 ? body : '# Project State\n';
|
||||
newBody = `${preface.trimEnd()}\n\n## Current Position\n${resetPositionBody}`;
|
||||
}
|
||||
|
||||
const fm = {
|
||||
gsd_state_version: existingFm.gsd_state_version || '1.0',
|
||||
milestone: version,
|
||||
milestone_name: resolvedName,
|
||||
status: 'planning',
|
||||
last_updated: new Date().toISOString(),
|
||||
last_activity: today,
|
||||
progress: {
|
||||
total_phases: 0,
|
||||
completed_phases: 0,
|
||||
total_plans: 0,
|
||||
completed_plans: 0,
|
||||
percent: 0,
|
||||
},
|
||||
};
|
||||
|
||||
const yamlStr = reconstructFrontmatter(fm);
|
||||
const assembled = `---\n${yamlStr}\n---\n\n${newBody.replace(/^\n+/, '')}`;
|
||||
atomicWriteFileSync(statePath, normalizeMd(assembled), 'utf-8');
|
||||
output(
|
||||
{ switched: true, version, name: resolvedName, status: 'planning' },
|
||||
raw,
|
||||
'true',
|
||||
);
|
||||
} finally {
|
||||
releaseStateLock(lockPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gate 1: Validate STATE.md against filesystem.
|
||||
* Returns { valid, warnings, drift } JSON.
|
||||
@@ -1644,6 +1708,7 @@ module.exports = {
|
||||
cmdStateValidate,
|
||||
cmdStateSync,
|
||||
cmdStatePrune,
|
||||
cmdStateMilestoneSwitch,
|
||||
cmdSignalWaiting,
|
||||
cmdSignalResume,
|
||||
};
|
||||
|
||||
@@ -555,7 +555,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
||||
|
||||
// ─── Check 1: .planning/ exists ───────────────────────────────────────────
|
||||
if (!fs.existsSync(planBase)) {
|
||||
addIssue('error', 'E001', '.planning/ directory not found', 'Run /gsd:new-project to initialize');
|
||||
addIssue('error', 'E001', '.planning/ directory not found', 'Run /gsd-new-project to initialize');
|
||||
output({
|
||||
status: 'broken',
|
||||
errors,
|
||||
@@ -568,7 +568,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
||||
|
||||
// ─── Check 2: PROJECT.md exists and has required sections ─────────────────
|
||||
if (!fs.existsSync(projectPath)) {
|
||||
addIssue('error', 'E002', 'PROJECT.md not found', 'Run /gsd:new-project to create');
|
||||
addIssue('error', 'E002', 'PROJECT.md not found', 'Run /gsd-new-project to create');
|
||||
} else {
|
||||
const content = fs.readFileSync(projectPath, 'utf-8');
|
||||
const requiredSections = ['## What This Is', '## Core Value', '## Requirements'];
|
||||
@@ -581,39 +581,68 @@ function cmdValidateHealth(cwd, options, raw) {
|
||||
|
||||
// ─── Check 3: ROADMAP.md exists ───────────────────────────────────────────
|
||||
if (!fs.existsSync(roadmapPath)) {
|
||||
addIssue('error', 'E003', 'ROADMAP.md not found', 'Run /gsd:new-milestone to create roadmap');
|
||||
addIssue('error', 'E003', 'ROADMAP.md not found', 'Run /gsd-new-milestone to create roadmap');
|
||||
}
|
||||
|
||||
// ─── Check 4: STATE.md exists and references valid phases ─────────────────
|
||||
if (!fs.existsSync(statePath)) {
|
||||
addIssue('error', 'E004', 'STATE.md not found', 'Run /gsd:health --repair to regenerate', true);
|
||||
addIssue('error', 'E004', 'STATE.md not found', 'Run /gsd-health --repair to regenerate', true);
|
||||
repairs.push('regenerateState');
|
||||
} else {
|
||||
const stateContent = fs.readFileSync(statePath, 'utf-8');
|
||||
// Extract phase references from STATE.md
|
||||
const phaseRefs = [...stateContent.matchAll(/[Pp]hase\s+(\d+(?:\.\d+)*)/g)].map(m => m[1]);
|
||||
// Get disk phases
|
||||
const diskPhases = new Set();
|
||||
const phaseRefs = [...stateContent.matchAll(/[Pp]hase\s+(\d+[A-Z]?(?:\.\d+)*)/g)].map(m => m[1]);
|
||||
// Bug #2633 — ROADMAP.md is the authority for which phases are valid.
|
||||
// STATE.md may legitimately reference current-milestone future phases
|
||||
// (not yet materialized on disk) and shipped-milestone history phases
|
||||
// (archived / cleared off disk). Matching only against on-disk dirs
|
||||
// produces false W002 warnings in both cases.
|
||||
const validPhases = new Set();
|
||||
try {
|
||||
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
||||
for (const e of entries) {
|
||||
if (e.isDirectory()) {
|
||||
const m = e.name.match(/^(\d+(?:\.\d+)*)/);
|
||||
if (m) diskPhases.add(m[1]);
|
||||
const m = e.name.match(/^(\d+[A-Z]?(?:\.\d+)*)/);
|
||||
if (m) validPhases.add(m[1]);
|
||||
}
|
||||
}
|
||||
} catch { /* intentionally empty */ }
|
||||
// Union in every phase declared anywhere in ROADMAP.md (current + shipped + backlog).
|
||||
try {
|
||||
if (fs.existsSync(roadmapPath)) {
|
||||
const roadmapRaw = fs.readFileSync(roadmapPath, 'utf-8');
|
||||
const all = [...roadmapRaw.matchAll(/#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)/gi)];
|
||||
for (const m of all) validPhases.add(m[1]);
|
||||
}
|
||||
} catch { /* intentionally empty */ }
|
||||
// Compare canonical full phase tokens. Also accept a leading-zero variant
|
||||
// on the integer prefix only (e.g. "03" matching "3", "03.1" matching
|
||||
// "3.1") so historic STATE.md formatting still validates. Suffix tokens
|
||||
// like "3A" must match exactly — never collapsed to "3".
|
||||
const normalizedValid = new Set();
|
||||
for (const p of validPhases) {
|
||||
normalizedValid.add(p);
|
||||
const dotIdx = p.indexOf('.');
|
||||
const head = dotIdx === -1 ? p : p.slice(0, dotIdx);
|
||||
const tail = dotIdx === -1 ? '' : p.slice(dotIdx);
|
||||
if (/^\d+$/.test(head)) {
|
||||
normalizedValid.add(head.padStart(2, '0') + tail);
|
||||
}
|
||||
}
|
||||
// Check for invalid references
|
||||
for (const ref of phaseRefs) {
|
||||
const normalizedRef = String(parseInt(ref, 10)).padStart(2, '0');
|
||||
if (!diskPhases.has(ref) && !diskPhases.has(normalizedRef) && !diskPhases.has(String(parseInt(ref, 10)))) {
|
||||
// Only warn if phases dir has any content (not just an empty project)
|
||||
if (diskPhases.size > 0) {
|
||||
const dotIdx = ref.indexOf('.');
|
||||
const head = dotIdx === -1 ? ref : ref.slice(0, dotIdx);
|
||||
const tail = dotIdx === -1 ? '' : ref.slice(dotIdx);
|
||||
const padded = /^\d+$/.test(head) ? head.padStart(2, '0') + tail : ref;
|
||||
if (!normalizedValid.has(ref) && !normalizedValid.has(padded)) {
|
||||
// Only warn if we know any valid phases (not just an empty project)
|
||||
if (normalizedValid.size > 0) {
|
||||
addIssue(
|
||||
'warning',
|
||||
'W002',
|
||||
`STATE.md references phase ${ref}, but only phases ${[...diskPhases].sort().join(', ')} exist`,
|
||||
'Review STATE.md manually before changing it; /gsd:health --repair will not overwrite an existing STATE.md for phase mismatches'
|
||||
`STATE.md references phase ${ref}, but only phases ${[...validPhases].sort().join(', ')} are declared`,
|
||||
'Review STATE.md manually before changing it; /gsd-health --repair will not overwrite an existing STATE.md for phase mismatches'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -622,7 +651,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
||||
|
||||
// ─── Check 5: config.json valid JSON + valid schema ───────────────────────
|
||||
if (!fs.existsSync(configPath)) {
|
||||
addIssue('warning', 'W003', 'config.json not found', 'Run /gsd:health --repair to create with defaults', true);
|
||||
addIssue('warning', 'W003', 'config.json not found', 'Run /gsd-health --repair to create with defaults', true);
|
||||
repairs.push('createConfig');
|
||||
} else {
|
||||
try {
|
||||
@@ -634,7 +663,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
||||
addIssue('warning', 'W004', `config.json: invalid model_profile "${parsed.model_profile}"`, `Valid values: ${validProfiles.join(', ')}`);
|
||||
}
|
||||
} catch (err) {
|
||||
addIssue('error', 'E005', `config.json: JSON parse error - ${err.message}`, 'Run /gsd:health --repair to reset to defaults', true);
|
||||
addIssue('error', 'E005', `config.json: JSON parse error - ${err.message}`, 'Run /gsd-health --repair to reset to defaults', true);
|
||||
repairs.push('resetConfig');
|
||||
}
|
||||
}
|
||||
@@ -645,11 +674,11 @@ function cmdValidateHealth(cwd, options, raw) {
|
||||
const configRaw = fs.readFileSync(configPath, 'utf-8');
|
||||
const configParsed = JSON.parse(configRaw);
|
||||
if (configParsed.workflow && configParsed.workflow.nyquist_validation === undefined) {
|
||||
addIssue('warning', 'W008', 'config.json: workflow.nyquist_validation absent (defaults to enabled but agents may skip)', 'Run /gsd:health --repair to add key', true);
|
||||
addIssue('warning', 'W008', 'config.json: workflow.nyquist_validation absent (defaults to enabled but agents may skip)', 'Run /gsd-health --repair to add key', true);
|
||||
if (!repairs.includes('addNyquistKey')) repairs.push('addNyquistKey');
|
||||
}
|
||||
if (configParsed.workflow && configParsed.workflow.ai_integration_phase === undefined) {
|
||||
addIssue('warning', 'W016', 'config.json: workflow.ai_integration_phase absent (defaults to enabled — run /gsd:ai-integration-phase before planning AI system phases)', 'Run /gsd:health --repair to add key', true);
|
||||
addIssue('warning', 'W016', 'config.json: workflow.ai_integration_phase absent (defaults to enabled — run /gsd-ai-integration-phase before planning AI system phases)', 'Run /gsd-health --repair to add key', true);
|
||||
if (!repairs.includes('addAiIntegrationPhaseKey')) repairs.push('addAiIntegrationPhaseKey');
|
||||
}
|
||||
} catch { /* intentionally empty */ }
|
||||
@@ -699,7 +728,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
||||
try {
|
||||
const researchContent = fs.readFileSync(path.join(phasesDir, e.name, researchFile), 'utf-8');
|
||||
if (researchContent.includes('## Validation Architecture')) {
|
||||
addIssue('warning', 'W009', `Phase ${e.name}: has Validation Architecture in RESEARCH.md but no VALIDATION.md`, 'Re-run /gsd:plan-phase with --research to regenerate');
|
||||
addIssue('warning', 'W009', `Phase ${e.name}: has Validation Architecture in RESEARCH.md but no VALIDATION.md`, 'Re-run /gsd-plan-phase with --research to regenerate');
|
||||
}
|
||||
} catch { /* intentionally empty */ }
|
||||
}
|
||||
@@ -792,7 +821,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
||||
if (statusVal !== 'complete' && statusVal !== 'done') {
|
||||
addIssue('warning', 'W011',
|
||||
`STATE.md says current phase is ${statePhase} (status: ${statusVal || 'unknown'}) but ROADMAP.md shows it as [x] complete — state files may be out of sync`,
|
||||
'Run /gsd:progress to re-derive current position, or manually update STATE.md');
|
||||
'Run /gsd-progress to re-derive current position, or manually update STATE.md');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -895,7 +924,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
||||
if (missingFromRegistry.length > 0) {
|
||||
addIssue('warning', 'W018',
|
||||
`MILESTONES.md missing ${missingFromRegistry.length} archived milestone(s): ${missingFromRegistry.join(', ')}`,
|
||||
'Run /gsd:health --backfill to synthesize missing entries from archive snapshots',
|
||||
'Run /gsd-health --backfill to synthesize missing entries from archive snapshots',
|
||||
true);
|
||||
repairs.push('backfillMilestones');
|
||||
}
|
||||
@@ -969,7 +998,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
||||
stateContent += `**Current phase:** (determining...)\n`;
|
||||
stateContent += `**Status:** Resuming\n\n`;
|
||||
stateContent += `## Session Log\n\n`;
|
||||
stateContent += `- ${new Date().toISOString().split('T')[0]}: STATE.md regenerated by /gsd:health --repair\n`;
|
||||
stateContent += `- ${new Date().toISOString().split('T')[0]}: STATE.md regenerated by /gsd-health --repair\n`;
|
||||
writeStateMd(statePath, stateContent, cwd);
|
||||
repairActions.push({ action: repair, success: true, path: 'STATE.md' });
|
||||
break;
|
||||
@@ -1019,7 +1048,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
||||
// Build minimal entry from snapshot title or version
|
||||
const titleMatch = snapshot && snapshot.match(/^#\s+(.+)$/m);
|
||||
const milestoneName = titleMatch ? titleMatch[1].replace(/^Milestone\s+/i, '').replace(/^v[\d.]+\s*/, '').trim() : ver;
|
||||
const entry = `## ${ver}${milestoneName && milestoneName !== ver ? ` ${milestoneName}` : ''} (Backfilled: ${today})\n\n**Note:** Synthesized from archive snapshot by \`/gsd:health --backfill\`. Original completion date unknown.\n\n---\n\n`;
|
||||
const entry = `## ${ver}${milestoneName && milestoneName !== ver ? ` ${milestoneName}` : ''} (Backfilled: ${today})\n\n**Note:** Synthesized from archive snapshot by \`/gsd-health --backfill\`. Original completion date unknown.\n\n---\n\n`;
|
||||
const milestonesContent = fs.existsSync(milestonesPath)
|
||||
? fs.readFileSync(milestonesPath, 'utf-8')
|
||||
: '';
|
||||
@@ -1169,6 +1198,141 @@ function cmdVerifySchemaDrift(cwd, phaseArg, skipFlag, raw) {
|
||||
}, raw);
|
||||
}
|
||||
|
||||
// ─── Codebase Drift Detection (#2003) ────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Detect structural drift between the committed tree and
|
||||
* `.planning/codebase/STRUCTURE.md`. Non-blocking: any failure returns a
|
||||
* `{ skipped: true }` JSON result with a reason; the command never exits
|
||||
* non-zero so `execute-phase`'s drift gate cannot fail the phase.
|
||||
*/
|
||||
function cmdVerifyCodebaseDrift(cwd, raw) {
|
||||
const drift = require('./drift.cjs');
|
||||
|
||||
const emit = (payload) => output(payload, raw);
|
||||
|
||||
try {
|
||||
const codebaseDir = path.join(planningDir(cwd), 'codebase');
|
||||
const structurePath = path.join(codebaseDir, 'STRUCTURE.md');
|
||||
if (!fs.existsSync(structurePath)) {
|
||||
emit({
|
||||
skipped: true,
|
||||
reason: 'no-structure-md',
|
||||
action_required: false,
|
||||
directive: 'none',
|
||||
elements: [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let structureMd;
|
||||
try {
|
||||
structureMd = fs.readFileSync(structurePath, 'utf-8');
|
||||
} catch (err) {
|
||||
emit({
|
||||
skipped: true,
|
||||
reason: 'cannot-read-structure-md: ' + err.message,
|
||||
action_required: false,
|
||||
directive: 'none',
|
||||
elements: [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const lastMapped = drift.readMappedCommit(structurePath);
|
||||
|
||||
// Verify we're inside a git repo and resolve the diff range.
|
||||
const revProbe = execGit(cwd, ['rev-parse', 'HEAD']);
|
||||
if (revProbe.exitCode !== 0) {
|
||||
emit({
|
||||
skipped: true,
|
||||
reason: 'not-a-git-repo',
|
||||
action_required: false,
|
||||
directive: 'none',
|
||||
elements: [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Empty-tree SHA is a stable fallback when no mapping commit is recorded.
|
||||
const EMPTY_TREE = '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
|
||||
let base = lastMapped;
|
||||
if (!base) {
|
||||
base = EMPTY_TREE;
|
||||
} else {
|
||||
// Verify the commit is reachable; if not, fall back to EMPTY_TREE.
|
||||
const verify = execGit(cwd, ['cat-file', '-t', base]);
|
||||
if (verify.exitCode !== 0) base = EMPTY_TREE;
|
||||
}
|
||||
|
||||
const diff = execGit(cwd, ['diff', '--name-status', base, 'HEAD']);
|
||||
if (diff.exitCode !== 0) {
|
||||
emit({
|
||||
skipped: true,
|
||||
reason: 'git-diff-failed',
|
||||
action_required: false,
|
||||
directive: 'none',
|
||||
elements: [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const added = [];
|
||||
const modified = [];
|
||||
const deleted = [];
|
||||
for (const line of diff.stdout.split(/\r?\n/)) {
|
||||
if (!line.trim()) continue;
|
||||
const m = line.match(/^([A-Z])\d*\t(.+?)(?:\t(.+))?$/);
|
||||
if (!m) continue;
|
||||
const status = m[1];
|
||||
// For renames (R), use the new path (m[3] if present, else m[2]).
|
||||
const file = m[3] || m[2];
|
||||
if (status === 'A' || status === 'R' || status === 'C') added.push(file);
|
||||
else if (status === 'M') modified.push(file);
|
||||
else if (status === 'D') deleted.push(file);
|
||||
}
|
||||
|
||||
// Threshold and action read from config, with defaults.
|
||||
const config = loadConfig(cwd);
|
||||
const threshold = Number.isInteger(config?.workflow?.drift_threshold) && config.workflow.drift_threshold >= 1
|
||||
? config.workflow.drift_threshold
|
||||
: 3;
|
||||
const action = config?.workflow?.drift_action === 'auto-remap' ? 'auto-remap' : 'warn';
|
||||
|
||||
const result = drift.detectDrift({
|
||||
addedFiles: added,
|
||||
modifiedFiles: modified,
|
||||
deletedFiles: deleted,
|
||||
structureMd,
|
||||
threshold,
|
||||
action,
|
||||
});
|
||||
|
||||
emit({
|
||||
skipped: !!result.skipped,
|
||||
reason: result.reason || null,
|
||||
action_required: !!result.actionRequired,
|
||||
directive: result.directive,
|
||||
spawn_mapper: !!result.spawnMapper,
|
||||
affected_paths: result.affectedPaths || [],
|
||||
elements: result.elements || [],
|
||||
threshold,
|
||||
action,
|
||||
last_mapped_commit: lastMapped,
|
||||
message: result.message || '',
|
||||
});
|
||||
} catch (err) {
|
||||
// Non-blocking: never bubble up an exception.
|
||||
emit({
|
||||
skipped: true,
|
||||
reason: 'exception: ' + (err && err.message ? err.message : String(err)),
|
||||
action_required: false,
|
||||
directive: 'none',
|
||||
elements: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
cmdVerifySummary,
|
||||
cmdVerifyPlanStructure,
|
||||
@@ -1181,4 +1345,5 @@ module.exports = {
|
||||
cmdValidateHealth,
|
||||
cmdValidateAgents,
|
||||
cmdVerifySchemaDrift,
|
||||
cmdVerifyCodebaseDrift,
|
||||
};
|
||||
|
||||
@@ -78,7 +78,7 @@ function cmdWorkstreamCreate(cwd, name, options, raw) {
|
||||
|
||||
const baseDir = planningRoot(cwd);
|
||||
if (!fs.existsSync(baseDir)) {
|
||||
error('.planning/ directory not found — run /gsd:new-project first');
|
||||
error('.planning/ directory not found — run /gsd-new-project first');
|
||||
}
|
||||
|
||||
const wsRoot = path.join(baseDir, 'workstreams');
|
||||
|
||||
@@ -72,21 +72,21 @@ reads is inert — the consumption mechanism is what gives an artifact meaning.
|
||||
- **Location**: `.planning/spikes/SPIKE-NNN/`
|
||||
- **Consumed by**: Planner when spike is referenced; `pause-work` for spike context handoff
|
||||
|
||||
### Spike README.md / MANIFEST.md (per-spike, via /gsd:spike)
|
||||
### Spike README.md / MANIFEST.md (per-spike, via /gsd-spike)
|
||||
- **Shape**: YAML frontmatter (spike, name, validates, verdict, related, tags) + run instructions + results
|
||||
- **Lifecycle**: Created by `/gsd:spike` → Verified → Wrapped up by `/gsd:spike-wrap-up`
|
||||
- **Lifecycle**: Created by `/gsd-spike` → Verified → Wrapped up by `/gsd-spike-wrap-up`
|
||||
- **Location**: `.planning/spikes/NNN-name/README.md`, `.planning/spikes/MANIFEST.md`
|
||||
- **Consumed by**: `/gsd:spike-wrap-up` for curation; `pause-work` for spike context handoff
|
||||
- **Consumed by**: `/gsd-spike-wrap-up` for curation; `pause-work` for spike context handoff
|
||||
|
||||
### Sketch README.md / MANIFEST.md / index.html (per-sketch)
|
||||
- **Shape**: YAML frontmatter (sketch, name, question, winner, tags) + variants as tabbed HTML
|
||||
- **Lifecycle**: Created by `/gsd:sketch` → Evaluated → Wrapped up by `/gsd:sketch-wrap-up`
|
||||
- **Lifecycle**: Created by `/gsd-sketch` → Evaluated → Wrapped up by `/gsd-sketch-wrap-up`
|
||||
- **Location**: `.planning/sketches/NNN-name/README.md`, `.planning/sketches/NNN-name/index.html`, `.planning/sketches/MANIFEST.md`
|
||||
- **Consumed by**: `/gsd:sketch-wrap-up` for curation; `pause-work` for sketch context handoff
|
||||
- **Consumed by**: `/gsd-sketch-wrap-up` for curation; `pause-work` for sketch context handoff
|
||||
|
||||
### WRAP-UP-SUMMARY.md (per wrap-up session)
|
||||
- **Shape**: Curation results, included/excluded items, feature/design area groupings
|
||||
- **Lifecycle**: Created by `/gsd:spike-wrap-up` or `/gsd:sketch-wrap-up`
|
||||
- **Lifecycle**: Created by `/gsd-spike-wrap-up` or `/gsd-sketch-wrap-up`
|
||||
- **Location**: `.planning/spikes/WRAP-UP-SUMMARY.md` or `.planning/sketches/WRAP-UP-SUMMARY.md`
|
||||
- **Consumed by**: Project history; not read by automated workflows
|
||||
|
||||
|
||||
@@ -50,13 +50,13 @@ Standard format for presenting next steps after completing a command or workflow
|
||||
|
||||
`/clear` then:
|
||||
|
||||
`/gsd:execute-phase 2`
|
||||
`/gsd-execute-phase 2`
|
||||
|
||||
---
|
||||
|
||||
**Also available:**
|
||||
- Review plan before executing
|
||||
- `/gsd:list-phase-assumptions 2` — check assumptions
|
||||
- `/gsd-list-phase-assumptions 2` — check assumptions
|
||||
|
||||
---
|
||||
```
|
||||
@@ -75,7 +75,7 @@ Add note that this is the last plan and what comes after:
|
||||
|
||||
`/clear` then:
|
||||
|
||||
`/gsd:execute-phase 2`
|
||||
`/gsd-execute-phase 2`
|
||||
|
||||
---
|
||||
|
||||
@@ -97,13 +97,13 @@ Add note that this is the last plan and what comes after:
|
||||
|
||||
`/clear` then:
|
||||
|
||||
`/gsd:plan-phase 2`
|
||||
`/gsd-plan-phase 2`
|
||||
|
||||
---
|
||||
|
||||
**Also available:**
|
||||
- `/gsd:discuss-phase 2` — gather context first
|
||||
- `/gsd:research-phase 2` — investigate unknowns
|
||||
- `/gsd-discuss-phase 2` — gather context first
|
||||
- `/gsd-research-phase 2` — investigate unknowns
|
||||
- Review roadmap
|
||||
|
||||
---
|
||||
@@ -126,13 +126,13 @@ Show completion status before next action:
|
||||
|
||||
`/clear` then:
|
||||
|
||||
`/gsd:plan-phase 3`
|
||||
`/gsd-plan-phase 3`
|
||||
|
||||
---
|
||||
|
||||
**Also available:**
|
||||
- `/gsd:discuss-phase 3` — gather context first
|
||||
- `/gsd:research-phase 3` — investigate unknowns
|
||||
- `/gsd-discuss-phase 3` — gather context first
|
||||
- `/gsd-research-phase 3` — investigate unknowns
|
||||
- Review what Phase 2 built
|
||||
|
||||
---
|
||||
@@ -151,11 +151,11 @@ When there's no clear primary action:
|
||||
|
||||
`/clear` then one of:
|
||||
|
||||
**To plan directly:** `/gsd:plan-phase 3`
|
||||
**To plan directly:** `/gsd-plan-phase 3`
|
||||
|
||||
**To discuss context first:** `/gsd:discuss-phase 3`
|
||||
**To discuss context first:** `/gsd-discuss-phase 3`
|
||||
|
||||
**To research unknowns:** `/gsd:research-phase 3`
|
||||
**To research unknowns:** `/gsd-research-phase 3`
|
||||
|
||||
---
|
||||
```
|
||||
@@ -175,7 +175,7 @@ All 4 phases shipped
|
||||
|
||||
`/clear` then:
|
||||
|
||||
`/gsd:new-milestone`
|
||||
`/gsd-new-milestone`
|
||||
|
||||
---
|
||||
```
|
||||
@@ -218,7 +218,7 @@ Extract: `**02-03: Refresh Token Rotation** — Add /api/auth/refresh with slidi
|
||||
## To Continue
|
||||
|
||||
Run `/clear`, then paste:
|
||||
/gsd:execute-phase 2
|
||||
/gsd-execute-phase 2
|
||||
```
|
||||
|
||||
User has no idea what 02-03 is about.
|
||||
@@ -226,7 +226,7 @@ User has no idea what 02-03 is about.
|
||||
### Don't: Missing /clear explanation
|
||||
|
||||
```
|
||||
`/gsd:plan-phase 3`
|
||||
`/gsd-plan-phase 3`
|
||||
|
||||
Run /clear first.
|
||||
```
|
||||
@@ -246,7 +246,7 @@ Sounds like an afterthought. Use "Also available:" instead.
|
||||
|
||||
```
|
||||
```
|
||||
/gsd:plan-phase 3
|
||||
/gsd-plan-phase 3
|
||||
```
|
||||
```
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Doc Conflict Engine
|
||||
|
||||
Shared conflict-detection contract for workflows that ingest external content into `.planning/` (e.g., `/gsd:import`, `/gsd:ingest-docs`). Defines the report format, severity semantics, and safety-gate behavior. The specific checks that populate each severity bucket are workflow-specific and defined by the calling workflow.
|
||||
Shared conflict-detection contract for workflows that ingest external content into `.planning/` (e.g., `/gsd-import`, `/gsd-ingest-docs`). Defines the report format, severity semantics, and safety-gate behavior. The specific checks that populate each severity bucket are workflow-specific and defined by the calling workflow.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Domain-Aware Probing Patterns
|
||||
|
||||
Shared reference for `/gsd-begin`, `/gsd:discuss-phase`, and domain exploration workflows.
|
||||
Shared reference for `/gsd-begin`, `/gsd-discuss-phase`, and domain exploration workflows.
|
||||
|
||||
When the user mentions a technology area, use these probes to ask insightful follow-up questions. Don't run through them as a checklist -- pick the 2-3 most relevant based on context. The goal is to surface hidden assumptions and trade-offs the user may not have considered yet.
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ Simple 2-option confirmation for re-planning, rebuild, replace plans, commit.
|
||||
4-option escalation for review escalation (max retries exceeded).
|
||||
- question: "Phase {N} has failed verification {attempt} times. How should we proceed?"
|
||||
- header: "Escalate"
|
||||
- options: Accept gaps | Re-plan (via /gsd:plan-phase) | Debug (via /gsd:debug) | Retry
|
||||
- options: Accept gaps | Re-plan (via /gsd-plan-phase) | Debug (via /gsd-debug) | Retry
|
||||
|
||||
## Pattern: multi-option-gaps
|
||||
4-option gap handler for review gaps-found.
|
||||
@@ -78,7 +78,7 @@ Up to 4 suggested next actions with selection (status, resume workflows).
|
||||
3-option confirmation for quick task scope validation.
|
||||
- question: "This task looks complex. Proceed as quick task or use full planning?"
|
||||
- header: "Scope"
|
||||
- options: Quick task | Full plan (via /gsd:plan-phase) | Revise
|
||||
- options: Quick task | Full plan (via /gsd-plan-phase) | Revise
|
||||
|
||||
## Pattern: depth-select
|
||||
3-option depth selection for planning workflow preferences.
|
||||
|
||||
@@ -38,7 +38,7 @@ Canonical gate types used across GSD workflows. Every validation checkpoint maps
|
||||
**Recovery:** Developer investigates root cause, fixes, restarts from checkpoint.
|
||||
**Examples:**
|
||||
- Context window critically low during execution
|
||||
- STATE.md in error state blocking /gsd:next
|
||||
- STATE.md in error state blocking /gsd-next
|
||||
- Verification finds critical missing deliverables
|
||||
|
||||
---
|
||||
|
||||
@@ -274,7 +274,7 @@ Set `commit_docs: false` so planning docs stay local and are not committed to an
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Auto-detection:** During `/gsd:new-project`, directories with their own `.git` folder are detected and offered for selection as sub-repos. On subsequent runs, `loadConfig` auto-syncs the `sub_repos` list with the filesystem — adding newly created repos and removing deleted ones. This means `config.json` may be rewritten automatically when repos change on disk.
|
||||
1. **Auto-detection:** During `/gsd-new-project`, directories with their own `.git` folder are detected and offered for selection as sub-repos. On subsequent runs, `loadConfig` auto-syncs the `sub_repos` list with the filesystem — adding newly created repos and removing deleted ones. This means `config.json` may be rewritten automatically when repos change on disk.
|
||||
2. **File grouping:** Code files are grouped by their sub-repo prefix (e.g., `backend/src/api/users.ts` belongs to the `backend/` repo).
|
||||
3. **Independent commits:** Each sub-repo receives its own atomic commit via `gsd-tools.cjs commit-to-subrepo`. File paths are made relative to the sub-repo root before staging.
|
||||
4. **Planning stays local:** The `.planning/` directory is not committed; it acts as cross-repo coordination.
|
||||
|
||||
@@ -75,7 +75,7 @@ If you're using Claude Code with OpenRouter, a local model, or any non-Anthropic
|
||||
|
||||
```bash
|
||||
# Via settings command
|
||||
/gsd:settings
|
||||
/gsd-settings
|
||||
# → Select "Inherit" for model profile
|
||||
|
||||
# Or manually in .planning/config.json
|
||||
@@ -115,7 +115,7 @@ Overrides take precedence over the profile. Valid values: `opus`, `sonnet`, `hai
|
||||
|
||||
## Switching Profiles
|
||||
|
||||
Runtime: `/gsd:set-profile <profile>`
|
||||
Runtime: `/gsd-set-profile <profile>`
|
||||
|
||||
Per-project default: Set in `.planning/config.json`:
|
||||
```json
|
||||
|
||||
@@ -36,7 +36,7 @@ Configuration options for `.planning/` directory behavior.
|
||||
| `workflow.use_worktrees` | `true` | Whether executor agents run in isolated git worktrees. Set to `false` to disable worktrees — agents execute sequentially on the main working tree instead. Recommended for solo developers or when worktree merges cause issues. |
|
||||
| `workflow.subagent_timeout` | `300000` | Timeout in milliseconds for parallel subagent tasks (e.g. codebase mapping). Increase for large codebases or slower models. Default: 300000 (5 minutes). |
|
||||
| `workflow.inline_plan_threshold` | `2` | Plans with this many tasks or fewer execute inline (Pattern C) instead of spawning a subagent. Avoids ~14K token spawn overhead for small plans. Set to `0` to always spawn subagents. |
|
||||
| `manager.flags.discuss` | `""` | Flags passed to `/gsd:discuss-phase` when dispatched from manager (e.g. `"--auto --analyze"`) |
|
||||
| `manager.flags.discuss` | `""` | Flags passed to `/gsd-discuss-phase` when dispatched from manager (e.g. `"--auto --analyze"`) |
|
||||
| `manager.flags.plan` | `""` | Flags passed to plan workflow when dispatched from manager |
|
||||
| `manager.flags.execute` | `""` | Flags passed to execute workflow when dispatched from manager |
|
||||
| `response_language` | `null` | Language for user-facing questions and prompts across all phases/subagents (e.g. `"Portuguese"`, `"Japanese"`, `"Spanish"`). When set, all spawned agents include a directive to respond in this language. |
|
||||
@@ -236,7 +236,7 @@ Generated from `CONFIG_DEFAULTS` (core.cjs) and `VALID_CONFIG_KEYS` (config.cjs)
|
||||
| `context_window` | number | `200000` | `200000`, `1000000` | Context window size; set `1000000` for 1M-context models |
|
||||
| `resolve_model_ids` | boolean\|string | `false` | `false`, `true`, `"omit"` | Map model aliases to full Claude IDs; `"omit"` returns empty string |
|
||||
| `context` | string\|null | `null` | `"dev"`, `"research"`, `"review"` | Execution context profile that adjusts agent behavior: `"dev"` for development tasks, `"research"` for investigation/exploration, `"review"` for code review workflows |
|
||||
| `review.models.<cli>` | string\|null | `null` | Any model ID string | Per-CLI model override for /gsd:review (e.g., `review.models.gemini`). Falls back to CLI default when null. |
|
||||
| `review.models.<cli>` | string\|null | `null` | Any model ID string | Per-CLI model override for /gsd-review (e.g., `review.models.gemini`). Falls back to CLI default when null. |
|
||||
|
||||
### Workflow Fields
|
||||
|
||||
@@ -252,7 +252,7 @@ Set via `workflow.*` namespace in config.json (e.g., `"workflow": { "research":
|
||||
| `workflow.auto_advance` | boolean | `false` | `true`, `false` | Auto-advance to next phase after completion |
|
||||
| `workflow.node_repair` | boolean | `true` | `true`, `false` | Attempt automatic repair of failed plan nodes |
|
||||
| `workflow.node_repair_budget` | number | `2` | Any positive integer | Max repair retries per failed node |
|
||||
| `workflow.ai_integration_phase` | boolean | `true` | `true`, `false` | Run /gsd:ai-integration-phase before planning AI system phases |
|
||||
| `workflow.ai_integration_phase` | boolean | `true` | `true`, `false` | Run /gsd-ai-integration-phase before planning AI system phases |
|
||||
| `workflow.ui_phase` | boolean | `true` | `true`, `false` | Generate UI-SPEC.md for frontend phases |
|
||||
| `workflow.ui_safety_gate` | boolean | `true` | `true`, `false` | Require safety gate approval for UI changes |
|
||||
| `workflow.text_mode` | boolean | `false` | `true`, `false` | Use plain-text numbered lists instead of AskUserQuestion menus |
|
||||
@@ -265,9 +265,10 @@ Set via `workflow.*` namespace in config.json (e.g., `"workflow": { "research":
|
||||
| `workflow.code_review` | boolean | `true` | `true`, `false` | Enable built-in code review step in the ship workflow |
|
||||
| `workflow.code_review_depth` | string | `"standard"` | `"light"`, `"standard"`, `"deep"` | Depth level for code review analysis in the ship workflow |
|
||||
| `workflow._auto_chain_active` | boolean | `false` | `true`, `false` | Internal: tracks whether autonomous chaining is active |
|
||||
| `workflow.security_enforcement` | boolean | `true` | `true`, `false` | Enable threat-model-anchored security verification via `/gsd:secure-phase`. When `false`, security checks are skipped entirely |
|
||||
| `workflow.security_enforcement` | boolean | `true` | `true`, `false` | Enable threat-model-anchored security verification via `/gsd-secure-phase`. When `false`, security checks are skipped entirely |
|
||||
| `workflow.security_asvs_level` | number | `1` | `1`, `2`, `3` | OWASP ASVS verification level. Level 1 = opportunistic, Level 2 = standard, Level 3 = comprehensive |
|
||||
| `workflow.security_block_on` | string | `"high"` | `"high"`, `"medium"`, `"low"` | Minimum severity that blocks phase advancement |
|
||||
| `workflow.post_planning_gaps` | boolean | `true` | `true`, `false` | Post-planning gap report (#2493). After plans are generated, scans REQUIREMENTS.md and CONTEXT.md `<decisions>` against all PLAN.md files and emits a unified `Source \| Item \| Status` table. Non-blocking. Set to `false` to skip Step 13e of plan-phase. _Alias:_ `post_planning_gaps` is the flat-key form used in `CONFIG_DEFAULTS`; `workflow.post_planning_gaps` is the canonical namespaced form. |
|
||||
|
||||
### Git Fields
|
||||
|
||||
@@ -318,11 +319,11 @@ Set via `learnings.*` namespace (e.g., `"learnings": { "max_inject": 5 }`). Used
|
||||
|
||||
### Intel Fields
|
||||
|
||||
Set via `intel.*` namespace (e.g., `"intel": { "enabled": true }`). Controls the queryable codebase intelligence system consumed by `/gsd:intel`.
|
||||
Set via `intel.*` namespace (e.g., `"intel": { "enabled": true }`). Controls the queryable codebase intelligence system consumed by `/gsd-intel`.
|
||||
|
||||
| Key | Type | Default | Allowed Values | Description |
|
||||
|-----|------|---------|----------------|-------------|
|
||||
| `intel.enabled` | boolean | `false` | `true`, `false` | Enable queryable codebase intelligence system. When `true`, `/gsd:intel` commands build and query a JSON index in `.planning/intel/`. |
|
||||
| `intel.enabled` | boolean | `false` | `true`, `false` | Enable queryable codebase intelligence system. When `true`, `/gsd-intel` commands build and query a JSON index in `.planning/intel/`. |
|
||||
|
||||
### Manager Fields
|
||||
|
||||
@@ -330,7 +331,7 @@ Set via `manager.*` namespace (e.g., `"manager": { "flags": { "discuss": "--auto
|
||||
|
||||
| Key | Type | Default | Allowed Values | Description |
|
||||
|-----|------|---------|----------------|-------------|
|
||||
| `manager.flags.discuss` | string | `""` | Any CLI flags string | Flags passed to `/gsd:discuss-phase` from manager (e.g., `"--auto --analyze"`) |
|
||||
| `manager.flags.discuss` | string | `""` | Any CLI flags string | Flags passed to `/gsd-discuss-phase` from manager (e.g., `"--auto --analyze"`) |
|
||||
| `manager.flags.plan` | string | `""` | Any CLI flags string | Flags passed to plan workflow from manager |
|
||||
| `manager.flags.execute` | string | `""` | Any CLI flags string | Flags passed to execute workflow from manager |
|
||||
|
||||
|
||||
51
get-shit-done/references/scout-codebase.md
Normal file
51
get-shit-done/references/scout-codebase.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Codebase scout — map selection table
|
||||
|
||||
> Lazy-loaded reference for the `scout_codebase` step in
|
||||
> `workflows/discuss-phase.md` (extracted via #2551 progressive-disclosure
|
||||
> refactor). Read this only when prior `.planning/codebase/*.md` maps exist
|
||||
> and the workflow needs to pick which 2–3 to load.
|
||||
|
||||
## Phase-type → recommended maps
|
||||
|
||||
Read 2–3 maps based on inferred phase type. Do NOT read all seven —
|
||||
that inflates context without improving discussion quality.
|
||||
|
||||
| Phase type (infer from title + ROADMAP entry) | Read these maps |
|
||||
|---|---|
|
||||
| UI / frontend / styling / design | CONVENTIONS.md, STRUCTURE.md, STACK.md |
|
||||
| Backend / API / service / data model | STACK.md, ARCHITECTURE.md, INTEGRATIONS.md |
|
||||
| Integration / third-party / provider | STACK.md, INTEGRATIONS.md, ARCHITECTURE.md |
|
||||
| Infrastructure / DevOps / CI / deploy | STACK.md, ARCHITECTURE.md, INTEGRATIONS.md |
|
||||
| Testing / QA / coverage | TESTING.md, CONVENTIONS.md, STRUCTURE.md |
|
||||
| Documentation / content | CONVENTIONS.md, STRUCTURE.md |
|
||||
| Mixed / unclear | STACK.md, ARCHITECTURE.md, CONVENTIONS.md |
|
||||
|
||||
Read CONCERNS.md only if the phase explicitly addresses known concerns or
|
||||
security issues.
|
||||
|
||||
## Single-read rule
|
||||
|
||||
Read each map file in a **single** Read call. Do not read the same file at
|
||||
two different offsets — split reads break prompt-cache reuse and cost more
|
||||
than a single full read.
|
||||
|
||||
## No-maps fallback
|
||||
|
||||
If `.planning/codebase/*.md` does not exist:
|
||||
1. Extract key terms from the phase goal (e.g., "feed" → "post", "card",
|
||||
"list"; "auth" → "login", "session", "token")
|
||||
2. `grep -rlE "{term1}|{term2}" src/ app/ --include="*.ts" ...` (use `-E`
|
||||
for extended regex so the `|` alternation works on both GNU grep and BSD
|
||||
grep / macOS), and `ls` the conventional component/hook/util dirs
|
||||
3. Read the 3–5 most relevant files
|
||||
|
||||
## Output (internal `<codebase_context>`)
|
||||
|
||||
From the scan, identify:
|
||||
- **Reusable assets** — components, hooks, utilities usable in this phase
|
||||
- **Established patterns** — state management, styling, data fetching
|
||||
- **Integration points** — routes, nav, providers where new code connects
|
||||
- **Creative options** — approaches the architecture enables or constrains
|
||||
|
||||
Used in `analyze_phase` and `present_gray_areas`. NOT written to a file —
|
||||
session-only.
|
||||
@@ -69,7 +69,7 @@ Apply this recommendation to the revision? [Yes] / [No, let me decide]
|
||||
### 3. Explore — Approach Comparison (requires #1729)
|
||||
|
||||
**When:** During Socratic conversation, when multiple viable approaches emerge.
|
||||
**Note:** This integration point will be added when /gsd:explore (#1729) lands.
|
||||
**Note:** This integration point will be added when /gsd-explore (#1729) lands.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -163,10 +163,10 @@ Then re-run verification to apply.
|
||||
|
||||
Overrides can also be managed through the verification workflow:
|
||||
|
||||
1. Run `/gsd:verify-work` — verification finds gaps
|
||||
1. Run `/gsd-verify-work` — verification finds gaps
|
||||
2. Review gaps — determine which are intentional deviations
|
||||
3. Add override entries to VERIFICATION.md frontmatter
|
||||
4. Re-run `/gsd:verify-work` — overrides are applied, remaining gaps shown
|
||||
4. Re-run `/gsd-verify-work` — overrides are applied, remaining gaps shown
|
||||
|
||||
</creating_overrides>
|
||||
|
||||
@@ -183,7 +183,7 @@ When a phase is re-verified (e.g., after gap closure):
|
||||
|
||||
### At Milestone Completion
|
||||
|
||||
During `/gsd:audit-milestone`, overrides are surfaced in the audit report:
|
||||
During `/gsd-audit-milestone`, overrides are surfaced in the audit report:
|
||||
|
||||
```
|
||||
### Verification Overrides ({count} across {phase_count} phases)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# AI-SPEC — Phase {N}: {phase_name}
|
||||
|
||||
> AI design contract generated by `/gsd:ai-integration-phase`. Consumed by `gsd-planner` and `gsd-eval-auditor`.
|
||||
> AI design contract generated by `/gsd-ai-integration-phase`. Consumed by `gsd-planner` and `gsd-eval-auditor`.
|
||||
> Locks framework selection, implementation guidance, and evaluation strategy before planning begins.
|
||||
|
||||
---
|
||||
|
||||
@@ -104,7 +104,7 @@ files_changed: []
|
||||
|
||||
<lifecycle>
|
||||
|
||||
**Creation:** Immediately when /gsd:debug is called
|
||||
**Creation:** Immediately when /gsd-debug is called
|
||||
- Create file with trigger from user input
|
||||
- Set status to "gathering"
|
||||
- Current Focus: next_action = "gather symptoms"
|
||||
|
||||
@@ -12,24 +12,24 @@ These files live directly at `.planning/` — not inside phase subdirectories.
|
||||
|
||||
| File | Template | Produced by | Purpose |
|
||||
|------|----------|-------------|---------|
|
||||
| `PROJECT.md` | `project.md` | `/gsd:new-project` | Project identity, goals, requirements summary |
|
||||
| `ROADMAP.md` | `roadmap.md` | `/gsd:new-milestone`, `/gsd:new-project` | Phase plan with milestones and progress tracking |
|
||||
| `STATE.md` | `state.md` | `/gsd:new-project`, `/gsd:health --repair` | Current session state, active phase, last activity |
|
||||
| `REQUIREMENTS.md` | `requirements.md` | `/gsd:new-milestone` | Functional requirements with traceability |
|
||||
| `MILESTONES.md` | `milestone.md` | `/gsd:complete-milestone` | Log of completed milestones with accomplishments |
|
||||
| `BACKLOG.md` | *(inline)* | `/gsd:add-backlog` | Pending ideas and deferred work |
|
||||
| `LEARNINGS.md` | *(inline)* | `/gsd-extract-learnings`, `/gsd:execute-phase` | Phase retrospective learnings for future plans |
|
||||
| `THREADS.md` | *(inline)* | `/gsd:thread` | Persistent discussion threads |
|
||||
| `config.json` | `config.json` | `/gsd:new-project`, `/gsd:health --repair` | Project-specific GSD configuration |
|
||||
| `PROJECT.md` | `project.md` | `/gsd-new-project` | Project identity, goals, requirements summary |
|
||||
| `ROADMAP.md` | `roadmap.md` | `/gsd-new-milestone`, `/gsd-new-project` | Phase plan with milestones and progress tracking |
|
||||
| `STATE.md` | `state.md` | `/gsd-new-project`, `/gsd-health --repair` | Current session state, active phase, last activity |
|
||||
| `REQUIREMENTS.md` | `requirements.md` | `/gsd-new-milestone` | Functional requirements with traceability |
|
||||
| `MILESTONES.md` | `milestone.md` | `/gsd-complete-milestone` | Log of completed milestones with accomplishments |
|
||||
| `BACKLOG.md` | *(inline)* | `/gsd-add-backlog` | Pending ideas and deferred work |
|
||||
| `LEARNINGS.md` | *(inline)* | `/gsd-extract-learnings`, `/gsd-execute-phase` | Phase retrospective learnings for future plans |
|
||||
| `THREADS.md` | *(inline)* | `/gsd-thread` | Persistent discussion threads |
|
||||
| `config.json` | `config.json` | `/gsd-new-project`, `/gsd-health --repair` | Project-specific GSD configuration |
|
||||
| `CLAUDE.md` | `claude-md.md` | `/gsd-profile` | Auto-assembled Claude Code context file |
|
||||
|
||||
### Version-stamped artifacts (pattern: `vX.Y-*.md`)
|
||||
|
||||
| Pattern | Produced by | Purpose |
|
||||
|---------|-------------|---------|
|
||||
| `vX.Y-MILESTONE-AUDIT.md` | `/gsd:audit-milestone` | Milestone audit report before archiving |
|
||||
| `vX.Y-MILESTONE-AUDIT.md` | `/gsd-audit-milestone` | Milestone audit report before archiving |
|
||||
|
||||
These files are archived to `.planning/milestones/` by `/gsd:complete-milestone`. Finding them at the `.planning/` root after completion indicates the archive step was skipped.
|
||||
These files are archived to `.planning/milestones/` by `/gsd-complete-milestone`. Finding them at the `.planning/` root after completion indicates the archive step was skipped.
|
||||
|
||||
---
|
||||
|
||||
@@ -39,24 +39,24 @@ These files live inside a phase directory. They are NOT checked by W019 (which o
|
||||
|
||||
| File Pattern | Template | Produced by | Purpose |
|
||||
|-------------|----------|-------------|---------|
|
||||
| `NN-MM-PLAN.md` | `phase-prompt.md` | `/gsd:plan-phase` | Executable implementation plan |
|
||||
| `NN-MM-SUMMARY.md` | `summary.md` | `/gsd:execute-phase` | Post-execution summary with learnings |
|
||||
| `NN-CONTEXT.md` | `context.md` | `/gsd:discuss-phase` | Scoped discussion decisions for the phase |
|
||||
| `NN-RESEARCH.md` | `research.md` | `/gsd:research-phase`, `/gsd:plan-phase` | Technical research for the phase |
|
||||
| `NN-VALIDATION.md` | `VALIDATION.md` | `/gsd:research-phase` (Nyquist) | Validation architecture (Nyquist method) |
|
||||
| `NN-UAT.md` | `UAT.md` | `/gsd:validate-phase` | User acceptance test results |
|
||||
| `NN-PATTERNS.md` | *(inline)* | `/gsd:plan-phase` (pattern mapper) | Analog file mapping for the phase |
|
||||
| `NN-UI-SPEC.md` | `UI-SPEC.md` | `/gsd:ui-phase` | UI design contract |
|
||||
| `NN-SECURITY.md` | `SECURITY.md` | `/gsd:secure-phase` | Security threat model |
|
||||
| `NN-AI-SPEC.md` | `AI-SPEC.md` | `/gsd:ai-integration-phase` | AI integration spec with eval strategy |
|
||||
| `NN-DEBUG.md` | `DEBUG.md` | `/gsd:debug` | Debug session log |
|
||||
| `NN-REVIEWS.md` | *(inline)* | `/gsd:review` | Cross-AI review feedback |
|
||||
| `NN-MM-PLAN.md` | `phase-prompt.md` | `/gsd-plan-phase` | Executable implementation plan |
|
||||
| `NN-MM-SUMMARY.md` | `summary.md` | `/gsd-execute-phase` | Post-execution summary with learnings |
|
||||
| `NN-CONTEXT.md` | `context.md` | `/gsd-discuss-phase` | Scoped discussion decisions for the phase |
|
||||
| `NN-RESEARCH.md` | `research.md` | `/gsd-research-phase`, `/gsd-plan-phase` | Technical research for the phase |
|
||||
| `NN-VALIDATION.md` | `VALIDATION.md` | `/gsd-research-phase` (Nyquist) | Validation architecture (Nyquist method) |
|
||||
| `NN-UAT.md` | `UAT.md` | `/gsd-validate-phase` | User acceptance test results |
|
||||
| `NN-PATTERNS.md` | *(inline)* | `/gsd-plan-phase` (pattern mapper) | Analog file mapping for the phase |
|
||||
| `NN-UI-SPEC.md` | `UI-SPEC.md` | `/gsd-ui-phase` | UI design contract |
|
||||
| `NN-SECURITY.md` | `SECURITY.md` | `/gsd-secure-phase` | Security threat model |
|
||||
| `NN-AI-SPEC.md` | `AI-SPEC.md` | `/gsd-ai-integration-phase` | AI integration spec with eval strategy |
|
||||
| `NN-DEBUG.md` | `DEBUG.md` | `/gsd-debug` | Debug session log |
|
||||
| `NN-REVIEWS.md` | *(inline)* | `/gsd-review` | Cross-AI review feedback |
|
||||
|
||||
---
|
||||
|
||||
## Milestone Archive (`.planning/milestones/`)
|
||||
|
||||
Files archived by `/gsd:complete-milestone`. These are never checked by W019.
|
||||
Files archived by `/gsd-complete-milestone`. These are never checked by W019.
|
||||
|
||||
| File Pattern | Source |
|
||||
|-------------|--------|
|
||||
|
||||
@@ -106,7 +106,7 @@ blocked: [N]
|
||||
**Gaps:**
|
||||
- APPEND only when issue found (YAML format)
|
||||
- After diagnosis: fill `root_cause`, `artifacts`, `missing`, `debug_session`
|
||||
- This section feeds directly into /gsd:plan-phase --gaps
|
||||
- This section feeds directly into /gsd-plan-phase --gaps
|
||||
|
||||
</section_rules>
|
||||
|
||||
@@ -120,7 +120,7 @@ blocked: [N]
|
||||
4. UAT.md Gaps section updated with diagnosis:
|
||||
- Each gap gets `root_cause`, `artifacts`, `missing`, `debug_session` filled
|
||||
5. status → "diagnosed"
|
||||
6. Ready for /gsd:plan-phase --gaps with root causes
|
||||
6. Ready for /gsd-plan-phase --gaps with root causes
|
||||
|
||||
**After diagnosis:**
|
||||
```yaml
|
||||
@@ -144,7 +144,7 @@ blocked: [N]
|
||||
|
||||
<lifecycle>
|
||||
|
||||
**Creation:** When /gsd:verify-work starts new session
|
||||
**Creation:** When /gsd-verify-work starts new session
|
||||
- Extract tests from SUMMARY.md files
|
||||
- Set status to "testing"
|
||||
- Current Test points to test 1
|
||||
@@ -171,7 +171,7 @@ blocked: [N]
|
||||
- Present summary with outstanding items highlighted
|
||||
|
||||
**Resuming partial session:**
|
||||
- `/gsd:verify-work {phase}` picks up from first pending/blocked test
|
||||
- `/gsd-verify-work {phase}` picks up from first pending/blocked test
|
||||
- When all items resolved, status advances to "complete"
|
||||
|
||||
**Resume after /clear:**
|
||||
|
||||
@@ -29,7 +29,7 @@ created: {date}
|
||||
|
||||
- **After every task commit:** Run `{quick run command}`
|
||||
- **After every plan wave:** Run `{full suite command}`
|
||||
- **Before `/gsd:verify-work`:** Full suite must be green
|
||||
- **Before `/gsd-verify-work`:** Full suite must be green
|
||||
- **Max feedback latency:** {N} seconds
|
||||
|
||||
---
|
||||
|
||||
@@ -21,7 +21,7 @@ The profile section is managed exclusively by `generate-claude-profile`.
|
||||
|
||||
**Fallback text:**
|
||||
```
|
||||
Project not yet initialized. Run /gsd:new-project to set up.
|
||||
Project not yet initialized. Run /gsd-new-project to set up.
|
||||
```
|
||||
|
||||
### Stack Section
|
||||
@@ -96,9 +96,9 @@ No project skills found. Add skills to any of: `.claude/skills/`, `.agents/skill
|
||||
Before using Edit, Write, or other file-changing tools, start work through a GSD command so planning artifacts and execution context stay in sync.
|
||||
|
||||
Use these entry points:
|
||||
- `/gsd:quick` for small fixes, doc updates, and ad-hoc tasks
|
||||
- `/gsd:debug` for investigation and bug fixing
|
||||
- `/gsd:execute-phase` for planned phase work
|
||||
- `/gsd-quick` for small fixes, doc updates, and ad-hoc tasks
|
||||
- `/gsd-debug` for investigation and bug fixing
|
||||
- `/gsd-execute-phase` for planned phase work
|
||||
|
||||
Do not make direct repo edits outside a GSD workflow unless the user explicitly asks to bypass it.
|
||||
<!-- GSD:workflow-end -->
|
||||
@@ -109,7 +109,7 @@ Do not make direct repo edits outside a GSD workflow unless the user explicitly
|
||||
<!-- GSD:profile-start -->
|
||||
## Developer Profile
|
||||
|
||||
> Profile not yet configured. Run `/gsd:profile-user` to generate your developer profile.
|
||||
> Profile not yet configured. Run `/gsd-profile-user` to generate your developer profile.
|
||||
> This section is managed by `generate-claude-profile` — do not edit manually.
|
||||
<!-- GSD:profile-end -->
|
||||
```
|
||||
|
||||
@@ -51,7 +51,7 @@ Create: .planning/debug/{slug}.md
|
||||
|
||||
## Usage
|
||||
|
||||
**From /gsd:debug:**
|
||||
**From /gsd-debug:**
|
||||
```python
|
||||
Task(
|
||||
prompt=filled_template,
|
||||
|
||||
@@ -5,7 +5,7 @@ description: Load developer preferences into this session
|
||||
# Developer Preferences
|
||||
|
||||
> Generated by GSD on {{generated_at}} from {{data_source}}.
|
||||
> Run `/gsd:profile-user --refresh` to regenerate.
|
||||
> Run `/gsd-profile-user --refresh` to regenerate.
|
||||
|
||||
## Behavioral Directives
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ Template for `.planning/phases/XX-name/DISCOVERY.md` - shallow research for libr
|
||||
|
||||
**Purpose:** Answer "which library/option should we use" questions during mandatory discovery in plan-phase.
|
||||
|
||||
For deep ecosystem research ("how do experts build this"), use `/gsd:research-phase` which produces RESEARCH.md.
|
||||
For deep ecosystem research ("how do experts build this"), use `/gsd-research-phase` which produces RESEARCH.md.
|
||||
|
||||
---
|
||||
|
||||
@@ -142,5 +142,5 @@ Create `.planning/phases/XX-name/DISCOVERY.md`:
|
||||
- Niche/complex domains (3D, games, audio, shaders)
|
||||
- Need ecosystem knowledge, not just library choice
|
||||
- "How do experts build this" questions
|
||||
- Use `/gsd:research-phase` for these
|
||||
- Use `/gsd-research-phase` for these
|
||||
</guidelines>
|
||||
|
||||
@@ -142,7 +142,7 @@ After completion, create `.planning/phases/XX-name/{phase}-{plan}-SUMMARY.md`
|
||||
| `user_setup` | No | Array of human-required setup items (external services) |
|
||||
| `must_haves` | Yes | Goal-backward verification criteria (see below) |
|
||||
|
||||
**Wave is pre-computed:** Wave numbers are assigned during `/gsd:plan-phase`. Execute-phase reads `wave` directly from frontmatter and groups plans by wave number. No runtime dependency analysis needed.
|
||||
**Wave is pre-computed:** Wave numbers are assigned during `/gsd-plan-phase`. Execute-phase reads `wave` directly from frontmatter and groups plans by wave number. No runtime dependency analysis needed.
|
||||
|
||||
**Must-haves enable verification:** The `must_haves` field carries goal-backward requirements from planning to execution. After all plans complete, execute-phase spawns a verification subagent that checks these criteria against the actual codebase.
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ Template for spawning gsd-planner agent. The agent contains all planning experti
|
||||
</planning_context>
|
||||
|
||||
<downstream_consumer>
|
||||
Output consumed by /gsd:execute-phase
|
||||
Output consumed by /gsd-execute-phase
|
||||
Plans must be executable prompts with:
|
||||
- Frontmatter (wave, depends_on, files_modified, autonomous)
|
||||
- Tasks in XML format
|
||||
@@ -68,7 +68,7 @@ Before returning PLANNING COMPLETE:
|
||||
|
||||
## Usage
|
||||
|
||||
**From /gsd:plan-phase (standard mode):**
|
||||
**From /gsd-plan-phase (standard mode):**
|
||||
```python
|
||||
Task(
|
||||
prompt=filled_template,
|
||||
@@ -77,7 +77,7 @@ Task(
|
||||
)
|
||||
```
|
||||
|
||||
**From /gsd:plan-phase --gaps (gap closure mode):**
|
||||
**From /gsd-plan-phase --gaps (gap closure mode):**
|
||||
```python
|
||||
Task(
|
||||
prompt=filled_template, # with mode: gap_closure
|
||||
|
||||
@@ -149,7 +149,7 @@ and implemented by workflows/transition.md and workflows/complete-milestone.md.
|
||||
|
||||
For existing codebases:
|
||||
|
||||
1. **Map codebase first** via `/gsd:map-codebase`
|
||||
1. **Map codebase first** via `/gsd-map-codebase`
|
||||
|
||||
2. **Infer Validated requirements** from existing code:
|
||||
- What does the codebase actually do?
|
||||
|
||||
@@ -18,7 +18,7 @@ Template for `.planning/phases/XX-name/{phase_num}-RESEARCH.md` - comprehensive
|
||||
<user_constraints>
|
||||
## User Constraints (from CONTEXT.md)
|
||||
|
||||
**CRITICAL:** If CONTEXT.md exists from /gsd:discuss-phase, copy locked decisions here verbatim. These MUST be honored by the planner.
|
||||
**CRITICAL:** If CONTEXT.md exists from /gsd-discuss-phase, copy locked decisions here verbatim. These MUST be honored by the planner.
|
||||
|
||||
### Locked Decisions
|
||||
[Copy from CONTEXT.md `## Decisions` section - these are NON-NEGOTIABLE]
|
||||
|
||||
@@ -96,7 +96,7 @@ Status: ✓ = met minimum, ⚠ = below minimum (planner treats as assumption)
|
||||
|
||||
*Phase: [XX-name]*
|
||||
*Spec created: [date]*
|
||||
*Next step: /gsd:discuss-phase [X] — implementation decisions (how to build what's specified above)*
|
||||
*Next step: /gsd-discuss-phase [X] — implementation decisions (how to build what's specified above)*
|
||||
```
|
||||
|
||||
<good_examples>
|
||||
@@ -192,7 +192,7 @@ The database has a `posts` table and `follows` table. No feed query or feed UI e
|
||||
|
||||
*Phase: 03-post-feed*
|
||||
*Spec created: 2025-01-20*
|
||||
*Next step: /gsd:discuss-phase 3 — implementation decisions (card layout, loading skeleton, etc.)*
|
||||
*Next step: /gsd-discuss-phase 3 — implementation decisions (card layout, loading skeleton, etc.)*
|
||||
```
|
||||
|
||||
**Example 2: CLI tool (Database backup)**
|
||||
@@ -280,7 +280,7 @@ No backup tooling exists. The project uses PostgreSQL. Developers currently use
|
||||
|
||||
*Phase: 02-backup-command*
|
||||
*Spec created: 2025-01-20*
|
||||
*Next step: /gsd:discuss-phase 2 — implementation decisions (progress reporting, flag design, etc.)*
|
||||
*Next step: /gsd-discuss-phase 2 — implementation decisions (progress reporting, flag design, etc.)*
|
||||
```
|
||||
|
||||
</good_examples>
|
||||
|
||||
@@ -153,10 +153,10 @@ Updated after each plan completion.
|
||||
|
||||
**Decisions:** Reference to PROJECT.md Key Decisions table, plus recent decisions summary for quick access. Full decision log lives in PROJECT.md.
|
||||
|
||||
**Pending Todos:** Ideas captured via /gsd:add-todo
|
||||
**Pending Todos:** Ideas captured via /gsd-add-todo
|
||||
- Count of pending todos
|
||||
- Reference to .planning/todos/pending/
|
||||
- Brief list if few, count if many (e.g., "5 pending todos — see /gsd:check-todos")
|
||||
- Brief list if few, count if many (e.g., "5 pending todos — see /gsd-check-todos")
|
||||
|
||||
**Blockers/Concerns:** From "Next Phase Readiness" sections
|
||||
- Issues that affect future work
|
||||
|
||||
@@ -11,15 +11,15 @@ Read all files referenced by the invoking prompt's execution_context before star
|
||||
<step name="parse_arguments">
|
||||
Parse the command arguments:
|
||||
- All arguments become the phase description
|
||||
- Example: `/gsd:add-phase Add authentication` → description = "Add authentication"
|
||||
- Example: `/gsd:add-phase Fix critical performance issues` → description = "Fix critical performance issues"
|
||||
- Example: `/gsd-add-phase Add authentication` → description = "Add authentication"
|
||||
- Example: `/gsd-add-phase Fix critical performance issues` → description = "Fix critical performance issues"
|
||||
|
||||
If no arguments provided:
|
||||
|
||||
```
|
||||
ERROR: Phase description required
|
||||
Usage: /gsd:add-phase <description>
|
||||
Example: /gsd:add-phase Add authentication system
|
||||
Usage: /gsd-add-phase <description>
|
||||
Example: /gsd-add-phase Add authentication system
|
||||
```
|
||||
|
||||
Exit.
|
||||
@@ -36,7 +36,7 @@ if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
Check `roadmap_exists` from init JSON. If false:
|
||||
```
|
||||
ERROR: No roadmap found (.planning/ROADMAP.md)
|
||||
Run /gsd:new-project to initialize.
|
||||
Run /gsd-new-project to initialize.
|
||||
```
|
||||
Exit.
|
||||
</step>
|
||||
@@ -89,12 +89,12 @@ Roadmap updated: .planning/ROADMAP.md
|
||||
|
||||
`/clear` then:
|
||||
|
||||
`/gsd:plan-phase {N}`
|
||||
`/gsd-plan-phase {N}`
|
||||
|
||||
---
|
||||
|
||||
**Also available:**
|
||||
- `/gsd:add-phase <description>` — add another phase
|
||||
- `/gsd-add-phase <description>` — add another phase
|
||||
- Review roadmap
|
||||
|
||||
---
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<purpose>
|
||||
Generate unit and E2E tests for a completed phase based on its SUMMARY.md, CONTEXT.md, and implementation. Classifies each changed file into TDD (unit), E2E (browser), or Skip categories, presents a test plan for user approval, then generates tests following RED-GREEN conventions.
|
||||
|
||||
Users currently hand-craft `/gsd:quick` prompts for test generation after each phase. This workflow standardizes the process with proper classification, quality gates, and gap reporting.
|
||||
Users currently hand-craft `/gsd-quick` prompts for test generation after each phase. This workflow standardizes the process with proper classification, quality gates, and gap reporting.
|
||||
</purpose>
|
||||
|
||||
<required_reading>
|
||||
@@ -15,15 +15,15 @@ Parse `$ARGUMENTS` for:
|
||||
- Phase number (integer, decimal, or letter-suffix) → store as `$PHASE_ARG`
|
||||
- Remaining text after phase number → store as `$EXTRA_INSTRUCTIONS` (optional)
|
||||
|
||||
Example: `/gsd:add-tests 12 focus on edge cases` → `$PHASE_ARG=12`, `$EXTRA_INSTRUCTIONS="focus on edge cases"`
|
||||
Example: `/gsd-add-tests 12 focus on edge cases` → `$PHASE_ARG=12`, `$EXTRA_INSTRUCTIONS="focus on edge cases"`
|
||||
|
||||
If no phase argument provided:
|
||||
|
||||
```
|
||||
ERROR: Phase number required
|
||||
Usage: /gsd:add-tests <phase> [additional instructions]
|
||||
Example: /gsd:add-tests 12
|
||||
Example: /gsd:add-tests 12 focus on edge cases in the pricing module
|
||||
Usage: /gsd-add-tests <phase> [additional instructions]
|
||||
Example: /gsd-add-tests 12
|
||||
Example: /gsd-add-tests 12 focus on edge cases in the pricing module
|
||||
```
|
||||
|
||||
Exit.
|
||||
@@ -54,7 +54,7 @@ Read the phase artifacts (in order of priority):
|
||||
If no SUMMARY.md exists:
|
||||
```
|
||||
ERROR: No SUMMARY.md found for phase ${PHASE_ARG}
|
||||
This command works on completed phases. Run /gsd:execute-phase first.
|
||||
This command works on completed phases. Run /gsd-execute-phase first.
|
||||
```
|
||||
Exit.
|
||||
|
||||
@@ -318,7 +318,7 @@ Present next steps:
|
||||
## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE}
|
||||
|
||||
{if bugs discovered:}
|
||||
**Fix discovered bugs:** `/gsd:quick fix the {N} test failures discovered in phase ${phase_number}`
|
||||
**Fix discovered bugs:** `/gsd-quick fix the {N} test failures discovered in phase ${phase_number}`
|
||||
|
||||
{if blocked tests:}
|
||||
**Resolve test blockers:** {description of what's needed}
|
||||
@@ -329,8 +329,8 @@ Present next steps:
|
||||
---
|
||||
|
||||
**Also available:**
|
||||
- `/gsd:add-tests {next_phase}` — test another phase
|
||||
- `/gsd:verify-work {phase_number}` — run UAT verification
|
||||
- `/gsd-add-tests {next_phase}` — test another phase
|
||||
- `/gsd-verify-work {phase_number}` — run UAT verification
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
@@ -28,7 +28,7 @@ Note existing areas from the todos array for consistency in infer_area step.
|
||||
|
||||
<step name="extract_content">
|
||||
**With arguments:** Use as the title/focus.
|
||||
- `/gsd:add-todo Add auth token refresh` → title = "Add auth token refresh"
|
||||
- `/gsd-add-todo Add auth token refresh` → title = "Add auth token refresh"
|
||||
|
||||
**Without arguments:** Analyze recent conversation to extract:
|
||||
- The specific problem, idea, or task discussed
|
||||
@@ -143,7 +143,7 @@ Would you like to:
|
||||
|
||||
1. Continue with current work
|
||||
2. Add another todo
|
||||
3. View all todos (/gsd:check-todos)
|
||||
3. View all todos (/gsd-check-todos)
|
||||
```
|
||||
</step>
|
||||
|
||||
|
||||
@@ -43,11 +43,11 @@ AI_PHASE_ENABLED=$(gsd-sdk query config-get workflow.ai_integration_phase 2>/dev
|
||||
|
||||
**If `AI_PHASE_ENABLED` is `false`:**
|
||||
```
|
||||
AI phase is disabled in config. Enable via /gsd:settings.
|
||||
AI phase is disabled in config. Enable via /gsd-settings.
|
||||
```
|
||||
Exit workflow.
|
||||
|
||||
**If `planning_exists` is false:** Error — run `/gsd:new-project` first.
|
||||
**If `planning_exists` is false:** Error — run `/gsd-new-project` first.
|
||||
|
||||
## 2. Parse and Validate Phase
|
||||
|
||||
@@ -64,7 +64,7 @@ PHASE_INFO=$(gsd-sdk query roadmap.get-phase "${PHASE}")
|
||||
**If `has_context` is false:**
|
||||
```
|
||||
No CONTEXT.md found for Phase {N}.
|
||||
Recommended: run /gsd:discuss-phase {N} first to capture framework preferences.
|
||||
Recommended: run /gsd-discuss-phase {N} first to capture framework preferences.
|
||||
Continuing without user decisions — framework selector will ask all questions.
|
||||
```
|
||||
Continue (non-blocking).
|
||||
@@ -122,7 +122,7 @@ Goal: {phase_goal}
|
||||
|
||||
Parse selector output for: `primary_framework`, `system_type`, `model_provider`, `eval_concerns`, `alternative_framework`.
|
||||
|
||||
**If selector fails or returns empty:** Exit with error — "Framework selection failed. Re-run /gsd:ai-integration-phase {N} or answer the framework question in /gsd:discuss-phase {N} first."
|
||||
**If selector fails or returns empty:** Exit with error — "Framework selection failed. Re-run /gsd-ai-integration-phase {N} or answer the framework question in /gsd-discuss-phase {N} first."
|
||||
|
||||
## 6. Initialize AI-SPEC.md
|
||||
|
||||
@@ -266,7 +266,7 @@ git commit -m "docs({phase_slug}): generate AI-SPEC.md — {primary_framework} +
|
||||
◆ Output: {ai_spec_path}
|
||||
|
||||
Next step:
|
||||
/gsd:plan-phase {N} — planner will consume AI-SPEC.md
|
||||
/gsd-plan-phase {N} — planner will consume AI-SPEC.md
|
||||
```
|
||||
|
||||
</process>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<purpose>
|
||||
Analyze ROADMAP.md phases for dependency relationships before execution. Detect file overlap between phases, semantic API/data-flow dependencies, and suggest `Depends on` entries to prevent merge conflicts during parallel execution by `/gsd:manager`.
|
||||
Analyze ROADMAP.md phases for dependency relationships before execution. Detect file overlap between phases, semantic API/data-flow dependencies, and suggest `Depends on` entries to prevent merge conflicts during parallel execution by `/gsd-manager`.
|
||||
</purpose>
|
||||
|
||||
<process>
|
||||
|
||||
## 1. Load ROADMAP.md
|
||||
|
||||
Read `.planning/ROADMAP.md`. If it does not exist, error: "No ROADMAP.md found — run `/gsd:new-project` first."
|
||||
Read `.planning/ROADMAP.md`. If it does not exist, error: "No ROADMAP.md found — run `/gsd-new-project` first."
|
||||
|
||||
Extract all phases. For each phase capture:
|
||||
- Phase number and name
|
||||
@@ -91,6 +91,6 @@ When writing to ROADMAP.md:
|
||||
- Preserve all other phase content unchanged
|
||||
- Do not reorder phases
|
||||
|
||||
After applying: "ROADMAP.md updated. Run `/gsd:manager` to execute phases in the correct order."
|
||||
After applying: "ROADMAP.md updated. Run `/gsd-manager` to execute phases in the correct order."
|
||||
|
||||
</process>
|
||||
|
||||
@@ -18,7 +18,7 @@ Valid GSD subagent types (use exact names — do not fall back to 'general-purpo
|
||||
```bash
|
||||
INIT=$(gsd-sdk query init.milestone-op)
|
||||
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
AGENT_SKILLS_CHECKER=$(gsd-sdk query agent-skills gsd-integration-checker 2>/dev/null)
|
||||
AGENT_SKILLS_CHECKER=$(gsd-sdk query agent-skills gsd-integration-checker)
|
||||
```
|
||||
|
||||
Extract from init JSON: `milestone_version`, `milestone_name`, `phase_count`, `completed_phases`, `commit_docs`.
|
||||
@@ -158,7 +158,7 @@ Classify per phase:
|
||||
|
||||
Add to audit YAML: `nyquist: { compliant_phases, partial_phases, missing_phases, overall }`
|
||||
|
||||
Discovery only — never auto-calls `/gsd:validate-phase`.
|
||||
Discovery only — never auto-calls `/gsd-validate-phase`.
|
||||
|
||||
## 6. Aggregate into v{version}-MILESTONE-AUDIT.md
|
||||
|
||||
@@ -231,7 +231,7 @@ All requirements covered. Cross-phase integration verified. E2E flows complete.
|
||||
|
||||
/clear then:
|
||||
|
||||
/gsd:complete-milestone {version}
|
||||
/gsd-complete-milestone {version}
|
||||
|
||||
───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -264,9 +264,9 @@ All requirements covered. Cross-phase integration verified. E2E flows complete.
|
||||
|
||||
| Phase | VALIDATION.md | Compliant | Action |
|
||||
|-------|---------------|-----------|--------|
|
||||
| {phase} | exists/missing | true/false/partial | `/gsd:validate-phase {N}` |
|
||||
| {phase} | exists/missing | true/false/partial | `/gsd-validate-phase {N}` |
|
||||
|
||||
Phases needing validation: run `/gsd:validate-phase {N}` for each flagged phase.
|
||||
Phases needing validation: run `/gsd-validate-phase {N}` for each flagged phase.
|
||||
|
||||
───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -276,13 +276,13 @@ Phases needing validation: run `/gsd:validate-phase {N}` for each flagged phase.
|
||||
|
||||
/clear then:
|
||||
|
||||
/gsd:plan-milestone-gaps
|
||||
/gsd-plan-milestone-gaps
|
||||
|
||||
───────────────────────────────────────────────────────────────
|
||||
|
||||
**Also available:**
|
||||
- cat .planning/v{version}-MILESTONE-AUDIT.md — see full report
|
||||
- /gsd:complete-milestone {version} — proceed anyway (accept tech debt)
|
||||
- /gsd-complete-milestone {version} — proceed anyway (accept tech debt)
|
||||
|
||||
───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -312,13 +312,13 @@ All requirements met. No critical blockers. Accumulated tech debt needs review.
|
||||
|
||||
**A. Complete milestone** — accept debt, track in backlog
|
||||
|
||||
/gsd:complete-milestone {version}
|
||||
/gsd-complete-milestone {version}
|
||||
|
||||
**B. Plan cleanup phase** — address debt before completing
|
||||
|
||||
/clear then:
|
||||
|
||||
/gsd:plan-milestone-gaps
|
||||
/gsd-plan-milestone-gaps
|
||||
|
||||
───────────────────────────────────────────────────────────────
|
||||
</offer_next>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user