feat(FN-4114): complete Step 1 — add worktree invariant helper

Fusion-Task-Id: FN-4114
Fusion-Task-Lineage: 44fd5718-42dc-44d1-8f5a-97b67dd05d13
This commit is contained in:
Fusion
2026-05-13 03:50:49 -07:00
committed by gsxdsm
parent 762f1b3413
commit 57862312bc
2 changed files with 121 additions and 3 deletions

View File

@@ -3,7 +3,7 @@ import { promisify } from "node:util";
const execAsync = promisify(exec);
import { delimiter, isAbsolute, join, relative, resolve as resolvePath } from "node:path";
import { existsSync } from "node:fs";
import { existsSync, realpathSync } from "node:fs";
import { readFile, writeFile } from "node:fs/promises";
import type { TaskStore, Task, TaskDetail, TaskTokenUsage, StepStatus, Settings, WorkflowStep, MissionStore, Slice, AgentState, AgentCapability, RunMutationContext, AgentHeartbeatConfig, Agent } from "@fusion/core";
import {
@@ -36,7 +36,7 @@ import { buildSessionSkillContext } from "./session-skill-context.js";
import { reviewStep, type ReviewVerdict } from "./reviewer.js";
import { ModelRegistry, SessionManager, type ToolDefinition, type AgentSession } from "@mariozechner/pi-coding-agent";
import { PRIORITY_EXECUTE, type AgentSemaphore } from "./concurrency.js";
import { getRegisteredWorktreePaths, isGitRepository, isRegisteredGitWorktree, type WorktreePool } from "./worktree-pool.js";
import { getRegisteredWorktreePaths, isGitRepository, isInsideWorktreesDir, isRegisteredGitWorktree, type WorktreePool } from "./worktree-pool.js";
import { BranchConflictError, isBranchConflictError, inspectBranchConflict } from "./branch-conflicts.js";
import { AgentLogger } from "./agent-logger.js";
import { executorLog, reviewerLog, formatError } from "./logger.js";
@@ -4312,6 +4312,124 @@ export class TaskExecutor {
});
}
private async verifyWorktreeInvariants(
task: Task,
): Promise<{ ok: true } | { ok: false; reason: "wrong_toplevel" | "wrong_branch" | "no_commits"; observed: string; expected: string }> {
const branchName = task.branch || `fusion/${task.id.toLowerCase()}`;
const worktreePath = task.worktree;
if (!worktreePath) {
return {
ok: false,
reason: "wrong_toplevel",
observed: "missing task.worktree",
expected: "registered task worktree under <repo>/.worktrees/*",
};
}
const expectedRoot = realpathSync(this.rootDir);
let expectedWorktreeRealpath: string;
try {
expectedWorktreeRealpath = realpathSync(worktreePath);
} catch (error) {
return {
ok: false,
reason: "wrong_toplevel",
observed: `unresolvable task.worktree (${worktreePath}): ${error instanceof Error ? error.message : String(error)}`,
expected: "resolvable task worktree under <repo>/.worktrees/*",
};
}
try {
const { stdout } = await execAsync("git rev-parse --show-toplevel", {
cwd: worktreePath,
encoding: "utf-8",
timeout: 10_000,
maxBuffer: 1024 * 1024,
});
const observedTopLevelRaw = stdout.trim();
const observedTopLevel = realpathSync(observedTopLevelRaw);
if (
observedTopLevel === expectedRoot ||
!isInsideWorktreesDir(this.rootDir, observedTopLevel) ||
observedTopLevel !== expectedWorktreeRealpath
) {
return {
ok: false,
reason: "wrong_toplevel",
observed: observedTopLevel,
expected: expectedWorktreeRealpath,
};
}
} catch (error) {
return {
ok: false,
reason: "wrong_toplevel",
observed: error instanceof Error ? error.message : String(error),
expected: expectedWorktreeRealpath,
};
}
try {
const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", {
cwd: worktreePath,
encoding: "utf-8",
timeout: 10_000,
maxBuffer: 1024 * 1024,
});
const observedBranch = stdout.trim();
if (observedBranch !== branchName) {
return {
ok: false,
reason: "wrong_branch",
observed: observedBranch,
expected: branchName,
};
}
} catch (error) {
return {
ok: false,
reason: "wrong_branch",
observed: error instanceof Error ? error.message : String(error),
expected: branchName,
};
}
const baseRef = await this.resolveDiffBaseRef(worktreePath, task.baseCommitSha);
if (!baseRef) {
executorLog.warn(`${task.id}: unable to resolve diff base for invariant commit-count check; skipping no_commits guard`);
return { ok: true };
}
try {
const { stdout } = await execAsync(`git rev-list --count ${baseRef}..HEAD`, {
cwd: worktreePath,
encoding: "utf-8",
timeout: 10_000,
maxBuffer: 1024 * 1024,
});
const count = Number.parseInt(stdout.trim(), 10);
if (!Number.isFinite(count) || count <= 0) {
return {
ok: false,
reason: "no_commits",
observed: Number.isFinite(count) ? String(count) : stdout.trim(),
expected: "> 0",
};
}
} catch (error) {
return {
ok: false,
reason: "no_commits",
observed: error instanceof Error ? error.message : String(error),
expected: `git rev-list --count ${baseRef}..HEAD > 0`,
};
}
return { ok: true };
}
private createTaskDoneTool(taskId: string, onDone: () => void): ToolDefinition {
const store = this.store;
return {

View File

@@ -67,7 +67,7 @@ export async function isUsableTaskWorktree(rootDir: string, worktreePath: string
hasRequiredWorktreeFiles(worktreePath);
}
function isInsideWorktreesDir(rootDir: string, worktreePath: string): boolean {
export function isInsideWorktreesDir(rootDir: string, worktreePath: string): boolean {
const worktreesDir = resolve(rootDir, ".worktrees");
const target = resolve(worktreePath);
const rel = relative(worktreesDir, target);