## What & why Workflow graph merge failures classified `implementation-incomplete` (i.e. the merge node reports there is no implementation proof — no branch / no committed work) could still be routed to the no-op merge requester and false-complete the task as **done**. This hides genuinely unlanded work behind a green "merge" and is the merge-side sibling of the "(no feedback captured)" no-verdict dispatch defect. Closes the truthfulness gap: an `implementation-incomplete` merge-graph failure now **fails closed** when there is no executable proof to resume, or **requeues resumable parsed steps** back to `todo` for execution — it is never handed to a no-branch no-op merge requester. Refs #1991 (no-op merge truthfulness). Sibling of #1946 (no-verdict "(no feedback captured)" dispatch defect). ## Change - New classifier `routeImplementationIncompleteMergeGraphFailure(live, failedNode)`: - clears paused-aborted state + active worktree, - requeues resumable parsed steps via the existing execution-resume router when the task still has non-terminal workflow steps, - otherwise fails closed (`status: "failed"` with a logged, explicit reason). - Defense-in-depth: `isRetryableBenignMergePauseAbort` and the merge-requester route both short-circuit (`return false`) for `implementation-incomplete`, so this value can never reach the no-op merge requester. - `handleGraphFailure` routes genuine (non-global-pause, non-completion-finalize, non-user-paused) `implementation-incomplete` merge-graph failures through the new classifier. - Resume-eligibility predicate treats an `implementation-incomplete` merge failure with **no** incomplete steps as fail-closed, and keeps the premature-merge-with-incomplete-steps requeue path. Legitimate `noCommitsExpected` no-op merges are explicitly preserved (regression test included). ## Tests New regression coverage in: - `packages/engine/src/__tests__/reliability-interactions/merge-node-paused-abort-retryable.test.ts` — parametrized across merge node ids: (a) no-proof `implementation-incomplete` fails closed without requesting a no-op merge; (b) resumable parsed steps are requeued to `todo` for execution resume, not no-op-merged. - `packages/engine/src/__tests__/executor-fast-mode-workflows.test.ts` — a legitimate `noCommitsExpected` builtin:coding merge is still allowed (guard does not over-block). Verification (engine package): pnpm --filter @fusion/engine exec vitest run \ src/__tests__/executor-fast-mode-workflows.test.ts \ src/__tests__/reliability-interactions/merge-node-paused-abort-retryable.test.ts # => 2 files, 69 tests, 0 failures pnpm check:changesets # pass pnpm --filter @fusion/engine typecheck # 0 errors A `patch` changeset for `@runfusion/fusion` is included. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Prevented “implementation-incomplete” workflow merge failures from being treated as successful no-op merges. * Ensured tasks with resumable implementation steps move back to execution to continue where they left off. * Ensured tasks without sufficient implementation evidence fail safely rather than entering misleading retry/no-op paths. * Improved paused/aborted merge-failure handling to avoid incorrect completion states. * **Tests** * Added/expanded coverage for fast-mode coding merges and implementation-incomplete pause/abort retry classification. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Fusion <noreply@runfusion.ai> Co-authored-by: gsxdsm <gsxdsm@users.noreply.github.com>
This commit is contained in:
7
.changeset/fn-1165-noop-merge-truthfulness.md
Normal file
7
.changeset/fn-1165-noop-merge-truthfulness.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Prevent implementation-incomplete workflow merge failures from false-completing as no-op done.
|
||||
category: fix
|
||||
dev: Merge graph failures with missing implementation proof now fail closed or requeue resumable parsed steps before any no-branch no-op merge requester path can run.
|
||||
@@ -378,6 +378,65 @@ describe("fast mode workflow/runtime invariants", () => {
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
it("allows noCommitsExpected builtin:coding merge even when parsed implementation steps are empty", async () => {
|
||||
const liveTask = task({
|
||||
id: "FN-1165-NOOP",
|
||||
executionMode: "fast",
|
||||
enabledWorkflowSteps: [],
|
||||
column: "in-progress",
|
||||
steps: [],
|
||||
noCommitsExpected: true,
|
||||
branch: null,
|
||||
worktree: null,
|
||||
prompt: "# Task\n\n## Steps\n\n### Step 1: Decide\n- [ ] Record no-code decision",
|
||||
});
|
||||
const inReviewTask = { ...liveTask, column: "in-review" } as typeof liveTask;
|
||||
const doneTask = {
|
||||
...liveTask,
|
||||
column: "done",
|
||||
mergeDetails: {
|
||||
mergeConfirmed: true,
|
||||
noOpMerge: true,
|
||||
noOpReason: "no-commits-expected",
|
||||
},
|
||||
} as typeof liveTask;
|
||||
const store = createMockStore();
|
||||
store.getTask.mockResolvedValue(liveTask);
|
||||
store.getTaskWorkflowSelection = vi.fn(() => ({ workflowId: "builtin:coding", stepIds: [] }));
|
||||
store.getWorkflowDefinition = vi.fn(async (id: string) => getBuiltinWorkflow(id));
|
||||
store.moveTask
|
||||
.mockResolvedValueOnce(inReviewTask)
|
||||
.mockResolvedValueOnce(doneTask);
|
||||
const executor = new TaskExecutor(store, "/tmp/test") as any;
|
||||
const mergeRequester = vi.fn(async () => ({
|
||||
task: inReviewTask,
|
||||
merged: true,
|
||||
noOp: true,
|
||||
mergeConfirmed: true,
|
||||
reason: "no-commits-expected",
|
||||
}));
|
||||
executor.setMergeRequester(mergeRequester);
|
||||
|
||||
const result = await executor.createAuthoritativeWorkflowPrimitives({ autoMerge: true }).requestMerge(
|
||||
{
|
||||
run: { runId: "FN-1165-NOOP:builtin:coding", taskId: "FN-1165-NOOP", workflowId: "builtin-stepwise-final-review-coding" },
|
||||
node: { node: { id: "merge" } },
|
||||
},
|
||||
liveTask,
|
||||
);
|
||||
|
||||
expect(result).toMatchObject({ outcome: "success", value: "merge-noop" });
|
||||
expect(mergeRequester).toHaveBeenCalledWith("FN-1165-NOOP", expect.objectContaining({ signal: expect.any(AbortSignal) }));
|
||||
expect(store.logEntry).not.toHaveBeenCalledWith(
|
||||
"FN-1165-NOOP",
|
||||
expect.stringContaining("implementation did not run"),
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-1165-NOOP", "done", expect.objectContaining({ preserveProgress: true }));
|
||||
});
|
||||
|
||||
it("fast builtin:coding executes plain Steps-section headings from fast triage specs", async () => {
|
||||
const calls: string[] = [];
|
||||
const prompt = `# Task
|
||||
|
||||
@@ -84,9 +84,10 @@ describe("merge-node paused-abort retry classification (FN-6735)", () => {
|
||||
Surface Enumeration coverage:
|
||||
- Merge seam node ids: legacy `merge`, `requestMerge`, primitive merge-region ids, and historical aliases all route through the same classifier.
|
||||
- Auto-merge paths: autopilot autoMerge:true and shared-branch local integration are both exercised.
|
||||
- Pause sources: benign hard-cancel/undefined-like generic pause is retried; global/user pause controls remain terminal.
|
||||
- Pause sources: benign hard-cancel/undefined-like generic pause is retried; global/user pause controls remain terminal; system pause (`paused` without userPaused/global-pause) still classifies implementation-incomplete fail-closed/resumable.
|
||||
- Retry/data states: retry budget, mergeConfirmed partial landing, conflict, foreign/contamination, and pre-existing failure all avoid retry.
|
||||
- FN-5147/FN-7749: autoMerge:false human-gated in-review tasks preserve the manual merge hold cleanly without failed parking or requeueing.
|
||||
- Worktree tracking: resumable implementation-incomplete requeue keeps activeWorktrees registration when a worktree is preserved; fail-closed releases it.
|
||||
*/
|
||||
it.each([
|
||||
"merge",
|
||||
@@ -275,4 +276,203 @@ describe("merge-node paused-abort retry classification (FN-6735)", () => {
|
||||
expect.objectContaining({ status: null, error: null, paused: false }),
|
||||
);
|
||||
});
|
||||
|
||||
const implementationIncompleteMergeNodes = [
|
||||
"merge",
|
||||
"requestMerge",
|
||||
"merge-gate",
|
||||
"merge-attempt",
|
||||
"manual-merge-hold",
|
||||
"merge-manual-hold",
|
||||
"retry-backoff",
|
||||
"merge-retry",
|
||||
] as const;
|
||||
|
||||
it.each(implementationIncompleteMergeNodes)("fails implementation-incomplete no-proof merge pause abort at node %s without requesting no-op merge", async (nodeId) => {
|
||||
const { store, task, executor, mergeRequester } = makeHarness({
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
branch: null,
|
||||
worktree: null,
|
||||
modifiedFiles: undefined,
|
||||
workflowStepResults: undefined,
|
||||
paused: false,
|
||||
} as Partial<TaskDetail>);
|
||||
mergeRequester.mockImplementation(async () => {
|
||||
await store.updateTask(task.id, {
|
||||
mergeDetails: {
|
||||
mergeConfirmed: true,
|
||||
noOpMerge: true,
|
||||
noOpReason: "no-branch",
|
||||
},
|
||||
});
|
||||
return {
|
||||
task,
|
||||
branch: null,
|
||||
merged: true,
|
||||
noOp: true,
|
||||
mergeConfirmed: true,
|
||||
reason: "no-branch",
|
||||
worktreeRemoved: false,
|
||||
branchDeleted: false,
|
||||
} as any;
|
||||
});
|
||||
|
||||
await invokeGraphFailure(executor, task, nodeId, "implementation-incomplete");
|
||||
|
||||
expect(mergeRequester).not.toHaveBeenCalled();
|
||||
expect(store.moveTask).not.toHaveBeenCalledWith(task.id, "done", expect.anything());
|
||||
expect(store.moveTask).not.toHaveBeenCalledWith(task.id, "todo", expect.anything());
|
||||
expect(store.updateTask).not.toHaveBeenCalledWith(
|
||||
task.id,
|
||||
expect.objectContaining({
|
||||
mergeDetails: expect.objectContaining({ noOpMerge: true, noOpReason: "no-branch" }),
|
||||
}),
|
||||
expect.anything(),
|
||||
);
|
||||
expect(store.updateTask).toHaveBeenCalledWith(
|
||||
task.id,
|
||||
expect.objectContaining({
|
||||
status: "failed",
|
||||
error: expect.stringContaining("implementation incomplete with no executable proof to resume"),
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
const messages = logText(store);
|
||||
expect(messages).toContain(`Workflow graph merge blocked at node '${nodeId}': implementation incomplete with no executable proof to resume — failing instead of retrying merge`);
|
||||
expect(messages).not.toContain("routed to bounded auto-merge retry after benign pause/resume abort");
|
||||
});
|
||||
|
||||
it.each(implementationIncompleteMergeNodes)("requeues resumable implementation-incomplete parsed steps at node %s without requesting merge", async (nodeId) => {
|
||||
const { store, task, executor, mergeRequester } = makeHarness({
|
||||
steps: [
|
||||
{ name: "Preflight", status: "done" },
|
||||
{ name: "Implement", status: "pending" },
|
||||
],
|
||||
currentStep: 1,
|
||||
branch: null,
|
||||
worktree: null,
|
||||
modifiedFiles: undefined,
|
||||
workflowStepResults: undefined,
|
||||
paused: false,
|
||||
} as Partial<TaskDetail>);
|
||||
|
||||
await invokeGraphFailure(executor, task, nodeId, "implementation-incomplete");
|
||||
|
||||
expect(mergeRequester).not.toHaveBeenCalled();
|
||||
expect(store.updateTask).toHaveBeenCalledWith(task.id, { status: null, error: null }, undefined);
|
||||
expect(store.moveTask).toHaveBeenCalledWith(task.id, "todo", expect.objectContaining({
|
||||
preserveProgress: true,
|
||||
moveSource: "engine",
|
||||
recoveryRehome: true,
|
||||
}));
|
||||
expect(store.moveTask).not.toHaveBeenCalledWith(task.id, "done", expect.anything());
|
||||
const messages = logText(store);
|
||||
expect(messages).toContain(`Workflow graph failed at node '${nodeId}' (implementation-incomplete) with incomplete steps — moved back to todo for execution resume`);
|
||||
expect(messages).not.toContain("routed to bounded auto-merge retry after benign pause/resume abort");
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:WorkflowMerge 2026-07-14-18:20:
|
||||
Greptile P1 regressions for FN-1165: system-paused rows must still classify, and resumable requeue must not drop active worktree tracking while preserving a persisted worktree.
|
||||
*/
|
||||
it("classifies system-paused implementation-incomplete merge failures fail-closed instead of pause-abort parking", async () => {
|
||||
const { store, task, executor, mergeRequester } = makeHarness({
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
branch: null,
|
||||
worktree: null,
|
||||
modifiedFiles: undefined,
|
||||
workflowStepResults: undefined,
|
||||
// System pause park (not userPaused / not global-pause provenance).
|
||||
paused: true,
|
||||
userPaused: false,
|
||||
pausedReason: "awaiting-engine-recovery",
|
||||
} as Partial<TaskDetail>);
|
||||
(executor as any).addActiveWorktree(task.id, "/tmp/fusion-fn-1165-fail-closed");
|
||||
|
||||
await invokeGraphFailure(executor, task, "merge", "implementation-incomplete");
|
||||
|
||||
expect(mergeRequester).not.toHaveBeenCalled();
|
||||
expect(store.moveTask).not.toHaveBeenCalledWith(task.id, "todo", expect.anything());
|
||||
expect(store.updateTask).toHaveBeenCalledWith(
|
||||
task.id,
|
||||
expect.objectContaining({
|
||||
status: "failed",
|
||||
error: expect.stringContaining("implementation incomplete with no executable proof to resume"),
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
const messages = logText(store);
|
||||
expect(messages).toContain("Workflow graph merge blocked at node 'merge': implementation incomplete with no executable proof to resume — failing instead of retrying merge");
|
||||
expect(messages).not.toContain("operator action required");
|
||||
expect(messages).not.toContain("benign, paused awaiting explicit unpause");
|
||||
// Fail-closed may release tracking — no second worktree will be allocated for a terminal row.
|
||||
expect((executor as any).activeWorktrees.has(task.id)).toBe(false);
|
||||
});
|
||||
|
||||
it("requeues system-paused implementation-incomplete incomplete steps and keeps active worktree tracking", async () => {
|
||||
const worktreePath = "/tmp/fusion-fn-1165-resumable-wt";
|
||||
const { store, task, executor, mergeRequester } = makeHarness({
|
||||
steps: [
|
||||
{ name: "Preflight", status: "done" },
|
||||
{ name: "Implement", status: "pending" },
|
||||
],
|
||||
currentStep: 1,
|
||||
branch: "fusion/fn-1165-resumable",
|
||||
worktree: worktreePath,
|
||||
modifiedFiles: undefined,
|
||||
workflowStepResults: undefined,
|
||||
paused: true,
|
||||
userPaused: false,
|
||||
pausedReason: "system-pause-park",
|
||||
} as Partial<TaskDetail>);
|
||||
(executor as any).addActiveWorktree(task.id, worktreePath);
|
||||
|
||||
await invokeGraphFailure(executor, task, "merge", "implementation-incomplete");
|
||||
|
||||
expect(mergeRequester).not.toHaveBeenCalled();
|
||||
// System pause park must be cleared so the requeued todo row is dispatchable.
|
||||
expect(store.updateTask).toHaveBeenCalledWith(
|
||||
task.id,
|
||||
expect.objectContaining({ paused: false, pausedReason: null }),
|
||||
undefined,
|
||||
);
|
||||
expect(store.moveTask).toHaveBeenCalledWith(task.id, "todo", expect.objectContaining({
|
||||
preserveProgress: true,
|
||||
moveSource: "engine",
|
||||
recoveryRehome: true,
|
||||
}));
|
||||
const messages = logText(store);
|
||||
expect(messages).toContain("Workflow graph failed at node 'merge' (implementation-incomplete) with incomplete steps — moved back to todo for execution resume");
|
||||
expect(messages).not.toContain("operator action required");
|
||||
// Resumable path keeps active registration so the preserved worktree stays counted.
|
||||
expect((executor as any).activeWorktrees.has(task.id)).toBe(true);
|
||||
expect((executor as any).getActiveWorktreePaths(task.id)).toEqual([worktreePath]);
|
||||
});
|
||||
|
||||
it("keeps active worktree tracking on non-paused resumable implementation-incomplete requeue", async () => {
|
||||
const worktreePath = "/tmp/fusion-fn-1165-unpaused-resumable-wt";
|
||||
const { store, task, executor, mergeRequester } = makeHarness({
|
||||
steps: [
|
||||
{ name: "Preflight", status: "done" },
|
||||
{ name: "Implement", status: "pending" },
|
||||
],
|
||||
currentStep: 1,
|
||||
branch: "fusion/fn-1165-unpaused",
|
||||
worktree: worktreePath,
|
||||
modifiedFiles: undefined,
|
||||
workflowStepResults: undefined,
|
||||
paused: false,
|
||||
} as Partial<TaskDetail>);
|
||||
(executor as any).addActiveWorktree(task.id, worktreePath);
|
||||
|
||||
await invokeGraphFailure(executor, task, "merge-gate", "implementation-incomplete");
|
||||
|
||||
expect(mergeRequester).not.toHaveBeenCalled();
|
||||
expect(store.moveTask).toHaveBeenCalledWith(task.id, "todo", expect.objectContaining({ preserveProgress: true }));
|
||||
expect((executor as any).activeWorktrees.has(task.id)).toBe(true);
|
||||
expect((executor as any).getActiveWorktreePaths(task.id)).toEqual([worktreePath]);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -8434,7 +8434,10 @@ export class TaskExecutor {
|
||||
if (abortProvenance === "completion-finalize") return false;
|
||||
if (live.column !== "in-review" || !this.isRetryableMergePauseAbortStatus(live.status) || live.error != null) return false;
|
||||
if (live.mergeDetails?.mergeConfirmed === true) return false;
|
||||
if (this.isTerminalMergeGraphFailureValue(this.graphFailureValue(result))) return false;
|
||||
const failureValue = this.graphFailureValue(result);
|
||||
if (this.isTerminalMergeGraphFailureValue(failureValue)) return false;
|
||||
/* FNXC:WorkflowMerge 2026-07-12-17:38: FN-1165 / Runfusion#1991 — missing implementation proof is not a transient merge pause. Let the implementation-incomplete classifier fail closed or requeue resumable parsed steps before any requester can mint a no-branch no-op merge proof. */
|
||||
if (failureValue === "implementation-incomplete") return false;
|
||||
const failedNode = result.visitedNodeIds[result.visitedNodeIds.length - 1];
|
||||
if (!this.isMergeGraphFailure(failedNode)) return false;
|
||||
let settings: Settings | undefined;
|
||||
@@ -8826,6 +8829,8 @@ export class TaskExecutor {
|
||||
abortProvenance: "global-pause" | "merge-seam" | "hard-cancel" | "completion-finalize" | undefined,
|
||||
): Promise<boolean> {
|
||||
if (!this.mergeRequester) return false;
|
||||
/* FNXC:WorkflowMerge 2026-07-12-17:38: FN-1165 defense in depth — implementation-incomplete merge graph failures must never reach the merge requester, because a no-branch task can otherwise be finalized as an intentional no-op. */
|
||||
if (this.graphFailureValue(result) === "implementation-incomplete") return false;
|
||||
const failedNode = result.visitedNodeIds[result.visitedNodeIds.length - 1] ?? "unknown";
|
||||
const message = `Workflow graph merge failure at node '${failedNode}' routed to bounded auto-merge retry${abortProvenance === "merge-seam" ? " after merge-seam abort" : abortProvenance === "hard-cancel" || abortProvenance === undefined ? " after benign pause/resume abort" : ""}`;
|
||||
executorLog.warn(`${live.id}: ${message}`);
|
||||
@@ -8845,6 +8850,40 @@ export class TaskExecutor {
|
||||
return true;
|
||||
}
|
||||
|
||||
private async routeImplementationIncompleteMergeGraphFailure(live: TaskDetail, failedNode: string): Promise<boolean> {
|
||||
/*
|
||||
FNXC:WorkflowMerge 2026-07-14-18:20:
|
||||
FN-1165 greptile P1s: (1) system-paused implementation-incomplete merge failures must still classify —
|
||||
clear only non-user pause parks so incomplete steps can requeue; real global/user pauses never enter this method.
|
||||
(2) Do not drop activeWorktrees until we know the outcome is terminal fail-closed. Resumable requeue preserves
|
||||
progress (and often the persisted worktree); releasing tracking early leaves that worktree uncounted while a later
|
||||
dispatch can allocate a second one. Keep the active registration on the resumable path; release only on fail-closed.
|
||||
*/
|
||||
this.clearPausedAborted(live.id);
|
||||
let resumeLive = live;
|
||||
if (live.paused === true && live.userPaused !== true) {
|
||||
// FNXC:WorkflowMerge 2026-07-14-18:35: TaskDetail.pausedReason is string|undefined (not null). Persist clear via updateTask (store accepts null); in-memory resume snapshot uses undefined to satisfy the type.
|
||||
await this.store.updateTask(live.id, {
|
||||
paused: false,
|
||||
pausedReason: null,
|
||||
}, this.getRunContextFor(live.id));
|
||||
resumeLive = { ...live, paused: false, pausedReason: undefined };
|
||||
}
|
||||
if (hasNonTerminalWorkflowSteps(resumeLive) && await this.routeGraphFailureToExecutionResume(resumeLive, failedNode, "implementation-incomplete")) {
|
||||
return true;
|
||||
}
|
||||
// Fail-closed terminal path — release active worktree tracking now that no resume will reuse it.
|
||||
this.activeWorktrees.delete(live.id);
|
||||
const message = `Workflow graph merge blocked at node '${failedNode}': implementation incomplete with no executable proof to resume — failing instead of retrying merge`;
|
||||
executorLog.warn(`${live.id}: ${message}`);
|
||||
await this.store.logEntry(live.id, message, undefined, this.getRunContextFor(live.id));
|
||||
if (live.column !== "done" && live.column !== "archived" && live.error == null) {
|
||||
await this.store.updateTask(live.id, { error: message, status: "failed" }, this.getRunContextFor(live.id));
|
||||
}
|
||||
await this.persistTokenUsage(live.id);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Terminal failure of a graph run: record the error and park the task in
|
||||
* review so a human can act — never leave it invisible in in-progress. */
|
||||
private async handleGraphFailure(task: Task, result: WorkflowGraphTaskRunResult): Promise<void> {
|
||||
@@ -8930,9 +8969,9 @@ export class TaskExecutor {
|
||||
|| (live.paused && !mergeSeamAborted && !suppressFinalizedCompletionAbort)
|
||||
|| (pausedAborted && !mergeSeamAborted && !completionFinalizeAborted && !suppressFinalizedCompletionAbort),
|
||||
);
|
||||
const failedNodeForLog = result.visitedNodeIds[result.visitedNodeIds.length - 1] ?? "unknown";
|
||||
const failureValueForLog = this.graphFailureValue(result) ?? "none";
|
||||
if (pausedAborted || live.paused || live.userPaused || abortProvenance) {
|
||||
const failedNodeForLog = result.visitedNodeIds[result.visitedNodeIds.length - 1] ?? "unknown";
|
||||
const failureValueForLog = this.graphFailureValue(result) ?? "none";
|
||||
this.safeLogEntry(
|
||||
task.id,
|
||||
`Pause abort classified: provenance=${abortProvenance ?? "unknown"}; node=${failedNodeForLog}; interrupted=${result.interruptedNodeId ?? "none"}; abortKind=${result.interruptedAbortKind ?? "none"}; column=${live.column}; status=${live.status ?? "none"}; paused=${live.paused === true}; userPaused=${live.userPaused === true}; value=${failureValueForLog}; genuine=${genuinePauseAbort}; mergeSeam=${mergeSeamAborted}; completionSuppressed=${suppressFinalizedCompletionAbort}`,
|
||||
@@ -8943,6 +8982,24 @@ export class TaskExecutor {
|
||||
return;
|
||||
}
|
||||
}
|
||||
/*
|
||||
FNXC:WorkflowMerge 2026-07-14-18:20:
|
||||
FN-1165 greptile P1: system pause (`live.paused` without userPaused/global-pause) must still enter the
|
||||
implementation-incomplete merge classifier. Requiring `live.paused !== true` let pause-abort parking win and
|
||||
skipped fail-closed/resumable routing for missing implementation proof. User pause and global-pause stay excluded.
|
||||
*/
|
||||
if (
|
||||
genuinePauseAbort
|
||||
&& abortProvenance !== "global-pause"
|
||||
&& abortProvenance !== "completion-finalize"
|
||||
&& live.userPaused !== true
|
||||
&& this.isMergeGraphFailure(failedNodeForLog)
|
||||
&& failureValueForLog === "implementation-incomplete"
|
||||
) {
|
||||
if (await this.routeImplementationIncompleteMergeGraphFailure(live, failedNodeForLog)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (genuinePauseAbort && await this.isRetryableBenignMergePauseAbort(live, result, abortProvenance, pausedAborted)) {
|
||||
if (await this.routeGraphMergeFailureToRetry(live, result, abortProvenance)) {
|
||||
return;
|
||||
@@ -9279,8 +9336,10 @@ export class TaskExecutor {
|
||||
await this.persistTokenUsage(task.id);
|
||||
return;
|
||||
}
|
||||
if (mergeGraphFailure && failureValue === "implementation-incomplete" && await this.routeGraphFailureToExecutionResume(live, failedNode ?? "unknown", failureValue)) {
|
||||
return;
|
||||
if (mergeGraphFailure && failureValue === "implementation-incomplete") {
|
||||
if (await this.routeImplementationIncompleteMergeGraphFailure(live, failedNode ?? "unknown")) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (mergeGraphFailure && !this.isTerminalMergeGraphFailureValue(failureValue) && await this.routeGraphMergeFailureToRetry(live, result, abortProvenance)) {
|
||||
return;
|
||||
@@ -9405,7 +9464,9 @@ export class TaskExecutor {
|
||||
*/
|
||||
if (failedNode === COMPLETION_SUMMARY_NODE_ID) return false;
|
||||
const incompleteSteps = hasNonTerminalWorkflowSteps(live);
|
||||
const prematureMergeWithIncompleteSteps = failedNode === "merge" && failureValue === "implementation-incomplete" && incompleteSteps;
|
||||
const implementationIncompleteMergeFailure = this.isMergeGraphFailure(failedNode) && failureValue === "implementation-incomplete";
|
||||
if (implementationIncompleteMergeFailure && !incompleteSteps) return false;
|
||||
const prematureMergeWithIncompleteSteps = implementationIncompleteMergeFailure && incompleteSteps;
|
||||
if (live.column !== "in-review" && !(incompleteSteps && live.column === "todo") && !(prematureMergeWithIncompleteSteps && live.column === "in-progress")) return false;
|
||||
|
||||
const message = incompleteSteps
|
||||
|
||||
Reference in New Issue
Block a user