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.
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.
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.
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.
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.
* chore(lint): upgrade oxlint to 1.71 and enable 7 new rules
Upgrade oxlint 1.67.0 -> 1.71.0 (1.72 was blocked by the repo's 3-day
minimum-release-age supply-chain guard; nothing here needs it). The
bump is a no-op on the existing config.
Enable 3 error rules (backlog autofixed to zero in this commit) and
4 warn rules (surface signal without gating CI):
error (autofixed, behavior-preserving):
- unicorn/prefer-node-protocol (~1531 sites: bare builtin -> node:)
- typescript/no-import-type-side-effects (~36: all-inline-type -> import type)
- unicorn/no-array-reverse (19: copy-then-reverse -> toReversed)
warn (real signal, current fires are test-only/correct):
- unicorn/no-array-fill-with-reference-type (aliasing footgun guard)
- typescript/no-unsafe-function-type (bans bare Function type)
- unicorn/prefer-array-flat-map (map().flat() -> flatMap())
- unicorn/prefer-regexp-test (.match() in bool ctx -> .test())
mobile/.oxlintrc.json extends root, so it inherits all 7; the autofix
ran from root and covered mobile/ too.
Verification (all green): oxlint 0 errors (root+mobile+aux configs),
oxfmt clean, typecheck (node+cli+web), vitest 22795 passed / 0 failed,
builds (electron-vite + web + cli) succeed. node: rewrites confirmed to
skip embedded SSH/CLI string payloads (AST-only); all toReversed sites
verified to operate on fresh copies or write-once locals.
* chore(lint): bump mobile oxlint to 1.71 so inherited rules parse
mobile/ is a standalone pnpm project pinning its own oxlint@1.67, which
lacks unicorn/no-array-fill-with-reference-type (needs >=1.70). Since
mobile/.oxlintrc.json extends the root config, mobile CI's 'cd mobile &&
oxlint' failed to parse the new rule. Bump mobile to match root (1.71).
Verified in mobile/: oxlint 0 errors, oxfmt --check clean, tsc --noEmit
pass, vitest 978 passed / 0 failed.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* feat(telemetry): instrument on_path:false triage on onboarding_agent_picked
Adds path_source and path_failure_reason to onboarding_agent_picked so the
~30% on_path:false rate on dashboard 1562016 can be split between shell
hydration failures and genuinely-not-on-PATH cases before picking a fix.
See docs/agent-on-path-detection.md.
Co-authored-by: Orca <help@stably.ai>
* fix(telemetry): close PathSource compile-time-sync hole
Add `_PathSourceSync` guard mirroring `_PathFailureReasonSync` so adding
a new `PathSource` value to the alias without updating the schema (or
vice versa) fails the build. Without it, drift would silently drop
`onboarding_agent_picked` at the strict validator. Also replace stale
line-number references in docs/agent-on-path-detection.md with named
function/handler references that survive future edits.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix(agents): detect opencode/pi CLIs installed under ~/.opencode/bin and ~/.vite-plus/bin
Packaged Electron inherits a minimal PATH without shell rc files, so
agent install-script fallback dirs stay invisible to `which` probes —
the Agents settings page then shows OpenCode/Pi as "Not installed"
even when the user can run them from Terminal.
Add ~/bin, ~/.opencode/bin, and ~/.vite-plus/bin to the packaged PATH
augmentation so preflight detection matches shell behavior.
Fixes#829
* feat(agents): hydrate PATH from user's login shell + Agents Refresh button
Packaged Electron inherits a minimal launchd PATH that misses whatever
the user's shell rc files append — ~/.opencode/bin, ~/.cargo/bin, nvm
shims, pyenv, custom tool dirs. The preceding commit hardcoded two known
install locations; this replaces that whack-a-mole pattern with a
generic approach.
On packaged startup (non-Windows), spawn `${SHELL} -ilc 'echo $PATH'`
with a 5s timeout, parse the delimited PATH, and prepend any new
segments to process.env.PATH. The result is cached for the app session
so we pay the shell-init cost at most once.
Surface a Refresh button in Settings > Agents that forces a re-probe
and re-detects installed agents — handy right after installing a new
CLI, no restart needed.
Live-verified that a `zsh -ilc` spawn with a minimal launchd-style env
still resolves the user's full PATH (32+ segments including the
rc-appended dirs).
* refactor(hydrate-shell-path): simplify dedup with Set + Set.difference
Set preserves insertion order, so PATH first-match-wins semantics are
preserved without manual tracking. Set.prototype.difference (Node 22+)
expresses the new-segments calculation in mergePathSegments as the
set-difference operation it always was.