pi-orchestrator-config 4.3.4 → 4.3.6

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
@@ -452,28 +452,34 @@ variables. Pi-config settings (image_model, acpx_agents, vertex_claude_1m, etc.)
452
452
  go in `.pi/pi-config-settings.json` — not here:
453
453
 
454
454
  ```env
455
- # Timezone (host timezone for correct timestamps)
456
- TZ=Asia/Jerusalem
455
+ # Timezone (host timezone for correct timestamps) — plain value, no inline comment:
456
+ # `docker run --env-file` does not strip trailing comments.
457
+ TZ=America/New_York
457
458
 
458
459
  # Host username (creates /home/<user> -> container home symlink so host paths resolve)
459
- PI_HOST_USER=myakove
460
+ PI_HOST_USER=youruser
461
+
462
+ # Host UID/GID belong on the docker alias (see below), not hardcoded here:
463
+ # -e PI_HOST_UID="$(id -u)" -e PI_HOST_GID="$(id -g)"
464
+ # The entrypoint remaps user `node` to those ids so bind-mounted 0600/0700
465
+ # files (~/.npmrc, ~/.ssh) are readable. IPA/LDAP users are not UID 1000.
460
466
 
461
467
  # Google Cloud / Vertex AI
462
468
  GOOGLE_CLOUD_PROJECT=your-project-id
463
469
  GOOGLE_CLOUD_LOCATION=us-east5
464
- GOOGLE_APPLICATION_CREDENTIALS=/home/myakove/.config/gcloud/application_default_credentials.json
470
+ GOOGLE_APPLICATION_CREDENTIALS=/home/youruser/.config/gcloud/application_default_credentials.json
465
471
 
466
472
  # GitHub
467
473
  GITHUB_TOKEN=ghp_xxx
468
474
  GITHUB_API_TOKEN=ghp_xxx
469
- GH_CONFIG_DIR=/home/myakove/.config/gh
475
+ GH_CONFIG_DIR=/home/youruser/.config/gh
470
476
 
471
477
  # Gemini (optional — required for image generation)
472
478
  GEMINI_API_KEY=xxx
473
479
 
474
480
  # mcpl (MCP Launchpad) config path inside the container (must match mount target)
475
481
  # Use your actual home path — it resolves inside the container via the PI_HOST_USER symlink
476
- MCPL_CONFIG_FILES=/home/myakove/.config/mcpl/mcp.json
482
+ MCPL_CONFIG_FILES=/home/youruser/.config/mcpl/mcp.json
477
483
  ```
478
484
 
479
485
  Pass via `--env-file /path/to/.env` in the docker run command.
@@ -645,6 +651,7 @@ PI_PIDIFF_ENABLE=false pi
645
651
  | `kubectl` / `oc` | Kubernetes and OpenShift CLI |
646
652
  | `agent-browser` | Browser automation CLI (navigate, click, screenshot, forms) |
647
653
  | `procps` | Process utilities (ps, top, pgrep, pkill) |
654
+ | `passwd` | `usermod`/`groupmod` — init remaps user `node` to `PI_HOST_UID`/`PI_HOST_GID` |
648
655
  | `docker` / `podman` | Container CLIs (used via `docker-safe` read-only wrapper) |
649
656
  | `docker-safe` | Restricted Docker/Podman wrapper — container only (ps, logs, inspect, top, stats) |
650
657
  | `jq` | JSON processing |
@@ -701,6 +708,8 @@ alias pi-docker='docker pull ghcr.io/myk-org/pi-config:latest && \
701
708
  --name "pi-config-$(basename $PWD)-$(date +%s)" \
702
709
  --network host \
703
710
  --env-file "$HOME/.pi/.env" \
711
+ -e PI_HOST_UID="$(id -u)" \
712
+ -e PI_HOST_GID="$(id -g)" \
704
713
  -v "$PWD":"$PWD":rw \
705
714
  -v "$HOME/.pi":"$HOME/.pi":rw \
706
715
  -v "$HOME/.gitconfig":"$HOME/.gitconfig":ro \
@@ -722,7 +731,10 @@ alias pi-docker='docker pull ghcr.io/myk-org/pi-config:latest && \
722
731
 
