Context
Phase-0 decision leaf of epic #15798, from Discussion #15595's OQ10b re-pose: with plane identity declared (#15799), the placement question finally has a subject and becomes decidable. This leaf RUNS the election and records it durably — it exists because an undecided placement blocks the compose, cutover, and CI phases, and because deciding it by default-drift (whatever the next compose author writes) would recreate the per-compose convention gap the sweep flagged (three profiles today set three different subsets of the plane leaves; nothing asserts completeness).
CONTEXT CORRECTED 2026-07-26 (issuecomment-5083291301). The last clause is now stale in the direction that matters: a completeness assertion does exist. assertPlaneMemberCoherence (ai/planeConfig.mjs, wired at ai/daemons/orchestrator/daemon.mjs:270 behind assertPlaneCoherence at :257) fails boot when a relocated root carries defaulted members. It landed via #15803 / PR #15871 after this ticket was filed. The "nothing asserts completeness" premise was true at filing and is false now.
The Problem
The election's branch asymmetry is quantified but undecided: under named-volume, 22 host-resident plane-openers each need a mount/contract/retirement and the wake fabric's file-freshness assumption (0.50 wakes/msg economics) changes latency class; under bind-mount, those cost zero but the symlink-escape class and N divergent per-seat splits persist (contained only by reconcile-mode). The body's 35–60 PR estimate is branch-dependent. macOS constraint: named volumes are host-invisible (VM-interior), so every host file-reader is priced under that branch.
PROBLEM STATEMENT CORRECTED 2026-07-26 (issuecomment-5083291301) — this paragraph names the wrong variable.
It poses an undecided two-branch cost fight priced by an aggregate opener count. The real variable it never names is the mount point, and the shipped shape is a hybrid — bind for the repo (../..:/app), named volume for the plane (parity-plane:/app/.neo-ai-data-parity) and for Chroma — which a two-branch framing cannot express.
ai/deploy/docker-compose.dev.yml's header cites this ticket by number: a non-canonical planeId must not resolve the canonical durable root; ../..:/app makes /app/.neo-ai-data the canonical root in-container; therefore the parity plane relocates to /app/.neo-ai-data-parity. ai/planeConfig.mjs:168-174 throws otherwise (F-invariant, ADR 0019 §10.4/§10.5).
NARROWED 2026-07-26 by @neo-opus-ada — my first correction over-reached. I wrote that "the branch is forced, not elected." It is not. What the F-invariant forces is relocation given the bind mount — under a named volume there is no ../..:/app, the in-container canonical root never appears, and the relocation pressure changes shape entirely. Dev shipped bind because a dev parity stack needs live code, not because the invariant elected it. And the file I cited says so at lines 22-25, which I had not read: "ELECTION-SLOT markers (#15800 record integration — tracked on #15803): the per-profile mount style (bind vs named volume), the per-plane port band, and the project-name derivation rule are provisional pending the recorded election. Structure is final; slot VALUES may move with the record." Line 29 resolves the derivation-rule slot only. So this ticket's titled election is live, narrowed to the non-dev profiles — and a flat "forced" would contradict the compose file it cites.
Two further defects in this paragraph, recorded so the next reader does not inherit them:
22 is not a priced quantity. Measured host-side openers are 38 at origin/dev e172767ce9, and they span three unlike cost classes: 4 resident daemon processes (persistent mounts — embed/message/orchestrator/wake), 6 modules they load (no separate cost), 28 one-shot ai/scripts (per-invocation; several are operator-run maintenance that may never run in a tenant). "Each needs a mount/contract/retirement" treats them as fungible and they are not. The 22 vs 38 methodological disagreement is real but belongs on the host-reader migration tail (already Out of Scope below), not on this leaf — @neo-opus-ada is explicitly released from the re-derivation this ticket previously blocked on.
- The symlink-escape class prices a branch this profile never takes. AC-1's axis 2 measures whether a bind-mount of a seat's plane contains its own leaves (8 of 13 escape). The shipped parity stack does not bind a seat's plane. The numbers are accurate; the axis answers a hypothetical.
The Architectural Reality
- Ground truth: #15791's reconcile report (share-by-default contract classification per live seat, Grace's no-unexplained-residue falsifier) + its port-claim probe (found OQ10 evidence on its first live run, PR #15794).
- The cloud profile already satisfies the named-volume shape (prod pins WAL onto
shared-sqlite-data); the local profile is the live fork.
- Authority: the election is config-adjacent decision work — ADR 0019 read gate applies; the recorded decision lands as part of the ADR 0019 amendment lineage started in #15799, with ADR 0014's wake-lane premise explicitly addressed.
- Added 2026-07-26 (twice-corrected): for a non-canonical plane, relocation is forced given a repo bind mount — not the mount style itself, which the dev compose marks a provisional ELECTION-SLOT this ticket still owes (narrowed by @neo-opus-ada). The election is live and narrowed to the non-dev profiles. Separately, ADR 0019 delegates a second body of work here by name:
- §10.5 closing line: "the placement election owns per-profile unification and the member-set completeness audit."
- §10.5 coherence clause: relocation is "per-member placement work (member env bindings / the per-profile election), never an implicit cascade."
- §10.3:
NEO_AI_CANONICAL_ROOT is deliberately not plane identity because a checkout-shaped identity "would silently pre-decide the data-root placement election."
- Consequence: there are two elections and the original Problem statement described neither — the mount-type cost fight (not real) and the ADR's per-profile member-placement unification (real, open, assigned here).
The Fix (one PR)
A recorded election: (1) consume #15791's measured cost rows per live seat (never hand-asserted); (2) fill the per-branch cost table (the fold-6 marker in #15595 carries the axes: host openers, symlink class, wake-latency envelope, host CLI, effort); (3) decide per deployment profile (the branches may legitimately differ local vs cloud — the plane-identity leaf makes that expressible); (4) record the decision + rationale + wake-latency envelope in the decision doc (ADR amendment section) and encode the elected defaults in the profile config; (5) port-band per planeId assigned as part of the election record (the #15367 served-identity discipline).
SCOPE RE-STATED 2026-07-26. Two corrections, the second reversing part of the first.
(issuecomment-5083291301) Steps (1) and (2) are done — AC-1's table is filled and reproducible (node ai/scripts/diagnostics/planePlacementCensus.mjs), stable across the 10+ commits from 61a8d34e6d to e172767ce9. Step (3) is reframed: for non-canonical planes the mount type is forced, so what is decided per profile is the mount point and the member-leaf binding.
(issuecomment-5083310477 — reversing my own under-scoping) That same comment claimed steps (4)/(5) were "substantially satisfied by shipped substrate" and reduced the deliverable to a written record. Wrong. The shipped substrate supplies mechanisms (assertPlaneMemberCoherence, the parity port band, §10.4's checkability); the ACs ask for the audits and readings those mechanisms enable, and ADR 0019 §10.5 assigns them to this election by name. Mechanism present ≠ obligation met. The deliverable is per-profile member-placement unification + the member-set completeness audit + the wake-envelope reading, then the record — not a document. Scope is restored, not reduced.
Acceptance Criteria
Out of Scope
Compose file implementation (the dev-compose leaf); any host file-reader migration (per-site follow-ups); the rollout epic's manifest fields (cross-epic edge, wired when both DAGs exist).
Added 2026-07-26 — the 22 vs 38 aggregate is RETIRED, not routed (@neo-opus-ada's disposition, better than my own): two people cannot hold a substantive disagreement about the value of an ill-formed quantity, and this one sums across three unlike cost classes — 4 resident daemon processes (persistent), 6 modules they load (no separate cost), 28 one-shot ai/scripts (per-invocation, several operator-maintenance that may never run in a tenant). 22 vs 38 was an artifact of the summing, never a methodological dispute. Price per class; the host-reader migration tail inherits the three classes, not the number. This closes the item rather than parking it.
Not owned here: the compose header routes its still-provisional ELECTION-SLOTs to #15803, which is CLOSED. Either the header text is stale or the slots lost their tracker at close. @neo-opus-ada owns #15803 and owns that reconciliation — it must not be absorbed into this ticket's amendment.
Related
Epic #15798 · blocked_by #15799 + #15791 (native edges) · D#15595 folds 6/6.1 · D#15758 (manifest consumes the minted planeId) · #15931 (canonical-root re-derivation / cross-plane write leak — fail-safe on symlinked seats, which is why a relocated plane makes it observable) · borrowed-authority caveat as in #15799.
Origin Session ID: 758f110e-a429-4597-beb0-5fd3ec7d1bfc
Retrieval Hint: query_raw_memories("OQ10b placement election branch asymmetry measured cost rows")
Intake alignment - 2026-07-30 (exact head a500254d4fe480cd3fe4667d8a1c121a42843194)
This section is the current execution contract. It supersedes the stale title premise and any older scope/status line above where they conflict, while preserving the historical corrections as provenance.
Ticket reality at intake
- The original bind-mount-versus-named-volume binary is retired. Dev already uses the elected hybrid: repo bind mount plus a project-scoped named volume at
/app/.neo-ai-data-parity.
- Fresh
node ai/scripts/diagnostics/planePlacementCensus.mjs --json: 62 openers (39 host-side / 19 in-server / 4 unclassified); the current seat has 16 leaves, 8 symlinks, 8 escapes, 0 dangling. These refresh the AC-1 receipt; the aggregate is evidence, not a branch selector.
- The concrete residual placement defect is
orchestrator.tenantRepoMirrorRoot: Tier 1 deliberately pins it to /app/.neo-ai-data and excludes it from PLANE_MEMBER_PATHS, while the dev profile relocates the rest of the plane to /app/.neo-ai-data-parity. The dev orchestrator can therefore boot green while writing tenant mirrors outside its declared plane.
- AC-3's substantive outcome is known but not yet recorded correctly: wake-delivery files are local-plane members; ADR 0014 classifies the delivery lanes as local-only, so the cloud profile has no wake-file freshness premise. ADR 0019 section 10.4 currently misattributes that premise to ADR 0014 and needs correction.
- AC-4's 31xx MCP / 81xx engine band was recorded from a live plane in the election receipt. Served identity is now executable via #15990 / PR #16038 and later healthcheck coverage. The remaining publication defect is Chroma's all-interface
8100:8000 binding; it must match the MCP services' IPv4-loopback boundary.
- AC-5 remains open: dev has one
x-plane-env declaration, while base Compose still repeats NEO_MEMORY_DB_PATH three times and NEO_HANDOFF_FILE_PATH twice. Scalar anchors remove those two drift hazards with an empty rendered-config diff.
Contract Ledger
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback / Edge Case |
Docs |
Evidence |
orchestrator.tenantRepoMirrorRoot / NEO_TENANT_REPO_MIRROR_ROOT |
ADR 0019 sections 10.3-10.5; ai/configBase.mjs descriptor and PLANE_MEMBER_PATHS exclusion |
Base/cloud keeps the canonical /app/.neo-ai-data default. Dev binds the leaf in x-plane-env beneath /app/.neo-ai-data-parity; parity CI inherits that placement. The test Compose file is explicitly dispositioned as an isolated integration fixture, not a durable parity profile. |
A relocated durable profile without an explicit profile-pinned binding is rejected by static Compose coverage; it must not rely on the member-coherence walk, because this leaf is intentionally planeMember:false. |
Amend ADR 0019 profile matrix and inline leaf rationale. |
Parse rendered dev/base/test Compose; assert the dev orchestrator resolves the mirror root beneath the parity root and base remains canonical. |
| ADR 0019 sections 10.4-10.6 profile record |
ADR 0014 scheduler taxonomy; ADR 0019 plane identity and member coherence |
Record base/cloud as canonical, dev as relocated named-volume hybrid, parity CI as dev-derived ephemeral overlay, and test as an isolated fixture. Wake-delivery files stay on the resolved local plane; cloud has no local wake-delivery lane, so the file-freshness concern is moot there. Correct the ADR 0014 attribution. |
A future durable profile that relocates the plane must explicitly place every declared and profile-pinned member, then re-run coherence and Compose-shape evidence. |
Yes - ADR 0019 amendment. |
Source audit of ADR 0014 local-only lanes plus focused config/Compose tests. |
ai/deploy/docker-compose.dev.yml host publications |
#15800 AC-4 live election receipt; ADR 0019 section 10.6; #15990 / PR #16038 |
The default parity plane publishes KB on 127.0.0.1:3100, MC on 127.0.0.1:3101, and Chroma on 127.0.0.1:8100. 31xx is the MCP band and 81xx the engine band. Plane identity stays opaque; additional concurrent planes must declare distinct host publications rather than hash-derive ports from identity. MCP readiness verifies served {plane.id, plane.dataRoot}. |
An occupied host port fails at Compose bind; probePortClaims is the preflight observation. A Chroma heartbeat is connectivity only and never substitutes for MCP served-identity proof. |
Finalize the Compose election comments and ADR record. |
Extend ParityPlaneVolumeScoping.spec.mjs for Chroma loopback, elected comments, and served-identity healthcheck args; retain integration-parity identity coverage. |
| Repeated base-Compose path scalars |
#15800 AC-5; behavior-neutral implementation spec in issuecomment-5091518268 |
Declare scalar YAML anchors once for the repeated memory DB and handoff paths, then alias only the existing occurrences. Rendered Compose is unchanged. |
Any non-empty normalized docker compose config diff blocks the change; do not broaden this row into a full env-map merge. |
Inline Compose comments only. |
Before/after normalized base-Compose render is byte-identical; focused static test asserts one declaration and the expected aliases. |
| Downstream parity evidence |
Epic #15798; #15800 AC-6 |
Re-run plane config/coherence unit coverage and the mandatory integration-parity topology suite after the record and bindings land; append exact-head receipts here. |
Credentialed positive boot remains operator-owned when a real provider PAT is required; credential-free negative and static identity proofs remain agent-owned. |
Ticket receipt. |
Focused unit suite, npm run test-integration-parity, config lint, and exact-head CI. |
Closure scope
One coherent PR may now close this ticket: ADR 0019 profile/election record, the dev tenant-mirror binding, Chroma loopback publication, stale election-comment cleanup, behavior-neutral base scalar anchors, and focused regression coverage. This is the residual edge of the existing parity lane, not a new compose subsystem. AC-6 is completed by exact-head downstream receipts after that diff lands.
Context
Phase-0 decision leaf of epic #15798, from Discussion #15595's OQ10b re-pose: with plane identity declared (#15799), the placement question finally has a subject and becomes decidable. This leaf RUNS the election and records it durably — it exists because an undecided placement blocks the compose, cutover, and CI phases, and because deciding it by default-drift (whatever the next compose author writes) would recreate the per-compose convention gap the sweep flagged (three profiles today set three different subsets of the plane leaves; nothing asserts completeness).
The Problem
The election's branch asymmetry is quantified but undecided: under named-volume, 22 host-resident plane-openers each need a mount/contract/retirement and the wake fabric's file-freshness assumption (0.50 wakes/msg economics) changes latency class; under bind-mount, those cost zero but the symlink-escape class and N divergent per-seat splits persist (contained only by reconcile-mode). The body's 35–60 PR estimate is branch-dependent. macOS constraint: named volumes are host-invisible (VM-interior), so every host file-reader is priced under that branch.
The Architectural Reality
shared-sqlite-data); the local profile is the live fork.NEO_AI_CANONICAL_ROOTis deliberately not plane identity because a checkout-shaped identity "would silently pre-decide the data-root placement election."The Fix (one PR)
A recorded election: (1) consume #15791's measured cost rows per live seat (never hand-asserted); (2) fill the per-branch cost table (the fold-6 marker in #15595 carries the axes: host openers, symlink class, wake-latency envelope, host CLI, effort); (3) decide per deployment profile (the branches may legitimately differ local vs cloud — the plane-identity leaf makes that expressible); (4) record the decision + rationale + wake-latency envelope in the decision doc (ADR amendment section) and encode the elected defaults in the profile config; (5) port-band per
planeIdassigned as part of the election record (the #15367 served-identity discipline).Acceptance Criteria
e172767ce9. The previously-blocking22-row is withdrawn as not decision-relevant.planeIdrecorded; boot verifies served identity (not merely connect). — open, and the compose header agrees: the per-plane port band is an explicit provisional ELECTION-SLOT (lines 22-25) pending this record. §10.6 further requires observed identity reported per process, never host-derived.assertPlaneMemberCoherence, boot-wired); the completeness audit — what this AC asks for, and what §10.5 delegates to the election — did not. One profile binding its leaves viax-plane-envis one profile, not per-profile unification.Out of Scope
Compose file implementation (the dev-compose leaf); any host file-reader migration (per-site follow-ups); the rollout epic's manifest fields (cross-epic edge, wired when both DAGs exist).
Added 2026-07-26 — the
22 vs 38aggregate is RETIRED, not routed (@neo-opus-ada's disposition, better than my own): two people cannot hold a substantive disagreement about the value of an ill-formed quantity, and this one sums across three unlike cost classes — 4 resident daemon processes (persistent), 6 modules they load (no separate cost), 28 one-shotai/scripts(per-invocation, several operator-maintenance that may never run in a tenant).22 vs 38was an artifact of the summing, never a methodological dispute. Price per class; the host-reader migration tail inherits the three classes, not the number. This closes the item rather than parking it.Not owned here: the compose header routes its still-provisional ELECTION-SLOTs to
#15803, which is CLOSED. Either the header text is stale or the slots lost their tracker at close. @neo-opus-ada owns#15803and owns that reconciliation — it must not be absorbed into this ticket's amendment.Related
Epic #15798 · blocked_by #15799 + #15791 (native edges) · D#15595 folds 6/6.1 · D#15758 (manifest consumes the minted
planeId) · #15931 (canonical-root re-derivation / cross-plane write leak — fail-safe on symlinked seats, which is why a relocated plane makes it observable) · borrowed-authority caveat as in #15799.Origin Session ID: 758f110e-a429-4597-beb0-5fd3ec7d1bfc Retrieval Hint:
query_raw_memories("OQ10b placement election branch asymmetry measured cost rows")Intake alignment - 2026-07-30 (exact head
a500254d4fe480cd3fe4667d8a1c121a42843194)This section is the current execution contract. It supersedes the stale title premise and any older scope/status line above where they conflict, while preserving the historical corrections as provenance.
Ticket reality at intake
/app/.neo-ai-data-parity.node ai/scripts/diagnostics/planePlacementCensus.mjs --json: 62 openers (39 host-side / 19 in-server / 4 unclassified); the current seat has 16 leaves, 8 symlinks, 8 escapes, 0 dangling. These refresh the AC-1 receipt; the aggregate is evidence, not a branch selector.orchestrator.tenantRepoMirrorRoot: Tier 1 deliberately pins it to/app/.neo-ai-dataand excludes it fromPLANE_MEMBER_PATHS, while the dev profile relocates the rest of the plane to/app/.neo-ai-data-parity. The dev orchestrator can therefore boot green while writing tenant mirrors outside its declared plane.8100:8000binding; it must match the MCP services' IPv4-loopback boundary.x-plane-envdeclaration, while base Compose still repeatsNEO_MEMORY_DB_PATHthree times andNEO_HANDOFF_FILE_PATHtwice. Scalar anchors remove those two drift hazards with an empty rendered-config diff.Contract Ledger
orchestrator.tenantRepoMirrorRoot/NEO_TENANT_REPO_MIRROR_ROOTai/configBase.mjsdescriptor andPLANE_MEMBER_PATHSexclusion/app/.neo-ai-datadefault. Dev binds the leaf inx-plane-envbeneath/app/.neo-ai-data-parity; parity CI inherits that placement. The test Compose file is explicitly dispositioned as an isolated integration fixture, not a durable parity profile.planeMember:false.ai/deploy/docker-compose.dev.ymlhost publications127.0.0.1:3100, MC on127.0.0.1:3101, and Chroma on127.0.0.1:8100. 31xx is the MCP band and 81xx the engine band. Plane identity stays opaque; additional concurrent planes must declare distinct host publications rather than hash-derive ports from identity. MCP readiness verifies served{plane.id, plane.dataRoot}.probePortClaimsis the preflight observation. A Chroma heartbeat is connectivity only and never substitutes for MCP served-identity proof.ParityPlaneVolumeScoping.spec.mjsfor Chroma loopback, elected comments, and served-identity healthcheck args; retain integration-parity identity coverage.docker compose configdiff blocks the change; do not broaden this row into a full env-map merge.npm run test-integration-parity, config lint, and exact-head CI.Closure scope
One coherent PR may now close this ticket: ADR 0019 profile/election record, the dev tenant-mirror binding, Chroma loopback publication, stale election-comment cleanup, behavior-neutral base scalar anchors, and focused regression coverage. This is the residual edge of the existing parity lane, not a new compose subsystem. AC-6 is completed by exact-head downstream receipts after that diff lands.