Claude Skill

test-roadmap

EXPERIMENTAL. Analyzes a repository and any existing test suite, grades existing tests for weakness, classifies mocks, emits a phased roadmap for building a test suite that catches real regressions, then executes those phases one at a time. Use when planning or building a test su

LLM Mart · 0 points · 14 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download ovid-paad-plugins_paad_skills_test-roadmap-a48b179.zip · 41 KB
Part of ovid/paad — 49 skills

Install

skills CLI npx skills add https://github.com/Ovid/paad/tree/main/plugins/paad/skills/test-roadmap
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install ovid-paad@llmmart
Git git clone https://github.com/Ovid/paad.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole ovid/paad collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

On invocation: announce "Running paad:test-roadmap v1.31.0" before anything else.

EXPERIMENTAL SKILL. Its arguments, output paths, and behavior may change or be withdrawn in any release, including patch releases. It is not covered by the semver guarantees the other paad skills carry. Unlike every other paad skill, this one writes code and commits it — tests, one commit per phase, onto your working branch. Report rough edges at https://github.com/Ovid/paad/issues.

test-roadmap

This file is the router. The routing itself stays dumb on purpose: one check, two routes, nothing else. A couple of preconditions guard it first. All the substance — grading, planning, writing tests, bug injection — lives in references/ and loads only once routing has picked a mode.

Pre-flight and routing:

digraph route {
  "Inside a git repo?" [shape=diamond];
  "STOP: needs a git checkout" [shape=box, style=bold];
  "Detached HEAD?" [shape=diamond];
  "origin/HEAD pointer resolves?" [shape=diamond];
  "Current branch == default branch?" [shape=diamond];
  "Name matches a well-known primary (main/master/trunk/develop/...)?" [shape=diamond];
  "ASK: is this your main development line?" [shape=diamond];
  "OFFER: create a working branch" [shape=box];
  "Developer agrees?" [shape=diamond];
  "STOP: never build on the primary branch" [shape=box, style=bold];
  "git switch -c <name>" [shape=box];
  "paad/test-roadmap/test-roadmap.md exists?" [shape=diamond];
  "Load references/build-test-roadmap.md (Detect, Grade, Plan, Critique, Write)" [shape=box];
  "Load references/execute-test-roadmap.md (next phase, break-it-check, commit)" [shape=box];

  "Inside a git repo?" -> "STOP: needs a git checkout" [label="no"];
  "Inside a git repo?" -> "Detached HEAD?" [label="yes"];
  "Detached HEAD?" -> "OFFER: create a working branch" [label="yes"];
  "Detached HEAD?" -> "origin/HEAD pointer resolves?" [label="no"];
  "origin/HEAD pointer resolves?" -> "Current branch == default branch?" [label="yes (authoritative)"];
  "origin/HEAD pointer resolves?" -> "Name matches a well-known primary (main/master/trunk/develop/...)?" [label="no"];
  "Current branch == default branch?" -> "OFFER: create a working branch" [label="yes"];
  "Current branch == default branch?" -> "paad/test-roadmap/test-roadmap.md exists?" [label="no"];
  "Name matches a well-known primary (main/master/trunk/develop/...)?" -> "OFFER: create a working branch" [label="yes"];
  "Name matches a well-known primary (main/master/trunk/develop/...)?" -> "ASK: is this your main development line?" [label="no"];
  "ASK: is this your main development line?" -> "OFFER: create a working branch" [label="yes / unsure"];
  "ASK: is this your main development line?" -> "paad/test-roadmap/test-roadmap.md exists?" [label="no"];
  "OFFER: create a working branch" -> "Developer agrees?";
  "Developer agrees?" -> "STOP: never build on the primary branch" [label="no"];
  "Developer agrees?" -> "git switch -c <name>" [label="yes"];
  "git switch -c <name>" -> "paad/test-roadmap/test-roadmap.md exists?";
  "paad/test-roadmap/test-roadmap.md exists?" -> "Load references/execute-test-roadmap.md (next phase, break-it-check, commit)" [label="yes"];
  "paad/test-roadmap/test-roadmap.md exists?" -> "Load references/build-test-roadmap.md (Detect, Grade, Plan, Critique, Write)" [label="no"];
}

Before routing: confirm you're in a repo

compatibility: Requires git above means this skill needs a working git checkout — build mode fans out grading subagents against the tree as it stands, and execute mode's break-it-check gate runs bug injection in a disposable git worktree. If the current directory isn't inside a git repo, say so and stop before loading either mode file.

Before routing: confirm you're on a working branch

This skill commits as it goes — build mode commits the roadmap, execute mode commits each phase's tests — all onto the branch you are on right now. A half-built test suite landing on the developer's main development line is exactly what this check prevents, the same spirit as running a code review on a feature branch rather than on main. So before routing, confirm the current branch is a working branch, not the primary one.

Identify the primary branch from repo signals, in order — stop at the first that decides:

  1. Detached HEAD — git symbolic-ref -q HEAD prints nothing. There is no branch for the suite to accumulate on at all; treat it like being on the primary branch and offer a working branch (below).
  2. The repo's own default-branch pointer — git symbolic-ref -q --short refs/remotes/origin/HEAD resolves to e.g. origin/main; strip the remote prefix for the default branch name. If the current branch (git symbolic-ref -q --short HEAD) equals it, you are on the primary branch. This is the authoritative signal and needs no built-in list of names.
  3. No such pointer (a local-only repo, or one where it was never set) — fall back to the well-known primary names: main, master, trunk, develop, devel, and the like — examples, not a closed list, the same stance Stage 1 takes on manifests. If the current branch name matches one, treat it as primary. If it matches none and step 2 could not confirm, ask the developer once, in plain words, whether this is their main development line — never silently proceed on a branch that might be it. Being wrong toward asking costs a keystroke; being wrong toward building on the main line is the harm this check exists to prevent.

When the current branch is the primary one (or HEAD is detached), do not route yet. Say why in plain words — "I build the test suite up commit by commit, and you don't want those landing on your main branch while it's half-done, so let's put them on a working branch" — then offer to make one: propose a name (test-roadmap is a fine default), and on the developer's OK run git switch -c <name> (or git checkout -b <name> on older git) and continue to routing. If they decline, stop — never build or execute on the primary branch.

This is the skill's one branch: a single working branch, created at invocation only when needed. It is not a per-phase branch — execute mode still commits every phase onto whatever working branch you are on, and the suite accumulates there (see references/execute-test-roadmap.md § What execute mode writes). Both mode files assume this check has already passed and never re-run it; the guard lives here, once.

Route

paad/test-roadmap/test-roadmap.md exists?  →  load references/execute-test-roadmap.md
                       absent  →  load references/build-test-roadmap.md

That is the entire routing logic — one file existence check, two branches. paad/test-roadmap/test-roadmap.md is the roadmap this skill itself writes at the end of build mode, so its presence is exactly the signal that a previous run already did Detect/Grade/Plan/Critique/Write and there is a phased plan to execute against. Its absence means this is either the first run against this repo, or a run after that file was deleted — either way, build it.

If it ever grows a third condition, that is a signal something has been put in the wrong place — take it back to build-test-roadmap.md or execute-test-roadmap.md, not to this file.

This router never loads references/break-it-check.md, references/test-pushback.md, or references/test-theater.md directly. Those three are loaded by whichever of the two mode files needs them, at the point in their own protocol that needs them — not from here.

Entering build mode

Before build mode can plan anything, it needs to know what it's planning for. The first thing it does — Stage 1, Detect — is identify the stack from manifests and config rather than assumption (package.json, pyproject.toml, go.mod, Cargo.toml, Gemfile, and so on: examples, not a closed table), then determine how tests are invoked and what test files already exist. The skill must never hardcode a language; where it needs a per-ecosystem fact, it looks for the signal in the repo instead of consulting a built-in list.

The full five-stage protocol — Detect, Grade, Plan, Critique, Write — lives in references/build-test-roadmap.md. Load it now if paad/test-roadmap/test-roadmap.md is absent.

Entering execute mode

Load references/execute-test-roadmap.md now if paad/test-roadmap/test-roadmap.md exists. It reads that file's ## Decisions section once, selects the next phase per the completion protocol, and gets on with writing tests — it does not re-detect or re-ask anything build mode already settled.

