* chore(#3560): delete two unreachable workflows, gate reachability in lint discovery-phase.md and plan-milestone-gaps.md shipped to all 19 runtime install trees with no command, agent, or skill referencing them. plan-milestone-gaps' command was deleted by #2790 and the workflow was left behind; discovery-phase's own header claimed a caller in plan-phase.md's mandatory_discovery step, and that step does not exist — plan-phase.md contains zero occurrences of "discovery". docs/INVENTORY.md asserted discovery-phase.md was an alternate entry for /gsd-new-project. new-project.md never referenced it. The row and the matching note sentence are removed across all five locales rather than corrected. Adds rule 6 to lint-command-contract: every shipped workflow must be reachable from a loader, walking the transitive closure over the three reference shapes this repo uses. The closure seeds ONLY from commands/agents/skills, so a workflow that references only itself and a pair that reference only each other are both correctly reported rather than satisfying themselves; a visited set makes reference cycles terminate. The measure is a mention in a LOADER — docs/ and install-tree fixtures deliberately do not count, because scan.md proved a file can be documented and shipped while entirely unreached. Ships blocking, not report-only: #3561 is in this branch's base, so the tree reports 0 unreachable from the start. Closes #3560 * test(#3560): drive rule 6 end-to-end, sweep a stale allowlist, update ADR-0002 Review findings. Rule 6 had no end-to-end coverage: the tests exercised the pure closure with in-memory data, so the wiring — file collection, exit code, diagnostic — was unproven, and #3560's acceptance list explicitly wants a fixture showing the rule FAILS on a planted orphan. Adds an optional --root to lint-command-contract (default behavior unchanged) and four tests driving the real CLI through the process seam against a temp fixture: clean=0, planted orphan=1, orphan referenced only from docs/=1, orphan reachable transitively=0. The docs/ case is what pins the Goodhart defense — a mention outside a loader must not confer reachability. Deletes two tests that were byte-identical to a third and could not assert anything loader-specific, since the closure is source-agnostic by design; that distinction lives in the lint script's file collection and is now covered above. Removes a stale ALLOWLIST entry for discovery-phase.md in planner-language-regression — the exact sweep-miss class rule 6 exists to catch, found in the PR that adds the rule. ADR-0002 described five per-file frontmatter checks; rule 6 is a repo-level reachability graph, so the Decision section now says so. Refs #3560 * test(#3560): cut the bug-3298 test pin on the deleted plan-milestone-gaps workflow The remote runner went red with four failures: tests/phase.test.cjs asserted the plan-milestone-gaps workflow exists and checked its mkdir patterns, so deleting the file broke the test that pinned it. This is the fence the epic describes — the content-sync test IS what keeps an unreachable file alive — and cutting the coupling is what makes the deletion safe. Removes only that arm. The bug-3298 block guards three workflows against phase-dir prefix drift; the import and add-backlog arms and both shared mkdir-pattern helpers are untouched. Worth recording where the sweep failed: my reachability walk covered commands, agents, skills, gsd-core and docs, and lint-removed-but-needed covers .github/workflows, gsd-core, docs and package.json. Neither looks at tests/, so a test-pinned deletion is invisible to both and surfaces only on the remote runner. The how-to added by this PR names that gap explicitly so the next deletion searches tests/ by hand. Refs #3560 * docs(#3560): add a how-to for resolving unreachable-workflow findings * chore(#3560): backfill changeset pr number to 3564 --------- Co-authored-by: sim <sim@local>
This commit is contained in:
5
.changeset/fierce-mice-munch.md
Normal file
5
.changeset/fierce-mice-munch.md
Normal file
@@ -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)
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 `<execution_context>` blocks resolves to an existing file on disk
|
||||
- `@`-references inside `<execution_context>` 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 `<execution_context>` 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
|
||||
|
||||
|
||||
117
docs/how-to/resolve-unreachable-workflow-findings.md
Normal file
117
docs/how-to/resolve-unreachable-workflow-findings.md
Normal file
@@ -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/<name>.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.
|
||||
@@ -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`)— これらはオーケストレーターワークフローによって内部的に呼び出されます。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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`) — 이들은 오케스트레이터 워크플로우에 의해 내부적으로 호출됩니다.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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`)— 它们由编排器工作流在内部调用。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,298 +0,0 @@
|
||||
<purpose>
|
||||
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.
|
||||
</purpose>
|
||||
```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.
|
||||
|
||||
|
||||
<depth_levels>
|
||||
**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.**
|
||||
</depth_levels>
|
||||
|
||||
<source_hierarchy>
|
||||
**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 `<discovery_protocol>` for full protocol.
|
||||
</source_hierarchy>
|
||||
|
||||
<process>
|
||||
|
||||
<step name="determine_depth">
|
||||
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.
|
||||
</step>
|
||||
|
||||
<step name="level_1_quick_verify">
|
||||
**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.
|
||||
</step>
|
||||
|
||||
<step name="level_2_standard">
|
||||
**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`
|
||||
</step>
|
||||
|
||||
<step name="level_3_deep_dive">
|
||||
**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)
|
||||
</step>
|
||||
|
||||
<step name="identify_unknowns">
|
||||
**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?
|
||||
</step>
|
||||
|
||||
<step name="create_discovery_scope">
|
||||
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
|
||||
</step>
|
||||
|
||||
<step name="execute_discovery">
|
||||
Run the discovery:
|
||||
- Use web search for current info
|
||||
- Use Context7 MCP for library docs
|
||||
- Prefer current year sources
|
||||
- Structure findings per template
|
||||
</step>
|
||||
|
||||
<step name="create_discovery_output">
|
||||
Write `.planning/phases/XX-name/DISCOVERY.md`:
|
||||
- Summary with recommendation
|
||||
- Key findings with sources
|
||||
- Code examples if applicable
|
||||
- Metadata (confidence, dependencies, open questions, assumptions)
|
||||
</step>
|
||||
|
||||
<step name="confidence_gate">
|
||||
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)."
|
||||
</step>
|
||||
|
||||
<step name="open_questions_gate">
|
||||
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.
|
||||
</step>
|
||||
|
||||
<step name="offer_next">
|
||||
```
|
||||
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.
|
||||
</step>
|
||||
|
||||
</process>
|
||||
|
||||
<success_criteria>
|
||||
**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
|
||||
</success_criteria>
|
||||
@@ -1,281 +0,0 @@
|
||||
<purpose>
|
||||
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.
|
||||
</purpose>
|
||||
|
||||
<required_reading>
|
||||
Read all files referenced by the invoking prompt's execution_context before starting.
|
||||
</required_reading>
|
||||
|
||||
<process>
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
</process>
|
||||
|
||||
<gap_to_phase_mapping>
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
</gap_to_phase_mapping>
|
||||
|
||||
<success_criteria>
|
||||
- [ ] 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
|
||||
</success_criteria>
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
'---',
|
||||
'',
|
||||
'<execution_context>',
|
||||
'@~/.claude/gsd-core/workflows/live.md',
|
||||
'</execution_context>',
|
||||
'',
|
||||
].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)
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
2
tests/fixtures/install-tree/antigravity.json
vendored
2
tests/fixtures/install-tree/antigravity.json
vendored
@@ -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",
|
||||
|
||||
2
tests/fixtures/install-tree/augment.json
vendored
2
tests/fixtures/install-tree/augment.json
vendored
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
2
tests/fixtures/install-tree/claude.json
vendored
2
tests/fixtures/install-tree/claude.json
vendored
@@ -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",
|
||||
|
||||
2
tests/fixtures/install-tree/cline.json
vendored
2
tests/fixtures/install-tree/cline.json
vendored
@@ -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",
|
||||
|
||||
2
tests/fixtures/install-tree/codebuddy.json
vendored
2
tests/fixtures/install-tree/codebuddy.json
vendored
@@ -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",
|
||||
|
||||
2
tests/fixtures/install-tree/codex.json
vendored
2
tests/fixtures/install-tree/codex.json
vendored
@@ -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",
|
||||
|
||||
2
tests/fixtures/install-tree/copilot.json
vendored
2
tests/fixtures/install-tree/copilot.json
vendored
@@ -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",
|
||||
|
||||
2
tests/fixtures/install-tree/cursor.json
vendored
2
tests/fixtures/install-tree/cursor.json
vendored
@@ -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",
|
||||
|
||||
2
tests/fixtures/install-tree/hermes.json
vendored
2
tests/fixtures/install-tree/hermes.json
vendored
@@ -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",
|
||||
|
||||
2
tests/fixtures/install-tree/kilo.json
vendored
2
tests/fixtures/install-tree/kilo.json
vendored
@@ -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",
|
||||
|
||||
2
tests/fixtures/install-tree/kimi-code.json
vendored
2
tests/fixtures/install-tree/kimi-code.json
vendored
@@ -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",
|
||||
|
||||
2
tests/fixtures/install-tree/kimi.json
vendored
2
tests/fixtures/install-tree/kimi.json
vendored
@@ -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",
|
||||
|
||||
2
tests/fixtures/install-tree/opencode.json
vendored
2
tests/fixtures/install-tree/opencode.json
vendored
@@ -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",
|
||||
|
||||
2
tests/fixtures/install-tree/pi.json
vendored
2
tests/fixtures/install-tree/pi.json
vendored
@@ -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",
|
||||
|
||||
2
tests/fixtures/install-tree/qwen.json
vendored
2
tests/fixtures/install-tree/qwen.json
vendored
@@ -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",
|
||||
|
||||
2
tests/fixtures/install-tree/trae.json
vendored
2
tests/fixtures/install-tree/trae.json
vendored
@@ -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",
|
||||
|
||||
2
tests/fixtures/install-tree/windsurf.json
vendored
2
tests/fixtures/install-tree/windsurf.json
vendored
@@ -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",
|
||||
|
||||
2
tests/fixtures/install-tree/zcode.json
vendored
2
tests/fixtures/install-tree/zcode.json
vendored
@@ -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",
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user