mirror of
https://github.com/glittercowboy/get-shit-done
synced 2026-05-05 23:02:20 +02:00
Compare commits
94 Commits
fix/3061-g
...
fix/3130-u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9de8e24463 | ||
|
|
811410be61 | ||
|
|
891eae1025 | ||
|
|
858c821829 | ||
|
|
851cddcc03 | ||
|
|
61773332d6 | ||
|
|
9987792c46 | ||
|
|
aa64638176 | ||
|
|
be4a9b3b43 | ||
|
|
e7ecd46bbe | ||
|
|
985b736d45 | ||
|
|
d3d995cfc4 | ||
|
|
43e5fef95e | ||
|
|
083e813aea | ||
|
|
fe4db16769 | ||
|
|
399bb80b40 | ||
|
|
d978ad6b2f | ||
|
|
0fe88b9e7a | ||
|
|
baf0d56063 | ||
|
|
d2d1205691 | ||
|
|
1c1e3b5de4 | ||
|
|
a6d4e61606 | ||
|
|
e2b12bfad2 | ||
|
|
915e7daced | ||
|
|
313f170cf0 | ||
|
|
199083777a | ||
|
|
dbbc7f0942 | ||
|
|
2113902daf | ||
|
|
f01f6b76dd | ||
|
|
4ee6ce4a01 | ||
|
|
67684626d8 | ||
|
|
b331c48261 | ||
|
|
3d2f2e85a0 | ||
|
|
5b63ba6ea9 | ||
|
|
a4d16c3c93 | ||
|
|
78846b1e6a | ||
|
|
59fd17251a | ||
|
|
efa642a078 | ||
|
|
120113c42b | ||
|
|
2d25c97706 | ||
|
|
2dcf374da0 | ||
|
|
50f714cdd5 | ||
|
|
471df09242 | ||
|
|
ecd5d11b32 | ||
|
|
58062a64a0 | ||
|
|
65024683fd | ||
|
|
72f4c3b362 | ||
|
|
538ef683be | ||
|
|
c7886415c3 | ||
|
|
a54dda3837 | ||
|
|
19e580137d | ||
|
|
78c794c016 | ||
|
|
40acf1f02e | ||
|
|
1642f47908 | ||
|
|
38718e9d4b | ||
|
|
a441f96f37 | ||
|
|
0500bdf619 | ||
|
|
c6a35d6398 | ||
|
|
969cfcf998 | ||
|
|
e0c791a5d0 | ||
|
|
deb4477375 | ||
|
|
5aaf0dbea5 | ||
|
|
ace241d0c2 | ||
|
|
0fffc7c055 | ||
|
|
6059a574f2 | ||
|
|
b0e616288b | ||
|
|
ed9d67c91b | ||
|
|
97019d274e | ||
|
|
7311e0a9ab | ||
|
|
c66ff96de8 | ||
|
|
a24de43f8b | ||
|
|
70faa0ff0f | ||
|
|
b9e3979fc1 | ||
|
|
c7d3f83b8b | ||
|
|
bc289fad4a | ||
|
|
9bee4dce4a | ||
|
|
9a469fa05c | ||
|
|
abf7779088 | ||
|
|
16bf552037 | ||
|
|
009cfb1562 | ||
|
|
6fe4af2546 | ||
|
|
41683b2f53 | ||
|
|
7dcafbc211 | ||
|
|
ccda572ade | ||
|
|
1ca7f58831 | ||
|
|
7298a76b20 | ||
|
|
5cfd874058 | ||
|
|
ba6100c548 | ||
|
|
9f5b011b35 | ||
|
|
1037b82a98 | ||
|
|
ac883f8150 | ||
|
|
3e22c70fac | ||
|
|
12fc34689e | ||
|
|
9d096b9925 |
5
.changeset/fix-3054-doc-anchor-and-token-check.md
Normal file
5
.changeset/fix-3054-doc-anchor-and-token-check.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 3114
|
||||
---
|
||||
**`/gsd-progress --next` doc migration is fully consistent** — command docs now use clear `--next` wording, FEATURES TOC anchors match renamed headings, and regression tests enforce stale-command detection via structured slash-command token checks.
|
||||
5
.changeset/fix-3056-worktree-path-assertion.md
Normal file
5
.changeset/fix-3056-worktree-path-assertion.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 3117
|
||||
---
|
||||
**Worktree prune regression checks are now path-normalized** — pruning safety tests now parse `git worktree list --porcelain` and assert structured normalized paths, preventing path-separator false negatives across platforms while preserving non-destructive prune guarantees.
|
||||
5
.changeset/fix-3072-findings-probe-assertions.md
Normal file
5
.changeset/fix-3072-findings-probe-assertions.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 3119
|
||||
---
|
||||
**Optional findings probe guard checks now use structured parsing** — regression tests now parse fenced bash blocks and validate sketch/spike findings probes as structured command records, ensuring non-fatal `|| true` guards are enforced without raw source grep assertions.
|
||||
5
.changeset/fix-3088-milestone-state-fallback-sections.md
Normal file
5
.changeset/fix-3088-milestone-state-fallback-sections.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 3122
|
||||
---
|
||||
**Milestone close now repairs missing STATE narrative sections** — when `## Current Position` or `## Operator Next Steps` headings are absent, milestone completion appends canonical sections so state remains deterministic and consistently points operators to `/gsd-new-milestone`.
|
||||
5
.changeset/fix-3094-progress-stale-assumptions.md
Normal file
5
.changeset/fix-3094-progress-stale-assumptions.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 3111
|
||||
---
|
||||
**Progress routing command guidance remains canonical** — pre-planning assumption checks in progress routing now consistently assert and document `/gsd-discuss-phase` as the replacement path, with tests enforcing structured slash-command token checks.
|
||||
5
.changeset/lively-moles-caper.md
Normal file
5
.changeset/lively-moles-caper.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 3043
|
||||
---
|
||||
milestone complete now scopes phase stats to the explicit version argument and errors when that version is missing from a versioned ROADMAP milestone section.
|
||||
5
.changeset/plucky-pandas-sprint.md
Normal file
5
.changeset/plucky-pandas-sprint.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Changed
|
||||
pr: 3108
|
||||
---
|
||||
Query module architecture deepened with compatibility-preserving seams — command policy now derives from command definitions, and dispatch/topology/registry seams are consolidated for better locality while preserving existing query behavior.
|
||||
5
.changeset/pr-3112-release-note.md
Normal file
5
.changeset/pr-3112-release-note.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 3112
|
||||
---
|
||||
Fixes for issue #3112 were applied to keep command/workflow behavior and SDK parity aligned with current documented usage.
|
||||
5
.changeset/pr-3113-release-note.md
Normal file
5
.changeset/pr-3113-release-note.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 3113
|
||||
---
|
||||
Fixes for issue #3113 were applied to keep command/workflow behavior and SDK parity aligned with current documented usage.
|
||||
5
.changeset/pr-3115-release-note.md
Normal file
5
.changeset/pr-3115-release-note.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 3115
|
||||
---
|
||||
Fixes for issue #3115 were applied to keep command/workflow behavior and SDK parity aligned with current documented usage.
|
||||
5
.changeset/pr-3116-release-note.md
Normal file
5
.changeset/pr-3116-release-note.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 3116
|
||||
---
|
||||
Fixes for issue #3116 were applied to keep command/workflow behavior and SDK parity aligned with current documented usage.
|
||||
5
.changeset/pr-3118-release-note.md
Normal file
5
.changeset/pr-3118-release-note.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 3118
|
||||
---
|
||||
Fixes for issue #3118 were applied to keep command/workflow behavior and SDK parity aligned with current documented usage.
|
||||
5
.changeset/pr-3123-release-note.md
Normal file
5
.changeset/pr-3123-release-note.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 3123
|
||||
---
|
||||
Fixes for issue #3123 were applied to keep command/workflow behavior and SDK parity aligned with current documented usage.
|
||||
5
.changeset/pr-3124-release-note.md
Normal file
5
.changeset/pr-3124-release-note.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 3124
|
||||
---
|
||||
Fixes for issue #3124 were applied to keep command/workflow behavior and SDK parity aligned with current documented usage.
|
||||
5
.changeset/pr-3125-release-note.md
Normal file
5
.changeset/pr-3125-release-note.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 3125
|
||||
---
|
||||
Fixes for issue #3098 were applied to keep command/workflow behavior and SDK parity aligned with current documented usage.
|
||||
6
.changeset/rewire-orphaned-workflows-3131.md
Normal file
6
.changeset/rewire-orphaned-workflows-3131.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
type: Changed
|
||||
pr: 3131
|
||||
---
|
||||
|
||||
**Re-wired 4 orphaned workflows as flags on parent commands** — six workflows were mis-categorised as "outright deleted dead skills" during the #2790 consolidation; two were caught by prior PRs (#3045, #3038) and four are fixed here. New flags: `/gsd-discuss-phase --assumptions` (surfaces Claude's implementation assumptions before planning), `/gsd-pause-work --report` (generates a post-session summary in `.planning/reports/`), `/gsd-manager --analyze-deps` (scans ROADMAP phases for dependency relationships before parallel execution), `/gsd-import --from-gsd2` (reverse-migrates a GSD-2 `.gsd/` project back to GSD v1 `.planning/` format). Also sweeps 29 stale `/gsd-*` command references across 27 user-facing files (English + 4 locales). Closes #3131.
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -66,3 +66,4 @@ vendor/
|
||||
.cache/
|
||||
tmp/
|
||||
.worktrees
|
||||
.envrc
|
||||
|
||||
@@ -80,7 +80,7 @@ GSDはそれを解決します。Claude Codeを信頼性の高いものにする
|
||||
完全なリストは [v1.39.0 リリースノート](https://github.com/gsd-build/get-shit-done/releases/tag/v1.39.0) を参照してください。
|
||||
|
||||
- **`--minimal` インストールプロファイル** — エイリアス `--core-only`。メインループの6スキル(`new-project`、`discuss-phase`、`plan-phase`、`execute-phase`、`help`、`update`)のみをインストールし、`gsd-*` サブエージェントはゼロ。コールドスタート時のシステムプロンプトのオーバーヘッドを ~12kトークンから ~700トークンへ削減(≥94%減)。32K〜128Kコンテキストのローカル LLM やトークン課金 API に有効。
|
||||
- **`/gsd-edit-phase`** — `ROADMAP.md` 上の既存フェーズの任意フィールドをその場で編集(番号や位置は変更されない)。`--force` で確認 diff をスキップ、`depends_on` の参照を検証し、書き込み時に `STATE.md` も更新。
|
||||
- **`/gsd-phase --edit`** — `ROADMAP.md` 上の既存フェーズの任意フィールドをその場で編集(番号や位置は変更されない)。`--force` で確認 diff をスキップ、`depends_on` の参照を検証し、書き込み時に `STATE.md` も更新。
|
||||
- **マージ後ビルド & テストゲート** — `execute-phase` のステップ 5.6 が `workflow.build_command` の設定を自動検出し、無ければ Xcode(`.xcodeproj`)、Makefile、Justfile、Cargo、Go、Python、npm の順にフォールバック。Xcode/iOS プロジェクトでは `xcodebuild build` と `xcodebuild test` を自動実行。並列・直列両モードで動作。
|
||||
- **ランタイム別レビューモデル選択** — `review.models.<cli>` で各外部レビュー CLI(codex、gemini など)が使うモデルをプランナー/実行プロファイルとは独立に指定可能。
|
||||
- **ワークストリーム設定の継承** — `GSD_WORKSTREAM` が設定されている場合、ルートの `.planning/config.json` を先に読み込み、ワークストリーム設定をディープマージ(衝突時はワークストリーム側が優先)。ワークストリーム設定で明示的に `null` を指定するとルート値を上書き可能。
|
||||
@@ -396,7 +396,7 @@ claude --dangerously-skip-permissions
|
||||
またはGSDに次のステップを自動判定させます:
|
||||
|
||||
```
|
||||
/gsd-next # 次のステップを自動検出して実行
|
||||
/gsd-progress --next # 次のステップを自動検出して実行
|
||||
```
|
||||
|
||||
**discuss → plan → execute → verify → ship** のループをマイルストーン完了まで繰り返します。
|
||||
@@ -544,7 +544,7 @@ lmn012o feat(08-02): create registration endpoint
|
||||
| `/gsd-execute-phase <N>` | 全プランを並列ウェーブで実行し、完了時に検証 |
|
||||
| `/gsd-verify-work [N]` | 手動ユーザー受入テスト ¹ |
|
||||
| `/gsd-ship [N] [--draft]` | 検証済みのフェーズ作業から自動生成された本文付きのPRを作成 |
|
||||
| `/gsd-next` | 次の論理的なワークフローステップに自動的に進む |
|
||||
| `/gsd-progress --next` | 次の論理的なワークフローステップに自動的に進む |
|
||||
| `/gsd-fast <text>` | インラインの軽微タスク — 計画を完全にスキップし即座に実行 |
|
||||
| `/gsd-audit-milestone` | マイルストーンが完了の定義を達成したか検証 |
|
||||
| `/gsd-complete-milestone` | マイルストーンをアーカイブし、リリースをタグ付け |
|
||||
@@ -565,9 +565,9 @@ lmn012o feat(08-02): create registration endpoint
|
||||
|
||||
| コマンド | 説明 |
|
||||
|---------|--------------|
|
||||
| `/gsd-new-workspace` | リポジトリのコピー(worktreeまたはクローン)で隔離されたワークスペースを作成 |
|
||||
| `/gsd-list-workspaces` | すべてのGSDワークスペースとそのステータスを表示 |
|
||||
| `/gsd-remove-workspace` | ワークスペースを削除しworktreeをクリーンアップ |
|
||||
| `/gsd-workspace --new` | リポジトリのコピー(worktreeまたはクローン)で隔離されたワークスペースを作成 |
|
||||
| `/gsd-workspace --list` | すべてのGSDワークスペースとそのステータスを表示 |
|
||||
| `/gsd-workspace --remove` | ワークスペースを削除しworktreeをクリーンアップ |
|
||||
|
||||
### UIデザイン
|
||||
|
||||
@@ -581,10 +581,9 @@ lmn012o feat(08-02): create registration endpoint
|
||||
| コマンド | 説明 |
|
||||
|---------|--------------|
|
||||
| `/gsd-progress` | 今どこにいる?次は何? |
|
||||
| `/gsd-next` | 状態を自動検出し次のステップを実行 |
|
||||
| `/gsd-progress --next` | 状態を自動検出し次のステップを実行 |
|
||||
| `/gsd-help` | 全コマンドと使い方ガイドを表示 |
|
||||
| `/gsd-update` | チェンジログプレビュー付きでGSDをアップデート |
|
||||
| `/gsd-join-discord` | GSD Discordコミュニティに参加 |
|
||||
| `/gsd-manager` | 複数フェーズ管理用のインタラクティブコマンドセンター |
|
||||
|
||||
### ブラウンフィールド
|
||||
@@ -597,12 +596,12 @@ lmn012o feat(08-02): create registration endpoint
|
||||
|
||||
| コマンド | 説明 |
|
||||
|---------|--------------|
|
||||
| `/gsd-add-phase` | ロードマップにフェーズを追加 |
|
||||
| `/gsd-insert-phase [N]` | フェーズ間に緊急作業を挿入 |
|
||||
| `/gsd-edit-phase [N] [--force]` | 既存フェーズの任意フィールドをその場で編集 — 番号と位置は変更されない |
|
||||
| `/gsd-remove-phase [N]` | 将来のフェーズを削除し番号を振り直し |
|
||||
| `/gsd-list-phase-assumptions [N]` | 計画前にClaudeの意図するアプローチを確認 |
|
||||
| `/gsd-plan-milestone-gaps` | 監査で見つかったギャップを埋めるフェーズを作成 |
|
||||
| `/gsd-phase` | ロードマップにフェーズを追加 |
|
||||
| `/gsd-phase --insert [N]` | フェーズ間に緊急作業を挿入 |
|
||||
| `/gsd-phase --edit [N] [--force]` | 既存フェーズの任意フィールドをその場で編集 — 番号と位置は変更されない |
|
||||
| `/gsd-phase --remove [N]` | 将来のフェーズを削除し番号を振り直し |
|
||||
| `/gsd-discuss-phase --assumptions [N]` | 計画前にClaudeの意図するアプローチを確認 |
|
||||
| `/gsd-audit-milestone --fix` | 監査で見つかったギャップを埋めるフェーズを作成 |
|
||||
|
||||
### セッション
|
||||
|
||||
@@ -610,7 +609,7 @@ lmn012o feat(08-02): create registration endpoint
|
||||
|---------|--------------|
|
||||
| `/gsd-pause-work` | フェーズ途中で停止する際の引き継ぎを作成(HANDOFF.jsonを書き込み) |
|
||||
| `/gsd-resume-work` | 前回のセッションから復元 |
|
||||
| `/gsd-session-report` | 実行した作業と結果のセッションサマリーを生成 |
|
||||
| `/gsd-pause-work --report` | 実行した作業と結果のセッションサマリーを生成 |
|
||||
|
||||
### ワークストリーム
|
||||
|
||||
@@ -630,8 +629,8 @@ lmn012o feat(08-02): create registration endpoint
|
||||
|
||||
| コマンド | 説明 |
|
||||
|---------|--------------|
|
||||
| `/gsd-plant-seed <idea>` | トリガー条件付きの将来志向のアイデアをキャプチャ — 適切なマイルストーンで浮上 |
|
||||
| `/gsd-add-backlog <desc>` | バックログのパーキングロットにアイデアを追加(999.xナンバリング、アクティブシーケンス外) |
|
||||
| `/gsd-capture --seed <idea>` | トリガー条件付きの将来志向のアイデアをキャプチャ — 適切なマイルストーンで浮上 |
|
||||
| `/gsd-capture --backlog <desc>` | バックログのパーキングロットにアイデアを追加(999.xナンバリング、アクティブシーケンス外) |
|
||||
| `/gsd-review-backlog` | バックログ項目をレビューし、アクティブマイルストーンに昇格またはstaleエントリを削除 |
|
||||
| `/gsd-thread [name]` | 永続コンテキストスレッド — 複数セッションにまたがる作業用の軽量クロスセッション知識 |
|
||||
|
||||
@@ -640,9 +639,9 @@ lmn012o feat(08-02): create registration endpoint
|
||||
| コマンド | 説明 |
|
||||
|---------|--------------|
|
||||
| `/gsd-settings` | モデルプロファイルとワークフローエージェントを設定 |
|
||||
| `/gsd-set-profile <profile>` | モデルプロファイルを切り替え(quality/balanced/budget/inherit) |
|
||||
| `/gsd-add-todo [desc]` | 後で取り組むアイデアをキャプチャ |
|
||||
| `/gsd-check-todos` | 保留中のtodoを一覧表示 |
|
||||
| `/gsd-config --profile <profile>` | モデルプロファイルを切り替え(quality/balanced/budget/inherit) |
|
||||
| `/gsd-capture [desc]` | 後で取り組むアイデアをキャプチャ |
|
||||
| `/gsd-capture --list` | 保留中のtodoを一覧表示 |
|
||||
| `/gsd-debug [desc]` | 永続状態を持つ体系的デバッグ |
|
||||
| `/gsd-do <text>` | フリーフォームテキストを適切なGSDコマンドに自動ルーティング |
|
||||
| `/gsd-note <text>` | ゼロフリクションのアイデアキャプチャ — ノートの追加、一覧、todoへの昇格 |
|
||||
@@ -679,7 +678,7 @@ GSDはプロジェクト設定を `.planning/config.json` に保存します。`
|
||||
|
||||
プロファイルの切り替え:
|
||||
```
|
||||
/gsd-set-profile budget
|
||||
/gsd-config --profile budget
|
||||
```
|
||||
|
||||
非Anthropicプロバイダー(OpenRouter、ローカルモデル)を使用する場合や、現在のランタイムのモデル選択に従う場合(例:OpenCode `/model`)は `inherit` を使用してください。
|
||||
|
||||
@@ -80,7 +80,7 @@ GSD가 그걸 고칩니다. Claude Code를 신뢰할 수 있게 만드는 컨텍
|
||||
전체 목록은 [v1.39.0 릴리스 노트](https://github.com/gsd-build/get-shit-done/releases/tag/v1.39.0)를 참고하세요.
|
||||
|
||||
- **`--minimal` 설치 프로파일** — 별칭 `--core-only`. 메인 루프 6개 스킬(`new-project`, `discuss-phase`, `plan-phase`, `execute-phase`, `help`, `update`)만 설치하고 `gsd-*` 서브에이전트는 설치하지 않음. 콜드 스타트 시스템 프롬프트 오버헤드를 ~12k 토큰에서 ~700 토큰으로 축소(≥94% 감소). 32K–128K 컨텍스트의 로컬 LLM이나 토큰 과금 API에 유용.
|
||||
- **`/gsd-edit-phase`** — `ROADMAP.md`에 있는 기존 단계의 임의 필드를 그 자리에서 수정(번호와 위치는 변경되지 않음). `--force`는 확인 diff를 건너뛰고, `depends_on` 참조를 검증하며 쓰기 시 `STATE.md`도 갱신.
|
||||
- **`/gsd-phase --edit`** — `ROADMAP.md`에 있는 기존 단계의 임의 필드를 그 자리에서 수정(번호와 위치는 변경되지 않음). `--force`는 확인 diff를 건너뛰고, `depends_on` 참조를 검증하며 쓰기 시 `STATE.md`도 갱신.
|
||||
- **머지 후 빌드 & 테스트 게이트** — `execute-phase` 5.6 단계가 `workflow.build_command` 설정을 우선 자동 감지하고, 없으면 Xcode(`.xcodeproj`), Makefile, Justfile, Cargo, Go, Python, npm 순으로 폴백. Xcode/iOS 프로젝트는 `xcodebuild build` 및 `xcodebuild test`를 자동 실행. 병렬·직렬 모드 모두에서 동작.
|
||||
- **런타임별 리뷰 모델 선택** — `review.models.<cli>`로 각 외부 리뷰 CLI(codex, gemini 등)가 플래너/실행 프로파일과 독립적으로 자체 모델을 선택할 수 있음.
|
||||
- **워크스트림 설정 상속** — `GSD_WORKSTREAM`이 설정되면 루트 `.planning/config.json`을 먼저 로드한 뒤 워크스트림 설정을 딥 머지(충돌 시 워크스트림 우선). 워크스트림 설정에서 명시적 `null`은 루트 값을 덮어씀.
|
||||
@@ -396,7 +396,7 @@ claude --dangerously-skip-permissions
|
||||
또는 GSD가 다음 단계를 자동으로 파악하게 합니다:
|
||||
|
||||
```
|
||||
/gsd-next # 다음 단계 자동 감지 및 실행
|
||||
/gsd-progress --next # 다음 단계 자동 감지 및 실행
|
||||
```
|
||||
|
||||
마일스톤이 완료될 때까지 **논의 → 기획 → 실행 → 검증 → 출시** 반복.
|
||||
@@ -541,7 +541,7 @@ lmn012o feat(08-02): create registration endpoint
|
||||
| `/gsd-execute-phase <N>` | 병렬 웨이브로 모든 계획 실행, 완료 시 검증 |
|
||||
| `/gsd-verify-work [N]` | 수동 사용자 인수 테스트 ¹ |
|
||||
| `/gsd-ship [N] [--draft]` | 자동 생성된 본문으로 검증된 단계 작업에서 PR 생성 |
|
||||
| `/gsd-next` | 다음 논리적 워크플로우 단계로 자동 진행 |
|
||||
| `/gsd-progress --next` | 다음 논리적 워크플로우 단계로 자동 진행 |
|
||||
| `/gsd-fast <text>` | 인라인 사소한 작업 — 기획 완전 건너뛰고 즉시 실행 |
|
||||
| `/gsd-audit-milestone` | 마일스톤이 완료 정의를 달성했는지 검증 |
|
||||
| `/gsd-complete-milestone` | 마일스톤 아카이브, 릴리스 태그 |
|
||||
@@ -562,9 +562,9 @@ lmn012o feat(08-02): create registration endpoint
|
||||
|
||||
| 명령어 | 역할 |
|
||||
|---------|------------|
|
||||
| `/gsd-new-workspace` | 저장소 복사본으로 격리된 워크스페이스 생성 (worktrees 또는 clones) |
|
||||
| `/gsd-list-workspaces` | 모든 GSD 워크스페이스와 상태 표시 |
|
||||
| `/gsd-remove-workspace` | 워크스페이스 제거 및 worktree 정리 |
|
||||
| `/gsd-workspace --new` | 저장소 복사본으로 격리된 워크스페이스 생성 (worktrees 또는 clones) |
|
||||
| `/gsd-workspace --list` | 모든 GSD 워크스페이스와 상태 표시 |
|
||||
| `/gsd-workspace --remove` | 워크스페이스 제거 및 worktree 정리 |
|
||||
|
||||
### UI 디자인
|
||||
|
||||
@@ -578,10 +578,9 @@ lmn012o feat(08-02): create registration endpoint
|
||||
| 명령어 | 역할 |
|
||||
|---------|------------|
|
||||
| `/gsd-progress` | 지금 어디에 있나? 다음은? |
|
||||
| `/gsd-next` | 상태 자동 감지 및 다음 단계 실행 |
|
||||
| `/gsd-progress --next` | 상태 자동 감지 및 다음 단계 실행 |
|
||||
| `/gsd-help` | 모든 명령어와 사용 가이드 표시 |
|
||||
| `/gsd-update` | 변경 로그 미리보기와 함께 GSD 업데이트 |
|
||||
| `/gsd-join-discord` | GSD Discord 커뮤니티 참여 |
|
||||
| `/gsd-manager` | 여러 단계 관리를 위한 대화형 커맨드 센터 |
|
||||
|
||||
### 브라운필드
|
||||
@@ -594,12 +593,12 @@ lmn012o feat(08-02): create registration endpoint
|
||||
|
||||
| 명령어 | 역할 |
|
||||
|---------|------------|
|
||||
| `/gsd-add-phase` | 로드맵에 단계 추가 |
|
||||
| `/gsd-insert-phase [N]` | 단계 사이에 긴급 작업 삽입 |
|
||||
| `/gsd-edit-phase [N] [--force]` | 기존 단계의 임의 필드를 그 자리에서 수정 — 번호와 위치는 그대로 |
|
||||
| `/gsd-remove-phase [N]` | 미래 단계 제거, 번호 재정렬 |
|
||||
| `/gsd-list-phase-assumptions [N]` | 기획 전 Claude의 의도된 접근 방식 확인 |
|
||||
| `/gsd-plan-milestone-gaps` | 감사에서 발견된 갭을 해소하기 위한 단계 생성 |
|
||||
| `/gsd-phase` | 로드맵에 단계 추가 |
|
||||
| `/gsd-phase --insert [N]` | 단계 사이에 긴급 작업 삽입 |
|
||||
| `/gsd-phase --edit [N] [--force]` | 기존 단계의 임의 필드를 그 자리에서 수정 — 번호와 위치는 그대로 |
|
||||
| `/gsd-phase --remove [N]` | 미래 단계 제거, 번호 재정렬 |
|
||||
| `/gsd-discuss-phase --assumptions [N]` | 기획 전 Claude의 의도된 접근 방식 확인 |
|
||||
| `/gsd-audit-milestone --fix` | 감사에서 발견된 갭을 해소하기 위한 단계 생성 |
|
||||
|
||||
### 세션
|
||||
|
||||
@@ -607,7 +606,7 @@ lmn012o feat(08-02): create registration endpoint
|
||||
|---------|------------|
|
||||
| `/gsd-pause-work` | 단계 중간에 멈출 때 핸드오프 생성 (HANDOFF.json 작성) |
|
||||
| `/gsd-resume-work` | 마지막 세션에서 복원 |
|
||||
| `/gsd-session-report` | 수행한 작업과 결과가 담긴 세션 요약 생성 |
|
||||
| `/gsd-pause-work --report` | 수행한 작업과 결과가 담긴 세션 요약 생성 |
|
||||
|
||||
### 코드 품질
|
||||
|
||||
@@ -621,8 +620,8 @@ lmn012o feat(08-02): create registration endpoint
|
||||
|
||||
| 명령어 | 역할 |
|
||||
|---------|------------|
|
||||
| `/gsd-plant-seed <idea>` | 트리거 조건이 있는 아이디어 저장 — 때가 되면 알아서 올라옴 |
|
||||
| `/gsd-add-backlog <desc>` | 백로그 파킹 롯에 아이디어 추가 (999.x 번호 지정, 활성 시퀀스 외부) |
|
||||
| `/gsd-capture --seed <idea>` | 트리거 조건이 있는 아이디어 저장 — 때가 되면 알아서 올라옴 |
|
||||
| `/gsd-capture --backlog <desc>` | 백로그 파킹 롯에 아이디어 추가 (999.x 번호 지정, 활성 시퀀스 외부) |
|
||||
| `/gsd-review-backlog` | 백로그 항목 리뷰 및 활성 마일스톤으로 승격하거나 오래된 항목 제거 |
|
||||
| `/gsd-thread [name]` | 지속적 컨텍스트 스레드 — 여러 세션에 걸친 작업을 위한 가벼운 크로스 세션 지식 |
|
||||
|
||||
@@ -631,9 +630,9 @@ lmn012o feat(08-02): create registration endpoint
|
||||
| 명령어 | 역할 |
|
||||
|---------|------------|
|
||||
| `/gsd-settings` | 모델 프로필 및 워크플로우 에이전트 설정 |
|
||||
| `/gsd-set-profile <profile>` | 모델 프로필 전환 (quality/balanced/budget/inherit) |
|
||||
| `/gsd-add-todo [desc]` | 나중을 위한 아이디어 캡처 |
|
||||
| `/gsd-check-todos` | 대기 중인 할 일 목록 |
|
||||
| `/gsd-config --profile <profile>` | 모델 프로필 전환 (quality/balanced/budget/inherit) |
|
||||
| `/gsd-capture [desc]` | 나중을 위한 아이디어 캡처 |
|
||||
| `/gsd-capture --list` | 대기 중인 할 일 목록 |
|
||||
| `/gsd-debug [desc]` | 지속적 상태를 이용한 체계적 디버깅 |
|
||||
| `/gsd-do <text>` | 자유 형식 텍스트를 적절한 GSD 명령어로 자동 라우팅 |
|
||||
| `/gsd-note <text>` | 마찰 없는 아이디어 캡처 — 추가, 목록, 또는 할 일로 승격 |
|
||||
@@ -670,7 +669,7 @@ GSD는 프로젝트 설정을 `.planning/config.json`에 저장합니다. `/gsd-
|
||||
|
||||
프로필 전환:
|
||||
```
|
||||
/gsd-set-profile budget
|
||||
/gsd-config --profile budget
|
||||
```
|
||||
|
||||
비-Anthropic 제공업체 (OpenRouter, 로컬 모델) 사용 시 또는 현재 런타임 모델 선택을 따를 때 (예: OpenCode `/model`) `inherit`를 사용하세요.
|
||||
|
||||
39
README.md
39
README.md
@@ -94,7 +94,7 @@ Built-in quality gates catch real problems: schema drift detection flags ORM cha
|
||||
See the [v1.39.0 release notes](https://github.com/gsd-build/get-shit-done/releases/tag/v1.39.0) for the full list.
|
||||
|
||||
- **`--minimal` install profile** — alias `--core-only`, writes only the six main-loop 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 ~700 (≥94% reduction). Useful for local LLMs with 32K–128K context and token-billed APIs.
|
||||
- **`/gsd-edit-phase`** — modify any field of an existing phase in `ROADMAP.md` in place, without changing its number or position. `--force` skips the confirmation diff; `depends_on` references are validated and `STATE.md` is updated on write.
|
||||
- **`/gsd-phase --edit`** — modify any field of an existing phase in `ROADMAP.md` in place, without changing its number or position. `--force` skips the confirmation diff; `depends_on` references are validated and `STATE.md` is updated on write.
|
||||
- **Post-merge build & test gate** — `execute-phase` step 5.6 now auto-detects the build command from `workflow.build_command`, then falls back to Xcode (`.xcodeproj`), Makefile, Justfile, Cargo, Go, Python, or npm. Xcode/iOS projects get `xcodebuild build` + `xcodebuild test` automatically. Runs in both parallel and serial mode.
|
||||
- **Per-runtime review-model selection** — `review.models.<cli>` lets each external review CLI (codex, gemini, etc.) pick its own model independently of the planner/executor profile.
|
||||
- **Workstream config inheritance** — when `GSD_WORKSTREAM` is set, the root `.planning/config.json` is loaded first and deep-merged with the workstream config (workstream wins on conflict). Explicit `null` in a workstream config now correctly overrides a root value.
|
||||
@@ -485,7 +485,7 @@ If everything passes, you move on. If something's broken, you don't manually deb
|
||||
Or let GSD figure out the next step automatically:
|
||||
|
||||
```
|
||||
/gsd-next # Auto-detect and run next step
|
||||
/gsd-progress --next # Auto-detect and run next step
|
||||
```
|
||||
|
||||
Loop **discuss → plan → execute → verify → ship** until milestone complete.
|
||||
@@ -630,7 +630,7 @@ You're never locked in. The system adapts.
|
||||
| `/gsd-execute-phase <N>` | Execute all plans in parallel waves, verify when complete |
|
||||
| `/gsd-verify-work [N]` | Manual user acceptance testing ¹ |
|
||||
| `/gsd-ship [N] [--draft]` | Create PR from verified phase work with auto-generated body |
|
||||
| `/gsd-next` | Automatically advance to the next logical workflow step |
|
||||
| `/gsd-progress --next` | Automatically advance to the next logical workflow step |
|
||||
| `/gsd-fast <text>` | Inline trivial tasks — skips planning entirely, executes immediately |
|
||||
| `/gsd-audit-milestone` | Verify milestone achieved its definition of done |
|
||||
| `/gsd-complete-milestone` | Archive milestone, tag release |
|
||||
@@ -652,8 +652,8 @@ You're never locked in. The system adapts.
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/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 |
|
||||
| `/gsd-workspace --list` | Show all GSD workspaces and their status |
|
||||
| `/gsd-workspace --remove` | Remove workspace and clean up worktrees |
|
||||
|
||||
### Spiking & Sketching
|
||||
|
||||
@@ -661,8 +661,8 @@ You're never locked in. The system adapts.
|
||||
|---------|--------------|
|
||||
| `/gsd-spike [idea] [--quick]` | Throwaway experiments to validate feasibility before planning — no project init required |
|
||||
| `/gsd-sketch [idea] [--quick]` | Throwaway HTML mockups with multi-variant exploration — no project init required |
|
||||
| `/gsd-spike-wrap-up` | Package spike findings into a project-local skill for future build conversations |
|
||||
| `/gsd-sketch-wrap-up` | Package sketch design findings into a project-local skill for future builds |
|
||||
| `/gsd-spike --wrap-up` | Package spike findings into a project-local skill for future build conversations |
|
||||
| `/gsd-sketch --wrap-up` | Package sketch design findings into a project-local skill for future builds |
|
||||
|
||||
### UI Design
|
||||
|
||||
@@ -676,10 +676,9 @@ You're never locked in. The system adapts.
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/gsd-progress` | Where am I? What's next? |
|
||||
| `/gsd-next` | Auto-detect state and run the next step |
|
||||
| `/gsd-progress --next` | Auto-detect state and run the next step |
|
||||
| `/gsd-help` | Show all commands and usage guide |
|
||||
| `/gsd-update` | Update GSD with changelog preview |
|
||||
| `/gsd-join-discord` | Join the GSD Discord community |
|
||||
| `/gsd-manager` | Interactive command center for managing multiple phases |
|
||||
|
||||
### Brownfield
|
||||
@@ -693,11 +692,11 @@ You're never locked in. The system adapts.
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/gsd-add-phase` | Append phase to roadmap |
|
||||
| `/gsd-insert-phase [N]` | Insert urgent work between phases |
|
||||
| `/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-phase` | Append phase to roadmap |
|
||||
| `/gsd-phase --insert [N]` | Insert urgent work between phases |
|
||||
| `/gsd-phase --edit [N] [--force]` | Modify any field of an existing phase in place — number and position unchanged |
|
||||
| `/gsd-phase --remove [N]` | Remove future phase, renumber |
|
||||
| `/gsd-discuss-phase --assumptions [N]` | See Claude's intended approach before planning |
|
||||
|
||||
### Session
|
||||
|
||||
@@ -705,7 +704,7 @@ You're never locked in. The system adapts.
|
||||
|---------|--------------|
|
||||
| `/gsd-pause-work` | Create handoff when stopping mid-phase (writes HANDOFF.json) |
|
||||
| `/gsd-resume-work` | Restore from last session |
|
||||
| `/gsd-session-report` | Generate session summary with work performed and outcomes |
|
||||
| `/gsd-pause-work --report` | Generate session summary with work performed and outcomes |
|
||||
|
||||
### Workstreams
|
||||
|
||||
@@ -727,8 +726,8 @@ You're never locked in. The system adapts.
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/gsd-plant-seed <idea>` | Capture forward-looking ideas with trigger conditions — surfaces at the right milestone |
|
||||
| `/gsd-add-backlog <desc>` | Add idea to backlog parking lot (999.x numbering, outside active sequence) |
|
||||
| `/gsd-capture --seed <idea>` | Capture forward-looking ideas with trigger conditions — surfaces at the right milestone |
|
||||
| `/gsd-capture --backlog <desc>` | Add idea to backlog parking lot (999.x numbering, outside active sequence) |
|
||||
| `/gsd-review-backlog` | Review and promote backlog items to active milestone or remove stale entries |
|
||||
| `/gsd-thread [name]` | Persistent context threads — lightweight cross-session knowledge for work spanning multiple sessions |
|
||||
|
||||
@@ -737,8 +736,8 @@ You're never locked in. The system adapts.
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/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-config --profile <profile>` | Switch model profile (quality/balanced/budget/inherit) |
|
||||
| `/gsd-capture [desc]` | Capture idea for later |
|
||||
| `/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 |
|
||||
@@ -779,7 +778,7 @@ Control which Claude model each agent uses. Balance quality vs token spend.
|
||||
|
||||
Switch profiles:
|
||||
```
|
||||
/gsd-set-profile budget
|
||||
/gsd-config --profile budget
|
||||
```
|
||||
|
||||
Use `inherit` when using non-Anthropic providers (OpenRouter, local models) or to follow the current runtime model selection (e.g. OpenCode `/model`).
|
||||
|
||||
@@ -78,7 +78,7 @@ Quality gates embutidos capturam problemas reais: detecção de schema drift sin
|
||||
Lista completa nas [notas de release v1.39.0](https://github.com/gsd-build/get-shit-done/releases/tag/v1.39.0).
|
||||
|
||||
- **Perfil de instalação `--minimal`** — alias `--core-only`. Instala apenas os 6 skills do loop principal (`new-project`, `discuss-phase`, `plan-phase`, `execute-phase`, `help`, `update`) e nenhum subagente `gsd-*`. Reduz o overhead do system prompt no cold-start de ~12k para ~700 tokens (≥94% de redução). Útil para LLMs locais com contexto de 32K–128K e APIs cobradas por token.
|
||||
- **`/gsd-edit-phase`** — edita qualquer campo de uma fase existente em `ROADMAP.md` no lugar, sem alterar o número ou a posição. `--force` pula o diff de confirmação; referências em `depends_on` são validadas e o `STATE.md` é atualizado na escrita.
|
||||
- **`/gsd-phase --edit`** — edita qualquer campo de uma fase existente em `ROADMAP.md` no lugar, sem alterar o número ou a posição. `--force` pula o diff de confirmação; referências em `depends_on` são validadas e o `STATE.md` é atualizado na escrita.
|
||||
- **Build & test gate pós-merge** — o passo 5.6 de `execute-phase` agora detecta automaticamente o comando de build em `workflow.build_command`, com fallback para Xcode (`.xcodeproj`), Makefile, Justfile, Cargo, Go, Python ou npm. Projetos Xcode/iOS rodam `xcodebuild build` e `xcodebuild test` automaticamente. Funciona em modo paralelo e serial.
|
||||
- **Modelo de review por runtime** — `review.models.<cli>` permite que cada CLI externa de review (codex, gemini, etc.) escolha seu próprio modelo, independente do perfil de planner/executor.
|
||||
- **Herança de configuração de workstream** — quando `GSD_WORKSTREAM` está definido, o `.planning/config.json` raiz é carregado primeiro e merge-deep com o config da workstream (workstream vence em conflito). Um `null` explícito no config da workstream sobrescreve corretamente o valor raiz.
|
||||
@@ -259,7 +259,7 @@ Validação manual orientada para confirmar que a feature realmente funciona com
|
||||
Ou deixe o GSD decidir:
|
||||
|
||||
```
|
||||
/gsd-next
|
||||
/gsd-progress --next
|
||||
```
|
||||
|
||||
### Modo rápido
|
||||
@@ -327,7 +327,7 @@ Cada tarefa gera commit próprio, facilitando `git bisect`, rollback e rastreabi
|
||||
| `/gsd-execute-phase <N>` | Executa planos em ondas paralelas |
|
||||
| `/gsd-verify-work [N]` | UAT manual |
|
||||
| `/gsd-ship [N] [--draft]` | Cria PR da fase validada |
|
||||
| `/gsd-next` | Avança automaticamente para o próximo passo |
|
||||
| `/gsd-progress --next` | Avança automaticamente para o próximo passo |
|
||||
| `/gsd-fast <text>` | Tarefas triviais sem planejamento |
|
||||
| `/gsd-complete-milestone` | Fecha o marco e marca release |
|
||||
| `/gsd-new-milestone [name]` | Inicia próximo marco |
|
||||
@@ -339,7 +339,7 @@ Cada tarefa gera commit próprio, facilitando `git bisect`, rollback e rastreabi
|
||||
| `/gsd-review` | Peer review com múltiplas IAs |
|
||||
| `/gsd-pr-branch` | Cria branch limpa para PR |
|
||||
| `/gsd-settings` | Configura perfis e agentes |
|
||||
| `/gsd-set-profile <profile>` | Troca perfil (quality/balanced/budget/inherit) |
|
||||
| `/gsd-config --profile <profile>` | Troca perfil (quality/balanced/budget/inherit) |
|
||||
| `/gsd-quick [--full] [--discuss] [--research]` | Execução rápida com garantias do GSD (`--full` ativa todas as etapas, `--validate` ativa apenas verificação) |
|
||||
| `/gsd-health [--repair]` | Verifica e repara `.planning/` |
|
||||
|
||||
@@ -370,7 +370,7 @@ Você pode configurar no `/gsd-new-project` ou ajustar depois com `/gsd-settings
|
||||
|
||||
Troca rápida:
|
||||
```
|
||||
/gsd-set-profile budget
|
||||
/gsd-config --profile budget
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -78,7 +78,7 @@ GSD 解决的就是这个问题。它是让 Claude Code 变得可靠的上下文
|
||||
完整列表请参阅 [v1.39.0 发行说明](https://github.com/gsd-build/get-shit-done/releases/tag/v1.39.0)。
|
||||
|
||||
- **`--minimal` 安装档** — 别名 `--core-only`。仅安装主循环的 6 个核心技能(`new-project`、`discuss-phase`、`plan-phase`、`execute-phase`、`help`、`update`),不安装任何 `gsd-*` 子代理。将冷启动系统提示开销从 ~12k token 降至 ~700 token(≥94% 减少)。适合 32K–128K 上下文的本地 LLM 和按 token 计费的 API。
|
||||
- **`/gsd-edit-phase`** — 就地修改 `ROADMAP.md` 中已有阶段的任意字段,不改变其编号或位置。`--force` 跳过确认 diff,验证 `depends_on` 引用,并在写入时更新 `STATE.md`。
|
||||
- **`/gsd-phase --edit`** — 就地修改 `ROADMAP.md` 中已有阶段的任意字段,不改变其编号或位置。`--force` 跳过确认 diff,验证 `depends_on` 引用,并在写入时更新 `STATE.md`。
|
||||
- **合并后构建与测试门** — `execute-phase` 步骤 5.6 优先自动检测 `workflow.build_command` 配置,否则按 Xcode(`.xcodeproj`)、Makefile、Justfile、Cargo、Go、Python、npm 顺序回退。Xcode/iOS 项目自动运行 `xcodebuild build` 和 `xcodebuild test`。在并行与串行模式下均生效。
|
||||
- **每运行时评审模型选择** — `review.models.<cli>` 让每个外部评审 CLI(codex、gemini 等)独立于规划/执行档选择自己的模型。
|
||||
- **工作流设置继承** — 设置 `GSD_WORKSTREAM` 后,先加载根 `.planning/config.json`,再与该工作流的配置进行深合并(冲突时工作流优先)。工作流配置中显式 `null` 会覆盖根值。
|
||||
@@ -396,7 +396,7 @@ claude --dangerously-skip-permissions
|
||||
或者让 GSD 自动判断下一步:
|
||||
|
||||
```
|
||||
/gsd-next # 自动检测并执行下一步
|
||||
/gsd-progress --next # 自动检测并执行下一步
|
||||
```
|
||||
|
||||
循环执行 **讨论 → 规划 → 执行 → 验证 → 发布**,直到整个里程碑完成。
|
||||
@@ -538,7 +538,7 @@ lmn012o feat(08-02): create registration endpoint
|
||||
| `/gsd-verify-work [N]` | 人工用户验收测试 ¹ |
|
||||
| `/gsd-ship [N] [--draft]` | 从已验证的阶段工作创建 PR,自动生成 PR 描述 |
|
||||
| `/gsd-fast <text>` | 内联处理琐碎任务——完全跳过规划,立即执行 |
|
||||
| `/gsd-next` | 自动推进到下一个逻辑工作流步骤 |
|
||||
| `/gsd-progress --next` | 自动推进到下一个逻辑工作流步骤 |
|
||||
| `/gsd-audit-milestone` | 验证里程碑是否达到完成定义 |
|
||||
| `/gsd-complete-milestone` | 归档里程碑并打 release tag |
|
||||
| `/gsd-new-milestone [name]` | 开始下一个版本:提问 → 研究 → 需求 → 路线图 |
|
||||
@@ -558,9 +558,9 @@ lmn012o feat(08-02): create registration endpoint
|
||||
|
||||
| 命令 | 作用 |
|
||||
|------|------|
|
||||
| `/gsd-new-workspace` | 创建隔离工作区,包含仓库副本(worktree 或 clone) |
|
||||
| `/gsd-list-workspaces` | 显示所有 GSD 工作区及其状态 |
|
||||
| `/gsd-remove-workspace` | 移除工作区并清理 worktree |
|
||||
| `/gsd-workspace --new` | 创建隔离工作区,包含仓库副本(worktree 或 clone) |
|
||||
| `/gsd-workspace --list` | 显示所有 GSD 工作区及其状态 |
|
||||
| `/gsd-workspace --remove` | 移除工作区并清理 worktree |
|
||||
|
||||
### UI 设计
|
||||
|
||||
@@ -574,10 +574,9 @@ lmn012o feat(08-02): create registration endpoint
|
||||
| 命令 | 作用 |
|
||||
|------|------|
|
||||
| `/gsd-progress` | 我现在在哪?下一步是什么? |
|
||||
| `/gsd-next` | 自动检测状态并执行下一步 |
|
||||
| `/gsd-progress --next` | 自动检测状态并执行下一步 |
|
||||
| `/gsd-help` | 显示全部命令和使用指南 |
|
||||
| `/gsd-update` | 更新 GSD,并预览变更日志 |
|
||||
| `/gsd-join-discord` | 加入 GSD Discord 社区 |
|
||||
|
||||
### Brownfield
|
||||
|
||||
@@ -589,12 +588,12 @@ lmn012o feat(08-02): create registration endpoint
|
||||
|
||||
| 命令 | 作用 |
|
||||
|------|------|
|
||||
| `/gsd-add-phase` | 在路线图末尾追加 phase |
|
||||
| `/gsd-insert-phase [N]` | 在 phase 之间插入紧急工作 |
|
||||
| `/gsd-edit-phase [N] [--force]` | 就地修改已有 phase 的任意字段 — 编号与位置保持不变 |
|
||||
| `/gsd-remove-phase [N]` | 删除未来 phase,并重编号 |
|
||||
| `/gsd-list-phase-assumptions [N]` | 在规划前查看 Claude 打算采用的方案 |
|
||||
| `/gsd-plan-milestone-gaps` | 为 audit 发现的缺口创建 phase |
|
||||
| `/gsd-phase` | 在路线图末尾追加 phase |
|
||||
| `/gsd-phase --insert [N]` | 在 phase 之间插入紧急工作 |
|
||||
| `/gsd-phase --edit [N] [--force]` | 就地修改已有 phase 的任意字段 — 编号与位置保持不变 |
|
||||
| `/gsd-phase --remove [N]` | 删除未来 phase,并重编号 |
|
||||
| `/gsd-discuss-phase --assumptions [N]` | 在规划前查看 Claude 打算采用的方案 |
|
||||
| `/gsd-audit-milestone --fix` | 为 audit 发现的缺口创建 phase |
|
||||
|
||||
### 代码质量
|
||||
|
||||
@@ -608,7 +607,7 @@ lmn012o feat(08-02): create registration endpoint
|
||||
|
||||
| 命令 | 作用 |
|
||||
|------|------|
|
||||
| `/gsd-plant-seed <idea>` | 将想法存入积压停车场,留待未来里程碑 |
|
||||
| `/gsd-capture --seed <idea>` | 将想法存入积压停车场,留待未来里程碑 |
|
||||
|
||||
### 会话
|
||||
|
||||
@@ -616,16 +615,16 @@ lmn012o feat(08-02): create registration endpoint
|
||||
|------|------|
|
||||
| `/gsd-pause-work` | 在中途暂停时创建交接上下文(写入 HANDOFF.json) |
|
||||
| `/gsd-resume-work` | 从上一次会话恢复 |
|
||||
| `/gsd-session-report` | 生成会话摘要,包含已完成工作和结果 |
|
||||
| `/gsd-pause-work --report` | 生成会话摘要,包含已完成工作和结果 |
|
||||
|
||||
### 工具
|
||||
|
||||
| 命令 | 作用 |
|
||||
|------|------|
|
||||
| `/gsd-settings` | 配置模型 profile 和工作流代理 |
|
||||
| `/gsd-set-profile <profile>` | 切换模型 profile(quality / balanced / budget / inherit) |
|
||||
| `/gsd-add-todo [desc]` | 记录一个待办想法 |
|
||||
| `/gsd-check-todos` | 查看待办列表 |
|
||||
| `/gsd-config --profile <profile>` | 切换模型 profile(quality / balanced / budget / inherit) |
|
||||
| `/gsd-capture [desc]` | 记录一个待办想法 |
|
||||
| `/gsd-capture --list` | 查看待办列表 |
|
||||
| `/gsd-debug [desc]` | 使用持久状态进行系统化调试 |
|
||||
| `/gsd-do <text>` | 将自由文本自动路由到正确的 GSD 命令 |
|
||||
| `/gsd-note <text>` | 零摩擦想法捕捉——追加、列出或提升为待办 |
|
||||
@@ -662,7 +661,7 @@ GSD 将项目设置保存在 `.planning/config.json`。你可以在 `/gsd-new-pr
|
||||
|
||||
切换方式:
|
||||
```
|
||||
/gsd-set-profile budget
|
||||
/gsd-config --profile budget
|
||||
```
|
||||
|
||||
使用非 Anthropic 提供商(OpenRouter、本地模型)时,或想跟随当前运行时的模型选择时(如 OpenCode 的 `/model`),可用 `inherit`。
|
||||
|
||||
@@ -74,7 +74,7 @@ Extract from init JSON: `executor_model`, `commit_docs`, `sub_repos`, `phase_dir
|
||||
|
||||
Also load planning state (position, decisions, blockers) via the SDK — **use `node` to invoke the CLI** (not `npx`):
|
||||
```bash
|
||||
node ./node_modules/@gsd-build/sdk/dist/cli.js query state.load 2>/dev/null
|
||||
gsd-sdk query state.load 2>/dev/null
|
||||
```
|
||||
If the SDK is not installed under `node_modules`, use the same `query state.load` argv with your local `gsd-sdk` CLI on `PATH`.
|
||||
|
||||
|
||||
@@ -655,11 +655,11 @@ Extract from init JSON: `phase_dir`, `phase_number`, `has_plans`, `plan_count`.
|
||||
Orchestrator provides CONTEXT.md content in the verification prompt. If provided, parse for locked decisions, discretion areas, deferred ideas.
|
||||
|
||||
```bash
|
||||
node ./node_modules/@gsd-build/sdk/dist/cli.js query phase.list-plans "$phase_number"
|
||||
gsd-sdk query phase.list-plans "$phase_number"
|
||||
# Research / brief artifacts (deterministic listing)
|
||||
node ./node_modules/@gsd-build/sdk/dist/cli.js query phase.list-artifacts "$phase_number" --type research
|
||||
node ./node_modules/@gsd-build/sdk/dist/cli.js query roadmap.get-phase "$phase_number"
|
||||
node ./node_modules/@gsd-build/sdk/dist/cli.js query phase.list-artifacts "$phase_number" --type summary
|
||||
gsd-sdk query phase.list-artifacts "$phase_number" --type research
|
||||
gsd-sdk query roadmap.get-phase "$phase_number"
|
||||
gsd-sdk query phase.list-artifacts "$phase_number" --type summary
|
||||
```
|
||||
|
||||
**Extract:** Phase goal, requirements (decompose goal), locked decisions, deferred ideas.
|
||||
@@ -747,7 +747,7 @@ The `tasks` array in the result shows each task's completeness:
|
||||
|
||||
**For manual validation of specificity** (`verify.plan-structure` checks structure, not content quality), use structured extraction instead of grepping raw XML:
|
||||
```bash
|
||||
node ./node_modules/@gsd-build/sdk/dist/cli.js query plan.task-structure "$PLAN_PATH"
|
||||
gsd-sdk query plan.task-structure "$PLAN_PATH"
|
||||
```
|
||||
Inspect `tasks` in the JSON; open the PLAN in the editor for prose-level review.
|
||||
|
||||
@@ -774,8 +774,8 @@ Missing: No mention of fetch/API call → Issue: Key link not planned
|
||||
## Step 8: Assess Scope
|
||||
|
||||
```bash
|
||||
node ./node_modules/@gsd-build/sdk/dist/cli.js query plan.task-structure "$PHASE_DIR/$PHASE-01-PLAN.md"
|
||||
node ./node_modules/@gsd-build/sdk/dist/cli.js query frontmatter.get "$PHASE_DIR/$PHASE-01-PLAN.md" files_modified
|
||||
gsd-sdk query plan.task-structure "$PHASE_DIR/$PHASE-01-PLAN.md"
|
||||
gsd-sdk query frontmatter.get "$PHASE_DIR/$PHASE-01-PLAN.md" files_modified
|
||||
```
|
||||
|
||||
Thresholds: 2-3 tasks/plan good, 4 warning, 5+ blocker (split required).
|
||||
|
||||
@@ -814,7 +814,7 @@ Extract from init JSON: `planner_model`, `researcher_model`, `checker_model`, `c
|
||||
|
||||
Also load planning state (position, decisions, blockers) via the SDK — **use `node` to invoke the CLI** (not `npx`):
|
||||
```bash
|
||||
node ./node_modules/@gsd-build/sdk/dist/cli.js query state.load 2>/dev/null
|
||||
gsd-sdk query state.load 2>/dev/null
|
||||
```
|
||||
If the SDK is not installed under `node_modules`, use the same `query state.load` argv with your local `gsd-sdk` CLI on `PATH`.
|
||||
|
||||
|
||||
@@ -560,7 +560,7 @@ When files are written and returning to orchestrator:
|
||||
|
||||
### Files Ready for Review
|
||||
|
||||
User can review actual files in the editor or via SDK queries (e.g. `node ./node_modules/@gsd-build/sdk/dist/cli.js query roadmap.analyze` and `query state.load`) instead of ad-hoc shell `cat`.
|
||||
User can review actual files in the editor or via SDK queries (e.g. `gsd-sdk query roadmap.analyze` and `gsd-sdk query state.load`) instead of ad-hoc shell `cat`.
|
||||
|
||||
{If gaps found during creation:}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd:discuss-phase
|
||||
description: Gather phase context through adaptive questioning before planning.
|
||||
argument-hint: "<phase> [--all] [--auto] [--chain] [--batch] [--analyze] [--text] [--power]"
|
||||
argument-hint: "<phase> [--all] [--auto] [--chain] [--batch] [--analyze] [--text] [--power] [--assumptions]"
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Write
|
||||
@@ -49,10 +49,14 @@ Context files are resolved in-workflow using `init phase-op` and roadmap/state t
|
||||
DISCUSS_MODE=$(gsd-sdk query config-get workflow.discuss_mode 2>/dev/null || echo "discuss")
|
||||
```
|
||||
|
||||
If `DISCUSS_MODE` is `"assumptions"`:
|
||||
If `--assumptions` is in $ARGUMENTS:
|
||||
Read and execute `~/.claude/get-shit-done/workflows/list-phase-assumptions.md` end-to-end.
|
||||
Stop here.
|
||||
|
||||
Otherwise, if `DISCUSS_MODE` is `"assumptions"`:
|
||||
Read and execute `~/.claude/get-shit-done/workflows/discuss-phase-assumptions.md` end-to-end.
|
||||
|
||||
If `DISCUSS_MODE` is `"discuss"` (or unset, or any other value):
|
||||
Otherwise (`"discuss"` / unset / any other value):
|
||||
Read and execute `~/.claude/get-shit-done/workflows/discuss-phase.md` end-to-end.
|
||||
|
||||
**MANDATORY:** Read the appropriate workflow file BEFORE taking any action. The objective and success_criteria sections in this command file are summaries — the workflow file contains the complete step-by-step process with all required behaviors, config checks, and interaction patterns. Do not improvise from the summary.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd:import
|
||||
description: Ingest external plans with conflict detection against project decisions before writing anything.
|
||||
argument-hint: "--from <filepath>"
|
||||
argument-hint: "--from <filepath> | --from-gsd2"
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Write
|
||||
@@ -17,8 +17,7 @@ allowed-tools:
|
||||
Import external plan files into the GSD planning system with conflict detection against PROJECT.md decisions.
|
||||
|
||||
- **--from**: Import an external plan file, detect conflicts, write as GSD PLAN.md, validate via gsd-plan-checker.
|
||||
|
||||
Future: `--prd` mode for PRD extraction is planned for a follow-up PR.
|
||||
- **--from-gsd2**: Reverse-migrate a GSD-2 project (`.gsd/` directory) back to GSD v1 (`.planning/`) format. Runs `gsd-tools.cjs from-gsd2`. Pass `--path <dir>` to migrate a project at a different path.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@@ -33,5 +32,10 @@ $ARGUMENTS
|
||||
</context>
|
||||
|
||||
<process>
|
||||
Execute the import workflow end-to-end.
|
||||
If `--from-gsd2` is in $ARGUMENTS:
|
||||
Run: `node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" from-gsd2`
|
||||
Pass `--path <dir>` if provided. Present the migration result to the user.
|
||||
Stop here (do not run the standard import workflow).
|
||||
|
||||
Otherwise, execute the import workflow end-to-end.
|
||||
</process>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: gsd:manager
|
||||
description: Interactive command center for managing multiple phases from one terminal
|
||||
argument-hint: "[--analyze-deps]"
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Write
|
||||
@@ -35,6 +36,9 @@ Project context, phase list, dependencies, and recommendations are resolved insi
|
||||
</context>
|
||||
|
||||
<process>
|
||||
If `--analyze-deps` is in $ARGUMENTS:
|
||||
Read and execute `~/.claude/get-shit-done/workflows/analyze-dependencies.md` end-to-end.
|
||||
|
||||
Execute the manager workflow from @~/.claude/get-shit-done/workflows/manager.md end-to-end.
|
||||
Maintain the dashboard refresh loop until the user exits or all phases complete.
|
||||
</process>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: gsd:pause-work
|
||||
description: Create context handoff when pausing work mid-phase
|
||||
argument-hint: "[--report]"
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Write
|
||||
@@ -27,6 +28,9 @@ State and phase progress are gathered in-workflow with targeted reads.
|
||||
</context>
|
||||
|
||||
<process>
|
||||
If `--report` is in $ARGUMENTS:
|
||||
Read and execute `~/.claude/get-shit-done/workflows/session-report.md` end-to-end.
|
||||
|
||||
**Follow the pause-work workflow** from `@~/.claude/get-shit-done/workflows/pause-work.md`.
|
||||
|
||||
The workflow handles all logic including:
|
||||
|
||||
@@ -480,7 +480,7 @@ Communication style, decision patterns, debugging approach, UX preferences, vend
|
||||
|
||||
## Advanced and Specialized Agents
|
||||
|
||||
Twelve additional agents ship under `agents/gsd-*.md` and are used by specialty workflows (`/gsd-ai-integration-phase`, `/gsd-eval-review`, `/gsd-code-review`, `/gsd-code-review --fix`, `/gsd-debug`, `/gsd-intel`, `/gsd-select-framework`, `/gsd-ingest-docs`) and by the planner pipeline. Each carries full frontmatter in its agent file; the stubs below are concise by design. The authoritative roster (with spawner and primary-doc status per agent) lives in [`docs/INVENTORY.md`](INVENTORY.md).
|
||||
Twelve additional agents ship under `agents/gsd-*.md` and are used by specialty workflows (`/gsd-ai-integration-phase`, `/gsd-eval-review`, `/gsd-code-review`, `/gsd-code-review --fix`, `/gsd-debug`, `/gsd-map-codebase --query`, `/gsd-select-framework`, `/gsd-ingest-docs`) and by the planner pipeline. Each carries full frontmatter in its agent file; the stubs below are concise by design. The authoritative roster (with spawner and primary-doc status per agent) lives in [`docs/INVENTORY.md`](INVENTORY.md).
|
||||
|
||||
### gsd-pattern-mapper
|
||||
|
||||
@@ -669,7 +669,7 @@ Twelve additional agents ship under `agents/gsd-*.md` and are used by specialty
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Spawned by** | `/gsd-intel` (refresh / update flows) |
|
||||
| **Spawned by** | `/gsd-map-codebase --query` (refresh / update flows) |
|
||||
| **Parallelism** | Single instance |
|
||||
| **Tools** | Read, Write, Bash, Glob, Grep |
|
||||
| **Model (balanced)** | Sonnet |
|
||||
|
||||
@@ -535,7 +535,7 @@ Equivalent paths for other runtimes:
|
||||
│ ├── pending/ # Captured ideas
|
||||
│ └── done/ # Completed todos
|
||||
├── threads/ # Persistent context threads (from /gsd-thread)
|
||||
├── seeds/ # Forward-looking ideas (from /gsd-plant-seed)
|
||||
├── seeds/ # Forward-looking ideas (from /gsd-capture --seed)
|
||||
├── debug/ # Active debug sessions
|
||||
│ ├── *.md # Active sessions
|
||||
│ ├── resolved/ # Archived sessions
|
||||
|
||||
@@ -479,7 +479,7 @@ User-facing entry point: `/gsd-graphify` (see [Command Reference](COMMANDS.md#gs
|
||||
| Learnings | `lib/learnings.cjs` | Extract learnings from phases/SUMMARY artifacts (backs `/gsd-extract-learnings`) |
|
||||
| Audit | `lib/audit.cjs` | Phase/milestone audit queue handlers; `audit-open` helper |
|
||||
| GSD2 Import | `lib/gsd2-import.cjs` | Reverse-migration importer from GSD-2 projects (backs `/gsd-from-gsd2`) |
|
||||
| Intel | `lib/intel.cjs` | Queryable codebase intelligence index (backs `/gsd-intel`) |
|
||||
| Intel | `lib/intel.cjs` | Queryable codebase intelligence index (backs `/gsd-map-codebase --query`) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -97,6 +97,7 @@ Gather phase context through adaptive questioning before planning.
|
||||
| `--batch` | Group questions for batch intake instead of one-by-one |
|
||||
| `--analyze` | Add trade-off analysis during discussion |
|
||||
| `--power` | File-based bulk question answering from a prepared answers file |
|
||||
| `--assumptions` | Surface Claude's implementation assumptions about the phase without an interactive session |
|
||||
|
||||
**Prerequisites:** `.planning/ROADMAP.md` exists
|
||||
**Produces:** `{phase}-CONTEXT.md`, `{phase}-DISCUSSION-LOG.md` (audit trail)
|
||||
@@ -108,6 +109,7 @@ Gather phase context through adaptive questioning before planning.
|
||||
/gsd-discuss-phase --batch # Batch mode for current phase
|
||||
/gsd-discuss-phase 2 --analyze # Discussion with trade-off analysis
|
||||
/gsd-discuss-phase 1 --power # Bulk answers from file
|
||||
/gsd-discuss-phase 3 --assumptions # Surface Claude's assumptions before planning
|
||||
```
|
||||
|
||||
---
|
||||
@@ -435,7 +437,7 @@ Show status, next steps, and automatically advance to the next logical workflow
|
||||
| `--do "task description"` | Analyze freeform intent and dispatch to the most appropriate GSD command |
|
||||
| `--forensic` | Append a 6-check integrity audit after the standard report (STATE consistency, orphaned handoffs, deferred scope drift, memory-flagged pending work, blocking todos, uncommitted code) |
|
||||
|
||||
**Auto-routing behavior (absorbed from `/gsd-next`):**
|
||||
**Auto-routing behavior (`--next`):**
|
||||
- No project → suggests `/gsd-new-project`
|
||||
- Phase needs discussion → runs `/gsd-discuss-phase`
|
||||
- Phase needs planning → runs `/gsd-plan-phase`
|
||||
@@ -462,8 +464,13 @@ Restore full context from last session.
|
||||
|
||||
Save context handoff when stopping mid-phase.
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--report` | Generate a post-session summary in `.planning/reports/` capturing commits, file changes, and phase progress |
|
||||
|
||||
```bash
|
||||
/gsd-pause-work # Creates continue-here.md
|
||||
/gsd-pause-work --report # Creates continue-here.md + session report
|
||||
```
|
||||
|
||||
### `/gsd-manager`
|
||||
@@ -480,6 +487,7 @@ Interactive command center for managing multiple phases from one terminal.
|
||||
|
||||
```bash
|
||||
/gsd-manager # Open command center dashboard
|
||||
/gsd-manager --analyze-deps # Scan ROADMAP phases for dependency relationships before parallel execution
|
||||
```
|
||||
|
||||
**Checkpoint Heartbeats (#2410):**
|
||||
@@ -570,13 +578,17 @@ Safe git revert — roll back GSD phase or plan commits using the phase manifest
|
||||
Ingest an external plan file into the GSD planning system with conflict detection against `PROJECT.md` decisions before writing anything.
|
||||
|
||||
| Flag | Required | Description |
|
||||
|------|----------|-------------|
|
||||
| `--from <filepath>` | **Yes** | Path to the external plan file to import |
|
||||
|------|----------|--------------|
|
||||
| `--from <filepath>` | Yes (or `--from-gsd2`) | Path to the external plan file to import |
|
||||
| `--from-gsd2` | Yes (or `--from`) | Reverse-migrate a GSD-2 (`.gsd/`) project back to GSD v1 (`.planning/`) format |
|
||||
| `--path <dir>` | No | With `--from-gsd2`: path to the GSD-2 project directory (defaults to current directory) |
|
||||
|
||||
**Process:** Detects conflicts → prompts for resolution → writes as GSD PLAN.md → validates via `gsd-plan-checker`
|
||||
|
||||
```bash
|
||||
/gsd-import --from /tmp/team-plan.md # Import and validate an external plan
|
||||
/gsd-import --from /tmp/team-plan.md # Import and validate an external plan
|
||||
/gsd-import --from-gsd2 # Migrate from GSD-2 back to v1 (current dir)
|
||||
/gsd-import --from-gsd2 --path ~/old-project # Migrate from a different path
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -352,7 +352,7 @@ Toggle optional capabilities via the `features.*` config namespace. Feature flag
|
||||
| `features.thinking_partner` | boolean | `false` | Enable thinking partner analysis at workflow decision points |
|
||||
| `features.global_learnings` | boolean | `false` | Enable cross-project learnings pipeline (auto-copy at phase completion, planner injection) |
|
||||
| `learnings.max_inject` | number | `10` | Maximum number of cross-project learnings injected into each planner prompt. Lower values reduce prompt size; higher values provide broader historical context |
|
||||
| `intel.enabled` | boolean | `false` | Enable queryable codebase intelligence system. When `true`, `/gsd-intel` commands build and query a JSON index in `.planning/intel/`. Added in v1.34 |
|
||||
| `intel.enabled` | boolean | `false` | Enable queryable codebase intelligence system. When `true`, `/gsd-map-codebase --query` commands build and query a JSON index in `.planning/intel/`. Added in v1.34 |
|
||||
|
||||
<a id="graphify-settings"></a>
|
||||
### Graphify Settings
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
- [Autonomous Audit-to-Fix](#98-autonomous-audit-to-fix)
|
||||
- [Improved Prompt Injection Scanner](#99-improved-prompt-injection-scanner)
|
||||
- [Stall Detection in Plan-Phase](#100-stall-detection-in-plan-phase)
|
||||
- [Hard Stop Safety Gates in /gsd-next](#101-hard-stop-safety-gates-in-gsd-next)
|
||||
- [Hard Stop Safety Gates in /gsd-progress --next](#101-hard-stop-safety-gates-in-gsd-progress---next)
|
||||
- [Adaptive Model Preset](#102-adaptive-model-preset)
|
||||
- [Post-Merge Hunk Verification](#103-post-merge-hunk-verification)
|
||||
- [v1.35.0 Features](#v1350-features)
|
||||
@@ -461,7 +461,7 @@
|
||||
|
||||
### 9. Phase Management
|
||||
|
||||
**Commands:** `/gsd-add-phase`, `/gsd-insert-phase [N]`, `/gsd-remove-phase [N]`
|
||||
**Commands:** `/gsd-phase`, `/gsd-phase --insert [N]`, `/gsd-phase --remove [N]`
|
||||
|
||||
**Purpose:** Dynamic roadmap modification during development.
|
||||
|
||||
@@ -539,7 +539,7 @@
|
||||
|
||||
### 14. Auto-Advance (Next)
|
||||
|
||||
**Command:** `/gsd-next`
|
||||
**Command:** `/gsd-progress --next`
|
||||
|
||||
**Purpose:** Automatically detect current project state and advance to the next logical workflow step, eliminating the need to remember which phase/step you're on.
|
||||
|
||||
@@ -709,7 +709,7 @@
|
||||
|
||||
### 24. Session Reporting
|
||||
|
||||
**Command:** `/gsd-session-report`
|
||||
**Command:** `/gsd-pause-work --report`
|
||||
|
||||
**Purpose:** Generate a structured post-session summary document capturing work performed, outcomes achieved, and estimated resource usage.
|
||||
|
||||
@@ -748,7 +748,7 @@
|
||||
|
||||
### 26. Model Profiles
|
||||
|
||||
**Command:** `/gsd-set-profile <quality|balanced|budget|inherit>`
|
||||
**Command:** `/gsd-config --profile <quality|balanced|budget|adaptive|inherit>`
|
||||
|
||||
**Purpose:** Control which AI model each agent uses, balancing quality vs cost.
|
||||
|
||||
@@ -869,7 +869,7 @@ continues. Drift detection cannot fail verification.
|
||||
|
||||
### 29. Todo Management
|
||||
|
||||
**Commands:** `/gsd-add-todo [desc]`, `/gsd-capture --list`
|
||||
**Commands:** `/gsd-capture [desc]`, `/gsd-capture --list`
|
||||
|
||||
**Purpose:** Capture ideas and tasks during sessions for later work.
|
||||
|
||||
@@ -1185,7 +1185,7 @@ When verification returns `human_needed`, items are persisted as a trackable HUM
|
||||
|
||||
### 43. Backlog Parking Lot
|
||||
|
||||
**Commands:** `/gsd-add-backlog <description>`, `/gsd-review-backlog`, `/gsd-plant-seed <idea>`
|
||||
**Commands:** `/gsd-capture --backlog <description>`, `/gsd-review-backlog`, `/gsd-capture --seed <idea>`
|
||||
|
||||
**Purpose:** Capture ideas that aren't ready for active planning. Backlog items use 999.x numbering to stay outside the active phase sequence. Seeds are forward-looking ideas with trigger conditions that surface automatically at the right milestone.
|
||||
|
||||
@@ -1845,7 +1845,7 @@ Test suite that scans all agent, workflow, and command files for embedded inject
|
||||
|
||||
### 77. Phase Dependency Analysis
|
||||
|
||||
**Command:** `/gsd-analyze-dependencies`
|
||||
**Command:** `/gsd-manager --analyze-deps`
|
||||
|
||||
**Purpose:** Detect phase dependencies and suggest `Depends on` entries for ROADMAP.md before running `/gsd-manager`.
|
||||
|
||||
@@ -2028,7 +2028,7 @@ Test suite that scans all agent, workflow, and command files for embedded inject
|
||||
- [Autonomous Audit-to-Fix](#98-autonomous-audit-to-fix)
|
||||
- [Improved Prompt Injection Scanner](#99-improved-prompt-injection-scanner)
|
||||
- [Stall Detection in Plan-Phase](#100-stall-detection-in-plan-phase)
|
||||
- [Hard Stop Safety Gates in /gsd-next](#101-hard-stop-safety-gates-in-gsd-next)
|
||||
- [Hard Stop Safety Gates in /gsd-progress --next](#101-hard-stop-safety-gates-in-gsd-progress---next)
|
||||
- [Adaptive Model Preset](#102-adaptive-model-preset)
|
||||
- [Post-Merge Hunk Verification](#103-post-merge-hunk-verification)
|
||||
|
||||
@@ -2245,14 +2245,14 @@ Test suite that scans all agent, workflow, and command files for embedded inject
|
||||
|
||||
---
|
||||
|
||||
### 101. Hard Stop Safety Gates in /gsd-next
|
||||
### 101. Hard Stop Safety Gates in /gsd-progress --next
|
||||
|
||||
**Command:** `/gsd-next`
|
||||
**Command:** `/gsd-progress --next`
|
||||
|
||||
**Purpose:** Prevent `/gsd-next` from entering runaway loops by adding hard stop safety gates and a consecutive-call guard that interrupts autonomous chaining when repeated identical steps are detected.
|
||||
**Purpose:** Prevent `/gsd-progress --next` from entering runaway loops by adding hard stop safety gates and a consecutive-call guard that interrupts autonomous chaining when repeated identical steps are detected.
|
||||
|
||||
**Requirements:**
|
||||
- REQ-NEXT-GATE-01: `/gsd-next` MUST track consecutive same-step calls
|
||||
- REQ-NEXT-GATE-01: `/gsd-progress --next` MUST track consecutive same-step calls
|
||||
- REQ-NEXT-GATE-02: On repeated same-step, system MUST present a hard stop gate to the user
|
||||
- REQ-NEXT-GATE-03: User MUST explicitly confirm to continue past a hard stop gate
|
||||
|
||||
@@ -2266,7 +2266,7 @@ Test suite that scans all agent, workflow, and command files for embedded inject
|
||||
|
||||
**Requirements:**
|
||||
- REQ-ADAPTIVE-01: `adaptive` preset MUST assign model tiers based on agent role (planner → quality tier, executor → balanced tier, etc.)
|
||||
- REQ-ADAPTIVE-02: `adaptive` MUST be selectable via `/gsd-set-profile adaptive`
|
||||
- REQ-ADAPTIVE-02: `adaptive` MUST be selectable via `/gsd-config --profile adaptive`
|
||||
|
||||
---
|
||||
|
||||
@@ -2531,7 +2531,7 @@ Users who run a memory / knowledge-base MCP server (for example, ExoCortex-style
|
||||
|
||||
**Command:** `/gsd-spike [idea] [--quick]`
|
||||
|
||||
**Purpose:** Run 2–5 focused feasibility experiments before committing to an implementation approach. Each experiment uses Given/When/Then framing, produces executable code, and returns a VALIDATED / INVALIDATED / PARTIAL verdict. Companion `/gsd-spike-wrap-up` packages findings into a project-local skill.
|
||||
**Purpose:** Run 2–5 focused feasibility experiments before committing to an implementation approach. Each experiment uses Given/When/Then framing, produces executable code, and returns a VALIDATED / INVALIDATED / PARTIAL verdict. Companion `/gsd-spike --wrap-up` packages findings into a project-local skill.
|
||||
|
||||
**Requirements:**
|
||||
- REQ-SPIKE-01: Each experiment MUST produce a Given/When/Then hypothesis before any code is written
|
||||
@@ -2539,14 +2539,15 @@ Users who run a memory / knowledge-base MCP server (for example, ExoCortex-style
|
||||
- REQ-SPIKE-03: Each experiment MUST return one of: VALIDATED, INVALIDATED, or PARTIAL verdict with evidence
|
||||
- REQ-SPIKE-04: Results MUST be stored in `.planning/spikes/NNN-experiment-name/` with a README and MANIFEST.md
|
||||
- REQ-SPIKE-05: `--quick` flag skips intake conversation and uses the argument text as the experiment direction
|
||||
- REQ-SPIKE-06: `/gsd-spike-wrap-up` MUST package findings into `.claude/skills/spike-findings-[project]/`
|
||||
- REQ-SPIKE-06: `/gsd-spike --wrap-up` MUST package findings into `.claude/skills/spike-findings-[project]/`
|
||||
|
||||
**Produces:**
|
||||
|
||||
| Artifact | Description |
|
||||
|----------|-------------|
|
||||
| `.planning/spikes/NNN-name/README.md` | Hypothesis, experiment code, verdict, and evidence |
|
||||
| `.planning/spikes/MANIFEST.md` | Index of all spikes with verdicts |
|
||||
| `.claude/skills/spike-findings-[project]/` | Packaged findings (via `/gsd-spike-wrap-up`) |
|
||||
| `.claude/skills/spike-findings-[project]/` | Packaged findings (via `/gsd-spike --wrap-up`) |
|
||||
|
||||
---
|
||||
|
||||
@@ -2554,7 +2555,7 @@ Users who run a memory / knowledge-base MCP server (for example, ExoCortex-style
|
||||
|
||||
**Command:** `/gsd-sketch [idea] [--quick] [--text]`
|
||||
|
||||
**Purpose:** Explore design directions through throwaway HTML mockups before committing to implementation. Produces 2–3 interactive variants per design question, all viewable directly in a browser with no build step. Companion `/gsd-sketch-wrap-up` packages winning decisions into a project-local skill.
|
||||
**Purpose:** Explore design directions through throwaway HTML mockups before committing to implementation. Produces 2–3 interactive variants per design question, all viewable directly in a browser with no build step. Companion `/gsd-sketch --wrap-up` packages winning decisions into a project-local skill.
|
||||
|
||||
**Requirements:**
|
||||
- REQ-SKETCH-01: Each sketch MUST answer one specific visual design question
|
||||
@@ -2564,7 +2565,7 @@ Users who run a memory / knowledge-base MCP server (for example, ExoCortex-style
|
||||
- REQ-SKETCH-05: A shared `themes/default.css` MUST provide CSS variables adapted to the agreed aesthetic
|
||||
- REQ-SKETCH-06: `--quick` flag skips mood intake; `--text` flag replaces `AskUserQuestion` with numbered lists for non-Claude runtimes
|
||||
- REQ-SKETCH-07: The winning variant MUST be marked in the README frontmatter and with a ★ in the HTML tab
|
||||
- REQ-SKETCH-08: `/gsd-sketch-wrap-up` MUST package winning decisions into `.claude/skills/sketch-findings-[project]/`
|
||||
- REQ-SKETCH-08: `/gsd-sketch --wrap-up` MUST package winning decisions into `.claude/skills/sketch-findings-[project]/`
|
||||
|
||||
**Produces:**
|
||||
| Artifact | Description |
|
||||
@@ -2573,7 +2574,7 @@ Users who run a memory / knowledge-base MCP server (for example, ExoCortex-style
|
||||
| `.planning/sketches/NNN-name/README.md` | Design question, variants, winner, what to look for |
|
||||
| `.planning/sketches/themes/default.css` | Shared CSS theme variables |
|
||||
| `.planning/sketches/MANIFEST.md` | Index of all sketches with winners |
|
||||
| `.claude/skills/sketch-findings-[project]/` | Packaged decisions (via `/gsd-sketch-wrap-up`) |
|
||||
| `.claude/skills/sketch-findings-[project]/` | Packaged decisions (via `/gsd-sketch --wrap-up`) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -172,7 +172,7 @@ Full roster at `get-shit-done/workflows/*.md`. Workflows are thin orchestrators
|
||||
| `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` |
|
||||
| `analyze-dependencies.md` | Analyze ROADMAP.md phases for file overlap and semantic dependencies; suggest `Depends on` edges. | `/gsd-analyze-dependencies` |
|
||||
| `analyze-dependencies.md` | Analyze ROADMAP.md phases for file overlap and semantic dependencies; suggest `Depends on` edges. | `/gsd-manager --analyze-deps` |
|
||||
| `audit-fix.md` | Autonomous audit-to-fix pipeline — run audit, parse, classify, fix, test, commit. | `/gsd-audit-fix` |
|
||||
| `audit-milestone.md` | Verify milestone met its definition of done by aggregating phase verifications. | `/gsd-audit-milestone` |
|
||||
| `audit-uat.md` | Cross-phase audit of UAT and verification files; produces prioritized outstanding-items list. | `/gsd-audit-uat` |
|
||||
@@ -204,7 +204,7 @@ Full roster at `get-shit-done/workflows/*.md`. Workflows are thin orchestrators
|
||||
| `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-phase --insert` |
|
||||
| `list-phase-assumptions.md` | Surface Claude's assumptions about a phase before planning. | `/gsd-list-phase-assumptions` |
|
||||
| `list-phase-assumptions.md` | Surface Claude's assumptions about a phase before planning. | `/gsd-discuss-phase --assumptions` |
|
||||
| `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` |
|
||||
@@ -230,7 +230,7 @@ Full roster at `get-shit-done/workflows/*.md`. Workflows are thin orchestrators
|
||||
| `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` |
|
||||
| `session-report.md` | Session report — token usage, work summary, outcomes. | `/gsd-pause-work --report` |
|
||||
| `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` |
|
||||
|
||||
@@ -261,7 +261,7 @@ For multi-phase projects, repeat the loop:
|
||||
Or let GSD figure out the next step automatically:
|
||||
|
||||
```
|
||||
/gsd-next
|
||||
/gsd-progress --next
|
||||
```
|
||||
|
||||
When all phases are done:
|
||||
@@ -870,11 +870,11 @@ claude --dangerously-skip-permissions
|
||||
/gsd-ship 1 # Create PR from verified work
|
||||
/gsd-ui-review 1 # Visual audit (frontend phases)
|
||||
/clear
|
||||
/gsd-next # Auto-detect and run next step
|
||||
/gsd-progress --next # Auto-detect and run next step
|
||||
...
|
||||
/gsd-audit-milestone # Check everything shipped
|
||||
/gsd-complete-milestone # Archive, tag, done
|
||||
/gsd-session-report # Generate session summary
|
||||
/gsd-pause-work --report # Generate session summary
|
||||
```
|
||||
|
||||
### New Project from Existing Document
|
||||
@@ -1020,7 +1020,7 @@ Clear your context window between major commands: `/clear` in Claude Code. GSD i
|
||||
|
||||
### Plans Seem Wrong or Misaligned
|
||||
|
||||
Run `/gsd-discuss-phase [N]` before planning. Most plan quality issues come from Claude making assumptions that `CONTEXT.md` would have prevented. You can also run `/gsd-list-phase-assumptions [N]` to see what Claude intends to do before committing to a plan.
|
||||
Run `/gsd-discuss-phase [N]` before planning. Most plan quality issues come from Claude making assumptions that `CONTEXT.md` would have prevented. You can also run `/gsd-discuss-phase --assumptions [N]` to see what Claude intends to do before committing to a plan.
|
||||
|
||||
### Discuss-Phase Uses Technical Jargon I Don't Understand
|
||||
|
||||
@@ -1393,8 +1393,8 @@ If the installer crashes with `EPERM: operation not permitted, scandir` on Windo
|
||||
| Plan doesn't match your vision | `/gsd-discuss-phase [N]` then re-plan |
|
||||
| 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` |
|
||||
| Want session summary for stakeholder | `/gsd-pause-work --report` |
|
||||
| Don't know what step is next | `/gsd-progress --next` |
|
||||
| Parallel execution build errors | Update GSD or set `parallelization.enabled: false` |
|
||||
|
||||
|
||||
@@ -1414,7 +1414,7 @@ For reference, here is what GSD creates in your project:
|
||||
MILESTONES.md # Completed milestone archive
|
||||
HANDOFF.json # Structured session handoff (from /gsd-pause-work)
|
||||
research/ # Domain research from /gsd-new-project
|
||||
reports/ # Session reports (from /gsd-session-report)
|
||||
reports/ # Session reports (from /gsd-pause-work --report)
|
||||
todos/
|
||||
pending/ # Captured ideas awaiting work
|
||||
done/ # Completed todos
|
||||
|
||||
@@ -53,7 +53,7 @@ Symphony docs, blog posts, or third-party orchestration write-ups.
|
||||
| Proof-of-work / test evidence | `/gsd-verify-work` (UAT.md persisted across `/clear`) |
|
||||
| Adversarial review | `/gsd-review` (cross-AI peer review of plans) |
|
||||
| Human merge gate | `/gsd-ship` (creates PR, optional code review, prepares merge) |
|
||||
| Follow-up capture | `/gsd-note`, `/gsd-plant-seed`, `/gsd-new-milestone`, or a manually opened tracker issue |
|
||||
| Follow-up capture | `/gsd-note`, `/gsd-capture --seed`, `/gsd-new-milestone`, or a manually opened tracker issue |
|
||||
| Concurrency control | Manager / background-agent semantics (no always-on poller) |
|
||||
|
||||
The mapping is one-way: GSD owns the safety gates (verification, human
|
||||
@@ -72,8 +72,8 @@ tracker issue end-to-end. Replace bracketed placeholders before running.
|
||||
dependencies that block execution.
|
||||
2. **Map to a GSD phase.** If the issue maps onto an existing phase in
|
||||
`ROADMAP.md`, select it. If not, run `/gsd-new-milestone` (for a new
|
||||
milestone of related issues) or open a phase via `/gsd-add-phase` /
|
||||
`/gsd-insert-phase`. Capture the tracker issue URL in the phase's
|
||||
milestone of related issues) or open a phase via `/gsd-phase` /
|
||||
`/gsd-phase --insert`. Capture the tracker issue URL in the phase's
|
||||
`CONTEXT.md` so traceability survives compaction.
|
||||
3. **Create an isolated workspace.** Run
|
||||
`/gsd-workspace --new --strategy worktree <slug>` to spin up a git
|
||||
@@ -98,7 +98,7 @@ tracker issue end-to-end. Replace bracketed placeholders before running.
|
||||
rich body assembled from the planning artifacts. Both gates require a
|
||||
human decision before anything reaches the remote.
|
||||
7. **Capture follow-up work explicitly.** Use `/gsd-note` for inline
|
||||
notes, `/gsd-plant-seed` for ideas worth a future phase, or
|
||||
notes, `/gsd-capture --seed` for ideas worth a future phase, or
|
||||
`/gsd-new-milestone` for a coherent group of follow-ups. Creating a
|
||||
tracker issue from a discovered follow-up requires explicit user
|
||||
confirmation — GSD does not post to remote trackers automatically.
|
||||
@@ -165,7 +165,7 @@ approved-enhancement could later add a *minimal* tracker bridge:
|
||||
|
||||
- Importing one GitHub or Linear issue into a GSD workspace / phase.
|
||||
- Exporting `UAT.md` evidence as a comment on the source issue.
|
||||
- Generating follow-up tracker issues from `/gsd-plant-seed` output.
|
||||
- Generating follow-up tracker issues from `/gsd-capture --seed` output.
|
||||
|
||||
Each of those would be its own enhancement proposal because each adds
|
||||
integration surface and ongoing maintenance burden. They are out of
|
||||
|
||||
@@ -198,7 +198,7 @@
|
||||
|
||||
---
|
||||
|
||||
### `/gsd-next`
|
||||
### `/gsd-progress --next`
|
||||
|
||||
次の論理的なワークフローステップに自動的に進みます。プロジェクトの状態を読み取り、適切なコマンドを実行します。
|
||||
|
||||
@@ -212,12 +212,12 @@
|
||||
- 全フェーズ完了 → `/gsd-complete-milestone` を提案
|
||||
|
||||
```bash
|
||||
/gsd-next # 次のステップを自動検出して実行
|
||||
/gsd-progress --next # 次のステップを自動検出して実行
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `/gsd-session-report`
|
||||
### `/gsd-pause-work --report`
|
||||
|
||||
作業サマリー、成果、推定リソース使用量を含むセッションレポートを生成します。
|
||||
|
||||
@@ -225,7 +225,7 @@
|
||||
**生成物:** `.planning/reports/SESSION_REPORT.md`
|
||||
|
||||
```bash
|
||||
/gsd-session-report # セッション後のサマリーを生成
|
||||
/gsd-pause-work --report # セッション後のサマリーを生成
|
||||
```
|
||||
|
||||
**レポートに含まれる内容:**
|
||||
@@ -400,7 +400,7 @@
|
||||
/gsd-phase --remove 7 # フェーズ7を削除、8→7、9→8等に番号振り直し
|
||||
```
|
||||
|
||||
### `/gsd-list-phase-assumptions`
|
||||
### `/gsd-discuss-phase --assumptions`
|
||||
|
||||
計画前にClaudeの意図するアプローチをプレビューします。
|
||||
|
||||
@@ -409,7 +409,7 @@
|
||||
| `N` | いいえ | フェーズ番号 |
|
||||
|
||||
```bash
|
||||
/gsd-list-phase-assumptions 2 # フェーズ2の前提を確認
|
||||
/gsd-discuss-phase --assumptions 2 # フェーズ2の前提を確認
|
||||
```
|
||||
|
||||
|
||||
@@ -482,7 +482,7 @@
|
||||
|
||||
---
|
||||
|
||||
### `/gsd-analyze-dependencies`
|
||||
### `/gsd-manager --analyze-deps`
|
||||
|
||||
フェーズ依存関係を検出し、ROADMAP.md に `Depends on` エントリを提案します。(v1.32)
|
||||
|
||||
@@ -491,7 +491,7 @@
|
||||
**動作:** 依存関係提案テーブルを表示し、ユーザー確認後に ROADMAP.md の `Depends on` フィールドを更新します。
|
||||
|
||||
```bash
|
||||
/gsd-analyze-dependencies # 依存関係の分析と提案
|
||||
/gsd-manager --analyze-deps # 依存関係の分析と提案
|
||||
```
|
||||
|
||||
---
|
||||
@@ -948,10 +948,3 @@ GSDアップデート後にローカルの変更を復元します。
|
||||
|
||||
## コミュニティコマンド
|
||||
|
||||
### `/gsd-join-discord`
|
||||
|
||||
Discordコミュニティの招待を開きます。
|
||||
|
||||
```bash
|
||||
/gsd-join-discord
|
||||
```
|
||||
|
||||
@@ -471,7 +471,7 @@
|
||||
|
||||
### 14. 自動進行(Next)
|
||||
|
||||
**コマンド:** `/gsd-next`
|
||||
**コマンド:** `/gsd-progress --next`
|
||||
|
||||
**目的:** 現在のプロジェクト状態を自動検出し、次の論理的なワークフローステップに進めます。どのフェーズ/ステップにいるかを覚えておく必要がなくなります。
|
||||
|
||||
@@ -641,7 +641,7 @@
|
||||
|
||||
### 24. セッションレポート
|
||||
|
||||
**コマンド:** `/gsd-session-report`
|
||||
**コマンド:** `/gsd-pause-work --report`
|
||||
|
||||
**目的:** 実施した作業、達成した成果、推定リソース使用量をキャプチャした、構造化されたセッション後のサマリードキュメントを生成します。
|
||||
|
||||
@@ -1725,7 +1725,7 @@ Claude が GSD ワークフローコンテキスト外でファイル編集を
|
||||
|
||||
### 77. フェーズ依存関係分析
|
||||
|
||||
**コマンド:** `/gsd-analyze-dependencies`
|
||||
**コマンド:** `/gsd-manager --analyze-deps`
|
||||
|
||||
**目的:** フェーズ依存関係を検出し、`/gsd-manager` 実行前に ROADMAP.md への `Depends on` エントリを提案します。
|
||||
|
||||
|
||||
@@ -391,7 +391,7 @@ GSD はマークダウンファイルを生成し、それが LLM のシステ
|
||||
| `/gsd-verify-work [N]` | 自動診断付き手動 UAT | 実行完了後 |
|
||||
| `/gsd-ship [N]` | 検証済みの作業から PR を作成 | 検証合格後 |
|
||||
| `/gsd-fast <text>` | インラインの軽微なタスク — プランニングを完全にスキップ | タイプミス修正、設定変更、小規模リファクタリング |
|
||||
| `/gsd-next` | 状態を自動検出して次のステップを実行 | いつでも — 「次に何をすべき?」 |
|
||||
| `/gsd-progress --next` | 状態を自動検出して次のステップを実行 | いつでも — 「次に何をすべき?」 |
|
||||
| `/gsd-ui-review [N]` | 遡及的6ピラービジュアル監査 | 実行後または verify-work 後(フロントエンドプロジェクト) |
|
||||
| `/gsd-audit-milestone` | マイルストーンの完了定義を満たしているか検証 | マイルストーン完了前 |
|
||||
| `/gsd-complete-milestone` | マイルストーンをアーカイブし、リリースタグを作成 | 全フェーズの検証完了後 |
|
||||
@@ -404,10 +404,9 @@ GSD はマークダウンファイルを生成し、それが LLM のシステ
|
||||
| `/gsd-progress` | 状態と次のステップを表示 | いつでも -- 「今どこにいる?」 |
|
||||
| `/gsd-resume-work` | 前回のセッションからフルコンテキストを復元 | 新しいセッションの開始時 |
|
||||
| `/gsd-pause-work` | 構造化されたハンドオフを保存(HANDOFF.json + continue-here.md) | フェーズの途中で作業を中断する時 |
|
||||
| `/gsd-session-report` | 作業内容と成果を含むセッションサマリーを生成 | セッション終了時、ステークホルダーへの共有時 |
|
||||
| `/gsd-pause-work --report` | 作業内容と成果を含むセッションサマリーを生成 | セッション終了時、ステークホルダーへの共有時 |
|
||||
| `/gsd-help` | すべてのコマンドを表示 | クイックリファレンス |
|
||||
| `/gsd-update` | 変更履歴プレビュー付きで GSD を更新 | 新バージョンの確認時 |
|
||||
| `/gsd-join-discord` | Discord コミュニティの招待リンクを開く | 質問やコミュニティ参加時 |
|
||||
|
||||
### フェーズ管理
|
||||
|
||||
@@ -416,7 +415,7 @@ GSD はマークダウンファイルを生成し、それが LLM のシステ
|
||||
| `/gsd-phase` | ロードマップに新しいフェーズを追加 | 初期プランニング後にスコープが拡大した場合 |
|
||||
| `/gsd-phase --insert [N]` | 緊急作業を挿入(小数番号) | マイルストーン中の緊急修正 |
|
||||
| `/gsd-phase --remove [N]` | 将来のフェーズを削除して番号を振り直す | 機能のスコープ縮小 |
|
||||
| `/gsd-list-phase-assumptions [N]` | Claude の意図するアプローチをプレビュー | プランニング前に方向性を確認 |
|
||||
| `/gsd-discuss-phase --assumptions [N]` | Claude の意図するアプローチをプレビュー | プランニング前に方向性を確認 |
|
||||
| `/gsd-plan-phase --research-phase [N]` | エコシステムの深いリサーチのみ | 複雑または不慣れなドメイン |
|
||||
|
||||
### ブラウンフィールドとユーティリティ
|
||||
@@ -599,11 +598,11 @@ claude --dangerously-skip-permissions
|
||||
/gsd-ship 1 # 検証済み作業から PR を作成
|
||||
/gsd-ui-review 1 # ビジュアル監査(フロントエンドフェーズ)
|
||||
/clear
|
||||
/gsd-next # 自動検出して次のステップを実行
|
||||
/gsd-progress --next # 自動検出して次のステップを実行
|
||||
...
|
||||
/gsd-audit-milestone # すべて出荷されたか確認
|
||||
/gsd-complete-milestone # アーカイブ、タグ付け、完了
|
||||
/gsd-session-report # セッションサマリーを生成
|
||||
/gsd-pause-work --report # セッションサマリーを生成
|
||||
```
|
||||
|
||||
### 既存ドキュメントからの新規プロジェクト
|
||||
@@ -703,7 +702,7 @@ cd ~/gsd-workspaces/feature-b
|
||||
|
||||
### プランが誤っている、または方向性がずれている
|
||||
|
||||
プランニング前に `/gsd-discuss-phase [N]` を実行してください。プランの品質問題のほとんどは、CONTEXT.md があれば防げたはずの前提を Claude が置いてしまうことに起因します。`/gsd-list-phase-assumptions [N]` を使用して、プランにコミットする前に Claude の意図を確認することもできます。
|
||||
プランニング前に `/gsd-discuss-phase [N]` を実行してください。プランの品質問題のほとんどは、CONTEXT.md があれば防げたはずの前提を Claude が置いてしまうことに起因します。`/gsd-discuss-phase --assumptions [N]` を使用して、プランにコミットする前に Claude の意図を確認することもできます。
|
||||
|
||||
### 実行が失敗する、またはスタブが生成される
|
||||
|
||||
@@ -799,8 +798,8 @@ Windows でインストーラーが `EPERM: operation not permitted, scandir`
|
||||
| プランがビジョンに合わない | `/gsd-discuss-phase [N]` で再プランニング |
|
||||
| コストが高い | `/gsd-config --profile budget` と `/gsd-settings` でエージェントをオフ |
|
||||
| アップデートがローカル変更を壊した | `/gsd-update --reapply` |
|
||||
| ステークホルダー向けセッションサマリーが欲しい | `/gsd-session-report` |
|
||||
| 次のステップがわからない | `/gsd-next` |
|
||||
| ステークホルダー向けセッションサマリーが欲しい | `/gsd-pause-work --report` |
|
||||
| 次のステップがわからない | `/gsd-progress --next` |
|
||||
| 並列実行でビルドエラー | GSD を更新するか `parallelization.enabled: false` を設定 |
|
||||
|
||||
---
|
||||
@@ -819,7 +818,7 @@ Windows でインストーラーが `EPERM: operation not permitted, scandir`
|
||||
MILESTONES.md # 完了したマイルストーンのアーカイブ
|
||||
HANDOFF.json # 構造化セッション引き継ぎ(/gsd-pause-work から)
|
||||
research/ # /gsd-new-project からのドメインリサーチ
|
||||
reports/ # セッションレポート(/gsd-session-report から)
|
||||
reports/ # セッションレポート(/gsd-pause-work --report から)
|
||||
todos/
|
||||
pending/ # 作業待ちのキャプチャされたアイデア
|
||||
done/ # 完了した TODO
|
||||
|
||||
@@ -198,7 +198,7 @@
|
||||
|
||||
---
|
||||
|
||||
### `/gsd-next`
|
||||
### `/gsd-progress --next`
|
||||
|
||||
다음 논리적 워크플로우 단계로 자동으로 이동합니다. 프로젝트 상태를 읽고 적절한 명령어를 실행합니다.
|
||||
|
||||
@@ -212,12 +212,12 @@
|
||||
- 모든 페이즈 완료 → `/gsd-complete-milestone` 제안
|
||||
|
||||
```bash
|
||||
/gsd-next # 다음 단계 자동 감지 및 실행
|
||||
/gsd-progress --next # 다음 단계 자동 감지 및 실행
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `/gsd-session-report`
|
||||
### `/gsd-pause-work --report`
|
||||
|
||||
작업 요약, 결과, 예상 리소스 사용량을 포함한 세션 보고서를 생성합니다.
|
||||
|
||||
@@ -225,7 +225,7 @@
|
||||
**생성 파일:** `.planning/reports/SESSION_REPORT.md`
|
||||
|
||||
```bash
|
||||
/gsd-session-report # 세션 종료 후 요약 생성
|
||||
/gsd-pause-work --report # 세션 종료 후 요약 생성
|
||||
```
|
||||
|
||||
**보고서 포함 내용.**
|
||||
@@ -400,7 +400,7 @@
|
||||
/gsd-phase --remove 7 # 페이즈 7 제거, 8→7, 9→8 등으로 재번호
|
||||
```
|
||||
|
||||
### `/gsd-list-phase-assumptions`
|
||||
### `/gsd-discuss-phase --assumptions`
|
||||
|
||||
계획 수립 전 Claude의 예상 접근 방식을 미리 확인합니다.
|
||||
|
||||
@@ -409,7 +409,7 @@
|
||||
| `N` | 아니오 | 페이즈 번호 |
|
||||
|
||||
```bash
|
||||
/gsd-list-phase-assumptions 2 # 페이즈 2 가정 사항 확인
|
||||
/gsd-discuss-phase --assumptions 2 # 페이즈 2 가정 사항 확인
|
||||
```
|
||||
|
||||
|
||||
@@ -482,7 +482,7 @@ Nyquist 검증 갭을 소급하여 감사하고 보완합니다.
|
||||
|
||||
---
|
||||
|
||||
### `/gsd-analyze-dependencies`
|
||||
### `/gsd-manager --analyze-deps`
|
||||
|
||||
페이즈 의존성을 감지하고 ROADMAP.md에 `Depends on` 항목을 제안합니다. (v1.32)
|
||||
|
||||
@@ -491,7 +491,7 @@ Nyquist 검증 갭을 소급하여 감사하고 보완합니다.
|
||||
**동작 방식:** 의존성 제안 테이블을 표시하고 사용자 확인 후 ROADMAP.md의 `Depends on` 필드를 업데이트합니다.
|
||||
|
||||
```bash
|
||||
/gsd-analyze-dependencies # 의존성 분석 및 제안
|
||||
/gsd-manager --analyze-deps # 의존성 분석 및 제안
|
||||
```
|
||||
|
||||
---
|
||||
@@ -948,10 +948,3 @@ GSD 업데이트 후 로컬 수정사항을 복원합니다.
|
||||
|
||||
## 커뮤니티 명령어
|
||||
|
||||
### `/gsd-join-discord`
|
||||
|
||||
Discord 커뮤니티 초대 링크를 엽니다.
|
||||
|
||||
```bash
|
||||
/gsd-join-discord
|
||||
```
|
||||
|
||||
@@ -471,7 +471,7 @@
|
||||
|
||||
### 14. Auto-Advance (Next)
|
||||
|
||||
**명령어:** `/gsd-next`
|
||||
**명령어:** `/gsd-progress --next`
|
||||
|
||||
**목적:** 현재 프로젝트 상태를 자동으로 감지하고 다음 논리적 워크플로우 단계로 진행합니다. 현재 어느 페이즈/단계에 있는지 기억할 필요가 없습니다.
|
||||
|
||||
@@ -641,7 +641,7 @@
|
||||
|
||||
### 24. Session Reporting
|
||||
|
||||
**명령어:** `/gsd-session-report`
|
||||
**명령어:** `/gsd-pause-work --report`
|
||||
|
||||
**목적:** 수행된 작업, 달성된 결과, 예상 리소스 사용량을 캡처하는 구조화된 세션 후 요약 문서를 생성합니다.
|
||||
|
||||
@@ -1725,7 +1725,7 @@ Claude가 GSD 워크플로우 컨텍스트 밖에서 파일 편집을 시도하
|
||||
|
||||
### 77. 페이즈 의존성 분석
|
||||
|
||||
**명령어:** `/gsd-analyze-dependencies`
|
||||
**명령어:** `/gsd-manager --analyze-deps`
|
||||
|
||||
**목적:** 페이즈 의존성을 감지하고 `/gsd-manager` 실행 전 ROADMAP.md에 `Depends on` 항목을 제안합니다.
|
||||
|
||||
|
||||
@@ -391,7 +391,7 @@ GSD는 LLM 시스템 프롬프트가 되는 마크다운 파일을 생성합니
|
||||
| `/gsd-verify-work [N]` | 자동 진단을 포함한 수동 UAT | 실행 완료 후 |
|
||||
| `/gsd-ship [N]` | 검증된 작업으로 PR 생성 | 검증 통과 후 |
|
||||
| `/gsd-fast <text>` | 계획을 완전히 건너뛰는 인라인 간단 작업 | 오타 수정, 설정 변경, 소규모 리팩터링 |
|
||||
| `/gsd-next` | 상태 자동 감지 및 다음 단계 실행 | 언제든 — "다음에 무엇을 해야 하나?" |
|
||||
| `/gsd-progress --next` | 상태 자동 감지 및 다음 단계 실행 | 언제든 — "다음에 무엇을 해야 하나?" |
|
||||
| `/gsd-ui-review [N]` | 6개 기둥 기반 시각적 감사 소급 수행 | 실행 또는 verify-work 이후 (프론트엔드 프로젝트) |
|
||||
| `/gsd-audit-milestone` | 마일스톤이 완료 정의를 충족했는지 검증 | 마일스톤 완료 전 |
|
||||
| `/gsd-complete-milestone` | 마일스톤 아카이브 및 릴리스 태그 생성 | 모든 페이즈 검증 완료 시 |
|
||||
@@ -404,10 +404,9 @@ GSD는 LLM 시스템 프롬프트가 되는 마크다운 파일을 생성합니
|
||||
| `/gsd-progress` | 상태 및 다음 단계 표시 | 언제든 -- "지금 어디 있나?" |
|
||||
| `/gsd-resume-work` | 마지막 세션의 전체 컨텍스트 복원 | 새 세션 시작 시 |
|
||||
| `/gsd-pause-work` | 구조화된 핸드오프 저장 (HANDOFF.json + continue-here.md) | 페이즈 중간에 중단할 때 |
|
||||
| `/gsd-session-report` | 작업 및 결과가 포함된 세션 요약 생성 | 세션 종료 시, 이해관계자 공유 시 |
|
||||
| `/gsd-pause-work --report` | 작업 및 결과가 포함된 세션 요약 생성 | 세션 종료 시, 이해관계자 공유 시 |
|
||||
| `/gsd-help` | 모든 명령어 표시 | 빠른 레퍼런스 |
|
||||
| `/gsd-update` | 변경 로그 미리보기와 함께 GSD 업데이트 | 새 버전 확인 시 |
|
||||
| `/gsd-join-discord` | Discord 커뮤니티 초대 링크 열기 | 질문이나 커뮤니티 참여 시 |
|
||||
|
||||
### 페이즈 관리
|
||||
|
||||
@@ -416,7 +415,7 @@ GSD는 LLM 시스템 프롬프트가 되는 마크다운 파일을 생성합니
|
||||
| `/gsd-phase` | 로드맵에 새 페이즈 추가 | 초기 계획 후 범위가 늘어날 때 |
|
||||
| `/gsd-phase --insert [N]` | 긴급 작업 삽입 (소수점 번호 체계) | 마일스톤 중간의 긴급 수정 시 |
|
||||
| `/gsd-phase --remove [N]` | 미래 페이즈 제거 및 재번호 | 기능 범위 축소 시 |
|
||||
| `/gsd-list-phase-assumptions [N]` | Claude의 예상 접근 방식 미리 확인 | 계획 전 방향 검증 시 |
|
||||
| `/gsd-discuss-phase --assumptions [N]` | Claude의 예상 접근 방식 미리 확인 | 계획 전 방향 검증 시 |
|
||||
| `/gsd-plan-phase --research-phase [N]` | 심층 에코시스템 조사만 수행 | 복잡하거나 익숙하지 않은 도메인 |
|
||||
|
||||
### 브라운필드 및 유틸리티
|
||||
@@ -599,11 +598,11 @@ claude --dangerously-skip-permissions
|
||||
/gsd-ship 1 # Create PR from verified work
|
||||
/gsd-ui-review 1 # Visual audit (frontend phases)
|
||||
/clear
|
||||
/gsd-next # Auto-detect and run next step
|
||||
/gsd-progress --next # Auto-detect and run next step
|
||||
...
|
||||
/gsd-audit-milestone # Check everything shipped
|
||||
/gsd-complete-milestone # Archive, tag, done
|
||||
/gsd-session-report # Generate session summary
|
||||
/gsd-pause-work --report # Generate session summary
|
||||
```
|
||||
|
||||
### 기존 문서로 새 프로젝트 시작
|
||||
@@ -703,7 +702,7 @@ cd ~/gsd-workspaces/feature-b
|
||||
|
||||
### 계획이 잘못되거나 맞지 않는 경우
|
||||
|
||||
계획 전에 `/gsd-discuss-phase [N]`을 실행하세요. 대부분의 계획 품질 문제는 `CONTEXT.md`가 있었다면 방지할 수 있었던 가정을 Claude가 세우기 때문에 발생합니다. `/gsd-list-phase-assumptions [N]`을 실행하여 계획에 동의하기 전에 Claude가 무엇을 하려는지 확인할 수도 있습니다.
|
||||
계획 전에 `/gsd-discuss-phase [N]`을 실행하세요. 대부분의 계획 품질 문제는 `CONTEXT.md`가 있었다면 방지할 수 있었던 가정을 Claude가 세우기 때문에 발생합니다. `/gsd-discuss-phase --assumptions [N]`을 실행하여 계획에 동의하기 전에 Claude가 무엇을 하려는지 확인할 수도 있습니다.
|
||||
|
||||
### 실행이 실패하거나 스텁을 생성하는 경우
|
||||
|
||||
@@ -799,8 +798,8 @@ Windows에서 설치 프로그램이 `EPERM: operation not permitted, scandir`
|
||||
| 계획이 비전과 맞지 않음 | `/gsd-discuss-phase [N]` 후 재계획 |
|
||||
| 비용이 높아짐 | `/gsd-config --profile budget` 및 `/gsd-settings`에서 에이전트 비활성화 |
|
||||
| 업데이트가 로컬 변경사항 파괴 | `/gsd-update --reapply` |
|
||||
| 이해관계자를 위한 세션 요약 필요 | `/gsd-session-report` |
|
||||
| 다음 단계를 모르겠음 | `/gsd-next` |
|
||||
| 이해관계자를 위한 세션 요약 필요 | `/gsd-pause-work --report` |
|
||||
| 다음 단계를 모르겠음 | `/gsd-progress --next` |
|
||||
| 병렬 실행 빌드 오류 | GSD 업데이트 또는 `parallelization.enabled: false` 설정 |
|
||||
|
||||
---
|
||||
@@ -819,7 +818,7 @@ Windows에서 설치 프로그램이 `EPERM: operation not permitted, scandir`
|
||||
MILESTONES.md # Completed milestone archive
|
||||
HANDOFF.json # Structured session handoff (from /gsd-pause-work)
|
||||
research/ # Domain research from /gsd-new-project
|
||||
reports/ # Session reports (from /gsd-session-report)
|
||||
reports/ # Session reports (from /gsd-pause-work --report)
|
||||
todos/
|
||||
pending/ # Captured ideas awaiting work
|
||||
done/ # Completed todos
|
||||
|
||||
@@ -16,7 +16,7 @@ Para detalhes completos de flags avançadas e mudanças recentes, consulte tamb
|
||||
| `/gsd-execute-phase <N>` | Executa planos em ondas paralelas | Após planejamento aprovado |
|
||||
| `/gsd-verify-work [N]` | UAT manual com diagnóstico automático | Após execução |
|
||||
| `/gsd-ship [N]` | Cria PR da fase validada | Ao concluir a fase |
|
||||
| `/gsd-next` | Detecta e executa o próximo passo lógico | Qualquer momento |
|
||||
| `/gsd-progress --next` | Detecta e executa o próximo passo lógico | Qualquer momento |
|
||||
| `/gsd-fast <texto>` | Tarefa curta sem planejamento completo | Ajustes triviais |
|
||||
|
||||
## Navegação e Sessão
|
||||
@@ -26,7 +26,7 @@ Para detalhes completos de flags avançadas e mudanças recentes, consulte tamb
|
||||
| `/gsd-progress` | Mostra status atual e próximos passos |
|
||||
| `/gsd-resume-work` | Retoma contexto da sessão anterior |
|
||||
| `/gsd-pause-work` | Salva handoff estruturado |
|
||||
| `/gsd-session-report` | Gera resumo da sessão |
|
||||
| `/gsd-pause-work --report` | Gera resumo da sessão |
|
||||
| `/gsd-autonomous` | Executa todas as fases restantes de forma autônoma (`--from N`, `--to N`, `--only N`) |
|
||||
| `/gsd-help` | Lista comandos e uso |
|
||||
| `/gsd-update` | Atualiza o GSD |
|
||||
@@ -38,7 +38,7 @@ Para detalhes completos de flags avançadas e mudanças recentes, consulte tamb
|
||||
| `/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-discuss-phase --assumptions [N]` | Mostra abordagem assumida pelo Claude |
|
||||
|
||||
## Brownfield e Utilidades
|
||||
|
||||
@@ -47,7 +47,7 @@ Para detalhes completos de flags avançadas e mudanças recentes, consulte tamb
|
||||
| `/gsd-map-codebase` | Mapeia base existente antes de novo projeto |
|
||||
| `/gsd-quick` | Tarefas ad-hoc com garantias do GSD |
|
||||
| `/gsd-debug [desc]` | Debug sistemático com estado persistente (`--diagnose` para modo diagnóstico) |
|
||||
| `/gsd-analyze-dependencies` | Detecta dependências entre fases e sugere `Depends on` no ROADMAP.md (v1.32) |
|
||||
| `/gsd-manager --analyze-deps` | 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-config --profile <perfil>` | Troca rápida de perfil de modelo |
|
||||
|
||||
@@ -37,7 +37,7 @@ Para catálogo completo e detalhamento exaustivo, consulte [FEATURES.md em ingl
|
||||
- **Perfis de modelo** (`quality`, `balanced`, `budget`, `inherit`)
|
||||
- **Ajuste por toggles** para custo/qualidade/velocidade
|
||||
- **Diagnóstico forense** com `/gsd-forensics`
|
||||
- **Relatório de sessão** com `/gsd-session-report`
|
||||
- **Relatório de sessão** com `/gsd-pause-work --report`
|
||||
|
||||
## Novidades v1.31--v1.32
|
||||
|
||||
@@ -55,7 +55,7 @@ Para catálogo completo e detalhamento exaustivo, consulte [FEATURES.md em ingl
|
||||
- **Context reduction** — truncamento de markdown e ordenação cache-friendly (v1.32)
|
||||
- **`--power` flag** — respostas em batch via arquivo para discuss-phase (v1.32)
|
||||
- **`--diagnose` flag** — modo diagnóstico sem modificações no `/gsd-debug` (v1.32)
|
||||
- **`/gsd-analyze-dependencies`** — detecta dependências entre fases (v1.32)
|
||||
- **`/gsd-manager --analyze-deps`** — detecta dependências entre fases (v1.32)
|
||||
- **Response language config** — `response_language` para saída consistente em idioma (v1.32)
|
||||
- **Novos runtimes** — Trae IDE, Cline, Augment Code (v1.32)
|
||||
- **Manual update** — procedimento de atualização sem npm (v1.32)
|
||||
|
||||
@@ -39,7 +39,7 @@ Para iniciar projeto novo:
|
||||
Para seguir automaticamente o próximo passo:
|
||||
|
||||
```bash
|
||||
/gsd-next
|
||||
/gsd-progress --next
|
||||
```
|
||||
|
||||
### Nyquist Validation
|
||||
@@ -162,7 +162,7 @@ Para arquivos sensíveis, use deny list no Claude Code.
|
||||
| `/gsd-execute-phase [N]` | Executar planos em ondas |
|
||||
| `/gsd-verify-work [N]` | UAT manual |
|
||||
| `/gsd-ship [N]` | Gerar PR da fase |
|
||||
| `/gsd-next` | Próximo passo automático |
|
||||
| `/gsd-progress --next` | Próximo passo automático |
|
||||
|
||||
### Gestão e utilidades
|
||||
|
||||
@@ -171,7 +171,7 @@ Para arquivos sensíveis, use deny list no Claude Code.
|
||||
| `/gsd-progress` | Ver status atual |
|
||||
| `/gsd-resume-work` | Retomar sessão |
|
||||
| `/gsd-pause-work` | Pausar com handoff |
|
||||
| `/gsd-session-report` | Resumo da sessão |
|
||||
| `/gsd-pause-work --report` | Resumo da sessão |
|
||||
| `/gsd-quick` | Tarefa ad-hoc com garantias GSD |
|
||||
| `/gsd-debug [desc]` | Debug sistemático |
|
||||
| `/gsd-forensics` | Diagnóstico de workflow quebrado |
|
||||
@@ -268,7 +268,7 @@ Use `/clear` entre etapas grandes e retome com `/gsd-resume-work` ou `/gsd-progr
|
||||
|
||||
### Plano desalinhado
|
||||
|
||||
Rode `/gsd-discuss-phase [N]` antes do plano e valide suposições com `/gsd-list-phase-assumptions [N]`.
|
||||
Rode `/gsd-discuss-phase [N]` antes do plano e valide suposições com `/gsd-discuss-phase --assumptions [N]`.
|
||||
|
||||
### Execução falhou ou saiu com stubs
|
||||
|
||||
@@ -298,7 +298,7 @@ Use `resolve_model_ids: "omit"` para deixar o runtime resolver modelos padrão.
|
||||
| Bug em workflow | `/gsd-forensics` |
|
||||
| Correção pontual | `/gsd-quick` |
|
||||
| Custo alto | `/gsd-config --profile budget` |
|
||||
| Não sabe próximo passo | `/gsd-next` |
|
||||
| Não sabe próximo passo | `/gsd-progress --next` |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -499,7 +499,6 @@ lmn012o feat(08-02): 创建注册端点
|
||||
| `/gsd-progress` | 我在哪?接下来做什么? |
|
||||
| `/gsd-help` | 显示所有命令和使用指南 |
|
||||
| `/gsd-update` | 更新 GSD 并预览变更日志 |
|
||||
| `/gsd-join-discord` | 加入 GSD Discord 社区 |
|
||||
|
||||
### 现有代码库
|
||||
|
||||
@@ -514,9 +513,9 @@ lmn012o feat(08-02): 创建注册端点
|
||||
| `/gsd-phase` | 向路线图追加阶段 |
|
||||
| `/gsd-phase --insert [N]` | 在阶段之间插入紧急工作 |
|
||||
| `/gsd-phase --remove [N]` | 删除未来阶段,重新编号 |
|
||||
| `/gsd-list-phase-assumptions [N]` | 规划前查看 Claude 的预期方法 |
|
||||
| `/gsd-discuss-phase --assumptions [N]` | 规划前查看 Claude 的预期方法 |
|
||||
| `/gsd-autonomous [--from N] [--to N] [--only N]` | 自主执行所有剩余阶段(`--to N` 执行到阶段 N 停止,`--only N` 只执行单个阶段) |
|
||||
| `/gsd-analyze-dependencies` | 检测阶段间依赖关系并建议 ROADMAP.md 的 `Depends on` 条目 |
|
||||
| `/gsd-manager --analyze-deps` | 检测阶段间依赖关系并建议 ROADMAP.md 的 `Depends on` 条目 |
|
||||
|
||||
### 会话
|
||||
|
||||
|
||||
@@ -199,7 +199,6 @@
|
||||
| `/gsd-pause-work` | 保存上下文交接 | 阶段中途停止 |
|
||||
| `/gsd-help` | 显示所有命令 | 快速参考 |
|
||||
| `/gsd-update` | 更新 GSD 并预览变更日志 | 检查新版本 |
|
||||
| `/gsd-join-discord` | 打开 Discord 社区邀请 | 问题或社区 |
|
||||
|
||||
### 阶段管理
|
||||
|
||||
@@ -208,10 +207,10 @@
|
||||
| `/gsd-phase` | 向路线图追加新阶段 | 初始规划后范围增长 |
|
||||
| `/gsd-phase --insert [N]` | 插入紧急工作(小数编号) | 里程碑中途紧急修复 |
|
||||
| `/gsd-phase --remove [N]` | 删除未来阶段并重新编号 | 移除某个功能 |
|
||||
| `/gsd-list-phase-assumptions [N]` | 预览 Claude 的预期方法 | 规划前,验证方向 |
|
||||
| `/gsd-discuss-phase --assumptions [N]` | 预览 Claude 的预期方法 | 规划前,验证方向 |
|
||||
| `/gsd-plan-phase --research-phase [N]` | 仅深度生态研究 | 复杂或不熟悉的领域 |
|
||||
| `/gsd-autonomous [--from N] [--to N] [--only N]` | 自主执行剩余阶段(`--to N` 到阶段 N 停止) | 批量自动处理 |
|
||||
| `/gsd-analyze-dependencies` | 检测阶段间依赖关系 | `/gsd-manager` 前分析 |
|
||||
| `/gsd-manager --analyze-deps` | 检测阶段间依赖关系 | `/gsd-manager` 前分析 |
|
||||
|
||||
### 状态管理
|
||||
|
||||
@@ -424,7 +423,7 @@ claude --dangerously-skip-permissions
|
||||
|
||||
### 计划看起来错误或不一致
|
||||
|
||||
在规划前运行 `/gsd-discuss-phase [N]`。大多数计划质量问题来自 Claude 做出了 `CONTEXT.md` 本可以防止的假设。你也可以运行 `/gsd-list-phase-assumptions [N]` 在提交计划前查看 Claude 打算做什么。
|
||||
在规划前运行 `/gsd-discuss-phase [N]`。大多数计划质量问题来自 Claude 做出了 `CONTEXT.md` 本可以防止的假设。你也可以运行 `/gsd-discuss-phase --assumptions [N]` 在提交计划前查看 Claude 打算做什么。
|
||||
|
||||
### 执行失败或产生存根
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
**也可选:**
|
||||
- 执行前审查计划
|
||||
- `/gsd-list-phase-assumptions 2` — 检查假设
|
||||
- `/gsd-discuss-phase --assumptions 2` — 检查假设
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
@@ -792,19 +792,13 @@ function parseWorktreePorcelain(porcelain) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove linked git worktrees whose branch has already been merged into the
|
||||
* current HEAD of the main worktree. Also runs `git worktree prune` to clear
|
||||
* any stale references left by manually-deleted worktree directories.
|
||||
* Clear stale worktree metadata references via `git worktree prune`.
|
||||
*
|
||||
* Safe guards:
|
||||
* - Never removes the main worktree (first entry in --porcelain output).
|
||||
* - Never removes the worktree at process.cwd().
|
||||
* - Never removes a worktree whose branch has unmerged commits.
|
||||
* - Skips detached-HEAD worktrees (no branch name).
|
||||
* Destructive linked-worktree removal is disabled by default for safety.
|
||||
*
|
||||
* @param {string} repoRoot - absolute path to the main (or any) worktree of
|
||||
* the repository; used as `cwd` for git commands.
|
||||
* @returns {string[]} list of worktree paths that were removed
|
||||
* @returns {string[]} list of worktree paths that were removed (always empty)
|
||||
*/
|
||||
function pruneOrphanedWorktrees(repoRoot) {
|
||||
const pruned = [];
|
||||
@@ -821,37 +815,14 @@ function pruneOrphanedWorktrees(repoRoot) {
|
||||
return pruned;
|
||||
}
|
||||
|
||||
// 2. First entry is the main worktree — never touch it
|
||||
const mainWorktreePath = worktrees[0].path;
|
||||
|
||||
// 3. Check each non-main worktree
|
||||
for (let i = 1; i < worktrees.length; i++) {
|
||||
const { path: wtPath, branch } = worktrees[i];
|
||||
|
||||
// Never remove the worktree for the current process directory
|
||||
if (wtPath === cwd || cwd.startsWith(wtPath + path.sep)) continue;
|
||||
|
||||
// Check if the branch is fully merged into HEAD (main)
|
||||
// git merge-base --is-ancestor <branch> HEAD exits 0 when merged
|
||||
const ancestorCheck = execGit(repoRoot, [
|
||||
'merge-base', '--is-ancestor', branch, 'HEAD',
|
||||
]);
|
||||
|
||||
if (ancestorCheck.exitCode !== 0) {
|
||||
// Not yet merged — leave it alone
|
||||
continue;
|
||||
}
|
||||
|
||||
// Remove the worktree and delete the branch
|
||||
const removeResult = execGit(repoRoot, ['worktree', 'remove', '--force', wtPath]);
|
||||
if (removeResult.exitCode === 0) {
|
||||
execGit(repoRoot, ['branch', '-D', branch]);
|
||||
pruned.push(wtPath);
|
||||
}
|
||||
}
|
||||
// Destructive removal of linked worktrees is intentionally disabled.
|
||||
// Keep metadata cleanup only (git worktree prune), which clears stale refs
|
||||
// for manually-deleted directories without removing active sibling worktrees.
|
||||
void cwd;
|
||||
void worktrees;
|
||||
} catch { /* never crash the caller */ }
|
||||
|
||||
// 4. Always run prune to clear stale references (e.g. manually-deleted dirs)
|
||||
// Always run prune to clear stale references (e.g. manually-deleted dirs)
|
||||
execGit(repoRoot, ['worktree', 'prune']);
|
||||
|
||||
return pruned;
|
||||
@@ -950,6 +921,17 @@ function phaseTokenMatches(dirName, normalized) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function extractCanonicalPlanId(filename) {
|
||||
const base = filename.replace(/-PLAN\.md$/i, '').replace(/-SUMMARY\.md$/i, '').replace(/\.md$/i, '');
|
||||
const parts = base.split('-').filter(Boolean);
|
||||
const tokenRe = /^\d+[A-Z]?(?:\.\d+)*$/i;
|
||||
const phaseIdx = parts.findIndex(p => tokenRe.test(p));
|
||||
if (phaseIdx >= 0 && phaseIdx + 1 < parts.length && tokenRe.test(parts[phaseIdx + 1])) {
|
||||
return `${parts[phaseIdx]}-${parts[phaseIdx + 1]}`;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
function searchPhaseInDir(baseDir, relBase, normalized) {
|
||||
try {
|
||||
const dirs = readSubdirectories(baseDir, true);
|
||||
@@ -970,11 +952,16 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
|
||||
const summaries = unsortedSummaries.sort();
|
||||
|
||||
const completedPlanIds = new Set(
|
||||
summaries.map(s => s.replace('-SUMMARY.md', '').replace('SUMMARY.md', ''))
|
||||
summaries.flatMap(s => {
|
||||
const exact = s.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
|
||||
const canonical = extractCanonicalPlanId(s);
|
||||
return canonical === exact ? [exact] : [exact, canonical];
|
||||
})
|
||||
);
|
||||
const incompletePlans = plans.filter(p => {
|
||||
const planId = p.replace('-PLAN.md', '').replace('PLAN.md', '');
|
||||
return !completedPlanIds.has(planId);
|
||||
const canonical = extractCanonicalPlanId(p);
|
||||
return !completedPlanIds.has(planId) && !completedPlanIds.has(canonical);
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -1895,10 +1882,62 @@ function getMilestoneInfo(cwd) {
|
||||
* to the current milestone based on ROADMAP.md phase headings.
|
||||
* If no ROADMAP exists or no phases are listed, returns a pass-all filter.
|
||||
*/
|
||||
function getMilestonePhaseFilter(cwd) {
|
||||
function getMilestonePhaseFilter(cwd, versionOverride) {
|
||||
const milestonePhaseNums = new Set();
|
||||
let missingExplicitVersion = false;
|
||||
try {
|
||||
const roadmap = extractCurrentMilestone(fs.readFileSync(path.join(planningDir(cwd), 'ROADMAP.md'), 'utf-8'), cwd);
|
||||
const roadmapPath = path.join(planningDir(cwd), 'ROADMAP.md');
|
||||
const roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
|
||||
let roadmap = extractCurrentMilestone(roadmapContent, cwd);
|
||||
|
||||
if (versionOverride) {
|
||||
const escapedVersion = escapeRegex(versionOverride);
|
||||
const sectionPattern = new RegExp(`(^#{1,3}\\s+.*${escapedVersion}[^\\n]*)`, 'mi');
|
||||
const sectionMatch = roadmapContent.match(sectionPattern);
|
||||
if (!sectionMatch) {
|
||||
// Only treat this as an error case when the roadmap is milestone-versioned.
|
||||
// Older/flat roadmap formats without vX.Y milestone headings should keep
|
||||
// legacy pass-through behavior for milestone.complete.
|
||||
const hasVersionedMilestones = /^#{1,3}\s+.*v\d+\.\d+/mi.test(roadmapContent);
|
||||
if (hasVersionedMilestones) {
|
||||
roadmap = '';
|
||||
missingExplicitVersion = true;
|
||||
}
|
||||
} else {
|
||||
const sectionStart = sectionMatch.index;
|
||||
const headingLevel = sectionMatch[1].match(/^(#{1,3})\s/)[1].length;
|
||||
const restContent = roadmapContent.slice(sectionStart + sectionMatch[0].length);
|
||||
const nextMilestonePattern = new RegExp(`^#{1,${headingLevel}}\\s+(?!Phase\\s+\\S)(?:.*v\\d+\\.\\d+|✅|📋|🚧)`, 'i');
|
||||
|
||||
let sectionEnd = roadmapContent.length;
|
||||
let fenceChar = null;
|
||||
let fenceLen = 0;
|
||||
let charOffset = 0;
|
||||
for (const line of restContent.split('\n')) {
|
||||
const fenceMatch = line.match(/^\s{0,3}((?:`{3,}|~{3,}))(.*)/);
|
||||
if (fenceMatch) {
|
||||
const char = fenceMatch[1][0];
|
||||
const len = fenceMatch[1].length;
|
||||
const trailing = fenceMatch[2] || '';
|
||||
if (!fenceChar) {
|
||||
fenceChar = char;
|
||||
fenceLen = len;
|
||||
} else if (char === fenceChar && len >= fenceLen && /^\s*$/.test(trailing)) {
|
||||
fenceChar = null;
|
||||
fenceLen = 0;
|
||||
}
|
||||
} else if (!fenceChar && nextMilestonePattern.test(line)) {
|
||||
sectionEnd = sectionStart + sectionMatch[0].length + charOffset;
|
||||
break;
|
||||
}
|
||||
charOffset += line.length + 1;
|
||||
}
|
||||
|
||||
const currentSection = roadmapContent.slice(sectionStart, sectionEnd);
|
||||
roadmap = currentSection;
|
||||
}
|
||||
}
|
||||
|
||||
// Match both numeric phases (Phase 1:) and custom IDs (Phase PROJ-42:)
|
||||
const phasePattern = /#{2,4}\s*Phase\s+([\w][\w.-]*)\s*:/gi;
|
||||
let m;
|
||||
@@ -1910,6 +1949,7 @@ function getMilestonePhaseFilter(cwd) {
|
||||
if (milestonePhaseNums.size === 0) {
|
||||
const passAll = () => true;
|
||||
passAll.phaseCount = 0;
|
||||
passAll.missingExplicitVersion = missingExplicitVersion;
|
||||
return passAll;
|
||||
}
|
||||
|
||||
@@ -1927,6 +1967,7 @@ function getMilestonePhaseFilter(cwd) {
|
||||
return false;
|
||||
}
|
||||
isDirInMilestone.phaseCount = milestonePhaseNums.size;
|
||||
isDirInMilestone.missingExplicitVersion = missingExplicitVersion;
|
||||
return isDirInMilestone;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,30 @@ const { maskIfSecret } = require('./secrets.cjs');
|
||||
// same in markdown but differ textually.
|
||||
const REQUIREMENTS_HEADER_RE = /^\*\*Requirements:?\*\*[^\S\n]*:?[^\S\n]*([^\n]*)$/m;
|
||||
|
||||
function listPhaseSummaryFiles(phaseDir) {
|
||||
const phaseFiles = fs.readdirSync(phaseDir);
|
||||
const rootSummaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
||||
const plansDir = path.join(phaseDir, 'plans');
|
||||
let nestedSummaries = [];
|
||||
if (fs.existsSync(plansDir)) {
|
||||
const files = fs.readdirSync(plansDir);
|
||||
nestedSummaries = files.filter(f => /^SUMMARY-\d+.*\.md$/i.test(f));
|
||||
}
|
||||
return rootSummaries.concat(nestedSummaries);
|
||||
}
|
||||
|
||||
function listPhasePlanFiles(phaseDir) {
|
||||
const phaseFiles = fs.readdirSync(phaseDir);
|
||||
const rootPlans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
|
||||
const plansDir = path.join(phaseDir, 'plans');
|
||||
let nestedPlans = [];
|
||||
if (fs.existsSync(plansDir)) {
|
||||
const files = fs.readdirSync(plansDir);
|
||||
nestedPlans = files.filter(f => /^PLAN-\d+.*\.md$/i.test(f));
|
||||
}
|
||||
return rootPlans.concat(nestedPlans);
|
||||
}
|
||||
|
||||
function getLatestCompletedMilestone(cwd) {
|
||||
const milestonesPath = path.join(planningRoot(cwd), 'MILESTONES.md');
|
||||
if (!fs.existsSync(milestonesPath)) return null;
|
||||
@@ -901,8 +925,7 @@ function cmdInitMilestoneOp(cwd, raw) {
|
||||
const dirName = diskPhaseDirs.get(canonicalizePhase(num));
|
||||
if (!dirName) continue;
|
||||
try {
|
||||
const phaseFiles = fs.readdirSync(path.join(phasesDir, dirName));
|
||||
const hasSummary = phaseFiles.some(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
||||
const hasSummary = listPhaseSummaryFiles(path.join(phasesDir, dirName)).length > 0;
|
||||
if (hasSummary) completedPhases++;
|
||||
} catch { /* intentionally empty */ }
|
||||
}
|
||||
@@ -914,8 +937,7 @@ function cmdInitMilestoneOp(cwd, raw) {
|
||||
phaseCount = dirs.length;
|
||||
for (const dir of dirs) {
|
||||
try {
|
||||
const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
|
||||
const hasSummary = phaseFiles.some(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
||||
const hasSummary = listPhaseSummaryFiles(path.join(phasesDir, dir)).length > 0;
|
||||
if (hasSummary) completedPhases++;
|
||||
} catch { /* intentionally empty */ }
|
||||
}
|
||||
@@ -1072,8 +1094,8 @@ function cmdInitManager(cwd, raw) {
|
||||
if (dirMatch) {
|
||||
const fullDir = path.join(phasesDir, dirMatch);
|
||||
const phaseFiles = fs.readdirSync(fullDir);
|
||||
planCount = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').length;
|
||||
summaryCount = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').length;
|
||||
planCount = listPhasePlanFiles(fullDir).length;
|
||||
summaryCount = listPhaseSummaryFiles(fullDir).length;
|
||||
hasContext = phaseFiles.some(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
|
||||
hasResearch = phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
|
||||
|
||||
@@ -1353,8 +1375,8 @@ function cmdInitProgress(cwd, raw) {
|
||||
const phasePath = path.join(phasesDir, dir);
|
||||
const phaseFiles = fs.readdirSync(phasePath);
|
||||
|
||||
const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
|
||||
const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
||||
const plans = listPhasePlanFiles(phasePath);
|
||||
const summaries = listPhaseSummaryFiles(phasePath);
|
||||
const hasResearch = phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
|
||||
|
||||
const status = summaries.length >= plans.length && plans.length > 0 ? 'complete' :
|
||||
|
||||
@@ -107,7 +107,10 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
||||
// Scope stats and accomplishments to only the phases belonging to the
|
||||
// current milestone's ROADMAP. Uses the shared filter from core.cjs
|
||||
// (same logic used by cmdPhasesList and other callers).
|
||||
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
||||
const isDirInMilestone = getMilestonePhaseFilter(cwd, version);
|
||||
if (isDirInMilestone.missingExplicitVersion) {
|
||||
error(`no phases found for milestone ${version} in ROADMAP.md`);
|
||||
}
|
||||
|
||||
// Gather stats from phases (scoped to current milestone only)
|
||||
let phaseCount = 0;
|
||||
@@ -196,7 +199,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
||||
atomicWriteFileSync(milestonesPath, normalizeMd(`# Milestones\n\n${milestoneEntry}`));
|
||||
}
|
||||
|
||||
// Update STATE.md — use shared helpers that handle both **bold:** and plain Field: formats
|
||||
// Update STATE.md — keep frontmatter/body semantically aligned after closure
|
||||
if (fs.existsSync(statePath)) {
|
||||
let stateContent = fs.readFileSync(statePath, 'utf-8');
|
||||
|
||||
@@ -205,6 +208,31 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
||||
stateContent = stateReplaceFieldWithFallback(stateContent, 'Last Activity Description', null,
|
||||
`${version} milestone completed and archived`);
|
||||
|
||||
// Reset Current Position narrative so resume/progress flows do not keep
|
||||
// pointing at closed-phase execution instructions.
|
||||
const positionPattern = /(##\s*Current Position\s*\n)([\s\S]*?)(?=\n##|$)/i;
|
||||
const closedPositionBody =
|
||||
`\nPhase: Milestone ${version} complete\n` +
|
||||
`Plan: —\n` +
|
||||
`Status: Awaiting next milestone\n` +
|
||||
`Last activity: ${today} — Milestone ${version} completed and archived\n\n`;
|
||||
if (positionPattern.test(stateContent)) {
|
||||
stateContent = stateContent.replace(positionPattern, (_m, header) => `${header}${closedPositionBody}`);
|
||||
} else {
|
||||
stateContent = `${stateContent.trimEnd()}\n\n## Current Position\n${closedPositionBody}`;
|
||||
}
|
||||
|
||||
// Normalize operator-next-step tails that can become stale after close.
|
||||
const operatorPattern = /(##\s*Operator Next Steps\s*\n)([\s\S]*?)(?=\n##|$)/i;
|
||||
if (operatorPattern.test(stateContent)) {
|
||||
stateContent = stateContent.replace(
|
||||
operatorPattern,
|
||||
`$1\n- Start the next milestone with /gsd-new-milestone\n\n`,
|
||||
);
|
||||
} else {
|
||||
stateContent = `${stateContent.trimEnd()}\n\n## Operator Next Steps\n\n- Start the next milestone with /gsd-new-milestone\n`;
|
||||
}
|
||||
|
||||
writeStateMd(statePath, stateContent, cwd);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,9 @@ function routePhaseCommand({ phase, args, cwd, raw, error }) {
|
||||
}
|
||||
phase.cmdPhaseAddBatch(cwd, descriptions, raw);
|
||||
} else if (subcommand === 'insert') {
|
||||
if (args.includes('--dry-run')) {
|
||||
error('phase insert does not support --dry-run');
|
||||
}
|
||||
phase.cmdPhaseInsert(cwd, args[2], args.slice(3).join(' '), raw);
|
||||
} else if (subcommand === 'remove') {
|
||||
const forceFlag = args.includes('--force');
|
||||
|
||||
@@ -50,6 +50,17 @@ function describeNonCanonicalPlans(dirFiles, matchedFiles) {
|
||||
);
|
||||
}
|
||||
|
||||
function extractCanonicalPlanId(filename) {
|
||||
const base = filename.replace(/-PLAN\.md$/i, '').replace(/-SUMMARY\.md$/i, '').replace(/\.md$/i, '');
|
||||
const parts = base.split('-').filter(Boolean);
|
||||
const tokenRe = /^\d+[A-Z]?(?:\.\d+)*$/i;
|
||||
const phaseIdx = parts.findIndex(p => tokenRe.test(p));
|
||||
if (phaseIdx >= 0 && phaseIdx + 1 < parts.length && tokenRe.test(parts[phaseIdx + 1])) {
|
||||
return `${parts[phaseIdx]}-${parts[phaseIdx + 1]}`;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
function cmdPhasesList(cwd, options, raw) {
|
||||
const phasesDir = path.join(planningDir(cwd), 'phases');
|
||||
const { type, phase, includeArchived } = options;
|
||||
@@ -288,7 +299,11 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
||||
|
||||
// Build set of plan IDs with summaries
|
||||
const completedPlanIds = new Set(
|
||||
summaryFiles.map(s => s.replace('-SUMMARY.md', '').replace('SUMMARY.md', ''))
|
||||
summaryFiles.flatMap(s => {
|
||||
const exact = s.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
|
||||
const canonical = extractCanonicalPlanId(s);
|
||||
return canonical === exact ? [exact] : [exact, canonical];
|
||||
})
|
||||
);
|
||||
|
||||
const plans = [];
|
||||
@@ -327,7 +342,7 @@ function cmdPhasePlanIndex(cwd, phase, raw) {
|
||||
filesModified = Array.isArray(fmFiles) ? fmFiles : [fmFiles];
|
||||
}
|
||||
|
||||
const hasSummary = completedPlanIds.has(planId);
|
||||
const hasSummary = completedPlanIds.has(planId) || completedPlanIds.has(extractCanonicalPlanId(planFile));
|
||||
if (!hasSummary) {
|
||||
incomplete.push(planId);
|
||||
}
|
||||
@@ -556,6 +571,10 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
||||
const afterPhaseEscaped = unpadded.replace(/\./g, '\\.');
|
||||
const targetPattern = new RegExp(`#{2,4}\\s*Phase\\s+0*${afterPhaseEscaped}:`, 'i');
|
||||
if (!targetPattern.test(content)) {
|
||||
const checklistPattern = new RegExp(`-\\s*\\[[ x]\\]\\s*\\*\\*Phase\\s+0*${afterPhaseEscaped}:`, 'i');
|
||||
if (checklistPattern.test(content)) {
|
||||
error(`Phase ${afterPhase} exists in roadmap summary but is missing a detail section (### Phase ${afterPhase}: ...).`);
|
||||
}
|
||||
error(`Phase ${afterPhase} not found in ROADMAP.md`);
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,28 @@ function coerceTruthToString(t) {
|
||||
return '';
|
||||
}
|
||||
|
||||
function countPhasePlansAndSummaries(phaseDir) {
|
||||
const phaseFiles = fs.readdirSync(phaseDir);
|
||||
const rootPlans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
|
||||
const rootSummaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
||||
|
||||
let nestedPlans = [];
|
||||
let nestedSummaries = [];
|
||||
const plansDir = path.join(phaseDir, 'plans');
|
||||
if (fs.existsSync(plansDir)) {
|
||||
const planFiles = fs.readdirSync(plansDir);
|
||||
nestedPlans = planFiles.filter(f => /^PLAN-\d+.*\.md$/i.test(f));
|
||||
nestedSummaries = planFiles.filter(f => /^SUMMARY-\d+.*\.md$/i.test(f));
|
||||
}
|
||||
|
||||
return {
|
||||
planCount: rootPlans.length + nestedPlans.length,
|
||||
summaryCount: rootSummaries.length + nestedSummaries.length,
|
||||
hasContext: phaseFiles.some(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md'),
|
||||
hasResearch: phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for a phase header (and its section) within the given content string.
|
||||
* Returns a result object if found (either a full match or a malformed_roadmap
|
||||
@@ -197,11 +219,11 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
||||
const dirMatch = _phaseDirNames.find(d => phaseTokenMatches(d, normalized));
|
||||
|
||||
if (dirMatch) {
|
||||
const phaseFiles = fs.readdirSync(path.join(phasesDir, dirMatch));
|
||||
planCount = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').length;
|
||||
summaryCount = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').length;
|
||||
hasContext = phaseFiles.some(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
|
||||
hasResearch = phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
|
||||
const counts = countPhasePlansAndSummaries(path.join(phasesDir, dirMatch));
|
||||
planCount = counts.planCount;
|
||||
summaryCount = counts.summaryCount;
|
||||
hasContext = counts.hasContext;
|
||||
hasResearch = counts.hasResearch;
|
||||
|
||||
if (summaryCount >= planCount && planCount > 0) diskStatus = 'complete';
|
||||
else if (summaryCount > 0) diskStatus = 'partial';
|
||||
|
||||
@@ -71,7 +71,8 @@ function routeStateCommand({ state, args, cwd, raw, parseNamedArgs, error }) {
|
||||
const { 'keep-recent': keepRecent, 'dry-run': dryRun } = parseNamedArgs(args, ['keep-recent'], ['dry-run']);
|
||||
state.cmdStatePrune(cwd, { keepRecent: keepRecent || '3', dryRun: !!dryRun }, raw);
|
||||
} else if (subcommand === 'complete-phase') {
|
||||
state.cmdStateCompletePhase(cwd, raw);
|
||||
const { phase: p } = parseNamedArgs(args, ['phase']);
|
||||
state.cmdStateCompletePhase(cwd, raw, p || args[2]);
|
||||
} else if (subcommand === 'milestone-switch') {
|
||||
const { milestone, name } = parseNamedArgs(args, ['milestone', 'name']);
|
||||
state.cmdStateMilestoneSwitch(cwd, milestone, name, raw);
|
||||
|
||||
@@ -1690,32 +1690,36 @@ function cmdStatePrune(cwd, options, raw) {
|
||||
* that the phase execution is finished and the project is ready for the next phase.
|
||||
* Implements the `gsd state complete-phase` subcommand (issue #2735).
|
||||
*/
|
||||
function cmdStateCompletePhase(cwd, raw) {
|
||||
function resolvePhaseIdForCompletePhase(content, overridePhase) {
|
||||
const candidate = overridePhase ||
|
||||
stateExtractField(content, 'Current Phase') ||
|
||||
stateExtractField(content, 'Phase') ||
|
||||
'';
|
||||
|
||||
// Accept canonical phase token only (e.g. 3, 03, 3A, 3.3, 10.2)
|
||||
const phaseMatch = String(candidate).match(/(\d+[A-Z]?(?:\.\d+)*)/i);
|
||||
return phaseMatch ? phaseMatch[1] : null;
|
||||
}
|
||||
|
||||
function cmdStateCompletePhase(cwd, raw, overridePhase) {
|
||||
const statePath = planningPaths(cwd).state;
|
||||
if (!fs.existsSync(statePath)) {
|
||||
output({ error: 'STATE.md not found' }, raw);
|
||||
return;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(statePath, 'utf-8');
|
||||
const resolvedPhase = resolvePhaseIdForCompletePhase(content, overridePhase);
|
||||
if (!resolvedPhase || /^phase$/i.test(resolvedPhase)) {
|
||||
output({ error: 'Unable to resolve current phase. Pass an explicit phase: state complete-phase --phase <N>' }, raw);
|
||||
return;
|
||||
}
|
||||
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const updated = [];
|
||||
let resolvedPhase = '?';
|
||||
|
||||
readModifyWriteStateMd(statePath, (content) => {
|
||||
// Read the current phase number for descriptive messages.
|
||||
//
|
||||
// The 'Phase' fallback can match the decorated body line under
|
||||
// `## Current Position` (e.g. `Phase: 01 (Foo) — EXECUTING`), which would
|
||||
// flow downstream into messy `Status: Phase 01 (Foo) — EXECUTING complete`
|
||||
// output. Strip everything past the leading numeric/decimal token so the
|
||||
// fallback path produces a clean phase identifier matching the canonical
|
||||
// `Current Phase` field. CodeRabbit nitpick on PR #2761.
|
||||
const rawPhase = stateExtractField(content, 'Current Phase') ||
|
||||
stateExtractField(content, 'Phase') ||
|
||||
'';
|
||||
const phaseToken = rawPhase.match(/^\s*([\w.-]+)/);
|
||||
const currentPhase = phaseToken ? phaseToken[1] : '?';
|
||||
resolvedPhase = currentPhase;
|
||||
const currentPhase = resolvedPhase;
|
||||
|
||||
// Update Status field
|
||||
const statusValue = `Phase ${currentPhase} complete`;
|
||||
|
||||
@@ -244,8 +244,8 @@ For each CONTEXT.md read: extract `<decisions>` (locked preferences), `<specific
|
||||
|
||||
**Spike/sketch findings:** Check for project-local skills:
|
||||
```bash
|
||||
SPIKE_FINDINGS=$(ls ./.claude/skills/spike-findings-*/SKILL.md 2>/dev/null | head -1)
|
||||
SKETCH_FINDINGS=$(ls ./.claude/skills/sketch-findings-*/SKILL.md 2>/dev/null | head -1)
|
||||
SPIKE_FINDINGS=$(ls ./.claude/skills/spike-findings-*/SKILL.md 2>/dev/null | head -1 || true)
|
||||
SKETCH_FINDINGS=$(ls ./.claude/skills/sketch-findings-*/SKILL.md 2>/dev/null | head -1 || true)
|
||||
RAW_SPIKES=$(ls .planning/spikes/MANIFEST.md 2>/dev/null)
|
||||
RAW_SKETCHES=$(ls .planning/sketches/MANIFEST.md 2>/dev/null)
|
||||
```
|
||||
|
||||
@@ -127,7 +127,7 @@ gsd-sdk query commit "docs: capture exploration — {topic_slug}" --files {file_
|
||||
**Outputs:** {count} artifact(s) created
|
||||
{list of created files}
|
||||
|
||||
Continue exploring with `/gsd-explore` or start working with `/gsd-next`.
|
||||
Continue exploring with `/gsd-explore` or start working with `/gsd-progress --next`.
|
||||
```
|
||||
|
||||
</process>
|
||||
|
||||
@@ -64,12 +64,13 @@ Usage: `/gsd-map-codebase`
|
||||
|
||||
### Phase Planning
|
||||
|
||||
**`/gsd-discuss-phase <number> [--chain | --analyze | --power] [--batch[=N]]`**
|
||||
**`/gsd-discuss-phase <number> [--chain | --analyze | --power | --assumptions] [--batch[=N]]`**
|
||||
Help articulate your vision for a phase before planning.
|
||||
|
||||
- `--chain` — chained-prompt discuss flow
|
||||
- `--analyze` — deep assumption analysis pass
|
||||
- `--power` — power-user mode with extended question set
|
||||
- `--assumptions` — surface Claude's implementation assumptions about the phase without an interactive session
|
||||
|
||||
- Captures how you imagine this phase working
|
||||
- Creates CONTEXT.md with your vision, essentials, and boundaries
|
||||
@@ -271,9 +272,10 @@ Resume work from previous session with full context restoration.
|
||||
|
||||
Usage: `/gsd-resume-work`
|
||||
|
||||
**`/gsd-pause-work`**
|
||||
**`/gsd-pause-work [--report]`**
|
||||
Create context handoff when pausing work mid-phase.
|
||||
|
||||
- `--report` — generate a post-session summary in `.planning/reports/` capturing commits, file changes, and phase progress
|
||||
- Creates .continue-here file with current state
|
||||
- Updates STATE.md session continuity section
|
||||
- Captures in-progress work context
|
||||
@@ -543,7 +545,7 @@ The commands above cover the most common day-to-day flows. Every command listed
|
||||
- **`/gsd-spec-phase <phase> [--auto] [--text]`** — Clarify WHAT a phase delivers with ambiguity scoring; produces a SPEC.md before discuss-phase.
|
||||
- **`/gsd-ai-integration-phase [phase]`** — Generate an AI-SPEC.md design contract for phases that involve building AI systems.
|
||||
- **`/gsd-ui-phase [phase]`** — Generate UI design contract (UI-SPEC.md) for frontend phases.
|
||||
- **`/gsd-import --from <filepath>`** — Ingest external plans with conflict detection against project decisions before writing anything.
|
||||
- **`/gsd-import --from <filepath> | --from-gsd2`** — Ingest external plans with conflict detection, or reverse-migrate a GSD-2 (`.gsd/`) project back to GSD v1 (`.planning/`) format.
|
||||
- **`/gsd-ingest-docs [path] [--mode new|merge] [--manifest <file>] [--resolve auto|interactive]`** — Bootstrap or merge a `.planning/` setup from existing ADRs, PRDs, SPECs, and docs in a repo.
|
||||
|
||||
### Planning & Execution
|
||||
@@ -579,7 +581,7 @@ The commands above cover the most common day-to-day flows. Every command listed
|
||||
|
||||
### Workflow & Orchestration
|
||||
|
||||
- **`/gsd-manager`** — Interactive command center for managing multiple phases from one terminal.
|
||||
- **`/gsd-manager [--analyze-deps]`** — Interactive command center for managing multiple phases from one terminal. `--analyze-deps` scans ROADMAP phases for dependency relationships before parallel execution.
|
||||
- **`/gsd-workspace [--new | --list | --remove] [name]`** — Manage GSD workspaces: create, list, or remove isolated workspace environments.
|
||||
- **`/gsd-workstreams`** — Manage parallel workstreams: list, create, switch, status, progress, complete, and resume.
|
||||
- **`/gsd-review-backlog`** — Review and promote backlog items to active milestone.
|
||||
|
||||
@@ -253,10 +253,10 @@ Check for existing spike and sketch work that should inform project setup:
|
||||
|
||||
```bash
|
||||
# Check for spike findings skill (project-local)
|
||||
SPIKE_SKILL=$(ls ./.claude/skills/spike-findings-*/SKILL.md 2>/dev/null | head -1)
|
||||
SPIKE_SKILL=$(ls ./.claude/skills/spike-findings-*/SKILL.md 2>/dev/null | head -1 || true)
|
||||
|
||||
# Check for sketch findings skill (project-local)
|
||||
SKETCH_SKILL=$(ls ./.claude/skills/sketch-findings-*/SKILL.md 2>/dev/null | head -1)
|
||||
SKETCH_SKILL=$(ls ./.claude/skills/sketch-findings-*/SKILL.md 2>/dev/null | head -1 || true)
|
||||
|
||||
# Check for raw spikes/sketches in .planning/
|
||||
HAS_SPIKES=$(ls .planning/spikes/MANIFEST.md 2>/dev/null)
|
||||
|
||||
@@ -121,7 +121,7 @@ Choice [S]:
|
||||
|
||||
**Goal:** Resolve plans that ran without producing summaries during Phase {src} execution
|
||||
**Source phase:** {src}
|
||||
**Deferred at:** {date} during /gsd-next advancement to Phase {dest}
|
||||
**Deferred at:** {date} during /gsd-progress --next advancement to Phase {dest}
|
||||
**Plans:**
|
||||
- [ ] {N}-{M}: {slug} (ran, no SUMMARY.md)
|
||||
```
|
||||
@@ -207,7 +207,7 @@ Display the determination:
|
||||
```
|
||||
|
||||
Then immediately invoke the determined command via SlashCommand.
|
||||
Do not ask for confirmation — the whole point of `/gsd-next` is zero-friction advancement.
|
||||
Do not ask for confirmation — the whole point of `/gsd-progress --next` is zero-friction advancement.
|
||||
</step>
|
||||
|
||||
</process>
|
||||
|
||||
@@ -668,8 +668,8 @@ REVIEWS_PATH=$(_gsd_field "$INIT" reviews_path)
|
||||
PATTERNS_PATH=$(_gsd_field "$INIT" patterns_path)
|
||||
|
||||
# Detect spike/sketch findings skills (project-local)
|
||||
SPIKE_FINDINGS_PATH=$(ls ./.claude/skills/spike-findings-*/SKILL.md 2>/dev/null | head -1)
|
||||
SKETCH_FINDINGS_PATH=$(ls ./.claude/skills/sketch-findings-*/SKILL.md 2>/dev/null | head -1)
|
||||
SPIKE_FINDINGS_PATH=$(ls ./.claude/skills/spike-findings-*/SKILL.md 2>/dev/null | head -1 || true)
|
||||
SKETCH_FINDINGS_PATH=$(ls ./.claude/skills/sketch-findings-*/SKILL.md 2>/dev/null | head -1 || true)
|
||||
```
|
||||
|
||||
## 7.5. Verify Nyquist Artifacts
|
||||
|
||||
@@ -275,7 +275,7 @@ PHASE_HAS_UI=$(echo "$PHASE_SECTION" | grep -qi "UI hint.*yes" && echo "true" ||
|
||||
**Also available:**
|
||||
- `/gsd-ui-phase {phase}` — generate UI design contract (recommended for frontend phases)
|
||||
- `/gsd-plan-phase {phase}` — skip discussion, plan directly
|
||||
- `/gsd-list-phase-assumptions {phase}` — see Claude's assumptions
|
||||
- `/gsd-discuss-phase {phase}` — include assumptions check before planning
|
||||
|
||||
---
|
||||
```
|
||||
@@ -297,7 +297,7 @@ PHASE_HAS_UI=$(echo "$PHASE_SECTION" | grep -qi "UI hint.*yes" && echo "true" ||
|
||||
|
||||
**Also available:**
|
||||
- `/gsd-plan-phase {phase} ${GSD_WS}` — skip discussion, plan directly
|
||||
- `/gsd-list-phase-assumptions {phase} ${GSD_WS}` — see Claude's assumptions
|
||||
- `/gsd-discuss-phase {phase} ${GSD_WS}` — include assumptions check before planning
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
@@ -128,8 +128,8 @@ If `$VALIDATE_MODE` only:
|
||||
if ! command -v gsd-sdk &>/dev/null; then
|
||||
echo "⚠ gsd-sdk not found in PATH — /gsd-quick requires it."
|
||||
echo ""
|
||||
echo "Install the GSD SDK:"
|
||||
echo " npm install -g @gsd-build/sdk"
|
||||
echo "Install the query-capable GSD SDK CLI:"
|
||||
echo " npm install -g get-shit-done-cc"
|
||||
echo ""
|
||||
echo "Or update GSD to get the latest packages:"
|
||||
echo " /gsd-update"
|
||||
|
||||
@@ -225,9 +225,11 @@ Wait for user selection.
|
||||
</step>
|
||||
|
||||
<step name="route_to_workflow">
|
||||
Based on user selection, route to appropriate workflow:
|
||||
Based on user selection, route to appropriate workflow.
|
||||
|
||||
- **Execute plan** → Show command for user to run after clearing:
|
||||
Resume-specific exception: do **not** emit `/clear then:` here. Resume is already a session-entry flow, so the next command should be shown directly.
|
||||
|
||||
- **Execute plan** → Show direct next command:
|
||||
```
|
||||
---
|
||||
|
||||
@@ -235,13 +237,11 @@ Based on user selection, route to appropriate workflow:
|
||||
|
||||
**{phase}-{plan}: [Plan Name]** — [objective from PLAN.md]
|
||||
|
||||
`/clear` then:
|
||||
|
||||
`/gsd-execute-phase {phase} ${GSD_WS}`
|
||||
|
||||
---
|
||||
```
|
||||
- **Plan phase** → Show command for user to run after clearing:
|
||||
- **Plan phase** → Show direct next command:
|
||||
```
|
||||
---
|
||||
|
||||
@@ -249,8 +249,6 @@ Based on user selection, route to appropriate workflow:
|
||||
|
||||
**Phase [N]: [Name]** — [Goal from ROADMAP.md]
|
||||
|
||||
`/clear` then:
|
||||
|
||||
`/gsd-plan-phase [phase-number] ${GSD_WS}`
|
||||
|
||||
---
|
||||
|
||||
@@ -31,7 +31,7 @@ Parse JSON for: `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded
|
||||
|
||||
Detect sketch findings:
|
||||
```bash
|
||||
SKETCH_FINDINGS_PATH=$(ls ./.claude/skills/sketch-findings-*/SKILL.md 2>/dev/null | head -1)
|
||||
SKETCH_FINDINGS_PATH=$(ls ./.claude/skills/sketch-findings-*/SKILL.md 2>/dev/null | head -1 || true)
|
||||
```
|
||||
|
||||
Resolve UI agent models:
|
||||
|
||||
@@ -486,10 +486,15 @@ const path = require('path');
|
||||
for (const relPath of custom_files) {
|
||||
const src = path.join(runtimeDir, relPath);
|
||||
const dst = path.join(backupDir, relPath);
|
||||
if (fs.existsSync(src)) {
|
||||
if (!fs.existsSync(src)) continue;
|
||||
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
||||
fs.copyFileSync(src, dst);
|
||||
console.log(' Backed up: ' + relPath);
|
||||
} catch (err) {
|
||||
const code = err && err.code ? String(err.code) : 'ERROR';
|
||||
console.log(' Skipped (non-fatal): ' + relPath + ' [' + code + ']');
|
||||
}
|
||||
}
|
||||
JSEOF
|
||||
|
||||
@@ -237,6 +237,26 @@ describe('loadConfig', () => {
|
||||
expect(config).toEqual(CONFIG_DEFAULTS);
|
||||
});
|
||||
|
||||
it('maps legacy top-level branching_strategy into git.branching_strategy', async () => {
|
||||
await writeFile(
|
||||
join(tmpDir, '.planning', 'config.json'),
|
||||
JSON.stringify({ branching_strategy: 'phase' }),
|
||||
);
|
||||
|
||||
const config = await loadConfig(tmpDir);
|
||||
expect(config.git.branching_strategy).toBe('phase');
|
||||
});
|
||||
|
||||
it('git.branching_strategy overrides legacy top-level branching_strategy when both are present', async () => {
|
||||
await writeFile(
|
||||
join(tmpDir, '.planning', 'config.json'),
|
||||
JSON.stringify({ branching_strategy: 'phase', git: { branching_strategy: 'milestone' } }),
|
||||
);
|
||||
|
||||
const config = await loadConfig(tmpDir);
|
||||
expect(config.git.branching_strategy).toBe('milestone');
|
||||
});
|
||||
|
||||
it('does not mutate CONFIG_DEFAULTS between calls', async () => {
|
||||
const before = structuredClone(CONFIG_DEFAULTS);
|
||||
|
||||
|
||||
@@ -177,11 +177,16 @@ export async function loadConfig(projectDir: string, workstream?: string): Promi
|
||||
}
|
||||
|
||||
function mergeDefaults(parsed: Record<string, unknown>): GSDConfig {
|
||||
const legacyBranchingStrategy = typeof parsed.branching_strategy === 'string'
|
||||
? parsed.branching_strategy
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
...structuredClone(CONFIG_DEFAULTS),
|
||||
...parsed,
|
||||
git: {
|
||||
...CONFIG_DEFAULTS.git,
|
||||
...(legacyBranchingStrategy ? { branching_strategy: legacyBranchingStrategy } : {}),
|
||||
...(parsed.git as Partial<GitConfig> ?? {}),
|
||||
},
|
||||
workflow: {
|
||||
|
||||
21
sdk/src/gsd-tools-error.test.ts
Normal file
21
sdk/src/gsd-tools-error.test.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { GSDToolsError } from './gsd-tools-error.js';
|
||||
|
||||
describe('GSDToolsError constructors', () => {
|
||||
it('builds timeout-classified errors', () => {
|
||||
const err = GSDToolsError.timeout('timeout', 'state', ['load'], '', 1000);
|
||||
expect(err.classification).toEqual({ kind: 'timeout', timeoutMs: 1000 });
|
||||
expect(err.exitCode).toBeNull();
|
||||
});
|
||||
|
||||
it('builds failure-classified errors', () => {
|
||||
const err = GSDToolsError.failure('boom', 'state', ['load'], 1);
|
||||
expect(err.classification).toEqual({ kind: 'failure' });
|
||||
expect(err.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
it('defaults direct constructor to failure classification', () => {
|
||||
const err = new GSDToolsError('boom', 'state', ['load'], 1, 'stderr');
|
||||
expect(err.classification).toEqual({ kind: 'failure' });
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,16 @@
|
||||
export interface GSDToolsErrorClassification {
|
||||
kind: 'timeout' | 'failure';
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
function timeoutClassification(timeoutMs?: number): GSDToolsErrorClassification {
|
||||
return timeoutMs === undefined ? { kind: 'timeout' } : { kind: 'timeout', timeoutMs };
|
||||
}
|
||||
|
||||
function failureClassification(): GSDToolsErrorClassification {
|
||||
return { kind: 'failure' };
|
||||
}
|
||||
|
||||
export class GSDToolsError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
@@ -5,9 +18,48 @@ export class GSDToolsError extends Error {
|
||||
public readonly args: string[],
|
||||
public readonly exitCode: number | null,
|
||||
public readonly stderr: string,
|
||||
options?: { cause?: unknown },
|
||||
options?: { cause?: unknown; classification?: GSDToolsErrorClassification },
|
||||
) {
|
||||
super(message, options);
|
||||
this.name = 'GSDToolsError';
|
||||
this.classification = options?.classification ?? failureClassification();
|
||||
}
|
||||
|
||||
static timeout(
|
||||
message: string,
|
||||
command: string,
|
||||
args: string[],
|
||||
stderr = '',
|
||||
timeoutMs?: number,
|
||||
options?: { cause?: unknown; exitCode?: number | null },
|
||||
): GSDToolsError {
|
||||
return new GSDToolsError(
|
||||
message,
|
||||
command,
|
||||
args,
|
||||
options?.exitCode ?? null,
|
||||
stderr,
|
||||
{ cause: options?.cause, classification: timeoutClassification(timeoutMs) },
|
||||
);
|
||||
}
|
||||
|
||||
static failure(
|
||||
message: string,
|
||||
command: string,
|
||||
args: string[],
|
||||
exitCode: number | null,
|
||||
stderr = '',
|
||||
options?: { cause?: unknown },
|
||||
): GSDToolsError {
|
||||
return new GSDToolsError(
|
||||
message,
|
||||
command,
|
||||
args,
|
||||
exitCode,
|
||||
stderr,
|
||||
{ cause: options?.cause, classification: failureClassification() },
|
||||
);
|
||||
}
|
||||
|
||||
public readonly classification: GSDToolsErrorClassification;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
import type { InitNewProjectInfo, PhaseOpInfo, PhasePlanIndex, RoadmapAnalysis } from './types.js';
|
||||
import type { GSDEventStream } from './event-stream.js';
|
||||
import { toGSDToolsError } from './query-tools-error-mapper.js';
|
||||
import { toToolsErrorFromUnknown } from './query-tools-error-factory.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';
|
||||
@@ -107,10 +107,6 @@ export class GSDTools {
|
||||
return resolveQueryCommand(command, args, this.registry);
|
||||
}
|
||||
|
||||
private toToolsError(command: string, args: string[], err: unknown): GSDToolsError {
|
||||
return toGSDToolsError(command, args, err);
|
||||
}
|
||||
|
||||
private async dispatchNativeHotpath(
|
||||
legacyCommand: string,
|
||||
legacyArgs: string[],
|
||||
@@ -118,17 +114,22 @@ export class GSDTools {
|
||||
registryArgs: string[],
|
||||
mode: 'json' | 'raw',
|
||||
): Promise<unknown> {
|
||||
try {
|
||||
return await this.nativeHotpathAdapter.dispatch(
|
||||
return this.executeWithToolsError(legacyCommand, legacyArgs, () =>
|
||||
this.nativeHotpathAdapter.dispatch(
|
||||
legacyCommand,
|
||||
legacyArgs,
|
||||
registryCommand,
|
||||
registryArgs,
|
||||
mode,
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
private async executeWithToolsError<T>(command: string, args: string[], work: () => Promise<T>): Promise<T> {
|
||||
try {
|
||||
return await work();
|
||||
} catch (err) {
|
||||
if (err instanceof GSDToolsError) throw err;
|
||||
throw this.toToolsError(legacyCommand, legacyArgs, err);
|
||||
throw toToolsErrorFromUnknown(command, args, err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,12 +140,7 @@ export class GSDTools {
|
||||
* Handles the `@file:` prefix pattern for large results.
|
||||
*/
|
||||
async exec(command: string, args: string[] = []): Promise<unknown> {
|
||||
try {
|
||||
return await this.commandExecutor.exec(command, args, 'json');
|
||||
} catch (err) {
|
||||
if (err instanceof GSDToolsError) throw err;
|
||||
throw this.toToolsError(command, args, err);
|
||||
}
|
||||
return this.executeWithToolsError(command, args, () => this.commandExecutor.exec(command, args, 'json'));
|
||||
}
|
||||
|
||||
// ─── Raw exec (no JSON parsing) ───────────────────────────────────────
|
||||
@@ -154,12 +150,10 @@ export class GSDTools {
|
||||
* Use for commands like `config-set` that return plain text, not JSON.
|
||||
*/
|
||||
async execRaw(command: string, args: string[] = []): Promise<string> {
|
||||
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);
|
||||
}
|
||||
return this.executeWithToolsError(command, args, async () => {
|
||||
const out = await this.commandExecutor.exec(command, args, 'raw');
|
||||
return typeof out === 'string' ? out : String(out ?? '');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { GSDToolsError } from './gsd-tools-error.js';
|
||||
import { QueryRegistry } from './query/registry.js';
|
||||
import { GSDTransport } from './gsd-transport.js';
|
||||
|
||||
@@ -119,6 +120,36 @@ describe('GSDTransport', () => {
|
||||
expect(adapters.execSubprocessJson).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not fallback after typed timeout native error', async () => {
|
||||
const registry = new QueryRegistry();
|
||||
registry.register('state.load', async () => ({ data: { ok: true } }));
|
||||
|
||||
const timeoutError = GSDToolsError.timeout('native timed out', 'state', ['load'], '', 500);
|
||||
const adapters = {
|
||||
dispatchNative: vi.fn(async () => {
|
||||
throw timeoutError;
|
||||
}),
|
||||
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.toBe(timeoutError);
|
||||
|
||||
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' } }));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { QueryResult } from './query/utils.js';
|
||||
import type { QueryRegistry } from './query/registry.js';
|
||||
import type { TransportMode } from './gsd-transport-policy.js';
|
||||
import { toFailureSignal } from './query-failure-classification.js';
|
||||
|
||||
export interface TransportRequest {
|
||||
legacyCommand: string;
|
||||
@@ -24,12 +25,6 @@ export interface TransportPolicyLike {
|
||||
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,
|
||||
@@ -37,33 +32,48 @@ export class GSDTransport {
|
||||
) {}
|
||||
|
||||
async run(request: TransportRequest, policy: TransportPolicyLike): Promise<unknown> {
|
||||
const forceSubprocess = Boolean(request.workstream);
|
||||
|
||||
if (!forceSubprocess && policy.preferNative && this.registry.has(request.registryCommand)) {
|
||||
if (this.shouldUseNative(request, policy)) {
|
||||
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;
|
||||
return this.projectNativeOutput(request, 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 (this.shouldRethrowNativeError(error, policy)) throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return this.dispatchSubprocess(request);
|
||||
}
|
||||
|
||||
private shouldUseNative(request: TransportRequest, policy: TransportPolicyLike): boolean {
|
||||
const forceSubprocess = Boolean(request.workstream);
|
||||
return !forceSubprocess && policy.preferNative && this.registry.has(request.registryCommand);
|
||||
}
|
||||
|
||||
private shouldRethrowNativeError(error: unknown, policy: TransportPolicyLike): boolean {
|
||||
if (!policy.allowFallbackToSubprocess) return true;
|
||||
// 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).
|
||||
return toFailureSignal(error).kind === 'timeout';
|
||||
}
|
||||
|
||||
private dispatchSubprocess(request: TransportRequest): Promise<unknown> {
|
||||
if (request.mode === 'raw') {
|
||||
return this.adapters.execSubprocessRaw(request.legacyCommand, request.legacyArgs);
|
||||
}
|
||||
return this.adapters.execSubprocessJson(request.legacyCommand, request.legacyArgs);
|
||||
}
|
||||
|
||||
private projectNativeOutput(request: TransportRequest, data: unknown): unknown {
|
||||
if (request.mode === 'raw') {
|
||||
if (this.adapters.formatNativeRaw) {
|
||||
return this.adapters.formatNativeRaw(request.registryCommand, data).trim();
|
||||
}
|
||||
return this.toRaw(data);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private toRaw(data: unknown): string {
|
||||
if (typeof data === 'string') return data.trim();
|
||||
const json = JSON.stringify(data, null, 2);
|
||||
|
||||
23
sdk/src/query-failure-classification.test.ts
Normal file
23
sdk/src/query-failure-classification.test.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
errorMessage,
|
||||
timeoutMessage,
|
||||
toFailureSignal,
|
||||
} from './query-failure-classification.js';
|
||||
import { GSDToolsError } from './gsd-tools-error.js';
|
||||
|
||||
describe('query failure classification', () => {
|
||||
it('extracts timeout metadata from message', () => {
|
||||
const msg = timeoutMessage('state', ['load'], 30000);
|
||||
expect(toFailureSignal(new Error(msg))).toEqual({ kind: 'timeout', message: msg, timeoutMs: 30000 });
|
||||
});
|
||||
it('normalizes unknown error values', () => {
|
||||
expect(errorMessage('boom')).toBe('boom');
|
||||
expect(errorMessage(new Error('x'))).toBe('x');
|
||||
});
|
||||
|
||||
it('prefers typed classification from GSDToolsError', () => {
|
||||
const err = GSDToolsError.timeout('x', 'state', ['load'], '', 2000);
|
||||
expect(toFailureSignal(err)).toEqual({ kind: 'timeout', message: 'x', timeoutMs: 2000 });
|
||||
});
|
||||
});
|
||||
42
sdk/src/query-failure-classification.ts
Normal file
42
sdk/src/query-failure-classification.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { GSDToolsError } from './gsd-tools-error.js';
|
||||
|
||||
export interface QueryFailureSignal {
|
||||
kind: 'timeout' | 'failure';
|
||||
message: string;
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
export function errorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
function parseTimeoutMs(message: string): number | undefined {
|
||||
const m = message.match(/timed out after\s+(\d+)ms/i);
|
||||
if (!m) return undefined;
|
||||
const n = Number.parseInt(m[1], 10);
|
||||
return Number.isFinite(n) ? n : undefined;
|
||||
}
|
||||
|
||||
function isTimeoutMessage(message: string): boolean {
|
||||
return /timed out after/i.test(message);
|
||||
}
|
||||
|
||||
export function timeoutMessage(command: string, args: string[], timeoutMs: number): string {
|
||||
return `gsd-tools timed out after ${timeoutMs}ms: ${command} ${args.join(' ')}`;
|
||||
}
|
||||
|
||||
export function toFailureSignal(error: unknown): QueryFailureSignal {
|
||||
if (error instanceof GSDToolsError && error.classification) {
|
||||
return {
|
||||
kind: error.classification.kind,
|
||||
message: error.message,
|
||||
timeoutMs: error.classification.timeoutMs,
|
||||
};
|
||||
}
|
||||
|
||||
const message = errorMessage(error);
|
||||
if (isTimeoutMessage(message)) {
|
||||
return { kind: 'timeout', message, timeoutMs: parseTimeoutMs(message) };
|
||||
}
|
||||
return { kind: 'failure', message };
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
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';
|
||||
import { createQueryNativeErrorFactory, createQueryToolsErrorFactory } from './query-tools-error-factory.js';
|
||||
|
||||
export interface GSDToolsRuntime {
|
||||
registry: ReturnType<typeof createRegistry>;
|
||||
@@ -28,30 +27,33 @@ export function createGSDToolsRuntime(opts: {
|
||||
}): GSDToolsRuntime {
|
||||
const registry = createRegistry(opts.eventStream, opts.sessionId);
|
||||
|
||||
const queryToolsErrorFactory = createQueryToolsErrorFactory();
|
||||
|
||||
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),
|
||||
...queryToolsErrorFactory,
|
||||
});
|
||||
|
||||
const nativeErrorFactory = createQueryNativeErrorFactory(opts.timeoutMs);
|
||||
|
||||
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, ''),
|
||||
...nativeErrorFactory,
|
||||
});
|
||||
|
||||
const transport = new GSDTransport(registry, {
|
||||
dispatchNative: async (request) => nativeDirectAdapter.dispatchResult(
|
||||
dispatchNative: (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),
|
||||
),
|
||||
execSubprocessJson: (legacyCommand, legacyArgs) => subprocessAdapter.execJson(legacyCommand, legacyArgs),
|
||||
execSubprocessRaw: (legacyCommand, legacyArgs) => subprocessAdapter.execRaw(legacyCommand, legacyArgs),
|
||||
formatNativeRaw: (registryCommand, data) => formatQueryRawOutput(registryCommand, data),
|
||||
});
|
||||
|
||||
|
||||
35
sdk/src/query-native-direct-adapter.test.ts
Normal file
35
sdk/src/query-native-direct-adapter.test.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { GSDToolsError } from './gsd-tools-error.js';
|
||||
import { QueryNativeDirectAdapter } from './query-native-direct-adapter.js';
|
||||
|
||||
describe('QueryNativeDirectAdapter', () => {
|
||||
it('wraps native failures as typed failure errors', async () => {
|
||||
const adapter = new QueryNativeDirectAdapter({
|
||||
timeoutMs: 1000,
|
||||
dispatch: async () => {
|
||||
throw new Error('boom');
|
||||
},
|
||||
createNativeTimeoutError: (message, command, args) => GSDToolsError.timeout(message, command, args),
|
||||
createNativeFailureError: (message, command, args, cause) => GSDToolsError.failure(message, command, args, 1, '', { cause }),
|
||||
});
|
||||
|
||||
await expect(adapter.dispatchJson('state', ['load'], 'state.load', [])).rejects.toMatchObject({
|
||||
classification: GSDToolsError.failure('x', 'state', ['load'], 1).classification,
|
||||
command: 'state',
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves timeout errors', async () => {
|
||||
const timeoutErr = GSDToolsError.timeout('timeout', 'state', ['load']);
|
||||
const adapter = new QueryNativeDirectAdapter({
|
||||
timeoutMs: 1000,
|
||||
dispatch: async () => {
|
||||
throw timeoutErr;
|
||||
},
|
||||
createNativeTimeoutError: (message, command, args) => GSDToolsError.timeout(message, command, args),
|
||||
createNativeFailureError: (message, command, args, cause) => GSDToolsError.failure(message, command, args, 1, '', { cause }),
|
||||
});
|
||||
|
||||
await expect(adapter.dispatchJson('state', ['load'], 'state.load', [])).rejects.toBe(timeoutErr);
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,12 @@
|
||||
import { formatQueryRawOutput } from './query-raw-output-projection.js';
|
||||
import { GSDToolsError } from './gsd-tools-error.js';
|
||||
import { errorMessage, timeoutMessage } from './query-failure-classification.js';
|
||||
import type { QueryNativeErrorFactory } from './query-tools-error-factory.js';
|
||||
import type { QueryResult } from './query/utils.js';
|
||||
|
||||
export interface QueryNativeDirectAdapterDeps {
|
||||
export interface QueryNativeDirectAdapterDeps extends QueryNativeErrorFactory {
|
||||
timeoutMs: number;
|
||||
dispatch: (registryCommand: string, registryArgs: string[]) => Promise<QueryResult>;
|
||||
createTimeoutError: (message: string, command: string, args: string[]) => Error;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -14,17 +16,35 @@ 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));
|
||||
try {
|
||||
return await this.withTimeout(legacyCommand, legacyArgs, this.deps.dispatch(registryCommand, registryArgs));
|
||||
} catch (error) {
|
||||
throw this.toNativeDispatchError(legacyCommand, legacyArgs, error);
|
||||
}
|
||||
}
|
||||
|
||||
async dispatchJson(legacyCommand: string, legacyArgs: string[], registryCommand: string, registryArgs: string[]): Promise<unknown> {
|
||||
return this.dispatchData(legacyCommand, legacyArgs, registryCommand, registryArgs);
|
||||
}
|
||||
|
||||
async dispatchRaw(legacyCommand: string, legacyArgs: string[], registryCommand: string, registryArgs: string[]): Promise<string> {
|
||||
const data = await this.dispatchData(legacyCommand, legacyArgs, registryCommand, registryArgs);
|
||||
return formatQueryRawOutput(registryCommand, data).trim();
|
||||
}
|
||||
|
||||
private async dispatchData(
|
||||
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 toNativeDispatchError(legacyCommand: string, legacyArgs: string[], error: unknown): GSDToolsError {
|
||||
if (error instanceof GSDToolsError) return error;
|
||||
return this.deps.createNativeFailureError(errorMessage(error), legacyCommand, legacyArgs, error);
|
||||
}
|
||||
|
||||
private async withTimeout<T>(legacyCommand: string, legacyArgs: string[], work: Promise<T>): Promise<T> {
|
||||
@@ -32,8 +52,8 @@ export class QueryNativeDirectAdapter {
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
timeoutId = setTimeout(() => {
|
||||
reject(
|
||||
this.deps.createTimeoutError(
|
||||
`gsd-tools timed out after ${this.deps.timeoutMs}ms: ${legacyCommand} ${legacyArgs.join(' ')}`,
|
||||
this.deps.createNativeTimeoutError(
|
||||
timeoutMessage(legacyCommand, legacyArgs, this.deps.timeoutMs),
|
||||
legacyCommand,
|
||||
legacyArgs,
|
||||
),
|
||||
|
||||
@@ -19,11 +19,25 @@ export class QueryNativeHotpathAdapter {
|
||||
mode: 'json' | 'raw',
|
||||
): Promise<unknown> {
|
||||
if (!this.shouldUseNativeQuery()) {
|
||||
return mode === 'raw'
|
||||
? this.execRawFallback(legacyCommand, legacyArgs)
|
||||
: this.execJsonFallback(legacyCommand, legacyArgs);
|
||||
return this.dispatchFallback(legacyCommand, legacyArgs, mode);
|
||||
}
|
||||
|
||||
return this.dispatchNative(legacyCommand, legacyArgs, registryCommand, registryArgs, mode);
|
||||
}
|
||||
|
||||
private dispatchFallback(legacyCommand: string, legacyArgs: string[], mode: 'json' | 'raw'): Promise<unknown> {
|
||||
return mode === 'raw'
|
||||
? this.execRawFallback(legacyCommand, legacyArgs)
|
||||
: this.execJsonFallback(legacyCommand, legacyArgs);
|
||||
}
|
||||
|
||||
private dispatchNative(
|
||||
legacyCommand: string,
|
||||
legacyArgs: string[],
|
||||
registryCommand: string,
|
||||
registryArgs: string[],
|
||||
mode: 'json' | 'raw',
|
||||
): Promise<unknown> {
|
||||
return mode === 'raw'
|
||||
? this.nativeDirect.dispatchRaw(legacyCommand, legacyArgs, registryCommand, registryArgs)
|
||||
: this.nativeDirect.dispatchJson(legacyCommand, legacyArgs, registryCommand, registryArgs);
|
||||
|
||||
@@ -31,4 +31,9 @@ describe('formatQueryRawOutput', () => {
|
||||
expect(formatQueryRawOutput('state begin-phase', { updated: ['x'] })).toBe('true');
|
||||
expect(formatQueryRawOutput('state begin-phase', { updated: [] })).toBe('false');
|
||||
});
|
||||
|
||||
it('never returns undefined for non-JSON top-level values', () => {
|
||||
expect(formatQueryRawOutput('commit', undefined)).toBe('undefined');
|
||||
expect(formatQueryRawOutput('commit', Symbol('x'))).toBe('Symbol(x)');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { formatStateLoadRawStdout } from './query/state-project-load.js';
|
||||
|
||||
function safeStringify(value: unknown): string {
|
||||
const out = JSON.stringify(value, null, 2);
|
||||
return out ?? String(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Raw output projection for native query results.
|
||||
* Owns CLI-facing string contracts for raw mode commands.
|
||||
@@ -11,7 +16,7 @@ export function formatQueryRawOutput(registryCommand: string, data: unknown): st
|
||||
|
||||
if (registryCommand === 'commit') {
|
||||
if (data == null || typeof data !== 'object' || Array.isArray(data)) {
|
||||
return JSON.stringify(data, null, 2);
|
||||
return safeStringify(data);
|
||||
}
|
||||
const d = data as Record<string, unknown>;
|
||||
if (d.committed === true) {
|
||||
@@ -32,12 +37,12 @@ export function formatQueryRawOutput(registryCommand: string, data: unknown): st
|
||||
}
|
||||
return r || 'nothing';
|
||||
}
|
||||
return JSON.stringify(data, null, 2);
|
||||
return safeStringify(data);
|
||||
}
|
||||
|
||||
if (registryCommand === 'config-set') {
|
||||
if (data == null || typeof data !== 'object' || Array.isArray(data)) {
|
||||
return JSON.stringify(data, null, 2);
|
||||
return safeStringify(data);
|
||||
}
|
||||
const d = data as Record<string, unknown>;
|
||||
if ((d.updated === true || d.set === true) && d.key !== undefined) {
|
||||
@@ -50,12 +55,12 @@ export function formatQueryRawOutput(registryCommand: string, data: unknown): st
|
||||
}
|
||||
return `${d.key}=${String(v)}`;
|
||||
}
|
||||
return JSON.stringify(data, null, 2);
|
||||
return safeStringify(data);
|
||||
}
|
||||
|
||||
if (registryCommand === 'state.begin-phase' || registryCommand === 'state begin-phase') {
|
||||
if (data == null || typeof data !== 'object' || Array.isArray(data)) {
|
||||
return JSON.stringify(data, null, 2);
|
||||
return safeStringify(data);
|
||||
}
|
||||
const d = data as Record<string, unknown>;
|
||||
const u = d.updated as string[] | undefined;
|
||||
@@ -65,5 +70,5 @@ export function formatQueryRawOutput(registryCommand: string, data: unknown): st
|
||||
if (typeof data === 'string') {
|
||||
return data;
|
||||
}
|
||||
return JSON.stringify(data, null, 2);
|
||||
return safeStringify(data);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,9 @@ describe('QuerySubprocessAdapter', () => {
|
||||
projectDir: dir,
|
||||
gsdToolsPath,
|
||||
timeoutMs: 2_000,
|
||||
createToolsError: (message, command, args, exitCode, stderr) =>
|
||||
createTimeoutError: (message, command, args, stderr) =>
|
||||
new FakeToolsError(message, command, args, null, stderr) as never,
|
||||
createFailureError: (message, command, args, exitCode, stderr) =>
|
||||
new FakeToolsError(message, command, args, exitCode, stderr) as never,
|
||||
});
|
||||
}
|
||||
@@ -62,6 +64,17 @@ describe('QuerySubprocessAdapter', () => {
|
||||
await expect(adapter.execJson('state', ['load'])).resolves.toEqual({ from: 'file' });
|
||||
});
|
||||
|
||||
it('execJson resolves relative @file output against projectDir', async () => {
|
||||
const relDir = join(dir, '.planning');
|
||||
await mkdir(relDir, { recursive: true });
|
||||
const relFile = join(relDir, 'out.json');
|
||||
await writeFile(relFile, JSON.stringify({ from: 'relative-file' }));
|
||||
const script = await createScript('file-relative.cjs', `process.stdout.write('@file:.planning/out.json');`);
|
||||
const adapter = createAdapter(script);
|
||||
|
||||
await expect(adapter.execJson('state', ['load'])).resolves.toEqual({ from: 'relative-file' });
|
||||
});
|
||||
|
||||
it('execRaw returns trimmed stdout', async () => {
|
||||
const script = await createScript('raw.cjs', `process.stdout.write(' hello ');`);
|
||||
const adapter = createAdapter(script);
|
||||
|
||||
@@ -1,27 +1,21 @@
|
||||
import { execFile } from 'node:child_process';
|
||||
import { isAbsolute, resolve } from 'node:path';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import type { GSDToolsError } from './gsd-tools-error.js';
|
||||
import { timeoutMessage } from './query-failure-classification.js';
|
||||
import type { QueryToolsErrorFactory } from './query-tools-error-factory.js';
|
||||
|
||||
export interface QuerySubprocessAdapterDeps {
|
||||
export interface QuerySubprocessAdapterDeps extends QueryToolsErrorFactory {
|
||||
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];
|
||||
const fullArgs = this.commandArgs(command, args);
|
||||
|
||||
return new Promise<unknown>((resolve, reject) => {
|
||||
const child = execFile(
|
||||
@@ -37,28 +31,7 @@ export class QuerySubprocessAdapter {
|
||||
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,
|
||||
),
|
||||
);
|
||||
reject(this.processExecutionError(command, args, error, stderrStr));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -68,7 +41,7 @@ export class QuerySubprocessAdapter {
|
||||
resolve(parsed);
|
||||
} catch (parseErr) {
|
||||
reject(
|
||||
this.deps.createToolsError(
|
||||
this.deps.createFailureError(
|
||||
`Failed to parse gsd-tools output for "${command}": ${parseErr instanceof Error ? parseErr.message : String(parseErr)}\nRaw output: ${raw.slice(0, 500)}`,
|
||||
command,
|
||||
args,
|
||||
@@ -81,14 +54,13 @@ export class QuerySubprocessAdapter {
|
||||
);
|
||||
|
||||
child.on('error', (err) => {
|
||||
reject(this.deps.createToolsError(`Failed to execute gsd-tools: ${err.message}`, command, args, null, ''));
|
||||
reject(this.processSpawnError(command, args, err));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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'];
|
||||
const fullArgs = [...this.commandArgs(command, args), '--raw'];
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const child = execFile(
|
||||
@@ -103,27 +75,7 @@ export class QuerySubprocessAdapter {
|
||||
(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,
|
||||
),
|
||||
);
|
||||
reject(this.processExecutionError(command, args, error, stderrStr));
|
||||
return;
|
||||
}
|
||||
resolve((stdout?.toString() ?? '').trim());
|
||||
@@ -131,11 +83,45 @@ export class QuerySubprocessAdapter {
|
||||
);
|
||||
|
||||
child.on('error', (err) => {
|
||||
reject(this.deps.createToolsError(`Failed to execute gsd-tools: ${err.message}`, command, args, null, ''));
|
||||
reject(this.processSpawnError(command, args, err));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private commandArgs(command: string, args: string[]): string[] {
|
||||
const wsArgs = this.deps.workstream ? ['--ws', this.deps.workstream] : [];
|
||||
return [this.deps.gsdToolsPath, command, ...args, ...wsArgs];
|
||||
}
|
||||
|
||||
private processExecutionError(
|
||||
command: string,
|
||||
args: string[],
|
||||
error: Error & { code?: unknown; status?: number; killed?: boolean },
|
||||
stderrStr: string,
|
||||
) {
|
||||
if (error.killed || (error as NodeJS.ErrnoException).code === 'ETIMEDOUT') {
|
||||
return this.deps.createTimeoutError(
|
||||
timeoutMessage(command, args, this.deps.timeoutMs),
|
||||
command,
|
||||
args,
|
||||
stderrStr,
|
||||
this.deps.timeoutMs,
|
||||
);
|
||||
}
|
||||
|
||||
return this.deps.createFailureError(
|
||||
`gsd-tools exited with code ${error.code ?? 'unknown'}: ${command} ${args.join(' ')}${stderrStr ? `\n${stderrStr}` : ''}`,
|
||||
command,
|
||||
args,
|
||||
typeof error.code === 'number' ? error.code : error.status ?? 1,
|
||||
stderrStr,
|
||||
);
|
||||
}
|
||||
|
||||
private processSpawnError(command: string, args: string[], err: Error) {
|
||||
return this.deps.createFailureError(`Failed to execute gsd-tools: ${err.message}`, command, args, null, '');
|
||||
}
|
||||
|
||||
private async parseOutput(raw: string): Promise<unknown> {
|
||||
const trimmed = raw.trim();
|
||||
|
||||
@@ -146,11 +132,12 @@ export class QuerySubprocessAdapter {
|
||||
let jsonStr = trimmed;
|
||||
if (jsonStr.startsWith('@file:')) {
|
||||
const filePath = jsonStr.slice(6).trim();
|
||||
const resolvedPath = isAbsolute(filePath) ? filePath : resolve(this.deps.projectDir, filePath);
|
||||
try {
|
||||
jsonStr = await readFile(filePath, 'utf-8');
|
||||
jsonStr = await readFile(resolvedPath, '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}`);
|
||||
throw new Error(`Failed to read gsd-tools @file: indirection at "${resolvedPath}": ${reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
35
sdk/src/query-tools-error-factory.test.ts
Normal file
35
sdk/src/query-tools-error-factory.test.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { ErrorClassification, GSDError } from './errors.js';
|
||||
import {
|
||||
createQueryNativeErrorFactory,
|
||||
createQueryToolsErrorFactory,
|
||||
toToolsErrorFromUnknown,
|
||||
} from './query-tools-error-factory.js';
|
||||
|
||||
describe('query tools error factory', () => {
|
||||
it('builds timeout and failure tools errors via seam factories', () => {
|
||||
const toolsFactory = createQueryToolsErrorFactory();
|
||||
expect(toolsFactory.createTimeoutError('t', 'state', ['load'], '', 10).classification).toEqual({ kind: 'timeout', timeoutMs: 10 });
|
||||
expect(toolsFactory.createFailureError('f', 'state', ['load'], 1, '').classification).toEqual({ kind: 'failure' });
|
||||
});
|
||||
it('maps GSDError to failure with semantic exit code', () => {
|
||||
const err = toToolsErrorFromUnknown('state', ['load'], new GSDError('bad', ErrorClassification.Validation));
|
||||
expect(err.exitCode).toBe(10);
|
||||
expect(err.classification).toEqual({ kind: 'failure' });
|
||||
});
|
||||
|
||||
it('maps timeout-like unknown errors to timeout classification', () => {
|
||||
const err = toToolsErrorFromUnknown('state', ['load'], new Error('gsd-tools timed out after 50ms: state load'));
|
||||
expect(err.classification).toEqual({ kind: 'timeout', timeoutMs: 50 });
|
||||
});
|
||||
|
||||
it('builds subprocess/native error factories', () => {
|
||||
const toolsFactory = createQueryToolsErrorFactory();
|
||||
const nativeFactory = createQueryNativeErrorFactory(777);
|
||||
|
||||
expect(toolsFactory.createFailureError('x', 'state', ['load'], 1, '').classification).toEqual({ kind: 'failure' });
|
||||
expect(toolsFactory.createTimeoutError('x', 'state', ['load'], '', 123).classification).toEqual({ kind: 'timeout', timeoutMs: 123 });
|
||||
expect(nativeFactory.createNativeTimeoutError('x', 'state', ['load']).classification).toEqual({ kind: 'timeout', timeoutMs: 777 });
|
||||
expect(nativeFactory.createNativeFailureError('x', 'state', ['load'], new Error('boom')).classification).toEqual({ kind: 'failure' });
|
||||
});
|
||||
});
|
||||
76
sdk/src/query-tools-error-factory.ts
Normal file
76
sdk/src/query-tools-error-factory.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { GSDError, exitCodeFor } from './errors.js';
|
||||
import { GSDToolsError } from './gsd-tools-error.js';
|
||||
import { errorMessage, toFailureSignal } from './query-failure-classification.js';
|
||||
|
||||
export interface QueryTimeoutErrorFactory {
|
||||
createTimeoutError: (
|
||||
message: string,
|
||||
command: string,
|
||||
args: string[],
|
||||
stderr: string,
|
||||
timeoutMs: number,
|
||||
) => GSDToolsError;
|
||||
}
|
||||
|
||||
export interface QueryFailureErrorFactory {
|
||||
createFailureError: (
|
||||
message: string,
|
||||
command: string,
|
||||
args: string[],
|
||||
exitCode: number | null,
|
||||
stderr: string,
|
||||
) => GSDToolsError;
|
||||
}
|
||||
|
||||
export type QueryToolsErrorFactory = QueryTimeoutErrorFactory & QueryFailureErrorFactory;
|
||||
|
||||
export interface QueryNativeErrorFactory {
|
||||
createNativeTimeoutError: (message: string, command: string, args: string[]) => GSDToolsError;
|
||||
createNativeFailureError: (message: string, command: string, args: string[], cause: unknown) => GSDToolsError;
|
||||
}
|
||||
|
||||
function timeoutToolsError(message: string, command: string, args: string[], stderr = '', timeoutMs?: number): GSDToolsError {
|
||||
return GSDToolsError.timeout(message, command, args, stderr, timeoutMs);
|
||||
}
|
||||
|
||||
function failureToolsError(
|
||||
message: string,
|
||||
command: string,
|
||||
args: string[],
|
||||
exitCode: number | null,
|
||||
stderr = '',
|
||||
cause?: unknown,
|
||||
): GSDToolsError {
|
||||
return GSDToolsError.failure(message, command, args, exitCode, stderr, cause === undefined ? undefined : { cause });
|
||||
}
|
||||
|
||||
export function toToolsErrorFromUnknown(command: string, args: string[], err: unknown): GSDToolsError {
|
||||
if (err instanceof GSDError) {
|
||||
return failureToolsError(err.message, command, args, exitCodeFor(err.classification), '', err);
|
||||
}
|
||||
|
||||
const msg = errorMessage(err);
|
||||
const signal = toFailureSignal(err);
|
||||
if (signal.kind === 'timeout') {
|
||||
return timeoutToolsError(msg, command, args, '', signal.timeoutMs);
|
||||
}
|
||||
return failureToolsError(msg, command, args, 1, '', err instanceof Error ? err : undefined);
|
||||
}
|
||||
|
||||
export function createQueryToolsErrorFactory(): QueryToolsErrorFactory {
|
||||
return {
|
||||
createTimeoutError: (message, command, args, stderr, timeoutMs) =>
|
||||
timeoutToolsError(message, command, args, stderr, timeoutMs),
|
||||
createFailureError: (message, command, args, exitCode, stderr) =>
|
||||
failureToolsError(message, command, args, exitCode, stderr),
|
||||
};
|
||||
}
|
||||
|
||||
export function createQueryNativeErrorFactory(defaultTimeoutMs: number): QueryNativeErrorFactory {
|
||||
return {
|
||||
createNativeTimeoutError: (message, command, args) =>
|
||||
timeoutToolsError(message, command, args, '', defaultTimeoutMs),
|
||||
createNativeFailureError: (message, command, args, cause) =>
|
||||
failureToolsError(message, command, args, 1, '', cause),
|
||||
};
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,18 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { COMMAND_DEFINITIONS, COMMAND_DEFINITIONS_BY_FAMILY, FAMILY_MUTATION_COMMANDS } from './command-definition.js';
|
||||
import {
|
||||
COMMAND_DEFINITIONS,
|
||||
COMMAND_DEFINITIONS_BY_FAMILY,
|
||||
FAMILY_MUTATION_COMMANDS,
|
||||
COMMAND_DEFINITION_BY_CANONICAL,
|
||||
COMMAND_MUTATION_SET,
|
||||
COMMAND_RAW_OUTPUT_SET,
|
||||
} from './command-definition.js';
|
||||
import { COMMAND_MANIFEST } from './command-manifest.js';
|
||||
import { NON_FAMILY_COMMAND_MANIFEST } from './command-manifest.non-family.js';
|
||||
|
||||
describe('command-definition module', () => {
|
||||
it('exposes canonical metadata with handler_key normalization contract', () => {
|
||||
expect(COMMAND_DEFINITIONS).toHaveLength(COMMAND_MANIFEST.length);
|
||||
expect(COMMAND_DEFINITIONS).toHaveLength(COMMAND_MANIFEST.length + NON_FAMILY_COMMAND_MANIFEST.length);
|
||||
for (const [index, manifestEntry] of COMMAND_MANIFEST.entries()) {
|
||||
const definition = COMMAND_DEFINITIONS[index];
|
||||
expect(definition.handler_key).toBe(manifestEntry.handlerKey ?? manifestEntry.canonical);
|
||||
@@ -15,11 +23,11 @@ describe('command-definition module', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps family index canonicals in sync with flat list', () => {
|
||||
it('keeps family index canonicals in sync with family definitions', () => {
|
||||
const indexed = Object.values(COMMAND_DEFINITIONS_BY_FAMILY).flat();
|
||||
expect(indexed).toHaveLength(COMMAND_DEFINITIONS.length);
|
||||
expect(indexed).toHaveLength(COMMAND_MANIFEST.length);
|
||||
expect(indexed.map((entry) => entry.canonical).sort()).toEqual(
|
||||
[...COMMAND_DEFINITIONS.map((entry) => entry.canonical)].sort(),
|
||||
[...COMMAND_MANIFEST.map((entry) => entry.canonical)].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -28,4 +36,12 @@ describe('command-definition module', () => {
|
||||
expect(FAMILY_MUTATION_COMMANDS).toContain('phase complete');
|
||||
expect(FAMILY_MUTATION_COMMANDS).toContain('roadmap.update-plan-progress');
|
||||
});
|
||||
|
||||
it('exposes indexed views for policy consumers', () => {
|
||||
expect(COMMAND_DEFINITION_BY_CANONICAL['state.load']?.canonical).toBe('state.load');
|
||||
expect(COMMAND_MUTATION_SET.has('state.update')).toBe(true);
|
||||
expect(COMMAND_MUTATION_SET.has('state.json')).toBe(false);
|
||||
expect(COMMAND_RAW_OUTPUT_SET.has('commit')).toBe(true);
|
||||
expect(COMMAND_RAW_OUTPUT_SET.has('verify summary')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { COMMAND_MANIFEST } from './command-manifest.js';
|
||||
import { NON_FAMILY_COMMAND_MANIFEST } from './command-manifest.non-family.js';
|
||||
import type { CommandFamily, OutputMode } from './command-manifest.types.js';
|
||||
|
||||
export interface CommandDefinition {
|
||||
family: CommandFamily;
|
||||
family?: CommandFamily;
|
||||
canonical: string;
|
||||
aliases: string[];
|
||||
mutation: boolean;
|
||||
output_mode: OutputMode;
|
||||
handler_key: string;
|
||||
handler_key?: string;
|
||||
}
|
||||
|
||||
export const COMMAND_DEFINITIONS: readonly CommandDefinition[] = COMMAND_MANIFEST.map((entry) => ({
|
||||
const FAMILY_COMMAND_DEFINITIONS: readonly CommandDefinition[] = COMMAND_MANIFEST.map((entry) => ({
|
||||
family: entry.family,
|
||||
canonical: entry.canonical,
|
||||
aliases: [...entry.aliases],
|
||||
@@ -19,6 +20,18 @@ export const COMMAND_DEFINITIONS: readonly CommandDefinition[] = COMMAND_MANIFES
|
||||
handler_key: entry.handlerKey ?? entry.canonical,
|
||||
})) as readonly CommandDefinition[];
|
||||
|
||||
const NON_FAMILY_COMMAND_DEFINITIONS: readonly CommandDefinition[] = NON_FAMILY_COMMAND_MANIFEST.map((entry) => ({
|
||||
canonical: entry.canonical,
|
||||
aliases: [...entry.aliases],
|
||||
mutation: entry.mutation,
|
||||
output_mode: entry.outputMode,
|
||||
})) as readonly CommandDefinition[];
|
||||
|
||||
export const COMMAND_DEFINITIONS: readonly CommandDefinition[] = [
|
||||
...FAMILY_COMMAND_DEFINITIONS,
|
||||
...NON_FAMILY_COMMAND_DEFINITIONS,
|
||||
] as const;
|
||||
|
||||
function byFamily(family: CommandFamily): readonly CommandDefinition[] {
|
||||
return COMMAND_DEFINITIONS.filter((entry) => entry.family === family);
|
||||
}
|
||||
@@ -33,10 +46,25 @@ export const COMMAND_DEFINITIONS_BY_FAMILY: Readonly<Record<CommandFamily, reado
|
||||
roadmap: byFamily('roadmap'),
|
||||
} as const;
|
||||
|
||||
export const FAMILY_MUTATION_COMMANDS: readonly string[] = COMMAND_DEFINITIONS
|
||||
export const COMMAND_DEFINITION_BY_CANONICAL: Readonly<Record<string, CommandDefinition>> = Object.fromEntries(
|
||||
COMMAND_DEFINITIONS.map((entry) => [entry.canonical, entry]),
|
||||
);
|
||||
|
||||
export const COMMAND_MUTATION_SET: ReadonlySet<string> = new Set(
|
||||
COMMAND_DEFINITIONS.filter((entry) => entry.mutation).flatMap((entry) => [entry.canonical, ...entry.aliases]),
|
||||
);
|
||||
|
||||
export const COMMAND_RAW_OUTPUT_SET: ReadonlySet<string> = new Set(
|
||||
COMMAND_DEFINITIONS.filter((entry) => entry.output_mode === 'raw').flatMap((entry) => [entry.canonical, ...entry.aliases]),
|
||||
);
|
||||
|
||||
export const FAMILY_MUTATION_COMMANDS: readonly string[] = FAMILY_COMMAND_DEFINITIONS
|
||||
.filter((entry) => entry.mutation)
|
||||
.flatMap((entry) => [entry.canonical, ...entry.aliases]);
|
||||
|
||||
export const FAMILY_RAW_OUTPUT_COMMANDS: readonly string[] = COMMAND_DEFINITIONS
|
||||
export const FAMILY_RAW_OUTPUT_COMMANDS: readonly string[] = FAMILY_COMMAND_DEFINITIONS
|
||||
.filter((entry) => entry.output_mode === 'raw')
|
||||
.flatMap((entry) => [entry.canonical, ...entry.aliases]);
|
||||
|
||||
export const QUERY_MUTATION_COMMANDS_FROM_DEFINITIONS: readonly string[] = Array.from(COMMAND_MUTATION_SET);
|
||||
export const TRANSPORT_RAW_COMMANDS_FROM_DEFINITIONS: readonly string[] = Array.from(COMMAND_RAW_OUTPUT_SET);
|
||||
|
||||
66
sdk/src/query/command-manifest.non-family.ts
Normal file
66
sdk/src/query/command-manifest.non-family.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import type { OutputMode } from './command-manifest.types.js';
|
||||
|
||||
export interface NonFamilyCommandManifestEntry {
|
||||
canonical: string;
|
||||
aliases: string[];
|
||||
mutation: boolean;
|
||||
outputMode: OutputMode;
|
||||
}
|
||||
|
||||
export const NON_FAMILY_COMMAND_MANIFEST: readonly NonFamilyCommandManifestEntry[] = [
|
||||
{ canonical: 'frontmatter.set', aliases: [], mutation: true, outputMode: 'json' },
|
||||
{ canonical: 'frontmatter.merge', aliases: [], mutation: true, outputMode: 'json' },
|
||||
{ canonical: 'frontmatter.validate', aliases: ['frontmatter validate'], mutation: true, outputMode: 'json' },
|
||||
|
||||
{ canonical: 'commit', aliases: [], mutation: true, outputMode: 'raw' },
|
||||
{ canonical: 'config-set', aliases: [], mutation: true, outputMode: 'raw' },
|
||||
{ canonical: 'config-set-model-profile', aliases: [], mutation: true, outputMode: 'json' },
|
||||
{ canonical: 'config-new-project', aliases: [], mutation: true, outputMode: 'json' },
|
||||
{ canonical: 'config-ensure-section', aliases: [], mutation: true, outputMode: 'json' },
|
||||
|
||||
{ canonical: 'check-commit', aliases: [], mutation: true, outputMode: 'json' },
|
||||
{ canonical: 'commit-to-subrepo', aliases: [], mutation: true, outputMode: 'json' },
|
||||
|
||||
{ canonical: 'template.fill', aliases: [], mutation: true, outputMode: 'json' },
|
||||
{ canonical: 'template.select', aliases: ['template select'], mutation: true, outputMode: 'json' },
|
||||
|
||||
{ canonical: 'requirements.mark-complete', aliases: ['requirements mark-complete'], mutation: true, outputMode: 'json' },
|
||||
{ canonical: 'todo.complete', aliases: ['todo complete'], mutation: true, outputMode: 'json' },
|
||||
{ canonical: 'milestone.complete', aliases: ['milestone complete'], mutation: true, outputMode: 'json' },
|
||||
|
||||
{
|
||||
canonical: 'workstream.create',
|
||||
aliases: ['workstream create'],
|
||||
mutation: true,
|
||||
outputMode: 'json',
|
||||
},
|
||||
{ canonical: 'workstream.set', aliases: ['workstream set'], mutation: true, outputMode: 'json' },
|
||||
{
|
||||
canonical: 'workstream.complete',
|
||||
aliases: ['workstream complete'],
|
||||
mutation: true,
|
||||
outputMode: 'json',
|
||||
},
|
||||
{
|
||||
canonical: 'workstream.progress',
|
||||
aliases: ['workstream progress'],
|
||||
mutation: true,
|
||||
outputMode: 'json',
|
||||
},
|
||||
|
||||
{ canonical: 'docs-init', aliases: [], mutation: true, outputMode: 'json' },
|
||||
|
||||
{ canonical: 'learnings.copy', aliases: ['learnings copy'], mutation: true, outputMode: 'json' },
|
||||
{ canonical: 'learnings.prune', aliases: ['learnings prune'], mutation: true, outputMode: 'json' },
|
||||
{ canonical: 'learnings.delete', aliases: ['learnings delete'], mutation: true, outputMode: 'json' },
|
||||
|
||||
{ canonical: 'intel.snapshot', aliases: ['intel snapshot'], mutation: true, outputMode: 'json' },
|
||||
{ canonical: 'intel.patch-meta', aliases: ['intel patch-meta'], mutation: true, outputMode: 'json' },
|
||||
|
||||
{ canonical: 'write-profile', aliases: [], mutation: true, outputMode: 'json' },
|
||||
{ canonical: 'generate-claude-profile', aliases: [], mutation: true, outputMode: 'json' },
|
||||
{ canonical: 'generate-dev-preferences', aliases: [], mutation: true, outputMode: 'json' },
|
||||
{ canonical: 'generate-claude-md', aliases: [], mutation: true, outputMode: 'json' },
|
||||
|
||||
{ canonical: 'verify-summary', aliases: ['verify.summary', 'verify summary'], mutation: false, outputMode: 'raw' },
|
||||
] as const;
|
||||
@@ -1,8 +1,13 @@
|
||||
import type { QueryRegistry } from './registry.js';
|
||||
import type { QueryHandler } from './utils.js';
|
||||
import { resolveQueryCommand } from './query-command-resolution-strategy.js';
|
||||
import { diagnoseUnknownCommand } from './query-command-diagnosis.js';
|
||||
import {
|
||||
resolveQueryCommand,
|
||||
explainQueryCommandNoMatch,
|
||||
type QueryCommandRegistryLike,
|
||||
} from './query-command-resolution-strategy.js';
|
||||
import { supportsMutationCommand, supportsRawOutputCommand } from './query-policy-capability.js';
|
||||
import { UNKNOWN_COMMAND_HINTS } from './query-unknown-command-hints.js';
|
||||
import { describeFallbackDisabledPolicy } from './query-fallback-policy.js';
|
||||
|
||||
export type CommandTopologyOutputMode = 'json' | 'text' | 'raw';
|
||||
|
||||
@@ -29,6 +34,35 @@ export interface CommandTopology {
|
||||
resolve(tokens: string[], fallbackRestricted?: boolean): CommandTopologyResult;
|
||||
}
|
||||
|
||||
export interface UnknownCommandDiagnosis {
|
||||
normalized: string;
|
||||
attempted: string[];
|
||||
hints: string[];
|
||||
message: string;
|
||||
}
|
||||
|
||||
export function diagnoseUnknownCommand(
|
||||
command: string,
|
||||
args: string[],
|
||||
registry: QueryCommandRegistryLike,
|
||||
fallbackRestricted: boolean,
|
||||
): UnknownCommandDiagnosis {
|
||||
const noMatch = explainQueryCommandNoMatch(command, args, registry);
|
||||
const normalized = [noMatch.normalized.command, ...noMatch.normalized.args].join(' ');
|
||||
const attempted = noMatch.attempted.dotted.slice(0, 2);
|
||||
const hints = [...UNKNOWN_COMMAND_HINTS];
|
||||
const attemptedSuffix = attempted.length > 0 ? ` Attempted dotted: ${attempted.join(' | ')}.` : '';
|
||||
const fallbackClause = fallbackRestricted ? `${describeFallbackDisabledPolicy()} ` : '';
|
||||
const message = `Error: Unknown command: "${normalized}". ${hints[0]} ${hints[1]} ${fallbackClause}${hints[2]}${attemptedSuffix}`;
|
||||
|
||||
return {
|
||||
normalized,
|
||||
attempted,
|
||||
hints,
|
||||
message,
|
||||
};
|
||||
}
|
||||
|
||||
export function createCommandTopology(registry: QueryRegistry): CommandTopology {
|
||||
return {
|
||||
resolve(tokens: string[], fallbackRestricted = false): CommandTopologyResult {
|
||||
|
||||
@@ -83,6 +83,26 @@ function deriveStatusFromCheckbox(
|
||||
return 'not_started';
|
||||
}
|
||||
|
||||
function listPhasePlanAndSummaryCounts(phasePath: string): { plans: string[]; summaries: string[] } {
|
||||
const phaseFiles = readdirSync(phasePath);
|
||||
const rootPlans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
|
||||
const rootSummaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
||||
|
||||
const plansDir = join(phasePath, 'plans');
|
||||
let nestedPlans: string[] = [];
|
||||
let nestedSummaries: string[] = [];
|
||||
if (existsSync(plansDir)) {
|
||||
const files = readdirSync(plansDir);
|
||||
nestedPlans = files.filter(f => /^PLAN-\d+.*\.md$/i.test(f));
|
||||
nestedSummaries = files.filter(f => /^SUMMARY-\d+.*\.md$/i.test(f));
|
||||
}
|
||||
|
||||
return {
|
||||
plans: rootPlans.concat(nestedPlans),
|
||||
summaries: rootSummaries.concat(nestedSummaries),
|
||||
};
|
||||
}
|
||||
|
||||
// ─── initNewProject ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -258,8 +278,7 @@ export const initProgress: QueryHandler = async (_args, projectDir, workstream)
|
||||
const phasePath = join(paths.phases, dir);
|
||||
const phaseFiles = readdirSync(phasePath);
|
||||
|
||||
const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
|
||||
const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
||||
const { plans, summaries } = listPhasePlanAndSummaryCounts(phasePath);
|
||||
const hasResearch = phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
|
||||
|
||||
let status =
|
||||
@@ -431,8 +450,9 @@ export const initManager: QueryHandler = async (_args, projectDir, workstream) =
|
||||
if (dirMatch) {
|
||||
const fullDir = join(paths.phases, dirMatch);
|
||||
const phaseFiles = readdirSync(fullDir);
|
||||
planCount = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').length;
|
||||
summaryCount = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').length;
|
||||
const counts = listPhasePlanAndSummaryCounts(fullDir);
|
||||
planCount = counts.plans.length;
|
||||
summaryCount = counts.summaries.length;
|
||||
hasContext = phaseFiles.some(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
|
||||
hasResearch = phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
|
||||
|
||||
|
||||
@@ -330,6 +330,20 @@ describe('initExecutePhase', () => {
|
||||
const data = result.data as Record<string, unknown>;
|
||||
expect(data.error).toBeDefined();
|
||||
});
|
||||
|
||||
it('honors legacy top-level branching_strategy in config for execute-phase init (#3055)', async () => {
|
||||
await writeFile(join(tmpDir, '.planning', 'config.json'), JSON.stringify({
|
||||
model_profile: 'balanced',
|
||||
commit_docs: false,
|
||||
branching_strategy: 'phase',
|
||||
workflow: { research: true, plan_check: true, verifier: true, nyquist_validation: true },
|
||||
}));
|
||||
|
||||
const result = await initExecutePhase(['9'], tmpDir);
|
||||
const data = result.data as Record<string, unknown>;
|
||||
expect(data.branching_strategy).toBe('phase');
|
||||
expect(typeof data.branch_name).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('initPlanPhase', () => {
|
||||
|
||||
@@ -1841,6 +1841,29 @@ export const milestoneComplete: QueryHandler = async (args, projectDir, workstre
|
||||
null,
|
||||
`${version} milestone completed and archived`,
|
||||
);
|
||||
|
||||
const positionPattern = /(##\s*Current Position\s*\n)([\s\S]*?)(?=\n##|$)/i;
|
||||
const closedPositionBody =
|
||||
`\nPhase: Milestone ${version} complete\n` +
|
||||
`Plan: —\n` +
|
||||
`Status: Awaiting next milestone\n` +
|
||||
`Last activity: ${today} — Milestone ${version} completed and archived\n\n`;
|
||||
if (positionPattern.test(next)) {
|
||||
next = next.replace(positionPattern, (_m, header) => `${header}${closedPositionBody}`);
|
||||
} else {
|
||||
next = `${next.trimEnd()}\n\n## Current Position\n${closedPositionBody}`;
|
||||
}
|
||||
|
||||
const operatorPattern = /(##\s*Operator Next Steps\s*\n)([\s\S]*?)(?=\n##|$)/i;
|
||||
if (operatorPattern.test(next)) {
|
||||
next = next.replace(
|
||||
operatorPattern,
|
||||
`$1\n- Start the next milestone with /gsd-new-milestone\n\n`,
|
||||
);
|
||||
} else {
|
||||
next = `${next.trimEnd()}\n\n## Operator Next Steps\n\n- Start the next milestone with /gsd-new-milestone\n`;
|
||||
}
|
||||
|
||||
return next;
|
||||
}, workstream);
|
||||
}
|
||||
|
||||
@@ -80,6 +80,17 @@ async function getPhaseFileStats(phaseDir: string): Promise<{
|
||||
*
|
||||
* Port of searchPhaseInDir from core.cjs lines 956-1000.
|
||||
*/
|
||||
function extractCanonicalPlanId(filename: string): string {
|
||||
const base = filename.replace(/-PLAN\.md$/i, '').replace(/-SUMMARY\.md$/i, '').replace(/\.md$/i, '');
|
||||
const parts = base.split('-').filter(Boolean);
|
||||
const tokenRe = /^\d+[A-Z]?(?:\.\d+)*$/i;
|
||||
const phaseIdx = parts.findIndex((p) => tokenRe.test(p));
|
||||
if (phaseIdx >= 0 && phaseIdx + 1 < parts.length && tokenRe.test(parts[phaseIdx + 1])) {
|
||||
return `${parts[phaseIdx]}-${parts[phaseIdx + 1]}`;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
async function searchPhaseInDir(baseDir: string, relBase: string, normalized: string): Promise<PhaseInfo | null> {
|
||||
try {
|
||||
const entries = await readdir(baseDir, { withFileTypes: true });
|
||||
@@ -105,11 +116,16 @@ async function searchPhaseInDir(baseDir: string, relBase: string, normalized: st
|
||||
const summaries = unsortedSummaries.sort();
|
||||
|
||||
const completedPlanIds = new Set(
|
||||
summaries.map(s => s.replace('-SUMMARY.md', '').replace('SUMMARY.md', ''))
|
||||
summaries.flatMap((s) => {
|
||||
const exact = s.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
|
||||
const canonical = extractCanonicalPlanId(s);
|
||||
return canonical === exact ? [exact] : [exact, canonical];
|
||||
})
|
||||
);
|
||||
const incompletePlans = plans.filter(p => {
|
||||
const incompletePlans = plans.filter((p) => {
|
||||
const planId = p.replace('-PLAN.md', '').replace('PLAN.md', '');
|
||||
return !completedPlanIds.has(planId);
|
||||
const canonical = extractCanonicalPlanId(p);
|
||||
return !completedPlanIds.has(planId) && !completedPlanIds.has(canonical);
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -265,7 +281,11 @@ export const phasePlanIndex: QueryHandler = async (args, projectDir, workstream)
|
||||
|
||||
// Build set of plan IDs with summaries — match the planId derivation logic
|
||||
const completedPlanIds = new Set(
|
||||
summaryFiles.map(s => s === 'SUMMARY.md' ? 'PLAN' : s.replace('-SUMMARY.md', ''))
|
||||
summaryFiles.flatMap((s) => {
|
||||
const exact = s === 'SUMMARY.md' ? 'PLAN' : s.replace('-SUMMARY.md', '');
|
||||
const canonical = extractCanonicalPlanId(s);
|
||||
return canonical === exact ? [exact] : [exact, canonical];
|
||||
})
|
||||
);
|
||||
|
||||
const plans: Array<Record<string, unknown>> = [];
|
||||
@@ -306,7 +326,7 @@ export const phasePlanIndex: QueryHandler = async (args, projectDir, workstream)
|
||||
filesModified = Array.isArray(fmFiles) ? fmFiles : [fmFiles];
|
||||
}
|
||||
|
||||
const hasSummary = completedPlanIds.has(planId);
|
||||
const hasSummary = completedPlanIds.has(planId) || completedPlanIds.has(extractCanonicalPlanId(planFile));
|
||||
if (!hasSummary) {
|
||||
incomplete.push(planId);
|
||||
}
|
||||
|
||||
@@ -4,28 +4,28 @@ import { buildQueryCliOutputFromError } from './query-cli-output.js';
|
||||
|
||||
describe('query-cli-output', () => {
|
||||
it('prefers raw gsd-tools stderr when present', () => {
|
||||
const err = new GSDToolsError('failed', 'list', ['json'], 2, 'line one\nline two\n');
|
||||
const err = GSDToolsError.failure('failed', 'list', ['json'], 2, 'line one\nline two\n');
|
||||
const out = buildQueryCliOutputFromError(err);
|
||||
expect(out.exitCode).toBe(2);
|
||||
expect(out.stderrLines).toEqual(['line one', 'line two']);
|
||||
});
|
||||
|
||||
it('falls back to Error: message when gsd-tools stderr is empty', () => {
|
||||
const err = new GSDToolsError('failed', 'list', ['json'], null, '');
|
||||
const err = GSDToolsError.failure('failed', 'list', ['json'], null, '');
|
||||
const out = buildQueryCliOutputFromError(err);
|
||||
expect(out.exitCode).toBe(1);
|
||||
expect(out.stderrLines).toEqual(['Error: failed']);
|
||||
});
|
||||
|
||||
it('falls back to Error: message when gsd-tools stderr is whitespace-only', () => {
|
||||
const err = new GSDToolsError('failed', 'build', ['json'], null, ' \n');
|
||||
const err = GSDToolsError.failure('failed', 'build', ['json'], null, ' \n');
|
||||
const out = buildQueryCliOutputFromError(err);
|
||||
expect(out.exitCode).toBe(1);
|
||||
expect(out.stderrLines).toEqual(['Error: failed']);
|
||||
});
|
||||
|
||||
it('uses exitCode 1 when gsd-tools exitCode is null and stderr is non-empty', () => {
|
||||
const err = new GSDToolsError('failed', 'build', ['json'], null, 'line');
|
||||
const err = GSDToolsError.failure('failed', 'build', ['json'], null, 'line');
|
||||
const out = buildQueryCliOutputFromError(err);
|
||||
expect(out.exitCode).toBe(1);
|
||||
expect(out.stderrLines).toEqual(['line']);
|
||||
|
||||
@@ -1,32 +1,5 @@
|
||||
import { explainQueryCommandNoMatch, type QueryCommandRegistryLike } from './query-command-semantics.js';
|
||||
import { UNKNOWN_COMMAND_HINTS } from './query-unknown-command-hints.js';
|
||||
import { describeFallbackDisabledPolicy } from './query-fallback-policy.js';
|
||||
|
||||
export interface UnknownCommandDiagnosis {
|
||||
normalized: string;
|
||||
attempted: string[];
|
||||
hints: string[];
|
||||
message: string;
|
||||
}
|
||||
|
||||
export function diagnoseUnknownCommand(
|
||||
command: string,
|
||||
args: string[],
|
||||
registry: QueryCommandRegistryLike,
|
||||
fallbackRestricted: boolean,
|
||||
): UnknownCommandDiagnosis {
|
||||
const noMatch = explainQueryCommandNoMatch(command, args, registry);
|
||||
const normalized = [noMatch.normalized.command, ...noMatch.normalized.args].join(' ');
|
||||
const attempted = noMatch.attempted.dotted.slice(0, 2);
|
||||
const hints = [...UNKNOWN_COMMAND_HINTS];
|
||||
const attemptedSuffix = attempted.length > 0 ? ` Attempted dotted: ${attempted.join(' | ')}.` : '';
|
||||
const fallbackClause = fallbackRestricted ? `${describeFallbackDisabledPolicy()} ` : '';
|
||||
const message = `Error: Unknown command: "${normalized}". ${hints[0]} ${hints[1]} ${fallbackClause}${hints[2]}${attemptedSuffix}`;
|
||||
|
||||
return {
|
||||
normalized,
|
||||
attempted,
|
||||
hints,
|
||||
message,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* @deprecated Compatibility seam after Command Topology Module deepening.
|
||||
* Remove-after: all imports migrate to `command-topology.ts`.
|
||||
*/
|
||||
export { diagnoseUnknownCommand, type UnknownCommandDiagnosis } from './command-topology.js';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user