Metadata-Version: 2.4
Name: spec-runner
Version: 2.31.0
Summary: Task automation from markdown specs via Claude CLI
Author: Andrei
License-Expression: MIT
Project-URL: Homepage, https://github.com/andrei-shtanakov/spec-runner
Project-URL: Repository, https://github.com/andrei-shtanakov/spec-runner
Keywords: automation,tasks,claude,cli,specs,markdown
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: MacOS
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Build Tools
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: mcp<3,>=2.0.0
Requires-Dist: PyYAML>=6.0
Requires-Dist: structlog>=24.0
Requires-Dist: textual>=1.0
Requires-Dist: ulid-py>=1.1.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Requires-Dist: ruff>=0.1.0; extra == "dev"
Requires-Dist: mypy>=1.0; extra == "dev"
Requires-Dist: types-PyYAML; extra == "dev"
Dynamic: license-file

# spec-runner

Task automation from markdown specs via Claude CLI. Execute tasks from a structured `tasks.md` file with automatic retries, 5-role code review, Git integration, compliance verification, traceability reporting, and live TUI dashboard.

## Installation

```bash
uv add spec-runner
```

Or for development:
```bash
uv sync
```

Requirements:
- Python 3.11+
- Claude CLI (`claude` command available)
- Git (for branch management)
- `gh` CLI (optional, for GitHub Issues sync)

## Quick Start

```bash
# Install Claude Code skills (creates .claude/skills in current project)
spec-runner-init

# Execute ONE task — the next ready one — and stop
spec-runner run

# Execute specific task
spec-runner run --task=TASK-001

# Drain the queue: keep executing while tasks become ready
spec-runner run --all

# Execute with live TUI dashboard
spec-runner run --all --tui

# Create tasks interactively
spec-runner plan "add user authentication"

# Watch mode — continuously execute ready tasks
spec-runner watch
```

## Features

- **Task-based execution** — reads tasks from `spec/tasks.md` with priorities, checklists, and dependencies
- **Specification traceability** — links tasks to requirements (REQ-XXX) and design (DESIGN-XXX)
- **Automatic retries** — configurable retry policy with exponential backoff and error context forwarding
- **Code review** — multi-agent review after task completion with enriched diff context
- **Git integration** — automatic branch creation, commits, and merges
- **TUI dashboard** — live Textual-based terminal UI with progress bars and log panel
- **Cost tracking** — per-task token usage and cost breakdown
- **Watch mode** — continuously poll and execute ready tasks
- **Plugin system** — extend with custom hooks via `spec/plugins/*/plugin.yaml`
- **MCP server** — Model Context Protocol server for Claude Code integration (read + write operations)
- **GitHub Issues sync** — bidirectional sync between tasks.md and GitHub Issues
- **Interactive planning** — generate specs (requirements + design + tasks) through dialogue with Claude
- **Structured logging** — JSON/console output via structlog
- **SQLite state** — persistent execution state with WAL mode, auto-migration from legacy JSON
- **HITL review** — optional human-in-the-loop approval gate after code review
- **Parallel review** — 5 specialized review agents (quality, implementation, testing, simplification, docs) running concurrently
- **Agent personas** — role-specific prompt templates and model selection (architect, implementer, reviewer)
- **Constitution guardrails** — inviolable project rules from `spec/constitution.md` injected into every prompt
- **Telegram / webhook notifications** — alerts on task failure, run completion, and degraded-mode persistence failures (Telegram Bot API + generic webhook)
- **Degraded-mode resilience** — SQLite write failures (disk-full, DB corruption) are caught, the run continues in memory, and operators are notified once
- **Compliance audit trail** — opt-in JSON-Lines log of every task lifecycle event (started, attempt, completed/failed, state_degraded, run start/end) with operator + run-id attribution
- **Pause/resume** — pause mid-run with Ctrl+\, edit tasks, resume; TUI keybinding `p`
- **Streaming events** — live stdout streaming from Claude CLI to TUI via EventBus
- **Session/idle timeouts** — automatic stop after configurable session or idle duration

## Task File Format

Tasks are defined in `spec/tasks.md`. Task ids are `<PREFIX>-<number>` — `TASK-001` by convention, but any uppercase prefix works (`KAP-002`, `ABC-17`); the parser recognizes whatever prefixes the task headers use:

