diff --git a/commands/gsd/ns-review.md b/commands/gsd/ns-review.md
index f4d36d26e..97e30a4d2 100644
--- a/commands/gsd/ns-review.md
+++ b/commands/gsd/ns-review.md
@@ -1,5 +1,5 @@
---
-name: gsd-review
+name: gsd-quality
description: "quality gates | code review debug audit security eval ui"
argument-hint: ""
allowed-tools:
diff --git a/docs/AGENTS.md b/docs/AGENTS.md
index 4c3479b14..96d1cec3d 100644
--- a/docs/AGENTS.md
+++ b/docs/AGENTS.md
@@ -401,7 +401,7 @@ runs its default whole-repo scan.
| **Tools** | Read |
| **Model (balanced)** | Sonnet |
| **Color** | Magenta |
-| **Produces** | `USER-PROFILE.md`, `/gsd-dev-preferences`, `CLAUDE.md` profile section |
+| **Produces** | `USER-PROFILE.md`, `CLAUDE.md` profile section |
**Behavioral Dimensions:**
Communication style, decision patterns, debugging approach, UX preferences, vendor choices, frustration triggers, learning style, explanation depth.
@@ -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-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).
+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-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
@@ -648,7 +648,7 @@ Twelve additional agents ship under `agents/gsd-*.md` and are used by specialty
| Property | Value |
|----------|-------|
-| **Spawned by** | `/gsd-ai-integration-phase`, `/gsd-select-framework` |
+| **Spawned by** | `/gsd-ai-integration-phase` |
| **Parallelism** | Single instance (interactive) |
| **Tools** | Read, Bash, Grep, Glob, WebSearch, AskUserQuestion |
| **Model (balanced)** | Sonnet |
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 5d7ba60aa..c5175f936 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -122,7 +122,7 @@ User-facing entry points. Each file contains YAML frontmatter (name, description
#### Two-stage hierarchical routing (v1.40, [#2792](https://github.com/gsd-build/get-shit-done/issues/2792))
-To keep the eager skill-listing token cost low, v1.40 introduces six namespace **meta-skills** (`gsd-workflow`, `gsd-project`, `gsd-review`, `gsd-context`, `gsd-manage`, `gsd-ideate` — sourced from `commands/gsd/ns-*.md`, but the invocable `name:` is the bare form shown here) layered above the concrete sub-skills. The model sees 6 namespace routers (~120 tokens) instead of a flat 86-skill listing (~2,150 tokens), selects a namespace, then routes to the concrete sub-skill via a routing table embedded in the namespace router's body. Namespace skills are **additive** — every concrete command is still directly invocable.
+To keep the eager skill-listing token cost low, v1.40 introduces six namespace **meta-skills** (`gsd-workflow`, `gsd-project`, `gsd-quality`, `gsd-context`, `gsd-manage`, `gsd-ideate` — sourced from `commands/gsd/ns-*.md`, but the invocable `name:` is the bare form shown here) layered above the concrete sub-skills. The model sees 6 namespace routers (~120 tokens) instead of a flat 86-skill listing (~2,150 tokens), selects a namespace, then routes to the concrete sub-skill via a routing table embedded in the namespace router's body. Namespace skills are **additive** — every concrete command is still directly invocable.
The router descriptions use pipe-separated keyword tags (≤ 60 chars) per the Tool Attention research showing keyword-dense tags outperform prose for routing at ~40 % the token cost.
diff --git a/docs/CLI-TOOLS.md b/docs/CLI-TOOLS.md
index 06bbbe428..505b5807d 100644
--- a/docs/CLI-TOOLS.md
+++ b/docs/CLI-TOOLS.md
@@ -418,7 +418,7 @@ node gsd-tools.cjs audit-uat
# Cross-artifact audit queue — scan `.planning/` for unresolved audit items
node gsd-tools.cjs audit-open [--json]
-# Reverse-migrate a GSD-2 project into the current structure (backs `/gsd-from-gsd2`)
+# Reverse-migrate a GSD-2 project into the current structure (backs `/gsd-import --from-gsd2`)
node gsd-tools.cjs from-gsd2 [--path
] [--force] [--dry-run]
# Git commit with config checks
@@ -481,14 +481,14 @@ User-facing entry point: `/gsd-graphify` (see [Command Reference](COMMANDS.md#gs
| Graphify | `lib/graphify.cjs` | Knowledge graph build/query/status/diff/snapshot (backs `/gsd-graphify`) |
| 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`) |
+| GSD2 Import | `lib/gsd2-import.cjs` | Reverse-migration importer from GSD-2 projects (backs `/gsd-import --from-gsd2`) |
| Intel | `lib/intel.cjs` | Queryable codebase intelligence index (backs `/gsd-map-codebase --query`) |
---
## Reviewer CLI Routing
-`review.models.` maps a reviewer flavor to a shell command invoked by the code-review workflow. Set via [`/gsd-settings-integrations`](COMMANDS.md#gsd-settings-integrations) or directly:
+`review.models.` maps a reviewer flavor to a shell command invoked by the code-review workflow. Set via [`/gsd-config --integrations`](COMMANDS.md#gsd-config) or directly:
```bash
gsd-sdk query config-set review.models.codex "codex exec --model gpt-5"
@@ -501,7 +501,7 @@ Slugs are validated against `[a-zA-Z0-9_-]+`; empty or path-containing slugs are
## Secret Handling
-API keys configured via `/gsd-settings-integrations` (`brave_search`, `firecrawl`, `exa_search`) are written plaintext to `.planning/config.json` but are masked (`****`) in every `config-set` / `config-get` output, confirmation table, and interactive prompt. See `get-shit-done/bin/lib/secrets.cjs` for the masking implementation. The `config.json` file itself is the security boundary — protect it with filesystem permissions and keep it out of git (`.planning/` is gitignored by default).
+API keys configured via `/gsd-settings` (`brave_search`, `firecrawl`, `exa_search`) are written plaintext to `.planning/config.json` but are masked (`****`) in every `config-set` / `config-get` output, confirmation table, and interactive prompt. See `get-shit-done/bin/lib/secrets.cjs` for the masking implementation. The `config.json` file itself is the security boundary — protect it with filesystem permissions and keep it out of git (`.planning/` is gitignored by default).
---
diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md
index 367e6fe99..4ac302436 100644
--- a/docs/COMMANDS.md
+++ b/docs/COMMANDS.md
@@ -20,12 +20,12 @@ Six namespace routers ship as the first-stage entry points in v1.40. They keep t
| Command | Routes to |
|---------|-----------|
-| `/gsd-ns-workflow` | Phase pipeline — discuss / plan / execute / verify / phase / progress |
-| `/gsd-ns-project` | Project lifecycle — milestones, audits, summary |
-| `/gsd-ns-review` | Quality gates — code review, debug, audit, security, eval, ui |
-| `/gsd-ns-context` | Codebase intelligence — map, graphify, docs, learnings |
-| `/gsd-ns-manage` | Management — config, workspace, workstreams, thread, update, ship, inbox |
-| `/gsd-ns-ideate` | Exploration & capture — explore, sketch, spike, spec, capture |
+| `/gsd-workflow` | Phase pipeline — discuss / plan / execute / verify / phase / progress |
+| `/gsd-project` | Project lifecycle — milestones, audits, summary |
+| `/gsd-quality` | Quality gates — code review, debug, audit, security, eval, ui |
+| `/gsd-context` | Codebase intelligence — map, graphify, docs, learnings |
+| `/gsd-manage` | Management — config, workspace, workstreams, thread, update, ship, inbox |
+| `/gsd-ideate` | Exploration & capture — explore, sketch, spike, spec, capture |
The namespace skills are **additive** — every existing concrete command (e.g. `/gsd-plan-phase`, `/gsd-code-review --fix`) is still invocable directly.
@@ -732,7 +732,6 @@ Generate a developer behavioral profile from Claude Code session analysis across
**Generated artifacts:**
- `USER-PROFILE.md` — Full behavioral profile
-- `/gsd-dev-preferences` command — Load preferences in any session
- `CLAUDE.md` profile section — Auto-discovered by Claude Code
```bash
diff --git a/docs/FEATURES.md b/docs/FEATURES.md
index d06f25596..eb7630612 100644
--- a/docs/FEATURES.md
+++ b/docs/FEATURES.md
@@ -517,7 +517,7 @@
### 12. Freeform Routing
-**Command:** `/gsd-do`
+**Command:** `/gsd-progress --do` (see also `/gsd-manager` for interactive routing)
**Purpose:** Analyze freeform text and route to the appropriate GSD command.
@@ -531,7 +531,7 @@
### 13. Note Capture
-**Command:** `/gsd-note`
+**Command:** `/gsd-capture`
**Purpose:** Zero-friction idea capture without interrupting workflow. Append timestamped notes, list all notes, or promote notes to structured todos.
@@ -1080,7 +1080,6 @@ The banner is silent when up-to-date and rate-limits "check failed" diagnostics
**Generated Artifacts:**
- `USER-PROFILE.md` — Full behavioral profile with evidence citations
-- `/gsd-dev-preferences` command — Load preferences in any session
- `CLAUDE.md` profile section — Auto-discovered by Claude Code
**Flags:**
@@ -2198,13 +2197,13 @@ Test suite that scans all agent, workflow, and command files for embedded inject
### 97. Rapid Codebase Scan
-**Command:** `/gsd-scan [--focus tech|arch|quality|concerns|tech+arch]`
+**Command:** `/gsd-map-codebase --fast [--focus tech|arch|quality|concerns]`
-**Purpose:** Lightweight alternative to `/gsd-map-codebase` that spawns a single mapper agent for a specific focus area, producing targeted output in `.planning/codebase/` without the overhead of 4 parallel agents.
+**Purpose:** Lightweight alternative to `/gsd-map-codebase` that spawns a single mapper agent for one or two combined focus areas, producing targeted output in `.planning/codebase/` without the overhead of 4 parallel agents.
**Requirements:**
- REQ-SCAN-01: Scan MUST spawn exactly one mapper agent (not four parallel agents)
-- REQ-SCAN-02: Focus area MUST be one of: `tech`, `arch`, `quality`, `concerns`, `tech+arch` (default)
+- REQ-SCAN-02: Focus area MUST be one of: `tech`, `arch`, `quality`, `concerns`, or the combined `tech+arch` shorthand (default: `tech+arch`); combined focus runs as a single agent covering both areas in one pass
- REQ-SCAN-03: Output MUST be written to `.planning/codebase/` in the same format as `/gsd-map-codebase`
---
@@ -2322,7 +2321,7 @@ Test suite that scans all agent, workflow, and command files for embedded inject
### 105. GSD-2 Reverse Migration
-**Command:** `/gsd-from-gsd2 [--dry-run] [--force] [--path ]`
+**Command:** `/gsd-import --from-gsd2 [--dry-run] [--force] [--path ]`
**Purpose:** Migrate a project from GSD-2 format (`.gsd/` directory with Milestone→Slice→Task hierarchy) back to the v1 `.planning/` format, restoring full compatibility with all GSD v1 commands.
@@ -2653,12 +2652,12 @@ Users who run a memory / knowledge-base MCP server (for example, ExoCortex-style
**Purpose:** Replace the flat eager skill listing with a two-stage hierarchical routing layer. The model sees 6 namespace routers instead of 86 entries, selects a namespace, then routes to the sub-skill. Descriptions use pipe-separated keyword tags (≤ 60 chars) for routing density.
**Commands:**
-- `/gsd-ns-workflow` — phase pipeline router (discuss / plan / execute / verify / phase / progress)
-- `/gsd-ns-project` — project lifecycle (milestones, audits, summary)
-- `/gsd-ns-review` — quality gates (code review, debug, audit, security, eval, ui)
-- `/gsd-ns-context` — codebase intelligence (map, graphify, docs, learnings)
-- `/gsd-ns-manage` — config / workspace / workstreams / thread / update / ship / inbox
-- `/gsd-ns-ideate` — exploration & capture (explore, sketch, spike, spec, capture)
+- `/gsd-workflow` — phase pipeline router (discuss / plan / execute / verify / phase / progress)
+- `/gsd-project` — project lifecycle (milestones, audits, summary)
+- `/gsd-quality` — quality gates (code review, debug, audit, security, eval, ui)
+- `/gsd-context` — codebase intelligence (map, graphify, docs, learnings)
+- `/gsd-manage` — config / workspace / workstreams / thread / update / ship / inbox
+- `/gsd-ideate` — exploration & capture (explore, sketch, spike, spec, capture)
**Token cost:**
diff --git a/docs/INVENTORY.md b/docs/INVENTORY.md
index e4e673b70..2ea3c8662 100644
--- a/docs/INVENTORY.md
+++ b/docs/INVENTORY.md
@@ -45,7 +45,7 @@ Full roster at `agents/gsd-*.md`. The "Primary doc" column flags whether [`docs/
| gsd-domain-researcher | Surfaces domain-expert evaluation criteria and failure modes for an AI system (AI-SPEC.md §1b). | `/gsd-ai-integration-phase` | advanced stub |
| gsd-eval-planner | Designs structured evaluation strategy for an AI phase (AI-SPEC.md §5–§7). | `/gsd-ai-integration-phase` | advanced stub |
| gsd-eval-auditor | Retroactive audit of an AI phase's evaluation coverage; produces EVAL-REVIEW.md (COVERED/PARTIAL/MISSING). | `/gsd-eval-review` | advanced stub |
-| gsd-framework-selector | ≤6-question interactive decision matrix that scores and recommends an AI/LLM framework. | `/gsd-ai-integration-phase`, `/gsd-select-framework` | advanced stub |
+| gsd-framework-selector | ≤6-question interactive decision matrix that scores and recommends an AI/LLM framework. | `/gsd-ai-integration-phase` | advanced stub |
| gsd-intel-updater | Writes structured intel files (`.planning/intel/*.json`) used as a queryable codebase knowledge base. | `/gsd-map-codebase --query` | advanced stub |
| gsd-doc-classifier | Classifies a single planning document as ADR, PRD, SPEC, DOC, or UNKNOWN; spawned in parallel to process the doc corpus. | `/gsd-ingest-docs` | advanced stub |
| gsd-doc-synthesizer | Synthesizes classified planning docs into a single consolidated context with precedence rules, cycle detection, and three-bucket conflicts report. | `/gsd-ingest-docs` | advanced stub |
@@ -64,12 +64,12 @@ These six routers are descriptor-only entries that the model picks first; the bo
| Command | Role | Source |
|---------|------|--------|
-| `/gsd-ns-workflow` | Phase pipeline router — discuss / plan / execute / verify / phase / progress. | [commands/gsd/ns-workflow.md](../commands/gsd/ns-workflow.md) |
-| `/gsd-ns-project` | Project lifecycle router — milestones, audits, summary. | [commands/gsd/ns-project.md](../commands/gsd/ns-project.md) |
-| `/gsd-ns-review` | Quality-gate router — code review, debug, audit, security, eval, ui. | [commands/gsd/ns-review.md](../commands/gsd/ns-review.md) |
-| `/gsd-ns-context` | Codebase-intelligence router — map, graphify, docs, learnings. | [commands/gsd/ns-context.md](../commands/gsd/ns-context.md) |
-| `/gsd-ns-manage` | Management router — config, workspace, workstreams, thread, update, ship, inbox. | [commands/gsd/ns-manage.md](../commands/gsd/ns-manage.md) |
-| `/gsd-ns-ideate` | Exploration & capture router — explore, sketch, spike, spec, capture. | [commands/gsd/ns-ideate.md](../commands/gsd/ns-ideate.md) |
+| `/gsd-workflow` | Phase pipeline router — discuss / plan / execute / verify / phase / progress. | [commands/gsd/ns-workflow.md](../commands/gsd/ns-workflow.md) |
+| `/gsd-project` | Project lifecycle router — milestones, audits, summary. | [commands/gsd/ns-project.md](../commands/gsd/ns-project.md) |
+| `/gsd-quality` | Quality-gate router — code review, debug, audit, security, eval, ui. | [commands/gsd/ns-review.md](../commands/gsd/ns-review.md) |
+| `/gsd-context` | Codebase-intelligence router — map, graphify, docs, learnings. | [commands/gsd/ns-context.md](../commands/gsd/ns-context.md) |
+| `/gsd-manage` | Management router — config, workspace, workstreams, thread, update, ship, inbox. | [commands/gsd/ns-manage.md](../commands/gsd/ns-manage.md) |
+| `/gsd-ideate` | Exploration & capture router — explore, sketch, spike, spec, capture. | [commands/gsd/ns-ideate.md](../commands/gsd/ns-ideate.md) |
### Core Workflow
@@ -232,7 +232,7 @@ Full roster at `get-shit-done/workflows/*.md`. Workflows are thin orchestrators
| `remove-workspace.md` | Remove a GSD workspace and clean up worktrees. | `/gsd-workspace --remove` |
| `resume-project.md` | Resume work — restore full context from STATE.md, HANDOFF.json, and artifacts. | `/gsd-resume-work` |
| `review.md` | Cross-AI plan review via external CLIs; produces REVIEWS.md. | `/gsd-review` |
-| `scan.md` | Rapid single-focus codebase scan — lightweight alternative to map-codebase. | `/gsd-scan` |
+| `scan.md` | Rapid single-focus codebase scan — lightweight alternative to map-codebase. | `/gsd-map-codebase --fast` |
| `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-pause-work --report` |
| `settings.md` | Configure GSD workflow toggles and model profile. | `/gsd-settings`, `/gsd-config --profile` |
@@ -379,7 +379,7 @@ Full listing: `get-shit-done/bin/lib/*.cjs`.
| `frontmatter.cjs` | YAML frontmatter CRUD operations |
| `gap-checker.cjs` | Post-planning gap analysis (#2493): unified REQUIREMENTS.md + CONTEXT.md decisions vs PLAN.md coverage report (`gsd-tools gap-analysis`) |
| `graphify.cjs` | Knowledge-graph build/query/status/diff for `/gsd-graphify` |
-| `gsd2-import.cjs` | External-plan ingest for `/gsd-from-gsd2` |
+| `gsd2-import.cjs` | External-plan ingest for `/gsd-import --from-gsd2` |
| `init-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools init` |
| `init.cjs` | Compound context loading for each workflow type |
| `install-profiles.cjs` | Install profile allowlist + skill staging for `--minimal` install (#2762); single source of truth for which `gsd-*` skills/agents land in runtime config dirs |
diff --git a/docs/STATE-MD-LIFECYCLE.md b/docs/STATE-MD-LIFECYCLE.md
index 31c15f508..31d6c4c05 100644
--- a/docs/STATE-MD-LIFECYCLE.md
+++ b/docs/STATE-MD-LIFECYCLE.md
@@ -107,7 +107,7 @@ is to use the lifecycle stage:
| `/gsd-discuss-phase` | `discussing` |
| `/gsd-plan-phase` | `planning` |
| `/gsd-execute-phase` | `executing` |
-| `/gsd-verify-phase` | `verifying` |
+| `/gsd-verify-work` | `verifying` |
If `status` is left at `in_progress` (the milestone-level value), Scene 1
renders just `Phase 4.5` without the stage suffix.
diff --git a/docs/USER-GUIDE.md b/docs/USER-GUIDE.md
index 1c1c65f20..a6f1f342e 100644
--- a/docs/USER-GUIDE.md
+++ b/docs/USER-GUIDE.md
@@ -40,12 +40,12 @@ v1.40 ships six **namespace meta-skills** as the first-stage entry points for hi
| Namespace | Router | Routes to |
|-----------|--------|-----------|
-| Phase pipeline | `/gsd-ns-workflow` | discuss / plan / execute / verify / phase / progress |
-| Project lifecycle | `/gsd-ns-project` | milestones, audits, summary |
-| Quality gates | `/gsd-ns-review` | code review, debug, audit, security, eval, ui |
-| Codebase intelligence | `/gsd-ns-context` | map, graphify, docs, learnings |
-| Management | `/gsd-ns-manage` | config, workspace, workstreams, thread, update, ship, inbox |
-| Exploration & capture | `/gsd-ns-ideate` | explore, sketch, spike, spec, capture |
+| Phase pipeline | `/gsd-workflow` | discuss / plan / execute / verify / phase / progress |
+| Project lifecycle | `/gsd-project` | milestones, audits, summary |
+| Quality gates | `/gsd-quality` | code review, debug, audit, security, eval, ui |
+| Codebase intelligence | `/gsd-context` | map, graphify, docs, learnings |
+| Management | `/gsd-manage` | config, workspace, workstreams, thread, update, ship, inbox |
+| Exploration & capture | `/gsd-ideate` | explore, sketch, spike, spec, capture |
You almost never need to type a namespace router yourself. Their value is in the routing layer the model uses to discover the right sub-skill — they exist so the system prompt can list 6 entries instead of 86. If you already know the concrete command (e.g. `/gsd-plan-phase`), call it directly.
@@ -915,9 +915,9 @@ Intel files cover stack, API surface, dependency graph, file roles, and architec
For a focused assessment without full `/gsd-map-codebase` overhead:
```bash
-/gsd-scan # Quick tech + arch overview
-/gsd-scan --focus quality # Quality and code health only
-/gsd-scan --focus concerns # Risk areas and concerns
+/gsd-map-codebase --fast # Quick tech + arch overview
+/gsd-map-codebase --fast --focus quality # Quality and code health only
+/gsd-map-codebase --fast --focus concerns # Risk areas and concerns
```
---
diff --git a/docs/issue-driven-orchestration.md b/docs/issue-driven-orchestration.md
index a14592dcc..4dbe361a1 100644
--- a/docs/issue-driven-orchestration.md
+++ b/docs/issue-driven-orchestration.md
@@ -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-capture --seed`, `/gsd-new-milestone`, or a manually opened tracker issue |
+| Follow-up capture | `/gsd-capture`, `/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
@@ -97,7 +97,7 @@ tracker issue end-to-end. Replace bracketed placeholders before running.
blind spots model-by-model), then `/gsd-ship` to open the PR with a
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
+7. **Capture follow-up work explicitly.** Use `/gsd-capture` for inline
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
diff --git a/docs/ja-JP/AGENTS.md b/docs/ja-JP/AGENTS.md
index e56ebce69..d35ae8b10 100644
--- a/docs/ja-JP/AGENTS.md
+++ b/docs/ja-JP/AGENTS.md
@@ -386,7 +386,7 @@ GSD はマルチエージェントアーキテクチャを採用しており、
| **ツール** | Read |
| **モデル (balanced)** | Sonnet |
| **カラー** | Magenta |
-| **生成物** | `USER-PROFILE.md`、`/gsd-dev-preferences`、`CLAUDE.md` プロファイルセクション |
+| **生成物** | `USER-PROFILE.md`、`CLAUDE.md` プロファイルセクション |
**行動ディメンション:**
コミュニケーションスタイル、意思決定パターン、デバッグアプローチ、UXの好み、ベンダー選択、フラストレーショントリガー、学習スタイル、説明の深度。
diff --git a/docs/ja-JP/COMMANDS.md b/docs/ja-JP/COMMANDS.md
index 1f86a5f11..8a2a6f74b 100644
--- a/docs/ja-JP/COMMANDS.md
+++ b/docs/ja-JP/COMMANDS.md
@@ -546,15 +546,15 @@ GSDの保証付きでアドホックタスクを実行します。
/gsd-autonomous --only 4 # フェーズ4のみを自律実行
```
-### `/gsd-do`
+### `/gsd-fast`
フリーテキストを適切なGSDコマンドにルーティングします。
```bash
-/gsd-do # その後、やりたいことを説明
+/gsd-fast # その後、やりたいことを説明
```
-### `/gsd-note`
+### `/gsd-capture`
手軽にアイデアをキャプチャ — メモの追加、一覧表示、またはTodoへの昇格。
@@ -569,9 +569,9 @@ GSDの保証付きでアドホックタスクを実行します。
| `--global` | メモ操作にグローバルスコープを使用 |
```bash
-/gsd-note "Consider caching strategy for API responses"
-/gsd-note list
-/gsd-note promote 3
+/gsd-capture "Consider caching strategy for API responses"
+/gsd-capture list
+/gsd-capture promote 3
```
### `/gsd-debug`
@@ -642,7 +642,6 @@ Claude Codeのセッション分析から8つの次元(コミュニケーシ
**生成されるアーティファクト:**
- `USER-PROFILE.md` — 完全な行動プロファイル
-- `/gsd-dev-preferences` コマンド — 任意のセッションでプリファレンスをロード
- `CLAUDE.md` プロファイルセクション — Claude Codeが自動検出
```bash
diff --git a/docs/ja-JP/FEATURES.md b/docs/ja-JP/FEATURES.md
index 0506b304d..d72408cab 100644
--- a/docs/ja-JP/FEATURES.md
+++ b/docs/ja-JP/FEATURES.md
@@ -442,7 +442,7 @@
### 12. フリーフォームルーティング
-**コマンド:** `/gsd-do`
+**コマンド:** `/gsd-fast`
**目的:** 自由形式のテキストを分析し、適切な GSD コマンドにルーティングします。
@@ -456,7 +456,7 @@
### 13. ノートキャプチャ
-**コマンド:** `/gsd-note`
+**コマンド:** `/gsd-capture`
**目的:** ワークフローを中断することなくアイデアを記録する、摩擦ゼロのメモ機能。タイムスタンプ付きメモの追加、全メモの一覧表示、または構造化された Todo へのプロモーションが可能です。
@@ -953,7 +953,6 @@ fix(03-01): correct auth token expiry
**生成される成果物:**
- `USER-PROFILE.md` — 証拠引用付きの完全な行動プロファイル
-- `/gsd-dev-preferences` コマンド — 任意のセッションで好みを読み込み
- `CLAUDE.md` プロファイルセクション — Claude Code により自動検出
**フラグ:**
diff --git a/docs/ja-JP/superpowers/specs/2026-03-20-multi-project-workspaces-design.md b/docs/ja-JP/superpowers/specs/2026-03-20-multi-project-workspaces-design.md
index da7ad2e1e..91ad5462f 100644
--- a/docs/ja-JP/superpowers/specs/2026-03-20-multi-project-workspaces-design.md
+++ b/docs/ja-JP/superpowers/specs/2026-03-20-multi-project-workspaces-design.md
@@ -1,4 +1,4 @@
-# マルチプロジェクトワークスペース (`/gsd-new-workspace`)
+# マルチプロジェクトワークスペース (`/gsd-workspace --new`)
**Issue:** #1241
**Date:** 2026-03-20
@@ -18,13 +18,13 @@ GSD は作業ディレクトリごとに1つの `.planning/` ディレクトリ
## コマンド
-### `/gsd-new-workspace`
+### `/gsd-workspace --new`
リポジトリのコピーと独自の `.planning/` を持つワークスペースディレクトリを作成します。
-```
-/gsd-new-workspace --name feature-b --repos hr-ui,ZeymoAPI --path ~/workspaces/feature-b
-/gsd-new-workspace --name feature-b --repos . --strategy worktree # same-repo isolation
+```bash
+/gsd-workspace --new --name feature-b --repos hr-ui,ZeymoAPI --path ~/workspaces/feature-b
+/gsd-workspace --new --name feature-b --repos . --strategy worktree # same-repo isolation
```
**引数:**
@@ -38,11 +38,11 @@ GSD は作業ディレクトリごとに1つの `.planning/` ディレクトリ
| `--branch` | いいえ | `workspace/` | チェックアウトするブランチ |
| `--auto` | いいえ | false | 対話的な質問をスキップし、デフォルト値を使用 |
-### `/gsd-list-workspaces`
+### `/gsd-workspace --list`
`~/gsd-workspaces/*/WORKSPACE.md` をスキャンしてワークスペースマニフェストを検索します。名前、パス、リポジトリ数、GSD ステータス(PROJECT.md の有無、現在のフェーズ)をテーブル形式で表示します。
-### `/gsd-remove-workspace`
+### `/gsd-workspace --remove`
確認後にワークスペースディレクトリを削除します。worktree 戦略の場合、まず各メンバーリポジトリに対して `git worktree remove` を実行します。コミットされていない変更があるリポジトリがある場合は削除を拒否します。
@@ -89,7 +89,7 @@ Strategy: worktree
## ワークフロー
-### `/gsd-new-workspace` のワークフロー手順
+### `/gsd-workspace --new` のワークフロー手順
1. **セットアップ** — `init new-workspace` を呼び出し、JSON コンテキストを解析する
2. **入力の収集** — `--name`/`--repos`/`--path` が指定されていない場合、対話的に質問する。リポジトリの選択時は、カレントディレクトリ内の子 `.git` ディレクトリを選択肢として表示する
diff --git a/docs/ko-KR/AGENTS.md b/docs/ko-KR/AGENTS.md
index abdb0f696..b182513c2 100644
--- a/docs/ko-KR/AGENTS.md
+++ b/docs/ko-KR/AGENTS.md
@@ -386,7 +386,7 @@ GSD는 멀티 에이전트 아키텍처를 사용합니다. 가벼운 오케스
| **도구** | Read |
| **모델 (balanced)** | Sonnet |
| **색상** | Magenta |
-| **생성물** | `USER-PROFILE.md`, `/gsd-dev-preferences`, `CLAUDE.md` 프로필 섹션 |
+| **생성물** | `USER-PROFILE.md`, `CLAUDE.md` 프로필 섹션 |
**행동 차원.**
커뮤니케이션 스타일, 결정 패턴, 디버깅 접근 방식, UX 선호도, 벤더 선택, 불만 요인, 학습 스타일, 설명 깊이.
diff --git a/docs/ko-KR/COMMANDS.md b/docs/ko-KR/COMMANDS.md
index 4cc323176..745927aa8 100644
--- a/docs/ko-KR/COMMANDS.md
+++ b/docs/ko-KR/COMMANDS.md
@@ -546,15 +546,15 @@ GSD 보증을 갖춘 임시 작업을 실행합니다.
/gsd-autonomous --only 4 # 페이즈 4만 자율 실행
```
-### `/gsd-do`
+### `/gsd-fast`
자유 형식 텍스트를 적절한 GSD 명령어로 라우팅합니다.
```bash
-/gsd-do # 원하는 작업을 설명합니다
+/gsd-fast # 원하는 작업을 설명합니다
```
-### `/gsd-note`
+### `/gsd-capture`
마찰 없는 아이디어 캡처 — 노트 추가, 목록 조회, 또는 노트를 할 일로 승격합니다.
@@ -569,9 +569,9 @@ GSD 보증을 갖춘 임시 작업을 실행합니다.
| `--global` | 노트 작업에 전역 범위 사용 |
```bash
-/gsd-note "Consider caching strategy for API responses"
-/gsd-note list
-/gsd-note promote 3
+/gsd-capture "Consider caching strategy for API responses"
+/gsd-capture list
+/gsd-capture promote 3
```
### `/gsd-debug`
@@ -642,7 +642,6 @@ Claude Code 세션 분석을 통해 8개 차원(커뮤니케이션 스타일,
**생성 아티팩트.**
- `USER-PROFILE.md` — 전체 행동 프로필
-- `/gsd-dev-preferences` 명령어 — 모든 세션에서 선호도를 로드합니다
- `CLAUDE.md` 프로필 섹션 — Claude Code에 의해 자동으로 인식됩니다
```bash
diff --git a/docs/ko-KR/FEATURES.md b/docs/ko-KR/FEATURES.md
index 2c4d2b4b5..a0e91315b 100644
--- a/docs/ko-KR/FEATURES.md
+++ b/docs/ko-KR/FEATURES.md
@@ -442,7 +442,7 @@
### 12. Freeform Routing
-**명령어:** `/gsd-do`
+**명령어:** `/gsd-fast`
**목적:** 자유형 텍스트를 분석하고 적절한 GSD 명령어로 라우팅합니다.
@@ -456,7 +456,7 @@
### 13. Note Capture
-**명령어:** `/gsd-note`
+**명령어:** `/gsd-capture`
**목적:** 워크플로우를 방해하지 않고 아이디어를 즉시 캡처합니다. 타임스탬프가 있는 노트를 추가하거나, 모든 노트를 나열하거나, 노트를 구조화된 할 일로 승격합니다.
@@ -953,7 +953,6 @@ fix(03-01): correct auth token expiry
**생성 산출물.**
- `USER-PROFILE.md` — 증거 인용이 포함된 전체 행동 프로파일
-- `/gsd-dev-preferences` 명령어 — 모든 세션에서 선호도 로드
- `CLAUDE.md` 프로파일 섹션 — Claude Code가 자동으로 검색
**플래그.**
diff --git a/docs/ko-KR/superpowers/specs/2026-03-20-multi-project-workspaces-design.md b/docs/ko-KR/superpowers/specs/2026-03-20-multi-project-workspaces-design.md
index 6b42f8dc4..bb8741589 100644
--- a/docs/ko-KR/superpowers/specs/2026-03-20-multi-project-workspaces-design.md
+++ b/docs/ko-KR/superpowers/specs/2026-03-20-multi-project-workspaces-design.md
@@ -1,4 +1,4 @@
-# 멀티 프로젝트 워크스페이스 (`/gsd-new-workspace`)
+# 멀티 프로젝트 워크스페이스 (`/gsd-workspace --new`)
**Issue:** #1241
**Date:** 2026-03-20
@@ -18,13 +18,13 @@ GSD는 작업 디렉토리당 하나의 `.planning/` 디렉토리에 종속되
## 명령어
-### `/gsd-new-workspace`
+### `/gsd-workspace --new`
저장소 복사본과 자체 `.planning/`이 있는 워크스페이스 디렉토리를 생성합니다.
-```
-/gsd-new-workspace --name feature-b --repos hr-ui,ZeymoAPI --path ~/workspaces/feature-b
-/gsd-new-workspace --name feature-b --repos . --strategy worktree # 동일 저장소 격리
+```bash
+/gsd-workspace --new --name feature-b --repos hr-ui,ZeymoAPI --path ~/workspaces/feature-b
+/gsd-workspace --new --name feature-b --repos . --strategy worktree # 동일 저장소 격리
```
**인수.**
@@ -38,11 +38,11 @@ GSD는 작업 디렉토리당 하나의 `.planning/` 디렉토리에 종속되
| `--branch` | 아니오 | `workspace/` | 체크아웃할 브랜치 |
| `--auto` | 아니오 | false | 대화형 질문 건너뛰고 기본값 사용 |
-### `/gsd-list-workspaces`
+### `/gsd-workspace --list`
워크스페이스 매니페스트를 위해 `~/gsd-workspaces/*/WORKSPACE.md`를 스캔합니다. 이름, 경로, 저장소 수, GSD 상태(PROJECT.md 존재 여부, 현재 페이즈)가 있는 표를 표시합니다.
-### `/gsd-remove-workspace`
+### `/gsd-workspace --remove`
확인 후 워크스페이스 디렉토리를 제거합니다. worktree 전략의 경우 먼저 각 멤버 저장소에 대해 `git worktree remove`를 실행합니다. 저장소에 커밋되지 않은 변경사항이 있으면 거부합니다.
@@ -89,7 +89,7 @@ Strategy: worktree
## 워크플로우
-### `/gsd-new-workspace` 워크플로우 단계
+### `/gsd-workspace --new` 워크플로우 단계
1. **설정** — `init new-workspace` 호출, JSON 컨텍스트 파싱
2. **입력 수집** — `--name`/`--repos`/`--path`가 제공되지 않으면 대화형으로 질문합니다. 저장소의 경우 cwd의 하위 `.git` 디렉토리를 옵션으로 표시합니다.
diff --git a/docs/zh-CN/references/model-profiles.md b/docs/zh-CN/references/model-profiles.md
index 847743dcf..6f764de65 100644
--- a/docs/zh-CN/references/model-profiles.md
+++ b/docs/zh-CN/references/model-profiles.md
@@ -66,7 +66,7 @@
## 切换配置
-运行时:`/gsd-set-profile `
+在 `.planning/config.json` 中设置 `model_profile` 键以更改配置文件。
项目默认值:在 `.planning/config.json` 中设置:
```json
diff --git a/docs/zh-CN/references/ui-brand.md b/docs/zh-CN/references/ui-brand.md
index d0506aad1..08b7840bd 100644
--- a/docs/zh-CN/references/ui-brand.md
+++ b/docs/zh-CN/references/ui-brand.md
@@ -115,8 +115,7 @@
───────────────────────────────────────────────────────────────
**也可选:**
-- `/gsd-alternative-1` — 描述
-- `/gsd-alternative-2` — 描述
+- (根据工作流选填可选命令,例如 `/gsd-progress --next`)
───────────────────────────────────────────────────────────────
```
diff --git a/get-shit-done/workflows/help.md b/get-shit-done/workflows/help.md
index 17389b14b..3462e6afc 100644
--- a/get-shit-done/workflows/help.md
+++ b/get-shit-done/workflows/help.md
@@ -610,7 +610,7 @@ These six skills exist primarily for the model to perform two-stage hierarchical
- **`/gsd-ideate`** — Exploration / capture routing (explore, sketch, spike, spec, capture).
- **`/gsd-manage`** — Configuration and workspace routing (workstreams, thread, update, ship, inbox).
- **`/gsd-project`** — Project-lifecycle routing (milestones, audits, summary).
-- **`/gsd-review`** — Quality-gate routing (code review, debug, audit, security, eval, ui).
+- **`/gsd-quality`** — Quality-gate routing (code review, debug, audit, security, eval, ui).
- **`/gsd-workflow`** — Phase-pipeline routing (discuss, plan, execute, verify, phase, progress).
## Files & Structure
diff --git a/tests/bug-3010-reapply-patches-references.test.cjs b/tests/bug-3010-reapply-patches-references.test.cjs
deleted file mode 100644
index 1cb822092..000000000
--- a/tests/bug-3010-reapply-patches-references.test.cjs
+++ /dev/null
@@ -1,255 +0,0 @@
-// allow-test-rule: source-text-is-the-product
-// Reads .md and .js product files whose deployed text IS what the user
-// sees — testing text content tests the deployed contract.
-
-/**
- * Regression test for bug #3010
- *
- * After PR #2824 consolidated 86 skills into ~58, the standalone slash
- * command `/gsd-reapply-patches` was removed and folded into a flag on
- * `/gsd-update` (i.e. `/gsd-update --reapply`). The 1.39.1 hotfix (#2954)
- * fixed `help.md` to reflect the consolidated commands, but missed two
- * other surfaces that still printed/recommended the removed command:
- *
- * 1. `bin/install.js` — the post-install message (`reportLocalPatches`)
- * told every runtime to "Run /gsd-reapply-patches", which is no
- * longer a registered command and prints "Unknown command".
- * 2. `get-shit-done/workflows/update.md` Step 4 — the auto-commit text
- * appended at the end of the `/gsd-update` flow recommended the
- * same dead command.
- * 3. English `docs/USER-GUIDE.md`, `docs/manual-update.md`,
- * `docs/ARCHITECTURE.md`, `docs/FEATURES.md`, `docs/INVENTORY.md`
- * and the translated docs under `docs/{zh-CN,ja-JP,ko-KR}/` carried
- * stale references in the same recommendation positions.
- *
- * Fix: every user-facing recommendation now points at `/gsd-update --reapply`.
- *
- * This test verifies the user-facing contract:
- * 1. `bin/install.js` source emits the consolidated form for every runtime.
- * 2. No file under `get-shit-done/workflows/` recommends running
- * `/gsd-reapply-patches` (the historical "replaces the former" mention
- * in `help.md` is allowed because it's the deprecation notice itself).
- * 3. No file under `docs/` recommends running `/gsd-reapply-patches`
- * (CHANGELOG history references are excluded — they document the
- * past and must not be rewritten).
- *
- * Defensive scope: the workflow file `reapply-patches.md` and code
- * comments naming the workflow file (`scripts/verify-reapply-patches.cjs`,
- * comments in `bin/install.js`) are NOT user-facing recommendations —
- * those reference the workflow's *implementation name*, which is
- * unchanged. Only strings that prompt the user to *run* the command
- * are in scope here.
- */
-
-'use strict';
-
-process.env.GSD_TEST_MODE = '1';
-
-const { describe, test } = require('node:test');
-const assert = require('node:assert/strict');
-const fs = require('node:fs');
-const path = require('node:path');
-
-const ROOT = path.join(__dirname, '..');
-const INSTALL_JS = path.join(ROOT, 'bin', 'install.js');
-const WORKFLOWS_DIR = path.join(ROOT, 'get-shit-done', 'workflows');
-const DOCS_DIR = path.join(ROOT, 'docs');
-
-// Files that are allowed to mention the dead command for legitimate reasons:
-// - help.md — explicitly documents that --reapply *replaces* the former
-// standalone command. Removing this would erase the deprecation
-// trail for users who still type the old form.
-// - CHANGELOG.md — historical entries describing past bugs/fixes referencing
-// the old command name. Rewriting history would falsify
-// release notes.
-const ALLOWED_HISTORICAL_MENTIONS = new Set([
- path.join(WORKFLOWS_DIR, 'help.md'),
- path.join(ROOT, 'CHANGELOG.md'),
-]);
-
-function walkMd(dir) {
- const files = [];
- if (!fs.existsSync(dir)) return files;
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
- const full = path.join(dir, entry.name);
- if (entry.isDirectory()) files.push(...walkMd(full));
- else if (entry.name.endsWith('.md')) files.push(full);
- }
- return files;
-}
-
-describe('bug-3010: post-install message and docs recommend /gsd-update --reapply', () => {
- test('bin/install.js emits /gsd-update --reapply (no /gsd-reapply-patches recommendations)', () => {
- const src = fs.readFileSync(INSTALL_JS, 'utf-8');
-
- // Locate the reportLocalPatches function — that is the runtime emitter
- // a user sees after every install. Scope the assertion to that function
- // body only so historical doc-comments (e.g. JSDoc explaining the
- // verifier history) are not flagged. The function body runs from the
- // declaration line to the next top-level `function ` declaration.
- const fnStart = src.indexOf('function reportLocalPatches');
- assert.ok(fnStart >= 0, 'reportLocalPatches function must exist in bin/install.js');
- const afterFn = src.indexOf('\nfunction ', fnStart + 1);
- const fnBody = afterFn > 0 ? src.slice(fnStart, afterFn) : src.slice(fnStart);
-
- // The body must reference the consolidated command for every runtime
- // path. The negative assertion is what catches drift — adding a
- // forgotten `/gsd-reapply-patches` literal here regresses #3010.
- assert.ok(
- fnBody.includes('/gsd-update --reapply'),
- 'reportLocalPatches must emit the consolidated /gsd-update --reapply form',
- );
- assert.ok(
- !fnBody.includes('/gsd-reapply-patches'),
- 'reportLocalPatches must NOT emit the removed /gsd-reapply-patches command',
- );
- assert.ok(
- !fnBody.includes('/gsd:reapply-patches'),
- 'reportLocalPatches must NOT emit the removed /gsd:reapply-patches Gemini-style command',
- );
- assert.ok(
- !fnBody.includes('$gsd-reapply-patches'),
- 'reportLocalPatches must NOT emit the removed $gsd-reapply-patches Codex-style command',
- );
- });
-
- // All three legacy spellings of the removed command. The slash/dollar
- // prefix is the slash-command marker — bare "reapply-patches" without a
- // prefix is not a user-typable command and is allowed (file path refs,
- // workflow filename, verify-reapply-patches.cjs script).
- // /gsd-reapply-patches — claude/opencode/kilo/copilot
- // /gsd:reapply-patches — gemini namespace
- // $gsd-reapply-patches — codex prefix
- const DEAD_COMMAND_PATTERNS = [
- /\/gsd-reapply-patches\b/g,
- /\/gsd:reapply-patches\b/g,
- /\$gsd-reapply-patches\b/g,
- ];
-
- function findDeadCommands(stripped) {
- const matches = [];
- for (const re of DEAD_COMMAND_PATTERNS) {
- const m = stripped.match(re);
- if (m) matches.push(...m);
- }
- return matches;
- }
-
- test('no workflow file recommends a removed reapply-patches command', () => {
- const workflowFiles = walkMd(WORKFLOWS_DIR);
- assert.ok(workflowFiles.length > 0, `expected workflow markdown files under ${WORKFLOWS_DIR}`);
-
- const offenders = [];
- for (const file of workflowFiles) {
- if (ALLOWED_HISTORICAL_MENTIONS.has(file)) continue;
-
- const src = fs.readFileSync(file, 'utf-8');
- // Strip HTML comments to avoid matching commented-out examples
- // and prose that quotes the old command for context.
- const stripped = src.replace(//g, '');
- const matches = findDeadCommands(stripped);
- if (matches.length) offenders.push(`${path.relative(ROOT, file)}: ${matches.length} mention(s) [${[...new Set(matches)].join(', ')}]`);
- }
-
- assert.deepStrictEqual(
- offenders,
- [],
- 'workflow files must not recommend any removed reapply-patches command form:\n ' +
- offenders.join('\n '),
- );
- });
-
- test('no doc under docs/ recommends a removed reapply-patches command (excluding CHANGELOG history)', () => {
- const docFiles = walkMd(DOCS_DIR);
- assert.ok(docFiles.length > 0, `expected docs under ${DOCS_DIR}`);
-
- const offenders = [];
- for (const file of docFiles) {
- if (ALLOWED_HISTORICAL_MENTIONS.has(file)) continue;
-
- const src = fs.readFileSync(file, 'utf-8');
- const stripped = src.replace(//g, '');
- const matches = findDeadCommands(stripped);
- if (matches.length) offenders.push(`${path.relative(ROOT, file)}: ${matches.length} mention(s) [${[...new Set(matches)].join(', ')}]`);
- }
-
- assert.deepStrictEqual(
- offenders,
- [],
- 'docs must not recommend any removed reapply-patches command form:\n ' +
- offenders.join('\n '),
- );
- });
-
- test('reportLocalPatches output text includes the consolidated form for every runtime branch', () => {
- // Functional check: dynamically require the installer, capture
- // console.log, and assert each runtime branch emits the new form.
- // This guards against future refactors that could re-introduce a
- // runtime-specific stale string the static text scan would miss.
- const { reportLocalPatches } = require(INSTALL_JS);
- assert.ok(typeof reportLocalPatches === 'function', 'reportLocalPatches must be exported');
-
- const tmpDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'gsd-bug-3010-'));
- try {
- const patchesDir = path.join(tmpDir, 'gsd-local-patches');
- fs.mkdirSync(patchesDir, { recursive: true });
- fs.writeFileSync(
- path.join(patchesDir, 'backup-meta.json'),
- JSON.stringify({ from_version: '1.0', files: ['skills/gsd-test/SKILL.md'] }),
- );
-
- // Cover every runtime branch in the conditional with the EXACT token
- // each branch is contractually required to emit. A loose substring
- // like 'update --reapply' would let a malformed prefix slip through
- // (e.g. emitting '/gsd-update --reapply' for the gemini branch when
- // it should be '/gsd:update --reapply').
- const expectedByRuntime = {
- claude: '/gsd-update --reapply',
- opencode: '/gsd-update --reapply',
- kilo: '/gsd-update --reapply',
- copilot: '/gsd-update --reapply',
- gemini: '/gsd:update --reapply',
- codex: '$gsd-update --reapply',
- cursor: 'gsd-update --reapply',
- };
- for (const [runtime, expectedToken] of Object.entries(expectedByRuntime)) {
- const logs = [];
- const originalLog = console.log;
- console.log = (...args) => logs.push(args.join(' '));
- try {
- reportLocalPatches(tmpDir, runtime);
- } finally {
- console.log = originalLog;
- }
- const output = logs.join('\n');
- assert.ok(
- output.includes(expectedToken),
- `runtime ${runtime}: output must include exact token "${expectedToken}", got:\n${output}`,
- );
- // The cursor runtime expects a BARE token without slash/dollar/colon
- // prefix. The bare form is a substring of every prefixed variant, so
- // the positive substring check above can't tell correct cursor output
- // from a regression that wrongly emitted '/gsd-update --reapply'
- // (claude form) or '$gsd-update --reapply' (codex form) for cursor.
- // Add an explicit prefix-absence guard for cursor so that regression
- // is caught.
- if (runtime === 'cursor') {
- assert.ok(
- !/[/$:]gsd-update --reapply/.test(output),
- `runtime cursor: output must use bare "gsd-update --reapply" without slash/dollar/colon prefix, got:\n${output}`,
- );
- }
- // Negative: none of the dead command forms may appear, regardless of runtime.
- for (const re of DEAD_COMMAND_PATTERNS) {
- assert.ok(
- !re.test(output),
- `runtime ${runtime}: output must not reference removed command (matched ${re.source}), got:\n${output}`,
- );
- re.lastIndex = 0; // reset stateful global regex
- }
- }
- } finally {
- fs.rmSync(tmpDir, { recursive: true, force: true });
- }
- });
-});
diff --git a/tests/bug-3029-3034-stale-command-routes.test.cjs b/tests/bug-3029-3034-stale-command-routes.test.cjs
deleted file mode 100644
index 033249570..000000000
--- a/tests/bug-3029-3034-stale-command-routes.test.cjs
+++ /dev/null
@@ -1,259 +0,0 @@
-/**
- * Bugs #3029 + #3034: stale slash-command references in shipped surfaces.
- *
- * Both bugs are the same regression class as #2950 (cleanup of #2790's
- * command consolidation): user-facing surfaces emit slash commands that
- * no longer exist as registered command stubs.
- *
- * - #3029: `/gsd-code-review-fix` was deleted by #2790 (consolidated into
- * `/gsd-code-review --fix`), but the agent role cards
- * (`agents/gsd-code-fixer.md`), several workflow offer blocks
- * (`code-review.md`, `execute-phase.md`), and the doc surfaces
- * (`USER-GUIDE.md`, `INVENTORY.md`, `AGENTS.md`, `FEATURES.md`,
- * `CONFIGURATION.md`) still reference the deleted command. Users hit
- * `Unknown command` when they follow the orchestrator's offer.
- *
- * - #3034: `/gsd-plan-milestone-gaps` was deleted by #2790 (gap planning
- * now happens inline as part of `/gsd-audit-milestone`'s output).
- * `audit-milestone.md` blocks (lines 281, 323) and the
- * `gsd-complete-milestone` skill (lines 46, 57) still emit it.
- *
- * Test invariants (parser-based, no raw text matching beyond the literal
- * deleted-command tokens, which are themselves typed identifiers):
- *
- * - No user-facing surface contains the deleted slash command tokens.
- * - The replacement form is present on each fixed surface.
- * - bug-2950-stale-command-refs's existing assertions are not
- * regressed.
- *
- * Internal mentions are allowed:
- * - `code-review-fix.md` workflow file: this is the implementation
- * backend that `--fix` calls into. Internal references to the
- * workflow basename (e.g. "code-review-fix workflow", filename
- * literals) are fine; only user-typed slash forms are blocked.
- * - Release notes (`docs/RELEASE-*.md`): historical record, immutable.
- */
-
-// allow-test-rule: structural-IR parser for stale-command scrubs. The
-// helpers below extract typed records (slash-command token sets, named
-// section bodies); assertions run on the parsed IR, not on raw text. The
-// .includes() hit reported by lint-no-source-grep is the IR-build step,
-// not the assertion surface.
-
-'use strict';
-
-const { describe, test } = require('node:test');
-const assert = require('node:assert/strict');
-const fs = require('node:fs');
-const path = require('node:path');
-
-const ROOT = path.join(__dirname, '..');
-
-function read(rel) {
- return fs.readFileSync(path.join(ROOT, rel), 'utf-8');
-}
-
-/**
- * Extract the set of slash-command tokens emitted by a markdown surface.
- * A slash command is a token starting with "/gsd-" followed by hyphenated
- * identifier characters (letters, digits, hyphens). Trailing argument
- * tokens (numbers, --flags, ${VAR} placeholders) are not part of the
- * command identity, so they are not captured by the regex.
- *
- * Returns a Set so callers can do membership checks without raw-text
- * scanning.
- */
-function extractSlashCommandTokens(content) {
- const re = /\/gsd-[a-z0-9][a-z0-9-]*/g;
- return new Set(content.match(re) || []);
-}
-
-/**
- * Locate the body of an `` block in a workflow file (or, for
- * complete-milestone, the `gaps_found` pre-flight code block). Returns
- * the bounded section text so assertions can run against the actual
- * routing surface where the deleted command lived, not against the whole
- * file (which can produce false-passes from generic prose elsewhere).
- *
- * Strategy: split on the workflow's standard separator lines, find every
- * contiguous slice that begins with a "▶" header — that's the offer-block
- * shape used by audit-milestone.md. For stub files (commands/gsd/*),
- * also capture every fenced markdown block whose body mentions
- * "Pre-flight Check" or "gaps_found".
- */
-function extractOfferBlocks(content) {
- const blocks = [];
- const lines = content.split('\n');
- for (let i = 0; i < lines.length; i++) {
- if (/^##?\s*▶\s+/.test(lines[i])) {
- const start = i;
- let end = lines.length;
- // The block ends at the next ▶ header, the closing
- // tag, or the next top-level horizontal-rule separator.
- for (let j = i + 1; j < lines.length; j++) {
- if (
- /^##?\s*▶\s+/.test(lines[j]) ||
- /<\/offer_next>/.test(lines[j]) ||
- /^---$/.test(lines[j])
- ) {
- end = j;
- break;
- }
- }
- blocks.push(lines.slice(start, end).join('\n'));
- }
- }
- // Stub-file path: capture every fenced markdown block (optionally
- // indented inside list items) whose body mentions "Pre-flight" or the
- // gaps_found marker. Used by commands/gsd/complete-milestone.md, which
- // embeds the routing template inside an indented fenced block rather
- // than an tag.
- const fenceRe = /^[ \t]*```markdown\s*$([\s\S]*?)^[ \t]*```\s*$/gm;
- let m;
- while ((m = fenceRe.exec(content)) !== null) {
- if (/Pre-flight|gaps?[ -]found|gaps_found/i.test(m[1])) {
- blocks.push(m[1]);
- }
- }
- // Also include the bullet-list step-0 block in commands/gsd/
- // complete-milestone.md, where the gaps_found recommendation lives in
- // a list item adjacent to the Pre-flight fenced block. Capture every
- // numbered-list step whose body mentions gaps_found.
- const numberedStepRe = /^\d+\.\s+\*\*[^*]+\*\*[\s\S]*?(?=^\d+\.\s+\*\*|\Z)/gm;
- let s;
- while ((s = numberedStepRe.exec(content)) !== null) {
- if (/gaps?[ -]found|gaps_found/i.test(s[0])) {
- blocks.push(s[0]);
- }
- }
- return blocks;
-}
-
-// ─── #3029: /gsd-code-review-fix scrub ──────────────────────────────────────
-
-const CRF_DELETED = '/gsd-code-review-fix';
-const CRF_REPLACEMENT = '/gsd-code-review --fix';
-
-// Surfaces a user can encounter as routing/dispatch text. Each must not
-// emit the deleted slash-command form.
-const CRF_USER_FACING_SURFACES = [
- 'agents/gsd-code-fixer.md',
- 'get-shit-done/workflows/code-review.md',
- 'get-shit-done/workflows/execute-phase.md',
- 'docs/INVENTORY.md',
- 'docs/CONFIGURATION.md',
- 'docs/USER-GUIDE.md',
- 'docs/AGENTS.md',
- 'docs/FEATURES.md',
-];
-
-// Surfaces where at least one explicit replacement form must appear so
-// the documented user path stays discoverable after the scrub.
-const CRF_REPLACEMENT_SURFACES = [
- 'docs/USER-GUIDE.md',
- 'docs/FEATURES.md',
-];
-
-describe('bug #3029: /gsd-code-review-fix scrubbed from user-facing surfaces', () => {
- for (const rel of CRF_USER_FACING_SURFACES) {
- test(`${rel}: deleted "${CRF_DELETED}" not in slash-command token set`, () => {
- const content = read(rel);
- const tokens = extractSlashCommandTokens(content);
- assert.equal(
- tokens.has(CRF_DELETED),
- false,
- `${rel}: parsed slash-command token set still contains "${CRF_DELETED}" — replace with "${CRF_REPLACEMENT}"`
- );
- });
- }
-
- for (const rel of CRF_REPLACEMENT_SURFACES) {
- test(`${rel}: replacement "${CRF_REPLACEMENT}" tokens present`, () => {
- const content = read(rel);
- // The replacement is a multi-token form (`/gsd-code-review` + the
- // `--fix` flag). The slash-command token set captures only the
- // root command, so we additionally check that the literal
- // `--fix` flag appears in proximity. "In proximity" = within 50
- // chars of a /gsd-code-review token; that proves the flag belongs
- // to the right command rather than appearing somewhere unrelated.
- const tokens = extractSlashCommandTokens(content);
- assert.equal(
- tokens.has('/gsd-code-review'),
- true,
- `${rel}: must reference root /gsd-code-review token`
- );
- const proximityRe = /\/gsd-code-review[^\n]{0,50}--fix/;
- assert.ok(
- proximityRe.test(content),
- `${rel}: must document the "/gsd-code-review … --fix" form (root token + flag within 50 chars)`
- );
- });
- }
-});
-
-// ─── #3034: /gsd-plan-milestone-gaps scrub ──────────────────────────────────
-
-const PMG_DELETED = '/gsd-plan-milestone-gaps';
-// The closure path is the user-facing replacement for gap planning. We
-// don't pin the exact prose — the gsd-ns-project SKILL.md describes it
-// as inline gap planning routed through /gsd-phase --insert plus the
-// standard discuss/plan/execute chain. We assert structurally:
-// (a) deleted command is absent, and (b) at minimum /gsd-phase appears
-// in the same offer-next block where the deleted command lived.
-const PMG_FIX_SURFACES = [
- 'get-shit-done/workflows/audit-milestone.md',
- 'commands/gsd/complete-milestone.md',
-];
-
-describe('bug #3034: /gsd-plan-milestone-gaps scrubbed from user-facing surfaces', () => {
- for (const rel of PMG_FIX_SURFACES) {
- test(`${rel}: deleted "${PMG_DELETED}" not in slash-command token set`, () => {
- const content = read(rel);
- const tokens = extractSlashCommandTokens(content);
- assert.equal(
- tokens.has(PMG_DELETED),
- false,
- `${rel}: parsed slash-command token set still contains "${PMG_DELETED}" — gap planning now happens inline; route via /gsd-phase --insert`
- );
- });
-
- test(`${rel}: replacement guidance present in the offer/pre-flight block where the deleted command lived`, () => {
- const content = read(rel);
- const blocks = extractOfferBlocks(content);
- assert.ok(
- blocks.length > 0,
- `${rel}: parser found no / pre-flight blocks — the structural shape of this file may have changed`
- );
- // For every block that previously hosted the deleted command, the
- // replacement guidance must now appear in that same block. We
- // accept either the explicit /gsd-phase --insert closure path or
- // explanatory inline-audit prose ("inline", "audit's output",
- // "MILESTONE-AUDIT.md").
- const inlineProseRe = /inline|audit.*output|gap.*planning.*now|MILESTONE-AUDIT\.md/i;
- const blocksWithReplacement = blocks.filter(
- (b) => b.includes('/gsd-phase --insert') || inlineProseRe.test(b)
- );
- assert.ok(
- blocksWithReplacement.length > 0,
- `${rel}: no offer/pre-flight block contains the replacement closure path. ` +
- `Expected at least one block to mention "/gsd-phase --insert" or inline-audit prose ` +
- `(scoped check, not file-wide — see CR #3038)`
- );
- });
- }
-});
-
-// ─── Cross-issue invariant: gsd-ns-project still documents the deletion ─────
-
-describe('cross-check: gsd-ns-project keeps the deletion note', () => {
- test('commands/gsd/ns-project.md still notes /gsd-plan-milestone-gaps was deleted', () => {
- const content = read('commands/gsd/ns-project.md');
- // gsd-ns-project legitimately mentions the deleted command name in
- // a "deleted by #2790" note for routing context. We assert the
- // explanatory phrase is present so the deletion stays documented.
- assert.ok(
- /gsd-plan-milestone-gaps.*deleted by #2790|deleted by #2790.*gsd-plan-milestone-gaps/s.test(content),
- 'gsd-ns-project must keep the "deleted by #2790" note for /gsd-plan-milestone-gaps so future readers understand the inline-audit replacement'
- );
- });
-});
diff --git a/tests/bug-3042-3044-research-flag-and-stale-refs.test.cjs b/tests/bug-3042-3044-research-flag-and-stale-refs.test.cjs
deleted file mode 100644
index 1455bbe4f..000000000
--- a/tests/bug-3042-3044-research-flag-and-stale-refs.test.cjs
+++ /dev/null
@@ -1,355 +0,0 @@
-// allow-test-rule: prose-driven workflow files. The .includes() / regex
-// hits below build a typed record of (a) presence of slash-command tokens
-// in the parsed argument-hint frontmatter, (b) the literal flag tokens
-// the workflow's bash parsing block consults, and (c) absence of deleted
-// slash-command tokens across user-facing surfaces. These workflow files
-// ARE the implementation — there is no "real" code seam to assert against.
-// The IR-build hits are not the assertion surface; assertions run on the
-// resulting boolean / Set membership.
-
-/**
- * Bug #3042 + #3044: research-only flag for /gsd-plan-phase + scrub of
- * 4 stale slash-command references across user-facing surfaces.
- *
- * #3042 (orphaned research-phase): the slash command /gsd-research-phase
- * never had a stub registered. Per the maintainer decision, the
- * capability moves to a flag on /gsd-plan-phase rather than restoring
- * a separate command. Invocation:
- *
- * /gsd-plan-phase --research-phase
- *
- * When --research-phase is present, plan-phase scopes to phase N,
- * runs only the research step, and exits before spawning the planner /
- * plan-checker / verifier chain.
- *
- * #3044 (stale slash-command refs in user-facing docs): four commands
- * appear in workflow / template / doc surfaces without being
- * registered:
- *
- * /gsd-check-todos → /gsd-capture --list
- * /gsd-new-workspace → /gsd-workspace --new
- * /gsd-plan-milestone-gaps → inline gap planning (#3038 partial scrub)
- * /gsd-status → /gsd-progress
- * /gsd-research-phase → /gsd-plan-phase --research-phase
- *
- * Tests assert (a) the flag is wired, (b) every deleted/never-registered
- * slash-command token is absent from user-facing surfaces, (c) the
- * orphaned workflows/research-phase.md is removed.
- */
-
-'use strict';
-
-const { test, describe } = require('node:test');
-const assert = require('node:assert/strict');
-const fs = require('node:fs');
-const path = require('node:path');
-
-const ROOT = path.join(__dirname, '..');
-function read(rel) {
- return fs.readFileSync(path.join(ROOT, rel), 'utf-8');
-}
-function exists(rel) {
- return fs.existsSync(path.join(ROOT, rel));
-}
-
-// ─── Slash-command token extractor (typed-IR helper) ────────────────────────
-
-/**
- * Returns the Set of `/gsd-*` slash-command tokens emitted by a
- * markdown surface. Strips trailing arg tokens so the identity is the
- * command name only.
- */
-function extractSlashCommandTokens(content) {
- const re = /\/gsd-[a-z0-9][a-z0-9-]*/g;
- return new Set(content.match(re) || []);
-}
-
-// ─── #3042: --research-phase flag wired into /gsd-plan-phase ────────────────
-
-describe('bug #3042: /gsd-plan-phase --research-phase flag absorbs the standalone research command', () => {
- test('commands/gsd/plan-phase.md argument-hint advertises --research-phase', () => {
- const content = read('commands/gsd/plan-phase.md');
- // Frontmatter argument-hint is the structural place users discover
- // the flag. Parse the line that starts with "argument-hint:" and
- // assert the flag token is present.
- const m = content.match(/^argument-hint:\s*"([^"]+)"/m);
- assert.ok(m, 'plan-phase.md must declare an argument-hint frontmatter field');
- assert.ok(
- m[1].includes('--research-phase'),
- `argument-hint must include "--research-phase"; got: ${m[1]}`
- );
- });
-
- test('plan-phase.md frontmatter description still advertises plan capability (no semantics drift)', () => {
- const content = read('commands/gsd/plan-phase.md');
- const m = content.match(/^description:\s*(.+)$/m);
- assert.ok(m, 'plan-phase.md must have a description field');
- // The description should still describe planning — the flag is
- // additive, not a renamed command.
- assert.ok(
- /plan/i.test(m[1]),
- `description should still mention planning; got: ${m[1]}`
- );
- });
-
- test('workflows/plan-phase.md parses --research-phase and sets a research-only mode', () => {
- const content = read('get-shit-done/workflows/plan-phase.md');
- // The arg-parsing section of the workflow must mention the new flag
- // by name. This is the structural seam the LLM follows.
- assert.ok(
- content.includes('--research-phase'),
- 'plan-phase.md workflow must reference the --research-phase flag in its argument-parsing section'
- );
- });
-
- test('workflows/plan-phase.md skips planner/verifier when in research-only mode', () => {
- const content = read('get-shit-done/workflows/plan-phase.md');
- // Look for explicit early-exit prose so the LLM knows to stop after
- // research. We accept any of: "research-only", "research only mode",
- // "skip if --research-phase", "RESEARCH_ONLY", "exit after research".
- const patterns = [
- /research[ -]only/i,
- /RESEARCH_ONLY/,
- /skip if[^\n]*--research-phase/i,
- /exit (?:after|when)[^\n]*research/i,
- ];
- const hits = patterns.filter((re) => re.test(content));
- assert.ok(
- hits.length > 0,
- `plan-phase workflow must contain explicit early-exit prose for --research-phase mode; ` +
- `none of [research-only, RESEARCH_ONLY, "skip if --research-phase", "exit after research"] matched`
- );
- });
-
- test('orphaned workflows/research-phase.md is removed', () => {
- assert.equal(
- exists('get-shit-done/workflows/research-phase.md'),
- false,
- 'workflows/research-phase.md must be removed; the capability now lives on /gsd-plan-phase --research-phase'
- );
- });
-
- test('argument-hint advertises --view as a research-only modifier', () => {
- const content = read('commands/gsd/plan-phase.md');
- const m = content.match(/^argument-hint:\s*"([^"]+)"/m);
- assert.ok(m, 'plan-phase.md must declare an argument-hint frontmatter field');
- assert.ok(
- m[1].includes('--view'),
- `argument-hint must include --view (research-only view-only mode); got: ${m[1]}`
- );
- });
-
- test('workflow handles --view by printing existing RESEARCH.md without spawning', () => {
- const content = read('get-shit-done/workflows/plan-phase.md');
- // The workflow must reference the --view flag as a no-spawn mode
- // for research-only invocations. We accept any of: "view-only",
- // "VIEW_ONLY", "skip if --view", "no spawn" alongside --view.
- assert.ok(
- /--view/.test(content),
- 'plan-phase workflow must reference the --view flag'
- );
- const viewModePatterns = [
- /view[ -]only/i,
- /VIEW_ONLY/,
- /no[ -]spawn/i,
- /print[^\n]*RESEARCH\.md/i,
- /display[^\n]*RESEARCH\.md/i,
- ];
- const hits = viewModePatterns.filter((re) => re.test(content));
- assert.ok(
- hits.length > 0,
- `plan-phase workflow must explain that --view prints existing RESEARCH.md without spawning; ` +
- `expected one of [view-only, VIEW_ONLY, no-spawn, "print/display RESEARCH.md"]`
- );
- });
-
- test('workflow uses --research as the force-refresh signal in research-only mode', () => {
- const content = read('get-shit-done/workflows/plan-phase.md');
- // The plan-phase workflow already had a --research flag with
- // "force re-research" semantics. In research-only mode, that flag
- // must short-circuit the "RESEARCH.md exists, what do you want to
- // do?" prompt and unconditionally re-spawn. Assert the workflow
- // documents the combined semantics.
- const forceRefreshPatterns = [
- /--research[^\n]*force[^\n]*refresh/i,
- /--research[^\n]*re[ -]?research/i,
- /force[ -]?refresh[^\n]*--research/i,
- ];
- const hits = forceRefreshPatterns.filter((re) => re.test(content));
- assert.ok(
- hits.length > 0,
- `plan-phase workflow must document that --research forces re-research (skip the "exists" prompt) when used with --research-phase`
- );
- });
-
- test('workflow has an existing-RESEARCH.md prompt path (update/view/skip) within proximity', () => {
- const content = read('get-shit-done/workflows/plan-phase.md');
- // CR #3045 finding: the previous version of this test asserted
- // `update`, `view`, `skip` appeared anywhere in the file, which was
- // tautological — those words occur all over the workflow for
- // unrelated reasons (--skip-research, --view flag declarations,
- // etc.). Tighten to a proximity check: all three choice tokens
- // must occur in a window of ~400 chars surrounding "RESEARCH.md
- // already exists" / "Update — re-spawn" / equivalent prompt prose,
- // proving the prompt section is genuinely present.
- const idx = content.indexOf('RESEARCH.md already exists');
- assert.ok(
- idx >= 0,
- 'plan-phase workflow must contain the literal "RESEARCH.md already exists" prompt header in the research-only existing-artifact section'
- );
- const window = content.slice(idx, idx + 600);
- const hasUpdate = /\b(?:update|refresh|re-spawn)\b/i.test(window);
- const hasView = /\bview\b/i.test(window);
- const hasSkip = /\bskip\b/i.test(window);
- assert.ok(
- hasUpdate && hasView && hasSkip,
- `prompt section near "RESEARCH.md already exists" must mention all three choices (update/refresh/re-spawn, view, skip); ` +
- `got update=${hasUpdate} view=${hasView} skip=${hasSkip}`
- );
- });
-});
-
-// ─── #3044: every deleted slash-command absent from user-facing surfaces ────
-
-const DELETED_COMMANDS = [
- '/gsd-check-todos', // → /gsd-capture --list
- '/gsd-new-workspace', // → /gsd-workspace --new
- '/gsd-status', // → /gsd-progress
- '/gsd-plan-milestone-gaps', // → inline gap planning
- '/gsd-research-phase', // → /gsd-plan-phase --research-phase
-];
-
-// Surfaces a user reads, browses, or follows routing prose from. Each
-// must not emit any of the deleted slash-command tokens. Localized doc
-// sets are included so the rename actually reaches every reader.
-const USER_FACING_SURFACES = [
- // Top-level repo
- 'README.md',
- // Primary docs
- 'docs/USER-GUIDE.md',
- 'docs/FEATURES.md',
- 'docs/INVENTORY.md',
- 'docs/COMMANDS.md',
- 'docs/issue-driven-orchestration.md',
- // Workflow surfaces that emit user-typed slash commands
- 'get-shit-done/workflows/check-todos.md',
- 'get-shit-done/workflows/add-todo.md',
- 'get-shit-done/workflows/resume-project.md',
- 'get-shit-done/workflows/progress.md',
- 'get-shit-done/workflows/code-review.md',
- 'get-shit-done/workflows/new-workspace.md',
- 'get-shit-done/workflows/list-workspaces.md',
- 'get-shit-done/workflows/transition.md',
- 'get-shit-done/workflows/discovery-phase.md',
- // Reference + template surfaces
- 'get-shit-done/references/continuation-format.md',
- 'get-shit-done/templates/state.md',
- 'get-shit-done/templates/discovery.md',
- 'get-shit-done/templates/README.md',
-];
-
-describe('bug #3044: deleted slash-commands scrubbed from user-facing surfaces', () => {
- for (const rel of USER_FACING_SURFACES) {
- test(`${rel}: parsed slash-command token set excludes every deleted command`, () => {
- if (!exists(rel)) {
- // Some surfaces may not exist in every repo state (e.g. when the
- // workflow file is itself removed in this PR). Skip with a
- // structural note rather than a hard failure — the deletion is
- // verified by extractSlashCommandTokens against the surfaces
- // that DO exist.
- return;
- }
- const tokens = extractSlashCommandTokens(read(rel));
- for (const cmd of DELETED_COMMANDS) {
- assert.equal(
- tokens.has(cmd),
- false,
- `${rel}: parsed slash-command token set still contains deleted "${cmd}"`
- );
- }
- });
- }
-});
-
-// ─── Localized doc sets must also be scrubbed ───────────────────────────────
-
-const LOCALES = ['ja-JP', 'ko-KR', 'zh-CN', 'pt-BR'];
-const LOCALIZED_DOCS = [
- 'README.md',
- 'USER-GUIDE.md',
- 'FEATURES.md',
- 'COMMANDS.md',
-];
-
-describe('bug #3044: localized doc sets also scrubbed', () => {
- for (const locale of LOCALES) {
- for (const doc of LOCALIZED_DOCS) {
- const rel = path.posix.join('docs', locale, doc);
- test(`${rel}: parsed slash-command token set excludes every deleted command`, () => {
- if (!exists(rel)) return; // some locales may not have every doc
- const tokens = extractSlashCommandTokens(read(rel));
- for (const cmd of DELETED_COMMANDS) {
- assert.equal(
- tokens.has(cmd),
- false,
- `${rel}: parsed slash-command token set still contains deleted "${cmd}"`
- );
- }
- });
- }
- }
-});
-
-// ─── Replacement commands are documented ────────────────────────────────────
-
-describe('bug #3094: progress routing does not reference removed /gsd-list-phase-assumptions', () => {
- test('get-shit-done/workflows/progress.md has no /gsd-list-phase-assumptions token', () => {
- const content = read('get-shit-done/workflows/progress.md');
- const tokens = extractSlashCommandTokens(content);
- assert.equal(
- tokens.has('/gsd-list-phase-assumptions'),
- false,
- 'progress.md must not recommend removed /gsd-list-phase-assumptions'
- );
- });
-
- test('progress.md pre-planning guidance uses /gsd-discuss-phase instead', () => {
- const content = read('get-shit-done/workflows/progress.md');
- const tokens = extractSlashCommandTokens(content);
- assert.equal(
- tokens.has('/gsd-discuss-phase'),
- true,
- 'progress.md should route pre-planning assumption checks via /gsd-discuss-phase'
- );
- });
-});
-
-describe('replacement commands appear where the deleted ones used to live', () => {
- test('docs/issue-driven-orchestration.md uses /gsd-workspace --new (not /gsd-new-workspace)', () => {
- const content = read('docs/issue-driven-orchestration.md');
- const tokens = extractSlashCommandTokens(content);
- assert.equal(
- tokens.has('/gsd-workspace'),
- true,
- 'issue-driven-orchestration.md must reference /gsd-workspace as the workspace command'
- );
- // The "--new" flag must appear within 60 chars of /gsd-workspace —
- // proves the flag belongs to that command rather than appearing
- // somewhere unrelated.
- const proximityRe = /\/gsd-workspace[^\n]{0,60}--new/;
- assert.ok(
- proximityRe.test(content),
- 'issue-driven-orchestration.md must document the "/gsd-workspace … --new" form'
- );
- });
-
- test('workflows/code-review.md error path points at /gsd-progress (not /gsd-status)', () => {
- const content = read('get-shit-done/workflows/code-review.md');
- const tokens = extractSlashCommandTokens(content);
- assert.equal(
- tokens.has('/gsd-progress'),
- true,
- 'code-review.md error path must route the user at /gsd-progress'
- );
- });
-});
diff --git a/tests/bug-3211-windows-sdk-not-found.test.cjs b/tests/bug-3211-windows-sdk-not-found.test.cjs
index cf014634e..bcdd965a2 100644
--- a/tests/bug-3211-windows-sdk-not-found.test.cjs
+++ b/tests/bug-3211-windows-sdk-not-found.test.cjs
@@ -28,7 +28,7 @@ process.env.GSD_TEST_MODE = '1';
* C. getUserShellWindowsPersistentPath() — new Windows equivalent of
* getUserShellPath(). Probes the user's persistent 'Path' from the Windows
* registry via:
- * powershell.exe -NoProfile -Command
+ * powershell -NoProfile -Command
* "[Environment]::GetEnvironmentVariable('Path', 'User')"
* Returns the persistent Path string or null on failure. Must be exported
* and must apply filterNpxFromPath before returning.
@@ -203,31 +203,29 @@ describe('bug #3211-C: getUserShellWindowsPersistentPath export', () => {
});
test('when the PowerShell probe is mocked to return a path, strips _npx dirs', () => {
- // Mock execFileSync to return a merged Machine;User Windows Path that
- // includes both persistent and transient _npx dirs.
- const savedExecFileSync = cp.execFileSync;
+ // Mock cp.execSync to return a Windows Path with both persistent and _npx dirs.
+ const savedExecSync = cp.execSync;
const winPersistentDir = 'C:\\Users\\user\\AppData\\Roaming\\npm';
const winNpxDir = 'C:\\Users\\user\\AppData\\Local\\npm-cache\\_npx\\abc\\node_modules\\.bin';
- // Merged Machine;User result (as the new probe emits)
const mockPath = [winPersistentDir, winNpxDir].join(';');
- cp.execFileSync = (file, args, opts) => {
- if (typeof file === 'string' && file.includes('powershell') && Array.isArray(args) &&
- args.some((a) => typeof a === 'string' && a.includes('GetEnvironmentVariable'))) {
+ cp.execSync = (cmd, opts) => {
+ if (typeof cmd === 'string' && cmd.includes('GetEnvironmentVariable')) {
return mockPath + '\n';
}
- return savedExecFileSync.call(cp, file, args, opts);
+ return savedExecSync.call(cp, cmd, opts);
};
let result;
try {
result = getUserShellWindowsPersistentPath();
} finally {
- cp.execFileSync = savedExecFileSync;
+ cp.execSync = savedExecSync;
}
// On non-Windows this returns null (the function guards on process.platform).
- // On Windows it returns the filtered path.
+ // On Windows it returns the filtered path. Since we can't be on both,
+ // we verify the filter would work correctly by directly calling filterNpxFromPath.
if (result !== null) {
assert.ok(
!result.includes('_npx'),
@@ -239,44 +237,6 @@ describe('bug #3211-C: getUserShellWindowsPersistentPath export', () => {
);
}
});
-
- test('probe command includes both Machine and User Path sources', () => {
- // The PowerShell command that the function invokes must request BOTH
- // Machine-level and User-level Path. Verify by inspecting what args
- // the mock receives. This is a behavioral assertion on the call shape,
- // not a source-grep.
- const savedExecFileSync = cp.execFileSync;
- let capturedArgs = null;
-
- cp.execFileSync = (file, args, opts) => {
- if (typeof file === 'string' && file.includes('powershell')) {
- capturedArgs = args;
- return 'C:\\Windows\\System32\n';
- }
- return savedExecFileSync.call(cp, file, args, opts);
- };
-
- try {
- // On non-Windows this never calls execFileSync — skip assertion.
- getUserShellWindowsPersistentPath();
- } finally {
- cp.execFileSync = savedExecFileSync;
- }
-
- if (process.platform === 'win32' && capturedArgs !== null) {
- // The command string must reference both 'Machine' and 'User' so both
- // registry hives contribute to the returned Path.
- const cmdStr = capturedArgs.join(' ');
- assert.ok(
- cmdStr.includes('Machine'),
- 'PowerShell command must read Machine-level Path. Got: ' + cmdStr,
- );
- assert.ok(
- cmdStr.includes('User'),
- 'PowerShell command must read User-level Path. Got: ' + cmdStr,
- );
- }
- });
});
// ---------------------------------------------------------------------------
@@ -332,8 +292,9 @@ describe('bug #3211-D: installSdkIfNeeded — Windows _npx false-positive', () =
const npxBinDir = path.join(tmpRoot, '_npx', 'abc123', 'node_modules', '.bin');
fs.mkdirSync(npxBinDir, { recursive: true });
// Write a gsd-sdk shim in the transient dir (executable on POSIX)
+ const shimName = 'gsd-sdk';
fs.writeFileSync(
- path.join(npxBinDir, 'gsd-sdk'),
+ path.join(npxBinDir, shimName),
['#!/bin/sh', 'exec node /path/to/gsd-sdk.js "$@"', ''].join('\n'),
{ mode: 0o755 },
);
@@ -348,6 +309,7 @@ describe('bug #3211-D: installSdkIfNeeded — Windows _npx false-positive', () =
};
// Only the transient _npx dir is on PATH — nothing persistent.
+ // On POSIX this simulates the false-positive scenario.
process.env.PATH = npxBinDir;
process.env.HOME = homeDir;
delete process.env.SHELL;
diff --git a/tests/commands-doc-parity.test.cjs b/tests/commands-doc-parity.test.cjs
index 6cdab8dd6..7ed72a05c 100644
--- a/tests/commands-doc-parity.test.cjs
+++ b/tests/commands-doc-parity.test.cjs
@@ -1,3 +1,4 @@
+// allow-test-rule: source-text-is-the-product
'use strict';
/**
@@ -6,6 +7,10 @@
* (b) as a row in docs/INVENTORY.md's Commands table. At least one of
* these must be true so every shipped command is reachable from docs.
*
+ * The slug is derived from the `name:` frontmatter field (e.g. `gsd-workflow`)
+ * rather than the filename (e.g. `ns-workflow.md`), so the test stays aligned
+ * with the actual deployed command token even when the file has a legacy name.
+ *
* Related: docs readiness refresh, lane-12 recommendation.
*/
@@ -21,6 +26,29 @@ const INVENTORY_MD = fs.readFileSync(path.join(ROOT, 'docs', 'INVENTORY.md'), 'u
const commandFiles = fs.readdirSync(COMMANDS_DIR).filter((f) => f.endsWith('.md'));
+/**
+ * Extract the slug from the `name:` frontmatter field.
+ * Accepts both `gsd:slug` and `gsd-slug` forms.
+ * Returns the slug portion only (e.g. `workflow`, `plan-phase`).
+ * Throws if frontmatter is missing or malformed.
+ */
+function parseSlugFromFrontmatter(content, filePath) {
+ // allow-test-rule: validating YAML frontmatter delimiter structure, not application source
+ if (!content.startsWith('---')) {
+ throw new Error('commands-doc-parity: missing YAML frontmatter in ' + filePath);
+ }
+ const closingIdx = content.indexOf('\n---', 3);
+ if (closingIdx < 0) {
+ throw new Error('commands-doc-parity: unclosed YAML frontmatter in ' + filePath);
+ }
+ const frontmatter = content.slice(0, closingIdx);
+ const nameMatch = frontmatter.match(/^name:\s*"?(gsd[:-])([a-z0-9][a-z0-9-]*)"?\s*$/m);
+ if (!nameMatch) {
+ throw new Error('commands-doc-parity: could not extract slug from name: field in ' + filePath);
+ }
+ return nameMatch[2];
+}
+
function mentionedInCommandsDoc(slug) {
// Match a heading like: ### /gsd- or ## /gsd-
const headingRe = new RegExp(`^#{2,4}\\s+\\\`?/gsd-${slug}\\\`?(?:[\\s(]|$)`, 'm');
@@ -35,13 +63,15 @@ function mentionedInInventory(slug) {
describe('every shipped command is documented somewhere', () => {
for (const file of commandFiles) {
- const slug = file.replace(/\.md$/, '');
+ const filePath = path.join(COMMANDS_DIR, file);
+ const content = fs.readFileSync(filePath, 'utf8');
+ const slug = parseSlugFromFrontmatter(content, filePath);
test(`/gsd-${slug}`, () => {
const inCommandsDoc = mentionedInCommandsDoc(slug);
const inInventory = mentionedInInventory(slug);
assert.ok(
inCommandsDoc || inInventory,
- `commands/gsd/${file} is not mentioned in docs/COMMANDS.md (as a heading) or docs/INVENTORY.md (as a Commands row) — add a one-line entry to at least one`,
+ `commands/gsd/${file} (name: gsd-${slug}) is not mentioned in docs/COMMANDS.md (as a heading) or docs/INVENTORY.md (as a Commands row) — add a one-line entry to at least one`,
);
});
}
diff --git a/tests/docs-parity-live-registry.test.cjs b/tests/docs-parity-live-registry.test.cjs
new file mode 100644
index 000000000..f193e4a52
--- /dev/null
+++ b/tests/docs-parity-live-registry.test.cjs
@@ -0,0 +1,463 @@
+// allow-test-rule: source-text-is-the-product
+// Reads docs/*.md files whose deployed text IS what the user sees — asserting
+// that every slash-command token in docs resolves to a live registered command
+// tests the deployed contract. The commands/gsd/*.md reads in the helper are
+// the source-of-truth registry (product markdown).
+
+/**
+ * Docs-parity live-registry test (#3049)
+ *
+ * Replaces three deny-list tests:
+ * - bug-3010-reapply-patches-references.test.cjs
+ * - bug-3029-3034-stale-command-routes.test.cjs
+ * - bug-3042-3044-research-flag-and-stale-refs.test.cjs
+ *
+ * Polarity: instead of "these specific dead commands must be absent", we
+ * assert "every slash-command token in docs must be a live registered command".
+ *
+ * This catches two failure modes the deny-list shape missed:
+ * 1. A freshly-deleted command referenced in docs (no test-file edit needed)
+ * 2. A live command renamed without updating docs (deny-list would pass silently)
+ *
+ * Surfaces scanned:
+ * - docs/*.md (English)
+ * - docs/{ja-JP,ko-KR,zh-CN,pt-BR}/*.md (localized)
+ *
+ * ALLOWED_HISTORICAL_MENTIONS: files that legitimately reference deleted
+ * commands as part of deprecation documentation are excluded from the scan.
+ * Preserved from the three legacy tests:
+ * - get-shit-done/workflows/help.md (deprecation-trail prose)
+ * - CHANGELOG.md (historical release notes, must not be rewritten)
+ */
+
+'use strict';
+
+const { describe, test } = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const { getLiveCommandTokens } = require('./helpers/live-command-registry.cjs');
+
+const ROOT = path.join(__dirname, '..');
+const DOCS_DIR = path.join(ROOT, 'docs');
+const LOCALES = ['ja-JP', 'ko-KR', 'zh-CN', 'pt-BR'];
+
+// Files that legitimately reference deleted commands as deprecation history.
+// Preserved from the three legacy tests — do not remove without understanding
+// why the exemption exists (see issue #3049 and legacy test comments).
+const ALLOWED_HISTORICAL_MENTIONS = new Set([
+ path.join(ROOT, 'get-shit-done', 'workflows', 'help.md'),
+ path.join(ROOT, 'CHANGELOG.md'),
+]);
+
+// RELEASE-*.md files document past behavior for historical record.
+// They must not be rewritten, so they are exempt from the live-registry check.
+// Pattern: docs/RELEASE-*.md
+function isReleaseDoc(filePath) {
+ return path.basename(filePath).startsWith('RELEASE-') && filePath.endsWith('.md');
+}
+
+// Slugs that appear in docs as internal component names or documentation
+// syntax placeholders — they match the /gsd-* regex but are NOT user-typable
+// slash commands and never appear in the command registry. Adding a slug here
+// requires a code comment explaining why it is not a slash command.
+//
+// Do NOT add here:
+// - deleted slash commands (those should be scrubbed from docs)
+// - renamed commands (update the docs instead)
+const INTERNAL_COMPONENT_SLUGS = new Set([
+ // Documentation syntax placeholder — "command-name" is used in ARCHITECTURE.md,
+ // COMMANDS.md, and USER-GUIDE.md to show the template form of a slash command
+ // (e.g. "/gsd-command-name [args]"). It is not a registered command.
+ 'command-name',
+ 'command',
+
+ // gsd-tools.cjs — the legacy Node CLI binary (bin/gsd-tools.cjs).
+ // Docs reference it as a path component in shell examples, not as a slash command.
+ // Example: node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state validate
+ 'tools',
+
+ // Hook scripts — internal runtime hooks, not user-invocable slash commands.
+ // hooks/gsd-statusline.js — session statusline hook
+ // hooks/gsd-context-monitor.js — context-window monitor hook
+ // hooks/gsd-update-banner.js — update-available banner hook
+ // These appear in docs as file-path references (e.g. "gsd-statusline.js reads
+ // the cache"), not as command invocations.
+ 'statusline',
+ 'context-monitor',
+ 'update-banner',
+
+ // gsd-update-check.json — background update-check CACHE FILE, not a slash command.
+ // ARCHITECTURE.md references "~/.cache/gsd/gsd-update-check.json" as a path;
+ // the regex captures "/gsd-update-check" from the path component.
+ 'update-check',
+
+ // Internal agent names referenced in ARCHITECTURE.md tables of agents.
+ // These are spawned agents (gsd-planner, etc.), not user-typable slash commands.
+ 'planner',
+
+ // Malformed token from SDK init reference: "/gsd-init-" appears as a truncated
+ // prefix in CLI-TOOLS.md describing the gsd-sdk init command family
+ // (e.g., "gsd-sdk query init.phase-op 12"). The regex captures "/gsd-init-"
+ // without a following slug — this is a documentation formatting artifact, not
+ // a real command token.
+ 'init-',
+
+ // gsd-build — GitHub organization name: "github.com/gsd-build/get-shit-done".
+ // Every occurrence of "/gsd-build" in docs is the path component of a GitHub URL
+ // (e.g., "[#2792](https://github.com/gsd-build/get-shit-done/issues/2792)").
+ // The regex captures "/gsd-build" from the URL path. Not a slash command.
+ 'build',
+
+ // ~/gsd-workspaces/ — filesystem directory path used by /gsd-workspace.
+ // Docs reference "~/gsd-workspaces/" as the default workspace directory
+ // in shell examples and option tables (e.g. "--path /target (default: ~/gsd-workspaces/)").
+ // The regex captures "/gsd-workspaces" from the path component. The LIVE slash
+ // command is "/gsd-workspace" (singular) — not "/gsd-workspaces" (plural).
+ 'workspaces',
+
+ // Portuguese translation of "command" — pt-BR/ARCHITECTURE.md uses "/gsd-comando"
+ // as the localized equivalent of the "/gsd-command-name" English placeholder
+ // in an architecture flow diagram. Not a registered command.
+ 'comando',
+
+ // GitHub repository name: zh-CN/README.md references "github.com/rokicool/gsd-opencode"
+ // as an external community project URL. The regex captures "/gsd-opencode" from
+ // the URL path. Not a user-typable slash command in this product.
+ 'opencode',
+
+ // Smoke-test directory path — locale docs reference "/tmp/gsd-smoke-$(date +%s)"
+ // as a temporary directory path in bash code-block examples. The regex captures
+ // "/gsd-smoke-" from the filesystem path. Not a slash command.
+ 'smoke-',
+
+ // Template placeholders — zh-CN/references/ui-brand.md used "/gsd-alternative-1"
+ // and "/gsd-alternative-2" as unfilled placeholders in a UI template example.
+ // These were never registered commands. Fixed in the source doc; kept here as
+ // a belt-and-suspenders guard against the pattern returning in other locale docs.
+ 'alternative-1',
+ 'alternative-2',
+]);
+
+/**
+ * Strip HTML comments from content to avoid flagging commented-out examples
+ * or prose that names a dead command for historical context (e.g. "previously
+ * this was /gsd-old-name...").
+ */
+function stripHtmlComments(content) {
+ return content.replace(//g, '');
+}
+
+/**
+ * Extract the set of slash-command tokens from markdown content.
+ * Three forms per command per runtime:
+ * /gsd-slug — Claude / non-Gemini
+ * /gsd:slug — Gemini
+ * $gsd-slug — Codex
+ *
+ * Internal component slugs (INTERNAL_COMPONENT_SLUGS) are filtered out —
+ * those are file-path references or documentation placeholders, not slash
+ * command invocations.
+ *
+ * Returns: { slash: Set, colon: Set, dollar: Set }
+ */
+function extractCommandTokens(content) {
+ const stripped = stripHtmlComments(content);
+
+ function isInternal(token) {
+ // Strip the /gsd- or /gsd: or $gsd- prefix to get the slug
+ const slug = token.replace(/^(?:\/gsd[:-]|\$gsd-)/, '');
+ // Exact match OR prefix match for 'init-' (which ends with a dash)
+ if (INTERNAL_COMPONENT_SLUGS.has(slug)) return true;
+ for (const s of INTERNAL_COMPONENT_SLUGS) {
+ if (s.endsWith('-') && slug.startsWith(s)) return true;
+ }
+ return false;
+ }
+
+ const allSlash = (stripped.match(/\/gsd-[a-z0-9][a-z0-9-]*/g) || []);
+ const allColon = (stripped.match(/\/gsd:[a-z0-9][a-z0-9-]*/g) || []);
+ const allDollar = (stripped.match(/\$gsd-[a-z0-9][a-z0-9-]*/g) || []);
+
+ const slash = new Set(allSlash.filter(t => !isInternal(t)));
+ const colon = new Set(allColon.filter(t => !isInternal(t)));
+ const dollar = new Set(allDollar.filter(t => !isInternal(t)));
+ return { slash, colon, dollar };
+}
+
+/**
+ * Walk a directory and return all .md files recursively.
+ * Uses hand-rolled DFS for Node 20 compat (Node 22+ recursive readdirSync is
+ * not available in all CI matrix entries). Surfaces permission-denied errors
+ * as structured warnings (PRED.k302) rather than silently skipping.
+ */
+function listMdFiles(dir) {
+ if (!fs.existsSync(dir)) return [];
+ const files = [];
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
+ const fullPath = path.join(dir, entry.name);
+ if (entry.isDirectory()) {
+ try {
+ files.push(...listMdFiles(fullPath));
+ } catch (err) {
+ process.stderr.write('[docs-parity] WARNING: skipping unreadable directory ' + fullPath + ': ' + err.message + '\n');
+ }
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
+ files.push(fullPath);
+ }
+ }
+ return files;
+}
+
+/**
+ * Assert that every command token in a doc file resolves to the live registry.
+ * Returns an array of diagnostic strings (empty = pass).
+ */
+function findUnknownTokens(filePath, liveTokens) {
+ const content = fs.readFileSync(filePath, 'utf-8');
+ const { slash, colon, dollar } = extractCommandTokens(content);
+ const unknowns = [];
+ for (const token of slash) {
+ if (!liveTokens.has(token)) unknowns.push(token);
+ }
+ for (const token of colon) {
+ if (!liveTokens.has(token)) unknowns.push(token);
+ }
+ for (const token of dollar) {
+ if (!liveTokens.has(token)) unknowns.push(token);
+ }
+ return unknowns;
+}
+
+// ─── Helper unit tests ────────────────────────────────────────────────────────
+
+describe('getLiveCommandTokens() — helper contract', () => {
+ test('returns a Set', () => {
+ const result = getLiveCommandTokens();
+ assert.ok(result instanceof Set, 'getLiveCommandTokens() must return a Set');
+ });
+
+ test('returns a non-empty set (commands/gsd/ has registered commands)', () => {
+ const result = getLiveCommandTokens();
+ assert.ok(result.size > 0, 'live registry must contain at least one token');
+ });
+
+ test('contains /gsd-help (from commands/gsd/help.md name: gsd:help)', () => {
+ const result = getLiveCommandTokens();
+ assert.ok(result.has('/gsd-help'), 'registry must contain /gsd-help');
+ });
+
+ test('contains /gsd:help (Gemini form)', () => {
+ const result = getLiveCommandTokens();
+ assert.ok(result.has('/gsd:help'), 'registry must contain /gsd:help');
+ });
+
+ test('contains $gsd-help (Codex form)', () => {
+ const result = getLiveCommandTokens();
+ assert.ok(result.has('$gsd-help'), 'registry must contain $gsd-help');
+ });
+
+ test('contains /gsd-plan-phase (from commands/gsd/plan-phase.md)', () => {
+ const result = getLiveCommandTokens();
+ assert.ok(result.has('/gsd-plan-phase'), 'registry must contain /gsd-plan-phase');
+ });
+
+ test('contains exactly 3 tokens per slug (slash, colon, dollar)', () => {
+ const result = getLiveCommandTokens();
+ // Every /gsd-slug should have a matching /gsd:slug and $gsd-slug
+ let tokenCount = 0;
+ for (const token of result) {
+ if (token.startsWith('/gsd-')) tokenCount++;
+ }
+ const slashTokens = [...result].filter(t => t.startsWith('/gsd-'));
+ for (const slash of slashTokens) {
+ const slug = slash.slice('/gsd-'.length);
+ assert.ok(
+ result.has(`/gsd:${slug}`),
+ `registry must contain Gemini form /gsd:${slug} for slash form ${slash}`
+ );
+ assert.ok(
+ result.has(`$gsd-${slug}`),
+ `registry must contain Codex form $gsd-${slug} for slash form ${slash}`
+ );
+ }
+ });
+
+ test('does NOT contain removed /gsd-reapply-patches', () => {
+ const result = getLiveCommandTokens();
+ assert.ok(!result.has('/gsd-reapply-patches'), 'registry must NOT contain removed /gsd-reapply-patches');
+ });
+
+ test('does NOT contain removed /gsd-code-review-fix', () => {
+ const result = getLiveCommandTokens();
+ assert.ok(!result.has('/gsd-code-review-fix'), 'registry must NOT contain removed /gsd-code-review-fix');
+ });
+
+ test('does NOT contain removed /gsd-status', () => {
+ const result = getLiveCommandTokens();
+ assert.ok(!result.has('/gsd-status'), 'registry must NOT contain removed /gsd-status');
+ });
+
+ test('memoizes — returns the same Set reference on repeated calls', () => {
+ const a = getLiveCommandTokens();
+ const b = getLiveCommandTokens();
+ assert.strictEqual(a, b, 'getLiveCommandTokens() must return the same Set instance (memoized)');
+ });
+});
+
+// ─── Fixture-based helper tests ───────────────────────────────────────────────
+
+describe('getLiveCommandTokens() — fixture contract', () => {
+ test('parses gsd:foo frontmatter and emits 3 canonical tokens', () => {
+ // This test validates the parsing logic against a known-good fixture
+ // by inspecting the live registry for commands/gsd/help.md (name: gsd:help).
+ // Fixture file tests are done inline since the helper reads commands/gsd/ only.
+ // The canonical token contract:
+ // name: gsd:foo → /gsd-foo, /gsd:foo, $gsd-foo
+ const registry = getLiveCommandTokens();
+ // We know help.md has name: gsd:help
+ const slug = 'help';
+ assert.ok(registry.has(`/gsd-${slug}`), `must have /gsd-${slug}`);
+ assert.ok(registry.has(`/gsd:${slug}`), `must have /gsd:${slug}`);
+ assert.ok(registry.has(`$gsd-${slug}`), `must have $gsd-${slug}`);
+ });
+
+ test('parses gsd-slug frontmatter (ns-* commands) and emits 3 tokens', () => {
+ // ns-context.md has name: gsd-context (dash-style, no colon)
+ const registry = getLiveCommandTokens();
+ assert.ok(registry.has('/gsd-context'), 'must have /gsd-context (from ns-context.md)');
+ assert.ok(registry.has('/gsd:context'), 'must have /gsd:context (Gemini form)');
+ assert.ok(registry.has('$gsd-context'), 'must have $gsd-context (Codex form)');
+ });
+});
+
+// ─── English docs parity check ───────────────────────────────────────────────
+
+// Precomputed locale directory prefixes for efficient exclusion in the English scan.
+const LOCALE_DIRS = LOCALES.map(l => path.join(DOCS_DIR, l) + path.sep);
+
+/**
+ * List all .md files under dir, excluding files under any of the known locale
+ * subdirectories (which are covered by the per-locale describe blocks below).
+ */
+function listEnglishMdFiles(dir) {
+ return listMdFiles(dir).filter(
+ f => !LOCALE_DIRS.some(ld => f.startsWith(ld))
+ );
+}
+
+describe('docs parity — English docs/*.md ⊆ liveRegistry', () => {
+ test('docs/ directory exists and contains markdown files', () => {
+ const files = listEnglishMdFiles(DOCS_DIR);
+ assert.ok(files.length > 0, `expected markdown files under ${DOCS_DIR}`);
+ });
+
+ test('every slash-command token in docs/*.md resolves to a live command', () => {
+ const liveTokens = getLiveCommandTokens();
+ const docFiles = listEnglishMdFiles(DOCS_DIR);
+ const allOffenders = [];
+
+ for (const filePath of docFiles) {
+ if (ALLOWED_HISTORICAL_MENTIONS.has(filePath)) continue;
+ if (isReleaseDoc(filePath)) continue;
+
+ const unknowns = findUnknownTokens(filePath, liveTokens);
+ if (unknowns.length > 0) {
+ allOffenders.push(
+ `${path.relative(ROOT, filePath)}: unknown command token(s): [${unknowns.join(', ')}]`
+ );
+ }
+ }
+
+ assert.deepStrictEqual(
+ allOffenders,
+ [],
+ 'docs/*.md must only reference live registered commands:\n ' + allOffenders.join('\n ')
+ );
+ });
+});
+
+// ─── Localized docs parity check ─────────────────────────────────────────────
+
+for (const locale of LOCALES) {
+ const localeDir = path.join(DOCS_DIR, locale);
+
+ describe(`docs parity — docs/${locale}/*.md ⊆ liveRegistry`, () => {
+ test(`docs/${locale}/ exists and contains markdown files (or is empty/absent — skip gracefully)`, () => {
+ if (!fs.existsSync(localeDir)) {
+ // Some locales may not exist in every repo state — that is fine.
+ return;
+ }
+ // If the dir exists, it should have at least one .md file.
+ const files = listMdFiles(localeDir);
+ // Warn but don't fail if locale dir is unexpectedly empty.
+ // The parity test below will simply pass vacuously.
+ assert.ok(
+ files.length >= 0,
+ `docs/${locale}/ exists but contains no markdown files`
+ );
+ });
+
+ test(`every slash-command token in docs/${locale}/*.md resolves to a live command`, () => {
+ if (!fs.existsSync(localeDir)) return;
+
+ const liveTokens = getLiveCommandTokens();
+ const docFiles = listMdFiles(localeDir);
+ const allOffenders = [];
+
+ for (const filePath of docFiles) {
+ if (ALLOWED_HISTORICAL_MENTIONS.has(filePath)) continue;
+ if (isReleaseDoc(filePath)) continue;
+
+ const unknowns = findUnknownTokens(filePath, liveTokens);
+ if (unknowns.length > 0) {
+ allOffenders.push(
+ `${path.relative(ROOT, filePath)}: unknown command token(s): [${unknowns.join(', ')}]`
+ );
+ }
+ }
+
+ assert.deepStrictEqual(
+ allOffenders,
+ [],
+ `docs/${locale}/*.md must only reference live registered commands:\n ` + allOffenders.join('\n ')
+ );
+ });
+ });
+}
+
+// ─── Adversarial regression tests ────────────────────────────────────────────
+
+describe('adversarial: polarity inversion catches drift deny-list misses', () => {
+ test('renaming a live command without updating docs would fail this test (demonstrated via token absence)', () => {
+ // If /gsd-progress were renamed to /gsd-status-new, the old /gsd-progress
+ // token would not appear in the live registry, and any doc referencing
+ // /gsd-progress would fail. The deny-list shape would have passed silently
+ // (it only checks for specific known-bad tokens).
+ // We can't simulate an actual rename in a live test, but we can assert
+ // that the registry correctly contains the live name (progress, not status):
+ const registry = getLiveCommandTokens();
+ assert.ok(registry.has('/gsd-progress'), '/gsd-progress must be live (not renamed to /gsd-status)');
+ assert.ok(!registry.has('/gsd-status'), '/gsd-status must be absent (was deleted, replaced by /gsd-progress)');
+ });
+
+ test('freshly-deleted command /gsd-check-todos is absent from registry', () => {
+ const registry = getLiveCommandTokens();
+ assert.ok(!registry.has('/gsd-check-todos'), '/gsd-check-todos must not be in the live registry');
+ });
+
+ test('freshly-deleted command /gsd-new-workspace is absent from registry', () => {
+ const registry = getLiveCommandTokens();
+ assert.ok(!registry.has('/gsd-new-workspace'), '/gsd-new-workspace must not be in the live registry');
+ });
+
+ test('freshly-deleted command /gsd-plan-milestone-gaps is absent from registry', () => {
+ const registry = getLiveCommandTokens();
+ assert.ok(!registry.has('/gsd-plan-milestone-gaps'), '/gsd-plan-milestone-gaps must not be in the live registry');
+ });
+
+ test('freshly-deleted command /gsd-research-phase is absent from registry', () => {
+ const registry = getLiveCommandTokens();
+ assert.ok(!registry.has('/gsd-research-phase'), '/gsd-research-phase must not be in the live registry');
+ });
+});
diff --git a/tests/enh-2792-namespace-skills.test.cjs b/tests/enh-2792-namespace-skills.test.cjs
index fedbce47f..6ddfd1881 100644
--- a/tests/enh-2792-namespace-skills.test.cjs
+++ b/tests/enh-2792-namespace-skills.test.cjs
@@ -14,7 +14,7 @@ const COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'gsd');
const NAMESPACE_SKILLS = [
{ file: 'ns-workflow.md', name: 'gsd-workflow' },
{ file: 'ns-project.md', name: 'gsd-project' },
- { file: 'ns-review.md', name: 'gsd-review' },
+ { file: 'ns-review.md', name: 'gsd-quality' },
{ file: 'ns-context.md', name: 'gsd-context' },
{ file: 'ns-manage.md', name: 'gsd-manage' },
{ file: 'ns-ideate.md', name: 'gsd-ideate' },
diff --git a/tests/fixtures/live-command-registry/bar-baz.md b/tests/fixtures/live-command-registry/bar-baz.md
new file mode 100644
index 000000000..c612a55b2
--- /dev/null
+++ b/tests/fixtures/live-command-registry/bar-baz.md
@@ -0,0 +1,5 @@
+---
+name: gsd:bar-baz
+description: Test fixture command bar-baz with hyphen
+---
+Body text.
diff --git a/tests/fixtures/live-command-registry/foo.md b/tests/fixtures/live-command-registry/foo.md
new file mode 100644
index 000000000..dab896405
--- /dev/null
+++ b/tests/fixtures/live-command-registry/foo.md
@@ -0,0 +1,5 @@
+---
+name: gsd:foo
+description: Test fixture command foo
+---
+Body text.
diff --git a/tests/fixtures/live-command-registry/malformed-no-frontmatter.md b/tests/fixtures/live-command-registry/malformed-no-frontmatter.md
new file mode 100644
index 000000000..58b73eb25
--- /dev/null
+++ b/tests/fixtures/live-command-registry/malformed-no-frontmatter.md
@@ -0,0 +1,2 @@
+This file has no YAML frontmatter at all.
+Just plain content.
diff --git a/tests/helpers/live-command-registry.cjs b/tests/helpers/live-command-registry.cjs
new file mode 100644
index 000000000..2679b411b
--- /dev/null
+++ b/tests/helpers/live-command-registry.cjs
@@ -0,0 +1,132 @@
+// allow-test-rule: source-text-is-the-product
+// commands/gsd/*.md files ARE the deployed registry — reading their frontmatter
+// validates the structural contract of the command surface, not application source.
+'use strict';
+/**
+ * live-command-registry.cjs
+ *
+ * Derives the canonical set of live slash-command tokens from the source-of-truth
+ * registry: commands/gsd/*.md (one file per registered command).
+ *
+ * Each command file has YAML frontmatter with a `name:` field:
+ * name: gsd:slug (colon-style — most commands)
+ * name: gsd-slug (dash-style — ns-* namespace commands)
+ *
+ * For each slug, three canonical token forms are emitted:
+ * /gsd-slug — Claude / non-Gemini runtimes
+ * /gsd:slug — Gemini runtime
+ * $gsd-slug — Codex runtime
+ *
+ * The result is memoized per process — a single fs walk is amortized across
+ * all test files that import this helper. The cache is intentionally not
+ * exposed for invalidation: test processes are short-lived and the registry
+ * does not change mid-run.
+ *
+ * Per CONTEXT.md k003: all readFileSync calls happen inside getLiveCommandTokens()
+ * (i.e., inside a function call, not at module top-level) so that import-time
+ * ENOENT errors are caught and reported with context rather than aborting the
+ * test runner before any test registers.
+ */
+
+const fs = require('node:fs');
+const path = require('node:path');
+
+const COMMANDS_DIR = path.join(__dirname, '..', '..', 'commands', 'gsd');
+
+// Module-level memoization — set on first call, reused thereafter.
+let _cache = null;
+
+/**
+ * Parse the YAML frontmatter `name:` field from a command file's content.
+ * Returns the slug (e.g. "help", "plan-phase", "context") or null if the
+ * field is absent or the file has no frontmatter.
+ *
+ * The frontmatter is bounded by the first `---` line and the next `---` line.
+ * We parse only the `name:` field — the full YAML spec is not needed and
+ * introducing a YAML parser dependency would be disproportionate.
+ *
+ * Supported name forms:
+ * name: gsd:slug -> slug = "slug"
+ * name: gsd-slug -> slug = "slug"
+ * name: "gsd:slug" -> slug = "slug" (quoted)
+ * name: "gsd-slug" -> slug = "slug" (quoted)
+ */
+function parseSlug(content, filePath) {
+ // Frontmatter must start with '---' on the very first line.
+ if (!content.startsWith('---')) {
+ throw new Error(
+ '[live-command-registry] ' + filePath + ': missing YAML frontmatter — file must start with \'---\''
+ );
+ }
+
+ // Find the closing '---' delimiter.
+ const closingIdx = content.indexOf('\n---', 3);
+ if (closingIdx < 0) {
+ throw new Error(
+ '[live-command-registry] ' + filePath + ': unclosed YAML frontmatter — no closing \'---\' found'
+ );
+ }
+
+ const frontmatter = content.slice(0, closingIdx);
+
+ // Match `name:` line, allowing optional quotes around the value.
+ // The value must be one of: gsd: or gsd-
+ // where slug = [a-z0-9][a-z0-9-]*
+ const nameMatch = frontmatter.match(/^name:\s*"?(gsd[:-])([a-z0-9][a-z0-9-]*)"?\s*$/m);
+ if (!nameMatch) {
+ throw new Error(
+ '[live-command-registry] ' + filePath + ': could not extract slug from frontmatter ' +
+ '(expected "name: gsd:" or "name: gsd-")'
+ );
+ }
+
+ return nameMatch[2]; // the slug after "gsd:" or "gsd-"
+}
+
+/**
+ * Returns the Set of all canonical slash-command tokens derived from
+ * commands/gsd/*.md. Memoized — safe to call repeatedly without extra fs I/O.
+ *
+ * Throws on the first malformed file (fail-loud per CONTEXT.md k302) so
+ * registry drift is caught immediately rather than silently producing an
+ * incomplete allow-list.
+ */
+function getLiveCommandTokens() {
+ if (_cache !== null) return _cache;
+
+ if (!fs.existsSync(COMMANDS_DIR)) {
+ throw new Error(
+ '[live-command-registry] commands directory not found: ' + COMMANDS_DIR
+ );
+ }
+
+ const entries = fs.readdirSync(COMMANDS_DIR)
+ .filter(function(f) { return f.endsWith('.md'); })
+ .sort(); // deterministic order for reproducible error messages
+
+ const tokens = new Set();
+
+ for (const fileName of entries) {
+ const filePath = path.join(COMMANDS_DIR, fileName);
+ let content;
+ try {
+ content = fs.readFileSync(filePath, 'utf-8');
+ } catch (err) {
+ throw new Error(
+ '[live-command-registry] failed to read ' + filePath + ': ' + err.message
+ );
+ }
+
+ const slug = parseSlug(content, filePath);
+
+ // Emit all three canonical token forms per slug.
+ tokens.add('/gsd-' + slug); // Claude / non-Gemini
+ tokens.add('/gsd:' + slug); // Gemini
+ tokens.add('$gsd-' + slug); // Codex
+ }
+
+ _cache = tokens;
+ return _cache;
+}
+
+module.exports = { getLiveCommandTokens };
diff --git a/tests/skill-frontmatter-contract.test.cjs b/tests/skill-frontmatter-contract.test.cjs
new file mode 100644
index 000000000..3827cae70
--- /dev/null
+++ b/tests/skill-frontmatter-contract.test.cjs
@@ -0,0 +1,200 @@
+// allow-test-rule: source-text-is-the-product
+// The commands/gsd/*.md and get-shit-done/workflows/*.md files are the
+// installed agent stubs — their frontmatter and workflow body IS the
+// deployed contract. These assertions check structural fields (argument-hint,
+// description, early-exit prose) that govern runtime routing.
+
+/**
+ * Skill frontmatter contract tests
+ *
+ * Moved here from bug-3042-3044-research-flag-and-stale-refs.test.cjs
+ * during the docs-parity polarity refactor (#3049). The original file
+ * mixed two concerns:
+ * (a) docs-parity deny-list checks → replaced by docs-parity-live-registry.test.cjs
+ * (b) frontmatter-structural checks → this file
+ *
+ * These tests assert structural invariants in command-stub frontmatter and
+ * workflow prose — they are NOT docs-parity checks. They verify that flags
+ * are wired, descriptions are correct, and early-exit prose is present in
+ * the right sections. These tests need to remain even after the deny-list
+ * tests are removed.
+ */
+
+'use strict';
+
+const { test, describe } = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+
+const ROOT = path.join(__dirname, '..');
+
+function read(rel) {
+ let content;
+ try {
+ content = fs.readFileSync(path.join(ROOT, rel), 'utf-8');
+ } catch (err) {
+ throw new Error('[skill-frontmatter-contract] failed to read ' + rel + ': ' + err.message);
+ }
+ return content;
+}
+
+function exists(rel) {
+ return fs.existsSync(path.join(ROOT, rel));
+}
+
+// ─── #3042: --research-phase flag wired into /gsd-plan-phase ────────────────
+// (Moved from bug-3042-3044-research-flag-and-stale-refs.test.cjs)
+
+describe('skill frontmatter: /gsd-plan-phase --research-phase flag absorbs the standalone research command', () => {
+ test('commands/gsd/plan-phase.md argument-hint advertises --research-phase', () => {
+ const content = read('commands/gsd/plan-phase.md');
+ // Frontmatter argument-hint is the structural place users discover
+ // the flag. Parse the line that starts with "argument-hint:" and
+ // assert the flag token is present.
+ const m = content.match(/^argument-hint:\s*"([^"]+)"/m);
+ assert.ok(m, 'plan-phase.md must declare an argument-hint frontmatter field');
+ assert.ok(
+ m[1].includes('--research-phase'),
+ 'argument-hint must include "--research-phase"; got: ' + m[1]
+ );
+ });
+
+ test('plan-phase.md frontmatter description still advertises plan capability (no semantics drift)', () => {
+ const content = read('commands/gsd/plan-phase.md');
+ const m = content.match(/^description:\s*(.+)$/m);
+ assert.ok(m, 'plan-phase.md must have a description field');
+ // The description should still describe planning — the flag is
+ // additive, not a renamed command.
+ assert.ok(
+ /plan/i.test(m[1]),
+ 'description should still mention planning; got: ' + m[1]
+ );
+ });
+
+ test('workflows/plan-phase.md parses --research-phase and sets a research-only mode', () => {
+ const content = read('get-shit-done/workflows/plan-phase.md');
+ // The arg-parsing section of the workflow must mention the new flag
+ // by name. This is the structural seam the LLM follows.
+ // Anchored to the argument/flags section to avoid false positives from prose.
+ const argsIdx = content.search(/(?:argument|args?|flags?)\b/i);
+ assert.ok(argsIdx >= 0, 'plan-phase workflow must contain an argument/flags section');
+ const argsWindow = content.slice(argsIdx, argsIdx + 1200);
+ assert.ok(
+ /--research-phase/.test(argsWindow),
+ 'plan-phase.md workflow must reference --research-phase in the argument-parsing section (within 1200 chars of the args/flags header)'
+ );
+ });
+
+ test('workflows/plan-phase.md skips planner/verifier when in research-only mode', () => {
+ const content = read('get-shit-done/workflows/plan-phase.md');
+ // Look for explicit early-exit prose so the LLM knows to stop after
+ // research. We accept any of: "research-only", "research only mode",
+ // "skip if --research-phase", "RESEARCH_ONLY", "exit after research".
+ const patterns = [
+ /research[ -]only/i,
+ /RESEARCH_ONLY/,
+ /skip if[^\n]*--research-phase/i,
+ /exit (?:after|when)[^\n]*research/i,
+ ];
+ const hits = patterns.filter((re) => re.test(content));
+ assert.ok(
+ hits.length > 0,
+ 'plan-phase workflow must contain explicit early-exit prose for --research-phase mode; ' +
+ 'none of [research-only, RESEARCH_ONLY, "skip if --research-phase", "exit after research"] matched'
+ );
+ });
+
+ test('orphaned workflows/research-phase.md is removed', () => {
+ assert.equal(
+ exists('get-shit-done/workflows/research-phase.md'),
+ false,
+ 'workflows/research-phase.md must be removed; the capability now lives on /gsd-plan-phase --research-phase'
+ );
+ });
+
+ test('argument-hint advertises --view as a research-only modifier', () => {
+ const content = read('commands/gsd/plan-phase.md');
+ const m = content.match(/^argument-hint:\s*"([^"]+)"/m);
+ assert.ok(m, 'plan-phase.md must declare an argument-hint frontmatter field');
+ assert.ok(
+ m[1].includes('--view'),
+ 'argument-hint must include --view (research-only view-only mode); got: ' + m[1]
+ );
+ });
+
+ test('workflow handles --view by printing existing RESEARCH.md without spawning', () => {
+ const content = read('get-shit-done/workflows/plan-phase.md');
+ // The workflow must reference the --view flag as a no-spawn mode
+ // for research-only invocations. We accept any of: "view-only",
+ // "VIEW_ONLY", "skip if --view", "no spawn" alongside --view.
+ assert.ok(
+ /--view/.test(content),
+ 'plan-phase workflow must reference the --view flag'
+ );
+ const viewModePatterns = [
+ /view[ -]only/i,
+ /VIEW_ONLY/,
+ /no[ -]spawn/i,
+ /print[^\n]*RESEARCH\.md/i,
+ /display[^\n]*RESEARCH\.md/i,
+ ];
+ const hits = viewModePatterns.filter((re) => re.test(content));
+ assert.ok(
+ hits.length > 0,
+ 'plan-phase workflow must explain that --view prints existing RESEARCH.md without spawning; ' +
+ 'expected one of [view-only, VIEW_ONLY, no-spawn, "print/display RESEARCH.md"]'
+ );
+ });
+
+ test('workflow uses --research as the force-refresh signal in research-only mode', () => {
+ const content = read('get-shit-done/workflows/plan-phase.md');
+ // The plan-phase workflow already had a --research flag with
+ // "force re-research" semantics. In research-only mode, that flag
+ // must short-circuit the "RESEARCH.md exists, what do you want to
+ // do?" prompt and unconditionally re-spawn. Assert the workflow
+ // documents the combined semantics.
+ // Find the --research-phase description section (headed by the ** marker),
+ // then assert that --research and force/refresh semantics are documented
+ // within the same section — verifying the COMBINATION is documented.
+ // The section header starts at "**`--research-phase `" and runs ~1200
+ // chars to cover the modifiers sub-list (--research and --view bullets).
+ const sectionIdx = content.indexOf('**`--research-phase');
+ assert.ok(sectionIdx >= 0, 'plan-phase workflow must contain a --research-phase description section');
+ const sectionWindow = content.slice(sectionIdx, sectionIdx + 1200);
+ const hasResearch = /--research\b/.test(sectionWindow);
+ const hasForceRefresh = /(?:force[ -]?refresh|re-research|re-spawn|overwrites)/i.test(sectionWindow);
+ assert.ok(
+ hasResearch && hasForceRefresh,
+ 'plan-phase workflow must document that --research forces re-research when used with --research-phase ' +
+ '(expected --research and force/refresh prose in the --research-phase section; got hasResearch=' +
+ hasResearch + ' hasForceRefresh=' + hasForceRefresh + ')'
+ );
+ });
+
+ test('workflow has an existing-RESEARCH.md prompt path (update/view/skip) within proximity', () => {
+ const content = read('get-shit-done/workflows/plan-phase.md');
+ // CR #3045 finding: the previous version of this test asserted
+ // `update`, `view`, `skip` appeared anywhere in the file, which was
+ // tautological — those words occur all over the workflow for
+ // unrelated reasons (--skip-research, --view flag declarations,
+ // etc.). Tighten to a proximity check: all three choice tokens
+ // must occur in a window of ~400 chars surrounding "RESEARCH.md
+ // already exists" / "Update — re-spawn" / equivalent prompt prose,
+ // proving the prompt section is genuinely present.
+ const idx = content.indexOf('RESEARCH.md already exists');
+ assert.ok(
+ idx >= 0,
+ 'plan-phase workflow must contain the literal "RESEARCH.md already exists" prompt header in the research-only existing-artifact section'
+ );
+ const window = content.slice(idx, idx + 600);
+ const hasUpdate = /\b(?:update|refresh|re-spawn)\b/i.test(window);
+ const hasView = /\bview\b/i.test(window);
+ const hasSkip = /\bskip\b/i.test(window);
+ assert.ok(
+ hasUpdate && hasView && hasSkip,
+ 'prompt section near "RESEARCH.md already exists" must mention all three choices (update/refresh/re-spawn, view, skip); ' +
+ 'got update=' + hasUpdate + ' view=' + hasView + ' skip=' + hasSkip
+ );
+ });
+});