agentleFS
greglas75greglas75/zuvo/skills/write-tests/SKILL.md

write-tests

Skill7 starsChanged 28 days agoMITView on GitHub

Write tests for existing production code. Processes ONE file at a time through a full pipeline: analyze, inventory (frozen BEFORE writing), write, executable coverage gate, verify, blind coverage audit, adversarial review, log. Uses CodeSift for discovery and analysis when available. Modes: [path] (specific target), auto (discover and loop until done), --dry-run (plan only; skips suite verification).

  • Installs packages

76 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use close-spec, design-with-images and 10 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • dzhngSkill

    close-spec

    When a spec under specs/<feature>/ is done shipping (a write-spec build, or any planned task that produced a spec), archive it to specs/done/ and rewrite it from a build-plan into a durable rationale — the why, the principles, the invariants — pointing back to the real code for the how. Use when implementation has landed and the plan no longer matches what shipped, or the user says a feature/spec is finished. Pairs with [write-spec](../write-spec/SKILL.md) (the plan this closes) and [review](../review/SKILL.md) (run before closing).

    Also in 5 projects that use this

  • dzhngSkill

    design-with-images

    Explore UI and visual design with image generation, then iterate the real implementation against the user-selected concept. Use when brainstorming visual directions, offering mockup variants, or implementing an approved generated design; do not stop at the mockups or the first code approximation.

    Also in 5 projects that use this

  • transitive-bullshitSkill

    compare-screenshots

    Compare screenshots against the intended design, distinguishing approved references from historical baselines. Use for iterative implementation matching, before/after visual review, objective image telemetry, or checking a lone capture for flat, empty, or badly framed content.

    Also in 5 projects that use this

  • LucasMatuszewskiSkill

    audit-agents

    Audit and maintain agent configuration files (.claude/agents, AGENTS.md, CLAUDE.md, agent-memory). Checks for duplicates, outdated info, naming conflicts, wrong technical facts, and files over 200 lines. Fixes all issues and commits.

    Also in 5 projects that use this

  • wolverin0Skill

    audit-decisions

    Standardize remediation decisions, baseline-policy entries, and legal-review banners for accepted, deferred, or policy-blocked audit findings.

    Also in 4 projects that use this

  • rhnfzlSkill

    explore-unknowns

    Walk the user through mapping the unknowns of a task before, during, and after they build it, and hand them a four-quadrant map (settled ground, open decisions, taste, and landmines) they keep. Reach for this eagerly whenever a request is ambiguous or underspecified, the codebase or domain is unfamiliar, the user will "know it when they see it", a reference implementation must be understood before porting, a build-queue or architecture-changing task is about to start, a mid-build deviation needs capturing, or a finished change needs buy-in or verified understanding before merge. Trigger even when the skill is not named: "do a blindspot pass", "find my unknown unknowns", "interview me about this change", "brainstorm the approach for a task I am about to build", "help me scope this", "what am I missing here", "plan this before I build", "I know nothing about X in this codebase". When a territory profile is present it reads memory and the graph first and feeds the map into the build pipeline; without one it degrades to a plain general map. Not for trivial or mechanical tasks with clear acceptance criteria, single-fact lookups, pure debugging of a known symptom, or pure code review, those have their own tools. Do not use it to run the build itself; the map is the deliverable and implementation remains a separate task, whether it has not started or is already underway.

    Also in 5 projects that use this

  • +5 more from dzhng

The file

Show its rules and text

We split this skill into the rules it gives an agent

