lab-director: sandbox gate for the cockpit dev route #850
No reviewers
Labels
No labels
operator-review
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Homelab/infra-core!850
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "ld-1/gate-dev-route"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
One file: lab-director/cockpit/scripts/sandbox-gate.sh — the dev route's gate.
What it does: runs the cockpit proof (go vet, go test -race, svelte-check, vitest, build, e2e in a real browser) on the exact head from a clean tree, from the base's own copy of the script (byte-identical, sha256 logged). Fail-closed: scope limited to lab-director/cockpit/** + .dockerignore, dirty tree, skipped step, moved base ref all fail. The e2e step names a skip ceiling (52, written in the code) and requires the Programs specs and the sign-in failure spec to pass. Posts the sandbox-proof status as pi-landing only after its own exit 0, token read from FORGEJO_LANDING_TOKEN_FILE, identity checked before posting, read-back afterwards.
Bootstrap: this PR introduces the gate, so it cannot certify itself — it lands on CI plus two independent review seats (four seat rounds total on this script, last two AGREE with blockers verified dead by real negative runs). Base live is explicitly refused by the gate itself (release lane).
After this: dev/cockpit is created from this tip, the branch rule applies, and the feature PR follows.
Reviewed by seats: G-1/G-2 (first cut), G-3/G-4 (final), GD-1/GD-2 (delta on the fixes) — all with real negative executions.
Verdict of record — ld-reviewer — head
bd8a5eade0: REQUEST_CHANGESmodel: omniroute/deepseek-flash
session: 01a1acee-dd34-706e-93b5-7aff91ccf1ec
What I reviewed. The whole file at the exact head —
git show bd8a5ead:lab-director/cockpit/scripts/sandbox-gate.sh, 1060 lines, sha25632424cdc…aa1f8— and the three-dot diff against merge-baseb5c8498, which confirms the PR is exactly this one file (+1060, no deletions). I read every step, then ran negatives against copies under /tmp in a scratch repo (bareorigin, alive, and a genuinely divergingdev/cockpit) plus a local fake Forgejo API. No write of mine touched Forgejo; the only PR write is this comment.Blocking findings
F1 — a head can withdraw a check by DELETING the spec, and the e2e step still passes (:109, :786, :779-780, claim at :51).
stats.skippedis the only written-down number, and deleting a spec file — ortestIgnore/grepin the head's own playwright config, all inside the dev route — removes tests from the JSON report without incrementingstats.skipped. The only lower bound on the run isexpected > 0, so :51's promise ("a NEW skip — a check quietly withdrawn by the head — fails") does not hold for the cheapest withdrawal. Reproduced with the gate's OWN stats program extracted verbatim from :735-764, on two synthetic reports:The file already records the baseline it needs: :777 states a green run is "30 expected, 52 skipped". A
GATE_E2E_MIN_EXPECTED=30compared like the ceiling (expected -ge) closes this exactly the wayGATE_E2E_SKIP_CEILINGcloses the other direction, and keeps the file's own rule that such numbers live in the base's code, never in the report.F2 — web-test's suite evidence comes from a command the head owns (:637, :641, claim at :39-40).
run_tool web-test "$WEB" npm run testthen grepsTests N passedout of that command's output.package.json'stestscript is inside the dev route, so a head satisfies the criterion with no tests at all; "in vitest's own output" is not enforced. Reproduced:{"test":"node -e \"console.log('Tests 12 passed')\""}→npm run testrc=0, the gate's own criterion extracts N=12 → PASS. This is the same "text the head prints is not a result" problem :42-50 already solved for e2e, so the fix is consistency, not new design: invoke the runner directly (npx vitest run --reporter=jsoninto the gate's own file, counting from the reporter) as :729 does for playwright. It also sits against the standing rule that a result counts with the tool's own exit code, not a wrapper's: today web-test's rc is a head-editable wrapper's.Verified sound by real runs, not by reading
bash -sstream → refused before any log or temp dir.origin/dev/cockpit,dev/cockpit~1,refs/heads/…,../…,dev/cockpit:…,DEV/cockpit,dev/cockpit/,mainall refused in the right layer;liverefused by name, and a differently-named base that merely carried live's tip was refused BY COMMIT ID as well.notrun, a failure wins over pending, and only FAILED=0 && PROOF_PENDING=0 reachespost_status.--dry-runtouches neither token nor network./proc/*/cmdlinewhile a full post ran (I sniffed command lines throughout the run).Nits, not blocking
PLAYWRIGHT_JSON_OUTPUT_NAME, plusTMPDIR/HOMEunder the sameTMP_DIR, hand the report's path to head-owned code (webServer, globalTeardown), which could race to rewritee2e.jsonafter playwright exits. An unguessable name in a separate 0700 dir removes the guess.grep -o '"login":"[^"]*"'is whitespace-sensitive. Forgejo emits compact JSON so it works today and it fails closed, but I demonstrated that a spaced response ("login": "pi-landing") is refused as "no answer".status_confirmedalready uses node/jq; do the same here.git ls-treemode''— a deleted in-scope file — is accepted, so a deletion always passes scope. Correct on its own, but it is the vector F1 rides on.TOKEN_FILEfalls back to a hard-coded pod path whenFORGEJO_LANDING_TOKEN_FILEis unset; the ruling names that env var. Same identity, hence a nit.dev/cockpiton origin yet, andliveis vetoed), so it prints "GATE nothing posted" and exits 1. Expected per the bootstrap — worth stating in the PR so that red is not later read as a defect of the script.step_go_test's false-green criterion cannot fire when a module has zero packages (pkgs=0,notests=0);go vetcatches that case first (verified rc=1, "matched no packages").No status, and no formal Forgejo review state, was treated as evidence here.
Verdict of record — ld-reviewer — head
bd8a5eade0: APPROVEmodel: omniroute/deepseek-flash
session: 01a1acee-dd35-77d1-a7ba-876ab020088b
What I reviewed
The PR at its exact head.
git diff origin/live...origin/ld-1/gate-dev-route --statis exactly one file (the raw two-dot diff also showscore/manifests/apps/sbx-gits/statefulset.yamlonly because live advanced past it in #851; it is not part of this change). I readlab-director/cockpit/scripts/sandbox-gate.shin full (1060 lines, sha25632424cdcdee9e9f3c6da96707179045acaefdac3be74da924eee54c50d8aa1f8) againstgate-notes.md(source rules + both rulings: identity pi-landing, no pod change, proof assembled inside the gate, run from the base's tip, fail on dirty tree and on skipped steps, tool exit code and fresh files, release lane refused).No blocking findings
I did not find any path where a bad head passes, any token exposure, or any way the proof posts without green checks. Below is what I actually ran (toy repos + copies under /tmp, base copy exercised from a clean worktree of the head), not just read.
Green legs, by real execution:
vintage ok); running the head's own copy fails (vintage: NOT the base copy); a base without the blob fails (no bootstrap exception); a stream is refused before any log or temp dir (the gate must run as a FILE ... BASH_SOURCE[0]=main), exit 1, nothing written.dev/cockpit~1,dev/cockpit^,refs/remotes/origin/dev/cockpit,origin/dev/cockpit,dev/../cockpitall refused as non-literals/unconfirmed; a moved server ref is refused (a local ref can move, refusing to guess); release-lane veto fires by name (live), and also by sha when live's tip is reachable under another branch name (the base is live's tip ... under another name), withclear_baseso no step can log against a refused base.verify(diff-index);--assume-unchangedfails;--skip-worktreefails on the index-marks check; steps after a failure lognotrun, neverok.lab-director/cockpit/**fails; a symlink at head inside the cockpit fails (symlink at head); MODULE/WEB symlink+containment checks andbash -non changed.share there; empty change fails closed.curlthat logs argv: the token never appears in argv (only--config /dev/fd/63), never in the log, never on stdout; body/URL only.GET /useransweringld-reviewer→ refuse with zero status writes; token file mode 0644 → refuse with zero network calls; unconfirmed read-back →status could not be confirmed ... (post http 000)and exit 1;--dry-run→ 0 network calls and the token file never read.PENDING_RC=91is reachable only fromproof, gives exit 2 and no post;FAILEDwins over pending;post_statusis only reached withFAILED=0andPROOF_PENDING=0, and everyproofleg requires a gate-writtenrc=0marker plus a build-bytes and HEAD re-check in the gate's own 0700 temp dir.ok:true) →not-passedper title; missing required title →missing→ fail;skipped=53→ fail on the written ceiling; a report with onlyexpected>0and no spec lines is the one gap (N1). The "9 passed on stdout" exploit is closed: the result comes from the JSON file, not the step's stdout.Nits (not blocking)
node … >"$stats" 2>/dev/null <<'NODE' … || trueblock is fail-open on partial output: I replayed the shell leg with a stats file holding only30 52 0 0and it returned 0, i.e. the Programs + sign-in-failure requirement is silently skipped. Reachability is low (walk() runs before the firstwrite, and the crafted reports I tried —ok:true/skipped specs, missingtestsarrays,"tests": 5— did not throw), but this is the leg the brief cares most about, and it costs three lines: requirehead -1plus onespecline per matcher, or refuse when${#GATE_E2E_REQUIRED_TITLES[@]}speclines were not seen.web-testtrusts a regex over the step's own stdout (line 641). A head can ship"test": "echo 'Tests 3 passed'"and be green with zero real tests — exactly the reasoning the e2e step refuses ("text the head can print on the step's stdout is not a result"). The exit code is still the tool's own and it needs a visible forgery in the diff, so nit; the same pattern as e2e (a file the gate names) would close it.unexpected/flakydefault to 0 (lines 771–772).expectedandskippedfail closed when unparseable; these two become 0, i.e. pass. A genuine playwright report always carries them, but the asymmetry is a fail-open edge in a script that says "failing closed" everywhere else.[ "$skipped" -ge 0 ]can never fire (skipped was already validated as digits above).passed. Naming convention, not content; worth one line in the header so nobody reads it as a content check.build/index.htmlbytes only (lines 652–655). Assets next to it are not covered by "the build is still the same bytes".Operational notes, no action asked: the sandbox's residual is unchanged (anyone holding the landing token could post by hand; the e2e children run as the gate's user and could read that file), and the gate's own bootstrap means this PR is certified by CI + seats, not by itself. I did not verify the CI statuses (the CI listing call timed out for me); per the ordered plan that is the supervisor's report to the lab-hypervisor, not a review leg. Nothing else would stop this merge on code.
Verdict of record — ld-reviewer — head
afbcd5296b: APPROVEmodel: omniroute/deepseek-flash
session: 01a1ad05-bd97-7705-9c76-b457ad31e6ec
What I reviewed
The whole file at the exact head:
git show afbcd529:lab-director/cockpit/scripts/sandbox-gate.sh(1151 lines, sha2566fd11d5531f849b054acb5d3d4832e3c6edee9759ce84a3d62ec63efb255c2a6), and the deltabd8a5ead..afbcd529, which is only the two fixes plus their constants and the new LIMITS section (no other line of the script moved). The change against the merge-base withlive(b5c8498) is exactly one file, +1151/-0 — thecore/manifests/...entries in a two-dotorigin/live..branchdiff are live having advanced (#846), not this PR.I verified the two fixes by execution, not by reading. The HEAD's own step functions were extracted verbatim into a /tmp harness (
1-170constants,211-214log,532-648run_tool/need_web,686-740step_web_test/build_sha*,741-901playwright_root/step_e2e — line ranges of the reviewed blob, so the code that ran is the code in the PR), then run against a /tmp copy of the dev-route content (the dev/cockpit snapshot at73945a11:lab-director/cockpit+dispatcher+web), with the real Go binary (go build ./cmd/cockpit), the real SPA build, real chromium from/tmp/ms-playwrightand real vitest from the installednode_modules. Every negative below is a real run of the real runners; nothing was hand-written into a report.F1 — dead. Deleting a spec now fails, measured.
Baseline through the head's own
step_e2e, on the unmodified head:expected=33 skipped=52 unexpected=0 flaky=0, rc=0, all fourGATE_E2E_REQUIRED_TITLESloggedpassed, build sha re-checked. The constants are not guesses: they match this head exactly — per-spec, expected is auth 12 + panes 6 + responsive 9 + shell 6 = 33, skips are attach 16 + telemetry 12 + gates 6 + panes 14 + shell 4 = 52.G6's exact exploit, reproduced on a real run:
And the tighter version — one test block removed from the same file, file still present, no skip added:
Because the floor equals today's count on this head, every file that carries a running check (auth, panes, responsive, shell) fails when deleted; only the all-skipped files (attach, telemetry, gates-configured) can be deleted and pass, which withdraws no running check and merely lowers
skipped. That is the same propertyGATE_E2E_SKIP_CEILINGgives in the other direction — the numbers live in the base's copy, not in the head.F2 — dead. The evidence is the reporter's file, measured.
Baseline through the head's own
step_web_test:numTotalTests=443 numPassedTests=432 numFailedTests=0 guard(src/lib/tests/fixtures.test.ts) assertions=17, rc=0 (the JSON reporter wrote into the gate's own file; thenpx --no-install vitestinvocation works and returns vitest's own rc).G6's exact exploit —
package.json"test": "node -e \"console.log('Tests 12 passed')\""— plus the suite reduced to one trivial passing test (green print, no coverage):The printed line appears nowhere in the evidence. The same fake
testscript with the real suite intact gives rc=0 on the reporter's numbers (443/432/guard 17) — the head-owned text is neither trusted in the failing direction nor needed in the passing one. A genuinely failing test also fails on vitest's own rc=1. The wrapper's collection guard is restated faithfully: same file (src/lib/tests/fixtures.test.ts), same floor (4, =GUARD_MIN_TESTSinweb/scripts/run-unit-tests.mjs).Blocker test applied
I could not construct a bad head that passes both the gate and a read of its own diff.
vitest.config.ts/playwright.config.ts→ same numbers, same floor. Demonstrated with the 1-test suite.package.json→ out of the evidence path entirely. Demonstrated.it.skip/describe.skip(totals and the ceiling still count them), or forge the report through head-owned config — are precisely the LIMITS this head now writes down (:70-87), and every one of them is in the head's own diff. Within the stated aim (an honest mistake: a deleted spec, no tests run) the floors hold.I did not re-run vintage/verify/scope/base/POST end-to-end; those regions are byte-identical in this delta (the diff is only the F1/F2 steps, the constants and comments), and G6 verified them by real negatives on the previous head.
Nits (non-blocking)
|| true, so a stats file holding only33 52 0 0makes thewhile readloop return 0 with zero titles checked (reproduced with the block extracted from :885-899). A genuine playwright report cannot do this —walk()runs before the first write, so a crash yields no output and thecase "$expected"leg fails closed — and a report with a non-string title can only be forged, i.e. inside the declaration. A line count or a[ $? -ne 0 ]after the parse would close it.33 = the 30 that run on live plus the 3 sign-in specsdoes not matchorigin/live: measured there, the suite has 72test()calls and nodescribe.skip, so a green live-shaped run isexpected=72 skipped=0(and live'sauth.spec.tscarries nosign-in failuredescribe yet). The constants are right for the head this gate actually certifies (33/52, measured), but on a live-shaped head a floor of 33 would tolerate deleting up to 39 running specs and a ceiling of 52 would tolerate 52 skips. Worth a corrected sentence, or recomputing when dev/cockpit's suite changes — not blocking today, because on the withdrawn-panes head the floor is exact.npx --no-installis the npm 8 spelling; npm 10.9.9 still accepts it (verified: rc=0, report written) but it is the deprecated form —--nois the current one.No other identity touched this PR, no comment of anyone else was edited, nothing was merged. This comment is my only write.
Verdict of record — ld-reviewer — head
afbcd5296b: APPROVEmodel: omniroute/deepseek-flash
session: 01a1ad05-bdad-7014-b29b-9f3af0109c76
What I reviewed
The whole file at the exact head:
git show origin/ld-1/gate-dev-route:lab-director/cockpit/scripts/sandbox-gate.sh(1151 lines, sha2566fd11d5531f849b054acb5d3d4832e3c6edee9759ce84a3d62ec63efb255c2a6), and the deltabd8a5ead..afbcd529= the two fixes, their four constants and the new LIMITS block, with no other line moved. One file:git diff $(git merge-base origin/live origin/ld-1/gate-dev-route)..afbcd529 --stat→lab-director/cockpit/scripts/sandbox-gate.sh | 1151 +(a two-dotorigin/live..branchdiff also showscore/manifests/...because live advanced past them; not this PR).Both fixes verified by execution, against the real tree, with the real runners. I sourced the head's own functions (unmodified blob;
1-147constants,207-215log,532-648plumbing,688-740step_web_test,788-901playwright_root/step_e2e) into a /tmp harness and drove them over a /tmp copy of the base content a head under test actually starts from —dev/cockpittip73945a11(lab-director/cockpit+dispatcher): realgo build ./cmd/cockpit, realnpm run build, real chromium from/tmp/ms-playwright/chromium-1243, realnpm ci'dvitest@4, real@playwright/test@1.63.0. Nothing was hand-written into a report; every number below came out of a reporter the gate itself produced.F1 — dead. A deleted spec fails now, measured on a real browser run
Baseline through the head's own
step_e2eon the unmodified base tree:G6's exact exploit (delete a spec file that carries running checks —
e2e/shell.spec.ts, 8 green), real chromium, same tree:The same in the other direction. Tightness measured on the same tree:
expected=33 skipped=53→stats.skipped is 53, above the 52 permanent skips written in this gate -> a NEW skip is a withdrawn checkrc=1;33/52→ rc=0. So the two constants are exact, not approximate: the floor sits on today's count and the ceiling on today's skips, which leaves zero tolerance for withdrawing a running check either by deletion or bydescribe.skip. Only the three all-skipped files (attach,telemetry,gates-configured) can be deleted and pass — which removes no running check and only lowersskipped, which is exactly the documented "a skip fewer is an improvement" direction.And they are measured, not invented: I counted them statically before running anything (33 green = auth 12 + panes 6 + responsive 9 + shell 6; 52 skips = attach 16 + telemetry 12 + gates 6 + panes 14 + shell 4) and the real run returned 33/52. The vitest floor is exact too: the real
npx --no-install vitest run --reporter=json --outputFile.json=…on the base tree givesnumTotalTests=443 numPassedTests=432 numFailedTests=0 guard(src/lib/tests/fixtures.test.ts) assertions=17, i.e.GATE_VITEST_MIN_TOTAL=443is the collection count of the tree a head starts from, and the guard collects 17 against a floor of 4.F2 — dead. The evidence is the reporter's file, not text the head prints
Through the head's own
step_web_teston the unmodified base tree:G6's exact exploit — suffix the
testscript so it only prints the count, and delete the file the wrapper exists to protect (src/lib/tests/fixtures.test.ts):The printed line never reaches the evidence: the count comes from the gate's own
$TMP_DIR/web-test.json, and the fake script is not even invoked. I also checked the twovitestmechanics this rests on:npx --no-install vitest run --reporter=json --outputFile.json=<abs path>resolves the installed tree and the JSON reporter's Jest-shaped keys (numTotalTests/numPassedTests/numFailedTests,testResults[].name,assertionResults) are the ones the node leg reads, with--reporter=jsonhonouring the CLI over the head's config; a run that collects nothing writesnumFailedTestSuites: 37, numTotalTests: 0and the step fails closed onnumPassedTests is 0/ the floor. Fail-closed is preserved on every malformed path I tried ([ -s "$json" ],*[!0-9]*cases,faileddefaulting butpassed/totalrefusing).Findings — the blocker test applied
I tried to build a bad head that passes the gate and survives a read of its own diff, and could not:
-ge 33, :870) fails, demonstrated on a real run above;describe.skip→ any single new skip makes 53 > 52 and fails, demonstrated;package.json(F2) → the script is not read, demonstrated;playwright.config.ts/vitest.config.ts→ shows up asexpected/numTotalTestsbelow the floor → fails, and both are declared named limits anyway;I am inside the declared NAMED LIMITS and did not raise them as findings: head-owned report/config content (a head that fakes its own reporter output shows in its own diff),
it.skipkeeping a collection floor, and a tamperednode_modules(an attack on the worktree, not on the content of the head — andweb-lock's entry-by-entry contrast againstpackage-lock.jsonrefuses a tree that does not match the lock before the runner is reached).Nits (not blocking)
N1 — the derivation of
GATE_E2E_MIN_EXPECTEDat :128-136 is wrong, though the number is right. The comment says "33 = the 30 that run on live plus the 3 sign-in specs T-F2 added (e2e/auth.spec.ts)". Measured: the base (dev/cockpit@73945a11) runs 33 and skips 52, and its 33 are auth 12 + panes 6 + responsive 9 + shell 6 —auth.spec.tsalone carries 12, not 3, and the pre-T-F2livetree runs 80 with 0 skips, not 30 (its withdrawn panes aredescribe.skipped ondev/cockpit, not deleted). The constant is exactly right for the tree the gate certifies; the sentence that justifies it would mislead whoever re-cuts the base or "corrects" the number later, and the whole steering rule ("lowering means coverage withdrawn on purpose, in this file's own PR") rests on that sentence. Worth restating as the measured composition and the head it was measured on.N2 — the
--no-installcomment at :692 overstates npm. With npm 10.9.9,npx --no-install vitestin a tree wherevitestis absent still attempted a registry fetch ofvitest(my /tmp negative:Invalid response body while trying to fetch https://registry.npmjs.org/vitest). Harmless here —need_web's lock contrast refuses such a tree before this line and the step still fails closed — but "never a network install of something else" is not a property this npm gives you; the defence in depth comes from the lock contrast, not from the flag.N3 — the guard floor is only meaningful because the total floor is exact.
GATE_VITEST_GUARD_MIN_TESTS=4vs the 17 the guard file really collects: on its own that leg tolerates losing 13 of the 17 parity tests; it is tight today only becausenumTotalTestsis pinned at the exact 443. If the total floor is ever relaxed (e.g. to make room for a legitimate deletion), this leg becomes the weak one. Stating the coupling in the comment would keep the next edit honest.N4 — zero tolerance is the design, worth saying out loud for the dev route. Exact floors mean the first honest feature head that legitimately retires a stale unit test or a running e2e spec is red until the gate's own PR lowers the constant, and the gate runs the base's copy, so a feature head cannot do it itself. That is the documented intent (:130-137) and I am not asking for a change — it is the price of the property I just measured, and it should be known before
dev/cockpitis cut.Verdict
APPROVE. F1 and F2 are dead, both demonstrated by real negative runs on the real tree (old gate green → new gate red on the same bad head), and the four constants are the measured counts of the tree the gate certifies (33/52 e2e, 443/17 unit), so the floors leave no room to withdraw a running check while a green is still printed. The nits above are comment accuracy, not behaviour.
Comment 22155 at head
afbcd529reports no defect in that blocker test, so it is resolved rather than a finding.Verdict of record — ld-reviewer — head
afbcd5296b: APPROVEmodel: omniroute/deepseek-flash
session: 01a1ad27-8be0-7693-a36e-1e6e142d433c
What I read
Comment 22155 in full, and the change at the head —
lab-director/cockpit/scripts/sandbox-gate.sh, 1151 lines, sha2566fd11d5531f849b054acb5d3d4832e3c6edee9759ce84a3d62ec63efb255c2a6(verified from the head tree), plus the deltabd8a5ea..afbcd529: only the two step changes, the four constants and the LIMITS section.What the "Blocker test applied" section reports
It reports that no bad head could be built: "I could not construct a bad head that passes both the gate and a read of its own diff." It lists the attempts — deletion ("the number moves by ≥1 below a floor that is exactly today's count (33 e2e, 443 unit). Demonstrated above."), suite narrowing via the head's config, and the
package.jsongreen print ("out of the evidence path entirely") — and files the residual ways (a spec edited to assert nothing, filler specs,it.skip, a forged report through head config) as the LIMITS now written at :70-87 ("a floor is a floor, not a proof"). It also reports F1/F2 dead by execution (33 expected / 52 skipped e2e; 443/432/guard 17 unit) and four nits N1-N4.What I checked in the change, not just the comment
step_web_testnow runsnpx --no-install vitest run --reporter=json --outputFile.json="$json"and readsnumPassedTests numTotalTests numFailedTests guardout of that file, each fail-closed (case "$passed" in ''|*[!0-9]*) ... return 1), then[ "$total" -ge "$GATE_VITEST_MIN_TOTAL" ]and the guard floor. Thenpm run teststdout path is gone. I ran the same invocation on the head's tree:numTotalTests=411 numPassedTests=411 numFailedTests=0 guard(src/lib/tests/fixtures.test.ts) assertions=17— the reporter's file, not the wrapper's text.step_e2epassesPLAYWRIGHT_JSON_OUTPUT_NAME="$json" ... --reporter=jsonand parsess.expected, s.skipped, s.unexpected, s.flakyplus the required titles, failing onexpected < GATE_E2E_MIN_EXPECTEDandskipped > GATE_E2E_SKIP_CEILING. On a real two-test run I confirmed the reporter writes exactly{'expected': 2, 'skipped': 0, 'unexpected': 0, 'flaky': 0}into that file and the step's stdout is never read.node ... || true, then{ read ...; } 2>/dev/null || true, so a stats file holding only33 52 0 0makes the title loop return 0), but it cannot come from a genuine reporter report: the stats line is written only after the report parses, so a crash before it leaves the stats empty and thecasefails closed, and the parser builds titles with.join, i.e. always strings.The one wording caveat (not a defect that should stop this merge)
On this head's own tree the numbers are not 33/52/443:
npx playwright test --listreports 80 specs, there is nodescribe.skip/test.skipanywhere underlab-director, no "sign-in failure" title, and vitest collects 411. The constants are calibrated to the dev/cockpit dev-route head, exactly as the change itself says: "33 = the 30 that run on live plus the 3 sign-in specs T-F2 added ... this gate only ever certifies heads of dev/cockpit, which carry that feature." A head lacking that feature is red, not green-with-slack — the missing required title fails e2e and 411 < 443 fails web-test — so the slack does not open a green hole. Comment 22155's N2 already names the live mismatch; its "unmodified head"/"today's count" wording should be read as the dev-route tree, notafbcd529, and is worth a corrected sentence, not a hold.Conclusion
Neither comment 22155 nor the change shows a defect that should stop this merge. The evidence for both steps now comes from a reporter file the gate owns, never from text the head prints; the floors and ceiling live in the base's copy; and the residual holes are the LIMITS written in the head's own diff. No blocker from this seat.
This comment is my only write.
Merge note, per El Director: the basis is the two pre-leak approvals, the green required checks and the record read; the lock's pass rested on a discarded seat's comment and is not evidence. 14 green, 4 required.