diff --git a/src/commands/run.test.ts b/src/commands/run.test.ts index d2ddde8..271f6b0 100644 --- a/src/commands/run.test.ts +++ b/src/commands/run.test.ts @@ -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, @@ -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")); + } + }); +}); diff --git a/src/commands/run.ts b/src/commands/run.ts index 7bfcade..847bcdf 100644 --- a/src/commands/run.ts +++ b/src/commands/run.ts @@ -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 { + 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. @@ -27,6 +67,12 @@ export async function preflightValidation(opts: RunOptions): Promise { + 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 { const repoRoot = await getMainRepoRoot(); const { stdout } = await exec("git", ["branch", "--list", "thinktank/*"], {