Skip to content

The deep-qa agent

deep-qa writes tests, proves they can fail, fixes what they catch, and stops. It is the only agent here that is allowed to change lib/, and it is deliberately hard to satisfy.

Terminal window
/qa lib/presentation/widgets/components/floating_nav_bar.dart # one target
/qa diff # everything uncommitted
/qa audit test/presentation/bloc/search_screen # are these tests real?
/qa gate # just run the gate and fix

With no argument it asks. It does not guess a target.

The test suite went from nothing to 2,297 tests in two weeks, and then the audit found tests that could not fail. One asserted isA<WatchlistScreenCollectionLoadedState>() and passed happily while copyWith blanked a watchlist’s artwork on every save — bug-log #27. The assertion was true for a correct state and equally true for a broken one.

So the agent’s contract is not “add tests”. It is “add tests and prove each one can fail”. The proof is mutation: break the code on purpose, watch the new test go red, put the code back. A test that stays green through its own mutation is deleted, not kept.

/qa <target>
│
▼
SCOPE ─────── record the before row: git status, analyzer count, coverage floors.
│ Open a ledger in the scratchpad; update it every phase, so a long
│ run can be resumed by reading one file instead of starting over.
▼
CONTRACT ──── what is this SUPPOSED to do? From the API fixtures, the .arb
│ strings, the entity, the sibling tests — never from the
│ implementation. Reading the code and writing it back down as an
│ assertion produces a test that agrees with any behaviour at all.
▼
RISK ──────── list the failure modes BEFORE writing a line. Missing API field,
│ null, [] where an object was promised, empty list, offline,
│ cache hit, 320-wide screen, longest string, signed out, disposed
│ mid-flight. This is the step that finds bugs; the tests just
│ record what it found.
▼
WRITE ─────── one behaviour per test, named as a sentence, asserting the
│ payload rather than the state's type, using test/helpers/.
▼
MUTATE ────── break the code. Every new test must go red. Restore with
│ git checkout --. Re-run green. ──┐
│ │ survived? the test
│ For widgets the blunt version works: │ is vacuous — back
│ swap pumpApp's child for an empty box │ to WRITE
│ and list who still passes. ──┘
▼
TRIAGE ────── a red test is exactly one of three things:
│ a test bug → fix the test
│ a real defect → fix lib/, or escalate if off-limits
│ intended behaviour→ pin it, flag it, change nothing
│ Never green by deletion.
▼
GATE ──────── tool/quality.sh fast. 10 format · 11 analyzer · 12 generated
│ stale · 13 tests · 14 coverage · 15 goldens · 16 e2e.
▼
LAND ──────── commit (fix + its test together, citing bug-log #N), report, stop.

It fixes clear, contained defects in lib/ — a missing null guard, a dropped field, a widget that overflows, a state carrying the wrong payload. It stops and hands you a filled-in escalation, with the failing test as evidence, for:

  • the player (lib/presentation/widgets/player/, ottstack_player, kvideo)
  • payments and IAP
  • auth, sessions and EncryptionHelper
  • Hive field numbers and typeIds
  • the shape of injection_container.dart
  • local_lib/ and anything generated

It also never pushes, never opens a PR, never amends an existing commit, never removes a null check, and never lowers a baseline.

One source, two runtimes. The agent file is real; everything else is a symlink, the same shape as the vendored flutter-* skills.

.claude/agents/deep-qa.md the agent ◄── .pi/agents/deep-qa.md
.agents/skills/deep-qa/SKILL.md the loop ◄── .claude/skills/deep-qa
references/ rules · recipes · harness · traps · gate
.claude/commands/qa.md /qa in Claude Code
.pi/prompts/qa.md /qa in pi

The frontmatter is name and description and nothing else. That is the intersection of the two parsers: tools would need lowercase names in pi and capitalised ones in Claude Code, and model means a different thing in each. Omitting both gives full tools and the inherited model on both sides, from one file.

The references are the part worth maintaining. traps.md in particular is the accumulated cost of every afternoon someone lost to VideoPlayerController.dispose() hanging a whole test file, or to the 800×600 default surface silently clamping a layout assertion. When something new bites, add it there.