Context
Surfaced by @tobiu while reviewing PR #15890, in two escalating steps.
Step 1 — the review catch. That PR declared its leaves as leaf(STOP_HOOK_DEFAULTS.x, STOP_HOOK_ENV.x, 'boolean'), sourcing a companion "pure-defaults twin" module, where the plain declarative form was correct: leaf(false, 'NEO_STOP_HOOK_LANE_CONTINUATION', 'boolean'). The twin was deleted in 3a6d8bfafc (−97 lines). The form itself is ADR-sanctioned — §10.1 literally prescribes leaf(PLANE_DEFAULTS.x, PLANE_ENV.x, 'string') — but only as an exception whose §10.2 gate is "serving no-Neo consumers", and the hooks are thread-entrypoints that read AiConfig.stopHook.* in ~50ms.
Step 2 — the challenge this ticket carries. @tobiu then asked whether "no-Neo consumers" can exist at all, since AiConfig is a Neo.state.Provider; and then, stepping back further: AiConfig was not always a Provider, so do we really need these two different exports?
Both questions survive contact with the evidence. This ticket is the amendment path.
The Problem
Finding 1 — the sanctioned audience is empirically empty
Census of every ai/planeConfig.mjs importer and the symbols each takes (excluding its own spec):
| Consumer |
Symbols taken |
ai/configBase.mjs |
PLANE_DEFAULTS, PLANE_ENV, parsePlaneIdEnv, resolvePlaneDataRoot |
ai/mcp/server/BaseServer.mjs |
assertPlaneCoherence, assertPlaneMemberCoherence, collectPlaneMembers, resolvePlaneDataRoot |
ai/mcp/server/neural-link/configBase.mjs |
resolvePlaneDataRoot |
ai/mcp/server/knowledge-base/configBase.mjs |
resolvePlaneDataRoot |
ai/mcp/server/memory-core/configBase.mjs |
resolvePlaneDataRoot |
The literal exports (PLANE_DEFAULTS, PLANE_ENV) have exactly ONE consumer: ai/configBase.mjs — the config file itself. Every other consumer takes only functions. There is no non-Neo consumer of the literals anywhere in the tree.
The literals are additionally used 9 times internally within planeConfig.mjs (:54, :87-88, :107, :115, :120, :159, :258-259), i.e. they are module-internal constants that were exported so one config file could declare leaves from them.
Finding 2 — §5.5's gate is incoherent as written, and §10.1 relocated authority without amending §2
§2 states: "AiConfig and its child providers are the single source of truth." §10.1 states the twin is "the leaf declarations' literal SOURCE." Both cannot hold. For twinned leaves the declaration authority lives in a plain module and AiConfig is its consumer — the inverse of §2.
The ADR conflates two distinct senses of SSOT and never separates them:
| Sense |
Owner |
| resolution — env layering, hierarchy chain, reactivity |
AiConfig, unambiguously |
| declaration — where the literal is written |
AiConfig, except twinned leaves, where it is the twin |
@tobiu's framing is the precise one: a consumer that cannot import Neo cannot read the SSOT at all, so the twin is not "serving a consumer of the SSOT" — it is a second source of truth for consumers the SSOT structurally excludes. Naming it a "pure-defaults companion" hides that.
Finding 3 — the gate is decidable by appearance, which is how a wrong answer passes review
§5.5 / §10.2 state the gate as a property of the consumer ("is it a no-Neo consumer?") when it is really a measured question: can this consumer bootstrap the namespace, and at what cost? PR #15890's hook looked like a no-Neo consumer — a bare script with no Neo import — and was one guarded dynamic import and ~50ms from the SSOT.
§5.5 gestures at exactly this ("should a frequent lightweight helper pay full-framework bootstrap weight to read a path?") and then leaves it as rhetoric: no threshold, no obligation to cite a number. The ADR's own author read it and still shipped the wrong shape — diligence is not the missing ingredient, a decidable gate is. This is the same empirical lesson §1 of the ADR already draws about PR #12420.
Finding 4 — the pattern is a pre-Provider fossil
Per @tobiu: AiConfig was not always a state Provider. Before it was, there was no resolved leaf to read, so every consumer needed its own resolver and the twin was the only available shape. Now that AiConfig is a Provider, the only legitimate remnant is the genuine chicken-and-egg named in §10.5: a leaf's default cannot read the Provider that does not exist yet, so a module-scope anchor computation is real. Everything else is fossil.
The Architectural Reality
ai/planeConfig.mjs — 260 lines, 9 exports: 2 frozen literal objects + 7 pure functions (isOpaquePlaneId, parsePlaneIdEnv, resolvePlaneId, resolvePlaneDataRoot, assertPlaneCoherence, collectPlaneMembers, assertPlaneMemberCoherence).
ai/configBase.mjs:5-10 — the sole literal consumer; declares the plane subtree at :85-92.
ai/mcp/server/BaseServer.mjs — takes validators only; they are wired at the head of runHealthcheckAndLogStatus() per §10.4.
learn/agentos/decisions/0019-aiconfig-reactive-provider-ssot.md — §5.5 (twin sanction), §10.1 (the inversion), §10.2 (twin resolvers are not A3), §10.5 (module-scope anchor). Status is still Draft ("Accepted on human merge of the implementing PR"), which makes this amendment cheaper than amending accepted authority — but it is treated as live authority by §critical_gates #10, so the amendment must be explicit rather than assumed.
ai/mcp/server/shared/helpers/seatToken.mjs, ai/services/fleet/fleetIngressAuth.mjs, ai/services/fleet/generateOpenCodeSeatConfig.mjs — matched a "pure-defaults / no Neo import" text sweep and must be classified during the work; they may be genuine module-scope-anchor cases, additional fossils, or unrelated.
The Fix (direction — implementer refines)
Collapse the two export kinds into one purposeful surface, then retire the subsections that licensed the pair.
Stop exporting the literals. They are already module-internal (9 uses). Export a single leaf-descriptor factory instead — planeLeafDescriptors() returning the plane subtree's descriptors — so configBase.mjs does plane: planeLeafDescriptors(). The literals never leave the module, the leaf shape and the resolvers share one definition, and §10.1's inversion becomes unnecessary rather than merely satisfied. (Alternative considered and rejected below: inlining the env-var names into the leaf, which re-creates the drift §10.1 exists to prevent, because the resolvers read the same names internally.)
Separate pure-function sharing from literal duplication in the doctrine. A Neo class importing assertPlaneCoherence is importing a helper, not consulting a second SSOT. §10.2's "the twin's resolvers are not A3" currently launders function-sharing into justifying literal-duplication; split the two claims.
Retire §5.5 / §10.1 / §10.2 and replace them with the module-scope-anchor rule they were approximating:
A leaf default computed at module scope may share constants with the module that computes it. Everything else reads the resolved leaf.
That sentence is decidable without judgment and would have rejected PR #15890's original shape on sight — a hook reads a resolved value; it does not compute a leaf default.
Reconcile §2. State that AiConfig is the resolution SSOT unconditionally, and — once (1) lands — the declaration SSOT too, with the module-scope anchor as the single named exception.
Classify the three text-sweep matches above as anchor-case / fossil / unrelated, and fold any fossils into this lane rather than leaving them to be discovered as precedent later.
Acceptance Criteria
Out of Scope
- PR #15890 itself. Its twin is already deleted; this ticket does not reopen it. Blocked-by #15890 only to avoid two lanes editing
ai/configBase.mjs's stopHook/plane region concurrently.
- Changing any leaf's resolved value, default, or env-var name. Declaration-site only; a behavior change here would be a separate ticket with an integration suite.
- The
ai/ A1/B1/B3 cleanup census (#12438 and friends). Same ADR, different antipattern group.
- Re-litigating whether AiConfig should be a
Neo.state.Provider. It should; that is the premise this ticket depends on, not questions.
ai/mcp/server/*/configBase.mjs per-server leaf inventories beyond the resolvePlaneDataRoot call site.
Avoided Traps
- Inlining the env-var names into the leaves. Superficially the "plain form" fix, but
resolvePlaneDataRoot and resolvePlaneId read PLANE_ENV.* internally, so inlining creates two sites for the same env name — exactly the drift §10.1 was written to prevent. The descriptor factory avoids both the duplication and the twin.
- Deleting
planeConfig.mjs wholesale. The validators and the module-scope anchor resolver are load-bearing: ADR §10.4's F-invariant boot assertion and §10.5's member-coherence clause depend on them, and ADR 0014's wake-lane freshness premise rides on plane members being where the declared plane says.
- Treating "Status: Draft" as licence to edit the ADR silently. It is turn-loaded authority via
§critical_gates #10; the amendment is explicit and seat-reviewed regardless of status.
- Asserting the fossil claim from the git log alone. The claim that the twin predates Provider-ness is @tobiu's, and the ticket treats it as the motivation; the load-bearing evidence is the consumer census in Finding 1, which stands independent of chronology.
Decision Record impact
amends ADR 0019 — retires §5.5, §10.1, §10.2; reconciles §2; preserves §10.4 and §10.5.
ADR successor-risk: adr-challenge — artifact #TBD dated 2026-07-25; ADR 0019 status Draft, §10 amended 2026-07-24 by @neo-fable-clio; evidence = the planeConfig.mjs consumer census (literal exports have 1 consumer, all in-config) plus PR #15890's measured 50ms entrypoint bootstrap; route amendment-required.
Related
- PR #15890 / #15877 — where the defect surfaced; commit
3a6d8bfafc removed the offending twin. Blocks this ticket.
- ADR 0019 §5.5 / §10.1 / §10.2 / §10.4 / §10.5; ADR 0019 §10 was itself an amendment (#15799 / PR #15811, @neo-fable-clio authoring, ADR author as falsification seat) — that is the precedent shape this ticket follows.
- Discussion #12453 (ADR 0019's graduation), #12420 (the superseded empirical anchor whose lesson — "a decidable gate beats reviewer diligence" — this ticket applies to the ADR itself).
- #12438 (A1 cleanup), #12435 (B4), #11976 (C3) — sibling antipattern lanes, not this one.
Live latest-open sweep: checked latest 20 open issues at 2026-07-25T09:50Z; nearest neighbours are #15842 and #15872 (both plane MEMBERSHIP reconciliation, not the export surface or the ADR subsections) — no equivalent found. A2A in-flight claim sweep at 2026-07-25T09:51Z over the latest messages, all read-states: no [lane-claim]/[lane-intent] on this scope; newest traffic is 2026-07-25T00:37Z (~9h stale, roster parked by operator).
Origin Session ID: cb8b1069-54cc-43fd-943f-0e20b45661c8
Retrieval Hint: query_raw_memories("planeConfig two exports pure-defaults twin no-Neo consumer empty audience ADR 0019 amendment declaration vs resolution SSOT")
Authored by Grace (@neo-opus-grace, Opus 5)
Context
Surfaced by @tobiu while reviewing PR #15890, in two escalating steps.
Step 1 — the review catch. That PR declared its leaves as
leaf(STOP_HOOK_DEFAULTS.x, STOP_HOOK_ENV.x, 'boolean'), sourcing a companion "pure-defaults twin" module, where the plain declarative form was correct:leaf(false, 'NEO_STOP_HOOK_LANE_CONTINUATION', 'boolean'). The twin was deleted in3a6d8bfafc(−97 lines). The form itself is ADR-sanctioned — §10.1 literally prescribesleaf(PLANE_DEFAULTS.x, PLANE_ENV.x, 'string')— but only as an exception whose §10.2 gate is "serving no-Neo consumers", and the hooks are thread-entrypoints that readAiConfig.stopHook.*in ~50ms.Step 2 — the challenge this ticket carries. @tobiu then asked whether "no-Neo consumers" can exist at all, since AiConfig is a
Neo.state.Provider; and then, stepping back further: AiConfig was not always a Provider, so do we really need these two different exports?Both questions survive contact with the evidence. This ticket is the amendment path.
The Problem
Finding 1 — the sanctioned audience is empirically empty
Census of every
ai/planeConfig.mjsimporter and the symbols each takes (excluding its own spec):ai/configBase.mjsPLANE_DEFAULTS,PLANE_ENV,parsePlaneIdEnv,resolvePlaneDataRootai/mcp/server/BaseServer.mjsassertPlaneCoherence,assertPlaneMemberCoherence,collectPlaneMembers,resolvePlaneDataRootai/mcp/server/neural-link/configBase.mjsresolvePlaneDataRootai/mcp/server/knowledge-base/configBase.mjsresolvePlaneDataRootai/mcp/server/memory-core/configBase.mjsresolvePlaneDataRootThe literal exports (
PLANE_DEFAULTS,PLANE_ENV) have exactly ONE consumer:ai/configBase.mjs— the config file itself. Every other consumer takes only functions. There is no non-Neo consumer of the literals anywhere in the tree.The literals are additionally used 9 times internally within
planeConfig.mjs(:54,:87-88,:107,:115,:120,:159,:258-259), i.e. they are module-internal constants that were exported so one config file could declare leaves from them.Finding 2 — §5.5's gate is incoherent as written, and §10.1 relocated authority without amending §2
§2 states: "AiConfig and its child providers are the single source of truth." §10.1 states the twin is "the leaf declarations' literal SOURCE." Both cannot hold. For twinned leaves the declaration authority lives in a plain module and AiConfig is its consumer — the inverse of §2.
The ADR conflates two distinct senses of SSOT and never separates them:
@tobiu's framing is the precise one: a consumer that cannot import Neo cannot read the SSOT at all, so the twin is not "serving a consumer of the SSOT" — it is a second source of truth for consumers the SSOT structurally excludes. Naming it a "pure-defaults companion" hides that.
Finding 3 — the gate is decidable by appearance, which is how a wrong answer passes review
§5.5 / §10.2 state the gate as a property of the consumer ("is it a no-Neo consumer?") when it is really a measured question: can this consumer bootstrap the namespace, and at what cost? PR #15890's hook looked like a no-Neo consumer — a bare script with no Neo import — and was one guarded dynamic import and ~50ms from the SSOT.
§5.5 gestures at exactly this ("should a frequent lightweight helper pay full-framework bootstrap weight to read a path?") and then leaves it as rhetoric: no threshold, no obligation to cite a number. The ADR's own author read it and still shipped the wrong shape — diligence is not the missing ingredient, a decidable gate is. This is the same empirical lesson §1 of the ADR already draws about PR #12420.
Finding 4 — the pattern is a pre-Provider fossil
Per @tobiu: AiConfig was not always a state Provider. Before it was, there was no resolved leaf to read, so every consumer needed its own resolver and the twin was the only available shape. Now that AiConfig is a Provider, the only legitimate remnant is the genuine chicken-and-egg named in §10.5: a leaf's default cannot read the Provider that does not exist yet, so a module-scope anchor computation is real. Everything else is fossil.
The Architectural Reality
ai/planeConfig.mjs— 260 lines, 9 exports: 2 frozen literal objects + 7 pure functions (isOpaquePlaneId,parsePlaneIdEnv,resolvePlaneId,resolvePlaneDataRoot,assertPlaneCoherence,collectPlaneMembers,assertPlaneMemberCoherence).ai/configBase.mjs:5-10— the sole literal consumer; declares theplanesubtree at:85-92.ai/mcp/server/BaseServer.mjs— takes validators only; they are wired at the head ofrunHealthcheckAndLogStatus()per §10.4.learn/agentos/decisions/0019-aiconfig-reactive-provider-ssot.md— §5.5 (twin sanction), §10.1 (the inversion), §10.2 (twin resolvers are not A3), §10.5 (module-scope anchor). Status is stillDraft("Accepted on human merge of the implementing PR"), which makes this amendment cheaper than amending accepted authority — but it is treated as live authority by§critical_gates#10, so the amendment must be explicit rather than assumed.ai/mcp/server/shared/helpers/seatToken.mjs,ai/services/fleet/fleetIngressAuth.mjs,ai/services/fleet/generateOpenCodeSeatConfig.mjs— matched a "pure-defaults / no Neo import" text sweep and must be classified during the work; they may be genuine module-scope-anchor cases, additional fossils, or unrelated.The Fix (direction — implementer refines)
Collapse the two export kinds into one purposeful surface, then retire the subsections that licensed the pair.
Stop exporting the literals. They are already module-internal (9 uses). Export a single leaf-descriptor factory instead —
planeLeafDescriptors()returning theplanesubtree's descriptors — soconfigBase.mjsdoesplane: planeLeafDescriptors(). The literals never leave the module, the leaf shape and the resolvers share one definition, and §10.1's inversion becomes unnecessary rather than merely satisfied. (Alternative considered and rejected below: inlining the env-var names into the leaf, which re-creates the drift §10.1 exists to prevent, because the resolvers read the same names internally.)Separate pure-function sharing from literal duplication in the doctrine. A Neo class importing
assertPlaneCoherenceis importing a helper, not consulting a second SSOT. §10.2's "the twin's resolvers are not A3" currently launders function-sharing into justifying literal-duplication; split the two claims.Retire §5.5 / §10.1 / §10.2 and replace them with the module-scope-anchor rule they were approximating:
That sentence is decidable without judgment and would have rejected PR #15890's original shape on sight — a hook reads a resolved value; it does not compute a leaf default.
Reconcile §2. State that AiConfig is the resolution SSOT unconditionally, and — once (1) lands — the declaration SSOT too, with the module-scope anchor as the single named exception.
Classify the three text-sweep matches above as anchor-case / fossil / unrelated, and fold any fossils into this lane rather than leaving them to be discovered as precedent later.
Acceptance Criteria
ai/planeConfig.mjsno longer exportsPLANE_DEFAULTS/PLANE_ENV; a single descriptor-shaped export servesconfigBase.mjs.ai/planeConfig.spec.mjsis updated to the new surface without losing coverage of the opacity invariant, the F-invariant boot assertion, or member coherence.node ai/scripts/lint/lint-config-template-ssot.mjsgreen, and the config-leaf parity snapshot shows no change to declared leaf paths (this is a declaration-site refactor, not a leaf change) — a parity delta means the refactor changed the resolved tree and must be re-scoped.Out of Scope
ai/configBase.mjs'sstopHook/planeregion concurrently.ai/A1/B1/B3 cleanup census (#12438 and friends). Same ADR, different antipattern group.Neo.state.Provider. It should; that is the premise this ticket depends on, not questions.ai/mcp/server/*/configBase.mjsper-server leaf inventories beyond theresolvePlaneDataRootcall site.Avoided Traps
resolvePlaneDataRootandresolvePlaneIdreadPLANE_ENV.*internally, so inlining creates two sites for the same env name — exactly the drift §10.1 was written to prevent. The descriptor factory avoids both the duplication and the twin.planeConfig.mjswholesale. The validators and the module-scope anchor resolver are load-bearing: ADR §10.4's F-invariant boot assertion and §10.5's member-coherence clause depend on them, and ADR 0014's wake-lane freshness premise rides on plane members being where the declared plane says.§critical_gates#10; the amendment is explicit and seat-reviewed regardless of status.Decision Record impact
amends ADR 0019— retires §5.5, §10.1, §10.2; reconciles §2; preserves §10.4 and §10.5.ADR successor-risk:
adr-challenge— artifact #TBD dated 2026-07-25; ADR 0019 statusDraft, §10 amended 2026-07-24 by @neo-fable-clio; evidence = theplaneConfig.mjsconsumer census (literal exports have 1 consumer, all in-config) plus PR #15890's measured 50ms entrypoint bootstrap; routeamendment-required.Related
3a6d8bfafcremoved the offending twin. Blocks this ticket.Live latest-open sweep: checked latest 20 open issues at 2026-07-25T09:50Z; nearest neighbours are #15842 and #15872 (both plane MEMBERSHIP reconciliation, not the export surface or the ADR subsections) — no equivalent found. A2A in-flight claim sweep at 2026-07-25T09:51Z over the latest messages, all read-states: no
[lane-claim]/[lane-intent]on this scope; newest traffic is 2026-07-25T00:37Z (~9h stale, roster parked by operator).Origin Session ID:
cb8b1069-54cc-43fd-943f-0e20b45661c8Retrieval Hint:
query_raw_memories("planeConfig two exports pure-defaults twin no-Neo consumer empty audience ADR 0019 amendment declaration vs resolution SSOT")Authored by Grace (@neo-opus-grace, Opus 5)