Skip to content

Commit 8e94cfd

Browse files
that-github-userunknownclaude
authored
Add apply command to apply selected agent's diff (#11)
- `thinktank apply` applies the recommended agent's changes - `thinktank apply --agent N` applies a specific agent's changes - Cleans up worktrees and branches after successful apply - Reports conflicts clearly if git apply fails - 4 unit tests for agent selection logic Closes #1 Co-authored-by: unknown <that-github-user@github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a40da36 commit 8e94cfd

File tree

3 files changed

+161
-0
lines changed

3 files changed

+161
-0
lines changed

src/cli.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { Command } from "commander";
44
import { run } from "./commands/run.js";
55
import { list } from "./commands/list.js";
6+
import { apply } from "./commands/apply.js";
67

78
const program = new Command();
89

@@ -36,6 +37,16 @@ program
3637
});
3738
});
3839

40+
program
41+
.command("apply")
42+
.description("Apply the recommended (or selected) agent's changes to your repo")
43+
.option("-a, --agent <number>", "Apply a specific agent's changes instead of the recommended one")
44+
.action(async (opts) => {
45+
await apply({
46+
agent: opts.agent ? parseInt(opts.agent, 10) : undefined,
47+
});
48+
});
49+
3950
program
4051
.command("list")
4152
.description("List results from the most recent ensemble run")

src/commands/apply.test.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { describe, it, beforeEach, mock } from "node:test";
2+
import assert from "node:assert/strict";
3+
4+
// Test the logic of agent selection without actually running git commands
5+
describe("apply agent selection logic", () => {
6+
const mockResult = {
7+
prompt: "test task",
8+
model: "sonnet",
9+
timestamp: "2026-03-28T00:00:00Z",
10+
agents: [
11+
{
12+
id: 1,
13+
worktree: "/tmp/agent-1",
14+
status: "success" as const,
15+
exitCode: 0,
16+
duration: 5000,
17+
output: "",
18+
diff: "diff --git a/test.ts b/test.ts\n+// hello",
19+
filesChanged: ["test.ts"],
20+
linesAdded: 1,
21+
linesRemoved: 0,
22+
},
23+
{
24+
id: 2,
25+
worktree: "/tmp/agent-2",
26+
status: "error" as const,
27+
exitCode: 1,
28+
duration: 3000,
29+
output: "",
30+
error: "failed",
31+
diff: "",
32+
filesChanged: [],
33+
linesAdded: 0,
34+
linesRemoved: 0,
35+
},
36+
],
37+
tests: [],
38+
convergence: [],
39+
recommended: 1,
40+
};
41+
42+
it("selects recommended agent when no --agent flag", () => {
43+
const userChoice: number | undefined = undefined;
44+
const agentId = userChoice !== undefined ? userChoice : mockResult.recommended;
45+
const agent = mockResult.agents.find((a) => a.id === agentId);
46+
assert.ok(agent);
47+
assert.equal(agent.id, 1);
48+
assert.equal(agent.status, "success");
49+
assert.ok(agent.diff.length > 0);
50+
});
51+
52+
it("selects specified agent with --agent flag", () => {
53+
const agentId = 2;
54+
const agent = mockResult.agents.find((a) => a.id === agentId);
55+
assert.ok(agent);
56+
assert.equal(agent.id, 2);
57+
});
58+
59+
it("returns undefined for non-existent agent", () => {
60+
const agent = mockResult.agents.find((a) => a.id === 99);
61+
assert.equal(agent, undefined);
62+
});
63+
64+
it("detects agent with no changes", () => {
65+
const agent = mockResult.agents.find((a) => a.id === 2);
66+
assert.ok(agent);
67+
assert.equal(agent.status, "error");
68+
assert.equal(agent.diff, "");
69+
});
70+
});

src/commands/apply.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { readFile } from "node:fs/promises";
2+
import { join } from "node:path";
3+
import { execFile } from "node:child_process";
4+
import { promisify } from "node:util";
5+
import type { EnsembleResult } from "../types.js";
6+
import { removeWorktree, cleanupBranches, getRepoRoot } from "../utils/git.js";
7+
8+
const exec = promisify(execFile);
9+
10+
export interface ApplyOptions {
11+
agent?: number;
12+
}
13+
14+
export async function apply(opts: ApplyOptions): Promise<void> {
15+
// Load latest result
16+
let result: EnsembleResult;
17+
try {
18+
const raw = await readFile(join(".thinktank", "latest.json"), "utf-8");
19+
result = JSON.parse(raw);
20+
} catch {
21+
console.error(" No results found. Run `thinktank run` first.");
22+
process.exit(1);
23+
}
24+
25+
// Determine which agent to apply
26+
const agentId = opts.agent ?? result.recommended;
27+
if (agentId === null || agentId === undefined) {
28+
console.error(" No recommended agent and no --agent specified.");
29+
process.exit(1);
30+
}
31+
32+
const agent = result.agents.find((a) => a.id === agentId);
33+
if (!agent) {
34+
console.error(` Agent #${agentId} not found in results.`);
35+
console.error(
36+
` Available agents: ${result.agents.map((a) => `#${a.id}`).join(", ")}`
37+
);
38+
process.exit(1);
39+
}
40+
41+
if (agent.status !== "success" || !agent.diff) {
42+
console.error(` Agent #${agentId} has no changes to apply (status: ${agent.status}).`);
43+
process.exit(1);
44+
}
45+
46+
// Apply the diff
47+
const repoRoot = await getRepoRoot();
48+
console.log(` Applying changes from Agent #${agentId}...`);
49+
50+
try {
51+
const child = exec("git", ["apply", "--3way", "-"], { cwd: repoRoot });
52+
child.child.stdin?.write(agent.diff);
53+
child.child.stdin?.end();
54+
await child;
55+
console.log(" Changes applied successfully.");
56+
} catch (err: unknown) {
57+
const e = err as { stderr?: string };
58+
console.error(" Failed to apply diff. There may be conflicts.");
59+
if (e.stderr) console.error(` ${e.stderr}`);
60+
console.error(` You can manually inspect the diff at: ${agent.worktree}`);
61+
process.exit(1);
62+
}
63+
64+
// Clean up worktrees
65+
console.log(" Cleaning up worktrees...");
66+
for (const a of result.agents) {
67+
try {
68+
await removeWorktree(a.worktree);
69+
} catch {
70+
// Best effort cleanup
71+
}
72+
}
73+
await cleanupBranches().catch(() => {});
74+
75+
console.log(" Done. Worktrees cleaned up.");
76+
console.log();
77+
console.log(" Review the changes with: git diff");
78+
console.log(" Commit when ready: git add -A && git commit");
79+
console.log();
80+
}

0 commit comments

Comments
 (0)