feat(core,dashboard): allow tasks to be respec'd from in-review
Add `triage` to VALID_TRANSITIONS["in-review"] so the dashboard's `Request AI Revision` and `Rebuild Spec` actions work for in-review tasks. moveTask now applies the same full reset on in-review → triage as on in-review → todo (clears branch/baseBranch/baseCommitSha/summary/ recovery metadata and workflowStepResults) so the respec'd task starts from scratch. The in-review task card's Move menu also gains Planning as a destination. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
13
.changeset/respec-from-in-review.md
Normal file
13
.changeset/respec-from-in-review.md
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
"@runfusion/fusion": minor
|
||||
"runfusion.ai": minor
|
||||
"@fusion/core": minor
|
||||
"@fusion/dashboard": minor
|
||||
"@fusion/desktop": minor
|
||||
"@fusion/engine": minor
|
||||
"@fusion/mobile": minor
|
||||
"@fusion/pi-claude-cli": minor
|
||||
"@fusion/plugin-sdk": minor
|
||||
---
|
||||
|
||||
Allow tasks to be respecified from `in-review`. `VALID_TRANSITIONS["in-review"]` now includes `triage`, so the dashboard's `Request AI Revision` and `Rebuild Spec` actions work for in-review tasks. Moving an in-review task to triage performs the same full reset as in-review → todo (clears branch/baseBranch/baseCommitSha/summary/recovery metadata and workflowStepResults) so the next run starts from scratch. The in-review card's `Move` menu also now offers `Planning` as a destination.
|
||||
@@ -79,7 +79,7 @@ describe("board", () => {
|
||||
});
|
||||
|
||||
it("returns correct transitions for in-review", () => {
|
||||
expect(getValidTransitions("in-review")).toEqual(["done", "in-progress", "todo"]);
|
||||
expect(getValidTransitions("in-review")).toEqual(["done", "in-progress", "todo", "triage"]);
|
||||
});
|
||||
|
||||
it("returns correct transitions for done", () => {
|
||||
|
||||
@@ -5991,6 +5991,45 @@ Task with acceptance criteria
|
||||
expect(retried.recoveryRetryCount).toBeUndefined();
|
||||
expect(retried.nextRecoveryAt).toBeUndefined();
|
||||
});
|
||||
|
||||
it("allows respec'ing in-review tasks back to triage and clears transient fields", async () => {
|
||||
const task = await store.createTask({ description: "test respec in-review task to triage" });
|
||||
await store.moveTask(task.id, "todo");
|
||||
await store.moveTask(task.id, "in-progress");
|
||||
await store.moveTask(task.id, "in-review");
|
||||
await store.updateTask(task.id, {
|
||||
status: "completed",
|
||||
error: "stale error",
|
||||
worktree: "stale-worktree",
|
||||
blockedBy: "FN-456",
|
||||
branch: "fn/stale-branch",
|
||||
baseBranch: "main",
|
||||
baseCommitSha: "abc123",
|
||||
summary: "stale summary from prior attempt",
|
||||
recoveryRetryCount: 2,
|
||||
nextRecoveryAt: new Date().toISOString(),
|
||||
workflowStepResults: [{
|
||||
workflowStepId: "wf-1",
|
||||
workflowStepName: "Workflow step 1",
|
||||
status: "passed",
|
||||
startedAt: new Date().toISOString(),
|
||||
}],
|
||||
});
|
||||
|
||||
const respec = await store.moveTask(task.id, "triage");
|
||||
expect(respec.column).toBe("triage");
|
||||
expect(respec.status).toBeUndefined();
|
||||
expect(respec.error).toBeUndefined();
|
||||
expect(respec.worktree).toBeUndefined();
|
||||
expect(respec.blockedBy).toBeUndefined();
|
||||
expect(respec.workflowStepResults).toBeUndefined();
|
||||
expect(respec.branch).toBeUndefined();
|
||||
expect(respec.baseBranch).toBeUndefined();
|
||||
expect(respec.baseCommitSha).toBeUndefined();
|
||||
expect(respec.summary).toBeUndefined();
|
||||
expect(respec.recoveryRetryCount).toBeUndefined();
|
||||
expect(respec.nextRecoveryAt).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("columnMovedAt", () => {
|
||||
|
||||
@@ -2640,15 +2640,16 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
// Clear workflow step results when reopening from review/completed states.
|
||||
// This ensures fresh workflow step runs on retry
|
||||
if (
|
||||
(fromColumn === "in-review" && (toColumn === "todo" || toColumn === "in-progress"))
|
||||
(fromColumn === "in-review" && (toColumn === "todo" || toColumn === "in-progress" || toColumn === "triage"))
|
||||
|| (fromColumn === "done" && (toColumn === "todo" || toColumn === "triage"))
|
||||
) {
|
||||
task.workflowStepResults = undefined;
|
||||
}
|
||||
|
||||
// Full reset when sending an in-review task back to todo: discard prior
|
||||
// branch/summary/recovery state so the next run starts from scratch.
|
||||
if (fromColumn === "in-review" && toColumn === "todo") {
|
||||
// Full reset when sending an in-review task back to todo or triage
|
||||
// (respec): discard prior branch/summary/recovery state so the next run
|
||||
// starts from scratch.
|
||||
if (fromColumn === "in-review" && (toColumn === "todo" || toColumn === "triage")) {
|
||||
task.branch = undefined;
|
||||
task.baseBranch = undefined;
|
||||
task.baseCommitSha = undefined;
|
||||
|
||||
@@ -1898,7 +1898,7 @@ export const VALID_TRANSITIONS: Record<Column, Column[]> = {
|
||||
// NOTE: "in-progress" → "done" is enabled for mission validation tasks that complete directly.
|
||||
// Regular implementation tasks should move through "in-review" before "done".
|
||||
"in-progress": ["in-review", "todo", "triage", "done"],
|
||||
"in-review": ["done", "in-progress", "todo"],
|
||||
"in-review": ["done", "in-progress", "todo", "triage"],
|
||||
done: ["todo", "triage", "archived"],
|
||||
archived: ["done"],
|
||||
};
|
||||
|
||||
@@ -1188,8 +1188,8 @@ export function TaskDetailModal({
|
||||
onClose();
|
||||
} catch (err) {
|
||||
const msg = getErrorMessage(err);
|
||||
if (msg.includes("in-review") || msg.includes("done")) {
|
||||
addToast("Cannot request revision: Task must be in 'todo' or 'in-progress' column.", "error");
|
||||
if (msg.includes("done") || msg.includes("archived")) {
|
||||
addToast("Cannot request revision: Task must be in 'triage', 'todo', 'in-progress', or 'in-review' column.", "error");
|
||||
} else {
|
||||
addToast(msg, "error");
|
||||
}
|
||||
|
||||
@@ -7824,9 +7824,12 @@ describe("POST /tasks/:id/spec/revise", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("returns 400 when task is in in-review", async () => {
|
||||
it("allows spec revision when task is in in-review (in-review can transition to triage)", async () => {
|
||||
const inReviewTask = { ...FAKE_TASK_DETAIL, column: "in-review" as const };
|
||||
const movedTask = { ...FAKE_TASK_DETAIL, column: "triage" as const };
|
||||
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(inReviewTask);
|
||||
(store.moveTask as ReturnType<typeof vi.fn>).mockResolvedValue(movedTask);
|
||||
(store.updateTask as ReturnType<typeof vi.fn>).mockResolvedValue(movedTask);
|
||||
|
||||
const res = await REQUEST(
|
||||
buildApp(),
|
||||
@@ -7836,10 +7839,9 @@ describe("POST /tasks/:id/spec/revise", () => {
|
||||
{ "Content-Type": "application/json" }
|
||||
);
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(res.body.error).toContain("in-review");
|
||||
expect(res.body.error).toContain("Move task to 'todo' or 'in-progress' first");
|
||||
expect(store.moveTask).not.toHaveBeenCalled();
|
||||
expect(res.status).toBe(200);
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "triage");
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-replan" });
|
||||
});
|
||||
|
||||
it("allows spec revision when task is in done (done can transition to triage)", async () => {
|
||||
@@ -8059,15 +8061,18 @@ describe("POST /tasks/:id/spec/rebuild", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("returns 400 when task is in in-review (cannot transition to triage)", async () => {
|
||||
it("allows spec rebuild when task is in in-review (in-review can transition to triage)", async () => {
|
||||
const inReviewTask = { ...FAKE_TASK_DETAIL, column: "in-review" as const };
|
||||
const movedTask = { ...FAKE_TASK_DETAIL, column: "triage" as const };
|
||||
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(inReviewTask);
|
||||
(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(400);
|
||||
expect(res.body.error).toContain("in-review");
|
||||
expect(store.moveTask).not.toHaveBeenCalled();
|
||||
expect(res.status).toBe(200);
|
||||
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "triage");
|
||||
expect(store.updateTask).toHaveBeenCalledWith("FN-001", { status: "needs-replan" });
|
||||
});
|
||||
|
||||
it("returns 404 when task not found", async () => {
|
||||
|
||||
Reference in New Issue
Block a user