fix: address stuck-loop parking review comments

This commit is contained in:
Phil Larson
2026-06-14 10:59:21 -07:00
parent 19eca3d74b
commit f1ac8d5f6b
3 changed files with 17 additions and 12 deletions

View File

@@ -190,7 +190,7 @@ describe("reliability interactions: non-progress churn", () => {
manager.stop();
});
it("re-queues incomplete STUCK_LOOP_EXHAUSTED tasks in todo when the churn signal does not fire", async () => {
it("parks incomplete STUCK_LOOP_EXHAUSTED tasks in todo when the churn signal does not fire", async () => {
const task = baseTask({ id: "FN-5168-LOOP", stuckKillCount: 6 });
const store = createStore(task);
const manager = new SelfHealingManager(store, { rootDir: "/tmp/repo" });
@@ -210,19 +210,19 @@ describe("reliability interactions: non-progress churn", () => {
await detector.killAndRetry(task.id, 60_000);
expect(task.error).toBeNull();
expect(task.status).toBe("queued");
expect(task.error).toContain("STUCK_LOOP_EXHAUSTED: incomplete task exhausted stuck kill budget");
expect(task.status).toBe("failed");
expect(task.column).toBe("todo");
expect(task.paused).toBe(false);
expect(task.paused).toBe(true);
// FN-6252 / Move-Task contract: engine rebounds do not write userPaused,
// so a never-user-paused task remains undefined while still not user-paused.
expect(task.userPaused).not.toBe(true);
expect(task.pausedReason).toBeNull();
expect(task.pausedReason).toBe("stuck-loop-exhausted-manual-intervention-required");
expect(task.stuckKillCount).toBe(7);
expect(task.steps).toEqual([{ name: "Implement", status: "in-progress" }]);
expect(task.log?.some((entry) => entry.action.includes("incomplete task exhausted stuck kill budget"))).toBe(true);
expect(task.log?.some((entry) => entry.action.includes("Parked in todo with progress preserved"))).toBe(true);
expect(store.handoffToReview).not.toHaveBeenCalled();
expect(isRunnableQueuedOverlapCandidate(task, [task])).toBe(true);
expect(isRunnableQueuedOverlapCandidate(task, [task])).toBe(false);
manager.stop();
});

View File

@@ -290,7 +290,7 @@ describe("FN-5941 reliability interactions: todo/in-progress flapping", () => {
manager.stop();
});
it("still requeues a genuinely dead task when stuck-kill budget is exhausted", async () => {
it("parks a genuinely dead incomplete task when stuck-kill budget is exhausted", async () => {
const task = makeTask(rootDir, {
id: "FN-5941-DEAD",
stuckKillCount: 6,
@@ -314,7 +314,10 @@ describe("FN-5941 reliability interactions: todo/in-progress flapping", () => {
}));
expect(task.column).toBe("todo");
expect(task.stuckKillCount).toBe(7);
expect(task.status).toBe("queued");
expect(task.status).toBe("failed");
expect(task.paused).toBe(true);
expect(task.pausedReason).toBe("stuck-loop-exhausted-manual-intervention-required");
expect(task.userPaused).not.toBe(true);
manager.stop();
});

View File

@@ -1214,6 +1214,10 @@ export class SelfHealingManager {
* progress preserved, marked failed, and paused for manual resume or
* decomposition; tasks with only terminal steps keep the legacy failed
* `in-review` handoff path.
*
* FNXC:SelfHealing 2026-06-14-10:51:
* Incomplete stuck-loop exhaustion must park work in a failed/paused state before moving columns, because a post-move patch failure must not leave the task scheduler-runnable.
* Engine-owned recovery must not mutate `userPaused`; user intent stays authoritative across races.
* - `STUCK_NO_PROGRESS_CHURN`: skips the budget entirely and terminalizes on
* the first trigger with operator guidance to decompose or rescope.
*
@@ -1316,9 +1320,7 @@ export class SelfHealingManager {
}
log.warn(`${taskId} exceeded stuck kill budget (${newCount}/${maxKills}, reason=${reason}) with incomplete steps — parking in todo with progress preserved`);
const exhaustedError =
`STUCK_LOOP_EXHAUSTED: incomplete task exhausted stuck kill budget (${newCount}/${maxKills}) after last reason=${reason}. ` +
"Progress was preserved; manually retry, decompose, or rescope before execution resumes.";
const exhaustedError = `STUCK_LOOP_EXHAUSTED: incomplete task exhausted stuck kill budget (${newCount}/${maxKills}) after last reason=${reason}. Progress was preserved; manually retry, decompose, or rescope before execution resumes.`;
const parkUpdate = {
stuckKillCount: newCount,
status: "failed",