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) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7557-plan-auto-approve-default.md
Normal file
7
.changeset/fn-7557-plan-auto-approve-default.md
Normal file
@@ -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.
|
||||
@@ -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). |
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
*/}
|
||||
<label htmlFor="planApprovalMode">{t("settings.merge.planApprovalMode", "Plan approval mode")}</label>
|
||||
<select id="planApprovalMode" className="select" value={form.planApprovalMode ?? "workflow"} onChange={(e) => {
|
||||
<select id="planApprovalMode" className="select" value={form.planApprovalMode ?? "auto-approve-all"} onChange={(e) => {
|
||||
const nextMode = e.target.value as Settings["planApprovalMode"];
|
||||
setForm((f) => ({ ...f, planApprovalMode: nextMode }));
|
||||
}} data-testid="plan-approval-mode-select">
|
||||
<option value="workflow">{t("settings.merge.planApprovalModeWorkflow", "Use workflow setting (default)")}</option>
|
||||
<option value="auto-approve-all">{t("settings.merge.planApprovalModeAutoApproveAll", "Auto-approve all tasks")}</option>
|
||||
<option value="workflow">{t("settings.merge.planApprovalModeWorkflow", "Use workflow setting")}</option>
|
||||
<option value="auto-approve-all">{t("settings.merge.planApprovalModeAutoApproveAll", "Auto-approve all tasks (default)")}</option>
|
||||
<option value="require-all">{t("settings.merge.planApprovalModeRequireAll", "Require approval for all tasks")}</option>
|
||||
</select>
|
||||
<details className="settings-option-details">
|
||||
|
||||
@@ -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" } });
|
||||
|
||||
@@ -163,7 +163,8 @@ const SETTING_DESCRIPTION_KEYS: Record<string, string> = {
|
||||
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",
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -63,7 +63,11 @@ export function useAppSettings(projectId?: string): UseAppSettingsResult {
|
||||
const [rootDir, setRootDir] = useState<string>(".");
|
||||
const [autoMerge, setAutoMerge] = useState(true);
|
||||
const [mergeStrategy, setMergeStrategy] = useState("direct");
|
||||
const [planApprovalMode, setPlanApprovalMode] = useState<PlanApprovalMode>("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<PlanApprovalMode>("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);
|
||||
|
||||
@@ -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 ",
|
||||
|
||||
2
packages/i18n/src/resources.d.ts
vendored
2
packages/i18n/src/resources.d.ts
vendored
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user