```markdown
## Milestone 1: MVP

### TASK-001: Implement user login
🔴 P0 | ⬜ TODO | Est: 2d

**Checklist:**
- [ ] Create login endpoint
- [ ] Add JWT token generation
- [ ] Write unit tests

**Traces to:** [REQ-001], [DESIGN-001]
**Depends on:** —
**Blocks:** [TASK-002], [TASK-003]
```

## CLI Commands

### spec-runner

```bash
# Execution
spec-runner run                            # Execute ONE task (the next ready one) and stop
spec-runner run --task=TASK-001            # Execute specific task
spec-runner run --all                      # Drain the queue, re-evaluating readiness after each
                                           # task (resets failed→pending by default)
spec-runner run --all --no-reset-failed    # Keep failed tasks sticky (skip the default reset)
spec-runner run --all --hitl-review        # Interactive HITL approval gate
spec-runner run --force                    # Skip lock check (stale lock)
spec-runner run --allow-dirty-spec         # Skip the dirty-spec pre-run guard
spec-runner watch --allow-dirty-spec       # Same override for watch/retry
spec-runner run --tui                      # Execute with live TUI dashboard
spec-runner run --dry-run                  # Show what would execute (JSON)
spec-runner run --json-result              # Structured JSON output (Maestro interop)
spec-runner run --budget=10.0              # Set global budget in USD
spec-runner run --log-level=DEBUG          # Set log verbosity
spec-runner run --log-json                 # Output logs as JSON

# Monitoring
spec-runner status                         # Show execution status
spec-runner status --json                  # JSON status output
spec-runner costs                          # Cost breakdown per task
spec-runner costs --json                   # JSON output for automation
spec-runner costs --sort=cost              # Sort by cost descending
spec-runner logs TASK-001                  # View task logs

# Operations
spec-runner retry TASK-001                 # Retry failed task
spec-runner reset                          # Reset state
spec-runner watch                          # Continuously execute ready tasks
spec-runner watch --tui                    # Watch with live TUI dashboard
spec-runner tui                            # Launch TUI status dashboard
spec-runner validate                       # Validate config and tasks
spec-runner sync                           # Post-merge sync: pull base, prune merged run/task branches
spec-runner sync --dry-run                 # Preview without changing anything
spec-runner review-pr <url-or-number>      # Review-bot loop: verify, fix valid, gate, push, reply
spec-runner review-pr 6 --verify-only      # Verdicts only, read-only (exit 0/1/2)
spec-runner review-pr 6 --json             # Machine-readable verdict/resolution report

# Verification & Reporting (v2.0)
spec-runner preflight                      # Read-only: what is missing before tasks can run
spec-runner preflight --json               # Machine-readable readiness report
spec-runner audit                          # Static pre-execution spec check
spec-runner audit --strict                 # Fail on warnings (orphans, uncovered)
spec-runner audit --json                   # JSON findings output (for CI)
spec-runner audit --csv                    # CSV for spreadsheet review
spec-runner verify                         # Verify post-execution compliance
spec-runner verify --task=TASK-001         # Verify specific task
spec-runner verify --json                  # JSON compliance output
spec-runner verify --strict                # Fail on warnings too
spec-runner report                         # Generate traceability matrix
spec-runner report --milestone=mvp         # Filter by milestone
spec-runner report --uncovered-only        # Show only uncovered requirements
spec-runner report --json                  # JSON matrix output

# Planning
spec-runner plan "description"             # Interactive task planning
spec-runner plan --full "description"      # Generate full spec (requirements + design + tasks)
spec-runner plan --full --from-file spec.md  # Read the description from a file instead of an arg
spec-runner plan --gated --profile lite    # Select the gated stage profile (default: lite)

# Diagnostics
spec-runner doctor                              # Probe the configured CLI/model (real mini-task)
spec-runner doctor --cli=codex --model=gpt-5.4  # Probe an ad-hoc CLI+model
spec-runner doctor --with-review                # Also probe the review stage
spec-runner doctor --json --yes                 # Machine-readable, no confirmation (CI)
spec-runner doctor --strict                     # Exit non-zero on DEGRADED too

# Integration
spec-runner mcp                            # Launch MCP server (stdio)
```

> **One task per `run`.** A bare `spec-runner run` (and `run --task`/`--milestone`)
> executes a single task and exits — a fully approved `tasks.md` with twelve ready
> tasks finishes one of them and reports `completed=1, remaining=11`. Only
> `run --all` re-evaluates readiness after each task and drains the queue;
> `watch` does the same, continuously. This is by design: a single `run` is the
> unit of work an orchestrator schedules.

