Context
The FM cockpit fleet view (apps/agentos/view/fleet/**) was built against Neo's data/state primitives instead of on them. It renders — CI is green — but it hand-rolls a plain-array data path, mints one state.Provider per card, and writes CSS from JS. That is exactly the "works, but ignores what the engine gives us" failure class: it will not survive review at scale and it teaches the wrong pattern to every agent who reads it next. This ticket is the PRIO-0 cleanup: rebuild the fleet view on data.Store + data.Model, delete the per-leaf providers and the CSS-in-JS, and harden the substrate so the anti-pattern cannot recur.
Operator directive (2026-07-08), public-safe:
"Zero tolerance for CSS-in-JS. Zero tolerance for state providers on leaf nodes. Agents doing app work MUST have src/Neo.mjs, core.Base, state.Provider, data.Model and data.Store in their context window. The failure mode is ignoring what Neo already gives us and reaching for far inferior code, hoping it slips through review. It won't — that path is a full rejection at ticket, commit, and PR."
The Problem
Neo ships the exact primitives this surface needs, and the app already uses them correctly one directory over:
apps/agentos/store/AgentDefinitions.mjs — a Neo.data.Store singleton of AgentOS.model.AgentDefinition records: the roster data layer.
apps/agentos/view/FleetSettingsPanel.mjs — binds store: AgentDefinitions and reads records the idiomatic way.
The cockpit fleet view ignored all of it and re-invented a worse data path by hand. A data.Store is an observable record collection that fires load (which grid/list surfaces consume to re-render), owns filters/sorters, and hydrates data.Model records whose fields are the data contract (src/data/Store.mjs, src/data/Model.mjs). A state.Provider is a hierarchical, container-scoped binding surface — getParent()/getStore() walk up the component tree (src/state/Provider.mjs); it belongs at a view root for a subtree with shared state, never one-per-row. Rows are records; the Store is the per-row reactive layer.
The Architectural Reality
Verified anti-patterns (read this session):
Hand-rolled data path — zero Store/Model.
apps/agentos/view/fleet/FleetCockpit.mjs:14 — FIXTURE_ROSTER, a hardcoded plain-array const.
apps/agentos/view/fleet/FleetCockpit.mjs:168 — loadRoster() maps registry defs to plain objects and calls grid.set({agents: defs.map(...)}) (:194), bypassing AgentDefinitions / AgentDefinition entirely.
apps/agentos/view/fleet/FleetGrid.mjs:86 — agents_: [], a plain-array config; :172 refreshGrid() does a manual removeAll(true) + add(cards) rebuild loop — reinventing (badly) what a Store-bound view does for free.
state.Provider per leaf card.
apps/agentos/view/fleet/AgentCard.mjs:64 — every card mints its own stateProvider: {module: StateProvider, data: {...}}. A 7–20-agent fleet spins up 7–20 provider instances, each duplicating one card's fields — where one Store of records is the single source of truth.
apps/agentos/view/fleet/FleetGrid.mjs:205 — agentCardConfig() seeds each per-card provider.
CSS-in-JS.
apps/agentos/view/fleet/StateDot.mjs:81 — afterSetState() writes this.style['--fm-dot'] from JS. Localized, but real. (Full audit required across every apps/agentos/view/fleet/** component for inline style writes / hardcoded colors.)
The unit spec pins the anti-pattern.
test/playwright/unit/apps/agentos/view/fleet/fleetCockpit.spec.mjs:102 — the loadRoster describe asserts grid.agents as a plain array of mapped objects. It must be rewritten to assert the Store-backed shape.
Verified clean — KEEP and reuse in the rebuild (do NOT delete working code to look thorough):
rankFleet() (FleetGrid.mjs:30) — pure, unit-tested ranking/fold. Adapt to operate on records (or express as Store sorters/filters).
stateToken() / kindToken() / kindLabel() (StateDot.mjs, kindRegistry.mjs) — pure resolvers over the --fm-state-* / --fm-kind-* SCSS token layer.
kindRegistry.mjs — a legitimate pure-data lookup module, not a data-path violation.
- The B4/C2 controller wiring (
AgentCardController.mjs, FleetCockpitController.mjs) — intent-emit + resolve-up-the-chain is correct; only the data it reads/writes moves from a per-card provider to the record.
- The SCSS token/skin layer under
resources/scss/**/apps/agentos/.
The Fix
- Model the card contract. Extend
AgentOS.model.AgentDefinition (or add a dedicated cockpit FleetAgent model) so its fields carry the card's display state (displayName, avatarUrl, engineTag, family, laneLine), session state, and the B4/C2 control fields (pendingAction, controlReason). The Model's fields are the contract.
- One Store, records flow. Bind a
data.Store (reuse/extend the AgentDefinitions singleton) at the cockpit/grid root. loadRoster() populates the Store (records) — no defs.map(→plain obj). Runtime status merges onto records (store.get(agentId).set({state})). Preserve the fail-closed behavior (empty / absent bridge / thrown source → keep last-known, honestly labelled).
- Grid renders from the Store.
FleetGrid binds the store; the ranked fold operates on the store's records (or Store sorters/filters produce the tiers); the card set derives from records; updates come from Store/record reactivity (load / recordChange), not a manual array diff.
- Cards bind their record — no per-card provider. Each
AgentCard reads its record (via the store row, or via ONE shared provider at the grid/cockpit root exposing the roster store — a state.Provider mirrors a Record's fields as bindable paths; see src/state/Provider.mjs). Zero providers at card grain.
- Control round-trip writes the record. Lane-C/C2 writes
pendingAction / controlReason as record fields; the card's control binds react. The honest-state matrix (unauthorized / timeout / rejected, per #14611) is preserved exactly — it just reads from the record.
- Kill CSS-in-JS.
StateDot drives color via a state cls (fm-state-ok …) mapped to the token in SCSS — remove the inline this.style write. Audit and fix every apps/agentos/view/fleet/** component; grep-proof in the PR.
- Rewrite the spec (
fleetCockpit.spec.mjs roster describe) to assert the Store-backed shape; unit CI green.
- Harden the substrate (mechanical, not a buried comment). Add the app-work required-context gate to
AGENTS.md (.claude/CLAUDE.md) — see AC — so every future app-code turn loads the primitives before writing. This is the only hardening that survives a spammed epic: it is loaded every turn.
Acceptance Criteria
Out of Scope
- The registry↔identity DTO producer (the source of
family / engineTag / laneLine, tracked at #14802). This ticket makes the Model carry the fields and flow gracefully-null until the producer lands; wiring the producer is separate.
- New cockpit features or zones. This is an architecture cleanup, not a feature add.
- Deleting the abandoned remote branch
feat/fm-cockpit-live-roster (operator authority; not agent-destructive-git scope).
Avoided Traps / Gold Standards Rejected
- Per-row
state.Provider as "one tidy binding surface per card." Looks clean; is N× the machinery for what records give free, and duplicates the Store's job. A Store of records is the per-row reactive layer. (This is precisely the pattern currently in AgentCard.mjs.)
defs.map() + grid.set({agents}) as "just seeding the grid." Reinvents Store.add/load badly and forfeits filters, sorters, record reactivity, and turbo-mode hydration.
- Inline
style CSS-var writes rationalized as "not really CSS-in-JS." It is. Class + SCSS token is the idiom.
- Blanket "delete everything." Rejected symmetrically: deleting the verified-clean pure resolvers / ranking / SCSS layer to look decisive is its own waste. Replace the data/render architecture; keep what is already right.
Decision Record impact
none — aligned-with the established Neo data.Store/data.Model + state.Provider architecture. Applies the same "read leaves bind, never hand-roll / never mutate the wrong layer" discipline that ADR-0019 sets for the AiConfig reactive-provider SSOT, here at the Body app layer. No ADR is created, amended, or challenged.
Related
- Parent epic: #13015 (Fleet Manager MVP — observe the fleet).
- #14611 — B4/C2 honest-state control contract (preserved by this rebuild).
- #14802 — registry↔identity DTO producer (the display-field source; Out of Scope here).
- #14868 — activity-feed binding (sibling
loadActivity path; same fail-closed discipline).
- Supersedes the abandoned
loadRoster approach on feat/fm-cockpit-live-roster (never PR'd).
Live Sweeps
- Live latest-open sweep: checked the 50 most-recently-updated open issues at 2026-07-08; no equivalent FM-cockpit-cleanup / Store-backed-roster ticket found.
- A2A in-flight claim sweep: mailbox scanned at 2026-07-08; no competing
[lane-claim] on this scope (newest traffic is 2026-07-06).
Origin Session ID
9085c64f-2eda-4eeb-bcf1-083ef888ce96
Handoff Retrieval Hints
query_raw_memories: "FM cockpit Store-backed roster data.Model per-leaf state.Provider CSS-in-JS rebuild"
- Anti-pattern anchor commit (abandoned, do not build on):
9c431fba4 on feat/fm-cockpit-live-roster (loadRoster plain-array path).
- Idiomatic reference already in-tree:
apps/agentos/view/FleetSettingsPanel.mjs binding apps/agentos/store/AgentDefinitions.mjs.
Context
The FM cockpit fleet view (
apps/agentos/view/fleet/**) was built against Neo's data/state primitives instead of on them. It renders — CI is green — but it hand-rolls a plain-array data path, mints onestate.Providerper card, and writes CSS from JS. That is exactly the "works, but ignores what the engine gives us" failure class: it will not survive review at scale and it teaches the wrong pattern to every agent who reads it next. This ticket is the PRIO-0 cleanup: rebuild the fleet view ondata.Store+data.Model, delete the per-leaf providers and the CSS-in-JS, and harden the substrate so the anti-pattern cannot recur.Operator directive (2026-07-08), public-safe:
The Problem
Neo ships the exact primitives this surface needs, and the app already uses them correctly one directory over:
apps/agentos/store/AgentDefinitions.mjs— aNeo.data.Storesingleton ofAgentOS.model.AgentDefinitionrecords: the roster data layer.apps/agentos/view/FleetSettingsPanel.mjs— bindsstore: AgentDefinitionsand reads records the idiomatic way.The cockpit fleet view ignored all of it and re-invented a worse data path by hand. A
data.Storeis an observable record collection that firesload(which grid/list surfaces consume to re-render), owns filters/sorters, and hydratesdata.Modelrecords whosefieldsare the data contract (src/data/Store.mjs,src/data/Model.mjs). Astate.Provideris a hierarchical, container-scoped binding surface —getParent()/getStore()walk up the component tree (src/state/Provider.mjs); it belongs at a view root for a subtree with shared state, never one-per-row. Rows are records; the Store is the per-row reactive layer.The Architectural Reality
Verified anti-patterns (read this session):
Hand-rolled data path — zero Store/Model.
apps/agentos/view/fleet/FleetCockpit.mjs:14—FIXTURE_ROSTER, a hardcoded plain-array const.apps/agentos/view/fleet/FleetCockpit.mjs:168—loadRoster()maps registry defs to plain objects and callsgrid.set({agents: defs.map(...)})(:194), bypassingAgentDefinitions/AgentDefinitionentirely.apps/agentos/view/fleet/FleetGrid.mjs:86—agents_: [], a plain-array config;:172refreshGrid()does a manualremoveAll(true)+add(cards)rebuild loop — reinventing (badly) what a Store-bound view does for free.state.Providerper leaf card.apps/agentos/view/fleet/AgentCard.mjs:64— every card mints its ownstateProvider: {module: StateProvider, data: {...}}. A 7–20-agent fleet spins up 7–20 provider instances, each duplicating one card's fields — where one Store of records is the single source of truth.apps/agentos/view/fleet/FleetGrid.mjs:205—agentCardConfig()seeds each per-card provider.CSS-in-JS.
apps/agentos/view/fleet/StateDot.mjs:81—afterSetState()writesthis.style['--fm-dot']from JS. Localized, but real. (Full audit required across everyapps/agentos/view/fleet/**component for inlinestylewrites / hardcoded colors.)The unit spec pins the anti-pattern.
test/playwright/unit/apps/agentos/view/fleet/fleetCockpit.spec.mjs:102— theloadRosterdescribe assertsgrid.agentsas a plain array of mapped objects. It must be rewritten to assert the Store-backed shape.Verified clean — KEEP and reuse in the rebuild (do NOT delete working code to look thorough):
rankFleet()(FleetGrid.mjs:30) — pure, unit-tested ranking/fold. Adapt to operate on records (or express as Store sorters/filters).stateToken()/kindToken()/kindLabel()(StateDot.mjs,kindRegistry.mjs) — pure resolvers over the--fm-state-*/--fm-kind-*SCSS token layer.kindRegistry.mjs— a legitimate pure-data lookup module, not a data-path violation.AgentCardController.mjs,FleetCockpitController.mjs) — intent-emit + resolve-up-the-chain is correct; only the data it reads/writes moves from a per-card provider to the record.resources/scss/**/apps/agentos/.The Fix
AgentOS.model.AgentDefinition(or add a dedicated cockpitFleetAgentmodel) so itsfieldscarry the card's display state (displayName,avatarUrl,engineTag,family,laneLine), sessionstate, and the B4/C2 control fields (pendingAction,controlReason). The Model'sfieldsare the contract.data.Store(reuse/extend theAgentDefinitionssingleton) at the cockpit/grid root.loadRoster()populates the Store (records) — nodefs.map(→plain obj). Runtime status merges onto records (store.get(agentId).set({state})). Preserve the fail-closed behavior (empty / absent bridge / thrown source → keep last-known, honestly labelled).FleetGridbinds the store; the ranked fold operates on the store's records (or Store sorters/filters produce the tiers); the card set derives from records; updates come from Store/record reactivity (load/recordChange), not a manual array diff.AgentCardreads its record (via the store row, or via ONE shared provider at the grid/cockpit root exposing the roster store — astate.Providermirrors a Record's fields as bindable paths; seesrc/state/Provider.mjs). Zero providers at card grain.pendingAction/controlReasonas record fields; the card's control binds react. The honest-state matrix (unauthorized/timeout/rejected, per #14611) is preserved exactly — it just reads from the record.StateDotdrives color via a statecls(fm-state-ok…) mapped to the token in SCSS — remove the inlinethis.stylewrite. Audit and fix everyapps/agentos/view/fleet/**component; grep-proof in the PR.fleetCockpit.spec.mjsroster describe) to assert the Store-backed shape; unit CI green.AGENTS.md(.claude/CLAUDE.md) — see AC — so every future app-code turn loads the primitives before writing. This is the only hardening that survives a spammed epic: it is loaded every turn.Acceptance Criteria
data.Storeofdata.Modelrecords. No plain-arrayagents_data path and nodefs.map(→plain obj)remains anywhere underapps/agentos/view/fleet/**.state.Providerinstances at card/leaf grain. At most one shared provider at the view root (or none — records suffice). Each card renders from its store record.stylewrites and no hardcoded color/spacing values in anyapps/agentos/view/fleet/**component. All styling via SCSS token/skin layers. (Include grep evidence in the PR.)loadRoster()populates the Store; runtime status merges onto records; fail-closed behavior (empty / absent / thrown → last-known, honestly labelled) preserved.pendingAction/controlReasonas record fields; card control binds react; the honest-state matrix (unauthorized / timeout / rejected, #14611) is preserved.fleetCockpit.spec.mjsasserts the Store-backed shape; unit CI green.AGENTS.mdcarries an app-work required-context gate: before writing/reviewingapps/**code, the agent MUST havesrc/Neo.mjs,src/core/Base.mjs,src/state/Provider.mjs,src/data/Model.mjs,src/data/Store.mjsin context; data-carrying UI binds a Store of Models;state.Providersits at view roots, never leaves; zero CSS-in-JS (SCSS token/skin only). Compact addition, net-justified by the negative-ROI cascade it prevents (substrate accretion defense).Out of Scope
family/engineTag/laneLine, tracked at #14802). This ticket makes the Model carry the fields and flow gracefully-null until the producer lands; wiring the producer is separate.feat/fm-cockpit-live-roster(operator authority; not agent-destructive-git scope).Avoided Traps / Gold Standards Rejected
state.Provideras "one tidy binding surface per card." Looks clean; is N× the machinery for what records give free, and duplicates the Store's job. A Store of records is the per-row reactive layer. (This is precisely the pattern currently inAgentCard.mjs.)defs.map()+grid.set({agents})as "just seeding the grid." ReinventsStore.add/loadbadly and forfeits filters, sorters, record reactivity, and turbo-mode hydration.styleCSS-var writes rationalized as "not really CSS-in-JS." It is. Class + SCSS token is the idiom.Decision Record impact
none— aligned-with the established Neodata.Store/data.Model+state.Providerarchitecture. Applies the same "read leaves bind, never hand-roll / never mutate the wrong layer" discipline that ADR-0019 sets for the AiConfig reactive-provider SSOT, here at the Body app layer. No ADR is created, amended, or challenged.Related
loadActivitypath; same fail-closed discipline).loadRosterapproach onfeat/fm-cockpit-live-roster(never PR'd).Live Sweeps
[lane-claim]on this scope (newest traffic is 2026-07-06).Origin Session ID
9085c64f-2eda-4eeb-bcf1-083ef888ce96
Handoff Retrieval Hints
query_raw_memories: "FM cockpit Store-backed roster data.Model per-leaf state.Provider CSS-in-JS rebuild"9c431fba4onfeat/fm-cockpit-live-roster(loadRosterplain-array path).apps/agentos/view/FleetSettingsPanel.mjsbindingapps/agentos/store/AgentDefinitions.mjs.