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:
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user