diff --git a/.changeset/fierce-mice-munch.md b/.changeset/fierce-mice-munch.md new file mode 100644 index 000000000..1df5f57b4 --- /dev/null +++ b/.changeset/fierce-mice-munch.md @@ -0,0 +1,5 @@ +--- +type: Removed +pr: 3564 +--- +**Two workflow files that shipped to every runtime but were never loaded are gone** — `discovery-phase.md` and `plan-milestone-gaps.md` had no command, agent, or skill referencing them, and `docs/INVENTORY.md` claimed callers for one that did not exist. A new lint rule now fails the build if any shipped workflow becomes unreachable again. (#3560) diff --git a/docs/INVENTORY-MANIFEST.json b/docs/INVENTORY-MANIFEST.json index ec8ffc0da..41e8800c0 100644 --- a/docs/INVENTORY-MANIFEST.json +++ b/docs/INVENTORY-MANIFEST.json @@ -127,7 +127,6 @@ "complete-milestone.md", "debug.md", "diagnose-issues.md", - "discovery-phase.md", "discuss-phase-assumptions.md", "discuss-phase-power.md", "discuss-phase.md", @@ -163,7 +162,6 @@ "note.md", "onboard.md", "pause-work.md", - "plan-milestone-gaps.md", "plan-phase.md", "plan-review-convergence.md", "plant-seed.md", diff --git a/docs/INVENTORY.md b/docs/INVENTORY.md index 16c9e25f7..b3d6e9377 100644 --- a/docs/INVENTORY.md +++ b/docs/INVENTORY.md @@ -193,7 +193,6 @@ Full roster at `gsd-core/workflows/*.md`. Workflows are thin orchestrators that | `code-review.md` | Review phase source changes via gsd-code-reviewer; produces REVIEW.md. | `/gsd-code-review` | | `complete-milestone.md` | Mark a shipped version as complete — MILESTONES.md entry, PROJECT.md evolution, tag. | `/gsd-complete-milestone` | | `diagnose-issues.md` | Orchestrate parallel debug agents to investigate UAT gaps and find root causes. | `/gsd-verify-work` (auto-diagnosis) | -| `discovery-phase.md` | Execute discovery at the appropriate depth level. | `/gsd-new-project` (discovery path) | | `discuss-phase-assumptions.md` | Assumptions-mode discuss — extract implementation decisions via codebase-first analysis. | `/gsd-discuss-phase` (when `discuss_mode=assumptions`) | | `discuss-phase-power.md` | Power-user discuss — pre-generate all questions into a JSON state file + HTML UI. | `/gsd-discuss-phase --power` | | `discuss-phase.md` | Extract implementation decisions through iterative gray-area discussion. | `/gsd-discuss-phase` | @@ -266,7 +265,7 @@ Full roster at `gsd-core/workflows/*.md`. Workflows are thin orchestrators that | `validate-phase.md` | Retroactively audit and fill Nyquist validation gaps for a completed phase. | `/gsd-validate-phase` | | `verify-work.md` | Conversational UAT with auto-diagnosis — produces UAT.md and fix plans. | `/gsd-verify-work` | -> **Note:** Some workflows have no direct user-facing command (e.g. `execute-plan.md`, `transition.md`, `node-repair.md`, `diagnose-issues.md`) — they are invoked internally by orchestrator workflows. `discovery-phase.md` is an alternate entry for `/gsd-new-project`. (The former `verify-phase` workflow — goal-backward verification with no loader of its own — was deleted in #1892; its still-live gates moved to `references/verifier-phase-gates.md` behind `gsd-verifier`.) +> **Note:** Some workflows have no direct user-facing command (e.g. `execute-plan.md`, `transition.md`, `node-repair.md`, `diagnose-issues.md`) — they are invoked internally by orchestrator workflows. (The former `verify-phase` workflow — goal-backward verification with no loader of its own — was deleted in #1892; its still-live gates moved to `references/verifier-phase-gates.md` behind `gsd-verifier`.) ### Workflow Sub-Files diff --git a/docs/README.md b/docs/README.md index e26cd532f..c76778665 100644 --- a/docs/README.md +++ b/docs/README.md @@ -23,6 +23,7 @@ Language versions: [English](README.md) · [Português (pt-BR)](pt-BR/README.md) - [Discuss a phase](how-to/discuss-a-phase.md) — capture implementation decisions before planning begins - [Resolve edge-coverage findings](how-to/resolve-edge-coverage-findings.md) — turn the spec phase's surfaced domain-boundary edges into covered, dismissed, or backstopped spec decisions - [Resolve prohibition findings](how-to/resolve-prohibition-findings.md) — turn the spec phase's surfaced must-NOT constraints into resolved, dismissed, or deferred spec decisions +- [Resolve an unreachable-workflow finding](how-to/resolve-unreachable-workflow-findings.md) — wire or fully sweep a shipped workflow that no command, agent, or skill references - [Resolve unreachable-guard findings](how-to/resolve-unreachable-guard-findings.md) — fix shell guards whose fallback arm cannot run, and tell "nothing to report" apart from "could not look" - [Resolve an ESLint glob-coverage finding](how-to/resolve-eslint-coverage-findings.md) — bring a source file that matches no lint rule under coverage, or record a reasoned exemption - [Plan a phase](how-to/plan-a-phase.md) — run research, decompose work, and verify plan quality diff --git a/docs/adr/0002-command-contract-validation-module.md b/docs/adr/0002-command-contract-validation-module.md index fac60d04c..2a4955c5c 100644 --- a/docs/adr/0002-command-contract-validation-module.md +++ b/docs/adr/0002-command-contract-validation-module.md @@ -14,6 +14,13 @@ The command file contract defines what makes a valid `commands/gsd/*.md`: - `allowed-tools:` block present and non-empty, all entries from the canonical tool set - Every `@`-reference inside `` blocks resolves to an existing file on disk - `@`-references inside `` blocks appear on their own line (no trailing prose) +- Every `gsd-core/workflows/*.md` file is reachable from at least one `commands/`, `agents/`, or `skills/` loader, transitively through `gsd-core/**` (#3560) + +The first five checks are per-file frontmatter/body rules. The sixth is a different concern in kind: a repo-level reachability graph, computed once per run rather than per command file. It walks every markdown file under `commands/`, `agents/`, and `skills/` as loader seeds, then follows references transitively through `gsd-core/**` (not just `gsd-core/workflows/`, since a `references/` or `templates/` file can itself name a workflow path) until every reachable workflow file is marked. Any `gsd-core/workflows/*.md` file left unmarked is reported as an orphan. + +A reference is recognized in three shapes: an eager `@`-include (the same `` syntax the first five checks already parse), a lazy path named only in prose or code that a command reads on demand rather than inlines, and a parent-relative sub-file path (`execute-phase/steps/post-merge-gate.md`) implicitly rooted under `workflows/`. + +Reachability is seeded only from loaders, never from a workflow file's own content — a workflow that references itself, or two workflows that reference only each other, must still be reported as unreachable, since no command, agent, or skill loader ever actually opens them. `docs/` and test fixtures deliberately do not count as loaders: a path merely mentioned in documentation or a test does not make it reachable by any runtime — only `commands/`, `agents/`, and `skills/` do. ## Context diff --git a/docs/how-to/resolve-unreachable-workflow-findings.md b/docs/how-to/resolve-unreachable-workflow-findings.md new file mode 100644 index 000000000..ba22e788a --- /dev/null +++ b/docs/how-to/resolve-unreachable-workflow-findings.md @@ -0,0 +1,117 @@ +# How to resolve an unreachable-workflow finding + +**Goal:** Turn a shipped workflow that no loader references into either a wired workflow or a fully swept deletion — so the instruction layer stays a single source of truth, instead of accumulating files that install to every runtime and are never read. + +**Prerequisites:** A failing `npm run lint:ci` (or a direct `node scripts/lint-command-contract.cjs`) reporting one or more unreachable workflows. The check runs automatically as part of `lint:ci` — you do not invoke it separately. + +For what counts as a reference and why the check is shaped this way, see [ADR-0002](../adr/0002-command-contract-validation-module.md). This guide covers only how to *act* on a finding. + +--- + +## Read a finding + +``` +ERROR lint-command-contract: 1 unreachable workflow file(s) + + gsd-core/workflows/scan.md + ships to every runtime install tree, but no command, agent, or skill references it +``` + +The check walks the transitive closure from every `commands/**`, `agents/**`, and `skills/**` file, following three reference shapes: + +| Shape | Example | When it is used | +|---|---|---| +| Eager include | `@~/.claude/gsd-core/workflows/x.md` | Inlined on every invocation. Reserve for workflows the command always needs. | +| Lazy path | `` `~/.claude/gsd-core/workflows/x.md` `` | Read on demand at the point of use. The default for flag-gated or conditional workflows. | +| Parent-relative | `execute-phase/steps/x.md` | A sub-file under an existing workflow directory. | + +A finding means **none** of those reaches the file. There are exactly two correct resolutions. + +--- + +## Wire it — the workflow is live and the reference is missing + +**Choose this when a command, flag, or agent is documented as using the workflow.** The file is not dead; the reference is. + +Add a reference in the loader that dispatches to it. Prefer a **lazy path** unless the workflow is needed on every invocation of that command: + +```markdown +- If it is `--fast`: strip the flag, then read and execute + `~/.claude/gsd-core/workflows/scan.md` (passing remaining args). +``` + +An eager `@`-include is inlined into context on *every* invocation of that command, including the paths that never use the workflow. The progressive-disclosure split ([#717](https://github.com/open-gsd/gsd-core/issues/717)) exists specifically to keep that cost off the common path, so reach for the lazy form first. + +If the loader is a `commands/gsd/*.md` file, regenerate the skill surface afterward: + +```bash +npm run gen:plugin-skills +``` + +**This was the right answer for `scan.md`** — `/gsd-map-codebase --fast` was a shipped, documented flag whose routing line named no resolvable path. Deleting the file would have removed the only implementation of a live feature ([#3561](https://github.com/open-gsd/gsd-core/issues/3561)). + +--- + +## Delete it — the workflow is genuinely dead + +**Choose this when nothing is supposed to reach it** — typically because its command was removed and the workflow was left behind. + +Before deleting, confirm the fence is not load-bearing: check the file's own header for a claimed caller and verify that caller exists. The `discovery-phase` workflow's header claimed it was "called from plan-phase.md's mandatory_discovery step"; that step did not exist. `docs/INVENTORY.md` claimed it was an alternate `/gsd-new-project` entry; `new-project.md` never referenced it. **A claimed caller is not a caller.** (The `discovery-phase` workflow was deleted in #3560.) + +Deletion is a five-step sweep, and skipping any step leaves the tree inconsistent: + +```bash +git rm gsd-core/workflows/.md +``` + +1. **Remove its `docs/INVENTORY.md` row — in all five locales.** `docs/INVENTORY.md` plus `docs/ja-JP/`, `docs/ko-KR/`, `docs/zh-CN/`, `docs/pt-BR/`. Check the explanatory note near the bottom of each file too; a workflow is sometimes named there as well as in the table. +2. **Regenerate the inventory manifest.** Build first — regenerating before `build:lib` silently drops modules: + ```bash + npm run build:lib && node scripts/gen-inventory-manifest.cjs --write + ``` +3. **Regenerate the golden install-tree fixtures.** All 19 runtimes list every shipped workflow: + ```bash + npm run gen:install-tree + ``` +4. **Sweep tests that name the file — allowlists *and* content assertions.** Two distinct traps live here, and #3560 hit both: + - An **allowlist keyed on the bare basename** will not match a full-path search. `tests/planner-language-regression.test.cjs` carried a stale entry for the `discovery-phase` workflow that a path-based sweep missed. + - A test that **asserts the file's existence or content** pins it. `tests/phase.test.cjs` required the `plan-milestone-gaps` workflow to exist and checked its `mkdir` patterns; deleting the file turned that into four red tests on the remote runner. Cut the coupling in the same change — but keep any sibling assertions that guard *other* files, since these blocks are often shared across several workflows. + + Note that `scripts/lint-removed-but-needed.cjs` will **not** catch either: it scans `.github/workflows/`, `gsd-core/`, `docs/`, and `package.json` — not `tests/`. Search `tests/` yourself. +5. **Add a `Removed` changeset fragment**, and remember that a `Removed` type requires a `docs/` change — which step 1 already satisfies. + +Then confirm the tree is consistent: + +```bash +npm run lint:ci +``` + +--- + +## What does *not* count as a reference + +The check deliberately scans only `commands/`, `agents/`, `skills/`, and `gsd-core/**`. A mention anywhere else confers no reachability: + +| Location | Counts? | Why | +|---|---|---| +| `docs/**` | **No** | Documentation is a claim about the system, not a loader. `scan.md` was documented in `docs/INVENTORY.md` while being entirely unreached. | +| `tests/fixtures/install-tree/*.json` | **No** | A shipping manifest proves the file *ships*, which is the problem being reported, not a refutation of it. | +| A changeset fragment | **No** | Historical record, not a load path. | + +If you are tempted to satisfy the check by adding a mention somewhere convenient, that is the failure mode this scoping exists to prevent — the file stays dead and the gate goes green. + +--- + +## When the check passes but something still feels wrong + +The rule proves **structural** reachability: some loader names the file. It cannot prove the file is read on any executed path. + +| You see | It means | +|---|---| +| `151 workflow files, 151 reachable, 0 unreachable` | Every shipped workflow is named by at least one loader. Not that every one is used. | +| A workflow reachable only from another unreachable workflow | Reported correctly — reachability is seeded only from loaders, so an unreachable file cannot confer reachability on anything else. | +| A pair of workflows that reference only each other | Both reported. A mutual-reference island satisfies nothing. | +| A workflow that references itself | Reported. A self-reference is not a loader. | +| A path named only inside a fenced code block | Counted as a reference. This is a deliberate over-count: the check fails builds, and a false positive on a correct tree is worse than a missed orphan, so ambiguous references resolve toward "reachable". | + +A workflow that is named by a loader but never actually executed is **not** caught here. That is a semantic question this structural check does not answer. diff --git a/docs/ja-JP/INVENTORY.md b/docs/ja-JP/INVENTORY.md index d5ab21208..6ea9cfc38 100644 --- a/docs/ja-JP/INVENTORY.md +++ b/docs/ja-JP/INVENTORY.md @@ -189,7 +189,6 @@ | `code-review.md` | gsd-code-reviewer でフェーズのソース変更をレビュー。REVIEW.md を作成。 | `/gsd-code-review` | | `complete-milestone.md` | 出荷されたバージョンを完了としてマーク — MILESTONES.md エントリー、PROJECT.md の進化、タグ。 | `/gsd-complete-milestone` | | `diagnose-issues.md` | 並列デバッグエージェントをオーケストレーションして UAT のギャップを調査し、根本原因を特定。 | `/gsd-verify-work` (auto-diagnosis) | -| `discovery-phase.md` | 適切な深さレベルでディスカバリーを実行。 | `/gsd-new-project` (discovery path) | | `discuss-phase-assumptions.md` | 仮定モードの discuss — コードベースファーストの分析で実装決定を抽出。 | `/gsd-discuss-phase` (when `discuss_mode=assumptions`) | | `discuss-phase-power.md` | パワーユーザー discuss — すべての質問を JSON 状態ファイル + HTML UI に事前生成。 | `/gsd-discuss-phase --power` | | `discuss-phase.md` | 反復的なグレーゾーンの議論を通じて実装決定を抽出。 | `/gsd-discuss-phase` | @@ -261,7 +260,7 @@ | `validate-phase.md` | 完了したフェーズの Nyquist バリデーションのギャップを遡及監査して埋める。 | `/gsd-validate-phase` | | `verify-work.md` | 自動診断付きの会話型 UAT — UAT.md と修正プランを作成。 | `/gsd-verify-work` | -> **注記:** 一部のワークフローには直接ユーザー向けのコマンドがありません(例: `execute-plan.md`、`transition.md`、`node-repair.md`、`diagnose-issues.md`)— これらはオーケストレーターワークフローによって内部的に呼び出されます。`discovery-phase.md` は `/gsd-new-project` の代替エントリーポイントです。 +> **注記:** 一部のワークフローには直接ユーザー向けのコマンドがありません(例: `execute-plan.md`、`transition.md`、`node-repair.md`、`diagnose-issues.md`)— これらはオーケストレーターワークフローによって内部的に呼び出されます。 --- diff --git a/docs/ko-KR/INVENTORY.md b/docs/ko-KR/INVENTORY.md index 48330de87..11bf8651a 100644 --- a/docs/ko-KR/INVENTORY.md +++ b/docs/ko-KR/INVENTORY.md @@ -189,7 +189,6 @@ | `code-review.md` | gsd-code-reviewer를 통한 단계 소스 변경 검토; REVIEW.md 생성. | `/gsd-code-review` | | `complete-milestone.md` | 출시된 버전을 완료로 표시 — MILESTONES.md 항목, PROJECT.md 발전, 태그. | `/gsd-complete-milestone` | | `diagnose-issues.md` | UAT 공백 조사 및 근본 원인 찾기를 위한 병렬 디버그 에이전트 오케스트레이션. | `/gsd-verify-work` (자동 진단) | -| `discovery-phase.md` | 적절한 깊이 수준에서 탐색 실행. | `/gsd-new-project` (탐색 경로) | | `discuss-phase-assumptions.md` | 가정 모드 discuss — 코드베이스 우선 분석을 통한 구현 결정 추출. | `/gsd-discuss-phase` (`discuss_mode=assumptions`일 때) | | `discuss-phase-power.md` | 파워 유저 discuss — 모든 질문을 JSON 상태 파일 + HTML UI로 사전 생성. | `/gsd-discuss-phase --power` | | `discuss-phase.md` | 반복적인 회색 지대 토론을 통한 구현 결정 추출. | `/gsd-discuss-phase` | @@ -261,7 +260,7 @@ | `validate-phase.md` | 완료된 단계의 나이퀴스트 검증 공백을 소급 감사 및 채움. | `/gsd-validate-phase` | | `verify-work.md` | 자동 진단이 포함된 대화형 UAT — UAT.md 및 수정 계획 생성. | `/gsd-verify-work` | -> **참고:** 일부 워크플로우는 직접적인 사용자 대면 명령어가 없습니다(예: `execute-plan.md`, `transition.md`, `node-repair.md`, `diagnose-issues.md`) — 이들은 오케스트레이터 워크플로우에 의해 내부적으로 호출됩니다. `discovery-phase.md`는 `/gsd-new-project`의 대체 진입점입니다. +> **참고:** 일부 워크플로우는 직접적인 사용자 대면 명령어가 없습니다(예: `execute-plan.md`, `transition.md`, `node-repair.md`, `diagnose-issues.md`) — 이들은 오케스트레이터 워크플로우에 의해 내부적으로 호출됩니다. --- diff --git a/docs/pt-BR/INVENTORY.md b/docs/pt-BR/INVENTORY.md index 0b61c5cf8..3baefd60d 100644 --- a/docs/pt-BR/INVENTORY.md +++ b/docs/pt-BR/INVENTORY.md @@ -189,7 +189,6 @@ Registro completo em `gsd-core/workflows/*.md`. Workflows são orquestradores en | `code-review.md` | Revisa alterações de código-fonte da fase via gsd-code-reviewer; produz REVIEW.md. | `/gsd-code-review` | | `complete-milestone.md` | Marca uma versão entregue como concluída — entrada no MILESTONES.md, evolução do PROJECT.md, tag. | `/gsd-complete-milestone` | | `diagnose-issues.md` | Orquestra agentes de debug paralelos para investigar lacunas de UAT e encontrar causas raiz. | `/gsd-verify-work` (autodiagnóstico) | -| `discovery-phase.md` | Executa a descoberta no nível de profundidade apropriado. | `/gsd-new-project` (caminho de descoberta) | | `discuss-phase-assumptions.md` | Discuss no modo de premissas — extrai decisões de implementação via análise com base no código primeiro. | `/gsd-discuss-phase` (quando `discuss_mode=assumptions`) | | `discuss-phase-power.md` | Discuss para usuário avançado — pré-gera todas as perguntas em um arquivo de estado JSON + UI HTML. | `/gsd-discuss-phase --power` | | `discuss-phase.md` | Extrai decisões de implementação por meio de discussão iterativa de zonas cinzentas. | `/gsd-discuss-phase` | @@ -261,7 +260,7 @@ Registro completo em `gsd-core/workflows/*.md`. Workflows são orquestradores en | `validate-phase.md` | Audita retroativamente e preenche lacunas de validação Nyquist para uma fase concluída. | `/gsd-validate-phase` | | `verify-work.md` | UAT conversacional com autodiagnóstico — produz UAT.md e planos de correção. | `/gsd-verify-work` | -> **Nota:** Alguns workflows não têm comando direto voltado ao usuário (p. ex. `execute-plan.md`, `transition.md`, `node-repair.md`, `diagnose-issues.md`) — eles são invocados internamente por workflows orquestradores. `discovery-phase.md` é uma entrada alternativa para `/gsd-new-project`. +> **Nota:** Alguns workflows não têm comando direto voltado ao usuário (p. ex. `execute-plan.md`, `transition.md`, `node-repair.md`, `diagnose-issues.md`) — eles são invocados internamente por workflows orquestradores. --- diff --git a/docs/zh-CN/INVENTORY.md b/docs/zh-CN/INVENTORY.md index 4cd5c913e..1c020396a 100644 --- a/docs/zh-CN/INVENTORY.md +++ b/docs/zh-CN/INVENTORY.md @@ -189,7 +189,6 @@ | `code-review.md` | 通过 gsd-code-reviewer 审查阶段源码变更;生成 REVIEW.md。 | `/gsd-code-review` | | `complete-milestone.md` | 将已发布版本标记为完成 — MILESTONES.md 条目、PROJECT.md 演进、标签。 | `/gsd-complete-milestone` | | `diagnose-issues.md` | 编排并行调试代理以调查 UAT 差距并找出根本原因。 | `/gsd-verify-work`(自动诊断) | -| `discovery-phase.md` | 以适当的深度级别执行发现。 | `/gsd-new-project`(发现路径) | | `discuss-phase-assumptions.md` | 假设模式讨论 — 通过以代码库为先的分析提取实施决策。 | `/gsd-discuss-phase`(当 `discuss_mode=assumptions` 时) | | `discuss-phase-power.md` | 高级用户讨论 — 将所有问题预生成到 JSON 状态文件和 HTML UI 中。 | `/gsd-discuss-phase --power` | | `discuss-phase.md` | 通过迭代灰色地带讨论提取实施决策。 | `/gsd-discuss-phase` | @@ -261,7 +260,7 @@ | `validate-phase.md` | 回溯审计并填补已完成阶段的奈奎斯特验证空缺。 | `/gsd-validate-phase` | | `verify-work.md` | 带自动诊断的对话式 UAT — 生成 UAT.md 和修复计划。 | `/gsd-verify-work` | -> **注意:** 某些工作流没有直接面向用户的命令(例如 `execute-plan.md`、`transition.md`、`node-repair.md`、`diagnose-issues.md`)— 它们由编排器工作流在内部调用。`discovery-phase.md` 是 `/gsd-new-project` 的备用入口。 +> **注意:** 某些工作流没有直接面向用户的命令(例如 `execute-plan.md`、`transition.md`、`node-repair.md`、`diagnose-issues.md`)— 它们由编排器工作流在内部调用。 --- diff --git a/gsd-core/workflows/discovery-phase.md b/gsd-core/workflows/discovery-phase.md deleted file mode 100644 index b80b12339..000000000 --- a/gsd-core/workflows/discovery-phase.md +++ /dev/null @@ -1,298 +0,0 @@ - -Execute discovery at the appropriate depth level. -Produces DISCOVERY.md (for Level 2-3) that informs PLAN.md creation. - -Called from plan-phase.md's mandatory_discovery step with a depth parameter. - -NOTE: For comprehensive ecosystem research ("how do experts build this"), use /gsd:plan-phase --research-phase instead, which produces RESEARCH.md. - -```bash -_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi -RESPONSE_LANGUAGE=$(gsd_run query config-get response_language --default "" 2>/dev/null || echo "") -``` - -**If `response_language` is set:** All user-facing questions, prompts, and explanations in this workflow MUST be presented in `{response_language}`. Technical terms, code, file paths, and subagent prompts stay in English — only user-facing output is translated. - - - -**This workflow supports three depth levels:** - -| Level | Name | Time | Output | When | -| ----- | ------------ | --------- | -------------------------------------------- | ----------------------------------------- | -| 1 | Quick Verify | 2-5 min | No file, proceed with verified knowledge | Single library, confirming current syntax | -| 2 | Standard | 15-30 min | DISCOVERY.md | Choosing between options, new integration | -| 3 | Deep Dive | 1+ hour | Detailed DISCOVERY.md with validation gates | Architectural decisions, novel problems | - -**Depth is determined by plan-phase.md before routing here.** - - - -**MANDATORY: Context7 BEFORE WebSearch** - -Claude's training data is 6-18 months stale. Always verify. - -1. **Context7 MCP FIRST** - Current docs, no hallucination -2. **Official docs** - When Context7 lacks coverage -3. **WebSearch LAST** - For comparisons and trends only - -See ~/.claude/gsd-core/templates/discovery.md `` for full protocol. - - - - - -Check the depth parameter passed from plan-phase.md: -- `depth=verify` → Level 1 (Quick Verification) -- `depth=standard` → Level 2 (Standard Discovery) -- `depth=deep` → Level 3 (Deep Dive) - -Route to appropriate level workflow below. - - - -**Level 1: Quick Verification (2-5 minutes)** - -For: Single known library, confirming syntax/version still correct. - -**Process:** - -1. Resolve library in Context7: - - ``` - mcp__context7__resolve-library-id with libraryName: "[library]" - ``` - -2. Fetch relevant docs: - - ``` - mcp__context7__query-docs with: - - libraryId: [from step 1] - - query: [specific concern] - ``` - -3. Verify: - - - Current version matches expectations - - API syntax unchanged - - No breaking changes in recent versions - -4. **If verified:** Return to plan-phase.md with confirmation. No DISCOVERY.md needed. - -5. **If concerns found:** Escalate to Level 2. - -**Output:** Verbal confirmation to proceed, or escalation to Level 2. - - - -**Level 2: Standard Discovery (15-30 minutes)** - -For: Choosing between options, new external integration. - -**Process:** - -1. **Identify what to discover:** - - - What options exist? - - What are the key comparison criteria? - - What's our specific use case? - -2. **Context7 for each option:** - - ``` - For each library/framework: - - mcp__context7__resolve-library-id - - mcp__context7__query-docs (frame the `query` for API usage vs concepts) - ``` - -3. **Official docs** for anything Context7 lacks. - -4. **WebSearch** for comparisons: - - - "[option A] vs [option B] {current_year}" - - "[option] known issues" - - "[option] with [our stack]" - -5. **Cross-verify:** Any WebSearch finding → confirm with Context7/official docs. - -6. **Create DISCOVERY.md** using ~/.claude/gsd-core/templates/discovery.md structure: - - - Summary with recommendation - - Key findings per option - - Code examples from Context7 - - Confidence level (should be MEDIUM-HIGH for Level 2) - -7. Return to plan-phase.md. - -**Output:** `.planning/phases/XX-name/DISCOVERY.md` - - - -**Level 3: Deep Dive (1+ hour)** - -For: Architectural decisions, novel problems, high-risk choices. - -**Process:** - -1. **Scope the discovery** using ~/.claude/gsd-core/templates/discovery.md: - - - Define clear scope - - Define include/exclude boundaries - - List specific questions to answer - -2. **Exhaustive Context7 research:** - - - All relevant libraries - - Related patterns and concepts - - Multiple topics per library if needed - -3. **Official documentation deep read:** - - - Architecture guides - - Best practices sections - - Migration/upgrade guides - - Known limitations - -4. **WebSearch for ecosystem context:** - - - How others solved similar problems - - Production experiences - - Gotchas and anti-patterns - - Recent changes/announcements - -5. **Cross-verify ALL findings:** - - - Every WebSearch claim → verify with authoritative source - - Mark what's verified vs assumed - - Flag contradictions - -6. **Create comprehensive DISCOVERY.md:** - - - Full structure from ~/.claude/gsd-core/templates/discovery.md - - Quality report with source attribution - - Confidence by finding - - If LOW confidence on any critical finding → add validation checkpoints - -7. **Confidence gate:** If overall confidence is LOW, present options before proceeding. - -8. Return to plan-phase.md. - -**Output:** `.planning/phases/XX-name/DISCOVERY.md` (comprehensive) - - - -**For Level 2-3:** Define what we need to learn. - -Ask: What do we need to learn before we can plan this phase? - -- Technology choices? -- Best practices? -- API patterns? -- Architecture approach? - - - -Use ~/.claude/gsd-core/templates/discovery.md. - -Include: - -- Clear discovery objective -- Scoped include/exclude lists -- Source preferences (official docs, Context7, current year) -- Output structure for DISCOVERY.md - - - -Run the discovery: -- Use web search for current info -- Use Context7 MCP for library docs -- Prefer current year sources -- Structure findings per template - - - -Write `.planning/phases/XX-name/DISCOVERY.md`: -- Summary with recommendation -- Key findings with sources -- Code examples if applicable -- Metadata (confidence, dependencies, open questions, assumptions) - - - -After creating DISCOVERY.md, check confidence level. - -If confidence is LOW: - -**Text mode (`workflow.text_mode: true` in config or `--text` flag):** Set `TEXT_MODE=true` if `--text` is present in `$ARGUMENTS` OR `text_mode` from init JSON is `true`. When TEXT_MODE is active, replace every `AskUserQuestion` call with a plain-text numbered list and ask the user to type their choice number. This is required for non-Claude runtimes (OpenAI Codex, Gemini CLI, etc.) where `AskUserQuestion` is not available. -Use AskUserQuestion: - -- header: "Low Conf." -- question: "Discovery confidence is LOW: [reason]. How would you like to proceed?" -- options: - - "Dig deeper" - Do more research before planning - - "Proceed anyway" - Accept uncertainty, plan with caveats - - "Pause" - I need to think about this - -If confidence is MEDIUM: -Inline: "Discovery complete (medium confidence). [brief reason]. Proceed to planning?" - -If confidence is HIGH: -Proceed directly, just note: "Discovery complete (high confidence)." - - - -If DISCOVERY.md has open_questions: - -Present them inline: -"Open questions from discovery: - -- [Question 1] -- [Question 2] - -These may affect implementation. Acknowledge and proceed? (yes / address first)" - -If "address first": Gather user input on questions, update discovery. - - - -``` -Discovery complete: .planning/phases/XX-name/DISCOVERY.md -Recommendation: [one-liner] -Confidence: [level] - -What's next? - -1. Discuss phase context (/gsd:discuss-phase [current-phase]) -2. Create phase plan (/gsd:plan-phase [current-phase]) -3. Refine discovery (dig deeper) -4. Review discovery - -``` - -NOTE: DISCOVERY.md is NOT committed separately. It will be committed with phase completion. - - - - - -**Level 1 (Quick Verify):** -- Context7 consulted for library/topic -- Current state verified or concerns escalated -- Verbal confirmation to proceed (no files) - -**Level 2 (Standard):** -- Context7 consulted for all options -- WebSearch findings cross-verified -- DISCOVERY.md created with recommendation -- Confidence level MEDIUM or higher -- Ready to inform PLAN.md creation - -**Level 3 (Deep Dive):** -- Discovery scope defined -- Context7 exhaustively consulted -- All WebSearch findings verified against authoritative sources -- DISCOVERY.md created with comprehensive analysis -- Quality report with source attribution -- If LOW confidence findings → validation checkpoints defined -- Confidence gate passed -- Ready to inform PLAN.md creation - diff --git a/gsd-core/workflows/plan-milestone-gaps.md b/gsd-core/workflows/plan-milestone-gaps.md deleted file mode 100644 index a3078c14f..000000000 --- a/gsd-core/workflows/plan-milestone-gaps.md +++ /dev/null @@ -1,281 +0,0 @@ - -Create all phases necessary to close gaps identified by `/gsd:audit-milestone`. Reads MILESTONE-AUDIT.md, groups gaps into logical phases, creates phase entries in ROADMAP.md, and offers to plan each phase. One command creates all fix phases — no manual `/gsd-add-phase` per gap. - - - -Read all files referenced by the invoking prompt's execution_context before starting. - - - - -## 1. Load Audit Results - -```bash -# Find the most recent audit file -(ls -t .planning/v*-MILESTONE-AUDIT.md 2>/dev/null || true) | head -1 -``` - -Parse YAML frontmatter to extract structured gaps: -- `gaps.requirements` — unsatisfied requirements -- `gaps.integration` — missing cross-phase connections -- `gaps.flows` — broken E2E flows - -If no audit file exists or has no gaps, error: -``` -No audit gaps found. Run `/gsd:audit-milestone` first. -``` - -## 2. Prioritize Gaps - -Group gaps by priority from REQUIREMENTS.md: - -| Priority | Action | -|----------|--------| -| `must` | Create phase, blocks milestone | -| `should` | Create phase, recommended | -| `nice` | Ask user: include or defer? | - -For integration/flow gaps, infer priority from affected requirements. - -## 3. Group Gaps into Phases - -Cluster related gaps into logical phases: - -**Grouping rules:** -- Same affected phase → combine into one fix phase -- Same subsystem (auth, API, UI) → combine -- Dependency order (fix stubs before wiring) -- Keep phases focused: 2-4 tasks each - -**Example grouping:** -``` -Gap: DASH-01 unsatisfied (Dashboard doesn't fetch) -Gap: Integration Phase 1→3 (Auth not passed to API calls) -Gap: Flow "View dashboard" broken at data fetch - -→ Phase 6: "Wire Dashboard to API" - - Add fetch to Dashboard.tsx - - Include auth header in fetch - - Handle response, update state - - Render user data -``` - -## 4. Determine Phase Numbers - -Find highest existing phase: -```bash -_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; if [ -f "$GSD_TOOLS" ]; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif command -v gsd-tools >/dev/null 2>&1; then GSD_TOOLS="$(command -v gsd-tools)"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif [ -f "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; elif [ -f "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}" ]; then GSD_TOOLS="${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd-tools is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi -# Get sorted phase list, extract last one -HIGHEST=$(gsd_run query phases.list --pick directories[-1]) -``` - -New phases continue from there: -- If Phase 5 is highest, gaps become Phase 6, 7, 8... - -## 5. Present Gap Closure Plan - -```markdown -## Gap Closure Plan - -**Milestone:** {version} -**Gaps to close:** {N} requirements, {M} integration, {K} flows - -### Proposed Phases - -**Phase {N}: {Name}** -Closes: -- {REQ-ID}: {description} -- Integration: {from} → {to} -Tasks: {count} - -**Phase {N+1}: {Name}** -Closes: -- {REQ-ID}: {description} -- Flow: {flow name} -Tasks: {count} - -{If nice-to-have gaps exist:} - -### Deferred (nice-to-have) - -These gaps are optional. Include them? -- {gap description} -- {gap description} - ---- - -Create these {X} phases? (yes / adjust / defer all optional) -``` - -Wait for user confirmation. - -## 6. Update ROADMAP.md - -Add new phases to current milestone: - -```markdown -### Phase {N}: {Name} -**Goal:** {derived from gaps being closed} -**Requirements:** {REQ-IDs being satisfied} -**Gap Closure:** Closes gaps from audit - -### Phase {N+1}: {Name} -... -``` - -## 7. Update REQUIREMENTS.md Traceability Table (REQUIRED) - -For each REQ-ID assigned to a gap closure phase: -- Update the Phase column to reflect the new gap closure phase -- Reset Status to `Pending` - -Reset checked-off requirements the audit found unsatisfied: -- Change `[x]` → `[ ]` for any requirement marked unsatisfied in the audit -- Update coverage count at top of REQUIREMENTS.md - -```bash -# Verify traceability table reflects gap closure assignments -grep -c "Pending" .planning/REQUIREMENTS.md -``` - -## 8. Create Phase Directories - -For each new phase (N, N+1, …), resolve the directory name via `init.phase-op` so the `project_code` prefix is honoured: - -```bash -INIT=$(gsd_run query init.phase-op "{NN}") -if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi -expected_phase_dir=$(echo "$INIT" | node -e "process.stdout.write(JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')).expected_phase_dir)") -mkdir -p "${expected_phase_dir}" -``` - -Repeat for each gap-closure phase number. This produces `{CODE}-{NN}-{slug}/` when `project_code` is set in `.planning/config.json`, and `{NN}-{slug}/` otherwise — consistent with all other phase-creation paths. - -## 9. Commit Roadmap and Requirements Update - -```bash -gsd_run query commit "docs(roadmap): add gap closure phases {N}-{M}" --files .planning/ROADMAP.md .planning/REQUIREMENTS.md -``` - -## 10. Offer Next Steps - -```markdown -## ✓ Gap Closure Phases Created - -**Phases added:** {N} - {M} -**Gaps addressed:** {count} requirements, {count} integration, {count} flows - ---- - -## ▶ Next Up — [${PROJECT_CODE}] ${PROJECT_TITLE} - -**Plan first gap closure phase** - -`/clear` then: - -`/gsd:plan-phase {N}` - ---- - -**Also available:** -- `/gsd:execute-phase {N}` — if plans already exist -- `cat .planning/ROADMAP.md` — see updated roadmap - ---- - -**After all gap phases complete:** - -`/gsd:audit-milestone` — re-audit to verify gaps closed -`/gsd:complete-milestone {version}` — archive when audit passes -``` - - - - - -## How Gaps Become Tasks - -**Requirement gap → Tasks:** -```yaml -gap: - id: DASH-01 - description: "User sees their data" - reason: "Dashboard exists but doesn't fetch from API" - missing: - - "useEffect with fetch to /api/user/data" - - "State for user data" - - "Render user data in JSX" - -becomes: - -phase: "Wire Dashboard Data" -tasks: - - name: "Add data fetching" - files: [src/components/Dashboard.tsx] - action: "Add useEffect that fetches /api/user/data on mount" - - - name: "Add state management" - files: [src/components/Dashboard.tsx] - action: "Add useState for userData, loading, error states" - - - name: "Render user data" - files: [src/components/Dashboard.tsx] - action: "Replace placeholder with userData.map rendering" -``` - -**Integration gap → Tasks:** -```yaml -gap: - from_phase: 1 - to_phase: 3 - connection: "Auth token → API calls" - reason: "Dashboard API calls don't include auth header" - missing: - - "Auth header in fetch calls" - - "Token refresh on 401" - -becomes: - -phase: "Add Auth to Dashboard API Calls" -tasks: - - name: "Add auth header to fetches" - files: [src/components/Dashboard.tsx, src/lib/api.ts] - action: "Include Authorization header with token in all API calls" - - - name: "Handle 401 responses" - files: [src/lib/api.ts] - action: "Add interceptor to refresh token or redirect to login on 401" -``` - -**Flow gap → Tasks:** -```yaml -gap: - name: "User views dashboard after login" - broken_at: "Dashboard data load" - reason: "No fetch call" - missing: - - "Fetch user data on mount" - - "Display loading state" - - "Render user data" - -becomes: - -# Usually same phase as requirement/integration gap -# Flow gaps often overlap with other gap types -``` - - - - -- [ ] MILESTONE-AUDIT.md loaded and gaps parsed -- [ ] Gaps prioritized (must/should/nice) -- [ ] Gaps grouped into logical phases -- [ ] User confirmed phase plan -- [ ] ROADMAP.md updated with new phases -- [ ] REQUIREMENTS.md traceability table updated with gap closure phase assignments -- [ ] Unsatisfied requirement checkboxes reset (`[x]` → `[ ]`) -- [ ] Coverage count updated in REQUIREMENTS.md -- [ ] Phase directories created -- [ ] Changes committed (includes REQUIREMENTS.md) -- [ ] User knows to run `/gsd:plan-phase` next - diff --git a/scripts/command-contract-helpers.cjs b/scripts/command-contract-helpers.cjs index 3a1179d6b..63e680774 100644 --- a/scripts/command-contract-helpers.cjs +++ b/scripts/command-contract-helpers.cjs @@ -124,4 +124,58 @@ function workflowPathRefs(content) { return refs; } -module.exports = { CANONICAL_TOOLS, parseFrontmatter, executionContextRefs, workflowPathRefs }; +/** + * unreachableWorkflows(loaderContents, gsdFiles, workflowPaths) + * + * Computes reachability over the gsd-core file graph and reports which + * `workflowPaths` are never reached, starting only from `loaderContents` + * (commands/agents/skills — the files a runtime actually loads) and walking + * `workflowPathRefs` edges transitively through `gsdFiles`. + * + * The seed set is deliberately restricted to loaders and never includes a + * workflow's own content. Seeding from workflows too would let two failure + * modes hide: a workflow that only references itself would satisfy its own + * reachability, and a pair of workflows that reference only each other would + * form an island that looks connected from the inside but that no command, + * agent, or skill ever actually opens. Both are orphans in every sense that + * matters — nothing external can reach them — and both must be reported. + * Requiring every path to originate at a loader is what makes "reachable" + * mean "a runtime can actually get here," not merely "something points to + * it." + * + * `gsdFiles` covers all of `gsd-core/**`, not just `workflows/`, because a + * `references/` or `templates/` file can itself name a workflow path and + * needs to be walked through to propagate reachability — restricting the map + * to `workflows/` would silently break any chain that passes through a + * non-workflow file. + * + * `visited` guards the walk against reference cycles (including the + * mutual/self cases above) so traversal always terminates. + */ +function unreachableWorkflows(loaderContents, gsdFiles, workflowPaths) { + const visited = new Set(); + const queue = []; + + for (const content of loaderContents) { + for (const ref of workflowPathRefs(content)) queue.push(ref); + } + + while (queue.length > 0) { + const p = queue.pop(); + if (visited.has(p)) continue; + visited.add(p); + if (gsdFiles.has(p)) { + for (const ref of workflowPathRefs(gsdFiles.get(p))) queue.push(ref); + } + } + + return workflowPaths.filter(p => !visited.has(p)); +} + +module.exports = { + CANONICAL_TOOLS, + parseFrontmatter, + executionContextRefs, + workflowPathRefs, + unreachableWorkflows, +}; diff --git a/scripts/lint-command-contract.cjs b/scripts/lint-command-contract.cjs index 13c2a52f4..4cce28d0f 100644 --- a/scripts/lint-command-contract.cjs +++ b/scripts/lint-command-contract.cjs @@ -9,6 +9,9 @@ * 3. allowed-tools: block present, non-empty, all entries from CANONICAL_TOOLS * 4. execution_context @-refs: every @-reference resolves to an existing file on disk * 5. execution_context @-refs: each appears on its own line (no trailing prose) + * 6. every gsd-core/workflows/*.md file is reachable from at least one + * commands/agents/skills loader, transitively through gsd-core/** + * (repo-level check, runs once — not per command file) * * Exit 0 = clean. Exit 1 = violations (with diagnostics). */ @@ -18,7 +21,17 @@ const fs = require('fs'); const path = require('path'); -const ROOT = path.join(__dirname, '..'); +function resolveRoot(argv) { + const idx = argv.indexOf('--root'); + if (idx === -1) return path.join(__dirname, '..'); + const value = argv[idx + 1]; + if (!value) { + throw new Error('lint-command-contract: --root requires a directory argument'); + } + return path.resolve(value); +} + +const ROOT = resolveRoot(process.argv.slice(2)); const COMMANDS_DIR = path.join(ROOT, 'commands', 'gsd'); const GSD_ROOT = path.join(ROOT, 'gsd-core'); @@ -26,10 +39,57 @@ const { CANONICAL_TOOLS, parseFrontmatter, executionContextRefs: extractExecutionContextRefs, + unreachableWorkflows, } = require('./command-contract-helpers.cjs'); const { runMain } = require('./lib/cli-exit.cjs'); +// ─── rule 6: repo-level workflow reachability ───────────────────────────────── + +function walkMarkdownFiles(dir, acc) { + let entries; + try { + entries = fs.readdirSync(dir, { withFileTypes: true }); + } catch (e) { + return acc; + } + for (const entry of entries) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + walkMarkdownFiles(full, acc); + } else if (entry.isFile() && entry.name.endsWith('.md')) { + acc.push(full); + } + } + return acc; +} + +function toGsdRelative(absPath) { + return path.relative(GSD_ROOT, absPath).split(path.sep).join('/'); +} + +function checkWorkflowReachability() { + const loaderFiles = [ + ...walkMarkdownFiles(path.join(ROOT, 'commands'), []), + ...walkMarkdownFiles(path.join(ROOT, 'agents'), []), + ...walkMarkdownFiles(path.join(ROOT, 'skills'), []), + ]; + const loaderContents = loaderFiles.map(f => fs.readFileSync(f, 'utf-8')); + + const gsdFileAbsPaths = walkMarkdownFiles(GSD_ROOT, []); + const gsdFiles = new Map(); + for (const abs of gsdFileAbsPaths) { + gsdFiles.set(toGsdRelative(abs), fs.readFileSync(abs, 'utf-8')); + } + + const workflowAbsPaths = walkMarkdownFiles(path.join(GSD_ROOT, 'workflows'), []); + const workflowPaths = workflowAbsPaths.map(toGsdRelative); + + const unreachable = unreachableWorkflows(loaderContents, gsdFiles, workflowPaths); + + return { workflowCount: workflowPaths.length, unreachable }; +} + // ─── check one file ─────────────────────────────────────────────────────────── function check(filePath) { @@ -88,27 +148,50 @@ function main() { .map(f => path.join(COMMANDS_DIR, f)); const results = commandFiles.map(check).filter(Boolean); + const { workflowCount, unreachable } = checkWorkflowReachability(); + + let ok = true; if (results.length === 0) { console.log( `ok lint-command-contract: ${commandFiles.length} command files checked, 0 violations`, ); - return 0; + } else { + ok = false; + const total = results.reduce((n, r) => n + r.violations.length, 0); + process.stderr.write( + `\nERROR lint-command-contract: ${total} violation(s) across ${results.length} file(s)\n\n`, + ); + for (const r of results) { + process.stderr.write(` ${r.file}\n`); + for (const v of r.violations) { + process.stderr.write(` - ${v}\n`); + } + process.stderr.write('\n'); + } + process.stderr.write('See docs/adr/0002-command-contract-validation-module.md for the contract spec.\n\n'); } - const total = results.reduce((n, r) => n + r.violations.length, 0); - process.stderr.write( - `\nERROR lint-command-contract: ${total} violation(s) across ${results.length} file(s)\n\n`, - ); - for (const r of results) { - process.stderr.write(` ${r.file}\n`); - for (const v of r.violations) { - process.stderr.write(` - ${v}\n`); + if (unreachable.length === 0) { + console.log( + `ok lint-command-contract: ${workflowCount} workflow files, ${workflowCount} reachable, 0 unreachable`, + ); + } else { + ok = false; + process.stderr.write( + `\nERROR lint-command-contract: ${unreachable.length} unreachable workflow file(s) of ${workflowCount}\n\n`, + ); + for (const p of unreachable) { + process.stderr.write(` gsd-core/${p}\n`); } - process.stderr.write('\n'); + process.stderr.write( + '\nEach file above ships to every runtime but is never referenced by any command,\n' + + 'agent, or skill loader (directly or transitively). Either wire it to a loader\n' + + 'or delete it — removing a command must sweep its orphaned workflow.\n\n', + ); } - process.stderr.write('See docs/adr/0002-command-contract-validation-module.md for the contract spec.\n\n'); - return 1; + + return ok ? 0 : 1; } runMain(main); diff --git a/tests/command-contract.test.cjs b/tests/command-contract.test.cjs index 3d6f136df..7cf1f5886 100644 --- a/tests/command-contract.test.cjs +++ b/tests/command-contract.test.cjs @@ -33,8 +33,15 @@ const { parseFrontmatter, executionContextRefs, workflowPathRefs, + unreachableWorkflows, } = require('../scripts/command-contract-helpers.cjs'); +const { runNode, OUTCOME } = require('./helpers/process-seam.cjs'); +const { PROBE_TIMEOUT_MS } = require('./helpers/timeouts.cjs'); +const { createTempDir, cleanup } = require('./helpers.cjs'); + +const LINT_SCRIPT = path.join(ROOT, 'scripts', 'lint-command-contract.cjs'); + const commandFiles = fs .readdirSync(COMMANDS_DIR) .filter(f => f.endsWith('.md')) @@ -284,6 +291,317 @@ describe('#3561 — every workflow path referenced by a command exists on disk', }); +describe('#3560 — unreachableWorkflows closure', () => { + test('a planted orphan is reported', () => { + const loaderContents = ['load workflows/live.md']; + const gsdFiles = new Map(); + const workflowPaths = ['workflows/live.md', 'workflows/orphan.md']; + assert.deepEqual( + unreachableWorkflows(loaderContents, gsdFiles, workflowPaths), + ['workflows/orphan.md'], + ); + }); + + test('eager include reaches', () => { + const loaderContents = ['@~/.claude/gsd-core/workflows/a.md']; + const gsdFiles = new Map(); + const workflowPaths = ['workflows/a.md']; + assert.deepEqual(unreachableWorkflows(loaderContents, gsdFiles, workflowPaths), []); + }); + + test('lazy path reaches', () => { + const loaderContents = ['read `gsd-core/workflows/a.md`']; + const gsdFiles = new Map(); + const workflowPaths = ['workflows/a.md']; + assert.deepEqual(unreachableWorkflows(loaderContents, gsdFiles, workflowPaths), []); + }); + + test('parent-relative reaches', () => { + const loaderContents = ['run execute-phase/steps/s.md']; + const gsdFiles = new Map(); + const workflowPaths = ['workflows/execute-phase/steps/s.md']; + assert.deepEqual(unreachableWorkflows(loaderContents, gsdFiles, workflowPaths), []); + }); + + test('reaches transitively', () => { + const loaderContents = ['see workflows/a.md']; + const gsdFiles = new Map([['workflows/a.md', 'see workflows/b.md']]); + const workflowPaths = ['workflows/a.md', 'workflows/b.md']; + assert.deepEqual(unreachableWorkflows(loaderContents, gsdFiles, workflowPaths), []); + }); + + test('reaches at depth three', () => { + const loaderContents = ['see workflows/a.md']; + const gsdFiles = new Map([ + ['workflows/a.md', 'see workflows/b.md'], + ['workflows/b.md', 'see workflows/c.md'], + ]); + const workflowPaths = ['workflows/a.md', 'workflows/b.md', 'workflows/c.md']; + assert.deepEqual(unreachableWorkflows(loaderContents, gsdFiles, workflowPaths), []); + }); + + test('a self-reference does not reach', () => { + // Nothing in loaderContents mentions self.md; the fact that self.md's + // own body references itself must not count as reachability. + const loaderContents = []; + const gsdFiles = new Map([['workflows/self.md', 'see workflows/self.md']]); + const workflowPaths = ['workflows/self.md']; + assert.deepEqual( + unreachableWorkflows(loaderContents, gsdFiles, workflowPaths), + ['workflows/self.md'], + ); + }); + + test('a mutual-reference island does not reach', () => { + // x and y point only at each other; no loader points at either. The + // island looks connected from the inside but no runtime ever opens it. + const loaderContents = []; + const gsdFiles = new Map([ + ['workflows/x.md', 'see workflows/y.md'], + ['workflows/y.md', 'see workflows/x.md'], + ]); + const workflowPaths = ['workflows/x.md', 'workflows/y.md']; + assert.deepEqual( + unreachableWorkflows(loaderContents, gsdFiles, workflowPaths), + ['workflows/x.md', 'workflows/y.md'], + ); + }); + + test('terminates on a cycle', () => { + // a -> b -> c -> a. All three are reachable from the loader; the walk + // must still terminate rather than looping forever on the cycle. + const loaderContents = ['see workflows/a.md']; + const gsdFiles = new Map([ + ['workflows/a.md', 'see workflows/b.md'], + ['workflows/b.md', 'see workflows/c.md'], + ['workflows/c.md', 'see workflows/a.md'], + ]); + const workflowPaths = ['workflows/a.md', 'workflows/b.md', 'workflows/c.md']; + assert.deepEqual(unreachableWorkflows(loaderContents, gsdFiles, workflowPaths), []); + }); + + test('a docs-only mention does not reach (Goodhart guard: mentioning a path is not the same as loading it)', () => { + // Simulates a hypothetical prose/docs file that names orphan.md — that + // string is simply never included in loaderContents (or gsdFiles), which + // is exactly what "a docs mention" looks like to this function: absent. + const loaderContents = []; + const gsdFiles = new Map(); + const workflowPaths = ['workflows/orphan.md']; + assert.deepEqual( + unreachableWorkflows(loaderContents, gsdFiles, workflowPaths), + ['workflows/orphan.md'], + ); + }); + + test('a dangling reference reaches nothing', () => { + const loaderContents = ['see workflows/missing.md']; + const gsdFiles = new Map(); + const workflowPaths = ['workflows/real-orphan.md']; + assert.doesNotThrow(() => unreachableWorkflows(loaderContents, gsdFiles, workflowPaths)); + assert.deepEqual( + unreachableWorkflows(loaderContents, gsdFiles, workflowPaths), + ['workflows/real-orphan.md'], + ); + }); + + test('empty workflow set', () => { + const loaderContents = ['see workflows/a.md']; + const gsdFiles = new Map(); + const workflowPaths = []; + assert.deepEqual(unreachableWorkflows(loaderContents, gsdFiles, workflowPaths), []); + }); + + test('no loaders means nothing is reached', () => { + const loaderContents = []; + const gsdFiles = new Map(); + const workflowPaths = ['workflows/a.md']; + assert.deepEqual( + unreachableWorkflows(loaderContents, gsdFiles, workflowPaths), + ['workflows/a.md'], + ); + }); + + test('CRLF-tolerant', () => { + const loaderContents = ['workflows/a.md\r\nworkflows/b.md']; + const gsdFiles = new Map(); + const workflowPaths = ['workflows/a.md', 'workflows/b.md']; + assert.deepEqual(unreachableWorkflows(loaderContents, gsdFiles, workflowPaths), []); + }); + + test('reports exactly the orphan among many', () => { + const reachable = Array.from({ length: 152 }, (_, i) => `workflows/gen-${i}.md`); + const loaderContents = reachable.slice(); + const gsdFiles = new Map(); + const orphan = 'workflows/orphan-152.md'; + const workflowPaths = [...reachable, orphan]; + const result = unreachableWorkflows(loaderContents, gsdFiles, workflowPaths); + assert.equal(result.length, 1); + assert.deepEqual(result, [orphan]); + }); +}); + +describe('#3560 — rule 6 fails the build on a real orphan', () => { + // Builds the minimal fixture tree the lint CLI needs to satisfy rules 1-5 + // for a single command file, plus an eagerly-loaded live.md workflow. + // `extra` lets each test layer on exactly the additional state it needs + // without coupling test execution order to shared mutable fixture state. + function buildBaseFixture(dir) { + fs.mkdirSync(path.join(dir, 'commands', 'gsd'), { recursive: true }); + fs.mkdirSync(path.join(dir, 'gsd-core', 'workflows'), { recursive: true }); + fs.mkdirSync(path.join(dir, 'agents'), { recursive: true }); + fs.mkdirSync(path.join(dir, 'skills'), { recursive: true }); + + fs.writeFileSync( + path.join(dir, 'commands', 'gsd', 'fixture-command.md'), + [ + '---', + 'name: gsd:fixture-command', + 'description: Fixture command for the #3560 rule-6 regression tests.', + 'allowed-tools:', + ' - Read', + ' - Bash', + '---', + '', + '', + '@~/.claude/gsd-core/workflows/live.md', + '', + '', + ].join('\n'), + ); + + fs.writeFileSync( + path.join(dir, 'gsd-core', 'workflows', 'live.md'), + '# live workflow\n\nNothing special here.\n', + ); + } + + function runLint(dir) { + return runNode([LINT_SCRIPT, '--root', dir], { timeoutMs: PROBE_TIMEOUT_MS }); + } + + test('clean fixture passes', (t) => { + const dir = createTempDir('gsd-3560-clean-'); + t.after(() => cleanup(dir)); + buildBaseFixture(dir); + + const result = runLint(dir); + assert.equal(result.outcome, OUTCOME.EXITED); + assert.equal(result.exitCode, 0); + }); + + test('a planted orphan fails the build', (t) => { + const dir = createTempDir('gsd-3560-orphan-'); + t.after(() => cleanup(dir)); + buildBaseFixture(dir); + fs.writeFileSync( + path.join(dir, 'gsd-core', 'workflows', 'orphan.md'), + '# orphan workflow\n\nNo loader references this file.\n', + ); + + const result = runLint(dir); + assert.equal(result.outcome, OUTCOME.EXITED); + assert.equal(result.exitCode, 1); + assert.ok( + (result.stdout + result.stderr).includes('gsd-core/workflows/orphan.md'), + 'diagnostic output must name the orphaned file path', + ); + }); + + test('an orphan referenced only from docs/ still fails', (t) => { + const dir = createTempDir('gsd-3560-docs-only-'); + t.after(() => cleanup(dir)); + buildBaseFixture(dir); + fs.writeFileSync( + path.join(dir, 'gsd-core', 'workflows', 'orphan.md'), + '# orphan workflow\n\nNo loader references this file.\n', + ); + fs.mkdirSync(path.join(dir, 'docs'), { recursive: true }); + fs.writeFileSync( + path.join(dir, 'docs', 'SOMETHING.md'), + 'See gsd-core/workflows/orphan.md for details.\n', + ); + + const result = runLint(dir); + assert.equal(result.outcome, OUTCOME.EXITED); + assert.equal(result.exitCode, 1); + }); + + test('an orphan reachable only transitively passes', (t) => { + const dir = createTempDir('gsd-3560-transitive-'); + t.after(() => cleanup(dir)); + buildBaseFixture(dir); + fs.writeFileSync( + path.join(dir, 'gsd-core', 'workflows', 'orphan.md'), + '# orphan workflow\n\nReachable transitively via live.md.\n', + ); + // live.md itself now names orphan.md; live.md is eagerly loaded by the + // fixture command, so orphan.md becomes reachable one hop out. + fs.writeFileSync( + path.join(dir, 'gsd-core', 'workflows', 'live.md'), + '# live workflow\n\nSee workflows/orphan.md for the follow-up.\n', + ); + + const result = runLint(dir); + assert.equal(result.outcome, OUTCOME.EXITED); + assert.equal(result.exitCode, 0); + }); +}); + +describe('#3560 — deleted orphan workflows do not ship', () => { + const deletedBasenames = ['discovery-phase.md', 'plan-milestone-gaps.md']; + + for (const basename of deletedBasenames) { + test(`${basename}: does not exist under gsd-core/workflows/`, () => { + assert.ok( + !fs.existsSync(path.join(GSD_ROOT, 'workflows', basename)), + `${basename}: expected file to be deleted from gsd-core/workflows/, but it still exists`, + ); + }); + } + + const fixturesDir = path.join(ROOT, 'tests', 'fixtures', 'install-tree'); + const fixtureFiles = fs.readdirSync(fixturesDir).filter(f => f.endsWith('.json')); + + for (const fixtureFile of fixtureFiles) { + test(`${fixtureFile}: install-tree manifest contains no entry for a deleted workflow`, () => { + const manifest = JSON.parse(fs.readFileSync(path.join(fixturesDir, fixtureFile), 'utf-8')); + for (const basename of deletedBasenames) { + const gsdRelative = `gsd-core/workflows/${basename}`; + assert.ok( + !manifest.includes(gsdRelative), + `${fixtureFile}: manifest still lists ${gsdRelative}`, + ); + } + }); + } + + test('scan.md is untouched by this PR', () => { + assert.ok( + fs.existsSync(path.join(GSD_ROOT, 'workflows', 'scan.md')), + 'gsd-core/workflows/scan.md is expected to still exist — this PR only deletes ' + + 'discovery-phase.md and plan-milestone-gaps.md', + ); + }); +}); + +describe('#3560 — INVENTORY carries no row for a deleted workflow', () => { + const inventoryFiles = [ + 'docs/INVENTORY.md', + 'docs/ja-JP/INVENTORY.md', + 'docs/ko-KR/INVENTORY.md', + 'docs/zh-CN/INVENTORY.md', + 'docs/pt-BR/INVENTORY.md', + ]; + + for (const rel of inventoryFiles) { + test(`${rel}: mentions neither discovery-phase.md nor plan-milestone-gaps.md`, () => { + const content = fs.readFileSync(path.join(ROOT, rel), 'utf-8'); + assert.ok(!content.includes('discovery-phase.md'), `${rel}: still mentions discovery-phase.md`); + assert.ok(!content.includes('plan-milestone-gaps.md'), `${rel}: still mentions plan-milestone-gaps.md`); + }); + } +}); + // ──────────────────────────────────────────────────────────────────────── // Folded from tests/bug-3168-task-to-agent-rename.test.cjs — consolidation epic #1969 (B3 #1972) // ──────────────────────────────────────────────────────────────────────── diff --git a/tests/fixtures/install-tree/antigravity.json b/tests/fixtures/install-tree/antigravity.json index 26ad303b5..28947622d 100644 --- a/tests/fixtures/install-tree/antigravity.json +++ b/tests/fixtures/install-tree/antigravity.json @@ -241,7 +241,6 @@ "gsd-core/workflows/complete-milestone/steps/git-tag.md", "gsd-core/workflows/debug.md", "gsd-core/workflows/diagnose-issues.md", - "gsd-core/workflows/discovery-phase.md", "gsd-core/workflows/discuss-phase-assumptions.md", "gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md", "gsd-core/workflows/discuss-phase-power.md", @@ -310,7 +309,6 @@ "gsd-core/workflows/note.md", "gsd-core/workflows/onboard.md", "gsd-core/workflows/pause-work.md", - "gsd-core/workflows/plan-milestone-gaps.md", "gsd-core/workflows/plan-phase.md", "gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md", "gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md", diff --git a/tests/fixtures/install-tree/augment.json b/tests/fixtures/install-tree/augment.json index be16a7471..2962913f2 100644 --- a/tests/fixtures/install-tree/augment.json +++ b/tests/fixtures/install-tree/augment.json @@ -312,7 +312,6 @@ "gsd-core/workflows/complete-milestone/steps/git-tag.md", "gsd-core/workflows/debug.md", "gsd-core/workflows/diagnose-issues.md", - "gsd-core/workflows/discovery-phase.md", "gsd-core/workflows/discuss-phase-assumptions.md", "gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md", "gsd-core/workflows/discuss-phase-power.md", @@ -381,7 +380,6 @@ "gsd-core/workflows/note.md", "gsd-core/workflows/onboard.md", "gsd-core/workflows/pause-work.md", - "gsd-core/workflows/plan-milestone-gaps.md", "gsd-core/workflows/plan-phase.md", "gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md", "gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md", diff --git a/tests/fixtures/install-tree/claude-local.json b/tests/fixtures/install-tree/claude-local.json index ce5789006..eaddb062c 100644 --- a/tests/fixtures/install-tree/claude-local.json +++ b/tests/fixtures/install-tree/claude-local.json @@ -311,7 +311,6 @@ "gsd-core/workflows/complete-milestone/steps/git-tag.md", "gsd-core/workflows/debug.md", "gsd-core/workflows/diagnose-issues.md", - "gsd-core/workflows/discovery-phase.md", "gsd-core/workflows/discuss-phase-assumptions.md", "gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md", "gsd-core/workflows/discuss-phase-power.md", @@ -380,7 +379,6 @@ "gsd-core/workflows/note.md", "gsd-core/workflows/onboard.md", "gsd-core/workflows/pause-work.md", - "gsd-core/workflows/plan-milestone-gaps.md", "gsd-core/workflows/plan-phase.md", "gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md", "gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md", diff --git a/tests/fixtures/install-tree/claude.json b/tests/fixtures/install-tree/claude.json index 826b7a0bf..1a5a4b4f4 100644 --- a/tests/fixtures/install-tree/claude.json +++ b/tests/fixtures/install-tree/claude.json @@ -240,7 +240,6 @@ "gsd-core/workflows/complete-milestone/steps/git-tag.md", "gsd-core/workflows/debug.md", "gsd-core/workflows/diagnose-issues.md", - "gsd-core/workflows/discovery-phase.md", "gsd-core/workflows/discuss-phase-assumptions.md", "gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md", "gsd-core/workflows/discuss-phase-power.md", @@ -309,7 +308,6 @@ "gsd-core/workflows/note.md", "gsd-core/workflows/onboard.md", "gsd-core/workflows/pause-work.md", - "gsd-core/workflows/plan-milestone-gaps.md", "gsd-core/workflows/plan-phase.md", "gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md", "gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md", diff --git a/tests/fixtures/install-tree/cline.json b/tests/fixtures/install-tree/cline.json index 3d7ae077b..1ab375a67 100644 --- a/tests/fixtures/install-tree/cline.json +++ b/tests/fixtures/install-tree/cline.json @@ -244,7 +244,6 @@ "gsd-core/workflows/complete-milestone/steps/git-tag.md", "gsd-core/workflows/debug.md", "gsd-core/workflows/diagnose-issues.md", - "gsd-core/workflows/discovery-phase.md", "gsd-core/workflows/discuss-phase-assumptions.md", "gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md", "gsd-core/workflows/discuss-phase-power.md", @@ -313,7 +312,6 @@ "gsd-core/workflows/note.md", "gsd-core/workflows/onboard.md", "gsd-core/workflows/pause-work.md", - "gsd-core/workflows/plan-milestone-gaps.md", "gsd-core/workflows/plan-phase.md", "gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md", "gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md", diff --git a/tests/fixtures/install-tree/codebuddy.json b/tests/fixtures/install-tree/codebuddy.json index 310294c44..6285b7c9e 100644 --- a/tests/fixtures/install-tree/codebuddy.json +++ b/tests/fixtures/install-tree/codebuddy.json @@ -312,7 +312,6 @@ "gsd-core/workflows/complete-milestone/steps/git-tag.md", "gsd-core/workflows/debug.md", "gsd-core/workflows/diagnose-issues.md", - "gsd-core/workflows/discovery-phase.md", "gsd-core/workflows/discuss-phase-assumptions.md", "gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md", "gsd-core/workflows/discuss-phase-power.md", @@ -381,7 +380,6 @@ "gsd-core/workflows/note.md", "gsd-core/workflows/onboard.md", "gsd-core/workflows/pause-work.md", - "gsd-core/workflows/plan-milestone-gaps.md", "gsd-core/workflows/plan-phase.md", "gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md", "gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md", diff --git a/tests/fixtures/install-tree/codex.json b/tests/fixtures/install-tree/codex.json index e08a4a9f6..ad211f83a 100644 --- a/tests/fixtures/install-tree/codex.json +++ b/tests/fixtures/install-tree/codex.json @@ -347,7 +347,6 @@ "gsd-core/workflows/complete-milestone/steps/git-tag.md", "gsd-core/workflows/debug.md", "gsd-core/workflows/diagnose-issues.md", - "gsd-core/workflows/discovery-phase.md", "gsd-core/workflows/discuss-phase-assumptions.md", "gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md", "gsd-core/workflows/discuss-phase-power.md", @@ -416,7 +415,6 @@ "gsd-core/workflows/note.md", "gsd-core/workflows/onboard.md", "gsd-core/workflows/pause-work.md", - "gsd-core/workflows/plan-milestone-gaps.md", "gsd-core/workflows/plan-phase.md", "gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md", "gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md", diff --git a/tests/fixtures/install-tree/copilot.json b/tests/fixtures/install-tree/copilot.json index 632d23154..6771b5508 100644 --- a/tests/fixtures/install-tree/copilot.json +++ b/tests/fixtures/install-tree/copilot.json @@ -242,7 +242,6 @@ "gsd-core/workflows/complete-milestone/steps/git-tag.md", "gsd-core/workflows/debug.md", "gsd-core/workflows/diagnose-issues.md", - "gsd-core/workflows/discovery-phase.md", "gsd-core/workflows/discuss-phase-assumptions.md", "gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md", "gsd-core/workflows/discuss-phase-power.md", @@ -311,7 +310,6 @@ "gsd-core/workflows/note.md", "gsd-core/workflows/onboard.md", "gsd-core/workflows/pause-work.md", - "gsd-core/workflows/plan-milestone-gaps.md", "gsd-core/workflows/plan-phase.md", "gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md", "gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md", diff --git a/tests/fixtures/install-tree/cursor.json b/tests/fixtures/install-tree/cursor.json index ca150c956..eaff0bd35 100644 --- a/tests/fixtures/install-tree/cursor.json +++ b/tests/fixtures/install-tree/cursor.json @@ -241,7 +241,6 @@ "gsd-core/workflows/complete-milestone/steps/git-tag.md", "gsd-core/workflows/debug.md", "gsd-core/workflows/diagnose-issues.md", - "gsd-core/workflows/discovery-phase.md", "gsd-core/workflows/discuss-phase-assumptions.md", "gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md", "gsd-core/workflows/discuss-phase-power.md", @@ -310,7 +309,6 @@ "gsd-core/workflows/note.md", "gsd-core/workflows/onboard.md", "gsd-core/workflows/pause-work.md", - "gsd-core/workflows/plan-milestone-gaps.md", "gsd-core/workflows/plan-phase.md", "gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md", "gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md", diff --git a/tests/fixtures/install-tree/hermes.json b/tests/fixtures/install-tree/hermes.json index 9b441d9fb..b780830e3 100644 --- a/tests/fixtures/install-tree/hermes.json +++ b/tests/fixtures/install-tree/hermes.json @@ -241,7 +241,6 @@ "gsd-core/workflows/complete-milestone/steps/git-tag.md", "gsd-core/workflows/debug.md", "gsd-core/workflows/diagnose-issues.md", - "gsd-core/workflows/discovery-phase.md", "gsd-core/workflows/discuss-phase-assumptions.md", "gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md", "gsd-core/workflows/discuss-phase-power.md", @@ -310,7 +309,6 @@ "gsd-core/workflows/note.md", "gsd-core/workflows/onboard.md", "gsd-core/workflows/pause-work.md", - "gsd-core/workflows/plan-milestone-gaps.md", "gsd-core/workflows/plan-phase.md", "gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md", "gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md", diff --git a/tests/fixtures/install-tree/kilo.json b/tests/fixtures/install-tree/kilo.json index 47bd57173..0d8d99d63 100644 --- a/tests/fixtures/install-tree/kilo.json +++ b/tests/fixtures/install-tree/kilo.json @@ -312,7 +312,6 @@ "gsd-core/workflows/complete-milestone/steps/git-tag.md", "gsd-core/workflows/debug.md", "gsd-core/workflows/diagnose-issues.md", - "gsd-core/workflows/discovery-phase.md", "gsd-core/workflows/discuss-phase-assumptions.md", "gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md", "gsd-core/workflows/discuss-phase-power.md", @@ -381,7 +380,6 @@ "gsd-core/workflows/note.md", "gsd-core/workflows/onboard.md", "gsd-core/workflows/pause-work.md", - "gsd-core/workflows/plan-milestone-gaps.md", "gsd-core/workflows/plan-phase.md", "gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md", "gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md", diff --git a/tests/fixtures/install-tree/kimi-code.json b/tests/fixtures/install-tree/kimi-code.json index 1797cd1ac..00672c92d 100644 --- a/tests/fixtures/install-tree/kimi-code.json +++ b/tests/fixtures/install-tree/kimi-code.json @@ -274,7 +274,6 @@ "gsd-core/workflows/complete-milestone/steps/git-tag.md", "gsd-core/workflows/debug.md", "gsd-core/workflows/diagnose-issues.md", - "gsd-core/workflows/discovery-phase.md", "gsd-core/workflows/discuss-phase-assumptions.md", "gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md", "gsd-core/workflows/discuss-phase-power.md", @@ -343,7 +342,6 @@ "gsd-core/workflows/note.md", "gsd-core/workflows/onboard.md", "gsd-core/workflows/pause-work.md", - "gsd-core/workflows/plan-milestone-gaps.md", "gsd-core/workflows/plan-phase.md", "gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md", "gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md", diff --git a/tests/fixtures/install-tree/kimi.json b/tests/fixtures/install-tree/kimi.json index 9c0febc51..29d78ac04 100644 --- a/tests/fixtures/install-tree/kimi.json +++ b/tests/fixtures/install-tree/kimi.json @@ -310,7 +310,6 @@ "gsd-core/workflows/complete-milestone/steps/git-tag.md", "gsd-core/workflows/debug.md", "gsd-core/workflows/diagnose-issues.md", - "gsd-core/workflows/discovery-phase.md", "gsd-core/workflows/discuss-phase-assumptions.md", "gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md", "gsd-core/workflows/discuss-phase-power.md", @@ -379,7 +378,6 @@ "gsd-core/workflows/note.md", "gsd-core/workflows/onboard.md", "gsd-core/workflows/pause-work.md", - "gsd-core/workflows/plan-milestone-gaps.md", "gsd-core/workflows/plan-phase.md", "gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md", "gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md", diff --git a/tests/fixtures/install-tree/opencode.json b/tests/fixtures/install-tree/opencode.json index 476354904..ec97504e3 100644 --- a/tests/fixtures/install-tree/opencode.json +++ b/tests/fixtures/install-tree/opencode.json @@ -312,7 +312,6 @@ "gsd-core/workflows/complete-milestone/steps/git-tag.md", "gsd-core/workflows/debug.md", "gsd-core/workflows/diagnose-issues.md", - "gsd-core/workflows/discovery-phase.md", "gsd-core/workflows/discuss-phase-assumptions.md", "gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md", "gsd-core/workflows/discuss-phase-power.md", @@ -381,7 +380,6 @@ "gsd-core/workflows/note.md", "gsd-core/workflows/onboard.md", "gsd-core/workflows/pause-work.md", - "gsd-core/workflows/plan-milestone-gaps.md", "gsd-core/workflows/plan-phase.md", "gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md", "gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md", diff --git a/tests/fixtures/install-tree/pi.json b/tests/fixtures/install-tree/pi.json index d057190f1..3aeb29bad 100644 --- a/tests/fixtures/install-tree/pi.json +++ b/tests/fixtures/install-tree/pi.json @@ -209,7 +209,6 @@ "gsd-core/workflows/complete-milestone/steps/git-tag.md", "gsd-core/workflows/debug.md", "gsd-core/workflows/diagnose-issues.md", - "gsd-core/workflows/discovery-phase.md", "gsd-core/workflows/discuss-phase-assumptions.md", "gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md", "gsd-core/workflows/discuss-phase-power.md", @@ -278,7 +277,6 @@ "gsd-core/workflows/note.md", "gsd-core/workflows/onboard.md", "gsd-core/workflows/pause-work.md", - "gsd-core/workflows/plan-milestone-gaps.md", "gsd-core/workflows/plan-phase.md", "gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md", "gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md", diff --git a/tests/fixtures/install-tree/qwen.json b/tests/fixtures/install-tree/qwen.json index 1f4061faf..e88b85544 100644 --- a/tests/fixtures/install-tree/qwen.json +++ b/tests/fixtures/install-tree/qwen.json @@ -241,7 +241,6 @@ "gsd-core/workflows/complete-milestone/steps/git-tag.md", "gsd-core/workflows/debug.md", "gsd-core/workflows/diagnose-issues.md", - "gsd-core/workflows/discovery-phase.md", "gsd-core/workflows/discuss-phase-assumptions.md", "gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md", "gsd-core/workflows/discuss-phase-power.md", @@ -310,7 +309,6 @@ "gsd-core/workflows/note.md", "gsd-core/workflows/onboard.md", "gsd-core/workflows/pause-work.md", - "gsd-core/workflows/plan-milestone-gaps.md", "gsd-core/workflows/plan-phase.md", "gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md", "gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md", diff --git a/tests/fixtures/install-tree/trae.json b/tests/fixtures/install-tree/trae.json index 603ff7c94..fb1bc5982 100644 --- a/tests/fixtures/install-tree/trae.json +++ b/tests/fixtures/install-tree/trae.json @@ -241,7 +241,6 @@ "gsd-core/workflows/complete-milestone/steps/git-tag.md", "gsd-core/workflows/debug.md", "gsd-core/workflows/diagnose-issues.md", - "gsd-core/workflows/discovery-phase.md", "gsd-core/workflows/discuss-phase-assumptions.md", "gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md", "gsd-core/workflows/discuss-phase-power.md", @@ -310,7 +309,6 @@ "gsd-core/workflows/note.md", "gsd-core/workflows/onboard.md", "gsd-core/workflows/pause-work.md", - "gsd-core/workflows/plan-milestone-gaps.md", "gsd-core/workflows/plan-phase.md", "gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md", "gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md", diff --git a/tests/fixtures/install-tree/windsurf.json b/tests/fixtures/install-tree/windsurf.json index 911112d72..322930e24 100644 --- a/tests/fixtures/install-tree/windsurf.json +++ b/tests/fixtures/install-tree/windsurf.json @@ -241,7 +241,6 @@ "gsd-core/workflows/complete-milestone/steps/git-tag.md", "gsd-core/workflows/debug.md", "gsd-core/workflows/diagnose-issues.md", - "gsd-core/workflows/discovery-phase.md", "gsd-core/workflows/discuss-phase-assumptions.md", "gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md", "gsd-core/workflows/discuss-phase-power.md", @@ -310,7 +309,6 @@ "gsd-core/workflows/note.md", "gsd-core/workflows/onboard.md", "gsd-core/workflows/pause-work.md", - "gsd-core/workflows/plan-milestone-gaps.md", "gsd-core/workflows/plan-phase.md", "gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md", "gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md", diff --git a/tests/fixtures/install-tree/zcode.json b/tests/fixtures/install-tree/zcode.json index 6c5729610..0434cd3e5 100644 --- a/tests/fixtures/install-tree/zcode.json +++ b/tests/fixtures/install-tree/zcode.json @@ -312,7 +312,6 @@ "gsd-core/workflows/complete-milestone/steps/git-tag.md", "gsd-core/workflows/debug.md", "gsd-core/workflows/diagnose-issues.md", - "gsd-core/workflows/discovery-phase.md", "gsd-core/workflows/discuss-phase-assumptions.md", "gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md", "gsd-core/workflows/discuss-phase-power.md", @@ -381,7 +380,6 @@ "gsd-core/workflows/note.md", "gsd-core/workflows/onboard.md", "gsd-core/workflows/pause-work.md", - "gsd-core/workflows/plan-milestone-gaps.md", "gsd-core/workflows/plan-phase.md", "gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md", "gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md", diff --git a/tests/phase.test.cjs b/tests/phase.test.cjs index b9648d1f3..93d2dd85c 100644 --- a/tests/phase.test.cjs +++ b/tests/phase.test.cjs @@ -6069,7 +6069,6 @@ describe('bug-3287 — init plan-phase exposes expected_phase_dir with project_c // ───────────────────────────────────────────────────────────────────────────── { - const PMG_WF = path.join(__dirname, '..', 'gsd-core', 'workflows', 'plan-milestone-gaps.md'); const IMPORT_WF = path.join(__dirname, '..', 'gsd-core', 'workflows', 'import.md'); const BACKLOG_WF = path.join(__dirname, '..', 'gsd-core', 'workflows', 'add-backlog.md'); @@ -6090,29 +6089,9 @@ describe('bug-3287 — init plan-phase exposes expected_phase_dir with project_c || /mkdir[^`\r\n]*\.planning\/phases\/\$\{(?:NEXT|NN|PHASE)[^}]*\}-/.test(content); } - describe('bug-3298 — plan-milestone-gaps.md must not construct bare {NN}-{name} phase dirs', () => { - test('workflow file exists', () => { - assert.ok(fs.existsSync(PMG_WF), `plan-milestone-gaps.md must exist at ${PMG_WF}`); - }); - - test('step 8 must not use bare {NN}-{name} mkdir pattern', () => { - const content = readWorkflow(PMG_WF); - assert.ok( - !containsBareTemplateMkdir(content), - 'plan-milestone-gaps.md must not contain bare mkdir .planning/phases/{NN}-{name} pattern — use phase.add or expected_phase_dir', - ); - }); - - test('step 8 must use expected_phase_dir or phase.add for directory creation', () => { - const content = readWorkflow(PMG_WF); - const usesExpectedPhaseDir = content.includes('expected_phase_dir'); - const usesPhaseAdd = content.includes('phase.add'); - assert.ok( - usesExpectedPhaseDir || usesPhaseAdd, - 'plan-milestone-gaps.md must use expected_phase_dir (from init.phase-op) or phase.add to create phase directories with project_code prefix', - ); - }); - }); + // The plan-milestone-gaps arm was removed along with that workflow file in + // #3560 (its command was deleted by #2790). The import.md and + // add-backlog.md arms below still guard the same phase-dir prefix drift. describe('bug-3298 — import.md must not construct bare {NN}-{slug} phase dirs', () => { test('workflow file exists', () => { diff --git a/tests/planner-language-regression.test.cjs b/tests/planner-language-regression.test.cjs index f13eeac10..89b8fad12 100644 --- a/tests/planner-language-regression.test.cjs +++ b/tests/planner-language-regression.test.cjs @@ -114,8 +114,6 @@ const ALLOWLIST = { 'gsd-debugger.md': ['time_sizing'], // Doc-writer uses "15 minutes" in API rate limit example, "2 minutes" for doc quality 'gsd-doc-writer.md': ['time_sizing'], - // Discovery-phase uses time for level descriptions (operational, not scope) - 'discovery-phase.md': ['time_sizing'], // Explore uses "~30 seconds" as operational estimate 'explore.md': ['time_sizing'], // Review uses "up to 5 minutes" for CodeRabbit timeout