agentleFS
andreymudriandreymudri/fleetmates/skills/phase-gate/SKILL.md

phase-gate

Skill3 starsChanged 10 days agoMITView on GitHub

Use when a phase finished and needs an automated verdict before integration - runs command, agent, and MCP checks and decides PASS or FAIL.

5 other projects use a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

10 rules. 0 are common, 4 are distinctive, and 6 are very specific to this project.

  • 0

    Common

    Most of the 4 edited versions we read keep these.

  • 4

    Distinctive

    This author's own choices. Worth a look.

  • 6

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Never omit the model to inherit the session's: in a session on a smaller model (haiku or sonnet) that would have the reviewer grading every …

    line 52
  2. Very specific:

    After any refusal, hand gate --results nothing at all: only a path printed on a results written to … line is this command's answer, and no …

    line 106
  3. Very specific:

    Add "source": "file" to an entry recovered from a reviewer's findings file rather than from its returned response.

    line 179
  4. Very specific:

    Only agent and mcp checks may be supplied.

    line 182
  5. Very specific:

    When the orchestrator is not Claude Code, dispatch the reviewers and the integrator through the CLI rather than the Agent path.

    line 194
  6. Very specific:

    Before the command checks run, the gate merges the phase's task branches into a scratch worktree under the system temp directory and runs …

    line 208
  7. Distinctive:

    Check that declaration before a run rather than at the first gate, when the fix is still a one-line manifest edit:

    line 218
  8. Distinctive:

    Ask the CLI for a fix decision before asking the user.

    line 249
  9. Distinctive:

    When both attempts fail, the check fails with the first attempt's exit code, with one exception: a run is excused only when every test that …

    line 431
  10. Distinctive:

    Evidence before claims, always: Never claim a task, phase, or fix is complete, fixed, or passing without having run the verification …

    line 467

“Common” is measured: kept by at least 6 in 10 of the 4 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Phase Gate

Phases run autonomously end to end. The boundary is where verification happens.

Run it

node "<fleetmates root>/scripts/cli.mjs" gate --run <runId> --root <project root> [--phase <name>]

Exit codes: 0 PASS, 1 FAIL, 2 the manifest is broken, 3 no manifest (an inferred one was printed), 5 a derive or run-state failure only (the only failed entries are derive and/or run-state, so nothing was judged; a check that failed beside them keeps exit 1).

On exit 3, show the user the inferred manifest, get confirmation, and save it as fleetmates.gate.json. Never invent checks silently.

Exit 2 is not a verdict — nothing was judged. fleetmates.gate.json is present and malformed, and the message names the file and what is wrong with it. Show that message and stop; do not re-run the gate, and never save an inferred manifest over the broken one, which would discard the checks the operator meant to fix. complete and fix read the same manifest and exit 2 the same way.

Finish the pending checks

