/** * Typed client for the @void-nav/api orchestrator backend. * * The orchestrator turns the implementation board into an agentic system: each * run hands a kanban card to a `pi` subprocess (running inside an isolated git * worktree) and streams its progress back to the UI. The agent drives the board * and the documentation through the server's internal endpoints, so the board * stays fully interactive and the docs remain the central store of truth. * * Keep these types in sync with `apps/api/src/types.ts`. */ export type RunStatus = 'queued' | 'running' | 'completed' | 'failed' | 'stopped'; export interface AgentRun { id: string; cardId: string; status: RunStatus; useWorktree: boolean; /** Whether this run created (and therefore owns/cleans up) its worktree. */ ownsWorktree: boolean; branch: string | null; worktreePath: string | null; /** pi session JSONL path persisted for this run (resumable by refinements). */ sessionFile: string | null; prompt: string; summary: string | null; commitSha: string | null; error: string | null; createdAt: string; startedAt: string | null; finishedAt: string | null; } export type RunEventType = | 'status' | 'text' | 'tool_start' | 'tool_end' | 'log' | 'bevy' | 'done' | 'error'; export interface RunEvent { seq?: number; type: RunEventType; data: Record; createdAt?: string; } const BASE = '/api/orchestrator'; class ApiError extends Error { constructor(message: string, readonly status: number) { super(message); this.name = 'ApiError'; } } async function req(path: string, init?: RequestInit): Promise { const res = await fetch(`${BASE}${path}`, { ...init, headers: { 'Content-Type': 'application/json', ...(init?.headers ?? {}) }, }); if (!res.ok) { let detail = `${res.status} ${res.statusText}`; try { const body = await res.json(); if (body?.error) detail = body.error; } catch { /* keep status text */ } throw new ApiError(detail, res.status); } if (res.status === 204) return undefined as T; return (await res.json()) as T; } export interface StartRunInput { cardId: string; prompt?: string; useWorktree?: boolean; cleanupOnFinish?: boolean; /** * If set, start a refinement run that continues this prior run's branch with * `prompt` as the operator's refinement feedback. */ refineRunId?: string; } /** A commit on a run's branch that is not yet on main. */ export interface DiffCommit { sha: string; subject: string; } /** A run's branch diff vs main. */ export interface DiffResult { branch: string; commits: DiffCommit[]; stat: string; patch: string; truncated: boolean; } /** Outcome of merging a run's branch into the main worktree. */ export interface MergeResult { ok: boolean; alreadyMerged: boolean; target: string; branch: string; output: string; } /** One branch's outcome in a batch (merge-all) merge. */ export interface BatchMergeItem extends MergeResult { /** The run this branch belonged to. */ runId: string; } /** Result of merging several branches sequentially into the main branch. */ export interface BatchMergeResult { target: string; /** One entry per candidate branch, in the order processed. */ items: BatchMergeItem[]; /** The branch that conflicted and stopped the batch, if any. */ haltedOn: string | null; /** True if the main worktree was dirty and nothing was attempted. */ aborted: boolean; /** Human-readable reason when aborted or halted. */ reason: string | null; } export const orchestratorApi = { listRuns: (cardId?: string) => req<{ runs: AgentRun[] }>(`/runs${cardId ? `?cardId=${encodeURIComponent(cardId)}` : ''}`), getRun: (id: string, since = 0) => req<{ run: AgentRun; events: RunEvent[] }>(`/runs/${id}?since=${since}`), startRun: (input: StartRunInput) => req<{ run: AgentRun }>(`/runs`, { method: 'POST', body: JSON.stringify(input) }), messageRun: (id: string, text: string, mode: 'steer' | 'followUp') => req<{ ok: boolean }>(`/runs/${id}/message`, { method: 'POST', body: JSON.stringify({ text, mode }), }), stopRun: (id: string) => req<{ ok: boolean }>(`/runs/${id}/stop`, { method: 'POST' }), deleteRun: (id: string) => req(`/runs/${id}`, { method: 'DELETE' }), // --- worktree review & playtesting --- getDiff: (id: string) => req(`/runs/${id}/diff`), mergeRun: (id: string) => req(`/runs/${id}/merge`, { method: 'POST' }), /** Merge all reviewable agent branches into the main branch sequentially. */ mergeAll: () => req('/runs/merge-all', { method: 'POST' }), bevyStatus: (id: string) => req<{ running: boolean }>(`/runs/${id}/bevy`), startBevy: (id: string) => req<{ ok: boolean }>(`/runs/${id}/bevy`, { method: 'POST' }), stopBevy: (id: string) => req<{ ok: boolean }>(`/runs/${id}/bevy/stop`, { method: 'POST' }), };