From 3dd227b945faf12cad82d333ec71fedb00ba850c Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 4 Jul 2026 20:28:11 -0700 Subject: [PATCH] FN-7557: default plan approval mode to auto-approve-all Changes the project-wide plan approval default from deferring to per-workflow settings to auto-approving all task plans, so new/unset projects skip the manual awaiting-approval gate by default. - Change DEFAULT_PROJECT_SETTINGS.planApprovalMode default from "workflow" to "auto-approve-all" in settings-schema.ts, with FNXC comments documenting the requirement change - Update ProjectSettings.planApprovalMode JSDoc in types.ts to reflect the new default - Update useAppSettings hook's initial state and hydration fallback to default to "auto-approve-all" while still honoring an explicit stored "workflow" value - Update MergeSection UI: move the "(default)" label from the "Use workflow setting" option to "Auto-approve all tasks", keeping the select's fallback value in sync - Update settings-reference.md docs and i18n locale/resource strings to match the new default label - Update existing tests (MergeSection legacy auto-merge cleanup, settings default descriptions, useAppSettings) to assert the new default, and add coverage for the updated hydration/fallback behavior - Add changeset fn-7557-plan-auto-approve-default.md documenting the behavior change Files changed: .changeset/fn-7557-plan-auto-approve-default.md | 7 +++++ docs/settings-reference.md | 2 +- packages/core/src/settings-schema.ts | 6 +++- packages/core/src/types.ts | 3 ++ .../dashboard/app/components/SettingsModal.tsx | 3 +- .../components/settings/sections/MergeSection.tsx | 9 ++++-- .../MergeSection.legacy-automerge-cleanup.test.tsx | 4 +-- .../settings-default-descriptions.test.tsx | 3 +- .../app/hooks/__tests__/useAppSettings.test.ts | 35 +++++++++++++++++++--- packages/dashboard/app/hooks/useAppSettings.ts | 12 ++++++-- packages/i18n/locales/en/app.json | 4 +-- packages/i18n/src/resources.d.ts | 2 +- 12 files changed, 71 insertions(+), 19 deletions(-) Fusion-Task-Id: FN-7557 Fusion-Task-Lineage: 7dcfe339-6088-4ebc-8387-eb81258a693d Co-authored-by: Fusion (runfusion.ai) --- .../fn-7557-plan-auto-approve-default.md | 7 ++++ docs/settings-reference.md | 2 +- packages/core/src/settings-schema.ts | 6 +++- packages/core/src/types.ts | 3 ++ .../app/components/SettingsModal.tsx | 3 +- .../settings/sections/MergeSection.tsx | 9 +++-- ...eSection.legacy-automerge-cleanup.test.tsx | 4 +-- .../settings-default-descriptions.test.tsx | 3 +- .../hooks/__tests__/useAppSettings.test.ts | 35 ++++++++++++++++--- .../dashboard/app/hooks/useAppSettings.ts | 12 +++++-- packages/i18n/locales/en/app.json | 4 +-- packages/i18n/src/resources.d.ts | 2 +- 12 files changed, 71 insertions(+), 19 deletions(-) create mode 100644 .changeset/fn-7557-plan-auto-approve-default.md diff --git a/.changeset/fn-7557-plan-auto-approve-default.md b/.changeset/fn-7557-plan-auto-approve-default.md new file mode 100644 index 0000000000..f849b72911 --- /dev/null +++ b/.changeset/fn-7557-plan-auto-approve-default.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Plan auto-approval is now the default; specified tasks skip manual approval unless you opt into workflow/require-all. +category: feature +dev: `DEFAULT_PROJECT_SETTINGS.planApprovalMode` flips `workflow` → `auto-approve-all`; existing projects with an explicit stored value are unchanged; consumed by `resolvePlanApprovalRequired` at the triage gating sites. diff --git a/docs/settings-reference.md b/docs/settings-reference.md index 0b59b4ab99..890fb8861f 100644 --- a/docs/settings-reference.md +++ b/docs/settings-reference.md @@ -419,7 +419,7 @@ Security-sensitive file-browser escape hatches are project-only. `allowAbsoluteF | `overlapIgnorePaths` | `string[]` | `[]` | Optional project-relative file or directory paths to exclude from overlap blocking (for example `docs` or `generated/openapi.json`). Entries are trimmed, deduplicated, and must not be absolute or contain `..` traversal. | | `allowAbsoluteFileBrowserPaths` | `boolean` | `false` | Project-scoped Settings → General toggle for the workspace file browser. When enabled, slash-prefixed paths such as `/tmp` can be listed/read/written/downloaded through workspace file-browser routes while keeping existing file-size, binary, type, null-byte, traversal, and permission checks. Windows drive-letter paths remain blocked, and task-local file routes, memory APIs, worktree-copy validation, plugin bundle paths, and other validators are unchanged. | | `autoMerge` | `boolean` | `true` | Auto-finalize tasks from `in-review`. Tasks can override this per-task (including at create time in New Task modal via **Auto-merge** = Default/Enabled/Disabled); explicit overrides are tagged with `autoMergeProvenance: "user"`, while tasks left at **Default** keep following the live global setting and do not snapshot it when entering review. Legacy pre-FN-6245 in-review rows that were stamped `autoMerge: true` are marked `autoMergeProvenance: "legacy-stamp"` on startup and can be inspected/cleared with Settings → Merge → **Legacy auto-merge stamp cleanup**, `fn pr automerge-cleanup [--apply] [--json]`, or `reconcileLegacyAutoMergeStamps({ apply: true })` after operator review. For grouped branch flows, per-task `autoMerge` governs member→group-integration landing while group `autoMerge` governs group→default-branch promotion eligibility. | -| `planApprovalMode` | `"workflow" \| "auto-approve-all" \| "require-all"` | `"workflow"` | Project-scoped override for the manual planning approval gate. `"workflow"` preserves the workflow-resolved `requirePlanApproval`; `"auto-approve-all"` moves every successfully specified task to `todo` without manual plan approval even when the selected workflow or stored workflow setting has `requirePlanApproval: true`; `"require-all"` parks every specified task at `status: "awaiting-approval"` regardless of workflow settings. Settings → Merge remains the full three-state editor; the Board Triage/intake **Auto-approve plan** switch is a binary shortcut for `"auto-approve-all"` vs `"workflow"`. This does not disable Workflow Plan Review, release authorization, or other non-plan safety gates. | +| `planApprovalMode` | `"workflow" \| "auto-approve-all" \| "require-all"` | `"auto-approve-all"` | Project-scoped override for the manual planning approval gate. Defaults to auto-approve-all (FN-7557) so new/unset projects skip the manual gate; `"workflow"` instead preserves the workflow-resolved `requirePlanApproval`; `"auto-approve-all"` moves every successfully specified task to `todo` without manual plan approval even when the selected workflow or stored workflow setting has `requirePlanApproval: true`; `"require-all"` parks every specified task at `status: "awaiting-approval"` regardless of workflow settings. Settings → Merge remains the full three-state editor; the Board Triage/intake **Auto-approve plan** switch is a binary shortcut for `"auto-approve-all"` vs `"workflow"`. This does not disable Workflow Plan Review, release authorization, or other non-plan safety gates. | | `maxAutoMergeRetries` | `number` | `3` | Project-scoped positive-integer cap for auto-merge conflict-resolution retries before Fusion parks or bounces a task for human/recovery handling. Unset, non-finite, zero, or negative values fall back to `3` to preserve historical behavior. | | `mergeRequestContractShadowEnabled` | `boolean` | `false` | Phase-1 FN-5741 write-only shadow flag (project/global setting). When enabled, executor/self-healing/merger persist merge-request records and `completion_handoff_accepted` markers for observation only; legacy mergeQueue + lifecycle remains authoritative. | | `mergeStrategy` | `"direct" \| "pull-request"` | `"direct"` | Completion mode (local direct merge vs PR-first). | diff --git a/packages/core/src/settings-schema.ts b/packages/core/src/settings-schema.ts index 714bc159a4..77aef733a4 100644 --- a/packages/core/src/settings-schema.ts +++ b/packages/core/src/settings-schema.ts @@ -321,7 +321,11 @@ export const DEFAULT_PROJECT_SETTINGS = { */ allowAbsoluteFileBrowserPaths: false, autoMerge: true, - planApprovalMode: "workflow", + /* + FNXC:PlanApproval 2026-07-04-00:00: + FN-7557: plan auto-approval is the default project posture; unset projects bypass the manual awaiting-approval gate. Previously defaulted to "workflow" (deferring to each workflow's requirePlanApproval); projects with an explicit stored value are unaffected. + */ + planApprovalMode: "auto-approve-all", // U18 (R15): the Review-response loop is default-on. Independent of `autoMerge` — // with this on but auto-merge off, review threads are resolved but the PR is not merged. autoResolveReviewComments: true, diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 674891e498..b389202104 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -4147,6 +4147,9 @@ export interface ProjectSettings { /** * FNXC:PlanApproval 2026-06-26-00:00: * Per-project setting to control plan approval for every task: workflow defers to the per-workflow requirePlanApproval setting, auto-approve-all bypasses approval for all tasks, and require-all parks every approved spec for manual approval. + * + * FNXC:PlanApproval 2026-07-04-00:00: + * FN-7557: default is now "auto-approve-all" (previously deferred to workflow via "workflow"). Unset/new projects bypass the manual awaiting-approval gate by default; projects with an explicit stored value are unaffected. */ planApprovalMode?: "workflow" | "auto-approve-all" | "require-all"; /** Controls task-worker execution mode. diff --git a/packages/dashboard/app/components/SettingsModal.tsx b/packages/dashboard/app/components/SettingsModal.tsx index d3a272175a..5737f86e5b 100644 --- a/packages/dashboard/app/components/SettingsModal.tsx +++ b/packages/dashboard/app/components/SettingsModal.tsx @@ -893,7 +893,8 @@ export function SettingsModal({ overlapIgnorePaths: [], allowAbsoluteFileBrowserPaths: false, autoMerge: true, - planApprovalMode: "workflow", + // FNXC:PlanApproval 2026-07-04-00:00: FN-7557: local fallback mirrors DEFAULT_PROJECT_SETTINGS — auto-approve-all is now the default project posture. + planApprovalMode: "auto-approve-all", mergeStrategy: "direct", maxAutoMergeRetries: 3, mergeIntegrationWorktree: "reuse-task-worktree", diff --git a/packages/dashboard/app/components/settings/sections/MergeSection.tsx b/packages/dashboard/app/components/settings/sections/MergeSection.tsx index e19b3afb53..5f9e4f7b5e 100644 --- a/packages/dashboard/app/components/settings/sections/MergeSection.tsx +++ b/packages/dashboard/app/components/settings/sections/MergeSection.tsx @@ -96,14 +96,17 @@ export function MergeSection({ scopeBanner, form, setForm, integrationBranchOpti {/* FNXC:PlanApproval 2026-06-26-00:00: Operators need one project-scoped control beside review/merge policy to force all tasks to auto-approve or require manual plan approval without editing each workflow. + + FNXC:PlanApproval 2026-07-04-00:00: + FN-7557: auto-approve-all is now the project default (previously workflow), so the select fallback and "(default)" label marker move to the auto-approve option to keep the dropdown truthful. */} - { const nextMode = e.target.value as Settings["planApprovalMode"]; setForm((f) => ({ ...f, planApprovalMode: nextMode })); }} data-testid="plan-approval-mode-select"> - - + +
diff --git a/packages/dashboard/app/components/settings/sections/__tests__/MergeSection.legacy-automerge-cleanup.test.tsx b/packages/dashboard/app/components/settings/sections/__tests__/MergeSection.legacy-automerge-cleanup.test.tsx index cc3a2e7c73..b0b997e9e5 100644 --- a/packages/dashboard/app/components/settings/sections/__tests__/MergeSection.legacy-automerge-cleanup.test.tsx +++ b/packages/dashboard/app/components/settings/sections/__tests__/MergeSection.legacy-automerge-cleanup.test.tsx @@ -48,8 +48,8 @@ describe("MergeSection legacy auto-merge stamp cleanup", () => { const select = screen.getByTestId("plan-approval-mode-select") as HTMLSelectElement; expect(select.value).toBe("require-all"); - expect(screen.getByRole("option", { name: "Use workflow setting (default)" })).toBeInTheDocument(); - expect(screen.getByRole("option", { name: "Auto-approve all tasks" })).toBeInTheDocument(); + expect(screen.getByRole("option", { name: "Use workflow setting" })).toBeInTheDocument(); + expect(screen.getByRole("option", { name: "Auto-approve all tasks (default)" })).toBeInTheDocument(); expect(screen.getByRole("option", { name: "Require approval for all tasks" })).toBeInTheDocument(); fireEvent.change(select, { target: { value: "auto-approve-all" } }); diff --git a/packages/dashboard/app/components/settings/sections/__tests__/settings-default-descriptions.test.tsx b/packages/dashboard/app/components/settings/sections/__tests__/settings-default-descriptions.test.tsx index 7b4af31567..77ae215c71 100644 --- a/packages/dashboard/app/components/settings/sections/__tests__/settings-default-descriptions.test.tsx +++ b/packages/dashboard/app/components/settings/sections/__tests__/settings-default-descriptions.test.tsx @@ -163,7 +163,8 @@ const SETTING_DESCRIPTION_KEYS: Record = { memoryBackendType: "memory.agentsGetMemorySearchMemoryGetAndMemory", // MergeSection autoMerge: "merge.whenEnabledTasksThatPassReviewAreAutomatically", - planApprovalMode: "merge.planApprovalModeWorkflow", + // FN-7557: planApprovalMode defaults to auto-approve-all; the "(default)" marker moved to the auto-approve option. + planApprovalMode: "merge.planApprovalModeAutoApproveAll", maxAutoMergeRetries: "merge.positiveIntegerRetryCapForAutoMergeConflict", merger: "merge.dangerousCompatibilityEscapeHatchLeaveOffUnlessYou", testMode: "merge.forcesAllAILanesToUseTheDeterministic", diff --git a/packages/dashboard/app/hooks/__tests__/useAppSettings.test.ts b/packages/dashboard/app/hooks/__tests__/useAppSettings.test.ts index 45c756edf3..634dc1ef5a 100644 --- a/packages/dashboard/app/hooks/__tests__/useAppSettings.test.ts +++ b/packages/dashboard/app/hooks/__tests__/useAppSettings.test.ts @@ -1,8 +1,17 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { act, renderHook, waitFor } from "@testing-library/react"; +import { DEFAULT_PROJECT_SETTINGS } from "@fusion/core"; import { useAppSettings } from "../useAppSettings"; import * as api from "../../api"; +// FN-7557: locks the project-default plan-approval posture so an accidental revert to +// "workflow" fails this test instead of silently reintroducing the manual approval gate. +describe("DEFAULT_PROJECT_SETTINGS.planApprovalMode", () => { + it("defaults to auto-approve-all", () => { + expect(DEFAULT_PROJECT_SETTINGS.planApprovalMode).toBe("auto-approve-all"); + }); +}); + vi.mock("../../api", () => ({ fetchConfig: vi.fn(), fetchSettings: vi.fn(), @@ -32,7 +41,6 @@ describe("useAppSettings", () => { staleHighFanoutBlockerAgeThresholdMs: 7200000, showQuickChatFAB: false, capacityRiskBannerEnabled: false, - planApprovalMode: "workflow", } as never); mockUpdateSettings.mockResolvedValue({} as never); @@ -57,8 +65,8 @@ describe("useAppSettings", () => { expect(result.current.quickChatCloseOnOutsideClick).toBe(true); expect(result.current.capacityRiskBannerEnabled).toBe(false); expect(result.current.capacityRiskTodoThreshold).toBe(20); - expect(result.current.planApprovalMode).toBe("workflow"); - expect(result.current.planAutoApproveEnabled).toBe(false); + expect(result.current.planApprovalMode).toBe("auto-approve-all"); + expect(result.current.planAutoApproveEnabled).toBe(true); }); expect(mockFetchConfig).toHaveBeenCalledWith("proj_123"); @@ -66,7 +74,7 @@ describe("useAppSettings", () => { }); it.each([ - [undefined, "workflow", false], + [undefined, "auto-approve-all", true], ["workflow", "workflow", false], ["auto-approve-all", "auto-approve-all", true], ["require-all", "require-all", false], @@ -90,6 +98,16 @@ describe("useAppSettings", () => { }); it("optimistically enables plan auto-approval and persists to API", async () => { + mockFetchSettings.mockResolvedValueOnce({ + autoMerge: false, + globalPause: true, + enginePaused: false, + prAuthAvailable: true, + taskStuckTimeoutMs: 600000, + showQuickChatFAB: false, + planApprovalMode: "workflow", + } as never); + const { result } = renderHook(() => useAppSettings("proj_123")); await waitFor(() => { @@ -158,6 +176,15 @@ describe("useAppSettings", () => { }); it("rolls back optimistic plan auto-approval state when toggle update fails", async () => { + mockFetchSettings.mockResolvedValueOnce({ + autoMerge: false, + globalPause: true, + enginePaused: false, + prAuthAvailable: true, + taskStuckTimeoutMs: 600000, + showQuickChatFAB: false, + planApprovalMode: "workflow", + } as never); mockUpdateSettings.mockRejectedValueOnce(new Error("network")); const { result } = renderHook(() => useAppSettings("proj_123")); diff --git a/packages/dashboard/app/hooks/useAppSettings.ts b/packages/dashboard/app/hooks/useAppSettings.ts index 5384ed5711..8d528262cd 100644 --- a/packages/dashboard/app/hooks/useAppSettings.ts +++ b/packages/dashboard/app/hooks/useAppSettings.ts @@ -63,7 +63,11 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult { const [rootDir, setRootDir] = useState("."); const [autoMerge, setAutoMerge] = useState(true); const [mergeStrategy, setMergeStrategy] = useState("direct"); - const [planApprovalMode, setPlanApprovalMode] = useState("workflow"); + /* + FNXC:PlanApproval 2026-07-04-00:00: + FN-7557: plan auto-approval is the default project posture; the pre-hydration state and any genuinely unset/invalid server value fall back to "auto-approve-all" instead of "workflow". Explicit server values ("workflow", "auto-approve-all", "require-all") are preserved during hydration below. + */ + const [planApprovalMode, setPlanApprovalMode] = useState("auto-approve-all"); const [showWorktreeGrouping, setShowWorktreeGrouping] = useState(false); const [testMode, setTestMode] = useState(false); const [isTestMode, setIsTestMode] = useState(false); @@ -118,9 +122,11 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult { */ setMergeStrategy(typeof settings.mergeStrategy === "string" ? settings.mergeStrategy : "direct"); const nextPlanApprovalMode: PlanApprovalMode = - settings.planApprovalMode === "auto-approve-all" || settings.planApprovalMode === "require-all" + settings.planApprovalMode === "auto-approve-all" || + settings.planApprovalMode === "require-all" || + settings.planApprovalMode === "workflow" ? settings.planApprovalMode - : "workflow"; + : "auto-approve-all"; planApprovalModeRef.current = nextPlanApprovalMode; setPlanApprovalMode(nextPlanApprovalMode); setShowWorktreeGrouping(settings.showWorktreeGrouping === true); diff --git a/packages/i18n/locales/en/app.json b/packages/i18n/locales/en/app.json index f6b4d8534d..8fe2cc183b 100644 --- a/packages/i18n/locales/en/app.json +++ b/packages/i18n/locales/en/app.json @@ -6155,10 +6155,10 @@ "pickALocalBranchFromTheDropdownCommon": "). Pick a local branch from the dropdown — common integration names like ", "positiveIntegerRetryCapForAutoMergeConflict": "Positive integer retry cap for auto-merge conflict resolution before a task parks for human recovery. Default 3.", "planApprovalMode": "Plan approval mode", - "planApprovalModeAutoApproveAll": "Auto-approve all tasks", + "planApprovalModeAutoApproveAll": "Auto-approve all tasks (default)", "planApprovalModeHelp": "Project-wide override for the planning approval gate. Leave on workflow to use each workflow's Require plan approval setting, or force all approved specs to bypass or wait for manual approval.", "planApprovalModeRequireAll": "Require approval for all tasks", - "planApprovalModeWorkflow": "Use workflow setting (default)", + "planApprovalModeWorkflow": "Use workflow setting", "postMergeAuditMode": "Post-merge audit mode", "pushRemote": "Push Remote", "pushToRemoteAfterMerge": " Push to remote after merge ", diff --git a/packages/i18n/src/resources.d.ts b/packages/i18n/src/resources.d.ts index 7449e9e944..a85f6426be 100644 --- a/packages/i18n/src/resources.d.ts +++ b/packages/i18n/src/resources.d.ts @@ -6129,7 +6129,7 @@ export default interface Resources { "personalAccessToken": "Personal access token", "pickALocalBranchFromTheDropdownCommon": "). Pick a local branch from the dropdown — common integration names like ", "planApprovalMode": "Plan approval mode", - "planApprovalModeAutoApproveAll": "Auto-approve all tasks", + "planApprovalModeAutoApproveAll": "Auto-approve all tasks (default)", "planApprovalModeHelp": "Project-wide override for the planning approval gate. Leave on workflow to use each workflow's Require plan approval setting, or force all approved specs to bypass or wait for manual approval.", "planApprovalModeRequireAll": "Require approval for all tasks", "planApprovalModeWorkflow": "Use workflow setting",