syntaxai/tdd.md · commit 9d21dfa

Live agent profile: list kata attempts with progress

/agents/:name now hits Forgejo for the user (404s cleanly if missing)
and their repos, then computes per-attempt progress in parallel via
the existing parseCommit pipeline. The page shows a table per kata:
verified count out of the kata's total steps, and a colored red /
green / refactor tally — same vocabulary as the repo page.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
author
syntaxai <[email protected]>
date
2026-05-03 17:30:55 +01:00
parent
312fbc3
commit
9d21dfa1ad310b2c65a65d22a5f578c92d8ea9c0

1 file changed · +50 −3

modified src/server.ts +50 −3
@@ -341,10 +341,57 @@ const server = Bun.serve({
341341 "/agents": htmlResponse(AGENTS_INDEX_HTML),
342342 "/agents/register": htmlResponse(REGISTER_HTML),
343343 "/agents/:name": async (req) => {
344+ const name = req.params.name;
345+ const userRes = await fetch(`${FORGEJO_INTERNAL}/api/v1/users/${encodeURIComponent(name)}`);
346+ if (userRes.status === 404) {
347+ const html = await renderPage({
348+ title: `${name} — agents — tdd.md`,
349+ bodyMarkdown: `# agents / ${name}\n\n> No agent registered with this name.\n\n[← all agents](/agents) · [register your own →](/agents/register)`,
350+ ogPath: `https://tdd.md/agents/${name}`,
351+ active: "agents",
352+ });
353+ return htmlResponse(html, 404);
354+ }
355+ const reposRes = await fetch(`${FORGEJO_INTERNAL}/api/v1/users/${encodeURIComponent(name)}/repos?limit=50`);
356+ const repos = reposRes.ok ? ((await reposRes.json()) as { name: string; description: string }[]) : [];
357+
358+ let body = `# agents / ${name}\n\n`;
359+ if (repos.length === 0) {
360+ body += "> Registered, but no kata attempts yet.\n\n[← all agents](/agents)";
361+ } else {
362+ const progressByRepo = await Promise.all(
363+ repos.map(async (r) => {
364+ const cRes = await fetch(`${FORGEJO_INTERNAL}/api/v1/repos/${encodeURIComponent(name)}/${encodeURIComponent(r.name)}/commits?limit=50&stat=false`);
365+ const commits = cRes.ok ? ((await cRes.json()) as { commit: { message: string } }[]) : [];
366+ return { repo: r, progress: computeProgress(commits) };
367+ }),
368+ );
369+
370+ const totals: Record<string, number> = {};
371+ for (const r of repos) {
372+ try {
373+ const game = await loadGame(r.name);
374+ totals[r.name] = game.steps.length;
375+ } catch {
376+ // unknown kata, no total
377+ }
378+ }
379+
380+ body += "## attempts\n\n";
381+ body += "| kata | verified | phases |\n|---|---|---|\n";
382+ for (const { repo: r, progress } of progressByRepo) {
383+ const total = totals[r.name];
384+ const verified = progress.verifiedSteps.size;
385+ const counter = total !== undefined ? `${verified} / ${total}` : `${verified} / ?`;
386+ const phases = `<span class="red">red ${progress.redCount}</span> · <span class="green">green ${progress.greenCount}</span> · <span class="blue">refactor ${progress.refactorCount}</span>`;
387+ body += `| [${r.name}](/${name}/${r.name}) | ${counter} | ${phases} |\n`;
388+ }
389+ }
390+
344391 const html = await renderPage({
345- title: `${req.params.name} — agents — tdd.md`,
346- bodyMarkdown: `# agents / ${req.params.name}\n\n> Not yet registered or no attempts.\n\nWhen this agent submits a run, their commits and verdicts will appear here.`,
347- ogPath: `https://tdd.md/agents/${req.params.name}`,
392+ title: `${name} — agents — tdd.md`,
393+ bodyMarkdown: body,
394+ ogPath: `https://tdd.md/agents/${name}`,
348395 active: "agents",
349396 });
350397 return htmlResponse(html);