723
732
  Then just run `pi-docker` from any project directory.
724
733
 
725
- > **Startup note:** The container runs as non-root user `node` (UID 1000).
734
+ > **Startup note:** The container runs as non-root user `node`. At start the
735
+ > entrypoint remaps `node` to `PI_HOST_UID`/`PI_HOST_GID` (pass `$(id -u)` /
736
+ > `$(id -g)` on the alias) so bind-mounted secrets match the host user.
737
+ > Without those env vars, `node` stays UID 1000 (breaks IPA/LDAP hosts).
726
738
  > `pi install` runs on each start.
727
739
  > A `WARNING` on stderr is normal when the package is already cached in `~/.pi`.
728
740
  > If pi misbehaves or the warning persists, verify network connectivity
@@ -919,34 +919,77 @@ export function registerAsyncAgents(
919
919
  }), { mode: 0o600 });
920
920
 
921
921
  // Find the runner script
922
- const runnerPath = path.join(path.dirname(new URL(import.meta.url).pathname), "async-runner.ts");
922
+ const runnerPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "async-runner.ts");
923
923
 
924
- // Find jiti for TypeScript execution
924
+ // Find jiti for TypeScript execution — walk UP from known roots until
925
+ // found; never assume a fixed depth (install layouts vary: dist/,
926
+ // dist/bundle/, workspace hoisting, nested node_modules).
925
927
  let jitiCliPath: string | undefined;
926
- // Strategy 1: resolve from pi's own require context
928
+ const jitiProbes: string[] = [];
929
+ const findJitiUnder = (label: string, startDir: string): string | undefined => {
930
+ let dir = startDir;
931
+ while (dir !== path.dirname(dir)) {
932
+ const candidate = path.join(dir, "node_modules/jiti/lib/jiti-cli.mjs");
933
+ jitiProbes.push(`${fs.existsSync(candidate) ? "HIT" : "--"} ${candidate}`);
934
+ if (fs.existsSync(candidate)) return candidate;
935
+ dir = path.dirname(dir);
936
+ }
937
+ jitiProbes.push(`(exhausted from ${label}: ${startDir})`);
938
+ return undefined;
939
+ };
940
+
941
+ // Strategy 1: resolve from pi's own install location (the running binary).
927
942
  try {
928
- const piPkgDir = path.dirname(require.resolve("@earendil-works/pi-coding-agent/package.json"));
929
- const candidate = path.join(piPkgDir, "node_modules/jiti/lib/jiti-cli.mjs");
930
- if (fs.existsSync(candidate)) jitiCliPath = candidate;
943
+ if (process.argv[1]) {
944
+ const piRoot = path.dirname(fs.realpathSync(process.argv[1]));
945
+ jitiCliPath = findJitiUnder("argv-binary", piRoot);
946
+ }
931
947
  } catch (e: any) {
932
- log.debug(`jiti strategy 1 (require.resolve) failed: ${e?.message || e}`);
948
+ log.debug(`jiti strategy 1 (running binary) failed: ${e?.message || e}`);
933
949
  }
