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:
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:
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):
<h1 class="neo-h1" data-record-id="6">Worktree case (existing behavior)</h1>
node ai/scripts/bootstrapWorktree.mjs --link-data
<h1 class="neo-h1" data-record-id="7">Independent-clone case (new)</h1>
node ai/scripts/bootstrapWorktree.mjs --link-data --canonical-root /Users/Shared/github/neomjs/neo
<h1 class="neo-h1" data-record-id="8">Or via env var (composable in CI / shell aliases)</h1>
NEO_AI_CANONICAL_ROOT=/Users/Shared/github/neomjs/neo \
node ai/scripts/bootstrapWorktree.mjs --link-dataCLI 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
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"
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.mjscurrently usesgit worktree list --porcelainto 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-promanuallyrm -rf + ln -s'dwake-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:backups,chroma,datasets,neo-sqlite,sqliteconcepts/wake-daemonSame 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:resolveMainCheckoutis the single chokepoint that distinguishes worktree-mode from independent-clone-mode. Currently: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 --porcelainreturns the clone's OWN root as the first entry. The script then seespath.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
resolveMainCheckoutwith an explicit-override option: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):
<h1 class="neo-h1" data-record-id="6">Worktree case (existing behavior)</h1> node ai/scripts/bootstrapWorktree.mjs --link-data <h1 class="neo-h1" data-record-id="7">Independent-clone case (new)</h1> node ai/scripts/bootstrapWorktree.mjs --link-data --canonical-root /Users/Shared/github/neomjs/neo <h1 class="neo-h1" data-record-id="8">Or via env var (composable in CI / shell aliases)</h1> NEO_AI_CANONICAL_ROOT=/Users/Shared/github/neomjs/neo \ node ai/scripts/bootstrapWorktree.mjs --link-dataCLI handler resolves
--canonical-rootflag → falls back toNEO_AI_CANONICAL_ROOTenv var → falls back to git-worktree-list (the current behavior, unchanged).Decision: Keep the filename
bootstrapWorktree.mjsCross-family-validated by
@neo-gemini-pro: option 1 (keep filename, broaden file-head JSDoc) over rename. Rationale:Decision: NO
DATA_FILES_TO_LINKallowlist (top-level cruft cleanup instead)Empirical investigation of the bonus question (whether to add file-symlink support for
.neo-ai-data/memory-core.sqlite):'memory-core.sqlite'literal is inai/mcp/server/neural-link/config.template.mjs:36, which resolves topath.join(os.homedir(), '.neo-ai-data', 'memory-core.sqlite')— a different filesystem location entirely (~/.neo-ai-data/...).sqlite/andneo-sqlite/subdirsVerdict: pure tech debt from an old code path. Adding
DATA_FILES_TO_LINKwould 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 optionalexplicitRootparameter; returns it directly when set--canonical-root <path>flagNEO_AI_CANONICAL_ROOTenv var (flag takes precedence)concepts/is still NEVER symlinked (#10433 invariant preserved)Out of Scope
DATA_FILES_TO_LINKfor.neo-ai-data/memory-core.sqlite(confirmed cruft; cleanup vs propagation is a separate concern)--canonical-rootinvocation handles them; no migration needed)memory-core.sqlitecruft file (warrants its own tiny ticket if pursued)Avoided Traps
git config remote.origin.urlmatching. Avoided: too much filesystem-walk + URL-comparison magic; explicit override is more transparent and predictable.DATA_FILES_TO_LINKto mirror the antigravity clone'smemory-core.sqlitesymlink. Avoided: empirical evidence shows the top-level file is unreferenced 0-byte cruft; symlinking it would propagate the tech debt across substrates.Related
wake-daemon/symlinking: #10423, #10425Origin Session ID: b3c0bfb8-44e1-4646-9c62-110ef16b0fad
Retrieval Hint: "bootstrapWorktree independent clone canonical-root antigravity sibling checkout"