FN-7970: deprecate unused builtin:brainstorming from new selection
Hide the built-in Brainstorming workflow from new selection after occupancy preflight, while keeping it resolvable for existing tasks. - Add DEPRECATED_BUILTIN_WORKFLOW_IDS and isBuiltinWorkflowDeprecated helper - Exclude deprecated built-ins from defaults and selection listings - Hide deprecated built-ins from Settings workflow enablement toggles - Update docs/tests and add a minor changeset for the operator-facing change Files changed: .changeset/fn-7970-deprecate-brainstorming.md | 7 ++++ docs/workflow-steps.md | 2 +- .../core/src/__tests__/builtin-workflows.test.ts | 40 ++++++++++++---------- packages/core/src/builtin-workflows.ts | 17 ++++++++- packages/core/src/index.gate.ts | 2 ++ packages/core/src/index.ts | 2 ++ packages/core/src/task-store/remaining-ops-8.ts | 10 ++++-- packages/core/src/types.ts | 9 +++++ .../app/__tests__/settings-sections.test.tsx | 28 ++++++++++++++- .../settings/sections/GeneralSection.tsx | 9 +++-- 10 files changed, 101 insertions(+), 25 deletions(-) Fusion-Task-Id: FN-7970 Fusion-Task-Lineage: 47f9cd6e-d843-4c14-b197-447ff2072e3b Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7970-deprecate-brainstorming.md
Normal file
7
.changeset/fn-7970-deprecate-brainstorming.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": minor
|
||||
---
|
||||
|
||||
summary: Deprecate the built-in Brainstorming workflow — it no longer appears for new task selection.
|
||||
category: internal
|
||||
dev: builtin:brainstorming is excluded from defaults and listWorkflowDefinitions through the deprecation registry/helper, but remains resolvable by id. Applied after a successful live-store query verified no active task selects it.
|
||||
@@ -386,7 +386,7 @@ start → ask (ask-user: "Anything to refine?")
|
||||
the PR review loop)
|
||||
```
|
||||
|
||||
Each turn, the user is asked to refine; once they reply "looks good" (or whatever the condition matches), the exit-gate routes the task out of the brainstorm loop. This composition is also available as a discoverable built-in: `builtin:brainstorming` (FN-7584) registers exactly this shape — `ask-user` → a refine prompt step → `exit-gate`-on-approval — ahead of the unmodified default Coding plan/execute/review/merge spine, selectable directly from the workflow picker. Copy the shape into a custom workflow's IR via `fn_workflow_create`/`fn_workflow_update` when you need a different downstream pipeline than the standard coding one.
|
||||
Each turn, the user is asked to refine; once they reply "looks good" (or whatever the condition matches), the exit-gate routes the task out of the brainstorm loop. The former `builtin:brainstorming` composition (FN-7584) is deprecated and hidden from new workflow selection, but remains resolvable for tasks that already use it. Its `ask-user` → refine prompt → `exit-gate`-on-approval shape can be copied into a custom workflow's IR via `fn_workflow_create`/`fn_workflow_update` when you need this behavior or a different downstream pipeline than the standard coding one.
|
||||
|
||||
#### Workflow-defined custom task fields
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
getRequiredPluginIdForBuiltinWorkflow,
|
||||
isBuiltinWorkflowId,
|
||||
isBuiltinWorkflowPluginGated,
|
||||
isBuiltinWorkflowDeprecated,
|
||||
} from "../builtin-workflows.js";
|
||||
import { BUILTIN_CODING_WORKFLOW_IR } from "../builtin-coding-workflow-ir.js";
|
||||
import { BUILTIN_STEPWISE_CODING_WORKFLOW_IR } from "../builtin-stepwise-coding-workflow-ir.js";
|
||||
@@ -344,29 +345,16 @@ describe("built-in workflows", () => {
|
||||
);
|
||||
});
|
||||
|
||||
/*
|
||||
* FNXC:WorkflowBrainstorming 2026-07-05-00:00:
|
||||
* FN-7584 parity coverage for the registered builtin:brainstorming built-in
|
||||
* (FN-7579's ask-user -> refine -> exit-gate-on-approval composition,
|
||||
* discoverable from the workflow picker ahead of the normal coding spine).
|
||||
*/
|
||||
it("registers builtin:brainstorming as a default-enabled workflow ordered after the existing built-ins", () => {
|
||||
it("keeps deprecated builtin:brainstorming resolvable while excluding it from defaults", () => {
|
||||
const brainstorming = getBuiltinWorkflow("builtin:brainstorming");
|
||||
expect(brainstorming).toBeDefined();
|
||||
expect(brainstorming!.kind).toBe("workflow");
|
||||
expect(() => parseWorkflowIr(brainstorming!.ir)).not.toThrow();
|
||||
expect(defaultEnabledBuiltinWorkflowIds()).toContain("builtin:brainstorming");
|
||||
expect(isBuiltinWorkflowDeprecated("builtin:brainstorming")).toBe(true);
|
||||
expect(defaultEnabledBuiltinWorkflowIds()).not.toContain("builtin:brainstorming");
|
||||
expect(BUILTIN_WORKFLOWS.findIndex((workflow) => workflow.id === "builtin:brainstorming")).toBeGreaterThan(
|
||||
BUILTIN_WORKFLOWS.findIndex((workflow) => workflow.id === "builtin:lead-generation"),
|
||||
);
|
||||
// Ordering assertion the suite pins elsewhere (`.slice(0, 5)`) must stay untouched.
|
||||
expect(defaultEnabledBuiltinWorkflowIds().slice(0, 5)).toEqual([
|
||||
"builtin:coding",
|
||||
"builtin:coding-ideas",
|
||||
"builtin:legacy-coding",
|
||||
"builtin:quick-fix",
|
||||
"builtin:review-heavy",
|
||||
]);
|
||||
});
|
||||
|
||||
it("orders builtin:brainstorming's ask-user/exit-gate loop ahead of the plan/execute spine", () => {
|
||||
@@ -739,12 +727,15 @@ describe("built-in workflows", () => {
|
||||
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),
|
||||
(workflow) => workflow.kind !== "fragment"
|
||||
&& !isBuiltinWorkflowPluginGated(workflow.id)
|
||||
&& !isBuiltinWorkflowDeprecated(workflow.id),
|
||||
).map((workflow) => workflow.id),
|
||||
);
|
||||
expect(defaultEnabledBuiltinWorkflowIds()).toContain("builtin:design");
|
||||
expect(defaultEnabledBuiltinWorkflowIds()).toContain("builtin:marketing");
|
||||
expect(defaultEnabledBuiltinWorkflowIds()).not.toContain("builtin:compound-engineering");
|
||||
expect(defaultEnabledBuiltinWorkflowIds()).not.toContain("builtin:brainstorming");
|
||||
expect(defaultEnabledBuiltinWorkflowIds()).not.toContain("builtin:pr-workflow");
|
||||
expect(getBuiltinWorkflow("builtin:pr-workflow")!.kind).toBe("fragment");
|
||||
expect(defaultEnabledBuiltinWorkflowIds().length).toBeGreaterThanOrEqual(5);
|
||||
@@ -758,10 +749,12 @@ describe("built-in workflows", () => {
|
||||
expect(defaultEnabledBuiltinWorkflowIds()).toContain("builtin:stepwise-coding");
|
||||
});
|
||||
|
||||
it("identifies plugin-gated built-in workflows", () => {
|
||||
it("identifies plugin-gated and deprecated built-in workflows", () => {
|
||||
expect(isBuiltinWorkflowPluginGated("builtin:compound-engineering")).toBe(true);
|
||||
expect(isBuiltinWorkflowPluginGated("builtin:coding")).toBe(false);
|
||||
expect(isBuiltinWorkflowPluginGated("builtin:quick-fix")).toBe(false);
|
||||
expect(isBuiltinWorkflowDeprecated("builtin:brainstorming")).toBe(true);
|
||||
expect(isBuiltinWorkflowDeprecated("builtin:coding")).toBe(false);
|
||||
});
|
||||
|
||||
it("resolves required plugin ids for plugin-gated built-in workflows", () => {
|
||||
@@ -1064,6 +1057,17 @@ describe("built-in workflows", () => {
|
||||
expect(managementList.some((workflow) => workflow.id === "builtin:compound-engineering")).toBe(false);
|
||||
});
|
||||
|
||||
it("hides deprecated brainstorming from selection listings while preserving management and direct resolution", async () => {
|
||||
const selectionList = await store.listWorkflowDefinitions();
|
||||
expect(selectionList.some((workflow) => workflow.id === "builtin:brainstorming")).toBe(false);
|
||||
|
||||
const managementList = await store.listWorkflowDefinitions({ includeDisabledBuiltins: true });
|
||||
expect(managementList.some((workflow) => workflow.id === "builtin:brainstorming")).toBe(true);
|
||||
expect(await store.getWorkflowDefinition("builtin:brainstorming")).toMatchObject({
|
||||
id: "builtin:brainstorming",
|
||||
});
|
||||
});
|
||||
|
||||
it("hides the compound-engineering built-in when its plugin is not installed", async () => {
|
||||
const list = await store.listWorkflowDefinitions();
|
||||
expect(list.some((workflow) => workflow.id === "builtin:compound-engineering")).toBe(false);
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
codeReviewRemediationNode,
|
||||
planReplanNode,
|
||||
} from "./builtin-workflow-remediation-nodes.js";
|
||||
import { DEPRECATED_BUILTIN_WORKFLOW_IDS } from "./types.js";
|
||||
import type { WorkflowDefinition } from "./workflow-definition-types.js";
|
||||
import type { WorkflowIr, WorkflowIrColumn, WorkflowIrNode } from "./workflow-ir-types.js";
|
||||
import { parseWorkflowIr } from "./workflow-ir.js";
|
||||
@@ -33,17 +34,31 @@ const PLUGIN_GATED_BUILTIN_WORKFLOWS: ReadonlyMap<string, string> = new Map([
|
||||
["builtin:compound-engineering", "fusion-plugin-compound-engineering"],
|
||||
]);
|
||||
|
||||
/*
|
||||
* FNXC:WorkflowBrainstorming 2026-07-15-15:49:
|
||||
* FN-7970 deprecates builtin:brainstorming after its occupancy preflight: hide it
|
||||
* from new selection while retaining its definition for existing task resolution.
|
||||
* Keep this generic registry so later deprecated built-ins share the same policy.
|
||||
*/
|
||||
const DEPRECATED_BUILTIN_WORKFLOWS: ReadonlySet<string> = DEPRECATED_BUILTIN_WORKFLOW_IDS;
|
||||
|
||||
export function isBuiltinWorkflowPluginGated(id: string): boolean {
|
||||
return PLUGIN_GATED_BUILTIN_WORKFLOWS.has(id);
|
||||
}
|
||||
|
||||
export function isBuiltinWorkflowDeprecated(id: string): boolean {
|
||||
return DEPRECATED_BUILTIN_WORKFLOWS.has(id);
|
||||
}
|
||||
|
||||
export function getRequiredPluginIdForBuiltinWorkflow(id: string): string | undefined {
|
||||
return PLUGIN_GATED_BUILTIN_WORKFLOWS.get(id);
|
||||
}
|
||||
|
||||
export function defaultEnabledBuiltinWorkflowIds(): string[] {
|
||||
return BUILTIN_WORKFLOWS.filter(
|
||||
(workflow) => workflow.kind !== "fragment" && !PLUGIN_GATED_BUILTIN_WORKFLOWS.has(workflow.id),
|
||||
(workflow) => workflow.kind !== "fragment"
|
||||
&& !isBuiltinWorkflowPluginGated(workflow.id)
|
||||
&& !isBuiltinWorkflowDeprecated(workflow.id),
|
||||
).map((workflow) => workflow.id);
|
||||
}
|
||||
|
||||
|
||||
@@ -473,6 +473,7 @@ export {
|
||||
stepToFragmentIr,
|
||||
layoutForIr,
|
||||
} from "./workflow-steps-to-ir.js";
|
||||
export { DEPRECATED_BUILTIN_WORKFLOW_IDS } from "./types.js";
|
||||
export {
|
||||
BUILTIN_WORKFLOWS,
|
||||
BUILTIN_WORKFLOW_ID_PREFIX,
|
||||
@@ -480,6 +481,7 @@ export {
|
||||
getRequiredPluginIdForBuiltinWorkflow,
|
||||
isBuiltinWorkflowId,
|
||||
isBuiltinWorkflowPluginGated,
|
||||
isBuiltinWorkflowDeprecated,
|
||||
} from "./builtin-workflows.js";
|
||||
export {
|
||||
COMPLETION_SUMMARY_NODE_ID,
|
||||
|
||||
@@ -462,6 +462,7 @@ export {
|
||||
stepToFragmentIr,
|
||||
layoutForIr,
|
||||
} from "./workflow-steps-to-ir.js";
|
||||
export { DEPRECATED_BUILTIN_WORKFLOW_IDS } from "./types.js";
|
||||
export {
|
||||
BUILTIN_WORKFLOWS,
|
||||
BUILTIN_WORKFLOW_ID_PREFIX,
|
||||
@@ -469,6 +470,7 @@ export {
|
||||
getRequiredPluginIdForBuiltinWorkflow,
|
||||
isBuiltinWorkflowId,
|
||||
isBuiltinWorkflowPluginGated,
|
||||
isBuiltinWorkflowDeprecated,
|
||||
} from "./builtin-workflows.js";
|
||||
export {
|
||||
COMPLETION_SUMMARY_NODE_ID,
|
||||
|
||||
@@ -11,7 +11,7 @@ import { TaskStore } from "../store.js";
|
||||
import {resolveEntryColumnId} from "../workflow-reconciliation.js";
|
||||
import { pruneAgentLogFiles as pruneAgentLogFileEntries, readAgentLogEntriesByTimeRange } from "../agent-log-file-store.js";
|
||||
import { BUILTIN_CODING_WORKFLOW_IR } from "../builtin-coding-workflow-ir.js";
|
||||
import { BUILTIN_WORKFLOWS, getBuiltinWorkflow, getRequiredPluginIdForBuiltinWorkflow, isBuiltinWorkflowEnabled, isBuiltinWorkflowId, isBuiltinWorkflowPluginGated } from "../builtin-workflows.js";
|
||||
import { BUILTIN_WORKFLOWS, getBuiltinWorkflow, getRequiredPluginIdForBuiltinWorkflow, isBuiltinWorkflowDeprecated, isBuiltinWorkflowEnabled, isBuiltinWorkflowId, isBuiltinWorkflowPluginGated } from "../builtin-workflows.js";
|
||||
import { CentralCore } from "../central-core.js";
|
||||
import { fromJson } from "../db.js";
|
||||
import { type DistributedTaskIdAllocator, createDistributedTaskIdAllocator } from "../distributed-task-id.js";
|
||||
@@ -235,8 +235,14 @@ export async function listWorkflowDefinitionsImpl(store: TaskStore,
|
||||
const enabledVisible = options?.includeDisabledBuiltins
|
||||
? all
|
||||
: all.filter((wf) => isBuiltinWorkflowEnabled(wf.id, enabledBuiltinWorkflowIds));
|
||||
// FNXC:WorkflowBrainstorming 2026-07-15-15:49:
|
||||
// FN-7970 removes deprecated built-ins only from new-selection listings.
|
||||
// Management listings retain them, and direct id resolution remains unconditional.
|
||||
const selectionVisible = options?.includeDisabledBuiltins
|
||||
? enabledVisible
|
||||
: enabledVisible.filter((wf) => !isBuiltinWorkflowDeprecated(wf.id));
|
||||
const visible = await Promise.all(
|
||||
enabledVisible.map(async (wf) => {
|
||||
selectionVisible.map(async (wf) => {
|
||||
const requiredPluginId = getRequiredPluginIdForBuiltinWorkflow(wf.id);
|
||||
if (!requiredPluginId) return wf;
|
||||
return (await store.isPluginInstalled(requiredPluginId)) ? wf : undefined;
|
||||
|
||||
@@ -32,6 +32,15 @@ export {
|
||||
export type { GitlabConfigSettingsSource, ResolvedGitlabConfig, ResolveGitlabConfigInput } from "./gitlab-config.js";
|
||||
export { validateMcpServerDefinitionDetailed, validateMcpServerDefinitionsDetailed } from "./settings-validation.js";
|
||||
|
||||
/*
|
||||
* FNXC:WorkflowBrainstorming 2026-07-15-15:49:
|
||||
* Keep deprecation IDs browser-safe because Settings renders the management list
|
||||
* (including disabled built-ins) and must not offer deprecated entries again.
|
||||
*/
|
||||
export const DEPRECATED_BUILTIN_WORKFLOW_IDS: ReadonlySet<string> = new Set([
|
||||
"builtin:brainstorming",
|
||||
]);
|
||||
|
||||
|
||||
/*
|
||||
FNXC:CodeOrganization 2026-07-15-00:00:
|
||||
|
||||
@@ -24,7 +24,7 @@ import { PromptsSection } from "../components/settings/sections/PromptsSection";
|
||||
import { SecretsSection } from "../components/settings/sections/SecretsSection";
|
||||
import { WorktreesSection } from "../components/settings/sections/WorktreesSection";
|
||||
import type { SettingsFormState } from "../components/settings/sections/context";
|
||||
import { fetchWorkflow, fetchWorkflowSettingValues, updateWorkflowSettingValues } from "../api";
|
||||
import { fetchWorkflow, fetchWorkflows, fetchWorkflowSettingValues, updateWorkflowSettingValues } from "../api";
|
||||
import type { WorkflowSettingValuesPayload } from "../api";
|
||||
|
||||
vi.mock("../components/AgentPromptsManager", () => ({
|
||||
@@ -75,9 +75,11 @@ vi.mock("../components/CustomModelDropdown", () => ({
|
||||
|
||||
expect.extend(jestDomMatchers);
|
||||
beforeEach(() => {
|
||||
vi.mocked(fetchWorkflows).mockReset();
|
||||
vi.mocked(fetchWorkflow).mockReset();
|
||||
vi.mocked(fetchWorkflowSettingValues).mockReset();
|
||||
vi.mocked(updateWorkflowSettingValues).mockReset();
|
||||
vi.mocked(fetchWorkflows).mockResolvedValue([]);
|
||||
vi.mocked(fetchWorkflow).mockResolvedValue({ id: "builtin:coding", name: "Coding", ir: {} } as never);
|
||||
vi.mocked(fetchWorkflowSettingValues).mockResolvedValue({ stored: {}, effective: {}, orphaned: [] });
|
||||
vi.mocked(updateWorkflowSettingValues).mockResolvedValue({ stored: {}, effective: {}, orphaned: [] });
|
||||
@@ -117,6 +119,30 @@ describe("AppearanceSection", () => {
|
||||
});
|
||||
|
||||
describe("GeneralSection", () => {
|
||||
it("hides deprecated built-ins from the workflow enablement toggles", async () => {
|
||||
vi.mocked(fetchWorkflows).mockResolvedValue([
|
||||
{ id: "builtin:coding", name: "Coding", kind: "workflow", ir: {} },
|
||||
{ id: "builtin:brainstorming", name: "Brainstorming", kind: "workflow", ir: {} },
|
||||
] as never);
|
||||
|
||||
render(
|
||||
<GeneralSection
|
||||
scopeBanner={null}
|
||||
form={emptyForm}
|
||||
setForm={vi.fn()}
|
||||
addToast={vi.fn()}
|
||||
prefixError={null}
|
||||
setPrefixError={vi.fn()}
|
||||
projectTrackingRepoOptions={[]}
|
||||
projectTrackingRepoLoading={false}
|
||||
projectTrackingRepoError={null}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => expect(screen.getByLabelText("Coding")).toBeInTheDocument());
|
||||
expect(screen.queryByLabelText("Brainstorming")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("emits the absolute file-browser path toggle via setForm", () => {
|
||||
function GeneralHost() {
|
||||
const [form, setForm] = useState({ allowAbsoluteFileBrowserPaths: false } as SettingsFormState);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
||||
import { isLocale, SUPPORTED_LOCALES, type WorkflowDefinition } from "@fusion/core";
|
||||
import { DEPRECATED_BUILTIN_WORKFLOW_IDS, isLocale, SUPPORTED_LOCALES, type WorkflowDefinition } from "@fusion/core";
|
||||
import { SettingsToggleRow } from "../SettingsToggleRow";
|
||||
import { SettingsSelectRow } from "../SettingsSelectRow";
|
||||
/*
|
||||
@@ -35,7 +35,12 @@ export function GeneralSection({ scopeBanner, form, setForm, projectId, addToast
|
||||
fetchWorkflows(projectId, { includeDisabledBuiltins: true })
|
||||
.then((workflows) => {
|
||||
if (!cancelled) {
|
||||
setBuiltinWorkflows(workflows.filter((workflow) => workflow.id.startsWith("builtin:") && workflow.kind !== "fragment"));
|
||||
// FNXC:WorkflowBrainstorming 2026-07-15-15:49: FN-7970 keeps deprecated built-ins out of Settings toggles, which are a new-selection surface.
|
||||
setBuiltinWorkflows(workflows.filter(
|
||||
(workflow) => workflow.id.startsWith("builtin:")
|
||||
&& workflow.kind !== "fragment"
|
||||
&& !DEPRECATED_BUILTIN_WORKFLOW_IDS.has(workflow.id),
|
||||
));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
|
||||
Reference in New Issue
Block a user