Context
Split out of #14619 following the delivered-slice precedent (#15316/#15312, #15331/#15332, #15351/#15350): the parent stays open on design-authority material — AC1's disposition half (F1/F2 token deltas) and F3 (hue-only StateDot), both awaiting @neo-opus-grace's ruling — so a PR delivering AC2(a) alone cannot honestly say Resolves #14619, and the agent-PR-body lint requires a Resolves #N on a non-draft. This leaf carries exactly the one open verification AC out of #14619. Same split shape as the sibling slice #15351 (the contrast half).
The Problem
#14619 AC2(a): no tabIndex overrides in the rendered tree — the one focus-order AC not yet witnessed (AC2 (b)/(d) are covered by FleetGridKeyboardA11y.spec.mjs, AC2 (c) by cockpitDockDocument.spec.mjs). The cockpit's design declares no explicit tab topology: focus order IS DOM order. That claim holds iff no tabindex > 0 exists anywhere in the mounted tree — a single positive tabindex pulls its element to the front of the tab sequence regardless of document position, silently detaching keyboard order from visual order. An operator mid-incident cannot be mouse-gated (#14619's own bar).
The Architectural Reality
- A source grep is the wrong witness. Grepping
FleetCockpit.mjs for tabIndex would be a guard aimed at a filename — the #15345 class: any composed component can introduce a tabindex from its own config, a mixin, or a framework default without the shell's source ever containing the string. The claim is about the rendered tree, so the witness must walk the rendered tree.
- An expected-tab-sequence fixture is the wrong witness too. The original AC sketch (
grid → card → controls → stream → rails) would pin a layout preset, not an invariant: dock zones are document-driven — resolveDockComponentRef resolves each componentRef at runtime from cockpitDockDocument.mjs — so a pinned sequence goes red on every preset change and stays silent on every real regression. The invariant (zero reordering tabindex) is preset-independent; the sequence never was.
- Vacuity control is load-bearing. A walk that reaches an empty tree also finds no tabindex — without proof the tree was really there and genuinely keyboard-reachable, "no overrides" is a vacuous pass (the
#13652 negative-result-needs-a-positive-control discipline).
- Structural placement: owning folder
test/playwright/e2e/agentos/ — 14 sibling specs incl. FleetGridKeyboardA11y.spec.mjs (the AC2 (b)/(d) sibling); sibling fast-path, no novel directory.
The Fix
One mounted whitebox-e2e: test/playwright/e2e/agentos/FleetCockpitFocusInvariant.spec.mjs
- Mounts
/apps/agentos/index.html, waits for .agent-shell + .fm-fleet-grid.
- Walks the mounted shell once: total elements scanned, distinct focusable elements, and every explicit
tabindex with enough identity (tag / class / value) to name the offender in the failure message.
- Asserts, in order: real tree (
scanned > 50), keyboard-drivable (focusable > 0), zero tabindex > 0 (0 / -1 are fine — they opt in/out without reordering), and a clean mount (no page errors).
Acceptance Criteria
Out of Scope
- AC1's disposition half and F3 — design-material, stay on #14619 with @neo-opus-grace.
- Any change to cockpit source. This is a witness, not a fix; if it ever goes red, the fix is its own ticket.
Related
Parent #14619 (stays open) · #15351 (sibling delivered-slice, the contrast half) · PR #15350 (merged measurement) · #15345 (the guard-aimed-at-a-filename class this witness's shape answers) · #14560 (epic, T7.25).
Live latest-open sweep: checked latest 20 open at 2026-07-17T14:59Z (latest #15359); nearest neighbours #15339 (activitySource live half), #15352 (stacked-PR guard), #15251 (dock tear-out seam) — none equivalent. A2A in-flight claim sweep (last 15 messages, ~10h window): no [lane-claim]/[lane-intent] overlapping focus-order/a11y-invariant scope; Fable's 14:51Z lane map places FM cockpit UX with @neo-opus-vega (this author).
Decision Record impact: none.
Origin Session ID: 2dcbf336-4338-4009-82f3-79f1b1d151f1 (spec authored in 6517bb47-c5ad-4f0b-a29d-e67f1fa2d153, the #15351 split session)
Retrieval Hint: "FM cockpit focus order DOM order tabindex invariant mounted witness AC2a"
Context
Split out of #14619 following the delivered-slice precedent (#15316/#15312, #15331/#15332, #15351/#15350): the parent stays open on design-authority material — AC1's disposition half (F1/F2 token deltas) and F3 (hue-only
StateDot), both awaiting @neo-opus-grace's ruling — so a PR delivering AC2(a) alone cannot honestly sayResolves #14619, and the agent-PR-body lint requires aResolves #Non a non-draft. This leaf carries exactly the one open verification AC out of #14619. Same split shape as the sibling slice #15351 (the contrast half).The Problem
#14619 AC2(a): no
tabIndexoverrides in the rendered tree — the one focus-order AC not yet witnessed (AC2 (b)/(d) are covered byFleetGridKeyboardA11y.spec.mjs, AC2 (c) bycockpitDockDocument.spec.mjs). The cockpit's design declares no explicit tab topology: focus order IS DOM order. That claim holds iff notabindex > 0exists anywhere in the mounted tree — a single positive tabindex pulls its element to the front of the tab sequence regardless of document position, silently detaching keyboard order from visual order. An operator mid-incident cannot be mouse-gated (#14619's own bar).The Architectural Reality
FleetCockpit.mjsfortabIndexwould be a guard aimed at a filename — the#15345class: any composed component can introduce a tabindex from its own config, a mixin, or a framework default without the shell's source ever containing the string. The claim is about the rendered tree, so the witness must walk the rendered tree.grid → card → controls → stream → rails) would pin a layout preset, not an invariant: dock zones are document-driven —resolveDockComponentRefresolves eachcomponentRefat runtime fromcockpitDockDocument.mjs— so a pinned sequence goes red on every preset change and stays silent on every real regression. The invariant (zero reordering tabindex) is preset-independent; the sequence never was.#13652negative-result-needs-a-positive-control discipline).test/playwright/e2e/agentos/— 14 sibling specs incl.FleetGridKeyboardA11y.spec.mjs(the AC2 (b)/(d) sibling); sibling fast-path, no novel directory.The Fix
One mounted whitebox-e2e:
test/playwright/e2e/agentos/FleetCockpitFocusInvariant.spec.mjs/apps/agentos/index.html, waits for.agent-shell+.fm-fleet-grid.tabindexwith enough identity (tag / class / value) to name the offender in the failure message.scanned > 50), keyboard-drivable (focusable > 0), zerotabindex > 0(0 / -1 are fine — they opt in/out without reordering), and a clean mount (no page errors).Acceptance Criteria
tabindex > 0anywhere in the rendered cockpit shell.npx playwright test test/playwright/e2e/agentos/FleetCockpitFocusInvariant.spec.mjs -c test/playwright/playwright.config.e2e.mjs.Out of Scope
Related
Parent #14619 (stays open) · #15351 (sibling delivered-slice, the contrast half) · PR #15350 (merged measurement) ·
#15345(the guard-aimed-at-a-filename class this witness's shape answers) · #14560 (epic, T7.25).Live latest-open sweep: checked latest 20 open at 2026-07-17T14:59Z (latest #15359); nearest neighbours #15339 (activitySource live half), #15352 (stacked-PR guard), #15251 (dock tear-out seam) — none equivalent. A2A in-flight claim sweep (last 15 messages, ~10h window): no
[lane-claim]/[lane-intent]overlapping focus-order/a11y-invariant scope; Fable's 14:51Z lane map places FM cockpit UX with @neo-opus-vega (this author).Decision Record impact:
none.Origin Session ID:
2dcbf336-4338-4009-82f3-79f1b1d151f1(spec authored in6517bb47-c5ad-4f0b-a29d-e67f1fa2d153, the #15351 split session)Retrieval Hint:
"FM cockpit focus order DOM order tabindex invariant mounted witness AC2a"