LearnNewsExamplesServices
Frontmatter
title>-
authorneo-fable
stateOpen
createdAt7:49 AM
updatedAt7:49 AM
closedAt
mergedAt
branchesdevagent/12456-adr0019-violations-batch2
urlhttps://github.com/neomjs/neo/pull/14941
contentTrust
projected
quarantined1
signals[]
Open
neo-fable
neo-fable commented on 7:49 AM

Contributes to #12456 (batch 2 — orchestrator dbPath/dataDir A1 module-level env re-derivation, plus the daemon.mjs drift twin).

Context

ai/daemons/orchestrator/taskDefinitions.mjs exported two module-level env re-derivations with hidden defaults (DEFAULT_DB_PATH from NEO_AI_DB_PATH, DEFAULT_DATA_DIR from NEO_AI_ORCHESTRATOR_DIR), and ai/daemons/orchestrator/daemon.mjs carried an independent drift twin of the NEO_AI_ORCHESTRATOR_DIR derivation — the ADR-0019 §3 A1 pattern (module-level re-derivation), with the drift twin as the empirical cost: two files each owned a copy of the same default.

The fix follows the batch-1 idiom (PR #14935, KB-domain slice):

  1. Root ai/config.template.mjs gains two orchestrator.* leaves — dataDir: leaf('.neo-ai-data/orchestrator-daemon', 'NEO_AI_ORCHESTRATOR_DIR', 'string') and dbPath: leaf('.neo-ai-data/sqlite/memory-core-graph.sqlite', 'NEO_AI_DB_PATH', 'string') — with JSDoc naming what each owns. The exact previous defaults are preserved verbatim (relative paths, resolved against the daemon's cwd == repo root).
  2. taskDefinitions.mjs: the exported constants become leaf-fed (DEFAULT_DB_PATH = AiConfig.orchestrator.dbPath, DEFAULT_DATA_DIR = AiConfig.orchestrator.dataDir), so the 5-consumer surface (Orchestrator, MaintenanceBackpressureService, TenantRepoSyncService, RecoveryActuatorService, ConfiguredTaskDefinitionsService/specs) stays byte-stable — no consumer chasing in this batch.
  3. daemon.mjs: the duplicate derivation is gone. As an entrypoint it reads the leaf directly (AiConfig.orchestrator.dataDir), matching its existing AiConfig.orchestrator.devSyncRoots use-site style.
  4. The gitignored ai/config.mjs overlay (a full materialized template copy) received the identical leaf edit, so the canonical deployment does not trip the boot-time freshness guard.

Bootstrap-chain finding (the C1 question, V-B-A'd): every importer of taskDefinitions.mjs — the four export consumers plus ConfiguredTaskDefinitionsService and the three orchestrator specs — already imports AiConfig (and the Neo namespace bootstrap) itself, so the added import introduces zero marginal bootstrap weight and no cycle (the config chain config.mjs → ConfigProvider → src/state/Provider never reaches back into ai/daemons/**). The one real behavior change: taskDefinitions.mjs is no longer importable without the entrypoint Neo bootstrap (src/core/Compare.mjs references the global Neo at module scope). No such bootstrap-free consumer exists anywhere in the repo (swept ai/, test/, bin/, buildScripts/); the daemon entrypoints bootstrap at module top before any class import, and all specs import src/Neo.mjs + core/_export.mjs first.

Evidence: 287/287 targeted tests green (see Test Evidence), including the integration spec that boots the real daemon.mjs child process with NEO_AI_ORCHESTRATOR_DIR/NEO_AI_DB_PATH env overrides; standalone smoke runs confirm the leaf-fed exports resolve the exact previous defaults and that both env bindings still override through the leaves.

Deltas from ticket

  • Lazy accessors in daemon.mjs instead of eager module-level path.join consts. A straight const PID_FILE = path.join(AiConfig.orchestrator.dataDir, …) would evaluate during module load — before the boot-time assertConfigFresh guard runs — so a stale overlay (missing exactly the leaves this PR adds) would crash with a cryptic path TypeError instead of the guard's actionable --migrate-config message. daemonDataDir() / pidFilePath() / logFilePath() defer the reads to use sites, all of which run post-guard. This preserves the guard's designed contract for precisely the drift class this PR creates in the wild.
  • The DEFAULT_* export surface itself (the §3 B1 pattern) is intentionally kept — batch-2 kills the A1 env re-derivation while holding the consumer surface stable; collapsing the re-exports into direct use-site reads is a later, consumer-chasing slice.
  • Overlay-ahead sync: the operator's live ai/config.mjs overlay had gained an mcpHttpHost leaf (from an in-flight lane) after this worktree's setup copy; the worktree overlay was re-synced from the live copy before applying the leaf edit, so both overlay copies stay byte-identical and ahead-of-template drift is preserved (harmless: the freshness guard only checks the template → overlay direction).

Test Evidence

All runs with --workers=1, port 18180 cleared before each run:

  • test/playwright/unit/ai/daemons/orchestrator/daemon.spec.mjs + Orchestrator.spec.mjs + Orchestrator.invariants.spec.mjs101 passed (32.8s). Covers the source-level invariants (fail-loud direct reads, no defensive ?., devSyncRoots direct read) and the taskDefinitions purity contracts.
  • services/MaintenanceBackpressureService.spec.mjs + services/RecoveryActuatorService.spec.mjs + services/TenantRepoSyncService.spec.mjs87 passed (33.4s). The three DEFAULT_DATA_DIR consumers.
  • services/ProcessSupervisorService.spec.mjs + services/TaskStateService.spec.mjs + ai/scripts/setup/initServerConfigs.spec.mjs96 passed (14.0s). Includes the template/overlay drift-detection machinery the new leaves flow through.
  • test/playwright/integration/ai/daemons/workspaceSafety.spec.mjs (integration config) — 3 passed (7.2s). Boots the real orchestrator daemon.mjs as a child process with env-overridden NEO_AI_ORCHESTRATOR_DIR/NEO_AI_DB_PATH, self-bootstraps sqlite, reaches [Orchestrator] Started — end-to-end proof of the lazy-accessor boot path and the leaf env bindings.
  • node --check green on all four touched files (template, overlay, taskDefinitions, daemon); block-alignment check clean.
  • Smoke (bootstrap + dynamic import): DEFAULT_DB_PATH === '.neo-ai-data/sqlite/memory-core-graph.sqlite', DEFAULT_DATA_DIR === '.neo-ai-data/orchestrator-daemon' (exact previous literals), and both env vars override through the leaves.

Post-Merge Validation

  • Orchestrator daemon restart on the canonical deployment resolves both paths from config: orchestrator.log / PID files continue under .neo-ai-data/orchestrator-daemon, and the graph DB opens at .neo-ai-data/sqlite/memory-core-graph.sqlite (or their env overrides where set).

Commits

  • dd6211c45 refactor(orchestrator): ADR-0019 batch-2 — dbPath/dataDir provider leaves replace module-level env derivation (#12456)

Authored by Mnemosyne (Claude Fable 5, Claude Code). Session b956ba53-01ed-4ea6-a1e5-62969f887bc3.

github-actions commented on 7:49 AM

🚨 Agent PR Body Lint Violation

@neo-fable — your PR body on PR #14941 [QUARANTINED_URL: github.com] 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: at least one recognized anchor like ``Resolves #N(mandatory closing keyword —Refs/Related alone is NOT sufficient) is missing.

Visible anchors missing (full list)
  • ``Resolves #N(mandatory closing keyword —Refs/Related alone is NOT sufficient)

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.