Resolves #14899
Related: #13015 (parent epic) · #14611 (B4/C2 honest-state contract, preserved) · #14802 (registry↔identity producer, out of scope) · #14868 (loadActivity fail-closed sibling)
Rebuilds the FM cockpit fleet view on Neo's data primitives instead of against them: one AgentOS.store.FleetRoster Store (singleton) of AgentOS.model.FleetAgent records is now the roster SSOT — the plain-array agents_ path, the hand-mapped fixture flow, and all seven-per-fleet per-card state.Provider instances are gone. Cards render from their record (applyRecord, in-place reference updates); the grid routes the store's reactivity (load re-derives, recordChange re-ranks on session-state change and updates the one affected card in place otherwise); the C2 lifecycle adapter writes pendingAction/controlReason via record.set(), so control state re-renders through the same store seam — never optimistic. CSS-in-JS is fully killed across the fleet primitives (StateDot, HealthSwatch, EventChip, FamilyRail) via class→token resolvers (stateClass/kindClass/familyClass) bound in the component SCSS, and AGENTS.md gains the operator-directed app-work required-context gate.
Evidence: L2 (live dev-server mount + computed-style/interaction probes + local unit runs; sandbox ceiling = local runtime) → L2 required (AC 8 mount-verified at live scale; all other ACs covered by unit/static L1). No residuals.
Deltas from ticket
- Anchor drift: the ticket's
FleetCockpit.mjs:168/:194 loadRoster anchors described the abandoned feat/fm-cockpit-live-roster branch; dev reality was FIXTURE_ROSTER-only. loadRoster() is implemented fresh on the Store path mirroring loadActivity's fail-closed routing: first wired payload populates the store (replacing the sample seed), later payloads merge onto records (record.set(row) per known agentId, add for a joiner), wired-but-EMPTY / not-wired / absent / thrown keep the last-known roster.
- keyProperty shadow (framework observation):
collection.Base defaults keyProperty: 'id' (truthy), so Store.getKeyProperty()'s this.keyProperty || this.model.keyProperty fallback can never reach a model-level key. FleetRoster mirrors keyProperty: 'agentId' at store level with an explanatory comment. Left un-ticketed per this ticket's own "no new tickets" AC — surfacing here for the swarm to route.
- CSS-in-JS audit widened per the ticket's "full audit required" clause: beyond
StateDot.style['--fm-dot'], the sweep found HealthSwatch (vdom node style), EventChip (--fm-chip) and FamilyRail (--fm-rail) — all four converted to the uniform class→token idiom with SCSS bindings per component.
- Dock blueprint reshaped:
fleetCardFactory emits record: {field bag} instead of stateProvider: {data} (a restored card would otherwise re-mint a provider). AgentCard accepts a live record or a plain snapshot bag through the same applyRecord path; the factory is spec-only-consumed today.
- Honest sample label:
FleetGrid.adapterState gains 'sample' — the seeded fixture renders "sample roster" instead of passing as live.
- AGENTS.md byte budget: the file sat 9 bytes under the 24KB harness cap, so the gate addition is paired with semantics-preserving compressions (net result: 26 bytes below the pre-PR size, 35 below the cap) — see slot rationale.
Substrate slot rationale (AGENTS.md)
- Added: the app-work context gate as an extension of the existing
§edge_case_triggers "Authoring Discipline" row (no new row/section). 3-Axis: trigger-frequency = every apps/** writing/reviewing turn; failure-severity = high (the operator-verdict full-rejection class this ticket cleans up — the anti-pattern shipped twice past review); enforceability = discipline-only today, MACHINE-ENFORCEABLE-CANDIDATE. Retirement trigger stated in-row: retire once a mechanical apps/** data-path/style lint enforces it.
- Modified (byte-budget enablers, semantics preserved, Atlas detail verified present before compressing): critical gate 6 tightened; gate 7's Maintainer-Polish sentence compressed to its
pull-request-workflow.md §10 pointer (gate enumeration verified there); gate 8's release-mutation sentence tightened; §pre_commit_gates Gates 1–2 tightened; the Skill-Adherence pre-flight tail tightened; §edge_case_triggers Wake/Heartbeat + Ticket-Creation-Freshness rows tightened. File: 24,567 → 24,541 bytes (cap 24,576).
Test Evidence
- Local unit runs (canonical unit config minus its Chroma webServer — which cannot start in this clone —
--workers=1 matching CI): 75 passed across the 11 fleet spec files, plus cockpitDockDocument.spec.mjs 5 passed. The 3 remaining failures are a pre-existing cross-file worker-reuse transient: reproduced IDENTICALLY on origin/dev in a clean worktree under the same runner (eventChip:27, fleetGrid below-threshold, healthSwatch:42 — dev: 57 passed/3 failed; this branch: 75 passed/3 failed, +13 net-new tests). CI's canonical runner passes these specs on dev.
- Direct node import probes:
FleetRoster seeds 7 records keyed agentId; record.set() fires recordChange with exact field arrays; no-op sets suppressed by the deep-equal guard; controlReason object fields survive typed parsing; store.add returns records.
- Live mount (AC 8): dev-server
apps/agentos#/fleet — zero console errors; header "FLEET · 7 AGENTS sample roster"; health legend 5 working / 1 idle / 0 wedged / 0 rate-limited / 1 benched matches the record seed; tiers ranked online (agentId-sorted) → idle → benched with the benched card showing only the state-reachable ▶.
- Class→token colors (AC 3, live): after
build-themes, computed styles resolve fm-state-ok → rgb(52,211,153), fm-state-idle → rgb(245,181,68), chip fm-kind-pr/fm-kind-a2a and rails fm-family-claude/fm-family-gpt to distinct token colors; zero inline --fm-* styles anywhere in the cockpit DOM (querySelectorAll('[style*="--fm"]') → 0).
- Live control round-trip (AC 5): clicking card controls with the dev bridge's endpoint absent renders "⚠ rejected: Failed to fetch" per card — the full chain
button → controller (record reads) → lifecycleIntent → cockpit controller → C2 adapter → record.set → store recordChange → in-place applyRecord, retry open, no optimistic success.
- AC 3 grep evidence:
rg -n "\.style\s*=|\.style\[|style:\s*\{" apps/agentos/view/fleet/*.mjs → zero code hits (comment mentions only).
node --check green on all touched .mjs; check-block-alignment green; npm run agent-preflight -- --no-fix green with this body.
Post-Merge Validation
Commits
- 4018ca323 — the full rebuild: FleetAgent model + FleetRoster store, record-driven card/grid/cockpit, C2 adapter record seam, CSS-in-JS kill + SCSS class→token bindings, 9 spec rewrites (+13 net-new tests), AGENTS.md app-work gate with paired compressions.
Authored by Vega (Claude Fable 5, Claude Code). Session d2fbbdb4-404b-47e1-bbb3-1b9e0330894b.
Resolves #14899
Related: #13015 (parent epic) · #14611 (B4/C2 honest-state contract, preserved) · #14802 (registry↔identity producer, out of scope) · #14868 (
loadActivityfail-closed sibling)Rebuilds the FM cockpit fleet view on Neo's data primitives instead of against them: one
AgentOS.store.FleetRosterStore (singleton) ofAgentOS.model.FleetAgentrecords is now the roster SSOT — the plain-arrayagents_path, the hand-mapped fixture flow, and all seven-per-fleet per-cardstate.Providerinstances are gone. Cards render from their record (applyRecord, in-place reference updates); the grid routes the store's reactivity (loadre-derives,recordChangere-ranks on session-statechange and updates the one affected card in place otherwise); the C2 lifecycle adapter writespendingAction/controlReasonviarecord.set(), so control state re-renders through the same store seam — never optimistic. CSS-in-JS is fully killed across the fleet primitives (StateDot, HealthSwatch, EventChip, FamilyRail) via class→token resolvers (stateClass/kindClass/familyClass) bound in the component SCSS, andAGENTS.mdgains the operator-directed app-work required-context gate.Evidence: L2 (live dev-server mount + computed-style/interaction probes + local unit runs; sandbox ceiling = local runtime) → L2 required (AC 8 mount-verified at live scale; all other ACs covered by unit/static L1). No residuals.
Deltas from ticket
FleetCockpit.mjs:168/:194loadRosteranchors described the abandonedfeat/fm-cockpit-live-rosterbranch; dev reality wasFIXTURE_ROSTER-only.loadRoster()is implemented fresh on the Store path mirroringloadActivity's fail-closed routing: first wired payload populates the store (replacing the sample seed), later payloads merge onto records (record.set(row)per knownagentId,addfor a joiner), wired-but-EMPTY / not-wired / absent / thrown keep the last-known roster.collection.BasedefaultskeyProperty: 'id'(truthy), soStore.getKeyProperty()'sthis.keyProperty || this.model.keyPropertyfallback can never reach a model-level key.FleetRostermirrorskeyProperty: 'agentId'at store level with an explanatory comment. Left un-ticketed per this ticket's own "no new tickets" AC — surfacing here for the swarm to route.StateDot.style['--fm-dot'], the sweep foundHealthSwatch(vdom node style),EventChip(--fm-chip) andFamilyRail(--fm-rail) — all four converted to the uniform class→token idiom with SCSS bindings per component.fleetCardFactoryemitsrecord: {field bag}instead ofstateProvider: {data}(a restored card would otherwise re-mint a provider). AgentCard accepts a live record or a plain snapshot bag through the sameapplyRecordpath; the factory is spec-only-consumed today.FleetGrid.adapterStategains'sample'— the seeded fixture renders "sample roster" instead of passing as live.Substrate slot rationale (AGENTS.md)
§edge_case_triggers"Authoring Discipline" row (no new row/section). 3-Axis: trigger-frequency = everyapps/**writing/reviewing turn; failure-severity = high (the operator-verdict full-rejection class this ticket cleans up — the anti-pattern shipped twice past review); enforceability = discipline-only today, MACHINE-ENFORCEABLE-CANDIDATE. Retirement trigger stated in-row: retire once a mechanicalapps/**data-path/style lint enforces it.pull-request-workflow.md §10pointer (gate enumeration verified there); gate 8's release-mutation sentence tightened;§pre_commit_gatesGates 1–2 tightened; the Skill-Adherence pre-flight tail tightened;§edge_case_triggersWake/Heartbeat + Ticket-Creation-Freshness rows tightened. File: 24,567 → 24,541 bytes (cap 24,576).Test Evidence
--workers=1matching CI): 75 passed across the 11 fleet spec files, pluscockpitDockDocument.spec.mjs5 passed. The 3 remaining failures are a pre-existing cross-file worker-reuse transient: reproduced IDENTICALLY onorigin/devin a clean worktree under the same runner (eventChip:27,fleetGridbelow-threshold,healthSwatch:42— dev: 57 passed/3 failed; this branch: 75 passed/3 failed, +13 net-new tests). CI's canonical runner passes these specs on dev.FleetRosterseeds 7 records keyedagentId;record.set()firesrecordChangewith exact field arrays; no-op sets suppressed by the deep-equal guard;controlReasonobject fields survive typed parsing;store.addreturns records.apps/agentos#/fleet— zero console errors; header "FLEET · 7 AGENTS sample roster"; health legend 5 working / 1 idle / 0 wedged / 0 rate-limited / 1 benched matches the record seed; tiers ranked online (agentId-sorted) → idle → benched with the benched card showing only the state-reachable ▶.build-themes, computed styles resolvefm-state-ok→rgb(52,211,153),fm-state-idle→rgb(245,181,68), chipfm-kind-pr/fm-kind-a2aand railsfm-family-claude/fm-family-gptto distinct token colors; zero inline--fm-*styles anywhere in the cockpit DOM (querySelectorAll('[style*="--fm"]')→ 0).button → controller (record reads) → lifecycleIntent → cockpit controller → C2 adapter → record.set → store recordChange → in-place applyRecord, retry open, no optimistic success.rg -n "\.style\s*=|\.style\[|style:\s*\{" apps/agentos/view/fleet/*.mjs→ zero code hits (comment mentions only).node --checkgreen on all touched.mjs;check-block-alignmentgreen;npm run agent-preflight -- --no-fixgreen with this body.Post-Merge Validation
FleetRosterwith class→token colors + the "sample roster" label after its theme build.fleetRosterbridge payload (the #14802 producer) populates the store and flips the gridlivethroughloadRoster's first-wire path.Commits
Authored by Vega (Claude Fable 5, Claude Code). Session d2fbbdb4-404b-47e1-bbb3-1b9e0330894b.