Green Gate
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 via mcp__dart__dart_format, test and coverage
via mcp__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 by block-cli-workarounds.sh and will be denied, and
dart analyze / dart format via Bash are redundant with the MCP tools.
Bash is reserved for parsing coverage/lcov.info — nothing else.
- 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_files
with applyFixes: true, mcp__dart__dart_format, mcp__very-good-cli__test
with the coverage triple), in gate order, with the precedence rule that makes
the order matter. Never substitute an improvised shell plan of flutter analyze / flutter test --coverage for 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_coverage is satisfied, all observed in the same round.
Declaring success from memory is forbidden; confirm success only with the
actual numbers observed in that final round.
- Pass
coverage: true, min_coverage, and check_ignore: true together —
omitting coverage: true silently produces no lcov.info (mimics a
misconfiguration); omitting check_ignore: true makes the // coverage:ignore
remedy a no-op.
- Defer test-writing to the
testing skill — when a fix requires authoring
tests, follow skills/testing/SKILL.md 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:ignore
reachable 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
directory argument or the
workspace root); confirm a pubspec.yaml exists. Initialize loop state
(iteration counter 0, empty fingerprints, empty touched-files set).
- Analyze — run
mcp__dart__analyze_files with applyFixes: true. If any
errors remain, this is the active gate. Fix, record the fingerprint, go to
step 7.
- Format — run
mcp__dart__dart_format on 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__test
with 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_coverage result is
authoritative pass/fail. Parse coverage/lcov.info for the displayed
percentage and per-file fix targets (advisory). If below target, author tests
for the ranked under-covered files (via the testing skill), 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 SF files |
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 parse
lcov.info after a compile
failure. Format runs after analyze so its reformatting does not churn over
code that analyze's applyFixes is 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_files with applyFixes: true so quick fixes are
applied before diagnostics return. roots takes [{ root: "file:///abs/path" }]
— a file: 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.sh hook 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_format each round. roots takes the same
[{ root: "file:///abs/path" }] shape as analyze_files; omit the optional
paths so 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 when M is 0. On a
round where M is non-zero the files are already fixed, so re-run the gate next
round to observe the 0 rather than declaring it green from the fix.
- Why format is a real gate, not just the hook — the PostToolUse
format.sh
hook 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__test with coverage: true, min_coverage: <target>,
check_ignore: true, and the exclude_coverage glob (below).
- Dart vs Flutter — omit
dart to let the tool auto-detect (Flutter is run
when a Flutter project is detected). Pass dart: true only 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 when pubspec.yaml is at the root.
timeout_seconds — always set a cap (e.g. 120). Flutter tests hang
indefinitely when pumpAndSettle() 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. The
coverage/lcov.info parse is advisory — it supplies the displayed
percentage and per-file fix targets.
- 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 via exclude_coverage and check_ignore: true honors
// coverage:ignore, so 100% means 100% of testable, hand-written code.
- Default
exclude_coverage — **/*.{g,freezed,gen}.dart plus generated and
l10n directories. One glob string (brace expansion; fall back to **/*.g.dart
if a CLI build does not honor it).
- Pass
coverage: true, min_coverage, and check_ignore: true together —
see Core Standards.
- When
coverage/lcov.info is missing or below target, follow the decision
tree and parsing rules in references/coverage.md —
it covers the three absence causes, stale-lcov handling, the lcov record fields
(SF/LF/LH/DA), the advisory-mirrors-the-gate rule, and the
check_ignore Dart-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
SF file,
write tests following skills/testing/SKILL.md. Prioritize files by uncovered
line count (LF - LH).
- 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.
- Never weaken a gate — no deleted assertions, no lowered target to dodge
work, no
// coverage:ignore on reachable code.
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 SF 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.yaml files. The
analyze_files roots set must match the package set
mcp__very-good-cli__test --recursive (recursive: true) covers.
- lcov path is
<package>/coverage/lcov.info — resolved per discovered root.
- Single
min_coverage applies 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, lcov fields, decision tree,
check_ignore, stale lcov).
skills/testing/SKILL.md — how to write Dart unit, Flutter widget, and golden
tests (structure, mocktail mocking, naming).
skills/testing/references/coverage.md — coverage-driven test patterns
(copyWith, branches, error paths) for closing per-file gaps.
hooks/scripts/block-cli-workarounds.sh — why the Bash test path is blocked and
every gate runs through its MCP tool.