From 0e6f94dbe6d4922201951c64ccaa9ed832975fda Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 12 Jul 2026 19:51:10 -0700 Subject: [PATCH] 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 --- .changeset/coding-ideas-intake-safety.md | 2 +- .../src/__tests__/routes-tasks-ops.test.ts | 41 +++++++++++++++++++ .../routes/register-task-workflow-routes.ts | 27 +++++++++--- 3 files changed, 63 insertions(+), 7 deletions(-) diff --git a/.changeset/coding-ideas-intake-safety.md b/.changeset/coding-ideas-intake-safety.md index 537f17b0bd..cc6618d2fb 100644 --- a/.changeset/coding-ideas-intake-safety.md +++ b/.changeset/coding-ideas-intake-safety.md @@ -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. diff --git a/packages/dashboard/src/__tests__/routes-tasks-ops.test.ts b/packages/dashboard/src/__tests__/routes-tasks-ops.test.ts index d4c7c62295..d1d3c0139f 100644 --- a/packages/dashboard/src/__tests__/routes-tasks-ops.test.ts +++ b/packages/dashboard/src/__tests__/routes-tasks-ops.test.ts @@ -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).mockResolvedValue(replanTask); + (store as unknown as Record).getTaskWorkflowSelection = vi.fn().mockReturnValue({ workflowId: "builtin:coding-ideas", stepIds: [] }); + (store.updateTask as ReturnType).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).mockResolvedValue(replanTask); + (store as unknown as Record).getTaskWorkflowSelection = vi.fn().mockReturnValue(undefined); + (store.updateTask as ReturnType).mockResolvedValue(replanTask); + (store.moveTask as ReturnType).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 }; diff --git a/packages/dashboard/src/routes/register-task-workflow-routes.ts b/packages/dashboard/src/routes/register-task-workflow-routes.ts index 63b753a0ed..47d456ab95 100644 --- a/packages/dashboard/src/routes/register-task-workflow-routes.ts +++ b/packages/dashboard/src/routes/register-task-workflow-routes.ts @@ -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(