Context
Surfaced during #12145 worktree setup: running node ai/scripts/migrations/bootstrapWorktree.mjs left the worktree's config.mjs files missing (forcing a manual template-copy fallback to run unit tests) and created a stray ai/ai/ directory containing exactly the BOOTSTRAP_CONFIGS set.
The Problem
The CLI entry (isMain block) computes the repo root one directory-level short:
const projectRoot = path.resolve(__dirname, '..', '..');
The script lives at ai/scripts/migrations/bootstrapWorktree.mjs (3 levels deep), so __dirname is <root>/ai/scripts/migrations. Two .. climbs only to <root>/ai, not the repo root. The comment ("ai/scripts/ → ai/ → root") is stale — it describes the pre-migrations/ location; the file was relocated into migrations/ without bumping the .. count. The config copy then does path.join('<root>/ai', 'ai/config.mjs') → writes to <root>/ai/ai/config.mjs, so the real ai/config.mjs never lands and every consumer that imports ai/services.mjs hits ERR_MODULE_NOT_FOUND.
The Architectural Reality
ai/scripts/migrations/bootstrapWorktree.mjs:901 — the buggy path.resolve(__dirname, '..', '..').
- The exported
bootstrapWorktree({projectRoot}) takes projectRoot as an argument, so the unit spec injects it correctly — the CLI-only inline computation is the untested glue. test/playwright/unit/ai/scripts/migrations/bootstrapWorktree.spec.mjs's JSDoc explicitly states the CLI path is "a thin wrapper ... not exercised here," which is how the off-by-one escaped coverage (same escape class as #12042).
The Fix
- Extract the CLI root computation to an exported, testable helper
resolveCliProjectRoot(moduleDir) returning path.resolve(moduleDir, '..', '..', '..') (migrations/ → scripts/ → ai/ → root), with a corrected comment.
- Use it in the
isMain block: const projectRoot = resolveCliProjectRoot(__dirname);.
- Add a spec case asserting it resolves
<root>/ai/scripts/migrations → <root> (with a regression note that the prior 2-level resolve mislanded at <root>/ai).
Acceptance Criteria
Out of Scope
- The
build-all step's separate failure modes (heavy full build) — orthogonal to the path bug.
- A broader CLI-path integration test — the helper unit test covers the escape class.
Related
- Surfaced during #12145 (pull-mode
tenantRepos tiered resolver) worktree setup.
- Bootstrap feature origin: #10095; the relocation into
migrations/ likely introduced the regression.
Decision Record impact
none — localized bug fix.
Handoff Retrieval Hints
- Retrieval Hint: "bootstrapWorktree projectRoot off-by-one ai/ai stray configs"
Context
Surfaced during #12145 worktree setup: running
node ai/scripts/migrations/bootstrapWorktree.mjsleft the worktree'sconfig.mjsfiles missing (forcing a manual template-copy fallback to run unit tests) and created a strayai/ai/directory containing exactly theBOOTSTRAP_CONFIGSset.The Problem
The CLI entry (
isMainblock) computes the repo root one directory-level short:const projectRoot = path.resolve(__dirname, '..', '..'); // ai/scripts/ → ai/ → rootThe script lives at
ai/scripts/migrations/bootstrapWorktree.mjs(3 levels deep), so__dirnameis<root>/ai/scripts/migrations. Two..climbs only to<root>/ai, not the repo root. The comment ("ai/scripts/ → ai/ → root") is stale — it describes the pre-migrations/location; the file was relocated intomigrations/without bumping the..count. The config copy then doespath.join('<root>/ai', 'ai/config.mjs')→ writes to<root>/ai/ai/config.mjs, so the realai/config.mjsnever lands and every consumer that importsai/services.mjshitsERR_MODULE_NOT_FOUND.The Architectural Reality
ai/scripts/migrations/bootstrapWorktree.mjs:901— the buggypath.resolve(__dirname, '..', '..').bootstrapWorktree({projectRoot})takesprojectRootas an argument, so the unit spec injects it correctly — the CLI-only inline computation is the untested glue.test/playwright/unit/ai/scripts/migrations/bootstrapWorktree.spec.mjs's JSDoc explicitly states the CLI path is "a thin wrapper ... not exercised here," which is how the off-by-one escaped coverage (same escape class as #12042).The Fix
resolveCliProjectRoot(moduleDir)returningpath.resolve(moduleDir, '..', '..', '..')(migrations/→scripts/→ai/→ root), with a corrected comment.isMainblock:const projectRoot = resolveCliProjectRoot(__dirname);.<root>/ai/scripts/migrations→<root>(with a regression note that the prior 2-level resolve mislanded at<root>/ai).Acceptance Criteria
resolveCliProjectRoot('<x>/ai/scripts/migrations')returns<x>(repo root), unit-tested.isMainCLI path uses the helper; no inline 2-levelpath.resolve(__dirname, '..', '..')remains.bootstrapWorktreespec passes.Out of Scope
build-allstep's separate failure modes (heavy full build) — orthogonal to the path bug.Related
tenantRepostiered resolver) worktree setup.migrations/likely introduced the regression.Decision Record impact
none— localized bug fix.Handoff Retrieval Hints