Gate
The Gate stage (stage 7): binds a PR's existing E2E results (Playwright and/or Cypress) plus an audit-test verdict into one evidence bundle, and derives an advisory ship/canary/hold decision by worst-wins. Ship requires the E2E run to clear an executed-floor and the audit-test verdict to clear an examined-floor (see docs/gate.md for the exact thresholds and what each floor does and doesn't catch) — Gate never re-runs anything, carries no confidence number, and is optionally DSSE-signed with a self-signed key (not Sigstore). Optionally join an external trace-matrix (--trace-json) against the audit-test verdict for a business-risk-coverage read (mutation-proven / unverified / hollow / not-covered per requirement) — purely informational, never a decision input. Use at the end of a PR for one honest release recommendation instead of eyeballing separate reports.From its SKILL.md
npx -y skills add TzolkinB/skills --skill gateAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
43.7 KB, as published. Nobody here has run it
Owns: the release Gate — aggregating a PR's existing test Evidence into one bundle and emitting an
advisory ship/canary/hold decision. the Gate owns the ship verdict; /qa-pass-the-orchestrator does not
speak shippability (#99).
Not this: running a suite or a browser → out of scope, the Gate ingests existing evidence only
(ADR-0010); proving a green test isn't
hollow → /audit-test (the Gate consumes its report); which specs a diff hits → /e2e-impact; diagnosing a
red spec → /debug-test.
the Gate reads what a PR already produced — an E2E result (a Playwright JSON report and/or a Cypress
CypressRunResult), and (if you ran it) an audit-test report — binds them into one readable evidence
bundle (in-toto-shaped Statement entries — ADR-0032
— one structured JSON record per stage, over content-addressed subjects: the PR head commit plus a sha256
digest of every ingested input file (#139,
ADR-0037 §2) — a DSSE envelope over that in-toto-shaped
Statement when a signing key is supplied (#141, ADR-0037 §1;
no standard in-toto tooling consumes this bundle as-is); unsigned by default, in which case a bundle stays
exactly what it always was — in-toto-shaped, not a signed attestation), and derives one categorical,
advisory release decision by taking the most conservative
category any input proposes — worst-wins, spelled out: if any input proposes hold → hold; else if any
input proposes canary → canary; else ship. The decision rule is deterministic code (gate.mjs), not a
judgment call: given the same bundle, it always yields the same decision. That determinism is scoped to this one
step — the gate decision over an already-produced bundle is reproducible; the upstream evidence-gathering that
fills the bundle (the Playwright/Cypress run, the audit-test mutation) is not itself claimed to be
deterministic, and the Gate makes no promise about it.
Steps
1. Resolve the evidence inputs
Execution evidence — at least one E2E result is required (both may be given; the gate takes worst-wins across them, so ship needs every suite green):
- Playwright report (
--playwright): the JSON reporter's output — from$ARGUMENTS, or discover it (Globfortest-results/results.json, or theoutputFileinplaywright.config.*). - Cypress result (
--cypress): the aggregate objectcypress.run()resolves to (aCypressRunResult). Cypress does not write this to a file on its own, so produce it with a tiny Node wrapper:
Why the Module API result and not// save-cypress-result.mjs → node save-cypress-result.mjs import cypress from 'cypress'; import { writeFileSync } from 'node:fs'; const r = await cypress.run(); // runs the suite, resolves to CypressRunResult writeFileSync('cypress-results.json', JSON.stringify(r, null, 2));cypress run --reporter json? Only the Module API result preserves per-testattempts[]— the only place a flaky (failed-then-passed-on-retry) test is recorded, because Cypress emits no aggregateflakycount. the Gate derives the WARNED signal from those attempts; the plain mochajsonreporter has no attempts and would silently drop the flake (a false green). Verified against the Cypress Module API + test-retries docs (2026-07-17).
If there is no E2E result at all, tell the user to run their suite first — the Gate ingests a report, it does
not run the suite. An empty or zero-test report (nothing executed to a pass/fail verdict — e.g. a suite
that never ran, or a wrong --playwright path) is treated as no execution evidence → hold, never as a
pass: a green-looking {} is exactly the false confidence the Gate exists to refuse (#111).
A report that DID execute something but only a sliver of what the framework discovered — a discovery/filter/
config mistake that runs 1 of 1000 tests and skips the rest — is a related but distinct false confidence: it
reads PASSED and is capped at canary by the executed-floor, not treated as green
(#157; see Step 3).
- audit-test verdict (optional) — two grades of credibility evidence, best first:
- Parsed emission (
--audit-test-json): agate-audit-test/v0.4tally written by/audit-test --emit-json=<path>. This is the graduated input — a parsed confirmed-clean verdict that also clears the examined-floor (deepAudited/audited≥ 50% by default) is the only thing that can lift the ceiling toship(#127, ADR-0035). - Opaque report (
--audit-test): a Markdown report from a prior/audit-testrun. Carried verbatim but not machine-read → caps the decision atcanary(human-must-read). - Neither: fine — absence also floors at
canary(no-credibility-evidence), so a bare green Playwright run can never launder intoship. - If both a
.jsonemission and a.mdreport are given, the parsed emission decides and the Markdown rides along inline for the human. A malformed or arithmetically-inconsistent emission is rejected, not silently dropped (hostile-review finding #2, 2026-07-25, ADR-0042): it persists as its own distinctrejectedentry — content-addressed into the bundle's subjects, visibly markedrejected(notabsent) in the rendered report — floors at the samecanaryceiling absence already got, but is never silently identical to "nothing was ever sent." A warning still prints; the difference is that the rejection now also survives into the one artifact you sign and keep.
- Parsed emission (
- Trace matrix (optional,
--trace-json) — a requirement→test traceability matrix in Gate's owngate-trace-matrix/v0shape (not any external tool's internal format), joined against--audit-test-jsonto answer "what business risks are actually covered?" without building a risk register (#199, ADR-0045). Purely informational — it never affects the ship/canary/hold decision. See Step 4 below. If the user has run TEA'straceworkflow, do not hand-author this file — convert their run withtea-to-trace-matrix.mjs(Step 1a). - PR head commit:
git rev-parse HEAD— the bundle's subject.
1a. Converting a TEA trace run into a trace matrix (only if there is one)
Skip this entirely unless the user has run TEA's trace workflow (bmad-testarch-trace) and wants the
business-risk read. Never hand-author a gate-trace-matrix/v0 file from a TEA report, and never
transcribe one by reading traceability-matrix.md yourself — that Markdown carries each mapped test as
`id` - file:line with no test title, and Gate's join key is <file>::<title>, so anything you
produced that way would be a guessed key that joins to nothing and renders as honest-looking unverified
coverage (ADR-0050).
Run the converter instead:
node "<skill base dir>/tea-to-trace-matrix.mjs" --trace-md=<path to traceability-matrix.md> \
[--gate-json=<gate-decision.json or e2e-trace-summary.json>] \
[--audit-test-json=<the same tally you will pass Gate>] \
[--test-key=path|basename] --out=trace-matrix.json
It reads the Phase-1 coverage-matrix JSON that TEA's own step-05 reads — the only TEA artifact carrying
per-requirement rows and per-test titles — by following the tempCoverageMatrixPath pointer in the
.md's frontmatter. Pass --coverage-matrix=<that file> directly if you already have it.
Three things to relay honestly rather than work around:
- It refuses more readily than it guesses. An unrecognized coverage value, a missing priority, a test with no title, a contradictory row — it names the row, writes nothing, and exits 2. That is the correct outcome: fix it in the TEA run and re-convert. Do not patch the output by hand to get past a refusal.
- The input is a temp file. TEA writes it to
/tmpwith a timestamped name, so convert in the same session as thetracerun. If the pointer is stale, the fix is to re-run*trace, not to parse the Markdown. - Check the join keys before gating. Passing
--audit-test-jsonmakes it report how many keys actually match aruns[]record. TEA spells files as repo paths; anaudit-testemission may use basenames — if none match, it says so and names--test-key=basenameas the fix. A matrix whose keys never match still produces a full, plausible-looking report in which every requirement readsunverified. Surface that warning to the user; never silently gate on a zero-match matrix. Note the one-sided limit it prints in basename mode: it can only check the files TEA mapped, so a same-named spec in two directories on theaudit-testside is undetectable — prefer--test-key=pathwhenever both sides can agree on paths.
2. Run the deterministic gate
Run the bundled script from this skill's base directory (shown to you when the skill was invoked):
node "<skill base dir>/gate.mjs" (--playwright=<results.json> | --cypress=<cypress-results.json>) \
[--audit-test-json=<tally.json>] [--audit-test=<report.md>] [--trace-json=<trace-matrix.json>] \
[--examined-floor=<pct>] [--executed-floor=<pct>] [--max-age=<minutes>] [--sign-key=<private-key.pem>] --commit=<sha> --out=gate-bundle.json
(Pass --playwright, --cypress, or both — at least one is required. --examined-floor defaults to
50; a requested value below the 25 minimum is clamped, with a warning, never silently accepted —
only pass it when you consciously want to accept a narrower deep-audited scope than the default.
--executed-floor works the same way, but on the execution axis instead of the credibility axis —
default 50, clamped to a 25 minimum — and gates how much of what the suite discovered actually
ran (#157); only pass it when you consciously accept
a narrower executed scope (e.g. a deliberately tag-filtered run) than the default.
--max-age is opt-in with no default — pass a number of minutes to cap a suite at canary when its
report claims to have started longer ago than that, relative to when this bundle is assembled (a stale
leftover results.json from an earlier run); omit it and no freshness check runs at all.
--sign-key is optional — omit it and the bundle is unsigned, exactly as before this option existed.)
The script (gate.mjs) ingests, assembles the bundle, runs the worst-wins gate, appends a
gate.local/gate/v0 entry, validates against the honesty guard
(schema/evidence-bundle.v0.schema.json), and prints the report.
Do not recompute or override the decision — it is the script's deterministic output.
Optional: sign the bundle (#141,
ADR-0037 §1). With no --sign-key, stop here — the bundle
is the same unsigned, in-toto-shaped JSON it has always been. To make it tamper-evident:
node "<skill base dir>/gate.mjs" --gen-key=<path-prefix> # once: writes <prefix>.pem (private,
# keep secret) + <prefix>.pub.pem (public)
node "<skill base dir>/gate.mjs" ... --sign-key=<prefix>.pem --out=gate-bundle.json # signs at gate time
node "<skill base dir>/gate.mjs" --verify --bundle=gate-bundle.json --pubkey=<prefix>.pub.pem # anyone with
# the public key can check it later
A signed bundle carries a dsseEnvelope — a DSSE envelope over an ed25519 signature (node:crypto, zero new
dependency) covering the whole normalized bundle: the subject[] (pr-head + the #139 input digests + a
per-entry digest for every parsed evidence entry, #158),
producedOn, schemaVersion, and the gate decision itself
(ADR-0040) — so editing any of them after signing
invalidates it. This is self-signed, proving integrity (unaltered since signing) and continuity
(same key across runs) — it is not Sigstore and proves nothing about third-party identity. Report it
accordingly: a signed bundle may be called "signed," "tamper-evident," or a "DSSE-signed attestation"; never
"Sigstore-verified," "trusted publisher," or "verified identity."
3. Present the decision — as-is
Show the script's report: the decision, the per-input proposals (it shows its work), and the rationale. Tell the user where the bundle was written. State plainly, every time, that the Gate ingested existing evidence and did not run the suite or launch a browser — this is the Gate's core boundary (see Ingests, never executes below); it must be said in the presented report itself, not left implicit in the decision or assumed from context. Then interpret it honestly:
hold— an E2E failure (Playwright or Cypress), an empty/zero-test report, or no execution evidence at all dominates. Route the red to/debug-test; the gate is not the place to fix it. (A confirmed-hollowaudit-testfinding is acanary, not ahold— the code may be fine; it's the test that needs fixing.)canary— release cautiously with monitoring / a human gate. Read the rationale for why it floored:human-must-read: an opaqueaudit-testreport is present — a human must read it (the Gate carries it but does not machine-read it). Re-gate with a parsed emission (--audit-test-json) to let the Gate read it.no-credibility-evidence: noaudit-testat all — run/audit-test --changed --emit-json=<path>and re-gate.- confirmed-hollow / likely-hollow / baseline-lock:
audit-testfound a real credibility defect — fix the flagged test(s) (/audit-testnames them), then re-gate. - examined-nothing / reasoning-only: the audit ran but proved nothing (deep-audited 0, or the env wasn't runnable) — nothing was execution-verified, so credibility is unconfirmed.
- below the examined-floor:
PASSED+confirmed, but the deep-audited fraction fell short of the floor (default 50%) — this was a diagnostic run (it examined the suspects a triage flagged, and found no problems among them), not a certification of the whole suite (#127, ADR-0038). Run audit-test's certification mode (--certify) for a representative-breadth verdict, or re-gate with a consciously lower--examined-floor(never below 25%) to accept this narrower certified scope. - execution incomplete (below the executed-floor): an execution suite reported
PASSED(orWARNED), but the tests it actually ran are a small fraction of what the framework discovered — skipped/pending dominate (e.g.expected:1, skipped:999). The rationale always states the executed-vs-discovered split for every execution suite, whether or not it trips the floor; a PASSED suite whose executed fraction falls short of the executed-floor (default 50%) is capped atcanaryinstead of proposingship(#157) — a green result over a sliver of the suite is not evidence the rest of it ran. Fix the discovery/filter/config that's skipping most of the suite, or re-gate with a consciously lower--executed-floor(never below 25%) if the narrower scope was intentional.
ship— every E2E suite you passed in (Playwright and/or Cypress) is green and clears an executed-floor against what the report itself says it discovered (executed/discovered≥ 50% by default, #157 — the discovered count is self-reported, so a discovery/filter/config change that quietly narrows the suite before the report is written isn't caught), and a parsedaudit-testverdict isPASSED+confirmedand the deep-audited fraction clears the examined-floor (deepAudited/audited≥ 50% by default): the deep audits ran, killed their mutations, found no hollow tests among the deep-audited subset, and that subset was big enough to call the result honest. This proves that subset, not the whole suite —unexaminedtests are not evidence of health, and the report states the examined/unexamined split so the scope is never oversold. This is the one path toship, and it is deliberately hard to reach — a single red suite, an empty/zero-test report, a suite that mostly skipped, an opaque, absent, or vacuous audit, or a confirmed-clean audit that examined too little of the suite, never gets here.
The decision is advisory only — the Gate does not abort the build, and a hold or canary does not by
itself stop a deployment; nothing here enforces anything, so treat the report as input to a human or CI decision,
not as the decision itself (blocking is a future opt-in,
ADR-0026).
4. Business-risk coverage (optional, only when --trace-json was passed)
If a trace matrix was supplied, present the report's ## Business-risk coverage section too — it is
separate from the decision above, never folds into it, and reads its own four states per
requirement: mutation-proven (every mapped test execution-confirmed solid), unverified (mapped,
but no execution-confirmed evidence either way), hollow (a mapped test survived a mutation — the
requirement is not actually guarded, even though the matrix's own presence read calls it covered),
and not-covered (the matrix itself says so). State plainly that this is a join, not a risk
register the Gate maintains (ADR-0045)
— it never appears at all when --trace-json wasn't given, and a malformed one renders as rejected,
the same distinct-state treatment a malformed --audit-test-json gets.
Output Format
Present the script's report verbatim. A canary (opaque audit-test) and the earned ship (parsed
confirmed-clean audit-test) look like:
The Gate ingested existing evidence — it did not run the suite or launch a browser.
## Gate decision: 🟡 CANARY · advisory (did not fail the build)
subject: pr-head `<sha>` · 3 entries
signed: ✗ unsigned — in-toto-shaped, not a signed attestation (pass --sign-key to sign)
### Input digests (content-addressed — swap a file's bytes and this changes)
- `playwright-json` — sha256:084b1c75a70790a66e486e598eca417147c7d010dea112c840d0d3c8a4609349
- `audit-test-report` — sha256:8d0f0197a96b852e3c4e4157efeae154b493e2a92eb1335bd6159b4611a55eb6
### Inputs — worst-wins (each input proposed a category)
- `playwright` — result=PASSED → proposes **ship**
- `audit-test` — present but opaque (unread) → proposes **canary**
### Rationale
- playwright PASSED (12 of 12 discovered tests executed — 100%; 0 skipped) → ship-baseline
- audit-test present but opaque → floor at canary (human must read the report)
- worst-wins over {ship, canary} → canary
> `ship` needs a *parsed* confirmed-clean `audit-test` verdict to unlock … Advisory / report-first.
> Optional: pass --trace-json for a business-risk coverage read … never changes ship/canary/hold.
Bundle written to gate-bundle.json
The Gate ingested existing evidence — it did not run the suite or launch a browser.
## Gate decision: 🟢 SHIP · advisory (did not fail the build)
subject: pr-head `<sha>` · 3 entries
signed: ✓ DSSE (ed25519, self-signed) — keyid `2801ebd3ab3cb4fd6944202388352de3593831f4f1ce0b3276f999a9a0e944d4`
### Input digests (content-addressed — swap a file's bytes and this changes)
- `playwright-json` — sha256:084b1c75a70790a66e486e598eca417147c7d010dea112c840d0d3c8a4609349
- `audit-test-json` — sha256:f638d225f3ccd62753cf623c05fc0e58e5a652b7a3838f4293ff8b60fe1d60e2
### Inputs — worst-wins (each input proposed a category)
- `playwright` — result=PASSED → proposes **ship**
- `audit-test` — PASSED · confirmed → proposes **ship**
### Rationale
- playwright PASSED (12 of 12 discovered tests executed — 100%; 0 skipped) → ship-baseline
- audit-test PASSED + confirmed → ship-eligible — no hollow tests among the deep-audited subset (6 of 12 triaged tests mutation-audited; 6 unexamined — not evidence of health) (50% examined, clears the 50% examined-floor)
- worst-wins over {ship} → ship
> `ship` earned: playwright passed and `audit-test` found no hollow tests among the deep-audited subset (6 of 12 triaged tests mutation-audited; 6 unexamined — not evidence of health). Advisory / report-first.
> Optional: pass --trace-json for a business-risk coverage read … never changes ship/canary/hold.
Bundle written to gate-bundle.json
Adding --trace-json=<trace-matrix.json> appends a separate section after the decision — here paired
with a confirmed-hollow audit-test emission, so the decision itself stays canary while the join still
resolves each requirement on its own:
## Business-risk coverage — informational, does not affect the ship/canary/hold decision
trace matrix: 6 requirement(s) · producer: TEA trace (bmad-testarch-trace) v1.19.1 · matrix gate: PASS
| Requirement | Priority | State |
|---|---|---|
| REQ-BOOKING-OVERLAP | P0 | 🟢 covered and mutation-proven |
| REQ-BOOKING-CONFIRM | P0 | 🟢 covered and mutation-proven |
| REQ-BOOKING-FEE | P1 | 🟢 covered and mutation-proven |
| REQ-BOOKING-ERROR-LOG | P1 | 🔴 covered by a test we proved hollow — booking.spec.ts::logs a booking error |
| REQ-BOOKING-EXPORT | P2 | ⚪ covered but unverified |
| REQ-BOOKING-SLA | P3 | — not covered — no mapped test (the traceability matrix already flags this) |
3 mutation-proven · 1 unverified · 1 hollow · 1 not-covered
> A JOIN over an external traceability matrix + an audit-test verdict, never a risk register this repo
maintains (ADR-0045) …
Notes
- Ingests, never executes (ADR-0010). the Gate reads a Playwright report and a Markdown file — pure consumption. It never launches a browser or a suite. Snapshotting a live response is an execution-layer artifact, out of scope.
- Two E2E frameworks on one execution axis. Playwright (JSON report) and Cypress (Module API
CypressRunResult) both ingest to the same result → proposal mapping; the gate takes worst-wins across every suite present, so a green Playwright can't paper over a red Cypress. The one asymmetry is honest, not hidden: Playwright reportsstats.flakydirectly; Cypress has no such count, so the Gate derives the flaky (WARNED) signal by scanning per-testattempts[]for a failed-then-passed retry — the metric is labelledflakyDerivedin the bundle to say so. (Unit-tested / component ingest is still a later increment.) audit-testrides in three grades. Parsed (--audit-test-json):/audit-test --emit-jsonwrites its batch tally asgate-audit-test/v0.4structured data — the per-class counts, not prose. the Gate derives the category (result+label) from those counts mechanically (same as it restates Playwright'sstats) and the gate reads only the derived category, never the counts (honesty guard #1). Opaque (--audit-test): the Markdown is carried verbatim and not prose-scraped, so it can only floor atcanary. Rejected: a.jsonemission was given but failedparseAuditEmission's shape/consistency check — a distinct state from opaque (unparsed prose) and absent (nothing given), persisted as its own entry rather than silently rendering identically to absence (hostile-review finding #2, 2026-07-25, ADR-0042; see Step 1 above). The theater guard is structural: only a parsedPASSED+confirmedverdict reachesship; opaque, rejected, absent, or examined-nothing all cap atcanary, so there is no "run less, grade better" incentive.scopedisclosure (#171, ADR-0038). A parsed emission may carry an optional free-textscopestring — e.g. a certification run naming how much of the whole suite a narrower scope like--changedcovered. the Gate passes it through unread by the decision (honesty guard #1 untouched —scopenever drivesresult/label) and prints it verbatim next to the deep-audited fraction in its own rendered report, so a certified--changedshipdiscloses its narrowness to whoever reads the Gate report, not only a reader of the raw emission. Whenscopeis absent, the report says so explicitly ("reported scope: none declared") rather than just omitting the clause. Known limit: there is no minimum onauditeditself — the examined-floor is a pure ratio, soaudited: 2, deepAudited: 1clears the default 50% floor on a two-test triage exactly as it would on a two-thousand-test one; the mechanically-derived fraction the report prints is the only anchor a reader gets against the suite's real size.- Run-trace cross-check (#142,
ADR-0037 §3). A parsed emission may also carry an optional
runs[]— one record per test a mutation was actually executed against, with its outcome (killed|survived) and exit code. When present, the Gate cross-checks it against the tally it rides alongside:confirmedSolidmust equal the killed-record count,confirmedHollowthe survived-record count, andruns.lengthmust never exceeddeepAudited. A tally that disagrees with its own trace is rejected the same way an arithmetically-impossible tally is (see the rejected grade above). At ingest this cross-checks the evidence behind aconfirmedlabel against a granular, per-test, internally-consistent trace instead of trusting a bare number (the bundle then records the verified record count,runsVerified, not the per-test trace itself); it does not make the verdict independently verified — the trace is stillaudit-test's own account of its run (Gate cannot re-execute it, ADR-0010), and it opens no new path toship. An emission with noruns[]is unaffected — this is purely additive.runsVerifiedis now surfaced in the rendered report too (hostile-review finding #5, 2026-07-25): theaudit-testline in "Inputs — worst-wins" states either(N run records cross-checked)or(no run trace carried — tally unverified against per-test records), so twoshipverdicts of different evidential weight no longer read identically apart from an input digest. - Business-risk coverage — the trace-matrix join (#199,
ADR-0045). Optional
--trace-jsonreads a requirement→test traceability matrix in Gate's owngate-trace-matrix/v0shape — not TEA's or any other tool's internal format, to avoid coupling tighter than the join needs (orchestrate-don't-absorb) — and joins it against--audit-test-json'sruns[]on test identity (<file>::<title>, the same keyruns[]already uses). The entry this produces is deliberately kept OUT ofgate()'s decision loop — it's appended tobundle.entriesonly after the ship/canary/hold decision is computed, so it can never become a decision input and a bundle with no--trace-jsonis byte-for-byte unaffected. The join exists because TEA's owntracegate is presence-based — Verified against thebmad-testarch-traceworkflow source (v1.19.1,comparisons/tea.md§3): a requirement is marked covered because a matching test exists, never because it would fail if the code broke — so a P0 requirement whose only test is hollow reads as covered and gates PASS. A malformed--trace-jsonis rejected, the same distinct-from-absent treatment--audit-test-jsongets; a valid matrix with no--audit-test-jsonruns[]evidence resolves every mapped requirement tounverifiedrather than a stronger claim. - The TEA conversion —
tea-to-trace-matrix.mjs(#220, ADR-0050). #199 shipped the join but not the last mile: using it against a real TEA run meant hand-authoring JSON. The converter closes that, and the decision behind it is where the value is. Re-reading thebmad-testarch-tracesource at v1.21.4 shows TEA'se2e-trace-summary.jsonandgate-decision.jsoncarry aggregates and a gate signal only, andtraceability-matrix.mdcarries rows but no test titles — while step-04 writes a Phase-1 coverage-matrix JSON that carries both, and records its path in the.mdfrontmatter astempCoverageMatrixPath. That JSON is the input; the Markdown body is never parsed, because a key built from it would be fabricated or line-drift-guessed, and a wrong key renders as plausibleunverifiedcoverage rather than as an error. TEA's five-valued coverage vocabulary flattens into Gate's three withUNIT-ONLY/INTEGRATION-ONLY→PARTIAL(neverFULL— a conversion may not widen TEA's own presence call), keeping TEA's verbatim value on each row asteaCoverage. The converter lives outsidegate.mjson purpose — teaching the gate to read a tool's private format is exactly the couplinggate-trace-matrix/v0exists to avoid — and validates its output by importinggate.mjs's ownparseTraceMatrixand its exported vocabularies, so it can never emit bytes Gate would reject nor refuse a value Gate would have accepted.gate.mjsgains no gate logic from this change: only threeexportkeywords. The TEA-side fixture is built from TEA's source, not captured from an observedtracerun — Confirmed at source, Unexamined at runtime (ADR-0050 Consequences). - Coverage-aware ship gate — the examined-floor (#127,
ADR-0035). A confirmed-clean verdict alone used to be enough to
ship, even if
deepAuditedwas a small minority ofaudited(the shipped fixture used to be4 of 12— 33%). The gate now ALSO requiresdeepAudited/auditedto clear a floor — default 50%, overridable via--examined-floorbut never below a 25% minimum, clamped (with a warning) rather than silently honored. Like everything else in the gate, the floor's numbers live only in rationale prose, never as a field on the gate predicate (honesty guard #3 still holds). - Execution-completeness gate — the executed-floor (#157).
The same coverage-aware shape, applied to the execution axis instead of the credibility axis:
PASSEDused to be enough to proposeship-baselineregardless of how much of the discovered suite actually ran —expected:1, skipped:999readsPASSEDexactly like a fully-run suite. The gate now surfaces the executed-vs-discovered split in the rationale for every execution suite (Playwright'sexpected+unexpected+flakyvsskipped; Cypress'stotalPassed+totalFailedvstotalPending+totalSkipped), and requiresexecuted/discoveredto clear a floor — default 50%, overridable via--executed-floorbut never below a 25% minimum, clamped (with a warning) rather than silently honored — before aPASSEDsuite proposesship-baseline; short of it, it proposescanary. Known limit:discoveredis read straight from the report's own counts (executed+skippedfor Playwright,totalTestsfor Cypress) — the Gate has no independent count of how many tests should exist, so a discovery/filter/config change that narrows the suite before the report is written (atestDir/testMatchchange, a title filter, a shard) can still produce a report that honestly saysskipped: 0and clears the floor at 100%. This floor catches a suite that reports its own skips; it does not catch one that was quietly filtered before it got the chance. Same honesty-guard #3 treatment: the floor's numbers live only in rationale prose, never as a field on the gate predicate. - Content-addressed inputs (#139,
ADR-0037 §2). Every ingested file (the Playwright JSON,
the Cypress JSON, the
audit-testemission and/or report) is sha256-digested and recorded as a subject of the gate Statement, alongside the existingpr-headcommit subject — a lowercase hex string, never a field on the gate predicate (honesty guard #3 untouched). Swap or edit an input file after the bundle is produced and its recorded digest no longer matches: the decision is bound to the exact bytes it ingested, not to a typed commit string. On its own this is not a signature — it detects a swapped input, it does not prove the bundle itself wasn't edited after the fact; pair it with--sign-key(below) for that. - Report freshness —
--max-age(hostile-review finding #3, 2026-07-25, ADR-0042). Opt-in, no default: unlike the examined/executed floors there is no universally-safe staleness threshold (a slow-but-legitimate CI run and a genuinely stale leftoverresults.jsonare indistinguishable without a number only you can supply), so this is off unless requested. Pass--max-age=<minutes>and the gate compares each execution entry's own recordedproducer.startedOn(Playwright'sstats.startTime/ Cypress'sstartedTestsAt— already captured, previously never read) against the bundle's ownproducedOn; a suite whose report claims to have started longer ago than the window caps atcanaryeven if it would otherwise ship, with the staleness named in the rationale. An entry with no recorded start time can't be checked and is silently unaffected — no evidence either way, not flagged stale. Known limit: this only catches a report that's old relative to when this bundle was assembled — it does not, on its own, bind a report to the specific--commitnamed on the bundle; that gap is closed by report-to-commit provenance, next. - Report-to-commit provenance (#177,
ADR-0043). Always on, no flag —
the closure
--max-ageabove can't reach: binding a report to the specific commit being gated, not just to a wall-clock window. A git-timestamp cross-check was considered for this and rejected: a report regenerated now for the wrong commit hasstartedOn ≈ now≥ any commit timestamp, so timestamps don't even catch that case, and the "report predates the commit" signal they could give false-positives the ordinary test-then-commit local workflow. The shipped closure instead measures the real question directly — producer-recorded SHA provenance: the Playwright/Cypress ingest adapters record the git commitgate.mjsitself was run against — preferringGITHUB_SHA(or an equivalent CI-supplied SHA; the checked-out SHA in CI is authoritative) overgit rev-parse HEAD(the honest local signal) — plus a dirty-worktree flag fromgit status --porcelain, captured once per invocation and stamped onto every Playwright/Cypress entry ingested that run. Theaudit-testemission carries its own version instead: passcommitSha/dirtyin the--emit-jsonoutput (see audit-test/SKILL.md "Structured emission"), captured by the model at audit time. Either way,gate()cross-checks the recordedcommitShaagainst--commit: a mismatch caps an otherwise-shipproposal atcanary, named in the rationale ("this report is evidence about a DIFFERENT commit"); a report with no recorded commit is unaffected either way — necessary-not-sufficient, the same discipline content-addressing (ADR-0037 §2) and the examined/executed floors already hold themselves to. A dirty worktree at capture time is disclosed in the rationale, never a cap on its own (only an actual SHA mismatch caps) — there is no single commit that fully describes evidence run against uncommitted changes. Honest, not adversary-proof: a producer can lie about its own recorded SHA (but then the content-addressed input bytes wouldn't correspond to the real commit either) — this closes the accidental wrong-commit-fresh- report case (a stale local checkout, a mismatched CI trigger event), not a motivated adversary; Gate is advisory and self-signed (not a third-party trust root), so that remains outside its honest threat model, same as everywhere else in this file. No schema-shape change to the gate predicate — the cross-check is prose-only inrationale, never a new fieldgatePredicate.inputs[]reads (honesty guard #1/#3 unaffected); the recordedcommitSha/dirtylive only on the additiveproducer.commitSha/producer.dirty(schema bumped togate-evidence-bundle/v0.9, additive — a bundle that never records either is byte-for-byte unaffected). - Optional DSSE signing (#141,
ADR-0037 §1, widened by
#158/ADR-0040)
— opt-in and unsigned by default. Pass
--sign-key=<private-key.pem>andgate.mjswraps the whole normalized bundle in a DSSE envelope: the bundle'ssubject[](pr-head + the content-addressed input digests above plus one content-addressed digest per parsed evidence entry),producedOn,schemaVersion, and the gate decision itself — an ed25519 signature (node:crypto, no new dependency) over the DSSE pre-authentication encoding, withkeyid= sha256 of the public key.--verify --bundle=<path> --pubkey=<path>then confirms a signed bundle wasn't altered after Gate produced it — a tampered payload or the wrong key both fail closed, and a structurally-malformed bundle is rejected before the signature is even trusted. Digest-binding an evidence entry is integrity, not endorsement: the gate predicate still asserts only Gate's own decision, never that a stage's producer vouched for its own result — Gate only attests "my decision was rendered over exactly these bytes." This is self-signed: it proves integrity (unaltered since signing) and continuity (same key across runs), never third-party identity — it is not Sigstore, and the skill must not say "Sigstore," "verified identity," or "trusted publisher." Only a bundle that is signed earns "signed" / "tamper-evident" / "attestation"; an unsigned bundle (the default) keeps saying "in-toto-shaped, not a signed attestation." Known limit:--verifyrejects anyschemaVersionmismatch as a shape error before it even checks the signature, with the same "cannot vouch for a malformed bundle" message tampering would produce — so a bundle signed under an older schema version fails--verifywith a tamper-shaped error, not a version-shaped one, onceSCHEMA_VERSIONmoves on. There is no version allow-list; a signed bundle must be re-generated (and re-signed) after a schema bump to keep verifying. - No manufactured number. There is no
confidence/score anywhere; the gate reasons over categories, not magnitudes. The schema forbids a numeric field in the gate entry — re-adding one requires a schema-version bump, which is the signal a real calibration loop has landed. - Advisory / report-first (ADR-0013) —
it recommends, it never blocks a merge in v0. A
holdorcanaryis a recommendation for a human or a CI pipeline to act on; the Gate has no mechanism of its own to stop a build or a deployment. This now holds for a malformed input too (hostile-review finding #4, 2026-07-25): a truncated/unparseable Playwright or Cypress report used to crash with an uncaughtSyntaxError, and a missing--audit-test/--audit-test-jsonpath used to throwENOENT— both exited 1 despite the "never fails the build" claim. Both now degrade instead: an unreadable execution report becomes anEMPTYentry (the same, already-understood "unrun report is not a pass" path, #111) capping the decision athold; a missing credibility-evidence path warns and falls back to whatever else is available (or absent). Exit code stays 0 either way —--verifyand internal bundle-shape-validation failures remain the only paths that exit non-zero. - Housing & extraction: everything lives under this one directory with a
gate://namespace, so lifting the Gate to a standalone plugin is a folder move (#99, #102). - Naming: this skill was called "Witness" before ADR-0032 renamed it to "Gate" (#113). ADRs and other historical records written before that rename still say "Witness" by design — history isn't rewritten — but every current user-facing surface (this file, the README, the CLI output) says "Gate."
--explainis not supported — procedural, not pedagogical.