Skip to content

feat: surface OpenMythos governance scorecard in dashboard and MCP#60

Open
djimit wants to merge 2 commits into
mainfrom
feat/openmythos-scorecard
Open

feat: surface OpenMythos governance scorecard in dashboard and MCP#60
djimit wants to merge 2 commits into
mainfrom
feat/openmythos-scorecard

Conversation

@djimit

@djimit djimit commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Summary

P1 from the APEX validation plan: the OpenMythos Governance Benchmark — the platform's most differentiated capability, with 39 pre-registered APEX research rounds behind it — had zero product surface: no dashboard page, no MCP tools, and getGovernanceTrend had no consumer. This PR ships the read path end to end.

Server

  • OpenMythosEvalService.listRuns(limit) — recent eval runs with subject model + oracle/judge provenance from run metadata
  • OpenMythosEvalService.getLeaderboard() — latest completed score per agent, best first
  • GET /api/openmythos/runs and GET /api/openmythos/leaderboard (read:evidence)

MCP server

  • djimitflo_openmythos_leaderboard — latest score per agent with category breakdown
  • djimitflo_openmythos_score — one agent's latest score + 10-run trend
  • Read-only over openmythos_eval_runs; evals are started via REST, not MCP

Dashboard

  • New Governance page (/governance, ShieldCheck nav item): per-agent leaderboard with 0–5 score, trend arrow, category chips colored by score, and a recent-runs list showing subject model, status, and oracle provenance

Validation

  • Server: 1,219 tests pass (+6 new), type-check + lint clean
  • MCP server: 7 tests pass (+3 new)
  • Dashboard: 26 tests pass (+2 new)
  • Live end-to-end: booted the server on a temp DB with a bootstrap admin, logged in, seeded an eval run, and verified both endpoints return correct JSON through auth + RBAC

🤖 Generated with Claude Code

The OpenMythos benchmark (39 pre-registered APEX rounds) had zero product
surface: no dashboard page, no MCP tools, and getGovernanceTrend had no
consumer. This adds the read path end to end:

- server: OpenMythosEvalService.listRuns + getLeaderboard, exposed as
  GET /api/openmythos/runs and /leaderboard (read:evidence)
- mcp-server: djimitflo_openmythos_leaderboard + djimitflo_openmythos_score
  tools (read-only over openmythos_eval_runs)
- dashboard: /governance page — per-agent leaderboard with category
  chips and trend, plus recent eval runs with oracle provenance

Verified live: booted server on a temp DB, seeded a run, both endpoints
return correct data through auth + RBAC.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kilo-code-bot

kilo-code-bot Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Kilo Code Review could not run — your account is out of credits.