Files (paad)
  • references
    • break-it-check.md 17 KB
      # break-it-check — the mandatory anti-theater gate
      
      **This gate is mandatory. No phase latches without it.** Execute mode's step 4
      is `break-it-check`, invoked between "write the tests for this phase" and
      "write `Landed:` and commit" — nothing skips it, and nothing writes `Landed:`
      without it passing.
      
      It discharges *"the test suite should fail loudly when you fix bugs."* For
      each behavior named on the phase's `Catches:` line: inject that bug into a
      **disposable copy** of the production code, confirm the new test goes red,
      discard the copy.
      
      The mechanism is adapted from `verification-before-completion/SKILL.md:84-88`:
      
      ```
      Write → Run (pass) → Inject bug → Run (MUST FAIL) → Discard copy → Run (pass)
      ```
      
      with two inversions from that skill: there is no fix to revert — the bug is
      **injected** into working code — and the injection happens in a throwaway
      `git worktree`, so nothing in the developer's tree is ever at risk (Inviolate
      #2: production code is never permanently modified).
      
      **When you tell the developer what you're doing, use plain words — never
      "break-it-check," "the gate," "mutation," or "latch"** (see
      `references/test-pushback.md § Talking to the developer`). For example: *"Before
      I call these tests done, I'll make sure they actually work — I'll slip one
      realistic bug into a throwaway copy of the code and confirm the tests catch it,
      then throw the copy away. Your real code is never touched."* What each possible
      result means for the developer is spelled out in *Explaining results*, below.
      
      ## Protocol
      
      Six steps, in order. The order is load-bearing — steps 1 and 2 each protect
      against a failure mode that only shows up if they run first.
      
      1. **Sweep, then create one worktree for the phase.** First fix this run's
         identity: run `echo "$(date +%Y%m%d-%H%M%S)-$$"` **once**, at the first
         `break-it-check` of the session, and reuse the exact string it prints —
         shape `<YYYYMMDD-HHMMSS>-<pid>` — as `<run-id>` for every phase after it.
         **Run the command; never invent an id, and never copy one out of any
         example.** Two runs holding the same id sweep each other's live worktrees,
         which is the failure the id exists to prevent. Never regenerate it per
         phase either: a run that changes its own id stops recognizing its own
         strands.
      
         Then reclaim strands left by a crashed or aborted prior run:
      
         - `git worktree prune` — reclaims records whose directory is gone or
           unreachable; it never deletes checked-out files.
         - Enumerate `git worktree list --porcelain` and force-remove a worktree
           **iff one of its path components is exactly `paad-test-roadmap-<run-id>`**
           — split the path on `/` and compare components for **equality**. Never a
           substring or "contains" test: run-ids differ only by PID, so a sibling's
           `paad-test-roadmap-20260726-141133-481712` *contains* your
           `paad-test-roadmap-20260726-141133-4817`, and a contains-test
           force-removes that sibling's live worktree mid-mutation. Never match on a
           `${TMPDIR:-/tmp}/…` prefix you construct either: git records the
           symlink-resolved path (`/var/…` prints as `/private/var/…`) and `$TMPDIR`
           already ends in `/`, so a constructed prefix matches nothing and the sweep
           silently becomes a no-op.
         - **Do not add an age or mtime arm to catch what run-id scoping leaves
           behind.** The worktree is created once and reused for the whole phase, so
           its mtime does not move while a slow suite runs — any threshold you pick
           force-removes a live phase mid-mutation, which is that same failure
           rescheduled onto a timer. *Why a disposable worktree* covers what happens
           to those strands instead.
      
         Then create the phase's worktree. Name the directory `phase-<N>` — never the
         roadmap's phase title, which is prose (`Phase X of Y`) and would silently
         nest if it ever contained a `/`:
      
         ```
         git worktree add "${TMPDIR:-/tmp}/paad-test-roadmap-<run-id>/phase-<N>" HEAD
         ```
      
         That worktree is **reused across the baseline run and every mutation in the
         phase** — one per phase, not one per mutation. This is structural, not an
         optimization to drop under time pressure: a fresh checkout has none of the
         gitignored build artifacts (`node_modules/`, `target/`, venv) that the skill
         cannot language-agnostically reinstall, so the cost is paid once per phase
         rather than once per mutation. See *Why a disposable worktree* for the full cost.
      
      2. **Copy the phase's new test files into the worktree, *then* mutate.**
         Never the other way around. In colocated-test ecosystems — Rust's
         `#[cfg(test)] mod tests` is the reproduced case — the test file *is* the
         production file. If the mutation were applied first and the test copied
         in second, the copy would silently overwrite the mutation: the test would
         stay green, and the gate would misread a mutation that was never actually
         applied as a theater test. Copy-then-mutate is the only order that can't
         erase its own injected bug.
      
      3. **Baseline run.** Run the phase's own tests, unmutated, in the worktree.
         They **must pass**, and the result must be **stable across two runs**. See
         the *Failure table* below for what each way of failing this step means —
         do not proceed past a baseline that isn't clean green and stable.
      
      4. **Per behavior on `Catches:`, inject one mutation via a blind mutator
         subagent.** The subagent receives:
      
         - the `Catches:` behavior text,
         - the production file,
         - the worktree path,
         - the operator list (below),
      
         and explicitly **not the test**. It returns one hunk. The main agent
         applies the **first** hunk returned, runs the phase's own tests, and
         checks for red. There is no re-rolling for a more convenient hunk — see
         *Constrained mutation* for why.
      
         Blinding the subagent to the test is the point: a subagent that can see
         the test could tune the mutation to whatever the test happens to check,
         which reintroduces the exact failure mode this gate exists to catch — the
         same author writing both the test and the thing meant to indict it.
      
      5. **`git worktree remove --force`.** `--force` is required, not optional —
         the worktree is unclean by construction (the copied-in test is untracked,
         the injected mutation is a tracked-file modification), and a bare
         `git worktree remove` refuses an unclean worktree, telling you to pass
         `--force`. There is nothing to restore, nothing to stage, no fence to
         satisfy first: this is only the fast path. Step 1's sweep is the
         guarantee — see *Why a disposable worktree* below.
      
      6. **Commit only after the check passes** — and after execute mode's step 5
         clean-run gate also passes — on the developer's real tree. This gate proves
         the phase's tests catch a bug; that one proves they run clean in the full
         suite on the branch. Both precede the commit. The operator name(s) used here
         are echoed to the transcript and recorded on the phase's `Landed:` line as
         provenance.
      
      **"Run the test" means the phase's own tests, not the whole suite**, at every
      step above. Mutating production code *should* break unrelated tests — that's
      expected, not signal — so there's no reason to run the full suite here, and a
      legacy suite's pre-existing failures stay out of frame.
      
      ## Constrained mutation
      
      The injected mutation is not free-form. It is a single change drawn from a
      named operator set, and the subagent must **declare which operator by
      name** when it returns its hunk:
      
      - flip a comparison (`<` ↔ `<=`, `==` ↔ `!=`)
      - off-by-one a boundary
      - negate a condition
      - alter a constant
      - drop a state transition
      
      **Explicitly banned, regardless of operator name:**
      
      - an early `return` / `raise` / `throw` at function entry
      - removing a whole function body
      
      These two are banned by shape, not by judgment call, because they are
      sledgehammers: either one makes *any* test go red — including a theater test
      like `assert result is not None` — which would let a genuinely worthless test
      pass the gate for the wrong reason. "Keep the mutation small" is a
      rationalizable instruction; "no early return at function entry" is a concrete
      line an agent cannot talk itself around. Requiring the operator to be named
      turns a dishonest mutation into an affirmative written claim instead of a
      silent choice.
      
      **If the first hunk leaves the test green, that is the theater row in the
      table below — rewrite the test. Do not re-roll the mutation.** Re-rolling
      would let the main agent shop for a hunk that happens to turn the test red,
      which quietly restores the identity the blind-mutator subagent exists to
      break: one author controlling both the test and the thing meant to indict
      it. A test that survives one honest, named mutation attempt on the behavior
      it claims to cover has already told you what it needs to tell you.
      
      ## Failure table
      
      Five outcomes. Each is a different failure of a different precondition the
      gate depends on, and each has one correct action — never "fix the code" and
      never "latch anyway."
      
      | Outcome | Meaning | Action |
      |---|---|---|
      | Baseline red (fails unmutated, run alone) — but green in a full-suite run | **Order dependence**, not a mischaracterization: the test relies on state another test sets up. The isolated gate cannot judge it. | **Surface it. The phase cannot use the isolated gate as-is.** Do not latch, do not "fix" the code. |
      | Baseline red (fails unmutated) — and also red in a full-suite run | The test does not describe current behavior — **mischaracterization**. | **Rewrite the test.** Do not fix the code, do not latch. |
      | Baseline result unstable across two runs | The test is **flaky** — its verdict is noise, so the gate cannot judge it. | **Surface it.** Do not latch. |
      | No code path implements this `Catches:` behavior (no valid site to inject) | The phase was planned against code that has since moved or vanished. | **Phase invalidated — re-plan it.** Do not latch. |
      | Mutation applied, test stayed green | **Theater** — the test does not catch what the phase claims. | **Rewrite the test.** Do not latch. Do not re-roll the mutation. |
      
      **"Run the test" in every row above means the phase's own tests, not the
      whole suite.** The order-dependence and flaky rows exist because the
      isolated gate silently *depends on* the phase's tests being isolable and
      deterministic — this table is where those two preconditions get checked,
      and where a violation of either gets named and surfaced rather than papered
      over. Naming the violation is the whole of this gate's engagement with
      suite health; it detects the two failure modes, it does not repair them.
      
      The "no code path" row is the design's staleness detector: a bug cannot be
      injected into a code path that has moved or vanished, and no phase latches
      without passing this gate — so a stale plan gets caught here, not silently
      latched against a `Produces:` path that no longer means what it once did.
      
      ## Explaining results to the developer
      
      The table's labels — "theater," "mischaracterization," "order dependence,"
      "flaky," "invalidated" — are for the agent. Tell the developer what the result
      means for their tests, in plain words (§ Talking to the developer):
      
      | Outcome | What to say to the developer |
      |---|---|
      | Mutation applied, test stayed green (*theater*) | *"These tests still passed after I deliberately broke the code they're supposed to check, so they aren't actually testing that behavior. I'll rewrite them so they catch it."* |
      | Baseline red, also red in full suite (*mischaracterization*) | *"These tests disagree with how the code behaves right now. Since the job here is to pin down current behavior, I'll fix the tests to match what the code actually does."* |
      | Baseline red alone, green in full suite (*order dependence*) | *"These tests only pass when run together with others — run on their own they fail — so I can't confirm them in isolation. That's worth cleaning up before relying on them."* |
      | Baseline unstable across two runs (*flaky*) | *"These tests give different results on repeat runs, so I can't trust the outcome yet. They need to be made reliable first."* |
      | No code path implements `Catches:` (*invalidated*) | *"The code this set of tests was planned around has moved or been removed, so the plan for it is out of date and I'll redo it."* |
      
      ## Why a disposable worktree, not an in-tree mutation
      
      An earlier draft mutated the developer's working tree directly and protected
      it with an elaborate write-fence: a whole-tree `git status --porcelain`
      snapshot compared before and after each mutation, plus a
      `git add`-before-`git checkout` staging dance to survive `git checkout`'s
      restore-from-index behavior. Roughly forty lines of protocol existed solely
      to make an in-tree mutation survivable — and it still had a window: a crash,
      context exhaustion, or a `--watch` runner mid-mutation could leave altered
      production code on disk, caught only on the *next* run, after the developer
      already had it in front of them.
      
      The worktree deletes the problem instead of fencing it. The mutated copy is
      disposable, so there is no invariant left to protect: no snapshot
      comparison, no staging, no path-based fence — and the colocated-test hazard
      that broke every such fence (in Rust/Zig/D the test file *is* under `src/`)
      becomes irrelevant, because you're mutating a copy you're about to throw
      away regardless.
      
      **Why `$TMPDIR` and not somewhere under the repo.** Nothing this gate creates
      may land in the working tree: a second full checkout under the repo root is
      visible to test discovery, linters, the `find`/`grep` recon in this plugin's
      own skills, and `git status` — and the skill cannot exempt it, because it does
      not edit the developer's `.gitignore`. An earlier draft used
      `.git/test-roadmap-worktrees/`, which satisfied that property but hard-fails
      wherever `.git` is a *file* rather than a directory: a `git worktree` checkout,
      a submodule, a `--separate-git-dir` repo. `$TMPDIR` keeps the
      nothing-in-the-working-tree property, behaves identically in all three of those
      repo shapes, and gives the sweep a fixed path component to match on.
      
      The **run-id** in that path is what keeps one run's sweep off another's live
      worktree. This is not hypothetical caution about a rare double-booking: a
      sibling's sweep force-removing a live worktree out from under a running
      mutation **has been reproduced**. Two agents on one repo is not exotic — a
      developer can open a second session while one sits mid-phase, and both modes
      of this skill are built to resume across sittings. Path prefix alone
      identifies "worktrees this skill made," not "worktrees belonging to *my*
      run." The `paad-test-roadmap-` half of the component is what keeps the
      sweep off the developer's *own* hand-made worktrees; the run-id half is what
      keeps it off a concurrent session. Both live in the path, so both are
      answerable from `git worktree list` output alone.
      
      The same kind of crash the write-fence could not survive can still strand
      the **worktree itself** — the disposable checkout, plus its
      `.git/worktrees/` record — on any exit that skips step 5: a failure-table
      short-circuit, context exhaustion, a `--watch` runner, a hard kill. That is
      hygiene, not a safety gap — the developer's tree is never touched either
      way, so Inviolate #2 holds regardless of whether step 5 ever runs — but the
      leak this time is closed the way the write-fence's window was **not**: on
      the next entry, never on the current exit. Step 1's sweep (`git worktree
      prune`, plus a force-remove filtered to test-roadmap's own path component
      **and** this run's id) runs on the one path a crash cannot skip — the start
      of the next phase — so a strand left by a short-circuited phase dies when
      the session next enters the gate, instead of persisting for the rest of the
      run.
      
      A strand left by a session that died *outright* is out of the run-id sweep's
      reach by design, and what reclaims it is `$TMPDIR` — eventually, with an
      honest ceiling: `systemd-tmpfiles` defaults to ten days on Linux, many
      container and CI images run no cleaner at all, and a project-local `$TMPDIR`
      may never be swept. So "the OS reclaims it" can mean "not for ten days" or
      "never." That is accepted rather than fixed because the residue is bounded and
      non-destructive: disk held by a checkout, plus a phantom row in `git worktree
      list` until the directory does go away and `git worktree prune` clears the
      record. The developer's tree is untouched either way, which is the property
      this design actually protects — and every mechanism that would reclaim it
      sooner (an age arm, an unscoped prefix sweep) buys that back by risking a live
      sibling's worktree. On-exit removal (step 5) is only the fast path;
      the sweep is the guarantee, because no on-exit cleanup survives a crash this
      design already declined to trust once.
      
      The cost, stated honestly: a fresh worktree starts with no `node_modules/`,
      no `target/`, no venv, no `_build/`. Some ecosystems need a full dependency
      install before their tests can even run, and that can cost minutes. This
      skill cannot know the install command stack-agnostically, so the mitigation
      here is structural, not a lookup table — one worktree per phase, reused
      across every mutation in that phase (protocol step 1) — never a built-in
      per-language command table.
      
    • build-test-roadmap.md 25.7 KB
      # Build mode — the five stages
      
      Loaded when `paad/test-roadmap/test-roadmap.md` does not exist yet — the first run against a
      repo, or any run after that file was deleted. Five stages, run in order, each
      handing a concrete artifact to the next: Detect, Grade, Plan, Critique, Write.
      
      ## Stage 1 — Detect (main agent)
      
      Identify the stack from manifests and config rather than assumption —
      `package.json`, `pyproject.toml`/`setup.cfg`, `go.mod`, `Cargo.toml`, `Gemfile`,
      `pom.xml`/`build.gradle`, `composer.json`, `cpanfile`/`Makefile.PL`, `*.csproj`,
      `mix.exs`, and so on. This list is examples, not a closed table — the skill must
      never hardcode a language. Where it needs a per-ecosystem fact, it looks for the
      signal in the repo rather than consulting a built-in list.
      
      Detect **four** things, all from repo signals:
      
      1. **How tests are invoked, and what test files exist.** Read the manifest's test
         script/target and enumerate the test files it would run.
      2. **Whether a coverage tool is available.** Used read-only, later, in Stage 3 for
         gap-finding — **never as a quality signal**. Where none is detectable, Stage 3
         falls back to agent judgment reading source, and says so in the plan rather
         than silently proceeding as if coverage output existed.
      3. **Whether a test-data mechanism exists** — fixtures, factories, seed scripts,
         testcontainers, a `conftest.py`, a `factories/` or `fixtures/` directory. This
         feeds Stage 3: an integration or e2e phase that needs constructed data cannot
         be planned honestly against a repo with no way to construct it.
      4. **How this repo separates test tiers, and the per-tier run + coverage
         command.** The three tiers — unit, integration, e2e — must end up
         *distinguishable* and *independently coverage-runnable* (a stated requirement,
         see *The three tiers must be independently runnable*, below). The mechanism
         that makes them so is **not universal** — it is a *selector* the ecosystem
         already expresses, and it varies: a test directory (`tests/unit/`,
         path-selected), a filename marker, a test tag/marker (`pytest -m`, Go
         `//go:build`), a build target (`cargo test --lib`/`--test`, a `.csproj` or
         Jest project), or a test label (CTest `-L`). Detect which the repo uses, and
         derive per tier the command that runs *just that tier* and the command that
         runs it *with coverage*. **Do not assume the selector is a directory.** These
         per-tier commands are recorded in `## Decisions` (Stage 5).
      
      **Stage 1's output is a discovery, not a boundary.** It tells you what tests exist
      and how to run them; it does not exhaustively partition the repo into "test" and
      "production," and nothing downstream may treat it as a safety fence. There is no
      path fence anywhere in this skill — bug injection happens in a disposable
      worktree (`references/break-it-check.md`), so no path-based fence is needed to
      keep mutation off production code.
      
      ## Stage 2 — Grade (fan-out subagents)
      
      **Skipped entirely when no tests exist.** Greenfield goes straight to Stage 3 —
      there is nothing to grade.
      
      Where tests exist, dispatch subagents partitioned by test tier or directory.
      Each subagent reads its partition against `references/test-theater.md` and
      returns a compact **structured verdict** — never pasted test source, never
      diffs. A legacy suite is thousands of lines the main agent must not absorb.
      
      Each subagent returns:
      
      - **Per weak test found:** file, line, pattern name (verbatim from
        `test-theater.md`), a one-sentence statement of why it fails to catch
        regressions, and a suggested replacement.
      - **Per mock or fixture encountered:** a ledger classification (see *The
        test-double & fixture ledger*, below).
      
      **Cardinality containment.** The number of instances is unbounded by design — a
      legacy suite may yield hundreds of weak tests — and the design contains that
      rather than merely asserting it away:
      
      - The **full verdict list** is written to `paad/test-roadmap/test-suite-analysis.md`, never
        returned to main context.
      - **Main context receives counts and the top-K by severity only.** A 400-item
        grading result must never land in the resume context budget. Bounding each
        verdict's size while leaving the list itself unbounded in main context is the
        same defect wearing a different hat — both the per-item size and the list
        length must be bounded before anything reaches the main agent.
      
      ## Stage 3 — Plan (main agent)
      
      Identify behaviors worth testing that are not tested. Group them into phases
      across the three tiers — unit, integration, e2e. Build the ledger. Draft phases
      in the canonical format defined in *Stage 5 — Write*, below.
      
      Two Stage 1 findings feed this stage:
      
      - **Coverage, for gap-finding only.** Where a coverage tool was detected, run it
        read-only to locate behaviors with *no test at all*. Coverage answers "what is
        untested," which it does well; it never answers "is this test any good," which
        it cannot — a line can be covered by a test that asserts nothing. This
        distinction is inviolate: coverage percentage is never evidence a test is
        worth keeping, in this stage or any other. Where no coverage tool exists, this
        is agent judgment reading source, and the plan states that plainly rather than
        presenting judgment as if it were tool output.
      - **Test-data mechanism.** If a phase needs constructed data and Stage 1 found
        no mechanism to construct it, the phase carries a **surfaced note** — *"requires
        a test-data mechanism this repo lacks"* — rather than being planned as if it
        were executable as written. Finding un-runnability at plan time is the point
        of having a plan stage; finding it at the gate is the expensive place to find
        it.
      
      **Legacy phases pin current behavior.** Where a phase characterizes existing
      code, every assertion it proposes must match what the code *currently does*.
      Behavior that looks wrong is never turned into a different assertion or a code
      fix — characterizing a legacy system and fixing its bugs are two hard problems,
      and this stage does the first one only. Where a wrong-looking behavior clears the
      inclusion gate in *The findings log* (below), it is recorded there — a concrete,
      actionable entry the developer works from later — and the phase that pins it
      carries a one-line pointer to that entry. Where it does not clear the gate, it is
      dropped, not written down as a vague note: a findings log of hunches is noise the
      developer learns to skip, the same cry-wolf failure the clean-run rule exists to
      prevent.
      
      ## Stage 4 — Critique
      
      Invoke `references/test-pushback.md` mode `critique-plan` against the draft
      plan Stage 3 just produced. Findings are **fixed inline, in the draft, on the
      spot** — no report is written; there is nothing to hand off, only phases to
      rewrite or drop before Stage 5 writes them to disk.
      
      The governing rule, restated because it is the gate the whole stage exists to
      enforce:
      
      > **Every phase must name the bug it would catch.** If a phase cannot answer
      > *"what breakage makes these tests go red?"*, it is not a phase — it is
      > coverage theater. Rewrite it or drop it.
      
      See `test-pushback.md` for the full pass, including the supporting rules (exit
      status and coverage are never evidence; every double and fixture is classified;
      legacy phases characterize, they don't fix).
      
      ## Stage 5 — Write (main agent)
      
      All generated files live under `paad/test-roadmap/` — create that directory if
      it does not yet exist. Two artifacts always, plus a third when this run turned up
      any qualifying finding:
      
      - **`paad/test-roadmap/test-roadmap.md`** — the `## Decisions` section first, then the
        phases.
      - **`paad/test-roadmap/test-suite-analysis.md`** — full grading verdicts and the ledger.
        This is the sink for anything unbounded (Stage 2's full list, the ledger in
        full); main context never carries it.
      - **`paad/test-roadmap/test-roadmap-findings.md`** — the findings log (see *The findings
        log*, below), written only if Stage 3/4 recorded at least one entry. If they
        recorded none, this file is not created here — execute mode creates it the
        first time a phase surfaces a qualifying finding.
      
      **These artifacts are read by the developer, so they follow `references/test-pushback.md
      § Talking to the developer`.** In particular the ledger's class names
      (`boundary`, `scaffold`, `data`) and any pattern names cited from
      `test-theater.md` are jargon to a newcomer — gloss each in plain words the first
      time it appears in the analysis doc (e.g. *"`scaffold` — a stand-in that only
      exists because the code is hard to test as written; it marks test debt to retire
      later"*). A weak-test verdict states, in plain terms, what regression the test
      would let through, not just its pattern label.
      
      **`paad/test-roadmap/test-roadmap.md` is always the target.** Everything this
      skill generates lives under `paad/test-roadmap/` — its own namespace, so it never
      clutters the developer's `docs/` and sits where PAAD tooling expects it. That
      also sidesteps colliding with a hand-written `docs/roadmap.md` a repo may keep for
      other reasons. The roadmap file's path is the router's signal for which mode to
      load on the next invocation, so it is **load-bearing and not configurable** — do
      not rename it, alias it, move it back under `docs/`, or write phases anywhere
      else.
      
      **Then commit the artifacts.** `git add paad/test-roadmap/test-roadmap.md
      paad/test-roadmap/test-suite-analysis.md` — and `paad/test-roadmap/test-roadmap-findings.md` if it was
      written — and commit them before build mode ends. This is
      not optional bookkeeping: the roadmap is the router's resume signal, and
      requirement 2 is resumability across *fresh clones*. An uncommitted roadmap
      does not survive a clone, so the next run finds no `paad/test-roadmap/test-roadmap.md` and
      silently rebuilds from scratch — the exact churn resumability exists to
      prevent. Execute mode already commits each `Landed:` line the same way (step
      5); build mode committing its own output is the same rule at the front of the
      lifecycle, not a new one.
      
      **Then list every file this run wrote**, one line per path — the roadmap is the
      whole deliverable of build mode and it is easy to miss among the conversation:
      
      ```
      Files written or updated:
        new  paad/test-roadmap/test-roadmap.md  (14 phases)
      ```
      
      **Then tell the developer the total, in plain words.** Build mode's approach
      menus fire *before* any plan exists, so during them there is no phase total to
      show — the first instant it is knowable is right here, once Stage 5 has written
      every phase. State it at the handoff, and make the loop explicit — **this run
      planned the work, it did not do any of it**, and one phase lands per run from
      here: *"Planned 14 phases — no tests written yet. Run `/test-roadmap` again
      to write Phase 1 of 14, and keep running it: one phase per run, 14 runs to
      finish."* This is the developer's first sight of the end of the tunnel, and from
      here on every phase the skill names carries its `Phase X of Y` per
      `references/test-pushback.md § Talking to the developer`. A developer who stops
      after this run has a plan and zero tests, so do not end build mode without
      saying what the next invocation does.
      
      ### The canonical phase format
      
      Every phase, in both build mode's draft and execute mode's updates, takes this
      exact shape:
      
      ```markdown
      ## Phase 3: Billing retry & dunning integration tests
      
      Tier:     integration
      Catches:  a retry exhausting without transitioning the account to dunning;
                a partial refund double-crediting.
      Produces: tests/integration/billing/
      Branch:   billing-integration-tests
      Landed:
      ```
      
      Field semantics:
      
      - **`Tier:`** — exactly one of `unit`, `integration`, `e2e`. Stage 3 already
        groups behaviors by tier, so this is honestly authorable at plan time (unlike
        the rejected `Covers:` field, which asked Stage 3 to enumerate something it
        never produces). It makes *which tier a phase belongs to* explicit in the
        roadmap itself — satisfying the distinguishability half of the tier
        requirement without depending on where the files physically land — and it
        selects which recorded per-tier run/coverage command (see `## Decisions`)
        applies to this phase.
      - **`Catches:`** — the anti-theater gate from Stage 4, preserved in the artifact
        so a later reader can audit whether the phase was honest. It is also
        `break-it-check`'s behavior input: it names the *behavior* to mutate, not a
        path. A path is neither necessary (the behavior locates the code) nor
        sufficient (directory granularity cannot pick a line to mutate).
      - **`Produces:`** — paths the phase creates, as **documentation only**. It is
        **not** a completion signal. A later reader uses it to find the phase's
        output; no control flow in this skill infers phase status from it.
      - **`Branch:`** — a human-readable breadcrumb recording the working branch the
        phase's tests were committed on. **Not a machine signal, and not a per-phase
        branch the skill creates** — execute mode commits each phase onto the current
        working branch and the suite accumulates there (see `execute-test-roadmap.md
        § What execute mode writes`).
      - **`Landed:`** — empty until `break-it-check` passes for this phase, then set
        to `YYYY-MM-DD <sha> (<operator>)`. **Human-clearable; the skill itself never
        clears it.** A cleared `Landed:` line is a human declaring the phase needs
        redoing — the skill has no mechanism that would ever produce that state on
        its own.
      
      Each phase's definition-of-done includes a recommendation to run `agentic-review` on the accumulated working branch before it is merged upstream, if available. This is not bundled into execute mode: `agentic-review` requires a fresh session with no substantive history, so calling it from within the test-roadmap session would be inert.
      
      ### The test-double & fixture ledger
      
      Every test double and fixture, existing or proposed, gets one classification:
      
      | Class | Meaning |
      |---|---|
      | `boundary` | Permanent and correct. A real external edge: network, clock, randomness, a payment gateway, a third-party API, filesystem where relevant. |
      | `scaffold` | Exists only because the surrounding code resists testing. Test debt. Names the refactor that retires it — that refactor is *deferred*, named now and actually done only when it is actually planned, not during this pass. |
      | `data` | Constructed test data that is permanent and correct — a seeded account, a factory-built order, a fixture record. Neither an external edge (`boundary`) nor debt a refactor retires (`scaffold`). |
      
      An unclassified double is a gap in the plan, not a detail to leave for later.
      **When in doubt between `boundary` and `scaffold`, or when a double goes
      unexamined, default to `scaffold`.** The harm is asymmetric: a `scaffold`
      silently promoted to permanent hides test debt indefinitely, while a `boundary`
      mistakenly recorded as `scaffold` only leaves a note a human later deletes.
      
      When a unit resists testing so completely that even a `scaffold`-mock test can't
      be written, the artifact is not a mock but a **skipped stub** that names the
      obstacle and surfaces it through the framework's skip-with-reason mechanism — see
      execute mode's *Units too hard to test*. It is still `scaffold`-class debt (the
      refactor that makes the unit testable retires it); it is a last resort, and it
      never counts as coverage.
      
      ### The findings log
      
      While pinning current behavior, the skill will notice code that looks wrong — a
      return that contradicts its own docstring, a validator that accepts what its name
      says it rejects. It never fixes these (Inviolate #1: characterize now, fix
      later), but it records the good ones so the developer finishes with a concrete
      to-do list instead of a vague memory that "something looked off." That list is
      `paad/test-roadmap/test-roadmap-findings.md`.
      
      This is **not** the "findings" of *Approaches vs findings* below — those are
      test-quality verdicts (a weak test, a mock's class) and go to the ledger. This
      log is production-code bugs the developer will fix later, watching the
      characterization tests break as they do.
      
      **The inclusion gate — verified and actionable, or it is not logged.** A log of
      hunches is noise the developer learns to skip, the same cry-wolf failure the
      clean-run rule guards against. An entry is written **only if all three hold**;
      miss any one and the observation is dropped, never downgraded to a vague note
      (there is no second-tier "maybe" list, and nothing lands elsewhere):
      
      1. **Demonstrable current behavior** — a specific input→output or code path, not
         a general unease. Where a characterization test pins it, cite that test: the
         test *is* the reproduction.
      2. **A concrete contradiction, cited** — in-repo evidence the behavior violates:
         a docstring, a type signature, an adjacent validation, a stated invariant, a
         test name asserting otherwise. This is a **citation of two things in the repo
         that disagree**, never the agent's own ruling on what the code *should* do —
         which of the two is right stays the developer's call, and Inviolate #1 stays
         intact.
      3. **A clear action** — a specific next step, e.g. *"reconcile the docstring at
         `email.py:40` with the return at `email.py:52`."*
      
      **Entry format** — one block per finding:
      
      Each field is its own bold-labeled paragraph, separated by blank lines — never
      bare adjacent lines, which markdown reflows into a single paragraph:
      
      ```markdown
      ## F3 — `is_valid_email` accepts the empty string
      
      **Where:** src/email.py:52
      
      **Behavior:** `is_valid_email("")` returns True.
      
      **Contradicts:** the function's own docstring (src/email.py:40): "returns True
      only for a syntactically valid address."
      
      **Action:** decide which is correct and reconcile them.
      
      **Pinned by:** Phase 6 — its test locks in the current True. Fixing the code
      turns that test red; that is the signal to update the test, not a regression.
      ```
      
      `Pinned by:` is filled where a phase's test pins the behavior (always so for a
      finding surfaced *while writing* that phase; for one surfaced at plan time, it
      names the phase that will pin it). It is the line that makes the finding
      actionable *and* ties it to the suite: the developer knows in advance which test
      will go red, and that red is success.
      
      The log is **append-only and committed with whatever produced it**, so it
      survives a fresh clone like the roadmap does. The main agent never holds the
      whole file in context — it appends entries, it does not re-read the accumulated
      log each run.
      
      ### Approaches vs findings
      
      Two different things reach the developer during a run, and they are not
      handled the same way.
      
      **Approaches** — genuine forks where the answer is taste or is expensive to
      reverse. Roughly four to six across a whole run: test framework/runner, suite
      strategy (unit-first / e2e-first / risk-first), phase ordering, whether to
      rewrite the weak tests Stage 2 found. Every approach goes to a menu whose full
      format is defined once in `references/test-pushback.md § Presenting approaches`
      — **load that section before presenting the first menu**, because by the time a
      run reaches this point it is deep in analysis and the format is no longer in
      context. The compact rule, restated here so it is in front of you at the moment
      you present rather than only in a file that may not be loaded:
      
      > Put the menu's substance in a **written prose menu in your own reply**: one
      > decision, every option with its own pro and con, a recommendation grounded in
      > this repo, and the deep-dive (a skeptical adversarial pass over the options)
      > as the last option. Then collect the answer with **one** structured-question
      > call — the harness's picker — carrying **exactly one question**, the options
      > as short labels only (the deep-dive included). That call is just the enter-key:
      > it blocks until the developer answers, so *one decision at a time* and *wait
      > for the answer* are enforced by the tool, not left to you remembering to stop.
      > **Never put more than one question in that call, and never let the picker
      > carry the menu's substance** — pros and cons, recommendation, and deep-dive
      > stay in the prose above it. A picker carrying the whole menu is what batched
      > decisions as tabs and dropped three of the four required parts in the field; a
      > one-question selector does neither, and unlike a bare prose menu it cannot be
      > followed by more work until the developer answers. Full format and wording:
      > `§ Presenting approaches`.
      
      This is the one place the reference's format is deliberately restated rather
      than only pointed at: it is a *reflex at the point of use*, and a pointer to an
      unloaded file is not a reflex.
      
      **Not on the menu — recorded defaults.** Some things look like a choice but have
      a dominant default and are cheap to reverse, so they are decided once and
      recorded, not voted on. **Test organization** is one of these: it is a *detected
      fact* about how the repo already separates tiers, not a taste choice, so it goes
      to `## Decisions`, not to a menu — putting it on one would blow the 4–6-approach
      ceiling for a decision that isn't the developer's to make.
      
      The organization must leave the tiers distinguishable and independently
      coverage-runnable (see below), but the *form* is whatever the detected ecosystem
      expresses — a tier directory only where the ecosystem uses an external,
      path-selected test tree, and a tag/marker/build-target/label elsewhere. An
      earlier version of this note argued against a "group by tier" layout on the
      grounds that two phases sharing a tier directory would manufacture a false-landed
      collision. That reasoning is now spent: the completion model was rewritten so
      `Landed:` is the sole signal (see `execute-test-roadmap.md § Completion model`) —
      nothing infers completion from a directory's contents anymore, so two phases in
      one tier directory each latch via their own `Landed:` line and collide on
      nothing. Tier grouping is therefore safe; it just isn't an operator *menu* item,
      because it's detected, not chosen.
      
      **Findings** — verdicts derived from evidence: a weak-test classification, a
      mock classification. These are not approaches; asking a human to vote on a
      classification is asking them to vote on a fact. Findings go to the ledger,
      consistent with Stage 4's own rule that findings are fixed inline, not
      reported.
      
      **`scaffold` mocks are batched, not surfaced one-by-one.** They are recorded to
      the ledger en bloc, not raised as individual decisions during grading — on a
      legacy codebase, the case that produces the most `scaffold` entries, one-by-one
      surfacing would mean dozens of serial decisions in a single sitting. The
      retiring refactor for each is named when that refactor is actually planned,
      which is both less overwhelming and a better moment to name it than during a
      grading fan-out.
      
      **Silence defaults to `scaffold`** — restated here because it governs what gets
      written to the ledger during this stage, same reasoning as above: the harm of
      under-classifying is asymmetric, so inattention is made safe by construction
      rather than relying on a human reading every entry carefully.
      
      ### The three tiers must be independently runnable
      
      The suite must leave unit, integration, and e2e (1) **distinguishable** — a
      developer new to the repo can tell which tier a test belongs to — and (2)
      **independently coverage-runnable** — they can run coverage over *just* the unit
      tier, or *just* integration, and so on.
      
      The distinguishability half is carried by the `Tier:` field on every phase (and,
      where the ecosystem's convention already separates tiers on disk, by the layout
      too). The independent-coverage half is carried by **recording, per tier, the
      command that runs it with coverage** — because per-tier coverage is *not* a
      universal "point the coverage tool at a directory" operation:
      
      - Coverage instruments the **source under test**, never the test files, so
        pointing a coverage tool at a directory of tests does not scope coverage to a
        tier. What scopes it is the tier's *selector* applied to the coverage run.
      - The selector is a directory only in ecosystems that use external,
        path-selected test trees (Python, PHP, Ruby, Perl). Elsewhere it is a tag
        (Go `-tags`, `pytest -m`), a build target (`cargo test --lib`/`--test`, a
        `.csproj`, a Jest project), or a label (CTest `-L`).
      - In colocated-test ecosystems the tier directory *cannot exist*: Go `_test.go`
        files must live in the package under test, and Rust unit tests are
        `#[cfg(test)]` modules compiler-bound inside `src/` — both reproduced by
        construction (see the design's *Verification notes*). Mandating a `unit/`
        directory there is impossible, not merely awkward.
      
      So the skill records the *commands*, not a layout. This is the only formulation
      that holds across every ecosystem — the invariant is "there is a command that
      runs exactly one tier with coverage," and its selector is detected, not assumed.
      
      ### `## Decisions`
      
      Every recorded default and every approach the developer picked is written to a
      `## Decisions` section at the top of `paad/test-roadmap/test-roadmap.md`, before the first
      phase. Execute mode reads this section on every resume and **never re-asks**
      anything recorded there — without it, menus would fire again on every run and
      the resumability requirement fails through the front door. Record at minimum:
      the chosen test framework/runner, the suite strategy, phase ordering, the
      weak-test rewrite decision (if grading ran), the test-organization default, and
      **the per-tier run + coverage commands** — one line each for unit, integration,
      and e2e — so both execute mode and the developer can run and cover each tier
      independently. A worked shape:
      
      ```markdown
      ## Decisions
      
      Tiers (run | coverage):
      - unit:        `pytest tests/unit`            | `pytest --cov=pkg tests/unit`
      - integration: `pytest -m integration`        | `pytest --cov=pkg -m integration`
      - e2e:         `playwright test`              | (no coverage tool for this tier)
      ```
      
      Where a tier has no coverage tool, say so on its line rather than omitting it —
      an absent line reads as "forgotten," a stated "(none)" reads as "checked."
      Keep each entry to the decision and its one-line reason — this section is a
      record for a machine to skip past, not a design document.
      
    • execute-test-roadmap.md 28.1 KB
      # Execute mode — the completion protocol
      
      Loaded when `paad/test-roadmap/test-roadmap.md` already exists — the path every run after
      the first takes. This is the run that must be quiet about anything the
      developer already settled: it reads `## Decisions` once, per phase asks
      exactly the one question the completion model requires, and otherwise gets on
      with writing tests.
      
      ## The execute-mode loop
      
      Eight steps, in order:
      
      1. **Read `## Decisions` from the roadmap. Never re-ask anything recorded
         there.** The chosen test framework/runner, suite strategy, phase ordering,
         the weak-test rewrite decision, the test-organization default, the per-tier
         run + coverage commands — all of it was already decided during build mode's
         Stage 3/Stage 5 and is written down for exactly this reason. Re-asking it
         here is not caution, it is the resumability requirement failing through the
         front door. Place the phase's tests according to its `Tier:` and the recorded
         organization, and use the recorded per-tier command when running or covering
         that tier.
      2. **Select the candidate phase per the *Completion model* protocol, below.**
         Exactly one phase at a time — the protocol's questioning fires for the
         candidate phase only, never as a batch sweep across every phase in the
         roadmap.
      3. **Write the tests for that phase.** This is the only code this step
         writes — see *What execute mode writes*, below. Reading the code this
         closely is where real bugs surface; log the ones that qualify to the
         findings file — see *Logging suspected bugs*, below.
      4. **Run `references/break-it-check.md`.** This gate is mandatory. **No
         phase latches without it** — not a fast path for a phase that "obviously"
         passes, not a phase where the developer is confident the tests are
         right. Skipping this step is skipping the entire mechanism that
         distinguishes this skill's suite from `assert result is not None` a green
         exit code would have waved through.
      5. **Run the developer's whole suite on their branch, and don't latch a red
         or noisy run.** Using the recorded `## Decisions` commands, run every test
         the way the developer would — once normally, once under coverage — and read
         both runs for failing tests, spurious output, and anomalies. See *Keep the
         test run clean*, below, for what to run, why the coverage pass earns its
         place, and how to dispose of what you find. Like `break-it-check`,
         this gate can only **block or surface** — it never edits production code and
         never latches a dirty run.
      6. **Verify the phase touched no production code, then latch.** Before writing
         `Landed:`, confirm from git that this phase's pending diff only *adds* test
         code and *writes* under `paad/test-roadmap/` — no existing production code
         modified, deleted, moved, or added (see *The phase touched no production
         code*, below). Only then write `Landed: YYYY-MM-DD <sha> (<operator>)` and
         commit on the current working branch — written only after steps 4, 5, *and*
         this check all pass, so it is those gates' own output, not bookkeeping that
         could drift out of sync with them.
      7. **Surface run instructions for the tests just landed.** See *Run
         instructions*, below.
      8. **Hand off to the next run — or say the roadmap is finished.** One phase
         lands per run, so the run always ends by telling the developer whether to
         invoke `/test-roadmap` again and why. See *Ending the run*, below. This
         step is not optional and is not merged into step 7: a developer who is not
         told to run it again assumes one invocation was the whole skill, and stops
         with a roadmap and one phase of tests.
      
      ### Ending the run
      
      **The single most common way this skill fails is a developer running it once
      and stopping.** They get a roadmap, or a roadmap plus one phase, and never
      learn there were thirteen more. Nothing in the phase's output implies "come
      back" unless the run says so, so every run says so — explicitly, as its last
      words.
      
      **First, list every artifact this run wrote or updated.** The test files are
      visible in the run instructions, but the roadmap and the findings log are not,
      and a developer who never learns a bug was logged never reads it. One line per
      path, each marked new or updated. Name the test files too when a phase landed
      only a few; past a handful, give them as a count with a pointer to the diff, so
      the roadmap and findings lines stay the first thing read:
      
      ```
      Files written or updated:
        new      tests/integration/billing/test_retry.py
        updated  paad/test-roadmap/test-roadmap.md          (Phase 3 marked done)
        updated  paad/test-roadmap/test-roadmap-findings.md (F4 added)
      ```
      
      Name the findings log only when this run actually added an entry to it, and say
      what the entry was about in a few words, so a real bug does not sit unread.
      
      Then recompute the counts from the roadmap
      (`references/test-pushback.md § Talking to the developer`) and end the run one
      of two ways:
      
      - **Phases remain** — name the next one and say plainly that another
        invocation is what runs it:
      
        ```
        Run /test-roadmap again to do the next one — one phase per run, so it
        takes another 6 runs to finish the roadmap.
      
        Next is Phase 8 of 14 — 7 done, 6 to go after this: Logger level filtering
        & message formatting.
        ```
      
        Say *why* it is another run, not a continuation: each phase is written,
        proved against an injected bug, and committed on its own, and a fresh run
        keeps the context clear for the phase it is working on. A new session is
        fine — the roadmap file is the memory, and the run picks up from it.
      
      - **No phases remain** (every phase in the roadmap has a populated `Landed:`)
        — do not ask for another run. Say the roadmap is finished: all N phases are
        done, the tests are committed on this branch, and point at
        `paad/test-roadmap/test-roadmap-findings.md` if it has entries, since that
        bug list is the developer's to act on and this skill never will. Running the
        skill again in this state is a no-op that finds nothing to do, so do not
        invite it.
      
      ### Run instructions
      
      The developer running this skill may not know the codebase, the test runner, or
      even the language. So when a phase's tests are ready, do not leave them to
      reconstruct how to run anything. Name the phase as `Phase X of Y` throughout this
      mode — the run-instructions header, the "next phase" prompt, and the completion
      question of step 2 — per `references/test-pushback.md § Talking to the developer`,
      so the developer always sees how many phases remain. Print a short block, drawn
      verbatim from the `## Decisions` per-tier commands (never invented on the spot),
      giving:
      
      - **The new tests** — the command that runs *just this phase's* tests.
      - **This tier with coverage** — the recorded coverage command for the phase's
        `Tier:`, so the developer can see what these tests cover. Where that tier has
        no coverage tool, say so explicitly rather than omitting the line.
      - **The whole suite** — the command that runs every test, so they can confirm
        nothing else broke.
      
      A worked shape:
      
      ```
      Phase 3 of 14 (integration) tests are in and committed. To run them:
      
        just these tests:   pytest tests/integration/billing/
        with coverage:      pytest --cov=billing -m integration
        the full suite:     pytest
      ```
      
      Keep it to the commands and one label each — this is an aide-mémoire for someone
      unfamiliar with the repo, not documentation. The commands are the recorded ones;
      if a command has drifted (the runner changed since build mode), that surfaces
      here as a failed run the developer can see, not as silent wrong advice.
      
      ### What execute mode writes
      
      Execute mode writes **test code only**. It never writes production code. Two
      different things enforce that, on two different axes: `break-it-check`'s
      *mutation* never touches the developer's tree because it runs in a throwaway
      `git worktree` (Inviolate #2), and this mode's *authoring* — step 3, writing the
      tests — is verified after the fact by the step 6 check (*The phase touched no
      production code*, below) before anything latches. Neither is a path-based fence:
      that was rejected (it fails open in colocated-test ecosystems), and the step 6
      check is deliberately diff-aware instead.
      
      **Each phase's tests commit onto the current working branch; the skill creates
      no per-phase branch.** The suite accumulates in place — as phases land, the
      test directory on the branch the developer is already on fills up, and the
      `Landed:` SHA points at a commit that branch can actually reach. This is the
      literal reading of `break-it-check`'s "commit on the developer's real tree"
      (step 6). The skill deliberately does **not** spin each phase onto its own
      branch and leave it unmerged: unmerged local branches do not survive a fresh
      clone — the very failure the completion model rejects branch-merge detection
      over — so putting the test *files* there would strand the suite off the
      working branch and break resumability (requirement 2). Per-phase review is not
      lost: each phase is its own commit, reviewable with `git show <Landed-sha>`,
      and `agentic-review` runs against the accumulated working branch before it is
      merged upstream.
      
      ### The phase touched no production code
      
      *What execute mode writes* states the rule — test code only. This step **verifies
      it from git** before a phase latches, because a rule the skill is trusted to
      follow is worth confirming: a stray edit while writing tests, or a
      refactor-to-make-testable that slipped past Inviolate #1, would otherwise commit
      onto the branch unnoticed.
      
      **The invariant:** a phase's commit may only *add* test code and *write* under
      `paad/test-roadmap/`. It may never modify, delete, move, or add production code.
      
      **The check is diff-aware, not path-aware.** A path fence — "no non-test *file*
      changed" — is the write-fence this design already rejected, and it fails the same
      way: in colocated-test ecosystems (Rust `#[cfg(test)] mod tests`, Zig, D) the unit
      test lives *inside* the production file, so a path fence flags every honest test
      or fails open on a real edit. Use the test organization Stage 1 already detected
      (recorded in `## Decisions`):
      
      - **Separate-file ecosystems** (Python, JS, Go's `_test.go`, Ruby, PHP, Perl):
        every changed path must be a test file or under `paad/test-roadmap/`.
      - **Same-file colocation** (Rust and the like): a change to a production file
        must be **purely additive test code** — a new test module or test functions,
        with *no* pre-existing line altered. Any edit to an existing line is a violation.
      
      Walk this phase's pending changes — `git diff --name-status` plus the diff itself,
      staged and unstaged, *this phase's own commit, not the whole branch* — and check:
      
      - A **delete or rename** (`D`/`R`) of any production path → violation.
      - A **new non-test file** → violation (the skill adds tests, not source).
      - A **modified production file** → violation, unless it is the colocated case
        above and the diff is purely additive test code.
      - A **build/config/manifest change** (`package.json`, `Cargo.toml`, `.gitignore`,
        CI config) → **do not latch silently; surface it and ask.** A test sometimes
        needs a dev-dependency, or the coverage tool the clean-run gate already asked to
        install; latch it only on the developer's OK. This is the one change outside
        tests + docs that may be legitimate — everything else in the production tree is
        not.
      - **Stray generated artifacts** (`.coverage`, `coverage/`, `target/`, editor
        cruft), caught by the same invariant → they must not be committed; drop them
        from the commit.
      
      **On any violation, stop — do not latch.** Show the developer the exact change in
      plain words — *"while writing these tests I ended up changing `src/billing.py`,
      which shouldn't happen; the skill only adds tests. Here's what changed — it needs
      undoing before I can call this phase done."* **Never silently revert it**: that
      could destroy work the developer meant to keep or bury a real bug. The human
      decides.
      
      ### Keep the test run clean
      
      A phase is not done until the developer's suite runs clean. So before `Landed:`
      (step 5 of the loop, after `break-it-check`), **run the whole suite on the
      developer's branch the way they would** — the recorded `## Decisions` commands,
      verbatim, never invented — **twice**:
      
      1. **Normally** — the plain whole-suite command.
      2. **Under coverage** — the recorded coverage command. Not for the percentage
         (coverage never certifies a test — Inviolate #3); this is an **anomaly probe**.
         Coverage instrumentation reorders imports and shifts timing, so a warning,
         failure, or hang can appear under coverage that a normal run hides. Run both
         and compare.
      
         Where the phase's tier has **no coverage tool** (`## Decisions` records it as
         `(none)`), ask the developer **once**, in plain words, whether to install one
         or go without — then record their answer in `## Decisions` so no later phase
         re-asks. That is the only place this pauses for input; it is not a per-phase
         question.
      
      Read both runs for three things: **failing tests**, **spurious output** (a
      `Wide character in print` warning, deprecation spam, stray STDOUT), and **any
      other anomaly** — a hang, a result that changes between runs — that would make a
      developer distrust the suite. Two reasons this matters, both about the developer:
      noise buries the signal (twenty lines of framework chatter around one real
      failure trains people to skim past it), and a suite that always warns teaches the
      developer to ignore warnings — at which point the one warning that *was* a real
      bug scrolls past unnoticed. The run must end **green and quiet**, or its remaining
      noise must be a deliberate, recorded decision.
      
      For output that appears, in order of preference:
      
      1. **Treat meaningful output as behavior — capture and assert it.** If the code
         under test emits a deprecation warning, a log line, or a message that *matters*
         (it should fire, or it should *not* fire), that is behavior worth pinning:
         capture it and assert on it, the same as any return value. A warning you
         assert on is no longer noise — it's a test.
      2. **Suppress noise you can't assert, at the test boundary.** Third-party
         chatter you can't control and don't need to pin gets silenced narrowly in the
         test setup — not by muting all output globally, but scoped so that a *new*,
         unexpected warning still stands out.
      3. **Never let the tests you write add their own noise** — no leftover debug
         prints, no `warn`/`console.log` scaffolding shipped in the committed test.
      
      **Every fix here is test-side. Production code is never edited (Inviolate #1) —
      where the fix belongs decides what you do:**
      
      - **A failure or noise rooted in the tests this mode wrote** — a leftover print, a
        test mishandling its own output, a bad assertion — is **yours to fix.** Fix it,
        re-run both ways, confirm clean.
      - **A failure or noise rooted in production code** — the code under test emits the
        wide character, not your test — is **never patched here.** Stay on the test side:
        assert it if it's meaningful behavior, suppress it narrowly if it's
        uncontrollable, and where it is a real defect, record it in the findings log
        (`build-test-roadmap.md § The findings log`) if it clears the gate. The suite
        goes quiet without production behavior changing — and the developer gets the bug
        on their list instead of a silenced warning they never see.
      - **A pre-existing failure in tests this mode did *not* write** is not this
        phase's job to fix — but it is **never silently latched over.** Surface it
        plainly and ask the developer how to proceed; do not fix unrelated tests, and do
        not mark a phase done on a suite you can see is red.
      
      If a clean run genuinely isn't achievable for a phase (the code is noisy by
      construction and the noise can't be captured or scoped), say so plainly rather
      than shipping a suite that cries wolf. Whenever any of the above needs a judgment
      you can't make alone, discuss it with the developer in plain words
      (`references/test-pushback.md § Talking to the developer`) — the failing test or
      the warning described for what it means, not by its label.
      
      ### Units too hard to test
      
      Some units resist testing so completely that neither a real characterization
      test nor a `scaffold`-mock test (see the ledger in `build-test-roadmap.md`) can
      be written honestly. When that happens — and **only as a last resort, after both
      a real test and a scaffold-mock have been judged infeasible, not merely
      inconvenient** — write a **skipped stub test** that names the obstacle, rather
      than silently leaving the unit out of the suite. An untested unit that is simply
      absent looks like one nobody needed to test; a skipped stub that says *why* keeps
      the gap visible.
      
      **Surface it through the framework's own skip mechanism, with a reason** —
      `pytest.skip(reason=…)`, `@Disabled("…")`, `t.Skip("…")`, whatever the detected
      framework provides — so the message rides with the test and shows in the run
      summary and CI, the same standard-tooling channel *Keep the test run clean* uses
      for meaningful output. The reason states in plain language that the unit is hard
      to test and what makes it so, e.g. *"skipped — builds its own DB handle in the
      constructor; untestable until that dependency is injected."* Only where the
      detected framework has no machine-surfaced skip reason does this fall back to a
      single one-line STDERR statement — never a wall of prints.
      
      A skipped stub is a **gap-marker, not coverage**:
      
      - It **never goes through `break-it-check`** — nothing is asserted, so there is
        nothing to mutate.
      - It **never counts toward a phase's `Landed:` line.** A phase latches on its
        real, bug-catching tests; a skip latches nothing.
      - **A phase made only of skipped stubs catches no bug and is dropped as theater
        (Inviolate #5), not landed.** Skips ride *alongside* real coverage; they never
        constitute a phase.
      - The obstacle is also recorded as `scaffold`-class debt in the ledger /
        `paad/test-roadmap/test-suite-analysis.md`, naming the refactor (usually a dependency-
        injection seam) that would make the unit testable and retire the skip.
      
      When the phase's tests land, tell the developer in plain words how many units
      were too hard to test and why, so the skips are a decision they can see rather
      than a silent omission.
      
      ### Logging suspected bugs
      
      Writing a phase's tests means reading the code closely, which is exactly when a
      real bug surfaces — a return that contradicts its own docstring, a check that
      lets through what it claims to reject. Do not fix it (Inviolate #1: pin current
      behavior; the developer fixes later, watching these tests break). Instead, where
      it clears the inclusion gate, record it in `paad/test-roadmap/test-roadmap-findings.md`.
      
      **The gate and the entry format are defined once in `build-test-roadmap.md
      § The findings log` — use them verbatim.** In short: log an entry only if you can
      state (1) the demonstrable current behavior, citing the characterization test
      that pins it; (2) a concrete in-repo contradiction it violates — a citation, not
      your own ruling on what is correct; and (3) a clear action. Miss any one and drop
      the observation — never write it down as a vague note. Set the entry's `Pinned
      by:` to this phase's test, and add a one-line pointer on the phase block in the
      roadmap where the finding maps to it.
      
      Create `paad/test-roadmap/test-roadmap-findings.md` if it does not yet exist (build mode
      writes it only when its own stages found something); otherwise append. **Commit
      it in the same commit as the phase's tests** (step 6 of the loop), so a finding
      never lands without the test that pins it, and both survive a fresh clone.
      
      When the phase lands, tell the developer in plain words how many findings you
      logged and point them at the file — *"I logged 2 concrete bugs I hit while
      writing these; they're in `paad/test-roadmap/test-roadmap-findings.md`, each with the test
      that proves it"* — so the log is a decision they can see, not a file they
      stumble on later.
      
      ## Why TDD is not bundled
      
      `superpowers:test-driven-development` is deliberately **not** loaded by
      execute mode, even though this mode is, superficially, "write a test, run it."
      
      For a characterization phase — the case this skill exists for — the code
      already works. The test step 3 writes therefore passes on its first run *by
      construction*: there is no bug yet to make it fail, because none has been
      injected. Verified at `test-driven-development/SKILL.md:126`:
      
      > **Test passes?** You're testing existing behavior. Fix test.
      
      Applied literally to a characterization test, that instruction is not merely
      inapplicable — it is destructive. The test passing on its first run is
      correct behavior here, not a signal that the test is wrong, and an agent
      following that rule would rewrite a correct characterization test until it
      fails for no reason, or invent a reason to make it fail. Either way it
      corrupts the one thing this phase was supposed to produce: an honest pin on
      current behavior.
      
      The red phase a characterization test actually needs does not come from
      authorship — writing the test a second, "more red," way — it comes from
      **mutation**: inject a real bug and confirm the existing test catches it.
      That is exactly what `break-it-check` (step 4, above) provides, and it is why
      this mode reaches for that gate instead of for TDD's red-green-refactor loop.
      
      ## Completion model
      
      ### Problem being solved
      
      A hand-written status label goes stale the moment work is merged outside the
      agent session — another developer lands the branch, or the same developer
      merges it between sessions. A fresh session with no memory of the previous
      one then reads the stale label and asks the human whether already-finished
      work is finished, which is exactly the churn requirement 2 (resumability)
      exists to prevent.
      
      An earlier draft solved this by inferring completion from a `Produces:` path:
      path present on disk → treat the phase as landed; path absent → treat that as
      proof the phase never started, and execute it silently. That is repo state
      the skill does not control, and it fails in both directions:
      
      - **Toward re-churn:** a directory renamed in a reorg, or a fresh clone of a
        repo whose layout changed since, makes landed work look absent, so the
        skill silently re-executes it.
      - **Toward silent skip:** two phases sharing a directory — any "group by
        tier" test-organization layout can produce this — makes an unstarted
        phase's path look present, so the inference finds the *other* phase's
        output and reports the unstarted one as landed. That is the one direction
        the governing principle below forbids outright.
      
      ### Governing principle
      
      **No signal auto-marks anything done, and no absence is ever proof of
      not-done** (Inviolate #4). Signals decide only whether to ask, and supply the
      evidence attached to the asking. Being wrong toward "ask with evidence" costs
      one keystroke. Being wrong toward "auto-mark done" silently skips real work —
      the one outcome the whole design is built to keep from happening quietly.
      
      The original bug behind the `Produces:`-path draft was never that the agent
      asked the human something. It was that it asked **empty-handed** — and, just as
      bad for a developer new to the repo, in the skill's own jargon. The question
      must carry its evidence *and* be in plain words (see `references/test-pushback.md
      § Talking to the developer`):
      
      - Bad (empty-handed): *"Phase 3 shows Pending. Is it complete?"*
      - Bad (jargon): *"Phase 3 declares `tests/integration/billing/`; its `Landed:`
        line is empty — did it land, or should I execute it?"*
      - Good: *"I don't see tests yet for **billing retries** (I'd add them under
        `tests/integration/billing/`). Did someone already write these — maybe on a
        branch called `billing-integration-tests` — or should I write them now?"*
      
      The phase's recorded details supply the evidence; the developer supplies the
      answer. Because they may not know the repo's history, tell them where to look if
      unsure — *"you can check with `git log tests/integration/billing/`, or see
      whether those test files already exist"* — so the question is answerable without
      prior knowledge of what has and hasn't been done here.
      
      ### Protocol
      
      `Landed:` is the sole completion signal, and this protocol runs for the
      **candidate phase only** — never as a sweep across the whole roadmap.
      
      | Step | Where | Action |
      |---|---|---|
      | 1 | main agent | `Landed:` populated? → done. Stop. No git, no question. |
      | 2 | main agent | `Landed:` empty → ask the developer, in plain words (§ Talking to the developer), whether these tests were already written or should be written now — using the phase's recorded details as evidence and pointing them at where to check. On *"write them,"* run the phase (the execute-mode loop, above). On *"already done,"* record it from what they report. |
      
      Step 1 ends the churn permanently for a phase that has already latched: once
      `Landed:` is populated, the control flow never re-examines that phase again —
      **except** that `Landed:` is human-clearable (see below), which is a human
      decision, not something step 1 itself does.
      
      Step 2 asks rather than infers, and **absence of a "yes" means execute** — the
      safe direction — never a silent skip. This is the case that fires only for
      work that may have landed *outside* an agent session; work the skill lands
      itself goes straight from `break-it-check` passing to a written `Landed:`
      line, no question asked, because there the skill *is* the authority on
      whether it happened.
      
      **`Landed:` is human-clearable; the skill itself never clears it.** A human
      who learns a phase's tests were condemned by a later review, or that the gate
      was gamed, can clear the line by hand, and the phase re-enters the flow at
      step 2 on the next run. No agent inference is ever the thing that un-lands
      work — only a human, deliberately, gets that lever.
      
      ### Why not branch-merge detection
      
      Considered and rejected as a signal. Verified behavior of
      `git branch --merged main`:
      
      | Merge style | Result |
      |---|---|
      | `--no-ff` merge commit | works |
      | Squash merge (common default) | **fails** — branch tip is not an ancestor |
      | Rebase merge | **fails** — commits rewritten, new SHAs |
      | Branch deleted after merge | **fails** — nothing left to query |
      | Fresh clone | **fails** — no local branches exist |
      
      Four of five cases fail, and all four fail **toward "not done"** — the
      skill would regenerate work already landed, which is churn, not safety, but
      churn that the resumability requirement was written to prevent.
      
      ### Why not commit-log scanning, or `Produces:`-absence
      
      Rejected for the same underlying reason: both infer "did this land" from repo
      state the skill does not control. Matching a phase's title against commit
      messages is string-matching human prose that was never written to be a
      machine contract. `Produces:`-absence infers "not started" from a path
      layout that renames, that two phases can collide on, and that — in a
      colocated-test ecosystem — may never independently exist at all. Git log
      answers *when* a thing landed and *which commit* did it well; it answers
      *whether* badly. So the skill does not use it as a signal. `Landed:` is the
      signal; a human who wants to know when a phase landed runs `git log`
      themselves, with the surrounding context to read what it means — context a
      blind scan does not have.
      
      ### Why not an anchor commit or a `Covers:` field
      
      Considered: record a `generated-at: <sha>` and a per-phase `Covers:` field
      naming the production paths it depends on, then diff `<sha>..HEAD` against
      `Covers:` on resume to detect a phase planned against code that has since
      moved.
      
      Rejected on two grounds:
      
      1. **It fails silent.** A change to a transitive dependency outside the
         listed paths produces an empty intersection, so the skill reports "no
         drift" for a phase that is actually invalidated. Closing that hole would
         require per-language transitive-closure analysis, which contradicts the
         stack-agnostic requirement this design is built to hold.
      2. **`Covers:` is not honestly authorable.** Stage 3 produces behaviors and
         phase groupings; it never enumerates the production paths behind them. The
         field would have to be filled in by inference after the fact, failing
         silent in exactly the same direction the mechanism was meant to close.
      
      `break-it-check`'s "no code path implements this `Catches:`" row already
      detects the same condition — code a phase depended on has moved or
      vanished — terminally and loudly, at gate time, at no authoring cost and with
      no separate field to keep in sync.
      
    • test-pushback.md 12.1 KB
      # test-pushback — how the skill talks to the developer, plus critique mode
      
      This file holds three independent, referenceable pieces. The mode files and
      `break-it-check.md` point at them by name rather than inlining them, so each
      stays defined once:
      
      - **`§ Talking to the developer`** — the plain-language rule that governs
        *everything* the skill says to the developer.
      - **`§ Presenting approaches`** — the menu format used whenever the skill
        puts a genuine fork in front of the developer.
      - **mode `critique-plan`** — the adversarial pass Stage 4 runs against the
        skill's *own* draft plan before Stage 5 writes it.
      
      The sections don't depend on each other and can be pointed at on their own.
      
      ---
      
      ## § Talking to the developer
      
      Everything the skill says to the developer is in **plain language**. The person
      reading may not know this codebase, the test runner, or the programming
      language — and they know nothing about this skill's internals. So the design's
      own working vocabulary stays *inside these files* and never reaches them
      unglossed: `break-it-check`, "the gate," "latch," "mutation," "operator," "the
      ledger," `boundary`/`scaffold`/`data`, "characterization," "theater,"
      `Landed:`, "the phase block," "build mode"/"execute mode" are words for the
      agent, not for the developer.
      
      Translate, don't emit:
      
      - **Name what you're doing in ordinary words, not by its codename.** Not
        "running break-it-check" — instead: *"I'll check these tests actually work by
        slipping a realistic bug into a throwaway copy of the code and confirming the
        tests catch it. Your real code is never touched."*
      - **Report findings as what they mean for the tests, not by their label.** Not
        "this test is theater" — instead: *"this test still passed after I
        deliberately broke the code it's meant to check, so it isn't really testing
        that behavior."*
      - **When a term does live in the written artifacts** (a tier name, a
        `boundary`/`scaffold` classification), gloss it in plain words the first time
        the developer sees it.
      
      The test is simple: if a sentence would only make sense to someone who has read
      this skill's design, rewrite it until it makes sense to someone who has not.
      
      **Always locate a phase for the developer: "Phase X of Y."** Whenever you name a
      phase to the developer, render `Phase <its written number> of <total phases in
      paad/test-roadmap/test-roadmap.md>`, so they always know how much is left — the end of the
      tunnel, not just the current step. On the forward-looking "next phase" prompt and
      on any completion question, add progress: `— N done, M to go after this`.
      Recompute all three from the roadmap on every run — never cache them:
      
      - **Y** = count of phase blocks currently in `paad/test-roadmap/test-roadmap.md`. Because it
        is recomputed each run, it stays honest when a human adds or drops a phase: the
        total just updates next time, rather than going stale.
      - **done** = count of phases whose `Landed:` line is filled in.
      - **M** = Y − done − 1 (everything neither finished nor the phase in hand).
      
      X (a phase's written number) and the done-count are **independent** — a phase can
      be finished out of order, so do not assume everything numbered below X is done;
      report both from the file. Use plain words: say "done," never "landed," "latched,"
      or "of Y phases in the roadmap." A worked shape, numbers adding up (7 + this one +
      6 = 14): *"Next is **Phase 8 of 14** — 7 done, 6 to go after this: Logger level
      filtering & message formatting."*
      
      ---
      
      ## § Presenting approaches
      
      Use this format only for **approaches** — genuine forks where the answer is
      taste or is expensive to reverse (test framework/runner, suite strategy,
      phase ordering, whether to rewrite weak tests grading found). Do not use it
      for **findings** — verdicts derived from evidence, such as a weak-test
      classification or a mock classification. A finding is a fact, not a choice;
      asking a human to vote on a fact is the wrong tool. Findings are fixed
      inline or recorded to the ledger, not put on a menu.
      
      **Assume the developer does not know this repository.** The person answering
      may have opened it for the first time this session — legacy and unfamiliar
      code is the case the skill exists for. So every menu must be answerable with
      no prior knowledge of the codebase: state each option in plain terms, and
      make the recommendation strong enough to follow blind, justified by what
      *this repo actually is* — its detected stack, its existing tests, its
      structure — not by generic preference. A developer who knows nothing about
      the code should be able to take the recommended option and be right.
      
      Every menu, presented unbatched, contains:
      
      - **Each option**, with **pros and cons** stated for it — in terms a
        newcomer to this codebase can weigh, not insider shorthand.
      - A **recommendation**, with the **reason** grounded in what was detected in
        *this* repo — not a bare pick and not a generic default.
      - A final **deep dive** option — internally, dispatch a subagent to run an
        adversarial pushback against the presented options; but *present it to the
        developer in plain words*, e.g. *"dig deeper: have a second, skeptical pass
        challenge these options and check whether there's a better one."* It
        challenges the menu itself and surfaces any better option it missed, before
        committing to any one on it. All menu text follows `§ Talking to the
        developer`.
      
      **One decision at a time, unbatched.** A menu asks for exactly one decision.
      Do not bundle a second question onto it ("...and while we're at it, do you
      also want X?") — it splits the developer's attention and degrades the
      answer to both. Resolve the decision in front of them, act on it, and only
      then raise the next one. A follow-up that is fully determined by the answer
      just given is not a second decision — it does not need its own menu, just do
      it.
      
      **Collect the answer with a single blocking question, not a prose menu you
      merely intend to stop after.** After presenting the menu above, make exactly
      **one** structured-question call — your harness's picker — carrying **one
      question**, the options as short labels (the deep-dive included). It blocks
      until the developer answers, and that is what actually enforces *wait for the
      answer* and *one at a time*: a prose menu you only plan to stop after does not
      stop the run — a blocking question does. Never carry more than one question in
      that call, and never move the menu's substance (pros and cons, the
      recommendation, the deep-dive rationale) into it — that stays in the prose above;
      the picker is only the selector. A picker made to carry the whole menu is what
      batched decisions as tabs and dropped three of the four required parts; a
      one-question selector does neither.
      
      If an external `pushback` skill is available in the environment, offer it
      as an addition to the adversarial-review option above — never as a
      replacement for it and never as a requirement to proceed. This skill is
      self-contained by design (Inviolate #6): the menu format works with or
      without that skill present, on any agent.
      
      ---
      
      ## mode `critique-plan`
      
      An adversarial pass against the plan Stage 3 just drafted — the skill
      critiquing its own work product before committing to it. Findings from this
      pass are **fixed inline, in the draft, on the spot**. No separate report is
      written; there is nothing here to hand off, only phases to rewrite or drop
      before Stage 5 writes them to disk.
      
      This is not the approach-menu format above — nothing in this mode asks the
      developer to choose anything. It is the plan's author, one pass later,
      reading the draft adversarially.
      
      ### The governing rule
      
      > **Every phase must name the bug it would catch.** If a phase cannot
      > answer *"what breakage makes these tests go red?"*, it is not a phase —
      > it is coverage theater. Rewrite it or drop it.
      
      This is a gate, not a suggestion. A phase that fails it does not proceed to
      Stage 5 in its current form — it gets a `Catches:` line that names an
      actual breakage, or it does not survive the draft. Walk every phase in the
      draft and ask the question explicitly; do not let a phase through on the
      strength of a plausible-sounding title. "Billing retry & dunning
      integration tests" is not, on its own, an answer — *"a retry exhausting
      without transitioning the account to dunning; a partial refund
      double-crediting"* is (see *Phase format*'s worked example). If the answer
      you write down is vague enough that it would survive being attached to any
      phase in any codebase, it has not actually named a bug — push until it
      names the specific breakage this phase's tests would turn red for.
      
      ### Supporting rules
      
      **1. No success criterion may reduce to "the command exits 0."** A green
      exit code and a covered line are never evidence a test is good — a test
      that asserts nothing produces both, forever, including against code that is
      obviously broken. This is verified tool behavior, not a hypothetical:
      
      - `go test ./...` exits 0 against a package with no `_test.go` files.
      - `jest --passWithNoTests` exits 0 with zero tests collected.
      - A fully-skipped test file exits 0 in every runner examined.
      
      Check every phase's stated success criterion against this. If a phase's
      definition of done is satisfied by any of the three cases above — or by
      anything with the same shape, a run that can complete having asserted
      nothing — rewrite the criterion to name the specific assertion that would
      have to hold, tied to the bug the phase names it would catch. Coverage
      percentage is not evidence either, for the same reason: it is legitimate
      only for finding code with no test at all, never for certifying that a
      covered line is well-tested.
      
      **2. Every test double and fixture is classified, and every `scaffold`
      mock names the refactor that retires it.** Walk the ledger entries the
      draft plan touches — new mocks and fixtures a phase would introduce, and
      existing ones a phase's tests would exercise. Each must carry one of the
      three classes: `boundary` (a genuine external edge — network, clock,
      randomness, a third-party API), `scaffold` (exists only because the
      surrounding code resists testing — test debt), or `data` (constructed test
      data that is permanent and correct — a seeded account, a factory-built
      order). An unclassified double is a gap in the draft, not a detail to leave
      for later. A `scaffold` mock additionally names the refactor that will
      retire it; that refactor is **deferred** — named now, actually done when
      that refactor is actually planned, not during this pass. When in doubt
      between `boundary` and `scaffold`, or when a double goes unexamined,
      default to `scaffold`: the harm is asymmetric, since a `scaffold` silently
      promoted to permanent hides test debt, while a `boundary` mistakenly
      recorded as `scaffold` only leaves a note someone later deletes.
      
      **3. Legacy phases characterize current behavior; they do not fix it.**
      Where a phase's tests would pin behavior of existing code, check that
      every assertion the phase proposes matches what the code *currently does*,
      not what it *should* do. If the pass turns up behavior that looks wrong,
      it is never turned into a different assertion or a code change.
      Characterizing a legacy system and fixing its bugs are two different hard
      problems; this pass exists to police the first, and to stop the second
      from sneaking in disguised as a "more correct" assertion. A wrong-looking
      behavior that clears the findings-log inclusion gate (`build-test-roadmap.md
      § The findings log`) is recorded there — verified, actionable, with the
      phase that pins it — so the developer gets a real to-do list; one that
      does not clear the gate is dropped, not written down as a vague note.
      
      ### After the pass
      
      Every phase that survives can answer the governing rule's question in one
      sentence, has every double and fixture it touches classified, and — where
      it characterizes legacy code — pins current behavior only. Phases that
      can't be fixed to clear this bar are dropped from the draft, not carried
      forward with a note to "revisit."
      
      If a `pushback` skill is available in the environment, running it in
      addition to this pass is strictly additive and should be offered — it is
      not required. This skill remains self-contained (Inviolate #6): the gate
      above is complete on its own, on any agent, whether or not that skill is
      present.
      
    • test-theater.md 10.2 KB
      # Test theater — the weak-test catalog
      
      Reference catalog, not a procedure. Stage 2 (*Grade*) subagents read a test
      partition against this file and cite pattern names from it in their verdicts.
      This file does not tell an agent how to run the grading pass — see the skill's
      Stage 2 instructions for that — it only names what to look for and what
      vocabulary to use when reporting it.
      
      **Governing rule, stated explicitly because it is load-bearing for every
      pattern below:** a passing command and a covered line are never evidence that
      a test is good. Exit status proves the test executed without throwing.
      Coverage proves a line ran while some assertion (anywhere in the test) held.
      Neither proves the test would fail if the behavior it's supposedly pinning
      changed. A test that never asserts on behavior can have 100% line coverage
      and a green exit code forever, on every version of the code, including
      versions that are obviously broken. Coverage is legitimate for one purpose
      only — finding code with *no test at all* — and illegitimate for every other
      purpose, including certifying that a test which does exist is worth keeping.
      
      Every pattern entry below states, as a fixed field, **what breakage it lets
      through** — the regression that could ship with this test green. A pattern
      entry that cannot fill in that field is not a weak-test pattern; drop it
      before it enters a verdict.
      
      No pattern here is tied to a language, framework, or test runner. Examples
      are illustrative pseudocode or named from more than one ecosystem; the
      detector cues describe a *shape*, not a syntax, because the shape recurs
      everywhere — a mock-echo in Python looks like a mock-echo in Go.
      
      ## Verdict shape
      
      Per weak test found, a Stage 2 verdict is: `file`, `line`, **pattern name**
      (one of the names below, verbatim), a one-sentence statement of why it fails
      to catch regressions, and a suggested replacement. Pattern names are the
      `##` headings below (e.g. `over-mocked test`, `happy-path-only test`) — cite
      the heading text, not a paraphrase, so verdicts stay greppable across a
      400-item list.
      
      This catalog does not define severity or ranking; that's a Stage 2 concern
      (top-K by severity, full list to `paad/test-roadmap/test-suite-analysis.md`). It only
      defines what the items in that list are called and how to tell them apart.
      
      ---
      
      ## assertion-free test
      
      **Detector cue:** the test runs code and then asserts only that *something
      non-crashing happened* — a null/undefined check, a "no exception thrown," a
      type check, an existence check — with no assertion on the *value* the
      behavior was supposed to produce. `assert result is not None` is the
      canonical case: it passes for a correct result, a wrong result, and often a
      partially-constructed garbage result, as long as it isn't literally absent.
      
      **What breakage it lets through:** any regression that changes the *content*
      of the result without making it disappear. A billing calculation that starts
      returning the wrong total, a parser that returns a malformed-but-non-null
      tree, a lookup that returns the wrong record — all pass.
      
      **What a real replacement asserts instead:** the specific expected value, or
      a specific expected shape with field-level checks (`total == 4200`, not
      `total is not None`), tied to a case the phase names as worth pinning.
      
      ---
      
      ## snapshot-only test with no meaningful invariant
      
      **Detector cue:** the test captures whatever the code currently outputs
      (a serialized blob, a rendered string, a full object dump) and asserts
      future runs match that capture byte-for-byte, with no comment or companion
      assertion identifying *which part* of the snapshot is the behavior under
      test. Passes on regeneration. The developer's daily move when it goes red is
      to regenerate it, because nothing in the test tells them what would make a
      diff meaningful versus noise.
      
      **What breakage it lets through:** none, technically — the first time it's
      run against a regression it *will* go red. The breakage is procedural, not
      detectable-in-code: the fix-it reflex for a failing snapshot is
      "regenerate," and a snapshot with no named invariant gives a developer under
      time pressure no way to tell a real regression from formatting churn before
      regenerating over it. In practice this pattern converts a real regression
      into a silently accepted new baseline.
      
      **What a real replacement asserts instead:** either a narrower assertion on
      the specific field/property that matters, or a snapshot scoped tightly
      enough (a single computed value, not a whole rendered page) that any diff is
      self-evidently the thing under test, plus a comment naming the invariant the
      snapshot exists to protect.
      
      ---
      
      ## tautological assertion
      
      **Detector cue:** the test configures a mock or stub to return a value, then
      asserts that calling the mocked function returns that value. The assertion
      checks the test double's own configuration, not the code under test. A
      variant: asserting a spy was called with exactly the arguments the test just
      constructed and passed in, with no check on what the code *did* with the
      result of that call.
      
      **What breakage it lets through:** every regression in the actual code path
      under test, because the assertion never touches the real code — it touches
      the double. The code under test could be deleted entirely, calling the mock
      directly, and the assertion would still pass.
      
      **What a real replacement asserts instead:** an assertion on what the
      *production code* did as a consequence of the mock's return value — the
      downstream state change, the value it computed from the mock's output, the
      side effect it triggered — not the mock's return value reflected back at
      itself.
      
      ---
      
      ## exit-status-or-coverage-as-evidence
      
      **Detector cue:** a test file, script, or CI step whose only success
      criterion is "the process exited zero" or "coverage stayed above N%," with
      no assertions inside the run that check specific behavior — e.g. a
      smoke-test script that just imports every module and exits, or a coverage
      gate treated as the definition of "tested."
      
      **What breakage it lets through:** any regression that doesn't throw and
      doesn't touch the covered-vs-not-covered boundary — which is most
      regressions. A function can be exercised (covered) by a test that asserts
      nothing, return a wrong value, and both the exit code and the coverage
      report stay green.
      
      **What a real replacement asserts instead:** behavior-level assertions
      inside the run itself. Coverage output is retained only as a *gap-finder* —
      "this branch has zero coverage, so nothing pins it" — never cited as
      evidence that a covered branch is adequately tested.
      
      ---
      
      ## over-mocked test
      
      **Detector cue:** enough of the collaborators around the unit under test are
      mocked or stubbed that the mocks, taken together, implement the behavior the
      test claims to verify. A giveaway: reading the test requires reading the
      mock setup to know what the "real" answer would be, because the mock *is*
      the answer. Another: a mock stands in for a same-process collaborator
      (another class in the same codebase) rather than a genuine external edge.
      
      **What breakage it lets through:** any regression in the mocked
      collaborator's real behavior, and any regression in how the unit under test
      actually integrates with that collaborator (wrong method called, wrong
      argument order, wrong error handling on a real failure the mock is never
      configured to produce).
      
      **What a real replacement asserts instead:** depends on what the mock is
      standing in for — this is exactly the `boundary` / `scaffold` / `data`
      distinction from the test-double & fixture ledger, and grading should
      classify the mock accordingly, not just flag the test:
      
      - If the double stands in for a genuine external edge (network, clock,
        randomness, a third-party API) — a `boundary` — keep the double, but move
        the assertion to what the unit under test *does* with the boundary's
        response, including its failure modes, not to the boundary's own
        configured return value.
      - If the double exists only because the surrounding code resists testing
        any other way — a `scaffold` — the replacement isn't a better mock, it's
        the refactor that retires the mock. Name that refactor in the verdict;
        don't paper over test debt with a more elaborate double.
      - If what's being asserted on is actually constructed test data (a seeded
        record, a factory-built object) rather than a stand-in for behavior, it
        isn't this pattern at all — classify it `data` and grade it as a real
        fixture, not a mock.
      
      ---
      
      ## happy-path-only test
      
      **Detector cue:** every test in a phase's partition exercises the success
      case — valid input, available dependency, expected response — and none
      exercises the boundary the phase names as the reason the phase exists: the
      error path, the timeout, the malformed input, the concurrent write, the
      retry-and-give-up. The phase's own stated purpose (see *Phase format* and
      Inviolate #5 — every phase names the bug it would catch) is checkable
      against what's actually asserted; a happy-path-only suite for a phase titled
      around retry/dunning behavior, timeout handling, or invalid-input rejection
      is a mismatch between the phase's stated purpose and its tests.
      
      **What breakage it lets through:** any regression in error handling,
      retries, timeouts, validation, or edge-of-range input — which for
      integration and e2e tiers is frequently the entire reason the phase was
      planned. A payment-retry phase with only successful-charge tests catches
      nothing about the retry logic it exists to pin.
      
      **What a real replacement asserts instead:** at least one case per boundary
      the phase names — the declined charge, the expired token, the dependency
      that's down, the input one past the valid range — asserting the specific
      handling behavior (retried once then surfaced, rejected with a specific
      error, degraded to a specific fallback), not merely that an exception was
      thrown.
      
      ---
      
      ## Using this catalog
      
      A test can match more than one pattern at once (a tautological assertion
      inside an otherwise happy-path-only phase is both, and both survive in the
      verdict). Matching zero patterns is not a certificate that a test is good —
      this catalog names known-weak shapes; it is not exhaustive, and a test
      absent from every pattern here still has to actually assert on behavior the
      phase cares about. Absence of theater is necessary, not sufficient.
      
  • SKILL.md 9.2 KB
    ---
    name: test-roadmap
    description: >
      EXPERIMENTAL. Analyzes a repository and any existing test suite, grades
      existing tests for weakness, classifies mocks, emits a phased roadmap for
      building a test suite that catches real regressions, then executes those
      phases one at a time. Use when planning or building a test suite, assessing
      whether existing tests are worth anything, adding tests to a legacy codebase,
      or when the user mentions test coverage, test strategy, or weak tests. Not for
      reviewing a branch for bugs, and not for fixing the bugs it logs.
    compatibility: Requires git
    ---
    
    **On invocation:** announce "Running paad:test-roadmap v1.31.0" before anything else.
    
    > **EXPERIMENTAL SKILL.** Its arguments, output paths, and behavior may
    > change or be withdrawn in any release, including patch releases. It is not
    > covered by the semver guarantees the other paad skills carry. Unlike every
    > other paad skill, this one **writes code and commits it** — tests, one commit
    > per phase, onto your working branch. Report rough edges at
    > <https://github.com/Ovid/paad/issues>.
    
    # test-roadmap
    
    This file is the router. The routing itself stays dumb on purpose: one check,
    two routes, nothing else. A couple of preconditions guard it first. All the
    substance — grading, planning, writing tests, bug injection — lives in
    `references/` and loads only once routing has picked a mode.
    
    **Pre-flight and routing:**
    
    ```dot
    digraph route {
      "Inside a git repo?" [shape=diamond];
      "STOP: needs a git checkout" [shape=box, style=bold];
      "Detached HEAD?" [shape=diamond];
      "origin/HEAD pointer resolves?" [shape=diamond];
      "Current branch == default branch?" [shape=diamond];
      "Name matches a well-known primary (main/master/trunk/develop/...)?" [shape=diamond];
      "ASK: is this your main development line?" [shape=diamond];
      "OFFER: create a working branch" [shape=box];
      "Developer agrees?" [shape=diamond];
      "STOP: never build on the primary branch" [shape=box, style=bold];
      "git switch -c <name>" [shape=box];
      "paad/test-roadmap/test-roadmap.md exists?" [shape=diamond];
      "Load references/build-test-roadmap.md (Detect, Grade, Plan, Critique, Write)" [shape=box];
      "Load references/execute-test-roadmap.md (next phase, break-it-check, commit)" [shape=box];
    
      "Inside a git repo?" -> "STOP: needs a git checkout" [label="no"];
      "Inside a git repo?" -> "Detached HEAD?" [label="yes"];
      "Detached HEAD?" -> "OFFER: create a working branch" [label="yes"];
      "Detached HEAD?" -> "origin/HEAD pointer resolves?" [label="no"];
      "origin/HEAD pointer resolves?" -> "Current branch == default branch?" [label="yes (authoritative)"];
      "origin/HEAD pointer resolves?" -> "Name matches a well-known primary (main/master/trunk/develop/...)?" [label="no"];
      "Current branch == default branch?" -> "OFFER: create a working branch" [label="yes"];
      "Current branch == default branch?" -> "paad/test-roadmap/test-roadmap.md exists?" [label="no"];
      "Name matches a well-known primary (main/master/trunk/develop/...)?" -> "OFFER: create a working branch" [label="yes"];
      "Name matches a well-known primary (main/master/trunk/develop/...)?" -> "ASK: is this your main development line?" [label="no"];
      "ASK: is this your main development line?" -> "OFFER: create a working branch" [label="yes / unsure"];
      "ASK: is this your main development line?" -> "paad/test-roadmap/test-roadmap.md exists?" [label="no"];
      "OFFER: create a working branch" -> "Developer agrees?";
      "Developer agrees?" -> "STOP: never build on the primary branch" [label="no"];
      "Developer agrees?" -> "git switch -c <name>" [label="yes"];
      "git switch -c <name>" -> "paad/test-roadmap/test-roadmap.md exists?";
      "paad/test-roadmap/test-roadmap.md exists?" -> "Load references/execute-test-roadmap.md (next phase, break-it-check, commit)" [label="yes"];
      "paad/test-roadmap/test-roadmap.md exists?" -> "Load references/build-test-roadmap.md (Detect, Grade, Plan, Critique, Write)" [label="no"];
    }
    ```
    
    ## Before routing: confirm you're in a repo
    
    `compatibility: Requires git` above means this skill needs a working git
    checkout — build mode fans out grading subagents against the tree as it
    stands, and execute mode's `break-it-check` gate runs bug injection in a
    disposable `git worktree`. If the current directory isn't inside a git repo,
    say so and stop before loading either mode file.
    
    ## Before routing: confirm you're on a working branch
    
    This skill commits as it goes — build mode commits the roadmap, execute mode
    commits each phase's tests — all onto the branch you are on right now. A
    half-built test suite landing on the developer's main development line is exactly
    what this check prevents, the same spirit as running a code review on a feature
    branch rather than on `main`. So before routing, confirm the current branch is a
    *working* branch, not the primary one.
    
    Identify the primary branch from repo signals, in order — stop at the first that
    decides:
    
    1. **Detached HEAD** — `git symbolic-ref -q HEAD` prints nothing. There is no
       branch for the suite to accumulate on at all; treat it like being on the
       primary branch and offer a working branch (below).
    2. **The repo's own default-branch pointer** — `git symbolic-ref -q --short
       refs/remotes/origin/HEAD` resolves to e.g. `origin/main`; strip the remote
       prefix for the default branch name. If the current branch (`git symbolic-ref
       -q --short HEAD`) equals it, you are on the primary branch. This is the
       authoritative signal and needs no built-in list of names.
    3. **No such pointer** (a local-only repo, or one where it was never set) — fall
       back to the well-known primary names: `main`, `master`, `trunk`, `develop`,
       `devel`, and the like — **examples, not a closed list**, the same stance
       Stage 1 takes on manifests. If the current branch name matches one, treat it
       as primary. If it matches none *and* step 2 could not confirm, **ask the
       developer once, in plain words**, whether this is their main development line —
       never silently proceed on a branch that might be it. Being wrong toward asking
       costs a keystroke; being wrong toward building on the main line is the harm
       this check exists to prevent.
    
    **When the current branch is the primary one (or HEAD is detached), do not route
    yet.** Say why in plain words — *"I build the test suite up commit by commit, and
    you don't want those landing on your main branch while it's half-done, so let's
    put them on a working branch"* — then offer to make one: propose a name
    (`test-roadmap` is a fine default), and on the developer's OK run `git switch -c
    <name>` (or `git checkout -b <name>` on older git) and continue to routing. If
    they decline, stop — never build or execute on the primary branch.
    
    This is the skill's **one** branch: a single working branch, created at
    invocation only when needed. It is **not** a per-phase branch — execute mode
    still commits every phase onto whatever working branch you are on, and the suite
    accumulates there (see `references/execute-test-roadmap.md § What execute mode
    writes`). Both mode files assume this check has already passed and never re-run
    it; the guard lives here, once.
    
    ## Route
    
    ```
    paad/test-roadmap/test-roadmap.md exists?  →  load references/execute-test-roadmap.md
                           absent  →  load references/build-test-roadmap.md
    ```
    
    That is the entire routing logic — one file existence check, two branches.
    `paad/test-roadmap/test-roadmap.md` is the roadmap this skill itself writes at the end of
    build mode, so its presence is exactly the signal that a previous run already
    did Detect/Grade/Plan/Critique/Write and there is a phased plan to execute
    against. Its absence means this is either the first run against this repo, or
    a run after that file was deleted — either way, build it.
    
    If it ever grows a third condition, that is a signal something has been put
    in the wrong place — take it back to `build-test-roadmap.md` or
    `execute-test-roadmap.md`, not to this file.
    
    This router never loads `references/break-it-check.md`,
    `references/test-pushback.md`, or `references/test-theater.md` directly.
    Those three are loaded by whichever of the two mode files needs them, at the
    point in their own protocol that needs them — not from here.
    
    ## Entering build mode
    
    Before build mode can plan anything, it needs to know what it's planning
    for. The first thing it does — Stage 1, Detect — is identify the stack from
    manifests and config rather than assumption (`package.json`,
    `pyproject.toml`, `go.mod`, `Cargo.toml`, `Gemfile`, and so on: examples, not
    a closed table), then determine how tests are invoked and what test files
    already exist. The skill must never hardcode a language; where it needs a
    per-ecosystem fact, it looks for the signal in the repo instead of consulting
    a built-in list.
    
    The full five-stage protocol — Detect, Grade, Plan, Critique, Write — lives
    in `references/build-test-roadmap.md`. Load it now if
    `paad/test-roadmap/test-roadmap.md` is absent.
    
    ## Entering execute mode
    
    Load `references/execute-test-roadmap.md` now if `paad/test-roadmap/test-roadmap.md`
    exists. It reads that file's `## Decisions` section once, selects the next
    phase per the completion protocol, and gets on with writing tests — it does
    not re-detect or re-ask anything build mode already settled.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related