mirror of
https://github.com/glittercowboy/get-shit-done
synced 2026-04-25 17:25:23 +02:00
Compare commits
85 Commits
feat/2494-
...
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 | ||
|
|
b35fdd51f3 | ||
|
|
7212cfd4de | ||
|
|
2b5c35cdb1 | ||
|
|
73c1af5168 | ||
|
|
533973700c | ||
|
|
349daf7e6a | ||
|
|
6b7b5c15a5 | ||
|
|
67a9550720 | ||
|
|
fba040c72c | ||
|
|
7032f44633 | ||
|
|
2404b40a15 | ||
|
|
0d6349a6c1 | ||
|
|
c47a6a2164 | ||
|
|
af2dba2328 | ||
|
|
9b5397a30f | ||
|
|
7397f580a5 | ||
|
|
9a67e350b3 | ||
|
|
98d92d7570 | ||
|
|
8eeaa20791 | ||
|
|
f32ffc9fb8 | ||
|
|
5676e2e4ef | ||
|
|
7bb6b6452a | ||
|
|
43ea92578b | ||
|
|
a42d5db742 | ||
|
|
c86ca1b3eb | ||
|
|
337e052aa9 | ||
|
|
969ee38ee5 | ||
|
|
2980f0ec48 | ||
|
|
8789211038 | ||
|
|
57bbfe652b | ||
|
|
a4764c5611 | ||
|
|
b2534e8a05 | ||
|
|
30433368a0 | ||
|
|
04fab926b5 | ||
|
|
f98ef1e460 | ||
|
|
d0565e95c1 | ||
|
|
4ef6275e86 | ||
|
|
6c50490766 | ||
|
|
4cbebfe78c | ||
|
|
9e87d43831 | ||
|
|
29ea90bc83 | ||
|
|
0c6172bfad | ||
|
|
e3bd06c9fd | ||
|
|
c69ecd975a | ||
|
|
06c4ded4ec | ||
|
|
341bb941c6 |
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"
|
||||
|
||||
29
.github/workflows/release.yml
vendored
29
.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: |
|
||||
@@ -342,23 +348,32 @@ jobs:
|
||||
|
||||
- name: Create PR to merge release back to main
|
||||
if: ${{ !inputs.dry_run }}
|
||||
continue-on-error: true
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
BRANCH: ${{ needs.validate-version.outputs.branch }}
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
EXISTING_PR=$(gh pr list --base main --head "$BRANCH" --state open --json number --jq '.[0].number')
|
||||
# Non-fatal: repos that disable "Allow GitHub Actions to create and
|
||||
# approve pull requests" cause this step to fail with GraphQL 403.
|
||||
# The release itself (tag + npm publish + GitHub Release) must still
|
||||
# proceed. Open the merge-back PR manually afterwards with:
|
||||
# gh pr create --base main --head release/${VERSION} \
|
||||
# --title "chore: merge release v${VERSION} to main"
|
||||
EXISTING_PR=$(gh pr list --base main --head "$BRANCH" --state open --json number --jq '.[0].number' 2>/dev/null || echo "")
|
||||
if [ -n "$EXISTING_PR" ]; then
|
||||
echo "PR #$EXISTING_PR already exists; updating"
|
||||
gh pr edit "$EXISTING_PR" \
|
||||
--title "chore: merge release v${VERSION} to main" \
|
||||
--body "Merge release branch back to main after v${VERSION} stable release."
|
||||
--body "Merge release branch back to main after v${VERSION} stable release." \
|
||||
|| echo "::warning::Could not update merge-back PR (likely PR-creation policy disabled). Open it manually after release."
|
||||
else
|
||||
gh pr create \
|
||||
--base main \
|
||||
--head "$BRANCH" \
|
||||
--title "chore: merge release v${VERSION} to main" \
|
||||
--body "Merge release branch back to main after v${VERSION} stable release."
|
||||
--body "Merge release branch back to main after v${VERSION} stable release." \
|
||||
|| echo "::warning::Could not create merge-back PR (likely PR-creation policy disabled). Open it manually after release."
|
||||
fi
|
||||
|
||||
- name: Tag and push
|
||||
|
||||
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">
|
||||
|
||||
@@ -145,7 +145,7 @@ When researching "best library for X": find what the ecosystem actually uses, do
|
||||
1. `mcp__context7__resolve-library-id` with libraryName
|
||||
2. `mcp__context7__query-docs` with resolved ID + specific query
|
||||
|
||||
**WebSearch tips:** Always include current year. Use multiple query variations. Cross-verify with authoritative sources.
|
||||
**WebSearch tips:** Use multiple query variations. Cross-verify with authoritative sources. Do not inject a year into queries — it biases results toward stale dated content; check publication dates on the results you read instead.
|
||||
|
||||
## Enhanced Web Search (Brave API)
|
||||
|
||||
@@ -836,6 +836,6 @@ Quality indicators:
|
||||
- **Verified, not assumed:** Findings cite Context7 or official docs
|
||||
- **Honest about gaps:** LOW confidence items flagged, unknowns admitted
|
||||
- **Actionable:** Planner could create tasks based on this research
|
||||
- **Current:** Year included in searches, publication dates checked
|
||||
- **Current:** Publication dates checked on sources (do not inject year into queries)
|
||||
|
||||
</success_criteria>
|
||||
@@ -1201,6 +1201,10 @@ Execute: `/gsd-execute-phase {phase} --gaps-only`
|
||||
|
||||
Follow templates in checkpoints and revision_mode sections respectively.
|
||||
|
||||
## Chunked Mode Returns
|
||||
|
||||
See @~/.claude/get-shit-done/references/planner-chunked.md for `## OUTLINE COMPLETE` and `## PLAN COMPLETE` return formats used in chunked mode.
|
||||
|
||||
</structured_returns>
|
||||
|
||||
<critical_rules>
|
||||
|
||||
@@ -116,12 +116,12 @@ For finding what exists, community patterns, real-world usage.
|
||||
|
||||
**Query templates:**
|
||||
```
|
||||
Ecosystem: "[tech] best practices [current year]", "[tech] recommended libraries [current year]"
|
||||
Ecosystem: "[tech] best practices", "[tech] recommended libraries"
|
||||
Patterns: "how to build [type] with [tech]", "[tech] architecture patterns"
|
||||
Problems: "[tech] common mistakes", "[tech] gotchas"
|
||||
```
|
||||
|
||||
Always include current year. Use multiple query variations. Mark WebSearch-only findings as LOW confidence.
|
||||
Use multiple query variations. Mark WebSearch-only findings as LOW confidence. Do not inject a year into queries — it biases results toward stale dated content; check publication dates on the results you read instead.
|
||||
|
||||
### Enhanced Web Search (Brave API)
|
||||
|
||||
@@ -672,6 +672,6 @@ Research is complete when:
|
||||
- [ ] Files written (DO NOT commit — orchestrator handles this)
|
||||
- [ ] Structured return provided to orchestrator
|
||||
|
||||
**Quality:** Comprehensive not shallow. Opinionated not wishy-washy. Verified not assumed. Honest about gaps. Actionable for roadmap. Current (year in searches).
|
||||
**Quality:** Comprehensive not shallow. Opinionated not wishy-washy. Verified not assumed. Honest about gaps. Actionable for roadmap. Current (check publication dates, do not inject year into queries).
|
||||
|
||||
</success_criteria>
|
||||
|
||||
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);
|
||||
836
bin/install.js
836
bin/install.js
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
---
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
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>
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd:sketch
|
||||
description: Rapidly sketch UI/design ideas using throwaway HTML mockups with multi-variant exploration
|
||||
argument-hint: "<design idea to explore> [--quick] [--text]"
|
||||
description: Sketch UI/design ideas with throwaway HTML mockups, or propose what to sketch next (frontier mode)
|
||||
argument-hint: "[design idea to explore] [--quick] [--text] or [frontier]"
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Write
|
||||
@@ -18,7 +18,12 @@ allowed-tools:
|
||||
<objective>
|
||||
Explore design directions through throwaway HTML mockups before committing to implementation.
|
||||
Each sketch produces 2-3 variants for comparison. Sketches live in `.planning/sketches/` and
|
||||
integrate with GSD commit patterns, state tracking, and handoff workflows.
|
||||
integrate with GSD commit patterns, state tracking, and handoff workflows. Loads spike
|
||||
findings to ground mockups in real data shapes and validated interaction patterns.
|
||||
|
||||
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.
|
||||
</objective>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd:spike
|
||||
description: Rapidly spike an idea with throwaway experiments to validate feasibility before planning
|
||||
argument-hint: "<idea to validate> [--quick] [--text]"
|
||||
description: Spike an idea through experiential exploration, or propose what to spike next (frontier mode)
|
||||
argument-hint: "[idea to validate] [--quick] [--text] or [frontier]"
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Write
|
||||
@@ -16,9 +16,14 @@ allowed-tools:
|
||||
- mcp__context7__query-docs
|
||||
---
|
||||
<objective>
|
||||
Rapid feasibility validation through focused, throwaway experiments. Each spike answers one
|
||||
specific question with observable evidence. Spikes live in `.planning/spikes/` and integrate
|
||||
with GSD commit patterns, state tracking, and handoff workflows.
|
||||
Spike an idea through experiential exploration — build focused experiments to feel the pieces
|
||||
of a future app, validate feasibility, and produce verified knowledge for the real build.
|
||||
Spikes live in `.planning/spikes/` and integrate with GSD commit patterns, state tracking,
|
||||
and handoff workflows.
|
||||
|
||||
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.
|
||||
</objective>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -30,10 +30,12 @@ GSD stores project settings in `.planning/config.json`. Created during `/gsd-new
|
||||
"nyquist_validation": true,
|
||||
"ui_phase": true,
|
||||
"ui_safety_gate": true,
|
||||
"ui_review": true,
|
||||
"node_repair": true,
|
||||
"node_repair_budget": 2,
|
||||
"research_before_questions": false,
|
||||
"discuss_mode": "discuss",
|
||||
"max_discuss_passes": 3,
|
||||
"skip_discuss": false,
|
||||
"tdd_mode": false,
|
||||
"text_mode": false,
|
||||
@@ -43,13 +45,15 @@ GSD stores project settings in `.planning/config.json`. Created during `/gsd-new
|
||||
"plan_bounce": false,
|
||||
"plan_bounce_script": null,
|
||||
"plan_bounce_passes": 2,
|
||||
"plan_chunked": false,
|
||||
"code_review_command": null,
|
||||
"cross_ai_execution": false,
|
||||
"cross_ai_command": null,
|
||||
"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,
|
||||
@@ -108,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 |
|
||||
@@ -125,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`.
|
||||
@@ -138,10 +180,12 @@ All workflow toggles follow the **absent = enabled** pattern. If a key is missin
|
||||
| `workflow.nyquist_validation` | boolean | `true` | Test coverage mapping during plan-phase research |
|
||||
| `workflow.ui_phase` | boolean | `true` | Generate UI design contracts for frontend phases |
|
||||
| `workflow.ui_safety_gate` | boolean | `true` | Prompt to run /gsd-ui-phase for frontend phases during plan-phase |
|
||||
| `workflow.ui_review` | boolean | `true` | Run visual quality audit (`/gsd-ui-review`) after phase execution in autonomous mode. When `false`, the UI audit step is skipped. |
|
||||
| `workflow.node_repair` | boolean | `true` | Autonomous task repair on verification failure |
|
||||
| `workflow.node_repair_budget` | number | `2` | Max repair attempts per failed task |
|
||||
| `workflow.research_before_questions` | boolean | `false` | Run research before discussion questions instead of after |
|
||||
| `workflow.discuss_mode` | string | `'discuss'` | Controls how `/gsd-discuss-phase` gathers context. `'discuss'` (default) asks questions one-by-one. `'assumptions'` reads the codebase first, generates structured assumptions with confidence levels, and only asks you to correct what's wrong. Added in v1.28 |
|
||||
| `workflow.max_discuss_passes` | number | `3` | Maximum number of question rounds in discuss-phase before the workflow stops asking. Useful in headless/auto mode to prevent infinite discussion loops. |
|
||||
| `workflow.skip_discuss` | boolean | `false` | When `true`, `/gsd-autonomous` bypasses the discuss-phase entirely, writing minimal CONTEXT.md from the ROADMAP phase goal. Useful for projects where developer preferences are fully captured in PROJECT.md/REQUIREMENTS.md. Added in v1.28 |
|
||||
| `workflow.text_mode` | boolean | `false` | Replaces AskUserQuestion TUI menus with plain-text numbered lists. Required for Claude Code remote sessions (`/rc` mode) where TUI menus don't render. Can also be set per-session with `--text` flag on discuss-phase. Added in v1.28 |
|
||||
| `workflow.use_worktrees` | boolean | `true` | When `false`, disables git worktree isolation for parallel execution. Users who prefer sequential execution or whose environment does not support worktrees can disable this. Added in v1.31 |
|
||||
@@ -150,6 +194,8 @@ 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 |
|
||||
| `workflow.cross_ai_execution` | boolean | `false` | Delegate phase execution to an external AI CLI instead of spawning local executor agents. Useful for leveraging a different model's strengths for specific phases. Added in v1.36 |
|
||||
@@ -160,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
|
||||
|
||||
@@ -179,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.
|
||||
@@ -191,6 +250,7 @@ If `.planning/` is in `.gitignore`, `commit_docs` is automatically `false` regar
|
||||
|---------|------|---------|-------------|
|
||||
| `hooks.context_warnings` | boolean | `true` | Show context window usage warnings via context monitor hook |
|
||||
| `hooks.workflow_guard` | boolean | `false` | Warn when file edits happen outside GSD workflow context (advises using `/gsd-quick` or `/gsd-fast`) |
|
||||
| `statusline.show_last_command` | boolean | `false` | Append `last: /<cmd>` suffix to the statusline showing the most recently invoked slash command. Opt-in; reads the active session transcript to extract the latest `<command-name>` tag (closes #2538) |
|
||||
|
||||
The prompt injection guard hook (`gsd-prompt-guard.js`) is always active and cannot be disabled — it's a security feature, not a workflow toggle.
|
||||
|
||||
@@ -405,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.
|
||||
@@ -506,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.
|
||||
@@ -543,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",
|
||||
@@ -229,6 +233,7 @@
|
||||
"model-profiles.md",
|
||||
"phase-argument-parsing.md",
|
||||
"planner-antipatterns.md",
|
||||
"planner-chunked.md",
|
||||
"planner-gap-closure.md",
|
||||
"planner-reviews.md",
|
||||
"planner-revision.md",
|
||||
@@ -237,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",
|
||||
@@ -262,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",
|
||||
@@ -276,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 (49 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. |
|
||||
@@ -344,16 +349,17 @@ The `gsd-planner` agent is decomposed into a core agent plus reference modules t
|
||||
| Reference | Role |
|
||||
|-----------|------|
|
||||
| `planner-antipatterns.md` | Planner anti-patterns and specificity examples. |
|
||||
| `planner-chunked.md` | Chunked mode return formats (`## OUTLINE COMPLETE`, `## PLAN COMPLETE`) for Windows stdio hang mitigation. |
|
||||
| `planner-gap-closure.md` | Gap-closure mode behavior (reads VERIFICATION.md, targeted replanning). |
|
||||
| `planner-reviews.md` | Cross-AI review integration (reads REVIEWS.md from `/gsd-review`). |
|
||||
| `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 49 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`.
|
||||
|
||||
@@ -365,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 |
|
||||
@@ -379,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') {
|
||||
@@ -1204,10 +1221,6 @@ async function runCommand(command, args, cwd, raw, defaultValue) {
|
||||
'agents',
|
||||
path.join('commands', 'gsd'),
|
||||
'hooks',
|
||||
// OpenCode/Kilo flat command dir
|
||||
'command',
|
||||
// Codex/Copilot skills dir
|
||||
'skills',
|
||||
];
|
||||
|
||||
function walkDir(dir, baseDir) {
|
||||
|
||||
@@ -25,7 +25,6 @@ const VALID_CONFIG_KEYS = new Set([
|
||||
'workflow.discuss_mode',
|
||||
'workflow.skip_discuss',
|
||||
'workflow.auto_prune_state',
|
||||
'workflow._auto_chain_active',
|
||||
'workflow.use_worktrees',
|
||||
'workflow.code_review',
|
||||
'workflow.code_review_depth',
|
||||
@@ -34,15 +33,24 @@ const VALID_CONFIG_KEYS = new Set([
|
||||
'workflow.plan_bounce',
|
||||
'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',
|
||||
'workflow.subagent_timeout',
|
||||
'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',
|
||||
'features.thinking_partner',
|
||||
'context',
|
||||
'features.global_learnings',
|
||||
@@ -50,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',
|
||||
]);
|
||||
|
||||
/**
|
||||
@@ -66,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',
|
||||
@@ -24,6 +25,8 @@ const CONFIG_KEY_SUGGESTIONS = {
|
||||
'workflow.code_review_level': 'workflow.code_review_depth',
|
||||
'workflow.review_depth': 'workflow.code_review_depth',
|
||||
'review.model': 'review.models.<cli-name>',
|
||||
'sub_repos': 'planning.sub_repos',
|
||||
'plan_checker': 'workflow.plan_check',
|
||||
};
|
||||
|
||||
function validateKnownConfigKeyPath(keyPath) {
|
||||
@@ -117,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,
|
||||
@@ -331,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}`);
|
||||
}
|
||||
|
||||
@@ -374,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));
|
||||
}
|
||||
|
||||
|
||||
@@ -266,6 +266,7 @@ const CONFIG_DEFAULTS = {
|
||||
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 ───────────────────────────────────────────────────────────
|
||||
@@ -1624,7 +1905,7 @@ function getMilestonePhaseFilter(cwd) {
|
||||
}
|
||||
|
||||
const normalized = new Set(
|
||||
[...milestonePhaseNums].map(n => (n.replace(/^0+/, '') || '0').toLowerCase())
|
||||
[...milestonePhaseNums].map(n => (n.replace(/^0+(?=\d)/, '') || '0').toLowerCase())
|
||||
);
|
||||
|
||||
function isDirInMilestone(dirName) {
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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');
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -870,9 +870,10 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
||||
const sectionText = phaseSectionMatch ? phaseSectionMatch[1] : '';
|
||||
const reqMatch = sectionText.match(/\*\*Requirements:\*\*\s*([^\n]+)/i);
|
||||
|
||||
let reqContent = fs.readFileSync(reqPath, 'utf-8');
|
||||
|
||||
if (reqMatch) {
|
||||
const reqIds = reqMatch[1].replace(/[\[\]]/g, '').split(/[,\s]+/).map(r => r.trim()).filter(Boolean);
|
||||
let reqContent = fs.readFileSync(reqPath, 'utf-8');
|
||||
|
||||
for (const reqId of reqIds) {
|
||||
const reqEscaped = escapeRegex(reqId);
|
||||
@@ -887,10 +888,40 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
||||
'$1 Complete $2'
|
||||
);
|
||||
}
|
||||
|
||||
atomicWriteFileSync(reqPath, reqContent);
|
||||
requirementsUpdated = true;
|
||||
}
|
||||
|
||||
// Scan body for all **REQ-ID** patterns, warn about any missing from the Traceability table.
|
||||
// Always runs regardless of whether the roadmap has a Requirements: line.
|
||||
const bodyReqIds = [];
|
||||
const bodyReqPattern = /\*\*([A-Z][A-Z0-9]*-\d+)\*\*/g;
|
||||
let bodyMatch;
|
||||
while ((bodyMatch = bodyReqPattern.exec(reqContent)) !== null) {
|
||||
const id = bodyMatch[1];
|
||||
if (!bodyReqIds.includes(id)) bodyReqIds.push(id);
|
||||
}
|
||||
|
||||
// Collect REQ-IDs present in the Traceability section only, to avoid
|
||||
// picking up IDs from other tables in the document.
|
||||
const traceabilityHeadingMatch = reqContent.match(/^#{1,6}\s+Traceability\b/im);
|
||||
const traceabilitySection = traceabilityHeadingMatch
|
||||
? reqContent.slice(traceabilityHeadingMatch.index)
|
||||
: '';
|
||||
const tableReqIds = new Set();
|
||||
const tableRowPattern = /^\|\s*([A-Z][A-Z0-9]*-\d+)\s*\|/gm;
|
||||
let tableMatch;
|
||||
while ((tableMatch = tableRowPattern.exec(traceabilitySection)) !== null) {
|
||||
tableReqIds.add(tableMatch[1]);
|
||||
}
|
||||
|
||||
const unregistered = bodyReqIds.filter(id => !tableReqIds.has(id));
|
||||
if (unregistered.length > 0) {
|
||||
warnings.push(
|
||||
`REQUIREMENTS.md: ${unregistered.length} REQ-ID(s) found in body but missing from Traceability table: ${unregistered.join(', ')} — add them manually to keep traceability in sync`
|
||||
);
|
||||
}
|
||||
|
||||
atomicWriteFileSync(reqPath, reqContent);
|
||||
requirementsUpdated = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -591,28 +591,57 @@ function cmdValidateHealth(cwd, options, raw) {
|
||||
} 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`,
|
||||
`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'
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
49
get-shit-done/references/planner-chunked.md
Normal file
49
get-shit-done/references/planner-chunked.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Chunked Mode Return Formats
|
||||
|
||||
Used when `plan-phase` spawns `gsd-planner` with `CHUNKED_MODE=true` (triggered by `--chunked`
|
||||
flag or `workflow.plan_chunked: true` config). Splits the single long-lived planner Task into
|
||||
shorter-lived Tasks to bound the blast radius of Windows stdio hangs.
|
||||
|
||||
## Modes
|
||||
|
||||
### outline-only
|
||||
|
||||
Write **only** `{PHASE_DIR}/{PADDED_PHASE}-PLAN-OUTLINE.md`. Do not write any PLAN.md files.
|
||||
Return:
|
||||
|
||||
```markdown
|
||||
## OUTLINE COMPLETE
|
||||
|
||||
**Phase:** {phase-name}
|
||||
**Plans:** {N} plan(s) in {M} wave(s)
|
||||
|
||||
| Plan ID | Objective | Wave | Depends On | Requirements |
|
||||
|---------|-----------|------|-----------|-------------|
|
||||
| {padded_phase}-01 | [brief objective] | 1 | none | REQ-001, REQ-002 |
|
||||
| {padded_phase}-02 | [brief objective] | 1 | none | REQ-003 |
|
||||
```
|
||||
|
||||
The orchestrator reads this table, then spawns one single-plan Task per row.
|
||||
|
||||
### single-plan
|
||||
|
||||
Write **exactly one** `{PHASE_DIR}/{plan_id}-PLAN.md`. Do not write any other plan files.
|
||||
Return:
|
||||
|
||||
```markdown
|
||||
## PLAN COMPLETE
|
||||
|
||||
**Plan:** {plan-id}
|
||||
**Objective:** {brief}
|
||||
**File:** {PHASE_DIR}/{plan-id}-PLAN.md
|
||||
**Tasks:** {N}
|
||||
```
|
||||
|
||||
The orchestrator verifies the file exists on disk after each return, commits it, then moves
|
||||
to the next plan entry from the outline.
|
||||
|
||||
## Resume Behaviour
|
||||
|
||||
If the orchestrator detects that `PLAN-OUTLINE.md` already exists (from a prior interrupted
|
||||
run), it skips the outline-only Task and goes directly to single-plan Tasks, skipping any
|
||||
`{plan_id}-PLAN.md` files that already exist on disk.
|
||||
@@ -268,6 +268,7 @@ Set via `workflow.*` namespace in config.json (e.g., `"workflow": { "research":
|
||||
| `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
|
||||
|
||||
|
||||
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.
|
||||
@@ -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`.
|
||||
|
||||
@@ -41,7 +41,7 @@ When a milestone completes:
|
||||
Before proceeding with milestone close, run the comprehensive open artifact audit.
|
||||
|
||||
```bash
|
||||
gsd-sdk query audit-open 2>/dev/null
|
||||
gsd-sdk query audit-open
|
||||
```
|
||||
|
||||
If the output contains open items (any section with count > 0):
|
||||
|
||||
@@ -87,7 +87,7 @@ This runs in parallel - all gaps investigated simultaneously.
|
||||
**Load agent skills:**
|
||||
|
||||
```bash
|
||||
AGENT_SKILLS_DEBUGGER=$(gsd-sdk query agent-skills gsd-debugger 2>/dev/null)
|
||||
AGENT_SKILLS_DEBUGGER=$(gsd-sdk query agent-skills gsd-debugger)
|
||||
EXPECTED_BASE=$(git rev-parse HEAD)
|
||||
```
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ Phase number from argument (required).
|
||||
```bash
|
||||
INIT=$(gsd-sdk query init.phase-op "${PHASE}")
|
||||
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
AGENT_SKILLS_ANALYZER=$(gsd-sdk query agent-skills gsd-assumptions-analyzer 2>/dev/null)
|
||||
AGENT_SKILLS_ANALYZER=$(gsd-sdk query agent-skills gsd-assumptions-analyzer)
|
||||
```
|
||||
|
||||
Parse JSON for: `commit_docs`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`,
|
||||
@@ -619,7 +619,7 @@ Check for auto-advance trigger:
|
||||
2. Sync chain flag:
|
||||
```bash
|
||||
if [[ ! "$ARGUMENTS" =~ --auto ]]; then
|
||||
gsd-sdk query config-set workflow._auto_chain_active false 2>/dev/null
|
||||
gsd-sdk query config-set workflow._auto_chain_active false || true
|
||||
fi
|
||||
```
|
||||
3. Read consolidated auto-mode (`active` = chain flag OR user preference):
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
173
get-shit-done/workflows/discuss-phase/modes/advisor.md
Normal file
173
get-shit-done/workflows/discuss-phase/modes/advisor.md
Normal file
@@ -0,0 +1,173 @@
|
||||
# Advisor mode — research-backed comparison tables
|
||||
|
||||
> **Lazy-loaded and gated.** The parent `workflows/discuss-phase.md` Reads
|
||||
> this file ONLY when `ADVISOR_MODE` is true (i.e., when
|
||||
> `$HOME/.claude/get-shit-done/USER-PROFILE.md` exists). Skip the Read
|
||||
> entirely when no profile is present — that's the inverse of the
|
||||
> `--advisor` flag from #2174 (don't pay the cost when unused).
|
||||
|
||||
## Activation
|
||||
|
||||
```bash
|
||||
PROFILE_PATH="$HOME/.claude/get-shit-done/USER-PROFILE.md"
|
||||
if [ -f "$PROFILE_PATH" ]; then
|
||||
ADVISOR_MODE=true
|
||||
else
|
||||
ADVISOR_MODE=false
|
||||
fi
|
||||
```
|
||||
|
||||
If `ADVISOR_MODE` is false, do **not** Read this file — proceed with the
|
||||
standard `default.md` discussion flow.
|
||||
|
||||
## Calibration tier
|
||||
|
||||
Resolve `vendor_philosophy` calibration tier:
|
||||
1. **Priority 1:** Read `config.json` > `preferences.vendor_philosophy`
|
||||
(project-level override)
|
||||
2. **Priority 2:** Read USER-PROFILE.md `Vendor Choices/Philosophy` rating
|
||||
(global)
|
||||
3. **Priority 3:** Default to `"standard"` if neither has a value or value
|
||||
is `UNSCORED`
|
||||
|
||||
Map to calibration tier:
|
||||
- `conservative` OR `thorough-evaluator` → `full_maturity`
|
||||
- `opinionated` → `minimal_decisive`
|
||||
- `pragmatic-fast` OR any other value OR empty → `standard`
|
||||
|
||||
Resolve advisor model:
|
||||
```bash
|
||||
ADVISOR_MODEL=$(gsd-sdk query resolve-model gsd-advisor-researcher --raw)
|
||||
```
|
||||
|
||||
## Non-technical owner detection
|
||||
|
||||
Read USER-PROFILE.md and check for product-owner signals:
|
||||
|
||||
```bash
|
||||
PROFILE_CONTENT=$(cat "$HOME/.claude/get-shit-done/USER-PROFILE.md" 2>/dev/null || true)
|
||||
```
|
||||
|
||||
Set `NON_TECHNICAL_OWNER = true` if ANY of the following are present:
|
||||
- `learning_style: guided`
|
||||
- The word `jargon` appears in a `frustration_triggers` section
|
||||
- `explanation_depth: practical-detailed` (without a technical modifier)
|
||||
- `explanation_depth: high-level`
|
||||
|
||||
**Tie-breaker / precedence (when signals conflict):**
|
||||
1. An explicit `technical_background: true` (or any `explanation_depth` value
|
||||
tagged with a technical modifier such as `practical-detailed:technical`)
|
||||
**overrides** all inferred non-technical signals — set
|
||||
`NON_TECHNICAL_OWNER = false`.
|
||||
2. Otherwise, ANY single matching signal is sufficient to set
|
||||
`NON_TECHNICAL_OWNER = true` (signals are OR-aggregated, not weighted).
|
||||
3. Contradictory `explanation_depth` values: the most recent entry wins.
|
||||
|
||||
Log the resolved value and the matched/overriding signal so the user can
|
||||
audit why a given framing was used.
|
||||
|
||||
When `NON_TECHNICAL_OWNER` is true, reframe gray area labels and
|
||||
descriptions in product-outcome language before presenting them. Preserve
|
||||
the same underlying decision — only change the framing:
|
||||
|
||||
- Technical implementation term → outcome the user will experience
|
||||
- "Token architecture" → "Color system: which approach prevents the dark theme from flashing white on open"
|
||||
- "CSS variable strategy" → "Theme colors: how your brand colors stay consistent in both light and dark mode"
|
||||
- "Component API surface area" → "How the building blocks connect: how tightly coupled should these parts be"
|
||||
- "Caching strategy: SWR vs React Query" → "Loading speed: should screens show saved data right away or wait for fresh data"
|
||||
|
||||
This reframing applies to:
|
||||
1. Gray area labels and descriptions in `present_gray_areas`
|
||||
2. Advisor research rationale rewrites in the synthesis step below
|
||||
|
||||
## advisor_research step
|
||||
|
||||
After the user selects gray areas in `present_gray_areas`, spawn parallel
|
||||
research agents.
|
||||
|
||||
1. Display brief status: `Researching {N} areas...`
|
||||
|
||||
2. For EACH user-selected gray area, spawn a `Task()` in parallel:
|
||||
|
||||
```
|
||||
Task(
|
||||
prompt="First, read @~/.claude/agents/gsd-advisor-researcher.md for your role and instructions.
|
||||
|
||||
<gray_area>{area_name}: {area_description from gray area identification}</gray_area>
|
||||
<phase_context>{phase_goal and description from ROADMAP.md}</phase_context>
|
||||
<project_context>{project name and brief description from PROJECT.md}</project_context>
|
||||
<calibration_tier>{resolved calibration tier: full_maturity | standard | minimal_decisive}</calibration_tier>
|
||||
|
||||
Research this gray area and return a structured comparison table with rationale.
|
||||
${AGENT_SKILLS_ADVISOR}",
|
||||
subagent_type="general-purpose",
|
||||
model="{ADVISOR_MODEL}",
|
||||
description="Research: {area_name}"
|
||||
)
|
||||
```
|
||||
|
||||
All `Task()` calls spawn simultaneously — do NOT wait for one before
|
||||
starting the next.
|
||||
|
||||
3. After ALL agents return, **synthesize results** before presenting:
|
||||
|
||||
For each agent's return:
|
||||
a. Parse the markdown comparison table and rationale paragraph
|
||||
b. Verify all 5 columns present (Option | Pros | Cons | Complexity | Recommendation) — fill any missing columns rather than showing broken table
|
||||
c. Verify option count matches calibration tier:
|
||||
- `full_maturity`: 3-5 options acceptable
|
||||
- `standard`: 2-4 options acceptable
|
||||
- `minimal_decisive`: 1-2 options acceptable
|
||||
If agent returned too many, trim least viable. If too few, accept as-is.
|
||||
d. Rewrite rationale paragraph to weave in project context and ongoing discussion context that the agent did not have access to
|
||||
e. If agent returned only 1 option, convert from table format to direct recommendation: "Standard approach for {area}: {option}. {rationale}"
|
||||
f. **If `NON_TECHNICAL_OWNER` is true:** apply a plain language rewrite to the rationale paragraph. Replace implementation-level terms with outcome descriptions the user can reason about without technical context. The Recommendation column value and the table structure remain intact. Do not remove detail; translate it. Example: "SWR uses stale-while-revalidate to serve cached responses immediately" → "This approach shows you something right away, then quietly updates in the background — users see data instantly."
|
||||
|
||||
4. Store synthesized tables for use in `discuss_areas` (table-first flow).
|
||||
|
||||
## discuss_areas (advisor table-first flow)
|
||||
|
||||
For each selected area:
|
||||
|
||||
1. **Present the synthesized comparison table + rationale paragraph** (from
|
||||
`advisor_research`)
|
||||
|
||||
2. **Use AskUserQuestion** (or text-mode equivalent if `--text` overlay):
|
||||
- header: `{area_name}`
|
||||
- question: `Which approach for {area_name}?`
|
||||
- options: extract from the table's Option column (AskUserQuestion adds
|
||||
"Other" automatically)
|
||||
|
||||
3. **Record the user's selection:**
|
||||
- If user picks from table options → record as locked decision for that
|
||||
area
|
||||
- If user picks "Other" → receive their input, reflect it back for
|
||||
confirmation, record
|
||||
|
||||
4. **Thinking partner (conditional):** same rule as default mode — if
|
||||
`features.thinking_partner` is enabled and tradeoff signals are
|
||||
detected, offer a 3-5 bullet analysis before locking in.
|
||||
|
||||
5. **After recording pick, decide whether follow-up questions are needed:**
|
||||
- If the pick has ambiguity that would affect downstream planning →
|
||||
ask 1-2 targeted follow-up questions using AskUserQuestion
|
||||
- If the pick is clear and self-contained → move to next area
|
||||
- Do NOT ask the standard 4 questions — the table already provided the
|
||||
context
|
||||
|
||||
6. **After all areas processed:**
|
||||
- header: "Done"
|
||||
- question: "That covers [list areas]. Ready to create context?"
|
||||
- options: "Create context" / "Revisit an area"
|
||||
|
||||
## Scope creep handling (advisor mode)
|
||||
|
||||
If user mentions something outside the phase domain:
|
||||
```
|
||||
"[Feature] sounds like a new capability — that belongs in its own phase.
|
||||
I'll note it as a deferred idea.
|
||||
|
||||
Back to [current area]: [return to current question]"
|
||||
```
|
||||
|
||||
Track deferred ideas internally.
|
||||
28
get-shit-done/workflows/discuss-phase/modes/all.md
Normal file
28
get-shit-done/workflows/discuss-phase/modes/all.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# --all mode — auto-select ALL gray areas, discuss interactively
|
||||
|
||||
> **Lazy-loaded.** Read this file from `workflows/discuss-phase.md` when
|
||||
> `--all` is present in `$ARGUMENTS`. Behavior overlays the default mode.
|
||||
|
||||
## Effect
|
||||
|
||||
- In `present_gray_areas`: auto-select ALL gray areas without asking the user
|
||||
(skips the AskUserQuestion area-selection step).
|
||||
- Discussion for each area proceeds **fully interactively** — the user drives
|
||||
every question for every area (use the default-mode `discuss_areas` flow).
|
||||
- Does NOT auto-advance to plan-phase afterward — use `--chain` or `--auto`
|
||||
if you want auto-advance.
|
||||
- Log: `[--all] Auto-selected all gray areas: [list area names].`
|
||||
|
||||
## Why this mode exists
|
||||
|
||||
This is the "discuss everything" shortcut: skip the selection friction, keep
|
||||
full interactive control over each individual question.
|
||||
|
||||
## Combination rules
|
||||
|
||||
- `--all --auto`: `--auto` wins for the discussion phase too (Claude picks
|
||||
recommended answers); `--all`'s contribution is just area auto-selection.
|
||||
- `--all --chain`: areas auto-selected, discussion interactive, then
|
||||
auto-advance to plan/execute (chain semantics).
|
||||
- `--all --batch` / `--all --text` / `--all --analyze`: layered overlays
|
||||
apply during discussion as documented in their respective files.
|
||||
44
get-shit-done/workflows/discuss-phase/modes/analyze.md
Normal file
44
get-shit-done/workflows/discuss-phase/modes/analyze.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# --analyze mode — trade-off tables before each question
|
||||
|
||||
> **Lazy-loaded overlay.** Read this file from `workflows/discuss-phase.md`
|
||||
> when `--analyze` is present in `$ARGUMENTS`. Combinable with default,
|
||||
> `--all`, `--chain`, `--text`, `--batch`.
|
||||
|
||||
## Effect
|
||||
|
||||
Before presenting each question (or question group, in batch mode), provide
|
||||
a brief **trade-off analysis** for the decision:
|
||||
- 2-3 options with pros/cons based on codebase context and common patterns
|
||||
- A recommended approach with reasoning
|
||||
- Known pitfalls or constraints from prior phases
|
||||
|
||||
## Example
|
||||
|
||||
```markdown
|
||||
**Trade-off analysis: Authentication strategy**
|
||||
|
||||
| Approach | Pros | Cons |
|
||||
|----------|------|------|
|
||||
| Session cookies | Simple, httpOnly prevents XSS | Requires CSRF protection, sticky sessions |
|
||||
| JWT (stateless) | Scalable, no server state | Token size, revocation complexity |
|
||||
| OAuth 2.0 + PKCE | Industry standard for SPAs | More setup, redirect flow UX |
|
||||
|
||||
💡 Recommended: OAuth 2.0 + PKCE — your app has social login in requirements (REQ-04) and this aligns with the existing NextAuth setup in `src/lib/auth.ts`.
|
||||
|
||||
How should users authenticate?
|
||||
```
|
||||
|
||||
This gives the user context to make informed decisions without extra
|
||||
prompting.
|
||||
|
||||
When `--analyze` is absent, present questions directly as before (no
|
||||
trade-off table).
|
||||
|
||||
## Sourcing the analysis
|
||||
|
||||
- Pros/cons should reflect the codebase context loaded in `scout_codebase`
|
||||
and any prior decisions surfaced in `load_prior_context`.
|
||||
- The recommendation must explicitly tie to project context (e.g.,
|
||||
existing libraries, prior phase decisions, documented requirements).
|
||||
- If a related ADR or spec is referenced in CONTEXT.md `<canonical_refs>`,
|
||||
cite it in the recommendation.
|
||||
56
get-shit-done/workflows/discuss-phase/modes/auto.md
Normal file
56
get-shit-done/workflows/discuss-phase/modes/auto.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# --auto mode — fully autonomous discuss-phase
|
||||
|
||||
> **Lazy-loaded.** Read this file from `workflows/discuss-phase.md` when
|
||||
> `--auto` is present in `$ARGUMENTS`. After the discussion completes, the
|
||||
> parent's `auto_advance` step also reads `modes/chain.md` to drive the
|
||||
> auto-advance to plan-phase.
|
||||
|
||||
## Effect across steps
|
||||
|
||||
- **`check_existing`**: if CONTEXT.md exists, auto-select "Update it" — load
|
||||
existing context and continue to `analyze_phase` (matches the parent step's
|
||||
documented `--auto` branch). If no context exists, continue without
|
||||
prompting. For interrupted checkpoints, auto-select "Resume". For existing
|
||||
plans, auto-select "Continue and replan after". Log every decision so the
|
||||
user can audit.
|
||||
- **`cross_reference_todos`**: fold all todos with relevance score >= 0.4
|
||||
automatically. Log the selection.
|
||||
- **`present_gray_areas`**: auto-select ALL gray areas. Log:
|
||||
`[--auto] Selected all gray areas: [list area names].`
|
||||
- **`discuss_areas`**: for each discussion question, choose the recommended
|
||||
option (first option, or the one explicitly marked "recommended") **without
|
||||
using AskUserQuestion**. Skip interactive prompts entirely. Log each
|
||||
auto-selected choice inline so the user can review decisions in the
|
||||
context file:
|
||||
```
|
||||
[auto] [Area] — Q: "[question text]" → Selected: "[chosen option]" (recommended default)
|
||||
```
|
||||
- After all areas are auto-resolved, skip the "Explore more gray areas"
|
||||
prompt and proceed directly to `write_context`.
|
||||
- After `write_context`, **auto-advance** to plan-phase via `modes/chain.md`.
|
||||
|
||||
## CRITICAL — Auto-mode pass cap
|
||||
|
||||
In `--auto` mode, the discuss step MUST complete in a **single pass**. After
|
||||
writing CONTEXT.md once, you are DONE — proceed immediately to
|
||||
`write_context` and then auto_advance. Do NOT re-read your own CONTEXT.md to
|
||||
find "gaps", "undefined types", or "missing decisions" and run additional
|
||||
passes. This creates a self-feeding loop where each pass generates references
|
||||
that the next pass treats as gaps, consuming unbounded time and resources.
|
||||
|
||||
Check the pass cap from config:
|
||||
```bash
|
||||
MAX_PASSES=$(gsd-sdk query config-get workflow.max_discuss_passes 2>/dev/null || echo "3")
|
||||
```
|
||||
|
||||
If you have already written and committed CONTEXT.md, the discuss step is
|
||||
complete. Move on.
|
||||
|
||||
## Combination rules
|
||||
|
||||
- `--auto --text` / `--auto --batch`: text/batch overlays are no-ops in
|
||||
auto mode (no user prompts to render).
|
||||
- `--auto --analyze`: trade-off tables can still be logged for the audit
|
||||
trail; selection still uses the recommended option.
|
||||
- `--auto --power`: `--power` wins (power mode generates files for offline
|
||||
answering — incompatible with autonomous selection).
|
||||
52
get-shit-done/workflows/discuss-phase/modes/batch.md
Normal file
52
get-shit-done/workflows/discuss-phase/modes/batch.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# --batch mode — grouped question batches
|
||||
|
||||
> **Lazy-loaded overlay.** Read this file from `workflows/discuss-phase.md`
|
||||
> when `--batch` is present in `$ARGUMENTS`. Combinable with default,
|
||||
> `--all`, `--chain`, `--text`, `--analyze`.
|
||||
|
||||
## Argument parsing
|
||||
|
||||
Parse optional `--batch` from `$ARGUMENTS`:
|
||||
- Accept `--batch`, `--batch=N`, or `--batch N`
|
||||
- Default to **4 questions per batch** when no number is provided
|
||||
- Clamp explicit sizes to **2–5** so a batch stays answerable
|
||||
- If `--batch` is absent, keep the existing one-question-at-a-time flow
|
||||
(default mode).
|
||||
|
||||
## Effect on discuss_areas
|
||||
|
||||
`--batch` mode: ask **2–5 numbered questions in one plain-text turn** per
|
||||
area, instead of the default 4 single-question AskUserQuestion turns.
|
||||
|
||||
- Group closely related questions for the current area into a single
|
||||
message
|
||||
- Keep each question concrete and answerable in one reply
|
||||
- When options are helpful, include short inline choices per question
|
||||
rather than a separate AskUserQuestion for every item
|
||||
- After the user replies, reflect back the captured decisions, note any
|
||||
unanswered items, and ask only the minimum follow-up needed before
|
||||
moving on
|
||||
- Preserve adaptiveness between batches: use the full set of answers to
|
||||
decide the next batch or whether the area is sufficiently clear
|
||||
|
||||
## Philosophy
|
||||
|
||||
Stay adaptive, but let the user choose the pacing.
|
||||
- Default mode: 4 single-question turns, then check whether to continue
|
||||
- `--batch` mode: 1 grouped turn with 2–5 numbered questions, then check
|
||||
whether to continue
|
||||
|
||||
Each answer set should reveal the next question or next batch.
|
||||
|
||||
## Example batch
|
||||
|
||||
```
|
||||
Authentication — please answer 1–4:
|
||||
|
||||
1. Which auth strategy? (a) Session cookies (b) JWT (c) OAuth 2.0 + PKCE
|
||||
2. Where do tokens live? (a) httpOnly cookie (b) localStorage (c) memory only
|
||||
3. Session lifetime? (a) 1h (b) 24h (c) 30d (d) configurable
|
||||
4. Account recovery? (a) email reset (b) magic link (c) both
|
||||
|
||||
Reply with your choices (e.g. "1c, 2a, 3b, 4c") or describe in your own words.
|
||||
```
|
||||
97
get-shit-done/workflows/discuss-phase/modes/chain.md
Normal file
97
get-shit-done/workflows/discuss-phase/modes/chain.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# --chain mode — interactive discuss, then auto-advance
|
||||
|
||||
> **Lazy-loaded.** Read this file from `workflows/discuss-phase.md` when
|
||||
> `--chain` is present in `$ARGUMENTS`, or when the parent's `auto_advance`
|
||||
> step needs to dispatch to plan-phase under `--auto`.
|
||||
|
||||
## Effect
|
||||
|
||||
- Discussion is **fully interactive** — questions, gray-area selection, and
|
||||
follow-ups behave exactly the same as default mode.
|
||||
- After discussion completes, **auto-advance to plan-phase → execute-phase**
|
||||
(same downstream behavior as `--auto`).
|
||||
- This is the middle ground: the user controls the discuss decisions, then
|
||||
plan and execute run autonomously.
|
||||
|
||||
## auto_advance step (executed by the parent file)
|
||||
|
||||
1. Parse `--auto` and `--chain` flags from `$ARGUMENTS`. **Note:** `--all`
|
||||
is NOT an auto-advance trigger — it only affects area selection. A
|
||||
session with `--all` but without `--auto` or `--chain` returns to manual
|
||||
next-steps after discussion completes.
|
||||
|
||||
2. **Sync chain flag with intent** — if user invoked manually (no `--auto`
|
||||
and no `--chain`), clear the ephemeral chain flag from any previous
|
||||
interrupted `--auto` chain. This does NOT touch `workflow.auto_advance`
|
||||
(the user's persistent settings preference):
|
||||
```bash
|
||||
if [[ ! "$ARGUMENTS" =~ --auto ]] && [[ ! "$ARGUMENTS" =~ --chain ]]; then
|
||||
gsd-sdk query config-set workflow._auto_chain_active false || true
|
||||
fi
|
||||
```
|
||||
|
||||
3. Read consolidated auto-mode (`active` = chain flag OR user preference):
|
||||
```bash
|
||||
AUTO_MODE=$(gsd-sdk query check auto-mode --pick active 2>/dev/null || echo "false")
|
||||
```
|
||||
|
||||
4. **If `--auto` or `--chain` flag present AND `AUTO_MODE` is not true:**
|
||||
Persist chain flag to config (handles direct usage without new-project):
|
||||
```bash
|
||||
gsd-sdk query config-set workflow._auto_chain_active true
|
||||
```
|
||||
|
||||
5. **If `--auto` flag present OR `--chain` flag present OR `AUTO_MODE` is
|
||||
true:** display banner and launch plan-phase.
|
||||
|
||||
Banner:
|
||||
```
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
GSD ► AUTO-ADVANCING TO PLAN
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
Context captured. Launching plan-phase...
|
||||
```
|
||||
|
||||
Launch plan-phase using the Skill tool to avoid nested Task sessions
|
||||
(which cause runtime freezes due to deep agent nesting — see #686):
|
||||
```
|
||||
Skill(skill="gsd-plan-phase", args="${PHASE} --auto ${GSD_WS}")
|
||||
```
|
||||
|
||||
This keeps the auto-advance chain flat — discuss, plan, and execute all
|
||||
run at the same nesting level rather than spawning increasingly deep
|
||||
Task agents.
|
||||
|
||||
6. **Handle plan-phase return:**
|
||||
|
||||
- **PHASE COMPLETE** → Full chain succeeded. Display:
|
||||
```
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
GSD ► PHASE ${PHASE} COMPLETE
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
Auto-advance pipeline finished: discuss → plan → execute
|
||||
|
||||
/clear then:
|
||||
|
||||
Next: /gsd-discuss-phase ${NEXT_PHASE} ${WAS_CHAIN ? "--chain" : "--auto"} ${GSD_WS}
|
||||
```
|
||||
- **PLANNING COMPLETE** → Planning done, execution didn't complete:
|
||||
```
|
||||
Auto-advance partial: Planning complete, execution did not finish.
|
||||
Continue: /gsd-execute-phase ${PHASE} ${GSD_WS}
|
||||
```
|
||||
- **PLANNING INCONCLUSIVE / CHECKPOINT** → Stop chain:
|
||||
```
|
||||
Auto-advance stopped: Planning needs input.
|
||||
Continue: /gsd-plan-phase ${PHASE} ${GSD_WS}
|
||||
```
|
||||
- **GAPS FOUND** → Stop chain:
|
||||
```
|
||||
Auto-advance stopped: Gaps found during execution.
|
||||
Continue: /gsd-plan-phase ${PHASE} --gaps ${GSD_WS}
|
||||
```
|
||||
|
||||
7. **If none of `--auto`, `--chain`, nor config enabled:** route to
|
||||
`confirm_creation` step (existing behavior — show manual next steps).
|
||||
141
get-shit-done/workflows/discuss-phase/modes/default.md
Normal file
141
get-shit-done/workflows/discuss-phase/modes/default.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# Default mode — interactive discuss-phase
|
||||
|
||||
> **Lazy-loaded.** Read this file from `workflows/discuss-phase.md` when no
|
||||
> mode flag is present (the baseline interactive flow). When `--text`,
|
||||
> `--batch`, or `--analyze` is also present, layer the corresponding overlay
|
||||
> file from this directory on top of the rules below.
|
||||
|
||||
This document defines `discuss_areas` for the default flow. The shared steps
|
||||
that come before (`initialize`, `check_blocking_antipatterns`, `check_spec`,
|
||||
`check_existing`, `load_prior_context`, `cross_reference_todos`,
|
||||
`scout_codebase`, `analyze_phase`, `present_gray_areas`) live in the parent
|
||||
file and run for every mode.
|
||||
|
||||
## discuss_areas (default, interactive)
|
||||
|
||||
For each selected area, conduct a focused discussion loop.
|
||||
|
||||
**Research-before-questions mode:** Check if `workflow.research_before_questions` is enabled in config (from init context or `.planning/config.json`). When enabled, before presenting questions for each area:
|
||||
1. Do a brief web search for best practices related to the area topic
|
||||
2. Summarize the top findings in 2-3 bullet points
|
||||
3. Present the research alongside the question so the user can make a more informed decision
|
||||
|
||||
Example with research enabled:
|
||||
```text
|
||||
Let's talk about [Authentication Strategy].
|
||||
|
||||
📊 Best practices research:
|
||||
• OAuth 2.0 + PKCE is the current standard for SPAs (replaces implicit flow)
|
||||
• Session tokens with httpOnly cookies preferred over localStorage for XSS protection
|
||||
• Consider passkey/WebAuthn support — adoption is accelerating in 2025-2026
|
||||
|
||||
With that context: How should users authenticate?
|
||||
```
|
||||
|
||||
When disabled (default), skip the research and present questions directly as before.
|
||||
|
||||
**Philosophy:** stay adaptive. Default flow is 4 single-question turns, then
|
||||
check whether to continue. Each answer should reveal the next question.
|
||||
|
||||
**For each area:**
|
||||
|
||||
1. **Announce the area:**
|
||||
```text
|
||||
Let's talk about [Area].
|
||||
```
|
||||
|
||||
2. **Ask 4 questions using AskUserQuestion:**
|
||||
- header: "[Area]" (max 12 chars — abbreviate if needed)
|
||||
- question: Specific decision for this area
|
||||
- options: 2-3 concrete choices (AskUserQuestion adds "Other" automatically), with the recommended choice highlighted and brief explanation why
|
||||
- **Annotate options with code context** when relevant:
|
||||
```text
|
||||
"How should posts be displayed?"
|
||||
- Cards (reuses existing Card component — consistent with Messages)
|
||||
- List (simpler, would be a new pattern)
|
||||
- Timeline (needs new Timeline component — none exists yet)
|
||||
```
|
||||
- Include "You decide" as an option when reasonable — captures Claude discretion
|
||||
- **Context7 for library choices:** When a gray area involves library selection (e.g., "magic links" → query next-auth docs) or API approach decisions, use `mcp__context7__*` tools to fetch current documentation and inform the options. Don't use Context7 for every question — only when library-specific knowledge improves the options.
|
||||
|
||||
3. **After the current set of questions, check:**
|
||||
- header: "[Area]" (max 12 chars)
|
||||
- question: "More questions about [area], or move to next? (Remaining: [list other unvisited areas])"
|
||||
- options: "More questions" / "Next area"
|
||||
|
||||
When building the question text, list the remaining unvisited areas so the user knows what's ahead. For example: "More questions about Layout, or move to next? (Remaining: Loading behavior, Content ordering)"
|
||||
|
||||
If "More questions" → ask another 4 single questions, then check again
|
||||
If "Next area" → proceed to next selected area
|
||||
If "Other" (free text) → interpret intent: continuation phrases ("chat more", "keep going", "yes", "more") map to "More questions"; advancement phrases ("done", "move on", "next", "skip") map to "Next area". If ambiguous, ask: "Continue with more questions about [area], or move to the next area?"
|
||||
|
||||
4. **After all initially-selected areas complete:**
|
||||
- Summarize what was captured from the discussion so far
|
||||
- AskUserQuestion:
|
||||
- header: "Done"
|
||||
- question: "We've discussed [list areas]. Which gray areas remain unclear?"
|
||||
- options: "Explore more gray areas" / "I'm ready for context"
|
||||
- If "Explore more gray areas":
|
||||
- Identify 2-4 additional gray areas based on what was learned
|
||||
- Return to present_gray_areas logic with these new areas
|
||||
- Loop: discuss new areas, then prompt again
|
||||
- If "I'm ready for context": Proceed to write_context
|
||||
|
||||
**Canonical ref accumulation during discussion:**
|
||||
When the user references a doc, spec, or ADR during any answer — e.g., "read adr-014", "check the MCP spec", "per browse-spec.md" — immediately:
|
||||
1. Read the referenced doc (or confirm it exists)
|
||||
2. Add it to the canonical refs accumulator with full relative path
|
||||
3. Use what you learned from the doc to inform subsequent questions
|
||||
|
||||
These user-referenced docs are often MORE important than ROADMAP.md refs because they represent docs the user specifically wants downstream agents to follow. Never drop them.
|
||||
|
||||
**Question design:**
|
||||
- Options should be concrete, not abstract ("Cards" not "Option A")
|
||||
- Each answer should inform the next question or next batch
|
||||
- If user picks "Other" to provide freeform input (e.g., "let me describe it", "something else", or an open-ended reply), ask your follow-up as plain text — NOT another AskUserQuestion. Wait for them to type at the normal prompt, then reflect their input back and confirm before resuming AskUserQuestion or the next numbered batch.
|
||||
|
||||
**Thinking partner (conditional):**
|
||||
If `features.thinking_partner` is enabled in config, check the user's answer for tradeoff signals
|
||||
(see `references/thinking-partner.md` for signal list). If tradeoff detected:
|
||||
|
||||
```text
|
||||
I notice competing priorities here — {option_A} optimizes for {goal_A} while {option_B} optimizes for {goal_B}.
|
||||
|
||||
Want me to think through the tradeoffs before we lock this in?
|
||||
[Yes, analyze] / [No, decision made]
|
||||
```
|
||||
|
||||
If yes: provide 3-5 bullet analysis (what each optimizes/sacrifices, alignment with PROJECT.md goals, recommendation). Then return to normal flow.
|
||||
|
||||
**Scope creep handling:**
|
||||
If user mentions something outside the phase domain:
|
||||
```text
|
||||
"[Feature] sounds like a new capability — that belongs in its own phase.
|
||||
I'll note it as a deferred idea.
|
||||
|
||||
Back to [current area]: [return to current question]"
|
||||
```
|
||||
|
||||
Track deferred ideas internally.
|
||||
|
||||
**Incremental checkpoint — save after each area completes:**
|
||||
|
||||
After each area is resolved (user says "Next area"), immediately write a checkpoint file with all decisions captured so far. This prevents data loss if the session is interrupted mid-discussion.
|
||||
|
||||
**Checkpoint file:** `${phase_dir}/${padded_phase}-DISCUSS-CHECKPOINT.json`
|
||||
|
||||
Schema: read `workflows/discuss-phase/templates/checkpoint.json` for the
|
||||
canonical structure — copy it and substitute the live values.
|
||||
|
||||
**On session resume:** Handled in the parent's `check_existing` step. After
|
||||
`write_context` completes successfully, the parent's `git_commit` step
|
||||
deletes the checkpoint.
|
||||
|
||||
**Track discussion log data internally:**
|
||||
For each question asked, accumulate:
|
||||
- Area name
|
||||
- All options presented (label + description)
|
||||
- Which option the user selected (or their free-text response)
|
||||
- Any follow-up notes or clarifications the user provided
|
||||
|
||||
This data is used to generate DISCUSSION-LOG.md in the parent's `git_commit` step.
|
||||
44
get-shit-done/workflows/discuss-phase/modes/power.md
Normal file
44
get-shit-done/workflows/discuss-phase/modes/power.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# --power mode — bulk question generation, async answering
|
||||
|
||||
> **Lazy-loaded.** Read this file from `workflows/discuss-phase.md` when
|
||||
> `--power` is present in `$ARGUMENTS`. The full step-by-step instructions
|
||||
> live in the existing `discuss-phase-power.md` workflow file (kept stable
|
||||
> at its original path so installed `@`-references continue to resolve).
|
||||
|
||||
## Dispatch
|
||||
|
||||
```
|
||||
Read @~/.claude/get-shit-done/workflows/discuss-phase-power.md
|
||||
```
|
||||
|
||||
Execute it end-to-end. Do not continue with the standard interactive steps.
|
||||
|
||||
## Summary of flow
|
||||
|
||||
The power user mode generates ALL questions upfront into machine-readable
|
||||
and human-friendly files, then waits for the user to answer at their own
|
||||
pace before processing all answers in a single pass.
|
||||
|
||||
1. Run the same phase analysis (gray area identification) as standard mode
|
||||
2. Write all questions to
|
||||
`{phase_dir}/{padded_phase}-QUESTIONS.json` and
|
||||
`{phase_dir}/{padded_phase}-QUESTIONS.html`
|
||||
3. Notify user with file paths and wait for a "refresh" or "finalize"
|
||||
command
|
||||
4. On "refresh": read the JSON, process answered questions, update stats
|
||||
and HTML
|
||||
5. On "finalize": read all answers from JSON, generate CONTEXT.md in the
|
||||
standard format
|
||||
|
||||
## When to use
|
||||
|
||||
Large phases with many gray areas, or when users prefer to answer
|
||||
questions offline / asynchronously rather than interactively in the chat
|
||||
session.
|
||||
|
||||
## Combination rules
|
||||
|
||||
- `--power --auto`: power wins. Power mode is incompatible with
|
||||
autonomous selection — its purpose is offline answering.
|
||||
- `--power --chain`: after the power-mode finalize step writes
|
||||
CONTEXT.md, the chain auto-advance still applies (Read `chain.md`).
|
||||
55
get-shit-done/workflows/discuss-phase/modes/text.md
Normal file
55
get-shit-done/workflows/discuss-phase/modes/text.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# --text mode — plain-text overlay (no AskUserQuestion)
|
||||
|
||||
> **Lazy-loaded overlay.** Read this file from `workflows/discuss-phase.md`
|
||||
> when `--text` is present in `$ARGUMENTS`, OR when
|
||||
> `workflow.text_mode: true` is set in config (e.g., per-project default).
|
||||
|
||||
## Effect
|
||||
|
||||
When text mode is active, **do not use AskUserQuestion at all**. Instead,
|
||||
present every question as a plain-text numbered list and ask the user to
|
||||
type their choice number. Free-text input maps to the "Other" branch of
|
||||
the equivalent AskUserQuestion call.
|
||||
|
||||
This is required for Claude Code remote sessions (`/rc` mode) where the
|
||||
Claude App cannot forward TUI menu selections back to the host.
|
||||
|
||||
## Activation
|
||||
|
||||
- Per-session: pass `--text` flag to any command (e.g.,
|
||||
`/gsd-discuss-phase --text`)
|
||||
- Per-project: `gsd-sdk query config-set workflow.text_mode true`
|
||||
|
||||
Text mode applies to ALL workflows in the session, not just discuss-phase.
|
||||
|
||||
## Question rendering
|
||||
|
||||
Replace this:
|
||||
```text
|
||||
AskUserQuestion(
|
||||
header="Layout",
|
||||
question="How should posts be displayed?",
|
||||
options=["Cards", "List", "Timeline"]
|
||||
)
|
||||
```
|
||||
|
||||
With this:
|
||||
```text
|
||||
Layout — How should posts be displayed?
|
||||
1. Cards
|
||||
2. List
|
||||
3. Timeline
|
||||
4. Other (type freeform)
|
||||
|
||||
Reply with a number, or describe your preference.
|
||||
```
|
||||
|
||||
Wait for the user's reply at the normal prompt. Parse:
|
||||
- Numeric reply → mapped to that option
|
||||
- Free text → treated as "Other" — reflect it back, confirm, then proceed
|
||||
|
||||
## Empty-answer handling
|
||||
|
||||
The same answer-validation rules from the parent file apply: empty
|
||||
responses trigger one retry, then a clarifying question. Do not proceed
|
||||
with empty input.
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"phase": "{PHASE_NUM}",
|
||||
"phase_name": "{phase_name}",
|
||||
"timestamp": "{ISO timestamp}",
|
||||
"areas_completed": ["Area 1", "Area 2"],
|
||||
"areas_remaining": ["Area 3", "Area 4"],
|
||||
"decisions": {
|
||||
"Area 1": [
|
||||
{"question": "...", "answer": "...", "options_presented": ["..."]},
|
||||
{"question": "...", "answer": "...", "options_presented": ["..."]}
|
||||
],
|
||||
"Area 2": [
|
||||
{"question": "...", "answer": "...", "options_presented": ["..."]}
|
||||
]
|
||||
},
|
||||
"deferred_ideas": ["..."],
|
||||
"canonical_refs": ["..."]
|
||||
}
|
||||
136
get-shit-done/workflows/discuss-phase/templates/context.md
Normal file
136
get-shit-done/workflows/discuss-phase/templates/context.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# CONTEXT.md template — for discuss-phase write_context step
|
||||
|
||||
> **Lazy-loaded.** Read this file only inside the `write_context` step of
|
||||
> `workflows/discuss-phase.md`, immediately before writing
|
||||
> `${phase_dir}/${padded_phase}-CONTEXT.md`. Do not put a reference to this
|
||||
> file in `<required_reading>` — that defeats the progressive-disclosure
|
||||
> savings introduced by issue #2551.
|
||||
|
||||
## Variable substitutions
|
||||
|
||||
The caller substitutes:
|
||||
- `[X]` → phase number
|
||||
- `[Name]` → phase name
|
||||
- `[date]` → ISO date when context was gathered
|
||||
- `${padded_phase}` → zero-padded phase number (e.g., `07`, `15`)
|
||||
- `{N}` → counts (requirements, etc.)
|
||||
|
||||
## Conditional sections
|
||||
|
||||
- **`<spec_lock>`** — include only when `spec_loaded = true` (a `*-SPEC.md`
|
||||
was found by `check_spec`). Otherwise omit the entire `<spec_lock>` block.
|
||||
- **Folded Todos / Reviewed Todos** — include subsections only when the
|
||||
`cross_reference_todos` step folded or reviewed at least one todo.
|
||||
|
||||
## Template body
|
||||
|
||||
```markdown
|
||||
# Phase [X]: [Name] - Context
|
||||
|
||||
**Gathered:** [date]
|
||||
**Status:** Ready for planning
|
||||
|
||||
<domain>
|
||||
## Phase Boundary
|
||||
|
||||
[Clear statement of what this phase delivers — the scope anchor]
|
||||
|
||||
</domain>
|
||||
|
||||
[If spec_loaded = true, insert this section:]
|
||||
<spec_lock>
|
||||
## Requirements (locked via SPEC.md)
|
||||
|
||||
**{N} requirements are locked.** See `{padded_phase}-SPEC.md` for full requirements, boundaries, and acceptance criteria.
|
||||
|
||||
Downstream agents MUST read `{padded_phase}-SPEC.md` before planning or implementing. Requirements are not duplicated here.
|
||||
|
||||
**In scope (from SPEC.md):** [copy the "In scope" bullet list from SPEC.md Boundaries]
|
||||
**Out of scope (from SPEC.md):** [copy the "Out of scope" bullet list from SPEC.md Boundaries]
|
||||
|
||||
</spec_lock>
|
||||
|
||||
<decisions>
|
||||
## Implementation Decisions
|
||||
|
||||
### [Category 1 that was discussed]
|
||||
- **D-01:** [Decision or preference captured]
|
||||
- **D-02:** [Another decision if applicable]
|
||||
|
||||
### [Category 2 that was discussed]
|
||||
- **D-03:** [Decision or preference captured]
|
||||
|
||||
### Claude's Discretion
|
||||
[Areas where user said "you decide" — note that Claude has flexibility here]
|
||||
|
||||
### Folded Todos
|
||||
[If any todos were folded into scope from the cross_reference_todos step, list them here.
|
||||
Each entry should include the todo title, original problem, and how it fits this phase's scope.
|
||||
If no todos were folded: omit this subsection entirely.]
|
||||
|
||||
</decisions>
|
||||
|
||||
<canonical_refs>
|
||||
## Canonical References
|
||||
|
||||
**Downstream agents MUST read these before planning or implementing.**
|
||||
|
||||
[MANDATORY section. Write the FULL accumulated canonical refs list here.
|
||||
Sources: ROADMAP.md refs + REQUIREMENTS.md refs + user-referenced docs during
|
||||
discussion + any docs discovered during codebase scout. Group by topic area.
|
||||
Every entry needs a full relative path — not just a name.]
|
||||
|
||||
### [Topic area 1]
|
||||
- `path/to/adr-or-spec.md` — [What it decides/defines that's relevant]
|
||||
- `path/to/doc.md` §N — [Specific section reference]
|
||||
|
||||
### [Topic area 2]
|
||||
- `path/to/feature-doc.md` — [What this doc defines]
|
||||
|
||||
[If no external specs: "No external specs — requirements fully captured in decisions above"]
|
||||
|
||||
</canonical_refs>
|
||||
|
||||
<code_context>
|
||||
## Existing Code Insights
|
||||
|
||||
### Reusable Assets
|
||||
- [Component/hook/utility]: [How it could be used in this phase]
|
||||
|
||||
### Established Patterns
|
||||
- [Pattern]: [How it constrains/enables this phase]
|
||||
|
||||
### Integration Points
|
||||
- [Where new code connects to existing system]
|
||||
|
||||
</code_context>
|
||||
|
||||
<specifics>
|
||||
## Specific Ideas
|
||||
|
||||
[Any particular references, examples, or "I want it like X" moments from discussion]
|
||||
|
||||
[If none: "No specific requirements — open to standard approaches"]
|
||||
|
||||
</specifics>
|
||||
|
||||
<deferred>
|
||||
## Deferred Ideas
|
||||
|
||||
[Ideas that came up but belong in other phases. Don't lose them.]
|
||||
|
||||
### Reviewed Todos (not folded)
|
||||
[If any todos were reviewed in cross_reference_todos but not folded into scope,
|
||||
list them here so future phases know they were considered.
|
||||
Each entry: todo title + reason it was deferred (out of scope, belongs in Phase Y, etc.)
|
||||
If no reviewed-but-deferred todos: omit this subsection entirely.]
|
||||
|
||||
[If none: "None — discussion stayed within phase scope"]
|
||||
|
||||
</deferred>
|
||||
|
||||
---
|
||||
|
||||
*Phase: [X]-[Name]*
|
||||
*Context gathered: [date]*
|
||||
```
|
||||
@@ -0,0 +1,50 @@
|
||||
# DISCUSSION-LOG.md template — for discuss-phase git_commit step
|
||||
|
||||
> **Lazy-loaded.** Read this file only inside the `git_commit` step of
|
||||
> `workflows/discuss-phase.md`, immediately before writing
|
||||
> `${phase_dir}/${padded_phase}-DISCUSSION-LOG.md`.
|
||||
|
||||
## Purpose
|
||||
|
||||
Audit trail for human review (compliance, learning, retrospectives). NOT
|
||||
consumed by downstream agents — those read CONTEXT.md only.
|
||||
|
||||
## Template body
|
||||
|
||||
```markdown
|
||||
# Phase [X]: [Name] - Discussion Log
|
||||
|
||||
> **Audit trail only.** Do not use as input to planning, research, or execution agents.
|
||||
> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered.
|
||||
|
||||
**Date:** [ISO date]
|
||||
**Phase:** [phase number]-[phase name]
|
||||
**Areas discussed:** [comma-separated list]
|
||||
|
||||
---
|
||||
|
||||
[For each gray area discussed:]
|
||||
|
||||
## [Area Name]
|
||||
|
||||
| Option | Description | Selected |
|
||||
|--------|-------------|----------|
|
||||
| [Option 1] | [Description from AskUserQuestion] | |
|
||||
| [Option 2] | [Description] | ✓ |
|
||||
| [Option 3] | [Description] | |
|
||||
|
||||
**User's choice:** [Selected option or free-text response]
|
||||
**Notes:** [Any clarifications, follow-up context, or rationale the user provided]
|
||||
|
||||
---
|
||||
|
||||
[Repeat for each area]
|
||||
|
||||
## Claude's Discretion
|
||||
|
||||
[List areas where user said "you decide" or deferred to Claude]
|
||||
|
||||
## Deferred Ideas
|
||||
|
||||
[Ideas mentioned during discussion that were noted for future phases]
|
||||
```
|
||||
@@ -16,7 +16,7 @@ Load docs-update context:
|
||||
```bash
|
||||
INIT=$(gsd-sdk query docs-init)
|
||||
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
AGENT_SKILLS=$(gsd-sdk query agent-skills gsd-doc-writer 2>/dev/null)
|
||||
AGENT_SKILLS=$(gsd-sdk query agent-skills gsd-doc-writer)
|
||||
```
|
||||
|
||||
Extract from init JSON:
|
||||
|
||||
@@ -69,11 +69,13 @@ Load all context in one call:
|
||||
```bash
|
||||
INIT=$(gsd-sdk query init.execute-phase "${PHASE_ARG}")
|
||||
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
AGENT_SKILLS=$(gsd-sdk query agent-skills gsd-executor 2>/dev/null)
|
||||
AGENT_SKILLS=$(gsd-sdk query agent-skills gsd-executor)
|
||||
```
|
||||
|
||||
Parse JSON for: `executor_model`, `verifier_model`, `commit_docs`, `parallelization`, `branching_strategy`, `branch_name`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `plans`, `incomplete_plans`, `plan_count`, `incomplete_count`, `state_exists`, `roadmap_exists`, `phase_req_ids`, `response_language`.
|
||||
|
||||
**Model resolution:** If `executor_model` is `"inherit"`, omit the `model=` parameter from all `Task()` calls — do NOT pass `model="inherit"` to Task. Omitting the `model=` parameter causes Claude Code to inherit the current orchestrator model automatically. Only set `model=` when `executor_model` is an explicit model name (e.g., `"claude-sonnet-4-6"`, `"claude-opus-4-7"`).
|
||||
|
||||
**If `response_language` is set:** Include `response_language: {value}` in all spawned subagent prompts so any user-facing output stays in the configured language.
|
||||
|
||||
Read worktree config:
|
||||
@@ -128,7 +130,7 @@ inline path for each plan.
|
||||
```bash
|
||||
# REQUIRED: prevents stale auto-chain from previous --auto runs
|
||||
if [[ ! "$ARGUMENTS" =~ --auto ]]; then
|
||||
gsd-sdk query config-set workflow._auto_chain_active false 2>/dev/null
|
||||
gsd-sdk query config-set workflow._auto_chain_active false || true
|
||||
fi
|
||||
```
|
||||
</step>
|
||||
@@ -334,6 +336,26 @@ CROSS_AI_TIMEOUT=$(gsd-sdk query config-get workflow.cross_ai_timeout 2>/dev/nul
|
||||
<step name="execute_waves">
|
||||
Execute each selected wave in sequence. Within a wave: parallel if `PARALLELIZATION=true`, sequential if `false`.
|
||||
|
||||
**Stream-idle-timeout prevention — checkpoint heartbeats (#2410):**
|
||||
|
||||
Multi-plan phases can accumulate enough subagent context that the Claude API
|
||||
SSE layer terminates with `Stream idle timeout - partial response received`
|
||||
between a large tool_result and the next assistant turn (seen on Claude Code
|
||||
+ Opus 4.7 at ~200K+ cache_read). To keep the stream warm, emit short
|
||||
assistant-text heartbeats — **no tool call, just a literal line** — at every
|
||||
wave and plan boundary. Each heartbeat MUST start with `[checkpoint]` so
|
||||
tooling and `/gsd-manager`'s background-completion handler can grep partial
|
||||
transcripts. `{P}/{Q}` is the phase-wide completed/total plans counter and
|
||||
increases monotonically across waves. `{status}` is `complete` (success),
|
||||
`failed` (executor error), or `checkpoint` (human-gate returned).
|
||||
|
||||
```
|
||||
[checkpoint] phase {PHASE_NUMBER} wave {N}/{M} starting, {wave_plan_count} plan(s), {P}/{Q} plans done
|
||||
[checkpoint] phase {PHASE_NUMBER} wave {N}/{M} plan {plan_id} starting ({P}/{Q} plans done)
|
||||
[checkpoint] phase {PHASE_NUMBER} wave {N}/{M} plan {plan_id} {status} ({P}/{Q} plans done)
|
||||
[checkpoint] phase {PHASE_NUMBER} wave {N}/{M} complete, {P}/{Q} plans done ({wave_success}/{wave_plan_count} ok)
|
||||
```
|
||||
|
||||
**For each wave:**
|
||||
|
||||
1. **Intra-wave files_modified overlap check (BEFORE spawning):**
|
||||
@@ -372,7 +394,15 @@ Execute each selected wave in sequence. Within a wave: parallel if `PARALLELIZAT
|
||||
|
||||
2. **Describe what's being built (BEFORE spawning):**
|
||||
|
||||
Read each plan's `<objective>`. Extract what's being built and why.
|
||||
**First, emit the wave-start checkpoint heartbeat as a literal assistant-text
|
||||
line — no tool call (#2410). Do NOT skip this even for single-plan waves; it
|
||||
is required before any further reasoning or spawning:**
|
||||
|
||||
```
|
||||
[checkpoint] phase {PHASE_NUMBER} wave {N}/{M} starting, {wave_plan_count} plan(s), {P}/{Q} plans done
|
||||
```
|
||||
|
||||
Then read each plan's `<objective>`. Extract what's being built and why.
|
||||
|
||||
```
|
||||
---
|
||||
@@ -390,6 +420,13 @@ Execute each selected wave in sequence. Within a wave: parallel if `PARALLELIZAT
|
||||
|
||||
3. **Spawn executor agents:**
|
||||
|
||||
**Emit a plan-start heartbeat (literal line, no tool call) immediately before
|
||||
each `Task()` dispatch (#2410):**
|
||||
|
||||
```
|
||||
[checkpoint] phase {PHASE_NUMBER} wave {N}/{M} plan {plan_id} starting ({P}/{Q} plans done)
|
||||
```
|
||||
|
||||
Pass paths only — executors read files themselves with their fresh context window.
|
||||
For 200k models, this keeps orchestrator context lean (~10-15%).
|
||||
For 1M+ models (Opus 4.6, Sonnet 4.6), richer context can be passed directly.
|
||||
@@ -421,7 +458,10 @@ Execute each selected wave in sequence. Within a wave: parallel if `PARALLELIZAT
|
||||
Task(
|
||||
subagent_type="gsd-executor",
|
||||
description="Execute plan {plan_number} of phase {phase_number}",
|
||||
model="{executor_model}",
|
||||
# Only include model= when executor_model is an explicit model name.
|
||||
# When executor_model is "inherit", omit this parameter entirely so
|
||||
# Claude Code inherits the orchestrator model automatically.
|
||||
model="{executor_model}", # omit this line when executor_model == "inherit"
|
||||
isolation="worktree",
|
||||
prompt="
|
||||
<objective>
|
||||
@@ -547,6 +587,16 @@ Execute each selected wave in sequence. Within a wave: parallel if `PARALLELIZAT
|
||||
|
||||
4. **Wait for all agents in wave to complete.**
|
||||
|
||||
**Plan-complete heartbeat (#2410):** as each executor returns (or is verified
|
||||
via spot-check below), emit one line — `complete` advances `{P}`, `failed`
|
||||
and `checkpoint` do not but still warm the stream:
|
||||
|
||||
```
|
||||
[checkpoint] phase {PHASE_NUMBER} wave {N}/{M} plan {plan_id} complete ({P}/{Q} plans done)
|
||||
[checkpoint] phase {PHASE_NUMBER} wave {N}/{M} plan {plan_id} failed ({P}/{Q} plans done)
|
||||
[checkpoint] phase {PHASE_NUMBER} wave {N}/{M} plan {plan_id} checkpoint ({P}/{Q} plans done)
|
||||
```
|
||||
|
||||
**Completion signal fallback (Copilot and runtimes where Task() may not return):**
|
||||
|
||||
If a spawned agent does not return a completion signal but appears to have finished
|
||||
@@ -649,10 +699,15 @@ Execute each selected wave in sequence. Within a wave: parallel if `PARALLELIZAT
|
||||
|
||||
# Detect files deleted on main but re-added by worktree merge
|
||||
# (e.g., archived phase directories that were intentionally removed)
|
||||
# A "resurrected" file must have a deletion event in main's ancestry —
|
||||
# brand-new files (e.g. SUMMARY.md just created by the executor) have no
|
||||
# such history and must NOT be removed (#2501).
|
||||
DELETED_FILES=$(git diff --diff-filter=A --name-only HEAD~1 -- .planning/ 2>/dev/null || true)
|
||||
for RESURRECTED in $DELETED_FILES; do
|
||||
# Check if this file was NOT in main's pre-merge tree
|
||||
if ! echo "$PRE_MERGE_FILES" | grep -qxF "$RESURRECTED"; then
|
||||
# Only delete if this file was previously tracked on main and then
|
||||
# deliberately removed (has a deletion event in git history).
|
||||
WAS_DELETED=$(git log --follow --diff-filter=D --name-only --format="" HEAD~1 -- "$RESURRECTED" 2>/dev/null | grep -c . || true)
|
||||
if [ "${WAS_DELETED:-0}" -gt 0 ]; then
|
||||
git rm -f "$RESURRECTED" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
@@ -820,6 +875,15 @@ Execute each selected wave in sequence. Within a wave: parallel if `PARALLELIZAT
|
||||
|
||||
6. **Report completion — spot-check claims first:**
|
||||
|
||||
**Wave-close heartbeat (#2410):** after spot-checks finish (pass or fail),
|
||||
before the `## Wave {N} Complete` summary, emit as a literal line:
|
||||
|
||||
```
|
||||
[checkpoint] phase {PHASE_NUMBER} wave {N}/{M} complete, {P}/{Q} plans done ({wave_success}/{wave_plan_count} ok)
|
||||
```
|
||||
|
||||
|
||||
|
||||
For each SUMMARY.md:
|
||||
- Verify first 2 files from `key-files.created` exist on disk
|
||||
- Check `git log --oneline --all --grep="{phase}-{plan}"` returns ≥1 commit
|
||||
@@ -1260,11 +1324,22 @@ If `TEXT_MODE` is true, present as a plain-text numbered list. Otherwise use Ask
|
||||
**If user selects option 3:** Stop execution. Report partial completion.
|
||||
</step>
|
||||
|
||||
<step name="codebase_drift_gate">
|
||||
Post-execution structural drift detection (#2003). Non-blocking by contract:
|
||||
any internal error here MUST fall through to `verify_phase_goal`. The phase
|
||||
is never failed by this gate.
|
||||
|
||||
Load and follow the full step spec from
|
||||
`get-shit-done/workflows/execute-phase/steps/codebase-drift-gate.md` —
|
||||
covers the SDK call, JSON contract, `warn` vs `auto-remap` branches, mapper
|
||||
spawn template, and the two `workflow.drift_*` config keys.
|
||||
</step>
|
||||
|
||||
<step name="verify_phase_goal">
|
||||
Verify phase achieved its GOAL, not just completed tasks.
|
||||
|
||||
```bash
|
||||
VERIFIER_SKILLS=$(gsd-sdk query agent-skills gsd-verifier 2>/dev/null)
|
||||
VERIFIER_SKILLS=$(gsd-sdk query agent-skills gsd-verifier)
|
||||
```
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# Step: codebase_drift_gate
|
||||
|
||||
Post-execution structural drift detection (#2003). Runs after the last wave
|
||||
commits, before verification. **Non-blocking by contract:** any internal
|
||||
error here MUST fall through and continue to `verify_phase_goal`. The phase
|
||||
is never failed by this gate.
|
||||
|
||||
```bash
|
||||
DRIFT=$(gsd-sdk query verify.codebase-drift 2>/dev/null || echo '{"skipped":true,"reason":"sdk-failed"}')
|
||||
```
|
||||
|
||||
Parse JSON for: `skipped`, `reason`, `action_required`, `directive`,
|
||||
`spawn_mapper`, `affected_paths`, `elements`, `threshold`, `action`,
|
||||
`last_mapped_commit`, `message`.
|
||||
|
||||
**If `skipped` is true (no STRUCTURE.md, missing git, or any internal error):**
|
||||
Log one line — `Codebase drift check skipped: {reason}` — and continue to
|
||||
`verify_phase_goal`. Do NOT prompt the user. Do NOT block.
|
||||
|
||||
**If `action_required` is false:** Continue silently to `verify_phase_goal`.
|
||||
|
||||
**If `action_required` is true AND `directive` is `warn`:**
|
||||
Print the `message` field verbatim. The format is:
|
||||
|
||||
```text
|
||||
Codebase drift detected: {N} structural element(s) since last mapping.
|
||||
|
||||
New directories:
|
||||
- {path}
|
||||
New barrel exports:
|
||||
- {path}
|
||||
New migrations:
|
||||
- {path}
|
||||
New route modules:
|
||||
- {path}
|
||||
|
||||
Run /gsd-map-codebase --paths {affected_paths} to refresh planning context.
|
||||
```
|
||||
|
||||
Then continue to `verify_phase_goal`. Do NOT block. Do NOT spawn anything.
|
||||
|
||||
**If `action_required` is true AND `directive` is `auto-remap`:**
|
||||
|
||||
First load the mapper agent's skill bundle (the executor's `AGENT_SKILLS`
|
||||
from step `init_context` is for `gsd-executor`, not the mapper):
|
||||
|
||||
```bash
|
||||
AGENT_SKILLS_MAPPER=$(gsd-sdk query agent-skills gsd-codebase-mapper)
|
||||
```
|
||||
|
||||
Then spawn `gsd-codebase-mapper` agents with the `--paths` hint:
|
||||
|
||||
```text
|
||||
Task(
|
||||
subagent_type="gsd-codebase-mapper",
|
||||
description="Incremental codebase remap (drift)",
|
||||
prompt="Focus: arch
|
||||
Today's date: {date}
|
||||
--paths {affected_paths joined by comma}
|
||||
|
||||
Refresh STRUCTURE.md and ARCHITECTURE.md scoped to the listed paths only.
|
||||
Stamp last_mapped_commit in each document's frontmatter.
|
||||
${AGENT_SKILLS_MAPPER}"
|
||||
)
|
||||
```
|
||||
|
||||
If the spawn fails or the agent reports an error: log `Codebase drift
|
||||
auto-remap failed: {reason}` and continue to `verify_phase_goal`. The phase
|
||||
is NOT failed by a remap failure.
|
||||
|
||||
If the remap succeeds: log `Codebase drift auto-remap completed for paths:
|
||||
{affected_paths}` and continue to `verify_phase_goal`.
|
||||
|
||||
The two relevant config keys (continue on error / failure if either is invalid):
|
||||
- `workflow.drift_threshold` (integer, default 3) — minimum drift elements before action
|
||||
- `workflow.drift_action` — `warn` (default) or `auto-remap`
|
||||
|
||||
This step is fully non-blocking — it never fails the phase, and any
|
||||
exception path returns control to `verify_phase_goal`.
|
||||
@@ -402,15 +402,19 @@ If SUMMARY "Issues Encountered" ≠ "None": yolo → log and continue. Interacti
|
||||
</step>
|
||||
|
||||
<step name="update_roadmap">
|
||||
**Skip this step if running in parallel mode** (the orchestrator handles ROADMAP.md
|
||||
updates centrally after merging worktrees).
|
||||
Run this step only when NOT executing inside a git worktree (i.e.
|
||||
`use_worktrees: false`, the bug #2661 reproducer). In worktree mode each
|
||||
worktree has its own ROADMAP.md, so per-plan writes here would diverge
|
||||
across siblings; the orchestrator owns the post-merge sync centrally
|
||||
(see execute-phase.md §5.7, single-writer contract from #1486 / dcb50396).
|
||||
|
||||
```bash
|
||||
# Auto-detect parallel mode: .git is a file in worktrees, a directory in main repo
|
||||
# Auto-detect worktree mode: .git is a file in worktrees, a directory in main repo.
|
||||
# This mirrors the use_worktrees config flag for the executing handler.
|
||||
IS_WORKTREE=$([ -f .git ] && echo "true" || echo "false")
|
||||
|
||||
# Skip in parallel mode — orchestrator handles ROADMAP.md centrally
|
||||
if [ "$IS_WORKTREE" != "true" ]; then
|
||||
# use_worktrees: false → this handler is the sole post-plan sync point (#2661)
|
||||
gsd-sdk query roadmap.update-plan-progress "${PHASE}"
|
||||
fi
|
||||
```
|
||||
|
||||
@@ -63,15 +63,35 @@ Extract from result: `phase_number`, `after_phase`, `name`, `slug`, `directory`.
|
||||
</step>
|
||||
|
||||
<step name="update_project_state">
|
||||
Update STATE.md to reflect the inserted phase:
|
||||
Update STATE.md to reflect the inserted phase via SDK handlers (never raw
|
||||
`Edit`/`Write` — projects may ship a `protect-files.sh` PreToolUse hook that
|
||||
blocks direct STATE.md writes):
|
||||
|
||||
1. Read `.planning/STATE.md`
|
||||
2. Under "## Accumulated Context" → "### Roadmap Evolution" add entry:
|
||||
```
|
||||
- Phase {decimal_phase} inserted after Phase {after_phase}: {description} (URGENT)
|
||||
1. Update STATE.md's next-phase pointer(s) to the newly inserted phase
|
||||
`{decimal_phase}`:
|
||||
|
||||
```bash
|
||||
gsd-sdk query state.patch '{"Current Phase":"{decimal_phase}","Next recommended run":"/gsd-plan-phase {decimal_phase}"}'
|
||||
```
|
||||
|
||||
If "Roadmap Evolution" section doesn't exist, create it.
|
||||
(Adjust field names to whatever pointers STATE.md exposes — the handler
|
||||
reports which fields it matched.)
|
||||
|
||||
2. Append a Roadmap Evolution entry via the dedicated handler. It creates the
|
||||
`### Roadmap Evolution` subsection under `## Accumulated Context` if missing
|
||||
and dedupes identical entries:
|
||||
|
||||
```bash
|
||||
gsd-sdk query state.add-roadmap-evolution \
|
||||
--phase {decimal_phase} \
|
||||
--action inserted \
|
||||
--after {after_phase} \
|
||||
--note "{description}" \
|
||||
--urgent
|
||||
```
|
||||
|
||||
Expected response shape: `{ added: true, entry: "- Phase ... (URGENT)" }`
|
||||
(or `{ added: false, reason: "duplicate", entry: ... }` on replay).
|
||||
</step>
|
||||
|
||||
<step name="completion">
|
||||
@@ -125,6 +145,7 @@ Phase insertion is complete when:
|
||||
- [ ] `gsd-sdk query phase.insert` executed successfully
|
||||
- [ ] Phase directory created
|
||||
- [ ] Roadmap updated with new phase entry (includes "(INSERTED)" marker)
|
||||
- [ ] STATE.md updated with roadmap evolution note
|
||||
- [ ] `gsd-sdk query state.add-roadmap-evolution ...` returned `{ added: true }` or `{ added: false, reason: "duplicate" }`
|
||||
- [ ] `gsd-sdk query state.patch` returned matched next-phase pointer field(s)
|
||||
- [ ] User informed of next steps and dependency implications
|
||||
</success_criteria>
|
||||
|
||||
@@ -23,7 +23,7 @@ INIT=$(gsd-sdk query init.manager)
|
||||
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
```
|
||||
|
||||
Parse JSON for: `milestone_version`, `milestone_name`, `phase_count`, `completed_count`, `in_progress_count`, `phases`, `recommended_actions`, `all_complete`, `waiting_signal`, `manager_flags`.
|
||||
Parse JSON for: `milestone_version`, `milestone_name`, `phase_count`, `completed_count`, `in_progress_count`, `phases`, `recommended_actions`, `all_complete`, `waiting_signal`, `manager_flags`, and the optional trio `queued_milestone_version`, `queued_milestone_name`, `queued_phases` (added in SDK fix `2495-2496-2497` — may be absent on older SDK versions, treat missing as empty).
|
||||
|
||||
`manager_flags` contains per-step passthrough flags from config:
|
||||
- `manager_flags.discuss` — appended to `/gsd-discuss-phase` args (e.g. `"--auto --analyze"`)
|
||||
@@ -103,6 +103,28 @@ Example output:
|
||||
| 6 | Polish & Final Mail… | 1-5 | · | · | · | · Up next |
|
||||
```
|
||||
|
||||
**Queued section (next milestone preview):**
|
||||
|
||||
If `queued_phases` is present and non-empty, render a compact preview of the next milestone's phases directly below the main table. This surfaces upcoming work without cluttering the active-milestone grid. Skip this section entirely when `queued_phases` is empty or missing (e.g. the active milestone is the last one in the roadmap).
|
||||
|
||||
Use `queued_milestone_version` and `queued_milestone_name` for the header. Phases render without D/P/E columns since they aren't discussed yet — just number, name (pre-truncated `display_name`), dependencies (`deps_display`), and a fixed `· Queued` status. Phase-name padding should match the active-table column width for visual alignment.
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
───────────────────────────────────────────────────────────────
|
||||
◆ Queued — {queued_milestone_version} {queued_milestone_name} ({queued_phases.length} phases)
|
||||
───────────────────────────────────────────────────────────────
|
||||
| # | Phase | Deps | Status |
|
||||
|---|----------------------|------|--------------|
|
||||
| 31| Email Logs | — | · Queued |
|
||||
| 32| Today's Sheets | 31 | · Queued |
|
||||
| 33| Resend Backfill | 31 | · Queued |
|
||||
| 34| Business Day Audit | 31 | · Queued |
|
||||
```
|
||||
|
||||
Queued phases are NOT eligible for the Continue action menu — they live in a future milestone and must wait for the current milestone to ship. The preview exists purely for situational awareness.
|
||||
|
||||
**Recommendations section:**
|
||||
|
||||
If `all_complete` is true:
|
||||
@@ -362,4 +384,5 @@ Display final status with progress bar:
|
||||
- [ ] Exit shows final status with resume instructions
|
||||
- [ ] "Other" free-text input parsed for phase number and action
|
||||
- [ ] Manager loop continues until user exits or milestone completes
|
||||
- [ ] Queued section renders when `queued_phases` is non-empty; skipped when absent or empty
|
||||
</success_criteria>
|
||||
|
||||
@@ -27,13 +27,51 @@ Documents are reference material for Claude when planning/executing. Always incl
|
||||
|
||||
<process>
|
||||
|
||||
<step name="parse_paths_flag" priority="first">
|
||||
Parse an optional `--paths <p1,p2,...>` argument. When supplied (by the
|
||||
post-execute codebase-drift gate in `/gsd-execute-phase` or by a user running
|
||||
`/gsd-map-codebase --paths apps/accounting,packages/ui`), the workflow
|
||||
operates in **incremental-remap mode**:
|
||||
|
||||
- Pass `--paths <p1>,<p2>,...` through to each spawned `gsd-codebase-mapper`
|
||||
agent's prompt. Agents scope their Glob/Grep/Bash exploration to the listed
|
||||
repo-relative prefixes only — no whole-repo scan.
|
||||
- Reject path values that contain `..`, start with `/`, or include shell
|
||||
metacharacters (`;`, `` ` ``, `$`, `&`, `|`, `<`, `>`). If all provided
|
||||
paths are invalid, fall back to a normal whole-repo run.
|
||||
- On write, each mapper stamps `last_mapped_commit: <HEAD sha>` into the YAML
|
||||
frontmatter of every document it produces (see `bin/lib/drift.cjs:writeMappedCommit`).
|
||||
|
||||
**Explicit contract — propagate `--paths` through a single normalized
|
||||
variable.** Downstream steps (`spawn_agents`, `sequential_mapping`, and any
|
||||
Task-mode prompt construction) MUST use `${PATH_SCOPE_HINT}` to ensure every
|
||||
mapper receives the same deterministic scope. Without this contract
|
||||
incremental-remap can silently regress to a whole-repo scan.
|
||||
|
||||
```bash
|
||||
# Validated, comma-separated paths (empty if --paths absent or all rejected):
|
||||
SCOPED_PATHS="<validated paths or empty>"
|
||||
if [ -n "$SCOPED_PATHS" ]; then
|
||||
PATH_SCOPE_HINT="--paths $SCOPED_PATHS"
|
||||
else
|
||||
PATH_SCOPE_HINT=""
|
||||
fi
|
||||
```
|
||||
|
||||
All mapper prompts built later in this workflow MUST include
|
||||
`${PATH_SCOPE_HINT}` (expanded to empty when full-repo mode is in effect).
|
||||
|
||||
When `--paths` is absent, behave exactly as before: full-repo scan, all 7
|
||||
documents refreshed.
|
||||
</step>
|
||||
|
||||
<step name="init_context" priority="first">
|
||||
Load codebase mapping context:
|
||||
|
||||
```bash
|
||||
INIT=$(gsd-sdk query init.map-codebase)
|
||||
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
AGENT_SKILLS_MAPPER=$(gsd-sdk query agent-skills gsd-codebase-mapper 2>/dev/null)
|
||||
AGENT_SKILLS_MAPPER=$(gsd-sdk query agent-skills gsd-codebase-mapper)
|
||||
```
|
||||
|
||||
Extract from init JSON: `mapper_model`, `commit_docs`, `codebase_dir`, `existing_maps`, `has_maps`, `codebase_dir_exists`, `subagent_timeout`, `date`.
|
||||
@@ -124,6 +162,8 @@ Write these documents to .planning/codebase/:
|
||||
|
||||
IMPORTANT: Use {date} for all [YYYY-MM-DD] date placeholders in documents.
|
||||
|
||||
Scope: ${PATH_SCOPE_HINT:-(full repo)} — when --paths is supplied, restrict exploration to those prefixes only.
|
||||
|
||||
Explore thoroughly. Write documents directly using templates. Return confirmation only.
|
||||
${AGENT_SKILLS_MAPPER}"
|
||||
)
|
||||
@@ -148,6 +188,8 @@ Write these documents to .planning/codebase/:
|
||||
|
||||
IMPORTANT: Use {date} for all [YYYY-MM-DD] date placeholders in documents.
|
||||
|
||||
Scope: ${PATH_SCOPE_HINT:-(full repo)} — when --paths is supplied, restrict exploration to those prefixes only.
|
||||
|
||||
Explore thoroughly. Write documents directly using templates. Return confirmation only.
|
||||
${AGENT_SKILLS_MAPPER}"
|
||||
)
|
||||
@@ -172,6 +214,8 @@ Write these documents to .planning/codebase/:
|
||||
|
||||
IMPORTANT: Use {date} for all [YYYY-MM-DD] date placeholders in documents.
|
||||
|
||||
Scope: ${PATH_SCOPE_HINT:-(full repo)} — when --paths is supplied, restrict exploration to those prefixes only.
|
||||
|
||||
Explore thoroughly. Write documents directly using templates. Return confirmation only.
|
||||
${AGENT_SKILLS_MAPPER}"
|
||||
)
|
||||
@@ -195,6 +239,8 @@ Write this document to .planning/codebase/:
|
||||
|
||||
IMPORTANT: Use {date} for all [YYYY-MM-DD] date placeholders in documents.
|
||||
|
||||
Scope: ${PATH_SCOPE_HINT:-(full repo)} — when --paths is supplied, restrict exploration to those prefixes only.
|
||||
|
||||
Explore thoroughly. Write document directly using template. Return confirmation only.
|
||||
${AGENT_SKILLS_MAPPER}"
|
||||
)
|
||||
@@ -246,6 +292,8 @@ When the `Task` tool is unavailable, perform codebase mapping sequentially in th
|
||||
|
||||
**IMPORTANT:** Use `{date}` from init context for all `[YYYY-MM-DD]` date placeholders in documents. NEVER guess the date.
|
||||
|
||||
**SCOPE:** When `${PATH_SCOPE_HINT}` is non-empty (i.e. `--paths` was supplied), restrict every pass below to the validated path prefixes in `${SCOPED_PATHS}`. Do NOT scan files outside those prefixes. When `${PATH_SCOPE_HINT}` is empty, perform a full-repo scan.
|
||||
|
||||
Perform all 4 mapping passes sequentially:
|
||||
|
||||
**Pass 1: Tech Focus**
|
||||
|
||||
@@ -173,6 +173,19 @@ This document evolves at phase transitions and milestone boundaries.
|
||||
|
||||
## 5. Update STATE.md
|
||||
|
||||
Reset STATE.md frontmatter AND body atomically via the SDK. This writes the new
|
||||
milestone version/name into the YAML frontmatter, resets `status` to
|
||||
`planning`, zeroes `progress.*` counters, and rewrites the `## Current Position`
|
||||
section to the new-milestone template. Accumulated Context (decisions,
|
||||
blockers, todos) is preserved across the switch — symmetric with
|
||||
`milestone.complete`.
|
||||
|
||||
```bash
|
||||
gsd-sdk query state.milestone-switch --milestone "v[X.Y]" --name "[Name]"
|
||||
```
|
||||
|
||||
The resulting Current Position section looks like:
|
||||
|
||||
```markdown
|
||||
## Current Position
|
||||
|
||||
@@ -182,7 +195,11 @@ Status: Defining requirements
|
||||
Last activity: [today] — Milestone v[X.Y] started
|
||||
```
|
||||
|
||||
Keep Accumulated Context section from previous milestone.
|
||||
Bug #2630: a prior version of this workflow rewrote the Current Position body
|
||||
manually but left the frontmatter pointing at the previous milestone, so every
|
||||
downstream reader (`state.json`, `getMilestoneInfo`, progress bars) reported the
|
||||
stale milestone until the first phase advance forced a resync. Always use the
|
||||
SDK handler above — do not hand-edit STATE.md here.
|
||||
|
||||
## 6. Cleanup and Commit
|
||||
|
||||
@@ -203,9 +220,9 @@ gsd-sdk query commit "docs: start milestone v[X.Y] [Name]" .planning/PROJECT.md
|
||||
```bash
|
||||
INIT=$(gsd-sdk query init.new-milestone)
|
||||
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
AGENT_SKILLS_RESEARCHER=$(gsd-sdk query agent-skills gsd-project-researcher 2>/dev/null)
|
||||
AGENT_SKILLS_SYNTHESIZER=$(gsd-sdk query agent-skills gsd-synthesizer 2>/dev/null)
|
||||
AGENT_SKILLS_ROADMAPPER=$(gsd-sdk query agent-skills gsd-roadmapper 2>/dev/null)
|
||||
AGENT_SKILLS_RESEARCHER=$(gsd-sdk query agent-skills gsd-project-researcher)
|
||||
AGENT_SKILLS_SYNTHESIZER=$(gsd-sdk query agent-skills gsd-research-synthesizer)
|
||||
AGENT_SKILLS_ROADMAPPER=$(gsd-sdk query agent-skills gsd-roadmapper)
|
||||
```
|
||||
|
||||
Extract from init JSON: `researcher_model`, `synthesizer_model`, `roadmapper_model`, `commit_docs`, `research_enabled`, `current_milestone`, `project_exists`, `roadmap_exists`, `latest_completed_milestone`, `phase_dir_count`, `phase_archive_path`, `agents_installed`, `missing_agents`.
|
||||
|
||||
@@ -59,9 +59,9 @@ The document should describe what you want to build.
|
||||
```bash
|
||||
INIT=$(gsd-sdk query init.new-project)
|
||||
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
AGENT_SKILLS_RESEARCHER=$(gsd-sdk query agent-skills gsd-project-researcher 2>/dev/null)
|
||||
AGENT_SKILLS_SYNTHESIZER=$(gsd-sdk query agent-skills gsd-synthesizer 2>/dev/null)
|
||||
AGENT_SKILLS_ROADMAPPER=$(gsd-sdk query agent-skills gsd-roadmapper 2>/dev/null)
|
||||
AGENT_SKILLS_RESEARCHER=$(gsd-sdk query agent-skills gsd-project-researcher)
|
||||
AGENT_SKILLS_SYNTHESIZER=$(gsd-sdk query agent-skills gsd-research-synthesizer)
|
||||
AGENT_SKILLS_ROADMAPPER=$(gsd-sdk query agent-skills gsd-roadmapper)
|
||||
```
|
||||
|
||||
Parse JSON for: `researcher_model`, `synthesizer_model`, `roadmapper_model`, `commit_docs`, `project_exists`, `has_codebase_map`, `planning_exists`, `has_existing_code`, `has_package_file`, `is_brownfield`, `needs_codebase_map`, `has_git`, `project_path`, `agents_installed`, `missing_agents`.
|
||||
|
||||
@@ -33,9 +33,9 @@ Load all context in one call (paths only to minimize orchestrator context):
|
||||
```bash
|
||||
INIT=$(gsd-sdk query init.plan-phase "$PHASE")
|
||||
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
AGENT_SKILLS_RESEARCHER=$(gsd-sdk query agent-skills gsd-researcher 2>/dev/null)
|
||||
AGENT_SKILLS_PLANNER=$(gsd-sdk query agent-skills gsd-planner 2>/dev/null)
|
||||
AGENT_SKILLS_CHECKER=$(gsd-sdk query agent-skills gsd-checker 2>/dev/null)
|
||||
AGENT_SKILLS_RESEARCHER=$(gsd-sdk query agent-skills gsd-phase-researcher)
|
||||
AGENT_SKILLS_PLANNER=$(gsd-sdk query agent-skills gsd-planner)
|
||||
AGENT_SKILLS_CHECKER=$(gsd-sdk query agent-skills gsd-plan-checker)
|
||||
CONTEXT_WINDOW=$(gsd-sdk query config-get context_window 2>/dev/null || echo "200000")
|
||||
TDD_MODE=$(gsd-sdk query config-get workflow.tdd_mode 2>/dev/null || echo "false")
|
||||
```
|
||||
@@ -54,7 +54,7 @@ Parse JSON for: `researcher_model`, `planner_model`, `checker_model`, `research_
|
||||
|
||||
## 2. Parse and Normalize Arguments
|
||||
|
||||
Extract from $ARGUMENTS: phase number (integer or decimal like `2.1`), flags (`--research`, `--skip-research`, `--gaps`, `--skip-verify`, `--skip-ui`, `--prd <filepath>`, `--reviews`, `--text`, `--bounce`, `--skip-bounce`).
|
||||
Extract from $ARGUMENTS: phase number (integer or decimal like `2.1`), flags (`--research`, `--skip-research`, `--gaps`, `--skip-verify`, `--skip-ui`, `--prd <filepath>`, `--reviews`, `--text`, `--bounce`, `--skip-bounce`, `--chunked`).
|
||||
|
||||
Set `TEXT_MODE=true` if `--text` is present in $ARGUMENTS OR `text_mode` from init JSON is `true`. When `TEXT_MODE` is active, replace every `AskUserQuestion` call with a plain-text numbered list and ask the user to type their choice number. This is required for Claude Code remote sessions (`/rc` mode) where TUI menus don't work through the Claude App.
|
||||
|
||||
@@ -69,6 +69,15 @@ mkdir -p ".planning/phases/${padded_phase}-${phase_slug}"
|
||||
|
||||
**Existing artifacts from init:** `has_research`, `has_plans`, `plan_count`.
|
||||
|
||||
Set `CHUNKED_MODE` from flag or config:
|
||||
```bash
|
||||
CHUNKED_CFG=$(gsd-sdk query config-get workflow.plan_chunked 2>/dev/null || echo "false")
|
||||
CHUNKED_MODE=false
|
||||
if [[ "$ARGUMENTS" =~ --chunked ]] || [[ "$CHUNKED_CFG" == "true" ]]; then
|
||||
CHUNKED_MODE=true
|
||||
fi
|
||||
```
|
||||
|
||||
## 2.5. Validate `--reviews` Prerequisite
|
||||
|
||||
**Skip if:** No `--reviews` flag.
|
||||
@@ -795,6 +804,8 @@ Every task MUST include these fields — they are NOT optional:
|
||||
</quality_gate>
|
||||
```
|
||||
|
||||
**If `CHUNKED_MODE` is `false` (default):** Spawn the planner as a single long-lived Task:
|
||||
|
||||
```
|
||||
Task(
|
||||
prompt=filled_prompt,
|
||||
@@ -804,6 +815,112 @@ Task(
|
||||
)
|
||||
```
|
||||
|
||||
**If `CHUNKED_MODE` is `true`:** Skip the Task() call above — proceed to step 8.5 instead.
|
||||
|
||||
## 8.5. Chunked Planning Mode
|
||||
|
||||
**Skip if `CHUNKED_MODE` is `false`.**
|
||||
|
||||
Chunked mode splits the single long-lived planner Task into a short outline Task followed by
|
||||
N short per-plan Tasks. Each Task is bounded to ~3–5 min; each plan is committed individually
|
||||
for crash resilience. If any Task hangs and the terminal is force-killed, rerunning
|
||||
`/gsd-plan-phase {N} --chunked` resumes from the last successfully committed plan.
|
||||
|
||||
**Intended for new or in-progress chunked runs.** To recover plans already written by a prior
|
||||
*non-chunked* run, use step 6's "Add more plans" or proceed directly to `/gsd-execute-phase`
|
||||
— don't start a fresh chunked run over existing non-chunked plans.
|
||||
|
||||
### 8.5.1 Outline Phase (outline-only mode, ~2 min)
|
||||
|
||||
**Resume detection:** If `${PHASE_DIR}/${PADDED_PHASE}-PLAN-OUTLINE.md` already exists **and
|
||||
is valid** (contains the `## OUTLINE COMPLETE` marker), skip this sub-step — the outline
|
||||
already exists from a previous run. Proceed directly to 8.5.2.
|
||||
|
||||
```bash
|
||||
OUTLINE_FILE="${PHASE_DIR}/${PADDED_PHASE}-PLAN-OUTLINE.md"
|
||||
if [[ -f "$OUTLINE_FILE" ]] && grep -q "^## OUTLINE COMPLETE" "$OUTLINE_FILE"; then
|
||||
# reuse existing outline — skip to 8.5.2
|
||||
fi
|
||||
```
|
||||
|
||||
Display:
|
||||
```text
|
||||
◆ Chunked mode: spawning outline planner...
|
||||
```
|
||||
|
||||
Spawn the planner in **outline-only** mode — it must write only the outline manifest, not any
|
||||
PLAN.md files:
|
||||
|
||||
```javascript
|
||||
Task(
|
||||
prompt="{same planning_context as step 8, plus:}
|
||||
|
||||
**Chunked mode: outline-only.**
|
||||
Do NOT write any PLAN.md files in this Task.
|
||||
Write only: {PHASE_DIR}/{PADDED_PHASE}-PLAN-OUTLINE.md
|
||||
|
||||
The outline must be a markdown table with columns:
|
||||
Plan ID | Objective | Wave | Depends On | Requirements
|
||||
|
||||
Return: ## OUTLINE COMPLETE with plan count.",
|
||||
subagent_type="gsd-planner",
|
||||
model="{planner_model}",
|
||||
description="Outline Phase {phase} (chunked)"
|
||||
)
|
||||
```
|
||||
|
||||
Handle return:
|
||||
- **`## OUTLINE COMPLETE`:** Read `PLAN-OUTLINE.md`, extract plan list. Continue to 8.5.2.
|
||||
- **Any other return or empty:** Display error. Offer: 1) Retry outline, 2) Stop.
|
||||
|
||||
### 8.5.2 Per-Plan Tasks (single-plan mode, ~3-5 min each)
|
||||
|
||||
For each plan entry extracted from `PLAN-OUTLINE.md`:
|
||||
|
||||
1. **Resume check:** If `${PHASE_DIR}/{plan_id}-PLAN.md` already exists on disk **and has
|
||||
valid YAML frontmatter** (opening `---` delimiter present), skip this plan (do not
|
||||
overwrite completed work — resume safety).
|
||||
|
||||
```bash
|
||||
PLAN_FILE="${PHASE_DIR}/${plan_id}-PLAN.md"
|
||||
if [[ -f "$PLAN_FILE" ]] && head -1 "$PLAN_FILE" | grep -q '^---'; then
|
||||
continue # plan already written, skip
|
||||
fi
|
||||
```
|
||||
|
||||
2. Display:
|
||||
```text
|
||||
◆ Chunked mode: planning {plan_id} ({k}/{N})...
|
||||
```
|
||||
|
||||
3. Spawn the planner in **single-plan** mode — it must write exactly one PLAN.md file:
|
||||
```javascript
|
||||
Task(
|
||||
prompt="{same planning_context as step 8, plus:}
|
||||
|
||||
**Chunked mode: single-plan.**
|
||||
Write exactly ONE plan file: {PHASE_DIR}/{plan_id}-PLAN.md
|
||||
Plan to write: {plan_id} — {objective}
|
||||
Wave: {wave} | Depends on: {depends_on}
|
||||
Phase requirement IDs to cover in this plan: {plan_requirements}
|
||||
|
||||
Return: ## PLAN COMPLETE with the plan ID.",
|
||||
subagent_type="gsd-planner",
|
||||
model="{planner_model}",
|
||||
description="Plan {plan_id} (chunked {k}/{N})"
|
||||
)
|
||||
```
|
||||
|
||||
4. **Verify disk:** Check `${PHASE_DIR}/{plan_id}-PLAN.md` exists. If missing: offer 1) Retry, 2) Stop.
|
||||
|
||||
5. **Commit per-plan:**
|
||||
```bash
|
||||
gsd-sdk query commit "docs(${PADDED_PHASE}): plan ${plan_id} (chunked)" "${PHASE_DIR}/${plan_id}-PLAN.md"
|
||||
```
|
||||
|
||||
After all N plans are written and committed, treat this as `## PLANNING COMPLETE` and continue
|
||||
to step 9.
|
||||
|
||||
## 9. Handle Planner Return
|
||||
|
||||
- **`## PLANNING COMPLETE`:** Display plan count. If `--skip-verify` or `plan_checker_enabled` is false (from init): skip to step 13. Otherwise: step 10.
|
||||
@@ -811,6 +928,35 @@ Task(
|
||||
- **`## ⚠ Source Audit: Unplanned Items Found`:** The planner's multi-source coverage audit found items from REQUIREMENTS.md, RESEARCH.md, ROADMAP goal, or CONTEXT.md decisions that are not covered by any plan. Handle in step 9c.
|
||||
- **`## CHECKPOINT REACHED`:** Present to user, get response, spawn continuation (step 12)
|
||||
- **`## PLANNING INCONCLUSIVE`:** Show attempts, offer: Add context / Retry / Manual
|
||||
- **Empty / truncated / no recognized marker:** → Filesystem fallback (step 9a).
|
||||
|
||||
## 9a. Filesystem Fallback (Planner)
|
||||
|
||||
**Triggered when:** Task() returns but the return contains no recognized marker (`## PLANNING COMPLETE`, `## PHASE SPLIT RECOMMENDED`, `## ⚠ Source Audit`, `## CHECKPOINT REACHED`, `## PLANNING INCONCLUSIVE`).
|
||||
|
||||
```bash
|
||||
DISK_PLANS=$(ls "${PHASE_DIR}"/*-PLAN.md 2>/dev/null | wc -l | tr -d ' ')
|
||||
```
|
||||
|
||||
**If `DISK_PLANS` > 0:** The planner wrote plans to disk but the Task() return was empty or
|
||||
truncated (the Windows stdio hang pattern — the subagent finished but the return never
|
||||
arrived). Display:
|
||||
|
||||
```text
|
||||
◆ Planner wrote {DISK_PLANS} plan(s) to disk but did not emit a PLANNING COMPLETE marker.
|
||||
This is a known Windows stdio hang pattern — work is likely recoverable.
|
||||
|
||||
Plans found on disk:
|
||||
{ls output of *-PLAN.md}
|
||||
```
|
||||
|
||||
Offer 3 options:
|
||||
1. **Accept plans** — treat as `## PLANNING COMPLETE` and continue through step 9 `## PLANNING COMPLETE` handling (so `--skip-verify` / `plan_checker_enabled=false` are honored — may skip to step 13 rather than step 10)
|
||||
2. **Retry planner** — re-spawn the planner with the same prompt (return to step 8)
|
||||
3. **Stop** — exit; user can re-run `/gsd-plan-phase {N}` to resume
|
||||
|
||||
**If `DISK_PLANS` is 0 and no marker:** The planner produced no output. Treat as
|
||||
`## PLANNING INCONCLUSIVE` and handle accordingly.
|
||||
|
||||
## 9b. Handle Phase Split Recommendation
|
||||
|
||||
@@ -925,6 +1071,7 @@ Task(
|
||||
|
||||
- **`## VERIFICATION PASSED`:** Display confirmation, proceed to step 13.
|
||||
- **`## ISSUES FOUND`:** Display issues, check iteration count, proceed to step 12.
|
||||
- **Empty / truncated / no recognized marker:** → Filesystem fallback (step 11a).
|
||||
|
||||
**Thinking partner for architectural tradeoffs (conditional):**
|
||||
If `features.thinking_partner` is enabled, scan the checker's issues for architectural tradeoff keywords
|
||||
@@ -945,6 +1092,29 @@ Apply this to the revision? [Yes] / [No, I'll decide]
|
||||
If yes: include the recommendation in the revision prompt. If no: proceed to revision loop as normal.
|
||||
If thinking_partner disabled: skip this block entirely.
|
||||
|
||||
## 11a. Filesystem Fallback (Checker)
|
||||
|
||||
**Triggered when:** Checker Task() returns but the return contains neither `## VERIFICATION PASSED` nor `## ISSUES FOUND`.
|
||||
|
||||
```bash
|
||||
DISK_PLANS=$(ls "${PHASE_DIR}"/*-PLAN.md 2>/dev/null | wc -l | tr -d ' ')
|
||||
```
|
||||
|
||||
**If `DISK_PLANS` > 0:** Plans exist on disk; the checker return was empty or truncated (the
|
||||
Windows stdio hang pattern — the subagent finished but the return never arrived). Display:
|
||||
|
||||
```text
|
||||
◆ Checker return was empty or truncated. {DISK_PLANS} plan(s) exist on disk.
|
||||
This is a known Windows stdio hang pattern — checker may have completed without returning.
|
||||
```
|
||||
|
||||
Offer 3 options:
|
||||
1. **Accept verification** — treat as `## VERIFICATION PASSED` and continue to step 13
|
||||
2. **Retry checker** — re-spawn the checker with the same prompt (return to step 10)
|
||||
3. **Stop** — exit; user can re-run `/gsd-plan-phase {N}` to resume
|
||||
|
||||
**If `DISK_PLANS` is 0:** No plans on disk — something is seriously wrong. Display error and stop.
|
||||
|
||||
## 12. Revision Loop (Max 3 Iterations)
|
||||
|
||||
Track `iteration_count` (starts at 1 after initial plan + check).
|
||||
@@ -1140,6 +1310,72 @@ Options:
|
||||
|
||||
If `TEXT_MODE` is true, present as a plain-text numbered list (options already shown in the block above). Otherwise use AskUserQuestion to present the options.
|
||||
|
||||
## 13a. Decision Coverage Gate
|
||||
|
||||
After the requirements coverage gate passes, verify that every trackable
|
||||
decision captured by discuss-phase in CONTEXT.md `<decisions>` is referenced
|
||||
by at least one plan. This is the **translation gate** from issue #2492 —
|
||||
its job is to refuse to mark a phase planned when a discuss-phase decision
|
||||
silently dropped on the way into the plans.
|
||||
|
||||
**Skip if** `workflow.context_coverage_gate` is explicitly set to `false`
|
||||
(absent key = enabled). Also skip if no CONTEXT.md exists for this phase
|
||||
(nothing to translate) or if its `<decisions>` block is empty.
|
||||
|
||||
```bash
|
||||
GATE_CFG=$(gsd-sdk query config-get workflow.context_coverage_gate 2>/dev/null || echo "true")
|
||||
if [ "$GATE_CFG" != "false" ]; then
|
||||
GATE_RESULT=$(gsd-sdk query check.decision-coverage-plan "${PHASE_DIR}" "${CONTEXT_PATH}")
|
||||
# BLOCKING: refuse to mark phase planned when a trackable decision is uncovered.
|
||||
# `passed: true` covers both real-pass and skipped cases (gate disabled / no CONTEXT.md /
|
||||
# no trackable decisions). Verify-phase counterpart deliberately omits this exit-1 — that
|
||||
# gate is non-blocking by design (review finding F15).
|
||||
echo "$GATE_RESULT" | jq -e '.data.passed == true' >/dev/null || {
|
||||
echo "$GATE_RESULT" | jq -r '.data.message'
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
```
|
||||
|
||||
The handler returns JSON:
|
||||
```json
|
||||
{
|
||||
"passed": true,
|
||||
"skipped": false,
|
||||
"total": 2,
|
||||
"covered": 2,
|
||||
"uncovered": [ { "id": "D-01", "text": "...", "category": "..." } ],
|
||||
"message": "..."
|
||||
}
|
||||
```
|
||||
|
||||
**If `passed` is true (or `skipped` is true):** Display
|
||||
`✓ Decision coverage: {M}/{N} CONTEXT.md decisions covered by plans` (or
|
||||
`(skipped — gate disabled)` / `(skipped — no decisions)`) and proceed to
|
||||
step 13b.
|
||||
|
||||
**If `passed` is false:** Display the handler's `message` block. It already
|
||||
names each uncovered decision (`D-NN | category | text`) and tells the user
|
||||
what to do — cite the id in a relevant plan's `must_haves` / `truths`, or
|
||||
move the decision under `### Claude's Discretion` / tag it `[informational]`
|
||||
if it should not be tracked. Then offer:
|
||||
|
||||
```text
|
||||
Options:
|
||||
1. Re-plan to cover missing decisions (recommended)
|
||||
2. Edit CONTEXT.md to mark dropped decisions as [informational] / Discretion
|
||||
3. Proceed anyway — accept the coverage gap
|
||||
```
|
||||
|
||||
If `TEXT_MODE` is true, present as a plain-text numbered list. Otherwise use
|
||||
AskUserQuestion. Selecting "Proceed anyway" continues to step 13b but
|
||||
records the override in STATE.md so verify-phase can re-surface it.
|
||||
|
||||
**Why this gate blocks:** failing here is cheap. The plans are the contract
|
||||
between discuss-phase and execute-phase; if a decision isn't visible in any
|
||||
plan, no executor will implement it. Catching that now beats discovering it
|
||||
after thousands of dollars of execution.
|
||||
|
||||
## 13b. Record Planning Completion in STATE.md
|
||||
|
||||
After plans pass all gates, record that planning is complete so STATE.md reflects the new phase status:
|
||||
@@ -1174,6 +1410,53 @@ gsd-sdk query commit "docs(${PADDED_PHASE}): create phase plan" --files "${PHASE
|
||||
|
||||
This commits all PLAN.md files for the phase plus the updated STATE.md and ROADMAP.md to version-control the planning artifacts. Skip this step if `commit_docs` is false.
|
||||
|
||||
## 13e. Post-Planning Gap Analysis
|
||||
|
||||
After all plans are generated, committed, and the Requirements Coverage Gate (§13)
|
||||
has run, emit a single unified gap report covering both REQUIREMENTS.md and the
|
||||
CONTEXT.md `<decisions>` section. This is a **proactive, post-hoc report** — it
|
||||
does not block phase advancement and does not re-plan. It exists so that any
|
||||
requirement or decision that slipped through the per-plan checks is surfaced in
|
||||
one place before execution begins.
|
||||
|
||||
**Skip if:** `workflow.post_planning_gaps` is `false`. Default is `true`.
|
||||
|
||||
```bash
|
||||
POST_PLANNING_GAPS=$(gsd-sdk query config-get workflow.post_planning_gaps --default true 2>/dev/null || echo true)
|
||||
if [ "$POST_PLANNING_GAPS" = "true" ]; then
|
||||
gsd-tools gap-analysis --phase-dir "${PHASE_DIR}"
|
||||
fi
|
||||
```
|
||||
|
||||
(`gsd-tools gap-analysis` reads `.planning/REQUIREMENTS.md`, `${PHASE_DIR}/CONTEXT.md`,
|
||||
and `${PHASE_DIR}/*-PLAN.md`, then prints a markdown table with one row per
|
||||
REQ-ID and D-ID. Word-boundary matching prevents `REQ-1` from being mistaken for
|
||||
`REQ-10`.)
|
||||
|
||||
**Output format (deterministic; sorted REQUIREMENTS.md → CONTEXT.md, then natural
|
||||
sort within source):**
|
||||
|
||||
```
|
||||
## Post-Planning Gap Analysis
|
||||
|
||||
| Source | Item | Status |
|
||||
|--------|------|--------|
|
||||
| REQUIREMENTS.md | REQ-01 | ✓ Covered |
|
||||
| REQUIREMENTS.md | REQ-02 | ✗ Not covered |
|
||||
| CONTEXT.md | D-01 | ✓ Covered |
|
||||
| CONTEXT.md | D-02 | ✗ Not covered |
|
||||
|
||||
⚠ N items not covered by any plan
|
||||
```
|
||||
|
||||
**Skip-gracefully behavior:**
|
||||
- REQUIREMENTS.md missing → CONTEXT-only report.
|
||||
- CONTEXT.md missing → REQUIREMENTS-only report.
|
||||
- Both missing or `<decisions>` block missing → "No requirements or decisions to check" line, no error.
|
||||
|
||||
This step is non-blocking. If items are reported as not covered, the user may
|
||||
re-run `/gsd-plan-phase --gaps` to add plans, or proceed to execute-phase as-is.
|
||||
|
||||
## 14. Present Final Status
|
||||
|
||||
Route to `<offer_next>` OR `auto_advance` depending on flags/config.
|
||||
@@ -1187,7 +1470,7 @@ Check for auto-advance trigger using values already loaded in step 1:
|
||||
3. **Sync chain flag with intent** — if user invoked manually (no `--auto` and no `--chain`), clear the ephemeral chain flag from any previous interrupted `--auto` chain. This does NOT touch `workflow.auto_advance` (the user's persistent settings preference):
|
||||
```bash
|
||||
if [[ ! "$ARGUMENTS" =~ --auto ]] && [[ ! "$ARGUMENTS" =~ --chain ]]; then
|
||||
gsd-sdk query config-set workflow._auto_chain_active false 2>/dev/null
|
||||
gsd-sdk query config-set workflow._auto_chain_active false || true
|
||||
fi
|
||||
```
|
||||
|
||||
|
||||
@@ -271,7 +271,7 @@ Write updated analysis JSON back to `$ANALYSIS_PATH`.
|
||||
Display: "◆ Writing profile..."
|
||||
|
||||
```bash
|
||||
gsd-sdk query write-profile --input "$ANALYSIS_PATH" --json 2>/dev/null
|
||||
gsd-sdk query write-profile --input "$ANALYSIS_PATH" --json
|
||||
```
|
||||
|
||||
Display: "✓ Profile written to $HOME/.claude/get-shit-done/USER-PROFILE.md"
|
||||
@@ -350,7 +350,7 @@ Generate selected artifacts sequentially (file I/O is fast, no benefit from para
|
||||
**For /gsd-dev-preferences (if selected):**
|
||||
|
||||
```bash
|
||||
gsd-sdk query generate-dev-preferences --analysis "$ANALYSIS_PATH" --json 2>/dev/null
|
||||
gsd-sdk query generate-dev-preferences --analysis "$ANALYSIS_PATH" --json
|
||||
```
|
||||
|
||||
Display: "✓ Generated /gsd-dev-preferences at $HOME/.claude/commands/gsd/dev-preferences.md"
|
||||
@@ -358,7 +358,7 @@ Display: "✓ Generated /gsd-dev-preferences at $HOME/.claude/commands/gsd/dev-p
|
||||
**For CLAUDE.md profile section (if selected):**
|
||||
|
||||
```bash
|
||||
gsd-sdk query generate-claude-profile --analysis "$ANALYSIS_PATH" --json 2>/dev/null
|
||||
gsd-sdk query generate-claude-profile --analysis "$ANALYSIS_PATH" --json
|
||||
```
|
||||
|
||||
Display: "✓ Added profile section to CLAUDE.md"
|
||||
@@ -366,7 +366,7 @@ Display: "✓ Added profile section to CLAUDE.md"
|
||||
**For Global CLAUDE.md (if selected):**
|
||||
|
||||
```bash
|
||||
gsd-sdk query generate-claude-profile --analysis "$ANALYSIS_PATH" --global --json 2>/dev/null
|
||||
gsd-sdk query generate-claude-profile --analysis "$ANALYSIS_PATH" --global --json
|
||||
```
|
||||
|
||||
Display: "✓ Added profile section to $HOME/.claude/CLAUDE.md"
|
||||
|
||||
@@ -140,10 +140,10 @@ fi
|
||||
```bash
|
||||
INIT=$(gsd-sdk query init.quick "$DESCRIPTION")
|
||||
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
AGENT_SKILLS_PLANNER=$(gsd-sdk query agent-skills gsd-planner 2>/dev/null)
|
||||
AGENT_SKILLS_EXECUTOR=$(gsd-sdk query agent-skills gsd-executor 2>/dev/null)
|
||||
AGENT_SKILLS_CHECKER=$(gsd-sdk query agent-skills gsd-checker 2>/dev/null)
|
||||
AGENT_SKILLS_VERIFIER=$(gsd-sdk query agent-skills gsd-verifier 2>/dev/null)
|
||||
AGENT_SKILLS_PLANNER=$(gsd-sdk query agent-skills gsd-planner)
|
||||
AGENT_SKILLS_EXECUTOR=$(gsd-sdk query agent-skills gsd-executor)
|
||||
AGENT_SKILLS_CHECKER=$(gsd-sdk query agent-skills gsd-plan-checker)
|
||||
AGENT_SKILLS_VERIFIER=$(gsd-sdk query agent-skills gsd-verifier)
|
||||
```
|
||||
|
||||
Parse JSON for: `planner_model`, `executor_model`, `checker_model`, `verifier_model`, `commit_docs`, `branch_name`, `quick_id`, `slug`, `date`, `timestamp`, `quick_dir`, `task_dir`, `roadmap_exists`, `planning_exists`.
|
||||
@@ -892,6 +892,7 @@ Build file list:
|
||||
- If `$DISCUSS_MODE` and context file exists: `${QUICK_DIR}/${quick_id}-CONTEXT.md`
|
||||
- If `$RESEARCH_MODE` and research file exists: `${QUICK_DIR}/${quick_id}-RESEARCH.md`
|
||||
- If `$VALIDATE_MODE` and verification file exists: `${QUICK_DIR}/${quick_id}-VERIFICATION.md`
|
||||
- If `${QUICK_DIR}/${quick_id}-deferred-items.md` exists: `${QUICK_DIR}/${quick_id}-deferred-items.md`
|
||||
|
||||
```bash
|
||||
# Explicitly stage all artifacts before commit — PLAN.md may be untracked
|
||||
|
||||
@@ -42,7 +42,7 @@ If exists: Offer update/view/skip options.
|
||||
INIT=$(gsd-sdk query init.phase-op "${PHASE}")
|
||||
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
# Extract: phase_dir, padded_phase, phase_number, state_path, requirements_path, context_path
|
||||
AGENT_SKILLS_RESEARCHER=$(gsd-sdk query agent-skills gsd-researcher 2>/dev/null)
|
||||
AGENT_SKILLS_RESEARCHER=$(gsd-sdk query agent-skills gsd-phase-researcher)
|
||||
```
|
||||
|
||||
## Step 4: Spawn Researcher
|
||||
|
||||
@@ -18,7 +18,7 @@ Valid GSD subagent types (use exact names — do not fall back to 'general-purpo
|
||||
```bash
|
||||
INIT=$(gsd-sdk query init.phase-op "${PHASE_ARG}")
|
||||
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
AGENT_SKILLS_AUDITOR=$(gsd-sdk query agent-skills gsd-security-auditor 2>/dev/null)
|
||||
AGENT_SKILLS_AUDITOR=$(gsd-sdk query agent-skills gsd-security-auditor)
|
||||
```
|
||||
|
||||
Parse: `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`.
|
||||
|
||||
435
get-shit-done/workflows/settings-advanced.md
Normal file
435
get-shit-done/workflows/settings-advanced.md
Normal file
@@ -0,0 +1,435 @@
|
||||
<purpose>
|
||||
Interactive configuration of GSD power-user knobs — plan bounce, node repair, subagent timeouts,
|
||||
inline plan threshold, cross-AI execution, base branch, branch templates, response language,
|
||||
context window, gitignored search, and graphify build timeout.
|
||||
|
||||
This is a companion to `/gsd-settings` — the common-case prompt there covers model profile,
|
||||
research/plan_check/verifier toggles, branching strategy, UI/AI phase gates, and worktree
|
||||
isolation. This advanced command covers everything else that is user-settable, grouped into
|
||||
six sections so each prompt batch stays cognitively scoped. Every answer pre-selects the
|
||||
current value; numeric-input answers that are non-numeric are rejected and re-prompted.
|
||||
</purpose>
|
||||
|
||||
<required_reading>
|
||||
Read all files referenced by the invoking prompt's execution_context before starting.
|
||||
</required_reading>
|
||||
|
||||
<process>
|
||||
|
||||
<step name="ensure_and_load_config">
|
||||
Ensure config exists and resolve the workstream-aware config path (mirrors `settings.md`):
|
||||
|
||||
```bash
|
||||
gsd-sdk query config-ensure-section
|
||||
if [[ -z "${GSD_CONFIG_PATH:-}" ]]; then
|
||||
if [[ -f .planning/active-workstream ]]; then
|
||||
WS=$(tr -d '\n\r' < .planning/active-workstream)
|
||||
GSD_CONFIG_PATH=".planning/workstreams/${WS}/config.json"
|
||||
else
|
||||
GSD_CONFIG_PATH=".planning/config.json"
|
||||
fi
|
||||
fi
|
||||
```
|
||||
|
||||
All subsequent reads and writes go through `$GSD_CONFIG_PATH`. Never hardcode
|
||||
`.planning/config.json` — workstream installs must route to their own config file.
|
||||
</step>
|
||||
|
||||
<step name="read_current">
|
||||
```bash
|
||||
cat "$GSD_CONFIG_PATH"
|
||||
```
|
||||
|
||||
Parse the following current values. If a key is absent, fall back to the documented default
|
||||
shown in parentheses:
|
||||
|
||||
Planning Tuning:
|
||||
- `workflow.plan_bounce` (default: `false`)
|
||||
- `workflow.plan_bounce_passes` (default: `2`)
|
||||
- `workflow.plan_bounce_script` (default: `null`)
|
||||
- `workflow.subagent_timeout` (default: `600`)
|
||||
- `workflow.inline_plan_threshold` (default: `3`)
|
||||
|
||||
Execution Tuning:
|
||||
- `workflow.node_repair` (default: `true`)
|
||||
- `workflow.node_repair_budget` (default: `2`)
|
||||
- `workflow.auto_prune_state` (default: `false`)
|
||||
|
||||
Discussion Tuning:
|
||||
- `workflow.max_discuss_passes` (default: `3`)
|
||||
|
||||
Cross-AI Execution:
|
||||
- `workflow.cross_ai_execution` (default: `false`)
|
||||
- `workflow.cross_ai_command` (default: `null`)
|
||||
- `workflow.cross_ai_timeout` (default: `300`)
|
||||
|
||||
Git Customization:
|
||||
- `git.base_branch` (default: `main`)
|
||||
- `git.phase_branch_template` (default: `gsd/phase-{phase}-{slug}`)
|
||||
- `git.milestone_branch_template` (default: `gsd/{milestone}-{slug}`)
|
||||
|
||||
Runtime / Output:
|
||||
- `response_language` (default: `null`)
|
||||
- `context_window` (default: `200000`)
|
||||
- `search_gitignored` (default: `false`)
|
||||
- `graphify.build_timeout` (default: `300`)
|
||||
|
||||
Each field's **current value is pre-selected** in the prompt rendering below. When the
|
||||
current value is absent from the config, render the documented default as the pre-selected
|
||||
option so the user sees what the effective value is.
|
||||
</step>
|
||||
|
||||
<step name="present_settings">
|
||||
|
||||
**Text mode (`workflow.text_mode: true` or `--text` flag):** Set `TEXT_MODE=true` if `--text` is
|
||||
in `$ARGUMENTS` OR `text_mode` is true in config. When `TEXT_MODE=true`, replace every
|
||||
`AskUserQuestion` call below with a plain-text numbered list and ask the user to type the
|
||||
choice number or free-text value.
|
||||
|
||||
**Numeric-input validation.** For any numeric field (`*_passes`, `*_budget`, `*_timeout`,
|
||||
`*_threshold`, `context_window`, `graphify.build_timeout`), if the user types a value that
|
||||
is not a non-negative integer, the workflow MUST reject it, state which value was invalid,
|
||||
and re-prompt that single field. The minimum accepted value is field-specific and is stated
|
||||
in each field's prompt below — `workflow.plan_bounce_passes` and `workflow.max_discuss_passes`
|
||||
require `>= 1`; all other numeric fields accept `>= 0`. An empty input means "keep current"
|
||||
— the existing value is retained. Non-numeric input is never silently coerced.
|
||||
|
||||
**Free-text validation.** For branch template fields (`git.phase_branch_template`,
|
||||
`git.milestone_branch_template`), if the user supplies a non-default value, it MUST be
|
||||
non-empty and SHOULD contain at least one `{placeholder}`. A template missing placeholders
|
||||
is rejected with a message explaining the available variables (`{phase}`, `{slug}`,
|
||||
`{milestone}`) and re-prompted. An empty input means "keep current."
|
||||
|
||||
**Null-allowed fields.** For `response_language`, `workflow.plan_bounce_script`,
|
||||
`workflow.cross_ai_command`: an empty input clears the field (`null`). A non-empty input is
|
||||
stored verbatim as a string.
|
||||
|
||||
---
|
||||
|
||||
### Section 1 — Planning Tuning
|
||||
|
||||
```text
|
||||
AskUserQuestion([
|
||||
{
|
||||
question: "Run external plan-bounce validator against generated PLAN.md? (current: <value or false>)",
|
||||
header: "Plan Bounce",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "No (default: false)", description: "Skip external plan validation." },
|
||||
{ label: "Yes", description: "Pipe each PLAN.md through `plan_bounce_script` and block on non-zero exit." }
|
||||
]
|
||||
},
|
||||
{
|
||||
question: "How many plan-bounce passes? (current: <value or 2>)",
|
||||
header: "Bounce Passes",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "Keep current", description: "Leave the existing value unchanged." },
|
||||
{ label: "Enter number", description: "Type an integer >= 1. Non-numeric input is rejected and re-prompted. Default: 2" }
|
||||
]
|
||||
},
|
||||
{
|
||||
question: "Path to plan-bounce validation script? (current: <value or null>)",
|
||||
header: "Bounce Script",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "Keep current", description: "Leave existing path unchanged." },
|
||||
{ label: "Clear (null)", description: "Unset the script path." },
|
||||
{ label: "Enter path", description: "Type an absolute or repo-relative path. Receives PLAN.md path as first argument." }
|
||||
]
|
||||
},
|
||||
{
|
||||
question: "Subagent timeout (seconds)? (current: <value or 600>)",
|
||||
header: "Subagent Timeout",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "Keep current", description: "Leave timeout unchanged." },
|
||||
{ label: "Enter seconds", description: "Integer number of seconds. Non-numeric rejected. Default: 600" }
|
||||
]
|
||||
},
|
||||
{
|
||||
question: "Inline plan threshold — tasks allowed inline before splitting to PLAN.md? (current: <value or 3>)",
|
||||
header: "Inline Plan Threshold",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "Keep current", description: "Leave threshold unchanged." },
|
||||
{ label: "Enter number", description: "Integer count. Non-numeric rejected. Default: 3" }
|
||||
]
|
||||
}
|
||||
])
|
||||
```
|
||||
|
||||
### Section 2 — Execution Tuning
|
||||
|
||||
```text
|
||||
AskUserQuestion([
|
||||
{
|
||||
question: "Enable autonomous node repair on verification failure? (current: <value or true>)",
|
||||
header: "Node Repair",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "Yes (default: true)", description: "Executor retries failed tasks up to the repair budget." },
|
||||
{ label: "No", description: "Stop on first verification failure." }
|
||||
]
|
||||
},
|
||||
{
|
||||
question: "Maximum node-repair attempts per failed task? (current: <value or 2>)",
|
||||
header: "Repair Budget",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "Keep current", description: "Leave existing budget unchanged." },
|
||||
{ label: "Enter number", description: "Integer >= 0. Non-numeric rejected. Default: 2" }
|
||||
]
|
||||
},
|
||||
{
|
||||
question: "Auto-prune stale STATE.md entries at phase boundaries? (current: <value or false>)",
|
||||
header: "Auto Prune",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "No (default: false)", description: "Prompt before pruning." },
|
||||
{ label: "Yes", description: "Prune stale entries without prompting." }
|
||||
]
|
||||
}
|
||||
])
|
||||
```
|
||||
|
||||
### Section 3 — Discussion Tuning
|
||||
|
||||
```text
|
||||
AskUserQuestion([
|
||||
{
|
||||
question: "Maximum discuss-phase question rounds? (current: <value or 3>)",
|
||||
header: "Max Discuss Passes",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "Keep current", description: "Leave existing value unchanged." },
|
||||
{ label: "Enter number", description: "Integer >= 1. Non-numeric rejected. Default: 3. Prevents infinite discussion loops in headless mode." }
|
||||
]
|
||||
}
|
||||
])
|
||||
```
|
||||
|
||||
### Section 4 — Cross-AI Execution
|
||||
|
||||
```text
|
||||
AskUserQuestion([
|
||||
{
|
||||
question: "Delegate phase execution to an external AI CLI? (current: <value or false>)",
|
||||
header: "Cross-AI",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "No (default: false)", description: "Use local executor agents." },
|
||||
{ label: "Yes", description: "Pipe phase prompt to `cross_ai_command` via stdin. Requires command to be set." }
|
||||
]
|
||||
},
|
||||
{
|
||||
question: "Cross-AI command template? (current: <value or null>)",
|
||||
header: "Cross-AI Command",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "Keep current", description: "Leave command unchanged." },
|
||||
{ label: "Clear (null)", description: "Unset the command." },
|
||||
{ label: "Enter command", description: "Shell command receiving phase prompt via stdin. Must produce SUMMARY.md-compatible output." }
|
||||
]
|
||||
},
|
||||
{
|
||||
question: "Cross-AI timeout (seconds)? (current: <value or 300>)",
|
||||
header: "Cross-AI Timeout",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "Keep current", description: "Leave timeout unchanged." },
|
||||
{ label: "Enter seconds", description: "Integer seconds. Non-numeric rejected. Default: 300" }
|
||||
]
|
||||
}
|
||||
])
|
||||
```
|
||||
|
||||
### Section 5 — Git Customization
|
||||
|
||||
```text
|
||||
AskUserQuestion([
|
||||
{
|
||||
question: "Git base branch? (current: <value or main>)",
|
||||
header: "Base Branch",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "Keep current", description: "Leave base branch unchanged." },
|
||||
{ label: "Enter branch name", description: "e.g., main, master, develop. Integration branch for phase/milestone branches." }
|
||||
]
|
||||
},
|
||||
{
|
||||
question: "Phase branch template? (current: <value or gsd/phase-{phase}-{slug}>)",
|
||||
header: "Phase Template",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "Keep current", description: "Leave template unchanged." },
|
||||
{ label: "Enter template", description: "Non-empty string with at least one placeholder. Available: {phase}, {slug}. Non-default values missing placeholders are rejected." }
|
||||
]
|
||||
},
|
||||
{
|
||||
question: "Milestone branch template? (current: <value or gsd/{milestone}-{slug}>)",
|
||||
header: "Milestone Template",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "Keep current", description: "Leave template unchanged." },
|
||||
{ label: "Enter template", description: "Non-empty string. Available placeholders: {milestone}, {slug}. Non-default values missing placeholders are rejected." }
|
||||
]
|
||||
}
|
||||
])
|
||||
```
|
||||
|
||||
### Section 6 — Runtime / Output
|
||||
|
||||
```text
|
||||
AskUserQuestion([
|
||||
{
|
||||
question: "Response language for agent output? (current: <value or null>)",
|
||||
header: "Language",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "Keep current", description: "Leave unchanged." },
|
||||
{ label: "Clear (null)", description: "Use Claude default (English)." },
|
||||
{ label: "Enter language", description: "Free-text language name or code (e.g., Japanese, pt, ko). Propagates to spawned agents." }
|
||||
]
|
||||
},
|
||||
{
|
||||
question: "Context window size (tokens)? (current: <value or 200000>)",
|
||||
header: "Context Window",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "Keep current", description: "Leave unchanged." },
|
||||
{ label: "Enter number", description: "Integer. Non-numeric rejected. Default: 200000. Use 1000000 for 1M-context models. Values >= 500000 enable adaptive enrichment." }
|
||||
]
|
||||
},
|
||||
{
|
||||
question: "Include gitignored files in broad searches? (current: <value or false>)",
|
||||
header: "Search Gitignored",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "No (default: false)", description: "Respect .gitignore during searches." },
|
||||
{ label: "Yes", description: "Add --no-ignore to broad searches (includes .planning/)." }
|
||||
]
|
||||
},
|
||||
{
|
||||
question: "Graphify build timeout (seconds)? (current: <value or 300>)",
|
||||
header: "Graphify Timeout",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "Keep current", description: "Leave timeout unchanged." },
|
||||
{ label: "Enter seconds", description: "Integer seconds. Non-numeric rejected. Default: 300" }
|
||||
]
|
||||
}
|
||||
])
|
||||
```
|
||||
|
||||
</step>
|
||||
|
||||
<step name="update_config">
|
||||
Merge the new settings into the existing config at `$GSD_CONFIG_PATH`. This merge is the
|
||||
core correctness invariant: **preserve every unrelated key** — do not clobber siblings.
|
||||
|
||||
Apply each selected value via `gsd-sdk query config-set <key> <value>` so the central
|
||||
validator (`isValidConfigKey`) accepts the write and the deep-merge preserves unrelated
|
||||
keys and sibling sub-objects.
|
||||
|
||||
```bash
|
||||
# Example — only write keys the user changed. "Keep current" selections are skipped.
|
||||
gsd-sdk query config-set workflow.plan_bounce_passes 5
|
||||
gsd-sdk query config-set workflow.subagent_timeout 900
|
||||
gsd-sdk query config-set git.base_branch main
|
||||
gsd-sdk query config-set context_window 1000000
|
||||
```
|
||||
|
||||
Conceptual shape after merge (unchanged top-level keys like `model_profile`,
|
||||
`granularity`, `mode`, `brave_search`, `agent_skills.*`, `hooks.context_warnings`, and
|
||||
anything not listed in Sections 1–6 MUST survive the update):
|
||||
|
||||
```json
|
||||
{
|
||||
...existing_config,
|
||||
"workflow": {
|
||||
...existing_workflow,
|
||||
"plan_bounce": <new|existing>,
|
||||
"plan_bounce_passes": <new|existing>,
|
||||
"plan_bounce_script": <new|existing|null>,
|
||||
"subagent_timeout": <new|existing>,
|
||||
"inline_plan_threshold": <new|existing>,
|
||||
"node_repair": <new|existing>,
|
||||
"node_repair_budget": <new|existing>,
|
||||
"auto_prune_state": <new|existing>,
|
||||
"max_discuss_passes": <new|existing>,
|
||||
"cross_ai_execution": <new|existing>,
|
||||
"cross_ai_command": <new|existing|null>,
|
||||
"cross_ai_timeout": <new|existing>
|
||||
},
|
||||
"git": {
|
||||
...existing_git,
|
||||
"base_branch": <new|existing>,
|
||||
"phase_branch_template": <new|existing>,
|
||||
"milestone_branch_template": <new|existing>
|
||||
},
|
||||
"response_language": <new|existing|null>,
|
||||
"context_window": <new|existing>,
|
||||
"search_gitignored": <new|existing>,
|
||||
"graphify": {
|
||||
...existing_graphify,
|
||||
"build_timeout": <new|existing>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Never emit a full overwrite of the file that omits keys the user did not touch. Always
|
||||
route each write through `gsd-sdk query config-set` so sibling preservation is handled by
|
||||
the central setter.
|
||||
</step>
|
||||
|
||||
<step name="confirm">
|
||||
Display:
|
||||
|
||||
```text
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
GSD ► ADVANCED SETTINGS UPDATED
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
| Setting | Value |
|
||||
|--------------------------------|-------|
|
||||
| workflow.plan_bounce | {on/off} |
|
||||
| workflow.plan_bounce_passes | {n} |
|
||||
| workflow.plan_bounce_script | {path/null} |
|
||||
| workflow.subagent_timeout | {seconds} |
|
||||
| workflow.inline_plan_threshold | {n} |
|
||||
| workflow.node_repair | {on/off} |
|
||||
| workflow.node_repair_budget | {n} |
|
||||
| workflow.auto_prune_state | {on/off} |
|
||||
| workflow.max_discuss_passes | {n} |
|
||||
| workflow.cross_ai_execution | {on/off} |
|
||||
| workflow.cross_ai_command | {cmd/null} |
|
||||
| workflow.cross_ai_timeout | {seconds} |
|
||||
| git.base_branch | {branch} |
|
||||
| git.phase_branch_template | {template} |
|
||||
| git.milestone_branch_template | {template} |
|
||||
| response_language | {lang/null} |
|
||||
| context_window | {tokens} |
|
||||
| search_gitignored | {on/off} |
|
||||
| graphify.build_timeout | {seconds} |
|
||||
|
||||
These settings apply to future /gsd-plan-phase, /gsd-execute-phase, /gsd-discuss-phase,
|
||||
and /gsd-ship runs.
|
||||
|
||||
For common-case toggles (model profile, research/plan_check/verifier, branching strategy,
|
||||
UI/AI phase gates), use /gsd-settings.
|
||||
```
|
||||
</step>
|
||||
|
||||
</process>
|
||||
|
||||
<success_criteria>
|
||||
- [ ] Current config read from resolved `$GSD_CONFIG_PATH`
|
||||
- [ ] Six sections rendered (Planning, Execution, Discussion, Cross-AI, Git, Runtime)
|
||||
- [ ] Every field pre-selected to its current value (or documented default if absent)
|
||||
- [ ] Numeric inputs validated — non-numeric rejected and re-prompted
|
||||
- [ ] Branch-template inputs validated — non-default must contain a placeholder
|
||||
- [ ] Null-allowed fields accept an empty input as a clear
|
||||
- [ ] Writes routed through `gsd-sdk query config-set` so unrelated keys are preserved
|
||||
- [ ] Confirmation table rendered listing all 19 fields
|
||||
</success_criteria>
|
||||
281
get-shit-done/workflows/settings-integrations.md
Normal file
281
get-shit-done/workflows/settings-integrations.md
Normal file
@@ -0,0 +1,281 @@
|
||||
<purpose>
|
||||
Interactive configuration of third-party integrations for GSD — search API keys
|
||||
(Brave / Firecrawl / Exa), code-review CLI routing (`review.models.<cli>`), and
|
||||
agent-skill injection (`agent_skills.<agent-type>`). Writes to
|
||||
`.planning/config.json` via `gsd-sdk`/`gsd-tools` so unrelated keys are
|
||||
preserved, never clobbered.
|
||||
|
||||
This command is deliberately separate from `/gsd-settings` (workflow toggles)
|
||||
and any `/gsd-settings-advanced` tuning surface. It exists because API keys and
|
||||
cross-tool routing are *connectivity* concerns, not workflow or tuning knobs.
|
||||
</purpose>
|
||||
|
||||
<security>
|
||||
**API keys are secrets.** They are written as plaintext to
|
||||
`.planning/config.json` — that is where secrets live on disk, and file
|
||||
permissions are the security boundary. The UI must never display, echo, or
|
||||
log the plaintext value. The workflow follows these rules:
|
||||
|
||||
- **Masking convention: `****<last-4>`** (e.g. `sk-abc123def456` → `****f456`).
|
||||
Strings shorter than 8 characters render as `****` with no tail so a short
|
||||
secret does not leak a meaningful fraction of its bytes. Unset values render
|
||||
as `(unset)`.
|
||||
- **Plaintext is never echoed by AskUserQuestion descriptions, confirmation
|
||||
tables, or any log line.** It is not written to any file under `.planning/`
|
||||
other than `config.json` itself.
|
||||
- **`config-set` output is masked** for keys in the secret set
|
||||
(`brave_search`, `firecrawl`, `exa_search`) — see
|
||||
`get-shit-done/bin/lib/secrets.cjs`.
|
||||
- **Agent-type and CLI slug validation.** `agent_skills.<agent-type>` and
|
||||
`review.models.<cli>` keys are matched against `^[a-zA-Z0-9_-]+$`. Inputs
|
||||
containing path separators (`/`, `\`, `..`), whitespace, or shell
|
||||
metacharacters are rejected. This closes off skill-injection attacks.
|
||||
</security>
|
||||
|
||||
<required_reading>
|
||||
Read all files referenced by the invoking prompt's execution_context before starting.
|
||||
</required_reading>
|
||||
|
||||
<process>
|
||||
|
||||
<step name="ensure_and_load_config">
|
||||
Ensure config exists and resolve the active config path (flat vs workstream, #2282):
|
||||
|
||||
```bash
|
||||
gsd-sdk query config-ensure-section
|
||||
if [[ -z "${GSD_CONFIG_PATH:-}" ]]; then
|
||||
if [[ -f .planning/active-workstream ]]; then
|
||||
WS=$(tr -d '\n\r' < .planning/active-workstream)
|
||||
GSD_CONFIG_PATH=".planning/workstreams/${WS}/config.json"
|
||||
else
|
||||
GSD_CONFIG_PATH=".planning/config.json"
|
||||
fi
|
||||
fi
|
||||
```
|
||||
|
||||
Store `$GSD_CONFIG_PATH`. Every subsequent read/write uses it.
|
||||
</step>
|
||||
|
||||
<step name="read_current">
|
||||
Read the current config and compute a masked view for display. For each
|
||||
integration field, compute one of:
|
||||
|
||||
- `(unset)` — field is null / missing
|
||||
- `****<last-4>` — secret field that is populated (plaintext never shown)
|
||||
- `<value>` — non-secret routing/skill string, shown as-is
|
||||
|
||||
```bash
|
||||
BRAVE=$(gsd-sdk query config-get brave_search --default null)
|
||||
FIRECRAWL=$(gsd-sdk query config-get firecrawl --default null)
|
||||
EXA=$(gsd-sdk query config-get exa_search --default null)
|
||||
SEARCH_GITIGNORED=$(gsd-sdk query config-get search_gitignored --default false)
|
||||
```
|
||||
|
||||
For each secret key (`brave_search`, `firecrawl`, `exa_search`) the displayed
|
||||
value is `****<last-4>` when set, never the raw string. Never echo the
|
||||
plaintext to stdout, stderr, or any log.
|
||||
</step>
|
||||
|
||||
<step name="section_1_search_integrations">
|
||||
|
||||
**Text mode (`workflow.text_mode: true` or `--text` flag):** Set
|
||||
`TEXT_MODE=true` and replace every `AskUserQuestion` call with a plain-text
|
||||
numbered list. Required for non-Claude runtimes.
|
||||
|
||||
Ask the user what they want to do for each search API key. For keys that are
|
||||
already set, show `**** already set` and offer Leave / Replace / Clear. For
|
||||
unset keys, offer Skip / Set.
|
||||
|
||||
```text
|
||||
AskUserQuestion([
|
||||
{
|
||||
question: "Brave Search API key — used for web research during plan/discuss phases",
|
||||
header: "Brave",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
// When already set:
|
||||
{ label: "Leave (**** already set)", description: "Keep current value" },
|
||||
{ label: "Replace", description: "Enter a new API key" },
|
||||
{ label: "Clear", description: "Remove the stored key" }
|
||||
// When unset:
|
||||
// { label: "Skip", description: "Leave unset" },
|
||||
// { label: "Set", description: "Enter an API key" }
|
||||
]
|
||||
},
|
||||
{
|
||||
question: "Firecrawl API key — used for deep-crawl scraping",
|
||||
header: "Firecrawl",
|
||||
multiSelect: false,
|
||||
options: [ /* same Leave/Replace/Clear or Skip/Set */ ]
|
||||
},
|
||||
{
|
||||
question: "Exa Search API key — used for semantic search",
|
||||
header: "Exa",
|
||||
multiSelect: false,
|
||||
options: [ /* same Leave/Replace/Clear or Skip/Set */ ]
|
||||
},
|
||||
{
|
||||
question: "Include gitignored files in local code searches?",
|
||||
header: "Gitignored",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "No (Recommended)", description: "Respect .gitignore. Safer — excludes secrets, node_modules, build artifacts." },
|
||||
{ label: "Yes", description: "Include gitignored files. Useful when secrets/artifacts genuinely contain searchable intent." }
|
||||
]
|
||||
}
|
||||
])
|
||||
```
|
||||
|
||||
For each "Set" or "Replace", follow with a text-input prompt that asks for the
|
||||
key value. **The answer must not be echoed back** in subsequent question
|
||||
descriptions or confirmation text. Write the value via:
|
||||
|
||||
```bash
|
||||
gsd-sdk query config-set brave_search "<value>" # masked in output
|
||||
gsd-sdk query config-set firecrawl "<value>" # masked in output
|
||||
gsd-sdk query config-set exa_search "<value>" # masked in output
|
||||
gsd-sdk query config-set search_gitignored true|false
|
||||
```
|
||||
|
||||
For "Clear", write `null`:
|
||||
|
||||
```bash
|
||||
gsd-sdk query config-set brave_search null
|
||||
```
|
||||
</step>
|
||||
|
||||
<step name="section_2_review_models">
|
||||
|
||||
`review.models.<cli>` is a map that tells the code-review workflow which
|
||||
shell command to invoke for a given reviewer flavor. Supported flavors:
|
||||
`claude`, `codex`, `gemini`, `opencode`.
|
||||
|
||||
```text
|
||||
AskUserQuestion([
|
||||
{
|
||||
question: "Which reviewer CLI do you want to configure?",
|
||||
header: "CLI",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "Claude", description: "review.models.claude — defaults to session model when unset" },
|
||||
{ label: "Codex", description: "review.models.codex — e.g. 'codex exec --model gpt-5'" },
|
||||
{ label: "Gemini", description: "review.models.gemini — e.g. 'gemini -m gemini-2.5-pro'" },
|
||||
{ label: "OpenCode", description: "review.models.opencode — e.g. 'opencode run --model claude-sonnet-4'" },
|
||||
{ label: "Done", description: "Skip — finish this section" }
|
||||
]
|
||||
}
|
||||
])
|
||||
```
|
||||
|
||||
For the selected CLI, show the current value (or `(unset)`) and offer
|
||||
Leave / Replace / Clear, followed by a text-input prompt for the new command
|
||||
string. Write via:
|
||||
|
||||
```bash
|
||||
gsd-sdk query config-set review.models.<cli> "<command string>"
|
||||
```
|
||||
|
||||
Loop until the user selects "Done".
|
||||
|
||||
The `review.models.<cli>` key is validated by the dynamic pattern
|
||||
`^review\.models\.[a-zA-Z0-9_-]+$`. Empty CLI slugs and path-containing slugs
|
||||
are rejected by `config-set` before any write.
|
||||
</step>
|
||||
|
||||
<step name="section_3_agent_skills">
|
||||
|
||||
`agent_skills.<agent-type>` injects extra skill names into an agent's spawn
|
||||
frontmatter. The slug is user-extensible, so input is free-text validated
|
||||
against `^[a-zA-Z0-9_-]+$`. Inputs with path separators, spaces, or shell
|
||||
metacharacters are rejected.
|
||||
|
||||
```text
|
||||
AskUserQuestion([
|
||||
{
|
||||
question: "Configure agent_skills for which agent type?",
|
||||
header: "Agent Type",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "gsd-executor", description: "Skills injected when spawning executor agents" },
|
||||
{ label: "gsd-planner", description: "Skills injected when spawning planner agents" },
|
||||
{ label: "gsd-verifier", description: "Skills injected when spawning verifier agents" },
|
||||
{ label: "Custom…", description: "Enter a custom agent-type slug" },
|
||||
{ label: "Done", description: "Skip — finish this section" }
|
||||
]
|
||||
}
|
||||
])
|
||||
```
|
||||
|
||||
For "Custom…", prompt for a slug and validate it matches
|
||||
`^[a-zA-Z0-9_-]+$`. If it fails validation, print:
|
||||
|
||||
```text
|
||||
Rejected: agent-type '<slug>' must match [a-zA-Z0-9_-]+ (no path separators,
|
||||
spaces, or shell metacharacters).
|
||||
```
|
||||
|
||||
and re-prompt.
|
||||
|
||||
For a selected slug, prompt for the comma-separated skill list (text input).
|
||||
Show the current value if any, offer Leave / Replace / Clear. Write via:
|
||||
|
||||
```bash
|
||||
gsd-sdk query config-set agent_skills.<slug> "<skill-a,skill-b,skill-c>"
|
||||
```
|
||||
|
||||
Loop until "Done".
|
||||
</step>
|
||||
|
||||
<step name="confirm">
|
||||
Display the masked confirmation table. **No plaintext API keys appear in this
|
||||
output under any circumstance.**
|
||||
|
||||
```text
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
GSD ► INTEGRATIONS UPDATED
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
Search Integrations
|
||||
| Field | Value |
|
||||
|--------------------|-------------------|
|
||||
| brave_search | ****<last-4> | (or "(unset)")
|
||||
| firecrawl | ****<last-4> |
|
||||
| exa_search | ****<last-4> |
|
||||
| search_gitignored | true | false |
|
||||
|
||||
Code Review CLI Routing
|
||||
| CLI | Command |
|
||||
|-------------|--------------------------------------|
|
||||
| claude | <value or (session model default)> |
|
||||
| codex | <value or (unset)> |
|
||||
| gemini | <value or (unset)> |
|
||||
| opencode | <value or (unset)> |
|
||||
|
||||
Agent Skills Injection
|
||||
| Agent Type | Skills |
|
||||
|------------------|---------------------------|
|
||||
| <slug> | <skill-a, skill-b> |
|
||||
| ... | ... |
|
||||
|
||||
Notes:
|
||||
- API keys are stored plaintext in .planning/config.json. The confirmation
|
||||
table above never displays plaintext — keys appear as ****<last-4>.
|
||||
- Plaintext is not echoed back by this workflow, not written to any log,
|
||||
and not displayed in error messages.
|
||||
|
||||
Quick commands:
|
||||
- /gsd-settings — workflow toggles and model profile
|
||||
- /gsd-set-profile <profile> — switch model profile
|
||||
```
|
||||
</step>
|
||||
|
||||
</process>
|
||||
|
||||
<success_criteria>
|
||||
- [ ] Current config read from `$GSD_CONFIG_PATH`
|
||||
- [ ] User presented with three sections: Search Integrations, Review CLI Routing, Agent Skills Injection
|
||||
- [ ] API keys written plaintext only to `config.json`; never echoed, never logged, never displayed
|
||||
- [ ] Masked confirmation table uses `****<last-4>` for set keys and `(unset)` for null
|
||||
- [ ] `review.models.<cli>` and `agent_skills.<agent-type>` keys validated against `[a-zA-Z0-9_-]+` before write
|
||||
- [ ] Config merge preserves all keys outside the three sections this workflow owns
|
||||
</success_criteria>
|
||||
@@ -40,9 +40,17 @@ Parse current values (default to `true` if not present):
|
||||
- `workflow.plan_check` — spawn plan checker during plan-phase
|
||||
- `workflow.verifier` — spawn verifier during execute-phase
|
||||
- `workflow.nyquist_validation` — validation architecture research during plan-phase (default: true if absent)
|
||||
- `workflow.pattern_mapper` — run gsd-pattern-mapper between research and planning (default: true if absent)
|
||||
- `workflow.ui_phase` — generate UI-SPEC.md design contracts for frontend phases (default: true if absent)
|
||||
- `workflow.ui_safety_gate` — prompt to run /gsd-ui-phase before planning frontend phases (default: true if absent)
|
||||
- `workflow.ai_integration_phase` — framework selection + eval strategy for AI phases (default: true if absent)
|
||||
- `workflow.tdd_mode` — enforce RED/GREEN/REFACTOR gate sequence during execute-phase (default: false if absent)
|
||||
- `workflow.code_review` — enable /gsd-code-review and /gsd-code-review-fix commands (default: true if absent)
|
||||
- `workflow.code_review_depth` — default depth for /gsd-code-review: `quick`, `standard`, or `deep` (default: `"standard"` if absent; only relevant when `code_review` is on)
|
||||
- `workflow.ui_review` — run visual quality audit (/gsd-ui-review) in autonomous mode (default: true if absent)
|
||||
- `commit_docs` — whether `.planning/` files are committed to git (default: true if absent)
|
||||
- `intel.enabled` — enable queryable codebase intelligence (/gsd-intel) (default: false if absent)
|
||||
- `graphify.enabled` — enable project knowledge graph (/gsd-graphify) (default: false if absent)
|
||||
- `model_profile` — which model each agent uses (default: `balanced`)
|
||||
- `git.branching_strategy` — branching approach (default: `"none"`)
|
||||
- `workflow.use_worktrees` — whether parallel executor agents run in worktree isolation (default: `true`)
|
||||
@@ -51,7 +59,46 @@ Parse current values (default to `true` if not present):
|
||||
<step name="present_settings">
|
||||
|
||||
**Text mode (`workflow.text_mode: true` in config or `--text` flag):** Set `TEXT_MODE=true` if `--text` is present in `$ARGUMENTS` OR `text_mode` from init JSON is `true`. When TEXT_MODE is active, replace every `AskUserQuestion` call with a plain-text numbered list and ask the user to type their choice number. This is required for non-Claude runtimes (OpenAI Codex, Gemini CLI, etc.) where `AskUserQuestion` is not available.
|
||||
Use AskUserQuestion with current values pre-selected:
|
||||
|
||||
**Non-Claude runtime note:** If `TEXT_MODE` is active (i.e. the runtime is non-Claude), prepend the following notice before the model profile question:
|
||||
|
||||
```
|
||||
Note: Quality, Balanced, Budget, and Adaptive profiles assign semantic tiers
|
||||
(Opus/Sonnet/Haiku) to each agent. When `runtime` is set in .planning/config.json,
|
||||
tiers resolve to runtime-native model IDs — on Codex that's gpt-5.4 / gpt-5.3-codex /
|
||||
gpt-5.4-mini with appropriate reasoning effort. See "Runtime-Aware Profiles" in
|
||||
docs/CONFIGURATION.md.
|
||||
|
||||
If `runtime` is unset on a non-Claude runtime, the profile tiers have no effect on
|
||||
actual model selection — agents use the runtime's default model. Choose "Inherit" to
|
||||
force session-model behavior, set `runtime` + a profile to get tiered models, or
|
||||
configure `model_overrides` manually in .planning/config.json to target specific
|
||||
models per agent.
|
||||
```
|
||||
|
||||
Use AskUserQuestion with current values pre-selected. Questions are grouped into six visual sections; the first question in each section carries the section-denoting `header` field (AskUserQuestion renders abbreviated section tags for grouping, max 12 chars).
|
||||
|
||||
Section layout:
|
||||
|
||||
### 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=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
|
||||
|
||||
**Conditional visibility — code_review_depth:** This question is shown only when the user's chosen `code_review` value (after they answer that question, or the pre-selected value if unchanged) is on. If `code_review` is off, omit the `code_review_depth` question from the AskUserQuestion block and preserve the existing `workflow.code_review_depth` value in config (do not overwrite). Implementation: ask the Model + Planning + Execution-up-to-Code-Review questions first; if `code_review=on`, include `code_review_depth` in the same batch; otherwise skip it. Conceptually this is a one-branch split on the `code_review` answer.
|
||||
|
||||
```
|
||||
AskUserQuestion([
|
||||
@@ -60,10 +107,10 @@ AskUserQuestion([
|
||||
header: "Model",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "Quality", description: "Opus everywhere except verification (highest cost)" },
|
||||
{ label: "Balanced (Recommended)", description: "Opus for planning, Sonnet for research/execution/verification" },
|
||||
{ label: "Budget", description: "Sonnet for writing, Haiku for research/verification (lowest cost)" },
|
||||
{ label: "Inherit", description: "Use current session model for all agents (best for OpenRouter, local models, or runtime model switching)" }
|
||||
{ label: "Quality", description: "Opus everywhere except verification (highest cost) — Claude only" },
|
||||
{ label: "Balanced (Recommended)", description: "Opus for planning, Sonnet for research/execution/verification — Claude only" },
|
||||
{ label: "Budget", description: "Sonnet for writing, Haiku for research/verification (lowest cost) — Claude only" },
|
||||
{ label: "Inherit", description: "Use current session model for all agents (required for non-Claude runtimes: Codex, Gemini CLI, OpenRouter, local models)" }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -93,6 +140,46 @@ AskUserQuestion([
|
||||
{ label: "No", description: "Skip post-execution verification" }
|
||||
]
|
||||
},
|
||||
{
|
||||
question: "Enable TDD Mode? (RED/GREEN/REFACTOR gates for eligible tasks)",
|
||||
header: "TDD",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "No (Recommended)", description: "Execute tasks normally. Tests written alongside implementation." },
|
||||
{ label: "Yes", description: "Planner applies type:tdd to business logic/APIs/validations; executor enforces gate sequence. End-of-phase review checks compliance." }
|
||||
]
|
||||
},
|
||||
{
|
||||
question: "Enable Code Review? (/gsd-code-review and /gsd-code-review-fix commands)",
|
||||
header: "Code Review",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "Yes (Recommended)", description: "Enable /gsd-code-review commands for reviewing source files changed during a phase." },
|
||||
{ label: "No", description: "Commands exit with a configuration gate message. Use when code review is handled externally." }
|
||||
]
|
||||
},
|
||||
// Conditional: include the following code_review_depth question ONLY when the user's
|
||||
// chosen code_review value is "Yes". If code_review is "No", omit this question from
|
||||
// the AskUserQuestion call and do not touch the existing workflow.code_review_depth value.
|
||||
{
|
||||
question: "Code Review Depth? (default depth for /gsd-code-review — override per-run with --depth=)",
|
||||
header: "Review Depth",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "Standard (Recommended)", description: "Per-file analysis. Balanced cost and signal." },
|
||||
{ label: "Quick", description: "Pattern-matching only. Fastest, lowest cost." },
|
||||
{ label: "Deep", description: "Cross-file analysis with import graphs. Highest cost, highest signal." }
|
||||
]
|
||||
},
|
||||
{
|
||||
question: "Enable UI Review? (visual quality audit via /gsd-ui-review in autonomous mode)",
|
||||
header: "UI Review",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "Yes (Recommended)", description: "Run visual quality audit after phase execution in autonomous mode." },
|
||||
{ label: "No", description: "Skip the UI audit step. Good for backend-only projects." }
|
||||
]
|
||||
},
|
||||
{
|
||||
question: "Auto-advance pipeline? (discuss → plan → execute automatically)",
|
||||
header: "Auto",
|
||||
@@ -102,6 +189,15 @@ AskUserQuestion([
|
||||
{ label: "Yes", description: "Chain stages via Task() subagents (same isolation)" }
|
||||
]
|
||||
},
|
||||
{
|
||||
question: "Run Pattern Mapper? (maps new files to existing codebase analogs between research and planning)",
|
||||
header: "Pattern Mapper",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "Yes (Recommended)", description: "gsd-pattern-mapper runs between research and plan steps. Surfaces conventions so new code follows house style." },
|
||||
{ label: "No", description: "Skip pattern mapping. Faster; lose consistency hinting for new files." }
|
||||
]
|
||||
},
|
||||
{
|
||||
question: "Enable Nyquist Validation? (researches test coverage during planning)",
|
||||
header: "Nyquist",
|
||||
@@ -136,7 +232,7 @@ AskUserQuestion([
|
||||
header: "AI Phase",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "Yes (Recommended)", description: "Run /gsd-ai-phase before planning AI system phases. Surfaces the right framework, researches its docs, and designs the evaluation strategy." },
|
||||
{ label: "Yes (Recommended)", description: "Run /gsd-ai-integration-phase before planning AI system phases. Surfaces the right framework, researches its docs, and designs the evaluation strategy." },
|
||||
{ label: "No", description: "Skip AI design contract. Good for non-AI phases or when framework is already decided." }
|
||||
]
|
||||
},
|
||||
@@ -168,6 +264,15 @@ AskUserQuestion([
|
||||
{ label: "Yes", description: "Search web for best practices before each question group. More informed questions but uses more tokens." }
|
||||
]
|
||||
},
|
||||
{
|
||||
question: "Commit .planning/ files to git? (controls whether plans/artifacts are tracked in your repo)",
|
||||
header: "Commit Docs",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "Yes (Recommended)", description: "Commit .planning/ to git. Plans, research, and phase artifacts travel with the repo." },
|
||||
{ label: "No", description: "Do not commit .planning/. Keep planning local only. Automatic when .planning/ is in .gitignore." }
|
||||
]
|
||||
},
|
||||
{
|
||||
question: "Skip discuss-phase in autonomous mode? (use ROADMAP phase goals as spec)",
|
||||
header: "Skip Discuss",
|
||||
@@ -185,6 +290,24 @@ AskUserQuestion([
|
||||
{ label: "Yes (Recommended)", description: "Each parallel executor runs in its own worktree branch — no conflicts between agents." },
|
||||
{ label: "No", description: "Disable worktree isolation. Agents run sequentially on the main working tree. Use if EnterWorktree creates branches from wrong base (known cross-platform issue)." }
|
||||
]
|
||||
},
|
||||
{
|
||||
question: "Enable Intel? (queryable codebase intelligence via /gsd-intel — builds a JSON index in .planning/intel/)",
|
||||
header: "Intel",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "No (Recommended)", description: "Skip intel indexing. Use when codebase is small or intel queries are not needed." },
|
||||
{ label: "Yes", description: "Enable /gsd-intel commands. Builds and queries a JSON index of the codebase." }
|
||||
]
|
||||
},
|
||||
{
|
||||
question: "Enable Graphify? (project knowledge graph via /gsd-graphify — builds a graph in .planning/graphs/)",
|
||||
header: "Graphify",
|
||||
multiSelect: false,
|
||||
options: [
|
||||
{ label: "No (Recommended)", description: "Skip knowledge graph. Use when dependency graphs are not needed." },
|
||||
{ label: "Yes", description: "Enable /gsd-graphify commands. Builds and queries a project knowledge graph." }
|
||||
]
|
||||
}
|
||||
])
|
||||
```
|
||||
@@ -197,21 +320,33 @@ Merge new settings into existing config.json:
|
||||
{
|
||||
...existing_config,
|
||||
"model_profile": "quality" | "balanced" | "budget" | "adaptive" | "inherit",
|
||||
"commit_docs": true/false,
|
||||
"workflow": {
|
||||
"research": true/false,
|
||||
"plan_check": true/false,
|
||||
"verifier": true/false,
|
||||
"auto_advance": true/false,
|
||||
"nyquist_validation": true/false,
|
||||
"pattern_mapper": true/false,
|
||||
"ui_phase": true/false,
|
||||
"ui_safety_gate": true/false,
|
||||
"ai_integration_phase": true/false,
|
||||
"tdd_mode": true/false,
|
||||
"code_review": true/false,
|
||||
"code_review_depth": "quick" | "standard" | "deep",
|
||||
"ui_review": true/false,
|
||||
"text_mode": true/false,
|
||||
"research_before_questions": true/false,
|
||||
"discuss_mode": "discuss" | "assumptions",
|
||||
"skip_discuss": true/false,
|
||||
"use_worktrees": true/false
|
||||
},
|
||||
"intel": {
|
||||
"enabled": true/false
|
||||
},
|
||||
"graphify": {
|
||||
"enabled": true/false
|
||||
},
|
||||
"git": {
|
||||
"branching_strategy": "none" | "phase" | "milestone",
|
||||
"quick_branch_template": <string|null>
|
||||
@@ -223,6 +358,8 @@ Merge new settings into existing config.json:
|
||||
}
|
||||
```
|
||||
|
||||
**Safe merge:** Apply each chosen value via `gsd-sdk query config-set <key.path> <value>` so unrelated keys are never clobbered. `code_review_depth` is written only if the code_review question was answered `on`; otherwise leave the existing value in place.
|
||||
|
||||
Write updated config to `$GSD_CONFIG_PATH` (the workstream-aware path resolved in `ensure_and_load_config`). Never hardcode `.planning/config.json` — workstream installs route to `.planning/workstreams/<slug>/config.json`.
|
||||
</step>
|
||||
|
||||
@@ -265,10 +402,21 @@ Write `~/.gsd/defaults.json` with:
|
||||
"verifier": <current>,
|
||||
"auto_advance": <current>,
|
||||
"nyquist_validation": <current>,
|
||||
"pattern_mapper": <current>,
|
||||
"ui_phase": <current>,
|
||||
"ui_safety_gate": <current>,
|
||||
"ai_integration_phase": <current>,
|
||||
"tdd_mode": <current>,
|
||||
"code_review": <current>,
|
||||
"code_review_depth": <current>,
|
||||
"ui_review": <current>,
|
||||
"skip_discuss": <current>
|
||||
},
|
||||
"intel": {
|
||||
"enabled": <current>
|
||||
},
|
||||
"graphify": {
|
||||
"enabled": <current>
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -287,7 +435,15 @@ Display:
|
||||
| Model Profile | {quality/balanced/budget/inherit} |
|
||||
| Plan Researcher | {On/Off} |
|
||||
| Plan Checker | {On/Off} |
|
||||
| Pattern Mapper | {On/Off} |
|
||||
| Execution Verifier | {On/Off} |
|
||||
| TDD Mode | {On/Off} |
|
||||
| Code Review | {On/Off} |
|
||||
| Code Review Depth | {quick/standard/deep} |
|
||||
| UI Review | {On/Off} |
|
||||
| Commit Docs | {On/Off} |
|
||||
| Intel | {On/Off} |
|
||||
| Graphify | {On/Off} |
|
||||
| Auto-Advance | {On/Off} |
|
||||
| Nyquist Validation | {On/Off} |
|
||||
| UI Phase | {On/Off} |
|
||||
@@ -301,10 +457,12 @@ Display:
|
||||
These settings apply to future /gsd-plan-phase and /gsd-execute-phase runs.
|
||||
|
||||
Quick commands:
|
||||
- /gsd-settings-integrations — configure API keys (Brave/Firecrawl/Exa), review.models CLI routing, and agent_skills injection
|
||||
- /gsd-set-profile <profile> — switch model profile
|
||||
- /gsd-plan-phase --research — force research
|
||||
- /gsd-plan-phase --skip-research — skip research
|
||||
- /gsd-plan-phase --skip-verify — skip plan check
|
||||
- /gsd-settings-advanced — power-user tuning (plan bounce, timeouts, branch templates, cross-AI, context window)
|
||||
```
|
||||
</step>
|
||||
|
||||
@@ -312,7 +470,7 @@ Quick commands:
|
||||
|
||||
<success_criteria>
|
||||
- [ ] Current config read
|
||||
- [ ] User presented with 14 settings (profile + 11 workflow toggles + git branching + ctx warnings)
|
||||
- [ ] User presented with 22 settings (profile + workflow toggles + features + git branching + ctx warnings), grouped into six sections: Planning, Execution, Docs & Output, Features, Model & Pipeline, Misc. `code_review_depth` is conditional on `code_review=on`.
|
||||
- [ ] Config updated with model_profile, workflow, and git sections
|
||||
- [ ] User offered to save as global defaults (~/.gsd/defaults.json)
|
||||
- [ ] Changes confirmed to user
|
||||
|
||||
@@ -255,15 +255,16 @@ The sketch-findings skill will auto-load when building the UI.
|
||||
|
||||
## ▶ Next Up
|
||||
|
||||
**Start building** — implement the validated design
|
||||
**Explore frontier sketches** — see what else is worth sketching based on what we've explored
|
||||
|
||||
`/gsd-plan-phase`
|
||||
`/gsd-sketch` (run with no argument — its frontier mode analyzes the sketch landscape and proposes consistency and frontier sketches)
|
||||
|
||||
───────────────────────────────────────────────────────────────
|
||||
|
||||
**Also available:**
|
||||
- `/gsd-plan-phase` — start building the real UI
|
||||
- `/gsd-ui-phase` — generate a UI design contract for a frontend phase
|
||||
- `/gsd-sketch` — sketch additional design areas
|
||||
- `/gsd-sketch [idea]` — sketch a specific new design area
|
||||
- `/gsd-explore` — continue exploring
|
||||
|
||||
───────────────────────────────────────────────────────────────
|
||||
@@ -279,5 +280,6 @@ The sketch-findings skill will auto-load when building the UI.
|
||||
- [ ] Reference files contain design decisions, CSS patterns, HTML structures, anti-patterns
|
||||
- [ ] `.planning/sketches/WRAP-UP-SUMMARY.md` written for project history
|
||||
- [ ] Project CLAUDE.md has auto-load routing line
|
||||
- [ ] Summary presented with next-step routing
|
||||
- [ ] Summary presented
|
||||
- [ ] Next-step options presented (including frontier sketch exploration via `/gsd-sketch`)
|
||||
</success_criteria>
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
Explore design directions through throwaway HTML mockups before committing to implementation.
|
||||
Each sketch produces 2-3 variants for comparison. Saves artifacts to `.planning/sketches/`.
|
||||
Companion to `/gsd-sketch-wrap-up`.
|
||||
|
||||
Supports two modes:
|
||||
- **Idea mode** (default) — user describes a design idea to sketch
|
||||
- **Frontier mode** — no argument or "frontier" / "what should I sketch?" — analyzes existing sketch landscape and proposes consistency and frontier sketches
|
||||
</purpose>
|
||||
|
||||
<required_reading>
|
||||
@@ -25,9 +29,60 @@ Read all files referenced by the invoking prompt's execution_context before star
|
||||
Parse `$ARGUMENTS` for:
|
||||
- `--quick` flag → set `QUICK_MODE=true`
|
||||
- `--text` flag → set `TEXT_MODE=true`
|
||||
- `frontier` or empty → set `FRONTIER_MODE=true`
|
||||
- Remaining text → the design idea to sketch
|
||||
|
||||
**Text mode (`workflow.text_mode: true` in config or `--text` flag):** Set `TEXT_MODE=true` if `--text` is present in `$ARGUMENTS` OR `text_mode` from init JSON is `true`. When TEXT_MODE is active, replace every `AskUserQuestion` call with a plain-text numbered list and ask the user to type their choice number. This is required for non-Claude runtimes (OpenAI Codex, Gemini CLI, etc.) where `AskUserQuestion` is not available.
|
||||
**Text mode:** If TEXT_MODE is enabled, replace AskUserQuestion calls with plain-text numbered lists.
|
||||
</step>
|
||||
|
||||
<step name="route">
|
||||
## Routing
|
||||
|
||||
- **FRONTIER_MODE is true** → Jump to `frontier_mode`
|
||||
- **Otherwise** → Continue to `setup_directory`
|
||||
</step>
|
||||
|
||||
<step name="frontier_mode">
|
||||
## Frontier Mode — Propose What to Sketch Next
|
||||
|
||||
### Load the Sketch Landscape
|
||||
|
||||
If no `.planning/sketches/` directory exists, tell the user there's nothing to analyze and offer to start fresh with an idea instead.
|
||||
|
||||
Otherwise, load in this order:
|
||||
|
||||
**a. MANIFEST.md** — the design direction, reference points, and sketch table with winners.
|
||||
|
||||
**b. Findings skills** — glob `./.claude/skills/sketch-findings-*/SKILL.md` and read any that exist, plus their `references/*.md`. These contain curated design decisions from prior wrap-ups.
|
||||
|
||||
**c. All sketch READMEs** — read `.planning/sketches/*/README.md` for design questions, winners, and tags.
|
||||
|
||||
### Analyze for Consistency Sketches
|
||||
|
||||
Review winning variants across all sketches. Look for:
|
||||
|
||||
- **Visual consistency gaps:** Two sketches made independent design choices that haven't been tested together.
|
||||
- **State combinations:** Individual states validated but not seen in sequence.
|
||||
- **Responsive gaps:** Validated at one viewport but the real app needs multiple.
|
||||
- **Theme coherence:** Individual components look good but haven't been composed into a full-page view.
|
||||
|
||||
If consistency risks exist, present them as concrete proposed sketches with names and design questions. If no meaningful gaps, say so and skip.
|
||||
|
||||
### Analyze for Frontier Sketches
|
||||
|
||||
Think laterally about the design direction from MANIFEST.md and what's been explored:
|
||||
|
||||
- **Unsketched screens:** UI surfaces assumed but unexplored.
|
||||
- **Interaction patterns:** Static layouts validated but transitions, loading, drag-and-drop need feeling.
|
||||
- **Edge case UI:** 0 items, 1000 items, errors, slow connections.
|
||||
- **Alternative directions:** Fresh takes on "fine but not great" sketches.
|
||||
- **Polish passes:** Typography, spacing, micro-interactions, empty states.
|
||||
|
||||
Present frontier sketches as concrete proposals numbered from the highest existing sketch number.
|
||||
|
||||
### Get Alignment and Execute
|
||||
|
||||
Present all consistency and frontier candidates, then ask which to run. When the user picks sketches, update `.planning/sketches/MANIFEST.md` and proceed directly to building them starting at `build_sketches`.
|
||||
</step>
|
||||
|
||||
<step name="setup_directory">
|
||||
@@ -49,25 +104,45 @@ COMMIT_DOCS=$(gsd-sdk query config-get commit_docs 2>/dev/null || echo "true")
|
||||
</step>
|
||||
|
||||
<step name="mood_intake">
|
||||
**If `QUICK_MODE` is true:** Skip mood intake. Use whatever the user provided in `$ARGUMENTS` as the design direction. Jump to `decompose`.
|
||||
**If `QUICK_MODE` is true:** Skip mood intake. Use whatever the user provided in `$ARGUMENTS` as the design direction. Jump to `load_spike_context`.
|
||||
|
||||
**Otherwise:**
|
||||
|
||||
Before sketching anything, explore the design intent through conversation. Ask one question at a time using AskUserQuestion, with a paragraph of context and reasoning for each.
|
||||
Before sketching anything, explore the design intent through conversation. Ask one question at a time — using AskUserQuestion in normal mode, or a plain-text numbered list if TEXT_MODE is active.
|
||||
|
||||
**Questions to cover (adapt to what the user has already shared):**
|
||||
|
||||
1. **Feel:** "What should this feel like? Give me adjectives, emotions, or a vibe." (e.g., "clean and clinical", "warm and playful", "dense and powerful")
|
||||
2. **References:** "What apps, sites, or products have a similar feel to what you're imagining?" (gives concrete visual anchors)
|
||||
3. **Core action:** "What's the single most important thing a user does here?" (focuses the sketch on what matters)
|
||||
1. **Feel:** "What should this feel like? Give me adjectives, emotions, or a vibe."
|
||||
2. **References:** "What apps, sites, or products have a similar feel to what you're imagining?"
|
||||
3. **Core action:** "What's the single most important thing a user does here?"
|
||||
|
||||
You may need more or fewer questions depending on how much the user shares upfront. After each answer, briefly reflect what you heard and how it shapes your thinking.
|
||||
After each answer, briefly reflect what you heard and how it shapes your thinking.
|
||||
|
||||
When you have enough signal, ask: **"I think I have a good sense of the direction. Ready for me to sketch, or want to keep discussing?"**
|
||||
|
||||
Only proceed when the user says go.
|
||||
</step>
|
||||
|
||||
<step name="load_spike_context">
|
||||
## Load Spike Context
|
||||
|
||||
If spikes exist for this project, read them to ground the sketches in reality. Mockups are still pure HTML, but they should reflect what's actually been proven — real data shapes, real component names, real interaction patterns.
|
||||
|
||||
**a.** Glob for `./.claude/skills/spike-findings-*/SKILL.md` and read any that exist, plus their `references/*.md`. These contain validated patterns and requirements.
|
||||
|
||||
**b.** Read `.planning/spikes/MANIFEST.md` if it exists — check the Requirements section for non-negotiable design constraints (e.g., "must support streaming", "must render markdown"). These requirements should be visible in the mockup even though the mockup doesn't implement them for real.
|
||||
|
||||
**c.** Read `.planning/spikes/CONVENTIONS.md` if it exists — the established stack informs what's buildable and what interaction patterns are idiomatic.
|
||||
|
||||
**How spike context improves sketches:**
|
||||
- Use real field names and data shapes from spike findings instead of generic placeholders
|
||||
- Show realistic UI states that match what the spikes proved (e.g., if streaming was validated, show a streaming message state)
|
||||
- Reference real component names and patterns from the target stack
|
||||
- Include interaction states that reflect what the spikes discovered (loading, error, reconnection states)
|
||||
|
||||
**If no spikes exist**, skip this step.
|
||||
</step>
|
||||
|
||||
<step name="decompose">
|
||||
Break the idea into 2-5 design questions. Present as a table:
|
||||
|
||||
@@ -98,18 +173,18 @@ Before sketching, ground the design in what's actually buildable. Sketches are H
|
||||
**a. Identify the target stack.** Check for package.json, Cargo.toml, etc. If the user mentioned a framework (React, SwiftUI, Flutter, etc.), note it.
|
||||
|
||||
**b. Check component/pattern availability.** Use context7 (resolve-library-id → query-docs) or web search to answer:
|
||||
- What layout primitives does the target framework provide? (grid systems, nav patterns, panel components)
|
||||
- Are there existing component libraries in use? (shadcn, Material UI, etc.) What components are available?
|
||||
- What interaction patterns are idiomatic? (e.g., sheet vs modal vs dialog in mobile)
|
||||
- What layout primitives does the target framework provide?
|
||||
- Are there existing component libraries in use? What components are available?
|
||||
- What interaction patterns are idiomatic?
|
||||
|
||||
**c. Note constraints that affect design.** Some things that look great in HTML are painful or impossible in certain stacks:
|
||||
**c. Note constraints that affect design:**
|
||||
- Platform conventions (iOS nav patterns, desktop menu bars, terminal grid constraints)
|
||||
- Framework limitations (what's easy vs requires custom work)
|
||||
- Existing design tokens or theme systems already in the project
|
||||
|
||||
**d. Let research inform variants.** Use findings to make variants that are actually buildable — at least one variant should follow the path of least resistance for the target stack.
|
||||
**d. Let research inform variants.** At least one variant should follow the path of least resistance for the target stack.
|
||||
|
||||
**Skip when unnecessary.** If it's a greenfield project with no stack chosen, or the user explicitly says "just explore visually, don't worry about implementation," skip this step entirely. The point is grounding, not gatekeeping.
|
||||
**Skip when unnecessary.** Greenfield project with no stack, or user says "just explore visually." The point is grounding, not gatekeeping.
|
||||
</step>
|
||||
|
||||
<step name="create_manifest">
|
||||
@@ -144,26 +219,24 @@ Build each sketch in order.
|
||||
|
||||
### For Each Sketch:
|
||||
|
||||
**a.** Find next available number by checking existing `.planning/sketches/NNN-*/` directories.
|
||||
Format: three-digit zero-padded + hyphenated descriptive name.
|
||||
**a.** Find next available number. Format: three-digit zero-padded + hyphenated descriptive name.
|
||||
|
||||
**b.** Create the sketch directory: `.planning/sketches/NNN-descriptive-name/`
|
||||
|
||||
**c.** Build `index.html` with 2-3 variants:
|
||||
|
||||
**First round — dramatic differences:** Build 2-3 meaningfully different approaches to the design question. Different layouts, different visual structures, different interaction models.
|
||||
|
||||
**Subsequent rounds — refinements:** Once the user has picked a direction or cherry-picked elements, build subtler variations within that direction.
|
||||
**First round — dramatic differences:** 2-3 meaningfully different approaches.
|
||||
**Subsequent rounds — refinements:** Subtler variations within the chosen direction.
|
||||
|
||||
Each variant is a page/tab in the same HTML file. Include:
|
||||
- Tab navigation to switch between variants (see `sketch-variant-patterns.md`)
|
||||
- Clear labels: "Variant A: Sidebar Layout", "Variant B: Top Nav", etc.
|
||||
- The sketch toolbar (see `sketch-tooling.md`)
|
||||
- All interactive elements functional (see `sketch-interactivity.md`)
|
||||
- Real-ish content, not lorem ipsum
|
||||
- Real-ish content, not lorem ipsum (use real field names from spike context if available)
|
||||
- Link to `../themes/default.css` for shared theme variables
|
||||
|
||||
**All sketches are plain HTML with inline CSS and JS.** No build step, no npm, no framework. Opens instantly in a browser.
|
||||
**All sketches are plain HTML with inline CSS and JS.** No build step, no npm, no framework.
|
||||
|
||||
**d.** Write `README.md`:
|
||||
|
||||
@@ -210,16 +283,16 @@ Compare: {what to look for between variants}
|
||||
──────────────────────────────────────────────────────────────
|
||||
|
||||
**f.** Handle feedback:
|
||||
- **Pick a direction:** "I like variant B" → mark winner in README, move to next sketch
|
||||
- **Cherry-pick elements:** "Rounded edges from A, color treatment from C" → build a synthesis as a new variant, show again
|
||||
- **Want more exploration:** "None of these feel right, try X instead" → build new variants
|
||||
- **Pick a direction:** mark winner, move to next sketch
|
||||
- **Cherry-pick elements:** build synthesis as new variant, show again
|
||||
- **Want more exploration:** build new variants
|
||||
|
||||
Iterate until the user is satisfied with a direction for this sketch.
|
||||
Iterate until satisfied.
|
||||
|
||||
**g.** Finalize:
|
||||
1. Mark the winning variant in the README frontmatter (`winner: "B"`)
|
||||
2. Add ★ indicator to the winning tab in the HTML
|
||||
3. Update `.planning/sketches/MANIFEST.md` with the sketch row
|
||||
1. Mark winning variant in README frontmatter (`winner: "B"`)
|
||||
2. Add ★ indicator to winning tab in HTML
|
||||
3. Update `.planning/sketches/MANIFEST.md`
|
||||
|
||||
**h.** Commit (if `COMMIT_DOCS` is true):
|
||||
```bash
|
||||
@@ -235,7 +308,7 @@ gsd-sdk query commit "docs(sketch-NNN): [winning direction] — [key visual insi
|
||||
</step>
|
||||
|
||||
<step name="report">
|
||||
After all sketches complete, present the summary:
|
||||
After all sketches complete:
|
||||
|
||||
```
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
@@ -263,8 +336,8 @@ After all sketches complete, present the summary:
|
||||
───────────────────────────────────────────────────────────────
|
||||
|
||||
**Also available:**
|
||||
- `/gsd-sketch` — sketch more (or run with no argument for frontier mode)
|
||||
- `/gsd-plan-phase` — start building the real UI
|
||||
- `/gsd-explore` — continue exploring the concept
|
||||
- `/gsd-spike` — spike technical feasibility of a design pattern
|
||||
|
||||
───────────────────────────────────────────────────────────────
|
||||
@@ -275,8 +348,9 @@ After all sketches complete, present the summary:
|
||||
<success_criteria>
|
||||
- [ ] `.planning/sketches/` created (auto-creates if needed, no project init required)
|
||||
- [ ] Design direction explored conversationally before any code (unless --quick)
|
||||
- [ ] Target stack researched — component availability, constraints, and idioms noted (unless greenfield/skipped)
|
||||
- [ ] Each sketch has 2-3 variants for comparison (at least one follows path of least resistance for target stack)
|
||||
- [ ] Spike context loaded — real data shapes, requirements, and conventions inform mockups
|
||||
- [ ] Target stack researched — component availability, constraints, idioms (unless greenfield/skipped)
|
||||
- [ ] Each sketch has 2-3 variants for comparison (at least one follows path of least resistance)
|
||||
- [ ] User can open and interact with sketches in a browser
|
||||
- [ ] Winning variant selected and marked for each sketch
|
||||
- [ ] All variants preserved (winner marked, not others deleted)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<purpose>
|
||||
Curate spike experiment findings and package them into a persistent project skill for future
|
||||
build conversations. Reads from `.planning/spikes/`, writes skill to `./.claude/skills/spike-findings-[project]/`
|
||||
(project-local) and summary to `.planning/spikes/WRAP-UP-SUMMARY.md`.
|
||||
Companion to `/gsd-spike`.
|
||||
Package spike experiment findings into a persistent project skill — an implementation blueprint
|
||||
for future build conversations. Reads from `.planning/spikes/`, writes skill to
|
||||
`./.claude/skills/spike-findings-[project]/` (project-local) and summary to
|
||||
`.planning/spikes/WRAP-UP-SUMMARY.md`. Companion to `/gsd-spike`.
|
||||
</purpose>
|
||||
|
||||
<required_reading>
|
||||
@@ -22,7 +22,7 @@ Read all files referenced by the invoking prompt's execution_context before star
|
||||
<step name="gather">
|
||||
## Gather Spike Inventory
|
||||
|
||||
1. Read `.planning/spikes/MANIFEST.md` for the overall idea context
|
||||
1. Read `.planning/spikes/MANIFEST.md` for the overall idea context and requirements
|
||||
2. Glob `.planning/spikes/*/README.md` and parse YAML frontmatter from each
|
||||
3. Check if `./.claude/skills/spike-findings-*/SKILL.md` exists for this project
|
||||
- If yes: read its `processed_spikes` list from the metadata section and filter those out
|
||||
@@ -93,21 +93,29 @@ For each included spike:
|
||||
<step name="synthesize">
|
||||
## Synthesize Reference Files
|
||||
|
||||
For each feature-area group, write a reference file at `references/[feature-area-name].md`:
|
||||
For each feature-area group, write a reference file at `references/[feature-area-name].md` as an **implementation blueprint** — it should read like a recipe, not a research paper. A future build session should be able to follow this and build the feature correctly without re-spiking anything.
|
||||
|
||||
```markdown
|
||||
# [Feature Area Name]
|
||||
|
||||
## Validated Patterns
|
||||
[For each validated finding: describe the approach that works, include key code snippets extracted from the spike source, explain why it works]
|
||||
## Requirements
|
||||
|
||||
## Landmines
|
||||
[Things that look right but aren't. Gotchas. Anti-patterns discovered during spiking.]
|
||||
[Non-negotiable design decisions from MANIFEST.md Requirements section that apply to this feature area. These MUST be honored in the real build. E.g., "Must use streaming JSON output", "Must support reconnection".]
|
||||
|
||||
## How to Build It
|
||||
|
||||
[Step-by-step: what to install, how to configure, what code pattern to use. Include key code snippets extracted from the spike source. This is the proven approach — not theory, but tested and working code.]
|
||||
|
||||
## What to Avoid
|
||||
|
||||
[Things that look right but aren't. Gotchas. Anti-patterns discovered during spiking. Dead ends that were tried and failed.]
|
||||
|
||||
## Constraints
|
||||
|
||||
[Hard facts: rate limits, library limitations, version requirements, incompatibilities]
|
||||
|
||||
## Origin
|
||||
|
||||
Synthesized from spikes: NNN, NNN, NNN
|
||||
Source files available in: sources/NNN-spike-name/, sources/NNN-spike-name/
|
||||
```
|
||||
@@ -121,7 +129,7 @@ Create (or update) the generated skill's SKILL.md:
|
||||
```markdown
|
||||
---
|
||||
name: spike-findings-[project-dir-name]
|
||||
description: Validated patterns, constraints, and implementation knowledge from spike experiments. Auto-loaded during implementation work on [project-dir-name].
|
||||
description: Implementation blueprint from spike experiments. Requirements, proven patterns, and verified knowledge for building [project-dir-name]. Auto-loaded during implementation work.
|
||||
---
|
||||
|
||||
<context>
|
||||
@@ -132,6 +140,15 @@ description: Validated patterns, constraints, and implementation knowledge from
|
||||
Spike sessions wrapped: [date(s)]
|
||||
</context>
|
||||
|
||||
<requirements>
|
||||
## Requirements
|
||||
|
||||
[Copied directly from MANIFEST.md Requirements section. These are non-negotiable design decisions that emerged from the user's choices during spiking. Every feature area reference must honor these.]
|
||||
|
||||
- [requirement 1]
|
||||
- [requirement 2]
|
||||
</requirements>
|
||||
|
||||
<findings_index>
|
||||
## Feature Areas
|
||||
|
||||
@@ -189,11 +206,47 @@ Add an auto-load routing line to the project's CLAUDE.md (create the file if it
|
||||
If this routing line already exists (append mode), leave it as-is.
|
||||
</step>
|
||||
|
||||
<step name="generate_conventions">
|
||||
## Generate or Update CONVENTIONS.md
|
||||
|
||||
Analyze all processed spikes for recurring patterns and write `.planning/spikes/CONVENTIONS.md`. This file tells future spike sessions *how we spike* — the stack, structure, and patterns that have been established.
|
||||
|
||||
1. Read all spike source code and READMEs looking for:
|
||||
- **Stack choices** — What language/framework/runtime appears across multiple spikes?
|
||||
- **Structure patterns** — Common file layouts, port numbers, naming schemes
|
||||
- **Recurring approaches** — How auth is handled, how styling is done, how data is served
|
||||
- **Tools & libraries** — Packages that showed up repeatedly with versions that worked
|
||||
|
||||
2. Write or update `.planning/spikes/CONVENTIONS.md`:
|
||||
|
||||
```markdown
|
||||
# Spike Conventions
|
||||
|
||||
Patterns and stack choices established across spike sessions. New spikes follow these unless the question requires otherwise.
|
||||
|
||||
## Stack
|
||||
[What we use for frontend, backend, scripts, and why — derived from what repeated across spikes]
|
||||
|
||||
## Structure
|
||||
[Common file layouts, port assignments, naming patterns]
|
||||
|
||||
## Patterns
|
||||
[Recurring approaches: how we handle auth, how we style, how we serve, etc.]
|
||||
|
||||
## Tools & Libraries
|
||||
[Preferred packages with versions that worked, and any to avoid]
|
||||
```
|
||||
|
||||
3. Only include patterns that appeared in 2+ spikes or were explicitly chosen by the user.
|
||||
|
||||
4. If `CONVENTIONS.md` already exists (append mode), update sections with new patterns. Remove entries contradicted by newer spikes.
|
||||
</step>
|
||||
|
||||
<step name="commit">
|
||||
Commit all artifacts (if `COMMIT_DOCS` is true):
|
||||
|
||||
```bash
|
||||
gsd-sdk query commit "docs(spike-wrap-up): package [N] spike findings into project skill" .planning/spikes/WRAP-UP-SUMMARY.md
|
||||
gsd-sdk query commit "docs(spike-wrap-up): package [N] spike findings into project skill" .planning/spikes/WRAP-UP-SUMMARY.md .planning/spikes/CONVENTIONS.md
|
||||
```
|
||||
</step>
|
||||
|
||||
@@ -206,6 +259,7 @@ gsd-sdk query commit "docs(spike-wrap-up): package [N] spike findings into proje
|
||||
**Processed:** {N} spikes
|
||||
**Feature areas:** {list}
|
||||
**Skill:** `./.claude/skills/spike-findings-[project]/`
|
||||
**Conventions:** `.planning/spikes/CONVENTIONS.md`
|
||||
**Summary:** `.planning/spikes/WRAP-UP-SUMMARY.md`
|
||||
**CLAUDE.md:** routing line added
|
||||
|
||||
@@ -214,56 +268,27 @@ The spike-findings skill will auto-load in future build conversations.
|
||||
</step>
|
||||
|
||||
<step name="whats_next">
|
||||
## What's Next — Intelligent Spike Routing
|
||||
## What's Next
|
||||
|
||||
Analyze the full spike landscape (MANIFEST.md, all curated findings, feature-area groupings, validated/invalidated/partial verdicts) and present three categories of next-step options:
|
||||
After the summary, present next-step options:
|
||||
|
||||
### Category A: Integration Spikes — "Do any validated spikes need to be tested together?"
|
||||
───────────────────────────────────────────────────────────────
|
||||
|
||||
Review every pair and cluster of VALIDATED spikes. Look for:
|
||||
## ▶ Next Up
|
||||
|
||||
- **Shared resources:** Two spikes that both touch the same API, database, state, or data format but were tested independently. Will they conflict, race, or step on each other?
|
||||
- **Data handoffs:** Spike A produces output that Spike B consumes. The formats were assumed compatible but never proven.
|
||||
- **Timing/ordering:** Spikes that work in isolation but have sequencing dependencies in the real flow (e.g., auth must complete before streaming starts).
|
||||
- **Resource contention:** Spikes that individually work but may compete for connections, memory, rate limits, or tokens when combined.
|
||||
**Explore frontier spikes** — see what else is worth spiking based on what we've learned
|
||||
|
||||
If integration risks exist, present them as concrete proposed spikes:
|
||||
`/gsd-spike` (run with no argument — its frontier mode analyzes the spike landscape and proposes integration and frontier spikes)
|
||||
|
||||
> **Integration spike candidates:**
|
||||
> - "Spikes 001 + 003 together: streaming through the authenticated connection" — these were tested separately but the real app needs both at once
|
||||
> - "Spikes 002 + 005 data handoff: does the parser output match what the renderer expects?"
|
||||
───────────────────────────────────────────────────────────────
|
||||
|
||||
If no meaningful integration risks exist, say so and skip this category.
|
||||
|
||||
### Category B: Frontier Spikes — "What else should we spike?"
|
||||
|
||||
Think laterally about the overall idea from MANIFEST.md and what's been proven so far. Consider:
|
||||
|
||||
- **Gaps in the vision:** What does the user's idea need that hasn't been spiked yet? Look at the MANIFEST.md idea description and identify capabilities that are assumed but unproven.
|
||||
- **Discovered dependencies:** Findings from completed spikes that reveal new questions. A spike that validated "X works" may imply "but we'd also need Y" — surface those implied needs.
|
||||
- **Alternative approaches:** If any spike was PARTIAL or INVALIDATED, suggest a different angle to achieve the same goal.
|
||||
- **Adjacent capabilities:** Things that aren't strictly required but would meaningfully improve the idea if feasible — worth a quick spike to find out.
|
||||
- **Comparison opportunities:** If a spike used one library/approach and it worked but felt heavy or awkward, suggest a comparison spike with an alternative.
|
||||
|
||||
Present frontier spikes as concrete proposals with names, validation questions (Given/When/Then), and risk-ordering:
|
||||
|
||||
> **Frontier spike candidates:**
|
||||
> 1. `NNN-descriptive-name` — Given [X], when [Y], then [Z]. *Why now: [reason this is the logical next thing to explore]*
|
||||
> 2. `NNN-descriptive-name` — Given [X], when [Y], then [Z]. *Why now: [reason]*
|
||||
|
||||
Number them continuing from the highest existing spike number.
|
||||
|
||||
### Category C: Standard Options
|
||||
|
||||
- `/gsd-plan-phase` — Start planning the real implementation
|
||||
- `/gsd-add-phase` — Add a phase based on spike findings
|
||||
- `/gsd-spike` — Spike additional ideas
|
||||
- `/gsd-explore` — Continue exploring
|
||||
**Also available:**
|
||||
- `/gsd-plan-phase` — start planning the real implementation
|
||||
- `/gsd-spike [idea]` — spike a specific new idea
|
||||
- `/gsd-explore` — continue exploring
|
||||
- Other
|
||||
|
||||
### Presenting the Options
|
||||
|
||||
Present all applicable categories, then ask the user which direction to go. If the user picks a frontier or integration spike, write the spike definitions directly into `.planning/spikes/MANIFEST.md` (appending to the existing table) and kick off `/gsd-spike` with those spikes pre-defined — the user shouldn't have to re-describe what was just proposed.
|
||||
───────────────────────────────────────────────────────────────
|
||||
</step>
|
||||
|
||||
</process>
|
||||
@@ -271,11 +296,11 @@ Present all applicable categories, then ask the user which direction to go. If t
|
||||
<success_criteria>
|
||||
- [ ] All unprocessed spikes auto-included and processed
|
||||
- [ ] Spikes grouped by feature area
|
||||
- [ ] Spike-findings skill exists at `./.claude/skills/` with SKILL.md, references/, sources/
|
||||
- [ ] Core source files from all spikes copied into sources/
|
||||
- [ ] Reference files contain validated patterns, code snippets, landmines, constraints
|
||||
- [ ] Spike-findings skill exists at `./.claude/skills/` with SKILL.md (including requirements), references/, sources/
|
||||
- [ ] Reference files are implementation blueprints with Requirements, How to Build It, What to Avoid, Constraints
|
||||
- [ ] `.planning/spikes/CONVENTIONS.md` created or updated with recurring stack/structure/pattern choices
|
||||
- [ ] `.planning/spikes/WRAP-UP-SUMMARY.md` written for project history
|
||||
- [ ] Project CLAUDE.md has auto-load routing line
|
||||
- [ ] Summary presented
|
||||
- [ ] Intelligent next-step analysis presented with integration spike candidates, frontier spike candidates, and standard options
|
||||
- [ ] Next-step options presented (including frontier spike exploration via `/gsd-spike`)
|
||||
</success_criteria>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<purpose>
|
||||
Rapid feasibility validation through focused, throwaway experiments. Each spike answers one
|
||||
specific question with observable evidence. Saves artifacts to `.planning/spikes/`.
|
||||
Companion to `/gsd-spike-wrap-up`.
|
||||
Spike an idea through experiential exploration — build focused experiments to feel the pieces
|
||||
of a future app, validate feasibility, and produce verified knowledge for the real build.
|
||||
Saves artifacts to `.planning/spikes/`. Companion to `/gsd-spike-wrap-up`.
|
||||
|
||||
Supports two modes:
|
||||
- **Idea mode** (default) — user describes an idea to spike
|
||||
- **Frontier mode** — no argument or "frontier" / "what should I spike?" — analyzes existing spike landscape and proposes integration and frontier spikes
|
||||
</purpose>
|
||||
|
||||
<required_reading>
|
||||
@@ -20,9 +24,62 @@ Read all files referenced by the invoking prompt's execution_context before star
|
||||
Parse `$ARGUMENTS` for:
|
||||
- `--quick` flag → set `QUICK_MODE=true`
|
||||
- `--text` flag → set `TEXT_MODE=true`
|
||||
- `frontier` or empty → set `FRONTIER_MODE=true`
|
||||
- Remaining text → the idea to spike
|
||||
|
||||
**Text mode:** If TEXT_MODE is enabled, replace AskUserQuestion calls with plain-text numbered lists — emit the options and ask the user to type the number of their choice.
|
||||
**Text mode:** If TEXT_MODE is enabled, replace AskUserQuestion calls with plain-text numbered lists.
|
||||
</step>
|
||||
|
||||
<step name="route">
|
||||
## Routing
|
||||
|
||||
- **FRONTIER_MODE is true** → Jump to `frontier_mode`
|
||||
- **Otherwise** → Continue to `setup_directory`
|
||||
</step>
|
||||
|
||||
<step name="frontier_mode">
|
||||
## Frontier Mode — Propose What to Spike Next
|
||||
|
||||
### Load the Spike Landscape
|
||||
|
||||
If no `.planning/spikes/` directory exists, tell the user there's nothing to analyze and offer to start fresh with an idea instead.
|
||||
|
||||
Otherwise, load in this order:
|
||||
|
||||
**a. MANIFEST.md** — the overall idea, requirements, and spike table with verdicts.
|
||||
|
||||
**b. Findings skills** — glob `./.claude/skills/spike-findings-*/SKILL.md` and read any that exist, plus their `references/*.md`. These contain curated knowledge from prior wrap-ups.
|
||||
|
||||
**c. CONVENTIONS.md** — read `.planning/spikes/CONVENTIONS.md` if it exists. Established stack and patterns.
|
||||
|
||||
**d. All spike READMEs** — read `.planning/spikes/*/README.md` for verdicts, results, investigation trails, and tags.
|
||||
|
||||
### Analyze for Integration Spikes
|
||||
|
||||
Review every pair and cluster of VALIDATED spikes. Look for:
|
||||
|
||||
- **Shared resources:** Two spikes that both touch the same API, database, state, or data format but were tested independently.
|
||||
- **Data handoffs:** Spike A produces output that Spike B consumes. The formats were assumed compatible but never proven.
|
||||
- **Timing/ordering:** Spikes that work in isolation but have sequencing dependencies in the real flow.
|
||||
- **Resource contention:** Spikes that individually work but may compete for connections, memory, rate limits, or tokens when combined.
|
||||
|
||||
If integration risks exist, present them as concrete proposed spikes with names and Given/When/Then validation questions. If no meaningful integration risks exist, say so and skip this category.
|
||||
|
||||
### Analyze for Frontier Spikes
|
||||
|
||||
Think laterally about the overall idea from MANIFEST.md and what's been proven so far. Consider:
|
||||
|
||||
- **Gaps in the vision:** Capabilities assumed but unproven.
|
||||
- **Discovered dependencies:** Findings that reveal new questions.
|
||||
- **Alternative approaches:** Different angles for PARTIAL or INVALIDATED spikes.
|
||||
- **Adjacent capabilities:** Things that would meaningfully improve the idea if feasible.
|
||||
- **Comparison opportunities:** Approaches that worked but felt heavy.
|
||||
|
||||
Present frontier spikes as concrete proposals numbered from the highest existing spike number with Given/When/Then and risk ordering.
|
||||
|
||||
### Get Alignment and Execute
|
||||
|
||||
Present all integration and frontier candidates, then ask which to run. When the user picks spikes, write definitions into `.planning/spikes/MANIFEST.md` (appending to existing table) and proceed directly to building them starting at `research`.
|
||||
</step>
|
||||
|
||||
<step name="setup_directory">
|
||||
@@ -44,13 +101,16 @@ COMMIT_DOCS=$(gsd-sdk query config-get commit_docs 2>/dev/null || echo "true")
|
||||
</step>
|
||||
|
||||
<step name="detect_stack">
|
||||
Check for the project's tech stack to inform spike technology choices:
|
||||
Check for the project's tech stack to inform spike technology choices.
|
||||
|
||||
**Check conventions first.** If `.planning/spikes/CONVENTIONS.md` exists, follow its stack and patterns — these represent validated choices the user expects to see continued.
|
||||
|
||||
**Then check the project stack:**
|
||||
```bash
|
||||
ls package.json pyproject.toml Cargo.toml go.mod 2>/dev/null
|
||||
```
|
||||
|
||||
Use the project's language/framework by default. For greenfield projects with no existing stack, pick whatever gets to a runnable result fastest (Python, Node, Bash, single HTML file).
|
||||
Use the project's language/framework by default. For greenfield projects with no conventions and no existing stack, pick whatever gets to a runnable result fastest.
|
||||
|
||||
Avoid unless the spike specifically requires it:
|
||||
- Complex package management beyond `npm install` or `pip install`
|
||||
@@ -59,22 +119,31 @@ Avoid unless the spike specifically requires it:
|
||||
- Env files or config systems — hardcode everything
|
||||
</step>
|
||||
|
||||
<step name="check_prior_spikes">
|
||||
If `.planning/spikes/MANIFEST.md` exists, read it. Scan the verdicts, names, and validation questions of all prior spikes. When decomposing the new idea, cross-reference against this history:
|
||||
<step name="load_prior_context">
|
||||
If `.planning/spikes/` has existing content, load context in this priority order:
|
||||
|
||||
- **Skip already-validated questions.** If a prior spike proved "WebSocket streaming works" with a VALIDATED verdict, don't re-spike it. Note the prior spike number and move on.
|
||||
- **Build on prior findings.** If a prior spike was INVALIDATED or PARTIAL, factor that into the new decomposition — don't repeat the same approach, and flag the constraint to the user.
|
||||
- **Call out relevant prior art.** When presenting the decomposition, mention any prior spikes that overlap: "Spike 003 already validated X, so we can skip that and focus on Y."
|
||||
**a. Conventions:** Read `.planning/spikes/CONVENTIONS.md` if it exists.
|
||||
|
||||
If no `.planning/spikes/MANIFEST.md` exists, skip this step.
|
||||
**b. Findings skills:** Glob for `./.claude/skills/spike-findings-*/SKILL.md` and read any that exist, plus their `references/*.md` files.
|
||||
|
||||
**c. Manifest:** Read `.planning/spikes/MANIFEST.md` for the index of all spikes.
|
||||
|
||||
**d. Related READMEs:** Based on the new idea, identify which prior spikes are related by matching tags, names, technologies, or domain overlap. Read only those `.planning/spikes/*/README.md` files. Skip unrelated ones.
|
||||
|
||||
Cross-reference against this full body of prior work:
|
||||
- **Skip already-validated questions.** Note the prior spike number and move on.
|
||||
- **Build on prior findings.** Don't repeat failed approaches. Use their Research and Results sections.
|
||||
- **Reuse prior research.** Carry findings forward rather than re-researching.
|
||||
- **Follow established conventions.** Mention any deviation.
|
||||
- **Call out relevant prior art** when presenting the decomposition.
|
||||
|
||||
If no `.planning/spikes/` exists, skip this step.
|
||||
</step>
|
||||
|
||||
<step name="decompose">
|
||||
**If `QUICK_MODE` is true:** Skip decomposition and alignment. Take the user's idea as a single spike question. Assign it spike number `001` (or next available). Jump to `research`.
|
||||
**If `QUICK_MODE` is true:** Skip decomposition and alignment. Take the user's idea as a single spike question. Assign it the next available number. Jump to `research`.
|
||||
|
||||
**Otherwise:**
|
||||
|
||||
Break the idea into 2-5 independent questions that each prove something specific. Frame each as an informal Given/When/Then. Present as a table:
|
||||
Break the idea into 2-5 independent questions. Frame each as Given/When/Then. Present as a table:
|
||||
|
||||
```
|
||||
| # | Spike | Type | Validates (Given/When/Then) | Risk |
|
||||
@@ -86,30 +155,17 @@ Break the idea into 2-5 independent questions that each prove something specific
|
||||
|
||||
**Spike types:**
|
||||
- **standard** — one approach answering one question
|
||||
- **comparison** — same question, different approaches. Use a shared number with lettered variants: `NNN-a-name` and `NNN-b-name`. Both built back-to-back, then head-to-head comparison.
|
||||
- **comparison** — same question, different approaches. Shared number with letter suffix.
|
||||
|
||||
Good spikes answer one specific feasibility question:
|
||||
- "Can we parse X format and extract Y?" — script that does it on a sample file
|
||||
- "How fast is X approach?" — benchmark with real-ish data
|
||||
- "Can we get X and Y to talk to each other?" — thinnest integration
|
||||
- "What does X feel like as a UI?" — minimal interactive prototype
|
||||
- "Does X API actually support Y?" — script that calls it and shows the response
|
||||
- "Should we use X or Y for this?" — **comparison spike**: same thin proof built with both
|
||||
Good spikes: specific feasibility questions with observable output.
|
||||
Bad spikes: too broad, no observable output, or just reading/planning.
|
||||
|
||||
Bad spikes are too broad or don't produce observable output:
|
||||
- "Set up the project" — not a question, just busywork
|
||||
- "Design the architecture" — planning, not spiking
|
||||
- "Build the backend" — too broad, no specific question
|
||||
- "Research best practices" — open-ended reading with no runnable output
|
||||
|
||||
Order by risk — the spike most likely to kill the idea runs first.
|
||||
Order by risk — most likely to kill the idea runs first.
|
||||
</step>
|
||||
|
||||
<step name="align">
|
||||
**If `QUICK_MODE` is true:** Skip.
|
||||
|
||||
Present the ordered spike list and ask which to build:
|
||||
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ CHECKPOINT: Decision Required ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
@@ -119,35 +175,33 @@ Present the ordered spike list and ask which to build:
|
||||
──────────────────────────────────────────────────────────────
|
||||
→ Build all in this order, or adjust the list?
|
||||
──────────────────────────────────────────────────────────────
|
||||
|
||||
The user may reorder, merge, split, or skip spikes. Wait for alignment.
|
||||
</step>
|
||||
|
||||
<step name="research">
|
||||
## Research Before Building
|
||||
## Research and Briefing Before Each Spike
|
||||
|
||||
Before writing any spike code, ground each spike in reality. This prevents building against outdated APIs, picking the wrong library, or discovering mid-spike that the approach is impossible.
|
||||
This step runs **before each individual spike**, not once at the start.
|
||||
|
||||
For each spike about to be built:
|
||||
**a. Present a spike briefing:**
|
||||
|
||||
**a. Identify unknowns.** What libraries, APIs, protocols, or techniques does this spike depend on? What assumptions are you making about how they work?
|
||||
> **Spike NNN: Descriptive Name**
|
||||
> [2-3 sentences: what this spike is, why it matters, key risk or unknown.]
|
||||
|
||||
**b. Check current docs.** Use context7 (resolve-library-id → query-docs) for any library or framework involved. Use web search for APIs, services, or techniques without a context7 entry. Read actual documentation — not training data, which may be stale.
|
||||
**b. Research the current state of the art.** Use context7 (resolve-library-id → query-docs) for libraries/frameworks. Use web search for APIs/services without a context7 entry. Read actual documentation.
|
||||
|
||||
**c. Validate feasibility before coding.** Specifically check:
|
||||
- Does the API/library actually support what the spike assumes? (Check endpoints, methods, capabilities)
|
||||
- What's the current recommended approach? (The "right way" changes — what was learned in training may be deprecated)
|
||||
- Are there version constraints, breaking changes, or migration gotchas?
|
||||
- Are there rate limits, auth requirements, or platform restrictions that would block the spike?
|
||||
**c. Surface competing approaches** as a table:
|
||||
|
||||
**d. Pick the right tool.** If multiple libraries could solve the problem, briefly compare them on: current maintenance status, API fit for the specific spike question, and complexity. Pick the one that gets to a runnable answer fastest with the fewest surprises.
|
||||
| Approach | Tool/Library | Pros | Cons | Status |
|
||||
|----------|-------------|------|------|--------|
|
||||
| ... | ... | ... | ... | ... |
|
||||
|
||||
**e. Capture research findings.** Add a `## Research` section to the spike's README (before `## How to Run`) with:
|
||||
- Which docs were checked and key findings
|
||||
- The chosen approach and why
|
||||
- Any gotchas or constraints discovered
|
||||
**Chosen approach:** [which one and why]
|
||||
|
||||
**Skip research when unnecessary.** If the spike uses only well-known, stable tools already verified in this session, or if the entire spike is pure logic with no external dependencies, skip this step. The goal is grounding in reality, not busywork.
|
||||
If 2+ credible approaches exist, plan to build quick variants within the spike and compare them.
|
||||
|
||||
**d. Capture research findings** in a `## Research` section in the README.
|
||||
|
||||
**Skip when unnecessary** for pure logic with no external dependencies.
|
||||
</step>
|
||||
|
||||
<step name="create_manifest">
|
||||
@@ -159,57 +213,67 @@ Create or update `.planning/spikes/MANIFEST.md`:
|
||||
## Idea
|
||||
[One paragraph describing the overall idea being explored]
|
||||
|
||||
## Requirements
|
||||
[Design decisions that emerged from the user's choices during spiking. Non-negotiable for the real build. Updated as spikes progress.]
|
||||
|
||||
- [e.g., "Must use streaming JSON output, not single-response"]
|
||||
- [e.g., "Must support reconnection on network failure"]
|
||||
|
||||
## Spikes
|
||||
|
||||
| # | Name | Type | Validates | Verdict | Tags |
|
||||
|---|------|------|-----------|---------|------|
|
||||
| 001 | websocket-streaming | standard | WS connections can stream LLM output | VALIDATED | websocket, real-time |
|
||||
| 002a | pdf-parse-pdfjs | comparison | PDF table extraction | WINNER | pdf, parsing |
|
||||
| 002b | pdf-parse-camelot | comparison | PDF table extraction | — | pdf, parsing |
|
||||
```
|
||||
|
||||
If MANIFEST.md already exists, append new spikes to the existing table.
|
||||
**Track requirements as they emerge.** When the user expresses a preference during spiking, add it to the Requirements section immediately.
|
||||
</step>
|
||||
|
||||
<step name="reground">
|
||||
## Re-Ground Before Each Spike
|
||||
|
||||
Before starting each spike (not just the first), re-read `.planning/spikes/MANIFEST.md` and `.planning/spikes/CONVENTIONS.md` to prevent drift within long sessions. Check the Requirements section — make sure the spike doesn't contradict any established requirements.
|
||||
</step>
|
||||
|
||||
<step name="build_spikes">
|
||||
Build each spike sequentially, highest-risk first.
|
||||
## Build Each Spike Sequentially
|
||||
|
||||
**Comparison spikes** use a shared number with lettered variants: `NNN-a-descriptive-name` and `NNN-b-descriptive-name`. Both answer the same question using different approaches. Build them back-to-back, then report a head-to-head comparison before moving on. Judge on criteria that matter for the real build: API ergonomics, output quality, complexity, performance, or whatever the user cares about. The comparison spike's verdict names the winner and why.
|
||||
**Depth over speed.** The goal is genuine understanding, not a quick verdict. Never declare VALIDATED after a single happy-path test. Follow surprising findings. Test edge cases. Document the investigation trail, not just the conclusion.
|
||||
|
||||
**Comparison spikes** use shared number with letter suffix: `NNN-a-name` / `NNN-b-name`. Build back-to-back, then head-to-head comparison.
|
||||
|
||||
### For Each Spike:
|
||||
|
||||
**a.** Find next available number by checking existing `.planning/spikes/NNN-*/` directories.
|
||||
Format: three-digit zero-padded + hyphenated descriptive name. Comparison spikes: same number with letter suffix — `002a-pdf-parse-pdfjs`, `002b-pdf-parse-camelot`.
|
||||
**a.** Create `.planning/spikes/NNN-descriptive-name/`
|
||||
|
||||
**b.** Create the spike directory: `.planning/spikes/NNN-descriptive-name/`
|
||||
**b.** Default to giving the user something they can experience. The bias should be toward building a simple UI or interactive demo, not toward stdout that only Claude reads. The user wants to *feel* the spike working, not just be told it works.
|
||||
|
||||
**c.** Assess observability needs before writing code. Ask: **can Claude fully verify this spike's outcome by running a command and reading stdout, or does it require human interaction with a runtime?**
|
||||
**The default is: build something the user can interact with.** This could be:
|
||||
- A simple HTML page that shows the result visually
|
||||
- A web UI with a button that triggers the action and shows the response
|
||||
- A page that displays data flowing through a pipeline
|
||||
- A minimal interface where the user can try different inputs and see outputs
|
||||
|
||||
Spikes that need runtime observability:
|
||||
- **UI spikes** — anything with a browser, clicks, visual feedback
|
||||
- **Streaming spikes** — WebSockets, SSE, real-time data flow
|
||||
- **Multi-process spikes** — client/server, IPC, subprocess orchestration
|
||||
- **Timing-sensitive spikes** — race conditions, debounce, polling, reconnection
|
||||
- **External API spikes** — where the API response shape, latency, or error behavior matters for the verdict
|
||||
**Only fall back to stdout/CLI verification when the spike is genuinely about a fact, not a feeling:**
|
||||
- Pure data transformation where the answer is "yes it parses correctly"
|
||||
- Binary yes/no questions (does this API authenticate? does this library exist?)
|
||||
- Benchmark numbers (how fast is X? how much memory does Y use?)
|
||||
|
||||
Spikes that do NOT need it:
|
||||
- Pure computation (parse this file, transform this data)
|
||||
- Single-run scripts with deterministic stdout
|
||||
- Anything Claude can run and check the output of directly
|
||||
When in doubt, build the UI. It takes a few extra minutes but produces a spike the user can actually demo and feel confident about.
|
||||
|
||||
**If the spike needs runtime observability,** build a forensic log layer into the spike:
|
||||
**If the spike needs runtime observability,** build a forensic log layer:
|
||||
1. Event log array with ISO timestamps and category tags
|
||||
2. Export mechanism (server: GET endpoint, CLI: JSON file, browser: Export button)
|
||||
3. Log summary (event counts, duration, errors, metadata)
|
||||
4. Analysis helpers if volume warrants it
|
||||
|
||||
1. **An event log array** at module level that captures every meaningful event with an ISO timestamp and a direction/category tag (e.g., `"user_input"`, `"api_response"`, `"sse_frame"`, `"error"`, `"state_change"`)
|
||||
2. **A log export mechanism** appropriate to the spike's runtime:
|
||||
- For server spikes: a `GET /api/export-log` endpoint returning downloadable JSON
|
||||
- For CLI spikes: write `spike-log-{timestamp}.json` to the spike directory on exit or on signal
|
||||
- For browser spikes: a visible "Export Log" button that triggers a JSON download
|
||||
3. **A log summary** included in the export: total event counts by category, duration, errors detected, environment metadata
|
||||
4. **Analysis helpers** if the event volume warrants it: a small script (bash/python) in the spike directory that extracts the signal from the log. Name it `analyze-log.sh` or similar.
|
||||
**c.** Build the code. Start with simplest version, then deepen.
|
||||
|
||||
Keep the logging lightweight — an array push per event, not a logging framework. Inline it in the spike code.
|
||||
**d.** Iterate when findings warrant it:
|
||||
- **Surprising surface?** Write a follow-up test that isolates and explores it.
|
||||
- **Answer feels shallow?** Probe edge cases — large inputs, concurrent requests, malformed data, network failures.
|
||||
- **Assumption wrong?** Adjust. Note the pivot in the README.
|
||||
|
||||
**d.** Build the minimum code that answers the spike's question (with the observability layer from step c if applicable). Every line must serve the question — nothing incidental.
|
||||
Multiple files per spike are expected for complex questions (e.g., `test-basic.js`, `test-edge-cases.js`, `benchmark.js`).
|
||||
|
||||
**e.** Write `README.md` with YAML frontmatter:
|
||||
|
||||
@@ -227,36 +291,38 @@ tags: [tag1, tag2]
|
||||
# Spike NNN: Descriptive Name
|
||||
|
||||
## What This Validates
|
||||
[The specific feasibility question, framed as Given/When/Then]
|
||||
[Given/When/Then]
|
||||
|
||||
## Research
|
||||
[Docs checked, key findings, chosen approach and why, gotchas discovered. Omit if no external dependencies.]
|
||||
[Docs checked, approach comparison table, chosen approach, gotchas. Omit if no external deps.]
|
||||
|
||||
## How to Run
|
||||
[Single command or short sequence to run the spike]
|
||||
[Command(s)]
|
||||
|
||||
## What to Expect
|
||||
[Concrete observable outcomes: "When you click X, you should see Y within Z seconds"]
|
||||
[Concrete observable outcomes]
|
||||
|
||||
## Observability
|
||||
[If this spike has a forensic log layer: describe what's captured, how to export the log, and how to analyze it. Omit for spikes without runtime observability.]
|
||||
[If forensic log layer exists. Omit otherwise.]
|
||||
|
||||
## Investigation Trail
|
||||
[Updated as spike progresses. Document each iteration: what tried, what revealed, what tried next.]
|
||||
|
||||
## Results
|
||||
[Filled in after running — verdict, evidence, surprises. If a forensic log was exported, include key findings from the log analysis here.]
|
||||
[Verdict, evidence, surprises, log analysis findings.]
|
||||
```
|
||||
|
||||
**f.** Auto-link related spikes: read existing spike READMEs and infer relationships from tags, names, and descriptions. Write the `related` field silently.
|
||||
**f.** Auto-link related spikes silently.
|
||||
|
||||
**g.** Run and verify:
|
||||
- If self-verifiable: run it, check output, update README verdict and Results section
|
||||
- If needs human judgment: run it, present instructions using a checkpoint box:
|
||||
- Self-verifiable: run, iterate if findings warrant deeper investigation, update verdict
|
||||
- Needs human judgment: present checkpoint box:
|
||||
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ CHECKPOINT: Verification Required ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
|
||||
**Spike {NNN}: {name}**
|
||||
|
||||
**How to run:** {command}
|
||||
**What to expect:** {concrete outcomes}
|
||||
|
||||
@@ -264,45 +330,69 @@ tags: [tag1, tag2]
|
||||
→ Does this match what you expected? Describe what you see.
|
||||
──────────────────────────────────────────────────────────────
|
||||
|
||||
- If the spike has a forensic log layer: after verification, export the log and include key findings in the Results section. If something went wrong, ask the user to export the log and provide it for diagnosis.
|
||||
**h.** Update `.planning/spikes/MANIFEST.md` with the spike's row.
|
||||
|
||||
**h.** Update verdict to VALIDATED / INVALIDATED / PARTIAL (or WINNER for comparison spike winners). Update Results section with evidence.
|
||||
|
||||
**i.** Update `.planning/spikes/MANIFEST.md` with the spike's row.
|
||||
|
||||
**j.** Commit (if `COMMIT_DOCS` is true):
|
||||
**i.** Commit (if `COMMIT_DOCS` is true):
|
||||
```bash
|
||||
gsd-sdk query commit "docs(spike-NNN): [VERDICT] — [key finding in one sentence]" .planning/spikes/NNN-descriptive-name/ .planning/spikes/MANIFEST.md
|
||||
gsd-sdk query commit "docs(spike-NNN): [VERDICT] — [key finding]" .planning/spikes/NNN-descriptive-name/ .planning/spikes/MANIFEST.md
|
||||
```
|
||||
|
||||
**k.** Report before moving to next spike:
|
||||
**j.** Report:
|
||||
```
|
||||
◆ Spike NNN: {name}
|
||||
Verdict: {VALIDATED ✓ / INVALIDATED ✗ / PARTIAL ⚠}
|
||||
Finding: {one sentence}
|
||||
Impact: {effect on remaining spikes, if any}
|
||||
Key findings: {not just verdict — investigation trail, surprises, edge cases explored}
|
||||
Impact: {effect on remaining spikes}
|
||||
```
|
||||
|
||||
**l.** If a spike invalidates a core assumption: stop and present:
|
||||
Do not rush to a verdict. A spike that says "VALIDATED — it works" with no nuance is almost always incomplete.
|
||||
|
||||
**k.** If core assumption invalidated:
|
||||
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ CHECKPOINT: Decision Required ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
|
||||
Core assumption invalidated by Spike {NNN}.
|
||||
|
||||
{what was invalidated and why}
|
||||
|
||||
──────────────────────────────────────────────────────────────
|
||||
→ Continue with remaining spikes / Pivot approach / Abandon
|
||||
──────────────────────────────────────────────────────────────
|
||||
</step>
|
||||
|
||||
Only proceed if the user says to.
|
||||
<step name="update_conventions">
|
||||
## Update Conventions
|
||||
|
||||
After all spikes in this session are built, update `.planning/spikes/CONVENTIONS.md` with patterns that emerged or solidified.
|
||||
|
||||
```markdown
|
||||
# Spike Conventions
|
||||
|
||||
Patterns and stack choices established across spike sessions. New spikes follow these unless the question requires otherwise.
|
||||
|
||||
## Stack
|
||||
[What we use for frontend, backend, scripts, and why]
|
||||
|
||||
## Structure
|
||||
[Common file layouts, port assignments, naming patterns]
|
||||
|
||||
## Patterns
|
||||
[Recurring approaches: how we handle auth, how we style, how we serve]
|
||||
|
||||
## Tools & Libraries
|
||||
[Preferred packages with versions that worked, and any to avoid]
|
||||
```
|
||||
|
||||
Only include patterns that repeated across 2+ spikes or were explicitly chosen by the user. If `CONVENTIONS.md` already exists, update sections with new patterns from this session.
|
||||
|
||||
Commit (if `COMMIT_DOCS` is true):
|
||||
```bash
|
||||
gsd-sdk query commit "docs(spikes): update conventions" .planning/spikes/CONVENTIONS.md
|
||||
```
|
||||
</step>
|
||||
|
||||
<step name="report">
|
||||
After all spikes complete, present the consolidated report:
|
||||
|
||||
```
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
GSD ► SPIKE COMPLETE ✓
|
||||
@@ -314,32 +404,31 @@ After all spikes complete, present the consolidated report:
|
||||
|---|------|------|---------|
|
||||
| 001 | {name} | standard | ✓ VALIDATED |
|
||||
| 002a | {name} | comparison | ✓ WINNER |
|
||||
| 002b | {name} | comparison | — |
|
||||
|
||||
## Key Discoveries
|
||||
{surprises, gotchas, things that weren't expected}
|
||||
{surprises, gotchas, investigation trail highlights}
|
||||
|
||||
## Feasibility Assessment
|
||||
{overall, is the idea viable?}
|
||||
{overall viability}
|
||||
|
||||
## Signal for the Build
|
||||
{what the real implementation should use, avoid, or watch out for}
|
||||
{what to use, avoid, watch out for}
|
||||
```
|
||||
|
||||
───────────────────────────────────────────────────────────────
|
||||
|
||||
## ▶ Next Up
|
||||
|
||||
**Package findings** — wrap spike knowledge into a reusable skill
|
||||
**Package findings** — wrap spike knowledge into an implementation blueprint
|
||||
|
||||
`/gsd-spike-wrap-up`
|
||||
|
||||
───────────────────────────────────────────────────────────────
|
||||
|
||||
**Also available:**
|
||||
- `/gsd-spike` — spike more ideas (or run with no argument for frontier mode)
|
||||
- `/gsd-plan-phase` — start planning the real implementation
|
||||
- `/gsd-explore` — continue exploring the idea
|
||||
- `/gsd-add-phase` — add a phase to the roadmap based on findings
|
||||
|
||||
───────────────────────────────────────────────────────────────
|
||||
</step>
|
||||
@@ -348,15 +437,16 @@ After all spikes complete, present the consolidated report:
|
||||
|
||||
<success_criteria>
|
||||
- [ ] `.planning/spikes/` created (auto-creates if needed, no project init required)
|
||||
- [ ] Prior spikes checked — already-validated questions skipped, prior findings factored in
|
||||
- [ ] Research grounded each spike in current docs before coding (unless pure logic/no deps)
|
||||
- [ ] Prior spikes and findings skills consulted before building
|
||||
- [ ] Conventions followed (or deviation documented)
|
||||
- [ ] Research grounded each spike in current docs before coding
|
||||
- [ ] Depth over speed — edge cases tested, surprising findings followed, investigation trail documented
|
||||
- [ ] Comparison spikes built back-to-back with head-to-head verdict
|
||||
- [ ] Spikes needing human interaction have forensic log layer (event capture, export, analysis)
|
||||
- [ ] Each spike answers one specific question with observable evidence
|
||||
- [ ] Each spike README has complete frontmatter (including type), run instructions, and results
|
||||
- [ ] User verified each spike (self-verified or human checkpoint)
|
||||
- [ ] MANIFEST.md is current (with Type column)
|
||||
- [ ] Spikes needing human interaction have forensic log layer
|
||||
- [ ] Requirements tracked in MANIFEST.md as they emerge from user choices
|
||||
- [ ] CONVENTIONS.md created or updated with patterns that emerged
|
||||
- [ ] Each spike README has complete frontmatter, Investigation Trail, and Results
|
||||
- [ ] MANIFEST.md is current (with Type column and Requirements section)
|
||||
- [ ] Commits use `docs(spike-NNN): [VERDICT]` format
|
||||
- [ ] Consolidated report presented with next-step routing
|
||||
- [ ] If core assumption invalidated, execution stopped and user consulted
|
||||
</success_criteria>
|
||||
|
||||
@@ -21,8 +21,8 @@ Valid GSD subagent types (use exact names — do not fall back to 'general-purpo
|
||||
```bash
|
||||
INIT=$(gsd-sdk query init.plan-phase "$PHASE")
|
||||
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
AGENT_SKILLS_UI=$(gsd-sdk query agent-skills gsd-ui-researcher 2>/dev/null)
|
||||
AGENT_SKILLS_UI_CHECKER=$(gsd-sdk query agent-skills gsd-ui-checker 2>/dev/null)
|
||||
AGENT_SKILLS_UI=$(gsd-sdk query agent-skills gsd-ui-researcher)
|
||||
AGENT_SKILLS_UI_CHECKER=$(gsd-sdk query agent-skills gsd-ui-checker)
|
||||
```
|
||||
|
||||
Parse JSON for: `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`, `has_context`, `has_research`, `commit_docs`.
|
||||
|
||||
@@ -18,7 +18,7 @@ Valid GSD subagent types (use exact names — do not fall back to 'general-purpo
|
||||
```bash
|
||||
INIT=$(gsd-sdk query init.phase-op "${PHASE_ARG}")
|
||||
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
AGENT_SKILLS_UI_REVIEWER=$(gsd-sdk query agent-skills gsd-ui-reviewer 2>/dev/null)
|
||||
AGENT_SKILLS_UI_REVIEWER=$(gsd-sdk query agent-skills gsd-ui-auditor)
|
||||
```
|
||||
|
||||
Parse: `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`, `commit_docs`.
|
||||
|
||||
@@ -18,7 +18,7 @@ Valid GSD subagent types (use exact names — do not fall back to 'general-purpo
|
||||
```bash
|
||||
INIT=$(gsd-sdk query init.phase-op "${PHASE_ARG}")
|
||||
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
AGENT_SKILLS_AUDITOR=$(gsd-sdk query agent-skills gsd-nyquist-auditor 2>/dev/null)
|
||||
AGENT_SKILLS_AUDITOR=$(gsd-sdk query agent-skills gsd-nyquist-auditor)
|
||||
```
|
||||
|
||||
Parse: `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`.
|
||||
|
||||
@@ -183,6 +183,57 @@ grep -E "Phase ${PHASE_NUM}" .planning/REQUIREMENTS.md 2>/dev/null || true
|
||||
For each requirement: parse description → identify supporting truths/artifacts → status: ✓ SATISFIED / ✗ BLOCKED / ? NEEDS HUMAN.
|
||||
</step>
|
||||
|
||||
<step name="verify_decisions">
|
||||
**Decision coverage validation gate (issue #2492).**
|
||||
|
||||
After requirements coverage, also check that each trackable CONTEXT.md
|
||||
`<decisions>` entry shows up somewhere in the shipped artifacts (plans,
|
||||
SUMMARY.md, files modified by the phase, or recent commit subjects on the
|
||||
phase branch).
|
||||
|
||||
This gate is **non-blocking / warning only** by deliberate asymmetry with
|
||||
the plan-phase translation gate. The plan-phase gate already blocked at
|
||||
translation time, so by the time verification runs every decision has
|
||||
either been translated or explicitly deferred. This gate's job is to
|
||||
surface decisions that *were* translated but vanished during execution —
|
||||
that's a soft signal because "honors a decision" is a fuzzy substring
|
||||
heuristic, and we don't want a paraphrase miss to fail an otherwise good
|
||||
phase.
|
||||
|
||||
**Skip if** `workflow.context_coverage_gate` is explicitly set to `false`
|
||||
(absent key = enabled). Also skip cleanly when CONTEXT.md is missing or has
|
||||
no `<decisions>` block.
|
||||
|
||||
```bash
|
||||
GATE_CFG=$(gsd-sdk query config-get workflow.context_coverage_gate 2>/dev/null || echo "true")
|
||||
if [ "$GATE_CFG" != "false" ]; then
|
||||
# Discover the phase CONTEXT.md via glob expansion rather than `ls | head`
|
||||
# (review F17 / ShellCheck SC2012). Globs preserve filenames containing
|
||||
# spaces and avoid an extra subprocess.
|
||||
CONTEXT_PATH=""
|
||||
for f in "${PHASE_DIR}"/*-CONTEXT.md; do
|
||||
[ -e "$f" ] && CONTEXT_PATH="$f" && break
|
||||
done
|
||||
DECISION_RESULT=$(gsd-sdk query check.decision-coverage-verify "${PHASE_DIR}" "${CONTEXT_PATH}")
|
||||
fi
|
||||
```
|
||||
|
||||
The handler returns JSON `{ skipped, blocking: false, total, honored,
|
||||
not_honored: [...], message }`.
|
||||
|
||||
**Reporting:** Append the handler's `message` (a `### Decision Coverage`
|
||||
section) to VERIFICATION.md regardless of outcome — even when all
|
||||
decisions are honored, recording the count helps reviewers spot drift over
|
||||
time. Set `decision_coverage` in the verification result to
|
||||
`{honored, total, not_honored: [...]}` so downstream tooling can read it.
|
||||
|
||||
**Status impact:** none. The decision gate does NOT influence the
|
||||
`gaps_found` / `human_needed` / `passed` decision tree in
|
||||
`determine_status`. Its findings are warnings the user reviews and may act
|
||||
on by re-opening the phase or by acknowledging the decision was abandoned
|
||||
intentionally.
|
||||
</step>
|
||||
|
||||
<step name="behavioral_verification">
|
||||
**Run the project's test suite and CLI commands to verify behavior, not just structure.**
|
||||
|
||||
@@ -374,9 +425,34 @@ If a requirement specifies a quantity of test cases (e.g., "30 calculations"), c
|
||||
</step>
|
||||
|
||||
<step name="identify_human_verification">
|
||||
**Always needs human:** Visual appearance, user flow completion, real-time behavior (WebSocket/SSE), external service integration, performance feel, error message clarity.
|
||||
**First: determine if this is an infrastructure/foundation phase.**
|
||||
|
||||
**Needs human if uncertain:** Complex wiring grep can't trace, dynamic state-dependent behavior, edge cases.
|
||||
Infrastructure and foundation phases — code foundations, database schema, internal APIs, data models, build tooling, CI/CD, internal service integrations — have no user-facing elements by definition. For these phases:
|
||||
|
||||
- Do NOT invent artificial manual steps (e.g., "manually run git commits", "manually invoke methods", "manually check database state").
|
||||
- Mark human verification as **N/A** with rationale: "Infrastructure/foundation phase — no user-facing elements to test manually."
|
||||
- Set `human_verification: []` and do **not** produce a `human_needed` status solely due to lack of user-facing features.
|
||||
- Only add human verification items if the phase goal or success criteria explicitly describe something a user would interact with (UI, CLI command output visible to end users, external service UX).
|
||||
|
||||
**How to determine if a phase is infrastructure/foundation:**
|
||||
- Phase goal or name contains: "foundation", "infrastructure", "schema", "database", "internal API", "data model", "scaffolding", "pipeline", "tooling", "CI", "migrations", "service layer", "backend", "core library"
|
||||
- Phase success criteria describe only technical artifacts (files exist, tests pass, schema is valid) with no user interaction required
|
||||
- There is no UI, CLI output visible to end users, or real-time behavior to observe
|
||||
|
||||
**If the phase IS infrastructure/foundation:** auto-pass UAT — skip the human verification items list entirely. Log:
|
||||
|
||||
```markdown
|
||||
## Human Verification
|
||||
|
||||
N/A — Infrastructure/foundation phase with no user-facing elements.
|
||||
All acceptance criteria are verifiable programmatically.
|
||||
```
|
||||
|
||||
**If the phase IS user-facing:** Only flag items that genuinely require a human. Do not invent steps.
|
||||
|
||||
**Always needs human (user-facing phases only):** Visual appearance, user flow completion, real-time behavior (WebSocket/SSE), external service integration, performance feel, error message clarity.
|
||||
|
||||
**Needs human if uncertain (user-facing phases only):** Complex wiring grep can't trace, dynamic state-dependent behavior, edge cases.
|
||||
|
||||
Format each as: Test Name → What to do → Expected result → Why can't verify programmatically.
|
||||
</step>
|
||||
@@ -454,6 +530,7 @@ Orchestrator routes: `passed` → update_roadmap | `gaps_found` → create/execu
|
||||
- [ ] All artifacts checked at all three levels
|
||||
- [ ] All key links verified
|
||||
- [ ] Requirements coverage assessed (if applicable)
|
||||
- [ ] CONTEXT.md decisions checked against shipped artifacts (#2492 — non-blocking)
|
||||
- [ ] Anti-patterns scanned and categorized
|
||||
- [ ] Test quality audited (disabled tests, circular patterns, assertion strength, provenance)
|
||||
- [ ] Human verification items identified
|
||||
|
||||
@@ -32,8 +32,8 @@ If $ARGUMENTS contains a phase number, load context:
|
||||
```bash
|
||||
INIT=$(gsd-sdk query init.verify-work "${PHASE_ARG}")
|
||||
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
AGENT_SKILLS_PLANNER=$(gsd-sdk query agent-skills gsd-planner 2>/dev/null)
|
||||
AGENT_SKILLS_CHECKER=$(gsd-sdk query agent-skills gsd-checker 2>/dev/null)
|
||||
AGENT_SKILLS_PLANNER=$(gsd-sdk query agent-skills gsd-planner)
|
||||
AGENT_SKILLS_CHECKER=$(gsd-sdk query agent-skills gsd-plan-checker)
|
||||
```
|
||||
|
||||
Parse JSON for: `planner_model`, `checker_model`, `commit_docs`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `has_verification`, `uat_path`.
|
||||
@@ -464,7 +464,7 @@ Run phase artifact scan to surface any open items before marking phase verified:
|
||||
`audit-open` is CJS-only until registered on `gsd-sdk query`:
|
||||
|
||||
```bash
|
||||
gsd-sdk query audit-open --json 2>/dev/null
|
||||
gsd-sdk query audit-open --json
|
||||
```
|
||||
|
||||
Parse the JSON output. For the CURRENT PHASE ONLY, surface:
|
||||
|
||||
@@ -36,8 +36,27 @@ process.stdin.on('end', () => {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Claude Code natively enforces read-before-edit — skip the advisory (#1984, #2344)
|
||||
if (process.env.CLAUDE_SESSION_ID || process.env.CLAUDECODE) {
|
||||
// Claude Code natively enforces read-before-edit — skip the advisory (#1984, #2344, #2520).
|
||||
//
|
||||
// Detection signals, in priority order:
|
||||
// 1. `data.session_id` on the hook's stdin payload — part of Claude
|
||||
// Code's documented PreToolUse hook-input schema, always present.
|
||||
// Reliable across Claude Code versions because it's schema, not env.
|
||||
// 2. `CLAUDE_CODE_ENTRYPOINT` / `CLAUDE_CODE_SSE_PORT` — env vars that
|
||||
// Claude Code does propagate to hook subprocesses (verified on
|
||||
// Claude Code CLI 2.1.116).
|
||||
// 3. `CLAUDE_SESSION_ID` / `CLAUDECODE` — kept for back-compat and in
|
||||
// case future Claude Code versions propagate them to hook
|
||||
// subprocesses. On 2.1.116 they reach Bash tool subprocesses but
|
||||
// not hook subprocesses, which is why checking them alone is
|
||||
// insufficient (regression of #2344 fixed here as #2520).
|
||||
const isClaudeCode =
|
||||
(typeof data.session_id === 'string' && data.session_id.length > 0) ||
|
||||
process.env.CLAUDE_CODE_ENTRYPOINT ||
|
||||
process.env.CLAUDE_CODE_SSE_PORT ||
|
||||
process.env.CLAUDE_SESSION_ID ||
|
||||
process.env.CLAUDECODE;
|
||||
if (isClaudeCode) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,92 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
// --- Config + last-command readers ------------------------------------------
|
||||
|
||||
/**
|
||||
* Walk up from dir looking for .planning/config.json and return its parsed contents.
|
||||
* Returns {} if not found or unreadable.
|
||||
*/
|
||||
function readGsdConfig(dir) {
|
||||
const home = os.homedir();
|
||||
let current = dir;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const candidate = path.join(current, '.planning', 'config.json');
|
||||
if (fs.existsSync(candidate)) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(candidate, 'utf8')) || {};
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
const parent = path.dirname(current);
|
||||
if (parent === current || current === home) break;
|
||||
current = parent;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup a dotted key path (e.g. 'statusline.show_last_command') in a config
|
||||
* object that may use either nested or flat keys.
|
||||
*/
|
||||
function getConfigValue(cfg, keyPath) {
|
||||
if (!cfg || typeof cfg !== 'object') return undefined;
|
||||
if (keyPath in cfg) return cfg[keyPath];
|
||||
const parts = keyPath.split('.');
|
||||
let cur = cfg;
|
||||
for (const p of parts) {
|
||||
if (cur == null || typeof cur !== 'object' || !(p in cur)) return undefined;
|
||||
cur = cur[p];
|
||||
}
|
||||
return cur;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the most recently invoked slash command from a Claude Code JSONL
|
||||
* transcript file. Returns the command name (no leading slash) or null.
|
||||
*
|
||||
* Claude Code embeds slash invocations in user messages as
|
||||
* <command-name>/foo</command-name>
|
||||
* We scan lines from the end of the file, stopping at the first match.
|
||||
*/
|
||||
function readLastSlashCommand(transcriptPath) {
|
||||
if (!transcriptPath || typeof transcriptPath !== 'string') return null;
|
||||
let content;
|
||||
try {
|
||||
if (!fs.existsSync(transcriptPath)) return null;
|
||||
// Read only the tail — typical transcripts grow large. 256 KiB comfortably
|
||||
// covers dozens of recent turns while staying cheap per render.
|
||||
const stat = fs.statSync(transcriptPath);
|
||||
const MAX = 256 * 1024;
|
||||
const start = Math.max(0, stat.size - MAX);
|
||||
const fd = fs.openSync(transcriptPath, 'r');
|
||||
try {
|
||||
const buf = Buffer.alloc(stat.size - start);
|
||||
fs.readSync(fd, buf, 0, buf.length, start);
|
||||
content = buf.toString('utf8');
|
||||
} finally {
|
||||
fs.closeSync(fd);
|
||||
}
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
// Find the LAST occurrence — scan right-to-left via lastIndexOf on the tag.
|
||||
const tagClose = '</command-name>';
|
||||
const idx = content.lastIndexOf(tagClose);
|
||||
if (idx < 0) return null;
|
||||
const openTag = '<command-name>';
|
||||
const openIdx = content.lastIndexOf(openTag, idx);
|
||||
if (openIdx < 0) return null;
|
||||
let name = content.slice(openIdx + openTag.length, idx).trim();
|
||||
// Strip a leading slash if present, and any trailing arguments-on-same-line noise.
|
||||
if (name.startsWith('/')) name = name.slice(1);
|
||||
// Command names in Claude Code transcripts are plain identifiers like "gsd-plan-phase"
|
||||
// or namespaced like "plugin:skill". Reject anything with whitespace/newlines/control chars.
|
||||
if (!name || /[\s\\"<>]/.test(name) || name.length > 80) return null;
|
||||
return name;
|
||||
}
|
||||
|
||||
// --- GSD state reader -------------------------------------------------------
|
||||
|
||||
/**
|
||||
@@ -240,6 +326,23 @@ function runStatusline() {
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// Last-slash-command suffix (opt-in via statusline.show_last_command, #2538).
|
||||
// Reads the active session transcript for the most recent <command-name> tag.
|
||||
// Failure here must never break the statusline — wrap the entire lookup.
|
||||
let lastCmdSuffix = '';
|
||||
try {
|
||||
const cfg = readGsdConfig(dir);
|
||||
if (getConfigValue(cfg, 'statusline.show_last_command') === true) {
|
||||
const transcriptPath = data.transcript_path;
|
||||
const lastCmd = readLastSlashCommand(transcriptPath);
|
||||
if (lastCmd) {
|
||||
lastCmdSuffix = ` │ \x1b[2mlast: /${lastCmd}\x1b[0m`;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Never break the statusline on config/transcript errors
|
||||
}
|
||||
|
||||
// Output
|
||||
const dirname = path.basename(dir);
|
||||
const middle = task
|
||||
@@ -249,9 +352,9 @@ function runStatusline() {
|
||||
: null;
|
||||
|
||||
if (middle) {
|
||||
process.stdout.write(`${gsdUpdate}\x1b[2m${model}\x1b[0m │ ${middle} │ \x1b[2m${dirname}\x1b[0m${ctx}`);
|
||||
process.stdout.write(`${gsdUpdate}\x1b[2m${model}\x1b[0m │ ${middle} │ \x1b[2m${dirname}\x1b[0m${ctx}${lastCmdSuffix}`);
|
||||
} else {
|
||||
process.stdout.write(`${gsdUpdate}\x1b[2m${model}\x1b[0m │ \x1b[2m${dirname}\x1b[0m${ctx}`);
|
||||
process.stdout.write(`${gsdUpdate}\x1b[2m${model}\x1b[0m │ \x1b[2m${dirname}\x1b[0m${ctx}${lastCmdSuffix}`);
|
||||
}
|
||||
} catch (e) {
|
||||
// Silent fail - don't break statusline on parse errors
|
||||
@@ -260,6 +363,39 @@ function runStatusline() {
|
||||
}
|
||||
|
||||
// Export helpers for unit tests. Harmless when run as a script.
|
||||
module.exports = { readGsdState, parseStateMd, formatGsdState };
|
||||
module.exports = {
|
||||
readGsdState, parseStateMd, formatGsdState,
|
||||
readGsdConfig, getConfigValue, readLastSlashCommand,
|
||||
};
|
||||
|
||||
/**
|
||||
* Render the statusline from an already-parsed hook input object. Exported for
|
||||
* testing without feeding stdin. Returns the rendered string.
|
||||
*/
|
||||
function renderStatusline(data) {
|
||||
const model = data.model?.display_name || 'Claude';
|
||||
const dir = data.workspace?.current_dir || process.cwd();
|
||||
const dirname = path.basename(dir);
|
||||
|
||||
let lastCmdSuffix = '';
|
||||
try {
|
||||
const cfg = readGsdConfig(dir);
|
||||
if (getConfigValue(cfg, 'statusline.show_last_command') === true) {
|
||||
const lastCmd = readLastSlashCommand(data.transcript_path);
|
||||
if (lastCmd) {
|
||||
lastCmdSuffix = ` │ \x1b[2mlast: /${lastCmd}\x1b[0m`;
|
||||
}
|
||||
}
|
||||
} catch (e) { /* swallow */ }
|
||||
|
||||
const gsdStateStr = formatGsdState(readGsdState(dir) || {});
|
||||
const middle = gsdStateStr ? `\x1b[2m${gsdStateStr}\x1b[0m` : null;
|
||||
if (middle) {
|
||||
return `\x1b[2m${model}\x1b[0m │ ${middle} │ \x1b[2m${dirname}\x1b[0m${lastCmdSuffix}`;
|
||||
}
|
||||
return `\x1b[2m${model}\x1b[0m │ \x1b[2m${dirname}\x1b[0m${lastCmdSuffix}`;
|
||||
}
|
||||
|
||||
module.exports.renderStatusline = renderStatusline;
|
||||
|
||||
if (require.main === module) runStatusline();
|
||||
|
||||
3308
package-lock.json
generated
3308
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"name": "get-shit-done-cc",
|
||||
"version": "1.37.1",
|
||||
"version": "1.38.2",
|
||||
"description": "A meta-prompting, context engineering and spec-driven development system for Claude Code, OpenCode, Gemini and Codex by TÂCHES.",
|
||||
"bin": {
|
||||
"get-shit-done-cc": "bin/install.js"
|
||||
"get-shit-done-cc": "bin/install.js",
|
||||
"gsd-sdk": "bin/gsd-sdk.js"
|
||||
},
|
||||
"files": [
|
||||
"bin",
|
||||
@@ -14,6 +15,7 @@
|
||||
"scripts",
|
||||
"sdk/src",
|
||||
"sdk/prompts",
|
||||
"sdk/dist",
|
||||
"sdk/package.json",
|
||||
"sdk/package-lock.json",
|
||||
"sdk/tsconfig.json"
|
||||
@@ -43,14 +45,19 @@
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.84",
|
||||
"ws": "^8.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"c8": "^11.0.0",
|
||||
"esbuild": "^0.24.0",
|
||||
"vitest": "^4.1.2"
|
||||
"c8": "^11.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build:hooks": "node scripts/build-hooks.js",
|
||||
"prepublishOnly": "npm run build:hooks",
|
||||
"build:sdk": "cd sdk && npm ci && npm run build",
|
||||
"prepublishOnly": "npm run build:hooks && npm run build:sdk",
|
||||
"pretest": "npm run build:sdk",
|
||||
"pretest:coverage": "npm run build:sdk",
|
||||
"test": "node scripts/run-tests.cjs",
|
||||
"test:coverage": "c8 --check-coverage --lines 70 --reporter text --include 'get-shit-done/bin/lib/*.cjs' --exclude 'tests/**' --all node scripts/run-tests.cjs"
|
||||
}
|
||||
|
||||
52
scripts/fix-slash-commands.cjs
Normal file
52
scripts/fix-slash-commands.cjs
Normal file
@@ -0,0 +1,52 @@
|
||||
'use strict';
|
||||
/**
|
||||
* One-shot script: replace /gsd-<cmd> with /gsd:<cmd> for known command names.
|
||||
* Only replaces when followed by a word boundary (space, newline, quote, backtick, ), end).
|
||||
*/
|
||||
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'gsd');
|
||||
const cmdNames = fs.readdirSync(COMMANDS_DIR)
|
||||
.filter(f => f.endsWith('.md'))
|
||||
.map(f => f.replace(/\.md$/, ''))
|
||||
.sort((a, b) => b.length - a.length); // longest first to avoid partial matches
|
||||
|
||||
// Build regex: /gsd-(cmd1|cmd2|...) followed by non-word-char or end
|
||||
const pattern = new RegExp(`/gsd-(${cmdNames.join('|')})(?=[^a-zA-Z0-9_-]|$)`, 'g');
|
||||
|
||||
const SEARCH_DIRS = [
|
||||
path.join(__dirname, '..', 'get-shit-done', 'bin', 'lib'),
|
||||
path.join(__dirname, '..', 'get-shit-done', 'workflows'),
|
||||
path.join(__dirname, '..', 'get-shit-done', 'references'),
|
||||
path.join(__dirname, '..', 'get-shit-done', 'templates'),
|
||||
path.join(__dirname, '..', 'get-shit-done', 'contexts'),
|
||||
path.join(__dirname, '..', 'commands', 'gsd'),
|
||||
];
|
||||
const EXTENSIONS = new Set(['.md', '.cjs', '.js']);
|
||||
|
||||
function processDir(dir) {
|
||||
let entries;
|
||||
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
||||
for (const e of entries) {
|
||||
const full = path.join(dir, e.name);
|
||||
if (e.isDirectory()) {
|
||||
processDir(full);
|
||||
} else if (EXTENSIONS.has(path.extname(e.name))) {
|
||||
const src = fs.readFileSync(full, 'utf-8');
|
||||
const replaced = src.replace(pattern, (_, cmd) => `/gsd:${cmd}`);
|
||||
if (replaced !== src) {
|
||||
fs.writeFileSync(full, replaced, 'utf-8');
|
||||
const count = (src.match(pattern) || []).length;
|
||||
console.log(` ${count} replacements: ${path.relative(path.join(__dirname, '..'), full)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let totalFiles = 0;
|
||||
for (const dir of SEARCH_DIRS) {
|
||||
processDir(dir);
|
||||
}
|
||||
console.log('Done.');
|
||||
69
scripts/verify-tarball-sdk-dist.sh
Executable file
69
scripts/verify-tarball-sdk-dist.sh
Executable file
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env bash
|
||||
# Verify the published get-shit-done-cc tarball actually contains
|
||||
# sdk/dist/cli.js and that the `query` subcommand is exposed.
|
||||
#
|
||||
# Guards regression of bug #2647: v1.38.3 shipped without sdk/dist/
|
||||
# because the outer `files` whitelist and `prepublishOnly` chain
|
||||
# drifted out of alignment. Any future drift fails release CI here.
|
||||
#
|
||||
# Run AFTER `npm run build:sdk` (so sdk/dist exists on disk) and
|
||||
# before `npm publish`. Exits non-zero on any mismatch.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
echo "==> Packing tarball (ignore-scripts: sdk/dist must already exist)"
|
||||
TARBALL=$(npm pack --ignore-scripts 2>/dev/null | tail -1)
|
||||
if [ -z "$TARBALL" ] || [ ! -f "$TARBALL" ]; then
|
||||
echo "::error::npm pack produced no tarball"
|
||||
exit 1
|
||||
fi
|
||||
echo " tarball: $TARBALL"
|
||||
|
||||
EXTRACT_DIR=$(mktemp -d)
|
||||
trap 'rm -rf "$EXTRACT_DIR" "$TARBALL"' EXIT
|
||||
|
||||
echo "==> Extracting tarball into $EXTRACT_DIR"
|
||||
tar -xzf "$TARBALL" -C "$EXTRACT_DIR"
|
||||
|
||||
CLI_JS="$EXTRACT_DIR/package/sdk/dist/cli.js"
|
||||
if [ ! -f "$CLI_JS" ]; then
|
||||
echo "::error::$CLI_JS is missing from the published tarball"
|
||||
echo "Tarball contents under sdk/:"
|
||||
find "$EXTRACT_DIR/package/sdk" -maxdepth 2 -print | head -40
|
||||
exit 1
|
||||
fi
|
||||
echo " OK: sdk/dist/cli.js present ($(wc -c < "$CLI_JS") bytes)"
|
||||
|
||||
echo "==> Installing runtime deps inside the extracted package and invoking gsd-sdk query --help"
|
||||
pushd "$EXTRACT_DIR/package" >/dev/null
|
||||
# Install only production deps so the extracted tarball resolves
|
||||
# @anthropic-ai/claude-agent-sdk / ws the same way a real user install would.
|
||||
npm install --omit=dev --no-audit --no-fund --silent
|
||||
OUTPUT=$(node sdk/dist/cli.js query --help 2>&1 || true)
|
||||
popd >/dev/null
|
||||
|
||||
echo "$OUTPUT" | head -20
|
||||
if ! echo "$OUTPUT" | grep -qi 'query'; then
|
||||
echo "::error::sdk/dist/cli.js did not expose a 'query' subcommand"
|
||||
exit 1
|
||||
fi
|
||||
if echo "$OUTPUT" | grep -qiE 'unknown command|unrecognized'; then
|
||||
echo "::error::sdk/dist/cli.js rejected 'query' as unknown"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> Also verifying gsd-sdk bin shim resolves ../sdk/dist/cli.js"
|
||||
SHIM="$EXTRACT_DIR/package/bin/gsd-sdk.js"
|
||||
if [ ! -f "$SHIM" ]; then
|
||||
echo "::error::bin/gsd-sdk.js missing from tarball"
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -qE "sdk.*dist.*cli\.js" "$SHIM"; then
|
||||
echo "::error::bin/gsd-sdk.js does not reference sdk/dist/cli.js"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> Tarball verification passed"
|
||||
@@ -34,7 +34,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"prepublishOnly": "npm run build",
|
||||
"prepublishOnly": "rm -rf dist && tsc && chmod +x dist/cli.js",
|
||||
"test": "vitest run",
|
||||
"test:unit": "vitest run --project unit",
|
||||
"test:integration": "vitest run --project integration"
|
||||
|
||||
@@ -73,6 +73,14 @@ After plans pass the checker (or checker is skipped), verify all phase requireme
|
||||
3. If gaps found: log as warning, continue (headless mode does not block for coverage gaps)
|
||||
</step>
|
||||
|
||||
<step name="post_planning_gaps">
|
||||
Unified post-planning gap report (#2493). Gated on `workflow.post_planning_gaps`
|
||||
(default true). When enabled, scan REQUIREMENTS.md and CONTEXT.md `<decisions>`
|
||||
against all generated PLAN.md files, then emit one `Source | Item | Status` table.
|
||||
Skip-gracefully on missing sources. Non-blocking — headless mode reports gaps
|
||||
via the event stream and continues.
|
||||
</step>
|
||||
|
||||
</process>
|
||||
|
||||
<success_criteria>
|
||||
|
||||
@@ -341,6 +341,21 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<void
|
||||
return;
|
||||
}
|
||||
|
||||
// Multi-repo project-root resolution (issue #2623).
|
||||
//
|
||||
// When the user launches `gsd-sdk` from inside a `sub_repos`-listed child repo,
|
||||
// `projectDir` defaults to `process.cwd()` which points at the child, not the
|
||||
// parent workspace that owns `.planning/`. Mirror the legacy `gsd-tools.cjs`
|
||||
// walk-up semantics so handlers see the correct project root.
|
||||
//
|
||||
// Idempotent: if `projectDir` already has its own `.planning/` (including an
|
||||
// explicit `--project-dir` pointing at the workspace root), findProjectRoot
|
||||
// returns it unchanged.
|
||||
{
|
||||
const { findProjectRoot } = await import('./query/helpers.js');
|
||||
args = { ...args, projectDir: findProjectRoot(args.projectDir) };
|
||||
}
|
||||
|
||||
// ─── Query command ──────────────────────────────────────────────────────
|
||||
if (args.command === 'query') {
|
||||
const { createRegistry } = await import('./query/index.js');
|
||||
@@ -408,7 +423,7 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<void
|
||||
}
|
||||
console.log(JSON.stringify(output, null, 2));
|
||||
} else {
|
||||
const result = await registry.dispatch(matched.cmd, matched.args, args.projectDir);
|
||||
const result = await registry.dispatch(matched.cmd, matched.args, args.projectDir, args.ws);
|
||||
let output: unknown = result.data;
|
||||
|
||||
if (pickField) {
|
||||
|
||||
@@ -6,16 +6,37 @@ import { tmpdir } from 'node:os';
|
||||
|
||||
describe('loadConfig', () => {
|
||||
let tmpDir: string;
|
||||
let fakeHome: string;
|
||||
let prevHome: string | undefined;
|
||||
let prevGsdHome: string | undefined;
|
||||
|
||||
beforeEach(async () => {
|
||||
tmpDir = join(tmpdir(), `gsd-config-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
||||
await mkdir(join(tmpDir, '.planning'), { recursive: true });
|
||||
// Isolate ~/.gsd/defaults.json by pointing HOME at an empty tmp dir.
|
||||
fakeHome = join(tmpdir(), `gsd-home-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
||||
await mkdir(fakeHome, { recursive: true });
|
||||
prevHome = process.env.HOME;
|
||||
process.env.HOME = fakeHome;
|
||||
// Also isolate GSD_HOME (loadUserDefaults prefers it over HOME).
|
||||
prevGsdHome = process.env.GSD_HOME;
|
||||
delete process.env.GSD_HOME;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(tmpDir, { recursive: true, force: true });
|
||||
await rm(fakeHome, { recursive: true, force: true });
|
||||
if (prevHome === undefined) delete process.env.HOME;
|
||||
else process.env.HOME = prevHome;
|
||||
if (prevGsdHome === undefined) delete process.env.GSD_HOME;
|
||||
else process.env.GSD_HOME = prevGsdHome;
|
||||
});
|
||||
|
||||
async function writeUserDefaults(defaults: unknown) {
|
||||
await mkdir(join(fakeHome, '.gsd'), { recursive: true });
|
||||
await writeFile(join(fakeHome, '.gsd', 'defaults.json'), JSON.stringify(defaults));
|
||||
}
|
||||
|
||||
it('returns all defaults when config file is missing', async () => {
|
||||
// No config.json created
|
||||
await rm(join(tmpDir, '.planning', 'config.json'), { force: true });
|
||||
@@ -154,6 +175,69 @@ describe('loadConfig', () => {
|
||||
expect(config.parallelization).toBe(0);
|
||||
});
|
||||
|
||||
// ─── User-level defaults (~/.gsd/defaults.json) ─────────────────────────
|
||||
// Regression: issue #2652 — SDK loadConfig ignored user-level defaults
|
||||
// for pre-project Codex installs, so init.quick still emitted Claude
|
||||
// model aliases from MODEL_PROFILES via resolveModel even when the user
|
||||
// had `resolve_model_ids: "omit"` in ~/.gsd/defaults.json.
|
||||
//
|
||||
// Mirrors CJS behavior in get-shit-done/bin/lib/core.cjs:421 (#1683):
|
||||
// user-level defaults only apply when no project .planning/config.json
|
||||
// exists (pre-project context). Once a project is initialized, its
|
||||
// config.json is authoritative — buildNewProjectConfig baked the user
|
||||
// defaults in at /gsd:new-project time.
|
||||
|
||||
it('pre-project: layers user defaults from ~/.gsd/defaults.json', async () => {
|
||||
await writeUserDefaults({ resolve_model_ids: 'omit' });
|
||||
// No project config.json
|
||||
const config = await loadConfig(tmpDir);
|
||||
expect((config as Record<string, unknown>).resolve_model_ids).toBe('omit');
|
||||
// Built-in defaults still present for keys user did not override
|
||||
expect(config.model_profile).toBe('balanced');
|
||||
expect(config.workflow.plan_check).toBe(true);
|
||||
});
|
||||
|
||||
it('pre-project: deep-merges nested keys from user defaults', async () => {
|
||||
await writeUserDefaults({
|
||||
git: { branching_strategy: 'milestone' },
|
||||
agent_skills: { planner: 'user-skill' },
|
||||
});
|
||||
|
||||
const config = await loadConfig(tmpDir);
|
||||
expect(config.git.branching_strategy).toBe('milestone');
|
||||
expect(config.git.phase_branch_template).toBe('gsd/phase-{phase}-{slug}');
|
||||
expect(config.agent_skills).toEqual({ planner: 'user-skill' });
|
||||
});
|
||||
|
||||
it('project config is authoritative over user defaults (CJS parity)', async () => {
|
||||
// User defaults set resolve_model_ids: "omit", but project config omits it.
|
||||
// Per CJS core.cjs loadConfig (#1683): once .planning/config.json exists,
|
||||
// ~/.gsd/defaults.json is ignored — buildNewProjectConfig already baked
|
||||
// the user defaults in at project creation time.
|
||||
await writeUserDefaults({
|
||||
resolve_model_ids: 'omit',
|
||||
model_profile: 'fast',
|
||||
});
|
||||
await writeFile(
|
||||
join(tmpDir, '.planning', 'config.json'),
|
||||
JSON.stringify({ model_profile: 'quality' }),
|
||||
);
|
||||
|
||||
const config = await loadConfig(tmpDir);
|
||||
expect(config.model_profile).toBe('quality');
|
||||
// User-defaults not layered when project config present
|
||||
expect((config as Record<string, unknown>).resolve_model_ids).toBeUndefined();
|
||||
});
|
||||
|
||||
it('ignores malformed ~/.gsd/defaults.json', async () => {
|
||||
await mkdir(join(fakeHome, '.gsd'), { recursive: true });
|
||||
await writeFile(join(fakeHome, '.gsd', 'defaults.json'), '{not json');
|
||||
|
||||
const config = await loadConfig(tmpDir);
|
||||
// Falls back to built-in defaults
|
||||
expect(config).toEqual(CONFIG_DEFAULTS);
|
||||
});
|
||||
|
||||
it('does not mutate CONFIG_DEFAULTS between calls', async () => {
|
||||
const before = structuredClone(CONFIG_DEFAULTS);
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { relPlanningPath } from './workstream-utils.js';
|
||||
|
||||
@@ -38,6 +39,13 @@ export interface WorkflowConfig {
|
||||
max_discuss_passes: number;
|
||||
/** Subagent timeout in ms (matches `get-shit-done/bin/lib/core.cjs` default 300000). */
|
||||
subagent_timeout: number;
|
||||
/**
|
||||
* Issue #2492. When true (default), enforces that every trackable decision in
|
||||
* CONTEXT.md `<decisions>` is referenced by at least one plan (translation
|
||||
* gate, blocking) and reports decisions not honored by shipped artifacts at
|
||||
* verify-phase (validation gate, non-blocking). Set false to disable both.
|
||||
*/
|
||||
context_coverage_gate: boolean;
|
||||
}
|
||||
|
||||
export interface HooksConfig {
|
||||
@@ -98,6 +106,7 @@ export const CONFIG_DEFAULTS: GSDConfig = {
|
||||
skip_discuss: false,
|
||||
max_discuss_passes: 3,
|
||||
subagent_timeout: 300000,
|
||||
context_coverage_gate: true,
|
||||
},
|
||||
hooks: {
|
||||
context_warnings: true,
|
||||
@@ -112,33 +121,76 @@ export const CONFIG_DEFAULTS: GSDConfig = {
|
||||
|
||||
/**
|
||||
* Load project config from `.planning/config.json`, merging with defaults.
|
||||
* Returns full defaults when file is missing or empty.
|
||||
* When project config is missing or empty, layers user defaults
|
||||
* (`~/.gsd/defaults.json`) over built-in defaults.
|
||||
* Throws on malformed JSON with a helpful error message.
|
||||
*/
|
||||
/**
|
||||
* Read user-level defaults from `~/.gsd/defaults.json` (or `$GSD_HOME/.gsd/`
|
||||
* when set). Returns `{}` when the file is missing, empty, or malformed —
|
||||
* matches CJS behavior in `get-shit-done/bin/lib/core.cjs` (#1683, #2652).
|
||||
*/
|
||||
async function loadUserDefaults(): Promise<Record<string, unknown>> {
|
||||
const home = process.env.GSD_HOME || homedir();
|
||||
const defaultsPath = join(home, '.gsd', 'defaults.json');
|
||||
let raw: string;
|
||||
try {
|
||||
raw = await readFile(defaultsPath, 'utf-8');
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
const trimmed = raw.trim();
|
||||
if (trimmed === '') return {};
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
||||
return {};
|
||||
}
|
||||
return parsed as Record<string, unknown>;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadConfig(projectDir: string, workstream?: string): Promise<GSDConfig> {
|
||||
const configPath = join(projectDir, relPlanningPath(workstream), 'config.json');
|
||||
const rootConfigPath = join(projectDir, '.planning', 'config.json');
|
||||
|
||||
let raw: string;
|
||||
let projectConfigFound = false;
|
||||
try {
|
||||
raw = await readFile(configPath, 'utf-8');
|
||||
projectConfigFound = true;
|
||||
} catch {
|
||||
// If workstream config missing, fall back to root config
|
||||
if (workstream) {
|
||||
try {
|
||||
raw = await readFile(rootConfigPath, 'utf-8');
|
||||
projectConfigFound = true;
|
||||
} catch {
|
||||
return structuredClone(CONFIG_DEFAULTS);
|
||||
raw = '';
|
||||
}
|
||||
} else {
|
||||
// File missing — normal for new projects
|
||||
return structuredClone(CONFIG_DEFAULTS);
|
||||
raw = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-project context: no .planning/config.json exists. Layer user-level
|
||||
// defaults from ~/.gsd/defaults.json over built-in defaults. Mirrors the
|
||||
// CJS fall-back branch in get-shit-done/bin/lib/core.cjs:421 (#1683) so
|
||||
// SDK-dispatched init queries (e.g. resolveModel in Codex installs, #2652)
|
||||
// honor user-level knobs like `resolve_model_ids: "omit"`.
|
||||
if (!projectConfigFound) {
|
||||
const userDefaults = await loadUserDefaults();
|
||||
return mergeDefaults(userDefaults);
|
||||
}
|
||||
|
||||
const trimmed = raw.trim();
|
||||
if (trimmed === '') {
|
||||
return structuredClone(CONFIG_DEFAULTS);
|
||||
// Empty project config — treat as no project config (CJS core.cjs
|
||||
// catches JSON.parse on empty and falls through to the pre-project path).
|
||||
const userDefaults = await loadUserDefaults();
|
||||
return mergeDefaults(userDefaults);
|
||||
}
|
||||
|
||||
let parsed: Record<string, unknown>;
|
||||
@@ -153,7 +205,12 @@ export async function loadConfig(projectDir: string, workstream?: string): Promi
|
||||
throw new Error(`Config at ${configPath} must be a JSON object`);
|
||||
}
|
||||
|
||||
// Three-level deep merge: defaults <- parsed
|
||||
// Project config exists — user-level defaults are ignored (CJS parity).
|
||||
// `buildNewProjectConfig` already baked them into config.json at /gsd:new-project.
|
||||
return mergeDefaults(parsed);
|
||||
}
|
||||
|
||||
function mergeDefaults(parsed: Record<string, unknown>): GSDConfig {
|
||||
return {
|
||||
...structuredClone(CONFIG_DEFAULTS),
|
||||
...parsed,
|
||||
|
||||
@@ -62,6 +62,8 @@ No `gsd-tools.cjs` mirror — agents use these instead of shell `ls`/`find`/`gre
|
||||
|
||||
Handlers for `**state.signal-waiting`**, `**state.signal-resume**`, `**state.validate**`, `**state.sync**` (supports `--verify` dry-run), and `**state.prune**` live in `state-mutation.ts`, with dotted and `state …` space aliases in `index.ts`.
|
||||
|
||||
**`state.add-roadmap-evolution`** (bug #2662) — appends one entry to the `### Roadmap Evolution` subsection under `## Accumulated Context` in STATE.md, creating the subsection if missing. argv: `--phase`, `--action` (`inserted|removed|moved|edited|added`), optional `--note`, `--after` (for `inserted`), and `--urgent` flag. Returns `{ added: true, entry }` or `{ added: false, reason: 'duplicate', entry }`. Throws `GSDError(Validation)` when `--phase` / `--action` are missing or action is not in the allowed set. Canonical replacement for raw `Edit`/`Write` on STATE.md in `insert-phase.md` / `add-phase.md` workflows — required when projects ship a `protect-files.sh` PreToolUse hook that blocks direct STATE.md writes.
|
||||
|
||||
**`state.json` vs `state.load` (different CJS commands):**
|
||||
|
||||
- **`state.json`** / `state json` — port of **`cmdStateJson`** (`state.ts` `stateJson`): rebuilt STATE.md frontmatter JSON. Read-only golden: `read-only-parity.integration.test.ts` compares to CJS `state json` with **`last_updated`** stripped.
|
||||
|
||||
@@ -491,8 +491,8 @@ export interface AuditOpenResult {
|
||||
/**
|
||||
* Same structured result as `gsd-tools.cjs audit-open` (JSON).
|
||||
*/
|
||||
export function auditOpenArtifacts(projectDir: string): AuditOpenResult {
|
||||
const planDir = planningPaths(projectDir).planning;
|
||||
export function auditOpenArtifacts(projectDir: string, workstream?: string): AuditOpenResult {
|
||||
const planDir = planningPaths(projectDir, workstream).planning;
|
||||
|
||||
const debugSessions = (() => {
|
||||
try { return scanDebugSessions(planDir); } catch { return [{ scan_error: true }]; }
|
||||
@@ -707,9 +707,9 @@ export function formatAuditReport(auditResult: AuditOpenResult): string {
|
||||
/**
|
||||
* `audit-open` / `audit.open` — optional `--json` for structured JSON only (default adds formatted report string).
|
||||
*/
|
||||
export const auditOpen: QueryHandler = async (args, projectDir) => {
|
||||
export const auditOpen: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const jsonOnly = args.includes('--json');
|
||||
const result = auditOpenArtifacts(projectDir);
|
||||
const result = auditOpenArtifacts(projectDir, workstream);
|
||||
if (jsonOnly) {
|
||||
return { data: result };
|
||||
}
|
||||
|
||||
519
sdk/src/query/check-decision-coverage.test.ts
Normal file
519
sdk/src/query/check-decision-coverage.test.ts
Normal file
@@ -0,0 +1,519 @@
|
||||
/**
|
||||
* Decision-coverage gate tests for issue #2492.
|
||||
*
|
||||
* Two gates, two semantics:
|
||||
*
|
||||
* - `check.decision-coverage-plan` — translation gate, BLOCKING.
|
||||
* Each trackable CONTEXT.md decision must appear (by id or text) in at
|
||||
* least one PLAN.md `must_haves` / `truths` / body.
|
||||
*
|
||||
* - `check.decision-coverage-verify` — validation gate, NON-BLOCKING.
|
||||
* Each trackable decision should appear in shipped artifacts (PLANs,
|
||||
* SUMMARY.md, files_modified, recent commit messages). Missing items
|
||||
* are reported as warnings only.
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { mkdtemp, writeFile, mkdir, rm } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import {
|
||||
checkDecisionCoveragePlan,
|
||||
checkDecisionCoverageVerify,
|
||||
} from './check-decision-coverage.js';
|
||||
|
||||
let tmp: string;
|
||||
let phaseDir: string;
|
||||
let contextPath: string;
|
||||
|
||||
async function setupPhase(decisionsBlock: string, plans: Record<string, string>, summary?: string) {
|
||||
await mkdir(phaseDir, { recursive: true });
|
||||
await writeFile(contextPath, `# Phase 17 Context\n\n${decisionsBlock}\n`, 'utf-8');
|
||||
for (const [name, content] of Object.entries(plans)) {
|
||||
await writeFile(join(phaseDir, name), content, 'utf-8');
|
||||
}
|
||||
if (summary !== undefined) {
|
||||
await writeFile(join(phaseDir, '17-SUMMARY.md'), summary, 'utf-8');
|
||||
}
|
||||
}
|
||||
|
||||
function planFile(mustHavesYaml: string, body = ''): string {
|
||||
return `---
|
||||
phase: 17
|
||||
plan: 1
|
||||
type: implementation
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified: []
|
||||
autonomous: true
|
||||
must_haves:
|
||||
${mustHavesYaml}
|
||||
---
|
||||
${body}
|
||||
`;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
tmp = await mkdtemp(join(tmpdir(), 'gsd-deccov-'));
|
||||
phaseDir = join(tmp, '.planning', 'phases', '17-foo');
|
||||
contextPath = join(phaseDir, '17-CONTEXT.md');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(tmp, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('checkDecisionCoveragePlan — translation gate (#2492)', () => {
|
||||
it('passes when every trackable decision is cited by id in a plan', async () => {
|
||||
await setupPhase(
|
||||
`<decisions>
|
||||
### Cat
|
||||
- **D-01:** Use bit offsets
|
||||
- **D-02:** Display TArray element type
|
||||
</decisions>`,
|
||||
{
|
||||
'17-01-PLAN.md': planFile(
|
||||
` truths:
|
||||
- "D-01: bit offsets are exposed via API"
|
||||
artifacts: []
|
||||
key_links: []`,
|
||||
// D-02 cited under a designated `## tasks` heading (review F4).
|
||||
'## tasks\n- Implements D-02: TArray display logic.\n',
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
const result = await checkDecisionCoveragePlan([phaseDir, contextPath], tmp);
|
||||
expect(result.data.passed).toBe(true);
|
||||
expect(result.data.uncovered).toEqual([]);
|
||||
expect(result.data.total).toBe(2);
|
||||
expect(result.data.covered).toBe(2);
|
||||
});
|
||||
|
||||
it('fails when a decision is not covered by any plan and names it', async () => {
|
||||
await setupPhase(
|
||||
`<decisions>
|
||||
### Cat
|
||||
- **D-01:** Use bit offsets, not byte offsets
|
||||
- **D-99:** A decision nobody bothered to plan
|
||||
</decisions>`,
|
||||
{
|
||||
'17-01-PLAN.md': planFile(
|
||||
` truths:
|
||||
- "D-01: bit offsets are exposed"
|
||||
artifacts: []
|
||||
key_links: []`,
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
const result = await checkDecisionCoveragePlan([phaseDir, contextPath], tmp);
|
||||
expect(result.data.passed).toBe(false);
|
||||
expect(result.data.uncovered.map((u: { id: string }) => u.id)).toEqual(['D-99']);
|
||||
expect(result.data.message).toMatch(/D-99/);
|
||||
});
|
||||
|
||||
it('honors `truths` AND `must_haves` body bullets', async () => {
|
||||
await setupPhase(
|
||||
`<decisions>
|
||||
### Cat
|
||||
- **D-01:** First decision
|
||||
- **D-02:** Second decision
|
||||
</decisions>`,
|
||||
{
|
||||
'17-01-PLAN.md': planFile(
|
||||
` truths:
|
||||
- "D-01 honored"
|
||||
artifacts: []
|
||||
key_links: []`,
|
||||
'## must_haves\n- D-02: also honored in body\n',
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
const result = await checkDecisionCoveragePlan([phaseDir, contextPath], tmp);
|
||||
expect(result.data.passed).toBe(true);
|
||||
});
|
||||
|
||||
it('skips when context_coverage_gate is disabled in config', async () => {
|
||||
await setupPhase(
|
||||
`<decisions>
|
||||
### Cat
|
||||
- **D-01:** Anything
|
||||
- **D-02:** Anything else
|
||||
</decisions>`,
|
||||
{ '17-01-PLAN.md': planFile(` truths: []\n artifacts: []\n key_links: []`) },
|
||||
);
|
||||
await mkdir(join(tmp, '.planning'), { recursive: true });
|
||||
await writeFile(
|
||||
join(tmp, '.planning', 'config.json'),
|
||||
JSON.stringify({ workflow: { context_coverage_gate: false } }),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const result = await checkDecisionCoveragePlan([phaseDir, contextPath], tmp);
|
||||
expect(result.data.skipped).toBe(true);
|
||||
expect(result.data.passed).toBe(true);
|
||||
});
|
||||
|
||||
it('skips cleanly when CONTEXT.md is missing', async () => {
|
||||
await mkdir(phaseDir, { recursive: true });
|
||||
const result = await checkDecisionCoveragePlan([phaseDir, contextPath], tmp);
|
||||
expect(result.data.skipped).toBe(true);
|
||||
expect(result.data.reason).toMatch(/CONTEXT/);
|
||||
});
|
||||
|
||||
it('skips cleanly when <decisions> block is missing', async () => {
|
||||
await mkdir(phaseDir, { recursive: true });
|
||||
await writeFile(contextPath, '# Phase 17\n\nNo decisions block here.\n', 'utf-8');
|
||||
const result = await checkDecisionCoveragePlan([phaseDir, contextPath], tmp);
|
||||
expect(result.data.skipped).toBe(true);
|
||||
});
|
||||
|
||||
it('does not flag non-trackable decisions (Discretion / informational / folded)', async () => {
|
||||
await setupPhase(
|
||||
`<decisions>
|
||||
### Cat
|
||||
- **D-01:** trackable
|
||||
- **D-02 [informational]:** opt-out
|
||||
- **D-03 [folded]:** opt-out
|
||||
|
||||
### Claude's Discretion
|
||||
- **D-99:** never tracked
|
||||
</decisions>`,
|
||||
{
|
||||
'17-01-PLAN.md': planFile(
|
||||
` truths:
|
||||
- "D-01"
|
||||
artifacts: []
|
||||
key_links: []`,
|
||||
),
|
||||
},
|
||||
);
|
||||
const result = await checkDecisionCoveragePlan([phaseDir, contextPath], tmp);
|
||||
expect(result.data.passed).toBe(true);
|
||||
expect(result.data.total).toBe(1); // only D-01 is trackable
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkDecisionCoverageVerify — validation gate (#2492)', () => {
|
||||
it('reports honored decisions when ID appears in shipped artifacts', async () => {
|
||||
await setupPhase(
|
||||
`<decisions>
|
||||
### Cat
|
||||
- **D-05:** Validate input
|
||||
</decisions>`,
|
||||
{ '17-01-PLAN.md': planFile(` truths: ["D-05"]\n artifacts: []\n key_links: []`) },
|
||||
'## Summary\nImplemented D-05.\nfiles_modified: []\n',
|
||||
);
|
||||
|
||||
const result = await checkDecisionCoverageVerify([phaseDir, contextPath], tmp);
|
||||
expect(result.data.honored).toBe(1);
|
||||
expect(result.data.not_honored).toEqual([]);
|
||||
expect(result.data.blocking).toBe(false);
|
||||
});
|
||||
|
||||
it('reports decisions not honored when ID appears nowhere', async () => {
|
||||
await setupPhase(
|
||||
`<decisions>
|
||||
### Cat
|
||||
- **D-50:** Add metrics endpoint
|
||||
</decisions>`,
|
||||
{ '17-01-PLAN.md': planFile(` truths: []\n artifacts: []\n key_links: []`) },
|
||||
'## Summary\nDid other things.\n',
|
||||
);
|
||||
|
||||
const result = await checkDecisionCoverageVerify([phaseDir, contextPath], tmp);
|
||||
expect(result.data.honored).toBe(0);
|
||||
expect(result.data.not_honored.map((u: { id: string }) => u.id)).toEqual(['D-50']);
|
||||
expect(result.data.blocking).toBe(false); // non-blocking by spec
|
||||
expect(result.data.message).toMatch(/D-50/);
|
||||
});
|
||||
|
||||
it('skips when context_coverage_gate is disabled', async () => {
|
||||
await setupPhase(
|
||||
`<decisions>
|
||||
### Cat
|
||||
- **D-50:** anything
|
||||
</decisions>`,
|
||||
{ '17-01-PLAN.md': planFile(` truths: []\n artifacts: []\n key_links: []`) },
|
||||
);
|
||||
await mkdir(join(tmp, '.planning'), { recursive: true });
|
||||
await writeFile(
|
||||
join(tmp, '.planning', 'config.json'),
|
||||
JSON.stringify({ workflow: { context_coverage_gate: false } }),
|
||||
'utf-8',
|
||||
);
|
||||
const result = await checkDecisionCoverageVerify([phaseDir, contextPath], tmp);
|
||||
expect(result.data.skipped).toBe(true);
|
||||
expect(result.data.blocking).toBe(false);
|
||||
});
|
||||
|
||||
it('skips cleanly when CONTEXT.md is missing', async () => {
|
||||
await mkdir(phaseDir, { recursive: true });
|
||||
const result = await checkDecisionCoverageVerify([phaseDir, contextPath], tmp);
|
||||
expect(result.data.skipped).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Adversarial-review regression tests ──────────────────────────────────
|
||||
|
||||
describe('translation gate haystack restriction (review F4)', () => {
|
||||
it('does NOT count a D-NN citation buried in an HTML comment', async () => {
|
||||
await setupPhase(
|
||||
`<decisions>
|
||||
### Cat
|
||||
- **D-77:** A trackable decision worth six or more words long
|
||||
</decisions>`,
|
||||
{
|
||||
'17-01-PLAN.md': planFile(
|
||||
` truths: []\n artifacts: []\n key_links: []`,
|
||||
'<!-- D-77 was here -->\nNothing else mentions the decision.',
|
||||
),
|
||||
},
|
||||
);
|
||||
const result = await checkDecisionCoveragePlan([phaseDir, contextPath], tmp);
|
||||
expect(result.data.passed).toBe(false);
|
||||
expect(result.data.uncovered.map((u: { id: string }) => u.id)).toContain('D-77');
|
||||
});
|
||||
|
||||
it('does NOT count a D-NN citation buried in a fenced code example', async () => {
|
||||
await setupPhase(
|
||||
`<decisions>
|
||||
### Cat
|
||||
- **D-78:** A trackable decision worth six or more words long
|
||||
</decisions>`,
|
||||
{
|
||||
'17-01-PLAN.md': planFile(
|
||||
` truths: []\n artifacts: []\n key_links: []`,
|
||||
'## Design notes\n\n```text\nExample: D-78 should appear here\n```\n',
|
||||
),
|
||||
},
|
||||
);
|
||||
const result = await checkDecisionCoveragePlan([phaseDir, contextPath], tmp);
|
||||
expect(result.data.passed).toBe(false);
|
||||
expect(result.data.uncovered.map((u: { id: string }) => u.id)).toContain('D-78');
|
||||
});
|
||||
|
||||
it('counts a citation in front-matter `must_haves`', async () => {
|
||||
await setupPhase(
|
||||
`<decisions>
|
||||
### Cat
|
||||
- **D-79:** Trackable decision text long enough to soft-match.
|
||||
</decisions>`,
|
||||
{
|
||||
'17-01-PLAN.md': `---
|
||||
phase: 17
|
||||
plan: 1
|
||||
must_haves:
|
||||
- "D-79 must be honored"
|
||||
truths: []
|
||||
artifacts: []
|
||||
key_links: []
|
||||
---
|
||||
`,
|
||||
},
|
||||
);
|
||||
const result = await checkDecisionCoveragePlan([phaseDir, contextPath], tmp);
|
||||
expect(result.data.passed).toBe(true);
|
||||
});
|
||||
|
||||
it('counts a citation in front-matter `truths`', async () => {
|
||||
await setupPhase(
|
||||
`<decisions>
|
||||
### Cat
|
||||
- **D-80:** Trackable decision text long enough to soft-match.
|
||||
</decisions>`,
|
||||
{
|
||||
'17-01-PLAN.md': planFile(` truths: ["D-80 honored"]\n artifacts: []\n key_links: []`),
|
||||
},
|
||||
);
|
||||
const result = await checkDecisionCoveragePlan([phaseDir, contextPath], tmp);
|
||||
expect(result.data.passed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('soft-phrase length gating (review F5)', () => {
|
||||
it('flags a sub-6-word decision when only the body paraphrases — id citation is required', async () => {
|
||||
await setupPhase(
|
||||
// 4 words → cannot soft-match; user must cite the id.
|
||||
`<decisions>
|
||||
### Cat
|
||||
- **D-81:** Use bit offsets always
|
||||
</decisions>`,
|
||||
{
|
||||
'17-01-PLAN.md': planFile(
|
||||
` truths: ["something else"]\n artifacts: []\n key_links: []`,
|
||||
// No D-81 citation, paraphrase only.
|
||||
'## tasks\n- Use bit offsets in storage layer\n',
|
||||
),
|
||||
},
|
||||
);
|
||||
const result = await checkDecisionCoveragePlan([phaseDir, contextPath], tmp);
|
||||
expect(result.data.passed).toBe(false);
|
||||
expect(result.data.uncovered.map((u: { id: string }) => u.id)).toEqual(['D-81']);
|
||||
});
|
||||
|
||||
it('still passes a sub-6-word decision when the id is cited', async () => {
|
||||
await setupPhase(
|
||||
`<decisions>
|
||||
### Cat
|
||||
- **D-82:** Disable cache
|
||||
</decisions>`,
|
||||
{
|
||||
'17-01-PLAN.md': planFile(` truths: ["D-82"]\n artifacts: []\n key_links: []`),
|
||||
},
|
||||
);
|
||||
const result = await checkDecisionCoveragePlan([phaseDir, contextPath], tmp);
|
||||
expect(result.data.passed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('verify-phase summary parsing (review F6, F7)', () => {
|
||||
it('reads files_modified from EVERY summary, not just the first', async () => {
|
||||
await mkdir(phaseDir, { recursive: true });
|
||||
await writeFile(
|
||||
contextPath,
|
||||
`# Phase 17 Context
|
||||
|
||||
<decisions>
|
||||
### Cat
|
||||
- **D-83:** A long-enough trackable decision text for soft matching honored elsewhere.
|
||||
</decisions>
|
||||
`,
|
||||
'utf-8',
|
||||
);
|
||||
await writeFile(
|
||||
join(phaseDir, '17-01-PLAN.md'),
|
||||
planFile(` truths: []\n artifacts: []\n key_links: []`),
|
||||
'utf-8',
|
||||
);
|
||||
// Summary 01 — no files_modified mentioning D-83.
|
||||
await writeFile(
|
||||
join(phaseDir, '17-01-SUMMARY.md'),
|
||||
'files_modified:\n - "src/unrelated.ts"\n',
|
||||
'utf-8',
|
||||
);
|
||||
// Summary 02 — files_modified entry whose content mentions D-83.
|
||||
await writeFile(
|
||||
join(phaseDir, '17-02-SUMMARY.md'),
|
||||
'files_modified:\n - "src/keeper.ts"\n',
|
||||
'utf-8',
|
||||
);
|
||||
await mkdir(join(tmp, 'src'), { recursive: true });
|
||||
await writeFile(join(tmp, 'src', 'unrelated.ts'), '// nothing relevant\n', 'utf-8');
|
||||
await writeFile(join(tmp, 'src', 'keeper.ts'), '// honors D-83 in code\n', 'utf-8');
|
||||
const result = await checkDecisionCoverageVerify([phaseDir, contextPath], tmp);
|
||||
// If only the first SUMMARY were parsed, D-83 would be missing.
|
||||
expect(result.data.honored).toBe(1);
|
||||
expect(result.data.not_honored).toEqual([]);
|
||||
});
|
||||
|
||||
it('rejects absolute files_modified paths outside projectDir (path traversal guard)', async () => {
|
||||
await mkdir(phaseDir, { recursive: true });
|
||||
await writeFile(
|
||||
contextPath,
|
||||
`# Phase 17
|
||||
|
||||
<decisions>
|
||||
### Cat
|
||||
- **D-84:** A trackable decision text spanning enough words to soft-match.
|
||||
</decisions>
|
||||
`,
|
||||
'utf-8',
|
||||
);
|
||||
await writeFile(
|
||||
join(phaseDir, '17-01-PLAN.md'),
|
||||
planFile(` truths: []\n artifacts: []\n key_links: []`),
|
||||
'utf-8',
|
||||
);
|
||||
// Summary points at /etc/passwd and a parent-traversal path. Both must be skipped.
|
||||
await writeFile(
|
||||
join(phaseDir, '17-01-SUMMARY.md'),
|
||||
'files_modified:\n - "/etc/passwd"\n - "../../../etc/hostname"\n',
|
||||
'utf-8',
|
||||
);
|
||||
const result = await checkDecisionCoverageVerify([phaseDir, contextPath], tmp);
|
||||
// Should not honor D-84 from those files (and should not throw).
|
||||
expect(result.data.honored).toBe(0);
|
||||
expect(result.data.not_honored.map((u: { id: string }) => u.id)).toEqual(['D-84']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('workstream-aware config (review F3)', () => {
|
||||
it('honors workstream-scoped context_coverage_gate=false', async () => {
|
||||
await setupPhase(
|
||||
`<decisions>
|
||||
### Cat
|
||||
- **D-85:** A trackable decision long enough to potentially soft match.
|
||||
</decisions>`,
|
||||
{ '17-01-PLAN.md': planFile(` truths: []\n artifacts: []\n key_links: []`) },
|
||||
);
|
||||
// Root config does NOT disable the gate.
|
||||
await mkdir(join(tmp, '.planning'), { recursive: true });
|
||||
await writeFile(
|
||||
join(tmp, '.planning', 'config.json'),
|
||||
JSON.stringify({ workflow: { context_coverage_gate: true } }),
|
||||
'utf-8',
|
||||
);
|
||||
// Workstream config DOES disable it.
|
||||
await mkdir(join(tmp, '.planning', 'workstreams', 'feat-x'), { recursive: true });
|
||||
await writeFile(
|
||||
join(tmp, '.planning', 'workstreams', 'feat-x', 'config.json'),
|
||||
JSON.stringify({ workflow: { context_coverage_gate: false } }),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
// Without workstream → enabled → would fail
|
||||
const rootResult = await checkDecisionCoveragePlan([phaseDir, contextPath], tmp);
|
||||
expect(rootResult.data.skipped).toBe(false);
|
||||
expect(rootResult.data.passed).toBe(false);
|
||||
|
||||
// With workstream → workstream config disables → skipped
|
||||
const wsResult = await checkDecisionCoveragePlan(
|
||||
[phaseDir, contextPath],
|
||||
tmp,
|
||||
'feat-x',
|
||||
);
|
||||
expect(wsResult.data.skipped).toBe(true);
|
||||
expect(wsResult.data.passed).toBe(true);
|
||||
|
||||
// Same for verify
|
||||
const wsVerify = await checkDecisionCoverageVerify(
|
||||
[phaseDir, contextPath],
|
||||
tmp,
|
||||
'feat-x',
|
||||
);
|
||||
expect(wsVerify.data.skipped).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('config-type validation (review F16)', () => {
|
||||
it('warns and defaults to ON when context_coverage_gate is a number', async () => {
|
||||
await setupPhase(
|
||||
`<decisions>
|
||||
### Cat
|
||||
- **D-86:** A trackable decision text long enough to soft-match.
|
||||
</decisions>`,
|
||||
{ '17-01-PLAN.md': planFile(` truths: []\n artifacts: []\n key_links: []`) },
|
||||
);
|
||||
await mkdir(join(tmp, '.planning'), { recursive: true });
|
||||
await writeFile(
|
||||
join(tmp, '.planning', 'config.json'),
|
||||
JSON.stringify({ workflow: { context_coverage_gate: 1 } }),
|
||||
'utf-8',
|
||||
);
|
||||
|
||||
const warnings: string[] = [];
|
||||
const origWarn = console.warn;
|
||||
console.warn = (msg: string) => warnings.push(String(msg));
|
||||
try {
|
||||
const result = await checkDecisionCoveragePlan([phaseDir, contextPath], tmp);
|
||||
// Defaulted to ON → not skipped, runs the gate (and fails with uncovered D-86).
|
||||
expect(result.data.skipped).toBe(false);
|
||||
expect(result.data.passed).toBe(false);
|
||||
} finally {
|
||||
console.warn = origWarn;
|
||||
}
|
||||
expect(warnings.some((w) => /context_coverage_gate.*invalid type/.test(w))).toBe(true);
|
||||
});
|
||||
});
|
||||
554
sdk/src/query/check-decision-coverage.ts
Normal file
554
sdk/src/query/check-decision-coverage.ts
Normal file
@@ -0,0 +1,554 @@
|
||||
/**
|
||||
* Decision-coverage gates — issue #2492.
|
||||
*
|
||||
* Two handlers, two semantics:
|
||||
*
|
||||
* - `check.decision-coverage-plan` — translation gate, BLOCKING.
|
||||
* Plan-phase calls this after the existing requirements coverage gate.
|
||||
* Each trackable CONTEXT.md decision must appear (by id or normalized
|
||||
* phrase) in at least one PLAN.md `must_haves` / `truths` block or in
|
||||
* the plan body. A miss returns `passed: false` with a clear message
|
||||
* naming the missed decision; the workflow surfaces this to the user
|
||||
* and refuses to mark the phase planned.
|
||||
*
|
||||
* - `check.decision-coverage-verify` — validation gate, NON-BLOCKING.
|
||||
* Verify-phase calls this. Each trackable decision is searched in the
|
||||
* phase's shipped artifacts (PLAN.md, SUMMARY.md, files_modified, recent
|
||||
* commit subjects). Misses are reported but do NOT change verification
|
||||
* status. Rationale: by verification time the work is done; a fuzzy
|
||||
* "honored" check is a soft signal, not a blocker.
|
||||
*
|
||||
* Both gates short-circuit when `workflow.context_coverage_gate` is `false`.
|
||||
*
|
||||
* Match strategy (used by both gates):
|
||||
* 1. Strict id match — `D-NN` appears verbatim somewhere in the searched
|
||||
* text. This is the path users should aim for.
|
||||
* 2. Soft phrase match — a normalized 6+-word slice of the decision text
|
||||
* appears as a substring. Catches plans/summaries that paraphrase but
|
||||
* forget the id.
|
||||
*/
|
||||
|
||||
import { readdir, readFile } from 'node:fs/promises';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { join, isAbsolute } from 'node:path';
|
||||
import { execFile as execFileCb } from 'node:child_process';
|
||||
import { promisify } from 'node:util';
|
||||
import { loadConfig } from '../config.js';
|
||||
import { parseDecisions, type ParsedDecision } from './decisions.js';
|
||||
import type { QueryHandler } from './utils.js';
|
||||
|
||||
const execFile = promisify(execFileCb);
|
||||
|
||||
interface GateUncoveredItem {
|
||||
id: string;
|
||||
text: string;
|
||||
category: string;
|
||||
}
|
||||
|
||||
interface PlanGateData {
|
||||
passed: boolean;
|
||||
skipped: boolean;
|
||||
reason?: string;
|
||||
total: number;
|
||||
covered: number;
|
||||
uncovered: GateUncoveredItem[];
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface VerifyGateData {
|
||||
skipped: boolean;
|
||||
blocking: false;
|
||||
reason?: string;
|
||||
total: number;
|
||||
honored: number;
|
||||
not_honored: GateUncoveredItem[];
|
||||
message: string;
|
||||
}
|
||||
|
||||
function normalizePhrase(text: string): string {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s]/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
/** Minimum normalized words a decision must have to be soft-matchable. */
|
||||
const SOFT_PHRASE_MIN_WORDS = 6;
|
||||
|
||||
/**
|
||||
* Build a soft-match phrase: the first 6 normalized words. Six is empirically
|
||||
* long enough to avoid collisions with common English fragments and short
|
||||
* enough to survive minor rewordings.
|
||||
*
|
||||
* Returns an empty string when the decision text has fewer than
|
||||
* SOFT_PHRASE_MIN_WORDS words — such decisions are effectively id-only and
|
||||
* callers must rely on a `D-NN` citation (review F5).
|
||||
*/
|
||||
function softPhrase(text: string): string {
|
||||
const words = normalizePhrase(text).split(' ').filter(Boolean);
|
||||
if (words.length < SOFT_PHRASE_MIN_WORDS) return '';
|
||||
return words.slice(0, SOFT_PHRASE_MIN_WORDS).join(' ');
|
||||
}
|
||||
|
||||
/** True when a decision is too short to soft-match — caller must cite by id. */
|
||||
function requiresIdCitation(decision: ParsedDecision): boolean {
|
||||
const wordCount = normalizePhrase(decision.text).split(' ').filter(Boolean).length;
|
||||
return wordCount < SOFT_PHRASE_MIN_WORDS;
|
||||
}
|
||||
|
||||
/** True when decision text or id appears in `haystack`. */
|
||||
function decisionMentioned(haystack: string, decision: ParsedDecision): boolean {
|
||||
if (!haystack) return false;
|
||||
const idRe = new RegExp(`\\b${decision.id}\\b`);
|
||||
if (idRe.test(haystack)) return true;
|
||||
const phrase = softPhrase(decision.text);
|
||||
if (!phrase) return false; // too short to soft-match — id citation required
|
||||
return normalizePhrase(haystack).includes(phrase);
|
||||
}
|
||||
|
||||
async function readIfExists(path: string): Promise<string> {
|
||||
try {
|
||||
return await readFile(path, 'utf-8');
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPlanContents(phaseDir: string): Promise<string[]> {
|
||||
if (!existsSync(phaseDir)) return [];
|
||||
let entries: string[] = [];
|
||||
try {
|
||||
entries = await readdir(phaseDir);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
const planFiles = entries.filter((e) => /-PLAN\.md$/.test(e));
|
||||
const out: string[] = [];
|
||||
for (const f of planFiles) {
|
||||
out.push(await readIfExists(join(phaseDir, f)));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* One plan reduced to the sections the BLOCKING translation gate searches.
|
||||
*
|
||||
* The plan-phase gate refuses to honor a decision mention buried in a code
|
||||
* fence, an HTML comment, or arbitrary prose elsewhere on the page. The user
|
||||
* must put a `D-NN` citation (or a 6+-word phrase) in a designated section
|
||||
* so they have an unambiguous way to make a decision deliberately uncovered.
|
||||
*
|
||||
* Designated sections (review F4):
|
||||
* - Front-matter `must_haves` block (YAML)
|
||||
* - Front-matter `truths` block (YAML)
|
||||
* - Front-matter `objective` field
|
||||
* - Body section under a heading whose text contains "must_haves",
|
||||
* "truths", "tasks", or "objective" (case-insensitive)
|
||||
*
|
||||
* HTML comments (`<!-- ... -->`) and fenced code blocks are stripped before
|
||||
* extraction so neither a commented-out citation nor a literal example
|
||||
* counts as coverage.
|
||||
*/
|
||||
interface PlanSections {
|
||||
/** Concatenation of all designated section text, with HTML comments and code fences stripped. */
|
||||
designated: string;
|
||||
}
|
||||
|
||||
const DESIGNATED_HEADINGS_RE = /^#{1,6}\s+(?:must[_ ]haves?|truths?|tasks?|objective)\b/i;
|
||||
|
||||
/** Strip HTML comments AND fenced code blocks from `text`. */
|
||||
function stripCommentsAndFences(text: string): string {
|
||||
return text
|
||||
.replace(/<!--[\s\S]*?-->/g, ' ')
|
||||
.replace(/```[\s\S]*?```/g, ' ')
|
||||
.replace(/~~~[\s\S]*?~~~/g, ' ');
|
||||
}
|
||||
|
||||
/** Extract a YAML block scalar (key followed by indented continuation lines). */
|
||||
function extractYamlBlock(frontmatter: string, key: string): string {
|
||||
const re = new RegExp(`^${key}\\s*:(.*)$`, 'm');
|
||||
const match = frontmatter.match(re);
|
||||
if (!match) return '';
|
||||
const startIdx = (match.index ?? 0) + match[0].length;
|
||||
const sameLine = match[1] ?? '';
|
||||
const rest = frontmatter.slice(startIdx + 1).split(/\r?\n/);
|
||||
const block: string[] = [sameLine];
|
||||
for (const line of rest) {
|
||||
// Stop at a non-indented, non-empty line (next top-level key) or end of frontmatter.
|
||||
if (line === '' || /^\s/.test(line)) {
|
||||
block.push(line);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return block.join('\n');
|
||||
}
|
||||
|
||||
function extractPlanSections(planContent: string): PlanSections {
|
||||
if (!planContent) return { designated: '' };
|
||||
const cleaned = stripCommentsAndFences(planContent);
|
||||
|
||||
// Split front-matter from body.
|
||||
const fmMatch = cleaned.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
||||
const frontmatter = fmMatch ? fmMatch[1] : '';
|
||||
const body = fmMatch ? fmMatch[2] : cleaned;
|
||||
|
||||
const fmParts: string[] = [];
|
||||
for (const key of ['must_haves', 'truths', 'objective']) {
|
||||
const block = extractYamlBlock(frontmatter, key);
|
||||
if (block) fmParts.push(block);
|
||||
}
|
||||
|
||||
// Body sections under designated headings (must_haves, truths, tasks, objective).
|
||||
const bodyLines = body.split(/\r?\n/);
|
||||
const bodyParts: string[] = [];
|
||||
let inDesignated = false;
|
||||
for (const line of bodyLines) {
|
||||
const heading = /^#{1,6}\s+/.test(line);
|
||||
if (heading) {
|
||||
inDesignated = DESIGNATED_HEADINGS_RE.test(line);
|
||||
if (inDesignated) bodyParts.push(line);
|
||||
continue;
|
||||
}
|
||||
if (inDesignated) bodyParts.push(line);
|
||||
}
|
||||
|
||||
return { designated: [...fmParts, bodyParts.join('\n')].join('\n\n') };
|
||||
}
|
||||
|
||||
async function loadPlanSections(phaseDir: string): Promise<PlanSections[]> {
|
||||
const contents = await loadPlanContents(phaseDir);
|
||||
return contents.map(extractPlanSections);
|
||||
}
|
||||
|
||||
/** True when a decision is mentioned in any plan's designated sections. */
|
||||
function planSectionsMention(planSections: PlanSections[], decision: ParsedDecision): boolean {
|
||||
for (const p of planSections) {
|
||||
if (decisionMentioned(p.designated, decision)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function loadGateConfig(projectDir: string, workstream?: string): Promise<boolean> {
|
||||
try {
|
||||
const cfg = await loadConfig(projectDir, workstream);
|
||||
const wf = (cfg.workflow ?? {}) as unknown as Record<string, unknown>;
|
||||
const v = wf.context_coverage_gate;
|
||||
if (typeof v === 'boolean') return v;
|
||||
// Tolerate stringified booleans coming from environment-variable-style configs,
|
||||
// but warn loudly on numeric / other-shaped values so silent type drift surfaces.
|
||||
// Schema-vs-loadConfig validation gap (review F16, mirror of #2609).
|
||||
if (typeof v === 'string') {
|
||||
const lower = v.toLowerCase();
|
||||
if (lower === 'false' || lower === 'true') return lower !== 'false';
|
||||
console.warn(
|
||||
`[gsd] workflow.context_coverage_gate is a string "${v}" — expected boolean. Defaulting to ON.`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
if (v !== undefined && v !== null) {
|
||||
console.warn(
|
||||
`[gsd] workflow.context_coverage_gate has invalid type ${typeof v} (value: ${JSON.stringify(v)}); expected boolean. Defaulting to ON.`,
|
||||
);
|
||||
}
|
||||
return true; // default ON
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePath(p: string, projectDir: string): string {
|
||||
return isAbsolute(p) ? p : join(projectDir, p);
|
||||
}
|
||||
|
||||
function buildPlanMessage(uncovered: GateUncoveredItem[]): string {
|
||||
if (uncovered.length === 0) return 'All trackable CONTEXT.md decisions are covered by plans.';
|
||||
const lines = [
|
||||
`## ⚠ Decision Coverage Gap`,
|
||||
``,
|
||||
`${uncovered.length} CONTEXT.md decision(s) are not covered by any plan:`,
|
||||
``,
|
||||
];
|
||||
for (const u of uncovered) {
|
||||
lines.push(`- **${u.id}** (${u.category || 'uncategorized'}): ${u.text}`);
|
||||
}
|
||||
lines.push('');
|
||||
lines.push(
|
||||
'Resolve by citing `D-NN:` in a relevant plan\'s `must_haves`/`truths` (or body),',
|
||||
);
|
||||
lines.push(
|
||||
'OR move the decision to `### Claude\'s Discretion` / tag it `[informational]` if it should not be tracked.',
|
||||
);
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function buildVerifyMessage(notHonored: GateUncoveredItem[]): string {
|
||||
if (notHonored.length === 0)
|
||||
return 'All trackable CONTEXT.md decisions are honored by shipped artifacts.';
|
||||
const lines = [
|
||||
`### Decision Coverage (warning)`,
|
||||
``,
|
||||
`${notHonored.length} decision(s) not found in shipped artifacts:`,
|
||||
``,
|
||||
];
|
||||
for (const u of notHonored) {
|
||||
lines.push(`- **${u.id}** (${u.category || 'uncategorized'}): ${u.text}`);
|
||||
}
|
||||
lines.push('');
|
||||
lines.push('This is a soft warning — verification status is unchanged.');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
// ─── Plan-phase gate ──────────────────────────────────────────────────────
|
||||
|
||||
export const checkDecisionCoveragePlan: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const phaseDir = args[0] ? resolvePath(args[0], projectDir) : '';
|
||||
const contextPath = args[1] ? resolvePath(args[1], projectDir) : '';
|
||||
|
||||
const enabled = await loadGateConfig(projectDir, workstream);
|
||||
if (!enabled) {
|
||||
const data: PlanGateData = {
|
||||
passed: true,
|
||||
skipped: true,
|
||||
reason: 'workflow.context_coverage_gate is false',
|
||||
total: 0,
|
||||
covered: 0,
|
||||
uncovered: [],
|
||||
message: 'Decision coverage gate disabled by config.',
|
||||
};
|
||||
return { data };
|
||||
}
|
||||
|
||||
if (!contextPath || !existsSync(contextPath)) {
|
||||
const data: PlanGateData = {
|
||||
passed: true,
|
||||
skipped: true,
|
||||
reason: 'CONTEXT.md missing',
|
||||
total: 0,
|
||||
covered: 0,
|
||||
uncovered: [],
|
||||
message: 'No CONTEXT.md — nothing to check.',
|
||||
};
|
||||
return { data };
|
||||
}
|
||||
|
||||
const contextRaw = await readIfExists(contextPath);
|
||||
const decisions = parseDecisions(contextRaw).filter((d) => d.trackable);
|
||||
if (decisions.length === 0) {
|
||||
const data: PlanGateData = {
|
||||
passed: true,
|
||||
skipped: true,
|
||||
reason: 'no trackable decisions',
|
||||
total: 0,
|
||||
covered: 0,
|
||||
uncovered: [],
|
||||
message: 'No trackable decisions in CONTEXT.md.',
|
||||
};
|
||||
return { data };
|
||||
}
|
||||
|
||||
const planSections = await loadPlanSections(phaseDir);
|
||||
|
||||
const uncovered: GateUncoveredItem[] = [];
|
||||
let covered = 0;
|
||||
for (const d of decisions) {
|
||||
if (planSectionsMention(planSections, d)) {
|
||||
covered++;
|
||||
} else {
|
||||
uncovered.push({ id: d.id, text: d.text, category: d.category });
|
||||
}
|
||||
}
|
||||
|
||||
const passed = uncovered.length === 0;
|
||||
const data: PlanGateData = {
|
||||
passed,
|
||||
skipped: false,
|
||||
total: decisions.length,
|
||||
covered,
|
||||
uncovered,
|
||||
message: buildPlanMessage(uncovered),
|
||||
};
|
||||
return { data };
|
||||
};
|
||||
|
||||
// ─── Verify-phase gate ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Recent commit subjects + bodies, capped at 200 to span typical phase boundaries
|
||||
* even on busy repos. The non-blocking verify gate trades precision for recall —
|
||||
* a few extra commits in the haystack only inflate "honored" counts harmlessly,
|
||||
* while too few commits could cause false misses on long-running phases (review F18).
|
||||
*/
|
||||
async function recentCommitMessages(projectDir: string, limit = 200): Promise<string> {
|
||||
try {
|
||||
const { stdout } = await execFile('git', ['log', `-n`, String(limit), '--pretty=%s%n%b'], {
|
||||
cwd: projectDir,
|
||||
maxBuffer: 4 * 1024 * 1024,
|
||||
});
|
||||
return stdout;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/** Per-file size cap when slurping modified-file contents into the verify haystack. */
|
||||
const MAX_MODIFIED_FILE_BYTES = 256 * 1024;
|
||||
|
||||
/** Read a file and truncate to MAX_MODIFIED_FILE_BYTES; returns '' on error. */
|
||||
async function readBoundedFile(absPath: string): Promise<string> {
|
||||
try {
|
||||
const raw = await readFile(absPath, 'utf-8');
|
||||
return raw.length > MAX_MODIFIED_FILE_BYTES ? raw.slice(0, MAX_MODIFIED_FILE_BYTES) : raw;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* True when `candidatePath` (after resolution) is contained within `rootDir`.
|
||||
* Rejects absolute paths outside the root, `..` traversal, and any input
|
||||
* whose canonical form escapes the project boundary (review F7).
|
||||
*
|
||||
* Note: this is a lexical check. Symlink targets are NOT resolved here — we
|
||||
* intentionally do not follow links, so a symlink inside the project pointing
|
||||
* outside is not de-referenced (we read the link's target only if it resolves
|
||||
* within projectDir). For full symlink hardening callers should run on a
|
||||
* trusted SUMMARY.md.
|
||||
*/
|
||||
function isInsideRoot(candidatePath: string, rootDir: string): boolean {
|
||||
const root = isAbsolute(rootDir) ? rootDir : join(process.cwd(), rootDir);
|
||||
const target = isAbsolute(candidatePath) ? candidatePath : join(root, candidatePath);
|
||||
// Normalize both via path.resolve-equivalent (join handles `..`).
|
||||
const normalizedRoot = root.endsWith('/') ? root : root + '/';
|
||||
const normalizedTarget = target;
|
||||
return normalizedTarget === root || normalizedTarget.startsWith(normalizedRoot);
|
||||
}
|
||||
|
||||
async function readModifiedFilesContent(projectDir: string, summaries: string[]): Promise<string> {
|
||||
// Walk EVERY summary independently and aggregate file paths. The previous
|
||||
// implementation matched only the first `files_modified:` block in a
|
||||
// concatenated string — when two summaries shipped in one phase the second
|
||||
// plan's files were silently dropped (review F6).
|
||||
const out: string[] = [];
|
||||
let total = 0;
|
||||
for (const summary of summaries) {
|
||||
if (!summary) continue;
|
||||
// /g so multiple `files_modified:` blocks in a single summary are also captured.
|
||||
const blockMatches = summary.matchAll(/files_modified:\s*\n((?:[ \t]*-\s+.+\n?)+)/g);
|
||||
for (const blockMatch of blockMatches) {
|
||||
const block = blockMatch[1] ?? '';
|
||||
const files = [...block.matchAll(/-\s+(.+)/g)].map((m) =>
|
||||
m[1].trim().replace(/^["']|["']$/g, ''),
|
||||
);
|
||||
for (const f of files) {
|
||||
if (!f) continue;
|
||||
if (total >= 50) break; // cap total files across all summaries
|
||||
// Reject absolute paths AND any relative path that escapes projectDir.
|
||||
if (!isInsideRoot(f, projectDir)) {
|
||||
console.warn(
|
||||
`[gsd] decision-coverage: skipping files_modified entry "${f}" — outside project root`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
out.push(await readBoundedFile(resolvePath(f, projectDir)));
|
||||
total++;
|
||||
}
|
||||
if (total >= 50) break;
|
||||
}
|
||||
if (total >= 50) break;
|
||||
}
|
||||
return out.join('\n\n');
|
||||
}
|
||||
|
||||
export const checkDecisionCoverageVerify: QueryHandler = async (args, projectDir, workstream) => {
|
||||
const phaseDir = args[0] ? resolvePath(args[0], projectDir) : '';
|
||||
const contextPath = args[1] ? resolvePath(args[1], projectDir) : '';
|
||||
|
||||
const enabled = await loadGateConfig(projectDir, workstream);
|
||||
if (!enabled) {
|
||||
const data: VerifyGateData = {
|
||||
skipped: true,
|
||||
blocking: false,
|
||||
reason: 'workflow.context_coverage_gate is false',
|
||||
total: 0,
|
||||
honored: 0,
|
||||
not_honored: [],
|
||||
message: 'Decision coverage gate disabled by config.',
|
||||
};
|
||||
return { data };
|
||||
}
|
||||
|
||||
if (!contextPath || !existsSync(contextPath)) {
|
||||
const data: VerifyGateData = {
|
||||
skipped: true,
|
||||
blocking: false,
|
||||
reason: 'CONTEXT.md missing',
|
||||
total: 0,
|
||||
honored: 0,
|
||||
not_honored: [],
|
||||
message: 'No CONTEXT.md — nothing to check.',
|
||||
};
|
||||
return { data };
|
||||
}
|
||||
|
||||
const contextRaw = await readIfExists(contextPath);
|
||||
const decisions = parseDecisions(contextRaw).filter((d) => d.trackable);
|
||||
if (decisions.length === 0) {
|
||||
const data: VerifyGateData = {
|
||||
skipped: true,
|
||||
blocking: false,
|
||||
reason: 'no trackable decisions',
|
||||
total: 0,
|
||||
honored: 0,
|
||||
not_honored: [],
|
||||
message: 'No trackable decisions in CONTEXT.md.',
|
||||
};
|
||||
return { data };
|
||||
}
|
||||
|
||||
// Verify-phase haystack is intentionally broad — this gate is non-blocking and looks
|
||||
// for honored decisions across all phase artifacts, not just plan front-matter sections.
|
||||
const planContents = await loadPlanContents(phaseDir);
|
||||
// Read all *-SUMMARY.md files in phaseDir, capped to keep the haystack bounded.
|
||||
const summaryParts: string[] = [];
|
||||
let summaryContent = '';
|
||||
if (existsSync(phaseDir)) {
|
||||
try {
|
||||
const entries = await readdir(phaseDir);
|
||||
for (const e of entries.filter((x) => /-SUMMARY\.md$/.test(x))) {
|
||||
summaryParts.push(await readIfExists(join(phaseDir, e)));
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
summaryContent = summaryParts.join('\n\n');
|
||||
|
||||
const filesModifiedContent = await readModifiedFilesContent(projectDir, summaryParts);
|
||||
const commits = await recentCommitMessages(projectDir);
|
||||
|
||||
const haystack = [planContents.join('\n\n'), summaryContent, filesModifiedContent, commits].join(
|
||||
'\n\n',
|
||||
);
|
||||
|
||||
const notHonored: GateUncoveredItem[] = [];
|
||||
let honored = 0;
|
||||
for (const d of decisions) {
|
||||
if (decisionMentioned(haystack, d)) {
|
||||
honored++;
|
||||
} else {
|
||||
notHonored.push({ id: d.id, text: d.text, category: d.category });
|
||||
}
|
||||
}
|
||||
|
||||
const data: VerifyGateData = {
|
||||
skipped: false,
|
||||
blocking: false,
|
||||
total: decisions.length,
|
||||
honored,
|
||||
not_honored: notHonored,
|
||||
message: buildVerifyMessage(notHonored),
|
||||
};
|
||||
return { data };
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user