fix(FN-7221): clear stale workflow pins on spec rebuild

This commit is contained in:
gsxdsm
2026-06-29 00:37:17 -07:00
parent 25ea42c5f1
commit c7cbd1dbf1
4 changed files with 45 additions and 0 deletions

View File

@@ -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.

View File

@@ -213,6 +213,7 @@ function createMockStore(overrides: Partial<TaskStore> = {}): 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<typeof vi.fn> }).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<typeof vi.fn> }).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<typeof vi.fn> }).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<typeof vi.fn> }).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<typeof vi.fn> }).clearWorkflowRunStepInstances).toHaveBeenCalledWith("FN-001");
expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-replan" });
});

View File

@@ -63,6 +63,21 @@ const DUPLICATE_STOPWORDS = new Set(["a", "an", "the", "and", "or", "of", "to",
const ARTIFACT_TYPES = new Set<ArtifactType>(["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");

View File

@@ -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) {