### Task Management (unified in v2.0)

```bash
# Task commands (use `spec-runner task` instead of deprecated `spec-task`)
spec-runner task list                      # List all tasks
spec-runner task list --status=todo        # Filter by status
spec-runner task list --priority=p0        # Filter by priority
spec-runner task list --milestone=mvp      # Filter by milestone
spec-runner task show TASK-001             # Task details
spec-runner task start TASK-001            # Mark as in_progress
spec-runner task done TASK-001             # Mark as done
spec-runner task block TASK-001            # Mark as blocked
spec-runner task check TASK-001 2          # Mark checklist item
spec-runner task stats                     # Statistics
spec-runner task next                      # Show next ready tasks
spec-runner task graph                     # ASCII dependency graph

# GitHub Issues
spec-runner task export-gh                 # Export to GitHub Issues format
spec-runner task sync-to-gh                # Sync tasks -> GitHub Issues
spec-runner task sync-to-gh --dry-run      # Preview without making changes
spec-runner task sync-from-gh              # Sync GitHub Issues -> tasks.md
```

### spec-runner-init

```bash
spec-runner-init                           # Install skills to ./.claude/skills
spec-runner-init --force                   # Overwrite existing skills
spec-runner-init /path/to/project          # Install to specific project
```

### Multi-phase Options

`--spec-prefix` namespaces tasks, state, logs, and history for phase-based workflows:

```bash
spec-runner run --spec-prefix=phase5-          # Uses spec/phase5-tasks.md
spec-runner task list --spec-prefix=phase5-    # List phase 5 tasks
```

Phase-scoped paths: `spec/phase5-{tasks,requirements,design}.md`, `spec/.executor-phase5-state.db`, `spec/.executor-phase5-logs/`, `spec/.phase5-task-history.log`. Multiple phases coexist without state bleed.

## Spec Governance (gated generation)

An opt-in workflow for generating and approving `requirements.md` / `design.md` /
`tasks.md` one stage at a time instead of all at once, with a human checkpoint
between stages:

```bash
spec-runner plan --gated "description"     # Generate the next stage as DRAFT (auto-resolved)
spec-runner plan --gated --stage design    # ...or a specific stage (upstream must be approved)
spec-runner plan --gated --no-interactive  # Skip the TTY checkpoint menu (CI / non-TTY runs)
spec-runner spec status                    # Show each stage's status + the recommended next action
# edit the generated file if needed, then:
spec-runner spec approve requirements      # Re-validates the body, then approves (bumps version)
spec-runner plan --gated --stage design    # Repeat per stage: requirements -> design -> tasks
spec-runner spec approve tasks
spec-runner run                            # Or `spec-runner run --strict` to enforce the gate below
```

Only the **first** stage of the chain needs a description: every later stage
inherits it from its approved upstream document, which is reproduced in the
generation prompt anyway. So `plan --gated --stage design` runs bare, exactly as
shown above. On a TTY, `plan --gated` opens a checkpoint menu after each draft
(approve / edit / regenerate / stop) and auto-continues to the next stage;
`--no-interactive` (or a non-TTY stdout) generates one stage and stops.

Each managed spec file carries a small YAML frontmatter block tracking one of
three statuses:

- **draft** — generated (or edited) but not yet approved.
- **approved** — validated and signed off; bumps the file's version.
- **stale** — an upstream stage was re-approved after this one was generated,
  so this stage may be out of sync and should be regenerated or re-reviewed.

Generation and approval also record where a stage came from: `traces_to` lists
its direct upstream stage plus the requirement/design ids it actually inherits,
and `spec approve` pins `upstream_hashes` — the `git hash-object` value of the
upstream file as approved, so a later upstream edit is detectable and not merely
assumed. Both are documented in `docs/CONTRACTS.md`.

Other useful commands: `spec-runner spec reject <stage>` reopens an approved/stale
stage as draft; `spec-runner spec adopt <stage>` stamps frontmatter onto an
existing unmanaged file (validates first — a failing file is adopted as draft
unless you pass `--force`); `spec-runner spec check <stage>` refreshes the
cached validation verdict without approving.

Set `spec_governance: strict` in `spec-runner.config.yaml` (default: `off`) to
make `run`/`watch` refuse to execute a **managed** `tasks.md` that isn't
`approved`; `--strict`/`--no-strict` override this per invocation.

