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 +19 -7
- package/extensions/orchestrator/async-agents.ts +91 -18
- package/extensions/orchestrator/utils.ts +16 -6
- package/package.json +1 -1
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
|
-
|
|
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=
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
929
|
-
|
|
930
|
-
|
|
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 (
|
|
948
|
+
log.debug(`jiti strategy 1 (running binary) failed: ${e?.message || e}`);
|
|
933
949
|
}
|
|
934
|
-
|
|
950
|
+
|
|
951
|
+
// Strategy 2: resolve the pi SDK package from this module's require context.
|
|
935
952
|
if (!jitiCliPath) {
|
|
936
953
|
try {
|
|
937
|
-
const
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
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 (
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
|