fix: offer the planning retry for needs-replan Todo cards in triage-less workflows
The task cards already show Retry for needs-replan/planning/failed states, but the retry route only offered the planning retry when the card sat in "triage", so plan-in-place workflows (Coding (Ideas) replans in Todo) got a 400 "not in a retryable state". The retrySpecification gate is now workflow-aware: a Todo card whose workflow declares no "triage" column takes the planning-retry path; default-workflow Todo cards keep the generic-retry semantics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,6 @@
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Ideas-intake cards no longer auto-process on restart, replan stays in Todo, and All-workflows shows every card.
|
||||
summary: Ideas-intake cards no longer auto-process on restart; replan and Retry work from Todo; All-workflows shows every card.
|
||||
category: fix
|
||||
dev: Store init now always runs the workflow-aware integrity pass instead of the retired flag-off evacuation (`evacuateCustomColumnsToLegacy` remains toggle-only), with a mis-mapping guard so stale selections are never physically rehomed into auto-triaged lanes; engine replan/stale-spec/fs-validation rebounds resolve `resolveReplanTargetColumn` instead of hardcoding `triage`; `needs-replan` counts as unplanned for hold-release dispatch; triage discovers `needs-replan` todo cards and refinement seed prompts via `isUnplannedSeedPrompt`/`buildRefinementSeedPrompt`; Board's aggregate grouping renders column-orphaned tasks (hidden columns stay hidden) and the FN-7591 refetch also fires on present-but-unrepresentable mappings.
|
||||
|
||||
@@ -878,6 +878,47 @@ describe("POST /tasks/:id/retry", () => {
|
||||
expect(engine.clearTaskPauseAbortState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:ManualRetry 2026-07-13-12:25:
|
||||
Plan-in-place workflows (Coding (Ideas): no "triage" column) keep needs-replan cards in
|
||||
"todo"; the Retry button the cards already show must map to the planning retry there
|
||||
instead of a 400. Default-workflow todo cards keep the generic-retry semantics.
|
||||
*/
|
||||
it("offers the planning retry for a needs-replan todo card in a workflow without a triage column", async () => {
|
||||
const replanTask = { ...FAKE_TASK_DETAIL, column: "todo", status: "needs-replan" };
|
||||
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(replanTask);
|
||||
(store as unknown as Record<string, unknown>).getTaskWorkflowSelection = vi.fn().mockReturnValue({ workflowId: "builtin:coding-ideas", stepIds: [] });
|
||||
(store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue(replanTask);
|
||||
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/retry", JSON.stringify({}), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
// Planning-retry semantics: status reset to needs-replan, no column move.
|
||||
expect(store.updateTask).toHaveBeenCalledWith("KB-001", expect.objectContaining({ status: "needs-replan" }));
|
||||
expect(store.moveTask).not.toHaveBeenCalled();
|
||||
expect(store.logEntry).toHaveBeenCalledWith("KB-001", "Retry requested from dashboard (planning retry budget reset)");
|
||||
});
|
||||
|
||||
it("keeps generic retry semantics for a needs-replan todo card in the default workflow", async () => {
|
||||
const replanTask = { ...FAKE_TASK_DETAIL, column: "todo", status: "needs-replan" };
|
||||
const movedTask = { ...FAKE_TASK_DETAIL, column: "todo", status: undefined };
|
||||
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(replanTask);
|
||||
(store as unknown as Record<string, unknown>).getTaskWorkflowSelection = vi.fn().mockReturnValue(undefined);
|
||||
(store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue(replanTask);
|
||||
(store.moveTask as ReturnType<typeof vi.fn>).mockResolvedValue(movedTask);
|
||||
|
||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/retry", JSON.stringify({}), {
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
|
||||
// Default workflow declares "triage": a needs-replan todo card is not a planning
|
||||
// retry there — and needs-replan alone is not a generic-retryable status.
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toContain("not in a retryable state");
|
||||
});
|
||||
|
||||
it("retries a failed task in any column (not just in-progress)", async () => {
|
||||
const failedTaskInTodo = { ...FAKE_TASK_DETAIL, column: "todo", status: "failed" };
|
||||
const movedTask = { ...FAKE_TASK_DETAIL, column: "todo", status: undefined };
|
||||
|
||||
@@ -43,6 +43,8 @@ import {
|
||||
isEphemeralAgent,
|
||||
parseExplicitDuplicateMarker,
|
||||
isWorkflowColumnsEnabled,
|
||||
resolveWorkflowIrForTask,
|
||||
workflowHasColumn,
|
||||
TransitionRejectionError,
|
||||
getPlannerInterventionTimeline,
|
||||
isBuiltinWorkflowId,
|
||||
@@ -2289,12 +2291,25 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
try {
|
||||
const { store: scopedStore, engine } = await getProjectContext(req);
|
||||
const task = await scopedStore.getTask(req.params.id);
|
||||
const retrySpecification =
|
||||
task.column === "triage" &&
|
||||
(task.status === "failed" ||
|
||||
task.status === "planning" ||
|
||||
task.status === "needs-replan" ||
|
||||
(task.stuckKillCount ?? 0) > 0);
|
||||
const retrySpecificationStatus =
|
||||
task.status === "failed" ||
|
||||
task.status === "planning" ||
|
||||
task.status === "needs-replan" ||
|
||||
(task.stuckKillCount ?? 0) > 0;
|
||||
let retrySpecification = task.column === "triage" && retrySpecificationStatus;
|
||||
/*
|
||||
FNXC:ManualRetry 2026-07-13-12:20:
|
||||
Plan-in-place workflows (Coding (Ideas): no "triage" column) keep planning/replanning
|
||||
cards in "todo", so the manual Retry button — which the cards already show for
|
||||
needs-replan/planning/failed states — must offer the planning retry there too instead
|
||||
of 400ing with "not in a retryable state". Gated on the task's OWN workflow declaring
|
||||
no "triage" column, so default-workflow todo cards (where todo failures are execution
|
||||
failures) keep the existing generic-retry semantics.
|
||||
*/
|
||||
if (!retrySpecification && task.column === "todo" && retrySpecificationStatus) {
|
||||
const workflowIr = await resolveWorkflowIrForTask(scopedStore, task.id);
|
||||
retrySpecification = !workflowHasColumn(workflowIr, "triage");
|
||||
}
|
||||
const isInReviewStatusNone =
|
||||
task.column === "in-review" && (task.status === null || task.status === undefined);
|
||||
const hasIncompleteSteps = task.steps.some(
|
||||
|
||||
Reference in New Issue
Block a user