LearnNewsExamplesServices
Frontmatter
id12854
titleprimary-dev-sync pulls dev + syncs KB but never migrates config.mjs
stateClosed
labels
enhancementaiarchitecturemodel-experience
assigneesneo-opus-ada
createdAtJun 10, 2026, 8:49 PM
updatedAtJun 10, 2026, 10:37 PM
githubUrlhttps://github.com/neomjs/neo/issues/12854
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtJun 10, 2026, 10:37 PM

primary-dev-sync pulls dev + syncs KB but never migrates config.mjs

Closed v13.0.0/archive-v13-0-0-chunk-17 enhancementaiarchitecturemodel-experience
neo-opus-ada
neo-opus-ada commented on Jun 10, 2026, 8:49 PM

Context

Operator-observed friction (@tobiu, 2026-06-10, v13 wrap-up): the primary/canonical checkout keeps going stale between sessions — both its dev branch AND its gitignored config.mjs operator-overlays — forcing a manual git switch dev && git pull && node ai/scripts/setup/initServerConfigs.mjs --migrate-config before merge/daemon work.

The intuitive read ("the sunset protocol is missing a refresh step") is false on investigation — the refresh substrate already exists on two layers:

  • #11013 (closed) shipped the Shape B interim sunset staleness probe — a reminder-only handover block.
  • #11017 (closed, merged via PR #11130) shipped the Shape A daemon path: ai/daemons/orchestrator/services/PrimaryRepoSyncService.mjs, registered as the primary-dev-sync orchestrator task, which FF-pulls primary's dev and cascades npm run ai:sync-kb.

So the design exists. The gap this ticket files is narrower, and verified by reading the shipped service.

The Problem

PrimaryRepoSyncService's post-pull cascade runs only npm run ai:sync-kb (KB re-embed). It never runs initServerConfigs.mjs --migrate-config. So when a config template evolves on dev (a new leaf(...), a new per-server config.mjs, a new env-binding), the daemon FF-pulls the updated config.template.mjs but the gitignored config.mjs operator-overlays are never reconciled to it. Every daemon that reads the overlay — the orchestrator itself, wake-daemon, DreamService, the KB pipeline — then runs current code against stale config.

This is the exact failure the #11013 sunset probe's own prose already warns about (.agents/skills/session-sunset/references/session-sunset-workflow.md):

A git pull alone is not enough: it updates the committed config.template.mjs, but the daemons read the gitignored config.mjs operator-overlay — which only reconciles to new template leaves via --migrate-config.

The daemon path (Shape A) silently violates the requirement its sibling probe (Shape B) documents. #11017's stated goal was zero-operator-action freshness; this missing config-migrate step is one reason the operator still runs it by hand — and one reason #11017 AC13's "retire the sunset probe once Shape A delivers" hasn't been able to close.

The Architectural Reality

  • ai/daemons/orchestrator/services/PrimaryRepoSyncService.mjsrunTask()syncPrimaryDev()syncDevRoot() / syncConfiguredDevRoots() → on FF-pull success calls runKbSync() (≈ L521, npm run ai:sync-kb). There is no initServerConfigs invocation anywhere in the file (read in full).
  • The service already has the right shape to extend: isKbRelevantChangePath() (≈ L42) + resolveKbSyncDecision() (≈ L640) gate the KB cascade to the pulled diff (git diff --name-only oldHead..newHead). A config-migrate step should mirror that gating, keyed on config-template paths.
  • ai/scripts/setup/initServerConfigs.mjs--migrate-config is the reconcile flag (argv.includes(MIGRATE_FLAG)); bare invocation is warn-only (drift detection, no writes). Direct-invokable (the import.meta.url === file://process.argv[1] guard at ≈ L571 runs initTier1Config() + initConfigs()). --migrate-config only rewrites the gitignored config.mjs (safe; never touches tracked files).
  • Config-template surfaces in the pulled diff: ai/config.template.mjs (Tier-1) and ai/mcp/server/*/config.template.mjs (per-server).

The Fix

Extend the PrimaryRepoSyncService cascade: after a successful FF-pull (the syncDevRoot clean-pull branch and the resolveMetaAndPull meta-sync-reset branch), when the pulled range (oldHead..newHead) touches a config-template path, run node <root>/ai/scripts/setup/initServerConfigs.mjs --migrate-config from that root, alongside the existing runKbSync() cascade.

Mirror the existing KB-relevance pattern:

  • add an isConfigTemplateChangePath(filePath) predicate (matches ai/config.template.mjs and ai/mcp/server/*/config.template.mjs);
  • add a resolveConfigMigrateDecision({root, oldHead, newHead, ...}) — or fold a configMigrateRequired flag into the existing resolveKbSyncDecision — so migrate fires only when a template actually changed (no-op on every other pull);
  • run it per-root on the configured multi-root path (syncConfiguredDevRoots);
  • record it as a first-class outcome via healthService.recordTaskOutcome(...) with {parent: 'primary-dev-sync'}, same as the kbSync cascade annotation.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
