Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/commands/run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { join } from "node:path";
import { afterEach, describe, it } from "node:test";
import type { AgentResult, EnsembleResult, RunOptions, TestResult } from "../types.js";
import {
checkDiskSpace,
findFailedAgents,
loadLatestResult,
makeResultFilename,
Expand Down Expand Up @@ -408,3 +409,30 @@ describe("retry edge cases", () => {
assert.equal(merged[2].diff, "new diff3");
});
});

describe("checkDiskSpace", () => {
it("returns null when enough space is available", async () => {
// With a small number of attempts in a real git repo, there should be enough space
const result = await checkDiskSpace(1);
assert.equal(result, null);
});

it("returns a warning string when space is insufficient", async () => {
// Request an absurd number of worktrees to trigger the warning
const result = await checkDiskSpace(1_000_000);
if (result !== null) {
assert.ok(result.includes("Low disk space"));
assert.ok(result.includes("available"));
assert.ok(result.includes("needed"));
assert.ok(result.includes("worktrees"));
}
// If the repo is tiny enough that even 1M copies fit, result may be null — that's OK
});

it("includes attempt count in warning message", async () => {
const result = await checkDiskSpace(1_000_000);
if (result !== null) {
assert.ok(result.includes("1000000 worktrees"));
}
});
});
50 changes: 48 additions & 2 deletions src/commands/run.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,51 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { mkdir, readFile, statfs, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { getDefaultRunner, getRunner } from "../runners/registry.js";
import { analyzeConvergence, copelandRecommend, recommend } from "../scoring/convergence.js";
import { runTests, validateTestCommand } from "../scoring/test-runner.js";
import type { AgentResult, EnsembleResult, RunOptions } from "../types.js";
import { displayApplyInstructions, displayHeader, displayResults } from "../utils/display.js";
import { cleanupBranches, createWorktree, getRepoRoot, removeWorktree } from "../utils/git.js";
import {
cleanupBranches,
createWorktree,
estimateRepoSize,
getRepoRoot,
removeWorktree,
} from "../utils/git.js";

function formatBytes(bytes: number): string {
if (bytes >= 1024 * 1024 * 1024) {
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
}
return `${(bytes / (1024 * 1024)).toFixed(0)} MB`;
}

/**
* Check whether the temp partition has enough free space for the planned worktrees.
* Returns a warning string if space is low, or null if OK.
*/
export async function checkDiskSpace(attempts: number): Promise<string | null> {
try {
const tempDir = tmpdir();
const stats = await statfs(tempDir);
const availableBytes = stats.bavail * stats.bsize;

const repoSize = await estimateRepoSize();
const estimatedNeed = repoSize * attempts;

if (availableBytes < estimatedNeed) {
return (
`Low disk space on temp partition: ${formatBytes(availableBytes)} available, ` +
`~${formatBytes(estimatedNeed)} needed for ${attempts} worktrees. ` +
"Consider freeing disk space or reducing --attempts."
);
}
} catch {
// statfs or estimateRepoSize failed — skip the check silently
}
return null;
}

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

// Check: disk space (warn only, do not block)
const diskWarning = await checkDiskSpace(opts.attempts);
if (diskWarning) {
console.warn(` ⚠ ${diskWarning}`);
}

return null;
}

Expand Down
17 changes: 17 additions & 0 deletions src/utils/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,23 @@ export async function getDiffStats(
}
}

/**
* Estimate the size of the git repo in bytes using `git count-objects -v`.
* Returns the sum of loose object size and pack size (a rough lower bound
* for the size of a checked-out worktree).
*/
export async function estimateRepoSize(): Promise<number> {
const { stdout } = await exec("git", ["count-objects", "-v"]);
let sizeKB = 0;
for (const line of stdout.split("\n")) {
const match = line.match(/^(size|size-pack):\s+(\d+)/);
if (match?.[2]) {
sizeKB += parseInt(match[2], 10);
}
}
return sizeKB * 1024;
}

export async function cleanupBranches(): Promise<void> {
const repoRoot = await getMainRepoRoot();
const { stdout } = await exec("git", ["branch", "--list", "thinktank/*"], {
Expand Down
Loading