From 9b29c3943c0ef12f4274f7de96c0712b1d23129b Mon Sep 17 00:00:00 2001 From: 0xRoy <1997roylee@gmail.com> Date: Tue, 23 Jun 2026 18:04:37 +0800 Subject: [PATCH 1/2] Add review past decisions skill design --- ...2026-06-23-review-past-decisions-design.md | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-23-review-past-decisions-design.md diff --git a/docs/superpowers/specs/2026-06-23-review-past-decisions-design.md b/docs/superpowers/specs/2026-06-23-review-past-decisions-design.md new file mode 100644 index 00000000..ed709a64 --- /dev/null +++ b/docs/superpowers/specs/2026-06-23-review-past-decisions-design.md @@ -0,0 +1,70 @@ +# Review Past Decisions Skill Design + +Date: 2026-06-23 + +## Goal + +Add a second bundled Ponytrail skill named `review-past-decisions`. The skill helps an agent inspect prior decision evidence before drafting or revising a new plan, so the next plan carries forward accepted constraints, avoids repeating rejected paths, and names any intentional change in direction. + +## Approved Approach + +Create a separate installable skill under `bundled-skills/review-past-decisions/`. + +This keeps the existing `pony-trail` skill focused on recording file-change snapshots. The new skill focuses on planning-time review and can be installed through the existing `ponytrail skills install ` surface. + +## Skill Behavior + +The skill should tell an agent to use it before proposing a new plan when prior decisions may matter. It should guide the agent to inspect local evidence sources such as: + +- `ponytrail history --details` +- `.pony-trail/sessions//tree.md` +- relevant docs, accepted specs, prior plans, or issue notes already present in the workspace + +The required output is a short decision carry-forward with: + +- decisions to preserve +- constraints that still apply +- rejected or superseded approaches to avoid +- open questions or stale decisions that need user confirmation +- the plan implications for the new work + +The skill must not invent history. If no relevant evidence exists, it should say so and continue with a normal plan using current context. + +## Files + +Add: + +- `bundled-skills/review-past-decisions/SKILL.md` +- `bundled-skills/review-past-decisions/agents/openai.yaml` + +Update: + +- `tests/skill-installer.test.ts` + +No helper scripts are needed in the first version. + +## Installation Flow + +The existing installer already resolves folders under `bundled-skills/` by skill name. After this change, users should be able to run: + +```bash +ponytrail skills install review-past-decisions +``` + +The same installer path should also render a Cursor rule for Cursor targets. + +## Testing + +Use test-first implementation: + +1. Add a failing installer test proving `resolveInstallSkillSource("review-past-decisions")` returns a bundled source whose `SKILL.md` has the expected name. +2. Add a failing installer test proving the skill installs into at least one directory target and one Cursor rule target. +3. Implement the skill files. +4. Run the focused installer tests, then the full `rtk bun run check` gate. + +## Non-Goals + +- Do not change the runtime manifest court skills in this slice. +- Do not add a summarizer script yet. +- Do not broaden the README unless explicitly requested. +- Do not alter the existing `pony-trail` snapshot skill behavior. From c03d5a83b999676304b699b800aefc121bd7d30a Mon Sep 17 00:00:00 2001 From: 0xRoy <1997roylee@gmail.com> Date: Thu, 25 Jun 2026 15:06:05 +0800 Subject: [PATCH 2/2] Add pony subagent rounds to requirement court --- docs/architecture.md | 9 +- src/cli.ts | 86 +++++++++++-- src/runtimes/ponytrail/requirement-court.ts | 135 ++++++++++++++++++-- tests/cli.test.ts | 113 ++++++++++++++++ tests/requirement-court.test.ts | 96 +++++++++++++- 5 files changed, 407 insertions(+), 32 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 81911e7d..f2fe06a1 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -32,7 +32,8 @@ src/ - create project onboarding files - run the requirements brainstorm gate - draft a goal contract from a raw request -- run the 4-bot / 3-approval requirement court plus a non-voting Judge +- run each requirement-court pony through a `PonySubagentRunner` +- tally the 4-bot / 3-approval requirement court plus a non-voting Judge - read Pony Trail snapshot history and plan file-level reverts - print visible role-bot discussion before worker execution is allowed @@ -46,6 +47,8 @@ The CLI should call this runtime through its exported interface instead of knowi The runtime treats `provider` and `name` as configuration values. This keeps goal discussion model selection editable in `.ponytrail/manifest.json` without coupling the core runtime to a specific vendor SDK or CLI flag shape. +`requirement-court.ts` exposes a `PonySubagentRunner` seam. The default runner is deterministic so the CLI works offline, while tests and future plugins can inject process-backed or model-backed subagents. The court invokes the runner once per configured voter in each manifest-defined round, stores the visible round discussion, and tallies final-round votes through the manifest decision rule. + ## Plugins `src/plugins` is the seam for things that vary by environment or integration: @@ -107,8 +110,8 @@ Human request -> ponytrail runtime -> requirements brainstorm -> ask human for details when unclear - -> Product Manager, Project Manager, Engineer, and Testing bots discuss - -> visible role-bot discussion is printed + -> Product Manager, Project Manager, Engineer, and Testing pony subagents discuss by round + -> visible round discussion is printed -> 3 of 4 voting bots approve the direction -> Requirement Judge summarizes and merges one detailed requirement -> human confirms the direction diff --git a/src/cli.ts b/src/cli.ts index 4d924a47..4b20aaeb 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -16,6 +16,7 @@ import { createOnboardingFiles, type InstructionContext, loadManifest, + type PonySubagentRunner, planSnapshotRevert, prepareGoalDiscussion, type RecordedSnapshotCommit, @@ -69,6 +70,7 @@ const skillInstallHistorySessionId = "ponytrail-skills"; export interface BuildProgramOptions { cwd?: string; streamRunner?: CliStreamRunner; + ponySubagentRunner?: PonySubagentRunner | undefined; clarificationPrompter?: GoalClarificationPrompter; projectNamePrompter?: ProjectNamePrompter; revertApprovalPrompter?: RevertApprovalPrompter; @@ -77,6 +79,7 @@ export interface BuildProgramOptions { export function buildProgram(options: BuildProgramOptions = {}): Command { const rootDir = options.cwd ?? process.cwd(); const clarificationPrompter = options.clarificationPrompter ?? promptForGoalClarifications; + const ponySubagentRunner = options.ponySubagentRunner; const projectNamePrompter = options.projectNamePrompter ?? promptForProjectName; const revertApprovalPrompter = options.revertApprovalPrompter ?? promptForRevertApproval; const program = new Command(); @@ -156,11 +159,28 @@ export function buildProgram(options: BuildProgramOptions = {}): Command { rootDir, clarificationPrompter, manifestPath: commandOptions.manifest, - printJson: commandOptions.json, + ponySubagentRunner, + jsonOutput: commandOptions.json ? "contract" : false, }); }, ); + program + .command("ponyrace") + .description("Run the requirement pony subagents and summarize their direction.") + .argument("", "raw requirement request") + .option("-m, --manifest ", "manifest path", defaultManifestPath) + .option("--json", "print JSON output", false) + .action(async (requestParts: string[], commandOptions: { manifest: string; json: boolean }) => { + await runGoalFlow(requestParts, { + rootDir, + clarificationPrompter, + manifestPath: commandOptions.manifest, + ponySubagentRunner, + jsonOutput: commandOptions.json ? "court" : false, + }); + }); + program .command("vote") .description("Apply the manifest decision rule to a JSON array of bot votes.") @@ -200,7 +220,8 @@ export function buildProgram(options: BuildProgramOptions = {}): Command { rootDir, clarificationPrompter, manifestPath: commandOptions.manifest, - printJson: false, + ponySubagentRunner, + jsonOutput: false, }); }, ); @@ -309,7 +330,8 @@ interface RunGoalFlowInput { rootDir: string; clarificationPrompter: GoalClarificationPrompter; manifestPath: string; - printJson: boolean; + ponySubagentRunner?: PonySubagentRunner | undefined; + jsonOutput: "contract" | "court" | false; } async function runGoalFlow(requestParts: string[], input: RunGoalFlowInput): Promise { @@ -318,7 +340,7 @@ async function runGoalFlow(requestParts: string[], input: RunGoalFlowInput): Pro const preparedDiscussion = prepareGoalDiscussion(request, { manifest }); if (preparedDiscussion.status === "needs_clarification") { - if (input.printJson) { + if (input.jsonOutput) { console.log(JSON.stringify(preparedDiscussion, null, 2)); return; } @@ -349,22 +371,67 @@ async function runGoalFlow(requestParts: string[], input: RunGoalFlowInput): Pro return; } - printRequirementCourtResult(runRequirementCourt(clarifiedDiscussion.contract, { manifest })); + printRequirementCourtResult( + await runRequirementCourt(clarifiedDiscussion.contract, { + manifest, + ponySubagentRunner: input.ponySubagentRunner, + }), + ); return; } - if (input.printJson) { + if (input.jsonOutput === "contract") { console.log(JSON.stringify(preparedDiscussion.contract, null, 2)); return; } - printRequirementCourtResult(runRequirementCourt(preparedDiscussion.contract, { manifest })); + const result = await runRequirementCourt(preparedDiscussion.contract, { + manifest, + ponySubagentRunner: input.ponySubagentRunner, + }); + + if (input.jsonOutput === "court") { + console.log(JSON.stringify(result, null, 2)); + return; + } + + printRequirementCourtResult(result); } function printRequirementCourtResult(result: RequirementCourtResult): void { console.log(pc.cyan("Requirement discussion")); - for (const entry of result.discussion) { - console.log(entry.line); + for (const round of result.rounds) { + console.log(""); + console.log(pc.cyan(`Round ${round.round}`)); + for (const entry of round.discussion) { + console.log(entry.line); + } + } + + const transcriptRounds = result.rounds + .map((round) => ({ + round: round.round, + lines: round.discussion.flatMap((entry) => + entry.transcript.map((line) => `${entry.botId}: ${line}`), + ), + })) + .filter((round) => round.lines.length > 0); + + if (transcriptRounds.length > 0) { + console.log(""); + console.log(pc.cyan("Pony transcripts")); + for (const round of transcriptRounds) { + console.log(pc.cyan(`Round ${round.round}`)); + for (const line of round.lines) { + console.log(line); + } + } + } + + console.log(""); + console.log(pc.cyan("Final votes")); + for (const vote of result.votes) { + console.log(`${vote.botId}: ${vote.vote} (${vote.confidence})`); } console.log(""); @@ -391,7 +458,6 @@ function printList(label: string, values: string[]): void { console.log(`- ${value}`); } } - function printSnapshotHistory( sessions: Array<{ sessionId: string; commits: SnapshotCommit[] }>, mode: SnapshotHistoryMode, diff --git a/src/runtimes/ponytrail/requirement-court.ts b/src/runtimes/ponytrail/requirement-court.ts index 8fc6c393..9e12f000 100644 --- a/src/runtimes/ponytrail/requirement-court.ts +++ b/src/runtimes/ponytrail/requirement-court.ts @@ -6,11 +6,18 @@ export interface RequirementDiscussionEntry { botId: string; displayName: string; role: string; + round: number; message: string; line: string; vote: ReviewVote["vote"]; confidence: number; requiredChanges: string[]; + transcript: string[]; +} + +export interface RequirementDiscussionRound { + round: number; + discussion: RequirementDiscussionEntry[]; } export interface RequirementJudgeResult { @@ -34,6 +41,7 @@ export interface RequirementCourtResult { rawRequest: string; clarifiedRequest: string; draft: GoalContract; + rounds: RequirementDiscussionRound[]; discussion: RequirementDiscussionEntry[]; votes: ReviewVote[]; verdict: VoteVerdict; @@ -44,6 +52,7 @@ export interface RequirementCourtResult { export interface RunRequirementCourtInput { manifest: Manifest; + ponySubagentRunner?: PonySubagentRunner | undefined; } const ROLE_MESSAGES: Record string> = { @@ -64,13 +73,54 @@ const ROLE_LABELS: Record = { testing_bot: "testing", }; -export function runRequirementCourt( +type RequirementCourtBot = Manifest["bots"][number]; + +export interface PonySubagentRunInput { + botId: string; + displayName: string; + role: string; + model: string; + instruction: string; + skills: string[]; + approvalConditions: string[]; + rejectOrAmendConditions: string[]; + contract: GoalContract; + round: number; + priorRounds: RequirementDiscussionRound[]; +} + +export interface PonySubagentRunResult { + message: string; + vote: ReviewVote["vote"]; + confidence: number; + requiredChanges: string[]; + transcript?: string[] | undefined; +} + +export type PonySubagentRunner = ( + input: PonySubagentRunInput, +) => PonySubagentRunResult | Promise; + +export async function runRequirementCourt( contract: GoalContract, input: RunRequirementCourtInput, -): RequirementCourtResult { - const discussion = input.manifest.deliberation.decisionRule.voterIds.map((botId) => - createDiscussionEntry(botId, contract, input.manifest), - ); +): Promise { + const runner = input.ponySubagentRunner ?? runDefaultPonySubagent; + const rounds: RequirementDiscussionRound[] = []; + const maxRounds = Math.max(1, input.manifest.deliberation.maxRounds); + + for (let round = 1; round <= maxRounds; round++) { + const priorRounds = rounds.slice(); + const discussion = await Promise.all( + input.manifest.deliberation.decisionRule.voterIds.map((botId) => + createDiscussionEntry(botId, contract, input.manifest, runner, round, priorRounds), + ), + ); + + rounds.push({ round, discussion }); + } + + const discussion = rounds.at(-1)?.discussion ?? []; const votes = discussion.map(toVote); const verdict = tallyVotes(votes, input.manifest.deliberation.decisionRule); const judge = createJudgeResult(verdict, input.manifest.deliberation.decisionRule.voters); @@ -79,6 +129,7 @@ export function runRequirementCourt( rawRequest: contract.rawRequest, clarifiedRequest: contract.intent, draft: contract, + rounds, discussion, votes, verdict, @@ -97,32 +148,88 @@ export function runRequirementCourt( }; } -function createDiscussionEntry( +async function createDiscussionEntry( botId: string, contract: GoalContract, manifest: Manifest, -): RequirementDiscussionEntry { + runner: PonySubagentRunner, + round: number, + priorRounds: RequirementDiscussionRound[], +): Promise { const bot = manifest.bots.find((candidate) => candidate.id === botId); if (!bot) { throw new Error(`Missing requirement court bot ${botId}`); } - const messageFactory = ROLE_MESSAGES[botId]; - if (!messageFactory) { - throw new Error(`Missing discussion message factory for ${botId}`); - } + const role = ROLE_LABELS[botId] ?? "review"; - const message = messageFactory(contract); + const result = await runner({ + botId, + displayName: bot.displayName, + role, + model: bot.model, + instruction: bot.instruction, + skills: bot.skills, + approvalConditions: bot.approvalConditions ?? [], + rejectOrAmendConditions: bot.rejectOrAmendConditions ?? [], + contract, + round, + priorRounds, + }); + const normalized = normalizePonySubagentResult(bot, result); return { botId, displayName: bot.displayName, - role: ROLE_LABELS[botId] ?? "review", + role, + round, + message: normalized.message, + line: `${botId}: ${normalized.message}`, + vote: normalized.vote, + confidence: normalized.confidence, + requiredChanges: normalized.requiredChanges, + transcript: normalized.transcript ?? [], + }; +} + +function runDefaultPonySubagent(input: PonySubagentRunInput): PonySubagentRunResult { + const messageFactory = ROLE_MESSAGES[input.botId]; + if (!messageFactory) { + throw new Error(`Missing discussion message factory for ${input.botId}`); + } + + const message = messageFactory(input.contract); + + return { message, - line: `${botId}: ${message}`, vote: "approve", confidence: 0.8, requiredChanges: [], + transcript: [ + `${input.displayName} reviewed the draft requirement as the ${input.role} pony.`, + `${input.displayName} voted approve with confidence 0.8.`, + ], + }; +} + +function normalizePonySubagentResult( + bot: RequirementCourtBot, + result: PonySubagentRunResult, +): PonySubagentRunResult { + if (!result.message.trim()) { + throw new Error(`Pony subagent ${bot.id} returned an empty discussion message`); + } + + if (result.confidence < 0 || result.confidence > 1) { + throw new Error(`Pony subagent ${bot.id} returned confidence outside 0..1`); + } + + return { + message: result.message.trim(), + vote: result.vote, + confidence: result.confidence, + requiredChanges: result.requiredChanges, + transcript: result.transcript?.map((line) => line.trim()).filter(Boolean), }; } diff --git a/tests/cli.test.ts b/tests/cli.test.ts index f7b91646..9491bd3f 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -4,6 +4,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { buildProgram, type GoalClarificationPrompter } from "../src/cli"; import type { CliInvocation, CliStreamRunner } from "../src/plugins/adapters"; +import type { PonySubagentRunner } from "../src/runtimes/ponytrail"; describe("cli", () => { test("registers onboarding, bot listing, goal drafting, and vote commands", () => { @@ -14,6 +15,7 @@ describe("cli", () => { "onboard", "bots", "goal", + "ponyrace", "vote", "stream-goal", "history", @@ -344,6 +346,117 @@ describe("cli", () => { } }); + test("ponyrace runs each pony through the configured subagent runner", async () => { + const rootDir = await mkdtemp(join(tmpdir(), "ponytrail-cli-")); + const logs: string[] = []; + const ponyCalls: string[] = []; + const originalLog = console.log; + const ponySubagentRunner: PonySubagentRunner = async ({ botId, contract }) => { + ponyCalls.push(botId); + + return { + message: `${botId} subagent accepted ${contract.title}`, + vote: "approve", + confidence: 0.91, + requiredChanges: [], + transcript: [`${botId} inspected requirement direction`], + }; + }; + + console.log = (...values: unknown[]) => { + logs.push(values.join(" ")); + }; + + try { + await buildProgram({ cwd: rootDir, ponySubagentRunner }).parseAsync( + ["onboard", "--dir", ".", "--name", "CLI Court", "--home", rootDir], + { from: "user" }, + ); + + await buildProgram({ cwd: rootDir, ponySubagentRunner }).parseAsync( + ["ponyrace", "Add", "CSV", "import", "to", "admin", "dashboard"], + { from: "user" }, + ); + + expect(ponyCalls).toEqual([ + "product_manager_bot", + "project_manager_bot", + "engineer_bot", + "testing_bot", + "product_manager_bot", + "project_manager_bot", + "engineer_bot", + "testing_bot", + ]); + expect(stripAnsiLines(logs)).toContain("Requirement discussion"); + expect(stripAnsiLines(logs)).toContain("Round 1"); + expect(stripAnsiLines(logs)).toContain("Round 2"); + expect( + logs.some((line) => + line.includes("product_manager_bot: product_manager_bot subagent accepted"), + ), + ).toBe(true); + expect(stripAnsiLines(logs)).toContain("Pony transcripts"); + expect(logs.some((line) => line.includes("inspected requirement direction"))).toBe(true); + expect(logs.some((line) => line.includes("Human confirmation: pending"))).toBe(true); + } finally { + console.log = originalLog; + await rm(rootDir, { recursive: true, force: true }); + } + }); + + test("ponyrace JSON output includes subagent discussion results", async () => { + const rootDir = await mkdtemp(join(tmpdir(), "ponytrail-cli-")); + const logs: string[] = []; + const ponyCalls: string[] = []; + const originalLog = console.log; + const ponySubagentRunner: PonySubagentRunner = async ({ botId }) => { + ponyCalls.push(botId); + + return { + message: `${botId} json review`, + vote: "approve", + confidence: 0.88, + requiredChanges: [], + transcript: [`${botId} json transcript`], + }; + }; + + console.log = (...values: unknown[]) => { + logs.push(values.join(" ")); + }; + + try { + await buildProgram({ cwd: rootDir, ponySubagentRunner }).parseAsync( + ["onboard", "--dir", ".", "--name", "CLI Court", "--home", rootDir], + { from: "user" }, + ); + + await buildProgram({ cwd: rootDir, ponySubagentRunner }).parseAsync( + ["ponyrace", "--json", "Add", "CSV", "import", "to", "admin", "dashboard"], + { from: "user" }, + ); + + const output = JSON.parse(logs.at(-1) ?? "{}"); + expect(ponyCalls).toEqual([ + "product_manager_bot", + "project_manager_bot", + "engineer_bot", + "testing_bot", + "product_manager_bot", + "project_manager_bot", + "engineer_bot", + "testing_bot", + ]); + expect(output.rounds.map((round: { round: number }) => round.round)).toEqual([1, 2]); + expect(output.discussion[0].message).toBe("product_manager_bot json review"); + expect(output.humanConfirmation).toBe("pending"); + } finally { + console.log = originalLog; + await rm(rootDir, { recursive: true, force: true }); + } + }); + test("skills install dry-runs bundled pony trail skill installation for npx and bunx usage", async () => { const homeDir = await mkdtemp(join(tmpdir(), "ponytrail-skill-home-")); const logs: string[] = []; diff --git a/tests/requirement-court.test.ts b/tests/requirement-court.test.ts index bc4d3628..1190f0c9 100644 --- a/tests/requirement-court.test.ts +++ b/tests/requirement-court.test.ts @@ -1,14 +1,17 @@ import { describe, expect, test } from "bun:test"; import { draftGoalContract } from "../src/runtimes/ponytrail/goal"; import { createDefaultManifest } from "../src/runtimes/ponytrail/manifest"; -import { runRequirementCourt } from "../src/runtimes/ponytrail/requirement-court"; +import { + type PonySubagentRunner, + runRequirementCourt, +} from "../src/runtimes/ponytrail/requirement-court"; describe("requirement court", () => { - test("creates visible role-bot discussion entries before the Judge summary", () => { + test("creates visible role-bot discussion entries before the Judge summary", async () => { const manifest = createDefaultManifest(); const contract = draftGoalContract("Add CSV import to admin dashboard", { manifest }); - const result = runRequirementCourt(contract, { manifest }); + const result = await runRequirementCourt(contract, { manifest }); expect(result.discussion.map((entry) => entry.botId)).toEqual([ "product_manager_bot", @@ -28,11 +31,11 @@ describe("requirement court", () => { expect(result.detailedRequirement.title).toBe("Add CSV import to admin dashboard"); }); - test("uses the manifest vote rule and does not count the Judge as a voter", () => { + test("uses the manifest vote rule and does not count the Judge as a voter", async () => { const manifest = createDefaultManifest(); const contract = draftGoalContract("Add CSV import to admin dashboard", { manifest }); - const result = runRequirementCourt(contract, { manifest }); + const result = await runRequirementCourt(contract, { manifest }); expect(result.votes.map((vote) => vote.botId)).toEqual([ "product_manager_bot", @@ -42,4 +45,87 @@ describe("requirement court", () => { ]); expect(result.votes.some((vote) => vote.botId === "requirement_judge_bot")).toBe(false); }); + + test("uses final-round messages from each voting pony before tallying votes", async () => { + const manifest = createDefaultManifest(); + const contract = draftGoalContract("Add CSV import to admin dashboard", { manifest }); + const calls: string[] = []; + const ponySubagentRunner: PonySubagentRunner = async ({ botId, contract }) => { + calls.push(botId); + + return { + message: `${botId} reviewed ${contract.title}`, + vote: "approve", + confidence: 0.9, + requiredChanges: [], + transcript: [`${botId} received the contract`, `${botId} returned approve`], + }; + }; + + const result = await runRequirementCourt(contract, { manifest, ponySubagentRunner }); + + expect(calls).toEqual([ + "product_manager_bot", + "project_manager_bot", + "engineer_bot", + "testing_bot", + "product_manager_bot", + "project_manager_bot", + "engineer_bot", + "testing_bot", + ]); + expect(result.discussion.map((entry) => entry.message)).toEqual([ + "product_manager_bot reviewed Add CSV import to admin dashboard", + "project_manager_bot reviewed Add CSV import to admin dashboard", + "engineer_bot reviewed Add CSV import to admin dashboard", + "testing_bot reviewed Add CSV import to admin dashboard", + ]); + expect(result.discussion[0]?.transcript).toEqual([ + "product_manager_bot received the contract", + "product_manager_bot returned approve", + ]); + expect(result.verdict.approvals).toBe(4); + }); + + test("runs each voting pony once per discussion round and votes from the final round", async () => { + const manifest = createDefaultManifest(); + const contract = draftGoalContract("Add CSV import to admin dashboard", { manifest }); + const calls: Array<{ botId: string; round: number; priorRounds: number }> = []; + const ponySubagentRunner: PonySubagentRunner = async ({ + botId, + contract, + priorRounds, + round, + }) => { + calls.push({ botId, round, priorRounds: priorRounds.length }); + + return { + message: `round ${round} ${botId} reviewed ${contract.title}`, + vote: "approve", + confidence: round === 2 ? 0.92 : 0.72, + requiredChanges: [], + transcript: [`${botId} saw ${priorRounds.length} prior rounds`], + }; + }; + + const result = await runRequirementCourt(contract, { manifest, ponySubagentRunner }); + + expect(result.rounds.map((round) => round.round)).toEqual([1, 2]); + expect(calls).toEqual([ + { botId: "product_manager_bot", round: 1, priorRounds: 0 }, + { botId: "project_manager_bot", round: 1, priorRounds: 0 }, + { botId: "engineer_bot", round: 1, priorRounds: 0 }, + { botId: "testing_bot", round: 1, priorRounds: 0 }, + { botId: "product_manager_bot", round: 2, priorRounds: 1 }, + { botId: "project_manager_bot", round: 2, priorRounds: 1 }, + { botId: "engineer_bot", round: 2, priorRounds: 1 }, + { botId: "testing_bot", round: 2, priorRounds: 1 }, + ]); + expect(result.votes.map((vote) => vote.reason)).toEqual([ + "round 2 product_manager_bot reviewed Add CSV import to admin dashboard", + "round 2 project_manager_bot reviewed Add CSV import to admin dashboard", + "round 2 engineer_bot reviewed Add CSV import to admin dashboard", + "round 2 testing_bot reviewed Add CSV import to admin dashboard", + ]); + }); });