PrimaryRepoSyncService post-pull cascade This ticket + #11017 Shape A Run initServerConfigs --migrate-config from the pulled root when a config-template path changed in oldHead..newHead On migrate error, record failed + operator warning; do NOT abort the KB cascade JSDoc on the new method + service class doc Unit test: template-change diff → migrate fires; non-template diff → no-op
ai/scripts/setup/initServerConfigs.mjs --migrate-config Direct read of the script (V-B-A) Existing surface; --migrate-config rewrites gitignored config.mjs from evolved template; bare = warn-only Bare run is a safe no-op ai/scripts/setup/initServerConfigs.mjs JSDoc Source confirms argv.includes(MIGRATE_FLAG) gate + import.meta.url direct-invoke guard (≈ L571)
Config-template change detection This ticket isConfigTemplateChangePath predicate over the pulled diff Migrate-on-probe-failure (mirrors kb-relevance-unknown-head) Predicate JSDoc Unit test over representative changed-path lists

Decision Record impact

aligned-with ADR 0019 (AiConfig reactive Provider SSOT): a daemon FF-pulling new template leaves while the operator-overlay config.mjs stays unreconciled is precisely an SSOT-staleness hazard at the resolved-leaf use site. This ticket keeps the overlay current with the template, consistent with the SSOT model. Extends shipped #11017; supersedes nothing.

Acceptance Criteria

  • AC1: PrimaryRepoSyncService runs initServerConfigs.mjs --migrate-config from the pulled root after a successful FF-pull (both the clean-pull and meta-sync-reset completed branches) only when the pulled range touches a config-template path.
  • AC2: isConfigTemplateChangePath (or equivalent) detects ai/config.template.mjs and ai/mcp/server/*/config.template.mjs; no other path triggers the migrate.
  • AC3: No-op verified — a pull whose diff contains no config-template change does not invoke initServerConfigs.
  • AC4: The migrate is recorded as a first-class outcome (recordTaskOutcome) with {parent: 'primary-dev-sync'} provenance, like the kbSync cascade.
  • AC5: Configured multi-root path (syncConfiguredDevRoots) runs the migrate per-root.
  • AC6: Migrate failure is isolated — recorded as failed/warning, does not abort the KB cascade or crash the task.
  • AC7: Unit tests cover: template-change → migrate fires; non-template change → no-op; failure isolation; multi-root; the --migrate-config argv is passed (not bare).
  • AC8: No tracked-file writes from the migrate (only gitignored config.mjs) — assert via post-run git status --porcelain showing no tracked changes in test/rehearsal.

Out of Scope

  • Why the daemon may not be FF-pulling at all on a given machine (orchestrator-daemon not running; NEO_ORCHESTRATOR_DEV_SYNC_ROOTS / primaryDevSyncEnabled not configured for the operator's primary; primary not on dev; or a local-divergence skip). That is runtime/operator-config diagnosis, not this code gap — flagged to the operator separately.
  • Retiring the Shape B sunset probe (#11017 AC13) — separate cleanup, gated on Shape A actually delivering.
  • Changing initServerConfigs.mjs itself — consumed as-is.
  • Worktree config hydration (#12808, bootstrapWorktree) — a different config surface (missing-in-worktree vs drifted-in-primary).

Avoided Traps

  • Run --migrate-config on every pull — wasteful; gate on actual config-template change in the pulled range, mirroring resolveKbSyncDecision.
  • Run bare initServerConfigs.mjs — that is warn-only; it logs drift but reconciles nothing. The flag is required.
  • Put the fix in the sunset reminder instead of the daemon — wrong substrate. #11017's design is the zero-operator-action daemon path; Shape B is slated to retire. The durable home is the cascade.
  • Re-open an agent→primary boundary at sunset — not needed; the daemon already owns the primary pull. This only adds one cascade step to that owned path.

Related

  • Extends: #11017 (Shape A primary-dev-sync daemon — shipped; this fills its config-migrate gap)
  • Documents the requirement: #11013 (Shape B sunset staleness probe) + .agents/skills/session-sunset/references/session-sunset-workflow.md
  • Config / multi-root: #11169 (dev-sync roots config), #11135 (multi-clone dev auto-sync)
  • Sibling config surface: #12808 (bootstrapWorktree worktree config hydration)
  • Refresh script: ai/scripts/setup/initServerConfigs.mjs

Handoff Retrieval Hints

  • query_raw_memories(query="primary-dev-sync PrimaryRepoSyncService cascade initServerConfigs migrate-config gitignored config.mjs drift stale daemon")
  • Key files: ai/daemons/orchestrator/services/PrimaryRepoSyncService.mjs (runKbSync ≈ L521, resolveKbSyncDecision ≈ L640, isKbRelevantChangePath ≈ L42); ai/scripts/setup/initServerConfigs.mjs (MIGRATE_FLAG).
  • Origin: 2026-06-10 v13 wrap-up session (@neo-opus-ada). Operator-observed friction; scope corrected from "sunset skill step" to "daemon cascade gap" after V-B-A.

Live latest-open sweep

Checked the latest 40 open issues at ~2026-06-10T18:38Z + grep over resources/content/ (matches only in historical PRs/discussions, no open-issue dup) + KB semantic (no equivalent; synthesis degraded). No duplicate. Closest neighbors are the parent feature #11017 (this extends it) and config-surface sibling #12808 (distinct surface).

tobiu closed this issue on Jun 10, 2026, 10:37 PM
tobiu referenced in commit e912b81 - "feat(orchestrator): primary-dev-sync migrates config.mjs after dev pull (#12854) (#12860) on Jun 10, 2026, 10:37 PM