LearnNewsExamplesServices
Frontmatter
id11871
titleEnv-primitive deduplication + shared MCP stderr logger
stateClosed
labels
epicairefactoringarchitecture
assigneesneo-opus-4-7
createdAtMay 24, 2026, 2:27 AM
updatedAtMay 27, 2026, 6:40 PM
githubUrlhttps://github.com/neomjs/neo/issues/11871
authorneo-opus-4-7
commentsCount1
parentIssuenull
subIssues
11873 Env-primitive deduplication: EnvConfig.mjs deletion + MCP migrations + Orchestrator scoped-binding
11878 Shared MCP stderr logger primitive
subIssuesCompleted2
subIssuesTotal2
blockedBy[]
blocking[]
closedAtMay 27, 2026, 6:40 PM

Env-primitive deduplication + shared MCP stderr logger

Closedepicairefactoringarchitecture
neo-opus-4-7
neo-opus-4-7 commented on May 24, 2026, 2:27 AM

Authored by: [Claude Opus 4.7] (Claude Code)

Graduated from: Discussion #11869 (cycle-2.2 §6.2 quorum: anthropic AUTHOR_SIGNAL + openai [GRADUATION_APPROVED] by @neo-gpt @ DC_kwDODSospM4BA_W0 + operator GO 2026-05-24).

Decision Record impact: none (no existing ADR addresses env-primitive ownership or MCP-server logger shape; this Epic establishes the foundational primitives; ADR 0014 Cloud Deployment Topology remains the deployment-target composability authority — preserved, not amended).

Context

V-B-A cascade earlier today surfaced 3 coexisting env-binding mechanisms with semantic divergence (EnvConfig.mjs + Neo.util.Env + ai/config/env.mjs), plus 5 per-server logger reimplementations. Cycle-1 Discussion #11869 framing ("single source of truth across all configs") was REJECTED by operator post-graduation as breaking deployment-target composability. Cycle-2 / cycle-2.2 narrow reframe preserves the existing deployment-subset substrate (docker-compose profiles + TARGET_SERVER build arg + per-container env injection + initServerConfigs.mjs) and addresses only the validated primitive-level drift.

The Problem

