Skip to content

RELEASE-5 W5: generated qualified release contract and tool evidence ledger - #54

Open
AminDhouib wants to merge 21 commits into
audit/release-integrationfrom
audit/release-5-w5
Open

AminDhouib wants to merge 21 commits into
audit/release-integrationfrom
audit/release-5-w5

Conversation

@AminDhouib

Copy link
Copy Markdown
Member

RELEASE-5 (W5) of plan_RELEASE — the qualified release contract, the limitations
register, and the ONE generated evidence source. Base: audit/release-integration
(PR #51, 31/31 green). Zero src/ edits.

This PR also merges PR #52 (audit/release-get-cookies, eaa574e): the contract may
only claim a tool whose cited node this run executes and passes, so the node had to be in
this tree rather than cited from a branch this gate does not run.


What landed

tools/release_evidence.py — the sole parser/generator for release-evidence/v1.
30 required job/matrix cells each write
release-evidence/<release_sha>/<job_id>/<matrix_cell>.json; the new release-evidence
job re-reads all of them through this one module and writes
release-evidence/<release_sha>/release-gate/aggregate.json.

Every cell declares what it owes (expects_pytest, expects_chrome,
expects_launched_chrome), so null can never be a way to omit evidence: a browser cell
with no Chrome identity fails, and a handshake-only macOS smoke cell that suddenly claims a
launch fails too.

Fail-closed rejection paths, each with a negative test (tests/test_release_evidence.py,
55 tests): missing / extra / duplicate child · body-vs-path disagreement · stale release SHA
· foreign run_id / run_attempt · non-success terminal outcome · invalid outcome enum ·
unknown or missing field · malformed SHA / run id / cell · runner with no GitHub-hosted image
identity (self-hosted is outside the matrix) · missing Chrome identity on a browser cell ·
launch claimed by a non-launching cell · pytest block omitted where owed or invented where
not · unsorted or duplicated node ids · JUnit hash mismatch · artifact hash mismatch ·
artifact never uploaded · unknown artifact kind · malformed / duplicated / absent MQ id ·
claim whose node never ran · claim whose node was skipped · claim citing the
representative journey
· stdio claim evidenced by a job that does not run the stdio
lane
· claim naming an undeclared cell · claim with no cells.

The gate wiring. Every required cell emits with if: always(), so a red cell still
writes a record saying it was red — and the aggregate rejects it. release-evidence is a
direct needs: edge of release-gate in addition to every child it validates, never
instead of one. The ledger can only make a green run red. actionlint clean.

tools/gen_release_contract.py + root RELEASE_CONTRACT.md — generated, with --check
in quality and a unit test that prints the diff on drift.


The number: 94 served, 3 release-qualified

set_cookie / get_cookies / clear_cookies, on
tests/test_e2e_transport_cookies.py::test_real_transport_cookie_round_trip, qualified on
Linux/X64 + Windows/X64 — two cells, never three (macOS transport is excluded under
F-773). The count is derived from the claim ledger and re-verified against the run's records;
nothing about it is typed.

The other 91 tools are served-unqualified. That is F-776, opened by this PR: §2.5 rules
out a .fn-only call, an in-memory client, an exemption, a characterization, and the
representative journey
as per-tool evidence — and the transport lane's only other node is
that journey. The get_cookies hard block turned out to be the visible tip of a general gap.


Also recorded

  • F-777get_cookies through the in-process .fn seam hangs and poisons the tab's
    CDP connection for the next call
    . Same tool, same Chrome, correct over real stdio: the
    blast radius is the harness seam, not the user's path.
  • F-778get_cookies declared -> list[dict[str, Any]], returns nodriver Cookie
    dataclasses; wire shape correct, only fastmcp's .data reconstruction is opaque.
  • install-smoke (sdist Linux/X64) cold-spawn flake — one first-attempt failure, passed
    on re-run, warmup retry did not absorb it. The contract therefore asserts no
    flake-freedom.

Written as a shipping 2.0.0 contract

  • names the version it ships (read from pyproject.toml; a tagged run already fails on a
    tag/version disagreement);
  • the 1.x breaking change is above the tables: STEALTH_MCP_SESSION_STORAGE_CAP_GB and
    --session-cap-gb renamed with no back-compat alias — the env var fails silently;
  • refuses the blind-push property (§0.2 rests it on parity, flake-freedom and mutation
    strength; none exist);
  • W6–W16 read NOT EVIDENCED in this release, never "planned".

Human actions

  1. The 2.0.0 version bump must regenerate the contract in the same commit (uv run python tools/gen_release_contract.py --write). The drift test will say so, with a diff.
  2. F-776 / F-777 / F-778 need ids registered wherever findings are tracked.
  3. PR RELEASE W5-prep: real-transport get_cookies success path #52 is merged here; if it also merges to the base, this becomes a no-op dedup.

Gates: ruff format/check clean · suppression owners clean · file budgets clean, no cap padded
· ty 76 diagnostics (baseline) · full unit suite 910 passed / 1 skipped, no new skips ·
actionlint clean.

AminDhouib and others added 21 commits July 25, 2026 12:33
plan_RELEASE §2.5's `get_cookies` hard block clears via **option (a)**: a real
Chrome + real transport test now sets a cookie, retrieves it, and asserts its
value. W5 may claim 94 release-qualified tools.

Evidence node:
  tests/test_e2e_transport_cookies.py::test_real_transport_cookie_round_trip

A dedicated collected node, deliberately: §2.5 rules that a *representative
journey* cannot carry a per-tool success claim, and §2.1 names the canonical
journey as exactly that — so folding these assertions into `_canonical_journey`
would have produced evidence W5 must reject. The machinery is still the one
harness (absolute installed launcher, isolated HOME/session root, fixture app,
stdio tools/call, bounded teardown); `stages="cookies"` selects a third declared
segment on top of it. No second journey mechanism, no second fixture.

The round trip, entirely through tools/call:
  set_cookie -> get_cookies(urls=[url]) -> ASSERT VALUE -> get_cookies()
  -> document.cookie cross-check -> clear_cookies -> assert gone

The value is unique per run so a stale cookie cannot forge the assertion, and
the document.cookie cross-check proves the cookie reached the browser rather
than get_cookies echoing what set_cookie was handed. Verified by mutation:
tampering with the value set makes the node fail on the value assertion.
set_cookie and clear_cookies are proved on the same real path (removal is
proved by re-reading, not by trusting the return value).

Why this was believed impossible, and what actually changed
----------------------------------------------------------
`get_cookies` was the sole `E2E_EXEMPT` name, on the grounds that it "hangs
against real Chrome ... and poisons the tab's CDP connection". That reason is
seam-specific, not product-wide. Measured on this base, same tool, same Chrome:

  * in-process `.fn` seam — Network.getCookies AND getAllCookies both hang
    (30s, no return), and the next call on that tab dies with a 10s CDP
    timeout. The exemption's description of the symptom was accurate.
  * real stdio transport + detached backend — both retrieval paths return,
    document.cookie agrees, clear_cookies works, later calls are fine.

The transport path is the one users actually have, so the tool works; the E2E
suite simply could not reach it. `get_cookies` therefore moves from E2E_EXEMPT
to E2E_COVERED (covered by the transport node) and E2E_EXEMPT is now empty. The
seam hang is recorded at both call sites rather than erased, and is routed as a
finding — not fixed here: plan_RELEASE is zero-`src`.

Also noted for routing, deliberately NOT fixed: `get_cookies` is declared
`-> list[dict[str, Any]]` but returns nodriver `cdp.network.Cookie` dataclasses.
pydantic serializes those correctly, so the wire shape a user receives is right
and this is cosmetic; but fastmcp's `result.data` reconstructs them as an opaque
`[Root()]`, which is why this node asserts on `structured_content`.

Gates: ruff format+check clean; ty 76 diagnostics (baseline); vulture,
suppression owners, file budgets clean; unit lane 821 passed / 1 skipped.
W1's canonical journey re-verified green alongside the new node.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
W5 decides what a green gate may be CLAIMED to mean. Three pieces, one truth
each.

1. `tools/release_evidence.py` -- the SOLE parser/generator for the closed
   `release-evidence/v1` schema. Every required job/matrix cell writes
   `release-evidence/<release_sha>/<job_id>/<matrix_cell>.json`; the
   `release-evidence` job re-reads all 30 of them through this one module and
   writes `.../release-gate/aggregate.json`. Each cell DECLARES what it owes
   (`expects_pytest`, `expects_chrome`, `expects_launched_chrome`), so `null` can
   never be a way to omit evidence: a browser cell with no Chrome identity, or a
   handshake-only macOS smoke cell that suddenly claims a launch, both fail.

   The aggregate fails closed on a missing/extra/duplicate child, a body that
   disagrees with its path, a stale release SHA, a foreign workflow run or
   attempt, a non-success terminal outcome, a runner with no GitHub-hosted image
   identity (self-hosted is outside the matrix), missing Chrome identity, an
   omitted pytest block, unsorted/duplicated node ids, a JUnit or artifact hash
   mismatch, an artifact never uploaded, a malformed/duplicate/absent MQ id --
   and on any per-tool claim the run did not actually prove.

2. The gate wiring. Every required cell emits its record with `if: always()`, so
   a RED cell still writes a record saying it was red and the aggregate rejects
   it. `release-evidence` is a direct `needs:` edge of `release-gate` IN ADDITION
   to every child job it validates, never instead of one: the ledger can only
   make a green run red.

3. `tools/gen_release_contract.py` + root `RELEASE_CONTRACT.md`. The contract is
   generated and CI fails on drift (`--check` in `quality`, plus a unit test).
   Every number in it is derived: the served count from SECTION_TOOLS, the
   qualified count from the claim ledger -- each claim re-verified against the
   run's real records -- and the matrix from the ledger's required cells, which a
   workflow pin holds equal to release-gate.yml's matrix.

What the contract says at this SHA, and why it is uncomfortable: plan_RELEASE
2.5 rules that a `.fn`-only call, an in-memory client, an exemption, a
characterization, or THE REPRESENTATIVE JOURNEY cannot qualify a tool. The
transport lane contains exactly one node -- the representative journey -- so no
served tool has per-tool real-transport success evidence here. That is recorded
as F-776 rather than smoothed over: the `get_cookies` hard block turns out to be
the visible tip of a general gap, not a lone exception.

The limitations register enumerates every open defect (E8-1..4, E7-1, E7-6,
F-165, F-181, close-path flake, the unreproduced macOS `close_tab` observation,
F-773, F-774, the F-775c/F-775d/`_replace_main_tab` residuals), every excluded
surface (HTTP unauthenticated, the code-execution boundary, architectures and
channels, native IME, live web), W7's exact public-surface exclusions, and every
workstream that has NOT run -- including W14, so the contract makes no upgrade or
rollback claim at all.

Zero `src/` edits: tests, CI and docs only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The contract may only claim a tool whose cited node THIS run executes and
passes -- the ledger rejects a claim it cannot verify. PR #52's dedicated
real-transport node is that evidence for set_cookie/get_cookies/clear_cookies,
so it is merged here rather than cited from a branch this gate does not run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The get_cookies hard block (plan_RELEASE §2.5) clears via option (a): PR #52's
dedicated real-transport node sets a cookie, reads it back and asserts the exact
value. Three claim rows follow from it — set_cookie, get_cookies, clear_cookies —
because that one node asserts a distinct user outcome for each: the cookie really
reaches the browser (document.cookie confirms it independently, so get_cookies
cannot be echoing its own input), the set value returns from BOTH CDP retrieval
paths against a per-run unique value, and removal is proved by re-reading rather
than by a return value.

The headline is therefore 3 of 94 release-qualified, not 93 or 94. Nothing else
in the tree clears the §2.5 bar: the only other real-stdio evidence is the
representative journey, which §2.5 disqualifies as per-tool evidence, and
everything else is the in-process `.fn` seam or the in-memory client. That gap is
F-776, and this number is its measure.

- MQ-53 rewritten as option (a): "returns the exact value that was set", citing
  the node, with the bound stated — Linux/X64 + Windows/X64 only, macOS excluded
  under F-773. The old `[KNOWN-BUG: get_cookies_hang]` block is recorded as
  MEASURED to be the `.fn` seam (F-777), not reinterpreted away.
- MQ-111 rewritten from an inventory requirement ("every tool has >=1 E2E test",
  which set equality can satisfy without behaviour) to "every served tool has a
  visible state in the contract", evidenced by the contract tests.
- New register rows: F-777 (get_cookies hangs through the `.fn` seam and poisons
  the tab's CDP connection while succeeding over real stdio — a seam failure can
  misrepresent the served path in EITHER direction, so an exemption justified by
  seam behaviour may rest on a false premise), F-778 (declared return type vs.
  nodriver dataclasses; the WIRE shape is correct), and the observed
  install-smoke cold-spawn flake, which is why the contract refuses the
  flake-freedom claim §0.2 would otherwise need.
- Ledger tests: the shipped claims are verified BOTH ways — clean when their node
  passes on the cells they name, red when it did not run. The structural tests
  use an empty claims document so they test the ledger, not the day's claims.

No CI run has validated any of this yet: the ledger has no current-SHA evidence,
so `release-evidence` will fail closed until this branch runs. That is the design
working, not a defect to route around.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Human decision 2026-07-25: tag 2.0.0 now, at W5, rather than after W16 as
plan_RELEASE originally stated. The cost was stated and accepted; the changelog
therefore declares W6-W16 as NOT EVIDENCED rather than letting their absence
read as a passing result.

Bumped 1.2.0 -> 2.0.0 in pyproject.toml, the README install pins, and the
RUNBOOK example.

Left alone deliberately:
- singleton.py's "<= 1.2.0" comments are historical statements about which
  released backends lack the version file. They are correct as written.
- tools/package_verify.py and tests/ use 1.2.0 as an illustrative sample
  version; nothing binds the real package version (verified: no
  importlib.metadata/__version__ assertion anywhere in src/ or tests/).
- smoke_mcp.py is gitignored (untracked local helper); its version assertion
  was updated in the working copy only.

The major bump is required regardless of scope: STEALTH_MCP_SESSION_STORAGE_CAP_GB
-> STEALTH_MCP_BROWSER_SESSION_STORAGE_CAP_GB and --session-cap-gb ->
--browser-session-cap-gb ship with NO back-compat alias, so the old names lapse
silently on upgrade. That is the lead item in the changelog.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…PPING one

Two inputs land here.

1. The `get_cookies` hard block cleared via plan_RELEASE 2.5 option (a). The
   merged node sets a cookie over real stdio against real Chrome, reads back its
   EXACT value from both CDP retrieval paths, cross-checks `document.cookie`, and
   proves removal by RE-READING. It is a dedicated collected node, never the
   representative journey, so 2.5 accepts it -- and it qualifies three tools on
   the one path: set_cookie, get_cookies, clear_cookies. Bound stated in the
   contract, not implied: the transport lane is Linux/X64 + Windows/X64, so those
   rows are qualified on exactly TWO cells, never three (F-773).

   The claim rows go through the ledger like any other: the `release-evidence`
   job re-checks that this run executed and passed that node on both cells, and a
   local test refuses a claim citing a node this tree does not define.

2. The human is tagging now rather than after W16, so this document is what
   users read before installing. Rewritten accordingly:

   * it names the version it ships (read from pyproject.toml -- a tagged run
     already fails on a tag/version disagreement, so the two cannot diverge);
   * the 1.x BREAKING CHANGE is above the tables, not buried in a register:
     STEALTH_MCP_SESSION_STORAGE_CAP_GB and --session-cap-gb were renamed with NO
     back-compat alias. The env var is the dangerous one -- nothing errors, the
     configured storage cap silently stops applying;
   * it refuses the blind-push property outright. plan_RELEASE 0.2 rests it on
     manual-QA parity, flake-freedom and mutation-informed strength; none of the
     three exists, so the contract says the gate is strong on what it covers and
     is not a substitute for a human release pass;
   * every workstream that produced nothing reads NOT EVIDENCED with "the reader
     may not infer that it was checked" -- not "planned", which reads as a
     roadmap for a document that is shipping.

Three findings recorded rather than smoothed over:

* F-777 -- `get_cookies` through the in-process `.fn` seam hangs AND poisons the
  tab's CDP connection for the next call. Same tool, same Chrome, fine over real
  stdio: the blast radius is the harness seam, not the user's path. That is why
  the E2E exemption's stated reason was wrong, and why the tool's coverage now
  lives in the transport lane.
* F-778 -- `get_cookies` is declared `-> list[dict[str, Any]]` but returns
  nodriver Cookie dataclasses. The wire shape is correct (pydantic serializes
  them); only fastmcp's `.data` reconstruction is opaque. Cosmetic, recorded.
* install-smoke (sdist Linux/X64) cold-spawn flake -- one first-attempt failure
  on a Chrome cold spawn, passed on re-run, warmup retry did not absorb it. A
  known flake in a required cell belongs in a shipping contract; W8 owns the
  disposition, so the contract asserts no flake-freedom.

F-776 narrows from "no tool has per-tool transport evidence" to "only the cookie
tools do" -- the gap is real for every other served tool and the register says so.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A drift check that only says "the contract is stale" sends the reader to
regenerate blindly -- which is exactly how a deliberate change gets
rubber-stamped instead of reviewed. The failure now prints the first differing
lines, so whoever hits it (the 2.0.0 version bump will, since the contract names
the version it ships) can see WHAT changed before they accept it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
W5's contract generator resolved to 94 served / 3 release-qualified, not 94
qualified. plan_RELEASE 2.5 disqualifies exactly the evidence the other 91 have:
a `.fn`-only call and the representative journey cannot satisfy a transport or
cross-OS success claim. Verified independently: `pytest -m transport
--collect-only` yields ONE node on this base. My earlier "the contract can claim
all 94" was wrong - I carried forward the get_cookies hard-block framing instead
of re-deriving the per-tool rule.

The changelog was silent on this. Stating it, and stating it in BOTH directions:
the 91 are covered end-to-end against real Chrome (set-equality tripwire, 94
covered / 0 exempt), they simply are not verified over the wire. Omitting it
would overclaim; reporting a bare "3 qualified" would read as "91 untested",
which is equally false.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e sdist

Found while dry-running the publish path for a v2.0.0 tag. The sdist was 15 MB
against a 192 KB wheel: 12.8 MB assets/ (demo media) + 2.8 MB audit/ (the
campaign's internal plans and findings). W3's install-smoke installs the sdist on
three runner cells, so the weight was paid on every gate run as well as by anyone
building from source.

Verified safe before changing anything: package_verify's `_check_sdist_members`
requires only pyproject.toml + README.md at the sdist root, and
`_check_package_data` requires the embedded/js files, which live under src/.
Neither excluded directory is structurally required. tests/ is kept deliberately
so a downstream packager can still run the suite from the sdist.

Measured locally, not assumed:
  sdist  15 MB  -> 592 KB   (25x)
  wheel  192 KB -> 192 KB   (unchanged)
  package_verify verify --expect-version v2.0.0 -> OK, before and after
  tests/test_package_verify.py + test_release_workflows.py -> 37 passed

Also confirms the tag/version agreement gate accepts the `v2.0.0` form, so that
failure mode cannot surface late in the tag run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…" reading as "untested"

The first CI run turned 24 cells red, and both causes were real.

1. Node ids. Every pytest cell wrote ids like
   `tests.test_backend_logging.TestBootstrapBackendProcessLogging::test_creates_backend_log_file`
   -- a dotted module path, which is NOT a node id and cannot be resolved against
   `pytest --collect-only`. The validator caught it on the first run, which is
   exactly what it is for.

   The fix is in the PRODUCER, not the check. This pytest configuration writes
   JUnit with `classname`/`name` only -- no `file` attribute -- so a node id could
   at best be guessed from a dotted string. The gate now passes
   `-o junit_family=xunit1`, which writes the source `file`, making the id exact
   (parametrised cases included, since the parameters live in `name`).

   The check stays strict: a dotted id is now pinned as a REJECTION case, and a
   real pytest subprocess pins the producer contract, so a future pytest that
   drops the `file` attribute fails in the unit suite instead of silently
   producing ids nobody can verify. A workflow pin asserts every pytest lane
   passes the flag.

2. `runner.image_os` rejected `win25-vs2026`. Hosted image ids carry hyphens and
   dots; the pattern was wrong, not the rule. What must stay rejected -- an
   ABSENT image identity, the self-hosted signal -- still is, and still has its
   test. The corrected pattern gets one too.

Then the other half, which is a correction in the opposite direction: "3
qualified / 91 served-unqualified" would be read as "91 untested tools". That is
false. Those tools are driven against REAL Chrome by the E2E suite, with a
set-equality tripwire keeping the coverage complete (94 covered, 0 exempt). What
they lack is a per-tool assertion over the stdio transport.

So the contract now says that in plain words immediately under the headline
number, every served-unqualified row states the evidence that EXISTS rather than
repeating its label, and a genuinely evidence-free tool would get its own
visibly weaker row instead of hiding in the same bucket (none is in that state).
F-776 carries the same two-directional framing, and tests pin both halves.

F-777 (the `.fn`-seam hang plus CDP connection poisoning) and F-778 (the
declared-type mismatch, wire shape correct) are now written up as findings and
cross-linked from the register.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ceiling paragraph and the limitations row both described this flake as
one cosmetic install-smoke cell that passed on re-run. Run 30168575218 says
otherwise: the same "Failed to connect to browser" cold-start race took out
`transport (Linux/X64)` AND `install-smoke (wheel Linux/X64)` in one run --
and `transport (Linux/X64)` is one of the two cells that carry every
qualified stdio claim in this contract. The cookie test in that same job
spawned Chrome successfully seconds later, so it is a race, not a defect;
but a reader deciding how much a qualified claim is worth needs to know the
flake can hit the evidence the claim rests on. Understating it was the kind
of laundering this whole workstream exists to prevent.

Also state the one-attempt property the ledger enforces: every record is
bound to a single run_id + run_attempt, so re-running only the failed jobs
can never certify -- the surviving records belong to the earlier attempt and
are refused on identity. That is the fail-closed rule working as designed,
but it is operationally surprising enough that the contract should say it
out loud rather than let someone read the resulting red as a second failure.

The flake-freedom test now also pins that the transport cell is named, so a
future edit cannot quietly shrink this back to a cosmetic-cell story.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The contract is generated FROM pyproject's version, so a version bump that
lands without regenerating it leaves the contract describing a version that
no longer exists. W5's completion note called this out explicitly: regenerate
in the same commit as any bump.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…scribes

The contract was generated at 1.2.0 while its own body documents a BREAKING
change from 1.x: STEALTH_MCP_SESSION_STORAGE_CAP_GB and --session-cap-gb were
renamed with no back-compat alias (verified in settings.py:57-58 and
cli.py:461). A 1.2.0 release cannot carry a break from 1.x, and the env rename
is the dangerous half - nothing errors, the configured storage cap silently
returns to default.

W5's completion note required regenerating the contract in the same commit as
any version bump. This merges the release/v2.0.0 line so the generated header,
the tag assertion, and the breaking-change section all agree.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mechanical output of `gen_release_contract.py --write` after the 2.0.0 merge.
The header now reads 2.0.0, so it agrees with pyproject, with the tag assertion,
and with its own "Breaking change from 1.x" section. `--check` is clean.

The drift tripwire earned its keep here: it failed on the real mismatch rather
than letting a contract ship that described a version it was not generated for.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Empty commit, no tree change. d13997e's only delta vs the fully-green
5c2505a run is version metadata + sdist excludes (already verified by
d13997e's own six green install-smoke cells); the integration
(macOS/ARM64) 'Event loop is closed' failure is therefore suspected
environmental. A second run on identical code arbitrates flake vs
deterministic before any code gets touched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The contract regeneration commit (d13997e) went red on
integration (macOS/ARM64) with `Event loop is closed` at teardown,
taking release-evidence and the release-gate aggregate down with it.

Re-running the gate against a BYTE-IDENTICAL tree -- an empty commit
built from d13997e's own tree object -- returned 32/32 success,
including that cell. Same code, same workflow, same runner image, two
different conclusions. The tree is exonerated; the gate is not.

The mechanism stays undiagnosed on purpose rather than by guess: job
logs require admin rights, so only check-run annotations were readable,
and they carry the message without a traceback. The finding says so
instead of inventing a cause.

This is the SECOND distinct gate flake on record, and unlike the Linux
cold-spawn one it reddens the aggregate check itself -- the very check
a ruleset is meant to require. A required check that intermittently
fails for reasons unrelated to the change trains reviewers to re-run
until green, which is indistinguishable from training them to ignore
it. So the contract's flake-freedom disclaimer now names both flakes
rather than describing the gate as having one bad cell.

Routed to W8 (flake quarantine), whose acceptance criterion should be
"identical tree, different conclusion" -- not a retry budget. Adding a
retry here would hide a teardown bug behind exactly the second-way-to-
do-something defect the repo conventions forbid.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 2.0.0 bump moved pyproject.toml's version but left uv.lock still
recording `version = "1.2.0"` for the editable root package. Any
`uv sync` regenerates it, so the file showed up dirty in a clean
worktree -- which is how it was found.

Left alone this is the same class of defect the release campaign keeps
hitting: two files that disagree about what this thing IS, where only
one of them is the one people read. The lockfile is what actually
governs an installed environment, so a stale version there is a
metadata lie with a real consumer, not cosmetic drift.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n (F-780)

#43, #45 and #47 are red on the legacy `Browser Integration Tests
(Chrome + Xvfb)` check while main and #42 are green on the same check.
Read at face value that says FIX-B broke browser integration. It did
not, and the reason is provable without reading a single CI log.

test.yml runs `pytest -m integration --timeout=120`. The harness it
drives declares INIT=60, LIST=130, SPAWN=120, WARMUP=150 with 4 attempts
-- under a comment saying "the pytest --timeout is the outer net". Two
of those single-step bounds are LARGER than the whole job's per-test
budget, and BACKEND_READY_TIMEOUT is exactly equal to it. A 120s outer
net cannot contain a 150s inner step. The job is structurally incapable
of finishing that test; no timing luck is involved.

It only started failing at FIX-B because W1 landed the transport journey
already xfail'd (B1 was open), and an xfail costs nothing on the clock.
FIX-B C2 correctly removed the marker once B1 was fixed, so from that
commit the test actually runs -- and immediately hits the wall.

The controlled comparison is #46: it contains FIX-B transitively, runs
the SAME `-m integration` selection on the SAME OS under the release
gate's 180s budget, and is green 23/23.

Deliberately NOT fixed. W2 deletes this job, so patching a doomed file
on three in-review branches is churn that also mutates PRs under human
review. Recorded instead, because the trap is expensive: the merge queue
will show red at #43 -> #45 -> #47 until W2 lands, and someone will
otherwise go hunting for a regression that was never there.

Not added to RELEASE_CONTRACT.md on purpose -- the contract describes
what a user receives, not our merge plumbing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…(2/6)

I wrote F-779 as "observed once then not reproduced". That was wrong
within hours, and the correction matters more than the original entry.

7c65374 adds exactly one file: a markdown finding. No code, no workflow,
no test, no dependency. integration (macOS/ARM64) failed on it with the
byte-identical signature -- `Event loop is closed` twice at teardown.
A markdown file cannot break a macOS integration test, so together with
the earlier identical-tree re-run the code is now exonerated twice by
two independent methods.

Measured across six consecutive runs on this line the cell fails 2/6
(~33%), successes bracketing each failure. Since the aggregate demands
every edge green, one cell at 33% puts the HEADLINE release-gate check
red about one run in three no matter how healthy the other 31 jobs are.

That is the honest headline of this release, so the contract now says it
in those words rather than burying it in a limitations row: while F-779
is open, a green check is evidence about that run only, and a red check
is not by itself evidence about your change. That ambiguity is precisely
what a release gate exists to remove, so the "green => blindly pushable"
goal is not met -- not because the software is bad, but because the
instrument is unreliable.

Correctness of 2.0.0 is unaffected; the tree is green at 6a8fa79 and
a97c970 and the failures are provably code-independent. Whether to tag
anyway is a human call. What is no longer available is calling the gate
trustworthy while this is open.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t it

Reading the teardown path gives a specific candidate, so the finding now
points at code rather than leaving the next person with "macOS is flaky".

close_instance Phase 3 wraps asyncio.to_thread in asyncio.wait_for with
CLOSE_KILL_TIMEOUT (5s). wait_for cancels the AWAITABLE; it cannot cancel
the worker THREAD -- the code's own comment says so ("worker thread
continues in background"). So on timeout a thread survives, still holding
`browser`, after close_instance returned.

Two ways that thread yields exactly `Event loop is closed` once the loop
is gone: the executor resolving its future via call_soon_threadsafe, and
browser._process.terminate() at :238 -- nodriver's _process is an ASYNCIO
subprocess bound to that loop, so terminate()/kill() from the thread hit
_check_closed(). The second path failing twice through the retry loop is
a plausible reading of why the annotation shows the message twice.

It fits the fingerprint: intermittent (only when kill exceeds 5s),
teardown-only, code-independent (hence the markdown-only commit), and
macOS-leaning, which is already the anomalous cell per F-773.

Explicitly labelled a HYPOTHESIS with zero direct confirmation -- no Mac
was available and job logs need admin rights, so none of this was
observed. The finding says how to falsify it in one step: check whether
the traceback originates in a ThreadPoolExecutor thread. If it does not,
the section should be deleted, not defended.

No fix here. plan_RELEASE forbids src/ edits, and the honest fix is
structural (a thread that cannot be cancelled is abandoned holding
loop-bound objects) rather than widening the timeout, which would only
move the race. Routed to a FIX plan.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The rate has now read 2/6, 2/7 and 2/8 -- not because anything changed,
but because every commit documenting it adds a sample to its own
denominator. That is a genuinely self-invalidating measurement, and the
honest response is to name the property rather than keep re-editing a
number that is structurally always slightly behind.

So the finding and the contract now lead with what the drift cannot
touch: the macOS/ARM64 integration cell fails intermittently at an order
of roughly one run in four, code-independently, and each failure takes
the release-gate aggregate down with it. The exact fraction is recorded
with its as-of commit and explicitly marked live.

Also fixed two internal inconsistencies left by the earlier correction
(a "fails a third of the time" and a "red one run in three" that the
newer sample had already outdated), and noted that one-in-four is a
FLOOR for the aggregate, not the whole story -- the Linux cold-spawn
flake can redden the same check independently.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant