Frontmatter
| title | feat(ai): aiConfig meta-leaf tree + Provider-based BaseConfig (#12103) |
| author | neo-opus-ada |
| state | Merged |
| createdAt | May 29, 2026, 5:23 AM |
| updatedAt | May 29, 2026, 11:35 AM |
| closedAt | May 29, 2026, 11:35 AM |
| mergedAt | May 29, 2026, 11:35 AM |
| branches | dev ← claude/serene-elgamal-5add8c |
| url | https://github.com/neomjs/neo/pull/12164 |
🚨 Agent PR Body Lint Violation
@neo-opus-ada — your PR body on PR #12164 does not match the pull-request template structure.
Required action: read .agents/skills/pull-request/SKILL.md BEFORE editing the PR body. The skill points at:
- Minimum-viable PR body structure:
.agents/skills/pull-request/references/pull-request-workflow.md §9 - Self-Identification mandate:
.agents/skills/pull-request/references/pull-request-workflow.md §5
Do NOT compose a substitute template or hallucinate section headings. The validator checks more structural anchors than this comment names. The only reliable path to passing is reading the actual workflow file and following its structure.
Diagnostic hint: visible anchors appear present but the structural template anchors do not.
Visible anchors missing (full list)
(none — visible layer passed; invisible structural layer caught the miss)
This is the CI tool-boundary lint companion to PR #11494's MCP manage_pr_review validator and PR #11502's agent-pr-review-body-lint.yml reviewer-side lint.
Resolves #11501.

Input from Claude Opus 4.8 (Claude Code):
❋ ## Review response — core.Base method-shadowing (fixed in ede3e6c43)
Two real, dangerous gaps confirmed + fixed:
1.
set()shadowedcore.Base#set(values)(the canonical batch config setter). Removed — the data-mutation primitive is the inheritedsetData(path, value)(Provider). Option F's "set" sub-point →setData.2.
observeConfig()shadowedcore.Base#observeConfig(publisher, configName, fn)— andProvider#createBindingcalls it internally (Provider.mjs:270,isDestroyingcleanup). My override would have broken Provider's own binding teardown. Renamed toobserveData(path, fn). Option F's "observeConfig" sub-point →observeData.Added a regression-guard spec asserting neither core method is shadowed. Root cause: I leaned on research-subagent summaries of Provider's data/reactivity mechanics instead of reading
core.Base.mjs+Provider.mjs's full inherited public-method surface myself. The original specs passed because they encoded my (wrong) model — they tested the shadows.Known limitations (transparent — your call on in-scope-here vs follow-up):
createConfigProxyimplementsget/setbut notownKeys/has/getOwnPropertyDescriptor, soObject.keys(aiConfig)/'x' in aiConfig/ spread reflect instance members, not data leaves. (The nativecreateHierarchicalDataProxyuses an empty target so it can freely report data keys; matching that on an instance-wrapping proxy runs into proxy-invariant constraints. No current consumer enumeratesaiConfig— they readaiConfig.X.Y.)Provider#constructsetsNeo.isUsingStateProviders = true(inherited) — a UI-coupling side-effect for a Node/MCP config. Harmless in the MCP runtime; inherent to the extend-Provider mandate.internalSetDatavalidation (Design B) is lenient type-validation; polymorphic leaves likekeep_alive(Number|String) emit a benignexpected Number, got String; keeping valuewarning. Design A (Env value-coercers) would resolve it.Provider#toJSON()returnsdata: me.data(the reactive proxy);JSON.stringify(aiConfig)would traverse it. Inherited Provider behavior; out of scope here.CI re-running on ede3e6c43.
Resolves #12103 Resolves #12104 Related: #12101
Authored by Claude Opus 4.8 (Claude Code). Session efd8dc2e-2052-4089-814a-ab22cd8c6a62.
FAIR-band: in-band — operator-directed lane (Epic #12101 execution requested directly by @tobiu).
Stages 1 + 2 of the aiConfig greenfield epic, landed together because they are build-coupled: rewriting
BaseConfigbreaks the liveconfig.templatefiles until the templates migrate, so shipping them apart would leave an intermediate broken build.ai/BaseConfig.mjsnow extendsNeo.state.Providerinstead of hand-rolling a Proxy overcore.Base, and the sixconfig.template.mjsfiles (Tier-1 + 5 MCP servers) collapse the paralleldefaultConfig+envBindingstrees into a singlemetaTreeof{env, default, parse}leaves. Consumer-side cleanup (Stages 3–6) is deferred to follow-ups — the new proxy preserves read/write-through andNeo.util.Envis untouched, so existing consumers keep working.Evidence: L2 (new BaseConfig.spec 9/9 + full
ai/unit suite at dev-baseline parity, zero net-new failures) → L1 required (config-shape contract, unit-covered). Runtime MCP-server restart + operatorconfig.mjsregen is post-merge follow-up (below).Deltas from ticket
internalSetDatachoke point (metadata-path-first type-validation, noEnv.mjsrefactor) over Design A (extract pure value-coercers fromEnv) — flagged for reviewer; resolves a deferred Stage-1 OQ.Env.mjskeep existing consumers working, so cleanup is non-blocking.What shipped
ai/BaseConfig.mjs extends Neo.state.Provider.compileMetaLeaves(tree → reactivedata+ a separate#leafMetadataRegistry),internalSetDatavalidation override (metadata-path-first),set/setEnvOverride/ path-scopedobserveConfig, construction-time env-layer, adaptedcreateConfigProxy(read-through + set-trap →setData, bound to the real instance). Newtest/playwright/unit/ai/BaseConfig.spec.mjs(9 cases).config.template.mjs→metaTree; coupled specs updated to the new API.Design decision (flagged for review — resolves a deferred OQ)
Parser-validation at the
internalSetData()choke point is Design B: metadata-path-first type-validation (lenient, warn-on-mismatch).Neo.util.Envparsers stay env-readers for the construction-time env-overlay; noEnv.mjsrefactor, keeping Stage 1 scoped. The alternative (Design A: extract pure value-coercers fromEnvand coerce at the choke point) is a larger cross-Env.mjschange — reviewer's call on upgrade-now vs. defer.Body-tier lineage (AC-Epic-4)
Extends, does not reinvent:
Neo.state.Provider(#6131 base / #6965 + #6973createHierarchicalDataProxy/ #6974 Effect reactivity / #6981 set-trap→setData / #6985 deep-mergedata_). PR-review checkpoint (grepsrc/state/before accepting new machinery): satisfied — no parallel reactivity added.MCP config-template change (per mcp-config-template-change-guide)
defaultConfig+envBindingsparallel trees are reshaped into onemetaTreeper leaf; all values + env-var bindings + parsers are preserved (verified via per-template leaf-count parity + compiled-defaults deep-equality).config.mjsfollow-up (REQUIRED after merge): each clone's gitignoredconfig.mjsoverlay is stale (old shape) and must be regenerated from the new templates —node ai/scripts/setup/initServerConfigs.mjs(auto-runs vianpm ci/prepare; pass--migrate-configwhen an overlay already exists). Graceful overlay-advancement is Stage 0 (#12102).config.mjsneed a restart.config.mjscommitted (gitignored; CI regenerates from templates viaprepare).Consensus / Signal Ledger (§6.1.1 — Discussion #12100 graduation)
[GRADUATION_APPROVED_INHERITS_WITH_STAGE1_REFINEMENT]— 8+ anchored confirmations v6→v14[GRADUATION_DEFERRED]converging (OQ8 + OQ9 landed; not quorum-required)Unresolved Dissent: none. Unresolved Liveness: none. Epic #12101 body now carries the full
## Discussion Criteria Mapping(6 rows) per @neo-gpt's epic-review.Test Evidence
npm run test-unit -- test/playwright/unit/ai/BaseConfig.spec.mjs→ 9/9 (caught + fixed a real first-cutobserveConfigsubscribe-id bug).npm run test-unit -- test/playwright/unit/ai/→ 2074 passed / 15 failed; the 15 are pre-existing flaky env/DB specs (checkSunsetted, TextEmbedding, SessionSummarization, backup — need live Ollama/Chroma/populated DB), identical to the dev baseline (16–17, same set). Zero net-new failures — verified by stashing this branch and running the same subtree on clean dev.node --checkon all 6 templates +BaseConfig.mjs;git diff --checkclean; lint-staged (whitespace + shorthand) passed at commit.dist/parse5.mjsfirst, which this worktree could not).Architectural findings (FYI for reviewer — tracked separately)
Neo.state.Providersemantics surfaced during spec migration: writingundefined/nullto a parent leaf is a one-way door (reactivity-bubbling breaks on a non-object parent);deleteon the hierarchical proxy is a no-op (nodeletePropertytrap);Config.set(undefined)is ignored. Specs were adapted to fresh-instance / explicit-nullpatterns.config.template.mjsandconfig.mjsregister the same className → collide inunitTestMode. Tests assert the template (stable, predictable) via text-read or freshNeo.create(BaseConfig, {metaTree})instances, never the user-mutableconfig.mjs.Deferred follow-ups (Epic #12101)
S3 #12105 (orchestrator delegation-getter codemod), S4 #12106 (services / MCP consumer codemod), S5 #12107 (retract
Env.mjsconsumer exports), S6 #12108 (delete legacyapplyEnv()), S0 #12102 (initServerConfigs overlay-merge).Post-Merge Validation
config.mjs(npm run prepare/initServerConfigs.mjs) + restarts MCP servers.