# Drift Impact
1 EnvConfig.mjs (ai/mcp/server/shared/helpers/, 83 LOC, strict parseBool) vs Neo.util.Env (src/util/Env.mjs, lifted via task #22 but EnvConfig never deleted — "3 partial extractions") Duplicate primitives + semantic divergence
2 ai/config/env.mjs (Sub 14 / PR #11868) introduced 3rd mechanism with runtime-mutable global Compounded drift
3 parseBool semantic divergence (strict 'true'/'false' vs permissive true/yes/on/1) Same env-var input has different truthiness depending on import path
4 5 per-server logger reimplementations Per-server-N-mechanisms; operator: "NO util.Logger inside ai scope. one stderr mcp logger"

The Architectural Reality

Cloud deployment topology already implements composability (per ai/deploy/docker-compose.yml + ADR 0014 + Sub B #11720): compose profiles select service subsets; TARGET_SERVER build arg + per-container environment: injection per service. github-workflow / gitlab-workflow / neural-link deliberately excluded from compose. No code-time merge across all servers is needed.

Consumer ledger:

  • Direct EnvConfig.mjs imports: knowledge-base, memory-core, neural-link, gitlab-workflow config.templates (4)
  • Indirect via BaseConfig.mjs: github-workflow
  • BaseConfig.mjs itself imports EnvConfig
  • initServerConfigs.spec.mjs asserts old EnvConfig import strings — must update
  • 5 per-server logger.mjs files with divergent sink/gating contracts:
    • github/gitlab-workflow: priority-filtered stderr, default warn
    • knowledge-base: daily-rotated file sink + debug-gated stderr
    • memory-core: same + flush() for short scripts
    • neural-link: daily-rotated file + tier-gated stderr (info/warn/error always write)

The Fix

Direct cutover, no shim (v13 unreleased per operator). Single Epic, 2 sub-tickets:

Sub 1 — Env-primitive dedup

  • Delete ai/mcp/server/shared/helpers/EnvConfig.mjs
  • Migrate 4 MCP server config.templates (knowledge-base, memory-core, neural-link, gitlab-workflow) + BaseConfig.mjs to Neo.util.Env.applyEnvBindings
  • Update initServerConfigs.spec.mjs assertions
  • Reconcile parseBool to permissive semantic (true/yes/on/1 / false/no/off/0); boolean-binding migration tests prove operator inputs valid under either old strict OR new permissive evaluate identically; semantic-change documented
  • Delete ai/config/env.mjs
  • Orchestrator getter chain stays pure 2-layer: each boolean getter becomes Env.parseBool(process.env.NEO_X, 'NEO_X') ?? resolveDeploymentEnabled(key) and each interval getter becomes Env.parseNumber(process.env.NEO_X, 'NEO_X') ?? AiConfig.orchestrator.intervals.X (parsed env layer inlined post-registry-deletion; raw process.env.X would reintroduce the boolean bug OQ1 fixes — per @neo-gpt cycle-2.2 implementation clarification)
  • Runtime auto-disable on swarm-heartbeat init failure refactors from env.NEO_X = false mutation to daemon-local instance field on the service (e.g., this.swarmHeartbeatService.initFailed = true), checked at poll() before calling pulse(). NOT a getter-chain layer. NOT env mutation. NOT AiConfig mutation.
  • dotenv/config placement: moves from deleted ai/config/env.mjs to orchestrator entrypoint (ai/scripts/orchestrator-daemon.mjs) deliberately

Sub 2 — Shared MCP stderr logger primitive

  • Create ONE shared stderr-based logger primitive at ai/mcp/server/shared/Logger.mjs (placement matches sibling BaseConfig.mjs / helpers/EnvConfig.mjsstructural pre-flight gate applies at implementation time per @neo-gpt cycle-2.2 reminder)
  • NO use of Neo.util.Logger inside ai/ scope. Neo.util.Logger (app/browser Neo.core.Base singleton, styled console.log, error() throws) stays for app/browser code; MCP-server side has different safety contracts (protocol-safe stderr-only, error() logs-not-throws)
  • Stderr-only (no stdout writes — MCP protocol safety invariant)
  • Migrate 5 per-server logger reimplementations to consume shared primitive
  • Behavior ledger AC: per-server config-driven sink/gating preserved as CONFIG, not separate primitives. Each server's config.template.mjs declares its own logger config (file prefix, log path, log level, error serialization, flush availability); shared primitive reads at boot
  • Deployment-subset model preserved: each container imports shared primitive + own server's config. NO central log-config catalog (same negative boundary as env per OQ4)

Acceptance Criteria

Sub 1 (env-primitive dedup):

  1. EnvConfig.mjs deleted; all 4 direct importers + BaseConfig.mjs migrated to Neo.util.Env.applyEnvBindings (grep-clean: zero EnvConfig.mjs references in ai/)
  2. Neo.util.Env.parseBool is the single canonical bool parser (permissive tokens true/yes/on/1 / false/no/off/0); migration tests prove dual-semantic compatibility for operator inputs
  3. ai/config/env.mjs deleted
  4. Orchestrator boolean getters become Env.parseBool(process.env.NEO_X, 'NEO_X') ?? resolveDeploymentEnabled(key); interval getters become Env.parseNumber(process.env.NEO_X, 'NEO_X') ?? AiConfig.orchestrator.intervals.X. Pure 2-layer chain; no runtime mutation of either layer
  5. Swarm-heartbeat init-failure auto-disable refactored to daemon-local instance field on the service; poll() checks the field before pulse()
  6. dotenv/config moved from deleted ai/config/env.mjs to orchestrator entrypoint
  7. initServerConfigs.spec.mjs assertions updated for new import strings
  8. All existing tests pass; no process.env.NEO_* direct (unparsed) reads in migrated consumers

Sub 2 (shared MCP stderr logger):

  1. New ai/mcp/server/shared/Logger.mjs (or structural-pre-flight-validated alternative placement) — ONE shared primitive
  2. Stderr-only output; error() logs (does NOT throw); no stdout writes (MCP protocol safety invariant)
  3. 5 per-server logger reimplementations deleted; all log call-sites migrated to shared primitive
  4. Per-server config-driven sink/gating preserved as CONFIG (file prefix, log path, log level, error serialization, flush availability); each server's config.template.mjs declares its logger config slot
  5. Per-server divergences captured as config differences:
    • github/gitlab-workflow: priority-filtered stderr, default warn, debug promotes verbosity
    • knowledge-base: daily-rotated always-on file sink + debug-gated stderr
    • memory-core: same file sink + logger.flush() for short-lived scripts
    • neural-link: daily-rotated file sink + tier-gated stderr (info/warn/error write without debug)
  6. NO central log-config catalog; each container imports shared primitive + own server's config (deployment-subset preservation; negative boundary parity with OQ4)
  7. NO use of Neo.util.Logger inside ai/ scope (grep-clean)
  8. All existing log call-site behavior preserved; per-server config tests cover the divergences

Out of Scope

(Each merits separate Discussion ONLY if later V-B-A surfaces real drift — do NOT pre-emptively spawn)

  • Top-level + per-server config merge contract (cycle-1 rejected; deployment-subset composition is deploy-time, not code-time)
  • Two-file .template → .mjs pattern at ai/ root
  • bootstrapWorktree.mjs scope extension to ai/config.template.mjs
  • npx neo-app workspaces JSON config path
  • Bare process.env.X || default reads in bridge/daemon.mjs / KbAlertingService.mjs / TaskDefinitions.mjs
  • Adapting Neo.util.Logger (src/util/Logger.mjs) for server-side use — explicitly OUT per operator direction

Avoided Traps

  • 3rd-mechanism extension (Sub 14's ai/config/env.mjs as permanent fixture) — resolved by AC 3
  • Strict parseBool retention — resolved by AC 2 permissive cutover
  • Unified-everything framing (cycle-1 rejected; broke deployment-target composability)
  • Neo.util.Logger reuse in ai/ — operator-rejected; app/browser logger has incompatible MCP semantics
  • Per-server logger primitives — replaced by shared primitive + per-server config (preserves divergent behavior without reimplementing)
  • Central NEO_* catalog OR central log-config catalog — explicit negative boundary preserves deployment-subset composability
  • Backwards-compat shim ceremony — operator-rejected per v13-unreleased

Discussion Criteria Mapping

Maps Discussion #11869 cycle-2.2 OQ resolutions → Epic ACs:

Discussion OQ Resolution Maps to Epic AC
OQ1 (parseBool) [RESOLVED_TO_AC] permissive Sub 1 AC 2
OQ2 (EnvConfig deletion strategy) [RESOLVED_TO_AC] direct cutover Sub 1 AC 1
OQ3 (ai/config/env.mjs disposition + runtime auto-disable) [RESOLVED_TO_AC] delete + pure 2-layer chain + daemon-local service field Sub 1 AC 3-6
OQ4 (per-server env isolation) [RESOLVED_TO_AC] per-server scoping; NO central catalog Sub 1 AC 1 + Sub 2 AC 6 (negative boundary parity)
OQ5 (bare process.env reads) [REJECTED_WITH_RATIONALE] out of scope Out of Scope section
OQ6 (prototype-pollution guard) [WITHDRAWN] per V-B-A commit b9ae97ebf N/A — withdrawn
OQ7 (per-server logger) [RESOLVED_TO_AC] shared MCP stderr primitive + per-server config Sub 2 AC 1-7

Signal Ledger

Family Identity Signal Anchor
anthropic @neo-opus-4-7 [AUTHOR_SIGNAL by @neo-opus-4-7 @ Discussion-11869-body-cycle-2.2] Discussion #11869 body
openai @neo-gpt [GRADUATION_APPROVED by @neo-gpt @ body-cycle-2.2 / DC_kwDODSospM4BA_W0] https://github.com/orgs/neomjs/discussions/11869#discussioncomment-DC_kwDODSospM4BA_W0

§6.2 quorum: ✓ (≥2 active families with signal + 1 non-author family [GRADUATION_APPROVED]).

Unresolved Dissent

(empty — operator's cycle-1 rejection ADDRESSED by cycle-2/cycle-2.2 reframe preserving deployment-target composability)

Unresolved Liveness

Family Identity Disposition
google @neo-gemini-3-1-pro Unavailable for ~1 month (operator-confirmed 2026-05-24). Floor-2 quorum reached without. Non-Tier-2 substrate; no revalidationTrigger AC required.

Authority

Discussion #11869 cycle-2.2 graduation (anthropic AUTHOR_SIGNAL + openai [GRADUATION_APPROVED] by @neo-gpt @ DC_kwDODSospM4BA_W0) + operator GO 2026-05-24.

tobiu closed this issue on May 27, 2026, 6:40 PM