The CLI runs command checks itself and returns agent and mcp checks as pending — those you execute:

  • agent — generate the dispatches rather than assembling them by hand:

    node "<fleetmates root>/scripts/cli.mjs" review-dispatch --run <runId> --root <project root> --phase <name> [--models <json>]

    It prints one reviewer per lens with the tier, effort, findings path, scratch worktree and prompt already resolved, and exits 4 rather than emitting a dispatch when the phase has no task branch to review. Every rule below is what it encodes; they are stated here because a dispatch assembled by hand still has to follow them.

    Dispatch one tm-reviewer per lens in parallel over the phase diff, without a name. A named reviewer becomes an addressable teammate that goes idle without emitting its result, and the review is lost; unnamed reviewers return normally. Six consecutive named dispatches were lost this way in run preview before the pattern was identified. Each dispatch carries the configured reviewer tier and effort, read with config get agents.reviewer.tier and config get agents.reviewer.effort. config get on an unset key exits 2 with unset: <key> — the same exit code every hard config failure uses, but here it is the normal case, not an error. The two keys fall back differently, and treating them alike is how a reviewer ends up judging below its guaranteed tier:

    • unset: agents.reviewer.tier — dispatch at the fixed reviewer tier, capable (model opus, per the tier→model map in parallel-execution — remap there and this line follows). Never omit the model to inherit the session's: in a session on a smaller model (haiku or sonnet) that would have the reviewer grading every agent check below the model this skill guarantees. The map now sends mid to opus as well, so mid and capable resolve to the same model; the reviewer is still dispatched at capable, the tier this skill pins. A configured tier replaces capable; nothing else does.
    • unset: agents.reviewer.effort — omit the effort option, and the dispatch inherits the session's effort. Only effort falls back this way.

    Name a findings path per lens in the dispatch — .fleetmates/<runId>/reviews/<phase>-<lens>.json — which the reviewer writes before it returns. The response stays the interface: read it first, and read that file before respawning a reviewer that idled without returning one. A file present with no response is a recovered review, not a fresh one; record it as recovered. Neither an absent response nor an absent file is a clean lens — a missing result is never an empty findings array, and the check stays pending, which the CLI scores as FAIL.

    Each findings file carries a stamp naming the phase, the lens and the branch tips it judged. collect-reviews refuses a file whose stamp names different tips — a fix round moves a branch, and findings about the old tree are not findings about this one.

    To rebuild the results file from those drops rather than by hand:

    node "<fleetmates root>/scripts/cli.mjs" collect-reviews --run <runId> --root <project root> --phase <name>

    --phase is not optional on a plan with more than one phase, on either command above: omitted, it names the manifest key default, which scopes the review to every task branch in the run — including ones integrated rounds ago. The CLI refuses that with exit 2 rather than reviewing it. The guard counts every distinct phase value, integer or not, so a plan.json mixing phase 1 with phase "2" is refused too. Only integer phases are listed in the refusal; a non-integer one is reported as a count, because --phase cannot select it — no --phase reviews that task's branch, and the fix is the plan, not the flag. Nothing in this repository writes a non-integer phase; a plan is agent-written, which is why the case is stated here.

    It prints a --results file with source: "file", applying the manifest's own blockOn. It also writes that same document to .fleetmates/<runId>/reviews/results-<phase>.json and prints that path last, so the gate --results <path> that follows names a file that exists — run without a redirect and without this, the review check stays pending forever while the gate reports FAIL with an empty failed list, naming nothing to fix. Pass the written path, not a capture of stdout: a > results.json redirect no longer round-trips, because the trailing path line is inside the captured bytes and gate --results on that capture exits 2 on --results must be a readable JSON file shaped { "results": [...] }, while the file the same command wrote exits 0 with verdict PASS — both measured on this tree. This is a breaking change to a workflow this skill used to invite, not a nicety. The file's existence is itself the claim: the previous results-<phase>.json is removed at the start of the command body, before the manifest is read and before any findings file is opened, so every refusal that judges this round's work is downstream of the clear and leaves no results file behind — measured, by making a second round refuse on a stale stamp and watching the first round's file go with it. Two classes of refusal sit above it and leave the earlier round's file where it was. An argv refusal — an unknown flag, a missing --run — exits 2 before this command body runs at all. And the clear itself can fail: a previous file that can be neither unlinked nor emptied exits 4 with could not clear the previous results file at …, and a reviews directory that is a symlink exits 4 with … must be a real directory before the clear is even attempted; either way the earlier round's file is still on disk saying what that round said. After any refusal, hand gate --results nothing at all: only a path printed on a results written to … line is this command's answer, and no refusal prints one — least of all the clear failure above, which names that very path while refusing to stand behind what is at it. The gate parses whatever file it is handed and trusts it, so a surviving "status": "pass" is read back as this round's verdict — measured on this tree for both shapes, one with the file and its directory owned by another uid under ordinary modes and one with the directory symlinked, each leaving a byte-identical file the gate then returned verdict PASS on. Scoping to the success line is what closes a bypass, and the bypass was measured too: a round-2 collection refused at the clear over findings carrying a blocking high, and gate --results on the path that refusal itself printed exited 0 with verdict PASS over round 1's document. A read-only directory ALONE is not one of these: there unlink fails and the in-place empty succeeds, and the gate refuses the zero-byte file it leaves. It exits 4 and prints nothing usable while any lens has no file, or when a file exists and does not parse — respawn those lenses instead. A third condition exits 4 and does print something usable: when the collection succeeded and only the write failed, the complete results go to stdout above cannot write the results file at …, so keep those bytes rather than re-running the reviews — but drop that last line before passing them on, because a plain redirect captures it and gate --results then exits 2 for exactly the reason above; head -n -1 is enough, and with it the gate exits 0 with verdict PASS, measured on this tree. A file for a lens this phase did not dispatch is reported and ignored, never merged.

    Where a line quotes a value an agent wrote — a lens, a stamp or a reason out of a findings file; a check name, kind or preview.link entry out of the gate manifest, whose contents this CLI validates for shape and not for content; the bytes a JSON parse error quotes back out of a --results file or the verdict file fix is handed below — the value is printed with its control bytes and line separators replaced by a visible <0x1B>-style token. Any of those files could otherwise carry an escape sequence that erases the line in the terminal and draws one reading like a PASS this CLI computed, or a line break that adds one. That covers the lines a refusal prints, the ones a passing command prints, and the check names inside finish's run summary table. It does not cover the bidi and format controls, which pass through. Read the exit code, not the shape of a line.

    Both come from the tracked manifest only — the reviewer grades the diff, so letting the gitignored layer choose its tier would let the party being judged pick its own judge. Then take every finding at a blockOn severity and dispatch a second reviewer prompted to refute it. Only findings that survive refutation block the gate.

    A claims finding is a claim whose mutation left the suite green — the code does not deliver what the comment, skill or spec says it does. At high that blocks the phase, and the fix is either to make the code deliver the claim or to correct the claim. Weakening the test is not a fix.

    A claims reviewer also writes two keys that are not findings, and collect-reviews acts on both: collectReviewResults reads lens, stamp, findings, unableToVerify and unprobed, and ignores every other key. It does not keep all of them — stamp is consumed to reject a stale file and then dropped from the emitted result, and neither key below survives into the result as a key either; what survives of unprobed is a count in the check's output.

    • unableToVerify means the reviewer could not build the phase's tree or get a green baseline in its scratch worktree, so it probed nothing. A non-empty unableToVerify is refused exactly like a lens with no file at all: nothing is emitted, collect-reviews names the lens and its reason and exits 4. Respawn that lens; do not record a pass for it. An empty string is not a report of failure and collects normally. The key is read only as a reason string: written as an array, an object, a boolean or a number it is reported as a file that cannot be read, and the fix is to the file rather than a respawn.
    • unprobed lists claims it enumerated and did not reach. The lens is bounded by its mutation cap, and a review that reached 8 of 40 claims would otherwise collect identically to an exhaustive clean one, so the count is surfaced in the emitted check's output — for a passing and a failing verdict alike. The list itself is only in the findings file. Like the key above it is read only in its documented shape, a list: written as a number, a string, an object or a boolean it is reported as a file that cannot be read rather than counted as nothing, because a review that says it reached a fifth of its claims must never emit a result with no bounded note. An empty list is a review that reached everything it enumerated, and collects silently.
  • mcp — call the declared tool and compare against passWhen. If the server is not connected and the check is optional, record skip and say so out loud. A missing optional server is never a silent pass.

