Skip to content

fix(hooks): resolve gitnexus on PATH with a pure-Node scan, all-OS (#1938)#1980

Merged
magyargergo merged 1 commit into
mainfrom
fix/hook-path-scan-fallback-1938
Jun 3, 2026
Merged

fix(hooks): resolve gitnexus on PATH with a pure-Node scan, all-OS (#1938)#1980
magyargergo merged 1 commit into
mainfrom
fix/hook-path-scan-fallback-1938

Conversation

@magyargergo

@magyargergo magyargergo commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

Finishes #1938 with the approach its reporter actually preferred ("Option A"): the stale-index hint resolves a PATH-installed gitnexus with one spawn-free, pure-Node PATH scan, replacing the where/which subprocess.

#1945 already routes all three hooks through formatAnalyzeCommand and prefers a global gitnexus over the npm-11-crashing npx. But its resolveOnPath shelled out to where/which, which fails outright when the probe binary itself is unreachable — a sanitized hook PATH stripped of System32 (Windows) or /usr/bin (a minimal container) — leaving the hint to degrade toward the npx crash path even though gitnexus is installed. That is exactly the Windows scenario #1938 reports.

Change

resolveOnPath is now a single pure-Node scan: each PATH dir × the platform's executable extensions (PATHEXT on Windows; bare name + X_OK on POSIX). One code path, identical on every OS:

  • No dependency on where/which being reachable — the original bug class disappears.
  • No shell-spawn surface (CVE-2024-27980) and no spawn timeout to tune.
  • Windows matches PATHEXT extensions only — mirroring where/cmd.exe — so neither an un-spawnable .ps1-only shim (absent from default PATHEXT) nor a bare extensionless file (which a shell can't launch as gitnexus) is a false positive.
  • preferExecExt keeps the .cmd/.bat/.exe wrapper preference for the gitnexus lookup.

resolveOnPath is now pure (platform/env injectable) and exported; pickPathMatch is removed. Version detection still spawns npm/pnpm --version (unavoidable — that needs to run the tool) and is unchanged. Both resolve-analyze-cmd.cjs copies stay byte-identical (parity test enforced).

Why this is equivalent-or-better, not a downgrade

where/which only ever resolve PATH dirs × executable rules — which this models exactly (Windows PATHEXT; POSIX X_OK). It does not lose the Windows "App Paths" registry, because where doesn't consult that for bare-command resolution either. So the scan is a faithful model of "will gitnexus analyze actually run", with no subprocess to be missing, hang, or need a shell.

Tests

  • resolveOnPath unit cases (injected platform/env, no host-PATH dependency): POSIX exec found / non-exec skipped (X_OK, skipIf on Windows) / absent / empty PATH; Windows PATHEXT .cmd / .ps1-only-not-matched / extensionless ignored when a shim exists / extensionless-only → null / preferExecExt prefers .cmd over .COM but accepts a lone .COM.
  • A cross-platform formatAnalyzeCommand end-to-end case: a launcher alone on PATH resolves gitnexus analyze (works on every OS now — no where/which reachability caveat).
  • The existing hook e2e auto-detection tests + new pathWithoutGitNexus / envWithPath / createGitNexusPathEntry helpers (mirror the hook's own isFile() + X_OK detection).

Validation

  • npx tsc --noEmit clean; prettier --check clean; eslint 0 errors.
  • vitest run312 targeted tests pass (unit + both hook e2e suites + the resolver's other consumers hooks.test.ts / cursor-hook.test.ts).
  • Behavioral proof of the original bug: with which/where unreachable but a launcher installed, the old resolver emits npx gitnexus@latest analyze; the pure scan emits gitnexus analyze.

Impact / risk

resolveOnPath is not in the GitNexus index (newer than the snapshot), so blast radius assessed manually: callers are resolveInvocationMode (default probe) and formatAnalyzeCommand (memoized probe), consuming the result only for truthiness; plus the CLI's resolve-invocation.ts via the unchanged export surface. This does change the common resolution path (every probe now scans instead of spawning), so it's validated across the cross-platform CI matrix, not just locally. Risk: LOW — equivalent model, simpler, no new failure modes.

Supersedes #1958 (a hand-rolled which() that replaced the centralized resolver and reintroduced the npm-11 npx crash fallback). This keeps the centralized resolver and makes its PATH lookup spawn-free and all-OS.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Jun 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
gitnexus Ready Ready Preview, Comment Jun 2, 2026 10:27pm

Request Review

@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

✨ PR Autofix

Found fixable formatting / unused-import issues across 15 changed lines. Comment /autofix on this PR to apply them, or run npm run lint:fix && npm run format locally.

{"schema":"gitnexus.pr-autofix/v2","state":"fixes-available","pr_number":1980,"changed_lines":15,"head_sha":"5a8ae4582d48d1e1ab97abc3a622ccef87ff94a8","run_id":"26847288046","apply_command":"/autofix"}

@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

CI Report

All checks passed

Pipeline Status

Stage Status Details
✅ Typecheck success tsc --noEmit
✅ Tests success unit tests, 3 platforms
✅ E2E success gitnexus-web changes only

Test Results

Tests Passed Failed Skipped Duration
10938 10926 0 12 663s

✅ All 10926 tests passed

12 test(s) skipped — expand for details
  • COBOL pipeline benchmark > scales with file count
  • C# pipeline benchmark > scales with file count — namespaces spread across the solution
  • C# pipeline benchmark > scales with file count — all types in one (global) namespace bucket
  • C# pipeline benchmark > scales with file count — all types in one (named) namespace bucket
  • Go pipeline benchmark > scales with file count (workers enabled)
  • Go pipeline benchmark — worker pool (issue Worker idle timeout kills long Go scope extraction and surfaces as Napi::Error during analyze #1848) > does not quarantine the large generated Go file on sub-batch idle timeout
  • Go structural interface detection benchmark > scales linearly with interface × struct count
  • PHP pipeline benchmark > scales with file count (workers enabled)
  • Ruby pipeline benchmark > scales with file count (workers enabled)
  • Rust pipeline benchmark > scales with file count (workers enabled)
  • run.cjs direct-exec entrypoint (fix(cli): steer docs, skills, and hooks through a CLI-neutral project-local runner (#1939) #1945) > resolves a .cmd shim via the Windows shell branch, passing args and exit code
  • buildTypeEnv > known limitations (documented skip tests) > Ruby block parameter: users.each { |user| } — closure param inference, different feature

Code Coverage

Tests

Metric Coverage Covered Base Delta Status
Statements 80.29% 38216/47593 79.84% 📈 +0.5 🟢 ████████████████░░░░
Branches 68.85% 24297/35288 68.5% 📈 +0.3 🟢 █████████████░░░░░░░
Functions 85.45% 3976/4653 84.94% 📈 +0.5 🟢 █████████████████░░░
Lines 83.9% 34375/40969 83.36% 📈 +0.5 🟢 ████████████████░░░░

📋 View full run · Generated by CI

@magyargergo magyargergo force-pushed the fix/hook-path-scan-fallback-1938 branch from 0339f9d to 096e145 Compare June 2, 2026 21:46
@magyargergo magyargergo changed the title fix(hooks): spawn-free PATH-scan fallback for the stale-index resolver (#1938) fix(hooks): resolve gitnexus on PATH with a pure-Node scan, all-OS (#1938) Jun 2, 2026
@magyargergo magyargergo force-pushed the fix/hook-path-scan-fallback-1938 branch from 096e145 to 0cde75b Compare June 2, 2026 22:00
…1938)

The stale-index hint must prefer a PATH-installed `gitnexus` over `npx gitnexus`
(which crashes npm 11's arborist, #1939). #1945 already does this via
`formatAnalyzeCommand`, but its `resolveOnPath` shelled out to `where`/`which` —
which fails outright when the probe binary is itself unreachable (a sanitized
hook PATH without System32 / `/usr/bin`), exactly the Windows case #1938 reports.

Replace the subprocess (and a hand-rolled fallback) with a single spawn-free
PATH scan — issue #1938's preferred "Option A". One code path, identical on every
OS:
- no dependency on `where`/`which` being reachable;
- no shell-spawn surface (CVE-2024-27980) and no spawn timeout to tune;
- Windows matches PATHEXT extensions only (mirrors `where`/cmd.exe), so neither a
  `.ps1`-only shim nor a bare extensionless file is a false positive;
- `preferExecExt` keeps the `.cmd`/`.bat`/`.exe` wrapper preference.

`resolveOnPath` is now pure (platform/env injectable) and exported; `pickPathMatch`
and the interim `scanPathForCommand` are removed. Version detection still spawns
`npm`/`pnpm --version` (unavoidable) and is unchanged.

Tests: drive `resolveOnPath` directly across POSIX/Windows cases (PATHEXT, `.ps1`,
extensionless, preferExecExt, X_OK) + a cross-platform `formatAnalyzeCommand`
end-to-end case, plus the existing hook e2e auto-detection tests and new
`pathWithoutGitNexus`/`envWithPath`/`createGitNexusPathEntry` helpers. Both
`resolve-analyze-cmd.cjs` copies kept byte-identical.

Validated: tsc clean, prettier clean, eslint 0 errors, 312 targeted tests pass
(unit + hook e2e + the resolver's other consumers).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@magyargergo magyargergo left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Reviewed 7 lanes across 2 engines. GitNexus lanes (Claude): risk-architect, test-ci-verifier. Compound-Engineering lanes (Claude): correctness, adversarial, maintainability, testing. Independent engine: Codex — gpt-5.5, reasoning effort xhigh — the only non-Claude reviewer. Six lanes share Claude's priors, so the strong signal is Codex + a Claude lane agreeing. Supplemental claude-mem context was requested but its index was degraded, so prior-session grounding came from local memory.

Disclosure: the PR author and this review's coordinator are the same. Independence rests on the agent lanes + Codex and on CI, not on the coordinator.

The review materially reshaped this PR, which is the point of running it:

  1. It started as a pure-Node scanPathForCommand fallback behind the existing where/which subprocess. Review (and a maintainer ask to make it elegant + all-OS) drove a redesign into issue #1938's "Option A": resolveOnPath is now a single spawn-free PATH scan, where/which + pickPathMatch removed.
  2. Codex caught that a squash commit had not actually included the refactor (it read the pushed blob, not the working tree) — fixed and re-verified on origin.
  3. Codex also flagged a PATH-delimiter seam; an attempt to derive the delimiter from the injected platform then failed the real windows-latest job (it split a C:\… path at the drive colon under a POSIX-injected test). Reverted to host path.delimiter (production-identical), which windows-latest now confirms green.

Final design (what's merging)

resolveOnPath(command, preferExecExt, { platform, env }) scans each PATH dir × the platform's executable extensions (PATHEXT on Windows; bare name + X_OK on POSIX). One code path, identical on every OS, no subprocess — so the original #1938 failure mode (the probe binary unreachable on a sanitized PATH) is gone, along with the shell-spawn surface and the .ps1/extensionless false positives (Windows matches PATHEXT only; preferExecExt keeps the .cmd/.bat/.exe preference). Version detection still spawns npm/pnpm --version (unavoidable) and is unchanged. Both resolve-analyze-cmd.cjs copies stay byte-identical.

Validated (credit)

  • Common path correct: the scan models exactly what where/which resolved (PATH × PATHEXT/X_OK); analyze CLI path still works on Windows. (correctness + adversarial + Codex)
  • resolveOnPath logic verified by simulation and unit tests: PATHEXT parse/normalize, precedence, dir-skip, symlink-follow, X_OK, .ps1-not-matched, extensionless-not-matched, preferExecExt.
  • Export surface: pickPathMatch/scanPathForCommand removed, resolveOnPath exported; the CLI's resolve-invocation.ts createRequire drift-guard surface (resolveInvocationMode/formatDocumentationDlxCommand/NPX_REF) intact. Byte-identical parity holds; setup.ts propagates to the antigravity install.
  • Tests leak-safe (try/finally + afterEach); --embeddings variant non-redundant; the Run \gitnexus analyze`` assertion is tight.

Lower-priority follow-ups (not blocking · Claude-only, weaker signal)

  • P3 runHook's test-helper env contract is replace-not-merge; a future partial-env caller would lose PATH (no current caller affected — worth a one-line JSDoc note).
  • P3 the scan has no per-iteration cap (acceptable: ~37 ms / 10k local-fs misses; only relevant on network-mounted PATH dirs).

CI / merge state

All checks green on 547172de: 30 success, 1 skipped, mergeable: CLEAN. Includes tests / windows-latest (platform-sensitive) and macos-latest (which run the changed resolve-invocation.test.ts + hooks-e2e.test.ts), ubuntu / coverage, scope-parity, and quality (typecheck/format/lint). One transient cli-e2e.test.ts Windows flake (eval-server/EPIPE/stdout-fd, #324 — unrelated to this diff) cleared on re-run with no code change. Local: tsc clean, prettier clean, eslint 0 errors, 312 targeted tests pass.

Verdict

Production-ready. No blocking issues; the design simplification and both review findings are resolved and CI-confirmed across the OS matrix. Two optional P3 follow-ups noted above.


Automated multi-tool digest (pr-tri-review): 6 Claude lanes + Codex gpt-5.5/xhigh. The coordinator authored the PR — verify before acting.

@magyargergo magyargergo merged commit f01d913 into main Jun 3, 2026
50 of 52 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant