pi-open-agents 0.1.27 → 0.1.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -398,7 +398,7 @@ opening an issue or pull request.
398
398
 
399
399
  ```bash
400
400
  npm install
401
- npm test # 178 tests
401
+ npm test # 191 tests
402
402
  npm run typecheck # tsc --noEmit
403
403
  ```
404
404
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-open-agents",
3
- "version": "0.1.27",
3
+ "version": "0.1.28",
4
4
  "description": "Unified agent and subagent management for pi coding agent, with OpenCode-compatible agent definitions.",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/index.ts CHANGED
@@ -261,8 +261,8 @@ export { matchPattern, isWildcardPattern } from "./permission/matcher.ts";
261
261
  export { evaluate, toolToPermission, getDisabledTools, filterDisabledTools } from "./permission/evaluator.ts";
262
262
 
263
263
  // Subagent re-exports
264
- export { runSubagent, resolveSubagentSession, resolvePiEntryPoint } from "./subagent/executor.ts";
265
- export type { AgentProgress, AgentResult, RunSubagentOptions, SubagentSessionInfo, SubagentSessionMode } from "./subagent/executor.ts";
264
+ export { runSubagent, resolveSubagentSession, resolvePiEntryPoint, isVirtualBunPath, resolveCompiledCommand } from "./subagent/executor.ts";
265
+ export type { AgentProgress, AgentResult, RunSubagentOptions, SubagentSessionInfo, SubagentSessionMode, HostProcessProbe } from "./subagent/executor.ts";
266
266
  export { resolveSkills } from "./subagent/skills.ts";
267
267
  export type { ResolvedSkill } from "./subagent/skills.ts";
268
268
  export { buildSubagentPrompt } from "./subagent/prompt.ts";
@@ -227,9 +227,95 @@ export function subagentSessionDir(
227
227
  return path.join(agentDir, "sessions", safeProject, "subagents");
228
228
  }
229
229
 
