Premise
Operator-direct audit at 2026-05-27T15:50Z surfaced systematic config/env-pattern violations in the orchestrator surface, predating the foundational NO HIDDEN DEFAULT VALUE FALLBACKS IN CODE contract enforcement (PR #12061 cycle-4 anchor). The orchestrator was authored before the config-as-SSOT envBindings discipline was codified; with the contract now established, the existing patterns are debt.
Audit verbatim from operator: "are you still aware of our config and env var patterns? this one did not get created inside this session. however, it is not the only spot. the orchestrator needs a review."
Audit findings
Quantified across ai/daemons/orchestrator/:
Pattern 1 — Env.parse*('NEO_X') ?? AiConfig.X getters (29 instances in Orchestrator.mjs:391-445+)
Per-access env probe + ?? fallback to config-default in code, bypassing the envBindings mechanism entirely. Examples:
get pollIntervalMs() { return Env.parseNumber('NEO_ORCHESTRATOR_POLL_INTERVAL_MS') ?? AiConfig.orchestrator.intervals.pollMs; }
get kbSyncEnabled() { return Env.parseBool('NEO_ORCHESTRATOR_KB_SYNC_ENABLED') ?? resolveDeploymentEnabled('kbSyncEnabled'); }
get mlxEnabled() { return Env.parseBool('NEO_ORCHESTRATOR_MLX_ENABLED') ?? !!AiConfig.orchestrator.mlx?.enabled; }
The envBindings.orchestrator.* declaration in ai/config.template.mjs is the canonical surface for env-var override; runtime getters compete with that and create split source of truth.
Pattern 2 — Resolver functions (5 instances)
| File |
Function |
Issue |
Orchestrator.mjs:94 |
resolvePrimaryDevSyncRootsConfig({envValue, configValue}) |
Custom env-vs-config precedence resolver — operator's anchor example |
Orchestrator.mjs:112 |
resolvePrimaryDevSyncRootsSource({envValue}) |
Returns env-var-name vs config-key-name — observability noise that should live at the consumer or in config |
Orchestrator.mjs:129 |
resolveDeploymentEnabled(key) |
Encodes null-means-deployment-default semantics in code; the operator-facing config template shows null but the runtime value comes from a code path the template doesn't expose |
Orchestrator.mjs:144 |
resolveCloudOnlyEnabled(key) |
Inverse of above |
MaintenanceBackpressureService.mjs:119 |
resolveHeavyMaintenanceLeasePath({heavyMaintenanceLeasePath, dataDir}) |
Should be a config-template getter or a clean constant per dataDir |
Pattern 3 — Direct process.env.X || hardcoded (SwarmHeartbeatService.mjs:60)
return process.env.NEO_HEARTBEAT_ALIVE_PATH || path.resolve(__dirname, '../../../../.neo-ai-data/wake-daemon/heartbeat.alive')
Bypasses both config template + env binding parser. Hardcoded fallback path is exactly the || substitution the operator contract bans.
Pattern 4 — Default-parameter env reads (PrimaryRepoSyncService.mjs:159, 215)
@param {String[]|String|undefined|null} [options.devSyncRootsConfig=process.env.NEO_ORCHESTRATOR_DEV_SYNC_ROOTS]
Even in JSDoc the env read is the default — same anti-pattern at the function-signature layer.
Prescription
Single coherent PR collapsing all four patterns into config-as-SSOT compliance:
- Add missing
envBindings.orchestrator.* in ai/config.template.mjs for all 29 env vars currently read by Orchestrator getters (intervals.* + tenantRepoSync.{sweepCadenceMs,jitterRatio} + dream/goldenPath/swarmHeartbeat intervals + swarmHeartbeat.{identity,targetSource,targets} + localOnly/cloudOnly enabled flags + mlx.{enabled,model,port} + lms.{enabled,model,port}).
- Delete the 29 getters from
Orchestrator.mjs. Consumers read AiConfig.orchestrator.X.Y directly (env-applied at config-load time).
- Collapse
resolveDeploymentEnabled + resolveCloudOnlyEnabled — replace with config-template getters (or store the deployment-default-resolved value at config load, not at every access).
- Fix
SwarmHeartbeatService.mjs:60 — move hardcoded path to ai/config.template.mjs defaultConfig.heartbeatAlivePath + env binding via Env.parsePath; service reads AiConfig.heartbeatAlivePath.
- Fix
PrimaryRepoSyncService.mjs:159+215 — remove env defaults from function signatures + JSDoc; callers pass the value explicitly.
- Collapse
resolvePrimaryDevSyncRootsConfig + resolvePrimaryDevSyncRootsSource — the precedence belongs in envBindings; the source-label belongs at the consumer (or in config metadata).
Acceptance Criteria
Avoided Traps
- ❌ Don't ship as 3-4 split PRs per per-pattern scope — operator-directed single-PR consolidation (
do we REALLY need 3-4 PRs, or can we do it with 1-2? go!)
- ❌ Don't keep "deprecated for backwards compat" wrappers around the deleted getters — clean cut; consumers update in the same PR
- ❌ Don't add new
|| / ?? hardcoded patterns in the replacement code — pure config-as-SSOT
- ❌ Don't touch unrelated orchestrator surfaces (refactor scope strictly limited to env/config pattern enforcement)
Related
- Operator-stated foundational contract:
feedback_no_hidden_default_fallbacks.md (banked from PR #12061 cycle-4)
- Architectural pattern source: post-M6 service-decomposition refactor that thinned Orchestrator.mjs
- Sandman restoration cascade: this PR is post-cascade cleanup (PR #12092 + #12093 + #12095 + #12096 just merged)
Premise
Operator-direct audit at 2026-05-27T15:50Z surfaced systematic config/env-pattern violations in the orchestrator surface, predating the foundational
NO HIDDEN DEFAULT VALUE FALLBACKS IN CODEcontract enforcement (PR #12061 cycle-4 anchor). The orchestrator was authored before the config-as-SSOT envBindings discipline was codified; with the contract now established, the existing patterns are debt.Audit verbatim from operator: "are you still aware of our config and env var patterns? this one did not get created inside this session. however, it is not the only spot. the orchestrator needs a review."
Audit findings
Quantified across
ai/daemons/orchestrator/:Pattern 1 —
Env.parse*('NEO_X') ?? AiConfig.Xgetters (29 instances inOrchestrator.mjs:391-445+)Per-access env probe +
??fallback to config-default in code, bypassing theenvBindingsmechanism entirely. Examples:get pollIntervalMs() { return Env.parseNumber('NEO_ORCHESTRATOR_POLL_INTERVAL_MS') ?? AiConfig.orchestrator.intervals.pollMs; } get kbSyncEnabled() { return Env.parseBool('NEO_ORCHESTRATOR_KB_SYNC_ENABLED') ?? resolveDeploymentEnabled('kbSyncEnabled'); } get mlxEnabled() { return Env.parseBool('NEO_ORCHESTRATOR_MLX_ENABLED') ?? !!AiConfig.orchestrator.mlx?.enabled; }The
envBindings.orchestrator.*declaration inai/config.template.mjsis the canonical surface for env-var override; runtime getters compete with that and create split source of truth.Pattern 2 — Resolver functions (5 instances)
Orchestrator.mjs:94resolvePrimaryDevSyncRootsConfig({envValue, configValue})Orchestrator.mjs:112resolvePrimaryDevSyncRootsSource({envValue})Orchestrator.mjs:129resolveDeploymentEnabled(key)null-means-deployment-default semantics in code; the operator-facing config template showsnullbut the runtime value comes from a code path the template doesn't exposeOrchestrator.mjs:144resolveCloudOnlyEnabled(key)MaintenanceBackpressureService.mjs:119resolveHeavyMaintenanceLeasePath({heavyMaintenanceLeasePath, dataDir})Pattern 3 — Direct
process.env.X || hardcoded(SwarmHeartbeatService.mjs:60)return process.env.NEO_HEARTBEAT_ALIVE_PATH || path.resolve(__dirname, '../../../../.neo-ai-data/wake-daemon/heartbeat.alive')Bypasses both config template + env binding parser. Hardcoded fallback path is exactly the
||substitution the operator contract bans.Pattern 4 — Default-parameter env reads (PrimaryRepoSyncService.mjs:159, 215)
@param {String[]|String|undefined|null} [options.devSyncRootsConfig=process.env.NEO_ORCHESTRATOR_DEV_SYNC_ROOTS]Even in JSDoc the env read is the default — same anti-pattern at the function-signature layer.
Prescription
Single coherent PR collapsing all four patterns into config-as-SSOT compliance:
envBindings.orchestrator.*inai/config.template.mjsfor all 29 env vars currently read by Orchestrator getters (intervals.* + tenantRepoSync.{sweepCadenceMs,jitterRatio} + dream/goldenPath/swarmHeartbeat intervals + swarmHeartbeat.{identity,targetSource,targets} + localOnly/cloudOnly enabled flags + mlx.{enabled,model,port} + lms.{enabled,model,port}).Orchestrator.mjs. Consumers readAiConfig.orchestrator.X.Ydirectly (env-applied at config-load time).resolveDeploymentEnabled+resolveCloudOnlyEnabled— replace with config-template getters (or store the deployment-default-resolved value at config load, not at every access).SwarmHeartbeatService.mjs:60— move hardcoded path toai/config.template.mjsdefaultConfig.heartbeatAlivePath+ env binding viaEnv.parsePath; service readsAiConfig.heartbeatAlivePath.PrimaryRepoSyncService.mjs:159+215— remove env defaults from function signatures + JSDoc; callers pass the value explicitly.resolvePrimaryDevSyncRootsConfig+resolvePrimaryDevSyncRootsSource— the precedence belongs in envBindings; the source-label belongs at the consumer (or in config metadata).Acceptance Criteria
Env.parse*('NEO_X') ?? AiConfig.Xgetters inOrchestrator.mjsdeleted; consumers readAiConfig.orchestrator.X.YdirectlyenvBindings.orchestrator.*inai/config.template.mjscontains every env var previously read by a deleted getter (Env.parseNumber/Env.parseBool/Env.parseStringdeclarations)resolveDeploymentEnabled+resolveCloudOnlyEnabledremoved from Orchestrator.mjs (replaced with config-time resolution)resolvePrimaryDevSyncRootsConfig+resolvePrimaryDevSyncRootsSourceremoved (collapsed into envBindings precedence)SwarmHeartbeatService.mjs:60readsAiConfig.X(noprocess.env.X || hardcoded-path)PrimaryRepoSyncService.mjs:159+215no[options.X=process.env.Y]default-parameter env readsMaintenanceBackpressureService.resolveHeavyMaintenanceLeasePathreviewed for the same anti-pattern; collapsed into config if applicableenvBindings(not through deleted code paths)Avoided Traps
do we REALLY need 3-4 PRs, or can we do it with 1-2? go!)||/?? hardcodedpatterns in the replacement code — pure config-as-SSOTRelated
feedback_no_hidden_default_fallbacks.md(banked from PR #12061 cycle-4)