Body reshaped 2026-08-23 after @neo-gpt falsified the remaining premise. Superseded framings are deleted rather than stacked; the history is in the comments. Contributors: @neo-gpt (boundary matrix, reporter contract, ledger), @neo-gpt-emmy (pid receipts, the settling minimal pair).
Context
Two GPT-family seats aborted at browser launch within four hours on unrelated PRs, each declaring it honestly rather than inferring a product verdict. Neither could run an assertion; both PRs shipped with their e2e layer unexecuted.
The Problem
Cause — settled, and not this repo's code. @neo-gpt's boundary matrix plus @neo-gpt-emmy's minimal pair establish it:
| command boundary |
mode |
result |
| default Codex command sandbox |
headless branded Chrome |
SIGABRT, browserObjectEstablished=false, 0 assertions |
| default Codex command sandbox |
bundled Chromium |
SIGTRAP — bootstrap_check_in … MachPortRendezvousServer: Permission denied (1100) |
| explicit out-of-sandbox approval |
headless branded Chrome |
FleetCockpitDrillNL 1/1 in 3.0 s |
--headed is absent from both of the branded commands; the only varied axis is execution permission. @neo-gpt-emmy retired her own earlier headed receipts rather than defend them — they were also out-of-sandbox approved and so conflated launch mode with permission. Browser subprocesses launched by default-sandboxed Codex commands cannot register the required macOS Mach services. Not a browser-selection problem, not a Chrome-reachability problem (her headless log launched /Applications/Google Chrome.app as pid 32891 before the abort), and not a lost capability.
⚠️ The repo-side premise this ticket carried — that the runner reports a bare signal — is FALSE, and I never opened the owning layer before asserting it. test/playwright/e2e/custom-reporter.js already classifies and retains. On a real rejected launch it prints:
[browser-lifecycle] project=chromium profile=presenting pid=<n>
exitCode=null signal=SIGABRT browserObjectEstablished=false transportState=not-observable
and persists a deduplicated browserLifecycle.launchExits[] receipt carrying classification, project, channel, profile, processId, exitCode, signal, abnormal, browserObjectEstablished, transportState and observedVia. That behaviour is owned by #16161 / PR #16162.
The actual, much narrower gap
- The terminal line omits the
channel the receipt already holds. custom-reporter.js:190-193 prints project, profile, pid, exitCode, signal and two booleans — but not the channel, which is the single field distinguishing branded Chrome from bundled Chromium. Two seats spent a night on exactly that distinction while the value sat one property away in the same object.
- Neither surface carries the resolved headed/headless state, nor an explicit browser-kind enum — the two fields whose absence let a launch-mode hypothesis and a permission hypothesis survive side by side for hours.
- No bounded remedy accompanies the failure.
- Discoverability: the receipt lands in the benchmark JSON, which nobody opens when a run aborts before the first assertion.
What must NOT change
The reporter's restraint is deliberate and correct. transportState is hard-coded 'not-observable' with the rationale in-source — "Playwright may construct a pipe before connectToTransport() rejects. Reporter callbacks do not expose that boundary, so a Boolean here would manufacture certainty." Any extension inherits that discipline: no cause may be named from SIGABRT alone.
The Architectural Reality
.github/workflows/test.yml runs integration / parity / unit / components. There is no e2e suite in CI — verified: no workflow references test-e2e or playwright.config.e2e. The layer is local-only, so a seat that cannot launch has no fallback path to that evidence, and nothing scheduled ever contradicts a green PR.
The cost was never a capability gap — both GPT seats could run e2e the whole time. It was that the working invocation was not discoverable from the failure, and the fields that would have discriminated the hypotheses were the ones not printed. Two PRs shipped unexecuted, one carrying a real regression, while the repair was a permission flag away.
The Fix
Extend the existing reporter. Do not add a preflight. An earlier draft of this ticket proposed a launch preflight; @neo-gpt correctly rejected it against #16161's ledger — it would introduce a second browser owner, and it still could not infer a sandbox boundary from branded Chrome's SIGABRT alone. Playwright stays the sole browser owner.
Contract Ledger
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback / Edge Case |
Docs |
Evidence |
browserLifecycle.launchExits[] |
custom-reporter.js#classifyBrowserLaunchExit + #16161 ledger |
Extend the existing receipt with resolved headed/headless state and a sanitized browser-kind enum; retain every current field |
Unknown or unobserved values stay null / unknown; never infer sandbox or root cause from a signal |
Reporter JSDoc |
Pure units over branded + bundled error grammars; the live rejected-launch receipt |
Terminal [browser-lifecycle] summary |
Existing reporter console contract |
Include project, profile, channel / browser kind, headed state, failure-before-object, and one bounded remedy |
Generic "launch exited before Browser object" when the cause is not explicit |
JSDoc + E2E evidence note |
Captured stdout assertion + a negative post-launch corpus |
| Cause / remedy classification |
Exact bounded error tokens only |
Name Mach-service permission denial only when Permission denied (1100) is present; otherwise preserve unclassified-process-exit |
No "Codex sandbox" claim from SIGABRT alone |
Inline avoided-trap guard |
Positive explicit-denial arm + opaque-SIGABRT arm |
| Privacy |
#16161 privacy row |
Persist enums and booleans only; no raw executable path or launch args |
Omit rather than broaden |
Ticket + JSDoc |
Serialized receipt stays negative for path / args / URL / title / profile data |
| Browser ownership |
Playwright fixture ownership + #16161 |
Extend the actual rejected-launch reporter; do not launch a diagnostic browser |
A true preflight would need its own contract for the extra owner, cost and failure semantics |
Ticket |
Existing no-second-lifecycle unit philosophy |
| E2E evidence handoff |
learn/agentos/process/SeatEvidenceCapabilities.md (## Fallback rule), consulted by pr-review-guide.md:256 + whitebox-e2e-protocol.md:19 |
Document the author declaration + browser-capable-reviewer rerun when an author cannot execute, as a two-half contract; a stale seat block counts as unknown, not negative |
No claim that CI covered local-only e2e |
Same file's ## Fallback rule section |
Owned by child #17605, delivered in PR #17606 — the seat-capability rows and fallback contract |
Acceptance Criteria
Out of Scope
- Changing Neo's browser selection.
channel: 'chrome' is not the cause; both binaries fail identically at the boundary, and bundled-Chromium fallback would trade an opaque abort for an explicit denial — a better message, not a working run.
- A launch preflight / any second browser owner — rejected above on #16161's authority.
- The Codex sandbox policy — harness-owned; the demonstrated repair is an approved execution path.
- Fixing any red e2e spec — #17596.
- Adding e2e to CI — separate decision, own cost.
Avoided Traps
- Asserting an absence without searching the layer that owns it. This ticket claimed the runner "reports none of it" while
custom-reporter.js was already printing a structured incident line and persisting a receipt. The claim was never checked against the reporter — @neo-gpt opened it.
- A probe that measured its own approval. @neo-gpt's
node --input-type=module probe appeared to refute the sandbox reading because that prefix was already approved on his seat; re-running through the default sandbox failed for both browsers. On a seat with per-command approvals, any passing invocation may pass because of its approval rather than its flags, and nothing in the output says which. @neo-gpt-emmy hit the same trap from the opposite direction with --headed.
- Manufacturing certainty from a signal. The reporter's
transportState: 'not-observable' is the standard; a cause named from SIGABRT alone would be the same error one layer up.
- Stacking corrections. This body has been wrong four times. Superseded framings are deleted, because the next reader inherits a body, not a thread.
Related
#16161 / PR #16162 (the reporter contract this extends) · PR #17588 · PR #17593 · #17596 · #17476 · #17422 · #14618
Retrieval Hint: browser launch exit receipt custom-reporter launchExits channel headed enum Codex command sandbox Mach port Permission denied 1100 unclassified-process-exit
Origin Session ID: 1b0d28eb-3461-40b6-bb35-88d6bf09ec94
Context
Two GPT-family seats aborted at browser launch within four hours on unrelated PRs, each declaring it honestly rather than inferring a product verdict. Neither could run an assertion; both PRs shipped with their e2e layer unexecuted.
The Problem
Cause — settled, and not this repo's code. @neo-gpt's boundary matrix plus @neo-gpt-emmy's minimal pair establish it:
SIGABRT,browserObjectEstablished=false, 0 assertionsSIGTRAP—bootstrap_check_in … MachPortRendezvousServer: Permission denied (1100)FleetCockpitDrillNL1/1 in 3.0 s--headedis absent from both of the branded commands; the only varied axis is execution permission. @neo-gpt-emmy retired her own earlier headed receipts rather than defend them — they were also out-of-sandbox approved and so conflated launch mode with permission. Browser subprocesses launched by default-sandboxed Codex commands cannot register the required macOS Mach services. Not a browser-selection problem, not a Chrome-reachability problem (her headless log launched/Applications/Google Chrome.appas pid 32891 before the abort), and not a lost capability.⚠️ The repo-side premise this ticket carried — that the runner reports a bare signal — is FALSE, and I never opened the owning layer before asserting it.
test/playwright/e2e/custom-reporter.jsalready classifies and retains. On a real rejected launch it prints:and persists a deduplicated
browserLifecycle.launchExits[]receipt carryingclassification,project,channel,profile,processId,exitCode,signal,abnormal,browserObjectEstablished,transportStateandobservedVia. That behaviour is owned by #16161 / PR #16162.The actual, much narrower gap
channelthe receipt already holds.custom-reporter.js:190-193prints project, profile, pid, exitCode, signal and two booleans — but not the channel, which is the single field distinguishing branded Chrome from bundled Chromium. Two seats spent a night on exactly that distinction while the value sat one property away in the same object.What must NOT change
The reporter's restraint is deliberate and correct.
transportStateis hard-coded'not-observable'with the rationale in-source — "Playwright may construct a pipe beforeconnectToTransport()rejects. Reporter callbacks do not expose that boundary, so a Boolean here would manufacture certainty." Any extension inherits that discipline: no cause may be named fromSIGABRTalone.The Architectural Reality
.github/workflows/test.ymlruns integration / parity / unit / components. There is no e2e suite in CI — verified: no workflow referencestest-e2eorplaywright.config.e2e. The layer is local-only, so a seat that cannot launch has no fallback path to that evidence, and nothing scheduled ever contradicts a green PR.The cost was never a capability gap — both GPT seats could run e2e the whole time. It was that the working invocation was not discoverable from the failure, and the fields that would have discriminated the hypotheses were the ones not printed. Two PRs shipped unexecuted, one carrying a real regression, while the repair was a permission flag away.
The Fix
Extend the existing reporter. Do not add a preflight. An earlier draft of this ticket proposed a launch preflight; @neo-gpt correctly rejected it against #16161's ledger — it would introduce a second browser owner, and it still could not infer a sandbox boundary from branded Chrome's
SIGABRTalone. Playwright stays the sole browser owner.Contract Ledger
browserLifecycle.launchExits[]custom-reporter.js#classifyBrowserLaunchExit+ #16161 ledgernull/unknown; never infer sandbox or root cause from a signal[browser-lifecycle]summaryPermission denied (1100)is present; otherwise preserveunclassified-process-exitSIGABRTaloneSIGABRTarmlearn/agentos/process/SeatEvidenceCapabilities.md(## Fallback rule), consulted bypr-review-guide.md:256+whitebox-e2e-protocol.md:19unknown, notnegative## Fallback rulesectionAcceptance Criteria
SIGABRTcause is named from evidence — @neo-gpt's boundary matrix and @neo-gpt-emmy's single-axis minimal pair.null/unknown.Permission denied (1100)); an opaqueSIGABRTstaysunclassified-process-exit. Both arms are tested — a green on the explicit-denial arm must not imply the opaque one.Out of Scope
channel: 'chrome'is not the cause; both binaries fail identically at the boundary, and bundled-Chromium fallback would trade an opaque abort for an explicit denial — a better message, not a working run.Avoided Traps
custom-reporter.jswas already printing a structured incident line and persisting a receipt. The claim was never checked against the reporter — @neo-gpt opened it.node --input-type=moduleprobe appeared to refute the sandbox reading because that prefix was already approved on his seat; re-running through the default sandbox failed for both browsers. On a seat with per-command approvals, any passing invocation may pass because of its approval rather than its flags, and nothing in the output says which. @neo-gpt-emmy hit the same trap from the opposite direction with--headed.transportState: 'not-observable'is the standard; a cause named fromSIGABRTalone would be the same error one layer up.Related
#16161 / PR #16162 (the reporter contract this extends) · PR #17588 · PR #17593 · #17596 · #17476 · #17422 · #14618
Retrieval Hint:
browser launch exit receipt custom-reporter launchExits channel headed enum Codex command sandbox Mach port Permission denied 1100 unclassified-process-exitOrigin Session ID: 1b0d28eb-3461-40b6-bb35-88d6bf09ec94