---
id: 10095
title: '[bug] Worktrees fail to bootstrap ai/services.mjs — gitignored config.mjs + symlink namespace collision'
state: CLOSED
labels:
  - bug
  - contributor-experience
  - ai
assignees:
  - tobiu
createdAt: '2026-04-19T12:20:24Z'
updatedAt: '2026-05-15T12:42:23Z'
githubUrl: 'https://github.com/neomjs/neo/issues/10095'
author: tobiu
commentsCount: 0
parentIssue: null
subIssues: []
subIssuesCompleted: 0
subIssuesTotal: 0
blockedBy: []
blocking: []
closedAt: '2026-04-19T13:30:11Z'
---
# [bug] Worktrees fail to bootstrap ai/services.mjs — gitignored config.mjs + symlink namespace collision

## Context

Fresh git worktrees created under `.claude/worktrees/<name>/` (Claude Code's default) cannot run any script that imports `ai/services.mjs`. Reproducer:

```bash
cd .claude/worktrees/<some-name>
node -e "import('./ai/services.mjs')"
# Error [ERR_MODULE_NOT_FOUND]: Cannot find module '.../ai/mcp/server/github-workflow/config.mjs'
```

Root cause: `.gitignore` excludes `ai/mcp/server/*/config.mjs` (these are copy-from-template files for local overrides). Worktrees inherit the tracked tree but none of the ignored files, so every `config.mjs` is missing.

Attempting symlinks from the main checkout triggers a SECOND bug:

```bash
ln -s /main/checkout/.../config.mjs ai/mcp/server/<srv>/config.mjs
# Running any Playwright test that imports via the service chain:
# Error: Namespace collision in unitTestMode for Neo.core.Base
```

The symlinked `config.mjs` imports `Base.mjs` via a relative path, which Node resolves against the real (canonical) path of the symlink target — the main checkout's `src/core/Base.mjs`. When the worktree-local `src/core/Base.mjs` ALSO gets imported (by the test itself), `Neo.setupClass` sees the same namespace registered from two different file paths and throws under `unitTestMode: true`. Copies (not symlinks) work because Node resolves relatives against the copy's own location.

## Impact

Every Claude Code session in a new worktree that touches `ai/scripts/`, `ai/services.mjs`, or any SDK-consuming code hits this wall. Discovered during #10090 and #10092 work — fixed both times by manually copying four `config.mjs` files. That's institutional tax per session.

## The Ask

Add a bootstrap script and a startup note:

1. **`ai/scripts/bootstrapWorktree.mjs`** — copies `ai/mcp/server/{github-workflow,knowledge-base,memory-core,neural-link}/config.mjs` from the repo's main checkout (resolved via `git worktree list --porcelain`) into the current worktree. Idempotent; skips files that already exist. Refuses to run if CWD is the main checkout itself.

2. **`AGENTS_STARTUP.md §Harness-Memory-Wiring`** — append a Claude Code note:
   > **Worktree bootstrap:** `ai/mcp/server/*/config.mjs` is gitignored and does not travel into new worktrees. Before running any SDK-consuming script or `test-unit` that imports `ai/services.mjs`, run `node ai/scripts/bootstrapWorktree.mjs`. Do NOT use symlinks — they break under `unitTestMode` due to realpath-based resolution of `Base.mjs`.

## Acceptance Criteria

- [ ] `ai/scripts/bootstrapWorktree.mjs` — resolves main checkout via git, copies four `config.mjs` files, idempotent
- [ ] Refuses to run when CWD is the main checkout (no-op safety)
- [ ] Playwright spec verifying copy-behavior against a fake main-checkout tmp dir
- [ ] `AGENTS_STARTUP.md` updated with the worktree bootstrap note under Harness-Memory-Wiring

## Origin Session ID
`d9eb5e76-5430-45f7-b3ea-8600664d28f9`

## Timeline

- 2026-04-19T12:20:26Z @tobiu added the `bug` label
- 2026-04-19T12:20:26Z @tobiu added the `contributor-experience` label
- 2026-04-19T12:20:26Z @tobiu added the `ai` label
- 2026-04-19T12:52:00Z @tobiu assigned to @tobiu
- 2026-04-19T13:07:39Z @tobiu referenced in commit `69b1e23` - "feat(ai): ai/scripts/bootstrapWorktree.mjs + AGENTS_STARTUP note (#10095)

Every fresh Claude Code worktree under .claude/worktrees/ hits
ERR_MODULE_NOT_FOUND the first time a script imports ai/services.mjs
because ai/mcp/server/<name>/config.mjs is gitignored and worktrees
inherit only the tracked tree. Symlinks are not a workaround — they
resolve relative imports against the canonical target path, so the
symlinked config.mjs loads Base.mjs from the main checkout, which
collides with the worktree-local Base.mjs under unitTestMode and throws
'Namespace collision in unitTestMode for Neo.core.Base'. Copies have
their own canonical path inside the worktree and resolve correctly.

Changes:
- ai/scripts/bootstrapWorktree.mjs exports two functions:
  (1) resolveMainCheckout(cwd): parses 'git worktree list --porcelain';
      the first 'worktree <path>' line is always the primary working
      tree regardless of invocation directory.
  (2) bootstrapWorktree({mainCheckout, projectRoot, configs, log}):
      pure copy logic, idempotent, refuses to run when invoked against
      the main checkout (no-op). Splits into copied/skipped/missing
      buckets for observable outcomes.
  CLI entry point resolves mainCheckout via git and logs a one-line
  summary. Invocation: node ai/scripts/bootstrapWorktree.mjs.

- AGENTS_STARTUP.md gains a 'Worktree Bootstrap (Claude Code)'
  subsection under Harness Memory-File Wiring. Documents the ERR, the
  required bootstrap command, and the symlink trap with its exact error
  signature so future agents don't waste turns rediscovering it.

- test/playwright/unit/ai/scripts/bootstrapWorktree.spec.mjs covers
  five cases against tmp fake-main-checkout + fake-worktree dirs:
  exports the canonical config list, copies all missing, idempotent
  on partial seed (preserves existing), no-op from main checkout,
  reports missing-in-source without throwing.

Live CLI verification:
  rm ai/mcp/server/github-workflow/config.mjs
  node ai/scripts/bootstrapWorktree.mjs
  # copied: ai/mcp/server/github-workflow/config.mjs
  # skip (exists): ai/mcp/server/knowledge-base/config.mjs
  # skip (exists): ai/mcp/server/memory-core/config.mjs
  # skip (exists): ai/mcp/server/neural-link/config.mjs"
- 2026-04-19T13:10:27Z @tobiu cross-referenced by PR #10099
- 2026-04-19T13:30:11Z @tobiu referenced in commit `4152a88` - "feat(ai): ai/scripts/bootstrapWorktree.mjs + AGENTS_STARTUP note (#10095) (#10099)

Every fresh Claude Code worktree under .claude/worktrees/ hits
ERR_MODULE_NOT_FOUND the first time a script imports ai/services.mjs
because ai/mcp/server/<name>/config.mjs is gitignored and worktrees
inherit only the tracked tree. Symlinks are not a workaround — they
resolve relative imports against the canonical target path, so the
symlinked config.mjs loads Base.mjs from the main checkout, which
collides with the worktree-local Base.mjs under unitTestMode and throws
'Namespace collision in unitTestMode for Neo.core.Base'. Copies have
their own canonical path inside the worktree and resolve correctly.

Changes:
- ai/scripts/bootstrapWorktree.mjs exports two functions:
  (1) resolveMainCheckout(cwd): parses 'git worktree list --porcelain';
      the first 'worktree <path>' line is always the primary working
      tree regardless of invocation directory.
  (2) bootstrapWorktree({mainCheckout, projectRoot, configs, log}):
      pure copy logic, idempotent, refuses to run when invoked against
      the main checkout (no-op). Splits into copied/skipped/missing
      buckets for observable outcomes.
  CLI entry point resolves mainCheckout via git and logs a one-line
  summary. Invocation: node ai/scripts/bootstrapWorktree.mjs.

- AGENTS_STARTUP.md gains a 'Worktree Bootstrap (Claude Code)'
  subsection under Harness Memory-File Wiring. Documents the ERR, the
  required bootstrap command, and the symlink trap with its exact error
  signature so future agents don't waste turns rediscovering it.

- test/playwright/unit/ai/scripts/bootstrapWorktree.spec.mjs covers
  five cases against tmp fake-main-checkout + fake-worktree dirs:
  exports the canonical config list, copies all missing, idempotent
  on partial seed (preserves existing), no-op from main checkout,
  reports missing-in-source without throwing.

Live CLI verification:
  rm ai/mcp/server/github-workflow/config.mjs
  node ai/scripts/bootstrapWorktree.mjs
  # copied: ai/mcp/server/github-workflow/config.mjs
  # skip (exists): ai/mcp/server/knowledge-base/config.mjs
  # skip (exists): ai/mcp/server/memory-core/config.mjs
  # skip (exists): ai/mcp/server/neural-link/config.mjs"
- 2026-04-19T13:30:12Z @tobiu closed this issue
- 2026-04-26T00:39:49Z @neo-opus-ada cross-referenced by #10351
- 2026-04-26T00:46:07Z @neo-opus-ada cross-referenced by PR #10352
- 2026-04-27T16:05:28Z @neo-opus-ada cross-referenced by #10432
- 2026-04-27T18:01:58Z @neo-opus-ada cross-referenced by PR #10436
- 2026-04-27T18:09:32Z @tobiu referenced in commit `63ccfd5` - "feat(bootstrap): support independent-clone canonical via --canonical-root (#10435) (#10436)

Closes #10435. Extends #10433's granular per-subdir symlinking to support
independent clones (e.g., an Antigravity-side clone mirroring the canonical
github-side clone) — same gitignore-boundary discipline, same per-subdir
result map, same `concepts/`-protection invariant.

**The Problem:**
`bootstrapWorktree.mjs --link-data` resolves canonical via `git worktree
list --porcelain`, which works for git worktrees (the original #10095 use
case). For independent clones — separate clones at sibling filesystem
paths, not worktrees — `git worktree list` returns the clone itself, and
the script correctly enters main-checkout mode (no-op). This left
independent clones unable to opt into substrate unification through the
script, despite needing the same architectural pattern.

Empirical anchor (2026-04-27): the `antigravity/neomjs/neo` clone had
been hand-curated months ago with manual `ln -s` calls that mirror the
exact #10433 pattern (`backups`, `chroma`, `datasets`, `neo-sqlite`,
`sqlite` → canonical; `concepts/` left as regular dir). When `wake-daemon/`
became a load-bearing subdir (per #10423/#10425), the clone needed it
manually re-symlinked. This ticket codifies what was previously
hand-curated.

**The Fix:**
- `resolveMainCheckout(cwd, {explicitRoot})` accepts an optional
  `explicitRoot` parameter; returns it (path-resolved to absolute) when
  set, else falls through to the existing git-worktree-list logic.
- CLI mode supports `--canonical-root <path>` flag and
  `NEO_AI_CANONICAL_ROOT` env var (flag takes precedence).
- The per-subdir symlink logic (#10433's `DATA_SUBDIRS_TO_LINK` +
  `symlinkDataDir`) is already topology-agnostic — once canonical is
  resolved correctly, the existing logic does the right thing. No
  changes needed beyond the resolution layer.
- File-head JSDoc broadened to enumerate the two supported topologies
  (worktrees vs independent clones) and link to #10435.

**What stays unchanged:**
- Worktree behavior (existing 21 tests pass without modification)
- `concepts/`-untouched invariant (worktree AND independent-clone cases)
- Per-subdir data-loss guard (`force=false` refuses to clobber non-symlink dirs)
- No `DATA_FILES_TO_LINK` allowlist (top-level `memory-core.sqlite` is
  unreferenced cruft per audit; symlinking it would propagate tech debt)

**Test evidence:**
- 4 new tests for `resolveMainCheckout` explicit-root behavior:
  - returns explicitRoot verbatim when absolute
  - resolves a relative explicitRoot to absolute via path.resolve
  - falls through to git resolution when explicitRoot is undefined
  - falls through correctly on null/empty (boundary safety)
- All 22 tests pass (5 existing config + 4 new for #10435 + 8 existing
  #10432 + 5 existing #10351)

**Out of scope:**
- Renaming the script (cross-family validated to keep `bootstrapWorktree.mjs`)
- Sibling-clone heuristic auto-discovery via origin URL matching (too much magic)
- Migration tooling for already-curated independent clones (one-time
  `--canonical-root` invocation handles them; no migration needed)
- Cleanup of unreferenced top-level `memory-core.sqlite` cruft (warrants
  its own tiny ticket if pursued — see #10435 body)

Cross-family-validated by @neo-gemini-pro: option 1 (keep filename,
broaden JSDoc) over rename. NO `DATA_FILES_TO_LINK` (file is confirmed cruft).

Co-authored-by: tobiu <tobiasuhlig78@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <claude@anthropic.com>"
- 2026-05-01T18:02:21Z @neo-opus-ada cross-referenced by #10591
- 2026-05-01T18:08:42Z @neo-opus-ada cross-referenced by PR #10592
- 2026-05-04T19:59:33Z @neo-opus-ada cross-referenced by #10714

