# Agent Gauntlet

> Multi-stage agent pipeline for feature development with deterministic quality gates - a specifier writes Gherkin acceptance criteria, a coder implements to green tests, a cleaner drives every function under a per-function CRAP threshold, a hardener kills surviving mutants via mutation testing. Use when implementing a user story with verifiable quality, when asked to "run the gauntlet", or when setting up quality gates for agent-written code.

- Skill: `n0an/agent-gauntlet` (Agent Skill, multi-file: 13 files)
- Install (CLI): `npx skillmds@latest add n0an/agent-gauntlet`
- Raw SKILL.md: https://api.skillmd.com/api/skills/n0an/agent-gauntlet/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- License: MIT
- Author: n0an (https://skillmd.com/u/n0an)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/n0an/agent-gauntlet

---


# The Agent Gauntlet

Uncle Bob's multi-agent methodology, as a portable skill. Core idea: **don't stuff quality rules into prompts - wrap the agent in deterministic gates.** Agents treat written rules as vague suggestions and forget the middle of long instructions; tools don't decay. Put the agent in a loop against a checker until the numbers pass.

## The pipeline

```
story ──▶ specifier ──▶ coder ──▶ cleaner ──▶ hardener ──▶ (qa: phase 2)
           Gherkin +     tests +    CRAP gate   mutation
           QA proc       impl       passes      score clean
```

Each stage runs in a **fresh context** ("born, do the task, die") and in **its own git worktree** - both deliberate. Handoff artifacts are files, not chat: `features/<slug>.feature` (Gherkin) and `features/<slug>.qa.md` (QA procedure) are the contract, the stage's commits are the payload, and a handoff file under `.gauntlet/runs/<slug>/handoffs/` carries the report to the next stage. A fresh context per stage means no trajectory contamination from the previous stage's struggles; a worktree per stage means the developer's checkout is never touched and every stage boundary is a commit you can diff.

## Stage contracts

Run each stage as a separate subagent/session. Full stage prompts live in `stages/` next to this file (`specifier.md`, `coder.md`, `cleaner.md`, `hardener.md`, `qa.md`) - use them as the stage's system prompt, or read them and follow them directly if your harness has no subagents. Summary:

1. **specifier** - story text → `features/<slug>.feature` (3-7 Given/When/Then scenarios, user-observable only, no implementation terms) + `features/<slug>.qa.md` (numbered manual QA steps). Never writes code.
2. **coder** - feature file → unit tests + implementation until green. May not edit the feature file; if the spec is unimplementable, it stops and reports. No refactoring of surrounding code.
3. **cleaner** - changed files → tests or refactors until every function is under the CRAP threshold. Behavior frozen: structure changes only, tests stay green, assertions never weakened.
4. **hardener** - changed modules → mutation testing scoped to the diff; write/strengthen tests until zero unjustified surviving mutants. Never weakens production code; waivers require one-line justification each.
5. **qa** (phase 2, optional) - QA procedure → executable UI check with screenshot evidence, PASS/FAIL per scenario.

Pipeline rules: stages run sequentially; a failing stage stops the pipeline (no skipping); stages commit only in their own worktree, nothing is merged or pushed - the run's only output is the branch `gauntlet/<slug>`, which the human reviews.

## The handoff protocol

`scripts/gauntlet/gauntlet.sh` is the runtime (bash + git, no daemon). Full contract in `references/handoff-protocol.md`; the short version:

```bash
scripts/gauntlet/gauntlet.sh start <slug> --story story.md [--qa]   # branch gauntlet/<slug> at HEAD, story queued for the specifier
scripts/gauntlet/gauntlet.sh stage <slug> coder                     # run by the stage: worktree at the inbound commit, prints TASK/WORKTREE/PAYLOAD
scripts/gauntlet/gauntlet.sh handoff <slug> coder --gate "swift test"   # run by the stage when done
scripts/gauntlet/gauntlet.sh status <slug>                          # where the run is, what to run next
scripts/gauntlet/gauntlet.sh finish <slug> [--purge]                # after review: remove worktrees (purge: branch + trail too)
```

`handoff` is where the discipline lives. It refuses uncommitted files, commits without the byline `By <stage>.`, a HEAD that does not descend from the inbound commit, a coder that edited `features/`, a missing report, and a failing gate (run inside the worktree, output kept under `gates/`). The first valid call answers `AUDIT_REQUIRED` with the stage's checklist and exits 4; the unchanged second call queues the handoff and fast-forwards the branch. Agents declare "done" too early; the second call is where they look again. Exit codes: 0 ok, 1 refused, 2 setup, 3 `NO_TASK`, 4 `AUDIT_REQUIRED`.

Every stage prompt tells the agent to begin with `stage`, work only in the printed `WORKTREE:`, use only the printed task payload, commit with the byline, write `<worktree>/.gauntlet/report.md`, and end with `handoff`. An orchestrator only needs to loop: launch the subagent named in `NEXT:`, then `status`.

## The deterministic gates

| Gate | Tool | Stage | Threshold |
|---|---|---|---|
| Tests green | `swift test` / project test cmd | coder | 100% pass |
| CRAP per function | lizard + coverage export, joined by `crap.py` via `crap-gate.sh` | cleaner | 6 (`GAUNTLET_CRAP_THRESHOLD`) |
| Module line coverage | llvm-cov totals, SPM mode only | cleaner | floor 70% (`GAUNTLET_COV_FLOOR`, 0 disables) |
| Structure (optional) | SwiftLint `swiftlint-gauntlet.yml`, when installed | cleaner | body 100 lines, nesting 3 |
| Mutation score | muter, diff-scoped | hardener | zero unjustified survivors |

CRAP = `complexity^2 * (1 - coverage)^3 + complexity`, per function. At full coverage a function scores exactly its complexity, so a threshold of 6 means "at most six paths, all of them tested". Uncoverage is cubed: complexity 12 at 0% scores 156. That is what makes it a change-risk gate rather than two disconnected numbers - a module-level coverage floor lets an untested complex function hide behind well-covered neighbours; the per-function score does not.

Thresholds are Uncle Bob's agent-calibrated values: he held humans to CRAP 4 and gives agents 6 (perfect short-term memory handles more complexity), and is trying 8. Adjust *thresholds* for agents, keep *values*; don't impose human *disciplines* (no strict TDD choreography).

### Other stacks

Complexity is lizard in every stack (27 languages, pure Python, auto-installed into `.crap/venv`). Coverage is whatever the repo already exports; hand it to the gate:

```bash
scripts/gauntlet/crap-gate.sh --lcov coverage/lcov.info src        # c8/nyc/jest/vitest, coverage.py lcov, gcov
scripts/gauntlet/crap-gate.sh --cobertura build/coverage.xml src   # JaCoCo, .NET, coverage.py xml
scripts/gauntlet/crap-gate.sh --xcresult .crap/cov.xcresult Sources  # iOS/macOS APP project
```

Mutation stays per stack: Stryker (JS/TS/C#), mutmut (Python), PIT (JVM), cargo-mutants (Rust). Keep the shape: one script, hard exit code, run in a loop until green.

## Setup

1. Copy `scripts/` from this skill into the target project as `scripts/gauntlet/` (the gate script expects to live at `<repo-root>/scripts/gauntlet/` and finds `crap.py` next to itself). Add `.crap/` and `.gauntlet/` to `.gitignore` (gate venv and exports; run state and worktrees) and **commit `scripts/gauntlet/`**: stages work in fresh worktrees that contain only committed files, and `start` refuses to run while the gates are untracked. `crap.py` is the scorer from [crap-check](https://github.com/n0an/crap-check), vendored so the gate needs no second skill install; the upstream commit is pinned in its header. Installing crap-check alongside is optional: the cleaner uses its repair loop when present.
2. Swift/SPM: Xcode or a Linux swift toolchain. Optional: `brew install swiftlint` (structure rules) and `brew install muter-mutation-testing/formulae/muter` (hardener).
3. Gate a module: `scripts/gauntlet/crap-gate.sh <ModuleName>` runs lizard, `swift test --enable-code-coverage`, `llvm-cov export` (never `--summary-only`, it strips per-function data) and the scorer. Env: `GAUNTLET_MODULES_DIR` (default `Modules`), `GAUNTLET_CRAP_THRESHOLD` (6), `GAUNTLET_COV_FLOOR` (70).

**On an app project, SPM mode cannot run - use `--xcresult`.** `swift test` builds nothing for an app target, and nothing for a package whose dependencies are iOS-only, so there is no profdata for llvm-cov. Such a project tests through the simulator, and its coverage lives in an `.xcresult`:

```bash
xcodebuild test -workspace App.xcworkspace -scheme App \
  -destination 'platform=iOS Simulator,name=iPhone 17 Pro' \
  -enableCodeCoverage YES -resultBundlePath .crap/cov.xcresult

scripts/gauntlet/crap-gate.sh --xcresult .crap/cov.xcresult Packages/Networking/Sources
```

The src-dirs you pass double as the `xccov` path filter, which matters: `xccov` needs one invocation per file at ~0.7s, so scoping to the sources being gated turns minutes into seconds (measured: 8.5s for a 196-function package inside an 847-file bundle). Override with `GAUNTLET_XCCOV_INCLUDE`. There is no module coverage floor in this mode - an `.xcresult` carries no per-module total - so `GAUNTLET_COV_FLOOR` is ignored and per-function CRAP is the whole gate.
4. Mutation config: copy `muter.conf.template.yml` into the module as `muter.conf.yml`; inner loop uses `muter --files-to-mutate <changed files>`.

Exit codes: 0 passed, 1 something over threshold, 2 could not measure (missing sources, red tests, no coverage export). A function flagged `not in the coverage report` means the test run never loaded that file - a build problem, not a testing gap.

## Practical notes

- Keep stories small (one story, 3-7 scenarios). Cost of change is ~zero; iterate instead of plan-maxing.
- Specs are **throwaway scaffolding**, not documentation: the feature file lives for one story cycle; the code is the only source of truth. Don't build a spec↔code sync habit - it never pays off.
- Prefer running the loop in fast, host-testable modules (SPM packages, workspace packages): mutation testing rebuilds per mutant, so test-loop speed decides whether the hardener is affordable. Each worktree also pays its own cold build and `.crap/venv`; that is the price of isolation, so `finish` runs as soon as the branch is reviewed.
- A stage that went wrong is restarted with `stage <slug> <stage> --retry` (the failed attempt stays under `refs/gauntlet/<slug>/<stage>/attempt-N`); a lost orchestrator recovers with `status`.
- In Claude Code with this repo installed as a plugin, the whole pipeline is one command: `/gauntlet <story text>` (add `--qa` for phase 2). Elsewhere, any harness that can run staged sessions drives the same five `gauntlet.sh` commands by hand.