21 rules. 0 are common, 6 are distinctive, and 15 are very specific to this project.

  • 0

    Common

    Most of the 71 edited versions we read keep these.

  • 6

    Distinctive

    This author's own choices. Worth a look.

  • 15

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    If codesift-setup.md is missing, print [CONTEXT] codesift-setup missing — assuming CodeSift unavailable and continuing in degraded mode.

    line 94
  2. Very specific:

    Read the production file fully, then read ../../shared/includes/test-code-types-core.md and classify from that file's canonical table.

    line 98
  3. Very specific:

    --dry-run: after the queue is built, run Step 1 (Analyze) per file, print the classification table, STOP.

    line 269
  4. Very specific:

    Every value is a block: the key alone on its line, then each value line indented two spaces, ending at the next unindented key.

    line 306
  5. Very specific:

    Every relational operator, boolean operator, throw/raise, optional chain (a?.b), arithmetic operator and literal index it prints is an …

    line 391
  6. Very specific:

    Add rows per symbol: entry + every owned branch + every explicit error path + owned side effects, including every boundary obligation from …

    line 410
  7. Very specific:

    Write the manifest to $ZUVO_DIR/contracts/<basename>.coverage.json (status: "inventory", current sha256, NO coverage claims).

    line 411
  8. Very specific:

    Extension pre-flight (JS/TS): verify the test extension matches the runner's include pattern (.spec vs .test).

    line 459
  9. Distinctive:

    Every program that runs against this suite — the coverage gate, scoped coverage, and the native mutation runner — runs in ONE call, and its …

    line 468
  10. Distinctive:

    Read this before concluding you are out of budget:

    line 568
  11. Distinctive:

    Context resume guard: after a compaction resume, prior-session claims ("clean blind audit") are UNVERIFIABLE — only tool output in the …

    line 662
  12. Very specific:

    When no runner is available, the helper installs one workspace-locally with npm install --no-save (writes no file the project keeps — …

    line 706
  13. Very specific:

    Record the verdicts: after triage: every finding is fixed, rejected (a false positive) or deferred — record each by its ID: in ONE call, …

    line 813
  14. Very specific:

    Absolute paths only: Read the FULL captured output — never tail/head as the triage source (a pass-2 CRITICAL was lost to tail -60).

    line 817
  15. Distinctive:

    Before rejecting a CRITICAL/WARNING: restate the attack vector in one sentence and verify the rejection defeats the VECTOR, not just the …

    line 819
  16. Very specific:

    Follow test-bugfix-protocol.md: fix-scope (not severity) decides fix-now-vs-escalate.

    line 838
  17. Very specific:

    Follow retrospective.md: gate check → structured questions → TSV emit → markdown append.

    line 915
  18. Very specific:

    Append via wrapper (REQUIRED): Never >> directly to ~/.zuvo/runs.log:

    line 965
  19. Very specific:

    Run one final full-suite validation, or explicitly scope the failure count to touched test files before printing new in scope: 0.

    line 973
  20. Distinctive:

    Read production code before planning tests.

    line 1002
  21. Distinctive:

    Test what the code OWNS, mock what it DELEGATES.

    line 1005

“Common” is measured: kept by at least 6 in 10 of the 71 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

zuvo:write-tests — Single-File Test Pipeline

Generate high-quality tests for production code. Each file goes through the full pipeline individually — no batching of files or pipeline steps, no skipping verification in normal mode, no skipping the coverage gate or audit.

The pipeline's spine is inventory-first + executable proof: the public surface is enumerated and FROZEN before the first test is written, and the only authority on coverage completeness is scripts/test-coverage-gate.py — a program, not the writer's own claim.

Scope: Existing production files with missing or partial test coverage. Out of scope: New feature tests (use zuvo:build), mass anti-pattern repair (use zuvo:fix-tests), audit without writing (use zuvo:test-audit).

Argument Parsing

InputBehavior
[file.ts]Write tests for one production file
[directory/]Write tests for all production files in the directory
autoDiscover uncovered files, process one at a time until done
--dry-runRun Phase 0 + Step 1 for all files, print plan, stop
--no-cacheRe-run discovery/classification from scratch: ignore any cached CodeSift index answer and any previously built queue for this run
--resume <basename>Resume ONE file from its persisted checkpoint: load contracts/<basename>.coverage.json + contracts/<basename>.contract.md, verify production_sha256 against the file on disk (mismatch → refuse and demand re-inventory — the existing hash rule), take classification from the contract (skip Phase 0.5/Step 1 re-derivation), then jump by state: manifest inventory + contract present → Step 2; final → Step 3; final with Q-scores synced → Step 3.3
--invoked-by <skill>Run on behalf of the named skill (refactor's Phase 3.6 Step 0, for one): no snapshot, no closing commit, and the report ends with Backlog writes: <path>, … (or Backlog writes: none) for that skill's closing commit. Without it the run is standalone and commits its own writes
--resume-run <ledger>Resume an auto-mode queue from its run ledger (see Auto-mode context boundary): reload run-level facts + remaining queue, continue with the next file in a clean window

--no-cache forces Step 7's queue build to re-derive from a fresh scan rather than reusing a queue computed earlier in the session. (It used to promise clearing a "project-profile cache" that no step in this skill ever reads or writes — a dead flag until 2026-08-02.)


Mandatory File Loading

PHASE 0 — Bootstrap (always, before reading production file)

  1. ../../shared/includes/codesift-setup.md            -- [READ | MISSING -> DEGRADED]
  2. ../../shared/includes/no-pause-protocol.md         -- [READ | MISSING -> WARN] (HARD: no mid-file pauses in batch/auto mode)
  3. ../../shared/includes/test-reviewer-routing.md     -- [READ | MISSING -> WARN] (preflight + all reviewer routing)
  4. ../../shared/includes/test-inventory-protocol.md   -- [READ | MISSING -> BLOCKED] (inventory-before-writing spine)
  5. ../../shared/includes/coverage-manifest-schema.md  -- [READ | MISSING -> BLOCKED] (manifest + validator contract)
  6. ../../shared/includes/test-quality-gate.md         -- [READ | MISSING -> WARN] (carries the dispatch-authorization rule)
  7. ../../shared/includes/env-compat.md               -- [READ | MISSING -> DEGRADED] (agent-dispatch lanes + Remote/Queued Execution — the Per-File Loop and Phase 0 step 8 both cite its sections; missing it means the context-boundary lane and the remote-result evidence rule are UNAVAILABLE, so say so and stay single-session rather than guessing a lane)

If codesift-setup.md is missing, print [CONTEXT] codesift-setup missing — assuming CodeSift unavailable and continuing in degraded mode. If test-inventory-protocol.md or coverage-manifest-schema.md is missing, the executable gate cannot be honored — stop the run with a loud include-integrity error rather than degrading to prose-only gating.

PHASE 0.5 — Classify (read production file, determine loading tier)

Read the production file fully, then read ../../shared/includes/test-code-types-core.md and classify from that file's canonical table. Do NOT classify from memory. (Phase 1 lists the same file — this Phase 0.5 read IS that load; do not read it twice.)

  • Code type: VALIDATOR / SERVICE / CONTROLLER / HOOK / PURE / COMPONENT / GUARD / API-CALL / ORCHESTRATOR / STATE-MACHINE / ORM-DB / TYPE_CONTRACT
  • Complexity: THIN / STANDARD / COMPLEX
  • Testability: UNIT_MOCKABLE / UNIT_REFLECTION / NEEDS_INTEGRATION / MIXED
  • Runtime: NO-DOM (real Node, node:test/vitest node env) / JSDOM / REAL-BROWSER (route to write-e2e). Decide BEFORE writing: if the repo has an ADR or CLAUDE.md runner-by-extension table (e.g. *.test.tsx → vitest/jsdom, *.spec.mjs → node:test/no DOM — rs_fe ADR-0001 exists because three files once ran under the wrong runner), that table is BINDING — mirror it and declare the chosen runtime in the spec header.

Evaluate TOP-DOWN, FIRST MATCH WINS (the list was unordered until 2026-08-02, so a COMPLEX COMPONENT was assignable to two different tiers depending on reading order):

IF code_type == TYPE_CONTRACT                                  → TYPE (first: a file that emits no
                                                                 runtime value has nothing the other
                                                                 branches can test, and COMPLEX would
                                                                 otherwise capture a large type module)
IF complexity == COMPLEX                                       → HEAVY
IF code_type IN (PURE, VALIDATOR) AND complexity == THIN       → LIGHT
IF code_type IN (PURE, VALIDATOR) AND complexity == STANDARD   → STANDARD
IF code_type IN (STATE-MACHINE) AND complexity == THIN         → LIGHT
IF code_type IN (COMPONENT, HOOK)                              → COMPONENT
IF code_type IN (CONTROLLER, ORCHESTRATOR)                     → HEAVY
IF module mixes code types                                     → STANDARD (HEAVY if any unit is COMPLEX)
ELSE                                                           → STANDARD

Print: [CLASSIFIED] {file}: {code_type} {complexity} → tier {TIER}. Then check the Cross-Cutting Families table in test-code-types-core.md; on any match print [FAMILY] {file}: +{NAMES}, record "families": [...] in the manifest, and append that family's mandatory rows to the test contract.

No silent default. ELSE → STANDARD is a fallback tier, never a classification. When no code-type row matched, print [UNCLASSIFIED] {file}: no code-type row matched — shape: {one-line structural description of what the file actually is}, record "unmatched_shape": "<that description>" in the manifest, and derive the test contract from READING the production file — the generic template alone is not a valid contract source for an unclassified shape. The completion report MUST list every UNCLASSIFIED file with its shape. Rationale: the silent ELSE → STANDARD is the exact mechanism that handed a type-only file a runtime-spec template (2026-08-19) and would do the same for the next unknown shape; the manifest record is the frequency data that decides which shapes earn a table row.

Classify TYPE_CONTRACT before reaching for ELSE. A file that exports only type / interface / declare (*.types.ts, types.ts, *.d.ts, anything under a types/ directory) matched no row in the table until 2026-08-19 and therefore fell through ELSE → STANDARD — which handed a file with no runtime surface to the runtime-spec template. The result was suites where every assertion compared a literal to itself and Q7/Q11 scored 0 truthfully. The quick check is the file's emit, not its name: if tsc would produce an empty .js, it is TYPE_CONTRACT.

TIER TYPE loads test-code-types-core.md (the TYPE_CONTRACT section) and nothing else — no mock safety, no edge-case checklist, no fixture rules; none of them have a subject here. Output is one <name>.test-d.ts per type module with zero runtime expect, and the run MUST first record whether a typecheck lane actually executes those files (type_tests: ENFORCED | NOT_ENFORCED). A NOT_ENFORCED suite is decorative and must be reported as such, never as coverage.

PHASE 1 — Conditional Load (based on tier + detected stack)

Load ONLY the includes matching tier AND stack. Print READ/SKIP per file. If an include is missing: print [PHASE1] MISSING: <file> — continuing with degraded rules, keep loading, then print loaded=<N>/<M>; if under half loaded, print [WARN] Low include availability — coverage planning and Q-score confidence are reduced. Do not overclaim clean states.

IncludeLIGHTSTANDARDHEAVYCOMPONENTTYPE
../../shared/includes/test-contract.mdFullFullFullFullFull
../../shared/includes/test-blocklist.md (incl. typed mock gate)FullFullFullFullFull
../../shared/includes/quality-gates.mdQ1-Q25 only*Q1-Q25 only*Q1-Q25 only*Q1-Q25 only*Q1-Q25 only*
../../rules/testing.mdFullFullFullFullSKIP§
../../shared/includes/test-mock-safety-core.mdFullFullFullFullSKIP§
../../shared/includes/test-code-types-core.mdFullFullFullFullFull
../../shared/includes/test-bugfix-protocol.mdFullFullFullFullSKIP§
test-mock-safety-{stack}.md (js/php/python)SKIPFullFullFull‡SKIP§
test-code-types-{stack}.md (js/php/python)SKIPFullFullFull‡Full
../../shared/includes/test-edge-cases.mdSKIPFullFullFullSKIP§
../../shared/includes/test-mutation-probes.mdSKIP†FullFullFullSKIP§

* quality-gates.md: Read ONLY ## Q1-Q25: Test Quality Gates to end of file. Skip CQ1-CQ40. † LIGHT loads it only when the file has an error fallback (probe class 2). § TIER TYPE skips these because they have no subject, not to save tokens. There is nothing to mock (no runtime), nothing to mutate (a mutation probe needs executable code — this is why a TYPE_CONTRACT file scores Q7/Q11=0 honestly rather than failing), no edge-case inputs (no inputs), and no bug to reproduce. Loading them produced the exact ceremony this tier exists to remove. What TIER TYPE does load is the TYPE_CONTRACT section of test-code-types-core.md, which carries the validity precondition, the six construct patterns and the ban on circular assertions. ‡ COMPONENT loads the stack files too (fixed 2026-08-01): test-code-types-core.md's COMPONENT Callback Routing Guard explicitly defers its framework example to test-code-types-js.md (Dispatch/Router template, Lazy/Suspense caveat, Time-Dependent fake-timer table) — skipping them left that pointer dangling for the one tier that needs it.

Stack detection: walk UP from the target file and STOP at the first directory containing ANY stack manifest — recognized or not. Signals: package.json => JS/TS; composer.json => PHP; pyproject.toml, requirements.txt, requirements-dev.txt, pytest.ini, setup.cfg, Pipfile, manage.py => Python. Ties inside ONE directory: package.json > composer.json > any Python signal; print the conflict decision. Target-file extension is a WRITTEN override (.py => Python, .php => PHP) that beats the manifest winner — print [STACK] {file}: {stack} (override: extension) when it fires. Load at most one stack-specific include family. (Until 2026-08-19 the only Python signal was pyproject.toml and the walk-up skipped unrecognized manifests: data-lab — 1,549 .py files, requirements.txt + pytest.ini, no pyproject.toml — classified as JS/TS, and a FastAPI file under python-service/ climbed past its own requirements.txt to the root package.json.)

DEFERRED — Load after queue empty (Completion only, once per run)

  D1. ../../shared/includes/run-logger.md           -- [READ at completion]
  D2. ../../shared/includes/retrospective.md        -- [READ at completion]
  D3. ../../shared/includes/knowledge-curate.md     -- [READ at completion]
  D4. ../../shared/includes/test-metrics.md         -- [READ at completion] (frozen quality/cost/speed formulas — cite, never restate)
Dispatch follows `../../shared/includes/execution-policy.md` through env-compat. Reuse existing
authorization within that policy; session restrictions take precedence. Run each required gate
and report its actual independence or an unmet requirement.

  D4. ../../shared/includes/test-quality-gate.md    -- [READ at completion] (final zuvo:test-audit gate → tier A)

Phase 0: Bootstrap + Preflight (once per run), Classify (per file)

  1. Resume branch (FIRST, before step 1 — only when --resume/--resume-run was passed). A flag no step reads is a dead flag; this skill already carries that scar for --no-cache.
    • --resume <basename>: read $ZUVO_DIR/contracts/<basename>.coverage.json and <basename>.contract.md. Missing either → print [RESUME] no checkpoint for <basename> — running the full pipeline and fall through to step 1. Recompute the production file's sha256 and compare with production_sha256. MISMATCH → refuse, print [RESUME] production file changed since freeze — re-inventory required, DISCARD the contract's classification (it describes the old bytes) and run the FULL pipeline from step 4 (classify) — never jump to Step 1.6, which cannot run without a stack and code type. MATCH → take stack/code_type/tier from the contract's classification line (skip steps 4-6 and Step 1), and enter the Per-File Loop at: manifest inventory → Step 2 · final → Step 3 · final with Q-scores synced → Step 3.3. Steps 1-3 of this phase ALWAYS run, on every resume path. CodeSift setup, $ZUVO_BASE resolution and the reviewer preflight are run-level preconditions, not per-file work a checkpoint can vouch for — a resume that skipped the preflight would write tests no reviewer can audit, which is the one thing this skill's spine exists to make impossible. The hash covers the PRODUCTION bytes only. The test file and the suite may both have moved while the run was interrupted, so a resumed run re-reads the existing test file at Step 1 and re-runs the scoped suite at its next gate rather than trusting the manifest's recorded result.
    • --resume-run <ledger>: read the ledger, restore its five fields, and SKIP steps 7-8 (queue build and baseline run) — the baseline is recorded there, and re-running the whole suite per file is the cost the ledger exists to remove. Steps 4-6 (classify + runner refinement) still run for every file — the ledger carries run-level facts, never a per-file classification. A baseline has a shelf life. The ledger records when it was taken; if the resume happens more than a few hours later, or git rev-parse HEAD differs from the SHA the ledger recorded, re-run step 8 instead of trusting it — an aged baseline silently reclassifies somebody else's new failure as pre-existing, i.e. as something this run may ignore. Two callers, two behaviours, and collapsing them breaks the one a human uses: · sub-agent resume (a file argument is present) — process THAT file only and return; do not touch the queue, do not dispatch onward. · user resume (no file argument, e.g. after /clear) — walk the ledger's queue: from its first entry and keep going to the end, exactly as a normal auto-mode run would. This is the whole point of the /clear + --resume-run line printed at the context boundary; a version that stops after one file makes the user re-type it per file.
    • Both flags are inert outside these two paths: no other step branches on them.
  2. CodeSift setup per codesift-setup.md. Note repo identifier.
  3. Resolve $ZUVO_BASE per test-reviewer-routing.md (absolute paths for every script call).
  4. Reviewer preflight (REQUIRED, before any test is written): run bash "$ZUVO_BASE/scripts/reviewer-preflight.sh" and act on preflight_status per the table in test-reviewer-routing.md. On no-provider/canary-failed: print review infrastructure unavailable NOW — the whole run is DRAFT/BLOCKED_INFRA from the start; tests may still be written for their standalone value, but no file may be reported PASS and the completion block must carry the BLOCKED_INFRA list. This replaces discovering a dead reviewer after the pipeline already spent its budget.
  5. Read production file. Detect stack. Classify. Load includes per PHASE 0.5 / PHASE 1 above.
  6. Dynamic context retrieval (when CodeSift available) — dimensions by tier: LIGHT → D1; STANDARD → D1 + D2/D3 (conditional) + D4; HEAVY → D1-D4; COMPONENT → D1 + D4. Skip any dimension that times out; partial context beats none.
    • D1 exemplar test (all tiers): find an existing test for this module (find_references on the main export → *.test.*/*.spec.*; fallback search_text for describe/extends TestCase/class Test... per stack). Read it fully — it defines mock style, structure, setup, matcher and cleanup conventions. Print [CONTEXT] Exemplar: {path} or — none, using generic patterns.
    • D2 import mocks (STANDARD+; skip if exemplar is same-module): for ≤5 target imports, search_text for existing vi.mock/jest.mock/createMock/mock.patch patterns in project tests.
    • D3 test setup (STANDARD+; skip if CLAUDE.md or exemplar covers it): search_text for setupFiles/_bootstrap/conftest config; read setup outlines.
    • D4 hub signatures (STANDARD+/COMPONENT): search_symbols (bare names, detail_level: "compact", include_source: true, token_budget: 800) for the target's imported utilities. Never get_symbols() on bare names.
    • On repo/index errors run the codesift-setup.md recovery loop once; on Transport closed abandon CodeSift for the rest of the run. Without CodeSift: skip all dimensions, print [CONTEXT] CodeSift unavailable — using legacy detection.
  7. Test runner refinement: read the nearest manifest/config; detect runner (vitest/jest/phpunit/pytest) and existing test conventions. If the repo carries a runner-by-extension table (ADR/CLAUDE.md), it is BINDING per the Runtime axis above — the extension selects the runner, and the written file's extension must match the intended runtime. No manifest → infer from extension; still unknown → mark file FAILED and record the runner process item (once per run; the file goes in the report, "Filing findings"). For JS/TS COMPONENT/HOOK targets check DOM-matcher registration and cleanup globality; reuse the exemplar's local pattern when global setup is absent.
  8. Build queue: explicit mode = user's targets. ALWAYS exclude from auto-discovery: **/migrations/**, *.sql, prisma/migrations/**, supabase/migrations/** (route: zuvo:db-audit — print the excluded count), and build artifacts storage/**, bootstrap/cache/**, **/.next/**, **/dist/** (indexed compiled output corrupts symbol counts — tgm-collect indexed PHPStan cache as source), and server-rendered views/templates (**/views/**/*.php, *.blade.php, *.twig, *.html.erb, *.jinja*) plus files marked @presentational — Worth Gate NO_BEHAVIOUR; print the excluded count. 0-symbol guard: extraction returning 0 symbols for a file over ~30 LOC means the EXTRACTOR failed (non-ASCII docblocks are a known cause), never "nothing to test" — mark the file MUST-VERIFY-MANUALLY and read it raw. Auto mode with CodeSift: gather dead/leaf candidates, 90-day hotspots, test-reference counts, role signals; 0 test refs = UNCOVERED; priority hub > high-churn > leaf; degraded discovery falls back to the manifest-root glob. Auto mode without CodeSift: glob production files under the source root; files without matching tests = UNCOVERED.
  9. Baseline test run (once per run, after queue, before loop): record pre-existing failures — they are ignored in verification. Remote execution (rt farm) follows env-compat Remote / Queued Execution: blocking attach with an upfront deadline, and a result without the runner's own summary + executed=true evidence is a failure re-dispatched to the farm — never a PASS and never re-run here. Skip in --dry-run. Runner/config unavailable → record the baseline process item ("Filing findings"), mark every queued file FAILED (Blind Audit=skipped, Adversarial=not_run), stop.
    • Schema-drift pre-probe (only when queue has an ORM-DB target or DB test helper): run one seed/schema probe; column/relation/table does not exist = run-level ENV blocker — record the schema-drift process item ("Filing findings"), mark every DB-backed file FAILED, stop. Skip in --dry-run.

--dry-run: after the queue is built, run Step 1 (Analyze) per file, print the classification table, STOP. Never run suite-mutating commands.


Filing findings (backlog-add)

Every finding this run defers is recorded through backlog-add, one call per finding, at the step that finds it, so a file that ends FAILED has recorded its findings too (backlog-protocol.md "Filing from a skill"; the bars, the dedupe and the confidence routing are the protocol's and are not restated here). Read that section before the first call. The reviewer and auditor sub-agents the run dispatches (the blind audit, the adversarial review, the test audit) return their findings in their reports and never call backlog-add; the run files them. A --resume-run instance of write-tests is the run for its one file: it files that file's findings itself and ends its report with Backlog writes: <path>, … (or Backlog writes: none), and the coordinator appends each such line's paths to <ledger path>.backlog-writes as the file returns.

  • A finding in a file under test or in its tests (an adversarial WARNING of more than 10 lines, since up to 10 lines is fixed now, an unresolved CRITICAL after the final pass, a security-context INFO the run rejects, a duplicate suite, an extraction a view needs): a Generic write, ~/.zuvo/backlog-add --repo "$R" --class <C> --conf <N> --source write-tests --file "$F" --line <n> [--anchor "$A"] --title "$T". <C> is CQ, or BUG only when it names the input and the wrong output (BUG, SEC and DATA need --anchor "$A"), or the class the finding is (DUP, TEST-GAP, SEC). A finding about the whole file cites --line 1 with a "Whole file: …" title and no --anchor ("The location rule"). When the run names an assertion the tests lack on a production line it changed, add it: ~/.zuvo/backlog-add --repo "$R" --class TEST-GAP --conf <N> --source write-tests --file "$F" --line <n> --anchor "$A" --missing-assertion "$M" --title "$T".
  • An environment blocker or a skipped gate is a process item (Process item): ~/.zuvo/backlog-add --repo "$R" --class PROCESS --conf 90 --source write-tests --target subject:write-tests-<gate> --title "$T", with <gate> and T from the table, the same words on every run. A gate item is one per run, however many files the gate fails: this run's reason and every file it marked FAILED or BLOCKED_INFRA go in the report entry, never in the title; name the F-id there. A file a run-level gate failed (runner, baseline, schema-drift) was never analysed, so it has no gaps to record yet: the gate's item stands for it and the report entry names it.
<gate>T (fixed title)Raised at
runnerwrite-tests: test runner could not be determined, file FAILEDPhase 0 step 6
baselinewrite-tests: baseline test run could not execute, queue FAILEDPhase 0 step 8
schema-driftwrite-tests: DB schema drift blocks DB-backed tests, files FAILEDPhase 0 step 8
blind-audit-budgetwrite-tests: blind audit still FIX or REWRITE after two passes, files FAILEDStep 3.5
blind-audit-unavailablewrite-tests: no blind-audit reviewer available, file BLOCKED_INFRAStep 3.5

Assign "$R", "$T" and, for a file finding, "$F" before each call, "$A" only when you pass --anchor, "$M" only when you pass --missing-assertion, and "$E" only when you pass --evidence, as "Free text goes through a variable" in that section says; an empty value you pass is a bug, and backlog-add refuses a missing required field (REFUSED missing-field:<f>). Report the B- or F-id each call prints. On a non-zero exit act on the protocol's exit table; a finding still not persisted is listed under NOT PERSISTED (<reason>), never appended by hand. A bug fixed in Step 4.5 gets no call (Closing, not removing).

Snapshot. Once, at the run's original start, before Phase 0 step 6, the first step that can call backlog-add: R='<absolute repo root>'; git -C "$R" status --porcelain -uall -- memory/, R being the root of the checkout under test (a linked worktree's own root), assigned in the same shell call. Save the output verbatim as a fenced block in the sidecar <ledger path>.backlog-snapshot beside the run ledger (an empty snapshot is an empty block; never one line), and write a backlog-snapshot: line in contracts/<basename>.contract.md when Step 1.6 writes the contract that names the sidecar's path, not its content. A resumed run reuses the saved one and never takes it again (backlog-protocol.md "Filing from a skill", Committing what backlog-add wrote): --resume-run <ledger> without a file argument reads <ledger path>.backlog-snapshot, --resume <basename> follows the contract's line to the sidecar. When neither is found the run takes no new snapshot, stages nothing at the end and lists its wrote= paths, as the protocol's failed-snapshot rule says. A --resume-run sub-agent (a file argument) commits nothing and needs none, and neither does a run with --invoked-by <skill> (refactor's Phase 3.6 Step 0 dispatch passes it): the calling skill's snapshot stands for it (Closing commit for backlog-add's writes below).


