feat(FN-000): create workflow merge work on handoff
Fusion-Task-Id: FN-000
This commit is contained in:
@@ -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`.
|
||||
@@ -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",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user