syntaxai/tdd.md · main · src / d21_handlers_agents.ts
// 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<Response> => {
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<string, { score: number; runs: number }>();
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<Response> => {
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<string, number> = {};
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 = `<span class="red">red ${progress.redCount}</span> · <span class="green">green ${progress.greenCount}</span> · <span class="blue">refactor ${progress.refactorCount}</span>`;
body += `| [${r.name}](/${name}/${r.name}) | ${counter} | ${phases} |\n`;
}
}
if (isSelf) {
body += `\n\n---\n\n[sign out](/auth/logout) · [toggle visibility](#) <span class="muted">(POST /api/agents/${name}/visibility with your push token)</span>`;
}
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);
};