Per-File Loop

Execute Steps 1 → 1.5 → 1.6 → 1.7 → 2 → 2.5 → 3 → 3.2 → 3.3 → 3.5 → 4 → (4.5) → 5 in order. Do NOT skip a step unless a later step explicitly defines a degraded terminal state. Do NOT proceed to the next file until every checkpoint completes or is explicitly downgraded.

Auto-mode context boundary (file boundary = context boundary). After each file's completion block, write/refresh the run ledger — the run-level facts that until now lived only in the session. Its path is stamped ONCE at Phase 0 and reused unchanged for the whole run: $ZUVO_DIR/checkpoints/run-<ISO-date>-<first-target-slug>.md. A bare run-<ISO-date>.md is wrong on both ends — two runs on one day overwrite each other, and a run crossing midnight would split into two half-ledgers. Fixed keys, in this order, so step 0 can read it back without guessing (a freeform ledger is the same unparseable state it replaces): queue: · runner: · baseline_failures: · exemplars: · stack:. Every value is a block: the key alone on its line, then each value line indented two spaces, ending at the next unindented key. That is what makes the multi-line ones (queue:, baseline_failures:, exemplars:) parseable rather than merely readable. Then, per the env-compat lanes:

  • Claude Code: dispatch the NEXT file to a FRESH implementer sub-agent — general-purpose, whose entire prompt is Skill(zuvo:write-tests <target-file>) plus --resume-run <ledger path> and nothing else. Only the coordinator dispatches. A run entered via --resume-run handles exactly the ONE file it was given and RETURNS its completion block — it must not reach this boundary and dispatch onward. Without that rule each sub-agent re-enters this block when it finishes and spawns the next one, so the queue drains through a chain of nested agents that each keep every ancestor's context alive instead of returning it. The coordinator keeps the queue and per-file syntheses and never accumulates production sources, specs, or tool outputs. UNCONDITIONAL for every file — the writer phase is isolated per the UNIVERSAL WRITER ISOLATION rule in Step 2, so no file ever inherits another file's transcript; the coordinator alone persists, holding only the ledger and per-file syntheses.
  • Harnesses with NO dispatch at all (Cursor, Antigravity) — NOT Codex, which dispatches mechanical workers; see env-compat.md: print [CONTEXT] {file} complete — safe point. Clean continue: start a fresh context (/clearin Claude Code, a NEW CONVERSATION in Codex — it has no/clear), then zuvo:write-tests --resume-run <ledger>. The skill cannot clear the context for the user; it makes clearing safe and cheap instead. Name the platform's own gesture — printing /clear to a Codex user names a command that does not exist there. The executable gates (2.5 validator, 3.2 coverage, 3.3 probes) read DISK, not context — every step boundary except mid-Step-2 (atomic Write) is a safe cut point.

