Files
Space-Game/apps/docs/src/lib/orchestratorApi.ts
francy51 d538ccdd4e feat(kanban): merge-all branches + floating running-agents dock
Two board-level additions:

Merge all reviewable agent branches into the main branch sequentially:
- worktrees.mergeBranches() merges candidates in order, advancing HEAD each
  time; refuses if the main tree is dirty, and on the first conflict aborts
  that merge (main left clean) and stops — prior merges stay, untried
  branches reported as unattempted.
- runManager.mergeAll() picks candidates (settled runs that own their
  worktree, branch present, not yet in HEAD; deduped, oldest-first) — so
  refinement runs (which inherit a worktree) are correctly excluded.
- POST /runs/merge-all; ⬇ Merge all button in the board header opens a
  results modal (per-branch ✓/⇢/⚠, conflict banner, inline git output).

Floating dock to view/open running agents (front-end only):
- RunningAgentsBar pins bottom-right (below the card modal), lists every
  running run with live elapsed times, and opens that card's modal on click;
  auto-hides when nothing runs, collapsible otherwise.
EOF && echo "" && git log --oneline -4
2026-06-17 21:04:51 -04:00

173 lines
4.9 KiB
TypeScript

/**
* 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<string, unknown>;
createdAt?: string;
}
const BASE = '/api/orchestrator';
class ApiError extends Error {
constructor(message: string, readonly status: number) {
super(message);
this.name = 'ApiError';
}
}
async function req<T>(path: string, init?: RequestInit): Promise<T> {
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<void>(`/runs/${id}`, { method: 'DELETE' }),
// --- worktree review & playtesting ---
getDiff: (id: string) => req<DiffResult>(`/runs/${id}/diff`),
mergeRun: (id: string) =>
req<MergeResult>(`/runs/${id}/merge`, { method: 'POST' }),
/** Merge all reviewable agent branches into the main branch sequentially. */
mergeAll: () => req<BatchMergeResult>('/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' }),
};