FN-7750: gate shared-branch-group auto-merge exemption on live groups

Fixes autoMerge=false being bypassed for engine-created branch-group member tasks whose branch group had already dissolved/finalized.

- Add isLiveSharedBranchGroupMemberIntegration(task, group) in @fusion/core, requiring the branch group's status be "open" before the shared-branch-member exemption bypasses the global/task autoMerge:false hold.
- Export the new helper from packages/core/src/index.ts and index.gate.ts.
- Thread the live-group check through packages/engine/src/project-engine.ts (allowInReviewMergeProcessing, enqueueEligibleInReviewTasks, merge-confirmed fast-path branch routing, and merge handoff paths).
- Add TaskExecutor.isLiveSharedBranchGroupMember helper in packages/engine/src/executor.ts and use it in retryable pre-merge remediation, no-op finalize, benign pause-abort classification, and merge-processing gates.
- Keep self-healing.ts's solo no-op finalize predicate on the pure branchContext-shape check (isSharedBranchGroupMemberIntegration) intentionally, so stale shared-group members stay excluded from solo finalize regardless of group liveness.
- Add regression tests covering the executor and project-engine auto-merge-hold behavior for stale/dissolved branch groups.
- Add a patch changeset documenting the fix.

Files changed:
 .../fn-7750-automerge-hold-stale-branch-group.md   |  7 ++
 packages/core/src/__tests__/task-merge.test.ts     | 42 +++++++++--
 packages/core/src/index.gate.ts                    |  1 +
 packages/core/src/index.ts                         |  1 +
 packages/core/src/task-merge.ts                    | 13 +++-
 ...cutor-live-branch-group-auto-merge-hold.test.ts | 85 ++++++++++++++++++++++
 .../engine/src/__tests__/project-engine.test.ts    | 37 +++++++++-
 packages/engine/src/executor.ts                    | 22 ++++--
 packages/engine/src/project-engine.ts              | 32 +++++---
 packages/engine/src/self-healing.ts                |  1 +
 10 files changed, 214 insertions(+), 27 deletions(-)

Fusion-Task-Id: FN-7750

Fusion-Task-Lineage: d61f8847-0b09-49b5-b66a-00018c8738bb

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-09 15:25:29 -07:00
parent 786a274ae6
commit eb377ba831
10 changed files with 214 additions and 27 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Manual merge hold now applies to shared-branch-group tasks whose group has dissolved.
category: fix
dev: `isLiveSharedBranchGroupMemberIntegration(task, group)` gates the shared-member auto-merge-off exemption on a live (`status: "open"`) branch group; a missing/finalized/abandoned group degrades to the standalone manual-hold path. Threaded through `project-engine.ts allowInReviewMergeProcessing` and the `executor.ts` merge gates. Fixes issue #1980 (FN-7750).

View File

