LearnNewsExamplesServices
Frontmatter
titlefeat(ai): ai/scripts/bootstrapWorktree.mjs + AGENTS_STARTUP note (#10095)
authortobiu
stateMerged
createdAtApr 19, 2026, 3:10 PM
updatedAtApr 19, 2026, 3:30 PM
closedAtApr 19, 2026, 3:30 PM
mergedAtApr 19, 2026, 3:30 PM
branchesdevagent/10095-worktree-bootstrap
urlhttps://github.com/neomjs/neo/pull/10099

5 passed (670ms)

Merged
tobiu
tobiu commented on Apr 19, 2026, 3:10 PM

Summary

Every fresh Claude Code worktree under .claude/worktrees/ hits ERR_MODULE_NOT_FOUND the first time any script imports ai/services.mjs:

Error [ERR_MODULE_NOT_FOUND]: Cannot find module '.../ai/mcp/server/github-workflow/config.mjs'

Root cause: ai/mcp/server/<name>/config.mjs is gitignored (copy-from-template files for local overrides), so fresh worktrees inherit the tracked tree without them. Symlinks from the main checkout are NOT a workaround — Node resolves relative imports inside a symlinked module against the canonical target path, so config.mjs's import Base from '../../../../src/core/Base.mjs' loads the main-checkout's Base.mjs. When the worktree-local Base.mjs also gets imported (e.g., by a Playwright spec), Neo.setupClass sees the same namespace registered from two different file paths and throws Namespace collision in unitTestMode for Neo.core.Base. Copies have their own canonical path inside the worktree and resolve correctly.

Impact

Discovered during #10090 (PR #10091) and again during #10092 (PR #10093). Both times the workaround was manually copying four config.mjs files from the main checkout — ~15 minutes of friction per session. Next fresh worktree session on #10030 (Concept Ontology) would hit the same wall. This PR unblocks every future Claude Code session.

Changes

  • ai/scripts/bootstrapWorktree.mjs exports:

    • resolveMainCheckout(cwd) — parses git worktree list --porcelain; the first worktree <path> line is always the primary working tree regardless of where the command is invoked.
    • 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. Uses process.argv[1]-vs-import.meta.url main guard so the module is importable by the spec without running the CLI.

  • AGENTS_STARTUP.md — new Worktree Bootstrap (Claude Code) subsection under Harness Memory-File Wiring documenting the error, the required bootstrap command, and the symlink trap with its exact error signature. Future agents don't rediscover it.

  • test/playwright/unit/ai/scripts/bootstrapWorktree.spec.mjs — five cases against tmp fake-main-checkout + fake-worktree dirs:

    1. Exports the canonical BOOTSTRAP_CONFIGS list.
    2. Copies all missing configs from main into worktree.
    3. Idempotent on partial seed (pre-existing local override preserved byte-for-byte).
    4. No-op when invoked against the main checkout itself.
    5. Reports configs missing in the main checkout without throwing.

Test Coverage

npm run test-unit -- test/playwright/unit/ai/scripts/bootstrapWorktree.spec.mjs

Live CLI Verification

rm ai/mcp/server/github-workflow/config.mjs
node ai/scripts/bootstrapWorktree.mjs
<h1 class="neo-h1" data-record-id="7">copied: ai/mcp/server/github-workflow/config.mjs</h1>

<h1 class="neo-h1" data-record-id="8">skip (exists): ai/mcp/server/knowledge-base/config.mjs</h1>

<h1 class="neo-h1" data-record-id="9">skip (exists): ai/mcp/server/memory-core/config.mjs</h1>

<h1 class="neo-h1" data-record-id="10">skip (exists): ai/mcp/server/neural-link/config.mjs</h1>

<h1 class="neo-h1" data-record-id="11">✓ Bootstrap complete: 1 copied, 3 skipped, 0 missing (4 total)</h1>

Commit Type

Chose feat per pull-request-workflow §3.1 — unlocks a new capability (fresh-worktree-ready in one command) that did not exist before. fix was considered since the underlying wall is a failure mode, but the fix IS the new capability, and worktrees weren't previously supported out of the box for SDK scripts.

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 (5 cases)
  • AGENTS_STARTUP.md updated with the worktree bootstrap note under Harness Memory-File Wiring

Test plan

  • npm run test-unit -- test/playwright/unit/ai/scripts/bootstrapWorktree.spec.mjs (5 passed)
  • Live CLI: delete + bootstrap cycle succeeds with correct copied/skipped counts
  • (Reviewer) On next Claude Code worktree session, run node ai/scripts/bootstrapWorktree.mjs and confirm any SDK-consuming script now works without manual intervention

Resolves #10095

tobiu
tobiu commented on Apr 19, 2026, 3:11 PM

Input from Claude Opus 4.7 (Claude Code):

❋ # PR Review Summary

Status: Comment (ready for human merge; non-blocking polish noted)

Self-Review Opening: Self-review of #10095. Chose feat over fix because the bootstrap command itself IS a new capability — worktrees weren't previously supported for SDK scripts out of the box. Structured as exported function + CLI guard for testability; the spec drives the pure function with tmp fake-main/fake-worktree dirs rather than spawning git, which avoids environment entanglement and runs in 670ms. Course-correction lesson from #10098 applied: the symlink trap was documented with its exact error signature BEFORE filing this PR, so a future agent can't even try the same doomed shortcut.


📊 Evaluation Metrics

  • [ARCH_ALIGNMENT]: 93 — Three concerns separated cleanly: git resolution (resolveMainCheckout), pure copy logic (bootstrapWorktree), and CLI glue. The pure function is what the spec exercises; the CLI is thin. Matches analyzeNlTelemetry.mjs precedent for ai/scripts/. Lost 7 points for the CLI's inline main guard — using process.argv[1] === fileURLToPath(import.meta.url) works but could break if invoked via an unusual launcher; Node 24+ has import.meta.main which is cleaner. Not using it because target Node versions aren't stated anywhere I could find.

  • [CONTENT_COMPLETENESS]: 94 — Top-of-file JSDoc explains both the bug AND the symlink trap inline (future agent reading source doesn't need to follow links). resolveMainCheckout and bootstrapWorktree both have structured @param / @returns blocks. PR body uses Fat Ticket convention. The AGENTS_STARTUP note is prescriptive ("Before running any SDK-consuming script...") not descriptive, so agents know what to DO, not just what's broken.

  • [EXECUTION_QUALITY]: 90 — Five Playwright cases cover the realistic failure modes: full copy, partial-seed idempotency, main-checkout refusal, missing-in-source handling, and the export contract. Live CLI round-trip verified end-to-end against the real repo (delete + bootstrap + confirm content). What's missing: (a) no spec for resolveMainCheckout itself — I treat the git-porcelain parsing as trusted; if git ever changes its output format the script breaks silently; (b) no coverage of the case where projectRoot === mainCheckout but the paths are expressed differently (trailing slash, symlinked parent, etc.) — path.resolve() should handle it but untested.

  • [PRODUCTIVITY]: 95 — Identified as highest-ROI of the three remaining tickets, filed, branched, built, tested, verified, and PR'd in one session. Chosen specifically to unblock the fresh-session pivot on #10030.

  • [IMPACT]: 82 — Every Claude Code worktree session is unblocked for SDK-consuming work forever. Compounds with every #10030-tier session that now starts running instead of debugging ERR_MODULE_NOT_FOUND. Not higher because the blast radius is the Claude Code harness specifically, not the framework.

  • [COMPLEXITY]: 30 — One new script, one doc edit, one spec. No framework changes.

  • [EFFORT_PROFILE]: Quick Win — High ROI for every future worktree session at minimal complexity.


🕸️ Context & Graph Linking

  • Target Epic / Issue ID: Resolves #10095
  • Related Graph Nodes:
    • #10091, #10093, #10098 (three prior PRs in this session that each discovered and manually worked around this bootstrap wall)
    • feedback_harness_isolation_models.md (the memory note about worktree vs shared-checkout branching models)
    • feedback_distributed_artifacts_invert_local_fs.md (lesson from #10098 about paths in distributed artifacts — conceptually adjacent: this PR is about paths in local-worktree artifacts)

🧠 Graph Ingestion Notes

  • [KB_GAP]: Two things are now documented in AGENTS_STARTUP that were undocumented before: (1) the ERR_MODULE_NOT_FOUND symptom on fresh worktrees, and (2) the symlink-under-unitTestMode collision. Both were discovered empirically during #10090 and #10092 — had the docs already had them, I would have saved ~30 minutes total across those two PRs. Reinforces the pattern that undocumented environmental pitfalls compound across agent sessions.

  • [KB_GAP]: ai/services.mjs SDK facade documents its imports via a JSDoc @module block (line 212-229) but doesn't mention that config.mjs files are a runtime dependency that may not be present. Adding a @precondition line pointing at bootstrapWorktree.mjs would make the failure mode discoverable from the consumer's side, not just the session-initialization side. Out of scope here.

  • [TOOLING_GAP]: Running the Playwright spec initially failed with SyntaxError: Missing semicolon. (3:60) because my JSDoc contained ai/mcp/server/*/config.mjs — the */ substring inside a /** ... */ comment terminates the block prematurely, turning the rest of the comment into invalid JavaScript. Babel's error message pointed at column 60 of line 3 which was a specific-but-still-cryptic pointer. Worth a repo-wide lint rule: forbid */ substrings inside JSDoc bodies. Out of scope; adding a rule would require choosing a linter direction I don't have context on.

  • [RETROSPECTIVE]: This PR turns a compounding-cost problem (every new agent session eats 15 min rediscovering the same workaround) into a one-time fix. The asymmetry is the point — friction baked into the initialization path multiplies across the swarm. Any future "agents keep hitting X" observation is a candidate for this treatment.

  • [RETROSPECTIVE]: The symlink-trap documentation is load-bearing. Without it, the next agent facing the ERR_MODULE_NOT_FOUND is reasonably likely to reach for ln -s first (it's the "obvious" Unix answer to "I need this file that exists over there") and then get a completely unrelated-looking Namespace collision in unitTestMode error. Documenting the trap with its exact error string means a future agent will grep for that string and land on the fix.


📋 Required Actions

No hard blockers. Optional polish items that could fold into a later cleanup PR or drop:

  • (Optional) Add a spec for resolveMainCheckout using a stubbed git output, guarding against git's porcelain format ever shifting.
  • (Optional) Switch the CLI isMain detection to import.meta.main once a Node baseline is chosen (Node 24+ supports it natively).
  • (Optional) Add a @precondition JSDoc line on ai/services.mjs pointing at bootstrapWorktree.mjs so consumers discover the dependency from either direction.
  • (Optional) Repo-level lint rule forbidding */ inside JSDoc bodies — would have caught my syntax error at edit time rather than at spec-run time.

None of these block merge. Reviewer can merge as-is and fold any that matter into a follow-up.