FN-5819: allow shared-group members to merge into group branch with auto-merge off
Allow shared branch-group members to continue member→group integration even when project auto-merge is disabled. - add core helper to detect shared branch-group member integrations and export it - update engine in-review enqueue/handoff/startup flows to permit shared-member integration while still honoring global/engine pause gates - keep self-healing from pulling shared-member in-review tasks backward during auto-merge-off maintenance - expand project-engine and reliability-interaction tests for shared-member autoMerge-off behavior and add FN-5819 backstop docs updates Files changed: AGENTS.md | 2 + docs/architecture.md | 3 +- packages/core/src/__tests__/task-merge.test.ts | 37 +++++++++ packages/core/src/index.ts | 1 + packages/core/src/task-merge.ts | 13 +++ packages/engine/src/__tests__/project-engine.test.ts | 64 ++++++++++++++ packages/engine/src/__tests__/reliability-interactions/branch-group-merge-routing.test.ts | 48 +++++++++++ packages/engine/src/__tests__/reliability-interactions/shared-group-member-integration.test.ts | 97 ++++++++++++++++++++++ packages/engine/src/project-engine.ts | 35 ++++---- packages/engine/src/self-healing.ts | 3 +- 10 files changed, 284 insertions(+), 19 deletions(-) Fusion-Task-Id: FN-5819 Fusion-Task-Lineage: b5eeced7-7c47-4573-9ab4-ca577bec65e1
This commit is contained in:
@@ -10,7 +10,7 @@ import type {
|
||||
ScheduledTask,
|
||||
AutomationRunResult,
|
||||
} from "@fusion/core";
|
||||
import { compareTasksByPriorityThenAgeAndId, getTaskHardMergeBlocker, normalizeMergerMode, sortTasksByPriorityThenAgeAndId } from "@fusion/core";
|
||||
import { compareTasksByPriorityThenAgeAndId, getTaskHardMergeBlocker, isSharedBranchGroupMemberIntegration, normalizeMergerMode, sortTasksByPriorityThenAgeAndId } from "@fusion/core";
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { InProcessRuntime } from "./runtimes/in-process-runtime.js";
|
||||
@@ -1375,9 +1375,13 @@ 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 enqueueEligibleInReviewTasks(tasks: readonly Task[]): number {
|
||||
private allowInReviewMergeProcessing(task: Pick<Task, "branchContext">, settings: Pick<Settings, "autoMerge">): boolean {
|
||||
return settings.autoMerge || isSharedBranchGroupMemberIntegration(task);
|
||||
}
|
||||
|
||||
private enqueueEligibleInReviewTasks(tasks: readonly Task[], settings: Pick<Settings, "autoMerge">): number {
|
||||
const eligible = sortTasksByPriorityThenAgeAndId(
|
||||
tasks.filter((t) => !t.paused && this.canMergeTask(t as any)) as Task[],
|
||||
tasks.filter((t) => !t.paused && this.canMergeTask(t as any) && this.allowInReviewMergeProcessing(t, settings)) as Task[],
|
||||
);
|
||||
for (const t of eligible) {
|
||||
this.internalEnqueueMerge(t.id);
|
||||
@@ -1487,15 +1491,14 @@ export class ProjectEngine {
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (!settings.autoMerge) {
|
||||
runtimeLog.log(`Auto-merge skipping ${taskId} — autoMerge disabled`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const task = await store.getTask(taskId);
|
||||
if (!task || task.column !== "in-review") {
|
||||
continue;
|
||||
}
|
||||
if (!this.allowInReviewMergeProcessing(task, settings)) {
|
||||
runtimeLog.log(`Auto-merge skipping ${taskId} — autoMerge disabled`);
|
||||
continue;
|
||||
}
|
||||
if (task.paused && !task.mergeDetails?.mergeConfirmed) {
|
||||
runtimeLog.log(`Auto-merge skipping ${taskId} — task is paused`);
|
||||
continue;
|
||||
@@ -2570,7 +2573,7 @@ export class ProjectEngine {
|
||||
runtimeLog.log(`Auto-merge handoff (${task.id}) skipped: ${settings.globalPause ? "globalPause" : "enginePaused"} active`);
|
||||
return;
|
||||
}
|
||||
if (!settings.autoMerge) {
|
||||
if (!this.allowInReviewMergeProcessing(latestTask, settings)) {
|
||||
runtimeLog.log(`Auto-merge handoff (${task.id}) skipped: autoMerge disabled`);
|
||||
return;
|
||||
}
|
||||
@@ -2687,7 +2690,7 @@ export class ProjectEngine {
|
||||
|
||||
try {
|
||||
const settings = await store.getSettings();
|
||||
if (settings.globalPause || settings.enginePaused || !settings.autoMerge) {
|
||||
if (settings.globalPause || settings.enginePaused || !this.allowInReviewMergeProcessing(task, settings)) {
|
||||
return;
|
||||
}
|
||||
if (this.options.getTaskMergeBlocker?.(task)) {
|
||||
@@ -2759,10 +2762,8 @@ export class ProjectEngine {
|
||||
}
|
||||
|
||||
const settings = await store.getSettings();
|
||||
if (!settings.autoMerge) return;
|
||||
|
||||
|
||||
const enqueued = this.enqueueEligibleInReviewTasks(tasks as Task[]);
|
||||
const enqueued = this.enqueueEligibleInReviewTasks(tasks as Task[], settings);
|
||||
if (enqueued > 0) {
|
||||
runtimeLog.log(`Auto-merge startup sweep: enqueueing ${enqueued} task(s)`);
|
||||
}
|
||||
@@ -2819,9 +2820,9 @@ export class ProjectEngine {
|
||||
|
||||
try {
|
||||
const settings = await store.getSettings();
|
||||
if (!settings.globalPause && !settings.enginePaused && settings.autoMerge) {
|
||||
if (!settings.globalPause && !settings.enginePaused) {
|
||||
const tasks = await store.listTasks({ column: "in-review" });
|
||||
this.enqueueEligibleInReviewTasks(tasks as Task[]);
|
||||
this.enqueueEligibleInReviewTasks(tasks as Task[], settings);
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
runtimeLog.warn(
|
||||
@@ -2876,13 +2877,13 @@ export class ProjectEngine {
|
||||
);
|
||||
}
|
||||
|
||||
if (settings.globalPause || settings.enginePaused || !settings.autoMerge) {
|
||||
if (settings.globalPause || settings.enginePaused) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const tasks = await store.listTasks({ column: "in-review" });
|
||||
this.enqueueEligibleInReviewTasks(tasks as Task[]);
|
||||
this.enqueueEligibleInReviewTasks(tasks as Task[], settings);
|
||||
} catch (err: unknown) {
|
||||
runtimeLog.warn(
|
||||
`${source}: failed to scan in-review tasks for auto-merge: ${err instanceof Error ? err.message : String(err)}`,
|
||||
|
||||
Reference in New Issue
Block a user