e2e-red-triage · git:20261001.8e39faf · 2026-10-01 · sha256 d877a2c2a1373c43
e2e-red-triage git:20261001.8e39fafA
Immutable. This exact content never changes and is served at /api/v1/blob/d877a2c2a1373c43.
---
name: e2e-red-triage
description: "Classify one unexpected end-to-end test failure as a product defect or an environment fault needing a new preflight check. MANUAL-ONLY: load when the suite runner names a specific failure. NEVER auto-load from context."
requires:
programs: []
harness:
- {slot: 4.shell, need: required}
- {slot: 4.file-read-write, need: required}
---
# e2e-red-triage — for the reds the preflight could not have predicted
Terms are as in `e2e-environment-preflight`: a **row** is one acceptance check in the test plan, a **red** is a row that failed or gave no answer, a **journey** is one end-to-end scenario made of rows, the **suite** is all the rows of a journey, and **preflight** is the set of environment checks that must be green before the first row runs.
**Never auto-triggered, and normally never needed.** One caller — the agent running the suite —
and one moment:
> The `e2e-environment-preflight` checks were **all green**, and a row went **red anyway** — or produced a
> null, an empty, a zero, or a silence nobody can yet explain.
If preflight was not green, the suite should not have started. If a red is an ordinary product
failure with a clear cause, fix it and move on; this is not the ceremony for an ordinary bug.
This is the residual after the gate has done its work — a small fraction, and one
that shrinks every time move 3 is honoured.
## Why this needs its own protocol
The preflight handles the failures somebody could enumerate in advance. What is left is the class
where **the failure produced nothing rather than an error, and the nothing appeared far from its
cause.** One null can mean five different things across five runs of the same suite; "no results" can mean the query matched nothing or the search never ran.
The hours such a red costs are spent **attributing an absence** — arguing about what a missing
thing meant, from evidence that never contained the answer. Guessing feels faster than
instrumenting and is not: a wrong guess costs a full suite run to disprove, and a *plausible*
wrong guess is worse than an implausible one because it stops the search.
The deeper version, and the one worth carrying: **the failure is usually a two-part outcome
collapsed into one verdict.** An HTTP 202 "accepted" means the request was queued, not that the work was done, and both caller and monitor read it as success until someone looks. A cleanly stopped service records
an exit code that looks like a crash. In each case the evidence was there, in plain
words, and nobody split it.
**So the first question is never "why did it fail" — it is "what are the two halves of this
result, and which half am I looking at?"**
## The three moves, in order
These are defaults; a project may choose otherwise and record it in its local binding.
### 1. Do not guess. Enter diagnosis.
Stop proposing causes. Write down first the two things this result could be a collapse of:
succeeded versus reached, stopped versus crashed, empty versus unreadable, absent versus
never-asked, zero-matches versus source-missing.
### 2. The first change makes the absence name itself. Not the product.
**The first commit after this kind of red does not touch product behaviour.** It changes whatever
produced the empty so that it says *why* it is empty — in the artifact, as a field, not in a log
line, which is read only by someone who already suspects the answer.
result: null
→ result: {value: null, because: "<the reason, as a code>"}
Then re-run. Only now diagnose. This inverts the usual instinct and that is the point:
instrumenting first looks slower for one iteration and is faster from the second onward, because
every future occurrence of this absence becomes a single read instead of an investigation.
Two shapes to watch for here, because both report health while broken:
- **A check that passes when its input source is missing.** A count over a path that does not
exist returns zero — the same zero a real path with no matches returns. Before any "it is
absent" claim, prove the place you looked at is real, then look.
- **A condition that can no longer fire.** An alarm testing an absolute count rather than a delta
switches itself off permanently the first time the count moves.
Feed every new check a known-wrong input and watch it go red. Instruments fail toward "healthy".
### 3. The exit condition: come back with a durable check.
**A diagnosis is not the end. The finding becomes a new preflight check for that system and a line in that system's primer (see `e2e-environment-preflight`), or this red is not closed.**
Invoke `e2e-environment-preflight` to write it, and record the red in the red scoreboard described in `e2e-environment-preflight`, with its bucket and the new check's id:
{"row":"<row-id>","bucket":"environment","note":"…","owed_check":"<check-id>"}
This is the only mechanism that makes the residual shrink. Without it the same class of absence
is rediscovered every campaign and the gate never gets stronger — the process runs, everyone is
diligent, and the hours are spent again. **Hitting the wall is allowed; coming back with nothing
durable is not.**
## Classify before you close
Every red gets exactly one bucket — `source`, `environment`, `stuck`, `harness`. Be honest about
`stuck`: a run that produced no verdict at all is not a pass and not a skip, and burying it is how
a campaign reports coverage it never had.
The counts are the point. A journey whose reds are mostly `source` found bugs. One whose reds are
mostly `environment` or `stuck` measured its own setup, and the fix for that is upstream in the preflight — never another round here.