230
+ /**
231
+ * Detect Bun's in-memory virtual paths (issue #28).
232
+ *
233
+ * Inside a `bun build --compile` single-file binary, `process.argv[1]` (and on
234
+ * some platforms `process.execPath`) resolves to a compile-time virtual entry
235
+ * such as `/$bunfs/root/pi`. That path does not exist on disk — any
236
+ * `realpathSync`/`lstat` on it throws ENOENT. A real on-disk executable path
237
+ * never contains these markers, so the check has no false positives.
238
+ */
239
+ export function isVirtualBunPath(candidate: string | undefined | null): boolean {
240
+ if (!candidate) return false;
241
+ return (
242
+ candidate.includes("$bunfs") || candidate.includes("~BUN") || candidate.includes("%7EBUN")
243
+ );
244
+ }
245
+
246
+ /**
247
+ * Injectable host-process values for {@link resolvePiEntryPoint}.
248
+ *
249
+ * The production default reads `process.execPath` / `process.argv` /
250
+ * `/proc/self/exe` directly. Tests inject exact strings (including the
251
+ * virtual `/$bunfs/...` paths a compiled host reports) without spawning
252
+ * a real compiled binary.
253
+ */
254
+ export interface HostProcessProbe {
255
+ execPath?: string;
256
+ argv0?: string;
257
+ argv1?: string;
258
+ readProcSelfExe?: () => string;
259
+ }
260
+
261
+ /**
262
+ * Pick the real executable for a compiled single-file host (issue #28).
263
+ *
264
+ * Priority order (most reliable first):
265
+ * 1. `/proc/self/exe` (kernel truth on Linux),
266
+ * 2. `process.execPath` (real in every observed compiled binary),
267
+ * 3. `process.argv[0]` — ONLY when absolute. A compiled Bun binary reports
268
+ * the bare word `"bun"` here, which must never become the spawn command
269
+ * (it would resolve via PATH to the Bun runtime instead of pi).
270
+ *
271
+ * Returns `undefined` when every candidate is missing or virtual — the caller
272
+ * turns that into a descriptive error instead of spawning a `$bunfs` path.
273
+ */
274
+ export function resolveCompiledCommand(probe: {
275
+ execPath?: string;
276
+ argv0?: string;
277
+ procSelfExe?: string | undefined;
278
+ }): string | undefined {
279
+ for (const candidate of [probe.procSelfExe, probe.execPath]) {
280
+ if (candidate && !isVirtualBunPath(candidate)) return candidate;
281
+ }
282
+ const argv0 = probe.argv0;
283
+ if (argv0 && path.isAbsolute(argv0) && !isVirtualBunPath(argv0)) return argv0;
284
+ return undefined;
285
+ }
286
+
230
287
  export function resolvePiEntryPoint(
231
288
  resolveSelf: (specifier: string) => string = (specifier) => import.meta.resolve(specifier),
289
+ hostProbe?: HostProcessProbe,
232
290
  ): PiResolution {
291
+ const execPath = hostProbe?.execPath ?? process.execPath;
292
+ const argv0 = hostProbe?.argv0 ?? process.argv[0];
293
+ const argv1 = hostProbe?.argv1 ?? process.argv[1];
294
+
295
+ // Compiled single-file host (`bun build --compile`): the entry "file" is a
296
+ // virtual `$bunfs` path that does not exist on disk. The binary IS the CLI,
297
+ // so resolve the real executable and return an empty entry point — the args
298
+ // builder omits it and the child receives only real CLI flags.
299
+ if (isVirtualBunPath(execPath) || isVirtualBunPath(argv1)) {
300
+ let procSelfExe: string | undefined;
301
+ try {
302
+ procSelfExe = hostProbe?.readProcSelfExe
303
+ ? hostProbe.readProcSelfExe()
304
+ : realpathSync("/proc/self/exe");
305
+ } catch {
306
+ procSelfExe = undefined;
307
+ }
308
+ const command = resolveCompiledCommand({ execPath, argv0, procSelfExe });
309
+ if (!command) {
310
+ throw new Error(
311
+ "pi-open-agents: running inside a compiled single-file pi binary but the real " +
312
+ "executable path could not be determined (only virtual paths available). " +
313
+ "Subagent delegation is unavailable in this environment.",
314
+ );
315
+ }
316
+ return { command, entryPoint: "" };
317
+ }
318
+
233
319
  let packageEntryPoint: string;
234
320
  try {
235
321
  packageEntryPoint = fileURLToPath(resolveSelf("@earendil-works/pi-coding-agent"));
@@ -239,14 +325,21 @@ export function resolvePiEntryPoint(
239
325
  // resolution which cannot see the host package from the plugin's install
240
326
  // tree (peer dependencies are never installed next to it). Fall back to
241
327
  // the running pi entry itself — the child must run the same pi anyway.
328
+ if (!argv1) {
329
+ throw new Error(
330
+ "pi-open-agents: cannot resolve the pi CLI entry point (host package " +
331
+ "self-resolution failed and process.argv[1] is missing). " +
332
+ "Subagent delegation is unavailable in this environment.",
333
+ );
334
+ }
242
335
  return {
243
- command: process.execPath,
244
- entryPoint: realpathSync(process.argv[1]),
336
+ command: execPath,
337
+ entryPoint: realpathSync(argv1),
245
338
  };
246
339
  }
247
340
  const packageRoot = path.dirname(path.dirname(packageEntryPoint));
248
341
  return {
249
- command: process.execPath,
342
+ command: execPath,
250
343
  entryPoint: path.join(packageRoot, "dist", "cli.js"),
251
344
  };
252
345
  }
@@ -542,8 +635,11 @@ export async function runSubagent(options: RunSubagentOptions): Promise<AgentRes
542
635
  const modelRegistry = buildModelRegistry(options.agentDir);
543
636
 
544
637
  // ── Build CLI args ──
638
+ // A compiled single-file host IS the CLI: its PiResolution carries an
639
+ // empty entryPoint (issue #28) and the child receives only real CLI
640
+ // flags. Non-compiled hosts keep runtime + script + flags, unchanged.
545
641
  const args = [
546
- pi.entryPoint,
642
+ ...(pi.entryPoint ? [pi.entryPoint] : []),
547
643
  "--mode", "json",
548
644
  "-p",
549
645
  "--no-skills",