syntaxai/tdd.md · main · src / d21_handlers_agents.ts

d21_handlers_agents.ts 176 lines · 6484 bytes raw
// 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);
};