FN-5818: fix shared-branch-group execution to use per-task working branches
Ensure shared branch-group flows consistently derive and acquire per-task working branches. - update engine merge/executor/scheduler/self-healing paths to resolve task-scoped working branch names instead of shared branch-group names - adjust worktree acquisition and already-merged detection logic to use the corrected branch resolution - add regression coverage for shared-branch-group working-branch behavior, worktree acquisition, and worktree name derivation - add a patch changeset for @runfusion/fusion Files changed: .changeset/fn-5818-shared-branch-working-branch.md | 5 ++ .../shared-branch-group-working-branch.test.ts | 53 ++++++++++++++++++++++ .../src/__tests__/worktree-acquisition.test.ts | 52 +++++++++++++++++++++ .../engine/src/__tests__/worktree-names.test.ts | 20 +++++++- packages/engine/src/already-merged-detector.ts | 6 +-- packages/engine/src/executor.ts | 10 ++-- packages/engine/src/merger-ai.ts | 4 +- packages/engine/src/merger.ts | 16 +++---- packages/engine/src/scheduler.ts | 6 +-- packages/engine/src/self-healing.ts | 12 ++--- packages/engine/src/worktree-acquisition.ts | 4 +- packages/engine/src/worktree-names.ts | 9 +++- 12 files changed, 166 insertions(+), 31 deletions(-) Fusion-Task-Id: FN-5818 Fusion-Task-Lineage: c7eb1ec4-971a-49ad-9156-5c0349c629aa
This commit is contained in:
5
.changeset/fn-5818-shared-branch-working-branch.md
Normal file
5
.changeset/fn-5818-shared-branch-working-branch.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix shared branch-group execution to always derive per-task working branches (`fusion/<task-id>`) for checkout/worktree operations while keeping the branch-group branch as the merge target.
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { acquireTaskWorktree } from "../../worktree-acquisition.js";
|
||||||
|
|
||||||
|
describe("shared branch group working branch regression", () => {
|
||||||
|
it("uses per-task working branches for shared members and keeps existing derivation modes", async () => {
|
||||||
|
const store = {
|
||||||
|
updateTask: vi.fn().mockResolvedValue(undefined),
|
||||||
|
logEntry: vi.fn().mockResolvedValue(undefined),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const createWorktree = vi.fn(async (branchName: string, worktreePath: string) => ({ path: worktreePath, branch: branchName }));
|
||||||
|
const shared = { assignmentMode: "shared", groupId: "BG-1", source: "planning" } as const;
|
||||||
|
|
||||||
|
const [a, b] = await Promise.all([
|
||||||
|
acquireTaskWorktree({
|
||||||
|
task: { id: "FN-201", title: "a", description: "a", branch: "clionboarding", branchContext: shared, worktree: null } as any,
|
||||||
|
rootDir: process.cwd(),
|
||||||
|
store,
|
||||||
|
settings: {},
|
||||||
|
createWorktree,
|
||||||
|
}),
|
||||||
|
acquireTaskWorktree({
|
||||||
|
task: { id: "FN-202", title: "b", description: "b", branch: "clionboarding", branchContext: shared, worktree: null } as any,
|
||||||
|
rootDir: process.cwd(),
|
||||||
|
store,
|
||||||
|
settings: {},
|
||||||
|
createWorktree,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(a.branch).toBe("fusion/fn-201");
|
||||||
|
expect(b.branch).toBe("fusion/fn-202");
|
||||||
|
expect(a.branch).not.toBe(b.branch);
|
||||||
|
|
||||||
|
const perTask = await acquireTaskWorktree({
|
||||||
|
task: { id: "FN-203", title: "c", description: "c", branch: "fusion/custom", branchContext: { assignmentMode: "per-task-derived", groupId: "BG-1", source: "planning" }, worktree: null } as any,
|
||||||
|
rootDir: process.cwd(),
|
||||||
|
store,
|
||||||
|
settings: {},
|
||||||
|
createWorktree,
|
||||||
|
});
|
||||||
|
const ungrouped = await acquireTaskWorktree({
|
||||||
|
task: { id: "FN-204", title: "d", description: "d", branch: null, worktree: null } as any,
|
||||||
|
rootDir: process.cwd(),
|
||||||
|
store,
|
||||||
|
settings: {},
|
||||||
|
createWorktree,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(perTask.branch).toBe("fusion/custom");
|
||||||
|
expect(ungrouped.branch).toBe("fusion/fn-204");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -109,6 +109,58 @@ describe("acquireTaskWorktree", () => {
|
|||||||
expect(vi.mocked(branchConflicts.reanchorBranchToBase)).not.toHaveBeenCalled();
|
expect(vi.mocked(branchConflicts.reanchorBranchToBase)).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("derives distinct per-task working branches for shared branch-group members", async () => {
|
||||||
|
const createWorktree = vi.fn(async (branchName: string, worktreePath: string) => ({ path: worktreePath, branch: branchName }));
|
||||||
|
const sharedBranch = "clionboarding";
|
||||||
|
const sharedContext = { assignmentMode: "shared", groupId: "BG-1", source: "planning" } as const;
|
||||||
|
|
||||||
|
const [first, second] = await Promise.all([
|
||||||
|
acquireTaskWorktree({
|
||||||
|
task: { ...task, id: "FN-100", worktree: null, branch: sharedBranch, branchContext: sharedContext },
|
||||||
|
rootDir: process.cwd(),
|
||||||
|
store,
|
||||||
|
settings: {},
|
||||||
|
createWorktree,
|
||||||
|
}),
|
||||||
|
acquireTaskWorktree({
|
||||||
|
task: { ...task, id: "FN-101", worktree: null, branch: sharedBranch, branchContext: sharedContext },
|
||||||
|
rootDir: process.cwd(),
|
||||||
|
store,
|
||||||
|
settings: {},
|
||||||
|
createWorktree,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(first.branch).toBe("fusion/fn-100");
|
||||||
|
expect(second.branch).toBe("fusion/fn-101");
|
||||||
|
expect(first.branch).not.toBe(second.branch);
|
||||||
|
expect(createWorktree).toHaveBeenCalledWith("fusion/fn-100", expect.any(String), "FN-100", undefined, false);
|
||||||
|
expect(createWorktree).toHaveBeenCalledWith("fusion/fn-101", expect.any(String), "FN-101", undefined, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps per-task-derived and ungrouped branch derivation unchanged", async () => {
|
||||||
|
const createWorktree = vi.fn(async (branchName: string, worktreePath: string) => ({ path: worktreePath, branch: branchName }));
|
||||||
|
|
||||||
|
const perTaskDerived = await acquireTaskWorktree({
|
||||||
|
task: { ...task, id: "FN-102", worktree: null, branch: "fusion/custom-derived", branchContext: { assignmentMode: "per-task-derived", groupId: "BG-1", source: "planning" } },
|
||||||
|
rootDir: process.cwd(),
|
||||||
|
store,
|
||||||
|
settings: {},
|
||||||
|
createWorktree,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ungrouped = await acquireTaskWorktree({
|
||||||
|
task: { ...task, id: "FN-103", worktree: null, branch: null },
|
||||||
|
rootDir: process.cwd(),
|
||||||
|
store,
|
||||||
|
settings: {},
|
||||||
|
createWorktree,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(perTaskDerived.branch).toBe("fusion/custom-derived");
|
||||||
|
expect(ungrouped.branch).toBe("fusion/fn-103");
|
||||||
|
});
|
||||||
|
|
||||||
it("acquires from pool when enabled", async () => {
|
it("acquires from pool when enabled", async () => {
|
||||||
const prepareForTask = vi.fn().mockResolvedValue({ branch: "fusion/fn-1", worktreePath: "/tmp/pooled", reclaimed: false });
|
const prepareForTask = vi.fn().mockResolvedValue({ branch: "fusion/fn-1", worktreePath: "/tmp/pooled", reclaimed: false });
|
||||||
const release = vi.fn();
|
const release = vi.fn();
|
||||||
|
|||||||
@@ -2,7 +2,25 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|||||||
import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
|
import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { generateWorktreeName, ADJECTIVES, NOUNS } from "../worktree-names.js";
|
import { generateWorktreeName, ADJECTIVES, NOUNS, resolveTaskWorkingBranch } from "../worktree-names.js";
|
||||||
|
|
||||||
|
describe("resolveTaskWorkingBranch", () => {
|
||||||
|
it("returns canonical per-task branch for shared assignment mode", () => {
|
||||||
|
expect(resolveTaskWorkingBranch({ id: "FN-5818", branch: "clionboarding", branchContext: { assignmentMode: "shared", groupId: "bg-1", source: "planning" } })).toBe("fusion/fn-5818");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns explicit branch for per-task-derived assignment mode", () => {
|
||||||
|
expect(resolveTaskWorkingBranch({ id: "FN-5818", branch: "fusion/custom", branchContext: { assignmentMode: "per-task-derived", groupId: "bg-1", source: "planning" } })).toBe("fusion/custom");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns canonical branch for ungrouped task without branch", () => {
|
||||||
|
expect(resolveTaskWorkingBranch({ id: "FN-5818", branch: undefined })).toBe("fusion/fn-5818");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns explicit branch for ungrouped task with branch", () => {
|
||||||
|
expect(resolveTaskWorkingBranch({ id: "FN-5818", branch: "feature/fn-5818" })).toBe("feature/fn-5818");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("generateWorktreeName", () => {
|
describe("generateWorktreeName", () => {
|
||||||
let tempDir: string;
|
let tempDir: string;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { exec, execSync } from "node:child_process";
|
import { exec, execSync } from "node:child_process";
|
||||||
import { promisify } from "node:util";
|
import { promisify } from "node:util";
|
||||||
|
|
||||||
import { canonicalFusionBranchName } from "./worktree-names.js";
|
import { resolveTaskWorkingBranch } from "./worktree-names.js";
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ export async function findAlreadyMergedTaskCommit(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let branchTip: string | null = null;
|
let branchTip: string | null = null;
|
||||||
const branchName = taskBranch || canonicalFusionBranchName(taskId);
|
const branchName = resolveTaskWorkingBranch({ id: taskId, branch: taskBranch });
|
||||||
try {
|
try {
|
||||||
branchTip = execSync(`git rev-parse --verify ${shellQuote(branchName)}`, {
|
branchTip = execSync(`git rev-parse --verify ${shellQuote(branchName)}`, {
|
||||||
cwd: repoDir,
|
cwd: repoDir,
|
||||||
@@ -184,7 +184,7 @@ export async function findAlreadyMergedTaskCommit(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const treeBranchName = taskBranch || canonicalFusionBranchName(taskId);
|
const treeBranchName = resolveTaskWorkingBranch({ id: taskId, branch: taskBranch });
|
||||||
execSync(`git rev-parse --verify ${shellQuote(treeBranchName)}`, {
|
execSync(`git rev-parse --verify ${shellQuote(treeBranchName)}`, {
|
||||||
cwd: repoDir,
|
cwd: repoDir,
|
||||||
encoding: "utf-8",
|
encoding: "utf-8",
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import {
|
|||||||
VERIFICATION_LOG_MAX_CHARS,
|
VERIFICATION_LOG_MAX_CHARS,
|
||||||
type VerificationResult,
|
type VerificationResult,
|
||||||
} from "./verification-utils.js";
|
} from "./verification-utils.js";
|
||||||
import { canonicalFusionBranchName, generateWorktreeName } from "./worktree-names.js";
|
import { generateWorktreeName, resolveTaskWorkingBranch } from "./worktree-names.js";
|
||||||
import { resolveTaskWorktreePath, resolveWorktreesDir } from "./worktree-paths.js";
|
import { resolveTaskWorktreePath, resolveWorktreesDir } from "./worktree-paths.js";
|
||||||
import { Type, type Static } from "@earendil-works/pi-ai";
|
import { Type, type Static } from "@earendil-works/pi-ai";
|
||||||
import { describeModel, promptWithFallback, compactSessionContext } from "./pi.js";
|
import { describeModel, promptWithFallback, compactSessionContext } from "./pi.js";
|
||||||
@@ -3480,7 +3480,7 @@ export class TaskExecutor {
|
|||||||
taskId: task.id,
|
taskId: task.id,
|
||||||
worktreePath,
|
worktreePath,
|
||||||
rootDir: this.rootDir,
|
rootDir: this.rootDir,
|
||||||
branch: task.branch ?? undefined,
|
branch: acquisition.branch ?? undefined,
|
||||||
});
|
});
|
||||||
const pathPrepend = runtimeEnvContribution?.pathPrepend ?? [];
|
const pathPrepend = runtimeEnvContribution?.pathPrepend ?? [];
|
||||||
const injectedEnv = runtimeEnvContribution?.env ?? {};
|
const injectedEnv = runtimeEnvContribution?.env ?? {};
|
||||||
@@ -5929,7 +5929,7 @@ export class TaskExecutor {
|
|||||||
allowReanchor = true,
|
allowReanchor = true,
|
||||||
): Promise<{ ok: true } | { ok: false; reason: "wrong_toplevel" | "wrong_branch" | "no_commits"; observed: string; expected: string }> {
|
): Promise<{ ok: true } | { ok: false; reason: "wrong_toplevel" | "wrong_branch" | "no_commits"; observed: string; expected: string }> {
|
||||||
const settings = await this.store.getSettings();
|
const settings = await this.store.getSettings();
|
||||||
const branchName = task.branch || canonicalFusionBranchName(task.id);
|
const branchName = resolveTaskWorkingBranch(task);
|
||||||
const worktreePath = worktreePathOverride ?? task.worktree ?? this.activeWorktrees.get(task.id) ?? null;
|
const worktreePath = worktreePathOverride ?? task.worktree ?? this.activeWorktrees.get(task.id) ?? null;
|
||||||
|
|
||||||
if (!worktreePath) {
|
if (!worktreePath) {
|
||||||
@@ -6764,7 +6764,7 @@ export class TaskExecutor {
|
|||||||
|
|
||||||
// Delete the branch — use stored branch name if available, fall back to convention
|
// Delete the branch — use stored branch name if available, fall back to convention
|
||||||
const task = await this.store.getTask(taskId);
|
const task = await this.store.getTask(taskId);
|
||||||
const branch = task.branch || canonicalFusionBranchName(taskId);
|
const branch = resolveTaskWorkingBranch(task);
|
||||||
let branchDeleted = false;
|
let branchDeleted = false;
|
||||||
try {
|
try {
|
||||||
await execAsync(`git branch -D "${branch}"`, { cwd: this.rootDir });
|
await execAsync(`git branch -D "${branch}"`, { cwd: this.rootDir });
|
||||||
@@ -10342,7 +10342,7 @@ Backward compat fallback: if JSON is unavailable, you may still begin output wit
|
|||||||
);
|
);
|
||||||
if (completedSteps.length === 0) return;
|
if (completedSteps.length === 0) return;
|
||||||
|
|
||||||
const branchName = task.branch || canonicalFusionBranchName(task.id);
|
const branchName = resolveTaskWorkingBranch(task);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if the branch has any unique commits vs main
|
// Check if the branch has any unique commits vs main
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ import {
|
|||||||
type Task,
|
type Task,
|
||||||
type TaskStore,
|
type TaskStore,
|
||||||
} from "@fusion/core";
|
} from "@fusion/core";
|
||||||
import { canonicalFusionBranchName } from "./worktree-names.js";
|
import { resolveTaskWorkingBranch } from "./worktree-names.js";
|
||||||
import { resolveIntegrationBranch } from "./integration-branch.js";
|
import { resolveIntegrationBranch } from "./integration-branch.js";
|
||||||
import { advanceIntegrationBranchRef } from "./merger-ref-update-advance.js";
|
import { advanceIntegrationBranchRef } from "./merger-ref-update-advance.js";
|
||||||
import { createResolvedAgentSession, resolveMergerSessionModel } from "./agent-session-helpers.js";
|
import { createResolvedAgentSession, resolveMergerSessionModel } from "./agent-session-helpers.js";
|
||||||
@@ -710,7 +710,7 @@ export async function runAiMerge(
|
|||||||
deps: AgentDeps = {},
|
deps: AgentDeps = {},
|
||||||
): Promise<MergeResult> {
|
): Promise<MergeResult> {
|
||||||
const task = await store.getTask(taskId);
|
const task = await store.getTask(taskId);
|
||||||
const branch = task.branch || canonicalFusionBranchName(taskId);
|
const branch = resolveTaskWorkingBranch(task);
|
||||||
|
|
||||||
if (task.column === "done" || task.column === "archived") {
|
if (task.column === "done" || task.column === "archived") {
|
||||||
return noOpResult(task, branch, "already-finalized");
|
return noOpResult(task, branch, "already-finalized");
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ import { existsSync, readFileSync, readdirSync, writeFileSync, unlinkSync, renam
|
|||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { resolveTaskWorktreePath } from "./worktree-paths.js";
|
import { resolveTaskWorktreePath } from "./worktree-paths.js";
|
||||||
import { canonicalFusionBranchName } from "./worktree-names.js";
|
import { resolveTaskWorkingBranch } from "./worktree-names.js";
|
||||||
import {
|
import {
|
||||||
collectOwnTaskCommitsForRange,
|
collectOwnTaskCommitsForRange,
|
||||||
filterFilesToOwnTaskCommits,
|
filterFilesToOwnTaskCommits,
|
||||||
@@ -707,7 +707,7 @@ export async function classifyOwnedLandedEvidence(
|
|||||||
task: Task,
|
task: Task,
|
||||||
opts: { mergeTargetBranch: string },
|
opts: { mergeTargetBranch: string },
|
||||||
): Promise<OwnedLandedClassification> {
|
): Promise<OwnedLandedClassification> {
|
||||||
const branch = task.branch || canonicalFusionBranchName(task.id);
|
const branch = resolveTaskWorkingBranch(task);
|
||||||
const mergeTargetBranch = opts.mergeTargetBranch;
|
const mergeTargetBranch = opts.mergeTargetBranch;
|
||||||
|
|
||||||
const ownedCommit = await findOwnedLandedCommitForTask(rootDir, task);
|
const ownedCommit = await findOwnedLandedCommitForTask(rootDir, task);
|
||||||
@@ -3189,7 +3189,7 @@ async function tryRecoverHardFailApply(params: {
|
|||||||
task,
|
task,
|
||||||
taskId,
|
taskId,
|
||||||
rootDir,
|
rootDir,
|
||||||
branch: task.branch || canonicalFusionBranchName(taskId),
|
branch: resolveTaskWorkingBranch(task),
|
||||||
mergeTargetBranch: task.baseBranch || "main",
|
mergeTargetBranch: task.baseBranch || "main",
|
||||||
conflictFiles: threeWayConflicted,
|
conflictFiles: threeWayConflicted,
|
||||||
auditor: undefined,
|
auditor: undefined,
|
||||||
@@ -3632,7 +3632,7 @@ export async function restoreUnrelatedRootDirChanges(
|
|||||||
task,
|
task,
|
||||||
taskId,
|
taskId,
|
||||||
rootDir,
|
rootDir,
|
||||||
branch: task.branch || canonicalFusionBranchName(taskId),
|
branch: resolveTaskWorkingBranch(task),
|
||||||
mergeTargetBranch: task.baseBranch || "main",
|
mergeTargetBranch: task.baseBranch || "main",
|
||||||
conflictFiles: conflictedFiles,
|
conflictFiles: conflictedFiles,
|
||||||
auditor: undefined,
|
auditor: undefined,
|
||||||
@@ -7176,7 +7176,7 @@ async function tryEarlyEmptyOwnDiffFinalize(input: {
|
|||||||
completeTask: (result: MergeResult) => Promise<void>;
|
completeTask: (result: MergeResult) => Promise<void>;
|
||||||
}): Promise<MergeResult | null> {
|
}): Promise<MergeResult | null> {
|
||||||
const { task, taskId, store, audit, log, projectRootDir, mergeTargetBranch } = input;
|
const { task, taskId, store, audit, log, projectRootDir, mergeTargetBranch } = input;
|
||||||
const branch = task.branch || canonicalFusionBranchName(taskId);
|
const branch = resolveTaskWorkingBranch(task);
|
||||||
|
|
||||||
// 1. Branch exists?
|
// 1. Branch exists?
|
||||||
try {
|
try {
|
||||||
@@ -7405,7 +7405,7 @@ export async function aiMergeTask(
|
|||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
task,
|
task,
|
||||||
branch: task.branch || canonicalFusionBranchName(taskId),
|
branch: resolveTaskWorkingBranch(task),
|
||||||
merged: false,
|
merged: false,
|
||||||
noOp: true,
|
noOp: true,
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -7517,7 +7517,7 @@ export async function aiMergeTask(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const integrationBranch = groupRouting ? mergeTarget.branch : resolvedIntegrationBranch;
|
const integrationBranch = groupRouting ? mergeTarget.branch : resolvedIntegrationBranch;
|
||||||
let branch = task.branch || canonicalFusionBranchName(taskId);
|
let branch = resolveTaskWorkingBranch(task);
|
||||||
|
|
||||||
const mergeRunId = generateSyntheticRunId("merge", taskId);
|
const mergeRunId = generateSyntheticRunId("merge", taskId);
|
||||||
const engineRunContext: EngineRunContext = {
|
const engineRunContext: EngineRunContext = {
|
||||||
@@ -7633,7 +7633,7 @@ export async function aiMergeTask(
|
|||||||
// so the pool's `leased` map stays consistent. Without that fall-through
|
// so the pool's `leased` map stays consistent. Without that fall-through
|
||||||
// the new path would bypass pool bookkeeping and could collide with
|
// the new path would bypass pool bookkeeping and could collide with
|
||||||
// `PoolDoubleLeaseError`.
|
// `PoolDoubleLeaseError`.
|
||||||
const expectedBranch = task.branch || canonicalFusionBranchName(taskId);
|
const expectedBranch = resolveTaskWorkingBranch(task);
|
||||||
// FN-4954: when a worktree pool is attached and recycling is enabled, pool
|
// FN-4954: when a worktree pool is attached and recycling is enabled, pool
|
||||||
// semantics REQUIRE going through `acquireTaskWorktree` so `WorktreePool`'s
|
// semantics REQUIRE going through `acquireTaskWorktree` so `WorktreePool`'s
|
||||||
// lease bookkeeping stays consistent. Skip the direct-reuse shortcut here
|
// lease bookkeeping stays consistent. Skip the direct-reuse shortcut here
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { existsSync } from "node:fs";
|
|||||||
import { readFile } from "node:fs/promises";
|
import { readFile } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import type { AgentSemaphore } from "./concurrency.js";
|
import type { AgentSemaphore } from "./concurrency.js";
|
||||||
import { canonicalFusionBranchName, planTaskWorktreePath } from "./worktree-names.js";
|
import { planTaskWorktreePath, resolveTaskWorkingBranch } from "./worktree-names.js";
|
||||||
import { schedulerLog } from "./logger.js";
|
import { schedulerLog } from "./logger.js";
|
||||||
import { type PrMonitor, type PrComment } from "./pr-monitor.js";
|
import { type PrMonitor, type PrComment } from "./pr-monitor.js";
|
||||||
import { reconcileMissionFeatureState } from "./mission-feature-sync.js";
|
import { reconcileMissionFeatureState } from "./mission-feature-sync.js";
|
||||||
@@ -1053,7 +1053,7 @@ export class Scheduler {
|
|||||||
for (const depId of task.dependencies) {
|
for (const depId of task.dependencies) {
|
||||||
const dep = allTasks.find((t) => t.id === depId);
|
const dep = allTasks.find((t) => t.id === depId);
|
||||||
if (dep && dep.column === "in-review" && dep.worktree) {
|
if (dep && dep.column === "in-review" && dep.worktree) {
|
||||||
return dep.branch || canonicalFusionBranchName(dep.id);
|
return resolveTaskWorkingBranch(dep);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1061,7 +1061,7 @@ export class Scheduler {
|
|||||||
if (task.blockedBy) {
|
if (task.blockedBy) {
|
||||||
const blocker = allTasks.find((t) => t.id === task.blockedBy);
|
const blocker = allTasks.find((t) => t.id === task.blockedBy);
|
||||||
if (blocker && blocker.column === "in-review" && blocker.worktree) {
|
if (blocker && blocker.column === "in-review" && blocker.worktree) {
|
||||||
return blocker.branch || canonicalFusionBranchName(blocker.id);
|
return resolveTaskWorkingBranch(blocker);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ import { AutoRecoveryDispatcher } from "./auto-recovery.js";
|
|||||||
import { activeSessionRegistry, executingTaskLock } from "./active-session-registry.js";
|
import { activeSessionRegistry, executingTaskLock } from "./active-session-registry.js";
|
||||||
import { findAlreadyMergedTaskCommit } from "./already-merged-detector.js";
|
import { findAlreadyMergedTaskCommit } from "./already-merged-detector.js";
|
||||||
import { resolveWorktreesDir } from "./worktree-paths.js";
|
import { resolveWorktreesDir } from "./worktree-paths.js";
|
||||||
import { canonicalFusionBranchName } from "./worktree-names.js";
|
import { canonicalFusionBranchName, resolveTaskWorkingBranch } from "./worktree-names.js";
|
||||||
import { resolveIntegrationBranch } from "./integration-branch.js";
|
import { resolveIntegrationBranch } from "./integration-branch.js";
|
||||||
import type { OwnedLandedClassification } from "./merger.js";
|
import type { OwnedLandedClassification } from "./merger.js";
|
||||||
import { regenerateBareMergeSubject } from "./merger-bare-subject.js";
|
import { regenerateBareMergeSubject } from "./merger-bare-subject.js";
|
||||||
@@ -499,7 +499,7 @@ export async function isBranchAheadOfBase(
|
|||||||
rootDir: string,
|
rootDir: string,
|
||||||
preferredBaseRef?: string,
|
preferredBaseRef?: string,
|
||||||
): Promise<{ aheadCount: number; baseRef: string } | null> {
|
): Promise<{ aheadCount: number; baseRef: string } | null> {
|
||||||
const branchName = task.branch || canonicalFusionBranchName(task.id);
|
const branchName = resolveTaskWorkingBranch(task);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await execAsync(`git rev-parse --verify ${shellQuote(branchName)}`, {
|
await execAsync(`git rev-parse --verify ${shellQuote(branchName)}`, {
|
||||||
@@ -1166,7 +1166,7 @@ export class SelfHealingManager {
|
|||||||
);
|
);
|
||||||
if (completedSteps.length === 0) return;
|
if (completedSteps.length === 0) return;
|
||||||
|
|
||||||
const branchName = task.branch || canonicalFusionBranchName(task.id);
|
const branchName = resolveTaskWorkingBranch(task);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { stdout: mergeBaseOut } = await execAsync(
|
const { stdout: mergeBaseOut } = await execAsync(
|
||||||
@@ -1431,7 +1431,7 @@ export class SelfHealingManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const branch = task.branch || canonicalFusionBranchName(task.id);
|
const branch = resolveTaskWorkingBranch(task);
|
||||||
try {
|
try {
|
||||||
await execAsync(`git branch -D ${shellQuote(branch)}`, {
|
await execAsync(`git branch -D ${shellQuote(branch)}`, {
|
||||||
cwd: this.options.rootDir,
|
cwd: this.options.rootDir,
|
||||||
@@ -2954,7 +2954,7 @@ export class SelfHealingManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const branchName = task?.branch || canonicalFusionBranchName(taskId);
|
const branchName = task ? resolveTaskWorkingBranch(task) : canonicalFusionBranchName(taskId);
|
||||||
const hintedWorktreePath = options?.worktreeHint;
|
const hintedWorktreePath = options?.worktreeHint;
|
||||||
let worktreePath = hintedWorktreePath;
|
let worktreePath = hintedWorktreePath;
|
||||||
if (!worktreePath || !existsSync(worktreePath)) {
|
if (!worktreePath || !existsSync(worktreePath)) {
|
||||||
@@ -7558,7 +7558,7 @@ export class SelfHealingManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const branchName = task.branch || canonicalFusionBranchName(task.id);
|
const branchName = resolveTaskWorkingBranch(task);
|
||||||
try {
|
try {
|
||||||
await execAsync(`git rev-parse --verify "${branchName}"`, {
|
await execAsync(`git rev-parse --verify "${branchName}"`, {
|
||||||
cwd: this.options.rootDir,
|
cwd: this.options.rootDir,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { existsSync } from "node:fs";
|
|||||||
import { exec } from "node:child_process";
|
import { exec } from "node:child_process";
|
||||||
import { promisify } from "node:util";
|
import { promisify } from "node:util";
|
||||||
import type { RunMutationContext, Settings, Task, TaskStore, SecretsStore } from "@fusion/core";
|
import type { RunMutationContext, Settings, Task, TaskStore, SecretsStore } from "@fusion/core";
|
||||||
import { canonicalFusionBranchName, generateWorktreeName, slugify } from "./worktree-names.js";
|
import { generateWorktreeName, resolveTaskWorkingBranch, slugify } from "./worktree-names.js";
|
||||||
import { resolveTaskWorktreePathForBackend } from "./worktree-paths.js";
|
import { resolveTaskWorktreePathForBackend } from "./worktree-paths.js";
|
||||||
import { hydrateWorktreeDb } from "./worktree-db-hydrate.js";
|
import { hydrateWorktreeDb } from "./worktree-db-hydrate.js";
|
||||||
import { formatError } from "./logger.js";
|
import { formatError } from "./logger.js";
|
||||||
@@ -202,7 +202,7 @@ export async function acquireTaskWorktree(opts: AcquireTaskWorktreeOptions): Pro
|
|||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
const branchName = task.branch || canonicalFusionBranchName(task.id);
|
const branchName = resolveTaskWorkingBranch(task);
|
||||||
const naming = settings.worktreeNaming || "random";
|
const naming = settings.worktreeNaming || "random";
|
||||||
const allowSiblingBranchRename = settings.executorAllowSiblingBranchRename === true;
|
const allowSiblingBranchRename = settings.executorAllowSiblingBranchRename === true;
|
||||||
const baseBranch = task.executionStartBranch || null;
|
const baseBranch = task.executionStartBranch || null;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { readdirSync } from "node:fs";
|
import { readdirSync } from "node:fs";
|
||||||
import { existsSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
import type { Settings } from "@fusion/core";
|
import type { Settings, Task } from "@fusion/core";
|
||||||
import { resolveTaskWorktreePath, resolveWorktreesDir } from "./worktree-paths.js";
|
import { resolveTaskWorktreePath, resolveWorktreesDir } from "./worktree-paths.js";
|
||||||
|
|
||||||
export const ADJECTIVES = [
|
export const ADJECTIVES = [
|
||||||
@@ -33,6 +33,13 @@ export function canonicalFusionBranchName(taskId: string): string {
|
|||||||
return `fusion/${taskId.toLowerCase()}`;
|
return `fusion/${taskId.toLowerCase()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveTaskWorkingBranch(task: Pick<Task, "id" | "branch" | "branchContext">): string {
|
||||||
|
if (task.branchContext?.assignmentMode === "shared") {
|
||||||
|
return canonicalFusionBranchName(task.id);
|
||||||
|
}
|
||||||
|
return task.branch || canonicalFusionBranchName(task.id);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a string to a URL-friendly slug.
|
* Convert a string to a URL-friendly slug.
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user