FN-5830: re-land shared branch promotion gate and API

Reintroduce shared-branch-group completion gating and promotion endpoints with reliability coverage.

- add GroupMergeCoordinator promotion gate behavior and shared branch group promotion logic updates
- expose promotion flow wiring through engine index and project engine APIs
- expand coordinator tests and add reliability interaction coverage for branch-group promotion
- document the new reliability backstop and add a changeset for @runfusion/fusion

Files changed:
 .changeset/fn-5830-branch-group-promotion.md       |   5 +
 AGENTS.md                                          |   1 +
 docs/architecture.md                               |   3 +-
 .../src/__tests__/group-merge-coordinator.test.ts  | 170 ++++++++++++++++++-
 .../branch-group-promotion.test.ts                 | 166 +++++++++++++++++++
 packages/engine/src/group-merge-coordinator.ts     | 183 ++++++++++++++++++++-
 packages/engine/src/index.ts                       |   4 +
 packages/engine/src/project-engine.ts              |  37 +++++
 8 files changed, 566 insertions(+), 3 deletions(-)

Fusion-Task-Id: FN-5830

Fusion-Task-Lineage: e9823f51-df0e-4da8-8f51-58dfefdc293d
This commit is contained in:
gsxdsm
2026-06-01 05:39:01 -07:00
parent eb425d17d9
commit 3373c0beed
8 changed files with 566 additions and 3 deletions

View File

@@ -27,6 +27,7 @@ import { CronRunner, createAiPromptExecutor } from "./cron-runner.js";
import type { RoutineRunner } from "./routine-runner.js";
import { aiMergeTask, sweepStaleAutostashes, VerificationError } from "./merger.js";
import { runAiMerge } from "./merger-ai.js";
import { promoteBranchGroup } from "./group-merge-coordinator.js";
import { PRIORITY_MERGE } from "./concurrency.js";
import { runtimeLog } from "./logger.js";
import type { HeartbeatTriggerScheduler } from "./agent-heartbeat.js";
@@ -1782,6 +1783,39 @@ export class ProjectEngine {
}
const mergeStrategy = this.options.getMergeStrategy?.(settings) ?? "direct";
const promotionSettings = {
autoMerge: settings.autoMerge,
globalPause: settings.globalPause,
enginePaused: settings.enginePaused,
mergeStrategy: settings.mergeStrategy,
integrationBranch: settings.integrationBranch,
baseBranch: settings.baseBranch,
};
const attemptBranchGroupPromotion = async (taskForPromotion: Task | null): Promise<void> => {
if (!taskForPromotion || !isSharedBranchGroupMemberIntegration(taskForPromotion)) {
return;
}
try {
await promoteBranchGroup({
store,
rootDir: cwd,
groupId: taskForPromotion.branchContext!.groupId,
settings: promotionSettings,
recordAudit: async (event) => {
await store.recordRunAuditEvent({
domain: event.domain as any,
mutationType: event.mutationType,
target: event.target,
metadata: event.metadata,
} as any);
},
});
} catch (promotionError) {
runtimeLog.warn(
`Branch-group promotion evaluation failed for ${taskId}: ${promotionError instanceof Error ? promotionError.message : String(promotionError)}`,
);
}
};
if (mergeStrategy === "pull-request" && this.options.processPullRequestMerge) {
this.activeMergeTaskId = taskId;
@@ -1807,6 +1841,7 @@ export class ProjectEngine {
mergeTargetBranch: mergedTask.mergeDetails?.mergeTargetBranch,
} as MergeResult);
}
await attemptBranchGroupPromotion(mergedTask);
} else if (result === "waiting") {
runtimeLog.log(`${manualResolver ? "Manual" : "Auto"}-merge PR waiting: ${taskId}`);
}
@@ -1882,6 +1917,8 @@ export class ProjectEngine {
if (latestTask?.mergeRetries && latestTask.mergeRetries > 0) {
await store.updateTask(taskId, { mergeRetries: 0 });
}
await attemptBranchGroupPromotion(latestTask);
}
} catch (err: unknown) {
this.activeMergeSession = null;