LearnNewsExamplesServices
Frontmatter
id11155
titleRestore github-workflow projectRoot to v12.1.0 cwd-primary shape (revert #11149)
stateClosed
labels
bugairegressionarchitecturemodel-experience
assigneesneo-opus-4-7
createdAtMay 10, 2026, 11:56 PM
updatedAtMay 11, 2026, 1:11 AM
githubUrlhttps://github.com/neomjs/neo/issues/11155
authorneo-opus-4-7
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtMay 11, 2026, 1:11 AM

Restore github-workflow projectRoot to v12.1.0 cwd-primary shape (revert #11149)

Closedbugairegressionarchitecturemodel-experience
neo-opus-4-7
neo-opus-4-7 commented on May 10, 2026, 11:56 PM

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:

  1. 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.

  2. 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.

  3. 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

  • (AC1) ai/mcp/server/github-workflow/config.template.mjs projectRoot resolution restored to v12.1.0 exact shape (4 lines: packageRoot + projectRoot derivations). NO env-var. NO __dirname.indexOf. NO multi-tier branching.
  • (AC2) ai/services/github-workflow/toolService.mjs defaultBranchDetector unchanged — still uses config.projectRoot (correct downstream consumption).
  • (AC3) test/playwright/unit/ai/services/github-workflow/toolService.spec.mjs passes unchanged. No test asserted the 3-tier heuristic, so no test updates needed.
  • (AC4) Empirical re-validation: invoke npm run ai:mcp-server-github-workflow from worktree root, confirm projectRoot resolves to worktree root (sandbox-runtime verify L2).
  • (AC5) Empirical re-validation: simulate the #11147 symptom (cwd=/) by spawning the server with cwd: '/', confirm projectRoot falls back to packageRoot (sandbox-runtime verify L2).
  • (AC6) PR body cites this ticket + v12.1.0 shape diff + operator-validated working-pattern anchor.

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")

tobiu referenced in commit b19f9c2 - "fix(github-workflow): revert 3-tier projectRoot heuristic to cwd-primary shape (#11155) (#11158) on May 11, 2026, 1:11 AM
tobiu closed this issue on May 11, 2026, 1:11 AM