---
id: 10435
title: 'bootstrapWorktree: support independent-clone canonical via --canonical-root'
state: CLOSED
labels:
  - enhancement
  - ai
  - architecture
assignees:
  - neo-opus-ada
createdAt: '2026-04-27T17:57:50Z'
updatedAt: '2026-06-07T17:22:12Z'
githubUrl: 'https://github.com/neomjs/neo/issues/10435'
author: neo-opus-ada
commentsCount: 0
parentIssue: null
subIssues: []
subIssuesCompleted: 0
subIssuesTotal: 0
blockedBy: []
blocking: []
closedAt: '2026-04-27T18:09:31Z'
---
# bootstrapWorktree: support independent-clone canonical via --canonical-root

## Context

Follow-up to #10433 (granular per-subdir symlinking for git worktrees). Surfaced empirically during the post-#10433 verification turn (2026-04-27), with cross-family agreement from `@neo-gemini-pro` (Antigravity).

`bootstrapWorktree.mjs` currently uses `git worktree list --porcelain` to resolve canonical. This works perfectly for git worktrees off a shared canonical checkout, but skips **independent clones** (e.g., `/Users/Shared/antigravity/neomjs/neo/` is a separate clone, not a worktree of `/Users/Shared/github/neomjs/neo/`). When the script runs in an independent clone, it correctly identifies that as the "main checkout" and skips the symlink operations — leaving the clone's `.neo-ai-data/wake-daemon/` (and other subdirs) as physical isolated dirs.

Empirical anchor: 2026-04-27, the bridge daemon ran in canonical (PID 12039), logging to canonical's `bridge.log`. The Antigravity clone's IDE was tailing its OWN local `.neo-ai-data/wake-daemon/bridge.log` (silent, since the daemon wasn't writing there). `@neo-gemini-pro` manually `rm -rf + ln -s`'d `wake-daemon/` to bridge the gap. The other 6 substrate-data subdirs in the Antigravity clone had been hand-curated to the same pattern back on April 22.

## The Problem

The Antigravity clone's `.neo-ai-data/` was already manually configured to **identical structure as a post-#10433 worktree**:

| Subdir | Antigravity clone | Worktree (post-#10433) |
|---|---|---|
| `backups`, `chroma`, `datasets`, `neo-sqlite`, `sqlite` | symlinks → canonical | symlinks → canonical |
| `concepts/` | regular dir ✓ | regular dir ✓ |
| `wake-daemon` | symlink → canonical (manually added 2026-04-27) | symlink → canonical |

Same architectural pattern, same gitignore-boundary discipline (concepts/ untouched), same per-subdir granularity. The only difference is canonical-resolution mechanism: git worktrees use `git worktree list`, independent clones need an explicit override.

## The Architectural Reality

`ai/scripts/bootstrapWorktree.mjs:resolveMainCheckout` is the single chokepoint that distinguishes worktree-mode from independent-clone-mode. Currently:

```javascript
export async function resolveMainCheckout(cwd) {
    const {stdout} = await execFileAsync('git', ['worktree', 'list', '--porcelain'], {cwd});
    const match    = stdout.match(/^worktree (.+)$/m);
    return match ? match[1] : null;
}
```

For independent clones, `git worktree list --porcelain` returns the clone's OWN root as the first entry. The script then sees `path.resolve(projectRoot) === path.resolve(mainCheckout)` and correctly enters main-checkout mode (no-op). The fix: layer an explicit canonical-root override on top.

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.

## The Fix

Extend `resolveMainCheckout` with an explicit-override option:

```javascript
export async function resolveMainCheckout(cwd, {explicitRoot} = {}) {
    if (explicitRoot) return path.resolve(explicitRoot);
    const {stdout} = await execFileAsync('git', ['worktree', 'list', '--porcelain'], {cwd});
    const match    = stdout.match(/^worktree (.+)$/m);
    return match ? match[1] : null;
}
```

CLI surface (additive — worktree case unchanged):

```bash
# Worktree case (existing behavior)
node ai/scripts/bootstrapWorktree.mjs --link-data

# Independent-clone case (new)
node ai/scripts/bootstrapWorktree.mjs --link-data --canonical-root /Users/Shared/github/neomjs/neo

# Or via env var (composable in CI / shell aliases)
NEO_AI_CANONICAL_ROOT=/Users/Shared/github/neomjs/neo \
    node ai/scripts/bootstrapWorktree.mjs --link-data
```

CLI handler resolves `--canonical-root` flag → falls back to `NEO_AI_CANONICAL_ROOT` env var → falls back to git-worktree-list (the current behavior, unchanged).

## Decision: Keep the filename `bootstrapWorktree.mjs`

Cross-family-validated by `@neo-gemini-pro`: option 1 (keep filename, broaden file-head JSDoc) over rename. Rationale:

- Renames break muscle memory and external documentation references
- Scope change is small (resolution-layer extension)
- File-head JSDoc already discusses "data symlinking across worktree+main checkouts" — broadening to include "sibling independent clones" is a documentation tweak

## Decision: NO `DATA_FILES_TO_LINK` allowlist (top-level cruft cleanup instead)

Empirical investigation of the bonus question (whether to add file-symlink support for `.neo-ai-data/memory-core.sqlite`):

- Zero references in any source file. The only `'memory-core.sqlite'` literal is in `ai/mcp/server/neural-link/config.template.mjs:36`, which resolves to `path.join(os.homedir(), '.neo-ai-data', 'memory-core.sqlite')` — a different filesystem location entirely (`~/.neo-ai-data/...`).
- File is 0 bytes since 2026-04-13 (14 days, never written to)
- Not git-tracked
- Substrate has moved into `sqlite/` and `neo-sqlite/` subdirs

**Verdict:** pure tech debt from an old code path. Adding `DATA_FILES_TO_LINK` would propagate cruft. Better to clean it up: delete the file from canonical (it's gitignored and unreferenced; broken symlinks in clones are harmless because nothing reads the path).

This may belong as a separate cleanup ticket if the broken-symlink-in-clones cosmetic concern warrants it; for the scope of this ticket, the recommendation is "no file-symlink support, plus a note that the existing top-level file can be safely deleted from canonical."

## Acceptance Criteria

- [ ] `resolveMainCheckout(cwd, {explicitRoot})` accepts an optional `explicitRoot` parameter; returns it directly when set
- [ ] CLI mode supports `--canonical-root <path>` flag
- [ ] CLI mode supports `NEO_AI_CANONICAL_ROOT` env var (flag takes precedence)
- [ ] Running with explicit canonical-root from an independent clone applies the existing per-subdir symlink logic correctly
- [ ] `concepts/` is still NEVER symlinked (#10433 invariant preserved)
- [ ] Worktree behavior unchanged (existing tests pass)
- [ ] Test coverage: explicit-root resolution, env-var fallback, flag-precedence-over-env-var, full per-subdir flow from independent-clone perspective

## Out of Scope

- Renaming the script or its exports (cross-family validated to skip)
- Adding `DATA_FILES_TO_LINK` for `.neo-ai-data/memory-core.sqlite` (confirmed cruft; cleanup vs propagation is a separate concern)
- Sibling-clone heuristic auto-discovery (e.g., walking parent dirs to find clones with matching origin URL) — out of scope, too much magic
- Migration tooling for existing manually-curated independent clones (one-time `--canonical-root` invocation handles them; no migration needed)
- Cleanup of the unreferenced top-level `memory-core.sqlite` cruft file (warrants its own tiny ticket if pursued)

## Avoided Traps

- **Trap:** Auto-discover sibling clones via `git config remote.origin.url` matching. **Avoided:** too much filesystem-walk + URL-comparison magic; explicit override is more transparent and predictable.
- **Trap:** Rename the script to reflect the broader scope. **Avoided:** rename cost > clarity value at this scope; cross-family validated to keep the filename.
- **Trap:** Add `DATA_FILES_TO_LINK` to mirror the antigravity clone's `memory-core.sqlite` symlink. **Avoided:** empirical evidence shows the top-level file is unreferenced 0-byte cruft; symlinking it would propagate the tech debt across substrates.

## Related

- Predecessor: #10433 (granular per-subdir symlinking that this extends to independent clones)
- Closed earlier predecessor: #10224 (coarse-grained parent-level symlink)
- Substrate-level coherence the granular fix unblocks: #10424
- Singleton enforcement that depends on `wake-daemon/` symlinking: #10423, #10425

Origin Session ID: b3c0bfb8-44e1-4646-9c62-110ef16b0fad

Retrieval Hint: "bootstrapWorktree independent clone canonical-root antigravity sibling checkout"

## Timeline

- 2026-04-27T17:57:52Z @neo-opus-ada added the `enhancement` label
- 2026-04-27T17:57:52Z @neo-opus-ada added the `ai` label
- 2026-04-27T17:57:52Z @neo-opus-ada added the `architecture` label
- 2026-04-27T17:58:54Z @neo-opus-ada assigned to @neo-opus-ada
- 2026-04-27T18:01:58Z @neo-opus-ada cross-referenced by PR #10436
- 2026-04-27T18:09:32Z @tobiu closed this issue
- 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-10T16:29:07Z @neo-gpt cross-referenced by #11135
- 2026-05-19T15:39:04Z @neo-opus-ada cross-referenced by #11654

