Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions apps/mission-control-ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ const VALID_MAIN_VIEWS: MainView[] = [
"hybrid-workflows", "schedule", "codegen", "gherkin", "metrics", "qc-dashboard", "qc-runs",
"qc-environments", "qc-findings", "qc-metrics", "qc-rulesets", "gateway", "live-chat", "schedules",
"hiring", "team", "system", "radar", "factory", "pipeline", "feedback", "ops-schedule", "goals",
"control-portfolio", "control-fleet", "control-approvals",
"control-portfolio", "control-work-orders", "control-fleet", "control-approvals",
];

function readPersistedView(): MainView | null {
Expand Down Expand Up @@ -172,6 +172,7 @@ const SECTION_TABS: Record<CommandSection, TabItem[] | null> = {
home: null,
control: [
{ id: "control-portfolio", label: "Portfolio" },
{ id: "control-work-orders", label: "Work Orders" },
{ id: "control-fleet", label: "Fleet" },
{ id: "control-approvals", label: "Approvals" },
],
Expand Down Expand Up @@ -256,7 +257,7 @@ const SECTION_TABS: Record<CommandSection, TabItem[] | null> = {

function viewToSection(view: MainView): CommandSection {
if (view === "home") return "home";
if (["control-portfolio", "control-fleet", "control-approvals"].includes(view)) return "control";
if (["control-portfolio", "control-work-orders", "control-fleet", "control-approvals"].includes(view)) return "control";
if (["tasks", "goals", "dag", "calendar", "ops-schedule", "audit", "telemetry"].includes(view)) return "ops";
if (["atc", "agents", "directory", "identity", "policies", "deployments", "gateway", "schedules"].includes(view)) return "agents";
if (["chat", "live-chat", "council", "command"].includes(view)) return "chat";
Expand Down Expand Up @@ -616,6 +617,7 @@ export default function App() {
return (
<ControlSection
currentView={currentView}
projectId={projectId}
onNavigate={setCurrentView}
/>
);
Expand Down
1 change: 1 addition & 0 deletions apps/mission-control-ui/src/TopNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export type MainView =
| "ops-schedule"
| "goals"
| "control-portfolio"
| "control-work-orders"
| "control-fleet"
| "control-approvals";

Expand Down
646 changes: 646 additions & 0 deletions apps/mission-control-ui/src/controlPlane/WorkOrdersView.tsx

Large diffs are not rendered by default.

67 changes: 67 additions & 0 deletions apps/mission-control-ui/src/controlPlane/workOrdersModel.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { describe, expect, it } from "vitest";
import {
DEFAULT_WORK_ORDER_FILTERS,
filterWorkOrders,
summarizeRequiredAttention,
type WorkOrderQueueItem,
} from "./workOrdersModel";

const ITEMS: WorkOrderQueueItem[] = [
{
_id: "wo-1",
title: "One",
desiredOutcome: "Outcome one",
repository: "repo-a",
state: "IN_PROGRESS",
riskLevel: "HIGH",
assignedAgent: "Pi",
requestedBy: "Hermes",
verificationStatus: "PENDING",
approvalStatus: "PENDING",
requiredHumanAction: "Review evidence",
latestExecutionRun: null,
},
{
_id: "wo-2",
title: "Two",
desiredOutcome: "Outcome two",
repository: "repo-b",
state: "DONE",
riskLevel: "LOW",
assignedAgent: "QA",
requestedBy: "Jay",
verificationStatus: "PASS",
approvalStatus: "APPROVED",
blockingIssue: "None",
latestExecutionRun: null,
},
];

describe("work order queue model", () => {
it("filters by repository and verification status", () => {
const filtered = filterWorkOrders(ITEMS, {
...DEFAULT_WORK_ORDER_FILTERS,
repository: "repo-b",
verificationStatus: "PASS",
});

expect(filtered.map((item) => item._id)).toEqual(["wo-2"]);
});

it("filters by assigned agent", () => {
const filtered = filterWorkOrders(ITEMS, {
...DEFAULT_WORK_ORDER_FILTERS,
assignedAgent: "Pi",
});

expect(filtered.map((item) => item._id)).toEqual(["wo-1"]);
});

it("prefers explicit required human action in attention summary", () => {
expect(summarizeRequiredAttention(ITEMS[0])).toBe("Review evidence");
});

it("falls back to blocking issue when human action is absent", () => {
expect(summarizeRequiredAttention(ITEMS[1])).toBe("None");
});
});
58 changes: 58 additions & 0 deletions apps/mission-control-ui/src/controlPlane/workOrdersModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
export interface WorkOrderQueueItem {
_id: string;
title: string;
desiredOutcome: string;
workflowId?: string;
repository?: string;
state: string;
riskLevel: string;
assignedAgent?: string;
assignedSquad?: string;
requestedBy?: string;
verificationStatus: string;
approvalStatus: string;
blockingIssue?: string;
requiredHumanAction?: string;
latestExecutionRun?: {
status: string;
workflowId: string;
currentStepLabel?: string | null;
} | null;
}

export interface WorkOrderQueueFilters {
repository: string;
state: string;
riskLevel: string;
assignedAgent: string;
requestedBy: string;
verificationStatus: string;
}

export const DEFAULT_WORK_ORDER_FILTERS: WorkOrderQueueFilters = {
repository: "all",
state: "all",
riskLevel: "all",
assignedAgent: "all",
requestedBy: "all",
verificationStatus: "all",
};

export function filterWorkOrders(
items: WorkOrderQueueItem[],
filters: WorkOrderQueueFilters
): WorkOrderQueueItem[] {
return items.filter((item) => {
if (filters.repository !== "all" && item.repository !== filters.repository) return false;
if (filters.state !== "all" && item.state !== filters.state) return false;
if (filters.riskLevel !== "all" && item.riskLevel !== filters.riskLevel) return false;
if (filters.assignedAgent !== "all" && item.assignedAgent !== filters.assignedAgent) return false;
if (filters.requestedBy !== "all" && item.requestedBy !== filters.requestedBy) return false;
if (filters.verificationStatus !== "all" && item.verificationStatus !== filters.verificationStatus) return false;
return true;
});
}

export function summarizeRequiredAttention(item: WorkOrderQueueItem): string {
return item.requiredHumanAction ?? item.blockingIssue ?? "None";
}
52 changes: 32 additions & 20 deletions apps/mission-control-ui/src/sections/ControlSection.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
import { Orbit, ShieldCheck, Waypoints } from "lucide-react";
import { ClipboardList, Orbit, ShieldCheck, Waypoints } from "lucide-react";
import type { Id } from "../../../../convex/_generated/dataModel";
import { PageHeader } from "@/components/PageHeader";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import type { MainView } from "../TopNav";
import { WorkOrdersView } from "../controlPlane/WorkOrdersView";

interface ControlSectionProps {
currentView: MainView;
projectId: Id<"projects"> | null;
onNavigate: (view: MainView) => void;
}

const VIEW_COPY: Record<Extract<MainView, "control-portfolio" | "control-fleet" | "control-approvals">, {
title: string;
description: string;
}> = {
type ControlView = Extract<
MainView,
"control-portfolio" | "control-work-orders" | "control-fleet" | "control-approvals"
>;

const VIEW_COPY: Record<ControlView, { title: string; description: string }> = {
"control-portfolio": {
title: "Portfolio",
description: "Track operator-facing software factory work at the request and outcome layer.",
},
"control-work-orders": {
title: "Work Orders",
description: "Create and inspect first-class software-factory requests, acceptance criteria, and linked execution runs.",
},
"control-fleet": {
title: "Fleet",
description: "Inspect execution capacity and future fleet-level controls from one shell entrypoint.",
Expand All @@ -27,27 +36,30 @@ const VIEW_COPY: Record<Extract<MainView, "control-portfolio" | "control-fleet"
},
};

const CONTROL_VIEWS: Array<{
id: Extract<MainView, "control-portfolio" | "control-fleet" | "control-approvals">;
icon: typeof Orbit;
}> = [
const CONTROL_VIEWS: Array<{ id: ControlView; icon: typeof Orbit }> = [
{ id: "control-portfolio", icon: Orbit },
{ id: "control-work-orders", icon: ClipboardList },
{ id: "control-fleet", icon: Waypoints },
{ id: "control-approvals", icon: ShieldCheck },
];

export function ControlSection({ currentView, onNavigate }: ControlSectionProps) {
export function ControlSection({ currentView, projectId, onNavigate }: ControlSectionProps) {
const activeView = CONTROL_VIEWS.some((view) => view.id === currentView)
? currentView as keyof typeof VIEW_COPY
? (currentView as ControlView)
: "control-portfolio";

if (activeView === "control-work-orders") {
return <WorkOrdersView projectId={projectId} />;
}

const activeCopy = VIEW_COPY[activeView];

return (
<main className="flex flex-1 flex-col overflow-hidden">
<PageHeader
eyebrow="Control plane"
title="Control"
description="Minimal application-shell foundation for portfolio, fleet, approvals, and upcoming WorkOrder control surfaces."
description="Minimal control-plane shell plus the Work Orders slice for governed software-factory execution."
icon={<Orbit className="h-5 w-5" />}
/>

Expand All @@ -57,7 +69,7 @@ export function ControlSection({ currentView, onNavigate }: ControlSectionProps)
<CardHeader>
<CardTitle>Surface navigation</CardTitle>
<CardDescription>
Stable shell entrypoints for the Control section. Each view is intentionally lightweight until live control-plane features are wired.
Stable shell entrypoints for the Control section. Work Orders is live; the other views remain intentionally lightweight until follow-on control-plane slices are wired.
</CardDescription>
</CardHeader>
<CardContent className="space-y-2">
Expand Down Expand Up @@ -86,22 +98,22 @@ export function ControlSection({ currentView, onNavigate }: ControlSectionProps)
</CardHeader>
<CardContent className="space-y-4 text-sm text-muted-foreground">
<p>
This landing state intentionally avoids demo-backed execution logic. It exists to provide a durable section,
navigation model, and rendering boundary for follow-on control-plane work.
This landing state keeps the application shell stable while follow-on control-plane work arrives incrementally.
It preserves the section boundary introduced by the shell PR without pulling demo-heavy execution logic back into the WorkOrder slice.
</p>

<div className="grid gap-3 md:grid-cols-3">
<ControlNote
title="Section boundary"
body="The Control section now routes independently from Home, Ops, and Platform while preserving all existing shell views."
body="Control routes independently from Home, Ops, and Platform while preserving all existing shell views."
/>
<ControlNote
title="Integration point"
body="Future WorkOrder, approval, and fleet features can mount inside this section without rewiring the global app shell."
title="Live slice"
body="Work Orders now uses real Convex-backed data and governed dispatch behavior within this section."
/>
<ControlNote
title="Safe placeholder"
body="No seeded control-plane records or orchestration side effects are required for this shell to compile, render, and navigate."
title="Follow-on space"
body="Portfolio, Fleet, and Approvals remain stable placeholders until those slices are implemented on their own merits."
/>
</div>
</CardContent>
Expand Down
3 changes: 3 additions & 0 deletions apps/orchestration-server/src/convexCalls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export const ConvexMutations = {
tasks: {
create: "tasks:create",
},
workOrders: {
dispatch: "workOrders:dispatch",
},
taskRouter: {
autoAssign: "taskRouter:autoAssign",
},
Expand Down
22 changes: 22 additions & 0 deletions apps/orchestration-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ app.get("/health", (c) => {
app.use("/status", requireAuth());
app.use("/tick", requireAuth());
app.use("/agents/*", requireAuth());
app.use("/workorders/*", requireAuth());

// Detailed status
app.get("/status", (c) => {
Expand Down Expand Up @@ -327,6 +328,27 @@ app.post("/agents/stop", async (c) => {
}
});

// Authoritative work-order dispatch path for orchestration consumers
app.post("/workorders/:workOrderId/dispatch", async (c) => {
try {
const workOrderId = c.req.param("workOrderId");
const body = await c.req.json().catch(() => ({}));
const result = await client.mutation(ConvexMutations.workOrders.dispatch as any, {
workOrderId,
workflowId: body.workflowId,
actorType: body.actorType ?? "SYSTEM",
actorId: body.actorId ?? "orchestration-server",
idempotencyKey: body.idempotencyKey ?? `orch-dispatch:${workOrderId}`,
runtime: body.runtime ?? "Hono Orchestration Server",
model: body.model,
worktree: body.worktree,
});
return c.json({ success: true, result });
} catch (err: any) {
return c.json({ error: err.message }, 400);
}
});

// List available personas
app.get("/agents/personas", (c) => {
try {
Expand Down
Loading
Loading