* Initial plan * Add workflow engine with step registry, expression engine, catalog system, and CLI commands Agent-Logs-Url: https://github.com/github/spec-kit/sessions/72a7bb5d-071f-4d67-a507-7e1abae2384d Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com> * Add comprehensive tests for workflow engine (94 tests) Agent-Logs-Url: https://github.com/github/spec-kit/sessions/72a7bb5d-071f-4d67-a507-7e1abae2384d Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com> * Address review feedback: do-while condition preservation and URL scheme validation Agent-Logs-Url: https://github.com/github/spec-kit/sessions/72a7bb5d-071f-4d67-a507-7e1abae2384d Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com> * Address review feedback, add CLI dispatch, interactive gates, and docs Review comments (7/7): - Add explanatory comment to empty except block - Implement workflow catalog download with cleanup on failure - Add input type coercion for number/boolean/enum - Fix example workflow to remove non-existent output references - Fix while_loop and if_then condition defaults (string 'false' → bool False) - Fix resume step index tracking with step_offset parameter CLI dispatch: - Add build_exec_args() and dispatch_command() to IntegrationBase - Override for Claude (skills: /speckit-specify), Gemini (-m flag), Codex (codex exec), Copilot (--agent speckit.specify) - CommandStep invokes installed commands by name via integration CLI - Add PromptStep for arbitrary inline prompts (10th step type) - Stream CLI output live to terminal (no silent blocking) - Remove timeout when streaming (user can Ctrl+C) - Ctrl+C saves state as PAUSED for clean resume Interactive gates: - Gate steps prompt [1] approve [2] reject in TTY - Fall back to PAUSED in non-interactive environments - Resume re-executes the gate for interactive prompting Documentation: - workflows/README.md — user guide - workflows/ARCHITECTURE.md — internals with Mermaid diagrams - workflows/PUBLISHING.md — catalog submission guide Tests: 94 → 122 workflow tests, 1362 total (all passing) * Fix ruff lint errors: unused imports, f-string placeholders, undefined name * Address second review: registry-backed validation, shell failures, loop/fan-out execution, URL validation - VALID_STEP_TYPES now queries STEP_REGISTRY dynamically - Shell step returns FAILED on non-zero exit code - Persist workflow YAML in run directory for reliable resume - Resume loads from run copy, falls back to installed workflow - Engine iterates while/do-while loops up to max_iterations - Engine expands fan-out per item with context.item - HTTPS URL validation for catalog workflow installs (HTTP allowed for localhost) - Fix catalog merge priority docstring (lower number wins) - Fix dispatch_command docstring (no build_exec_args_for_command) - Gate on_reject=retry pauses for re-prompt on resume - Update docs to 10 step types, add prompt step to tables and README * Potential fix for pull request finding 'Empty except' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> * Address third review: fan-out IDs, catalog guards, shell coercion, docs - Fan-out generates unique per-item step IDs and collects results - Catalog merge skips non-dict workflow entries (malformed data guard) - Shell step coerces run_cmd to str after expression evaluation - urlopen timeout=30 for catalog workflow installs - yaml.dump with sort_keys=False, allow_unicode=True for catalog configs - Document streaming timeout as intentionally unbounded (user Ctrl+C) - Document --allow-all-tools as required for non-interactive + future enhancement - Update test docstring and PUBLISHING.md to 10 step types with prompt * Validate final URL after redirects in catalog fetch urlopen follows redirects, so validate the response URL against the same HTTPS/localhost rules to prevent redirect-based downgrade attacks. * Address fourth review: filter arg eval, tags normalization, install redirect check - Filter arguments now evaluated via _evaluate_simple_expression() so default(42) returns int not string - Tags normalized: non-list/non-string values handled gracefully - Install URL redirect validation (same as catalog fetch) - Remove unused 'skipped' variable in catalog config parsing - Author 'github' → 'GitHub' in example workflow - Document nested step resume limitation (re-runs parent step) * Add explanatory comment to empty except ValueError block * Address fifth review: expression parsing, fan-out output, URL install, gate options - Move string literal parsing before operator detection in expressions so quoted strings with operators (e.g. 'a in b') are not mis-parsed - Fan-out: remove max_concurrency from persisted output, fix docstring to reflect sequential execution - workflow add: support URL sources with HTTPS/redirect validation, validate workflow ID is non-empty before writing files - Deduplicate local install logic via _validate_and_install_local() - Remove 'edit' gate option from speckit workflow (not implemented) * Add comments to empty except ValueError blocks in URL install * Address sixth review: operator precedence, fan_in cleanup, registry resilience, docs - Fix or/and operator precedence (or parsed first = lower precedence) - Restore context.fan_in after fan-in step completes - Catch JSONDecodeError in registry load for corrupted files - Replace print() with on_step_start callback (library-safe) - Gate validation warns when on_reject set but no reject option - Shell step: document shell=True security tradeoff - README: sdd-pipeline → speckit, parallel → sequential for fan-out - ARCHITECTURE.md: parallel → fan-out/fan-in in diagram * Address seventh review: string literal before pipe, type annotations, validate on install - Move string literal check above pipe filter parsing so 'a | b' works - Fix type annotations: input_values list[str] | None, run_id str | None - Run validate_workflow() before installing from local path/URL - Remove duplicate string literal check from expression parser * Address eighth review: fan-out namespaced IDs, early return, catalog validation - Fan-out per-item step IDs use _fanout_{step_id}_{base}_{idx} namespace to avoid collisions with user-defined step IDs - Early return after fan-out loop when state is paused/failed/aborted - Catalog installs parse + validate downloaded YAML before registering, using definition metadata instead of catalog entry for registry * Address ninth review: populate catalog, fix indentation, priority, README - Add speckit workflow entry to catalog.json so it's discoverable - Fix shell step output dict indentation - Catalog add_catalog priority derived from max existing + 1 - README Quick Start clarified with install + local file examples * Address tenth review: max_iterations validation, catalog config guard, version alignment - Validate max_iterations is int >= 1 in while and do-while steps - Guard add_catalog against corrupted config (non-dict/non-list) - Align speckit_version requirement to >=0.6.1 (current package version) - Fan-out template validation uses separate seen_ids set to avoid false duplication errors with user-defined step IDs * Address eleventh review: command step fails without CLI, ID mismatch warning, state persistence - Command step returns FAILED when CLI not installed (was silent COMPLETED) - Catalog install warns on workflow ID vs catalog key mismatch - Engine persists state.save() before returning on unknown step type - Update tests to expect FAILED for command steps without CLI - Integration tests use shell steps for CLI-independent execution * Address twelfth review: type annotations, version examples, streaming docs, requires - Fix workflow_search type annotations (str | None) - PUBLISHING.md: speckit_version >=0.15.0 → >=0.6.1 - Document that exit_code is captured and referenceable by later steps - Mark requires as declared-but-not-enforced (planned enhancement) - Note full stdout/stderr capture as planned enhancement * Enforce catalog key matches workflow ID (fail instead of warn) * Bundle speckit workflow: auto-install during specify init - Add workflows/speckit to pyproject.toml force-include for wheel builds - Add _locate_bundled_workflow() helper (mirrors _locate_bundled_extension) - Auto-install speckit workflow during specify init (after git extension) - Update all integration file inventory tests to expect workflow files * Address fourteenth review: prompt fails without CLI, resolved step data, fan-out normalization - PromptStep returns FAILED when CLI not installed (was silent COMPLETED) - Engine step_data prefers resolved values from step output - Fan-out normalizes output.results=[] for empty item lists - subprocess.run inherits stdout/stderr (no explicit sys.stdout) - Registry tests use issubset for extensibility * Address fifteenth review: fan_in docstring, gate defaults, validation guards, reserved prefix - FanInStep docstring: aggregate-only, no blocking semantics - FanInStep: guard output_config as dict, handle None - Gate validate: use same default options as execute - Validate inputs is dict and steps is list before iterating - Reserve _fanout_ prefix in step ID validation - PUBLISHING.md: remove unenforced checklist items, add _fanout_ note * Address sixteenth review: docs regex, fan_in try/finally, hyphenated dot-path keys - PUBLISHING.md: update ID regex docs to match implementation (single-char OK) - FanInStep: wrap expression evaluation in try/finally for context.fan_in - Expression dot-path: allow hyphens in keys before list index (e.g. run-tests[0]) * Make speckit workflow integration-agnostic, document Copilot CLI requirement - Workflow integration selectable via input (default: claude) - Each command step uses {{ inputs.integration }} instead of hardcoded copilot - Copilot docstring documents CLI requirement for workflow dispatch - Added install_url for Copilot CLI docs * Address seventeenth review: project checks, catalog robustness - Add .specify/ project check to workflow run/resume/status/search/info - remove_catalog validates config shape (dict + list) before indexing - _fetch_single_catalog validates response is a dict - _get_merged_workflows raises when all catalogs fail to fetch - add_catalog guards against non-dict catalog entries in config * Address eighteenth review: condition coercion, gate abort result, while default, cache guard, resume log - evaluate_condition treats plain 'false'/'true' strings as booleans - Gate abort returns StepResult(FAILED) instead of raising exception so step output is persisted in state for inspection - while_loop max_iterations optional (default 10), validation aligned - Catalog cache fallback catches invalid JSON gracefully - resume() appends workflow_finished log entry like execute() * Address nineteenth review: allow-all-tools opt-in, empty catalogs, abort dead code, while docstring - --allow-all-tools controlled by SPECKIT_ALLOW_ALL_TOOLS env var (default: 1) Set to 0 to disable automatic tool approval for Copilot CLI - Empty catalogs list falls back to built-in defaults (not an error) - Remove unreachable WorkflowAbortError catches from execute/resume (gate abort now returns StepResult(FAILED) instead of raising) - while_loop docstring updated: max_iterations is optional (default 10) * Address twentieth review: gate abort maps to ABORTED status, do-while max_iterations optional - Engine detects output.aborted from gate step and sets RunStatus.ABORTED (was unreachable — gate abort returned FAILED but status was always FAILED) - do-while max_iterations now optional (default 10), aligned with while_loop - do-while docstring and validation updated accordingly * Coerce default_options to dict, align bundled workflow ID regex with validator * Gate validates string options, prompt uses resolved integration, loop normalizes max_iterations * Use parentId:childId convention for nested step IDs - Fan-out per-item IDs use parentId:templateId:index (e.g. parallel:impl:0) - Reserve ':' in user step IDs (validation rejects) - Replaces _fanout_ prefix with cleaner namespacing - Expressions like {{ steps.parallel:impl:0.output.file }} work naturally * Validate workflow version is semantic versioning (X.Y.Z) * Schema version validation, strict semver, load_workflow docstring, preserve max_concurrency - Validate schema_version is '1.0' (reject unknown future schemas) - Strict semver regex: ^\d+\.\d+\.\d+$ (rejects 1.0.0beta etc.) - load_workflow docstring: 'parsed' not 'validated' - Keep max_concurrency in fan-out output (was dropped) - do_while docstring: engine re-evaluates step_config condition - ARCHITECTURE.md: document nested resume limitation * Path traversal prevention, loop step ID namespacing - RunState validates run_id is alphanumeric+hyphens (no path separators) - workflow_add validates catalog source doesn't escape workflows_dir - Loop iterations namespace nested step IDs as parentId:childId:iteration so multiple iterations don't overwrite each other in context/state --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: mnriem <15701806+mnriem@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
7.9 KiB
Workflows
Workflows are multi-step, resumable automation pipelines defined in YAML. They orchestrate Spec Kit commands across integrations, evaluate control flow, and pause at human review gates — enabling end-to-end Spec-Driven Development cycles without manual step-by-step invocation.
How It Works
A workflow definition declares a sequence of steps. The engine executes them in order, dispatching commands to AI integrations, running shell commands, evaluating conditions for branching, and pausing at gates for human review. State is persisted after each step, so workflows can be resumed after interruption.
steps:
- id: specify
command: speckit.specify
input:
args: "{{ inputs.feature_name }}"
- id: review
type: gate
message: "Review the spec before planning."
options: [approve, reject]
on_reject: abort
- id: plan
command: speckit.plan
For detailed architecture and internals, see ARCHITECTURE.md.
Quick Start
# Search available workflows
specify workflow search
# Install the built-in SDD workflow
specify workflow add speckit
# Or run directly from a local YAML file
specify workflow run ./workflow.yml --input feature_name="user-auth"
# Run an installed workflow with inputs
specify workflow run speckit --input feature_name="user-auth"
# Check run status
specify workflow status
# Resume after a gate pause
specify workflow resume <run_id>
# Get detailed workflow info
specify workflow info speckit
# Remove a workflow
specify workflow remove speckit
Running Workflows
From an Installed Workflow
specify workflow add speckit
specify workflow run speckit --input feature_name="user-auth"
From a Local YAML File
specify workflow run ./my-workflow.yml --input feature_name="user-auth"
Multiple Inputs
specify workflow run speckit \
--input feature_name="user-auth" \
--input scope="backend-only"
Step Types
Workflows support 10 built-in step types:
Command Steps (default)
Invoke an installed Spec Kit command by name via the integration CLI:
- id: specify
command: speckit.specify
input:
args: "{{ inputs.feature_name }}"
integration: claude # Optional: override workflow default
model: "claude-sonnet-4-20250514" # Optional: override model
Prompt Steps
Send an arbitrary inline prompt to an integration CLI (no command file needed):
- id: security-review
type: prompt
prompt: "Review {{ inputs.file }} for security vulnerabilities"
integration: claude
Shell Steps
Run a shell command and capture output:
- id: run-tests
type: shell
run: "cd {{ inputs.project_dir }} && npm test"
Gate Steps
Pause for human review. The workflow resumes when specify workflow resume is called:
- id: review-spec
type: gate
message: "Review the generated spec before planning."
options: [approve, edit, reject]
on_reject: abort
If/Then/Else Steps
Conditional branching based on an expression:
- id: check-scope
type: if
condition: "{{ inputs.scope == 'full' }}"
then:
- id: full-plan
command: speckit.plan
else:
- id: quick-plan
command: speckit.plan
options:
quick: true
Switch Steps
Multi-branch dispatch on an expression value:
- id: route
type: switch
expression: "{{ steps.review.output.choice }}"
cases:
approve:
- id: plan
command: speckit.plan
reject:
- id: log
type: shell
run: "echo 'Rejected'"
default:
- id: fallback
type: gate
message: "Unexpected choice"
While Loop Steps
Repeat steps while a condition is truthy:
- id: retry
type: while
condition: "{{ steps.run-tests.output.exit_code != 0 }}"
max_iterations: 5
steps:
- id: fix
command: speckit.implement
Do-While Loop Steps
Execute steps at least once, then repeat while condition holds:
- id: refine
type: do-while
condition: "{{ steps.review.output.choice == 'edit' }}"
max_iterations: 3
steps:
- id: revise
command: speckit.specify
Fan-Out Steps
Dispatch a step template for each item in a collection (sequential):
- id: parallel-impl
type: fan-out
items: "{{ steps.tasks.output.task_list }}"
max_concurrency: 3
step:
id: impl
command: speckit.implement
Fan-In Steps
Aggregate results from fan-out steps:
- id: collect
type: fan-in
wait_for: [parallel-impl]
output: {}
Expressions
Workflow definitions use {{ expression }} syntax for dynamic values:
# Access inputs
args: "{{ inputs.feature_name }}"
# Access previous step outputs
args: "{{ steps.specify.output.file }}"
# Comparisons
condition: "{{ steps.run-tests.output.exit_code != 0 }}"
# Filters
message: "{{ status | default('pending') }}"
Supported filters: default, join, contains, map.
Input Types
Workflow inputs are type-checked and coerced from CLI string values:
inputs:
feature_name:
type: string
required: true
prompt: "Feature name"
task_count:
type: number
default: 5
dry_run:
type: boolean
default: false
scope:
type: string
default: "full"
enum: ["full", "backend-only", "frontend-only"]
| Type | Accepts | Example |
|---|---|---|
string |
Any string | "user-auth" |
number |
Numeric strings → int/float | "42" → 42 |
boolean |
true/1/yes → True, false/0/no → False |
"true" → True |
State and Resume
Every workflow run persists state to .specify/workflows/runs/<run_id>/:
# List all runs with status
specify workflow status
# Check a specific run
specify workflow status <run_id>
# Resume a paused run (after approving a gate)
specify workflow resume <run_id>
# Resume a failed run (retries from the failed step)
specify workflow resume <run_id>
Run states: created → running → completed | paused | failed | aborted
Catalog Management
Workflows are discovered through catalogs. By default, Spec Kit uses the official and community catalogs:
Note
Community workflows are independently created and maintained by their respective authors. GitHub and the Spec Kit maintainers may review pull requests that add entries to the community catalog for formatting and structure, but they do not review, audit, endorse, or support the workflow definitions themselves. Review workflow source before installation and use at your own discretion.
# List active catalogs
specify workflow catalog list
# Add a custom catalog
specify workflow catalog add https://example.com/catalog.json --name my-org
# Remove a catalog
specify workflow catalog remove <index>
Creating a Workflow
- Create a
workflow.ymlfollowing the schema above - Test locally with
specify workflow run ./workflow.yml --input key=value - Verify with
specify workflow info ./workflow.yml - See PUBLISHING.md to submit to the catalog
Environment Variables
| Variable | Description |
|---|---|
SPECKIT_WORKFLOW_CATALOG_URL |
Override the catalog URL (replaces all defaults) |
Configuration Files
| File | Scope | Description |
|---|---|---|
.specify/workflow-catalogs.yml |
Project | Custom catalog stack for this project |
~/.specify/workflow-catalogs.yml |
User | Custom catalog stack for all projects |
Repository Layout
workflows/
├── ARCHITECTURE.md # Internal architecture documentation
├── PUBLISHING.md # Guide for submitting workflows to the catalog
├── README.md # This file
├── catalog.json # Official workflow catalog
├── catalog.community.json # Community workflow catalog
└── speckit/ # Built-in SDD cycle workflow
└── workflow.yml