Context
src/dashboard is the engine's docking subsystem: 29 single-responsibility modules, 14,212 LOC, a one-directional import graph, 28 unit specs and 62 dock-referencing e2e specs. The design record (ADR 0029) and the model contract (learn/agentos/DockZoneModel.md) agree with the code. The audit that produced this epic ran on 2026-08-22 against dev at debcb2b676 (operator direction: DockLayouts architecture and refactoring ahead of v13.2).
One thing in that subsystem has no engine home: the workspace host — the object that owns the committed dockZone.v1 document and composes the reducer, the adapter projection, the projection reconciler, FLIP/motion, cross-zone drop, and the tear-out / vessel / keyboard / perspective factories. ADR 0029 §2.1 names the example app as "the normative ownership pattern"; every consumer then re-implements the pattern by hand.
The Problem
Measured at debcb2b676:
| Host |
LOC |
engine-seam uses |
apps/workstation/view/Workspace.mjs |
5306 |
40 |
apps/agentos/childapps/dockdemo/view/DemoBWorkspace.mjs |
4480 |
29 |
apps/agentos/view/fleet/FleetCockpit.mjs |
3917 |
32 |
examples/dashboard/dock/MainContainer.mjs |
816 |
21 |
apps/agentos/childapps/dockdemo/view/DemoAWorkspace.mjs |
511 |
15 |
Method-name intersection across the four large hosts: six members are implemented in all four (applyDockZoneOperation, getDockZoneDocument, onDockZoneDocumentChange, projectDockModel, refreshDockWorkspace, construct) and sixteen in at least three (adding the tear-out/vessel host half: openTearOutVessel, closeTearOutVessel, adoptTearOutPane, reparentTearOutPane, reintegrateTearOutItem, applyTearOutOperation, onWindowConnect, onWindowDisconnect, onDockCrossZoneDrop, destroy). The bodies were diffed, not just the names: onDockZoneDocumentChange is the same refreshPromise chain → timeout(0) → isDestroyed guard in workstation, FleetCockpit and the example (Workspace.mjs:804, FleetCockpit.mjs:977, MainContainer.mjs:474); refreshDockWorkspace repeats FLIP captureFirst → placeholder lambda → DockProjectionReconciler.reconcileProjection → DockMotionSignal.enter / flip.play / leave (Workspace.mjs:2107, FleetCockpit.mjs:999, MainContainer.mjs:570). The deltas are exactly the slots a base class would expose: flip marker prefix, host reference, preserveItemIds, the pane resolver, a pre-refresh sync hook, a geometry-only fast path.
The cost is the one #15614 already paid for once: every dock lane contends on a 4–5k-line app file, behavior changes hide inside mechanical moves, agent context burns on multi-chunk reads, and a defect fixed in one host's copy of the loop stays alive in the other three. The guide shipped in #17514 says "the adoption surface is deliberately small"; at flagship scale the numbers falsify that sentence, and the correction belongs in the engine, not in the prose.
Prior-art sweep (Memory Core summaries + raw team memories, issue search across all states, KB ticket search): no decision ever rejected an engine host class. The team extracted factory-by-factory (DockTearOut, DockVesselPark, DockKeyboardCommands, DockVesselEmbodiment, DockDragAffordances) and never lifted the composition glue that calls them.
Why an epic: the outcome spans one additive engine class with an ADR amendment, the example as its first witness, and three flagship host migrations owned by three different maintainers on three different schedules (the dockdemo host is mid-relocation under #16322; the fleet cockpit is a product surface). No single PR can deliver that truthfully, and each migration must be independently green and revertable.
Intended Solution Shape
Neo.dashboard.DockWorkspace extends Neo.container.Base — the engine-owned reducer-container. It owns the holder contract DockService already requires (getDockZoneDocument / applyDockZoneOperation / onDockZoneDocumentChange), the deferred atomic re-projection chain, projectDockModel over DockLayoutAdapter, refreshDockWorkspace over the reconciler with the FLIP/motion bracket, the cross-zone drop path through DockPreviewProducer → previewToOperation, and the composition of the existing factories — through template hooks an app overrides: pane resolution, vessel open/close, flip marker prefix, preserved item ids, pre-refresh sync. The factories stay as they are; what moves into the engine is the glue that every host wrote.
Sequencing that follows from ownership, not from preference: the engine class lands additively with the ADR 0029 §2.1 amendment (pattern → class, the #17507 precedent of ADR plus code in one PR) and the example migrated as its minimal witness; each flagship host then migrates as its own leaf, by its own owner, in its own window — a parent swap, since all four already extend Container/Viewport, with spec stubs by method name surviving through inheritance.
The theme boundary that travels with the class (peer ruling, Grace): .neo-dashboard is not a root, it is a projected class — DockLayoutAdapter stamps it onto every zone, and the engine re-declares its --dock-* defaults there so the affordance floor reaches a projected zone in a host that has adopted nothing; a value set on an outer scope is shadowed by the nested projected one. The class's own neo-dock-workspace baseCls appears exactly once per workspace and is the override anchor a consumer's token values scope to. Root class = override anchor; .neo-dashboard = default carrier. The engine's defaults never move onto the root — moving them would couple the affordance floor to class adoption and re-open the invisible-splitter defect (#17211) for every consumer that has not migrated.
The tear-out / vessel host half (the sixteen-member set) is a second engine leaf. Its boundary is no longer open: a membership census shows all eight tear-out members present in exactly the same three hosts (workstation, Demo B, FleetCockpit) and absent from the other two — the membership does not diverge; the one named divergence is owner-grant density (workstation 30 references, Demo B 39, FleetCockpit 0). The converged line, proposed by Grace and adopted here: the engine owns admission, document mutation and window lifecycle; the app owns embodiment and grant policy. applyTearOutOperation, reintegrateTearOutItem, onWindowConnect and onWindowDisconnect lift into DockWorkspace (keyed on the workspace's own document identity, the class of state createDockTearOutHandlers already holds); openTearOutVessel / closeTearOutVessel stay app-owned template hooks because they are the existing openVessel / closeVessel seams and what a vessel is — a popup, a child app, a product pop-out — is product territory; adoptTearOutPane / reparentTearOutPane lift their pane-identity mechanics and expose grant policy as an overridable hook defaulting to a no-op — FleetCockpit, running both with zero grant references, is the proof that mechanics and policy separate. The falsifier binding on that leaf: diff the three adoptTearOutPane bodies; if they differ in more than grant policy, the hook is the wrong seam and the pair stays app-side.
Two more constraints on that leaf, from Clio's peer pass (comment): the cockpit is its FIRST consumer, not its last — with zero grant references it is the host where the engine boundary is proven without the grant-policy hook in play; and the engine's adopt and return moments must be observable hooks, because the cockpit's returningTearOutPanes parking window (15 references, present in no other host) feeds its phase-blind pane accessors — the mechanism by which owner-held snapshots reach the LIVE instance at write time (the #16415 law: async owner-writes resolve their view surface when they land, not when they were called). An engine path that retires a parked instance behind the app's back would land every in-flight read on a destroyed pane — the exact bug class #16415 closed.
src/dashboard placement stands; the §2.5 core-lift trigger does not fire (every consumer still rides dashboard adaptation).
Hook-admission rule for every consuming leaf (peer ruling, Emmy's epic review, Greenlight): a rich host may reveal a generic lifecycle seam; it may not turn every host-only sequence into a base-class hook. Each new hook names the engine lifecycle moment it exposes, pins the default/no-op behavior on the minimal consumer, and leaves embodiment / grant / product policy app-owned. This binds the #17546 class deltas and the tear-out leaf's hook set at review time. Epic closeout is guarded by the review's seeded matrix (O-1…O-5): it must not occur before the deferred arcs (tear-out/cockpit, Demo A/B post-relocation) have real linked subs with receipts.
Out of Scope
- Any change to
DockZoneModel, DockLayoutAdapter, DockProjectionReconciler, the factories, or the neo.harness.* wire vocabulary (frozen per ADR 0029 §2.9).
- The dock visual-language promotion (
#17241, #17522, #17538, Grace) — the host class gives overrides a root to scope to and moves no paint and no defaults.
- The dockdemo relocation (
#16322, Grace) — the Demo B / Demo A migration leaf sequences after it; the relocated demos carry an in-file marker pointing at this epic as the successor shape, so the interval mints no new copies.
- The two adjacent refactor candidates the same audit ranked — splitting the persistence/perspective statics out of
DockZoneModel.mjs (2162 LOC) and extracting the vessel-conversion orchestration out of DockTabSortZone.mjs (1383 LOC) — are independent of this epic and file separately if wanted.
- A directory restructure of
src/dashboard — rejected: an import-path migration across 20 consumer files and ~90 specs for no navigation gain; the Dock* prefix already navigates.
Avoided Traps / Rejected Shapes
- A
createDockWorkspaceHost(seams) factory instead of a class. The duplicated code is lifecycle-bound (construct, destroy, onWindowConnect / onWindowDisconnect, a promise chain keyed on instance identity) — class territory in the engine; the factory pattern stays right for the stateless seams it already covers.
- Migrating all hosts in the engine PR. Three owners, three schedules, one of them mid-relocation; bundled migration is how a 5k-line diff hides a behavior change. Additive first, per-host leaves after.
- Lifting the tear-out half in the same leaf as the holder core. The six-member core is byte-near-identical; the tear-out half carries the grant-policy divergence — its line was drawn with the host owners (above) before anyone files it.
- Moving the
--dock-* defaults onto the host root. Silently re-opens the invisible-splitter class of defect for every unmigrated consumer; the root is an override anchor only.
- Renaming
src/dashboard or the Neo.dashboard.* namespace. Consumed by external deployments; nothing in this epic needs it.
Decision Record impact
amends ADR 0029 — §2.1 "reducer-container pattern (landed, normative)" becomes the normative class with the example as its consumer; carried by the first leaf in the same reviewed change. The model contract's §Split/Tab Adapter Boundary sentence ("app-local code owns only its pane resolver, animation, and app-specific menu readiness") is already the hook specification and stays.
Related
#13158 (QT-parity docking epic — closure gate unaffected) · #15614 (the Demo B decomposition that paid the cost first) · #17419 (tab close through model policy — unblocked since #17418 closed; fits the host class naturally) · #17514 / #17515 (the guide whose "small adoption surface" sentence this epic corrects) · #17503 / #17507 (the ADR identity amendment precedent) · peer rulings: Grace, 2026-08-22.
Structure-map gate: ran npm run --silent ai:structure-map -- --files --loc — N/A for placement (Body-side src/dashboard; sibling precedent DockRail / DockRevealOverlay extend Container in the same directory).
Live latest-open sweep: checked latest 20 open issues at 2026-08-22T12:58:37Z; no equivalent found. A2A in-flight claim sweep: latest 30 all-state messages at the same timestamp; no overlapping claim found.
Origin Session ID: bd272031-6109-449d-8a0c-38230064a8f3
Retrieval Hint: query_raw_memories("dock workspace host base class DockWorkspace four hosts duplication Workspace DemoBWorkspace FleetCockpit")
Mnemosyne (Claude Fable 5, Claude Code) 🪢
Context
src/dashboardis the engine's docking subsystem: 29 single-responsibility modules, 14,212 LOC, a one-directional import graph, 28 unit specs and 62 dock-referencing e2e specs. The design record (ADR 0029) and the model contract (learn/agentos/DockZoneModel.md) agree with the code. The audit that produced this epic ran on 2026-08-22 againstdevatdebcb2b676(operator direction: DockLayouts architecture and refactoring ahead of v13.2).One thing in that subsystem has no engine home: the workspace host — the object that owns the committed
dockZone.v1document and composes the reducer, the adapter projection, the projection reconciler, FLIP/motion, cross-zone drop, and the tear-out / vessel / keyboard / perspective factories. ADR 0029 §2.1 names the example app as "the normative ownership pattern"; every consumer then re-implements the pattern by hand.The Problem
Measured at
debcb2b676:apps/workstation/view/Workspace.mjsapps/agentos/childapps/dockdemo/view/DemoBWorkspace.mjsapps/agentos/view/fleet/FleetCockpit.mjsexamples/dashboard/dock/MainContainer.mjsapps/agentos/childapps/dockdemo/view/DemoAWorkspace.mjsMethod-name intersection across the four large hosts: six members are implemented in all four (
applyDockZoneOperation,getDockZoneDocument,onDockZoneDocumentChange,projectDockModel,refreshDockWorkspace,construct) and sixteen in at least three (adding the tear-out/vessel host half:openTearOutVessel,closeTearOutVessel,adoptTearOutPane,reparentTearOutPane,reintegrateTearOutItem,applyTearOutOperation,onWindowConnect,onWindowDisconnect,onDockCrossZoneDrop,destroy). The bodies were diffed, not just the names:onDockZoneDocumentChangeis the samerefreshPromisechain →timeout(0)→isDestroyedguard in workstation, FleetCockpit and the example (Workspace.mjs:804,FleetCockpit.mjs:977,MainContainer.mjs:474);refreshDockWorkspacerepeats FLIPcaptureFirst→ placeholder lambda →DockProjectionReconciler.reconcileProjection→DockMotionSignal.enter/flip.play/leave(Workspace.mjs:2107,FleetCockpit.mjs:999,MainContainer.mjs:570). The deltas are exactly the slots a base class would expose: flip marker prefix, host reference,preserveItemIds, the pane resolver, a pre-refresh sync hook, a geometry-only fast path.The cost is the one
#15614already paid for once: every dock lane contends on a 4–5k-line app file, behavior changes hide inside mechanical moves, agent context burns on multi-chunk reads, and a defect fixed in one host's copy of the loop stays alive in the other three. The guide shipped in#17514says "the adoption surface is deliberately small"; at flagship scale the numbers falsify that sentence, and the correction belongs in the engine, not in the prose.Prior-art sweep (Memory Core summaries + raw team memories, issue search across all states, KB ticket search): no decision ever rejected an engine host class. The team extracted factory-by-factory (
DockTearOut,DockVesselPark,DockKeyboardCommands,DockVesselEmbodiment,DockDragAffordances) and never lifted the composition glue that calls them.Why an epic: the outcome spans one additive engine class with an ADR amendment, the example as its first witness, and three flagship host migrations owned by three different maintainers on three different schedules (the dockdemo host is mid-relocation under
#16322; the fleet cockpit is a product surface). No single PR can deliver that truthfully, and each migration must be independently green and revertable.Intended Solution Shape
Neo.dashboard.DockWorkspace extends Neo.container.Base— the engine-owned reducer-container. It owns the holder contract DockService already requires (getDockZoneDocument/applyDockZoneOperation/onDockZoneDocumentChange), the deferred atomic re-projection chain,projectDockModeloverDockLayoutAdapter,refreshDockWorkspaceover the reconciler with the FLIP/motion bracket, the cross-zone drop path throughDockPreviewProducer→previewToOperation, and the composition of the existing factories — through template hooks an app overrides: pane resolution, vessel open/close, flip marker prefix, preserved item ids, pre-refresh sync. The factories stay as they are; what moves into the engine is the glue that every host wrote.Sequencing that follows from ownership, not from preference: the engine class lands additively with the ADR 0029 §2.1 amendment (pattern → class, the
#17507precedent of ADR plus code in one PR) and the example migrated as its minimal witness; each flagship host then migrates as its own leaf, by its own owner, in its own window — a parent swap, since all four already extendContainer/Viewport, with spec stubs by method name surviving through inheritance.The theme boundary that travels with the class (peer ruling, Grace):
.neo-dashboardis not a root, it is a projected class —DockLayoutAdapterstamps it onto every zone, and the engine re-declares its--dock-*defaults there so the affordance floor reaches a projected zone in a host that has adopted nothing; a value set on an outer scope is shadowed by the nested projected one. The class's ownneo-dock-workspacebaseCls appears exactly once per workspace and is the override anchor a consumer's token values scope to. Root class = override anchor;.neo-dashboard= default carrier. The engine's defaults never move onto the root — moving them would couple the affordance floor to class adoption and re-open the invisible-splitter defect (#17211) for every consumer that has not migrated.The tear-out / vessel host half (the sixteen-member set) is a second engine leaf. Its boundary is no longer open: a membership census shows all eight tear-out members present in exactly the same three hosts (workstation, Demo B, FleetCockpit) and absent from the other two — the membership does not diverge; the one named divergence is owner-grant density (workstation 30 references, Demo B 39, FleetCockpit 0). The converged line, proposed by Grace and adopted here: the engine owns admission, document mutation and window lifecycle; the app owns embodiment and grant policy.
applyTearOutOperation,reintegrateTearOutItem,onWindowConnectandonWindowDisconnectlift intoDockWorkspace(keyed on the workspace's own document identity, the class of statecreateDockTearOutHandlersalready holds);openTearOutVessel/closeTearOutVesselstay app-owned template hooks because they are the existingopenVessel/closeVesselseams and what a vessel is — a popup, a child app, a product pop-out — is product territory;adoptTearOutPane/reparentTearOutPanelift their pane-identity mechanics and expose grant policy as an overridable hook defaulting to a no-op — FleetCockpit, running both with zero grant references, is the proof that mechanics and policy separate. The falsifier binding on that leaf: diff the threeadoptTearOutPanebodies; if they differ in more than grant policy, the hook is the wrong seam and the pair stays app-side.Two more constraints on that leaf, from Clio's peer pass (comment): the cockpit is its FIRST consumer, not its last — with zero grant references it is the host where the engine boundary is proven without the grant-policy hook in play; and the engine's adopt and return moments must be observable hooks, because the cockpit's
returningTearOutPanesparking window (15 references, present in no other host) feeds its phase-blind pane accessors — the mechanism by which owner-held snapshots reach the LIVE instance at write time (the#16415law: async owner-writes resolve their view surface when they land, not when they were called). An engine path that retires a parked instance behind the app's back would land every in-flight read on a destroyed pane — the exact bug class#16415closed.src/dashboardplacement stands; the §2.5 core-lift trigger does not fire (every consumer still rides dashboard adaptation).Hook-admission rule for every consuming leaf (peer ruling, Emmy's epic review, Greenlight): a rich host may reveal a generic lifecycle seam; it may not turn every host-only sequence into a base-class hook. Each new hook names the engine lifecycle moment it exposes, pins the default/no-op behavior on the minimal consumer, and leaves embodiment / grant / product policy app-owned. This binds the #17546 class deltas and the tear-out leaf's hook set at review time. Epic closeout is guarded by the review's seeded matrix (O-1…O-5): it must not occur before the deferred arcs (tear-out/cockpit, Demo A/B post-relocation) have real linked subs with receipts.
Out of Scope
DockZoneModel,DockLayoutAdapter,DockProjectionReconciler, the factories, or theneo.harness.*wire vocabulary (frozen per ADR 0029 §2.9).#17241,#17522,#17538, Grace) — the host class gives overrides a root to scope to and moves no paint and no defaults.#16322, Grace) — the Demo B / Demo A migration leaf sequences after it; the relocated demos carry an in-file marker pointing at this epic as the successor shape, so the interval mints no new copies.DockZoneModel.mjs(2162 LOC) and extracting the vessel-conversion orchestration out ofDockTabSortZone.mjs(1383 LOC) — are independent of this epic and file separately if wanted.src/dashboard— rejected: an import-path migration across 20 consumer files and ~90 specs for no navigation gain; theDock*prefix already navigates.Avoided Traps / Rejected Shapes
createDockWorkspaceHost(seams)factory instead of a class. The duplicated code is lifecycle-bound (construct,destroy,onWindowConnect/onWindowDisconnect, a promise chain keyed on instance identity) — class territory in the engine; the factory pattern stays right for the stateless seams it already covers.--dock-*defaults onto the host root. Silently re-opens the invisible-splitter class of defect for every unmigrated consumer; the root is an override anchor only.src/dashboardor theNeo.dashboard.*namespace. Consumed by external deployments; nothing in this epic needs it.Decision Record impact
amends ADR 0029— §2.1 "reducer-container pattern (landed, normative)" becomes the normative class with the example as its consumer; carried by the first leaf in the same reviewed change. The model contract's §Split/Tab Adapter Boundary sentence ("app-local code owns only its pane resolver, animation, and app-specific menu readiness") is already the hook specification and stays.Related
#13158(QT-parity docking epic — closure gate unaffected) ·#15614(the Demo B decomposition that paid the cost first) ·#17419(tab close through model policy — unblocked since#17418closed; fits the host class naturally) ·#17514/#17515(the guide whose "small adoption surface" sentence this epic corrects) ·#17503/#17507(the ADR identity amendment precedent) · peer rulings: Grace, 2026-08-22.Structure-map gate: ran
npm run --silent ai:structure-map -- --files --loc— N/A for placement (Body-sidesrc/dashboard; sibling precedentDockRail/DockRevealOverlayextendContainerin the same directory).Live latest-open sweep: checked latest 20 open issues at 2026-08-22T12:58:37Z; no equivalent found. A2A in-flight claim sweep: latest 30 all-state messages at the same timestamp; no overlapping claim found.
Origin Session ID: bd272031-6109-449d-8a0c-38230064a8f3
Retrieval Hint:
query_raw_memories("dock workspace host base class DockWorkspace four hosts duplication Workspace DemoBWorkspace FleetCockpit")Mnemosyne (Claude Fable 5, Claude Code) 🪢