agentv 4.3.4 → 4.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,27 +24,32 @@ import {
24
24
  validateFileReferences,
25
25
  validateTargetsFile,
26
26
  writeArtifactsFromResults
27
- } from "./chunk-WICUFOIA.js";
27
+ } from "./chunk-VYZQMN57.js";
28
28
  import {
29
29
  DEFAULT_CATEGORY,
30
30
  PASS_THRESHOLD,
31
+ addProject,
31
32
  createBuiltinRegistry,
32
33
  deriveCategory,
34
+ discoverProjects,
33
35
  executeScript,
34
36
  getAgentvHome,
35
37
  getOutputFilenames,
38
+ getProject,
36
39
  getWorkspacePoolRoot,
37
40
  isAgentSkillsFormat,
41
+ loadProjectRegistry,
38
42
  loadTestById,
39
43
  loadTestSuite,
40
44
  loadTests,
41
45
  normalizeLineEndings,
42
46
  parseAgentSkillsEvals,
47
+ removeProject,
43
48
  toCamelCaseDeep,
44
49
  toSnakeCaseDeep as toSnakeCaseDeep2,
45
50
  transpileEvalYamlFile,
46
51
  trimBaselineResult
47
- } from "./chunk-HMOXP7T5.js";
52
+ } from "./chunk-63NDZ6UC.js";
48
53
  import {
49
54
  __commonJS,
50
55
  __esm,
@@ -4218,7 +4223,7 @@ var evalRunCommand = command({
4218
4223
  },
4219
4224
  handler: async (args) => {
4220
4225
  if (args.evalPaths.length === 0 && process.stdin.isTTY) {
4221
- const { launchInteractiveWizard } = await import("./interactive-GVBU4GSC.js");
4226
+ const { launchInteractiveWizard } = await import("./interactive-VJP2AEPT.js");
4222
4227
  await launchInteractiveWizard();
4223
4228
  return;
4224
4229
  }
@@ -6309,11 +6314,366 @@ function writeFeedback(cwd, data) {
6309
6314
  writeFileSync4(feedbackPath(cwd), `${JSON.stringify(data, null, 2)}
6310
6315
  `, "utf8");
6311
6316
  }
6317
+ function buildFileTree(dirPath, relativeTo) {
6318
+ if (!existsSync8(dirPath) || !statSync4(dirPath).isDirectory()) {
6319
+ return [];
6320
+ }
6321
+ const entries2 = readdirSync3(dirPath, { withFileTypes: true });
6322
+ return entries2.sort((a, b) => {
6323
+ if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
6324
+ return a.name.localeCompare(b.name);
6325
+ }).map((entry) => {
6326
+ const fullPath = path10.join(dirPath, entry.name);
6327
+ const relPath = path10.relative(relativeTo, fullPath);
6328
+ if (entry.isDirectory()) {
6329
+ return {
6330
+ name: entry.name,
6331
+ path: relPath,
6332
+ type: "dir",
6333
+ children: buildFileTree(fullPath, relativeTo)
6334
+ };
6335
+ }
6336
+ return { name: entry.name, path: relPath, type: "file" };
6337
+ });
6338
+ }
6339
+ function inferLanguage(filePath) {
6340
+ const ext = path10.extname(filePath).toLowerCase();
6341
+ const langMap = {
6342
+ ".json": "json",
6343
+ ".jsonl": "json",
6344
+ ".ts": "typescript",
6345
+ ".tsx": "typescript",
6346
+ ".js": "javascript",
6347
+ ".jsx": "javascript",
6348
+ ".md": "markdown",
6349
+ ".yaml": "yaml",
6350
+ ".yml": "yaml",
6351
+ ".log": "plaintext",
6352
+ ".txt": "plaintext",
6353
+ ".py": "python",
6354
+ ".sh": "shell",
6355
+ ".bash": "shell",
6356
+ ".css": "css",
6357
+ ".html": "html",
6358
+ ".xml": "xml",
6359
+ ".svg": "xml",
6360
+ ".toml": "toml",
6361
+ ".diff": "diff",
6362
+ ".patch": "diff"
6363
+ };
6364
+ return langMap[ext] ?? "plaintext";
6365
+ }
6366
+ function stripHeavyFields(results) {
6367
+ return results.map((r) => {
6368
+ const { requests, trace, ...rest } = r;
6369
+ const toolCalls = trace?.toolCalls && Object.keys(trace.toolCalls).length > 0 ? trace.toolCalls : void 0;
6370
+ const graderDurationMs = (r.scores ?? []).reduce((sum, s) => sum + (s.durationMs ?? 0), 0);
6371
+ return {
6372
+ ...rest,
6373
+ ...toolCalls && { _toolCalls: toolCalls },
6374
+ ...graderDurationMs > 0 && { _graderDurationMs: graderDurationMs }
6375
+ };
6376
+ });
6377
+ }
6378
+ function handleRuns(c3, { searchDir }) {
6379
+ const metas = listResultFiles(searchDir);
6380
+ return c3.json({
6381
+ runs: metas.map((m) => {
6382
+ let target;
6383
+ let experiment;
6384
+ try {
6385
+ const records = loadLightweightResults(m.path);
6386
+ if (records.length > 0) {
6387
+ target = records[0].target;
6388
+ experiment = records[0].experiment;
6389
+ }
6390
+ } catch {
6391
+ }
6392
+ return {
6393
+ filename: m.filename,
6394
+ path: m.path,
6395
+ timestamp: m.timestamp,
6396
+ test_count: m.testCount,
6397
+ pass_rate: m.passRate,
6398
+ avg_score: m.avgScore,
6399
+ size_bytes: m.sizeBytes,
6400
+ ...target && { target },
6401
+ ...experiment && { experiment }
6402
+ };
6403
+ })
6404
+ });
6405
+ }
6406
+ function handleRunDetail(c3, { searchDir }) {
6407
+ const filename = c3.req.param("filename");
6408
+ const meta = listResultFiles(searchDir).find((m) => m.filename === filename);
6409
+ if (!meta) return c3.json({ error: "Run not found" }, 404);
6410
+ try {
6411
+ const loaded = patchTestIds(loadManifestResults(meta.path));
6412
+ return c3.json({ results: stripHeavyFields(loaded), source: meta.filename });
6413
+ } catch {
6414
+ return c3.json({ error: "Failed to load run" }, 500);
6415
+ }
6416
+ }
6417
+ function handleRunDatasets(c3, { searchDir, agentvDir }) {
6418
+ const filename = c3.req.param("filename");
6419
+ const meta = listResultFiles(searchDir).find((m) => m.filename === filename);
6420
+ if (!meta) return c3.json({ error: "Run not found" }, 404);
6421
+ try {
6422
+ const loaded = patchTestIds(loadManifestResults(meta.path));
6423
+ const { pass_threshold } = loadStudioConfig(agentvDir);
6424
+ const datasetMap = /* @__PURE__ */ new Map();
6425
+ for (const r of loaded) {
6426
+ const ds = r.dataset ?? r.target ?? "default";
6427
+ const entry = datasetMap.get(ds) ?? { total: 0, passed: 0, scoreSum: 0 };
6428
+ entry.total++;
6429
+ if (r.score >= pass_threshold) entry.passed++;
6430
+ entry.scoreSum += r.score;
6431
+ datasetMap.set(ds, entry);
6432
+ }
6433
+ const datasets = [...datasetMap.entries()].map(([name, entry]) => ({
6434
+ name,
6435
+ total: entry.total,
6436
+ passed: entry.passed,
6437
+ failed: entry.total - entry.passed,
6438
+ avg_score: entry.total > 0 ? entry.scoreSum / entry.total : 0
6439
+ }));
6440
+ return c3.json({ datasets });
6441
+ } catch {
6442
+ return c3.json({ error: "Failed to load datasets" }, 500);
6443
+ }
6444
+ }
6445
+ function handleRunCategories(c3, { searchDir, agentvDir }) {
6446
+ const filename = c3.req.param("filename");
6447
+ const meta = listResultFiles(searchDir).find((m) => m.filename === filename);
6448
+ if (!meta) return c3.json({ error: "Run not found" }, 404);
6449
+ try {
6450
+ const loaded = patchTestIds(loadManifestResults(meta.path));
6451
+ const { pass_threshold } = loadStudioConfig(agentvDir);
6452
+ const categoryMap = /* @__PURE__ */ new Map();
6453
+ for (const r of loaded) {
6454
+ const cat = r.category ?? DEFAULT_CATEGORY;
6455
+ const entry = categoryMap.get(cat) ?? {
6456
+ total: 0,
6457
+ passed: 0,
6458
+ scoreSum: 0,
6459
+ datasets: /* @__PURE__ */ new Set()
6460
+ };
6461
+ entry.total++;
6462
+ if (r.score >= pass_threshold) entry.passed++;
6463
+ entry.scoreSum += r.score;
6464
+ entry.datasets.add(r.dataset ?? r.target ?? "default");
6465
+ categoryMap.set(cat, entry);
6466
+ }
6467
+ const categories = [...categoryMap.entries()].map(([name, entry]) => ({
6468
+ name,
6469
+ total: entry.total,
6470
+ passed: entry.passed,
6471
+ failed: entry.total - entry.passed,
6472
+ avg_score: entry.total > 0 ? entry.scoreSum / entry.total : 0,
6473
+ dataset_count: entry.datasets.size
6474
+ }));
6475
+ return c3.json({ categories });
6476
+ } catch {
6477
+ return c3.json({ error: "Failed to load categories" }, 500);
6478
+ }
6479
+ }
6480
+ function handleCategoryDatasets(c3, { searchDir, agentvDir }) {
6481
+ const filename = c3.req.param("filename");
6482
+ const category = decodeURIComponent(c3.req.param("category") ?? "");
6483
+ const meta = listResultFiles(searchDir).find((m) => m.filename === filename);
6484
+ if (!meta) return c3.json({ error: "Run not found" }, 404);
6485
+ try {
6486
+ const loaded = patchTestIds(loadManifestResults(meta.path));
6487
+ const { pass_threshold } = loadStudioConfig(agentvDir);
6488
+ const filtered = loaded.filter((r) => (r.category ?? DEFAULT_CATEGORY) === category);
6489
+ const datasetMap = /* @__PURE__ */ new Map();
6490
+ for (const r of filtered) {
6491
+ const ds = r.dataset ?? r.target ?? "default";
6492
+ const entry = datasetMap.get(ds) ?? { total: 0, passed: 0, scoreSum: 0 };
6493
+ entry.total++;
6494
+ if (r.score >= pass_threshold) entry.passed++;
6495
+ entry.scoreSum += r.score;
6496
+ datasetMap.set(ds, entry);
6497
+ }
6498
+ const datasets = [...datasetMap.entries()].map(([name, entry]) => ({
6499
+ name,
6500
+ total: entry.total,
6501
+ passed: entry.passed,
6502
+ failed: entry.total - entry.passed,
6503
+ avg_score: entry.total > 0 ? entry.scoreSum / entry.total : 0
6504
+ }));
6505
+ return c3.json({ datasets });
6506
+ } catch {
6507
+ return c3.json({ error: "Failed to load datasets" }, 500);
6508
+ }
6509
+ }
6510
+ function handleEvalDetail(c3, { searchDir }) {
6511
+ const filename = c3.req.param("filename");
6512
+ const evalId = c3.req.param("evalId");
6513
+ const meta = listResultFiles(searchDir).find((m) => m.filename === filename);
6514
+ if (!meta) return c3.json({ error: "Run not found" }, 404);
6515
+ try {
6516
+ const loaded = patchTestIds(loadManifestResults(meta.path));
6517
+ const result = loaded.find((r) => r.testId === evalId);
6518
+ if (!result) return c3.json({ error: "Eval not found" }, 404);
6519
+ return c3.json({ eval: result });
6520
+ } catch {
6521
+ return c3.json({ error: "Failed to load eval" }, 500);
6522
+ }
6523
+ }
6524
+ function handleEvalFiles(c3, { searchDir }) {
6525
+ const filename = c3.req.param("filename");
6526
+ const evalId = c3.req.param("evalId");
6527
+ const meta = listResultFiles(searchDir).find((m) => m.filename === filename);
6528
+ if (!meta) return c3.json({ error: "Run not found" }, 404);
6529
+ try {
6530
+ const content = readFileSync9(meta.path, "utf8");
6531
+ const records = parseResultManifest(content);
6532
+ const record = records.find((r) => (r.test_id ?? r.eval_id) === evalId);
6533
+ if (!record) return c3.json({ error: "Eval not found" }, 404);
6534
+ const baseDir = path10.dirname(meta.path);
6535
+ const knownPaths = [
6536
+ record.grading_path,
6537
+ record.timing_path,
6538
+ record.input_path,
6539
+ record.output_path,
6540
+ record.response_path
6541
+ ].filter((p) => !!p);
6542
+ if (knownPaths.length === 0) return c3.json({ files: [] });
6543
+ const artifactDirs = knownPaths.map((p) => path10.dirname(p));
6544
+ let commonDir = artifactDirs[0];
6545
+ for (const dir of artifactDirs) {
6546
+ while (!dir.startsWith(commonDir)) {
6547
+ commonDir = path10.dirname(commonDir);
6548
+ }
6549
+ }
6550
+ const artifactAbsDir = path10.join(baseDir, commonDir);
6551
+ const files = buildFileTree(artifactAbsDir, baseDir);
6552
+ return c3.json({ files });
6553
+ } catch {
6554
+ return c3.json({ error: "Failed to load file tree" }, 500);
6555
+ }
6556
+ }
6557
+ function handleEvalFileContent(c3, { searchDir }) {
6558
+ const filename = c3.req.param("filename");
6559
+ const evalId = c3.req.param("evalId");
6560
+ const meta = listResultFiles(searchDir).find((m) => m.filename === filename);
6561
+ if (!meta) return c3.json({ error: "Run not found" }, 404);
6562
+ const marker = `/runs/${filename}/evals/${evalId}/files/`;
6563
+ const markerIdx = c3.req.path.indexOf(marker);
6564
+ const filePath = markerIdx >= 0 ? c3.req.path.slice(markerIdx + marker.length) : "";
6565
+ if (!filePath) return c3.json({ error: "No file path specified" }, 400);
6566
+ const baseDir = path10.dirname(meta.path);
6567
+ const absolutePath = path10.resolve(baseDir, filePath);
6568
+ if (!absolutePath.startsWith(path10.resolve(baseDir) + path10.sep) && absolutePath !== path10.resolve(baseDir)) {
6569
+ return c3.json({ error: "Path traversal not allowed" }, 403);
6570
+ }
6571
+ if (!existsSync8(absolutePath) || !statSync4(absolutePath).isFile()) {
6572
+ return c3.json({ error: "File not found" }, 404);
6573
+ }
6574
+ try {
6575
+ const fileContent = readFileSync9(absolutePath, "utf8");
6576
+ const language = inferLanguage(absolutePath);
6577
+ return c3.json({ content: fileContent, language });
6578
+ } catch {
6579
+ return c3.json({ error: "Failed to read file" }, 500);
6580
+ }
6581
+ }
6582
+ function handleExperiments(c3, { searchDir, agentvDir }) {
6583
+ const metas = listResultFiles(searchDir);
6584
+ const { pass_threshold } = loadStudioConfig(agentvDir);
6585
+ const experimentMap = /* @__PURE__ */ new Map();
6586
+ for (const m of metas) {
6587
+ try {
6588
+ const records = loadLightweightResults(m.path);
6589
+ for (const r of records) {
6590
+ const experiment = r.experiment ?? "default";
6591
+ const entry = experimentMap.get(experiment) ?? {
6592
+ targets: /* @__PURE__ */ new Set(),
6593
+ runFilenames: /* @__PURE__ */ new Set(),
6594
+ evalCount: 0,
6595
+ passedCount: 0,
6596
+ lastTimestamp: ""
6597
+ };
6598
+ entry.runFilenames.add(m.filename);
6599
+ if (r.target) entry.targets.add(r.target);
6600
+ entry.evalCount++;
6601
+ if (r.score >= pass_threshold) entry.passedCount++;
6602
+ if (r.timestamp && r.timestamp > entry.lastTimestamp) {
6603
+ entry.lastTimestamp = r.timestamp;
6604
+ }
6605
+ experimentMap.set(experiment, entry);
6606
+ }
6607
+ } catch {
6608
+ }
6609
+ }
6610
+ const experiments = [...experimentMap.entries()].map(([name, entry]) => ({
6611
+ name,
6612
+ run_count: entry.runFilenames.size,
6613
+ target_count: entry.targets.size,
6614
+ eval_count: entry.evalCount,
6615
+ passed_count: entry.passedCount,
6616
+ pass_rate: entry.evalCount > 0 ? entry.passedCount / entry.evalCount : 0,
6617
+ last_run: entry.lastTimestamp || null
6618
+ }));
6619
+ return c3.json({ experiments });
6620
+ }
6621
+ function handleTargets(c3, { searchDir, agentvDir }) {
6622
+ const metas = listResultFiles(searchDir);
6623
+ const { pass_threshold } = loadStudioConfig(agentvDir);
6624
+ const targetMap = /* @__PURE__ */ new Map();
6625
+ for (const m of metas) {
6626
+ try {
6627
+ const records = loadLightweightResults(m.path);
6628
+ for (const r of records) {
6629
+ const target = r.target ?? "default";
6630
+ const entry = targetMap.get(target) ?? {
6631
+ experiments: /* @__PURE__ */ new Set(),
6632
+ runFilenames: /* @__PURE__ */ new Set(),
6633
+ evalCount: 0,
6634
+ passedCount: 0
6635
+ };
6636
+ entry.runFilenames.add(m.filename);
6637
+ if (r.experiment) entry.experiments.add(r.experiment);
6638
+ entry.evalCount++;
6639
+ if (r.score >= pass_threshold) entry.passedCount++;
6640
+ targetMap.set(target, entry);
6641
+ }
6642
+ } catch {
6643
+ }
6644
+ }
6645
+ const targets = [...targetMap.entries()].map(([name, entry]) => ({
6646
+ name,
6647
+ run_count: entry.runFilenames.size,
6648
+ experiment_count: entry.experiments.size,
6649
+ eval_count: entry.evalCount,
6650
+ passed_count: entry.passedCount,
6651
+ pass_rate: entry.evalCount > 0 ? entry.passedCount / entry.evalCount : 0
6652
+ }));
6653
+ return c3.json({ targets });
6654
+ }
6655
+ function handleConfig(c3, { agentvDir }) {
6656
+ return c3.json(loadStudioConfig(agentvDir));
6657
+ }
6658
+ function handleFeedbackRead(c3, { searchDir }) {
6659
+ const resultsDir = path10.join(searchDir, ".agentv", "results");
6660
+ return c3.json(readFeedback(existsSync8(resultsDir) ? resultsDir : searchDir));
6661
+ }
6312
6662
  function createApp(results, resultDir, cwd, sourceFile, options) {
6313
6663
  const searchDir = cwd ?? resultDir;
6314
6664
  const agentvDir = path10.join(searchDir, ".agentv");
6665
+ const defaultCtx = { searchDir, agentvDir };
6315
6666
  const app2 = new Hono();
6316
- app2.get("/api/config", (c3) => c3.json(loadStudioConfig(agentvDir)));
6667
+ function withProject(c3, handler) {
6668
+ const project = getProject(c3.req.param("projectId") ?? "");
6669
+ if (!project || !existsSync8(project.path)) {
6670
+ return c3.json({ error: "Project not found" }, 404);
6671
+ }
6672
+ return handler(c3, {
6673
+ searchDir: project.path,
6674
+ agentvDir: path10.join(project.path, ".agentv")
6675
+ });
6676
+ }
6317
6677
  app2.post("/api/config", async (c3) => {
6318
6678
  try {
6319
6679
  const body = await c3.req.json();
@@ -6328,60 +6688,137 @@ function createApp(results, resultDir, cwd, sourceFile, options) {
6328
6688
  return c3.json({ error: "Failed to save config" }, 500);
6329
6689
  }
6330
6690
  });
6331
- const studioDistPath = options?.studioDir ?? resolveStudioDistDir();
6332
- if (!studioDistPath || !existsSync8(path10.join(studioDistPath, "index.html"))) {
6333
- throw new Error('Studio dist not found. Run "bun run build" in apps/studio/ to build the SPA.');
6691
+ function projectEntryToWire(entry) {
6692
+ return {
6693
+ id: entry.id,
6694
+ name: entry.name,
6695
+ path: entry.path,
6696
+ added_at: entry.addedAt,
6697
+ last_opened_at: entry.lastOpenedAt
6698
+ };
6334
6699
  }
6335
- app2.get("/", (c3) => {
6336
- const indexPath = path10.join(studioDistPath, "index.html");
6337
- if (existsSync8(indexPath)) {
6338
- return c3.html(readFileSync9(indexPath, "utf8"));
6339
- }
6340
- return c3.notFound();
6341
- });
6342
- app2.get("/api/runs", (c3) => {
6343
- const metas = listResultFiles(searchDir);
6344
- return c3.json({
6345
- runs: metas.map((m) => {
6346
- let target;
6347
- let experiment;
6348
- try {
6349
- const records = loadLightweightResults(m.path);
6350
- if (records.length > 0) {
6351
- target = records[0].target;
6352
- experiment = records[0].experiment;
6353
- }
6354
- } catch {
6700
+ app2.get("/api/projects", (c3) => {
6701
+ const registry = loadProjectRegistry();
6702
+ const projects = registry.projects.map((p) => {
6703
+ let runCount = 0;
6704
+ let passRate = 0;
6705
+ let lastRun = null;
6706
+ try {
6707
+ const metas = listResultFiles(p.path);
6708
+ runCount = metas.length;
6709
+ if (metas.length > 0) {
6710
+ const totalPassRate = metas.reduce((sum, m) => sum + m.passRate, 0);
6711
+ passRate = totalPassRate / metas.length;
6712
+ lastRun = metas[0].timestamp;
6355
6713
  }
6356
- return {
6357
- filename: m.filename,
6358
- path: m.path,
6359
- timestamp: m.timestamp,
6360
- test_count: m.testCount,
6361
- pass_rate: m.passRate,
6362
- avg_score: m.avgScore,
6363
- size_bytes: m.sizeBytes,
6364
- ...target && { target },
6365
- ...experiment && { experiment }
6366
- };
6367
- })
6714
+ } catch {
6715
+ }
6716
+ return {
6717
+ ...projectEntryToWire(p),
6718
+ run_count: runCount,
6719
+ pass_rate: passRate,
6720
+ last_run: lastRun
6721
+ };
6368
6722
  });
6723
+ return c3.json({ projects });
6369
6724
  });
6370
- app2.get("/api/runs/:filename", (c3) => {
6371
- const filename = c3.req.param("filename");
6372
- const metas = listResultFiles(searchDir);
6373
- const meta = metas.find((m) => m.filename === filename);
6374
- if (!meta) {
6375
- return c3.json({ error: "Run not found" }, 404);
6725
+ app2.post("/api/projects", async (c3) => {
6726
+ try {
6727
+ const body = await c3.req.json();
6728
+ if (!body.path) return c3.json({ error: "Missing path" }, 400);
6729
+ const entry = addProject(body.path);
6730
+ return c3.json(projectEntryToWire(entry), 201);
6731
+ } catch (err2) {
6732
+ return c3.json({ error: err2.message }, 400);
6733
+ }
6734
+ });
6735
+ app2.delete("/api/projects/:projectId", (c3) => {
6736
+ const removed = removeProject(c3.req.param("projectId") ?? "");
6737
+ if (!removed) return c3.json({ error: "Project not found" }, 404);
6738
+ return c3.json({ ok: true });
6739
+ });
6740
+ app2.get("/api/projects/:projectId/summary", (c3) => {
6741
+ const project = getProject(c3.req.param("projectId") ?? "");
6742
+ if (!project) return c3.json({ error: "Project not found" }, 404);
6743
+ try {
6744
+ const metas = listResultFiles(project.path);
6745
+ const runCount = metas.length;
6746
+ const passRate = runCount > 0 ? metas.reduce((s, m) => s + m.passRate, 0) / runCount : 0;
6747
+ const lastRun = metas.length > 0 ? metas[0].timestamp : null;
6748
+ return c3.json({
6749
+ id: project.id,
6750
+ name: project.name,
6751
+ path: project.path,
6752
+ run_count: runCount,
6753
+ pass_rate: passRate,
6754
+ last_run: lastRun
6755
+ });
6756
+ } catch {
6757
+ return c3.json({ error: "Failed to read project" }, 500);
6376
6758
  }
6759
+ });
6760
+ app2.post("/api/projects/discover", async (c3) => {
6377
6761
  try {
6378
- const loaded = patchTestIds(loadManifestResults(meta.path));
6379
- const lightResults = stripHeavyFields(loaded);
6380
- return c3.json({ results: lightResults, source: meta.filename });
6762
+ const body = await c3.req.json();
6763
+ if (!body.path) return c3.json({ error: "Missing path" }, 400);
6764
+ const discovered = discoverProjects(body.path);
6765
+ const registered = discovered.map((p) => projectEntryToWire(addProject(p)));
6766
+ return c3.json({ discovered: registered });
6381
6767
  } catch (err2) {
6382
- return c3.json({ error: "Failed to load run" }, 500);
6768
+ return c3.json({ error: err2.message }, 400);
6383
6769
  }
6384
6770
  });
6771
+ app2.get("/api/projects/all-runs", (c3) => {
6772
+ const registry = loadProjectRegistry();
6773
+ const allRuns = [];
6774
+ for (const p of registry.projects) {
6775
+ try {
6776
+ const metas = listResultFiles(p.path);
6777
+ for (const m of metas) {
6778
+ let target;
6779
+ let experiment;
6780
+ try {
6781
+ const records = loadLightweightResults(m.path);
6782
+ if (records.length > 0) {
6783
+ target = records[0].target;
6784
+ experiment = records[0].experiment;
6785
+ }
6786
+ } catch {
6787
+ }
6788
+ allRuns.push({
6789
+ filename: m.filename,
6790
+ path: m.path,
6791
+ timestamp: m.timestamp,
6792
+ test_count: m.testCount,
6793
+ pass_rate: m.passRate,
6794
+ avg_score: m.avgScore,
6795
+ size_bytes: m.sizeBytes,
6796
+ ...target && { target },
6797
+ ...experiment && { experiment },
6798
+ project_id: p.id,
6799
+ project_name: p.name
6800
+ });
6801
+ }
6802
+ } catch {
6803
+ }
6804
+ }
6805
+ allRuns.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
6806
+ return c3.json({ runs: allRuns });
6807
+ });
6808
+ app2.get("/api/config", (c3) => handleConfig(c3, defaultCtx));
6809
+ app2.get("/api/runs", (c3) => handleRuns(c3, defaultCtx));
6810
+ app2.get("/api/runs/:filename", (c3) => handleRunDetail(c3, defaultCtx));
6811
+ app2.get("/api/runs/:filename/datasets", (c3) => handleRunDatasets(c3, defaultCtx));
6812
+ app2.get("/api/runs/:filename/categories", (c3) => handleRunCategories(c3, defaultCtx));
6813
+ app2.get(
6814
+ "/api/runs/:filename/categories/:category/datasets",
6815
+ (c3) => handleCategoryDatasets(c3, defaultCtx)
6816
+ );
6817
+ app2.get("/api/runs/:filename/evals/:evalId", (c3) => handleEvalDetail(c3, defaultCtx));
6818
+ app2.get("/api/runs/:filename/evals/:evalId/files", (c3) => handleEvalFiles(c3, defaultCtx));
6819
+ app2.get("/api/runs/:filename/evals/:evalId/files/*", (c3) => handleEvalFileContent(c3, defaultCtx));
6820
+ app2.get("/api/experiments", (c3) => handleExperiments(c3, defaultCtx));
6821
+ app2.get("/api/targets", (c3) => handleTargets(c3, defaultCtx));
6385
6822
  app2.get("/api/feedback", (c3) => {
6386
6823
  const data = readFeedback(resultDir);
6387
6824
  return c3.json(data);
@@ -6424,127 +6861,6 @@ function createApp(results, resultDir, cwd, sourceFile, options) {
6424
6861
  writeFeedback(resultDir, existing);
6425
6862
  return c3.json(existing);
6426
6863
  });
6427
- app2.get("/api/runs/:filename/datasets", (c3) => {
6428
- const filename = c3.req.param("filename");
6429
- const metas = listResultFiles(searchDir);
6430
- const meta = metas.find((m) => m.filename === filename);
6431
- if (!meta) {
6432
- return c3.json({ error: "Run not found" }, 404);
6433
- }
6434
- try {
6435
- const loaded = patchTestIds(loadManifestResults(meta.path));
6436
- const { pass_threshold } = loadStudioConfig(agentvDir);
6437
- const datasetMap = /* @__PURE__ */ new Map();
6438
- for (const r of loaded) {
6439
- const ds = r.dataset ?? r.target ?? "default";
6440
- const entry = datasetMap.get(ds) ?? { total: 0, passed: 0, scoreSum: 0 };
6441
- entry.total++;
6442
- if (r.score >= pass_threshold) entry.passed++;
6443
- entry.scoreSum += r.score;
6444
- datasetMap.set(ds, entry);
6445
- }
6446
- const datasets = [...datasetMap.entries()].map(([name, entry]) => ({
6447
- name,
6448
- total: entry.total,
6449
- passed: entry.passed,
6450
- failed: entry.total - entry.passed,
6451
- avg_score: entry.total > 0 ? entry.scoreSum / entry.total : 0
6452
- }));
6453
- return c3.json({ datasets });
6454
- } catch {
6455
- return c3.json({ error: "Failed to load datasets" }, 500);
6456
- }
6457
- });
6458
- app2.get("/api/runs/:filename/categories", (c3) => {
6459
- const filename = c3.req.param("filename");
6460
- const metas = listResultFiles(searchDir);
6461
- const meta = metas.find((m) => m.filename === filename);
6462
- if (!meta) {
6463
- return c3.json({ error: "Run not found" }, 404);
6464
- }
6465
- try {
6466
- const loaded = patchTestIds(loadManifestResults(meta.path));
6467
- const { pass_threshold } = loadStudioConfig(agentvDir);
6468
- const categoryMap = /* @__PURE__ */ new Map();
6469
- for (const r of loaded) {
6470
- const cat = r.category ?? DEFAULT_CATEGORY;
6471
- const entry = categoryMap.get(cat) ?? {
6472
- total: 0,
6473
- passed: 0,
6474
- scoreSum: 0,
6475
- datasets: /* @__PURE__ */ new Set()
6476
- };
6477
- entry.total++;
6478
- if (r.score >= pass_threshold) entry.passed++;
6479
- entry.scoreSum += r.score;
6480
- entry.datasets.add(r.dataset ?? r.target ?? "default");
6481
- categoryMap.set(cat, entry);
6482
- }
6483
- const categories = [...categoryMap.entries()].map(([name, entry]) => ({
6484
- name,
6485
- total: entry.total,
6486
- passed: entry.passed,
6487
- failed: entry.total - entry.passed,
6488
- avg_score: entry.total > 0 ? entry.scoreSum / entry.total : 0,
6489
- dataset_count: entry.datasets.size
6490
- }));
6491
- return c3.json({ categories });
6492
- } catch {
6493
- return c3.json({ error: "Failed to load categories" }, 500);
6494
- }
6495
- });
6496
- app2.get("/api/runs/:filename/categories/:category/datasets", (c3) => {
6497
- const filename = c3.req.param("filename");
6498
- const category = decodeURIComponent(c3.req.param("category"));
6499
- const metas = listResultFiles(searchDir);
6500
- const meta = metas.find((m) => m.filename === filename);
6501
- if (!meta) {
6502
- return c3.json({ error: "Run not found" }, 404);
6503
- }
6504
- try {
6505
- const loaded = patchTestIds(loadManifestResults(meta.path));
6506
- const { pass_threshold } = loadStudioConfig(agentvDir);
6507
- const filtered = loaded.filter((r) => (r.category ?? DEFAULT_CATEGORY) === category);
6508
- const datasetMap = /* @__PURE__ */ new Map();
6509
- for (const r of filtered) {
6510
- const ds = r.dataset ?? r.target ?? "default";
6511
- const entry = datasetMap.get(ds) ?? { total: 0, passed: 0, scoreSum: 0 };
6512
- entry.total++;
6513
- if (r.score >= pass_threshold) entry.passed++;
6514
- entry.scoreSum += r.score;
6515
- datasetMap.set(ds, entry);
6516
- }
6517
- const datasets = [...datasetMap.entries()].map(([name, entry]) => ({
6518
- name,
6519
- total: entry.total,
6520
- passed: entry.passed,
6521
- failed: entry.total - entry.passed,
6522
- avg_score: entry.total > 0 ? entry.scoreSum / entry.total : 0
6523
- }));
6524
- return c3.json({ datasets });
6525
- } catch {
6526
- return c3.json({ error: "Failed to load datasets" }, 500);
6527
- }
6528
- });
6529
- app2.get("/api/runs/:filename/evals/:evalId", (c3) => {
6530
- const filename = c3.req.param("filename");
6531
- const evalId = c3.req.param("evalId");
6532
- const metas = listResultFiles(searchDir);
6533
- const meta = metas.find((m) => m.filename === filename);
6534
- if (!meta) {
6535
- return c3.json({ error: "Run not found" }, 404);
6536
- }
6537
- try {
6538
- const loaded = patchTestIds(loadManifestResults(meta.path));
6539
- const result = loaded.find((r) => r.testId === evalId);
6540
- if (!result) {
6541
- return c3.json({ error: "Eval not found" }, 404);
6542
- }
6543
- return c3.json({ eval: result });
6544
- } catch {
6545
- return c3.json({ error: "Failed to load eval" }, 500);
6546
- }
6547
- });
6548
6864
  app2.get("/api/index", (c3) => {
6549
6865
  const metas = listResultFiles(searchDir);
6550
6866
  const entries2 = metas.map((m) => {
@@ -6565,204 +6881,49 @@ function createApp(results, resultDir, cwd, sourceFile, options) {
6565
6881
  });
6566
6882
  return c3.json({ entries: entries2 });
6567
6883
  });
6568
- function buildFileTree(dirPath, relativeTo) {
6569
- if (!existsSync8(dirPath) || !statSync4(dirPath).isDirectory()) {
6570
- return [];
6571
- }
6572
- const entries2 = readdirSync3(dirPath, { withFileTypes: true });
6573
- return entries2.sort((a, b) => {
6574
- if (a.isDirectory() !== b.isDirectory()) return a.isDirectory() ? -1 : 1;
6575
- return a.name.localeCompare(b.name);
6576
- }).map((entry) => {
6577
- const fullPath = path10.join(dirPath, entry.name);
6578
- const relPath = path10.relative(relativeTo, fullPath);
6579
- if (entry.isDirectory()) {
6580
- return {
6581
- name: entry.name,
6582
- path: relPath,
6583
- type: "dir",
6584
- children: buildFileTree(fullPath, relativeTo)
6585
- };
6586
- }
6587
- return { name: entry.name, path: relPath, type: "file" };
6588
- });
6589
- }
6590
- function inferLanguage(filePath) {
6591
- const ext = path10.extname(filePath).toLowerCase();
6592
- const langMap = {
6593
- ".json": "json",
6594
- ".jsonl": "json",
6595
- ".ts": "typescript",
6596
- ".tsx": "typescript",
6597
- ".js": "javascript",
6598
- ".jsx": "javascript",
6599
- ".md": "markdown",
6600
- ".yaml": "yaml",
6601
- ".yml": "yaml",
6602
- ".log": "plaintext",
6603
- ".txt": "plaintext",
6604
- ".py": "python",
6605
- ".sh": "shell",
6606
- ".bash": "shell",
6607
- ".css": "css",
6608
- ".html": "html",
6609
- ".xml": "xml",
6610
- ".svg": "xml",
6611
- ".toml": "toml",
6612
- ".diff": "diff",
6613
- ".patch": "diff"
6614
- };
6615
- return langMap[ext] ?? "plaintext";
6884
+ app2.get("/api/projects/:projectId/config", (c3) => withProject(c3, handleConfig));
6885
+ app2.get("/api/projects/:projectId/runs", (c3) => withProject(c3, handleRuns));
6886
+ app2.get("/api/projects/:projectId/runs/:filename", (c3) => withProject(c3, handleRunDetail));
6887
+ app2.get(
6888
+ "/api/projects/:projectId/runs/:filename/datasets",
6889
+ (c3) => withProject(c3, handleRunDatasets)
6890
+ );
6891
+ app2.get(
6892
+ "/api/projects/:projectId/runs/:filename/categories",
6893
+ (c3) => withProject(c3, handleRunCategories)
6894
+ );
6895
+ app2.get(
6896
+ "/api/projects/:projectId/runs/:filename/categories/:category/datasets",
6897
+ (c3) => withProject(c3, handleCategoryDatasets)
6898
+ );
6899
+ app2.get(
6900
+ "/api/projects/:projectId/runs/:filename/evals/:evalId",
6901
+ (c3) => withProject(c3, handleEvalDetail)
6902
+ );
6903
+ app2.get(
6904
+ "/api/projects/:projectId/runs/:filename/evals/:evalId/files",
6905
+ (c3) => withProject(c3, handleEvalFiles)
6906
+ );
6907
+ app2.get(
6908
+ "/api/projects/:projectId/runs/:filename/evals/:evalId/files/*",
6909
+ (c3) => withProject(c3, handleEvalFileContent)
6910
+ );
6911
+ app2.get("/api/projects/:projectId/experiments", (c3) => withProject(c3, handleExperiments));
6912
+ app2.get("/api/projects/:projectId/targets", (c3) => withProject(c3, handleTargets));
6913
+ app2.get("/api/projects/:projectId/feedback", (c3) => withProject(c3, handleFeedbackRead));
6914
+ const studioDistPath = options?.studioDir ?? resolveStudioDistDir();
6915
+ if (!studioDistPath || !existsSync8(path10.join(studioDistPath, "index.html"))) {
6916
+ throw new Error('Studio dist not found. Run "bun run build" in apps/studio/ to build the SPA.');
6616
6917
  }
6617
- app2.get("/api/runs/:filename/evals/:evalId/files", (c3) => {
6618
- const filename = c3.req.param("filename");
6619
- const evalId = c3.req.param("evalId");
6620
- const metas = listResultFiles(searchDir);
6621
- const meta = metas.find((m) => m.filename === filename);
6622
- if (!meta) {
6623
- return c3.json({ error: "Run not found" }, 404);
6624
- }
6625
- try {
6626
- const content = readFileSync9(meta.path, "utf8");
6627
- const records = parseResultManifest(content);
6628
- const record = records.find((r) => (r.test_id ?? r.eval_id) === evalId);
6629
- if (!record) {
6630
- return c3.json({ error: "Eval not found" }, 404);
6631
- }
6632
- const baseDir = path10.dirname(meta.path);
6633
- const knownPaths = [
6634
- record.grading_path,
6635
- record.timing_path,
6636
- record.input_path,
6637
- record.output_path,
6638
- record.response_path
6639
- ].filter((p) => !!p);
6640
- if (knownPaths.length === 0) {
6641
- return c3.json({ files: [] });
6642
- }
6643
- const artifactDirs = knownPaths.map((p) => path10.dirname(p));
6644
- let commonDir = artifactDirs[0];
6645
- for (const dir of artifactDirs) {
6646
- while (!dir.startsWith(commonDir)) {
6647
- commonDir = path10.dirname(commonDir);
6648
- }
6649
- }
6650
- const artifactAbsDir = path10.join(baseDir, commonDir);
6651
- const files = buildFileTree(artifactAbsDir, baseDir);
6652
- return c3.json({ files });
6653
- } catch {
6654
- return c3.json({ error: "Failed to load file tree" }, 500);
6655
- }
6656
- });
6657
- app2.get("/api/runs/:filename/evals/:evalId/files/*", (c3) => {
6658
- const filename = c3.req.param("filename");
6659
- const evalId = c3.req.param("evalId");
6660
- const metas = listResultFiles(searchDir);
6661
- const meta = metas.find((m) => m.filename === filename);
6662
- if (!meta) {
6663
- return c3.json({ error: "Run not found" }, 404);
6664
- }
6665
- const requestPath = c3.req.path;
6666
- const prefix = `/api/runs/${filename}/evals/${evalId}/files/`;
6667
- const filePath = requestPath.slice(prefix.length);
6668
- if (!filePath) {
6669
- return c3.json({ error: "No file path specified" }, 400);
6670
- }
6671
- const baseDir = path10.dirname(meta.path);
6672
- const absolutePath = path10.resolve(baseDir, filePath);
6673
- if (!absolutePath.startsWith(path10.resolve(baseDir) + path10.sep) && absolutePath !== path10.resolve(baseDir)) {
6674
- return c3.json({ error: "Path traversal not allowed" }, 403);
6675
- }
6676
- if (!existsSync8(absolutePath) || !statSync4(absolutePath).isFile()) {
6677
- return c3.json({ error: "File not found" }, 404);
6678
- }
6679
- try {
6680
- const fileContent = readFileSync9(absolutePath, "utf8");
6681
- const language = inferLanguage(absolutePath);
6682
- return c3.json({ content: fileContent, language });
6683
- } catch {
6684
- return c3.json({ error: "Failed to read file" }, 500);
6685
- }
6686
- });
6687
- app2.get("/api/experiments", (c3) => {
6688
- const metas = listResultFiles(searchDir);
6689
- const { pass_threshold } = loadStudioConfig(agentvDir);
6690
- const experimentMap = /* @__PURE__ */ new Map();
6691
- for (const m of metas) {
6692
- try {
6693
- const records = loadLightweightResults(m.path);
6694
- for (const r of records) {
6695
- const experiment = r.experiment ?? "default";
6696
- const entry = experimentMap.get(experiment) ?? {
6697
- targets: /* @__PURE__ */ new Set(),
6698
- runFilenames: /* @__PURE__ */ new Set(),
6699
- evalCount: 0,
6700
- passedCount: 0,
6701
- lastTimestamp: ""
6702
- };
6703
- entry.runFilenames.add(m.filename);
6704
- if (r.target) entry.targets.add(r.target);
6705
- entry.evalCount++;
6706
- if (r.score >= pass_threshold) entry.passedCount++;
6707
- if (r.timestamp && r.timestamp > entry.lastTimestamp) {
6708
- entry.lastTimestamp = r.timestamp;
6709
- }
6710
- experimentMap.set(experiment, entry);
6711
- }
6712
- } catch {
6713
- }
6714
- }
6715
- const experiments = [...experimentMap.entries()].map(([name, entry]) => ({
6716
- name,
6717
- run_count: entry.runFilenames.size,
6718
- target_count: entry.targets.size,
6719
- eval_count: entry.evalCount,
6720
- passed_count: entry.passedCount,
6721
- pass_rate: entry.evalCount > 0 ? entry.passedCount / entry.evalCount : 0,
6722
- last_run: entry.lastTimestamp || null
6723
- }));
6724
- return c3.json({ experiments });
6725
- });
6726
- app2.get("/api/targets", (c3) => {
6727
- const metas = listResultFiles(searchDir);
6728
- const { pass_threshold } = loadStudioConfig(agentvDir);
6729
- const targetMap = /* @__PURE__ */ new Map();
6730
- for (const m of metas) {
6731
- try {
6732
- const records = loadLightweightResults(m.path);
6733
- for (const r of records) {
6734
- const target = r.target ?? "default";
6735
- const entry = targetMap.get(target) ?? {
6736
- experiments: /* @__PURE__ */ new Set(),
6737
- runFilenames: /* @__PURE__ */ new Set(),
6738
- evalCount: 0,
6739
- passedCount: 0
6740
- };
6741
- entry.runFilenames.add(m.filename);
6742
- if (r.experiment) entry.experiments.add(r.experiment);
6743
- entry.evalCount++;
6744
- if (r.score >= pass_threshold) entry.passedCount++;
6745
- targetMap.set(target, entry);
6746
- }
6747
- } catch {
6748
- }
6749
- }
6750
- const targets = [...targetMap.entries()].map(([name, entry]) => ({
6751
- name,
6752
- run_count: entry.runFilenames.size,
6753
- experiment_count: entry.experiments.size,
6754
- eval_count: entry.evalCount,
6755
- passed_count: entry.passedCount,
6756
- pass_rate: entry.evalCount > 0 ? entry.passedCount / entry.evalCount : 0
6757
- }));
6758
- return c3.json({ targets });
6918
+ app2.get("/", (c3) => {
6919
+ const indexPath = path10.join(studioDistPath, "index.html");
6920
+ if (existsSync8(indexPath)) return c3.html(readFileSync9(indexPath, "utf8"));
6921
+ return c3.notFound();
6759
6922
  });
6760
6923
  app2.get("/assets/*", (c3) => {
6761
6924
  const assetPath = c3.req.path;
6762
6925
  const filePath = path10.join(studioDistPath, assetPath);
6763
- if (!existsSync8(filePath)) {
6764
- return c3.notFound();
6765
- }
6926
+ if (!existsSync8(filePath)) return c3.notFound();
6766
6927
  const content = readFileSync9(filePath);
6767
6928
  const ext = path10.extname(filePath);
6768
6929
  const mimeTypes = {
@@ -6784,13 +6945,9 @@ function createApp(results, resultDir, cwd, sourceFile, options) {
6784
6945
  });
6785
6946
  });
6786
6947
  app2.get("*", (c3) => {
6787
- if (c3.req.path.startsWith("/api/")) {
6788
- return c3.json({ error: "Not found" }, 404);
6789
- }
6948
+ if (c3.req.path.startsWith("/api/")) return c3.json({ error: "Not found" }, 404);
6790
6949
  const indexPath = path10.join(studioDistPath, "index.html");
6791
- if (existsSync8(indexPath)) {
6792
- return c3.html(readFileSync9(indexPath, "utf8"));
6793
- }
6950
+ if (existsSync8(indexPath)) return c3.html(readFileSync9(indexPath, "utf8"));
6794
6951
  return c3.notFound();
6795
6952
  });
6796
6953
  return app2;
@@ -6814,18 +6971,6 @@ function resolveStudioDistDir() {
6814
6971
  }
6815
6972
  return void 0;
6816
6973
  }
6817
- function stripHeavyFields(results) {
6818
- return results.map((r) => {
6819
- const { requests, trace, ...rest } = r;
6820
- const toolCalls = trace?.toolCalls && Object.keys(trace.toolCalls).length > 0 ? trace.toolCalls : void 0;
6821
- const graderDurationMs = (r.scores ?? []).reduce((sum, s) => sum + (s.durationMs ?? 0), 0);
6822
- return {
6823
- ...rest,
6824
- ...toolCalls && { _toolCalls: toolCalls },
6825
- ...graderDurationMs > 0 && { _graderDurationMs: graderDurationMs }
6826
- };
6827
- });
6828
- }
6829
6974
  var resultsServeCommand = command({
6830
6975
  name: "studio",
6831
6976
  description: "Start AgentV Studio \u2014 a local dashboard for reviewing evaluation results",
@@ -6846,11 +6991,66 @@ var resultsServeCommand = command({
6846
6991
  long: "dir",
6847
6992
  short: "d",
6848
6993
  description: "Working directory (default: current directory)"
6994
+ }),
6995
+ multi: flag({
6996
+ long: "multi",
6997
+ description: "Launch in multi-project dashboard mode"
6998
+ }),
6999
+ add: option({
7000
+ type: optional(string),
7001
+ long: "add",
7002
+ description: "Register a project by path"
7003
+ }),
7004
+ remove: option({
7005
+ type: optional(string),
7006
+ long: "remove",
7007
+ description: "Unregister a project by ID"
7008
+ }),
7009
+ discover: option({
7010
+ type: optional(string),
7011
+ long: "discover",
7012
+ description: "Scan a directory tree for repos with .agentv/"
6849
7013
  })
6850
7014
  },
6851
- handler: async ({ source, port, dir }) => {
7015
+ handler: async ({ source, port, dir, multi, add, remove, discover }) => {
6852
7016
  const cwd = dir ?? process.cwd();
6853
7017
  const listenPort = port ?? (process.env.PORT ? Number(process.env.PORT) : 3117);
7018
+ if (add) {
7019
+ try {
7020
+ const entry = addProject(add);
7021
+ console.log(`Registered project: ${entry.name} (${entry.id}) at ${entry.path}`);
7022
+ } catch (err2) {
7023
+ console.error(`Error: ${err2.message}`);
7024
+ process.exit(1);
7025
+ }
7026
+ return;
7027
+ }
7028
+ if (remove) {
7029
+ const removed = removeProject(remove);
7030
+ if (removed) {
7031
+ console.log(`Unregistered project: ${remove}`);
7032
+ } else {
7033
+ console.error(`Project not found: ${remove}`);
7034
+ process.exit(1);
7035
+ }
7036
+ return;
7037
+ }
7038
+ if (discover) {
7039
+ const discovered = discoverProjects(discover);
7040
+ if (discovered.length === 0) {
7041
+ console.log(`No projects with .agentv/ found under ${discover}`);
7042
+ return;
7043
+ }
7044
+ for (const p of discovered) {
7045
+ const entry = addProject(p);
7046
+ console.log(`Registered: ${entry.name} (${entry.id}) at ${entry.path}`);
7047
+ }
7048
+ console.log(`
7049
+ Discovered ${discovered.length} project(s).`);
7050
+ return;
7051
+ }
7052
+ const registry = loadProjectRegistry();
7053
+ const isMultiProject = multi || registry.projects.length > 0;
6854
7054
  try {
6855
7055
  let results = [];
6856
7056
  let sourceFile;
@@ -6878,16 +7078,16 @@ var resultsServeCommand = command({
6878
7078
  }
6879
7079
  const resultDir = sourceFile ? path10.dirname(path10.resolve(sourceFile)) : cwd;
6880
7080
  const app2 = createApp(results, resultDir, cwd, sourceFile);
6881
- if (results.length > 0 && sourceFile) {
7081
+ if (isMultiProject) {
7082
+ console.log(`Multi-project mode: ${registry.projects.length} project(s) registered`);
7083
+ } else if (results.length > 0 && sourceFile) {
6882
7084
  console.log(`Serving ${results.length} result(s) from ${sourceFile}`);
6883
7085
  } else {
6884
7086
  console.log("No results found. Dashboard will show an empty state.");
6885
7087
  console.log("Run an evaluation to see results: agentv eval <eval-file>");
6886
7088
  }
6887
7089
  console.log(`Dashboard: http://localhost:${listenPort}`);
6888
- console.log(`Feedback API: http://localhost:${listenPort}/api/feedback`);
6889
- console.log(`Result picker API: http://localhost:${listenPort}/api/runs`);
6890
- console.log(`Feedback file: ${feedbackPath(resultDir)}`);
7090
+ console.log(`Projects API: http://localhost:${listenPort}/api/projects`);
6891
7091
  console.log("Press Ctrl+C to stop");
6892
7092
  const { serve: startServer } = await import("@hono/node-server");
6893
7093
  startServer({
@@ -8451,4 +8651,4 @@ export {
8451
8651
  preprocessArgv,
8452
8652
  runCli
8453
8653
  };
8454
- //# sourceMappingURL=chunk-TCJKPOU7.js.map
8654
+ //# sourceMappingURL=chunk-4WMLJHW5.js.map