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

@@ -13,6 +13,14 @@ import { db } from '../db.js';
import type { Card } from '../types.js';
import { hydrateCard } from './kanban.js';
import { RunNotFoundError, runManager } from '../orchestrator/runs.js';
import {
commitsAheadOfHead,
diffPatch,
diffStat,
mainDirtySummary,
isWorktreePresent,
mergeBranch,
} from '../orchestrator/worktrees.js';
export const orchestrator = new Hono();
@@ -101,6 +109,65 @@ orchestrator.delete('/runs/:id', (c) => {
}
});
// --- worktree review & playtesting ----------------------------------------
/** Diff of a run's branch vs main (commits, --stat, and the capped patch). */
orchestrator.get('/runs/:id/diff', (c) => {
const run = runManager.get(c.req.param('id'));
if (!run) return c.json({ error: 'run not found' }, 404);
if (!run.branch || !run.worktreePath || !isWorktreePresent(run.worktreePath)) {
return c.json({ error: 'run has no worktree to diff' }, 409);
}
const { patch, truncated } = diffPatch(run.branch);
return c.json({
branch: run.branch,
commits: commitsAheadOfHead(run.branch),
stat: diffStat(run.branch),
patch,
truncated,
});
});
/** Merge a run's branch into the main worktree's checked-out branch. */
orchestrator.post('/runs/:id/merge', (c) => {
const run = runManager.get(c.req.param('id'));
if (!run) return c.json({ error: 'run not found' }, 404);
if (!run.branch) return c.json({ error: 'run has no branch to merge' }, 409);
// Refuse if the main worktree is dirty — merging would be unsafe.
const dirty = mainDirtySummary();
if (dirty) return c.json({ error: `Cannot merge: ${dirty}` }, 409);
// The merge outcome (success or conflict-aborted) is in the body; only hard
// preconditions (above) throw HTTP errors so the UI can read `ok` directly.
return c.json(mergeBranch(run.branch));
});
/** Whether a Bevy playtest is running for a run. */
orchestrator.get('/runs/:id/bevy', (c) => {
const run = runManager.get(c.req.param('id'));
if (!run) return c.json({ error: 'run not found' }, 404);
return c.json({ running: runManager.bevyRunning(run.id) });
});
/** Launch a Bevy playtest (`cargo run`) in a run's worktree. */
orchestrator.post('/runs/:id/bevy', (c) => {
const id = c.req.param('id');
try {
runManager.startBevy(id);
return c.json({ ok: true });
} catch (err) {
if (err instanceof RunNotFoundError) return c.json({ error: 'run not found' }, 404);
const message = err instanceof Error ? err.message : 'failed to start Bevy';
const status = message.includes('no worktree') || message.includes('already running') ? 409 : 500;
return c.json({ error: message }, status);
}
});
/** Stop a run's Bevy playtest. */
orchestrator.post('/runs/:id/bevy/stop', (c) => {
runManager.stopBevy(c.req.param('id'));
return c.json({ ok: true });
});
/** Live event stream for a run (Server-Sent Events). */
orchestrator.get('/runs/:id/stream', (c) => {
const id = c.req.param('id');