Verify Coverage¶
Bucket: Engineering ·
Slash command: /zsl:verify-coverage ·
Source: skills/engineering/verify-coverage/SKILL.md
When this skill activates
Claude Code matches this skill against the trigger text below. You can also invoke it explicitly with the slash command.
Verify every PRD user story is covered by a passing, non-vacuous behavioral test — Tier A maps to an existing test; Tier B generates one, mutation-proves it, and runs it. Auto-files gaps as new sub-issues and writes a coverage receipt. Used as /tdd-parallel's step-4b subroutine in --auto mode (the common path), or invoked directly against any PRD whose slices have shipped.
/to-issues slices a PRD into issues and /tdd builds them, but the
only post-implementation completeness signal is structural: the tracker
auto-closes the parent when every child closes. "All slices shipped" is
not "all user stories covered" — those come apart exactly when slicing
drops or misframes a story.
This skill closes that loop. It uses the PRD's ## User Stories section
as an acceptance oracle (not as implementation context — /tdd
deliberately never sees it) and answers, per story, is there a passing
behavioral test that proves this is built? It does not trust
prose-vs-prose or code-search judgement — a story is covered only when an
executable, non-vacuous test says so.
It is also the termination oracle for /tdd-parallel's auto-loop:
the loop iterates fanout → integration review → verify-coverage until
this skill returns zero gaps (or a circuit breaker fires). Without it,
/tdd-parallel has no semantic completeness signal — only the
structural "all slices merged" one, which is exactly the signal that's
insufficient.
Role in the pipeline¶
This is a verification primitive, almost always chained by an
orchestrator (/tdd-parallel step 4b, in --auto mode). Direct user
invocation is supported for two cases:
- Auditing a PRD whose slices shipped via a different path (hotfix merged by hand, legacy work).
- Re-verifying after a manual edit of the integrated branch.
Three human gates that earlier versions of this skill held — story classification, HITL human-attestation, and a matrix-confirmation review — have all moved upstream:
- Classification moved to
/to-prd: every story carries one or moreAC<n>:acceptance-criteria sub-bullets written at PRD-authoring time. This skill reads them instead of judging. - HITL human-attestation is gone. Non-automatable stories
(visual/UX, "feels welcoming", real external systems) are refused at
/to-prdtime — they don't enter this pipeline. - Matrix confirmation is gone in
--automode. Auto-filed gap issues land asready-for-agentso/tdd-parallelcan fanout the remediation directly; the human review surface moves to/triage(which is where false-positive gaps get caught, no different from any other triage-able backlog item).
What remains is the load-bearing core: Tier A finds an existing passing test; Tier B generates one, mutation-proves it non-vacuous, runs it; gaps get filed and quarantined; a receipt is written.
Usage¶
/zsl:verify-coverage <parent-prd-issue> # default mode
/zsl:verify-coverage <parent-prd-issue> --auto # for orchestrator chaining
/zsl:verify-coverage # no arg → picker of tracking PRDs
Flags:
--auto— orchestrator mode. Skips the matrix-confirmation step (no user prompt before filing), files gap issues asready-for-agentdirectly (noneeds-triagehop), and emits a structured terminal block the orchestrator parses. Use this when/tdd-parallelchains the skill; direct user invocations should usually omit it.--no-file— produce the matrix and quarantined failing tests, but do not auto-file gap issues. Report-only. Still a full run — writes a gate-satisfying receipt (verification happened; you chose not to file). No effect ondeferred: deferred stories are never filed in any mode (there's nothing to fix yet), so thedeferredcount is identical with or without--no-file.--no-generate— Tier A only. Stories without an existing passing test are reported asunverifiedrather than driven through Tier B. Fast pass for a quick read; no tests are written. Writes a partial receipt; does not satisfy/tdd-parallel's auto-loop termination — unverified rows mean those stories weren't actually checked. Deferral is computed from tracker state before Tier A, so a story claimed only by open slices is classifieddeferred, notunverified, even under--no-generate(deferral takes precedence — it's a known not-yet, not an unchecked unknown).
Pre-flight¶
Refuse with a clear message if any fails:
docs/agents/issue-tracker.mdexists (run/setup-zsl-superpowersif not).docs/agents/triage-labels.mdexists.- The input resolves to a PRD: an issue with a
## User Storiessection. No argument → present a numbered picker of issues in thetrackingstate. If the resolved issue has no## User Storiessection it is not a PRD — refuse and say so. - Every user story has at least one
AC<n>:acceptance-criterion sub-bullet. Any story carrying noAC<n>:criterion is invalid — the PRD must be reworked in/to-prdor split. Refuse with the offending story numbers and a pointer to/to-prd's story format. Back-compat: a legacyobservable:sub-bullet (PRDs authored before the AC split) counts as a single acceptance criterion — accept it in place of anAC<n>:bullet; a legacyacceptance:tag, if present, is ignored. - The working tree is on the branch carrying the integrated work (the
PRD/integration branch for a
/tdd-parallelrun, or wherever the shipped slices live). Refuse if dirty (git status --porcelainnon-empty) — Tier B writes and runs tests, and a dirty tree makes the non-vacuity mutation unsafe to revert cleanly.
Use the project domain glossary so story vocabulary maps onto the
codebase's and test suite's vocabulary; respect ADRs in the touched
area. See engineering/tdd/tests.md for what a behavioural test is and
engineering/tdd/mocking.md for what disqualifies one.
Process¶
1. Build the story inventory¶
Parse the PRD's ## User Stories into a numbered list, verbatim. For
each story, capture its AC<n>: acceptance criteria (or, on a legacy
PRD, its single observable: line treated as one criterion) — Tier B
uses them as the test-generation hint. Parse ## Out of Scope: any story (or
behaviour) that ## Out of Scope excludes is marked out-of-scope now
and never verified — record the PRD line that excludes it as the
evidence.
The pre-flight already validated that every story carries at least one
AC<n>: criterion, so there is no classification step at runtime —
every in-scope story goes through Tier A then Tier B.
2. Build the story → slice map¶
Fetch the PRD's sub-issues per docs/agents/issue-tracker.md. Read the
## User stories covered section of both shipped and open slices —
the mapping /to-issues persists into each issue body. For each
shipped (closed/done) slice, invert it into story → [shipped
slices]. For each open (not-shipped) slice, invert it into
story → [open slices] separately — this is what tells deferred apart
from a real gap.
- A slice whose section says
None — enabling/infrastructure slicecontributes no story coverage; that is expected, not a gap. - Slices created before
/to-issuespersisted this section won't have it. Fall back to inferring the map from each slice's## What to build+ (for shipped slices) its merged diff, and warn that the map is inferred and lower-confidence for those slices. Apply the same inference to open slices so the shipped-vs-open split still holds. - A story claimed only by still-open slices (none shipped) is
deferred, not a gap — the work to cover it hasn't landed yet
(typically the covering slice is gated behind an open
[HITL]and/tdd-paralleldeferred it to a partial run). Record the blocking open slice ref as evidence. Deferred stories are not driven through Tier B and not filed as gaps (see steps 4–7). - A story with no claiming slice at all (neither shipped nor open) is a strong gap candidate — a real hole — but absence of a claim is not proof of absence, and presence of a claim is not proof of coverage. Every non-deferred in-scope story is still verified by test below regardless of what the map says; the map only decides Tier A search order, separates deferred from gap, and surfaces suspicious holes early.
This deferral derivation is always on — it's a function of tracker state (shipped vs open slices), not a flag. When every slice is shipped, no story is deferred and the run is identical to before.
3. Tier A — map to an existing passing test¶
Set aside the deferred stories from step 2 (claimed only by still-open
slices) and the out-of-scope stories — neither is verified this run. A
deferred story has no shipped code to test yet; driving it through Tier A
would always miss and Tier B would manufacture a spurious gap. The
remaining in-scope, not-deferred stories go through Tier A then Tier B.
For each such story, search the test suite for the behavioural
test(s) that exercise it (the story → slice map narrows where to look;
the glossary aligns naming; the story's AC<n>: criteria name the
behaviour). A story is covered only when:
- mapped test(s) covering its acceptance criteria pass when run now, and
- reading the test body confirms it exercises this story's
behaviour through a public interface — not a name coincidence, not
an implementation-detail assertion (
tests.mdrules apply).
Run the mapped tests (scope to the relevant suite/files; a full run is fine if cheap). Record the passing test's path + name as the evidence. Anything not satisfied here falls through to Tier B.
4. Tier B — generate, prove non-vacuous, run¶
Skip this step entirely under --no-generate (such stories →
unverified).
For each story Tier A did not satisfy:
- Write acceptance test(s) covering the story's acceptance
criteria through the public interface. The story's
AC<n>:lines are the contract — turn each into an assertion against the public interface (one test may cover several closely-related criteria, or one test per criterion where they're independent). Behaviour, not implementation — it must read like the story (tdd/SKILL.mdphilosophy). - Prove it is non-vacuous by mutation. A test that passes
against broken code proves nothing. Perturb the implementing code
path (force a wrong return / comment out the effect), run the
test, and confirm it goes RED. Then revert the perturbation
exactly (the pre-flight clean-tree check makes this safe). A test
that stays GREEN under perturbation is vacuous — discard it and
re-derive against the criterion it should pin (re-read the
AC<n>:line — it should pin the behaviour precisely). - Run it against the real integrated code:
- GREEN + proven non-vacuous → the criterion is exercised. A story is covered when every one of its acceptance criteria has a passing, non-vacuous test. The generated tests are durable regression artifacts — keep them.
- RED → genuine gap. Keep the failing test as the receipt; it is dispositioned in step 7.
Work one story at a time — write its test(s), prove each non-vacuous,
run them, move on; never batch-write Tier B tests across stories (same
anti-horizontal-slicing reason as tdd/SKILL.md).
5. Coverage matrix¶
Render one row per story:
| State | Meaning | Evidence |
|---|---|---|
covered |
Tier A or Tier B GREEN | test path+name |
gap |
Tier B RED, or no claiming slice at all (real hole) | failing test path / "no claiming slice" |
deferred |
claimed only by still-open slice(s); none shipped yet | the blocking open slice ref |
unverified |
--no-generate, no existing test |
— |
out-of-scope |
excluded by PRD ## Out of Scope |
the excluding PRD line |
deferred is distinct from both gap (a hole to fix now) and
out-of-scope (a permanent exclusion): it's a temporary not-yet —
the covering slice is open and will land on a later /tdd-parallel run.
Deferred stories never become gaps and are never filed.
Print aggregate counts.
Default mode (no --auto): before filing anything, confirm with
the user — this matrix is a review surface. The user can override any
row (e.g. accept a gap as out-of-scope, or reclassify).
--auto mode: skip the confirmation; proceed directly to step 6.
The human review surface moves downstream to /triage (for filed gap
issues) — if Tier B misjudged an acceptance criterion and filed a bogus gap, the
triage step is where it's caught and dropped.
6. Disposition the failing Tier B tests¶
A red test cannot land on a green suite. For each gap with a failing
test, quarantine it with the project's skip/xfail marker (infer from
the test framework — @pytest.mark.skip, it.skip, t.Skip,
xit, …), and put the gap issue reference in the skip reason:
Commit the quarantined tests via /commit (never craft commits
yourself). This makes every gap traceable from the suite itself, and
the remediation slice's acceptance criterion becomes literally
"un-skip this test and make it green." Under --no-file there is no
issue ref yet — use gap: PRD story <N> — unfiled and tell the user.
7. Auto-file the gaps¶
Skip under --no-file. For each gap (Tier B RED), publish one issue
per docs/agents/issue-tracker.md:
- Title:
Cover PRD story <N>: <short description>. Do not pre-assign an[AFK]/wave-number prefix — these are un-sliced work items, not slices. If the auto-loop later re-sliced them, that prefix would conflict. - Body (issue template below).
- Labels:
- Default mode:
needs-triage+backlog(same convention/to-issuesuses) so each enters normal triage. --automode:ready-for-agent+backlog. The acceptance criterion is fully specified (the quarantined test is the contract), the work is definitionally AFK (manual stories were refused at PRD ingestion), and the orchestrator is about to fanout these issues in its next loop iteration — going through/triagefirst would just stall the loop. Quality risk (a false-positive gap getting auto-fixed) is bounded: the remediation slice will write code to make the (potentially wrong) test green; a subsequent verify-coverage round will either declare the story covered or surface the inconsistency. The circuit breakers in/tdd-parallelcap the worst case.- Link as a sub-issue of the PRD using the same mechanism
/to-issuesuses, so the PRD stays the tracking container and does not spuriously auto-close while gaps are open:
PARENT_ID=$(gh api graphql -f query='query{repository(owner:"OWNER",name:"REPO"){issue(number:PARENT){id}}}' -q .data.repository.issue.id)
CHILD_ID=$(gh api graphql -f query='query{repository(owner:"OWNER",name:"REPO"){issue(number:CHILD){id}}}' -q .data.repository.issue.id)
gh api graphql -f query='mutation($p:ID!,$c:ID!){addSubIssue(input:{issueId:$p,subIssueId:$c}){subIssue{number}}}' -f p="$PARENT_ID" -f c="$CHILD_ID"
Linear: set parentId. GitLab / local-markdown / unsupported: the
## Parent text reference is the only link.
Parent¶
A reference to the PRD issue on the issue tracker.
What to build¶
The PRD user story this gap leaves uncovered, quoted verbatim
(including its AC<n>: acceptance criteria), plus a one-line
statement of the observed gap (Tier B test RED against
the integrated branch).
Acceptance criteria¶
- [ ] The quarantined test
<path::name>is un-skipped and passes - [ ] Behaviour is reachable through the public interface, not an implementation-detail assertion
Blocked by¶
None - can start immediately
If docs/agents/project-board.md exists, newly filed issues are
auto-added to the project by the user's existing workflow; do not
move the PRD's own card (it remains a tracking container).
8. Post the coverage receipt¶
This is the artifact /tdd-parallel's auto-loop consumes — write it
on every completed run (including --no-file and --no-generate),
after the matrix is computed and any filing/disposition is done.
Capture git rev-parse HEAD as the verified-sha — the receipt
asserts "coverage was checked against this tree." Write the receipt
per docs/agents/issue-tracker.md conventions:
- GitHub / GitLab: post a comment on the PRD issue, led by the
literal marker line
## Coverage receipt — verify-coverageso the orchestrator can find the latest one. - Local markdown: write/overwrite
.scratch/<NNN>-<feature-slug>/verify-coverage-receipt.mdand include it in the same commit as the quarantined tests.
Receipt body (stable fields the orchestrator parses):
## Coverage receipt — verify-coverage
- prd: <PRD ref>
- branch: <branch name>
- verified-sha: <full git sha>
- mode: full | partial (--no-generate)
- invocation: auto | manual
- matrix: covered=<n> gap=<n> deferred=<n> unverified=<n> out-of-scope=<n>
- deferred-stories: <story N → blocking open slice ref, … | none>
- gaps-filed: <#a, #b | none (--no-file) | none (no gaps)>
- ts: <ISO-8601 UTC>
mode: partial (a --no-generate run) is recorded honestly and will
not terminate /tdd-parallel's auto-loop — the orchestrator wants
every story actually exercised, not skipped as unverified.
invocation records whether this run was chained by an orchestrator
(auto) or invoked directly by a human (manual) — useful for audit
("when did this story start failing?").
9. Terminal report¶
Print the structured terminal block the orchestrator parses:
verify-coverage: matrix covered=<n> gap=<n> deferred=<n> unverified=<n> out-of-scope=<n>
gaps-filed: <#a, #b | none>
deferred-stories: <story N → open slice ref, … | none>
quarantined-tests-commit: <sha | none>
receipt: <comment URL or file path>
verified-sha: <full git sha>
/tdd-parallel's step 4b reads deferred=<n> here: gap=0 deferred>0
is its partial-complete signal (proceed to the [partial] PR, don't
loop), while gap>0 still drives the auto-fix loop. The two counts are
independent — a run can report both (some stories have a real gap to fix
now, others are deferred behind an open [HITL]).
Plus, for direct invocation (no --auto), a one-line next-step hint:
Filed N gap issues as sub-issues of #PRD. Run
/triageto walk them toready-for-agent, then/tdd-parallel <PRD>to clear them — the remediation slices un-skip the quarantined tests.
Under --auto, omit the hint — the orchestrator is the next step.
Do not chain — this skill ends here.
Not in scope¶
- Implementing the gaps — that's
/tdd//tdd-parallel. - Triaging or slicing the filed gap issues — that's
/triage//to-issues. (In--automode, the orchestrator fanouts them directly without a triage hop because the gap is fully specified by its quarantined test.) - Verifying anything against a PRD that has no
## User Storiessection, or whose stories lack anAC<n>:acceptance criterion — refuse in pre-flight.
Constraints¶
- Test, don't assess. No story reaches
coveredon prose or code-search judgement alone — only a passing, non-vacuous behavioural test (Tier A/B). - Every Tier B test must survive the mutation check before its result is trusted. Vacuous tests are worse than no test.
- The matrix outcome is advisory in default mode, automatic in
--auto. In default mode, ends in user confirmation. In--auto, the human review surface moves to/triage(for filed gaps);/tdd-parallel's circuit breakers (round limit, per-story retry limit, no-progress halt) cap the worst case of a misjudged acceptance criterion looping.
Bundled book rules¶
Do not hand-edit content between the BEGIN/END markers — scripts/sync_book_rules.py overwrites it from vendor/agent-rules-books/.
Rules from "Working Effectively with Legacy Code" by Michael Feathers¶
OBEY Working Effectively with Legacy Code by Michael Feathers¶
When to use¶
Use when changing code that is expensive to change safely because behavior is unclear, tests are weak or missing, dependencies are hidden, or runtime/framework setup blocks local feedback.
Primary bias to correct¶
Gain control before improving design. Understand current behavior, protect what must stay, create the smallest useful seam, break the dependency that blocks feedback, make the requested change, then leave the area more testable.
Decision rules¶
- Treat any area without trustworthy tests as legacy code; do not start with rewrite or module-wide cleanup unless that is explicitly required or clearly safer.
- Before editing, state the requested behavior change and the current behavior that must remain; characterize uncertain or suspicious behavior instead of silently fixing it.
- Follow the legacy loop: identify the change point, check existing protection, add characterization where possible, find or create a seam, break the blocking dependency, change behavior, then refactor locally.
- Prefer fast, focused tests around the slice being changed; use broader interception or integration tests only when they are the safest first observation point.
- Choose test points by tracing effects outward from the change point through values, calls, fields, outputs, collaborators, interception points, and pinch points.
- Use the smallest seam that allows substitution, observation, or interception; make clear whether the seam is for sensing, separation, or both.
- Break dependencies deliberately: expose hidden inputs, hard outputs, hard construction, globals, statics, ambient context, and framework callbacks only where they block testing or safe change.
- Keep behavior changes, structural refactorings, and cleanup separate; verify small steps and avoid checking in exploratory restructuring used only for understanding.
- When direct edits are risky, add behavior with sprout method, sprout class, wrap method, wrap class, or extract-and-override style moves, then fold the temporary structure into better design when tests support it.
- For hard-to-test methods, split construction from use, extract side effects behind collaborators, carve pure computation first, and isolate policy from runtime, persistence, UI, or framework mechanisms.
- Use dependency-breaking techniques according to the actual barrier: adapt narrow parameters, extract interfaces or implementers, parameterize constructors or methods, encapsulate globals, introduce instance delegators, override factories/calls, or use link/preprocessing seams only when ordinary object seams are impractical.
- In large code, sketch effects and group responsibilities before moving behavior; let excessive setup, impossible observation, and repeated changes point to smaller extracted responsibilities.
- During review, treat no tests around modified logic, mixed structural and behavioral edits, broad edits in poorly understood modules, hard-coded collaborators, global/static reach-through, constructor side effects, and business logic trapped in framework entry points as legacy-change risks.
- Reject changes that expand hidden dependencies, mock around untestable structure without improving it, rename or format while leaving the real dependency knots intact, or introduce large architecture before basic seams exist.
- Leave the touched area easier to understand, test, or change; do not mistake test-only seams, wrappers, subclass tricks, or build tricks for design improvement by themselves.
Trigger rules¶
- When behavior is uncertain, consumers may rely on ugly behavior, or a branch/path is hard to prove, add characterization or another explicit observation path before changing semantics.
- When tests require too much setup or a class cannot be instantiated cheaply, break the first real barrier: constructor work, hidden allocation, factory call, global state, static construction, framework object, or hard parameter.
- When time, randomness, environment, thread-local state, current user/request, files, network, process exits, database writes, messages, or control-flow logging block repeatable tests, wrap or inject that boundary.
- When a large method or class defeats local reasoning, sketch effects, find interception or pinch points, extract pure computation first, and avoid editing many branches at once.
- When changing database-heavy, UI, framework, or API-boundary code, separate policy from query/mapping/persistence, handlers/callbacks, adapters, and runtime setup; keep real-boundary integration tests where they matter.
- When a seam is magical, temporary, public-for-test, subclass-only, link/preprocessor-based, or probe/sensing-variable-based, add a cleanup obligation and remove it once safer structure exists.
- When repeated edits cluster across several places, remove duplication incrementally under tests instead of launching a broad redesign.
- When rewrite or heroic cleanup feels tempting, choose the smallest sprout, wrap, seam, characterization, or refactoring step that makes today's requested change safer.
Final checklist¶
- Untested or weakly tested area treated as legacy risk?
- Behavior delta and behavior-to-preserve stated?
- Uncertain current behavior characterized or explicitly observed?
- Tests close enough and fast enough to diagnose the change?
- Smallest useful seam chosen, with sensing vs separation clear?
- Blocking dependency reduced without expanding hidden dependencies?
- Behavior change, refactoring, and cleanup kept separate?
- Temporary seam or dependency-breaking trick has a cleanup path?
- Touched area is more understandable, testable, or changeable?