blog /
A check nobody ran is not one that passed
An agent runs the checks and then commits, so HEAD moves under the run. Carrying it forward honestly means comparing trees rather than commit ids.
Twenty agents in one repository will produce, between them, a great deal of green. The question that decides whether any of it is worth anything is narrower than “did the tests pass”: which commit did they pass on, and is that the commit in front of me.
Tade records 243 check runs in the two repositories behind this site. Every one of them names the commit it ran against, the command it ran, who started it, and what it counted. None of them is allowed to be a blank that reads as green.
The ledger
One line per run, in .tade/checks.jsonl inside the project. Trimmed of its
output tail, a real one:
{
"id": "ea04575:pre-commit:here:1",
"check": "pre-commit",
"commit": "ea045755e326bf4e4dd0655b8d6ad92a236d46cb",
"state": "passed",
"where": { "kind": "here", "runner": "local", "host": "MK7PQ3XVFW.local" },
"required": true,
"startedAt": "2026-09-25T19:18:59.066Z",
"finishedAt": "2026-09-25T19:19:06.562Z",
"code": 0,
"by": "tade/quota-across-logins"
}
by is the part that only matters with several agents about: the run above was
started by one task, and the commit it ran against may belong to another. Both
facts are kept, and neither is inferred from the other.
What that adds up to over the seven days to 28 September, as Tade reports it:
$ tade spend --days 7
checks
tests 131 runs 52 failed 1m typically
types 112 runs 8 failed 3.6s typically
format 110 runs 15 failed 0.7s typically
pre-commit 25 runs 7 failed 7.5s typically
build 10 runs 0 failed 1.3s typically
unit-tests 10 runs 0 failed 0.4s typically
browser-checks-against-the-built-site 8 runs 0 failed 7.7s typically
Fifty-two red test runs in a week is not a bad week. It is what running the suite before every commit looks like when it is written down.
Why the obvious thing does not work
A run is recorded against the commit that was checked out when it started. And an agent’s ordinary minute is: run the checks, then commit.
So HEAD moves. The commit the agent just made has no run against it and reads
not run — even though the bytes the commands read are byte-for-byte what was
committed. Do nothing about that and the gate is useless, because it is red on
every commit anybody ever makes. Fudge it and the gate is worse than useless,
because it will one day call a commit green that nothing was ever run over.
A run is about a tree, not a commit id
That is the sentence the whole thing turns on. The same commands over the same bytes give the same answer, so a run covers a commit exactly when the commit’s content is the content the run read.
So each run records what it read: the tree of the commit it ran at, every tracked path whose bytes on disk differed from that tree, and the untracked files that were also lying about. A later commit carries the run only when applying that record to the run’s tree yields the commit’s tree — every path the run read differently is in the commit with the bytes the run read, and the commit changes nothing else.
That is exact about the case this project actually lives in. Agents share one
checkout, so the bytes a run reads routinely include another agent’s
uncommitted work. Committing your own files leaves that agent’s changes in the
run’s record and out of the commit, the two trees differ, and the answer stays
unknown — as it must, because that commit’s tree is a tree nobody has run
anything over.
The same falls out for a partial commit of your own files, and for anything edited between the run and the commit. The bytes committed are not the bytes read, so nothing carries.
git diff --stat says what the
bytes on disk are. Checks are run through Tade rather than here, so each run is
recorded against the tree it actually read.Untracked files are the one thing that cannot be in the tree comparison, because no commit’s tree holds them. They are recorded so that the commonest carry of all — a new file written, checked, and committed — can be matched against the bytes the run read at that path.
Four numbers bound the cost of asking: at most 64 untracked files read into a
record, none bigger than 4MB, nothing recorded at all beyond 200 tracked
changes, and at most four git diff-tree calls per question however many runs
are on file. Past any of those the answer is unknown rather than expensive.
What a project checks is what the project already says
There is no .tade/checks.yaml and nothing writes one. Tade reads the
project’s own commit hook and its workflows, and a step’s name: in
.github/workflows/ci.yml is the check id. In tade those steps are
format, types and tests; on this site they are written Unit tests,
Build and Browser checks against the built site, which is where
unit-tests, build and browser-checks-against-the-built-site come from.
Which is why “reproduce it locally” is a meaningful instruction rather than a
hope. What CI runs and what checks_run tests runs on somebody’s laptop are
the same commands by construction, not because a test holds two files in step.
Reading may only ever narrow what Tade claims. A step that only CI can run
— anything with ${{ }} in it, anything needing a secret or a service, the
install steps that prepare the machine rather than check the code — is named
rather than run. A check that cannot run here keeps its row with a skip and
is required: false, because a rollup is what a run here adds up to. And where
a project says nothing at all, Tade invents no gate: unknown stands, and the
agent has to work out what checking means, run it, and say what it ran.
The failure mode is therefore always the same direction: Tade checked less than CI does, and said so.
The gate, and exactly how much it can do
An override is a written act rather than a setting. checks_override refuses
without a reason — an override nobody hears about is a broken gate — lasts
four hours at the most, and comes in three scopes. An agent may overrule the
rule for its own task and nobody else’s: another task’s work, a whole project,
or a night of them is a person’s decision.
Every override is read back out of the tool_call line that recorded the act,
so closing the window loses none and a crash mid-push loses none either.
And here is the limit, in the gate’s own words rather than the marketing’s. A
push an agent makes is seen by the supervisor only under approvals.mode: 'policy'; under the default nothing can be held, and a person typing git push in a terminal is nobody’s to hold. So the gate never runs anything
itself. It reads what is recorded for the commit in hand — which is instant —
and refuses a push with no green run behind it, naming what is missing and the
one call that fixes it.
Holding a tool call for the three minutes a suite takes is the thing that would break the agent, not the thing that would protect the branch.
What it is worth
tests is the slow check in tade and it ran 131 times in the week above. The
tree comparison is what lets a run like that count for the commit made straight
after it instead of being thrown away — and it is the same comparison that
refuses to let it count for a commit it did not read.
Both halves matter, and only together. Without the first the gate is red on every commit anybody makes and gets turned off within a day. Without the second it is a green tick whose provenance somebody has to remember, and nobody remembers the provenance of a green tick.
npm i -g tade-sh
The tree rule is packages/checks/core/src/coverage.ts, the gate is
packages/workbench/src/checks.ts, and the overrides are
packages/core/src/checks.ts, all in
the repository.