# 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.

Published 2026-09-28 · by The Tade project · tagged checks, ci.
Published at https://tade.sh/blog/a-check-nobody-ran-is-not-a-check-that-passed/ — part of https://tade.sh/blog/.

---

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:

```json
{
  "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:

```console
$ 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.

> **A screen from Tade — `what-an-agent-has-done`.** The ACTIONS tab: the commits this agent made, what is not committed, the review it is out for, and each of the project’s own checks with what it ran, how long it took and what it counted — with the tests red.
>
> 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 screen from Tade — `a-shell-beside-the-agent`.** A shell open beside the agent, in the same worktree, with a divider you can drag.
>
> 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.

```sh
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](https://github.com/mujacica/tade).
