pi-orchestrator-config 4.3.4 → 4.3.5
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
|
-
|
|
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,15 +919,31 @@ 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
924
|
// Find jiti for TypeScript execution
|
|
925
925
|
let jitiCliPath: string | undefined;
|
|
926
926
|
// Strategy 1: resolve from pi's own require context
|
|
927
|
+
// NOTE: newer pi SDK "exports" maps hide ./package.json — fall back to
|
|
928
|
+
// resolving the main entry and walking up to its package root.
|
|
927
929
|
try {
|
|
928
|
-
const
|
|
929
|
-
|
|
930
|
-
|
|
930
|
+
const req = createRequire(import.meta.url);
|
|
931
|
+
let piPkgJsonPath: string | undefined;
|
|
932
|
+
try {
|
|
933
|
+
piPkgJsonPath = req.resolve("@earendil-works/pi-coding-agent/package.json");
|
|
934
|
+
} catch {
|
|
935
|
+
const entry = req.resolve("@earendil-works/pi-coding-agent");
|
|
936
|
+
let dir = path.dirname(entry);
|
|
937
|
+
while (dir !== path.dirname(dir)) {
|
|
938
|
+
const pj = path.join(dir, "package.json");
|
|
939
|
+
if (fs.existsSync(pj) && JSON.parse(fs.readFileSync(pj, "utf8"))?.name === "@earendil-works/pi-coding-agent") { piPkgJsonPath = pj; break; }
|
|
940
|
+
dir = path.dirname(dir);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
if (piPkgJsonPath) {
|
|
944
|
+
const candidate = path.join(path.dirname(piPkgJsonPath), "node_modules/jiti/lib/jiti-cli.mjs");
|
|
945
|
+
if (fs.existsSync(candidate)) jitiCliPath = candidate;
|
|
946
|
+
}
|
|
931
947
|
} catch (e: any) {
|
|
932
948
|
log.debug(`jiti strategy 1 (require.resolve) failed: ${e?.message || e}`);
|
|
933
949
|
}
|
|
@@ -947,6 +963,28 @@ export function registerAsyncAgents(
|
|
|
947
963
|
}
|
|
948
964
|
}
|
|
949
965
|
|
|
966
|
+
// Strategy 3: this package's own node_modules — git/stow installs ship
|
|
967
|
+
// jiti themselves (npm consumers are covered by strategies 1-2).
|
|
968
|
+
if (!jitiCliPath) {
|
|
969
|
+
try {
|
|
970
|
+
const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
971
|
+
const candidate = path.join(pkgRoot, "node_modules/jiti/lib/jiti-cli.mjs");
|
|
972
|
+
if (fs.existsSync(candidate)) jitiCliPath = candidate;
|
|
973
|
+
} catch (e: any) {
|
|
974
|
+
log.debug(`jiti strategy 3 (package root) failed: ${e?.message || e}`);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// Fail fast with a self-explanatory error instead of spawning a child that
|
|
979
|
+
// cannot load TypeScript (bare node dies on ".js"→".ts" specifiers).
|
|
980
|
+
if (!jitiCliPath) {
|
|
981
|
+
log.error(`async-spawn: ${id} — no jiti-cli.mjs found (strategies 1-3 failed); refusing to spawn`);
|
|
982
|
+
// Clean up the artifacts we already created — nothing will consume them.
|
|
983
|
+
try { fs.rmSync(configPath, { force: true }); } catch {}
|
|
984
|
+
try { fs.rmSync(workerDir, { recursive: true, force: true }); } catch {}
|
|
985
|
+
return { id, error: "jiti-cli.mjs not found — cannot execute async agent (see orchestrator logs)" };
|
|
986
|
+
}
|
|
987
|
+
|
|
950
988
|
const spawnArgs = jitiCliPath
|
|
951
989
|
? [jitiCliPath, runnerPath, configPath]
|
|
952
990
|
: [runnerPath, configPath];
|
|
@@ -957,6 +995,10 @@ export function registerAsyncAgents(
|
|
|
957
995
|
PI_SUBAGENT_CHILD: "1",
|
|
958
996
|
PI_AGENT_NAME: agentName,
|
|
959
997
|
PI_PRIMARY_MODEL: process.env.PI_PRIMARY_MODEL || process.env.PI_MODEL || "",
|
|
998
|
+
// Isolate children from the shared jiti FS cache — other sessions running
|
|
999
|
+
// different node/jiti versions can poison it and kill children at boot.
|
|
1000
|
+
JITI_CACHE: "false",
|
|
1001
|
+
JITI_FS_CACHE: "false",
|
|
960
1002
|
__PI_CONFIG_SESSION_ID: (globalThis as any).__piConfigSessionId || "",
|
|
961
1003
|
__PI_PARENT_SESSION_ID: (globalThis as any).__piConfigSessionId || "",
|
|
962
1004
|
};
|
|
@@ -1004,14 +1046,21 @@ export function registerAsyncAgents(
|
|
|
1004
1046
|
};
|
|
1005
1047
|
asyncState.jobs.set(id, job);
|
|
1006
1048
|
|
|
1049
|
+
const stderrLogPath = path.join(workerDir, "child-stderr.log");
|
|
1050
|
+
const stderrLog = fs.createWriteStream(stderrLogPath, { flags: "w" });
|
|
1051
|
+
stderrLog.on("error", () => {}); // never let a log write failure crash the session
|
|
1052
|
+
log.debug(`async-spawn: ${job.id} stderr → ${stderrLogPath}`);
|
|
1053
|
+
|
|
1007
1054
|
const proc = spawn(process.execPath, spawnArgs, {
|
|
1008
1055
|
cwd,
|
|
1009
|
-
stdio: "ignore",
|
|
1056
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
1010
1057
|
windowsHide: true,
|
|
1011
1058
|
env: spawnEnv,
|
|
1012
1059
|
});
|
|
1060
|
+
proc.stderr?.pipe(stderrLog);
|
|
1013
1061
|
|
|
1014
1062
|
proc.once("error", (err) => {
|
|
1063
|
+
try { stderrLog.destroy(); } catch {}
|
|
1015
1064
|
log.info(`spawn-error: ${job.id} — ${err.message}`);
|
|
1016
1065
|
if (job.status === "complete" || job.status === "failed") return;
|
|
1017
1066
|
job.status = "failed";
|
|
@@ -1037,8 +1086,27 @@ export function registerAsyncAgents(
|
|
|
1037
1086
|
setTimeout(() => {
|
|
1038
1087
|
if (job.status === "complete" || job.status === "failed") return;
|
|
1039
1088
|
log.info(`spawn-exit: ${job.id} — code=${code} signal=${signal}`);
|
|
1089
|
+
let stderrTail = "";
|
|
1090
|
+
try {
|
|
1091
|
+
if (fs.existsSync(stderrLogPath)) {
|
|
1092
|
+
// Tail-read only — the log can be arbitrarily large on noisy failures.
|
|
1093
|
+
const size = fs.statSync(stderrLogPath).size;
|
|
1094
|
+
const start = Math.max(0, size - 4096);
|
|
1095
|
+
let raw = "";
|
|
1096
|
+
if (size > 0) {
|
|
1097
|
+
const fd = fs.openSync(stderrLogPath, "r");
|
|
1098
|
+
try {
|
|
1099
|
+
const buf = Buffer.alloc(Math.min(size - start, 4096));
|
|
1100
|
+
fs.readSync(fd, buf, 0, buf.length, start);
|
|
1101
|
+
raw = buf.toString("utf8");
|
|
1102
|
+
} finally { fs.closeSync(fd); }
|
|
1103
|
+
}
|
|
1104
|
+
stderrTail = raw.trim().slice(-1200);
|
|
1105
|
+
}
|
|
1106
|
+
} catch {}
|
|
1107
|
+
log.warn(`async-child failed: ${job.id} code=${code}${stderrTail ? " (see child-stderr.log)" : " (no stderr captured)"}`);
|
|
1040
1108
|
job.status = "failed";
|
|
1041
|
-
job.output = `Process exited with code ${code} signal ${signal}`;
|
|
1109
|
+
job.output = `Process exited with code ${code} signal ${signal}${stderrTail ? `\n--- child stderr (tail) ---\n${stderrTail}` : ""}`;
|
|
1042
1110
|
job.durationMs = Date.now() - job.startedAt;
|
|
1043
1111
|
job.updatedAt = Date.now();
|
|
1044
1112
|
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
|
|