Then hand those results back and let the CLI recompute:

node "<fleetmates root>/scripts/cli.mjs" gate --run <runId> --plan <planPath> --root <project root> --results <path>

The file is { "results": [ { "name": "review", "kind": "agent", "status": "pass", "findings": [] } ] }. Add "source": "file" to an entry recovered from a reviewer's findings file rather than from its returned response; unset means response. It is provenance only — the verdict is identical either way — and anything but those two values is rejected. Only agent and mcp checks may be supplied; a command, fileset, or ownership entry is rejected, because those are computed and supplying one would be a way to hand the gate a passing enforcement check. The verdict is recomputed from the merged set, so a recorded PASS is always CLI-computed — never hand-written into status.json.

finish recomputes every phase, and every agent check comes back pending because nothing runs one. Hand it the same results, keyed by phase: { "phases": { "1": { "results": [...] } } }. A phase that passed on supplied results is marked (review supplied) in its output, so a reader can tell a recomputed pass from a reported one.

On a harness other than Claude Code

When the orchestrator is not Claude Code, dispatch the reviewers and the integrator through the CLI rather than the Agent path. The reviewer dispatch is:

node "<fleetmates root>/scripts/cli.mjs" dispatch-reviews --run <id> --phase <name> --harness <h> --root <project root>