934
- // Strategy 2: resolve from the pi binary (works for global npm installs)
950
+
951
+ // Strategy 2: resolve the pi SDK package from this module's require context.
935
952
  if (!jitiCliPath) {
936
953
  try {
937
- const piScript = process.argv[1];
938
- if (piScript) {
939
- const realPath = fs.realpathSync(piScript);
940
- // pi script -> dist/cli.js, package root is 2 levels up
941
- const piPkgDir = path.dirname(path.dirname(realPath));
942
- const candidate = path.join(piPkgDir, "node_modules/jiti/lib/jiti-cli.mjs");
943
- if (fs.existsSync(candidate)) jitiCliPath = candidate;
954
+ const req = createRequire(fileURLToPath(import.meta.url));
955
+ let sdkPkgJson: string | undefined;
956
+ try {
957
+ sdkPkgJson = req.resolve("@earendil-works/pi-coding-agent/package.json");
958
+ } catch {
959
+ // Newer SDK "exports" maps hide ./package.json — resolve the entry
960
+ // and walk up to the owning package.json instead.
961
+ let dir = path.dirname(req.resolve("@earendil-works/pi-coding-agent"));
962
+ while (dir !== path.dirname(dir)) {
963
+ const pj = path.join(dir, "package.json");
964
+ if (fs.existsSync(pj) && JSON.parse(fs.readFileSync(pj, "utf8"))?.name === "@earendil-works/pi-coding-agent") { sdkPkgJson = pj; break; }
965
+ dir = path.dirname(dir);
966
+ }
944
967
  }
968
+ if (sdkPkgJson) jitiCliPath ??= findJitiUnder("sdk-pkg", path.dirname(sdkPkgJson));
945
969
  } catch (e: any) {
946
- log.debug(`jiti strategy 2 (process.argv) failed: ${e?.message || e}`);
970
+ log.debug(`jiti strategy 2 (SDK package) failed: ${e?.message || e}`);
947
971
  }
948
972
  }
949
973
 
974
+ // Strategy 3: this package's own node_modules (git/stow clones ship jiti).
975
+ if (!jitiCliPath) {
976
+ try {
977
+ jitiCliPath = findJitiUnder("extension-dir", path.dirname(fileURLToPath(import.meta.url)));
978
+ } catch (e: any) {
979
+ log.debug(`jiti strategy 3 (package root) failed: ${e?.message || e}`);
980
+ }
981
+ }
982
+
983
+ // Fail fast with a self-explanatory error instead of spawning a child that
984
+ // cannot load TypeScript (bare node dies on ".js"→".ts" specifiers).
985
+ if (!jitiCliPath) {
986
+ log.error(`async-spawn: ${id} — no jiti-cli.mjs found. Probes:\n ${(jitiProbes.slice(-14)).join("\n ")}`);
987
+ // Clean up the artifacts we already created — nothing will consume them.
988
+ try { fs.rmSync(configPath, { force: true }); } catch {}
989
+ try { fs.rmSync(workerDir, { recursive: true, force: true }); } catch {}
990
+ return { id, error: "jiti-cli.mjs not found — cannot execute async agent (see orchestrator logs)" };
991
+ }
992
+
950
993
  const spawnArgs = jitiCliPath
951
994
  ? [jitiCliPath, runnerPath, configPath]
952
995
  : [runnerPath, configPath];
@@ -957,6 +1000,10 @@ export function registerAsyncAgents(
957
1000
  PI_SUBAGENT_CHILD: "1",
958
1001
  PI_AGENT_NAME: agentName,
959
1002
  PI_PRIMARY_MODEL: process.env.PI_PRIMARY_MODEL || process.env.PI_MODEL || "",
1003
+ // Isolate children from the shared jiti FS cache — other sessions running
1004
+ // different node/jiti versions can poison it and kill children at boot.
1005
+ JITI_CACHE: "false",
1006
+ JITI_FS_CACHE: "false",
960
1007
  __PI_CONFIG_SESSION_ID: (globalThis as any).__piConfigSessionId || "",
961
1008
  __PI_PARENT_SESSION_ID: (globalThis as any).__piConfigSessionId || "",
962
1009
  };
@@ -1004,14 +1051,21 @@ export function registerAsyncAgents(
1004
1051
  };
1005
1052
  asyncState.jobs.set(id, job);
1006
1053
 
1054
+ const stderrLogPath = path.join(workerDir, "child-stderr.log");
1055
+ const stderrLog = fs.createWriteStream(stderrLogPath, { flags: "w" });
1056
+ stderrLog.on("error", () => {}); // never let a log write failure crash the session
1057
+ log.debug(`async-spawn: ${job.id} stderr → ${stderrLogPath}`);
1058
+
1007
1059
  const proc = spawn(process.execPath, spawnArgs, {
1008
1060
  cwd,
1009
- stdio: "ignore",
1061
+ stdio: ["ignore", "ignore", "pipe"],
1010
1062
  windowsHide: true,
1011
1063
  env: spawnEnv,
1012
1064
  });
1065
+ proc.stderr?.pipe(stderrLog);
1013
1066
 
1014
1067
  proc.once("error", (err) => {
1068
+ try { stderrLog.destroy(); } catch {}
1015
1069
  log.info(`spawn-error: ${job.id} — ${err.message}`);
1016
1070
  if (job.status === "complete" || job.status === "failed") return;
1017
1071
  job.status = "failed";
@@ -1037,8 +1091,27 @@ export function registerAsyncAgents(
1037
1091
  setTimeout(() => {
1038
1092
  if (job.status === "complete" || job.status === "failed") return;
1039
1093
  log.info(`spawn-exit: ${job.id} — code=${code} signal=${signal}`);
1094
+ let stderrTail = "";
1095
+ try {
1096
+ if (fs.existsSync(stderrLogPath)) {
1097
+ // Tail-read only — the log can be arbitrarily large on noisy failures.
1098
+ const size = fs.statSync(stderrLogPath).size;
1099
+ const start = Math.max(0, size - 4096);
1100
+ let raw = "";
1101
+ if (size > 0) {
1102
+ const fd = fs.openSync(stderrLogPath, "r");
1103
+ try {
1104
+ const buf = Buffer.alloc(Math.min(size - start, 4096));
1105
+ fs.readSync(fd, buf, 0, buf.length, start);
1106
+ raw = buf.toString("utf8");
1107
+ } finally { fs.closeSync(fd); }
1108
+ }
1109
+ stderrTail = raw.trim().slice(-1200);
1110
+ }
1111
+ } catch {}
1112
+ log.warn(`async-child failed: ${job.id} code=${code}${stderrTail ? " (see child-stderr.log)" : " (no stderr captured)"}`);
1040
1113
  job.status = "failed";
1041
- job.output = `Process exited with code ${code} signal ${signal}`;
1114
+ job.output = `Process exited with code ${code} signal ${signal}${stderrTail ? `\n--- child stderr (tail) ---\n${stderrTail}` : ""}`;
1042
1115
  job.durationMs = Date.now() - job.startedAt;
1043
1116
  job.updatedAt = Date.now();
1044
1117
  job.sideEffectsApplied = true;
@@ -152,18 +152,28 @@ export const MIN_PI_VERSION = "0.84.0";
152
152
 
153
153
  /** Get the installed pi version from its package.json. */
154
154
  export function getPiVersion(): string | null {
155
+ // 1) Resolve from the running binary — walk up until we find the package
156
+ // root (works for any layout: dist/cli.js, dist/bundle/cli.js, src/…).
155
157
  try {
156
- // Resolve from pi's own install location (process.argv[1] → dist/cli.js → package root)
157
158
  const piScript = process.argv[1];
158
159
  if (piScript) {
159
- const realPath = fs.realpathSync(piScript);
160
- const piPkgPath = path.join(path.dirname(path.dirname(realPath)), "package.json");
161
- if (fs.existsSync(piPkgPath)) {
162
- const pkg = JSON.parse(fs.readFileSync(piPkgPath, "utf-8"));
163
- if (pkg.name === "@earendil-works/pi-coding-agent" && pkg.version) return pkg.version;
160
+ let dir = path.dirname(fs.realpathSync(piScript));
161
+ while (dir !== path.dirname(dir)) {
162
+ const pkgPath = path.join(dir, "package.json");
163
+ if (fs.existsSync(pkgPath)) {
164
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
165
+ if (pkg.name === "@earendil-works/pi-coding-agent" && pkg.version) return pkg.version;
166
+ }
167
+ dir = path.dirname(dir);
164
168
  }
165
169
  }
166
170
  } catch {}
171
+ // 2) Ask the CLI the user actually runs — authoritative for wrapper/bundle layouts.
172
+ try {
173
+ const out = execFileSync("pi", ["--version"], { timeout: 5000, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
174
+ const m = out.match(/\d+\.\d+\.\d+(?:-[\w.]+)?/);
175
+ if (m) return m[0];
176
+ } catch {}
167
177
  return null;
168
178
  }
169
179
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-orchestrator-config",
3
- "version": "4.3.4",
3
+ "version": "4.3.6",
4
4
  "description": "Orchestrator pattern for pi: specialist subagents, enforcement, code review loop, workflow commands",
5
5
  "keywords": [
6
6
  "pi-package"