### Stage profiles

The stage chain is data, loaded from a bundled profile. The default `lite`
profile is `requirements → design → tasks` (the historical chain). Select a
profile with `spec_profile:` in `spec-runner.config.yaml` or the `--profile`
flag on `plan --gated` and the `spec` command family; an unknown profile name
fails with a clear error listing the available profiles. Profiles live in
`src/spec_runner/profiles/*.yaml`, each stage declaring its template, marker
prefix, validator, and upstream stages.

> **Guardrail, not an enforcement boundary.** `strict` mode only blocks tasks.md
> files that carry gated-spec frontmatter. Deleting the frontmatter (or never
> adopting it) makes the file "unmanaged," which always passes the gate for
> backward compatibility with unmanaged and Maestro-produced specs. Treat this
> as a workflow guardrail against accidental unapproved runs, not a security
> control.

## Usage as Library

```python
from spec_runner import Task, ExecutorConfig, parse_tasks, get_next_tasks
from pathlib import Path

tasks = parse_tasks(Path("spec/tasks.md"))
ready = get_next_tasks(tasks)

for task in ready:
    print(f"{task.id}: {task.name} ({task.priority})")
```

## MCP Server (Claude Code Integration)

spec-runner includes an MCP server for querying status and executing tasks from Claude Code.

Add to `.mcp.json`:

```json
{
  "mcpServers": {
    "spec-runner": {
      "command": "spec-runner",
      "args": ["mcp"]
    }
  }
}
```

Available tools:

| Tool | Kind | Effect |
|---|---|---|
| `spec_runner_status` | read | Returns aggregate status (completed/failed/running, cost, tokens) |
| `spec_runner_tasks` | read | Lists tasks with id/name/priority/status/deps |
| `spec_runner_next_tasks` | read | Lists ready-to-run tasks |
| `spec_runner_task_detail` | read | Returns per-task checklist, attempts, last review, cost |
| `spec_runner_costs` | read | Per-task cost/token breakdown |
| `spec_runner_logs` | read | Tail of a task's execution log |
| `spec_runner_run_task` | **write** | **Spawns a subprocess that runs Claude CLI** against the workspace. Can modify files, create git branches, run hooks (tests/lint/commit) |
| `spec_runner_stop` | **write** | Writes a stop-file that asks a running executor to shut down gracefully |

### Security model

**Authentication.** The MCP server has **no built-in authentication**. It uses `stdio` transport and inherits the trust boundary of whatever started it (typically your terminal or Claude Code). Whoever can run the server can call any of its tools.

**Safe deployment patterns:**

- ✅ **Local stdio only** (default). Run via `spec-runner mcp` from `.mcp.json` on a single developer machine. Same trust boundary as your shell.
- ✅ **Claude Code inside your own workspace.** The MCP server operates on the workspace it's invoked in; tools like `spec_runner_run_task` will modify files in that workspace.
- ❌ **Do NOT expose over TCP, HTTP, or a shared socket** without adding authentication and audit logging — the write tools execute subprocesses that run Claude CLI with full filesystem access.
- ❌ **Do NOT run under a shared service account** that multiple users or agents share. There is no per-caller identity, so audit logs cannot attribute actions.

**Write-tool blast radius.** `spec_runner_run_task` does not sandbox execution: the spawned `spec-runner run --task TASK-XXX` can:

- edit any file in the project root
- create git branches and auto-commit (if `hooks.post_done.auto_commit: true`)
- run tests, linters, and any configured hook command
- spend budget (Claude API cost) up to `budget_usd` / `task_budget_usd`

Treat the MCP server as equivalent to giving the caller shell access to the workspace.

**Hardening options** (if you need tighter limits):

- Run in a disposable container or Maestro worktree so writes are isolated
- Set `budget_usd` low to bound accidental cost spend — see the guarantee below
- Disable `hooks.post_done.auto_commit` if you want manual review before commits
- Restrict `commands.test`/`commands.lint` to safe allow-listed shell commands — they run verbatim

See also: `docs/state-schema.md` for the read contract, and `src/spec_runner/mcp_server.py` for tool implementations.

## Configuration

Configuration file: `spec-runner.config.yaml` (project root, v2.0)

Legacy location `spec/executor.config.yaml` is still supported with a deprecation warning.

