LearnNewsExamplesServices
Frontmatter
id17477
titleA worktree has no `node_modules`, so the Brain-tier detector reports absent for packages that load
stateClosed
labels
bugaitestingagent-os
assigneesneo-opus-ada
createdAtAug 21, 2026, 4:54 PM
updatedAtAug 21, 2026, 6:35 PM
githubUrlhttps://github.com/neomjs/neo/issues/17477
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 21, 2026, 6:35 PM

A worktree has no node_modules, so the Brain-tier detector reports absent for packages that load

Closed Backlog/active-chunk-18 bugaitestingagent-os
neo-opus-ada
neo-opus-ada commented on Aug 21, 2026, 4:54 PM

Context

Every maintainer works from linked git worktrees — nine were live on this host while filing. npm install runs once, in the main clone, so a linked worktree has no node_modules of its own. Node's resolver walks upward and finds the clone's, so imports work from the worktree exactly as they do from the clone.

The Playwright unit config does not walk upward.

The Problem

hasBrainTier(rootDir) in test/playwright/playwright.config.unit.mjs:89-97 checks a hard-coded path:

entrypoints.every(entry => existsSync(path.join(rootDir, 'node_modules', pkg, entry)))

rootDir is repoRoot, derived from the config file's own location — the worktree. So from any linked worktree the check is false, buildProjects({brainPresent: false}) returns [bodyBulk] alone, and the whole Brain matrix leaves the project list.

Measured on this host, from .claude/worktrees/ada-17338:

$ npx playwright test --config=test/playwright/playwright.config.unit.mjs --project=unit-brain -g "maintenance health"
[playwright.config.unit] Brain-tier set not installed (see package.brain.json) — skipping chroma-setup + unit-brain* projects. Run `npm run install-brain` to arm them.
Error: Project(s) "unit-brain" not found. Available projects: "unit"

The banner is false, and it is falsifiable in one line — the packages it names load fine from that same directory:

$ node -e "import('chromadb').then(()=>console.log('ok'))"      # ok
$ node -e "import('better-sqlite3').then(()=>console.log('ok'))" # ok
$ node -e "import('@chroma-core/default-embed').then(()=>console.log('ok'))" # ok

npm run install-brain — what the banner tells you to run — reports the tier already armed and changes nothing, because nothing is missing. Symlinking node_modules into the worktree fixes it immediately, which is the confirming control: the tier was always installed, only the lookup was wrong.

The Architectural Reality

  • test/playwright/playwright.config.unit.mjs:89-97hasBrainTier(rootDir), the only path-based check.
  • :125-168buildProjects({brainPresent}), which returns [bodyBulk] alone when false, dropping chroma-setup, unit-brain, unit-brain-knowledge-base-config, unit-brain-memory-core-config, and chroma-teardown.
  • :108-116assertBrainTierForEnvironment throws when isCI, with the reason stated in its own docblock: "a skipped brain matrix on a green CI run is silent coverage loss, so it must fail before collection."

That guard is the argument for this ticket. The condition is already understood to be serious enough to fail a run — and it is armed only where no human is watching. Locally, the same condition is one console.info.

The Fix

CORRECTED after reading the existing specs. The first version of this section proposed createRequire(...).resolve(pkg + '/' + entry), and that fix is wrong — it would have silently deleted the guard this probe exists for. Left visible rather than rewritten clean, because anyone implementing the original would ship the regression.

Why require.resolve is the wrong tool. chromaProcess.spec.mjs pins a four-state husk ladder: three bare directories → false; entrypoints present but no build/Release/better_sqlite3.nodefalse; native artifact added → true. Resolution answers "is there an entrypoint", never "did the native build produce its artifact", so it reports armed for exactly the broken-build case the docblock calls "the thing a broken build actually loses".

The actual fix: resolve the package DIRECTORY, keep the file checks inside it. One helper doing Node's upward walk, with first-match-wins and no fall-through:

export function resolvePackageDir(fromDir, pkg) {
    let current = path.resolve(fromDir);
    for (;;) {
        const candidate = path.join(current, 'node_modules', pkg);
        if (fs.existsSync(candidate)) return candidate;
        const parent = path.dirname(current);
        if (parent === current) return null;
        current = parent
    }
}

hasBrainTier then checks its entrypoints under the resolved directory instead of a joined one — husk detection unchanged, worktrees fixed, rootDir still injected so the existing pure specs keep exercising both tiers.

Second layer, same defect. test/playwright/chromaProcess.mjs:141 joins the same way for the CLI it spawns:

const cliPath = path.join(repoRoot, 'node_modules', 'chromadb', 'dist', 'cli.mjs');

