feat(api): add bevy playtest, worktree review, and drop seeding

- bevy.ts: spawn `cargo run` in a run's worktree to playtest its branch,
  batching build/runtime output as `bevy` events (capped at 2000/run)
- events.ts: shared appendRunEvent/nextSeq so the runner, the agent-driven
  internal mutations, and the bevy launcher all persist through one path
- runs.ts: track per-run bevy processes; stop them before worktree teardown
- worktrees.ts: review/merge ops (isWorktreePresent, mainDirtySummary,
  commitsAheadOfHead, diffPatch/stat, mergeBranch)
- orchestrator routes: diff, merge, bevy start/stop, bevy-status endpoints
- remove seeding: drop seed.ts + data/kanbanCards.ts, the /reset endpoint,
  and boot-time seeding; cards are plain persisted DB records now
This commit is contained in:
2026-06-16 18:17:27 -04:00
parent aee13cb81a
commit e4f0abed20
12 changed files with 422 additions and 646 deletions

View File

@@ -10,10 +10,12 @@
import { randomUUID } from 'node:crypto';
import { db, type AgentRunRow } from '../db.js';
import type { AgentRun, AgentRunEvent, Card, RunStatus } from '../types.js';
import { appendRunEvent } from './events.js';
import { newToken, REPO_ROOT } from './config.js';
import { buildPrompt } from './prompt.js';
import { Runner, type PersistedEvent } from './runner.js';
import { createWorktree, dirtySummary, headSha, removeWorktree, type Worktree } from './worktrees.js';
import { BevyProcess } from './bevy.js';
import { createWorktree, dirtySummary, headSha, isWorktreePresent, removeWorktree, type Worktree } from './worktrees.js';
export interface StartRunInput {
cardId: string;
@@ -26,8 +28,10 @@ export interface StartRunInput {
}
class RunManager {
/** Active runners keyed by run id. */
/** Active agent runners keyed by run id. */
private active = new Map<string, { runner: Runner; worktree: Worktree | null; cleanup: boolean }>();
/** Active Bevy test processes keyed by run id (independent of agent runs). */
private bevy = new Map<string, BevyProcess>();
/** Create and start a run for a card. Returns the persisted run. */
start(card: Card, input: StartRunInput): AgentRun {
@@ -103,12 +107,43 @@ class RunManager {
remove(id: string): void {
const entry = this.active.get(id);
if (entry) throw new Error('cannot delete an active run; stop it first');
// Stop any live Bevy test before tearing down its worktree.
this.bevy.get(id)?.stop();
this.bevy.delete(id);
const row = this.get(id);
if (row?.worktreePath && row.branch) removeWorktree(id, row.branch);
db.prepare('DELETE FROM agent_run_events WHERE run_id = ?').run(id);
db.prepare('DELETE FROM agent_runs WHERE id = ?').run(id);
}
// --- Bevy playtesting ----------------------------------------------------
/** Whether a Bevy test is currently running for a run. */
bevyRunning(id: string): boolean {
return this.bevy.get(id)?.running ?? false;
}
/** Spawn `cargo run` in a run's worktree to playtest its branch. */
startBevy(id: string): void {
const run = this.get(id);
if (!run) throw new RunNotFoundError(id);
if (!run.worktreePath || !isWorktreePresent(run.worktreePath)) {
throw new Error('run has no worktree to run Bevy in');
}
let bp = this.bevy.get(id);
if (bp?.running) throw new Error('Bevy is already running for this run');
if (!bp) {
bp = new BevyProcess(id);
this.bevy.set(id, bp);
}
bp.start(run.worktreePath);
}
/** Stop a run's Bevy test (best-effort). */
stopBevy(id: string): void {
this.bevy.get(id)?.stop();
}
// --- reads ---------------------------------------------------------------
get(id: string): AgentRun | undefined {
@@ -167,18 +202,9 @@ class RunManager {
commitSha = headSha(entry.worktree.path);
const dirty = dirtySummary(entry.worktree.path);
if (dirty) {
// Uncommitted changes left behind — surface as an error note.
// Uncommitted changes left behind — surface as an error note + event.
extraError = `worktree has uncommitted changes:\n${dirty}`;
// Persist it as a run event so it shows in the feed.
db.prepare(
`INSERT INTO agent_run_events (run_id, seq, type, data, created_at)
VALUES (?, ?, 'log', ?, ?)`,
).run(
id,
nextSeq(id),
JSON.stringify({ level: 'warn', text: extraError }),
now,
);
appendRunEvent(id, 'log', { level: 'warn', text: extraError });
}
}
@@ -206,8 +232,7 @@ export class RunNotFoundError extends Error {
// --- helpers ---------------------------------------------------------------
function hydrateRun(row: AgentRunRow): AgentRun {
return {
function hydrateRun(row: AgentRunRow): AgentRun { return {
id: row.id,
cardId: row.card_id,
status: row.status as RunStatus,
@@ -224,14 +249,6 @@ function hydrateRun(row: AgentRunRow): AgentRun {
};
}
function nextSeq(runId: string): number {
return (
db
.prepare('SELECT COALESCE(MAX(seq), 0) + 1 AS n FROM agent_run_events WHERE run_id = ?')
.get(runId) as { n: number }
).n;
}
function safeParse(s: string): Record<string, unknown> {
try {
return JSON.parse(s) as Record<string, unknown>;