From afaefe5431d95d156d4c3576a1ab062b943384b4 Mon Sep 17 00:00:00 2001 From: domattioli Date: Sat, 4 Jul 2026 21:42:41 -0400 Subject: [PATCH 1/3] Add pipeline extension Chains the Spec Kit phases into one guided, single-invocation pipeline with a deterministic phase resolver and one interactive clarify gate. --- extensions/pipeline/.gitignore | 4 + extensions/pipeline/CHANGELOG.md | 21 ++ extensions/pipeline/LICENSE | 21 ++ extensions/pipeline/README.md | 98 ++++++++ extensions/pipeline/commands/preview.md | 29 +++ extensions/pipeline/commands/run.md | 87 +++++++ extensions/pipeline/config-template.yml | 21 ++ extensions/pipeline/extension.yml | 37 +++ .../pipeline/scripts/bash/resolve-phases.sh | 7 + extensions/pipeline/scripts/phase_registry.py | 221 ++++++++++++++++++ .../scripts/powershell/resolve-phases.ps1 | 8 + extensions/pipeline/scripts/resolve_phases.py | 104 +++++++++ .../pipeline/tests/test_phase_registry.py | 141 +++++++++++ 13 files changed, 799 insertions(+) create mode 100644 extensions/pipeline/.gitignore create mode 100644 extensions/pipeline/CHANGELOG.md create mode 100644 extensions/pipeline/LICENSE create mode 100644 extensions/pipeline/README.md create mode 100644 extensions/pipeline/commands/preview.md create mode 100644 extensions/pipeline/commands/run.md create mode 100644 extensions/pipeline/config-template.yml create mode 100644 extensions/pipeline/extension.yml create mode 100755 extensions/pipeline/scripts/bash/resolve-phases.sh create mode 100755 extensions/pipeline/scripts/phase_registry.py create mode 100644 extensions/pipeline/scripts/powershell/resolve-phases.ps1 create mode 100755 extensions/pipeline/scripts/resolve_phases.py create mode 100644 extensions/pipeline/tests/test_phase_registry.py diff --git a/extensions/pipeline/.gitignore b/extensions/pipeline/.gitignore new file mode 100644 index 0000000000..10b940d843 --- /dev/null +++ b/extensions/pipeline/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +*.pyc +.pytest_cache/ +.DS_Store diff --git a/extensions/pipeline/CHANGELOG.md b/extensions/pipeline/CHANGELOG.md new file mode 100644 index 0000000000..d88a6a96fa --- /dev/null +++ b/extensions/pipeline/CHANGELOG.md @@ -0,0 +1,21 @@ +# Changelog + +All notable changes to the Pipeline extension are documented here. Format based on +[Keep a Changelog](https://keepachangelog.com/); this project adheres to Semantic Versioning. + +## [1.0.0] - 2026-07-05 + +### Added + +- Initial release. +- `speckit.pipeline.run` — chains `specify → clarify → plan → tasks → analyze → implement` + into one guided invocation with a single interactive clarify gate and an + analyze → fix → re-analyze loop (≤3 cycles) before implement. +- `speckit.pipeline.preview` — dry-run printer for the resolved phase plan. +- `--skip` / `--add` flags with a deterministic phase resolver (`scripts/resolve_phases.py`), + strict validation, and dedicated exit codes (10–14). +- Insertable phases: `constitution` (before specify), `checklist` (after tasks). +- `--yes` unattended mode: answers the clarify gate in-place, halting before `plan` + on a question too consequential to answer without a human. +- Bash + PowerShell wrappers over the Python resolver. +- Stdlib `unittest` suite covering default order, permutation invariance, and every exit code. diff --git a/extensions/pipeline/LICENSE b/extensions/pipeline/LICENSE new file mode 100644 index 0000000000..1f2f1cb34b --- /dev/null +++ b/extensions/pipeline/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Dominik Mattioli + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/extensions/pipeline/README.md b/extensions/pipeline/README.md new file mode 100644 index 0000000000..14e99d89ab --- /dev/null +++ b/extensions/pipeline/README.md @@ -0,0 +1,98 @@ +# Pipeline — a Spec Kit extension + +Chain the [Spec Kit](https://github.com/github/spec-kit) phases into **one guided, single-invocation pipeline** instead of hand-running seven commands. + +``` +specify → clarify → plan → tasks → analyze → implement +``` + +`/speckit.pipeline.run` drives each phase in order, pausing exactly once — at an interactive **clarify** gate — then advances unattended through the rest, re-running `analyze` until findings are resolved (up to 3 cycles) before it implements. The phase order is produced by a small, deterministic resolver, so a tailored pipeline (`--skip`, `--add`) always lands in the same canonical order. + +## Why + +Taking a feature from description to implemented change means issuing `/speckit.specify`, `/speckit.clarify`, `/speckit.plan`, `/speckit.tasks`, `/speckit.analyze`, `/speckit.implement` by hand — and hand-resolving each analyze finding in between. This extension collapses that to a single invocation with one human checkpoint, while keeping every phase's own command as the source of truth (it *drives* the stock commands, it does not reimplement them). + +## Commands + +| Command | What it does | +|---|---| +| `speckit.pipeline.run` | Run the full pipeline from one feature description, with a single clarify checkpoint. | +| `speckit.pipeline.preview` | Print the resolved phase plan for the given flags without running anything (dry run). | + +### Flags + +- `--skip ` — drop default phases (e.g. `--skip clarify,analyze`). `specify` and `implement` cannot be skipped. +- `--add ` — insert optional phases: `constitution` (before specify), `checklist` (after tasks). +- `--yes` — unattended run. Answers the clarify gate itself (grounded in the spec/repo) rather than pausing for a human, and **halts before `plan`** if a question is too consequential to answer unattended. `--yes` never means "no clarification" — use `--skip clarify` for that. + +## Examples + +``` +# Full default run +/speckit.pipeline.run Add rate limiting to the public API + +# See the plan first, then run a tailored pipeline +/speckit.pipeline.preview --add checklist --skip clarify +/speckit.pipeline.run --add checklist Add a healthcheck endpoint + +# Unattended (CI / routine), clarify answered in-place, halts on a consequential question +/speckit.pipeline.run --yes Migrate config loading to env vars +``` + +## How the resolver works + +`scripts/resolve_phases.py` (pure Python, stdlib only) is the deterministic core. It takes the requested skip/add sets, validates them, and returns the phase list in a fixed canonical order. Ordering is a pure function of the *effective set*, so flag ordering never changes the result. Validation is strict, with dedicated exit codes: + +| Exit | Meaning | +|---|---| +| 0 | resolved OK | +| 10 | unknown phase name | +| 11 | a phase is in both `--skip` and `--add` | +| 12 | `--add` names a non-insertable phase | +| 13 | `--skip` targets a required phase (`specify`/`implement`) | +| 14 | dependency break — a retained phase's dependency was skipped | + +`run` and `preview` both call the resolver, so a bad flag combination is caught before any phase runs. `scripts/bash/resolve-phases.sh` and `scripts/powershell/resolve-phases.ps1` are thin wrappers over the same Python, matching Spec Kit's `scripts.sh` / `scripts.ps` command convention. + +## Agent-neutral + +The extension ships no dependency on any specific AI agent, model, or vendor tooling — it drives only stock `/speckit.*` commands (or their skills-mode equivalents `speckit-plan`, `speckit-tasks`, …). It works in any Spec Kit-initialized project regardless of which of the 30+ supported agents you use. + +## Layout + +``` +pipeline/ +├── extension.yml # manifest +├── commands/ +│ ├── run.md # the orchestrator command +│ └── preview.md # dry-run phase-plan printer +├── scripts/ +│ ├── phase_registry.py # deterministic resolver core (pure, no I/O) +│ ├── resolve_phases.py # CLI over the resolver (exit codes 10–14) +│ ├── bash/resolve-phases.sh +│ └── powershell/resolve-phases.ps1 +├── config-template.yml # optional per-project defaults +├── tests/test_phase_registry.py # stdlib unittest — determinism + exit codes +├── README.md +├── CHANGELOG.md +└── LICENSE +``` + +## Install + +Until it is listed in the community catalog, install by copying `pipeline/` into your project's Spec Kit extensions location (or your fork's `extensions/pipeline/`) so the two commands register on init. See the catalog submission notes in the parent directory's contribution guide. + +## Tests + +``` +cd pipeline +python3 -m unittest discover -s tests -p 'test_*.py' +``` + +## License + +MIT — see [LICENSE](LICENSE). + +## Provenance + +Ported from the `speckit-pipeline` orchestration skill (formerly `speckit-workflow`), decoupled from its origin repo's internal tooling into a portable, agent-neutral Spec Kit extension. diff --git a/extensions/pipeline/commands/preview.md b/extensions/pipeline/commands/preview.md new file mode 100644 index 0000000000..4b303867de --- /dev/null +++ b/extensions/pipeline/commands/preview.md @@ -0,0 +1,29 @@ +--- +description: "Print the resolved Spec Kit pipeline phase plan for the given --skip/--add flags without running anything — a dry run of the deterministic phase resolver." +scripts: + sh: scripts/bash/resolve-phases.sh + ps: scripts/powershell/resolve-phases.ps1 +--- + +# Pipeline: preview + +Show the phase plan `/speckit.pipeline.run` *would* execute for a given set of flags, without running any phase. Use it to confirm a tailored pipeline (skips/adds) resolves the way you expect before committing to a full run. + +## Input + +`$ARGUMENTS` — optional flags only (no feature description needed): + +- `--skip ` — default phases to drop. +- `--add ` — insertable phases to add (`constitution`, `checklist`). + +## Behavior + +Run the resolver and print its output: + +``` +{SCRIPT} --skip "" --add "" +``` + +Each line is `order phase command gate description`, in the exact order `run` would execute. `--list` (no flags) dumps the full registry of orderable phases. + +Report the resolver's exit code and, on any non-zero code (`10`–`14`), its stderr message verbatim — the same validation `run` performs, so a bad flag combination is caught here first. This command never edits files and never invokes a `/speckit.*` phase. diff --git a/extensions/pipeline/commands/run.md b/extensions/pipeline/commands/run.md new file mode 100644 index 0000000000..424551ed1a --- /dev/null +++ b/extensions/pipeline/commands/run.md @@ -0,0 +1,87 @@ +--- +description: "Run the full Spec Kit pipeline (specify → clarify → plan → tasks → analyze → implement) from one feature description, with a single interactive clarify checkpoint and a deterministic, tailorable phase order." +scripts: + sh: scripts/bash/resolve-phases.sh + ps: scripts/powershell/resolve-phases.ps1 +--- + +# Pipeline: run + +Carry one feature from a plain-language description to an implemented change by chaining the existing Spec Kit phase commands into a single guided run. You (the agent) **are** the orchestrator: you follow this procedure turn by turn, invoking each `/speckit.*` command in order and verifying its artifact landed before moving on. This replaces hand-running `/speckit.specify`, `/speckit.clarify`, `/speckit.plan`, `/speckit.tasks`, `/speckit.analyze`, and `/speckit.implement` one at a time and hand-resolving analyze findings. + +## Input + +`$ARGUMENTS` — the feature description, optionally followed by flags: + +- `--skip ` — drop default phases (e.g. `--skip clarify,analyze`). Cannot drop `specify` or `implement`. +- `--add ` — insert optional phases: `constitution` (before specify), `checklist` (after tasks). +- `--yes` — unattended run. Does **not** skip clarification; instead you answer the clarify questions yourself, grounded in the spec and repo conventions, and halt before `plan` if a question is too consequential to answer without a human (see Step 4). Use `--skip clarify` if you genuinely want zero clarification. + +If the feature description is empty, report the missing input and stop — do not start. + +## Step 1 — Resolve the phase plan (deterministic) + +Run the resolver once and branch on its exit code: + +``` +{SCRIPT} --skip "" --add "" --json +``` + +(`{SCRIPT}` is this command's configured `scripts.sh` / `scripts.ps`.) + +Exit codes: `0` OK · `10` unknown phase name · `11` skip/add name conflict · `12` add-name not insertable · `13` skip targets a required phase · `14` dependency break. On any non-zero code, report the resolver's stderr message verbatim and stop — do not run a partial or incoherent pipeline. The order is deterministic: the resolver consumes skip/add as sets and derives order from one fixed key, so flag ordering never changes the plan. + +## Step 2 — Preflight + +Confirm the project is Spec Kit-initialized (a `.specify/` directory exists) and that every `/speckit.*` command named in the resolved plan is available in this agent's command set. Report anything missing now — never mid-pipeline. If `--add constitution` or `--add checklist` was requested but that command isn't installed, report and stop. + +## Step 3 — Execute each phase in order + +For each phase in the resolved plan, invoke its `/speckit.*` command (the `command` field from the resolver output). After each phase: + +- **Verify the artifact.** Confirm the expected file was written (`spec.md`, `plan.md`, `tasks.md`, etc.) before proceeding. A phase reporting success is not enough — check the artifact is on disk. +- **Halt on failure.** If a phase fails and the failure is not something you can safely resolve, stop and name the phase and reason. Never proceed past a broken artifact. + +Keep a short running ledger (which phases ran, which were skipped, outcome of each) so the run survives a context reset. + +## Step 4 — The clarify gate + +`clarify` is the single interactive checkpoint; everything after it runs unattended. + +**Default (no `--yes`)** — run `/speckit.clarify`, present its questions to the human, and **end your turn to wait** for answers. This is the one human checkpoint. + +**`--yes` set** — no human is present, so `--yes` does not mean "skip clarification", it means "answer it responsibly yourself": + +1. Run `/speckit.clarify`'s question generation as normal, but don't present the questions to a human. +2. Answer each question as the operator plausibly would, grounded strictly in the spec's own content, the project's constitution/conventions, and established repo patterns. Integrate each answer into the spec exactly as `/speckit.clarify` integrates a human's answer. +3. Emit one line noting that clarify was answered unattended, not by a human, so the audit trail is honest. +4. **If any question is too consequential to answer without a human** — insufficient grounding, security/scope/privacy stakes, or a materially shape-changing decision the spec leaves open — do **not** guess. Halt before `plan`, name the unresolved question and why. This is a correct outcome, not a failure: it means the run recognized it should not proceed unattended past this point. + +## Step 5 — The analyze → resolve loop + +After `/speckit.analyze` reports findings, fix each finding (edit the spec/plan/tasks as needed), then re-run `/speckit.analyze`. Repeat **up to 3 cycles**. If findings remain unresolved after the third cycle, halt and report them rather than implementing against a known-inconsistent spec. + +## Step 6 — Unattended discipline (clarify-satisfied → implement) + +From the moment clarify is satisfied through `implement`, run without acknowledgment chatter between phases, but: + +- **Log every skip and every fallback.** Silent deviation from the plan is not allowed — if you skip or work around something, say so in the ledger. +- **Halt on destructive or irreversible actions** during `implement` (deleting data, force-pushing, rewriting shared history) and ask, rather than proceeding blindly. +- Any phase failure that isn't auto-resolvable, or is unsafe to continue past, halts **before** `implement`, naming the phase and reason. + +## Hard stops + +| Condition | Behavior | +|---|---| +| Empty feature description | Report missing input; do not start. | +| Resolver returns non-zero | Report its message; run no phase. | +| `.specify/` or a required `/speckit.*` command missing at preflight | Report; do not start. | +| Expected artifact absent after a phase | Halt, name the phase; do not proceed. | +| Analyze findings unresolved after 3 cycles | Halt; report the unresolved findings. | +| A clarify question too consequential to answer unattended (`--yes`) | Halt before `plan`, name the question and why; never guess. | +| Destructive/irreversible action during implement | Halt and ask. | + +## Notes + +- **Agent-neutral.** This command drives only stock `/speckit.*` commands and ships no dependency on any specific AI agent, model, or vendor tooling. In an agent that renders Spec Kit commands as skills, the same phases appear as `speckit-plan`, `speckit-tasks`, etc. — invoke whichever form your agent exposes. +- **Preview first.** Run `/speckit.pipeline.preview` with the same flags to see the resolved plan before committing to a full run. diff --git a/extensions/pipeline/config-template.yml b/extensions/pipeline/config-template.yml new file mode 100644 index 0000000000..2139c703ca --- /dev/null +++ b/extensions/pipeline/config-template.yml @@ -0,0 +1,21 @@ +# Pipeline extension configuration. +# Copy to `pipeline-config.yml` in your project's Spec Kit config location and edit. +# Every key is optional; the values below are the built-in defaults. + +pipeline: + # Default phases to skip on every `/speckit.pipeline.run` unless overridden by --skip. + # Cannot include the required phases `specify` or `implement`. + default_skip: [] + + # Default insertable phases to add on every run unless overridden by --add. + # Valid values: constitution, checklist. + default_add: [] + + # Maximum analyze → fix → re-analyze cycles before the run halts and reports + # remaining findings instead of implementing against an inconsistent spec. + analyze_max_cycles: 3 + + # When true, `--yes` runs answer the clarify gate unattended (grounded in the + # spec/repo) instead of pausing for a human. A question too consequential to + # answer without a human still halts the run before `plan`. + answer_clarify_unattended: false diff --git a/extensions/pipeline/extension.yml b/extensions/pipeline/extension.yml new file mode 100644 index 0000000000..fea7dcdc33 --- /dev/null +++ b/extensions/pipeline/extension.yml @@ -0,0 +1,37 @@ +schema_version: "1.0" + +extension: + id: "pipeline" + name: "Pipeline" + version: "1.0.0" + description: "Chain the Spec Kit phases (specify → clarify → plan → tasks → analyze → implement) into one guided, single-invocation pipeline with a deterministic phase resolver and one interactive clarify gate." + author: "Dominik Mattioli" + repository: "https://github.com/domattioli/spec-kit-pipeline" + license: "MIT" + homepage: "https://github.com/domattioli/spec-kit-pipeline" + +requires: + speckit_version: ">=0.2.0" + +provides: + commands: + - name: "speckit.pipeline.run" + file: "commands/run.md" + description: "Run the full spec → implement pipeline from one feature description, with a single interactive clarify checkpoint." + - name: "speckit.pipeline.preview" + file: "commands/preview.md" + description: "Print the resolved phase plan for the given --skip/--add flags without running anything (dry run)." + + config: + - name: "pipeline-config.yml" + template: "config-template.yml" + description: "Pipeline defaults — auto-resolve cap for analyze findings, default skip/add sets." + required: false + +tags: + - "workflow" + - "orchestration" + - "pipeline" + - "automation" + - "sdd" + - "spec-driven-development" diff --git a/extensions/pipeline/scripts/bash/resolve-phases.sh b/extensions/pipeline/scripts/bash/resolve-phases.sh new file mode 100755 index 0000000000..d6f8dd34c9 --- /dev/null +++ b/extensions/pipeline/scripts/bash/resolve-phases.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# Thin POSIX wrapper: resolve the Spec Kit pipeline phase plan. +# Delegates to the pure-Python resolver so there is one source of truth. +# Usage: resolve-phases.sh [--skip a,b] [--add x,y] [--json|--list] +set -euo pipefail +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec python3 "${HERE}/../resolve_phases.py" "$@" diff --git a/extensions/pipeline/scripts/phase_registry.py b/extensions/pipeline/scripts/phase_registry.py new file mode 100755 index 0000000000..14a7f07686 --- /dev/null +++ b/extensions/pipeline/scripts/phase_registry.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 +"""Phase registry and resolver for the Spec Kit `pipeline` extension. + +Pure function, zero I/O. Encodes every orderable Spec Kit phase with its +dependencies and the `/speckit.*` command it maps to, and resolves a +requested skip/add set into a single deterministic ordered plan. + +This is the reusable core of the extension: the command prose calls +`resolve_phases.py` (a thin CLI over `resolve()` below) to decide which +phases run and in what order, then drives each `/speckit.*` command in turn. + +No agent/model/vendor coupling lives here — the registry is portable to any +Spec Kit project and any AI coding agent. +""" + +from collections import namedtuple + +# Phase registry entry. `command` is the slash command the phase drives +# (agent-neutral: Spec Kit renders it as `/speckit.plan`, or the skills-mode +# equivalent `speckit-plan`, per the active integration). +Phase = namedtuple( + "Phase", + ["order", "default", "insertable", "required", "interactive", "deps", "command", "description"], +) + +# Complete phase registry: id -> Phase. +REGISTRY = { + "constitution": Phase( + order=5, + default=False, + insertable=True, + required=False, + interactive=False, + deps=[], + command="speckit.constitution", + description="Establish or update the project constitution before specifying.", + ), + "specify": Phase( + order=10, + default=True, + insertable=False, + required=True, + interactive=False, + deps=[], + command="speckit.specify", + description="Turn the feature description into a specification.", + ), + "clarify": Phase( + order=20, + default=True, + insertable=False, + required=False, + interactive=True, + deps=["specify"], + command="speckit.clarify", + description="Resolve underspecified areas. The single interactive human gate.", + ), + "plan": Phase( + order=30, + default=True, + insertable=False, + required=False, + interactive=False, + deps=["specify"], + command="speckit.plan", + description="Produce the implementation plan and design artifacts.", + ), + "tasks": Phase( + order=40, + default=True, + insertable=False, + required=False, + interactive=False, + deps=["plan"], + command="speckit.tasks", + description="Generate the dependency-ordered task list.", + ), + "checklist": Phase( + order=45, + default=False, + insertable=True, + required=False, + interactive=False, + deps=["tasks"], + command="speckit.checklist", + description="Generate a quality checklist for the feature.", + ), + "analyze": Phase( + order=60, + default=True, + insertable=False, + required=False, + interactive=False, + deps=["specify", "plan", "tasks"], + command="speckit.analyze", + description="Cross-artifact consistency and quality analysis before implementing.", + ), + "implement": Phase( + order=70, + default=True, + insertable=False, + required=True, + interactive=False, + deps=["plan", "tasks"], + command="speckit.implement", + description="Execute the plan and produce the change.", + ), +} + +# Orderable partitions. +DEFAULTS = {"specify", "clarify", "plan", "tasks", "analyze", "implement"} +INSERTABLE = {"constitution", "checklist"} +REQUIRED = {"specify", "implement"} + + +class UnknownPhase(Exception): + """Exit code 10: unrecognized phase name(s).""" + + code = 10 + + def __init__(self, names, valid): + self.names = names + self.valid = valid + super().__init__(f"Unknown phase name(s): {', '.join(names)}. Valid: {', '.join(valid)}") + + +class SkipAddConflict(Exception): + """Exit code 11: phase appears in both --skip and --add.""" + + code = 11 + + def __init__(self, names): + self.names = names + super().__init__(f"Phase(s) appear in both --skip and --add: {', '.join(names)}") + + +class NotInsertable(Exception): + """Exit code 12: attempted to add a non-insertable phase.""" + + code = 12 + + def __init__(self, names, insertable_set): + self.names = names + self.insertable_set = insertable_set + super().__init__( + f"Cannot add non-insertable phase(s): {', '.join(names)}. " + f"Insertable phases: {', '.join(sorted(insertable_set))}" + ) + + +class RequiredPhaseSkip(Exception): + """Exit code 13: attempted to skip a required phase.""" + + code = 13 + + def __init__(self, names): + self.names = names + super().__init__(f"Cannot skip required phase(s): {', '.join(names)}") + + +class DepBreak(Exception): + """Exit code 14: unmet dependencies in the effective set.""" + + code = 14 + + def __init__(self, violations): + self.violations = violations # list of (phase, missing_dep) tuples + super().__init__( + "Dependency broken. Phase(s) missing required input:\n" + + "\n".join(f" {phase} requires {dep}" for phase, dep in violations) + ) + + +def resolve(skip, add): + """Deterministic phase resolver. + + Args: + skip: set of phase names to exclude from the default set. + add: set of insertable phase names to include. + + Returns: + list of phase names in canonical order. + + Raises: + UnknownPhase (10), SkipAddConflict (11), NotInsertable (12), + RequiredPhaseSkip (13), DepBreak (14) — checked in that order. + + Ordering is a pure function of the effective set (skip/add consumed as + sets, order derived from the single fixed `order` key), so the output is + invariant to the input ordering of --skip / --add. + """ + orderable = set(DEFAULTS) | set(INSERTABLE) + + unknown = (skip | add) - orderable + if unknown: + raise UnknownPhase(sorted(unknown), valid=sorted(orderable)) + + conflict = skip & add + if conflict: + raise SkipAddConflict(sorted(conflict)) + + not_insertable = add - INSERTABLE + if not_insertable: + raise NotInsertable(sorted(not_insertable), INSERTABLE) + + required_hit = skip & REQUIRED + if required_hit: + raise RequiredPhaseSkip(sorted(required_hit)) + + effective = (DEFAULTS - skip) | add + + violations = [ + (phase, dep) + for phase in sorted(effective, key=lambda p: REGISTRY[p].order) + for dep in REGISTRY[phase].deps + if dep not in effective + ] + if violations: + raise DepBreak(violations) + + return sorted(effective, key=lambda p: REGISTRY[p].order) diff --git a/extensions/pipeline/scripts/powershell/resolve-phases.ps1 b/extensions/pipeline/scripts/powershell/resolve-phases.ps1 new file mode 100644 index 0000000000..2b9deea1aa --- /dev/null +++ b/extensions/pipeline/scripts/powershell/resolve-phases.ps1 @@ -0,0 +1,8 @@ +# Thin PowerShell wrapper: resolve the Spec Kit pipeline phase plan. +# Delegates to the pure-Python resolver so there is one source of truth. +# Usage: resolve-phases.ps1 [--skip a,b] [--add x,y] [--json|--list] +$ErrorActionPreference = "Stop" +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$resolver = Join-Path $here "..\resolve_phases.py" +python3 $resolver @args +exit $LASTEXITCODE diff --git a/extensions/pipeline/scripts/resolve_phases.py b/extensions/pipeline/scripts/resolve_phases.py new file mode 100755 index 0000000000..36039090d8 --- /dev/null +++ b/extensions/pipeline/scripts/resolve_phases.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""CLI wrapper over the phase resolver for the Spec Kit `pipeline` extension. + +Usage: + resolve_phases.py [--skip a,b] [--add x,y] [--json] + resolve_phases.py --list + +Exit codes: + 0 success (ordered phase plan on stdout) + 2 usage error + 10 unknown phase name + 11 skip/add conflict + 12 add not insertable + 13 skip required phase + 14 dependency break +""" + +import argparse +import json +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from phase_registry import ( # noqa: E402 + REGISTRY, + DEFAULTS, + INSERTABLE, + DepBreak, + NotInsertable, + RequiredPhaseSkip, + SkipAddConflict, + UnknownPhase, + resolve, +) + + +def parse_csv(csv_str): + if not csv_str: + return set() + return {item.strip() for item in csv_str.split(",") if item.strip()} + + +def _row(phase_id): + p = REGISTRY[phase_id] + gate = "gate" if p.interactive else "-" + return f"{p.order}\t{phase_id}\t{p.command}\t{gate}\t{p.description}" + + +def main(): + parser = argparse.ArgumentParser( + prog="resolve_phases.py", + description="Deterministic phase resolver for the Spec Kit pipeline extension.", + ) + parser.add_argument("--skip", type=str, default="", help="CSV list of phases to skip") + parser.add_argument("--add", type=str, default="", help="CSV list of insertable phases to add") + parser.add_argument("--json", action="store_true", help="Output as JSON") + parser.add_argument("--list", action="store_true", help="List all orderable phases and exit") + + try: + args = parser.parse_args() + except SystemExit as e: + if e.code != 0: + sys.exit(2) + raise + + if args.list: + orderable = sorted( + (set(DEFAULTS) | set(INSERTABLE)), key=lambda p: REGISTRY[p].order + ) + for phase_id in orderable: + print(_row(phase_id)) + sys.exit(0) + + skip_set = parse_csv(args.skip) + add_set = parse_csv(args.add) + + try: + effective = resolve(skip_set, add_set) + except (UnknownPhase, SkipAddConflict, NotInsertable, RequiredPhaseSkip, DepBreak) as e: + print(str(e), file=sys.stderr) + sys.exit(e.code) + + if args.json: + output = [ + { + "phase": phase_id, + "order": REGISTRY[phase_id].order, + "command": REGISTRY[phase_id].command, + "interactive": REGISTRY[phase_id].interactive, + "description": REGISTRY[phase_id].description, + } + for phase_id in effective + ] + print(json.dumps(output)) + else: + for phase_id in effective: + print(_row(phase_id)) + + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/extensions/pipeline/tests/test_phase_registry.py b/extensions/pipeline/tests/test_phase_registry.py new file mode 100644 index 0000000000..e58d1824aa --- /dev/null +++ b/extensions/pipeline/tests/test_phase_registry.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +"""Unit tests for the pipeline phase resolver. Stdlib unittest only; no pytest. + +Run from the extension root: + python3 -m unittest discover -s tests -p 'test_*.py' +""" + +import json +import subprocess +import sys +import unittest +from pathlib import Path + +SCRIPTS_DIR = Path(__file__).parent.parent / "scripts" +sys.path.insert(0, str(SCRIPTS_DIR)) + +from phase_registry import ( # noqa: E402 + REGISTRY, + DEFAULTS, + INSERTABLE, + DepBreak, + NotInsertable, + RequiredPhaseSkip, + SkipAddConflict, + UnknownPhase, + resolve, +) + + +class TestPhaseRegistry(unittest.TestCase): + def test_default_order(self): + self.assertEqual( + resolve(set(), set()), + ["specify", "clarify", "plan", "tasks", "analyze", "implement"], + ) + + def test_skip_two_defaults_preserves_relative_order(self): + self.assertEqual( + resolve({"clarify", "analyze"}, set()), + ["specify", "plan", "tasks", "implement"], + ) + + def test_add_out_of_order_lands_canonical(self): + expected = ["constitution", "specify", "clarify", "plan", "tasks", "checklist", "analyze", "implement"] + self.assertEqual(resolve(set(), {"constitution", "checklist"}), expected) + self.assertEqual(resolve(set(), {"checklist", "constitution"}), expected) + + def test_permutation_invariance(self): + outputs = [] + for order in ("clarify,analyze", "analyze,clarify"): + cmd = [sys.executable, str(SCRIPTS_DIR / "resolve_phases.py"), "--skip", order, "--json"] + r = subprocess.run(cmd, capture_output=True, text=True, cwd=str(SCRIPTS_DIR)) + self.assertEqual(r.returncode, 0, r.stderr) + outputs.append(r.stdout) + self.assertEqual(len(set(outputs)), 1) + + def test_unknown_name(self): + with self.assertRaises(UnknownPhase) as cm: + resolve(set(), {"nonexistent"}) + self.assertIn("nonexistent", cm.exception.names) + self.assertEqual(set(cm.exception.valid), set(DEFAULTS) | set(INSERTABLE)) + + def test_skip_add_conflict_first(self): + with self.assertRaises(SkipAddConflict) as cm: + resolve({"clarify"}, {"clarify"}) + self.assertIn("clarify", cm.exception.names) + + def test_add_non_insertable_rejected(self): + with self.assertRaises(NotInsertable) as cm: + resolve(set(), {"plan"}) + self.assertIn("plan", cm.exception.names) + + def test_skip_required_specify(self): + with self.assertRaises(RequiredPhaseSkip): + resolve({"specify"}, set()) + + def test_skip_required_implement(self): + with self.assertRaises(RequiredPhaseSkip): + resolve({"implement"}, set()) + + def test_skip_plan_collects_dep_breaks(self): + with self.assertRaises(DepBreak) as cm: + resolve({"plan"}, set()) + broken = {phase for phase, _ in cm.exception.violations} + self.assertIn("tasks", broken) + self.assertIn("analyze", broken) + self.assertIn("implement", broken) + ordered = [phase for phase, _ in cm.exception.violations] + self.assertEqual(ordered, sorted(ordered, key=lambda p: REGISTRY[p].order)) + + def test_skip_non_default_is_noop(self): + self.assertEqual(resolve({"checklist"}, set()), resolve(set(), set())) + + def test_add_checklist_after_tasks(self): + result = resolve(set(), {"checklist"}) + self.assertLess(result.index("tasks"), result.index("checklist")) + self.assertLess(result.index("checklist"), result.index("analyze")) + + def test_cli_exit_codes(self): + cases = [ + (["--skip", "nonexistent"], 10), + (["--skip", "clarify", "--add", "clarify"], 11), + (["--add", "plan"], 12), + (["--skip", "specify"], 13), + (["--skip", "plan"], 14), + ([], 0), + ] + for args, code in cases: + cmd = [sys.executable, str(SCRIPTS_DIR / "resolve_phases.py")] + args + r = subprocess.run(cmd, capture_output=True, text=True, cwd=str(SCRIPTS_DIR)) + self.assertEqual(r.returncode, code, f"{args}: {r.stderr}") + + def test_json_shape(self): + cmd = [sys.executable, str(SCRIPTS_DIR / "resolve_phases.py"), "--json"] + r = subprocess.run(cmd, capture_output=True, text=True, cwd=str(SCRIPTS_DIR)) + self.assertEqual(r.returncode, 0) + out = json.loads(r.stdout) + self.assertIsInstance(out, list) + for item in out: + for key in ("phase", "order", "command", "interactive", "description"): + self.assertIn(key, item) + orders = [item["order"] for item in out] + self.assertEqual(orders, sorted(orders)) + + def test_list_short_circuits(self): + base = subprocess.run( + [sys.executable, str(SCRIPTS_DIR / "resolve_phases.py"), "--list"], + capture_output=True, text=True, cwd=str(SCRIPTS_DIR), + ) + self.assertEqual(base.returncode, 0) + base_lines = base.stdout.strip().split("\n") + self.assertEqual(len(base_lines), len(set(DEFAULTS) | set(INSERTABLE))) + withargs = subprocess.run( + [sys.executable, str(SCRIPTS_DIR / "resolve_phases.py"), "--list", "--skip", "clarify", "--add", "constitution"], + capture_output=True, text=True, cwd=str(SCRIPTS_DIR), + ) + self.assertEqual(base_lines, withargs.stdout.strip().split("\n")) + + +if __name__ == "__main__": + unittest.main(verbosity=2) From ed6fbc01e8038c73c60d743274bdff0b810e17a5 Mon Sep 17 00:00:00 2001 From: domattioli Date: Thu, 9 Jul 2026 21:07:23 -0400 Subject: [PATCH 2/3] refactor(pipeline): replace extension with converge-based workflow Drop the extensions/pipeline Python resolver, command wrappers, and test suite in favor of a single workflows/pipeline/workflow.yml, per review feedback that the workflows engine already owns phase orchestration, ordering, validation, and resume. Rework the loop around the new speckit.converge command: analyze runs as a single pre-implement consistency pass (its findings are artifact-level, not code-level), and a bounded post-implement convergence loop (implement -> converge, up to 3 cycles) closes any code/spec gaps. Co-Authored-By: Claude Opus 4.8 --- extensions/pipeline/.gitignore | 4 - extensions/pipeline/CHANGELOG.md | 21 -- extensions/pipeline/LICENSE | 21 -- extensions/pipeline/README.md | 98 -------- extensions/pipeline/commands/preview.md | 29 --- extensions/pipeline/commands/run.md | 87 ------- extensions/pipeline/config-template.yml | 21 -- extensions/pipeline/extension.yml | 37 --- .../pipeline/scripts/bash/resolve-phases.sh | 7 - extensions/pipeline/scripts/phase_registry.py | 221 ------------------ .../scripts/powershell/resolve-phases.ps1 | 8 - extensions/pipeline/scripts/resolve_phases.py | 104 --------- .../pipeline/tests/test_phase_registry.py | 141 ----------- workflows/pipeline/CHANGELOG.md | 16 ++ workflows/pipeline/README.md | 124 ++++++++++ workflows/pipeline/workflow.yml | 125 ++++++++++ 16 files changed, 265 insertions(+), 799 deletions(-) delete mode 100644 extensions/pipeline/.gitignore delete mode 100644 extensions/pipeline/CHANGELOG.md delete mode 100644 extensions/pipeline/LICENSE delete mode 100644 extensions/pipeline/README.md delete mode 100644 extensions/pipeline/commands/preview.md delete mode 100644 extensions/pipeline/commands/run.md delete mode 100644 extensions/pipeline/config-template.yml delete mode 100644 extensions/pipeline/extension.yml delete mode 100755 extensions/pipeline/scripts/bash/resolve-phases.sh delete mode 100755 extensions/pipeline/scripts/phase_registry.py delete mode 100644 extensions/pipeline/scripts/powershell/resolve-phases.ps1 delete mode 100755 extensions/pipeline/scripts/resolve_phases.py delete mode 100644 extensions/pipeline/tests/test_phase_registry.py create mode 100644 workflows/pipeline/CHANGELOG.md create mode 100644 workflows/pipeline/README.md create mode 100644 workflows/pipeline/workflow.yml diff --git a/extensions/pipeline/.gitignore b/extensions/pipeline/.gitignore deleted file mode 100644 index 10b940d843..0000000000 --- a/extensions/pipeline/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -__pycache__/ -*.pyc -.pytest_cache/ -.DS_Store diff --git a/extensions/pipeline/CHANGELOG.md b/extensions/pipeline/CHANGELOG.md deleted file mode 100644 index d88a6a96fa..0000000000 --- a/extensions/pipeline/CHANGELOG.md +++ /dev/null @@ -1,21 +0,0 @@ -# Changelog - -All notable changes to the Pipeline extension are documented here. Format based on -[Keep a Changelog](https://keepachangelog.com/); this project adheres to Semantic Versioning. - -## [1.0.0] - 2026-07-05 - -### Added - -- Initial release. -- `speckit.pipeline.run` — chains `specify → clarify → plan → tasks → analyze → implement` - into one guided invocation with a single interactive clarify gate and an - analyze → fix → re-analyze loop (≤3 cycles) before implement. -- `speckit.pipeline.preview` — dry-run printer for the resolved phase plan. -- `--skip` / `--add` flags with a deterministic phase resolver (`scripts/resolve_phases.py`), - strict validation, and dedicated exit codes (10–14). -- Insertable phases: `constitution` (before specify), `checklist` (after tasks). -- `--yes` unattended mode: answers the clarify gate in-place, halting before `plan` - on a question too consequential to answer without a human. -- Bash + PowerShell wrappers over the Python resolver. -- Stdlib `unittest` suite covering default order, permutation invariance, and every exit code. diff --git a/extensions/pipeline/LICENSE b/extensions/pipeline/LICENSE deleted file mode 100644 index 1f2f1cb34b..0000000000 --- a/extensions/pipeline/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2026 Dominik Mattioli - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/extensions/pipeline/README.md b/extensions/pipeline/README.md deleted file mode 100644 index 14e99d89ab..0000000000 --- a/extensions/pipeline/README.md +++ /dev/null @@ -1,98 +0,0 @@ -# Pipeline — a Spec Kit extension - -Chain the [Spec Kit](https://github.com/github/spec-kit) phases into **one guided, single-invocation pipeline** instead of hand-running seven commands. - -``` -specify → clarify → plan → tasks → analyze → implement -``` - -`/speckit.pipeline.run` drives each phase in order, pausing exactly once — at an interactive **clarify** gate — then advances unattended through the rest, re-running `analyze` until findings are resolved (up to 3 cycles) before it implements. The phase order is produced by a small, deterministic resolver, so a tailored pipeline (`--skip`, `--add`) always lands in the same canonical order. - -## Why - -Taking a feature from description to implemented change means issuing `/speckit.specify`, `/speckit.clarify`, `/speckit.plan`, `/speckit.tasks`, `/speckit.analyze`, `/speckit.implement` by hand — and hand-resolving each analyze finding in between. This extension collapses that to a single invocation with one human checkpoint, while keeping every phase's own command as the source of truth (it *drives* the stock commands, it does not reimplement them). - -## Commands - -| Command | What it does | -|---|---| -| `speckit.pipeline.run` | Run the full pipeline from one feature description, with a single clarify checkpoint. | -| `speckit.pipeline.preview` | Print the resolved phase plan for the given flags without running anything (dry run). | - -### Flags - -- `--skip ` — drop default phases (e.g. `--skip clarify,analyze`). `specify` and `implement` cannot be skipped. -- `--add ` — insert optional phases: `constitution` (before specify), `checklist` (after tasks). -- `--yes` — unattended run. Answers the clarify gate itself (grounded in the spec/repo) rather than pausing for a human, and **halts before `plan`** if a question is too consequential to answer unattended. `--yes` never means "no clarification" — use `--skip clarify` for that. - -## Examples - -``` -# Full default run -/speckit.pipeline.run Add rate limiting to the public API - -# See the plan first, then run a tailored pipeline -/speckit.pipeline.preview --add checklist --skip clarify -/speckit.pipeline.run --add checklist Add a healthcheck endpoint - -# Unattended (CI / routine), clarify answered in-place, halts on a consequential question -/speckit.pipeline.run --yes Migrate config loading to env vars -``` - -## How the resolver works - -`scripts/resolve_phases.py` (pure Python, stdlib only) is the deterministic core. It takes the requested skip/add sets, validates them, and returns the phase list in a fixed canonical order. Ordering is a pure function of the *effective set*, so flag ordering never changes the result. Validation is strict, with dedicated exit codes: - -| Exit | Meaning | -|---|---| -| 0 | resolved OK | -| 10 | unknown phase name | -| 11 | a phase is in both `--skip` and `--add` | -| 12 | `--add` names a non-insertable phase | -| 13 | `--skip` targets a required phase (`specify`/`implement`) | -| 14 | dependency break — a retained phase's dependency was skipped | - -`run` and `preview` both call the resolver, so a bad flag combination is caught before any phase runs. `scripts/bash/resolve-phases.sh` and `scripts/powershell/resolve-phases.ps1` are thin wrappers over the same Python, matching Spec Kit's `scripts.sh` / `scripts.ps` command convention. - -## Agent-neutral - -The extension ships no dependency on any specific AI agent, model, or vendor tooling — it drives only stock `/speckit.*` commands (or their skills-mode equivalents `speckit-plan`, `speckit-tasks`, …). It works in any Spec Kit-initialized project regardless of which of the 30+ supported agents you use. - -## Layout - -``` -pipeline/ -├── extension.yml # manifest -├── commands/ -│ ├── run.md # the orchestrator command -│ └── preview.md # dry-run phase-plan printer -├── scripts/ -│ ├── phase_registry.py # deterministic resolver core (pure, no I/O) -│ ├── resolve_phases.py # CLI over the resolver (exit codes 10–14) -│ ├── bash/resolve-phases.sh -│ └── powershell/resolve-phases.ps1 -├── config-template.yml # optional per-project defaults -├── tests/test_phase_registry.py # stdlib unittest — determinism + exit codes -├── README.md -├── CHANGELOG.md -└── LICENSE -``` - -## Install - -Until it is listed in the community catalog, install by copying `pipeline/` into your project's Spec Kit extensions location (or your fork's `extensions/pipeline/`) so the two commands register on init. See the catalog submission notes in the parent directory's contribution guide. - -## Tests - -``` -cd pipeline -python3 -m unittest discover -s tests -p 'test_*.py' -``` - -## License - -MIT — see [LICENSE](LICENSE). - -## Provenance - -Ported from the `speckit-pipeline` orchestration skill (formerly `speckit-workflow`), decoupled from its origin repo's internal tooling into a portable, agent-neutral Spec Kit extension. diff --git a/extensions/pipeline/commands/preview.md b/extensions/pipeline/commands/preview.md deleted file mode 100644 index 4b303867de..0000000000 --- a/extensions/pipeline/commands/preview.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -description: "Print the resolved Spec Kit pipeline phase plan for the given --skip/--add flags without running anything — a dry run of the deterministic phase resolver." -scripts: - sh: scripts/bash/resolve-phases.sh - ps: scripts/powershell/resolve-phases.ps1 ---- - -# Pipeline: preview - -Show the phase plan `/speckit.pipeline.run` *would* execute for a given set of flags, without running any phase. Use it to confirm a tailored pipeline (skips/adds) resolves the way you expect before committing to a full run. - -## Input - -`$ARGUMENTS` — optional flags only (no feature description needed): - -- `--skip ` — default phases to drop. -- `--add ` — insertable phases to add (`constitution`, `checklist`). - -## Behavior - -Run the resolver and print its output: - -``` -{SCRIPT} --skip "" --add "" -``` - -Each line is `order phase command gate description`, in the exact order `run` would execute. `--list` (no flags) dumps the full registry of orderable phases. - -Report the resolver's exit code and, on any non-zero code (`10`–`14`), its stderr message verbatim — the same validation `run` performs, so a bad flag combination is caught here first. This command never edits files and never invokes a `/speckit.*` phase. diff --git a/extensions/pipeline/commands/run.md b/extensions/pipeline/commands/run.md deleted file mode 100644 index 424551ed1a..0000000000 --- a/extensions/pipeline/commands/run.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -description: "Run the full Spec Kit pipeline (specify → clarify → plan → tasks → analyze → implement) from one feature description, with a single interactive clarify checkpoint and a deterministic, tailorable phase order." -scripts: - sh: scripts/bash/resolve-phases.sh - ps: scripts/powershell/resolve-phases.ps1 ---- - -# Pipeline: run - -Carry one feature from a plain-language description to an implemented change by chaining the existing Spec Kit phase commands into a single guided run. You (the agent) **are** the orchestrator: you follow this procedure turn by turn, invoking each `/speckit.*` command in order and verifying its artifact landed before moving on. This replaces hand-running `/speckit.specify`, `/speckit.clarify`, `/speckit.plan`, `/speckit.tasks`, `/speckit.analyze`, and `/speckit.implement` one at a time and hand-resolving analyze findings. - -## Input - -`$ARGUMENTS` — the feature description, optionally followed by flags: - -- `--skip ` — drop default phases (e.g. `--skip clarify,analyze`). Cannot drop `specify` or `implement`. -- `--add ` — insert optional phases: `constitution` (before specify), `checklist` (after tasks). -- `--yes` — unattended run. Does **not** skip clarification; instead you answer the clarify questions yourself, grounded in the spec and repo conventions, and halt before `plan` if a question is too consequential to answer without a human (see Step 4). Use `--skip clarify` if you genuinely want zero clarification. - -If the feature description is empty, report the missing input and stop — do not start. - -## Step 1 — Resolve the phase plan (deterministic) - -Run the resolver once and branch on its exit code: - -``` -{SCRIPT} --skip "" --add "" --json -``` - -(`{SCRIPT}` is this command's configured `scripts.sh` / `scripts.ps`.) - -Exit codes: `0` OK · `10` unknown phase name · `11` skip/add name conflict · `12` add-name not insertable · `13` skip targets a required phase · `14` dependency break. On any non-zero code, report the resolver's stderr message verbatim and stop — do not run a partial or incoherent pipeline. The order is deterministic: the resolver consumes skip/add as sets and derives order from one fixed key, so flag ordering never changes the plan. - -## Step 2 — Preflight - -Confirm the project is Spec Kit-initialized (a `.specify/` directory exists) and that every `/speckit.*` command named in the resolved plan is available in this agent's command set. Report anything missing now — never mid-pipeline. If `--add constitution` or `--add checklist` was requested but that command isn't installed, report and stop. - -## Step 3 — Execute each phase in order - -For each phase in the resolved plan, invoke its `/speckit.*` command (the `command` field from the resolver output). After each phase: - -- **Verify the artifact.** Confirm the expected file was written (`spec.md`, `plan.md`, `tasks.md`, etc.) before proceeding. A phase reporting success is not enough — check the artifact is on disk. -- **Halt on failure.** If a phase fails and the failure is not something you can safely resolve, stop and name the phase and reason. Never proceed past a broken artifact. - -Keep a short running ledger (which phases ran, which were skipped, outcome of each) so the run survives a context reset. - -## Step 4 — The clarify gate - -`clarify` is the single interactive checkpoint; everything after it runs unattended. - -**Default (no `--yes`)** — run `/speckit.clarify`, present its questions to the human, and **end your turn to wait** for answers. This is the one human checkpoint. - -**`--yes` set** — no human is present, so `--yes` does not mean "skip clarification", it means "answer it responsibly yourself": - -1. Run `/speckit.clarify`'s question generation as normal, but don't present the questions to a human. -2. Answer each question as the operator plausibly would, grounded strictly in the spec's own content, the project's constitution/conventions, and established repo patterns. Integrate each answer into the spec exactly as `/speckit.clarify` integrates a human's answer. -3. Emit one line noting that clarify was answered unattended, not by a human, so the audit trail is honest. -4. **If any question is too consequential to answer without a human** — insufficient grounding, security/scope/privacy stakes, or a materially shape-changing decision the spec leaves open — do **not** guess. Halt before `plan`, name the unresolved question and why. This is a correct outcome, not a failure: it means the run recognized it should not proceed unattended past this point. - -## Step 5 — The analyze → resolve loop - -After `/speckit.analyze` reports findings, fix each finding (edit the spec/plan/tasks as needed), then re-run `/speckit.analyze`. Repeat **up to 3 cycles**. If findings remain unresolved after the third cycle, halt and report them rather than implementing against a known-inconsistent spec. - -## Step 6 — Unattended discipline (clarify-satisfied → implement) - -From the moment clarify is satisfied through `implement`, run without acknowledgment chatter between phases, but: - -- **Log every skip and every fallback.** Silent deviation from the plan is not allowed — if you skip or work around something, say so in the ledger. -- **Halt on destructive or irreversible actions** during `implement` (deleting data, force-pushing, rewriting shared history) and ask, rather than proceeding blindly. -- Any phase failure that isn't auto-resolvable, or is unsafe to continue past, halts **before** `implement`, naming the phase and reason. - -## Hard stops - -| Condition | Behavior | -|---|---| -| Empty feature description | Report missing input; do not start. | -| Resolver returns non-zero | Report its message; run no phase. | -| `.specify/` or a required `/speckit.*` command missing at preflight | Report; do not start. | -| Expected artifact absent after a phase | Halt, name the phase; do not proceed. | -| Analyze findings unresolved after 3 cycles | Halt; report the unresolved findings. | -| A clarify question too consequential to answer unattended (`--yes`) | Halt before `plan`, name the question and why; never guess. | -| Destructive/irreversible action during implement | Halt and ask. | - -## Notes - -- **Agent-neutral.** This command drives only stock `/speckit.*` commands and ships no dependency on any specific AI agent, model, or vendor tooling. In an agent that renders Spec Kit commands as skills, the same phases appear as `speckit-plan`, `speckit-tasks`, etc. — invoke whichever form your agent exposes. -- **Preview first.** Run `/speckit.pipeline.preview` with the same flags to see the resolved plan before committing to a full run. diff --git a/extensions/pipeline/config-template.yml b/extensions/pipeline/config-template.yml deleted file mode 100644 index 2139c703ca..0000000000 --- a/extensions/pipeline/config-template.yml +++ /dev/null @@ -1,21 +0,0 @@ -# Pipeline extension configuration. -# Copy to `pipeline-config.yml` in your project's Spec Kit config location and edit. -# Every key is optional; the values below are the built-in defaults. - -pipeline: - # Default phases to skip on every `/speckit.pipeline.run` unless overridden by --skip. - # Cannot include the required phases `specify` or `implement`. - default_skip: [] - - # Default insertable phases to add on every run unless overridden by --add. - # Valid values: constitution, checklist. - default_add: [] - - # Maximum analyze → fix → re-analyze cycles before the run halts and reports - # remaining findings instead of implementing against an inconsistent spec. - analyze_max_cycles: 3 - - # When true, `--yes` runs answer the clarify gate unattended (grounded in the - # spec/repo) instead of pausing for a human. A question too consequential to - # answer without a human still halts the run before `plan`. - answer_clarify_unattended: false diff --git a/extensions/pipeline/extension.yml b/extensions/pipeline/extension.yml deleted file mode 100644 index fea7dcdc33..0000000000 --- a/extensions/pipeline/extension.yml +++ /dev/null @@ -1,37 +0,0 @@ -schema_version: "1.0" - -extension: - id: "pipeline" - name: "Pipeline" - version: "1.0.0" - description: "Chain the Spec Kit phases (specify → clarify → plan → tasks → analyze → implement) into one guided, single-invocation pipeline with a deterministic phase resolver and one interactive clarify gate." - author: "Dominik Mattioli" - repository: "https://github.com/domattioli/spec-kit-pipeline" - license: "MIT" - homepage: "https://github.com/domattioli/spec-kit-pipeline" - -requires: - speckit_version: ">=0.2.0" - -provides: - commands: - - name: "speckit.pipeline.run" - file: "commands/run.md" - description: "Run the full spec → implement pipeline from one feature description, with a single interactive clarify checkpoint." - - name: "speckit.pipeline.preview" - file: "commands/preview.md" - description: "Print the resolved phase plan for the given --skip/--add flags without running anything (dry run)." - - config: - - name: "pipeline-config.yml" - template: "config-template.yml" - description: "Pipeline defaults — auto-resolve cap for analyze findings, default skip/add sets." - required: false - -tags: - - "workflow" - - "orchestration" - - "pipeline" - - "automation" - - "sdd" - - "spec-driven-development" diff --git a/extensions/pipeline/scripts/bash/resolve-phases.sh b/extensions/pipeline/scripts/bash/resolve-phases.sh deleted file mode 100755 index d6f8dd34c9..0000000000 --- a/extensions/pipeline/scripts/bash/resolve-phases.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash -# Thin POSIX wrapper: resolve the Spec Kit pipeline phase plan. -# Delegates to the pure-Python resolver so there is one source of truth. -# Usage: resolve-phases.sh [--skip a,b] [--add x,y] [--json|--list] -set -euo pipefail -HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -exec python3 "${HERE}/../resolve_phases.py" "$@" diff --git a/extensions/pipeline/scripts/phase_registry.py b/extensions/pipeline/scripts/phase_registry.py deleted file mode 100755 index 14a7f07686..0000000000 --- a/extensions/pipeline/scripts/phase_registry.py +++ /dev/null @@ -1,221 +0,0 @@ -#!/usr/bin/env python3 -"""Phase registry and resolver for the Spec Kit `pipeline` extension. - -Pure function, zero I/O. Encodes every orderable Spec Kit phase with its -dependencies and the `/speckit.*` command it maps to, and resolves a -requested skip/add set into a single deterministic ordered plan. - -This is the reusable core of the extension: the command prose calls -`resolve_phases.py` (a thin CLI over `resolve()` below) to decide which -phases run and in what order, then drives each `/speckit.*` command in turn. - -No agent/model/vendor coupling lives here — the registry is portable to any -Spec Kit project and any AI coding agent. -""" - -from collections import namedtuple - -# Phase registry entry. `command` is the slash command the phase drives -# (agent-neutral: Spec Kit renders it as `/speckit.plan`, or the skills-mode -# equivalent `speckit-plan`, per the active integration). -Phase = namedtuple( - "Phase", - ["order", "default", "insertable", "required", "interactive", "deps", "command", "description"], -) - -# Complete phase registry: id -> Phase. -REGISTRY = { - "constitution": Phase( - order=5, - default=False, - insertable=True, - required=False, - interactive=False, - deps=[], - command="speckit.constitution", - description="Establish or update the project constitution before specifying.", - ), - "specify": Phase( - order=10, - default=True, - insertable=False, - required=True, - interactive=False, - deps=[], - command="speckit.specify", - description="Turn the feature description into a specification.", - ), - "clarify": Phase( - order=20, - default=True, - insertable=False, - required=False, - interactive=True, - deps=["specify"], - command="speckit.clarify", - description="Resolve underspecified areas. The single interactive human gate.", - ), - "plan": Phase( - order=30, - default=True, - insertable=False, - required=False, - interactive=False, - deps=["specify"], - command="speckit.plan", - description="Produce the implementation plan and design artifacts.", - ), - "tasks": Phase( - order=40, - default=True, - insertable=False, - required=False, - interactive=False, - deps=["plan"], - command="speckit.tasks", - description="Generate the dependency-ordered task list.", - ), - "checklist": Phase( - order=45, - default=False, - insertable=True, - required=False, - interactive=False, - deps=["tasks"], - command="speckit.checklist", - description="Generate a quality checklist for the feature.", - ), - "analyze": Phase( - order=60, - default=True, - insertable=False, - required=False, - interactive=False, - deps=["specify", "plan", "tasks"], - command="speckit.analyze", - description="Cross-artifact consistency and quality analysis before implementing.", - ), - "implement": Phase( - order=70, - default=True, - insertable=False, - required=True, - interactive=False, - deps=["plan", "tasks"], - command="speckit.implement", - description="Execute the plan and produce the change.", - ), -} - -# Orderable partitions. -DEFAULTS = {"specify", "clarify", "plan", "tasks", "analyze", "implement"} -INSERTABLE = {"constitution", "checklist"} -REQUIRED = {"specify", "implement"} - - -class UnknownPhase(Exception): - """Exit code 10: unrecognized phase name(s).""" - - code = 10 - - def __init__(self, names, valid): - self.names = names - self.valid = valid - super().__init__(f"Unknown phase name(s): {', '.join(names)}. Valid: {', '.join(valid)}") - - -class SkipAddConflict(Exception): - """Exit code 11: phase appears in both --skip and --add.""" - - code = 11 - - def __init__(self, names): - self.names = names - super().__init__(f"Phase(s) appear in both --skip and --add: {', '.join(names)}") - - -class NotInsertable(Exception): - """Exit code 12: attempted to add a non-insertable phase.""" - - code = 12 - - def __init__(self, names, insertable_set): - self.names = names - self.insertable_set = insertable_set - super().__init__( - f"Cannot add non-insertable phase(s): {', '.join(names)}. " - f"Insertable phases: {', '.join(sorted(insertable_set))}" - ) - - -class RequiredPhaseSkip(Exception): - """Exit code 13: attempted to skip a required phase.""" - - code = 13 - - def __init__(self, names): - self.names = names - super().__init__(f"Cannot skip required phase(s): {', '.join(names)}") - - -class DepBreak(Exception): - """Exit code 14: unmet dependencies in the effective set.""" - - code = 14 - - def __init__(self, violations): - self.violations = violations # list of (phase, missing_dep) tuples - super().__init__( - "Dependency broken. Phase(s) missing required input:\n" - + "\n".join(f" {phase} requires {dep}" for phase, dep in violations) - ) - - -def resolve(skip, add): - """Deterministic phase resolver. - - Args: - skip: set of phase names to exclude from the default set. - add: set of insertable phase names to include. - - Returns: - list of phase names in canonical order. - - Raises: - UnknownPhase (10), SkipAddConflict (11), NotInsertable (12), - RequiredPhaseSkip (13), DepBreak (14) — checked in that order. - - Ordering is a pure function of the effective set (skip/add consumed as - sets, order derived from the single fixed `order` key), so the output is - invariant to the input ordering of --skip / --add. - """ - orderable = set(DEFAULTS) | set(INSERTABLE) - - unknown = (skip | add) - orderable - if unknown: - raise UnknownPhase(sorted(unknown), valid=sorted(orderable)) - - conflict = skip & add - if conflict: - raise SkipAddConflict(sorted(conflict)) - - not_insertable = add - INSERTABLE - if not_insertable: - raise NotInsertable(sorted(not_insertable), INSERTABLE) - - required_hit = skip & REQUIRED - if required_hit: - raise RequiredPhaseSkip(sorted(required_hit)) - - effective = (DEFAULTS - skip) | add - - violations = [ - (phase, dep) - for phase in sorted(effective, key=lambda p: REGISTRY[p].order) - for dep in REGISTRY[phase].deps - if dep not in effective - ] - if violations: - raise DepBreak(violations) - - return sorted(effective, key=lambda p: REGISTRY[p].order) diff --git a/extensions/pipeline/scripts/powershell/resolve-phases.ps1 b/extensions/pipeline/scripts/powershell/resolve-phases.ps1 deleted file mode 100644 index 2b9deea1aa..0000000000 --- a/extensions/pipeline/scripts/powershell/resolve-phases.ps1 +++ /dev/null @@ -1,8 +0,0 @@ -# Thin PowerShell wrapper: resolve the Spec Kit pipeline phase plan. -# Delegates to the pure-Python resolver so there is one source of truth. -# Usage: resolve-phases.ps1 [--skip a,b] [--add x,y] [--json|--list] -$ErrorActionPreference = "Stop" -$here = Split-Path -Parent $MyInvocation.MyCommand.Path -$resolver = Join-Path $here "..\resolve_phases.py" -python3 $resolver @args -exit $LASTEXITCODE diff --git a/extensions/pipeline/scripts/resolve_phases.py b/extensions/pipeline/scripts/resolve_phases.py deleted file mode 100755 index 36039090d8..0000000000 --- a/extensions/pipeline/scripts/resolve_phases.py +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env python3 -"""CLI wrapper over the phase resolver for the Spec Kit `pipeline` extension. - -Usage: - resolve_phases.py [--skip a,b] [--add x,y] [--json] - resolve_phases.py --list - -Exit codes: - 0 success (ordered phase plan on stdout) - 2 usage error - 10 unknown phase name - 11 skip/add conflict - 12 add not insertable - 13 skip required phase - 14 dependency break -""" - -import argparse -import json -import os -import sys - -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) - -from phase_registry import ( # noqa: E402 - REGISTRY, - DEFAULTS, - INSERTABLE, - DepBreak, - NotInsertable, - RequiredPhaseSkip, - SkipAddConflict, - UnknownPhase, - resolve, -) - - -def parse_csv(csv_str): - if not csv_str: - return set() - return {item.strip() for item in csv_str.split(",") if item.strip()} - - -def _row(phase_id): - p = REGISTRY[phase_id] - gate = "gate" if p.interactive else "-" - return f"{p.order}\t{phase_id}\t{p.command}\t{gate}\t{p.description}" - - -def main(): - parser = argparse.ArgumentParser( - prog="resolve_phases.py", - description="Deterministic phase resolver for the Spec Kit pipeline extension.", - ) - parser.add_argument("--skip", type=str, default="", help="CSV list of phases to skip") - parser.add_argument("--add", type=str, default="", help="CSV list of insertable phases to add") - parser.add_argument("--json", action="store_true", help="Output as JSON") - parser.add_argument("--list", action="store_true", help="List all orderable phases and exit") - - try: - args = parser.parse_args() - except SystemExit as e: - if e.code != 0: - sys.exit(2) - raise - - if args.list: - orderable = sorted( - (set(DEFAULTS) | set(INSERTABLE)), key=lambda p: REGISTRY[p].order - ) - for phase_id in orderable: - print(_row(phase_id)) - sys.exit(0) - - skip_set = parse_csv(args.skip) - add_set = parse_csv(args.add) - - try: - effective = resolve(skip_set, add_set) - except (UnknownPhase, SkipAddConflict, NotInsertable, RequiredPhaseSkip, DepBreak) as e: - print(str(e), file=sys.stderr) - sys.exit(e.code) - - if args.json: - output = [ - { - "phase": phase_id, - "order": REGISTRY[phase_id].order, - "command": REGISTRY[phase_id].command, - "interactive": REGISTRY[phase_id].interactive, - "description": REGISTRY[phase_id].description, - } - for phase_id in effective - ] - print(json.dumps(output)) - else: - for phase_id in effective: - print(_row(phase_id)) - - sys.exit(0) - - -if __name__ == "__main__": - main() diff --git a/extensions/pipeline/tests/test_phase_registry.py b/extensions/pipeline/tests/test_phase_registry.py deleted file mode 100644 index e58d1824aa..0000000000 --- a/extensions/pipeline/tests/test_phase_registry.py +++ /dev/null @@ -1,141 +0,0 @@ -#!/usr/bin/env python3 -"""Unit tests for the pipeline phase resolver. Stdlib unittest only; no pytest. - -Run from the extension root: - python3 -m unittest discover -s tests -p 'test_*.py' -""" - -import json -import subprocess -import sys -import unittest -from pathlib import Path - -SCRIPTS_DIR = Path(__file__).parent.parent / "scripts" -sys.path.insert(0, str(SCRIPTS_DIR)) - -from phase_registry import ( # noqa: E402 - REGISTRY, - DEFAULTS, - INSERTABLE, - DepBreak, - NotInsertable, - RequiredPhaseSkip, - SkipAddConflict, - UnknownPhase, - resolve, -) - - -class TestPhaseRegistry(unittest.TestCase): - def test_default_order(self): - self.assertEqual( - resolve(set(), set()), - ["specify", "clarify", "plan", "tasks", "analyze", "implement"], - ) - - def test_skip_two_defaults_preserves_relative_order(self): - self.assertEqual( - resolve({"clarify", "analyze"}, set()), - ["specify", "plan", "tasks", "implement"], - ) - - def test_add_out_of_order_lands_canonical(self): - expected = ["constitution", "specify", "clarify", "plan", "tasks", "checklist", "analyze", "implement"] - self.assertEqual(resolve(set(), {"constitution", "checklist"}), expected) - self.assertEqual(resolve(set(), {"checklist", "constitution"}), expected) - - def test_permutation_invariance(self): - outputs = [] - for order in ("clarify,analyze", "analyze,clarify"): - cmd = [sys.executable, str(SCRIPTS_DIR / "resolve_phases.py"), "--skip", order, "--json"] - r = subprocess.run(cmd, capture_output=True, text=True, cwd=str(SCRIPTS_DIR)) - self.assertEqual(r.returncode, 0, r.stderr) - outputs.append(r.stdout) - self.assertEqual(len(set(outputs)), 1) - - def test_unknown_name(self): - with self.assertRaises(UnknownPhase) as cm: - resolve(set(), {"nonexistent"}) - self.assertIn("nonexistent", cm.exception.names) - self.assertEqual(set(cm.exception.valid), set(DEFAULTS) | set(INSERTABLE)) - - def test_skip_add_conflict_first(self): - with self.assertRaises(SkipAddConflict) as cm: - resolve({"clarify"}, {"clarify"}) - self.assertIn("clarify", cm.exception.names) - - def test_add_non_insertable_rejected(self): - with self.assertRaises(NotInsertable) as cm: - resolve(set(), {"plan"}) - self.assertIn("plan", cm.exception.names) - - def test_skip_required_specify(self): - with self.assertRaises(RequiredPhaseSkip): - resolve({"specify"}, set()) - - def test_skip_required_implement(self): - with self.assertRaises(RequiredPhaseSkip): - resolve({"implement"}, set()) - - def test_skip_plan_collects_dep_breaks(self): - with self.assertRaises(DepBreak) as cm: - resolve({"plan"}, set()) - broken = {phase for phase, _ in cm.exception.violations} - self.assertIn("tasks", broken) - self.assertIn("analyze", broken) - self.assertIn("implement", broken) - ordered = [phase for phase, _ in cm.exception.violations] - self.assertEqual(ordered, sorted(ordered, key=lambda p: REGISTRY[p].order)) - - def test_skip_non_default_is_noop(self): - self.assertEqual(resolve({"checklist"}, set()), resolve(set(), set())) - - def test_add_checklist_after_tasks(self): - result = resolve(set(), {"checklist"}) - self.assertLess(result.index("tasks"), result.index("checklist")) - self.assertLess(result.index("checklist"), result.index("analyze")) - - def test_cli_exit_codes(self): - cases = [ - (["--skip", "nonexistent"], 10), - (["--skip", "clarify", "--add", "clarify"], 11), - (["--add", "plan"], 12), - (["--skip", "specify"], 13), - (["--skip", "plan"], 14), - ([], 0), - ] - for args, code in cases: - cmd = [sys.executable, str(SCRIPTS_DIR / "resolve_phases.py")] + args - r = subprocess.run(cmd, capture_output=True, text=True, cwd=str(SCRIPTS_DIR)) - self.assertEqual(r.returncode, code, f"{args}: {r.stderr}") - - def test_json_shape(self): - cmd = [sys.executable, str(SCRIPTS_DIR / "resolve_phases.py"), "--json"] - r = subprocess.run(cmd, capture_output=True, text=True, cwd=str(SCRIPTS_DIR)) - self.assertEqual(r.returncode, 0) - out = json.loads(r.stdout) - self.assertIsInstance(out, list) - for item in out: - for key in ("phase", "order", "command", "interactive", "description"): - self.assertIn(key, item) - orders = [item["order"] for item in out] - self.assertEqual(orders, sorted(orders)) - - def test_list_short_circuits(self): - base = subprocess.run( - [sys.executable, str(SCRIPTS_DIR / "resolve_phases.py"), "--list"], - capture_output=True, text=True, cwd=str(SCRIPTS_DIR), - ) - self.assertEqual(base.returncode, 0) - base_lines = base.stdout.strip().split("\n") - self.assertEqual(len(base_lines), len(set(DEFAULTS) | set(INSERTABLE))) - withargs = subprocess.run( - [sys.executable, str(SCRIPTS_DIR / "resolve_phases.py"), "--list", "--skip", "clarify", "--add", "constitution"], - capture_output=True, text=True, cwd=str(SCRIPTS_DIR), - ) - self.assertEqual(base_lines, withargs.stdout.strip().split("\n")) - - -if __name__ == "__main__": - unittest.main(verbosity=2) diff --git a/workflows/pipeline/CHANGELOG.md b/workflows/pipeline/CHANGELOG.md new file mode 100644 index 0000000000..7f2349dbb4 --- /dev/null +++ b/workflows/pipeline/CHANGELOG.md @@ -0,0 +1,16 @@ +# Changelog + +All notable changes to the Guided SDD Pipeline workflow will be documented in this file. + +## [1.0.0] - 2026-07-08 + +### Added +- Initial release of the Guided SDD Pipeline workflow +- Support for optional constitution phase (`with_constitution`) +- Support for optional checklist phase (`with_checklist`) +- Support for clarify gate with approval/rejection options (`skip_clarify`) +- Single pre-implement analyze pass (cross-artifact consistency check) +- Post-implement convergence loop via `speckit.converge` (up to 3 cycles) +- Integration-agnostic design supporting Claude, Copilot, Gemini, OpenCode +- Full workflow state persistence for resumable runs +- Comprehensive documentation and usage examples diff --git a/workflows/pipeline/README.md b/workflows/pipeline/README.md new file mode 100644 index 0000000000..db343399c2 --- /dev/null +++ b/workflows/pipeline/README.md @@ -0,0 +1,124 @@ +# Guided SDD Pipeline Workflow + +A comprehensive workflow that chains the core Spec Kit commands into a single, guided execution with optional phases and a post-implement convergence loop. + +## Overview + +This workflow orchestrates the full SDD (Software Design Document) pipeline: + +``` +[Constitution] → Specify → [Clarify Gate] → Plan → [Checklist] → Tasks + ↓ + Analyze → Implement → Converge → [Convergence Loop] +``` + +## Features + +- **Modular phases**: Enable/disable constitution and checklist phases as needed +- **Clarify gate**: Optional human checkpoint after clarification +- **Analyze pass**: Single pre-implement consistency check across spec, plan, and tasks +- **Convergence loop**: Post-implement loop using `speckit.converge` to append remaining work as tasks and re-implement (up to 3 cycles) +- **Integration agnostic**: Works with Claude, Copilot, Gemini, OpenCode +- **Resumable**: Full workflow state persistence for interrupted runs + +## Inputs + +| Input | Type | Default | Description | +|-------|------|---------|-------------| +| `spec` | string | required | Describe what you want to build | +| `integration` | string | `auto` | Integration to use (e.g. claude, copilot, gemini) | +| `with_constitution` | boolean | `false` | Include the constitution phase | +| `with_checklist` | boolean | `false` | Include the checklist phase | +| `skip_clarify` | boolean | `false` | Skip the clarify gate | + +## Usage + +### Run with defaults +```bash +specify workflow run pipeline "build a todo app" +``` + +### Run with all optional phases +```bash +specify workflow run pipeline \ + "build a todo app" \ + --with-constitution \ + --with-checklist +``` + +### Run with a specific integration +```bash +specify workflow run pipeline \ + "build a todo app" \ + --integration claude +``` + +### Skip clarify gate +```bash +specify workflow run pipeline \ + "build a todo app" \ + --skip-clarify +``` + +## Step Details + +### 1. Constitution (Optional) +Generates or validates project constitution if `with_constitution` is enabled. + +### 2. Specify +Generates the specification from your feature description. + +### 3. Clarify (Optional Gate) +Provides clarifications on the spec, then pauses for human review/approval before proceeding to planning. + +Can be skipped entirely with `--skip-clarify`. + +### 4. Plan +Generates the implementation plan based on the spec. + +### 5. Checklist (Optional) +Generates a task checklist if `with_checklist` is enabled. + +### 6. Tasks +Breaks the plan into actionable tasks. + +### 7. Analyze +Performs a single pre-implement consistency check across spec, plan, and tasks. Findings are surfaced but do not block progress; artifacts are updated as needed before implementation. + +### 8. Implement +Generates the implementation code based on the spec, plan, and tasks. + +### 9. Convergence Loop +- **Converge**: Runs `speckit.converge` to assess code against spec, plan, and tasks +- **Append tasks**: If convergence finds remaining work, appends new tasks to tasks.md +- **Re-implement**: Runs implement again to address the appended tasks +- **Repeats** up to 3 cycles +- **Converged pass**: When converge finds no remaining work, tasks.md is left unchanged (no-op iteration) + +## Prerequisites + +- Spec Kit >= 0.8.5 +- Initialized project with one of: Claude, Copilot, Gemini, OpenCode + +## Notes + +### Convergence Loop Behavior +The convergence loop is bounded to 3 iterations rather than condition-driven, since prompt-template commands don't reliably signal findings via exit codes. A converged pass leaves tasks.md byte-for-byte unchanged, so extra iterations are safe no-ops. + +## Related Workflows + +- **speckit**: Full SDD cycle with review gates (simpler, no convergence loop) +- See `specify workflow search` for more options + +## Contributing + +To improve this workflow: + +1. Test changes locally with `specify workflow run` +2. Update version in `workflow.yml` +3. Add entry to `CHANGELOG.md` +4. Submit PR to the Spec Kit repository + +## License + +MIT diff --git a/workflows/pipeline/workflow.yml b/workflows/pipeline/workflow.yml new file mode 100644 index 0000000000..7ea1219ee3 --- /dev/null +++ b/workflows/pipeline/workflow.yml @@ -0,0 +1,125 @@ +schema_version: "1.0" +workflow: + id: "pipeline" + name: "Guided SDD Pipeline" + version: "1.0.0" + author: "domattioli" + description: > + Chains specify → clarify → plan → tasks → analyze → implement → converge + into one guided run with a single clarify gate and a post-implement + convergence loop. + +requires: + speckit_version: ">=0.8.5" + integrations: + any: ["claude", "copilot", "gemini", "opencode"] + +inputs: + spec: + type: string + required: true + prompt: "Describe what you want to build" + integration: + type: string + default: "auto" + prompt: "Integration to use (e.g. claude, copilot, gemini; 'auto' uses the project's initialized integration)" + with_constitution: + type: boolean + default: false + prompt: "Add constitution phase to the pipeline?" + with_checklist: + type: boolean + default: false + prompt: "Add checklist phase to the pipeline?" + skip_clarify: + type: boolean + default: false + prompt: "Skip the clarify gate?" + +steps: + - id: constitution + type: if + condition: "{{ inputs.with_constitution }}" + then: + - id: constitution-run + command: speckit.constitution + integration: "{{ inputs.integration }}" + input: { args: "{{ inputs.spec }}" } + + - id: specify + command: speckit.specify + integration: "{{ inputs.integration }}" + input: { args: "{{ inputs.spec }}" } + + - id: clarify + type: if + condition: "{{ inputs.skip_clarify == false }}" + then: + - id: clarify-run + command: speckit.clarify + integration: "{{ inputs.integration }}" + input: { args: "{{ inputs.spec }}" } + - id: clarify-gate + type: gate + message: "Review clarifications before planning." + options: [approve, reject] + on_reject: abort + + - id: plan + command: speckit.plan + integration: "{{ inputs.integration }}" + input: { args: "{{ inputs.spec }}" } + + - id: checklist + type: if + condition: "{{ inputs.with_checklist }}" + then: + - id: checklist-run + command: speckit.checklist + integration: "{{ inputs.integration }}" + input: { args: "{{ inputs.spec }}" } + + - id: tasks + command: speckit.tasks + integration: "{{ inputs.integration }}" + input: { args: "{{ inputs.spec }}" } + + # analyze is a non-destructive cross-artifact consistency check (spec/plan/tasks); + # its findings are resolved in the artifacts, not by implement, so it runs as a + # single pre-implement pass rather than a fix loop. + - id: analyze + command: speckit.analyze + integration: "{{ inputs.integration }}" + continue_on_error: true + input: { args: "{{ inputs.spec }}" } + + - id: implement + command: speckit.implement + integration: "{{ inputs.integration }}" + input: { args: "{{ inputs.spec }}" } + + # converge must run after implement (per its contract): it assesses the code + # against spec/plan/tasks and appends any remaining work as new tasks. The loop + # is bounded rather than condition-driven because prompt-template commands do + # not reliably signal findings via exit codes; a `converged` pass leaves + # tasks.md untouched, so extra iterations are safe no-ops. + - id: converge-first + command: speckit.converge + integration: "{{ inputs.integration }}" + continue_on_error: true + input: { args: "{{ inputs.spec }}" } + + - id: converge-loop + type: while + condition: "{{ steps.converge-first.output.exit_code != 0 }}" + max_iterations: 3 + steps: + - id: implement-remaining + command: speckit.implement + integration: "{{ inputs.integration }}" + input: { args: "Complete the appended convergence tasks for {{ inputs.spec }}" } + - id: converge-again + command: speckit.converge + integration: "{{ inputs.integration }}" + continue_on_error: true + input: { args: "{{ inputs.spec }}" } From 7b0365d7cfabaa35e40b89f9c60d5e2396da2c6f Mon Sep 17 00:00:00 2001 From: domattioli Date: Fri, 10 Jul 2026 15:23:53 -0400 Subject: [PATCH 3/3] refactor(pipeline): move workflow to external repo, register in community catalog --- workflows/catalog.community.json | 25 ++++++- workflows/pipeline/CHANGELOG.md | 16 ---- workflows/pipeline/README.md | 124 ------------------------------ workflows/pipeline/workflow.yml | 125 ------------------------------- 4 files changed, 23 insertions(+), 267 deletions(-) delete mode 100644 workflows/pipeline/CHANGELOG.md delete mode 100644 workflows/pipeline/README.md delete mode 100644 workflows/pipeline/workflow.yml diff --git a/workflows/catalog.community.json b/workflows/catalog.community.json index c654f5ed22..81fac88e41 100644 --- a/workflows/catalog.community.json +++ b/workflows/catalog.community.json @@ -1,6 +1,27 @@ { "schema_version": "1.0", - "updated_at": "2026-04-10T00:00:00Z", + "updated_at": "2026-07-10T00:00:00Z", "catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/workflows/catalog.community.json", - "workflows": {} + "workflows": { + "pipeline": { + "id": "pipeline", + "name": "Guided SDD Pipeline", + "description": "Chains specify, clarify, plan, tasks, analyze, implement, and converge into one guided run with a single clarify gate and a post-implement convergence loop", + "author": "domattioli", + "version": "1.0.0", + "url": "https://raw.githubusercontent.com/domattioli/spec-kit-workflow-pipeline/v1.0.0/workflow.yml", + "repository": "https://github.com/domattioli/spec-kit-workflow-pipeline", + "license": "MIT", + "requires": { + "speckit_version": ">=0.8.5" + }, + "tags": [ + "sdd", + "pipeline", + "automation" + ], + "created_at": "2026-07-10T00:00:00Z", + "updated_at": "2026-07-10T00:00:00Z" + } + } } diff --git a/workflows/pipeline/CHANGELOG.md b/workflows/pipeline/CHANGELOG.md deleted file mode 100644 index 7f2349dbb4..0000000000 --- a/workflows/pipeline/CHANGELOG.md +++ /dev/null @@ -1,16 +0,0 @@ -# Changelog - -All notable changes to the Guided SDD Pipeline workflow will be documented in this file. - -## [1.0.0] - 2026-07-08 - -### Added -- Initial release of the Guided SDD Pipeline workflow -- Support for optional constitution phase (`with_constitution`) -- Support for optional checklist phase (`with_checklist`) -- Support for clarify gate with approval/rejection options (`skip_clarify`) -- Single pre-implement analyze pass (cross-artifact consistency check) -- Post-implement convergence loop via `speckit.converge` (up to 3 cycles) -- Integration-agnostic design supporting Claude, Copilot, Gemini, OpenCode -- Full workflow state persistence for resumable runs -- Comprehensive documentation and usage examples diff --git a/workflows/pipeline/README.md b/workflows/pipeline/README.md deleted file mode 100644 index db343399c2..0000000000 --- a/workflows/pipeline/README.md +++ /dev/null @@ -1,124 +0,0 @@ -# Guided SDD Pipeline Workflow - -A comprehensive workflow that chains the core Spec Kit commands into a single, guided execution with optional phases and a post-implement convergence loop. - -## Overview - -This workflow orchestrates the full SDD (Software Design Document) pipeline: - -``` -[Constitution] → Specify → [Clarify Gate] → Plan → [Checklist] → Tasks - ↓ - Analyze → Implement → Converge → [Convergence Loop] -``` - -## Features - -- **Modular phases**: Enable/disable constitution and checklist phases as needed -- **Clarify gate**: Optional human checkpoint after clarification -- **Analyze pass**: Single pre-implement consistency check across spec, plan, and tasks -- **Convergence loop**: Post-implement loop using `speckit.converge` to append remaining work as tasks and re-implement (up to 3 cycles) -- **Integration agnostic**: Works with Claude, Copilot, Gemini, OpenCode -- **Resumable**: Full workflow state persistence for interrupted runs - -## Inputs - -| Input | Type | Default | Description | -|-------|------|---------|-------------| -| `spec` | string | required | Describe what you want to build | -| `integration` | string | `auto` | Integration to use (e.g. claude, copilot, gemini) | -| `with_constitution` | boolean | `false` | Include the constitution phase | -| `with_checklist` | boolean | `false` | Include the checklist phase | -| `skip_clarify` | boolean | `false` | Skip the clarify gate | - -## Usage - -### Run with defaults -```bash -specify workflow run pipeline "build a todo app" -``` - -### Run with all optional phases -```bash -specify workflow run pipeline \ - "build a todo app" \ - --with-constitution \ - --with-checklist -``` - -### Run with a specific integration -```bash -specify workflow run pipeline \ - "build a todo app" \ - --integration claude -``` - -### Skip clarify gate -```bash -specify workflow run pipeline \ - "build a todo app" \ - --skip-clarify -``` - -## Step Details - -### 1. Constitution (Optional) -Generates or validates project constitution if `with_constitution` is enabled. - -### 2. Specify -Generates the specification from your feature description. - -### 3. Clarify (Optional Gate) -Provides clarifications on the spec, then pauses for human review/approval before proceeding to planning. - -Can be skipped entirely with `--skip-clarify`. - -### 4. Plan -Generates the implementation plan based on the spec. - -### 5. Checklist (Optional) -Generates a task checklist if `with_checklist` is enabled. - -### 6. Tasks -Breaks the plan into actionable tasks. - -### 7. Analyze -Performs a single pre-implement consistency check across spec, plan, and tasks. Findings are surfaced but do not block progress; artifacts are updated as needed before implementation. - -### 8. Implement -Generates the implementation code based on the spec, plan, and tasks. - -### 9. Convergence Loop -- **Converge**: Runs `speckit.converge` to assess code against spec, plan, and tasks -- **Append tasks**: If convergence finds remaining work, appends new tasks to tasks.md -- **Re-implement**: Runs implement again to address the appended tasks -- **Repeats** up to 3 cycles -- **Converged pass**: When converge finds no remaining work, tasks.md is left unchanged (no-op iteration) - -## Prerequisites - -- Spec Kit >= 0.8.5 -- Initialized project with one of: Claude, Copilot, Gemini, OpenCode - -## Notes - -### Convergence Loop Behavior -The convergence loop is bounded to 3 iterations rather than condition-driven, since prompt-template commands don't reliably signal findings via exit codes. A converged pass leaves tasks.md byte-for-byte unchanged, so extra iterations are safe no-ops. - -## Related Workflows - -- **speckit**: Full SDD cycle with review gates (simpler, no convergence loop) -- See `specify workflow search` for more options - -## Contributing - -To improve this workflow: - -1. Test changes locally with `specify workflow run` -2. Update version in `workflow.yml` -3. Add entry to `CHANGELOG.md` -4. Submit PR to the Spec Kit repository - -## License - -MIT diff --git a/workflows/pipeline/workflow.yml b/workflows/pipeline/workflow.yml deleted file mode 100644 index 7ea1219ee3..0000000000 --- a/workflows/pipeline/workflow.yml +++ /dev/null @@ -1,125 +0,0 @@ -schema_version: "1.0" -workflow: - id: "pipeline" - name: "Guided SDD Pipeline" - version: "1.0.0" - author: "domattioli" - description: > - Chains specify → clarify → plan → tasks → analyze → implement → converge - into one guided run with a single clarify gate and a post-implement - convergence loop. - -requires: - speckit_version: ">=0.8.5" - integrations: - any: ["claude", "copilot", "gemini", "opencode"] - -inputs: - spec: - type: string - required: true - prompt: "Describe what you want to build" - integration: - type: string - default: "auto" - prompt: "Integration to use (e.g. claude, copilot, gemini; 'auto' uses the project's initialized integration)" - with_constitution: - type: boolean - default: false - prompt: "Add constitution phase to the pipeline?" - with_checklist: - type: boolean - default: false - prompt: "Add checklist phase to the pipeline?" - skip_clarify: - type: boolean - default: false - prompt: "Skip the clarify gate?" - -steps: - - id: constitution - type: if - condition: "{{ inputs.with_constitution }}" - then: - - id: constitution-run - command: speckit.constitution - integration: "{{ inputs.integration }}" - input: { args: "{{ inputs.spec }}" } - - - id: specify - command: speckit.specify - integration: "{{ inputs.integration }}" - input: { args: "{{ inputs.spec }}" } - - - id: clarify - type: if - condition: "{{ inputs.skip_clarify == false }}" - then: - - id: clarify-run - command: speckit.clarify - integration: "{{ inputs.integration }}" - input: { args: "{{ inputs.spec }}" } - - id: clarify-gate - type: gate - message: "Review clarifications before planning." - options: [approve, reject] - on_reject: abort - - - id: plan - command: speckit.plan - integration: "{{ inputs.integration }}" - input: { args: "{{ inputs.spec }}" } - - - id: checklist - type: if - condition: "{{ inputs.with_checklist }}" - then: - - id: checklist-run - command: speckit.checklist - integration: "{{ inputs.integration }}" - input: { args: "{{ inputs.spec }}" } - - - id: tasks - command: speckit.tasks - integration: "{{ inputs.integration }}" - input: { args: "{{ inputs.spec }}" } - - # analyze is a non-destructive cross-artifact consistency check (spec/plan/tasks); - # its findings are resolved in the artifacts, not by implement, so it runs as a - # single pre-implement pass rather than a fix loop. - - id: analyze - command: speckit.analyze - integration: "{{ inputs.integration }}" - continue_on_error: true - input: { args: "{{ inputs.spec }}" } - - - id: implement - command: speckit.implement - integration: "{{ inputs.integration }}" - input: { args: "{{ inputs.spec }}" } - - # converge must run after implement (per its contract): it assesses the code - # against spec/plan/tasks and appends any remaining work as new tasks. The loop - # is bounded rather than condition-driven because prompt-template commands do - # not reliably signal findings via exit codes; a `converged` pass leaves - # tasks.md untouched, so extra iterations are safe no-ops. - - id: converge-first - command: speckit.converge - integration: "{{ inputs.integration }}" - continue_on_error: true - input: { args: "{{ inputs.spec }}" } - - - id: converge-loop - type: while - condition: "{{ steps.converge-first.output.exit_code != 0 }}" - max_iterations: 3 - steps: - - id: implement-remaining - command: speckit.implement - integration: "{{ inputs.integration }}" - input: { args: "Complete the appended convergence tasks for {{ inputs.spec }}" } - - id: converge-again - command: speckit.converge - integration: "{{ inputs.integration }}" - continue_on_error: true - input: { args: "{{ inputs.spec }}" }