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
- acprk/crypto-research-agentstheir own version, first seen Nov 4, 2026
- andreymudri/claude-teammatesword for word, first seen Nov 2, 2026
- Ahsan-ire/rag-pipeline-v1their own version, first seen Oct 12, 2026
- acgs-ai/ACGStheir own version, first seen Aug 6, 2026
- huguryildiz/ALETHEIAtheir own version, first seen Jul 6, 2026
Other versions5 projects use a version of this.
Word for word · 1
- andreymudri/claude-teammates★ 3100% same
Edited · 0
None found.
Wrote their own version · 4
- acprk/crypto-research-agents★ 15same name
- acgs-ai/ACGS★ 2same name
- Ahsan-ire/rag-pipeline-v1★ 0same name
- huguryildiz/ALETHEIA★ 0same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository15 other files it gives its agents.
- AGENTS.mdAGENTS.md
- brainstormingSkill
- executing-plansSkill
- finishing-a-development-branchSkill
- fleet-lifecycleSkill
- fleetmates-configSkill
- fleet-supervisionSkill
- grillingSkill
- parallel-executionSkill
- receiving-code-reviewSkill
- systematic-debuggingSkill
- test-driven-developmentSkill
- using-fleetmatesSkill
- writing-plansSkill
- writing-skillsSkill
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
- Very specific: line 52
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 …
- Very specific: line 106
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 …
- Very specific: line 179
Add "source": "file" to an entry recovered from a reviewer's findings file rather than from its returned response.
- Very specific: line 182
Only agent and mcp checks may be supplied.
- Very specific: line 194
When the orchestrator is not Claude Code, dispatch the reviewers and the integrator through the CLI rather than the Agent path.
- Very specific: line 208
Before the command checks run, the gate merges the phase's task branches into a scratch worktree under the system temp directory and runs …
- Distinctive: line 218
Check that declaration before a run rather than at the first gate, when the fix is still a one-line manifest edit:
- Distinctive: line 249
Ask the CLI for a fix decision before asking the user.
- Distinctive: line 431
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 …
- Distinctive: line 467
Evidence before claims, always: Never claim a task, phase, or fix is complete, fixed, or passing without having run the verification …
“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.
Files
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-reviewerper lens in parallel over the phase diff, without aname. 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 runpreviewbefore the pattern was identified. Each dispatch carries the configured reviewer tier and effort, read withconfig get agents.reviewer.tierandconfig get agents.reviewer.effort.config geton an unset key exits 2 withunset: <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(modelopus, per the tier→model map inparallel-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 everyagentcheck below the model this skill guarantees. The map now sendsmidto opus as well, somidandcapableresolve to the same model; the reviewer is still dispatched atcapable, the tier this skill pins. A configured tier replacescapable; nothing else does.unset: agents.reviewer.effort— omit theeffortoption, 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 stayspending, which the CLI scores as FAIL.Each findings file carries a
stampnaming the phase, the lens and the branch tips it judged.collect-reviewsrefuses 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>--phaseis not optional on a plan with more than one phase, on either command above: omitted, it names the manifest keydefault, 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 aplan.jsonmixing phase1with phase"2"is refused too. Only integer phases are listed in the refusal; a non-integer one is reported as a count, because--phasecannot select it — no--phasereviews 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
--resultsfile withsource: "file", applying the manifest's ownblockOn. It also writes that same document to.fleetmates/<runId>/reviews/results-<phase>.jsonand prints that path last, so thegate --results <path>that follows names a file that exists — run without a redirect and without this, the review check stayspendingforever while the gate reports FAIL with an emptyfailedlist, naming nothing to fix. Pass the written path, not a capture of stdout: a> results.jsonredirect no longer round-trips, because the trailing path line is inside the captured bytes andgate --resultson 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 previousresults-<phase>.jsonis 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 withcould not clear the previous results file at …, and areviewsdirectory that is a symlink exits 4 with… must be a real directorybefore the clear is even attempted; either way the earlier round's file is still on disk saying what that round said. After any refusal, handgate --resultsnothing at all: only a path printed on aresults 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, andgate --resultson 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: thereunlinkfails 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 abovecannot 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 andgate --resultsthen exits 2 for exactly the reason above;head -n -1is 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.linkentry 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--resultsfile or the verdict filefixis 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 insidefinish'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
blockOnseverity and dispatch a second reviewer prompted to refute it. Only findings that survive refutation block the gate.A
claimsfinding is a claim whose mutation left the suite green — the code does not deliver what the comment, skill or spec says it does. Athighthat 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
claimsreviewer also writes two keys that are not findings, andcollect-reviewsacts on both:collectReviewResultsreadslens,stamp,findings,unableToVerifyandunprobed, and ignores every other key. It does not keep all of them —stampis 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 ofunprobedis a count in the check'soutput.unableToVerifymeans the reviewer could not build the phase's tree or get a green baseline in its scratch worktree, so it probed nothing. A non-emptyunableToVerifyis refused exactly like a lens with no file at all: nothing is emitted,collect-reviewsnames 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.unprobedlists 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'soutput— 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 isoptional, recordskipand 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'sskipswith a reason; the manifest is protected, so that is an operator decision.standing skip:andstale skips entry:lines ride on a PASS and never fail it;finishnames 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.
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
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.

