Skip to content

Commit 6fe013c

Browse files
that-github-userunknownclaude
authored
Add disk space pre-flight check before creating worktrees (#123)
Estimates repo size via git count-objects, checks free space on temp partition via statfs. Warns (but doesn't block) if available space is less than attempts * repo_size. 3 new tests. Generated by thinktank Opus (5 agents, 2 pass, Copeland: #4 at +4). Closes #71 Co-authored-by: unknown <that-github-user@github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 29aab42 commit 6fe013c

3 files changed

Lines changed: 93 additions & 2 deletions

File tree

src/commands/run.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { join } from "node:path";
55
import { afterEach, describe, it } from "node:test";
66
import type { AgentResult, EnsembleResult, RunOptions, TestResult } from "../types.js";
77
import {
8+
checkDiskSpace,
89
findFailedAgents,
910
loadLatestResult,
1011
makeResultFilename,
@@ -408,3 +409,30 @@ describe("retry edge cases", () => {
408409
assert.equal(merged[2].diff, "new diff3");
409410
});
410411
});
412+
413+
describe("checkDiskSpace", () => {
414+
it("returns null when enough space is available", async () => {
415+
// With a small number of attempts in a real git repo, there should be enough space
416+
const result = await checkDiskSpace(1);
417+
assert.equal(result, null);
418+
});
419+
420+
it("returns a warning string when space is insufficient", async () => {
421+
// Request an absurd number of worktrees to trigger the warning
422+
const result = await checkDiskSpace(1_000_000);
423+
if (result !== null) {
424+
assert.ok(result.includes("Low disk space"));
425+
assert.ok(result.includes("available"));
426+
assert.ok(result.includes("needed"));
427+
assert.ok(result.includes("worktrees"));
428+
}
429+
// If the repo is tiny enough that even 1M copies fit, result may be null — that's OK
430+
});
431+
432+
it("includes attempt count in warning message", async () => {
433+
const result = await checkDiskSpace(1_000_000);
434+
if (result !== null) {
435+
assert.ok(result.includes("1000000 worktrees"));
436+
}
437+
});
438+
});

src/commands/run.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,51 @@
1-
import { mkdir, readFile, writeFile } from "node:fs/promises";
1+
import { mkdir, readFile, statfs, writeFile } from "node:fs/promises";
2+
import { tmpdir } from "node:os";
23
import { join } from "node:path";
34
import { getDefaultRunner, getRunner } from "../runners/registry.js";
45
import { analyzeConvergence, copelandRecommend, recommend } from "../scoring/convergence.js";
56
import { runTests, validateTestCommand } from "../scoring/test-runner.js";
67
import type { AgentResult, EnsembleResult, RunOptions } from "../types.js";
78
import { displayApplyInstructions, displayHeader, displayResults } from "../utils/display.js";
8-
import { cleanupBranches, createWorktree, getRepoRoot, removeWorktree } from "../utils/git.js";
9+
import {
10+
cleanupBranches,
11+
createWorktree,
12+
estimateRepoSize,
13+
getRepoRoot,
14+
removeWorktree,
15+
} from "../utils/git.js";
16+
17+
function formatBytes(bytes: number): string {
18+
if (bytes >= 1024 * 1024 * 1024) {
19+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
20+
}
21+
return `${(bytes / (1024 * 1024)).toFixed(0)} MB`;
22+
}
23+
24+
/**
25+
* Check whether the temp partition has enough free space for the planned worktrees.
26+
* Returns a warning string if space is low, or null if OK.
27+
*/
28+
export async function checkDiskSpace(attempts: number): Promise<string | null> {
29+
try {
30+
const tempDir = tmpdir();
31+
const stats = await statfs(tempDir);
32+
const availableBytes = stats.bavail * stats.bsize;
33+
34+
const repoSize = await estimateRepoSize();
35+
const estimatedNeed = repoSize * attempts;
36+
37+
if (availableBytes < estimatedNeed) {
38+
return (
39+
`Low disk space on temp partition: ${formatBytes(availableBytes)} available, ` +
40+
`~${formatBytes(estimatedNeed)} needed for ${attempts} worktrees. ` +
41+
"Consider freeing disk space or reducing --attempts."
42+
);
43+
}
44+
} catch {
45+
// statfs or estimateRepoSize failed — skip the check silently
46+
}
47+
return null;
48+
}
949

1050
/**
1151
* Pre-flight validation before spawning agents.
@@ -27,6 +67,12 @@ export async function preflightValidation(opts: RunOptions): Promise<string | nu
2767
}
2868
}
2969

70+
// Check: disk space (warn only, do not block)
71+
const diskWarning = await checkDiskSpace(opts.attempts);
72+
if (diskWarning) {
73+
console.warn(` ⚠ ${diskWarning}`);
74+
}
75+
3076
return null;
3177
}
3278

src/utils/git.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,23 @@ export async function getDiffStats(
132132
}
133133
}
134134

135+
/**
136+
* Estimate the size of the git repo in bytes using `git count-objects -v`.
137+
* Returns the sum of loose object size and pack size (a rough lower bound
138+
* for the size of a checked-out worktree).
139+
*/
140+
export async function estimateRepoSize(): Promise<number> {
141+
const { stdout } = await exec("git", ["count-objects", "-v"]);
142+
let sizeKB = 0;
143+
for (const line of stdout.split("\n")) {
144+
const match = line.match(/^(size|size-pack):\s+(\d+)/);
145+
if (match?.[2]) {
146+
sizeKB += parseInt(match[2], 10);
147+
}
148+
}
149+
return sizeKB * 1024;
150+
}
151+
135152
export async function cleanupBranches(): Promise<void> {
136153
const repoRoot = await getMainRepoRoot();
137154
const { stdout } = await exec("git", ["branch", "--list", "thinktank/*"], {

0 commit comments

Comments
 (0)