import { execFile, execSync, execFileSync } from "node:child_process"; import { promisify } from "node:util"; import type { NativeAgentObservation, NativeAgentStatus } from "./native-supervision.ts"; import type { CreatedHerdrWorktree, GitWorktreeSnapshot, ManagedSubagentWorktree, } from "./subagent-worktree.ts"; import { samePortablePath } from "./path-utils.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 { // execFile always pipes stdio; it has no `stdio` option to set. const { stdout } = await execFileAsync("herdr", args, { encoding: "utf8", }); 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 buildWorktreeCreateArgs(input: { cwd: string; branch: string; baseCommit: string; label: string; }): string[] { return [ "worktree", "create", "--cwd", input.cwd, "--branch", input.branch, "--base", input.baseCommit, "--label", input.label, "--no-focus", ]; } function extractHerdrWorktree(output: string): CreatedHerdrWorktree { const parsed = parseHerdrJson(output) as { result?: { workspace?: { workspace_id?: unknown }; root_pane?: { pane_id?: unknown }; worktree?: { path?: unknown }; }; } | null; const workspaceId = parsed?.result?.workspace?.workspace_id; const surface = parsed?.result?.root_pane?.pane_id; const path = parsed?.result?.worktree?.path; if ( typeof workspaceId !== "string" || !workspaceId || typeof surface !== "string" || !surface || typeof path !== "string" || !path ) { throw new Error(`Unexpected herdr worktree create output: ${output.trim() || "(empty)"}`); } return { workspaceId, surface, path }; } function runGit(args: string[]): string { return execFileSync("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], }); } function inspectGitWorktreeWith( cwd: string, executeGit: (args: string[]) => string, ): GitWorktreeSnapshot { let head: string; let status: string; try { head = executeGit(["-C", cwd, "rev-parse", "HEAD"]).trim(); status = executeGit(["-C", cwd, "status", "--porcelain=v1", "--untracked-files=all"]); } catch (error: any) { const detail = [error?.stderr, error?.message] .filter((value): value is string => typeof value === "string" && value.trim().length > 0) .join("\n"); throw new Error(`Unable to inspect Git worktree at ${JSON.stringify(cwd)}${detail ? `: ${detail}` : "."}`); } if (!head) throw new Error(`Git worktree at ${JSON.stringify(cwd)} has no HEAD commit.`); return { head, dirty: status.length > 0 }; } export function inspectGitWorktree(cwd: string): GitWorktreeSnapshot { return inspectGitWorktreeWith(cwd, runGit); } function createHerdrWorktreeWith( input: { cwd: string; branch: string; baseCommit: string; label: string; env?: Record; }, execute: (args: string[]) => string, ): CreatedHerdrWorktree { const created = extractHerdrWorktree(execute(buildWorktreeCreateArgs(input))); let surface: string; try { // worktree.create has no env parameter. Create a child tab in the linked // workspace so the operation protocol reaches Pi before it starts. surface = extractHerdrRootPaneId( execute(buildTabCreateArgs(input.label, created.path, created.workspaceId, input.env)), "tab create", ); } catch (error: any) { try { execute(["worktree", "remove", "--workspace", created.workspaceId]); } catch (cleanupError: any) { try { execute(["pane", "close", created.surface]); } catch { // Preserve the original startup error; the created path is included below. } const cleanupMessage = cleanupError?.message ? String(cleanupError.message) : String(cleanupError); const failure = new Error( `Unable to create the child pane in Herdr worktree ${JSON.stringify(created.path)}; worktree cleanup also failed: ${cleanupMessage}`, { cause: error }, ) as Error & { surface: string; worktree: ManagedSubagentWorktree }; failure.surface = created.surface; failure.worktree = { workspaceId: created.workspaceId, path: created.path, branch: input.branch, baseCommit: input.baseCommit, }; throw failure; } throw error; } try { execute(["pane", "close", created.surface]); } catch { // The extra shell pane is cosmetic; the operation-owned tab is ready. } try { execute(["pane", "rename", surface, input.label]); } catch { // Optional — pane label is cosmetic. } return { ...created, surface }; } export function createHerdrWorktree(input: { cwd: string; branch: string; baseCommit: string; label: string; env?: Record; }): CreatedHerdrWorktree { return createHerdrWorktreeWith(input, herdrExec); } export function removeHerdrWorktree(workspaceId: string): void { herdrExec(["worktree", "remove", "--workspace", workspaceId]); } /** One worktree as Herdr reports it for a source repository. */ export interface HerdrWorktreeEntry { path: string; branch: string; /** Herdr workspace hosting this checkout right now; null when none does. */ workspaceId: string | null; } export interface HerdrWorktreeListing { repoRoot: string; sourceCheckout: string; /** Branch checked out in the source checkout, when Herdr reports one. */ sourceBranch?: string; worktrees: HerdrWorktreeEntry[]; } function extractHerdrWorktreeListing(output: string): HerdrWorktreeListing { const parsed = parseHerdrJson(output) as { result?: { source?: { repo_root?: unknown; source_checkout_path?: unknown }; worktrees?: unknown; }; } | null; const repoRoot = parsed?.result?.source?.repo_root; const sourceCheckout = parsed?.result?.source?.source_checkout_path; const rawWorktrees = parsed?.result?.worktrees; if ( typeof repoRoot !== "string" || !repoRoot || typeof sourceCheckout !== "string" || !sourceCheckout || !Array.isArray(rawWorktrees) ) { throw new Error(`Unexpected herdr worktree list output: ${output.trim() || "(empty)"}`); } const worktrees: HerdrWorktreeEntry[] = []; for (const raw of rawWorktrees) { const path = (raw as { path?: unknown })?.path; const branch = (raw as { branch?: unknown })?.branch; const openWorkspace = (raw as { open_workspace_id?: unknown })?.open_workspace_id; // A bare worktree, or one Herdr reports without a branch, is not // addressable by the inventory; skip it rather than guess. if (typeof path !== "string" || !path || typeof branch !== "string" || !branch) continue; worktrees.push({ path, branch, workspaceId: typeof openWorkspace === "string" && openWorkspace ? openWorkspace : null, }); } if (worktrees.length === 0) { throw new Error( `Herdr worktree list reported no worktrees for the repository at ${JSON.stringify(sourceCheckout)}.`, ); } const sourceBranch = worktrees.find((entry) => samePortablePath(entry.path, sourceCheckout))?.branch; return { repoRoot, sourceCheckout, ...(sourceBranch ? { sourceBranch } : {}), worktrees, }; } /** * Herdr's worktree view for the repository containing `cwd`. Herdr scopes its * default to the focused workspace's repository, so the caller must always * name the path it means. */ export function listHerdrWorktrees(cwd: string): HerdrWorktreeListing { return extractHerdrWorktreeListing(herdrExec(["worktree", "list", "--cwd", cwd])); } /** One Herdr pane reduced to what the inventory needs as ownership evidence. */ export interface HerdrPaneEntry { paneId: string; workspaceId: string; /** Recognized agent in the pane, e.g. "pi"; absent behind a bare shell. */ agent?: string; agentStatus: NativeAgentStatus; } function extractHerdrPaneEntries(output: string): HerdrPaneEntry[] { const parsed = parseHerdrJson(output) as { result?: { panes?: unknown } } | null; const rawPanes = parsed?.result?.panes; if (!Array.isArray(rawPanes)) { throw new Error(`Unexpected herdr pane list output: ${output.trim() || "(empty)"}`); } const panes: HerdrPaneEntry[] = []; for (const raw of rawPanes) { const paneId = (raw as { pane_id?: unknown })?.pane_id; const workspaceId = (raw as { workspace_id?: unknown })?.workspace_id; if (typeof paneId !== "string" || !paneId || typeof workspaceId !== "string" || !workspaceId) { continue; } const agent = (raw as { agent?: unknown })?.agent; panes.push({ paneId, workspaceId, ...(typeof agent === "string" && agent ? { agent } : {}), agentStatus: normalizeAgentStatus((raw as { agent_status?: unknown })?.agent_status), }); } return panes; } /** Every Herdr pane; the inventory reads it once per call as ownership evidence. */ export function listHerdrPanes(): HerdrPaneEntry[] { return extractHerdrPaneEntries(herdrExec(["pane", "list"])); } function countWorktreeCommitsAheadWith( checkout: string, sourceBranch: string, executeGit: (args: string[]) => string, ): number { const output = executeGit([ "-C", checkout, "rev-list", "--count", `refs/heads/${sourceBranch}..HEAD`, ]); const count = Number.parseInt(output.trim(), 10); if (!Number.isSafeInteger(count) || count < 0) { throw new Error( `Unexpected git rev-list output for ${JSON.stringify(checkout)}: ${output.trim() || "(empty)"}`, ); } return count; } /** Commits the worktree at `checkout` adds on top of `sourceBranch`. */ export function countWorktreeCommitsAhead(checkout: string, sourceBranch: string): number { return countWorktreeCommitsAheadWith(checkout, sourceBranch, runGit); } 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, buildWorktreeCreateArgs, extractHerdrWorktree, createHerdrWorktreeWith, inspectGitWorktreeWith, extractHerdrWorktreeListing, extractHerdrPaneEntries, countWorktreeCommitsAheadWith, parseAgentGetOutput, parseAgentGetError, normalizeAgentStatus, isAgentPaneBusyError, retryAgentStart, };