Files
github-spec-kit/workflows
Quratulain-bilal 409ec59704 fix(workflow): support integration: auto to follow project's initialized AI (#2421)
* fix(workflow): support integration: auto to follow project's initialized AI

Closes #2406

(squashed)

* fix(workflow): combine JSONDecodeError and UnicodeDecodeError handling

Address Copilot feedback: UnicodeDecodeError can be raised by both
read_text() and json.loads(), so combining the handlers ensures both
cases produce a consistent, clear error message.

* fix(workflows): honor integration_state schema guard and modern state in 'integration: auto'

Three Copilot follow-ups on PR #2421:

1. engine.py:799 — `_load_project_integration` was bypassing the same
   schema guard `_read_integration_json` enforces. It now reads the
   schema field directly, returns None on a future schema (so the
   workflow falls back to the literal 'auto' default rather than
   guessing), and routes through `normalize_integration_state` /
   `default_integration_key` so modern installs that record
   `default_integration` / `installed_integrations` (without the
   legacy top-level `integration` field) resolve correctly.

2. test_workflows.py — added two regression cases:
   - `integration: auto` resolves a modern normalized state file
   - `integration: auto` falls back when the state file declares a
     newer `integration_state_schema` than this CLI supports

3. test_cli.py — added a CLI-level regression for the `UnicodeDecodeError`
   branch in `_read_integration_json` to match the existing
   malformed-JSON coverage.

* refactor(integration): extract shared try_read_integration_json helper

Address Copilot review on PR #2421:

Both `_read_integration_json` (CLI) and `_load_project_integration`
(workflow engine) were parsing `.specify/integration.json` independently,
duplicating the schema guard and risking drift between the two readers.

Extract the parse + schema validation into a single low-level helper
`try_read_integration_json` in `integration_state.py` that returns either
the normalized state or a structured `IntegrationReadError`. Both callers
now delegate to this helper:

- CLI keeps its loud-fail UX: each error kind ("decode", "os",
  "not_object", "schema_too_new") is translated into the existing console
  message + typer.Exit(1).
- Engine keeps its silent fallback: any error simply returns None so
  `integration: auto` falls back to the workflow's literal default.

This eliminates the divergence Copilot flagged without changing observable
behavior for either caller.

* fix(integration): distinguish missing file from non-regular path

Address Copilot review on PR #2421:

`try_read_integration_json` was collapsing two distinct cases into a
single `(None, None)` return:

1. `.specify/integration.json` truly missing — silent fallback is correct.
2. Path exists but is a directory, socket, or other non-regular file —
   this is a misconfiguration the CLI should surface loudly.

Split the check: `exists()` falsey returns `(None, None)`; existing-but-
not-a-regular-file returns `(None, IntegrationReadError(kind="os", ...))`
so the CLI's loud-fail path produces an actionable error while the
engine still treats it as a fallback to the workflow's literal default.

* docs(workflow): clarify version pin, advisory integrations list, enum exemption

- workflow.yml: fix comment that said 0.8.3 was first release with auto
  resolution; the pin is >=0.8.5 so the comment now matches the pin.
- workflow.yml: clarify that requires.integrations.any is an advisory,
  non-exhaustive compatibility hint, not a closed set.
- engine.py: clarify that the auto-sentinel exemption only skips enum
  membership; declared type is still enforced through _coerce_input.

* fix(workflow): resolve auto sentinel for provided values; report stat errors

Two Copilot findings fixed:

1. _resolve_inputs only resolved the ``integration: auto`` sentinel when it
   came from the input default. A caller explicitly providing
   ``{"integration": "auto"}`` (which the workflow prompt advertises as a
   valid value) bypassed _resolve_default and the literal "auto" leaked
   to dispatch. Provided values now go through the same resolution path
   as defaults, and the enum-membership exemption applies in both cases.
   Regression test added.

2. try_read_integration_json used Path.exists() / Path.is_file() as a
   pre-check. Both return False on some OSErrors (e.g. permission errors
   during stat), which silently treated an unreadable-but-present file
   as missing — the engine fell back without warning and the CLI failed
   to surface the loud error. The pre-check is gone: read_text() is
   attempted directly, FileNotFoundError means missing (silent fallback),
   IsADirectoryError and other OSErrors become loud IntegrationReadError.

* fix(workflow): enforce declared type for string inputs, reject bool-as-number

Two Copilot findings fixed:

1. _coerce_input previously coerced/validated only ``number`` and
   ``boolean`` types, so ``type: string`` silently accepted any Python
   value (numbers, lists, dicts). A YAML authoring mistake like
   ``type: string`` + ``default: 5`` slipped through. Strings are now
   required to actually be strings; non-strings raise ValueError, which
   surfaces as an ``invalid default`` error from validate_workflow.

2. ``type: number`` accepted ``default: true`` because ``bool`` is a
   subclass of ``int`` (``float(True) == 1.0``). Bools are now rejected
   explicitly in the number path so the YAML mistake fails fast. The
   boolean path is also tightened to reject non-bool / non-string
   values for symmetry.

Comment on the auto-sentinel enum exemption updated to reflect the
stronger guarantee. Regression tests added for both rejections.

* fix(cli): drop unused normalize_integration_state import to satisfy ruff

CI's `uvx ruff check src/` flagged this as F401: the symbol was imported
under a private alias but never referenced. Tests stay green after
removal.
2026-05-15 16:03:33 -05:00
..

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.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.

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 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

specify workflow add speckit
specify workflow run speckit --input spec="Build a user authentication system with OAuth support"

From a Local YAML File

specify workflow run ./my-workflow.yml --input spec="Build a user authentication system with OAuth support"

Multiple Inputs

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:

- 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):

- 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.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:

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/yesTrue, false/0/noFalse "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: createdrunningcompleted | 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

  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 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