and the integrator step is:

node "<fleetmates root>/scripts/cli.mjs" dispatch-integrator --run <id> --phase <name> --harness <h> --root <project root>

Each is worktree-isolated and gate-identical, and the harness name is the one the orchestrator is running in, passed explicitly.

What the merge check does

Before the command checks run, the gate merges the phase's task branches into a scratch worktree under the system temp directory and runs those checks there. So test measures what integration will actually produce, not the run branch as it stands.

The preview contains tracked content only — git worktree add does not materialize node_modules, virtualenvs, or generated artifacts. A project whose test runner is itself a dependency declares what to link:

{ "preview": { "link": ["node_modules"] } }

Check that declaration before a run rather than at the first gate, when the fix is still a one-line manifest edit:

node "<fleetmates root>/scripts/cli.mjs" preview-check --root <project root>

It applies the same rules the merge check applies and exits 1 naming any entry that is missing, not a directory, escaping, repeated, or tracked.

The gate symlinks each declared directory into the preview after the merge and removes the links before the worktree. Entries must be repo-relative and inside the repository; an absolute path, a .. escape, a missing target, or a path the repository already tracks fails the merge check naming the entry and the reason. A link that cannot be made is reported as a merge failure rather than left to surface as a command-check failure, because a preview missing its build inputs produces errors that look like code defects and are not.

Links are shared, not copies. A check that writes into node_modules writes to the real one, because it is the same directory. That is the cost of linking; copying a real dependency tree is minutes per gate run.

--no-fleet builds no preview, so nothing is linked and preview.link is not consulted.

A conflict fails the merge check and names both branches and the conflicting paths. The command checks are then recorded skip — never pass. Treat a conflict like a process violation: escalate it, do not retry. No single teammate can fix a conflict between two file sets, so redispatching one owner cannot resolve it.

The preview is a preview. tm-integrator still performs the real merge with --no-ff, and a passing preview is not permission to skip it.

On FAIL

Ask the CLI for a fix decision before asking the user. Hand it the run, the failing phase, the run root, and the verdict JSON you just produced; it prints one of three decisions — none, retry, or escalate — and exits 0 for all three, so read the decision field rather than the exit status.

node "<fleetmates root>/scripts/cli.mjs" fix --run <runId> --phase <n> --verdict <path> --root <project root>

--verdict names a file holding that same JSON, and --phase must match its own phase field — a mismatch exits 2 rather than adjudicating the wrong phase's findings, and so does a malformed fleetmates.gate.json. Exit 1 means the run has no plan at all or the verdict file could not be read: an argument error, not a decision. Exit 0 covers none, retry, and escalate alike, so the exit status never tells them apart — only the decision field does.

