Context
Operator direction, 2026-08-16: src/util/Env.mjs does not relate to the Neo engine (Body) — it belongs to the Brain (ai/).
The file's own JSDoc already says so, in its first two paragraphs:
"Internal typed-parser registry for Neo.ai.ConfigProvider. … Not consumer-facing. These parsers are the env-decode layer that ConfigProvider wires into its meta-leaf registry … Application code does NOT call Env.parseX directly."
So the file documents itself as Brain-internal and explicitly not consumer-facing — while src/util/_export.mjs:5 publishes it into the public Neo.util.* namespace. The placement contradicts the file's own stated contract.
Live latest-open sweep: checked the latest 22 open issues (created-descending) at 2026-08-16T12:0xZ — no equivalent found. Nearest adjacent are #17217 (plane predicates) and #17158 (tenant-sync knobs); neither concerns Env.mjs placement. A2A in-flight sweep over the 30 most recent messages found no [lane-claim]/[lane-intent] on this scope.
The Problem
Env.mjs was lifted to src/util/ by #11832 ("Neo.util.Env THIN substrate primitive") under the #11870 unified config/env consolidation. At that time the framing was "a thin Neo substrate primitive." What the codebase actually grew into is narrower: exactly one runtime consumer, and it lives in the Brain.
Verified importers (excluding tests and archived ticket JSON):
| importer |
kind |
ai/ConfigProvider.mjs:4 |
the only real runtime consumer |
src/util/_export.mjs:5 |
the barrel that publishes it as public API |
Nothing in src/, apps/, or the framework runtime calls Env.parseX. A grep for Neo.util.Env outside ai/ and test/ returns only the file's own log strings, ADR-0019's prose, and archived ticket JSON — zero framework or app consumers.
The cost of the current placement is not a runtime bug. It is that the engine's public API surface advertises a symbol whose own documentation tells callers not to use it, and a reader auditing Neo.util.* has to open the file to learn it is not part of the engine.
The Architectural Reality
Per ADR-0018 OD-3, /src/ is the Body (engine + Possession Interface) and /ai/ is the Brain (Agent OS). Env.mjs sits in the Body and serves only the Brain.
Three structural facts the fix must respect:
Env.mjs depends on the Neo global. Neo.isEmpty at :51, :73, :97, :119, :137, and export default Neo.gatekeep(Env, 'Neo.util.Env') at :211. Relocating does not make it Neo-free.
- This does not sever an
ai/ → src/ dependency, and must not be framed as if it does. ai/ConfigProvider.mjs:3 already imports ../src/state/Provider.mjs and is @extends Neo.state.Provider. The Brain legitimately consumes Body primitives. What changes is who owns the symbol and whether it is published, not the dependency direction.
- ADR-0019 C1 ("NEO imports ONLY in thread-entrypoints") is adjacent but not violated either way:
ConfigProvider is a Neo class by construction, so a Neo-global-using sibling in ai/ is consistent with how ai/ConfigProvider.mjs, ai/configBase.mjs and ai/planeConfig.mjs already operate. ADR-0019 does not pin Env.mjs's location (zero matches for Env.mjs / src/util in that ADR).
Sibling precedent for the destination — ai/ root already holds the config layer: ConfigProvider.mjs, configBase.mjs, config.mjs, config.template.mjs, embeddingProviders.mjs, embeddingSafeBand.mjs, planeConfig.mjs, providerLaneLiveShape.mjs. ai/Env.mjs is a direct sibling-pattern match to ai/ConfigProvider.mjs, which is its sole consumer.
The Fix
- Move
src/util/Env.mjs → ai/Env.mjs.
- Remove
Env from src/util/_export.mjs (both the import at :5 and the export {} list) — this is the actual API-surface change.
- Update
ai/ConfigProvider.mjs:4 to import Env from './Env.mjs'.
- Re-namespace:
@namespace Neo.util.Env → Neo.ai.Env, and update the Neo.gatekeep(Env, '…') id at :211 plus the six [Neo.util.Env] warn-prefix strings.
- Move
test/playwright/unit/util/Env.spec.mjs → test/playwright/unit/ai/Env.spec.mjs and fix its import path.
- Update the remaining test path references:
test/playwright/unit/ai/config.template.spec.mjs:11, test/playwright/unit/ai/ConfigProvider.spec.mjs:49, and the initServerConfigs.spec.mjs fixtures (:245–:858, which carry src/util/Env.mjs as string literals inside import-drift fixtures — these are data, not imports, and are easy to miss with an IDE rename).
- Update the JSDoc prose in
ai/scripts/setup/initServerConfigs.mjs:340–:347 which cites the old path in examples.
- Update ADR-0019's
Neo.util.Env mention at :34 to the new namespace.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
Neo.util.Env (public export via src/util/_export.mjs:5) |
src/util/_export.mjs export list |
Removed from the public Neo.util.* namespace |
None — no deprecation shim; zero external consumers found |
ADR-0019 :34 mention updated |
grep for Neo.util.Env / util.Env across src/, apps/, learn/ returns only self-references, ADR prose, archived JSON |
Neo.ai.Env (new) |
ai/Env.mjs |
Brain-owned namespace, unexported from the framework barrel |
n/a |
file JSDoc + ADR-0019 :34 |
new location is a direct sibling of its sole consumer ai/ConfigProvider.mjs |
ai/ConfigProvider.mjs leaf-type parser wiring |
ai/ConfigProvider.mjs:4, :7 |
Import path changes only; parser behavior unchanged |
n/a |
JSDoc :7 type-token map |
ConfigProvider.spec.mjs + config.template.spec.mjs must stay green unmodified except for the path |
Decision Record impact
aligned-with ADR 0018 (Body/Brain separation — OD-3 two-hemisphere topology) and aligned-with ADR 0019 (the AiConfig SSOT: this relocates the env-decode layer next to the Provider that owns it, and changes no resolution semantics). No ADR is amended, superseded, or challenged. ADR-0019 needs one prose touch at :34 for the namespace rename, not a decision change.
Acceptance Criteria
Out of Scope
- No parser behavior changes. No new types, no token-set edits, no validation changes. Pure relocation + rename.
- No deprecation shim / re-export stub at
src/util/Env.mjs. Zero external consumers were found; a shim would preserve exactly the public-surface problem this ticket removes.
- No change to
ai/ConfigProvider.mjs's dependency on src/state/Provider.mjs. The Brain→Body dependency is legitimate and stays.
- No attempt to make
Env.mjs Neo-global-free. It uses Neo.isEmpty and Neo.gatekeep; that is consistent with its ai/ siblings and is not this ticket's concern.
- Archived ticket/PR JSON under
apps/portal/resources/data/** and resources/content/** is historical record and must not be rewritten.
Avoided Traps
- Framing this as "decouple ai/ from src/". It is not.
ConfigProvider extends Neo.state.Provider; the dependency direction is unchanged and correct. The defect is a published namespace, not a dependency.
- IDE-rename-only migration.
initServerConfigs.spec.mjs carries '../../../../src/util/Env.mjs' as string literals inside import-drift test fixtures (:245, :253, :278, :369, :402, :417, :424, :467, :482–:493, :537, :853, :858). A symbol rename will not touch them, and they assert on the literal path — they will fail, or worse, silently keep asserting a dead path.
- Leaving a re-export in
_export.mjs "for safety". That keeps Neo.util.Env public and defeats the entire ticket.
- Reopening
#11832's decision as a mistake. It was a reasonable call under the "thin substrate primitive" framing at the time; what changed is that the consumer set resolved to exactly one Brain-side caller. Record it as lineage, not as a reversal.
Related
#11832 — Sub 6: Neo.util.Env THIN substrate primitive (lift EnvConfig.mjs parsers to src/util/Env.mjs) — the origin of the current placement.
#11871 / #11873 — Env-primitive deduplication (EnvConfig.mjs deletion, MCP migrations).
#11870 — Unified config + env substrate consolidation (parent framing).
- ADR-0018 (Body/Brain two-hemisphere topology), ADR-0019 (AiConfig reactive-provider SSOT).
Handoff Retrieval Hints
query_raw_memories: "Neo.util.Env placement Body Brain src/util ai relocation"
- Structural anchors:
src/util/_export.mjs:5, src/util/Env.mjs:211, ai/ConfigProvider.mjs:4, learn/agentos/decisions/0019-aiconfig-reactive-provider-ssot.md:34
Origin Session ID: 3f264a19-c7d4-481e-bc80-5c288bca177f
⚖️ Ada · @neo-opus-ada · Claude Opus 5 · Claude Code
Context
Operator direction, 2026-08-16:
src/util/Env.mjsdoes not relate to the Neo engine (Body) — it belongs to the Brain (ai/).The file's own JSDoc already says so, in its first two paragraphs:
So the file documents itself as Brain-internal and explicitly not consumer-facing — while
src/util/_export.mjs:5publishes it into the publicNeo.util.*namespace. The placement contradicts the file's own stated contract.Live latest-open sweep: checked the latest 22 open issues (created-descending) at 2026-08-16T12:0xZ — no equivalent found. Nearest adjacent are
#17217(plane predicates) and#17158(tenant-sync knobs); neither concernsEnv.mjsplacement. A2A in-flight sweep over the 30 most recent messages found no[lane-claim]/[lane-intent]on this scope.The Problem
Env.mjswas lifted tosrc/util/by#11832("Neo.util.Env THIN substrate primitive") under the#11870unified config/env consolidation. At that time the framing was "a thin Neo substrate primitive." What the codebase actually grew into is narrower: exactly one runtime consumer, and it lives in the Brain.Verified importers (excluding tests and archived ticket JSON):
ai/ConfigProvider.mjs:4src/util/_export.mjs:5Nothing in
src/,apps/, or the framework runtime callsEnv.parseX. A grep forNeo.util.Envoutsideai/andtest/returns only the file's own log strings, ADR-0019's prose, and archived ticket JSON — zero framework or app consumers.The cost of the current placement is not a runtime bug. It is that the engine's public API surface advertises a symbol whose own documentation tells callers not to use it, and a reader auditing
Neo.util.*has to open the file to learn it is not part of the engine.The Architectural Reality
Per ADR-0018 OD-3,
/src/is the Body (engine + Possession Interface) and/ai/is the Brain (Agent OS).Env.mjssits in the Body and serves only the Brain.Three structural facts the fix must respect:
Env.mjsdepends on theNeoglobal.Neo.isEmptyat:51,:73,:97,:119,:137, andexport default Neo.gatekeep(Env, 'Neo.util.Env')at:211. Relocating does not make it Neo-free.ai/ → src/dependency, and must not be framed as if it does.ai/ConfigProvider.mjs:3already imports../src/state/Provider.mjsand is@extends Neo.state.Provider. The Brain legitimately consumes Body primitives. What changes is who owns the symbol and whether it is published, not the dependency direction.ConfigProvideris a Neo class by construction, so a Neo-global-using sibling inai/is consistent with howai/ConfigProvider.mjs,ai/configBase.mjsandai/planeConfig.mjsalready operate. ADR-0019 does not pinEnv.mjs's location (zero matches forEnv.mjs/src/utilin that ADR).Sibling precedent for the destination —
ai/root already holds the config layer:ConfigProvider.mjs,configBase.mjs,config.mjs,config.template.mjs,embeddingProviders.mjs,embeddingSafeBand.mjs,planeConfig.mjs,providerLaneLiveShape.mjs.ai/Env.mjsis a direct sibling-pattern match toai/ConfigProvider.mjs, which is its sole consumer.The Fix
src/util/Env.mjs→ai/Env.mjs.Envfromsrc/util/_export.mjs(both theimportat:5and theexport {}list) — this is the actual API-surface change.ai/ConfigProvider.mjs:4toimport Env from './Env.mjs'.@namespace Neo.util.Env→Neo.ai.Env, and update theNeo.gatekeep(Env, '…')id at:211plus the six[Neo.util.Env]warn-prefix strings.test/playwright/unit/util/Env.spec.mjs→test/playwright/unit/ai/Env.spec.mjsand fix its import path.test/playwright/unit/ai/config.template.spec.mjs:11,test/playwright/unit/ai/ConfigProvider.spec.mjs:49, and theinitServerConfigs.spec.mjsfixtures (:245–:858, which carrysrc/util/Env.mjsas string literals inside import-drift fixtures — these are data, not imports, and are easy to miss with an IDE rename).ai/scripts/setup/initServerConfigs.mjs:340–:347which cites the old path in examples.Neo.util.Envmention at:34to the new namespace.Contract Ledger Matrix
Neo.util.Env(public export viasrc/util/_export.mjs:5)src/util/_export.mjsexport listNeo.util.*namespace:34mention updatedNeo.util.Env/util.Envacrosssrc/,apps/,learn/returns only self-references, ADR prose, archived JSONNeo.ai.Env(new)ai/Env.mjs:34ai/ConfigProvider.mjsai/ConfigProvider.mjsleaf-type parser wiringai/ConfigProvider.mjs:4,:7:7type-token mapConfigProvider.spec.mjs+config.template.spec.mjsmust stay green unmodified except for the pathDecision Record impact
aligned-with ADR 0018(Body/Brain separation — OD-3 two-hemisphere topology) andaligned-with ADR 0019(the AiConfig SSOT: this relocates the env-decode layer next to the Provider that owns it, and changes no resolution semantics). No ADR is amended, superseded, or challenged. ADR-0019 needs one prose touch at:34for the namespace rename, not a decision change.Acceptance Criteria
ai/Env.mjsexists;src/util/Env.mjsno longer exists.src/util/_export.mjscontains noEnvimport and noEnvin its export list.grep -rn 'src/util/Env.mjs'across the repo (excludingresources/content/**archives andapps/portal/resources/data/**) returns zero hits — including the string-literal fixtures ininitServerConfigs.spec.mjs.grep -rn 'Neo.util.Env'returns zero hits outsideresources/content/**archives.Neo.ai.Env.Env.spec.mjspasses at its new location with no assertion edits — only the import path and file location change. An assertion edit in this PR is a scope violation and should fail review.ConfigProvider.spec.mjsandconfig.template.spec.mjspass unmodified except for import paths.leaf(default, 'NEO_X', type)still resolves its env override identically. Verify by running the AiConfig unit suite, not by inspection.Out of Scope
src/util/Env.mjs. Zero external consumers were found; a shim would preserve exactly the public-surface problem this ticket removes.ai/ConfigProvider.mjs's dependency onsrc/state/Provider.mjs. The Brain→Body dependency is legitimate and stays.Env.mjsNeo-global-free. It usesNeo.isEmptyandNeo.gatekeep; that is consistent with itsai/siblings and is not this ticket's concern.apps/portal/resources/data/**andresources/content/**is historical record and must not be rewritten.Avoided Traps
ConfigProviderextendsNeo.state.Provider; the dependency direction is unchanged and correct. The defect is a published namespace, not a dependency.initServerConfigs.spec.mjscarries'../../../../src/util/Env.mjs'as string literals inside import-drift test fixtures (:245,:253,:278,:369,:402,:417,:424,:467,:482–:493,:537,:853,:858). A symbol rename will not touch them, and they assert on the literal path — they will fail, or worse, silently keep asserting a dead path._export.mjs"for safety". That keepsNeo.util.Envpublic and defeats the entire ticket.#11832's decision as a mistake. It was a reasonable call under the "thin substrate primitive" framing at the time; what changed is that the consumer set resolved to exactly one Brain-side caller. Record it as lineage, not as a reversal.Related
#11832— Sub 6:Neo.util.EnvTHIN substrate primitive (liftEnvConfig.mjsparsers tosrc/util/Env.mjs) — the origin of the current placement.#11871/#11873— Env-primitive deduplication (EnvConfig.mjsdeletion, MCP migrations).#11870— Unified config + env substrate consolidation (parent framing).Handoff Retrieval Hints
query_raw_memories:"Neo.util.Env placement Body Brain src/util ai relocation"src/util/_export.mjs:5,src/util/Env.mjs:211,ai/ConfigProvider.mjs:4,learn/agentos/decisions/0019-aiconfig-reactive-provider-ssot.md:34Origin Session ID: 3f264a19-c7d4-481e-bc80-5c288bca177f
⚖️ Ada ·
@neo-opus-ada· Claude Opus 5 · Claude Code