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'))" # oknpm 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-97 — hasBrainTier(rootDir), the only path-based check.
:125-168 — buildProjects({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-116 — assertBrainTierForEnvironment 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.node → false; 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
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, #16885 —
unit-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")
Context
Every maintainer works from linked git worktrees — nine were live on this host while filing.
npm installruns once, in the main clone, so a linked worktree has nonode_modulesof 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)intest/playwright/playwright.config.unit.mjs:89-97checks a hard-coded path:entrypoints.every(entry => existsSync(path.join(rootDir, 'node_modules', pkg, entry)))rootDirisrepoRoot, 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: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'))" # oknpm run install-brain— what the banner tells you to run — reports the tier already armed and changes nothing, because nothing is missing. Symlinkingnode_modulesinto 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-97—hasBrainTier(rootDir), the only path-based check.:125-168—buildProjects({brainPresent}), which returns[bodyBulk]alone when false, droppingchroma-setup,unit-brain,unit-brain-knowledge-base-config,unit-brain-memory-core-config, andchroma-teardown.:108-116—assertBrainTierForEnvironmentthrows whenisCI, 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
Why
require.resolveis the wrong tool.chromaProcess.spec.mjspins a four-state husk ladder: three bare directories →false; entrypoints present but nobuild/Release/better_sqlite3.node→false; 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 } }hasBrainTierthen checks its entrypoints under the resolved directory instead of a joined one — husk detection unchanged, worktrees fixed,rootDirstill injected so the existing pure specs keep exercising both tiers.Second layer, same defect.
test/playwright/chromaProcess.mjs:141joins 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-setupwith "Chroma exited before its heartbeat became ready" — a symptom two layers fromnode <path-that-does-not-exist>. Measured, in that order. Both layers share the one helper, which lives inchromaProcess.mjsbecause that module already owns where the chromadb package is; the config imports it, and there is no cycle (chromaProcess.mjsimports only node builtins).Contract Ledger Matrix
hasBrainTiertest/playwright/playwright.config.unit.mjsfalsewhen they genuinely do not resolve--project=unit-brainruns from a worktree with no localnode_modulesassertBrainTierForEnvironmentresolvePackageDir(new export)test/playwright/chromaProcess.mjsnull, which every caller treats as not-installedCHROMA_CLI_ENTRYPOINT(new export)chroma-setupexecutes, consumed by both the probe and the spawnstartChromaProcessCLI resolutionrepoRoot; verifies the CLI artifact beforespawnFnand throws naming the missing entrypointstartChromaProcessresolveFn(new option)spawnFn/probeFn, so a spec controls the walkresolvePackageDir@param() => nullDecision Record impact
none.Acceptance Criteria
--project=unit-brainresolves AND itschroma-setupboots from a linked worktree that has nonode_modulesof its own, with the Brain tier installed in the main clone. Both layers, because the first alone stops at the heartbeat.dist/cli.mjsexists → true. This is the arm that rejectsrequire.resolve, so it is the one to run first on any alternative implementation.hasBrainTierandstartChromaProcessmust agree onchromadb's CLI entrypoint. A package carryingdist/chromadb.mjswithoutdist/cli.mjsis refused by both — admission returns false, and the spawn throws naming the missing entrypoint without reachingspawnFn. Control: adding that one file reaches spawn on the same fixture.node_modulesdecides. 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.resolvePackageDir— a symlink to a directory still does.chromadbnames itself at the spawn site rather than surfacing as a heartbeat timeout.path.parse(root).root; termination is proven with a package name no real install can carry, and the unlocatable case injects its resolver.assertBrainTierForEnvironmentstill throws underisCIwith a genuinely absent tier; its existing arms are unchanged.buildProjectsspecs still exercise both tiers without touching the filesystem.Out of Scope
configTemplateResolverfailure. Running the config without--projectfails on this host attest/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---projectrun to measure what a peer's default invocation loses. What is measured here is the--project=unit-brainpath; the wider silent-narrowing is whatbuildProjectsreturns by inspection, not something I ran. Worth its own look by someone who can reproduce it..claude/and.codex/worktree bootstrap generally.Avoided Traps
npm installin each worktree is the answer. Nine worktrees ofnode_modulesis a disk and drift cost to fix a lookup.Related
unit-brainisolation 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")