FN-7515: expose planner oversight configuration

Expose planner oversight as workflow-native configuration across task and workflow surfaces.

- Add a shared TaskForm selector for per-task planner oversight overrides with inherit semantics.
- Thread plannerOversightLevel through new task creation, task detail edits, and legacy dashboard API payloads.
- Add a Workflow Editor Values display group and documentation for configuring workflow defaults.
- Cover create, edit, form, and workflow settings behavior with dashboard tests.

Files changed:
 .changeset/fn-7515-planner-oversight-config-exposure.md   |  7 ++
 docs/dashboard-guide.md                                    |  1 +
 docs/settings-reference.md                                 |  2 +-
 packages/dashboard/app/api/legacy.ts                       |  3 +
 packages/dashboard/app/components/NewTaskModal.tsx         | 12 +++-
 packages/dashboard/app/components/TaskDetailModal.tsx      | 11 +++-
 packages/dashboard/app/components/TaskForm.tsx             | 34 ++++++++++
 packages/dashboard/app/components/__tests__/NewTaskModal.test.tsx | 31 +++++++++
 packages/dashboard/app/components/__tests__/TaskDetailModal.inline-editing-and-integrations.test.tsx | 76 ++++++++++++++++++++++
 packages/dashboard/app/components/__tests__/TaskForm.test.tsx | 28 ++++++++
 packages/dashboard/app/components/__tests__/WorkflowSettingsPanel.test.tsx | 34 ++++++++++
 packages/dashboard/app/components/workflow-setting-display.ts | 17 ++++-
 12 files changed, 251 insertions(+), 5 deletions(-)

Fusion-Task-Id: FN-7515

Fusion-Task-Lineage: aded67c0-835c-4046-b691-04dc2bb2d314

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-04 14:26:46 -07:00
parent 79ab367547
commit c16cc9e08a
12 changed files with 251 additions and 5 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": minor
---
summary: Configure planner oversight level per task and per project in the workflow editor and task create/detail.
category: feature
dev: Per-task `plannerOversightLevel` override exposed via TaskForm (Inherit/off/observe/steer/autonomous), threaded through createTask/updateTask; workflow-editor Values tab gets a first-class display entry. Workflow-native setting; not a project setting.

View File

@@ -431,6 +431,7 @@ Rules:
- `auto-new` creates a branch after task creation using `fusion/{task-id}-{short-name}` (for example `fusion/fn-5671-branch-strategy-dropdown`).
- `Merge target / base branch` stays optional for all modes and uses the same branch-dropdown + `Custom…` fallback behavior as Planning Mode.
- In **More options → Model Configuration**, **Auto-merge** is a per-task override with three states: **Default** (follow project setting), **Enabled**, or **Disabled**.
- In **More options → Model Configuration**, **Planner oversight** is a per-task override of the workflow-native `plannerOversightLevel` setting (FN-7508): **Inherit from workflow** (default) plus **Off**, **Observe**, **Steer**, and **Autonomous recovery**. This selector appears in both the New Task dialog and the Task Detail edit form (same shared control). Selecting **Inherit from workflow** clears the per-task override (sent as `null` on edit, omitted on create) so the task falls back to the effective `plannerOversightLevel` configured on its workflow — set project/global defaults for this in the **Workflow Editor → Values** tab, not in Project Settings; it is workflow-native, not a project setting.
The dialog also exposes AI handoffs that quick-add no longer shows: **Plan** opens Planning Mode with the current description, and **Subtask** opens Subtask Breakdown with the current description when **Settings → Experimental Features → Subtask Breakdown** is enabled. The Subtask handoff is hidden by default; visible handoff buttons remain disabled until the description has content, matching the quick-add row behavior for Subtask. **Execution mode** and optional workflow-step selection are available in the New Task dialog as well as quick entry, so users can choose Fast or standard execution and opt into workflow-specific creation-time steps before creating a task from either surface.

View File

