tade — a check nobody ran is not one that passed

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.

The Tade project·5 min read·checksci

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.

what-an-agent-has-done · maindrawn by Tade
The ACTIONS tab: this agent's own commits, what is not committed, and each check with the command it ran, how long it took and what it counted. The rollup names the commit it is red at rather than leaving that to be guessed.

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.

a-shell-beside-the-agent · panedrawn by Tade
A shell in the agent's own worktree, where 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.

blog← olderThe Tade project│2026-09-28│5 min│top