Step 1: Analyze

Production file already read/classified. If a test file exists, read it now and pick the action:

  • No test file → CREATE
  • Exists, quality OK (behavioral assertions, no anti-patterns) → ADD TO
  • Exists, quality BAD (fragile string tests, tautological oracles, security theatre, duplicated positives, structural duplicates) → REWRITE the whole file. Net test count MAY decrease. Do NOT add good tests on top of bad tests.

Duplicate test files: search sibling/legacy trees for other test files targeting the same module. 2+ active → print [DUPLICATE] ..., read all, prefer the co-located file as canonical; never silently extend a second overlapping suite. Unconsolidatable overlap → the queued file is FAILED, and a whole-file DUP finding (--class DUP, --line 1) is recorded on the non-canonical suite (the same file when the queued file is it; "Filing findings").

Worth Gate first (test-code-types-core.md § Worth Gate): if the file is a NO_BEHAVIOUR shape — presentational component, server-rendered view/template/widget HTML, wiring, library-only DTO, pure delegation, constants — write NO tests. Record Status=SKIPPED_NO_BEHAVIOUR, Tests=0, Q Score=N/A, Blind Audit=skipped, Adversarial=not_run, print [NO_BEHAVIOUR] {file}: {shape}. Business logic found inside a view → print [EXTRACT] {file}:{line} {what} and record the extraction ("Filing findings", --class CQ at that line); the test belongs to the extracted unit, never to the view. A contract on the file (bot data-*, export layout, payload shape) is tested on its own and does not make the rest of the file test-worthy. An existing suite on a NO_BEHAVIOUR file is not extended; delete it only when the user asked for cleanup. For files that pass the gate, every TEST OUTLINE row names the defect it would catch; rows that cannot are dropped. Project/user rules that narrow test scope outrank this skill's budgets.

Barrel file (only export { X } from ... lines): do NOT write delegation tests. Record Status=SKIPPED_BARREL, Tests=0, Q Score=N/A, Blind Audit=skipped, Adversarial=not_run; expand the queue to its sub-modules. Print [BARREL] {file} — expanding to {N} sub-modules.

With an exemplar (D1): extract and follow its cleanup pattern, matcher library, async pattern, mock factory style, and import conventions. Do NOT invent new patterns. D2 mock patterns feed MOCK INVENTORY; D4 signatures feed assertion planning.

Rewrite scope stays single-file — broad anti-pattern campaigns belong to zuvo:fix-tests.

Step 1.5: Bug Scan (before planning tests)

Scan the production code for bugs: missing error handling, logic errors (wrong operator, off-by-one, inverted condition), security gaps, unhandled edge cases. Every confirmed find is a fix-in-run candidate for Step 4.5 per test-bugfix-protocol.md — never a silent backlog row. If the strongest honest test would be RED, write a characterization test now and fix in Step 4.5 (never weaken the assertion).

Print: [BUG-SCAN] Found {N} potential issues. or [BUG-SCAN] Clean.

Step 1.6: Production Surface Inventory (FROZEN before writing)

Generate the manifest — do not transcribe it. One command writes every public symbol and every boundary as a row at coverage: NONE:

ZUVO_DIR="${ZUVO_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)/zuvo}"
python3 "$ZUVO_BASE/scripts/test-coverage-gate.py" scaffold \
  --production <file> --out "$ZUVO_DIR/contracts/<basename>.coverage.json" \
  --test-files <planned spec paths> --repo-root "$(git rev-parse --show-toplevel)"

Your job is then the half that needs judgement: each row's description, its coverage, and the test-file:line evidence that proves it. The rows themselves come from the same extractor the validator checks them against, so the two agree by construction — hand-authoring them is transcription whose only possible outcome is a disagreement the gate reports back to you.

This exists because the entry cost was killing the protocol outright, not because typing is tedious. Measured on the benchmark corpus: a 304-line file with 88 branches was abandoned in nine consecutive runs across three skill versions — no manifest was ever written, so the gate never ran and neither did anything downstream. The runs said why, and at ~90 hand-authored rows they were not wrong: "isn't practical to run in full here." One command is practical.

Then follow test-inventory-protocol.md Step 1.6 for the parts the generator cannot know:

  1. Cross-check the extractor's symbol list: python3 "$ZUVO_BASE/scripts/test-coverage-gate.py" extract --production <file> — its symbol list is the floor; add rows for surface it cannot see (routes, indirect callers), never remove one it found. 1b. Run the boundary extractor in the same breath — it reads the source, not the classification:
python3 "$ZUVO_BASE/scripts/test-coverage-gate.py" boundaries --production <file>

Every relational operator, boolean operator, throw/raise, optional chain (a?.b), arithmetic operator and literal index it prints is an inventory row of type branch or error_path, and its evidence must be a test that sits ON the boundary — not merely one that exercises the line. The printed list is ordered by measured survival frequency and capped at 60; --all shows the rest, and a file needing more than that is a split candidate under the rule below.

Why this is mechanical rather than a judgement call: measured across 39 suites for one file, the mutants that separate an 88% suite from a 91% one are all boundaries the tests never sat exactly on — value < 0 surviving a change to value <= 0 (27 of 39 suites), a literal 0 bumped to 1 (27 of 39), a throw deleted outright (34 of 39), normalized[0] shifted to normalized[1] (15 of 39). On the React cases the top survivors are removed optional chains and swapped booleans instead: same shape, different operator mix. test-edge-cases.md already says "exact threshold N, N-1, N+1" — but in a row keyed on code TYPE, so a bare comparison inside a pure function never triggers it. Classification decides whether the rule applies, and classification happens before the comparisons are known. Deriving the obligations from the source removes that ordering problem.

Exit 3 means no parser for this language: record BLOCKED_DEGRADED for boundary evidence — that is not the same as "this file has no boundaries".

  1. Add rows per symbol: entry + every owned branch + every explicit error path + owned side effects, including every boundary obligation from 1b. Honest owned/delegated classification.
  2. Write the manifest to $ZUVO_DIR/contracts/<basename>.coverage.json (status: "inventory", current sha256, NO coverage claims).
  3. Print INVENTORY FROZEN with the N/N projected metrics. For COMPLEX files these N/N metrics — public methods, owned rows, error paths — are the ONLY progress numbers; never present a test count as progress.

Split rule (mandatory): >15 public entry points OR >40 owned rows OR >800 production LOC OR >800 projected test LOC → split into sibling specs by responsibility per the protocol; one manifest aggregates all siblings.

Step 1.7: Inventory Validation

python3 "$ZUVO_BASE/scripts/test-coverage-gate.py" validate \
  --manifest "$ZUVO_DIR/contracts/<basename>.coverage.json" \
  --phase inventory --repo-root "$(git rev-parse --show-toplevel)"

exit 0 → frozen, proceed. exit 1 → extractor found symbols the inventory missed: add them, rerun. Never start writing tests over a failing freeze. exit 3 → degraded extraction: record BLOCKED_DEGRADED evidence quality for the rest of the file (see coverage-manifest-schema.md). After this point the symbol list is immutable for the run; any production edit invalidates the manifest (hash) and forces a re-freeze.

Step 2: Write

UNIVERSAL WRITER ISOLATION (every file, every tier — no exceptions). Writing executes in a FRESH context whose entire payload is: contract.md + the production source + the runner command

  • baseline pre-existing failures. NOT the skill, NOT this session's transcript. The writer is a MECHANICAL worker — it applies a frozen contract — so it is dispatchable wherever dispatch exists, and a HANDOFF is the LAST resort, never the default for "not Claude Code":
  1. Claude Code — dispatch a writer sub-agent with exactly that payload.
  2. Codex (>= 0.128) — dispatch it too. env-compat.md permits mechanical-worker dispatch here; Codex has native sub-agents (~/.codex/agents/, multi_agent feature) and this build generates their TOMLs. Use ONE explicitly bounded wait sized to the task, no re-poll loop, and record codex-dispatch:bounded-wait. Do NOT print a HANDOFF just because the harness is not Claude Code — that reads as a capability check and is not one.
  3. Only where dispatch genuinely does not exist (Cursor, Antigravity), or when the bounded wait above expires, print [HANDOFF] contract frozen — clean-window write: fresh context (/clear in Claude Code, a NEW CONVERSATION in Codex), then zuvo:write-tests --resume <basename> and record codex-handoff:fallback.

On resume, load ONLY the payload above (the --resume path already skips Phase 0/1). The writer follows the contract; a gap in the contract is reported back and the contract is amended — the writer never improvises around it silently. Rationale: the skill's ~240KB prefix is needed to PRODUCE the contract, not to type tests from it; re-billing it across every writing turn is the single largest cost in the pipeline (CASE-01: 96% of billed tokens were context re-reads, not output). Isolation is unconditional precisely so that no classification decision can ever route a file around it — cost falls by architecture, never by waived rigor.

  1. Fill the test contract per test-contract.md (BRANCHES, ERROR PATHS, EXPECTED VALUES, MOCK INVENTORY, MUTATION TARGETS, TEST OUTLINE) — derived from the frozen inventory, not re-derived from scratch. 3+ methods sharing a control-flow pattern → per-pattern mode. Write the FULL contract to $ZUVO_DIR/contracts/<basename>.contract.md — all six sections, PLUS the classification line (stack / code_type / families / tier / runtime), the exemplar excerpts to mirror, the exact runner command, and the run's baseline pre-existing failures. Manifest + contract.md together are the resumable checkpoint of everything before Step 2; until now the contract lived only in the conversation, which is why an interrupted run could never resume. Do not print the full contract; show only branch table + outline + planned metrics.
  2. Check test-blocklist.md — including the typed mock gate (no Record<string, Mock> service mocks, no as never, no broad as any, no unused mocks, no expect.anything() on domain arguments; typed Pick<Service, ...>/MockedMethods<T, K> instead).
  3. Apply mock rules per loaded test-mock-safety-* includes.
  4. Write the test file with Write (full file, atomic — NEVER sequential Edit for creation/rewrite; linters can rewrite between edits). Edit only for targeted single-hunk changes after all tests pass. Prepend the stack-native marker comment Generated by zuvo:write-tests. When splitting, write and green one sibling spec at a time.
  5. Extension pre-flight (JS/TS): verify the test extension matches the runner's include pattern (.spec vs .test); rename BEFORE the first run — wrong extensions compile but never run in CI.
  6. Run the target tests. All new tests must pass; pre-existing failures ignored. On truncated/unclear output or 5+ failures switch to structured diagnostics (isolate one failing test; JSON/verbose reporter; fix the first concrete root cause before mass-editing). Testing-library specifics: missing DOM matcher → local import only when global setup lacks it; repeated Found multiple elements → local afterEach(cleanup) only when cleanup is not global. Context discipline: once the scoped run is GREEN, only its one-line summary (pass count + duration) stays load-bearing — do not re-quote or re-read the full runner output afterwards; a green run's raw output had value only while it was red. Three failed fix rounds on the SAME failing test = the context is now part of the problem: hand the file to a FRESH context (sub-agent on Claude Code; on single-agent harnesses print the /clear + --resume line) carrying the checkpoint plus a ≤5-line distillation of what was tried and rejected — never the transcript. Round 4 in the same session repeats round 3 (field data 2026-08-19).

