LearnNewsExamplesServices
Frontmatter
id14899
titlePRIO-0: Rebuild the FM cockpit fleet view on data.Store/Model
stateOpen
labels
enhancementairefactoringtestingarchitecture
assigneesneo-opus-vega
createdAtJul 8, 2026, 1:54 AM
updatedAtJul 8, 2026, 1:56 AM
githubUrlhttps://github.com/neomjs/neo/issues/14899
authorneo-opus-vega
commentsCount0
parentIssue13015
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]

PRIO-0: Rebuild the FM cockpit fleet view on data.Store/Model

Open Backlog/active-chunk-4 enhancementairefactoringtestingarchitecture
neo-opus-vega
neo-opus-vega commented on Jul 8, 2026, 1:54 AM

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):

  1. Hand-rolled data path — zero Store/Model.

    • apps/agentos/view/fleet/FleetCockpit.mjs:14FIXTURE_ROSTER, a hardcoded plain-array const.
    • apps/agentos/view/fleet/FleetCockpit.mjs:168loadRoster() maps registry defs to plain objects and calls grid.set({agents: defs.map(...)}) (:194), bypassing AgentDefinitions / AgentDefinition entirely.
    • apps/agentos/view/fleet/FleetGrid.mjs:86agents_: [], 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.
  2. 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:205agentCardConfig() seeds each per-card provider.
  3. CSS-in-JS.

    • apps/agentos/view/fleet/StateDot.mjs:81afterSetState() 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.)
  4. 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

  1. 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.
  2. 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).
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. Rewrite the spec (fleetCockpit.spec.mjs roster describe) to assert the Store-backed shape; unit CI green.
  8. 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

  • The fleet roster is a data.Store of data.Model records. No plain-array agents_ data path and no defs.map(→plain obj) remains anywhere under apps/agentos/view/fleet/**.
  • Zero state.Provider instances at card/leaf grain. At most one shared provider at the view root (or none — records suffice). Each card renders from its store record.
  • Zero CSS-in-JS: no inline style writes and no hardcoded color/spacing values in any apps/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.
  • B4/C2 control round-trip writes pendingAction/controlReason as record fields; card control binds react; the honest-state matrix (unauthorized / timeout / rejected, #14611) is preserved.
  • fleetCockpit.spec.mjs asserts the Store-backed shape; unit CI green.
  • AGENTS.md carries an app-work required-context gate: before writing/reviewing apps/** code, the agent MUST have src/Neo.mjs, src/core/Base.mjs, src/state/Provider.mjs, src/data/Model.mjs, src/data/Store.mjs in context; data-carrying UI binds a Store of Models; state.Provider sits 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).
  • The rebuilt view is mount-verified at live scale (whitebox-e2e or Neural-Link-verified render).
  • No new tickets/epics spawned. This is the single cleanup sub under #13015.

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.