@@ -359,7 +359,7 @@ The built-in workflows also declare triage/spec policy settings that were **not*
| `autoApproveSpec` | `false` | Legacy compatibility setting. Workflow Plan Review now owns optional pre-execution AI plan approval. |
| `planReviewMaxRevisions` | unset | Workflow-native Plan Review/spec revision cap. Unset/empty means unbounded automatic replans; a non-negative integer caps attempts; `0` disables automatic Plan Review revision. |
| `codeReviewMaxRevisions` | unset | Workflow-native Code Review remediation cap. Unset/empty means unbounded automatic code-fix passes; a non-negative integer caps attempts; `0` disables automatic Code Review remediation. |
| `plannerOversightLevel` | `autonomous` | Workflow-native planner oversight mode. `off` disables oversight; `observe` watches only; `steer` injects guidance or suggests revisions; `autonomous` enables bounded retry and targeted-fix recovery — but merge/PR progression and any destructive or external-service side effect ALWAYS require an explicit, recorded human confirmation before they run, even at `autonomous` (FN-7513's confirmation gate; see `docs/architecture.md` → "Planner overseer confirmation gate"). Tasks may set a nullable `Task.plannerOversightLevel` override (same four values) that wins over this workflow value when present; `null`/unset means "inherit the workflow value". `resolveEffectivePlannerOversightLevel` in `@fusion/core` computes the effective level (task override → workflow effective → `autonomous`). Dashboard UI/API threading for the per-task override and engine read-site behavior are follow-up work (FN-7515, FN-7510+). |
| `plannerOversightLevel` | `autonomous` | Workflow-native planner oversight mode. `off` disables oversight; `observe` watches only; `steer` injects guidance or suggests revisions; `autonomous` enables bounded retry and targeted-fix recovery — but merge/PR progression and any destructive or external-service side effect ALWAYS require an explicit, recorded human confirmation before they run, even at `autonomous` (FN-7513's confirmation gate; see `docs/architecture.md` → "Planner overseer confirmation gate"). Tasks may set a nullable `Task.plannerOversightLevel` override (same four values) that wins over this workflow value when present; `null`/unset means "inherit the workflow value". `resolveEffectivePlannerOversightLevel` in `@fusion/core` computes the effective level (task override → workflow effective → `autonomous`). The per-task override is exposed in the dashboard as a "Planner oversight" selector (Inherit from workflow / Off / Observe / Steer / Autonomous recovery) in both the New Task dialog and Task Detail edit form, threaded through `createTask`/`updateTask` (FN-7515); the project/global default is set via the **Workflow Editor → Values** tab on the default workflow's `plannerOversightLevel` value, not in Project Settings. Engine read-site behavior beyond the FN-7513 confirmation gate remains follow-up work (FN-7510+). |
When `triageProactiveSubtaskSplittingEnabled` is `true` (the default), triage may proactively replace a large task with 2-5 child tasks when the size, step-count, package breadth, file-scope, or remediation-batch signals justify the coordination overhead. When it is `false`, those automatic oversized-task signals are advisory only for writing a realistic single-task spec; triage must not split solely because the task is large. The per-task `breakIntoSubtasks: true` flag is separate and remains mandatory: if a user explicitly asks for subtask breakdown, triage still evaluates and creates child tasks when the work is meaningfully decomposable.

View File

@@ -420,6 +420,7 @@ export async function createTask(
planningModelProvider,
planningModelId,
thinkingLevel,
plannerOversightLevel,
summarize,
reviewLevel,
executionMode,
@@ -457,6 +458,7 @@ export async function createTask(
planningModelProvider,
planningModelId,
thinkingLevel,
plannerOversightLevel,
summarize,
reviewLevel,
executionMode,
@@ -523,6 +525,7 @@ export function updateTask(
planningModelProvider?: string | null;
planningModelId?: string | null;
thinkingLevel?: string | null;
plannerOversightLevel?: "off" | "observe" | "steer" | "autonomous" | null;
reviewLevel?: number | null;
executionMode?: "standard" | "fast" | null;
noCommitsExpected?: boolean;

View File

@@ -569,6 +569,8 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask,
const [validatorModel, setValidatorModel] = useState("");
const [planningModel, setPlanningModel] = useState("");
const [thinkingLevel, setThinkingLevel] = useState<string>("");
// FNXC:PlannerOversight 2026-07-04-00:00: Per-task override of the workflow-native plannerOversightLevel setting (FN-7508). "" means inherit from workflow.
const [plannerOversightLevel, setPlannerOversightLevel] = useState<string>("");
const [selectedPresetId, setSelectedPresetId] = useState<string>("");
const [presetMode, setPresetMode] = useState<"default" | "preset" | "custom">("default");
const [hasDirtyState, setHasDirtyState] = useState(false);
@@ -713,6 +715,7 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask,
validatorModel !== "" ||
planningModel !== "" ||
thinkingLevel !== "" ||
plannerOversightLevel !== "" ||
selectedAgentId !== null ||
reviewLevel !== undefined ||
autoMerge !== undefined ||
@@ -725,7 +728,7 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask,
githubTrackingEnabled ||
githubRepoOverrideTrimmed !== "";
setHasDirtyState(isDirty);
}, [description, dependencies, pendingImages, selectedWorkflowId, shouldSubmitEnabledWorkflowSteps, enabledWorkflowSteps, executorModel, validatorModel, planningModel, thinkingLevel, selectedAgentId, reviewLevel, autoMerge, priority, nodeId, executionMode, branchMode, branch, baseBranch, githubTrackingEnabled, githubRepoOverrideTrimmed]);
}, [description, dependencies, pendingImages, selectedWorkflowId, shouldSubmitEnabledWorkflowSteps, enabledWorkflowSteps, executorModel, validatorModel, planningModel, thinkingLevel, plannerOversightLevel, selectedAgentId, reviewLevel, autoMerge, priority, nodeId, executionMode, branchMode, branch, baseBranch, githubTrackingEnabled, githubRepoOverrideTrimmed]);
const resetForm = useCallback(() => {
// Clean up object URLs
@@ -738,6 +741,7 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask,
setValidatorModel("");
setPlanningModel("");
setThinkingLevel("");
setPlannerOversightLevel("");
setSelectedPresetId("");
setPresetMode("default");
setSelectedWorkflowId(undefined);
@@ -808,6 +812,8 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask,
planningModelProvider: planningModel && planningSlashIdx !== -1 ? planningModel.slice(0, planningSlashIdx) : undefined,
planningModelId: planningModel && planningSlashIdx !== -1 ? planningModel.slice(planningSlashIdx + 1) : undefined,
thinkingLevel: thinkingLevel !== "" ? thinkingLevel as "minimal" | "low" | "medium" | "high" | "xhigh" : undefined,
// FNXC:PlannerOversight 2026-07-04-00:00: omit when "Inherit from workflow" ("") is selected so the task falls back to the workflow's effective plannerOversightLevel.
...(plannerOversightLevel !== "" ? { plannerOversightLevel: plannerOversightLevel as "off" | "observe" | "steer" | "autonomous" } : {}),
reviewLevel,
...(autoMerge !== undefined ? { autoMerge } : {}),
priority,
@@ -852,7 +858,7 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask,
resetForm();
addToast(t("newTaskModal.taskCreated", "Created {{taskId}}", { taskId: task.id }), "success");
onClose();
}, [executorModel, validatorModel, planningModel, thinkingLevel, dependencies, selectedWorkflowId, shouldSubmitEnabledWorkflowSteps, enabledWorkflowSteps, selectedAgentId, presetMode, selectedPresetId, reviewLevel, autoMerge, priority, nodeId, executionMode, branchMode, isBranchNameRequired, branch, baseBranch, githubTrackingEnabled, githubRepoOverrideTrimmed, onCreateTask, pendingImages, resetForm, addToast, t, onClose, projectId]);
}, [executorModel, validatorModel, planningModel, thinkingLevel, plannerOversightLevel, dependencies, selectedWorkflowId, shouldSubmitEnabledWorkflowSteps, enabledWorkflowSteps, selectedAgentId, presetMode, selectedPresetId, reviewLevel, autoMerge, priority, nodeId, executionMode, branchMode, isBranchNameRequired, branch, baseBranch, githubTrackingEnabled, githubRepoOverrideTrimmed, onCreateTask, pendingImages, resetForm, addToast, t, onClose, projectId]);
const handleSubmit = useCallback(async () => {
const trimmedDesc = description.trim();
@@ -1176,6 +1182,8 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask,
planningModel={planningModel}
onPlanningModelChange={setPlanningModel}
thinkingLevel={thinkingLevel}
plannerOversightLevel={plannerOversightLevel}
onPlannerOversightLevelChange={setPlannerOversightLevel}
onThinkingLevelChange={setThinkingLevel}
reviewLevel={reviewLevel}
onReviewLevelChange={setReviewLevel}

View File

@@ -919,6 +919,8 @@ export function TaskDetailContent({
const [editValidatorModel, setEditValidatorModel] = useState("");
const [editPlanningModel, setEditPlanningModel] = useState("");
const [editThinkingLevel, setEditThinkingLevel] = useState("");
// FNXC:PlannerOversight 2026-07-04-00:00: Per-task override of the workflow-native plannerOversightLevel setting (FN-7508). "" means "inherit from workflow" (clear-to-default).
const [editPlannerOversightLevel, setEditPlannerOversightLevel] = useState("");
const [editPresetMode, setEditPresetMode] = useState<"default" | "preset" | "custom">("default");
const [editReviewLevel, setEditReviewLevel] = useState<number | undefined>(undefined);
const [editPriority, setEditPriority] = useState<TaskPriority>(DEFAULT_TASK_PRIORITY);
@@ -1546,6 +1548,7 @@ export function TaskDetailContent({
setEditValidatorModel(valModel);
setEditPlanningModel(planModel);
setEditThinkingLevel(task.thinkingLevel ?? "");
setEditPlannerOversightLevel(task.plannerOversightLevel ?? "");
setEditNodeId(task.nodeId);
setEditPresetMode(execModel || valModel || planModel ? "custom" : "default");
setEditSelectedPresetId("");
@@ -1624,6 +1627,9 @@ export function TaskDetailContent({
const currentThinkingLevel = task.thinkingLevel ?? "";
if (editThinkingLevel !== currentThinkingLevel) updates.thinkingLevel = editThinkingLevel !== "" ? (editThinkingLevel as "minimal" | "low" | "medium" | "high" | "xhigh") : null;
// FNXC:PlannerOversight 2026-07-04-00:00: "" (Inherit from workflow) clears the per-task override to null so the workflow's effective plannerOversightLevel applies.
const currentPlannerOversightLevel = task.plannerOversightLevel ?? "";
if (editPlannerOversightLevel !== currentPlannerOversightLevel) updates.plannerOversightLevel = editPlannerOversightLevel !== "" ? (editPlannerOversightLevel as "off" | "observe" | "steer" | "autonomous") : null;
if ((task.nodeId ?? undefined) !== editNodeId) updates.nodeId = editNodeId ?? null;
if (editReviewLevel !== task.reviewLevel) updates.reviewLevel = editReviewLevel;
if (editPriority !== normalizeTaskPriorityValue(task.priority)) updates.priority = editPriority;
@@ -1664,7 +1670,7 @@ export function TaskDetailContent({
}
return { updates, error: null as string | null };
}, [editBaseBranch, editBranch, editDependencies, editDescription, editExecutionMode, editExecutorModel, editNodeId, editPlanningModel, editPriority, editReviewLevel, editSelectedWorkflowSteps, editSourceIssueExternalId, editSourceIssueProvider, editSourceIssueRepository, editSourceIssueUrl, editThinkingLevel, editTitle, editValidatorModel, task]);
}, [editBaseBranch, editBranch, editDependencies, editDescription, editExecutionMode, editExecutorModel, editNodeId, editPlanningModel, editPriority, editReviewLevel, editSelectedWorkflowSteps, editSourceIssueExternalId, editSourceIssueProvider, editSourceIssueRepository, editSourceIssueUrl, editThinkingLevel, editPlannerOversightLevel, editTitle, editValidatorModel, task]);
const persistEditChanges = useCallback(async (includeDescription: boolean) => {
const { updates, error } = buildEditUpdates(includeDescription);
@@ -1768,6 +1774,7 @@ export function TaskDetailContent({
editValidatorModel,
editPlanningModel,
editThinkingLevel,
editPlannerOversightLevel,
editNodeId,
editReviewLevel,
editPriority,
@@ -3266,6 +3273,8 @@ export function TaskDetailContent({
onPlanningModelChange={setEditPlanningModel}
thinkingLevel={editThinkingLevel}
onThinkingLevelChange={setEditThinkingLevel}
plannerOversightLevel={editPlannerOversightLevel}
onPlannerOversightLevelChange={setEditPlannerOversightLevel}
presetMode={editPresetMode}
onPresetModeChange={setEditPresetMode}
selectedPresetId={editSelectedPresetId}

View File

@@ -92,6 +92,17 @@ export interface TaskFormProps {
onPlanningModelChange?: (value: string) => void;
thinkingLevel?: string;
onThinkingLevelChange?: (value: string) => void;
/*
* FNXC:PlannerOversight 2026-07-04-00:00:
* Per-task override selector for the workflow-native `plannerOversightLevel` setting
* (FN-7508). Empty string ("") means "inherit from workflow" — the task uses the
* default workflow's effective `plannerOversightLevel` value. The four non-empty
* values (off/observe/steer/autonomous) mirror `BUILTIN_OVERSIGHT_SETTINGS` verbatim.
* This control is configuration only; runtime oversight controls (quick change,
* manual nudge, stop, explain-current-action) are FN-7517, not this task.
*/
plannerOversightLevel?: string;
onPlannerOversightLevelChange?: (value: string) => void;
presetMode: "default" | "preset" | "custom";
onPresetModeChange: (mode: "default" | "preset" | "custom") => void;
selectedPresetId: string;
@@ -196,6 +207,8 @@ export function TaskForm({
onPlanningModelChange,
thinkingLevel,
onThinkingLevelChange,
plannerOversightLevel,
onPlannerOversightLevelChange,
presetMode,
onPresetModeChange,
selectedPresetId,
@@ -244,6 +257,7 @@ export function TaskForm({
validatorModel !== "" ||
(planningModel || "") !== "" ||
(thinkingLevel || "") !== "" ||
(plannerOversightLevel || "") !== "" ||
reviewLevel !== undefined ||
autoMerge !== undefined ||
executionMode === "fast" ||
@@ -419,6 +433,7 @@ export function TaskForm({
validatorModel !== "" ||
(planningModel || "") !== "" ||
(thinkingLevel || "") !== "" ||
(plannerOversightLevel || "") !== "" ||
reviewLevel !== undefined ||
autoMerge !== undefined ||
executionMode === "fast" ||
@@ -1574,6 +1589,25 @@ export function TaskForm({
</select>
</div>
)}
{onPlannerOversightLevelChange && (
<div className="model-select-row">
{/* FNXC:PlannerOversight 2026-07-04-00:00: Per-task override for the workflow-native plannerOversightLevel setting (FN-7508). Empty value inherits the workflow's effective value; the four levels mirror BUILTIN_OVERSIGHT_SETTINGS verbatim. Configuration only — runtime controls are FN-7517. */}
<label htmlFor="planner-oversight-level" className="model-select-label">{t("taskForm.plannerOversightLabel", "Planner oversight")}</label>
<select
id="planner-oversight-level"
data-testid="planner-oversight-level-select"
value={plannerOversightLevel || ""}
onChange={(e) => onPlannerOversightLevelChange(e.target.value)}
disabled={disabled}
>
<option value="">{t("taskForm.plannerOversightInherit", "Inherit from workflow")}</option>
<option value="off">{t("taskForm.plannerOversightOff", "Off")}</option>
<option value="observe">{t("taskForm.plannerOversightObserve", "Observe")}</option>
<option value="steer">{t("taskForm.plannerOversightSteer", "Steer")}</option>
<option value="autonomous">{t("taskForm.plannerOversightAutonomous", "Autonomous recovery")}</option>
</select>
</div>
)}
{onReviewLevelChange && (
<div className="model-select-row">
<label htmlFor="review-level" className="model-select-label">{t("taskForm.reviewLabel", "Review")}</label>

View File

@@ -640,6 +640,37 @@ describe("NewTaskModal", () => {
expect(payload).not.toHaveProperty("executionMode");
});
// FNXC:PlannerOversight 2026-07-04-00:00: leaving the selector on "Inherit from workflow" ("") must omit plannerOversightLevel from the create payload so the task falls back to the workflow's effective value.
it("omits plannerOversightLevel from the create payload when left on Inherit", async () => {
const { props } = renderNewTaskModal();
fireEvent.change(screen.getByPlaceholderText("What needs to be done?"), { target: { value: "Inherit oversight task" } });
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
await waitFor(() => {
expect(props.onCreateTask).toHaveBeenCalledTimes(1);
});
const payload = vi.mocked(props.onCreateTask).mock.calls[0][0] as Record<string, unknown>;
expect(payload).not.toHaveProperty("plannerOversightLevel");
});
it("includes the selected plannerOversightLevel in the create payload", async () => {
const { props } = renderNewTaskModal();
fireEvent.click(screen.getByTestId("task-form-more-options-toggle"));
const select = await screen.findByTestId("planner-oversight-level-select");
fireEvent.change(select, { target: { value: "steer" } });
fireEvent.change(screen.getByPlaceholderText("What needs to be done?"), { target: { value: "Steer oversight task" } });
fireEvent.click(screen.getByRole("button", { name: "Create Task" }));
await waitFor(() => {
expect(props.onCreateTask).toHaveBeenCalledWith(
expect.objectContaining({ plannerOversightLevel: "steer" }),
);
});
});
it("resets executionMode to standard after canceling and discarding changes", async () => {
const { props, rerender } = renderNewTaskModal();

View File

@@ -963,6 +963,82 @@ describe("TaskDetailModal", () => {
});
});
// FNXC:PlannerOversight 2026-07-04-00:00: the edit path clear-to-default contract — selecting a level sends the value, returning to Inherit sends null, no change emits nothing.
it("emits plannerOversightLevel when changed from Inherit to a level", async () => {
const { updateTask, fetchModels } = await import("../../api");
const mockUpdate = vi.mocked(updateTask);
mockUpdate.mockResolvedValue({ id: "FN-001" } as Task);
vi.mocked(fetchModels).mockResolvedValue({
models: [{ provider: "anthropic", id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5", reasoning: true, contextWindow: 200000 }],
favoriteProviders: [],
favoriteModels: [],
});
const { container } = render(
<TaskDetailModal
initialTab="definition"
task={makeTask({ id: "FN-001", column: "triage", title: "Test", description: "Desc" })}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={noop}
/>,
);
// No change → no updateTask call.
fireEvent.click(container.querySelector(".modal-edit-btn")!);
fireEvent.click(screen.getByText("Save"));
await waitFor(() => {
expect(mockUpdate).not.toHaveBeenCalled();
});
// Selecting a level sends that value.
fireEvent.click(container.querySelector(".modal-edit-btn")!);
fireEvent.click(screen.getByTestId("task-form-more-options-toggle"));
const select = await screen.findByTestId("planner-oversight-level-select");
fireEvent.change(select, { target: { value: "observe" } });
fireEvent.click(screen.getByText("Save"));
await waitFor(() => {
expect(mockUpdate).toHaveBeenCalledWith("FN-001", { plannerOversightLevel: "observe" }, undefined);
});
});
it("emits plannerOversightLevel: null when changed from a level back to Inherit (clear-to-default)", async () => {
const { updateTask, fetchModels } = await import("../../api");
const mockUpdate = vi.mocked(updateTask);
mockUpdate.mockResolvedValue({ id: "FN-001" } as Task);
vi.mocked(fetchModels).mockResolvedValue({
models: [{ provider: "anthropic", id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5", reasoning: true, contextWindow: 200000 }],
favoriteProviders: [],
favoriteModels: [],
});
const { container } = render(
<TaskDetailModal
initialTab="definition"
task={makeTask({ id: "FN-001", column: "triage", title: "Test", description: "Desc", plannerOversightLevel: "observe" as Task["plannerOversightLevel"] })}
onClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={noop}
/>,
);
fireEvent.click(container.querySelector(".modal-edit-btn")!);
fireEvent.click(screen.getByTestId("task-form-more-options-toggle"));
const select = await screen.findByTestId("planner-oversight-level-select");
expect(select).toHaveValue("observe");
fireEvent.change(select, { target: { value: "" } });
fireEvent.click(screen.getByText("Save"));
await waitFor(() => {
expect(mockUpdate).toHaveBeenCalledWith("FN-001", { plannerOversightLevel: null }, undefined);
});
});
it("sends executionMode: \"fast\" when changed from standard to fast", async () => {
const { updateTask } = await import("../../api");
const mockUpdate = vi.mocked(updateTask);

View File

@@ -160,6 +160,34 @@ describe("TaskForm", () => {
expect(screen.getByRole("option", { name: /Very High/i })).toHaveAttribute("value", "xhigh");
});
// FNXC:PlannerOversight 2026-07-04-00:00: per-task override selector must render Inherit + the four BUILTIN_OVERSIGHT_SETTINGS levels and fire onPlannerOversightLevelChange on selection.
it("renders the planner oversight selector with Inherit + four levels and fires onPlannerOversightLevelChange", async () => {
const onPlannerOversightLevelChange = vi.fn();
renderTaskForm({
plannerOversightLevel: "",
onPlannerOversightLevelChange,
});
fireEvent.click(screen.getByTestId("task-form-more-options-toggle"));
const select = await screen.findByTestId("planner-oversight-level-select");
expect(select).toHaveValue("");
expect(screen.getByRole("option", { name: /Inherit from workflow/i })).toHaveAttribute("value", "");
expect(screen.getByRole("option", { name: /^Off$/i })).toHaveAttribute("value", "off");
expect(screen.getByRole("option", { name: /^Observe$/i })).toHaveAttribute("value", "observe");
expect(screen.getByRole("option", { name: /^Steer$/i })).toHaveAttribute("value", "steer");
expect(screen.getByRole("option", { name: /Autonomous recovery/i })).toHaveAttribute("value", "autonomous");
fireEvent.change(select, { target: { value: "steer" } });
expect(onPlannerOversightLevelChange).toHaveBeenCalledWith("steer");
});
it("does not render the planner oversight selector when onPlannerOversightLevelChange is not provided", () => {
renderTaskForm({});
fireEvent.click(screen.getByTestId("task-form-more-options-toggle"));
expect(screen.queryByTestId("planner-oversight-level-select")).toBeNull();
});
it("renders description field with AI refine button when text is present", () => {
renderTaskForm({ description: "Some text" });

View File

@@ -216,6 +216,40 @@ describe("WorkflowSettingsPanel — Values tab", () => {
expect(screen.getByText(/Leave empty for unbounded automatic Code Review remediation/i)).toBeInTheDocument();
});
// FNXC:PlannerOversight 2026-07-04-00:00: the Values tab is the project/global default
// surface for the workflow-native plannerOversightLevel setting (FN-7508/FN-7515) —
// confirm it renders with its own first-class group/label and all four options.
it("renders plannerOversightLevel in the Values tab with its four options and a first-class oversight group", async () => {
mockFetchValues.mockResolvedValue(payload({ effective: { plannerOversightLevel: "autonomous" } }));
render(
<Host
readOnly
initial={[
{
id: "plannerOversightLevel",
name: "Planner oversight level",
type: "enum",
default: "autonomous",
options: [
{ value: "off", label: "Off" },
{ value: "observe", label: "Observe" },
{ value: "steer", label: "Steer" },
{ value: "autonomous", label: "Autonomous recovery" },
],
},
]}
/>,
);
await waitFor(() => expect(mockFetchValues).toHaveBeenCalledWith("wf-1", "proj-1"));
expect(within(screen.getByTestId("wf-settings-group-oversight")).getByText("Planner Oversight")).toBeInTheDocument();
const select = screen.getByLabelText("Planner oversight level") as HTMLSelectElement;
expect(select).toBeInTheDocument();
expect(select).toHaveValue("autonomous");
const optionValues = Array.from(select.options).map((o) => o.value);
expect(optionValues).toEqual(expect.arrayContaining(["off", "observe", "steer", "autonomous"]));
});
it("renders and saves the automatic large-task splitting workflow toggle once", async () => {
const triageToggle: WorkflowSettingDefinition[] = [
{

View File

@@ -1,6 +1,6 @@
import type { WorkflowSettingDefinition } from "../api";
export type WorkflowSettingGroup = "models" | "review" | "steps" | "advanced";
export type WorkflowSettingGroup = "models" | "review" | "oversight" | "steps" | "advanced";
export interface WorkflowSettingDisplay {
group: WorkflowSettingGroup;
@@ -151,11 +151,25 @@ const DISPLAY: Record<string, WorkflowSettingDisplay> = {
group: "steps",
label: "Post-review fix passes",
},
/*
* FNXC:PlannerOversight 2026-07-04-00:00:
* First-class Values-tab display entry for the workflow-native plannerOversightLevel
* setting (FN-7508). This is the project/global default surface: the default
* workflow's stored value here is the effective oversight level for every task
* under it that does not set a per-task override (FN-7515, TaskForm selector).
*/
plannerOversightLevel: {
group: "oversight",
label: "Planner oversight level",
description:
"Off disables oversight; Observe watches only; Steer injects guidance or suggests revisions; Autonomous recovery enables bounded retry and targeted-fix recovery. Per-task overrides are set on individual tasks and win over this project/global default.",
},
};
export const WORKFLOW_SETTING_GROUP_ORDER: WorkflowSettingGroup[] = [
"models",
"review",
"oversight",
"steps",
"advanced",
];
@@ -163,6 +177,7 @@ export const WORKFLOW_SETTING_GROUP_ORDER: WorkflowSettingGroup[] = [
export const WORKFLOW_SETTING_GROUP_LABELS: Record<WorkflowSettingGroup, string> = {
models: "Models",
review: "Review & Approval",
oversight: "Planner Oversight",
steps: "Step Execution",
advanced: "Advanced",
};