import { query } from "@anthropic-ai/claude-agent-sdk"; import { readFileSync, writeFileSync, mkdirSync, readdirSync, existsSync, } from "fs"; import { resolve, relative, join } from "path"; import { execSync } from "child_process"; // 3-build-team.ts — The Build Team with A2A coordination. // // Two-phase build with git-based type distribution: // Phase 1: MODELS agent builds domain types + repository interface // → merge feature/models INTO main // → publish artifacts to A2A hub // Phase 2: API + DATABASE branch from updated main (types in git history) // Each agent writes integration tests and verifies their work // Merge phase-2 branches only → final test const SPEC_PATH = resolve(import.meta.dir, "sample/spec.md"); const REPO_DIR = resolve(import.meta.dir, "../../../multi-agent"); const spec = readFileSync(SPEC_PATH, "utf-8"); // ── Agent definitions ─────────────────────────────────────────────────────── const agents = [ { name: "MODELS", branch: "feature/models", phase: 1, role: "Build the domain model layer AND the repository interface. Create: model/PaymentId.java (record wrapping UUID), model/PaymentStatus.java (enum: PENDING, APPROVED, DECLINED), model/Payment.java (record), model/CreatePaymentRequest.java (with Jakarta validation), and repository/PaymentRepository.java (interface with save, findById, findByMerchantId methods). Write unit tests for each model class testing both valid and invalid inputs.", }, { name: "API", branch: "feature/api", phase: 2, role: "Build the REST controller layer ONLY. Create controller/PaymentController.java that autowires the PaymentRepository interface (already in your worktree — do NOT create it). Write integration tests using @WebMvcTest with @MockBean for PaymentRepository — test POST /payments (valid + invalid requests), GET /payments/{id} (found + 404), and GET /merchants/{merchantId}/payments. Verify HTTP status codes, JSON response bodies, and validation error responses.", }, { name: "DATABASE", branch: "feature/database", phase: 2, role: "Build the repository implementation ONLY. Create repository/InMemoryPaymentRepository.java — a @Repository class implementing the PaymentRepository interface (already in your worktree — do NOT recreate it) using ConcurrentHashMap. Write integration tests that verify: save returns the stored payment, findById for existing and missing IDs, findByMerchantId filters correctly, and concurrent access is safe.", }, ]; // ── Helpers ────────────────────────────────────────────────────────────────── function run(cmd: string, cwd = REPO_DIR): string { return execSync(cmd, { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 120_000, }).trim(); } // ── A2A Hub ───────────────────────────────────────────────────────────────── // A simplified Agent-to-Agent protocol hub (JSON-RPC 2.0 over HTTP). // The team leader uses this to share MODELS artifacts with downstream agents. // This is what makes the two-phase build work — without it, API and DATABASE // agents would have to guess at model types and create stubs. const A2A_PORT = 9100; type AgentCard = { name: string; description: string; skills: string[]; status: "registered" | "working" | "completed" | "failed"; }; type Artifact = { name: string; path: string; content: string; }; const hub = { agents: new Map(), artifacts: new Map(), }; function handleA2A(method: string, params: any): any { switch (method) { case "agent/register": { hub.agents.set(params.name, { name: params.name, description: params.description ?? "", skills: params.skills ?? [], status: "registered", }); return { registered: true }; } case "agent/updateStatus": { const card = hub.agents.get(params.name); if (card) card.status = params.status; return { updated: true }; } case "agent/list": { const entries: Record = {}; for (const [k, v] of hub.agents) entries[k] = v; return entries; } case "artifacts/publish": { const existing = hub.artifacts.get(params.agent) ?? []; existing.push(...params.artifacts); hub.artifacts.set(params.agent, existing); return { published: params.artifacts.length }; } case "artifacts/get": { if (params.agent) return hub.artifacts.get(params.agent) ?? []; const all: Record = {}; for (const [k, v] of hub.artifacts) all[k] = v; return all; } default: throw new Error(`Unknown method: ${method}`); } } /** Call the A2A hub directly (team leader is an in-process client). */ function a2a(method: string, params: any): any { return handleA2A(method, params); } function startA2AHub() { const server = Bun.serve({ port: A2A_PORT, fetch(req) { const url = new URL(req.url); // A2A agent card discovery if (url.pathname === "/.well-known/agent.json") { return Response.json({ name: "Build Team Hub", description: "A2A hub coordinating parallel builder agents", url: `http://localhost:${A2A_PORT}/a2a`, version: "1.0", capabilities: { streaming: false }, skills: [ { name: "agent/register", description: "Register a builder agent" }, { name: "agent/updateStatus", description: "Update agent status", }, { name: "artifacts/publish", description: "Publish built artifacts", }, { name: "artifacts/get", description: "Fetch artifacts from agents", }, ], }); } // JSON-RPC 2.0 endpoint if (url.pathname === "/a2a" && req.method === "POST") { return req.json().then((body: any) => { try { const result = handleA2A(body.method, body.params ?? {}); return Response.json({ jsonrpc: "2.0", id: body.id, result }); } catch (e: any) { return Response.json({ jsonrpc: "2.0", id: body.id, error: { code: -32601, message: e.message }, }); } }); } return new Response("Not Found", { status: 404 }); }, }); console.log(` Hub listening on http://localhost:${A2A_PORT}`); console.log( ` Agent card: http://localhost:${A2A_PORT}/.well-known/agent.json` ); return server; } // ── Collect .java files from a worktree ───────────────────────────────────── function collectJavaFiles(dir: string, base: string = dir): Artifact[] { const artifacts: Artifact[] = []; if (!existsSync(dir)) return artifacts; for (const entry of readdirSync(dir, { withFileTypes: true })) { const full = join(dir, entry.name); if (entry.isDirectory()) { artifacts.push(...collectJavaFiles(full, base)); } else if (entry.name.endsWith(".java")) { artifacts.push({ name: entry.name, path: relative(base, full), content: readFileSync(full, "utf-8"), }); } } return artifacts; } // ── Build model files summary from A2A artifacts ──────────────────────────── // Extracts class/record/enum/interface signatures so phase-2 agents know // the exact types without having to read every file. function buildModelFilesSummary(artifacts: Artifact[]): string { const mainFiles = artifacts.filter((a) => a.path.includes("src/main/")); if (mainFiles.length === 0) return ""; let summary = "## Model Types & Repository Interface Already in Your Worktree\n\n"; summary += "These files are committed in git history. DO NOT modify, overwrite, or recreate them.\n\n"; for (const file of mainFiles) { summary += `### ${file.path}\n\`\`\`java\n`; const lines = file.content.split("\n"); for (const line of lines) { if ( line.match( /^\s*(public|protected|private)?\s*(static\s+)?(record|enum|class|interface|sealed)\s+/ ) || line.match( /^\s*(public|protected)?\s*[\w<>,\s]+\s+\w+\s*\([^)]*\)\s*[;{]/ ) || line.match(/^\s*@\w+/) || line.match(/^\s*package\s+/) ) { summary += line + "\n"; } } summary += "```\n\n"; } return summary; } // ── System prompt builder ─────────────────────────────────────────────────── function buildSystemPrompt( agent: (typeof agents)[0], modelsSummary?: string ): string { const modelContext = modelsSummary ? `\n\n${modelsSummary} CRITICAL RULES FOR MODEL TYPES: - The model types and repository interface above are COMMITTED in your worktree's git history - DO NOT modify, overwrite, or recreate ANY file under model/ or repository/PaymentRepository.java - Import and use types exactly as they are defined above - If a model uses PaymentId (record wrapping UUID), use PaymentId — NOT raw UUID - If PaymentStatus is an enum, use it — do NOT use String \n` : ""; // Narrow git add patterns — each agent only stages its own files const gitAddPattern = agent.name === "API" ? "git add 'payments-gateway/src/main/java/com/teya/payments/controller/**' 'payments-gateway/src/test/**/*.java'" : agent.name === "DATABASE" ? "git add 'payments-gateway/src/main/java/com/teya/payments/repository/InMemory*' 'payments-gateway/src/test/**/*.java'" : "git add 'payments-gateway/src/**/*.java'"; return `You are a specialist builder agent. Your role: ${agent.role} Read the project spec below carefully, then build ONLY your assigned layer. Follow the package structure and conventions from the spec exactly. IMPORTANT RULES: - Only create files relevant to YOUR layer — do not build other layers - Write thorough integration tests for everything you build - After building, run: ./mvnw test -pl payments-gateway -am - If tests pass, run: ${gitAddPattern} && git commit -m "feat: " && git push origin ${agent.branch} - If tests fail, read the error output carefully, fix the issues, and retry - NEVER modify pom.xml (neither root pom.xml nor payments-gateway/pom.xml) — the module and all dependencies are already configured - NEVER modify application.yml — it is already configured - NEVER modify or create any XML files — only create .java files under src/ - NEVER modify or create Application.java — it already exists${modelContext} ## Project Spec ${spec}`; } // ── Run one agent ─────────────────────────────────────────────────────────── type AgentResult = { name: string; success: boolean; cost: number; result: string; }; async function runAgent( agent: (typeof agents)[0], modelsSummary?: string ): Promise { const worktreeDir = resolve( REPO_DIR, `.worktrees/${agent.name.toLowerCase()}` ); const q = query({ prompt: `Build your assigned layer as described in your system prompt. Create the source files with proper validation, write integration tests that verify your work, run them, and push to ${agent.branch} if tests pass.`, options: { systemPrompt: buildSystemPrompt(agent, modelsSummary), allowedTools: ["Read", "Write", "Edit", "Bash", "Glob", "Grep"], maxTurns: 40, cwd: worktreeDir, permissionMode: "bypassPermissions", }, }); let result = ""; let cost = 0; for await (const msg of q) { if (msg.type === "assistant" && msg.message) { for (const block of msg.message.content) { if (block.type === "tool_use") { const detail = block.input?.file_path ?? block.input?.command ?? block.input?.pattern ?? ""; const short = typeof detail === "string" ? detail.slice(0, 80) : JSON.stringify(detail).slice(0, 80); console.log(` [${agent.name}] ${block.name}(${short})`); } } } if (msg.type === "result") { result = msg.result ?? ""; cost = msg.total_cost_usd; } } return { name: agent.name, success: true, cost, result }; } // ── Main ───────────────────────────────────────────────────────────────────── console.log("══════════════════════════════════"); console.log(" THE BUILD TEAM (A2A)"); console.log("══════════════════════════════════\n"); console.log(`Spec: ${SPEC_PATH}`); console.log(`Repo: ${REPO_DIR}\n`); // ── Step 0: Scaffold module on main ───────────────────────────────────────── // Scaffolds pom.xml, application.yml, AND Application.java so no agent // needs to create them — preventing merge conflicts on shared files. console.log("Step 0 · Scaffold payments-gateway module on main"); run("git checkout main"); const rootPom = readFileSync(resolve(REPO_DIR, "pom.xml"), "utf-8"); if (!rootPom.includes("payments-gateway")) { const updatedPom = rootPom.replace( "", " payments-gateway\n " ); writeFileSync(resolve(REPO_DIR, "pom.xml"), updatedPom); } const gatewayPom = ` 4.0.0 com.teya my-full-app 1.0-SNAPSHOT payments-gateway org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-validation org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-maven-plugin `; mkdirSync(resolve(REPO_DIR, "payments-gateway"), { recursive: true }); writeFileSync(resolve(REPO_DIR, "payments-gateway/pom.xml"), gatewayPom); mkdirSync(resolve(REPO_DIR, "payments-gateway/src/main/resources"), { recursive: true, }); writeFileSync( resolve(REPO_DIR, "payments-gateway/src/main/resources/application.yml"), "server:\n port: 3000\n" ); // Scaffold Application.java — both API and DATABASE might try to create this, // so we put it on main to prevent conflicts. const appJavaDir = resolve( REPO_DIR, "payments-gateway/src/main/java/com/teya/payments" ); mkdirSync(appJavaDir, { recursive: true }); writeFileSync( resolve(appJavaDir, "Application.java"), `package com.teya.payments; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } ` ); run("git add -A && git commit -m 'chore: scaffold payments-gateway module'"); console.log(" Module scaffolded (pom.xml + application.yml + Application.java) and committed.\n"); // ── Step 1: Start A2A Hub ─────────────────────────────────────────────────── console.log("Step 1 · Start A2A Hub"); const a2aServer = startA2AHub(); console.log(); // ── Step 2: Create MODELS worktree only ───────────────────────────────────── // Phase-2 worktrees are created AFTER MODELS merges into main, so they // inherit model types via git history (not filesystem copy). console.log("Step 2 · Create MODELS worktree & register on hub"); const modelsAgent = agents.find((a) => a.name === "MODELS")!; const modelsWorktreeDir = `.worktrees/${modelsAgent.name.toLowerCase()}`; try { run(`git worktree remove --force ${modelsWorktreeDir}`); } catch { /* doesn't exist yet */ } try { run(`git branch -D ${modelsAgent.branch}`); } catch { /* doesn't exist yet */ } run(`git worktree add ${modelsWorktreeDir} -b ${modelsAgent.branch}`); a2a("agent/register", { name: modelsAgent.name, description: modelsAgent.role, skills: ["models"], }); console.log( ` ${modelsAgent.name} → ${modelsWorktreeDir} (${modelsAgent.branch}) — registered\n` ); const startTime = Date.now(); let totalCost = 0; // ── Step 3: Phase 1 — MODELS builds, then merges into main ───────────────── // MODELS builds domain types + repository interface. // After completion, merge feature/models INTO main so phase-2 worktrees // branch from a main that already has these types in git history. console.log("Step 3 · Phase 1: MODELS agent builds domain types + repo interface\n"); a2a("agent/updateStatus", { name: "MODELS", status: "working" }); const modelsResult = await runAgent(modelsAgent); if (modelsResult.success) { totalCost += modelsResult.cost; a2a("agent/updateStatus", { name: "MODELS", status: "completed" }); console.log(`\n MODELS: OK $${modelsResult.cost.toFixed(4)}`); // Collect built .java files and publish to A2A hub const modelsWorktreeSrc = resolve( REPO_DIR, ".worktrees/models/payments-gateway/src" ); const artifacts = collectJavaFiles( modelsWorktreeSrc, resolve(REPO_DIR, ".worktrees/models/payments-gateway") ); a2a("artifacts/publish", { agent: "MODELS", artifacts }); console.log(` Published ${artifacts.length} artifact(s) to A2A hub`); // Merge feature/models INTO main — this is the key change. // Phase-2 worktrees will branch from this updated main, // so model types are in git history, not copied files. console.log(" Merging feature/models into main..."); run("git checkout main"); try { run(`git merge ${modelsAgent.branch} --no-edit`); console.log(" MERGED feature/models → main\n"); } catch { // MODELS agent may have touched pom.xml despite instructions — resolve // by keeping main's version of shared files and taking MODELS' src/ files. console.log(" CONFLICT on merge — auto-resolving (keep main's pom.xml)..."); run("git checkout --ours pom.xml payments-gateway/pom.xml"); run("git add -A && git commit --no-edit"); console.log(" RESOLVED & MERGED feature/models → main\n"); } } else { a2a("agent/updateStatus", { name: "MODELS", status: "failed" }); console.log(`\n MODELS: FAILED — aborting.\n`); a2aServer.stop(); process.exit(1); } // ── Step 4: Create phase-2 worktrees from updated main ────────────────────── // main now has model types in git history. Phase-2 agents branch from here, // so they see model types as committed code — not untracked files. const phase2Agents = agents.filter((a) => a.phase === 2); console.log("Step 4 · Create phase-2 worktrees from updated main & register"); // Build a summary of model types from A2A artifacts for system prompts const modelsArtifacts: Artifact[] = a2a("artifacts/get", { agent: "MODELS" }); const modelsSummary = buildModelFilesSummary(modelsArtifacts); for (const agent of phase2Agents) { const worktreeDir = `.worktrees/${agent.name.toLowerCase()}`; try { run(`git worktree remove --force ${worktreeDir}`); } catch { /* doesn't exist yet */ } try { run(`git branch -D ${agent.branch}`); } catch { /* doesn't exist yet */ } // Branch from main which now includes MODELS code in git history run(`git worktree add ${worktreeDir} -b ${agent.branch}`); a2a("agent/register", { name: agent.name, description: agent.role, skills: [agent.name.toLowerCase()], }); console.log( ` ${agent.name} → ${worktreeDir} (${agent.branch}) — registered (model types in git)` ); } console.log(); // ── Step 5: Phase 2 — API + DATABASE build in parallel ────────────────────── // Both agents have model types in git history. Each writes integration tests // and verifies their work in their own isolated worktree. console.log( `Step 5 · Phase 2: ${phase2Agents.map((a) => a.name).join(" + ")} in parallel\n` ); for (const a of phase2Agents) { a2a("agent/updateStatus", { name: a.name, status: "working" }); } const phase2Results = await Promise.allSettled( phase2Agents.map((a) => runAgent(a, modelsSummary)) ); console.log(); for (const [i, r] of phase2Results.entries()) { const agent = phase2Agents[i]; if (r.status === "fulfilled") { totalCost += r.value.cost; a2a("agent/updateStatus", { name: agent.name, status: "completed" }); console.log(` ${agent.name}: OK $${r.value.cost.toFixed(4)}`); // Publish phase-2 artifacts to hub const worktreeSrc = resolve( REPO_DIR, `.worktrees/${agent.name.toLowerCase()}/payments-gateway/src` ); const artifacts = collectJavaFiles( worktreeSrc, resolve( REPO_DIR, `.worktrees/${agent.name.toLowerCase()}/payments-gateway` ) ); a2a("artifacts/publish", { agent: agent.name, artifacts }); console.log(` Published ${artifacts.length} artifact(s) to hub`); } else { a2a("agent/updateStatus", { name: agent.name, status: "failed" }); console.log(` ${agent.name}: FAILED ${r.reason}`); } } const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); console.log(`\nAll agents finished in ${elapsed}s\n`); // ── Step 6: A2A Hub summary ───────────────────────────────────────────────── console.log("Step 6 · A2A Hub summary"); for (const [name, card] of hub.agents) { const count = hub.artifacts.get(name)?.length ?? 0; console.log(` ${name}: ${card.status}, ${count} artifact(s)`); } console.log(); // ── Step 7: Merge phase-2 branches into main ──────────────────────────────── // feature/models is already merged (Step 3). Only merge API + DATABASE. console.log("Step 7 · Merge phase-2 branches into main"); run("git checkout main"); for (const [i, agent] of phase2Agents.entries()) { if (phase2Results[i].status !== "fulfilled") { console.log(` SKIP ${agent.branch} (agent failed)`); continue; } try { run(`git merge ${agent.branch} --no-edit`); console.log(` MERGED ${agent.branch}`); } catch { console.log(` CONFLICT ${agent.branch} — spawning resolver...`); const resolverQ = query({ prompt: "There is a git merge conflict. Run 'git diff' to see the conflicts, then resolve them in the source files. After resolving, run 'git add -A && git commit --no-edit' to complete the merge. Then run './mvnw test -pl payments-gateway -am' to verify everything still works.", options: { systemPrompt: `You are a merge conflict resolver. Fix conflicts so the code compiles and tests pass. IMPORTANT: model/ files and repository/PaymentRepository.java (the interface) were built by the MODELS agent and are the source of truth. Always keep those versions — never overwrite them with conflicting versions. ## Project Spec ${spec}`, allowedTools: ["Read", "Write", "Edit", "Bash", "Glob", "Grep"], maxTurns: 15, cwd: REPO_DIR, permissionMode: "bypassPermissions", }, }); for await (const msg of resolverQ) { if (msg.type === "assistant" && msg.message) { for (const block of msg.message.content) { if (block.type === "tool_use") { const detail = block.input?.file_path ?? block.input?.command ?? ""; const short = typeof detail === "string" ? detail.slice(0, 80) : ""; console.log(` [RESOLVER] ${block.name}(${short})`); } } } if (msg.type === "result") { totalCost += msg.total_cost_usd; console.log(` RESOLVED $${msg.total_cost_usd.toFixed(4)}`); } } } } console.log(); // ── Step 8: Final test ────────────────────────────────────────────────────── console.log("Step 8 · Final test suite"); try { run("./mvnw test -pl payments-gateway -am", REPO_DIR); console.log(" All tests pass\n"); } catch (e: any) { console.log(" Tests failed after merge\n"); console.log( e.stderr?.slice?.(-500) ?? e.stdout?.slice?.(-500) ?? e.message ); console.log(); } // ── Step 9: Cleanup ───────────────────────────────────────────────────────── console.log("Step 9 · Cleanup"); for (const agent of agents) { try { run(`git worktree remove .worktrees/${agent.name.toLowerCase()}`); } catch { /* already removed */ } } try { run("rm -rf .worktrees"); } catch { /* ignore */ } a2aServer.stop(); console.log(" Worktrees removed, A2A Hub stopped.\n"); // ── Summary ───────────────────────────────────────────────────────────────── console.log("══════════════════════════════════"); console.log(" BUILD COMPLETE"); console.log(` Agents: ${agents.length}`); console.log(` Time: ${elapsed}s`); console.log(` Cost: $${totalCost.toFixed(4)}`); console.log("══════════════════════════════════");