# Write Tests

> 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).

- Skill: `greglas75/write-tests` (Agent Skill, multi-file: 6 files)
- Install (CLI): `npx skillmds@latest add greglas75/write-tests`
- Raw SKILL.md: https://api.skillmd.com/api/skills/greglas75/write-tests/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Product & Planning
- Author: greglas75 (https://skillmd.com/u/greglas75)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/greglas75/write-tests

---


# 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

| Input | Behavior |
|-------|----------|
| `[file.ts]` | Write tests for one production file |
| `[directory/]` | Write tests for all production files in the directory |
| `auto` | Discover uncovered files, process one at a time until done |
| `--dry-run` | Run Phase 0 + Step 1 for all files, print plan, stop |
| `--no-cache` | Re-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 |
| `--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.`

| Include | LIGHT | STANDARD | HEAVY | COMPONENT | TYPE |
|---------|-------|----------|-------|-----------|------|
| `../../shared/includes/test-contract.md` | Full | Full | Full | Full | Full |
| `../../shared/includes/test-blocklist.md` (incl. typed mock gate) | Full | Full | Full | Full | Full |
| `../../shared/includes/quality-gates.md` | Q1-Q25 only* | Q1-Q25 only* | Q1-Q25 only* | Q1-Q25 only* | Q1-Q25 only* |
| `../../rules/testing.md` | Full | Full | Full | Full | **SKIP**§ |
| `../../shared/includes/test-mock-safety-core.md` | Full | Full | Full | Full | **SKIP**§ |
| `../../shared/includes/test-code-types-core.md` | Full | Full | Full | Full | Full |
| `../../shared/includes/test-bugfix-protocol.md` | Full | Full | Full | Full | **SKIP**§ |
| `test-mock-safety-{stack}.md` (js/php/python) | **SKIP** | Full | Full | Full‡ | **SKIP**§ |
| `test-code-types-{stack}.md` (js/php/python) | **SKIP** | Full | Full | Full‡ | Full |
| `../../shared/includes/test-edge-cases.md` | **SKIP** | Full | Full | Full | **SKIP**§ |
| `../../shared/includes/test-mutation-probes.md` | **SKIP**† | Full | Full | Full | **SKIP**§ |

\* **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)

0. **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.
1. **CodeSift setup** per `codesift-setup.md`. Note repo identifier.
2. **Resolve `$ZUVO_BASE`** per `test-reviewer-routing.md` (absolute paths for every script call).
3. **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.
4. **Read production file. Detect stack. Classify. Load includes** per PHASE 0.5 / PHASE 1 above.
5. **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.`
6. **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`, backlog the environment issue. For JS/TS COMPONENT/HOOK targets check DOM-matcher registration and cleanup globality; reuse the exemplar's local pattern when global setup is absent.
7. **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). **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.
8. **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 → backlog one run-level environment issue, 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 — backlog, 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.

---

## 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 (`/clear` in 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 → `FAILED` + backlog `duplicate-test-suite`.

**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`:

```bash
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:

```bash
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".

2. 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.
3. Write the manifest to `$ZUVO_DIR/contracts/<basename>.coverage.json` (`status: "inventory"`, current sha256, NO coverage claims).
4. 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

```bash
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:

```bash
# $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.

**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 backlogged — 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 a

…(truncated)
