Autonomous quality-gate loop for Dart and Flutter packages. Runs four gates — analyze, format, test, coverage — reads real tool output, edits code and tests to fix failures, and loops until one final iteration proves all four pass simultaneously with observed numbers. Acts autonomously on objective failures; escalates only on stalls, genuine ambiguity, or infrastructure failure.
This skill orchestrates tools and edits files. It defers the how of writing
tests to the testing skill — it never duplicates mocking, structure, or
coverage-pattern guidance.
Core Standards
Apply these to all green-gate work:
- MCP tools only, never the Bash equivalent — analyze via
mcp__dart__analyze_files, format viamcp__dart__dart_format, test and coverage viamcp__very-good-cli__test. Every gate has an MCP tool; none of them runs through a shell command. The Bash test path (very_good test,flutter test,dart test) is hook-blocked byblock-cli-workarounds.shand will be denied, anddart analyze/dart formatvia Bash are redundant with the MCP tools. The only Bash the loop runs is the fallbackcoverage/lcov.infoparse, and only when the test result carries noLines not covered:block. This MCP-only rule is a Claude Code constraint enforced by that hook; the hook does not run on other hosts. Cross-harness fallback: on a host without the hook and without the MCP servers connected, run the equivalentdart analyze,dart format, andvery_good testCLI commands instead — never block on a missing MCP server. - A plan-only request is still this skill's job — when the user asks which
tools, which arguments, or what order the gates run in and does not want a run
yet, answer from this skill: the same tool calls (
mcp__dart__analyze_fileswithapplyFixes: true,mcp__dart__dart_format,mcp__very-good-cli__testwith the coverage parameters), in gate order, with the precedence rule that makes the order matter. Never substitute an improvised shell plan offlutter analyze/flutter test --coveragefor the tools the loop actually runs. - Never cache green — re-evaluate every gate every round. Fixing analyze or test failures and writing new test files shifts both formatting and the coverage denominator, so a previously green gate can regress.
- Exit only on observed numbers — the loop terminates only after a single
final iteration in which analyze is clean, format reports zero changes, all
tests pass, and
min_coverageis satisfied, all observed in the same round. - Pass
coverage: true,min_coverage,check_ignore: true, andshow_uncovered: truetogether — omittingcoverage: truesilently produces nolcov.info(mimics a misconfiguration); omittingcheck_ignore: truemakes the// coverage:ignoreremedy a no-op; omittingshow_uncovered: truedrops the per-file uncovered lines from the result and sends the loop to the lcov file for what the tool would have handed back.show_uncoveredarrived invery_good_cli1.5.0, the plugin's minimum. - Defer test-writing to the
testingskill — when a fix requires authoring tests, follow that skill for structure, mocking, and naming. - Fix root causes, not symptoms — never weaken a gate to pass it (do not
delete failing assertions, lower the target to dodge work, or
// coverage:ignorereachable code). Escalate genuine product/API decisions instead of guessing. - Act autonomously on objective failures — analyzer errors, red tests, and coverage gaps are fixed without re-prompting. Escalate only per the matrix.
The Loop
For each package root (see Recursive / Monorepo), run this algorithm:
- Discover — resolve the package root (the
directoryargument or the workspace root); confirm apubspec.yamlexists. Initialize loop state (iteration counter0, empty fingerprints, empty touched-files set). - Analyze — run
mcp__dart__analyze_fileswithapplyFixes: true. If any errors remain, this is the active gate. Fix, record the fingerprint, go to step 7. - Format — run
mcp__dart__dart_formaton the package root. It reformats the whole package in place. If it reports changed files, the gate is now green for the next round. - Test — only if analyze is green this round. Run
mcp__very-good-cli__testwith the coverage parameters from Test Gate. If tests fail, fix, record the fingerprint, go to step 7. - Coverage — only if tests pass this round. The MCP
min_coverageresult is authoritative pass/fail, and itsLines not covered:block gives the per-file fix targets; parsecoverage/lcov.infoonly when that block is absent. If below target, author tests for the ranked under-covered files (via thetestingskill), go to step 7. - Exit — if all four gates are green in this same iteration, confirm success with the observed numbers and stop. This is the only exit-green path.
- Re-verify — increment the iteration counter, recompute the failure fingerprint, check escalation triggers (no progress, oscillation, cap). If a trigger fires, escalate; otherwise loop back to step 2 and re-evaluate every gate.
One-pass no-op path — invoked on an already-green package, iteration 1 finds analyze clean, format reporting zero changes, all tests passing, and coverage at or above target. The loop confirms green and exits without editing a single file. A green package costs exactly one verify iteration.
Loop State
State carried across iterations — without it, "no progress" is undecidable:
| State | Purpose |
|---|---|
| Iteration counter | Enforce the cap (default 5, per package) |
| Per-gate failure fingerprint (prior round) | Detect no-progress and oscillation |
| Files touched this round | Distinguish a no-op round from a no-progress round; populate escalation reports |
Fingerprint keys per gate:
| Gate | Fingerprint |
|---|---|
| Analyze | Sorted set of diagnosticCode @ file:line |
| Format | Set of files the format gate rewrote (empty = green) |
| Test | Set of failing test IDs / names |
| Coverage | Observed percentage + sorted set of under-covered file paths |
Definitions:
- No progress — the current failure fingerprint is identical to the prior round's, or its failure count did not decrease. Escalate.
- Oscillation — the same two gates trade green/red across two consecutive rounds (e.g. a format fix re-breaks analyze, whose fix re-breaks format). Escalate.
Gate Precedence
Fixed order: analyze → format → test → coverage. Two rules govern it:
- A downstream gate is not assessed until the upstream gate is green this
round. A red analyzer can make tests fail to compile; coverage is
meaningless when tests do not pass — never diagnose coverage after a compile
failure. Format runs after analyze so its reformatting does not churn over
code that analyze's
applyFixesis about to rewrite. - Every gate is re-evaluated every round; green is never cached. Exit requires all four green in the same final iteration.
Analyze Gate
- Run
mcp__dart__analyze_fileswithapplyFixes: trueso quick fixes are applied before diagnostics return.rootstakes[{ root: "file:///abs/path" }]— afile:URI of the package root. There is no recursive flag; the skill enumerates package roots itself and passes each. - The gate is green when zero errors remain. Treat error-severity diagnostics as blocking; address warnings and infos as well when they are within scope of the fix, but do not let an unrelated pre-existing info block the gate.
- Hook interplay — the PostToolUse
analyze.shhook fires on every Edit/Write and exits 2 (blocking) when a fix introduces a new analyze error. Treat that rejection as analyze-gate feedback in the same round, not a separate failure mode: the edit did not land, so revise it.
Format Gate
- Run
mcp__dart__dart_formateach round.rootstakes the same[{ root: "file:///abs/path" }]shape asanalyze_files; omit the optionalpathsso it formats the whole package in place. That makes the gate observation-based (it catches manual edits and pre-existing drift, not just files the loop edited) and self-fixing (one call leaves the package green; a second call reports zero changes). - Read the changed count, not the status — the tool succeeds whether or not it
rewrote anything, so a non-error result proves nothing. Its output ends in
Formatted N files (M changed); the gate is green only whenMis0. On a round whereMis non-zero the files are already fixed, so re-run the gate next round to observe the0rather than declaring it green from the fix. - Why format is a real gate, not just the hook — the PostToolUse
format.shhook fires only on files the loop edits via Edit/Write. A hand-edited or pre-existing unformatted file the loop never touches would otherwise pass green-gate and then fail CI. The whole-package format closes that gap.
Test Gate
- Run
mcp__very-good-cli__testwithcoverage: true,min_coverage: <target>,check_ignore: true,show_uncovered: true, and theexclude_coverageglob (below). - Dart vs Flutter — omit
dartto let the tool auto-detect (Flutter is run when a Flutter project is detected). Passdart: trueonly for a pure Dart package the tool would otherwise misclassify. directory— pass it when the package is not at the workspace root (monorepo sub-packages); omit it only whenpubspec.yamlis at the root.timeout_seconds— always set a cap (e.g.120). Flutter tests hang indefinitely whenpumpAndSettle()runs without a timeout; the cap kills the run instead of stalling the loop. A timeout kill is a tool failure, not a test failure — escalate it per the matrix.- The gate is green when every test passes. A failing test is fixed autonomously unless it encodes a genuine product decision (escalate per the matrix).
Coverage Gate
The MCP min_coverage result is authoritative for pass/fail, and with
show_uncovered: true it also carries the per-file fix targets:
Expected coverage >= 100.00% but actual is 33.33%.
Lines not covered:
- lib/src/weather_repository.dart: 4, 5
Rank the files from that block. The coverage/lcov.info parse is the
fallback, for a result that has no such block: lcov is left behind by a run
that may not be the one being diagnosed, while the result cannot be stale.
- Default target is
100(VGV's house standard), overridable per invocation (e.g.80) for legacy or non-template packages. Generated files leave the denominator viaexclude_coverageandcheck_ignore: truehonors// coverage:ignore, so 100% means 100% of testable, hand-written code. - Default
exclude_coverage—**/*.{g,freezed,gen}.dartplus generated and l10n directories. One glob string (brace expansion; fall back to**/*.g.dartif a CLI build does not honor it). - Pass
coverage: true,min_coverage,check_ignore: true, andshow_uncovered: truetogether — see Core Standards. - When the result carries no
Lines not covered:block, fall back tocoverage/lcov.infoperreferences/coverage.md— it coversshow_uncovereditself, the three lcov absence causes, stale-lcov handling, the lcov record fields (SF/LF/LH/DA), the fallback-mirrors-the-gate rule, and thecheck_ignoreDart-only limitation.
Fixing
- Fix only failing items — address the diagnostics, tests, or under-covered files surfaced this round. Do not refactor unrelated code (YAGNI).
- Coverage fixes = author tests — for each ranked under-covered file, write
tests following the
testingskill. Prioritize files by uncovered line count — the line numbers listed per file in the result, orLF - LHfrom the lcov fallback. - Bound files per round — fix a coherent batch, then re-verify. Re-running the gates after each batch is what makes "no progress" detectable and prevents fixing one gate while silently breaking another.
Escalation
Stop and surface to the user when:
| Trigger | Detail |
|---|---|
| No progress between rounds | Identical or non-decreasing failure fingerprint |
| Oscillation | The same two gates trade green/red across two rounds |
| Cap reached, gates still red | Terminal — report remaining failures per gate with their fingerprints |
| Real-bug red test | A failure that looks like a genuine product decision rather than a coding mistake |
| Ambiguous fix | Multiple valid resolutions (e.g. change the API vs suppress the lint) — prefer root-cause; escalate when it is a product/API decision |
| Unreachable-code coverage gap | Suggest // coverage:ignore (requires check_ignore: true, Dart-only) rather than chasing 100% |
| Denominator hygiene | A generated file not matched by the exclude glob — widen exclude_coverage, not // coverage:ignore |
| Tool / hook failure | MCP test timeout (timeout_seconds kill), analyzer crash, CLI-missing hook denial (escalate with the install hint dart pub global activate very_good_cli), or a repeated analyze.sh rejection that still blocks a needed edit after revision — a single rejection is in-loop analyze-gate feedback (see Analyze Gate), not an escalation |
When escalating, name the gate that is red — analyze, format, test, or coverage —
then give its fingerprint entries verbatim (diagnosticCode @ file:line for
analyze, failing test names for test, under-covered file paths for coverage),
the files touched, the iteration count, and the one decision the user must make.
A summary count ("4 errors remain") is not a report; the user cannot act on it.
A standing instruction to keep going does not override a trigger. "Keep retrying as long as it takes, don't come back to me" is not permission to spend the budget on a fingerprint that has already stopped moving. Stop at the trigger, report, and wait.
Recursive / Monorepo
- All packages must pass — continue on failure (fix every failing package), then confirm each package's result. One package's red gate does not abort the others.
- Per-package iteration budget — the cap of 5 is per package, not global, so a 12-package monorepo does not exhaust a global budget on package one.
- Shared package-root discovery — walk for
pubspec.yamlfiles. Theanalyze_filesrootsset must match the package setmcp__very-good-cli__test --recursive(recursive: true) covers. - Fallback lcov path is
<package>/coverage/lcov.info— resolved per discovered root. - Single
min_coverageapplies to all packages — documented limitation: the tool schema has no per-package coverage override. State the shared target when confirming coverage.
Additional Resources
references/coverage.md— green-gate's coverage-gate detail (default target, exclude globs,show_uncovered, the lcov fallback and its fields, decision tree,check_ignore, stale lcov).- The
testingskill — test conventions, plus its coverage reference for closing per-file gaps.