Context
Surfaced 2026-05-26 while debugging the unitTestMode collision in PR #12044. Operator pointed at the substrate duplication between two scripts that both manage gitignored config substrate but with different semantics:
ai/scripts/setup/initServerConfigs.mjs — runs at npm prepare. For missing per-server config.mjs, clones from config.template.mjs AND applies materializeServerConfigTemplate to rewrite import AiConfig from '../../../config.template.mjs' → '../../../config.mjs' (operator overlay).
ai/scripts/migrations/bootstrapWorktree.mjs — runs after git worktree add. Copies the canonical checkout's per-server config.mjs verbatim into the worktree. No materialize step. Also doesn't copy ai/config.mjs (Tier-1 operator overlay).
The Problem
Three distinct issues from the same substrate gap:
1. No materialize-on-copy in bootstrapWorktree
bootstrapWorktree.mjs:247 does await fs.copyFile(src, dst) — raw byte copy from canonical to worktree. If the canonical checkout's per-server config.mjs is un-materialized (predates the materialize logic, or was never re-run with --migrate-config), the worktree inherits the un-materialized state.
This propagates the import-path bug from canonical to every spawned worktree. With Agent(isolation: "worktree") being the standard Claude Code workflow, this affects most agent-spawned work.
2. Tier-1 ai/config.mjs not bootstrapped
BOOTSTRAP_CONFIGS at bootstrapWorktree.mjs:113-118 lists only the 4 per-server configs:
export const BOOTSTRAP_CONFIGS = [
'ai/mcp/server/github-workflow/config.mjs',
'ai/mcp/server/knowledge-base/config.mjs',
'ai/mcp/server/memory-core/config.mjs',
'ai/mcp/server/neural-link/config.mjs'
];The Tier-1 ai/config.mjs (canonical operator overlay) is missing. If a worktree's per-server config.mjs imports '../../../config.mjs' (post-materialize), the import will fail because Tier-1 doesn't exist in the worktree. This works today only because most per-server configs are un-materialized (importing config.template.mjs directly) — i.e., bug #1 masks bug #2.
3. Duplication of substrate responsibility
Both scripts manage gitignored config.mjs files. They diverged because:
initServerConfigs is a generic "set up from template" tool (npm-prepare cadence).
bootstrapWorktree is a "hydrate gitignored from canonical" tool (worktree-add cadence).
But the worktree case is structurally equivalent to "fresh clone" once you accept that the worktree should generate its own materialized configs from template. The right abstraction is to have bootstrapWorktree delegate to initServerConfigs rather than implement its own copy logic.
The Architectural Reality
The Fix
Three viable shapes (PR author picks); not mutually exclusive:
Option A — bootstrapWorktree delegates to initServerConfigs
Replace the copy-from-canonical loop with a call to initConfigs() + initTier1Config() (export them as a reusable bootstrap entry point if not already). The worktree gets fresh, materialized configs from template. Operator-edited content (tenantRepos[], etc.) does NOT propagate — operator has to re-edit per worktree. Pro: correct materialization. Con: loses operator-edit propagation, which is the current bootstrapWorktree value-add.
Option B — bootstrapWorktree adds a materialize-on-copy step
After fs.copyFile, run the canonical's content through materializeServerConfigTemplate (read, apply, write). Operator-edited content propagates AND the import-path rewrite always lands. Pro: preserves current semantic + fixes the bug. Con: the materialize logic now lives in two places (drift risk).
Option C — bootstrapWorktree adds Tier-1 to BOOTSTRAP_CONFIGS + applies materialize
Combines option B with adding 'ai/config.mjs' to the bootstrap list so post-materialize imports resolve. Worktree carries a copy of canonical's Tier-1 overlay. Pro: complete; matches current bootstrap-as-hydrate semantic with the materialize correctness. Con: even more substrate coupling between the two scripts.
My read: Option B is the immediate fix (1-line behavior change + minimal blast radius). Long-term, Option A is the architecturally right shape (single substrate; bootstrapWorktree becomes a thin worktree-aware wrapper around initServerConfigs). Recommend filing Option A as a follow-up after Option B lands.
Decision Record impact
amends the operator-overlay bootstrap contract documented across the two scripts. ADR successor-risk audit needed before Option A (might affect operator's expected worktree-edit-propagation behavior).
Acceptance Criteria
Out of Scope
- The materialization-migration concern for existing un-materialized canonical checkouts (covered by #12047).
- Renaming / restructuring the
BOOTSTRAP_CONFIGS list semantics beyond adding Tier-1.
Avoided Traps
- Option-A-first: tempting because it's the right long-term shape, but the operator-edit-propagation semantic is currently load-bearing for some workflows (worktree gets canonical's
tenantRepos[] so the agent can resolve tenant configs). Option B preserves that semantic while fixing the bug. Architectural cleanup comes after the immediate fix lands.
- Treating this as a worktree-only concern: the materialization-state of canonical's
config.mjs is the actual variable. If canonical is materialized correctly, the current copy works. If not, the gap propagates. Fixing materialize-on-copy means worktrees always work regardless of canonical state.
Related
- Sibling: #12047 — the materialization-migration concern for existing canonical checkouts.
- Empirical anchor: PR #12044 cycle-2 → cycle-2.1 simplification. Manual import-path fix on operator-local per-server
config.mjs revealed both: the un-materialized state AND the bootstrapWorktree gap.
- Memory:
reference_bootstrap_worktree_for_configs (Claude Code agent memory): "Restoration fallback: cp config.template.mjs config.mjs per missing server. Empirical 2026-05-26" — flagged as a fallback because the canonical bootstrap path itself has the materialization gap.
- Empirical session: first real-world cloud Agent OS deployment, 2026-05-26.
Context
Surfaced 2026-05-26 while debugging the unitTestMode collision in PR #12044. Operator pointed at the substrate duplication between two scripts that both manage gitignored config substrate but with different semantics:
ai/scripts/setup/initServerConfigs.mjs— runs atnpm prepare. For missing per-serverconfig.mjs, clones fromconfig.template.mjsAND appliesmaterializeServerConfigTemplateto rewriteimport AiConfig from '../../../config.template.mjs'→'../../../config.mjs'(operator overlay).ai/scripts/migrations/bootstrapWorktree.mjs— runs aftergit worktree add. Copies the canonical checkout's per-serverconfig.mjsverbatim into the worktree. No materialize step. Also doesn't copyai/config.mjs(Tier-1 operator overlay).The Problem
Three distinct issues from the same substrate gap:
1. No materialize-on-copy in
bootstrapWorktreebootstrapWorktree.mjs:247doesawait fs.copyFile(src, dst)— raw byte copy from canonical to worktree. If the canonical checkout's per-serverconfig.mjsis un-materialized (predates the materialize logic, or was never re-run with--migrate-config), the worktree inherits the un-materialized state.This propagates the import-path bug from canonical to every spawned worktree. With
Agent(isolation: "worktree")being the standard Claude Code workflow, this affects most agent-spawned work.2. Tier-1
ai/config.mjsnot bootstrappedBOOTSTRAP_CONFIGSatbootstrapWorktree.mjs:113-118lists only the 4 per-server configs:export const BOOTSTRAP_CONFIGS = [ 'ai/mcp/server/github-workflow/config.mjs', 'ai/mcp/server/knowledge-base/config.mjs', 'ai/mcp/server/memory-core/config.mjs', 'ai/mcp/server/neural-link/config.mjs' ];The Tier-1
ai/config.mjs(canonical operator overlay) is missing. If a worktree's per-serverconfig.mjsimports'../../../config.mjs'(post-materialize), the import will fail because Tier-1 doesn't exist in the worktree. This works today only because most per-server configs are un-materialized (importingconfig.template.mjsdirectly) — i.e., bug #1 masks bug #2.3. Duplication of substrate responsibility
Both scripts manage gitignored config.mjs files. They diverged because:
initServerConfigsis a generic "set up from template" tool (npm-prepare cadence).bootstrapWorktreeis a "hydrate gitignored from canonical" tool (worktree-add cadence).But the worktree case is structurally equivalent to "fresh clone" once you accept that the worktree should generate its own materialized configs from template. The right abstraction is to have
bootstrapWorktreedelegate toinitServerConfigsrather than implement its own copy logic.The Architectural Reality
ai/scripts/setup/initServerConfigs.mjs:129-133—materializeServerConfigTemplate(the canonical rewrite logic).ai/scripts/setup/initServerConfigs.mjs:173-231—initConfigs(per-server bootstrap with template-copy + materialize on missing config).ai/scripts/setup/initServerConfigs.mjs:252-292—initTier1Config(Tier-1 overlay bootstrap fromconfig.template.mjs).ai/scripts/migrations/bootstrapWorktree.mjs:113-118—BOOTSTRAP_CONFIGSlist.ai/scripts/migrations/bootstrapWorktree.mjs:226-253— copy-from-canonical loop.The Fix
Three viable shapes (PR author picks); not mutually exclusive:
Option A —
bootstrapWorktreedelegates toinitServerConfigsReplace the copy-from-canonical loop with a call to
initConfigs()+initTier1Config()(export them as a reusable bootstrap entry point if not already). The worktree gets fresh, materialized configs from template. Operator-edited content (tenantRepos[], etc.) does NOT propagate — operator has to re-edit per worktree. Pro: correct materialization. Con: loses operator-edit propagation, which is the current bootstrapWorktree value-add.Option B —
bootstrapWorktreeadds a materialize-on-copy stepAfter
fs.copyFile, run the canonical's content throughmaterializeServerConfigTemplate(read, apply, write). Operator-edited content propagates AND the import-path rewrite always lands. Pro: preserves current semantic + fixes the bug. Con: the materialize logic now lives in two places (drift risk).Option C —
bootstrapWorktreeadds Tier-1 toBOOTSTRAP_CONFIGS+ applies materializeCombines option B with adding
'ai/config.mjs'to the bootstrap list so post-materialize imports resolve. Worktree carries a copy of canonical's Tier-1 overlay. Pro: complete; matches current bootstrap-as-hydrate semantic with the materialize correctness. Con: even more substrate coupling between the two scripts.My read: Option B is the immediate fix (1-line behavior change + minimal blast radius). Long-term, Option A is the architecturally right shape (single substrate;
bootstrapWorktreebecomes a thin worktree-aware wrapper aroundinitServerConfigs). Recommend filing Option A as a follow-up after Option B lands.Decision Record impact
amendsthe operator-overlay bootstrap contract documented across the two scripts. ADR successor-risk audit needed before Option A (might affect operator's expected worktree-edit-propagation behavior).Acceptance Criteria
bootstrapWorktree.mjsno longer copies un-materialized per-serverconfig.mjsinto worktrees — whichever option lands ensures the worktree's copy has the correct import path.ai/config.mjsis bootstrapped into worktrees (either via Option C copy OR Option A'sinitTier1Configinvocation).initServerConfigsbehavior unchanged for the non-worktree case).config.mjsimportsconfig.template.mjs; verify the worktree's copy imports'../../../config.mjs'(materialize applied OR delegated).ai/config.mjs; verify the worktree'sai/config.mjsexists post-bootstrap.bootstrapWorktree.mjsJSDoc reflecting the chosen approach.Out of Scope
BOOTSTRAP_CONFIGSlist semantics beyond adding Tier-1.Avoided Traps
tenantRepos[]so the agent can resolve tenant configs). Option B preserves that semantic while fixing the bug. Architectural cleanup comes after the immediate fix lands.config.mjsis the actual variable. If canonical is materialized correctly, the current copy works. If not, the gap propagates. Fixing materialize-on-copy means worktrees always work regardless of canonical state.Related
config.mjsrevealed both: the un-materialized state AND the bootstrapWorktree gap.reference_bootstrap_worktree_for_configs(Claude Code agent memory): "Restoration fallback: cp config.template.mjs config.mjs per missing server. Empirical 2026-05-26" — flagged as a fallback because the canonical bootstrap path itself has the materialization gap.