From 6ce0b440588de6214ddb1d9d5fa1c862ed05ca7b Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 28 Jun 2026 23:24:29 -0700 Subject: [PATCH] feat(workflows): make coding stepwise with final review --- .changeset/stepwise-default-coding.md | 7 + docs/getting-started.md | 2 +- ...eat-stepwise-final-review-workflow-plan.md | 328 ++++++++++++++++++ docs/workflow-editor.md | 5 +- docs/workflow-steps.md | 17 +- .../src/__tests__/builtin-workflows.test.ts | 149 +++++++- .../__tests__/workflow-optional-steps.test.ts | 3 + ...tepwise-final-review-coding-workflow-ir.ts | 43 +++ packages/core/src/builtin-workflows.ts | 33 +- packages/core/src/index.ts | 1 + packages/core/src/store.ts | 43 ++- .../__tests__/WorkflowNodeEditor.test.tsx | 2 +- .../task-create-workflow-route.test.ts | 98 +++++- 13 files changed, 681 insertions(+), 50 deletions(-) create mode 100644 .changeset/stepwise-default-coding.md create mode 100644 docs/plans/2026-06-29-001-feat-stepwise-final-review-workflow-plan.md create mode 100644 packages/core/src/builtin-stepwise-final-review-coding-workflow-ir.ts diff --git a/.changeset/stepwise-default-coding.md b/.changeset/stepwise-default-coding.md new file mode 100644 index 0000000000..c9c9ff7edf --- /dev/null +++ b/.changeset/stepwise-default-coding.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Make Coding use stepwise execution with final review and keep Legacy coding available. +category: feature +dev: `builtin:coding` now uses the stepwise final-review graph; the old monolithic graph is `builtin:legacy-coding`. diff --git a/docs/getting-started.md b/docs/getting-started.md index deafc3eca7..86277902ff 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -130,7 +130,7 @@ You can also use expanded board controls (Refine, Deps, Attachments, model overr Most tasks can use the default **Coding** workflow. When the workflow selector is visible on a task or board creation surface, choose a different workflow if the work needs a shorter path, extra review, stepwise execution, Compound Engineering skills, or a custom policy your project authored. -Built-ins include task-selectable Coding, Quick fix, Review-heavy, plugin-gated Compound engineering, Stepwise coding, and Design workflows, plus PR lifecycle fragments for workflow authors. For the full catalog and runtime behavior, see [Workflow Steps](./workflow-steps.md#workflow-overview). To inspect built-ins or author custom workflows, open the dashboard [Workflow Editor](./workflow-editor.md). +Built-ins include task-selectable Coding, Legacy coding, Quick fix, Review-heavy, plugin-gated Compound engineering, Coding (per-step review), and Design workflows, plus PR lifecycle fragments for workflow authors. For the full catalog and runtime behavior, see [Workflow Steps](./workflow-steps.md#workflow-overview). To inspect built-ins or author custom workflows, open the dashboard [Workflow Editor](./workflow-editor.md). ## Understand the Task Lifecycle diff --git a/docs/plans/2026-06-29-001-feat-stepwise-final-review-workflow-plan.md b/docs/plans/2026-06-29-001-feat-stepwise-final-review-workflow-plan.md new file mode 100644 index 0000000000..bc660e91f6 --- /dev/null +++ b/docs/plans/2026-06-29-001-feat-stepwise-final-review-workflow-plan.md @@ -0,0 +1,328 @@ +--- +title: "feat: Add final-review stepwise workflow and fix workflow selection" +date: 2026-06-29 +type: feature +depth: standard +artifact_contract: ce-unified-plan/v1 +artifact_readiness: implementation-ready +product_contract_source: ce-plan-bootstrap +execution: code +--- + +# feat: Add final-review stepwise workflow and fix workflow selection + +## Goal Capsule + +| Field | Value | +|---|---| +| Objective | Make default Coding use stepwise execution with one final review, keep the original monolithic graph as Legacy coding, rename old Stepwise coding to Coding (per-step review), and fix workflow selection so new tasks actually attach to selected built-ins such as Coding (per-step review) and Compound engineering. | +| Authority | User requests in this session: "create a built in stepwise coding version that doesn't review each step but just does a review of everything at the end"; "creating a new task in stepwise coding don't attach it to the workflow. it goes to the default coding built in workflow"; "and the compound engineering workflow doesn't work." | +| Execution profile | Standard code change across `@fusion/core`, dashboard task creation surfaces/routes, docs, and focused tests. | +| Stop conditions | Do not change existing `builtin:stepwise-coding` per-step review semantics; do not mask workflow selection bugs by only changing display labels; do not alter runtime primitives unless validation proves a missing generic capability. | +| Tail ownership | Implementation should add focused core tests and run file-scoped verification, not the full suite. | + +--- + +## Product Contract + +### Summary + +Fusion's default Coding workflow should use graph-owned step execution without the overhead of per-step AI review. The workflow should preserve the stepwise execution model (`PROMPT.md` parsing plus one-step-at-a-time execution) and then rely on the existing whole-task review path after all implementation steps complete. The original monolithic coding graph should remain available as Legacy coding, and the old per-step-review Stepwise workflow should display as Coding (per-step review). + +Workflow selection must also be reliable. When an operator creates a task from a workflow lane or picker, the selected workflow must persist onto the task's `task_workflow_selection` row and govern execution. The reported failures are that creating a new task in Stepwise coding falls back to `builtin:coding`, and the Compound engineering workflow does not work. Those are selection/materialization bugs, not just labeling issues. + +### Problem Frame + +`builtin:stepwise-coding` currently demonstrates step inversion by parsing `PROMPT.md`, iterating through `Task.steps[]`, running each step, and routing each step through `step-review` with approve/revise/rethink outcomes. That is useful when every step needs independent review, but it is heavier than needed for workflows that only want a final whole-task review. + +The default `builtin:coding` already performs a whole-task execution seam followed by optional pre-merge gates and final review, but it does not expose per-step execution as authored workflow graph structure. The requested variant fills the middle: graph-owned step execution, no per-step review, final review after the full implementation. + +During initial investigation, the store-level explicit/default workflow paths already have tests for default `builtin:stepwise-coding` selection seeding. That makes the likely failure surface the dashboard create-task path: the UI may display a selected lane while submitting `workflowId` as `undefined`, or it may submit optional-step state in a way that suppresses `input.workflowId` in `TaskStore.createTask`. Compound engineering adds another dimension because it is plugin-gated (`fusion-plugin-compound-engineering`) and can fail either at visibility/selection time or at runtime if the required bundled plugin/skills are unavailable. + +### Requirements + +- R1. `builtin:coding` uses the Stepwise-derived final-review graph and remains the default coding workflow/fallback. +- R2. The new workflow preserves stepwise planning and execution: planning produces `PROMPT.md`, a `parse-steps` node parses it, and a `foreach(source:"task-steps")` region runs one `step-execute` node per planned step. +- R3. The new workflow must not include a `step-review` node in the foreach template and must not include per-step revise/rethink routing. +- R4. After all planned steps complete, the workflow runs the same pre-merge optional groups as the coding built-ins: `browser-verification` default off and `code-review` default on. +- R5. After optional pre-merge groups, the workflow runs the existing final `review` seam and the standard merge-gate / branch-group / merge-attempt region. +- R6. The existing `builtin:stepwise-coding` graph remains unchanged for users who rely on per-step review and rework, but its user-facing name becomes Coding (per-step review). +- R7. The original monolithic coding graph remains selectable as `builtin:legacy-coding` with user-facing name Legacy coding. +- R8. Creating a task while Coding (per-step review) is selected must persist `workflowId: "builtin:stepwise-coding"` and not silently resolve the task to `builtin:coding`. +- R9. Creating a task while Compound engineering is selected must either persist `workflowId: "builtin:compound-engineering"` and execute that workflow, or clearly block selection/create with a plugin-gating explanation when the required plugin is unavailable. It must not silently fall back to `builtin:coding`. +- R10. Task creation surfaces must preserve the distinction between `workflowId: undefined` (inherit project default), `workflowId: null` (explicit no workflow), and `workflowId: string` (explicit selected workflow). +- R11. Explicit enabled optional steps must not accidentally suppress an explicit workflow selection unless the caller intentionally opts into trusted low-level behavior. User-facing create flows must be able to submit both the selected workflow and enabled optional-group IDs. +- R12. Board workflow lane task creation, global New Task modal creation, list view creation, quick-entry creation, planning/subtask/mission task creation, and agent/tool-created tasks must be enumerated and tested according to the workflow selection contract. + +### Scope Boundaries + +- In scope: a Stepwise-derived final-review IR module, built-in registry/export updates, docs catalog updates, and tests that prove default Coding has no per-step review node while final review remains. +- In scope: diagnose and fix create-time workflow selection loss for Stepwise coding and Compound engineering across dashboard/API/store boundaries. +- In scope: add regression coverage proving selected workflow IDs are persisted, returned, and used to resolve the task workflow IR. +- Out of scope: runtime changes to `WorkflowGraphExecutor`, per-step parallelization, new reviewer verdict semantics, dashboard redesign, or changes to `builtin:coding` / `builtin:stepwise-coding` behavior. +- Out of scope: removing the default-on pre-merge `code-review` optional group. The request removes per-step review only; the whole-task pre-merge code review remains the "review of everything at the end" gate alongside the final `review` seam. +- Out of scope: redesigning plugin installation. This plan may add a clearer gating/error path for Compound engineering, but it should not rebuild the plugin manager. + +### Acceptance Examples + +- AE1. Given a task selects `builtin:coding`, when workflow selection resolves, then the built-in registry returns a v2 IR with `parse-steps`, `foreach`, `browser-verification`, `code-review`, `review`, and merge nodes. +- AE2. Given the `builtin:coding` IR, when its foreach template is inspected, then it contains `step-execute` and a pass-through exit node, but no `step-review`. +- AE3. Given optional-step defaults are resolved for `builtin:coding`, then `code-review` is seeded by default and `browser-verification` is not. +- AE4. Given docs list built-in workflows, then the new workflow is documented separately from `builtin:stepwise-coding`. +- AE5. Given the board/list workflow selector is set to Stepwise coding, when a new task is created from that workflow context, then `store.getTaskWorkflowSelection(task.id)?.workflowId` is `builtin:stepwise-coding`. +- AE6. Given the board/list workflow selector is set to Compound engineering and the required plugin is available/enabled, when a new task is created from that workflow context, then `store.getTaskWorkflowSelection(task.id)?.workflowId` is `builtin:compound-engineering`. +- AE7. Given Compound engineering is unavailable because `fusion-plugin-compound-engineering` is not installed/enabled, when a user tries to create/select a Compound engineering task, then the UI/API reports the plugin requirement instead of creating a default coding task. +- AE8. Given a user toggles optional steps while selecting a non-default workflow, when the task is created, then the selected workflow persists and the enabled optional-group IDs reflect the user's toggles. + +### Symptom Verification + +- **Original symptom 1:** Creating a new task from/with Stepwise coding selected attaches the task to default `builtin:coding` instead of `builtin:stepwise-coding`. +- **Exact reproduction 1:** In a project with built-in workflows enabled, select Stepwise coding in a task creation surface, create a task, then inspect `GET /api/tasks/:id/workflow` or `TaskStore.getTaskWorkflowSelection(task.id)`. +- **Assertion it is gone 1:** The created task's workflow selection row has `workflowId: "builtin:stepwise-coding"` and `enabledWorkflowSteps` is seeded according to that workflow (`["code-review"]` by default). +- **Original symptom 2:** Compound engineering workflow "doesn't work". +- **Exact reproduction 2:** Select/create a task with `workflowId: "builtin:compound-engineering"` through the same surfaces and inspect both selection persistence and the first workflow resolution/execution failure. +- **Assertion it is gone 2:** With the CE plugin available/enabled, the task persists `workflowId: "builtin:compound-engineering"` and resolves to the CE IR; without the plugin, creation/selection fails visibly with the required plugin ID rather than silently falling back. + +### Surface Enumeration + +- **Create surfaces:** New Task modal (`packages/dashboard/app/components/NewTaskModal.tsx`), TaskForm workflow picker (`packages/dashboard/app/components/TaskForm.tsx`), board/list workflow lane quick create (`packages/dashboard/app/components/Column.tsx`, `packages/dashboard/app/components/QuickEntryBox.tsx`, `packages/dashboard/app/components/ListView.tsx`), mission triage (`packages/dashboard/app/components/MissionManager.tsx` and `packages/dashboard/src/mission-routes.ts`), planning/subtask routes (`packages/dashboard/src/routes/register-planning-subtask-routes.ts`), and agent/API creation (`packages/dashboard/src/routes/register-task-workflow-routes.ts`, `packages/engine/src/agent-tools.ts`). +- **Workflow IDs:** `builtin:coding`, `builtin:stepwise-coding`, new final-review stepwise workflow, `builtin:compound-engineering`, custom workflow IDs, `null` no-workflow, and `undefined` inherit-default. +- **Plugin states:** Compound engineering plugin installed/enabled, installed/disabled, unavailable, and bundled path resolution failure. +- **Optional-step states:** `enabledWorkflowSteps` omitted, empty array, default-on only, custom toggled values, and explicit values combined with non-default workflow ID. +- **Board states:** task starts in default `triage`/workflow intake column, workflow-specific custom columns, and workflow lane selected independently from project default. +- **Breakpoints/surfaces:** desktop and mobile New Task / inline-create affordances, since workflow selection must not be display-only on one viewport. + +--- + +## Planning Contract + +### Key Technical Decisions + +- KTD-1. Make `builtin:coding` the Stepwise-derived final-review workflow and preserve the old graphs under explicit names. + The default Coding ID is the resolver fallback, so mapping it to the Stepwise-derived final-review graph makes new ordinary coding tasks use the requested workflow. The original monolithic graph remains available as `builtin:legacy-coding`, while `builtin:stepwise-coding` keeps its graph behavior and displays as Coding (per-step review). + +- KTD-2. Model step completion as `step-execute -> step-done` inside the foreach template. + The existing stepwise workflow uses `step-review` as the authority that marks a step done. Without per-step review, the foreach template should let `step-execute` complete the step through the existing step-execution primitive. The template still needs a single exit, so use a config-less `gate` node as the pass-through sink, mirroring the existing `step-done` pattern. + +- KTD-3. Keep whole-task review surfaces unchanged. + The new workflow should keep `browser-verification`, `code-review`, final `review`, and the merge region after the foreach. This satisfies the requested "review of everything at the end" without introducing a new review mechanism. + +- KTD-4. Register as a normal selectable built-in. + Add the new workflow to `BUILTIN_WORKFLOWS` with `kind: "workflow"` and let `defaultEnabledBuiltinWorkflowIds()` include it by default, matching non-plugin-gated selectable built-ins. + +- KTD-5. Focus verification on IR shape and registry behavior. + This is primarily a built-in graph definition change. Targeted tests should validate parse/round-trip, registry presence, non-compilable built-in classification, optional-group defaults, docs-adjacent catalog expectations, and the exact absence of `step-review`. + +- KTD-6. Fix workflow selection at the boundary where intent is lost. + Store-level explicit workflow creation already records `task_workflow_selection` for built-ins, including Stepwise coding, when `workflowId` reaches `TaskStore.createTask` and `enabledWorkflowSteps` is omitted. The implementation must prove whether the lost value happens in the UI submit payload, API normalization, duplicate-reconcile response, or store precedence rule. The fix should be at that boundary, not by changing resolver fallback behavior. + +- KTD-7. User-facing create flows must support workflow ID plus optional-step toggles together. + Current store logic intentionally treats explicit `enabledWorkflowSteps` as a trusted override that can suppress `input.workflowId`. That is dangerous for UI create flows because the workflow picker and optional-step toggles are independent controls. Either the UI/API must omit `enabledWorkflowSteps` unless the user explicitly changed them, or the store/API must preserve `workflowId` while applying explicit optional IDs for that selected workflow. The chosen fix must keep low-level backward compatibility explicit and tested. + +- KTD-8. Compound engineering must fail closed on missing plugin requirements. + `builtin:compound-engineering` is plugin-gated by `fusion-plugin-compound-engineering`. A missing plugin should prevent selection/execution with a visible requirement, not degrade into default coding. Tests should cover both the persistence path and plugin-gated availability semantics. + +### High-Level Technical Design + +```mermaid +flowchart TB + Start[start] --> Plan[plan prompt] + Plan --> Parse[parse-steps: PROMPT.md] + Parse --> Steps[foreach task-steps] + Steps --> StepExecute[step-execute] + StepExecute --> StepDone[step-done gate] + Steps --> Browser[browser-verification optional-group] + Browser --> CodeReview[code-review optional-group] + CodeReview --> FinalReview[final review seam] + FinalReview --> MergeGate[merge-gate and merge region] + MergeGate --> End[end] +``` + +### Existing Patterns To Follow + +- `packages/core/src/builtin-stepwise-coding-workflow-ir.ts` for columns, artifacts, parse/foreach structure, optional groups, and merge region. +- `packages/core/src/builtin-coding-workflow-ir.ts` for the standard post-execute optional-group and review/merge suffix. +- `packages/core/src/builtin-workflows.ts` for built-in registry metadata, layouts, and stable timestamp conventions. +- `packages/core/src/__tests__/builtin-workflows.test.ts` for registry and selectable built-in assertions. +- `packages/core/src/__tests__/workflow-optional-steps.test.ts` and `packages/core/src/__tests__/builtin-code-review-group.test.ts` for optional-group default behavior. +- `docs/workflow-steps.md` and `docs/workflow-editor.md` for built-in catalog descriptions. + +### Assumptions + +- The existing `step-execute` primitive marks the active step done on success when no `step-review` node is present. This is documented in `packages/engine/src/step-runner.ts` and should be confirmed with a focused graph test if implementation exposes uncertainty. +- The Stepwise-derived final-review graph is implemented as a reusable IR module but registered under `builtin:coding`; the original monolithic graph is registered as `builtin:legacy-coding`. +- Pre-merge `code-review` remains the default-on final whole-task code review gate. The final `review` seam remains the lifecycle review before merge. + +### Sequencing + +1. Diagnose workflow selection loss with a failing test that reproduces Stepwise task creation through the affected UI/API path. +2. Fix the selection boundary and add Compound engineering plugin-gating regression coverage. +3. Add and export the new built-in IR. +4. Register it in `BUILTIN_WORKFLOWS` with layout and description. +5. Extend tests for registry, optional defaults, and IR shape. +6. Add the new workflow to the now-fixed create/selection matrix. +7. Update docs catalog and built-in workflow descriptions. +8. Add the changeset and run targeted verification. + +--- + +## Implementation Units + +### U1. Add the stepwise-final-review default Coding IR + +- **Goal:** Create a Stepwise-derived workflow IR for `builtin:coding` that mirrors `builtin:stepwise-coding` except the foreach template omits `step-review` and routes `step-execute` success directly to a pass-through `step-done` exit. +- **Requirements:** R1, R2, R3, R4, R5, R6 +- **Files:** + - Create `packages/core/src/builtin-stepwise-final-review-coding-workflow-ir.ts` + - Modify `packages/core/src/index.ts` +- **Approach:** Copy the stable lifecycle skeleton from `packages/core/src/builtin-stepwise-coding-workflow-ir.ts`: same columns, `PROMPT.md` artifact declaration, planning/parse/foreach, optional groups, final review, merge region, and settings. Inside the foreach template, include only `step-execute` and `step-done` with a success edge. Remove per-step `step-review`, `outcome:revise`, `outcome:rethink`, and rework-hold routing unless validation requires a generic failure path. +- **FNXC comment requirement:** Add or update a concise FNXC comment in the new IR file explaining that this built-in exists because operators need graph-owned step execution with one whole-task review at the end rather than per-step review. +- **Test Scenarios:** + - The IR parses and round-trips. + - The top-level graph contains `parse-steps`, `foreach`, `browser-verification`, `code-review`, `review`, and merge nodes. + - The foreach template contains `step-execute` and `step-done`. + - The foreach template contains no `step-review`. + - No template edge carries `kind: "rework"`. +- **Verification:** Add assertions in `packages/core/src/__tests__/builtin-coding-workflow-ir.test.ts` or a new focused `packages/core/src/__tests__/builtin-stepwise-final-review-coding-workflow-ir.test.ts`. + +### U2. Register and expose the new built-in workflow + +- **Goal:** Make the workflow discoverable and selectable through the built-in workflow registry and package exports. +- **Requirements:** R1, R6, R7 +- **Files:** + - Modify `packages/core/src/builtin-workflows.ts` + - Modify `packages/core/src/index.ts` + - Possibly modify `packages/core/src/types.ts` if the enabled built-in IDs type or docs list is closed over explicit IDs. +- **Approach:** Import the new IR, add a `BUILTIN_WORKFLOWS` entry near `builtin:stepwise-coding`, and provide a layout that includes `plan`, `parse`, `steps`, `browser-verification`, `code-review`, `review`, and merge/end nodes. Add the workflow ID to any tests or type-level inventories that enumerate selectable built-ins. +- **Test Scenarios:** + - `getBuiltinWorkflow("builtin:stepwise-final-review-coding")` returns the new workflow. + - `defaultEnabledBuiltinWorkflowIds()` includes the new ID because it is selectable and not plugin-gated. + - `NON_COMPILABLE_BUILTIN_IDS` includes the new ID in compiler tests because the graph uses interpreter-only node kinds. + - Built-in registry tests distinguish existing `builtin:stepwise-coding` as per-step-review and the new workflow as final-review-only. +- **Verification:** Extend `packages/core/src/__tests__/builtin-workflows.test.ts`. + +### U3. Preserve optional-gate behavior for the new workflow + +- **Goal:** Ensure the new workflow has the same end-of-task optional gates as existing coding built-ins. +- **Requirements:** R4, R5, R7, AE3 +- **Files:** + - Modify `packages/core/src/__tests__/workflow-optional-steps.test.ts` + - Modify `packages/core/src/__tests__/builtin-code-review-group.test.ts` + - Modify `packages/core/src/__tests__/builtin-coding-workflow-ir.test.ts` if it asserts "both coding built-ins" +- **Approach:** Expand test matrices that currently cover `BUILTIN_CODING_WORKFLOW_IR` and `BUILTIN_STEPWISE_CODING_WORKFLOW_IR` to include the new IR where the assertion is about shared coding optional gates. Keep per-step-review-specific assertions targeted only at `BUILTIN_STEPWISE_CODING_WORKFLOW_IR`. +- **Test Scenarios:** + - `resolveWorkflowOptionalSteps(newIr)` returns browser verification default off and code review default on. + - `resolveDefaultOnOptionalGroupIds(newIr)` returns `["code-review"]`. + - The new workflow routes `browser-verification -> code-review -> review`. + - Code review failure routes to `end` as in the other coding built-ins. +- **Verification:** Run focused core tests listed in the Verification Contract. + +### U4. Update operator-facing docs + +- **Goal:** Document the new built-in so users understand when to choose it instead of `builtin:coding` or `builtin:stepwise-coding`. +- **Requirements:** R7, AE4 +- **Files:** + - Modify `docs/workflow-steps.md` + - Modify `docs/workflow-editor.md` + - Modify `docs/getting-started.md` +- **Approach:** Add a catalog row and short runtime note. Phrase the difference plainly: `Stepwise coding` reviews each step; the new workflow executes steps one by one and reviews the full result at the end. +- **Test Scenarios:** + - Existing docs inventory tests, if any, still pass. + - Docs mention the new workflow ID exactly once in the built-in catalog and use consistent naming elsewhere. +- **Verification:** Run any focused docs/lazy inventory test only if affected by the docs change; otherwise rely on core tests plus markdown review. + +### U5. Add a changeset + +- **Goal:** Record the new published CLI/operator-visible built-in workflow for `@runfusion/fusion`. +- **Requirements:** R1, R7 +- **Files:** + - Create `.changeset/.md` +- **Approach:** This affects published `@runfusion/fusion`, so add a minor changeset with the required labeled body format. +- **Test Scenarios:** + - Changeset body passes `pnpm check:changesets` format expectations. +- **Verification:** Include the changeset in review; run `pnpm check:changesets` if available and fast enough for the focused pass. + +### U6. Reproduce and fix Stepwise coding create-task selection loss + +- **Goal:** Ensure a task created while Stepwise coding is selected persists `builtin:stepwise-coding` instead of falling back to `builtin:coding`. +- **Requirements:** R8, R10, R11, R12, AE5, AE8 +- **Files:** + - Modify `packages/dashboard/app/components/TaskForm.tsx` + - Modify `packages/dashboard/app/components/NewTaskModal.tsx` + - Modify `packages/dashboard/app/components/Board.tsx` + - Modify `packages/dashboard/app/components/Column.tsx` + - Modify `packages/dashboard/app/components/ListView.tsx` + - Modify `packages/dashboard/app/components/QuickEntryBox.tsx` + - Modify `packages/dashboard/src/routes/register-task-workflow-routes.ts` if payload normalization is the loss boundary + - Modify `packages/core/src/store.ts` only if the root cause is the `workflowId` + `enabledWorkflowSteps` precedence rule + - Add/update tests in `packages/dashboard/app/components/__tests__/TaskForm.test.tsx`, `packages/dashboard/app/components/__tests__/NewTaskModal.test.tsx`, `packages/dashboard/app/components/__tests__/ListView.test.tsx`, `packages/dashboard/app/components/__tests__/QuickEntryBox.test.tsx`, `packages/dashboard/app/components/__tests__/board-quickcreate-workflow-lane-visibility.test.tsx`, `packages/dashboard/app/__tests__/api-tasks.test.ts`, `packages/dashboard/src/routes/__tests__/task-create-workflow-route.test.ts`, and/or `packages/core/src/__tests__/builtin-workflows.test.ts` depending on the confirmed loss boundary +- **Approach:** Start with a failing test that simulates selecting `builtin:stepwise-coding` in the same create surface the user used. Assert the request payload includes `workflowId: "builtin:stepwise-coding"` and the created task selection resolves to that workflow. Trace whether `selectedWorkflowId` is `undefined` despite the visible workflow label, whether `enabledWorkflowSteps` is always sent and suppresses `workflowId`, or whether the API response drops the workflow metadata and causes a client-side reclassification. +- **FNXC comment requirement:** Add/update FNXC comments at the fixed boundary explaining that visible workflow lane selection is user intent and must persist as `workflowId`, while optional-group toggles are a separate control. +- **Test Scenarios:** + - New Task modal selecting Stepwise coding submits `workflowId: "builtin:stepwise-coding"`. + - Board/list workflow lane create submits the lane workflow ID, not the project default. + - Optional-step toggles do not erase the selected workflow. + - `workflowId: undefined`, `null`, and string each preserve their distinct meanings. +- **Verification:** Run focused dashboard/core tests for the changed create surface and store selection behavior. + +### U7. Reproduce and fix Compound engineering workflow create/selection failure + +- **Goal:** Make Compound engineering workflow selection either work end-to-end when the required plugin is available or fail visibly when unavailable. +- **Requirements:** R9, R10, R12, AE6, AE7 +- **Files:** + - Modify `packages/core/src/builtin-workflows.ts` only if plugin-gated metadata is incomplete + - Modify `packages/dashboard/src/routes/register-workflow-routes.ts` if task workflow selection should reject unavailable plugin-gated built-ins + - Modify `packages/dashboard/src/routes/register-task-workflow-routes.ts` if task creation accepts unavailable plugin-gated workflows silently + - Modify dashboard picker components if disabled/gated built-ins are shown as selectable without explanation + - Add/update tests in `packages/core/src/__tests__/builtin-workflows.test.ts`, `packages/dashboard/src/routes/__tests__/task-create-workflow-route.test.ts`, `packages/dashboard/src/routes/__tests__/workflow-design-route.test.ts`, or a new focused route test if neither existing file owns the confirmed failure boundary +- **Approach:** First define "doesn't work" mechanically by reproducing whether CE fails at list visibility, create payload, selection persistence, workflow resolution, skill loading, or plugin runtime. Use the existing plugin-gated map (`PLUGIN_GATED_BUILTIN_WORKFLOWS`) as the source of truth. If unavailable CE workflows are currently selectable, either filter/disable them with a visible required-plugin reason or reject create/select requests with a 4xx that names `fusion-plugin-compound-engineering`. If the plugin is available but execution fails, trace skill loading through `FUSION_CE_SKILLS_DIR` and the CE skill-backed workflow nodes. +- **FNXC comment requirement:** Add/update FNXC comments where CE plugin gating is enforced, explaining that plugin-gated built-ins must not silently degrade to coding because that hides operator workflow intent. +- **Test Scenarios:** + - With CE plugin available/enabled, create/select persists `builtin:compound-engineering`. + - With CE plugin unavailable, create/select returns a clear client error or disabled option naming `fusion-plugin-compound-engineering`. + - CE workflow resolution does not fall back to `builtin:coding` after explicit selection. + - Skill-backed CE nodes still request CE skills by both namespaced and bare forms. +- **Verification:** Run focused core/route tests for CE gating and selection. + +### U8. Add the new final-review workflow to the fixed creation matrix + +- **Goal:** Ensure the new final-review stepwise workflow benefits from the same fixed creation path and appears in workflow selectors. +- **Requirements:** R1, R7, R10, R11, R12 +- **Files:** + - Modify the tests added in U2, U3, U6, and U7 to include the new workflow ID once U1/U2 create it +- **Approach:** After the selection bug is fixed for existing Stepwise and Compound engineering workflows, include the new final-review workflow in the same create/select matrix so future built-ins do not regress. +- **Test Scenarios:** + - Creating a task with the new workflow selected persists the new workflow ID. + - Optional-step defaults seed `code-review` and do not erase the selected workflow. +- **Verification:** Covered by the focused create/selection tests. + +--- + +## Verification Contract + +| Scope | Command | Proves | +|---|---|---| +| Built-in registry and IR shape | `pnpm --filter @fusion/core exec vitest run src/__tests__/builtin-workflows.test.ts src/__tests__/builtin-coding-workflow-ir.test.ts --silent=passed-only --reporter=dot` | New workflow registers, parses, round-trips, and has final-review-only stepwise shape. | +| Optional groups | `pnpm --filter @fusion/core exec vitest run src/__tests__/workflow-optional-steps.test.ts src/__tests__/builtin-code-review-group.test.ts --silent=passed-only --reporter=dot` | Browser verification and code review defaults/wiring match other coding built-ins. | +| Stepwise create regression | `pnpm --filter @fusion/dashboard exec vitest run app/components/__tests__/TaskForm.test.tsx app/components/__tests__/NewTaskModal.test.tsx app/components/__tests__/ListView.test.tsx app/components/__tests__/QuickEntryBox.test.tsx app/components/__tests__/board-quickcreate-workflow-lane-visibility.test.tsx app/__tests__/api-tasks.test.ts src/routes/__tests__/task-create-workflow-route.test.ts --silent=passed-only --reporter=dot` | Selected workflow ID survives picker, quick-create, UI/task-create payload, and route boundaries and does not fall back to `builtin:coding`. | +| Store workflow selection | `pnpm --filter @fusion/core exec vitest run src/__tests__/builtin-workflows.test.ts --silent=passed-only --reporter=dot` | `TaskStore.createTask` and reserved-ID creation persist explicit/default workflow selections correctly. | +| Compound engineering gating | `pnpm --filter @fusion/dashboard exec vitest run src/routes/__tests__/task-create-workflow-route.test.ts src/routes/__tests__/workflow-design-route.test.ts --silent=passed-only --reporter=dot` | Plugin-gated CE create/select works when available and fails visibly when unavailable. If implementation creates a more focused route test, run that file instead of the broader existing route-design file. | +| Changeset format | `pnpm check:changesets` | Published package changeset uses required labeled fields. | +| Type safety if exports/types changed | `pnpm --filter @fusion/core typecheck` | New exports and registry changes typecheck in core. | + +Do not run `pnpm test:full` or `pnpm verify:workspace` for this scoped change. Use the merge gate later if the branch is being prepared for merge. + +--- + +## Definition of Done + +- The new workflow is available from `getBuiltinWorkflow` under a stable `builtin:` ID. +- The new workflow executes planned steps through `parse-steps` and `foreach` without any `step-review` node. +- The new workflow keeps browser verification, code review, final review, and merge behavior aligned with existing coding built-ins. +- Existing `builtin:stepwise-coding` behavior and tests remain intact. +- Creating a task from Stepwise coding persists `builtin:stepwise-coding` and never silently falls back to `builtin:coding`. +- Creating/selecting Compound engineering either persists `builtin:compound-engineering` when available or gives a clear plugin-gating error when unavailable. +- Workflow creation tests cover UI/API/store surfaces where workflow intent can be lost. +- Docs describe the distinction between per-step-review stepwise coding and final-review-only stepwise coding. +- A valid changeset exists for `@runfusion/fusion`. +- Focused tests in the Verification Contract pass. diff --git a/docs/workflow-editor.md b/docs/workflow-editor.md index 3912438b2c..02ea100535 100644 --- a/docs/workflow-editor.md +++ b/docs/workflow-editor.md @@ -165,11 +165,12 @@ Save is blocked by client-side issues such as unplaced nodes and blocking column Fusion ships built-in workflows as read-only references: -- `builtin:coding` — the default coding lifecycle and fallback for tasks without a workflow selection. +- `builtin:coding` — the default Stepwise-based coding lifecycle: plan steps, execute them one at a time, then review and merge the full result. +- `builtin:legacy-coding` — the original monolithic coding lifecycle for tasks that should not use graph-owned step execution. - `builtin:quick-fix` — a short path for trivial or no-commit/decision work. - `builtin:review-heavy` — a standard execute/review/merge path with an additional gated security review. - `builtin:compound-engineering` — a plugin-gated Compound Engineering pipeline: `/ce-plan` writes the CE plan doc, optional `ce-doc-review` can pressure-test plans (markdown gets autofix/Open Questions write-back; HTML uses DOM-safe helper mutations, including canonical checklist repair, only when safety is proven and otherwise report-only with no write), `/ce-work` implements, `/ce-code-review` gates merge, and autoMerge-off projects route through the CE PR/feedback skills before Fusion's manual merge seam. -- `builtin:stepwise-coding` — a graph variant that models per-step parse, execute, review, and rework structure. +- `builtin:stepwise-coding` — Coding (per-step review): a graph variant that models per-step parse, execute, review, and rework structure. - `builtin:design` — a UI-heavy work path with a gated design/UX review before standard review and merge. Built-ins can be viewed, exported, and used as templates, but their graph, columns, field declarations, and setting declarations are not editable. Their per-project setting **values** are editable from the Settings panel's Values tab. Selectable built-ins all use a capacity-released queue column (`todo` or a workflow-specific backlog) that dispatches to the active WIP column through the standard hold/release sweep. diff --git a/docs/workflow-steps.md b/docs/workflow-steps.md index d08e5295eb..ef4337dfe0 100644 --- a/docs/workflow-steps.md +++ b/docs/workflow-steps.md @@ -44,12 +44,13 @@ Decision-only or investigation tasks can also declare `noCommitsExpected` / `**N | Workflow | ID | Notes | |---|---|---| -| Coding | `builtin:coding` | Default coding lifecycle and fallback for tasks without an explicit selection. | +| Coding | `builtin:coding` | Default Stepwise-based coding lifecycle: plan steps, execute them one at a time, then review and merge the full result. | +| Legacy coding | `builtin:legacy-coding` | Original monolithic coding lifecycle for tasks that should not use graph-owned step execution. | | Quick fix | `builtin:quick-fix` | Short path for trivial or no-commit/decision work; omits the standard review stage. | | Review-heavy | `builtin:review-heavy` | Standard execute/review/merge path with an additional gated security review. | | Marketing | `builtin:marketing` | Content pipeline with custom Ideation, Backlog, Drafting, Editorial review, Published, and Archived columns plus structured marketing brief/draft/editorial prompts; drafts are persisted as task documents for review while the workflow reuses standard lifecycle traits and merge primitives. | | Compound engineering | `builtin:compound-engineering` | Plugin-gated CE workflow that invokes `/ce-plan`, optional advisory `ce-doc-review` (markdown autofix; HTML DOM-safe mutation with report-only fallback), `/ce-work`, merge-blocking `/ce-code-review`, CE PR/feedback skills, Fusion merge, and learnings capture. | -| Stepwise coding | `builtin:stepwise-coding` | Graph-executor workflow that models per-step parse/execute/review/rework explicitly. | +| Coding (per-step review) | `builtin:stepwise-coding` | Graph-executor workflow that models per-step parse/execute/review/rework explicitly. | | Design | `builtin:design` | UI-heavy work path that implements, persists a user-facing design preview task document, runs a gated design/UX review, then performs the standard review and merge. | | PR lifecycle | `builtin:pr-workflow` | Reusable PR lifecycle graph fragment (create PR → await review → respond → gate → merge); it is a fragment, not directly selectable as a task workflow. | | Lead generation | `builtin:lead-generation` | Selectable business workflow for sourcing, qualifying, enriching, and contacting leads with custom lead fields, stage columns, and reviewable enrichment/outreach task documents; requires the workflow graph executor for custom board columns. | @@ -177,11 +178,13 @@ The workflow runtime is the authoritative execution path for task lifecycle work The engine remains the substrate for scheduler dispatch, routing claims, persistence, concurrency limits, process supervision, storage, and audit plumbing. Lifecycle policy belongs in built-in or custom workflows. -The default built-in catalog entry `builtin:coding` is backed by the canonical `BUILTIN_CODING_WORKFLOW_IR`, which is also the resolver/runtime fallback for tasks with no workflow selection or an explicit default selection. Missing/corrupt explicit custom selections fail closed as workflow-resolution failures instead of silently running the default. The built-in IR encodes the legacy lifecycle path as graph stages, with merge represented by workflow-native policy primitives rather than a single linear merge seam: +The default built-in catalog entry `builtin:coding` is backed by a Stepwise-derived final-review graph. It is the resolver/runtime fallback for tasks with no workflow selection or an explicit default selection. Missing/corrupt explicit custom selections fail closed as workflow-resolution failures instead of silently running the default. The built-in IR parses planned steps, executes them sequentially without per-step review, then runs the whole-task review and merge region: -- `triage/planning` → `execute` → `optional-group` quality gates → `review` → `merge-gate` / branch-group integration / `merge-attempt` / retry or manual hold → post-merge `optional-group` gates → `end` +- `triage` → `plan` → `parse-steps` → `foreach(step-execute)` → `optional-group` quality gates → `review` → `merge-gate` / branch-group integration / `merge-attempt` / retry or manual hold → `end` -`builtin:stepwise-coding` is a separate graph variant backed by `BUILTIN_STEPWISE_CODING_WORKFLOW_IR`; it keeps the same lifecycle columns/traits while modeling per-step parse/execute/review/rework as authored graph structure. +`builtin:legacy-coding` is backed by the original monolithic `BUILTIN_CODING_WORKFLOW_IR`: `planning` → `execute` → optional quality gates → `review` → merge region. + +`builtin:stepwise-coding` displays as Coding (per-step review). It is backed by `BUILTIN_STEPWISE_CODING_WORKFLOW_IR`; it keeps the same lifecycle columns/traits while modeling per-step parse/execute/review/rework as authored graph structure. `builtin:marketing` is a non-coding content workflow with marketing-specific columns (`ideation`, `backlog`, `drafting`, `editorial-review`, `published`, `archived`) and prompt seams for content brief, draft, and editorial review. Its draft stage saves the primary content deliverable as a task document for human review, while the workflow uses the same lifecycle traits (`intake`, `hold`, `wip`, `merge-blocker`, `human-review`, `complete`, `archived`) and the same merge-gate/branch-group/merge-attempt primitive region as coding workflows, so scheduler, capacity, review blocking, and merge orchestration behavior remain standard. @@ -341,7 +344,7 @@ Node config (`WorkflowOptionalGroupConfig`): `{ name?, defaultOn?, maxRevisions? Built-in optional gates ship as inlined IR builders, not as a template catalog: -- `builtin:coding` carries the `browser-verification` optional-group node (`builtin-browser-verification-group.ts`), opt-in by default, so browser verification runs only for tasks whose `enabledWorkflowSteps` includes `browser-verification`. +- `builtin:coding` carries the `browser-verification` optional-group node (`builtin-browser-verification-group.ts`), default-off, so browser verification runs only for tasks whose `enabledWorkflowSteps` includes `browser-verification`. - The `code-review` optional-group node (`builtin-code-review-group.ts`) is the inlined code-review gate. - A workflow (for example compound-engineering) can add a **post-merge** optional-group node via the generic `postMergeOptionalGroupNode(...)` builder (`builtin-post-merge-group.ts`) — e.g. a `document` step that runs after merge. @@ -422,7 +425,7 @@ FN-7039 (U6) DELETED the `WORKFLOW_STEP_TEMPLATES` built-in catalog array (the f The built-in quality gates ship as inlined `optional-group` node builders in `@fusion/core`, not as a template catalog (the former `WORKFLOW_STEP_TEMPLATES` array was removed): -- **Browser Verification** (`browser-verification`, `builtin-browser-verification-group.ts`) — browser-automation-style checks for UI validation flows; an optional-group node on `builtin:coding` and `builtin:stepwise-coding`. +- **Browser Verification** (`browser-verification`, `builtin-browser-verification-group.ts`) — browser-automation-style checks for UI validation flows; an optional-group node on `builtin:coding`, `builtin:legacy-coding`, and `builtin:stepwise-coding`. - **Code Review** (`code-review`, `builtin-code-review-group.ts`) — the inlined code-review gate. The Browser Verification inner prompt node carries `requiresBrowser: true` while keeping `toolMode: "coding"`. When that step runs, the executor best-effort adds the `agent-browser-navigation` skill (when the agent-browser plugin is installed), runs a bounded non-fatal `agent-browser --version` preflight, and writes start, availability, and finish entries into both the task log and the task's agent log. A missing or timed-out `agent-browser` binary is logged as an actionable warning rather than failing the step solely because of the preflight; the prompt can still fast-bail or report a normal verification failure. Because Bash tool events are already streamed to the agent log, `agent-browser open ...`, `agent-browser snapshot ...`, and related commands appear as the browser-verification activity the step performed. diff --git a/packages/core/src/__tests__/builtin-workflows.test.ts b/packages/core/src/__tests__/builtin-workflows.test.ts index ff45c36520..0d79068420 100644 --- a/packages/core/src/__tests__/builtin-workflows.test.ts +++ b/packages/core/src/__tests__/builtin-workflows.test.ts @@ -16,6 +16,7 @@ import { resolveColumnFlags } from "../trait-registry.js"; import { compileWorkflowToSteps } from "../workflow-compiler.js"; import { DEFAULT_WORKFLOW_COLUMN_IDS, parseWorkflowIr, serializeWorkflowIr } from "../workflow-ir.js"; import { createSharedTaskStoreTestHarness } from "./store-test-helpers.js"; +import { BUILTIN_STEPWISE_FINAL_REVIEW_CODING_WORKFLOW_IR } from "../builtin-stepwise-final-review-coding-workflow-ir.js"; const EXECUTE_NODE_MAX_RETRIES = 2; const LINEAR_BUILTIN_IDS = [ @@ -43,6 +44,7 @@ describe("built-in workflows", () => { // linear compiler cannot lower to a step list. They still must parse as valid IR. const NON_COMPILABLE_BUILTIN_IDS = new Set([ "builtin:coding", + "builtin:legacy-coding", "builtin:marketing", "builtin:stepwise-coding", "builtin:pr-workflow", @@ -83,6 +85,37 @@ describe("built-in workflows", () => { expect(template.nodes.some((n) => n.config?.seam === "step-execute")).toBe(true); }); + it("backs default coding with stepwise execution without per-step review", () => { + const workflow = getBuiltinWorkflow("builtin:coding"); + expect(workflow).toBeDefined(); + expect(workflow!.ir).toBe(BUILTIN_STEPWISE_FINAL_REVIEW_CODING_WORKFLOW_IR); + const ir = parseWorkflowIr(workflow!.ir); + if (ir.version !== "v2") throw new Error("expected v2"); + + expect(ir.nodes.some((node) => node.kind === "parse-steps")).toBe(true); + expect(ir.nodes.map((node) => node.id)).toEqual( + expect.arrayContaining(["plan", "parse", "steps", "browser-verification", "code-review", "review", "merge-gate", "merge-attempt"]), + ); + expect(ir.nodes.some((node) => node.id === "rework-hold")).toBe(false); + + const foreach = ir.nodes.find((node) => node.kind === "foreach"); + expect(foreach).toBeDefined(); + const template = ( + foreach!.config as { + template: { + nodes: Array<{ id: string; kind: string }>; + edges: Array<{ from: string; to: string; condition?: string; kind?: string }>; + }; + } + ).template; + expect(template.nodes.map((node) => node.id)).toEqual(["step-execute", "step-done"]); + expect(template.nodes.some((node) => node.kind === "step-review")).toBe(false); + expect(template.edges).toEqual([ + expect.objectContaining({ from: "step-execute", to: "step-done", condition: "success" }), + ]); + expect(template.edges.some((edge) => edge.kind === "rework")).toBe(false); + }); + it("includes the PR lifecycle built-in wiring the PR nodes end to end (U9)", () => { const pr = getBuiltinWorkflow("builtin:pr-workflow"); expect(pr).toBeDefined(); @@ -142,17 +175,28 @@ describe("built-in workflows", () => { ]); }); - it("builtin:coding catalog entry is backed by the canonical coding IR", () => { + it("builtin:coding catalog entry is backed by the stepwise final-review IR", () => { const coding = getBuiltinWorkflow("builtin:coding"); expect(coding).toBeDefined(); expect(coding!.id).toBe("builtin:coding"); expect(coding!.name).toBe("Coding (built-in)"); - expect(coding!.description).toContain("standard coding pipeline"); + expect(coding!.description).toContain("execute them one at a time"); expect(coding!.kind).toBe("workflow"); expect(coding!.createdAt).toBe("2026-01-01T00:00:00.000Z"); expect(coding!.updatedAt).toBe("2026-01-01T00:00:00.000Z"); - expect(coding!.ir).toBe(BUILTIN_CODING_WORKFLOW_IR); - expect(serializeWorkflowIr(coding!.ir)).toBe(serializeWorkflowIr(BUILTIN_CODING_WORKFLOW_IR)); + expect(coding!.ir).toBe(BUILTIN_STEPWISE_FINAL_REVIEW_CODING_WORKFLOW_IR); + expect(serializeWorkflowIr(coding!.ir)).toBe(serializeWorkflowIr(BUILTIN_STEPWISE_FINAL_REVIEW_CODING_WORKFLOW_IR)); + }); + + it("builtin:legacy-coding catalog entry preserves the original monolithic coding IR", () => { + const legacy = getBuiltinWorkflow("builtin:legacy-coding"); + expect(legacy).toBeDefined(); + expect(legacy!.id).toBe("builtin:legacy-coding"); + expect(legacy!.name).toBe("Legacy coding (built-in)"); + expect(legacy!.description).toContain("original monolithic coding pipeline"); + expect(legacy!.kind).toBe("workflow"); + expect(legacy!.ir).toBe(BUILTIN_CODING_WORKFLOW_IR); + expect(serializeWorkflowIr(legacy!.ir)).toBe(serializeWorkflowIr(BUILTIN_CODING_WORKFLOW_IR)); }); it("linear built-ins use the canonical trait-bearing default columns", () => { @@ -216,6 +260,17 @@ describe("built-in workflows", () => { { id: "archived", traits: ["archived"] }, ], ], + [ + "builtin:legacy-coding", + [ + { id: "triage", traits: ["intake"] }, + { id: "todo", traits: ["hold", "reset-on-entry"] }, + { id: "in-progress", traits: ["wip", "abort-on-exit", "timing"] }, + { id: "in-review", traits: ["merge-blocker", "human-review", "stall-detection", "merge"] }, + { id: "done", traits: ["complete"] }, + { id: "archived", traits: ["archived"] }, + ], + ], [ "builtin:lead-generation", [ @@ -276,7 +331,9 @@ describe("built-in workflows", () => { ]); const byId = new Map(ir.nodes.map((node) => [node.id, node])); - expect(byId.get("execute")?.column).toBe("in-progress"); + expect(byId.get("plan")?.column).toBe("in-progress"); + expect(byId.get("parse")?.column).toBe("in-progress"); + expect(byId.get("steps")?.column).toBe("in-progress"); // U6: the legacy `workflow-step` seam is replaced by the pre-merge // `browser-verification` optional-group, placed in the implementation column. expect(byId.get("workflow-step")).toBeUndefined(); @@ -392,9 +449,9 @@ describe("built-in workflows", () => { }); it("repeated catalog reads and listings keep builtin:coding in the enabled order", () => { - expect(getBuiltinWorkflow("builtin:coding")?.ir).toBe(BUILTIN_CODING_WORKFLOW_IR); - expect(getBuiltinWorkflow("builtin:coding")?.ir).toBe(BUILTIN_CODING_WORKFLOW_IR); - expect(BUILTIN_WORKFLOWS.find((workflow) => workflow.id === "builtin:coding")?.ir).toBe(BUILTIN_CODING_WORKFLOW_IR); + expect(getBuiltinWorkflow("builtin:coding")?.ir).toBe(BUILTIN_STEPWISE_FINAL_REVIEW_CODING_WORKFLOW_IR); + expect(getBuiltinWorkflow("builtin:coding")?.ir).toBe(BUILTIN_STEPWISE_FINAL_REVIEW_CODING_WORKFLOW_IR); + expect(BUILTIN_WORKFLOWS.find((workflow) => workflow.id === "builtin:coding")?.ir).toBe(BUILTIN_STEPWISE_FINAL_REVIEW_CODING_WORKFLOW_IR); expect(defaultEnabledBuiltinWorkflowIds()).toEqual( BUILTIN_WORKFLOWS.filter( (workflow) => workflow.kind !== "fragment" && !isBuiltinWorkflowPluginGated(workflow.id), @@ -408,11 +465,12 @@ describe("built-in workflows", () => { expect(defaultEnabledBuiltinWorkflowIds().length).toBeGreaterThanOrEqual(5); expect(defaultEnabledBuiltinWorkflowIds().slice(0, 5)).toEqual([ "builtin:coding", + "builtin:legacy-coding", "builtin:quick-fix", "builtin:review-heavy", "builtin:marketing", - "builtin:stepwise-coding", ]); + expect(defaultEnabledBuiltinWorkflowIds()).toContain("builtin:stepwise-coding"); }); it("identifies plugin-gated built-in workflows", () => { @@ -428,8 +486,8 @@ describe("built-in workflows", () => { expect(getRequiredPluginIdForBuiltinWorkflow("builtin:coding")).toBeUndefined(); expect(getRequiredPluginIdForBuiltinWorkflow("builtin:quick-fix")).toBeUndefined(); }); - it("builtin:coding exposes execute retries after registry lookup and parse round-trip", () => { - const coding = getBuiltinWorkflow("builtin:coding"); + it("builtin:legacy-coding exposes execute retries after registry lookup and parse round-trip", () => { + const coding = getBuiltinWorkflow("builtin:legacy-coding"); expect(coding).toBeDefined(); const ir = parseWorkflowIr(coding!.ir); const reparsed = parseWorkflowIr(serializeWorkflowIr(ir)); @@ -726,13 +784,18 @@ describe("built-in workflows", () => { expect(await store.getWorkflowDefinition("builtin:compound-engineering")).toBeDefined(); }); - it("shows the built-in seam prompt text in node config", () => { + it("shows the built-in prompt text in node config", () => { const coding = getBuiltinWorkflow("builtin:coding"); - const execute = coding?.ir.nodes.find((node) => node.id === "execute"); + const plan = coding?.ir.nodes.find((node) => node.id === "plan"); + const steps = coding?.ir.nodes.find((node) => node.id === "steps"); const review = coding?.ir.nodes.find((node) => node.id === "review"); + const legacy = getBuiltinWorkflow("builtin:legacy-coding"); + const legacyExecute = legacy?.ir.nodes.find((node) => node.id === "execute"); - expect((execute?.config as { prompt?: string } | undefined)?.prompt).toContain("You are a task execution agent"); + expect((plan?.config as { prompt?: string } | undefined)?.prompt).toContain("You are a task specification agent"); + expect(steps?.kind).toBe("foreach"); expect((review?.config as { prompt?: string } | undefined)?.prompt).toContain("You are an independent code and plan reviewer"); + expect((legacyExecute?.config as { prompt?: string } | undefined)?.prompt).toContain("You are a task execution agent"); // No `merge` seam node post-FN-6035 — merge runs as native primitives. expect(coding?.ir.nodes.find((node) => node.id === "merge")).toBeUndefined(); }); @@ -752,10 +815,11 @@ describe("built-in workflows", () => { // with builtin:coding — previously select returned [] and silently skipped the gate). const expectedGroups: Record = { "builtin:coding": ["code-review"], + "builtin:legacy-coding": ["code-review"], "builtin:marketing": [], "builtin:stepwise-coding": ["code-review"], }; - for (const workflowId of ["builtin:coding", "builtin:marketing", "builtin:stepwise-coding"]) { + for (const workflowId of ["builtin:coding", "builtin:legacy-coding", "builtin:marketing", "builtin:stepwise-coding"]) { const task = await store.createTask({ description: `select ${workflowId}`, enabledWorkflowSteps: [] }); const expected = expectedGroups[workflowId]; @@ -779,8 +843,10 @@ describe("built-in workflows", () => { }); it("a task can disable code-review by creating with explicit enabledWorkflowSteps excluding it", async () => { - // Default-on but TOGGLEABLE: an explicit (non-empty) enabledWorkflowSteps wins over - // the workflow's default-on seeding, so omitting `code-review` disables it. + // FNXC:WorkflowCreation 2026-06-28-23:09: + // Default-on optional groups are toggleable, but toggling them must not erase + // the explicit workflow selection row. User-facing create flows send workflowId + // and enabledWorkflowSteps together. const task = await store.createTask({ description: "coding without code review", workflowId: "builtin:coding", @@ -789,6 +855,55 @@ describe("built-in workflows", () => { const detail = await store.getTask(task.id); expect(detail.enabledWorkflowSteps ?? []).not.toContain("code-review"); expect(detail.enabledWorkflowSteps ?? []).toEqual(["browser-verification"]); + expect(store.getTaskWorkflowSelection(task.id)).toEqual({ + workflowId: "builtin:coding", + stepIds: ["browser-verification"], + }); + }); + + it("create-time stepwise workflowId persists when optional steps are submitted", async () => { + const task = await store.createTask({ + description: "stepwise with toggles", + workflowId: "builtin:stepwise-coding", + enabledWorkflowSteps: ["code-review"], + }); + + expect((await store.getTask(task.id)).enabledWorkflowSteps ?? []).toEqual(["code-review"]); + expect(store.getTaskWorkflowSelection(task.id)).toEqual({ + workflowId: "builtin:stepwise-coding", + stepIds: ["code-review"], + }); + }); + + it("create-time workflowId with empty optional steps disables default-on groups but keeps selection", async () => { + const task = await store.createTask({ + description: "coding with all optional groups off", + workflowId: "builtin:coding", + enabledWorkflowSteps: [], + }); + + expect((await store.getTask(task.id)).enabledWorkflowSteps ?? []).toEqual([]); + expect(store.getTaskWorkflowSelection(task.id)).toEqual({ + workflowId: "builtin:coding", + stepIds: [], + }); + }); + + it("reserved-id create-time workflowId persists when optional steps are submitted", async () => { + const task = await store.createTaskWithReservedId( + { + description: "reserved stepwise with toggles", + workflowId: "builtin:stepwise-coding", + enabledWorkflowSteps: ["code-review"], + }, + { taskId: "reserved-stepwise-with-toggles" }, + ); + + expect((await store.getTask(task.id)).enabledWorkflowSteps ?? []).toEqual(["code-review"]); + expect(store.getTaskWorkflowSelection(task.id)).toEqual({ + workflowId: "builtin:stepwise-coding", + stepIds: ["code-review"], + }); }); it("branching built-in project defaults do not throw", async () => { diff --git a/packages/core/src/__tests__/workflow-optional-steps.test.ts b/packages/core/src/__tests__/workflow-optional-steps.test.ts index 941bce6495..398cfd7880 100644 --- a/packages/core/src/__tests__/workflow-optional-steps.test.ts +++ b/packages/core/src/__tests__/workflow-optional-steps.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { BUILTIN_CODING_WORKFLOW_IR } from "../builtin-coding-workflow-ir.js"; import { BUILTIN_STEPWISE_CODING_WORKFLOW_IR } from "../builtin-stepwise-coding-workflow-ir.js"; +import { BUILTIN_STEPWISE_FINAL_REVIEW_CODING_WORKFLOW_IR } from "../builtin-stepwise-final-review-coding-workflow-ir.js"; import { resolveDefaultOnOptionalGroupIds, resolveWorkflowOptionalSteps, @@ -130,6 +131,7 @@ describe("resolveWorkflowOptionalSteps (optional-group nodes)", () => { ]; expect(resolveWorkflowOptionalSteps(BUILTIN_CODING_WORKFLOW_IR)).toEqual(expected); expect(resolveWorkflowOptionalSteps(BUILTIN_STEPWISE_CODING_WORKFLOW_IR)).toEqual(expected); + expect(resolveWorkflowOptionalSteps(BUILTIN_STEPWISE_FINAL_REVIEW_CODING_WORKFLOW_IR)).toEqual(expected); }); it("seeds code-review (default ON) but not browser-verification (default OFF) for the built-ins", () => { @@ -137,6 +139,7 @@ describe("resolveWorkflowOptionalSteps (optional-group nodes)", () => { // default: code-review is on, browser-verification is off. expect(resolveDefaultOnOptionalGroupIds(BUILTIN_CODING_WORKFLOW_IR)).toEqual(["code-review"]); expect(resolveDefaultOnOptionalGroupIds(BUILTIN_STEPWISE_CODING_WORKFLOW_IR)).toEqual(["code-review"]); + expect(resolveDefaultOnOptionalGroupIds(BUILTIN_STEPWISE_FINAL_REVIEW_CODING_WORKFLOW_IR)).toEqual(["code-review"]); }); }); diff --git a/packages/core/src/builtin-stepwise-final-review-coding-workflow-ir.ts b/packages/core/src/builtin-stepwise-final-review-coding-workflow-ir.ts new file mode 100644 index 0000000000..8cd8c36eb3 --- /dev/null +++ b/packages/core/src/builtin-stepwise-final-review-coding-workflow-ir.ts @@ -0,0 +1,43 @@ +import type { WorkflowIr } from "./workflow-ir-types.js"; +import { parseWorkflowIr } from "./workflow-ir.js"; +import { BUILTIN_STEPWISE_CODING_WORKFLOW_IR } from "./builtin-stepwise-coding-workflow-ir.js"; + +function cloneWorkflowIr(ir: WorkflowIr): WorkflowIr { + return JSON.parse(JSON.stringify(ir)) as WorkflowIr; +} + +/* +FNXC:WorkflowBuiltins 2026-06-28-23:09: +Operators need graph-owned step execution without per-step AI review. This built-in preserves the per-step-review workflow's parse-steps and sequential foreach model, then runs the normal end-of-task browser/code-review/final-review/merge suffix after all planned steps finish. +*/ +const RAW_BUILTIN_STEPWISE_FINAL_REVIEW_CODING_WORKFLOW_IR: WorkflowIr = (() => { + const ir = cloneWorkflowIr(BUILTIN_STEPWISE_CODING_WORKFLOW_IR); + ir.name = "builtin-stepwise-final-review-coding"; + + const foreach = ir.nodes.find((node) => node.id === "steps" && node.kind === "foreach"); + const template = foreach?.config?.template as + | { + nodes?: Array<{ id: string; kind: string; config?: Record }>; + edges?: Array<{ from: string; to: string; condition?: string; kind?: string }>; + } + | undefined; + if (!template?.nodes || !template.edges) { + throw new Error("stepwise final-review built-in requires the stepwise foreach template"); + } + + template.nodes = template.nodes.filter((node) => node.id !== "step-review"); + template.edges = [ + { from: "step-execute", to: "step-done", condition: "success" }, + ]; + + ir.nodes = ir.nodes.filter((node) => node.id !== "rework-hold"); + ir.edges = ir.edges.filter( + (edge) => edge.from !== "rework-hold" && edge.to !== "rework-hold", + ); + + return ir; +})(); + +export const BUILTIN_STEPWISE_FINAL_REVIEW_CODING_WORKFLOW_IR = parseWorkflowIr( + RAW_BUILTIN_STEPWISE_FINAL_REVIEW_CODING_WORKFLOW_IR, +); diff --git a/packages/core/src/builtin-workflows.ts b/packages/core/src/builtin-workflows.ts index 64d8d5d5f2..b0d6eed3e5 100644 --- a/packages/core/src/builtin-workflows.ts +++ b/packages/core/src/builtin-workflows.ts @@ -3,6 +3,7 @@ import { BUILTIN_LEAD_GENERATION_WORKFLOW_IR } from "./builtin-lead-generation-w import { BUILTIN_MARKETING_WORKFLOW_IR } from "./builtin-marketing-workflow-ir.js"; import { BUILTIN_PR_WORKFLOW_IR } from "./builtin-pr-workflow-ir.js"; import { BUILTIN_STEPWISE_CODING_WORKFLOW_IR } from "./builtin-stepwise-coding-workflow-ir.js"; +import { BUILTIN_STEPWISE_FINAL_REVIEW_CODING_WORKFLOW_IR } from "./builtin-stepwise-final-review-coding-workflow-ir.js"; import { BUILTIN_WORKFLOW_SETTINGS } from "./builtin-workflow-settings.js"; import { builtinPromptConfig } from "./builtin-workflow-prompts.js"; import type { WorkflowDefinition } from "./workflow-definition-types.js"; @@ -138,7 +139,33 @@ export const BUILTIN_WORKFLOWS: WorkflowDefinition[] = [ { id: "builtin:coding", name: "Coding (built-in)", - description: "The standard coding pipeline: implement, review, then merge. Equivalent to the default behavior.", + description: "Default coding pipeline: plan steps, execute them one at a time, then review and merge the full result.", + kind: "workflow", + ir: BUILTIN_STEPWISE_FINAL_REVIEW_CODING_WORKFLOW_IR, + layout: { + start: { x: 60, y: 160 }, + plan: { x: 230, y: 160 }, + parse: { x: 400, y: 160 }, + steps: { x: 570, y: 160 }, + "browser-verification": { x: 740, y: 160 }, + "code-review": { x: 910, y: 160 }, + review: { x: 1080, y: 160 }, + "merge-gate": { x: 1250, y: 160 }, + "branch-group-member-integration": { x: 1420, y: 80 }, + "branch-group-promotion": { x: 1590, y: 80 }, + "merge-attempt": { x: 1760, y: 160 }, + "merge-retry": { x: 1930, y: 80 }, + "recovery-router": { x: 1930, y: 240 }, + "merge-manual-hold": { x: 1420, y: 240 }, + end: { x: 2100, y: 160 }, + }, + createdAt: BUILTIN_TS, + updatedAt: BUILTIN_TS, + }, + { + id: "builtin:legacy-coding", + name: "Legacy coding (built-in)", + description: "The original monolithic coding pipeline: implement, review, then merge without graph-owned per-step execution.", kind: "workflow", ir: BUILTIN_CODING_WORKFLOW_IR, layout: { @@ -353,9 +380,9 @@ export const BUILTIN_WORKFLOWS: WorkflowDefinition[] = [ // workflow graph runtime. { id: "builtin:stepwise-coding", - name: "Stepwise coding (built-in)", + name: "Coding (per-step review) (built-in)", description: - "Per-step plan, execute, and review modeled as graph structure: each planned step runs and is reviewed (approve / revise / rethink) before the next, with bounded rework.", + "Per-step review coding pipeline: each planned step runs and is reviewed (approve / revise / rethink) before the next, with bounded rework before final review and merge.", kind: "workflow", ir: BUILTIN_STEPWISE_CODING_WORKFLOW_IR, layout: { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 6f730111ff..947cd72df1 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -165,6 +165,7 @@ export { } from "./workflow-prompt-overrides.js"; export type { WorkflowPromptDefault, WorkflowPromptOverrides } from "./workflow-prompt-overrides.js"; export { BUILTIN_STEPWISE_CODING_WORKFLOW_IR } from "./builtin-stepwise-coding-workflow-ir.js"; +export { BUILTIN_STEPWISE_FINAL_REVIEW_CODING_WORKFLOW_IR } from "./builtin-stepwise-final-review-coding-workflow-ir.js"; export { BUILTIN_PR_WORKFLOW_IR } from "./builtin-pr-workflow-ir.js"; export { BUILTIN_LEAD_GENERATION_WORKFLOW_IR } from "./builtin-lead-generation-workflow-ir.js"; export { diff --git a/packages/core/src/store.ts b/packages/core/src/store.ts index 8be7f30746..54efad9b52 100644 --- a/packages/core/src/store.ts +++ b/packages/core/src/store.ts @@ -4556,12 +4556,12 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS} // When a project default workflow is configured, new tasks inherit it // (compiled to steps) ahead of the legacy default-on step behavior. let pendingWorkflowSelection: { workflowId: string; stepIds: string[] } | undefined; - // U6/R3/KTD-4: an explicit create-time workflowId beats the project default. - // `null` is an explicit opt-out (no workflow), `string` materializes that - // workflow, `undefined` falls through to the default-workflow behavior below. - // Explicit enabledWorkflowSteps still wins over workflowId for trusted callers. - const explicitWorkflowId = - input.enabledWorkflowSteps === undefined ? input.workflowId : undefined; + /* + FNXC:WorkflowCreation 2026-06-28-23:09: + User-facing task creation can submit a selected workflowId and optional-group toggles together. The visible workflow selection is operator intent and must persist as task_workflow_selection; enabledWorkflowSteps only overrides that workflow's default optional-group seed. + Legacy trusted callers that submit enabledWorkflowSteps without workflowId still bypass workflow selection materialization. + */ + const explicitWorkflowId = input.workflowId; if (explicitWorkflowId !== undefined) { if (explicitWorkflowId === null) { // Explicit "No workflow": skip default materialization entirely. @@ -4570,8 +4570,14 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS} // Compile + materialize up front so unknown/fragment ids throw BEFORE // the task row is created (no orphaned steps, no half-created task). const selected = await this.materializeExplicitWorkflowSteps(explicitWorkflowId); - resolvedWorkflowSteps = selected.stepIds; - pendingWorkflowSelection = selected; + const explicitStepIds = input.enabledWorkflowSteps !== undefined + ? (resolvedWorkflowSteps ?? []) + : undefined; + resolvedWorkflowSteps = explicitStepIds ?? selected.stepIds; + pendingWorkflowSelection = { + workflowId: selected.workflowId, + stepIds: explicitStepIds ?? selected.stepIds, + }; } } else if (input.enabledWorkflowSteps === undefined) { try { @@ -4744,12 +4750,11 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS} : undefined; let pendingWorkflowSelection: { workflowId: string; stepIds: string[] } | undefined; - // U6/R3/KTD-4: an explicit create-time workflowId beats the project default, - // mirroring createTask(). `null` is an explicit opt-out, `string` materializes - // that workflow, `undefined` falls through to the default-workflow behavior. - // Explicit enabledWorkflowSteps still wins over workflowId for trusted callers. - const explicitWorkflowId = - input.enabledWorkflowSteps === undefined ? input.workflowId : undefined; + /* + FNXC:WorkflowCreation 2026-06-28-23:09: + Reserved-id task creation must match normal task creation: workflowId and enabledWorkflowSteps are independent create controls, so explicit optional toggles do not erase the selected workflow row. + */ + const explicitWorkflowId = input.workflowId; if (explicitWorkflowId !== undefined) { if (explicitWorkflowId === null) { // Explicit "No workflow": skip default materialization entirely. @@ -4758,8 +4763,14 @@ ${TASK_UPSERT_SQL_ASSIGNMENTS} // Compile + materialize up front so unknown/fragment ids throw BEFORE // the task row is created (no orphaned steps, no half-created task). const selected = await this.materializeExplicitWorkflowSteps(explicitWorkflowId); - resolvedWorkflowSteps = selected.stepIds; - pendingWorkflowSelection = selected; + const explicitStepIds = input.enabledWorkflowSteps !== undefined + ? (resolvedWorkflowSteps ?? []) + : undefined; + resolvedWorkflowSteps = explicitStepIds ?? selected.stepIds; + pendingWorkflowSelection = { + workflowId: selected.workflowId, + stepIds: explicitStepIds ?? selected.stepIds, + }; } } else if (input.enabledWorkflowSteps === undefined && options.applyDefaultWorkflowSteps !== false) { // Mirror createTask: a configured project default workflow takes diff --git a/packages/dashboard/app/components/__tests__/WorkflowNodeEditor.test.tsx b/packages/dashboard/app/components/__tests__/WorkflowNodeEditor.test.tsx index 435a10a11d..f21393b7db 100644 --- a/packages/dashboard/app/components/__tests__/WorkflowNodeEditor.test.tsx +++ b/packages/dashboard/app/components/__tests__/WorkflowNodeEditor.test.tsx @@ -2262,7 +2262,7 @@ function builtinStepwiseDef(): WorkflowDefinition { return { id: "builtin:stepwise-coding", kind: "workflow", - name: "Stepwise coding (built-in)", + name: "Coding (per-step review) (built-in)", description: "", ir: BUILTIN_STEPWISE_CODING_WORKFLOW_IR, layout: {}, diff --git a/packages/dashboard/src/routes/__tests__/task-create-workflow-route.test.ts b/packages/dashboard/src/routes/__tests__/task-create-workflow-route.test.ts index c0ebb6cae6..0ae59aa6a0 100644 --- a/packages/dashboard/src/routes/__tests__/task-create-workflow-route.test.ts +++ b/packages/dashboard/src/routes/__tests__/task-create-workflow-route.test.ts @@ -80,7 +80,7 @@ describe("POST /tasks workflowId (U6/R3)", () => { const post = (path: string, body: unknown) => REQUEST(app, "POST", path, JSON.stringify(body), { "content-type": "application/json" }); - it("workflowId → created task has populated enabledWorkflowSteps", async () => { + it("workflowId → created task records workflow selection", async () => { const wf = await store.createWorkflowDefinition({ name: "QA", ir: linearIr("qa") }); const res = await post("/api/tasks", { description: "with workflow", workflowId: wf.id }); @@ -88,8 +88,100 @@ describe("POST /tasks workflowId (U6/R3)", () => { const created = res.body as { id: string }; const detail = await store.getTask(created.id); - expect(detail.enabledWorkflowSteps).toHaveLength(2); - expect(store.getTaskWorkflowSelection(created.id)?.workflowId).toBe(wf.id); + expect(detail.enabledWorkflowSteps ?? []).toEqual([]); + expect(store.getTaskWorkflowSelection(created.id)).toEqual({ workflowId: wf.id, stepIds: [] }); + }); + + it("workflowId plus enabledWorkflowSteps → created task keeps explicit workflow selection", async () => { + const res = await post("/api/tasks", { + description: "stepwise with toggles", + workflowId: "builtin:stepwise-coding", + enabledWorkflowSteps: ["code-review"], + }); + expect(res.status).toBe(201); + const created = res.body as { id: string }; + + const detail = await store.getTask(created.id); + expect(detail.enabledWorkflowSteps ?? []).toEqual(["code-review"]); + expect(store.getTaskWorkflowSelection(created.id)).toEqual({ + workflowId: "builtin:stepwise-coding", + stepIds: ["code-review"], + }); + }); + + it("workflowId plus empty enabledWorkflowSteps → disables default optional groups and keeps selection", async () => { + const res = await post("/api/tasks", { + description: "coding with optional groups off", + workflowId: "builtin:coding", + enabledWorkflowSteps: [], + }); + expect(res.status).toBe(201); + const created = res.body as { id: string }; + + const detail = await store.getTask(created.id); + expect(detail.enabledWorkflowSteps ?? []).toEqual([]); + expect(store.getTaskWorkflowSelection(created.id)).toEqual({ + workflowId: "builtin:coding", + stepIds: [], + }); + }); + + it.each([ + ["default coding", "builtin:coding", ["code-review"]], + ["legacy coding", "builtin:legacy-coding", ["code-review"]], + ["coding per-step review", "builtin:stepwise-coding", ["code-review"]], + ])("%s workflow create/select/resolve works end to end", async (_label, workflowId, defaultSteps) => { + const res = await post("/api/tasks", { + description: `exercise ${workflowId}`, + workflowId, + }); + expect(res.status).toBe(201); + const created = res.body as { id: string }; + + const detail = await store.getTask(created.id); + expect(detail.enabledWorkflowSteps ?? []).toEqual(defaultSteps); + expect(store.getTaskWorkflowSelection(created.id)).toEqual({ + workflowId, + stepIds: defaultSteps, + }); + const definition = await store.getWorkflowDefinition(workflowId); + expect(definition?.id).toBe(workflowId); + expect(definition?.kind).toBe("workflow"); + }); + + it("unavailable Compound engineering workflow → 4xx instead of default coding fallback", async () => { + const before = (await store.listTasks({ includeArchived: true })).length; + + const res = await post("/api/tasks", { + description: "compound unavailable", + workflowId: "builtin:compound-engineering", + }); + + expect(res.status).toBeGreaterThanOrEqual(400); + expect(res.status).toBeLessThan(500); + expect(String((res.body as { error?: unknown }).error ?? "")).toContain("builtin:compound-engineering"); + const after = (await store.listTasks({ includeArchived: true })).length; + expect(after).toBe(before); + }); + + it("registered Compound engineering plugin → created task keeps Compound workflow selection", async () => { + await store.getPluginStore().registerPlugin({ + manifest: { + id: "fusion-plugin-compound-engineering", + name: "Compound Engineering", + version: "0.0.0", + }, + path: rootDir, + }); + + const res = await post("/api/tasks", { + description: "compound available", + workflowId: "builtin:compound-engineering", + }); + expect(res.status).toBe(201); + const created = res.body as { id: string }; + + expect(store.getTaskWorkflowSelection(created.id)?.workflowId).toBe("builtin:compound-engineering"); }); it("workflowId: null → task created with no workflow steps", async () => {