From 36d090c326c37f8156bd179dbf27f75bc0a5fbc5 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Mon, 29 Jun 2026 02:47:17 -0700 Subject: [PATCH] fix(FN-7225): keep plan review in triage --- .changeset/fn-plan-review-triage-gate.md | 7 + .../src/__tests__/builtin-workflows.test.ts | 8 +- .../core/src/__tests__/store-create.test.ts | 1 + packages/core/src/agent-prompts.ts | 2 +- packages/core/src/store.ts | 10 +- .../app/components/InlineCreateCard.tsx | 8 +- .../dashboard/app/components/TaskCard.css | 29 ++++ .../dashboard/app/components/TaskCard.tsx | 20 ++- .../__tests__/InlineCreateCard.test.tsx | 39 ++++- .../components/__tests__/TaskCard.test.tsx | 23 +++ packages/engine/src/__tests__/triage.test.ts | 109 +++++++++++++ .../workflow-graph-optional-group.test.ts | 60 ++++++- packages/engine/src/triage.ts | 152 +++++++++++++++++- .../engine/src/workflow-graph-executor.ts | 27 +++- 14 files changed, 476 insertions(+), 19 deletions(-) create mode 100644 .changeset/fn-plan-review-triage-gate.md diff --git a/.changeset/fn-plan-review-triage-gate.md b/.changeset/fn-plan-review-triage-gate.md new file mode 100644 index 0000000000..5856f3b59b --- /dev/null +++ b/.changeset/fn-plan-review-triage-gate.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Keep Plan Review in triage and prevent duplicate execution-time plan reviews. +category: fix +dev: Triage now runs enabled Plan Review before releasing tasks to execution; execution graph skips an already-passed Plan Review. diff --git a/packages/core/src/__tests__/builtin-workflows.test.ts b/packages/core/src/__tests__/builtin-workflows.test.ts index 0996b0bc29..c4a555e02c 100644 --- a/packages/core/src/__tests__/builtin-workflows.test.ts +++ b/packages/core/src/__tests__/builtin-workflows.test.ts @@ -922,7 +922,13 @@ describe("built-in workflows", () => { enabledWorkflowSteps: [], }); - expect((await store.getTask(task.id)).enabledWorkflowSteps ?? []).toEqual([]); + /* + FNXC:WorkflowOptionalSteps 2026-06-29-02:55: + An explicit empty optional-step selection must hydrate back as `[]`, not + `undefined`; otherwise later workflow execution can confuse "all disabled" + with "not materialized" and re-run default-on Plan Review / Code Review. + */ + expect((await store.getTask(task.id)).enabledWorkflowSteps).toEqual([]); expect(store.getTaskWorkflowSelection(task.id)).toEqual({ workflowId: "builtin:coding", stepIds: [], diff --git a/packages/core/src/__tests__/store-create.test.ts b/packages/core/src/__tests__/store-create.test.ts index 70befaf386..5fd09f35d1 100644 --- a/packages/core/src/__tests__/store-create.test.ts +++ b/packages/core/src/__tests__/store-create.test.ts @@ -963,6 +963,7 @@ describe("TaskStore", () => { const result = await store.applyReplicatedTaskCreate(payload); expect(result.applied).toBe(true); expect(result.task.enabledWorkflowSteps).toBeUndefined(); + expect((await store.getTask(payload.taskId)).enabledWorkflowSteps).toEqual([]); }); it("applyReplicatedTaskCreate is idempotent and detects collisions", async () => { diff --git a/packages/core/src/agent-prompts.ts b/packages/core/src/agent-prompts.ts index 556037208c..5fb2fb407e 100644 --- a/packages/core/src/agent-prompts.ts +++ b/packages/core/src/agent-prompts.ts @@ -555,7 +555,7 @@ Standard triage must not infer workflow changes from task type. Agents preserve ## Plan Review -Workflow Plan Review is the single optional plan quality gate before execution. Your job in triage is to write a complete PROMPT.md; do not call \`fn_review_spec()\` or any other review tool. If Plan Review is enabled for the task, the workflow graph runs it after triage and before execution. +Workflow Plan Review is the single optional plan quality gate before execution. Your job in triage is to write a complete PROMPT.md; do not call \`fn_review_spec()\` or any other review tool. If Plan Review is enabled for the task, the triage engine runs it before releasing the task to execution, and the task stays in triage while that review runs. ## PROMPT.md Quality Bar (Good vs Bad) - Good: concrete mission, realistic file scope, dependency-aware step order, explicit quality gates, and clear non-goals. diff --git a/packages/core/src/store.ts b/packages/core/src/store.ts index fa473b78fa..6961507c59 100644 --- a/packages/core/src/store.ts +++ b/packages/core/src/store.ts @@ -2200,7 +2200,11 @@ export class TaskStore extends EventEmitter { })(), breakIntoSubtasks: row.breakIntoSubtasks ? true : undefined, noCommitsExpected: row.noCommitsExpected ? true : undefined, - enabledWorkflowSteps: (() => { const e = fromJson(row.enabledWorkflowSteps); return e && e.length > 0 ? e : undefined; })(), + /* + FNXC:WorkflowOptionalSteps 2026-06-29-02:55: + Preserve an explicitly empty optional-step selection as `[]`. Quick Add, inline create, and task details use `[]` to mean "the operator disabled every optional workflow group"; converting it back to `undefined` lets later workflow hydration re-seed default-on Plan Review / Code Review and run gates the task opted out of. + */ + enabledWorkflowSteps: (() => { const e = fromJson(row.enabledWorkflowSteps); return Array.isArray(e) ? e : undefined; })(), modifiedFiles: (() => { const m = fromJson(row.modifiedFiles); return m && m.length > 0 ? m : undefined; })(), missionId: row.missionId || undefined, sliceId: row.sliceId || undefined, @@ -4612,7 +4616,7 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS} } } } else if (input.enabledWorkflowSteps.length === 0) { - resolvedWorkflowSteps = undefined; + resolvedWorkflowSteps = []; } // U7c: selection seeds are optional-group node ids (not materialized @@ -4807,7 +4811,7 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS} } } } else if (Array.isArray(input.enabledWorkflowSteps) && input.enabledWorkflowSteps.length === 0) { - resolvedWorkflowSteps = undefined; + resolvedWorkflowSteps = []; } // U7c: selection seeds are optional-group node ids (not materialized diff --git a/packages/dashboard/app/components/InlineCreateCard.tsx b/packages/dashboard/app/components/InlineCreateCard.tsx index cffe986978..4feec3e9cb 100644 --- a/packages/dashboard/app/components/InlineCreateCard.tsx +++ b/packages/dashboard/app/components/InlineCreateCard.tsx @@ -477,7 +477,11 @@ export function InlineCreateCard({ validatorModelId: hasValidatorOverride ? validatorModelId : undefined, planningModelProvider: hasPlanningOverride ? planningProvider : undefined, planningModelId: hasPlanningOverride ? planningModelId : undefined, - enabledWorkflowSteps: enabledOptionalStepIds.length ? enabledOptionalStepIds : undefined, + /* + FNXC:InlineCreateWorkflowSteps 2026-06-29-02:45: + Inline create optional-step toggles are explicit task intent. When a workflow exposes optional steps and the operator unchecks all of them, submit `[]` so default-on Plan Review / Code Review stay disabled on the created task instead of reappearing from workflow defaults. + */ + enabledWorkflowSteps: optionalSteps.length > 0 ? enabledOptionalStepIds : undefined, priority, nodeId, }; @@ -494,7 +498,7 @@ export function InlineCreateCard({ } await submitTask(input); - }, [description, submitting, dependencies, selectedAgentId, selectedPresetId, hasExecutorOverride, executorProvider, executorModelId, hasValidatorOverride, validatorProvider, validatorModelId, hasPlanningOverride, planningProvider, planningModelId, enabledOptionalStepIds, priority, nodeId, projectId, addToast, submitTask]); + }, [description, submitting, dependencies, selectedAgentId, selectedPresetId, hasExecutorOverride, executorProvider, executorModelId, hasValidatorOverride, validatorProvider, validatorModelId, hasPlanningOverride, planningProvider, planningModelId, optionalSteps.length, enabledOptionalStepIds, priority, nodeId, projectId, addToast, submitTask]); const handleDuplicateProceed = useCallback(async () => { const matches = duplicateMatches; diff --git a/packages/dashboard/app/components/TaskCard.css b/packages/dashboard/app/components/TaskCard.css index 2ba6d4a305..429aedac7c 100644 --- a/packages/dashboard/app/components/TaskCard.css +++ b/packages/dashboard/app/components/TaskCard.css @@ -978,6 +978,18 @@ The execution-time badge is part of the footer's bottom-right chip cluster, so i flex-shrink: 0; } +.card-progress-active { + flex-shrink: 0; + padding: 1px var(--space-xs); + border: 1px solid color-mix(in srgb, var(--in-progress) 45%, transparent); + border-radius: var(--radius-pill); + background: color-mix(in srgb, var(--in-progress) 12%, transparent); + color: var(--in-progress); + font-size: 0.625rem; + font-weight: 650; + line-height: 1.2; +} + /* Steps toggle and list */ .card-steps-toggle { display: flex; @@ -1082,6 +1094,23 @@ executing. These map 1:1 to the unified progress status so the dot color encodes opacity: 0.6; } +.card-step-name.active { + color: var(--text); + font-weight: 650; +} + +.card-step-active-badge { + flex-shrink: 0; + padding: 1px var(--space-xs); + border: 1px solid color-mix(in srgb, var(--in-progress) 40%, transparent); + border-radius: var(--radius-pill); + color: var(--in-progress); + font-size: 0.5625rem; + font-weight: 650; + line-height: 1.2; + text-transform: uppercase; +} + .card-step-workflow-badge { margin-left: auto; padding: 0 var(--space-xs); diff --git a/packages/dashboard/app/components/TaskCard.tsx b/packages/dashboard/app/components/TaskCard.tsx index be82df9d2b..b9e081cfe7 100644 --- a/packages/dashboard/app/components/TaskCard.tsx +++ b/packages/dashboard/app/components/TaskCard.tsx @@ -1074,6 +1074,14 @@ function TaskCardComponent({ () => getUnifiedTaskProgress(task), [task.steps, task.enabledWorkflowSteps, task.workflowStepResults], ); + /* + FNXC:TaskCardProgress 2026-06-29-02:26: + Operators need to see active step work on the card before it becomes `done`. Keep the completed count strict, but surface `in-progress` task steps and running workflow checks as an active badge so card progress does not look stale while execution is underway. + */ + const activeProgressCount = useMemo( + () => unifiedProgress.items.filter((item) => item.status === "in-progress" || item.status === "running").length, + [unifiedProgress.items], + ); const showProgressSection = unifiedProgress.total > 0 && (task.status === "executing" || task.column === "in-progress"); @@ -2281,6 +2289,11 @@ function TaskCardComponent({ /> {unifiedProgress.completed}/{unifiedProgress.total} + {activeProgressCount > 0 && ( + + {t("tasks.activeStepCount", "{{count}} active", { count: activeProgressCount })} + + )}