From c7cbd1dbf1c013128072877b6e85e0bf1c5c871d Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Mon, 29 Jun 2026 00:37:17 -0700 Subject: [PATCH] fix(FN-7221): clear stale workflow pins on spec rebuild --- .../fn-7221-clear-rebuilt-workflow-pins.md | 7 +++++++ .../src/__tests__/routes-github.test.ts | 6 ++++++ .../src/routes/register-task-workflow-routes.ts | 17 +++++++++++++++++ packages/engine/src/triage.ts | 15 +++++++++++++++ 4 files changed, 45 insertions(+) create mode 100644 .changeset/fn-7221-clear-rebuilt-workflow-pins.md diff --git a/.changeset/fn-7221-clear-rebuilt-workflow-pins.md b/.changeset/fn-7221-clear-rebuilt-workflow-pins.md new file mode 100644 index 0000000000..bce05ab83b --- /dev/null +++ b/.changeset/fn-7221-clear-rebuilt-workflow-pins.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Prevent rebuilt stepwise workflow tasks from failing at parse on stale step pins. +category: fix +dev: Spec rebuild and AI replan handoff now clear persisted workflow foreach instances before reparsing PROMPT.md. diff --git a/packages/dashboard/src/__tests__/routes-github.test.ts b/packages/dashboard/src/__tests__/routes-github.test.ts index 8c2a45bf3b..c4f43fe063 100644 --- a/packages/dashboard/src/__tests__/routes-github.test.ts +++ b/packages/dashboard/src/__tests__/routes-github.test.ts @@ -213,6 +213,7 @@ function createMockStore(overrides: Partial = {}): TaskStore { getWorkflowStep: vi.fn(), updateWorkflowStep: vi.fn(), deleteWorkflowStep: vi.fn(), + clearWorkflowRunStepInstances: vi.fn(), getMissionStore: vi.fn().mockReturnValue({ listMissions: vi.fn().mockReturnValue([]), createMission: vi.fn(), @@ -1821,6 +1822,7 @@ describe("POST /tasks/:id/spec/rebuild", () => { "Specification rebuild requested by user" ); expect(store.moveTask).toHaveBeenCalledWith("FN-001", "triage"); + expect((store as unknown as { clearWorkflowRunStepInstances: ReturnType }).clearWorkflowRunStepInstances).toHaveBeenCalledWith("FN-001"); expect(existsSync(join(taskDir, "PROMPT.md"))).toBe(false); expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-replan" }); } finally { @@ -1840,6 +1842,7 @@ describe("POST /tasks/:id/spec/rebuild", () => { expect(res.status).toBe(200); expect(store.moveTask).toHaveBeenCalledWith("FN-001", "triage"); + expect((store as unknown as { clearWorkflowRunStepInstances: ReturnType }).clearWorkflowRunStepInstances).toHaveBeenCalledWith("FN-001"); expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-replan" }); }); @@ -1855,6 +1858,7 @@ describe("POST /tasks/:id/spec/rebuild", () => { expect(res.status).toBe(200); expect(store.moveTask).toHaveBeenCalledWith("FN-001", "triage"); + expect((store as unknown as { clearWorkflowRunStepInstances: ReturnType }).clearWorkflowRunStepInstances).toHaveBeenCalledWith("FN-001"); }); it("allows rebuild for task already in triage", async () => { @@ -1880,6 +1884,7 @@ describe("POST /tasks/:id/spec/rebuild", () => { "Specification rebuild requested by user" ); expect(store.moveTask).not.toHaveBeenCalled(); + expect((store as unknown as { clearWorkflowRunStepInstances: ReturnType }).clearWorkflowRunStepInstances).toHaveBeenCalledWith("FN-001"); expect(existsSync(join(taskDir, "PROMPT.md"))).toBe(false); expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-replan" }); } finally { @@ -1898,6 +1903,7 @@ describe("POST /tasks/:id/spec/rebuild", () => { expect(res.status).toBe(200); expect(store.moveTask).toHaveBeenCalledWith("FN-001", "triage"); + expect((store as unknown as { clearWorkflowRunStepInstances: ReturnType }).clearWorkflowRunStepInstances).toHaveBeenCalledWith("FN-001"); expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-replan" }); }); diff --git a/packages/dashboard/src/routes/register-task-workflow-routes.ts b/packages/dashboard/src/routes/register-task-workflow-routes.ts index bd6764951d..4862320e20 100644 --- a/packages/dashboard/src/routes/register-task-workflow-routes.ts +++ b/packages/dashboard/src/routes/register-task-workflow-routes.ts @@ -63,6 +63,21 @@ const DUPLICATE_STOPWORDS = new Set(["a", "an", "the", "and", "or", "of", "to", const ARTIFACT_TYPES = new Set(["document", "image", "video", "audio", "other"]); const ADDRESS_PR_FEEDBACK_PROMPT = "Run /ce-resolve-pr-feedback to resolve open PR review feedback: evaluate each thread, fix valid issues, and reply."; +function clearRebuiltSpecWorkflowPins(store: TaskStore, taskId: string): void { + /* + FNXC:WorkflowReplan 2026-06-29-00:33: + Spec rebuild intentionally invalidates the planned step source, so persisted graph foreach pins from the previous PROMPT.md must be cleared before the next parse-steps node runs. Keeping the stale pins makes rebuilt tasks fail closed with pin-mismatch at parse instead of executing the fresh plan. + */ + const maybeStore = store as unknown as { + clearWorkflowRunStepInstances?: (taskId: string) => void; + }; + try { + maybeStore.clearWorkflowRunStepInstances?.(taskId); + } catch { + // Legacy stores may not have workflow-run instance persistence; rebuild must still proceed. + } +} + function isArtifactType(value: string): value is ArtifactType { return ARTIFACT_TYPES.has(value as ArtifactType); } @@ -3071,6 +3086,7 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork if (task.column === "triage") { // Log the rebuild request await scopedStore.logEntry(task.id, "Specification rebuild requested by user"); + clearRebuiltSpecWorkflowPins(scopedStore, task.id); // Remove the existing spec so rebuilds produce a fresh PROMPT.md instead // of asking triage to revise whatever was already on disk. @@ -3099,6 +3115,7 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork // Log the rebuild request await scopedStore.logEntry(task.id, "Specification rebuild requested by user"); + clearRebuiltSpecWorkflowPins(scopedStore, task.id); // Move to triage for replanning const updated = await scopedStore.moveTask(task.id, "triage"); diff --git a/packages/engine/src/triage.ts b/packages/engine/src/triage.ts index d3d7a473f2..5677cdbbce 100644 --- a/packages/engine/src/triage.ts +++ b/packages/engine/src/triage.ts @@ -2636,6 +2636,21 @@ export class TriageProcessor { return; } + if (options.isReplan) { + /* + FNXC:WorkflowReplan 2026-06-29-00:33: + AI spec revision replaces the task's step-source PROMPT.md, so graph foreach instance pins from the previous plan must be discarded before execution reparses steps. Otherwise rebuilt tasks can fail at parse with a stale pin-mismatch even though the new plan is valid. + */ + const maybeStore = this.store as unknown as { + clearWorkflowRunStepInstances?: (taskId: string) => void; + }; + try { + maybeStore.clearWorkflowRunStepInstances?.(task.id); + } catch { + // Older stores may not persist graph step instances; replanning remains valid without cleanup. + } + } + await this.store.moveTask(task.id, "todo"); if (shouldApplyPromptDeclaredTitle && promptDeclaredTitle) {