runSshSync and runSshCaptureSync in connection-manager.ts invoked ssh
through the Bun shell with .quiet().nothrow() but no timeout and no
abort signal. They sit on the ensureHostInfo -> probeHostInfo /
ensureConnection path that runs *before* SshTool.execute applies the
user-supplied command timeout, so an unreachable host or wedged
control-master hung the tool forever.
Switch both helpers to ptree.exec with a 30s timeout,
allowNonZero: true, allowAbort: true, and stderr: "full". The
timeout is a parameter (default 30_000) so tests can override with a
short bound; both helpers still return their existing failure-result
shape (exitCode / stdout / stderr), so ensureConnection can surface a
'Failed to start SSH master ...' error instead of blocking.
Add a regression test that stages a fake ssh binary trapping SIGTERM
and sleeping 300s, then confirms both helpers return within a bounded
window with a non-zero exit code.
Fixes#4232
When csh/tcsh aborts the initial host-info marker probe before emitting
PI_HOST_PROBE=, the previous fallback returned os/shell unknown without
ever checking whether sh -lc still worked. That kept ssh:// rejecting a
POSIX-capable host (P2 from PR #3722 review).
The marker-missing fallback now runs probeTransferShell before returning.
If sh/bash/zsh round-trips the transfer marker, the fallback carries
transferShell and derives os from that probe's uname -s output. Windows
compat unames (MINGW/MSYS/Cygwin/Windows) still classify as windows so
ssh:// keeps refusing Windows hosts.
Added osFromUname coverage for Linux, GNU/Linux, Darwin, Windows compat
unames, and unknown payloads.
The host-info probe already recovers its marker from stderr (some
remotes have dotfiles that swap fd 1/2), but `probeTransferShell` only
scanned `probe.stdout`. That left `transferShell` unset on those
hosts and made `ssh://` refuse a POSIX-capable remote (P2 from PR
#3722 review).
Extracted the both-streams scan into `findProbeMarker(stdout, stderr,
marker)` and routed the transfer probe through it: stdout first,
stderr as the rescue. Same recovery contract as the host probe.
`TRANSFER_PROBE_MARKER` exported alongside `findProbeMarker` so the
recovery branch is unit-testable without touching disk.
Tests: covers stdout-only, stderr-only, both-streams-prefer-stdout,
and neither-stream.
Replace the login-shell-name allowlist in `ensurePosixRemote` with a
capability check against a newly probed `transferShell`. The host probe
runs `sh -lc` / `bash -lc` / `zsh -lc` against the remote and records
the first candidate whose printf marker round-trips; `uname -s` from the
same probe also refines the OS classification when the first probe could
not resolve it.
Three compounding problems fixed:
- The host probe parsed only the first stdout line, so login-shell
banners or any startup noise would land ahead of the payload and
classify the host as `shell: "unknown"`. The probe now frames its
payload with a `PI_HOST_PROBE=` marker (see `extractProbePayload`)
and scans both streams for the marker line.
- `shouldRefreshHostInfo` did not treat `{os: "linux", shell: "unknown"}`
as stale, so a single bad classification stuck and kept failing
later `ssh://` operations. It now refreshes any non-Windows cache
entry without a verified `transferShell`.
- The transfer guard refused the host on the self-reported login-shell
name. It now gates on `info.transferShell`, which is the shell OMP
actually verified can run `head`/`cat`/`mv`/`test`/`ls`. The
refusal message names the capability we couldn't confirm.
`HOST_INFO_VERSION` bumped 3 → 4 so existing caches re-probe and pick
up `transferShell`. `parseHostInfo` exported so the cache round-trip
of `transferShell` is testable without touching disk.
Fixes#3719
remotePathFromUrl now rejects a URL query string or fragment, so a mistyped ssh://host/tmp/a?draft no longer silently operates on /tmp/a; a literal ?/# in a remote filename must be percent-encoded (%3F/%23).
ssh:// transfers now require a sh/bash/zsh login shell. fish can't parse the POSIX transfer snippets and csh/tcsh apply ! history expansion to the command line, so they're refused (use the ssh tool). Host-shell detection no longer misclassifies fish/csh/tcsh as sh (basename allowlist over endsWith), and HOST_INFO_VERSION is bumped to re-probe caches that stored the old classification.
Kept missing-file and directory identity validation on Windows while skipping only the Unix mode-bit rejection.
Added regression tests for Windows missing and directory identity paths.
Fixes#2850
Skipped Unix mode-bit validation for ssh identity files on native Windows while preserving the stricter check for Unix-like platforms.
Added regression coverage for Windows ssh argument building with permissive low mode bits and for Unix rejection of group/world-readable keys.
Fixes#2850
- Fixed help rendering so `--help` no longer triggers unrelated command loaders.
- Fixed startup span logging to emit markers only with PI_DEBUG_STARTUP set.
- Fixed logger startup trace behavior for `:start`, `:done`, and `:fail` phases.
- Fixed prompt template processing with cached raw-template compilation and safer formatting.
- Optimized symbol and tag parsing in prompt templates via manual parsers.
- Replaced all Bun.which() calls with $which() utility from @oh-my-pi/pi-utils across 22 files.
- Removed findBashOnPath() wrapper function from procmgr.ts, consolidating binary path resolution.
- Updated AGENTS.md documentation to reflect new $which() API usage pattern.
- Centralized binary detection logic through shared utility, reducing code duplication.
- Consolidated @oh-my-pi/pi-utils subpath imports into single package root import across 100+ files.
- Moved tryParseJson utility from local web scrapers module to @oh-my-pi/pi-utils package for centralized JSON parsing.
- Renamed loadSkillsFromDir to scanSkillsFromDir and refactored skill discovery to use fs.promises.readdir instead of glob-based approach.
- Replaced custom parseJSON with tryParseJson across discovery modules for consistent error handling.
- Removed emitCustomToolSessionEvent method and cleanupSshResources function, consolidating shutdown logic into dispose method.
- Updated glob pattern construction to use GlobBuilder with literal_separator(true) for improved path handling.
- Extracted directory path utilities from multiple packages into a centralized '@oh-my-pi/pi-utils/dirs' module.
- Moved 30+ path helper functions (getAgentDir, getConfigRootDir, getPluginsDir, getMCPConfigPath, etc.) from scattered locations into a single shared utility module.
- Consolidated APP_NAME, CONFIG_DIR_NAME, and VERSION constants into the centralized dirs module for reuse across packages.
- Updated 70+ import statements across packages/ai, packages/coding-agent, packages/stats, and packages/tui to use the new centralized module.
- Removed local path construction logic and replaced with utility function calls for improved maintainability and consistency.
- Deleted packages/coding-agent/src/extensibility/plugins/paths.ts as its functions were moved to the centralized dirs module.
- Migrated environment variable access from direct process.env to centralized getEnv() utility function across all packages.
- Renamed environment variable prefix from OMP_ to PI_ throughout codebase (e.g., OMP_CODING_AGENT_DIR -> PI_CODING_AGENT_DIR).
- Removed automatic environment variable migration from PI_ to OMP_ prefixes via migrate-env.ts module.
- Removed env setting from configuration schema and applyEnvironmentVariables() method from settings.
- Updated CI/CD build configuration to use PI_COMPILED flag instead of OMP_COMPILED.
- Changed venvPath property in PythonRuntime from nullable (string | null) to optional (string | undefined).
- Removed Prettier configuration files (.prettierignore and .prettierrc) and migrated formatting to Biome.
- Updated Biome configuration from version 2.3.11 to 2.3.12 and changed arrowParentheses rule from 'always' to 'asNeeded'.
- Pinned @biomejs/biome dependency to exact version 2.3.12 in package.json and bun.lock.
- Applied consistent arrow function formatting across 489 files by removing unnecessary parentheses around single parameters.
- Removed blank lines after comment blocks and reorganized imports for consistency across the codebase.
- Converted named imports from node modules (fs, path, os) to namespace imports across all packages.
- Extended extension loader error handling with isEacces and hasFsCode type guards.
- Converted readdirSync, readFileSync, and statSync to async readdir, readFile, stat across skills and agent discovery.
- Made scanDirectoryForSkills async and refactored custom directory scanning to use Promise.all for concurrent processing.
- Updated agent discovery to use fs/promises for async file reading and refactored helper patterns.
- Added AgentParsingError exception class for better error handling during agent parsing.
- Added filesystem error type guards (isEnoent, isEacces, isPerm, etc.) to pi-utils for safe error checking.
- Added color manipulation utilities to pi-utils for accessibility features.
- Added color-blind mode setting to settings manager.
- Migrated plugins, settings, and config modules from sync to async file operations.
- Updated error handling to use new pi-utils type guards for type-safe checking.
- Removed WASM generation script; use Bun `wasm?raw` loader for imports.
- Added bunfig.toml with loaders for `.md`, `.py`, and `.wasm?raw` text imports.
- Added types/assets/index.d.ts for global TypeScript module declarations.
- Unified TypeScript configuration with tsgo-based checking across monorepo.
- Removed build and WASM steps from install and publish pipelines.
- Added tsconfig.publish.json files to all packages with optimized publish-time configuration.
- Updated all package.json scripts with prepublishOnly hooks for correct type checking during publish.
- Added @oh-my-pi/omp-stats path mappings to root tsconfig.json for consistent imports.
- Added WASM generation script for photon module and integrated into install:dev script.