fix(engine): paused-task guard for merged + misclassified review recovery

Two recovery scans operated on in-review tasks without checking
!task.paused:

- recoverMergedReviewTasks would move a paused task whose merge was
  already confirmed to done, against user intent.
- recoverMisclassifiedFailures would clear the error on a paused failed
  task, defeating the user's intent to investigate manually.

Add the paused guard to both, matching the pattern used by all other
recovery scans. Completes the pause-vs-stuck audit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-28 17:20:04 -07:00
parent 9d09b4c599
commit 07a11437c0
2 changed files with 54 additions and 0 deletions

View File

@@ -1024,6 +1024,31 @@ describe("SelfHealingManager", () => {
managerWithRecovery.stop();
});
it("does not clear errors on paused tasks (respects user investigate intent)", async () => {
const managerWithRecovery = new SelfHealingManager(store, {
rootDir: "/tmp/test-project",
});
(store.listTasks as ReturnType<typeof vi.fn>).mockResolvedValue([
{
id: "FN-303",
column: "in-review",
status: "failed",
paused: true,
error: "Agent finished without calling task_done",
steps: [{ status: "done" }, { status: "done" }],
log: [],
},
]);
const result = await managerWithRecovery.recoverMisclassifiedFailures();
expect(result).toBe(0);
expect(store.updateTask).not.toHaveBeenCalled();
managerWithRecovery.stop();
});
});
describe("recoverPartialProgressNoTaskDoneFailures", () => {
@@ -1642,6 +1667,33 @@ describe("SelfHealingManager", () => {
managerWithRecovery.stop();
});
it("does not move paused merged tasks to done (respects user pause intent)", async () => {
const managerWithRecovery = new SelfHealingManager(store, {
rootDir: "/tmp/test-project",
});
(store.listTasks as ReturnType<typeof vi.fn>).mockResolvedValue([
{
id: "FN-352",
column: "in-review",
paused: true,
mergeDetails: {
mergeConfirmed: true,
mergedAt: "2026-01-01T00:00:00.000Z",
},
log: [],
},
]);
const result = await managerWithRecovery.recoverMergedReviewTasks();
expect(result).toBe(0);
expect(store.updateTask).not.toHaveBeenCalled();
expect(store.moveTask).not.toHaveBeenCalled();
managerWithRecovery.stop();
});
});
describe("recoverReviewTasksWithFailedPreMergeSteps", () => {

View File

@@ -1016,6 +1016,7 @@ export class SelfHealingManager {
const mergedButNotDone = tasks.filter((t) =>
t.column === "in-review" &&
!t.paused &&
t.mergeDetails?.mergeConfirmed === true,
);
@@ -1069,6 +1070,7 @@ export class SelfHealingManager {
const misclassified = tasks.filter((t) =>
t.column === "in-review" &&
!t.paused &&
t.status === "failed" &&
t.error?.includes("without calling task_done") &&
t.steps.length > 0 &&