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:
7
.changeset/fn-7515-planner-oversight-config-exposure.md
Normal file
7
.changeset/fn-7515-planner-oversight-config-exposure.md
Normal 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.
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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" });
|
||||
|
||||
|
||||
@@ -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[] = [
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user