fix does not apply to a --no-fleet gate, and this whole section does not either. --phase here must be an INTEGER, because tasks and fix rounds are keyed by numeric phase. A --no-fleet gate names its phase from the manifest instead — default, typically — and emits a verdict carrying phaseName with no integer phase, so handing it to fix exits 2 (a third cause, alongside the phase mismatch and the malformed manifest). That is not a defect to work around: --no-fleet builds no preview and has no task branches, so there is no teammate to redispatch and nothing for a decision engine to attribute a finding to. Fix the findings directly, then re-run the gate from scratch for a fresh verdict. Everything below about retry, escalate and the round budget presumes a fleet phase with task branches. Two rules still bind on a solo gate, and both are restated here in full rather than cross-referenced: a PASS reached after N rounds of fixes is reported as such, never as a clean first-pass PASS, and a check that was skipped is reported as skipped, every time. A solo gate writes a verdict into status.json only when the caller passes --run, and a solo gate invoked without --run writes no verdict anywhere.

The verdict you hand it must be the JSON this gate printed in this same pass, and must never be read back from .fleetmates/. The only verdict persisted on disk lives in status.gates[<phase>] inside .fleetmates/<run>/status.json, written by the very agents this gate exists to enforce — the same file scripts/enforce.mjs refuses to consult when picking a branch. Feeding that record in today degenerates harmlessly, because the persisted object carries no results key and the decision comes back none; that is incidental, not guaranteed. Treat it as a rule so a change that starts persisting results cannot quietly turn the on-disk record into a decision input.

On none, the decision engine found no failing check in the verdict you handed it. This does not mean "the failure needs no fix" and it is never permission to integrate. You reached this section because the gate failed, so a none decision means the verdict you passed is not the one that failed — a stale file, the wrong phase, the wrong run root, or a verdict written before the last check completed. Re-derive the verdict by running the gate again from scratch and ask again. Integrate only on a freshly recomputed PASS, never on none.

On retry, redispatch each listed task at the listed tier, resuming the same teammate so it keeps its task context, and hand it the failing check names and the surviving findings. Then run the gate again from scratch — never reuse the previous verdict, and never re-run only the failing check.

A retried teammate that returns blocked ends the loop immediately. blocked means missing input, and further rounds cannot supply it.

On escalate, halt before integration and report, in this order: the escalation reason, which check failed, the exact command output or finding list, the offending diff hunks, the owning teammate, and the round history — which tier ran each round and what failed each time. Then offer three choices: retry the failing task with the findings fed back, override and proceed, or abort the phase.

A PASS reached after N fix rounds is reported as such, never as a clean first-pass PASS.

The round budget is a cost bound, not a security bound. fixRounds lives in status.json, which is written by the agents the gate enforces. A teammate that rewrites its own count buys itself more retries — wasted tokens, not a false PASS, because the verdict is recomputed from git every round. Do not describe the loop as tamper-evident; only fileset and ownership carry that property.

What the enforcement checks do and do not cover

fileset and ownership are computed by the gate from git, at the moment it runs. They read no file from .fleetmates/: the run phase, the anchor, and the plan all come from git, because status.json is written by the agents being enforced. complete recomputes rather than trusting a recorded verdict.

These checks are tamper-evident, not tamper-proof. They catch drift, mistakes, and attacks that are not aimed at them. They do not stop a teammate that targets them: a teammate runs its own tests, and running a teammate's code is arbitrary execution, so a determined teammate can do anything the user can — including fast-forwarding the run branch to make a phase look integrated. docs/specs/2026-08-05-tamper-evident-enforcement-design.md lists what is out of scope; tests/adversarial.test.mjs pins each limit with a test.

ownership also fails a task branch that is an ancestor of the base branch but not of the run branch. This run's work reaches the base by riding the run branch, which lands as a whole after a PASS; any other route puts integrated content in front of users with no gate verdict behind it — a reviewer merging task branches into master, a teammate landing its own work. The question is asked of the two refs as they stand, so there is no recorded base sha for the enforced party to rewrite; the cost is that it says the branch is there, not when it arrived.