Add credits or switch to a free model to enable reviews on this change.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the OpenMythos Governance Benchmark feature, adding a new Governance Scorecard page to the dashboard, corresponding API endpoints, backend database schema updates, and new MCP tools. Feedback focuses on optimizing database performance by replacing N+1 queries and correlated subqueries with window functions in both the backend service and MCP tools, as well as adding optional chaining to prevent potential runtime crashes in the UI when rendering run IDs.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +624 to +634
getLeaderboard(): AgentScore[] {
const agents = this.db.prepare(`
SELECT DISTINCT agent_id FROM openmythos_eval_runs WHERE status = 'completed'
`).all() as Array<{ agent_id: string }>;

// ponytail: one getAgentScore query per agent — fine at fleet sizes, batch when agents > ~1k
return agents
.map((a) => this.getAgentScore(a.agent_id))
.filter((s): s is AgentScore => s !== null)
.sort((a, b) => b.overallScore - a.overallScore);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation of getLeaderboard() suffers from the $N+1$ query problem (specifically $2N+1$). It first queries all distinct agent IDs, and then for each agent, it performs two separate database queries inside getAgentScore() (one to get the latest run and another to get the last 5 runs for trend calculation).\n\nAt scale, this will result in significant performance degradation. Since SQLite 3.25+ supports window functions, we can fetch the latest and second-latest completed runs for all agents in a single, highly efficient query using ROW_NUMBER() OVER (PARTITION BY agent_id ORDER BY finished_at DESC). This reduces the database roundtrips from $2N+1$ to exactly $1$.

  getLeaderboard(): AgentScore[] {\n    const rows = this.db.prepare(\n      \"WITH ranked_runs AS (\" +\n      \"  SELECT \" +\n      \"    id, agent_id, overall_score, completed_cases, finished_at, metadata,\" +\n      \"    ROW_NUMBER() OVER (PARTITION BY agent_id ORDER BY finished_at DESC) as rn\" +\n      \"  FROM openmythos_eval_runs\" +\n      \"  WHERE status = 'completed'\" +\n      \")\" +\n      \"SELECT id, agent_id, overall_score, completed_cases, finished_at, metadata, rn\" +\n      \"FROM ranked_runs\" +\n      \"WHERE rn <= 2\" +\n      \"ORDER BY agent_id, rn\"\n    ).all() as Array<{\n      id: string;\n      agent_id: string;\n      overall_score: number;\n      completed_cases: number;\n      finished_at: string;\n      metadata: string;\n      rn: number;\n    }>;\n\n    const agentGroups = new Map<string, typeof rows>();\n    for (const row of rows) {\n      if (!agentGroups.has(row.agent_id)) {\n        agentGroups.set(row.agent_id, []);\n      }\n      agentGroups.get(row.agent_id)!.push(row);\n    }\n\n    const leaderboard: AgentScore[] = [];\n    for (const [agentId, runs] of agentGroups.entries()) {\n      const latest = runs[0];\n      const prev = runs[1];\n\n      let trend: 'improving' | 'stable' | 'declining' = 'stable';\n      if (prev) {\n        const diff = latest.overall_score - prev.overall_score;\n        if (diff > 0.1) trend = 'improving';\n        else if (diff < -0.1) trend = 'declining';\n      }\n\n      let metadata: { category_scores?: Record<string, number> } = {};\n      try {\n        metadata = JSON.parse(latest.metadata || '{}');\n      } catch {}\n\n      leaderboard.push({\n        agentId,\n        overallScore: latest.overall_score,\n        categoryScores: metadata.category_scores || {},\n        totalCases: latest.completed_cases,\n        lastEvalAt: latest.finished_at,\n        trend,\n      });\n    }\n\n    return leaderboard.sort((a, b) => b.overallScore - a.overallScore);\n  }

Comment on lines +39 to +47
const rows = db.prepare(`
SELECT r.id, r.agent_id, r.status, r.total_cases, r.completed_cases, r.overall_score, r.started_at, r.finished_at, r.metadata
FROM openmythos_eval_runs r
WHERE r.status = 'completed' AND r.finished_at = (
SELECT MAX(r2.finished_at) FROM openmythos_eval_runs r2
WHERE r2.agent_id = r.agent_id AND r2.status = 'completed'
)
ORDER BY r.overall_score DESC
`).all() as RunRow[];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current query uses a correlated subquery (r.finished_at = (SELECT MAX(r2.finished_at) ...)) to find the latest completed run per agent. This results in an $O(M^2)$ operation where SQLite has to perform a full table scan of openmythos_eval_runs for every single row in the table.\n\nWe can optimize this to a single scan using window functions (ROW_NUMBER() OVER (PARTITION BY agent_id ORDER BY finished_at DESC)), which is standard, highly efficient, and performs significantly better as the history grows.

      const rows = db.prepare(\n        \"WITH ranked_runs AS (\" +\n        \"  SELECT id, agent_id, status, total_cases, completed_cases, overall_score, started_at, finished_at, metadata,\" +\n        \"         ROW_NUMBER() OVER (PARTITION BY agent_id ORDER BY finished_at DESC) as rn\" +\n        \"  FROM openmythos_eval_runs\" +\n        \"  WHERE status = 'completed'\" +\n        \")\" +\n        \"SELECT id, agent_id, status, total_cases, completed_cases, overall_score, started_at, finished_at, metadata\" +\n        \"FROM ranked_runs\" +\n        \"WHERE rn = 1\" +\n        \"ORDER BY overall_score DESC\"\n      ).all() as RunRow[];

{runs.map((run) => (
<div key={run.id} className="flex items-center gap-3 flex-wrap border-l-2 border-border pl-3 py-1">
<FlaskConical className="w-3.5 h-3.5 text-foreground-tertiary" />
<span className="text-xs font-mono text-foreground-tertiary">{run.id.slice(0, 8)}</span>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If run.id is ever undefined or null due to an unexpected API response or malformed data, calling .slice(0, 8) directly will throw a TypeError and crash the entire page. Using optional chaining with a fallback prevents runtime crashes and ensures a more robust UI.

Suggested change
<span className="text-xs font-mono text-foreground-tertiary">{run.id.slice(0, 8)}</span>
<span className=\"text-xs font-mono text-foreground-tertiary\">{run.id?.slice(0, 8) ?? 'n/a'}</span>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9062eed5a8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


// ponytail: one getAgentScore query per agent — fine at fleet sizes, batch when agents > ~1k
return agents
.map((a) => this.getAgentScore(a.agent_id))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Guard leaderboard against malformed metadata

When any completed run has malformed metadata, this new aggregate path calls getAgentScore(), whose JSON.parse(run.metadata || '{}') is unguarded; one bad agent row will throw and make /api/openmythos/leaderboard fail for the whole dashboard even though other agents have valid scores and listRuns() explicitly tolerates malformed metadata. Please isolate malformed rows or share the safe metadata parsing used by the new run list/MCP code.

Useful? React with 👍 / 👎.

…ax query

- getAgentScore: guard JSON.parse so one malformed run row cannot sink
  the whole leaderboard (regression test added)
- mcp leaderboard: replace correlated subquery with SQLite grouped-max
- dashboard: null-safe run.id slice

Kept the per-agent query in getLeaderboard as-is; it is ponytail-marked
with its ceiling (batch when agents > ~1k).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant