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.mjs — runTask() → 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
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).
Context
Operator-observed friction (@tobiu, 2026-06-10, v13 wrap-up): the primary/canonical checkout keeps going stale between sessions — both its
devbranch AND its gitignoredconfig.mjsoperator-overlays — forcing a manualgit switch dev && git pull && node ai/scripts/setup/initServerConfigs.mjs --migrate-configbefore 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 theprimary-dev-syncorchestrator task, which FF-pulls primary'sdevand cascadesnpm 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 onlynpm run ai:sync-kb(KB re-embed). It never runsinitServerConfigs.mjs --migrate-config. So when a config template evolves ondev(a newleaf(...), a new per-serverconfig.mjs, a new env-binding), the daemon FF-pulls the updatedconfig.template.mjsbut the gitignoredconfig.mjsoperator-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
#11013sunset probe's own prose already warns about (.agents/skills/session-sunset/references/session-sunset-workflow.md):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#11017AC13's "retire the sunset probe once Shape A delivers" hasn't been able to close.The Architectural Reality
ai/daemons/orchestrator/services/PrimaryRepoSyncService.mjs—runTask()→syncPrimaryDev()→syncDevRoot()/syncConfiguredDevRoots()→ on FF-pull success callsrunKbSync()(≈ L521,npm run ai:sync-kb). There is noinitServerConfigsinvocation anywhere in the file (read in full).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-configis the reconcile flag (argv.includes(MIGRATE_FLAG)); bare invocation is warn-only (drift detection, no writes). Direct-invokable (theimport.meta.url === file://process.argv[1]guard at ≈ L571 runsinitTier1Config()+initConfigs()).--migrate-configonly rewrites the gitignoredconfig.mjs(safe; never touches tracked files).ai/config.template.mjs(Tier-1) andai/mcp/server/*/config.template.mjs(per-server).The Fix
Extend the
PrimaryRepoSyncServicecascade: after a successful FF-pull (thesyncDevRootclean-pull branch and theresolveMetaAndPullmeta-sync-resetbranch), when the pulled range (oldHead..newHead) touches a config-template path, runnode <root>/ai/scripts/setup/initServerConfigs.mjs --migrate-configfrom thatroot, alongside the existingrunKbSync()cascade.Mirror the existing KB-relevance pattern:
isConfigTemplateChangePath(filePath)predicate (matchesai/config.template.mjsandai/mcp/server/*/config.template.mjs);resolveConfigMigrateDecision({root, oldHead, newHead, ...})— or fold aconfigMigrateRequiredflag into the existingresolveKbSyncDecision— so migrate fires only when a template actually changed (no-op on every other pull);syncConfiguredDevRoots);healthService.recordTaskOutcome(...)with{parent: 'primary-dev-sync'}, same as thekbSynccascade annotation.Contract Ledger Matrix
PrimaryRepoSyncServicepost-pull cascade#11017Shape AinitServerConfigs --migrate-configfrom the pulled root when a config-template path changed inoldHead..newHeadfailed+ operator warning; do NOT abort the KB cascadeai/scripts/setup/initServerConfigs.mjs --migrate-config--migrate-configrewrites gitignoredconfig.mjsfrom evolved template; bare = warn-onlyai/scripts/setup/initServerConfigs.mjsJSDocargv.includes(MIGRATE_FLAG)gate +import.meta.urldirect-invoke guard (≈ L571)isConfigTemplateChangePathpredicate over the pulled diffkb-relevance-unknown-head)Decision Record impact
aligned-withADR 0019 (AiConfig reactive Provider SSOT): a daemon FF-pulling new template leaves while the operator-overlayconfig.mjsstays 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
PrimaryRepoSyncServicerunsinitServerConfigs.mjs --migrate-configfrom the pulled root after a successful FF-pull (both the clean-pull andmeta-sync-resetcompleted branches) only when the pulled range touches a config-template path.isConfigTemplateChangePath(or equivalent) detectsai/config.template.mjsandai/mcp/server/*/config.template.mjs; no other path triggers the migrate.initServerConfigs.recordTaskOutcome) with{parent: 'primary-dev-sync'}provenance, like thekbSynccascade.syncConfiguredDevRoots) runs the migrate per-root.failed/warning, does not abort the KB cascade or crash the task.--migrate-configargv is passed (not bare).config.mjs) — assert via post-rungit status --porcelainshowing no tracked changes in test/rehearsal.Out of Scope
NEO_ORCHESTRATOR_DEV_SYNC_ROOTS/primaryDevSyncEnablednot configured for the operator's primary; primary not ondev; or alocal-divergenceskip). That is runtime/operator-config diagnosis, not this code gap — flagged to the operator separately.#11017AC13) — separate cleanup, gated on Shape A actually delivering.initServerConfigs.mjsitself — consumed as-is.#12808,bootstrapWorktree) — a different config surface (missing-in-worktree vs drifted-in-primary).Avoided Traps
--migrate-configon every pull — wasteful; gate on actual config-template change in the pulled range, mirroringresolveKbSyncDecision.initServerConfigs.mjs— that is warn-only; it logs drift but reconciles nothing. The flag is required.#11017's design is the zero-operator-action daemon path; Shape B is slated to retire. The durable home is the cascade.Related
primary-dev-syncdaemon — shipped; this fills its config-migrate gap).agents/skills/session-sunset/references/session-sunset-workflow.mdbootstrapWorktreeworktree config hydration)ai/scripts/setup/initServerConfigs.mjsHandoff Retrieval Hints
query_raw_memories(query="primary-dev-sync PrimaryRepoSyncService cascade initServerConfigs migrate-config gitignored config.mjs drift stale daemon")ai/daemons/orchestrator/services/PrimaryRepoSyncService.mjs(runKbSync≈ L521,resolveKbSyncDecision≈ L640,isKbRelevantChangePath≈ L42);ai/scripts/setup/initServerConfigs.mjs(MIGRATE_FLAG).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).