@@ -14,6 +14,7 @@ import {
isTaskReadyForMerge,
allowsAutoMergeProcessing,
isSharedBranchGroupMemberIntegration,
isLiveSharedBranchGroupMemberIntegration,
resolveEffectiveAutoMerge,
resolveEffectiveGroupAutoMerge,
resolveTaskMergeTarget,
@@ -164,14 +165,16 @@ describe("resolveEffectiveGroupAutoMerge", () => {
});
describe("isSharedBranchGroupMemberIntegration", () => {
const sharedTask = {
branchContext: {
assignmentMode: "shared" as const,
groupId: "BG-1",
source: "planning" as const,
},
};
it("returns true for shared members with a resolvable group id", () => {
expect(isSharedBranchGroupMemberIntegration({
branchContext: {
assignmentMode: "shared",
groupId: "BG-1",
source: "planning",
},
})).toBe(true);
expect(isSharedBranchGroupMemberIntegration(sharedTask)).toBe(true);
});
it("returns false for per-task-derived grouped members", () => {
@@ -197,6 +200,31 @@ describe("isSharedBranchGroupMemberIntegration", () => {
it("returns false when branch context is absent", () => {
expect(isSharedBranchGroupMemberIntegration({ branchContext: undefined })).toBe(false);
});
it("requires a live open group for auto-merge-off shared-member integration", () => {
expect(isLiveSharedBranchGroupMemberIntegration(sharedTask, { status: "open" })).toBe(true);
expect(isLiveSharedBranchGroupMemberIntegration(sharedTask, { status: "finalized" })).toBe(false);
expect(isLiveSharedBranchGroupMemberIntegration(sharedTask, { status: "abandoned" })).toBe(false);
expect(isLiveSharedBranchGroupMemberIntegration(sharedTask, null)).toBe(false);
expect(isLiveSharedBranchGroupMemberIntegration(sharedTask, undefined)).toBe(false);
});
it("does not grant the live-group exemption to non-shared or blank-group contexts", () => {
expect(isLiveSharedBranchGroupMemberIntegration({
branchContext: {
assignmentMode: "per-task-derived",
groupId: "BG-1",
source: "planning",
},
}, { status: "open" })).toBe(false);
expect(isLiveSharedBranchGroupMemberIntegration({
branchContext: {
assignmentMode: "shared",
groupId: " ",
source: "planning",
},
}, { status: "open" })).toBe(false);
});
});
describe("resolveTaskMergeTarget", () => {

View File

@@ -904,6 +904,7 @@ export {
isTaskReadyForMerge,
allowsAutoMergeProcessing,
isSharedBranchGroupMemberIntegration,
isLiveSharedBranchGroupMemberIntegration,
resolveEffectiveAutoMerge,
resolveEffectiveGroupAutoMerge,
resolveTaskMergeTarget,

View File

@@ -913,6 +913,7 @@ export {
isTaskReadyForMerge,
allowsAutoMergeProcessing,
isSharedBranchGroupMemberIntegration,
isLiveSharedBranchGroupMemberIntegration,
resolveEffectiveAutoMerge,
resolveEffectiveGroupAutoMerge,
resolveTaskMergeTarget,

View File

@@ -63,7 +63,7 @@ export function resolveEffectiveAutoMerge(
* FNXC:PrAutoMergeGate 2026-06-28-00:33:
* FN-7182: a dashboard-created open PR is a human handoff, so exclude it from all automatic merge processing and self-healing recovery until the human merges or closes the PR.
* This mirrors the `autoMerge:false` in-review gate while preserving manual Merge PR/manual done paths and pipeline PRs without `manual: true`.
* Shared-branch member integration still bypasses this function via `allowInReviewMergeProcessing(... ) || isSharedBranchGroupMemberIntegration(task)`, so a manual PR on a shared member can still be integrated to its group branch; group-to-default promotion remains gated separately.
* Shared-branch member integration still bypasses this function via `allowInReviewMergeProcessing(... ) || isLiveSharedBranchGroupMemberIntegration(task, group)`, so a manual PR on a live shared member can still be integrated to its group branch; group-to-default promotion remains gated separately.
*/
export function allowsAutoMergeProcessing(
task: Pick<Task, "autoMerge" | "prInfo" | "prInfos">,
@@ -93,6 +93,17 @@ export function isSharedBranchGroupMemberIntegration(
&& Boolean(task.branchContext.groupId?.trim());
}
/**
* FNXC:AutoMergeHold 2026-07-09-16:42:
* FN-7750 / Runfusion#1980: the `autoMerge:false` exemption for shared-branch members is valid only while the branch group is live. Missing, finalized, abandoned, or dissolved groups must degrade to the standalone manual-hold path so operator Merge & Close control is honored regardless of whether the task was API-, user-, or engine-created.
*/
export function isLiveSharedBranchGroupMemberIntegration(
task: Pick<Task, "branchContext">,
group: Pick<BranchGroup, "status"> | null | undefined,
): boolean {
return isSharedBranchGroupMemberIntegration(task) && group != null && group.status === "open";
}
export function resolveTaskMergeTarget(
task: Pick<Task, "baseBranch" | "branchContext">,
options: MergeTargetResolverOptions = {},

View File

@@ -0,0 +1,85 @@
import { describe, expect, it, vi } from "vitest";
import "./executor-test-helpers.js";
import { TaskExecutor } from "../executor.js";
import { createMockStore } from "./executor-test-helpers.js";
import type { TaskDetail } from "@fusion/core";
const now = "2026-07-09T17:18:00.000Z";
function makeInReviewTask(overrides: Partial<TaskDetail> = {}): TaskDetail {
return {
id: "FN-1980",
title: "engine-created stale branch-group member",
description: "Reproduces Runfusion/Fusion#1980 stale branch-group auto-merge-off bypass",
column: "in-review",
dependencies: [],
steps: [{ name: "Implement", status: "done" }],
currentStep: 0,
log: [],
branch: "fusion/fn-1980",
baseBranch: "main",
worktree: "/tmp/fusion-fn-1980",
status: "reviewing",
error: null,
paused: false,
userPaused: false,
autoMerge: undefined,
mergeRetries: 0,
createdAt: now,
updatedAt: now,
branchContext: { assignmentMode: "shared", groupId: "BG-STALE", source: "mission" },
sourceType: "unknown",
sourceMetadata: {
fusionBranchContext: { assignmentMode: "shared", groupId: "BG-STALE", source: "mission" },
},
...overrides,
} as TaskDetail;
}
function makeExecutor(branchGroup: { status: "open" | "finalized" | "abandoned" } | null) {
const store = createMockStore();
store.getSettings.mockResolvedValue({
maxConcurrent: 2,
maxWorktrees: 4,
pollIntervalMs: 15_000,
autoMerge: false,
maxAutoMergeRetries: 3,
});
store.getBranchGroup = vi.fn(() => branchGroup);
const executor = new TaskExecutor(store, "/tmp/test", {});
return { executor, store };
}
const mergeAbortResult = {
visitedNodeIds: ["merge"],
context: { "node:merge:value": "aborted" },
};
describe("executor shared-branch autoMerge:false liveness gates", () => {
it("does not route an engine-created dissolved-group member to auto-merge retry", async () => {
const { executor, store } = makeExecutor(null);
const task = makeInReviewTask();
const retryable = await (executor as any).isRetryableBenignMergePauseAbort(
task,
mergeAbortResult,
"merge-seam",
true,
);
expect(retryable).toBe(false);
expect(store.getBranchGroup).toHaveBeenCalledWith("BG-STALE");
});
it("still routes live shared-group members through the local integration retry gate", async () => {
const { executor } = makeExecutor({ status: "open" });
const task = makeInReviewTask();
await expect((executor as any).isRetryableBenignMergePauseAbort(
task,
mergeAbortResult,
"merge-seam",
true,
)).resolves.toBe(true);
});
});

View File

@@ -244,6 +244,7 @@ function createMockStore(initialSettings: Record<string, unknown>) {
emit: vi.fn(),
addTaskComment: vi.fn(async () => undefined),
getActiveMergingTask: vi.fn(() => null),
getBranchGroup: vi.fn(() => null),
on: vi.fn((event: string, handler: (...args: unknown[]) => void | Promise<void>) => {
if (event === "settings:updated") {
settingsHandlers.add(handler as (payload: SettingsHandlerPayload) => void | Promise<void>);
@@ -2546,6 +2547,7 @@ describe("ProjectEngine paused in-review auto-merge behavior", () => {
it("startup merge sweep enqueues shared-group members when autoMerge is false", async () => {
const mockStore = createMockStore({ ...baseSettings, autoMerge: false });
mockStore.store.getBranchGroup.mockReturnValue({ id: "BG-5819", status: "open", branchName: "fusion/groups/bg-5819" });
mockStore.store.listTasks.mockResolvedValueOnce([
{
id: "FN-shared",
@@ -3321,8 +3323,8 @@ describe("ProjectEngine stale mergeActive rescue (FN-3900)", () => {
});
describe("allowInReviewMergeProcessing per-task autoMerge override", () => {
const gate = (task: Partial<Task>, settings: { autoMerge: boolean }) =>
(createEngine() as any).allowInReviewMergeProcessing(task, settings) as boolean;
const gate = (task: Partial<Task>, settings: { autoMerge: boolean }, branchGroup: { status: "open" | "finalized" | "abandoned" } | null = null) =>
(createEngine() as any).allowInReviewMergeProcessing(task, settings, { getBranchGroup: vi.fn(() => branchGroup) }) as boolean;
it("lets an explicit per-task autoMerge:true through when the global setting is off", () => {
expect(gate({ autoMerge: true }, { autoMerge: false })).toBe(true);
@@ -3338,12 +3340,41 @@ describe("allowInReviewMergeProcessing per-task autoMerge override", () => {
expect(gate({ autoMerge: false }, { autoMerge: true })).toBe(true);
});
it("still exempts shared-branch-group member integration when the global setting is off", () => {
it("still exempts live shared-branch-group member integration when the global setting is off", () => {
expect(gate(
{ branchContext: { assignmentMode: "shared", groupId: "grp-1" } as Task["branchContext"] },
{ autoMerge: false },
{ status: "open" },
)).toBe(true);
});
it.each([
["missing", null],
["finalized", { status: "finalized" as const }],
["abandoned", { status: "abandoned" as const }],
])("blocks shared-branch-group member integration for %s groups when global autoMerge is off", (_label, branchGroup) => {
expect(gate(
{ branchContext: { assignmentMode: "shared", groupId: "grp-1" } as Task["branchContext"] },
{ autoMerge: false },
branchGroup,
)).toBe(false);
});
it.each([
["api", { sourceType: "api" }],
["user-created", { sourceType: undefined }],
["engine-created", { sourceType: "unknown", sourceMetadata: { fusionBranchContext: { assignmentMode: "shared", groupId: "grp-1", source: "mission" } } }],
])("applies the dissolved-group manual hold regardless of %s provenance", (_label, provenance) => {
expect(gate(
{
...provenance,
autoMerge: undefined,
branchContext: { assignmentMode: "shared", groupId: "grp-1", source: "mission" } as Task["branchContext"],
},
{ autoMerge: false },
null,
)).toBe(false);
});
});
// ## Surface Enumeration

View File

@@ -11,7 +11,7 @@ import { existsSync, lstatSync, realpathSync } from "node:fs";
import { readFile, rm, writeFile } from "node:fs/promises";
import type { TaskStore, Task, TaskDetail, TaskTokenUsage, StepStatus, Settings, WorkflowStep, MissionStore, Slice, AgentState, AgentCapability, RunMutationContext, AgentHeartbeatConfig, Agent, AgentMemoryInclusionMode, ProjectSettings, MergeResult, WorkflowIrNode, WorkflowIrNodeKind, WorkflowStepResult as CoreWorkflowStepResult } from "@fusion/core";
import { getUnmetSchedulingDependencies } from "./scheduler.js";
import { RetryStormError, TaskDeletedError, serializeRetryStormError, isExperimentalFeatureEnabled, resolveWorkflowIrForTask, resolveColumnAgentBinding, resolveEffectiveAgent, instanceNodeId, getWorkflowExtensionRegistry, getBuiltinWorkflow, parseNoOpCompletionMarker, allowsAutoMergeProcessing, resolveEffectiveAutoMerge, isSharedBranchGroupMemberIntegration, resolveMaxAutoMergeRetries, resolveOptionalStepRevisionBudget, resolveOptionalReviewRevisionBudget, COMPLETION_SUMMARY_NODE_ID, upsertWorkflowStepResult, AWAITING_APPROVAL_PAUSE_REASON } from "@fusion/core";
import { RetryStormError, TaskDeletedError, serializeRetryStormError, isExperimentalFeatureEnabled, resolveWorkflowIrForTask, resolveColumnAgentBinding, resolveEffectiveAgent, instanceNodeId, getWorkflowExtensionRegistry, getBuiltinWorkflow, parseNoOpCompletionMarker, allowsAutoMergeProcessing, resolveEffectiveAutoMerge, isLiveSharedBranchGroupMemberIntegration, resolveMaxAutoMergeRetries, resolveOptionalStepRevisionBudget, resolveOptionalReviewRevisionBudget, COMPLETION_SUMMARY_NODE_ID, upsertWorkflowStepResult, AWAITING_APPROVAL_PAUSE_REASON } from "@fusion/core";
import { finalizeProvenAutoMergeTask } from "./auto-merge-finalization.js";
import { mergeEffectiveSettings } from "./effective-settings.js";
import type { TaskStep, WorkflowIr, WorkflowFieldDefinition, WorkflowColumnAgent, EffectiveAgentInput, WorkflowWorkEngineDispatchResult } from "@fusion/core";
@@ -8015,6 +8015,12 @@ export class TaskExecutor {
};
}
private isLiveSharedBranchGroupMember(live: Pick<TaskDetail, "branchContext">): boolean {
const groupId = live.branchContext?.groupId?.trim();
const branchGroup = groupId ? this.store.getBranchGroup(groupId) : null;
return isLiveSharedBranchGroupMemberIntegration(live, branchGroup);
}
private async routeRetryableRemediationGraphFailureToPreMergeFix(
live: TaskDetail,
failedNode: string | undefined,
@@ -8030,7 +8036,8 @@ export class TaskExecutor {
if (!live.worktree) return false;
const settings = await this.store.getSettings().catch(() => undefined);
if (!settings || settings.globalPause === true || settings.enginePaused === true) return false;
if (!allowsAutoMergeProcessing(live, settings) && !isSharedBranchGroupMemberIntegration(live)) return false;
/* FNXC:AutoMergeHold 2026-07-09-17:04: FN-7750 requires retryable pre-merge remediation to treat stale shared-group members as standalone manual-hold rows when global auto-merge is off; only live/open groups retain the shared-member exemption. */
if (!allowsAutoMergeProcessing(live, settings) && !this.isLiveSharedBranchGroupMember(live)) return false;
const target = this.latestFailedPreMergeWorkflowStep(live);
if (!target) return false;
const budget = await this.resolveFailedPreMergeWorkflowStepBudget(live, target);
@@ -8084,7 +8091,7 @@ export class TaskExecutor {
} catch {
return false;
}
const sharedBranchMember = isSharedBranchGroupMemberIntegration(live);
const sharedBranchMember = this.isLiveSharedBranchGroupMember(live);
if (!sharedBranchMember && !allowsAutoMergeProcessing(live, settings)) return false;
if (!sharedBranchMember && resolveEffectiveAutoMerge(live, settings) === false) return false;
if ((live.mergeRetries ?? 0) >= resolveMaxAutoMergeRetries(settings)) return false;
@@ -8152,7 +8159,8 @@ export class TaskExecutor {
} catch {
return false;
}
if (isSharedBranchGroupMemberIntegration(live)) return false;
/* FNXC:AutoMergeHold 2026-07-09-17:07: FN-7749's benign manual-hold classifier must exclude only live shared-group integrations. FN-7750 stale shared-group members are standalone manual-hold rows and should not be stranded as pause-abort failures. */
if (this.isLiveSharedBranchGroupMember(live)) return false;
return !allowsAutoMergeProcessing(live, settings) || resolveEffectiveAutoMerge(live, settings) === false;
}
@@ -8193,7 +8201,7 @@ export class TaskExecutor {
return false;
}
if (settings.globalPause === true || settings.enginePaused === true) return false;
if (!allowsAutoMergeProcessing(live, settings) && !isSharedBranchGroupMemberIntegration(live)) return false;
if (!allowsAutoMergeProcessing(live, settings) && !this.isLiveSharedBranchGroupMember(live)) return false;
this.clearPausedAborted(live.id);
this.activeWorktrees.delete(live.id);
@@ -8266,7 +8274,7 @@ export class TaskExecutor {
return false;
}
if (settings.globalPause === true || settings.enginePaused === true) return false;
if (!allowsAutoMergeProcessing(live, settings) && !isSharedBranchGroupMemberIntegration(live)) return false;
if (!allowsAutoMergeProcessing(live, settings) && !this.isLiveSharedBranchGroupMember(live)) return false;
const nextRetries = priorRetries + 1;
this.clearPausedAborted(live.id);
@@ -8372,7 +8380,7 @@ export class TaskExecutor {
if (live.column === "in-review") {
if (live.autoMerge === false) return false;
if (!settings) return false;
const sharedBranchMember = isSharedBranchGroupMemberIntegration(live);
const sharedBranchMember = this.isLiveSharedBranchGroupMember(live);
if (!sharedBranchMember && !allowsAutoMergeProcessing(live, settings)) return false;
if (live.mergeDetails?.mergeConfirmed === true) return false;
}

View File

@@ -26,6 +26,7 @@ import {
emitOverseerRetry,
emitOverseerSteering,
getTaskHardMergeBlocker,
isLiveSharedBranchGroupMemberIntegration,
isSharedBranchGroupMemberIntegration,
isWorkspaceTask,
normalizeMergerMode,
@@ -1817,8 +1818,8 @@ export class ProjectEngine {
async requestInterpreterMerge(taskId: string, options: { signal?: AbortSignal } = {}): Promise<MergeResult> {
let task: Task | null = null;
let settings: Settings | undefined;
const store = this.runtime.getTaskStore();
try {
const store = this.runtime.getTaskStore();
settings = await store.getSettings();
task = await store.getTask(taskId);
} catch {
@@ -1827,7 +1828,7 @@ export class ProjectEngine {
const eligible = !!task && !!settings
&& task.column === "in-review"
&& !settings.globalPause && !settings.enginePaused
&& this.allowInReviewMergeProcessing(task, settings)
&& this.allowInReviewMergeProcessing(task, settings, store)
&& !(task.paused && !task.mergeDetails?.mergeConfirmed);
if (!eligible) {
// A null task means the lookup failed or the task was deleted; never hand
@@ -2286,8 +2287,14 @@ export class ProjectEngine {
* pushed wins. listTasks returns createdAt ASC — without this sort an
* older low-priority task would start before a later urgent one.
*/
private allowInReviewMergeProcessing(task: Pick<Task, "branchContext" | "autoMerge">, settings: Pick<Settings, "autoMerge">): boolean {
return allowsAutoMergeProcessing(task, settings) || isSharedBranchGroupMemberIntegration(task);
private allowInReviewMergeProcessing(task: Pick<Task, "branchContext" | "autoMerge">, settings: Pick<Settings, "autoMerge">, store: Partial<Pick<TaskStore, "getBranchGroup">> = this.runtime.getTaskStore()): boolean {
const groupId = task.branchContext?.groupId?.trim();
const branchGroup = groupId ? store.getBranchGroup?.(groupId) : null;
/*
FNXC:AutoMergeHold 2026-07-09-16:53:
FN-7750 / Runfusion#1980: shared-branch member integration may bypass the global `autoMerge:false` hold only while its group row is still open. Stale, finalized, abandoned, or missing groups must flow through the standalone manual-hold gate so no task provenance can solo auto-merge to main.
*/
return allowsAutoMergeProcessing(task, settings) || isLiveSharedBranchGroupMemberIntegration(task, branchGroup);
}
private async emitLegacyAutoMergeStampAdvisory(store: TaskStore): Promise<void> {
@@ -2330,7 +2337,7 @@ export class ProjectEngine {
private enqueueEligibleInReviewTasks(tasks: readonly Task[], settings: Pick<Settings, "autoMerge" | "maxAutoMergeRetries">): number {
const maxAutoMergeRetries = resolveMaxAutoMergeRetries(settings);
const eligible = sortTasksByPriorityThenAgeAndId(
tasks.filter((t) => !t.paused && this.canMergeTask(t as any, maxAutoMergeRetries) && this.allowInReviewMergeProcessing(t, settings)) as Task[],
tasks.filter((t) => !t.paused && this.canMergeTask(t as any, maxAutoMergeRetries) && this.allowInReviewMergeProcessing(t, settings, this.runtime.getTaskStore())) as Task[],
);
for (const t of eligible) {
this.internalEnqueueMerge(t.id);
@@ -2544,7 +2551,7 @@ export class ProjectEngine {
if (!task || task.column !== "in-review") {
continue;
}
if (!this.allowInReviewMergeProcessing(task, settings)) {
if (!this.allowInReviewMergeProcessing(task, settings, store)) {
runtimeLog.log(`Auto-merge skipping ${taskId} — autoMerge disabled`);
continue;
}
@@ -2586,9 +2593,16 @@ export class ProjectEngine {
// silently promote the poisoned row to `done` — exactly the
// false-positive completion class that lost FN-5612/5613/5614/5616/
// 5623/5625 work on 2026-05-27/28.
const branchGroupForFastPath = isSharedBranchGroupMemberIntegration(task)
const branchGroupForFastPathCandidate = isSharedBranchGroupMemberIntegration(task)
? (store as any).getBranchGroup?.(task.branchContext?.groupId)
: null;
/*
FNXC:AutoMergeHold 2026-07-09-16:58:
FN-7750: merge-confirmed fast-path rerouting to a branch-group integration branch is safe only for a live/open group. A missing or terminal group must leave the row on its stored standalone target instead of reviving a stale group route that could bypass the manual merge hold.
*/
const branchGroupForFastPath = isLiveSharedBranchGroupMemberIntegration(task, branchGroupForFastPathCandidate)
? branchGroupForFastPathCandidate
: null;
const routedFastPathTarget = branchGroupForFastPath?.branchName?.trim();
const integrationBranchForGate =
routedFastPathTarget || task.mergeDetails.mergeTargetBranch || task.baseBranch || "main";
@@ -4049,7 +4063,7 @@ export class ProjectEngine {
runtimeLog.log(`Auto-merge handoff (${task.id}) skipped: ${settings.globalPause ? "globalPause" : "enginePaused"} active`);
return;
}
if (!this.allowInReviewMergeProcessing(latestTask, settings)) {
if (!this.allowInReviewMergeProcessing(latestTask, settings, store)) {
runtimeLog.log(`Auto-merge handoff (${task.id}) skipped: autoMerge disabled`);
return;
}
@@ -4166,7 +4180,7 @@ export class ProjectEngine {
try {
const settings = await store.getSettings();
if (settings.globalPause || settings.enginePaused || !this.allowInReviewMergeProcessing(task, settings)) {
if (settings.globalPause || settings.enginePaused || !this.allowInReviewMergeProcessing(task, settings, store)) {
return;
}
if (this.options.getTaskMergeBlocker?.(task)) {

View File

@@ -5935,6 +5935,7 @@ export class SelfHealingManager {
t.column === "in-review" &&
allowsAutoMergeProcessing(t, settings) &&
!t.paused &&
// FNXC:AutoMergeHold 2026-07-09-17:10: FN-7750 intentionally keeps the pure branchContext-shape predicate here. Stale shared-group members must stay OUT of solo no-op finalize even when their group is not live; only the positive auto-merge-off exemption gates use the live-group predicate.
!isSharedBranchGroupMemberIntegration(t) &&
// FNXC:Workspace 2026-06-22-14:10 (Phase D review A — workspace single-commit-finalize gate):
// This no-op finalize classifies one branch against one base over `this.options.rootDir`