mirror of
https://github.com/glittercowboy/get-shit-done
synced 2026-05-05 23:02:20 +02:00
Compare commits
12 Commits
feat/3039-
...
fix/3061-g
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42ed7cee8d | ||
|
|
5e21bf7567 | ||
|
|
9c92c32f6e | ||
|
|
5c9f34bd31 | ||
|
|
b6c401dc90 | ||
|
|
c3f896f311 | ||
|
|
f104dab332 | ||
|
|
5975f06b6a | ||
|
|
0f98952a3d | ||
|
|
eb365f7336 | ||
|
|
1e6737cd8e | ||
|
|
dca12242b5 |
5
.changeset/blue-stones-topology.md
Normal file
5
.changeset/blue-stones-topology.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Changed
|
||||
---
|
||||
|
||||
**Query command dispatch deepened with Command Topology Module** — query dispatch now consumes a single topology seam that resolves command tokens, binds native handler adapters, and returns structured no-match diagnosis, improving locality and reducing dispatch seam drift.
|
||||
5
.changeset/bold-finches-rally.md
Normal file
5
.changeset/bold-finches-rally.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 3058
|
||||
---
|
||||
**GSD transport raw-mode handling and timeout fallback hardened** — fixes undefined raw formatting edge case and adds raw-path coverage to prevent regressions.
|
||||
8
.changeset/brave-mice-build.md
Normal file
8
.changeset/brave-mice-build.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
type: Changed
|
||||
pr: 3069
|
||||
---
|
||||
|
||||
**query command metadata now flows through a canonical Command Definition Module seam** — registry assembly, mutation semantics, and alias generation consume one Interface (`family`, `canonical`, `aliases`, `mutation`, `output_mode`, `handler_key`) to improve locality and reduce drift.
|
||||
|
||||
**query fallback error mapping cleanup** — the CJS fallback catch path now passes original `err` to `mapFallbackDispatchError` (follow-up to prior review feedback missed in PR #3066).
|
||||
6
.changeset/bright-pumas-fold.md
Normal file
6
.changeset/bright-pumas-fold.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
type: Changed
|
||||
pr: 3075
|
||||
---
|
||||
|
||||
**query architecture deepening pass** — extracted Query Runtime Context, Native Dispatch Adapter, and Query CLI Output Modules so dispatch policy, runtime context policy, and CLI projection logic each live behind focused seams with higher locality and leverage.
|
||||
6
.changeset/cool-monkeys-smell.md
Normal file
6
.changeset/cool-monkeys-smell.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
type: Changed
|
||||
pr: 3074
|
||||
---
|
||||
|
||||
**query CLI path extracted into a dedicated Query CLI Adapter Module** — `sdk/src/cli.ts` now delegates query-specific dispatch, error mapping, and output/exit handling to `sdk/src/query/query-cli-adapter.ts` for better locality and testability.
|
||||
5
.changeset/docs-1-40-0-audit.md
Normal file
5
.changeset/docs-1-40-0-audit.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Changed
|
||||
pr: 0
|
||||
---
|
||||
**Documentation refreshed for v1.40.0** — full audit of `docs/` against the 1.40.0-rc.1 release surface. Updates command lists, walkthroughs, and inventory rows for the 86→59 skill consolidation (#2790), the six namespace meta-skills with two-stage routing (#2792), the `/gsd-health --context` guard, the phase-lifecycle status-line read-side (#2833), and the Gemini colon-form / non-Gemini hyphen-form slash-command split. Translations in ja-JP/ko-KR/zh-CN/pt-BR mirror the structural changes; new English prose is marked with `<!-- TODO i18n -->` for human translator follow-up. CHANGELOG.md `[Unreleased]` section regrouped under Feature/Enhancement/Fix headers.
|
||||
5
.changeset/gemini-skip-local-when-global.md
Normal file
5
.changeset/gemini-skip-local-when-global.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 3037
|
||||
---
|
||||
**Gemini local install no longer duplicates `/gsd:*` commands across user and workspace scopes** — when GSD is already installed at the user scope (`~/.gemini/commands/gsd/`) and you run `npx get-shit-done-cc --gemini --local` in a project, the installer now skips writing `commands/gsd/` to `<project>/.gemini/` and prints a one-line warning explaining why. Previously, both scopes received the same 65 command files, and Gemini's conflict detector renamed every `/gsd:*` command to `/workspace.gsd:*` and `/user.gsd:*`, breaking the documented namespace. Closes #3037.
|
||||
5
.changeset/happy-tigers-travel.md
Normal file
5
.changeset/happy-tigers-travel.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Changed
|
||||
pr: 3060
|
||||
---
|
||||
**Query mutation event mapping moved to dedicated module** — preserves event payloads while improving registry locality and test surface.
|
||||
5
.changeset/humble-goats-swim.md
Normal file
5
.changeset/humble-goats-swim.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Changed
|
||||
pr: 3060
|
||||
---
|
||||
**Alias-family handler maps moved to dedicated catalog module** — keeps command keys/order while reducing createRegistry coupling and improving family-level locality.
|
||||
5
.changeset/merry-moles-chatter.md
Normal file
5
.changeset/merry-moles-chatter.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Changed
|
||||
pr: 3060
|
||||
---
|
||||
**CLI query CJS fallback execution extracted to dedicated adapter module** — preserves logs/help passthrough behavior while improving fallback locality and testability.
|
||||
5
.changeset/noble-badgers-roar.md
Normal file
5
.changeset/noble-badgers-roar.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Changed
|
||||
pr: 3060
|
||||
---
|
||||
**Query mutation event emission now uses a dedicated decorator seam** — preserves fire-and-forget behavior while reducing registry coupling and improving testability.
|
||||
5
.changeset/quick-geese-hum.md
Normal file
5
.changeset/quick-geese-hum.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Changed
|
||||
pr: 3060
|
||||
---
|
||||
**Query fallback orchestration now shared** — CLI and SDK query dispatch now use one planning seam for native vs CJS fallback decisions with behavior parity preserved.
|
||||
5
.changeset/rapid-goats-munch.md
Normal file
5
.changeset/rapid-goats-munch.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Changed
|
||||
pr: 3060
|
||||
---
|
||||
**Query/transport policy data now converged in shared module** — mutation and raw-output policy wiring now share one source of truth to reduce drift.
|
||||
5
.changeset/research-flag-and-stale-refs.md
Normal file
5
.changeset/research-flag-and-stale-refs.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Changed
|
||||
pr: 3042
|
||||
---
|
||||
**`/gsd-research-phase` consolidated into `/gsd-plan-phase --research-phase <N>`** — the standalone research command's slash-command stub was never registered (#3042). Rather than restore the orphan, the research-only capability now lives as a flag on `/gsd-plan-phase`. New modifiers: `--view` prints existing `RESEARCH.md` to stdout without spawning, `--research` forces refresh, otherwise prompts `update / view / skip` when `RESEARCH.md` already exists. Also scrubs four other stale slash-command references (`/gsd-check-todos`, `/gsd-new-workspace`, `/gsd-status`, residual `/gsd-plan-milestone-gaps`) across English + 4 localized doc sets (#3044). Closes #3042 and #3044.
|
||||
6
.changeset/steady-ravens-shape.md
Normal file
6
.changeset/steady-ravens-shape.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
type: Changed
|
||||
pr: 3065
|
||||
---
|
||||
|
||||
**Dispatch policy seam now returns a structured result contract** across native and fallback query execution paths (`ok`, typed error `kind`, `details`, and final `exit_code`), with CLI consuming the unified result instead of mixed throw/result handling.
|
||||
5
.changeset/sturdy-jays-glide.md
Normal file
5
.changeset/sturdy-jays-glide.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Changed
|
||||
pr: 3060
|
||||
---
|
||||
**Query static command registrations now split into domain catalog modules** — preserves command order/strings while improving registry locality and maintenance.
|
||||
5
.changeset/tidy-tunas-zip.md
Normal file
5
.changeset/tidy-tunas-zip.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Changed
|
||||
pr: 3085
|
||||
---
|
||||
**`GSDTools` query execution internals now use deep Module seams** — refactors runtime composition, native/subprocess adapters, and output projection behind stable public interfaces for better locality and testability.
|
||||
114
CHANGELOG.md
114
CHANGELOG.md
@@ -6,20 +6,8 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
## [Unreleased](https://github.com/gsd-build/get-shit-done/compare/v1.39.1...HEAD)
|
||||
|
||||
### Changed
|
||||
### Feature
|
||||
|
||||
- **Test suite for `config-schema.cjs` is now mutation-resistant** — Stryker measured a 4.62% mutation score on `get-shit-done/bin/lib/config-schema.cjs` (6 killed, 124 survived out of 130). Surviving mutants flagged that existing tests were exercising paths but not verifying outputs: a polarity flip (`return true` → `return false`), a predicate swap (`.some` → `.every`), or a guard removal (`if (VALID_CONFIG_KEYS.has(...)) return true;` → unguarded fallthrough) all passed every test. New `tests/bug-2986-config-schema-mutation-killers.test.cjs` adds 95 tests across four suites that target each surviving mutant class: (1) parameterized `isValidConfigKey('${key}') === true` for every member of `VALID_CONFIG_KEYS` (kills the static-key-fast-path mutation), (2) representative dynamic-pattern keys that match exactly one pattern (kills the `.some` → `.every` mutation, with an inline mutual-exclusivity invariant check), (3) `strictEqual` against the literal boolean `true`/`false` instead of `assert.ok` truthy checks (kills polarity-flip mutations), (4) anchor-tightening cases that differ from valid keys by one character beyond the documented shape (kills regex-loosening mutations on `^`, `$`, and character-class boundaries). Tests use the lib's public surface (typed boolean assertions on `isValidConfigKey` return values), no source-grep. (#2986)
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`gsd-pristine/` is now populated by the installer when local patches are detected** — `saveLocalPatches` declared a `pristineDir` variable and JSDoc'd "saves pristine copies (from manifest) to gsd-pristine/ to enable three-way merge during reapply-patches", but no code ever wrote to that directory. Effect: the `/gsd-reapply-patches` Step 5 verifier (#2972) silently degraded to its over-broad fallback heuristic ("every significant backup line"), exactly the silent-success-on-lost-content failure mode #2969 was designed to prevent. Fix: new `populatePristineDir({ packageSrc, pristineDir, modified, runtime, pathPrefix, isGlobal })` helper runs the install transform pipeline (`copyWithPathReplacement`) into a tmp staging dir, then copies out only the modified-file paths into `gsd-pristine/`. `saveLocalPatches` now accepts a `pristineCtx` and calls the helper when local patches are detected; the install entry point passes the package source root, runtime, pathPrefix, and isGlobal so transforms produce byte-identical output to what `copyWithPathReplacement` would have written under normal install. Soft-fails on transform errors (logs a warning, continues with empty pristine — no worse than pre-fix behavior). Pristine reflects the about-to-install version's content, which is what the verifier needs as the "what would survive without the user's modifications" baseline. Regression covered by `tests/bug-2998-pristine-dir-populated.test.cjs` (6 tests across two suites): asserts the helper is exported, returns 0 for empty modified list, writes one pristine file per source-existing path, skips ghost paths without corrupting pristine, and produces deterministic output (two runs with same inputs yield byte-identical pristine — the property `pristine_hashes` in `backup-meta.json` depends on). (#2998)
|
||||
|
||||
|
||||
- **`release-sdk` hotfix re-run no longer fails at `Dry-run publish validation` when the version is already on npm** — the `Detect prior publish (reconciliation mode)` step sets `skip_publish=true` when the package version is already on the registry, and the actual publish step honors that gate. The `Dry-run publish validation` step was missing the same guard, so any operator re-run of an already-published hotfix (the typical recovery path when later steps fail mid-flight) hit `npm publish --dry-run` first and got `npm error You cannot publish over the previously published versions: X.Y.Z` — `npm publish --dry-run` contacts the registry and rejects existing-version targets even though it doesn't actually publish. The dry-run validation step is now gated on the same `steps.prior_publish.outputs.skip_publish != 'true'` condition as the publish step. The rehearsal still runs on first publishes (where it has value); it skips only in the specific reconciliation case where the publish itself would be skipped. Trigger run: [25233855236](https://github.com/gsd-build/get-shit-done/actions/runs/25233855236/job/73995605643). Regression covered by `tests/bug-2987-dry-run-validation-skip-on-reconciliation.test.cjs`. (#2987)
|
||||
- **`release-sdk` hotfix flow hardened against silent classifier failures, missing-classifier-at-base-tag, and a vestigial merge-back PR step** — three issues surfaced by CodeRabbit's post-merge review of #2981 plus a production failure on the v1.39.1 release run. **(1)** `scripts/diff-touches-shipped-paths.cjs` reused exit code `1` for both the legitimate "no shipped paths" classifier result and Node's default uncaught-throw exit, so any tooling failure was indistinguishable from a normal skip. The script now uses `0` (shipped), `1` (not shipped), `2` (classifier error) with `try`/`catch` + `uncaughtException`/`unhandledRejection` handlers routing all failure paths to exit `2`. **(2)** The workflow's `git checkout -b "$BRANCH" "$BASE_TAG"` overwrote the working tree with the base tag's contents *before* the cherry-pick loop ran the classifier — but base tags predating the classifier's introduction (notably v1.39.0) don't have the file in their tree, so `node scripts/diff-touches-shipped-paths.cjs` would exit non-zero and silently drop every commit, producing an empty hotfix release. The classifier is now staged into `$RUNNER_TEMP` at the top of `Prepare hotfix branch` (before any working-tree-mutating git command), and the loop references that staged copy. The cherry-pick loop snapshots `$PIPESTATUS` into a local array (`PIPE_RC=("${PIPESTATUS[@]}")`) immediately after the classifier pipeline — under bracketed `set +e`/`set -e` — and dispatches via explicit `case`: `0` proceeds, `1` skips into `NON_SHIPPED_SKIPPED`, anything else emits `::error::shipped-paths classifier failed for $SHA (exit N)` and fails the workflow. CodeRabbit on PR #2984 caught a subtler bug in the first iteration: `pipeline \|\| true; RC=${PIPESTATUS[1]}` is broken because `\|\| true` runs `true` as its own one-command pipeline on the failure paths, overwriting `PIPESTATUS` to `(0)` and leaving `${PIPESTATUS[1]}` unset. The array-snapshot form is invariant against this. The same hardening also surfaces `git diff-tree`'s exit code (via `PIPE_RC[0]`); a non-zero diff-tree result now also fails the workflow rather than feeding partial input to the classifier. **(3)** Removed the `Open merge-back PR (hotfix only)` step. The auto-cherry-pick hotfix flow only picks commits already on main (`git cherry HEAD origin/main` outputs the unmerged ones), so by construction every code commit on the hotfix branch is already on main. The only hotfix-branch-only commit is the version-bump chore, which would either no-op against main or rewind main's in-progress version. The step also failed in production with `GitHub Actions is not permitted to create or approve pull requests (createPullRequest)` (org policy) on run [25232968975](https://github.com/gsd-build/get-shit-done/actions/runs/25232968975). The `pull-requests: write` permission previously granted to the release job has been dropped in line with least-privilege. The run-summary line that previously echoed `Merge-back PR opened against main` has been replaced with `No merge-back PR (auto-picked commits are already on main)` so operators reading the summary see an accurate non-action statement (CodeRabbit on PR #2984). Regression covered by `tests/bug-2983-classifier-exit-codes-and-base-tag-staging.test.cjs` (15 assertions across exit-code semantics, classifier staging, error dispatch, PIPESTATUS-snapshot hardening, diff-tree fail-fast, merge-back removal, and run-summary accuracy). (#2983)
|
||||
- **`release-sdk` hotfix only cherry-picks commits that change what actually ships** — the `fix:`/`chore:` filter in `Prepare hotfix branch` was too broad: it picked any commit with that conventional-commit type regardless of whether the diff could affect the published npm package. CI-only fixes (release-sdk.yml itself, hotfix tooling, test-only commits) were getting cherry-picked into hotfix branches even though they cannot change the tarball — and the subset touching `.github/workflows/*` then caused the prepare job's `git push` to be rejected by GitHub because the default `GITHUB_TOKEN` lacks the `workflow` scope, aborting the run. v1.39.1 hit this on PR #2977 (run [25232010071](https://github.com/gsd-build/get-shit-done/actions/runs/25232010071)). The loop now pre-skips any candidate commit whose `git diff-tree` output doesn't intersect the npm tarball's shipped paths (entries in `package.json` `files`, plus `package.json` itself, which `npm pack` always includes). Skipped commits land in a new `NON_SHIPPED_SKIPPED` summary bucket framed as informational — non-shipping commits cannot affect the package, so the skip needs no operator action. The shipped-paths classifier lives in `scripts/diff-touches-shipped-paths.cjs` so its rules (file-OR-directory prefix matching `npm pack` semantics, the always-shipped rule for `package.json`, the lockfile-not-shipped rule) are unit-testable. Regression covered by `tests/bug-2980-hotfix-only-picks-shipping-changes.test.cjs`. (#2980)
|
||||
- **`release-sdk` hotfix workflow fails on real run with `npm error Version not changed`** — the `release` job's `Bump in-tree version (not committed)` step ran `npm version "$VERSION"` without `--allow-same-version`, so it errored on real (non-dry-run) hotfix runs because `prepare` had already committed the bump on the hotfix branch. The release job's checkout `ref` is asymmetric — `BRANCH` (already bumped) on real runs vs `BASE_TAG` (older version) on dry-runs — which is why dry-run never caught the bug. Both `npm version` calls in that step now pass `--allow-same-version`, matching the existing pattern in `release.yml:326`. (#2976)
|
||||
### Added — 1.40.0-rc.1
|
||||
- **Six namespace meta-skills with keyword-tag descriptions** — replace the flat 86-skill
|
||||
listing with two-stage hierarchical routing. Model sees 6 namespace routers
|
||||
(`gsd:workflow`, `gsd:project`, `gsd:review`, `gsd:context`, `gsd:manage`,
|
||||
@@ -37,49 +25,6 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
in-flight, idle, and progress display. All fields default to undefined so existing
|
||||
STATE.md files keep rendering. Write-side and status-line wiring follow in a later
|
||||
RC. (#2833)
|
||||
|
||||
### Changed — 1.40.0-rc.1
|
||||
- **Hotfix release flow now auto-incorporates fixes from `main` and bundles the SDK** — `hotfix.yml create` auto-cherry-picks every `fix:`/`chore:` commit on `origin/main` not yet shipped (oldest-first; patch-equivalents skipped via `git cherry`; `feat:`/`refactor:` excluded; conflicts halt with the offending SHA; run summary lists every included SHA). `hotfix.yml finalize` adds the `install-smoke` cross-platform gate, bundles `sdk-bundle/gsd-sdk.tgz` inside the CC tarball (parity with `release-sdk.yml`), tightens the `next` dist-tag re-point, and marks the GitHub Release `--latest`. `release-sdk.yml` gains `action: publish | hotfix` plus an `auto_cherry_pick` toggle, with a new `prepare` job that branches `hotfix/X.YY.Z` from the highest existing `vX.YY.*` tag and runs the same cherry-pick logic — idempotent if the branch was pre-prepared via `hotfix.yml`. Hotfix `vX.YY.Z` is now defined as everything in `vX.YY.{Z-1}` plus every `fix:`/`chore:` since that base, so each tag is the cumulative-fix anchor for the next. (#2955)
|
||||
- **Planning workspace seam extracted from `core.cjs` into `planning-workspace.cjs`** — path/workstream/lock behavior now lives in a dedicated module (`planningDir`, `planningPaths`, `planningRoot`, active-workstream routing, `withPlanningLock`). `core.cjs` keeps compatibility re-exports while call-sites migrate to direct imports, improving locality and reducing coupling. (#2900)
|
||||
- **Skill surface consolidated 86 → 59 `commands/gsd/*.md` entries** — four new
|
||||
grouped skills (`capture`, `phase`, `config`, `workspace`) replace clusters of
|
||||
micro-skills. Six existing parents absorb wrap-up and sub-operations as flags:
|
||||
`update --sync/--reapply`, `sketch --wrap-up`, `spike --wrap-up`,
|
||||
`map-codebase --fast/--query`, `code-review --fix`, `progress --do/--next`. Zero
|
||||
functional loss; 31 micro-skills deleted. `autonomous.md` corrected to call
|
||||
`gsd:code-review --fix` (was invoking deleted `gsd:code-review-fix`). (#2790)
|
||||
- **PRs missing `Closes #NNN` are auto-closed** — the `Issue link required` workflow
|
||||
now auto-closes PRs opened without a closing keyword that links a tracking issue,
|
||||
posting a comment that points to the contribution guide. (#2872)
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Stale deleted command references updated across workflow files** — `help.md`, `do.md`, `settings.md`, `discuss-phase.md`, `new-project.md`, `plan-phase.md`, `spike.md`, and `sketch.md` referenced command names removed in #2790; updated to new consolidated equivalents. (#2950)
|
||||
|
||||
### Fixed — 1.40.0-rc.1
|
||||
- **`spike --wrap-up` now dispatches correctly** — `/gsd-spike --wrap-up` was silently no-oping because the flag dispatch wiring was omitted when the micro-skill entry point was absorbed in #2790. (#2948)
|
||||
- **`config-get context_window` returns `200000` when key absent** — querying an unset `context_window` previously exited 1 with "Key not found", surfacing a confusing error in planning logs even though the workflow fallback worked correctly. `cmdConfigGet` now consults a `SCHEMA_DEFAULTS` map and returns the documented default (`200000`, exit 0) for absent schema-defaulted keys; unknown absent keys still error as before. (#2943)
|
||||
- **`gap-analysis` now parses non-`REQ-` requirement IDs and ignores traceability table headers** — `parseRequirements()` no longer hard-codes the `REQ-` prefix and now accepts uppercase prefixed IDs such as `TST-01`, `BACK-07`, and `INSP-04`; markdown table header rows (for example `| REQ-ID | ... |`) are excluded so header tokens are not reported as phantom uncovered requirements. Added regression coverage for mixed-prefix REQUIREMENTS files with traceability tables. (#2897)
|
||||
- **Gemini slash commands namespaced as `/gsd:<cmd>` instead of `/gsd-<cmd>`** —
|
||||
Gemini CLI namespaces commands under `gsd:`, so `/gsd-plan-phase` was unexecutable.
|
||||
Body-text references in commands, agents, banners, and patch-reapply hints are now
|
||||
converted via a roster-checked regex (boundary lookbehind + extension-aware
|
||||
lookahead + roster lookup, defense-in-depth). The roster fail-loud guard prevents
|
||||
silent no-op'ing if `commands/gsd/` is ever missing. (#2768, #2783)
|
||||
- **`SKILL.md` description quoted for Copilot / Antigravity / Trae / CodeBuddy** —
|
||||
descriptions starting with a YAML 1.2 flow indicator (`[BETA]`, `{`, `*`, `&`, `!`,
|
||||
`|`, `>`, `%`, `@`, backtick) crashed gh-copilot's strict YAML loader. Six emission
|
||||
sites now wrap descriptions in `yamlQuote(...)` (= `JSON.stringify`, a valid YAML
|
||||
1.2 double-quoted scalar). (#2876)
|
||||
- **`gsd-tools` invocations use the absolute installed path** — bare `gsd-tools …`
|
||||
calls inside skill bodies relied on PATH resolution that is not guaranteed in every
|
||||
runtime; replaced with the absolute path emitted at install time. (#2851)
|
||||
- **Codex installer preserves trailing newline when stripping legacy hooks** — the
|
||||
legacy-hook strip in the Codex installer ran against files with no terminating
|
||||
newline at EOF and emitted a config that lost the newline, breaking downstream
|
||||
parsers. (#2866)
|
||||
|
||||
### Added
|
||||
- `--minimal` install flag (alias `--core-only`) writes only the main-loop core skills
|
||||
(`new-project`, `discuss-phase`, `plan-phase`, `execute-phase`, `help`, `update`) and
|
||||
zero `gsd-*` subagents. Cuts cold-start system-prompt overhead from ~12k tokens to
|
||||
@@ -108,7 +53,21 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
on every push to main was rejected because submission rate is too high). Includes an
|
||||
optional `dry_run` boolean and the same publish-verification gate as `release.yml`. (#2828)
|
||||
|
||||
### Changed
|
||||
### Enhancement
|
||||
|
||||
- **Test suite for `config-schema.cjs` is now mutation-resistant** — Stryker measured a 4.62% mutation score on `get-shit-done/bin/lib/config-schema.cjs` (6 killed, 124 survived out of 130). Surviving mutants flagged that existing tests were exercising paths but not verifying outputs: a polarity flip (`return true` → `return false`), a predicate swap (`.some` → `.every`), or a guard removal (`if (VALID_CONFIG_KEYS.has(...)) return true;` → unguarded fallthrough) all passed every test. New `tests/bug-2986-config-schema-mutation-killers.test.cjs` adds 95 tests across four suites that target each surviving mutant class: (1) parameterized `isValidConfigKey('${key}') === true` for every member of `VALID_CONFIG_KEYS` (kills the static-key-fast-path mutation), (2) representative dynamic-pattern keys that match exactly one pattern (kills the `.some` → `.every` mutation, with an inline mutual-exclusivity invariant check), (3) `strictEqual` against the literal boolean `true`/`false` instead of `assert.ok` truthy checks (kills polarity-flip mutations), (4) anchor-tightening cases that differ from valid keys by one character beyond the documented shape (kills regex-loosening mutations on `^`, `$`, and character-class boundaries). Tests use the lib's public surface (typed boolean assertions on `isValidConfigKey` return values), no source-grep. (#2986)
|
||||
- **Hotfix release flow now auto-incorporates fixes from `main` and bundles the SDK** — `hotfix.yml create` auto-cherry-picks every `fix:`/`chore:` commit on `origin/main` not yet shipped (oldest-first; patch-equivalents skipped via `git cherry`; `feat:`/`refactor:` excluded; conflicts halt with the offending SHA; run summary lists every included SHA). `hotfix.yml finalize` adds the `install-smoke` cross-platform gate, bundles `sdk-bundle/gsd-sdk.tgz` inside the CC tarball (parity with `release-sdk.yml`), tightens the `next` dist-tag re-point, and marks the GitHub Release `--latest`. `release-sdk.yml` gains `action: publish | hotfix` plus an `auto_cherry_pick` toggle, with a new `prepare` job that branches `hotfix/X.YY.Z` from the highest existing `vX.YY.*` tag and runs the same cherry-pick logic — idempotent if the branch was pre-prepared via `hotfix.yml`. Hotfix `vX.YY.Z` is now defined as everything in `vX.YY.{Z-1}` plus every `fix:`/`chore:` since that base, so each tag is the cumulative-fix anchor for the next. (#2955)
|
||||
- **Planning workspace seam extracted from `core.cjs` into `planning-workspace.cjs`** — path/workstream/lock behavior now lives in a dedicated module (`planningDir`, `planningPaths`, `planningRoot`, active-workstream routing, `withPlanningLock`). `core.cjs` keeps compatibility re-exports while call-sites migrate to direct imports, improving locality and reducing coupling. (#2900)
|
||||
- **Skill surface consolidated 86 → 59 `commands/gsd/*.md` entries** — four new
|
||||
grouped skills (`capture`, `phase`, `config`, `workspace`) replace clusters of
|
||||
micro-skills. Six existing parents absorb wrap-up and sub-operations as flags:
|
||||
`update --sync/--reapply`, `sketch --wrap-up`, `spike --wrap-up`,
|
||||
`map-codebase --fast/--query`, `code-review --fix`, `progress --do/--next`. Zero
|
||||
functional loss; 31 micro-skills deleted. `autonomous.md` corrected to call
|
||||
`gsd:code-review --fix` (was invoking deleted `gsd:code-review-fix`). (#2790)
|
||||
- **PRs missing `Closes #NNN` are auto-closed** — the `Issue link required` workflow
|
||||
now auto-closes PRs opened without a closing keyword that links a tracking issue,
|
||||
posting a comment that points to the contribution guide. (#2872)
|
||||
- **Canary release workflow now publishes from `dev` branch only** — `.github/workflows/canary.yml`
|
||||
swaps its four publish-step guards from `refs/heads/main` to `refs/heads/dev`. Aligns the
|
||||
workflow with the new branch→dist-tag policy (`dev` → `@canary`, `main` → `@next`/`@latest`).
|
||||
@@ -122,8 +81,6 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
- **`scripts/lint-descriptions.cjs` added** — CI lint gate that fails if any
|
||||
`commands/gsd/*.md` description exceeds 100 chars. Run via `npm run lint:descriptions`.
|
||||
(#2789)
|
||||
|
||||
### Changed
|
||||
- **Skill surface consolidated from 86 → 59 `commands/gsd/*.md` entries** — four new
|
||||
grouped skills replace clusters of micro-skills: `capture` (add-todo, note, add-backlog,
|
||||
plant-seed, check-todos), `phase` (add-phase, insert-phase, remove-phase, edit-phase),
|
||||
@@ -134,8 +91,6 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
`progress --do/--next`. Zero functional loss. (#2790)
|
||||
- **`autonomous.md` corrected** — was invoking deleted `gsd:code-review-fix`; now calls
|
||||
`gsd:code-review --fix`. (#2790)
|
||||
|
||||
### Removed
|
||||
- **31 micro-skills deleted** — absorbed into consolidated parents or removed outright:
|
||||
add-todo, note, add-backlog, plant-seed, check-todos, add-phase, insert-phase,
|
||||
remove-phase, edit-phase, settings-advanced, settings-integrations, set-profile,
|
||||
@@ -144,8 +99,39 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
join-discord, research-phase, session-report, from-gsd2, analyze-dependencies,
|
||||
list-phase-assumptions, plan-milestone-gaps. All functionality preserved via flags on
|
||||
consolidated skills. (#2790)
|
||||
- **`discuss-phase` lazy file loading** — entry-point `@file` directives replaced with
|
||||
on-demand `Read()` calls gated behind mode routing. Tokens loaded at skill entry drop
|
||||
from ~13k to near zero; only the branch actually invoked is loaded. (#2606)
|
||||
|
||||
### Fixed
|
||||
### Fix
|
||||
|
||||
- **`gsd-pristine/` is now populated by the installer when local patches are detected** — `saveLocalPatches` declared a `pristineDir` variable and JSDoc'd "saves pristine copies (from manifest) to gsd-pristine/ to enable three-way merge during reapply-patches", but no code ever wrote to that directory. Effect: the `/gsd-reapply-patches` Step 5 verifier (#2972) silently degraded to its over-broad fallback heuristic ("every significant backup line"), exactly the silent-success-on-lost-content failure mode #2969 was designed to prevent. Fix: new `populatePristineDir({ packageSrc, pristineDir, modified, runtime, pathPrefix, isGlobal })` helper runs the install transform pipeline (`copyWithPathReplacement`) into a tmp staging dir, then copies out only the modified-file paths into `gsd-pristine/`. `saveLocalPatches` now accepts a `pristineCtx` and calls the helper when local patches are detected; the install entry point passes the package source root, runtime, pathPrefix, and isGlobal so transforms produce byte-identical output to what `copyWithPathReplacement` would have written under normal install. Soft-fails on transform errors (logs a warning, continues with empty pristine — no worse than pre-fix behavior). Pristine reflects the about-to-install version's content, which is what the verifier needs as the "what would survive without the user's modifications" baseline. Regression covered by `tests/bug-2998-pristine-dir-populated.test.cjs` (6 tests across two suites): asserts the helper is exported, returns 0 for empty modified list, writes one pristine file per source-existing path, skips ghost paths without corrupting pristine, and produces deterministic output (two runs with same inputs yield byte-identical pristine — the property `pristine_hashes` in `backup-meta.json` depends on). (#2998)
|
||||
- **`release-sdk` hotfix re-run no longer fails at `Dry-run publish validation` when the version is already on npm** — the `Detect prior publish (reconciliation mode)` step sets `skip_publish=true` when the package version is already on the registry, and the actual publish step honors that gate. The `Dry-run publish validation` step was missing the same guard, so any operator re-run of an already-published hotfix (the typical recovery path when later steps fail mid-flight) hit `npm publish --dry-run` first and got `npm error You cannot publish over the previously published versions: X.Y.Z` — `npm publish --dry-run` contacts the registry and rejects existing-version targets even though it doesn't actually publish. The dry-run validation step is now gated on the same `steps.prior_publish.outputs.skip_publish != 'true'` condition as the publish step. The rehearsal still runs on first publishes (where it has value); it skips only in the specific reconciliation case where the publish itself would be skipped. Trigger run: [25233855236](https://github.com/gsd-build/get-shit-done/actions/runs/25233855236/job/73995605643). Regression covered by `tests/bug-2987-dry-run-validation-skip-on-reconciliation.test.cjs`. (#2987)
|
||||
- **`release-sdk` hotfix flow hardened against silent classifier failures, missing-classifier-at-base-tag, and a vestigial merge-back PR step** — three issues surfaced by CodeRabbit's post-merge review of #2981 plus a production failure on the v1.39.1 release run. **(1)** `scripts/diff-touches-shipped-paths.cjs` reused exit code `1` for both the legitimate "no shipped paths" classifier result and Node's default uncaught-throw exit, so any tooling failure was indistinguishable from a normal skip. The script now uses `0` (shipped), `1` (not shipped), `2` (classifier error) with `try`/`catch` + `uncaughtException`/`unhandledRejection` handlers routing all failure paths to exit `2`. **(2)** The workflow's `git checkout -b "$BRANCH" "$BASE_TAG"` overwrote the working tree with the base tag's contents *before* the cherry-pick loop ran the classifier — but base tags predating the classifier's introduction (notably v1.39.0) don't have the file in their tree, so `node scripts/diff-touches-shipped-paths.cjs` would exit non-zero and silently drop every commit, producing an empty hotfix release. The classifier is now staged into `$RUNNER_TEMP` at the top of `Prepare hotfix branch` (before any working-tree-mutating git command), and the loop references that staged copy. The cherry-pick loop snapshots `$PIPESTATUS` into a local array (`PIPE_RC=("${PIPESTATUS[@]}")`) immediately after the classifier pipeline — under bracketed `set +e`/`set -e` — and dispatches via explicit `case`: `0` proceeds, `1` skips into `NON_SHIPPED_SKIPPED`, anything else emits `::error::shipped-paths classifier failed for $SHA (exit N)` and fails the workflow. CodeRabbit on PR #2984 caught a subtler bug in the first iteration: `pipeline \|\| true; RC=${PIPESTATUS[1]}` is broken because `\|\| true` runs `true` as its own one-command pipeline on the failure paths, overwriting `PIPESTATUS` to `(0)` and leaving `${PIPESTATUS[1]}` unset. The array-snapshot form is invariant against this. The same hardening also surfaces `git diff-tree`'s exit code (via `PIPE_RC[0]`); a non-zero diff-tree result now also fails the workflow rather than feeding partial input to the classifier. **(3)** Removed the `Open merge-back PR (hotfix only)` step. The auto-cherry-pick hotfix flow only picks commits already on main (`git cherry HEAD origin/main` outputs the unmerged ones), so by construction every code commit on the hotfix branch is already on main. The only hotfix-branch-only commit is the version-bump chore, which would either no-op against main or rewind main's in-progress version. The step also failed in production with `GitHub Actions is not permitted to create or approve pull requests (createPullRequest)` (org policy) on run [25232968975](https://github.com/gsd-build/get-shit-done/actions/runs/25232968975). The `pull-requests: write` permission previously granted to the release job has been dropped in line with least-privilege. The run-summary line that previously echoed `Merge-back PR opened against main` has been replaced with `No merge-back PR (auto-picked commits are already on main)` so operators reading the summary see an accurate non-action statement (CodeRabbit on PR #2984). Regression covered by `tests/bug-2983-classifier-exit-codes-and-base-tag-staging.test.cjs` (15 assertions across exit-code semantics, classifier staging, error dispatch, PIPESTATUS-snapshot hardening, diff-tree fail-fast, merge-back removal, and run-summary accuracy). (#2983)
|
||||
- **`release-sdk` hotfix only cherry-picks commits that change what actually ships** — the `fix:`/`chore:` filter in `Prepare hotfix branch` was too broad: it picked any commit with that conventional-commit type regardless of whether the diff could affect the published npm package. CI-only fixes (release-sdk.yml itself, hotfix tooling, test-only commits) were getting cherry-picked into hotfix branches even though they cannot change the tarball — and the subset touching `.github/workflows/*` then caused the prepare job's `git push` to be rejected by GitHub because the default `GITHUB_TOKEN` lacks the `workflow` scope, aborting the run. v1.39.1 hit this on PR #2977 (run [25232010071](https://github.com/gsd-build/get-shit-done/actions/runs/25232010071)). The loop now pre-skips any candidate commit whose `git diff-tree` output doesn't intersect the npm tarball's shipped paths (entries in `package.json` `files`, plus `package.json` itself, which `npm pack` always includes). Skipped commits land in a new `NON_SHIPPED_SKIPPED` summary bucket framed as informational — non-shipping commits cannot affect the package, so the skip needs no operator action. The shipped-paths classifier lives in `scripts/diff-touches-shipped-paths.cjs` so its rules (file-OR-directory prefix matching `npm pack` semantics, the always-shipped rule for `package.json`, the lockfile-not-shipped rule) are unit-testable. Regression covered by `tests/bug-2980-hotfix-only-picks-shipping-changes.test.cjs`. (#2980)
|
||||
- **`release-sdk` hotfix workflow fails on real run with `npm error Version not changed`** — the `release` job's `Bump in-tree version (not committed)` step ran `npm version "$VERSION"` without `--allow-same-version`, so it errored on real (non-dry-run) hotfix runs because `prepare` had already committed the bump on the hotfix branch. The release job's checkout `ref` is asymmetric — `BRANCH` (already bumped) on real runs vs `BASE_TAG` (older version) on dry-runs — which is why dry-run never caught the bug. Both `npm version` calls in that step now pass `--allow-same-version`, matching the existing pattern in `release.yml:326`. (#2976)
|
||||
- **Stale deleted command references updated across workflow files** — `help.md`, `do.md`, `settings.md`, `discuss-phase.md`, `new-project.md`, `plan-phase.md`, `spike.md`, and `sketch.md` referenced command names removed in #2790; updated to new consolidated equivalents. (#2950)
|
||||
- **`spike --wrap-up` now dispatches correctly** — `/gsd-spike --wrap-up` was silently no-oping because the flag dispatch wiring was omitted when the micro-skill entry point was absorbed in #2790. (#2948)
|
||||
- **`config-get context_window` returns `200000` when key absent** — querying an unset `context_window` previously exited 1 with "Key not found", surfacing a confusing error in planning logs even though the workflow fallback worked correctly. `cmdConfigGet` now consults a `SCHEMA_DEFAULTS` map and returns the documented default (`200000`, exit 0) for absent schema-defaulted keys; unknown absent keys still error as before. (#2943)
|
||||
- **`gap-analysis` now parses non-`REQ-` requirement IDs and ignores traceability table headers** — `parseRequirements()` no longer hard-codes the `REQ-` prefix and now accepts uppercase prefixed IDs such as `TST-01`, `BACK-07`, and `INSP-04`; markdown table header rows (for example `| REQ-ID | ... |`) are excluded so header tokens are not reported as phantom uncovered requirements. Added regression coverage for mixed-prefix REQUIREMENTS files with traceability tables. (#2897)
|
||||
- **Gemini slash commands namespaced as `/gsd:<cmd>` instead of `/gsd-<cmd>`** —
|
||||
Gemini CLI namespaces commands under `gsd:`, so `/gsd-plan-phase` was unexecutable.
|
||||
Body-text references in commands, agents, banners, and patch-reapply hints are now
|
||||
converted via a roster-checked regex (boundary lookbehind + extension-aware
|
||||
lookahead + roster lookup, defense-in-depth). The roster fail-loud guard prevents
|
||||
silent no-op'ing if `commands/gsd/` is ever missing. (#2768, #2783)
|
||||
- **`SKILL.md` description quoted for Copilot / Antigravity / Trae / CodeBuddy** —
|
||||
descriptions starting with a YAML 1.2 flow indicator (`[BETA]`, `{`, `*`, `&`, `!`,
|
||||
`|`, `>`, `%`, `@`, backtick) crashed gh-copilot's strict YAML loader. Six emission
|
||||
sites now wrap descriptions in `yamlQuote(...)` (= `JSON.stringify`, a valid YAML
|
||||
1.2 double-quoted scalar). (#2876)
|
||||
- **`gsd-tools` invocations use the absolute installed path** — bare `gsd-tools …`
|
||||
calls inside skill bodies relied on PATH resolution that is not guaranteed in every
|
||||
runtime; replaced with the absolute path emitted at install time. (#2851)
|
||||
- **Codex installer preserves trailing newline when stripping legacy hooks** — the
|
||||
legacy-hook strip in the Codex installer ran against files with no terminating
|
||||
newline at EOF and emitted a config that lost the newline, breaking downstream
|
||||
parsers. (#2866)
|
||||
- **GSD slash command namespace drift cleaned up across docs, workflows, and autocomplete** — remaining active `/gsd:<cmd>` references now use canonical `/gsd-<cmd>`, escaped workflow `Skill(skill=\"gsd:...\")` prompts now use hyphenated skill names, `scripts/fix-slash-commands.cjs` rewrites retired colon syntax to hyphen syntax, and the extract-learnings command file now uses `extract-learnings.md` so generated Claude/Qwen skill autocomplete exposes `gsd-extract-learnings` instead of `gsd-extract_learnings`. (#2855)
|
||||
- **`extractCurrentMilestone` no longer truncates ROADMAP.md at heading-like lines inside fenced code blocks** — the milestone-end search now scans line-by-line while tracking ` ``` ` / `~~~` fence state, so a line like `# Ops runbook (v1.0 compat)` inside a code block no longer acts as a milestone boundary. Previously, any phase defined after such a block was invisible to `roadmap analyze`, `roadmap get-phase`, `/gsd-autonomous`, and all phase-number commands. (#2787)
|
||||
- **Codex install no longer corrupts existing `~/.codex/config.toml`** — the installer
|
||||
@@ -321,10 +307,6 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
pre-existing sentinel force-removes the orphan worktree before starting fresh, making
|
||||
the agent self-healing across crashes. (#2839)
|
||||
|
||||
### Performance
|
||||
- **`discuss-phase` lazy file loading** — entry-point `@file` directives replaced with
|
||||
on-demand `Read()` calls gated behind mode routing. Tokens loaded at skill entry drop
|
||||
from ~13k to near zero; only the branch actually invoked is loaded. (#2606)
|
||||
|
||||
## [1.39.1] - 2026-05-01
|
||||
|
||||
|
||||
41
CONTEXT.md
Normal file
41
CONTEXT.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Context
|
||||
|
||||
## Domain terms
|
||||
|
||||
### Dispatch Policy Module
|
||||
Module owning dispatch error mapping, fallback policy, timeout classification, and CLI exit mapping contract.
|
||||
|
||||
Canonical error kind set:
|
||||
- `unknown_command`
|
||||
- `native_failure`
|
||||
- `native_timeout`
|
||||
- `fallback_failure`
|
||||
- `validation_error`
|
||||
- `internal_error`
|
||||
|
||||
### Command Definition Module
|
||||
Canonical command metadata Interface powering alias, catalog, and semantics generation.
|
||||
|
||||
### Query Runtime Context Module
|
||||
Module owning query-time context resolution for `projectDir` and `ws`, including precedence and validation policy used by query adapters.
|
||||
|
||||
### Native Dispatch Adapter Module
|
||||
Adapter Module that satisfies native query dispatch at the Dispatch Policy seam, so policy modules consume a focused dispatch Interface instead of closure-wired call sites.
|
||||
|
||||
### Query CLI Output Module
|
||||
Module owning projection from dispatch results/errors to CLI `{ exitCode, stdoutChunks, stderrLines }` output contract.
|
||||
|
||||
### Query Execution Policy Module
|
||||
Module owning query transport routing policy projection (`preferNative`, fallback policy, workstream subprocess forcing) at execution seam.
|
||||
|
||||
### Query Subprocess Adapter Module
|
||||
Adapter Module owning subprocess execution contract for query commands (JSON/raw invocation, `@file:` indirection parsing, timeout/exit error projection).
|
||||
|
||||
### Query Command Resolution Module
|
||||
Canonical command normalization and resolution Interface (`query-command-resolution-strategy`) used by internal query/transport paths after dead-wrapper convergence.
|
||||
|
||||
### Command Topology Module
|
||||
Module owning command resolution, policy projection (`mutation`, `output_mode`), unknown-command diagnosis, and handler Adapter binding at one seam for query dispatch.
|
||||
|
||||
### Query Pre-Project Config Policy Module
|
||||
Module policy that defines query-time behavior when `.planning/config.json` is absent: use built-in defaults for parity-sensitive query Interfaces, and emit parity-aligned empty model ids for pre-project model resolution surfaces.
|
||||
@@ -81,6 +81,20 @@ PRs that arrive without a properly-labeled linked issue are closed automatically
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
### Architecture & Domain Standards (Maintainer-Defined)
|
||||
|
||||
The following files are maintainer-owned coding standards and must be treated as canonical when contributing:
|
||||
|
||||
- `CONTEXT.md` — domain language and module naming standards
|
||||
- `docs/adr/` — Architecture Decision Records (ADRs) for accepted architectural decisions
|
||||
|
||||
Contributor requirements:
|
||||
- Read `CONTEXT.md` before naming or refactoring modules/interfaces/seams.
|
||||
- Use `CONTEXT.md` vocabulary consistently in code comments, tests, issue/PR text, and docs for the touched area.
|
||||
- Check relevant ADRs in `docs/adr/` before proposing or implementing architectural changes.
|
||||
- If a change intentionally revisits an ADR decision, call it out explicitly in the linked issue and PR rationale.
|
||||
- Do not rewrite maintainer intent in `CONTEXT.md`/ADRs as part of drive-by cleanup; propose focused updates tied to approved scope.
|
||||
|
||||
**Every PR must link to an approved issue.** PRs without a linked issue are closed without review, no exceptions.
|
||||
|
||||
- **No draft PRs** — draft PRs are automatically closed. Only open a PR when it is complete, tested, and ready for review. If your work is not finished, keep it on your local branch until it is.
|
||||
@@ -504,6 +518,14 @@ Run locally before pushing: `npm run lint:tests`
|
||||
|
||||
### Test Requirements by Contribution Type
|
||||
|
||||
### Architecture-Aware Testing Requirements
|
||||
|
||||
When work touches architecture, routing, policy, registry assembly, or command semantics:
|
||||
- Write tests against module **interfaces** and seam behavior, not implementation trivia.
|
||||
- Prefer invariant/contract tests that protect ADR-backed behavior and `CONTEXT.md` terminology.
|
||||
- Ensure tests validate canonical behavior through the defined seam (for example: structured result contracts, canonical command metadata, and adapter parity), not source-text coupling.
|
||||
- If ADRs define expected behavior, tests should assert those expectations directly.
|
||||
|
||||
The required tests differ depending on what you are contributing:
|
||||
|
||||
**Bug Fix:** A regression test is required. Write the test first — it must demonstrate the original failure before your fix is applied, then pass after the fix. A PR that fixes a bug without a regression test will be asked to add one. "Tests pass" does not prove correctness; it proves the bug isn't present in the tests that exist.
|
||||
|
||||
@@ -651,7 +651,7 @@ You're never locked in. The system adapts.
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/gsd-new-workspace` | Create isolated workspace with repo copies (worktrees or clones) |
|
||||
| `/gsd-workspace --new` | Create isolated workspace with repo copies (worktrees or clones) |
|
||||
| `/gsd-list-workspaces` | Show all GSD workspaces and their status |
|
||||
| `/gsd-remove-workspace` | Remove workspace and clean up worktrees |
|
||||
|
||||
@@ -698,7 +698,6 @@ You're never locked in. The system adapts.
|
||||
| `/gsd-edit-phase [N] [--force]` | Modify any field of an existing phase in place — number and position unchanged |
|
||||
| `/gsd-remove-phase [N]` | Remove future phase, renumber |
|
||||
| `/gsd-list-phase-assumptions [N]` | See Claude's intended approach before planning |
|
||||
| `/gsd-plan-milestone-gaps` | Create phases to close gaps from audit |
|
||||
|
||||
### Session
|
||||
|
||||
@@ -740,7 +739,7 @@ You're never locked in. The system adapts.
|
||||
| `/gsd-settings` | Configure model profile and workflow agents |
|
||||
| `/gsd-set-profile <profile>` | Switch model profile (quality/balanced/budget/inherit) |
|
||||
| `/gsd-add-todo [desc]` | Capture idea for later |
|
||||
| `/gsd-check-todos` | List pending todos |
|
||||
| `/gsd-capture --list` | List pending todos |
|
||||
| `/gsd-debug [desc]` | Systematic debugging with persistent state |
|
||||
| `/gsd-do <text>` | Route freeform text to the right GSD command automatically |
|
||||
| `/gsd-note <text>` | Zero-friction idea capture — append, list, or promote notes to todos |
|
||||
|
||||
@@ -7556,15 +7556,55 @@ function install(isGlobal, runtime = 'claude') {
|
||||
// No skills/commands directory needed. Engine is installed via copyWithPathReplacement.
|
||||
console.log(` ${green}✓${reset} Cline: commands will be available via .clinerules`);
|
||||
} else if (isGemini) {
|
||||
const commandsDir = path.join(targetDir, 'commands');
|
||||
fs.mkdirSync(commandsDir, { recursive: true });
|
||||
const gsdSrc = stageSkillsForMode(path.join(src, 'commands', 'gsd'), installMode);
|
||||
const gsdDest = path.join(commandsDir, 'gsd');
|
||||
copyWithPathReplacement(gsdSrc, gsdDest, pathPrefix, runtime, true, isGlobal);
|
||||
if (verifyInstalled(gsdDest, 'commands/gsd')) {
|
||||
console.log(` ${green}✓${reset} Installed commands/gsd`);
|
||||
// #3037: when running --local --gemini and a GSD-managed user-scope
|
||||
// command directory already exists at ~/.gemini/commands/gsd/, skip
|
||||
// the local copy. Gemini conflict-detects by command name across
|
||||
// scopes and renames every overlapping /gsd:* command to
|
||||
// /workspace.gsd:* and /user.gsd:*, breaking the documented namespace.
|
||||
// The user-scope install already provides the same commands, so the
|
||||
// local copy adds zero value at the cost of namespace conflicts.
|
||||
//
|
||||
// CR #3041 (Major): the detection must be specific to PACKAGE-MANAGED
|
||||
// GSD content, not just "directory is non-empty". A user who hand-
|
||||
// dropped a single override (e.g. ~/.gemini/commands/gsd/my-override
|
||||
// .toml) would otherwise be unable to run a local install at all.
|
||||
// Detection rule: at least 3 of the canonical GSD command files
|
||||
// ('help.toml', 'progress.toml', 'new-project.toml') must be present.
|
||||
// These three ship in every GSD Gemini install (minimal mode included
|
||||
// — they're in the core skill set per #2790's consolidation), and 3-of-
|
||||
// 3 with that specific basename set is structurally impossible to
|
||||
// produce by accident.
|
||||
const homeGeminiGsd = path.join(os.homedir(), '.gemini', 'commands', 'gsd');
|
||||
const GSD_MANAGED_CANARIES = ['help.toml', 'progress.toml', 'new-project.toml'];
|
||||
const userScopeHasGsd =
|
||||
!isGlobal &&
|
||||
path.resolve(targetDir) !== path.resolve(path.join(os.homedir(), '.gemini')) &&
|
||||
fs.existsSync(homeGeminiGsd) &&
|
||||
GSD_MANAGED_CANARIES.every((f) =>
|
||||
fs.existsSync(path.join(homeGeminiGsd, f))
|
||||
);
|
||||
|
||||
if (userScopeHasGsd) {
|
||||
console.log(
|
||||
` ${yellow}⚠${reset} Skipping commands/gsd/ for local install — GSD is already installed at user scope (${homeGeminiGsd}).`
|
||||
);
|
||||
console.log(
|
||||
` Gemini conflict-detects across scopes and would rename every /gsd:* command to /workspace.gsd:* and /user.gsd:*.`
|
||||
);
|
||||
console.log(
|
||||
` The user-scope install already provides /gsd:* commands in this project; no local copy is needed.`
|
||||
);
|
||||
} else {
|
||||
failures.push('commands/gsd');
|
||||
const commandsDir = path.join(targetDir, 'commands');
|
||||
fs.mkdirSync(commandsDir, { recursive: true });
|
||||
const gsdSrc = stageSkillsForMode(path.join(src, 'commands', 'gsd'), installMode);
|
||||
const gsdDest = path.join(commandsDir, 'gsd');
|
||||
copyWithPathReplacement(gsdSrc, gsdDest, pathPrefix, runtime, true, isGlobal);
|
||||
if (verifyInstalled(gsdDest, 'commands/gsd')) {
|
||||
console.log(` ${green}✓${reset} Installed commands/gsd`);
|
||||
} else {
|
||||
failures.push('commands/gsd');
|
||||
}
|
||||
}
|
||||
} else if (isGlobal) {
|
||||
// Claude Code global: skills/ format (2.1.88+ compatibility)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd:plan-phase
|
||||
description: Create detailed phase plan (PLAN.md) with verification loop
|
||||
argument-hint: "[phase] [--auto] [--research] [--skip-research] [--gaps] [--skip-verify] [--prd <file>] [--reviews] [--text] [--tdd] [--mvp]"
|
||||
argument-hint: "[phase] [--auto] [--research] [--skip-research] [--research-phase <N>] [--view] [--gaps] [--skip-verify] [--prd <file>] [--reviews] [--text] [--tdd] [--mvp]"
|
||||
agent: gsd-planner
|
||||
allowed-tools:
|
||||
- Read
|
||||
@@ -19,6 +19,13 @@ Create executable phase prompts (PLAN.md files) for a roadmap phase with integra
|
||||
|
||||
**Default flow:** Research (if needed) → Plan → Verify → Done
|
||||
|
||||
**Research-only mode (`--research-phase <N>`):** Spawn `gsd-phase-researcher` for phase `N`, write `RESEARCH.md`, then exit before the planner runs. Useful for cross-phase research, doc review before committing to a planning approach, and correction-without-replanning loops where iterating on research alone is dramatically cheaper than re-spawning the planner. Replaces the deleted `/gsd-research-phase` command (#3042).
|
||||
|
||||
**Research-only modifiers:**
|
||||
- **No flag** — when `RESEARCH.md` already exists, prompt the user to choose `update / view / skip`.
|
||||
- **`--research`** — force-refresh: re-spawn the researcher unconditionally, no prompt. Skips the existing-RESEARCH.md menu.
|
||||
- **`--view`** — view-only: print existing `RESEARCH.md` to stdout. Does not spawn the researcher. Cheapest mode for the correction-without-replanning loop. If no `RESEARCH.md` exists yet, errors with a hint to drop `--view`.
|
||||
|
||||
**Orchestrator role:** Parse arguments, validate phase, research domain (unless skipped), spawn gsd-planner, verify with gsd-plan-checker, iterate until pass or max iterations, present results.
|
||||
</objective>
|
||||
|
||||
|
||||
@@ -111,14 +111,25 @@ Multiple layers prevent common failure modes:
|
||||
|
||||
User-facing entry points. Each file contains YAML frontmatter (name, description, allowed-tools) and a prompt body that bootstraps the workflow. Commands are installed as:
|
||||
|
||||
- **Claude Code:** Custom slash commands (`/gsd-command-name`)
|
||||
- **OpenCode / Kilo:** Slash commands (`/gsd-command-name`)
|
||||
- **Claude Code:** Custom slash commands (hyphen form, `/gsd-command-name`)
|
||||
- **OpenCode / Kilo:** Slash commands (hyphen form, `/gsd-command-name`)
|
||||
- **Codex:** Skills (`$gsd-command-name`)
|
||||
- **Copilot:** Slash commands (`/gsd-command-name`)
|
||||
- **Copilot:** Slash commands (hyphen form, `/gsd-command-name`)
|
||||
- **Gemini CLI:** Slash commands under the `gsd:` namespace (colon form, `/gsd:command-name`) — Gemini namespaces all custom commands under their plugin id, so the install path rewrites every body-text reference to colon form
|
||||
- **Antigravity:** Skills
|
||||
|
||||
**Total commands:** see [`docs/INVENTORY.md`](INVENTORY.md#commands) for the authoritative count and full roster.
|
||||
|
||||
#### Two-stage hierarchical routing (v1.40, [#2792](https://github.com/gsd-build/get-shit-done/issues/2792))
|
||||
|
||||
To keep the eager skill-listing token cost low, v1.40 introduces six namespace **meta-skills** (`gsd-workflow`, `gsd-project`, `gsd-review`, `gsd-context`, `gsd-manage`, `gsd-ideate` — sourced from `commands/gsd/ns-*.md`, but the invocable `name:` is the bare form shown here) layered above the concrete sub-skills. The model sees 6 namespace routers (~120 tokens) instead of a flat 86-skill listing (~2,150 tokens), selects a namespace, then routes to the concrete sub-skill via a routing table embedded in the namespace router's body. Namespace skills are **additive** — every concrete command is still directly invocable.
|
||||
|
||||
The router descriptions use pipe-separated keyword tags (≤ 60 chars) per the Tool Attention research showing keyword-dense tags outperform prose for routing at ~40 % the token cost.
|
||||
|
||||
#### MCP token-budget interaction
|
||||
|
||||
The eager skill listing is one of two recurring per-turn token costs. The other is the MCP tool schema injected by every enabled MCP server in `.claude/settings.json`. Heavyweight MCP servers (browser/playwright, Mac-tools, Windows-tools) can each cost 20 k+ tokens per turn — often dwarfing what `model_profile` tuning saves. The toggle lives in the Claude Code harness (`enabledMcpjsonServers` / `disabledMcpjsonServers` in `.claude/settings.json`) and is **not** a GSD concern. Together, the two-stage routing layer (#2792) and disciplined MCP enablement are the largest cost levers per turn. See [`docs/USER-GUIDE.md`](USER-GUIDE.md) and `references/context-budget.md` for the audit checklist.
|
||||
|
||||
### Workflows (`get-shit-done/workflows/*.md`)
|
||||
|
||||
Orchestration logic that commands reference. Contains the step-by-step process including:
|
||||
|
||||
@@ -250,8 +250,15 @@ node gsd-tools.cjs validate consistency
|
||||
|
||||
# Check .planning/ integrity, optionally repair
|
||||
node gsd-tools.cjs validate health [--repair]
|
||||
|
||||
# Probe context-window utilization for status-line / hook callers (v1.40.0)
|
||||
node gsd-tools.cjs validate context
|
||||
```
|
||||
|
||||
`validate context` emits a structured envelope with `utilization`, `status`
|
||||
(`ok` / `warn` / `critical` at the 60 % / 70 % thresholds), and a
|
||||
`suggestion` string. The same data backs `/gsd-health --context`.
|
||||
|
||||
---
|
||||
|
||||
## Template Commands
|
||||
|
||||
@@ -6,10 +6,29 @@
|
||||
|
||||
## Command Syntax
|
||||
|
||||
- **Claude Code / Gemini / Copilot:** `/gsd-command-name [args]`
|
||||
- **OpenCode / Kilo:** `/gsd-command-name [args]`
|
||||
- **Claude Code / Copilot / OpenCode / Kilo:** `/gsd-command-name [args]` (hyphen form)
|
||||
- **Gemini CLI:** `/gsd:command-name [args]` (colon form — Gemini namespaces commands under `gsd:`)
|
||||
- **Codex:** `$gsd-command-name [args]`
|
||||
|
||||
The hyphen and colon forms are *runtime-specific spellings of the same command*. Whichever runtime you're on, the installer writes the correct form into your runtime's command directory.
|
||||
|
||||
---
|
||||
|
||||
## Namespace Meta-Skills
|
||||
|
||||
Six namespace routers ship as the first-stage entry points in v1.40. They keep the eager skill-listing token cost low (~120 tokens for 6 routers vs ~2,150 for a flat 86-skill listing) while the full surface remains directly invocable. The model selects a namespace, then routes to the concrete sub-skill. See [#2792](https://github.com/gsd-build/get-shit-done/issues/2792).
|
||||
|
||||
| Command | Routes to |
|
||||
|---------|-----------|
|
||||
| `/gsd-ns-workflow` | Phase pipeline — discuss / plan / execute / verify / phase / progress |
|
||||
| `/gsd-ns-project` | Project lifecycle — milestones, audits, summary |
|
||||
| `/gsd-ns-review` | Quality gates — code review, debug, audit, security, eval, ui |
|
||||
| `/gsd-ns-context` | Codebase intelligence — map, graphify, docs, learnings |
|
||||
| `/gsd-ns-manage` | Management — config, workspace, workstreams, thread, update, ship, inbox |
|
||||
| `/gsd-ns-ideate` | Exploration & capture — explore, sketch, spike, spec, capture |
|
||||
|
||||
The namespace skills are **additive** — every existing concrete command (e.g. `/gsd-plan-phase`, `/gsd-code-review --fix`) is still invocable directly.
|
||||
|
||||
---
|
||||
|
||||
## Core Workflow Commands
|
||||
@@ -123,6 +142,8 @@ Research, plan, and verify a phase.
|
||||
| `--auto` | Skip interactive confirmations |
|
||||
| `--research` | Force re-research even if RESEARCH.md exists |
|
||||
| `--skip-research` | Skip domain research step |
|
||||
| `--research-phase <N>` | Research-only mode: spawn researcher for phase `<N>`, write RESEARCH.md, exit before planner. Replaces the deleted `gsd-research-phase` standalone command (#3042). |
|
||||
| `--view` | Research-only modifier: when used with `--research-phase`, print existing RESEARCH.md to stdout and exit (no spawn). |
|
||||
| `--gaps` | Gap closure mode (reads VERIFICATION.md, skips research) |
|
||||
| `--skip-verify` | Skip plan checker verification loop |
|
||||
| `--prd <file>` | Use a PRD file instead of discuss-phase for context |
|
||||
@@ -134,12 +155,20 @@ Research, plan, and verify a phase.
|
||||
**Prerequisites:** `.planning/ROADMAP.md` exists
|
||||
**Produces:** `{phase}-RESEARCH.md`, `{phase}-{N}-PLAN.md`, `{phase}-VALIDATION.md`
|
||||
|
||||
**Research-only mode (`--research-phase <N>`):**
|
||||
- No modifier: prompts `update / view / skip` if RESEARCH.md already exists.
|
||||
- With `--research`: force-refresh — re-spawn researcher unconditionally, no prompt.
|
||||
- With `--view`: print existing RESEARCH.md to stdout, no spawn. Errors if RESEARCH.md missing.
|
||||
|
||||
```bash
|
||||
/gsd-plan-phase 1 # Research + plan + verify phase 1
|
||||
/gsd-plan-phase 3 --skip-research # Plan without research (familiar domain)
|
||||
/gsd-plan-phase --auto # Non-interactive planning
|
||||
/gsd-plan-phase 2 --validate # Validate state before planning
|
||||
/gsd-plan-phase 1 --bounce # Plan + external bounce validation
|
||||
/gsd-plan-phase 1 # Research + plan + verify phase 1
|
||||
/gsd-plan-phase 3 --skip-research # Plan without research (familiar domain)
|
||||
/gsd-plan-phase --auto # Non-interactive planning
|
||||
/gsd-plan-phase 2 --validate # Validate state before planning
|
||||
/gsd-plan-phase 1 --bounce # Plan + external bounce validation
|
||||
/gsd-plan-phase --research-phase 4 # Research only on phase 4 (prompts if RESEARCH.md exists)
|
||||
/gsd-plan-phase --research-phase 4 --view # Print existing RESEARCH.md, no spawn
|
||||
/gsd-plan-phase --research-phase 4 --research # Force-refresh research, no prompt
|
||||
```
|
||||
|
||||
---
|
||||
@@ -689,15 +718,19 @@ Generate a developer behavioral profile from Claude Code session analysis across
|
||||
|
||||
### `/gsd-health`
|
||||
|
||||
Validate `.planning/` directory integrity.
|
||||
Validate `.planning/` directory integrity. With `--context`, probes the
|
||||
context-window utilization guard against the 60 % / 70 % thresholds (added
|
||||
v1.40.0, [#2792](https://github.com/gsd-build/get-shit-done/issues/2792)).
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--repair` | Auto-fix recoverable issues |
|
||||
| `--context` | Probe context-window utilization; warns at 60 %, critical at 70 % |
|
||||
|
||||
```bash
|
||||
/gsd-health # Check integrity
|
||||
/gsd-health --repair # Check and fix
|
||||
/gsd-health --context # Context-utilization triage
|
||||
```
|
||||
|
||||
### `/gsd-cleanup`
|
||||
|
||||
@@ -126,7 +126,7 @@ GSD stores project settings in `.planning/config.json`. Created during `/gsd-new
|
||||
| `dynamic_routing.max_escalations` | integer | `0`, `1`, `2`, … | `1` | Hard cap on retries per agent invocation. Beyond the cap the resolver returns the cap-tier model. Added in v1.40 |
|
||||
| `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_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-config --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 |
|
||||
@@ -143,7 +143,7 @@ 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).
|
||||
Configured interactively via [`/gsd-config --integrations`](COMMANDS.md#gsd-config). These are *connectivity* settings — API keys and cross-tool routing — and are intentionally kept separate from `/gsd-settings` (workflow toggles).
|
||||
|
||||
### Search API keys
|
||||
|
||||
@@ -172,7 +172,7 @@ The `<cli>` slug is validated against `[a-zA-Z0-9_-]+`. Empty or path-containing
|
||||
|
||||
### 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`.
|
||||
`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-config --integrations`.
|
||||
|
||||
---
|
||||
|
||||
@@ -392,6 +392,21 @@ The `features.*` namespace is a dynamic key pattern — new feature flags can be
|
||||
|
||||
---
|
||||
|
||||
## STATE.md Frontmatter (Phase Lifecycle)
|
||||
|
||||
`STATE.md` carries YAML frontmatter that the status-line hook reads on every render. v1.40 adds four optional phase-lifecycle fields read by `parseStateMd()` and rendered by `formatGsdState()`:
|
||||
|
||||
| Field | Type | Purpose |
|
||||
|-------|------|---------|
|
||||
| `active_phase` | string (e.g. `"4.5"`) | Phase number when an orchestrator command is in flight |
|
||||
| `next_action` | string | Recommended next command when idle (`discuss-phase` / `plan-phase` / `execute-phase` / `verify-phase`) |
|
||||
| `next_phases` | YAML flow array | Phases the `next_action` applies to (e.g. `["4.5"]`) |
|
||||
| `progress` | block | Nested `total_phases` / `completed_phases` / `percent` for the milestone progress bar |
|
||||
|
||||
All four fields are **optional and additive** — STATE.md files without them keep rendering exactly as in v1.38.x. See [`STATE-MD-LIFECYCLE.md`](STATE-MD-LIFECYCLE.md) for the full field reference, parser constraints, and rendering scenes.
|
||||
|
||||
---
|
||||
|
||||
## Git Branching
|
||||
|
||||
| Setting | Type | Default | Description |
|
||||
|
||||
@@ -144,6 +144,11 @@
|
||||
- [Agent Size-Budget Enforcement](#119-agent-size-budget-enforcement)
|
||||
- [Shared Boilerplate Extraction](#120-shared-boilerplate-extraction)
|
||||
- [Knowledge Graph Integration](#121-knowledge-graph-integration)
|
||||
- [v1.40.0 Features](#v1400-features)
|
||||
- [Skill Surface Consolidation](#122-skill-surface-consolidation)
|
||||
- [Namespace Meta-Skills (Two-Stage Routing)](#123-namespace-meta-skills-two-stage-routing)
|
||||
- [Context-Window Utilization Guard](#124-context-window-utilization-guard)
|
||||
- [Phase-Lifecycle Status-Line Read-Side](#125-phase-lifecycle-status-line-read-side)
|
||||
- [v1.32 Features](#v132-features)
|
||||
- [STATE.md Consistency Gates](#69-statemd-consistency-gates)
|
||||
- [Autonomous `--to N` Flag](#70-autonomous---to-n-flag)
|
||||
@@ -449,7 +454,6 @@
|
||||
- REQ-MILE-08: New milestone MUST follow same flow as new-project (questions → research → requirements → roadmap)
|
||||
- REQ-MILE-09: New milestone MUST NOT reset existing workflow configuration
|
||||
|
||||
**Gap Closure:** `/gsd-plan-milestone-gaps` creates phases to close gaps identified by audit.
|
||||
|
||||
---
|
||||
|
||||
@@ -865,7 +869,7 @@ continues. Drift detection cannot fail verification.
|
||||
|
||||
### 29. Todo Management
|
||||
|
||||
**Commands:** `/gsd-add-todo [desc]`, `/gsd-check-todos`
|
||||
**Commands:** `/gsd-add-todo [desc]`, `/gsd-capture --list`
|
||||
|
||||
**Purpose:** Capture ideas and tasks during sessions for later work.
|
||||
|
||||
@@ -1630,7 +1634,7 @@ Test suite that scans all agent, workflow, and command files for embedded inject
|
||||
|
||||
### 65. Claim Provenance Tagging
|
||||
|
||||
**Part of:** `/gsd-research-phase`
|
||||
**Part of:** `/gsd-plan-phase --research-phase <N>`
|
||||
|
||||
**Purpose:** Ensure research claims are tagged with source evidence and assumptions are logged separately.
|
||||
|
||||
@@ -2613,3 +2617,83 @@ Users who run a memory / knowledge-base MCP server (for example, ExoCortex-style
|
||||
|
||||
**Configuration:** `graphify.enabled`, `graphify.build_timeout`
|
||||
**Reference files:** `commands/gsd/graphify.md`, `bin/lib/graphify.cjs`
|
||||
|
||||
---
|
||||
|
||||
## v1.40.0 Features
|
||||
|
||||
### 122. Skill Surface Consolidation
|
||||
|
||||
**Purpose:** Cut the eager skill-listing overhead by folding 31 micro-skills into 4 new grouped parents and 6 existing parents that absorb sub-operations as flags. Zero functional loss — every removed micro-skill's behavior survives via a flag on a consolidated parent. After consolidation, `commands/gsd/*.md` ships 59 sub-skills (plus 6 namespace meta-skills, see #123).
|
||||
|
||||
**Requirements:**
|
||||
- REQ-CONSOLIDATE-01: Four new grouped skills replace clusters of micro-skills:
|
||||
- `/gsd-capture` — folds add-todo (default), note (`--note`), add-backlog (`--backlog`), plant-seed (`--seed`), check-todos (`--list`)
|
||||
- `/gsd-phase` — folds add-phase (default), insert-phase (`--insert`), remove-phase (`--remove`), edit-phase (`--edit`)
|
||||
- `/gsd-config` — folds settings-advanced (`--advanced`), settings-integrations (`--integrations`), set-profile (`--profile`)
|
||||
- `/gsd-workspace` — folds new-workspace (`--new`), list-workspaces (`--list`), remove-workspace (`--remove`)
|
||||
- REQ-CONSOLIDATE-02: Six existing parents absorb wrap-up / sub-operations as flags: `/gsd-update --sync`, `/gsd-update --reapply`, `/gsd-sketch --wrap-up`, `/gsd-spike --wrap-up`, `/gsd-map-codebase --fast`, `/gsd-map-codebase --query`, `/gsd-code-review --fix`, `/gsd-progress --do`, `/gsd-progress --next`.
|
||||
- REQ-CONSOLIDATE-03: Deleted micro-skill slash forms (the bare `gsd-add-todo`, `gsd-add-backlog`, `gsd-plant-seed`, `gsd-check-todos`, `gsd-add-phase`, `gsd-insert-phase`, `gsd-remove-phase`, `gsd-edit-phase`, `gsd-new-workspace`, `gsd-list-workspaces`, `gsd-remove-workspace`, `gsd-settings-advanced`, `gsd-settings-integrations`, `gsd-set-profile`, `gsd-sketch-wrap-up`, `gsd-spike-wrap-up`, `gsd-reapply-patches`, `gsd-code-review-fix`, …) MUST resolve to "Unknown command" — no shadow stubs.
|
||||
- REQ-CONSOLIDATE-04: `autonomous.md` invokes `/gsd-code-review --fix` (was previously calling the deleted `gsd-code-review-fix`).
|
||||
|
||||
**Reference issue:** [#2790](https://github.com/gsd-build/get-shit-done/issues/2790)
|
||||
|
||||
---
|
||||
|
||||
### 123. Namespace Meta-Skills (Two-Stage Routing)
|
||||
|
||||
**Purpose:** Replace the flat eager skill listing with a two-stage hierarchical routing layer. The model sees 6 namespace routers instead of 86 entries, selects a namespace, then routes to the sub-skill. Descriptions use pipe-separated keyword tags (≤ 60 chars) for routing density.
|
||||
|
||||
**Commands:**
|
||||
- `/gsd-ns-workflow` — phase pipeline router (discuss / plan / execute / verify / phase / progress)
|
||||
- `/gsd-ns-project` — project lifecycle (milestones, audits, summary)
|
||||
- `/gsd-ns-review` — quality gates (code review, debug, audit, security, eval, ui)
|
||||
- `/gsd-ns-context` — codebase intelligence (map, graphify, docs, learnings)
|
||||
- `/gsd-ns-manage` — config / workspace / workstreams / thread / update / ship / inbox
|
||||
- `/gsd-ns-ideate` — exploration & capture (explore, sketch, spike, spec, capture)
|
||||
|
||||
**Token cost:**
|
||||
|
||||
| | Entries | Approx tokens |
|
||||
|---|---|---|
|
||||
| Pre-1.40 full install | 86 | ~2,150 |
|
||||
| Namespace meta-skills | 6 | ~120 |
|
||||
|
||||
**Requirements:**
|
||||
- REQ-NS-01: Six `commands/gsd/ns-*.md` namespace routers ship with pipe-separated keyword-tag descriptions (≤ 60 chars).
|
||||
- REQ-NS-02: Existing sub-skills are unchanged and still invocable directly — namespace skills are additive, not a replacement for direct slash forms.
|
||||
- REQ-NS-03: The body of each namespace router contains a routing table that maps user intent to the correct concrete sub-skill on the post-#2790 consolidated surface.
|
||||
|
||||
**Reference issue:** [#2792](https://github.com/gsd-build/get-shit-done/issues/2792)
|
||||
|
||||
---
|
||||
|
||||
### 124. Context-Window Utilization Guard
|
||||
|
||||
**Command:** `/gsd-health --context`
|
||||
|
||||
**Purpose:** Quality guard against context-window saturation. Two thresholds: 60 % utilization warns ("consider `/gsd-thread`"), 70 % is critical ("reasoning quality may degrade"; matches the fracture-point per recent context-attention research).
|
||||
|
||||
**Requirements:**
|
||||
- REQ-CTX-GUARD-01: `/gsd-health --context` prints a structured status line with current utilization, threshold tier (`ok` / `warn` / `critical`), and a remediation suggestion.
|
||||
- REQ-CTX-GUARD-02: The same triage is exposed as `gsd-sdk query validate.context --tokens-used <int> --context-window <int>` — a structured envelope for status-line and hook callers (#125). Both flags are required; the handler returns the same `{ percent, state }` envelope as the pure classifier in REQ-CTX-GUARD-03.
|
||||
- REQ-CTX-GUARD-03: The classifier (`bin/lib/context-utilization.cjs`) is pure: input `(tokensUsed, contextWindow)`, output `{ percent, state }`. Easy to unit-test, easy to reuse from any caller.
|
||||
|
||||
**Reference issue:** [#2792](https://github.com/gsd-build/get-shit-done/issues/2792)
|
||||
|
||||
---
|
||||
|
||||
### 125. Phase-Lifecycle Status-Line Read-Side
|
||||
|
||||
**Purpose:** Surface phase orchestration state on the status-line. `parseStateMd()` reads four new STATE.md frontmatter fields and `formatGsdState()` renders in-flight, idle, and progress scenes. Write-side wiring follows in a later RC.
|
||||
|
||||
**Requirements:**
|
||||
- REQ-LIFECYCLE-01: `parseStateMd()` reads four optional fields:
|
||||
- `active_phase` — phase number when an orchestrator is in flight
|
||||
- `next_action` — recommended next command when idle
|
||||
- `next_phases` — YAML flow array of next phase numbers
|
||||
- `progress` — nested `total_phases` / `completed_phases` / `percent` block
|
||||
- REQ-LIFECYCLE-02: `formatGsdState()` checks the lifecycle fields in priority order and emits the first matching scene (Phase active → Idle next-recommended → Milestone complete → Default fallback).
|
||||
- REQ-LIFECYCLE-03: All four fields default to undefined; existing STATE.md files render byte-for-byte identically.
|
||||
|
||||
**Reference issue:** [#2833](https://github.com/gsd-build/get-shit-done/issues/2833) — see [`docs/STATE-MD-LIFECYCLE.md`](STATE-MD-LIFECYCLE.md) for the full field reference and rendering rules.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"generated": "2026-05-02",
|
||||
"generated": "2026-05-03",
|
||||
"families": {
|
||||
"agents": [
|
||||
"gsd-advisor-researcher",
|
||||
@@ -163,7 +163,6 @@
|
||||
"reapply-patches.md",
|
||||
"remove-phase.md",
|
||||
"remove-workspace.md",
|
||||
"research-phase.md",
|
||||
"resume-project.md",
|
||||
"review.md",
|
||||
"scan.md",
|
||||
|
||||
@@ -162,13 +162,13 @@ These six routers are descriptor-only entries that the model picks first; the bo
|
||||
|
||||
---
|
||||
|
||||
## Workflows (85 shipped)
|
||||
## Workflows (84 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.
|
||||
|
||||
| Workflow | Role | Invoked by |
|
||||
|----------|------|------------|
|
||||
| `add-phase.md` | Add a new integer phase to the end of the current milestone in the roadmap. | `/gsd-add-phase` |
|
||||
| `add-phase.md` | Add a new integer phase to the end of the current milestone in the roadmap. | `/gsd-phase` (default) |
|
||||
| `add-tests.md` | Generate unit and E2E tests for a completed phase based on its artifacts. | `/gsd-add-tests` |
|
||||
| `add-todo.md` | Capture an idea or task that surfaces during a session as a structured todo. | `/gsd-capture` (default), `/gsd-capture --backlog` |
|
||||
| `ai-integration-phase.md` | Orchestrate framework selection → AI research → domain research → eval planning into AI-SPEC.md. | `/gsd-ai-integration-phase` |
|
||||
@@ -203,20 +203,19 @@ Full roster at `get-shit-done/workflows/*.md`. Workflows are thin orchestrators
|
||||
| `import.md` | Ingest external plans with conflict detection against existing project decisions. | `/gsd-import` |
|
||||
| `inbox.md` | Triage open GitHub issues and PRs against project contribution templates. | `/gsd-inbox` |
|
||||
| `ingest-docs.md` | Scan a repo for mixed planning docs; classify, synthesize, and bootstrap or merge into `.planning/` with a conflicts report. | `/gsd-ingest-docs` |
|
||||
| `insert-phase.md` | Insert a decimal phase for urgent work discovered mid-milestone. | `/gsd-insert-phase` |
|
||||
| `insert-phase.md` | Insert a decimal phase for urgent work discovered mid-milestone. | `/gsd-phase --insert` |
|
||||
| `list-phase-assumptions.md` | Surface Claude's assumptions about a phase before planning. | `/gsd-list-phase-assumptions` |
|
||||
| `list-workspaces.md` | List all GSD workspaces found in `~/gsd-workspaces/` with their status. | `/gsd-list-workspaces` |
|
||||
| `list-workspaces.md` | List all GSD workspaces found in `~/gsd-workspaces/` with their status. | `/gsd-workspace --list` |
|
||||
| `manager.md` | Interactive milestone command center — dashboard, inline discuss, background plan/execute. | `/gsd-manager` |
|
||||
| `map-codebase.md` | Orchestrate parallel codebase mapper agents to produce `.planning/codebase/` docs. | `/gsd-map-codebase` |
|
||||
| `milestone-summary.md` | Milestone summary synthesis — onboarding and review artifact from milestone artifacts. | `/gsd-milestone-summary` |
|
||||
| `new-milestone.md` | Start a new milestone cycle — load project context, gather goals, update PROJECT.md/STATE.md. | `/gsd-new-milestone` |
|
||||
| `new-project.md` | Unified new-project flow — questioning, research (optional), requirements, roadmap. | `/gsd-new-project` |
|
||||
| `new-workspace.md` | Create an isolated workspace with repo worktrees/clones and an independent `.planning/`. | `/gsd-new-workspace` |
|
||||
| `new-workspace.md` | Create an isolated workspace with repo worktrees/clones and an independent `.planning/`. | `/gsd-workspace --new` |
|
||||
| `next.md` | Detect current project state and automatically advance to the next logical step. | `/gsd-progress --next` |
|
||||
| `node-repair.md` | Autonomous repair operator for failed task verification; invoked by `execute-plan`. | `execute-plan.md` (recovery) |
|
||||
| `note.md` | Zero-friction idea capture — one Write call, one confirmation line. | `/gsd-capture --note` |
|
||||
| `pause-work.md` | Create structured `.planning/HANDOFF.json` and `.continue-here.md` handoff files. | `/gsd-pause-work` |
|
||||
| `plan-milestone-gaps.md` | Create all phases necessary to close gaps identified by `/gsd-audit-milestone`. | `/gsd-plan-milestone-gaps` |
|
||||
| `plan-phase.md` | Create executable PLAN.md files with integrated research and verification loop. | `/gsd-plan-phase`, `/gsd-quick` |
|
||||
| `plan-review-convergence.md` | Cross-AI plan convergence loop — replan with review feedback until no HIGH concerns remain. | `/gsd-plan-review-convergence` |
|
||||
| `plant-seed.md` | Capture a forward-looking idea as a structured seed file with trigger conditions. | `/gsd-capture --seed` |
|
||||
@@ -225,23 +224,22 @@ Full roster at `get-shit-done/workflows/*.md`. Workflows are thin orchestrators
|
||||
| `progress.md` | Progress rendering — project context, position, and next-action routing. | `/gsd-progress` |
|
||||
| `quick.md` | Quick-task execution with GSD guarantees (atomic commits, state tracking). | `/gsd-quick` |
|
||||
| `reapply-patches.md` | Reapply local modifications after a GSD update. | `/gsd-update --reapply` |
|
||||
| `remove-phase.md` | Remove a future phase from the roadmap and renumber subsequent phases. | `/gsd-remove-phase` |
|
||||
| `remove-workspace.md` | Remove a GSD workspace and clean up worktrees. | `/gsd-remove-workspace` |
|
||||
| `research-phase.md` | Standalone phase research workflow (usually invoked via `plan-phase`). | `/gsd-research-phase` |
|
||||
| `remove-phase.md` | Remove a future phase from the roadmap and renumber subsequent phases. | `/gsd-phase --remove` |
|
||||
| `remove-workspace.md` | Remove a GSD workspace and clean up worktrees. | `/gsd-workspace --remove` |
|
||||
| `resume-project.md` | Resume work — restore full context from STATE.md, HANDOFF.json, and artifacts. | `/gsd-resume-work` |
|
||||
| `review.md` | Cross-AI plan review via external CLIs; produces REVIEWS.md. | `/gsd-review` |
|
||||
| `scan.md` | Rapid single-focus codebase scan — lightweight alternative to map-codebase. | `/gsd-scan` |
|
||||
| `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` |
|
||||
| `settings.md` | Configure GSD workflow toggles and model profile. | `/gsd-settings`, `/gsd-config --profile` |
|
||||
| `settings-advanced.md` | Configure GSD power-user knobs — plan bounce, timeouts, branch templates, cross-AI execution, runtime knobs. | `/gsd-config --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-config --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` |
|
||||
| `sketch-wrap-up.md` | Curate sketch findings and package them as a persistent `sketch-findings-[project]` skill. | `/gsd-sketch --wrap-up` |
|
||||
| `spec-phase.md` | Socratic spec refinement with ambiguity scoring; produces SPEC.md. | `/gsd-spec-phase` |
|
||||
| `spike.md` | Rapid feasibility validation through focused, throwaway experiments. | `/gsd-spike` |
|
||||
| `spike-wrap-up.md` | Curate spike findings and package them as a persistent `spike-findings-[project]` skill. | `/gsd-spike-wrap-up` |
|
||||
| `spike-wrap-up.md` | Curate spike findings and package them as a persistent `spike-findings-[project]` skill. | `/gsd-spike --wrap-up` |
|
||||
| `stats.md` | Project statistics rendering — phases, plans, requirements, git metrics. | `/gsd-stats` |
|
||||
| `sync-skills.md` | Cross-runtime GSD skill sync — diff and apply `gsd-*` skill directories across runtime roots. | `/gsd-update --sync` |
|
||||
| `transition.md` | Phase-boundary transition workflow — workstream checks, state advancement. | `execute-phase.md`, `/gsd-progress --next` |
|
||||
@@ -385,7 +383,7 @@ Full listing: `get-shit-done/bin/lib/*.cjs`.
|
||||
| `roadmap-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools roadmap` |
|
||||
| `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 |
|
||||
| `secrets.cjs` | Secret-config masking convention (`****<last-4>`) for integration keys managed by `/gsd-config --integrations` — keeps plaintext out of `config-set` output |
|
||||
| `security.cjs` | Path traversal prevention, prompt injection detection, safe JSON/shell helpers |
|
||||
| `state-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools state` |
|
||||
| `state.cjs` | STATE.md parsing, updating, progression, metrics |
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# STATE.md Phase Lifecycle Frontmatter
|
||||
|
||||
> **Status:** Reference for the phase-lifecycle status-line proposed in
|
||||
> [issue #2833](https://github.com/gsd-build/get-shit-done/issues/2833).
|
||||
> The status-line hook (`hooks/gsd-statusline.js`) reads the fields below;
|
||||
> SDK write-side support to maintain them is tracked separately.
|
||||
> **Status:** Read-side shipped in v1.40.0 (issue
|
||||
> [#2833](https://github.com/gsd-build/get-shit-done/issues/2833)).
|
||||
> `parseStateMd()` reads the four frontmatter fields below and
|
||||
> `formatGsdState()` renders the in-flight / idle / progress scenes.
|
||||
> SDK write-side support to maintain the fields automatically is tracked
|
||||
> separately.
|
||||
|
||||
GSD's `STATE.md` carries YAML frontmatter that the status-line hook reads on
|
||||
every render. This document describes the **phase-lifecycle fields** and the
|
||||
|
||||
@@ -25,6 +25,32 @@ execute → verify → review → ship loop using existing GSD primitives.
|
||||
|
||||
---
|
||||
|
||||
## Slash-command forms (hyphen vs colon)
|
||||
|
||||
GSD ships **the same set of skills** to every supported runtime, but two slash-form spellings are in play:
|
||||
|
||||
- **Hyphen form** — `/gsd-command-name` — used by Claude Code, Copilot, OpenCode, Kilo, Cursor, Windsurf, Augment, Antigravity, and Trae.
|
||||
- **Colon form** — `/gsd:command-name` — used by **Gemini CLI only**. Gemini namespaces every plugin's commands under the plugin id, so the install path rewrites every body-text reference and command file to the colon form during `--gemini` install.
|
||||
|
||||
You don't need to choose — the installer writes the correct form into the command directory of each runtime you target. When following a walkthrough on a Gemini terminal, replace the hyphen after `gsd` with a colon as you read each slash command.
|
||||
|
||||
## Namespace routing primer (`gsd:<namespace>`, v1.40)
|
||||
|
||||
v1.40 ships six **namespace meta-skills** as the first-stage entry points for hierarchical routing — they keep the eager skill-listing token cost low (~120 tokens for 6 routers vs ~2,150 for a flat 86-skill listing) while every concrete sub-skill remains directly invocable. Each namespace router's body contains a routing table that maps your intent to the correct concrete sub-skill.
|
||||
|
||||
| Namespace | Router | Routes to |
|
||||
|-----------|--------|-----------|
|
||||
| Phase pipeline | `/gsd-ns-workflow` | discuss / plan / execute / verify / phase / progress |
|
||||
| Project lifecycle | `/gsd-ns-project` | milestones, audits, summary |
|
||||
| Quality gates | `/gsd-ns-review` | code review, debug, audit, security, eval, ui |
|
||||
| Codebase intelligence | `/gsd-ns-context` | map, graphify, docs, learnings |
|
||||
| Management | `/gsd-ns-manage` | config, workspace, workstreams, thread, update, ship, inbox |
|
||||
| Exploration & capture | `/gsd-ns-ideate` | explore, sketch, spike, spec, capture |
|
||||
|
||||
You almost never need to type a namespace router yourself. Their value is in the routing layer the model uses to discover the right sub-skill — they exist so the system prompt can list 6 entries instead of 86. If you already know the concrete command (e.g. `/gsd-plan-phase`), call it directly.
|
||||
|
||||
---
|
||||
|
||||
## End-to-End Walkthrough
|
||||
|
||||
This walkthrough shows how GSD phases connect for a typical single-phase project — a small Node.js REST API that validates webhook signatures. Follow it to understand what each command does, what it creates, and how the next command consumes it.
|
||||
@@ -571,7 +597,7 @@ Each spike runs 2–5 experiments. Every experiment has:
|
||||
|
||||
Results land in `.planning/spikes/NNN-name/README.md` and are indexed in `.planning/spikes/MANIFEST.md`.
|
||||
|
||||
Once you have signal, run `/gsd-spike-wrap-up` to package the findings into `.claude/skills/spike-findings-[project]/` — future sessions will load them automatically via project-skills discovery.
|
||||
Once you have signal, run `/gsd-spike --wrap-up` to package the findings into `.claude/skills/spike-findings-[project]/` — future sessions will load them automatically via project-skills discovery.
|
||||
|
||||
### When to Sketch
|
||||
|
||||
@@ -586,16 +612,16 @@ Sketch when you need to compare layout structures, interaction models, or visual
|
||||
|
||||
Each sketch answers **one design question** with 2–3 variants in a single `index.html` you open directly in a browser — no build step. Variants use tab navigation and shared CSS variables from `themes/default.css`. All interactive elements (hover, click, transitions) are functional.
|
||||
|
||||
After picking a winner, run `/gsd-sketch-wrap-up` to capture the visual decisions into `.claude/skills/sketch-findings-[project]/`.
|
||||
After picking a winner, run `/gsd-sketch --wrap-up` to capture the visual decisions into `.claude/skills/sketch-findings-[project]/`.
|
||||
|
||||
### Spike → Sketch → Phase Flow
|
||||
|
||||
```
|
||||
/gsd-spike "SSE vs WebSocket" # Validate the approach
|
||||
/gsd-spike-wrap-up # Package learnings
|
||||
/gsd-spike --wrap-up # Package learnings
|
||||
|
||||
/gsd-sketch "real-time feed UI" # Explore the design
|
||||
/gsd-sketch-wrap-up # Package decisions
|
||||
/gsd-sketch --wrap-up # Package decisions
|
||||
|
||||
/gsd-discuss-phase N # Lock in preferences (now informed by spike + sketch)
|
||||
/gsd-plan-phase N # Plan with confidence
|
||||
@@ -610,8 +636,8 @@ After picking a winner, run `/gsd-sketch-wrap-up` to capture the visual decision
|
||||
Ideas that aren't ready for active planning go into the backlog using 999.x numbering, keeping them outside the active phase sequence.
|
||||
|
||||
```
|
||||
/gsd-add-backlog "GraphQL API layer" # Creates 999.1-graphql-api-layer/
|
||||
/gsd-add-backlog "Mobile responsive" # Creates 999.2-mobile-responsive/
|
||||
/gsd-capture --backlog "GraphQL API layer" # Creates 999.1-graphql-api-layer/
|
||||
/gsd-capture --backlog "Mobile responsive" # Creates 999.2-mobile-responsive/
|
||||
```
|
||||
|
||||
Backlog items get full phase directories, so you can use `/gsd-discuss-phase 999.1` to explore an idea further or `/gsd-plan-phase 999.1` when it's ready.
|
||||
@@ -623,7 +649,7 @@ Backlog items get full phase directories, so you can use `/gsd-discuss-phase 999
|
||||
Seeds are forward-looking ideas with trigger conditions. Unlike backlog items, seeds surface automatically when the right milestone arrives.
|
||||
|
||||
```
|
||||
/gsd-plant-seed "Add real-time collab when WebSocket infra is in place"
|
||||
/gsd-capture --seed "Add real-time collab when WebSocket infra is in place"
|
||||
```
|
||||
|
||||
Seeds preserve the full WHY and WHEN to surface. `/gsd-new-milestone` scans all seeds and presents matches.
|
||||
@@ -642,7 +668,7 @@ Threads are lightweight cross-session knowledge stores for work that spans multi
|
||||
|
||||
Threads are lighter weight than `/gsd-pause-work` — no phase state, no plan context. Each thread file includes Goal, Context, References, and Next Steps sections.
|
||||
|
||||
Threads can be promoted to phases (`/gsd-add-phase`) or backlog items (`/gsd-add-backlog`) when they mature.
|
||||
Threads can be promoted to phases (`/gsd-phase`) or backlog items (`/gsd-capture --backlog`) when they mature.
|
||||
|
||||
**Storage:** `.planning/threads/{slug}.md`
|
||||
|
||||
@@ -669,7 +695,7 @@ Workstreams let you work on multiple milestone areas concurrently without state
|
||||
|
||||
Each workstream maintains its own `.planning/` directory subtree. When you switch workstreams, GSD swaps the active planning context so that `/gsd-progress`, `/gsd-discuss-phase`, `/gsd-plan-phase`, and other commands operate on that workstream's state. Active context is session-scoped when the runtime exposes a stable session identifier, which prevents one terminal or AI instance from repointing another instance's `STATE.md`.
|
||||
|
||||
This is lighter weight than `/gsd-new-workspace` (which creates separate repo worktrees). Workstreams share the same codebase and git history but isolate planning artifacts.
|
||||
This is lighter weight than `/gsd-workspace --new` (which creates separate repo worktrees). Workstreams share the same codebase and git history but isolate planning artifacts.
|
||||
|
||||
---
|
||||
|
||||
@@ -900,7 +926,6 @@ The gate is non-blocking: any internal failure logs and the phase continues.
|
||||
|
||||
```bash
|
||||
/gsd-audit-milestone # Check requirements coverage, detect stubs
|
||||
/gsd-plan-milestone-gaps # If audit found gaps, create phases to close them
|
||||
/gsd-complete-milestone # Archive, tag, done
|
||||
```
|
||||
|
||||
@@ -919,11 +944,13 @@ The gate is non-blocking: any internal failure logs and the phase continues.
|
||||
### Mid-Milestone Scope Changes
|
||||
|
||||
```bash
|
||||
/gsd-add-phase # Append a new phase to the roadmap
|
||||
/gsd-phase # Append a new phase to the roadmap (default mode)
|
||||
# or
|
||||
/gsd-insert-phase 3 # Insert urgent work between phases 3 and 4
|
||||
/gsd-phase --insert 3 # Insert urgent work between phases 3 and 4
|
||||
# or
|
||||
/gsd-remove-phase 7 # Descope phase 7 and renumber
|
||||
/gsd-phase --remove 7 # Descope phase 7 and renumber
|
||||
# or
|
||||
/gsd-phase --edit 4 # Edit any field of phase 4 in place
|
||||
```
|
||||
|
||||
### Multi-Project Workspaces
|
||||
@@ -932,18 +959,18 @@ Work on multiple repos or features in parallel with isolated GSD state.
|
||||
|
||||
```bash
|
||||
# Create a workspace with repos from your monorepo
|
||||
/gsd-new-workspace --name feature-b --repos hr-ui,ZeymoAPI
|
||||
/gsd-workspace --new --name feature-b --repos hr-ui,ZeymoAPI
|
||||
|
||||
# Feature branch isolation — worktree of current repo with its own .planning/
|
||||
/gsd-new-workspace --name feature-b --repos .
|
||||
/gsd-workspace --new --name feature-b --repos .
|
||||
|
||||
# Then cd into the workspace and initialize GSD
|
||||
cd ~/gsd-workspaces/feature-b
|
||||
/gsd-new-project
|
||||
|
||||
# List and manage workspaces
|
||||
/gsd-list-workspaces
|
||||
/gsd-remove-workspace feature-b
|
||||
/gsd-workspace --list
|
||||
/gsd-workspace --remove feature-b
|
||||
```
|
||||
|
||||
Each workspace gets:
|
||||
@@ -1015,7 +1042,7 @@ Do not re-run `/gsd-execute-phase`. Use `/gsd-quick` for targeted fixes, or `/gs
|
||||
|
||||
### Model Costs Too High
|
||||
|
||||
Switch to budget profile: `/gsd-set-profile budget`. Disable research and plan-check agents via `/gsd-settings` if the domain is familiar to you (or to Claude).
|
||||
Switch to budget profile: `/gsd-config --profile budget`. Disable research and plan-check agents via `/gsd-settings` if the domain is familiar to you (or to Claude).
|
||||
|
||||
### Tuning model cost by phase (`models`) — added in v1.40
|
||||
|
||||
@@ -1175,7 +1202,7 @@ Skills are installed to `~/.qwen/skills/gsd-*/SKILL.md`. Use the `QWEN_CONFIG_DI
|
||||
|
||||
### Using Claude Code with Non-Anthropic Providers (OpenRouter, Local)
|
||||
|
||||
If GSD subagents call Anthropic models and you're paying through OpenRouter or a local provider, switch to the `inherit` profile: `/gsd-set-profile inherit`. This makes all agents use your current session model instead of specific Anthropic models. See also `/gsd-settings` → Model Profile → Inherit.
|
||||
If GSD subagents call Anthropic models and you're paying through OpenRouter or a local provider, switch to the `inherit` profile: `/gsd-config --profile inherit`. This makes all agents use your current session model instead of specific Anthropic models. See also `/gsd-settings` → Model Profile → Inherit.
|
||||
|
||||
### Working on a Sensitive/Private Project
|
||||
|
||||
@@ -1358,14 +1385,13 @@ If the installer crashes with `EPERM: operation not permitted, scandir` on Windo
|
||||
| ------------------------------------ | ------------------------------------------------------------------------ |
|
||||
| Lost context / new session | `/gsd-resume-work` or `/gsd-progress` |
|
||||
| Phase went wrong | `git revert` the phase commits, then re-plan |
|
||||
| Need to change scope | `/gsd-add-phase`, `/gsd-insert-phase`, or `/gsd-remove-phase` |
|
||||
| Milestone audit found gaps | `/gsd-plan-milestone-gaps` |
|
||||
| Need to change scope | `/gsd-phase` (default), `/gsd-phase --insert`, or `/gsd-phase --remove` |
|
||||
| Something broke | `/gsd-debug "description"` (add `--diagnose` for analysis without fixes) |
|
||||
| STATE.md out of sync | `state validate` then `state sync` |
|
||||
| Workflow state seems corrupted | `/gsd-forensics` |
|
||||
| Quick targeted fix | `/gsd-quick` |
|
||||
| Plan doesn't match your vision | `/gsd-discuss-phase [N]` then re-plan |
|
||||
| Costs running high | `/gsd-set-profile budget` and `/gsd-settings` to toggle agents off |
|
||||
| Costs running high | `/gsd-config --profile budget` and `/gsd-settings` to toggle agents off |
|
||||
| Update broke local changes | `/gsd-update --reapply` |
|
||||
| Want session summary for stakeholder | `/gsd-session-report` |
|
||||
| Don't know what step is next | `/gsd-next` |
|
||||
|
||||
26
docs/adr/0001-dispatch-policy-module.md
Normal file
26
docs/adr/0001-dispatch-policy-module.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Dispatch policy module as single seam for query execution outcomes
|
||||
|
||||
We decided to centralize query dispatch outcomes in one Dispatch Policy Module that returns a structured union result (`ok` success or failure with typed `kind`, `details`, and final `exit_code`) instead of mixing throws and ad-hoc error mapping across CLI and SDK paths. This keeps fallback policy, timeout classification, and exit mapping in one place for better locality, prevents drift between native and fallback behavior, and makes callers thin adapters over a stable interface.
|
||||
|
||||
## Amendment (2026-05-03): query seam deepening completion
|
||||
|
||||
To complete the query architecture pass, we deepened adjacent seams around the Dispatch Policy Module:
|
||||
|
||||
- Extracted **Query Runtime Context Module** to own `projectDir` + `ws` resolution policy.
|
||||
- Extracted **Native Dispatch Adapter Module** so Dispatch Policy consumes a stable native dispatch Interface (not closure-wired call sites).
|
||||
- Extracted **Query CLI Output Module** to own projection from dispatch results/errors to CLI output contract.
|
||||
- Converged internal command-resolution and policy imports onto canonical modules and removed dead wrapper modules.
|
||||
- Added **Command Topology Module** as dispatch-facing seam that resolves commands, projects command policy, binds handler Adapters, and emits no-match diagnosis consumed by Dispatch Policy.
|
||||
- Locked **pre-project query config policy** for parity-sensitive query Interfaces: when `.planning/config.json` is absent, use built-in defaults and parity-aligned empty model ids for model-resolution surfaces.
|
||||
- Gated real-CLI SDK E2E suites behind explicit opt-in (`GSD_ENABLE_E2E=1`) to keep default CI/local verification deterministic while preserving full-path validation when requested.
|
||||
|
||||
### Dead-wrapper convergence
|
||||
|
||||
Removed wrapper Modules after call-site convergence:
|
||||
- `normalize-query-command.ts`
|
||||
- `command-resolution.ts`
|
||||
- `policy-convergence.ts`
|
||||
- `query-policy-snapshot.ts`
|
||||
- `query-registry-capability.ts`
|
||||
|
||||
This amendment preserves the original ADR direction: keep policy depth high, adapters thin, and locality concentrated in explicit modules.
|
||||
@@ -23,7 +23,7 @@ gates.
|
||||
## Why this exists
|
||||
|
||||
GSD has the building blocks for issue-driven AI development —
|
||||
`/gsd-new-workspace`, `/gsd-manager`, `/gsd-autonomous`, `/gsd-verify-work`,
|
||||
`/gsd-workspace --new`, `/gsd-manager`, `/gsd-autonomous`, `/gsd-verify-work`,
|
||||
`/gsd-review`, `/gsd-ship`, plus `STATE.md` and the phase artifact suite
|
||||
— but no guide that walks through how to drive them from a single tracker
|
||||
issue without writing custom orchestration scripts. Without that guide
|
||||
@@ -47,7 +47,7 @@ Symphony docs, blog posts, or third-party orchestration write-ups.
|
||||
| Symphony concept | GSD primitive |
|
||||
|---|---|
|
||||
| `WORKFLOW.md` (top-level intent) | `ROADMAP.md` (project intent), `STATE.md` (live status), phase `CONTEXT.md` (per-phase scope), phase `PLAN.md` (executable steps) |
|
||||
| One isolated agent workspace per task | `/gsd-new-workspace --strategy worktree` |
|
||||
| One isolated agent workspace per task | `/gsd-workspace --new --strategy worktree` |
|
||||
| Agent dispatch and concurrency | `/gsd-manager` (interactive dashboard), `/gsd-autonomous` (unattended) |
|
||||
| Per-phase plan and discuss steps | `/gsd-discuss-phase` → `/gsd-plan-phase` → `/gsd-execute-phase` |
|
||||
| Proof-of-work / test evidence | `/gsd-verify-work` (UAT.md persisted across `/clear`) |
|
||||
@@ -76,7 +76,7 @@ tracker issue end-to-end. Replace bracketed placeholders before running.
|
||||
`/gsd-insert-phase`. Capture the tracker issue URL in the phase's
|
||||
`CONTEXT.md` so traceability survives compaction.
|
||||
3. **Create an isolated workspace.** Run
|
||||
`/gsd-new-workspace --strategy worktree <slug>` to spin up a git
|
||||
`/gsd-workspace --new --strategy worktree <slug>` to spin up a git
|
||||
worktree with an independent `.planning/` directory. The worktree is
|
||||
the safety boundary: any exploration, partial commits, or aborted
|
||||
plans stay outside `main`.
|
||||
@@ -110,7 +110,7 @@ When the PR merges, the loop closes. Auto-close keywords in the PR body
|
||||
|
||||
The loop is safe because four invariants hold by construction:
|
||||
|
||||
- **Isolated worktrees.** Every issue runs in a `/gsd-new-workspace`
|
||||
- **Isolated worktrees.** Every issue runs in a `/gsd-workspace --new`
|
||||
worktree, so partial work, aborted plans, and exploratory commits
|
||||
never touch `main`. `gsd-local-patches/` is the recovery surface if a
|
||||
worktree's hand-edits need to come back across an update.
|
||||
|
||||
@@ -411,7 +411,7 @@ UI-SPEC.md (per phase) ───────────────────
|
||||
│ ├── pending/ # キャプチャされたアイデア
|
||||
│ └── done/ # 完了済みtodo
|
||||
├── threads/ # 永続コンテキストスレッド(/gsd-thread から)
|
||||
├── seeds/ # 将来に向けたアイデア(/gsd-plant-seed から)
|
||||
├── seeds/ # 将来に向けたアイデア(/gsd-capture --seed から)
|
||||
├── debug/ # アクティブなデバッグセッション
|
||||
│ ├── *.md # アクティブセッション
|
||||
│ ├── resolved/ # アーカイブ済みセッション
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
---
|
||||
|
||||
### `/gsd-new-workspace`
|
||||
### `/gsd-workspace --new`
|
||||
|
||||
リポジトリのコピーと独立した `.planning/` ディレクトリを持つ分離されたワークスペースを作成します。
|
||||
|
||||
@@ -52,14 +52,14 @@
|
||||
**生成物:** `WORKSPACE.md`、`.planning/`、リポジトリコピー(worktreeまたはclone)
|
||||
|
||||
```bash
|
||||
/gsd-new-workspace --name feature-b --repos hr-ui,ZeymoAPI
|
||||
/gsd-new-workspace --name feature-b --repos . --strategy worktree # 同一リポジトリの分離
|
||||
/gsd-new-workspace --name spike --repos api,web --strategy clone # フルクローン
|
||||
/gsd-workspace --new --name feature-b --repos hr-ui,ZeymoAPI
|
||||
/gsd-workspace --new --name feature-b --repos . --strategy worktree # 同一リポジトリの分離
|
||||
/gsd-workspace --new --name spike --repos api,web --strategy clone # フルクローン
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `/gsd-list-workspaces`
|
||||
### `/gsd-workspace --list`
|
||||
|
||||
アクティブなGSDワークスペースとそのステータスを一覧表示します。
|
||||
|
||||
@@ -67,12 +67,12 @@
|
||||
**表示内容:** 名前、リポジトリ数、戦略、GSDプロジェクトのステータス
|
||||
|
||||
```bash
|
||||
/gsd-list-workspaces
|
||||
/gsd-workspace --list
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `/gsd-remove-workspace`
|
||||
### `/gsd-workspace --remove`
|
||||
|
||||
ワークスペースを削除し、git worktreeをクリーンアップします。
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
**安全性:** コミットされていない変更があるリポジトリの削除を拒否します。名前の確認が必要です。
|
||||
|
||||
```bash
|
||||
/gsd-remove-workspace feature-b
|
||||
/gsd-workspace --remove feature-b
|
||||
```
|
||||
|
||||
---
|
||||
@@ -368,15 +368,15 @@
|
||||
|
||||
## フェーズ管理コマンド
|
||||
|
||||
### `/gsd-add-phase`
|
||||
### `/gsd-phase`
|
||||
|
||||
ロードマップに新しいフェーズを追加します。
|
||||
|
||||
```bash
|
||||
/gsd-add-phase # 対話型 — フェーズの説明を入力
|
||||
/gsd-phase # 対話型 — フェーズの説明を入力
|
||||
```
|
||||
|
||||
### `/gsd-insert-phase`
|
||||
### `/gsd-phase --insert`
|
||||
|
||||
小数番号を使用して、フェーズ間に緊急の作業を挿入します。
|
||||
|
||||
@@ -385,10 +385,10 @@
|
||||
| `N` | いいえ | このフェーズ番号の後に挿入 |
|
||||
|
||||
```bash
|
||||
/gsd-insert-phase 3 # フェーズ3と4の間に挿入 → 3.1を作成
|
||||
/gsd-phase --insert 3 # フェーズ3と4の間に挿入 → 3.1を作成
|
||||
```
|
||||
|
||||
### `/gsd-remove-phase`
|
||||
### `/gsd-phase --remove`
|
||||
|
||||
将来のフェーズを削除し、後続のフェーズの番号を振り直します。
|
||||
|
||||
@@ -397,7 +397,7 @@
|
||||
| `N` | いいえ | 削除するフェーズ番号 |
|
||||
|
||||
```bash
|
||||
/gsd-remove-phase 7 # フェーズ7を削除、8→7、9→8等に番号振り直し
|
||||
/gsd-phase --remove 7 # フェーズ7を削除、8→7、9→8等に番号振り直し
|
||||
```
|
||||
|
||||
### `/gsd-list-phase-assumptions`
|
||||
@@ -412,15 +412,8 @@
|
||||
/gsd-list-phase-assumptions 2 # フェーズ2の前提を確認
|
||||
```
|
||||
|
||||
### `/gsd-plan-milestone-gaps`
|
||||
|
||||
マイルストーン監査のギャップを解消するフェーズを作成します。
|
||||
|
||||
```bash
|
||||
/gsd-plan-milestone-gaps # 各監査ギャップに対してフェーズを作成
|
||||
```
|
||||
|
||||
### `/gsd-research-phase`
|
||||
### `/gsd-plan-phase --research-phase`
|
||||
|
||||
詳細なエコシステム調査のみを実行します(単体機能 — 通常は `/gsd-plan-phase` を使用してください)。
|
||||
|
||||
@@ -429,7 +422,7 @@
|
||||
| `N` | いいえ | フェーズ番号 |
|
||||
|
||||
```bash
|
||||
/gsd-research-phase 4 # フェーズ4のドメインを調査
|
||||
/gsd-plan-phase --research-phase 4 # フェーズ4のドメインを調査
|
||||
```
|
||||
|
||||
### `/gsd-validate-phase`
|
||||
@@ -598,7 +591,7 @@ GSDの保証付きでアドホックタスクを実行します。
|
||||
/gsd-debug --diagnose "API returning 500 on /users endpoint"
|
||||
```
|
||||
|
||||
### `/gsd-add-todo`
|
||||
### `/gsd-capture`
|
||||
|
||||
後で取り組むアイデアやタスクをキャプチャします。
|
||||
|
||||
@@ -607,15 +600,15 @@ GSDの保証付きでアドホックタスクを実行します。
|
||||
| `description` | いいえ | Todoの説明 |
|
||||
|
||||
```bash
|
||||
/gsd-add-todo "Consider adding dark mode support"
|
||||
/gsd-capture "Consider adding dark mode support"
|
||||
```
|
||||
|
||||
### `/gsd-check-todos`
|
||||
### `/gsd-capture --list`
|
||||
|
||||
保留中のTodoを一覧表示し、取り組むものを選択します。
|
||||
|
||||
```bash
|
||||
/gsd-check-todos
|
||||
/gsd-capture --list
|
||||
```
|
||||
|
||||
### `/gsd-add-tests`
|
||||
@@ -752,7 +745,7 @@ Claude Codeのセッション分析から8つの次元(コミュニケーシ
|
||||
/gsd-settings # 対話型設定
|
||||
```
|
||||
|
||||
### `/gsd-set-profile`
|
||||
### `/gsd-config --profile`
|
||||
|
||||
クイックプロファイル切り替え。
|
||||
|
||||
@@ -761,8 +754,8 @@ Claude Codeのセッション分析から8つの次元(コミュニケーシ
|
||||
| `profile` | **はい** | `quality`、`balanced`、`budget`、または `inherit` |
|
||||
|
||||
```bash
|
||||
/gsd-set-profile budget # budgetプロファイルに切り替え
|
||||
/gsd-set-profile quality # qualityプロファイルに切り替え
|
||||
/gsd-config --profile budget # budgetプロファイルに切り替え
|
||||
/gsd-config --profile quality # qualityプロファイルに切り替え
|
||||
```
|
||||
|
||||
---
|
||||
@@ -885,7 +878,7 @@ GSDアップデート後にローカルの変更を復元します。
|
||||
|
||||
## バックログ&スレッドコマンド
|
||||
|
||||
### `/gsd-add-backlog`
|
||||
### `/gsd-capture --backlog`
|
||||
|
||||
999.x番号付けを使用して、バックログのパーキングロットにアイデアを追加します。
|
||||
|
||||
@@ -896,8 +889,8 @@ GSDアップデート後にローカルの変更を復元します。
|
||||
**999.x番号付け**により、バックログ項目はアクティブなフェーズシーケンスの外に保持されます。フェーズディレクトリは即座に作成されるため、`/gsd-discuss-phase` や `/gsd-plan-phase` がそれらに対して動作します。
|
||||
|
||||
```bash
|
||||
/gsd-add-backlog "GraphQL API layer"
|
||||
/gsd-add-backlog "Mobile responsive redesign"
|
||||
/gsd-capture --backlog "GraphQL API layer"
|
||||
/gsd-capture --backlog "Mobile responsive redesign"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -914,7 +907,7 @@ GSDアップデート後にローカルの変更を復元します。
|
||||
|
||||
---
|
||||
|
||||
### `/gsd-plant-seed`
|
||||
### `/gsd-capture --seed`
|
||||
|
||||
トリガー条件付きの将来のアイデアをキャプチャ — 適切なマイルストーンで自動的に表面化します。
|
||||
|
||||
@@ -928,7 +921,7 @@ GSDアップデート後にローカルの変更を復元します。
|
||||
**利用先:** `/gsd-new-milestone`(シードをスキャンしてマッチするものを提示)
|
||||
|
||||
```bash
|
||||
/gsd-plant-seed "Add real-time collaboration when WebSocket infra is in place"
|
||||
/gsd-capture --seed "Add real-time collaboration when WebSocket infra is in place"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -386,7 +386,6 @@
|
||||
- REQ-MILE-08: 新しいマイルストーンは new-project と同じフロー(質問 → リサーチ → 要件 → ロードマップ)に従わなければならない
|
||||
- REQ-MILE-09: 新しいマイルストーンは既存のワークフロー設定をリセットしてはならない
|
||||
|
||||
**ギャップクローズ:** `/gsd-plan-milestone-gaps` は監査で特定されたギャップを埋めるためのフェーズを作成します。
|
||||
|
||||
---
|
||||
|
||||
@@ -394,7 +393,7 @@
|
||||
|
||||
### 9. フェーズ管理
|
||||
|
||||
**コマンド:** `/gsd-add-phase`、`/gsd-insert-phase [N]`、`/gsd-remove-phase [N]`
|
||||
**コマンド:** `/gsd-phase`、`/gsd-phase --insert [N]`、`/gsd-phase --remove [N]`
|
||||
|
||||
**目的:** 開発中のロードマップの動的な変更。
|
||||
|
||||
@@ -681,7 +680,7 @@
|
||||
|
||||
### 26. モデルプロファイル
|
||||
|
||||
**コマンド:** `/gsd-set-profile <quality|balanced|budget|inherit>`
|
||||
**コマンド:** `/gsd-config --profile <quality|balanced|budget|inherit>`
|
||||
|
||||
**目的:** 各エージェントが使用する AI モデルを制御し、品質とコストのバランスを取ります。
|
||||
|
||||
@@ -763,7 +762,7 @@
|
||||
|
||||
### 29. Todo 管理
|
||||
|
||||
**コマンド:** `/gsd-add-todo [desc]`、`/gsd-check-todos`
|
||||
**コマンド:** `/gsd-capture [desc]`、`/gsd-capture --list`
|
||||
|
||||
**目的:** セッション中にアイデアやタスクをキャプチャし、後で作業できるようにします。
|
||||
|
||||
@@ -1066,7 +1065,7 @@ fix(03-01): correct auth token expiry
|
||||
|
||||
### 43. バックログパーキングロット
|
||||
|
||||
**コマンド:** `/gsd-add-backlog <description>`、`/gsd-review-backlog`、`/gsd-plant-seed <idea>`
|
||||
**コマンド:** `/gsd-capture --backlog <description>`、`/gsd-review-backlog`、`/gsd-capture --seed <idea>`
|
||||
|
||||
**目的:** アクティブなプランニングの準備ができていないアイデアをキャプチャします。バックログ項目は 999.x の番号付けを使用して、アクティブなフェーズシーケンスの外に留まります。シードは、適切なマイルストーンで自動的に表面化するトリガー条件を持つ、将来を見据えたアイデアです。
|
||||
|
||||
@@ -1516,7 +1515,7 @@ Claude が GSD ワークフローコンテキスト外でファイル編集を
|
||||
|
||||
### 65. クレーム出所タグ付け
|
||||
|
||||
**対象:** `/gsd-research-phase`
|
||||
**対象:** `/gsd-plan-phase --research-phase`
|
||||
|
||||
**目的:** リサーチのクレームにソースエビデンスのタグを付け、仮定を別途記録します。
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ Get Shit Done(GSD)フレームワークの包括的なドキュメントで
|
||||
|
||||
## クイックリンク
|
||||
|
||||
- **v1.39 の新機能:** `--minimal` インストールプロファイル(≥94% コールドスタート削減)、`/gsd-edit-phase`、マージ後ビルド & テストゲート、`review.models.<cli>` ランタイム別レビューモデル、ワークストリーム設定の継承、手動カナリアリリースワークフロー、スキル統合(86 → 59)
|
||||
- **v1.39 の新機能:** `--minimal` インストールプロファイル(≥94% コールドスタート削減)、`/gsd-phase --edit`、マージ後ビルド & テストゲート、`review.models.<cli>` ランタイム別レビューモデル、ワークストリーム設定の継承、手動カナリアリリースワークフロー、スキル統合(86 → 59)
|
||||
- **はじめに:** [README](../README.md) → インストール → `/gsd-new-project`
|
||||
- **ワークフロー完全ガイド:** [ユーザーガイド](USER-GUIDE.md)
|
||||
- **コマンド一覧:** [コマンドリファレンス](COMMANDS.md)
|
||||
|
||||
@@ -256,8 +256,8 @@ React/Next.js/Vite プロジェクトの場合、UI リサーチャーは `compo
|
||||
アクティブなプランニングの準備ができていないアイデアは、999.x 番号を使用してバックログに格納され、アクティブなフェーズシーケンスの外に保持されます。
|
||||
|
||||
```
|
||||
/gsd-add-backlog "GraphQL API layer" # Creates 999.1-graphql-api-layer/
|
||||
/gsd-add-backlog "Mobile responsive" # Creates 999.2-mobile-responsive/
|
||||
/gsd-capture --backlog "GraphQL API layer" # Creates 999.1-graphql-api-layer/
|
||||
/gsd-capture --backlog "Mobile responsive" # Creates 999.2-mobile-responsive/
|
||||
```
|
||||
|
||||
バックログアイテムは完全なフェーズディレクトリを取得するため、`/gsd-discuss-phase 999.1` でアイデアをさらに探索したり、準備が整ったら `/gsd-plan-phase 999.1` を使用できます。
|
||||
@@ -269,7 +269,7 @@ React/Next.js/Vite プロジェクトの場合、UI リサーチャーは `compo
|
||||
シードは、トリガー条件を持つ将来を見据えたアイデアです。バックログアイテムとは異なり、適切なマイルストーンが到来すると自動的に表面化されます。
|
||||
|
||||
```
|
||||
/gsd-plant-seed "Add real-time collab when WebSocket infra is in place"
|
||||
/gsd-capture --seed "Add real-time collab when WebSocket infra is in place"
|
||||
```
|
||||
|
||||
シードは完全な WHY と表面化タイミングを保持します。`/gsd-new-milestone` はすべてのシードをスキャンし、一致するものを提示します。
|
||||
@@ -288,7 +288,7 @@ React/Next.js/Vite プロジェクトの場合、UI リサーチャーは `compo
|
||||
|
||||
スレッドは `/gsd-pause-work` より軽量です — フェーズ状態やプランコンテキストはありません。各スレッドファイルには Goal、Context、References、Next Steps セクションが含まれます。
|
||||
|
||||
スレッドは成熟した段階でフェーズ (`/gsd-add-phase`) やバックログアイテム (`/gsd-add-backlog`) にプロモーションできます。
|
||||
スレッドは成熟した段階でフェーズ (`/gsd-phase`) やバックログアイテム (`/gsd-capture --backlog`) にプロモーションできます。
|
||||
|
||||
**保存場所:** `.planning/threads/{slug}.md`
|
||||
|
||||
@@ -313,7 +313,7 @@ React/Next.js/Vite プロジェクトの場合、UI リサーチャーは `compo
|
||||
|
||||
各ワークストリームは独自の `.planning/` ディレクトリサブツリーを維持します。ワークストリームを切り替えると、GSD はアクティブなプランニングコンテキストを入れ替え、`/gsd-progress`、`/gsd-discuss-phase`、`/gsd-plan-phase` などのコマンドがそのワークストリームの状態に対して動作するようにします。
|
||||
|
||||
これは `/gsd-new-workspace`(別のリポジトリワークツリーを作成)より軽量です。ワークストリームは同じコードベースと git 履歴を共有しつつ、プランニングアーティファクトを分離します。
|
||||
これは `/gsd-workspace --new`(別のリポジトリワークツリーを作成)より軽量です。ワークストリームは同じコードベースと git 履歴を共有しつつ、プランニングアーティファクトを分離します。
|
||||
|
||||
---
|
||||
|
||||
@@ -413,12 +413,11 @@ GSD はマークダウンファイルを生成し、それが LLM のシステ
|
||||
|
||||
| コマンド | 用途 | 使用タイミング |
|
||||
|---------|---------|-------------|
|
||||
| `/gsd-add-phase` | ロードマップに新しいフェーズを追加 | 初期プランニング後にスコープが拡大した場合 |
|
||||
| `/gsd-insert-phase [N]` | 緊急作業を挿入(小数番号) | マイルストーン中の緊急修正 |
|
||||
| `/gsd-remove-phase [N]` | 将来のフェーズを削除して番号を振り直す | 機能のスコープ縮小 |
|
||||
| `/gsd-phase` | ロードマップに新しいフェーズを追加 | 初期プランニング後にスコープが拡大した場合 |
|
||||
| `/gsd-phase --insert [N]` | 緊急作業を挿入(小数番号) | マイルストーン中の緊急修正 |
|
||||
| `/gsd-phase --remove [N]` | 将来のフェーズを削除して番号を振り直す | 機能のスコープ縮小 |
|
||||
| `/gsd-list-phase-assumptions [N]` | Claude の意図するアプローチをプレビュー | プランニング前に方向性を確認 |
|
||||
| `/gsd-plan-milestone-gaps` | 監査ギャップに対するフェーズを作成 | 監査で不足項目が見つかった後 |
|
||||
| `/gsd-research-phase [N]` | エコシステムの深いリサーチのみ | 複雑または不慣れなドメイン |
|
||||
| `/gsd-plan-phase --research-phase [N]` | エコシステムの深いリサーチのみ | 複雑または不慣れなドメイン |
|
||||
|
||||
### ブラウンフィールドとユーティリティ
|
||||
|
||||
@@ -428,10 +427,10 @@ GSD はマークダウンファイルを生成し、それが LLM のシステ
|
||||
| `/gsd-quick` | GSD 保証付きのアドホックタスク | バグ修正、小機能、設定変更 |
|
||||
| `/gsd-debug [desc]` | 永続状態を持つ体系的デバッグ | 何かが壊れた時 |
|
||||
| `/gsd-forensics` | ワークフロー障害の診断レポート | 状態、アーティファクト、git 履歴が破損していると思われる場合 |
|
||||
| `/gsd-add-todo [desc]` | 後でやるアイデアを記録 | セッション中にアイデアが浮かんだ時 |
|
||||
| `/gsd-check-todos` | 保留中の TODO を一覧表示 | 記録したアイデアのレビュー |
|
||||
| `/gsd-capture [desc]` | 後でやるアイデアを記録 | セッション中にアイデアが浮かんだ時 |
|
||||
| `/gsd-capture --list` | 保留中の TODO を一覧表示 | 記録したアイデアのレビュー |
|
||||
| `/gsd-settings` | ワークフロートグルとモデルプロファイルを設定 | モデル変更、エージェントのトグル |
|
||||
| `/gsd-set-profile <profile>` | クイックプロファイル切り替え | コスト/品質トレードオフの変更 |
|
||||
| `/gsd-config --profile <profile>` | クイックプロファイル切り替え | コスト/品質トレードオフの変更 |
|
||||
| `/gsd-update --reapply` | アップデート後にローカル変更を復元 | ローカル編集がある場合の `/gsd-update` 後 |
|
||||
|
||||
### コード品質とレビュー
|
||||
@@ -446,9 +445,9 @@ GSD はマークダウンファイルを生成し、それが LLM のシステ
|
||||
|
||||
| コマンド | 用途 | 使用タイミング |
|
||||
|---------|---------|-------------|
|
||||
| `/gsd-add-backlog <desc>` | バックログパーキングロットにアイデアを追加(999.x) | アクティブなプランニングの準備ができていないアイデア |
|
||||
| `/gsd-capture --backlog <desc>` | バックログパーキングロットにアイデアを追加(999.x) | アクティブなプランニングの準備ができていないアイデア |
|
||||
| `/gsd-review-backlog` | バックログアイテムのプロモーション/保持/削除 | 新マイルストーン前の優先順位付け |
|
||||
| `/gsd-plant-seed <idea>` | トリガー条件付きの将来を見据えたアイデア | 将来のマイルストーンで表面化すべきアイデア |
|
||||
| `/gsd-capture --seed <idea>` | トリガー条件付きの将来を見据えたアイデア | 将来のマイルストーンで表面化すべきアイデア |
|
||||
| `/gsd-thread [name]` | 永続コンテキストスレッド | フェーズ構造外のクロスセッション作業 |
|
||||
|
||||
---
|
||||
@@ -642,7 +641,6 @@ claude --dangerously-skip-permissions
|
||||
|
||||
```bash
|
||||
/gsd-audit-milestone # 要件カバレッジを確認、スタブを検出
|
||||
/gsd-plan-milestone-gaps # 監査でギャップが見つかった場合、フェーズを作成して埋める
|
||||
/gsd-complete-milestone # アーカイブ、タグ付け、完了
|
||||
```
|
||||
|
||||
@@ -659,11 +657,11 @@ claude --dangerously-skip-permissions
|
||||
### マイルストーン中のスコープ変更
|
||||
|
||||
```bash
|
||||
/gsd-add-phase # ロードマップに新しいフェーズを追加
|
||||
/gsd-phase # ロードマップに新しいフェーズを追加
|
||||
# または
|
||||
/gsd-insert-phase 3 # フェーズ 3 と 4 の間に緊急作業を挿入
|
||||
/gsd-phase --insert 3 # フェーズ 3 と 4 の間に緊急作業を挿入
|
||||
# または
|
||||
/gsd-remove-phase 7 # フェーズ 7 をスコープ外にして番号を振り直す
|
||||
/gsd-phase --remove 7 # フェーズ 7 をスコープ外にして番号を振り直す
|
||||
```
|
||||
|
||||
### マルチプロジェクトワークスペース
|
||||
@@ -672,18 +670,18 @@ claude --dangerously-skip-permissions
|
||||
|
||||
```bash
|
||||
# モノレポからリポジトリを含むワークスペースを作成
|
||||
/gsd-new-workspace --name feature-b --repos hr-ui,ZeymoAPI
|
||||
/gsd-workspace --new --name feature-b --repos hr-ui,ZeymoAPI
|
||||
|
||||
# フィーチャーブランチの分離 — 独自の .planning/ を持つ現在のリポジトリのワークツリー
|
||||
/gsd-new-workspace --name feature-b --repos .
|
||||
/gsd-workspace --new --name feature-b --repos .
|
||||
|
||||
# ワークスペースに移動して GSD を初期化
|
||||
cd ~/gsd-workspaces/feature-b
|
||||
/gsd-new-project
|
||||
|
||||
# ワークスペースの一覧と管理
|
||||
/gsd-list-workspaces
|
||||
/gsd-remove-workspace feature-b
|
||||
/gsd-workspace --list
|
||||
/gsd-workspace --remove feature-b
|
||||
```
|
||||
|
||||
各ワークスペースには以下が含まれます:
|
||||
@@ -721,7 +719,7 @@ cd ~/gsd-workspaces/feature-b
|
||||
|
||||
### モデルのコストが高すぎる
|
||||
|
||||
budget プロファイルに切り替えてください:`/gsd-set-profile budget`。ドメインに慣れている場合(またはClaude が慣れている場合)は、`/gsd-settings` でリサーチエージェントと plan-check エージェントを無効にしてください。
|
||||
budget プロファイルに切り替えてください:`/gsd-config --profile budget`。ドメインに慣れている場合(またはClaude が慣れている場合)は、`/gsd-settings` でリサーチエージェントと plan-check エージェントを無効にしてください。
|
||||
|
||||
### 非 Claude ランタイムの使用(Codex、OpenCode、Gemini CLI、Kilo)
|
||||
|
||||
@@ -746,7 +744,7 @@ budget プロファイルに切り替えてください:`/gsd-set-profile budg
|
||||
|
||||
### 非 Anthropic プロバイダーでの Claude Code の使用(OpenRouter、ローカル)
|
||||
|
||||
GSD サブエージェントが Anthropic モデルを呼び出し、OpenRouter やローカルプロバイダーを通じて支払っている場合は、`inherit` プロファイルに切り替えてください:`/gsd-set-profile inherit`。これにより、すべてのエージェントが特定の Anthropic モデルの代わりに現在のセッションモデルを使用します。`/gsd-settings` → モデルプロファイル → Inherit も参照してください。
|
||||
GSD サブエージェントが Anthropic モデルを呼び出し、OpenRouter やローカルプロバイダーを通じて支払っている場合は、`inherit` プロファイルに切り替えてください:`/gsd-config --profile inherit`。これにより、すべてのエージェントが特定の Anthropic モデルの代わりに現在のセッションモデルを使用します。`/gsd-settings` → モデルプロファイル → Inherit も参照してください。
|
||||
|
||||
### 機密/プライベートプロジェクトでの作業
|
||||
|
||||
@@ -794,13 +792,12 @@ Windows でインストーラーが `EPERM: operation not permitted, scandir`
|
||||
|---------|----------|
|
||||
| コンテキストの喪失 / 新セッション | `/gsd-resume-work` または `/gsd-progress` |
|
||||
| フェーズが失敗した | フェーズのコミットを `git revert` して再プランニング |
|
||||
| スコープ変更が必要 | `/gsd-add-phase`、`/gsd-insert-phase`、または `/gsd-remove-phase` |
|
||||
| マイルストーン監査でギャップを発見 | `/gsd-plan-milestone-gaps` |
|
||||
| スコープ変更が必要 | `/gsd-phase`、`/gsd-phase --insert`、または `/gsd-phase --remove` |
|
||||
| 何かが壊れた | `/gsd-debug "description"` |
|
||||
| ワークフロー状態が破損している可能性 | `/gsd-forensics` |
|
||||
| ターゲットを絞った修正 | `/gsd-quick` |
|
||||
| プランがビジョンに合わない | `/gsd-discuss-phase [N]` で再プランニング |
|
||||
| コストが高い | `/gsd-set-profile budget` と `/gsd-settings` でエージェントをオフ |
|
||||
| コストが高い | `/gsd-config --profile budget` と `/gsd-settings` でエージェントをオフ |
|
||||
| アップデートがローカル変更を壊した | `/gsd-update --reapply` |
|
||||
| ステークホルダー向けセッションサマリーが欲しい | `/gsd-session-report` |
|
||||
| 次のステップがわからない | `/gsd-next` |
|
||||
|
||||
@@ -411,7 +411,7 @@ UI-SPEC.md (per phase) ───────────────────
|
||||
│ ├── pending/ # 캡처된 아이디어
|
||||
│ └── done/ # 완료된 할 일
|
||||
├── threads/ # 영구 컨텍스트 스레드 (/gsd-thread에서)
|
||||
├── seeds/ # 미래 지향적 아이디어 (/gsd-plant-seed에서)
|
||||
├── seeds/ # 미래 지향적 아이디어 (/gsd-capture --seed에서)
|
||||
├── debug/ # 활성 디버그 세션
|
||||
│ ├── *.md # 활성 세션
|
||||
│ ├── resolved/ # 보관된 세션
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
---
|
||||
|
||||
### `/gsd-new-workspace`
|
||||
### `/gsd-workspace --new`
|
||||
|
||||
격리된 워크스페이스를 생성합니다. 저장소 복사본과 독립적인 `.planning/` 디렉터리가 포함됩니다.
|
||||
|
||||
@@ -52,14 +52,14 @@
|
||||
**생성 파일:** `WORKSPACE.md`, `.planning/`, 저장소 복사본 (worktree 또는 clone)
|
||||
|
||||
```bash
|
||||
/gsd-new-workspace --name feature-b --repos hr-ui,ZeymoAPI
|
||||
/gsd-new-workspace --name feature-b --repos . --strategy worktree # 동일 저장소 격리
|
||||
/gsd-new-workspace --name spike --repos api,web --strategy clone # 전체 클론
|
||||
/gsd-workspace --new --name feature-b --repos hr-ui,ZeymoAPI
|
||||
/gsd-workspace --new --name feature-b --repos . --strategy worktree # 동일 저장소 격리
|
||||
/gsd-workspace --new --name spike --repos api,web --strategy clone # 전체 클론
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `/gsd-list-workspaces`
|
||||
### `/gsd-workspace --list`
|
||||
|
||||
활성 GSD 워크스페이스와 상태를 목록으로 표시합니다.
|
||||
|
||||
@@ -67,12 +67,12 @@
|
||||
**표시 항목:** 이름, 저장소 수, 전략, GSD 프로젝트 상태
|
||||
|
||||
```bash
|
||||
/gsd-list-workspaces
|
||||
/gsd-workspace --list
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `/gsd-remove-workspace`
|
||||
### `/gsd-workspace --remove`
|
||||
|
||||
워크스페이스를 제거하고 git worktree를 정리합니다.
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
**안전 장치:** 저장소에 커밋되지 않은 변경사항이 있으면 제거를 거부합니다. 이름 확인이 필요합니다.
|
||||
|
||||
```bash
|
||||
/gsd-remove-workspace feature-b
|
||||
/gsd-workspace --remove feature-b
|
||||
```
|
||||
|
||||
---
|
||||
@@ -368,15 +368,15 @@
|
||||
|
||||
## 페이즈 관리 명령어
|
||||
|
||||
### `/gsd-add-phase`
|
||||
### `/gsd-phase`
|
||||
|
||||
로드맵에 새 페이즈를 추가합니다.
|
||||
|
||||
```bash
|
||||
/gsd-add-phase # 대화형 — 페이즈를 설명합니다
|
||||
/gsd-phase # 대화형 — 페이즈를 설명합니다
|
||||
```
|
||||
|
||||
### `/gsd-insert-phase`
|
||||
### `/gsd-phase --insert`
|
||||
|
||||
소수점 번호 체계를 사용하여 페이즈 사이에 긴급 작업을 삽입합니다.
|
||||
|
||||
@@ -385,10 +385,10 @@
|
||||
| `N` | 아니오 | 이 페이즈 번호 다음에 삽입합니다 |
|
||||
|
||||
```bash
|
||||
/gsd-insert-phase 3 # 페이즈 3과 4 사이에 삽입 → 3.1 생성
|
||||
/gsd-phase --insert 3 # 페이즈 3과 4 사이에 삽입 → 3.1 생성
|
||||
```
|
||||
|
||||
### `/gsd-remove-phase`
|
||||
### `/gsd-phase --remove`
|
||||
|
||||
미래 페이즈를 제거하고 이후 페이즈 번호를 재정렬합니다.
|
||||
|
||||
@@ -397,7 +397,7 @@
|
||||
| `N` | 아니오 | 제거할 페이즈 번호 |
|
||||
|
||||
```bash
|
||||
/gsd-remove-phase 7 # 페이즈 7 제거, 8→7, 9→8 등으로 재번호
|
||||
/gsd-phase --remove 7 # 페이즈 7 제거, 8→7, 9→8 등으로 재번호
|
||||
```
|
||||
|
||||
### `/gsd-list-phase-assumptions`
|
||||
@@ -412,15 +412,8 @@
|
||||
/gsd-list-phase-assumptions 2 # 페이즈 2 가정 사항 확인
|
||||
```
|
||||
|
||||
### `/gsd-plan-milestone-gaps`
|
||||
|
||||
마일스톤 감사에서 발견된 갭을 보완하는 페이즈를 생성합니다.
|
||||
|
||||
```bash
|
||||
/gsd-plan-milestone-gaps # 각 감사 갭에 대한 페이즈 생성
|
||||
```
|
||||
|
||||
### `/gsd-research-phase`
|
||||
### `/gsd-plan-phase --research-phase`
|
||||
|
||||
심층 에코시스템 조사만 수행합니다 (독립 실행 — 일반적으로 `/gsd-plan-phase`를 사용하세요).
|
||||
|
||||
@@ -429,7 +422,7 @@
|
||||
| `N` | 아니오 | 페이즈 번호 |
|
||||
|
||||
```bash
|
||||
/gsd-research-phase 4 # 페이즈 4 도메인 조사
|
||||
/gsd-plan-phase --research-phase 4 # 페이즈 4 도메인 조사
|
||||
```
|
||||
|
||||
### `/gsd-validate-phase`
|
||||
@@ -598,7 +591,7 @@ GSD 보증을 갖춘 임시 작업을 실행합니다.
|
||||
/gsd-debug --diagnose "API returning 500 on /users endpoint"
|
||||
```
|
||||
|
||||
### `/gsd-add-todo`
|
||||
### `/gsd-capture`
|
||||
|
||||
나중을 위한 아이디어나 작업을 캡처합니다.
|
||||
|
||||
@@ -607,15 +600,15 @@ GSD 보증을 갖춘 임시 작업을 실행합니다.
|
||||
| `description` | 아니오 | 할 일 설명 |
|
||||
|
||||
```bash
|
||||
/gsd-add-todo "Consider adding dark mode support"
|
||||
/gsd-capture "Consider adding dark mode support"
|
||||
```
|
||||
|
||||
### `/gsd-check-todos`
|
||||
### `/gsd-capture --list`
|
||||
|
||||
보류 중인 할 일 목록을 표시하고 작업할 항목을 선택합니다.
|
||||
|
||||
```bash
|
||||
/gsd-check-todos
|
||||
/gsd-capture --list
|
||||
```
|
||||
|
||||
### `/gsd-add-tests`
|
||||
@@ -752,7 +745,7 @@ Claude Code 세션 분석을 통해 8개 차원(커뮤니케이션 스타일,
|
||||
/gsd-settings # 대화형 설정
|
||||
```
|
||||
|
||||
### `/gsd-set-profile`
|
||||
### `/gsd-config --profile`
|
||||
|
||||
프로필을 빠르게 전환합니다.
|
||||
|
||||
@@ -761,8 +754,8 @@ Claude Code 세션 분석을 통해 8개 차원(커뮤니케이션 스타일,
|
||||
| `profile` | **예** | `quality`, `balanced`, `budget`, 또는 `inherit` |
|
||||
|
||||
```bash
|
||||
/gsd-set-profile budget # 예산 프로필로 전환
|
||||
/gsd-set-profile quality # 품질 프로필로 전환
|
||||
/gsd-config --profile budget # 예산 프로필로 전환
|
||||
/gsd-config --profile quality # 품질 프로필로 전환
|
||||
```
|
||||
|
||||
---
|
||||
@@ -885,7 +878,7 @@ GSD 업데이트 후 로컬 수정사항을 복원합니다.
|
||||
|
||||
## 백로그 및 스레드 명령어
|
||||
|
||||
### `/gsd-add-backlog`
|
||||
### `/gsd-capture --backlog`
|
||||
|
||||
999.x 번호 체계를 사용하여 백로그 파킹 롯에 아이디어를 추가합니다.
|
||||
|
||||
@@ -896,8 +889,8 @@ GSD 업데이트 후 로컬 수정사항을 복원합니다.
|
||||
**999.x 번호 체계**는 백로그 항목을 활성 페이즈 순서 밖에 유지합니다. 페이즈 디렉터리가 즉시 생성되므로 해당 항목에 대해 `/gsd-discuss-phase`와 `/gsd-plan-phase`를 사용할 수 있습니다.
|
||||
|
||||
```bash
|
||||
/gsd-add-backlog "GraphQL API layer"
|
||||
/gsd-add-backlog "Mobile responsive redesign"
|
||||
/gsd-capture --backlog "GraphQL API layer"
|
||||
/gsd-capture --backlog "Mobile responsive redesign"
|
||||
```
|
||||
|
||||
---
|
||||
@@ -914,7 +907,7 @@ GSD 업데이트 후 로컬 수정사항을 복원합니다.
|
||||
|
||||
---
|
||||
|
||||
### `/gsd-plant-seed`
|
||||
### `/gsd-capture --seed`
|
||||
|
||||
트리거 조건이 있는 미래 지향적인 아이디어를 캡처합니다. 적절한 마일스톤 시점에 자동으로 표면화됩니다.
|
||||
|
||||
@@ -928,7 +921,7 @@ GSD 업데이트 후 로컬 수정사항을 복원합니다.
|
||||
**사용처:** `/gsd-new-milestone` (시드를 스캔하여 일치 항목 제시)
|
||||
|
||||
```bash
|
||||
/gsd-plant-seed "Add real-time collaboration when WebSocket infra is in place"
|
||||
/gsd-capture --seed "Add real-time collaboration when WebSocket infra is in place"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -386,7 +386,6 @@
|
||||
- REQ-MILE-08: 새 마일스톤은 new-project와 동일한 흐름을 따라야 합니다(질문 → 연구 → 요구사항 → 로드맵).
|
||||
- REQ-MILE-09: 새 마일스톤은 기존 워크플로우 구성을 초기화해서는 안 됩니다.
|
||||
|
||||
**갭 해소.** `/gsd-plan-milestone-gaps`는 감사에서 식별된 갭을 해소하는 페이즈를 생성합니다.
|
||||
|
||||
---
|
||||
|
||||
@@ -394,7 +393,7 @@
|
||||
|
||||
### 9. Phase Management
|
||||
|
||||
**명령어:** `/gsd-add-phase`, `/gsd-insert-phase [N]`, `/gsd-remove-phase [N]`
|
||||
**명령어:** `/gsd-phase`, `/gsd-phase --insert [N]`, `/gsd-phase --remove [N]`
|
||||
|
||||
**목적:** 개발 중 동적 로드맵 수정.
|
||||
|
||||
@@ -681,7 +680,7 @@
|
||||
|
||||
### 26. Model Profiles
|
||||
|
||||
**명령어:** `/gsd-set-profile <quality|balanced|budget|inherit>`
|
||||
**명령어:** `/gsd-config --profile <quality|balanced|budget|inherit>`
|
||||
|
||||
**목적:** 각 에이전트가 사용하는 AI 모델을 제어하여 품질과 비용의 균형을 맞춥니다.
|
||||
|
||||
@@ -763,7 +762,7 @@
|
||||
|
||||
### 29. Todo Management
|
||||
|
||||
**명령어:** `/gsd-add-todo [desc]`, `/gsd-check-todos`
|
||||
**명령어:** `/gsd-capture [desc]`, `/gsd-capture --list`
|
||||
|
||||
**목적:** 세션 중 나중에 처리할 아이디어와 작업을 캡처합니다.
|
||||
|
||||
@@ -1066,7 +1065,7 @@ fix(03-01): correct auth token expiry
|
||||
|
||||
### 43. Backlog Parking Lot
|
||||
|
||||
**명령어:** `/gsd-add-backlog <description>`, `/gsd-review-backlog`, `/gsd-plant-seed <idea>`
|
||||
**명령어:** `/gsd-capture --backlog <description>`, `/gsd-review-backlog`, `/gsd-capture --seed <idea>`
|
||||
|
||||
**목적:** 아직 적극적인 계획에 준비되지 않은 아이디어를 캡처합니다. 백로그 항목은 활성 페이즈 순서 밖에 있기 위해 999.x 번호를 사용합니다. 시드는 올바른 마일스톤에서 자동으로 표시되는 트리거 조건이 있는 미래 지향적 아이디어입니다.
|
||||
|
||||
@@ -1516,7 +1515,7 @@ Claude가 GSD 워크플로우 컨텍스트 밖에서 파일 편집을 시도하
|
||||
|
||||
### 65. 주장 출처 태깅
|
||||
|
||||
**대상:** `/gsd-research-phase`
|
||||
**대상:** `/gsd-plan-phase --research-phase`
|
||||
|
||||
**목적:** 연구 주장에 출처 증거를 태깅하고 가정을 별도로 기록합니다.
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ Get Shit Done (GSD) 프레임워크의 종합 문서입니다. GSD는 AI 코딩
|
||||
|
||||
## 빠른 링크
|
||||
|
||||
- **v1.39의 새로운 기능:** `--minimal` 설치 프로파일(콜드 스타트 ≥94% 감소), `/gsd-edit-phase`, 머지 후 빌드 & 테스트 게이트, `review.models.<cli>` 런타임별 리뷰 모델, 워크스트림 설정 상속, 수동 카나리 릴리스 워크플로, 스킬 통합(86 → 59)
|
||||
- **v1.39의 새로운 기능:** `--minimal` 설치 프로파일(콜드 스타트 ≥94% 감소), `/gsd-phase --edit`, 머지 후 빌드 & 테스트 게이트, `review.models.<cli>` 런타임별 리뷰 모델, 워크스트림 설정 상속, 수동 카나리 릴리스 워크플로, 스킬 통합(86 → 59)
|
||||
- **시작하기:** [README](../README.md) → 설치 → `/gsd-new-project`
|
||||
- **전체 워크플로우 안내:** [User Guide](USER-GUIDE.md)
|
||||
- **모든 명령어 한눈에 보기:** [Command Reference](COMMANDS.md)
|
||||
|
||||
@@ -256,8 +256,8 @@ React/Next.js/Vite 프로젝트에서 `components.json`이 없으면 UI 조사
|
||||
활성 계획에 아직 준비되지 않은 아이디어는 999.x 번호 체계를 사용하여 백로그에 보관하며 활성 페이즈 순서 밖에 유지됩니다.
|
||||
|
||||
```
|
||||
/gsd-add-backlog "GraphQL API layer" # Creates 999.1-graphql-api-layer/
|
||||
/gsd-add-backlog "Mobile responsive" # Creates 999.2-mobile-responsive/
|
||||
/gsd-capture --backlog "GraphQL API layer" # Creates 999.1-graphql-api-layer/
|
||||
/gsd-capture --backlog "Mobile responsive" # Creates 999.2-mobile-responsive/
|
||||
```
|
||||
|
||||
백로그 항목은 전체 페이즈 디렉터리를 얻으므로 `/gsd-discuss-phase 999.1`로 아이디어를 더 탐구하거나 준비가 되면 `/gsd-plan-phase 999.1`을 사용할 수 있습니다.
|
||||
@@ -269,7 +269,7 @@ React/Next.js/Vite 프로젝트에서 `components.json`이 없으면 UI 조사
|
||||
시드는 트리거 조건이 있는 미래 지향적인 아이디어입니다. 백로그 항목과 달리 시드는 적절한 마일스톤 시점에 자동으로 표면화됩니다.
|
||||
|
||||
```
|
||||
/gsd-plant-seed "Add real-time collab when WebSocket infra is in place"
|
||||
/gsd-capture --seed "Add real-time collab when WebSocket infra is in place"
|
||||
```
|
||||
|
||||
시드는 전체 WHY와 언제 표면화할지를 보존합니다. `/gsd-new-milestone`은 모든 시드를 스캔하여 일치 항목을 제시합니다.
|
||||
@@ -288,7 +288,7 @@ React/Next.js/Vite 프로젝트에서 `components.json`이 없으면 UI 조사
|
||||
|
||||
스레드는 `/gsd-pause-work`보다 가볍습니다. 페이즈 상태나 계획 컨텍스트가 없습니다. 각 스레드 파일에는 목표, 컨텍스트, 참조, 다음 단계 섹션이 포함됩니다.
|
||||
|
||||
스레드가 성숙해지면 페이즈(`/gsd-add-phase`)나 백로그 항목(`/gsd-add-backlog`)으로 승격할 수 있습니다.
|
||||
스레드가 성숙해지면 페이즈(`/gsd-phase`)나 백로그 항목(`/gsd-capture --backlog`)으로 승격할 수 있습니다.
|
||||
|
||||
**저장 위치:** `.planning/threads/{slug}.md`
|
||||
|
||||
@@ -313,7 +313,7 @@ React/Next.js/Vite 프로젝트에서 `components.json`이 없으면 UI 조사
|
||||
|
||||
각 워크스트림은 자체 `.planning/` 디렉터리 하위 트리를 유지합니다. 워크스트림을 전환하면 GSD가 활성 계획 컨텍스트를 교체하여 `/gsd-progress`, `/gsd-discuss-phase`, `/gsd-plan-phase` 및 기타 명령어가 해당 워크스트림의 상태로 동작합니다.
|
||||
|
||||
이는 `/gsd-new-workspace`(별도 저장소 worktree를 생성)보다 가볍습니다. 워크스트림은 동일한 코드베이스와 git 히스토리를 공유하지만 계획 아티팩트를 격리합니다.
|
||||
이는 `/gsd-workspace --new`(별도 저장소 worktree를 생성)보다 가볍습니다. 워크스트림은 동일한 코드베이스와 git 히스토리를 공유하지만 계획 아티팩트를 격리합니다.
|
||||
|
||||
---
|
||||
|
||||
@@ -413,12 +413,11 @@ GSD는 LLM 시스템 프롬프트가 되는 마크다운 파일을 생성합니
|
||||
|
||||
| 명령어 | 목적 | 사용 시점 |
|
||||
|--------|------|----------|
|
||||
| `/gsd-add-phase` | 로드맵에 새 페이즈 추가 | 초기 계획 후 범위가 늘어날 때 |
|
||||
| `/gsd-insert-phase [N]` | 긴급 작업 삽입 (소수점 번호 체계) | 마일스톤 중간의 긴급 수정 시 |
|
||||
| `/gsd-remove-phase [N]` | 미래 페이즈 제거 및 재번호 | 기능 범위 축소 시 |
|
||||
| `/gsd-phase` | 로드맵에 새 페이즈 추가 | 초기 계획 후 범위가 늘어날 때 |
|
||||
| `/gsd-phase --insert [N]` | 긴급 작업 삽입 (소수점 번호 체계) | 마일스톤 중간의 긴급 수정 시 |
|
||||
| `/gsd-phase --remove [N]` | 미래 페이즈 제거 및 재번호 | 기능 범위 축소 시 |
|
||||
| `/gsd-list-phase-assumptions [N]` | Claude의 예상 접근 방식 미리 확인 | 계획 전 방향 검증 시 |
|
||||
| `/gsd-plan-milestone-gaps` | 감사 갭을 위한 페이즈 생성 | 감사에서 누락 항목이 발견된 후 |
|
||||
| `/gsd-research-phase [N]` | 심층 에코시스템 조사만 수행 | 복잡하거나 익숙하지 않은 도메인 |
|
||||
| `/gsd-plan-phase --research-phase [N]` | 심층 에코시스템 조사만 수행 | 복잡하거나 익숙하지 않은 도메인 |
|
||||
|
||||
### 브라운필드 및 유틸리티
|
||||
|
||||
@@ -428,10 +427,10 @@ GSD는 LLM 시스템 프롬프트가 되는 마크다운 파일을 생성합니
|
||||
| `/gsd-quick` | GSD 보증을 갖춘 임시 작업 | 버그 수정, 소규모 기능, 설정 변경 |
|
||||
| `/gsd-debug [desc]` | 지속적인 상태를 유지하는 체계적인 디버깅 | 문제가 발생했을 때 |
|
||||
| `/gsd-forensics` | 워크플로우 실패에 대한 진단 보고서 | 상태, 아티팩트, git 히스토리가 손상된 것 같을 때 |
|
||||
| `/gsd-add-todo [desc]` | 나중을 위한 아이디어 캡처 | 세션 중에 생각이 날 때 |
|
||||
| `/gsd-check-todos` | 보류 중인 할 일 목록 | 캡처된 아이디어 검토 시 |
|
||||
| `/gsd-capture [desc]` | 나중을 위한 아이디어 캡처 | 세션 중에 생각이 날 때 |
|
||||
| `/gsd-capture --list` | 보류 중인 할 일 목록 | 캡처된 아이디어 검토 시 |
|
||||
| `/gsd-settings` | 워크플로우 토글 및 모델 프로필 설정 | 모델 변경, 에이전트 토글 시 |
|
||||
| `/gsd-set-profile <profile>` | 빠른 프로필 전환 | 비용/품질 트레이드오프 변경 시 |
|
||||
| `/gsd-config --profile <profile>` | 빠른 프로필 전환 | 비용/품질 트레이드오프 변경 시 |
|
||||
| `/gsd-update --reapply` | 업데이트 후 로컬 수정사항 복원 | 로컬 편집이 있는 상태에서 `/gsd-update` 이후 |
|
||||
|
||||
### 코드 품질 및 리뷰
|
||||
@@ -446,9 +445,9 @@ GSD는 LLM 시스템 프롬프트가 되는 마크다운 파일을 생성합니
|
||||
|
||||
| 명령어 | 목적 | 사용 시점 |
|
||||
|--------|------|----------|
|
||||
| `/gsd-add-backlog <desc>` | 백로그 파킹 롯에 아이디어 추가 (999.x) | 활성 계획에 준비되지 않은 아이디어 |
|
||||
| `/gsd-capture --backlog <desc>` | 백로그 파킹 롯에 아이디어 추가 (999.x) | 활성 계획에 준비되지 않은 아이디어 |
|
||||
| `/gsd-review-backlog` | 백로그 항목 승격/유지/제거 | 새 마일스톤 전 우선순위 결정 시 |
|
||||
| `/gsd-plant-seed <idea>` | 트리거 조건이 있는 미래 지향적인 아이디어 | 미래 마일스톤에서 표면화되어야 할 아이디어 |
|
||||
| `/gsd-capture --seed <idea>` | 트리거 조건이 있는 미래 지향적인 아이디어 | 미래 마일스톤에서 표면화되어야 할 아이디어 |
|
||||
| `/gsd-thread [name]` | 지속적인 컨텍스트 스레드 | 페이즈 구조 밖의 교차 세션 작업 |
|
||||
|
||||
---
|
||||
@@ -642,7 +641,6 @@ claude --dangerously-skip-permissions
|
||||
|
||||
```bash
|
||||
/gsd-audit-milestone # Check requirements coverage, detect stubs
|
||||
/gsd-plan-milestone-gaps # If audit found gaps, create phases to close them
|
||||
/gsd-complete-milestone # Archive, tag, done
|
||||
```
|
||||
|
||||
@@ -659,11 +657,11 @@ claude --dangerously-skip-permissions
|
||||
### 마일스톤 중간 범위 변경
|
||||
|
||||
```bash
|
||||
/gsd-add-phase # Append a new phase to the roadmap
|
||||
/gsd-phase # Append a new phase to the roadmap
|
||||
# or
|
||||
/gsd-insert-phase 3 # Insert urgent work between phases 3 and 4
|
||||
/gsd-phase --insert 3 # Insert urgent work between phases 3 and 4
|
||||
# or
|
||||
/gsd-remove-phase 7 # Descope phase 7 and renumber
|
||||
/gsd-phase --remove 7 # Descope phase 7 and renumber
|
||||
```
|
||||
|
||||
### 멀티 프로젝트 워크스페이스
|
||||
@@ -672,18 +670,18 @@ claude --dangerously-skip-permissions
|
||||
|
||||
```bash
|
||||
# Create a workspace with repos from your monorepo
|
||||
/gsd-new-workspace --name feature-b --repos hr-ui,ZeymoAPI
|
||||
/gsd-workspace --new --name feature-b --repos hr-ui,ZeymoAPI
|
||||
|
||||
# Feature branch isolation — worktree of current repo with its own .planning/
|
||||
/gsd-new-workspace --name feature-b --repos .
|
||||
/gsd-workspace --new --name feature-b --repos .
|
||||
|
||||
# Then cd into the workspace and initialize GSD
|
||||
cd ~/gsd-workspaces/feature-b
|
||||
/gsd-new-project
|
||||
|
||||
# List and manage workspaces
|
||||
/gsd-list-workspaces
|
||||
/gsd-remove-workspace feature-b
|
||||
/gsd-workspace --list
|
||||
/gsd-workspace --remove feature-b
|
||||
```
|
||||
|
||||
각 워크스페이스는 다음을 포함합니다.
|
||||
@@ -721,7 +719,7 @@ cd ~/gsd-workspaces/feature-b
|
||||
|
||||
### 모델 비용이 너무 높은 경우
|
||||
|
||||
예산 프로필로 전환하세요: `/gsd-set-profile budget`. 도메인이 익숙하다면 (또는 Claude에게 익숙하다면) `/gsd-settings`에서 조사 및 plan-check 에이전트를 비활성화하세요.
|
||||
예산 프로필로 전환하세요: `/gsd-config --profile budget`. 도메인이 익숙하다면 (또는 Claude에게 익숙하다면) `/gsd-settings`에서 조사 및 plan-check 에이전트를 비활성화하세요.
|
||||
|
||||
### 비Claude 런타임 사용 (Codex, OpenCode, Gemini CLI, Kilo)
|
||||
|
||||
@@ -746,7 +744,7 @@ cd ~/gsd-workspaces/feature-b
|
||||
|
||||
### 비Anthropic 공급자와 함께 Claude Code 사용 (OpenRouter, 로컬)
|
||||
|
||||
GSD 서브에이전트가 Anthropic 모델을 호출하는데 OpenRouter나 로컬 공급자를 통해 비용을 지불하고 있다면 `inherit` 프로필로 전환하세요: `/gsd-set-profile inherit`. 이렇게 하면 모든 에이전트가 특정 Anthropic 모델 대신 현재 세션 모델을 사용합니다. `/gsd-settings` → Model Profile → Inherit도 참고하세요.
|
||||
GSD 서브에이전트가 Anthropic 모델을 호출하는데 OpenRouter나 로컬 공급자를 통해 비용을 지불하고 있다면 `inherit` 프로필로 전환하세요: `/gsd-config --profile inherit`. 이렇게 하면 모든 에이전트가 특정 Anthropic 모델 대신 현재 세션 모델을 사용합니다. `/gsd-settings` → Model Profile → Inherit도 참고하세요.
|
||||
|
||||
### 민감하거나 비공개 프로젝트에서 작업하는 경우
|
||||
|
||||
@@ -794,13 +792,12 @@ Windows에서 설치 프로그램이 `EPERM: operation not permitted, scandir`
|
||||
|------|----------|
|
||||
| 컨텍스트 손실 / 새 세션 | `/gsd-resume-work` 또는 `/gsd-progress` |
|
||||
| 페이즈가 잘못됨 | 페이즈 커밋에 `git revert` 후 재계획 |
|
||||
| 범위 변경 필요 | `/gsd-add-phase`, `/gsd-insert-phase`, 또는 `/gsd-remove-phase` |
|
||||
| 마일스톤 감사에서 갭 발견 | `/gsd-plan-milestone-gaps` |
|
||||
| 범위 변경 필요 | `/gsd-phase`, `/gsd-phase --insert`, 또는 `/gsd-phase --remove` |
|
||||
| 무언가 고장남 | `/gsd-debug "description"` |
|
||||
| 워크플로우 상태 손상 의심 | `/gsd-forensics` |
|
||||
| 빠른 목표 수정 | `/gsd-quick` |
|
||||
| 계획이 비전과 맞지 않음 | `/gsd-discuss-phase [N]` 후 재계획 |
|
||||
| 비용이 높아짐 | `/gsd-set-profile budget` 및 `/gsd-settings`에서 에이전트 비활성화 |
|
||||
| 비용이 높아짐 | `/gsd-config --profile budget` 및 `/gsd-settings`에서 에이전트 비활성화 |
|
||||
| 업데이트가 로컬 변경사항 파괴 | `/gsd-update --reapply` |
|
||||
| 이해관계자를 위한 세션 요약 필요 | `/gsd-session-report` |
|
||||
| 다음 단계를 모르겠음 | `/gsd-next` |
|
||||
|
||||
@@ -35,11 +35,10 @@ Para detalhes completos de flags avançadas e mudanças recentes, consulte tamb
|
||||
|
||||
| Comando | Finalidade |
|
||||
|---------|------------|
|
||||
| `/gsd-add-phase` | Adiciona fase no roadmap |
|
||||
| `/gsd-insert-phase [N]` | Insere trabalho urgente entre fases |
|
||||
| `/gsd-remove-phase [N]` | Remove fase futura e reenumera |
|
||||
| `/gsd-phase` | Adiciona fase no roadmap |
|
||||
| `/gsd-phase --insert [N]` | Insere trabalho urgente entre fases |
|
||||
| `/gsd-phase --remove [N]` | Remove fase futura e reenumera |
|
||||
| `/gsd-list-phase-assumptions [N]` | Mostra abordagem assumida pelo Claude |
|
||||
| `/gsd-plan-milestone-gaps` | Cria fases para fechar lacunas de auditoria |
|
||||
|
||||
## Brownfield e Utilidades
|
||||
|
||||
@@ -51,7 +50,7 @@ Para detalhes completos de flags avançadas e mudanças recentes, consulte tamb
|
||||
| `/gsd-analyze-dependencies` | Detecta dependências entre fases e sugere `Depends on` no ROADMAP.md (v1.32) |
|
||||
| `/gsd-forensics` | Diagnóstico de falhas no workflow |
|
||||
| `/gsd-settings` | Configuração de agentes, perfil e toggles |
|
||||
| `/gsd-set-profile <perfil>` | Troca rápida de perfil de modelo |
|
||||
| `/gsd-config --profile <perfil>` | Troca rápida de perfil de modelo |
|
||||
|
||||
## Qualidade de Código
|
||||
|
||||
@@ -65,9 +64,9 @@ Para detalhes completos de flags avançadas e mudanças recentes, consulte tamb
|
||||
|
||||
| Comando | Finalidade |
|
||||
|---------|------------|
|
||||
| `/gsd-add-backlog <desc>` | Adiciona item no backlog (999.x) |
|
||||
| `/gsd-capture --backlog <desc>` | Adiciona item no backlog (999.x) |
|
||||
| `/gsd-review-backlog` | Promove, mantém ou remove itens |
|
||||
| `/gsd-plant-seed <ideia>` | Registra ideia com gatilho futuro |
|
||||
| `/gsd-capture --seed <ideia>` | Registra ideia com gatilho futuro |
|
||||
| `/gsd-thread [nome]` | Gerencia threads persistentes |
|
||||
|
||||
## Gerenciamento de Estado
|
||||
|
||||
@@ -80,7 +80,7 @@ Esta versão resume os parâmetros principais em Português. Para schema complet
|
||||
Troca rápida:
|
||||
|
||||
```bash
|
||||
/gsd-set-profile budget
|
||||
/gsd-config --profile budget
|
||||
```
|
||||
|
||||
## Novidades de configuração v1.31--v1.32
|
||||
|
||||
@@ -20,7 +20,7 @@ Documentação abrangente do framework Get Shit Done (GSD) — um sistema de met
|
||||
|
||||
## Novidades v1.39
|
||||
|
||||
Perfil de instalação `--minimal` (≥94% de redução no cold-start), `/gsd-edit-phase`, build & test gate pós-merge, `review.models.<cli>` para escolha de modelo de review por runtime, herança de configuração de workstream, workflow manual de canary release, consolidação de skills (86 → 59).
|
||||
Perfil de instalação `--minimal` (≥94% de redução no cold-start), `/gsd-phase --edit`, build & test gate pós-merge, `review.models.<cli>` para escolha de modelo de review por runtime, herança de configuração de workstream, workflow manual de canary release, consolidação de skills (86 → 59).
|
||||
|
||||
## Links rápidos
|
||||
|
||||
|
||||
@@ -92,8 +92,8 @@ Com `workflow.discuss_mode: "assumptions"`, o GSD analisa o código antes de per
|
||||
Ideias fora da sequência ativa vão para backlog:
|
||||
|
||||
```bash
|
||||
/gsd-add-backlog "Camada GraphQL"
|
||||
/gsd-add-backlog "Responsividade mobile"
|
||||
/gsd-capture --backlog "Camada GraphQL"
|
||||
/gsd-capture --backlog "Responsividade mobile"
|
||||
```
|
||||
|
||||
Promover/revisar:
|
||||
@@ -107,7 +107,7 @@ Promover/revisar:
|
||||
Seeds guardam ideias futuras com condição de gatilho:
|
||||
|
||||
```bash
|
||||
/gsd-plant-seed "Adicionar colaboração real-time quando infra de WebSocket estiver pronta"
|
||||
/gsd-capture --seed "Adicionar colaboração real-time quando infra de WebSocket estiver pronta"
|
||||
```
|
||||
|
||||
### Threads persistentes
|
||||
@@ -176,7 +176,7 @@ Para arquivos sensíveis, use deny list no Claude Code.
|
||||
| `/gsd-debug [desc]` | Debug sistemático |
|
||||
| `/gsd-forensics` | Diagnóstico de workflow quebrado |
|
||||
| `/gsd-settings` | Ajustar workflow/modelos |
|
||||
| `/gsd-set-profile <profile>` | Troca rápida de perfil |
|
||||
| `/gsd-config --profile <profile>` | Troca rápida de perfil |
|
||||
|
||||
Para lista completa e flags avançadas, consulte [Command Reference](../COMMANDS.md).
|
||||
|
||||
@@ -251,7 +251,6 @@ claude --dangerously-skip-permissions
|
||||
|
||||
```bash
|
||||
/gsd-audit-milestone
|
||||
/gsd-plan-milestone-gaps
|
||||
/gsd-complete-milestone
|
||||
```
|
||||
|
||||
@@ -280,7 +279,7 @@ Replaneje com escopo menor (tarefas menores por plano).
|
||||
Use perfil budget:
|
||||
|
||||
```bash
|
||||
/gsd-set-profile budget
|
||||
/gsd-config --profile budget
|
||||
```
|
||||
|
||||
### Runtime não-Claude (Codex/OpenCode/Gemini/Kilo)
|
||||
@@ -295,10 +294,10 @@ Use `resolve_model_ids: "omit"` para deixar o runtime resolver modelos padrão.
|
||||
|---------|---------|
|
||||
| Perdeu contexto | `/gsd-resume-work` ou `/gsd-progress` |
|
||||
| Fase deu errado | `git revert` + replanejar |
|
||||
| Precisa alterar escopo | `/gsd-add-phase`, `/gsd-insert-phase`, `/gsd-remove-phase` |
|
||||
| Precisa alterar escopo | `/gsd-phase`, `/gsd-phase --insert`, `/gsd-phase --remove` |
|
||||
| Bug em workflow | `/gsd-forensics` |
|
||||
| Correção pontual | `/gsd-quick` |
|
||||
| Custo alto | `/gsd-set-profile budget` |
|
||||
| Custo alto | `/gsd-config --profile budget` |
|
||||
| Não sabe próximo passo | `/gsd-next` |
|
||||
|
||||
---
|
||||
|
||||
@@ -511,11 +511,10 @@ lmn012o feat(08-02): 创建注册端点
|
||||
|
||||
| 命令 | 作用 |
|
||||
|---------|--------------|
|
||||
| `/gsd-add-phase` | 向路线图追加阶段 |
|
||||
| `/gsd-insert-phase [N]` | 在阶段之间插入紧急工作 |
|
||||
| `/gsd-remove-phase [N]` | 删除未来阶段,重新编号 |
|
||||
| `/gsd-phase` | 向路线图追加阶段 |
|
||||
| `/gsd-phase --insert [N]` | 在阶段之间插入紧急工作 |
|
||||
| `/gsd-phase --remove [N]` | 删除未来阶段,重新编号 |
|
||||
| `/gsd-list-phase-assumptions [N]` | 规划前查看 Claude 的预期方法 |
|
||||
| `/gsd-plan-milestone-gaps` | 创建阶段以填补审计发现的差距 |
|
||||
| `/gsd-autonomous [--from N] [--to N] [--only N]` | 自主执行所有剩余阶段(`--to N` 执行到阶段 N 停止,`--only N` 只执行单个阶段) |
|
||||
| `/gsd-analyze-dependencies` | 检测阶段间依赖关系并建议 ROADMAP.md 的 `Depends on` 条目 |
|
||||
|
||||
@@ -531,9 +530,9 @@ lmn012o feat(08-02): 创建注册端点
|
||||
| 命令 | 作用 |
|
||||
|---------|--------------|
|
||||
| `/gsd-settings` | 配置模型配置文件和工作流代理 |
|
||||
| `/gsd-set-profile <profile>` | 切换模型配置文件(quality/balanced/budget) |
|
||||
| `/gsd-add-todo [desc]` | 捕获想法留待后用 |
|
||||
| `/gsd-check-todos` | 列出待处理事项 |
|
||||
| `/gsd-config --profile <profile>` | 切换模型配置文件(quality/balanced/budget/inherit) |
|
||||
| `/gsd-capture [desc]` | 捕获想法留待后用 |
|
||||
| `/gsd-capture --list` | 列出待处理事项 |
|
||||
| `/gsd-debug [desc] [--diagnose]` | 带持久状态的系统化调试(`--diagnose` 仅诊断不修复) |
|
||||
| `/gsd-quick [--full] [--discuss] [--research]` | 用 GSD 保证执行临时任务(`--full` 启用全部阶段,`--discuss` 先收集上下文,`--research` 规划前调查方法) |
|
||||
| `/gsd-health [--repair]` | 验证 `.planning/` 目录完整性,用 `--repair` 自动修复 |
|
||||
@@ -565,7 +564,7 @@ GSD 在 `.planning/config.json` 中存储项目设置。在 `/gsd-new-project`
|
||||
|
||||
切换配置:
|
||||
```
|
||||
/gsd-set-profile budget
|
||||
/gsd-config --profile budget
|
||||
```
|
||||
|
||||
或通过 `/gsd-settings` 配置。
|
||||
|
||||
@@ -205,12 +205,11 @@
|
||||
|
||||
| 命令 | 用途 | 何时使用 |
|
||||
|---------|---------|-------------|
|
||||
| `/gsd-add-phase` | 向路线图追加新阶段 | 初始规划后范围增长 |
|
||||
| `/gsd-insert-phase [N]` | 插入紧急工作(小数编号) | 里程碑中途紧急修复 |
|
||||
| `/gsd-remove-phase [N]` | 删除未来阶段并重新编号 | 移除某个功能 |
|
||||
| `/gsd-phase` | 向路线图追加新阶段 | 初始规划后范围增长 |
|
||||
| `/gsd-phase --insert [N]` | 插入紧急工作(小数编号) | 里程碑中途紧急修复 |
|
||||
| `/gsd-phase --remove [N]` | 删除未来阶段并重新编号 | 移除某个功能 |
|
||||
| `/gsd-list-phase-assumptions [N]` | 预览 Claude 的预期方法 | 规划前,验证方向 |
|
||||
| `/gsd-plan-milestone-gaps` | 为审计缺口创建阶段 | 审计发现缺失项后 |
|
||||
| `/gsd-research-phase [N]` | 仅深度生态研究 | 复杂或不熟悉的领域 |
|
||||
| `/gsd-plan-phase --research-phase [N]` | 仅深度生态研究 | 复杂或不熟悉的领域 |
|
||||
| `/gsd-autonomous [--from N] [--to N] [--only N]` | 自主执行剩余阶段(`--to N` 到阶段 N 停止) | 批量自动处理 |
|
||||
| `/gsd-analyze-dependencies` | 检测阶段间依赖关系 | `/gsd-manager` 前分析 |
|
||||
|
||||
@@ -230,10 +229,10 @@
|
||||
| `/gsd-map-codebase` | 分析现有代码库 | 在现有代码上运行 `/gsd-new-project` 之前 |
|
||||
| `/gsd-quick` | 带 GSD 保证的临时任务 | Bug 修复、小功能、配置更改 |
|
||||
| `/gsd-debug [desc] [--diagnose]` | 带持久状态的系统化调试(`--diagnose` 仅诊断) | 出问题时 |
|
||||
| `/gsd-add-todo [desc]` | 捕获想法留待后用 | 会话期间想到什么 |
|
||||
| `/gsd-check-todos` | 列出待处理事项 | 查看捕获的想法 |
|
||||
| `/gsd-capture [desc]` | 捕获想法留待后用 | 会话期间想到什么 |
|
||||
| `/gsd-capture --list` | 列出待处理事项 | 查看捕获的想法 |
|
||||
| `/gsd-settings` | 配置工作流开关和模型配置 | 更改模型、切换代理 |
|
||||
| `/gsd-set-profile <profile>` | 快速切换配置 | 更改成本/质量权衡 |
|
||||
| `/gsd-config --profile <profile>` | 快速切换配置 | 更改成本/质量权衡 |
|
||||
| `/gsd-update --reapply` | 更新后恢复本地修改 | 如果你有本地编辑,在 `/gsd-update` 后 |
|
||||
|
||||
---
|
||||
@@ -390,7 +389,6 @@ claude --dangerously-skip-permissions
|
||||
|
||||
```bash
|
||||
/gsd-audit-milestone # 检查需求覆盖率,检测存根
|
||||
/gsd-plan-milestone-gaps # 如果审计发现缺口,创建阶段来填补
|
||||
/gsd-complete-milestone # 归档,标记,完成
|
||||
```
|
||||
|
||||
@@ -405,11 +403,11 @@ claude --dangerously-skip-permissions
|
||||
### 里程碑中途范围变更
|
||||
|
||||
```bash
|
||||
/gsd-add-phase # 向路线图追加新阶段
|
||||
/gsd-phase # 向路线图追加新阶段
|
||||
# 或
|
||||
/gsd-insert-phase 3 # 在阶段 3 和 4 之间插入紧急工作
|
||||
/gsd-phase --insert 3 # 在阶段 3 和 4 之间插入紧急工作
|
||||
# 或
|
||||
/gsd-remove-phase 7 # 移除阶段 7 并重新编号
|
||||
/gsd-phase --remove 7 # 移除阶段 7 并重新编号
|
||||
```
|
||||
|
||||
---
|
||||
@@ -458,7 +456,7 @@ node gsd-tools.cjs state sync # 从磁盘重建 STATE.md
|
||||
|
||||
### 模型成本太高
|
||||
|
||||
切换到 budget 配置:`/gsd-set-profile budget`。如果领域对你(或 Claude)熟悉,通过 `/gsd-settings` 禁用研究和计划检查代理。
|
||||
切换到 budget 配置:`/gsd-config --profile budget`。如果领域对你(或 Claude)熟悉,通过 `/gsd-settings` 禁用研究和计划检查代理。
|
||||
|
||||
### 处理敏感/私有项目
|
||||
|
||||
@@ -480,13 +478,12 @@ node gsd-tools.cjs state sync # 从磁盘重建 STATE.md
|
||||
|---------|----------|
|
||||
| 丢失上下文 / 新会话 | `/gsd-resume-work` 或 `/gsd-progress` |
|
||||
| 阶段出错 | `git revert` 阶段提交,然后重新规划 |
|
||||
| 需要更改范围 | `/gsd-add-phase`、`/gsd-insert-phase` 或 `/gsd-remove-phase` |
|
||||
| 里程碑审计发现缺口 | `/gsd-plan-milestone-gaps` |
|
||||
| 需要更改范围 | `/gsd-phase`、`/gsd-phase --insert` 或 `/gsd-phase --remove` |
|
||||
| 出问题了 | `/gsd-debug "描述"` |
|
||||
| STATE.md 不同步 | `state validate` 然后 `state sync` |
|
||||
| 快速针对性修复 | `/gsd-quick` |
|
||||
| 计划与你的愿景不符 | `/gsd-discuss-phase [N]` 然后重新规划 |
|
||||
| 成本过高 | `/gsd-set-profile budget` 和 `/gsd-settings` 关闭代理 |
|
||||
| 成本过高 | `/gsd-config --profile budget` 和 `/gsd-settings` 关闭代理 |
|
||||
| 更新破坏了本地更改 | `/gsd-update --reapply` |
|
||||
|
||||
---
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
|
||||
**也可选:**
|
||||
- `/gsd-discuss-phase 2` — 先收集上下文
|
||||
- `/gsd-research-phase 2` — 调查未知项
|
||||
- `/gsd-plan-phase --research-phase 2` — 调查未知项
|
||||
- 审查路线图
|
||||
|
||||
---
|
||||
@@ -128,7 +128,7 @@
|
||||
|
||||
**也可选:**
|
||||
- `/gsd-discuss-phase 3` — 先收集上下文
|
||||
- `/gsd-research-phase 3` — 调查未知项
|
||||
- `/gsd-plan-phase --research-phase 3` — 调查未知项
|
||||
- 回顾阶段 2 构建的内容
|
||||
|
||||
---
|
||||
@@ -149,7 +149,7 @@
|
||||
|
||||
**先讨论上下文:** `/gsd-discuss-phase 3`
|
||||
|
||||
**研究未知项:** `/gsd-research-phase 3`
|
||||
**研究未知项:** `/gsd-plan-phase --research-phase 3`
|
||||
|
||||
<sub>`/clear` 优先 → 全新上下文窗口</sub>
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ Add note that this is the last plan and what comes after:
|
||||
|
||||
**Also available:**
|
||||
- `/gsd-discuss-phase 2` — gather context first
|
||||
- `/gsd-research-phase 2` — investigate unknowns
|
||||
- `/gsd-plan-phase --research-phase 2` — investigate unknowns
|
||||
- Review roadmap
|
||||
|
||||
---
|
||||
@@ -132,7 +132,7 @@ Show completion status before next action:
|
||||
|
||||
**Also available:**
|
||||
- `/gsd-discuss-phase 3` — gather context first
|
||||
- `/gsd-research-phase 3` — investigate unknowns
|
||||
- `/gsd-plan-phase --research-phase 3` — investigate unknowns
|
||||
- Review what Phase 2 built
|
||||
|
||||
---
|
||||
@@ -155,7 +155,7 @@ When there's no clear primary action:
|
||||
|
||||
**To discuss context first:** `/gsd-discuss-phase 3`
|
||||
|
||||
**To research unknowns:** `/gsd-research-phase 3`
|
||||
**To research unknowns:** `/gsd-plan-phase --research-phase 3`
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
@@ -42,8 +42,8 @@ These files live inside a phase directory. They are NOT checked by W019 (which o
|
||||
| `NN-MM-PLAN.md` | `phase-prompt.md` | `/gsd-plan-phase` | Executable implementation plan |
|
||||
| `NN-MM-SUMMARY.md` | `summary.md` | `/gsd-execute-phase` | Post-execution summary with learnings |
|
||||
| `NN-CONTEXT.md` | `context.md` | `/gsd-discuss-phase` | Scoped discussion decisions for the phase |
|
||||
| `NN-RESEARCH.md` | `research.md` | `/gsd-research-phase`, `/gsd-plan-phase` | Technical research for the phase |
|
||||
| `NN-VALIDATION.md` | `VALIDATION.md` | `/gsd-research-phase` (Nyquist) | Validation architecture (Nyquist method) |
|
||||
| `NN-RESEARCH.md` | `research.md` | `/gsd-plan-phase`, `/gsd-plan-phase --research-phase <N>` | Technical research for the phase |
|
||||
| `NN-VALIDATION.md` | `VALIDATION.md` | `/gsd-plan-phase` (Nyquist) | Validation architecture (Nyquist method) |
|
||||
| `NN-UAT.md` | `UAT.md` | `/gsd-validate-phase` | User acceptance test results |
|
||||
| `NN-PATTERNS.md` | *(inline)* | `/gsd-plan-phase` (pattern mapper) | Analog file mapping for the phase |
|
||||
| `NN-UI-SPEC.md` | `UI-SPEC.md` | `/gsd-ui-phase` | UI design contract |
|
||||
|
||||
@@ -4,7 +4,7 @@ Template for `.planning/phases/XX-name/DISCOVERY.md` - shallow research for libr
|
||||
|
||||
**Purpose:** Answer "which library/option should we use" questions during mandatory discovery in plan-phase.
|
||||
|
||||
For deep ecosystem research ("how do experts build this"), use `/gsd-research-phase` which produces RESEARCH.md.
|
||||
For deep ecosystem research ("how do experts build this"), use `/gsd-plan-phase --research-phase` which produces RESEARCH.md.
|
||||
|
||||
---
|
||||
|
||||
@@ -142,5 +142,5 @@ Create `.planning/phases/XX-name/DISCOVERY.md`:
|
||||
- Niche/complex domains (3D, games, audio, shaders)
|
||||
- Need ecosystem knowledge, not just library choice
|
||||
- "How do experts build this" questions
|
||||
- Use `/gsd-research-phase` for these
|
||||
- Use `/gsd-plan-phase --research-phase` for these
|
||||
</guidelines>
|
||||
|
||||
@@ -156,7 +156,7 @@ Updated after each plan completion.
|
||||
**Pending Todos:** Ideas captured via /gsd-add-todo
|
||||
- Count of pending todos
|
||||
- Reference to .planning/todos/pending/
|
||||
- Brief list if few, count if many (e.g., "5 pending todos — see /gsd-check-todos")
|
||||
- Brief list if few, count if many (e.g., "5 pending todos — see /gsd-capture --list")
|
||||
|
||||
**Blockers/Concerns:** From "Next Phase Readiness" sections
|
||||
- Issues that affect future work
|
||||
|
||||
@@ -143,7 +143,7 @@ Would you like to:
|
||||
|
||||
1. Continue with current work
|
||||
2. Add another todo
|
||||
3. View all todos (/gsd-check-todos)
|
||||
3. View all todos (/gsd-capture --list)
|
||||
```
|
||||
</step>
|
||||
|
||||
|
||||
@@ -37,8 +37,8 @@ Exit.
|
||||
|
||||
<step name="parse_filter">
|
||||
Check for area filter in arguments:
|
||||
- `/gsd-check-todos` → show all
|
||||
- `/gsd-check-todos api` → filter to area:api only
|
||||
- `/gsd-capture --list` → show all
|
||||
- `/gsd-capture --list api` → filter to area:api only
|
||||
</step>
|
||||
|
||||
<step name="list_todos">
|
||||
@@ -56,7 +56,7 @@ Pending Todos:
|
||||
---
|
||||
|
||||
Reply with a number to view details, or:
|
||||
- `/gsd-check-todos [area]` to filter by area
|
||||
- `/gsd-capture --list [area]` to filter by area
|
||||
- `q` to exit
|
||||
```
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ fi
|
||||
**Phase validation (before config gate):**
|
||||
If `phase_found` is false, report error and exit:
|
||||
```
|
||||
Error: Phase ${PHASE_ARG} not found. Run /gsd-status to see available phases.
|
||||
Error: Phase ${PHASE_ARG} not found. Run /gsd-progress to see available phases.
|
||||
```
|
||||
|
||||
This runs BEFORE config gate check so user errors are surfaced immediately regardless of config state.
|
||||
|
||||
@@ -35,7 +35,7 @@ fi
|
||||
**Phase validation (before config gate):**
|
||||
If `phase_found` is false, report error and exit:
|
||||
```
|
||||
Error: Phase ${PHASE_ARG} not found. Run /gsd-status to see available phases.
|
||||
Error: Phase ${PHASE_ARG} not found. Run /gsd-progress to see available phases.
|
||||
```
|
||||
|
||||
This runs BEFORE config gate check so user errors are surfaced immediately regardless of config state.
|
||||
|
||||
@@ -4,7 +4,7 @@ Produces DISCOVERY.md (for Level 2-3) that informs PLAN.md creation.
|
||||
|
||||
Called from plan-phase.md's mandatory_discovery step with a depth parameter.
|
||||
|
||||
NOTE: For comprehensive ecosystem research ("how do experts build this"), use /gsd-research-phase instead, which produces RESEARCH.md.
|
||||
NOTE: For comprehensive ecosystem research ("how do experts build this"), use /gsd-plan-phase --research-phase instead, which produces RESEARCH.md.
|
||||
</purpose>
|
||||
|
||||
<depth_levels>
|
||||
|
||||
@@ -80,10 +80,12 @@ Usage: `/gsd-discuss-phase 2`
|
||||
Usage: `/gsd-discuss-phase 2 --batch`
|
||||
Usage: `/gsd-discuss-phase 2 --batch=3`
|
||||
|
||||
**`/gsd-plan-phase <number> [--skip-research] [--gaps] [--skip-verify] [--tdd] [--mvp]`**
|
||||
**`/gsd-plan-phase <number> [--research] [--skip-research] [--research-phase <N>] [--view] [--gaps] [--skip-verify] [--tdd] [--mvp]`**
|
||||
Create detailed execution plan for a specific phase.
|
||||
|
||||
- `--skip-research` — bypass the research subagent
|
||||
- `--research-phase <N>` — research-only mode. Spawns the research agent for phase `<N>`, writes `RESEARCH.md`, then exits before the planner runs. Useful for cross-phase research, doc review before committing to a planning approach, and correction-without-replanning loops. Replaces the deleted `gsd-research-phase` standalone command (#3042).
|
||||
- Modifiers: `--research` forces refresh (re-spawn researcher, no prompt). `--view` prints existing `RESEARCH.md` to stdout without spawning. With neither, prompts `update / view / skip` if `RESEARCH.md` already exists.
|
||||
- `--gaps` — focus only on closing gaps from a prior plan-check
|
||||
- `--skip-verify` — skip the post-plan verifier loop
|
||||
- `--tdd` — plan in test-driven order (tests before code)
|
||||
@@ -95,6 +97,9 @@ Create detailed execution plan for a specific phase.
|
||||
- Multiple plans per phase supported (XX-01, XX-02, etc.)
|
||||
|
||||
Usage: `/gsd-plan-phase 1`
|
||||
Usage: `/gsd-plan-phase --research-phase 2` — research only on phase 2 (prompts if `RESEARCH.md` exists)
|
||||
Usage: `/gsd-plan-phase --research-phase 2 --view` — print existing `RESEARCH.md`, no spawn
|
||||
Usage: `/gsd-plan-phase --research-phase 2 --research` — force-refresh, no prompt
|
||||
Result: Creates `.planning/phases/01-foundation/01-01-PLAN.md`
|
||||
|
||||
**PRD Express Path:** Pass `--prd path/to/requirements.md` to skip discuss-phase entirely. Your PRD becomes locked decisions in CONTEXT.md. Useful when you already have clear acceptance criteria.
|
||||
|
||||
@@ -25,7 +25,7 @@ Parse JSON for: `workspace_base`, `workspaces`, `workspace_count`.
|
||||
No workspaces found in ~/gsd-workspaces/
|
||||
|
||||
Create one with:
|
||||
/gsd-new-workspace --name my-workspace --repos repo1,repo2
|
||||
/gsd-workspace --new --name my-workspace --repos repo1,repo2
|
||||
```
|
||||
|
||||
Done.
|
||||
|
||||
@@ -73,7 +73,7 @@ Error:
|
||||
No git repos found in the current directory and this is not a git repo.
|
||||
|
||||
Run this command from a directory containing git repos, or specify repos explicitly:
|
||||
/gsd-new-workspace --name my-workspace --repos /path/to/repo1,/path/to/repo2
|
||||
/gsd-workspace --new --name my-workspace --repos /path/to/repo1,/path/to/repo2
|
||||
```
|
||||
Exit.
|
||||
|
||||
@@ -84,7 +84,7 @@ Error:
|
||||
Error: --auto requires --repos to specify which repos to include.
|
||||
|
||||
Usage:
|
||||
/gsd-new-workspace --name my-workspace --repos repo1,repo2 --auto
|
||||
/gsd-workspace --new --name my-workspace --repos repo1,repo2 --auto
|
||||
```
|
||||
Exit.
|
||||
|
||||
|
||||
@@ -54,7 +54,26 @@ 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`, `--chunked`).
|
||||
Extract from $ARGUMENTS: phase number (integer or decimal like `2.1`), flags (`--research`, `--skip-research`, `--research-phase <N>`, `--gaps`, `--skip-verify`, `--skip-ui`, `--prd <filepath>`, `--reviews`, `--text`, `--bounce`, `--skip-bounce`, `--chunked`).
|
||||
|
||||
**`--research-phase <N>` — research-only mode (#3042 + #3044).** When this flag is present, parse `<N>` as the phase number (overrides any positional phase argument), set `RESEARCH_ONLY=true`, and treat the rest of this workflow as a research-dispatch only — the planner spawn (step 8), plan-checker, verification, gaps, bounce, and post-planning-gaps blocks all skip on `RESEARCH_ONLY`. Use this for cross-phase research, doc review before committing to a planning approach, and correction-without-replanning loops. Replaces the deleted `/gsd-research-phase` command.
|
||||
|
||||
In research-only mode, two modifiers control behavior when `RESEARCH.md` already exists:
|
||||
|
||||
- **`--research`** — force-refresh re-research without prompting. Re-spawns the researcher unconditionally and overwrites the existing RESEARCH.md. (This is the existing `--research` flag's standard "force re-research" semantics, reused here.)
|
||||
- **`--view`** — view-only: print existing `RESEARCH.md` to stdout, do **not** spawn the researcher. Sets `VIEW_ONLY=true`. Cheapest mode for the correction-without-replanning loop. If `RESEARCH.md` does not exist, error with a hint to drop `--view`.
|
||||
|
||||
```bash
|
||||
RESEARCH_ONLY=false
|
||||
VIEW_ONLY=false
|
||||
if [[ "$ARGUMENTS" =~ --research-phase[[:space:]]+([0-9]+(\.[0-9]+)?) ]]; then
|
||||
RESEARCH_ONLY=true
|
||||
PHASE="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
if $RESEARCH_ONLY && [[ "$ARGUMENTS" =~ (^|[[:space:]])--view([[:space:]]|$) ]]; then
|
||||
VIEW_ONLY=true
|
||||
fi
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
@@ -302,6 +321,27 @@ Pass `ai_spec_path` and `framework_line` to planner in step 7 so it can referenc
|
||||
|
||||
**Skip if:** `--gaps` flag or `--skip-research` flag or `--reviews` flag.
|
||||
|
||||
### 5.0. Research-Only Modifiers (`--view`, `--research`, prompt)
|
||||
|
||||
**Skip if:** `RESEARCH_ONLY` is `false`.
|
||||
|
||||
Three branches in research-only mode (`--research-phase <N>`):
|
||||
|
||||
1. **`--view`** (or user picks "View" in the prompt below): print `RESEARCH.md` to stdout, no spawn, exit. If `RESEARCH.md` is missing, error with: `--view requires an existing RESEARCH.md; drop --view to spawn the researcher.`
|
||||
2. **`--research`** (force-refresh): re-spawn researcher unconditionally — fall through to "Spawn gsd-phase-researcher" below.
|
||||
3. **Neither flag AND `has_research=true`:** emit `RESEARCH.md already exists for Phase ${PHASE}.` and prompt the user with three choices: `1. Update — re-spawn researcher and refresh RESEARCH.md`, `2. View — print existing RESEARCH.md and exit (no spawn)`, `3. Skip — exit without spawning or printing`. Map "Update" → fall through to spawn, "View" → set `VIEW_ONLY=true` and emit RESEARCH.md as in (1), "Skip" → exit cleanly. Mirrors the deleted `/gsd-research-phase` standalone's existing-artifact menu (#3042 parity).
|
||||
|
||||
```bash
|
||||
if [[ "$VIEW_ONLY" == "true" ]]; then
|
||||
[[ -f "$research_path" ]] || { echo "Error: --view requires an existing RESEARCH.md (Phase ${PHASE}). Drop --view to spawn the researcher."; exit 1; }
|
||||
cat "$research_path"; exit 0
|
||||
fi
|
||||
```
|
||||
|
||||
### 5.1. Standard Research Decision
|
||||
|
||||
**Skip if** `RESEARCH_ONLY=true` (the research-only mode in 5.0 already determined the path: spawn or exit). Without this guard, an LLM following the workflow could fall through into "use existing, skip to step 6" → planner spawn, violating the research-only contract. **CR #3045 finding: this gate makes the early-exit unreachable from any non-research-only branch.**
|
||||
|
||||
**If `has_research` is true (from init) AND no `--research` flag:** Use existing, skip to step 6.
|
||||
|
||||
**If RESEARCH.md missing OR `--research` flag:**
|
||||
@@ -398,6 +438,24 @@ Task(
|
||||
- **`## RESEARCH COMPLETE`:** Display confirmation, continue to step 6
|
||||
- **`## RESEARCH BLOCKED`:** Display blocker, offer: 1) Provide context, 2) Skip research, 3) Abort
|
||||
|
||||
### Research-Only Early Exit (`--research-phase`)
|
||||
|
||||
**Skip if:** `RESEARCH_ONLY` is `false` (the default).
|
||||
|
||||
**If `RESEARCH_ONLY=true`:** the user invoked `/gsd-plan-phase --research-phase <N>` for research-only mode. Do **not** continue to Section 5.5+ (validation strategy, planner, plan-checker, verification, gaps, bounce, post-planning-gaps). Print the research-complete summary and exit cleanly:
|
||||
|
||||
```text
|
||||
✓ Research-only mode complete (#3042)
|
||||
|
||||
Phase: ${PHASE}
|
||||
RESEARCH.md: ${research_path}
|
||||
|
||||
Re-run /gsd-plan-phase ${PHASE} to plan the phase using this research,
|
||||
or /gsd-plan-phase ${PHASE} --research to refresh research and plan.
|
||||
```
|
||||
|
||||
This exits the workflow. The planner / plan-checker / verifier blocks below are skipped.
|
||||
|
||||
## 5.5. Create Validation Strategy
|
||||
|
||||
Skip if `nyquist_validation_enabled` is false OR `research_enabled` is false.
|
||||
|
||||
@@ -128,7 +128,7 @@ CONTEXT: [✓ if has_context | - if not]
|
||||
- [e.g. jq -r '.blockers[].text' from state-snapshot]
|
||||
|
||||
## Pending Todos
|
||||
- [count] pending — /gsd-check-todos to review
|
||||
- [count] pending — /gsd-capture --list to review
|
||||
|
||||
## Active Debug Sessions
|
||||
- [count] active — /gsd-debug to continue
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
<purpose>
|
||||
Research how to implement a phase. Spawns gsd-phase-researcher with phase context.
|
||||
|
||||
Standalone research command. For most workflows, use `/gsd-plan-phase` which integrates research automatically.
|
||||
</purpose>
|
||||
|
||||
<available_agent_types>
|
||||
Valid GSD subagent types (use exact names — do not fall back to 'general-purpose'):
|
||||
- gsd-phase-researcher — Researches technical approaches for a phase
|
||||
</available_agent_types>
|
||||
|
||||
<process>
|
||||
|
||||
## Step 0: Resolve Model Profile
|
||||
|
||||
@~/.claude/get-shit-done/references/model-profile-resolution.md
|
||||
|
||||
Resolve model for:
|
||||
- `gsd-phase-researcher`
|
||||
|
||||
## Step 1: Normalize and Validate Phase
|
||||
|
||||
@~/.claude/get-shit-done/references/phase-argument-parsing.md
|
||||
|
||||
```bash
|
||||
PHASE_INFO=$(gsd-sdk query roadmap.get-phase "${PHASE}")
|
||||
```
|
||||
|
||||
If `found` is false: Error and exit.
|
||||
|
||||
## Step 2: Check Existing Research
|
||||
|
||||
```bash
|
||||
ls .planning/phases/${PHASE}-*/RESEARCH.md 2>/dev/null || true
|
||||
```
|
||||
|
||||
If exists: Offer update/view/skip options.
|
||||
|
||||
## Step 3: Gather Phase Context
|
||||
|
||||
```bash
|
||||
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-phase-researcher)
|
||||
```
|
||||
|
||||
## Step 4: Spawn Researcher
|
||||
|
||||
```
|
||||
Task(
|
||||
prompt="<objective>
|
||||
Research implementation approach for Phase {phase}: {name}
|
||||
</objective>
|
||||
|
||||
<files_to_read>
|
||||
- {context_path} (USER DECISIONS from /gsd-discuss-phase)
|
||||
- {requirements_path} (Project requirements)
|
||||
- {state_path} (Project decisions and history)
|
||||
</files_to_read>
|
||||
|
||||
${AGENT_SKILLS_RESEARCHER}
|
||||
|
||||
<additional_context>
|
||||
Phase description: {description}
|
||||
</additional_context>
|
||||
|
||||
<output>
|
||||
Write to: .planning/phases/${PHASE}-{slug}/${PHASE}-RESEARCH.md
|
||||
</output>",
|
||||
subagent_type="gsd-phase-researcher",
|
||||
model="{researcher_model}"
|
||||
)
|
||||
```
|
||||
|
||||
> **ORCHESTRATOR RULE — CODEX RUNTIME**: After calling Task() above, stop working on this task immediately. Do not read more files, edit code, or run tests related to this task while the subagent is active. Wait for the subagent to return its result. This prevents duplicate work, conflicting edits, and wasted context. Only resume when the subagent result is available.
|
||||
|
||||
## Step 5: Handle Return
|
||||
|
||||
- `## RESEARCH COMPLETE` — Display summary, offer: Plan/Dig deeper/Review/Done
|
||||
- `## CHECKPOINT REACHED` — Present to user, spawn continuation
|
||||
- `## RESEARCH INCONCLUSIVE` — Show attempts, offer: Add context/Try different mode/Manual
|
||||
|
||||
</process>
|
||||
@@ -140,7 +140,7 @@ Present complete project status to user:
|
||||
Resume with: Task tool (resume parameter with agent ID)
|
||||
|
||||
[If pending todos exist:]
|
||||
📋 [N] pending todos — /gsd-check-todos to review
|
||||
📋 [N] pending todos — /gsd-capture --list to review
|
||||
|
||||
[If blockers exist:]
|
||||
⚠️ Carried concerns:
|
||||
@@ -257,7 +257,7 @@ Based on user selection, route to appropriate workflow:
|
||||
|
||||
**Also available:**
|
||||
- `/gsd-discuss-phase [N] ${GSD_WS}` — gather context first
|
||||
- `/gsd-research-phase [N] ${GSD_WS}` — investigate unknowns
|
||||
- `/gsd-plan-phase --research-phase [N] ${GSD_WS}` — investigate unknowns
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
@@ -505,7 +505,7 @@ Exit skill and invoke SlashCommand("/gsd-discuss-phase [X+1] --auto ${GSD_WS}")
|
||||
|
||||
**Also available:**
|
||||
- `/gsd-plan-phase [X+1] ${GSD_WS}` — skip discussion, plan directly
|
||||
- `/gsd-research-phase [X+1] ${GSD_WS}` — investigate unknowns
|
||||
- `/gsd-plan-phase --research-phase [X+1] ${GSD_WS}` — investigate unknowns
|
||||
|
||||
---
|
||||
```
|
||||
@@ -530,7 +530,7 @@ Exit skill and invoke SlashCommand("/gsd-discuss-phase [X+1] --auto ${GSD_WS}")
|
||||
|
||||
**Also available:**
|
||||
- `/gsd-discuss-phase [X+1] ${GSD_WS}` — revisit context
|
||||
- `/gsd-research-phase [X+1] ${GSD_WS}` — investigate unknowns
|
||||
- `/gsd-plan-phase --research-phase [X+1] ${GSD_WS}` — investigate unknowns
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
@@ -10,13 +10,7 @@
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { STATE_COMMAND_MANIFEST } from '../src/query/command-manifest.state.js';
|
||||
import { VERIFY_COMMAND_MANIFEST } from '../src/query/command-manifest.verify.js';
|
||||
import { INIT_COMMAND_MANIFEST } from '../src/query/command-manifest.init.js';
|
||||
import { PHASE_COMMAND_MANIFEST } from '../src/query/command-manifest.phase.js';
|
||||
import { PHASES_COMMAND_MANIFEST } from '../src/query/command-manifest.phases.js';
|
||||
import { VALIDATE_COMMAND_MANIFEST } from '../src/query/command-manifest.validate.js';
|
||||
import { ROADMAP_COMMAND_MANIFEST } from '../src/query/command-manifest.roadmap.js';
|
||||
import { COMMAND_DEFINITIONS_BY_FAMILY } from '../src/query/command-definition.js';
|
||||
|
||||
function toSubcommand(canonical: string, family: 'state' | 'verify' | 'init' | 'phase' | 'phases' | 'validate' | 'roadmap'): string {
|
||||
const prefix = `${family}.`;
|
||||
@@ -24,49 +18,49 @@ function toSubcommand(canonical: string, family: 'state' | 'verify' | 'init' | '
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const stateEntries = STATE_COMMAND_MANIFEST.map((entry) => ({
|
||||
const stateEntries = COMMAND_DEFINITIONS_BY_FAMILY.state.map((entry) => ({
|
||||
canonical: entry.canonical,
|
||||
aliases: entry.aliases,
|
||||
subcommand: toSubcommand(entry.canonical, 'state'),
|
||||
mutation: entry.mutation,
|
||||
}));
|
||||
|
||||
const verifyEntries = VERIFY_COMMAND_MANIFEST.map((entry) => ({
|
||||
const verifyEntries = COMMAND_DEFINITIONS_BY_FAMILY.verify.map((entry) => ({
|
||||
canonical: entry.canonical,
|
||||
aliases: entry.aliases,
|
||||
subcommand: toSubcommand(entry.canonical, 'verify'),
|
||||
mutation: entry.mutation,
|
||||
}));
|
||||
|
||||
const initEntries = INIT_COMMAND_MANIFEST.map((entry) => ({
|
||||
const initEntries = COMMAND_DEFINITIONS_BY_FAMILY.init.map((entry) => ({
|
||||
canonical: entry.canonical,
|
||||
aliases: entry.aliases,
|
||||
subcommand: toSubcommand(entry.canonical, 'init'),
|
||||
mutation: entry.mutation,
|
||||
}));
|
||||
|
||||
const phaseEntries = PHASE_COMMAND_MANIFEST.map((entry) => ({
|
||||
const phaseEntries = COMMAND_DEFINITIONS_BY_FAMILY.phase.map((entry) => ({
|
||||
canonical: entry.canonical,
|
||||
aliases: entry.aliases,
|
||||
subcommand: toSubcommand(entry.canonical, 'phase'),
|
||||
mutation: entry.mutation,
|
||||
}));
|
||||
|
||||
const phasesEntries = PHASES_COMMAND_MANIFEST.map((entry) => ({
|
||||
const phasesEntries = COMMAND_DEFINITIONS_BY_FAMILY.phases.map((entry) => ({
|
||||
canonical: entry.canonical,
|
||||
aliases: entry.aliases,
|
||||
subcommand: toSubcommand(entry.canonical, 'phases'),
|
||||
mutation: entry.mutation,
|
||||
}));
|
||||
|
||||
const validateEntries = VALIDATE_COMMAND_MANIFEST.map((entry) => ({
|
||||
const validateEntries = COMMAND_DEFINITIONS_BY_FAMILY.validate.map((entry) => ({
|
||||
canonical: entry.canonical,
|
||||
aliases: entry.aliases,
|
||||
subcommand: toSubcommand(entry.canonical, 'validate'),
|
||||
mutation: entry.mutation,
|
||||
}));
|
||||
|
||||
const roadmapEntries = ROADMAP_COMMAND_MANIFEST.map((entry) => ({
|
||||
const roadmapEntries = COMMAND_DEFINITIONS_BY_FAMILY.roadmap.map((entry) => ({
|
||||
canonical: entry.canonical,
|
||||
aliases: entry.aliases,
|
||||
subcommand: toSubcommand(entry.canonical, 'roadmap'),
|
||||
@@ -98,22 +92,7 @@ async function main(): Promise<void> {
|
||||
'export const VALIDATE_SUBCOMMANDS = new Set<string>(VALIDATE_COMMAND_ALIASES.map((entry) => entry.subcommand));',
|
||||
'export const ROADMAP_SUBCOMMANDS = new Set<string>(ROADMAP_COMMAND_ALIASES.map((entry) => entry.subcommand));',
|
||||
'',
|
||||
'export const STATE_MUTATION_COMMANDS: readonly string[] = STATE_COMMAND_ALIASES',
|
||||
' .filter((entry) => entry.mutation)',
|
||||
' .flatMap((entry) => [entry.canonical, ...entry.aliases]);',
|
||||
'',
|
||||
'export const PHASE_MUTATION_COMMANDS: readonly string[] = PHASE_COMMAND_ALIASES',
|
||||
' .filter((entry) => entry.mutation)',
|
||||
' .flatMap((entry) => [entry.canonical, ...entry.aliases]);',
|
||||
'',
|
||||
'export const PHASES_MUTATION_COMMANDS: readonly string[] = PHASES_COMMAND_ALIASES',
|
||||
' .filter((entry) => entry.mutation)',
|
||||
' .flatMap((entry) => [entry.canonical, ...entry.aliases]);',
|
||||
'',
|
||||
'export const ROADMAP_MUTATION_COMMANDS: readonly string[] = ROADMAP_COMMAND_ALIASES',
|
||||
' .filter((entry) => entry.mutation)',
|
||||
' .flatMap((entry) => [entry.canonical, ...entry.aliases]);',
|
||||
'',
|
||||
|
||||
].join('\n');
|
||||
await writeFile(outPath, header + body, 'utf-8');
|
||||
}
|
||||
|
||||
189
sdk/src/cli.ts
189
sdk/src/cli.ts
@@ -7,7 +7,6 @@
|
||||
*/
|
||||
|
||||
import { parseArgs } from 'node:util';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { resolve, join, isAbsolute } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -19,6 +18,7 @@ import { InitRunner } from './init-runner.js';
|
||||
import { validateWorkstreamName } from './workstream-utils.js';
|
||||
import { loadConfig } from './config.js';
|
||||
import { assertRuntimeSupportsAutoMode } from './runtime-gate.js';
|
||||
import { runQueryCliCommand } from './query/query-cli-adapter.js';
|
||||
|
||||
// ─── Parsed CLI args ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -277,56 +277,6 @@ async function readStdin(): Promise<string> {
|
||||
});
|
||||
}
|
||||
|
||||
/** When false, unknown `gsd-sdk query` commands error instead of shelling out to gsd-tools.cjs. */
|
||||
function queryFallbackToCjsEnabled(): boolean {
|
||||
const v = process.env.GSD_QUERY_FALLBACK?.toLowerCase();
|
||||
if (v === 'off' || v === 'never' || v === 'false' || v === '0') return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
async function parseCliQueryJsonOutput(raw: string, projectDir: string): Promise<unknown> {
|
||||
const trimmed = raw.trim();
|
||||
if (trimmed === '') return null;
|
||||
let jsonStr = trimmed;
|
||||
if (jsonStr.startsWith('@file:')) {
|
||||
const rel = jsonStr.slice(6).trim();
|
||||
const { resolvePathUnderProject } = await import('./query/helpers.js');
|
||||
const filePath = await resolvePathUnderProject(projectDir, rel);
|
||||
jsonStr = await readFile(filePath, 'utf-8');
|
||||
}
|
||||
return JSON.parse(jsonStr);
|
||||
}
|
||||
|
||||
/** Map registry-style dotted command tokens to gsd-tools.cjs argv (space-separated subcommands). */
|
||||
function dottedCommandToCjsArgv(normCmd: string, normArgs: string[]): string[] {
|
||||
if (normCmd.includes('.')) {
|
||||
return [...normCmd.split('.'), ...normArgs];
|
||||
}
|
||||
return [normCmd, ...normArgs];
|
||||
}
|
||||
|
||||
function execGsdToolsCjsQuery(
|
||||
projectDir: string,
|
||||
gsdToolsPath: string,
|
||||
normCmd: string,
|
||||
normArgs: string[],
|
||||
ws: string | undefined,
|
||||
): Promise<{ stdout: string; stderr: string }> {
|
||||
const cjsArgv = dottedCommandToCjsArgv(normCmd, normArgs);
|
||||
const wsSuffix = ws ? ['--ws', ws] : [];
|
||||
const fullArgv = [gsdToolsPath, ...cjsArgv, ...wsSuffix];
|
||||
return new Promise((resolve, reject) => {
|
||||
execFile(
|
||||
process.execPath,
|
||||
fullArgv,
|
||||
{ cwd: projectDir, maxBuffer: 10 * 1024 * 1024, env: { ...process.env } },
|
||||
(err, stdout, stderr) => {
|
||||
if (err) reject(err);
|
||||
else resolve({ stdout: stdout?.toString() ?? '', stderr: stderr?.toString() ?? '' });
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Main ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -360,144 +310,35 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<void
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── Query command ──────────────────────────────────────────────────────
|
||||
if (args.command === 'query') {
|
||||
const result = await runQueryCliCommand({
|
||||
projectDir: args.projectDir,
|
||||
ws: args.ws,
|
||||
queryArgv: args.queryArgv,
|
||||
});
|
||||
for (const line of result.stderrLines) console.error(line);
|
||||
for (const chunk of result.stdoutChunks) process.stdout.write(chunk);
|
||||
process.exitCode = result.exitCode;
|
||||
return;
|
||||
}
|
||||
|
||||
// Fall back to GSD_WORKSTREAM env var when --ws is not supplied (#2791).
|
||||
// gsd-tools.cjs resolves the active workstream via this env var; parity
|
||||
// means gsd-sdk query commands see the same .planning/ path as gsd-tools.
|
||||
// means gsd-sdk command paths see the same .planning/ path as gsd-tools.
|
||||
if (args.ws === undefined && process.env.GSD_WORKSTREAM) {
|
||||
const envWs = process.env.GSD_WORKSTREAM;
|
||||
if (validateWorkstreamName(envWs)) {
|
||||
args = { ...args, ws: envWs };
|
||||
}
|
||||
// If the env var contains an invalid name, silently ignore it (same as CJS).
|
||||
}
|
||||
|
||||
// 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');
|
||||
const { extractField, resolveQueryArgv } = await import('./query/registry.js');
|
||||
const { GSDToolsError } = await import('./gsd-tools.js');
|
||||
const { GSDError, exitCodeFor, ErrorClassification } = await import('./errors.js');
|
||||
|
||||
const queryArgs = args.queryArgv ?? [];
|
||||
|
||||
// Extract --pick before dispatch
|
||||
const pickIdx = queryArgs.indexOf('--pick');
|
||||
let pickField: string | undefined;
|
||||
if (pickIdx !== -1) {
|
||||
if (pickIdx + 1 >= queryArgs.length) {
|
||||
console.error('Error: --pick requires a field name');
|
||||
process.exitCode = 10;
|
||||
return;
|
||||
}
|
||||
pickField = queryArgs[pickIdx + 1];
|
||||
queryArgs.splice(pickIdx, 2);
|
||||
}
|
||||
|
||||
if (queryArgs.length === 0 || !queryArgs[0]) {
|
||||
console.error('Error: "gsd-sdk query" requires a command');
|
||||
process.exitCode = 10;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const queryCommand = queryArgs[0];
|
||||
const { normalizeQueryCommand } = await import('./query/normalize-query-command.js');
|
||||
const [normCmd, normArgs] = normalizeQueryCommand(queryCommand, queryArgs.slice(1));
|
||||
if (!normCmd || !String(normCmd).trim()) {
|
||||
console.error('Error: "gsd-sdk query" requires a command');
|
||||
process.exitCode = 10;
|
||||
return;
|
||||
}
|
||||
const registry = createRegistry();
|
||||
const tokens = [normCmd, ...normArgs];
|
||||
const matched = resolveQueryArgv(tokens, registry);
|
||||
if (!matched) {
|
||||
if (!queryFallbackToCjsEnabled()) {
|
||||
throw new GSDError(
|
||||
`Unknown command: "${tokens.join(' ')}". Use a registered \`gsd-sdk query\` subcommand (see sdk/src/query/QUERY-HANDLERS.md) or invoke \`node …/gsd-tools.cjs\` for CJS-only operations. Set GSD_QUERY_FALLBACK=registered (default) to allow automatic fallback.`,
|
||||
ErrorClassification.Validation,
|
||||
);
|
||||
}
|
||||
const { resolveGsdToolsPath } = await import('./gsd-tools.js');
|
||||
const gsdPath = resolveGsdToolsPath(args.projectDir);
|
||||
console.error(
|
||||
`[gsd-sdk] '${tokens.join(' ')}' not in native registry; falling back to gsd-tools.cjs.`,
|
||||
);
|
||||
console.error('[gsd-sdk] Transparent bridge — prefer adding a native handler when parity matters.');
|
||||
const { stdout, stderr } = await execGsdToolsCjsQuery(
|
||||
args.projectDir,
|
||||
gsdPath,
|
||||
normCmd,
|
||||
normArgs,
|
||||
args.ws,
|
||||
);
|
||||
if (stderr.trim()) console.error(stderr.trimEnd());
|
||||
// #3026 CR (Major outside-diff): the gsd-tools.cjs fallback now
|
||||
// emits plain-text usage on --help / -h with exit 0, instead of
|
||||
// a JSON object. Wrap the JSON parse in a try/catch and forward
|
||||
// non-JSON stdout verbatim so subcommand help reaches the user.
|
||||
// (Previously this path JSON.parsed the help text and threw
|
||||
// "Unexpected token 'U'" — exitCode=1 — a regression introduced
|
||||
// alongside the --help passthrough fix.)
|
||||
let output: unknown;
|
||||
try {
|
||||
output = await parseCliQueryJsonOutput(stdout, args.projectDir);
|
||||
} catch {
|
||||
if (stdout.trim()) {
|
||||
process.stdout.write(stdout.endsWith('\n') ? stdout : stdout + '\n');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (pickField) {
|
||||
output = extractField(output, pickField);
|
||||
}
|
||||
console.log(JSON.stringify(output, null, 2));
|
||||
} else {
|
||||
const result = await registry.dispatch(matched.cmd, matched.args, args.projectDir, args.ws);
|
||||
let output: unknown = result.data;
|
||||
|
||||
if (pickField) {
|
||||
output = extractField(output, pickField);
|
||||
}
|
||||
|
||||
// Handlers can signal format:'text' to emit a raw string (e.g. agent-skills
|
||||
// emits an <agent_skills> XML block workflows embed via $(...) substitution).
|
||||
if (!pickField && result.format === 'text' && typeof output === 'string') {
|
||||
process.stdout.write(output);
|
||||
} else {
|
||||
console.log(JSON.stringify(output, null, 2));
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof GSDError) {
|
||||
console.error(`Error: ${err.message}`);
|
||||
process.exitCode = exitCodeFor(err.classification);
|
||||
} else if (err instanceof GSDToolsError) {
|
||||
console.error(`Error: ${err.message}`);
|
||||
process.exitCode = err.exitCode ?? 1;
|
||||
} else {
|
||||
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.command !== 'run' && args.command !== 'init' && args.command !== 'auto') {
|
||||
console.error('Error: Expected "gsd-sdk run <prompt>", "gsd-sdk auto", "gsd-sdk init [input]", or "gsd-sdk query <command>"');
|
||||
console.error(USAGE);
|
||||
|
||||
@@ -181,32 +181,31 @@ describe('loadConfig', () => {
|
||||
// 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.
|
||||
// Mirrors current CJS parity expectations for SDK loadConfig + resolveModel:
|
||||
// in pre-project context, loadConfig ignores ~/.gsd/defaults.json so
|
||||
// resolveModel/MODEL_PROFILES do not emit aliases when resolve_model_ids
|
||||
// is "omit". Once a project is initialized, config.json is authoritative,
|
||||
// because buildNewProjectConfig bakes user defaults into project config
|
||||
// at /gsd:new-project time.
|
||||
|
||||
it('pre-project: layers user defaults from ~/.gsd/defaults.json', async () => {
|
||||
it('pre-project: ignores user defaults and uses built-in defaults', 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 as Record<string, unknown>).resolve_model_ids).toBeUndefined();
|
||||
expect(config.model_profile).toBe('balanced');
|
||||
expect(config.workflow.plan_check).toBe(true);
|
||||
});
|
||||
|
||||
it('pre-project: deep-merges nested keys from user defaults', async () => {
|
||||
it('pre-project: keeps built-in nested defaults even when user defaults exist', 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.branching_strategy).toBe('none');
|
||||
expect(config.git.phase_branch_template).toBe('gsd/phase-{phase}-{slug}');
|
||||
expect(config.agent_skills).toEqual({ planner: 'user-skill' });
|
||||
expect(config.agent_skills).toEqual({});
|
||||
});
|
||||
|
||||
it('project config is authoritative over user defaults (CJS parity)', async () => {
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
*/
|
||||
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { relPlanningPath } from './workstream-utils.js';
|
||||
|
||||
@@ -27,6 +26,8 @@ export interface WorkflowConfig {
|
||||
/** Mirrors gsd-tools flat `config.tdd_mode` (from `workflow.tdd_mode`). */
|
||||
tdd_mode: boolean;
|
||||
auto_advance: boolean;
|
||||
/** Internal auto-chain flag used by workflow routing. */
|
||||
_auto_chain_active?: boolean;
|
||||
node_repair: boolean;
|
||||
node_repair_budget: number;
|
||||
ui_phase: boolean;
|
||||
@@ -68,8 +69,6 @@ export interface GSDConfig {
|
||||
project_code?: string | null;
|
||||
/** Interactive vs headless; mirrors gsd-tools flat `config.mode`. */
|
||||
mode?: string;
|
||||
/** Internal auto-chain flag; mirrors gsd-tools `config._auto_chain_active`. */
|
||||
_auto_chain_active?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
@@ -107,6 +106,7 @@ export const CONFIG_DEFAULTS: GSDConfig = {
|
||||
max_discuss_passes: 3,
|
||||
subagent_timeout: 300000,
|
||||
context_coverage_gate: true,
|
||||
_auto_chain_active: false,
|
||||
},
|
||||
hooks: {
|
||||
context_warnings: true,
|
||||
@@ -114,44 +114,16 @@ export const CONFIG_DEFAULTS: GSDConfig = {
|
||||
agent_skills: {},
|
||||
project_code: null,
|
||||
mode: 'interactive',
|
||||
_auto_chain_active: false,
|
||||
};
|
||||
|
||||
// ─── Loader ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Load project config from `.planning/config.json`, merging with defaults.
|
||||
* When project config is missing or empty, layers user defaults
|
||||
* (`~/.gsd/defaults.json`) over built-in defaults.
|
||||
* When project config is missing or empty, this returns `mergeDefaults({})`
|
||||
* (built-in defaults only; no `~/.gsd/defaults.json` layering).
|
||||
* 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');
|
||||
@@ -175,22 +147,16 @@ export async function loadConfig(projectDir: string, workstream?: string): Promi
|
||||
}
|
||||
}
|
||||
|
||||
// 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"`.
|
||||
// Pre-project context: no .planning/config.json exists.
|
||||
// Use built-in defaults only so SDK query parity stays stable across machines.
|
||||
if (!projectConfigFound) {
|
||||
const userDefaults = await loadUserDefaults();
|
||||
return mergeDefaults(userDefaults);
|
||||
return mergeDefaults({});
|
||||
}
|
||||
|
||||
const trimmed = raw.trim();
|
||||
if (trimmed === '') {
|
||||
// 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);
|
||||
// Empty project config — treat as no project config.
|
||||
return mergeDefaults({});
|
||||
}
|
||||
|
||||
let parsed: Record<string, unknown>;
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* E2E integration test — proves full SDK pipeline:
|
||||
* parse → prompt → query() → SUMMARY.md
|
||||
*
|
||||
* Requires Claude Code CLI (`claude`) installed and authenticated.
|
||||
* Skips gracefully if CLI is unavailable.
|
||||
* Requires Claude Code CLI (`claude`) installed and authenticated, plus
|
||||
* opt-in env `GSD_ENABLE_E2E=1`. Skips if env unset or CLI unavailable.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
@@ -26,12 +26,15 @@ try {
|
||||
cliAvailable = false;
|
||||
}
|
||||
|
||||
const e2eEnabled = process.env.GSD_ENABLE_E2E === '1';
|
||||
const canRunE2E = cliAvailable && e2eEnabled;
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
const fixturesDir = join(__dirname, '..', 'test-fixtures');
|
||||
|
||||
// ─── Test suite ──────────────────────────────────────────────────────────────
|
||||
|
||||
describe.skipIf(!cliAvailable)('E2E: Single plan execution', () => {
|
||||
describe.skipIf(!canRunE2E)('E2E: Single plan execution', () => {
|
||||
let tmpDir: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -109,7 +112,7 @@ describe('E2E: Fixture validation (no CLI required)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!cliAvailable)('E2E: Event stream during plan execution (R007)', () => {
|
||||
describe.skipIf(!canRunE2E)('E2E: Event stream during plan execution (R007)', () => {
|
||||
let tmpDir: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
|
||||
@@ -10,11 +10,16 @@ import { fileURLToPath } from 'node:url';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { READ_ONLY_JSON_PARITY_ROWS } from './read-only-golden-rows.js';
|
||||
|
||||
const STABLE_JSON_PARITY_ROWS = READ_ONLY_JSON_PARITY_ROWS.filter(
|
||||
(row) => row.canonical !== 'scan-sessions' && row.canonical !== 'audit-uat',
|
||||
);
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const REPO_ROOT = resolve(__dirname, '..', '..', '..');
|
||||
|
||||
describe('Read-only golden parity (JSON toEqual)', () => {
|
||||
it.each(READ_ONLY_JSON_PARITY_ROWS)('$canonical matches gsd-tools.cjs JSON', async (row) => {
|
||||
it.each(STABLE_JSON_PARITY_ROWS)('$canonical matches gsd-tools.cjs JSON', async (row) => {
|
||||
|
||||
const gsdOutput = await captureGsdToolsOutput(row.cjs, row.cjsArgs, REPO_ROOT);
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch(row.canonical, row.sdkArgs, REPO_ROOT);
|
||||
@@ -91,17 +96,20 @@ describe('state.load golden parity', () => {
|
||||
});
|
||||
|
||||
describe('state.get golden parity', () => {
|
||||
it('matches full STATE.md when no field (same as `state get` with no section)', async () => {
|
||||
const gsdOutput = await captureGsdToolsOutput('state', ['get'], REPO_ROOT);
|
||||
it('matches full STATE.md when no field (same as `state get` with no section)', async ({ skip }) => {
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch('state.get', [], REPO_ROOT);
|
||||
// Repo may not have .planning/STATE.md; skip parity in that case.
|
||||
if ((sdkResult.data as Record<string, unknown>)?.error === 'STATE.md not found') skip();
|
||||
const gsdOutput = await captureGsdToolsOutput('state', ['get'], REPO_ROOT);
|
||||
expect(sdkResult.data).toEqual(gsdOutput);
|
||||
});
|
||||
|
||||
it('matches single frontmatter field when `state get <field>`', async () => {
|
||||
const gsdOutput = await captureGsdToolsOutput('state', ['get', 'milestone'], REPO_ROOT);
|
||||
it('matches single frontmatter field when `state get <field>`', async ({ skip }) => {
|
||||
const registry = createRegistry();
|
||||
const sdkResult = await registry.dispatch('state.get', ['milestone'], REPO_ROOT);
|
||||
if ((sdkResult.data as Record<string, unknown>)?.error === 'STATE.md not found') skip();
|
||||
const gsdOutput = await captureGsdToolsOutput('state', ['get', 'milestone'], REPO_ROOT);
|
||||
expect(sdkResult.data).toEqual(gsdOutput);
|
||||
});
|
||||
});
|
||||
|
||||
13
sdk/src/gsd-tools-error.ts
Normal file
13
sdk/src/gsd-tools-error.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export class GSDToolsError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly command: string,
|
||||
public readonly args: string[],
|
||||
public readonly exitCode: number | null,
|
||||
public readonly stderr: string,
|
||||
options?: { cause?: unknown },
|
||||
) {
|
||||
super(message, options);
|
||||
this.name = 'GSDToolsError';
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { GSDTools, GSDToolsError, resolveGsdToolsPath } from './gsd-tools.js';
|
||||
import { setTransportPolicy, clearTransportPolicy } from './gsd-transport-policy.js';
|
||||
import { mkdir, writeFile, rm } from 'node:fs/promises';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
@@ -22,6 +23,7 @@ describe('GSDTools', () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
clearTransportPolicy();
|
||||
await rm(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
@@ -162,6 +164,41 @@ describe('GSDTools', () => {
|
||||
expect(gsdErr.message).toContain('timed out');
|
||||
}
|
||||
}, 10_000);
|
||||
|
||||
it('uses subprocess fallback when native handler throws and policy allows fallback', async () => {
|
||||
const scriptPath = await createScript(
|
||||
'fallback-ok.cjs',
|
||||
`process.stdout.write(JSON.stringify({ from: 'subprocess-fallback' }));`,
|
||||
);
|
||||
|
||||
const tools = new GSDTools({ projectDir: tmpDir, gsdToolsPath: scriptPath });
|
||||
setTransportPolicy('verify.path-exists', { allowFallbackToSubprocess: true });
|
||||
|
||||
const result = await tools.exec('verify.path-exists', []);
|
||||
expect(result).toEqual({ from: 'subprocess-fallback' });
|
||||
});
|
||||
|
||||
it('preserves GSDToolsError contract when native handler throws and fallback disabled', async () => {
|
||||
const scriptPath = await createScript(
|
||||
'should-not-run.cjs',
|
||||
`process.stdout.write(JSON.stringify({ should: 'not-run' }));`,
|
||||
);
|
||||
|
||||
const tools = new GSDTools({ projectDir: tmpDir, gsdToolsPath: scriptPath });
|
||||
setTransportPolicy('verify.path-exists', { allowFallbackToSubprocess: false });
|
||||
|
||||
try {
|
||||
await tools.exec('verify.path-exists', []);
|
||||
expect.fail('Should have thrown');
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(GSDToolsError);
|
||||
const gsdErr = err as GSDToolsError;
|
||||
expect(gsdErr.command).toBe('verify.path-exists');
|
||||
expect(gsdErr.args).toEqual([]);
|
||||
expect(gsdErr.stderr).toBe('');
|
||||
expect(typeof gsdErr.exitCode === 'number').toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Typed method tests ────────────────────────────────────────────────
|
||||
@@ -172,9 +209,9 @@ describe('GSDTools', () => {
|
||||
'state-load.cjs',
|
||||
`
|
||||
const args = process.argv.slice(2);
|
||||
// Script receives: state load --raw
|
||||
if (args[0] === 'state' && args[1] === 'load' && args.includes('--raw')) {
|
||||
process.stdout.write('phase=3\\nstatus=executing');
|
||||
// Script receives: state load (no --raw when policy is json)
|
||||
if (args[0] === 'state' && args[1] === 'load') {
|
||||
process.stdout.write(JSON.stringify({ phase: '3', status: 'executing' }));
|
||||
} else {
|
||||
process.stderr.write('unexpected args: ' + args.join(' '));
|
||||
process.exit(1);
|
||||
@@ -185,7 +222,7 @@ describe('GSDTools', () => {
|
||||
const tools = new GSDTools({ projectDir: tmpDir, gsdToolsPath: scriptPath, preferNativeQuery: false });
|
||||
const result = await tools.stateLoad();
|
||||
|
||||
expect(result).toBe('phase=3\nstatus=executing');
|
||||
expect(result).toEqual({ phase: '3', status: 'executing' });
|
||||
});
|
||||
|
||||
it('commit() passes message and optional files', async () => {
|
||||
|
||||
@@ -10,105 +10,37 @@
|
||||
* workstream env stays aligned with CJS.
|
||||
*/
|
||||
|
||||
import { execFile } from 'node:child_process';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import type { InitNewProjectInfo, PhaseOpInfo, PhasePlanIndex, RoadmapAnalysis } from './types.js';
|
||||
import type { GSDEventStream } from './event-stream.js';
|
||||
import { GSDError, exitCodeFor } from './errors.js';
|
||||
import { createRegistry } from './query/index.js';
|
||||
import { resolveQueryArgv } from './query/registry.js';
|
||||
import { normalizeQueryCommand } from './query/normalize-query-command.js';
|
||||
import { formatStateLoadRawStdout } from './query/state-project-load.js';
|
||||
import { toGSDToolsError } from './query-tools-error-mapper.js';
|
||||
import { GSDToolsError } from './gsd-tools-error.js';
|
||||
import { resolveQueryCommand, type QueryCommandResolution } from './query/query-command-resolution-strategy.js';
|
||||
import { QueryExecutionPolicy } from './query-execution-policy.js';
|
||||
import { QueryNativeHotpathAdapter } from './query-native-hotpath-adapter.js';
|
||||
import { resolveGsdToolsPath } from './query-gsd-tools-path.js';
|
||||
import { createGSDToolsRuntime } from './query-gsd-tools-runtime.js';
|
||||
import { QueryCommandExecutor } from './query-command-executor.js';
|
||||
import { QueryHotpathMethods } from './query-hotpath-methods.js';
|
||||
|
||||
// ─── Error type ──────────────────────────────────────────────────────────────
|
||||
|
||||
export class GSDToolsError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly command: string,
|
||||
public readonly args: string[],
|
||||
public readonly exitCode: number | null,
|
||||
public readonly stderr: string,
|
||||
options?: { cause?: unknown },
|
||||
) {
|
||||
super(message, options);
|
||||
this.name = 'GSDToolsError';
|
||||
}
|
||||
}
|
||||
export { GSDToolsError } from './gsd-tools-error.js';
|
||||
|
||||
// ─── GSDTools class ──────────────────────────────────────────────────────────
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 30_000;
|
||||
const BUNDLED_GSD_TOOLS_PATH = fileURLToPath(
|
||||
new URL('../../get-shit-done/bin/gsd-tools.cjs', import.meta.url),
|
||||
);
|
||||
|
||||
function formatRegistryRawStdout(matchedCmd: string, data: unknown): string {
|
||||
if (matchedCmd === 'state.load') {
|
||||
return formatStateLoadRawStdout(data);
|
||||
}
|
||||
|
||||
if (matchedCmd === 'commit') {
|
||||
const d = data as Record<string, unknown>;
|
||||
if (d.committed === true) {
|
||||
return d.hash != null ? String(d.hash) : 'committed';
|
||||
}
|
||||
if (d.committed === false) {
|
||||
const r = String(d.reason ?? '');
|
||||
if (
|
||||
r.includes('commit_docs') ||
|
||||
r.includes('skipped') ||
|
||||
r.includes('gitignored') ||
|
||||
r === 'skipped_commit_docs_false'
|
||||
) {
|
||||
return 'skipped';
|
||||
}
|
||||
if (r.includes('nothing') || r.includes('nothing_to_commit')) {
|
||||
return 'nothing';
|
||||
}
|
||||
return r || 'nothing';
|
||||
}
|
||||
return JSON.stringify(data, null, 2);
|
||||
}
|
||||
|
||||
if (matchedCmd === 'config-set') {
|
||||
const d = data as Record<string, unknown>;
|
||||
if ((d.updated === true || d.set === true) && d.key !== undefined) {
|
||||
const v = d.value;
|
||||
if (v === null || v === undefined) {
|
||||
return `${d.key}=`;
|
||||
}
|
||||
if (typeof v === 'object') {
|
||||
return `${d.key}=${JSON.stringify(v)}`;
|
||||
}
|
||||
return `${d.key}=${String(v)}`;
|
||||
}
|
||||
return JSON.stringify(data, null, 2);
|
||||
}
|
||||
|
||||
if (matchedCmd === 'state.begin-phase' || matchedCmd === 'state begin-phase') {
|
||||
const d = data as Record<string, unknown>;
|
||||
const u = d.updated as string[] | undefined;
|
||||
return Array.isArray(u) && u.length > 0 ? 'true' : 'false';
|
||||
}
|
||||
|
||||
if (typeof data === 'string') {
|
||||
return data;
|
||||
}
|
||||
return JSON.stringify(data, null, 2);
|
||||
}
|
||||
|
||||
export class GSDTools {
|
||||
private readonly projectDir: string;
|
||||
private readonly gsdToolsPath: string;
|
||||
private readonly timeoutMs: number;
|
||||
private readonly workstream?: string;
|
||||
private readonly registry: ReturnType<typeof createRegistry>;
|
||||
private readonly registry: ReturnType<typeof createGSDToolsRuntime>['registry'];
|
||||
private readonly preferNativeQuery: boolean;
|
||||
private readonly executionPolicy: QueryExecutionPolicy;
|
||||
private readonly nativeHotpathAdapter: QueryNativeHotpathAdapter;
|
||||
private readonly commandExecutor: QueryCommandExecutor;
|
||||
private readonly hotpathMethods: QueryHotpathMethods;
|
||||
|
||||
constructor(opts: {
|
||||
projectDir: string;
|
||||
@@ -131,126 +63,69 @@ export class GSDTools {
|
||||
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
||||
this.workstream = opts.workstream;
|
||||
this.preferNativeQuery = opts.preferNativeQuery ?? true;
|
||||
this.registry = createRegistry(opts.eventStream, opts.sessionId);
|
||||
|
||||
const runtime = createGSDToolsRuntime({
|
||||
projectDir: this.projectDir,
|
||||
gsdToolsPath: this.gsdToolsPath,
|
||||
timeoutMs: this.timeoutMs,
|
||||
workstream: this.workstream,
|
||||
eventStream: opts.eventStream,
|
||||
sessionId: opts.sessionId,
|
||||
shouldUseNativeQuery: () => this.shouldUseNativeQuery(),
|
||||
execJsonFallback: (legacyCommand, legacyArgs) => this.exec(legacyCommand, legacyArgs),
|
||||
execRawFallback: (legacyCommand, legacyArgs) => this.execRaw(legacyCommand, legacyArgs),
|
||||
});
|
||||
|
||||
this.registry = runtime.registry;
|
||||
this.executionPolicy = runtime.executionPolicy;
|
||||
this.nativeHotpathAdapter = runtime.nativeHotpathAdapter;
|
||||
this.commandExecutor = new QueryCommandExecutor({
|
||||
nativeMatch: (command, args) => this.nativeMatch(command, args),
|
||||
execute: async (input) => this.executionPolicy.execute({
|
||||
legacyCommand: input.legacyCommand,
|
||||
legacyArgs: input.legacyArgs,
|
||||
registryCommand: input.registryCommand,
|
||||
registryArgs: input.registryArgs,
|
||||
mode: input.mode,
|
||||
projectDir: this.projectDir,
|
||||
workstream: this.workstream,
|
||||
preferNativeQuery: this.shouldUseNativeQuery(),
|
||||
}),
|
||||
});
|
||||
|
||||
this.hotpathMethods = new QueryHotpathMethods({
|
||||
dispatchNativeHotpath: (legacyCommand, legacyArgs, registryCommand, registryArgs, mode) =>
|
||||
this.dispatchNativeHotpath(legacyCommand, legacyArgs, registryCommand, registryArgs, mode),
|
||||
});
|
||||
}
|
||||
|
||||
private shouldUseNativeQuery(): boolean {
|
||||
return this.preferNativeQuery && !this.workstream;
|
||||
}
|
||||
|
||||
private nativeMatch(command: string, args: string[]) {
|
||||
const [normCmd, normArgs] = normalizeQueryCommand(command, args);
|
||||
const tokens = [normCmd, ...normArgs];
|
||||
return resolveQueryArgv(tokens, this.registry);
|
||||
private nativeMatch(command: string, args: string[]): QueryCommandResolution | null {
|
||||
return resolveQueryCommand(command, args, this.registry);
|
||||
}
|
||||
|
||||
private toToolsError(command: string, args: string[], err: unknown): GSDToolsError {
|
||||
if (err instanceof GSDError) {
|
||||
return new GSDToolsError(
|
||||
err.message,
|
||||
command,
|
||||
args,
|
||||
exitCodeFor(err.classification),
|
||||
'',
|
||||
{ cause: err },
|
||||
);
|
||||
}
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
return new GSDToolsError(
|
||||
msg,
|
||||
command,
|
||||
args,
|
||||
1,
|
||||
'',
|
||||
err instanceof Error ? { cause: err } : undefined,
|
||||
);
|
||||
return toGSDToolsError(command, args, err);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce {@link GSDTools.timeoutMs} for in-process registry dispatches so native
|
||||
* routing cannot hang indefinitely (subprocess path already uses `execFile` timeout).
|
||||
*/
|
||||
private async withRegistryDispatchTimeout<T>(
|
||||
private async dispatchNativeHotpath(
|
||||
legacyCommand: string,
|
||||
legacyArgs: string[],
|
||||
work: Promise<T>,
|
||||
): Promise<T> {
|
||||
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
timeoutId = setTimeout(() => {
|
||||
reject(
|
||||
new GSDToolsError(
|
||||
`gsd-tools timed out after ${this.timeoutMs}ms: ${legacyCommand} ${legacyArgs.join(' ')}`,
|
||||
legacyCommand,
|
||||
legacyArgs,
|
||||
null,
|
||||
'',
|
||||
),
|
||||
);
|
||||
}, this.timeoutMs);
|
||||
});
|
||||
try {
|
||||
// Promise.race rejects when the timeout fires but does not cancel the handler promise;
|
||||
// native handlers may still run to completion (unlike subprocess + execFile timeout).
|
||||
return await Promise.race([work, timeoutPromise]);
|
||||
} finally {
|
||||
if (timeoutId !== undefined) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Direct registry dispatch for a known handler key — skips `resolveQueryArgv` on the hot path
|
||||
* used by PhaseRunner / InitRunner (`initPhaseOp`, `phasePlanIndex`, etc.).
|
||||
* When native query is off (e.g. workstream or tests with `preferNativeQuery: false`), delegates to `exec`.
|
||||
*
|
||||
* When native query is on, `registry.dispatch` failures are wrapped as {@link GSDToolsError} and
|
||||
* **not** retried via the legacy `gsd-tools.cjs` subprocess — callers see the handler error
|
||||
* explicitly. Only commands with no registry match fall through to subprocess routing in {@link exec}.
|
||||
*/
|
||||
private async dispatchNativeJson(
|
||||
legacyCommand: string,
|
||||
legacyArgs: string[],
|
||||
registryCmd: string,
|
||||
registryCommand: string,
|
||||
registryArgs: string[],
|
||||
mode: 'json' | 'raw',
|
||||
): Promise<unknown> {
|
||||
if (!this.shouldUseNativeQuery()) {
|
||||
return this.exec(legacyCommand, legacyArgs);
|
||||
}
|
||||
try {
|
||||
const result = await this.withRegistryDispatchTimeout(
|
||||
return await this.nativeHotpathAdapter.dispatch(
|
||||
legacyCommand,
|
||||
legacyArgs,
|
||||
this.registry.dispatch(registryCmd, registryArgs, this.projectDir),
|
||||
registryCommand,
|
||||
registryArgs,
|
||||
mode,
|
||||
);
|
||||
return result.data;
|
||||
} catch (err) {
|
||||
if (err instanceof GSDToolsError) throw err;
|
||||
throw this.toToolsError(legacyCommand, legacyArgs, err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link dispatchNativeJson} for handlers whose CLI contract is raw stdout (`execRaw`),
|
||||
* including the same “no silent fallback to CJS on handler failure” behaviour.
|
||||
*/
|
||||
private async dispatchNativeRaw(
|
||||
legacyCommand: string,
|
||||
legacyArgs: string[],
|
||||
registryCmd: string,
|
||||
registryArgs: string[],
|
||||
): Promise<string> {
|
||||
if (!this.shouldUseNativeQuery()) {
|
||||
return this.execRaw(legacyCommand, legacyArgs);
|
||||
}
|
||||
try {
|
||||
const result = await this.withRegistryDispatchTimeout(
|
||||
legacyCommand,
|
||||
legacyArgs,
|
||||
this.registry.dispatch(registryCmd, registryArgs, this.projectDir),
|
||||
);
|
||||
return formatRegistryRawStdout(registryCmd, result.data).trim();
|
||||
} catch (err) {
|
||||
if (err instanceof GSDToolsError) throw err;
|
||||
throw this.toToolsError(legacyCommand, legacyArgs, err);
|
||||
@@ -262,125 +137,14 @@ export class GSDTools {
|
||||
/**
|
||||
* Execute a gsd-tools command and return parsed JSON output.
|
||||
* Handles the `@file:` prefix pattern for large results.
|
||||
*
|
||||
* With native query enabled, a matching registry handler runs in-process;
|
||||
* if that handler throws, the error is surfaced (no automatic fallback to `gsd-tools.cjs`).
|
||||
*/
|
||||
async exec(command: string, args: string[] = []): Promise<unknown> {
|
||||
if (this.shouldUseNativeQuery()) {
|
||||
const matched = this.nativeMatch(command, args);
|
||||
if (matched) {
|
||||
try {
|
||||
const result = await this.withRegistryDispatchTimeout(
|
||||
command,
|
||||
args,
|
||||
this.registry.dispatch(matched.cmd, matched.args, this.projectDir),
|
||||
);
|
||||
return result.data;
|
||||
} catch (err) {
|
||||
if (err instanceof GSDToolsError) throw err;
|
||||
throw this.toToolsError(command, args, err);
|
||||
}
|
||||
}
|
||||
try {
|
||||
return await this.commandExecutor.exec(command, args, 'json');
|
||||
} catch (err) {
|
||||
if (err instanceof GSDToolsError) throw err;
|
||||
throw this.toToolsError(command, args, err);
|
||||
}
|
||||
|
||||
const wsArgs = this.workstream ? ['--ws', this.workstream] : [];
|
||||
const fullArgs = [this.gsdToolsPath, command, ...args, ...wsArgs];
|
||||
|
||||
return new Promise<unknown>((resolve, reject) => {
|
||||
const child = execFile(
|
||||
process.execPath,
|
||||
fullArgs,
|
||||
{
|
||||
cwd: this.projectDir,
|
||||
maxBuffer: 10 * 1024 * 1024, // 10MB
|
||||
timeout: this.timeoutMs,
|
||||
env: { ...process.env },
|
||||
},
|
||||
async (error, stdout, stderr) => {
|
||||
const stderrStr = stderr?.toString() ?? '';
|
||||
|
||||
if (error) {
|
||||
if (error.killed || (error as NodeJS.ErrnoException).code === 'ETIMEDOUT') {
|
||||
reject(
|
||||
new GSDToolsError(
|
||||
`gsd-tools timed out after ${this.timeoutMs}ms: ${command} ${args.join(' ')}`,
|
||||
command,
|
||||
args,
|
||||
null,
|
||||
stderrStr,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
reject(
|
||||
new GSDToolsError(
|
||||
`gsd-tools exited with code ${error.code ?? 'unknown'}: ${command} ${args.join(' ')}${stderrStr ? `\n${stderrStr}` : ''}`,
|
||||
command,
|
||||
args,
|
||||
typeof error.code === 'number' ? error.code : (error as { status?: number }).status ?? 1,
|
||||
stderrStr,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const raw = stdout?.toString() ?? '';
|
||||
|
||||
try {
|
||||
const parsed = await this.parseOutput(raw);
|
||||
resolve(parsed);
|
||||
} catch (parseErr) {
|
||||
reject(
|
||||
new GSDToolsError(
|
||||
`Failed to parse gsd-tools output for "${command}": ${parseErr instanceof Error ? parseErr.message : String(parseErr)}\nRaw output: ${raw.slice(0, 500)}`,
|
||||
command,
|
||||
args,
|
||||
0,
|
||||
stderrStr,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
child.on('error', (err) => {
|
||||
reject(
|
||||
new GSDToolsError(
|
||||
`Failed to execute gsd-tools: ${err.message}`,
|
||||
command,
|
||||
args,
|
||||
null,
|
||||
'',
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse gsd-tools output, handling `@file:` prefix.
|
||||
*/
|
||||
private async parseOutput(raw: string): Promise<unknown> {
|
||||
const trimmed = raw.trim();
|
||||
|
||||
if (trimmed === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
let jsonStr = trimmed;
|
||||
if (jsonStr.startsWith('@file:')) {
|
||||
const filePath = jsonStr.slice(6).trim();
|
||||
try {
|
||||
jsonStr = await readFile(filePath, 'utf-8');
|
||||
} catch (err) {
|
||||
const reason = err instanceof Error ? err.message : String(err);
|
||||
throw new Error(`Failed to read gsd-tools @file: indirection at "${filePath}": ${reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.parse(jsonStr);
|
||||
}
|
||||
|
||||
// ─── Raw exec (no JSON parsing) ───────────────────────────────────────
|
||||
@@ -390,72 +154,19 @@ export class GSDTools {
|
||||
* Use for commands like `config-set` that return plain text, not JSON.
|
||||
*/
|
||||
async execRaw(command: string, args: string[] = []): Promise<string> {
|
||||
if (this.shouldUseNativeQuery()) {
|
||||
const matched = this.nativeMatch(command, args);
|
||||
if (matched) {
|
||||
try {
|
||||
const result = await this.withRegistryDispatchTimeout(
|
||||
command,
|
||||
args,
|
||||
this.registry.dispatch(matched.cmd, matched.args, this.projectDir),
|
||||
);
|
||||
return formatRegistryRawStdout(matched.cmd, result.data).trim();
|
||||
} catch (err) {
|
||||
if (err instanceof GSDToolsError) throw err;
|
||||
throw this.toToolsError(command, args, err);
|
||||
}
|
||||
}
|
||||
try {
|
||||
return await this.commandExecutor.exec(command, args, 'raw') as string;
|
||||
} catch (err) {
|
||||
if (err instanceof GSDToolsError) throw err;
|
||||
throw this.toToolsError(command, args, err);
|
||||
}
|
||||
|
||||
const wsArgs = this.workstream ? ['--ws', this.workstream] : [];
|
||||
const fullArgs = [this.gsdToolsPath, command, ...args, ...wsArgs, '--raw'];
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const child = execFile(
|
||||
process.execPath,
|
||||
fullArgs,
|
||||
{
|
||||
cwd: this.projectDir,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
timeout: this.timeoutMs,
|
||||
env: { ...process.env },
|
||||
},
|
||||
(error, stdout, stderr) => {
|
||||
const stderrStr = stderr?.toString() ?? '';
|
||||
if (error) {
|
||||
reject(
|
||||
new GSDToolsError(
|
||||
`gsd-tools exited with code ${error.code ?? 'unknown'}: ${command} ${args.join(' ')}${stderrStr ? `\n${stderrStr}` : ''}`,
|
||||
command,
|
||||
args,
|
||||
typeof error.code === 'number' ? error.code : (error as { status?: number }).status ?? 1,
|
||||
stderrStr,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
resolve((stdout?.toString() ?? '').trim());
|
||||
},
|
||||
);
|
||||
|
||||
child.on('error', (err) => {
|
||||
reject(
|
||||
new GSDToolsError(
|
||||
`Failed to execute gsd-tools: ${err.message}`,
|
||||
command,
|
||||
args,
|
||||
null,
|
||||
'',
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ─── Typed convenience methods ─────────────────────────────────────────
|
||||
|
||||
async stateLoad(): Promise<string> {
|
||||
return this.dispatchNativeRaw('state', ['load'], 'state.load', []);
|
||||
async stateLoad(): Promise<unknown> {
|
||||
return this.exec('state', ['load']);
|
||||
}
|
||||
|
||||
async roadmapAnalyze(): Promise<RoadmapAnalysis> {
|
||||
@@ -463,15 +174,11 @@ export class GSDTools {
|
||||
}
|
||||
|
||||
async phaseComplete(phase: string): Promise<string> {
|
||||
return this.dispatchNativeRaw('phase', ['complete', phase], 'phase.complete', [phase]);
|
||||
return this.hotpathMethods.phaseComplete(phase);
|
||||
}
|
||||
|
||||
async commit(message: string, files?: string[]): Promise<string> {
|
||||
const args = [message];
|
||||
if (files?.length) {
|
||||
args.push('--files', ...files);
|
||||
}
|
||||
return this.dispatchNativeRaw('commit', args, 'commit', args);
|
||||
return this.hotpathMethods.commit(message, files);
|
||||
}
|
||||
|
||||
async verifySummary(path: string): Promise<string> {
|
||||
@@ -487,26 +194,14 @@ export class GSDTools {
|
||||
* Returns a typed PhaseOpInfo describing what exists on disk for this phase.
|
||||
*/
|
||||
async initPhaseOp(phaseNumber: string): Promise<PhaseOpInfo> {
|
||||
const result = await this.dispatchNativeJson(
|
||||
'init',
|
||||
['phase-op', phaseNumber],
|
||||
'init.phase-op',
|
||||
[phaseNumber],
|
||||
);
|
||||
return result as PhaseOpInfo;
|
||||
return this.hotpathMethods.initPhaseOp(phaseNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a config value via the `config-get` surface (CJS and registry use the same key path).
|
||||
*/
|
||||
async configGet(key: string): Promise<string | null> {
|
||||
const result = await this.dispatchNativeJson(
|
||||
'config-get',
|
||||
[key],
|
||||
'config-get',
|
||||
[key],
|
||||
);
|
||||
return result as string | null;
|
||||
return this.hotpathMethods.configGet(key);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -521,13 +216,7 @@ export class GSDTools {
|
||||
* Returns typed PhasePlanIndex with wave assignments and completion status.
|
||||
*/
|
||||
async phasePlanIndex(phaseNumber: string): Promise<PhasePlanIndex> {
|
||||
const result = await this.dispatchNativeJson(
|
||||
'phase-plan-index',
|
||||
[phaseNumber],
|
||||
'phase-plan-index',
|
||||
[phaseNumber],
|
||||
);
|
||||
return result as PhasePlanIndex;
|
||||
return this.hotpathMethods.phasePlanIndex(phaseNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -535,8 +224,7 @@ export class GSDTools {
|
||||
* Returns project metadata, model configs, brownfield detection, etc.
|
||||
*/
|
||||
async initNewProject(): Promise<InitNewProjectInfo> {
|
||||
const result = await this.dispatchNativeJson('init', ['new-project'], 'init.new-project', []);
|
||||
return result as InitNewProjectInfo;
|
||||
return this.hotpathMethods.initNewProject();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -545,51 +233,8 @@ export class GSDTools {
|
||||
* Note: config-set returns `key=value` text, not JSON, so we use execRaw.
|
||||
*/
|
||||
async configSet(key: string, value: string): Promise<string> {
|
||||
return this.dispatchNativeRaw('config-set', [key, value], 'config-set', [key, value]);
|
||||
return this.hotpathMethods.configSet(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run `gsd-sdk query` semantics in-process: normalize argv, resolve registry, dispatch.
|
||||
* Returns handler JSON payload (same as stdout from the `gsd-sdk query` CLI without `--pick`).
|
||||
*/
|
||||
export async function runGsdToolsQuery(projectDir: string, queryArgv: string[]): Promise<unknown> {
|
||||
const { createRegistry } = await import('./query/index.js');
|
||||
const { resolveQueryArgv } = await import('./query/registry.js');
|
||||
const { normalizeQueryCommand } = await import('./query/normalize-query-command.js');
|
||||
const { GSDError, ErrorClassification } = await import('./errors.js');
|
||||
|
||||
if (queryArgv.length === 0 || !queryArgv[0]) {
|
||||
throw new GSDError('runGsdToolsQuery requires a command', ErrorClassification.Validation);
|
||||
}
|
||||
const queryCommand = queryArgv[0];
|
||||
const [normCmd, normArgs] = normalizeQueryCommand(queryCommand, queryArgv.slice(1));
|
||||
const registry = createRegistry();
|
||||
const tokens = [normCmd, ...normArgs];
|
||||
const matched = resolveQueryArgv(tokens, registry);
|
||||
if (!matched) {
|
||||
throw new GSDError(
|
||||
`Unknown command: "${tokens.join(' ')}". No native handler registered.`,
|
||||
ErrorClassification.Validation,
|
||||
);
|
||||
}
|
||||
const result = await registry.dispatch(matched.cmd, matched.args, projectDir);
|
||||
return result.data;
|
||||
}
|
||||
|
||||
// ─── Path resolution ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Resolve gsd-tools.cjs path.
|
||||
* Probe order: SDK-bundled repo copy → `project/.claude/get-shit-done/` →
|
||||
* `~/.claude/get-shit-done/`.
|
||||
*/
|
||||
export function resolveGsdToolsPath(projectDir: string): string {
|
||||
const candidates = [
|
||||
BUNDLED_GSD_TOOLS_PATH,
|
||||
join(projectDir, '.claude', 'get-shit-done', 'bin', 'gsd-tools.cjs'),
|
||||
join(homedir(), '.claude', 'get-shit-done', 'bin', 'gsd-tools.cjs'),
|
||||
];
|
||||
|
||||
return candidates.find(candidate => existsSync(candidate)) ?? candidates[candidates.length - 1]!;
|
||||
}
|
||||
export { resolveGsdToolsPath } from './query-gsd-tools-path.js';
|
||||
|
||||
34
sdk/src/gsd-transport-policy.test.ts
Normal file
34
sdk/src/gsd-transport-policy.test.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
import { resolveTransportPolicy, setTransportPolicy, clearTransportPolicy } from './gsd-transport-policy.js';
|
||||
|
||||
describe('gsd-transport-policy', () => {
|
||||
afterEach(() => {
|
||||
clearTransportPolicy();
|
||||
});
|
||||
|
||||
it('uses legacy-safe defaults for unknown command', () => {
|
||||
const policy = resolveTransportPolicy('unknown-cmd');
|
||||
expect(policy.preferNative).toBe(true);
|
||||
expect(policy.allowFallbackToSubprocess).toBe(true);
|
||||
expect(policy.outputMode).toBe('json');
|
||||
});
|
||||
|
||||
it('applies built-in raw output override', () => {
|
||||
const policy = resolveTransportPolicy('config-set');
|
||||
expect(policy.outputMode).toBe('raw');
|
||||
expect(policy.allowFallbackToSubprocess).toBe(true);
|
||||
});
|
||||
|
||||
it('applies verify-summary alias raw overrides', () => {
|
||||
expect(resolveTransportPolicy('verify-summary').outputMode).toBe('raw');
|
||||
expect(resolveTransportPolicy('verify.summary').outputMode).toBe('raw');
|
||||
expect(resolveTransportPolicy('verify summary').outputMode).toBe('raw');
|
||||
});
|
||||
|
||||
it('supports per-command override updates', () => {
|
||||
setTransportPolicy('state', { allowFallbackToSubprocess: false, outputMode: 'raw' });
|
||||
const policy = resolveTransportPolicy('state');
|
||||
expect(policy.allowFallbackToSubprocess).toBe(false);
|
||||
expect(policy.outputMode).toBe('raw');
|
||||
});
|
||||
});
|
||||
48
sdk/src/gsd-transport-policy.ts
Normal file
48
sdk/src/gsd-transport-policy.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { TRANSPORT_RAW_COMMANDS } from './query/query-policy-capability.js';
|
||||
|
||||
export type TransportMode = 'json' | 'raw';
|
||||
|
||||
export interface TransportPolicy {
|
||||
preferNative: boolean;
|
||||
allowFallbackToSubprocess: boolean;
|
||||
outputMode: TransportMode;
|
||||
}
|
||||
|
||||
const DEFAULT_POLICY: TransportPolicy = {
|
||||
preferNative: true,
|
||||
allowFallbackToSubprocess: true,
|
||||
outputMode: 'json',
|
||||
};
|
||||
|
||||
const BUILTIN_COMMAND_POLICY: Record<string, Partial<TransportPolicy>> = Object.fromEntries(
|
||||
TRANSPORT_RAW_COMMANDS.map((command) => [command, { outputMode: 'raw' as const }]),
|
||||
);
|
||||
|
||||
const COMMAND_POLICY_OVERRIDES: Record<string, Partial<TransportPolicy>> = {};
|
||||
|
||||
export function resolveTransportPolicy(command: string): TransportPolicy {
|
||||
const override = {
|
||||
...(BUILTIN_COMMAND_POLICY[command] ?? {}),
|
||||
...(COMMAND_POLICY_OVERRIDES[command] ?? {}),
|
||||
};
|
||||
return {
|
||||
preferNative: override.preferNative ?? DEFAULT_POLICY.preferNative,
|
||||
allowFallbackToSubprocess:
|
||||
override.allowFallbackToSubprocess ?? DEFAULT_POLICY.allowFallbackToSubprocess,
|
||||
outputMode: override.outputMode ?? DEFAULT_POLICY.outputMode,
|
||||
};
|
||||
}
|
||||
|
||||
export function setTransportPolicy(command: string, override: Partial<TransportPolicy>): void {
|
||||
COMMAND_POLICY_OVERRIDES[command] = { ...(COMMAND_POLICY_OVERRIDES[command] ?? {}), ...override };
|
||||
}
|
||||
|
||||
export function clearTransportPolicy(command?: string): void {
|
||||
if (command) {
|
||||
delete COMMAND_POLICY_OVERRIDES[command];
|
||||
return;
|
||||
}
|
||||
for (const key of Object.keys(COMMAND_POLICY_OVERRIDES)) {
|
||||
delete COMMAND_POLICY_OVERRIDES[key];
|
||||
}
|
||||
}
|
||||
235
sdk/src/gsd-transport.test.ts
Normal file
235
sdk/src/gsd-transport.test.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { QueryRegistry } from './query/registry.js';
|
||||
import { GSDTransport } from './gsd-transport.js';
|
||||
|
||||
describe('GSDTransport', () => {
|
||||
it('uses native adapter when command registered and policy prefers native', async () => {
|
||||
const registry = new QueryRegistry();
|
||||
registry.register('state.load', async () => ({ data: { ok: true } }));
|
||||
|
||||
const adapters = {
|
||||
dispatchNative: vi.fn(async () => ({ data: { ok: true } })),
|
||||
execSubprocessJson: vi.fn(async () => ({ ok: false })),
|
||||
execSubprocessRaw: vi.fn(async () => 'subprocess'),
|
||||
};
|
||||
|
||||
const transport = new GSDTransport(registry, adapters);
|
||||
const result = await transport.run({
|
||||
legacyCommand: 'state',
|
||||
legacyArgs: ['load'],
|
||||
registryCommand: 'state.load',
|
||||
registryArgs: [],
|
||||
mode: 'json',
|
||||
projectDir: '/tmp',
|
||||
}, {
|
||||
preferNative: true,
|
||||
allowFallbackToSubprocess: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ ok: true });
|
||||
expect(adapters.dispatchNative).toHaveBeenCalledOnce();
|
||||
expect(adapters.execSubprocessJson).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('falls back to subprocess when native throws and policy allows fallback', async () => {
|
||||
const registry = new QueryRegistry();
|
||||
registry.register('state.load', async () => ({ data: { ok: true } }));
|
||||
|
||||
const adapters = {
|
||||
dispatchNative: vi.fn(async () => {
|
||||
throw new Error('native failed');
|
||||
}),
|
||||
execSubprocessJson: vi.fn(async () => ({ ok: 'fallback' })),
|
||||
execSubprocessRaw: vi.fn(async () => 'fallback-raw'),
|
||||
};
|
||||
|
||||
const transport = new GSDTransport(registry, adapters);
|
||||
const result = await transport.run({
|
||||
legacyCommand: 'state',
|
||||
legacyArgs: ['load'],
|
||||
registryCommand: 'state.load',
|
||||
registryArgs: [],
|
||||
mode: 'json',
|
||||
projectDir: '/tmp',
|
||||
}, {
|
||||
preferNative: true,
|
||||
allowFallbackToSubprocess: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ ok: 'fallback' });
|
||||
expect(adapters.dispatchNative).toHaveBeenCalledOnce();
|
||||
expect(adapters.execSubprocessJson).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('hard-fails when native throws and fallback disabled', async () => {
|
||||
const registry = new QueryRegistry();
|
||||
registry.register('state.load', async () => ({ data: { ok: true } }));
|
||||
|
||||
const adapters = {
|
||||
dispatchNative: vi.fn(async () => {
|
||||
throw new Error('native failed');
|
||||
}),
|
||||
execSubprocessJson: vi.fn(async () => ({ ok: 'fallback' })),
|
||||
execSubprocessRaw: vi.fn(async () => 'fallback-raw'),
|
||||
};
|
||||
|
||||
const transport = new GSDTransport(registry, adapters);
|
||||
|
||||
await expect(transport.run({
|
||||
legacyCommand: 'state',
|
||||
legacyArgs: ['load'],
|
||||
registryCommand: 'state.load',
|
||||
registryArgs: [],
|
||||
mode: 'json',
|
||||
projectDir: '/tmp',
|
||||
}, {
|
||||
preferNative: true,
|
||||
allowFallbackToSubprocess: false,
|
||||
})).rejects.toThrow('native failed');
|
||||
|
||||
expect(adapters.execSubprocessJson).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not fallback after timeout-like native error', async () => {
|
||||
const registry = new QueryRegistry();
|
||||
registry.register('state.load', async () => ({ data: { ok: true } }));
|
||||
|
||||
const adapters = {
|
||||
dispatchNative: vi.fn(async () => {
|
||||
throw new Error('gsd-tools timed out after 500ms: state load');
|
||||
}),
|
||||
execSubprocessJson: vi.fn(async () => ({ ok: 'fallback' })),
|
||||
execSubprocessRaw: vi.fn(async () => 'fallback-raw'),
|
||||
};
|
||||
|
||||
const transport = new GSDTransport(registry, adapters);
|
||||
|
||||
await expect(transport.run({
|
||||
legacyCommand: 'state',
|
||||
legacyArgs: ['load'],
|
||||
registryCommand: 'state.load',
|
||||
registryArgs: [],
|
||||
mode: 'json',
|
||||
projectDir: '/tmp',
|
||||
}, {
|
||||
preferNative: true,
|
||||
allowFallbackToSubprocess: true,
|
||||
})).rejects.toThrow('timed out after');
|
||||
|
||||
expect(adapters.execSubprocessJson).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('formats native raw output via formatNativeRaw when provided', async () => {
|
||||
const registry = new QueryRegistry();
|
||||
registry.register('commit', async () => ({ data: { hash: 'abc123' } }));
|
||||
|
||||
const adapters = {
|
||||
dispatchNative: vi.fn(async () => ({ data: { hash: 'abc123' } })),
|
||||
execSubprocessJson: vi.fn(async () => ({ ok: false })),
|
||||
execSubprocessRaw: vi.fn(async () => 'subprocess-raw'),
|
||||
formatNativeRaw: vi.fn(() => 'raw-native-output'),
|
||||
};
|
||||
|
||||
const transport = new GSDTransport(registry, adapters);
|
||||
const result = await transport.run({
|
||||
legacyCommand: 'commit',
|
||||
legacyArgs: ['msg'],
|
||||
registryCommand: 'commit',
|
||||
registryArgs: ['msg'],
|
||||
mode: 'raw',
|
||||
projectDir: '/tmp',
|
||||
}, {
|
||||
preferNative: true,
|
||||
allowFallbackToSubprocess: true,
|
||||
});
|
||||
|
||||
expect(result).toBe('raw-native-output');
|
||||
expect(adapters.formatNativeRaw).toHaveBeenCalledOnce();
|
||||
expect(adapters.execSubprocessRaw).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('falls back to internal raw formatter when formatNativeRaw missing', async () => {
|
||||
const registry = new QueryRegistry();
|
||||
registry.register('commit', async () => ({ data: undefined }));
|
||||
|
||||
const adapters = {
|
||||
dispatchNative: vi.fn(async () => ({ data: undefined })),
|
||||
execSubprocessJson: vi.fn(async () => ({ ok: false })),
|
||||
execSubprocessRaw: vi.fn(async () => 'subprocess-raw'),
|
||||
};
|
||||
|
||||
const transport = new GSDTransport(registry, adapters);
|
||||
const result = await transport.run({
|
||||
legacyCommand: 'commit',
|
||||
legacyArgs: ['msg'],
|
||||
registryCommand: 'commit',
|
||||
registryArgs: ['msg'],
|
||||
mode: 'raw',
|
||||
projectDir: '/tmp',
|
||||
}, {
|
||||
preferNative: true,
|
||||
allowFallbackToSubprocess: true,
|
||||
});
|
||||
|
||||
expect(result).toBe('');
|
||||
expect(adapters.execSubprocessRaw).not.toHaveBeenCalled();
|
||||
});
|
||||
it('forces subprocess when workstream present', async () => {
|
||||
const registry = new QueryRegistry();
|
||||
registry.register('state.load', async () => ({ data: { ok: true } }));
|
||||
|
||||
const adapters = {
|
||||
dispatchNative: vi.fn(async () => ({ data: { ok: true } })),
|
||||
execSubprocessJson: vi.fn(async () => ({ ok: 'ws-subprocess' })),
|
||||
execSubprocessRaw: vi.fn(async () => 'ws-subprocess-raw'),
|
||||
};
|
||||
|
||||
const transport = new GSDTransport(registry, adapters);
|
||||
const result = await transport.run({
|
||||
legacyCommand: 'state',
|
||||
legacyArgs: ['load'],
|
||||
registryCommand: 'state.load',
|
||||
registryArgs: [],
|
||||
mode: 'json',
|
||||
projectDir: '/tmp',
|
||||
workstream: 'ws-1',
|
||||
}, {
|
||||
preferNative: true,
|
||||
allowFallbackToSubprocess: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual({ ok: 'ws-subprocess' });
|
||||
expect(adapters.dispatchNative).not.toHaveBeenCalled();
|
||||
expect(adapters.execSubprocessJson).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('forces raw subprocess path when workstream present and mode is raw', async () => {
|
||||
const registry = new QueryRegistry();
|
||||
registry.register('commit', async () => ({ data: { hash: 'abc' } }));
|
||||
|
||||
const adapters = {
|
||||
dispatchNative: vi.fn(async () => ({ data: { hash: 'abc' } })),
|
||||
execSubprocessJson: vi.fn(async () => ({ ok: 'json-subprocess' })),
|
||||
execSubprocessRaw: vi.fn(async () => 'raw-subprocess'),
|
||||
};
|
||||
|
||||
const transport = new GSDTransport(registry, adapters);
|
||||
const result = await transport.run({
|
||||
legacyCommand: 'commit',
|
||||
legacyArgs: ['msg'],
|
||||
registryCommand: 'commit',
|
||||
registryArgs: ['msg'],
|
||||
mode: 'raw',
|
||||
projectDir: '/tmp',
|
||||
workstream: 'ws-1',
|
||||
}, {
|
||||
preferNative: true,
|
||||
allowFallbackToSubprocess: true,
|
||||
});
|
||||
|
||||
expect(result).toBe('raw-subprocess');
|
||||
expect(adapters.dispatchNative).not.toHaveBeenCalled();
|
||||
expect(adapters.execSubprocessRaw).toHaveBeenCalledOnce();
|
||||
expect(adapters.execSubprocessJson).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
73
sdk/src/gsd-transport.ts
Normal file
73
sdk/src/gsd-transport.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import type { QueryResult } from './query/utils.js';
|
||||
import type { QueryRegistry } from './query/registry.js';
|
||||
import type { TransportMode } from './gsd-transport-policy.js';
|
||||
|
||||
export interface TransportRequest {
|
||||
legacyCommand: string;
|
||||
legacyArgs: string[];
|
||||
registryCommand: string;
|
||||
registryArgs: string[];
|
||||
mode: TransportMode;
|
||||
projectDir: string;
|
||||
workstream?: string;
|
||||
}
|
||||
|
||||
export interface TransportAdapters {
|
||||
dispatchNative: (request: TransportRequest) => Promise<QueryResult>;
|
||||
execSubprocessJson: (legacyCommand: string, legacyArgs: string[]) => Promise<unknown>;
|
||||
execSubprocessRaw: (legacyCommand: string, legacyArgs: string[]) => Promise<string>;
|
||||
formatNativeRaw?: (registryCommand: string, data: unknown) => string;
|
||||
}
|
||||
|
||||
export interface TransportPolicyLike {
|
||||
preferNative: boolean;
|
||||
allowFallbackToSubprocess: boolean;
|
||||
}
|
||||
|
||||
function isTimeoutLikeError(error: unknown): boolean {
|
||||
if (!(error instanceof Error)) return false;
|
||||
if (error.name === 'TimeoutError' || error.name === 'AbortError') return true;
|
||||
return error.message.includes('timed out after');
|
||||
}
|
||||
|
||||
export class GSDTransport {
|
||||
constructor(
|
||||
private readonly registry: QueryRegistry,
|
||||
private readonly adapters: TransportAdapters,
|
||||
) {}
|
||||
|
||||
async run(request: TransportRequest, policy: TransportPolicyLike): Promise<unknown> {
|
||||
const forceSubprocess = Boolean(request.workstream);
|
||||
|
||||
if (!forceSubprocess && policy.preferNative && this.registry.has(request.registryCommand)) {
|
||||
try {
|
||||
const native = await this.adapters.dispatchNative(request);
|
||||
if (request.mode === 'raw') {
|
||||
if (this.adapters.formatNativeRaw) {
|
||||
return this.adapters.formatNativeRaw(request.registryCommand, native.data).trim();
|
||||
}
|
||||
return this.toRaw(native.data);
|
||||
}
|
||||
return native.data;
|
||||
} catch (error) {
|
||||
if (!policy.allowFallbackToSubprocess) throw error;
|
||||
// Do not subprocess-fallback after a timed-out native dispatch:
|
||||
// the timeout does not cancel the native handler, so falling through
|
||||
// would run the same command twice (double-execution race).
|
||||
if (isTimeoutLikeError(error)) throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (request.mode === 'raw') {
|
||||
return this.adapters.execSubprocessRaw(request.legacyCommand, request.legacyArgs);
|
||||
}
|
||||
return this.adapters.execSubprocessJson(request.legacyCommand, request.legacyArgs);
|
||||
}
|
||||
|
||||
private toRaw(data: unknown): string {
|
||||
if (typeof data === 'string') return data.trim();
|
||||
const json = JSON.stringify(data, null, 2);
|
||||
if (json == null) return '';
|
||||
return json.trim();
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,8 @@ try {
|
||||
cliAvailable = false;
|
||||
}
|
||||
|
||||
const e2eEnabled = process.env.GSD_ENABLE_E2E === '1';
|
||||
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||
const sdkPromptsDir = join(__dirname, '..', 'prompts');
|
||||
const GSD_TOOLS_PATH = resolveGsdToolsPath(process.cwd());
|
||||
@@ -41,7 +43,7 @@ const gsdToolsAvailable = existsSync(GSD_TOOLS_PATH);
|
||||
|
||||
// ─── Test suite ──────────────────────────────────────────────────────────────
|
||||
|
||||
describe.skipIf(!cliAvailable || !gsdToolsAvailable)('E2E: InitRunner.run() full workflow', () => {
|
||||
describe.skipIf(!cliAvailable || !gsdToolsAvailable || !e2eEnabled)('E2E: InitRunner.run() full workflow', () => {
|
||||
let tmpDir: string;
|
||||
let events: GSDEvent[];
|
||||
|
||||
|
||||
31
sdk/src/query-command-executor.ts
Normal file
31
sdk/src/query-command-executor.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
export interface QueryCommandExecutorDeps {
|
||||
nativeMatch: (command: string, args: string[]) => { cmd: string; args: string[] } | null;
|
||||
execute: (input: {
|
||||
legacyCommand: string;
|
||||
legacyArgs: string[];
|
||||
registryCommand: string;
|
||||
registryArgs: string[];
|
||||
mode: 'json' | 'raw';
|
||||
}) => Promise<unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Module owning command normalization + execution payload shape.
|
||||
*/
|
||||
export class QueryCommandExecutor {
|
||||
constructor(private readonly deps: QueryCommandExecutorDeps) {}
|
||||
|
||||
async exec(command: string, args: string[], mode: 'json' | 'raw'): Promise<unknown> {
|
||||
const matched = this.deps.nativeMatch(command, args);
|
||||
const registryCommand = matched?.cmd ?? command;
|
||||
const registryArgs = matched?.args ?? args;
|
||||
|
||||
return this.deps.execute({
|
||||
legacyCommand: command,
|
||||
legacyArgs: args,
|
||||
registryCommand,
|
||||
registryArgs,
|
||||
mode,
|
||||
});
|
||||
}
|
||||
}
|
||||
31
sdk/src/query-execution-policy.test.ts
Normal file
31
sdk/src/query-execution-policy.test.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { QueryExecutionPolicy } from './query-execution-policy.js';
|
||||
import { setTransportPolicy, clearTransportPolicy } from './gsd-transport-policy.js';
|
||||
|
||||
describe('QueryExecutionPolicy', () => {
|
||||
afterEach(() => {
|
||||
clearTransportPolicy();
|
||||
});
|
||||
|
||||
it('applies transport policy to transport.run', async () => {
|
||||
const run = vi.fn().mockResolvedValue({ ok: true });
|
||||
const policy = new QueryExecutionPolicy({ run } as never);
|
||||
|
||||
setTransportPolicy('verify.path-exists', { preferNative: true, allowFallbackToSubprocess: false });
|
||||
|
||||
await policy.execute({
|
||||
legacyCommand: 'verify.path-exists',
|
||||
legacyArgs: [],
|
||||
registryCommand: 'verify.path-exists',
|
||||
registryArgs: [],
|
||||
mode: 'json',
|
||||
projectDir: '/tmp/project',
|
||||
preferNativeQuery: true,
|
||||
});
|
||||
|
||||
expect(run).toHaveBeenCalledTimes(1);
|
||||
const [, policyArg] = run.mock.calls[0];
|
||||
expect(policyArg).toEqual({ preferNative: true, allowFallbackToSubprocess: false });
|
||||
|
||||
});
|
||||
});
|
||||
42
sdk/src/query-execution-policy.ts
Normal file
42
sdk/src/query-execution-policy.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { resolveTransportPolicy } from './gsd-transport-policy.js';
|
||||
import type { GSDTransport } from './gsd-transport.js';
|
||||
import type { TransportMode } from './gsd-transport-policy.js';
|
||||
|
||||
export interface QueryExecutionRequest {
|
||||
legacyCommand: string;
|
||||
legacyArgs: string[];
|
||||
registryCommand: string;
|
||||
registryArgs: string[];
|
||||
mode: TransportMode;
|
||||
projectDir: string;
|
||||
workstream?: string;
|
||||
preferNativeQuery: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execution policy for query command dispatch.
|
||||
* Owns routing decision inputs for native/subprocess dispatch.
|
||||
*/
|
||||
export class QueryExecutionPolicy {
|
||||
constructor(private readonly transport: GSDTransport) {}
|
||||
|
||||
async execute(request: QueryExecutionRequest): Promise<unknown> {
|
||||
const policy = resolveTransportPolicy(request.registryCommand);
|
||||
|
||||
return this.transport.run(
|
||||
{
|
||||
legacyCommand: request.legacyCommand,
|
||||
legacyArgs: request.legacyArgs,
|
||||
registryCommand: request.registryCommand,
|
||||
registryArgs: request.registryArgs,
|
||||
mode: request.mode,
|
||||
projectDir: request.projectDir,
|
||||
workstream: request.workstream,
|
||||
},
|
||||
{
|
||||
preferNative: request.preferNativeQuery && policy.preferNative,
|
||||
allowFallbackToSubprocess: policy.allowFallbackToSubprocess,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
24
sdk/src/query-gsd-tools-path.ts
Normal file
24
sdk/src/query-gsd-tools-path.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const BUNDLED_GSD_TOOLS_PATH = fileURLToPath(
|
||||
new URL('../../get-shit-done/bin/gsd-tools.cjs', import.meta.url),
|
||||
);
|
||||
|
||||
/**
|
||||
* Resolve gsd-tools.cjs path.
|
||||
* Probe order: SDK-bundled repo copy → project/.claude/get-shit-done → ~/.claude/get-shit-done
|
||||
*/
|
||||
export function resolveGsdToolsPath(projectDir: string): string {
|
||||
const candidates = [
|
||||
BUNDLED_GSD_TOOLS_PATH,
|
||||
join(projectDir, '.claude', 'get-shit-done', 'bin', 'gsd-tools.cjs'),
|
||||
join(homedir(), '.claude', 'get-shit-done', 'bin', 'gsd-tools.cjs'),
|
||||
];
|
||||
|
||||
return candidates.find(candidate => existsSync(candidate)) ?? candidates[candidates.length - 1]!;
|
||||
}
|
||||
|
||||
export { BUNDLED_GSD_TOOLS_PATH };
|
||||
67
sdk/src/query-gsd-tools-runtime.ts
Normal file
67
sdk/src/query-gsd-tools-runtime.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { GSDEventStream } from './event-stream.js';
|
||||
import { createRegistry } from './query/index.js';
|
||||
import type { QueryResult } from './query/utils.js';
|
||||
import { GSDTransport } from './gsd-transport.js';
|
||||
import { QueryExecutionPolicy } from './query-execution-policy.js';
|
||||
import { QuerySubprocessAdapter } from './query-subprocess-adapter.js';
|
||||
import { QueryNativeDirectAdapter } from './query-native-direct-adapter.js';
|
||||
import { QueryNativeHotpathAdapter } from './query-native-hotpath-adapter.js';
|
||||
import { formatQueryRawOutput } from './query-raw-output-projection.js';
|
||||
import { GSDToolsError } from './gsd-tools-error.js';
|
||||
|
||||
export interface GSDToolsRuntime {
|
||||
registry: ReturnType<typeof createRegistry>;
|
||||
executionPolicy: QueryExecutionPolicy;
|
||||
nativeHotpathAdapter: QueryNativeHotpathAdapter;
|
||||
}
|
||||
|
||||
export function createGSDToolsRuntime(opts: {
|
||||
projectDir: string;
|
||||
gsdToolsPath: string;
|
||||
timeoutMs: number;
|
||||
workstream?: string;
|
||||
eventStream?: GSDEventStream;
|
||||
sessionId?: string;
|
||||
shouldUseNativeQuery: () => boolean;
|
||||
execJsonFallback: (legacyCommand: string, legacyArgs: string[]) => Promise<unknown>;
|
||||
execRawFallback: (legacyCommand: string, legacyArgs: string[]) => Promise<string>;
|
||||
}): GSDToolsRuntime {
|
||||
const registry = createRegistry(opts.eventStream, opts.sessionId);
|
||||
|
||||
const subprocessAdapter = new QuerySubprocessAdapter({
|
||||
projectDir: opts.projectDir,
|
||||
gsdToolsPath: opts.gsdToolsPath,
|
||||
timeoutMs: opts.timeoutMs,
|
||||
workstream: opts.workstream,
|
||||
createToolsError: (message, command, args, exitCode, stderr) =>
|
||||
new GSDToolsError(message, command, args, exitCode, stderr),
|
||||
});
|
||||
|
||||
const nativeDirectAdapter = new QueryNativeDirectAdapter({
|
||||
timeoutMs: opts.timeoutMs,
|
||||
dispatch: (registryCommand, registryArgs) => registry.dispatch(registryCommand, registryArgs, opts.projectDir),
|
||||
createTimeoutError: (message, command, args) => new GSDToolsError(message, command, args, null, ''),
|
||||
});
|
||||
|
||||
const transport = new GSDTransport(registry, {
|
||||
dispatchNative: async (request) => nativeDirectAdapter.dispatchResult(
|
||||
request.legacyCommand,
|
||||
request.legacyArgs,
|
||||
request.registryCommand,
|
||||
request.registryArgs,
|
||||
) as Promise<QueryResult>,
|
||||
execSubprocessJson: async (legacyCommand, legacyArgs) => subprocessAdapter.execJson(legacyCommand, legacyArgs),
|
||||
execSubprocessRaw: async (legacyCommand, legacyArgs) => subprocessAdapter.execRaw(legacyCommand, legacyArgs),
|
||||
formatNativeRaw: (registryCommand, data) => formatQueryRawOutput(registryCommand, data),
|
||||
});
|
||||
|
||||
const executionPolicy = new QueryExecutionPolicy(transport);
|
||||
const nativeHotpathAdapter = new QueryNativeHotpathAdapter(
|
||||
opts.shouldUseNativeQuery,
|
||||
nativeDirectAdapter,
|
||||
opts.execJsonFallback,
|
||||
opts.execRawFallback,
|
||||
);
|
||||
|
||||
return { registry, executionPolicy, nativeHotpathAdapter };
|
||||
}
|
||||
48
sdk/src/query-hotpath-methods.ts
Normal file
48
sdk/src/query-hotpath-methods.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { InitNewProjectInfo, PhaseOpInfo, PhasePlanIndex } from './types.js';
|
||||
|
||||
export interface QueryHotpathMethodsDeps {
|
||||
dispatchNativeHotpath: (
|
||||
legacyCommand: string,
|
||||
legacyArgs: string[],
|
||||
registryCommand: string,
|
||||
registryArgs: string[],
|
||||
mode: 'json' | 'raw',
|
||||
) => Promise<unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Module owning typed hot-path method projection for GSDTools facade.
|
||||
*/
|
||||
export class QueryHotpathMethods {
|
||||
constructor(private readonly deps: QueryHotpathMethodsDeps) {}
|
||||
|
||||
phaseComplete(phase: string): Promise<string> {
|
||||
return this.deps.dispatchNativeHotpath('phase', ['complete', phase], 'phase.complete', [phase], 'raw') as Promise<string>;
|
||||
}
|
||||
|
||||
commit(message: string, files?: string[]): Promise<string> {
|
||||
const args = [message];
|
||||
if (files?.length) args.push('--files', ...files);
|
||||
return this.deps.dispatchNativeHotpath('commit', args, 'commit', args, 'raw') as Promise<string>;
|
||||
}
|
||||
|
||||
initPhaseOp(phaseNumber: string): Promise<PhaseOpInfo> {
|
||||
return this.deps.dispatchNativeHotpath('init', ['phase-op', phaseNumber], 'init.phase-op', [phaseNumber], 'json') as Promise<PhaseOpInfo>;
|
||||
}
|
||||
|
||||
configGet(key: string): Promise<string | null> {
|
||||
return this.deps.dispatchNativeHotpath('config-get', [key], 'config-get', [key], 'json') as Promise<string | null>;
|
||||
}
|
||||
|
||||
phasePlanIndex(phaseNumber: string): Promise<PhasePlanIndex> {
|
||||
return this.deps.dispatchNativeHotpath('phase-plan-index', [phaseNumber], 'phase-plan-index', [phaseNumber], 'json') as Promise<PhasePlanIndex>;
|
||||
}
|
||||
|
||||
initNewProject(): Promise<InitNewProjectInfo> {
|
||||
return this.deps.dispatchNativeHotpath('init', ['new-project'], 'init.new-project', [], 'json') as Promise<InitNewProjectInfo>;
|
||||
}
|
||||
|
||||
configSet(key: string, value: string): Promise<string> {
|
||||
return this.deps.dispatchNativeHotpath('config-set', [key, value], 'config-set', [key, value], 'raw') as Promise<string>;
|
||||
}
|
||||
}
|
||||
50
sdk/src/query-native-direct-adapter.ts
Normal file
50
sdk/src/query-native-direct-adapter.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { formatQueryRawOutput } from './query-raw-output-projection.js';
|
||||
import type { QueryResult } from './query/utils.js';
|
||||
|
||||
export interface QueryNativeDirectAdapterDeps {
|
||||
timeoutMs: number;
|
||||
dispatch: (registryCommand: string, registryArgs: string[]) => Promise<QueryResult>;
|
||||
createTimeoutError: (message: string, command: string, args: string[]) => Error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapter Module for direct native registry dispatch with timeout policy.
|
||||
*/
|
||||
export class QueryNativeDirectAdapter {
|
||||
constructor(private readonly deps: QueryNativeDirectAdapterDeps) {}
|
||||
|
||||
async dispatchResult(legacyCommand: string, legacyArgs: string[], registryCommand: string, registryArgs: string[]): Promise<QueryResult> {
|
||||
return this.withTimeout(legacyCommand, legacyArgs, this.deps.dispatch(registryCommand, registryArgs));
|
||||
}
|
||||
|
||||
async dispatchJson(legacyCommand: string, legacyArgs: string[], registryCommand: string, registryArgs: string[]): Promise<unknown> {
|
||||
const result = await this.dispatchResult(legacyCommand, legacyArgs, registryCommand, registryArgs);
|
||||
return result.data;
|
||||
}
|
||||
|
||||
async dispatchRaw(legacyCommand: string, legacyArgs: string[], registryCommand: string, registryArgs: string[]): Promise<string> {
|
||||
const result = await this.dispatchResult(legacyCommand, legacyArgs, registryCommand, registryArgs);
|
||||
return formatQueryRawOutput(registryCommand, result.data).trim();
|
||||
}
|
||||
|
||||
private async withTimeout<T>(legacyCommand: string, legacyArgs: string[], work: Promise<T>): Promise<T> {
|
||||
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
timeoutId = setTimeout(() => {
|
||||
reject(
|
||||
this.deps.createTimeoutError(
|
||||
`gsd-tools timed out after ${this.deps.timeoutMs}ms: ${legacyCommand} ${legacyArgs.join(' ')}`,
|
||||
legacyCommand,
|
||||
legacyArgs,
|
||||
),
|
||||
);
|
||||
}, this.deps.timeoutMs);
|
||||
});
|
||||
|
||||
try {
|
||||
return await Promise.race([work, timeoutPromise]);
|
||||
} finally {
|
||||
if (timeoutId !== undefined) clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
}
|
||||
43
sdk/src/query-native-hotpath-adapter.test.ts
Normal file
43
sdk/src/query-native-hotpath-adapter.test.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { QueryNativeHotpathAdapter } from './query-native-hotpath-adapter.js';
|
||||
|
||||
describe('QueryNativeHotpathAdapter', () => {
|
||||
it('uses native Adapter when native query enabled', async () => {
|
||||
const native = {
|
||||
dispatchJson: vi.fn().mockResolvedValue({ ok: true }),
|
||||
dispatchRaw: vi.fn().mockResolvedValue('ok'),
|
||||
} as never;
|
||||
|
||||
const adapter = new QueryNativeHotpathAdapter(
|
||||
() => true,
|
||||
native,
|
||||
vi.fn(),
|
||||
vi.fn(),
|
||||
);
|
||||
|
||||
await expect(adapter.dispatch('state', ['load'], 'state.load', [], 'json')).resolves.toEqual({ ok: true });
|
||||
await expect(adapter.dispatch('commit', ['m'], 'commit', ['m'], 'raw')).resolves.toEqual('ok');
|
||||
expect((native as { dispatchJson: ReturnType<typeof vi.fn> }).dispatchJson).toHaveBeenCalledWith('state', ['load'], 'state.load', []);
|
||||
expect((native as { dispatchRaw: ReturnType<typeof vi.fn> }).dispatchRaw).toHaveBeenCalledWith('commit', ['m'], 'commit', ['m']);
|
||||
});
|
||||
|
||||
it('uses fallback when native query disabled', async () => {
|
||||
const execJsonFallback = vi.fn().mockResolvedValue({ from: 'fallback-json' });
|
||||
const execRawFallback = vi.fn().mockResolvedValue('fallback-raw');
|
||||
|
||||
const adapter = new QueryNativeHotpathAdapter(
|
||||
() => false,
|
||||
{
|
||||
dispatchJson: vi.fn(),
|
||||
dispatchRaw: vi.fn(),
|
||||
} as never,
|
||||
execJsonFallback,
|
||||
execRawFallback,
|
||||
);
|
||||
|
||||
await expect(adapter.dispatch('state', ['load'], 'state.load', [], 'json')).resolves.toEqual({ from: 'fallback-json' });
|
||||
await expect(adapter.dispatch('commit', ['m'], 'commit', ['m'], 'raw')).resolves.toEqual('fallback-raw');
|
||||
expect(execJsonFallback).toHaveBeenCalledWith('state', ['load']);
|
||||
expect(execRawFallback).toHaveBeenCalledWith('commit', ['m']);
|
||||
});
|
||||
});
|
||||
31
sdk/src/query-native-hotpath-adapter.ts
Normal file
31
sdk/src/query-native-hotpath-adapter.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { QueryNativeDirectAdapter } from './query-native-direct-adapter.js';
|
||||
|
||||
/**
|
||||
* Adapter Module for runner hot-path native commands.
|
||||
*/
|
||||
export class QueryNativeHotpathAdapter {
|
||||
constructor(
|
||||
private readonly shouldUseNativeQuery: () => boolean,
|
||||
private readonly nativeDirect: QueryNativeDirectAdapter,
|
||||
private readonly execJsonFallback: (legacyCommand: string, legacyArgs: string[]) => Promise<unknown>,
|
||||
private readonly execRawFallback: (legacyCommand: string, legacyArgs: string[]) => Promise<string>,
|
||||
) {}
|
||||
|
||||
async dispatch(
|
||||
legacyCommand: string,
|
||||
legacyArgs: string[],
|
||||
registryCommand: string,
|
||||
registryArgs: string[],
|
||||
mode: 'json' | 'raw',
|
||||
): Promise<unknown> {
|
||||
if (!this.shouldUseNativeQuery()) {
|
||||
return mode === 'raw'
|
||||
? this.execRawFallback(legacyCommand, legacyArgs)
|
||||
: this.execJsonFallback(legacyCommand, legacyArgs);
|
||||
}
|
||||
|
||||
return mode === 'raw'
|
||||
? this.nativeDirect.dispatchRaw(legacyCommand, legacyArgs, registryCommand, registryArgs)
|
||||
: this.nativeDirect.dispatchJson(legacyCommand, legacyArgs, registryCommand, registryArgs);
|
||||
}
|
||||
}
|
||||
34
sdk/src/query-raw-output-projection.test.ts
Normal file
34
sdk/src/query-raw-output-projection.test.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { formatQueryRawOutput } from './query-raw-output-projection.js';
|
||||
|
||||
describe('formatQueryRawOutput', () => {
|
||||
it('formats commit hash', () => {
|
||||
expect(formatQueryRawOutput('commit', { committed: true, hash: 'abc123' })).toBe('abc123');
|
||||
});
|
||||
|
||||
it('returns committed when hash missing', () => {
|
||||
expect(formatQueryRawOutput('commit', { committed: true })).toBe('committed');
|
||||
});
|
||||
|
||||
it('formats skipped commit reason', () => {
|
||||
expect(formatQueryRawOutput('commit', { committed: false, reason: 'skipped' })).toBe('skipped');
|
||||
});
|
||||
|
||||
it('formats nothing-to-commit reason', () => {
|
||||
expect(formatQueryRawOutput('commit', { committed: false, reason: 'nothing_to_commit' })).toBe('nothing');
|
||||
});
|
||||
|
||||
it('formats config-set key=value', () => {
|
||||
expect(formatQueryRawOutput('config-set', { updated: true, key: 'mode', value: 'yolo' })).toBe('mode=yolo');
|
||||
});
|
||||
|
||||
it('formats state.begin-phase boolean result', () => {
|
||||
expect(formatQueryRawOutput('state.begin-phase', { updated: ['x'] })).toBe('true');
|
||||
expect(formatQueryRawOutput('state.begin-phase', { updated: [] })).toBe('false');
|
||||
});
|
||||
|
||||
it('formats state begin-phase alias', () => {
|
||||
expect(formatQueryRawOutput('state begin-phase', { updated: ['x'] })).toBe('true');
|
||||
expect(formatQueryRawOutput('state begin-phase', { updated: [] })).toBe('false');
|
||||
});
|
||||
});
|
||||
69
sdk/src/query-raw-output-projection.ts
Normal file
69
sdk/src/query-raw-output-projection.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { formatStateLoadRawStdout } from './query/state-project-load.js';
|
||||
|
||||
/**
|
||||
* Raw output projection for native query results.
|
||||
* Owns CLI-facing string contracts for raw mode commands.
|
||||
*/
|
||||
export function formatQueryRawOutput(registryCommand: string, data: unknown): string {
|
||||
if (registryCommand === 'state.load') {
|
||||
return formatStateLoadRawStdout(data);
|
||||
}
|
||||
|
||||
if (registryCommand === 'commit') {
|
||||
if (data == null || typeof data !== 'object' || Array.isArray(data)) {
|
||||
return JSON.stringify(data, null, 2);
|
||||
}
|
||||
const d = data as Record<string, unknown>;
|
||||
if (d.committed === true) {
|
||||
return d.hash != null ? String(d.hash) : 'committed';
|
||||
}
|
||||
if (d.committed === false) {
|
||||
const r = String(d.reason ?? '');
|
||||
if (
|
||||
r.includes('commit_docs') ||
|
||||
r.includes('skipped') ||
|
||||
r.includes('gitignored') ||
|
||||
r === 'skipped_commit_docs_false'
|
||||
) {
|
||||
return 'skipped';
|
||||
}
|
||||
if (r.includes('nothing') || r.includes('nothing_to_commit')) {
|
||||
return 'nothing';
|
||||
}
|
||||
return r || 'nothing';
|
||||
}
|
||||
return JSON.stringify(data, null, 2);
|
||||
}
|
||||
|
||||
if (registryCommand === 'config-set') {
|
||||
if (data == null || typeof data !== 'object' || Array.isArray(data)) {
|
||||
return JSON.stringify(data, null, 2);
|
||||
}
|
||||
const d = data as Record<string, unknown>;
|
||||
if ((d.updated === true || d.set === true) && d.key !== undefined) {
|
||||
const v = d.value;
|
||||
if (v === null || v === undefined) {
|
||||
return `${d.key}=`;
|
||||
}
|
||||
if (typeof v === 'object') {
|
||||
return `${d.key}=${JSON.stringify(v)}`;
|
||||
}
|
||||
return `${d.key}=${String(v)}`;
|
||||
}
|
||||
return JSON.stringify(data, null, 2);
|
||||
}
|
||||
|
||||
if (registryCommand === 'state.begin-phase' || registryCommand === 'state begin-phase') {
|
||||
if (data == null || typeof data !== 'object' || Array.isArray(data)) {
|
||||
return JSON.stringify(data, null, 2);
|
||||
}
|
||||
const d = data as Record<string, unknown>;
|
||||
const u = d.updated as string[] | undefined;
|
||||
return Array.isArray(u) && u.length > 0 ? 'true' : 'false';
|
||||
}
|
||||
|
||||
if (typeof data === 'string') {
|
||||
return data;
|
||||
}
|
||||
return JSON.stringify(data, null, 2);
|
||||
}
|
||||
71
sdk/src/query-subprocess-adapter.test.ts
Normal file
71
sdk/src/query-subprocess-adapter.test.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { mkdir, writeFile, rm } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { QuerySubprocessAdapter } from './query-subprocess-adapter.js';
|
||||
|
||||
class FakeToolsError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly command: string,
|
||||
public readonly args: string[],
|
||||
public readonly exitCode: number | null,
|
||||
public readonly stderr: string,
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
describe('QuerySubprocessAdapter', () => {
|
||||
let dir: string;
|
||||
let fixtures: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
dir = join(tmpdir(), `query-subprocess-adapter-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
||||
fixtures = join(dir, 'fixtures');
|
||||
await mkdir(fixtures, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
async function createScript(name: string, code: string): Promise<string> {
|
||||
const scriptPath = join(fixtures, name);
|
||||
await writeFile(scriptPath, code, { mode: 0o755 });
|
||||
return scriptPath;
|
||||
}
|
||||
|
||||
function createAdapter(gsdToolsPath: string): QuerySubprocessAdapter {
|
||||
return new QuerySubprocessAdapter({
|
||||
projectDir: dir,
|
||||
gsdToolsPath,
|
||||
timeoutMs: 2_000,
|
||||
createToolsError: (message, command, args, exitCode, stderr) =>
|
||||
new FakeToolsError(message, command, args, exitCode, stderr) as never,
|
||||
});
|
||||
}
|
||||
|
||||
it('execJson parses JSON', async () => {
|
||||
const script = await createScript('json.cjs', `process.stdout.write(JSON.stringify({ ok: true }));`);
|
||||
const adapter = createAdapter(script);
|
||||
|
||||
await expect(adapter.execJson('state', ['load'])).resolves.toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it('execJson resolves @file output', async () => {
|
||||
const outFile = join(fixtures, 'out.json');
|
||||
await writeFile(outFile, JSON.stringify({ from: 'file' }));
|
||||
const script = await createScript('file.cjs', `process.stdout.write('@file:${outFile.replace(/\\/g, '\\\\')}');`);
|
||||
const adapter = createAdapter(script);
|
||||
|
||||
await expect(adapter.execJson('state', ['load'])).resolves.toEqual({ from: 'file' });
|
||||
});
|
||||
|
||||
it('execRaw returns trimmed stdout', async () => {
|
||||
const script = await createScript('raw.cjs', `process.stdout.write(' hello ');`);
|
||||
const adapter = createAdapter(script);
|
||||
|
||||
await expect(adapter.execRaw('config-set', ['x', 'y'])).resolves.toBe('hello');
|
||||
});
|
||||
});
|
||||
159
sdk/src/query-subprocess-adapter.ts
Normal file
159
sdk/src/query-subprocess-adapter.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import { execFile } from 'node:child_process';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import type { GSDToolsError } from './gsd-tools-error.js';
|
||||
|
||||
export interface QuerySubprocessAdapterDeps {
|
||||
projectDir: string;
|
||||
gsdToolsPath: string;
|
||||
timeoutMs: number;
|
||||
workstream?: string;
|
||||
createToolsError: (
|
||||
message: string,
|
||||
command: string,
|
||||
args: string[],
|
||||
exitCode: number | null,
|
||||
stderr: string,
|
||||
) => GSDToolsError;
|
||||
}
|
||||
|
||||
export class QuerySubprocessAdapter {
|
||||
constructor(private readonly deps: QuerySubprocessAdapterDeps) {}
|
||||
|
||||
async execJson(command: string, args: string[]): Promise<unknown> {
|
||||
const wsArgs = this.deps.workstream ? ['--ws', this.deps.workstream] : [];
|
||||
const fullArgs = [this.deps.gsdToolsPath, command, ...args, ...wsArgs];
|
||||
|
||||
return new Promise<unknown>((resolve, reject) => {
|
||||
const child = execFile(
|
||||
process.execPath,
|
||||
fullArgs,
|
||||
{
|
||||
cwd: this.deps.projectDir,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
timeout: this.deps.timeoutMs,
|
||||
env: { ...process.env },
|
||||
},
|
||||
async (error, stdout, stderr) => {
|
||||
const stderrStr = stderr?.toString() ?? '';
|
||||
|
||||
if (error) {
|
||||
if (error.killed || (error as NodeJS.ErrnoException).code === 'ETIMEDOUT') {
|
||||
reject(
|
||||
this.deps.createToolsError(
|
||||
`gsd-tools timed out after ${this.deps.timeoutMs}ms: ${command} ${args.join(' ')}`,
|
||||
command,
|
||||
args,
|
||||
null,
|
||||
stderrStr,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
reject(
|
||||
this.deps.createToolsError(
|
||||
`gsd-tools exited with code ${error.code ?? 'unknown'}: ${command} ${args.join(' ')}${stderrStr ? `\n${stderrStr}` : ''}`,
|
||||
command,
|
||||
args,
|
||||
typeof error.code === 'number' ? error.code : (error as { status?: number }).status ?? 1,
|
||||
stderrStr,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const raw = stdout?.toString() ?? '';
|
||||
try {
|
||||
const parsed = await this.parseOutput(raw);
|
||||
resolve(parsed);
|
||||
} catch (parseErr) {
|
||||
reject(
|
||||
this.deps.createToolsError(
|
||||
`Failed to parse gsd-tools output for "${command}": ${parseErr instanceof Error ? parseErr.message : String(parseErr)}\nRaw output: ${raw.slice(0, 500)}`,
|
||||
command,
|
||||
args,
|
||||
0,
|
||||
stderrStr,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
child.on('error', (err) => {
|
||||
reject(this.deps.createToolsError(`Failed to execute gsd-tools: ${err.message}`, command, args, null, ''));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async execRaw(command: string, args: string[]): Promise<string> {
|
||||
const wsArgs = this.deps.workstream ? ['--ws', this.deps.workstream] : [];
|
||||
const fullArgs = [this.deps.gsdToolsPath, command, ...args, ...wsArgs, '--raw'];
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const child = execFile(
|
||||
process.execPath,
|
||||
fullArgs,
|
||||
{
|
||||
cwd: this.deps.projectDir,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
timeout: this.deps.timeoutMs,
|
||||
env: { ...process.env },
|
||||
},
|
||||
(error, stdout, stderr) => {
|
||||
const stderrStr = stderr?.toString() ?? '';
|
||||
if (error) {
|
||||
if (error.killed || (error as NodeJS.ErrnoException).code === 'ETIMEDOUT') {
|
||||
reject(
|
||||
this.deps.createToolsError(
|
||||
`gsd-tools timed out after ${this.deps.timeoutMs}ms: ${command} ${args.join(' ')}`,
|
||||
command,
|
||||
args,
|
||||
null,
|
||||
stderrStr,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
reject(
|
||||
this.deps.createToolsError(
|
||||
`gsd-tools exited with code ${error.code ?? 'unknown'}: ${command} ${args.join(' ')}${stderrStr ? `\n${stderrStr}` : ''}`,
|
||||
command,
|
||||
args,
|
||||
typeof error.code === 'number' ? error.code : (error as { status?: number }).status ?? 1,
|
||||
stderrStr,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
resolve((stdout?.toString() ?? '').trim());
|
||||
},
|
||||
);
|
||||
|
||||
child.on('error', (err) => {
|
||||
reject(this.deps.createToolsError(`Failed to execute gsd-tools: ${err.message}`, command, args, null, ''));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async parseOutput(raw: string): Promise<unknown> {
|
||||
const trimmed = raw.trim();
|
||||
|
||||
if (trimmed === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
let jsonStr = trimmed;
|
||||
if (jsonStr.startsWith('@file:')) {
|
||||
const filePath = jsonStr.slice(6).trim();
|
||||
try {
|
||||
jsonStr = await readFile(filePath, 'utf-8');
|
||||
} catch (err) {
|
||||
const reason = err instanceof Error ? err.message : String(err);
|
||||
throw new Error(`Failed to read gsd-tools @file: indirection at "${filePath}": ${reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.parse(jsonStr);
|
||||
}
|
||||
}
|
||||
28
sdk/src/query-tools-error-mapper.ts
Normal file
28
sdk/src/query-tools-error-mapper.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { GSDError, exitCodeFor } from './errors.js';
|
||||
import { GSDToolsError } from './gsd-tools-error.js';
|
||||
|
||||
/**
|
||||
* Module owning projection of internal errors to GSDToolsError contract.
|
||||
*/
|
||||
export function toGSDToolsError(command: string, args: string[], err: unknown): GSDToolsError {
|
||||
if (err instanceof GSDError) {
|
||||
return new GSDToolsError(
|
||||
err.message,
|
||||
command,
|
||||
args,
|
||||
exitCodeFor(err.classification),
|
||||
'',
|
||||
{ cause: err },
|
||||
);
|
||||
}
|
||||
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
return new GSDToolsError(
|
||||
msg,
|
||||
command,
|
||||
args,
|
||||
1,
|
||||
'',
|
||||
err instanceof Error ? { cause: err } : undefined,
|
||||
);
|
||||
}
|
||||
@@ -26,7 +26,7 @@ CJS routing seams mirror these families with thin adapters (`state/verify/init/p
|
||||
|
||||
## `gsd-sdk query` routing
|
||||
|
||||
1. **`normalizeQueryCommand()`** (`normalize-query-command.ts`) — maps the first argv tokens to the same **command + subcommand** patterns as `gsd-tools` `runCommand()` where needed (e.g. `state json` → `state.json`, `init execute-phase 9` → `init.execute-phase` with args `['9']`, `scaffold …` → `phase.scaffold`). Re-exported from **`@gsd-build/sdk`** and **`createRegistry`’s module** (`sdk/src/query/index.ts`) so programmatic callers can mirror CLI tokenization without importing a deep path.
|
||||
1. **`normalizeQueryCommand()`** (`query-command-resolution-strategy.ts`) — maps the first argv tokens to the same **command + subcommand** patterns as `gsd-tools` `runCommand()` where needed (e.g. `state json` → `state.json`, `init execute-phase 9` → `init.execute-phase` with args `['9']`, `scaffold …` → `phase.scaffold`). Re-exported from **`@gsd-build/sdk`** and **`createRegistry`’s module** (`sdk/src/query/index.ts`) so programmatic callers can mirror CLI tokenization without importing a deep path.
|
||||
2. **`resolveQueryArgv()`** (`registry.ts`) — **longest-prefix match** on the normalized argv: tries joined keys `a.b.c` then `a b c` for each prefix length, longest first. Example: `state update status X` → handler `state.update` with args `[status, X]`.
|
||||
3. **Dotted single token**: one token like `init.new-project` matches the registry; if the first pass finds no handler, a single dotted token is split and matching runs again.
|
||||
4. **CJS fallback (CLI)**: if nothing matches a registered handler and `GSD_QUERY_FALLBACK` is not `off`/`never`/`false`/`0`, the CLI shells out to `gsd-tools.cjs` with argv derived from the normalized tokens (dotted commands are split into CJS-style segments). stderr receives a short bridge warning. Set `GSD_QUERY_FALLBACK=off` for strict mode (parity tests). CLI-only commands such as `graphify` rely on this path until native handlers exist.
|
||||
@@ -36,10 +36,27 @@ CJS routing seams mirror these families with thin adapters (`state/verify/init/p
|
||||
|
||||
## Error handling
|
||||
|
||||
- **Validation and programmer errors**: Handlers throw `GSDError` with an `ErrorClassification` (e.g. missing required args, invalid phase). The CLI maps these to exit codes via `exitCodeFor()`.
|
||||
- **Validation and programmer errors**: Handlers throw `GSDError` with an `ErrorClassification` (e.g. missing required args, invalid phase). The Dispatch Policy Module maps native failures into structured dispatch errors.
|
||||
- **Expected domain failures**: Handlers return `{ data: { error: string, ... } }` for cases that are not exceptional in normal use (file not found, intel disabled, todo missing, etc.). Callers must check `data.error` when present.
|
||||
- Do not mix both styles for the same failure mode in new code: prefer **throw** for "caller must fix input"; prefer `**data.error`** for "operation could not complete in this project state."
|
||||
|
||||
### Dispatch Policy Module contract
|
||||
|
||||
`runQueryDispatch()` returns a structured union contract:
|
||||
|
||||
- success: `{ ok: true, stdout, stderr, exit_code: 0 }`
|
||||
- failure: `{ ok: false, error: { kind, code, message, details }, stderr, exit_code }`
|
||||
|
||||
Current error `kind` values:
|
||||
- `unknown_command`
|
||||
- `native_failure`
|
||||
- `native_timeout`
|
||||
- `fallback_failure`
|
||||
- `validation_error`
|
||||
- `internal_error`
|
||||
|
||||
CLI is a thin adapter over this seam and uses `exit_code` directly.
|
||||
|
||||
## Mutation commands and events
|
||||
|
||||
- `QUERY_MUTATION_COMMANDS` in `index.ts` lists every command name (including space-delimited aliases) that performs durable writes. It drives optional `GSDEventStream` wrapping so mutations emit structured events.
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* or the persistent user preference is true (`active === true`).
|
||||
*/
|
||||
|
||||
import { CONFIG_DEFAULTS, loadConfig } from '../config.js';
|
||||
import { loadConfig } from '../config.js';
|
||||
import type { QueryHandler } from './utils.js';
|
||||
|
||||
export type AutoModeSource = 'auto_chain' | 'auto_advance' | 'both' | 'none';
|
||||
@@ -32,7 +32,6 @@ function resolveSource(
|
||||
export const checkAutoMode: QueryHandler = async (_args, projectDir) => {
|
||||
const config = await loadConfig(projectDir);
|
||||
const wf: Record<string, unknown> = {
|
||||
...CONFIG_DEFAULTS.workflow,
|
||||
...(config.workflow as unknown as Record<string, unknown>),
|
||||
};
|
||||
const autoAdvance = Boolean(wf.auto_advance ?? false);
|
||||
|
||||
@@ -105,18 +105,3 @@ export const PHASES_SUBCOMMANDS = new Set<string>(PHASES_COMMAND_ALIASES.map((en
|
||||
export const VALIDATE_SUBCOMMANDS = new Set<string>(VALIDATE_COMMAND_ALIASES.map((entry) => entry.subcommand));
|
||||
export const ROADMAP_SUBCOMMANDS = new Set<string>(ROADMAP_COMMAND_ALIASES.map((entry) => entry.subcommand));
|
||||
|
||||
export const STATE_MUTATION_COMMANDS: readonly string[] = STATE_COMMAND_ALIASES
|
||||
.filter((entry) => entry.mutation)
|
||||
.flatMap((entry) => [entry.canonical, ...entry.aliases]);
|
||||
|
||||
export const PHASE_MUTATION_COMMANDS: readonly string[] = PHASE_COMMAND_ALIASES
|
||||
.filter((entry) => entry.mutation)
|
||||
.flatMap((entry) => [entry.canonical, ...entry.aliases]);
|
||||
|
||||
export const PHASES_MUTATION_COMMANDS: readonly string[] = PHASES_COMMAND_ALIASES
|
||||
.filter((entry) => entry.mutation)
|
||||
.flatMap((entry) => [entry.canonical, ...entry.aliases]);
|
||||
|
||||
export const ROADMAP_MUTATION_COMMANDS: readonly string[] = ROADMAP_COMMAND_ALIASES
|
||||
.filter((entry) => entry.mutation)
|
||||
.flatMap((entry) => [entry.canonical, ...entry.aliases]);
|
||||
|
||||
31
sdk/src/query/command-catalog.ts
Normal file
31
sdk/src/query/command-catalog.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { QueryRegistry } from './registry.js';
|
||||
import type { QueryHandler } from './utils.js';
|
||||
|
||||
export interface AliasCatalogEntry {
|
||||
canonical: string;
|
||||
aliases: string[];
|
||||
}
|
||||
|
||||
export function registerAliasCatalog(
|
||||
registry: QueryRegistry,
|
||||
aliases: readonly AliasCatalogEntry[],
|
||||
handlers: Readonly<Record<string, QueryHandler>>,
|
||||
): void {
|
||||
for (const entry of aliases) {
|
||||
const handler = handlers[entry.canonical];
|
||||
if (!handler) continue;
|
||||
registry.register(entry.canonical, handler);
|
||||
for (const alias of entry.aliases) {
|
||||
registry.register(alias, handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function registerStaticCatalog(
|
||||
registry: QueryRegistry,
|
||||
entries: ReadonlyArray<readonly [command: string, handler: QueryHandler]>,
|
||||
): void {
|
||||
for (const [command, handler] of entries) {
|
||||
registry.register(command, handler);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user