import { execFile, execSync, execFileSync } from "node:child_process"; import { promisify } from "node:util"; import type { NativeAgentObservation, NativeAgentStatus } from "./native-supervision.ts"; const execFileAsync = promisify(execFile); const commandAvailability = new Map(); function hasCommand(command: string): boolean { if (commandAvailability.has(command)) { return commandAvailability.get(command)!; } let available = false; if (process.platform === "win32") { try { execFileSync("where.exe", [command], { stdio: "ignore" }); available = true; } catch { try { execSync(`command -v ${command}`, { stdio: "ignore" }); available = true; } catch { available = false; } } } else { try { execSync(`command -v ${command}`, { stdio: "ignore" }); available = true; } catch { available = false; } } commandAvailability.set(command, available); return available; } export function isHerdrAvailable(): boolean { return process.env.HERDR_ENV === "1" && hasCommand("herdr"); } const SETUP_HINT = "Start pi inside herdr (`herdr`, then run `pi`)."; export function herdrSetupHint(): string { return SETUP_HINT; } function parseHerdrJson(value: string): unknown { try { return JSON.parse(value); } catch { return null; } } function extractHerdrRootPaneId(output: string, context: string): string { const parsed = parseHerdrJson(output); const paneId = (parsed as { result?: { root_pane?: { pane_id?: unknown } } })?.result?.root_pane ?.pane_id; if (typeof paneId !== "string" || !paneId) { throw new Error(`Unexpected herdr ${context} output: ${output.trim() || "(empty)"}`); } return paneId; } function herdrExec(args: string[]): string { // Keep Herdr diagnostics in the thrown error object. The default sync // stdio mode forwards child stderr directly to Pi's terminal, which leaks // raw Herdr JSON before the caller can classify or render the failure. return execFileSync("herdr", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], }); } async function herdrExecAsync(args: string[]): Promise { const { stdout } = await execFileAsync("herdr", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], }); return stdout; } const AGENT_START_RETRY_WINDOW_MS = 5_000; const AGENT_START_RETRY_INITIAL_DELAY_MS = 25; const AGENT_START_RETRY_MAX_DELAY_MS = 500; type AgentStartRetryOptions = { sleep?: (milliseconds: number) => void; now?: () => number; }; function sleepSync(milliseconds: number): void { if (milliseconds <= 0) return; const signal = new Int32Array(new SharedArrayBuffer(4)); Atomics.wait(signal, 0, 0, milliseconds); } function isAgentPaneBusyError(error: unknown): boolean { if (error == null || typeof error !== "object") return false; const candidate = error as { message?: unknown; stderr?: unknown; stdout?: unknown; }; const text = [candidate.message, candidate.stderr, candidate.stdout] .filter((value): value is string => typeof value === "string") .join("\n"); return /\bagent_pane_busy\b/.test(text) || /not an available shell/.test(text); } /** * A freshly-created shell can take a short time to become claimable by * Herdr's native agent transport. Retry only that explicit transient result; * all other startup failures retain their original synchronous semantics. */ function retryAgentStart( start: () => void, timeoutMs: number, options: AgentStartRetryOptions = {}, ): void { const now = options.now ?? Date.now; const sleep = options.sleep ?? sleepSync; const deadline = now() + Math.min(Math.max(timeoutMs, 0), AGENT_START_RETRY_WINDOW_MS); let delay = AGENT_START_RETRY_INITIAL_DELAY_MS; while (true) { try { start(); return; } catch (error) { if (!isAgentPaneBusyError(error)) throw error; const remaining = deadline - now(); if (remaining <= 0) throw error; sleep(Math.min(delay, remaining)); delay = Math.min(delay * 2, AGENT_START_RETRY_MAX_DELAY_MS); } } } function getHerdrCurrentPaneInfo(): { pane_id: string; tab_id: string; workspace_id: string; } { const paneId = process.env.HERDR_PANE_ID; const tabId = process.env.HERDR_TAB_ID; const workspaceId = process.env.HERDR_WORKSPACE_ID; // Fall back to `herdr pane current` if any identity env var is missing — // older herdr versions may not set all three. if (!paneId || !tabId || !workspaceId) { const output = herdrExec(["pane", "current"]); const parsed = parseHerdrJson(output); const pane = (parsed as { result?: { pane?: unknown } } | null)?.result?.pane as | { pane_id?: string; tab_id?: string; workspace_id?: string } | undefined; if (!pane?.pane_id || !pane?.tab_id || !pane?.workspace_id) { throw new Error(`Unexpected herdr pane current output: ${output.trim() || "(empty)"}`); } return { pane_id: pane.pane_id, tab_id: pane.tab_id, workspace_id: pane.workspace_id, }; } return { pane_id: paneId, tab_id: tabId, workspace_id: workspaceId }; } function buildTabCreateArgs( name: string, cwd: string, workspaceId: string, env: Record = {}, ): string[] { const args = [ "tab", "create", "--workspace", workspaceId, "--label", name, "--cwd", cwd, ]; for (const [key, value] of Object.entries(env).sort(([a], [b]) => a.localeCompare(b))) { args.push("--env", `${key}=${value}`); } args.push("--no-focus"); return args; } export function createHerdrSurface( name: string, options: { cwd?: string; env?: Record } = {}, ): string { // Create a new tab per subagent so parallel spawns each get a full tab // instead of ever-narrower splits of the parent pane. Target the current // workspace explicitly because Herdr's implicit default may be another space. const { workspace_id: workspaceId } = getHerdrCurrentPaneInfo(); const output = herdrExec( buildTabCreateArgs(name, options.cwd ?? process.cwd(), workspaceId, options.env), ); const paneId = extractHerdrRootPaneId(output, "tab create"); try { herdrExec(["pane", "rename", paneId, name]); } catch { // Optional — pane label is cosmetic. } return paneId; } export function startHerdrAgent( name: string, pane: string, args: string[], timeoutMs: number, ): void { const command = [ "agent", "start", name, "--kind", "pi", "--pane", pane, "--timeout", String(timeoutMs), "--", ...args, ]; retryAgentStart(() => herdrExec(command), timeoutMs); } export function promptHerdrAgent(target: string, text: string): void { herdrExec(["agent", "prompt", target, text]); } type AgentInspectionResult = | { kind: "present"; agent?: string; agentStatus: "idle" | "working" | "blocked" | "done" | "unknown" } | { kind: "missing"; error?: string } | { kind: "unavailable"; error: string }; function normalizeAgentStatus(value: unknown): NativeAgentStatus { return value === "idle" || value === "working" || value === "blocked" || value === "done" ? value : "unknown"; } function parseAgentGetOutput(output: string, surface: string): AgentInspectionResult { const parsed = parseHerdrJson(output) as | { result?: { agent?: unknown }; error?: { code?: unknown; message?: unknown } } | null; const errorObj = parsed?.error; if (errorObj?.code === "agent_not_found" || errorObj?.code === "pane_not_found" || errorObj?.code === "not_found") { return { kind: "missing", error: typeof errorObj.message === "string" ? errorObj.message : "agent not found" }; } const agentRecord = parsed?.result?.agent; if (!agentRecord || typeof agentRecord !== "object") { return { kind: "unavailable", error: "agent get returned no agent record" }; } const record = agentRecord as { pane_id?: unknown; agent?: unknown; agent_status?: unknown }; if (record.pane_id !== surface) return { kind: "unavailable", error: "agent pane id mismatch" }; const agent = typeof record.agent === "string" ? record.agent : undefined; return { kind: "present", ...(agent ? { agent } : {}), agentStatus: normalizeAgentStatus(record.agent_status) }; } function parseAgentGetError(error: any): AgentInspectionResult { for (const raw of [error?.stderr, error?.stdout]) { if (typeof raw !== "string" || !raw.trim()) continue; try { const parsed = parseAgentGetOutput(raw, ""); if (parsed.kind === "missing") return parsed; } catch { // A CLI may emit plain diagnostics on one stream and structured JSON on // the other. Parse each stream independently before giving up. } if (/\b(?:agent_not_found|pane_not_found|not_found)\b/.test(raw)) { return { kind: "missing", error: raw.trim() }; } } const message = error?.message ? String(error.message) : "herdr agent get failed"; return { kind: "unavailable", error: message }; } /** * Structured native agent query. * - present: the recognized agent is reachable * - missing: the agent is gone * - unavailable: the server/query failed; caller should keep polling */ export async function inspectHerdrAgent(surface: string): Promise { try { const result = parseAgentGetOutput(await herdrExecAsync(["agent", "get", surface]), surface); // Stamp the observation time at query time, closest to the transport. return result.kind === "present" ? { ...result, observedAt: Date.now() } : result; } catch (error: any) { return parseAgentGetError(error); } } export function sendHerdrAgentEscape(surface: string): void { herdrExec(["agent", "send-keys", surface, "esc"]); } export function closeHerdrSurface(surface: string): void { herdrExec(["pane", "close", surface]); } export const __herdrTest__ = { buildTabCreateArgs, parseHerdrJson, extractHerdrRootPaneId, parseAgentGetOutput, parseAgentGetError, normalizeAgentStatus, isAgentPaneBusyError, retryAgentStart, };