Add workspace branch lineage regression

Reconcile forward git-worktree branch records inside the heartbeat runtime when the experimental flag is enabled, and carry the reconciled branch through realization, persisted reuse, and finalization.

Add an embedded-Postgres regression covering a parent run that moves a shared execution workspace branch forward, a git-handoff child that reuses the reconciled workspace without a second reconcile, and an isolated sibling workspace record that remains untouched.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Nicky LeachandPaperclip committed 2026-07-07 15:42:27 +00:00
1 parent 390627b46e
commit d3fd589c1e
3 files changed
+639 -9

No files matched your search

@@ -83,6 +83,10 @@ async function runGit(cwd: string, args: string[]) {
await execFileAsync("git", args, { cwd });
}
async function readGit(cwd: string, args: string[]) {
return (await execFileAsync("git", args, { cwd })).stdout.trim();
}
async function createGitRepo() {
const repoRoot = await mkdtemp(path.join(os.tmpdir(), "paperclip-branch-containment-repo-"));
await runGit(repoRoot, ["init"]);
@@ -231,6 +235,21 @@ function readAdapterWorkspace(input: unknown) {
return { cwd, branchName, executionWorkspaceId };
}
async function wakeIssue(heartbeat: Heartbeat, agentId: string, issueId: string) {
return heartbeat.wakeup(agentId, {
source: "automation",
triggerDetail: "system",
reason: "issue_commented",
payload: { issueId },
contextSnapshot: {
issueId,
taskId: issueId,
wakeReason: "issue_commented",
skipIssueComment: true,
},
});
}
async function seedBranchContainmentRun(db: Db, repoRoot: string, callSite: BranchContainmentCallSite) {
const companyId = randomUUID();
const projectId = randomUUID();
@@ -490,6 +509,179 @@ async function seedBranchContainmentRun(db: Db, repoRoot: string, callSite: Bran
};
}
async function seedLineagePropagationRun(db: Db, repoRoot: string) {
const companyId = randomUUID();
const projectId = randomUUID();
const projectWorkspaceId = randomUUID();
const agentId = randomUUID();
const parentIssueId = randomUUID();
const childIssueId = randomUUID();
const isolatedSiblingIssueId = randomUUID();
const isolatedSiblingExecutionWorkspaceId = randomUUID();
const issuePrefix = `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`;
const parentIdentifier = `${issuePrefix}-1`;
const childIdentifier = `${issuePrefix}-2`;
const isolatedSiblingIdentifier = `${issuePrefix}-3`;
const isolatedSiblingBranch = `${isolatedSiblingIdentifier}-recorded`;
const now = new Date("2026-07-07T00:00:00.000Z");
await instanceSettingsService(db).updateExperimental({
enableIsolatedWorkspaces: true,
enableWorkspaceBranchReconcileForward: true,
});
await db.insert(companies).values({
id: companyId,
name: "Acme",
issuePrefix,
status: "active",
defaultResponsibleUserId: "responsible-user",
createdAt: now,
updatedAt: now,
});
await db.insert(projects).values({
id: projectId,
companyId,
name: "Lineage propagation",
status: "active",
executionWorkspacePolicy: {
enabled: true,
defaultMode: "isolated_workspace",
workspaceStrategy: {
type: "git_worktree",
baseRef: "HEAD",
branchTemplate: "{{issue.identifier}}-recorded",
},
},
createdAt: now,
updatedAt: now,
});
await db.insert(projectWorkspaces).values({
id: projectWorkspaceId,
companyId,
projectId,
name: "Primary",
cwd: repoRoot,
isPrimary: true,
createdAt: now,
updatedAt: now,
});
await db.insert(agents).values({
id: agentId,
companyId,
name: "CodexCoder",
role: "engineer",
status: "idle",
adapterType: "codex_local",
adapterConfig: {},
runtimeConfig: {
heartbeat: {
wakeOnDemand: true,
maxConcurrentRuns: 1,
},
},
permissions: {},
createdAt: now,
updatedAt: now,
});
await db.insert(executionWorkspaces).values({
id: isolatedSiblingExecutionWorkspaceId,
companyId,
projectId,
projectWorkspaceId,
sourceIssueId: null,
mode: "isolated_workspace",
strategyType: "git_worktree",
name: isolatedSiblingBranch,
status: "active",
cwd: path.join(repoRoot, ".paperclip", "worktrees", isolatedSiblingBranch),
repoUrl: null,
baseRef: "HEAD",
branchName: isolatedSiblingBranch,
providerType: "git_worktree",
providerRef: path.join(repoRoot, ".paperclip", "worktrees", isolatedSiblingBranch),
lastUsedAt: now,
openedAt: now,
createdAt: now,
updatedAt: now,
});
await db.insert(issues).values([
{
id: parentIssueId,
companyId,
projectId,
projectWorkspaceId,
title: "Parent branch reconciliation source",
status: "in_progress",
workMode: "standard",
priority: "medium",
assigneeAgentId: agentId,
responsibleUserId: "responsible-user",
issueNumber: 1,
identifier: parentIdentifier,
executionWorkspaceSettings: {
mode: "isolated_workspace",
},
startedAt: now,
createdAt: now,
updatedAt: now,
},
{
id: childIssueId,
companyId,
projectId,
projectWorkspaceId,
parentId: parentIssueId,
title: "Git handoff child",
status: "todo",
workMode: "standard",
priority: "medium",
assigneeAgentId: agentId,
responsibleUserId: "responsible-user",
issueNumber: 2,
identifier: childIdentifier,
executionWorkspacePreference: "reuse_existing",
executionWorkspaceSettings: {
mode: "isolated_workspace",
},
createdAt: now,
updatedAt: now,
},
{
id: isolatedSiblingIssueId,
companyId,
projectId,
projectWorkspaceId,
title: "Isolated sibling",
status: "todo",
workMode: "standard",
priority: "medium",
assigneeAgentId: agentId,
responsibleUserId: "responsible-user",
issueNumber: 3,
identifier: isolatedSiblingIdentifier,
executionWorkspaceId: isolatedSiblingExecutionWorkspaceId,
executionWorkspacePreference: "reuse_existing",
executionWorkspaceSettings: {
mode: "isolated_workspace",
},
createdAt: now,
updatedAt: now,
},
]);
return {
companyId,
agentId,
parentIssueId,
childIssueId,
isolatedSiblingIssueId,
isolatedSiblingExecutionWorkspaceId,
parentIdentifier,
childIdentifier,
isolatedSiblingBranch,
};
}
function asRecord(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value)
? value as Record<string, unknown>
@@ -722,4 +914,177 @@ describeEmbeddedPostgres("heartbeat workspace branch containment", () => {
});
expect(adapterExecute).toHaveBeenCalledTimes(callSite === "finalize" ? 1 : 0);
}, 30_000);
it("propagates a reconciled parent execution workspace branch to a git-handoff child without reuse clobbering the record", async () => {
const repoRoot = await createGitRepo();
tempRoots.push(repoRoot);
const seeded = await seedLineagePropagationRun(db, repoRoot);
const heartbeat = heartbeatService(db);
let sharedExecutionWorkspaceId: string | null = null;
let worktreePath: string | null = null;
let recordedBranch: string | null = null;
let reconciledBranch: string | null = null;
let reconciledHead: string | null = null;
const childTemplateBranch = `${seeded.childIdentifier}-recorded`;
adapterExecute.mockImplementationOnce(async (adapterInput) => {
const workspace = readAdapterWorkspace(adapterInput);
sharedExecutionWorkspaceId = workspace.executionWorkspaceId;
worktreePath = workspace.cwd;
recordedBranch = workspace.branchName;
reconciledBranch = `${workspace.branchName.replace(/-recorded$/, "")}-actual`;
await db
.update(issues)
.set({
executionWorkspaceId: workspace.executionWorkspaceId,
executionWorkspacePreference: "reuse_existing",
executionWorkspaceSettings: { mode: "isolated_workspace" },
updatedAt: new Date(),
})
.where(eq(issues.id, seeded.childIssueId));
await runGit(workspace.cwd, ["checkout", "-b", reconciledBranch]);
await writeFile(path.join(workspace.cwd, "parent-forward.txt"), "parent moved branch forward\n", "utf8");
await runGit(workspace.cwd, ["add", "parent-forward.txt"]);
await runGit(workspace.cwd, ["commit", "-m", "Move parent workspace branch forward"]);
reconciledHead = await readGit(workspace.cwd, ["rev-parse", "HEAD"]);
await db
.update(issues)
.set({
status: "done",
completedAt: new Date(),
checkoutRunId: null,
executionRunId: null,
updatedAt: new Date(),
})
.where(eq(issues.id, seeded.parentIssueId));
return {
exitCode: 0,
signal: null,
timedOut: false,
summary: "Parent completed after moving the workspace branch forward.",
provider: "test",
model: "test-model",
};
});
const parentRun = await wakeIssue(heartbeat, seeded.agentId, seeded.parentIssueId);
expect(parentRun).not.toBeNull();
const finishedParentRun = await waitForRunToFinish(heartbeat, parentRun!.id);
expect(finishedParentRun).toMatchObject({
status: "succeeded",
errorCode: null,
});
expect(sharedExecutionWorkspaceId).toEqual(expect.any(String));
expect(worktreePath).toEqual(expect.any(String));
expect(recordedBranch).toBe(`${seeded.parentIdentifier}-recorded`);
expect(reconciledBranch).toBe(`${seeded.parentIdentifier}-actual`);
expect(reconciledHead).toEqual(expect.stringMatching(/^[a-f0-9]{40}$/));
const [workspaceAfterParent] = await db
.select({
name: executionWorkspaces.name,
branchName: executionWorkspaces.branchName,
providerRef: executionWorkspaces.providerRef,
})
.from(executionWorkspaces)
.where(eq(executionWorkspaces.id, sharedExecutionWorkspaceId!));
expect(workspaceAfterParent).toMatchObject({
name: reconciledBranch,
branchName: reconciledBranch,
providerRef: worktreePath,
});
const [isolatedSiblingWorkspaceAfterParent] = await db
.select({ branchName: executionWorkspaces.branchName })
.from(executionWorkspaces)
.where(eq(executionWorkspaces.id, seeded.isolatedSiblingExecutionWorkspaceId));
expect(isolatedSiblingWorkspaceAfterParent?.branchName).toBe(seeded.isolatedSiblingBranch);
const parentOperations = await db
.select()
.from(workspaceOperations)
.where(eq(workspaceOperations.heartbeatRunId, parentRun!.id));
expect(parentOperations.filter((operation) =>
asRecord(operation.metadata).branchIncoherenceReconcileForward === true
)).toHaveLength(1);
const branchReconcileComments = await readContainmentComments(db, [seeded.parentIssueId]);
expect(branchReconcileComments).toEqual(
expect.arrayContaining([
expect.objectContaining({
authorType: "system",
body: expect.stringContaining("Execution workspace branch reconciled."),
}),
]),
);
adapterExecute.mockImplementationOnce(async (adapterInput) => {
const workspace = readAdapterWorkspace(adapterInput);
expect(workspace.executionWorkspaceId).toBe(sharedExecutionWorkspaceId);
expect(workspace.cwd).toBe(worktreePath);
expect(workspace.branchName).toBe(reconciledBranch);
await expect(readGit(workspace.cwd, ["branch", "--show-current"])).resolves.toBe(reconciledBranch);
await expect(readGit(workspace.cwd, ["rev-parse", "HEAD"])).resolves.toBe(reconciledHead);
await db
.update(issues)
.set({
status: "done",
completedAt: new Date(),
checkoutRunId: null,
executionRunId: null,
updatedAt: new Date(),
})
.where(eq(issues.id, seeded.childIssueId));
return {
exitCode: 0,
signal: null,
timedOut: false,
summary: "Git handoff child reused the reconciled workspace.",
provider: "test",
model: "test-model",
};
});
const childRun = await wakeIssue(heartbeat, seeded.agentId, seeded.childIssueId);
expect(childRun).not.toBeNull();
const finishedChildRun = await waitForRunToFinish(heartbeat, childRun!.id);
expect(finishedChildRun).toMatchObject({
status: "succeeded",
errorCode: null,
});
const [workspaceAfterChild] = await db
.select({
name: executionWorkspaces.name,
branchName: executionWorkspaces.branchName,
})
.from(executionWorkspaces)
.where(eq(executionWorkspaces.id, sharedExecutionWorkspaceId!));
expect(workspaceAfterChild).toMatchObject({
name: reconciledBranch,
branchName: reconciledBranch,
});
expect(workspaceAfterChild?.branchName).not.toBe(childTemplateBranch);
const childOperations = await db
.select()
.from(workspaceOperations)
.where(eq(workspaceOperations.heartbeatRunId, childRun!.id));
expect(childOperations.filter((operation) =>
asRecord(operation.metadata).branchIncoherenceReconcileForward === true
)).toHaveLength(0);
const [isolatedSiblingWorkspaceAfterChild] = await db
.select({ branchName: executionWorkspaces.branchName })
.from(executionWorkspaces)
.where(eq(executionWorkspaces.id, seeded.isolatedSiblingExecutionWorkspaceId));
expect(isolatedSiblingWorkspaceAfterChild?.branchName).toBe(seeded.isolatedSiblingBranch);
expect(adapterExecute).toHaveBeenCalledTimes(2);
}, 30_000);
});
+19 -2
View File
@@ -10779,6 +10779,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
workspaceConfigFreshness,
restoreExistingWorkspace: reusableExistingExecutionWorkspace
? () => ensurePersistedExecutionWorkspaceAvailable({
db,
base: executionWorkspaceBase,
workspace: {
id: reusableExistingExecutionWorkspace.id,
@@ -10806,10 +10807,14 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
name: agent.name,
companyId: agent.companyId,
},
heartbeatRunId: run.id,
enableWorkspaceBranchReconcileForward:
resolvedInstanceSettings.experimental.enableWorkspaceBranchReconcileForward,
recorder: workspaceOperationRecorder,
})
: null,
realizeWorkspace: () => realizeExecutionWorkspace({
db,
base: executionWorkspaceBase,
config: hostExecutionWorkspaceConfig,
issue: issueRef,
@@ -10818,6 +10823,9 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
name: agent.name,
companyId: agent.companyId,
},
heartbeatRunId: run.id,
enableWorkspaceBranchReconcileForward:
resolvedInstanceSettings.experimental.enableWorkspaceBranchReconcileForward,
recorder: workspaceOperationRecorder,
}),
});
@@ -11568,8 +11576,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
let inspection = branchInspection.inspection;
const initialManagedGitWorktreeBranch = formatManagedGitWorktreeBranchInspection(inspection);
if (!inspection.valid && inspection.reasonCode === "branch_mismatch" && inspection.repoRoot) {
let reconciledBranchName: string | null = null;
try {
await ensureGitWorktreeBranchCoherent({
const coherence = await ensureGitWorktreeBranchCoherent({
db,
repoRoot: inspection.repoRoot,
worktreePath: inspection.worktreePath,
expectedBranchName: inspection.expectedBranchName,
@@ -11583,8 +11593,15 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
}
: null,
executionWorkspaceId: branchInspection.workspaceRecord.id,
heartbeatRunId: run.id,
enableWorkspaceBranchReconcileForward:
resolvedInstanceSettings.experimental.enableWorkspaceBranchReconcileForward,
reconcileOperationPhase: "workspace_finalize",
recorder: workspaceOperationRecorder,
});
if (coherence.reconciledForward && coherence.branchName) {
reconciledBranchName = coherence.branchName;
}
} catch (repairErr) {
const workspaceValidationFailure = isWorkspaceValidationFailure(repairErr) ? repairErr : null;
finalizeBranchMetadata = {
@@ -11621,7 +11638,7 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {})
const repairedInspection = await inspectManagedGitWorktreeBranch({
worktreePath: inspection.worktreePath,
expectedBranchName: inspection.expectedBranchName,
expectedBranchName: reconciledBranchName ?? inspection.expectedBranchName,
repoRoot: inspection.repoRoot,
});
finalizeBranchRepairMetadata = {
+255 -7
View File
@@ -7,7 +7,7 @@ import path from "node:path";
import { setTimeout as delay } from "node:timers/promises";
import type { AdapterRuntimeServiceReport } from "@paperclipai/adapter-utils";
import type { Db } from "@paperclipai/db";
import { executionWorkspaces, projectWorkspaces, workspaceRuntimeServices } from "@paperclipai/db";
import { executionWorkspaces, issueComments, projectWorkspaces, workspaceRuntimeServices } from "@paperclipai/db";
import {
listWorkspaceServiceCommandDefinitions,
type GitWorktreeBranchAncestryVerdict,
@@ -28,6 +28,7 @@ import {
touchLocalServiceRegistryRecord,
writeLocalServiceRegistryRecord,
} from "./local-service-supervisor.js";
import { logActivity } from "./activity-log.js";
import type { WorkspaceOperationRecorder } from "./workspace-operations.js";
import { readExecutionWorkspaceConfig } from "./execution-workspaces.js";
import { readProjectWorkspaceRuntimeConfig } from "./project-workspace-runtime-config.js";
@@ -659,6 +660,11 @@ type GitWorktreeCleanliness = SharedGitWorktreeBranchIncoherenceEvidence["cleanl
type GitWorktreeBranchIncoherenceEvidence = SharedGitWorktreeBranchIncoherenceEvidence;
type GitWorktreeBranchCoherenceResult = {
branchName: string | null;
reconciledForward: boolean;
};
function formatBranchForMessage(branch: string | null | undefined) {
return branch && branch.length > 0 ? branch : "<detached>";
}
@@ -845,22 +851,181 @@ function branchIncoherenceValidationFailure(evidence: GitWorktreeBranchIncoheren
);
}
async function recordForwardBranchReconcileOperation(input: {
recorder?: WorkspaceOperationRecorder | null;
phase?: "worktree_prepare" | "workspace_finalize";
cwd: string;
repoRoot: string;
worktreePath: string;
expectedBranchName: string;
actualBranchName: string;
executionWorkspaceId: string | null;
sourceIssueId: string | null;
fingerprint: string;
expectedHeadSha: string | null;
actualHeadSha: string | null;
ancestryVerdict: GitWorktreeBranchAncestryVerdict;
mode: "record_updated" | "adopt_for_realize";
auditCommentId?: string | null;
}) {
if (!input.recorder) return;
await input.recorder.recordOperation({
phase: input.phase ?? "worktree_prepare",
cwd: input.cwd,
metadata: {
repoRoot: input.repoRoot,
worktreePath: input.worktreePath,
expectedBranchName: input.expectedBranchName,
actualBranchName: input.actualBranchName,
branchIncoherenceReconcileForward: true,
reconcileMode: input.mode,
fingerprint: input.fingerprint,
sourceIssueId: input.sourceIssueId,
executionWorkspaceId: input.executionWorkspaceId,
expectedHeadSha: input.expectedHeadSha,
actualHeadSha: input.actualHeadSha,
ancestryVerdict: input.ancestryVerdict,
auditCommentId: input.auditCommentId ?? null,
},
run: async () => ({
status: "succeeded",
system:
input.mode === "record_updated"
? `Reconciled execution workspace branch record from ${input.expectedBranchName} to ${input.actualBranchName}; worktree left unchanged.\n`
: `Adopted live git worktree branch ${input.actualBranchName} for this execution workspace realization; worktree left unchanged.\n`,
}),
});
}
function formatForwardBranchReconcileComment(input: {
workspaceId: string;
evidence: GitWorktreeBranchIncoherenceEvidence;
actualBranchName: string;
}) {
return [
"Execution workspace branch reconciled.",
"",
`- Workspace: \`${input.workspaceId}\``,
"- Mode: `forward`",
`- From branch: \`${input.evidence.expectedBranch}\``,
`- To branch: \`${input.actualBranchName}\``,
`- From SHA: \`${input.evidence.provenance.expectedHeadSha ?? "unknown"}\``,
`- To SHA: \`${input.evidence.provenance.actualHeadSha ?? "unknown"}\``,
`- Verdict: \`${input.evidence.provenance.ancestryVerdict}\``,
`- Fingerprint: \`${input.evidence.fingerprint}\``,
"- Operator reason: Automatic forward reconciliation: recorded branch is an ancestor of the checked-out branch.",
].join("\n");
}
async function reconcileForwardPersistedExecutionWorkspaceBranch(input: {
db: Db;
executionWorkspaceId: string;
evidence: GitWorktreeBranchIncoherenceEvidence;
actualBranchName: string;
heartbeatRunId?: string | null;
}) {
const existing = await input.db
.select()
.from(executionWorkspaces)
.where(eq(executionWorkspaces.id, input.executionWorkspaceId))
.then((rows) => rows[0] ?? null);
if (!existing) {
throw new Error(`Execution workspace ${input.executionWorkspaceId} not found for branch reconciliation`);
}
const recordedBranch = existing.branchName?.trim() || null;
if (recordedBranch && recordedBranch !== input.evidence.expectedBranch) {
if (recordedBranch === input.actualBranchName) {
return { branchName: input.actualBranchName, auditCommentId: null };
}
throw new Error(
`Execution workspace ${input.executionWorkspaceId} branch changed from "${input.evidence.expectedBranch}" to "${recordedBranch}" before reconciliation`,
);
}
const now = new Date();
await input.db
.update(executionWorkspaces)
.set({
branchName: input.actualBranchName,
...(existing.name === input.evidence.expectedBranch ? { name: input.actualBranchName } : {}),
updatedAt: now,
})
.where(eq(executionWorkspaces.id, existing.id));
const auditComment = existing.sourceIssueId
? await input.db
.insert(issueComments)
.values({
companyId: existing.companyId,
issueId: existing.sourceIssueId,
authorAgentId: null,
authorUserId: null,
authorType: "system",
createdByRunId: input.heartbeatRunId ?? null,
body: formatForwardBranchReconcileComment({
workspaceId: existing.id,
evidence: input.evidence,
actualBranchName: input.actualBranchName,
}),
})
.returning({ id: issueComments.id })
.then((rows) => rows[0] ?? null)
: null;
await logActivity(input.db, {
companyId: existing.companyId,
actorType: "system",
actorId: "workspace_runtime",
runId: input.heartbeatRunId ?? null,
action: "execution_workspace.branch_reconciled",
entityType: "execution_workspace",
entityId: existing.id,
details: {
mode: "forward",
reason: "Automatic forward reconciliation: recorded branch is an ancestor of the checked-out branch.",
fromBranch: input.evidence.expectedBranch,
toBranch: input.actualBranchName,
fromSha: input.evidence.provenance.expectedHeadSha,
toSha: input.evidence.provenance.actualHeadSha,
ancestryVerdict: input.evidence.provenance.ancestryVerdict,
fingerprint: input.evidence.fingerprint,
sourceIssueId: existing.sourceIssueId ?? input.evidence.sourceIssueId,
auditCommentId: auditComment?.id ?? null,
actor: {
type: "system",
id: "workspace_runtime",
source: "workspace_runtime",
},
},
});
return { branchName: input.actualBranchName, auditCommentId: auditComment?.id ?? null };
}
export async function ensureGitWorktreeBranchCoherent(input: {
db?: Db | null;
repoRoot: string;
worktreePath: string;
expectedBranchName: string | null;
sourceIssue: ExecutionWorkspaceIssueRef | null;
executionWorkspaceId?: string | null;
actualBranchName?: string | null;
heartbeatRunId?: string | null;
enableWorkspaceBranchReconcileForward?: boolean;
reconcileOperationPhase?: "worktree_prepare" | "workspace_finalize";
recorder?: WorkspaceOperationRecorder | null;
}) {
}): Promise<GitWorktreeBranchCoherenceResult> {
const expectedBranchName = input.expectedBranchName?.trim();
if (!expectedBranchName) return;
if (!expectedBranchName) return { branchName: null, reconciledForward: false };
const currentBranch = input.actualBranchName !== undefined
? input.actualBranchName
: await runGit(["symbolic-ref", "--quiet", "--short", "HEAD"], input.worktreePath).catch(() => null);
if (currentBranch === expectedBranchName) return;
if (currentBranch === expectedBranchName) {
return { branchName: expectedBranchName, reconciledForward: false };
}
const evidence = await inspectGitWorktreeBranchIncoherence({
repoRoot: input.repoRoot,
@@ -871,6 +1036,68 @@ export async function ensureGitWorktreeBranchCoherent(input: {
executionWorkspaceId: input.executionWorkspaceId ?? null,
});
if (
input.enableWorkspaceBranchReconcileForward === true &&
evidence.provenance.ancestryVerdict === "ancestor" &&
currentBranch
) {
if (input.executionWorkspaceId) {
if (!input.db) {
evidence.safeRepair.reason = "forward reconciliation requires database access to update the execution workspace record";
throw branchIncoherenceValidationFailure(evidence);
}
try {
const result = await reconcileForwardPersistedExecutionWorkspaceBranch({
db: input.db,
executionWorkspaceId: input.executionWorkspaceId,
evidence,
actualBranchName: currentBranch,
heartbeatRunId: input.heartbeatRunId ?? null,
});
await recordForwardBranchReconcileOperation({
recorder: input.recorder,
phase: input.reconcileOperationPhase,
cwd: input.worktreePath,
repoRoot: input.repoRoot,
worktreePath: evidence.worktreePath,
expectedBranchName,
actualBranchName: currentBranch,
executionWorkspaceId: input.executionWorkspaceId,
sourceIssueId: evidence.sourceIssueId,
fingerprint: evidence.fingerprint,
expectedHeadSha: evidence.provenance.expectedHeadSha,
actualHeadSha: evidence.provenance.actualHeadSha,
ancestryVerdict: evidence.provenance.ancestryVerdict,
mode: "record_updated",
auditCommentId: result.auditCommentId,
});
return { branchName: result.branchName, reconciledForward: true };
} catch (error) {
evidence.safeRepair.reason =
`forward reconciliation failed: ${error instanceof Error ? error.message : String(error)}`;
throw branchIncoherenceValidationFailure(evidence);
}
}
await recordForwardBranchReconcileOperation({
recorder: input.recorder,
phase: input.reconcileOperationPhase,
cwd: input.worktreePath,
repoRoot: input.repoRoot,
worktreePath: evidence.worktreePath,
expectedBranchName,
actualBranchName: currentBranch,
executionWorkspaceId: null,
sourceIssueId: evidence.sourceIssueId,
fingerprint: evidence.fingerprint,
expectedHeadSha: evidence.provenance.expectedHeadSha,
actualHeadSha: evidence.provenance.actualHeadSha,
ancestryVerdict: evidence.provenance.ancestryVerdict,
mode: "adopt_for_realize",
});
return { branchName: currentBranch, reconciledForward: true };
}
if (!evidence.safeRepair.eligible) {
throw branchIncoherenceValidationFailure(evidence);
}
@@ -910,6 +1137,7 @@ export async function ensureGitWorktreeBranchCoherent(input: {
evidence.safeRepair.succeeded = true;
evidence.safeRepair.reason = "clean worktree checked out the recorded branch";
return { branchName: expectedBranchName, reconciledForward: false };
}
// Resolve the authoritative base ref for a fresh worktree. A configured local
@@ -1603,10 +1831,13 @@ async function resolveGitRepoRootForWorkspaceCleanup(
}
export async function realizeExecutionWorkspace(input: {
db?: Db | null;
base: ExecutionWorkspaceInput;
config: Record<string, unknown>;
issue: ExecutionWorkspaceIssueRef | null;
agent: ExecutionWorkspaceAgentRef;
heartbeatRunId?: string | null;
enableWorkspaceBranchReconcileForward?: boolean;
recorder?: WorkspaceOperationRecorder | null;
}): Promise<RealizedExecutionWorkspace> {
const rawStrategy = parseObject(input.config.workspaceStrategy);
@@ -1632,7 +1863,7 @@ export async function realizeExecutionWorkspace(input: {
projectId: input.base.projectId,
repoRef: input.base.repoRef,
});
const branchName = sanitizeBranchName(renderedBranch);
let branchName = sanitizeBranchName(renderedBranch);
const configuredParentDir = asString(rawStrategy.worktreeParentDir, "");
const worktreeParentDir = configuredParentDir
? resolveConfiguredPath(configuredParentDir, repoRoot)
@@ -1725,15 +1956,22 @@ export async function realizeExecutionWorkspace(input: {
expectedBranchName: branchName,
}).catch(() => null);
if (validation && !validation.valid && validation.reasonCode === "branch_mismatch") {
await ensureGitWorktreeBranchCoherent({
const coherence = await ensureGitWorktreeBranchCoherent({
db: input.db ?? null,
repoRoot,
worktreePath: reusablePath,
expectedBranchName: branchName,
actualBranchName: validation.actualBranchName ?? null,
sourceIssue: input.issue,
executionWorkspaceId: null,
heartbeatRunId: input.heartbeatRunId ?? null,
enableWorkspaceBranchReconcileForward: input.enableWorkspaceBranchReconcileForward === true,
reconcileOperationPhase: "worktree_prepare",
recorder: input.recorder ?? null,
});
if (coherence.reconciledForward && coherence.branchName) {
branchName = coherence.branchName;
}
return await validateLinkedGitWorktree({
repoRoot,
worktreePath: reusablePath,
@@ -1837,6 +2075,7 @@ export async function realizeExecutionWorkspace(input: {
}
export async function ensurePersistedExecutionWorkspaceAvailable(input: {
db?: Db | null;
base: ExecutionWorkspaceInput;
workspace: {
id?: string | null;
@@ -1856,6 +2095,8 @@ export async function ensurePersistedExecutionWorkspaceAvailable(input: {
};
issue: ExecutionWorkspaceIssueRef | null;
agent: ExecutionWorkspaceAgentRef;
heartbeatRunId?: string | null;
enableWorkspaceBranchReconcileForward?: boolean;
recorder?: WorkspaceOperationRecorder | null;
}): Promise<RealizedExecutionWorkspace | null> {
const cwd = asString(input.workspace.cwd ?? input.workspace.providerRef, "").trim();
@@ -1891,14 +2132,21 @@ export async function ensurePersistedExecutionWorkspaceAvailable(input: {
const reuseBaseRef = input.workspace.baseRef ?? input.base.repoRef ?? null;
const reuseWorktreePath = realized.worktreePath ?? cwd;
if (await isGitCheckout(reuseWorktreePath)) {
await ensureGitWorktreeBranchCoherent({
const coherence = await ensureGitWorktreeBranchCoherent({
db: input.db ?? null,
repoRoot,
worktreePath: reuseWorktreePath,
expectedBranchName: realized.branchName,
sourceIssue: input.issue,
executionWorkspaceId: input.workspace.id ?? null,
heartbeatRunId: input.heartbeatRunId ?? null,
enableWorkspaceBranchReconcileForward: input.enableWorkspaceBranchReconcileForward === true,
reconcileOperationPhase: "worktree_prepare",
recorder: input.recorder ?? null,
});
if (coherence.reconciledForward && coherence.branchName) {
realized.branchName = coherence.branchName;
}
}
const validation = await validateLinkedGitWorktree({
repoRoot,