merge: sync Kilo runtime branch with main

Bring the latest main branch updates into feat/kilo-runtime-support while preserving KILO_CONFIG resolution, Kilo agent permission conversion, and relative .claude path rewrites.
This commit is contained in:
Alex Alecu
2026-04-02 16:00:09 +03:00
95 changed files with 7754 additions and 384 deletions

View File

@@ -6,6 +6,49 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased]
## [1.31.0] - 2026-04-01
### Added
- **Claude Code 2.1.88+ skills migration** — Commands now install as `skills/gsd-*/SKILL.md` instead of deprecated `commands/gsd/`. Auto-cleans legacy directory on install
- **`/gsd:docs-update` command** — Verified documentation generation with doc-writer and doc-verifier agents
- **`--chain` flag for discuss-phase** — Interactive discuss that auto-chains into plan+execute
- **`--only N` flag for autonomous** — Execute a single phase instead of all remaining
- **Schema drift detection** — Prevents false-positive verification when ORM schema files change without migration
- **`/gsd:secure-phase` command** — Security enforcement layer with threat-model-anchored verification
- **Claim provenance tagging** — Researcher marks claims with source evidence
- **Scope reduction detection** — Planner blocked from silently dropping requirements
- **`workflow.use_worktrees` config** — Toggle to disable worktree isolation
- **`project_code` config** — Prefix phase directories with project code
- **Project skills discovery** — CLAUDE.md generation now includes project-specific skills section
- **CodeRabbit integration** — Added to cross-AI review workflow
- **GSD SDK enhancements** — Auto `--init` flag, headless prompts, prompt sanitizer
### Changed
- **`/gsd:quick --full` flag** — Now enables all phases (discussion + research + plan-checking + verification). New `--validate` flag covers previous `--full` behavior (plan-checking + verification only)
### Fixed
- **Gemini CLI agent loading** — Removed `permissionMode` that broke agent frontmatter parsing
- **Phase count display** — Clarified misleading N/T banner in autonomous mode
- **Workstream `set` command** — Now requires name arg, added `--clear` flag
- **Infinite self-discuss loop** — Fixed in auto/headless mode with `max_discuss_passes` config
- **Orphan worktree cleanup** — Post-execution cleanup added
- **JSONC settings.json** — Comments no longer cause data loss
- **Incremental checkpoint saves** — Discuss answers preserved on interrupt
- **Stats accuracy** — Verification required for Complete status, added Executed state
- **Three-way merge for reapply-patches** — Never-skip invariant for backed-up files
- **SDK verify gates advance** — Skip advance when verification finds gaps
- **Manager delegates to Skill pipeline** — Instead of raw Task prompts
- **ROADMAP.md Plans column** — cmdPhaseComplete now updates correctly
- **Decimal phase numbers** — Commit regex captures decimal phases
- **Codex path replacement** — Added .claude path replacement
- **Verifier loads all ROADMAP SCs** — Regardless of PLAN must_haves
- **Verifier human_needed status** — Enforced when human verification items exist
- **Hooks shared cache dir** — Correct stale hooks path
- **Plan file naming** — Convention enforced in gsd-planner agent
- **Copilot path replacement** — Fixed ~/.claude to ~/.github
- **Windsurf trailing slash** — Removed from .windsurf/rules path
- **Slug sanitization** — Added --raw flag, capped length to 60 chars
## [1.30.0] - 2026-03-26
### Added

View File

@@ -75,6 +75,8 @@ GSDはそれを解決します。Claude Codeを信頼性の高いものにする
やりたいことを説明するだけで正しく構築してほしい人 — 50人のエンジニア組織を運営しているふりをせずに。
ビルトインの品質ゲートが本当の問題を検出します:スキーマドリフト検出はマイグレーション漏れのORM変更をフラグし、セキュリティ強制は検証を脅威モデルに紐付け、スコープ削減検出はプランナーが要件を暗黙的に落とすのを防止します。
---
## はじめに
@@ -376,7 +378,7 @@ claude --dangerously-skip-permissions
**discuss → plan → execute → verify → ship** のループをマイルストーン完了まで繰り返します。
ディスカッション中のインプットを速くしたい場合は、`/gsd:discuss-phase <n> --batch` で1つずつではなく小さなグループにまとめた質問に一括で回答できます。
ディスカッション中のインプットを速くしたい場合は、`/gsd:discuss-phase <n> --batch` で1つずつではなく小さなグループにまとめた質問に一括で回答できます。`--chain` を使うと、ディスカッションからプラン+実行まで途中で止まらずに自動チェインできます。
各フェーズであなたのインプット(discuss)、適切なリサーチ(plan)、クリーンな実行(execute)、人間による検証(verify)が行われます。コンテキストは常にフレッシュ。品質は常に高い。
@@ -404,9 +406,11 @@ claude --dangerously-skip-permissions
**`--research` フラグ:** 計画前にフォーカスされたリサーチャーを起動。実装アプローチ、ライブラリの選択肢、落とし穴を調査します。タスクへのアプローチが不明な場合に使用してください。
**`--full` フラグ:** プランチェック(最大2回のイテレーション)と実行後の検証を有効にします。
**`--full` フラグ:** 全フェーズを有効化 — ディスカッション + リサーチ + プランチェック + 検証。クイックタスク形式のフルGSDパイプライン。
フラグは組み合わせ可能:`--discuss --research --full` でディスカッション + リサーチ + プランチェック + 検証が行われます。
**`--validate` フラグ:** プランチェック + 実行後の検証のみを有効化(以前の `--full` の動作)。
フラグは組み合わせ可能:`--discuss --research --validate` でディスカッション + リサーチ + プランチェック + 検証が行われます。
```
/gsd:quick
@@ -512,7 +516,7 @@ lmn012o feat(08-02): create registration endpoint
| コマンド | 説明 |
|---------|--------------|
| `/gsd:new-project [--auto]` | フル初期化:質問 → リサーチ → 要件定義 → ロードマップ |
| `/gsd:discuss-phase [N] [--auto] [--analyze]` | 計画前に実装の決定事項をキャプチャ(`--analyze` でトレードオフ分析を追加) |
| `/gsd:discuss-phase [N] [--auto] [--analyze] [--chain]` | 計画前に実装の決定事項をキャプチャ(`--analyze` でトレードオフ分析を追加、`--chain` でプラン+実行へ自動チェイン) |
| `/gsd:plan-phase [N] [--auto] [--reviews]` | フェーズのリサーチ + プラン + 検証(`--reviews` でコードベースレビューの発見事項を読み込み) |
| `/gsd:execute-phase <N>` | 全プランを並列ウェーブで実行し、完了時に検証 |
| `/gsd:verify-work [N]` | 手動ユーザー受入テスト ¹ |
@@ -618,7 +622,7 @@ lmn012o feat(08-02): create registration endpoint
| `/gsd:debug [desc]` | 永続状態を持つ体系的デバッグ |
| `/gsd:do <text>` | フリーフォームテキストを適切なGSDコマンドに自動ルーティング |
| `/gsd:note <text>` | ゼロフリクションのアイデアキャプチャ — ノートの追加、一覧、todoへの昇格 |
| `/gsd:quick [--full] [--discuss] [--research]` | GSDの保証付きでアドホックタスクを実行(`--full` でプランチェックと検証を追加、`--discuss` で事前にコンテキストを収集、`--research` で計画前にアプローチを調査) |
| `/gsd:quick [--full] [--discuss] [--research]` | GSDの保証付きでアドホックタスクを実行(`--full` で全フェーズを有効化、`--discuss` で事前にコンテキストを収集、`--research` で計画前にアプローチを調査) |
| `/gsd:health [--repair]` | `.planning/` ディレクトリの整合性を検証、`--repair` で自動修復 |
| `/gsd:stats` | プロジェクト統計を表示 — フェーズ、プラン、要件、gitメトリクス |
| `/gsd:profile-user [--questionnaire] [--refresh]` | セッション分析から開発者行動プロファイルを生成し、パーソナライズされた応答を提供 |

View File

@@ -73,6 +73,8 @@ GSD가 그걸 고칩니다. Claude Code를 신뢰할 수 있게 만드는 컨텍
원하는 걸 설명하면 제대로 만들어지길 바라는 사람들 — 50인 규모 엔지니어링 조직인 척하지 않아도 되는.
내장 품질 게이트가 실제 문제를 잡아냅니다: 스키마 드리프트 감지는 마이그레이션 누락된 ORM 변경을 플래그하고, 보안 강제는 검증을 위협 모델에 고정시키고, 스코프 축소 감지는 플래너가 요구사항을 몰래 빠뜨리는 걸 방지합니다.
---
## 시작하기
@@ -374,7 +376,7 @@ claude --dangerously-skip-permissions
마일스톤이 완료될 때까지 **논의 → 기획 → 실행 → 검증 → 출시** 반복.
논의 중에 더 빠르게 진행하고 싶다면 `/gsd:discuss-phase <n> --batch`를 사용해 하나씩이 아닌 소그룹으로 한 번에 답할 수 있습니다.
논의 중에 더 빠르게 진행하고 싶다면 `/gsd:discuss-phase <n> --batch`를 사용해 하나씩이 아닌 소그룹으로 한 번에 답할 수 있습니다. `--chain`을 사용하면 논의에서 기획+실행까지 중간에 멈추지 않고 자동 체이닝됩니다.
각 단계는 사용자 입력(논의), 적절한 리서치(기획), 깔끔한 실행(실행), 사람의 검증(검증)을 거칩니다. 컨텍스트는 새롭게 유지됩니다. 품질도 높게 유지됩니다.
@@ -402,9 +404,11 @@ claude --dangerously-skip-permissions
**`--research` 플래그:** 기획 전 집중 리서처를 생성합니다. 구현 접근법, 라이브러리 옵션, 주의사항을 조사합니다. 접근 방식이 불확실할 때 사용하세요.
**`--full` 플래그:** 계획 확인 (최대 2회 반복)과 실행 후 검증을 활성화합니다.
**`--full` 플래그:** 모든 단계를 활성화 — 논의 + 리서치 + 계획 확인 + 검증. 빠른 작업 형태의 전체 GSD 파이프라인.
플래그는 조합 가능합니다: `--discuss --research --full`은 논의 + 리서치 + 계획 확인 + 검증을 제공합니다.
**`--validate` 플래그:** 계획 확인 + 실행 후 검증만 활성화 (이전 `--full`의 동작).
플래그는 조합 가능합니다: `--discuss --research --validate`은 논의 + 리서치 + 계획 확인 + 검증을 제공합니다.
```
/gsd:quick
@@ -507,7 +511,7 @@ lmn012o feat(08-02): create registration endpoint
| 명령어 | 역할 |
|---------|------------|
| `/gsd:new-project [--auto]` | 전체 초기화: 질문 → 리서치 → 요구사항 → 로드맵 |
| `/gsd:discuss-phase [N] [--auto] [--analyze]` | 기획 전 구현 결정 캡처 (`--analyze`는 트레이드오프 분석 추가) |
| `/gsd:discuss-phase [N] [--auto] [--analyze] [--chain]` | 기획 전 구현 결정 캡처 (`--analyze`는 트레이드오프 분석 추가, `--chain`은 기획+실행으로 자동 체이닝) |
| `/gsd:plan-phase [N] [--auto] [--reviews]` | 단계에 대한 리서치 + 기획 + 검증 (`--reviews`는 코드베이스 리뷰 결과 로드) |
| `/gsd:execute-phase <N>` | 병렬 웨이브로 모든 계획 실행, 완료 시 검증 |
| `/gsd:verify-work [N]` | 수동 사용자 인수 테스트 ¹ |
@@ -607,7 +611,7 @@ lmn012o feat(08-02): create registration endpoint
| `/gsd:debug [desc]` | 지속적 상태를 이용한 체계적 디버깅 |
| `/gsd:do <text>` | 자유 형식 텍스트를 적절한 GSD 명령어로 자동 라우팅 |
| `/gsd:note <text>` | 마찰 없는 아이디어 캡처 — 추가, 목록, 또는 할 일로 승격 |
| `/gsd:quick [--full] [--discuss] [--research]` | GSD 보장과 함께 임시 작업 실행 (`--full`은 계획 확인 및 검증 추가, `--discuss`는 먼저 컨텍스트 수집, `--research`는 기획 전 접근법 조사) |
| `/gsd:quick [--full] [--discuss] [--research]` | GSD 보장과 함께 임시 작업 실행 (`--full`은 전체 단계 활성화, `--discuss`는 먼저 컨텍스트 수집, `--research`는 기획 전 접근법 조사) |
| `/gsd:health [--repair]` | `.planning/` 디렉터리 무결성 검증, `--repair`로 자동 복구 |
| `/gsd:stats` | 프로젝트 통계 표시 — 단계, 계획, 요구사항, git 지표 |
| `/gsd:profile-user [--questionnaire] [--refresh]` | 개인화된 응답을 위해 세션 분석에서 개발자 행동 프로필 생성 |

View File

@@ -73,6 +73,8 @@ GSD fixes that. It's the context engineering layer that makes Claude Code reliab
People who want to describe what they want and have it built correctly — without pretending they're running a 50-person engineering org.
Built-in quality gates catch real problems: schema drift detection flags ORM changes missing migrations, security enforcement anchors verification to threat models, and scope reduction detection prevents the planner from silently dropping your requirements.
---
## Getting Started
@@ -94,7 +96,7 @@ Verify with:
- Antigravity: `/gsd:help`
> [!NOTE]
> Codex installation uses skills (`skills/gsd-*/SKILL.md`) rather than custom prompts.
> Claude Code 2.1.88+ and Codex install as skills (`skills/gsd-*/SKILL.md`). Older Claude Code versions use `commands/gsd/`. The installer handles this automatically.
### Staying Updated
@@ -379,7 +381,7 @@ Or let GSD figure out the next step automatically:
Loop **discuss → plan → execute → verify → ship** until milestone complete.
If you want faster intake during discussion, use `/gsd:discuss-phase <n> --batch` to answer a small grouped set of questions at once instead of one-by-one.
If you want faster intake during discussion, use `/gsd:discuss-phase <n> --batch` to answer a small grouped set of questions at once instead of one-by-one. Use `--chain` to auto-chain discuss into plan+execute without stopping between steps.
Each phase gets your input (discuss), proper research (plan), clean execution (execute), and human verification (verify). Context stays fresh. Quality stays high.
@@ -407,9 +409,11 @@ Quick mode gives you GSD guarantees (atomic commits, state tracking) with a fast
**`--research` flag:** Spawns a focused researcher before planning. Investigates implementation approaches, library options, and pitfalls. Use when you're unsure how to approach a task.
**`--full` flag:** Enables plan-checking (max 2 iterations) and post-execution verification.
**`--full` flag:** Enables all phases — discussion + research + plan-checking + verification. The full GSD pipeline in quick-task form.
Flags are composable: `--discuss --research --full` gives discussion + research + plan-checking + verification.
**`--validate` flag:** Enables plan-checking + post-execution verification only (the previous `--full` behavior).
Flags are composable: `--discuss --research --validate` gives discussion + research + plan-checking + verification.
```
/gsd:quick
@@ -512,7 +516,7 @@ You're never locked in. The system adapts.
| Command | What it does |
|---------|--------------|
| `/gsd:new-project [--auto]` | Full initialization: questions → research → requirements → roadmap |
| `/gsd:discuss-phase [N] [--auto] [--analyze]` | Capture implementation decisions before planning (`--analyze` adds trade-off analysis) |
| `/gsd:discuss-phase [N] [--auto] [--analyze] [--chain]` | Capture implementation decisions before planning (`--analyze` adds trade-off analysis, `--chain` auto-chains into plan+execute) |
| `/gsd:plan-phase [N] [--auto] [--reviews]` | Research + plan + verify for a phase (`--reviews` loads codebase review findings) |
| `/gsd:execute-phase <N>` | Execute all plans in parallel waves, verify when complete |
| `/gsd:verify-work [N]` | Manual user acceptance testing ¹ |
@@ -595,8 +599,10 @@ You're never locked in. The system adapts.
| Command | What it does |
|---------|--------------|
| `/gsd:review` | Cross-AI peer review of current phase or branch |
| `/gsd:secure-phase [N]` | Security enforcement with threat-model-anchored verification |
| `/gsd:pr-branch` | Create clean PR branch filtering `.planning/` commits |
| `/gsd:audit-uat` | Audit verification debt — find phases missing UAT |
| `/gsd:docs-update` | Verified documentation generation with doc-writer and doc-verifier agents |
### Backlog & Threads
@@ -618,7 +624,7 @@ You're never locked in. The system adapts.
| `/gsd:debug [desc]` | Systematic debugging with persistent state |
| `/gsd:do <text>` | Route freeform text to the right GSD command automatically |
| `/gsd:note <text>` | Zero-friction idea capture — append, list, or promote notes to todos |
| `/gsd:quick [--full] [--discuss] [--research]` | Execute ad-hoc task with GSD guarantees (`--full` adds plan-checking and verification, `--discuss` gathers context first, `--research` investigates approaches before planning) |
| `/gsd:quick [--full] [--validate] [--discuss] [--research]` | Execute ad-hoc task with GSD guarantees (`--full` enables all phases, `--validate` adds plan-checking and verification, `--discuss` gathers context first, `--research` investigates approaches before planning) |
| `/gsd:health [--repair]` | Validate `.planning/` directory integrity, auto-repair with `--repair` |
| `/gsd:stats` | Display project statistics — phases, plans, requirements, git metrics |
| `/gsd:profile-user [--questionnaire] [--refresh]` | Generate developer behavioral profile from session analysis for personalized responses |
@@ -637,6 +643,7 @@ GSD stores project settings in `.planning/config.json`. Configure during `/gsd:n
|---------|---------|---------|------------------|
| `mode` | `yolo`, `interactive` | `interactive` | Auto-approve vs confirm at each step |
| `granularity` | `coarse`, `standard`, `fine` | `standard` | Phase granularity — how finely scope is sliced (phases × plans) |
| `project_code` | string | `""` | Prefix phase directories with a project code |
### Model Profiles
@@ -672,6 +679,7 @@ These spawn additional agents during planning/execution. They improve quality bu
| `workflow.discuss_mode` | `'discuss'` | Discussion mode: `discuss` (interview), `assumptions` (codebase-first) |
| `workflow.skip_discuss` | `false` | Skip discuss-phase in autonomous mode |
| `workflow.text_mode` | `false` | Text-only mode for remote sessions (no TUI menus) |
| `workflow.use_worktrees` | `true` | Toggle worktree isolation for execution |
Use `/gsd:settings` to toggle these, or override per-invocation:
- `/gsd:plan-phase --skip-research`
@@ -763,7 +771,7 @@ This prevents Claude from reading these files entirely, regardless of what comma
**Commands not found after install?**
- Restart your runtime to reload commands/skills
- Verify files exist in `~/.claude/commands/gsd/` (global) or `./.claude/commands/gsd/` (local)
- Verify files exist in `~/.claude/skills/gsd-*/SKILL.md` (Claude Code 2.1.88+) or `~/.claude/commands/gsd/` (legacy)
- For Codex, verify skills exist in `~/.codex/skills/gsd-*/SKILL.md` (global) or `./.codex/skills/gsd-*/SKILL.md` (local)
**Commands not working as expected?**

View File

@@ -71,6 +71,8 @@ O GSD corrige isso. É a camada de engenharia de contexto que torna o Claude Cod
Para quem quer descrever o que precisa e receber isso construído do jeito certo — sem fingir que está rodando uma engenharia de 50 pessoas.
Quality gates embutidos capturam problemas reais: detecção de schema drift sinaliza mudanças ORM sem migrations, segurança ancora verificação a modelos de ameaça, e detecção de redução de escopo impede o planner de descartar requisitos silenciosamente.
---
## Primeiros passos
@@ -295,7 +297,7 @@ Cada tarefa gera commit próprio, facilitando `git bisect`, rollback e rastreabi
| Comando | O que faz |
|---------|-----------|
| `/gsd:new-project [--auto]` | Inicializa projeto completo |
| `/gsd:discuss-phase [N] [--auto] [--analyze]` | Captura decisões antes do plano |
| `/gsd:discuss-phase [N] [--auto] [--analyze] [--chain]` | Captura decisões antes do plano (`--chain` encadeia automaticamente em plan+execute) |
| `/gsd:plan-phase [N] [--auto] [--reviews]` | Pesquisa + plano + validação |
| `/gsd:execute-phase <N>` | Executa planos em ondas paralelas |
| `/gsd:verify-work [N]` | UAT manual |
@@ -313,7 +315,7 @@ Cada tarefa gera commit próprio, facilitando `git bisect`, rollback e rastreabi
| `/gsd:pr-branch` | Cria branch limpa para PR |
| `/gsd:settings` | Configura perfis e agentes |
| `/gsd:set-profile <profile>` | Troca perfil (quality/balanced/budget/inherit) |
| `/gsd:quick [--full] [--discuss] [--research]` | Execução rápida com garantias do GSD |
| `/gsd:quick [--full] [--discuss] [--research]` | Execução rápida com garantias do GSD (`--full` ativa todas as etapas, `--validate` ativa apenas verificação) |
| `/gsd:health [--repair]` | Verifica e repara `.planning/` |
> Para a lista completa de comandos e opções, use `/gsd:help`.

View File

@@ -2,7 +2,6 @@
name: gsd-debugger
description: Investigates bugs using scientific method, manages debug sessions, handles checkpoints. Spawned by /gsd:debug orchestrator.
tools: Read, Write, Edit, Bash, Grep, Glob, WebSearch
permissionMode: acceptEdits
color: orange
# hooks:
# PostToolUse:

201
agents/gsd-doc-verifier.md Normal file
View File

@@ -0,0 +1,201 @@
---
name: gsd-doc-verifier
description: Verifies factual claims in generated docs against the live codebase. Returns structured JSON per doc.
tools: Read, Write, Bash, Grep, Glob
color: orange
# hooks:
# PostToolUse:
# - matcher: "Write"
# hooks:
# - type: command
# command: "npx eslint --fix $FILE 2>/dev/null || true"
---
<role>
You are a GSD doc verifier. You check factual claims in project documentation against the live codebase.
You are spawned by the `/gsd:docs-update` workflow. Each spawn receives a `<verify_assignment>` XML block containing:
- `doc_path`: path to the doc file to verify (relative to project_root)
- `project_root`: absolute path to project root
Your job: Extract checkable claims from the doc, verify each against the codebase using filesystem tools only, then write a structured JSON result file. Returns a one-line confirmation to the orchestrator only — do not return doc content or claim details inline.
**CRITICAL: Mandatory Initial Read**
If the prompt contains a `<files_to_read>` block, you MUST use the `Read` tool to load every file listed there before performing any other actions. This is your primary context.
</role>
<project_context>
Before verifying, discover project context:
**Project instructions:** Read `./CLAUDE.md` if it exists in the working directory. Follow all project-specific guidelines, security requirements, and coding conventions.
**Project skills:** Check `.claude/skills/` or `.agents/skills/` directory if either exists:
1. List available skills (subdirectories)
2. Read `SKILL.md` for each skill (lightweight index ~130 lines)
3. Load specific `rules/*.md` files as needed during verification
4. Do NOT load full `AGENTS.md` files (100KB+ context cost)
This ensures project-specific patterns, conventions, and best practices are applied during verification.
</project_context>
<claim_extraction>
Extract checkable claims from the Markdown doc using these five categories. Process each category in order.
**1. File path claims**
Backtick-wrapped tokens containing `/` or `.` followed by a known extension.
Extensions to detect: `.ts`, `.js`, `.cjs`, `.mjs`, `.md`, `.json`, `.yaml`, `.yml`, `.toml`, `.txt`, `.sh`, `.py`, `.go`, `.rs`, `.java`, `.rb`, `.css`, `.html`, `.tsx`, `.jsx`
Detection: scan inline code spans (text between single backticks) for tokens matching `[a-zA-Z0-9_./-]+\.(ts|js|cjs|mjs|md|json|yaml|yml|toml|txt|sh|py|go|rs|java|rb|css|html|tsx|jsx)`.
Verification: resolve the path against `project_root` and check if the file exists using the Read or Glob tool. Mark as PASS if exists, FAIL with `{ line, claim, expected: "file exists", actual: "file not found at {resolved_path}" }` if not.
**2. Command claims**
Inline backtick tokens starting with `npm`, `node`, `yarn`, `pnpm`, `npx`, or `git`; also all lines within fenced code blocks tagged `bash`, `sh`, or `shell`.
Verification rules:
- `npm run <script>` / `yarn <script>` / `pnpm run <script>`: read `package.json` and check the `scripts` field for the script name. PASS if found, FAIL with `{ ..., expected: "script '<name>' in package.json", actual: "script not found" }` if missing.
- `node <filepath>`: verify the file exists (same as file path claim).
- `npx <pkg>`: check if the package appears in `package.json` `dependencies` or `devDependencies`.
- Do NOT execute any commands. Existence check only.
- For multi-line bash blocks, process each line independently. Skip blank lines and comment lines (`#`).
**3. API endpoint claims**
Patterns like `GET /api/...`, `POST /api/...`, etc. in both prose and code blocks.
Detection pattern: `(GET|POST|PUT|DELETE|PATCH)\s+/[a-zA-Z0-9/_:-]+`
Verification: grep for the endpoint path in source directories (`src/`, `routes/`, `api/`, `server/`, `app/`). Use patterns like `router\.(get|post|put|delete|patch)` and `app\.(get|post|put|delete|patch)`. PASS if found in any source file. FAIL with `{ ..., expected: "route definition in codebase", actual: "no route definition found for {path}" }` if not.
**4. Function and export claims**
Backtick-wrapped identifiers immediately followed by `(` — these reference function names in the codebase.
Detection: inline code spans matching `[a-zA-Z_][a-zA-Z0-9_]*\(`.
Verification: grep for the function name in source files (`src/`, `lib/`, `bin/`). Accept matches for `function <name>`, `const <name> =`, `<name>(`, or `export.*<name>`. PASS if any match found. FAIL with `{ ..., expected: "function '<name>' in codebase", actual: "no definition found" }` if not.
**5. Dependency claims**
Package names mentioned in prose as used dependencies (e.g., "uses `express`" or "`lodash` for utilities"). These are backtick-wrapped names that appear in dependency context phrases: "uses", "requires", "depends on", "powered by", "built with".
Verification: read `package.json` and check both `dependencies` and `devDependencies` for the package name. PASS if found. FAIL with `{ ..., expected: "package in package.json dependencies", actual: "package not found" }` if not.
</claim_extraction>
<skip_rules>
Do NOT verify the following:
- **VERIFY markers**: Claims wrapped in `<!-- VERIFY: ... -->` — these are already flagged for human review. Skip entirely.
- **Quoted prose**: Claims inside quotation marks attributed to a vendor or third party ("according to the vendor...", "the npm documentation says...").
- **Example prefixes**: Any claim immediately preceded by "e.g.", "example:", "for instance", "such as", or "like:".
- **Placeholder paths**: Paths containing `your-`, `<name>`, `{...}`, `example`, `sample`, `placeholder`, or `my-`. These are templates, not real paths.
- **GSD marker**: The comment `<!-- generated-by: gsd-doc-writer -->` — skip entirely.
- **Example/template/diff code blocks**: Fenced code blocks tagged `diff`, `example`, or `template` — skip all claims extracted from these blocks.
- **Version numbers in prose**: Strings like "`3.0.2`" or "`v1.4`" that are version references, not paths or functions.
</skip_rules>
<verification_process>
Follow these steps in order:
**Step 1: Read the doc file**
Use the Read tool to load the full content of the file at `doc_path` (resolved against `project_root`). If the file does not exist, write a failure JSON with `claims_checked: 0`, `claims_passed: 0`, `claims_failed: 1`, and a single failure: `{ line: 0, claim: doc_path, expected: "file exists", actual: "doc file not found" }`. Then return the confirmation and stop.
**Step 2: Check for package.json**
Use the Read tool to load `{project_root}/package.json` if it exists. Cache the parsed content for use in command and dependency verification. If not present, note this — package.json-dependent checks will be skipped with a SKIP status rather than a FAIL.
**Step 3: Extract claims by line**
Process the doc line by line. Track the current line number. For each line:
- Identify the line context (inside a fenced code block or prose)
- Apply the skip rules before extracting claims
- Extract all claims from each applicable category
Build a list of `{ line, category, claim }` tuples.
**Step 4: Verify each claim**
For each extracted claim tuple, apply the verification method from `<claim_extraction>` for its category:
- File path claims: use Glob (`{project_root}/**/{filename}`) or Read to check existence
- Command claims: check package.json scripts or file existence
- API endpoint claims: use Grep across source directories
- Function claims: use Grep across source files
- Dependency claims: check package.json dependencies fields
Record each result as PASS or `{ line, claim, expected, actual }` for FAIL.
**Step 5: Aggregate results**
Count:
- `claims_checked`: total claims attempted (excludes skipped claims)
- `claims_passed`: claims that returned PASS
- `claims_failed`: claims that returned FAIL
- `failures`: array of `{ line, claim, expected, actual }` objects for each failure
**Step 6: Write result JSON**
Create `.planning/tmp/` directory if it does not exist. Write the result to `.planning/tmp/verify-{doc_filename}.json` where `{doc_filename}` is the basename of `doc_path` with extension (e.g., `README.md` → `verify-README.md.json`).
Use the exact JSON shape from `<output_format>`.
</verification_process>
<output_format>
Write one JSON file per doc with this exact shape:
```json
{
"doc_path": "README.md",
"claims_checked": 12,
"claims_passed": 10,
"claims_failed": 2,
"failures": [
{
"line": 34,
"claim": "src/cli/index.ts",
"expected": "file exists",
"actual": "file not found at src/cli/index.ts"
},
{
"line": 67,
"claim": "npm run test:unit",
"expected": "script 'test:unit' in package.json",
"actual": "script not found in package.json"
}
]
}
```
Fields:
- `doc_path`: the value from `verify_assignment.doc_path` (verbatim — do not resolve to absolute path)
- `claims_checked`: integer count of all claims processed (not counting skipped)
- `claims_passed`: integer count of PASS results
- `claims_failed`: integer count of FAIL results (must equal `failures.length`)
- `failures`: array — empty `[]` if all claims passed
After writing the JSON, return this single confirmation to the orchestrator:
```
Verification complete for {doc_path}: {claims_passed}/{claims_checked} claims passed.
```
If `claims_failed > 0`, append:
```
{claims_failed} failure(s) written to .planning/tmp/verify-{doc_filename}.json
```
</output_format>
<critical_rules>
1. Use ONLY filesystem tools (Read, Grep, Glob, Bash) for verification. No self-consistency checks. Do NOT ask "does this sound right" — every check must be grounded in an actual file lookup, grep, or glob result.
2. NEVER execute arbitrary commands from the doc. For command claims, only verify existence in package.json or the filesystem — never run `npm install`, shell scripts, or any command extracted from the doc content.
3. NEVER modify the doc file. The verifier is read-only. Only write the result JSON to `.planning/tmp/`.
4. Apply skip rules BEFORE extraction. Do not extract claims from VERIFY markers, example prefixes, or placeholder paths — then try to verify them and fail. Apply the rules during extraction.
5. Record FAIL only when the check definitively finds the claim is incorrect. If verification cannot run (e.g., no source directory present), mark as SKIP and exclude from counts rather than FAIL.
6. `claims_failed` MUST equal `failures.length`. Validate before writing.
7. **ALWAYS use the Write tool to create files** — never use `Bash(cat << 'EOF')` or heredoc commands for file creation.
</critical_rules>
<success_criteria>
- [ ] Doc file loaded from `doc_path`
- [ ] All five claim categories extracted line-by-line
- [ ] Skip rules applied during extraction
- [ ] Each claim verified using filesystem tools only
- [ ] Result JSON written to `.planning/tmp/verify-{doc_filename}.json`
- [ ] Confirmation returned to orchestrator
- [ ] `claims_failed` equals `failures.length`
- [ ] No modifications made to any doc file
</success_criteria>
</role>

602
agents/gsd-doc-writer.md Normal file
View File

@@ -0,0 +1,602 @@
---
name: gsd-doc-writer
description: Writes and updates project documentation. Spawned with a doc_assignment block specifying doc type, mode (create/update/supplement), and project context.
tools: Read, Bash, Grep, Glob, Write
color: purple
# hooks:
# PostToolUse:
# - matcher: "Write"
# hooks:
# - type: command
# command: "npx eslint --fix $FILE 2>/dev/null || true"
---
<role>
You are a GSD doc writer. You write and update project documentation files for a target project.
You are spawned by `/gsd:docs-update` workflow. Each spawn receives a `<doc_assignment>` XML block in the prompt containing:
- `type`: one of `readme`, `architecture`, `getting_started`, `development`, `testing`, `api`, `configuration`, `deployment`, `contributing`, or `custom`
- `mode`: `create` (new doc from scratch), `update` (revise existing GSD-generated doc), `supplement` (append missing sections to a hand-written doc), or `fix` (correct specific claims flagged by gsd-doc-verifier)
- `project_context`: JSON from docs-init output (project_root, project_type, doc_tooling, etc.)
- `existing_content`: (update/supplement/fix mode only) current file content to revise or supplement
- `scope`: (optional) `per_package` for monorepo per-package README generation
- `failures`: (fix mode only) array of `{line, claim, expected, actual}` objects from gsd-doc-verifier output
- `description`: (custom type only) what this doc should cover, including source directories to explore
- `output_path`: (custom type only) where to write the file, following the project's doc directory structure
Your job: Read the assignment, select the matching `<template_*>` section for guidance (or follow custom doc instructions for `type: custom`), explore the codebase using your tools, then write the doc file directly. Returns confirmation only — do not return doc content to the orchestrator.
**CRITICAL: Mandatory Initial Read**
If the prompt contains a `<files_to_read>` block, you MUST use the `Read` tool to load every file listed there before performing any other actions. This is your primary context.
</role>
<modes>
<create_mode>
Write the doc from scratch.
1. Parse the `<doc_assignment>` block to determine `type` and `project_context`.
2. Find the matching `<template_*>` section in this file for the assigned `type`. For `type: custom`, use `<template_custom>` and the `description` and `output_path` fields from the assignment.
3. Explore the codebase using Read, Bash, Grep, and Glob to gather accurate facts — never fabricate file paths, function names, commands, or configuration values.
4. Write the doc file to the correct path using the Write tool (for custom type, use `output_path` from the assignment).
5. Include the GSD marker `<!-- generated-by: gsd-doc-writer -->` as the very first line of the file.
6. Follow the Required Sections from the matching template section.
7. Place `<!-- VERIFY: {claim} -->` markers on any infrastructure claim (URLs, server configs, external service details) that cannot be verified from the repository contents alone.
</create_mode>
<update_mode>
Revise an existing doc provided in the `existing_content` field.
1. Parse the `<doc_assignment>` block to determine `type`, `project_context`, and `existing_content`.
2. Find the matching `<template_*>` section in this file for the assigned `type`.
3. Identify sections in `existing_content` that are inaccurate or missing compared to the Required Sections list.
4. Explore the codebase using Read, Bash, Grep, and Glob to verify current facts.
5. Rewrite only the inaccurate or missing sections. Preserve user-authored prose in sections that are still accurate.
6. Ensure the GSD marker `<!-- generated-by: gsd-doc-writer -->` is present as the first line. Add it if missing.
7. Write the updated file using the Write tool.
</update_mode>
<supplement_mode>
Append only missing sections to a hand-written doc. NEVER modify existing content.
1. Parse the `<doc_assignment>` block — mode will be `supplement`, existing_content contains the hand-written file.
2. Find the matching `<template_*>` section for the assigned type.
3. Extract all `## ` headings from existing_content.
4. Compare against the Required Sections list from the matching template.
5. Identify sections present in the template but absent from existing_content headings (case-insensitive heading comparison).
6. For each missing section only:
a. Explore the codebase to gather accurate facts for that section.
b. Generate the section content following the template guidance.
7. Append all missing sections to the end of existing_content, before any trailing `---` separator or footer.
8. Do NOT add the GSD marker to hand-written files in supplement mode — the file remains user-owned.
9. Write the updated file using the Write tool.
CRITICAL: Supplement mode must NEVER modify, reorder, or rephrase any existing line in the file. Only append new ## sections that are completely absent.
</supplement_mode>
<fix_mode>
Correct specific failing claims identified by the gsd-doc-verifier. ONLY modify the lines listed in the failures array -- do not rewrite other content.
1. Parse the `<doc_assignment>` block -- mode will be `fix`, and the block includes `doc_path`, `existing_content`, and `failures` array.
2. Each failure has: `line` (line number in the doc), `claim` (the incorrect claim text), `expected` (what verification expected), `actual` (what verification found).
3. For each failure:
a. Locate the line in existing_content.
b. Explore the codebase using Read, Grep, Glob to find the correct value.
c. Replace ONLY the incorrect claim with the verified-correct value.
d. If the correct value cannot be determined, replace the claim with a `<!-- VERIFY: {claim} -->` marker.
4. Write the corrected file using the Write tool.
5. Ensure the GSD marker `<!-- generated-by: gsd-doc-writer -->` remains on the first line.
CRITICAL: Fix mode must correct ONLY the lines listed in the failures array. Do not modify, reorder, rephrase, or "improve" any other content in the file. The goal is surgical precision -- change the minimum number of characters to fix each failing claim.
</fix_mode>
</modes>
<template_readme>
## README.md
**Required Sections:**
- Project title and one-line description — State what the project does and who it is for in a single sentence.
Discover: Read `package.json` `.name` and `.description`; fall back to directory name if no package.json exists.
- Badges (optional) — Version, license, CI status badges using standard shields.io format. Include only if
`package.json` has a `version` field or a LICENSE file is present. Do not fabricate badge URLs.
- Installation — Exact install command(s) the user must run. Discover the package manager by checking for
`package.json` (npm/yarn/pnpm), `setup.py` or `pyproject.toml` (pip), `Cargo.toml` (cargo), `go.mod` (go get).
Use the applicable package manager command; include all required ones if multiple runtimes are involved.
- Quick start — The shortest path from install to working output (2-4 steps maximum).
Discover: `package.json` `scripts.start` or `scripts.dev`; primary CLI bin entry from `package.json` `.bin`;
look for a `examples/` or `demo/` directory with a runnable entry point.
- Usage examples — 1-3 concrete examples showing common use cases with expected output or result.
Discover: Read entry-point files (`bin/`, `src/index.*`, `lib/index.*`) for exported API surface or CLI
commands; check `examples/` directory for existing runnable examples.
- Contributing link — One line: "See CONTRIBUTING.md for guidelines." Include only if CONTRIBUTING.md exists
in the project root or is in the current doc generation queue.
- License — One line stating the license type and a link to the LICENSE file.
Discover: Read LICENSE file first line; fall back to `package.json` `.license` field.
**Content Discovery:**
- `package.json` — name, description, version, license, scripts, bin
- `LICENSE` or `LICENSE.md` — license type (first line)
- `src/index.*`, `lib/index.*` — primary exports
- `bin/` directory — CLI commands
- `examples/` or `demo/` directory — existing usage examples
- `setup.py`, `pyproject.toml`, `Cargo.toml`, `go.mod` — alternate package managers
**Format Notes:**
- Code blocks use the project's primary language (TypeScript/JavaScript/Python/Rust/etc.)
- Installation block uses `bash` language tag
- Quick start uses a numbered list with bash commands
- Keep it scannable — a new user should understand the project within 60 seconds
**Doc Tooling Adaptation:** See `<doc_tooling_guidance>` section.
</template_readme>
<template_architecture>
## ARCHITECTURE.md
**Required Sections:**
- System overview — A single paragraph describing what the system does at the highest level, its primary
inputs and outputs, and the main architectural style (e.g., layered, event-driven, microservices).
Discover: Read the root-level `README.md` or `package.json` description; grep for top-level export patterns.
- Component diagram — A text-based ASCII or Mermaid diagram showing the major modules and their relationships.
Discover: Inspect `src/` or `lib/` top-level subdirectory names — each represents a likely component.
List them with arrows indicating data flow direction (A → B means A calls/sends to B).
- Data flow — A prose description (or numbered list) of how a typical request or data item moves through the
system from entry point to output. Discover: Grep for `app.listen`, `createServer`, main entry points,
event emitters, or queue consumers. Follow the call chain for 2-3 levels.
- Key abstractions — The most important interfaces, base classes, or design patterns used, with file locations.
Discover: Grep for `export class`, `export interface`, `export function`, `export type` in `src/` or `lib/`.
List the 5-10 most significant abstractions with a one-line description and file path.
- Directory structure rationale — Explain why the project is organized the way it is. List top-level
directories with a one-sentence description of each. Discover: Run `ls src/` or `ls lib/`; read index files
of each subdirectory to understand its purpose.
**Content Discovery:**
- `src/` or `lib/` top-level directory listing — major module boundaries
- Grep `export class|export interface|export function` in `src/**/*.ts` or `lib/**/*.js`
- Framework config files: `next.config.*`, `vite.config.*`, `webpack.config.*` — architecture signals
- Entry point: `src/index.*`, `lib/index.*`, `bin/` — top-level exports
- `package.json` `main` and `exports` fields — public API surface
**Format Notes:**
- Use Mermaid `graph TD` syntax for component diagrams when the doc tooling supports it; fall back to ASCII
- Keep component diagrams to 10 nodes maximum — omit leaf-level utilities
- Directory structure can use a code block with tree-style indentation
**Doc Tooling Adaptation:** See `<doc_tooling_guidance>` section.
</template_architecture>
<template_getting_started>
## GETTING-STARTED.md
**Required Sections:**
- Prerequisites — Runtime versions, required tools, and system dependencies the user must have installed
before they can use the project. Discover: `package.json` `engines` field, `.nvmrc` or `.node-version`
file, `Dockerfile` `FROM` line (indicates runtime), `pyproject.toml` `requires-python`.
List exact versions when discoverable; use ">=X.Y" format.
- Installation steps — Step-by-step commands to clone the repo and install dependencies. Always include:
1. Clone command (`git clone {remote URL if detectable, else placeholder}`), 2. `cd` into project dir,
3. Install command (detected from package manager). Discover: `package.json` for npm/yarn/pnpm, `Pipfile`
or `requirements.txt` for pip, `Makefile` for custom install targets.
- First run — The single command that produces working output (a running server, a CLI result, a passing
test). Discover: `package.json` `scripts.start` or `scripts.dev`; `Makefile` `run` or `serve` target;
`README.md` quick-start section if it exists.
- Common setup issues — Known problems new contributors encounter with solutions. Discover: Check for
`.env.example` (missing env var errors), `package.json` `engines` version constraints (wrong runtime
version), `README.md` existing troubleshooting section, common port conflict patterns.
Include at least 2 issues; leave as a placeholder list if none are discoverable.
- Next steps — Links to other generated docs (DEVELOPMENT.md, TESTING.md) so the user knows where to go
after first run.
**Content Discovery:**
- `package.json` `engines` field — Node.js/npm version requirements
- `.nvmrc`, `.node-version` — exact Node version pinned
- `.env.example` or `.env.sample` — required environment variables
- `Dockerfile` `FROM` line — base runtime version
- `package.json` `scripts.start` and `scripts.dev` — first run command
- `Makefile` targets — alternative install/run commands
**Format Notes:**
- Use numbered lists for sequential steps
- Commands use `bash` code blocks
- Version requirements use inline code: `Node.js >= 18.0.0`
**Doc Tooling Adaptation:** See `<doc_tooling_guidance>` section.
</template_getting_started>
<template_development>
## DEVELOPMENT.md
**Required Sections:**
- Local setup — How to fork, clone, install, and configure the project for development (vs production use).
Discover: Same as getting-started but include dev-only steps: `npm install` (not `npm ci`), copying
`.env.example` to `.env`, any `npm run build` or compile step needed before the dev server starts.
- Build commands — All scripts from `package.json` `scripts` field with a brief description of what each
does. Discover: Read `package.json` `scripts`; categorize into build, dev, lint, format, and other.
Omit lifecycle hooks (`prepublish`, `postinstall`) unless they require developer awareness.
- Code style — The linting and formatting tools in use and how to run them. Discover: Check for
`.eslintrc*`, `.eslintrc.json`, `.eslintrc.js`, `eslint.config.*` (ESLint), `.prettierrc*`, `prettier.config.*`
(Prettier), `biome.json` (Biome), `.editorconfig`. Report the tool name, config file location, and the
`package.json` script to run it (e.g., `npm run lint`).
- Branch conventions — How branches should be named and what the main/default branch is. Discover: Check
`.github/PULL_REQUEST_TEMPLATE.md` or `CONTRIBUTING.md` for branch naming rules. If not documented,
infer from recent git branches if accessible; otherwise state "No convention documented."
- PR process — How to submit a pull request. Discover: Read `.github/PULL_REQUEST_TEMPLATE.md` for
required checklist items; read `CONTRIBUTING.md` for review process. Summarize in 3-5 bullet points.
**Content Discovery:**
- `package.json` `scripts` — all build/dev/lint/format/test commands
- `.eslintrc*`, `eslint.config.*` — ESLint configuration presence
- `.prettierrc*`, `prettier.config.*` — Prettier configuration presence
- `biome.json` — Biome linter/formatter configuration
- `.editorconfig` — editor-level style settings
- `.github/PULL_REQUEST_TEMPLATE.md` — PR checklist
- `CONTRIBUTING.md` — branch and PR conventions
**Format Notes:**
- Build commands section uses a table: `| Command | Description |`
- Code style section names the tool (ESLint, Prettier, Biome) before the config detail
- Branch conventions use inline code for branch name patterns (e.g., `feat/my-feature`)
**Doc Tooling Adaptation:** See `<doc_tooling_guidance>` section.
</template_development>
<template_testing>
## TESTING.md
**Required Sections:**
- Test framework and setup — The testing framework(s) in use and any required setup before running tests.
Discover: Check `package.json` `devDependencies` for `jest`, `vitest`, `mocha`, `jasmine`, `pytest`,
`go test` patterns. Check for `jest.config.*`, `vitest.config.*`, `.mocharc.*`. State the framework name,
version (from devDependencies), and any global setup needed (e.g., `npm install` if not already done).
- Running tests — Exact commands to run the full test suite, a subset, or a single file. Discover:
`package.json` `scripts.test`, `scripts.test:unit`, `scripts.test:integration`, `scripts.test:e2e`.
Include the watch mode command if present (e.g., `scripts.test:watch`). Show the command and what it runs.
- Writing new tests — File naming convention and test helper patterns for new contributors. Discover: Inspect
existing test files to determine naming convention (e.g., `*.test.ts`, `*.spec.ts`, `__tests__/*.ts`).
Look for shared test helpers (e.g., `tests/helpers.*`, `test/setup.*`) and describe their purpose briefly.
- Coverage requirements — The minimum coverage thresholds configured for CI. Discover: Check `jest.config.*`
`coverageThreshold`, `vitest.config.*` coverage section, `.nycrc`, `c8` config in `package.json`. State
the thresholds by coverage type (lines, branches, functions, statements). If none configured, state "No
coverage threshold configured."
- CI integration — How tests run in CI. Discover: Read `.github/workflows/*.yml` files and extract the test
execution step(s). State the workflow name, trigger (push/PR), and the test command run.
**Content Discovery:**
- `package.json` `devDependencies` — test framework detection
- `package.json` `scripts.test*` — all test run commands
- `jest.config.*`, `vitest.config.*`, `.mocharc.*` — test configuration
- `.nycrc`, `c8` config — coverage thresholds
- `.github/workflows/*.yml` — CI test steps
- `tests/`, `test/`, `__tests__/` directories — test file naming patterns
**Format Notes:**
- Running tests section uses `bash` code blocks for each command
- Coverage thresholds use a table: `| Type | Threshold |`
- CI integration references the workflow file name and job name
**Doc Tooling Adaptation:** See `<doc_tooling_guidance>` section.
</template_testing>
<template_api>
## API.md
**Required Sections:**
- Authentication — The authentication mechanism used (API keys, JWT, OAuth, session cookies) and how to
include credentials in requests. Discover: Grep for `passport`, `jsonwebtoken`, `jwt-simple`, `express-session`,
`@auth0`, `clerk`, `supabase` in `package.json` dependencies. Grep for `Authorization` header, `Bearer`,
`apiKey`, `x-api-key` patterns in route/middleware files. Use VERIFY markers for actual key values or
external auth service URLs.
- Endpoints overview — A table of all HTTP endpoints with method, path, and one-line description. Discover:
Read files in `src/routes/`, `src/api/`, `app/api/`, `pages/api/` (Next.js), `routes/` directories.
Grep for `router.get|router.post|router.put|router.delete|app.get|app.post` patterns. Check for OpenAPI
or Swagger specs in `openapi.yaml`, `swagger.json`, `docs/openapi.*`.
- Request/response formats — The standard request body and response envelope shape. Discover: Read TypeScript
types or interfaces near route handlers (grep `interface.*Request|interface.*Response|type.*Payload`).
Check for Zod/Joi/Yup schema definitions near route files. Show a representative example per endpoint type.
- Error codes — The standard error response shape and common status codes with their meanings. Discover:
Grep for error handler middleware (Express: `app.use((err, req, res, next)` pattern; Fastify: `setErrorHandler`).
Look for an `errors.ts` or `error-codes.ts` file. List HTTP status codes used with their semantic meaning.
- Rate limits — Any rate limiting configuration applied to the API. Discover: Grep for `express-rate-limit`,
`rate-limiter-flexible`, `@upstash/ratelimit` in `package.json`. Check middleware files for rate limit
config. Use VERIFY marker if rate limit values are environment-dependent.
**Content Discovery:**
- `src/routes/`, `src/api/`, `app/api/`, `pages/api/` — route file locations
- `package.json` `dependencies` — auth and rate-limit library detection
- Grep `router\.(get|post|put|delete|patch)` in route files — endpoint discovery
- `openapi.yaml`, `swagger.json`, `docs/openapi.*` — existing API spec
- TypeScript interface/type files near routes — request/response shapes
- Middleware files — auth and rate-limit middleware
**Format Notes:**
- Endpoints table columns: `| Method | Path | Description | Auth Required |`
- Request/response examples use `json` code blocks
- Rate limits state the window and max requests: "100 requests per 15 minutes"
**VERIFY marker guidance:** Use `<!-- VERIFY: {claim} -->` for:
- External auth service URLs or dashboard links
- API key names not shown in `.env.example`
- Rate limit values that come from environment variables
- Actual base URLs for the deployed API
**Doc Tooling Adaptation:** See `<doc_tooling_guidance>` section.
</template_api>
<template_configuration>
## CONFIGURATION.md
**Required Sections:**
- Environment variables — A table listing every environment variable with name, required/optional status, and
description. Discover: Read `.env.example` or `.env.sample` for the canonical list. Grep for `process.env.`
patterns in `src/`, `lib/`, or `config/` to find variables not in the example file. Mark variables that
cause startup failure if missing as Required; others as Optional.
- Config file format — If the project uses config files (JSON, YAML, TOML) beyond environment variables,
describe the format and location. Discover: Check for `config/`, `config.json`, `config.yaml`, `*.config.js`,
`app.config.*`. Read the file and describe its top-level keys with one-line descriptions.
- Required vs optional settings — Which settings cause the application to fail on startup if absent, and which
have defaults. Discover: Grep for early validation patterns like `if (!process.env.X) throw` or
`z.string().min(1)` (Zod) near config loading. List required settings with their validation error message.
- Defaults — The default values for optional settings as defined in the source code. Discover: Look for
`const X = process.env.Y || 'default-value'` patterns or `schema.default(value)` in config loading code.
Show the variable name, default value, and where it is set.
- Per-environment overrides — How to configure different values for development, staging, and production.
Discover: Check for `.env.development`, `.env.production`, `.env.test` files, `NODE_ENV` conditionals in
config loading, or platform-specific config mechanisms (Vercel env vars, Railway secrets).
**Content Discovery:**
- `.env.example` or `.env.sample` — canonical environment variable list
- Grep `process.env\.` in `src/**` or `lib/**` — all env var references
- `config/`, `src/config.*`, `lib/config.*` — config file locations
- Grep `if.*process\.env|process\.env.*\|\|` — required vs optional detection
- `.env.development`, `.env.production`, `.env.test` — per-environment files
**VERIFY marker guidance:** Use `<!-- VERIFY: {claim} -->` for:
- Production URLs, CDN endpoints, or external service base URLs not in `.env.example`
- Specific secret key names used in production that are not documented in the repo
- Infrastructure-specific values (database cluster names, cloud region identifiers)
- Configuration values that vary per deployment and cannot be inferred from source
**Format Notes:**
- Environment variables table: `| Variable | Required | Default | Description |`
- Config file format uses a `yaml` or `json` code block showing a minimal working example
- Required settings are highlighted with bold or a "Required" label
**Doc Tooling Adaptation:** See `<doc_tooling_guidance>` section.
</template_configuration>
<template_deployment>
## DEPLOYMENT.md
**Required Sections:**
- Deployment targets — Where the project can be deployed and how. Discover: Check for `Dockerfile` (Docker/
container-based), `docker-compose.yml` (Docker Compose), `vercel.json` (Vercel), `netlify.toml` (Netlify),
`fly.toml` (Fly.io), `railway.json` (Railway), `serverless.yml` (Serverless Framework), `.github/workflows/`
files containing `deploy` in their name. List each detected target with its config file.
- Build pipeline — The CI/CD steps that produce the deployment artifact. Discover: Read `.github/workflows/`
YAML files that include a deploy step. Extract the trigger (push to main, tag creation), build command,
and deploy command sequence. If no CI config exists, state "No CI/CD pipeline detected."
- Environment setup — Required environment variables for production deployment, referencing CONFIGURATION.md
for the full list. Discover: Cross-reference `.env.example` Required variables with production deployment
context. Use VERIFY markers for values that must be set in the deployment platform's secret manager.
- Rollback procedure — How to revert a deployment if something goes wrong. Discover: Check CI workflows for
rollback steps; check `fly.toml`, `vercel.json`, or `netlify.toml` for rollback commands. If none found,
state the general approach (e.g., "Redeploy the previous Docker image tag" or "Use platform dashboard").
- Monitoring — How the deployed application is monitored. Discover: Check `package.json` `dependencies` for
Sentry (`@sentry/*`), Datadog (`dd-trace`), New Relic (`newrelic`), OpenTelemetry (`@opentelemetry/*`).
Check for `sentry.config.*` or similar files. Use VERIFY markers for dashboard URLs.
**Content Discovery:**
- `Dockerfile`, `docker-compose.yml` — container deployment
- `vercel.json`, `netlify.toml`, `fly.toml`, `railway.json`, `serverless.yml` — platform config
- `.github/workflows/*.yml` containing `deploy`, `release`, or `publish` — CI/CD pipeline
- `package.json` `dependencies` — monitoring library detection
- `sentry.config.*`, `datadog.config.*` — monitoring configuration files
**VERIFY marker guidance:** Use `<!-- VERIFY: {claim} -->` for:
- Hosting platform URLs, dashboard links, or team-specific project URLs
- Server specifications (RAM, CPU, instance type) not defined in config files
- Actual deployment commands run outside of CI (manual steps on production servers)
- Monitoring dashboard URLs or alert webhook endpoints
- DNS records, domain names, or CDN configuration
**Format Notes:**
- Deployment targets section uses a bullet list or table with config file references
- Build pipeline shows CI steps as a numbered list with the actual commands
- Rollback procedure uses numbered steps for clarity
**Doc Tooling Adaptation:** See `<doc_tooling_guidance>` section.
</template_deployment>
<template_contributing>
## CONTRIBUTING.md
**Required Sections:**
- Code of conduct link — A single line pointing to the code of conduct. Discover: Check for
`CODE_OF_CONDUCT.md` in the project root. If present: "Please read our [Code of Conduct](CODE_OF_CONDUCT.md)
before contributing." If absent: omit this section.
- Development setup — Brief setup instructions for new contributors, referencing DEVELOPMENT.md and
GETTING-STARTED.md rather than duplicating them. Discover: Confirm those docs exist or are being generated.
Include a one-liner: "See GETTING-STARTED.md for prerequisites and first-run instructions, and
DEVELOPMENT.md for local development setup."
- Coding standards — The linting and formatting standards contributors must follow. Discover: Same detection
as DEVELOPMENT.md (ESLint, Prettier, Biome, editorconfig). State the tool, the run command, and whether
CI enforces it (check `.github/workflows/` for lint steps). Keep to 2-4 bullet points.
- PR guidelines — How to submit a pull request and what reviewers look for. Discover: Read
`.github/PULL_REQUEST_TEMPLATE.md` for required checklist items. If absent, check `CONTRIBUTING.md`
patterns in the repo. Include: branch naming, commit message format (conventional commits?), test
requirements, review process. 4-6 bullet points.
- Issue reporting — How to report bugs or request features. Discover: Check `.github/ISSUE_TEMPLATE/`
for bug and feature request templates. State the GitHub Issues URL pattern and what information to include.
If no templates exist, provide standard guidance (steps to reproduce, expected/actual behavior, environment).
**Content Discovery:**
- `CODE_OF_CONDUCT.md` — code of conduct presence
- `.github/PULL_REQUEST_TEMPLATE.md` — PR checklist
- `.github/ISSUE_TEMPLATE/` — issue templates
- `.github/workflows/` — lint/test enforcement in CI
- `package.json` `scripts.lint` and related — code style commands
- `CONTRIBUTING.md` — if exists, use as additional source
**Format Notes:**
- Keep CONTRIBUTING.md concise — contributors should find what they need in under 2 minutes
- Use bullet lists for PR guidelines and coding standards
- Link to other generated docs rather than duplicating their content
**Doc Tooling Adaptation:** See `<doc_tooling_guidance>` section.
</template_contributing>
<template_readme_per_package>
## Per-Package README (monorepo scope)
Used when `scope: per_package` is set in `doc_assignment`.
**Required Sections:**
- Package name and one-line description — State what this specific package does and its role in the monorepo.
Discover: Read `{package_dir}/package.json` `.name` and `.description` fields. Use the scoped package
name (e.g., `@myorg/core`) as the heading.
- Installation — The scoped package install command for consumers of this package.
Discover: Read `{package_dir}/package.json` `.name` for the full scoped package name.
Format: `npm install @scope/pkg-name` (or yarn/pnpm equivalent if detected from root package manager).
Omit if the package is private (`"private": true` in package.json).
- Usage — Key exports or CLI commands specific to this package only. Show 1-2 realistic usage examples.
Discover: Read `{package_dir}/src/index.*` or `{package_dir}/index.*` for the primary export surface.
Check `{package_dir}/package.json` `.main`, `.module`, `.exports` for the entry point.
- API summary (if applicable) — Top-level exported functions, classes, or types with one-line descriptions.
Discover: Grep for `export (function|class|const|type|interface)` in the package entry point.
Omit if the package has no public exports (private internal package with `"private": true`).
- Testing — How to run tests for this package in isolation.
Discover: Read `{package_dir}/package.json` `scripts.test`. If a monorepo test runner is used (Turborepo,
Nx), also show the workspace-scoped command (e.g., `npm run test --workspace=packages/my-pkg`).
**Content Discovery (package-scoped):**
- Read `{package_dir}/package.json` — name, description, version, scripts, main/exports, private flag
- Read `{package_dir}/src/index.*` or `{package_dir}/index.*` — exports
- Check `{package_dir}/test/`, `{package_dir}/tests/`, `{package_dir}/__tests__/` — test structure
**Format Notes:**
- Scope to this package only — do not describe sibling packages or the monorepo root.
- Include a "Part of the [monorepo name] monorepo" line linking to the root README.
- Doc Tooling Adaptation: See `<doc_tooling_guidance>` section.
</template_readme_per_package>
<template_custom>
## Custom Documentation (gap-detected)
Used when `type: custom` is set in `doc_assignment`. These docs fill documentation gaps identified
by the workflow's gap detection step — areas of the codebase that need documentation but don't
have any yet (e.g., frontend components, service modules, utility libraries).
**Inputs from doc_assignment:**
- `description`: What this doc should cover (e.g., "Frontend components in src/components/")
- `output_path`: Where to write the file (follows project's existing doc structure)
**Writing approach:**
1. Read the `description` to understand what area of the codebase to document.
2. Explore the relevant source directories using Read, Grep, Glob to discover:
- What modules/components/services exist
- Their purpose (from exports, JSDoc, comments, naming)
- Key interfaces, props, parameters, return types
- Dependencies and relationships between modules
3. Follow the project's existing documentation style:
- If other docs in the same directory use a specific heading structure, match it
- If other docs include code examples, include them here too
- Match the level of detail present in sibling docs
4. Write the doc to `output_path`.
**Required Sections (adapt based on what's being documented):**
- Overview — One paragraph describing what this area of the codebase does
- Module/component listing — Each significant item with a one-line description
- Key interfaces or APIs — The most important exports, props, or function signatures
- Usage examples — 1-2 concrete examples if applicable
**Content Discovery:**
- Read source files in the directories mentioned in `description`
- Grep for `export`, `module.exports`, `export default` to find public APIs
- Check for existing JSDoc, docstrings, or README files in the source directory
- Read test files if present for usage patterns
**Format Notes:**
- Match the project's existing doc style (discovered from sibling docs in the same directory)
- Use the project's primary language for code blocks
- Keep it practical — focus on what a developer needs to know to use or modify these modules
**Doc Tooling Adaptation:** See `<doc_tooling_guidance>` section.
</template_custom>
<doc_tooling_guidance>
## Doc Tooling Adaptation
When `doc_tooling` in `project_context` indicates a documentation framework, adapt file
placement and frontmatter accordingly. Content structure (sections, headings) does not
change — only location and metadata change.
**Docusaurus** (`doc_tooling.docusaurus: true`):
- Write to `docs/{canonical-filename}` (e.g., `docs/ARCHITECTURE.md`)
- Add YAML frontmatter block at top of file (before GSD marker):
```yaml
---
title: Architecture
sidebar_position: 2
description: System architecture and component overview
---
```
- `sidebar_position`: use 1 for README/overview, 2 for Architecture, 3 for Getting Started, etc.
**VitePress** (`doc_tooling.vitepress: true`):
- Write to `docs/{canonical-filename}` (primary docs directory)
- Add YAML frontmatter:
```yaml
---
title: Architecture
description: System architecture and component overview
---
```
- No `sidebar_position` — VitePress sidebars are configured in `.vitepress/config.*`
**MkDocs** (`doc_tooling.mkdocs: true`):
- Write to `docs/{canonical-filename}` (MkDocs default docs directory)
- Add YAML frontmatter with `title` only:
```yaml
---
title: Architecture
---
```
- Respect the `nav:` section in `mkdocs.yml` if present — use matching filenames.
Read `mkdocs.yml` and check if a nav entry references the target doc before writing.
**Storybook** (`doc_tooling.storybook: true`):
- No special doc placement — Storybook handles component stories, not project docs.
- Generate docs to project root as normal. Storybook detection has no effect on
placement or frontmatter.
**No tooling detected:**
- Write to `docs/` directory by default. Exceptions: `README.md` and `CONTRIBUTING.md` stay at project root.
- The `resolve_modes` table in the workflow determines the exact path for each doc type.
- Create the `docs/` directory if it does not exist.
- No frontmatter added.
</doc_tooling_guidance>
<critical_rules>
1. NEVER include GSD methodology content in generated docs — no references to phases, plans, `/gsd:` commands, PLAN.md, ROADMAP.md, or any GSD workflow concepts. Generated docs describe the TARGET PROJECT exclusively.
2. NEVER touch CHANGELOG.md — it is managed by `/gsd:ship` and is out of scope.
3. ALWAYS include the GSD marker `<!-- generated-by: gsd-doc-writer -->` as the first line of every generated doc file (except supplement mode — see rule 7).
4. ALWAYS explore the actual codebase before writing — never fabricate file paths, function names, endpoints, or configuration values.
8. **ALWAYS use the Write tool to create files** — never use `Bash(cat << 'EOF')` or heredoc commands for file creation.
5. Use `<!-- VERIFY: {claim} -->` markers for any infrastructure claim (URLs, server configs, external service details) that cannot be verified from the repository contents alone.
6. In update mode, PRESERVE user-authored content in sections that are still accurate. Only rewrite inaccurate or missing sections.
7. In supplement mode, NEVER modify existing content. Only append missing sections. Do NOT add the GSD marker to hand-written files.
</critical_rules>
<success_criteria>
- [ ] Doc file written to the correct path
- [ ] GSD marker present as first line
- [ ] All required sections from template are present
- [ ] No GSD methodology references in output
- [ ] All file paths, function names, and commands verified against codebase
- [ ] VERIFY markers placed on undiscoverable infrastructure claims
- [ ] (update mode) User-authored accurate sections preserved
- [ ] (supplement mode) Only missing sections were appended; no existing content was modified
</success_criteria>

View File

@@ -2,7 +2,6 @@
name: gsd-executor
description: Executes GSD plans with atomic commits, deviation handling, checkpoint protocols, and state management. Spawned by execute-phase orchestrator or execute-plan command.
tools: Read, Write, Edit, Bash, Grep, Glob
permissionMode: acceptEdits
color: yellow
# hooks:
# PostToolUse:
@@ -133,6 +132,8 @@ No user permission needed for Rules 1-3.
**Critical = required for correct/secure/performant operation.** These aren't "features" — they're correctness requirements.
**Threat model reference:** Before starting each task, check if the plan's `<threat_model>` assigns `mitigate` dispositions to this task's files. Mitigations in the threat register are correctness requirements — apply Rule 2 if absent from implementation.
---
**RULE 3: Auto-fix blocking issues**
@@ -394,6 +395,18 @@ Or: "None - plan executed exactly as written."
- Components with no data source wired (props always receiving empty/mock data)
If any stubs exist, add a `## Known Stubs` section to the SUMMARY listing each stub with its file, line, and reason. These are tracked for the verifier to catch. Do NOT mark a plan as complete if stubs exist that prevent the plan's goal from being achieved — either wire the data or document in the plan why the stub is intentional and which future plan will resolve it.
**Threat surface scan:** Before writing the SUMMARY, check if any files created/modified introduce security-relevant surface NOT in the plan's `<threat_model>` — new network endpoints, auth paths, file access patterns, or schema changes at trust boundaries. If found, add:
```markdown
## Threat Flags
| Flag | File | Description |
|------|------|-------------|
| threat_flag: {type} | {file} | {new surface description} |
```
Omit section if nothing found.
</summary_creation>
<self_check>

View File

@@ -25,6 +25,13 @@ If the prompt contains a `<files_to_read>` block, you MUST use the `Read` tool t
- Document findings with confidence levels (HIGH/MEDIUM/LOW)
- Write RESEARCH.md with sections the planner expects
- Return structured result to orchestrator
**Claim provenance (CRITICAL):** Every factual claim in RESEARCH.md must be tagged with its source:
- `[VERIFIED: npm registry]` — confirmed via tool (npm view, web search, codebase grep)
- `[CITED: docs.example.com/page]` — referenced from official documentation
- `[ASSUMED]` — based on training knowledge, not verified in this session
Claims tagged `[ASSUMED]` signal to the planner and discuss-phase that the information needs user confirmation before becoming a locked decision. Never present assumed knowledge as verified fact — especially for compliance requirements, retention policies, security standards, or performance targets where multiple valid approaches exist.
</role>
<project_context>
@@ -222,6 +229,8 @@ Priority: Context7 > Exa (verified) > Firecrawl (official docs) > Official GitHu
- [ ] Confidence levels assigned honestly
- [ ] "What might I have missed?" review completed
- [ ] **If rename/refactor phase:** Runtime State Inventory completed — all 5 categories answered explicitly (not left blank)
- [ ] Security domain included (or `security_enforcement: false` confirmed)
- [ ] ASVS categories verified against phase tech stack
</verification_protocol>
@@ -343,6 +352,17 @@ Verified patterns from official sources:
**Deprecated/outdated:**
- [Thing]: [why, what replaced it]
## Assumptions Log
> List all claims tagged `[ASSUMED]` in this research. The planner and discuss-phase use this
> section to identify decisions that need user confirmation before execution.
| # | Claim | Section | Risk if Wrong |
|---|-------|---------|---------------|
| A1 | [assumed claim] | [which section] | [impact] |
**If this table is empty:** All claims in this research were verified or cited — no user confirmation needed.
## Open Questions
1. **[Question]**
@@ -393,6 +413,27 @@ Verified patterns from official sources:
*(If no gaps: "None — existing test infrastructure covers all phase requirements")*
## Security Domain
> Required when `security_enforcement` is enabled (absent = enabled). Omit only if explicitly `false` in config.
### Applicable ASVS Categories
| ASVS Category | Applies | Standard Control |
|---------------|---------|-----------------|
| V2 Authentication | {yes/no} | {library or pattern} |
| V3 Session Management | {yes/no} | {library or pattern} |
| V4 Access Control | {yes/no} | {library or pattern} |
| V5 Input Validation | yes | {e.g., zod / joi / pydantic} |
| V6 Cryptography | {yes/no} | {library — never hand-roll} |
### Known Threat Patterns for {stack}
| Pattern | STRIDE | Standard Mitigation |
|---------|--------|---------------------|
| {e.g., SQL injection} | Tampering | {parameterized queries / ORM} |
| {pattern} | {category} | {mitigation} |
## Sources
### Primary (HIGH confidence)

View File

@@ -314,6 +314,49 @@ issue:
fix_hint: "Remove search task - belongs in future phase per user decision"
```
## Dimension 7b: Scope Reduction Detection
**Question:** Did the planner silently simplify user decisions instead of delivering them fully?
**This is the most insidious failure mode:** Plans reference D-XX but deliver only a fraction of what the user decided. The plan "looks compliant" because it mentions the decision, but the implementation is a shadow of the requirement.
**Process:**
1. For each task action in all plans, scan for scope reduction language:
- `"v1"`, `"v2"`, `"simplified"`, `"static for now"`, `"hardcoded"`
- `"future enhancement"`, `"placeholder"`, `"basic version"`, `"minimal"`
- `"will be wired later"`, `"dynamic in future"`, `"skip for now"`
- `"not wired to"`, `"not connected to"`, `"stub"`
2. For each match, cross-reference with the CONTEXT.md decision it claims to implement
3. Compare: does the task deliver what D-XX actually says, or a reduced version?
4. If reduced: BLOCKER — the planner must either deliver fully or propose phase split
**Red flags (from real incident):**
- CONTEXT.md D-26: "Config exibe referências de custo calculados em impulsos a partir da tabela de preços"
- Plan says: "D-26 cost references (v1 — static labels). NOT wired to billingPrecosOriginaisModel — dynamic pricing display is a future enhancement"
- This is a BLOCKER: the planner invented "v1/v2" versioning that doesn't exist in the user's decision
**Severity:** ALWAYS BLOCKER. Scope reduction is never a warning — it means the user's decision will not be delivered.
**Example:**
```yaml
issue:
dimension: scope_reduction
severity: blocker
description: "Plan reduces D-26 from 'calculated costs in impulses' to 'static hardcoded labels'"
plan: "03"
task: 1
decision: "D-26: Config exibe referências de custo calculados em impulsos"
plan_action: "static labels v1 — NOT wired to billing"
fix_hint: "Either implement D-26 fully (fetch from billingPrecosOriginaisModel) or return PHASE SPLIT RECOMMENDED"
```
**Fix path:** When scope reduction is detected, the checker returns ISSUES FOUND with recommendation:
```
Plans reduce {N} user decisions. Options:
1. Revise plans to deliver decisions fully (may increase plan count)
2. Split phase: [suggested grouping of D-XX into sub-phases]
```
## Dimension 8: Nyquist Compliance
Skip if: `workflow.nyquist_validation` is explicitly set to `false` in config.json (absent key = enabled), phase has no RESEARCH.md, or RESEARCH.md has no "Validation Architecture" section. Output: "Dimension 8: SKIPPED (nyquist_validation disabled or not applicable)"

View File

@@ -81,6 +81,45 @@ The orchestrator provides user decisions in `<user_decisions>` tags from `/gsd:d
- Note in task action: "Using X per user decision (research suggested Y)"
</context_fidelity>
<scope_reduction_prohibition>
## CRITICAL: Never Simplify User Decisions — Split Instead
**PROHIBITED language/patterns in task actions:**
- "v1", "v2", "simplified version", "static for now", "hardcoded for now"
- "future enhancement", "placeholder", "basic version", "minimal implementation"
- "will be wired later", "dynamic in future phase", "skip for now"
- Any language that reduces a CONTEXT.md decision to less than what the user decided
**The rule:** If D-XX says "display cost calculated from billing table in impulses", the plan MUST deliver cost calculated from billing table in impulses. NOT "static label /min" as a "v1".
**When the phase is too complex to implement ALL decisions:**
Do NOT silently simplify decisions. Instead:
1. **Create a decision coverage matrix** mapping every D-XX to a plan/task
2. **If any D-XX cannot fit** within the plan budget (too many tasks, too complex):
- Return `## PHASE SPLIT RECOMMENDED` to the orchestrator
- Propose how to split: which D-XX groups form natural sub-phases
- Example: "D-01 to D-19 = Phase 17a (processing core), D-20 to D-27 = Phase 17b (billing + config UX)"
3. The orchestrator will present the split to the user for approval
4. After approval, plan each sub-phase within budget
**Why this matters:** The user spent time making decisions. Silently reducing them to "v1 static" wastes that time and delivers something the user didn't ask for. Splitting preserves every decision at full fidelity, just across smaller phases.
**Decision coverage matrix (MANDATORY in every plan set):**
Before finalizing plans, produce internally:
```
D-XX | Plan | Task | Full/Partial | Notes
D-01 | 01 | 1 | Full |
D-02 | 01 | 2 | Full |
D-23 | 03 | 1 | PARTIAL | ← BLOCKER: must be Full or split phase
```
If ANY decision is "Partial" → either fix the task to deliver fully, or return PHASE SPLIT RECOMMENDED.
</scope_reduction_prohibition>
<philosophy>
## Solo Developer + Claude Workflow
@@ -454,6 +493,21 @@ Output: [Artifacts created]
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| {e.g., client→API} | {untrusted input crosses here} |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-{phase}-01 | {S/T/R/I/D/E} | {function/endpoint/file} | mitigate | {specific: e.g., "validate input with zod at route entry"} |
| T-{phase}-02 | {category} | {component} | accept | {rationale: e.g., "no PII, low-value target"} |
</threat_model>
<verification>
[Overall phase checks]
</verification>
@@ -585,6 +639,8 @@ Only include what Claude literally cannot do.
**Step 0: Extract Requirement IDs**
Read ROADMAP.md `**Requirements:**` line for this phase. Strip brackets if present (e.g., `[AUTH-01, AUTH-02]` → `AUTH-01, AUTH-02`). Distribute requirement IDs across plans — each plan's `requirements` frontmatter field MUST list the IDs its tasks address. **CRITICAL:** Every requirement ID MUST appear in at least one plan. Plans with an empty `requirements` field are invalid.
**Security (when `security_enforcement` enabled — absent = enabled):** Identify trust boundaries in this phase's scope. Map STRIDE categories to applicable tech stack from RESEARCH.md security domain. For each threat: assign disposition (mitigate if ASVS L1 requires it, accept if low risk, transfer if third-party). Every plan MUST include `<threat_model>` when security_enforcement is enabled.
**Step 1: State the Goal**
Take phase goal from ROADMAP.md. Must be outcome-shaped, not task-shaped.
- Good: "Working chat interface" (outcome)
@@ -1193,7 +1249,26 @@ Use template structure for each PLAN.md.
**ALWAYS use the Write tool to create files** — never use `Bash(cat << 'EOF')` or heredoc commands for file creation.
Write to `.planning/phases/XX-name/{phase}-{NN}-PLAN.md`
**CRITICAL — File naming convention (enforced):**
The filename MUST follow the exact pattern: `{padded_phase}-{NN}-PLAN.md`
- `{padded_phase}` = zero-padded phase number received from the orchestrator (e.g. `01`, `02`, `03`, `02.1`)
- `{NN}` = zero-padded sequential plan number within the phase (e.g. `01`, `02`, `03`)
- The suffix is always `-PLAN.md` — NEVER `PLAN-NN.md`, `NN-PLAN.md`, or any other variation
**Correct examples:**
- Phase 1, Plan 1 → `01-01-PLAN.md`
- Phase 3, Plan 2 → `03-02-PLAN.md`
- Phase 2.1, Plan 1 → `02.1-01-PLAN.md`
**Incorrect (will break gsd-tools detection):**
- ❌ `PLAN-01-auth.md`
- ❌ `01-PLAN-01.md`
- ❌ `plan-01.md`
- ❌ `01-01-plan.md` (lowercase)
Full write path: `.planning/phases/{padded_phase}-{slug}/{padded_phase}-{NN}-PLAN.md`
Include all frontmatter fields.
</step>
@@ -1338,6 +1413,9 @@ Phase planning complete when:
- [ ] Wave structure maximizes parallelism
- [ ] PLAN file(s) committed to git
- [ ] User knows next steps and wave structure
- [ ] `<threat_model>` present with STRIDE register (when `security_enforcement` enabled)
- [ ] Every threat has a disposition (mitigate / accept / transfer)
- [ ] Mitigations reference specific implementation (not generic advice)
## Gap Closure Mode

View File

@@ -0,0 +1,128 @@
---
name: gsd-security-auditor
description: Verifies threat mitigations from PLAN.md threat model exist in implemented code. Produces SECURITY.md. Spawned by /gsd:secure-phase.
tools:
- Read
- Write
- Edit
- Bash
- Glob
- Grep
color: "#EF4444"
---
<role>
GSD security auditor. Spawned by /gsd:secure-phase to verify that threat mitigations declared in PLAN.md are present in implemented code.
Does NOT scan blindly for new vulnerabilities. Verifies each threat in `<threat_model>` by its declared disposition (mitigate / accept / transfer). Reports gaps. Writes SECURITY.md.
**Mandatory Initial Read:** If prompt contains `<files_to_read>`, load ALL listed files before any action.
**Implementation files are READ-ONLY.** Only create/modify: SECURITY.md. Implementation security gaps → OPEN_THREATS or ESCALATE. Never patch implementation.
</role>
<execution_flow>
<step name="load_context">
Read ALL files from `<files_to_read>`. Extract:
- PLAN.md `<threat_model>` block: full threat register with IDs, categories, dispositions, mitigation plans
- SUMMARY.md `## Threat Flags` section: new attack surface detected by executor during implementation
- `<config>` block: `asvs_level` (1/2/3), `block_on` (open / unregistered / none)
- Implementation files: exports, auth patterns, input handling, data flows
</step>
<step name="analyze_threats">
For each threat in `<threat_model>`, determine verification method by disposition:
| Disposition | Verification Method |
|-------------|---------------------|
| `mitigate` | Grep for mitigation pattern in files cited in mitigation plan |
| `accept` | Verify entry present in SECURITY.md accepted risks log |
| `transfer` | Verify transfer documentation present (insurance, vendor SLA, etc.) |
Classify each threat before verification. Record classification for every threat — no threat skipped.
</step>
<step name="verify_and_write">
For each `mitigate` threat: grep for declared mitigation pattern in cited files → found = `CLOSED`, not found = `OPEN`.
For `accept` threats: check SECURITY.md accepted risks log → entry present = `CLOSED`, absent = `OPEN`.
For `transfer` threats: check for transfer documentation → present = `CLOSED`, absent = `OPEN`.
For each `threat_flag` in SUMMARY.md `## Threat Flags`: if maps to existing threat ID → informational. If no mapping → log as `unregistered_flag` in SECURITY.md (not a blocker).
Write SECURITY.md. Set `threats_open` count. Return structured result.
</step>
</execution_flow>
<structured_returns>
## SECURED
```markdown
## SECURED
**Phase:** {N} — {name}
**Threats Closed:** {count}/{total}
**ASVS Level:** {1/2/3}
### Threat Verification
| Threat ID | Category | Disposition | Evidence |
|-----------|----------|-------------|----------|
| {id} | {category} | {mitigate/accept/transfer} | {file:line or doc reference} |
### Unregistered Flags
{none / list from SUMMARY.md ## Threat Flags with no threat mapping}
SECURITY.md: {path}
```
## OPEN_THREATS
```markdown
## OPEN_THREATS
**Phase:** {N} — {name}
**Closed:** {M}/{total} | **Open:** {K}/{total}
**ASVS Level:** {1/2/3}
### Closed
| Threat ID | Category | Disposition | Evidence |
|-----------|----------|-------------|----------|
| {id} | {category} | {disposition} | {evidence} |
### Open
| Threat ID | Category | Mitigation Expected | Files Searched |
|-----------|----------|---------------------|----------------|
| {id} | {category} | {pattern not found} | {file paths} |
Next: Implement mitigations or document as accepted in SECURITY.md accepted risks log, then re-run /gsd:secure-phase.
SECURITY.md: {path}
```
## ESCALATE
```markdown
## ESCALATE
**Phase:** {N} — {name}
**Closed:** 0/{total}
### Details
| Threat ID | Reason Blocked | Suggested Action |
|-----------|----------------|------------------|
| {id} | {reason} | {action} |
```
</structured_returns>
<success_criteria>
- [ ] All `<files_to_read>` loaded before any analysis
- [ ] Threat register extracted from PLAN.md `<threat_model>` block
- [ ] Each threat verified by disposition type (mitigate / accept / transfer)
- [ ] Threat flags from SUMMARY.md `## Threat Flags` incorporated
- [ ] Implementation files never modified
- [ ] SECURITY.md written to correct path
- [ ] Structured return: SECURED / OPEN_THREATS / ESCALATE
</success_criteria>

View File

@@ -88,13 +88,21 @@ Extract phase goal from ROADMAP.md — this is the outcome to verify, not the ta
In re-verification mode, must-haves come from Step 0.
**Option A: Must-haves in PLAN frontmatter**
**Step 2a: Always load ROADMAP Success Criteria**
```bash
PHASE_DATA=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" roadmap get-phase "$PHASE_NUM" --raw)
```
Parse the `success_criteria` array from the JSON output. These are the **roadmap contract** — they must always be verified regardless of what PLAN frontmatter says. Store them as `roadmap_truths`.
**Step 2b: Load PLAN frontmatter must-haves (if present)**
```bash
grep -l "must_haves:" "$PHASE_DIR"/*-PLAN.md 2>/dev/null
```
If found, extract and use:
If found, extract:
```yaml
must_haves:
@@ -110,25 +118,20 @@ must_haves:
via: "fetch in useEffect"
```
**Option B: Use Success Criteria from ROADMAP.md**
**Step 2c: Merge must-haves**
If no must_haves in frontmatter, check for Success Criteria:
Combine all sources into a single must-haves list:
```bash
PHASE_DATA=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" roadmap get-phase "$PHASE_NUM" --raw)
```
1. **Start with `roadmap_truths`** from Step 2a (these are non-negotiable)
2. **Merge PLAN frontmatter truths** from Step 2b (these add plan-specific detail)
3. **Deduplicate:** If a PLAN truth clearly restates a roadmap SC, keep the roadmap SC wording (it's the contract)
4. **If neither 2a nor 2b produced any truths**, fall back to Option C below
Parse the `success_criteria` array from the JSON output. If non-empty:
1. **Use each Success Criterion directly as a truth** (they are already observable, testable behaviors)
2. **Derive artifacts:** For each truth, "What must EXIST?" — map to concrete file paths
3. **Derive key links:** For each artifact, "What must be CONNECTED?" — this is where stubs hide
4. **Document must-haves** before proceeding
Success Criteria from ROADMAP.md are the contract — they take priority over Goal-derived truths.
**CRITICAL:** PLAN frontmatter must-haves must NOT reduce scope. If ROADMAP.md defines 5 Success Criteria but the plan only lists 3 in must_haves, all 5 must still be verified. The plan can ADD must-haves but never subtract roadmap SCs.
**Option C: Derive from phase goal (fallback)**
If no must_haves in frontmatter AND no Success Criteria in ROADMAP:
If no Success Criteria in ROADMAP AND no must_haves in frontmatter:
1. **State the goal** from ROADMAP.md
2. **Derive truths:** "What must be TRUE?" — list 3-7 observable, testable behaviors
@@ -442,16 +445,26 @@ npm test -- --grep "$PHASE_TEST_PATTERN" 2>&1 | grep -q "passing"
## Step 9: Determine Overall Status
**Status: passed** — All truths VERIFIED, all artifacts pass levels 1-3, all key links WIRED, no blocker anti-patterns.
Classify status using this decision tree IN ORDER (most restrictive first):
**Status: gaps_found** — One or more truths FAILED, artifacts MISSING/STUB, key links NOT_WIRED, or blocker anti-patterns found.
1. IF any truth FAILED, artifact MISSING/STUB, key link NOT_WIRED, or blocker anti-pattern found:
→ **status: gaps_found**
**Status: human_needed** — All automated checks pass but items flagged for human verification.
2. IF Step 8 produced ANY human verification items (section is non-empty):
→ **status: human_needed**
(Even if all truths are VERIFIED and score is N/N — human items take priority)
3. IF all truths VERIFIED, all artifacts pass, all links WIRED, no blockers, AND no human verification items:
→ **status: passed**
**passed is ONLY valid when the human verification section is empty.** If you identified items requiring human testing in Step 8, status MUST be human_needed.
**Score:** `verified_truths / total_truths`
## Step 10: Structure Gap Output (If Gaps Found)
Before writing VERIFICATION.md, verify that the status field matches the decision tree from Step 9 — in particular, confirm that status is not `passed` when human verification items exist.
Structure gaps in YAML frontmatter for `/gsd:plan-phase --gaps`:
```yaml

View File

@@ -412,14 +412,78 @@ function resolveKiloConfigPath(configDir) {
}
/**
* Read and parse settings.json, returning empty object if it doesn't exist
* Strip JSONC comments (// and /* *​/) from a string to produce valid JSON.
* Handles comments inside strings correctly (does not strip them).
*/
function stripJsonComments(text) {
let result = '';
let i = 0;
let inString = false;
let stringChar = '';
while (i < text.length) {
// Handle string literals — don't strip comments inside strings
if (inString) {
if (text[i] === '\\') {
result += text[i] + (text[i + 1] || '');
i += 2;
continue;
}
if (text[i] === stringChar) {
inString = false;
}
result += text[i];
i++;
continue;
}
// Start of string
if (text[i] === '"' || text[i] === "'") {
inString = true;
stringChar = text[i];
result += text[i];
i++;
continue;
}
// Line comment
if (text[i] === '/' && text[i + 1] === '/') {
// Skip to end of line
while (i < text.length && text[i] !== '\n') i++;
continue;
}
// Block comment
if (text[i] === '/' && text[i + 1] === '*') {
i += 2;
while (i < text.length && !(text[i] === '*' && text[i + 1] === '/')) i++;
i += 2; // skip closing */
continue;
}
result += text[i];
i++;
}
// Remove trailing commas before } or ] (common in JSONC)
return result.replace(/,\s*([}\]])/g, '$1');
}
/**
* Read and parse settings.json, returning empty object if it doesn't exist.
* Supports JSONC (JSON with comments) — many CLI tools allow comments in
* their settings files, so we strip them before parsing to avoid silent
* data loss from JSON.parse failures.
*/
function readSettings(settingsPath) {
if (fs.existsSync(settingsPath)) {
try {
return JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
const raw = fs.readFileSync(settingsPath, 'utf8');
// Try standard JSON first (fast path)
try {
return JSON.parse(raw);
} catch {
// Fall back to JSONC stripping
return JSON.parse(stripJsonComments(raw));
}
} catch (e) {
return {};
// If even JSONC stripping fails, warn instead of silently returning {}
console.warn(' ' + yellow + '⚠' + reset + ' Warning: Could not parse ' + settingsPath + ' — file may be malformed. Existing settings preserved.');
return null;
}
}
return {};
@@ -466,12 +530,12 @@ function getCommitAttribution(runtime) {
const resolveConfigPath = runtime === 'opencode'
? resolveOpencodeConfigPath
: resolveKiloConfigPath;
const config = readJsoncSettings(resolveConfigPath(getGlobalDir(runtime, null)));
result = config.disable_ai_attribution === true ? null : undefined;
const config = readSettings(resolveConfigPath(getGlobalDir(runtime, null)));
result = (config && config.disable_ai_attribution === true) ? null : undefined;
} else if (runtime === 'gemini') {
// Gemini: check gemini settings.json for attribution config
const settings = readSettings(path.join(getGlobalDir('gemini', explicitConfigDir), 'settings.json'));
if (!settings.attribution || settings.attribution.commit === undefined) {
if (!settings || !settings.attribution || settings.attribution.commit === undefined) {
result = undefined;
} else if (settings.attribution.commit === '') {
result = null;
@@ -481,7 +545,7 @@ function getCommitAttribution(runtime) {
} else if (runtime === 'claude') {
// Claude Code
const settings = readSettings(path.join(getGlobalDir('claude', explicitConfigDir), 'settings.json'));
if (!settings.attribution || settings.attribution.commit === undefined) {
if (!settings || !settings.attribution || settings.attribution.commit === undefined) {
result = undefined;
} else if (settings.attribution.commit === '') {
result = null;
@@ -710,6 +774,7 @@ function convertClaudeToCopilotContent(content, isGlobal = false) {
} else {
c = c.replace(/\$HOME\/\.claude\//g, '.github/');
c = c.replace(/~\/\.claude\//g, '.github/');
c = c.replace(/~\/\.claude\n/g, '.github/');
}
c = c.replace(/\.\/\.claude\//g, './.github/');
c = c.replace(/\.claude\//g, '.github/');
@@ -754,6 +819,39 @@ function convertClaudeCommandToCopilotSkill(content, skillName, isGlobal = false
return `${fm}\n${body}`;
}
/**
* Convert a Claude command (.md) to a Claude skill (SKILL.md).
* Claude Code is the native format, so minimal conversion needed —
* preserve allowed-tools as YAML multiline list, preserve argument-hint,
* convert name from gsd:xxx to gsd-xxx format.
*/
function convertClaudeCommandToClaudeSkill(content, skillName) {
const { frontmatter, body } = extractFrontmatterAndBody(content);
if (!frontmatter) return content;
const description = extractFrontmatterField(frontmatter, 'description') || '';
const argumentHint = extractFrontmatterField(frontmatter, 'argument-hint');
const agent = extractFrontmatterField(frontmatter, 'agent');
// Preserve allowed-tools as YAML multiline list (Claude native format)
const toolsMatch = frontmatter.match(/^allowed-tools:\s*\n((?:\s+-\s+.+\n?)*)/m);
let toolsBlock = '';
if (toolsMatch) {
toolsBlock = 'allowed-tools:\n' + toolsMatch[1];
// Ensure trailing newline
if (!toolsBlock.endsWith('\n')) toolsBlock += '\n';
}
// Reconstruct frontmatter in Claude skill format
let fm = `---\nname: ${skillName}\ndescription: ${yamlQuote(description)}\n`;
if (argumentHint) fm += `argument-hint: ${yamlQuote(argumentHint)}\n`;
if (agent) fm += `agent: ${agent}\n`;
if (toolsBlock) fm += toolsBlock;
fm += '---';
return `${fm}\n${body}`;
}
/**
* Convert a Claude agent (.md) to a Copilot agent (.agent.md).
* Applies tool mapping + deduplication, formats tools as JSON array.
@@ -1051,10 +1149,10 @@ function convertClaudeToWindsurfMarkdown(content) {
converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"');
converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
// Replace project-level Claude conventions with Windsurf equivalents
converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.windsurf/rules/`');
converted = converted.replace(/\.\/CLAUDE\.md/g, '.windsurf/rules/');
converted = converted.replace(/`CLAUDE\.md`/g, '`.windsurf/rules/`');
converted = converted.replace(/\bCLAUDE\.md\b/g, '.windsurf/rules/');
converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.windsurf/rules`');
converted = converted.replace(/\.\/CLAUDE\.md/g, '.windsurf/rules');
converted = converted.replace(/`CLAUDE\.md`/g, '`.windsurf/rules`');
converted = converted.replace(/\bCLAUDE\.md\b/g, '.windsurf/rules');
converted = converted.replace(/\.claude\/skills\//g, '.windsurf/skills/');
// Remove Claude Code-specific bug workarounds before brand replacement
converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
@@ -1137,6 +1235,10 @@ function convertSlashCommandsToCodexSkillMentions(content) {
function convertClaudeToCodexMarkdown(content) {
let converted = convertSlashCommandsToCodexSkillMentions(content);
converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
// Path replacement: .claude → .codex (#1430)
converted = converted.replace(/\$HOME\/\.claude\//g, '$HOME/.codex/');
converted = converted.replace(/~\/\.claude\//g, '~/.codex/');
converted = converted.replace(/\.\/\.claude\//g, './.codex/');
// Runtime-neutral agent name replacement (#766)
converted = neutralizeAgentReferences(converted, 'AGENTS.md');
return converted;
@@ -3279,6 +3381,63 @@ function copyCommandsAsCopilotSkills(srcDir, skillsDir, prefix, isGlobal = false
recurse(srcDir, prefix);
}
/**
* Copy Claude commands as Claude skills — one folder per skill with SKILL.md.
* Claude Code 2.1.88+ uses skills/xxx/SKILL.md instead of commands/gsd/xxx.md.
* Claude is the native format so no path replacement is needed — only
* frontmatter restructuring via convertClaudeCommandToClaudeSkill.
* @param {string} srcDir - Source commands directory
* @param {string} skillsDir - Target skills directory
* @param {string} prefix - Skill name prefix (e.g. 'gsd')
* @param {string} pathPrefix - Path prefix for file references
* @param {string} runtime - Target runtime
* @param {boolean} isGlobal - Whether this is a global install
*/
function copyCommandsAsClaudeSkills(srcDir, skillsDir, prefix, pathPrefix, runtime, isGlobal = false) {
if (!fs.existsSync(srcDir)) {
return;
}
fs.mkdirSync(skillsDir, { recursive: true });
// Remove previous GSD Claude skills to avoid stale command skills
const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
for (const entry of existing) {
if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
fs.rmSync(path.join(skillsDir, entry.name), { recursive: true });
}
}
function recurse(currentSrcDir, currentPrefix) {
const entries = fs.readdirSync(currentSrcDir, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path.join(currentSrcDir, entry.name);
if (entry.isDirectory()) {
recurse(srcPath, `${currentPrefix}-${entry.name}`);
continue;
}
if (!entry.name.endsWith('.md')) {
continue;
}
const baseName = entry.name.replace('.md', '');
const skillName = `${currentPrefix}-${baseName}`;
const skillDir = path.join(skillsDir, skillName);
fs.mkdirSync(skillDir, { recursive: true });
let content = fs.readFileSync(srcPath, 'utf8');
content = processAttribution(content, getCommitAttribution('claude'));
content = convertClaudeCommandToClaudeSkill(content, skillName);
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
}
}
recurse(srcDir, prefix);
}
/**
* Recursively install GSD commands as Antigravity skills.
* Each command becomes a skill-name/ folder containing SKILL.md.
@@ -3439,7 +3598,7 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
let jsContent = fs.readFileSync(srcPath, 'utf8');
jsContent = jsContent.replace(/gsd:/gi, 'gsd-');
jsContent = jsContent.replace(/\.claude\/skills\//g, '.windsurf/skills/');
jsContent = jsContent.replace(/CLAUDE\.md/g, '.windsurf/rules/');
jsContent = jsContent.replace(/CLAUDE\.md/g, '.windsurf/rules');
jsContent = jsContent.replace(/\bClaude Code\b/g, 'Windsurf');
fs.writeFileSync(destPath, jsContent);
} else {
@@ -3611,6 +3770,7 @@ function validateHookFields(settings) {
function uninstall(isGlobal, runtime = 'claude') {
const isOpencode = runtime === 'opencode';
const isKilo = runtime === 'kilo';
const isGemini = runtime === 'gemini';
const isCodex = runtime === 'codex';
const isCopilot = runtime === 'copilot';
const isAntigravity = runtime === 'antigravity';
@@ -3799,13 +3959,39 @@ function uninstall(isGlobal, runtime = 'claude') {
console.log(` ${green}✓${reset} Removed ${skillCount} Windsurf skills`);
}
}
} else {
} else if (isGemini) {
// Gemini: still uses commands/gsd/
const gsdCommandsDir = path.join(targetDir, 'commands', 'gsd');
if (fs.existsSync(gsdCommandsDir)) {
fs.rmSync(gsdCommandsDir, { recursive: true });
removedCount++;
console.log(` ${green}✓${reset} Removed commands/gsd/`);
}
} else {
// Claude Code: remove skills/gsd-*/ directories
const skillsDir = path.join(targetDir, 'skills');
if (fs.existsSync(skillsDir)) {
let skillCount = 0;
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && entry.name.startsWith('gsd-')) {
fs.rmSync(path.join(skillsDir, entry.name), { recursive: true });
skillCount++;
}
}
if (skillCount > 0) {
removedCount++;
console.log(` ${green}✓${reset} Removed ${skillCount} Claude Code skills`);
}
}
// Also clean up legacy commands/gsd/ from older installs
const legacyCommandsDir = path.join(targetDir, 'commands', 'gsd');
if (fs.existsSync(legacyCommandsDir)) {
fs.rmSync(legacyCommandsDir, { recursive: true });
removedCount++;
console.log(` ${green}✓${reset} Removed legacy commands/gsd/`);
}
}
// 2. Remove get-shit-done directory
@@ -3871,6 +4057,10 @@ function uninstall(isGlobal, runtime = 'claude') {
const settingsPath = path.join(targetDir, 'settings.json');
if (fs.existsSync(settingsPath)) {
let settings = readSettings(settingsPath);
if (settings === null) {
console.log(` ${yellow}i${reset} Skipping settings.json cleanup — file could not be parsed`);
settings = {}; // prevent downstream crashes, but don't write back
}
let settingsModified = false;
// Remove GSD statusline if it references our hook
@@ -4341,6 +4531,7 @@ function generateManifest(dir, baseDir) {
function writeManifest(configDir, runtime = 'claude') {
const isOpencode = runtime === 'opencode';
const isKilo = runtime === 'kilo';
const isGemini = runtime === 'gemini';
const isCodex = runtime === 'codex';
const isCopilot = runtime === 'copilot';
const isAntigravity = runtime === 'antigravity';
@@ -4357,7 +4548,7 @@ function writeManifest(configDir, runtime = 'claude') {
for (const [rel, hash] of Object.entries(gsdHashes)) {
manifest.files['get-shit-done/' + rel] = hash;
}
if (!isOpencode && !isCodex && !isCopilot && !isAntigravity && !isCursor && !isWindsurf && fs.existsSync(commandsDir)) {
if (isGemini && fs.existsSync(commandsDir)) {
const cmdHashes = generateManifest(commandsDir);
for (const [rel, hash] of Object.entries(cmdHashes)) {
manifest.files['commands/gsd/' + rel] = hash;
@@ -4370,7 +4561,7 @@ function writeManifest(configDir, runtime = 'claude') {
}
}
}
if ((isCodex || isCopilot || isAntigravity || isCursor || isWindsurf) && fs.existsSync(codexSkillsDir)) {
if ((isCodex || isCopilot || isAntigravity || isCursor || isWindsurf || (!isOpencode && !isGemini)) && fs.existsSync(codexSkillsDir)) {
for (const skillName of listCodexSkillNames(codexSkillsDir)) {
const skillRoot = path.join(codexSkillsDir, skillName);
const skillHashes = generateManifest(skillRoot);
@@ -4406,6 +4597,8 @@ function writeManifest(configDir, runtime = 'claude') {
/**
* Detect user-modified GSD files by comparing against install manifest.
* Backs up modified files to gsd-local-patches/ for reapply after update.
* Also saves pristine copies (from manifest) to gsd-pristine/ to enable
* three-way merge during reapply-patches (pristine vs user vs new).
*/
function saveLocalPatches(configDir) {
const manifestPath = path.join(configDir, MANIFEST_NAME);
@@ -4415,6 +4608,7 @@ function saveLocalPatches(configDir) {
try { manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); } catch { return []; }
const patchesDir = path.join(configDir, PATCHES_DIR_NAME);
const pristineDir = path.join(configDir, 'gsd-pristine');
const modified = [];
for (const [relPath, originalHash] of Object.entries(manifest.files || {})) {
@@ -4422,6 +4616,7 @@ function saveLocalPatches(configDir) {
if (!fs.existsSync(fullPath)) continue;
const currentHash = fileHash(fullPath);
if (currentHash !== originalHash) {
// Back up the user's modified version
const backupPath = path.join(patchesDir, relPath);
fs.mkdirSync(path.dirname(backupPath), { recursive: true });
fs.copyFileSync(fullPath, backupPath);
@@ -4429,12 +4624,28 @@ function saveLocalPatches(configDir) {
}
}
// Save pristine copies of modified files from the CURRENT install (before wipe)
// These represent the original GSD distribution files that the user then modified.
// The reapply-patches workflow uses these for three-way merge:
// pristine (original) → user's version (what they changed) → new version (after update)
if (modified.length > 0) {
// We need the pristine originals, but the current files on disk are user-modified.
// The manifest records SHA-256 hashes but not content. However, we can reconstruct
// the pristine version from the npm package cache or git history.
// As a practical approach: save the manifest's version info so the reapply workflow
// knows which GSD version these files came from, enabling npm-based reconstruction.
const meta = {
backed_up_at: new Date().toISOString(),
from_version: manifest.version,
files: modified
from_manifest_timestamp: manifest.timestamp,
files: modified,
pristine_hashes: {}
};
// Record the original (pristine) hash for each modified file
// This lets the reapply workflow verify reconstructed pristine files
for (const relPath of modified) {
meta.pristine_hashes[relPath] = manifest.files[relPath];
}
fs.writeFileSync(path.join(patchesDir, 'backup-meta.json'), JSON.stringify(meta, null, 2));
console.log(' ' + yellow + 'i' + reset + ' Found ' + modified.length + ' locally modified GSD file(s) — backed up to ' + PATCHES_DIR_NAME + '/');
for (const f of modified) {
@@ -4605,11 +4816,11 @@ function install(isGlobal, runtime = 'claude') {
} else {
failures.push('skills/gsd-*');
}
} else {
// Claude Code & Gemini: nested structure in commands/ directory
} else if (isGemini) {
// Gemini: nested structure in commands/ directory (still supported)
const commandsDir = path.join(targetDir, 'commands');
fs.mkdirSync(commandsDir, { recursive: true });
const gsdSrc = path.join(src, 'commands', 'gsd');
const gsdDest = path.join(commandsDir, 'gsd');
copyWithPathReplacement(gsdSrc, gsdDest, pathPrefix, runtime, true, isGlobal);
@@ -4618,6 +4829,29 @@ function install(isGlobal, runtime = 'claude') {
} else {
failures.push('commands/gsd');
}
} else {
// Claude Code: skills/ format (2.1.88+ compatibility)
const skillsDir = path.join(targetDir, 'skills');
const gsdSrc = path.join(src, 'commands', 'gsd');
copyCommandsAsClaudeSkills(gsdSrc, skillsDir, 'gsd', pathPrefix, runtime, isGlobal);
if (fs.existsSync(skillsDir)) {
const count = fs.readdirSync(skillsDir, { withFileTypes: true })
.filter(e => e.isDirectory() && e.name.startsWith('gsd-')).length;
if (count > 0) {
console.log(` ${green}✓${reset} Installed ${count} skills to skills/`);
} else {
failures.push('skills/gsd-*');
}
} else {
failures.push('skills/gsd-*');
}
// Clean up legacy commands/gsd/ from previous installs
const legacyCommandsDir = path.join(targetDir, 'commands', 'gsd');
if (fs.existsSync(legacyCommandsDir)) {
fs.rmSync(legacyCommandsDir, { recursive: true });
console.log(` ${green}✓${reset} Removed legacy commands/gsd/ directory`);
}
}
// Copy get-shit-done skill with path replacement
@@ -4878,7 +5112,12 @@ function install(isGlobal, runtime = 'claude') {
// Gemini and Antigravity use AfterTool instead of PostToolUse for post-tool hooks
const postToolEvent = (runtime === 'gemini' || runtime === 'antigravity') ? 'AfterTool' : 'PostToolUse';
const settingsPath = path.join(targetDir, 'settings.json');
const settings = validateHookFields(cleanupOrphanedHooks(readSettings(settingsPath)));
const rawSettings = readSettings(settingsPath);
if (rawSettings === null) {
console.log(' ' + yellow + 'i' + reset + ' Skipping settings.json configuration — file could not be parsed (comments or malformed JSON). Your existing settings are preserved.');
return;
}
const settings = validateHookFields(cleanupOrphanedHooks(rawSettings));
const statuslineCommand = isGlobal
? buildHookCommand(targetDir, 'gsd-statusline.js')
: 'node ' + dirName + '/hooks/gsd-statusline.js';
@@ -5402,6 +5641,8 @@ if (process.env.GSD_TEST_MODE) {
convertClaudeCommandToAntigravitySkill,
convertClaudeAgentToAntigravityAgent,
copyCommandsAsAntigravitySkills,
convertClaudeCommandToClaudeSkill,
copyCommandsAsClaudeSkills,
convertClaudeToWindsurfMarkdown,
convertClaudeCommandToWindsurfSkill,
convertClaudeAgentToWindsurfAgent,

View File

@@ -29,7 +29,7 @@ the normal phase sequence and accumulate context over time.
3. **Create the phase directory:**
```bash
SLUG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" generate-slug "$ARGUMENTS")
SLUG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" generate-slug "$ARGUMENTS" --raw)
mkdir -p ".planning/phases/${NEXT}-${SLUG}"
touch ".planning/phases/${NEXT}-${SLUG}/.gitkeep"
```

View File

@@ -1,7 +1,7 @@
---
name: gsd:discuss-phase
description: Gather phase context through adaptive questioning before planning. Use --auto to skip interactive questions (Claude picks recommended defaults).
argument-hint: "<phase> [--auto] [--batch] [--analyze] [--text]"
description: Gather phase context through adaptive questioning before planning. Use --auto to skip interactive questions (Claude picks recommended defaults). Use --chain for interactive discuss followed by automatic plan+execute.
argument-hint: "<phase> [--auto] [--chain] [--batch] [--analyze] [--text]"
allowed-tools:
- Read
- Write

View File

@@ -0,0 +1,48 @@
---
name: gsd:docs-update
description: Generate or update project documentation verified against the codebase
argument-hint: "[--force] [--verify-only]"
allowed-tools:
- Read
- Write
- Edit
- Bash
- Glob
- Grep
- Task
- AskUserQuestion
---
<objective>
Generate and update up to 9 documentation files for the current project. Each doc type is written by a gsd-doc-writer subagent that explores the codebase directly — no hallucinated paths, phantom endpoints, or stale signatures.
Flag handling rule:
- The optional flags documented below are available behaviors, not implied active behaviors
- A flag is active only when its literal token appears in `$ARGUMENTS`
- If a documented flag is absent from `$ARGUMENTS`, treat it as inactive
- `--force`: skip preservation prompts, regenerate all docs regardless of existing content or GSD markers
- `--verify-only`: check existing docs for accuracy against codebase, no generation (full verification requires Phase 4 verifier)
- If `--force` and `--verify-only` both appear in `$ARGUMENTS`, `--force` takes precedence
</objective>
<execution_context>
@~/.claude/get-shit-done/workflows/docs-update.md
</execution_context>
<context>
Arguments: $ARGUMENTS
**Available optional flags (documentation only — not automatically active):**
- `--force` — Regenerate all docs. Overwrites hand-written and GSD docs alike. No preservation prompts.
- `--verify-only` — Check existing docs for accuracy against the codebase. No files are written. Reports VERIFY marker count. Full codebase fact-checking requires the gsd-doc-verifier agent (Phase 4).
**Active flags must be derived from `$ARGUMENTS`:**
- `--force` is active only if the literal `--force` token is present in `$ARGUMENTS`
- `--verify-only` is active only if the literal `--verify-only` token is present in `$ARGUMENTS`
- If neither token appears, run the standard full-phase generation flow
- Do not infer that a flag is active just because it is documented in this prompt
</context>
<process>
Execute the docs-update workflow from @~/.claude/get-shit-done/workflows/docs-update.md end-to-end.
Preserve all workflow gates (preservation_check, flag handling, wave execution, monorepo dispatch, commit, reporting).
</process>

View File

@@ -8,6 +8,7 @@ allowed-tools:
- Glob
- Grep
- AskUserQuestion
- Skill
- Task
---
<objective>

View File

@@ -1,7 +1,7 @@
---
name: gsd:quick
description: Execute a quick task with GSD guarantees (atomic commits, state tracking) but skip optional agents
argument-hint: "[--full] [--discuss] [--research]"
argument-hint: "[--full] [--validate] [--discuss] [--research]"
allowed-tools:
- Read
- Write
@@ -24,11 +24,13 @@ Quick mode is the same system with a shorter path:
**`--discuss` flag:** Lightweight discussion phase before planning. Surfaces assumptions, clarifies gray areas, captures decisions in CONTEXT.md. Use when the task has ambiguity worth resolving upfront.
**`--full` flag:** Enables plan-checking (max 2 iterations) and post-execution verification. Use when you want quality guarantees without full milestone ceremony.
**`--full` flag:** Enables the complete quality pipeline — discussion + research + plan-checking + verification. One flag for everything.
**`--validate` flag:** Enables plan-checking (max 2 iterations) and post-execution verification only. Use when you want quality guarantees without discussion or research.
**`--research` flag:** Spawns a focused research agent before planning. Investigates implementation approaches, library options, and pitfalls for the task. Use when you're unsure of the best approach.
Flags are composable: `--discuss --research --full` gives discussion + research + plan-checking + verification.
Granular flags are composable: `--discuss --research --validate` gives the same result as `--full`.
</objective>
<execution_context>

View File

@@ -4,7 +4,9 @@ allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion
---
<purpose>
After a GSD update wipes and reinstalls files, this command merges user's previously saved local modifications back into the new version. Uses intelligent comparison to handle cases where the upstream file also changed.
After a GSD update wipes and reinstalls files, this command merges user's previously saved local modifications back into the new version. Uses three-way comparison (pristine baseline, user-modified backup, newly installed version) to reliably distinguish user customizations from version drift.
**Critical invariant:** Every file in `gsd-local-patches/` was backed up because the installer's hash comparison detected it was modified. The workflow must NEVER conclude "no custom content" for any backed-up file — that is a logical contradiction. When in doubt, classify as CONFLICT requiring user review, not SKIP.
</purpose>
<process>
@@ -117,7 +119,43 @@ after modifying any GSD workflow, command, or agent files.
```
Exit.
## Step 2: Show patch summary
## Step 2: Determine baseline for three-way comparison
The quality of the merge depends on having a **pristine baseline** — the original unmodified version of each file from the pre-update GSD release. This enables three-way comparison:
- **Pristine baseline** (original GSD file before any user edits)
- **User's version** (backed up in `gsd-local-patches/`)
- **New version** (freshly installed after update)
Check for baseline sources in priority order:
### Option A: Git history (most reliable)
If the config directory is a git repository:
```bash
CONFIG_DIR=$(dirname "$PATCHES_DIR")
if git -C "$CONFIG_DIR" rev-parse --git-dir >/dev/null 2>&1; then
HAS_GIT=true
fi
```
When `HAS_GIT=true`, use `git log` to find the commit where GSD was originally installed (before user edits). For each file, the pristine baseline can be extracted with:
```bash
git -C "$CONFIG_DIR" log --diff-filter=A --format="%H" -- "{file_path}"
```
This gives the commit that first added the file (the install commit). Extract the pristine version:
```bash
git -C "$CONFIG_DIR" show {install_commit}:{file_path}
```
### Option B: Pristine snapshot directory
Check if a `gsd-pristine/` directory exists alongside `gsd-local-patches/`:
```bash
PRISTINE_DIR="$CONFIG_DIR/gsd-pristine"
```
If it exists, the installer saved pristine copies at install time. Use these as the baseline.
### Option C: No baseline available (two-way fallback)
If neither git history nor pristine snapshots are available, fall back to two-way comparison — but with **strengthened heuristics** (see Step 3).
## Step 3: Show patch summary
```
## Local Patches to Reapply
@@ -125,6 +163,7 @@ Exit.
**Backed up from:** v{from_version}
**Current version:** {read VERSION file}
**Files modified:** {count}
**Merge strategy:** {three-way (git) | three-way (pristine) | two-way (enhanced)}
| # | File | Status |
|---|------|--------|
@@ -132,30 +171,59 @@ Exit.
| 2 | {file_path} | Pending |
```
## Step 3: Merge each file
## Step 4: Merge each file
For each file in `backup-meta.json`:
1. **Read the backed-up version** (user's modified copy from `gsd-local-patches/`)
2. **Read the newly installed version** (current file after update)
3. **Compare and merge:**
3. **If available, read the pristine baseline** (from git history or `gsd-pristine/`)
- If the new file is identical to the backed-up file: skip (modification was incorporated upstream)
- If the new file differs: identify the user's modifications and apply them to the new version
### Three-way merge (when baseline is available)
**Merge strategy:**
- Read both versions fully
- Identify sections the user added or modified (look for additions, not just differences from path replacement)
- Apply user's additions/modifications to the new version
- If a section the user modified was also changed upstream: flag as conflict, show both versions, ask user which to keep
Compare the three versions to isolate changes:
- **User changes** = diff(pristine → user's version) — these are the customizations to preserve
- **Upstream changes** = diff(pristine → new version) — these are version updates to accept
**Merge rules:**
- Sections changed only by user → apply user's version
- Sections changed only by upstream → accept upstream version
- Sections changed by both → flag as CONFLICT, show both, ask user
- Sections unchanged by either → use new version (identical to all three)
### Two-way merge (fallback when no baseline)
When no pristine baseline is available, use these **strengthened heuristics**:
**CRITICAL RULE: Every file in this backup directory was explicitly detected as modified by the installer's SHA-256 hash comparison. "No custom content" is never a valid conclusion.**
For each file:
a. Read both versions completely
b. Identify ALL differences, then classify each as:
- **Mechanical drift** — path substitutions (e.g. `/Users/xxx/.claude/` → `$HOME/.claude/`), variable additions (`${GSD_WS}`, `${AGENT_SKILLS_*}`), error handling additions (`|| true`)
- **User customization** — added steps/sections, removed sections, reordered content, changed behavior, added frontmatter fields, modified instructions
c. **If ANY differences remain after filtering out mechanical drift → those are user customizations. Merge them.**
d. **If ALL differences appear to be mechanical drift → still flag as CONFLICT.** The installer's hash check already proved this file was modified. Ask the user: "This file appears to only have path/variable differences. Were there intentional customizations?" Do NOT silently skip.
### Git-enhanced two-way merge
When the config directory is a git repo but the pristine install commit can't be found, use commit history to identify user changes:
```bash
# Find non-update commits that touched this file
git -C "$CONFIG_DIR" log --oneline --no-merges -- "{file_path}" | grep -v "gsd:update\|GSD update\|gsd-install"
```
Each matching commit represents an intentional user modification. Use the commit messages and diffs to understand what was changed and why.
4. **Write merged result** to the installed location
5. **Report status:**
- `Merged` — user modifications applied cleanly
- `Skipped` — modification already in upstream
- `Conflict` — user chose resolution
5. **Report status per file:**
- `Merged` — user modifications applied cleanly (show summary of what was preserved)
- `Conflict` — user reviewed and chose resolution
- `Incorporated` — user's modification was already adopted upstream (only valid when pristine baseline confirms this)
## Step 4: Update manifest
**Never report `Skipped — no custom content`.** If a file is in the backup, it has custom content.
## Step 5: Update manifest
After reapplying, regenerate the file manifest so future updates correctly detect these as user modifications:
@@ -164,22 +232,22 @@ After reapplying, regenerate the file manifest so future updates correctly detec
# For now, just note which files were modified
```
## Step 5: Cleanup option
## Step 6: Cleanup option
Ask user:
- "Keep patch backups for reference?" → preserve `gsd-local-patches/`
- "Clean up patch backups?" → remove `gsd-local-patches/` directory
## Step 6: Report
## Step 7: Report
```
## Patches Reapplied
| # | File | Status |
|---|------|--------|
| 1 | {file_path} | ✓ Merged |
| 2 | {file_path} | ○ Skipped (already upstream) |
| 3 | {file_path} | ⚠ Conflict resolved |
| # | File | Result | User Changes Preserved |
|---|------|--------|----------------------|
| 1 | {file_path} | Merged | Added step X, modified section Y |
| 2 | {file_path} | Incorporated | Already in upstream v{version} |
| 3 | {file_path} | Conflict resolved | User chose: keep custom section |
{count} file(s) updated. Your local modifications are active again.
```
@@ -187,8 +255,10 @@ Ask user:
</process>
<success_criteria>
- [ ] All backed-up patches processed
- [ ] User modifications merged into new version
- [ ] Conflicts resolved with user input
- [ ] Status reported for each file
- [ ] All backed-up patches processed — zero files left unhandled
- [ ] No file classified as "no custom content" or "SKIP" — every backed-up file is definitionally modified
- [ ] Three-way merge used when pristine baseline available (git history or gsd-pristine/)
- [ ] User modifications identified and merged into new version
- [ ] Conflicts surfaced to user with both versions shown
- [ ] Status reported for each file with summary of what was preserved
</success_criteria>

View File

@@ -0,0 +1,35 @@
---
name: gsd:secure-phase
description: Retroactively verify threat mitigations for a completed phase
argument-hint: "[phase number]"
allowed-tools:
- Read
- Write
- Edit
- Bash
- Glob
- Grep
- Task
- AskUserQuestion
---
<objective>
Verify threat mitigations for a completed phase. Three states:
- (A) SECURITY.md exists — audit and verify mitigations
- (B) No SECURITY.md, PLAN.md with threat model exists — run from artifacts
- (C) Phase not executed — exit with guidance
Output: updated SECURITY.md.
</objective>
<execution_context>
@~/.claude/get-shit-done/workflows/secure-phase.md
</execution_context>
<context>
Phase: $ARGUMENTS — optional, defaults to last completed phase.
</context>
<process>
Execute @~/.claude/get-shit-done/workflows/secure-phase.md.
Preserve all workflow gates.
</process>

View File

@@ -62,7 +62,7 @@ Create a new thread:
1. Generate slug from description:
```bash
SLUG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" generate-slug "$ARGUMENTS")
SLUG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" generate-slug "$ARGUMENTS" --raw)
```
2. Create the threads directory if needed:

View File

@@ -811,6 +811,7 @@ Cross-AI peer review of phase plans from external AI CLIs.
| `--gemini` | Include Gemini CLI review |
| `--claude` | Include Claude CLI review (separate session) |
| `--codex` | Include Codex CLI review |
| `--coderabbit` | Include CodeRabbit review |
| `--all` | Include all available CLIs |
**Produces:** `{phase}-REVIEWS.md` — consumable by `/gsd:plan-phase --reviews`

View File

@@ -33,7 +33,8 @@ GSD stores project settings in `.planning/config.json`. Created during `/gsd:new
"research_before_questions": false,
"discuss_mode": "discuss",
"skip_discuss": false,
"text_mode": false
"text_mode": false,
"use_worktrees": true
},
"hooks": {
"context_warnings": true,
@@ -67,6 +68,10 @@ GSD stores project settings in `.planning/config.json`. Created during `/gsd:new
"always_confirm_destructive": true,
"always_confirm_external_services": true
},
"project_code": null,
"security_enforcement": true,
"security_asvs_level": 1,
"security_block_on": "high",
"agent_skills": {}
}
```
@@ -80,6 +85,7 @@ GSD stores project settings in `.planning/config.json`. Created during `/gsd:new
| `mode` | enum | `interactive`, `yolo` | `interactive` | `yolo` auto-approves decisions; `interactive` confirms at each step |
| `granularity` | enum | `coarse`, `standard`, `fine` | `standard` | Controls phase count: `coarse` (3-5), `standard` (5-8), `fine` (8-12) |
| `model_profile` | enum | `quality`, `balanced`, `budget`, `inherit` | `balanced` | Model tier for each agent (see [Model Profiles](#model-profiles)) |
| `project_code` | string | any short string | (none) | Prefix for phase directory names (e.g., `"ABC"` produces `ABC-01-setup/`). Added in v1.31 |
> **Note:** `granularity` was renamed from `depth` in v1.22.3. Existing configs are auto-migrated.
@@ -104,6 +110,7 @@ All workflow toggles follow the **absent = enabled** pattern. If a key is missin
| `workflow.discuss_mode` | string | `'discuss'` | Controls how `/gsd:discuss-phase` gathers context. `'discuss'` (default) asks questions one-by-one. `'assumptions'` reads the codebase first, generates structured assumptions with confidence levels, and only asks you to correct what's wrong. Added in v1.28 |
| `workflow.skip_discuss` | boolean | `false` | When `true`, `/gsd:autonomous` bypasses the discuss-phase entirely, writing minimal CONTEXT.md from the ROADMAP phase goal. Useful for projects where developer preferences are fully captured in PROJECT.md/REQUIREMENTS.md. Added in v1.28 |
| `workflow.text_mode` | boolean | `false` | Replaces AskUserQuestion TUI menus with plain-text numbered lists. Required for Claude Code remote sessions (`/rc` mode) where TUI menus don't render. Can also be set per-session with `--text` flag on discuss-phase. Added in v1.28 |
| `workflow.use_worktrees` | boolean | `true` | When `false`, disables git worktree isolation for parallel execution. Users who prefer sequential execution or whose environment does not support worktrees can disable this. Added in v1.31 |
### Recommended Presets
@@ -299,6 +306,18 @@ Control confirmation prompts during workflows.
---
## Security Settings
Settings for the security enforcement feature (v1.31). All follow the **absent = enabled** pattern.
| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `security_enforcement` | boolean | `true` | Enable threat-model-anchored security verification via `/gsd:secure-phase`. When `false`, security checks are skipped entirely |
| `security_asvs_level` | number (1-3) | `1` | OWASP ASVS verification level. Level 1 = opportunistic, Level 2 = standard, Level 3 = comprehensive |
| `security_block_on` | string | `"high"` | Minimum severity that blocks phase advancement. Options: `"high"`, `"medium"`, `"low"` |
---
## Hook Settings
| Setting | Type | Default | Description |
@@ -397,6 +416,7 @@ The intent is the same as the Claude profile tiers -- use a stronger model for p
| `CLAUDE_CONFIG_DIR` | Override default config directory (`~/.claude/`) |
| `GEMINI_API_KEY` | Detected by context monitor to switch hook event name |
| `WSL_DISTRO_NAME` | Detected by installer for WSL path handling |
| `GSD_SKIP_SCHEMA_CHECK` | Skip schema drift detection during execute-phase (v1.31) |
---

View File

@@ -70,6 +70,22 @@
- [Assumptions Discussion Mode](#53-assumptions-discussion-mode)
- [UI Phase Auto-Detection](#54-ui-phase-auto-detection)
- [Multi-Runtime Installer Selection](#55-multi-runtime-installer-selection)
- [v1.29 Features](#v129-features)
- [Windsurf Runtime Support](#56-windsurf-runtime-support)
- [Internationalized Documentation](#57-internationalized-documentation)
- [v1.30 Features](#v130-features)
- [GSD SDK](#58-gsd-sdk)
- [v1.31 Features](#v131-features)
- [Schema Drift Detection](#59-schema-drift-detection)
- [Security Enforcement](#60-security-enforcement)
- [Documentation Generation](#61-documentation-generation)
- [Discuss Chain Mode](#62-discuss-chain-mode)
- [Single-Phase Autonomous](#63-single-phase-autonomous)
- [Scope Reduction Detection](#64-scope-reduction-detection)
- [Claim Provenance Tagging](#65-claim-provenance-tagging)
- [Worktree Toggle](#66-worktree-toggle)
- [Project Code Prefixing](#67-project-code-prefixing)
- [Claude Code Skills Migration](#68-claude-code-skills-migration)
---
@@ -736,7 +752,7 @@
**Requirements:**
- REQ-TODO-01: System MUST capture todo from current conversation context
- REQ-TODO-02: Todos MUST be stored in `.planning/todos/pending/`
- REQ-TODO-03: Completed todos MUST move to `.planning/todos/done/`
- REQ-TODO-03: Completed todos MUST move to `.planning/todos/completed/`
- REQ-TODO-04: Check-todos MUST list all pending items with selection to work on one
---
@@ -1015,9 +1031,9 @@ When verification returns `human_needed`, items are persisted as a trackable HUM
### 42. Cross-AI Peer Review
**Command:** `/gsd:review --phase N [--gemini] [--claude] [--codex] [--all]`
**Command:** `/gsd:review --phase N [--gemini] [--claude] [--codex] [--coderabbit] [--all]`
**Purpose:** Invoke external AI CLIs (Gemini, Claude, Codex) to independently review phase plans. Produces structured REVIEWS.md with per-reviewer feedback.
**Purpose:** Invoke external AI CLIs (Gemini, Claude, Codex, CodeRabbit) to independently review phase plans. Produces structured REVIEWS.md with per-reviewer feedback.
**Requirements:**
- REQ-REVIEW-01: System MUST detect available AI CLIs on the system
@@ -1288,3 +1304,264 @@ Test suite that scans all agent, workflow, and command files for embedded inject
1. **Detect** — Identify available AI CLI runtimes on the system
2. **Prompt** — Present multi-select interface for runtime selection
3. **Install** — Configure GSD for all selected runtimes in a single session
---
## v1.29 Features
### 56. Windsurf Runtime Support
**Part of:** `npx get-shit-done-cc`
**Purpose:** Add Windsurf as a supported AI CLI runtime for GSD installation and execution.
**Requirements:**
- REQ-WINDSURF-01: Installer MUST detect Windsurf runtime and offer it as a target
- REQ-WINDSURF-02: GSD commands MUST function correctly within Windsurf sessions
**Process:**
1. **Detect** — Identify Windsurf runtime availability on the system
2. **Install** — Configure GSD skills and hooks for the Windsurf environment
---
### 57. Internationalized Documentation
**Part of:** `docs/`
**Purpose:** Provide GSD documentation in Portuguese, Korean, and Japanese.
**Requirements:**
- REQ-I18N-01: Documentation MUST be available in Portuguese (pt), Korean (ko), and Japanese (ja)
- REQ-I18N-02: Translations MUST stay synchronized with English source documents
**Process:**
1. **Translate** — Convert core documentation into target languages
2. **Publish** — Make translated documentation accessible alongside English originals
---
## v1.30 Features
### 58. GSD SDK
**Command:** Programmatic API (headless)
**Purpose:** Headless TypeScript SDK for running GSD workflows programmatically without a CLI session.
**Requirements:**
- REQ-SDK-01: SDK MUST expose GSD workflow operations as TypeScript functions
- REQ-SDK-02: SDK MUST support headless execution without interactive prompts
- REQ-SDK-03: SDK MUST produce the same artifacts as CLI-driven workflows
**Process:**
1. **Import** — Import GSD SDK into a TypeScript/JavaScript project
2. **Configure** — Set project path and workflow options programmatically
3. **Execute** — Run GSD phases (discuss, plan, execute) via API calls
---
## v1.31 Features
### 59. Schema Drift Detection
**Command:** Automatic during `/gsd:execute-phase`
**Purpose:** Detect when ORM schema files are modified without corresponding migration or push commands, preventing false-positive verification.
**Requirements:**
- REQ-SCHEMA-01: System MUST detect modifications to ORM schema files (Prisma, Drizzle, Payload, Sanity, Mongoose)
- REQ-SCHEMA-02: System MUST verify corresponding migration/push commands exist when schema changes are detected
- REQ-SCHEMA-03: System MUST implement two-layer defense: plan-time injection and execute-time gate
- REQ-SCHEMA-04: System MUST support `GSD_SKIP_SCHEMA_CHECK` env var to override detection
- REQ-SCHEMA-05: System MUST prevent false-positive verification when schema is modified without migration
**Process:**
1. **Detect** — Monitor ORM schema file modifications during plan execution
2. **Verify** — Check that corresponding migration/push commands are present in the plan
3. **Gate** — Block execution if schema drift is detected without migration (execute-time gate)
4. **Inject** — Add migration reminders during plan generation (plan-time injection)
**Config:** `GSD_SKIP_SCHEMA_CHECK` environment variable to bypass detection.
---
### 60. Security Enforcement
**Command:** `/gsd:secure-phase <N>`
**Purpose:** Threat-model-anchored security verification for phase implementations.
**Requirements:**
- REQ-SEC-01: System MUST perform threat-model-anchored verification (not blind scanning)
- REQ-SEC-02: System MUST support configurable OWASP ASVS verification levels (1-3)
- REQ-SEC-03: System MUST block phase advancement based on configurable severity threshold
- REQ-SEC-04: System MUST spawn `gsd-security-auditor` agent for analysis
**Produces:**
| Artifact | Description |
|----------|-------------|
| Security audit report | Threat-model-anchored findings with severity classification |
**Process:**
1. **Model** — Build threat model from phase implementation context
2. **Audit** — Spawn `gsd-security-auditor` to verify against threat model
3. **Gate** — Block phase advancement if findings meet or exceed `security_block_on` severity
**Config:**
| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `security_enforcement` | boolean | `true` | Enable threat-model security verification |
| `security_asvs_level` | number (1-3) | `1` | OWASP ASVS verification level |
| `security_block_on` | string | `"high"` | Minimum severity to block phase advancement |
---
### 61. Documentation Generation
**Command:** `/gsd:docs-update`
**Purpose:** Generate and verify project documentation with accuracy checks.
**Requirements:**
- REQ-DOCS-01: System MUST spawn `gsd-doc-writer` agent to generate documentation
- REQ-DOCS-02: System MUST spawn `gsd-doc-verifier` agent to check accuracy
- REQ-DOCS-03: System MUST verify generated documentation against actual implementation
**Produces:**
| Artifact | Description |
|----------|-------------|
| Updated project documentation | Generated and verified documentation files |
**Process:**
1. **Generate** — Spawn `gsd-doc-writer` to create or update documentation from implementation
2. **Verify** — Spawn `gsd-doc-verifier` to check documentation accuracy against codebase
3. **Output** — Produce verified documentation with accuracy annotations
---
### 62. Discuss Chain Mode
**Flag:** `/gsd:discuss-phase <N> --chain`
**Purpose:** Auto-chain discuss, plan, and execute phases in one flow to reduce manual command sequencing.
**Requirements:**
- REQ-CHAIN-01: System MUST auto-chain discuss → plan → execute when `--chain` flag is provided
- REQ-CHAIN-02: System MUST respect all gate settings between chained phases
- REQ-CHAIN-03: System MUST halt the chain if any phase fails
**Process:**
1. **Discuss** — Run discuss-phase to gather context
2. **Plan** — Automatically invoke plan-phase with gathered context
3. **Execute** — Automatically invoke execute-phase with generated plan
---
### 63. Single-Phase Autonomous
**Flag:** `/gsd:autonomous --only N`
**Purpose:** Execute just one phase autonomously instead of all remaining phases.
**Requirements:**
- REQ-ONLY-01: System MUST execute only the specified phase number when `--only N` is provided
- REQ-ONLY-02: System MUST follow the same discuss → plan → execute flow as full autonomous mode
- REQ-ONLY-03: System MUST stop after the specified phase completes
**Process:**
1. **Select** — Identify the target phase from `--only N` argument
2. **Execute** — Run full autonomous flow (discuss → plan → execute) for that single phase
3. **Stop** — Halt after the phase completes instead of advancing to the next
---
### 64. Scope Reduction Detection
**Part of:** `/gsd:plan-phase`
**Purpose:** Prevent silent requirement dropping during plan generation with three-layer defense.
**Requirements:**
- REQ-SCOPE-01: System MUST prohibit planners from reducing scope without explicit justification
- REQ-SCOPE-02: System MUST have plan-checker verify requirement dimension coverage
- REQ-SCOPE-03: System MUST have orchestrator recover dropped requirements and re-inject them
- REQ-SCOPE-04: System MUST implement three-layer defense: planner prohibition, checker dimension, orchestrator recovery
**Process:**
1. **Prohibit** — Planner instructions explicitly forbid scope reduction
2. **Check** — Plan-checker verifies all phase requirements are covered in the plan
3. **Recover** — Orchestrator detects dropped requirements and re-injects them into the planning loop
---
### 65. Claim Provenance Tagging
**Part of:** `/gsd:research-phase`
**Purpose:** Ensure research claims are tagged with source evidence and assumptions are logged separately.
**Requirements:**
- REQ-PROVENANCE-01: Researcher MUST mark claims with source evidence references
- REQ-PROVENANCE-02: Assumptions MUST be logged separately from sourced claims
- REQ-PROVENANCE-03: System MUST distinguish between evidenced facts and inferred assumptions
**Process:**
1. **Research** — Researcher gathers information from codebase and domain sources
2. **Tag** — Each claim is annotated with its source (file path, documentation, API response)
3. **Separate** — Assumptions without direct evidence are logged in a distinct section
---
### 66. Worktree Toggle
**Config:** `workflow.use_worktrees: false`
**Purpose:** Disable git worktree isolation for users who prefer sequential execution.
**Requirements:**
- REQ-WORKTREE-01: System MUST respect `workflow.use_worktrees` setting when deciding isolation strategy
- REQ-WORKTREE-02: System MUST default to `true` (worktrees enabled) for backward compatibility
- REQ-WORKTREE-03: System MUST fall back to sequential execution when worktrees are disabled
**Config:**
| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `workflow.use_worktrees` | boolean | `true` | When `false`, disables git worktree isolation |
---
### 67. Project Code Prefixing
**Config:** `project_code: "ABC"`
**Purpose:** Prefix phase directory names with a project code for multi-project disambiguation.
**Requirements:**
- REQ-PREFIX-01: System MUST prefix phase directories with project code when configured (e.g., `ABC-01-setup/`)
- REQ-PREFIX-02: System MUST use standard naming when `project_code` is not set
- REQ-PREFIX-03: System MUST apply prefix consistently across all phase operations
**Config:**
| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `project_code` | string | (none) | Prefix for phase directory names |
---
### 68. Claude Code Skills Migration
**Part of:** `npx get-shit-done-cc`
**Purpose:** Migrate GSD commands to Claude Code 2.1.88+ skills format with backward compatibility.
**Requirements:**
- REQ-SKILLS-01: Installer MUST write `skills/gsd-*/SKILL.md` for Claude Code 2.1.88+
- REQ-SKILLS-02: Installer MUST auto-clean legacy `commands/gsd/` directory
- REQ-SKILLS-03: Installer MUST maintain backward compatibility with older Claude Code versions via Gemini path
**Process:**
1. **Detect** — Check Claude Code version to determine skills support
2. **Migrate** — Write `skills/gsd-*/SKILL.md` files for each GSD command
3. **Clean** — Remove legacy `commands/gsd/` directory if skills are installed
4. **Fallback** — Maintain Gemini path compatibility for older Claude Code versions

View File

@@ -811,6 +811,7 @@ GSDアップデート後にローカルの変更を復元します。
| `--gemini` | Gemini CLIレビューを含める |
| `--claude` | Claude CLIレビューを含める(別セッション) |
| `--codex` | Codex CLIレビューを含める |
| `--coderabbit` | CodeRabbitレビューを含める |
| `--all` | 利用可能なすべてのCLIを含める |
**生成物:** `{phase}-REVIEWS.md` — `/gsd:plan-phase --reviews` で利用可能

View File

@@ -736,7 +736,7 @@
**要件:**
- REQ-TODO-01: システムは現在の会話コンテキストから Todo をキャプチャしなければならない
- REQ-TODO-02: Todo は `.planning/todos/pending/` に保存されなければならない
- REQ-TODO-03: 完了した Todo は `.planning/todos/done/` に移動されなければならない
- REQ-TODO-03: 完了した Todo は `.planning/todos/completed/` に移動されなければならない
- REQ-TODO-04: check-todos は保留中のすべてのアイテムを一覧表示し、作業するアイテムを選択できなければならない
---
@@ -1015,9 +1015,9 @@ fix(03-01): correct auth token expiry
### 42. クロス AI ピアレビュー
**コマンド:** `/gsd:review --phase N [--gemini] [--claude] [--codex] [--all]`
**コマンド:** `/gsd:review --phase N [--gemini] [--claude] [--codex] [--coderabbit] [--all]`
**目的:** 外部の AI CLI(Gemini、Claude、Codex)を呼び出して、フェーズプランを独立してレビューします。レビュアーごとのフィードバックを含む構造化された REVIEWS.md を生成します。
**目的:** 外部の AI CLI(Gemini、Claude、Codex、CodeRabbit)を呼び出して、フェーズプランを独立してレビューします。レビュアーごとのフィードバックを含む構造化された REVIEWS.md を生成します。
**要件:**
- REQ-REVIEW-01: システムはシステム上で利用可能な AI CLI を検出しなければならない

View File

@@ -811,6 +811,7 @@ GSD 업데이트 후 로컬 수정사항을 복원합니다.
| `--gemini` | Gemini CLI 리뷰 포함 |
| `--claude` | Claude CLI 리뷰 포함 (별도 세션) |
| `--codex` | Codex CLI 리뷰 포함 |
| `--coderabbit` | CodeRabbit 리뷰 포함 |
| `--all` | 사용 가능한 모든 CLI 포함 |
**생성 파일:** `{phase}-REVIEWS.md` — `/gsd:plan-phase --reviews`에서 사용 가능

View File

@@ -736,7 +736,7 @@
**요구사항.**
- REQ-TODO-01: 현재 대화 컨텍스트에서 할 일을 캡처해야 합니다.
- REQ-TODO-02: 할 일은 `.planning/todos/pending/`에 저장되어야 합니다.
- REQ-TODO-03: 완료된 할 일은 `.planning/todos/done/`으로 이동해야 합니다.
- REQ-TODO-03: 완료된 할 일은 `.planning/todos/completed/`으로 이동해야 합니다.
- REQ-TODO-04: check-todos는 모든 보류 항목을 나열하고 하나를 선택하여 작업할 수 있어야 합니다.
---
@@ -1015,9 +1015,9 @@ fix(03-01): correct auth token expiry
### 42. Cross-AI Peer Review
**명령어:** `/gsd:review --phase N [--gemini] [--claude] [--codex] [--all]`
**명령어:** `/gsd:review --phase N [--gemini] [--claude] [--codex] [--coderabbit] [--all]`
**목적:** 외부 AI CLI(Gemini, Claude, Codex)를 호출하여 페이즈 계획을 독립적으로 검토합니다. 검토자별 피드백이 담긴 구조화된 REVIEWS.md를 생성합니다.
**목적:** 외부 AI CLI(Gemini, Claude, Codex, CodeRabbit)를 호출하여 페이즈 계획을 독립적으로 검토합니다. 검토자별 피드백이 담긴 구조화된 REVIEWS.md를 생성합니다.
**요구사항.**
- REQ-REVIEW-01: 시스템에서 사용 가능한 AI CLI를 감지해야 합니다.

View File

@@ -71,6 +71,8 @@ GSD 解决了这个问题。它是让 Claude Code 变得可靠的上下文工程
想要描述需求然后正确构建出来的人 —— 不用假装自己在运营一个 50 人的工程组织。
内置的质量门禁能捕获真正的问题:模式漂移检测会标记缺少迁移的 ORM 变更,安全强制将验证锚定到威胁模型,范围缩减检测防止规划器默默丢弃你的需求。
---
## 快速开始
@@ -349,7 +351,7 @@ claude --dangerously-skip-permissions
循环 **讨论 → 规划 → 执行 → 验证** 直到里程碑完成。
如果你想在讨论期间更快速地输入,使用 `/gsd:discuss-phase <n> --batch` 一次回答一组小问题,而不是一个一个来。
如果你想在讨论期间更快速地输入,使用 `/gsd:discuss-phase <n> --batch` 一次回答一组小问题,而不是一个一个来。使用 `--chain` 可以自动链式执行从讨论到规划+执行,中间不停顿。
每个阶段都会获得你的输入(讨论)、适当的研究(规划)、干净的执行(执行)和人工验证(验证)。上下文保持新鲜。质量保持高水平。
@@ -370,10 +372,18 @@ claude --dangerously-skip-permissions
快速模式给你 GSD 保证(原子提交、状态跟踪)和更快的路径:
- **相同代理** —— 规划者 + 执行者,相同质量
- **跳过可选步骤** —— 无研究、无计划检查器、无验证器
- **跳过可选步骤** —— 默认无研究、无计划检查器、无验证器
- **独立跟踪** —— 存放在 `.planning/quick/`,不是阶段
用于:bug 修复、小功能、配置更改、一次性任务。
**`--discuss` 标志:** 规划前的轻量讨论,发现灰色地带。
**`--research` 标志:** 规划前启动聚焦研究员。调查实现方法、库选项和陷阱。当你不确定如何处理任务时使用。
**`--full` 标志:** 启用所有阶段 —— 讨论 + 研究 + 计划检查 + 验证。快速任务形式的完整 GSD 管道。
**`--validate` 标志:** 仅启用计划检查 + 执行后验证(之前 `--full` 的行为)。
标志可组合:`--discuss --research --validate` 提供讨论 + 研究 + 计划检查 + 验证。
```
/gsd:quick
@@ -474,7 +484,7 @@ lmn012o feat(08-02): 创建注册端点
| 命令 | 作用 |
|---------|--------------|
| `/gsd:new-project [--auto]` | 完整初始化:提问 → 研究 → 需求 → 路线图 |
| `/gsd:discuss-phase [N] [--auto]` | 在规划前捕获实现决策 |
| `/gsd:discuss-phase [N] [--auto] [--chain]` | 在规划前捕获实现决策(`--chain` 自动链式执行规划+执行) |
| `/gsd:plan-phase [N] [--auto]` | 阶段的研究 + 规划 + 验证 |
| `/gsd:execute-phase <N>` | 在并行波次中执行所有计划,完成后验证 |
| `/gsd:verify-work [N]` | 手动用户验收测试 ¹ |
@@ -523,7 +533,7 @@ lmn012o feat(08-02): 创建注册端点
| `/gsd:add-todo [desc]` | 捕获想法留待后用 |
| `/gsd:check-todos` | 列出待处理事项 |
| `/gsd:debug [desc]` | 带持久状态的系统化调试 |
| `/gsd:quick [--full] [--discuss]` | 用 GSD 保证执行临时任务(`--full` 添加计划检查和验证,`--discuss` 先收集上下文) |
| `/gsd:quick [--full] [--discuss] [--research]` | 用 GSD 保证执行临时任务(`--full` 启用全部阶段,`--discuss` 先收集上下文,`--research` 规划前调查方法) |
| `/gsd:health [--repair]` | 验证 `.planning/` 目录完整性,用 `--repair` 自动修复 |
<sup>¹ 由 Reddit 用户 OracleGreyBeard 贡献</sup>

View File

@@ -93,6 +93,7 @@
* verify commits <h1> [h2] ... Batch verify commit hashes
* verify artifacts <plan-file> Check must_haves.artifacts
* verify key-links <plan-file> Check must_haves.key_links
* verify schema-drift <phase> [--skip] Detect schema file changes without push
*
* Template Fill:
* template fill summary --phase N Create pre-filled SUMMARY.md
@@ -133,6 +134,9 @@
* init milestone-op All context for milestone operations
* init map-codebase All context for map-codebase workflow
* init progress All context for progress workflow
*
* Documentation:
* docs-init Project context for docs-update workflow
*/
const fs = require('fs');
@@ -152,6 +156,7 @@ const frontmatter = require('./lib/frontmatter.cjs');
const profilePipeline = require('./lib/profile-pipeline.cjs');
const profileOutput = require('./lib/profile-output.cjs');
const workstream = require('./lib/workstream.cjs');
const docs = require('./lib/docs.cjs');
// ─── Arg parsing helpers ──────────────────────────────────────────────────────
@@ -274,7 +279,7 @@ async function main() {
const command = args[0];
if (!command) {
error('Usage: gsd-tools <command> [args] [--raw] [--pick <field>] [--cwd <path>] [--ws <name>]\nCommands: state, resolve-model, find-phase, commit, verify-summary, verify, frontmatter, template, generate-slug, current-timestamp, list-todos, verify-path-exists, config-ensure-section, config-new-project, init, workstream');
error('Usage: gsd-tools <command> [args] [--raw] [--pick <field>] [--cwd <path>] [--ws <name>]\nCommands: state, resolve-model, find-phase, commit, verify-summary, verify, frontmatter, template, generate-slug, current-timestamp, list-todos, verify-path-exists, config-ensure-section, config-new-project, init, workstream, docs-init');
}
// Multi-repo guard: resolve project root for commands that read/write .planning/.
@@ -498,8 +503,11 @@ async function runCommand(command, args, cwd, raw) {
verify.cmdVerifyArtifacts(cwd, args[2], raw);
} else if (subcommand === 'key-links') {
verify.cmdVerifyKeyLinks(cwd, args[2], raw);
} else if (subcommand === 'schema-drift') {
const skipFlag = args.includes('--skip');
verify.cmdVerifySchemaDrift(cwd, args[2], skipFlag, raw);
} else {
error('Unknown verify subcommand. Available: plan-structure, phase-completeness, references, commits, artifacts, key-links');
error('Unknown verify subcommand. Available: plan-structure, phase-completeness, references, commits, artifacts, key-links, schema-drift');
}
break;
}
@@ -910,6 +918,13 @@ async function runCommand(command, args, cwd, raw) {
break;
}
// ─── Documentation ────────────────────────────────────────────────────
case 'docs-init': {
docs.cmdDocsInit(cwd, raw);
break;
}
default:
error(`Unknown command: ${command}`);
}

View File

@@ -8,6 +8,33 @@ const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, com
const { extractFrontmatter } = require('./frontmatter.cjs');
const { MODEL_PROFILES } = require('./model-profiles.cjs');
/**
* Determine phase status by checking plan/summary counts AND verification state.
* Introduces "Executed" for phases with all summaries but no passing verification.
*/
function determinePhaseStatus(plans, summaries, phaseDir, defaultPending) {
if (plans === 0) return defaultPending;
if (summaries < plans && summaries > 0) return 'In Progress';
if (summaries < plans) return 'Planned';
// summaries >= plans — check verification
try {
const files = fs.readdirSync(phaseDir);
const verificationFile = files.find(f => f === 'VERIFICATION.md' || f.endsWith('-VERIFICATION.md'));
if (verificationFile) {
const content = fs.readFileSync(path.join(phaseDir, verificationFile), 'utf-8');
if (/status:\s*passed/i.test(content)) return 'Complete';
if (/status:\s*human_needed/i.test(content)) return 'Needs Review';
if (/status:\s*gaps_found/i.test(content)) return 'Executed';
// Verification exists but unrecognized status — treat as executed
return 'Executed';
}
} catch { /* directory read failed — fall through */ }
// No verification file — executed but not verified
return 'Executed';
}
function cmdGenerateSlug(text, raw) {
if (!text) {
error('text required for slug generation');
@@ -16,7 +43,8 @@ function cmdGenerateSlug(text, raw) {
const slug = text
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
.replace(/^-+|-+$/g, '')
.substring(0, 60);
const result = { slug };
output(result, raw, slug);
@@ -254,7 +282,7 @@ function cmdCommit(cwd, message, files, raw, amend, noVerify) {
let branchName = null;
if (config.branching_strategy === 'phase') {
// Determine which phase we're committing for from the file paths
const phaseMatch = (files || []).join(' ').match(/(\d+)-/);
const phaseMatch = (files || []).join(' ').match(/(\d+(?:\.\d+)*)-/);
if (phaseMatch) {
const phaseNum = phaseMatch[1];
const phaseInfo = findPhaseInternal(cwd, phaseNum);
@@ -528,11 +556,7 @@ function cmdProgressRender(cwd, format, raw) {
totalPlans += plans;
totalSummaries += summaries;
let status;
if (plans === 0) status = 'Pending';
else if (summaries >= plans) status = 'Complete';
else if (summaries > 0) status = 'In Progress';
else status = 'Planned';
const status = determinePhaseStatus(plans, summaries, path.join(phasesDir, dir), 'Pending');
phases.push({ number: phaseNum, name: phaseName, plans, summaries, status });
}
@@ -828,18 +852,14 @@ function cmdStats(cwd, format, raw) {
totalPlans += plans;
totalSummaries += summaries;
let status;
if (plans === 0) status = 'Not Started';
else if (summaries >= plans) status = 'Complete';
else if (summaries > 0) status = 'In Progress';
else status = 'Planned';
const status = determinePhaseStatus(plans, summaries, path.join(phasesDir, dir), 'Not Started');
const existing = phasesByNumber.get(phaseNum);
phasesByNumber.set(phaseNum, {
number: phaseNum,
name: existing?.name || phaseName,
plans,
summaries,
plans: (existing?.plans || 0) + plans,
summaries: (existing?.summaries || 0) + summaries,
status,
});
}

View File

@@ -22,9 +22,11 @@ const VALID_CONFIG_KEYS = new Set([
'workflow.discuss_mode',
'workflow.skip_discuss',
'workflow._auto_chain_active',
'workflow.use_worktrees',
'git.branching_strategy', 'git.phase_branch_template', 'git.milestone_branch_template', 'git.quick_branch_template',
'planning.commit_docs', 'planning.search_gitignored',
'hooks.context_warnings',
'project_code', 'phase_naming',
]);
/**
@@ -132,6 +134,8 @@ function buildNewProjectConfig(userChoices) {
hooks: {
context_warnings: true,
},
project_code: null,
phase_naming: 'sequential',
agent_skills: {},
};

View File

@@ -217,6 +217,7 @@ function loadConfig(cwd) {
resolve_model_ids: false, // false: return alias as-is | true: map to full Claude model ID | "omit": return '' (runtime uses its default)
context_window: 200000, // default 200k; set to 1000000 for Opus/Sonnet 4.6 1M models
phase_naming: 'sequential', // 'sequential' (default, auto-increment) or 'custom' (arbitrary string IDs)
project_code: null, // optional short prefix for phase dirs (e.g., 'CK' → 'CK-01-foundation')
};
try {
@@ -308,6 +309,7 @@ function loadConfig(cwd) {
resolve_model_ids: get('resolve_model_ids') ?? defaults.resolve_model_ids,
context_window: get('context_window') ?? defaults.context_window,
phase_naming: get('phase_naming') ?? defaults.phase_naming,
project_code: get('project_code') ?? defaults.project_code,
model_overrides: parsed.model_overrides || null,
agent_skills: parsed.agent_skills || {},
};
@@ -619,8 +621,10 @@ function escapeRegex(value) {
function normalizePhaseName(phase) {
const str = String(phase);
// Strip optional project_code prefix (e.g., 'CK-01' → '01')
const stripped = str.replace(/^[A-Z]{1,6}-(?=\d)/, '');
// Standard numeric phases: 1, 01, 12A, 12.1
const match = str.match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
const match = stripped.match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
if (match) {
const padded = match[1].padStart(2, '0');
const letter = match[2] ? match[2].toUpperCase() : '';
@@ -632,8 +636,11 @@ function normalizePhaseName(phase) {
}
function comparePhaseNum(a, b) {
const pa = String(a).match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
const pb = String(b).match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
// Strip optional project_code prefix before comparing (e.g., 'CK-01-name' → '01-name')
const sa = String(a).replace(/^[A-Z]{1,6}-/, '');
const sb = String(b).replace(/^[A-Z]{1,6}-/, '');
const pa = sa.match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
const pb = sb.match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
// If either is non-numeric (custom ID), fall back to string comparison
if (!pa || !pb) return String(a).localeCompare(String(b));
const intDiff = parseInt(pa[1], 10) - parseInt(pb[1], 10);
@@ -668,12 +675,17 @@ function searchPhaseInDir(baseDir, relBase, normalized) {
if (d.startsWith(normalized)) return true;
// For custom IDs like PROJ-42, match case-insensitively
if (d.toUpperCase().startsWith(normalized.toUpperCase())) return true;
// Strip optional project_code prefix (e.g., 'CK-01-name' → '01-name') and retry
const stripped = d.replace(/^[A-Z]{1,6}-/, '');
if (stripped.startsWith(normalized)) return true;
if (stripped.toUpperCase().startsWith(normalized.toUpperCase())) return true;
return false;
});
if (!match) return null;
// Extract phase number and name — supports both numeric (01-name) and custom (PROJ-42-name)
const dirMatch = match.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i)
// Extract phase number and name — supports numeric (01-name), project-code-prefixed (CK-01-name), and custom (PROJ-42-name)
const dirMatch = match.match(/^(?:[A-Z]{1,6}-)(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i)
|| match.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i)
|| match.match(/^([A-Z][A-Z0-9]*(?:-[A-Z0-9]+)*)-(.+)/i)
|| [null, match, null];
const phaseNumber = dirMatch ? dirMatch[1] : normalized;
@@ -1061,7 +1073,7 @@ function pathExistsInternal(cwd, targetPath) {
function generateSlugInternal(text) {
if (!text) return null;
return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').substring(0, 60);
}
function getMilestoneInfo(cwd) {

View File

@@ -0,0 +1,267 @@
/**
* Docs — Commands for the docs-update workflow
*
* Provides `cmdDocsInit` which returns project signals, existing doc inventory
* with GSD marker detection, doc tooling detection, monorepo awareness, and
* model resolution. Used by Phase 2 to route doc generation appropriately.
*/
const fs = require('fs');
const path = require('path');
const { output, loadConfig, resolveModelInternal, pathExistsInternal, toPosixPath, checkAgentsInstalled } = require('./core.cjs');
// ─── Constants ────────────────────────────────────────────────────────────────
const GSD_MARKER = '<!-- generated-by: gsd-doc-writer -->';
const SKIP_DIRS = new Set([
'node_modules', '.git', '.planning', '.claude', '__pycache__',
'target', 'dist', 'build', '.next', '.nuxt', 'coverage',
'.vscode', '.idea',
]);
// ─── Private helpers ──────────────────────────────────────────────────────────
/**
* Check whether a file begins with the GSD doc writer marker.
* Reads the first 500 bytes only — avoids loading large files.
*
* @param {string} filePath - Absolute path to the file
* @returns {boolean}
*/
function hasGsdMarker(filePath) {
try {
const buf = Buffer.alloc(500);
const fd = fs.openSync(filePath, 'r');
const bytesRead = fs.readSync(fd, buf, 0, 500, 0);
fs.closeSync(fd);
return buf.slice(0, bytesRead).toString('utf-8').includes(GSD_MARKER);
} catch {
return false;
}
}
/**
* Recursively scan the project root (immediate .md files) and docs/ directory
* (up to 4 levels deep) for Markdown files, excluding dirs in SKIP_DIRS.
*
* @param {string} cwd - Project root
* @returns {Array<{path: string, has_gsd_marker: boolean}>}
*/
function scanExistingDocs(cwd) {
const MAX_DEPTH = 4;
const results = [];
/**
* Recursively walk a directory for .md files up to MAX_DEPTH levels.
* @param {string} dir - Directory to scan
* @param {number} depth - Current depth (1-based)
*/
function walkDir(dir, depth) {
if (depth > MAX_DEPTH) return;
try {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (SKIP_DIRS.has(entry.name)) continue;
const abs = path.join(dir, entry.name);
if (entry.isDirectory()) {
walkDir(abs, depth + 1);
} else if (entry.isFile() && entry.name.toLowerCase().endsWith('.md')) {
const rel = toPosixPath(path.relative(cwd, abs));
results.push({ path: rel, has_gsd_marker: hasGsdMarker(abs) });
}
}
} catch { /* directory may not exist — best-effort */ }
}
// Scan root-level .md files (non-recursive)
try {
const entries = fs.readdirSync(cwd, { withFileTypes: true });
for (const entry of entries) {
if (entry.isFile() && entry.name.toLowerCase().endsWith('.md')) {
const abs = path.join(cwd, entry.name);
const rel = toPosixPath(path.relative(cwd, abs));
results.push({ path: rel, has_gsd_marker: hasGsdMarker(abs) });
}
}
} catch { /* best-effort */ }
// Recursively scan docs/ directory
const docsDir = path.join(cwd, 'docs');
walkDir(docsDir, 1);
// Fallback: if docs/ does not exist, try documentation/ or doc/
try {
fs.statSync(docsDir);
} catch {
const alternatives = ['documentation', 'doc'];
for (const alt of alternatives) {
const altDir = path.join(cwd, alt);
try {
const stat = fs.statSync(altDir);
if (stat.isDirectory()) {
walkDir(altDir, 1);
break;
}
} catch { /* not present */ }
}
}
return results.sort((a, b) => a.path.localeCompare(b.path));
}
/**
* Detect project type signals from the filesystem and package.json.
* All checks are best-effort and never throw.
*
* @param {string} cwd - Project root
* @returns {Object} Boolean signal fields
*/
function detectProjectType(cwd) {
const exists = (rel) => {
try { return pathExistsInternal(cwd, rel); } catch { return false; }
};
// has_cli_bin: package.json has a `bin` field
let has_cli_bin = false;
try {
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf-8'));
has_cli_bin = !!(pkg.bin && (typeof pkg.bin === 'string' || Object.keys(pkg.bin).length > 0));
} catch { /* no package.json or invalid JSON */ }
// is_monorepo: pnpm-workspace.yaml, lerna.json, or package.json workspaces
let is_monorepo = exists('pnpm-workspace.yaml') || exists('lerna.json');
if (!is_monorepo) {
try {
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf-8'));
is_monorepo = Array.isArray(pkg.workspaces) && pkg.workspaces.length > 0;
} catch { /* ignore */ }
}
// has_tests: common test directories or test frameworks in devDependencies
let has_tests = exists('test') || exists('tests') || exists('__tests__') || exists('spec');
if (!has_tests) {
try {
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf-8'));
const devDeps = Object.keys(pkg.devDependencies || {});
has_tests = devDeps.some(d => ['vitest', 'jest', 'mocha', 'jasmine', 'ava'].includes(d));
} catch { /* ignore */ }
}
// has_deploy_config: various deployment config files
const deployFiles = [
'Dockerfile', 'docker-compose.yml', 'docker-compose.yaml',
'fly.toml', 'render.yaml', 'vercel.json', 'netlify.toml', 'railway.json',
'.github/workflows/deploy.yml', '.github/workflows/deploy.yaml',
];
const has_deploy_config = deployFiles.some(f => exists(f));
return {
has_package_json: exists('package.json'),
has_api_routes: (
exists('src/app/api') || exists('routes') || exists('src/routes') ||
exists('api') || exists('server')
),
has_cli_bin,
is_open_source: exists('LICENSE') || exists('LICENSE.md'),
has_deploy_config,
is_monorepo,
has_tests,
};
}
/**
* Detect known documentation tooling in the project.
*
* @param {string} cwd - Project root
* @returns {Object} Boolean detection fields
*/
function detectDocTooling(cwd) {
const exists = (rel) => {
try { return pathExistsInternal(cwd, rel); } catch { return false; }
};
return {
docusaurus: exists('docusaurus.config.js') || exists('docusaurus.config.ts'),
vitepress: (
exists('.vitepress/config.js') ||
exists('.vitepress/config.ts') ||
exists('.vitepress/config.mts')
),
mkdocs: exists('mkdocs.yml'),
storybook: exists('.storybook'),
};
}
/**
* Extract monorepo workspace globs from pnpm-workspace.yaml, package.json
* workspaces, or lerna.json.
*
* @param {string} cwd - Project root
* @returns {string[]} Array of workspace glob patterns, or [] if not a monorepo
*/
function detectMonorepoWorkspaces(cwd) {
// pnpm-workspace.yaml
try {
const content = fs.readFileSync(path.join(cwd, 'pnpm-workspace.yaml'), 'utf-8');
const lines = content.split('\n');
const workspaces = [];
for (const line of lines) {
const m = line.match(/^\s*-\s+['"]?(.+?)['"]?\s*$/);
if (m) workspaces.push(m[1].trim());
}
if (workspaces.length > 0) return workspaces;
} catch { /* not present */ }
// package.json workspaces
try {
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf-8'));
if (Array.isArray(pkg.workspaces) && pkg.workspaces.length > 0) {
return pkg.workspaces;
}
} catch { /* not present or invalid */ }
// lerna.json
try {
const lerna = JSON.parse(fs.readFileSync(path.join(cwd, 'lerna.json'), 'utf-8'));
if (Array.isArray(lerna.packages) && lerna.packages.length > 0) {
return lerna.packages;
}
} catch { /* not present or invalid */ }
return [];
}
// ─── Public commands ──────────────────────────────────────────────────────────
/**
* Return JSON context for the docs-update workflow: project signals, existing
* doc inventory, doc tooling detection, monorepo workspaces, and model
* resolution. Follows the cmdInitMapCodebase pattern.
*
* @example
* node gsd-tools.cjs docs-init --raw
*
* @param {string} cwd - Project root directory
* @param {boolean} raw - Pass raw JSON flag through to output()
*/
function cmdDocsInit(cwd, raw) {
const config = loadConfig(cwd);
const result = {
doc_writer_model: resolveModelInternal(cwd, 'gsd-doc-writer'),
commit_docs: config.commit_docs,
existing_docs: scanExistingDocs(cwd),
project_type: detectProjectType(cwd),
doc_tooling: detectDocTooling(cwd),
monorepo_workspaces: detectMonorepoWorkspaces(cwd),
planning_exists: pathExistsInternal(cwd, '.planning'),
};
// Inject project_root and agent installation status (mirrors withProjectRoot in init.cjs)
result.project_root = cwd;
const agentStatus = checkAgentsInstalled();
result.agents_installed = agentStatus.agents_installed;
result.missing_agents = agentStatus.missing_agents;
output(result, raw);
}
module.exports = { cmdDocsInit };

View File

@@ -108,6 +108,7 @@ function cmdInitExecutePhase(cwd, phase, raw) {
// Branch name (pre-computed)
branch_name: config.branching_strategy === 'phase' && phaseInfo
? config.phase_branch_template
.replace('{project}', config.project_code || '')
.replace('{phase}', phaseInfo.phase_number)
.replace('{slug}', phaseInfo.phase_slug || 'phase')
: config.branching_strategy === 'milestone'

View File

@@ -22,6 +22,8 @@ const MODEL_PROFILES = {
'gsd-ui-researcher': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
'gsd-ui-checker': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
'gsd-ui-auditor': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
'gsd-doc-writer': { quality: 'opus', balanced: 'sonnet', budget: 'haiku' },
'gsd-doc-verifier': { quality: 'sonnet', balanced: 'sonnet', budget: 'haiku' },
};
const VALID_PROFILES = Object.keys(MODEL_PROFILES['gsd-planner']);

View File

@@ -163,13 +163,22 @@ function cmdFindPhase(cwd, phase, raw) {
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
const match = dirs.find(d => d.startsWith(normalized));
const match = dirs.find(d => {
if (d.startsWith(normalized)) return true;
if (d.toUpperCase().startsWith(normalized.toUpperCase())) return true;
// Strip optional project_code prefix (e.g., 'CK-01-name' → '01-name') and retry
const stripped = d.replace(/^[A-Z]{1,6}-/, '');
if (stripped.startsWith(normalized)) return true;
return false;
});
if (!match) {
output(notFound, raw, '');
return;
}
const dirMatch = match.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
// Extract phase number — supports project-code-prefixed (CK-01-name), numeric (01-name), and custom IDs
const dirMatch = match.match(/^(?:[A-Z]{1,6}-)(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i)
|| match.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
const phaseNumber = dirMatch ? dirMatch[1] : normalized;
const phaseName = dirMatch && dirMatch[2] ? dirMatch[2] : null;
@@ -326,11 +335,15 @@ function cmdPhaseAdd(cwd, description, raw, customId) {
let newPhaseId;
let dirName;
// Optional project code prefix (e.g., 'CK' → 'CK-01-foundation')
const projectCode = config.project_code || '';
const prefix = projectCode ? `${projectCode}-` : '';
if (customId || config.phase_naming === 'custom') {
// Custom phase naming: use provided ID or generate from description
newPhaseId = customId || slug.toUpperCase().replace(/-/g, '-');
if (!newPhaseId) error('--id required when phase_naming is "custom"');
dirName = `${newPhaseId}-${slug}`;
dirName = `${prefix}${newPhaseId}-${slug}`;
} else {
// Sequential mode: find highest integer phase number (in current milestone only)
const phasePattern = /#{2,4}\s*Phase\s+(\d+)[A-Z]?(?:\.\d+)*:/gi;
@@ -343,7 +356,7 @@ function cmdPhaseAdd(cwd, description, raw, customId) {
newPhaseId = maxPhase + 1;
const paddedNum = String(newPhaseId).padStart(2, '0');
dirName = `${paddedNum}-${slug}`;
dirName = `${prefix}${paddedNum}-${slug}`;
}
const dirPath = path.join(planningDir(cwd), 'phases', dirName);
@@ -410,7 +423,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
try {
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
const decimalPattern = new RegExp(`^${normalizedBase}\\.(\\d+)`);
const decimalPattern = new RegExp(`^(?:[A-Z]{1,6}-)?${normalizedBase}\\.(\\d+)`);
for (const dir of dirs) {
const dm = dir.match(decimalPattern);
if (dm) existingDecimals.push(parseInt(dm[1], 10));
@@ -419,7 +432,12 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
const nextDecimal = existingDecimals.length === 0 ? 1 : Math.max(...existingDecimals) + 1;
const decimalPhase = `${normalizedBase}.${nextDecimal}`;
const dirName = `${decimalPhase}-${slug}`;
// Optional project code prefix
const config = loadConfig(cwd);
const projectCode = config.project_code || '';
const prefix = projectCode ? `${projectCode}-` : '';
const dirName = `${prefix}${decimalPhase}-${slug}`;
const dirPath = path.join(planningDir(cwd), 'phases', dirName);
// Create directory with .gitkeep so git tracks empty folders
@@ -689,10 +707,12 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
const cells = fullRow.split('|').slice(1, -1);
if (cells.length === 5) {
// 5-col: Phase | Milestone | Plans | Status | Completed
cells[2] = ` ${summaryCount}/${planCount} `;
cells[3] = ' Complete ';
cells[4] = ` ${today} `;
} else if (cells.length === 4) {
// 4-col: Phase | Plans | Status | Completed
cells[1] = ` ${summaryCount}/${planCount} `;
cells[2] = ' Complete ';
cells[3] = ` ${today} `;
}
@@ -709,6 +729,18 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
`$1${summaryCount}/${planCount} plans complete`
);
// Mark completed plan checkboxes (safety net for missed per-plan updates)
for (const summaryFile of phaseInfo.summaries) {
const planId = summaryFile.replace('-SUMMARY.md', '').replace('SUMMARY.md', '');
if (!planId) continue;
const planEscaped = escapeRegex(planId);
const planCheckboxPattern = new RegExp(
`(-\\s*\\[) (\\]\\s*${planEscaped})`,
'i'
);
roadmapContent = roadmapContent.replace(planCheckboxPattern, '$1x$2');
}
fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8');
// Update REQUIREMENTS.md traceability for this phase's requirements

View File

@@ -177,8 +177,12 @@ const CLAUDE_MD_FALLBACKS = {
stack: 'Technology stack not yet documented. Will populate after codebase mapping or first phase.',
conventions: 'Conventions not yet established. Will populate as patterns emerge during development.',
architecture: 'Architecture not yet mapped. Follow existing patterns found in the codebase.',
skills: 'No project skills found. Add skills to any of: `.claude/skills/`, `.agents/skills/`, `.cursor/skills/`, or `.github/skills/` with a `SKILL.md` index file.',
};
// Directories where project skills may live (checked in order)
const SKILL_SEARCH_DIRS = ['.claude/skills', '.agents/skills', '.cursor/skills', '.github/skills'];
const CLAUDE_MD_WORKFLOW_ENFORCEMENT = [
'Before using Edit, Write, or other file-changing tools, start work through a GSD command so planning artifacts and execution context stay in sync.',
'',
@@ -375,6 +379,96 @@ function generateWorkflowSection() {
};
}
/**
* Discover project skills from standard directories and extract frontmatter
* (name + description) for each. Returns a table summary for CLAUDE.md so
* agents know which skills are available at session startup (Layer 1 discovery).
*/
function generateSkillsSection(cwd) {
const discovered = [];
for (const dir of SKILL_SEARCH_DIRS) {
const absDir = path.join(cwd, dir);
if (!fs.existsSync(absDir)) continue;
let entries;
try {
entries = fs.readdirSync(absDir, { withFileTypes: true });
} catch {
continue;
}
for (const entry of entries) {
if (!entry.isDirectory()) continue;
// Skip GSD's own installed skills — only surface project-specific skills
if (entry.name.startsWith('gsd-')) continue;
const skillMdPath = path.join(absDir, entry.name, 'SKILL.md');
if (!fs.existsSync(skillMdPath)) continue;
const content = safeReadFile(skillMdPath);
if (!content) continue;
const frontmatter = extractSkillFrontmatter(content);
const name = frontmatter.name || entry.name;
const description = frontmatter.description || '';
// Avoid duplicates when same skill dir is symlinked from multiple locations
if (discovered.some(s => s.name === name)) continue;
discovered.push({ name, description, path: `${dir}/${entry.name}` });
}
}
if (discovered.length === 0) {
return { content: CLAUDE_MD_FALLBACKS.skills, source: 'skills/', hasFallback: true };
}
const lines = ['| Skill | Description | Path |', '|-------|-------------|------|'];
for (const skill of discovered) {
// Sanitize table cell content (escape pipes)
const desc = skill.description.replace(/\|/g, '\\|').replace(/\n/g, ' ').trim();
const safeName = skill.name.replace(/\|/g, '\\|');
lines.push(`| ${safeName} | ${desc} | \`${skill.path}/SKILL.md\` |`);
}
return { content: lines.join('\n'), source: 'skills/', hasFallback: false };
}
/**
* Extract name and description from YAML-like frontmatter in a SKILL.md file.
* Handles multi-line description values (continuation lines indented with spaces).
*/
function extractSkillFrontmatter(content) {
const result = { name: '', description: '' };
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
if (!fmMatch) return result;
const fmBlock = fmMatch[1];
const lines = fmBlock.split('\n');
let currentKey = '';
for (const line of lines) {
// Top-level key: value
const kvMatch = line.match(/^(\w[\w-]*):\s*(.*)/);
if (kvMatch) {
currentKey = kvMatch[1];
const value = kvMatch[2].trim();
if (currentKey === 'name') result.name = value;
if (currentKey === 'description') result.description = value;
continue;
}
// Continuation line (indented) for multi-line values
if (currentKey === 'description' && /^\s+/.test(line)) {
result.description += ' ' + line.trim();
} else {
currentKey = '';
}
}
return result;
}
// ─── Commands ─────────────────────────────────────────────────────────────────
function cmdWriteProfile(cwd, options, raw) {
@@ -815,12 +909,13 @@ function cmdGenerateClaudeProfile(cwd, options, raw) {
}
function cmdGenerateClaudeMd(cwd, options, raw) {
const MANAGED_SECTIONS = ['project', 'stack', 'conventions', 'architecture', 'workflow'];
const MANAGED_SECTIONS = ['project', 'stack', 'conventions', 'architecture', 'skills', 'workflow'];
const generators = {
project: generateProjectSection,
stack: generateStackSection,
conventions: generateConventionsSection,
architecture: generateArchitectureSection,
skills: generateSkillsSection,
workflow: generateWorkflowSection,
};
const sectionHeadings = {
@@ -828,6 +923,7 @@ function cmdGenerateClaudeMd(cwd, options, raw) {
stack: '## Technology Stack',
conventions: '## Conventions',
architecture: '## Architecture',
skills: '## Project Skills',
workflow: '## GSD Workflow Enforcement',
};

View File

@@ -0,0 +1,238 @@
/**
* Schema Drift Detection — Detects schema-relevant file changes and verifies
* that the appropriate database push command was executed during a phase.
*
* Prevents false-positive verification when schema files change but no push
* occurs — TypeScript types come from config, not the live database, so
* build/types pass on a broken state.
*/
'use strict';
// ─── ORM Patterns ────────────────────────────────────────────────────────────
//
// Each entry maps a glob-like pattern to an ORM name. Patterns use forward
// slashes internally — Windows backslash paths are normalized before matching.
const SCHEMA_PATTERNS = [
// Payload CMS
{ pattern: /^src\/collections\/.*\.ts$/, orm: 'payload' },
{ pattern: /^src\/globals\/.*\.ts$/, orm: 'payload' },
// Prisma
{ pattern: /^prisma\/schema\.prisma$/, orm: 'prisma' },
{ pattern: /^prisma\/schema\/.*\.prisma$/, orm: 'prisma' },
// Drizzle
{ pattern: /^drizzle\/schema\.ts$/, orm: 'drizzle' },
{ pattern: /^src\/db\/schema\.ts$/, orm: 'drizzle' },
{ pattern: /^drizzle\/.*\.ts$/, orm: 'drizzle' },
// Supabase
{ pattern: /^supabase\/migrations\/.*\.sql$/, orm: 'supabase' },
// TypeORM
{ pattern: /^src\/entities\/.*\.ts$/, orm: 'typeorm' },
{ pattern: /^src\/migrations\/.*\.ts$/, orm: 'typeorm' },
];
// ─── Push Commands & Evidence Patterns ───────────────────────────────────────
//
// For each ORM, the push command that agents should run, plus regex patterns
// that indicate the push was actually executed (matched against execution logs,
// SUMMARY.md content, and git commit messages).
const ORM_INFO = {
payload: {
pushCommand: 'npx payload migrate',
envHint: 'CI=true PAYLOAD_MIGRATING=true npx payload migrate',
interactiveWarning: 'Payload migrate may require interactive prompts — use CI=true PAYLOAD_MIGRATING=true to suppress',
evidencePatterns: [
/payload\s+migrate/i,
/PAYLOAD_MIGRATING/,
],
},
prisma: {
pushCommand: 'npx prisma db push',
envHint: 'npx prisma db push --accept-data-loss (if destructive changes are intended)',
interactiveWarning: 'Prisma db push may prompt for confirmation on destructive changes — use --accept-data-loss to bypass',
evidencePatterns: [
/prisma\s+db\s+push/i,
/prisma\s+migrate\s+deploy/i,
/prisma\s+migrate\s+dev/i,
],
},
drizzle: {
pushCommand: 'npx drizzle-kit push',
envHint: 'npx drizzle-kit push',
interactiveWarning: null,
evidencePatterns: [
/drizzle-kit\s+push/i,
/drizzle-kit\s+migrate/i,
],
},
supabase: {
pushCommand: 'supabase db push',
envHint: 'supabase db push',
interactiveWarning: 'Supabase db push may require authentication — ensure SUPABASE_ACCESS_TOKEN is set',
evidencePatterns: [
/supabase\s+db\s+push/i,
/supabase\s+migration\s+up/i,
],
},
typeorm: {
pushCommand: 'npx typeorm migration:run',
envHint: 'npx typeorm migration:run -d src/data-source.ts',
interactiveWarning: null,
evidencePatterns: [
/typeorm\s+migration:run/i,
/typeorm\s+schema:sync/i,
],
},
};
// ─── Public API ──────────────────────────────────────────────────────────────
/**
* Detect schema-relevant files in a list of file paths.
*
* @param {string[]} files - List of file paths (relative to project root)
* @returns {{ detected: boolean, matches: string[], orms: string[] }}
*/
function detectSchemaFiles(files) {
const matches = [];
const orms = new Set();
for (const rawFile of files) {
// Normalize Windows backslash paths
const file = rawFile.replace(/\\/g, '/');
for (const { pattern, orm } of SCHEMA_PATTERNS) {
if (pattern.test(file)) {
matches.push(rawFile);
orms.add(orm);
break; // One match per file is enough
}
}
}
return {
detected: matches.length > 0,
matches,
orms: Array.from(orms),
};
}
/**
* Get ORM-specific push command info.
*
* @param {string} ormName - ORM identifier (payload, prisma, drizzle, supabase, typeorm)
* @returns {{ pushCommand: string, envHint: string, interactiveWarning: string|null, evidencePatterns: RegExp[] } | null}
*/
function detectSchemaOrm(ormName) {
return ORM_INFO[ormName] || null;
}
/**
* Check for schema drift: schema files changed but no push evidence found.
*
* @param {string[]} changedFiles - Files changed during the phase
* @param {string} executionLog - Combined text from SUMMARY.md, commit messages, and execution logs
* @param {{ skipCheck?: boolean }} [options] - Options
* @returns {{ driftDetected: boolean, blocking: boolean, schemaFiles: string[], orms: string[], unpushedOrms: string[], message: string, skipped?: boolean }}
*/
function checkSchemaDrift(changedFiles, executionLog, options = {}) {
const { skipCheck = false } = options;
const detection = detectSchemaFiles(changedFiles);
if (!detection.detected) {
return {
driftDetected: false,
blocking: false,
schemaFiles: [],
orms: [],
unpushedOrms: [],
message: '',
};
}
// Check which ORMs have push evidence in the execution log
const pushedOrms = new Set();
const unpushedOrms = [];
for (const orm of detection.orms) {
const info = ORM_INFO[orm];
if (!info) continue;
const hasPushEvidence = info.evidencePatterns.some(p => p.test(executionLog));
if (hasPushEvidence) {
pushedOrms.add(orm);
} else {
unpushedOrms.push(orm);
}
}
const driftDetected = unpushedOrms.length > 0;
if (!driftDetected) {
return {
driftDetected: false,
blocking: false,
schemaFiles: detection.matches,
orms: detection.orms,
unpushedOrms: [],
message: '',
};
}
// Build actionable message
const pushCommands = unpushedOrms
.map(orm => {
const info = ORM_INFO[orm];
return info ? ` ${orm}: ${info.envHint || info.pushCommand}` : null;
})
.filter(Boolean)
.join('\n');
const message = [
'Schema drift detected: schema-relevant files changed but no database push was executed.',
'',
`Schema files changed: ${detection.matches.join(', ')}`,
`ORMs requiring push: ${unpushedOrms.join(', ')}`,
'',
'Required push commands:',
pushCommands,
'',
'Run the appropriate push command, or set GSD_SKIP_SCHEMA_CHECK=true to bypass this gate.',
].join('\n');
if (skipCheck) {
return {
driftDetected: true,
blocking: false,
skipped: true,
schemaFiles: detection.matches,
orms: detection.orms,
unpushedOrms,
message: 'Schema drift detected but check was skipped (GSD_SKIP_SCHEMA_CHECK=true).',
};
}
return {
driftDetected: true,
blocking: true,
schemaFiles: detection.matches,
orms: detection.orms,
unpushedOrms,
message,
};
}
module.exports = {
SCHEMA_PATTERNS,
ORM_INFO,
detectSchemaFiles,
detectSchemaOrm,
checkSchemaDrift,
};

View File

@@ -874,6 +874,84 @@ function cmdValidateAgents(cwd, raw) {
}, raw);
}
// ─── Schema Drift Detection ──────────────────────────────────────────────────
function cmdVerifySchemaDrift(cwd, phaseArg, skipFlag, raw) {
const { detectSchemaFiles, checkSchemaDrift } = require('./schema-detect.cjs');
if (!phaseArg) {
error('Usage: verify schema-drift <phase> [--skip]');
return;
}
// Find phase directory
const pDir = planningDir(cwd);
const phasesDir = path.join(pDir, 'phases');
if (!fs.existsSync(phasesDir)) {
output({ drift_detected: false, blocking: false, message: 'No phases directory' }, raw);
return;
}
// Find matching phase directory
let phaseDir = null;
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && entry.name.includes(phaseArg)) {
phaseDir = path.join(phasesDir, entry.name);
break;
}
}
// Also try exact match
if (!phaseDir) {
const exact = path.join(phasesDir, phaseArg);
if (fs.existsSync(exact)) phaseDir = exact;
}
if (!phaseDir) {
output({ drift_detected: false, blocking: false, message: `Phase directory not found: ${phaseArg}` }, raw);
return;
}
// Collect files_modified from all PLAN.md files in the phase
const allFiles = [];
const planFiles = fs.readdirSync(phaseDir).filter(f => f.endsWith('-PLAN.md'));
for (const pf of planFiles) {
const content = fs.readFileSync(path.join(phaseDir, pf), 'utf-8');
// Extract files_modified from frontmatter
const fmMatch = content.match(/files_modified:\s*\[([^\]]*)\]/);
if (fmMatch) {
const files = fmMatch[1].split(',').map(f => f.trim()).filter(Boolean);
allFiles.push(...files);
}
}
// Collect execution log from SUMMARY.md files
let executionLog = '';
const summaryFiles = fs.readdirSync(phaseDir).filter(f => f.endsWith('-SUMMARY.md'));
for (const sf of summaryFiles) {
executionLog += fs.readFileSync(path.join(phaseDir, sf), 'utf-8') + '\n';
}
// Also check git commit messages for push evidence
const gitLog = execGit(cwd, ['log', '--oneline', '--all', '-50']);
if (gitLog.exitCode === 0) {
executionLog += '\n' + gitLog.stdout;
}
const result = checkSchemaDrift(allFiles, executionLog, { skipCheck: !!skipFlag });
output({
drift_detected: result.driftDetected,
blocking: result.blocking,
schema_files: result.schemaFiles,
orms: result.orms,
unpushed_orms: result.unpushedOrms,
message: result.message,
skipped: result.skipped || false,
}, raw);
}
module.exports = {
cmdVerifySummary,
cmdVerifyPlanStructure,
@@ -885,4 +963,5 @@ module.exports = {
cmdValidateConsistency,
cmdValidateHealth,
cmdValidateAgents,
cmdVerifySchemaDrift,
};

View File

@@ -339,9 +339,13 @@ function cmdWorkstreamComplete(cwd, name, options, raw) {
// ─── Active Workstream Commands ──────────────────────────────────────────────
function cmdWorkstreamSet(cwd, name, raw) {
if (!name) {
if (!name || name === '--clear') {
if (name !== '--clear') {
error('Workstream name required. Usage: workstream set <name> (or workstream set --clear to unset)');
}
const previous = getActiveWorkstream(cwd);
setActiveWorkstream(cwd, null);
output({ active: null, cleared: true }, raw);
output({ active: null, cleared: true, previous: previous || null }, raw);
return;
}

View File

@@ -1,63 +0,0 @@
---
description: Manage parallel workstreams — list, create, switch, status, progress, complete, and resume
---
# /gsd:workstreams
Manage parallel workstreams for concurrent milestone work.
## Usage
`/gsd:workstreams [subcommand] [args]`
### Subcommands
| Command | Description |
|---------|-------------|
| `list` | List all workstreams with status |
| `create <name>` | Create a new workstream |
| `status <name>` | Detailed status for one workstream |
| `switch <name>` | Set active workstream |
| `progress` | Progress summary across all workstreams |
| `complete <name>` | Archive a completed workstream |
| `resume <name>` | Resume work in a workstream |
## Step 1: Parse Subcommand
Parse the user's input to determine which workstream operation to perform.
If no subcommand given, default to `list`.
## Step 2: Execute Operation
### list
Run: `node "$GSD_TOOLS" workstream list --raw --cwd "$CWD"`
Display the workstreams in a table format showing name, status, current phase, and progress.
### create
Run: `node "$GSD_TOOLS" workstream create <name> --raw --cwd "$CWD"`
After creation, display the new workstream path and suggest next steps:
- `/gsd:new-milestone --ws <name>` to set up the milestone
### status
Run: `node "$GSD_TOOLS" workstream status <name> --raw --cwd "$CWD"`
Display detailed phase breakdown and state information.
### switch
Run: `node "$GSD_TOOLS" workstream set <name> --raw --cwd "$CWD"`
Also set `GSD_WORKSTREAM` env var for the current session.
### progress
Run: `node "$GSD_TOOLS" workstream progress --raw --cwd "$CWD"`
Display a progress overview across all workstreams.
### complete
Run: `node "$GSD_TOOLS" workstream complete <name> --raw --cwd "$CWD"`
Archive the workstream to milestones/.
### resume
Set the workstream as active and suggest `/gsd:resume-work --ws <name>`.
## Step 3: Display Results
Format the JSON output from gsd-tools into a human-readable display.
Include the `${GSD_WS}` flag in any routing suggestions.

View File

@@ -24,6 +24,7 @@ Configuration options for `.planning/` directory behavior.
| `git.phase_branch_template` | `"gsd/phase-{phase}-{slug}"` | Branch template for phase strategy |
| `git.milestone_branch_template` | `"gsd/{milestone}-{slug}"` | Branch template for milestone strategy |
| `git.quick_branch_template` | `null` | Optional branch template for quick-task runs |
| `workflow.use_worktrees` | `true` | Whether executor agents run in isolated git worktrees. Set to `false` to disable worktrees — agents execute sequentially on the main working tree instead. Recommended for solo developers or when worktree merges cause issues. |
</config_schema>
<commit_docs_behavior>

View File

@@ -0,0 +1,61 @@
---
phase: {N}
slug: {phase-slug}
status: draft
threats_open: 0
asvs_level: 1
created: {date}
---
# Phase {N} — Security
> Per-phase security contract: threat register, accepted risks, and audit trail.
---
## Trust Boundaries
| Boundary | Description | Data Crossing |
|----------|-------------|---------------|
| {boundary} | {description} | {data type / sensitivity} |
---
## Threat Register
| Threat ID | Category | Component | Disposition | Mitigation | Status |
|-----------|----------|-----------|-------------|------------|--------|
| T-{N}-01 | {STRIDE category} | {component} | {mitigate / accept / transfer} | {control or reference} | open |
*Status: open · closed*
*Disposition: mitigate (implementation required) · accept (documented risk) · transfer (third-party)*
---
## Accepted Risks Log
| Risk ID | Threat Ref | Rationale | Accepted By | Date |
|---------|------------|-----------|-------------|------|
*Accepted risks do not resurface in future audit runs.*
*If none: "No accepted risks."*
---
## Security Audit Trail
| Audit Date | Threats Total | Closed | Open | Run By |
|------------|---------------|--------|------|--------|
| {YYYY-MM-DD} | {N} | {N} | {N} | {name / agent} |
---
## Sign-Off
- [ ] All threats have a disposition (mitigate / accept / transfer)
- [ ] Accepted risks documented in Accepted Risks Log
- [ ] `threats_open: 0` confirmed
- [ ] `status: verified` set in frontmatter
**Approval:** {pending / verified YYYY-MM-DD}

View File

@@ -36,9 +36,9 @@ created: {date}
## Per-Task Verification Map
| Task ID | Plan | Wave | Requirement | Test Type | Automated Command | File Exists | Status |
|---------|------|------|-------------|-----------|-------------------|-------------|--------|
| {N}-01-01 | 01 | 1 | REQ-{XX} | unit | `{command}` | ✅ / ❌ W0 | ⬜ pending |
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
| {N}-01-01 | 01 | 1 | REQ-{XX} | T-{N}-01 / — | {expected secure behavior or "N/A"} | unit | `{command}` | ✅ / ❌ W0 | ⬜ pending |
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*

View File

@@ -2,8 +2,8 @@
Template for project-root `CLAUDE.md` — auto-generated by `gsd-tools generate-claude-md`.
Contains 6 marker-bounded sections. Each section is independently updatable.
The `generate-claude-md` subcommand manages 5 sections (project, stack, conventions, architecture, workflow enforcement).
Contains 7 marker-bounded sections. Each section is independently updatable.
The `generate-claude-md` subcommand manages 6 sections (project, stack, conventions, architecture, skills, workflow enforcement).
The profile section is managed exclusively by `generate-claude-profile`.
---
@@ -66,6 +66,28 @@ Conventions not yet established. Will populate as patterns emerge during develop
Architecture not yet mapped. Follow existing patterns found in the codebase.
```
### Skills Section
```
<!-- GSD:skills-start source:skills/ -->
## Project Skills
| Skill | Description | Path |
| -------------- | --------------------- | ------------------------- |
| {{skill_name}} | {{skill_description}} | `{{skill_path}}/SKILL.md` |
<!-- GSD:skills-end -->
```
**Fallback text:**
```
No project skills found. Add skills to any of: `.claude/skills/`, `.agents/skills/`, `.cursor/skills/`, or `.github/skills/` with a `SKILL.md` index file.
```
**Discovery behavior:**
- Scans `.claude/skills/`, `.agents/skills/`, `.cursor/skills/`, `.github/skills/` for subdirectories containing `SKILL.md`
- Extracts `name` and `description` from YAML frontmatter (supports multi-line descriptions)
- Skips GSD's own installed skills (directories starting with `gsd-`)
- Deduplicates by skill name across directories
### Workflow Enforcement Section
```
<!-- GSD:workflow-start source:GSD defaults -->
@@ -104,8 +126,9 @@ CLAUDE.md file and no profile section exists yet.
2. **Stack** — Technology choices (what tools are used)
3. **Conventions** — Code patterns and rules (how code is written)
4. **Architecture** — System structure (how components fit together)
5. **Workflow Enforcement** — Default GSD entry points for file-changing work
6. **Profile** — Developer behavioral preferences (how to interact)
5. **Skills** — Discovered project skills with name and description (what domain knowledge is available)
6. **Workflow Enforcement** — Default GSD entry points for file-changing work
7. **Profile** — Developer behavioral preferences (how to interact)
## Marker Format

View File

@@ -7,6 +7,9 @@
"verifier": true,
"auto_advance": false,
"nyquist_validation": true,
"security_enforcement": true,
"security_asvs_level": 1,
"security_block_on": "high",
"discuss_mode": "discuss",
"research_before_questions": false
},
@@ -40,5 +43,6 @@
"hooks": {
"context_warnings": true
},
"project_code": null,
"agent_skills": {}
}

View File

@@ -20,7 +20,7 @@ Extract from init JSON: `commit_docs`, `date`, `timestamp`, `todo_count`, `todos
Ensure directories exist:
```bash
mkdir -p .planning/todos/pending .planning/todos/done
mkdir -p .planning/todos/pending .planning/todos/completed
```
Note existing areas from the todos array for consistency in infer_area step.

View File

@@ -1,6 +1,6 @@
<purpose>
Drive all remaining milestone phases autonomously. For each incomplete phase: discuss → plan → execute using Skill() flat invocations. Pauses only for explicit user decisions (grey area acceptance, blockers, validation requests). Re-reads ROADMAP.md after each phase to catch dynamically inserted phases.
Drive milestone phases autonomously — all remaining phases, or a single phase via `--only N`. For each incomplete phase: discuss → plan → execute using Skill() flat invocations. Pauses only for explicit user decisions (grey area acceptance, blockers, validation requests). Re-reads ROADMAP.md after each phase to catch dynamically inserted phases.
</purpose>
@@ -16,15 +16,23 @@ Read all files referenced by the invoking prompt's execution_context before star
## 1. Initialize
Parse `$ARGUMENTS` for `--from N` flag:
Parse `$ARGUMENTS` for `--from N` and `--only N` flags:
```bash
FROM_PHASE=""
if echo "$ARGUMENTS" | grep -qE '\-\-from\s+[0-9]'; then
FROM_PHASE=$(echo "$ARGUMENTS" | grep -oE '\-\-from\s+[0-9]+\.?[0-9]*' | awk '{print $2}')
fi
ONLY_PHASE=""
if echo "$ARGUMENTS" | grep -qE '\-\-only\s+[0-9]'; then
ONLY_PHASE=$(echo "$ARGUMENTS" | grep -oE '\-\-only\s+[0-9]+\.?[0-9]*' | awk '{print $2}')
FROM_PHASE="$ONLY_PHASE"
fi
```
When `--only` is set, also set `FROM_PHASE` to the same value so existing filter logic applies.
Bootstrap via milestone-level init:
```bash
@@ -48,7 +56,8 @@ Display startup banner:
Phases: {phase_count} total, {completed_phases} complete
```
If `FROM_PHASE` is set, display: `Starting from phase ${FROM_PHASE}`
If `ONLY_PHASE` is set, display: `Single phase mode: Phase ${ONLY_PHASE}`
Else if `FROM_PHASE` is set, display: `Starting from phase ${FROM_PHASE}`
</step>
@@ -68,6 +77,16 @@ Parse the JSON `phases` array.
**Apply `--from N` filter:** If `FROM_PHASE` was provided, additionally filter out phases where `number < FROM_PHASE` (use numeric comparison — handles decimal phases like "5.1").
**Apply `--only N` filter:** If `ONLY_PHASE` was provided, additionally filter OUT phases where `number != ONLY_PHASE`. This means the phase list will contain exactly one phase (or zero if already complete).
**If `ONLY_PHASE` is set and no phases remain** (phase already complete):
```
Phase ${ONLY_PHASE} is already complete. Nothing to do.
```
Exit cleanly.
**Sort by `number`** in numeric ascending order.
**If no incomplete phases remain:**
@@ -117,7 +136,9 @@ For the current phase, display the progress banner:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
Where N = current phase number (from the ROADMAP, e.g., 6), T = total milestone phases (from `phase_count` parsed in initialize step, e.g., 8), P = percentage of all milestone phases completed so far. Calculate P as: (number of phases with `disk_status` "complete" from the latest `roadmap analyze` / T × 100). Use █ for filled and ░ for empty segments in the progress bar (8 characters wide).
Where N = current phase number (from the ROADMAP, e.g., 63), T = total milestone phases (from `phase_count` parsed in initialize step, e.g., 67). **Important:** T must be `phase_count` (the total number of phases in this milestone), NOT the count of remaining/incomplete phases. When phases are numbered 61-67, T=7 and the banner should read `Phase 63/7` (phase 63, 7 total in milestone), not `Phase 63/3` (which would confuse 3 remaining with 3 total). P = percentage of all milestone phases completed so far. Calculate P as: (number of phases with `disk_status` "complete" from the latest `roadmap analyze` / T × 100). Use █ for filled and ░ for empty segments in the progress bar (8 characters wide).
**Alternative display when phase numbers exceed total** (e.g., multi-milestone projects where phases are numbered globally): If N > T (phase number exceeds milestone phase count), use the format `Phase {N} ({position}/{T})` where `position` is the 1-based index of this phase among incomplete phases being processed. This prevents confusing displays like "Phase 63/5".
**3a. Smart Discuss**
@@ -211,6 +232,9 @@ Proceed to 3b.
**If SKIP_DISCUSS is `false` (or unset):** Execute the smart_discuss step for this phase.
**IMPORTANT — Discuss must be single-pass in autonomous mode.**
The discuss step in `--auto` mode MUST NOT loop. If CONTEXT.md already exists after discuss completes, do NOT re-invoke discuss for the same phase. The `has_context` check below is authoritative — once true, discuss is done for this phase regardless of perceived "gaps" in the context file.
After smart_discuss completes, verify context was written:
```bash
@@ -686,7 +710,9 @@ Decisions captured: {count} across {area_count} areas
## 4. Iterate
After each phase completes, re-read ROADMAP.md to catch phases inserted mid-execution (decimal phases like 5.1):
**If `ONLY_PHASE` is set:** Do not iterate. Proceed directly to lifecycle step (which exits cleanly per single-phase mode).
**Otherwise:** After each phase completes, re-read ROADMAP.md to catch phases inserted mid-execution (decimal phases like 5.1):
```bash
ROADMAP=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" roadmap analyze)
@@ -715,7 +741,23 @@ If all phases complete, proceed to lifecycle step.
## 5. Lifecycle
After all phases complete, run the milestone lifecycle sequence: audit → complete → cleanup.
**If `ONLY_PHASE` is set:** Skip lifecycle. A single phase does not trigger audit/complete/cleanup. Display:
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GSD ► AUTONOMOUS ▸ PHASE ${ONLY_PHASE} COMPLETE ✓
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Phase ${ONLY_PHASE}: ${PHASE_NAME} — Done
Mode: Single phase (--only)
Lifecycle skipped — run /gsd:autonomous without --only
after all phases complete to trigger audit/complete/cleanup.
```
Exit cleanly.
**Otherwise:** After all phases complete, run the milestone lifecycle sequence: audit → complete → cleanup.
Display lifecycle transition banner:
@@ -852,7 +894,7 @@ When any phase operation fails or a blocker is detected, present 3 options via A
Skipped: {list of skipped phases}
Remaining: {list of remaining phases}
Resume with: /gsd:autonomous --from {next_phase}
Resume with: /gsd:autonomous ${ONLY_PHASE ? "--only " + ONLY_PHASE : "--from " + next_phase}
```
</step>
@@ -882,10 +924,15 @@ When any phase operation fails or a blocker is detected, present 3 options via A
- [ ] Complete-milestone invoked via Skill() with ${milestone_version} arg
- [ ] Cleanup invoked via Skill() — internal confirmation is acceptable (CTRL-01)
- [ ] Final completion banner displayed after lifecycle
- [ ] Progress bar uses phase number / total milestone phases (not position among incomplete)
- [ ] Progress bar uses phase number / total milestone phases (not position among incomplete), with fallback display when phase numbers exceed total
- [ ] Smart discuss documents relationship to discuss-phase with CTRL-03 note
- [ ] Frontend phases get UI-SPEC generated before planning (step 3a.5) if not already present
- [ ] Frontend phases get UI review audit after successful execution (step 3d.5) if UI-SPEC exists
- [ ] UI phase and UI review respect workflow.ui_phase and workflow.ui_review config toggles
- [ ] UI review is advisory (non-blocking) — phase proceeds to iterate regardless of score
- [ ] `--only N` restricts execution to exactly one phase
- [ ] `--only N` skips lifecycle step (audit/complete/cleanup)
- [ ] `--only N` exits cleanly after single phase completes
- [ ] `--only N` on already-complete phase exits with message
- [ ] `--only N` handle_blocker resume message uses --only flag
</success_criteria>

View File

@@ -126,7 +126,7 @@ Use AskUserQuestion:
<step name="execute_action">
**Work on it now:**
```bash
mv ".planning/todos/pending/[filename]" ".planning/todos/done/"
mv ".planning/todos/pending/[filename]" ".planning/todos/completed/"
```
Update STATE.md todo count. Present problem/solution context. Begin work or ask how to proceed.
@@ -155,7 +155,7 @@ If todo was moved to done/, commit the change:
```bash
git rm --cached .planning/todos/pending/[filename] 2>/dev/null || true
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: start work on todo - [title]" --files .planning/todos/done/[filename] .planning/STATE.md
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: start work on todo - [title]" --files .planning/todos/completed/[filename] .planning/STATE.md
```
Tool respects `commit_docs` config and gitignore automatically.

View File

@@ -55,6 +55,12 @@ gaps = [
</step>
<step name="report_plan">
**Read worktree config:**
```bash
USE_WORKTREES=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get workflow.use_worktrees 2>/dev/null || echo "true")
```
**Report diagnosis plan to user:**
```
@@ -92,7 +98,7 @@ For each gap, fill the debug-subagent-prompt template and spawn:
Task(
prompt=filled_debug_subagent_prompt + "\n\n<files_to_read>\n- {phase_dir}/{phase_num}-UAT.md\n- .planning/STATE.md\n</files_to_read>\n${AGENT_SKILLS_DEBUGGER}",
subagent_type="gsd-debugger",
isolation="worktree",
${USE_WORKTREES !== "false" ? 'isolation="worktree",' : ''}
description="Debug: {truth_short}"
)
```

View File

@@ -155,6 +155,11 @@ Exit workflow.
- In `discuss_areas`: for each discussion question, choose the recommended option (first option, or the one marked "recommended") without using AskUserQuestion
- Log each auto-selected choice inline so the user can review decisions in the context file
- After discussion completes, auto-advance to plan-phase (existing behavior)
**Chain mode** — If `--chain` is present in ARGUMENTS:
- Discussion is fully interactive (questions, gray area selection — same as default mode)
- After discussion completes, auto-advance to plan-phase → execute-phase (same as `--auto`)
- This is the middle ground: user controls the discuss decisions, then plan+execute run autonomously
</step>
<step name="check_existing">
@@ -182,6 +187,26 @@ If "Skip": Exit workflow
**If doesn't exist:**
**Check for interrupted discussion checkpoint:**
```bash
ls ${phase_dir}/*-DISCUSS-CHECKPOINT.json 2>/dev/null || true
```
If a checkpoint file exists (previous session was interrupted before CONTEXT.md was written):
**If `--auto`:** Auto-select "Resume" — load checkpoint and continue from last completed area.
**Otherwise:** Use AskUserQuestion:
- header: "Resume"
- question: "Found interrupted discussion checkpoint ({N} areas completed out of {M}). Resume from where you left off?"
- options:
- "Resume" — Load checkpoint, skip completed areas, continue discussion
- "Start fresh" — Delete checkpoint, start discussion from scratch
If "Resume": Parse the checkpoint JSON. Load `decisions` into the internal accumulator. Set `areas_completed` to skip those areas. Continue to `present_gray_areas` with only the remaining areas.
If "Start fresh": Delete the checkpoint file. Continue as if no checkpoint existed.
Check `has_plans` and `plan_count` from init. **If `has_plans` is true:**
**If `--auto`:** Auto-select "Continue and replan after". Log: `[auto] Plans exist — continuing with context capture, will replan after.`
@@ -640,6 +665,16 @@ Each answer (or answer set, in batch mode) should reveal the next question or ne
```
After all areas are auto-resolved, skip the "Explore more gray areas" prompt and proceed directly to write_context.
**CRITICAL — Auto-mode pass cap:**
In `--auto` mode, the discuss step MUST complete in a **single pass**. After writing CONTEXT.md once, you are DONE — proceed immediately to write_context and then auto_advance. Do NOT re-read your own CONTEXT.md to find "gaps", "undefined types", or "missing decisions" and run additional passes. This creates a self-feeding loop where each pass generates references that the next pass treats as gaps, consuming unbounded time and resources.
Check the pass cap from config:
```bash
MAX_PASSES=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get workflow.max_discuss_passes 2>/dev/null || echo "3")
```
If you have already written and committed CONTEXT.md, the discuss step is complete. Move on.
**Interactive mode (no `--auto`):**
**For each area:**
@@ -719,6 +754,44 @@ Back to [current area]: [return to current question]"
Track deferred ideas internally.
**Incremental checkpoint — save after each area completes:**
After each area is resolved (user says "Next area" or area auto-resolves in `--auto` mode), immediately write a checkpoint file with all decisions captured so far. This prevents data loss if the session is interrupted mid-discussion.
**Checkpoint file:** `${phase_dir}/${padded_phase}-DISCUSS-CHECKPOINT.json`
Write after each area:
```json
{
"phase": "{PHASE_NUM}",
"phase_name": "{phase_name}",
"timestamp": "{ISO timestamp}",
"areas_completed": ["Area 1", "Area 2"],
"areas_remaining": ["Area 3", "Area 4"],
"decisions": {
"Area 1": [
{"question": "...", "answer": "...", "options_presented": ["..."]},
{"question": "...", "answer": "...", "options_presented": ["..."]}
],
"Area 2": [
{"question": "...", "answer": "...", "options_presented": ["..."]}
]
},
"deferred_ideas": ["..."],
"canonical_refs": ["..."]
}
```
This is a structured checkpoint, not the final CONTEXT.md — the `write_context` step still produces the canonical output. But if the session dies, the next `gsd:discuss-phase` invocation can detect this checkpoint and offer to resume from it instead of starting from scratch.
**On session resume:** In the `check_existing` step, also check for `*-DISCUSS-CHECKPOINT.json`. If found and no CONTEXT.md exists:
- Display: "Found interrupted discussion checkpoint ({N} areas completed). Resume from checkpoint?"
- Options: "Resume" / "Start fresh"
- On "Resume": Load the checkpoint, skip completed areas, continue from where it left off
- On "Start fresh": Delete the checkpoint, proceed as normal
**After write_context completes successfully:** Delete the checkpoint file — the canonical CONTEXT.md now has all decisions.
**Track discussion log data internally:**
For each question asked, accumulate:
- Area name
@@ -880,6 +953,7 @@ Created: .planning/phases/${PADDED_PHASE}-${SLUG}/${PADDED_PHASE}-CONTEXT.md
---
**Also available:**
- `/gsd:discuss-phase ${PHASE} --chain ${GSD_WS}` — re-run with auto plan+execute after
- `/gsd:plan-phase ${PHASE} --skip-research ${GSD_WS}` — plan without research
- `/gsd:ui-phase ${PHASE} ${GSD_WS}` — generate UI design contract before planning (if phase has frontend work)
- Review/edit CONTEXT.md before continuing
@@ -933,6 +1007,12 @@ Created: .planning/phases/${PADDED_PHASE}-${SLUG}/${PADDED_PHASE}-CONTEXT.md
Write file.
**Clean up checkpoint file** — CONTEXT.md is now the canonical record:
```bash
rm -f "${phase_dir}/${padded_phase}-DISCUSS-CHECKPOINT.json"
```
Commit phase context and discussion log:
```bash
@@ -961,10 +1041,10 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(state): record
<step name="auto_advance">
Check for auto-advance trigger:
1. Parse `--auto` flag from $ARGUMENTS
2. **Sync chain flag with intent** — if user invoked manually (no `--auto`), clear the ephemeral chain flag from any previous interrupted `--auto` chain. This does NOT touch `workflow.auto_advance` (the user's persistent settings preference):
1. Parse `--auto` and `--chain` flags from $ARGUMENTS
2. **Sync chain flag with intent** — if user invoked manually (no `--auto` and no `--chain`), clear the ephemeral chain flag from any previous interrupted `--auto` chain. This does NOT touch `workflow.auto_advance` (the user's persistent settings preference):
```bash
if [[ ! "$ARGUMENTS" =~ --auto ]]; then
if [[ ! "$ARGUMENTS" =~ --auto ]] && [[ ! "$ARGUMENTS" =~ --chain ]]; then
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-set workflow._auto_chain_active false 2>/dev/null
fi
```
@@ -974,12 +1054,12 @@ Check for auto-advance trigger:
AUTO_CFG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get workflow.auto_advance 2>/dev/null || echo "false")
```
**If `--auto` flag present AND `AUTO_CHAIN` is not true:** Persist chain flag to config (handles direct `--auto` usage without new-project):
**If `--auto` or `--chain` flag present AND `AUTO_CHAIN` is not true:** Persist chain flag to config (handles direct usage without new-project):
```bash
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-set workflow._auto_chain_active true
```
**If `--auto` flag present OR `AUTO_CHAIN` is true OR `AUTO_CFG` is true:**
**If `--auto` flag present OR `--chain` flag present OR `AUTO_CHAIN` is true OR `AUTO_CFG` is true:**
Display banner:
```
@@ -1006,7 +1086,7 @@ This keeps the auto-advance chain flat — discuss, plan, and execute all run at
Auto-advance pipeline finished: discuss → plan → execute
Next: /gsd:discuss-phase ${NEXT_PHASE} --auto ${GSD_WS}
Next: /gsd:discuss-phase ${NEXT_PHASE} ${WAS_CHAIN ? "--chain" : "--auto"} ${GSD_WS}
<sub>/clear first → fresh context window</sub>
```
- **PLANNING COMPLETE** → Planning done, execution didn't complete:
@@ -1025,7 +1105,7 @@ This keeps the auto-advance chain flat — discuss, plan, and execute all run at
Continue: /gsd:plan-phase ${PHASE} --gaps ${GSD_WS}
```
**If neither `--auto` nor config enabled:**
**If none of `--auto`, `--chain`, nor config enabled:**
Route to `confirm_creation` step (existing behavior — show manual next steps).
</step>
@@ -1046,4 +1126,9 @@ Route to `confirm_creation` step (existing behavior — show manual next steps).
- Deferred ideas preserved for future phases
- STATE.md updated with session info
- User knows next steps
- Checkpoint file written after each area completes (incremental save)
- Interrupted sessions can be resumed from checkpoint (no re-answering completed areas)
- Checkpoint file cleaned up after successful CONTEXT.md write
- `--chain` triggers interactive discuss followed by auto plan+execute (no auto-answering)
- `--chain` and `--auto` both persist chain flag and auto-advance to plan-phase
</success_criteria>

File diff suppressed because it is too large Load Diff

View File

@@ -68,6 +68,14 @@ AGENT_SKILLS=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" agent-skills
Parse JSON for: `executor_model`, `verifier_model`, `commit_docs`, `parallelization`, `branching_strategy`, `branch_name`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `plans`, `incomplete_plans`, `plan_count`, `incomplete_count`, `state_exists`, `roadmap_exists`, `phase_req_ids`.
Read worktree config:
```bash
USE_WORKTREES=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get workflow.use_worktrees 2>/dev/null || echo "true")
```
When `USE_WORKTREES` is `false`, all executor agents run without `isolation="worktree"` — they execute sequentially on the main working tree instead of in parallel worktrees.
**If `phase_found` is false:** Error — phase directory not found.
**If `plan_count` is 0:** Error — no plans found in phase.
**If `state_exists` is false but `.planning/` exists:** Offer reconstruct or continue.
@@ -223,6 +231,8 @@ Execute each selected wave in sequence. Within a wave: parallel if `PARALLELIZAT
For 200k models, this keeps orchestrator context lean (~10-15%).
For 1M+ models (Opus 4.6, Sonnet 4.6), richer context can be passed directly.
**Worktree mode** (`USE_WORKTREES` is not `false`):
```
Task(
subagent_type="gsd-executor",
@@ -279,6 +289,19 @@ Execute each selected wave in sequence. Within a wave: parallel if `PARALLELIZAT
)
```
**Sequential mode** (`USE_WORKTREES` is `false`):
Omit `isolation="worktree"` from the Task call. Replace the `<parallel_execution>` block with:
```
<sequential_execution>
You are running as a SEQUENTIAL executor agent on the main working tree.
Use normal git commits (with hooks). Do NOT use --no-verify.
</sequential_execution>
```
When worktrees are disabled, execute plans **one at a time within each wave** (sequential) regardless of the `PARALLELIZATION` setting — multiple agents writing to the same working tree concurrently would cause conflicts.
3. **Wait for all agents in wave to complete.**
**Completion signal fallback (Copilot and runtimes where Task() may not return):**
@@ -313,6 +336,39 @@ Execute each selected wave in sequence. Within a wave: parallel if `PARALLELIZAT
```
If hooks fail: report the failure and ask "Fix hook issues now?" or "Continue to next wave?"
4.5. **Worktree cleanup (when `isolation="worktree"` was used):**
When executor agents ran in worktree isolation, their commits land on temporary branches in separate working trees. After the wave completes, merge these changes back and clean up:
```bash
# List worktrees created by this wave's agents
WORKTREES=$(git worktree list --porcelain | grep "^worktree " | grep -v "$(pwd)$" | sed 's/^worktree //')
for WT in $WORKTREES; do
# Get the branch name for this worktree
WT_BRANCH=$(git -C "$WT" rev-parse --abbrev-ref HEAD 2>/dev/null)
if [ -n "$WT_BRANCH" ] && [ "$WT_BRANCH" != "HEAD" ]; then
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
# Merge the worktree branch into the current branch
git merge "$WT_BRANCH" --no-edit -m "chore: merge executor worktree ($WT_BRANCH)" 2>&1 || {
echo "⚠ Merge conflict from worktree $WT_BRANCH — resolve manually"
continue
}
# Remove the worktree
git worktree remove "$WT" --force 2>/dev/null || true
# Delete the temporary branch
git branch -D "$WT_BRANCH" 2>/dev/null || true
fi
done
```
**If `workflow.use_worktrees` is `false`:** Agents ran on the main working tree — skip this step entirely.
**If no worktrees found:** Skip silently — agents may have been spawned without worktree isolation.
5. **Report completion — spot-check claims first:**
For each SUMMARY.md:
@@ -436,6 +492,27 @@ After all waves:
### Issues Encountered
[Aggregate from SUMMARYs, or "None"]
```
**Security gate check:**
```bash
SECURITY_CFG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get workflow.security_enforcement --raw 2>/dev/null || echo "true")
SECURITY_FILE=$(ls "${PHASE_DIR}"/*-SECURITY.md 2>/dev/null | head -1)
```
If `SECURITY_CFG` is `false`: skip.
If `SECURITY_CFG` is `true` AND `SECURITY_FILE` is empty (no SECURITY.md yet):
Include in the next-steps routing output:
```
⚠ Security enforcement enabled — run before advancing:
/gsd:secure-phase {PHASE} ${GSD_WS}
```
If `SECURITY_CFG` is `true` AND SECURITY.md exists: check frontmatter `threats_open`. If > 0:
```
⚠ Security gate: {threats_open} threats open
/gsd:secure-phase {PHASE} — resolve before advancing
```
</step>
<step name="handle_partial_wave_execution">
@@ -580,6 +657,72 @@ Options:
Use AskUserQuestion to present the options.
</step>
<step name="schema_drift_gate">
Post-execution schema drift detection. Catches false-positive verification where
build/types pass because TypeScript types come from config, not the live database.
**Run after execution completes but BEFORE verification marks success.**
```bash
SCHEMA_DRIFT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" verify schema-drift "${PHASE_NUMBER}" 2>/dev/null)
```
Parse JSON result for: `drift_detected`, `blocking`, `schema_files`, `orms`, `unpushed_orms`, `message`.
**If `drift_detected` is false:** Skip to verify_phase_goal.
**If `drift_detected` is true AND `blocking` is true:**
Check for override:
```bash
SKIP_SCHEMA=$(echo "${GSD_SKIP_SCHEMA_CHECK:-false}")
```
**If `SKIP_SCHEMA` is `true`:**
Display:
```
⚠ Schema drift detected but GSD_SKIP_SCHEMA_CHECK=true — bypassing gate.
Schema files changed: {schema_files}
ORMs requiring push: {unpushed_orms}
Proceeding to verification (database may be out of sync).
```
→ Continue to verify_phase_goal.
**If `SKIP_SCHEMA` is not `true`:**
BLOCK verification. Display:
```
## BLOCKED: Schema Drift Detected
Schema-relevant files changed during this phase but no database push command
was executed. Build and type checks pass because TypeScript types come from
config, not the live database — verification would produce a false positive.
Schema files changed: {schema_files}
ORMs requiring push: {unpushed_orms}
Required push commands:
{For each unpushed ORM, show the push command from the message}
Options:
1. Run push command now (recommended) — execute the push, then re-verify
2. Skip schema check (GSD_SKIP_SCHEMA_CHECK=true) — bypass this gate
3. Abort — stop execution and investigate
```
If `TEXT_MODE` is true, present as a plain-text numbered list. Otherwise use AskUserQuestion.
**If user selects option 1:** Present the specific push command(s) to run. After user confirms execution, re-run the schema drift check. If it passes, continue to verify_phase_goal.
**If user selects option 2:** Set override and continue to verify_phase_goal.
**If user selects option 3:** Stop execution. Report partial completion.
</step>
<step name="verify_phase_goal">
Verify phase achieved its GOAL, not just completed tasks.

View File

@@ -72,7 +72,7 @@ grep -n "type=\"checkpoint" .planning/phases/XX-name/{phase}-{plan}-PLAN.md
| Verify-only | B (segmented) | Segments between checkpoints. After none/human-verify → SUBAGENT. After decision/human-action → MAIN |
| Decision | C (main) | Execute entirely in main context |
**Pattern A:** init_agent_tracking → spawn Task(subagent_type="gsd-executor", model=executor_model, isolation="worktree") with prompt: execute plan at [path], autonomous, all tasks + SUMMARY + commit, follow deviation/auth rules, report: plan name, tasks, SUMMARY path, commit hash → track agent_id → wait → update tracking → report.
**Pattern A:** init_agent_tracking → spawn Task(subagent_type="gsd-executor", model=executor_model) with prompt: execute plan at [path], autonomous, all tasks + SUMMARY + commit, follow deviation/auth rules, report: plan name, tasks, SUMMARY path, commit hash → track agent_id → wait → update tracking → report. **Include `isolation="worktree"` only if `workflow.use_worktrees` is not `false`** (read via `config-get workflow.use_worktrees`).
**Pattern B:** Execute segment-by-segment. Autonomous segments: spawn subagent for assigned tasks only (no SUMMARY/commit). Checkpoints: main context. After all segments: aggregate, create SUMMARY, commit. See segment_execution.

View File

@@ -134,7 +134,7 @@ Usage: `/gsd:do I want to start a new milestone`
### Quick Mode
**`/gsd:quick [--full] [--discuss] [--research]`**
**`/gsd:quick [--full] [--validate] [--discuss] [--research]`**
Execute small, ad-hoc tasks with GSD guarantees but skip optional agents.
Quick mode uses the same system with a shorter path:
@@ -143,14 +143,16 @@ Quick mode uses the same system with a shorter path:
- Updates STATE.md tracking (not ROADMAP.md)
Flags enable additional quality steps:
- `--full` — Complete quality pipeline: discussion + research + plan-checking + verification
- `--validate` — Plan-checking (max 2 iterations) and post-execution verification only
- `--discuss` — Lightweight discussion to surface gray areas before planning
- `--research` — Focused research agent investigates approaches before planning
- `--full` — Adds plan-checking (max 2 iterations) and post-execution verification
Flags are composable: `--discuss --research --full` gives the complete quality pipeline for a single task.
Granular flags are composable: `--discuss --research --validate` gives the same as `--full`.
Usage: `/gsd:quick`
Usage: `/gsd:quick --research --full`
Usage: `/gsd:quick --full`
Usage: `/gsd:quick --research --validate`
Result: Creates `.planning/quick/NNN-slug/PLAN.md`, `.planning/quick/NNN-slug/SUMMARY.md`
---
@@ -343,11 +345,12 @@ Usage: `/gsd:ship 4` or `/gsd:ship 4 --draft`
---
**`/gsd:review --phase N [--gemini] [--claude] [--codex] [--all]`**
**`/gsd:review --phase N [--gemini] [--claude] [--codex] [--coderabbit] [--all]`**
Cross-AI peer review — invoke external AI CLIs to independently review phase plans.
- Detects available CLIs (gemini, claude, codex)
- Detects available CLIs (gemini, claude, codex, coderabbit)
- Each CLI reviews plans independently with the same structured prompt
- CodeRabbit reviews the current git diff (not a prompt) — may take up to 5 minutes
- Produces REVIEWS.md with per-reviewer feedback and consensus summary
- Feed reviews back into planning: `/gsd:plan-phase N --reviews`

View File

@@ -210,7 +210,7 @@ After discuss completes, loop back to dashboard step.
### Plan Phase N
Planning runs autonomously. Spawn a background agent:
Planning runs autonomously. Spawn a background agent that delegates to the Skill pipeline:
```
Task(
@@ -222,16 +222,12 @@ Working directory: {cwd}
Phase: {N} — {phase_name}
Goal: {goal}
Steps:
1. Read the plan-phase workflow: cat ~/.claude/get-shit-done/workflows/plan-phase.md
2. Run: node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" init plan-phase {N}
3. Follow the workflow steps to produce PLAN.md files for this phase.
4. If research is enabled in config, run the research step first.
5. Spawn a gsd-planner subagent via Task() to create the plans.
6. If plan-checker is enabled, spawn a gsd-plan-checker subagent to verify.
7. Commit plan files when complete.
Run the plan-phase Skill:
Skill(skill=\"gsd:plan-phase\", args=\"{N} --auto\")
Important: You are running in the background. Do NOT use AskUserQuestion — make autonomous decisions based on project context. If you hit a blocker, write it to STATE.md as a blocker and stop. Do NOT silently work around permission or file access errors — let them fail so the manager can surface them with resolution hints."
This delegates to the full plan-phase pipeline including local patches, research, plan-checker, and all quality gates.
Important: You are running in the background. Do NOT use AskUserQuestion — make autonomous decisions based on project context. If you hit a blocker, write it to STATE.md as a blocker and stop. Do NOT silently work around permission or file access errors — let them fail so the manager can surface them with resolution hints. Do NOT use --no-verify on git commits."
)
```
@@ -245,7 +241,7 @@ Loop back to dashboard step.
### Execute Phase N
Execution runs autonomously. Spawn a background agent:
Execution runs autonomously. Spawn a background agent that delegates to the Skill pipeline:
```
Task(
@@ -257,16 +253,12 @@ Working directory: {cwd}
Phase: {N} — {phase_name}
Goal: {goal}
Steps:
1. Read the execute-phase workflow: cat ~/.claude/get-shit-done/workflows/execute-phase.md
2. Run: node \"$HOME/.claude/get-shit-done/bin/gsd-tools.cjs\" init execute-phase {N}
3. Follow the workflow steps: discover plans, analyze dependencies, group into waves.
4. For each wave, spawn gsd-executor subagents via Task() to execute plans in parallel.
5. After all waves complete, spawn a gsd-verifier subagent if verifier is enabled.
6. Update ROADMAP.md and STATE.md with progress.
7. Commit all changes.
Run the execute-phase Skill:
Skill(skill=\"gsd:execute-phase\", args=\"{N}\")
Important: You are running in the background. Do NOT use AskUserQuestion — make autonomous decisions. Use --no-verify on git commits. If you hit a permission error, file lock, or any access issue, do NOT work around it — let it fail and write the error to STATE.md as a blocker so the manager can surface it with resolution guidance."
This delegates to the full execute-phase pipeline including local patches, branching, wave-based execution, verification, and all quality gates.
Important: You are running in the background. Do NOT use AskUserQuestion — make autonomous decisions. Do NOT use --no-verify on git commits — let pre-commit hooks run normally. If you hit a permission error, file lock, or any access issue, do NOT work around it — let it fail and write the error to STATE.md as a blocker so the manager can surface it with resolution guidance."
)
```
@@ -306,7 +298,7 @@ Classify the error:
- **question:** "Phase {N} failed — permission denied for `{tool_or_command}`. Want me to add it to settings.local.json so it's allowed?"
- **options:** "Add permission and retry" / "Run this phase inline instead" / "Skip and continue"
- "Add permission and retry": Use `Skill(skill="update-config")` to add the permission to `settings.local.json`, then re-spawn the background agent. Loop to dashboard.
- "Run this phase inline instead": Dispatch the same action (plan/execute) inline via `Skill()` instead of a background Task. Loop to dashboard after.
- "Run this phase inline instead": Dispatch the same action inline via the appropriate Skill — use `Skill(skill="gsd:plan-phase", args="{N}")` if the failed action was planning, or `Skill(skill="gsd:execute-phase", args="{N}")` if the failed action was execution. Loop to dashboard after.
- "Skip and continue": Loop to dashboard (phase stays in current state).
**Other errors** (git lock, file conflict, logic error, etc.):
@@ -314,7 +306,7 @@ Classify the error:
- **question:** "Background agent for Phase {N} encountered an issue: {error}. What next?"
- **options:** "Retry" / "Run inline instead" / "Skip and continue" / "View details"
- "Retry": Re-spawn the same background agent. Loop to dashboard.
- "Run inline instead": Dispatch the action inline via `Skill()`. Loop to dashboard after.
- "Run inline instead": Dispatch the action inline via the appropriate Skill — use `Skill(skill="gsd:plan-phase", args="{N}")` if the failed action was planning, or `Skill(skill="gsd:execute-phase", args="{N}")` if the failed action was execution. Loop to dashboard after.
- "Skip and continue": Loop to dashboard (phase stays in current state).
- "View details": Read STATE.md blockers section, display, then re-present options.

View File

@@ -55,7 +55,7 @@ If plans exist but not all have matching summaries:
**Route 5: All plans have summaries → verify and complete**
If all plans in the current phase have summaries:
→ Next action: `/gsd:verify-work` then `/gsd:complete-phase`
→ Next action: `/gsd:verify-work`
**Route 6: Phase complete, next phase exists → advance**
If the current phase is complete and the next phase exists in ROADMAP:

View File

@@ -101,7 +101,7 @@ If a scope has no directory or no entries, show: `(no notes)`
3. If N is invalid or refers to an already-promoted note, tell the user and stop
4. **Requires `.planning/` directory** — if it doesn't exist, warn: "Todos require a GSD project. Run `/gsd:new-project` to initialize one."
5. Ensure `.planning/todos/pending/` directory exists
6. Generate todo ID: `{NNN}-{slug}` where NNN is the next sequential number (scan both `.planning/todos/pending/` and `.planning/todos/done/` for the highest existing number, increment by 1, zero-pad to 3 digits) and slug is the first ~4 meaningful words of the note text
6. Generate todo ID: `{NNN}-{slug}` where NNN is the next sequential number (scan both `.planning/todos/pending/` and `.planning/todos/completed/` for the highest existing number, increment by 1, zero-pad to 3 digits) and slug is the first ~4 meaningful words of the note text
7. Extract the note text from the source file (body after frontmatter)
8. Create `.planning/todos/pending/{id}.md`:

View File

@@ -360,6 +360,32 @@ test -f "${PHASE_DIR}/${PADDED_PHASE}-VALIDATION.md" && echo "VALIDATION_CREATED
**If not found:** Warn and continue — plans may fail Dimension 8.
## 5.55. Security Threat Model Gate
> Skip if `workflow.security_enforcement` is explicitly `false`. Absent = enabled.
```bash
SECURITY_CFG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get workflow.security_enforcement --raw 2>/dev/null || echo "true")
SECURITY_ASVS=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get workflow.security_asvs_level --raw 2>/dev/null || echo "1")
SECURITY_BLOCK=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get workflow.security_block_on --raw 2>/dev/null || echo "high")
```
**If `SECURITY_CFG` is `false`:** Skip to step 5.6.
**If `SECURITY_CFG` is `true`:** Display banner:
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GSD ► SECURITY THREAT MODEL REQUIRED (ASVS L{SECURITY_ASVS})
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Each PLAN.md must include a <threat_model> block.
Block on: {SECURITY_BLOCK} severity threats.
Opt out: set security_enforcement: false in .planning/config.json
```
Continue to step 5.6. Security config is passed to the planner in step 8.
## 5.6. UI Design Contract Gate
> Skip if `workflow.ui_phase` is explicitly `false` AND `workflow.ui_safety_gate` is explicitly `false` in `.planning/config.json`. If keys are absent, treat as enabled.
@@ -409,7 +435,69 @@ Otherwise use AskUserQuestion:
- "Continue without UI-SPEC" → Continue to step 6.
- "Not a frontend phase" → Continue to step 6.
**If `HAS_UI` is 1 (no frontend indicators):** Skip silently to step 6.
**If `HAS_UI` is 1 (no frontend indicators):** Skip silently to step 5.7.
## 5.7. Schema Push Detection Gate
> Detects schema-relevant files in the phase scope and injects a mandatory `[BLOCKING]` schema push task into the plan. Prevents false-positive verification where build/types pass because TypeScript types come from config, not the live database.
Check if any files in the phase scope match schema patterns:
```bash
PHASE_SECTION=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" roadmap get-phase "${PHASE}" --pick section 2>/dev/null)
```
Scan `PHASE_SECTION`, `CONTEXT.md` (if loaded), and `RESEARCH.md` (if exists) for file paths matching these ORM patterns:
| ORM | File Patterns |
|-----|--------------|
| Payload CMS | `src/collections/**/*.ts`, `src/globals/**/*.ts` |
| Prisma | `prisma/schema.prisma`, `prisma/schema/*.prisma` |
| Drizzle | `drizzle/schema.ts`, `src/db/schema.ts`, `drizzle/*.ts` |
| Supabase | `supabase/migrations/*.sql` |
| TypeORM | `src/entities/**/*.ts`, `src/migrations/**/*.ts` |
Also check if any existing PLAN.md files for this phase already reference these file patterns in `files_modified`.
**If schema-relevant files detected:**
Set `SCHEMA_PUSH_REQUIRED=true` and `SCHEMA_ORM={detected_orm}`.
Determine the push command for the detected ORM:
| ORM | Push Command | Non-TTY Workaround |
|-----|-------------|-------------------|
| Payload CMS | `npx payload migrate` | `CI=true PAYLOAD_MIGRATING=true npx payload migrate` |
| Prisma | `npx prisma db push` | `npx prisma db push --accept-data-loss` (if destructive) |
| Drizzle | `npx drizzle-kit push` | `npx drizzle-kit push` |
| Supabase | `supabase db push` | Set `SUPABASE_ACCESS_TOKEN` env var |
| TypeORM | `npx typeorm migration:run` | `npx typeorm migration:run -d src/data-source.ts` |
Inject the following into the planner prompt (step 8) as an additional constraint:
```markdown
<schema_push_requirement>
**[BLOCKING] Schema Push Required**
This phase modifies schema-relevant files ({detected_files}). The planner MUST include
a `[BLOCKING]` task that runs the database schema push command AFTER all schema file
modifications are complete but BEFORE verification.
- ORM detected: {SCHEMA_ORM}
- Push command: {push_command}
- Non-TTY workaround: {env_hint}
- If push requires interactive prompts that cannot be suppressed, flag the task for
manual intervention with `autonomous: false`
This task is mandatory — the phase CANNOT pass verification without it. Build and
type checks will pass without the push (types come from config, not the live database),
creating a false-positive verification state.
</schema_push_requirement>
```
Display: `Schema files detected ({SCHEMA_ORM}) — [BLOCKING] push task will be injected into plans`
**If no schema-relevant files detected:** Skip silently to step 6.
## 6. Check Existing Plans
@@ -496,6 +584,7 @@ ${AGENT_SKILLS_PLANNER}
**Project instructions:** Read ./CLAUDE.md if exists — follow project-specific guidelines
**Project skills:** Check .claude/skills/ or .agents/skills/ directory (if either exists) — read SKILL.md files, plans should account for project skill rules
</planning_context>
<downstream_consumer>
@@ -560,9 +649,42 @@ Task(
## 9. Handle Planner Return
- **`## PLANNING COMPLETE`:** Display plan count. If `--skip-verify` or `plan_checker_enabled` is false (from init): skip to step 13. Otherwise: step 10.
- **`## PHASE SPLIT RECOMMENDED`:** The planner determined the phase is too complex to implement all user decisions without simplifying them. Handle in step 9b.
- **`## CHECKPOINT REACHED`:** Present to user, get response, spawn continuation (step 12)
- **`## PLANNING INCONCLUSIVE`:** Show attempts, offer: Add context / Retry / Manual
## 9b. Handle Phase Split Recommendation
When the planner returns `## PHASE SPLIT RECOMMENDED`, it means the phase has too many decisions to implement at full fidelity within the plan budget. The planner proposes groupings.
**Extract from planner return:**
- Proposed sub-phases (e.g., "17a: processing core (D-01 to D-19)", "17b: billing + config UX (D-20 to D-27)")
- Which D-XX decisions go in each sub-phase
- Why the split is necessary (decision count, complexity estimate)
**Present to user:**
```
## Phase {X} is too complex for full-fidelity implementation
The planner found {N} decisions that cannot all be implemented without
simplifying some. Instead of reducing your decisions, we recommend splitting:
**Option 1: Split into sub-phases**
- Phase {X}a: {name} — {D-XX to D-YY} ({N} decisions)
- Phase {X}b: {name} — {D-XX to D-YY} ({M} decisions)
**Option 2: Proceed anyway** (planner will attempt all, quality may degrade)
**Option 3: Prioritize** — you choose which decisions to implement now,
rest become a follow-up phase
```
Use AskUserQuestion with these 3 options.
**If "Split":** Use `/gsd:insert-phase` to create the sub-phases, then replan each.
**If "Proceed":** Return to planner with instruction to attempt all decisions at full fidelity, accepting more plans/tasks.
**If "Prioritize":** Use AskUserQuestion (multiSelect) to let user pick which D-XX are "now" vs "later". Create CONTEXT.md for each sub-phase with the selected decisions.
## 10. Spawn gsd-plan-checker Agent
Display banner:

View File

@@ -1,13 +1,15 @@
<purpose>
Execute small, ad-hoc tasks with GSD guarantees (atomic commits, STATE.md tracking). Quick mode spawns gsd-planner (quick mode) + gsd-executor(s), tracks tasks in `.planning/quick/`, and updates STATE.md's "Quick Tasks Completed" table.
With `--discuss` flag: lightweight discussion phase before planning. Surfaces assumptions, clarifies gray areas, captures decisions in CONTEXT.md so the planner treats them as locked.
With `--full` flag: enables the complete quality pipeline — discussion + research + plan-checking + verification. One flag for everything.
With `--full` flag: enables plan-checking (max 2 iterations) and post-execution verification for quality guarantees without full milestone ceremony.
With `--validate` flag: enables plan-checking (max 2 iterations) and post-execution verification only. Use when you want quality guarantees without discussion or research.
With `--discuss` flag: lightweight discussion phase before planning. Surfaces assumptions, clarifies gray areas, captures decisions in CONTEXT.md so the planner treats them as locked.
With `--research` flag: spawns a focused research agent before planning. Investigates implementation approaches, library options, and pitfalls. Use when you're unsure how to approach a task.
Flags are composable: `--discuss --research --full` gives discussion + research + plan-checking + verification.
Granular flags are composable: `--discuss --research --validate` gives the same result as `--full`.
</purpose>
<required_reading>
@@ -27,9 +29,10 @@ Valid GSD subagent types (use exact names — do not fall back to 'general-purpo
**Step 1: Parse arguments and get task description**
Parse `$ARGUMENTS` for:
- `--full` flag → store as `$FULL_MODE` (true/false)
- `--discuss` flag → store as `$DISCUSS_MODE` (true/false)
- `--research` flag → store as `$RESEARCH_MODE` (true/false)
- `--full` flag → store `$FULL_MODE=true`, `$DISCUSS_MODE=true`, `$RESEARCH_MODE=true`, `$VALIDATE_MODE=true`
- `--validate` flag → store `$VALIDATE_MODE=true`
- `--discuss` flag → store `$DISCUSS_MODE=true`
- `--research` flag → store `$RESEARCH_MODE=true`
- Remaining text → use as `$DESCRIPTION` if non-empty
If `$DESCRIPTION` is empty after parsing, prompt user interactively:
@@ -48,25 +51,34 @@ If still empty, re-prompt: "Please provide a task description."
Display banner based on active flags:
If `$DISCUSS_MODE` and `$RESEARCH_MODE` and `$FULL_MODE`:
If `$FULL_MODE` (all phases enabled — `--full` or all granular flags):
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GSD ► QUICK TASK (DISCUSS + RESEARCH + FULL)
GSD ► QUICK TASK (FULL)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
◆ Discussion + research + plan checking + verification enabled
```
If `$DISCUSS_MODE` and `$FULL_MODE` (no research):
If `$DISCUSS_MODE` and `$RESEARCH_MODE` and `$VALIDATE_MODE` (no `$FULL_MODE` — composed granularly):
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GSD ► QUICK TASK (DISCUSS + FULL)
GSD ► QUICK TASK (DISCUSS + RESEARCH + VALIDATE)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
◆ Discussion + research + plan checking + verification enabled
```
If `$DISCUSS_MODE` and `$VALIDATE_MODE` (no research):
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GSD ► QUICK TASK (DISCUSS + VALIDATE)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
◆ Discussion + plan checking + verification enabled
```
If `$DISCUSS_MODE` and `$RESEARCH_MODE` (no full):
If `$DISCUSS_MODE` and `$RESEARCH_MODE` (no validate):
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GSD ► QUICK TASK (DISCUSS + RESEARCH)
@@ -75,10 +87,10 @@ If `$DISCUSS_MODE` and `$RESEARCH_MODE` (no full):
◆ Discussion + research enabled
```
If `$RESEARCH_MODE` and `$FULL_MODE` (no discuss):
If `$RESEARCH_MODE` and `$VALIDATE_MODE` (no discuss):
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GSD ► QUICK TASK (RESEARCH + FULL)
GSD ► QUICK TASK (RESEARCH + VALIDATE)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
◆ Research + plan checking + verification enabled
@@ -102,10 +114,10 @@ If `$RESEARCH_MODE` only:
◆ Research phase enabled — investigating approaches before planning
```
If `$FULL_MODE` only:
If `$VALIDATE_MODE` only:
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GSD ► QUICK TASK (FULL MODE)
GSD ► QUICK TASK (VALIDATE)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
◆ Plan checking + verification enabled
@@ -126,6 +138,10 @@ AGENT_SKILLS_VERIFIER=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" age
Parse JSON for: `planner_model`, `executor_model`, `checker_model`, `verifier_model`, `commit_docs`, `branch_name`, `quick_id`, `slug`, `date`, `timestamp`, `quick_dir`, `task_dir`, `roadmap_exists`, `planning_exists`.
```bash
USE_WORKTREES=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get workflow.use_worktrees 2>/dev/null || echo "true")
```
**If `roadmap_exists` is false:** Error — Quick mode requires an active project with ROADMAP.md. Run `/gsd:new-project` first.
Quick tasks can run mid-phase - validation only checks ROADMAP.md exists, not phase status.
@@ -367,16 +383,16 @@ If research file not found, warn but continue: "Research agent did not produce o
**Step 5: Spawn planner (quick mode)**
**If `$FULL_MODE`:** Use `quick-full` mode with stricter constraints.
**If `$VALIDATE_MODE`:** Use `quick-full` mode with stricter constraints.
**If NOT `$FULL_MODE`:** Use standard `quick` mode.
**If NOT `$VALIDATE_MODE`:** Use standard `quick` mode.
```
Task(
prompt="
<planning_context>
**Mode:** ${FULL_MODE ? 'quick-full' : 'quick'}
**Mode:** ${VALIDATE_MODE ? 'quick-full' : 'quick'}
**Directory:** ${QUICK_DIR}
**Description:** ${DESCRIPTION}
@@ -397,9 +413,9 @@ ${AGENT_SKILLS_PLANNER}
- Create a SINGLE plan with 1-3 focused tasks
- Quick tasks should be atomic and self-contained
${RESEARCH_MODE ? '- Research findings are available — use them to inform library/pattern choices' : '- No research phase'}
${FULL_MODE ? '- Target ~40% context usage (structured for verification)' : '- Target ~30% context usage (simple, focused)'}
${FULL_MODE ? '- MUST generate `must_haves` in plan frontmatter (truths, artifacts, key_links)' : ''}
${FULL_MODE ? '- Each task MUST have `files`, `action`, `verify`, `done` fields' : ''}
${VALIDATE_MODE ? '- Target ~40% context usage (structured for verification)' : '- Target ~30% context usage (simple, focused)'}
${VALIDATE_MODE ? '- MUST generate `must_haves` in plan frontmatter (truths, artifacts, key_links)' : ''}
${VALIDATE_MODE ? '- Each task MUST have `files`, `action`, `verify`, `done` fields' : ''}
</constraints>
<output>
@@ -422,9 +438,9 @@ If plan not found, error: "Planner failed to create ${quick_id}-PLAN.md"
---
**Step 5.5: Plan-checker loop (only when `$FULL_MODE`)**
**Step 5.5: Plan-checker loop (only when `$VALIDATE_MODE`)**
Skip this step entirely if NOT `$FULL_MODE`.
Skip this step entirely if NOT `$VALIDATE_MODE`.
Display banner:
```
@@ -552,22 +568,37 @@ ${AGENT_SKILLS_EXECUTOR}
<constraints>
- Execute all tasks in the plan
- Commit each task atomically
- Commit each task atomically (code changes only)
- Create summary at: ${QUICK_DIR}/${quick_id}-SUMMARY.md
- Do NOT commit docs artifacts (SUMMARY.md, STATE.md, PLAN.md) — the orchestrator handles the docs commit in Step 8
- Do NOT update ROADMAP.md (quick tasks are separate from planned phases)
</constraints>
",
subagent_type="gsd-executor",
model="{executor_model}",
isolation="worktree",
${USE_WORKTREES !== "false" ? 'isolation="worktree",' : ''}
description="Execute: ${DESCRIPTION}"
)
```
After executor returns:
1. Verify summary exists at `${QUICK_DIR}/${quick_id}-SUMMARY.md`
2. Extract commit hash from executor output
3. Report completion status
1. **Worktree cleanup:** If the executor ran with `isolation="worktree"`, merge the worktree branch back and clean up:
```bash
# Find worktrees created by the executor
WORKTREES=$(git worktree list --porcelain | grep "^worktree " | grep -v "$(pwd)$" | sed 's/^worktree //')
for WT in $WORKTREES; do
WT_BRANCH=$(git -C "$WT" rev-parse --abbrev-ref HEAD 2>/dev/null)
if [ -n "$WT_BRANCH" ] && [ "$WT_BRANCH" != "HEAD" ]; then
git merge "$WT_BRANCH" --no-edit -m "chore: merge quick task worktree ($WT_BRANCH)" 2>&1 || echo "⚠ Merge conflict — resolve manually"
git worktree remove "$WT" --force 2>/dev/null || true
git branch -D "$WT_BRANCH" 2>/dev/null || true
fi
done
```
If `workflow.use_worktrees` is `false`, skip this step.
2. Verify summary exists at `${QUICK_DIR}/${quick_id}-SUMMARY.md`
3. Extract commit hash from executor output
4. Report completion status
**Known Claude Code bug (classifyHandoffIfNeeded):** If executor reports "failed" with error `classifyHandoffIfNeeded is not defined`, this is a Claude Code runtime bug — not a real failure. Check if summary file exists and git log shows commits. If so, treat as successful.
@@ -577,9 +608,9 @@ Note: For quick tasks producing multiple plans (rare), spawn executors in parall
---
**Step 6.5: Verification (only when `$FULL_MODE`)**
**Step 6.5: Verification (only when `$VALIDATE_MODE`)**
Skip this step entirely if NOT `$FULL_MODE`.
Skip this step entirely if NOT `$VALIDATE_MODE`.
Display banner:
```
@@ -636,7 +667,7 @@ Read STATE.md and check for `### Quick Tasks Completed` section.
Insert after `### Blockers/Concerns` section:
**If `$FULL_MODE`:**
**If `$VALIDATE_MODE`:**
```markdown
### Quick Tasks Completed
@@ -644,7 +675,7 @@ Insert after `### Blockers/Concerns` section:
|---|-------------|------|--------|--------|-----------|
```
**If NOT `$FULL_MODE`:**
**If NOT `$VALIDATE_MODE`:**
```markdown
### Quick Tasks Completed
@@ -652,18 +683,18 @@ Insert after `### Blockers/Concerns` section:
|---|-------------|------|--------|-----------|
```
**Note:** If the table already exists, match its existing column format. If adding `--full` to a project that already has quick tasks without a Status column, add the Status column to the header and separator rows, and leave Status empty for the new row's predecessors.
**Note:** If the table already exists, match its existing column format. If adding `--validate` (or `--full`) to a project that already has quick tasks without a Status column, add the Status column to the header and separator rows, and leave Status empty for the new row's predecessors.
**7c. Append new row to table:**
Use `date` from init:
**If `$FULL_MODE` (or table has Status column):**
**If `$VALIDATE_MODE` (or table has Status column):**
```markdown
| ${quick_id} | ${DESCRIPTION} | ${date} | ${commit_hash} | ${VERIFICATION_STATUS} | [${quick_id}-${slug}](./quick/${quick_id}-${slug}/) |
```
**If NOT `$FULL_MODE` (and table has no Status column):**
**If NOT `$VALIDATE_MODE` (and table has no Status column):**
```markdown
| ${quick_id} | ${DESCRIPTION} | ${date} | ${commit_hash} | [${quick_id}-${slug}](./quick/${quick_id}-${slug}/) |
```
@@ -681,7 +712,7 @@ Use Edit tool to make these changes atomically
**Step 8: Final commit and completion**
Stage and commit quick task artifacts:
Stage and commit quick task artifacts. This step MUST always run — even if the executor already committed some files (e.g. when running without worktree isolation). The `gsd-tools commit` command handles already-committed files gracefully.
Build file list:
- `${QUICK_DIR}/${quick_id}-PLAN.md`
@@ -689,9 +720,12 @@ Build file list:
- `.planning/STATE.md`
- If `$DISCUSS_MODE` and context file exists: `${QUICK_DIR}/${quick_id}-CONTEXT.md`
- If `$RESEARCH_MODE` and research file exists: `${QUICK_DIR}/${quick_id}-RESEARCH.md`
- If `$FULL_MODE` and verification file exists: `${QUICK_DIR}/${quick_id}-VERIFICATION.md`
- If `$VALIDATE_MODE` and verification file exists: `${QUICK_DIR}/${quick_id}-VERIFICATION.md`
```bash
# Explicitly stage all artifacts before commit — PLAN.md may be untracked
# if the executor ran without worktree isolation and committed docs early
git add ${file_list} 2>/dev/null
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(quick-${quick_id}): ${DESCRIPTION}" --files ${file_list}
```
@@ -702,11 +736,11 @@ commit_hash=$(git rev-parse --short HEAD)
Display completion output:
**If `$FULL_MODE`:**
**If `$VALIDATE_MODE`:**
```
---
GSD > QUICK TASK COMPLETE (FULL MODE)
GSD > QUICK TASK COMPLETE (VALIDATED)
Quick Task ${quick_id}: ${DESCRIPTION}
@@ -720,7 +754,7 @@ Commit: ${commit_hash}
Ready for next task: /gsd:quick ${GSD_WS}
```
**If NOT `$FULL_MODE`:**
**If NOT `$VALIDATE_MODE`:**
```
---
@@ -742,16 +776,17 @@ Ready for next task: /gsd:quick ${GSD_WS}
<success_criteria>
- [ ] ROADMAP.md validation passes
- [ ] User provides task description
- [ ] `--full`, `--discuss`, and `--research` flags parsed from arguments when present
- [ ] `--full`, `--validate`, `--discuss`, and `--research` flags parsed from arguments when present
- [ ] `--full` sets all booleans (`$FULL_MODE`, `$DISCUSS_MODE`, `$RESEARCH_MODE`, `$VALIDATE_MODE`)
- [ ] Slug generated (lowercase, hyphens, max 40 chars)
- [ ] Quick ID generated (YYMMDD-xxx format, 2s Base36 precision)
- [ ] Directory created at `.planning/quick/YYMMDD-xxx-slug/`
- [ ] (--discuss) Gray areas identified and presented, decisions captured in `${quick_id}-CONTEXT.md`
- [ ] (--research) Research agent spawned, `${quick_id}-RESEARCH.md` created
- [ ] `${quick_id}-PLAN.md` created by planner (honors CONTEXT.md decisions when --discuss, uses RESEARCH.md findings when --research)
- [ ] (--full) Plan checker validates plan, revision loop capped at 2
- [ ] (--validate) Plan checker validates plan, revision loop capped at 2
- [ ] `${quick_id}-SUMMARY.md` created by executor
- [ ] (--full) `${quick_id}-VERIFICATION.md` created by verifier
- [ ] STATE.md updated with quick task row (Status column when --full)
- [ ] (--validate) `${quick_id}-VERIFICATION.md` created by verifier
- [ ] STATE.md updated with quick task row (Status column when --validate)
- [ ] Artifacts committed
</success_criteria>

View File

@@ -18,12 +18,14 @@ Check which AI CLIs are available on the system:
command -v gemini >/dev/null 2>&1 && echo "gemini:available" || echo "gemini:missing"
command -v claude >/dev/null 2>&1 && echo "claude:available" || echo "claude:missing"
command -v codex >/dev/null 2>&1 && echo "codex:available" || echo "codex:missing"
command -v coderabbit >/dev/null 2>&1 && echo "coderabbit:available" || echo "coderabbit:missing"
```
Parse flags from `$ARGUMENTS`:
- `--gemini` → include Gemini
- `--claude` → include Claude
- `--codex` → include Codex
- `--coderabbit` → include CodeRabbit
- `--all` → include all available
- No flags → include all available
@@ -131,6 +133,14 @@ claude -p "$(cat /tmp/gsd-review-prompt-{phase}.md)" --no-input 2>/dev/null > /t
codex exec --skip-git-repo-check "$(cat /tmp/gsd-review-prompt-{phase}.md)" 2>/dev/null > /tmp/gsd-review-codex-{phase}.md
```
**CodeRabbit:**
Note: CodeRabbit reviews the current git diff/working tree — it does not accept a prompt. It may take up to 5 minutes. Use `timeout: 360000` on the Bash tool call.
```bash
coderabbit review --prompt-only 2>/dev/null > /tmp/gsd-review-coderabbit-{phase}.md
```
If a CLI fails, log the error and continue with remaining CLIs.
Display progress:
@@ -150,7 +160,7 @@ Combine all review responses into `{phase_dir}/{padded_phase}-REVIEWS.md`:
```markdown
---
phase: {N}
reviewers: [gemini, claude, codex]
reviewers: [gemini, claude, codex, coderabbit]
reviewed_at: {ISO timestamp}
plans_reviewed: [{list of PLAN.md files}]
---
@@ -175,6 +185,12 @@ plans_reviewed: [{list of PLAN.md files}]
---
## CodeRabbit Review
{coderabbit review content}
---
## Consensus Summary
{synthesize common concerns across all reviewers}

View File

@@ -0,0 +1,164 @@
<purpose>
Verify threat mitigations for a completed phase. Confirm PLAN.md threat register dispositions are resolved. Update SECURITY.md.
</purpose>
<required_reading>
@~/.claude/get-shit-done/references/ui-brand.md
</required_reading>
<available_agent_types>
Valid GSD subagent types (use exact names — do not fall back to 'general-purpose'):
- gsd-security-auditor — Verifies threat mitigation coverage
</available_agent_types>
<process>
## 0. Initialize
```bash
INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init phase-op "${PHASE_ARG}")
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
AGENT_SKILLS_AUDITOR=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" agent-skills gsd-security-auditor 2>/dev/null)
```
Parse: `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`.
```bash
AUDITOR_MODEL=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" resolve-model gsd-security-auditor --raw)
SECURITY_CFG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get workflow.security_enforcement --raw 2>/dev/null || echo "true")
```
If `SECURITY_CFG` is `false`: exit with "Security enforcement disabled. Enable via /gsd:settings."
Display banner: `GSD > SECURE PHASE {N}: {name}`
## 1. Detect Input State
```bash
SECURITY_FILE=$(ls "${PHASE_DIR}"/*-SECURITY.md 2>/dev/null | head -1)
PLAN_FILES=$(ls "${PHASE_DIR}"/*-PLAN.md 2>/dev/null)
SUMMARY_FILES=$(ls "${PHASE_DIR}"/*-SUMMARY.md 2>/dev/null)
```
- **State A** (`SECURITY_FILE` non-empty): Audit existing
- **State B** (`SECURITY_FILE` empty, `PLAN_FILES` and `SUMMARY_FILES` non-empty): Run from artifacts
- **State C** (`SUMMARY_FILES` empty): Exit — "Phase {N} not executed. Run /gsd:execute-phase {N} first."
## 2. Discovery
### 2a. Read Phase Artifacts
Read PLAN.md — extract `<threat_model>` block: trust boundaries, STRIDE register (`threat_id`, `category`, `component`, `disposition`, `mitigation_plan`).
### 2b. Read Summary Threat Flags
Read SUMMARY.md — extract `## Threat Flags` entries.
### 2c. Build Threat Register
Per threat: `{ threat_id, category, component, disposition, mitigation_pattern, files_to_check }`
## 3. Threat Classification
Classify each threat:
| Status | Criteria |
|--------|----------|
| CLOSED | mitigation found OR accepted risk documented in SECURITY.md OR transfer documented |
| OPEN | none of the above |
Build: `{ threat_id, category, component, disposition, status, evidence }`
If `threats_open: 0` → skip to Step 6 directly.
## 4. Present Threat Plan
Call AskUserQuestion with threat table and options:
1. "Verify all open threats" → Step 5
2. "Accept all open — document in accepted risks log" → add to SECURITY.md accepted risks, set all CLOSED, Step 6
3. "Cancel" → exit
## 5. Spawn gsd-security-auditor
```
Task(
prompt="Read ~/.claude/agents/gsd-security-auditor.md for instructions.\n\n" +
"<files_to_read>{PLAN, SUMMARY, impl files, SECURITY.md}</files_to_read>" +
"<threat_register>{threat register}</threat_register>" +
"<config>asvs_level: {SECURITY_ASVS}, block_on: {SECURITY_BLOCK_ON}</config>" +
"<constraints>Never modify implementation files. Verify mitigations exist — do not scan for new threats. Escalate implementation gaps.</constraints>" +
"${AGENT_SKILLS_AUDITOR}",
subagent_type="gsd-security-auditor",
model="{AUDITOR_MODEL}",
description="Verify threat mitigations for Phase {N}"
)
```
Handle return:
- `## SECURED` → record closures → Step 6
- `## OPEN_THREATS` → record closed + open, present user with accept/block choice → Step 6
- `## ESCALATE` → present to user → Step 6
## 6. Write/Update SECURITY.md
**State B (create):**
1. Read template from `~/.claude/get-shit-done/templates/SECURITY.md`
2. Fill: frontmatter, threat register, accepted risks, audit trail
3. Write to `${PHASE_DIR}/${PADDED_PHASE}-SECURITY.md`
**State A (update):**
1. Update threat register statuses, append to audit trail:
```markdown
## Security Audit {date}
| Metric | Count |
|--------|-------|
| Threats found | {N} |
| Closed | {M} |
| Open | {K} |
```
**ENFORCING GATE:** If `threats_open > 0` after all options exhausted (user did not accept, not all verified closed):
```
GSD > PHASE {N} SECURITY BLOCKED
{K} threats open — phase advancement blocked until threats_open: 0
▶ Fix mitigations then re-run: /gsd:secure-phase {N}
▶ Or document accepted risks in SECURITY.md and re-run.
```
Do NOT emit next-phase routing. Stop here.
## 7. Commit
```bash
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(phase-${PHASE}): add/update security threat verification"
```
## 8. Results + Routing
**Secured (threats_open: 0):**
```
GSD > PHASE {N} THREAT-SECURE
threats_open: 0 — all threats have dispositions.
▶ /gsd:validate-phase {N} validate test coverage
▶ /gsd:verify-work {N} run UAT
```
Display `/clear` reminder.
</process>
<success_criteria>
- [ ] Security enforcement checked — exit if false
- [ ] Input state detected (A/B/C) — state C exits cleanly
- [ ] PLAN.md threat model parsed, register built
- [ ] SUMMARY.md threat flags incorporated
- [ ] threats_open: 0 → skip directly to Step 6
- [ ] User gate with threat table presented
- [ ] Auditor spawned with complete context
- [ ] All three return formats (SECURED/OPEN_THREATS/ESCALATE) handled
- [ ] SECURITY.md created or updated
- [ ] threats_open > 0 BLOCKS advancement (no next-phase routing emitted)
- [ ] Results with routing presented on success
</success_criteria>

View File

@@ -199,11 +199,18 @@ Format each as: Test Name → What to do → Expected result → Why can't verif
</step>
<step name="determine_status">
**passed:** All truths VERIFIED, all artifacts pass levels 1-3, all key links WIRED, no blocker anti-patterns.
Classify status using this decision tree IN ORDER (most restrictive first):
**gaps_found:** Any truth FAILED, artifact MISSING/STUB, key link NOT_WIRED, or blocker found.
1. IF any truth FAILED, artifact MISSING/STUB, key link NOT_WIRED, or blocker found:
→ **gaps_found**
**human_needed:** All automated checks pass but human verification items remain.
2. IF the previous step produced ANY human verification items:
→ **human_needed** (even if all truths VERIFIED and score is N/N)
3. IF all checks pass AND no human verification items:
→ **passed**
**passed is ONLY valid when no human verification items exist.**
**Score:** `verified_truths / total_truths`
</step>

View File

@@ -375,11 +375,38 @@ Present summary:
**If issues > 0:** Proceed to `diagnose_issues`
**If issues == 0:**
```bash
SECURITY_CFG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get workflow.security_enforcement --raw 2>/dev/null || echo "true")
SECURITY_FILE=$(ls "${PHASE_DIR}"/*-SECURITY.md 2>/dev/null | head -1)
```
If `SECURITY_CFG` is `true` AND `SECURITY_FILE` is empty:
```
⚠ Security enforcement enabled — /gsd:secure-phase {phase} has not run.
Run before advancing to the next phase.
All tests passed. Ready to continue.
- `/gsd:secure-phase {phase}` — security review (required before advancing)
- `/gsd:plan-phase {next}` — Plan next phase
- `/gsd:execute-phase {next}` — Execute next phase
- `/gsd:ui-review {phase}` — visual quality audit (if frontend files were modified)
```
If `SECURITY_CFG` is `true` AND `SECURITY_FILE` exists: check frontmatter `threats_open`. If > 0:
```
⚠ Security gate: {threats_open} threats open
/gsd:secure-phase {phase} — resolve before advancing
```
If `SECURITY_CFG` is `false` OR (`SECURITY_FILE` exists AND `threats_open` is `0`):
```
All tests passed. Ready to continue.
- `/gsd:plan-phase {next}` — Plan next phase
- `/gsd:execute-phase {next}` — Execute next phase
- `/gsd:secure-phase {phase}` — security review
- `/gsd:ui-review {phase}` — visual quality audit (if frontend files were modified)
```
</step>

View File

@@ -29,7 +29,10 @@ function detectConfigDir(baseDir) {
const globalConfigDir = detectConfigDir(homeDir);
const projectConfigDir = detectConfigDir(cwd);
const cacheDir = path.join(globalConfigDir, 'cache');
// Use a shared, tool-agnostic cache directory to avoid multi-runtime
// resolution mismatches where check-update writes to one runtime's cache
// but statusline reads from another (#1421).
const cacheDir = path.join(homeDir, '.cache', 'gsd');
const cacheFile = path.join(cacheDir, 'gsd-update-check.json');
// VERSION file locations (check project first, then global)
@@ -65,10 +68,10 @@ const child = spawn(process.execPath, ['-e', `
} catch (e) {}
// Check for stale hooks — compare hook version headers against installed VERSION
// Hooks live inside get-shit-done/hooks/, not configDir/hooks/
// Hooks are installed at configDir/hooks/ (e.g. ~/.claude/hooks/) (#1421)
let staleHooks = [];
if (configDir) {
const hooksDir = path.join(configDir, 'get-shit-done', 'hooks');
const hooksDir = path.join(configDir, 'hooks');
try {
if (fs.existsSync(hooksDir)) {
const hookFiles = fs.readdirSync(hooksDir).filter(f => f.startsWith('gsd-') && f.endsWith('.js'));

View File

@@ -92,8 +92,12 @@ process.stdin.on('end', () => {
}
// GSD update available?
// Check shared cache first (#1421), fall back to runtime-specific cache for
// backward compatibility with older gsd-check-update.js versions.
let gsdUpdate = '';
const cacheFile = path.join(claudeDir, 'cache', 'gsd-update-check.json');
const sharedCacheFile = path.join(homeDir, '.cache', 'gsd', 'gsd-update-check.json');
const legacyCacheFile = path.join(claudeDir, 'cache', 'gsd-update-check.json');
const cacheFile = fs.existsSync(sharedCacheFile) ? sharedCacheFile : legacyCacheFile;
if (fs.existsSync(cacheFile)) {
try {
const cache = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));

View File

@@ -1,6 +1,6 @@
{
"name": "get-shit-done-cc",
"version": "1.30.0",
"version": "1.31.0",
"description": "A meta-prompting, context engineering and spec-driven development system for Claude Code, OpenCode, Gemini and Codex by TÂCHES.",
"bin": {
"get-shit-done-cc": "bin/install.js"

View File

@@ -64,6 +64,19 @@ Analyze the phase to identify gray areas:
5. **Log each decision** with rationale
</step>
<step name="pass_guard">
**CRITICAL — Single-pass guard:**
This step MUST complete in ONE pass. After writing CONTEXT.md, you are DONE. Do NOT re-read your own CONTEXT.md to identify "gaps", "undefined types", or "missing references" and run additional passes. Each decision naturally references other types and interfaces — this is expected, not a gap. The planner and executor will handle implementation details.
Self-referential gap-finding creates an infinite loop where:
1. Pass N creates decisions referencing types/interfaces
2. Pass N+1 "discovers" those references as "gaps"
3. Pass N+1 creates new decisions that reference more types
4. Repeat forever
Write your decisions once, comprehensively, then stop.
</step>
<step name="write_context">
Create CONTEXT.md capturing decisions made:

View File

@@ -31,6 +31,8 @@ export interface WorkflowConfig {
research_before_questions: boolean;
discuss_mode: string;
skip_discuss: boolean;
/** Maximum self-discuss passes in auto/headless mode before forcing proceed. Default: 3. */
max_discuss_passes: number;
}
export interface HooksConfig {
@@ -82,6 +84,7 @@ export const CONFIG_DEFAULTS: GSDConfig = {
research_before_questions: false,
discuss_mode: 'discuss',
skip_discuss: false,
max_discuss_passes: 3,
},
hooks: {
context_warnings: true,

View File

@@ -577,9 +577,9 @@ describe('PhaseRunner', () => {
// 1 initial + 1 retry = 2 calls (not 3)
expect(verifyCallCount).toBe(2);
// Verify step still succeeds (gap closure exhausted → proceed)
// Verify step fails when gaps persist after exhausting retries
const verifyStep = result.steps.find(s => s.step === PhaseStepType.Verify);
expect(verifyStep!.success).toBe(true);
expect(verifyStep!.success).toBe(false);
});
it('gaps_found triggers plan → execute → re-verify cycle', async () => {
@@ -659,9 +659,9 @@ describe('PhaseRunner', () => {
expect(afterVerify).not.toContain(PhaseStepType.Plan);
expect(afterVerify.filter(s => s === PhaseStepType.Execute)).toHaveLength(0);
// Verify step still reports success (exhausted retries → proceed)
// Verify step fails when gaps persist (no retries allowed)
const verifyStep = result.steps.find(s => s.step === PhaseStepType.Verify);
expect(verifyStep!.success).toBe(true);
expect(verifyStep!.success).toBe(false);
});
it('gap closure plan step failure proceeds to re-verify without executing', async () => {
@@ -724,8 +724,9 @@ describe('PhaseRunner', () => {
// 1 initial + 3 retries = 4 verify calls
expect(verifyCallCount).toBe(4);
// Verify step fails when gaps persist after all retries exhausted
const verifyStep = result.steps.find(s => s.step === PhaseStepType.Verify);
expect(verifyStep!.success).toBe(true);
expect(verifyStep!.success).toBe(false);
});
it('gap closure results are included in the final verify step planResults', async () => {
@@ -774,6 +775,69 @@ describe('PhaseRunner', () => {
});
});
// ─── Advance gate on persistent gaps ──────────────────────────────────
describe('advance gate on persistent gaps', () => {
it('persistent gaps_found does NOT append Advance step', async () => {
const phaseOp = makePhaseOp({ has_context: true, has_plans: true, plan_count: 1 });
const config = makeConfig({ workflow: { research: false, skip_discuss: true, plan_check: false } as any });
const deps = makeDeps({ config });
(deps.tools.initPhaseOp as ReturnType<typeof vi.fn>).mockResolvedValue(phaseOp);
mockRunPhaseStepSession.mockImplementation(async (_prompt, step) => {
if (step === PhaseStepType.Verify) {
return makePlanResult({
success: false,
error: { subtype: 'verification_failed', messages: ['Gaps persist'] },
});
}
return makePlanResult();
});
const runner = new PhaseRunner(deps);
const result = await runner.run('1');
const stepTypes = result.steps.map(s => s.step);
expect(stepTypes).not.toContain(PhaseStepType.Advance);
});
it('persistent gaps_found does NOT call phaseComplete', async () => {
const phaseOp = makePhaseOp({ has_context: true, has_plans: true, plan_count: 1 });
const config = makeConfig({ workflow: { research: false, skip_discuss: true, plan_check: false } as any });
const deps = makeDeps({ config });
(deps.tools.initPhaseOp as ReturnType<typeof vi.fn>).mockResolvedValue(phaseOp);
mockRunPhaseStepSession.mockImplementation(async (_prompt, step) => {
if (step === PhaseStepType.Verify) {
return makePlanResult({
success: false,
error: { subtype: 'verification_failed', messages: ['Gaps persist'] },
});
}
return makePlanResult();
});
const runner = new PhaseRunner(deps);
await runner.run('1');
expect(deps.tools.phaseComplete).not.toHaveBeenCalled();
});
it('verifier disabled still advances normally', async () => {
const phaseOp = makePhaseOp({ has_context: true, has_plans: true, plan_count: 1 });
const config = makeConfig({ workflow: { research: false, verifier: false, skip_discuss: true, plan_check: false } as any });
const deps = makeDeps({ config });
(deps.tools.initPhaseOp as ReturnType<typeof vi.fn>).mockResolvedValue(phaseOp);
const runner = new PhaseRunner(deps);
const result = await runner.run('1');
const stepTypes = result.steps.map(s => s.step);
expect(stepTypes).toContain(PhaseStepType.Advance);
expect(result.success).toBe(true);
});
});
// ─── Phase lifecycle events ────────────────────────────────────────────
describe('phase lifecycle events', () => {

View File

@@ -239,6 +239,8 @@ export class PhaseRunner {
if (!this.config.workflow.verifier) {
this.logger?.debug('Skipping verify: config.workflow.verifier=false');
} else {
// Verify has its own internal retry logic (gap closure). retryOnce only
// retries on unexpected session throws, not on verification outcomes like gaps_found.
const verifyResult = await this.retryOnce('verify', () => this.runVerifyStep(phaseNumber, sessionOpts, callbacks, options));
steps.push(verifyResult);
@@ -250,9 +252,13 @@ export class PhaseRunner {
}
// ── Step 6: Advance ──
if (!halted) {
// Only advance if verify passed — never mark a phase complete when gaps were found.
const verifyPassed = steps.every(s => s.step !== PhaseStepType.Verify || s.success);
if (!halted && verifyPassed) {
const advanceResult = await this.runAdvanceStep(phaseNumber, sessionOpts, callbacks);
steps.push(advanceResult);
} else if (!halted && !verifyPassed) {
this.logger?.warn(`Skipping advance for phase ${phaseNumber}: verification found gaps`);
}
const totalDurationMs = Date.now() - startTime;
@@ -296,6 +302,9 @@ export class PhaseRunner {
const result = await fn();
if (result.success) return result;
// Don't retry verify outcomes (gaps_found, human_needed) — they have their own retry logic.
if (result.error?.startsWith('verification_')) return result;
this.logger?.warn(`Step "${label}" failed, retrying once...`);
return fn();
}
@@ -410,8 +419,9 @@ export class PhaseRunner {
const contextFiles = await this.contextEngine.resolveContextFiles(PhaseType.Discuss);
let prompt = await this.promptFactory.buildPrompt(PhaseType.Discuss, null, contextFiles);
// Supplement with self-discuss instructions
prompt += '\n\n## Self-Discuss Mode\n\nYou are the AI discussing decisions with yourself. No human is present. Identify 3-5 gray areas in the project scope, reason through each one, make opinionated choices, and write CONTEXT.md with your decisions.';
// Supplement with self-discuss instructions with pass cap
const maxPasses = this.config.workflow.max_discuss_passes ?? 3;
prompt += `\n\n## Self-Discuss Mode\n\nYou are the AI discussing decisions with yourself. No human is present. Identify 3-5 gray areas in the project scope, reason through each one, make opinionated choices, and write CONTEXT.md with your decisions.\n\n**CRITICAL: Single-pass only.** You MUST complete all decisions in ONE pass and write CONTEXT.md once. Do NOT re-read your own CONTEXT.md to find "gaps" and do additional passes. The maximum allowed passes is ${maxPasses} — if you have already written CONTEXT.md, you are DONE. Proceed to the next workflow step. Self-referential gap-finding loops waste resources without adding value.`;
planResult = await runPhaseStepSession(
prompt,
@@ -838,6 +848,7 @@ export class PhaseRunner {
});
if (decision === 'accept') {
outcome = 'passed';
break; // Treat as passed
} else if (decision === 'retry' && gapRetryCount < maxGapRetries) {
gapRetryCount++;
@@ -912,6 +923,7 @@ export class PhaseRunner {
}
const durationMs = Date.now() - stepStart;
const verifySuccess = outcome === 'passed';
this.eventStream.emitEvent({
type: GSDEventType.PhaseStepComplete,
@@ -919,15 +931,17 @@ export class PhaseRunner {
sessionId: lastResult?.sessionId ?? '',
phaseNumber,
step: PhaseStepType.Verify,
success: true,
success: verifySuccess,
durationMs,
...(!verifySuccess && { error: `verification_${outcome}` }),
});
return {
step: PhaseStepType.Verify,
success: true,
success: verifySuccess,
durationMs,
planResults: allPlanResults,
...(!verifySuccess && { error: `verification_${outcome}` }),
};
}

View File

@@ -386,55 +386,23 @@ describe('DISCUSS: discussion log generation', () => {
});
});
// ─── Worktree Permission Mode (#1334) ───────────────────────────────────────
// ─── Cross-runtime agent compatibility (#1522) ──────────────────────────────
describe('PERM: worktree agents have permissionMode: acceptEdits', () => {
// Agents spawned with isolation="worktree" need permissionMode: acceptEdits
// to avoid per-directory edit permission prompts in the worktree path.
// See: anthropics/claude-code#29110, anthropics/claude-code#28041
const WORKTREE_AGENTS = ['gsd-executor', 'gsd-debugger'];
describe('COMPAT: agents must not use runtime-specific frontmatter keys', () => {
// permissionMode is Claude Code-specific and breaks Gemini CLI agent loading.
// It also has no effect on subagent Write permissions in Claude Code (blocked
// at runtime level regardless). See #1522, #1387.
const AGENTS_WITH_WRITE = ['gsd-executor', 'gsd-debugger'];
for (const agent of WORKTREE_AGENTS) {
test(`${agent} has permissionMode: acceptEdits`, () => {
for (const agent of AGENTS_WITH_WRITE) {
test(`${agent} does not have permissionMode (breaks Gemini CLI)`, () => {
const content = fs.readFileSync(path.join(AGENTS_DIR, agent + '.md'), 'utf-8');
const frontmatter = content.split('---')[1] || '';
assert.ok(
frontmatter.includes('permissionMode: acceptEdits'),
`${agent} must have permissionMode: acceptEdits — worktree agents need this to avoid ` +
`per-directory edit permission prompts (see #1334)`
!frontmatter.includes('permissionMode'),
`${agent} must not have permissionMode — it breaks Gemini CLI agent loading (#1522) ` +
`and has no effect in Claude Code (#1387)`
);
});
}
test('worktree-spawned agents are covered', () => {
// Verify that agents referenced with isolation="worktree" in workflows
// are included in the WORKTREE_AGENTS list above
const dirs = [WORKFLOWS_DIR, COMMANDS_DIR];
const worktreeAgentTypes = new Set();
for (const dir of dirs) {
if (!fs.existsSync(dir)) continue;
const files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
for (const file of files) {
const content = fs.readFileSync(path.join(dir, file), 'utf-8');
// Find patterns like: subagent_type="gsd-executor" ... isolation="worktree"
// These can span multiple lines in Task() calls
const taskBlocks = content.match(/Task\([^)]*isolation="worktree"[^)]*\)/gs) || [];
for (const block of taskBlocks) {
const typeMatch = block.match(/subagent_type="([^"]+)"/);
if (typeMatch) {
worktreeAgentTypes.add(typeMatch[1]);
}
}
}
}
for (const agentType of worktreeAgentTypes) {
assert.ok(
WORKTREE_AGENTS.includes(agentType),
`${agentType} is spawned with isolation="worktree" but not in WORKTREE_AGENTS list — ` +
`add permissionMode: acceptEdits to its frontmatter and update this test`
);
}
});
});

View File

@@ -30,7 +30,7 @@ describe('generate-claude-md', () => {
const output = JSON.parse(result.output);
assert.strictEqual(output.action, 'created');
assert.strictEqual(output.sections_total, 5);
assert.strictEqual(output.sections_total, 6);
assert.ok(output.sections_generated.includes('workflow'));
const claudePath = path.join(tmpDir, 'CLAUDE.md');
@@ -80,3 +80,170 @@ describe('new-project workflow includes CLAUDE.md generation', () => {
assert.ok(commandsContent.includes('`CLAUDE.md`'));
});
});
describe('generate-claude-md skills section', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
fs.writeFileSync(
path.join(tmpDir, '.planning', 'PROJECT.md'),
'# Test Project\n\n## What This Is\n\nA test project.\n'
);
});
afterEach(() => {
cleanup(tmpDir);
});
test('includes skills fallback when no skills directories exist', () => {
const result = runGsdTools('generate-claude-md', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.ok(output.sections_fallback.includes('skills'));
const content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
assert.ok(content.includes('<!-- GSD:skills-start'));
assert.ok(content.includes('<!-- GSD:skills-end -->'));
assert.ok(content.includes('No project skills found. Add skills to any of'));
});
test('discovers skills from .claude/skills/ directory', () => {
const skillDir = path.join(tmpDir, '.claude', 'skills', 'api-payments');
fs.mkdirSync(skillDir, { recursive: true });
fs.writeFileSync(
path.join(skillDir, 'SKILL.md'),
'---\nname: api-payments\ndescription: Payment gateway integration.\n---\n\n# API Payments\n'
);
const result = runGsdTools('generate-claude-md', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.ok(output.sections_generated.includes('skills'));
assert.ok(!output.sections_fallback.includes('skills'));
const content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
assert.ok(content.includes('api-payments'));
assert.ok(content.includes('Payment gateway integration'));
assert.ok(content.includes('## Project Skills'));
});
test('discovers skills from .agents/skills/ directory', () => {
const skillDir = path.join(tmpDir, '.agents', 'skills', 'data-sync');
fs.mkdirSync(skillDir, { recursive: true });
fs.writeFileSync(
path.join(skillDir, 'SKILL.md'),
'---\nname: data-sync\ndescription: ERP synchronization flows.\n---\n\n# Data Sync\n'
);
const result = runGsdTools('generate-claude-md', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
assert.ok(content.includes('data-sync'));
assert.ok(content.includes('ERP synchronization flows'));
});
test('skips gsd- prefixed skill directories', () => {
const gsdSkillDir = path.join(tmpDir, '.claude', 'skills', 'gsd-plan-phase');
const userSkillDir = path.join(tmpDir, '.claude', 'skills', 'my-feature');
fs.mkdirSync(gsdSkillDir, { recursive: true });
fs.mkdirSync(userSkillDir, { recursive: true });
fs.writeFileSync(
path.join(gsdSkillDir, 'SKILL.md'),
'---\nname: gsd-plan-phase\ndescription: GSD internal skill.\n---\n'
);
fs.writeFileSync(
path.join(userSkillDir, 'SKILL.md'),
'---\nname: my-feature\ndescription: Custom project skill.\n---\n'
);
const result = runGsdTools('generate-claude-md', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
assert.ok(!content.includes('gsd-plan-phase'));
assert.ok(content.includes('my-feature'));
assert.ok(content.includes('Custom project skill'));
});
test('handles multi-line description in frontmatter', () => {
const skillDir = path.join(tmpDir, '.claude', 'skills', 'complex-skill');
fs.mkdirSync(skillDir, { recursive: true });
fs.writeFileSync(
path.join(skillDir, 'SKILL.md'),
'---\nname: complex-skill\ndescription: First line of description.\n Continued on second line.\n And a third line.\n---\n'
);
const result = runGsdTools('generate-claude-md', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
assert.ok(content.includes('First line of description'));
assert.ok(content.includes('Continued on second line'));
assert.ok(content.includes('And a third line'));
});
test('deduplicates skills found in multiple directories', () => {
// Same skill in both .claude/skills/ and .agents/skills/
const dir1 = path.join(tmpDir, '.claude', 'skills', 'shared-skill');
const dir2 = path.join(tmpDir, '.agents', 'skills', 'shared-skill');
fs.mkdirSync(dir1, { recursive: true });
fs.mkdirSync(dir2, { recursive: true });
const skillContent = '---\nname: shared-skill\ndescription: Appears twice.\n---\n';
fs.writeFileSync(path.join(dir1, 'SKILL.md'), skillContent);
fs.writeFileSync(path.join(dir2, 'SKILL.md'), skillContent);
const result = runGsdTools('generate-claude-md', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
const matches = content.match(/shared-skill/g);
// Should appear exactly twice: once in name column, once in path column (single row)
assert.strictEqual(matches.length, 2);
});
test('updates existing skills section on regeneration', () => {
// First generation — no skills
runGsdTools('generate-claude-md', tmpDir);
let content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
assert.ok(content.includes('No project skills found'));
// Add a skill and regenerate
const skillDir = path.join(tmpDir, '.claude', 'skills', 'new-skill');
fs.mkdirSync(skillDir, { recursive: true });
fs.writeFileSync(
path.join(skillDir, 'SKILL.md'),
'---\nname: new-skill\ndescription: Just added.\n---\n'
);
const result = runGsdTools('generate-claude-md', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
assert.ok(!content.includes('No project skills found'));
assert.ok(content.includes('new-skill'));
assert.ok(content.includes('Just added'));
});
test('skills section appears between architecture and workflow', () => {
const skillDir = path.join(tmpDir, '.claude', 'skills', 'ordering-test');
fs.mkdirSync(skillDir, { recursive: true });
fs.writeFileSync(
path.join(skillDir, 'SKILL.md'),
'---\nname: ordering-test\ndescription: Verify section order.\n---\n'
);
const result = runGsdTools('generate-claude-md', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const content = fs.readFileSync(path.join(tmpDir, 'CLAUDE.md'), 'utf-8');
const archIdx = content.indexOf('## Architecture');
const skillsIdx = content.indexOf('## Project Skills');
const workflowIdx = content.indexOf('## GSD Workflow Enforcement');
assert.ok(archIdx < skillsIdx, 'Skills section should come after Architecture');
assert.ok(skillsIdx < workflowIdx, 'Skills section should come before Workflow Enforcement');
});
});

View File

@@ -0,0 +1,378 @@
/**
* GSD Tools Tests - Claude Skills Migration (#1504)
*
* Tests for migrating Claude Code from commands/gsd/ to skills/gsd-xxx/SKILL.md
* format for compatibility with Claude Code 2.1.88+.
*
* Uses node:test and node:assert (NOT Jest).
*/
process.env.GSD_TEST_MODE = '1';
const { test, describe, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert');
const path = require('path');
const os = require('os');
const fs = require('fs');
const {
convertClaudeCommandToClaudeSkill,
copyCommandsAsClaudeSkills,
writeManifest,
install,
} = require('../bin/install.js');
// ─── convertClaudeCommandToClaudeSkill ──────────────────────────────────────
describe('convertClaudeCommandToClaudeSkill', () => {
test('preserves allowed-tools multiline YAML list', () => {
const input = [
'---',
'name: gsd:next',
'description: Advance to the next step',
'allowed-tools:',
' - Read',
' - Bash',
' - Grep',
'---',
'',
'Body content here.',
].join('\n');
const result = convertClaudeCommandToClaudeSkill(input, 'gsd-next');
assert.ok(result.includes('allowed-tools:'), 'allowed-tools field is present');
assert.ok(result.includes('Read'), 'Read tool preserved');
assert.ok(result.includes('Bash'), 'Bash tool preserved');
assert.ok(result.includes('Grep'), 'Grep tool preserved');
});
test('preserves argument-hint', () => {
const input = [
'---',
'name: gsd:debug',
'description: Debug issues',
'argument-hint: "[issue description]"',
'allowed-tools:',
' - Read',
' - Bash',
'---',
'',
'Debug body.',
].join('\n');
const result = convertClaudeCommandToClaudeSkill(input, 'gsd-debug');
assert.ok(result.includes('argument-hint:'), 'argument-hint field is present');
// The value should be preserved (possibly yaml-quoted)
assert.ok(
result.includes('[issue description]'),
'argument-hint value preserved'
);
});
test('converts name format from gsd:xxx to skill naming', () => {
const input = [
'---',
'name: gsd:next',
'description: Advance workflow',
'---',
'',
'Body.',
].join('\n');
const result = convertClaudeCommandToClaudeSkill(input, 'gsd-next');
assert.ok(result.includes('name: gsd-next'), 'name uses skill naming convention');
assert.ok(!result.includes('name: gsd:next'), 'old name format removed');
});
test('preserves body content unchanged', () => {
const body = '\n<objective>\nDo the thing.\n</objective>\n\n<process>\nStep 1.\nStep 2.\n</process>\n';
const input = [
'---',
'name: gsd:test',
'description: Test command',
'---',
body,
].join('');
const result = convertClaudeCommandToClaudeSkill(input, 'gsd-test');
assert.ok(result.includes('<objective>'), 'objective tag preserved');
assert.ok(result.includes('Do the thing.'), 'body text preserved');
assert.ok(result.includes('<process>'), 'process tag preserved');
assert.ok(result.includes('Step 1.'), 'step text preserved');
});
test('preserves agent field', () => {
const input = [
'---',
'name: gsd:plan-phase',
'description: Plan a phase',
'agent: true',
'allowed-tools:',
' - Read',
'---',
'',
'Plan body.',
].join('\n');
const result = convertClaudeCommandToClaudeSkill(input, 'gsd-plan-phase');
assert.ok(result.includes('agent:'), 'agent field is present');
});
test('handles content with no frontmatter', () => {
const input = 'Just some plain markdown content.';
const result = convertClaudeCommandToClaudeSkill(input, 'gsd-plain');
assert.strictEqual(result, input, 'content returned unchanged');
});
test('preserves allowed-tools as multiline YAML list (not flattened)', () => {
const input = [
'---',
'name: gsd:debug',
'description: Debug',
'allowed-tools:',
' - Read',
' - Bash',
' - Task',
' - AskUserQuestion',
'---',
'',
'Body.',
].join('\n');
const result = convertClaudeCommandToClaudeSkill(input, 'gsd-debug');
// Claude Code native format keeps YAML multiline list
assert.ok(result.includes(' - Read'), 'Read in multiline list');
assert.ok(result.includes(' - Bash'), 'Bash in multiline list');
assert.ok(result.includes(' - Task'), 'Task in multiline list');
assert.ok(result.includes(' - AskUserQuestion'), 'AskUserQuestion in multiline list');
});
});
// ─── copyCommandsAsClaudeSkills ─────────────────────────────────────────────
describe('copyCommandsAsClaudeSkills', () => {
let tmpDir;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-claude-skills-test-'));
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
test('creates correct directory structure skills/gsd-xxx/SKILL.md', () => {
// Create source commands
const srcDir = path.join(tmpDir, 'src');
fs.mkdirSync(srcDir, { recursive: true });
fs.writeFileSync(
path.join(srcDir, 'next.md'),
'---\nname: gsd:next\ndescription: Advance\nallowed-tools:\n - Read\n---\n\nBody.'
);
fs.writeFileSync(
path.join(srcDir, 'health.md'),
'---\nname: gsd:health\ndescription: Check health\n---\n\nHealth body.'
);
const skillsDir = path.join(tmpDir, 'skills');
copyCommandsAsClaudeSkills(srcDir, skillsDir, 'gsd', '$HOME/.claude/', 'claude', true);
// Verify directory structure
assert.ok(
fs.existsSync(path.join(skillsDir, 'gsd-next', 'SKILL.md')),
'skills/gsd-next/SKILL.md exists'
);
assert.ok(
fs.existsSync(path.join(skillsDir, 'gsd-health', 'SKILL.md')),
'skills/gsd-health/SKILL.md exists'
);
});
test('cleans up old skills before installing new ones', () => {
const srcDir = path.join(tmpDir, 'src');
fs.mkdirSync(srcDir, { recursive: true });
fs.writeFileSync(
path.join(srcDir, 'next.md'),
'---\nname: gsd:next\ndescription: Advance\n---\n\nBody.'
);
const skillsDir = path.join(tmpDir, 'skills');
// Create a stale skill that should be removed
const staleDir = path.join(skillsDir, 'gsd-old-command');
fs.mkdirSync(staleDir, { recursive: true });
fs.writeFileSync(path.join(staleDir, 'SKILL.md'), 'stale content');
copyCommandsAsClaudeSkills(srcDir, skillsDir, 'gsd', '$HOME/.claude/', 'claude', true);
// Stale skill removed
assert.ok(
!fs.existsSync(staleDir),
'stale skill directory removed'
);
// New skill created
assert.ok(
fs.existsSync(path.join(skillsDir, 'gsd-next', 'SKILL.md')),
'new skill created'
);
});
test('does not remove non-GSD skills', () => {
const srcDir = path.join(tmpDir, 'src');
fs.mkdirSync(srcDir, { recursive: true });
fs.writeFileSync(
path.join(srcDir, 'next.md'),
'---\nname: gsd:next\ndescription: Advance\n---\n\nBody.'
);
const skillsDir = path.join(tmpDir, 'skills');
// Create a non-GSD skill
const otherDir = path.join(skillsDir, 'my-custom-skill');
fs.mkdirSync(otherDir, { recursive: true });
fs.writeFileSync(path.join(otherDir, 'SKILL.md'), 'custom content');
copyCommandsAsClaudeSkills(srcDir, skillsDir, 'gsd', '$HOME/.claude/', 'claude', true);
// Non-GSD skill preserved
assert.ok(
fs.existsSync(otherDir),
'non-GSD skill preserved'
);
});
test('handles recursive subdirectories', () => {
const srcDir = path.join(tmpDir, 'src');
const subDir = path.join(srcDir, 'wired');
fs.mkdirSync(subDir, { recursive: true });
fs.writeFileSync(
path.join(subDir, 'ready.md'),
'---\nname: gsd-wired:ready\ndescription: Show ready tasks\n---\n\nBody.'
);
const skillsDir = path.join(tmpDir, 'skills');
copyCommandsAsClaudeSkills(srcDir, skillsDir, 'gsd', '$HOME/.claude/', 'claude', true);
assert.ok(
fs.existsSync(path.join(skillsDir, 'gsd-wired-ready', 'SKILL.md')),
'nested command creates gsd-wired-ready/SKILL.md'
);
});
test('no-ops when source directory does not exist', () => {
const skillsDir = path.join(tmpDir, 'skills');
// Should not throw
copyCommandsAsClaudeSkills(
path.join(tmpDir, 'nonexistent'),
skillsDir,
'gsd',
'$HOME/.claude/',
'claude',
true
);
assert.ok(!fs.existsSync(skillsDir), 'skills dir not created when src missing');
});
});
// ─── Legacy cleanup during install ──────────────────────────────────────────
describe('Legacy commands/gsd/ cleanup', () => {
let tmpDir;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-legacy-test-'));
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
test('install removes legacy commands/gsd/ directory when present', () => {
// Create a mock legacy commands/gsd/ directory
const legacyDir = path.join(tmpDir, 'commands', 'gsd');
fs.mkdirSync(legacyDir, { recursive: true });
fs.writeFileSync(path.join(legacyDir, 'next.md'), 'legacy content');
// Create source commands for the installer to read
const srcDir = path.join(tmpDir, 'src');
fs.mkdirSync(srcDir, { recursive: true });
fs.writeFileSync(
path.join(srcDir, 'next.md'),
'---\nname: gsd:next\ndescription: Advance\n---\n\nBody.'
);
const skillsDir = path.join(tmpDir, 'skills');
// Install skills
copyCommandsAsClaudeSkills(srcDir, skillsDir, 'gsd', '$HOME/.claude/', 'claude', true);
// Simulate the legacy cleanup that install() does after copyCommandsAsClaudeSkills
if (fs.existsSync(legacyDir)) {
fs.rmSync(legacyDir, { recursive: true });
}
assert.ok(!fs.existsSync(legacyDir), 'legacy commands/gsd/ removed');
assert.ok(
fs.existsSync(path.join(skillsDir, 'gsd-next', 'SKILL.md')),
'new skill installed'
);
});
});
// ─── writeManifest tracks skills/ for Claude ────────────────────────────────
describe('writeManifest tracks skills/ for Claude', () => {
let tmpDir;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-manifest-test-'));
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
test('manifest includes skills/gsd-xxx/SKILL.md entries for Claude runtime', () => {
// Create skills directory structure (as install would)
const skillsDir = path.join(tmpDir, 'skills');
const skillDir = path.join(skillsDir, 'gsd-next');
fs.mkdirSync(skillDir, { recursive: true });
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), 'skill content');
// Create get-shit-done directory (required by writeManifest)
const gsdDir = path.join(tmpDir, 'get-shit-done');
fs.mkdirSync(gsdDir, { recursive: true });
fs.writeFileSync(path.join(gsdDir, 'test.md'), 'test');
writeManifest(tmpDir, 'claude');
const manifest = JSON.parse(
fs.readFileSync(path.join(tmpDir, 'gsd-file-manifest.json'), 'utf8')
);
// Should have skills/ entries
const skillEntries = Object.keys(manifest.files).filter(k =>
k.startsWith('skills/')
);
assert.ok(skillEntries.length > 0, 'manifest has skills/ entries');
assert.ok(
skillEntries.some(k => k === 'skills/gsd-next/SKILL.md'),
'manifest has skills/gsd-next/SKILL.md'
);
// Should NOT have commands/gsd/ entries
const cmdEntries = Object.keys(manifest.files).filter(k =>
k.startsWith('commands/gsd/')
);
assert.strictEqual(cmdEntries.length, 0, 'manifest has no commands/gsd/ entries');
});
});
// ─── Exports exist ──────────────────────────────────────────────────────────
describe('Claude skills migration exports', () => {
test('convertClaudeCommandToClaudeSkill is exported', () => {
assert.strictEqual(typeof convertClaudeCommandToClaudeSkill, 'function');
});
test('copyCommandsAsClaudeSkills is exported', () => {
assert.strictEqual(typeof copyCommandsAsClaudeSkills, 'function');
});
});

View File

@@ -169,6 +169,21 @@ Run /gsd:execute-phase to proceed.`;
const result = convertClaudeAgentToCodexAgent(input);
assert.strictEqual(result, input, 'returns input unchanged');
});
test('replaces .claude paths with .codex paths (#1430)', () => {
const input = `---
name: gsd-debugger
description: Debugs issues
tools: Read, Bash
---
INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state load)
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: resolve"`;
const result = convertClaudeAgentToCodexAgent(input);
assert.ok(result.includes('$HOME/.codex/get-shit-done/bin/gsd-tools.cjs'), 'replaces $HOME/.claude/ with $HOME/.codex/');
assert.ok(!result.includes('$HOME/.claude/'), 'no .claude paths remain');
});
});
// ─── generateCodexAgentToml ─────────────────────────────────────────────────────

View File

@@ -1250,6 +1250,41 @@ describe('commit command', () => {
const branch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: tmpDir, encoding: 'utf-8' }).trim();
assert.strictEqual(branch, 'gsd/phase-01-setup', 'should be on phase branch');
});
test('decimal phase numbers are captured correctly in branching strategy', () => {
// Configure phase branching strategy
fs.writeFileSync(
path.join(tmpDir, '.planning', 'config.json'),
JSON.stringify({
commit_docs: true,
branching_strategy: 'phase',
phase_branch_template: 'gsd/phase-{phase}-{slug}',
})
);
// Create ROADMAP.md with a decimal phase
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '45.14-golden-capture'), { recursive: true });
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
'# Roadmap\n\n## Phase 45.14: Golden Capture\nGoal: Capture golden standard\n'
);
// Create a context file for phase 45.14
fs.writeFileSync(path.join(tmpDir, '.planning', 'phases', '45.14-golden-capture', '45.14-CONTEXT.md'), '# Context\n');
const result = runGsdTools(
'commit "docs(45.14): add context" --files .planning/phases/45.14-golden-capture/45.14-CONTEXT.md',
tmpDir
);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.committed, true, 'should have committed');
// Verify we're on the correct branch (45.14, not 14)
const { execFileSync } = require('child_process');
const branch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: tmpDir, encoding: 'utf-8' }).trim();
assert.strictEqual(branch, 'gsd/phase-45.14-golden-capture', 'should be on decimal phase branch, not integer-only');
});
});
// ─────────────────────────────────────────────────────────────────────────────
@@ -1409,11 +1444,12 @@ describe('stats command', () => {
fs.mkdirSync(p1, { recursive: true });
fs.mkdirSync(p2, { recursive: true });
// Phase 1: 2 plans, 2 summaries (complete)
// Phase 1: 2 plans, 2 summaries, passing verification (complete)
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
fs.writeFileSync(path.join(p1, '01-02-PLAN.md'), '# Plan');
fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
fs.writeFileSync(path.join(p1, '01-02-SUMMARY.md'), '# Summary');
fs.writeFileSync(path.join(p1, 'VERIFICATION.md'), '---\nstatus: passed\n---\n# Verification');
// Phase 2: 1 plan, 0 summaries (planned)
fs.writeFileSync(path.join(p2, '02-01-PLAN.md'), '# Plan');
@@ -1485,8 +1521,10 @@ describe('stats command', () => {
fs.mkdirSync(p2, { recursive: true });
fs.writeFileSync(path.join(p1, '14-01-PLAN.md'), '# Plan');
fs.writeFileSync(path.join(p1, '14-01-SUMMARY.md'), '# Summary');
fs.writeFileSync(path.join(p1, 'VERIFICATION.md'), '---\nstatus: passed\n---\n# Verified');
fs.writeFileSync(path.join(p2, '15-01-PLAN.md'), '# Plan');
fs.writeFileSync(path.join(p2, '15-01-SUMMARY.md'), '# Summary');
fs.writeFileSync(path.join(p2, 'VERIFICATION.md'), '---\nstatus: passed\n---\n# Verified');
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
@@ -1569,6 +1607,7 @@ describe('stats command', () => {
fs.mkdirSync(p1, { recursive: true });
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
fs.writeFileSync(path.join(p1, 'VERIFICATION.md'), '---\nstatus: passed\n---\n# Verified');
const result = runGsdTools('stats table', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
@@ -1580,4 +1619,78 @@ describe('stats command', () => {
assert.ok(parsed.rendered.includes('| 1 |'), 'should include phase row');
assert.ok(parsed.rendered.includes('1/1 phases'), 'should report phase progress');
});
test('phase with summaries but no verification is Executed, not Complete', () => {
const p1 = path.join(tmpDir, '.planning', 'phases', '01-auth');
fs.mkdirSync(p1, { recursive: true });
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
const result = runGsdTools('stats', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const stats = JSON.parse(result.output);
const phase = stats.phases.find(p => p.number === '01' || p.number === '1');
assert.strictEqual(phase.status, 'Executed', 'should be Executed without verification');
assert.strictEqual(stats.phases_completed, 0, 'unverified phase should not count as completed');
});
test('phase with passing verification is Complete', () => {
const p1 = path.join(tmpDir, '.planning', 'phases', '01-auth');
fs.mkdirSync(p1, { recursive: true });
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
fs.writeFileSync(path.join(p1, 'VERIFICATION.md'), '---\nstatus: passed\n---\n# Verification');
const result = runGsdTools('stats', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const stats = JSON.parse(result.output);
const phase = stats.phases.find(p => p.number === '01' || p.number === '1');
assert.strictEqual(phase.status, 'Complete', 'should be Complete with passing verification');
assert.strictEqual(stats.phases_completed, 1);
});
test('phase with gaps_found verification is Executed', () => {
const p1 = path.join(tmpDir, '.planning', 'phases', '01-auth');
fs.mkdirSync(p1, { recursive: true });
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
fs.writeFileSync(path.join(p1, 'VERIFICATION.md'), '---\nstatus: gaps_found\n---\n# Verification');
const result = runGsdTools('stats', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const stats = JSON.parse(result.output);
const phase = stats.phases.find(p => p.number === '01' || p.number === '1');
assert.strictEqual(phase.status, 'Executed', 'gaps_found should show as Executed');
});
test('phase with human_needed verification shows Needs Review', () => {
const p1 = path.join(tmpDir, '.planning', 'phases', '01-auth');
fs.mkdirSync(p1, { recursive: true });
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
fs.writeFileSync(path.join(p1, 'VERIFICATION.md'), '---\nstatus: human_needed\n---\n# Verification');
const result = runGsdTools('stats', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const stats = JSON.parse(result.output);
const phase = stats.phases.find(p => p.number === '01' || p.number === '1');
assert.strictEqual(phase.status, 'Needs Review', 'human_needed should show as Needs Review');
});
test('progress command also uses verification-aware status', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap v1.0 MVP\n`
);
const p1 = path.join(tmpDir, '.planning', 'phases', '01-auth');
fs.mkdirSync(p1, { recursive: true });
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
const result = runGsdTools('progress json', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.phases[0].status, 'Executed', 'progress should show Executed without verification');
});
});

View File

@@ -232,6 +232,16 @@ describe('config-set command', () => {
assert.strictEqual(config.workflow.text_mode, true);
});
test('sets workflow.use_worktrees to disable worktree isolation', () => {
writeConfig(tmpDir, {});
const result = runGsdTools('config-set workflow.use_worktrees false', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.workflow.use_worktrees, false);
});
test('errors when no key path provided', () => {
const result = runGsdTools('config-set', tmpDir);
assert.strictEqual(result.success, false);
@@ -765,3 +775,58 @@ describe('config-set workflow.skip_discuss', () => {
assert.strictEqual(output, true);
});
});
// ─── config-set/config-get workflow.use_worktrees ────────────────────────────
describe('config-set/config-get workflow.use_worktrees', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
runGsdTools('config-ensure-section', tmpDir, { HOME: tmpDir, USERPROFILE: tmpDir });
});
afterEach(() => {
cleanup(tmpDir);
});
test('config-get workflow.use_worktrees returns false after setting to false', () => {
runGsdTools('config-set workflow.use_worktrees false', tmpDir);
const result = runGsdTools('config-get workflow.use_worktrees', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output, false);
});
test('config-get workflow.use_worktrees errors when not set (default config)', () => {
// config-ensure-section does NOT include use_worktrees in hardcoded defaults,
// so config-get should error with "Key not found". This is the expected behavior
// that workflows rely on: the shell fallback `|| echo "true"` provides the default.
const result = runGsdTools('config-get workflow.use_worktrees', tmpDir);
assert.strictEqual(result.success, false);
assert.ok(
result.error.includes('Key not found'),
`Expected "Key not found" in error: ${result.error}`
);
});
test('config-get workflow.use_worktrees returns true after setting to true', () => {
runGsdTools('config-set workflow.use_worktrees true', tmpDir);
const result = runGsdTools('config-get workflow.use_worktrees', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output, true);
});
test('use_worktrees can be toggled back and forth', () => {
runGsdTools('config-set workflow.use_worktrees false', tmpDir);
runGsdTools('config-set workflow.use_worktrees true', tmpDir);
const result = runGsdTools('config-get workflow.use_worktrees', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output, true);
});
});

View File

@@ -1174,6 +1174,8 @@ describe('E2E: Copilot full install verification', () => {
'gsd-assumptions-analyzer.agent.md',
'gsd-codebase-mapper.agent.md',
'gsd-debugger.agent.md',
'gsd-doc-verifier.agent.md',
'gsd-doc-writer.agent.md',
'gsd-executor.agent.md',
'gsd-integration-checker.agent.md',
'gsd-nyquist-auditor.agent.md',
@@ -1183,6 +1185,7 @@ describe('E2E: Copilot full install verification', () => {
'gsd-project-researcher.agent.md',
'gsd-research-synthesizer.agent.md',
'gsd-roadmapper.agent.md',
'gsd-security-auditor.agent.md',
'gsd-ui-auditor.agent.md',
'gsd-ui-checker.agent.md',
'gsd-ui-researcher.agent.md',

View File

@@ -366,6 +366,17 @@ describe('generateSlugInternal', () => {
test('returns null for empty string', () => {
assert.strictEqual(generateSlugInternal(''), null);
});
test('strips newlines and control characters', () => {
assert.strictEqual(generateSlugInternal('hello\nworld'), 'hello-world');
assert.strictEqual(generateSlugInternal('tab\there'), 'tab-here');
});
test('truncates to 60 characters', () => {
const long = 'a'.repeat(100);
const result = generateSlugInternal(long);
assert.ok(result.length <= 60, `slug should be <=60 chars, got ${result.length}`);
});
});
// ─── normalizePhaseName / comparePhaseNum ──────────────────────────────────────
@@ -996,19 +1007,54 @@ describe('stale hook filter', () => {
// ─── stale hook path regression (#1249) ──────────────────────────────────────
describe('stale hook path', () => {
test('gsd-check-update.js checks get-shit-done/hooks/ not configDir/hooks/', () => {
test('gsd-check-update.js checks configDir/hooks/ where hooks are actually installed (#1421)', () => {
const content = fs.readFileSync(
path.join(__dirname, '..', 'hooks', 'gsd-check-update.js'), 'utf-8'
);
// Hooks are installed at configDir/hooks/ (e.g. ~/.claude/hooks/),
// not configDir/get-shit-done/hooks/ which doesn't exist (#1421)
assert.ok(
content.includes("path.join(configDir, 'get-shit-done', 'hooks')"),
'stale hook check must look in configDir/get-shit-done/hooks/, not configDir/hooks/'
content.includes("path.join(configDir, 'hooks')"),
'stale hook check must look in configDir/hooks/ where hooks are actually installed'
);
});
});
// ─── shared cache directory regression (#1421) ─────────────────────────────────
describe('shared cache directory (#1421)', () => {
test('gsd-check-update.js writes cache to shared ~/.cache/gsd/ directory', () => {
const content = fs.readFileSync(
path.join(__dirname, '..', 'hooks', 'gsd-check-update.js'), 'utf-8'
);
// Cache must use a tool-agnostic path so statusline can find it
// regardless of which runtime (Claude, Gemini, OpenCode) ran the check
assert.ok(
!content.includes("path.join(configDir, 'hooks')") ||
content.indexOf("path.join(configDir, 'get-shit-done', 'hooks')") <
content.indexOf("path.join(configDir, 'hooks')") + 100, // allow the old pattern only if corrected version exists first
'should not use the wrong hooks path'
content.includes("path.join(homeDir, '.cache', 'gsd')"),
'check-update must write cache to ~/.cache/gsd/ (shared, tool-agnostic)'
);
});
test('gsd-statusline.js checks shared cache first, falls back to legacy (#1421)', () => {
const content = fs.readFileSync(
path.join(__dirname, '..', 'hooks', 'gsd-statusline.js'), 'utf-8'
);
// Statusline must check the shared cache path first
assert.ok(
content.includes("path.join(homeDir, '.cache', 'gsd', 'gsd-update-check.json')"),
'statusline must check shared cache at ~/.cache/gsd/gsd-update-check.json'
);
// Must fall back to legacy runtime-specific cache for backward compat
assert.ok(
content.includes("path.join(claudeDir, 'cache', 'gsd-update-check.json')"),
'statusline must fall back to legacy cache at claudeDir/cache/gsd-update-check.json'
);
// Shared cache must be checked before legacy (existsSync order matters)
const sharedIdx = content.indexOf('sharedCacheFile');
const legacyIdx = content.indexOf('legacyCacheFile');
assert.ok(
sharedIdx < legacyIdx,
'shared cache must be defined and checked before legacy cache'
);
});
});

View File

@@ -0,0 +1,72 @@
/**
* GSD Tools Tests - discuss-phase incremental checkpoint saves
*
* Validates that the discuss-phase workflow includes incremental
* checkpoint logic to prevent answer loss on session interruption.
*
* Closes: #1485
*/
const { test, describe } = require('node:test');
const assert = require('node:assert');
const fs = require('fs');
const path = require('path');
describe('discuss-phase incremental checkpoint saves (#1485)', () => {
const workflowPath = path.join(__dirname, '..', 'get-shit-done', 'workflows', 'discuss-phase.md');
test('workflow writes checkpoint file after each area completes', () => {
const content = fs.readFileSync(workflowPath, 'utf8');
assert.ok(
content.includes('DISCUSS-CHECKPOINT.json'),
'workflow should reference checkpoint JSON file'
);
assert.ok(
content.includes('Incremental checkpoint') || content.includes('incremental checkpoint'),
'workflow should describe incremental checkpoint saves'
);
});
test('checkpoint includes decisions, areas completed, and areas remaining', () => {
const content = fs.readFileSync(workflowPath, 'utf8');
assert.ok(content.includes('areas_completed'), 'checkpoint should track completed areas');
assert.ok(content.includes('areas_remaining'), 'checkpoint should track remaining areas');
assert.ok(content.includes('"decisions"'), 'checkpoint should include decisions object');
});
test('check_existing step detects checkpoint for session resume', () => {
const content = fs.readFileSync(workflowPath, 'utf8');
// The check_existing step should look for checkpoint files
assert.ok(
content.includes('DISCUSS-CHECKPOINT.json') && content.includes('Resume'),
'check_existing should detect checkpoint and offer resume'
);
});
test('checkpoint is cleaned up after successful CONTEXT.md write', () => {
const content = fs.readFileSync(workflowPath, 'utf8');
assert.ok(
content.includes('rm -f') && content.includes('DISCUSS-CHECKPOINT'),
'checkpoint file should be deleted after successful write_context'
);
});
test('success criteria include checkpoint requirements', () => {
const content = fs.readFileSync(workflowPath, 'utf8');
const criteriaMatch = content.match(/<success_criteria>([\s\S]*?)<\/success_criteria>/);
const criteria = criteriaMatch ? criteriaMatch[1] : '';
assert.ok(criteria.includes('checkpoint') || criteria.includes('Checkpoint'),
'success criteria should mention checkpoints');
assert.ok(criteria.includes('resume') || criteria.includes('Resume'),
'success criteria should mention session resume capability');
});
test('auto mode also writes checkpoints', () => {
const content = fs.readFileSync(workflowPath, 'utf8');
// The checkpoint section should mention auto mode
assert.ok(
content.includes('auto-resolves') || content.includes('--auto'),
'checkpoint logic should apply to both interactive and auto modes'
);
});
});

272
tests/docs-update.test.cjs Normal file
View File

@@ -0,0 +1,272 @@
/**
* GSD Tools Tests - docs-update
*
* Integration tests for the docs-init gsd-tools subcommand.
* Covers: JSON output shape, project type detection, existing doc scanning,
* GSD marker detection, and doc tooling detection.
*
* Requirements: VERF-03
*/
const { test, describe, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
// ─── JSON output shape ────────────────────────────────────────────────────────
describe('docs-init command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('returns expected JSON shape', () => {
const result = runGsdTools(['docs-init'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const data = JSON.parse(result.output);
// Top-level scalar fields
assert.strictEqual(typeof data.doc_writer_model, 'string');
assert.strictEqual(typeof data.commit_docs, 'boolean');
assert.strictEqual(typeof data.planning_exists, 'boolean');
assert.strictEqual(typeof data.project_root, 'string');
assert.strictEqual(typeof data.agents_installed, 'boolean');
// Array fields
assert.ok(Array.isArray(data.existing_docs), 'existing_docs should be an array');
assert.ok(Array.isArray(data.monorepo_workspaces), 'monorepo_workspaces should be an array');
assert.ok(Array.isArray(data.missing_agents), 'missing_agents should be an array');
// project_type object with 7 boolean fields
assert.ok(data.project_type && typeof data.project_type === 'object', 'project_type should be an object');
assert.strictEqual(typeof data.project_type.has_package_json, 'boolean');
assert.strictEqual(typeof data.project_type.has_api_routes, 'boolean');
assert.strictEqual(typeof data.project_type.has_cli_bin, 'boolean');
assert.strictEqual(typeof data.project_type.is_open_source, 'boolean');
assert.strictEqual(typeof data.project_type.has_deploy_config, 'boolean');
assert.strictEqual(typeof data.project_type.is_monorepo, 'boolean');
assert.strictEqual(typeof data.project_type.has_tests, 'boolean');
// doc_tooling object with 4 boolean fields
assert.ok(data.doc_tooling && typeof data.doc_tooling === 'object', 'doc_tooling should be an object');
assert.strictEqual(typeof data.doc_tooling.docusaurus, 'boolean');
assert.strictEqual(typeof data.doc_tooling.vitepress, 'boolean');
assert.strictEqual(typeof data.doc_tooling.mkdocs, 'boolean');
assert.strictEqual(typeof data.doc_tooling.storybook, 'boolean');
// planning_exists is true since createTempProject creates .planning/
assert.strictEqual(data.planning_exists, true);
});
test('bare project returns all false signals', () => {
const result = runGsdTools(['docs-init'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const data = JSON.parse(result.output);
// All project_type fields should be false for a bare project
assert.strictEqual(data.project_type.has_package_json, false);
assert.strictEqual(data.project_type.has_api_routes, false);
assert.strictEqual(data.project_type.has_cli_bin, false);
assert.strictEqual(data.project_type.is_open_source, false);
assert.strictEqual(data.project_type.has_deploy_config, false);
assert.strictEqual(data.project_type.is_monorepo, false);
assert.strictEqual(data.project_type.has_tests, false);
// No docs, no workspaces, no doc tooling
assert.deepEqual(data.existing_docs, []);
assert.deepEqual(data.monorepo_workspaces, []);
assert.strictEqual(data.doc_tooling.docusaurus, false);
assert.strictEqual(data.doc_tooling.vitepress, false);
assert.strictEqual(data.doc_tooling.mkdocs, false);
assert.strictEqual(data.doc_tooling.storybook, false);
});
});
// ─── project type detection ───────────────────────────────────────────────────
describe('project type detection', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('detects CLI tool from package.json bin field', () => {
fs.writeFileSync(
path.join(tmpDir, 'package.json'),
JSON.stringify({ name: 'my-cli', bin: { mycli: 'bin/cli.js' } }),
'utf-8'
);
const result = runGsdTools(['docs-init'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const data = JSON.parse(result.output);
assert.strictEqual(data.project_type.has_cli_bin, true);
assert.strictEqual(data.project_type.has_package_json, true);
});
test('detects open source from LICENSE file', () => {
fs.writeFileSync(path.join(tmpDir, 'LICENSE'), 'MIT License', 'utf-8');
const result = runGsdTools(['docs-init'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const data = JSON.parse(result.output);
assert.strictEqual(data.project_type.is_open_source, true);
});
test('detects monorepo from package.json workspaces', () => {
fs.writeFileSync(
path.join(tmpDir, 'package.json'),
JSON.stringify({ name: 'mono', workspaces: ['packages/*'] }),
'utf-8'
);
const result = runGsdTools(['docs-init'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const data = JSON.parse(result.output);
assert.strictEqual(data.project_type.is_monorepo, true);
assert.ok(data.monorepo_workspaces.includes('packages/*'), 'monorepo_workspaces should contain packages/*');
});
test('detects tests from tests directory', () => {
fs.mkdirSync(path.join(tmpDir, 'tests'), { recursive: true });
const result = runGsdTools(['docs-init'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const data = JSON.parse(result.output);
assert.strictEqual(data.project_type.has_tests, true);
});
test('detects deploy config from Dockerfile', () => {
fs.writeFileSync(path.join(tmpDir, 'Dockerfile'), 'FROM node:20', 'utf-8');
const result = runGsdTools(['docs-init'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const data = JSON.parse(result.output);
assert.strictEqual(data.project_type.has_deploy_config, true);
});
test('detects API routes from src/app/api directory', () => {
fs.mkdirSync(path.join(tmpDir, 'src', 'app', 'api'), { recursive: true });
const result = runGsdTools(['docs-init'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const data = JSON.parse(result.output);
assert.strictEqual(data.project_type.has_api_routes, true);
});
});
// ─── existing doc scanning ────────────────────────────────────────────────────
describe('existing doc scanning', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('scans .md files in project root', () => {
fs.writeFileSync(path.join(tmpDir, 'README.md'), '# README\n', 'utf-8');
fs.writeFileSync(path.join(tmpDir, 'ARCHITECTURE.md'), '# Architecture\n', 'utf-8');
const result = runGsdTools(['docs-init'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const data = JSON.parse(result.output);
assert.ok(data.existing_docs.length >= 2, 'existing_docs should contain at least 2 entries');
const paths = data.existing_docs.map(d => d.path);
assert.ok(paths.includes('README.md'), 'existing_docs should contain README.md');
assert.ok(paths.includes('ARCHITECTURE.md'), 'existing_docs should contain ARCHITECTURE.md');
});
test('detects GSD marker in existing docs', () => {
fs.writeFileSync(
path.join(tmpDir, 'README.md'),
'<!-- generated-by: gsd-doc-writer -->\n# README\n',
'utf-8'
);
fs.writeFileSync(path.join(tmpDir, 'NOTES.md'), '# Notes\n', 'utf-8');
const result = runGsdTools(['docs-init'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const data = JSON.parse(result.output);
const readmeEntry = data.existing_docs.find(d => d.path === 'README.md');
assert.ok(readmeEntry, 'README.md should appear in existing_docs');
assert.strictEqual(readmeEntry.has_gsd_marker, true, 'README.md should have GSD marker');
const notesEntry = data.existing_docs.find(d => d.path === 'NOTES.md');
assert.ok(notesEntry, 'NOTES.md should appear in existing_docs');
assert.strictEqual(notesEntry.has_gsd_marker, false, 'NOTES.md should not have GSD marker');
});
});
// ─── doc tooling detection ────────────────────────────────────────────────────
describe('doc tooling detection', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('detects Docusaurus config', () => {
fs.writeFileSync(path.join(tmpDir, 'docusaurus.config.js'), 'module.exports = {};', 'utf-8');
const result = runGsdTools(['docs-init'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const data = JSON.parse(result.output);
assert.strictEqual(data.doc_tooling.docusaurus, true);
});
test('detects VitePress config', () => {
fs.mkdirSync(path.join(tmpDir, '.vitepress'), { recursive: true });
fs.writeFileSync(path.join(tmpDir, '.vitepress', 'config.ts'), 'export default {};', 'utf-8');
const result = runGsdTools(['docs-init'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const data = JSON.parse(result.output);
assert.strictEqual(data.doc_tooling.vitepress, true);
});
test('detects MkDocs config', () => {
fs.writeFileSync(path.join(tmpDir, 'mkdocs.yml'), 'site_name: test', 'utf-8');
const result = runGsdTools(['docs-init'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const data = JSON.parse(result.output);
assert.strictEqual(data.doc_tooling.mkdocs, true);
});
});

View File

@@ -105,4 +105,80 @@ describe('execute-phase docs: user-facing wave flag', () => {
'help.md should include wave-filter usage'
);
});
test('workflow supports use_worktrees config toggle', () => {
const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8');
assert.ok(
content.includes('USE_WORKTREES'),
'workflow should reference USE_WORKTREES variable'
);
assert.ok(
content.includes('config-get workflow.use_worktrees'),
'workflow should read use_worktrees from config'
);
assert.ok(
content.includes('Sequential mode'),
'workflow should document sequential mode when worktrees disabled'
);
});
});
describe('use_worktrees config: cross-workflow structural coverage', () => {
const QUICK_PATH = path.join(__dirname, '..', 'get-shit-done', 'workflows', 'quick.md');
const DIAGNOSE_PATH = path.join(__dirname, '..', 'get-shit-done', 'workflows', 'diagnose-issues.md');
const EXECUTE_PLAN_PATH = path.join(__dirname, '..', 'get-shit-done', 'workflows', 'execute-plan.md');
const PLANNING_CONFIG_PATH = path.join(__dirname, '..', 'get-shit-done', 'references', 'planning-config.md');
const CONFIG_CJS_PATH = path.join(__dirname, '..', 'get-shit-done', 'bin', 'lib', 'config.cjs');
test('quick workflow reads USE_WORKTREES from config', () => {
const content = fs.readFileSync(QUICK_PATH, 'utf-8');
assert.ok(
content.includes('config-get workflow.use_worktrees'),
'quick.md should read use_worktrees from config'
);
assert.ok(
content.includes('USE_WORKTREES'),
'quick.md should reference USE_WORKTREES variable'
);
});
test('diagnose-issues workflow reads USE_WORKTREES from config', () => {
const content = fs.readFileSync(DIAGNOSE_PATH, 'utf-8');
assert.ok(
content.includes('config-get workflow.use_worktrees'),
'diagnose-issues.md should read use_worktrees from config'
);
assert.ok(
content.includes('USE_WORKTREES'),
'diagnose-issues.md should reference USE_WORKTREES variable'
);
});
test('execute-plan workflow references use_worktrees config', () => {
const content = fs.readFileSync(EXECUTE_PLAN_PATH, 'utf-8');
assert.ok(
content.includes('workflow.use_worktrees'),
'execute-plan.md should reference workflow.use_worktrees'
);
});
test('planning-config reference documents use_worktrees', () => {
const content = fs.readFileSync(PLANNING_CONFIG_PATH, 'utf-8');
assert.ok(
content.includes('workflow.use_worktrees'),
'planning-config.md should document workflow.use_worktrees'
);
assert.ok(
content.includes('worktree'),
'planning-config.md should describe worktree behavior'
);
});
test('config.cjs includes workflow.use_worktrees in VALID_CONFIG_KEYS', () => {
const content = fs.readFileSync(CONFIG_CJS_PATH, 'utf-8');
assert.ok(
content.includes("'workflow.use_worktrees'"),
'config.cjs VALID_CONFIG_KEYS should include workflow.use_worktrees'
);
});
});

View File

@@ -657,6 +657,92 @@ describe('phase add command', () => {
});
});
// ─────────────────────────────────────────────────────────────────────────────
// phase add with project_code prefix
// ─────────────────────────────────────────────────────────────────────────────
describe('phase add with project_code', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('prefixes phase directory with project_code', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'config.json'),
JSON.stringify({ project_code: 'CK' })
);
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
'# Roadmap v1.0\n\n### Phase 1: Foundation\n**Goal:** Setup\n\n---\n'
);
const result = runGsdTools('phase add User Dashboard', tmpDir, { HOME: tmpDir });
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.phase_number, 2, 'should be phase 2');
assert.ok(
fs.existsSync(path.join(tmpDir, '.planning', 'phases', 'CK-02-user-dashboard')),
'directory should have CK- prefix'
);
});
test('no prefix when project_code is null', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'config.json'),
JSON.stringify({ project_code: null })
);
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
'# Roadmap v1.0\n\n### Phase 1: Foundation\n**Goal:** Setup\n\n---\n'
);
const result = runGsdTools('phase add User Dashboard', tmpDir, { HOME: tmpDir });
assert.ok(result.success, `Command failed: ${result.error}`);
assert.ok(
fs.existsSync(path.join(tmpDir, '.planning', 'phases', '02-user-dashboard')),
'directory should have no prefix'
);
});
test('find-phase resolves prefixed directories', () => {
const phaseDir = path.join(tmpDir, '.planning', 'phases', 'CK-01-foundation');
fs.mkdirSync(phaseDir, { recursive: true });
fs.writeFileSync(path.join(phaseDir, '01-01-PLAN.md'), '# Plan');
const result = runGsdTools('find-phase 01', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.found, true, 'should find prefixed phase');
assert.strictEqual(output.phase_number, '01', 'should extract numeric phase number');
});
test('phases list sorts prefixed directories correctly', () => {
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', 'CK-02-api'), { recursive: true });
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', 'CK-01-foundation'), { recursive: true });
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', 'CK-03-ui'), { recursive: true });
const result = runGsdTools('phases list', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.deepStrictEqual(
output.directories,
['CK-01-foundation', 'CK-02-api', 'CK-03-ui'],
'prefixed phases should sort numerically'
);
});
});
// ─────────────────────────────────────────────────────────────────────────────
// phase insert command
// ─────────────────────────────────────────────────────────────────────────────
@@ -1533,6 +1619,130 @@ describe('phase complete command', () => {
// Verify compound format preserved
assert.ok(state.match(/Phase:.*of\s+1/), 'should preserve "of N" in compound Phase format');
});
test('updates Plans Complete column in 4-column progress table', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap
- [ ] Phase 1: Foundation
- [ ] Phase 2: API
### Phase 1: Foundation
**Goal:** Setup
**Plans:** 1 plans
### Phase 2: API
**Goal:** Build API
## Progress
| Phase | Plans Complete | Status | Completed |
|-------|----------------|--------|-----------|
| 1. Foundation | 0/1 | Not started | - |
| 2. API | 0/1 | Not started | - |
`
);
fs.writeFileSync(
path.join(tmpDir, '.planning', 'STATE.md'),
`# State\n\n**Current Phase:** 01\n**Status:** In progress\n**Current Plan:** 01-01\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n`
);
const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation');
fs.mkdirSync(p1, { recursive: true });
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-api'), { recursive: true });
const result = runGsdTools('phase complete 1', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
const rowMatch = roadmap.match(/^\|[^\n]*1\. Foundation[^\n]*$/m);
assert.ok(rowMatch, 'table row should exist');
const cells = rowMatch[0].split('|').slice(1, -1).map(c => c.trim());
assert.strictEqual(cells.length, 4, 'should have 4 columns');
assert.strictEqual(cells[1], '1/1', 'Plans Complete column should be updated to 1/1');
assert.ok(cells[2].includes('Complete'), 'Status column should be Complete');
});
test('updates Plans Complete column in 5-column progress table', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap
- [ ] Phase 1: Foundation
### Phase 1: Foundation
**Goal:** Setup
**Plans:** 1 plans
## Progress
| Phase | Milestone | Plans Complete | Status | Completed |
|-------|-----------|----------------|--------|-----------|
| 1. Foundation | v1.0 | 0/1 | Planned | |
`
);
fs.writeFileSync(
path.join(tmpDir, '.planning', 'STATE.md'),
`# State\n\n**Current Phase:** 01\n**Status:** In progress\n**Current Plan:** 01-01\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n`
);
const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation');
fs.mkdirSync(p1, { recursive: true });
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
const result = runGsdTools('phase complete 1', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
const rowMatch = roadmap.match(/^\|[^\n]*1\. Foundation[^\n]*$/m);
assert.ok(rowMatch, 'table row should exist');
const cells = rowMatch[0].split('|').slice(1, -1).map(c => c.trim());
assert.strictEqual(cells.length, 5, 'should have 5 columns');
assert.strictEqual(cells[2], '1/1', 'Plans Complete column should be updated to 1/1');
assert.ok(cells[3].includes('Complete'), 'Status column should be Complete');
});
test('marks plan-level checkboxes on phase complete', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap
- [ ] Phase 1: Foundation
### Phase 1: Foundation
**Goal:** Setup
**Plans:** 2 plans
Plans:
- [ ] 01-01-PLAN.md \u2014 Schema migration
- [ ] 01-02-PLAN.md \u2014 Auth setup
`
);
fs.writeFileSync(
path.join(tmpDir, '.planning', 'STATE.md'),
`# State\n\n**Current Phase:** 01\n**Status:** In progress\n**Current Plan:** 01-02\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n`
);
const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation');
fs.mkdirSync(p1, { recursive: true });
fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan');
fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary');
fs.writeFileSync(path.join(p1, '01-02-PLAN.md'), '# Plan');
fs.writeFileSync(path.join(p1, '01-02-SUMMARY.md'), '# Summary');
const result = runGsdTools('phase complete 1', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8');
assert.ok(roadmap.includes('[x] 01-01-PLAN.md'), 'plan 01-01 checkbox should be checked');
assert.ok(roadmap.includes('[x] 01-02-PLAN.md'), 'plan 01-02 checkbox should be checked');
assert.ok(!roadmap.includes('[ ] 01-01-PLAN.md'), 'plan 01-01 should not remain unchecked');
assert.ok(!roadmap.includes('[ ] 01-02-PLAN.md'), 'plan 01-02 should not remain unchecked');
});
});
// ─────────────────────────────────────────────────────────────────────────────

View File

@@ -0,0 +1,53 @@
/**
* GSD Quick Workflow — Commit Boundary Tests (#1503)
*
* Validates that the quick workflow correctly separates executor
* responsibilities (code commits) from orchestrator responsibilities
* (docs artifact commit), preventing PLAN.md from being left untracked
* when the executor runs without worktree isolation.
*/
const { test, describe } = require('node:test');
const assert = require('node:assert');
const fs = require('fs');
const path = require('path');
const WORKFLOWS_DIR = path.join(__dirname, '..', 'get-shit-done', 'workflows');
describe('quick workflow commit boundary (#1503)', () => {
const quickPath = path.join(WORKFLOWS_DIR, 'quick.md');
let content;
test('quick.md exists', () => {
assert.ok(fs.existsSync(quickPath), 'workflows/quick.md should exist');
content = fs.readFileSync(quickPath, 'utf-8');
});
test('executor constraints prohibit committing docs artifacts', () => {
assert.ok(
content.includes('Do NOT commit docs artifacts'),
'executor constraints should prohibit committing SUMMARY.md, STATE.md, PLAN.md'
);
});
test('Step 8 explicitly stages artifacts with git add before commit', () => {
assert.ok(
content.includes('git add ${file_list}'),
'Step 8 should explicitly git add the file list before gsd-tools commit'
);
});
test('Step 8 includes PLAN.md in file list', () => {
assert.ok(
content.includes('${QUICK_DIR}/${quick_id}-PLAN.md'),
'Step 8 file list must include PLAN.md'
);
});
test('Step 8 runs unconditionally', () => {
assert.ok(
content.includes('MUST always run'),
'Step 8 should state it must always run regardless of executor commits'
);
});
});

View File

@@ -279,19 +279,19 @@ describe('quick workflow: banner variants for flag combinations', () => {
);
});
test('has banner for research + full mode', () => {
test('has banner for research + validate mode', () => {
content = fs.readFileSync(path.join(WORKFLOWS_DIR, 'quick.md'), 'utf-8');
assert.ok(
content.includes('RESEARCH + FULL)'),
'should have banner for --research --full'
content.includes('RESEARCH + VALIDATE)'),
'should have banner for --research --validate'
);
});
test('has banner for all three flags', () => {
test('has banner for full mode (all phases)', () => {
content = fs.readFileSync(path.join(WORKFLOWS_DIR, 'quick.md'), 'utf-8');
assert.ok(
content.includes('DISCUSS + RESEARCH + FULL)'),
'should have banner for --discuss --research --full'
content.includes('QUICK TASK (FULL)'),
'should have banner for --full (all phases enabled)'
);
});
});

View File

@@ -0,0 +1,276 @@
/**
* GSD Tools Tests - reapply-patches backup logic
*
* Validates that saveLocalPatches() in the installer correctly detects
* user-modified files and saves pristine hashes for three-way merge.
*
* Closes: #1469
*/
const { test, describe, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
// ─── helpers ──────────────────────────────────────────────────────────────────
function sha256(content) {
return crypto.createHash('sha256').update(content).digest('hex');
}
function createTempDir() {
return fs.mkdtempSync(path.join(require('os').tmpdir(), 'gsd-patch-test-'));
}
function cleanup(dir) {
try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
}
/**
* Simulate what the installer does: create a manifest, modify a file,
* then run the saveLocalPatches detection logic.
*/
function simulateManifestAndPatch(configDir, files) {
// Create the GSD files
for (const [relPath, content] of Object.entries(files.original)) {
const fullPath = path.join(configDir, relPath);
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
fs.writeFileSync(fullPath, content);
}
// Create manifest with hashes of original files
const manifest = {
version: '1.0.0',
timestamp: new Date().toISOString(),
files: {}
};
for (const [relPath, content] of Object.entries(files.original)) {
manifest.files[relPath] = sha256(content);
}
fs.writeFileSync(
path.join(configDir, 'gsd-file-manifest.json'),
JSON.stringify(manifest, null, 2)
);
// Now modify files to simulate user edits
for (const [relPath, content] of Object.entries(files.modified || {})) {
fs.writeFileSync(path.join(configDir, relPath), content);
}
return manifest;
}
// ─── inline saveLocalPatches (mirrors install.js logic) ──────────────────────
function fileHash(filePath) {
const content = fs.readFileSync(filePath);
return crypto.createHash('sha256').update(content).digest('hex');
}
function saveLocalPatches(configDir) {
const PATCHES_DIR_NAME = 'gsd-local-patches';
const MANIFEST_NAME = 'gsd-file-manifest.json';
const manifestPath = path.join(configDir, MANIFEST_NAME);
if (!fs.existsSync(manifestPath)) return [];
let manifest;
try { manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); } catch { return []; }
const patchesDir = path.join(configDir, PATCHES_DIR_NAME);
const modified = [];
for (const [relPath, originalHash] of Object.entries(manifest.files || {})) {
const fullPath = path.join(configDir, relPath);
if (!fs.existsSync(fullPath)) continue;
const currentHash = fileHash(fullPath);
if (currentHash !== originalHash) {
const backupPath = path.join(patchesDir, relPath);
fs.mkdirSync(path.dirname(backupPath), { recursive: true });
fs.copyFileSync(fullPath, backupPath);
modified.push(relPath);
}
}
if (modified.length > 0) {
const meta = {
backed_up_at: new Date().toISOString(),
from_version: manifest.version,
from_manifest_timestamp: manifest.timestamp,
files: modified,
pristine_hashes: {}
};
for (const relPath of modified) {
meta.pristine_hashes[relPath] = manifest.files[relPath];
}
fs.writeFileSync(path.join(patchesDir, 'backup-meta.json'), JSON.stringify(meta, null, 2));
}
return modified;
}
// ─── tests ───────────────────────────────────────────────────────────────────
describe('saveLocalPatches — patch backup and pristine hash tracking (#1469)', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempDir();
});
afterEach(() => {
cleanup(tmpDir);
});
test('detects modified files and backs them up', () => {
simulateManifestAndPatch(tmpDir, {
original: {
'get-shit-done/workflows/execute-phase.md': '# Execute Phase\nOriginal content\n',
'get-shit-done/workflows/plan-phase.md': '# Plan Phase\nOriginal content\n',
},
modified: {
'get-shit-done/workflows/execute-phase.md': '# Execute Phase\nOriginal content\n\n## My Custom Step\nDo something special\n',
},
});
const result = saveLocalPatches(tmpDir);
assert.strictEqual(result.length, 1, 'should detect exactly one modified file');
assert.ok(result.includes('get-shit-done/workflows/execute-phase.md'));
// Verify backup exists
const backupPath = path.join(tmpDir, 'gsd-local-patches', 'get-shit-done/workflows/execute-phase.md');
assert.ok(fs.existsSync(backupPath), 'backup file should exist');
const backupContent = fs.readFileSync(backupPath, 'utf8');
assert.ok(backupContent.includes('My Custom Step'), 'backup should contain user modification');
});
test('backup-meta.json includes pristine_hashes for three-way merge', () => {
const originalContent = '# Execute Phase\nOriginal content\n';
simulateManifestAndPatch(tmpDir, {
original: {
'get-shit-done/workflows/execute-phase.md': originalContent,
},
modified: {
'get-shit-done/workflows/execute-phase.md': originalContent + '\n## Custom\n',
},
});
saveLocalPatches(tmpDir);
const metaPath = path.join(tmpDir, 'gsd-local-patches', 'backup-meta.json');
assert.ok(fs.existsSync(metaPath), 'backup-meta.json should exist');
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
// Verify pristine_hashes field exists and contains correct hash
assert.ok(meta.pristine_hashes, 'meta should have pristine_hashes field');
const expectedHash = sha256(originalContent);
assert.strictEqual(
meta.pristine_hashes['get-shit-done/workflows/execute-phase.md'],
expectedHash,
'pristine hash should match SHA-256 of original file content'
);
});
test('backup-meta.json includes from_version and from_manifest_timestamp', () => {
simulateManifestAndPatch(tmpDir, {
original: { 'get-shit-done/workflows/test.md': 'original' },
modified: { 'get-shit-done/workflows/test.md': 'modified' },
});
saveLocalPatches(tmpDir);
const meta = JSON.parse(fs.readFileSync(
path.join(tmpDir, 'gsd-local-patches', 'backup-meta.json'), 'utf8'
));
assert.strictEqual(meta.from_version, '1.0.0');
assert.ok(meta.from_manifest_timestamp, 'should have from_manifest_timestamp');
assert.ok(meta.backed_up_at, 'should have backed_up_at timestamp');
});
test('unmodified files are not backed up', () => {
simulateManifestAndPatch(tmpDir, {
original: {
'get-shit-done/workflows/a.md': 'content A',
'get-shit-done/workflows/b.md': 'content B',
},
// No modifications
});
const result = saveLocalPatches(tmpDir);
assert.strictEqual(result.length, 0, 'no files should be detected as modified');
assert.ok(!fs.existsSync(path.join(tmpDir, 'gsd-local-patches')), 'patches dir should not be created');
});
test('multiple modified files all get pristine hashes', () => {
simulateManifestAndPatch(tmpDir, {
original: {
'get-shit-done/workflows/a.md': 'original A',
'get-shit-done/workflows/b.md': 'original B',
'get-shit-done/workflows/c.md': 'original C',
},
modified: {
'get-shit-done/workflows/a.md': 'modified A',
'get-shit-done/workflows/b.md': 'modified B',
},
});
const result = saveLocalPatches(tmpDir);
assert.strictEqual(result.length, 2);
const meta = JSON.parse(fs.readFileSync(
path.join(tmpDir, 'gsd-local-patches', 'backup-meta.json'), 'utf8'
));
assert.strictEqual(Object.keys(meta.pristine_hashes).length, 2);
assert.strictEqual(meta.pristine_hashes['get-shit-done/workflows/a.md'], sha256('original A'));
assert.strictEqual(meta.pristine_hashes['get-shit-done/workflows/b.md'], sha256('original B'));
// c.md should NOT have a pristine hash (it wasn't modified)
assert.strictEqual(meta.pristine_hashes['get-shit-done/workflows/c.md'], undefined);
});
test('returns empty array when no manifest exists', () => {
const result = saveLocalPatches(tmpDir);
assert.strictEqual(result.length, 0);
});
test('returns empty array when manifest is malformed', () => {
fs.writeFileSync(path.join(tmpDir, 'gsd-file-manifest.json'), 'not json');
const result = saveLocalPatches(tmpDir);
assert.strictEqual(result.length, 0);
});
});
describe('reapply-patches workflow contract (#1469)', () => {
test('workflow file contains critical invariant about never skipping backed-up files', () => {
const workflowPath = path.join(__dirname, '..', 'commands', 'gsd', 'reapply-patches.md');
const content = fs.readFileSync(workflowPath, 'utf8');
// The workflow must explicitly state that "no custom content" is never valid
assert.ok(
content.includes('NEVER conclude "no custom content"') ||
content.includes('never a valid conclusion'),
'workflow must contain the critical invariant about never skipping backed-up files'
);
});
test('workflow file describes three-way merge strategy', () => {
const workflowPath = path.join(__dirname, '..', 'commands', 'gsd', 'reapply-patches.md');
const content = fs.readFileSync(workflowPath, 'utf8');
assert.ok(content.includes('three-way') || content.includes('Three-way'),
'workflow must describe three-way merge strategy');
assert.ok(content.includes('pristine'),
'workflow must reference pristine baseline for comparison');
});
test('workflow file describes git-aware detection path', () => {
const workflowPath = path.join(__dirname, '..', 'commands', 'gsd', 'reapply-patches.md');
const content = fs.readFileSync(workflowPath, 'utf8');
assert.ok(content.includes('git log') || content.includes('git -C'),
'workflow must describe git-based detection of user changes');
});
});

358
tests/schema-drift.test.cjs Normal file
View File

@@ -0,0 +1,358 @@
/**
* GSD Tools Tests - Schema Drift Detection
*
* Tests for schema-relevant file detection (plan-phase injection)
* and post-execution schema drift gate (execute-phase verification).
*/
const { test, describe, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const { createTempProject, createTempGitProject, cleanup, runGsdTools } = require('./helpers.cjs');
// ─── Unit: detectSchemaFiles ─────────────────────────────────────────────────
const { detectSchemaFiles, detectSchemaOrm, checkSchemaDrift } = require(
path.join(__dirname, '..', 'get-shit-done', 'bin', 'lib', 'schema-detect.cjs')
);
describe('detectSchemaFiles', () => {
test('detects Payload CMS collection files', () => {
const files = ['src/collections/Posts.ts', 'src/collections/Users.ts', 'src/lib/utils.ts'];
const result = detectSchemaFiles(files);
assert.ok(result.detected, 'should detect schema files');
assert.deepStrictEqual(result.matches, [
'src/collections/Posts.ts',
'src/collections/Users.ts',
]);
assert.ok(result.orms.includes('payload'), 'should identify Payload CMS');
});
test('detects Payload CMS globals files', () => {
const files = ['src/globals/Settings.ts'];
const result = detectSchemaFiles(files);
assert.ok(result.detected);
assert.ok(result.orms.includes('payload'));
});
test('detects Prisma schema file', () => {
const files = ['prisma/schema.prisma', 'src/index.ts'];
const result = detectSchemaFiles(files);
assert.ok(result.detected);
assert.deepStrictEqual(result.matches, ['prisma/schema.prisma']);
assert.ok(result.orms.includes('prisma'));
});
test('detects Prisma multi-file schema', () => {
const files = ['prisma/schema/user.prisma', 'prisma/schema/post.prisma'];
const result = detectSchemaFiles(files);
assert.ok(result.detected);
assert.strictEqual(result.matches.length, 2);
assert.ok(result.orms.includes('prisma'));
});
test('detects Drizzle schema files', () => {
const files = ['drizzle/schema.ts', 'src/routes/api.ts'];
const result = detectSchemaFiles(files);
assert.ok(result.detected);
assert.ok(result.orms.includes('drizzle'));
});
test('detects Drizzle schema in src/db/', () => {
const files = ['src/db/schema.ts'];
const result = detectSchemaFiles(files);
assert.ok(result.detected);
assert.ok(result.orms.includes('drizzle'));
});
test('detects Drizzle multi-file schemas', () => {
const files = ['drizzle/users.ts', 'drizzle/posts.ts'];
const result = detectSchemaFiles(files);
assert.ok(result.detected);
assert.ok(result.orms.includes('drizzle'));
});
test('detects Supabase migration files', () => {
const files = ['supabase/migrations/20240101_add_users.sql'];
const result = detectSchemaFiles(files);
assert.ok(result.detected);
assert.ok(result.orms.includes('supabase'));
});
test('detects TypeORM entity files', () => {
const files = ['src/entities/User.ts', 'src/entities/Post.ts'];
const result = detectSchemaFiles(files);
assert.ok(result.detected);
assert.ok(result.orms.includes('typeorm'));
});
test('detects TypeORM migration files', () => {
const files = ['src/migrations/1234567890-CreateUsers.ts'];
const result = detectSchemaFiles(files);
assert.ok(result.detected);
assert.ok(result.orms.includes('typeorm'));
});
test('returns not detected for non-schema files', () => {
const files = ['src/index.ts', 'src/utils/helpers.ts', 'package.json', 'README.md'];
const result = detectSchemaFiles(files);
assert.strictEqual(result.detected, false);
assert.strictEqual(result.matches.length, 0);
assert.strictEqual(result.orms.length, 0);
});
test('returns empty for empty file list', () => {
const result = detectSchemaFiles([]);
assert.strictEqual(result.detected, false);
assert.strictEqual(result.matches.length, 0);
});
test('detects multiple ORMs in same file list', () => {
const files = ['prisma/schema.prisma', 'src/collections/Posts.ts'];
const result = detectSchemaFiles(files);
assert.ok(result.detected);
assert.ok(result.orms.includes('prisma'));
assert.ok(result.orms.includes('payload'));
});
test('handles Windows-style paths', () => {
const files = ['src\\collections\\Posts.ts', 'src\\globals\\Settings.ts'];
const result = detectSchemaFiles(files);
assert.ok(result.detected, 'should detect schema files with backslash paths');
});
});
// ─── Unit: detectSchemaOrm ───────────────────────────────────────────────────
describe('detectSchemaOrm', () => {
test('returns push command for Payload CMS', () => {
const info = detectSchemaOrm('payload');
assert.ok(info.pushCommand);
assert.ok(info.pushCommand.includes('payload'));
assert.ok(info.envHint, 'should include env hint for non-TTY');
});
test('returns push command for Prisma', () => {
const info = detectSchemaOrm('prisma');
assert.ok(info.pushCommand.includes('prisma'));
});
test('returns push command for Drizzle', () => {
const info = detectSchemaOrm('drizzle');
assert.ok(info.pushCommand.includes('drizzle'));
});
test('returns push command for Supabase', () => {
const info = detectSchemaOrm('supabase');
assert.ok(info.pushCommand.includes('supabase'));
});
test('returns push command for TypeORM', () => {
const info = detectSchemaOrm('typeorm');
assert.ok(info.pushCommand.includes('typeorm'));
});
test('returns null for unknown ORM', () => {
const info = detectSchemaOrm('unknown-orm');
assert.strictEqual(info, null);
});
});
// ─── Unit: checkSchemaDrift ──────────────────────────────────────────────────
describe('checkSchemaDrift', () => {
test('returns no drift when no schema files changed', () => {
const changedFiles = ['src/index.ts', 'package.json'];
const executionLog = '';
const result = checkSchemaDrift(changedFiles, executionLog);
assert.strictEqual(result.driftDetected, false);
assert.strictEqual(result.blocking, false);
});
test('detects drift when schema files changed but no push executed', () => {
const changedFiles = ['src/collections/Posts.ts', 'src/index.ts'];
const executionLog = 'npm run build\nnpm run test';
const result = checkSchemaDrift(changedFiles, executionLog);
assert.strictEqual(result.driftDetected, true);
assert.strictEqual(result.blocking, true);
assert.ok(result.schemaFiles.length > 0);
assert.ok(result.orms.includes('payload'));
assert.ok(result.message.length > 0);
});
test('no drift when schema files changed AND push was executed (payload)', () => {
const changedFiles = ['src/collections/Posts.ts'];
const executionLog = 'npx payload migrate\nnpm run build';
const result = checkSchemaDrift(changedFiles, executionLog);
assert.strictEqual(result.driftDetected, false);
assert.strictEqual(result.blocking, false);
});
test('no drift when schema files changed AND push was executed (prisma)', () => {
const changedFiles = ['prisma/schema.prisma'];
const executionLog = 'npx prisma db push\nnpm run build';
const result = checkSchemaDrift(changedFiles, executionLog);
assert.strictEqual(result.driftDetected, false);
assert.strictEqual(result.blocking, false);
});
test('no drift when schema files changed AND push was executed (drizzle)', () => {
const changedFiles = ['drizzle/schema.ts'];
const executionLog = 'npx drizzle-kit push\nnpm run test';
const result = checkSchemaDrift(changedFiles, executionLog);
assert.strictEqual(result.driftDetected, false);
assert.strictEqual(result.blocking, false);
});
test('no drift when schema files changed AND push was executed (supabase)', () => {
const changedFiles = ['supabase/migrations/001_init.sql'];
const executionLog = 'supabase db push\nnpm run test';
const result = checkSchemaDrift(changedFiles, executionLog);
assert.strictEqual(result.driftDetected, false);
assert.strictEqual(result.blocking, false);
});
test('no drift when schema files changed AND push was executed (typeorm)', () => {
const changedFiles = ['src/entities/User.ts'];
const executionLog = 'npx typeorm migration:run\nnpm run test';
const result = checkSchemaDrift(changedFiles, executionLog);
assert.strictEqual(result.driftDetected, false);
assert.strictEqual(result.blocking, false);
});
test('respects GSD_SKIP_SCHEMA_CHECK override', () => {
const changedFiles = ['src/collections/Posts.ts'];
const executionLog = 'npm run build';
const result = checkSchemaDrift(changedFiles, executionLog, { skipCheck: true });
assert.strictEqual(result.driftDetected, true);
assert.strictEqual(result.blocking, false, 'should not block when skip override is set');
assert.ok(result.skipped, 'should indicate the check was skipped');
});
test('detects drift with multiple ORMs and partial push', () => {
const changedFiles = ['prisma/schema.prisma', 'src/collections/Posts.ts'];
const executionLog = 'npx prisma db push';
const result = checkSchemaDrift(changedFiles, executionLog);
// Prisma was pushed but Payload was not
assert.strictEqual(result.driftDetected, true);
assert.strictEqual(result.blocking, true);
assert.ok(result.unpushedOrms.includes('payload'));
assert.ok(!result.unpushedOrms.includes('prisma'));
});
test('includes actionable message with push commands', () => {
const changedFiles = ['prisma/schema.prisma'];
const executionLog = '';
const result = checkSchemaDrift(changedFiles, executionLog);
assert.ok(result.message.includes('prisma'));
});
});
// ─── CLI: verify schema-drift ────────────────────────────────────────────────
describe('verify schema-drift CLI command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempGitProject('gsd-schema-drift-');
});
afterEach(() => {
cleanup(tmpDir);
});
test('passes when no schema files in phase diff', () => {
// Create a phase dir with a plan that modifies non-schema files
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-setup');
fs.mkdirSync(phaseDir, { recursive: true });
fs.writeFileSync(path.join(phaseDir, '01-01-PLAN.md'), [
'---',
'files_modified: [src/index.ts, src/utils.ts]',
'---',
'',
'Plan content',
].join('\n'));
const result = runGsdTools(['verify', 'schema-drift', '01-setup'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.drift_detected, false);
assert.strictEqual(output.blocking, false);
});
test('detects drift when schema files in plan but no push evidence', () => {
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-setup');
fs.mkdirSync(phaseDir, { recursive: true });
fs.writeFileSync(path.join(phaseDir, '01-01-PLAN.md'), [
'---',
'files_modified: [src/collections/Posts.ts, src/index.ts]',
'---',
'',
'Plan content',
].join('\n'));
// No SUMMARY.md with push evidence
fs.writeFileSync(path.join(phaseDir, '01-01-SUMMARY.md'), [
'# Summary',
'',
'## Accomplishments',
'- Added Post collection',
'',
'## Commands Run',
'- npm run build',
'- npm run test',
].join('\n'));
const result = runGsdTools(['verify', 'schema-drift', '01-setup'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.drift_detected, true);
assert.strictEqual(output.blocking, true);
});
test('passes when schema files in plan AND push evidence in summary', () => {
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-setup');
fs.mkdirSync(phaseDir, { recursive: true });
fs.writeFileSync(path.join(phaseDir, '01-01-PLAN.md'), [
'---',
'files_modified: [src/collections/Posts.ts]',
'---',
'',
'Plan content',
].join('\n'));
fs.writeFileSync(path.join(phaseDir, '01-01-SUMMARY.md'), [
'# Summary',
'',
'## Accomplishments',
'- Added Post collection',
'',
'## Commands Run',
'- npx payload migrate',
'- npm run build',
].join('\n'));
const result = runGsdTools(['verify', 'schema-drift', '01-setup'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.drift_detected, false);
assert.strictEqual(output.blocking, false);
});
test('respects skip flag', () => {
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-setup');
fs.mkdirSync(phaseDir, { recursive: true });
fs.writeFileSync(path.join(phaseDir, '01-01-PLAN.md'), [
'---',
'files_modified: [src/collections/Posts.ts]',
'---',
'',
'Plan content',
].join('\n'));
fs.writeFileSync(path.join(phaseDir, '01-01-SUMMARY.md'), '# Summary\n');
const result = runGsdTools(['verify', 'schema-drift', '01-setup', '--skip'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.blocking, false);
});
});

440
tests/secure-phase.test.cjs Normal file
View File

@@ -0,0 +1,440 @@
/**
* GSD Secure-Phase Tests
*
* Validates the security-first enforcement layer:
* - gsd-security-auditor agent frontmatter and structure
* - secure-phase command file
* - secure-phase workflow file
* - SECURITY.md template
* - config.json security defaults
* - VALIDATION.md security columns
* - Threat-model-anchored behaviour (structural)
*/
const { test, describe } = require('node:test');
const assert = require('node:assert');
const fs = require('fs');
const path = require('path');
const REPO_ROOT = path.join(__dirname, '..');
const AGENTS_DIR = path.join(REPO_ROOT, 'agents');
const COMMANDS_DIR = path.join(REPO_ROOT, 'commands', 'gsd');
const WORKFLOWS_DIR = path.join(REPO_ROOT, 'get-shit-done', 'workflows');
const TEMPLATES_DIR = path.join(REPO_ROOT, 'get-shit-done', 'templates');
// ─── 1. Agent frontmatter — gsd-security-auditor.md ─────────────────────────
describe('SECURE: gsd-security-auditor agent', () => {
const agentPath = path.join(AGENTS_DIR, 'gsd-security-auditor.md');
test('agent file exists', () => {
assert.ok(
fs.existsSync(agentPath),
'gsd-security-auditor.md must exist in agents/'
);
});
test('has valid frontmatter with name, description, tools, color', () => {
const content = fs.readFileSync(agentPath, 'utf-8');
const frontmatter = content.split('---')[1] || '';
assert.ok(frontmatter.includes('name:'), 'missing name:');
assert.ok(frontmatter.includes('description:'), 'missing description:');
assert.ok(frontmatter.includes('tools:'), 'missing tools:');
assert.ok(frontmatter.includes('color:'), 'missing color:');
});
test('name is gsd-security-auditor', () => {
const content = fs.readFileSync(agentPath, 'utf-8');
const frontmatter = content.split('---')[1] || '';
assert.ok(
frontmatter.includes('name: gsd-security-auditor'),
'name must be gsd-security-auditor'
);
});
test('tools include Read, Write, Bash, Glob, Grep', () => {
const content = fs.readFileSync(agentPath, 'utf-8');
const requiredTools = ['Read', 'Write', 'Bash', 'Glob', 'Grep'];
for (const tool of requiredTools) {
assert.ok(
content.includes(`- ${tool}`),
`tools must include ${tool}`
);
}
});
test('has <role> section', () => {
const content = fs.readFileSync(agentPath, 'utf-8');
assert.ok(content.includes('<role>'), 'must have <role> section');
assert.ok(content.includes('</role>'), 'must close <role> section');
});
test('has <execution_flow> section', () => {
const content = fs.readFileSync(agentPath, 'utf-8');
assert.ok(content.includes('<execution_flow>'), 'must have <execution_flow> section');
assert.ok(content.includes('</execution_flow>'), 'must close <execution_flow> section');
});
test('has <structured_returns> with SECURED, OPEN_THREATS, ESCALATE', () => {
const content = fs.readFileSync(agentPath, 'utf-8');
assert.ok(content.includes('<structured_returns>'), 'must have <structured_returns> section');
assert.ok(content.includes('## SECURED'), 'must have SECURED return type');
assert.ok(content.includes('## OPEN_THREATS'), 'must have OPEN_THREATS return type');
assert.ok(content.includes('## ESCALATE'), 'must have ESCALATE return type');
});
test('has <success_criteria> section', () => {
const content = fs.readFileSync(agentPath, 'utf-8');
assert.ok(content.includes('<success_criteria>'), 'must have <success_criteria> section');
assert.ok(content.includes('</success_criteria>'), 'must close <success_criteria> section');
});
test('has READ-ONLY rule — does NOT modify implementation files', () => {
const content = fs.readFileSync(agentPath, 'utf-8');
assert.ok(
content.includes('READ-ONLY'),
'must contain READ-ONLY rule for implementation files'
);
});
});
// ─── 2. Command file — secure-phase.md ──────────────────────────────────────
describe('SECURE: secure-phase command file', () => {
const cmdPath = path.join(COMMANDS_DIR, 'secure-phase.md');
test('command file exists', () => {
assert.ok(
fs.existsSync(cmdPath),
'secure-phase.md must exist in commands/gsd/'
);
});
test('has valid frontmatter with name gsd:secure-phase', () => {
const content = fs.readFileSync(cmdPath, 'utf-8');
const frontmatter = content.split('---')[1] || '';
assert.ok(
frontmatter.includes('name: gsd:secure-phase'),
'name must be gsd:secure-phase'
);
});
test('has allowed-tools list', () => {
const content = fs.readFileSync(cmdPath, 'utf-8');
const frontmatter = content.split('---')[1] || '';
assert.ok(
frontmatter.includes('allowed-tools:'),
'must have allowed-tools in frontmatter'
);
});
test('contains reference to secure-phase.md workflow', () => {
const content = fs.readFileSync(cmdPath, 'utf-8');
assert.ok(
content.includes('secure-phase.md'),
'must reference secure-phase.md workflow'
);
});
test('has <objective> section mentioning states A, B, C', () => {
const content = fs.readFileSync(cmdPath, 'utf-8');
assert.ok(content.includes('<objective>'), 'must have <objective> section');
assert.ok(content.includes('(A)'), 'must mention state A');
assert.ok(content.includes('(B)'), 'must mention state B');
assert.ok(content.includes('(C)'), 'must mention state C');
});
});
// ─── 3. Workflow file — secure-phase.md ─────────────────────────────────────
describe('SECURE: secure-phase workflow file', () => {
const wfPath = path.join(WORKFLOWS_DIR, 'secure-phase.md');
test('workflow file exists', () => {
assert.ok(
fs.existsSync(wfPath),
'secure-phase.md must exist in get-shit-done/workflows/'
);
});
test('contains gsd-security-auditor reference', () => {
const content = fs.readFileSync(wfPath, 'utf-8');
assert.ok(
content.includes('gsd-security-auditor'),
'must reference gsd-security-auditor agent'
);
});
test('contains threats_open enforcement logic', () => {
const content = fs.readFileSync(wfPath, 'utf-8');
assert.ok(
content.includes('threats_open'),
'must contain threats_open enforcement logic'
);
});
test('contains security_enforcement config check', () => {
const content = fs.readFileSync(wfPath, 'utf-8');
assert.ok(
content.includes('security_enforcement'),
'must check security_enforcement config setting'
);
});
test('contains SECURITY.md template reference', () => {
const content = fs.readFileSync(wfPath, 'utf-8');
assert.ok(
content.includes('SECURITY.md'),
'must reference SECURITY.md template'
);
});
test('has success_criteria section', () => {
const content = fs.readFileSync(wfPath, 'utf-8');
assert.ok(
content.includes('<success_criteria>'),
'must have <success_criteria> section'
);
assert.ok(
content.includes('</success_criteria>'),
'must close <success_criteria> section'
);
});
});
// ─── 4. SECURITY.md template ────────────────────────────────────────────────
describe('SECURE: SECURITY.md template', () => {
const tplPath = path.join(TEMPLATES_DIR, 'SECURITY.md');
test('template exists', () => {
assert.ok(
fs.existsSync(tplPath),
'SECURITY.md must exist in get-shit-done/templates/'
);
});
test('has YAML frontmatter with required fields', () => {
const content = fs.readFileSync(tplPath, 'utf-8');
const frontmatter = content.split('---')[1] || '';
const requiredFields = ['phase', 'slug', 'status', 'threats_open', 'asvs_level', 'created'];
for (const field of requiredFields) {
assert.ok(
frontmatter.includes(`${field}:`),
`frontmatter must have ${field}: field`
);
}
});
test('has ## Trust Boundaries section', () => {
const content = fs.readFileSync(tplPath, 'utf-8');
assert.ok(
content.includes('## Trust Boundaries'),
'must have ## Trust Boundaries section'
);
});
test('has ## Threat Register table with required columns', () => {
const content = fs.readFileSync(tplPath, 'utf-8');
assert.ok(content.includes('## Threat Register'), 'must have ## Threat Register section');
const requiredColumns = ['Threat ID', 'Category', 'Component', 'Disposition', 'Mitigation', 'Status'];
for (const col of requiredColumns) {
assert.ok(
content.includes(col),
`Threat Register table must have ${col} column`
);
}
});
test('has ## Accepted Risks Log section', () => {
const content = fs.readFileSync(tplPath, 'utf-8');
assert.ok(
content.includes('## Accepted Risks Log'),
'must have ## Accepted Risks Log section'
);
});
test('has ## Security Audit Trail section', () => {
const content = fs.readFileSync(tplPath, 'utf-8');
assert.ok(
content.includes('## Security Audit Trail'),
'must have ## Security Audit Trail section'
);
});
test('has sign-off checklist', () => {
const content = fs.readFileSync(tplPath, 'utf-8');
assert.ok(
content.includes('## Sign-Off'),
'must have ## Sign-Off section'
);
assert.ok(
content.includes('- [ ]'),
'sign-off must have checklist items'
);
});
test('threats_open field is present (terminal condition field)', () => {
const content = fs.readFileSync(tplPath, 'utf-8');
const frontmatter = content.split('---')[1] || '';
assert.ok(
frontmatter.includes('threats_open:'),
'threats_open must be present in frontmatter as terminal condition field'
);
});
});
// ─── 5. Config defaults ─────────────────────────────────────────────────────
describe('SECURE: config.json security defaults', () => {
const configPath = path.join(TEMPLATES_DIR, 'config.json');
test('config template exists', () => {
assert.ok(
fs.existsSync(configPath),
'config.json must exist in get-shit-done/templates/'
);
});
test('has workflow.security_enforcement set to true', () => {
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
assert.strictEqual(
config.workflow.security_enforcement,
true,
'security_enforcement must default to true'
);
});
test('has workflow.security_asvs_level set to 1', () => {
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
assert.strictEqual(
config.workflow.security_asvs_level,
1,
'security_asvs_level must default to 1'
);
});
test('has workflow.security_block_on set to "high"', () => {
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
assert.strictEqual(
config.workflow.security_block_on,
'high',
'security_block_on must default to "high"'
);
});
test('security_enforcement appears after nyquist_validation (opt-out pattern parity)', () => {
const raw = fs.readFileSync(configPath, 'utf-8');
const nyquistPos = raw.indexOf('nyquist_validation');
const securityPos = raw.indexOf('security_enforcement');
assert.ok(nyquistPos > -1, 'nyquist_validation must exist in config');
assert.ok(securityPos > -1, 'security_enforcement must exist in config');
assert.ok(
securityPos > nyquistPos,
'security_enforcement must appear after nyquist_validation for opt-out pattern parity'
);
});
});
// ─── 6. VALIDATION.md template security columns ────────────────────────────
describe('SECURE: VALIDATION.md security columns', () => {
const valPath = path.join(TEMPLATES_DIR, 'VALIDATION.md');
test('VALIDATION.md template exists', () => {
assert.ok(
fs.existsSync(valPath),
'VALIDATION.md must exist in get-shit-done/templates/'
);
});
test('contains Threat Ref column header', () => {
const content = fs.readFileSync(valPath, 'utf-8');
assert.ok(
content.includes('Threat Ref'),
'must have Threat Ref column in Per-Task Verification Map'
);
});
test('contains Secure Behavior column header', () => {
const content = fs.readFileSync(valPath, 'utf-8');
assert.ok(
content.includes('Secure Behavior'),
'must have Secure Behavior column in Per-Task Verification Map'
);
});
test('both columns appear in the Per-Task Verification Map table', () => {
const content = fs.readFileSync(valPath, 'utf-8');
// Find the table header row containing both columns
const lines = content.split('\n');
const headerLine = lines.find(
line => line.includes('Threat Ref') && line.includes('Secure Behavior')
);
assert.ok(
headerLine,
'Threat Ref and Secure Behavior must appear in the same table header row'
);
// Verify this is in the Per-Task Verification Map section
const mapIdx = content.indexOf('## Per-Task Verification Map');
const threatRefIdx = content.indexOf('Threat Ref');
assert.ok(mapIdx > -1, 'must have Per-Task Verification Map section');
assert.ok(
threatRefIdx > mapIdx,
'Threat Ref column must appear after Per-Task Verification Map heading'
);
});
});
// ─── 7. Threat-model-anchored behaviour (structural) ────────────────────────
describe('SECURE: threat-model-anchored behaviour', () => {
const agentPath = path.join(AGENTS_DIR, 'gsd-security-auditor.md');
const wfPath = path.join(WORKFLOWS_DIR, 'secure-phase.md');
test('agent does NOT contain "scan for vulnerabilities" (verifies, not scans)', () => {
const content = fs.readFileSync(agentPath, 'utf-8');
assert.ok(
!content.toLowerCase().includes('scan for vulnerabilities'),
'agent must NOT scan for vulnerabilities — it verifies threat mitigations'
);
});
test('agent does NOT contain "find vulnerabilities" (verifies, not scans)', () => {
const content = fs.readFileSync(agentPath, 'utf-8');
assert.ok(
!content.toLowerCase().includes('find vulnerabilities'),
'agent must NOT find vulnerabilities — it verifies threat mitigations'
);
});
test('agent contains mitigate, accept, transfer disposition types', () => {
const content = fs.readFileSync(agentPath, 'utf-8');
assert.ok(content.includes('mitigate'), 'must contain mitigate disposition');
assert.ok(content.includes('accept'), 'must contain accept disposition');
assert.ok(content.includes('transfer'), 'must contain transfer disposition');
});
test('agent contains OPEN and CLOSED status values', () => {
const content = fs.readFileSync(agentPath, 'utf-8');
assert.ok(content.includes('OPEN'), 'must contain OPEN status');
assert.ok(content.includes('CLOSED'), 'must contain CLOSED status');
});
test('workflow contains enforcing gate (threats_open + block pattern)', () => {
const content = fs.readFileSync(wfPath, 'utf-8');
assert.ok(
content.includes('threats_open'),
'workflow must reference threats_open for enforcement'
);
assert.ok(
content.includes('BLOCKED') || content.includes('blocked'),
'workflow must contain a blocking pattern when threats are open'
);
// Verify it does NOT emit next-phase routing when blocked
assert.ok(
content.includes('Do NOT emit next-phase routing'),
'workflow must explicitly prevent next-phase routing when blocked'
);
});
});

View File

@@ -0,0 +1,186 @@
/**
* GSD Tools Tests - settings.json JSONC (JSON with comments) support
*
* Validates that the installer's readSettings() correctly handles
* settings.json files containing comments (line and block) without
* silently overwriting them with empty objects.
*
* Closes: #1461
*/
const { test, describe, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert');
const fs = require('fs');
const path = require('path');
// ─── inline stripJsonComments (mirrors install.js logic) ─────────────────────
function stripJsonComments(text) {
let result = '';
let i = 0;
let inString = false;
let stringChar = '';
while (i < text.length) {
if (inString) {
if (text[i] === '\\') {
result += text[i] + (text[i + 1] || '');
i += 2;
continue;
}
if (text[i] === stringChar) {
inString = false;
}
result += text[i];
i++;
continue;
}
if (text[i] === '"' || text[i] === "'") {
inString = true;
stringChar = text[i];
result += text[i];
i++;
continue;
}
if (text[i] === '/' && text[i + 1] === '/') {
while (i < text.length && text[i] !== '\n') i++;
continue;
}
if (text[i] === '/' && text[i + 1] === '*') {
i += 2;
while (i < text.length && !(text[i] === '*' && text[i + 1] === '/')) i++;
i += 2;
continue;
}
result += text[i];
i++;
}
return result.replace(/,\s*([}\]])/g, '$1');
}
// ─── tests ───────────────────────────────────────────────────────────────────
describe('stripJsonComments (#1461)', () => {
test('strips line comments', () => {
const input = `{
// This is a comment
"key": "value"
}`;
const result = JSON.parse(stripJsonComments(input));
assert.deepStrictEqual(result, { key: 'value' });
});
test('strips block comments', () => {
const input = `{
/* Block comment */
"key": "value"
}`;
const result = JSON.parse(stripJsonComments(input));
assert.deepStrictEqual(result, { key: 'value' });
});
test('strips multi-line block comments', () => {
const input = `{
/*
* Multi-line
* block comment
*/
"key": "value"
}`;
const result = JSON.parse(stripJsonComments(input));
assert.deepStrictEqual(result, { key: 'value' });
});
test('preserves comments inside string values', () => {
const input = `{
"url": "https://example.com/path",
"description": "Use // for line comments"
}`;
const result = JSON.parse(stripJsonComments(input));
assert.strictEqual(result.url, 'https://example.com/path');
assert.strictEqual(result.description, 'Use // for line comments');
});
test('handles trailing commas', () => {
const input = `{
"a": 1,
"b": 2,
}`;
const result = JSON.parse(stripJsonComments(input));
assert.deepStrictEqual(result, { a: 1, b: 2 });
});
test('handles inline comments after values', () => {
const input = `{
"timeout": 5000, // milliseconds
"retries": 3 // max attempts
}`;
const result = JSON.parse(stripJsonComments(input));
assert.strictEqual(result.timeout, 5000);
assert.strictEqual(result.retries, 3);
});
test('handles standard JSON (no comments) unchanged', () => {
const input = '{"key": "value", "num": 42}';
const result = JSON.parse(stripJsonComments(input));
assert.deepStrictEqual(result, { key: 'value', num: 42 });
});
test('handles empty object', () => {
const result = JSON.parse(stripJsonComments('{}'));
assert.deepStrictEqual(result, {});
});
test('handles real-world settings.json with comments', () => {
const input = `{
// My configuration
"hooks": {
"SessionStart": [
{
"matcher": "", /* match all */
"hooks": [
{
"type": "command",
"command": "node ~/.claude/hooks/gsd-statusline.js"
}
]
}
]
},
"statusLine": {
"command": "node ~/.claude/hooks/gsd-statusline.js",
"refreshInterval": 10
}
}`;
const result = JSON.parse(stripJsonComments(input));
assert.ok(result.hooks, 'should have hooks');
assert.ok(result.statusLine, 'should have statusLine');
assert.strictEqual(result.statusLine.refreshInterval, 10);
});
});
describe('readSettings null return on malformed files (#1461)', () => {
test('install.js contains JSONC stripping in readSettings', () => {
const installPath = path.join(__dirname, '..', 'bin', 'install.js');
const content = fs.readFileSync(installPath, 'utf8');
assert.ok(content.includes('stripJsonComments'),
'install.js should use stripJsonComments in readSettings');
});
test('readSettings returns null on truly malformed files (not empty object)', () => {
const installPath = path.join(__dirname, '..', 'bin', 'install.js');
const content = fs.readFileSync(installPath, 'utf8');
assert.ok(content.includes('return null'),
'readSettings should return null on parse failure, not empty object');
});
test('callers guard against null readSettings return', () => {
const installPath = path.join(__dirname, '..', 'bin', 'install.js');
const content = fs.readFileSync(installPath, 'utf8');
// Should have null guards at the settings configuration call sites
assert.ok(
content.includes('=== null') || content.includes('rawSettings === null'),
'callers should check for null return from readSettings'
);
});
});

View File

@@ -105,10 +105,11 @@ describe('convertClaudeToWindsurfMarkdown', () => {
assert.ok(!result.includes('Claude Code'), 'original brand removed');
});
test('replaces CLAUDE.md with .windsurf/rules/', () => {
test('replaces CLAUDE.md with .windsurf/rules (no trailing slash)', () => {
const input = 'See `CLAUDE.md` for configuration. Also check ./CLAUDE.md file.';
const result = convertClaudeToWindsurfMarkdown(input);
assert.ok(result.includes('.windsurf/rules/'), 'CLAUDE.md replaced');
assert.ok(result.includes('.windsurf/rules'), 'CLAUDE.md replaced');
assert.ok(!result.includes('.windsurf/rules/'), 'no trailing slash (Node v25 compat)');
});
test('replaces .claude/skills/ with .windsurf/skills/', () => {

View File

@@ -268,6 +268,24 @@ describe('workstream set/get', () => {
assert.ok(result.success);
assert.strictEqual(result.output, 'ws-a');
});
test('errors when set called with no name (#1527)', () => {
const result = runGsdTools(['workstream', 'set', '--raw'], tmpDir);
assert.ok(!result.success, 'should fail when no name provided');
assert.ok(result.error.includes('name required'), 'error should mention name required');
});
test('--clear explicitly unsets active workstream', () => {
// First set one
runGsdTools(['workstream', 'set', 'ws-b', '--raw'], tmpDir);
// Then clear
const result = runGsdTools(['workstream', 'set', '--clear', '--raw'], tmpDir);
assert.ok(result.success);
const data = JSON.parse(result.output);
assert.strictEqual(data.active, null);
assert.strictEqual(data.cleared, true);
assert.strictEqual(data.previous, 'ws-b');
});
});
// ─── Collision Detection ────────────────────────────────────────────────────

View File

@@ -0,0 +1,70 @@
/**
* GSD Tools Tests - worktree cleanup after executor completes
*
* Validates that execute-phase.md and quick.md include post-execution
* worktree cleanup logic (merge branch, remove worktree, delete branch).
*
* Closes: #1496
*/
const { test, describe } = require('node:test');
const assert = require('node:assert');
const fs = require('fs');
const path = require('path');
describe('worktree cleanup after executor completes (#1496)', () => {
const executePhasePath = path.join(__dirname, '..', 'get-shit-done', 'workflows', 'execute-phase.md');
const quickPath = path.join(__dirname, '..', 'get-shit-done', 'workflows', 'quick.md');
test('execute-phase.md includes worktree cleanup step', () => {
const content = fs.readFileSync(executePhasePath, 'utf8');
assert.ok(content.includes('Worktree cleanup'),
'execute-phase should have a worktree cleanup step');
assert.ok(content.includes('git worktree remove'),
'cleanup should remove worktrees');
assert.ok(content.includes('git branch -D'),
'cleanup should delete temporary branches');
});
test('execute-phase.md merges worktree branch before removing', () => {
const content = fs.readFileSync(executePhasePath, 'utf8');
assert.ok(content.includes('git merge'),
'cleanup should merge worktree branch into current branch');
});
test('execute-phase.md handles merge conflicts gracefully', () => {
const content = fs.readFileSync(executePhasePath, 'utf8');
assert.ok(
content.includes('Merge conflict') || content.includes('merge conflict'),
'cleanup should handle merge conflicts gracefully'
);
});
test('execute-phase.md skips cleanup when use_worktrees is false', () => {
const content = fs.readFileSync(executePhasePath, 'utf8');
assert.ok(content.includes('use_worktrees'),
'cleanup should respect workflow.use_worktrees config');
});
test('quick.md includes worktree cleanup after executor returns', () => {
const content = fs.readFileSync(quickPath, 'utf8');
assert.ok(content.includes('Worktree cleanup') || content.includes('worktree cleanup'),
'quick should have worktree cleanup');
assert.ok(content.includes('git worktree remove'),
'quick cleanup should remove worktrees');
assert.ok(content.includes('git branch -D'),
'quick cleanup should delete temporary branches');
});
test('quick.md merges worktree branch before removing', () => {
const content = fs.readFileSync(quickPath, 'utf8');
assert.ok(content.includes('git merge'),
'quick cleanup should merge worktree branch');
});
test('cleanup uses git worktree list to discover orphans', () => {
const content = fs.readFileSync(executePhasePath, 'utf8');
assert.ok(content.includes('git worktree list'),
'cleanup should discover worktrees via git worktree list');
});
});