mirror of
https://github.com/github/spec-kit.git
synced 2026-07-03 12:28:06 +08:00
Adds an optional `continue_on_error: bool` field on every step.
When set to `true` and the step fails, the engine records the
result (`exit_code`, `stderr` on `steps.<id>.output` plus `status`
as a sibling key on `steps.<id>`) and continues to the next sibling
step instead of halting the run. Downstream `if`, `switch`, or
`gate` steps can then branch on
`{{ steps.<id>.output.exit_code }}` to route the recovery path.
Engine details
--------------
`WorkflowEngine._execute_steps` now consults the step config when a
step returns `StepStatus.FAILED`:
- Gate aborts (`output.aborted`) always halt the run — operator
decisions take precedence over the flag.
- Otherwise, if `continue_on_error` is the literal `True`, log a
`step_continue_on_error` event and proceed to the next sibling.
The runtime check uses identity comparison (`is True`) rather
than truthiness, so truthy non-bool values like the string
`"true"` cannot silently change run semantics even if a caller
bypasses `validate_workflow()`.
- Otherwise, behave as before: log `step_failed`, set
`RunStatus.FAILED`, and return.
Validation
----------
`_validate_steps` rejects non-bool values for `continue_on_error`.
Coerced strings like `"true"` are not accepted so authoring
mistakes surface at validation time rather than silently changing
run semantics.
Tests
-----
`TestContinueOnError` in `tests/test_workflows.py` (8 tests):
- `test_undeclared_failure_halts_run` — default halt behaviour.
- `test_declared_and_fired_continues_run` — flag + fail → continue.
- `test_declared_but_step_succeeded_is_noop` — flag + success → no-op.
- `test_if_branch_routes_around_failure` — end-to-end recovery.
- `test_gate_abort_still_halts_with_continue_on_error` — abort
always halts.
- `test_validation_rejects_non_bool_continue_on_error` — `"true"`
rejected at validation.
- `test_validation_accepts_bool_continue_on_error` — `true`/`false`
pass cleanly.
- `test_engine_ignores_truthy_non_bool_continue_on_error` —
defense-in-depth: engine ignores string `"true"` even when
validation is bypassed.
Rebased onto current upstream/main (post #2664 merge); the new
`TestContinueOnError` class sits immediately after upstream's
`TestContextRunId` so the two feature suites coexist cleanly.
Closes #2591.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
385 lines
9.6 KiB
Markdown
385 lines
9.6 KiB
Markdown
# 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.
|
|
|
|
```yaml
|
|
steps:
|
|
- id: specify
|
|
command: speckit.specify
|
|
input:
|
|
args: "{{ inputs.spec }}"
|
|
|
|
- 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](ARCHITECTURE.md).
|
|
|
|
## Quick Start
|
|
|
|
```bash
|
|
# 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 spec="Build a user authentication system with OAuth support"
|
|
|
|
# Run an installed workflow with inputs
|
|
specify workflow run speckit --input spec="Build a user authentication system with OAuth support"
|
|
|
|
# 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
|
|
|
|
```bash
|
|
specify workflow add speckit
|
|
specify workflow run speckit --input spec="Build a user authentication system with OAuth support"
|
|
```
|
|
|
|
### From a Local YAML File
|
|
|
|
```bash
|
|
specify workflow run ./my-workflow.yml --input spec="Build a user authentication system with OAuth support"
|
|
```
|
|
|
|
### Multiple Inputs
|
|
|
|
```bash
|
|
specify workflow run speckit \
|
|
--input spec="Build a user authentication system with OAuth support" \
|
|
--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:
|
|
|
|
```yaml
|
|
- id: specify
|
|
command: speckit.specify
|
|
input:
|
|
args: "{{ inputs.spec }}"
|
|
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):
|
|
|
|
```yaml
|
|
- id: security-review
|
|
type: prompt
|
|
prompt: "Review {{ inputs.file }} for security vulnerabilities"
|
|
integration: claude
|
|
```
|
|
|
|
### Shell Steps
|
|
|
|
Run a shell command and capture output:
|
|
|
|
```yaml
|
|
- 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:
|
|
|
|
```yaml
|
|
- 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:
|
|
|
|
```yaml
|
|
- 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:
|
|
|
|
```yaml
|
|
- 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:
|
|
|
|
```yaml
|
|
- 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:
|
|
|
|
```yaml
|
|
- 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):
|
|
|
|
```yaml
|
|
- 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:
|
|
|
|
```yaml
|
|
- id: collect
|
|
type: fan-in
|
|
wait_for: [parallel-impl]
|
|
output: {}
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
By default, any step that ends in `StepStatus.FAILED` at runtime halts
|
|
the entire run — most commonly a `shell` or `command` step exiting
|
|
non-zero, but also any other runtime error raised during step
|
|
execution. (Invalid workflow definitions are rejected up-front by
|
|
`specify workflow run` before the run even starts, so structural
|
|
validation failures never reach this code path.) Set
|
|
`continue_on_error: true` on a step to record its result and continue
|
|
to the next sibling step instead. When the failure was a non-zero
|
|
exit, the exit code remains available on
|
|
`steps.<id>.output.exit_code` so downstream `if`, `switch`, or `gate`
|
|
steps can branch on it:
|
|
|
|
```yaml
|
|
- id: heavy-thing
|
|
type: command
|
|
integration: claude
|
|
command: speckit.heavy-thing
|
|
continue_on_error: true
|
|
|
|
- id: check-result
|
|
type: if
|
|
condition: "{{ steps.heavy-thing.output.exit_code != 0 }}"
|
|
then:
|
|
- id: review
|
|
type: gate
|
|
message: "Step failed (exit {{ steps.heavy-thing.output.exit_code }}). Retry or skip?"
|
|
on_reject: skip
|
|
else:
|
|
- id: next-thing
|
|
command: speckit.next-thing
|
|
```
|
|
|
|
**Notes:**
|
|
|
|
- The field must be a literal boolean (`true` / `false`); coerced
|
|
strings like `"true"` are rejected at validation time.
|
|
- Gate aborts (`on_reject: abort` chosen by the operator) always halt
|
|
the run — `continue_on_error` does not override them. The flag is
|
|
for transient/expected step failures, not for overriding deliberate
|
|
operator decisions.
|
|
- When the flag is omitted, behaviour is byte-equivalent to before
|
|
this feature.
|
|
|
|
## Expressions
|
|
|
|
Workflow definitions use `{{ expression }}` syntax for dynamic values:
|
|
|
|
```yaml
|
|
# Access inputs
|
|
args: "{{ inputs.spec }}"
|
|
|
|
# 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:
|
|
|
|
```yaml
|
|
inputs:
|
|
spec:
|
|
type: string
|
|
required: true
|
|
prompt: "Describe what you want to build"
|
|
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>/`:
|
|
|
|
```bash
|
|
# 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.
|
|
|
|
```bash
|
|
# 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
|
|
|
|
1. Create a `workflow.yml` following the schema above
|
|
2. Test locally with `specify workflow run ./workflow.yml --input key=value`
|
|
3. Verify with `specify workflow info ./workflow.yml`
|
|
4. See [PUBLISHING.md](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
|
|
```
|