Both run on every phase whether or not the manifest lists them: the gate injects whichever one the phase's check list lacks, and gate names them in an injected field of its JSON (complete, finish and prune-run print a line). A merge can rewrite the manifest the next gate reads, so a check the manifest could remove is a check that merge could switch off. An injected ownership that fails adds a note: the commits it names may predate this fleetmates version, or come from an inline run that should have passed --no-fleet.

ownership explains a commit on the run branch's own first-parent chain — every integration merge, every direct write — only by the merge rule, never by its being reachable from a task branch: the next phase's branches fork from the run tip and carry every earlier write in their history. And an integration merge may carry only files declared by the tasks it integrates — a task integrates a secondary parent when the parent is on its branch's own first-parent chain, which a later phase's branch, forked from the run tip, never has — plus what only a base parent changed. So a payload merged in from a side branch, or a landed task branch that grew a commit outside its set and was merged again, fails, and dispatching the next phase does not change that.

Protected paths. fileset also fails a task that changes a protected path its plan line does not mark (protected), even when the path is declared: the output line reads T1: protected — <path> (…), separate from the outside declared set line. The manifest is always protected, under both its current and legacy names; its top-level protected array adds paths. ownership fails a merge whose hand-resolved conflict touches a protected path unless a task it integrates marks the path; a later task's marking does not count. The one exception is the base: a merge of the base branch may resolve a conflict on a protected path that only the base side changed. Approving such an escalation is a plan amendment on the base branch (see parallel-execution, "Amending a plan mid-run"), then a re-gate.

Test inventory. A command check whose manifest entry carries a report gets a computed result <name>:inventory right after it. The gate runs the suite twice — on the run tip before this phase's merges (the baseline, in a worktree of its own) and in the preview — and compares the JUnit reports test by test:

  • drop: <id> — ran at the baseline, absent|skipped now (changed by T2) — a test that ran is gone or skipped. Approved by marking the file - Test (drops) in the plan on the base branch.
  • new skip: <id> — a test that did not exist is skipped where the gate runs. Approved by declaring its unit in the manifest's skips with a reason; the manifest is protected, so that is an operator decision.
  • standing skip: and stale skips entry: lines ride on a PASS and never fail it; finish names the standing skips of the last phase at the end of the run.

An absent or unparseable report is a FAIL. complete reports the inventory as skip (no baseline); so does a solo gate. An inventory FAIL is a process violation and escalates, like fileset and ownership. What it cannot see: a loosened assertion (the test still runs), and a test that writes its own report — the report comes from a process running teammate code.

Skipped only when the caller passes --no-fleet. Missing state is a failure, never a skip.

A fileset or ownership failure is a process violation, not a code defect. Do not widen the plan's file set to make it pass.

Platform and timing failures

Two flags on a command check in fleetmates.gate.json aim at failures that otherwise show up only on the remote CI: a check that breaks on another operating system, and a test that fails once and passes on a rerun. Both are true or false; any other value fails the check with <flag> must be true or false, and is never read as a default.

"platformSim": true runs the check under the platform simulator instead of running it plainly, so the gate does no extra run. The simulator points TMPDIR, TMP and TEMP at a directory reached through a symlink whose path holds a space, points GIT_CONFIG_SYSTEM at a generated file that sets core.autocrlf=true and includes the user's own system git config (the level Git for Windows sets it at on a Windows runner; the global config is left alone), and on POSIX runs the command under umask 077. The check's output carries one platform-sim: applied ... line naming those conditions and any it skipped, and the scratch directory is removed afterwards, on a failure too. On Windows the symlink and the umask are skipped and named as skipped; this is unit-tested with an injected platform, not on a Windows host. The same simulator runs by hand:

node "<fleetmates root>/scripts/cli.mjs" platform-sim --root <project root> -- <command...>

It exits with the command's own exit code. A failure that appears only on a plain Linux run, such as one that needs the real temp directory, still reaches the remote CI.

