chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
# Workflow System Architecture
|
||||
|
||||
This document describes the internal architecture of the workflow engine — how definitions are parsed, steps are dispatched, state is persisted, and catalogs are resolved.
|
||||
|
||||
For usage instructions, see [README.md](README.md).
|
||||
|
||||
## Execution Model
|
||||
|
||||
When `specify workflow run` is invoked, the engine loads a YAML definition, resolves inputs, and dispatches steps sequentially through the step registry:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["specify workflow run my-workflow"] --> B["WorkflowEngine.load_workflow()"]
|
||||
B --> C["WorkflowDefinition.from_yaml()"]
|
||||
C --> D["_resolve_inputs()"]
|
||||
D --> E["validate_workflow()"]
|
||||
E --> F["RunState.create()"]
|
||||
F --> G["_execute_steps()"]
|
||||
G --> H{Step type?}
|
||||
H -- command --> I["CommandStep.execute()"]
|
||||
H -- shell --> J["ShellStep.execute()"]
|
||||
H -- gate --> K["GateStep.execute()"]
|
||||
H -- "if" --> L["IfThenStep.execute()"]
|
||||
H -- switch --> M["SwitchStep.execute()"]
|
||||
H -- "while/do-while" --> N["Loop steps"]
|
||||
H -- "fan-out/fan-in" --> O["Fan-out/fan-in"]
|
||||
|
||||
I --> P{Result status?}
|
||||
J --> P
|
||||
K --> P
|
||||
L --> P
|
||||
M --> P
|
||||
N --> P
|
||||
O --> P
|
||||
P -- COMPLETED --> Q{Has next_steps?}
|
||||
P -- PAUSED --> R["Save state → exit"]
|
||||
P -- FAILED --> S["Log error → exit"]
|
||||
Q -- Yes --> G
|
||||
Q -- No --> T{More steps?}
|
||||
T -- Yes --> G
|
||||
T -- No --> U["Status = COMPLETED"]
|
||||
|
||||
style R fill:#ff9800,color:#fff
|
||||
style S fill:#f44336,color:#fff
|
||||
style U fill:#4caf50,color:#fff
|
||||
```
|
||||
|
||||
### Sequential Execution
|
||||
|
||||
Steps execute sequentially. Each step receives a `StepContext` containing resolved inputs, accumulated step results, and workflow-level defaults. After execution, the step's output is stored in `context.steps[step_id]` and made available to subsequent steps via expressions like `{{ steps.specify.output.file }}`.
|
||||
|
||||
### Nested Steps (Control Flow)
|
||||
|
||||
Steps like `if`, `switch`, `while`, and `do-while` return `next_steps` — inline step definitions that the engine executes recursively via `_execute_steps()`. Nested steps share the same `StepContext` and `RunState`, so their outputs are visible to later top-level steps.
|
||||
|
||||
### State Persistence and Resume
|
||||
|
||||
The engine saves `RunState` to disk after each step, enabling resume from the exact point of interruption:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["CREATED"] --> B["RUNNING"]
|
||||
B --> C["COMPLETED"]
|
||||
B --> D["PAUSED"]
|
||||
B --> E["FAILED"]
|
||||
B --> F["ABORTED"]
|
||||
D -- "resume()" --> B
|
||||
E -- "resume()" --> B
|
||||
```
|
||||
|
||||
When a `gate` step pauses execution, the engine persists `current_step_index` and all accumulated `step_results`. On `specify workflow resume <run_id>`, the engine restores the context and continues from the paused step.
|
||||
|
||||
> **Note:** Resume tracking is at the top-level step index only. If a
|
||||
> nested step (inside `if`/`switch`/`while`) pauses, resume re-runs
|
||||
> the parent control-flow step and its nested body. A nested step-path
|
||||
> stack for exact resume is a planned enhancement.
|
||||
|
||||
## Step Types
|
||||
|
||||
The engine ships with 11 built-in step types, each in its own subpackage under `src/specify_cli/workflows/steps/`:
|
||||
|
||||
| Type Key | Class | Purpose | Returns `next_steps`? |
|
||||
|----------|-------|---------|-----------------------|
|
||||
| `command` | `CommandStep` | Invoke an installed Spec Kit command via integration CLI | No |
|
||||
| `prompt` | `PromptStep` | Send an arbitrary inline prompt to integration CLI | No |
|
||||
| `shell` | `ShellStep` | Run a shell command, capture output | No |
|
||||
| `init` | `InitStep` | Bootstrap a project (equivalent to `specify init`) | No |
|
||||
| `gate` | `GateStep` | Interactive human review/approval | No (pauses in CI) |
|
||||
| `if` | `IfThenStep` | Conditional branching (then/else) | Yes |
|
||||
| `switch` | `SwitchStep` | Multi-branch dispatch on expression | Yes |
|
||||
| `while` | `WhileStep` | Loop while condition is truthy | Yes (if true) |
|
||||
| `do-while` | `DoWhileStep` | Loop, always runs body at least once | Yes (always) |
|
||||
| `fan-out` | `FanOutStep` | Dispatch per item over a collection | No (engine expands) |
|
||||
| `fan-in` | `FanInStep` | Aggregate results from fan-out | No |
|
||||
|
||||
## Step Registry
|
||||
|
||||
All step types register into `STEP_REGISTRY` via `_register_builtin_steps()` in `src/specify_cli/workflows/__init__.py`. The registry maps `type_key` strings to step instances:
|
||||
|
||||
```python
|
||||
STEP_REGISTRY: dict[str, StepBase] # e.g., {"command": CommandStep(), "gate": GateStep(), ...}
|
||||
```
|
||||
|
||||
Registration is explicit — each step class is imported and instantiated. New step types follow the same pattern: subclass `StepBase`, set `type_key`, implement `execute()` and optionally `validate()`.
|
||||
|
||||
## Expression Engine
|
||||
|
||||
Workflow definitions use Jinja2-like `{{ expression }}` syntax for dynamic values. The expression engine in `src/specify_cli/workflows/expressions.py` supports:
|
||||
|
||||
| Feature | Syntax | Example |
|
||||
|---------|--------|---------|
|
||||
| Variable access | `{{ inputs.name }}` | Dot-path traversal into context |
|
||||
| Step outputs | `{{ steps.plan.output.file }}` | Access previous step results |
|
||||
| Comparisons | `==`, `!=`, `>`, `<`, `>=`, `<=` | `{{ count > 5 }}` |
|
||||
| Boolean logic | `and`, `or`, `not` | `{{ items and status == 'ok' }}` |
|
||||
| Membership | `in`, `not in` | `{{ 'error' not in status }}` |
|
||||
| Literals | strings, numbers, booleans, lists | `{{ true }}`, `{{ [1, 2] }}` |
|
||||
| Filter: `default` | `{{ val \| default('fallback') }}` | Fallback for None/empty |
|
||||
| Filter: `join` | `{{ list \| join(', ') }}` | Join list elements |
|
||||
| Filter: `contains` | `{{ text \| contains('sub') }}` | Substring/membership check |
|
||||
| Filter: `map` | `{{ list \| map('attr') }}` | Extract attribute from each item |
|
||||
| Filter: `from_json` | `{{ steps.emit.output.stdout \| from_json }}` | Parse a JSON string into a typed value (raises on invalid JSON) |
|
||||
|
||||
**Single expressions** (`{{ expr }}` only) return typed values. **Mixed templates** (`"text {{ expr }} more"`) return interpolated strings.
|
||||
|
||||
### Namespace
|
||||
|
||||
The expression evaluator builds a namespace from the `StepContext`:
|
||||
|
||||
| Key | Source | Available when |
|
||||
|-----|--------|----------------|
|
||||
| `inputs` | Resolved workflow inputs | Always |
|
||||
| `steps` | Accumulated step results | After first step |
|
||||
| `item` | Current iteration item | Inside fan-out |
|
||||
| `fan_in` | Aggregated results | Inside fan-in |
|
||||
|
||||
## Input Resolution
|
||||
|
||||
When a workflow is executed, `_resolve_inputs()` validates and coerces provided values against the `inputs:` schema:
|
||||
|
||||
| Declared Type | Coercion | Example |
|
||||
|---------------|----------|---------|
|
||||
| `string` | None (pass-through) | `"my-feature"` |
|
||||
| `number` | `float()` → `int()` if whole | `"42"` → `42` |
|
||||
| `boolean` | `"true"/"1"/"yes"` → `True` | `"false"` → `False` |
|
||||
| `enum` | Validates against allowed values | `["full", "backend-only"]` |
|
||||
|
||||
Missing required inputs raise `ValueError`. Inputs with `default` values use the default when not provided.
|
||||
|
||||
## Catalog System
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["specify workflow search"] --> B["WorkflowCatalog.get_active_catalogs()"]
|
||||
B --> C{SPECKIT_WORKFLOW_CATALOG_URL set?}
|
||||
C -- Yes --> D["Single custom catalog"]
|
||||
C -- No --> E{.specify/workflow-catalogs.yml exists?}
|
||||
E -- Yes --> F["Project-level catalog stack"]
|
||||
E -- No --> G{"~/.specify/workflow-catalogs.yml exists?"}
|
||||
G -- Yes --> H["User-level catalog stack"]
|
||||
G -- No --> I["Built-in defaults"]
|
||||
I --> J["default (install allowed)"]
|
||||
I --> K["community (discovery only)"]
|
||||
|
||||
style D fill:#ff9800,color:#fff
|
||||
style F fill:#2196f3,color:#fff
|
||||
style H fill:#2196f3,color:#fff
|
||||
style J fill:#4caf50,color:#fff
|
||||
style K fill:#9e9e9e,color:#fff
|
||||
```
|
||||
|
||||
Catalogs are fetched with a 1-hour cache (per-URL, SHA256-hashed cache files in `.specify/workflows/.cache/`). Each catalog entry has a `priority` (for merge ordering) and `install_allowed` flag.
|
||||
|
||||
When `specify workflow add <id>` installs from catalog, it downloads the workflow YAML from the catalog entry's `url` field into `.specify/workflows/<id>/workflow.yml`.
|
||||
|
||||
## State and Configuration Locations
|
||||
|
||||
| Component | Location | Format | Purpose |
|
||||
|-----------|----------|--------|---------|
|
||||
| Workflow definitions | `.specify/workflows/{id}/workflow.yml` | YAML | Installed workflow definitions |
|
||||
| Workflow registry | `.specify/workflows/workflow-registry.json` | JSON | Installed workflows metadata |
|
||||
| Run state | `.specify/workflows/runs/{run_id}/state.json` | JSON | Persisted execution state |
|
||||
| Run inputs | `.specify/workflows/runs/{run_id}/inputs.json` | JSON | Resolved input values |
|
||||
| Run log | `.specify/workflows/runs/{run_id}/log.jsonl` | JSONL | Append-only event log |
|
||||
| Catalog cache | `.specify/workflows/.cache/*.json` | JSON | Cached catalog entries (1hr TTL) |
|
||||
| Project catalogs | `.specify/workflow-catalogs.yml` | YAML | Project-level catalog sources |
|
||||
| User catalogs | `~/.specify/workflow-catalogs.yml` | YAML | User-level catalog sources |
|
||||
|
||||
## Module Structure
|
||||
|
||||
```
|
||||
src/specify_cli/
|
||||
├── workflows/
|
||||
│ ├── __init__.py # STEP_REGISTRY + _register_builtin_steps()
|
||||
│ ├── base.py # StepBase, StepContext, StepResult, StepStatus, RunStatus
|
||||
│ ├── catalog.py # WorkflowCatalog, WorkflowCatalogEntry, WorkflowRegistry
|
||||
│ ├── engine.py # WorkflowDefinition, WorkflowEngine, RunState, validate_workflow()
|
||||
│ ├── expressions.py # evaluate_expression(), evaluate_condition(), filters
|
||||
│ └── steps/
|
||||
│ ├── command/ # Dispatch command to AI integration
|
||||
│ ├── shell/ # Run shell command
|
||||
│ ├── init/ # Bootstrap a project (specify init)
|
||||
│ ├── gate/ # Human review checkpoint
|
||||
│ ├── if_then/ # Conditional branching
|
||||
│ ├── prompt/ # Arbitrary inline prompts
|
||||
│ ├── switch/ # Multi-branch dispatch
|
||||
│ ├── while_loop/ # While loop
|
||||
│ ├── do_while/ # Do-while loop
|
||||
│ ├── fan_out/ # Sequential per-item dispatch
|
||||
│ └── fan_in/ # Result aggregation
|
||||
└── __init__.py # CLI commands: specify workflow run/resume/status/
|
||||
# list/add/remove/search/info,
|
||||
# specify workflow catalog list/add/remove
|
||||
```
|
||||
@@ -0,0 +1,297 @@
|
||||
# Workflow Publishing Guide
|
||||
|
||||
This guide explains how to publish your workflow to the Spec Kit workflow catalog, making it discoverable by `specify workflow search`.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Prerequisites](#prerequisites)
|
||||
2. [Prepare Your Workflow](#prepare-your-workflow)
|
||||
3. [Submit to Catalog](#submit-to-catalog)
|
||||
4. [Verification Process](#verification-process)
|
||||
5. [Release Workflow](#release-workflow)
|
||||
6. [Best Practices](#best-practices)
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before publishing a workflow, ensure you have:
|
||||
|
||||
1. **Valid Workflow**: A working `workflow.yml` that passes `specify workflow run` validation
|
||||
2. **Git Repository**: Workflow hosted on GitHub (or other public git hosting)
|
||||
3. **Documentation**: README.md with description, inputs, and step graph
|
||||
4. **License**: Open source license file (MIT, Apache 2.0, etc.)
|
||||
5. **Versioning**: Semantic versioning in the `workflow.version` field
|
||||
6. **Testing**: Workflow tested on real projects
|
||||
|
||||
---
|
||||
|
||||
## Prepare Your Workflow
|
||||
|
||||
### 1. Workflow Structure
|
||||
|
||||
Host your workflow in a repository with this structure:
|
||||
|
||||
```text
|
||||
your-workflow/
|
||||
├── workflow.yml # Required: Workflow definition
|
||||
├── README.md # Required: Documentation
|
||||
├── LICENSE # Required: License file
|
||||
└── CHANGELOG.md # Recommended: Version history
|
||||
```
|
||||
|
||||
### 2. workflow.yml Validation
|
||||
|
||||
Verify your definition is valid:
|
||||
|
||||
```yaml
|
||||
schema_version: "1.0"
|
||||
|
||||
workflow:
|
||||
id: "your-workflow" # Unique lowercase-hyphenated ID
|
||||
name: "Your Workflow Name" # Human-readable name
|
||||
version: "1.0.0" # Semantic version
|
||||
author: "Your Name or Organization"
|
||||
description: "Brief description (one sentence)"
|
||||
integration: claude # Default integration (optional)
|
||||
model: "claude-sonnet-4-20250514" # Default model (optional)
|
||||
|
||||
requires:
|
||||
speckit_version: ">=0.6.1"
|
||||
integrations:
|
||||
any: ["claude", "gemini"] # At least one required
|
||||
|
||||
inputs:
|
||||
spec:
|
||||
type: string
|
||||
required: true
|
||||
prompt: "Describe what you want to build"
|
||||
scope:
|
||||
type: string
|
||||
default: "full"
|
||||
enum: ["full", "backend-only", "frontend-only"]
|
||||
|
||||
steps:
|
||||
- id: specify
|
||||
command: speckit.specify
|
||||
input:
|
||||
args: "{{ inputs.spec }}"
|
||||
|
||||
- id: review
|
||||
type: gate
|
||||
message: "Review the output."
|
||||
options: [approve, reject]
|
||||
on_reject: abort
|
||||
```
|
||||
|
||||
**Validation Checklist**:
|
||||
|
||||
- ✅ `id` is lowercase alphanumeric with hyphens (single-character IDs are allowed)
|
||||
- ✅ `version` follows semantic versioning (X.Y.Z)
|
||||
- ✅ `description` is concise
|
||||
- ✅ All step IDs are unique
|
||||
- ✅ Step types are valid: `command`, `prompt`, `shell`, `gate`, `if`, `switch`, `while`, `do-while`, `fan-out`, `fan-in`
|
||||
- ✅ Required fields present per step type (e.g., `condition` for `if`, `expression` for `switch`)
|
||||
- ✅ Input types are valid: `string`, `number`, `boolean`
|
||||
- ✅ Step IDs do not contain `:` (reserved for engine-generated nested IDs like `parentId:childId`)
|
||||
|
||||
### 3. Test Locally
|
||||
|
||||
```bash
|
||||
# Run with required inputs
|
||||
specify workflow run ./workflow.yml --input spec="Build a user authentication system with OAuth support"
|
||||
|
||||
# Check validation
|
||||
specify workflow info ./workflow.yml
|
||||
|
||||
# Resume after a gate pause
|
||||
specify workflow resume <run_id>
|
||||
|
||||
# Check run status
|
||||
specify workflow status <run_id>
|
||||
```
|
||||
|
||||
### 4. Create GitHub Release
|
||||
|
||||
Create a GitHub release for your workflow version:
|
||||
|
||||
```bash
|
||||
git tag v1.0.0
|
||||
git push origin v1.0.0
|
||||
```
|
||||
|
||||
The raw YAML URL will be:
|
||||
|
||||
```text
|
||||
https://raw.githubusercontent.com/your-org/spec-kit-workflow-your-workflow/v1.0.0/workflow.yml
|
||||
```
|
||||
|
||||
### 5. Test Installation from URL
|
||||
|
||||
```bash
|
||||
specify workflow add your-workflow
|
||||
# (once published to catalog)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Submit to Catalog
|
||||
|
||||
### Understanding the Catalogs
|
||||
|
||||
Spec Kit uses a dual-catalog system:
|
||||
|
||||
- **`catalog.json`** — Official, verified workflows (install allowed by default)
|
||||
- **`catalog.community.json`** — Community-contributed workflows (discovery only by default)
|
||||
|
||||
All community workflows should be submitted to `catalog.community.json`.
|
||||
|
||||
### 1. Fork the spec-kit Repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/YOUR-USERNAME/spec-kit.git
|
||||
cd spec-kit
|
||||
```
|
||||
|
||||
### 2. Add Workflow to Community Catalog
|
||||
|
||||
Edit `workflows/catalog.community.json` and add your workflow.
|
||||
|
||||
> **⚠️ Entries must be sorted alphabetically by workflow ID.** Insert your workflow in the correct position within the `"workflows"` object.
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"updated_at": "2026-04-10T00:00:00Z",
|
||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/workflows/catalog.community.json",
|
||||
"workflows": {
|
||||
"your-workflow": {
|
||||
"id": "your-workflow",
|
||||
"name": "Your Workflow Name",
|
||||
"description": "Brief description of what your workflow automates",
|
||||
"author": "Your Name",
|
||||
"version": "1.0.0",
|
||||
"url": "https://raw.githubusercontent.com/your-org/spec-kit-workflow-your-workflow/v1.0.0/workflow.yml",
|
||||
"repository": "https://github.com/your-org/spec-kit-workflow-your-workflow",
|
||||
"license": "MIT",
|
||||
"requires": {
|
||||
"speckit_version": ">=0.15.0"
|
||||
},
|
||||
"tags": [
|
||||
"category",
|
||||
"automation"
|
||||
],
|
||||
"created_at": "2026-04-10T00:00:00Z",
|
||||
"updated_at": "2026-04-10T00:00:00Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Submit Pull Request
|
||||
|
||||
```bash
|
||||
git checkout -b add-your-workflow
|
||||
git add workflows/catalog.community.json
|
||||
git commit -m "Add your-workflow to community catalog
|
||||
|
||||
- Workflow ID: your-workflow
|
||||
- Version: 1.0.0
|
||||
- Author: Your Name
|
||||
- Description: Brief description
|
||||
"
|
||||
git push origin add-your-workflow
|
||||
```
|
||||
|
||||
**Pull Request Checklist**:
|
||||
|
||||
```markdown
|
||||
## Workflow Submission
|
||||
|
||||
**Workflow Name**: Your Workflow Name
|
||||
**Workflow ID**: your-workflow
|
||||
**Version**: 1.0.0
|
||||
**Repository**: https://github.com/your-org/spec-kit-workflow-your-workflow
|
||||
|
||||
### Checklist
|
||||
- [ ] Valid workflow.yml (passes `specify workflow info`)
|
||||
- [ ] README.md with description, inputs, and step graph
|
||||
- [ ] LICENSE file included
|
||||
- [ ] GitHub release created with raw YAML URL
|
||||
- [ ] Workflow tested end-to-end with `specify workflow run`
|
||||
- [ ] All gate steps have clear review messages
|
||||
- [ ] Input prompts are descriptive
|
||||
- [ ] Added to workflows/catalog.community.json (alphabetical order)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification Process
|
||||
|
||||
After submission, maintainers will review:
|
||||
|
||||
1. **Definition validation** — valid `workflow.yml`, correct schema
|
||||
2. **Step correctness** — all step types used correctly, no dangling references
|
||||
3. **Input design** — clear prompts, sensible defaults and enums
|
||||
4. **Security** — no malicious shell commands, safe operations
|
||||
5. **Documentation** — clear README explaining what the workflow does and when to use it
|
||||
|
||||
Once verified, the workflow appears in `specify workflow search`.
|
||||
|
||||
---
|
||||
|
||||
## Release Workflow
|
||||
|
||||
When releasing a new version:
|
||||
|
||||
1. Update `version` in `workflow.yml`
|
||||
2. Update CHANGELOG.md
|
||||
3. Tag and push: `git tag v1.1.0 && git push origin v1.1.0`
|
||||
4. Submit PR to update `version` and `url` in `workflows/catalog.community.json`
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Step Design
|
||||
|
||||
- **Use gates at decision points** — place `gate` steps after each major output so users can review before proceeding
|
||||
- **Keep steps focused** — each step should do one thing; prefer more steps over complex single steps
|
||||
- **Provide clear gate messages** — explain what to review and what approve/reject means
|
||||
|
||||
### Inputs
|
||||
|
||||
- **Use descriptive prompts** — the `prompt` field is shown to users when running the workflow
|
||||
- **Set sensible defaults** — optional inputs should have defaults that work for the common case
|
||||
- **Constrain with enums** — when there's a fixed set of valid values, use `enum` for validation
|
||||
- **Type appropriately** — use `number` for counts, `boolean` for flags, `string` for names
|
||||
|
||||
### Shell Steps
|
||||
|
||||
- **Shell runs with the user's privileges** — a `shell` step executes a local command directly; there is no capability sandbox. `requires` is an advisory pre-condition block (recognised keys: `speckit_version`, `integrations`), **not** a runtime permission gate — there is no `requires.permissions`. Gate sensitive commands explicitly with a `gate` step.
|
||||
- **Avoid destructive commands** — don't delete files or directories without explicit confirmation via a gate
|
||||
- **Quote variables** — use proper quoting in shell commands to handle spaces
|
||||
- **Check exit codes** — shell step failures stop the workflow; make sure commands are robust
|
||||
|
||||
#### Security: shell steps execute arbitrary code
|
||||
|
||||
Workflow `shell` steps execute their `run` field through `/bin/sh` (POSIX) or the platform shell. There is no sandbox between the step and the user's machine: a malicious or buggy `run` block can read environment variables, modify files outside the project, exfiltrate data, or escalate privileges.
|
||||
|
||||
Catalog-listed workflows are reviewed at submission time (see [Verification Process](#verification-process)), but you should still treat every install as code-execution from an untrusted source until you have read the `workflow.yml`:
|
||||
|
||||
- **Before installing a workflow**, fetch the raw YAML and audit every `shell` step's `run` field directly. `specify workflow info <name>` only shows metadata (name, version, inputs, step IDs/types) — not the shell content that would actually execute.
|
||||
- **Prefer explicit commands over interpolation** in `run` blocks: `{{ inputs.something }}` substitutions should be quoted and constrained via `enum` so a malicious input can't inject shell syntax.
|
||||
- **Limit privilege**: shell steps inherit the user's environment. Workflows that need elevated access (sudo, secrets, GitHub tokens) should call them out explicitly in the README so reviewers can spot the requirement.
|
||||
- **Authors**: if your workflow has shell steps that look risky out of context (deletions, network calls, credential reads), document the rationale in your README. Maintainers will reject submissions whose shell steps can't be justified at review time.
|
||||
|
||||
### Integration Flexibility
|
||||
|
||||
- **Set `integration` at workflow level** — use the `workflow.integration` field as the default
|
||||
- **Allow per-step overrides** — let individual steps specify a different integration if needed
|
||||
- **Document required integrations** — list which integrations must be installed in `requires.integrations`
|
||||
|
||||
### Expression References
|
||||
|
||||
- **Only reference prior steps** — expressions like `{{ steps.plan.output.file }}` only work if `plan` ran before the current step
|
||||
- **Use `default` filter** — `{{ val | default('fallback') }}` prevents failures from missing values
|
||||
- **Keep expressions simple** — complex logic should be in shell steps, not expressions
|
||||
@@ -0,0 +1,461 @@
|
||||
# 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 11 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"
|
||||
```
|
||||
|
||||
### Init Steps
|
||||
|
||||
Bootstrap a project the same way `specify init` does — scaffolding
|
||||
templates, scripts, shared infrastructure, and the selected coding agent
|
||||
integration. Runs non-interactively (defaults to `--ignore-agent-tools`)
|
||||
and resolves the integration from the step config or the workflow default:
|
||||
|
||||
```yaml
|
||||
- id: bootstrap
|
||||
type: init
|
||||
here: true # or: project: my-project
|
||||
integration: copilot # Optional: defaults to workflow integration
|
||||
integration_options: "--skills" # Optional: extra options for the integration
|
||||
script: sh # Optional: sh or ps
|
||||
force: true # Optional: required when target directory already exists
|
||||
preset: healthcare-compliance # Optional preset ID
|
||||
```
|
||||
|
||||
### 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 returns `StepResult(status=StepStatus.FAILED, ...)`
|
||||
at runtime halts the entire run — most commonly a `shell` or
|
||||
`command` step exiting non-zero. 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 a downstream
|
||||
`if` or `switch` can branch on it (or a `gate` can surface it to
|
||||
the operator via `{{ }}` interpolation in `message`):
|
||||
|
||||
```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 }}). Approve to run the recovery path, or reject to leave the failure recorded and move on."
|
||||
on_reject: skip
|
||||
- id: recover
|
||||
type: if
|
||||
condition: "{{ steps.review.output.choice == 'approve' }}"
|
||||
then:
|
||||
- id: rerun
|
||||
command: speckit.recovery
|
||||
else:
|
||||
- id: next-thing
|
||||
command: speckit.next-thing
|
||||
```
|
||||
|
||||
A few things worth knowing about that example:
|
||||
|
||||
- Both gate options (`approve`, `reject`) return `StepStatus.COMPLETED`;
|
||||
`on_reject: skip` controls only whether the engine aborts on reject
|
||||
(it doesn't, with `skip`) — it does **not** auto-skip subsequent
|
||||
sibling steps in the `then:` list. Downstream branching is the
|
||||
workflow author's responsibility: read
|
||||
`{{ steps.<gate-id>.output.choice }}` in a follow-up `if`, `switch`,
|
||||
or expression, as the `recover` step above does.
|
||||
- `on_reject` has three values: `abort` (default — reject → `StepStatus.FAILED`
|
||||
with `output.aborted = True`, halts the run), `skip` (reject →
|
||||
`StepStatus.COMPLETED`, author handles branching as shown), and `retry`
|
||||
(reject → `StepStatus.PAUSED` so the next `specify workflow resume` re-runs
|
||||
the gate).
|
||||
- Gates do not automatically re-run the failed step. To express a
|
||||
retry path, either define custom gate options and branch on the
|
||||
choice downstream, or wrap the failing step in your own loop.
|
||||
|
||||
**Notes:**
|
||||
|
||||
- The field must be a literal boolean (`true` / `false`); coerced
|
||||
strings like `"true"` are rejected at validation time.
|
||||
- **Scope: returned failures only.** The flag applies to step results
|
||||
with `status=StepStatus.FAILED`. Unhandled exceptions raised out of a step's
|
||||
`execute()` method are caught one level up by `WorkflowEngine.execute()`,
|
||||
logged as `workflow_failed`, and abort the run regardless of
|
||||
`continue_on_error`. If a step author wants the flag to cover an
|
||||
exceptional path, the step must catch the exception internally and
|
||||
return `StepResult(status=StepStatus.FAILED, ...)` with the failure encoded in
|
||||
`output` (e.g. `exit_code`, `stderr`, or a custom field).
|
||||
- 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.
|
||||
- Structural validation runs up-front: `specify workflow run` rejects
|
||||
invalid workflow definitions before the run is created, so
|
||||
validation failures never reach this code path.
|
||||
- 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`, `from_json`.
|
||||
|
||||
### Runtime Context
|
||||
|
||||
`{{ context.* }}` exposes engine-managed runtime metadata for the
|
||||
current run:
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `context.run_id` | The current workflow run id (the same value Spec Kit prints as `Run ID:` at the end of `workflow run`). Auto-generated runs are 8-character hex from `uuid4`; operator-supplied ids may be any alphanumeric string with hyphens or underscores. Empty string outside a run context. |
|
||||
|
||||
```yaml
|
||||
# Stamp telemetry events with the run id for cross-system join.
|
||||
- id: emit-event
|
||||
type: shell
|
||||
run: 'echo "{\"run_id\":\"{{ context.run_id }}\",\"event\":\"started\"}" >> events.jsonl'
|
||||
|
||||
# Per-run scratch directory.
|
||||
- id: prep-scratch
|
||||
type: shell
|
||||
run: 'mkdir -p /tmp/run-{{ context.run_id }}'
|
||||
|
||||
# Pass run id into a command for artifact metadata.
|
||||
- id: tag-artifact
|
||||
command: speckit.specify
|
||||
input:
|
||||
args: "{{ context.run_id }}"
|
||||
```
|
||||
|
||||
## 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
|
||||
```
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"updated_at": "2026-04-10T00:00:00Z",
|
||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/workflows/catalog.community.json",
|
||||
"workflows": {}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"updated_at": "2026-04-13T00:00:00Z",
|
||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/workflows/catalog.json",
|
||||
"workflows": {
|
||||
"speckit": {
|
||||
"id": "speckit",
|
||||
"name": "Full SDD Cycle",
|
||||
"description": "Runs specify \u2192 plan \u2192 tasks \u2192 implement with review gates",
|
||||
"author": "GitHub",
|
||||
"version": "1.0.0",
|
||||
"url": "https://raw.githubusercontent.com/github/spec-kit/main/workflows/speckit/workflow.yml",
|
||||
"tags": ["sdd", "full-cycle"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
schema_version: "1.0"
|
||||
workflow:
|
||||
id: "speckit"
|
||||
name: "Full SDD Cycle"
|
||||
version: "1.0.0"
|
||||
author: "GitHub"
|
||||
description: "Runs specify → plan → tasks → implement with review gates"
|
||||
|
||||
requires:
|
||||
# 0.8.5 is the first release with engine-side resolution of the
|
||||
# ``integration: "auto"`` default. Older versions would treat "auto"
|
||||
# as a literal integration key and fail at dispatch.
|
||||
speckit_version: ">=0.8.5"
|
||||
integrations:
|
||||
# The four commands below (specify, plan, tasks, implement) are core
|
||||
# spec-kit commands provided by every integration. The list here is an
|
||||
# advisory, non-exhaustive compatibility hint following the documented
|
||||
# ``any: [...]`` schema -- it is NOT a closed set. The workflow runs
|
||||
# against any integration the project was initialized with, including
|
||||
# ones not listed below, as long as that integration provides the four
|
||||
# core commands referenced in ``steps``.
|
||||
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)"
|
||||
scope:
|
||||
type: string
|
||||
default: "full"
|
||||
enum: ["full", "backend-only", "frontend-only"]
|
||||
|
||||
steps:
|
||||
- id: specify
|
||||
command: speckit.specify
|
||||
integration: "{{ inputs.integration }}"
|
||||
input:
|
||||
args: "{{ inputs.spec }}"
|
||||
|
||||
- id: review-spec
|
||||
type: gate
|
||||
message: "Review the generated spec before planning."
|
||||
options: [approve, reject]
|
||||
on_reject: abort
|
||||
|
||||
- id: plan
|
||||
command: speckit.plan
|
||||
integration: "{{ inputs.integration }}"
|
||||
input:
|
||||
args: "{{ inputs.spec }}"
|
||||
|
||||
- id: review-plan
|
||||
type: gate
|
||||
message: "Review the plan before generating tasks."
|
||||
options: [approve, reject]
|
||||
on_reject: abort
|
||||
|
||||
- id: tasks
|
||||
command: speckit.tasks
|
||||
integration: "{{ inputs.integration }}"
|
||||
input:
|
||||
args: "{{ inputs.spec }}"
|
||||
|
||||
- id: implement
|
||||
command: speckit.implement
|
||||
integration: "{{ inputs.integration }}"
|
||||
input:
|
||||
args: "{{ inputs.spec }}"
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"updated_at": "2026-04-28T00:00:00Z",
|
||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/workflows/step-catalog.community.json",
|
||||
"steps": {}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"updated_at": "2026-04-28T00:00:00Z",
|
||||
"catalog_url": "https://raw.githubusercontent.com/github/spec-kit/main/workflows/step-catalog.json",
|
||||
"steps": {}
|
||||
}
|
||||
Reference in New Issue
Block a user