diff --git a/.changeset/fn-7099-plan-approval-mode.md b/.changeset/fn-7099-plan-approval-mode.md new file mode 100644 index 0000000000..7b06b857d2 --- /dev/null +++ b/.changeset/fn-7099-plan-approval-mode.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Add a per-project plan-approval mode to auto-approve or require approval for all tasks. +category: feature +dev: New project setting `planApprovalMode` ("workflow" | "auto-approve-all" | "require-all"); overrides the per-workflow `requirePlanApproval` via `resolvePlanApprovalRequired` at the triage gating sites. diff --git a/docs/settings-reference.md b/docs/settings-reference.md index cf38359116..f72bd6b828 100644 --- a/docs/settings-reference.md +++ b/docs/settings-reference.md @@ -297,7 +297,7 @@ These groups moved out of project settings and into workflow settings (built-in | Group | Keys (examples) | |---|---| | **Step execution** | `workflowStepTimeoutMs`, `runStepsInNewSessions`, `maxParallelSteps`, `workflowStepScopeEnforcement`, `strictScopeEnforcement`, `verificationFixRetries`, `maxPostReviewFixes`, `buildRetryCount` | -| **Review / approval** | `requirePrApproval`, `requirePlanApproval`, `reviewHandoffPolicy`, `maxReviewerContextRetries`, `maxReviewerFallbackRetries` | +| **Review / approval** | Workflow values: `requirePrApproval`, `requirePlanApproval`, `reviewHandoffPolicy`, `maxReviewerContextRetries`, `maxReviewerFallbackRetries`; project override: `planApprovalMode` | | **Per-phase model lanes** | `executionProvider`/`executionModelId`, `planningProvider`/`planningModelId` (+ fallbacks), `validatorProvider`/`validatorModelId` (+ fallbacks) | ### Workflow-native triage policy settings @@ -378,6 +378,7 @@ Defaults from `DEFAULT_PROJECT_SETTINGS`; key scope from `PROJECT_SETTINGS_KEYS` | `ignoreHiddenOverlapPaths` | `boolean` | `true` | Exclude hidden dot paths from overlap serialization by default. A hidden path is any normalized project-relative path with a segment beginning with `.`, such as `.fusion/tasks/FN-1/PROMPT.md`, `.changeset/fix.md`, `.github/workflows/ci.yml`, `.env`, or `packages/.cache/out.js`. Set to `false` to restore legacy strict counting of dot paths. Explicit `overlapIgnorePaths` entries still apply in addition to this default filter, and still apply when hidden-path filtering is disabled. | | `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. | | `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 planning approval gate. `"workflow"` preserves the workflow-resolved `requirePlanApproval`; `"auto-approve-all"` moves every approved spec to todo without manual approval; `"require-all"` parks every approved spec at `status: "awaiting-approval"` regardless of workflow settings. | | `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/docs/storage.md b/docs/storage.md index db4edcaaf4..a7db2a49f3 100644 --- a/docs/storage.md +++ b/docs/storage.md @@ -222,9 +222,9 @@ Important execution nuance: ## 1) Summary - **localStorage keys in runtime dashboard code:** **20** -- **Backend settings keys defined in `@fusion/core`:** **78** total +- **Backend settings keys defined in `@fusion/core`:** **79** total - **Global settings:** 17 (`GlobalSettings`) - - **Project settings:** 61 (`ProjectSettings`) + - **Project settings:** 62 (`ProjectSettings`) - **SQLite tables in project DB schema (`packages/core/src/db.ts`):** **47** (including migration-created tables) - **Issues identified:** **9** - High: 2 @@ -320,6 +320,7 @@ API endpoints reviewed: | `groupOverlappingFiles` | Project | `GET/PUT /api/settings` | Serialize overlapping file work | | `overlapIgnorePaths` | Project | `GET/PUT /api/settings` | Project-relative file/directory paths ignored by overlap blocking | | `autoMerge` | Project | `GET/PUT /api/settings` | Enable auto merge | +| `planApprovalMode` | Project | `GET/PUT /api/settings` | Project-wide plan approval override: `workflow`, `auto-approve-all`, or `require-all` | | `mergeStrategy` | Project | `GET/PUT /api/settings` | Direct vs PR merge strategy | | `worktreeInitCommand` | Project | `GET/PUT /api/settings` | Command run on worktree init | | `testCommand` | Project | `GET/PUT /api/settings` | Project test command | diff --git a/packages/core/src/__tests__/plan-approval.test.ts b/packages/core/src/__tests__/plan-approval.test.ts new file mode 100644 index 0000000000..418ca0d5e8 --- /dev/null +++ b/packages/core/src/__tests__/plan-approval.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from "vitest"; +import { resolvePlanApprovalRequired, type PlanApprovalMode } from "../plan-approval.js"; + +const workflowValues = [true, false, undefined] as const; + +describe("resolvePlanApprovalRequired", () => { + it.each(workflowValues)("defers to requirePlanApproval when mode is workflow and workflow value is %s", (requirePlanApproval) => { + expect(resolvePlanApprovalRequired({ planApprovalMode: "workflow", requirePlanApproval })).toBe(Boolean(requirePlanApproval)); + }); + + it.each(workflowValues)("defers to requirePlanApproval when mode is undefined and workflow value is %s", (requirePlanApproval) => { + expect(resolvePlanApprovalRequired({ requirePlanApproval })).toBe(Boolean(requirePlanApproval)); + }); + + it.each(workflowValues)("auto-approve-all bypasses approval when workflow value is %s", (requirePlanApproval) => { + expect(resolvePlanApprovalRequired({ planApprovalMode: "auto-approve-all", requirePlanApproval })).toBe(false); + }); + + it.each(workflowValues)("require-all requires approval when workflow value is %s", (requirePlanApproval) => { + expect(resolvePlanApprovalRequired({ planApprovalMode: "require-all", requirePlanApproval })).toBe(true); + }); + + it("falls back to workflow behavior for unknown persisted modes", () => { + expect( + resolvePlanApprovalRequired({ + planApprovalMode: "future-mode" as PlanApprovalMode, + requirePlanApproval: true, + }), + ).toBe(true); + expect( + resolvePlanApprovalRequired({ + planApprovalMode: "future-mode" as PlanApprovalMode, + requirePlanApproval: false, + }), + ).toBe(false); + }); +}); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 5ba4a5cc44..482c117ee8 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -17,6 +17,8 @@ export type { } from "./branch-assignment.js"; export { customProviderRegistryKey } from "./custom-provider-key.js"; export { redactSecrets } from "./redact-secrets.js"; +export { resolvePlanApprovalRequired } from "./plan-approval.js"; +export type { PlanApprovalMode } from "./plan-approval.js"; export { isActiveNearDuplicateColumn, isNearDuplicateCanonicalInactive } from "./near-duplicate-canonical.js"; export type { NearDuplicateCanonicalState } from "./near-duplicate-canonical.js"; export * from "./frontend-ux-policy.js"; diff --git a/packages/core/src/plan-approval.ts b/packages/core/src/plan-approval.ts new file mode 100644 index 0000000000..13041bb29c --- /dev/null +++ b/packages/core/src/plan-approval.ts @@ -0,0 +1,21 @@ +import type { ProjectSettings } from "./types.js"; + +export type PlanApprovalMode = NonNullable; + +/** + * FNXC:PlanApproval 2026-06-26-00:00: + * Per-project planApprovalMode controls the planning approval gate for every task in the project: require-all always parks approved specs for manual approval, auto-approve-all always bypasses the gate, and workflow/undefined preserves the workflow-resolved requirePlanApproval value. + */ +export function resolvePlanApprovalRequired( + settings: Pick, +): boolean { + switch (settings.planApprovalMode) { + case "require-all": + return true; + case "auto-approve-all": + return false; + case "workflow": + default: + return Boolean(settings.requirePlanApproval); + } +} diff --git a/packages/core/src/settings-schema.ts b/packages/core/src/settings-schema.ts index aa60d48b05..0e917cd48a 100644 --- a/packages/core/src/settings-schema.ts +++ b/packages/core/src/settings-schema.ts @@ -283,6 +283,7 @@ export const DEFAULT_PROJECT_SETTINGS = { ignoreHiddenOverlapPaths: true, overlapIgnorePaths: [], autoMerge: true, + planApprovalMode: "workflow", // 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, @@ -382,7 +383,7 @@ export const DEFAULT_PROJECT_SETTINGS = { // planOnlyScopeLeakEnforcement, workflowRevisionForkOnScopeMismatch, // strictScopeEnforcement, buildRetryCount, verificationFixRetries, // requirePlanApproval) MOVED to workflow settings (U4) — see - // MOVED_SETTINGS_KEYS. `buildTimeoutMs` and `verificationCommandTimeoutMs` + // MOVED_SETTINGS_KEYS. `planApprovalMode`, `buildTimeoutMs`, and `verificationCommandTimeoutMs` // are NOT moved and stay plain project settings. Keep verificationCommandTimeoutMs // undefined so fn_run_verification preserves legacy per-scope defaults until a // project opts into a single default budget. diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 1967a7af1e..f0ca6bf240 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -3911,6 +3911,11 @@ export interface ProjectSettings { * remain in triage with status "awaiting-approval" until a user approves * or rejects the plan. Default: false. */ requirePlanApproval?: boolean; + /** + * 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. + */ + planApprovalMode?: "workflow" | "auto-approve-all" | "require-all"; /** Controls task-worker execution mode. * - true (default): spawn short-lived `executor-FN-XXXX` ephemeral workers per task * - false: disable ephemeral workers; scheduler auto-assigns dispatchable tasks diff --git a/packages/dashboard/app/components/SettingsModal.tsx b/packages/dashboard/app/components/SettingsModal.tsx index 57308cad92..7423df5f7f 100644 --- a/packages/dashboard/app/components/SettingsModal.tsx +++ b/packages/dashboard/app/components/SettingsModal.tsx @@ -725,6 +725,7 @@ export function SettingsModal({ ignoreHiddenOverlapPaths: true, overlapIgnorePaths: [], autoMerge: true, + planApprovalMode: "workflow", mergeStrategy: "direct", maxAutoMergeRetries: 3, mergeIntegrationWorktree: "reuse-task-worktree", diff --git a/packages/dashboard/app/components/__tests__/SettingsModal.scheduling-merge.test.tsx b/packages/dashboard/app/components/__tests__/SettingsModal.scheduling-merge.test.tsx index 0f844d3154..13a2bbc89d 100644 --- a/packages/dashboard/app/components/__tests__/SettingsModal.scheduling-merge.test.tsx +++ b/packages/dashboard/app/components/__tests__/SettingsModal.scheduling-merge.test.tsx @@ -1257,6 +1257,23 @@ describe("SettingsModal", () => { expect(payload.pushAfterMerge).toBe(true); expect(payload.pushRemote).toBe("upstream main"); }); + + it("round-trips plan approval mode through project settings save", async () => { + const select = screen.getByLabelText("Plan approval mode"); + expect(select).toHaveValue("workflow"); + + await settingsModalUser.selectOptions(select, "require-all"); + await settingsModalUser.click(screen.getByText("Save")); + + await waitFor(() => { + expect(mockUpdateSettings).toHaveBeenCalledTimes(1); + }); + + const payload = mockUpdateSettings.mock.calls[0][0] as Record; + expect(payload.planApprovalMode).toBe("require-all"); + expect(mockUpdateGlobalSettings).toHaveBeenCalledTimes(1); + expect(mockUpdateGlobalSettings.mock.calls[0][0]).not.toHaveProperty("planApprovalMode"); + }); }); describe("verificationFixRetries (moved to workflow settings)", () => { diff --git a/packages/dashboard/app/components/__tests__/SettingsModal.test-harness.tsx b/packages/dashboard/app/components/__tests__/SettingsModal.test-harness.tsx index 7e30284e96..4316e00787 100644 --- a/packages/dashboard/app/components/__tests__/SettingsModal.test-harness.tsx +++ b/packages/dashboard/app/components/__tests__/SettingsModal.test-harness.tsx @@ -95,6 +95,7 @@ export const defaultSettings = { ignoreHiddenOverlapPaths: true, overlapIgnorePaths: [], autoMerge: true, + planApprovalMode: "workflow", mergeStrategy: "direct", merger: { mode: "deterministic" }, directMergeCommitStrategy: "auto", diff --git a/packages/dashboard/app/components/settings/sections/MergeSection.tsx b/packages/dashboard/app/components/settings/sections/MergeSection.tsx index e558b846d4..e2dcd13af5 100644 --- a/packages/dashboard/app/components/settings/sections/MergeSection.tsx +++ b/packages/dashboard/app/components/settings/sections/MergeSection.tsx @@ -92,6 +92,25 @@ export function MergeSection({ scopeBanner, form, setForm, integrationBranchOpti {t("settings.merge.whenEnabledTasksThatPassReviewAreAutomatically", "When enabled, tasks that pass review are automatically merged into the main branch")} +
+ {/* + 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. + */} + + +
+ {t("settings.merge.moreDetails", "More details")} + {t("settings.merge.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.")} +
+
{/* 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 25c8e71bc3..2b8d924458 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 @@ -15,14 +15,16 @@ function jsonResponse(body: unknown, ok = true): Response { } as Response; } -function makeProps(): MergeSectionProps { +function makeProps(overrides: Partial = {}): MergeSectionProps { return { scopeBanner: null, form: { autoMerge: true, + planApprovalMode: "workflow", merger: { mode: "ai" }, testMode: false, mergeStrategy: "direct", + ...overrides, } as MergeSectionProps["form"], setForm: vi.fn(), integrationBranchOptions: ["main"], @@ -38,6 +40,25 @@ describe("MergeSection legacy auto-merge stamp cleanup", () => { vi.spyOn(window, "confirm").mockReturnValue(true); }); + it("renders and updates the project-scoped plan approval mode select", async () => { + vi.stubGlobal("fetch", vi.fn().mockResolvedValue(jsonResponse({ candidates: [], count: 0 }))); + const props = makeProps({ planApprovalMode: "require-all" }); + + render(); + + const select = screen.getByTestId("plan-approval-mode-select") as HTMLSelectElement; + expect(select.value).toBe("require-all"); + expect(screen.getByRole("option", { name: "Use workflow setting" })).toBeInTheDocument(); + expect(screen.getByRole("option", { name: "Auto-approve all tasks" })).toBeInTheDocument(); + expect(screen.getByRole("option", { name: "Require approval for all tasks" })).toBeInTheDocument(); + + fireEvent.change(select, { target: { value: "auto-approve-all" } }); + + expect(props.setForm).toHaveBeenCalledWith(expect.any(Function)); + const updater = vi.mocked(props.setForm).mock.calls.at(-1)?.[0] as (state: MergeSectionProps["form"]) => MergeSectionProps["form"]; + expect(updater(props.form).planApprovalMode).toBe("auto-approve-all"); + }); + it("renders the store-provided candidate list without client-side filtering", async () => { const fetchMock = vi.fn().mockResolvedValue(jsonResponse({ candidates: [ diff --git a/packages/engine/src/__tests__/triage.test.ts b/packages/engine/src/__tests__/triage.test.ts index c76e96a26b..a69d71603c 100644 --- a/packages/engine/src/__tests__/triage.test.ts +++ b/packages/engine/src/__tests__/triage.test.ts @@ -1749,6 +1749,64 @@ describe("requirePlanApproval setting", () => { const settings = await store.getSettings(); expect(settings.requirePlanApproval).toBeUndefined(); }); + + async function finalizeWithSettings(settings: Settings) { + const task = createTriageTask({ + id: "FN-APPROVAL", + title: "Approval mode task", + status: "planning", + } as Partial); + const store = createMockStore({ + getTask: vi.fn().mockResolvedValue(task), + }); + const processor = new TriageProcessor(store, rootDir); + await (processor as unknown as { + finalizeApprovedTask(task: Task, writtenInput: string, settings: Settings): Promise; + }).finalizeApprovedTask( + task, + "# Task: FN-APPROVAL - Approval mode task\n\n**Size:** M\n\n## Review Level: 1\n\n## File Scope\n\n- packages/engine/src/triage.ts\n", + settings, + ); + return store; + } + + it("auto-approve-all moves to todo even when workflow requires plan approval", async () => { + const store = await finalizeWithSettings({ + requirePlanApproval: true, + planApprovalMode: "auto-approve-all", + } as Settings); + + expect(store.moveTask).toHaveBeenCalledWith("FN-APPROVAL", "todo"); + expect(store.updateTask).not.toHaveBeenCalledWith("FN-APPROVAL", expect.objectContaining({ status: "awaiting-approval" })); + }); + + it("require-all parks for manual approval even when workflow disables plan approval", async () => { + const store = await finalizeWithSettings({ + requirePlanApproval: false, + planApprovalMode: "require-all", + } as Settings); + + expect(store.updateTask).toHaveBeenCalledWith("FN-APPROVAL", expect.objectContaining({ status: "awaiting-approval" })); + expect(store.moveTask).not.toHaveBeenCalled(); + }); + + it.each([ + { mode: "workflow" as const, requirePlanApproval: true, expectedApproval: true }, + { mode: undefined, requirePlanApproval: false, expectedApproval: false }, + ])("defers to workflow requirePlanApproval when mode is $mode", async ({ mode, requirePlanApproval, expectedApproval }) => { + const store = await finalizeWithSettings({ + requirePlanApproval, + planApprovalMode: mode, + } as Settings); + + if (expectedApproval) { + expect(store.updateTask).toHaveBeenCalledWith("FN-APPROVAL", expect.objectContaining({ status: "awaiting-approval" })); + expect(store.moveTask).not.toHaveBeenCalled(); + } else { + expect(store.moveTask).toHaveBeenCalledWith("FN-APPROVAL", "todo"); + expect(store.updateTask).not.toHaveBeenCalledWith("FN-APPROVAL", expect.objectContaining({ status: "awaiting-approval" })); + } + }); }); describe("approved triage recovery", () => { diff --git a/packages/engine/src/triage.ts b/packages/engine/src/triage.ts index e1688f04aa..4972cce5a3 100644 --- a/packages/engine/src/triage.ts +++ b/packages/engine/src/triage.ts @@ -23,6 +23,7 @@ import { sortTasksByPriorityThenAgeAndId, compareTaskIdNumeric, resolveAgentMemoryInclusionMode, + resolvePlanApprovalRequired, extractIntentSignature, findNearDuplicates, isNearDuplicateCanonicalInactive, @@ -491,6 +492,7 @@ export class TriageProcessor { // Merge per-task effective workflow settings (U3, KTD-3) so requirePlanApproval // resolves from the workflow. Behavior-inert when nothing is customized. const settings = await mergeEffectiveSettings(this.store, task, await this.store.getSettings()); + const approvalRequired = resolvePlanApprovalRequired(settings); const promptPath = join(this.rootDir, ".fusion", "tasks", task.id, "PROMPT.md"); const written = await readFile(promptPath, "utf-8").catch((err: unknown) => { const msg = err instanceof Error ? err.message : String(err); @@ -504,7 +506,7 @@ export class TriageProcessor { } await this.finalizeApprovedTask(task, written, settings, { - recoveryLogAction: settings.requirePlanApproval + recoveryLogAction: approvalRequired ? "Auto-recovered approved specification stuck in planning — awaiting manual approval" : "Auto-recovered approved specification stuck in planning — moved to todo", }); @@ -2420,7 +2422,11 @@ export class TriageProcessor { return; } - if (settings.requirePlanApproval) { + /* + FNXC:PlanApproval 2026-06-26-00:00: + Project planApprovalMode has precedence over the workflow-resolved requirePlanApproval value so operators can force auto-approval or manual approval for every task in this project. + */ + if (resolvePlanApprovalRequired(settings)) { const approvalUpdates: Record = { status: "awaiting-approval" }; if (shouldApplyPromptDeclaredTitle && promptDeclaredTitle) { approvalUpdates.title = promptDeclaredTitle; diff --git a/packages/i18n/locales/en/app.json b/packages/i18n/locales/en/app.json index e457c512c3..f91ea02b2c 100644 --- a/packages/i18n/locales/en/app.json +++ b/packages/i18n/locales/en/app.json @@ -6067,6 +6067,11 @@ "personalAccessToken": "Personal access token", "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", + "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", "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 3b952c754f..79127d10fd 100644 --- a/packages/i18n/src/resources.d.ts +++ b/packages/i18n/src/resources.d.ts @@ -6071,6 +6071,11 @@ export default interface Resources { "personalAccessToken": "Personal access token", "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", + "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", "postMergeAuditMode": "Post-merge audit mode", "pushRemote": "Push Remote", "pushToRemoteAfterMerge": " Push to remote after merge ",