feat(FN-000): create workflow merge work on handoff

Fusion-Task-Id: FN-000
This commit is contained in:
gsxdsm
2026-06-09 17:17:34 -07:00
parent 283c64dfee
commit 296f191a83
3 changed files with 138 additions and 0 deletions

View File

@@ -0,0 +1,46 @@
---
title: "S07: completion handoff creates merge work"
type: refactor
status: draft-stack-handoff
date: 2026-06-09
slice: S07
milestone: "Runtime"
origin: docs/plans/2026-06-09-003-refactor-workflow-owned-merge-full-migration-slices-plan.md
stack_base: feature/workflow-owned-merge-s06-git-merge-capabilities
---
# S07: completion handoff creates merge work
## Stack Role
This draft PR reserves the S07 review slot in the workflow-owned merge,
retry, scheduling, and recovery migration stack. It is intentionally a handoff
artifact, not the completed implementation for this slice.
## Milestone
Runtime
## Depends On
S2 projection, S5 runtime driver, and S6 merge capabilities.
## Goal
Replace task-moved in-review auto-enqueue as policy authority with workflow completion handoff creating merge work.
## Expected File Scope
packages/engine/src/project-engine.ts; packages/engine/src/merger.ts; packages/core/src/store.ts; completion and cutover tests.
## Expected Tests
Coding completion creates merge work, autoMerge false creates manual hold, duplicate handoff idempotency, soft-delete cancellation, startup projection dedupe.
## Exit Gate
New task completions produce workflow merge work before old queue processing runs.
## Full Plan
See `docs/plans/2026-06-09-003-refactor-workflow-owned-merge-full-migration-slices-plan.md`.

View File

@@ -222,4 +222,54 @@ describe("TaskStore merge request record + completion handoff marker", () => {
lastError: "cancelled-by-user-hard-cancel",
});
});
it("creates idempotent workflow merge work during completion handoff", async () => {
const taskId = await createTask();
await store.moveTask(taskId, "todo");
await store.moveTask(taskId, "in-progress");
await store.handoffToReview(taskId, {
ownerAgentId: "agent-test",
evidence: { reason: "fn_task_done", runId: "run-handoff", agentId: "agent-test" },
now: "2026-05-30T00:00:00.000Z",
});
await store.handoffToReview(taskId, {
ownerAgentId: "agent-test",
evidence: { reason: "fn_task_done", runId: "run-handoff", agentId: "agent-test" },
now: "2026-05-30T00:00:01.000Z",
});
expect(store.listWorkflowWorkItemsForTask(taskId, { kinds: ["merge"] })).toEqual([
expect.objectContaining({
runId: "run-handoff",
taskId,
nodeId: "merge-gate",
kind: "merge",
state: "runnable",
}),
]);
});
it("creates manual hold workflow work instead of merge work when autoMerge is false", async () => {
const taskId = await createTask();
await store.updateTask(taskId, { autoMerge: false });
await store.moveTask(taskId, "todo");
await store.moveTask(taskId, "in-progress");
await store.handoffToReview(taskId, {
ownerAgentId: "agent-test",
evidence: { reason: "fn_task_done", runId: "run-manual", agentId: "agent-test" },
});
expect(store.listWorkflowWorkItemsForTask(taskId)).toEqual([
expect.objectContaining({
runId: "run-manual",
taskId,
nodeId: "merge-manual-hold",
kind: "manual-hold",
state: "manual-required",
blockedReason: "autoMerge:false",
}),
]);
});
});

View File

@@ -6625,6 +6625,11 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS}
},
});
this.enqueueMergeQueue(id, { priority: task.priority, now: internal.now });
this.createCompletionHandoffWorkflowWork(task, {
runId: internal.runContext?.runId,
now: internal.now,
source: internal.evidence?.reason,
});
this.insertRunAuditEventRow({
taskId: id,
agentId: internal.runContext?.agentId,
@@ -7092,6 +7097,11 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS}
if (internal.fromHandoff) {
alreadyEnqueued = Boolean(this.db.prepare("SELECT 1 FROM mergeQueue WHERE taskId = ?").get(id));
this.enqueueMergeQueue(id, { priority: task.priority, now: internal.now });
this.createCompletionHandoffWorkflowWork(task, {
runId: internal.runContext?.runId,
now: internal.now,
source: internal.evidence?.reason,
});
this.insertRunAuditEventRow({
taskId: id,
agentId: internal.runContext?.agentId,
@@ -9007,6 +9017,38 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS}
});
}
createCompletionHandoffWorkflowWork(
task: Pick<Task, "id" | "autoMerge" | "priority">,
opts: { runId?: string; now?: string; source?: string } = {},
): WorkflowWorkItem {
const autoMerge = task.autoMerge !== false;
const item = this.upsertWorkflowWorkItem({
runId: opts.runId ?? `completion-handoff:${task.id}`,
taskId: task.id,
nodeId: autoMerge ? "merge-gate" : "merge-manual-hold",
kind: autoMerge ? "merge" : "manual-hold",
state: autoMerge ? "runnable" : "manual-required",
blockedReason: autoMerge ? null : "autoMerge:false",
now: opts.now,
});
this.insertRunAuditEventRow({
taskId: task.id,
runId: item.runId,
domain: "database",
mutationType: "workflowWorkItem:completion-handoff",
target: item.id,
metadata: {
taskId: task.id,
autoMerge,
source: opts.source ?? "completion-handoff",
workItemId: item.id,
nodeId: item.nodeId,
state: item.state,
},
});
return item;
}
upsertWorkflowWorkItem(input: WorkflowWorkItemUpsertInput): WorkflowWorkItem {
return this.db.transactionImmediate(() => {
const existing = this.db