Repository navigation
fix(startup): stop the PATH seed pinning nvm to its newest install - #16314
Conversation
patchPackagedProcessPath prepends the newest nvm version dir to process.env.PATH, then hydrateShellPath probes the login shell with that same env. nvm's startup `use` honors whatever node is already on PATH instead of the user's `default` alias, so the probe returns a PATH pinned to the newest install and every terminal pane inherits it. A user whose newest nvm node is a bare install then loses every global CLI (codex, claude, gemini, vercel...) inside Orca while they still resolve in Ghostty/Terminal, which start from the bare GUI PATH and fall through to `default`. Probe with the PATH the process launched with. Windows already gets this through WindowsShellPathOwnership.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (7)
Included review availability: Your plan provides up to 10 included reviews per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe startup module now captures the platform-specific launch PATH at load time and creates isolated probe environments with a marker and restored PATH. Merge Risk: ⚪ Minimal · up to The change makes shell PATH probing use the launch-time PATH so Node-based CLI resolution follows the user’s configured version instead of a newly installed version. The change is localized and tested; no actionable merge-blocking risk remains after normal checks and review. 🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (3 warnings)
✅ Passed checks (2 passed)
Full details: Description checkExplanation The description provides detailed problem, fix, rationale, linked issue, platform, and testing information, but it omits several required template sections, including Visual Proof/N/A, Review, Notes, Agent skill upstream boundary, and Checklist. Full details: Linked Issues checkExplanation The implementation insulates the shell probe from seeded PATH values, but it does not satisfy issue ✨ Finishing Touches 💡 1📝 Generate docstrings 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 7bdca57c-aa16-49ad-a958-0505cf3194ac
📒 Files selected for processing (4)
src/main/startup/configure-process.test.tssrc/main/startup/configure-process.tssrc/main/startup/hydrate-shell-path.test.tssrc/main/startup/hydrate-shell-path.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.
shellProbeEnv short-circuits on win32, so the POSIX-only assertion failed for anyone running the suite on Windows. Matches the convention in hydrate-shell-path.windows.test.ts. Also narrows the win32 exemption comment: WindowsShellPathOwnership snapshots its baseline after the seeds land, so it does not unwind them. The exemption holds because Windows keys PATH as `Path` and no Windows seed pins a node version.
The previous commit exempted win32 on the stated grounds that no Windows seed pins a node version. That is wrong: getVersionManagerDirectories calls getNvmVersionDirectories on every platform, so a Git Bash user whose nvm uses the POSIX ~/.nvm/versions/node layout gets the newest version dir seeded on Windows too, and the -ilc Git Bash probe inherits it. Record the PATH key alongside the value and overwrite that entry in place, so Windows never carries both `Path` and `PATH` — which was the only real reason to skip win32.
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 9a148b2f-9d73-41c2-a729-9422ab6512ec
📒 Files selected for processing (4)
src/main/startup/configure-process.test.tssrc/main/startup/configure-process.tssrc/main/startup/hydrate-shell-path.test.tssrc/main/startup/hydrate-shell-path.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 2 remain after this review.
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Preserve the first recorded launch PATH.
recordLaunchPath overwrites launchPath on every call. If patchPackagedProcessPath() runs twice, the second call records the seeded PATH and can reintroduce nvm version pinning. Record only when launchPath === null, and add a two-call regression test.
#!/bin/bash
set -euo pipefail
rg -n -P '\bpatchPackagedProcessPath\s*\(' --glob '*.ts' --glob '*.tsx' .…ures Three loose ends from the review, folded in rather than deferred. The probe's clean PATH was handed over by an explicit recordLaunchPath call from the seeding site, so the invariant lived across three files and a refactor that moved the seed call would silently re-pin nvm. Snapshot PATH during module init instead: that runs while the import graph is evaluated, strictly before any statement in main's body, so it cannot observe the seeds and there is no call ordering left to break. All seven importers are static, so no lazy import can defeat it. A test asserts the probe ignores later process.env mutation, and fails if the live read is reintroduced. A failed startup probe leaves the seeded newest-nvm dir in front and said nothing, so the population whose rc files blow the 5s budget hit the original symptom with no diagnosable trace. Log the failureReason. The probe is an interactive login shell, so rc files that exec into a multiplexer or start a heavy prompt can outrun that budget with no way to opt out. Set ORCA_SHELL_PATH_PROBE=1 so they can take a fast path.
Caught by running the suite on a real Windows machine, not a mocked platform. The spread of process.env is a plain, case-sensitive object, while Windows resolves env names case-insensitively. Writing the captured `Path` back onto it left the seeded value still live under `PATH`, so the probe shell could read either one — the exact duplicate-key hazard the win32 branch was supposed to prevent. Drop any other-cased variant of the key before writing.
* fix(cli): seed nvm's default version, not the newest install #16314 stopped the login-shell probe inheriting the seeded PATH, but left the seed itself picking the newest installed nvm version. That ordering decides which node a CLI runs under whenever the probe does not land — a timeout, or a login shell whose rc never initializes nvm — and newest is precisely the wrong guess: it is usually the version the user just added and has installed nothing into. That is the root cause reported in #10932. Resolve `alias/default` instead, mirroring nvm: follow the alias chain (`default` -> `lts/*` -> `lts/krypton` -> a version), resolve a partial version like `24` to the highest matching install, and treat `system`/`node`/`stable` as no preference. The chain is bounded and cycle-guarded because nvm's own resolver tracks seen aliases and hand-edited files can point at each other. Ordering is a preference, not a restriction: the remaining versions stay behind the default, so a CLI installed outside it is still reachable. Measured on a real machine with nvm default=24 and a bare v26.7.0 installed: the old resolver seeds v26.7.0/bin (no CLIs), the new one seeds v24.18.0/bin (every CLI). Tests were written first and verified to fail on the three bug cases against main before the fix existed. Also raise the probe budget from 5s to 10s. The old value was never measured against a real profile: a bash -ilc loading nvm, rvm, conda and gcloud takes ~1s idle but 6-7s on a loaded machine, so a cold start under load silently fell back to the seed. Startup does not block on the probe, and the one awaited consumer is agent detection, which is better served by a probe that finishes late than one that gives up early. * fix(cli): reject non-version alias tokens instead of matching v0.x Review finding, and a real bug I introduced. parseVersionSegment coerces every unparseable segment to 0, so an unresolvable default alias — `garbage`, `iojs`, `lts/nonexistent`, any hand-named alias — became [0] and prefix-matched a `v0.12.x` install, or any stray non-version directory. Orca would then seed a decade-old node as the preferred runtime. Real nvm answers N/A for all of them. The `wanted.length === 0` bail could never have caught this: ''.split('.') is [''], never empty. Replaced with a shape check that still admits legitimate numeric prefixes — verified against nvm itself, which resolves `24` to v24.18.0 and `0` to an installed v0.x while answering N/A for the rest. Also corrects two comments that no longer described the code: the seed is no longer "newest install", and the probe budget note claimed startup never blocks on hydration, which is false on packaged Windows where it gates terminal services and git. The traversal-guard comment claimed a containment join() already normalizes away; the real guarantee is that matchNvmVersion can only return an entry of the versions directory. * fix(cli): match nvm's version-token grammar, not just its first character Round-2 review finding, and the same bug one layer down. The previous guard anchored only the first character, but parseInt stops at the first non-digit, so `0x18`, `00` and `0abc` still parsed to [0] and prefix-matched a v0.12.x install — the decade-old-node seed the earlier fix was supposed to close. Reachable: `nvm alias default 0x18` warns that the version does not exist and writes the alias anyway, then resolves it to N/A. Use nvm's actual grammar, leading zeros included — nvm calls `00` and `024` N/A while parseInt reads them as 0 and 24. Verified by executing 17 tokens against a five-version fixture: every one now agrees with nvm, including the legitimate prefixes `0`, `0.12`, `24` and `v24.18.0`. Also drops a dead disjunct (the hop bound already caps the loop, so seen.size can never exceed it) and corrects the log comment in index.ts, which still told the reader a failed probe leaves the newest install in front. It leaves the default version in front now, which is usually survivable but still not what the shell would have resolved. * test(cli): skip the lts/* chain fixture on Windows Round-3 review finding. makeNvmHome materializes each alias as a real file, and the chain case uses nvm's actual `lts/*` alias — `*` is a reserved Win32 filename character, so writeFileSync fails with EINVAL. PR CI runs a Windows allowlist that excludes this file, so the breakage only reaches a Windows developer running the suite locally. Skipped rather than renamed: `lts/*` is the alias nvm really ships, and the assertion pins platform: 'darwin' anyway, so the real name costs no coverage. Matches the skipIf convention already used across src/shared. Also reflows a comment line that a previous edit ran to 143 characters; oxfmt does not reflow comments, so nothing would have caught it.
…tablyai#16314) * fix(startup): stop the PATH seed pinning nvm to its newest install patchPackagedProcessPath prepends the newest nvm version dir to process.env.PATH, then hydrateShellPath probes the login shell with that same env. nvm's startup `use` honors whatever node is already on PATH instead of the user's `default` alias, so the probe returns a PATH pinned to the newest install and every terminal pane inherits it. A user whose newest nvm node is a bare install then loses every global CLI (codex, claude, gemini, vercel...) inside Orca while they still resolve in Ghostty/Terminal, which start from the bare GUI PATH and fall through to `default`. Probe with the PATH the process launched with. Windows already gets this through WindowsShellPathOwnership. * test(startup): pin the platform in the probe-env test shellProbeEnv short-circuits on win32, so the POSIX-only assertion failed for anyone running the suite on Windows. Matches the convention in hydrate-shell-path.windows.test.ts. Also narrows the win32 exemption comment: WindowsShellPathOwnership snapshots its baseline after the seeds land, so it does not unwind them. The exemption holds because Windows keys PATH as `Path` and no Windows seed pins a node version. * fix(startup): give win32 the same probe insulation, keyed by Path The previous commit exempted win32 on the stated grounds that no Windows seed pins a node version. That is wrong: getVersionManagerDirectories calls getNvmVersionDirectories on every platform, so a Git Bash user whose nvm uses the POSIX ~/.nvm/versions/node layout gets the newest version dir seeded on Windows too, and the -ilc Git Bash probe inherits it. Record the PATH key alongside the value and overwrite that entry in place, so Windows never carries both `Path` and `PATH` — which was the only real reason to skip win32. * fix(startup): snapshot the launch PATH at module load, log probe failures Three loose ends from the review, folded in rather than deferred. The probe's clean PATH was handed over by an explicit recordLaunchPath call from the seeding site, so the invariant lived across three files and a refactor that moved the seed call would silently re-pin nvm. Snapshot PATH during module init instead: that runs while the import graph is evaluated, strictly before any statement in main's body, so it cannot observe the seeds and there is no call ordering left to break. All seven importers are static, so no lazy import can defeat it. A test asserts the probe ignores later process.env mutation, and fails if the live read is reintroduced. A failed startup probe leaves the seeded newest-nvm dir in front and said nothing, so the population whose rc files blow the 5s budget hit the original symptom with no diagnosable trace. Log the failureReason. The probe is an interactive login shell, so rc files that exec into a multiplexer or start a heavy prompt can outrun that budget with no way to opt out. Set ORCA_SHELL_PATH_PROBE=1 so they can take a fast path. * fix(startup): drop the other-cased PATH key from the Windows probe env Caught by running the suite on a real Windows machine, not a mocked platform. The spread of process.env is a plain, case-sensitive object, while Windows resolves env names case-insensitively. Writing the captured `Path` back onto it left the seeded value still live under `PATH`, so the probe shell could read either one — the exact duplicate-key hazard the win32 branch was supposed to prevent. Drop any other-cased variant of the key before writing. * fix(startup): preserve launch PATH across app restarts (cherry picked from commit 5c116b6)
…tablyai#16314) * fix(startup): stop the PATH seed pinning nvm to its newest install patchPackagedProcessPath prepends the newest nvm version dir to process.env.PATH, then hydrateShellPath probes the login shell with that same env. nvm's startup `use` honors whatever node is already on PATH instead of the user's `default` alias, so the probe returns a PATH pinned to the newest install and every terminal pane inherits it. A user whose newest nvm node is a bare install then loses every global CLI (codex, claude, gemini, vercel...) inside Orca while they still resolve in Ghostty/Terminal, which start from the bare GUI PATH and fall through to `default`. Probe with the PATH the process launched with. Windows already gets this through WindowsShellPathOwnership. * test(startup): pin the platform in the probe-env test shellProbeEnv short-circuits on win32, so the POSIX-only assertion failed for anyone running the suite on Windows. Matches the convention in hydrate-shell-path.windows.test.ts. Also narrows the win32 exemption comment: WindowsShellPathOwnership snapshots its baseline after the seeds land, so it does not unwind them. The exemption holds because Windows keys PATH as `Path` and no Windows seed pins a node version. * fix(startup): give win32 the same probe insulation, keyed by Path The previous commit exempted win32 on the stated grounds that no Windows seed pins a node version. That is wrong: getVersionManagerDirectories calls getNvmVersionDirectories on every platform, so a Git Bash user whose nvm uses the POSIX ~/.nvm/versions/node layout gets the newest version dir seeded on Windows too, and the -ilc Git Bash probe inherits it. Record the PATH key alongside the value and overwrite that entry in place, so Windows never carries both `Path` and `PATH` — which was the only real reason to skip win32. * fix(startup): snapshot the launch PATH at module load, log probe failures Three loose ends from the review, folded in rather than deferred. The probe's clean PATH was handed over by an explicit recordLaunchPath call from the seeding site, so the invariant lived across three files and a refactor that moved the seed call would silently re-pin nvm. Snapshot PATH during module init instead: that runs while the import graph is evaluated, strictly before any statement in main's body, so it cannot observe the seeds and there is no call ordering left to break. All seven importers are static, so no lazy import can defeat it. A test asserts the probe ignores later process.env mutation, and fails if the live read is reintroduced. A failed startup probe leaves the seeded newest-nvm dir in front and said nothing, so the population whose rc files blow the 5s budget hit the original symptom with no diagnosable trace. Log the failureReason. The probe is an interactive login shell, so rc files that exec into a multiplexer or start a heavy prompt can outrun that budget with no way to opt out. Set ORCA_SHELL_PATH_PROBE=1 so they can take a fast path. * fix(startup): drop the other-cased PATH key from the Windows probe env Caught by running the suite on a real Windows machine, not a mocked platform. The spread of process.env is a plain, case-sensitive object, while Windows resolves env names case-insensitively. Writing the captured `Path` back onto it left the seeded value still live under `PATH`, so the probe shell could read either one — the exact duplicate-key hazard the win32 branch was supposed to prevent. Drop any other-cased variant of the key before writing. * fix(startup): preserve launch PATH across app restarts
…i#16420) * fix(cli): seed nvm's default version, not the newest install stablyai#16314 stopped the login-shell probe inheriting the seeded PATH, but left the seed itself picking the newest installed nvm version. That ordering decides which node a CLI runs under whenever the probe does not land — a timeout, or a login shell whose rc never initializes nvm — and newest is precisely the wrong guess: it is usually the version the user just added and has installed nothing into. That is the root cause reported in stablyai#10932. Resolve `alias/default` instead, mirroring nvm: follow the alias chain (`default` -> `lts/*` -> `lts/krypton` -> a version), resolve a partial version like `24` to the highest matching install, and treat `system`/`node`/`stable` as no preference. The chain is bounded and cycle-guarded because nvm's own resolver tracks seen aliases and hand-edited files can point at each other. Ordering is a preference, not a restriction: the remaining versions stay behind the default, so a CLI installed outside it is still reachable. Measured on a real machine with nvm default=24 and a bare v26.7.0 installed: the old resolver seeds v26.7.0/bin (no CLIs), the new one seeds v24.18.0/bin (every CLI). Tests were written first and verified to fail on the three bug cases against main before the fix existed. Also raise the probe budget from 5s to 10s. The old value was never measured against a real profile: a bash -ilc loading nvm, rvm, conda and gcloud takes ~1s idle but 6-7s on a loaded machine, so a cold start under load silently fell back to the seed. Startup does not block on the probe, and the one awaited consumer is agent detection, which is better served by a probe that finishes late than one that gives up early. * fix(cli): reject non-version alias tokens instead of matching v0.x Review finding, and a real bug I introduced. parseVersionSegment coerces every unparseable segment to 0, so an unresolvable default alias — `garbage`, `iojs`, `lts/nonexistent`, any hand-named alias — became [0] and prefix-matched a `v0.12.x` install, or any stray non-version directory. Orca would then seed a decade-old node as the preferred runtime. Real nvm answers N/A for all of them. The `wanted.length === 0` bail could never have caught this: ''.split('.') is [''], never empty. Replaced with a shape check that still admits legitimate numeric prefixes — verified against nvm itself, which resolves `24` to v24.18.0 and `0` to an installed v0.x while answering N/A for the rest. Also corrects two comments that no longer described the code: the seed is no longer "newest install", and the probe budget note claimed startup never blocks on hydration, which is false on packaged Windows where it gates terminal services and git. The traversal-guard comment claimed a containment join() already normalizes away; the real guarantee is that matchNvmVersion can only return an entry of the versions directory. * fix(cli): match nvm's version-token grammar, not just its first character Round-2 review finding, and the same bug one layer down. The previous guard anchored only the first character, but parseInt stops at the first non-digit, so `0x18`, `00` and `0abc` still parsed to [0] and prefix-matched a v0.12.x install — the decade-old-node seed the earlier fix was supposed to close. Reachable: `nvm alias default 0x18` warns that the version does not exist and writes the alias anyway, then resolves it to N/A. Use nvm's actual grammar, leading zeros included — nvm calls `00` and `024` N/A while parseInt reads them as 0 and 24. Verified by executing 17 tokens against a five-version fixture: every one now agrees with nvm, including the legitimate prefixes `0`, `0.12`, `24` and `v24.18.0`. Also drops a dead disjunct (the hop bound already caps the loop, so seen.size can never exceed it) and corrects the log comment in index.ts, which still told the reader a failed probe leaves the newest install in front. It leaves the default version in front now, which is usually survivable but still not what the shell would have resolved. * test(cli): skip the lts/* chain fixture on Windows Round-3 review finding. makeNvmHome materializes each alias as a real file, and the chain case uses nvm's actual `lts/*` alias — `*` is a reserved Win32 filename character, so writeFileSync fails with EINVAL. PR CI runs a Windows allowlist that excludes this file, so the breakage only reaches a Windows developer running the suite locally. Skipped rather than renamed: `lts/*` is the alias nvm really ships, and the assertion pins platform: 'darwin' anyway, so the real name costs no coverage. Matches the skipIf convention already used across src/shared. Also reflows a comment line that a previous edit ran to 143 characters; oxfmt does not reflow comments, so nothing would have caught it.
…tablyai#16314) * fix(startup): stop the PATH seed pinning nvm to its newest install patchPackagedProcessPath prepends the newest nvm version dir to process.env.PATH, then hydrateShellPath probes the login shell with that same env. nvm's startup `use` honors whatever node is already on PATH instead of the user's `default` alias, so the probe returns a PATH pinned to the newest install and every terminal pane inherits it. A user whose newest nvm node is a bare install then loses every global CLI (codex, claude, gemini, vercel...) inside Orca while they still resolve in Ghostty/Terminal, which start from the bare GUI PATH and fall through to `default`. Probe with the PATH the process launched with. Windows already gets this through WindowsShellPathOwnership. * test(startup): pin the platform in the probe-env test shellProbeEnv short-circuits on win32, so the POSIX-only assertion failed for anyone running the suite on Windows. Matches the convention in hydrate-shell-path.windows.test.ts. Also narrows the win32 exemption comment: WindowsShellPathOwnership snapshots its baseline after the seeds land, so it does not unwind them. The exemption holds because Windows keys PATH as `Path` and no Windows seed pins a node version. * fix(startup): give win32 the same probe insulation, keyed by Path The previous commit exempted win32 on the stated grounds that no Windows seed pins a node version. That is wrong: getVersionManagerDirectories calls getNvmVersionDirectories on every platform, so a Git Bash user whose nvm uses the POSIX ~/.nvm/versions/node layout gets the newest version dir seeded on Windows too, and the -ilc Git Bash probe inherits it. Record the PATH key alongside the value and overwrite that entry in place, so Windows never carries both `Path` and `PATH` — which was the only real reason to skip win32. * fix(startup): snapshot the launch PATH at module load, log probe failures Three loose ends from the review, folded in rather than deferred. The probe's clean PATH was handed over by an explicit recordLaunchPath call from the seeding site, so the invariant lived across three files and a refactor that moved the seed call would silently re-pin nvm. Snapshot PATH during module init instead: that runs while the import graph is evaluated, strictly before any statement in main's body, so it cannot observe the seeds and there is no call ordering left to break. All seven importers are static, so no lazy import can defeat it. A test asserts the probe ignores later process.env mutation, and fails if the live read is reintroduced. A failed startup probe leaves the seeded newest-nvm dir in front and said nothing, so the population whose rc files blow the 5s budget hit the original symptom with no diagnosable trace. Log the failureReason. The probe is an interactive login shell, so rc files that exec into a multiplexer or start a heavy prompt can outrun that budget with no way to opt out. Set ORCA_SHELL_PATH_PROBE=1 so they can take a fast path. * fix(startup): drop the other-cased PATH key from the Windows probe env Caught by running the suite on a real Windows machine, not a mocked platform. The spread of process.env is a plain, case-sensitive object, while Windows resolves env names case-insensitively. Writing the captured `Path` back onto it left the seeded value still live under `PATH`, so the probe shell could read either one — the exact duplicate-key hazard the win32 branch was supposed to prevent. Drop any other-cased variant of the key before writing. * fix(startup): preserve launch PATH across app restarts
…i#16420) * fix(cli): seed nvm's default version, not the newest install stablyai#16314 stopped the login-shell probe inheriting the seeded PATH, but left the seed itself picking the newest installed nvm version. That ordering decides which node a CLI runs under whenever the probe does not land — a timeout, or a login shell whose rc never initializes nvm — and newest is precisely the wrong guess: it is usually the version the user just added and has installed nothing into. That is the root cause reported in stablyai#10932. Resolve `alias/default` instead, mirroring nvm: follow the alias chain (`default` -> `lts/*` -> `lts/krypton` -> a version), resolve a partial version like `24` to the highest matching install, and treat `system`/`node`/`stable` as no preference. The chain is bounded and cycle-guarded because nvm's own resolver tracks seen aliases and hand-edited files can point at each other. Ordering is a preference, not a restriction: the remaining versions stay behind the default, so a CLI installed outside it is still reachable. Measured on a real machine with nvm default=24 and a bare v26.7.0 installed: the old resolver seeds v26.7.0/bin (no CLIs), the new one seeds v24.18.0/bin (every CLI). Tests were written first and verified to fail on the three bug cases against main before the fix existed. Also raise the probe budget from 5s to 10s. The old value was never measured against a real profile: a bash -ilc loading nvm, rvm, conda and gcloud takes ~1s idle but 6-7s on a loaded machine, so a cold start under load silently fell back to the seed. Startup does not block on the probe, and the one awaited consumer is agent detection, which is better served by a probe that finishes late than one that gives up early. * fix(cli): reject non-version alias tokens instead of matching v0.x Review finding, and a real bug I introduced. parseVersionSegment coerces every unparseable segment to 0, so an unresolvable default alias — `garbage`, `iojs`, `lts/nonexistent`, any hand-named alias — became [0] and prefix-matched a `v0.12.x` install, or any stray non-version directory. Orca would then seed a decade-old node as the preferred runtime. Real nvm answers N/A for all of them. The `wanted.length === 0` bail could never have caught this: ''.split('.') is [''], never empty. Replaced with a shape check that still admits legitimate numeric prefixes — verified against nvm itself, which resolves `24` to v24.18.0 and `0` to an installed v0.x while answering N/A for the rest. Also corrects two comments that no longer described the code: the seed is no longer "newest install", and the probe budget note claimed startup never blocks on hydration, which is false on packaged Windows where it gates terminal services and git. The traversal-guard comment claimed a containment join() already normalizes away; the real guarantee is that matchNvmVersion can only return an entry of the versions directory. * fix(cli): match nvm's version-token grammar, not just its first character Round-2 review finding, and the same bug one layer down. The previous guard anchored only the first character, but parseInt stops at the first non-digit, so `0x18`, `00` and `0abc` still parsed to [0] and prefix-matched a v0.12.x install — the decade-old-node seed the earlier fix was supposed to close. Reachable: `nvm alias default 0x18` warns that the version does not exist and writes the alias anyway, then resolves it to N/A. Use nvm's actual grammar, leading zeros included — nvm calls `00` and `024` N/A while parseInt reads them as 0 and 24. Verified by executing 17 tokens against a five-version fixture: every one now agrees with nvm, including the legitimate prefixes `0`, `0.12`, `24` and `v24.18.0`. Also drops a dead disjunct (the hop bound already caps the loop, so seen.size can never exceed it) and corrects the log comment in index.ts, which still told the reader a failed probe leaves the newest install in front. It leaves the default version in front now, which is usually survivable but still not what the shell would have resolved. * test(cli): skip the lts/* chain fixture on Windows Round-3 review finding. makeNvmHome materializes each alias as a real file, and the chain case uses nvm's actual `lts/*` alias — `*` is a reserved Win32 filename character, so writeFileSync fails with EINVAL. PR CI runs a Windows allowlist that excludes this file, so the breakage only reaches a Windows developer running the suite locally. Skipped rather than renamed: `lts/*` is the alias nvm really ships, and the assertion pins platform: 'darwin' anyway, so the real name costs no coverage. Matches the skipIf convention already used across src/shared. Also reflows a comment line that a previous edit ran to 143 characters; oxfmt does not reflow comments, so nothing would have caught it.

ELI5
nvm keeps several Node versions on your machine and lets you mark one as
default. All your globally-installed CLIs (codex,claude,gemini, ...) live inside whichever version you had active when you installed them — usually the default one.At startup, Orca tries to be helpful: "GUI apps launch with a nearly-empty PATH, so let me add the places Node tools usually live." For nvm it adds the newest version folder it can find. But newest ≠ your default. If you ever ran
nvm install <something newer>and never installed your tools there, that folder is basically empty.Then Orca asks your shell "what's your real PATH?" — and asks it while holding the note it just wrote itself. nvm reads that note, sees a Node version already selected, and politely says "great, I'll keep using that one" instead of picking your default. Orca believes the answer and hands it to every terminal pane.
So Orca's terminals end up pointed at an empty Node folder, and your CLIs vanish — while Ghostty, which never saw the note, asks the same question and gets the right answer.
The fix: stop handing the shell our own note before asking it the question. Windows already worked this way; this makes macOS/Linux match.
Problem
Typing
codexin an Orca terminal fails withcommand not found, while the exact same command works in Ghostty/Terminal on the same machine. Same forclaude,gemini,vercel,agent-slack— any globally-installed Node CLI.Two things combine:
patchPackagedProcessPath()(configure-process.ts:138) prependsgetVersionManagerBinPaths()toprocess.env.PATH. For nvm,getNvmVersionDirectoriessorts version dirs descending, so this seeds the newest installed node — not the user'sdefaultalias.index.ts:696→:699),hydrateShellPath()probes the login shell withenv: process.env— which now carries that seed.nvm's startup
usehonors whatever node is already onPATHover thedefaultalias:So the probe returns a PATH pinned to the newest install,
mergePathSegmentspromotes it to the front of Orca's PATH, and every terminal pane inherits it. Ghostty starts from the bare GUI PATH, hits thenonebranch, and resolvesdefault— which is why it works.Reproduced on my machine:
nvm install 26left a barev26.7.0(onlynode/npm/npx) whiledefault→v24.18.0held every global CLI. Every pane spawned after the next app launch gotv26.7.0:The repo's own
engines.node: 24started failing inside Orca panes too (WARN Unsupported engine: wanted 24, current v26.7.0).This is self-reinforcing: it re-pins on every launch and on every
hydrateShellPath({ force: true })refresh, since those also inherit the merged PATH.Fix
Probe the login shell with the PATH the process was launched with, recorded by
patchPackagedProcessPath()before it seeds. The seeds still land onprocess.env.PATHfor Orca's own spawns — only the probe is insulated, so it reports what the user's shell actually resolves.Windows already has the equivalent protection via
WindowsShellPathOwnership.restore(); this closes the same gap on POSIX. It also lines up with the rule already documented inposix-version-manager-bin-dirs.ts("Append, never prepend... a prepended fallback could shadow the binary the user actually runs with an older one from a stale nvm version directory") — appending alone wouldn't fix it here, because a bare GUI PATH has no othernodefornvm_ls_currentto find.Tests
hydrate-shell-path.test.ts— probe uses the recorded launch PATH, not the seededprocess.env.PATH; falls back toprocess.envwhen nothing was recorded (dev/unpackaged).configure-process.test.ts— the pre-seed PATH is recorded, and recorded before the seeds land.pnpm typecheckclean;src/main/startup/,ipc/cli,preflight/,local-agent-cli-presencesuites pass (292 tests).Verification
Replaying a real pane's env through Orca's own bash wrapper, before and after removing the stray bare install:
Windows
Initially I exempted win32 from the probe insulation, on the grounds that no Windows seed pins a node version. That was wrong.
getVersionManagerDirectoriescallsgetNvmVersionDirectorieson every platform (node-cli-command-resolution.ts:132-137), so a Git Bash user whose nvm uses the POSIX~/.nvm/versions/nodelayout gets the newest version dir seeded on Windows too — and the Git Bash probe is-ilc, same as POSIX. Verified with a win32 fixture:getVersionManagerBinPaths({ platform: 'win32', homePath })returns<home>/.nvm/versions/node/v22.9.0/binfirst when both v20 and v22 exist.So win32 now gets the same insulation. The one genuine Windows difference is the env key — Windows keys it
Path, sorecordLaunchPathrecords the key alongside the value andshellProbeEnvoverwrites that entry in place, never leaving bothPathandPATHon the spawn env. Covered by a new test.No ordering contract
The probe's clean PATH is not handed over from the seeding site.
hydrate-shell-path.tssnapshots PATH during module init, which runs while the import graph is evaluated — strictly before any statement in main's body — so it cannot observe the seeds. All seven importers are static, so no lazy import defeats it, andconfigure-process.tsdoes not import the probe module at all. There is no call ordering left for a refactor to break. A test asserts the probe ignores laterprocess.envmutation and fails if a live read is reintroduced.Probe diagnosability
A failed probe left the seeded newest-nvm dir in front and said nothing, so the population whose rc files blow the 5s budget hit the original symptom with no trace. It now logs
[shell-path] login-shell probe failed (<failureReason>). The probe env also setsORCA_SHELL_PATH_PROBE=1, so rc files that exec into a multiplexer or start a heavy prompt can take a fast path — the probe is an interactive login shell and previously offered no opt-out.Verification
node=v26.7.0 codex=NOTFOUNDbefore,node=v24.18.0 codex=<default>/bin/codexafter.src/main/startup/,src/main/codex-cli/,src/main/preflight/— 39 files, 313 passed, 5 skipped.That run is what caught the last commit: spreading
process.envyields a plain case-sensitive object while Windows resolves env names case-insensitively, so writing the capturedPathback left the seeded value live underPATH. Mocked-platform tests on macOS could not see it.pnpm typecheckclean; 327 tests green across the blast radius locally.Related
Same root cause as #10932, which reported it via a different symptom (Node ABI mismatch when the shebang picks up the newest nvm node for a CLI installed under an older one). This PR should resolve that repro in the common case, but does not close it — the newest-nvm-wins policy in
getNvmVersionDirectoriesis untouched, and the seed still wins whenever the probe does not land. Details in a comment there.Closes #16339 and #16340 — both were folded into this PR rather than deferred.