The simulated core.autocrlf reaches git only through GIT_CONFIG_SYSTEM. Code that drops every GIT_* variable before calling git, as a hardened git wrapper does, reads the user's real system config instead, so files checked out under the simulator read to it as modified. On a real machine both read the same system config and agree. A test of such code pins core.autocrlf in its fixture repository's own config, which every git reads.

"flaky": true runs the check through the flaky detector. On a non-zero exit the check is retried exactly once, and a pass on that retry passes the check while keeping a flaky: <id> line in its output, so a flake is green but never silent. The check's timeout bounds both attempts together, and an attempt that timed out is not retried.

In the gate, the retry reruns the whole command. The check runs through a shell (/bin/sh -c <run>, or cmd.exe /d /s /c "<run>" on Windows), so the detector cannot read the test runner off its arguments and never filters the retry to the failed tests; the Windows form is unit-tested only. The detector names failed tests only from a report that is a single JUnit file, as <file>::<classname>::<name> (or the part of that the report carries). Otherwise it names the whole command, as /bin/sh -c <run> on POSIX, and a whole command is never quarantined as a named test. In the gate that covers both report forms in the main worktree: a "dir": true report is a directory, which the detector does not read names from, and an in-tree "path" report is refused outside a worktree the gate owns. Since the gate writes the quarantine file only from the main worktree, it never records a named test there; named tests reach the file through the flaky command with a single-file --report. Inside a merge preview an in-tree "path" report does name the failed tests, so a test already in the file can still be excused there.

When both attempts fail, the check fails with the first attempt's exit code, with one exception: a run is excused only when every test that failed on the first attempt either passed on the retry or is already quarantined, and every test still failing is quarantined. Only named tests from a report are ever quarantined, so a whole command that fails twice is red every time, and so is a retry that exits non-zero while its report names no failed test. An excused check passes and prints flaky: quarantined test failed: <id> and a line saying which exit code was overridden and why. That line is there because of a known limit: a failure outside any test (a crash, a coverage threshold) in the same run as a quarantined test failing twice cannot be told apart with a single retry.

Both flags together run each attempt, the retry included, under the simulator.

The quarantine file is fleetmates.flaky.json at the project root. Each entry records when the test was first and last seen flaky, how many times, and its current pass streak. A test that passes on its retry is added or updated there; a quarantined test that passes adds one to its streak, and one that fails resets it. A malformed file fails the check, and the same file refuses the flaky command with exit 2. The gate writes the file only when the check runs in the main worktree outside a merge preview; the flaky command writes it only from the main worktree's top level or when GITHUB_ACTIONS=true. A teammate's worktree never writes it.

The file is protected like the gate manifest: it is always in the protected set, so a task that changes it fails fileset unless its plan line marks the path (protected). A teammate therefore cannot quarantine its own failing test. After 20 passes in a row, doctor suggests removing a test from the file; nothing removes it automatically.

The portability review lens is added to a phase's review without any manifest change when a workflow under .github/workflows runs on macOS or Windows, through runs-on or a matrix os list. review-dispatch emits a reviewer for it and collect-reviews expects its findings: with no findings file for it, collect-reviews exits 4 naming portability among the missing lenses. Listing portability in the manifest adds it on any project. An explicit empty lens list stays empty.

Reporting rule

Never report a phase done without a recorded PASS in status.json. A check that was skipped is reported as skipped, every time. No "should work".

Evidence before claims, always. Never claim a task, phase, or fix is complete, fixed, or passing without having run the verification yourself in this pass and seen its output — a prior run, an agent's self-report, or "should pass now" is not evidence. "Tests pass" requires the test command's fresh output showing the count and zero failures, not recollection of an earlier green run. "Bug fixed" requires re-running the original failing case and watching it pass now, not the diff alone. If a tm-implementer reports DONE, that report is not the evidence — the gate's own check run against its diff is. Skipping this because a check "obviously" passes is the same defect as skipping the check.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.