FN-5782: wire branch groups into merge routing

Connect branch_group metadata through planning and merge execution for grouped integration branches.

- propagate `branchGroup` and `branchGroupName` through core store types, merge metadata, and CLI task lifecycle APIs
- add merge coordination that computes branch-group plans and routes grouped tasks via `group-merge-coordinator`
- extend finalize-plan, merger, and reliability tests to cover grouped merge routing and integration behavior
- document the architecture update and add a patch changeset for `@runfusion/fusion`

Files changed:
 .changeset/fn-5782-branch-group-merge.md           |   7 +
 docs/architecture.md                               |   2 +
 packages/cli/src/commands/__tests__/task-lifecycle.test.ts  |  64 ++++++++-
 packages/cli/src/commands/task-lifecycle.ts        |   4 +
 packages/core/src/__tests__/branch-group-store.test.ts  |  20 +++
 packages/core/src/__tests__/task-merge.test.ts     |  64 +++++++++
 packages/core/src/store.ts                         |  22 ++-
 packages/core/src/task-merge.ts                    |  26 +++-
 packages/core/src/types.ts                         |   2 +-
 packages/engine/src/__tests__/experiment-finalize-plan.test.ts |  33 ++++-
 packages/engine/src/__tests__/group-merge-coordinator.test.ts  |  62 +++++++++
 packages/engine/src/__tests__/reliability-interactions/branch-group-merge-routing.test.ts | 153 +++++++++++++++++++++
 packages/engine/src/experiment/finalize-plan.ts    |  62 +++++++--
 packages/engine/src/group-merge-coordinator.ts     |  51 +++++++
 packages/engine/src/index.ts                       |   4 +
 packages/engine/src/merger.ts                      |  47 ++++++-
 16 files changed, 601 insertions(+), 22 deletions(-)

Fusion-Task-Id: FN-5782
Fusion-Task-Lineage: 0a70cfaa-2379-4955-b295-64de1f3093c8
This commit is contained in:
gsxdsm
2026-05-31 12:45:35 -07:00
parent f1aba5b534
commit 0c425788cb
16 changed files with 601 additions and 22 deletions

View File

@@ -115,6 +115,26 @@ describe("TaskStore branch groups", () => {
expect(slim.find((entry) => entry.id === task.id)).toBeUndefined();
});
it("lists tasks by branch group and records landed member metadata", async () => {
const group = store.createBranchGroup({ sourceType: "planning", sourceId: "PS-9", branchName: "fn/grouped" });
const taskA = await store.createTask({ description: "group-a" });
const taskB = await store.createTask({ description: "group-b" });
const taskC = await store.createTask({ description: "group-c" });
await store.setTaskBranchGroup(taskA.id, group.id);
await store.setTaskBranchGroup(taskC.id, group.id);
const groupedTasks = await store.listTasksByBranchGroup(group.id);
expect(groupedTasks.map((task) => task.id)).toEqual([taskA.id, taskC.id]);
expect(groupedTasks.find((task) => task.id === taskB.id)).toBeUndefined();
const landed = store.recordBranchGroupMemberLanded(group.id, {
worktreePath: "/tmp/fusion/grouped",
status: "open",
});
expect(landed.worktreePath).toBe("/tmp/fusion/grouped");
expect(landed.status).toBe("open");
});
it("preserves autoMerge + branchContext in slim list/search/modifiedSince and archived slim", async () => {
const task = await store.createTask({ description: "slim check" });
const group = store.createBranchGroup({ sourceType: "mission", sourceId: "M-2", branchName: "fn/mission" });

View File

@@ -52,6 +52,25 @@ describe("resolveTaskMergeTarget", () => {
});
});
it("routes shared branch-group members to branch group integration branch", () => {
expect(resolveTaskMergeTarget({
baseBranch: undefined,
branchContext: {
groupId: "G-1",
source: "planning",
assignmentMode: "shared",
inheritedBaseBranch: "develop",
},
}, {
branchGroup: {
branchName: "fusion/groups/planning-g-1",
},
})).toEqual({
branch: "fusion/groups/planning-g-1",
source: "branch-group-integration",
});
});
it("falls back to inherited branch context", () => {
expect(resolveTaskMergeTarget({
baseBranch: undefined,
@@ -82,6 +101,25 @@ describe("resolveTaskMergeTarget", () => {
});
});
it("keeps per-task-derived grouped members on inherited branch context", () => {
expect(resolveTaskMergeTarget({
baseBranch: undefined,
branchContext: {
groupId: "G-2",
source: "planning",
assignmentMode: "per-task-derived",
inheritedBaseBranch: "develop",
},
}, {
branchGroup: {
branchName: "fusion/groups/planning-g-2",
},
})).toEqual({
branch: "develop",
source: "task-branch-context",
});
});
it("uses project default branch when task has no explicit target", () => {
expect(resolveTaskMergeTarget(
{ baseBranch: undefined, branchContext: undefined },
@@ -117,6 +155,32 @@ describe("resolveTaskMergeTarget", () => {
});
});
it("rejects branch-group integration branch when it points at a sibling fusion/fn-* branch", () => {
const result = resolveTaskMergeTarget(
{
baseBranch: undefined,
branchContext: {
groupId: "G-1",
source: "planning",
assignmentMode: "shared",
},
},
{
branchGroup: {
branchName: "fusion/fn-1234",
},
projectDefaultBranch: "main",
},
);
expect(result.branch).toBe("main");
expect(result.source).toBe("project-default");
expect(result.rejected).toEqual({
branch: "fusion/fn-1234",
source: "branch-group-integration",
reason: "fusion-sibling-branch",
});
});
it("rejects inherited branch context that points at a sibling fusion/fn-* branch", () => {
const result = resolveTaskMergeTarget(
{