**Pick one shape.** Flat v2.0 and the legacy `executor:` wrapper are each fine
on their own; a file that mixes them is **refused** (#182). The wrapper wins, so
mixing means every top-level setting is silently discarded — `claude_command`
among them, which sends the run to the default CLI and a paid model. The
refusal names the discarded keys, at load time and in `spec-runner validate`.

v2.0 flat format (no `executor:` wrapper):

```yaml
max_retries: 3
task_timeout_minutes: 30
claude_command: "claude"
claude_model: "sonnet"
spec_prefix: ""                # e.g. "phase5-" for phase5-tasks.md
tdd_runner: ""                 # pytest (empty = infer; only where inference cannot be wrong)
budget_usd: 50.0               # Total budget guard (whole run)
task_budget_usd: 10.0          # Per-task guard incl. first attempt
max_retry_cost_usd: 2.0        # Cap on retry cost only (attempts 2+)

# Telegram notifications (optional)
telegram_bot_token: ""         # Bot token from @BotFather
telegram_chat_id: ""           # Chat ID to send notifications to
notify_on: [run_complete, task_failed, state_degraded, pr_opened]

# Generic webhook (optional — works with Slack, Discord, ntfy.sh, etc.)
webhook_url: ""                # Webhook URL (empty = disabled)
webhook_template: '{"text": "{{event}}: {{message}}"}'

# Review-bot loop (optional — spec-runner review-pr, issue #102)
review_pr:
  allowed_bots: [Copilot, "copilot-pull-request-reviewer[bot]"]
  post_pr: off                 # off | verify | full — stage after integration_pr
  post_pr_wait_seconds: 120    # let the review bot comment first
  max_rounds: 3                # bounded rounds per PR (new head SHA = new round)
  max_comments: 20
  max_changed_lines: 300       # per-fix diff cap
  max_cost_usd: 5.0            # every paid call (verify + fix), checked before each;
                               # non-positive (0 or negative) = no limit
  max_wall_minutes: 30

# Compliance audit trail (optional — JSON Lines, opt-in)
audit_log_path: ""             # e.g. "spec/.executor-audit.jsonl"; empty = disabled
audit_log_operator: ""         # Override the auto-detected "user@host" tag

# Agent personas (optional)
personas:
  implementer:
    system_prompt: "You are a focused Python developer"
    model: "sonnet"
  reviewer:
    system_prompt: "You are a senior code reviewer"
    model: "haiku"

hooks:
  pre_start:
    create_git_branch: true
  post_done:
    run_tests: true
    run_lint: true
    auto_commit: true
    run_review: true
    review_parallel: false     # Run 5 review agents in parallel
    review_roles: [quality, implementation, testing]

harness_guard: warn          # Harness-mutation tripwire: off | warn | strict
                             # (agent editing pyproject/pytest.ini/CI files etc.)
harness_files: []            # Extra harness paths to watch
harness_allow: []            # Globs exempt from strict-mode violations

commands:
  test: "uv run pytest tests/ -v"
  lint: "uv run ruff check ."   # absent = no linter: under execution_mode: tdd the
                                # pre-freeze lint is skipped rather than guessing ruff
  sync: "mix deps.get"       # Dependency sync before each task. Empty/absent =
                             # auto: `uv sync` when pyproject.toml exists, else skip

paths:
  root: "."
  logs: "spec/.executor-logs"
```

### Budgets: what the caps actually guarantee

`budget_usd` and `task_budget_usd` are **pre-call guards, not hard caps.** The
guarantee, exactly:

> Once recorded spend has reached the limit, no new paid call is started; the
> maximum consecutive overshoot is bounded by one call.

A call's cost is known only after it returns, so no state-based check can stop
the call that crosses the line. The only true hard cap would be a
backend-enforced per-call limit, which spec-runner deliberately does not pass:
a hard mid-call cap turns a slight overage into a hard failure. Until then, one
call can exceed the remainder.

Three consequences worth knowing before you rely on a cap:

- **A task attempt can make several paid calls.** Under `execution_mode: tdd`
  it makes three — RED authoring, GREEN implementation, review — and each is
  guarded separately. A refusal before GREEN keeps the confirmed red; a
  refusal before review keeps the candidate commit and records the review as
  `not_run` (never as passed).
- **Parallel review runs one role at a time while a cap is set.** Five roles
  launched together would all pass the same check before any of them reported
  a cost, and the guarantee above would be false.
- **A CLI that reports no cost cannot be combined with a cap.** The first such
  call is recorded unpriced, and the guard then refuses the next one: the
  remaining budget cannot be proven from a figure known to be a floor.
  `spec-runner costs` shows unpriced calls and marks such totals with `≥`.

### Raising a ceiling deliberately

A cap that has done its job can leave a task one cheap step from done — the
pilot hit exactly that: the implementation finished and green, the budget spent,
and the only remaining step forbidden. `budget authorize` is the audited way
out, and it raises the limit rather than pretending the money was not spent:

```bash
spec-runner budget authorize TASK-101 --task-limit 6.00 --run-limit 6.00 \
    --reason "continuing after the instrument failures were fixed"
```

Both axes, because raising one leaves the other refusing the next call. The
reason is mandatory, the actor is recorded, it refuses while a run is live or
when invoked from inside an agent, a second authorization needs
`--after <id>` (quoted in every refusal), and it **only raises** — lowering is
not supported by this command or any flag on it.

The record keeps the previous and new limits plus the recorded spend and the
number of unpriced calls at the moment of the decision, because "$6.00
authorised" means one thing against a proven total and another against a floor.

**A budget lives in one state file.** A new state file starts a new budget
domain: no authorization and no spend carry over. Rotating the state file
mid-pilot is therefore not a neutral act — it resets the financial record while
leaving the work in place.

`review-pr` carries its own limit, `review_pr.max_cost_usd`, with the same
guarantee and the same consequences. It counts **both** kinds of call the loop
makes — one verification per collected comment and one fix per valid one — and
checks before each. Comments it stops short of keep no verdict and no
resolution, which is what the `NEEDS_HUMAN` exit (2) already reports. Because a
CLI that reports no cost would otherwise stall the loop after its first call,
a non-positive `max_cost_usd` — `0` or negative — disables the limit outright.

### Git Branch Workflow

1. **Branch detection**: Auto-detects `main` or `master`, or use `main_branch` config
2. **Task branches**: Creates `task/TASK-001-short-name` branches for each task
3. **Auto-merge**: Merges task branch to main after completion

Each task starts from a clean tree: the branch stage reverts tracked changes and
removes untracked files so one task's leftovers cannot contaminate the next
one's tests. Anything it finds is **stashed first**, labelled
`spec-runner rescue: <TASK-ID> at <time>` — recover it with `git stash list` /
`git stash pop`. If the stash cannot be taken, the task refuses to start rather
than clean. A tree that is already clean creates no stash.

### Supported CLIs

| CLI | Auto-detected | Example template |
|-----|--------------|------------------|
| Claude | Yes | `{cmd} -p {prompt} --model {model}` |
| Codex | Yes | `{cmd} exec -m {model} {prompt}` (codex's `-p` is `--profile`, not the prompt) |
| OpenCode ([sst/opencode](https://opencode.ai)) | Yes | `{cmd} run --model {model} {prompt}` |
| Pi Agent ([pi.dev](https://pi.dev)) | Yes (basename match) | `{cmd} -p --model {model} {prompt}` |
| Ollama | Yes | `{cmd} run {model} {prompt}` |
| llama-cli | Yes | `{cmd} -m {model} -p {prompt} --no-display-prompt` |
| Custom | Use template | `{cmd} --prompt {prompt}` |

> **Full pi-driven loop:** `pi` can run the entire dev → review → test cycle (with native
> skills, per-stage tool control and a read-only review gate) using only config and a small
> script — no core code. See [docs/pi-workflow.md](docs/pi-workflow.md) and the runnable
> [examples/pi-loop/](examples/pi-loop/).

### Switching CLI (claude / codex / pi / ...)

Apply a preset instead of hand-editing `spec-runner.config.yaml`:

```bash
spec-runner config --preset codex                 # everything on codex
spec-runner config --exec claude --review codex    # claude codes, codex reviews
spec-runner config --list-presets                  # claude codex opencode pi ollama llama-cli qwen copilot
spec-runner config --preset pi --apply             # update an existing config
```

Tests stay on your `test_command` (e.g. pytest); presets only set the exec and
review CLIs. Run `spec-runner doctor` afterwards to verify the profile.

#### Using Qwen

Qwen works two cheap ways, both already supported:

- **Cloud (cheap), via OpenCode:** `spec-runner config --preset opencode --model "openrouter/qwen/qwen3-coder"` (any OpenCode-supported Qwen provider/model string).
- **Local, via Ollama:** `ollama pull qwen2.5-coder:32b` then `spec-runner config --preset ollama --model "qwen2.5-coder:32b"`.

Or use the official agents directly:

- **Qwen Code CLI:** `spec-runner config --preset qwen --model qwen-coder-plus` (or set the model in `~/.qwen/settings.json`).
- **GitHub Copilot CLI:** `spec-runner config --preset copilot --model claude-haiku-4.5` (needs Copilot access; set the model via `COPILOT_MODEL`).

Run `spec-runner doctor` afterwards to confirm the chosen CLI is READY.

### Checking CLI/model compatibility

`spec-runner doctor` runs a real one-task probe through the actual execution
path and reports, per capability, whether your CLI/model works:

- **invocation** — the command runs and authenticates
- **completion_marker** — the model prints `TASK_COMPLETE` (not all models do)
- **task_action** — the model actually performs the work
- **cost_tracking** — token/cost parsing works (needed for `costs`/`--budget`)
- **error_classification** — failures are classified (diagnostic)
- **review** *(with `--with-review`)* — the reviewer prints `REVIEW_PASSED`/`FAILED`

Verdict: **READY** / **DEGRADED** (works, but something like cost tracking is
unavailable) / **BROKEN**. It makes real, billable model calls (capped by
`--budget`, default $0.50) and asks for confirmation unless `--yes`.

## Project Structure

```
project/
├── pyproject.toml
├── spec-runner.config.yaml      # v2.0 config location
├── Makefile
├── .pre-commit-config.yaml
├── src/
│   └── spec_runner/
│       ├── __init__.py
│       ├── executor.py          # Re-exports (backward compat)
│       ├── cli.py               # Main CLI dispatcher, cmd_run, cmd_watch
│       ├── cli_info.py          # Status, costs, logs, validate, verify, report, TUI, MCP
│       ├── cli_plan.py          # Interactive planning command
│       ├── execution.py         # Task execution + retry logic
│       ├── errors.py            # CLI stderr → human-readable failure reasons
│       ├── stages.py            # Per-task sub-stage tracking (StageReporter)
│       ├── config.py            # ExecutorConfig + YAML loading
│       ├── state.py             # SQLite state persistence + degraded-mode fallback
│       ├── prompt.py            # Prompt building + templates
│       ├── hooks.py             # Pre/post hook orchestration
│       ├── git_ops.py           # Git branch/commit/merge operations
│       ├── review.py            # 5-role code review + HITL gate
│       ├── runner.py            # Subprocess execution + event streaming
│       ├── task.py              # Task parsing + dependency resolution
│       ├── task_commands.py     # Task CLI commands (list, show, start, etc.)
│       ├── github_sync.py       # GitHub Issues sync (to/from)
│       ├── audit.py             # Pre-execution static audit (LABS-37)
│       ├── audit_log.py         # JSON Lines compliance audit trail (LABS-40)
│       ├── verify.py            # Post-execution compliance verification
│       ├── report.py            # Traceability matrix generation
│       ├── validate.py          # Config + task validation
│       ├── plugins.py           # Plugin discovery + hooks
│       ├── logging.py           # Structured logging (structlog back-compat shim)
│       ├── obs.py               # OTel JSONL observability emitter (shared contract)
│       ├── events.py            # EventBus for streaming to TUI
│       ├── notifications.py     # Telegram + webhook notifications
│       ├── tui.py               # Textual TUI dashboard
│       ├── mcp_server.py        # MCP server (MCPServer, stdio)
│       ├── init_cmd.py          # Skill installer
│       ├── profiles/            # Bundled gated-spec stage profiles (lite.yaml)
│       └── skills/
│           └── spec-generator-skill/
├── docs/
│   └── state-schema.md          # Maestro interop contract (SQLite + --json-result)
├── schemas/
│   ├── executor-state.schema.json   # JSON Schema for .executor-state.db contents
│   └── json-result.schema.json      # JSON Schema for `run --json-result` stdout
├── tests/
│   └── fixtures/maestro-interop/    # Golden fixtures copied by Maestro contract tests
└── spec/
    ├── tasks.md
    ├── requirements.md
    ├── design.md
    ├── FORMAT.md                # Task format specification
    └── plugins/                 # Optional: per-plugin subdirectories with plugin.yaml
```

## License

MIT