Fixing only the probe arms the projects and then dies at chroma-setup with "Chroma exited before its heartbeat became ready" — a symptom two layers from node <path-that-does-not-exist>. Measured, in that order. Both layers share the one helper, which lives in chromaProcess.mjs because that module already owns where the chromadb package is; the config imports it, and there is no cycle (chromaProcess.mjs imports only node builtins).

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
hasBrainTier test/playwright/playwright.config.unit.mjs Reports present whenever the three entrypoints resolve, from a clone or a linked worktree unchanged false when they genuinely do not resolve function docblock --project=unit-brain runs from a worktree with no local node_modules
the skip banner same Emitted only when the tier is genuinely absent unchanged text banner absent from a worktree run
assertBrainTierForEnvironment same UNCHANGED — still throws in CI existing arms stay green
resolvePackageDir (new export) test/playwright/chromaProcess.mjs Node's upward walk, first match wins, directory only — a regular file is not a package null, which every caller treats as not-installed function docblock worktree + nearest-husk + regular-file arms
CHROMA_CLI_ENTRYPOINT (new export) same One constant naming the artifact chroma-setup executes, consumed by both the probe and the spawn function docblock the husk ladder now fails without it
startChromaProcess CLI resolution same Resolves the package from repoRoot; verifies the CLI artifact before spawnFn and throws naming the missing entrypoint unlocatable package throws naming the package; neither reaches spawn function docblock located-but-partial arm, with a spawn spy asserting non-reach
startChromaProcess resolveFn (new option) same Injection seam matching the existing spawnFn / probeFn, so a spec controls the walk defaults to resolvePackageDir JSDoc @param unlocatable arm injects () => null

Decision Record impact

none.

Acceptance Criteria

  • --project=unit-brain resolves AND its chroma-setup boots from a linked worktree that has no node_modules of its own, with the Brain tier installed in the main clone. Both layers, because the first alone stops at the heartbeat.
  • The skip banner is not emitted in that case. Control: a tier that is genuinely absent still emits it and still drops the projects — asserting only that the banner disappeared would also pass on a detector that always returns true.
  • The husk ladder holds, extended by one rung. Three bare directories → entrypoints without the native artifact → the artifact → still false until dist/cli.mjs exists → true. This is the arm that rejects require.resolve, so it is the one to run first on any alternative implementation.
  • Admission proves the artifact its dependent EXECUTES. hasBrainTier and startChromaProcess must agree on chromadb's CLI entrypoint. A package carrying dist/chromadb.mjs without dist/cli.mjs is refused by both — admission returns false, and the spawn throws naming the missing entrypoint without reaching spawnFn. Control: adding that one file reaches spawn on the same fixture.
  • CONTROL: the nearest node_modules decides. A husked copy beside you must stay a husk even when an intact copy sits in an ancestor. Without this, "resolve upward" quietly becomes "search until something works" and the husk ladder above passes vacuously.
  • A regular file named like a package does not satisfy resolvePackageDir — a symlink to a directory still does.
  • An unlocatable chromadb names itself at the spawn site rather than surfacing as a heartbeat timeout.
  • No fixture lets the host decide the result. No assertion reads path.parse(root).root; termination is proven with a package name no real install can carry, and the unlocatable case injects its resolver.
  • assertBrainTierForEnvironment still throws under isCI with a genuinely absent tier; its existing arms are unchanged.
  • The existing pure-by-injection buildProjects specs still exercise both tiers without touching the filesystem.

Out of Scope

  • The configTemplateResolver failure. Running the config without --project fails on this host at test/playwright/configTemplateResolver.mjs:96, from a worktree, with and without the symlink, and from a detached worktree too. It is therefore not this defect and not caused by it — but it does mean I could not exercise the plain no---project run to measure what a peer's default invocation loses. What is measured here is the --project=unit-brain path; the wider silent-narrowing is what buildProjects returns by inspection, not something I ran. Worth its own look by someone who can reproduce it.
  • .claude/ and .codex/ worktree bootstrap generally.
  • The Brain-tier package set itself.

Avoided Traps

  • Do not "fix" this by documenting the symlink. A workaround in a README is read by people who already know; the peer this costs is the one who reads a banner, believes it, runs the command it suggests, and gets a narrower green.
  • Do not relax the CI assertion. It is the half that works. The detector is wrong, not the policy.
  • Do not assume npm install in each worktree is the answer. Nine worktrees of node_modules is a disk and drift cost to fix a lookup.

Related

  • #16649, #16488, #17239 — Brain-tier boundary work; none touch the detector.
  • #15874, #16885unit-brain isolation defects; both assume the matrix runs at all.

Live latest-open sweep: checked latest 20 open issues at 2026-08-21T14:51:36Z, plus keyword sweeps for brain tier / hasBrainTier / worktree node_modules / unit-brain skip; no equivalent found.

Origin Session ID: ab15d2b8-eb14-4237-ad18-ce48584b2d07

Retrieval Hint: query_raw_memories("hasBrainTier worktree node_modules detector resolver disagree unit-brain project not found")

tobiu referenced in commit 1fa742f - "fix(test): the Brain-tier probe resolves node_modules instead of joining it (#17477) (#17480) on Aug 21, 2026, 6:35 PM
tobiu closed this issue on Aug 21, 2026, 6:35 PM