diff --git a/panel/src/components/tasks/task-detail/__tests__/task-header-godmode.test.tsx b/panel/src/components/tasks/task-detail/__tests__/task-header-godmode.test.tsx
new file mode 100644
index 00000000..9672fefb
--- /dev/null
+++ b/panel/src/components/tasks/task-detail/__tests__/task-header-godmode.test.tsx
@@ -0,0 +1,98 @@
+import { describe, it, expect, vi } from "vitest";
+import { render } from "@testing-library/react";
+import React from "react";
+import { TaskStatus, Team, TaskType, type Task } from "@/types";
+
+// God-mode status override: forcing a task into a hatch/terminal state via the
+// header Select must send `force: true`, else the backend refuses the PATCH
+// with 400 (the lifecycle-bypass acknowledgement added by the gap sweep).
+
+const { mutateAsync } = vi.hoisted(() => ({
+ mutateAsync: vi.fn().mockResolvedValue(undefined),
+}));
+
+vi.mock("next/navigation", () => ({
+ useRouter: () => ({ push: vi.fn(), back: vi.fn() }),
+}));
+
+vi.mock("@/hooks/use-tasks", () => ({
+ useUpdateTask: () => ({ mutateAsync, isPending: false }),
+ useDeleteTask: () => ({ mutateAsync: vi.fn(), isPending: false }),
+ // Empty valid-transitions => every other status is a god-mode override.
+ useTaskValidTransitions: () => ({ data: [], isLoading: false }),
+}));
+
+vi.mock("sonner", () => ({
+ toast: { success: vi.fn(), error: vi.fn() },
+}));
+
+// Make the Select testable without Radix's portal/pointer machinery: each
+// SelectItem renders a button carrying its value; clicking it invokes the
+// nearest Select's onValueChange (scoped via context so the status Select and
+// the team Select don't cross-fire).
+vi.mock("@/components/ui/select", () => {
+ const Ctx = React.createContext<(v: string) => void>(() => {});
+ return {
+ Select: ({
+ onValueChange,
+ children,
+ }: {
+ onValueChange?: (v: string) => void;
+ children: React.ReactNode;
+ }) => (
+ {})}>{children}
+ ),
+ SelectTrigger: ({ children }: { children: React.ReactNode }) => (
+
{children}
+ ),
+ SelectValue: () => null,
+ SelectContent: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+ SelectItem: ({
+ value,
+ children,
+ }: {
+ value: string;
+ children: React.ReactNode;
+ }) => {
+ const onValueChange = React.useContext(Ctx);
+ return (
+
+ );
+ },
+ };
+});
+
+import { TaskHeader } from "../task-header";
+
+function buildTask(): Task {
+ return {
+ id: "t1",
+ title: "Wedged task",
+ description: "d",
+ status: TaskStatus.IN_PROGRESS,
+ team: Team.BACKEND,
+ task_type: TaskType.CODE,
+ acceptance_criteria: [],
+ } as unknown as Task;
+}
+
+describe("TaskHeader god-mode status override", () => {
+ it("sends force: true when forcing a hatch/terminal status", async () => {
+ const { container } = render();
+ const btn = container.querySelector(
+ `[data-value="${TaskStatus.COMPLETED}"]`,
+ );
+ expect(btn).not.toBeNull();
+ btn?.click();
+ await vi.waitFor(() =>
+ expect(mutateAsync).toHaveBeenCalledWith({
+ taskId: "t1",
+ updates: { status: TaskStatus.COMPLETED, force: true },
+ }),
+ );
+ });
+});
diff --git a/panel/src/components/tasks/task-detail/task-header.tsx b/panel/src/components/tasks/task-detail/task-header.tsx
index 3ba4cbf9..da1109ec 100644
--- a/panel/src/components/tasks/task-detail/task-header.tsx
+++ b/panel/src/components/tasks/task-detail/task-header.tsx
@@ -236,10 +236,13 @@ export function TaskHeader({ task, onAction }: TaskHeaderProps) {
// reopening a `cancelled` task). Audited via PATCH /tasks/{id} {status} ->
// admin_set_status. No PR merge / lifecycle side effects fire — this is a
// pure, operator-driven state correction the CEO is entitled to make.
+ // `force: true` is the explicit acknowledgement the backend requires to
+ // override into a hatch state (awaiting_*, completed, cancelled) or to
+ // resurrect a terminal task; without it the PATCH is refused with 400.
try {
await updateTask.mutateAsync({
taskId: task.id,
- updates: { status: newStatus },
+ updates: { status: newStatus, force: true },
});
toast.success(`Status forced to ${statusLabels[newStatus]}`);
} catch {