Red truthful tests for production bugs are not a terminal state: characterization-now + fix-in-4.5, or out-of-scope escalation per test-bugfix-protocol.md. Backlogging a fixable bug is not a valid exit.

PURE optimization (LIGHT): contract may skip MOCK INVENTORY if only Logger; keep BRANCHES, ERROR PATHS, EXPECTED VALUES. COMPONENT: follow exemplar cleanup/matcher/async patterns. Neither skips adversarial.

Step 2.5: VERIFY (one command, one verdict)

Every program that runs against this suite — the coverage gate, scoped coverage, and the native mutation runner — runs in ONE call, and its printed block is the only acceptable evidence:

# $ZUVO_DIR is often unset; this is the same default report-output-location.md documents.
ZUVO_DIR="${ZUVO_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)/zuvo}"
~/.zuvo/verify-tests --manifest "$ZUVO_DIR/contracts/<basename>.coverage.json"

Give the tool call a timeout of 600000 (10 min). A cold pass runs a suite, a validator, a coverage run, a typecheck and a mutation run; on a loaded machine that can pass the Bash tool's default 120-second limit, and when it does the harness backgrounds the call and hands back a task id instead of the block. Measured on the rig: every run that hit this then built a polling loop — sleep 90; kill -0 <pid>, sleep 60; echo tick, tail /tmp/verify-out.txt — spending four to six turns waiting for output that one turn would have returned. A shell timeout 590 prefix does NOT help: it bounds the program, not the harness. Set the tool parameter.

There is no version of this step you can do by hand. The command writes a verification receipt into the manifest — the suite's hashes at the moment it was measured — and the coverage gate rejects a manifest marked final without one, or with one whose hashes no longer match the specs on disk. This is not ceremony: measured across the benchmark corpus, roughly one run in three skipped this command entirely, reasoning that the full apparatus was not practical to run here and that following the spine pragmatically would do. Those suites shipped unmeasured and reported success. Writing the receipt yourself is forging a measurement, and the only thing it buys is a green gate over a suite nothing ran.

Before the first call, fill the FROZEN manifest per test-inventory-protocol.md Step 2.5: each row's coverage + test-file:line evidence, status: "final", and quality_gates — run Step 3's critical-gate scoring for Q7/Q11 NOW so the manifest is complete on the first pass. A manifest that is missing its Q values is a bookkeeping reason to run the gate twice, and this pipeline has exactly one budget for that.

The command runs the suite, the validator, scoped coverage, a scoped typecheck and StrykerJS in one process; restores the production file; verifies its sha256 against production_sha256; clears the runner's debris (.stryker-tmp/, mutants.out/, report dirs); and prints one block listing every gap still open. Paste that block verbatim — never paraphrase it, never claim a check it did not print.

