---
id: 12147
title: bootstrapWorktree CLI mis-resolves projectRoot one level short
state: CLOSED
labels:
  - bug
  - ai
  - build
assignees:
  - neo-opus-ada
createdAt: '2026-05-28T19:47:16Z'
updatedAt: '2026-05-28T20:14:24Z'
githubUrl: 'https://github.com/neomjs/neo/issues/12147'
author: neo-opus-ada
commentsCount: 0
parentIssue: null
subIssues: []
subIssuesCompleted: 0
subIssuesTotal: 0
blockedBy: []
blocking: []
closedAt: '2026-05-28T20:14:24Z'
---
# bootstrapWorktree CLI mis-resolves projectRoot one level short

## 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:

```js
const projectRoot = path.resolve(__dirname, '..', '..'); // ai/scripts/ → ai/ → root
```

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

1. Extract the CLI root computation to an exported, testable helper `resolveCliProjectRoot(moduleDir)` returning `path.resolve(moduleDir, '..', '..', '..')` (`migrations/` → `scripts/` → `ai/` → root), with a corrected comment.
2. Use it in the `isMain` block: `const projectRoot = resolveCliProjectRoot(__dirname);`.
3. 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

- [ ] AC1: `resolveCliProjectRoot('<x>/ai/scripts/migrations')` returns `<x>` (repo root), unit-tested.
- [ ] AC2: the `isMain` CLI path uses the helper; no inline 2-level `path.resolve(__dirname, '..', '..')` remains.
- [ ] AC3: the stale comment is corrected.
- [ ] AC4: the `bootstrapWorktree` spec passes.

## 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"

## Timeline

- 2026-05-28T19:47:17Z @neo-opus-ada added the `bug` label
- 2026-05-28T19:47:17Z @neo-opus-ada added the `ai` label
- 2026-05-28T19:47:17Z @neo-opus-ada added the `build` label
- 2026-05-28T19:47:17Z @neo-opus-ada assigned to @neo-opus-ada
- 2026-05-28T19:50:25Z @neo-opus-ada cross-referenced by PR #12148
- 2026-05-28T20:14:24Z @tobiu referenced in commit `147dc26` - "fix(build): bootstrapWorktree CLI resolves projectRoot to repo root, not <root>/ai (#12147) (#12148)

The isMain CLI block computed `path.resolve(__dirname, '..', '..')` — but the script
lives at ai/scripts/migrations/ (3 levels deep), so 2 `..` climbed only to <root>/ai.
Config copies then landed in <root>/ai/ai/ and the real ai/config.mjs never materialized,
leaving every ai/services.mjs consumer in the worktree at ERR_MODULE_NOT_FOUND.

- Extract resolveCliProjectRoot(moduleDir) (3 `..`: migrations/ → scripts/ → ai/ → root),
  exported so the CLI projectRoot computation is unit-testable.
- isMain uses the helper; corrected the stale comment.
- Spec: assert resolveCliProjectRoot('<x>/ai/scripts/migrations') === '<x>' + a regression
  guard that the prior 2-level resolve mislanded at <root>/ai.

The CLI path was previously untested (the spec injects projectRoot into the exported fn),
which is how the off-by-one escaped coverage — same escape class as #12042.

35/35 bootstrapWorktree spec passing.

Co-authored-by: tobiu <tobiasuhlig78@gmail.com>"
- 2026-05-28T20:14:25Z @tobiu closed this issue

