FN-8148: fix Respecify workflow replanning
Make Respecify reliably return tasks to their workflow's planning lane. - Resolve task-specific planner lanes with recovery rehoming for custom workflows - Return the persisted needs-replan task for in-place replans and cover legacy triage - Hide unsupported Respecify actions for archived tasks Files changed: .changeset/fn-8148-respecify-fix.md | 7 ++ .../dashboard/app/components/TaskContextMenu.tsx | 10 +- .../components/__tests__/TaskContextMenu.test.tsx | 6 +- .../dashboard/src/__tests__/routes-github.test.ts | 103 ++++++++++++++++++++- .../src/routes/register-task-workflow-routes.ts | 66 ++++++------- 5 files changed, 152 insertions(+), 40 deletions(-) Fusion-Task-Id: FN-8148 Fusion-Task-Lineage: 7184bd58-e841-4bb1-a7fa-c1c336d6270a Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8148-respecify-fix.md
Normal file
7
.changeset/fn-8148-respecify-fix.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Make Respecify replan tasks across workflow board layouts.
|
||||||
|
category: fix
|
||||||
|
dev: Resolves workflow planner lanes with recovery rehome and hides the unsupported archived action.
|
||||||
@@ -264,7 +264,15 @@ export function buildTaskActionMenuModel(options: BuildTaskActionMenuModelOption
|
|||||||
actions.push({ id: "refine", label: t("taskDetail.refine.btn", "Refine"), onSelect: options.onOpenRefine });
|
actions.push({ id: "refine", label: t("taskDetail.refine.btn", "Refine"), onSelect: options.onOpenRefine });
|
||||||
}
|
}
|
||||||
|
|
||||||
actions.push({ id: "respecify", label: t("taskDetail.respecify.btn", "Respecify"), onSelect: options.onRespecify });
|
/*
|
||||||
|
FNXC:TaskContextMenu 2026-07-16-12:00:
|
||||||
|
Archived is an unsupported Respecify source: the rebuild route rejects it rather than
|
||||||
|
resurrecting intentionally archived work into a planner lane. Check both the semantic
|
||||||
|
workflow trait and legacy id so every menu host omits this dead affordance.
|
||||||
|
*/
|
||||||
|
if (task.column !== "archived" && currentColumnFlags?.archived !== true) {
|
||||||
|
actions.push({ id: "respecify", label: t("taskDetail.respecify.btn", "Respecify"), onSelect: options.onRespecify });
|
||||||
|
}
|
||||||
|
|
||||||
if (canRetryTask && hasRetryHandler) {
|
if (canRetryTask && hasRetryHandler) {
|
||||||
actions.push({ id: "retry", label: t("taskDetail.retry.btn", "Retry"), onSelect: options.onRetry });
|
actions.push({ id: "retry", label: t("taskDetail.retry.btn", "Retry"), onSelect: options.onRetry });
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ describe("TaskContextMenu shared task action model", () => {
|
|||||||
]);
|
]);
|
||||||
expect(actionIds(makeTask({ column: "done" }), { hasResetHandler: true, onOpenRefine: vi.fn() })).toEqual(["refine", "respecify", "delete"]);
|
expect(actionIds(makeTask({ column: "done" }), { hasResetHandler: true, onOpenRefine: vi.fn() })).toEqual(["refine", "respecify", "delete"]);
|
||||||
expect(actionIds(makeTask({ column: "done" }), { hasResetHandler: true })).toEqual(["respecify", "delete"]);
|
expect(actionIds(makeTask({ column: "done" }), { hasResetHandler: true })).toEqual(["respecify", "delete"]);
|
||||||
expect(actionIds(makeTask({ column: "archived" }), { hasResetHandler: true })).toEqual(["respecify", "delete"]);
|
expect(actionIds(makeTask({ column: "archived" }), { hasResetHandler: true })).toEqual(["delete"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("exposes Plan only for pre-execution hold columns with a host callback", () => {
|
it("exposes Plan only for pre-execution hold columns with a host callback", () => {
|
||||||
@@ -199,7 +199,7 @@ describe("TaskContextMenu shared task action model", () => {
|
|||||||
workflowMoveColumns,
|
workflowMoveColumns,
|
||||||
hasResetHandler: true,
|
hasResetHandler: true,
|
||||||
});
|
});
|
||||||
expect(archivedModel.actions.map((action) => action.id)).toEqual(["respecify", "delete"]);
|
expect(archivedModel.actions.map((action) => action.id)).toEqual(["delete"]);
|
||||||
expect(archivedModel.actions.map((action) => action.id)).not.toContain("reset");
|
expect(archivedModel.actions.map((action) => action.id)).not.toContain("reset");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -254,7 +254,7 @@ describe("TaskContextMenu shared task action model", () => {
|
|||||||
onDelete,
|
onDelete,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(archivedModel.actions.map((action) => action.id)).toEqual(["respecify", "delete"]);
|
expect(archivedModel.actions.map((action) => action.id)).toEqual(["delete"]);
|
||||||
expect(archivedModel.actions.map((action) => action.id)).not.toContain("pause");
|
expect(archivedModel.actions.map((action) => action.id)).not.toContain("pause");
|
||||||
expect(archivedModel.actions.map((action) => action.id)).not.toContain("reset");
|
expect(archivedModel.actions.map((action) => action.id)).not.toContain("reset");
|
||||||
archivedModel.actions.find((action) => action.id === "delete")?.onSelect?.();
|
archivedModel.actions.find((action) => action.id === "delete")?.onSelect?.();
|
||||||
|
|||||||
@@ -2271,7 +2271,7 @@ describe("POST /tasks/:id/spec/rebuild", () => {
|
|||||||
"FN-001",
|
"FN-001",
|
||||||
"Specification rebuild requested by user"
|
"Specification rebuild requested by user"
|
||||||
);
|
);
|
||||||
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "triage");
|
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "triage", { moveSource: "user", recoveryRehome: true });
|
||||||
expect((store as unknown as { clearWorkflowRunStepInstances: ReturnType<typeof vi.fn> }).clearWorkflowRunStepInstances).toHaveBeenCalledWith("FN-001");
|
expect((store as unknown as { clearWorkflowRunStepInstances: ReturnType<typeof vi.fn> }).clearWorkflowRunStepInstances).toHaveBeenCalledWith("FN-001");
|
||||||
expect(existsSync(join(taskDir, "PROMPT.md"))).toBe(false);
|
expect(existsSync(join(taskDir, "PROMPT.md"))).toBe(false);
|
||||||
expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-replan" });
|
expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-replan" });
|
||||||
@@ -2291,7 +2291,7 @@ describe("POST /tasks/:id/spec/rebuild", () => {
|
|||||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/spec/rebuild");
|
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/spec/rebuild");
|
||||||
|
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "triage");
|
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "triage", { moveSource: "user", recoveryRehome: true });
|
||||||
expect((store as unknown as { clearWorkflowRunStepInstances: ReturnType<typeof vi.fn> }).clearWorkflowRunStepInstances).toHaveBeenCalledWith("FN-001");
|
expect((store as unknown as { clearWorkflowRunStepInstances: ReturnType<typeof vi.fn> }).clearWorkflowRunStepInstances).toHaveBeenCalledWith("FN-001");
|
||||||
expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-replan" });
|
expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-replan" });
|
||||||
});
|
});
|
||||||
@@ -2307,7 +2307,7 @@ describe("POST /tasks/:id/spec/rebuild", () => {
|
|||||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/spec/rebuild");
|
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/spec/rebuild");
|
||||||
|
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "triage");
|
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "triage", { moveSource: "user", recoveryRehome: true });
|
||||||
expect((store as unknown as { clearWorkflowRunStepInstances: ReturnType<typeof vi.fn> }).clearWorkflowRunStepInstances).toHaveBeenCalledWith("FN-001");
|
expect((store as unknown as { clearWorkflowRunStepInstances: ReturnType<typeof vi.fn> }).clearWorkflowRunStepInstances).toHaveBeenCalledWith("FN-001");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2337,6 +2337,7 @@ describe("POST /tasks/:id/spec/rebuild", () => {
|
|||||||
expect((store as unknown as { clearWorkflowRunStepInstances: ReturnType<typeof vi.fn> }).clearWorkflowRunStepInstances).toHaveBeenCalledWith("FN-001");
|
expect((store as unknown as { clearWorkflowRunStepInstances: ReturnType<typeof vi.fn> }).clearWorkflowRunStepInstances).toHaveBeenCalledWith("FN-001");
|
||||||
expect(existsSync(join(taskDir, "PROMPT.md"))).toBe(false);
|
expect(existsSync(join(taskDir, "PROMPT.md"))).toBe(false);
|
||||||
expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-replan" });
|
expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-replan" });
|
||||||
|
expect(res.body.status).toBe("needs-replan");
|
||||||
} finally {
|
} finally {
|
||||||
rmSync(tempRoot, { recursive: true, force: true });
|
rmSync(tempRoot, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
@@ -2352,11 +2353,105 @@ describe("POST /tasks/:id/spec/rebuild", () => {
|
|||||||
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/spec/rebuild");
|
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/spec/rebuild");
|
||||||
|
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "triage");
|
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "triage", { moveSource: "user", recoveryRehome: true });
|
||||||
expect((store as unknown as { clearWorkflowRunStepInstances: ReturnType<typeof vi.fn> }).clearWorkflowRunStepInstances).toHaveBeenCalledWith("FN-001");
|
expect((store as unknown as { clearWorkflowRunStepInstances: ReturnType<typeof vi.fn> }).clearWorkflowRunStepInstances).toHaveBeenCalledWith("FN-001");
|
||||||
expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-replan" });
|
expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-replan" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function selectWorkflow(columns: Array<{ id: string; traits?: Array<{ trait: string }> }>) {
|
||||||
|
const workflowIr = {
|
||||||
|
version: 2,
|
||||||
|
columns: columns.map((column) => ({ name: column.id, traits: [], ...column })),
|
||||||
|
nodes: [],
|
||||||
|
edges: [],
|
||||||
|
};
|
||||||
|
(store as unknown as { getTaskWorkflowSelection: ReturnType<typeof vi.fn> }).getTaskWorkflowSelection = vi.fn()
|
||||||
|
.mockReturnValue({ workflowId: "WF-rebuild-test" });
|
||||||
|
(store as unknown as { getWorkflowDefinition: ReturnType<typeof vi.fn> }).getWorkflowDefinition = vi.fn()
|
||||||
|
.mockResolvedValue({ id: "WF-rebuild-test", ir: workflowIr });
|
||||||
|
}
|
||||||
|
|
||||||
|
it("rebuilds a no-triage/no-todo workflow through legacy triage recovery rehome", async () => {
|
||||||
|
const customTask = { ...FAKE_TASK_DETAIL, column: "publish" as any };
|
||||||
|
const movedTask = { ...customTask, column: "triage" as any };
|
||||||
|
const tempRoot = mkdtempSync(join(tmpdir(), "kb-spec-rebuild-legacy-rehome-"));
|
||||||
|
const taskDir = join(tempRoot, ".fusion", "tasks", "FN-001");
|
||||||
|
mkdirSync(taskDir, { recursive: true });
|
||||||
|
writeFileSync(join(taskDir, "PROMPT.md"), "# stale spec\n");
|
||||||
|
selectWorkflow([{ id: "publish" }]);
|
||||||
|
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(customTask);
|
||||||
|
(store.moveTask as ReturnType<typeof vi.fn>).mockResolvedValue(movedTask);
|
||||||
|
(store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue(movedTask);
|
||||||
|
(store.getRootDir as ReturnType<typeof vi.fn>).mockReturnValue(tempRoot);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/spec/rebuild");
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "triage", { moveSource: "user", recoveryRehome: true });
|
||||||
|
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 {
|
||||||
|
rmSync(tempRoot, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the updated task when a plan-in-place workflow is already in todo", async () => {
|
||||||
|
const todoTask = { ...FAKE_TASK_DETAIL, column: "todo" as any };
|
||||||
|
const updatedTask = { ...todoTask, status: "needs-replan" as const };
|
||||||
|
selectWorkflow([{ id: "todo" }, { id: "in-review" }]);
|
||||||
|
(store.getTask as ReturnType<typeof vi.fn>)
|
||||||
|
.mockResolvedValueOnce(todoTask)
|
||||||
|
.mockResolvedValueOnce(updatedTask);
|
||||||
|
(store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue(updatedTask);
|
||||||
|
|
||||||
|
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/spec/rebuild");
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(store.moveTask).not.toHaveBeenCalled();
|
||||||
|
expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-replan" });
|
||||||
|
expect(res.body.status).toBe("needs-replan");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rebuilds a plan-in-place workflow into todo", async () => {
|
||||||
|
const customTask = { ...FAKE_TASK_DETAIL, column: "in-review" as any };
|
||||||
|
const movedTask = { ...customTask, column: "todo" as any };
|
||||||
|
selectWorkflow([{ id: "todo" }, { id: "in-review" }]);
|
||||||
|
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(customTask);
|
||||||
|
(store.moveTask as ReturnType<typeof vi.fn>).mockResolvedValue(movedTask);
|
||||||
|
(store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue(movedTask);
|
||||||
|
|
||||||
|
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/spec/rebuild");
|
||||||
|
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "todo", { moveSource: "user", recoveryRehome: true });
|
||||||
|
expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-replan" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects legacy and semantic archived tasks before rebuilding", async () => {
|
||||||
|
const tempRoot = mkdtempSync(join(tmpdir(), "kb-spec-rebuild-archived-"));
|
||||||
|
const taskDir = join(tempRoot, ".fusion", "tasks", "FN-001");
|
||||||
|
mkdirSync(taskDir, { recursive: true });
|
||||||
|
writeFileSync(join(taskDir, "PROMPT.md"), "# retained spec\n");
|
||||||
|
const archivedTask = { ...FAKE_TASK_DETAIL, column: "cold-storage" as any };
|
||||||
|
selectWorkflow([{ id: "cold-storage", traits: [{ trait: "archived" }] }]);
|
||||||
|
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(archivedTask);
|
||||||
|
(store.getRootDir as ReturnType<typeof vi.fn>).mockReturnValue(tempRoot);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await REQUEST(buildApp(), "POST", "/api/tasks/KB-001/spec/rebuild");
|
||||||
|
|
||||||
|
expect(res.status).toBe(400);
|
||||||
|
expect(res.body.error).toContain("not available for archived tasks");
|
||||||
|
expect(store.moveTask).not.toHaveBeenCalled();
|
||||||
|
expect(store.updateTask).not.toHaveBeenCalled();
|
||||||
|
expect(existsSync(join(taskDir, "PROMPT.md"))).toBe(true);
|
||||||
|
} finally {
|
||||||
|
rmSync(tempRoot, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("returns 404 when task not found", async () => {
|
it("returns 404 when task not found", async () => {
|
||||||
const error = new Error("Task not found") as Error & { code?: string };
|
const error = new Error("Task not found") as Error & { code?: string };
|
||||||
error.code = "ENOENT";
|
error.code = "ENOENT";
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import {
|
|||||||
isWorkflowColumnsEnabled,
|
isWorkflowColumnsEnabled,
|
||||||
resolveWorkflowIrForTask,
|
resolveWorkflowIrForTask,
|
||||||
workflowHasColumn,
|
workflowHasColumn,
|
||||||
|
resolveColumnFlags,
|
||||||
TransitionRejectionError,
|
TransitionRejectionError,
|
||||||
getPlannerInterventionTimeline,
|
getPlannerInterventionTimeline,
|
||||||
isBuiltinWorkflowId,
|
isBuiltinWorkflowId,
|
||||||
@@ -4260,44 +4261,39 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
|||||||
// Get current task state
|
// Get current task state
|
||||||
const task = await scopedStore.getTask(req.params.id);
|
const task = await scopedStore.getTask(req.params.id);
|
||||||
|
|
||||||
// If task is already in triage, skip the transition check and moveTask.
|
const workflowIr = await resolveWorkflowIrForTask(scopedStore, task.id);
|
||||||
// Just reset for replanning in place.
|
const currentColumn = "columns" in workflowIr
|
||||||
if (task.column === "triage") {
|
? workflowIr.columns.find((column) => column.id === task.column)
|
||||||
// Log the rebuild request
|
: undefined;
|
||||||
await scopedStore.logEntry(task.id, "Specification rebuild requested by user");
|
const isArchived = task.column === "archived" || (currentColumn != null && resolveColumnFlags(currentColumn).archived);
|
||||||
clearRebuiltSpecWorkflowPins(scopedStore, task.id);
|
if (isArchived) {
|
||||||
|
throw badRequest("Respecify is not available for archived tasks; unarchive first.");
|
||||||
// Remove the existing spec so rebuilds produce a fresh PROMPT.md instead
|
|
||||||
// of asking triage to revise whatever was already on disk.
|
|
||||||
const { rm } = await import("node:fs/promises");
|
|
||||||
const { join } = await import("node:path");
|
|
||||||
const promptPath = join(scopedStore.getRootDir(), ".fusion", "tasks", task.id, "PROMPT.md");
|
|
||||||
await rm(promptPath, { force: true });
|
|
||||||
|
|
||||||
// Update status to indicate needs replanning
|
|
||||||
await scopedStore.updateTask(task.id, { status: "needs-replan" });
|
|
||||||
|
|
||||||
const updated = await scopedStore.getTask(task.id);
|
|
||||||
res.json(updated);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if task can transition to triage
|
/*
|
||||||
// #1403: task.column is ColumnId; VALID_TRANSITIONS is keyed by the legacy
|
FNXC:WorkflowReplan 2026-07-16-12:00:
|
||||||
// closed union. A non-legacy custom column id has no legacy transition row,
|
Respecify must park work in a planner lane belonging to the task's own workflow:
|
||||||
// so it correctly resolves to "cannot transition" here.
|
triage when declared, otherwise plan-in-place todo, then legacy triage for workflows
|
||||||
const canTransition =
|
with neither. The legacy fallback is intentionally recovery-rehomed: plain moves reject
|
||||||
isColumn(task.column) && VALID_TRANSITIONS[task.column].includes("triage");
|
an undeclared triage target as unknown-column (and reject non-adjacent sources), which
|
||||||
if (!canTransition) {
|
previously stranded no-triage workflows before their needs-replan status was written.
|
||||||
throw badRequest(`Cannot rebuild spec for tasks in '${task.column}' column. Move task to a valid column first.`);
|
Archived cards are rejected above rather than resurrected into a planner lane.
|
||||||
}
|
*/
|
||||||
|
const replanColumn = workflowHasColumn(workflowIr, "triage")
|
||||||
|
? "triage"
|
||||||
|
: workflowHasColumn(workflowIr, "todo")
|
||||||
|
? "todo"
|
||||||
|
: "triage";
|
||||||
|
|
||||||
// Log the rebuild request
|
|
||||||
await scopedStore.logEntry(task.id, "Specification rebuild requested by user");
|
await scopedStore.logEntry(task.id, "Specification rebuild requested by user");
|
||||||
clearRebuiltSpecWorkflowPins(scopedStore, task.id);
|
clearRebuiltSpecWorkflowPins(scopedStore, task.id);
|
||||||
|
|
||||||
// Move to triage for replanning
|
if (task.column !== replanColumn) {
|
||||||
const updated = await scopedStore.moveTask(task.id, "triage");
|
await scopedStore.moveTask(task.id, replanColumn, {
|
||||||
|
moveSource: "user",
|
||||||
|
recoveryRehome: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Remove the existing spec so rebuilds produce a fresh PROMPT.md instead
|
// Remove the existing spec so rebuilds produce a fresh PROMPT.md instead
|
||||||
// of asking triage to revise whatever was already on disk.
|
// of asking triage to revise whatever was already on disk.
|
||||||
@@ -4309,6 +4305,12 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
|||||||
// Update status to indicate needs replanning
|
// Update status to indicate needs replanning
|
||||||
await scopedStore.updateTask(task.id, { status: "needs-replan" });
|
await scopedStore.updateTask(task.id, { status: "needs-replan" });
|
||||||
|
|
||||||
|
/*
|
||||||
|
FNXC:WorkflowReplan 2026-07-16-12:00:
|
||||||
|
Respecify responses must re-read the persisted task after setting needs-replan so
|
||||||
|
planner-lane-in-place requests, including legacy triage, never return stale status.
|
||||||
|
*/
|
||||||
|
const updated = await scopedStore.getTask(task.id);
|
||||||
res.json(updated);
|
res.json(updated);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
if (err instanceof ApiError) {
|
if (err instanceof ApiError) {
|
||||||
|
|||||||
Reference in New Issue
Block a user