PHP (Codeception + Infection). With codeception.yml present the helper runs codecept run <suite> <spec>, Codeception's text coverage and Infection (needs infection/infection + infection/codeception-adapter and an infection.json5). When the project's PHP lives in a container or on the farm, set ZUVO_VERIFY_EXEC to the prefix that gets there (docker exec -w /var/www/html <container> or rt --full --light — --full is not optional on the farm: without it rt condenses a long job to its tail and Codeception's coverage report, which prints BEFORE the test summary, is cut off and reads as "no coverage") — a host PHP without the project's extensions or a coverage driver is not a measurement.

Do not run tsc yourself. Typechecking was the largest wall-clock block in every arm measured on the rig — 122s median even with NO skill loaded, 247s in the heaviest arm, 722s at the worst — because a project-wide tsc --noEmit is the reflex. On the file under test that spend bought nothing: the project carries 6050 pre-existing type errors, so the run drowned its own two diagnostics in somebody else's debt. The helper runs it once per pass against the tsconfig that OWNS the file, incrementally (57s cold, 16s warm), and reports only errors in the spec you wrote. Errors in the production file are printed as context and recorded as a whole-file finding for that file ("Filing findings"; the distinct errors in --evidence "$E"), not one call per error. Each distinct error is filed once: a later pass files only errors it had not seen before, so an error a Step 4.5 fix introduced is still filed and a repeat of one already filed makes no call. They are not this run's gaps.

Mutation waits for COVERAGE, not for the manifest gate. The helper defers StrykerJS until the suite is green and scoped coverage is adequate, because mutation is the most expensive measurement in the pipeline by an order of magnitude (270s median, 1089s worst, against 16-57s for the typecheck) and a number taken over an under-covered suite describes very little.

The FIRST pass always measures mutation, whatever coverage says. Deferring it means writing a whole suite blind and learning what it missed at the end, when changing course is expensive. Measured on the shield case: a run that sees its survivors once leaves 24 of them at 85.3%; runs that iterate on the list close six more and reach 90%. The list handed over on pass one is a specification, not a verdict. Later passes do wait for coverage — by then the list exists, and re-measuring an under-covered suite says nothing new.

It deliberately does NOT wait for the coverage GATE. That gate checks whether inventory rows carry evidence — bookkeeping — and an earlier version keyed the deferral on it. On one benchmark case the gate never went green, so mutation never ran at all, and the run finished having never learned which mutants survived: it scored exactly the no-skill control's number while a slower arm on the same file reached 90.7%. A run with imperfect bookkeeping and a good suite still deserves its most valuable signal.

mutation DEFER is the expected state on a first pass, not a missing measurement; it is also a different state from SKIP, which means no runner exists here at all.

exitmeaningwhat to do
0every applicable check greenproceed to Step 3. Do not run it again.
1gaps open, budget leftONE fix round closing EVERY listed gap, then call again
4budget exhausted (passes or clock)record the OPEN items in the manifest, finish the file BLOCKED_INCOMPLETE
2infrastructurefix the tooling. An unavailable runner is BLOCKED_DEGRADED, never a pass

Two budgets, both owned by the program. Three passes bound how many times the same suite is re-measured; a 15-minute clock of MEASURED time bounds how expensive that gets. Every pass prints pass N of 3 X.X of 15 min measured, so the budget is visible before it runs out.

Why a clock exists at all: across 33 scored runs on one file, within a single arm, working 3-5x longer moves mutation kill by about half a point — 60 turns and 288 turns of the same arm both scored 88.9% — and three of the four suites that came out RED were among the most expensive runs. Past the plateau, more effort buys variance, not coverage. When the clock expires, the remaining gaps get recorded with their IDs and the file finishes; that is the correct outcome, not a failure to try hard enough.

What the budget does NOT count

Mined 2026-09-22 over 15,857 retros: 118 of 325 write-tests retros — better than one in three — were complaints about this budget, and none of them were about gap-chasing. Four mechanisms spent passes without measuring anything, and all four are now closed in the helper. Read this before concluding you are out of budget:

wasis now
The first pass after ANY spec edit failed its own coverage gate (UNVERIFIED/stale receipt) — the gate validated the receipt the same pass was about to write. 47 retros.The receipt is stamped before the gate reads it. A pass following a fix round is green on the first try.
A pass where the suite never ran (missing runner, ZUVO_BASE unset, a Stryker/Babel major mismatch) cost a budget slot. 38 retros.That is exit 2, and the pass is refunded — the block says did NOT cost budget. Capped at 3 refunds, so an unfixable box still terminates.
A downstream gate mandated a test edit after the writing loop was spent, and the only way on was a human ZUVO_VERIFY_RESET=1. 50 retros, 29 of them needing that interrupt.--gate-round <name> — see below.
The clock counted wall time, so an adversarial review running between two passes ate the budget (one run hit BUDGET EXHAUSTED at 17.6/15 min having measured nothing). 16 retros.The clock accumulates time spent inside passes only.

--gate-round — for edits the pipeline itself demanded. Steps 3.5, 4 and A1/A2 all mandate test edits after the writing loop. That is the pipeline working, not a run overrunning, so each named gate carries its own allowance:

~/.zuvo/verify-tests --manifest <m> --gate-round blind-audit|adversarial|test-audit|fix-in-run

Each name grants 2 passes and is claimable exactly once; the name is printed in the block and recorded in the state file, so a widened budget is never silent. The set is closed — an invented name is refused by the parser.

A round is granted on EVIDENCE, not on your say-so. The helper refuses the claim unless both hold, and a refused claim consumes nothing:

  1. the base budget is spent — claimed on pass 1 it would be a bigger budget requested up front, which is what --budget 999 was closed against;
  2. at least one declared spec no longer matches the receipt — the receipt records each spec's sha256 at the moment it was measured, so "a gate mandated a test edit" is observable as "a spec changed since the last pass". No edit, no round. Deleting a spec does not count: evidence is a hash that differs from a hash, never the absence of one.

What condition 2 proves, and what it does not. It proves a spec's bytes changed since the last measured pass. It does NOT prove the named gate is the thing that changed them — a cosmetic edit would satisfy it, and the receipt re-stamps on every green pass, so the evidence resets each time. That is a real residual gap, named here rather than papered over: the honest claim is "a round costs a test edit", not "a round costs a gate". Tightening it to the gate's own artifact (the blind-audit report, the adversarial proof file) is tracked in the backlog.

The first version took the name on trust, and a cross-model review of it pointed out that four typed names took the budget from 3 passes to 11 with no gate having run — --reset-budget in a nicer hat. Condition 2 is what closes that.

Claim a round only when a gate produced the edit. "The mutation score is still 87%" is gap-chasing and gets BLOCKED_INCOMPLETE, not a round. If all four rounds are spent and gaps remain, that is the stop condition doing its job.

The ceiling, stated once. 3 base passes + up to 4 evidence-gated rounds × 2 = 11 passes, reachable only if a real test edit preceded each claim (see the limitation above — the edit is what is proven, not which gate caused it). Infrastructure refunds do not raise it: they give back a slot the tooling spent, and after 3 of them a still-broken toolchain costs passes like anything else, so a box that cannot run the suite still terminates.

Why there is no wall clock between passes any more. Wall time between two calls is where the pipeline's other gates run — the blind audit, the adversarial review — and each of those carries its own budget. Counting it here charged this budget for work it does not own (16 retros). What ends the loop is the pass count; the measured clock bounds how expensive the passes themselves get.

ZUVO_VERIFY_RESET=1 --reset-budget still exists and is still a human decision. It should now be rare: if the reason for another pass is a gate-mandated edit, the round is the correct instrument.

One fix round per pass. The gaps are printed as one list precisely so they can be closed together. Fixing the first one and calling again spends a pass to be told what the block already said. The command counts its own passes (budget 3, --budget N to change) and says so on every line of output; when it prints BUDGET EXHAUSTED the remaining gaps get recorded with their IDs, not chased.

Do not substitute the individual commands for the helper. A separate vitest run, validator call, coverage run and stryker run is the exact shape this replaces. Measured 2026-08-21 across 28 runs writing tests for one file: turn count tracks how many times a run re-issued a verification command it had already issued — 0 repeats → 30-111 turns, 9-14 repeats → 193-321, 28-30 repeats → 395-485 — with no gain in mutation kill at the top end. Two of the three most iteration-heavy runs ended with a suite that fails on unmutated source.

"Helper absent" has exactly one test: [ -x ~/.zuvo/verify-tests ]. Nothing else counts. An empty $ZUVO_BASE, an unfamiliar install path, cat-ing the file and not recognising it, or a guess about the harness are NOT evidence — the helper resolves its own install root ($ZUVO_BASE, then ~/.zuvo-plugin, then ~/.claude) and says so if it cannot. Measured on the rig 2026-08-21: three of five runs read ZUVO_BASE= as "the helper will not work", took the fallback, and re-issued the separate commands 5-11 times each — in containers where the helper ran correctly on the first try.

So: run it. Only if the file is genuinely not executable, or it exits 2 with a tooling error you cannot fix, fall back to the four commands — ONCE each, in order, stopping at the first failure: target suite → test-coverage-gate.py validate --phase final → scoped coverage → mutation. Record verify: degraded (<the exact error text>). A fallback without that error in hand is gate substitution; the four separate commands are precisely the shape this step exists to remove.

A row that cannot be closed because required infrastructure or a cross-module contract is unavailable: persist Status=BLOCKED_INCOMPLETE with the uncovered rows + reason; do not continue to Step 3.5, do not mark PASS, do not print WRITE-TESTS COMPLETE. Lack of time, a large method count, or an already-high test count are not valid waivers.

Step 3: Verify (quality only — every executable check ran in Step 2.5)

Context resume guard: after a compaction resume, prior-session claims ("clean blind audit") are UNVERIFIABLE — only tool output in the current window counts; re-run Steps 3.5/4 for the current file pair.

  1. Anti-tautology check: grep for mock-return-echoed-in-assertion; every expected value spec-derived. Exception: THIN pure delegation — echo + CalledWith IS the behavioral test (P-70 does not apply). 1b. COMPONENT interaction gate (files that passed the Worth Gate — a component that only passes callbacks through unchanged is NO_BEHAVIOUR and has no test to gate): production routes or wraps callbacks + test has 0 fireEvent/userEvent → STOP, add flow tests. Every owned handler-routing decision gets ≥1 interaction test proving the right handler fires and the competing one does not. Render/label-only assertions never satisfy Q3/Q14 for routing rows.
  2. Q1-Q25 self-eval per quality-gates.md, with per-gate test-file:line evidence for the critical gates:
    Self-eval: Q1=1 Q2=1 ... → [N]/[applicable] [PASS|FIX|REWRITE]
    Critical gates: Q7=[0|1] Q11=[0|1] Q13=[0|1] Q15=[0|1] Q17=[0|1]
    Q7 error-path proof (or evidence-backed N/A), Q11 per-branch tests, Q13 real-module import line, Q15 content-not-shape assertion, Q17 non-echoed oracle. No citable line = score 0. No tier-based waivers anywhere in this pipeline: every file passes the same gates — validator, coverage, probes, blind audit, adversarial. Savings come from isolation, batching and provider caps, never from skipping verification for "simple" files (a conditional skip is a loophole the classifier will be pushed through). Step 3 passes only with Q7=1, Q11=1, Q13=1, Q15=1, Q17=1 — a zero critical gate is BLOCKED_INCOMPLETE, never a publishable partial result. Q7/Q11 were already scored and written into the manifest before Step 2.5 ran, so there is nothing to sync back and no reason to run the verifier a second time for bookkeeping. If this step genuinely CHANGES a critical gate's value, that is a test change — fix the tests and spend one verify pass on the result, not a pass on the manifest edit alone.

Step 3.2: Target Coverage — already measured

verify-tests runs coverage scoped to the production file and prints the four numbers against statements >= 85% branches >= 75% functions >= 90% lines >= 85%, listing any shortfall (and the uncovered lines) as gaps in the same block. There is no separate coverage invocation.

For COMPLEX files the binding rule remains zero uncovered public functions — the percentages supplement, never replace, the gate. A runner with no coverage support is reported as a SKIP with its reason, which is not a failure.

Step 3.3: Mutation — native first, probes for what it cannot express

verify-tests already ran the native mutation runner and printed the score, the mutant counts and the highest-risk survivors (no-coverage mutants first, capped at 5). That number is the primary measurement: on a 200-line file a scoped StrykerJS run produced 235 mutants in ~71s of CPU and zero conversational turns, against 3-6 hand probes that each cost a turn and cover less.

The helper scopes BOTH sides — --mutate to the production file, and the TEST RUN to the spec just written, via a generated runner config (vitest and jest both supported; the jest one inherits the project's package.json#jest / jest.config.js and anchors testMatch on <rootDir>, since Stryker executes in a sandbox copy where an absolute path matches nothing). Scoping only --mutate is why a native run dies on any repo with pre-existing red tests: the dry run executes whatever the project config includes, and that failure reads like a broken mutation setup rather than an unrelated failing suite.

Measured 2026-08-21 on both stacks: vitest (tgm-survey-platform, 200-line file) 235 mutants in ~71s; jest (NestJS shape, rootDir: "src", config in package.json) 31 mutants in ~22s. A jest.config.ts or .mjs cannot be inherited by the generated CommonJS wrapper — the helper says so and skips rather than guessing at its contents.

When no runner is available, the helper installs one workspace-locally with npm install --no-save (writes no file the project keeps — package.json and the lockfile are untouched) and reports stryker workspace-local. --no-install suppresses that; the install failing is reported as error: ..., never silently skipped.

Hand probes remain the floor for the classes a native mutator does not generate — error-path removal, state mutation, async hazards (test-mutation-probes.md classes 2-4). Run them when the helper reported mutation SKIP (no runner exists here) or error, or when the classification names a behaviour group the runner cannot reach.

How to run them, when you do: 3 probes (STANDARD) / 5 (HEAVY-COMPLEX, ≥1 per behavior group), byte-restore protocol, scoped runs. Execute ALL probes as ONE script in ONE tool call (mutate → run → restore in a loop, emitting only the final table) — per-probe conversational turns re-bill the whole prefix for zero information. Post-restore sha256 must equal the manifest hash.

mutation DEFER is not SKIP, and it is not a reason to run probes. DEFER means the helper is holding the measurement back until the suite is final and will run it on a later pass; there is nothing to substitute for. Reaching for hand probes on a DEFER duplicates work that is about to happen for free — measured at 677s over 24 turns in one run, 29% of its wall.

native: has four states, and collapsing them hides an infra failure as normal absence: <score>% (<runner>) when a run produced a number, deferred when the helper is waiting for the suite to be final, none when no runner exists and none could be installed, error: <reason> when a detected runner could not be scoped, timed out, or exited non-zero.

Triage before fixing. Native runners generate equivalent mutants routinely (x * 2 → x << 1). An equivalent mutant is recorded equivalent with the reason and does NOT block completion — the same triage as zuvo:mutation-test § 4.2. Only a survivor triaged gap is a gap.

A perTest survivor is unconfirmed, and the verdict still counts it. Under the default coverageAnalysis: perTest, Stryker can attribute a mutant to the wrong tests (module-level code is the known case), so each printed survivor's gap ends with the ~/.zuvo/mutation-survivor-reprobe.sh command that applies it physically and runs the suite (the rest are in <manifest>.survivors.json). Run it with a --test-cmd that runs these specs, then record its output: ~/.zuvo/verify-tests --manifest <m> --record-reprobe <file> (or - for stdin). SURVIVED makes the row confirmed and KILLED makes it refuted; an inconclusive probe (ERROR, or restored=no, which also warns and exits 3: the production file was left mutated, so restore it first) leaves it unconfirmed with the reason. Only an unconfirmed row moves, once. These are labels, not a verdict: a refuted survivor is triage evidence, never a pass. The mutation check still reports FAIL, and only the triage above (the survivor recorded with its reason) lets the file complete.

The full survivor list is already on disk — never re-run the mutator to get it. The block prints the five highest-risk survivors; every one of them, with the boundary obligation on its line, is written to <manifest>.survivors.json and the path is named in the block. Measured on the rig: one run rebuilt its own Stryker config and re-ran the entire mutation pass to recover that list — 998 seconds across 14 turns, 24% of its wall, recomputing a report the helper had already produced.

Context discipline: once the block is printed, the per-mutant diffs and runner output are spent — carry only the table forward.

Step 3.5: Blind Coverage Audit

Read ../../shared/includes/blind-coverage-audit.md now — it is the audit protocol. The invocation (via the 3-provider panel), its exit codes, and the fallback agent are defined in test-reviewer-routing.md (already loaded, "Blind-audit invocation (Step 3.5)"); follow it exactly, run the Bash call with timeout: 600000 (per-provider timeout is 480s by default, whole-run deadline ≈555 s by default, never above 585 s — the 600000ms call always outlives it), and on exit 0/3 print the merged block's Audit panel: line after the run and again in the final block. If Claude Code moves the call to the background because it ran long: that is expected, not a failure — wait for the completion notification and read the output file; never report BLOCKED_INFRA on a background move alone.

Strict contract-blind isolation is required for a passing audit. The audit is production-first (inventory → ownership → evidence mapping → verdict CLEAN|FIX|REWRITE → one highest-value missing test). Thin delegators audited on forwarding contract only; barrels out of scope; rendered a11y fallbacks are owned behavior.

Pass budget: max 2 — and it counts FIX/REWRITE ITERATIONS, not invocations. Pass 1 audits current files; FIX → patch tests, rerun tests, rerun audit once; REWRITE → rewrite from Step 2, rerun Step 3 chain, audit once. Still FIX/REWRITE after pass 2 → FAILED, record the blind-audit-budget process item once per run, by the first file that exhausts the budget, and name every such file in its report entry ("Filing findings": a gate item is one per run), and the open gaps as a whole-file TEST-GAP (--line 1, a "Whole file: …" title, the gaps in --evidence "$E"), no Step 4.

A confirmation pass forced by the freshness guard is not an iteration. When Step 4 or Step 4.5 changes the pair — an adversarial fix, a production defect repaired in-run — the freshness guard below invalidates the prior CLEAN by construction. Re-auditing then is not the audit↔rework loop this budget exists to stop; it is the only way the shipped pair is one an auditor ever saw. So:

  • A pass whose trigger is a changed normhash/production sha from a later step does NOT consume budget. Run it, and label the result clean:strict (post-adversarial, pass N — freshness re-audit, disclosed).
  • At most one such re-audit per step invocation that changed the pair — each Step 4 adversarial pass that led to an edit, plus Step 4.5 if it repaired production. Step 4 is itself capped (at most 3 passes, per its tier table), so the total is bounded at 4, not open-ended. An earlier wording said "one per adversarial pass", which left the Step 4 + Step 4.5 case — both changing the pair in one run — without a rule.
  • A pass whose trigger is a FIX/REWRITE verdict always consumes budget. That is the loop the cap is for.

This resolves a contradiction that was hit repeatedly: Step A2 mandates re-running the validator for every file it modified, while the 2-pass budget read as FAILED the moment it did. Following both literally forced a run to either overrun the budget or ship a pair no auditor had seen.

Driver exit / verdictWhat happens (Step 4)coverage.md valueResume
0Audit panel: strict + CLEAN (≥2 valid panel answers) — proceedclean:strictresume at Step 4 if adversarial missing
3Audit panel: degraded + CLEAN (exactly 1 valid panel answer) — proceedclean:degradedadversarial compensates
FIX pass 1 (any panel state)Block; patch + rerun oncefix:<n>resume at Step 3.5
REWRITE pass 1 (any panel state)Block; rewrite, Step 3 chain, rerun oncerewriteresume at Step 2
FIX/REWRITE pass 2No Step 4; FAILED, Adversarial=blockedfix:<n>/rewriteskip once recorded
1no provider lane after exclusion — fall back to blind-coverage-auditor (or blind-coverage-auditor-alt on the review-alt route — see test-reviewer-routing.md); its own verdict then follows the rows above: CLEAN proceeds exactly like exit 3 (adversarial compensates), FIX/REWRITE follow their own rowsper the fallback's own verdict, per the rows aboveper the fallback verdict's row above
2no valid panel answer — fall back to blind-coverage-auditor (or blind-coverage-auditor-alt on the review-alt route — see test-reviewer-routing.md); its own verdict then follows the rows above: CLEAN proceeds exactly like exit 3 (adversarial compensates), FIX/REWRITE follow their own rowsper the fallback's own verdict, per the rows aboveper the fallback verdict's row above
124all providers timed out, or the whole-run deadline fired — fall back same as exit 1/2 (a timeout says nothing about the input; same fallback agent, including the review-alt variant); its own verdict then follows the rows above the same wayper the fallback's own verdict, per the rows aboveper the fallback verdict's row above
125the HOST slept mid-run; providers never had a chance — re-run the panel once; the RE-RUN's exit is then handled by this same table (a second 125 → fall back same as exit 1/2/124)n/a until re-run/fallbackresume at Step 3.5
5empty or unauditable production/test file — fix the input, not a reviewer failuren/a until rerunresume at Step 3.5
6input over the byte cap — fix the input (split/shrink), not a reviewer failuren/a until rerunresume at Step 3.5
Fallback also unavailable (routing-failed; or preflight no-provider/canary-failed where the out-of-band check also finds nothing; or the fallback agent missing/invalid)No Step 4; BLOCKED_INFRA, Adversarial=blocked; record the blind-audit-unavailable process item ("Filing findings")skipped + failure causeskip once recorded

Freshness guard (semantic): before each pass record the production file's sha256 AND the test file's normalized hash (test-coverage-gate.py normhash --file <test>). A result is valid only for that exact pair. A later edit whose normhash is UNCHANGED (comments/whitespace/line-wrap/trailing-comma only — the program proves it) does NOT invalidate a CLEAN; any production sha change or test normhash change does. Never widen this by judgment — the normhash decides, not intent. Emit the exact table schema from blind-coverage-audit.md; summary prose is not enough.

Step 4: Adversarial Review (iterative, complexity-tiered)

Enter only when Step 3.5 returned Audit mode: strict + Coverage verdict: CLEAN. Sequential passes, EVERY available provider per pass (--multi)

ComplexityMax passes
TYPE_CONTRACT1, --mode code not --mode test (the reviewer is judging a type contract,
not test behavior; ask it for illegal states the union still accepts)
THIN1
STANDARD2
COMPLEX2 + 3rd ONLY if pass 2 found high-confidence CRITICAL
~/.zuvo/adversarial-review --multi --mode test \
  --context "STACK: [language+version / test-framework+version]. Code type: [type] [complexity] [testability]. Q-GATES: Q7..Q17. [pass 2+: FIXED/REJECTED/KNOWN lists]" \
  --files "<abs-production> <abs-test>" > zuvo/review.txt 2>&1

Record the verdicts after triage: every finding is fixed, rejected (a false positive) or deferred — record each by its ID: in ONE call, ~/.zuvo/adversarial-review --record-disposition ID-1 fixed --record-disposition ID-2 rejected …, per ../../shared/includes/adversarial-loop.md Step 4.9 (the review lists its IDs on stderr).

  • Pass 4+ on the same rejected finding → fresh context with checkpoint + ≤5-line failure distillation (same rule as Step 2's three-rounds trigger).
  • STACK is mandatory (prevents JS-assumption false positives on PHP/Python).
  • Absolute paths only. Read the FULL captured output — never tail/head as the triage source (a pass-2 CRITICAL was lost to tail -60).
  • Pass 2+: --exclude <prior-provider> when known; context carries FIXED (never re-raised), REJECTED (severity-capped: max re-raise: INFO; escalation above cap auto-ignored), KNOWN.
  • Before rejecting a CRITICAL/WARNING: restate the attack vector in one sentence and verify the rejection defeats the VECTOR, not just the suggested fix; otherwise fix another way or carry as KNOWN.
  • ORCHESTRATOR stub fidelity: route stubs use all(); "stubs don't verify HTTP methods" → REJECT (route-module responsibility).
  • Primary path missing/empty → fallback-local per test-reviewer-routing.md; unroutable → SKIPPED_REVIEW.
FindingAction
CRITICALFix immediately, re-run tests
WARNING ≤10 linesFix immediately
WARNING >10 linesRecord with file:line ("Filing findings")
INFO on security-context file (lines 1-5 contain CQ4,CQ5,GDPR,PII,auth,token,password,secret)Treat as WARNING; a rejection is itself recorded ("Filing findings")
INFO otherwiseKnown concerns (max 3), rejectable with justification
0 findingsEarly exit — clean
Unresolved CRITICAL after final passFAILED; record it ("Filing findings")
No provider on all passes + no fallback-localSKIPPED_REVIEW

High-confidence production bugs found here: verify against source, then route to Step 4.5 — fix in-run, never FAILED-and-hand-off for an in-scope fix. Context discipline: carry forward only the verdict + findings list; the audit transcript is spent once findings are extracted (the adversarial review in this same step follows the rule too).

Step 4.5: Fix surfaced production bugs (in-run)

Follow test-bugfix-protocol.md: fix-scope (not severity) decides fix-now-vs-escalate; stacked commits (characterization → fix + flipped regression); terminal state PASS, not FAILED. Production edits change the hash: rebuild affected manifest rows, re-freeze, rerun the Step 2.5 validator, and re-run the blind audit on the new pair.

Both re-runs are mandated by this step, not chosen: if the Step 2.5 budget is spent, call it with --gate-round fix-in-run, and treat the blind-audit re-run as a freshness re-audit under Step 3.5 rather than a budgeted iteration. Neither is a reason to ask for ZUVO_VERIFY_RESET.

Step 5: Log

Update memory/coverage.md:

| File | Status | Metrics | Q Score | Coverage Gate | Blind Audit | Adversarial | Date |
  • Statuses: PASS, FAILED, BLOCKED_INCOMPLETE, BLOCKED_INFRA, SKIPPED_REVIEW, SKIPPED_BARREL, SKIPPED_NO_BEHAVIOUR
  • Metrics: methods N/N, rows N/N, probes N/N (COMPLEX files never log a bare test count as the metric)
  • Coverage Gate: pass, degraded, fail:<n> (verbatim from the validator exit)
  • Blind Audit: clean:strict, clean:degraded, fix:<n>, rewrite, skipped
  • Adversarial: clean, clean:fallback-local, clean:fallback-local:possibly-same-model, <n> findings, <n> findings:fallback-local, <n> findings:fallback-local:possibly-same-model, skipped, blocked, not_run — the :possibly-same-model forms exactly when Step 4 actually RAN fallback-local on routing_status=unknown-writer-model from its own --fallback router call (test-reviewer-routing.md Step 4); skipped, blocked and not_run never carry it — no fallback-local ran
  • Q Score persisted durably: <score>/<applicable> (Q7=?,Q11=?,Q13=?,Q15=?,Q17=?)

SKIPPED_REVIEW is degraded, never silently PASS. BLOCKED_* are non-success — never counted as completed or described as covered. Rows that never enter Step 4 persist Adversarial=blocked/not_run. A file is complete only when Status, Coverage Gate, Blind Audit, and Adversarial are all populated — and Adversarial carries :possibly-same-model whenever Step 4 ran fallback-local on routing_status=unknown-writer-model.

Per-file summary print (adversarial: = the Adversarial value above, verbatim): [status] [file] — methods [N]/[N], rows [N]/[N], Q [N]/[applicable], gate: [pass|degraded|fail], blind: [...], adversarial: [...]

→ NEXT file in queue.


Completion (after queue empty)

  1. Backlog persistence: unfixed out-of-scope issues were recorded through backlog-add where they arose ("Filing findings"); the closing commit follows the Final Test Quality Audit below. 1b. Drive-by findings: follow ../../shared/includes/drive-by-findings.md with <skill> = write-tests — record the problems noticed OUTSIDE the files under test in code the run read (record only), each with ~/.zuvo/backlog-add --repo "$R" --class <C> --conf <N> --source drive-by:write-tests --file "$F" --line <n> [--anchor "$A"] --title "$T" (--anchor "$A" is required for BUG, SEC and DATA and optional otherwise; the variables are assigned as in "Filing findings"), list every SEC one in the completion summary, then ALWAYS run ~/.zuvo/log-drive-by --skill write-tests --sec <a> --bug <b> … (all zeros is an honest outcome).
  2. Knowledge curation per knowledge-curate.md 2b. Content-keyed review artifact (success only): if the run modified any production file (incl. Step 4.5 fixes), write memory/reviews/<base7>..<head7>-<slug>.md with range:/files: headers per review-artifact.md — this run's blind audit + adversarial already reviewed that content, so pipeline gates accept it without a redundant zuvo:review. Skip when only test/docs files changed. If the reviewed production edits are NOT yet committed (no-commit session), HEAD cannot content-key them: write the PROVISIONAL form from review-artifact.md (working-tree blob hashes, status: PROVISIONAL) — it grants no coverage until the upgrade step after commit verifies the blobs and rewrites the range. Never write a normal-form artifact whose head does not contain the reviewed blobs.

Final Test Quality Audit (REQUIRED — separate audit and fix steps, before the retro)

The per-file gates prove coverage; this closing gate proves QUALITY across everything the run touched, graded by the real auditor. Read ../../shared/includes/test-quality-gate.md now and run its sequence — the literal Skill(zuvo:test-audit ...) dispatch with an on-disk zuvo/audits/ report as proof; an inline Q-rescoring pass reported as this gate is a substituted gate = INVALID.

Step A1 — Audit per the include, with:

  • TEST_SCOPE = every test file this run created, extended, or rewrote, PLUS every pre-existing test file covering this run's production files (Step 1 duplicates, sibling suites — the "already weak before we got here" tests).
  • FIX_COMMIT_PREFIX = test(write-tests):

Step A2 — Fix to Tier A per the include (target: Tier A per file, ≥82% + all critical gates; strengthening only, test files only, max 2 fix→re-audit iterations, below-A after the cap = loud WARN with each file recorded once as the include's step 5 says, never silence). Two write-tests-specific additions on top of the include:

  • Tier D (AP13/AP16) on a PRE-EXISTING file is a rewrite, not a patch: route it through this skill's own Step 1 REWRITE path.
  • Fixing tests can invalidate prior evidence: for every file A2 modified, re-run the Step 2.5 validator (line-form evidence may have shifted — prefer path::test name evidence, or run test-coverage-gate.py refresh, to make this a no-op), and re-run one blind-audit pass ONLY when the file's normhash changed per the Step 3.5 semantic freshness guard. The normhash decides, not intent.
  • That re-run is a gate-mandated edit, so if the Step 2.5 budget is spent, claim its allowance rather than asking for a reset: ~/.zuvo/verify-tests --manifest <m> --gate-round test-audit. Use --gate-round adversarial for a Step 4 fix and --gate-round fix-in-run for a Step 4.5 production repair. Each is claimable once; the block names which gate forced it, and the completion block must repeat that.

Print the include's [GATE: test-quality] line and record test_quality=<PASS|WARN|N/A>:<worst tier>:<report path> in the run telemetry.

Closing commit for backlog-add's writes

After the run's last backlog-add call (the per-file steps, the drive-by step above and the include's steps 2 and 5), make the one closing commit, as backlog-protocol.md "Filing from a skill", Committing what backlog-add wrote, says, against the snapshot saved beside the run ledger. This is the only commit that stages a wrote= path: the stacked commits of Step 4.5 and FIX_COMMIT_PREFIX never do. With no path left, there is no commit and the report reads none: nothing to stage. A run entered through --resume-run with a file argument makes no commit; it ends its report with one line, Backlog writes: <path>, … (or Backlog writes: none), and the coordinator's closing commit stages the union of those lines (read from <ledger path>.backlog-writes) and its own wrote= paths. With no saved snapshot found the coordinator stages nothing and lists that union as not committed (no snapshot). A run with --invoked-by <skill> makes no commit either and ends its report with the same Backlog writes: line; the calling skill's closing commit carries those paths.

Retrospective (REQUIRED)

Follow retrospective.md: gate check → structured questions → TSV emit → markdown append. Write the retro BEFORE the terminal report.

  1. Report:
COMPLETION GATE CHECK
[ ] Phase 0: reviewer preflight ran; failures announced at run start (not discovered at Step 3.5)
[ ] Step 1.6/1.7: inventory frozen to zuvo/contracts/ BEFORE writing; freeze validation exit 0 (or recorded degraded)
[ ] Step 2: test file(s) written with Write tool; split rule honored for large files
[ ] Step 2.5: executable validator ran; its output pasted; every public entry point FULL; Uncovered owned rows: 0
[ ] Step 3: Q-score with per-gate evidence (Q7,Q11,Q13,Q15,Q17); manifest Q values synced
[ ] Step 3.2: scoped coverage measured (or runner-lacks-support printed)
[ ] Step 3.3: mutation probes all killed; native runner used if the project has one (or `native: none`); production restored (hash verified)
[ ] Step 3.5: blind audit ran (clean:strict|clean:degraded|skipped|blocked_infra)
[ ] Step 4: adversarial ran (clean|Nfindings|skipped|blocked|not_run)
[ ] Step 4.5: every confirmed in-scope production bug FIXED in-run; escalations loud
[ ] Step 5: coverage.md rows fully populated (Status+Gate+Blind+Adversarial)
[ ] Step A1/A2: [GATE: test-quality] emitted from a REAL zuvo:test-audit dispatch (on-disk zuvo/audits report) over touched + pre-existing in-scope test files; sub-A files fixed (or WARN with each file recorded through backlog-add); validator + blind audit re-run for A2-modified files
[ ] Every finding the run did not fix recorded through backlog-add, each B- or F-id reported or named under NOT PERSISTED (<reason>), at the step that found it; the closing commit made, or none: nothing to stage ("Committing what backlog-add wrote"), or the `Backlog writes:` line printed (a `--resume-run` sub-agent)
[ ] Step 2b: review artifact written IF production files changed
[ ] Drive-by findings recorded per drive-by-findings.md: the `Drive-by: N recorded (…)` line and every SEC in the final summary, and the `~/.zuvo/log-drive-by` receipt written — zeros allowed
[ ] Final test run: all tests pass (N/N)

Any unchecked box → go back and complete that step first. After compaction/resume verify each against actual tool output in the current window, not memory.

WRITE-TESTS COMPLETE
-----
Files tested:  [N] ([M] new, [K] extended, [J] fixed)
Surface:       methods [N]/[N] FULL, owned rows [N]/[N], error paths [N]/[N]
Coverage gate: [N] pass, [M] degraded, [K] fail
Target cov:    st [%] / br [%] / fn [%] / ln [%] (scoped to production files)
Mutation:      [N]/[N] probes killed | native: <score>% (<runner>) | none | error: <reason>
Test audit:    [GATE: test-quality] [PASS|WARN|N/A] tier=[worst] files=[N] ([M] fixed up in-run) report=[zuvo/audits/...]
Below tier A:  [list with blocking findings, or "none"]
Q gates:       [N]/[applicable] avg (critical gates: all pass)
Blind audit:   [N] clean, [M] failed/rewrite, [K] skipped
Validation:    [full-suite|scoped:touched-tests]
Failures:      pre-existing: [N], new in scope: 0
FAILED files:  [list or "none"]
BLOCKED_INCOMPLETE: [list or "none"]
BLOCKED_INFRA: [list or "none"]
SKIPPED_REVIEW: [list or "none"]
SKIPPED_BARREL: [list or "none"]
SKIPPED_NO_BEHAVIOUR: [list with shape, or "none"]
Run: <ISO-8601-Z>	write-tests	<project>	-	<Q>	<VERDICT>	<TASKS>	<DURATION>	<NOTES>	<BRANCH>	<SHA7>	<INCLUDES>	<TIER>
-----

Append via wrapper (REQUIRED). Never >> directly to ~/.zuvo/runs.log:

printf '%b\n' "$RUN_LINE" | ~/.zuvo/append-runlog

Expected stdout: OK: appended to runs.log (retro verified for <skill> on <project>). Exit 2 RETRO_REQUIRED → execute the retro bash first; never ZUVO_SKIP_RETRO_GATE=1. After success print a Logs: evidence line (tail -1 ~/.zuvo/retros.log, grep -c "^<!-- RETRO -->" ~/.zuvo/retros.md, tail -1 ~/.zuvo/runs.log).

Run one final full-suite validation, or explicitly scope the failure count to touched test files before printing new in scope: 0.

Do NOT print WRITE-TESTS COMPLETE if any file is missing Status/Coverage Gate/Blind Audit/Adversarial, has uncovered owned rows, a failing or unrun validator, Q7=0 or Q11=0, a surviving mutation probe, an untriaged native-runner survivor (triaged equivalent or capped-and-recorded per Step 3.3 does NOT block), an undispatched final test audit (Step A1), or status BLOCKED_INCOMPLETE/BLOCKED_INFRA. A file legitimately below Tier A after 2 fix cycles does not block the report — it blocks only its own silent inclusion in the tier-A count. (A run that is DRAFT/BLOCKED_INFRA from preflight reports its terminal block instead — with Q7=1 and Q11=1 still required of every written file, so the tests are sound even though review could not run.)


Resume / Crash Recovery

On start, read memory/coverage.md AND glob $ZUVO_DIR/contracts/*.coverage.json. Old pre-blind-audit rows: normalize once (add empty Blind Audit/Adversarial cells, note legacy-pre-blind-audit), resume at Step 3.5. Rows without a Coverage Gate cell are pre-validator legacy: re-run Step 2.5 against the current files before trusting their status.

StatusBlind AuditAdversarialResume action
PASSclean:strictpresentSkip
FAILEDanyanySkip (recorded, or listed NOT PERSISTED)
BLOCKED_INCOMPLETEanyblocked/not_runResume at Step 2.5, close every uncovered row
BLOCKED_INFRAskippedblockedRe-run preflight; resume at Step 3.5 when infrastructure is back
SKIPPED_REVIEWclean:strictskippedRe-process Step 4 only
SKIPPED_BARRELskippednot_runSkip
SKIPPED_NO_BEHAVIOURskippednot_runSkip
(row absent, manifest status=inventory)--Resume at Step 2 (inventory already frozen — validate freshness first)
(row absent, manifest status=final, validator passes)--Resume at Step 3
(absent entirely)--Process from Step 1

A test file on disk absent from coverage.md = partial run: if it carries the Generated by zuvo:write-tests marker, delete and re-process from Step 1; otherwise assess in Step 1 (ADD TO / REWRITE). A manifest whose hash no longer matches production is stale — rebuild it, never trust its rows. Auto mode re-runs discovery to rebuild the queue.


Principles

  1. Read production code before planning tests. Every assertion traces to real behavior.
  2. Inventory before writing. The public surface is frozen before the first test exists — writing first invites rationalizing the gaps away.
  3. Coverage completeness is proved by a program, not claimed by the writer. The validator's output is the gate; test count is never a progress signal for COMPLEX files.
  4. Test what the code OWNS, mock what it DELEGATES.
  5. ONE file, FULL pipeline. No batching of files or pipeline steps.
  6. Blind coverage audit and adversarial review are separate gates. Step 4 never runs until Step 3.5 is clean, and reviewer availability is proven in Phase 0 — not discovered at Step 3.5.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.