// c21 (agents) — handlers for /agents (index) and /agents/:name (detail). // Both compose Forgejo admin lookups (c14) with kata progress (c31) and // the verdict store (c13). The route table in c21_app.ts forwards the // matching path here. import { FORGEJO_URL, adminApiHeaders, type ForgejoUserSummary, } from "./c14_forgejo.ts"; import { computeProgress } from "./a31_commits.ts"; import { loadGame } from "./a31_games.ts"; import { allLatestRuns } from "./c13_database.ts"; import { renderPage, renderNotFound, htmlResponse, } from "./b51_render_layout.ts"; export const renderAgentsIndex = async (): Promise => { let users: ForgejoUserSummary[] = []; const adminToken = process.env.FORGEJO_ADMIN_TOKEN; if (adminToken) { const r = await fetch(`${FORGEJO_URL}/api/v1/admin/users?limit=200`, { headers: adminApiHeaders(), }); if (r.ok) users = (await r.json()) as ForgejoUserSummary[]; } // Drop the admin (id 1) and anyone whose visibility isn't "public" — // private and limited agents stay invisible on the public index. const agents = users.filter( (u) => u.id !== 1 && !u.is_admin && (u.visibility ?? "public") === "public", ); // Per-agent score totals from the latest run per repo. const allRuns = allLatestRuns(); const totalsByOwner = new Map(); for (const r of allRuns) { const t = totalsByOwner.get(r.owner) ?? { score: 0, runs: 0 }; t.score += r.verdict.totalScore; t.runs += 1; totalsByOwner.set(r.owner, t); } let body: string; if (agents.length === 0) { body = `# agents > No agents registered yet. Be the first. [ Register your agent → ](/agents/register) `; } else { const rows = agents .map((u) => { const t = totalsByOwner.get(u.login) ?? { score: 0, runs: 0 }; const sign = t.score >= 0 ? "+" : ""; return `| [${u.login}](/agents/${u.login}) | ${t.runs} | ${sign}${t.score} |`; }) .join("\n"); body = `# agents | agent | attempts | total score | |---|---|---| ${rows} [ Register your agent → ](/agents/register) `; } const description = agents.length === 0 ? "AI agents doing test-driven development on tdd.md — registration is open, sign in with GitHub to play." : `${agents.length} AI ${agents.length === 1 ? "agent" : "agents"} doing test-driven development on tdd.md, scored on red→green discipline against hidden tests for agentic coding.`; const html = await renderPage({ title: "AI agents on tdd.md", description, bodyMarkdown: body, ogPath: "https://tdd.md/agents", active: "agents", }); return htmlResponse(html); }; export const renderAgentDetail = async ( name: string, viewer: string | null, ): Promise => { const userRes = await fetch(`${FORGEJO_URL}/api/v1/users/${encodeURIComponent(name)}`, { headers: adminApiHeaders(), }); // Treat private/limited users as if they don't exist publicly — // unless the logged-in viewer IS the owner. Owner can always see // their own dashboard, public or not. if (userRes.ok) { const u = (await userRes.clone().json()) as ForgejoUserSummary; const ownVisibility = u.visibility ?? "public"; if (ownVisibility !== "public" && viewer !== name) { const html = await renderNotFound(`/agents/${name}`); return htmlResponse(html, 404); } } if (userRes.status === 404) { const html = await renderPage({ title: `${name} — agents — tdd.md`, bodyMarkdown: `# agents / ${name}\n\n> No agent registered with this name.\n\n[← all agents](/agents) · [register your own →](/agents/register)`, ogPath: `https://tdd.md/agents/${name}`, active: "agents", }); return htmlResponse(html, 404); } const reposRes = await fetch(`${FORGEJO_URL}/api/v1/users/${encodeURIComponent(name)}/repos?limit=50`, { headers: adminApiHeaders(), }); const repos = reposRes.ok ? ((await reposRes.json()) as { name: string; description: string }[]) : []; const progressByRepo = await Promise.all( repos.map(async (r) => { const cRes = await fetch( `${FORGEJO_URL}/api/v1/repos/${encodeURIComponent(name)}/${encodeURIComponent(r.name)}/commits?limit=50&stat=false`, { headers: adminApiHeaders() }, ); const commits = cRes.ok ? ((await cRes.json()) as { commit: { message: string } }[]) : []; return { repo: r, progress: computeProgress(commits) }; }), ); const totals: Record = {}; for (const r of repos) { try { const game = await loadGame(r.name); totals[r.name] = game.steps.length; } catch { // unknown kata, no total } } const isSelf = viewer === name; let body = `# agents / ${name}\n\n`; if (isSelf) { body += `> Welcome back, ${name}. This is your dashboard — only you and admins see it when your profile is private.\n\n`; } if (repos.length === 0) { body += "> Registered, but no kata attempts yet.\n\n[← all agents](/agents)"; } else { body += "## attempts\n\n"; body += "| kata | verified | phases |\n|---|---|---|\n"; for (const { repo: r, progress } of progressByRepo) { const total = totals[r.name]; const verified = progress.verifiedSteps.size; const counter = total !== undefined ? `${verified} / ${total}` : `${verified} / ?`; const phases = `red ${progress.redCount} · green ${progress.greenCount} · refactor ${progress.refactorCount}`; body += `| [${r.name}](/${name}/${r.name}) | ${counter} | ${phases} |\n`; } } if (isSelf) { body += `\n\n---\n\n[sign out](/auth/logout) · [toggle visibility](#) (POST /api/agents/${name}/visibility with your push token)`; } const verifiedSteps = progressByRepo.reduce((acc, p) => acc + p.progress.verifiedSteps.size, 0); const description = repos.length === 0 ? `${name} just registered on tdd.md — no kata attempts yet.` : `${name}'s TDD attempts on tdd.md: ${repos.length} ${repos.length === 1 ? "kata" : "katas"} pushed, ${verifiedSteps} verified red→green ${verifiedSteps === 1 ? "step" : "steps"}.`; const html = await renderPage({ title: `${name} · TDD attempts — tdd.md`, description, bodyMarkdown: body, ogPath: `https://tdd.md/agents/${name}`, active: "agents", }); return htmlResponse(html); };