Files
pi-goals/src/herdr.ts
T
wassname ddd552b1a5 Remember project model choices for planning worker and supervisor
Keep choices separate by role, ignore automatic restores, and fail when a remembered model is unavailable. Restore the worker model only after the planning fork is ready.
2026-09-08 16:49:46 +08:00

85 lines
2.9 KiB
TypeScript

import { execFile } from "node:child_process";
import { promisify } from "node:util";
const execFileAsync = promisify(execFile);
interface LaunchSupervisorInput {
cwd: string;
sourceSessionFile: string;
workerSessionId: string;
planPath: string;
approvalId: string;
extensionPath: string;
model: string | null;
}
function shellQuote(value: string): string {
return `'${value.replaceAll("'", "'\\''")}'`;
}
function findPaneId(value: unknown): string | null {
if (!value || typeof value !== "object") return null;
const record = value as Record<string, unknown>;
for (const key of ["pane_id", "paneId"]) {
if (typeof record[key] === "string") return record[key];
}
for (const child of Object.values(record)) {
const found = findPaneId(child);
if (found) return found;
}
return null;
}
async function herdr(args: string[], json = true): Promise<unknown> {
const bin = process.env.HERDR_BIN_PATH ?? "herdr";
const { stdout } = await execFileAsync(bin, args, { encoding: "utf8", timeout: 15_000 });
if (!json) return stdout.trim();
return stdout.trim() ? JSON.parse(stdout) : {};
}
function stalePaneError(error: unknown): boolean {
const record = error as { stdout?: unknown; stderr?: unknown; message?: unknown };
const text = [record.stdout, record.stderr, record.message].filter((value): value is string => typeof value === "string").join("\n");
return /\b(?:NOT_FOUND|PANE_GONE|PANE_NOT_FOUND)\b/i.test(text);
}
export function supervisorCommand(input: LaunchSupervisorInput): string {
const env = [
"PI_GOALS_ROLE=supervisor",
`PI_GOALS_MODEL_EXPLICIT=${input.model ? "1" : "0"}`,
`PI_GOALS_WORKER_ID=${input.workerSessionId}`,
`PI_GOALS_PLAN_PATH=${input.planPath}`,
`PI_GOALS_APPROVAL_ID=${input.approvalId}`,
`PI_GOALS_OWNER_SESSION_ID=${input.workerSessionId}`,
];
const args = [
"pi",
"--no-extensions",
"-e", input.extensionPath,
"--fork", input.sourceSessionFile,
"--name", `goals-supervisor-${input.workerSessionId.slice(0, 8)}`,
];
if (input.model) args.push("--model", input.model);
return `env ${[...env, ...args].map(shellQuote).join(" ")}`;
}
export async function openSupervisorPane(input: LaunchSupervisorInput, onOpened: (paneId: string) => void): Promise<string> {
if (process.env.HERDR_ENV !== "1") throw new Error("Ready needs a Herdr session so pi-goals can open the supervisor session.");
await herdr(["--version"], false);
const split = await herdr(["pane", "split", "--current", "--direction", "right", "--cwd", input.cwd, "--no-focus"]);
const paneId = findPaneId(split);
if (!paneId) throw new Error("Herdr did not return the new supervisor pane ID.");
onOpened(paneId);
await herdr(["pane", "run", paneId, supervisorCommand(input)]);
return paneId;
}
export async function closeSupervisorPane(paneId: string): Promise<void> {
try {
await herdr(["pane", "close", paneId]);
} catch (error) {
if (stalePaneError(error)) return;
throw error;
}
}