Context
Operator-surfaced 2026-05-10: the v12.1.0 release had github-workflow's projectRoot resolution working correctly for the consumer-workspace install pattern (Repo-A installs neo.mjs as a dependency, configures gh-workflow server via .gemini/settings.json to sync from a DIFFERENT repo Repo-B, and the synced files land in Repo-A's root, not inside Repo-A/node_modules/neo.mjs/).
Operator-quoted V-B-A anchor: "context of what worked in neo v12.1 => repo => install neo as a dependency. add e.g. .gemini/settings.json => configure the gh worklow server to a different repo. sync => into the repo root, not the neo node module. we had that working fine."
PR #11149 (closed-merged 2026-05-10, addressing #11147) replaced the v12.1 shape with a 3-tier heuristic. Then PR #11153 (closed-unmerged via Cycle-3 V-B-A retraction) attempted to extend with a hallucinated pnpm 4th tier. Today's drift cascade exposed that #11149's 3-tier heuristic itself diverges from the v12.1 working shape.
The Problem
ai/mcp/server/github-workflow/config.template.mjs (post-#11149) resolves projectRoot via:
let projectRoot;
if (process.env.NEO_WORKSPACE_ROOT) {
projectRoot = process.env.NEO_WORKSPACE_ROOT;
} else {
const nodeModulesIndex = __dirname.indexOf(`${path.sep}node_modules${path.sep}neo.mjs`);
if (nodeModulesIndex !== -1) {
projectRoot = __dirname.slice(0, nodeModulesIndex) || path.sep;
} else {
projectRoot = path.resolve(__dirname, '../../../../');
}
}
Three problems with this shape, in increasing severity:
Aesthetic (operator-flagged): __dirname.indexOf(\${path.sep}node_modules${path.sep}neo.mjs`)` is string-walking with separator interpolation. Operator-quoted: "this is UGLY. there is path.join() and other methods that resolve slashes cross OS nicer." The whole heuristic uses path-as-string semantics where path-as-structure semantics exist.
Substrate-divergent (right-hemisphere precedent): Sibling MCP configs all derive projectRoot (or equivalent neoRootDir) via path.resolve(__dirname, '../../../../') directly with no heuristic chains:
ai/mcp/server/memory-core/config.template.mjs: const neoRootDir = path.resolve(__dirname, '../../../../'); const cwd = neoRootDir;
ai/mcp/server/knowledge-base/config.template.mjs: const neoRootDir = path.resolve(__dirname, '../../../../');
ai/mcp/server/neural-link/config.template.mjs: const neoRootDir = path.resolve(__dirname, '../../../../'); const cwd = process.cwd();
None walks node_modules. None probes for env-var overrides.
Functional regression (operator-validated): v12.1.0 had github-workflow working with the consumer-workspace pattern operator described. Verified via git show 12.1.0:ai/mcp/server/github-workflow/config.mjs:
const packageRoot = path.resolve(__dirname, '../../../../');
const projectRoot = process.cwd() === '/' ? packageRoot : process.cwd();
This handles BOTH the / cwd edge (the symptom #11147 described) AND the consumer-workspace case (projectRoot = consumer's cwd, NOT node_modules/neo.mjs/). The process.cwd() === '/' check is the explicit guard against the MCP-host-context bug #11147 reported.
PR #11149's 3-tier heuristic broke the consumer-workspace expectation: for a consumer running npm run ai:mcp-server-github-workflow from Repo-A, the __dirname.indexOf('node_modules/neo.mjs') check fires and slices projectRoot back to Repo-A (correct) — but ONLY if Repo-A's neo install is at exactly node_modules/neo.mjs/. If neo is installed via any non-default layout (workspaces, monorepo tools, etc.), the heuristic mis-resolves.
The Architectural Reality
- The MCP server is invoked via
npm run ai:mcp-server-github-workflow, which means process.cwd() is the invocation workspace root by npm's contract — already the right answer in 99% of cases.
- The
process.cwd() === '/' edge case happens when the MCP host (Claude Code / Antigravity / Codex Desktop) spawns the server with cwd not propagated. v12.1's fallback to path.resolve(__dirname, '../../../../') correctly anchors to the install location in that case.
- These are the only two cases. Both handled cleanly by the v12.1 shape. The 3-tier heuristic addresses no third real case.
The shape v12.1.0 used is also conceptually equivalent to the LEFT-hemisphere build-script canonical (buildScripts/build/all.mjs, buildScripts/build/themes.mjs) which solves the same "find workspace root in repo OR consumer-install" problem via process.cwd() + package.json discriminator. The right-hemisphere github-workflow special case (project-data is deployment-context-specific, unlike Neo-internal substrate in kb/mc/nl) makes process.cwd() the natural primary source — matching v12.1.
The Fix
Revert ai/mcp/server/github-workflow/config.template.mjs projectRoot resolution to the exact v12.1.0 shape:
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const packageRoot = path.resolve(__dirname, '../../../../');
const projectRoot = process.cwd() === '/' ? packageRoot : process.cwd();
Scope is config.template.mjs ONLY. ai/services/github-workflow/toolService.mjs's defaultBranchDetector (introduced in #11146 + improved in #11149 to use config.projectRoot instead of bare process.cwd()) is the right shape and stays unchanged — once projectRoot resolves correctly via cwd-primary, downstream consumers of config.projectRoot (including the branch detector) inherit the correct value.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
config.template.mjs projectRoot resolution |
v12.1.0 working shape (operator-validated empirical anchor) |
process.cwd() === '/' ? packageRoot : process.cwd() |
None — both branches deterministic |
Inline JSDoc on the const declarations |
L2: git show 12.1.0:ai/mcp/server/github-workflow/config.mjs + sandbox-runtime via npm run test-unit |
Removed env-var NEO_WORKSPACE_ROOT |
n/a — removed |
Not consumed; documentation removed |
Use cwd directly |
n/a |
L1: grep confirms only this file consumes the env-var |
Removed __dirname.indexOf('node_modules/neo.mjs') heuristic |
n/a — removed |
Not consumed; logic deleted |
Use cwd directly |
n/a |
L1: only consumer was this file |
Acceptance Criteria
Out of Scope
- Adjusting kb/mc/nl projectRoot resolution to match github-workflow: rejected. Those are Neo-internal substrate (Memory Core memories, KB docs, Neural Link app target) —
neoRootDir-pinning via __dirname is correct for them. github-workflow is the outlier because its data is consumer-workspace-relative.
- Removing/modifying
defaultBranchDetector: out of scope. That's correct.
- Documenting consumer-workspace install pattern: could be a follow-up doc-ticket; not blocking the revert.
- Branch deletion of closed PR's stale branch
agent/11152-pnpm-layout-and-docs: orthogonal cleanup. Operator decision per shared-checkout/worktree identity incident handling.
Avoided Traps
- Follow #11147's stated prescription verbatim (
projectRoot = path.resolve(__dirname, '../../../../')): rejected. This is the kb/mc/nl right-hemisphere shape and is correct for THOSE configs, but breaks github-workflow's consumer-workspace contract. In a consumer install where npm i neo.mjs, this resolves to node_modules/neo.mjs/ — meaning the consumer's .gemini/settings.json-targeted issue sync would land in node_modules/neo.mjs/resources/content/issues/, polluting the neo package. Operator-confirmed v12.1 worked with files landing in repo root, not the node module. cwd-primary is required.
- Use
path.join or path.relative to improve indexOf aesthetic without changing semantics: rejected. Operator's "indexOf is ugly" critique was a substrate-quality signal, not a request for a cosmetic patch. The right fix removes the entire heuristic chain by returning to v12.1's cwd-primary shape — no path-walking at all.
- Preserve
NEO_WORKSPACE_ROOT env-var for future flexibility: rejected. Truth-in-code memory note applies — no production consumer exists. Speculative-support-code is worse than absent-support; remove unverified branches.
- Frame as new design instead of revert: rejected. v12.1.0 is the operator-validated working shape. Revert is precise, minimal, and respects substrate continuity (rather than introducing yet another shape).
- Bundle the toolService.mjs
defaultBranchDetector change: rejected. That change is correct (uses config.projectRoot once projectRoot is right). Bundling would inflate scope. Single-concern PR.
Related
- Parent regression: #11149 (merged, Gemini-authored, my reviewer of record) — introduced the 3-tier heuristic. This ticket reverts the projectRoot portion only.
- Original symptom ticket: #11147 (closed) — described the cwd=
/ symptom. v12.1.0's process.cwd() === '/' ? packageRoot : process.cwd() handles this correctly; #11149's heuristic was an over-engineering response.
- Closed-unmerged exploration: #11152 + #11153 — attempted pnpm 4th-tier extension. Now properly retired.
- Substrate-meta: #11154 — cross-PR reviewer-seeded drift discipline (extends pr-review-guide §7.4). This ticket is the third drift instance in the same cascade (plant → implement → retraction-V-B-A failure).
- Empirical anchor: v12.1.0 release tag —
git show 12.1.0:ai/mcp/server/github-workflow/config.mjs
- Right-hemisphere precedent:
ai/mcp/server/{memory-core,knowledge-base,neural-link}/config.template.mjs — all use path.resolve(__dirname, '../../../../') directly. github-workflow is the deliberate cwd-primary outlier due to consumer-workspace-relative data semantics.
Origin Session ID: c2912891-b459-4a03-b2af-154d5e264df1
Retrieval Hint: query_raw_memories(query="github-workflow projectRoot revert v12.1 cwd-primary consumer workspace install neo.mjs as dependency 11149 3-tier heuristic regression")
Context
Operator-surfaced 2026-05-10: the v12.1.0 release had github-workflow's projectRoot resolution working correctly for the consumer-workspace install pattern (Repo-A installs
neo.mjsas a dependency, configures gh-workflow server via.gemini/settings.jsonto sync from a DIFFERENT repo Repo-B, and the synced files land in Repo-A's root, not insideRepo-A/node_modules/neo.mjs/).Operator-quoted V-B-A anchor: "context of what worked in neo v12.1 => repo => install neo as a dependency. add e.g. .gemini/settings.json => configure the gh worklow server to a different repo. sync => into the repo root, not the neo node module. we had that working fine."
PR #11149 (closed-merged 2026-05-10, addressing #11147) replaced the v12.1 shape with a 3-tier heuristic. Then PR #11153 (closed-unmerged via Cycle-3 V-B-A retraction) attempted to extend with a hallucinated pnpm 4th tier. Today's drift cascade exposed that #11149's 3-tier heuristic itself diverges from the v12.1 working shape.
The Problem
ai/mcp/server/github-workflow/config.template.mjs(post-#11149) resolvesprojectRootvia:let projectRoot; if (process.env.NEO_WORKSPACE_ROOT) { projectRoot = process.env.NEO_WORKSPACE_ROOT; } else { const nodeModulesIndex = __dirname.indexOf(`${path.sep}node_modules${path.sep}neo.mjs`); if (nodeModulesIndex !== -1) { projectRoot = __dirname.slice(0, nodeModulesIndex) || path.sep; } else { projectRoot = path.resolve(__dirname, '../../../../'); } }Three problems with this shape, in increasing severity:
Aesthetic (operator-flagged):
__dirname.indexOf(\${path.sep}node_modules${path.sep}neo.mjs`)` is string-walking with separator interpolation. Operator-quoted: "this is UGLY. there is path.join() and other methods that resolve slashes cross OS nicer." The whole heuristic uses path-as-string semantics where path-as-structure semantics exist.Substrate-divergent (right-hemisphere precedent): Sibling MCP configs all derive
projectRoot(or equivalentneoRootDir) viapath.resolve(__dirname, '../../../../')directly with no heuristic chains:ai/mcp/server/memory-core/config.template.mjs:const neoRootDir = path.resolve(__dirname, '../../../../'); const cwd = neoRootDir;ai/mcp/server/knowledge-base/config.template.mjs:const neoRootDir = path.resolve(__dirname, '../../../../');ai/mcp/server/neural-link/config.template.mjs:const neoRootDir = path.resolve(__dirname, '../../../../'); const cwd = process.cwd();None walks
node_modules. None probes for env-var overrides.Functional regression (operator-validated): v12.1.0 had github-workflow working with the consumer-workspace pattern operator described. Verified via
git show 12.1.0:ai/mcp/server/github-workflow/config.mjs:const packageRoot = path.resolve(__dirname, '../../../../'); const projectRoot = process.cwd() === '/' ? packageRoot : process.cwd();This handles BOTH the
/cwd edge (the symptom #11147 described) AND the consumer-workspace case (projectRoot = consumer's cwd, NOTnode_modules/neo.mjs/). Theprocess.cwd() === '/'check is the explicit guard against the MCP-host-context bug #11147 reported.PR #11149's 3-tier heuristic broke the consumer-workspace expectation: for a consumer running
npm run ai:mcp-server-github-workflowfrom Repo-A, the__dirname.indexOf('node_modules/neo.mjs')check fires and slicesprojectRootback toRepo-A(correct) — but ONLY if Repo-A's neo install is at exactlynode_modules/neo.mjs/. If neo is installed via any non-default layout (workspaces, monorepo tools, etc.), the heuristic mis-resolves.The Architectural Reality
npm run ai:mcp-server-github-workflow, which meansprocess.cwd()is the invocation workspace root by npm's contract — already the right answer in 99% of cases.process.cwd() === '/'edge case happens when the MCP host (Claude Code / Antigravity / Codex Desktop) spawns the server with cwd not propagated. v12.1's fallback topath.resolve(__dirname, '../../../../')correctly anchors to the install location in that case.The shape v12.1.0 used is also conceptually equivalent to the LEFT-hemisphere build-script canonical (
buildScripts/build/all.mjs,buildScripts/build/themes.mjs) which solves the same "find workspace root in repo OR consumer-install" problem viaprocess.cwd()+ package.json discriminator. The right-hemisphere github-workflow special case (project-data is deployment-context-specific, unlike Neo-internal substrate in kb/mc/nl) makesprocess.cwd()the natural primary source — matching v12.1.The Fix
Revert
ai/mcp/server/github-workflow/config.template.mjsprojectRoot resolution to the exact v12.1.0 shape:const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const packageRoot = path.resolve(__dirname, '../../../../'); const projectRoot = process.cwd() === '/' ? packageRoot : process.cwd();Scope is config.template.mjs ONLY.
ai/services/github-workflow/toolService.mjs'sdefaultBranchDetector(introduced in #11146 + improved in #11149 to useconfig.projectRootinstead of bareprocess.cwd()) is the right shape and stays unchanged — onceprojectRootresolves correctly via cwd-primary, downstream consumers ofconfig.projectRoot(including the branch detector) inherit the correct value.Contract Ledger Matrix
config.template.mjsprojectRoot resolutionprocess.cwd() === '/' ? packageRoot : process.cwd()git show 12.1.0:ai/mcp/server/github-workflow/config.mjs+ sandbox-runtime vianpm run test-unitNEO_WORKSPACE_ROOT__dirname.indexOf('node_modules/neo.mjs')heuristicAcceptance Criteria
ai/mcp/server/github-workflow/config.template.mjsprojectRoot resolution restored to v12.1.0 exact shape (4 lines:packageRoot+projectRootderivations). NO env-var. NO__dirname.indexOf. NO multi-tier branching.ai/services/github-workflow/toolService.mjsdefaultBranchDetectorunchanged — still usesconfig.projectRoot(correct downstream consumption).test/playwright/unit/ai/services/github-workflow/toolService.spec.mjspasses unchanged. No test asserted the 3-tier heuristic, so no test updates needed.npm run ai:mcp-server-github-workflowfrom worktree root, confirmprojectRootresolves to worktree root (sandbox-runtime verify L2)./) by spawning the server withcwd: '/', confirmprojectRootfalls back topackageRoot(sandbox-runtime verify L2).Out of Scope
neoRootDir-pinning via__dirnameis correct for them. github-workflow is the outlier because its data is consumer-workspace-relative.defaultBranchDetector: out of scope. That's correct.agent/11152-pnpm-layout-and-docs: orthogonal cleanup. Operator decision per shared-checkout/worktree identity incident handling.Avoided Traps
projectRoot = path.resolve(__dirname, '../../../../')): rejected. This is the kb/mc/nl right-hemisphere shape and is correct for THOSE configs, but breaks github-workflow's consumer-workspace contract. In a consumer install wherenpm i neo.mjs, this resolves tonode_modules/neo.mjs/— meaning the consumer's.gemini/settings.json-targeted issue sync would land innode_modules/neo.mjs/resources/content/issues/, polluting the neo package. Operator-confirmed v12.1 worked with files landing in repo root, not the node module. cwd-primary is required.path.joinorpath.relativeto improve indexOf aesthetic without changing semantics: rejected. Operator's "indexOf is ugly" critique was a substrate-quality signal, not a request for a cosmetic patch. The right fix removes the entire heuristic chain by returning to v12.1's cwd-primary shape — no path-walking at all.NEO_WORKSPACE_ROOTenv-var for future flexibility: rejected. Truth-in-code memory note applies — no production consumer exists. Speculative-support-code is worse than absent-support; remove unverified branches.defaultBranchDetectorchange: rejected. That change is correct (uses config.projectRoot once projectRoot is right). Bundling would inflate scope. Single-concern PR.Related
/symptom. v12.1.0'sprocess.cwd() === '/' ? packageRoot : process.cwd()handles this correctly; #11149's heuristic was an over-engineering response.git show 12.1.0:ai/mcp/server/github-workflow/config.mjsai/mcp/server/{memory-core,knowledge-base,neural-link}/config.template.mjs— all usepath.resolve(__dirname, '../../../../')directly. github-workflow is the deliberate cwd-primary outlier due to consumer-workspace-relative data semantics.Origin Session ID: c2912891-b459-4a03-b2af-154d5e264df1
Retrieval Hint:
query_raw_memories(query="github-workflow projectRoot revert v12.1 cwd-primary consumer workspace install neo.mjs as dependency 11149 3-tier heuristic regression")