Prompt library is currently empty.
';
}
+ if (currentView === 'tournaments') {
+ const res = await fetch(`/api/tournaments?project=${encodeURIComponent(currentProject)}`);
+ const data = await res.json();
+ document.getElementById('tournament-history').innerHTML = data.map(t => {
+ let details = {};
+ try { details = JSON.parse(t.details_json); } catch(e) {}
+
+ let winnerHtml = `
+
+ Tournament ${t.id.substring(0,8)}...
+ ${winnerHtml}
+
+
Created: ${new Date(t.created_at).toLocaleString()}
+
+
BASELINE: ${escapeHtml(t.baseline_prompt)}
+
+
+
+
VARIANT A (Score: ${scoreA})
+
${escapeHtml(t.variant_a)}
+
+
+
VARIANT B (Score: ${scoreB})
+
${escapeHtml(t.variant_b)}
+
+
+
+ `;
+ }).join('') || 'No tournament history found.
';
+ }
+
if (currentView === 'health') {
const res = await fetch(`/api/health?project=${encodeURIComponent(currentProject)}`);
const health = await res.json();
diff --git a/universal-refiner/src/core/dashboard.ts b/universal-refiner/src/core/dashboard.ts
index e4a3102..52a0868 100644
--- a/universal-refiner/src/core/dashboard.ts
+++ b/universal-refiner/src/core/dashboard.ts
@@ -13,6 +13,8 @@ import { ConfigManager } from "./config.js";
import { TimelineProvider } from "../history/timeline.js";
import { EventStore } from "../history/event-store.js";
import { AutoPilotStatus } from "./autopilot-status.js";
+import { createABEvaluationRecord } from "../evaluation/prompt-evaluator.js";
+import { randomUUID } from "crypto";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -393,6 +395,69 @@ export class CommandCenterDashboard {
}
});
+ app.get("/api/tournaments", async (c) => {
+ const selectedPath = this.resolveSelectedPath(c.req.query("project"));
+ try {
+ const repoId = EventStore.getInstance().ensureRepository(selectedPath).id;
+ const tournaments = EventStore.getInstance().getTournaments(repoId);
+ return c.json(tournaments);
+ } catch (error) {
+ this.logRouteError("api/tournaments", error, selectedPath);
+ return c.json({ error: "Failed to fetch tournaments" }, 500);
+ }
+ });
+
+ app.post("/api/tournaments/run", async (c) => {
+ const selectedPath = this.resolveSelectedPath(c.req.query("project"));
+ try {
+ if (!isSameOriginRequest(c.req.header("origin"), c.req.url)) {
+ return c.json({ error: "Cross-origin tournament requests are not allowed" }, 403);
+ }
+ if (!isJsonContentType(c.req.header("content-type"))) {
+ return c.json({ error: "Tournament requests must use application/json" }, 415);
+ }
+
+ let body: { baseline?: unknown; variantA?: unknown; variantB?: unknown };
+ try {
+ body = await c.req.json() as { baseline?: unknown; variantA?: unknown; variantB?: unknown };
+ } catch {
+ return c.json({ error: "Tournament request body must be valid JSON" }, 400);
+ }
+
+ const { baseline, variantA, variantB } = body;
+ if (
+ typeof baseline !== "string" || baseline.trim().length === 0 ||
+ typeof variantA !== "string" || variantA.trim().length === 0 ||
+ typeof variantB !== "string" || variantB.trim().length === 0
+ ) {
+ return c.json({ error: "Tournament baseline, variantA, and variantB must be non-empty strings" }, 400);
+ }
+
+ const experiment = createABEvaluationRecord({
+ experimentId: `exp_${randomUUID()}`,
+ baselinePrompt: baseline,
+ variantA: { id: "A", prompt: variantA },
+ variantB: { id: "B", prompt: variantB }
+ });
+
+ const repoId = EventStore.getInstance().ensureRepository(selectedPath).id;
+ EventStore.getInstance().recordTournament({
+ id: experiment.experimentId,
+ repo_id: repoId,
+ baseline_prompt: baseline,
+ variant_a: variantA,
+ variant_b: variantB,
+ winner_observed: experiment.heuristicPreference,
+ details_json: JSON.stringify(experiment)
+ });
+
+ return c.json(experiment);
+ } catch (error) {
+ this.logRouteError("api/tournaments/run", error, selectedPath);
+ return c.json({ error: "Failed to run tournament" }, 500);
+ }
+ });
+
app.get("/api/events", async (c) => {
try {
return streamSSE(c, async (stream) => {
diff --git a/universal-refiner/src/history/event-store.ts b/universal-refiner/src/history/event-store.ts
index 90736c4..62bd4e8 100644
--- a/universal-refiner/src/history/event-store.ts
+++ b/universal-refiner/src/history/event-store.ts
@@ -403,6 +403,43 @@ export class EventStore {
);
}
+ recordTournament(tournament: {
+ id: string;
+ repo_id?: string | null;
+ baseline_prompt: string;
+ variant_a: string;
+ variant_b: string;
+ winner_observed: string;
+ details_json: string;
+ }) {
+ const now = new Date().toISOString();
+ const stmt = this.db.prepare(`
+ INSERT INTO tournaments (
+ id, repo_id, baseline_prompt, variant_a, variant_b, winner_observed, details_json, created_at
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+ `);
+ stmt.run(
+ tournament.id,
+ tournament.repo_id || null,
+ tournament.baseline_prompt,
+ tournament.variant_a,
+ tournament.variant_b,
+ tournament.winner_observed,
+ tournament.details_json,
+ now
+ );
+ }
+
+ getTournaments(repoId: string, limit = 50) {
+ const stmt = this.db.prepare(`
+ SELECT * FROM tournaments
+ WHERE repo_id = ? OR repo_id IS NULL
+ ORDER BY created_at DESC
+ LIMIT ?
+ `);
+ return stmt.all(repoId, limit);
+ }
+
recordTemplate(template: {
id: string;
repo_id: string;
diff --git a/universal-refiner/src/history/schema.ts b/universal-refiner/src/history/schema.ts
index 2d012b2..77d5d30 100644
--- a/universal-refiner/src/history/schema.ts
+++ b/universal-refiner/src/history/schema.ts
@@ -154,4 +154,15 @@ CREATE TABLE IF NOT EXISTS prompt_template_links (
commit_id TEXT,
lesson_id TEXT
);
+
+CREATE TABLE IF NOT EXISTS tournaments (
+ id TEXT PRIMARY KEY,
+ repo_id TEXT,
+ baseline_prompt TEXT NOT NULL,
+ variant_a TEXT NOT NULL,
+ variant_b TEXT NOT NULL,
+ winner_observed TEXT NOT NULL,
+ details_json TEXT NOT NULL DEFAULT '{}',
+ created_at TEXT NOT NULL
+);
`;
diff --git a/universal-refiner/tests/dashboard-api.test.ts b/universal-refiner/tests/dashboard-api.test.ts
index 4c16178..acfb81e 100644
--- a/universal-refiner/tests/dashboard-api.test.ts
+++ b/universal-refiner/tests/dashboard-api.test.ts
@@ -123,6 +123,49 @@ describe("dashboard review and health APIs", () => {
expect(invalidJson.status).toBe(400);
});
+ it("runs and persists prompt tournaments through same-origin JSON requests", async () => {
+ const app = CommandCenterDashboard.createApp(repoDir);
+ const response = await app.request("/api/tournaments/run", {
+ method: "POST",
+ headers: { "content-type": "application/json", origin: "http://localhost" },
+ body: JSON.stringify({
+ baseline: "Fix failing tests",
+ variantA: "Fix failing tests with regression coverage and verification",
+ variantB: "Fix tests",
+ }),
+ });
+
+ expect(response.status).toBe(200);
+ const experiment = await response.json() as any;
+ expect(experiment.experimentId).toMatch(/^exp_/);
+ expect(experiment.heuristicPreference).toBe("A");
+
+ const listResponse = await app.request("/api/tournaments");
+ const tournaments = await listResponse.json() as any[];
+ expect(listResponse.status).toBe(200);
+ expect(tournaments).toEqual([
+ expect.objectContaining({
+ id: experiment.experimentId,
+ baseline_prompt: "Fix failing tests",
+ winner_observed: "A",
+ }),
+ ]);
+ });
+
+ it("rejects unsafe or malformed tournament mutations", async () => {
+ const app = CommandCenterDashboard.createApp(repoDir);
+ const request = (body: string, headers: Record