Latch
September 18, 2026 ยท View on GitHub
Your CI is red. Is it your code โ or the environment?
Latch reads a finished test run and returns one verdict before you merge:
| Verdict | What it means |
|---|---|
๐ข Gate: PASS | It's the environment. Ignore it, merge. |
๐ด Gate: BLOCK | It's a real failure. Don't merge. |
Read-only ยท no test rewrite ยท works with the runner you already use.
npm install && npm run demo # no key, no network, ~10 seconds
How it works
flowchart LR
run["Red test run<br/>Playwright ยท Jest ยท pytest"] --> cluster["Cluster failures<br/>by signature"]
cluster --> judge["Jev labels each cause<br/>one call per cluster"]
judge --> policy{"Code owns the decision"}
policy -->|env_cascade + infra fingerprint| pass["๐ข Gate: PASS"]
policy -->|real failure| block["๐ด Gate: BLOCK"]
Clustering and the final decision are code. The judge is TypeSafe's Jev (systemOne): one call per cluster, up to 8, and it never gets to say "ignore" on its own.
Why teams use it
| Without Latch | With Latch |
|---|---|
| Red build โ dig through logs โ guess | Red build โ one line โ merge or don't |
| "Is it infra again?" | Gate: PASS |
| "Please don't be my bug" | Gate: BLOCK |
A red E2E suite costs you twice: half an hour working out whether it's real, or a habit of ignoring red โ until the day you merge a real bug.
Who it's for: teams running Playwright (or Jest / pytest) on CI whose suite goes red several times a week for reasons that are not the code.
See it
$ npm run demo
=== 1) an infra outage (8 identical connection errors) ===
Latch: 8 failed โ 1 cause
P0 env_cascade n=8 conf=1.00 same_root=0.90 blocks=0.40 action=ignore_as_infra (env_cascade) [cached] [new]
Gate: PASS (no blocking cluster) # exit 0 โ merge
=== 2) a real regression (5 failing assertions) ===
Latch: 5 failed โ 1 cause
P0 assertion_bug n=5 conf=1.00 same_root=0.80 blocks=0.60 action=fix_product (assertion_bug) [cached] [new]
Gate: BLOCK โ 1 cluster to look at # exit 1 โ do not merge
What you get
- A verdict, not a dashboard โ
PASSorBLOCK, per run. - One line per cause โ infra outage, flaky test, moved locator, wrong assertion.
- Memory โ
[seen x12, flake 40%]: this cluster keeps coming back. - A filter for known noise โ suppress a cluster once, it stops blocking.
- No silent ignores โ
ignore_as_infrarequires an explicit network fingerprint.
Install
// playwright.config.ts
reporter: [["list"], ["./src/reporter.ts"]]
One key in .env: TYPESAFE_API_KEY. Missing key still prints clusters (needs_human / no_key) and never fails Playwright.
Optional: LATCH_MODEL (default jev-latest), LATCH_STORE (ledger path), LATCH_INPUT_USD_PER_MTOK, LATCH_OUTPUT_USD_PER_MTOK (cost estimate only).
Then gate your CI:
npm run latch -- junit.xml --gate # exit 1 when a real failure blocks
latch reads a JUnit XML report (or a { run, attempts } JSON). The reporter itself never fails Playwright, so the gate exit code is a separate CI step.
Every run writes traces/latch-report.json, traces/latch.md and a self-contained traces/latch-report.html (inline CSS, no external asset) you can open or attach as a CI artifact. latch <file> --html <path> writes the same HTML from the CLI.
Reference โ gate, any runner, ledger, policy, proof
Merge gate
--gate exits non-zero when a reported cluster is not confirmed infra noise. ignore_as_infra and suppressed clusters pass; fix_product, fix_test and needs_human block. The reporter itself never fails Playwright โ the gate exit code belongs to the CLI, so CI runs it as its own step.
Any runner (JUnit XML)
npm run latch -- testdata/junit/jest.xml # or any .xml / golden .json
apiName comes from Playwright patterns, else the failure type, else the exception name in the message; the concise message is preferred over the traceback so a test name cannot leak into the signature. Measured live: jest 5 โ 2, pytest 3 โ 2; go.xml is the canary for the limit (no stable message โ no collapse).
Failure ledger
Every run appends its clusters to .latch/store.json (override LATCH_STORE; empty disables) and labels them from past runs:
P0 env_cascade n=4 action=ignore_as_infra (env_cascade) [seen x2]
P1 assertion_bug n=1 action=fix_product (assertion_bug) [new]
P2 locator.waitFor n=8 action=fix_test (flake) [seen x5, flake 40%]
Judgments are cached per signature ([cached]): a known cluster costs no API call, does not count against the 8-call cap, and a known run reports with no key at all. The store is written atomically.
Policy (code owns the decision)
- No key / API error โ
needs_human cause.confidence < 0.55โneeds_humansame_root < 0.5โneeds_humanenv_cascade+same_root >= 0.7+ an infra fingerprint โignore_as_infra; otherwiseneeds_humanflakeandflaky_count >= 1โfix_testlocator_driftโfix_testassertion_bugand (blocks_merge >= 0.55or Jevaction = fix_product) โfix_product- else โ
needs_human
Thresholds are calibrated (npm run calibrate); blocks_merge sits in a noise band ~ยฑ0.03 and 0.55 falls in the empty gap, so the verdict does not flip.
Proof
npm run test:unit # goldens, policy, ledger, ingestion โ no network
npm run test:e2e # real Playwright, intentional failures
npm run test:stability # signature drift over repeated runs
npm run calibrate # verdict flip rate (needs a key)
npm run test:live # Jev on goldens (needs a key)
npm run demo # offline merge-gate demo
Good at โ and not yet
- โ Good at: collapsing an infra cascade (70 identical connection errors โ 1 cause) and refusing to silently ignore a non-infra failure.
- โ ๏ธ Not yet: grouping a logic regression whose many tests fail with different assertion messages โ those fragment into separate clusters. Measured, not hidden:
experiments/click-real.
Limits
- Grouping is message-based, so a logic regression fragments. On
pallets/click: 2 real regressions โ 13 failures โ 10 clusters. The "70 โ 1" figure is an infra-cascade property, not a general one. - Signature =
apiName+ first 80 chars of the normalized error (ANSI/UUID/id tokens/timestamps/durations/pixel diffs stripped, secrets redacted; ports kept as service identity). - Error text is redacted (tokens, keys,
password=) in every output โ terminal,traces/, PR comment and the Jev state. - At most 8 Jev calls per run; cached clusters are free.
Docs
docs/positioning.mdโ the problem, the wedge, the business model, the risks.docs/validation-plan.mdโ the 30-day plan to prove demand before building more.
License
MIT โ see LICENSE.