Summary
On Windows, the native rlm_query tool fails to spawn child Pi processes with spawn pi ENOENT. The child never starts; the calling agent sees the raw ENOENT error.
Environment
- OS: Windows 11 (
win32)
- Node: v24.14.0 (also reproduced shape of the issue on any modern Node ≥ 18.20/20.12, where spawning
.cmd/.bat without shell: true is impossible by design)
- pi: 0.85.1 (
pi -p headless, installed via npm global → pi.cmd shim)
- pi-recursive: 0.6.1 (npm)
Reproduction
- On Windows, install the extension (requires bypassing the
os field — see related note below).
- In any Pi session, ask:
Use the native rlm_query tool to ask a child agent what 12 * 7 is.
- The tool call returns a child error containing:
The calling agent can still work around it by manually spawning pi -p, but the native tool path is dead.
Root cause
extensions/ypi/native-tool.ts → spawnChildPi():
const child = spawn(process.env.YPI_PI_BIN || "pi", args, { ... });
On Windows, npm global installs create pi.cmd shims, not executables. Node's spawn() cannot execute .cmd shims without shell: true (and since the CVE-2024-27980 fix, spawning .cmd/.bat without shell: true is rejected outright). So spawn("pi", ...) → ENOENT.
Two adjacent observations:
detached: process.platform !== "win32" shows the code already special-cases win32, but the bin resolution was missed.
- The registration block already debug-warns when
YPI_PI_BIN is not executable (canExecute), so bin-resolution problems were anticipated — but the default pi path itself is broken on win32.
Why not shell: true
The obvious one-liner (shell: process.platform === "win32") is unsafe here: args ends with args.push(params.prompt) — a multi-line, arbitrary-text prompt passed as a spawn argument. With shell: true, Node concatenates args unquoted into a cmd.exe command line: quotes, &, ^, %VAR%, and newlines in the prompt would break or inject into the shell command. Multi-line args cannot be carried through cmd.exe at all.
Tested fix
On win32, spawn process.execPath (node/bun) with process.argv[1] (Pi's own entry script) prepended to the args. No shell involved, no quoting hazards, and it inherits whatever runtime launched the parent (node or bun). Verified working on Windows: rlm_query → child Pi → correct answer, no errors.
function spawnChildPi(args: string[], env: NodeJS.ProcessEnv, cwd: string, timeoutSeconds: number | undefined, signal?: AbortSignal): Promise<NativeRunResult> {
return new Promise((resolve, reject) => {
// win32: npm's `pi` is a .cmd shim — spawn("pi") throws ENOENT (and .cmd
// requires shell:true since CVE-2024-27980). shell:true is unsafe here because
// args carries the multi-line prompt. Spawn the parent's own runtime + entry instead.
const useNodeEntry = process.platform === "win32" && !process.env.YPI_PI_BIN && !!process.argv[1];
const bin = useNodeEntry ? process.execPath : (process.env.YPI_PI_BIN || "pi");
const childArgs = useNodeEntry ? [process.argv[1], ...args] : args;
const child = spawn(bin, childArgs, {
cwd,
env,
stdio: ["ignore", "pipe", "pipe"],
detached: process.platform !== "win32",
});
// ... unchanged
YPI_PI_BIN keeps priority for users who explicitly set it.
Related note: os field blocks Windows entirely
package.json restricts the package to "os": ["linux", "darwin"], so both documented install paths fail on win32:
pi install npm:pi-recursive → npm notsup (the install above only works with --force)
pi -e npm:pi-recursive → Pi runs its own npm install (no --force) into a temp dir → same notsup failure, so the one-shot mode documented in the README can never work on Windows
The extension itself is pure TypeScript and runs fine on win32 once installed (the only real blocker was the spawn bug above). If Windows is meant to be supported, dropping the os restriction (or adding "win32") plus the spawn fix would make both paths work; if not, a README note would save Windows users the debugging session.
Happy to turn the patch into a PR if useful.
Summary
On Windows, the native
rlm_querytool fails to spawn child Pi processes withspawn pi ENOENT. The child never starts; the calling agent sees the raw ENOENT error.Environment
win32).cmd/.batwithoutshell: trueis impossible by design)pi -pheadless, installed via npm global →pi.cmdshim)Reproduction
osfield — see related note below).Use the native rlm_query tool to ask a child agent what 12 * 7 is.The calling agent can still work around it by manually spawning
pi -p, but the native tool path is dead.Root cause
extensions/ypi/native-tool.ts→spawnChildPi():On Windows, npm global installs create
pi.cmdshims, not executables. Node'sspawn()cannot execute.cmdshims withoutshell: true(and since the CVE-2024-27980 fix, spawning.cmd/.batwithoutshell: trueis rejected outright). Sospawn("pi", ...)→ENOENT.Two adjacent observations:
detached: process.platform !== "win32"shows the code already special-cases win32, but the bin resolution was missed.YPI_PI_BINis not executable (canExecute), so bin-resolution problems were anticipated — but the defaultpipath itself is broken on win32.Why not
shell: trueThe obvious one-liner (
shell: process.platform === "win32") is unsafe here:argsends withargs.push(params.prompt)— a multi-line, arbitrary-text prompt passed as a spawn argument. Withshell: true, Node concatenates args unquoted into a cmd.exe command line: quotes,&,^,%VAR%, and newlines in the prompt would break or inject into the shell command. Multi-line args cannot be carried through cmd.exe at all.Tested fix
On win32, spawn
process.execPath(node/bun) withprocess.argv[1](Pi's own entry script) prepended to the args. No shell involved, no quoting hazards, and it inherits whatever runtime launched the parent (node or bun). Verified working on Windows:rlm_query→ child Pi → correct answer, no errors.YPI_PI_BINkeeps priority for users who explicitly set it.Related note:
osfield blocks Windows entirelypackage.jsonrestricts the package to"os": ["linux", "darwin"], so both documented install paths fail on win32:pi install npm:pi-recursive→ npmnotsup(the install above only works with--force)pi -e npm:pi-recursive→ Pi runs its ownnpm install(no--force) into a temp dir → samenotsupfailure, so the one-shot mode documented in the README can never work on WindowsThe extension itself is pure TypeScript and runs fine on win32 once installed (the only real blocker was the spawn bug above). If Windows is meant to be supported, dropping the
osrestriction (or adding"win32") plus the spawn fix would make both paths work; if not, a README note would save Windows users the debugging session.Happy to turn the patch into a PR if useful.