coverage-loop
A configuration of loop-controller. That skill supplies the loop
machinery — primitive selection, the full guardrail stack, state
externalization. This skill supplies the two things specific to "grow the suite
to a coverage number": a mechanical proof (the coverage report meets the
configured target with a green suite) and the anti-gaming discipline that
keeps the number honest. Read loop-controller for the guardrails; they're
inherited, not repeated here.
Why disable-model-invocation: this loop writes test files and commits on
its own and spends tokens re-running the full coverage suite each round, until
the target is hit. You want to type /coverage-loop (or have the
orchestrator dispatch it) — not have Claude start an autonomous test-writing
loop because some file looked under-tested.
The 5-part contract
| Part |
This loop |
| trigger |
coverage below the configured target, a build wave needing more tests, or an explicit /coverage-loop (optionally scoped to a path or per-target) |
| action |
ONE iteration: run coverage → find the lowest-covered meaningful unit → add real, behavior-checking tests for it → re-run coverage over the whole report (and the full suite) |
| proof |
the coverage report shows total AND every configured per-target threshold >= the target AND the full suite exits 0 — default-FAIL: assume below target until the freshly-generated report proves otherwise, read from the named artifact (coverage.xml / lcov.info / the N% covered summary line), not from memory of a prior run |
| memory |
coverage_plan.md (the live TODO of under-covered units + a per-round unit → tests added → new % log), plus a git checkpoint per coverage-raising round |
| stop |
report meets target with the suite green OR iteration cap OR no-progress for 3 rounds OR budget cap |
The proof: a fresh coverage report at-or-above target, default-FAIL
"Done" is not "I added some tests" and not "the file I was looking at is
covered now." It is a freshly-generated coverage report whose total (and every
configured per-target) percentage is at or above the target, with the full suite
green in the same run. Assume below target until that report exists —
that's the default-FAIL stance. A loop that trusts a stale percentage, or that
raises one file while the global number slips, has not met the proof.
Read the target from .claude/profile.yaml (e.g. coverage.target,
coverage.per_target), falling back to the project's own coverage config
(.coveragerc, jest.config coverageThreshold, pytest --cov-fail-under,
tarpaulin.toml, …). Never hard-code 80 or 100 — the target is the
project's, not this skill's. The per-stack coverage commands, how to read each
report format, and the anti-gaming rules are in
references/coverage.md.
Step 1 — Resolve the target, the command, and the report artifact
Before looping, pin three things for this project and record them in
coverage_plan.md so every iteration re-measures identically:
- The target — from
.claude/profile.yaml, else the project's coverage
config. Capture both the total target and any per-target thresholds. If
neither declares a target, stop and ask — a coverage loop with no target
cannot converge; never invent one.
- The coverage command — prefer the project's own named script (
npm run coverage, make coverage, pytest --cov); stack defaults are in
references/coverage.md. Run what CI runs.
- The report artifact — the file/summary the proof reads (
coverage.xml,
lcov.info, coverage/coverage-summary.json, the terminal TOTAL … N%
line). The proof is read from this artifact, never asserted.
Step 2 — Run coverage, find the lowest-covered meaningful unit
Generate a fresh report and parse it. Pick the lowest-covered meaningful
unit — a function, branch, or module that represents real untested behavior —
not whichever file is alphabetically first and not trivial generated/boilerplate
lines. One unit per iteration (loop-controller Step 5); chasing ten files at
once destroys the signal about which tests moved the number. Log the chosen unit
and its current % in coverage_plan.md.
Step 3 — Add REAL tests for that unit
Write tests that exercise the unit and assert on its observable behavior —
inputs mapped to expected outputs, error paths, edge cases, branch conditions.
A test that calls a function purely to execute its lines without asserting on the
result is not a test; it is coverage theater (see the anti-cheat rules below).
Where the existing suite is itself red, delegate to fix-until-green to
restore green before adding more — coverage of a broken suite is meaningless, and
that loop owns the three-exit-code proof so this one doesn't re-implement it.
Step 4 — Re-measure the WHOLE report, checkpoint
Re-run coverage over the entire report — not just the unit you touched — and
re-run the full suite. A round that raises one file but drops the global total,
or greens coverage while reding a test, has made things worse; only a whole-report
re-measure catches it (loop-controller Step 5, "restart the streak"). On a
coverage-raising round (total moved up, nothing regressed, suite green),
commit a checkpoint naming the unit covered — the git trail is the loop's undo
and post-mortem. When total and every per-target threshold are at or above target
with the suite green, the loop is done; report the final coverage report as
evidence.
Guardrails specific to this loop
Inherits the full stack from loop-controller → references/safety.md. The caps
this loop sets:
- Iteration cap — default ~15–25 add-tests rounds (read from
.claude/profile.yaml if set). Hitting the cap is a stop-and-escalate, not a
license to game the number. The last percent toward a 100% target is often the
cap-hitter — surface "stuck at X% on these units" rather than faking it.
- No-progress detection — if the coverage total does not increase for
3 consecutive rounds, stop and escalate. Three rounds of flat coverage
means the remaining gap is structurally hard (dead code, untestable glue,
environment-bound paths) — a human call, surfaced with what was tried.
- Budget cap — re-running the full coverage suite every round is
materially more expensive than a single test run; watch
/cost and terminate
at the ceiling, don't just warn.
- Never game the coverage number. Forbidden, and each is a finding if you
catch it (mirrors
fix-until-green's never-cheat rule, points back to
loop-controller guardrail 6): writing assertion-free "tests" that execute
lines without checking behavior; excluding files / adding ignore pragmas
(.coveragerc omit, /* istanbul ignore */, # pragma: no cover,
coveragePathIgnorePatterns) to make the denominator shrink instead of covering
the code; lowering the configured threshold to meet it; or deleting/skipping
hard-to-cover tests. When the percentage jumps, read the diff that did it — a
jump from new excludes, not new assertions, is the cheat.
- AFK-safe within the reversible boundary. Writing test files + running
coverage is reversible and has a hard verifier — fine unattended. A test that
would touch something irreversible (a real DB, an external API) is an HITL
checkpoint — pause for the human (
loop-controller guardrail 4).
Choosing the driver primitive
Per loop-controller Step 1, the coverage % is provable from command output, so
the default is /goal:
- Default —
/goal: /goal "the coverage report shows total and every per-target threshold at or above the project's configured target, and the full suite exits 0, with no file excluded and no assertion-free test added — or stop after N rounds." The Haiku evaluator reads the coverage summary you surface
each turn; remember /goal has no native budget, so the turn cap and /cost
are the backstops.
- Stop-hook gate when you want a coverage floor to ship with the build and
block exit deterministically (an orchestrator wave gate that fails the wave if
coverage regresses below target). The gate script runs the coverage command and
parses the artifact; wire it per
loop-controller → references/safety.md
(stop_hook_active guard included).
Using it under the orchestrator
This is the QE coverage inner loop and a natural wave gate. The
orchestrator dispatches it after a build wave to raise coverage on the wave's new
code, or wires the Stop-hook as a coverage floor. Under-covered code routes back
to the owning agent by file (loop-controller's by-file routing): a thin
coverage report in src/api/ is the backend agent's gap, not a generic "add
tests." The orchestrator does not override a stuck loop — if coverage-loop
escalates after flat coverage, that's a real testability blocker, not a number to
paper over. As always, the loop informs; the qe-agent's qa-report.json
still decides the gate.
How this differs from its neighbors
- vs.
fix-until-green — that loop drives an existing suite to GREEN
(three exit codes, a binary stop). This loop grows the suite to a coverage
NUMBER. They compose: coverage-loop calls fix-until-green to keep the suite
green while it adds tests, then measures the number fix-until-green can't see.
- vs. the
qe-agent — the role agent owns the QA gate and qa-report.json
one-shot. This is the bounded iterative loop that drives coverage up to the
target before that gate is evaluated.
Reference files
references/coverage.md — per-stack coverage
commands (node/jest+vitest+c8, python/coverage.py+pytest-cov, go, rust/tarpaulin,
ruby/simplecov, java/jacoco, …), how to read each report format and per-target
threshold, and the full anti-gaming rule set (assertion-free tests, exclude/omit
config, threshold-lowering — and how to detect each from the diff).
1---2name: coverage-loop3description: Grow a test suite until a coverage target is met: run coverage, find the lowest-covered meaningful unit, add REAL behavior-checking tests for it, then re-measure the whole report — looping until total (and per-target) coverage is at or above the configured target AND the full suite is green. Reads the target from .claude/profile.yaml (no hard-coded 80 or 100). Forbids gaming the number with assertion-free tests or excluding files from the coverage config — both are findings. Use when coverage is below target, when a build wave needs more tests, or as the QE coverage inner loop under an orchestrated build. Trigger on: "raise coverage", "hit the coverage target", "add tests until coverage", "get to N percent coverage", "coverage is too low", "loop until coverage passes", "grow the test suite", "cover the untested code", "/coverage-loop". A configuration of loop-controller.4---56# coverage-loop78> **A configuration of [`loop-controller`].** That skill supplies the loop9> machinery — primitive selection, the full guardrail stack, state10> externalization. This skill supplies the two things specific to "grow the suite11> to a coverage number": a **mechanical proof** (the coverage report meets the12> configured target with a green suite) and the **anti-gaming discipline** that13> keeps the number honest. Read `loop-controller` for the guardrails; they're14> inherited, not repeated here.15>16> **Why `disable-model-invocation`:** this loop writes test files and commits on17> its own and spends tokens re-running the full coverage suite each round, until18> the target is hit. You want to *type* `/coverage-loop` (or have the19> orchestrator dispatch it) — not have Claude start an autonomous test-writing20> loop because some file looked under-tested.2122## The 5-part contract2324| Part | This loop |25|---|---|26| **trigger** | coverage below the configured target, a build wave needing more tests, or an explicit `/coverage-loop` (optionally scoped to a path or per-target) |27| **action** | ONE iteration: run coverage → find the **lowest-covered meaningful unit** → add **real, behavior-checking** tests for it → re-run coverage over the **whole** report (and the full suite) |28| **proof** | the coverage report shows total **AND** every configured per-target threshold **>=** the target **AND** the full suite exits 0 — default-FAIL: assume **below target** until the freshly-generated report proves otherwise, read from the named artifact (`coverage.xml` / `lcov.info` / the `N% covered` summary line), not from memory of a prior run |29| **memory** | `coverage_plan.md` (the live TODO of under-covered units + a per-round `unit → tests added → new %` log), plus a git checkpoint per coverage-raising round |30| **stop** | report meets target **with the suite green** **OR** iteration cap **OR** no-progress for 3 rounds **OR** budget cap |3132## The proof: a fresh coverage report at-or-above target, default-FAIL3334"Done" is **not** "I added some tests" and **not** "the file I was looking at is35covered now." It is a **freshly-generated coverage report whose total (and every36configured per-target) percentage is at or above the target, with the full suite37green in the same run**. Assume **below target** until that report exists —38that's the default-FAIL stance. A loop that trusts a stale percentage, or that39raises one file while the global number slips, has not met the proof.4041**Read the target from `.claude/profile.yaml`** (e.g. `coverage.target`,42`coverage.per_target`), falling back to the project's own coverage config43(`.coveragerc`, `jest.config` `coverageThreshold`, `pytest` `--cov-fail-under`,44`tarpaulin.toml`, …). **Never hard-code 80 or 100** — the target is the45project's, not this skill's. The per-stack coverage commands, how to read each46report format, and the anti-gaming rules are in47[`references/coverage.md`](references/coverage.md).4849## Step 1 — Resolve the target, the command, and the report artifact5051Before looping, pin three things for *this* project and record them in52`coverage_plan.md` so every iteration re-measures identically:53541. **The target** — from `.claude/profile.yaml`, else the project's coverage55 config. Capture both the total target and any per-target thresholds. **If56 neither declares a target, stop and ask** — a coverage loop with no target57 cannot converge; never invent one.582. **The coverage command** — prefer the project's own named script (`npm run59 coverage`, `make coverage`, `pytest --cov`); stack defaults are in60 [`references/coverage.md`](references/coverage.md). Run what CI runs.613. **The report artifact** — the file/summary the proof reads (`coverage.xml`,62 `lcov.info`, `coverage/coverage-summary.json`, the terminal `TOTAL … N%`63 line). The proof is read from this artifact, never asserted.6465## Step 2 — Run coverage, find the lowest-covered meaningful unit6667Generate a fresh report and parse it. Pick the **lowest-covered *meaningful*68unit** — a function, branch, or module that represents real untested behavior —69not whichever file is alphabetically first and not trivial generated/boilerplate70lines. One unit per iteration (`loop-controller` Step 5); chasing ten files at71once destroys the signal about which tests moved the number. Log the chosen unit72and its current % in `coverage_plan.md`.7374## Step 3 — Add REAL tests for that unit7576Write tests that **exercise the unit and assert on its observable behavior** —77inputs mapped to expected outputs, error paths, edge cases, branch conditions.78A test that calls a function purely to execute its lines without asserting on the79result is not a test; it is coverage theater (see the anti-cheat rules below).80Where the existing suite is itself red, **delegate to [`fix-until-green`]** to81restore green before adding more — coverage of a broken suite is meaningless, and82that loop owns the three-exit-code proof so this one doesn't re-implement it.8384## Step 4 — Re-measure the WHOLE report, checkpoint8586Re-run coverage over the **entire** report — not just the unit you touched — and87re-run the full suite. A round that raises one file but drops the global total,88or greens coverage while reding a test, has made things worse; only a whole-report89re-measure catches it (`loop-controller` Step 5, "restart the streak"). On a90coverage-raising round (total moved up, nothing regressed, suite green),91**commit a checkpoint** naming the unit covered — the git trail is the loop's undo92and post-mortem. When total and every per-target threshold are at or above target93with the suite green, the loop is done; report the final coverage report as94evidence.9596## Guardrails specific to this loop9798Inherits the full stack from `loop-controller` → `references/safety.md`. The caps99this loop sets:100101- **Iteration cap** — default ~15–25 add-tests rounds (read from102 `.claude/profile.yaml` if set). Hitting the cap is a *stop-and-escalate*, not a103 license to game the number. The last percent toward a 100% target is often the104 cap-hitter — surface "stuck at X% on these units" rather than faking it.105- **No-progress detection** — if the coverage total **does not increase** for106 **3 consecutive rounds**, stop and escalate. Three rounds of flat coverage107 means the remaining gap is structurally hard (dead code, untestable glue,108 environment-bound paths) — a human call, surfaced with what was tried.109- **Budget cap** — re-running the full coverage suite **every round** is110 materially more expensive than a single test run; watch `/cost` and terminate111 at the ceiling, don't just warn.112- **Never game the coverage number.** Forbidden, and each is a *finding* if you113 catch it (mirrors [`fix-until-green`]'s never-cheat rule, points back to114 `loop-controller` guardrail 6): writing **assertion-free "tests"** that execute115 lines without checking behavior; **excluding files / adding ignore pragmas**116 (`.coveragerc` `omit`, `/* istanbul ignore */`, `# pragma: no cover`,117 `coveragePathIgnorePatterns`) to make the denominator shrink instead of covering118 the code; **lowering the configured threshold** to meet it; or deleting/skipping119 hard-to-cover tests. When the percentage jumps, read the diff that did it — a120 jump from new excludes, not new assertions, is the cheat.121- **AFK-safe within the reversible boundary.** Writing test files + running122 coverage is reversible and has a hard verifier — fine unattended. A test that123 would touch something irreversible (a real DB, an external API) is an HITL124 checkpoint — pause for the human (`loop-controller` guardrail 4).125126## Choosing the driver primitive127128Per `loop-controller` Step 1, the coverage % is provable from command output, so129the default is **`/goal`**:130131- **Default — `/goal`:** `/goal "the coverage report shows total and every132 per-target threshold at or above the project's configured target, and the full133 suite exits 0, with no file excluded and no assertion-free test added — or stop134 after N rounds."` The Haiku evaluator reads the coverage summary you surface135 each turn; remember `/goal` has no native budget, so the turn cap and `/cost`136 are the backstops.137- **Stop-hook gate** when you want a coverage floor to ship *with* the build and138 block exit deterministically (an orchestrator wave gate that fails the wave if139 coverage regresses below target). The gate script runs the coverage command and140 parses the artifact; wire it per `loop-controller` → `references/safety.md`141 (`stop_hook_active` guard included).142143## Using it under the orchestrator144145This is the **QE coverage inner loop** and a natural **wave gate**. The146orchestrator dispatches it after a build wave to raise coverage on the wave's new147code, or wires the Stop-hook as a coverage floor. Under-covered code routes back148to the owning agent **by file** (`loop-controller`'s by-file routing): a thin149coverage report in `src/api/` is the backend agent's gap, not a generic "add150tests." The orchestrator does **not** override a stuck loop — if coverage-loop151escalates after flat coverage, that's a real testability blocker, not a number to152paper over. As always, the loop informs; the [`qe-agent`]'s `qa-report.json`153still decides the gate.154155## How this differs from its neighbors156157- **vs. [`fix-until-green`]** — that loop drives an **existing** suite to GREEN158 (three exit codes, a binary stop). This loop **grows** the suite to a coverage159 NUMBER. They compose: coverage-loop calls fix-until-green to keep the suite160 green while it adds tests, then measures the number fix-until-green can't see.161- **vs. the [`qe-agent`]** — the role agent owns the QA gate and `qa-report.json`162 one-shot. This is the bounded *iterative* loop that drives coverage up to the163 target before that gate is evaluated.164165## Reference files166167- [`references/coverage.md`](references/coverage.md) — per-stack coverage168 commands (node/jest+vitest+c8, python/coverage.py+pytest-cov, go, rust/tarpaulin,169 ruby/simplecov, java/jacoco, …), how to read each report format and per-target170 threshold, and the full anti-gaming rule set (assertion-free tests, exclude/omit171 config, threshold-lowering — and how to detect each from the diff).172173[`loop-controller`]: ../loop-controller/SKILL.md174[`fix-until-green`]: ../fix-until-green/SKILL.md175[`qe-agent`]: ../../roles/qe-agent/SKILL.md