diff options
| author | soryu <soryu@soryu.co> | 2026-02-07 00:01:50 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-02-07 00:01:50 +0000 |
| commit | b8d563d45f14a2b1db1f684aa0a8dcd7e5b6ad56 (patch) | |
| tree | 95543fd150270018e384fbcf9d3df3dc45f052f6 | |
| parent | cececbf326e258211ceae7afce716a5d1e46014f (diff) | |
| download | soryu-b8d563d45f14a2b1db1f684aa0a8dcd7e5b6ad56.tar.gz soryu-b8d563d45f14a2b1db1f684aa0a8dcd7e5b6ad56.zip | |
Remove directives for reimplementation
39 files changed, 336 insertions, 11241 deletions
diff --git a/makima/frontend/src/components/NavStrip.tsx b/makima/frontend/src/components/NavStrip.tsx index 9bb7777..fb95c7f 100644 --- a/makima/frontend/src/components/NavStrip.tsx +++ b/makima/frontend/src/components/NavStrip.tsx @@ -10,7 +10,6 @@ interface NavLink { const NAV_LINKS: NavLink[] = [ { label: "Listen", href: "/listen" }, - { label: "Directives", href: "/directives", requiresAuth: true }, { label: "Contracts", href: "/contracts", requiresAuth: true }, { label: "Board", href: "/workflow", requiresAuth: true }, { label: "Mesh", href: "/mesh", requiresAuth: true }, diff --git a/makima/frontend/src/components/directives/ApprovalsTab.tsx b/makima/frontend/src/components/directives/ApprovalsTab.tsx deleted file mode 100644 index dca48df..0000000 --- a/makima/frontend/src/components/directives/ApprovalsTab.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import type { DirectiveWithProgress } from "../../lib/api"; - -export function ApprovalsTab({ directive, onRefresh }: { directive: DirectiveWithProgress; onRefresh: () => void }) { - if (directive.pendingApprovals.length === 0) { - return ( - <div className="text-center py-8"> - <p className="font-mono text-sm text-[#556677]">No pending approvals</p> - </div> - ); - } - - const handleApprove = async (approvalId: string) => { - try { - const { approveDirectiveRequest } = await import("../../lib/api"); - await approveDirectiveRequest(directive.id, approvalId); - onRefresh(); - } catch (err) { - console.error("Failed to approve:", err); - } - }; - - const handleDeny = async (approvalId: string) => { - try { - const { denyDirectiveRequest } = await import("../../lib/api"); - await denyDirectiveRequest(directive.id, approvalId); - onRefresh(); - } catch (err) { - console.error("Failed to deny:", err); - } - }; - - return ( - <div className="space-y-3"> - {directive.pendingApprovals.map((approval) => { - const urgencyColor = { - low: "text-[#556677]", - normal: "text-[#75aafc]", - high: "text-yellow-400", - critical: "text-red-400", - }[approval.urgency] || "text-[#556677]"; - - return ( - <div - key={approval.id} - className="p-4 bg-[rgba(117,170,252,0.05)] border border-[rgba(117,170,252,0.2)]" - > - <div className="flex items-start justify-between"> - <div> - <div className="flex items-center gap-2"> - <span className="font-mono text-sm text-[#dbe7ff]">{approval.approvalType}</span> - <span className={`font-mono text-[10px] uppercase ${urgencyColor}`}> - {approval.urgency} - </span> - </div> - <p className="font-mono text-xs text-[#9bc3ff] mt-1">{approval.description}</p> - </div> - <div className="flex gap-2"> - <button - onClick={() => handleApprove(approval.id)} - className="px-3 py-1 font-mono text-[10px] text-[#dbe7ff] bg-green-700 border border-green-600 hover:bg-green-600 uppercase" - > - Approve - </button> - <button - onClick={() => handleDeny(approval.id)} - className="px-3 py-1 font-mono text-[10px] text-[#dbe7ff] bg-red-700 border border-red-600 hover:bg-red-600 uppercase" - > - Deny - </button> - </div> - </div> - </div> - ); - })} - </div> - ); -} diff --git a/makima/frontend/src/components/directives/ChainTab.tsx b/makima/frontend/src/components/directives/ChainTab.tsx deleted file mode 100644 index ccefe81..0000000 --- a/makima/frontend/src/components/directives/ChainTab.tsx +++ /dev/null @@ -1,212 +0,0 @@ -import { useState, useMemo } from "react"; -import { - ReactFlow, - Edge, - Controls, - Background, - BackgroundVariant, - MarkerType, -} from "@xyflow/react"; -import "@xyflow/react/dist/style.css"; -import type { DirectiveWithProgress, DirectiveGraphResponse } from "../../lib/api"; -import { StepNodeComponent, stepStatusStyles } from "./StepNode"; - -// Node types for React Flow -const nodeTypes = { - step: StepNodeComponent, -}; - -export function ChainTab({ directive, graph }: { directive: DirectiveWithProgress; graph: DirectiveGraphResponse | null }) { - const [viewMode, setViewMode] = useState<"dag" | "list">("dag"); - - // Convert graph to React Flow nodes and edges - const { nodes, edges } = useMemo(() => { - if (!graph || !graph.nodes.length) { - // Fallback: generate positions from directive.steps - const stepNodes = directive.steps.map((step, index) => ({ - id: step.id, - type: "step" as const, - position: { - x: (index % 3) * 220 + 50, - y: Math.floor(index / 3) * 120 + 50, - }, - data: { - id: step.id, - name: step.name, - stepType: step.stepType, - status: step.status, - confidenceScore: step.confidenceScore, - confidenceLevel: step.confidenceLevel, - contractId: step.contractId, - editorX: null, - editorY: null, - }, - })); - - // Build edges from dependencies - const stepEdges: Edge[] = []; - directive.steps.forEach((step) => { - (step.dependsOn ?? []).forEach((depName) => { - const depStep = directive.steps.find((s) => s.name === depName); - if (depStep) { - stepEdges.push({ - id: `${depStep.id}-${step.id}`, - source: depStep.id, - target: step.id, - type: "smoothstep", - markerEnd: { type: MarkerType.ArrowClosed, color: "#556677" }, - style: { stroke: "#556677", strokeWidth: 2 }, - }); - } - }); - }); - - return { nodes: stepNodes, edges: stepEdges }; - } - - // Use graph data - const graphNodes = graph.nodes.map((node) => ({ - id: node.id, - type: "step" as const, - position: { - x: node.editorX ?? 50, - y: node.editorY ?? 50, - }, - data: { ...node }, - })); - - const graphEdges: Edge[] = graph.edges.map((edge) => ({ - id: `${edge.source}-${edge.target}`, - source: edge.source, - target: edge.target, - type: "smoothstep", - markerEnd: { type: MarkerType.ArrowClosed, color: "#556677" }, - style: { stroke: "#556677", strokeWidth: 2 }, - })); - - return { nodes: graphNodes, edges: graphEdges }; - }, [graph, directive.steps]); - - if (!directive.chain) { - return ( - <div className="text-center py-8"> - <p className="font-mono text-sm text-[#556677]"> - No chain generated yet. Start the directive to generate a chain. - </p> - </div> - ); - } - - return ( - <div className="space-y-4"> - {/* Chain info header */} - <div className="flex items-center justify-between p-3 bg-[rgba(117,170,252,0.05)] border border-[rgba(117,170,252,0.15)]"> - <div> - <div className="font-mono text-sm text-[#dbe7ff]">{directive.chain.name}</div> - <div className="font-mono text-[10px] text-[#556677]">Generation {directive.chain.generation}</div> - </div> - <div className="flex items-center gap-4"> - <div className="font-mono text-xs text-[#556677]"> - {directive.chain.completedSteps}/{directive.chain.totalSteps} steps - </div> - {/* View toggle */} - <div className="flex border border-[rgba(117,170,252,0.2)]"> - <button - onClick={() => setViewMode("dag")} - className={`px-2 py-1 font-mono text-[10px] uppercase ${ - viewMode === "dag" - ? "bg-[rgba(117,170,252,0.2)] text-[#75aafc]" - : "text-[#556677] hover:text-[#75aafc]" - }`} - > - DAG - </button> - <button - onClick={() => setViewMode("list")} - className={`px-2 py-1 font-mono text-[10px] uppercase ${ - viewMode === "list" - ? "bg-[rgba(117,170,252,0.2)] text-[#75aafc]" - : "text-[#556677] hover:text-[#75aafc]" - }`} - > - List - </button> - </div> - </div> - </div> - - {viewMode === "dag" ? ( - /* DAG visualization */ - <div className="h-[400px] border border-[rgba(117,170,252,0.1)] rounded bg-[#050d18]"> - {directive.steps.length === 0 ? ( - <div className="flex items-center justify-center h-full"> - <p className="font-mono text-sm text-[#556677]">No steps in chain</p> - </div> - ) : ( - <ReactFlow - nodes={nodes} - edges={edges} - nodeTypes={nodeTypes} - fitView - fitViewOptions={{ padding: 0.2 }} - minZoom={0.5} - maxZoom={1.5} - defaultEdgeOptions={{ - type: "smoothstep", - style: { stroke: "#556677", strokeWidth: 2 }, - }} - > - <Background variant={BackgroundVariant.Dots} gap={20} size={1} color="#1a2a3a" /> - <Controls className="!bg-[#0a1628] !border-[rgba(117,170,252,0.2)]" /> - </ReactFlow> - )} - </div> - ) : ( - /* List view */ - <div className="space-y-2"> - <h3 className="font-mono text-xs text-[#75aafc] uppercase">Steps</h3> - {directive.steps.length === 0 ? ( - <p className="font-mono text-sm text-[#556677]">No steps in chain</p> - ) : ( - directive.steps.map((step) => { - const styles = stepStatusStyles[step.status] || stepStatusStyles.pending; - - return ( - <div - key={step.id} - className="p-3 bg-[rgba(117,170,252,0.02)] border border-[rgba(117,170,252,0.1)]" - > - <div className="flex items-center justify-between"> - <div className="flex items-center gap-2"> - <span className="font-mono text-sm text-[#dbe7ff]">{step.name}</span> - <span - className="px-1.5 py-0.5 font-mono text-[10px] uppercase border" - style={{ color: styles.text, borderColor: `${styles.border}50` }} - > - {step.status} - </span> - {step.confidenceScore !== null && ( - <span className="font-mono text-[10px] text-[#556677]"> - ({Math.round(step.confidenceScore * 100)}%) - </span> - )} - </div> - <div className="font-mono text-[10px] text-[#556677]">{step.stepType}</div> - </div> - {step.description && ( - <p className="font-mono text-xs text-[#556677] mt-1">{step.description}</p> - )} - {step.dependsOn?.length > 0 && ( - <div className="font-mono text-[10px] text-[#556677] mt-1"> - Depends on: {step.dependsOn.join(", ")} - </div> - )} - </div> - ); - }) - )} - </div> - )} - </div> - ); -} diff --git a/makima/frontend/src/components/directives/CreateDirectiveModal.tsx b/makima/frontend/src/components/directives/CreateDirectiveModal.tsx deleted file mode 100644 index 7f52a7e..0000000 --- a/makima/frontend/src/components/directives/CreateDirectiveModal.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { useState, useEffect } from "react"; -import type { AutonomyLevel, RepositoryHistoryEntry } from "../../lib/api"; -import { getRepositorySuggestions } from "../../lib/api"; - -interface CreateDirectiveModalProps { - onSubmit: (goal: string, repositoryUrl: string | undefined, autonomyLevel: AutonomyLevel) => void; - onCancel: () => void; -} - -export function CreateDirectiveModal({ onSubmit, onCancel }: CreateDirectiveModalProps) { - const [goal, setGoal] = useState(""); - const [repositoryUrl, setRepositoryUrl] = useState(""); - const [autonomyLevel, setAutonomyLevel] = useState<AutonomyLevel>("guardrails"); - const [suggestions, setSuggestions] = useState<RepositoryHistoryEntry[]>([]); - const [showSuggestions, setShowSuggestions] = useState(false); - - // Load suggestions - useEffect(() => { - getRepositorySuggestions("remote", undefined, 5) - .then((res) => { - setSuggestions(res.entries); - }) - .catch(() => { - setSuggestions([]); - }); - }, []); - - const handleSubmit = () => { - if (goal.trim()) { - onSubmit(goal.trim(), repositoryUrl.trim() || undefined, autonomyLevel); - } - }; - - return ( - <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"> - <div className="w-full max-w-lg p-6 bg-[#0a1628] border border-[rgba(117,170,252,0.3)] max-h-[90vh] overflow-y-auto"> - <h3 className="font-mono text-sm text-[#75aafc] uppercase mb-4"> - Create Directive - </h3> - - <div className="space-y-4"> - {/* Goal */} - <div> - <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1"> - Goal * - </label> - <textarea - value={goal} - onChange={(e) => setGoal(e.target.value)} - placeholder="Describe what you want to accomplish..." - rows={3} - className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc] resize-none" - autoFocus - /> - </div> - - {/* Repository URL */} - <div> - <label className="block font-mono text-xs text-[#8b949e] uppercase mb-1"> - Repository URL (optional) - </label> - <div className="relative"> - <input - type="text" - value={repositoryUrl} - onChange={(e) => setRepositoryUrl(e.target.value)} - onFocus={() => suggestions.length > 0 && setShowSuggestions(true)} - onBlur={() => setTimeout(() => setShowSuggestions(false), 200)} - placeholder="https://github.com/owner/repo" - className="w-full px-3 py-2 bg-[#0d1b2d] border border-[#3f6fb3] text-[#dbe7ff] font-mono text-sm focus:outline-none focus:border-[#75aafc]" - /> - {showSuggestions && suggestions.length > 0 && ( - <div className="absolute top-full left-0 right-0 mt-1 border border-[rgba(117,170,252,0.2)] bg-[#0a1525] max-h-32 overflow-y-auto z-10"> - {suggestions.map((s) => ( - <button - key={s.id} - type="button" - onClick={() => { - setRepositoryUrl(s.repositoryUrl || ""); - setShowSuggestions(false); - }} - className="w-full text-left px-3 py-2 font-mono text-xs hover:bg-[rgba(117,170,252,0.1)] border-b border-[rgba(117,170,252,0.1)] last:border-b-0" - > - <div className="text-[#9bc3ff] truncate">{s.name}</div> - <div className="text-[10px] text-[#556677] truncate">{s.repositoryUrl}</div> - </button> - ))} - </div> - )} - </div> - </div> - - {/* Autonomy Level */} - <div> - <label className="block font-mono text-xs text-[#8b949e] uppercase mb-2"> - Autonomy Level - </label> - <div className="flex gap-2"> - {(["full_auto", "guardrails", "manual"] as const).map((level) => ( - <button - key={level} - type="button" - onClick={() => setAutonomyLevel(level)} - className={`flex-1 px-3 py-2 font-mono text-xs uppercase ${ - autonomyLevel === level - ? "text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3]" - : "text-[#556677] border border-[rgba(117,170,252,0.2)] hover:border-[#3f6fb3]" - }`} - > - {level.replace("_", " ")} - </button> - ))} - </div> - <p className="font-mono text-[10px] text-[#556677] mt-1"> - {autonomyLevel === "full_auto" && "Automatic progression without approval gates"} - {autonomyLevel === "guardrails" && "Request approval for yellow/red confidence scores"} - {autonomyLevel === "manual" && "Request approval for all step completions"} - </p> - </div> - - <p className="font-mono text-xs text-[#8b949e]"> - A directive is a top-level goal that generates a chain of steps. Each step spawns - contracts that are verified before progression. - </p> - - {/* Actions */} - <div className="flex gap-2 justify-end pt-2"> - <button - onClick={onCancel} - className="px-4 py-2 font-mono text-xs text-[#9bc3ff] hover:text-[#dbe7ff] transition-colors" - > - Cancel - </button> - <button - onClick={handleSubmit} - disabled={!goal.trim()} - className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors disabled:opacity-50 disabled:cursor-not-allowed" - > - Create - </button> - </div> - </div> - </div> - </div> - ); -} diff --git a/makima/frontend/src/components/directives/DirectiveDetail.tsx b/makima/frontend/src/components/directives/DirectiveDetail.tsx deleted file mode 100644 index 06b24bb..0000000 --- a/makima/frontend/src/components/directives/DirectiveDetail.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { useState } from "react"; -import type { DirectiveWithProgress, DirectiveGraphResponse } from "../../lib/api"; -import { OverviewTab } from "./OverviewTab"; -import { ChainTab } from "./ChainTab"; -import { EventsTab } from "./EventsTab"; -import { EvaluationsTab } from "./EvaluationsTab"; -import { ApprovalsTab } from "./ApprovalsTab"; -import { VerifiersTab } from "./VerifiersTab"; - -interface DirectiveDetailProps { - directive: DirectiveWithProgress; - graph: DirectiveGraphResponse | null; - loading: boolean; - onBack: () => void; - onRefresh: () => void; - onStart: () => void; - onPause: () => void; - onResume: () => void; - onStop: () => void; -} - -export function DirectiveDetail({ - directive, - graph, - loading, - onBack, - onRefresh, - onStart, - onPause, - onResume, - onStop, -}: DirectiveDetailProps) { - const [activeTab, setActiveTab] = useState<"overview" | "chain" | "events" | "evaluations" | "approvals" | "verifiers">("overview"); - - if (loading) { - return ( - <div className="panel h-full flex items-center justify-center"> - <p className="font-mono text-xs text-[#556677]">Loading...</p> - </div> - ); - } - - const statusColor = { - draft: "text-[#556677] bg-[#556677]/10 border-[#556677]/30", - planning: "text-yellow-400 bg-yellow-400/10 border-yellow-400/30", - active: "text-green-400 bg-green-400/10 border-green-400/30", - paused: "text-yellow-400 bg-yellow-400/10 border-yellow-400/30", - completed: "text-[#75aafc] bg-[#75aafc]/10 border-[#75aafc]/30", - archived: "text-[#556677] bg-[#556677]/10 border-[#556677]/30", - failed: "text-red-400 bg-red-400/10 border-red-400/30", - }[directive.status] || "text-[#556677] bg-[#556677]/10 border-[#556677]/30"; - - return ( - <div className="panel h-full flex flex-col overflow-hidden"> - {/* Header */} - <div className="p-3 border-b border-[rgba(117,170,252,0.15)]"> - <div className="flex items-center justify-between"> - <div className="flex items-center gap-3"> - <button - onClick={onBack} - className="font-mono text-xs text-[#556677] hover:text-[#9bc3ff]" - > - ← Back - </button> - <h2 className="font-mono text-sm text-[#dbe7ff]"> - {directive.title || directive.goal.slice(0, 50)} - </h2> - <span className={`px-2 py-0.5 font-mono text-[10px] uppercase border ${statusColor}`}> - {directive.status} - </span> - </div> - <div className="flex items-center gap-2"> - {directive.status === "draft" && ( - <button - onClick={onStart} - className="px-3 py-1 font-mono text-[10px] text-[#dbe7ff] bg-green-700 border border-green-600 hover:bg-green-600 uppercase" - > - Start - </button> - )} - {directive.status === "active" && ( - <button - onClick={onPause} - className="px-3 py-1 font-mono text-[10px] text-[#dbe7ff] bg-yellow-700 border border-yellow-600 hover:bg-yellow-600 uppercase" - > - Pause - </button> - )} - {directive.status === "paused" && ( - <button - onClick={onResume} - className="px-3 py-1 font-mono text-[10px] text-[#dbe7ff] bg-green-700 border border-green-600 hover:bg-green-600 uppercase" - > - Resume - </button> - )} - {["active", "paused"].includes(directive.status) && ( - <button - onClick={onStop} - className="px-3 py-1 font-mono text-[10px] text-[#dbe7ff] bg-red-700 border border-red-600 hover:bg-red-600 uppercase" - > - Stop - </button> - )} - <button - onClick={onRefresh} - className="px-3 py-1 font-mono text-[10px] text-[#9bc3ff] hover:text-[#dbe7ff]" - > - Refresh - </button> - </div> - </div> - </div> - - {/* Tabs */} - <div className="flex gap-1 p-2 border-b border-[rgba(117,170,252,0.1)]"> - {(["overview", "chain", "events", "evaluations", "approvals", "verifiers"] as const).map((tab) => ( - <button - key={tab} - onClick={() => setActiveTab(tab)} - className={`px-3 py-1.5 font-mono text-[10px] uppercase ${ - activeTab === tab - ? "text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3]" - : "text-[#556677] hover:text-[#9bc3ff]" - }`} - > - {tab} - {tab === "approvals" && directive.pendingApprovals.length > 0 && ( - <span className="ml-1 px-1 bg-yellow-500 text-black rounded text-[8px]"> - {directive.pendingApprovals.length} - </span> - )} - </button> - ))} - </div> - - {/* Tab Content */} - <div className="flex-1 overflow-y-auto p-4"> - {activeTab === "overview" && ( - <OverviewTab directive={directive} /> - )} - {activeTab === "chain" && ( - <ChainTab directive={directive} graph={graph} /> - )} - {activeTab === "events" && ( - <EventsTab directive={directive} /> - )} - {activeTab === "evaluations" && ( - <EvaluationsTab directive={directive} /> - )} - {activeTab === "approvals" && ( - <ApprovalsTab directive={directive} onRefresh={onRefresh} /> - )} - {activeTab === "verifiers" && ( - <VerifiersTab directive={directive} /> - )} - </div> - </div> - ); -} diff --git a/makima/frontend/src/components/directives/DirectiveList.tsx b/makima/frontend/src/components/directives/DirectiveList.tsx deleted file mode 100644 index d0371e0..0000000 --- a/makima/frontend/src/components/directives/DirectiveList.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { useState } from "react"; -import type { DirectiveSummary } from "../../lib/api"; -import { DirectiveListItem } from "./DirectiveListItem"; - -interface DirectiveListProps { - directives: DirectiveSummary[]; - loading: boolean; - onSelect: (id: string) => void; - onCreate: () => void; - selectedId?: string; - onArchive: (directive: DirectiveSummary) => void; -} - -export function DirectiveList({ - directives, - loading, - onSelect, - onCreate, - selectedId, - onArchive, -}: DirectiveListProps) { - const [filter, setFilter] = useState<"all" | "active" | "completed" | "failed">("all"); - - const filteredDirectives = directives.filter((d) => { - if (filter === "all") return true; - if (filter === "active") return ["draft", "planning", "active", "paused"].includes(d.status); - if (filter === "completed") return d.status === "completed"; - if (filter === "failed") return d.status === "failed"; - return true; - }); - - return ( - <div className="panel h-full flex flex-col overflow-hidden"> - <div className="flex items-center justify-between p-3 border-b border-[rgba(117,170,252,0.15)]"> - <h2 className="font-mono text-sm text-[#75aafc] uppercase">Directives</h2> - <button - onClick={onCreate} - className="px-3 py-1 font-mono text-[10px] text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase" - > - + New - </button> - </div> - - {/* Filters */} - <div className="flex gap-1 p-2 border-b border-[rgba(117,170,252,0.1)]"> - {(["all", "active", "completed", "failed"] as const).map((f) => ( - <button - key={f} - onClick={() => setFilter(f)} - className={`px-2 py-1 font-mono text-[10px] uppercase ${ - filter === f - ? "text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3]" - : "text-[#556677] hover:text-[#9bc3ff]" - }`} - > - {f} - </button> - ))} - </div> - - {/* List */} - <div className="flex-1 overflow-y-auto"> - {loading ? ( - <div className="p-4 text-center"> - <p className="font-mono text-xs text-[#556677]">Loading...</p> - </div> - ) : filteredDirectives.length === 0 ? ( - <div className="p-4 text-center"> - <p className="font-mono text-xs text-[#556677]">No directives found</p> - </div> - ) : ( - filteredDirectives.map((d) => ( - <DirectiveListItem - key={d.id} - directive={d} - selected={d.id === selectedId} - onClick={() => onSelect(d.id)} - onArchive={() => onArchive(d)} - /> - )) - )} - </div> - </div> - ); -} diff --git a/makima/frontend/src/components/directives/DirectiveListItem.tsx b/makima/frontend/src/components/directives/DirectiveListItem.tsx deleted file mode 100644 index 6ff82e4..0000000 --- a/makima/frontend/src/components/directives/DirectiveListItem.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import type { DirectiveSummary } from "../../lib/api"; - -interface DirectiveListItemProps { - directive: DirectiveSummary; - selected: boolean; - onClick: () => void; - onArchive: () => void; -} - -export function DirectiveListItem({ directive, selected, onClick, onArchive }: DirectiveListItemProps) { - const progress = directive.totalSteps > 0 - ? Math.round((directive.completedSteps / directive.totalSteps) * 100) - : 0; - - const statusColor = { - draft: "text-[#556677]", - planning: "text-yellow-400", - active: "text-green-400", - paused: "text-yellow-400", - completed: "text-[#75aafc]", - archived: "text-[#556677]", - failed: "text-red-400", - }[directive.status] || "text-[#556677]"; - - const confidenceColor = { - green: "bg-green-500", - yellow: "bg-yellow-500", - red: "bg-red-500", - }[directive.currentConfidence !== null && directive.currentConfidence >= 0.8 - ? "green" - : directive.currentConfidence !== null && directive.currentConfidence >= 0.5 - ? "yellow" - : "red"] || "bg-[#556677]"; - - return ( - <div - onClick={onClick} - className={`p-3 cursor-pointer border-b border-[rgba(117,170,252,0.1)] hover:bg-[rgba(117,170,252,0.05)] ${ - selected ? "bg-[rgba(117,170,252,0.1)]" : "" - }`} - > - <div className="flex items-start justify-between gap-2"> - <div className="flex-1 min-w-0"> - <div className="font-mono text-sm text-[#dbe7ff] truncate"> - {directive.title || directive.goal.slice(0, 50)} - </div> - <div className="flex items-center gap-2 mt-1"> - <span className={`font-mono text-[10px] uppercase ${statusColor}`}> - {directive.status} - </span> - <span className="font-mono text-[10px] text-[#556677]"> - {directive.completedSteps}/{directive.totalSteps} steps - </span> - </div> - </div> - <div className="flex flex-col items-end gap-1"> - {directive.currentConfidence !== null && ( - <div className={`w-2 h-2 rounded-full ${confidenceColor}`} title={`Confidence: ${Math.round(directive.currentConfidence * 100)}%`} /> - )} - <button - onClick={(e) => { - e.stopPropagation(); - onArchive(); - }} - className="font-mono text-[10px] text-[#556677] hover:text-red-400" - > - Archive - </button> - </div> - </div> - - {/* Progress bar */} - {directive.totalSteps > 0 && ( - <div className="mt-2 h-1 bg-[rgba(117,170,252,0.1)] overflow-hidden"> - <div - className="h-full bg-[#75aafc] transition-all duration-300" - style={{ width: `${progress}%` }} - /> - </div> - )} - </div> - ); -} diff --git a/makima/frontend/src/components/directives/EvaluationsTab.tsx b/makima/frontend/src/components/directives/EvaluationsTab.tsx deleted file mode 100644 index c1d65db..0000000 --- a/makima/frontend/src/components/directives/EvaluationsTab.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import type { DirectiveWithProgress } from "../../lib/api"; - -export function EvaluationsTab({ directive: _directive }: { directive: DirectiveWithProgress }) { - // TODO: Fetch evaluations separately - return ( - <div className="text-center py-8"> - <p className="font-mono text-sm text-[#556677]"> - Evaluations will be shown here after steps are evaluated - </p> - </div> - ); -} diff --git a/makima/frontend/src/components/directives/EventsTab.tsx b/makima/frontend/src/components/directives/EventsTab.tsx deleted file mode 100644 index 4dd739a..0000000 --- a/makima/frontend/src/components/directives/EventsTab.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { useMemo } from "react"; -import type { DirectiveWithProgress } from "../../lib/api"; -import { useDirectiveEventSubscription } from "../../hooks/useDirectives"; - -export function EventsTab({ directive }: { directive: DirectiveWithProgress }) { - // Subscribe to real-time events via SSE - const { events: streamEvents, isConnected, error: sseError } = useDirectiveEventSubscription(directive.id); - - // Combine initial events with streamed events (avoiding duplicates) - const allEvents = useMemo(() => { - const eventMap = new Map(); - // Add initial events first - directive.recentEvents.forEach((e) => eventMap.set(e.id, e)); - // Add streamed events (will override any duplicates) - streamEvents.forEach((e) => eventMap.set(e.id, e)); - // Sort by created_at descending (most recent first) - return Array.from(eventMap.values()).sort( - (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() - ); - }, [directive.recentEvents, streamEvents]); - - return ( - <div className="space-y-4"> - {/* Connection status */} - <div className="flex items-center justify-between text-[10px] font-mono"> - <div className="flex items-center gap-2"> - <span className={isConnected ? "text-green-400" : "text-[#556677]"}> - {isConnected ? "\u25CF Live" : "\u25CB Connecting..."} - </span> - {sseError && <span className="text-red-400">{sseError}</span>} - </div> - <span className="text-[#556677]">{allEvents.length} events</span> - </div> - - {/* Event list */} - {allEvents.length === 0 ? ( - <div className="text-center py-8"> - <p className="font-mono text-sm text-[#556677]">No events yet</p> - </div> - ) : ( - <div className="space-y-2"> - {allEvents.map((event) => { - const severityColors: Record<string, string> = { - info: "text-[#75aafc]", - warning: "text-yellow-400", - error: "text-red-400", - critical: "text-red-600", - }; - const severityColor = severityColors[event.severity] || "text-[#556677]"; - - return ( - <div - key={event.id} - className="p-3 bg-[rgba(117,170,252,0.02)] border border-[rgba(117,170,252,0.1)]" - > - <div className="flex items-center justify-between"> - <div className="flex items-center gap-2"> - <span className={`font-mono text-xs ${severityColor}`}>{event.eventType}</span> - <span className="font-mono text-[10px] text-[#556677]">{event.actorType}</span> - </div> - <span className="font-mono text-[10px] text-[#556677]"> - {new Date(event.createdAt).toLocaleString()} - </span> - </div> - {event.eventData != null && ( - <pre className="font-mono text-[10px] text-[#556677] mt-1 overflow-x-auto"> - {JSON.stringify(event.eventData, null, 2)} - </pre> - )} - </div> - ); - })} - </div> - )} - </div> - ); -} diff --git a/makima/frontend/src/components/directives/OverviewTab.tsx b/makima/frontend/src/components/directives/OverviewTab.tsx deleted file mode 100644 index 41cd7dc..0000000 --- a/makima/frontend/src/components/directives/OverviewTab.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import type { DirectiveWithProgress } from "../../lib/api"; - -export function OverviewTab({ directive }: { directive: DirectiveWithProgress }) { - return ( - <div className="space-y-6"> - {/* Goal */} - <div> - <h3 className="font-mono text-xs text-[#75aafc] uppercase mb-2">Goal</h3> - <p className="font-mono text-sm text-[#dbe7ff] whitespace-pre-wrap"> - {directive.goal} - </p> - </div> - - {/* Progress */} - <div> - <h3 className="font-mono text-xs text-[#75aafc] uppercase mb-2">Progress</h3> - <div className="grid grid-cols-3 gap-4"> - <div className="p-3 bg-[rgba(117,170,252,0.05)] border border-[rgba(117,170,252,0.15)]"> - <div className="font-mono text-2xl text-[#dbe7ff]"> - {directive.chain?.completedSteps || 0} - </div> - <div className="font-mono text-[10px] text-[#556677] uppercase">Completed Steps</div> - </div> - <div className="p-3 bg-[rgba(117,170,252,0.05)] border border-[rgba(117,170,252,0.15)]"> - <div className="font-mono text-2xl text-[#dbe7ff]"> - {directive.chain?.totalSteps || 0} - </div> - <div className="font-mono text-[10px] text-[#556677] uppercase">Total Steps</div> - </div> - <div className="p-3 bg-[rgba(117,170,252,0.05)] border border-[rgba(117,170,252,0.15)]"> - <div className="font-mono text-2xl text-[#dbe7ff]"> - {directive.chain?.currentConfidence != null - ? `${Math.round((directive.chain?.currentConfidence ?? 0) * 100)}%` - : "-"} - </div> - <div className="font-mono text-[10px] text-[#556677] uppercase">Confidence</div> - </div> - </div> - </div> - - {/* Configuration */} - <div> - <h3 className="font-mono text-xs text-[#75aafc] uppercase mb-2">Configuration</h3> - <div className="grid grid-cols-2 gap-2 text-sm"> - <div className="flex justify-between"> - <span className="font-mono text-[#556677]">Autonomy Level</span> - <span className="font-mono text-[#dbe7ff]">{directive.autonomyLevel}</span> - </div> - <div className="flex justify-between"> - <span className="font-mono text-[#556677]">Max Rework Cycles</span> - <span className="font-mono text-[#dbe7ff]">{directive.maxReworkCycles}</span> - </div> - <div className="flex justify-between"> - <span className="font-mono text-[#556677]">Green Threshold</span> - <span className="font-mono text-[#dbe7ff]">{directive.confidenceThresholdGreen}</span> - </div> - <div className="flex justify-between"> - <span className="font-mono text-[#556677]">Yellow Threshold</span> - <span className="font-mono text-[#dbe7ff]">{directive.confidenceThresholdYellow}</span> - </div> - </div> - </div> - - {/* Repository */} - {directive.repositoryUrl && ( - <div> - <h3 className="font-mono text-xs text-[#75aafc] uppercase mb-2">Repository</h3> - <p className="font-mono text-sm text-[#9bc3ff]">{directive.repositoryUrl}</p> - </div> - )} - </div> - ); -} diff --git a/makima/frontend/src/components/directives/StepNode.tsx b/makima/frontend/src/components/directives/StepNode.tsx deleted file mode 100644 index e54f5eb..0000000 --- a/makima/frontend/src/components/directives/StepNode.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { Handle, Position } from "@xyflow/react"; -import type { StepStatus, ConfidenceLevel, DirectiveGraphNode } from "../../lib/api"; - -// Step status colors for both list and DAG views -export const stepStatusStyles: Record<StepStatus, { border: string; bg: string; text: string }> = { - pending: { border: "#556677", bg: "#556677", text: "#556677" }, - ready: { border: "#3b82f6", bg: "#3b82f6", text: "#3b82f6" }, - running: { border: "#22c55e", bg: "#22c55e", text: "#22c55e" }, - evaluating: { border: "#eab308", bg: "#eab308", text: "#eab308" }, - passed: { border: "#75aafc", bg: "#75aafc", text: "#75aafc" }, - failed: { border: "#ef4444", bg: "#ef4444", text: "#ef4444" }, - rework: { border: "#f97316", bg: "#f97316", text: "#f97316" }, - skipped: { border: "#556677", bg: "#556677", text: "#556677" }, - blocked: { border: "#ef4444", bg: "#ef4444", text: "#ef4444" }, -}; - -// Confidence level colors -export const confidenceColors: Record<ConfidenceLevel, string> = { - green: "#22c55e", - yellow: "#eab308", - red: "#ef4444", -}; - -// Node dimensions -export const NODE_WIDTH = 180; -export const NODE_HEIGHT = 70; - -// Custom node component for steps -export function StepNodeComponent({ data }: { data: DirectiveGraphNode & { selected?: boolean } }) { - const styles = stepStatusStyles[data.status] || stepStatusStyles.pending; - const isRunning = data.status === "running" || data.status === "evaluating"; - - return ( - <div - className={`rounded-lg border-2 bg-[#0a1628] overflow-hidden ${ - isRunning ? "animate-pulse" : "" - }`} - style={{ - width: NODE_WIDTH, - height: NODE_HEIGHT, - borderColor: styles.border, - borderStyle: data.status === "pending" ? "dashed" : "solid", - }} - > - <Handle - type="target" - position={Position.Top} - className="!bg-[#75aafc] !w-3 !h-3 !border-2 !border-[#0a1628]" - /> - - {/* Status indicator bar */} - <div className="h-1.5" style={{ backgroundColor: styles.bg }} /> - - {/* Content */} - <div className="p-2"> - <div className="flex items-center justify-between mb-1"> - <span className="font-mono text-xs text-[#dbe7ff] truncate flex-1">{data.name}</span> - {data.confidenceScore !== null && data.confidenceLevel && ( - <div - className="w-2 h-2 rounded-full flex-shrink-0 ml-1" - style={{ backgroundColor: confidenceColors[data.confidenceLevel] }} - title={`Confidence: ${Math.round(data.confidenceScore * 100)}%`} - /> - )} - </div> - <div className="flex items-center justify-between"> - <span - className="font-mono text-[10px] uppercase px-1.5 py-0.5 rounded" - style={{ - color: styles.text, - backgroundColor: `${styles.bg}20`, - }} - > - {data.status} - </span> - <span className="font-mono text-[10px] text-[#8b949e]">{data.stepType}</span> - </div> - </div> - - <Handle - type="source" - position={Position.Bottom} - className="!bg-[#f59e0b] !w-3 !h-3 !border-2 !border-[#0a1628]" - /> - </div> - ); -} diff --git a/makima/frontend/src/components/directives/VerifiersTab.tsx b/makima/frontend/src/components/directives/VerifiersTab.tsx deleted file mode 100644 index cfcfdd8..0000000 --- a/makima/frontend/src/components/directives/VerifiersTab.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import type { DirectiveWithProgress } from "../../lib/api"; - -export function VerifiersTab({ directive: _directive }: { directive: DirectiveWithProgress }) { - // TODO: Fetch verifiers separately - return ( - <div className="text-center py-8"> - <p className="font-mono text-sm text-[#556677]"> - Verifiers will be shown here. Use auto-detect to find available verifiers. - </p> - </div> - ); -} diff --git a/makima/frontend/src/components/directives/index.ts b/makima/frontend/src/components/directives/index.ts deleted file mode 100644 index 718b1f2..0000000 --- a/makima/frontend/src/components/directives/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export { DirectiveList } from "./DirectiveList"; -export { DirectiveListItem } from "./DirectiveListItem"; -export { DirectiveDetail } from "./DirectiveDetail"; -export { OverviewTab } from "./OverviewTab"; -export { ChainTab } from "./ChainTab"; -export { EventsTab } from "./EventsTab"; -export { EvaluationsTab } from "./EvaluationsTab"; -export { ApprovalsTab } from "./ApprovalsTab"; -export { VerifiersTab } from "./VerifiersTab"; -export { CreateDirectiveModal } from "./CreateDirectiveModal"; -export { StepNodeComponent, stepStatusStyles, confidenceColors, NODE_WIDTH, NODE_HEIGHT } from "./StepNode"; diff --git a/makima/frontend/src/hooks/useDirectiveDetail.ts b/makima/frontend/src/hooks/useDirectiveDetail.ts deleted file mode 100644 index 1167242..0000000 --- a/makima/frontend/src/hooks/useDirectiveDetail.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { useState, useCallback, useEffect } from "react"; -import { - getDirective, - getDirectiveGraph, - startDirective, - pauseDirective, - resumeDirective, - stopDirective, - type DirectiveWithProgress, - type DirectiveGraphResponse, - type StartDirectiveResponse, -} from "../lib/api"; - -interface UseDirectiveDetailResult { - directive: DirectiveWithProgress | null; - graph: DirectiveGraphResponse | null; - loading: boolean; - error: string | null; - refresh: () => Promise<void>; - start: () => Promise<StartDirectiveResponse | null>; - pause: () => Promise<boolean>; - resume: () => Promise<boolean>; - stop: () => Promise<boolean>; -} - -export function useDirectiveDetail(directiveId: string | undefined): UseDirectiveDetailResult { - const [directive, setDirective] = useState<DirectiveWithProgress | null>(null); - const [graph, setGraph] = useState<DirectiveGraphResponse | null>(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState<string | null>(null); - - const fetchDetail = useCallback(async () => { - if (!directiveId) { - setDirective(null); - setGraph(null); - return; - } - - setLoading(true); - setError(null); - try { - const [d, g] = await Promise.all([ - getDirective(directiveId), - getDirectiveGraph(directiveId).catch(() => null), - ]); - setDirective(d); - setGraph(g); - } catch (err) { - console.error("Failed to fetch directive detail:", err); - setError(err instanceof Error ? err.message : "Failed to fetch directive"); - setDirective(null); - setGraph(null); - } finally { - setLoading(false); - } - }, [directiveId]); - - useEffect(() => { - fetchDetail(); - }, [fetchDetail]); - - const start = useCallback(async (): Promise<StartDirectiveResponse | null> => { - if (!directiveId) return null; - try { - const response = await startDirective(directiveId); - await fetchDetail(); - return response; - } catch (err) { - console.error("Failed to start directive:", err); - setError(err instanceof Error ? err.message : "Failed to start directive"); - return null; - } - }, [directiveId, fetchDetail]); - - const pause = useCallback(async (): Promise<boolean> => { - if (!directiveId) return false; - try { - await pauseDirective(directiveId); - await fetchDetail(); - return true; - } catch (err) { - console.error("Failed to pause directive:", err); - setError(err instanceof Error ? err.message : "Failed to pause directive"); - return false; - } - }, [directiveId, fetchDetail]); - - const resume = useCallback(async (): Promise<boolean> => { - if (!directiveId) return false; - try { - await resumeDirective(directiveId); - await fetchDetail(); - return true; - } catch (err) { - console.error("Failed to resume directive:", err); - setError(err instanceof Error ? err.message : "Failed to resume directive"); - return false; - } - }, [directiveId, fetchDetail]); - - const stop = useCallback(async (): Promise<boolean> => { - if (!directiveId) return false; - try { - await stopDirective(directiveId); - await fetchDetail(); - return true; - } catch (err) { - console.error("Failed to stop directive:", err); - setError(err instanceof Error ? err.message : "Failed to stop directive"); - return false; - } - }, [directiveId, fetchDetail]); - - return { - directive, - graph, - loading, - error, - refresh: fetchDetail, - start, - pause, - resume, - stop, - }; -} diff --git a/makima/frontend/src/hooks/useDirectives.ts b/makima/frontend/src/hooks/useDirectives.ts deleted file mode 100644 index 7ae24a5..0000000 --- a/makima/frontend/src/hooks/useDirectives.ts +++ /dev/null @@ -1,298 +0,0 @@ -import { useState, useCallback, useEffect, useRef } from "react"; -import { - listDirectives, - getDirective, - createDirective, - updateDirective, - archiveDirective, - startDirective, - pauseDirective, - resumeDirective, - stopDirective, - getDirectiveGraph, - subscribeToDirectiveEvents, - type DirectiveSummary, - type DirectiveWithProgress, - type DirectiveGraphResponse, - type DirectiveStatus, - type DirectiveEvent, - type CreateDirectiveRequest, - type UpdateDirectiveRequest, - type StartDirectiveResponse, -} from "../lib/api"; - -interface UseDirectivesResult { - directives: DirectiveSummary[]; - loading: boolean; - error: string | null; - refresh: () => Promise<void>; - createNewDirective: (req: CreateDirectiveRequest) => Promise<DirectiveWithProgress | null>; - updateExistingDirective: ( - directiveId: string, - req: UpdateDirectiveRequest - ) => Promise<DirectiveWithProgress | null>; - archiveExistingDirective: (directiveId: string) => Promise<boolean>; - getDirectiveById: (directiveId: string) => Promise<DirectiveWithProgress | null>; - getGraph: (directiveId: string) => Promise<DirectiveGraphResponse | null>; - start: (directiveId: string) => Promise<StartDirectiveResponse | null>; - pause: (directiveId: string) => Promise<boolean>; - resume: (directiveId: string) => Promise<boolean>; - stop: (directiveId: string) => Promise<boolean>; -} - -export function useDirectives(statusFilter?: DirectiveStatus): UseDirectivesResult { - const [directives, setDirectives] = useState<DirectiveSummary[]>([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState<string | null>(null); - - const fetchDirectives = useCallback(async () => { - setLoading(true); - setError(null); - try { - const response = await listDirectives(statusFilter); - setDirectives(response.directives ?? []); - } catch (err) { - console.error("Failed to fetch directives:", err); - setError(err instanceof Error ? err.message : "Failed to fetch directives"); - } finally { - setLoading(false); - } - }, [statusFilter]); - - useEffect(() => { - fetchDirectives(); - }, [fetchDirectives]); - - const createNewDirective = useCallback( - async (req: CreateDirectiveRequest): Promise<DirectiveWithProgress | null> => { - try { - const directive = await createDirective(req); - // Refresh the list - await fetchDirectives(); - // Return the full directive with progress - return await getDirective(directive.id); - } catch (err) { - console.error("Failed to create directive:", err); - setError(err instanceof Error ? err.message : "Failed to create directive"); - return null; - } - }, - [fetchDirectives] - ); - - const updateExistingDirective = useCallback( - async ( - directiveId: string, - req: UpdateDirectiveRequest - ): Promise<DirectiveWithProgress | null> => { - try { - await updateDirective(directiveId, req); - // Refresh the list - await fetchDirectives(); - // Return the updated directive - return await getDirective(directiveId); - } catch (err) { - console.error("Failed to update directive:", err); - setError(err instanceof Error ? err.message : "Failed to update directive"); - return null; - } - }, - [fetchDirectives] - ); - - const archiveExistingDirective = useCallback( - async (directiveId: string): Promise<boolean> => { - try { - await archiveDirective(directiveId); - // Refresh the list - await fetchDirectives(); - return true; - } catch (err) { - console.error("Failed to archive directive:", err); - setError(err instanceof Error ? err.message : "Failed to archive directive"); - return false; - } - }, - [fetchDirectives] - ); - - const getDirectiveById = useCallback( - async (directiveId: string): Promise<DirectiveWithProgress | null> => { - try { - return await getDirective(directiveId); - } catch (err) { - console.error("Failed to get directive:", err); - setError(err instanceof Error ? err.message : "Failed to get directive"); - return null; - } - }, - [] - ); - - const getGraph = useCallback( - async (directiveId: string): Promise<DirectiveGraphResponse | null> => { - try { - return await getDirectiveGraph(directiveId); - } catch (err) { - console.error("Failed to get directive graph:", err); - setError(err instanceof Error ? err.message : "Failed to get directive graph"); - return null; - } - }, - [] - ); - - const start = useCallback( - async (directiveId: string): Promise<StartDirectiveResponse | null> => { - try { - const response = await startDirective(directiveId); - await fetchDirectives(); - return response; - } catch (err) { - console.error("Failed to start directive:", err); - setError(err instanceof Error ? err.message : "Failed to start directive"); - return null; - } - }, - [fetchDirectives] - ); - - const pause = useCallback( - async (directiveId: string): Promise<boolean> => { - try { - await pauseDirective(directiveId); - await fetchDirectives(); - return true; - } catch (err) { - console.error("Failed to pause directive:", err); - setError(err instanceof Error ? err.message : "Failed to pause directive"); - return false; - } - }, - [fetchDirectives] - ); - - const resume = useCallback( - async (directiveId: string): Promise<boolean> => { - try { - await resumeDirective(directiveId); - await fetchDirectives(); - return true; - } catch (err) { - console.error("Failed to resume directive:", err); - setError(err instanceof Error ? err.message : "Failed to resume directive"); - return false; - } - }, - [fetchDirectives] - ); - - const stop = useCallback( - async (directiveId: string): Promise<boolean> => { - try { - await stopDirective(directiveId); - await fetchDirectives(); - return true; - } catch (err) { - console.error("Failed to stop directive:", err); - setError(err instanceof Error ? err.message : "Failed to stop directive"); - return false; - } - }, - [fetchDirectives] - ); - - return { - directives, - loading, - error, - refresh: fetchDirectives, - createNewDirective, - updateExistingDirective, - archiveExistingDirective, - getDirectiveById, - getGraph, - start, - pause, - resume, - stop, - }; -} - -/** Hook for subscribing to real-time directive events via SSE */ -export function useDirectiveEventSubscription( - directiveId: string | null, - onEvent?: (event: DirectiveEvent) => void -): { - events: DirectiveEvent[]; - isConnected: boolean; - error: string | null; -} { - const [events, setEvents] = useState<DirectiveEvent[]>([]); - const [isConnected, setIsConnected] = useState(false); - const [error, setError] = useState<string | null>(null); - const cleanupRef = useRef<(() => void) | null>(null); - - useEffect(() => { - // Clean up any existing subscription - if (cleanupRef.current) { - cleanupRef.current(); - cleanupRef.current = null; - } - - if (!directiveId) { - setIsConnected(false); - setEvents([]); - return; - } - - // Subscribe to events - let mounted = true; - - const setupSubscription = async () => { - try { - const cleanup = await subscribeToDirectiveEvents( - directiveId, - (event) => { - if (mounted) { - setEvents((prev) => [...prev, event]); - onEvent?.(event); - } - }, - (err) => { - if (mounted) { - setError(err.message); - setIsConnected(false); - } - } - ); - - if (mounted) { - cleanupRef.current = cleanup; - setIsConnected(true); - setError(null); - } else { - // Component unmounted during setup, clean up immediately - cleanup(); - } - } catch (err) { - if (mounted) { - setError(err instanceof Error ? err.message : "Failed to subscribe to events"); - setIsConnected(false); - } - } - }; - - setupSubscription(); - - return () => { - mounted = false; - if (cleanupRef.current) { - cleanupRef.current(); - cleanupRef.current = null; - } - }; - }, [directiveId, onEvent]); - - return { events, isConnected, error }; -} diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts index 466a794..9f5ff88 100644 --- a/makima/frontend/src/lib/api.ts +++ b/makima/frontend/src/lib/api.ts @@ -3003,1286 +3003,3 @@ export async function listTaskPatches(taskId: string, contractId: string): Promi return res.json(); } -// ============================================================================= -// Chain Types and API -// ============================================================================= - -/** Chain status */ -export type ChainStatus = "pending" | "active" | "completed" | "archived"; - -/** Chain summary for list view */ -export interface ChainSummary { - id: string; - name: string; - description: string | null; - status: ChainStatus; - contractCount: number; - completedContractCount: number; - loopEnabled: boolean; - loopCurrentIteration: number | null; - loopMaxIterations: number | null; - createdAt: string; - updatedAt: string; -} - -/** Chain repository */ -export interface ChainRepository { - id: string; - chainId: string; - name: string; - repositoryUrl: string | null; - localPath: string | null; - sourceType: string; - status: string; - isPrimary: boolean; - createdAt: string; - updatedAt: string; -} - -/** Full chain with contracts */ -export interface Chain { - id: string; - ownerId: string; - name: string; - description: string | null; - status: ChainStatus; - loopEnabled: boolean; - loopMaxIterations: number | null; - loopCurrentIteration: number | null; - loopProgressCheck: string | null; - version: number; - createdAt: string; - updatedAt: string; -} - -/** Contract detail within a chain */ -export interface ChainContractDetail { - id: string; - chainId: string; - contractId: string; - contractName: string; - contractStatus: string; - contractPhase: string; - dependsOn: string[]; - orderIndex: number; - editorX: number | null; - editorY: number | null; - createdAt: string; -} - -/** Chain with contracts (chain fields are flattened via serde(flatten)) */ -export interface ChainWithContracts extends Chain { - contracts: ChainContractDetail[]; - repositories: ChainRepository[]; -} - -/** Node in chain graph visualization */ -export interface ChainGraphNode { - id: string; - contractId: string; - name: string; - status: string; - phase: string; - x: number; - y: number; -} - -/** Edge in chain graph */ -export interface ChainGraphEdge { - from: string; - to: string; -} - -/** Chain graph response */ -export interface ChainGraphResponse { - chainId: string; - chainName: string; - chainStatus: string; - nodes: ChainGraphNode[]; - edges: ChainGraphEdge[]; -} - -/** Chain event */ -export interface ChainEvent { - id: string; - chainId: string; - eventType: string; - contractId: string | null; - eventData: Record<string, unknown> | null; - createdAt: string; -} - -/** Chain list response */ -export interface ChainListResponse { - chains: ChainSummary[]; - total: number; -} - -/** Add chain repository request */ -export interface AddChainRepositoryRequest { - name: string; - repositoryUrl?: string; - localPath?: string; - sourceType?: string; - isPrimary?: boolean; -} - -/** Create chain request */ -export interface CreateChainRequest { - name: string; - description?: string; - repositoryUrl?: string; // Legacy field for backwards compatibility - repositories?: AddChainRepositoryRequest[]; - loopEnabled?: boolean; - loopMaxIterations?: number; - loopProgressCheck?: string; - contracts?: CreateChainContractRequest[]; -} - -/** Create chain contract request */ -export interface CreateChainContractRequest { - name: string; - description?: string; - contractType?: string; - initialPhase?: string; - phases?: string[]; - dependsOn?: string[]; - tasks?: { name: string; plan: string }[]; - deliverables?: { id: string; name: string; priority?: string }[]; - editorX?: number; - editorY?: number; -} - -/** Update chain request */ -export interface UpdateChainRequest { - name?: string; - description?: string; - status?: ChainStatus; - loopEnabled?: boolean; - loopMaxIterations?: number; - loopProgressCheck?: string; - version?: number; -} - -/** List chains */ -export async function listChains( - status?: ChainStatus, - limit = 50, - offset = 0 -): Promise<ChainListResponse> { - const params = new URLSearchParams(); - if (status) params.set("status", status); - params.set("limit", String(limit)); - params.set("offset", String(offset)); - - const res = await authFetch(`${API_BASE}/api/v1/chains?${params}`); - if (!res.ok) { - throw new Error(`Failed to list chains: ${res.statusText}`); - } - return res.json(); -} - -/** Get chain by ID */ -export async function getChain(chainId: string): Promise<ChainWithContracts> { - const res = await authFetch(`${API_BASE}/api/v1/chains/${chainId}`); - if (!res.ok) { - throw new Error(`Failed to get chain: ${res.statusText}`); - } - return res.json(); -} - -/** Create a new chain */ -export async function createChain(req: CreateChainRequest): Promise<Chain> { - const res = await authFetch(`${API_BASE}/api/v1/chains`, { - method: "POST", - body: JSON.stringify(req), - }); - if (!res.ok) { - const errorText = await res.text(); - throw new Error(`Failed to create chain: ${errorText || res.statusText}`); - } - return res.json(); -} - -/** Update a chain */ -export async function updateChain( - chainId: string, - req: UpdateChainRequest -): Promise<Chain> { - const res = await authFetch(`${API_BASE}/api/v1/chains/${chainId}`, { - method: "PUT", - body: JSON.stringify(req), - }); - if (!res.ok) { - const errorText = await res.text(); - throw new Error(`Failed to update chain: ${errorText || res.statusText}`); - } - return res.json(); -} - -/** Archive a chain */ -export async function archiveChain(chainId: string): Promise<{ archived: boolean }> { - const res = await authFetch(`${API_BASE}/api/v1/chains/${chainId}`, { - method: "DELETE", - }); - if (!res.ok) { - throw new Error(`Failed to archive chain: ${res.statusText}`); - } - return res.json(); -} - -/** Get chain contracts */ -export async function getChainContracts( - chainId: string -): Promise<ChainContractDetail[]> { - const res = await authFetch(`${API_BASE}/api/v1/chains/${chainId}/contracts`); - if (!res.ok) { - throw new Error(`Failed to get chain contracts: ${res.statusText}`); - } - return res.json(); -} - -/** Get chain graph for visualization */ -export async function getChainGraph(chainId: string): Promise<ChainGraphResponse> { - const res = await authFetch(`${API_BASE}/api/v1/chains/${chainId}/graph`); - if (!res.ok) { - throw new Error(`Failed to get chain graph: ${res.statusText}`); - } - return res.json(); -} - -/** Get chain events */ -export async function getChainEvents(chainId: string): Promise<ChainEvent[]> { - const res = await authFetch(`${API_BASE}/api/v1/chains/${chainId}/events`); - if (!res.ok) { - throw new Error(`Failed to get chain events: ${res.statusText}`); - } - return res.json(); -} - -// ============================================================================= -// Chain Contract Definitions -// ============================================================================= - -/** Task definition for chain contract definitions */ -export interface ChainTaskDefinition { - name: string; - plan: string; -} - -/** Deliverable definition for chain contract definitions (optional priority) */ -export interface ChainDeliverableDefinition { - id: string; - name: string; - priority?: string; -} - -/** Validation configuration for checkpoint contracts */ -export interface CheckpointValidation { - /** Check that all required deliverables from upstream contracts exist */ - checkDeliverables?: boolean; - /** Run tests in the repository */ - runTests?: boolean; - /** Custom validation instructions for Claude */ - checkContent?: string; - /** Action on failure: "block", "retry", "warn" */ - onFailure?: "block" | "retry" | "warn"; - /** Max retry attempts for upstream contracts */ - maxRetries?: number; -} - -/** Contract definition stored in chain (before actual contract is created) */ -export interface ChainContractDefinition { - id: string; - chainId: string; - name: string; - description: string | null; - contractType: string; - initialPhase: string | null; - dependsOnNames: string[]; - tasks: ChainTaskDefinition[] | null; - deliverables: ChainDeliverableDefinition[] | null; - /** Validation config for checkpoint contracts */ - validation: CheckpointValidation | null; - editorX: number | null; - editorY: number | null; - orderIndex: number; - createdAt: string; -} - -/** Request to add a contract definition to a chain */ -export interface AddContractDefinitionRequest { - name: string; - description?: string; - contractType?: string; - initialPhase?: string; - dependsOn?: string[]; - tasks?: ChainTaskDefinition[]; - deliverables?: ChainDeliverableDefinition[]; - /** Validation config (for checkpoint contracts) */ - validation?: CheckpointValidation; - editorX?: number; - editorY?: number; - orderIndex?: number; -} - -/** Request to update a contract definition */ -export interface UpdateContractDefinitionRequest { - name?: string; - description?: string; - contractType?: string; - initialPhase?: string; - dependsOn?: string[]; - tasks?: ChainTaskDefinition[]; - deliverables?: ChainDeliverableDefinition[]; - /** Validation config (for checkpoint contracts) */ - validation?: CheckpointValidation; - editorX?: number; - editorY?: number; - orderIndex?: number; -} - -/** Response when starting a chain */ -export interface StartChainResponse { - chainId: string; - contractsCreated: string[]; - status: string; -} - -/** Node in definition graph (shows definitions + instantiation status) */ -export interface ChainDefinitionGraphNode { - id: string; - name: string; - contractType: string; - x: number; - y: number; - isInstantiated: boolean; - contractId: string | null; - contractStatus: string | null; -} - -/** Definition graph response */ -export interface ChainDefinitionGraphResponse { - chainId: string; - chainName: string; - chainStatus: string; - nodes: ChainDefinitionGraphNode[]; - edges: ChainGraphEdge[]; -} - -/** List contract definitions for a chain */ -export async function listChainDefinitions( - chainId: string -): Promise<ChainContractDefinition[]> { - const res = await authFetch(`${API_BASE}/api/v1/chains/${chainId}/definitions`); - if (!res.ok) { - throw new Error(`Failed to list chain definitions: ${res.statusText}`); - } - return res.json(); -} - -/** Create a contract definition for a chain */ -export async function createChainDefinition( - chainId: string, - req: AddContractDefinitionRequest -): Promise<ChainContractDefinition> { - const res = await authFetch(`${API_BASE}/api/v1/chains/${chainId}/definitions`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(req), - }); - if (!res.ok) { - throw new Error(`Failed to create chain definition: ${res.statusText}`); - } - return res.json(); -} - -/** Update a contract definition */ -export async function updateChainDefinition( - chainId: string, - definitionId: string, - req: UpdateContractDefinitionRequest -): Promise<ChainContractDefinition> { - const res = await authFetch( - `${API_BASE}/api/v1/chains/${chainId}/definitions/${definitionId}`, - { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(req), - } - ); - if (!res.ok) { - throw new Error(`Failed to update chain definition: ${res.statusText}`); - } - return res.json(); -} - -/** Delete a contract definition */ -export async function deleteChainDefinition( - chainId: string, - definitionId: string -): Promise<{ deleted: boolean }> { - const res = await authFetch( - `${API_BASE}/api/v1/chains/${chainId}/definitions/${definitionId}`, - { method: "DELETE" } - ); - if (!res.ok) { - throw new Error(`Failed to delete chain definition: ${res.statusText}`); - } - return res.json(); -} - -/** Get definition graph for a chain */ -export async function getChainDefinitionGraph( - chainId: string -): Promise<ChainDefinitionGraphResponse> { - const res = await authFetch(`${API_BASE}/api/v1/chains/${chainId}/definitions/graph`); - if (!res.ok) { - throw new Error(`Failed to get chain definition graph: ${res.statusText}`); - } - return res.json(); -} - -/** Start a chain (creates root contracts based on DAG) */ -export async function startChain(chainId: string): Promise<StartChainResponse> { - const res = await authFetch(`${API_BASE}/api/v1/chains/${chainId}/start`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({}), - }); - if (!res.ok) { - const error = await res.json().catch(() => ({ message: res.statusText })); - throw new Error(error.message || `Failed to start chain: ${res.statusText}`); - } - return res.json(); -} - -/** Stop a chain (marks as archived) */ -export async function stopChain(chainId: string): Promise<{ stopped: boolean; status: string }> { - const res = await authFetch(`${API_BASE}/api/v1/chains/${chainId}/stop`, { - method: "POST", - }); - if (!res.ok) { - const error = await res.json().catch(() => ({ message: res.statusText })); - throw new Error(error.message || `Failed to stop chain: ${res.statusText}`); - } - return res.json(); -} - -// ============================================================================ -// Chain Repository Operations -// ============================================================================ - -/** List repositories for a chain */ -export async function listChainRepositories(chainId: string): Promise<ChainRepository[]> { - const res = await authFetch(`${API_BASE}/api/v1/chains/${chainId}/repositories`); - if (!res.ok) { - throw new Error(`Failed to list chain repositories: ${res.statusText}`); - } - return res.json(); -} - -/** Add a repository to a chain */ -export async function addChainRepository( - chainId: string, - req: AddChainRepositoryRequest -): Promise<ChainRepository> { - const res = await authFetch(`${API_BASE}/api/v1/chains/${chainId}/repositories`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(req), - }); - if (!res.ok) { - const error = await res.json().catch(() => ({ message: res.statusText })); - throw new Error(error.message || `Failed to add chain repository: ${res.statusText}`); - } - return res.json(); -} - -/** Delete a repository from a chain */ -export async function deleteChainRepository( - chainId: string, - repositoryId: string -): Promise<{ deleted: boolean }> { - const res = await authFetch( - `${API_BASE}/api/v1/chains/${chainId}/repositories/${repositoryId}`, - { method: "DELETE" } - ); - if (!res.ok) { - throw new Error(`Failed to delete chain repository: ${res.statusText}`); - } - return res.json(); -} - -/** Set a repository as primary for a chain */ -export async function setChainRepositoryPrimary( - chainId: string, - repositoryId: string -): Promise<ChainRepository> { - const res = await authFetch( - `${API_BASE}/api/v1/chains/${chainId}/repositories/${repositoryId}/primary`, - { method: "PUT" } - ); - if (!res.ok) { - throw new Error(`Failed to set chain repository as primary: ${res.statusText}`); - } - return res.json(); -} - -// ============================================================================= -// Directive Types and API -// ============================================================================= - -/** Directive status */ -export type DirectiveStatus = - | "draft" - | "planning" - | "active" - | "paused" - | "completed" - | "archived" - | "failed"; - -/** Autonomy level */ -export type AutonomyLevel = "full_auto" | "guardrails" | "manual"; - -/** Confidence level (traffic light) */ -export type ConfidenceLevel = "green" | "yellow" | "red"; - -/** Step status */ -export type StepStatus = - | "pending" - | "ready" - | "running" - | "evaluating" - | "passed" - | "failed" - | "rework" - | "skipped" - | "blocked"; - -/** Evaluation type */ -export type EvaluationType = "programmatic" | "llm" | "composite" | "manual"; - -/** Directive summary for list view */ -export interface DirectiveSummary { - id: string; - title: string; - goal: string; - status: DirectiveStatus; - autonomyLevel: AutonomyLevel; - repositoryUrl: string | null; - currentChainId: string | null; - currentChainGeneration: number | null; - totalSteps: number; - completedSteps: number; - failedSteps: number; - currentConfidence: number | null; - totalCostUsd: number; - createdAt: string; - updatedAt: string; -} - -/** Full directive */ -export interface Directive { - id: string; - ownerId: string; - title: string; - goal: string; - status: DirectiveStatus; - autonomyLevel: AutonomyLevel; - repositoryUrl: string | null; - localPath: string | null; - requirements: unknown | null; - acceptanceCriteria: unknown | null; - constraints: unknown | null; - confidenceThresholdGreen: number; - confidenceThresholdYellow: number; - maxReworkCycles: number; - maxTotalCostUsd: number | null; - maxWallTimeMinutes: number | null; - maxChainRegenerations: number; - totalCostUsd: number; - currentChainId: string | null; - version: number; - createdAt: string; - updatedAt: string; - startedAt: string | null; - completedAt: string | null; -} - -/** Directive chain */ -export interface DirectiveChain { - id: string; - directiveId: string; - generation: number; - name: string; - description: string | null; - rationale: string | null; - planningModel: string | null; - status: string; - totalSteps: number; - completedSteps: number; - failedSteps: number; - currentConfidence: number | null; - startedAt: string | null; - completedAt: string | null; - version: number; - createdAt: string; - updatedAt: string; -} - -/** Chain step */ -export interface ChainStep { - id: string; - chainId: string; - name: string; - description: string | null; - stepType: string; - contractType: string; - initialPhase: string | null; - taskPlan: string | null; - phases: string[]; - dependsOn: string[]; - parallelGroup: string | null; - requirementIds: string[]; - acceptanceCriteriaIds: string[]; - verifierConfig: unknown; - status: StepStatus; - contractId: string | null; - supervisorTaskId: string | null; - confidenceScore: number | null; - confidenceLevel: ConfidenceLevel | null; - evaluationCount: number; - reworkCount: number; - lastEvaluationId: string | null; - editorX: number | null; - editorY: number | null; - startedAt: string | null; - completedAt: string | null; - createdAt: string; - updatedAt: string; -} - -/** Directive with progress info */ -export interface DirectiveWithProgress extends Directive { - chain: DirectiveChain | null; - steps: ChainStep[]; - recentEvents: DirectiveEvent[]; - pendingApprovals: DirectiveApproval[]; -} - -/** Directive evaluation */ -export interface DirectiveEvaluation { - id: string; - directiveId: string; - chainId: string | null; - stepId: string | null; - evaluationType: EvaluationType; - passed: boolean; - overallScore: number; - confidenceLevel: ConfidenceLevel; - programmaticResults: unknown | null; - llmResults: unknown | null; - compositeBreakdown: unknown | null; - feedback: string | null; - reworkInstructions: string | null; - verifierIds: string[]; - evaluatedBy: string | null; - createdAt: string; -} - -/** Directive event */ -export interface DirectiveEvent { - id: string; - directiveId: string; - chainId: string | null; - stepId: string | null; - eventType: string; - severity: string; - eventData: unknown | null; - actorType: string; - actorId: string | null; - createdAt: string; -} - -/** Directive approval */ -export interface DirectiveApproval { - id: string; - directiveId: string; - chainId: string | null; - stepId: string | null; - approvalType: string; - description: string; - context: unknown | null; - urgency: string; - status: string; - requestedAt: string; - resolvedAt: string | null; - resolvedBy: string | null; - response: string | null; -} - -/** Directive verifier */ -export interface DirectiveVerifier { - id: string; - directiveId: string; - name: string; - verifierType: string; - command: string | null; - workingDirectory: string | null; - timeoutSeconds: number; - environment: unknown; - autoDetect: boolean; - detectFiles: string[]; - weight: number; - required: boolean; - enabled: boolean; - lastRunAt: string | null; - lastResult: unknown | null; - createdAt: string; - updatedAt: string; -} - -/** Directive graph node */ -export interface DirectiveGraphNode { - id: string; - name: string; - stepType: string; - status: StepStatus; - confidenceScore: number | null; - confidenceLevel: ConfidenceLevel | null; - contractId: string | null; - editorX: number | null; - editorY: number | null; -} - -/** Directive graph edge */ -export interface DirectiveGraphEdge { - source: string; - target: string; -} - -/** Directive graph response */ -export interface DirectiveGraphResponse { - chainId: string; - directiveId: string; - nodes: DirectiveGraphNode[]; - edges: DirectiveGraphEdge[]; -} - -/** Create directive request */ -export interface CreateDirectiveRequest { - goal: string; - repositoryUrl?: string; - localPath?: string; - autonomyLevel?: AutonomyLevel; - confidenceThresholdGreen?: number; - confidenceThresholdYellow?: number; - maxReworkCycles?: number; - maxTotalCostUsd?: number; - maxWallTimeMinutes?: number; -} - -/** Update directive request */ -export interface UpdateDirectiveRequest { - title?: string; - goal?: string; - requirements?: unknown; - acceptanceCriteria?: unknown; - constraints?: unknown; - autonomyLevel?: AutonomyLevel; - confidenceThresholdGreen?: number; - confidenceThresholdYellow?: number; - maxReworkCycles?: number; - maxTotalCostUsd?: number; - maxWallTimeMinutes?: number; - version?: number; -} - -/** Add step request */ -export interface AddStepRequest { - name: string; - description?: string; - stepType?: string; - contractType?: string; - initialPhase?: string; - taskPlan?: string; - phases?: string[]; - dependsOn?: string[]; - parallelGroup?: string; - requirementIds?: string[]; - acceptanceCriteriaIds?: string[]; - verifierConfig?: unknown; - editorX?: number; - editorY?: number; -} - -/** Update step request */ -export interface UpdateStepRequest { - name?: string; - description?: string; - initialPhase?: string; - taskPlan?: string; - phases?: string[]; - dependsOn?: string[]; - parallelGroup?: string; - requirementIds?: string[]; - acceptanceCriteriaIds?: string[]; - verifierConfig?: unknown; - editorX?: number; - editorY?: number; -} - -/** Create verifier request */ -export interface CreateVerifierRequest { - name: string; - verifierType: string; - command?: string; - workingDirectory?: string; - timeoutSeconds?: number; - weight?: number; - required?: boolean; - enabled?: boolean; -} - -/** Update verifier request */ -export interface UpdateVerifierRequest { - command?: string; - weight?: number; - required?: boolean; - enabled?: boolean; -} - -/** Approval action request */ -export interface ApprovalActionRequest { - response?: string; -} - -/** Start directive response */ -export interface StartDirectiveResponse { - directiveId: string; - chainId: string; - chainGeneration: number; - steps: ChainStep[]; - status: string; -} - -/** Directive list response */ -export interface DirectiveListResponse { - directives: DirectiveSummary[]; - total: number; -} - -// ============================================================================= -// Directive API Functions -// ============================================================================= - -/** List directives */ -export async function listDirectives( - status?: DirectiveStatus, - limit = 50, - offset = 0 -): Promise<DirectiveListResponse> { - const params = new URLSearchParams(); - if (status) params.set("status", status); - params.set("limit", String(limit)); - params.set("offset", String(offset)); - - const res = await authFetch(`${API_BASE}/api/v1/directives?${params}`); - if (!res.ok) { - throw new Error(`Failed to list directives: ${res.statusText}`); - } - return res.json(); -} - -/** Get directive by ID */ -export async function getDirective(directiveId: string): Promise<DirectiveWithProgress> { - const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}`); - if (!res.ok) { - throw new Error(`Failed to get directive: ${res.statusText}`); - } - return res.json(); -} - -/** Create a new directive */ -export async function createDirective(req: CreateDirectiveRequest): Promise<Directive> { - const res = await authFetch(`${API_BASE}/api/v1/directives`, { - method: "POST", - body: JSON.stringify(req), - }); - if (!res.ok) { - const errorText = await res.text(); - throw new Error(`Failed to create directive: ${errorText || res.statusText}`); - } - return res.json(); -} - -/** Update directive */ -export async function updateDirective( - directiveId: string, - req: UpdateDirectiveRequest -): Promise<Directive> { - const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}`, { - method: "PUT", - body: JSON.stringify(req), - }); - if (!res.ok) { - throw new Error(`Failed to update directive: ${res.statusText}`); - } - return res.json(); -} - -/** Archive directive */ -export async function archiveDirective(directiveId: string): Promise<{ archived: boolean }> { - const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}`, { - method: "DELETE", - }); - if (!res.ok) { - throw new Error(`Failed to archive directive: ${res.statusText}`); - } - return res.json(); -} - -/** Start directive */ -export async function startDirective(directiveId: string): Promise<StartDirectiveResponse> { - const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/start`, { - method: "POST", - }); - if (!res.ok) { - throw new Error(`Failed to start directive: ${res.statusText}`); - } - return res.json(); -} - -/** Pause directive */ -export async function pauseDirective(directiveId: string): Promise<Directive> { - const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/pause`, { - method: "POST", - }); - if (!res.ok) { - throw new Error(`Failed to pause directive: ${res.statusText}`); - } - return res.json(); -} - -/** Resume directive */ -export async function resumeDirective(directiveId: string): Promise<Directive> { - const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/resume`, { - method: "POST", - }); - if (!res.ok) { - throw new Error(`Failed to resume directive: ${res.statusText}`); - } - return res.json(); -} - -/** Stop directive */ -export async function stopDirective(directiveId: string): Promise<Directive> { - const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/stop`, { - method: "POST", - }); - if (!res.ok) { - throw new Error(`Failed to stop directive: ${res.statusText}`); - } - return res.json(); -} - -/** Get directive chain */ -export async function getDirectiveChain( - directiveId: string -): Promise<{ chain: DirectiveChain | null; steps: ChainStep[] }> { - const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/chain`); - if (!res.ok) { - throw new Error(`Failed to get directive chain: ${res.statusText}`); - } - return res.json(); -} - -/** Get directive chain graph */ -export async function getDirectiveGraph(directiveId: string): Promise<DirectiveGraphResponse> { - const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/chain/graph`); - if (!res.ok) { - throw new Error(`Failed to get directive graph: ${res.statusText}`); - } - return res.json(); -} - -/** Replan directive chain */ -export async function replanDirectiveChain(directiveId: string): Promise<DirectiveChain> { - const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/chain/replan`, { - method: "POST", - }); - if (!res.ok) { - throw new Error(`Failed to replan directive chain: ${res.statusText}`); - } - return res.json(); -} - -/** Add step to directive chain */ -export async function addDirectiveStep( - directiveId: string, - req: AddStepRequest -): Promise<ChainStep> { - const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/chain/steps`, { - method: "POST", - body: JSON.stringify(req), - }); - if (!res.ok) { - throw new Error(`Failed to add step: ${res.statusText}`); - } - return res.json(); -} - -/** Get step details */ -export async function getDirectiveStep( - directiveId: string, - stepId: string -): Promise<ChainStep> { - const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/steps/${stepId}`); - if (!res.ok) { - throw new Error(`Failed to get step: ${res.statusText}`); - } - return res.json(); -} - -/** Update step */ -export async function updateDirectiveStep( - directiveId: string, - stepId: string, - req: UpdateStepRequest -): Promise<ChainStep> { - const res = await authFetch( - `${API_BASE}/api/v1/directives/${directiveId}/chain/steps/${stepId}`, - { - method: "PUT", - body: JSON.stringify(req), - } - ); - if (!res.ok) { - throw new Error(`Failed to update step: ${res.statusText}`); - } - return res.json(); -} - -/** Delete step */ -export async function deleteDirectiveStep( - directiveId: string, - stepId: string -): Promise<{ deleted: boolean }> { - const res = await authFetch( - `${API_BASE}/api/v1/directives/${directiveId}/chain/steps/${stepId}`, - { - method: "DELETE", - } - ); - if (!res.ok) { - throw new Error(`Failed to delete step: ${res.statusText}`); - } - return res.json(); -} - -/** Skip step */ -export async function skipDirectiveStep( - directiveId: string, - stepId: string -): Promise<ChainStep> { - const res = await authFetch( - `${API_BASE}/api/v1/directives/${directiveId}/steps/${stepId}/skip`, - { - method: "POST", - } - ); - if (!res.ok) { - throw new Error(`Failed to skip step: ${res.statusText}`); - } - return res.json(); -} - -/** List directive evaluations */ -export async function listDirectiveEvaluations( - directiveId: string, - limit = 50 -): Promise<DirectiveEvaluation[]> { - const res = await authFetch( - `${API_BASE}/api/v1/directives/${directiveId}/evaluations?limit=${limit}` - ); - if (!res.ok) { - throw new Error(`Failed to list evaluations: ${res.statusText}`); - } - return res.json(); -} - -/** List directive events */ -export async function listDirectiveEvents( - directiveId: string, - limit = 50 -): Promise<DirectiveEvent[]> { - const res = await authFetch( - `${API_BASE}/api/v1/directives/${directiveId}/events?limit=${limit}` - ); - if (!res.ok) { - throw new Error(`Failed to list events: ${res.statusText}`); - } - return res.json(); -} - -/** List directive verifiers */ -export async function listDirectiveVerifiers( - directiveId: string -): Promise<DirectiveVerifier[]> { - const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/verifiers`); - if (!res.ok) { - throw new Error(`Failed to list verifiers: ${res.statusText}`); - } - return res.json(); -} - -/** Add verifier */ -export async function addDirectiveVerifier( - directiveId: string, - req: CreateVerifierRequest -): Promise<DirectiveVerifier> { - const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/verifiers`, { - method: "POST", - body: JSON.stringify(req), - }); - if (!res.ok) { - throw new Error(`Failed to add verifier: ${res.statusText}`); - } - return res.json(); -} - -/** Update verifier */ -export async function updateDirectiveVerifier( - directiveId: string, - verifierId: string, - req: UpdateVerifierRequest -): Promise<DirectiveVerifier> { - const res = await authFetch( - `${API_BASE}/api/v1/directives/${directiveId}/verifiers/${verifierId}`, - { - method: "PUT", - body: JSON.stringify(req), - } - ); - if (!res.ok) { - throw new Error(`Failed to update verifier: ${res.statusText}`); - } - return res.json(); -} - -/** Auto-detect verifiers */ -export async function autoDetectVerifiers( - directiveId: string -): Promise<DirectiveVerifier[]> { - const res = await authFetch( - `${API_BASE}/api/v1/directives/${directiveId}/verifiers/auto-detect`, - { - method: "POST", - } - ); - if (!res.ok) { - throw new Error(`Failed to auto-detect verifiers: ${res.statusText}`); - } - return res.json(); -} - -/** List pending approvals */ -export async function listDirectiveApprovals( - directiveId: string -): Promise<DirectiveApproval[]> { - const res = await authFetch(`${API_BASE}/api/v1/directives/${directiveId}/approvals`); - if (!res.ok) { - throw new Error(`Failed to list approvals: ${res.statusText}`); - } - return res.json(); -} - -/** Approve request */ -export async function approveDirectiveRequest( - directiveId: string, - approvalId: string, - req?: ApprovalActionRequest -): Promise<DirectiveApproval> { - const res = await authFetch( - `${API_BASE}/api/v1/directives/${directiveId}/approvals/${approvalId}/approve`, - { - method: "POST", - body: JSON.stringify(req || {}), - } - ); - if (!res.ok) { - throw new Error(`Failed to approve request: ${res.statusText}`); - } - return res.json(); -} - -/** Deny request */ -export async function denyDirectiveRequest( - directiveId: string, - approvalId: string, - req?: ApprovalActionRequest -): Promise<DirectiveApproval> { - const res = await authFetch( - `${API_BASE}/api/v1/directives/${directiveId}/approvals/${approvalId}/deny`, - { - method: "POST", - body: JSON.stringify(req || {}), - } - ); - if (!res.ok) { - throw new Error(`Failed to deny request: ${res.statusText}`); - } - return res.json(); -} - -/** Subscribe to directive events via SSE */ -export async function subscribeToDirectiveEvents( - directiveId: string, - onEvent: (event: DirectiveEvent) => void, - onError?: (error: Error) => void -): Promise<() => void> { - // Get auth token for the request - let authToken: string | null = null; - if (supabase) { - const { data: { session } } = await supabase.auth.getSession(); - if (session?.access_token) { - authToken = session.access_token; - } - } - - // Build URL with auth token as query param (since EventSource doesn't support headers) - const url = new URL(`${API_BASE}/api/v1/directives/${directiveId}/events/stream`); - if (authToken) { - url.searchParams.set("token", authToken); - } else { - const apiKey = getStoredApiKey(); - if (apiKey) { - url.searchParams.set("api_key", apiKey); - } - } - - // Create EventSource connection - const eventSource = new EventSource(url.toString()); - - eventSource.onmessage = (e) => { - try { - const event = JSON.parse(e.data) as DirectiveEvent; - onEvent(event); - } catch (err) { - console.error("Failed to parse SSE event:", err); - } - }; - - eventSource.onerror = (_e) => { - if (onError) { - onError(new Error("SSE connection error")); - } - }; - - // Return cleanup function - return () => { - eventSource.close(); - }; -} diff --git a/makima/frontend/src/main.tsx b/makima/frontend/src/main.tsx index c90d292..50fffe4 100644 --- a/makima/frontend/src/main.tsx +++ b/makima/frontend/src/main.tsx @@ -12,7 +12,6 @@ import HomePage from "./routes/_index"; import ListenPage from "./routes/listen"; import FilesPage from "./routes/files"; import ContractsPage from "./routes/contracts"; -import DirectivesPage from "./routes/directives"; import WorkflowPage from "./routes/workflow"; import MeshPage from "./routes/mesh"; import HistoryPage from "./routes/history"; @@ -73,22 +72,6 @@ createRoot(document.getElementById("root")!).render( } /> <Route - path="/directives" - element={ - <ProtectedRoute> - <DirectivesPage /> - </ProtectedRoute> - } - /> - <Route - path="/directives/:id" - element={ - <ProtectedRoute> - <DirectivesPage /> - </ProtectedRoute> - } - /> - <Route path="/contracts/:id/files/:fileId" element={ <ProtectedRoute> diff --git a/makima/frontend/src/routes/directives.tsx b/makima/frontend/src/routes/directives.tsx deleted file mode 100644 index 90f0854..0000000 --- a/makima/frontend/src/routes/directives.tsx +++ /dev/null @@ -1,184 +0,0 @@ -import { useState, useCallback, useEffect } from "react"; -import { useParams, useNavigate } from "react-router"; -import { Masthead } from "../components/Masthead"; -import { useDirectives } from "../hooks/useDirectives"; -import { useDirectiveDetail } from "../hooks/useDirectiveDetail"; -import { useAuth } from "../contexts/AuthContext"; -import type { - DirectiveSummary, - CreateDirectiveRequest, - AutonomyLevel, -} from "../lib/api"; -import { DirectiveList } from "../components/directives/DirectiveList"; -import { DirectiveDetail } from "../components/directives/DirectiveDetail"; -import { CreateDirectiveModal } from "../components/directives/CreateDirectiveModal"; - -export default function DirectivesPage() { - const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth(); - const navigate = useNavigate(); - - // Redirect to login if not authenticated (when auth is configured) - useEffect(() => { - if (!authLoading && isAuthConfigured && !isAuthenticated) { - navigate("/login"); - } - }, [authLoading, isAuthConfigured, isAuthenticated, navigate]); - - // Show loading while checking auth - if (authLoading) { - return ( - <div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]"> - <Masthead showNav /> - <main className="flex-1 flex items-center justify-center"> - <p className="text-[#7788aa] font-mono text-sm">Loading...</p> - </main> - </div> - ); - } - - // Don't render if not authenticated (will redirect) - if (isAuthConfigured && !isAuthenticated) { - return null; - } - - return <DirectivesPageContent />; -} - -function DirectivesPageContent() { - const { id } = useParams<{ id: string }>(); - const navigate = useNavigate(); - const { - directives, - loading, - error, - createNewDirective, - archiveExistingDirective, - } = useDirectives(); - - const { - directive: directiveDetail, - graph: directiveGraph, - loading: detailLoading, - refresh: refreshDetail, - start: handleStart, - pause: handlePause, - resume: handleResume, - stop: handleStop, - } = useDirectiveDetail(id); - - const [isCreating, setIsCreating] = useState(false); - - const handleSelect = useCallback( - (directiveId: string) => { - navigate(`/directives/${directiveId}`); - }, - [navigate] - ); - - const handleBack = useCallback(() => { - navigate("/directives"); - }, [navigate]); - - const handleCreate = useCallback(() => { - setIsCreating(true); - }, []); - - const handleCreateSubmit = useCallback( - async (goal: string, repositoryUrl: string | undefined, autonomyLevel: AutonomyLevel) => { - const data: CreateDirectiveRequest = { - goal: goal.trim(), - repositoryUrl: repositoryUrl?.trim() || undefined, - autonomyLevel, - }; - - try { - const result = await createNewDirective(data); - if (result) { - setIsCreating(false); - navigate(`/directives/${result.id}`); - } - } catch (err) { - console.error("Failed to create directive:", err); - } - }, - [createNewDirective, navigate] - ); - - const handleCreateCancel = useCallback(() => { - setIsCreating(false); - }, []); - - const handleArchive = useCallback( - async (directive: DirectiveSummary) => { - if (confirm(`Are you sure you want to archive this directive?`)) { - const success = await archiveExistingDirective(directive.id); - if (success && directive.id === id) { - navigate("/directives"); - } - } - }, - [archiveExistingDirective, id, navigate] - ); - - return ( - <div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]"> - <Masthead showNav /> - <main className="flex-1 flex flex-col p-4 pt-2 gap-4 overflow-hidden"> - {error && ( - <div className="p-3 bg-red-400/10 border border-red-400/30 text-red-400 font-mono text-sm"> - {error} - </div> - )} - - {/* Create directive modal */} - {isCreating && ( - <CreateDirectiveModal - onSubmit={handleCreateSubmit} - onCancel={handleCreateCancel} - /> - )} - - <div className="flex-1 grid grid-cols-[350px_1fr] gap-4 min-h-0"> - {/* Directive list */} - <DirectiveList - directives={directives} - loading={loading} - onSelect={handleSelect} - onCreate={handleCreate} - selectedId={id} - onArchive={handleArchive} - /> - - {/* Directive detail or empty state */} - {directiveDetail ? ( - <DirectiveDetail - directive={directiveDetail} - graph={directiveGraph} - loading={detailLoading} - onBack={handleBack} - onRefresh={refreshDetail} - onStart={handleStart} - onPause={handlePause} - onResume={handleResume} - onStop={handleStop} - /> - ) : ( - <div className="panel h-full flex items-center justify-center"> - <div className="text-center"> - <p className="font-mono text-sm text-[#555] mb-4"> - Select a directive or create a new one - </p> - <button - onClick={handleCreate} - className="px-4 py-2 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase" - > - + New Directive - </button> - </div> - </div> - )} - </div> - </main> - </div> - ); -} diff --git a/makima/frontend/tsconfig.tsbuildinfo b/makima/frontend/tsconfig.tsbuildinfo index cbaa81f..0fbecaa 100644 --- a/makima/frontend/tsconfig.tsbuildinfo +++ b/makima/frontend/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/gridoverlay.tsx","./src/components/japanesehovertext.tsx","./src/components/logo.tsx","./src/components/masthead.tsx","./src/components/navstrip.tsx","./src/components/phaseconfirmationnotification.tsx","./src/components/protectedroute.tsx","./src/components/rewritelink.tsx","./src/components/simplemarkdown.tsx","./src/components/supervisorquestionnotification.tsx","./src/components/charts/chartrenderer.tsx","./src/components/contracts/commandmodepanel.tsx","./src/components/contracts/contractcliinput.tsx","./src/components/contracts/contractcontextmenu.tsx","./src/components/contracts/contractdetail.tsx","./src/components/contracts/contractlist.tsx","./src/components/contracts/phasebadge.tsx","./src/components/contracts/phaseconfirmationmodal.tsx","./src/components/contracts/phasedeliverablespanel.tsx","./src/components/contracts/phasehint.tsx","./src/components/contracts/phaseprogressbar.tsx","./src/components/contracts/quickactionbuttons.tsx","./src/components/contracts/repositorypanel.tsx","./src/components/contracts/taskderivationpreview.tsx","./src/components/directives/approvalstab.tsx","./src/components/directives/chaintab.tsx","./src/components/directives/createdirectivemodal.tsx","./src/components/directives/directivedetail.tsx","./src/components/directives/directivelist.tsx","./src/components/directives/directivelistitem.tsx","./src/components/directives/evaluationstab.tsx","./src/components/directives/eventstab.tsx","./src/components/directives/overviewtab.tsx","./src/components/directives/stepnode.tsx","./src/components/directives/verifierstab.tsx","./src/components/directives/index.ts","./src/components/files/bodyrenderer.tsx","./src/components/files/cliinput.tsx","./src/components/files/conflictnotification.tsx","./src/components/files/elementcontextmenu.tsx","./src/components/files/filedetail.tsx","./src/components/files/filelist.tsx","./src/components/files/reposyncindicator.tsx","./src/components/files/updatenotification.tsx","./src/components/files/versionhistorydropdown.tsx","./src/components/history/checkpointcard.tsx","./src/components/history/checkpointlist.tsx","./src/components/history/conversationmessage.tsx","./src/components/history/conversationview.tsx","./src/components/history/historyfilters.tsx","./src/components/history/resumecontrols.tsx","./src/components/history/timelineeventcard.tsx","./src/components/history/timelinelist.tsx","./src/components/history/index.ts","./src/components/listen/contractpickermodal.tsx","./src/components/listen/controlpanel.tsx","./src/components/listen/discusscontractmodal.tsx","./src/components/listen/speakerpanel.tsx","./src/components/listen/transcriptanalysispanel.tsx","./src/components/listen/transcriptpanel.tsx","./src/components/mesh/branchtaskmodal.tsx","./src/components/mesh/contractcompletequestion.tsx","./src/components/mesh/directoryinput.tsx","./src/components/mesh/gitactionspanel.tsx","./src/components/mesh/inlinesubtaskeditor.tsx","./src/components/mesh/mergeconflictresolver.tsx","./src/components/mesh/overlaydiffviewer.tsx","./src/components/mesh/prpreview.tsx","./src/components/mesh/patcheslistpanel.tsx","./src/components/mesh/subtasktree.tsx","./src/components/mesh/taskdetail.tsx","./src/components/mesh/tasklist.tsx","./src/components/mesh/taskoutput.tsx","./src/components/mesh/tasktree.tsx","./src/components/mesh/unifiedmeshchatinput.tsx","./src/components/mesh/worktreefilespanel.tsx","./src/components/workflow/phasecolumn.tsx","./src/components/workflow/workflowboard.tsx","./src/components/workflow/workflowcontractcard.tsx","./src/contexts/authcontext.tsx","./src/contexts/supervisorquestionscontext.tsx","./src/hooks/usecontracts.ts","./src/hooks/usedirectivedetail.ts","./src/hooks/usedirectives.ts","./src/hooks/usefilesubscription.ts","./src/hooks/usefiles.ts","./src/hooks/usemeshchathistory.ts","./src/hooks/usemicrophone.ts","./src/hooks/usespeakwebsocket.ts","./src/hooks/usetasksubscription.ts","./src/hooks/usetasks.ts","./src/hooks/usetextscramble.ts","./src/hooks/useversionhistory.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/listenapi.ts","./src/lib/markdown.ts","./src/lib/supabase.ts","./src/routes/_index.tsx","./src/routes/contract-file.tsx","./src/routes/contracts.tsx","./src/routes/directives.tsx","./src/routes/files.tsx","./src/routes/history.tsx","./src/routes/listen.tsx","./src/routes/login.tsx","./src/routes/mesh.tsx","./src/routes/settings.tsx","./src/routes/speak.tsx","./src/routes/workflow.tsx","./src/types/messages.ts"],"version":"5.9.3"}
\ No newline at end of file +{"root":["./src/main.tsx","./src/vite-env.d.ts","./src/components/gridoverlay.tsx","./src/components/japanesehovertext.tsx","./src/components/logo.tsx","./src/components/masthead.tsx","./src/components/navstrip.tsx","./src/components/phaseconfirmationnotification.tsx","./src/components/protectedroute.tsx","./src/components/rewritelink.tsx","./src/components/simplemarkdown.tsx","./src/components/supervisorquestionnotification.tsx","./src/components/charts/chartrenderer.tsx","./src/components/contracts/commandmodepanel.tsx","./src/components/contracts/contractcliinput.tsx","./src/components/contracts/contractcontextmenu.tsx","./src/components/contracts/contractdetail.tsx","./src/components/contracts/contractlist.tsx","./src/components/contracts/phasebadge.tsx","./src/components/contracts/phaseconfirmationmodal.tsx","./src/components/contracts/phasedeliverablespanel.tsx","./src/components/contracts/phasehint.tsx","./src/components/contracts/phaseprogressbar.tsx","./src/components/contracts/quickactionbuttons.tsx","./src/components/contracts/repositorypanel.tsx","./src/components/contracts/taskderivationpreview.tsx","./src/components/files/bodyrenderer.tsx","./src/components/files/cliinput.tsx","./src/components/files/conflictnotification.tsx","./src/components/files/elementcontextmenu.tsx","./src/components/files/filedetail.tsx","./src/components/files/filelist.tsx","./src/components/files/reposyncindicator.tsx","./src/components/files/updatenotification.tsx","./src/components/files/versionhistorydropdown.tsx","./src/components/history/checkpointcard.tsx","./src/components/history/checkpointlist.tsx","./src/components/history/conversationmessage.tsx","./src/components/history/conversationview.tsx","./src/components/history/historyfilters.tsx","./src/components/history/resumecontrols.tsx","./src/components/history/timelineeventcard.tsx","./src/components/history/timelinelist.tsx","./src/components/history/index.ts","./src/components/listen/contractpickermodal.tsx","./src/components/listen/controlpanel.tsx","./src/components/listen/discusscontractmodal.tsx","./src/components/listen/speakerpanel.tsx","./src/components/listen/transcriptanalysispanel.tsx","./src/components/listen/transcriptpanel.tsx","./src/components/mesh/branchtaskmodal.tsx","./src/components/mesh/contractcompletequestion.tsx","./src/components/mesh/directoryinput.tsx","./src/components/mesh/gitactionspanel.tsx","./src/components/mesh/inlinesubtaskeditor.tsx","./src/components/mesh/mergeconflictresolver.tsx","./src/components/mesh/overlaydiffviewer.tsx","./src/components/mesh/prpreview.tsx","./src/components/mesh/patcheslistpanel.tsx","./src/components/mesh/subtasktree.tsx","./src/components/mesh/taskdetail.tsx","./src/components/mesh/tasklist.tsx","./src/components/mesh/taskoutput.tsx","./src/components/mesh/tasktree.tsx","./src/components/mesh/unifiedmeshchatinput.tsx","./src/components/mesh/worktreefilespanel.tsx","./src/components/workflow/phasecolumn.tsx","./src/components/workflow/workflowboard.tsx","./src/components/workflow/workflowcontractcard.tsx","./src/contexts/authcontext.tsx","./src/contexts/supervisorquestionscontext.tsx","./src/hooks/usecontracts.ts","./src/hooks/usefilesubscription.ts","./src/hooks/usefiles.ts","./src/hooks/usemeshchathistory.ts","./src/hooks/usemicrophone.ts","./src/hooks/usespeakwebsocket.ts","./src/hooks/usetasksubscription.ts","./src/hooks/usetasks.ts","./src/hooks/usetextscramble.ts","./src/hooks/useversionhistory.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/listenapi.ts","./src/lib/markdown.ts","./src/lib/supabase.ts","./src/routes/_index.tsx","./src/routes/contract-file.tsx","./src/routes/contracts.tsx","./src/routes/files.tsx","./src/routes/history.tsx","./src/routes/listen.tsx","./src/routes/login.tsx","./src/routes/mesh.tsx","./src/routes/settings.tsx","./src/routes/speak.tsx","./src/routes/workflow.tsx","./src/types/messages.ts"],"version":"5.9.3"}
\ No newline at end of file diff --git a/makima/migrations/20260207000000_directive_v2_wipe.sql b/makima/migrations/20260207000000_directive_v2_wipe.sql new file mode 100644 index 0000000..605b4b4 --- /dev/null +++ b/makima/migrations/20260207000000_directive_v2_wipe.sql @@ -0,0 +1,331 @@ +-- ============================================================================ +-- Migration: directive_v2_wipe.sql +-- Wipes all directive system tables and recreates them clean. +-- This is a destructive migration — all directive data will be lost. +-- ============================================================================ + +-- Drop in reverse dependency order +DROP TABLE IF EXISTS directive_approvals CASCADE; +DROP TABLE IF EXISTS directive_verifiers CASCADE; +DROP TABLE IF EXISTS directive_events CASCADE; +DROP TABLE IF EXISTS directive_evaluations CASCADE; +DROP TABLE IF EXISTS chain_steps CASCADE; +DROP TABLE IF EXISTS directive_chains CASCADE; + +-- Drop directive columns from contracts BEFORE dropping directives table +ALTER TABLE contracts DROP COLUMN IF EXISTS directive_id; +ALTER TABLE contracts DROP COLUMN IF EXISTS is_directive_orchestrator; +ALTER TABLE contracts DROP COLUMN IF EXISTS spawned_directive_id; + +DROP TABLE IF EXISTS directives CASCADE; + +-- ============================================================================ +-- 1. DIRECTIVES +-- ============================================================================ +CREATE TABLE directives ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + owner_id UUID NOT NULL REFERENCES owners(id) ON DELETE CASCADE, + + -- Goal specification + title VARCHAR(500) NOT NULL, + goal TEXT NOT NULL, + + -- Structured specification (JSONB arrays) + requirements JSONB NOT NULL DEFAULT '[]', + acceptance_criteria JSONB NOT NULL DEFAULT '[]', + constraints JSONB NOT NULL DEFAULT '[]', + external_dependencies JSONB NOT NULL DEFAULT '[]', + + -- State + status VARCHAR(32) NOT NULL DEFAULT 'draft' + CHECK (status IN ('draft', 'planning', 'active', 'paused', 'completed', 'archived', 'failed')), + + -- Autonomy configuration + autonomy_level VARCHAR(32) NOT NULL DEFAULT 'guardrails' + CHECK (autonomy_level IN ('full_auto', 'guardrails', 'manual')), + confidence_threshold_green FLOAT NOT NULL DEFAULT 0.85, + confidence_threshold_yellow FLOAT NOT NULL DEFAULT 0.60, + + -- Circuit breaker limits + max_total_cost_usd FLOAT, + max_wall_time_minutes INTEGER, + max_rework_cycles INTEGER DEFAULT 3, + max_chain_regenerations INTEGER DEFAULT 2, + + -- Repository configuration (inherited by all steps) + repository_url VARCHAR(512), + local_path VARCHAR(512), + base_branch VARCHAR(255), + + -- Orchestrator contract reference + orchestrator_contract_id UUID REFERENCES contracts(id) ON DELETE SET NULL, + + -- Tracking + current_chain_id UUID, -- FK added after directive_chains table + chain_generation_count INTEGER NOT NULL DEFAULT 0, + total_cost_usd FLOAT NOT NULL DEFAULT 0.0, + started_at TIMESTAMPTZ, + completed_at TIMESTAMPTZ, + + version INTEGER NOT NULL DEFAULT 1, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_directives_owner_id ON directives(owner_id); +CREATE INDEX idx_directives_status ON directives(status); +CREATE INDEX idx_directives_orchestrator_contract + ON directives(orchestrator_contract_id) + WHERE orchestrator_contract_id IS NOT NULL; + +-- Add directive reference to contracts +ALTER TABLE contracts ADD COLUMN directive_id UUID REFERENCES directives(id) ON DELETE SET NULL; +ALTER TABLE contracts ADD COLUMN is_directive_orchestrator BOOLEAN NOT NULL DEFAULT false; +ALTER TABLE contracts ADD COLUMN spawned_directive_id UUID REFERENCES directives(id) ON DELETE SET NULL; + +-- ============================================================================ +-- 2. DIRECTIVE CHAINS +-- ============================================================================ +CREATE TABLE directive_chains ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + directive_id UUID NOT NULL REFERENCES directives(id) ON DELETE CASCADE, + generation INTEGER NOT NULL DEFAULT 1, + + -- Plan metadata + name VARCHAR(255) NOT NULL, + description TEXT, + rationale TEXT, + planning_model VARCHAR(100), + + -- State + status VARCHAR(32) NOT NULL DEFAULT 'pending' + CHECK (status IN ('pending', 'running', 'completed', 'failed', 'superseded')), + + -- Execution tracking + total_steps INTEGER NOT NULL DEFAULT 0, + completed_steps INTEGER NOT NULL DEFAULT 0, + failed_steps INTEGER NOT NULL DEFAULT 0, + current_confidence FLOAT, + + -- Timestamps + started_at TIMESTAMPTZ, + completed_at TIMESTAMPTZ, + version INTEGER NOT NULL DEFAULT 1, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_directive_chains_directive ON directive_chains(directive_id); +CREATE INDEX idx_directive_chains_status ON directive_chains(status); + +-- Add FK from directives to chains +ALTER TABLE directives + ADD CONSTRAINT fk_directives_current_chain + FOREIGN KEY (current_chain_id) REFERENCES directive_chains(id) + ON DELETE SET NULL; + +-- ============================================================================ +-- 3. CHAIN STEPS +-- ============================================================================ +CREATE TABLE chain_steps ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + chain_id UUID NOT NULL REFERENCES directive_chains(id) ON DELETE CASCADE, + + -- Step definition + name VARCHAR(255) NOT NULL, + description TEXT, + step_type VARCHAR(32) NOT NULL DEFAULT 'execute', + + -- Contract template + contract_type VARCHAR(32) NOT NULL DEFAULT 'simple', + initial_phase VARCHAR(32) DEFAULT 'plan', + task_plan TEXT, + phases TEXT[] DEFAULT '{}', + + -- DAG edges + depends_on UUID[] DEFAULT '{}', + parallel_group VARCHAR(100), + + -- Requirement traceability + requirement_ids TEXT[] DEFAULT '{}', + acceptance_criteria_ids TEXT[] DEFAULT '{}', + + -- Verification configuration + verifier_config JSONB DEFAULT '{}', + + -- State + status VARCHAR(32) NOT NULL DEFAULT 'pending' + CHECK (status IN ('pending', 'ready', 'running', 'evaluating', 'passed', 'failed', 'rework', 'skipped', 'blocked')), + + -- Instantiated references + contract_id UUID REFERENCES contracts(id) ON DELETE SET NULL, + supervisor_task_id UUID, + + -- Evaluation tracking + confidence_score FLOAT, + confidence_level VARCHAR(10), + evaluation_count INTEGER NOT NULL DEFAULT 0, + rework_count INTEGER NOT NULL DEFAULT 0, + last_evaluation_id UUID, + + -- Editor layout + editor_x FLOAT DEFAULT 0, + editor_y FLOAT DEFAULT 0, + order_index INTEGER NOT NULL DEFAULT 0, + + -- Timestamps + started_at TIMESTAMPTZ, + completed_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_chain_steps_chain ON chain_steps(chain_id); +CREATE INDEX idx_chain_steps_status ON chain_steps(status); +CREATE INDEX idx_chain_steps_contract ON chain_steps(contract_id) WHERE contract_id IS NOT NULL; +CREATE INDEX idx_chain_steps_supervisor_task + ON chain_steps(supervisor_task_id) + WHERE supervisor_task_id IS NOT NULL; + +-- ============================================================================ +-- 4. EVALUATIONS +-- ============================================================================ +CREATE TABLE directive_evaluations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + directive_id UUID NOT NULL REFERENCES directives(id) ON DELETE CASCADE, + chain_id UUID REFERENCES directive_chains(id) ON DELETE SET NULL, + step_id UUID REFERENCES chain_steps(id) ON DELETE SET NULL, + contract_id UUID REFERENCES contracts(id) ON DELETE SET NULL, + + -- Evaluation metadata + evaluation_type VARCHAR(32) NOT NULL, + evaluation_number INTEGER NOT NULL DEFAULT 1, + evaluator VARCHAR(100), + + -- Results + passed BOOLEAN NOT NULL, + overall_score FLOAT, + confidence_level VARCHAR(10), + + -- Programmatic results + programmatic_results JSONB DEFAULT '[]', + + -- LLM evaluation results + llm_results JSONB DEFAULT '{}', + + -- Composite results + criteria_results JSONB NOT NULL DEFAULT '[]', + summary_feedback TEXT NOT NULL DEFAULT '', + rework_instructions TEXT, + + -- Snapshots + directive_snapshot JSONB, + deliverables_snapshot JSONB, + + -- Timing + started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + completed_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_evaluations_directive ON directive_evaluations(directive_id); +CREATE INDEX idx_evaluations_step ON directive_evaluations(step_id); +CREATE INDEX idx_evaluations_chain ON directive_evaluations(chain_id); + +-- Add FK from chain_steps to evaluations +ALTER TABLE chain_steps + ADD CONSTRAINT fk_steps_last_evaluation + FOREIGN KEY (last_evaluation_id) REFERENCES directive_evaluations(id) + ON DELETE SET NULL; + +-- ============================================================================ +-- 5. EVENTS +-- ============================================================================ +CREATE TABLE directive_events ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + directive_id UUID NOT NULL REFERENCES directives(id) ON DELETE CASCADE, + chain_id UUID REFERENCES directive_chains(id) ON DELETE SET NULL, + step_id UUID REFERENCES chain_steps(id) ON DELETE SET NULL, + + -- Event classification + event_type VARCHAR(64) NOT NULL, + severity VARCHAR(16) NOT NULL DEFAULT 'info', + + -- Payload + event_data JSONB, + + -- Actor + actor_type VARCHAR(32) NOT NULL DEFAULT 'system', + actor_id UUID, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_events_directive ON directive_events(directive_id); +CREATE INDEX idx_events_chain ON directive_events(chain_id); +CREATE INDEX idx_events_step ON directive_events(step_id); +CREATE INDEX idx_events_type ON directive_events(event_type); +CREATE INDEX idx_events_created ON directive_events(created_at); + +-- ============================================================================ +-- 6. VERIFIERS +-- ============================================================================ +CREATE TABLE directive_verifiers ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + directive_id UUID NOT NULL REFERENCES directives(id) ON DELETE CASCADE, + + -- Definition + name VARCHAR(100) NOT NULL, + verifier_type VARCHAR(32) NOT NULL, + + -- Configuration + command VARCHAR(1000), + working_directory VARCHAR(500), + timeout_seconds INTEGER DEFAULT 300, + environment JSONB DEFAULT '{}', + + -- Detection + auto_detect BOOLEAN NOT NULL DEFAULT true, + detect_files TEXT[] DEFAULT '{}', + + -- Scoring + weight FLOAT NOT NULL DEFAULT 1.0, + required BOOLEAN NOT NULL DEFAULT false, + + -- State + enabled BOOLEAN NOT NULL DEFAULT true, + last_run_at TIMESTAMPTZ, + last_result JSONB, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_verifiers_directive ON directive_verifiers(directive_id); + +-- ============================================================================ +-- 7. APPROVALS +-- ============================================================================ +CREATE TABLE directive_approvals ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + directive_id UUID NOT NULL REFERENCES directives(id) ON DELETE CASCADE, + step_id UUID REFERENCES chain_steps(id) ON DELETE SET NULL, + + -- Request + approval_type VARCHAR(64) NOT NULL, + description TEXT NOT NULL, + context JSONB, + urgency VARCHAR(16) NOT NULL DEFAULT 'normal', + + -- Response + status VARCHAR(32) NOT NULL DEFAULT 'pending' + CHECK (status IN ('pending', 'approved', 'denied', 'expired')), + response TEXT, + responded_by UUID, + responded_at TIMESTAMPTZ, + + expires_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_approvals_directive ON directive_approvals(directive_id); +CREATE INDEX idx_approvals_status ON directive_approvals(status); diff --git a/makima/src/bin/makima.rs b/makima/src/bin/makima.rs index d7646c2..ee5895c 100644 --- a/makima/src/bin/makima.rs +++ b/makima/src/bin/makima.rs @@ -7,7 +7,7 @@ use std::sync::Arc; use makima::daemon::api::{ApiClient, CreateContractRequest}; use makima::daemon::cli::{ Cli, CliConfig, Commands, ConfigCommand, ContractCommand, - DirectiveCommand, SupervisorCommand, ViewArgs, + SupervisorCommand, ViewArgs, }; use makima::daemon::tui::{self, Action, App, ListItem, ViewType, TuiWsClient, WsEvent, OutputLine, OutputMessageType, WsConnectionState, RepositorySuggestion}; use makima::daemon::config::{DaemonConfig, RepoEntry}; @@ -31,7 +31,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { Commands::Contract(cmd) => run_contract(cmd).await, Commands::View(args) => run_view(args).await, Commands::Config(cmd) => run_config(cmd).await, - Commands::Directive(cmd) => run_directive(cmd).await, } } @@ -802,154 +801,6 @@ async fn run_config(cmd: ConfigCommand) -> Result<(), Box<dyn std::error::Error } } -/// Run directive commands. -async fn run_directive( - cmd: DirectiveCommand, -) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { - match cmd { - DirectiveCommand::Create(args) => { - let client = ApiClient::new(args.common.api_url, args.common.api_key)?; - let result = client - .create_directive(&args.goal, args.repository.as_deref(), &args.autonomy) - .await?; - println!("{}", serde_json::to_string(&result.0)?); - } - DirectiveCommand::Status(args) => { - let client = ApiClient::new(args.common.api_url, args.common.api_key)?; - let result = client.get_directive(args.directive_id).await?; - println!("{}", serde_json::to_string(&result.0)?); - } - DirectiveCommand::List(args) => { - let client = ApiClient::new(args.common.api_url, args.common.api_key)?; - let result = client - .list_directives(args.status.as_deref(), args.limit) - .await?; - println!("{}", serde_json::to_string(&result.0)?); - } - DirectiveCommand::Steps(args) => { - let client = ApiClient::new(args.common.api_url, args.common.api_key)?; - let result = client.get_directive_chain(args.directive_id).await?; - println!("{}", serde_json::to_string(&result.0)?); - } - DirectiveCommand::Graph(args) => { - let client = ApiClient::new(args.common.api_url, args.common.api_key)?; - let result = client.get_directive_graph(args.directive_id).await?; - - if args.with_status { - // Enhanced ASCII visualization with status - if let Some(nodes) = result.0.get("nodes").and_then(|v| v.as_array()) { - let mut by_depth: std::collections::HashMap<i32, Vec<(&str, &str)>> = - std::collections::HashMap::new(); - - for node in nodes { - let name = node.get("name").and_then(|v| v.as_str()).unwrap_or("?"); - let status = node - .get("status") - .and_then(|v| v.as_str()) - .unwrap_or("pending"); - let depth = node.get("depth").and_then(|v| v.as_i64()).unwrap_or(0) as i32; - by_depth.entry(depth).or_default().push((name, status)); - } - - let directive_name = result - .0 - .get("name") - .and_then(|v| v.as_str()) - .unwrap_or("Directive"); - println!("Directive: {}", directive_name); - println!(); - - let max_depth = by_depth.keys().max().copied().unwrap_or(0); - for depth in 0..=max_depth { - if let Some(steps) = by_depth.get(&depth) { - let indent = " ".repeat(depth as usize); - for (name, status) in steps { - let status_icon = match *status { - "passed" | "completed" => "\u{2713}", - "running" | "evaluating" => "\u{21bb}", - "failed" | "blocked" => "\u{2717}", - "rework" => "\u{21ba}", - "skipped" => "\u{2212}", - "ready" => "\u{25b7}", - _ => "\u{25cb}", - }; - println!("{}[{}] {} {}", indent, name, status_icon, status); - } - if depth < max_depth { - println!("{} |", indent); - println!("{} v", indent); - } - } - } - } - } else { - println!("{}", serde_json::to_string_pretty(&result.0)?); - } - } - DirectiveCommand::Events(args) => { - let client = ApiClient::new(args.common.api_url, args.common.api_key)?; - let result = client - .list_directive_events(args.directive_id, args.limit) - .await?; - println!("{}", serde_json::to_string(&result.0)?); - } - DirectiveCommand::Approve(args) => { - let client = ApiClient::new(args.common.api_url, args.common.api_key)?; - let result = client - .approve_directive_request( - args.directive_id, - args.approval_id, - args.response.as_deref(), - ) - .await?; - println!("{}", serde_json::to_string(&result.0)?); - } - DirectiveCommand::Deny(args) => { - let client = ApiClient::new(args.common.api_url, args.common.api_key)?; - let result = client - .deny_directive_request( - args.directive_id, - args.approval_id, - args.reason.as_deref(), - ) - .await?; - println!("{}", serde_json::to_string(&result.0)?); - } - DirectiveCommand::Start(args) => { - let client = ApiClient::new(args.common.api_url, args.common.api_key)?; - eprintln!("Starting directive {}...", args.directive_id); - let result = client.start_directive(args.directive_id).await?; - println!("{}", serde_json::to_string(&result.0)?); - } - DirectiveCommand::Pause(args) => { - let client = ApiClient::new(args.common.api_url, args.common.api_key)?; - eprintln!("Pausing directive {}...", args.directive_id); - let result = client.pause_directive(args.directive_id).await?; - println!("{}", serde_json::to_string(&result.0)?); - } - DirectiveCommand::Resume(args) => { - let client = ApiClient::new(args.common.api_url, args.common.api_key)?; - eprintln!("Resuming directive {}...", args.directive_id); - let result = client.resume_directive(args.directive_id).await?; - println!("{}", serde_json::to_string(&result.0)?); - } - DirectiveCommand::Stop(args) => { - let client = ApiClient::new(args.common.api_url, args.common.api_key)?; - eprintln!("Stopping directive {}...", args.directive_id); - let result = client.stop_directive(args.directive_id).await?; - println!("{}", serde_json::to_string(&result.0)?); - } - DirectiveCommand::Archive(args) => { - let client = ApiClient::new(args.common.api_url, args.common.api_key)?; - eprintln!("Archiving directive {}...", args.directive_id); - let result = client.archive_directive(args.directive_id).await?; - println!("{}", serde_json::to_string(&result.0)?); - } - } - - Ok(()) -} - /// Load contracts from API async fn load_contracts(client: &ApiClient) -> Result<Vec<ListItem>, Box<dyn std::error::Error + Send + Sync>> { let result = client.list_contracts().await?; diff --git a/makima/src/daemon/api/directive.rs b/makima/src/daemon/api/directive.rs deleted file mode 100644 index 48762d6..0000000 --- a/makima/src/daemon/api/directive.rs +++ /dev/null @@ -1,447 +0,0 @@ -//! Directive API methods. - -use uuid::Uuid; - -use super::client::{ApiClient, ApiError}; -use super::supervisor::JsonValue; - -impl ApiClient { - /// Create a new directive. - pub async fn create_directive( - &self, - goal: &str, - repository_url: Option<&str>, - autonomy_level: &str, - ) -> Result<JsonValue, ApiError> { - #[derive(serde::Serialize)] - #[serde(rename_all = "camelCase")] - struct CreateRequest<'a> { - goal: &'a str, - repository_url: Option<&'a str>, - autonomy_level: &'a str, - } - let req = CreateRequest { - goal, - repository_url, - autonomy_level, - }; - self.post("/api/v1/directives", &req).await - } - - /// List all directives for the authenticated user. - pub async fn list_directives( - &self, - status: Option<&str>, - limit: i32, - ) -> Result<JsonValue, ApiError> { - let mut params = Vec::new(); - if let Some(s) = status { - params.push(format!("status={}", s)); - } - params.push(format!("limit={}", limit)); - let query_string = format!("?{}", params.join("&")); - self.get(&format!("/api/v1/directives{}", query_string)) - .await - } - - /// Get a directive by ID (includes progress info). - pub async fn get_directive(&self, directive_id: Uuid) -> Result<JsonValue, ApiError> { - self.get(&format!("/api/v1/directives/{}", directive_id)) - .await - } - - /// Archive a directive. - pub async fn archive_directive(&self, directive_id: Uuid) -> Result<JsonValue, ApiError> { - self.delete_with_response(&format!("/api/v1/directives/{}", directive_id)) - .await - } - - /// Start a directive (plans and begins execution). - pub async fn start_directive(&self, directive_id: Uuid) -> Result<JsonValue, ApiError> { - self.post_empty(&format!("/api/v1/directives/{}/start", directive_id)) - .await - } - - /// Pause a directive. - pub async fn pause_directive(&self, directive_id: Uuid) -> Result<JsonValue, ApiError> { - self.post_empty(&format!("/api/v1/directives/{}/pause", directive_id)) - .await - } - - /// Resume a paused directive. - pub async fn resume_directive(&self, directive_id: Uuid) -> Result<JsonValue, ApiError> { - self.post_empty(&format!("/api/v1/directives/{}/resume", directive_id)) - .await - } - - /// Stop a directive. - pub async fn stop_directive(&self, directive_id: Uuid) -> Result<JsonValue, ApiError> { - self.post_empty(&format!("/api/v1/directives/{}/stop", directive_id)) - .await - } - - /// Get the current chain and steps for a directive. - pub async fn get_directive_chain(&self, directive_id: Uuid) -> Result<JsonValue, ApiError> { - self.get(&format!("/api/v1/directives/{}/chain", directive_id)) - .await - } - - /// Get directive DAG structure for visualization. - pub async fn get_directive_graph(&self, directive_id: Uuid) -> Result<JsonValue, ApiError> { - self.get(&format!("/api/v1/directives/{}/chain/graph", directive_id)) - .await - } - - /// List events for a directive. - pub async fn list_directive_events( - &self, - directive_id: Uuid, - limit: i32, - ) -> Result<JsonValue, ApiError> { - self.get(&format!( - "/api/v1/directives/{}/events?limit={}", - directive_id, limit - )) - .await - } - - /// List pending approvals for a directive. - pub async fn list_directive_approvals( - &self, - directive_id: Uuid, - ) -> Result<JsonValue, ApiError> { - self.get(&format!("/api/v1/directives/{}/approvals", directive_id)) - .await - } - - /// Approve an approval request. - pub async fn approve_directive_request( - &self, - directive_id: Uuid, - approval_id: Uuid, - response: Option<&str>, - ) -> Result<JsonValue, ApiError> { - #[derive(serde::Serialize)] - #[serde(rename_all = "camelCase")] - struct ApprovalRequest<'a> { - response: Option<&'a str>, - } - let req = ApprovalRequest { response }; - self.post( - &format!( - "/api/v1/directives/{}/approvals/{}/approve", - directive_id, approval_id - ), - &req, - ) - .await - } - - /// Deny an approval request. - pub async fn deny_directive_request( - &self, - directive_id: Uuid, - approval_id: Uuid, - response: Option<&str>, - ) -> Result<JsonValue, ApiError> { - #[derive(serde::Serialize)] - #[serde(rename_all = "camelCase")] - struct ApprovalRequest<'a> { - response: Option<&'a str>, - } - let req = ApprovalRequest { response }; - self.post( - &format!( - "/api/v1/directives/{}/approvals/{}/deny", - directive_id, approval_id - ), - &req, - ) - .await - } - - // ========================================================================= - // Chain operations - // ========================================================================= - - /// Force chain regeneration (replan). - pub async fn replan_directive_chain( - &self, - directive_id: Uuid, - ) -> Result<JsonValue, ApiError> { - self.post_empty(&format!( - "/api/v1/directives/{}/chain/replan", - directive_id - )) - .await - } - - // ========================================================================= - // Step management - // ========================================================================= - - /// Add a step to a directive's chain. - pub async fn add_directive_step( - &self, - directive_id: Uuid, - name: &str, - description: Option<&str>, - step_type: Option<&str>, - depends_on: Option<Vec<Uuid>>, - ) -> Result<JsonValue, ApiError> { - #[derive(serde::Serialize)] - #[serde(rename_all = "camelCase")] - struct AddStepReq<'a> { - name: &'a str, - #[serde(skip_serializing_if = "Option::is_none")] - description: Option<&'a str>, - #[serde(skip_serializing_if = "Option::is_none")] - step_type: Option<&'a str>, - #[serde(skip_serializing_if = "Option::is_none")] - depends_on: Option<Vec<Uuid>>, - } - let req = AddStepReq { - name, - description, - step_type, - depends_on, - }; - self.post( - &format!("/api/v1/directives/{}/chain/steps", directive_id), - &req, - ) - .await - } - - /// Get a step by ID. - pub async fn get_directive_step( - &self, - directive_id: Uuid, - step_id: Uuid, - ) -> Result<JsonValue, ApiError> { - self.get(&format!( - "/api/v1/directives/{}/chain/steps/{}", - directive_id, step_id - )) - .await - } - - /// Update a step. - pub async fn update_directive_step( - &self, - directive_id: Uuid, - step_id: Uuid, - update: serde_json::Value, - ) -> Result<JsonValue, ApiError> { - self.put( - &format!( - "/api/v1/directives/{}/chain/steps/{}", - directive_id, step_id - ), - &update, - ) - .await - } - - /// Delete a step. - pub async fn delete_directive_step( - &self, - directive_id: Uuid, - step_id: Uuid, - ) -> Result<(), ApiError> { - self.delete(&format!( - "/api/v1/directives/{}/chain/steps/{}", - directive_id, step_id - )) - .await - } - - /// Skip a step. - pub async fn skip_directive_step( - &self, - directive_id: Uuid, - step_id: Uuid, - ) -> Result<JsonValue, ApiError> { - self.post_empty(&format!( - "/api/v1/directives/{}/chain/steps/{}/skip", - directive_id, step_id - )) - .await - } - - /// Force re-evaluation of a step. - pub async fn evaluate_directive_step( - &self, - directive_id: Uuid, - step_id: Uuid, - ) -> Result<JsonValue, ApiError> { - self.post_empty(&format!( - "/api/v1/directives/{}/chain/steps/{}/evaluate", - directive_id, step_id - )) - .await - } - - /// Trigger manual rework for a step. - pub async fn rework_directive_step( - &self, - directive_id: Uuid, - step_id: Uuid, - instructions: Option<&str>, - ) -> Result<JsonValue, ApiError> { - #[derive(serde::Serialize)] - #[serde(rename_all = "camelCase")] - struct ReworkReq<'a> { - instructions: Option<&'a str>, - } - let req = ReworkReq { instructions }; - self.post( - &format!( - "/api/v1/directives/{}/chain/steps/{}/rework", - directive_id, step_id - ), - &req, - ) - .await - } - - // ========================================================================= - // Evaluations - // ========================================================================= - - /// List evaluations for a directive. - pub async fn list_directive_evaluations( - &self, - directive_id: Uuid, - ) -> Result<JsonValue, ApiError> { - self.get(&format!( - "/api/v1/directives/{}/evaluations", - directive_id - )) - .await - } - - // ========================================================================= - // Verifiers - // ========================================================================= - - /// List verifiers for a directive. - pub async fn list_directive_verifiers( - &self, - directive_id: Uuid, - ) -> Result<JsonValue, ApiError> { - self.get(&format!("/api/v1/directives/{}/verifiers", directive_id)) - .await - } - - /// Add a verifier to a directive. - pub async fn add_directive_verifier( - &self, - directive_id: Uuid, - name: &str, - verifier_type: &str, - command: Option<&str>, - ) -> Result<JsonValue, ApiError> { - #[derive(serde::Serialize)] - #[serde(rename_all = "camelCase")] - struct CreateVerifierReq<'a> { - name: &'a str, - verifier_type: &'a str, - #[serde(skip_serializing_if = "Option::is_none")] - command: Option<&'a str>, - } - let req = CreateVerifierReq { - name, - verifier_type, - command, - }; - self.post( - &format!("/api/v1/directives/{}/verifiers", directive_id), - &req, - ) - .await - } - - /// Update a verifier. - pub async fn update_directive_verifier( - &self, - directive_id: Uuid, - verifier_id: Uuid, - update: serde_json::Value, - ) -> Result<JsonValue, ApiError> { - self.put( - &format!( - "/api/v1/directives/{}/verifiers/{}", - directive_id, verifier_id - ), - &update, - ) - .await - } - - /// Auto-detect verifiers based on repository content. - pub async fn auto_detect_directive_verifiers( - &self, - directive_id: Uuid, - ) -> Result<JsonValue, ApiError> { - self.post_empty(&format!( - "/api/v1/directives/{}/verifiers/auto-detect", - directive_id - )) - .await - } - - // ========================================================================= - // Requirements & Spec - // ========================================================================= - - /// Update directive requirements. - pub async fn update_directive_requirements( - &self, - directive_id: Uuid, - requirements: serde_json::Value, - ) -> Result<JsonValue, ApiError> { - #[derive(serde::Serialize)] - #[serde(rename_all = "camelCase")] - struct UpdateReq { - requirements: serde_json::Value, - } - let req = UpdateReq { requirements }; - self.put( - &format!("/api/v1/directives/{}/requirements", directive_id), - &req, - ) - .await - } - - /// Update directive acceptance criteria. - pub async fn update_directive_criteria( - &self, - directive_id: Uuid, - acceptance_criteria: serde_json::Value, - ) -> Result<JsonValue, ApiError> { - #[derive(serde::Serialize)] - #[serde(rename_all = "camelCase")] - struct UpdateReq { - acceptance_criteria: serde_json::Value, - } - let req = UpdateReq { acceptance_criteria }; - self.put( - &format!("/api/v1/directives/{}/criteria", directive_id), - &req, - ) - .await - } - - /// Generate a specification from the directive's goal. - pub async fn generate_directive_spec( - &self, - directive_id: Uuid, - ) -> Result<JsonValue, ApiError> { - self.post_empty(&format!( - "/api/v1/directives/{}/generate-spec", - directive_id - )) - .await - } -} diff --git a/makima/src/daemon/api/mod.rs b/makima/src/daemon/api/mod.rs index 2d1efbf..49d80e0 100644 --- a/makima/src/daemon/api/mod.rs +++ b/makima/src/daemon/api/mod.rs @@ -2,7 +2,6 @@ pub mod client; pub mod contract; -pub mod directive; pub mod supervisor; pub use client::ApiClient; diff --git a/makima/src/daemon/cli/directive.rs b/makima/src/daemon/cli/directive.rs deleted file mode 100644 index a2bb34b..0000000 --- a/makima/src/daemon/cli/directive.rs +++ /dev/null @@ -1,186 +0,0 @@ -//! Directive CLI commands for autonomous goal-driven orchestration. -//! -//! Directives are top-level goals that the system works toward. Each directive -//! generates a chain of steps that are executed autonomously with configurable -//! guardrails. - -use clap::Args; -use uuid::Uuid; - -/// Common arguments for directive commands requiring API access. -#[derive(Args, Debug, Clone)] -pub struct DirectiveArgs { - /// API URL - #[arg(long, env = "MAKIMA_API_URL", default_value = "https://api.makima.jp", global = true)] - pub api_url: String, - - /// API key for authentication - #[arg(long, env = "MAKIMA_API_KEY", global = true)] - pub api_key: String, -} - -/// Arguments for the `create` command. -#[derive(Args, Debug)] -pub struct CreateArgs { - #[command(flatten)] - pub common: DirectiveArgs, - - /// The goal for the directive - #[arg(short, long)] - pub goal: String, - - /// Repository URL (optional) - #[arg(short, long)] - pub repository: Option<String>, - - /// Autonomy level: full_auto, guardrails, or manual - #[arg(short, long, default_value = "guardrails")] - pub autonomy: String, -} - -/// Arguments for the `status` command. -#[derive(Args, Debug)] -pub struct StatusArgs { - #[command(flatten)] - pub common: DirectiveArgs, - - /// Directive ID - pub directive_id: Uuid, -} - -/// Arguments for the `list` command. -#[derive(Args, Debug)] -pub struct ListArgs { - #[command(flatten)] - pub common: DirectiveArgs, - - /// Filter by status (draft, planning, active, paused, completed, archived, failed) - #[arg(long)] - pub status: Option<String>, - - /// Limit number of results - #[arg(long, default_value = "50")] - pub limit: i32, -} - -/// Arguments for the `steps` command. -#[derive(Args, Debug)] -pub struct StepsArgs { - #[command(flatten)] - pub common: DirectiveArgs, - - /// Directive ID - pub directive_id: Uuid, -} - -/// Arguments for the `events` command. -#[derive(Args, Debug)] -pub struct EventsArgs { - #[command(flatten)] - pub common: DirectiveArgs, - - /// Directive ID - pub directive_id: Uuid, - - /// Limit number of events - #[arg(long, default_value = "50")] - pub limit: i32, -} - -/// Arguments for the `approve` command. -#[derive(Args, Debug)] -pub struct ApproveArgs { - #[command(flatten)] - pub common: DirectiveArgs, - - /// Directive ID - pub directive_id: Uuid, - - /// Approval ID - pub approval_id: Uuid, - - /// Response message (optional) - #[arg(short, long)] - pub response: Option<String>, -} - -/// Arguments for the `deny` command. -#[derive(Args, Debug)] -pub struct DenyArgs { - #[command(flatten)] - pub common: DirectiveArgs, - - /// Directive ID - pub directive_id: Uuid, - - /// Approval ID - pub approval_id: Uuid, - - /// Reason for denial (optional) - #[arg(short, long)] - pub reason: Option<String>, -} - -/// Arguments for the `start` command. -#[derive(Args, Debug)] -pub struct StartArgs { - #[command(flatten)] - pub common: DirectiveArgs, - - /// Directive ID - pub directive_id: Uuid, -} - -/// Arguments for the `pause` command. -#[derive(Args, Debug)] -pub struct PauseArgs { - #[command(flatten)] - pub common: DirectiveArgs, - - /// Directive ID - pub directive_id: Uuid, -} - -/// Arguments for the `resume` command. -#[derive(Args, Debug)] -pub struct ResumeArgs { - #[command(flatten)] - pub common: DirectiveArgs, - - /// Directive ID - pub directive_id: Uuid, -} - -/// Arguments for the `stop` command. -#[derive(Args, Debug)] -pub struct StopArgs { - #[command(flatten)] - pub common: DirectiveArgs, - - /// Directive ID - pub directive_id: Uuid, -} - -/// Arguments for the `archive` command. -#[derive(Args, Debug)] -pub struct ArchiveArgs { - #[command(flatten)] - pub common: DirectiveArgs, - - /// Directive ID - pub directive_id: Uuid, -} - -/// Arguments for the `graph` command (ASCII DAG visualization). -#[derive(Args, Debug)] -pub struct GraphArgs { - #[command(flatten)] - pub common: DirectiveArgs, - - /// Directive ID - pub directive_id: Uuid, - - /// Show step status in nodes - #[arg(long)] - pub with_status: bool, -} diff --git a/makima/src/daemon/cli/mod.rs b/makima/src/daemon/cli/mod.rs index 77eee80..0805edd 100644 --- a/makima/src/daemon/cli/mod.rs +++ b/makima/src/daemon/cli/mod.rs @@ -3,7 +3,6 @@ pub mod config; pub mod contract; pub mod daemon; -pub mod directive; pub mod server; pub mod supervisor; pub mod view; @@ -13,7 +12,6 @@ use clap::{Parser, Subcommand}; pub use config::CliConfig; pub use contract::ContractArgs; pub use daemon::DaemonArgs; -pub use directive::DirectiveArgs; pub use server::ServerArgs; pub use supervisor::SupervisorArgs; pub use view::ViewArgs; @@ -60,14 +58,6 @@ pub enum Commands { /// Saves configuration to ~/.makima/config.toml for use by CLI commands. #[command(subcommand)] Config(ConfigCommand), - - /// Directive commands for autonomous goal-driven orchestration - /// - /// Directives are top-level goals that generate chains of steps executed - /// autonomously with configurable guardrails. Steps spawn contracts with - /// supervisors and are verified with programmatic and LLM evaluation. - #[command(subcommand)] - Directive(DirectiveCommand), } /// Config subcommands for CLI configuration. @@ -206,52 +196,6 @@ pub enum ContractCommand { CreateFile(contract::CreateFileArgs), } -/// Directive subcommands for autonomous goal-driven orchestration. -#[derive(Subcommand, Debug)] -pub enum DirectiveCommand { - /// Create a new directive from a goal - Create(directive::CreateArgs), - - /// Get directive status and progress - Status(directive::StatusArgs), - - /// List all directives - List(directive::ListArgs), - - /// List steps in the directive's chain - Steps(directive::StepsArgs), - - /// Display ASCII DAG visualization - /// - /// Shows the directive's chain structure as an ASCII graph with - /// steps as nodes and dependencies as edges. - Graph(directive::GraphArgs), - - /// Show recent events for a directive - Events(directive::EventsArgs), - - /// Approve a pending approval request - Approve(directive::ApproveArgs), - - /// Deny a pending approval request - Deny(directive::DenyArgs), - - /// Start a directive (generates chain and begins execution) - Start(directive::StartArgs), - - /// Pause a running directive - Pause(directive::PauseArgs), - - /// Resume a paused directive - Resume(directive::ResumeArgs), - - /// Stop a directive - Stop(directive::StopArgs), - - /// Archive a directive - Archive(directive::ArchiveArgs), -} - impl Cli { /// Parse command-line arguments pub fn parse_args() -> Self { diff --git a/makima/src/daemon/skills/directive.md b/makima/src/daemon/skills/directive.md deleted file mode 100644 index 97e8e20..0000000 --- a/makima/src/daemon/skills/directive.md +++ /dev/null @@ -1,303 +0,0 @@ ---- -name: makima-directive -description: Directive orchestration tools for autonomous goal-driven execution. Use when working with directives, chains, steps, verifiers, and approvals. ---- - -# Directive Orchestration Tools - -Directives are top-level goals that drive autonomous execution with configurable guardrails. Each directive generates a chain of steps that spawn contracts with supervisors, verified by programmatic checks and LLM evaluation. - -## Architecture - -``` -Directive (goal + requirements + acceptance criteria) - | - +-- Chain (generated DAG execution plan) - | +-- Step 1 (pending -> ready -> running -> evaluating -> passed) - | | +-- Contract (spawned when step reaches 'ready') - | | +-- Supervisor Task - | +-- Step 2 (depends_on: [Step 1]) - | +-- Step 3 (depends_on: [Step 1], parallel with Step 2) - | - +-- Verifiers (test runner, linter, build, type checker) - +-- Evaluations (programmatic + LLM composite scores) - +-- Events (audit stream) - +-- Approvals (human-in-the-loop gates) -``` - -## Status Flow - -### Directive Status -- `draft` - Created but not started -- `planning` - Generating chain from requirements -- `active` - Executing steps -- `paused` - Temporarily stopped -- `completed` - All steps passed -- `archived` - No longer active -- `failed` - Execution failed - -### Step Status -- `pending` - Waiting for dependencies -- `ready` - Dependencies met, ready to start -- `running` - Contract executing -- `evaluating` - Running verifiers -- `passed` - Evaluation succeeded -- `failed` - Evaluation failed, exceeded retries -- `rework` - Sent back for corrections -- `skipped` - Manually skipped -- `blocked` - Blocked by failed dependency - -## Autonomy Levels - -- `full_auto` - No approval gates, automatic progression -- `guardrails` - Request approval for yellow/red confidence scores -- `manual` - Request approval for all step completions - -## Confidence Scoring - -Each step evaluation produces a composite confidence score: - -1. **Programmatic verifiers** run first (tests, lint, build) - - Weight: 1.0 each - - If any required verifier fails: automatic RED - -2. **LLM evaluation** runs second - - Weight: 2.0 - - Evaluates against acceptance criteria - -3. **Composite score** computed from weighted average - - GREEN: >= configured threshold (default 0.8) - - YELLOW: >= yellow threshold (default 0.5) - - RED: below yellow threshold - -## CLI Commands - -```bash -# Create a new directive -makima directive create --goal "Add OAuth2 authentication" --repository https://github.com/org/repo - -# List directives -makima directive list [--status active] - -# Get directive status with progress -makima directive status <directive-id> - -# Start execution (generates chain and begins) -makima directive start <directive-id> - -# View chain steps -makima directive steps <directive-id> - -# View DAG visualization -makima directive graph <directive-id> --with-status - -# View recent events -makima directive events <directive-id> --limit 20 - -# Approve a pending request -makima directive approve <directive-id> <approval-id> [--response "Looks good"] - -# Deny a pending request -makima directive deny <directive-id> <approval-id> [--reason "Need more testing"] - -# Lifecycle commands -makima directive pause <directive-id> -makima directive resume <directive-id> -makima directive stop <directive-id> -makima directive archive <directive-id> -``` - -## API Endpoints - -### Directive CRUD -``` -POST /api/v1/directives # Create from goal -GET /api/v1/directives # List -GET /api/v1/directives/:id # Get with progress -PUT /api/v1/directives/:id # Update -DELETE /api/v1/directives/:id # Archive -``` - -### Lifecycle -``` -POST /api/v1/directives/:id/start # Plan + execute -POST /api/v1/directives/:id/pause # Pause -POST /api/v1/directives/:id/resume # Resume -POST /api/v1/directives/:id/stop # Stop -``` - -### Chain & Steps -``` -GET /api/v1/directives/:id/chain # Current chain + steps -GET /api/v1/directives/:id/chain/graph # DAG for visualization -POST /api/v1/directives/:id/chain/replan # Force regeneration -POST /api/v1/directives/:id/chain/steps # Add step -PUT /api/v1/directives/:id/chain/steps/:sid # Modify step -DELETE /api/v1/directives/:id/chain/steps/:sid # Remove step -``` - -### Step Operations -``` -GET /api/v1/directives/:id/steps/:sid # Step detail -POST /api/v1/directives/:id/steps/:sid/evaluate # Force re-evaluation -POST /api/v1/directives/:id/steps/:sid/skip # Skip step -POST /api/v1/directives/:id/steps/:sid/rework # Manual rework -``` - -### Monitoring -``` -GET /api/v1/directives/:id/evaluations # List evaluations -GET /api/v1/directives/:id/events # Event log (polling) -GET /api/v1/directives/:id/events/stream # Event stream (SSE) -``` - -### Verifiers -``` -GET /api/v1/directives/:id/verifiers # List verifiers -POST /api/v1/directives/:id/verifiers # Add verifier -PUT /api/v1/directives/:id/verifiers/:vid # Update verifier -POST /api/v1/directives/:id/verifiers/auto-detect # Auto-detect -``` - -### Approvals -``` -GET /api/v1/directives/:id/approvals # Pending approvals -POST /api/v1/directives/:id/approvals/:aid/approve # Approve -POST /api/v1/directives/:id/approvals/:aid/deny # Deny -``` - -## Creating a Directive - -### Request -```json -POST /api/v1/directives -{ - "goal": "Implement user authentication with OAuth2", - "repositoryUrl": "https://github.com/org/repo", - "autonomyLevel": "guardrails", - "confidenceThresholdGreen": 0.8, - "confidenceThresholdYellow": 0.5, - "maxReworkCycles": 3, - "maxTotalCostUsd": 100.0, - "maxWallTimeMinutes": 480 -} -``` - -### Response -```json -{ - "id": "uuid", - "title": "Implement user authentication with OAuth2", - "goal": "Implement user authentication with OAuth2", - "status": "draft", - "autonomyLevel": "guardrails", - "createdAt": "2026-02-05T12:00:00Z" -} -``` - -## Starting a Directive - -When you start a directive: -1. System generates requirements from the goal -2. Chain planner creates a DAG of steps -3. Root steps (no dependencies) transition to `ready` -4. Contracts spawn for ready steps with supervisors -5. Verifiers auto-detect from repository - -## Evaluation Flow - -When a contract completes: - -1. Step transitions to `evaluating` -2. **Programmatic verifiers** run (tests, lint, build) - - Each produces pass/fail + output -3. **LLM evaluation** runs - - Reviews code against acceptance criteria - - Provides feedback and score -4. **Composite score** computed -5. Based on confidence level and autonomy: - - GREEN: Step passes, downstream unblocks - - YELLOW (guardrails): Request approval - - RED: Initiate rework or request approval - -## Rework Flow - -When a step needs rework: - -1. Contract phase reset to editing -2. Supervisor receives rework instructions -3. Rework count incremented -4. If max reworks exceeded: escalate or fail - -## Event Types - -Events are logged for audit and monitoring: - -- `directive_created`, `directive_started`, `directive_paused`, `directive_completed` -- `chain_generated`, `chain_regenerated` -- `step_ready`, `step_started`, `step_evaluating`, `step_passed`, `step_failed` -- `rework_initiated`, `rework_completed` -- `approval_requested`, `approval_granted`, `approval_denied` -- `verifier_run`, `evaluation_completed` -- `circuit_breaker_triggered` - -## Verifier Configuration - -Verifiers can be auto-detected or manually configured: - -```json -POST /api/v1/directives/:id/verifiers -{ - "name": "Test Runner", - "verifierType": "test_runner", - "command": "npm test", - "workingDirectory": ".", - "timeoutSeconds": 300, - "weight": 1.0, - "required": true, - "enabled": true -} -``` - -### Auto-Detection - -The system detects verifiers from: -- `package.json` - npm test, npm run lint, npm run build -- `Cargo.toml` - cargo test, cargo clippy, cargo build -- `pyproject.toml` - pytest, ruff, mypy - -## Circuit Breakers - -Directives have built-in circuit breakers: - -- `maxTotalCostUsd` - Stop if cumulative cost exceeds limit -- `maxWallTimeMinutes` - Stop if elapsed time exceeds limit -- `maxReworkCycles` - Fail step after N rework attempts -- `maxChainRegenerations` - Fail if chain regenerated too many times - -## Example Workflow - -```bash -# 1. Create a directive -makima directive create \ - --goal "Add dark mode to the application" \ - --repository https://github.com/myorg/myapp \ - --autonomy guardrails - -# Returns directive ID: 123e4567-e89b-12d3-a456-426614174000 - -# 2. Start execution -makima directive start 123e4567-e89b-12d3-a456-426614174000 - -# 3. Monitor progress -makima directive status 123e4567-e89b-12d3-a456-426614174000 - -# 4. View the execution graph -makima directive graph 123e4567-e89b-12d3-a456-426614174000 --with-status - -# 5. Watch events -makima directive events 123e4567-e89b-12d3-a456-426614174000 - -# 6. If approval needed, approve or deny -makima directive approve 123e4567-e89b-12d3-a456-426614174000 <approval-id> -``` diff --git a/makima/src/daemon/skills/mod.rs b/makima/src/daemon/skills/mod.rs index c32f550..0b05f3a 100644 --- a/makima/src/daemon/skills/mod.rs +++ b/makima/src/daemon/skills/mod.rs @@ -9,12 +9,8 @@ pub const SUPERVISOR_SKILL: &str = include_str!("supervisor.md"); /// Contract skill content - task-contract interaction commands pub const CONTRACT_SKILL: &str = include_str!("contract.md"); -/// Directive skill content - autonomous goal-driven orchestration -pub const DIRECTIVE_SKILL: &str = include_str!("directive.md"); - /// All skills as (name, content) pairs for installation pub const ALL_SKILLS: &[(&str, &str)] = &[ ("makima-supervisor", SUPERVISOR_SKILL), ("makima-contract", CONTRACT_SKILL), - ("makima-directive", DIRECTIVE_SKILL), ]; diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs index f951751..3b10cb5 100644 --- a/makima/src/db/models.rs +++ b/makima/src/db/models.rs @@ -2596,672 +2596,6 @@ pub struct HeartbeatHistoryQuery { } // ============================================================================= -// Directives (Goal-driven orchestration with chains of steps) -// ============================================================================= - -/// Directive status -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "snake_case")] -pub enum DirectiveStatus { - Draft, - Planning, - Active, - Paused, - Completed, - Archived, - Failed, -} - -impl Default for DirectiveStatus { - fn default() -> Self { - DirectiveStatus::Draft - } -} - -impl std::fmt::Display for DirectiveStatus { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - DirectiveStatus::Draft => write!(f, "draft"), - DirectiveStatus::Planning => write!(f, "planning"), - DirectiveStatus::Active => write!(f, "active"), - DirectiveStatus::Paused => write!(f, "paused"), - DirectiveStatus::Completed => write!(f, "completed"), - DirectiveStatus::Archived => write!(f, "archived"), - DirectiveStatus::Failed => write!(f, "failed"), - } - } -} - -impl std::str::FromStr for DirectiveStatus { - type Err = String; - - fn from_str(s: &str) -> Result<Self, Self::Err> { - match s.to_lowercase().as_str() { - "draft" => Ok(DirectiveStatus::Draft), - "planning" => Ok(DirectiveStatus::Planning), - "active" => Ok(DirectiveStatus::Active), - "paused" => Ok(DirectiveStatus::Paused), - "completed" => Ok(DirectiveStatus::Completed), - "archived" => Ok(DirectiveStatus::Archived), - "failed" => Ok(DirectiveStatus::Failed), - _ => Err(format!("Invalid directive status: {}", s)), - } - } -} - -/// Directive - the top-level goal-driven orchestration entity -#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct Directive { - pub id: Uuid, - pub owner_id: Uuid, - pub title: String, - pub goal: String, - /// Structured requirements: [{ id, title, description, priority, category }] - #[sqlx(json)] - pub requirements: serde_json::Value, - /// Acceptance criteria: [{ id, requirementIds, description, testable, verificationMethod }] - #[sqlx(json)] - pub acceptance_criteria: serde_json::Value, - /// Constraints: [{ id, type, description, impact }] - #[sqlx(json)] - pub constraints: serde_json::Value, - /// External dependencies: [{ id, name, type, status, requiredBy }] - #[sqlx(json)] - pub external_dependencies: serde_json::Value, - pub status: String, - pub autonomy_level: String, - pub confidence_threshold_green: f64, - pub confidence_threshold_yellow: f64, - pub max_total_cost_usd: Option<f64>, - pub max_wall_time_minutes: Option<i32>, - pub max_rework_cycles: Option<i32>, - pub max_chain_regenerations: Option<i32>, - pub repository_url: Option<String>, - pub local_path: Option<String>, - pub base_branch: Option<String>, - pub orchestrator_contract_id: Option<Uuid>, - pub current_chain_id: Option<Uuid>, - pub chain_generation_count: i32, - pub total_cost_usd: f64, - pub started_at: Option<DateTime<Utc>>, - pub completed_at: Option<DateTime<Utc>>, - pub version: i32, - pub created_at: DateTime<Utc>, - pub updated_at: DateTime<Utc>, -} - -impl Directive { - /// Parse status string to DirectiveStatus enum - pub fn status_enum(&self) -> Result<DirectiveStatus, String> { - self.status.parse() - } -} - -/// Directive chain - a generated execution plan (DAG) for a directive -#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct DirectiveChain { - pub id: Uuid, - pub directive_id: Uuid, - pub generation: i32, - pub name: String, - pub description: Option<String>, - pub rationale: Option<String>, - pub planning_model: Option<String>, - pub status: String, - pub total_steps: i32, - pub completed_steps: i32, - pub failed_steps: i32, - pub current_confidence: Option<f64>, - pub started_at: Option<DateTime<Utc>>, - pub completed_at: Option<DateTime<Utc>>, - pub version: i32, - pub created_at: DateTime<Utc>, - pub updated_at: DateTime<Utc>, -} - -/// Chain step status -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "snake_case")] -pub enum StepStatus { - Pending, - Ready, - Running, - Evaluating, - Passed, - Failed, - Rework, - Skipped, - Blocked, -} - -impl Default for StepStatus { - fn default() -> Self { - StepStatus::Pending - } -} - -impl std::fmt::Display for StepStatus { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - StepStatus::Pending => write!(f, "pending"), - StepStatus::Ready => write!(f, "ready"), - StepStatus::Running => write!(f, "running"), - StepStatus::Evaluating => write!(f, "evaluating"), - StepStatus::Passed => write!(f, "passed"), - StepStatus::Failed => write!(f, "failed"), - StepStatus::Rework => write!(f, "rework"), - StepStatus::Skipped => write!(f, "skipped"), - StepStatus::Blocked => write!(f, "blocked"), - } - } -} - -impl std::str::FromStr for StepStatus { - type Err = String; - fn from_str(s: &str) -> Result<Self, Self::Err> { - match s.to_lowercase().as_str() { - "pending" => Ok(StepStatus::Pending), - "ready" => Ok(StepStatus::Ready), - "running" => Ok(StepStatus::Running), - "evaluating" => Ok(StepStatus::Evaluating), - "passed" => Ok(StepStatus::Passed), - "failed" => Ok(StepStatus::Failed), - "rework" => Ok(StepStatus::Rework), - "skipped" => Ok(StepStatus::Skipped), - "blocked" => Ok(StepStatus::Blocked), - _ => Err(format!("Invalid step status: {}", s)), - } - } -} - -/// Chain step - a node in the DAG execution plan -#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct ChainStep { - pub id: Uuid, - pub chain_id: Uuid, - pub name: String, - pub description: Option<String>, - pub step_type: String, - pub contract_type: String, - pub initial_phase: Option<String>, - pub task_plan: Option<String>, - #[sqlx(default)] - pub phases: Vec<String>, - #[sqlx(default)] - pub depends_on: Vec<Uuid>, - pub parallel_group: Option<String>, - #[sqlx(default)] - pub requirement_ids: Vec<String>, - #[sqlx(default)] - pub acceptance_criteria_ids: Vec<String>, - #[sqlx(json)] - #[serde(default)] - pub verifier_config: serde_json::Value, - pub status: String, - pub contract_id: Option<Uuid>, - pub supervisor_task_id: Option<Uuid>, - pub confidence_score: Option<f64>, - pub confidence_level: Option<String>, - pub evaluation_count: i32, - pub rework_count: i32, - pub last_evaluation_id: Option<Uuid>, - pub editor_x: Option<f64>, - pub editor_y: Option<f64>, - pub order_index: i32, - pub started_at: Option<DateTime<Utc>>, - pub completed_at: Option<DateTime<Utc>>, - pub created_at: DateTime<Utc>, -} - -impl ChainStep { - /// Parse status string to StepStatus enum - pub fn status_enum(&self) -> Result<StepStatus, String> { - self.status.parse() - } -} - -/// Confidence level (traffic light) -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "snake_case")] -pub enum ConfidenceLevel { - Green, - Yellow, - Red, -} - -impl ConfidenceLevel { - pub fn from_score(score: f64, green_threshold: f64, yellow_threshold: f64) -> Self { - if score >= green_threshold { - Self::Green - } else if score >= yellow_threshold { - Self::Yellow - } else { - Self::Red - } - } -} - -impl std::fmt::Display for ConfidenceLevel { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ConfidenceLevel::Green => write!(f, "green"), - ConfidenceLevel::Yellow => write!(f, "yellow"), - ConfidenceLevel::Red => write!(f, "red"), - } - } -} - -/// Directive evaluation - composite programmatic + LLM evaluation result -#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct DirectiveEvaluation { - pub id: Uuid, - pub directive_id: Uuid, - pub chain_id: Option<Uuid>, - pub step_id: Option<Uuid>, - pub contract_id: Option<Uuid>, - pub evaluation_type: String, - pub evaluation_number: i32, - pub evaluator: Option<String>, - pub passed: bool, - pub overall_score: Option<f64>, - pub confidence_level: Option<String>, - #[sqlx(json)] - #[serde(default)] - pub programmatic_results: serde_json::Value, - #[sqlx(json)] - #[serde(default)] - pub llm_results: serde_json::Value, - #[sqlx(json)] - #[serde(default)] - pub criteria_results: serde_json::Value, - pub summary_feedback: String, - pub rework_instructions: Option<String>, - #[sqlx(json)] - pub directive_snapshot: Option<serde_json::Value>, - #[sqlx(json)] - pub deliverables_snapshot: Option<serde_json::Value>, - pub started_at: DateTime<Utc>, - pub completed_at: Option<DateTime<Utc>>, - pub created_at: DateTime<Utc>, -} - -/// Directive event - audit stream entry -#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct DirectiveEvent { - pub id: Uuid, - pub directive_id: Uuid, - pub chain_id: Option<Uuid>, - pub step_id: Option<Uuid>, - pub event_type: String, - pub severity: String, - #[sqlx(json)] - pub event_data: Option<serde_json::Value>, - pub actor_type: String, - pub actor_id: Option<Uuid>, - pub created_at: DateTime<Utc>, -} - -/// Directive verifier - pluggable verification configuration -#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct DirectiveVerifier { - pub id: Uuid, - pub directive_id: Uuid, - pub name: String, - pub verifier_type: String, - pub command: Option<String>, - pub working_directory: Option<String>, - pub timeout_seconds: Option<i32>, - #[sqlx(json)] - #[serde(default)] - pub environment: serde_json::Value, - pub auto_detect: bool, - #[sqlx(default)] - pub detect_files: Vec<String>, - pub weight: f64, - pub required: bool, - pub enabled: bool, - pub last_run_at: Option<DateTime<Utc>>, - #[sqlx(json)] - pub last_result: Option<serde_json::Value>, - pub created_at: DateTime<Utc>, - pub updated_at: DateTime<Utc>, -} - -/// Directive approval - human-in-the-loop gate -#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct DirectiveApproval { - pub id: Uuid, - pub directive_id: Uuid, - pub step_id: Option<Uuid>, - pub approval_type: String, - pub description: String, - #[sqlx(json)] - pub context: Option<serde_json::Value>, - pub urgency: String, - pub status: String, - pub response: Option<String>, - pub responded_by: Option<Uuid>, - pub responded_at: Option<DateTime<Utc>>, - pub expires_at: Option<DateTime<Utc>>, - pub created_at: DateTime<Utc>, -} - -// ============================================================================= -// Directive Request/Response Types -// ============================================================================= - -/// Request to create a directive from a goal -#[derive(Debug, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct CreateDirectiveRequest { - pub goal: String, - pub title: Option<String>, - pub repository_url: Option<String>, - pub local_path: Option<String>, - pub base_branch: Option<String>, - pub autonomy_level: Option<String>, - pub requirements: Option<serde_json::Value>, - pub acceptance_criteria: Option<serde_json::Value>, - pub confidence_threshold_green: Option<f64>, - pub confidence_threshold_yellow: Option<f64>, - pub max_total_cost_usd: Option<f64>, - pub max_wall_time_minutes: Option<i32>, -} - -/// Request to update a directive -#[derive(Debug, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct UpdateDirectiveRequest { - pub title: Option<String>, - pub goal: Option<String>, - pub requirements: Option<serde_json::Value>, - pub acceptance_criteria: Option<serde_json::Value>, - pub constraints: Option<serde_json::Value>, - pub external_dependencies: Option<serde_json::Value>, - pub autonomy_level: Option<String>, - pub confidence_threshold_green: Option<f64>, - pub confidence_threshold_yellow: Option<f64>, - pub max_total_cost_usd: Option<f64>, - pub max_wall_time_minutes: Option<i32>, - pub max_rework_cycles: Option<i32>, - pub max_chain_regenerations: Option<i32>, - pub version: i32, -} - -/// Directive summary for list views -#[derive(Debug, Clone, Serialize, FromRow, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct DirectiveSummary { - pub id: Uuid, - pub title: String, - pub goal: String, - pub status: String, - pub autonomy_level: String, - pub current_confidence: Option<f64>, - pub completed_steps: i32, - pub total_steps: i32, - pub chain_generation_count: i32, - pub started_at: Option<DateTime<Utc>>, - pub created_at: DateTime<Utc>, -} - -/// Directive with progress, chain, events, and approvals -#[derive(Debug, Serialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct DirectiveWithProgress { - #[serde(flatten)] - pub directive: Directive, - pub chain: Option<DirectiveChain>, - pub steps: Vec<ChainStep>, - pub recent_events: Vec<DirectiveEvent>, - pub pending_approvals: Vec<DirectiveApproval>, -} - -/// Request to add a step to a chain -#[derive(Debug, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct AddStepRequest { - pub name: String, - pub description: Option<String>, - pub step_type: Option<String>, - pub contract_type: Option<String>, - pub initial_phase: Option<String>, - pub task_plan: Option<String>, - pub phases: Option<Vec<String>>, - pub depends_on: Option<Vec<Uuid>>, - pub parallel_group: Option<String>, - pub requirement_ids: Option<Vec<String>>, - pub acceptance_criteria_ids: Option<Vec<String>>, - pub verifier_config: Option<serde_json::Value>, - pub editor_x: Option<f64>, - pub editor_y: Option<f64>, -} - -/// Request to update a step -#[derive(Debug, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct UpdateStepRequest { - pub name: Option<String>, - pub description: Option<String>, - pub task_plan: Option<String>, - pub depends_on: Option<Vec<Uuid>>, - pub requirement_ids: Option<Vec<String>>, - pub acceptance_criteria_ids: Option<Vec<String>>, - pub verifier_config: Option<serde_json::Value>, - pub editor_x: Option<f64>, - pub editor_y: Option<f64>, -} - -/// Chain graph response for DAG visualization -#[derive(Debug, Serialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct DirectiveChainGraphResponse { - pub chain_id: Uuid, - pub directive_id: Uuid, - pub nodes: Vec<DirectiveChainGraphNode>, - pub edges: Vec<DirectiveChainGraphEdge>, -} - -/// Node in directive chain graph -#[derive(Debug, Serialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct DirectiveChainGraphNode { - pub id: Uuid, - pub name: String, - pub step_type: String, - pub status: String, - pub confidence_score: Option<f64>, - pub confidence_level: Option<String>, - pub contract_id: Option<Uuid>, - pub editor_x: Option<f64>, - pub editor_y: Option<f64>, -} - -/// Edge in directive chain graph -#[derive(Debug, Serialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct DirectiveChainGraphEdge { - pub source: Uuid, - pub target: Uuid, -} - -/// Start directive response -#[derive(Debug, Serialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct StartDirectiveResponse { - pub directive_id: Uuid, - pub chain_id: Uuid, - pub chain_generation: i32, - pub steps: Vec<ChainStep>, - pub status: String, -} - -/// Request to create a verifier -#[derive(Debug, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct CreateVerifierRequest { - pub name: String, - pub verifier_type: String, - pub command: Option<String>, - pub working_directory: Option<String>, - pub timeout_seconds: Option<i32>, - pub environment: Option<serde_json::Value>, - pub weight: Option<f64>, - pub required: Option<bool>, -} - -/// Request to update a verifier -#[derive(Debug, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct UpdateVerifierRequest { - pub name: Option<String>, - pub command: Option<String>, - pub working_directory: Option<String>, - pub timeout_seconds: Option<i32>, - pub weight: Option<f64>, - pub required: Option<bool>, - pub enabled: Option<bool>, -} - -/// Approval action request -#[derive(Debug, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct ApprovalActionRequest { - pub response: Option<String>, -} - -/// Request to update directive requirements -#[derive(Debug, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct UpdateRequirementsRequest { - pub requirements: Vec<DirectiveRequirement>, -} - -/// Request to update directive acceptance criteria -#[derive(Debug, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct UpdateCriteriaRequest { - pub acceptance_criteria: Vec<DirectiveAcceptanceCriterion>, -} - -/// Request to trigger step rework -#[derive(Debug, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct ReworkStepRequest { - pub instructions: Option<String>, -} - -/// Directive requirement (shared type used in directive specification) -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct DirectiveRequirement { - pub id: String, - pub title: String, - pub description: String, - pub priority: String, - pub category: Option<String>, - #[serde(skip_serializing_if = "Option::is_none")] - pub parent_id: Option<String>, -} - -/// Directive acceptance criterion -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct DirectiveAcceptanceCriterion { - pub id: String, - #[serde(default)] - pub requirement_ids: Vec<String>, - pub description: String, - #[serde(default = "default_true")] - pub testable: bool, - pub verification_method: Option<String>, -} - -fn default_true() -> bool { - true -} - -// Old chain types (Chain, ChainContract, ChainContractDefinition, ChainDirective, -// ContractEvaluation, ChainEvent, ChainRepository, etc.) have been replaced by -// the directive system above: Directive, DirectiveChain, ChainStep, -// DirectiveEvaluation, DirectiveEvent, DirectiveVerifier, DirectiveApproval. - -// Legacy types kept temporarily for chain runner/parser compatibility during migration. -// These will be removed once the chain daemon module is replaced. - -/// Request payload for creating a new chain (legacy - used by chain runner) -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CreateChainRequest { - pub name: String, - pub description: Option<String>, - pub repository_url: Option<String>, - pub repositories: Option<Vec<AddChainRepositoryRequest>>, - pub loop_enabled: Option<bool>, - pub loop_max_iterations: Option<i32>, - pub loop_progress_check: Option<String>, - pub contracts: Option<Vec<CreateChainContractRequest>>, -} - -/// Request to add a repository to a chain (legacy - used by chain runner) -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AddChainRepositoryRequest { - pub name: String, - pub repository_url: Option<String>, - pub local_path: Option<String>, - #[serde(default = "default_source_type")] - pub source_type: String, - #[serde(default)] - pub is_primary: bool, -} - -fn default_source_type() -> String { - "remote".to_string() -} - -/// Request to create a contract within a chain (legacy - used by chain runner) -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CreateChainContractRequest { - pub name: String, - pub description: Option<String>, - #[serde(default)] - pub contract_type: Option<String>, - pub initial_phase: Option<String>, - pub phases: Option<Vec<String>>, - pub depends_on: Option<Vec<String>>, - pub tasks: Option<Vec<CreateChainTaskRequest>>, - pub deliverables: Option<Vec<CreateChainDeliverableRequest>>, - pub editor_x: Option<f64>, - pub editor_y: Option<f64>, -} - -/// Task definition within a chain contract (legacy - used by chain runner) -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CreateChainTaskRequest { - pub name: String, - pub plan: String, -} - -/// Deliverable definition within a chain contract (legacy - used by chain runner) -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CreateChainDeliverableRequest { - pub id: String, - pub name: String, - pub priority: Option<String>, -} - -// ============================================================================= // Unit Tests // ============================================================================= diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs index cd806f0..863d927 100644 --- a/makima/src/db/repository.rs +++ b/makima/src/db/repository.rs @@ -6,7 +6,6 @@ use sqlx::PgPool; use uuid::Uuid; use super::models::{ - // Core types CheckpointPatch, CheckpointPatchInfo, Contract, ContractChatConversation, ContractChatMessageRecord, ContractEvent, ContractRepository, ContractSummary, ContractTypeTemplateRecord, ConversationMessage, ConversationSnapshot, @@ -17,11 +16,6 @@ use super::models::{ PhaseDefinition, SupervisorHeartbeatRecord, SupervisorState, Task, TaskCheckpoint, TaskEvent, TaskSummary, UpdateContractRequest, UpdateFileRequest, UpdateTaskRequest, UpdateTemplateRequest, - // Directive types - AddStepRequest, ChainStep, CreateDirectiveRequest, Directive, DirectiveApproval, - DirectiveChain, DirectiveChainGraphEdge, DirectiveChainGraphNode, DirectiveChainGraphResponse, - DirectiveEvaluation, DirectiveEvent, DirectiveSummary, DirectiveVerifier, - DirectiveWithProgress, UpdateDirectiveRequest, UpdateStepRequest, }; /// Repository error types. @@ -4905,1169 +4899,6 @@ pub async fn sync_supervisor_state( } // ============================================================================= -// Directive Operations (top-level orchestration entity) -// ============================================================================= -// TODO: Implement directive CRUD functions -// - create_directive_for_owner -// - get_directive_for_owner -// - list_directives_for_owner -// - update_directive_for_owner -// - archive_directive_for_owner -// - update_directive_status - -// ============================================================================= -// Directive Chain Operations (generated execution plans) -// ============================================================================= -// TODO: Implement chain CRUD functions -// - create_directive_chain -// - get_current_chain -// - supersede_chain - -// ============================================================================= -// Chain Step Operations (nodes in the DAG) -// ============================================================================= -// TODO: Implement step CRUD functions -// - create_chain_step -// - update_chain_step -// - delete_chain_step -// - find_ready_steps -// - update_step_status -// - update_step_contract -// - update_step_confidence -// - increment_step_rework_count - -// ============================================================================= -// Directive Evaluation Operations -// ============================================================================= -// TODO: Implement evaluation functions -// - create_directive_evaluation -// - list_step_evaluations -// - list_directive_evaluations - -// ============================================================================= -// Directive Event Operations (audit stream) -// ============================================================================= -// TODO: Implement event functions -// - emit_directive_event -// - list_directive_events - -// ============================================================================= -// Directive Verifier Operations -// ============================================================================= -// TODO: Implement verifier CRUD functions -// - create_directive_verifier -// - list_directive_verifiers -// - update_directive_verifier - -// ============================================================================= -// Directive Approval Operations (human-in-the-loop) -// ============================================================================= -// TODO: Implement approval functions -// - create_approval_request -// - resolve_approval -// - list_pending_approvals - -// NOTE: Old chain functions removed. See git history for reference. -// Old functions included: create_chain_for_owner, get_chain_for_owner, -// list_chains_for_owner, update_chain_for_owner, delete_chain_for_owner, -// add_contract_to_chain, remove_contract_from_chain, list_chain_contracts, -// get_chain_with_contracts, list_chain_repositories, add_chain_repository, -// delete_chain_repository, set_chain_repository_primary, get_chain_graph, -// record_chain_event, list_chain_events, increment_chain_loop, complete_chain, -// get_ready_chain_contracts, is_chain_complete, get_chain_editor_data, -// create_chain_contract_definition, list_chain_contract_definitions, -// update_chain_contract_definition, delete_chain_contract_definition, -// get_chain_definition_graph, update_chain_status, progress_chain, -// create_chain_directive, get_chain_directive, update_chain_directive, -// delete_chain_directive, create_contract_evaluation, get_contract_evaluation, -// list_chain_evaluations, update_chain_contract_evaluation_status, -// mark_chain_contract_original_completion, get_chain_contract_by_contract_id, -// init_chain_for_owner. - -// ============================================================================= -// Directive Operations -// ============================================================================= - -/// Create a new directive for an owner. -pub async fn create_directive_for_owner( - pool: &PgPool, - owner_id: Uuid, - req: CreateDirectiveRequest, -) -> Result<Directive, sqlx::Error> { - let title = req.title.unwrap_or_else(|| truncate_string(&req.goal, 100)); - let autonomy_level = req.autonomy_level.unwrap_or_else(|| "guardrails".to_string()); - let green_threshold = req.confidence_threshold_green.unwrap_or(0.85); - let yellow_threshold = req.confidence_threshold_yellow.unwrap_or(0.60); - let requirements = req.requirements.unwrap_or(serde_json::json!([])); - let acceptance_criteria = req.acceptance_criteria.unwrap_or(serde_json::json!([])); - - sqlx::query_as::<_, Directive>( - r#" - INSERT INTO directives ( - owner_id, title, goal, requirements, acceptance_criteria, - autonomy_level, confidence_threshold_green, confidence_threshold_yellow, - repository_url, local_path, base_branch, - max_total_cost_usd, max_wall_time_minutes - ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) - RETURNING * - "#, - ) - .bind(owner_id) - .bind(&title) - .bind(&req.goal) - .bind(&requirements) - .bind(&acceptance_criteria) - .bind(&autonomy_level) - .bind(green_threshold) - .bind(yellow_threshold) - .bind(&req.repository_url) - .bind(&req.local_path) - .bind(&req.base_branch) - .bind(req.max_total_cost_usd) - .bind(req.max_wall_time_minutes) - .fetch_one(pool) - .await -} - -/// Get a directive by ID, scoped to owner. -pub async fn get_directive_for_owner( - pool: &PgPool, - id: Uuid, - owner_id: Uuid, -) -> Result<Option<Directive>, sqlx::Error> { - sqlx::query_as::<_, Directive>( - r#" - SELECT * FROM directives WHERE id = $1 AND owner_id = $2 - "#, - ) - .bind(id) - .bind(owner_id) - .fetch_optional(pool) - .await -} - -/// Get a directive by ID (no owner check - for internal use). -pub async fn get_directive(pool: &PgPool, id: Uuid) -> Result<Option<Directive>, sqlx::Error> { - sqlx::query_as::<_, Directive>( - r#"SELECT * FROM directives WHERE id = $1"#, - ) - .bind(id) - .fetch_optional(pool) - .await -} - -/// List directives for an owner. -pub async fn list_directives_for_owner( - pool: &PgPool, - owner_id: Uuid, - status_filter: Option<&str>, -) -> Result<Vec<DirectiveSummary>, sqlx::Error> { - let query = if let Some(status) = status_filter { - sqlx::query_as::<_, DirectiveSummary>( - r#" - SELECT - d.id, d.title, d.goal, d.status, d.autonomy_level, - dc.current_confidence, - COALESCE(dc.completed_steps, 0) as completed_steps, - COALESCE(dc.total_steps, 0) as total_steps, - d.chain_generation_count, d.started_at, d.created_at - FROM directives d - LEFT JOIN directive_chains dc ON dc.id = d.current_chain_id - WHERE d.owner_id = $1 AND d.status = $2 - ORDER BY d.created_at DESC - "#, - ) - .bind(owner_id) - .bind(status) - } else { - sqlx::query_as::<_, DirectiveSummary>( - r#" - SELECT - d.id, d.title, d.goal, d.status, d.autonomy_level, - dc.current_confidence, - COALESCE(dc.completed_steps, 0) as completed_steps, - COALESCE(dc.total_steps, 0) as total_steps, - d.chain_generation_count, d.started_at, d.created_at - FROM directives d - LEFT JOIN directive_chains dc ON dc.id = d.current_chain_id - WHERE d.owner_id = $1 - ORDER BY d.created_at DESC - "#, - ) - .bind(owner_id) - }; - query.fetch_all(pool).await -} - -/// Update a directive with optimistic locking. -pub async fn update_directive_for_owner( - pool: &PgPool, - id: Uuid, - owner_id: Uuid, - req: UpdateDirectiveRequest, -) -> Result<Directive, RepositoryError> { - // First get current version - let current = sqlx::query_scalar::<_, i32>( - "SELECT version FROM directives WHERE id = $1 AND owner_id = $2" - ) - .bind(id) - .bind(owner_id) - .fetch_optional(pool) - .await? - .ok_or_else(|| RepositoryError::Database(sqlx::Error::RowNotFound))?; - - if current != req.version { - return Err(RepositoryError::VersionConflict { - expected: req.version, - actual: current, - }); - } - - let directive = sqlx::query_as::<_, Directive>( - r#" - UPDATE directives SET - title = COALESCE($3, title), - goal = COALESCE($4, goal), - requirements = COALESCE($5, requirements), - acceptance_criteria = COALESCE($6, acceptance_criteria), - constraints = COALESCE($7, constraints), - external_dependencies = COALESCE($8, external_dependencies), - autonomy_level = COALESCE($9, autonomy_level), - confidence_threshold_green = COALESCE($10, confidence_threshold_green), - confidence_threshold_yellow = COALESCE($11, confidence_threshold_yellow), - max_total_cost_usd = COALESCE($12, max_total_cost_usd), - max_wall_time_minutes = COALESCE($13, max_wall_time_minutes), - max_rework_cycles = COALESCE($14, max_rework_cycles), - max_chain_regenerations = COALESCE($15, max_chain_regenerations), - version = version + 1, - updated_at = NOW() - WHERE id = $1 AND owner_id = $2 AND version = $16 - RETURNING * - "#, - ) - .bind(id) - .bind(owner_id) - .bind(&req.title) - .bind(&req.goal) - .bind(&req.requirements) - .bind(&req.acceptance_criteria) - .bind(&req.constraints) - .bind(&req.external_dependencies) - .bind(&req.autonomy_level) - .bind(req.confidence_threshold_green) - .bind(req.confidence_threshold_yellow) - .bind(req.max_total_cost_usd) - .bind(req.max_wall_time_minutes) - .bind(req.max_rework_cycles) - .bind(req.max_chain_regenerations) - .bind(req.version) - .fetch_one(pool) - .await?; - - Ok(directive) -} - -/// Update directive status. -pub async fn update_directive_status( - pool: &PgPool, - id: Uuid, - status: &str, -) -> Result<Directive, sqlx::Error> { - sqlx::query_as::<_, Directive>( - r#" - UPDATE directives SET - status = $2, - started_at = CASE WHEN $2 = 'active' AND started_at IS NULL THEN NOW() ELSE started_at END, - completed_at = CASE WHEN $2 IN ('completed', 'failed', 'archived') THEN NOW() ELSE completed_at END, - updated_at = NOW() - WHERE id = $1 - RETURNING * - "#, - ) - .bind(id) - .bind(status) - .fetch_one(pool) - .await -} - -/// Set the orchestrator contract ID for a directive. -pub async fn set_directive_orchestrator_contract( - pool: &PgPool, - directive_id: Uuid, - contract_id: Uuid, -) -> Result<(), sqlx::Error> { - sqlx::query( - r#" - UPDATE directives SET orchestrator_contract_id = $2, updated_at = NOW() - WHERE id = $1 - "#, - ) - .bind(directive_id) - .bind(contract_id) - .execute(pool) - .await?; - Ok(()) -} - -/// Find a directive by its orchestrator contract ID. -pub async fn get_directive_by_orchestrator_contract_id( - pool: &PgPool, - contract_id: Uuid, -) -> Result<Option<Directive>, sqlx::Error> { - sqlx::query_as::<_, Directive>( - r#" - SELECT * FROM directives WHERE orchestrator_contract_id = $1 - "#, - ) - .bind(contract_id) - .fetch_optional(pool) - .await -} - -/// Archive a directive (soft delete). -pub async fn archive_directive_for_owner( - pool: &PgPool, - id: Uuid, - owner_id: Uuid, -) -> Result<bool, sqlx::Error> { - let result = sqlx::query( - r#" - UPDATE directives SET status = 'archived', updated_at = NOW() - WHERE id = $1 AND owner_id = $2 - "#, - ) - .bind(id) - .bind(owner_id) - .execute(pool) - .await?; - Ok(result.rows_affected() > 0) -} - -/// Get directive with full progress info. -pub async fn get_directive_with_progress( - pool: &PgPool, - id: Uuid, - owner_id: Uuid, -) -> Result<Option<DirectiveWithProgress>, sqlx::Error> { - let directive = match get_directive_for_owner(pool, id, owner_id).await? { - Some(d) => d, - None => return Ok(None), - }; - - let chain = if let Some(chain_id) = directive.current_chain_id { - get_directive_chain(pool, chain_id).await? - } else { - None - }; - - let steps = if let Some(ref c) = chain { - list_chain_steps(pool, c.id).await? - } else { - vec![] - }; - - let recent_events = list_directive_events(pool, id, Some(20)).await?; - let pending_approvals = list_pending_approvals(pool, id).await?; - - Ok(Some(DirectiveWithProgress { - directive, - chain, - steps, - recent_events, - pending_approvals, - })) -} - -// ============================================================================= -// Directive Chain Operations -// ============================================================================= - -/// Create a new chain generation for a directive. -pub async fn create_directive_chain( - pool: &PgPool, - directive_id: Uuid, - name: &str, - description: Option<&str>, - rationale: Option<&str>, - planning_model: Option<&str>, -) -> Result<DirectiveChain, sqlx::Error> { - // Get next generation number - let generation = sqlx::query_scalar::<_, i32>( - "SELECT COALESCE(MAX(generation), 0) + 1 FROM directive_chains WHERE directive_id = $1" - ) - .bind(directive_id) - .fetch_one(pool) - .await?; - - let chain = sqlx::query_as::<_, DirectiveChain>( - r#" - INSERT INTO directive_chains (directive_id, generation, name, description, rationale, planning_model) - VALUES ($1, $2, $3, $4, $5, $6) - RETURNING * - "#, - ) - .bind(directive_id) - .bind(generation) - .bind(name) - .bind(description) - .bind(rationale) - .bind(planning_model) - .fetch_one(pool) - .await?; - - // Update directive to point to new chain and increment generation count - sqlx::query( - r#" - UPDATE directives SET - current_chain_id = $2, - chain_generation_count = chain_generation_count + 1, - updated_at = NOW() - WHERE id = $1 - "#, - ) - .bind(directive_id) - .bind(chain.id) - .execute(pool) - .await?; - - Ok(chain) -} - -/// Get a directive chain by ID. -pub async fn get_directive_chain(pool: &PgPool, id: Uuid) -> Result<Option<DirectiveChain>, sqlx::Error> { - sqlx::query_as::<_, DirectiveChain>( - "SELECT * FROM directive_chains WHERE id = $1" - ) - .bind(id) - .fetch_optional(pool) - .await -} - -/// Get the current chain for a directive. -pub async fn get_current_chain(pool: &PgPool, directive_id: Uuid) -> Result<Option<DirectiveChain>, sqlx::Error> { - sqlx::query_as::<_, DirectiveChain>( - r#" - SELECT dc.* FROM directive_chains dc - JOIN directives d ON d.current_chain_id = dc.id - WHERE d.id = $1 - "#, - ) - .bind(directive_id) - .fetch_optional(pool) - .await -} - -/// Update chain status. -pub async fn update_chain_status( - pool: &PgPool, - chain_id: Uuid, - status: &str, -) -> Result<DirectiveChain, sqlx::Error> { - sqlx::query_as::<_, DirectiveChain>( - r#" - UPDATE directive_chains SET - status = $2, - started_at = CASE WHEN $2 = 'active' AND started_at IS NULL THEN NOW() ELSE started_at END, - completed_at = CASE WHEN $2 IN ('completed', 'failed', 'superseded') THEN NOW() ELSE completed_at END, - updated_at = NOW() - WHERE id = $1 - RETURNING * - "#, - ) - .bind(chain_id) - .bind(status) - .fetch_one(pool) - .await -} - -/// Supersede a chain (mark as superseded and update directive). -pub async fn supersede_chain(pool: &PgPool, chain_id: Uuid) -> Result<(), sqlx::Error> { - sqlx::query( - r#" - UPDATE directive_chains SET status = 'superseded', completed_at = NOW(), updated_at = NOW() - WHERE id = $1 - "#, - ) - .bind(chain_id) - .execute(pool) - .await?; - Ok(()) -} - -// ============================================================================= -// Chain Step Operations -// ============================================================================= - -/// Create a new step in a chain. -pub async fn create_chain_step( - pool: &PgPool, - chain_id: Uuid, - req: AddStepRequest, -) -> Result<ChainStep, sqlx::Error> { - let step_type = req.step_type.unwrap_or_else(|| "execute".to_string()); - let contract_type = req.contract_type.unwrap_or_else(|| "simple".to_string()); - let phases = req.phases.unwrap_or_default(); - let depends_on = req.depends_on.unwrap_or_default(); - let requirement_ids = req.requirement_ids.unwrap_or_default(); - let acceptance_criteria_ids = req.acceptance_criteria_ids.unwrap_or_default(); - let verifier_config = req.verifier_config.unwrap_or(serde_json::json!({})); - - // Get next order index - let order_index = sqlx::query_scalar::<_, i32>( - "SELECT COALESCE(MAX(order_index), 0) + 1 FROM chain_steps WHERE chain_id = $1" - ) - .bind(chain_id) - .fetch_one(pool) - .await?; - - let step = sqlx::query_as::<_, ChainStep>( - r#" - INSERT INTO chain_steps ( - chain_id, name, description, step_type, contract_type, - initial_phase, task_plan, phases, depends_on, parallel_group, - requirement_ids, acceptance_criteria_ids, verifier_config, - editor_x, editor_y, order_index - ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) - RETURNING * - "#, - ) - .bind(chain_id) - .bind(&req.name) - .bind(&req.description) - .bind(&step_type) - .bind(&contract_type) - .bind(&req.initial_phase) - .bind(&req.task_plan) - .bind(&phases) - .bind(&depends_on) - .bind(&req.parallel_group) - .bind(&requirement_ids) - .bind(&acceptance_criteria_ids) - .bind(&verifier_config) - .bind(req.editor_x.unwrap_or(0.0)) - .bind(req.editor_y.unwrap_or(0.0)) - .bind(order_index) - .fetch_one(pool) - .await?; - - // Update chain total_steps count - sqlx::query( - "UPDATE directive_chains SET total_steps = total_steps + 1, updated_at = NOW() WHERE id = $1" - ) - .bind(chain_id) - .execute(pool) - .await?; - - Ok(step) -} - -/// Get a chain step by ID. -pub async fn get_chain_step(pool: &PgPool, id: Uuid) -> Result<Option<ChainStep>, sqlx::Error> { - sqlx::query_as::<_, ChainStep>( - "SELECT * FROM chain_steps WHERE id = $1" - ) - .bind(id) - .fetch_optional(pool) - .await -} - -/// List all steps in a chain. -pub async fn list_chain_steps(pool: &PgPool, chain_id: Uuid) -> Result<Vec<ChainStep>, sqlx::Error> { - sqlx::query_as::<_, ChainStep>( - "SELECT * FROM chain_steps WHERE chain_id = $1 ORDER BY order_index" - ) - .bind(chain_id) - .fetch_all(pool) - .await -} - -/// Update a chain step. -pub async fn update_chain_step( - pool: &PgPool, - step_id: Uuid, - req: UpdateStepRequest, -) -> Result<ChainStep, sqlx::Error> { - sqlx::query_as::<_, ChainStep>( - r#" - UPDATE chain_steps SET - name = COALESCE($2, name), - description = COALESCE($3, description), - task_plan = COALESCE($4, task_plan), - depends_on = COALESCE($5, depends_on), - requirement_ids = COALESCE($6, requirement_ids), - acceptance_criteria_ids = COALESCE($7, acceptance_criteria_ids), - verifier_config = COALESCE($8, verifier_config), - editor_x = COALESCE($9, editor_x), - editor_y = COALESCE($10, editor_y) - WHERE id = $1 - RETURNING * - "#, - ) - .bind(step_id) - .bind(&req.name) - .bind(&req.description) - .bind(&req.task_plan) - .bind(&req.depends_on) - .bind(&req.requirement_ids) - .bind(&req.acceptance_criteria_ids) - .bind(&req.verifier_config) - .bind(req.editor_x) - .bind(req.editor_y) - .fetch_one(pool) - .await -} - -/// Delete a chain step. -pub async fn delete_chain_step(pool: &PgPool, step_id: Uuid) -> Result<bool, sqlx::Error> { - // Get chain_id first for updating count - let chain_id = sqlx::query_scalar::<_, Uuid>( - "SELECT chain_id FROM chain_steps WHERE id = $1" - ) - .bind(step_id) - .fetch_optional(pool) - .await?; - - let result = sqlx::query("DELETE FROM chain_steps WHERE id = $1") - .bind(step_id) - .execute(pool) - .await?; - - // Update chain total_steps count - if let Some(cid) = chain_id { - sqlx::query( - "UPDATE directive_chains SET total_steps = total_steps - 1, updated_at = NOW() WHERE id = $1" - ) - .bind(cid) - .execute(pool) - .await?; - } - - Ok(result.rows_affected() > 0) -} - -/// Find steps that are ready to execute (all dependencies met, status=pending). -pub async fn find_ready_steps(pool: &PgPool, chain_id: Uuid) -> Result<Vec<ChainStep>, sqlx::Error> { - sqlx::query_as::<_, ChainStep>( - r#" - SELECT s.* FROM chain_steps s - WHERE s.chain_id = $1 - AND s.status = 'pending' - AND NOT EXISTS ( - SELECT 1 FROM chain_steps dep - WHERE dep.id = ANY(s.depends_on) - AND dep.status NOT IN ('passed', 'skipped') - ) - ORDER BY s.order_index - "#, - ) - .bind(chain_id) - .fetch_all(pool) - .await -} - -/// Update step status. -pub async fn update_step_status( - pool: &PgPool, - step_id: Uuid, - status: &str, -) -> Result<ChainStep, sqlx::Error> { - let step = sqlx::query_as::<_, ChainStep>( - r#" - UPDATE chain_steps SET - status = $2, - started_at = CASE WHEN $2 = 'running' AND started_at IS NULL THEN NOW() ELSE started_at END, - completed_at = CASE WHEN $2 IN ('passed', 'failed', 'skipped') THEN NOW() ELSE completed_at END - WHERE id = $1 - RETURNING * - "#, - ) - .bind(step_id) - .bind(status) - .fetch_one(pool) - .await?; - - // Update chain completed_steps and failed_steps counts - if status == "passed" || status == "skipped" { - sqlx::query( - "UPDATE directive_chains SET completed_steps = completed_steps + 1, updated_at = NOW() WHERE id = $1" - ) - .bind(step.chain_id) - .execute(pool) - .await?; - } else if status == "failed" { - sqlx::query( - "UPDATE directive_chains SET failed_steps = failed_steps + 1, updated_at = NOW() WHERE id = $1" - ) - .bind(step.chain_id) - .execute(pool) - .await?; - } - - Ok(step) -} - -/// Link a step to a contract. -pub async fn update_step_contract( - pool: &PgPool, - step_id: Uuid, - contract_id: Uuid, - supervisor_task_id: Option<Uuid>, -) -> Result<ChainStep, sqlx::Error> { - sqlx::query_as::<_, ChainStep>( - r#" - UPDATE chain_steps SET contract_id = $2, supervisor_task_id = $3 - WHERE id = $1 - RETURNING * - "#, - ) - .bind(step_id) - .bind(contract_id) - .bind(supervisor_task_id) - .fetch_one(pool) - .await -} - -/// Update step confidence score and level. -pub async fn update_step_confidence( - pool: &PgPool, - step_id: Uuid, - score: f64, - level: &str, - evaluation_id: Uuid, -) -> Result<ChainStep, sqlx::Error> { - sqlx::query_as::<_, ChainStep>( - r#" - UPDATE chain_steps SET - confidence_score = $2, - confidence_level = $3, - last_evaluation_id = $4, - evaluation_count = evaluation_count + 1 - WHERE id = $1 - RETURNING * - "#, - ) - .bind(step_id) - .bind(score) - .bind(level) - .bind(evaluation_id) - .fetch_one(pool) - .await -} - -/// Increment step rework count. -pub async fn increment_step_rework_count(pool: &PgPool, step_id: Uuid) -> Result<ChainStep, sqlx::Error> { - sqlx::query_as::<_, ChainStep>( - r#" - UPDATE chain_steps SET rework_count = rework_count + 1, status = 'rework' - WHERE id = $1 - RETURNING * - "#, - ) - .bind(step_id) - .fetch_one(pool) - .await -} - -/// Get chain graph for visualization. -pub async fn get_chain_graph( - pool: &PgPool, - chain_id: Uuid, -) -> Result<DirectiveChainGraphResponse, sqlx::Error> { - let chain = get_directive_chain(pool, chain_id).await? - .ok_or_else(|| sqlx::Error::RowNotFound)?; - - let steps = list_chain_steps(pool, chain_id).await?; - - let nodes: Vec<DirectiveChainGraphNode> = steps.iter().map(|s| { - DirectiveChainGraphNode { - id: s.id, - name: s.name.clone(), - step_type: s.step_type.clone(), - status: s.status.clone(), - confidence_score: s.confidence_score, - confidence_level: s.confidence_level.clone(), - contract_id: s.contract_id, - editor_x: s.editor_x, - editor_y: s.editor_y, - } - }).collect(); - - let mut edges = Vec::new(); - for step in &steps { - for dep_id in &step.depends_on { - edges.push(DirectiveChainGraphEdge { - source: *dep_id, - target: step.id, - }); - } - } - - Ok(DirectiveChainGraphResponse { - chain_id, - directive_id: chain.directive_id, - nodes, - edges, - }) -} - -// ============================================================================= -// Directive Evaluation Operations -// ============================================================================= - -/// Create a directive evaluation. -pub async fn create_directive_evaluation( - pool: &PgPool, - directive_id: Uuid, - chain_id: Option<Uuid>, - step_id: Option<Uuid>, - contract_id: Option<Uuid>, - evaluation_type: &str, - evaluator: Option<&str>, - passed: bool, - overall_score: Option<f64>, - confidence_level: Option<&str>, - programmatic_results: serde_json::Value, - llm_results: serde_json::Value, - criteria_results: serde_json::Value, - summary_feedback: &str, - rework_instructions: Option<&str>, -) -> Result<DirectiveEvaluation, sqlx::Error> { - // Get next evaluation number for this step/directive - let evaluation_number = if let Some(sid) = step_id { - sqlx::query_scalar::<_, i32>( - "SELECT COALESCE(MAX(evaluation_number), 0) + 1 FROM directive_evaluations WHERE step_id = $1" - ) - .bind(sid) - .fetch_one(pool) - .await? - } else { - sqlx::query_scalar::<_, i32>( - "SELECT COALESCE(MAX(evaluation_number), 0) + 1 FROM directive_evaluations WHERE directive_id = $1 AND step_id IS NULL" - ) - .bind(directive_id) - .fetch_one(pool) - .await? - }; - - sqlx::query_as::<_, DirectiveEvaluation>( - r#" - INSERT INTO directive_evaluations ( - directive_id, chain_id, step_id, contract_id, - evaluation_type, evaluation_number, evaluator, - passed, overall_score, confidence_level, - programmatic_results, llm_results, criteria_results, - summary_feedback, rework_instructions, - completed_at - ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, NOW()) - RETURNING * - "#, - ) - .bind(directive_id) - .bind(chain_id) - .bind(step_id) - .bind(contract_id) - .bind(evaluation_type) - .bind(evaluation_number) - .bind(evaluator) - .bind(passed) - .bind(overall_score) - .bind(confidence_level) - .bind(&programmatic_results) - .bind(&llm_results) - .bind(&criteria_results) - .bind(summary_feedback) - .bind(rework_instructions) - .fetch_one(pool) - .await -} - -/// List evaluations for a step. -pub async fn list_step_evaluations( - pool: &PgPool, - step_id: Uuid, -) -> Result<Vec<DirectiveEvaluation>, sqlx::Error> { - sqlx::query_as::<_, DirectiveEvaluation>( - "SELECT * FROM directive_evaluations WHERE step_id = $1 ORDER BY evaluation_number DESC" - ) - .bind(step_id) - .fetch_all(pool) - .await -} - -/// List evaluations for a directive. -pub async fn list_directive_evaluations( - pool: &PgPool, - directive_id: Uuid, - limit: Option<i64>, -) -> Result<Vec<DirectiveEvaluation>, sqlx::Error> { - let limit = limit.unwrap_or(100); - sqlx::query_as::<_, DirectiveEvaluation>( - "SELECT * FROM directive_evaluations WHERE directive_id = $1 ORDER BY created_at DESC LIMIT $2" - ) - .bind(directive_id) - .bind(limit) - .fetch_all(pool) - .await -} - -// ============================================================================= -// Directive Event Operations -// ============================================================================= - -/// Emit a directive event. -pub async fn emit_directive_event( - pool: &PgPool, - directive_id: Uuid, - chain_id: Option<Uuid>, - step_id: Option<Uuid>, - event_type: &str, - severity: &str, - event_data: Option<serde_json::Value>, - actor_type: &str, - actor_id: Option<Uuid>, -) -> Result<DirectiveEvent, sqlx::Error> { - sqlx::query_as::<_, DirectiveEvent>( - r#" - INSERT INTO directive_events ( - directive_id, chain_id, step_id, event_type, severity, event_data, actor_type, actor_id - ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8) - RETURNING * - "#, - ) - .bind(directive_id) - .bind(chain_id) - .bind(step_id) - .bind(event_type) - .bind(severity) - .bind(event_data) - .bind(actor_type) - .bind(actor_id) - .fetch_one(pool) - .await -} - -/// List directive events. -pub async fn list_directive_events( - pool: &PgPool, - directive_id: Uuid, - limit: Option<i64>, -) -> Result<Vec<DirectiveEvent>, sqlx::Error> { - let limit = limit.unwrap_or(100); - sqlx::query_as::<_, DirectiveEvent>( - "SELECT * FROM directive_events WHERE directive_id = $1 ORDER BY created_at DESC LIMIT $2" - ) - .bind(directive_id) - .bind(limit) - .fetch_all(pool) - .await -} - -// ============================================================================= -// Directive Verifier Operations -// ============================================================================= - -/// Create a directive verifier. -pub async fn create_directive_verifier( - pool: &PgPool, - directive_id: Uuid, - name: &str, - verifier_type: &str, - command: Option<&str>, - working_directory: Option<&str>, - auto_detect: bool, - detect_files: Vec<String>, - weight: f64, - required: bool, -) -> Result<DirectiveVerifier, sqlx::Error> { - sqlx::query_as::<_, DirectiveVerifier>( - r#" - INSERT INTO directive_verifiers ( - directive_id, name, verifier_type, command, working_directory, - auto_detect, detect_files, weight, required - ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) - RETURNING * - "#, - ) - .bind(directive_id) - .bind(name) - .bind(verifier_type) - .bind(command) - .bind(working_directory) - .bind(auto_detect) - .bind(&detect_files) - .bind(weight) - .bind(required) - .fetch_one(pool) - .await -} - -/// List verifiers for a directive. -pub async fn list_directive_verifiers( - pool: &PgPool, - directive_id: Uuid, -) -> Result<Vec<DirectiveVerifier>, sqlx::Error> { - sqlx::query_as::<_, DirectiveVerifier>( - "SELECT * FROM directive_verifiers WHERE directive_id = $1 ORDER BY name" - ) - .bind(directive_id) - .fetch_all(pool) - .await -} - -/// Update a directive verifier. -pub async fn update_directive_verifier( - pool: &PgPool, - verifier_id: Uuid, - enabled: Option<bool>, - command: Option<&str>, - weight: Option<f64>, - required: Option<bool>, -) -> Result<DirectiveVerifier, sqlx::Error> { - sqlx::query_as::<_, DirectiveVerifier>( - r#" - UPDATE directive_verifiers SET - enabled = COALESCE($2, enabled), - command = COALESCE($3, command), - weight = COALESCE($4, weight), - required = COALESCE($5, required), - updated_at = NOW() - WHERE id = $1 - RETURNING * - "#, - ) - .bind(verifier_id) - .bind(enabled) - .bind(command) - .bind(weight) - .bind(required) - .fetch_one(pool) - .await -} - -/// Update verifier last run result. -pub async fn update_verifier_result( - pool: &PgPool, - verifier_id: Uuid, - result: serde_json::Value, -) -> Result<DirectiveVerifier, sqlx::Error> { - sqlx::query_as::<_, DirectiveVerifier>( - r#" - UPDATE directive_verifiers SET last_run_at = NOW(), last_result = $2, updated_at = NOW() - WHERE id = $1 - RETURNING * - "#, - ) - .bind(verifier_id) - .bind(result) - .fetch_one(pool) - .await -} - -// ============================================================================= -// Directive Approval Operations -// ============================================================================= - -/// Create an approval request. -pub async fn create_approval_request( - pool: &PgPool, - directive_id: Uuid, - step_id: Option<Uuid>, - approval_type: &str, - description: &str, - context: Option<serde_json::Value>, - urgency: &str, - expires_at: Option<chrono::DateTime<Utc>>, -) -> Result<DirectiveApproval, sqlx::Error> { - sqlx::query_as::<_, DirectiveApproval>( - r#" - INSERT INTO directive_approvals ( - directive_id, step_id, approval_type, description, context, urgency, expires_at - ) - VALUES ($1, $2, $3, $4, $5, $6, $7) - RETURNING * - "#, - ) - .bind(directive_id) - .bind(step_id) - .bind(approval_type) - .bind(description) - .bind(context) - .bind(urgency) - .bind(expires_at) - .fetch_one(pool) - .await -} - -/// Resolve an approval request. -pub async fn resolve_approval( - pool: &PgPool, - approval_id: Uuid, - status: &str, - response: Option<&str>, - responded_by: Uuid, -) -> Result<DirectiveApproval, sqlx::Error> { - sqlx::query_as::<_, DirectiveApproval>( - r#" - UPDATE directive_approvals SET - status = $2, - response = $3, - responded_by = $4, - responded_at = NOW() - WHERE id = $1 - RETURNING * - "#, - ) - .bind(approval_id) - .bind(status) - .bind(response) - .bind(responded_by) - .fetch_one(pool) - .await -} - -/// List pending approvals for a directive. -pub async fn list_pending_approvals( - pool: &PgPool, - directive_id: Uuid, -) -> Result<Vec<DirectiveApproval>, sqlx::Error> { - sqlx::query_as::<_, DirectiveApproval>( - r#" - SELECT * FROM directive_approvals - WHERE directive_id = $1 AND status = 'pending' - ORDER BY - CASE urgency - WHEN 'critical' THEN 1 - WHEN 'high' THEN 2 - WHEN 'normal' THEN 3 - ELSE 4 - END, - created_at - "#, - ) - .bind(directive_id) - .fetch_all(pool) - .await -} - -/// Get step by contract ID. -pub async fn get_step_by_contract_id( - pool: &PgPool, - contract_id: Uuid, -) -> Result<Option<ChainStep>, sqlx::Error> { - sqlx::query_as::<_, ChainStep>( - "SELECT * FROM chain_steps WHERE contract_id = $1" - ) - .bind(contract_id) - .fetch_optional(pool) - .await -} - -// ============================================================================= // Helper Functions // ============================================================================= diff --git a/makima/src/lib.rs b/makima/src/lib.rs index 3bc460b..8d3db58 100644 --- a/makima/src/lib.rs +++ b/makima/src/lib.rs @@ -3,6 +3,5 @@ pub mod daemon; pub mod db; pub mod listen; pub mod llm; -pub mod orchestration; pub mod server; pub mod tts; diff --git a/makima/src/orchestration/engine.rs b/makima/src/orchestration/engine.rs deleted file mode 100644 index 9f7c3b1..0000000 --- a/makima/src/orchestration/engine.rs +++ /dev/null @@ -1,1335 +0,0 @@ -//! Directive orchestration engine. -//! -//! Manages the lifecycle of directives: -//! - Starts directives and generates initial chains -//! - Monitors step execution and triggers evaluations -//! - Handles rework, escalation, and chain regeneration -//! - Enforces circuit breakers (cost, time, rework limits) - -use std::collections::HashMap; - -use sqlx::PgPool; -use thiserror::Error; -use tokio::sync::broadcast; -use uuid::Uuid; - -use crate::db::models::{ - AddStepRequest, ChainStep, CreateContractRequest, CreateTaskRequest, Directive, - DirectiveEvent, UpdateStepRequest, -}; -use crate::db::repository::{self, RepositoryError}; - -use super::planner::{ChainPlanner, GeneratedChain, PlannerError}; -use super::verifier::{ - auto_detect_verifiers, CompositeEvaluator, ConfidenceLevel, EvaluationResult, - VerificationContext, -}; - -/// Error type for engine operations. -#[derive(Error, Debug)] -pub enum EngineError { - #[error("Database error: {0}")] - Database(#[from] sqlx::Error), - - #[error("Repository error: {0}")] - Repository(#[from] RepositoryError), - - #[error("Planner error: {0}")] - Planner(#[from] PlannerError), - - #[error("Directive not found: {0}")] - DirectiveNotFound(Uuid), - - #[error("Chain not found for directive: {0}")] - ChainNotFound(Uuid), - - #[error("Step not found: {0}")] - StepNotFound(Uuid), - - #[error("Invalid state transition: {from} -> {to}")] - InvalidStateTransition { from: String, to: String }, - - #[error("Circuit breaker triggered: {0}")] - CircuitBreaker(String), - - #[error("Directive is paused")] - DirectivePaused, - - #[error("Contract creation failed: {0}")] - ContractCreation(String), - - #[error("LLM error: {0}")] - LlmError(String), -} - -/// Event emitted by the engine for UI updates. -#[derive(Debug, Clone)] -pub enum EngineEvent { - /// Directive status changed - DirectiveStatusChanged { - directive_id: Uuid, - old_status: String, - new_status: String, - }, - /// Step status changed - StepStatusChanged { - directive_id: Uuid, - step_id: Uuid, - old_status: String, - new_status: String, - }, - /// Evaluation completed - EvaluationCompleted { - directive_id: Uuid, - step_id: Uuid, - passed: bool, - confidence_level: ConfidenceLevel, - }, - /// Approval required - ApprovalRequired { - directive_id: Uuid, - approval_id: Uuid, - approval_type: String, - }, - /// Chain regenerated - ChainRegenerated { - directive_id: Uuid, - old_chain_id: Uuid, - new_chain_id: Uuid, - }, -} - -/// Result from starting a directive, containing info needed for auto-start. -pub struct PlanningStartResult { - /// The planning task ID that needs to be started on a daemon - pub task_id: Uuid, - /// The owner ID for finding available daemons - pub owner_id: Uuid, - /// The planning task details needed for the SpawnTask command - pub task_name: String, - pub plan: String, - pub contract_id: Uuid, - pub repository_url: Option<String>, - pub base_branch: Option<String>, -} - -/// Main orchestration engine for directives. -pub struct DirectiveEngine { - pool: PgPool, - planner: ChainPlanner, - event_tx: Option<broadcast::Sender<EngineEvent>>, -} - -impl DirectiveEngine { - /// Create a new directive engine. - pub fn new(pool: PgPool) -> Self { - Self { - planner: ChainPlanner::new(pool.clone()), - pool, - event_tx: None, - } - } - - /// Set the event broadcast channel for UI updates. - pub fn with_event_channel(mut self, tx: broadcast::Sender<EngineEvent>) -> Self { - self.event_tx = Some(tx); - self - } - - /// Emit an event if channel is configured. - fn emit_event(&self, event: EngineEvent) { - if let Some(tx) = &self.event_tx { - let _ = tx.send(event); - } - } - - // ======================================================================== - // Directive Lifecycle - // ======================================================================== - - /// Start a directive: spawn a planning contract+task to generate the chain. - /// Returns a `PlanningStartResult` so the caller can auto-start the task on a daemon. - pub async fn start_directive(&self, directive_id: Uuid) -> Result<PlanningStartResult, EngineError> { - let directive = repository::get_directive(&self.pool, directive_id) - .await? - .ok_or(EngineError::DirectiveNotFound(directive_id))?; - - // Validate current state - if directive.status != "draft" && directive.status != "paused" { - return Err(EngineError::InvalidStateTransition { - from: directive.status, - to: "planning".to_string(), - }); - } - - // Update status to planning - repository::update_directive_status(&self.pool, directive_id, "planning").await?; - self.emit_directive_event( - directive_id, - "status_changed", - "info", - serde_json::json!({"old_status": directive.status, "new_status": "planning"}), - "system", - ) - .await?; - - // Create an empty chain for the planning task to populate - let chain_name = format!( - "{}-chain", - directive.title.to_lowercase().replace(' ', "-") - ); - let _db_chain = repository::create_directive_chain( - &self.pool, - directive_id, - &chain_name, - Some(&format!("Execution plan for: {}", directive.goal)), - None, // rationale - None, // planning_model - ) - .await?; - - // Create a planning contract (type "execute", no phase guard) - let contract = repository::create_contract_for_owner( - &self.pool, - directive.owner_id, - CreateContractRequest { - name: format!("{} - Planning", directive.title), - description: Some(format!( - "Planning contract for directive: {}", - directive.goal - )), - contract_type: Some("execute".to_string()), - template_id: None, - initial_phase: Some("execute".to_string()), - autonomous_loop: Some(true), - phase_guard: Some(false), - local_only: Some(false), - auto_merge_local: None, - }, - ) - .await - .map_err(|e| { - EngineError::ContractCreation(format!("Failed to create planning contract: {}", e)) - })?; - - // Build instructions for the planning task - let plan = self.build_planning_task_instructions(&directive); - - // Create the planning task - let task_name = format!("{} - Planning", directive.title); - let task = repository::create_task_for_owner( - &self.pool, - directive.owner_id, - CreateTaskRequest { - contract_id: Some(contract.id), - name: task_name.clone(), - description: Some(format!( - "Plan the execution chain for directive: {}", - directive.goal - )), - plan: plan.clone(), - parent_task_id: None, - is_supervisor: true, - priority: 5, - repository_url: directive.repository_url.clone(), - base_branch: directive.base_branch.clone(), - target_branch: None, - merge_mode: None, - target_repo_path: None, - completion_action: Some("none".to_string()), - continue_from_task_id: None, - copy_files: None, - checkpoint_sha: None, - branched_from_task_id: None, - conversation_history: None, - supervisor_worktree_task_id: None, - }, - ) - .await - .map_err(|e| { - EngineError::ContractCreation(format!("Failed to create planning task: {}", e)) - })?; - - // Link the supervisor task to the contract - if let Err(e) = repository::update_contract_supervisor( - &self.pool, - contract.id, - task.id, - ) - .await - { - tracing::warn!( - contract_id = %contract.id, - task_id = %task.id, - error = %e, - "Failed to link supervisor task to planning contract" - ); - } - - // Link the planning contract to the directive - repository::set_directive_orchestrator_contract( - &self.pool, - directive_id, - contract.id, - ) - .await?; - - self.emit_directive_event( - directive_id, - "planning_started", - "info", - serde_json::json!({ - "contract_id": contract.id, - "task_id": task.id, - "message": "Planning task spawned, waiting for chain generation", - }), - "system", - ) - .await?; - - Ok(PlanningStartResult { - task_id: task.id, - owner_id: directive.owner_id, - task_name, - plan, - contract_id: contract.id, - repository_url: directive.repository_url.clone(), - base_branch: directive.base_branch.clone(), - }) - } - - /// Pause a directive. - pub async fn pause_directive(&self, directive_id: Uuid) -> Result<(), EngineError> { - let directive = repository::get_directive(&self.pool, directive_id) - .await? - .ok_or(EngineError::DirectiveNotFound(directive_id))?; - - if directive.status != "active" { - return Err(EngineError::InvalidStateTransition { - from: directive.status, - to: "paused".to_string(), - }); - } - - repository::update_directive_status(&self.pool, directive_id, "paused").await?; - self.emit_event(EngineEvent::DirectiveStatusChanged { - directive_id, - old_status: "active".to_string(), - new_status: "paused".to_string(), - }); - - Ok(()) - } - - /// Resume a paused directive. - pub async fn resume_directive(&self, directive_id: Uuid) -> Result<(), EngineError> { - let directive = repository::get_directive(&self.pool, directive_id) - .await? - .ok_or(EngineError::DirectiveNotFound(directive_id))?; - - if directive.status != "paused" { - return Err(EngineError::InvalidStateTransition { - from: directive.status, - to: "active".to_string(), - }); - } - - repository::update_directive_status(&self.pool, directive_id, "active").await?; - self.emit_event(EngineEvent::DirectiveStatusChanged { - directive_id, - old_status: "paused".to_string(), - new_status: "active".to_string(), - }); - - // Continue execution - self.advance_chain(directive_id).await?; - - Ok(()) - } - - /// Stop a directive (cannot be resumed). - pub async fn stop_directive(&self, directive_id: Uuid) -> Result<(), EngineError> { - let directive = repository::get_directive(&self.pool, directive_id) - .await? - .ok_or(EngineError::DirectiveNotFound(directive_id))?; - - if directive.status == "completed" || directive.status == "failed" { - return Err(EngineError::InvalidStateTransition { - from: directive.status, - to: "failed".to_string(), - }); - } - - repository::update_directive_status(&self.pool, directive_id, "failed").await?; - self.emit_event(EngineEvent::DirectiveStatusChanged { - directive_id, - old_status: directive.status, - new_status: "failed".to_string(), - }); - - Ok(()) - } - - // ======================================================================== - // Chain Management - // ======================================================================== - - /// Build a default chain as a fallback. - fn build_default_chain(&self, directive: &Directive) -> GeneratedChain { - GeneratedChain { - name: format!( - "{}-chain", - directive.title.to_lowercase().replace(' ', "-") - ), - description: format!("Execution plan for: {}", directive.goal), - steps: vec![ - super::planner::GeneratedStep { - name: "research".to_string(), - step_type: "research".to_string(), - description: format!( - "Research and understand the requirements for: {}", - directive.goal - ), - depends_on: vec![], - requirement_ids: vec![], - contract_template: None, - }, - super::planner::GeneratedStep { - name: "implement".to_string(), - step_type: "implement".to_string(), - description: format!("Implement the solution for: {}", directive.goal), - depends_on: vec!["research".to_string()], - requirement_ids: vec![], - contract_template: None, - }, - super::planner::GeneratedStep { - name: "test".to_string(), - step_type: "test".to_string(), - description: "Test and verify the implementation".to_string(), - depends_on: vec!["implement".to_string()], - requirement_ids: vec![], - contract_template: None, - }, - ], - } - } - - /// Create database steps from a generated chain. - async fn create_steps_from_chain( - &self, - chain_id: &Uuid, - chain: &GeneratedChain, - ) -> Result<(), EngineError> { - // First pass: create all steps and build name-to-id map - let mut step_id_map: HashMap<String, Uuid> = HashMap::new(); - - // Get editor positions - let positions = self.planner.compute_editor_positions(chain); - - for step in &chain.steps { - let (editor_x, editor_y) = positions - .get(&step.name) - .copied() - .unwrap_or((100.0, 100.0)); - - let task_plan = step - .contract_template - .as_ref() - .and_then(|t| t.tasks.first()) - .map(|t| t.plan.clone()) - .or_else(|| Some(step.description.clone())); - - let request = AddStepRequest { - name: step.name.clone(), - description: Some(step.description.clone()), - step_type: Some(step.step_type.clone()), - contract_type: step.contract_template.as_ref().map(|t| t.contract_type.clone()), - initial_phase: Some("plan".to_string()), - task_plan, - phases: step.contract_template.as_ref().map(|t| t.phases.clone()), - depends_on: None, // Will update in second pass - parallel_group: None, - requirement_ids: Some(step.requirement_ids.clone()), - acceptance_criteria_ids: None, - verifier_config: None, - editor_x: Some(editor_x), - editor_y: Some(editor_y), - }; - - let db_step = repository::create_chain_step(&self.pool, *chain_id, request).await?; - step_id_map.insert(step.name.clone(), db_step.id); - } - - // Second pass: update dependencies - for step in &chain.steps { - if step.depends_on.is_empty() { - continue; - } - - let step_id = step_id_map.get(&step.name).unwrap(); - let dep_ids: Vec<Uuid> = step - .depends_on - .iter() - .filter_map(|name| step_id_map.get(name)) - .copied() - .collect(); - - // Update step with proper dependencies - let update = UpdateStepRequest { - name: None, - description: None, - task_plan: None, - depends_on: Some(dep_ids), - requirement_ids: None, - acceptance_criteria_ids: None, - verifier_config: None, - editor_x: None, - editor_y: None, - }; - - repository::update_chain_step(&self.pool, *step_id, update).await?; - } - - Ok(()) - } - - /// Regenerate chain while preserving completed steps. - pub async fn regenerate_chain( - &self, - directive_id: Uuid, - reason: &str, - ) -> Result<Uuid, EngineError> { - let directive = repository::get_directive(&self.pool, directive_id) - .await? - .ok_or(EngineError::DirectiveNotFound(directive_id))?; - - let current_chain = repository::get_current_chain(&self.pool, directive_id) - .await? - .ok_or(EngineError::ChainNotFound(directive_id))?; - - // Use default chain for regeneration - // (planning contract handles initial generation; regeneration uses fallback) - let new_chain = self.build_default_chain(&directive); - - // Supersede old chain - repository::supersede_chain(&self.pool, current_chain.id).await?; - - // Create new chain - let db_chain = repository::create_directive_chain( - &self.pool, - directive_id, - &new_chain.name, - Some(&new_chain.description), - Some(reason), // rationale - None, // planning_model - ) - .await?; - - // Create steps - self.create_steps_from_chain(&db_chain.id, &new_chain).await?; - - self.emit_event(EngineEvent::ChainRegenerated { - directive_id, - old_chain_id: current_chain.id, - new_chain_id: db_chain.id, - }); - - // Continue execution - self.advance_chain(directive_id).await?; - - Ok(db_chain.id) - } - - // ======================================================================== - // Step Execution - // ======================================================================== - - /// Advance chain execution: find ready steps and start them. - pub async fn advance_chain(&self, directive_id: Uuid) -> Result<(), EngineError> { - let directive = repository::get_directive(&self.pool, directive_id) - .await? - .ok_or(EngineError::DirectiveNotFound(directive_id))?; - - // Check if directive is active - if directive.status == "paused" { - return Err(EngineError::DirectivePaused); - } - if directive.status != "active" { - return Ok(()); // Not an error, just nothing to do - } - - // Check circuit breakers - self.check_circuit_breakers(&directive).await?; - - // Get current chain - let chain = repository::get_current_chain(&self.pool, directive_id) - .await? - .ok_or(EngineError::ChainNotFound(directive_id))?; - - // Find ready steps (dependencies met, status=pending) - let ready_steps = repository::find_ready_steps(&self.pool, chain.id).await?; - - // Start each ready step - for step in ready_steps { - self.start_step(&directive, &step).await?; - } - - // Check if chain is complete - let all_steps = repository::list_chain_steps(&self.pool, chain.id).await?; - let all_passed = all_steps.iter().all(|s| s.status == "passed" || s.status == "skipped"); - let any_blocked = all_steps.iter().any(|s| s.status == "blocked" || s.status == "failed"); - - if all_passed && !all_steps.is_empty() { - // Complete the directive - self.complete_directive(directive_id).await?; - } else if any_blocked { - // Check if we should regenerate or fail - let failed_count = all_steps.iter().filter(|s| s.status == "failed").count(); - if failed_count > 3 { - // Too many failures, fail the directive - repository::update_directive_status(&self.pool, directive_id, "failed").await?; - } - } - - Ok(()) - } - - /// Start a step by creating its contract and supervisor task. - async fn start_step(&self, directive: &Directive, step: &ChainStep) -> Result<(), EngineError> { - // Update step status to ready - repository::update_step_status(&self.pool, step.id, "ready").await?; - self.emit_event(EngineEvent::StepStatusChanged { - directive_id: directive.id, - step_id: step.id, - old_status: "pending".to_string(), - new_status: "ready".to_string(), - }); - - // Get contract details from step template - let (name, description, contract_type, initial_phase) = - self.get_contract_details(directive, step); - - // Create contract for this step - let contract = repository::create_contract_for_owner( - &self.pool, - directive.owner_id, - CreateContractRequest { - name: name.clone(), - description: description.clone(), - contract_type: Some(contract_type), - template_id: None, - initial_phase: Some(initial_phase), - autonomous_loop: Some(directive.autonomy_level == "full_auto"), - phase_guard: Some(true), - local_only: Some(false), - auto_merge_local: None, - }, - ) - .await - .map_err(|e| EngineError::ContractCreation(format!("Failed to create contract: {}", e)))?; - - // Build task plan from step description and task_plan - let task_plan = step - .task_plan - .clone() - .unwrap_or_else(|| { - format!( - "## Step: {}\n\n{}\n\n## Directive Goal\n{}", - step.name, - description.as_deref().unwrap_or("Complete this step."), - directive.goal, - ) - }); - - // Create supervisor task linked to the contract - let task = repository::create_task_for_owner( - &self.pool, - directive.owner_id, - CreateTaskRequest { - contract_id: Some(contract.id), - name: name.clone(), - description: description.clone(), - plan: task_plan, - parent_task_id: None, - is_supervisor: true, - priority: 5, - repository_url: directive.repository_url.clone(), - base_branch: directive.base_branch.clone(), - target_branch: None, - merge_mode: Some("pr".to_string()), - target_repo_path: None, - completion_action: Some("pr".to_string()), - continue_from_task_id: None, - copy_files: None, - checkpoint_sha: None, - branched_from_task_id: None, - conversation_history: None, - supervisor_worktree_task_id: None, - }, - ) - .await - .map_err(|e| EngineError::ContractCreation(format!("Failed to create task: {}", e)))?; - - // Link contract and task to step - repository::update_step_contract(&self.pool, step.id, contract.id, Some(task.id)).await?; - - // Update step status to running - repository::update_step_status(&self.pool, step.id, "running").await?; - self.emit_event(EngineEvent::StepStatusChanged { - directive_id: directive.id, - step_id: step.id, - old_status: "ready".to_string(), - new_status: "running".to_string(), - }); - - self.emit_directive_event( - directive.id, - "step_started", - "info", - serde_json::json!({ - "step_id": step.id, - "step_name": step.name, - "contract_id": contract.id, - "task_id": task.id, - }), - "system", - ) - .await?; - - Ok(()) - } - - /// Build contract details from a step. - /// Returns (name, description, contract_type, initial_phase) - fn get_contract_details( - &self, - directive: &Directive, - step: &ChainStep, - ) -> (String, Option<String>, String, String) { - let name = format!("{} - {}", directive.title, step.name); - let description = step.description.clone(); - let contract_type = step.contract_type.clone(); - let initial_phase = step.initial_phase.clone().unwrap_or_else(|| "plan".to_string()); - - (name, description, contract_type, initial_phase) - } - - // ======================================================================== - // Evaluation - // ======================================================================== - - /// Handle contract completion: evaluate the step. - pub async fn on_contract_completed( - &self, - contract_id: Uuid, - ) -> Result<(), EngineError> { - // Find the step for this contract - let step = repository::get_step_by_contract_id(&self.pool, contract_id) - .await? - .ok_or(EngineError::StepNotFound(contract_id))?; - - // Get directive - let chain = repository::get_directive_chain(&self.pool, step.chain_id) - .await? - .ok_or(EngineError::ChainNotFound(step.chain_id))?; - - let directive = repository::get_directive(&self.pool, chain.directive_id) - .await? - .ok_or(EngineError::DirectiveNotFound(chain.directive_id))?; - - // Update step status to evaluating - repository::update_step_status(&self.pool, step.id, "evaluating").await?; - self.emit_event(EngineEvent::StepStatusChanged { - directive_id: directive.id, - step_id: step.id, - old_status: "running".to_string(), - new_status: "evaluating".to_string(), - }); - - // Run evaluation - let result = self.evaluate_step(&directive, &step).await?; - - // Record evaluation - let programmatic_results = result - .verifier_results - .iter() - .filter(|r| r.verifier_type != super::verifier::VerifierType::Llm) - .map(|r| serde_json::to_value(r).unwrap_or(serde_json::Value::Null)) - .collect::<Vec<_>>(); - - let llm_results = result - .verifier_results - .iter() - .filter(|r| r.verifier_type == super::verifier::VerifierType::Llm) - .map(|r| serde_json::to_value(r).unwrap_or(serde_json::Value::Null)) - .collect::<Vec<_>>(); - - // Get chain_id from step - let chain_id = step.chain_id; - - let _evaluation = repository::create_directive_evaluation( - &self.pool, - directive.id, - Some(chain_id), - Some(step.id), - step.contract_id, - "composite", - Some("orchestration_engine"), - result.passed, - Some(result.composite_score), - Some(result.confidence_level.as_str()), - serde_json::Value::Array(programmatic_results), - serde_json::Value::Array(llm_results), - serde_json::Value::Null, // criteria_results - &result.summary, - result.rework_instructions.as_deref(), - ) - .await?; - - // Update step based on result - let new_status = match result.confidence_level { - ConfidenceLevel::Green => "passed", - ConfidenceLevel::Yellow => { - // Check autonomy level - if directive.autonomy_level == "full_auto" { - "passed" // Accept yellow in full auto mode - } else { - // Create approval request - self.request_approval( - &directive, - &step, - "step_review", - &format!( - "Step '{}' completed with yellow confidence ({:.0}%). Review required.", - step.name, - result.composite_score * 100.0 - ), - ) - .await?; - "evaluating" // Wait for approval - } - } - ConfidenceLevel::Red => { - // Initiate rework - self.initiate_rework(&directive, &step, &result).await?; - "rework" - } - }; - - repository::update_step_status(&self.pool, step.id, new_status).await?; - repository::update_step_confidence( - &self.pool, - step.id, - result.composite_score, - result.confidence_level.as_str(), - result.id, - ) - .await?; - - self.emit_event(EngineEvent::EvaluationCompleted { - directive_id: directive.id, - step_id: step.id, - passed: result.passed, - confidence_level: result.confidence_level, - }); - - // If passed, continue chain execution - if new_status == "passed" { - self.advance_chain(directive.id).await?; - } - - Ok(()) - } - - /// Evaluate a step using tiered verification. - async fn evaluate_step( - &self, - directive: &Directive, - step: &ChainStep, - ) -> Result<EvaluationResult, EngineError> { - // Get repository path - let repo_path = directive - .local_path - .as_ref() - .map(std::path::PathBuf::from) - .unwrap_or_else(|| std::path::PathBuf::from(".")); - - // Auto-detect verifiers - let verifiers = auto_detect_verifiers(&repo_path).await; - - // Build verification context - let context = VerificationContext { - step_id: step.id, - contract_id: step.contract_id, - modified_files: vec![], // TODO: Get from contract/git - step_description: step.description.clone().unwrap_or_default(), - acceptance_criteria: vec![], // TODO: Get from directive - directive_context: directive.goal.clone(), - }; - - // Run composite evaluation - let evaluator = CompositeEvaluator::new(verifiers) - .with_thresholds( - directive.confidence_threshold_green, - directive.confidence_threshold_yellow, - ); - - Ok(evaluator.evaluate(&repo_path, &context).await) - } - - /// Initiate rework for a failed step. - async fn initiate_rework( - &self, - directive: &Directive, - step: &ChainStep, - result: &EvaluationResult, - ) -> Result<(), EngineError> { - // Increment rework count - let updated_step = repository::increment_step_rework_count(&self.pool, step.id).await?; - - // Check rework limit - let max_rework = directive.max_rework_cycles.unwrap_or(3); - if updated_step.rework_count >= max_rework { - // Too many rework attempts, mark as blocked - repository::update_step_status(&self.pool, step.id, "blocked").await?; - self.emit_directive_event( - directive.id, - "step_blocked", - "warning", - serde_json::json!({ - "step_id": step.id, - "step_name": step.name, - "reason": "Max rework attempts reached", - }), - "system", - ) - .await?; - return Ok(()); - } - - // Log rework event - self.emit_directive_event( - directive.id, - "step_rework", - "info", - serde_json::json!({ - "step_id": step.id, - "step_name": step.name, - "rework_count": updated_step.rework_count, - "instructions": result.rework_instructions, - }), - "system", - ) - .await?; - - // TODO: Send rework instructions to supervisor task - // This would involve: - // 1. Reset contract phase to 'plan' - // 2. Send message to supervisor with rework instructions - // 3. Update step status to 'running' - - Ok(()) - } - - /// Request human approval for a step. - async fn request_approval( - &self, - directive: &Directive, - step: &ChainStep, - approval_type: &str, - description: &str, - ) -> Result<Uuid, EngineError> { - let context = serde_json::json!({ - "step_id": step.id, - "step_name": step.name, - "confidence_score": step.confidence_score, - }); - - let approval = repository::create_approval_request( - &self.pool, - directive.id, - Some(step.id), - approval_type, - description, - Some(context), - "medium", - None, // expires_at - ) - .await?; - - self.emit_event(EngineEvent::ApprovalRequired { - directive_id: directive.id, - approval_id: approval.id, - approval_type: approval_type.to_string(), - }); - - Ok(approval.id) - } - - /// Handle approval resolution. - pub async fn on_approval_resolved( - &self, - approval_id: Uuid, - approved: bool, - responded_by: Uuid, - ) -> Result<(), EngineError> { - let status = if approved { "approved" } else { "denied" }; - let approval = repository::resolve_approval( - &self.pool, - approval_id, - status, - None, - responded_by, - ) - .await?; - - if let Some(step_id) = approval.step_id { - let step = repository::get_chain_step(&self.pool, step_id) - .await? - .ok_or(EngineError::StepNotFound(step_id))?; - - let chain = repository::get_directive_chain(&self.pool, step.chain_id) - .await? - .ok_or(EngineError::ChainNotFound(step.chain_id))?; - - if approved { - // Mark step as passed and continue - repository::update_step_status(&self.pool, step_id, "passed").await?; - self.advance_chain(chain.directive_id).await?; - } else { - // Mark step as failed/blocked - repository::update_step_status(&self.pool, step_id, "blocked").await?; - } - } - - Ok(()) - } - - // ======================================================================== - // Planning - // ======================================================================== - - /// Build the task instructions for the planning task. - fn build_planning_task_instructions(&self, directive: &Directive) -> String { - let requirements: Vec<String> = directive - .requirements - .as_array() - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_object()) - .map(|obj| { - let id = obj.get("id").and_then(|v| v.as_str()).unwrap_or("?"); - let desc = obj - .get("description") - .and_then(|v| v.as_str()) - .unwrap_or("?"); - format!("- {}: {}", id, desc) - }) - .collect() - }) - .unwrap_or_default(); - - let criteria: Vec<String> = directive - .acceptance_criteria - .as_array() - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_object()) - .map(|obj| { - let id = obj.get("id").and_then(|v| v.as_str()).unwrap_or("?"); - let criterion = obj - .get("criterion") - .and_then(|v| v.as_str()) - .unwrap_or("?"); - format!("- {}: {}", id, criterion) - }) - .collect() - }) - .unwrap_or_default(); - - let constraints: Vec<String> = directive - .constraints - .as_array() - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_str()) - .map(|s| format!("- {}", s)) - .collect() - }) - .unwrap_or_default(); - - let repo_info = directive - .repository_url - .as_deref() - .unwrap_or("(not specified)"); - - format!( - r#"You are planning an execution chain for a directive. - -## Directive: {title} -## Goal -{goal} - -## Requirements -{requirements} - -## Acceptance Criteria -{criteria} - -## Constraints -{constraints} - -## Repository: {repo} - -## Your Task - -Analyze the repository and create a chain of execution steps. -For each step, add it via the API: - -```bash -curl -s -X POST "$MAKIMA_URL/api/v1/directives/{directive_id}/chain/steps" \ - -H "Authorization: Bearer $MAKIMA_API_KEY" \ - -H "Content-Type: application/json" \ - -d '{{ - "name": "step-name", - "description": "What this step accomplishes", - "step_type": "implement", - "depends_on": [], - "contract_type": "execute", - "initial_phase": "execute", - "task_plan": "Detailed instructions for the step executor" - }}' -``` - -### Step types -Use these step types: research, design, implement, test, review, document - -### Dependencies -Each step can depend on other steps by name. Use the `depends_on` field with an array of step names. -Steps with no dependencies will run in parallel. - -### Guidelines -1. Break the work into logical, independently executable steps -2. Each step should be completable by a single Claude Code session -3. Use dependencies to enforce ordering where needed -4. Include a "test" or "verify" step at the end -5. Keep step names in kebab-case -6. The `task_plan` field should contain detailed instructions for the agent that will execute the step - -When you have added all steps, your task is complete."#, - title = directive.title, - goal = directive.goal, - requirements = if requirements.is_empty() { - "(none)".to_string() - } else { - requirements.join("\n") - }, - criteria = if criteria.is_empty() { - "(none)".to_string() - } else { - criteria.join("\n") - }, - constraints = if constraints.is_empty() { - "(none)".to_string() - } else { - constraints.join("\n") - }, - repo = repo_info, - directive_id = directive.id, - ) - } - - /// Handle planning task completion. - pub async fn on_planning_complete( - &self, - directive_id: Uuid, - success: bool, - ) -> Result<(), EngineError> { - let directive = repository::get_directive(&self.pool, directive_id) - .await? - .ok_or(EngineError::DirectiveNotFound(directive_id))?; - - // Only process if directive is still in planning state - if directive.status != "planning" { - tracing::warn!( - "Directive {} is in state '{}', not 'planning'. Skipping planning completion.", - directive_id, - directive.status - ); - return Ok(()); - } - - // Get current chain - let chain = repository::get_current_chain(&self.pool, directive_id) - .await? - .ok_or(EngineError::ChainNotFound(directive_id))?; - - // Check if chain has steps - let steps = repository::list_chain_steps(&self.pool, chain.id).await?; - - if success && !steps.is_empty() { - tracing::info!( - "Planning completed successfully for directive {} with {} steps", - directive_id, - steps.len() - ); - } else { - // Fall back to default chain - let reason = if !success { - "Planning task failed" - } else { - "Planning task produced no steps" - }; - tracing::warn!( - "{} for directive {}, using default chain", - reason, - directive_id - ); - - let default_chain = self.build_default_chain(&directive); - self.create_steps_from_chain(&chain.id, &default_chain) - .await?; - } - - // Activate the directive - repository::update_directive_status(&self.pool, directive_id, "active").await?; - self.emit_event(EngineEvent::DirectiveStatusChanged { - directive_id, - old_status: "planning".to_string(), - new_status: "active".to_string(), - }); - - self.emit_directive_event( - directive_id, - "planning_completed", - "info", - serde_json::json!({"success": success}), - "system", - ) - .await?; - - // Start ready steps - self.advance_chain(directive_id).await?; - - Ok(()) - } - - // ======================================================================== - // Circuit Breakers - // ======================================================================== - - /// Check circuit breakers for a directive. - async fn check_circuit_breakers(&self, directive: &Directive) -> Result<(), EngineError> { - // Check cost limit - if let Some(max_cost) = directive.max_total_cost_usd { - let current_cost = directive.total_cost_usd; - if current_cost >= max_cost { - return Err(EngineError::CircuitBreaker(format!( - "Cost limit exceeded: ${:.2} >= ${:.2}", - current_cost, max_cost - ))); - } - } - - // Check time limit (stored in minutes) - if let Some(max_minutes) = directive.max_wall_time_minutes { - if let Some(started_at) = directive.started_at { - let elapsed = chrono::Utc::now().signed_duration_since(started_at); - let elapsed_minutes = elapsed.num_minutes(); - if elapsed_minutes >= max_minutes as i64 { - return Err(EngineError::CircuitBreaker(format!( - "Time limit exceeded: {} min >= {} min", - elapsed_minutes, max_minutes - ))); - } - } - } - - // Check chain generation limit - if let Some(max_gen) = directive.max_chain_regenerations { - let current_gen = directive.chain_generation_count; - if current_gen >= max_gen { - return Err(EngineError::CircuitBreaker(format!( - "Chain generation limit exceeded: {} >= {}", - current_gen, max_gen - ))); - } - } - - Ok(()) - } - - // ======================================================================== - // Completion - // ======================================================================== - - /// Complete a directive after all steps pass. - async fn complete_directive(&self, directive_id: Uuid) -> Result<(), EngineError> { - // Run final evaluation (optional) - // TODO: LLM evaluation of overall directive completion - - // Update directive status - repository::update_directive_status(&self.pool, directive_id, "completed").await?; - - self.emit_event(EngineEvent::DirectiveStatusChanged { - directive_id, - old_status: "active".to_string(), - new_status: "completed".to_string(), - }); - - self.emit_directive_event( - directive_id, - "directive_completed", - "info", - serde_json::json!({}), - "system", - ) - .await?; - - Ok(()) - } - - // ======================================================================== - // Event Logging - // ======================================================================== - - /// Emit a directive event to the database. - async fn emit_directive_event( - &self, - directive_id: Uuid, - event_type: &str, - severity: &str, - event_data: serde_json::Value, - actor_type: &str, - ) -> Result<DirectiveEvent, EngineError> { - Ok(repository::emit_directive_event( - &self.pool, - directive_id, - None, // chain_id - None, // step_id - event_type, - severity, - Some(event_data), - actor_type, - None, // actor_id - ) - .await?) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_confidence_level_decision() { - // Green confidence should pass in all modes - assert_eq!(ConfidenceLevel::Green.as_str(), "green"); - - // Yellow confidence behavior depends on autonomy level - assert_eq!(ConfidenceLevel::Yellow.as_str(), "yellow"); - - // Red confidence should always trigger rework - assert_eq!(ConfidenceLevel::Red.as_str(), "red"); - } -} diff --git a/makima/src/orchestration/mod.rs b/makima/src/orchestration/mod.rs deleted file mode 100644 index 8fca5ba..0000000 --- a/makima/src/orchestration/mod.rs +++ /dev/null @@ -1,26 +0,0 @@ -//! Orchestration engine for directive-driven autonomous execution. -//! -//! This module provides the core orchestration capabilities: -//! - [`DirectiveEngine`]: Main orchestration loop that manages directive lifecycle -//! - [`ChainPlanner`]: LLM-based chain generation from directive goals -//! - [`Verifier`]: Pluggable verification system for step validation -//! -//! # Architecture -//! -//! The orchestration system follows a directive-first approach: -//! 1. Directives define goals, requirements, and acceptance criteria -//! 2. Chains are generated execution plans (DAGs of steps) -//! 3. Steps map to contracts that are created and monitored -//! 4. Tiered verification (programmatic first, then LLM) determines confidence -//! 5. Confidence scoring (green/yellow/red) drives autonomy decisions - -mod engine; -mod planner; -mod verifier; - -pub use engine::{DirectiveEngine, EngineError, PlanningStartResult}; -pub use planner::{ChainPlanner, GeneratedSpec, PlannerError}; -pub use verifier::{ - auto_detect_verifiers, CompositeEvaluator, ConfidenceLevel, EvaluationResult, Verifier, - VerifierError, VerifierInfo, VerifierResult, VerifierType, -}; diff --git a/makima/src/orchestration/planner.rs b/makima/src/orchestration/planner.rs deleted file mode 100644 index aec2e48..0000000 --- a/makima/src/orchestration/planner.rs +++ /dev/null @@ -1,848 +0,0 @@ -//! Chain planner for LLM-based execution plan generation. -//! -//! Generates chains (DAGs of steps) from directive goals and requirements. -//! Supports both initial plan generation and replanning while preserving -//! completed work. - -use serde::{Deserialize, Serialize}; -use std::collections::{HashMap, HashSet}; -use thiserror::Error; -use uuid::Uuid; - -use crate::db::models::{AddStepRequest, ChainStep, Directive}; - -/// Error type for planner operations. -#[derive(Error, Debug)] -pub enum PlannerError { - #[error("Cycle detected in DAG: {0}")] - CycleDetected(String), - - #[error("Invalid dependency: step '{step}' depends on unknown step '{dependency}'")] - InvalidDependency { step: String, dependency: String }, - - #[error("LLM generation failed: {0}")] - LlmError(String), - - #[error("Requirement not covered: {0}")] - RequirementNotCovered(String), - - #[error("Invalid plan: {0}")] - InvalidPlan(String), - - #[error("Empty plan generated")] - EmptyPlan, -} - -/// Generated step from LLM planning. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GeneratedStep { - /// Unique name within the chain - pub name: String, - /// Type of step (e.g., "research", "implement", "test", "review") - pub step_type: String, - /// Description of what this step accomplishes - pub description: String, - /// Names of steps this depends on - pub depends_on: Vec<String>, - /// IDs of requirements this step addresses - pub requirement_ids: Vec<String>, - /// Contract template fields - pub contract_template: Option<ContractTemplate>, -} - -/// Template for contract creation from step. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ContractTemplate { - /// Contract name - pub name: String, - /// Contract description - pub description: String, - /// Contract type (e.g., "simple", "agentic") - pub contract_type: String, - /// Phases for the contract - pub phases: Vec<String>, - /// Tasks within the contract - pub tasks: Vec<TaskTemplate>, - /// Deliverables expected - pub deliverables: Vec<DeliverableTemplate>, -} - -/// Template for task within contract. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TaskTemplate { - pub name: String, - pub plan: String, -} - -/// Template for deliverable. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DeliverableTemplate { - pub id: String, - pub name: String, - pub priority: String, -} - -/// Generated chain from planning. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GeneratedChain { - /// Name for the chain - pub name: String, - /// Description of the execution plan - pub description: String, - /// Steps in the chain - pub steps: Vec<GeneratedStep>, -} - -/// Generated specification from LLM. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GeneratedSpec { - /// Generated title (if improved from goal) - pub title: Option<String>, - /// Structured requirements - pub requirements: serde_json::Value, - /// Structured acceptance criteria - pub acceptance_criteria: serde_json::Value, - /// Constraints extracted from goal - pub constraints: Option<serde_json::Value>, -} - -/// Chain planner for LLM-based plan generation. -pub struct ChainPlanner { - /// Default step types to suggest (reserved for future use) - #[allow(dead_code)] - default_step_types: Vec<String>, - /// Database pool for persistence - #[allow(dead_code)] - pool: Option<sqlx::PgPool>, -} - -impl Default for ChainPlanner { - fn default() -> Self { - Self::without_pool() - } -} - -impl ChainPlanner { - /// Create a new chain planner without database pool. - pub fn without_pool() -> Self { - Self { - default_step_types: vec![ - "research".to_string(), - "design".to_string(), - "implement".to_string(), - "test".to_string(), - "review".to_string(), - "document".to_string(), - ], - pool: None, - } - } - - /// Create a new chain planner (backwards compatible). - pub fn new(pool: sqlx::PgPool) -> Self { - Self { - default_step_types: vec![ - "research".to_string(), - "design".to_string(), - "implement".to_string(), - "test".to_string(), - "review".to_string(), - "document".to_string(), - ], - pool: Some(pool), - } - } - - /// Generate a specification from a directive's goal. - /// - /// Analyzes the goal text to produce structured requirements and - /// acceptance criteria. In production, this would call an LLM for - /// richer spec generation. - pub async fn generate_spec( - &self, - directive: &Directive, - ) -> Result<GeneratedSpec, PlannerError> { - // Build a prompt for spec generation - let prompt = format!( - r#"Analyze this goal and generate structured requirements and acceptance criteria. - -Goal: {} - -Generate a JSON response with: -- title: A concise title -- requirements: Array of {{id, title, description, priority, category}} -- acceptance_criteria: Array of {{id, requirementIds, description, testable, verificationMethod}} -- constraints: Array of constraint strings"#, - directive.goal - ); - - // For now, generate a basic spec from the goal text. - // When LLM integration is available, this will call the LLM with the prompt. - let _prompt = prompt; // Will be used when LLM is wired up - - let title = generate_title_from_goal(&directive.goal); - - let requirements = serde_json::json!([ - { - "id": "REQ-001", - "title": title, - "description": directive.goal, - "priority": "required", - "category": "core" - } - ]); - - let acceptance_criteria = serde_json::json!([ - { - "id": "AC-001", - "requirementIds": ["REQ-001"], - "description": format!("Goal is achieved: {}", directive.goal), - "testable": true, - "verificationMethod": "manual" - } - ]); - - Ok(GeneratedSpec { - title: Some(title), - requirements, - acceptance_criteria, - constraints: None, - }) - } - - /// Build a planning prompt for the LLM. - pub fn build_planning_prompt(&self, directive: &Directive) -> String { - let requirements: Vec<String> = directive - .requirements - .as_array() - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_object()) - .map(|obj| { - let id = obj.get("id").and_then(|v| v.as_str()).unwrap_or("?"); - let desc = obj - .get("description") - .and_then(|v| v.as_str()) - .unwrap_or("?"); - format!("- {}: {}", id, desc) - }) - .collect() - }) - .unwrap_or_default(); - - let criteria: Vec<String> = directive - .acceptance_criteria - .as_array() - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_object()) - .map(|obj| { - let id = obj.get("id").and_then(|v| v.as_str()).unwrap_or("?"); - let criterion = obj - .get("criterion") - .and_then(|v| v.as_str()) - .unwrap_or("?"); - format!("- {}: {}", id, criterion) - }) - .collect() - }) - .unwrap_or_default(); - - let constraints: Vec<String> = directive - .constraints - .as_array() - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_str()) - .map(|s| format!("- {}", s)) - .collect() - }) - .unwrap_or_default(); - - format!( - r#"You are a software architect planning an execution chain for a coding task. - -## Directive Goal -{goal} - -## Requirements -{requirements} - -## Acceptance Criteria -{criteria} - -## Constraints -{constraints} - -## Instructions - -Create an execution plan as a chain of steps. Each step should: -1. Have a unique, descriptive name (kebab-case) -2. Specify its type (research, design, implement, test, review, document) -3. Declare dependencies on prior steps (if any) -4. Map to specific requirement IDs it addresses -5. Include a contract template with tasks and deliverables - -The chain should form a valid DAG (no cycles). Steps can run in parallel if they don't depend on each other. - -Respond with a JSON object in this format: -```json -{{ - "name": "chain-name", - "description": "Brief description of the plan", - "steps": [ - {{ - "name": "step-name", - "step_type": "implement", - "description": "What this step does", - "depends_on": ["prior-step-name"], - "requirement_ids": ["REQ-001"], - "contract_template": {{ - "name": "Contract Name", - "description": "Contract description", - "contract_type": "simple", - "phases": ["plan", "execute"], - "tasks": [ - {{"name": "Task 1", "plan": "Detailed plan for this task"}} - ], - "deliverables": [ - {{"id": "del-1", "name": "Deliverable 1", "priority": "required"}} - ] - }} - }} - ] -}} -``` - -Generate the optimal execution plan now."#, - goal = directive.goal, - requirements = requirements.join("\n"), - criteria = criteria.join("\n"), - constraints = constraints.join("\n"), - ) - } - - /// Parse LLM response into a generated chain. - pub fn parse_plan_response(&self, response: &str) -> Result<GeneratedChain, PlannerError> { - // Extract JSON from response (may be wrapped in markdown code blocks) - let json_str = extract_json_from_response(response)?; - - let chain: GeneratedChain = serde_json::from_str(&json_str) - .map_err(|e| PlannerError::InvalidPlan(format!("JSON parse error: {}", e)))?; - - if chain.steps.is_empty() { - return Err(PlannerError::EmptyPlan); - } - - // Validate the chain - self.validate_chain(&chain)?; - - Ok(chain) - } - - /// Validate a generated chain. - pub fn validate_chain(&self, chain: &GeneratedChain) -> Result<(), PlannerError> { - // Build step name set - let step_names: HashSet<&str> = chain.steps.iter().map(|s| s.name.as_str()).collect(); - - // Check for duplicate names - if step_names.len() != chain.steps.len() { - return Err(PlannerError::InvalidPlan( - "Duplicate step names detected".to_string(), - )); - } - - // Validate dependencies exist - for step in &chain.steps { - for dep in &step.depends_on { - if !step_names.contains(dep.as_str()) { - return Err(PlannerError::InvalidDependency { - step: step.name.clone(), - dependency: dep.clone(), - }); - } - } - } - - // Check for cycles using DFS - self.detect_cycles(chain)?; - - Ok(()) - } - - /// Detect cycles in the chain DAG using DFS. - fn detect_cycles(&self, chain: &GeneratedChain) -> Result<(), PlannerError> { - let mut visited = HashSet::new(); - let mut rec_stack = HashSet::new(); - - // Build adjacency map - let adj: HashMap<&str, Vec<&str>> = chain - .steps - .iter() - .map(|s| (s.name.as_str(), s.depends_on.iter().map(|d| d.as_str()).collect())) - .collect(); - - for step in &chain.steps { - if !visited.contains(step.name.as_str()) { - if self.has_cycle(&step.name, &adj, &mut visited, &mut rec_stack) { - return Err(PlannerError::CycleDetected(step.name.clone())); - } - } - } - - Ok(()) - } - - fn has_cycle<'a>( - &self, - node: &'a str, - adj: &HashMap<&'a str, Vec<&'a str>>, - visited: &mut HashSet<&'a str>, - rec_stack: &mut HashSet<&'a str>, - ) -> bool { - visited.insert(node); - rec_stack.insert(node); - - if let Some(deps) = adj.get(node) { - for &dep in deps { - if !visited.contains(dep) { - if self.has_cycle(dep, adj, visited, rec_stack) { - return true; - } - } else if rec_stack.contains(dep) { - return true; - } - } - } - - rec_stack.remove(node); - false - } - - /// Check that all requirements are covered by at least one step. - pub fn check_requirement_coverage( - &self, - chain: &GeneratedChain, - directive: &Directive, - ) -> Result<(), PlannerError> { - let required_ids: HashSet<String> = directive - .requirements - .as_array() - .map(|arr| { - arr.iter() - .filter_map(|v| v.get("id").and_then(|id| id.as_str())) - .map(|s| s.to_string()) - .collect() - }) - .unwrap_or_default(); - - let covered_ids: HashSet<String> = chain - .steps - .iter() - .flat_map(|s| s.requirement_ids.clone()) - .collect(); - - for req_id in required_ids { - if !covered_ids.contains(&req_id) { - return Err(PlannerError::RequirementNotCovered(req_id)); - } - } - - Ok(()) - } - - /// Get topological order of steps. - pub fn topological_sort<'a>( - &self, - chain: &'a GeneratedChain, - ) -> Result<Vec<&'a str>, PlannerError> { - let mut in_degree: HashMap<&str, usize> = HashMap::new(); - let mut adj: HashMap<&str, Vec<&str>> = HashMap::new(); - - // Initialize - for step in &chain.steps { - in_degree.entry(step.name.as_str()).or_insert(0); - adj.entry(step.name.as_str()).or_insert_with(Vec::new); - } - - // Build graph (reversed - edges from dependency to dependent) - for step in &chain.steps { - for dep in &step.depends_on { - adj.entry(dep.as_str()) - .or_insert_with(Vec::new) - .push(step.name.as_str()); - *in_degree.entry(step.name.as_str()).or_insert(0) += 1; - } - } - - // Kahn's algorithm - let mut queue: Vec<&str> = in_degree - .iter() - .filter(|&(_, deg)| *deg == 0) - .map(|(&name, _)| name) - .collect(); - - let mut result = Vec::new(); - - while let Some(node) = queue.pop() { - result.push(node); - - if let Some(neighbors) = adj.get(node) { - for &neighbor in neighbors { - let deg = in_degree.get_mut(neighbor).unwrap(); - *deg -= 1; - if *deg == 0 { - queue.push(neighbor); - } - } - } - } - - if result.len() != chain.steps.len() { - return Err(PlannerError::CycleDetected( - "Cycle detected during topological sort".to_string(), - )); - } - - Ok(result) - } - - /// Convert generated steps to AddStepRequest for database insertion. - pub fn steps_to_requests( - &self, - chain: &GeneratedChain, - step_id_map: &HashMap<String, Uuid>, - ) -> Vec<AddStepRequest> { - chain - .steps - .iter() - .map(|step| { - let depends_on: Vec<Uuid> = step - .depends_on - .iter() - .filter_map(|name| step_id_map.get(name)) - .copied() - .collect(); - - let task_plan = step - .contract_template - .as_ref() - .and_then(|t| t.tasks.first()) - .map(|t| t.plan.clone()); - - AddStepRequest { - name: step.name.clone(), - description: Some(step.description.clone()), - step_type: Some(step.step_type.clone()), - contract_type: step.contract_template.as_ref().map(|t| t.contract_type.clone()), - initial_phase: Some("plan".to_string()), - task_plan, - phases: step.contract_template.as_ref().map(|t| t.phases.clone()), - depends_on: Some(depends_on), - parallel_group: None, - requirement_ids: Some(step.requirement_ids.clone()), - acceptance_criteria_ids: None, - verifier_config: None, - editor_x: None, - editor_y: None, - } - }) - .collect() - } - - /// Compute editor positions for steps based on DAG layout. - pub fn compute_editor_positions( - &self, - chain: &GeneratedChain, - ) -> HashMap<String, (f64, f64)> { - let depths = self.get_step_depths(chain); - let mut positions: HashMap<String, (f64, f64)> = HashMap::new(); - - // Group by depth - let mut by_depth: HashMap<usize, Vec<&str>> = HashMap::new(); - for step in &chain.steps { - let depth = depths.get(&step.name).copied().unwrap_or(0); - by_depth.entry(depth).or_default().push(&step.name); - } - - // Compute positions: x based on depth, y based on index within depth - let x_spacing = 250.0; - let y_spacing = 150.0; - - for (depth, steps) in &by_depth { - let x = (*depth as f64) * x_spacing + 100.0; - for (i, name) in steps.iter().enumerate() { - let y = (i as f64) * y_spacing + 100.0; - positions.insert(name.to_string(), (x, y)); - } - } - - positions - } - - /// Get depth of each step in the DAG. - fn get_step_depths(&self, chain: &GeneratedChain) -> HashMap<String, usize> { - let mut depths: HashMap<String, usize> = HashMap::new(); - - // Build dependency map - let deps: HashMap<String, Vec<String>> = chain - .steps - .iter() - .map(|s| (s.name.clone(), s.depends_on.clone())) - .collect(); - - fn compute_depth( - name: &str, - deps: &HashMap<String, Vec<String>>, - depths: &mut HashMap<String, usize>, - ) -> usize { - if let Some(&d) = depths.get(name) { - return d; - } - - let depth = deps - .get(name) - .map(|dep_list| { - dep_list - .iter() - .map(|d| compute_depth(d, deps, depths) + 1) - .max() - .unwrap_or(0) - }) - .unwrap_or(0); - - depths.insert(name.to_string(), depth); - depth - } - - for step in &chain.steps { - compute_depth(&step.name, &deps, &mut depths); - } - - depths - } - - /// Build a replanning prompt that preserves completed steps. - pub fn build_replan_prompt( - &self, - directive: &Directive, - completed_steps: &[ChainStep], - failed_step: Option<&ChainStep>, - reason: &str, - ) -> String { - let completed_summary: Vec<String> = completed_steps - .iter() - .map(|s| format!("- {} ({}): completed", s.name, s.step_type)) - .collect(); - - let failed_summary = failed_step - .map(|s| format!("Failed step: {} - {}", s.name, s.description.as_deref().unwrap_or(""))) - .unwrap_or_default(); - - format!( - r#"You are a software architect replanning an execution chain. - -## Original Goal -{goal} - -## Completed Steps (preserve these) -{completed} - -## Failure Information -{failed} -Reason: {reason} - -## Instructions -Generate a new execution plan that: -1. Preserves all completed work -2. Addresses the failure -3. Continues toward the original goal - -Use the same JSON format as before. Do not include already completed steps."#, - goal = directive.goal, - completed = completed_summary.join("\n"), - failed = failed_summary, - reason = reason, - ) - } -} - -/// Generate a concise title from a goal string. -fn generate_title_from_goal(goal: &str) -> String { - // Take the first sentence or first 80 chars - let title = if let Some(pos) = goal.find('.') { - if pos < 100 { - &goal[..pos] - } else { - &goal[..80.min(goal.len())] - } - } else if goal.len() > 80 { - &goal[..80] - } else { - goal - }; - title.trim().to_string() -} - -/// Extract JSON from LLM response (handles markdown code blocks). -fn extract_json_from_response(response: &str) -> Result<String, PlannerError> { - // Try to find JSON in code block - if let Some(start) = response.find("```json") { - let json_start = start + 7; - if let Some(end) = response[json_start..].find("```") { - return Ok(response[json_start..json_start + end].trim().to_string()); - } - } - - // Try to find JSON in generic code block - if let Some(start) = response.find("```") { - let block_start = start + 3; - // Skip language identifier if present - let json_start = response[block_start..] - .find('\n') - .map(|i| block_start + i + 1) - .unwrap_or(block_start); - if let Some(end) = response[json_start..].find("```") { - return Ok(response[json_start..json_start + end].trim().to_string()); - } - } - - // Try to parse the whole thing as JSON - if response.trim().starts_with('{') { - return Ok(response.trim().to_string()); - } - - Err(PlannerError::InvalidPlan( - "Could not extract JSON from response".to_string(), - )) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn make_test_chain() -> GeneratedChain { - GeneratedChain { - name: "test-chain".to_string(), - description: "Test chain".to_string(), - steps: vec![ - GeneratedStep { - name: "step-a".to_string(), - step_type: "research".to_string(), - description: "Research step".to_string(), - depends_on: vec![], - requirement_ids: vec!["REQ-001".to_string()], - contract_template: None, - }, - GeneratedStep { - name: "step-b".to_string(), - step_type: "implement".to_string(), - description: "Implementation step".to_string(), - depends_on: vec!["step-a".to_string()], - requirement_ids: vec!["REQ-002".to_string()], - contract_template: None, - }, - GeneratedStep { - name: "step-c".to_string(), - step_type: "test".to_string(), - description: "Test step".to_string(), - depends_on: vec!["step-b".to_string()], - requirement_ids: vec!["REQ-001".to_string()], - contract_template: None, - }, - ], - } - } - - #[test] - fn test_validate_chain_valid() { - let planner = ChainPlanner::without_pool(); - let chain = make_test_chain(); - assert!(planner.validate_chain(&chain).is_ok()); - } - - #[test] - fn test_validate_chain_invalid_dependency() { - let planner = ChainPlanner::without_pool(); - let mut chain = make_test_chain(); - chain.steps[1].depends_on = vec!["nonexistent".to_string()]; - - let result = planner.validate_chain(&chain); - assert!(matches!(result, Err(PlannerError::InvalidDependency { .. }))); - } - - #[test] - fn test_validate_chain_cycle() { - let planner = ChainPlanner::without_pool(); - let chain = GeneratedChain { - name: "cyclic".to_string(), - description: "Has cycle".to_string(), - steps: vec![ - GeneratedStep { - name: "a".to_string(), - step_type: "research".to_string(), - description: "A".to_string(), - depends_on: vec!["c".to_string()], - requirement_ids: vec![], - contract_template: None, - }, - GeneratedStep { - name: "b".to_string(), - step_type: "implement".to_string(), - description: "B".to_string(), - depends_on: vec!["a".to_string()], - requirement_ids: vec![], - contract_template: None, - }, - GeneratedStep { - name: "c".to_string(), - step_type: "test".to_string(), - description: "C".to_string(), - depends_on: vec!["b".to_string()], - requirement_ids: vec![], - contract_template: None, - }, - ], - }; - - let result = planner.validate_chain(&chain); - assert!(matches!(result, Err(PlannerError::CycleDetected(_)))); - } - - #[test] - fn test_topological_sort() { - let planner = ChainPlanner::without_pool(); - let chain = make_test_chain(); - let order = planner.topological_sort(&chain).unwrap(); - - // step-a must come before step-b, step-b before step-c - let pos_a = order.iter().position(|&n| n == "step-a").unwrap(); - let pos_b = order.iter().position(|&n| n == "step-b").unwrap(); - let pos_c = order.iter().position(|&n| n == "step-c").unwrap(); - - assert!(pos_a < pos_b); - assert!(pos_b < pos_c); - } - - #[test] - fn test_extract_json_from_code_block() { - let response = r#" -Here's the plan: - -```json -{"name": "test"} -``` - -That's it! -"#; - let json = extract_json_from_response(response).unwrap(); - assert_eq!(json, r#"{"name": "test"}"#); - } - - #[test] - fn test_extract_json_raw() { - let response = r#"{"name": "test"}"#; - let json = extract_json_from_response(response).unwrap(); - assert_eq!(json, r#"{"name": "test"}"#); - } -} diff --git a/makima/src/orchestration/verifier.rs b/makima/src/orchestration/verifier.rs deleted file mode 100644 index bc29e47..0000000 --- a/makima/src/orchestration/verifier.rs +++ /dev/null @@ -1,833 +0,0 @@ -//! Verification system for directive step evaluation. -//! -//! Provides tiered verification: programmatic verifiers run first, -//! then LLM evaluation if programmatic checks pass. Composite scoring -//! combines results with configurable weights. - -use async_trait::async_trait; -use serde::{Deserialize, Serialize}; -use serde_json::Value as JsonValue; -use std::path::Path; -use thiserror::Error; -use uuid::Uuid; - -/// Confidence level based on composite score and thresholds. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum ConfidenceLevel { - /// High confidence (score >= green threshold) - Green, - /// Medium confidence (score >= yellow threshold but < green) - Yellow, - /// Low confidence (score < yellow threshold) - Red, -} - -impl ConfidenceLevel { - /// Compute confidence level from score and thresholds. - pub fn from_score(score: f64, green_threshold: f64, yellow_threshold: f64) -> Self { - if score >= green_threshold { - ConfidenceLevel::Green - } else if score >= yellow_threshold { - ConfidenceLevel::Yellow - } else { - ConfidenceLevel::Red - } - } - - /// Convert to string for database storage. - pub fn as_str(&self) -> &'static str { - match self { - ConfidenceLevel::Green => "green", - ConfidenceLevel::Yellow => "yellow", - ConfidenceLevel::Red => "red", - } - } -} - -impl std::fmt::Display for ConfidenceLevel { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.as_str()) - } -} - -/// Type of verifier for categorization. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum VerifierType { - /// Run test suite (npm test, cargo test, pytest, etc.) - TestRunner, - /// Run linter (eslint, clippy, ruff, etc.) - Linter, - /// Run type checker (tsc, mypy, etc.) - TypeChecker, - /// Run build command (npm build, cargo build, etc.) - Build, - /// Custom command verifier - Custom, - /// LLM-based semantic evaluation - Llm, -} - -impl VerifierType { - pub fn as_str(&self) -> &'static str { - match self { - VerifierType::TestRunner => "test_runner", - VerifierType::Linter => "linter", - VerifierType::TypeChecker => "type_checker", - VerifierType::Build => "build", - VerifierType::Custom => "custom", - VerifierType::Llm => "llm", - } - } -} - -/// Result of a single verifier run. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct VerifierResult { - /// Name of the verifier - pub name: String, - /// Type of verifier - pub verifier_type: VerifierType, - /// Whether the verification passed - pub passed: bool, - /// Score from 0.0 to 1.0 (1.0 = perfect, 0.0 = complete failure) - pub score: f64, - /// Weight for composite scoring (default 1.0 for programmatic, 2.0 for LLM) - pub weight: f64, - /// Whether this verifier is required (failure = automatic red confidence) - pub required: bool, - /// Human-readable output/feedback - pub output: String, - /// Structured details (test counts, lint errors, etc.) - pub details: Option<JsonValue>, - /// Execution time in milliseconds - pub duration_ms: u64, -} - -impl VerifierResult { - /// Create a passed result with full score. - pub fn passed(name: String, verifier_type: VerifierType, output: String) -> Self { - Self { - name, - verifier_type, - passed: true, - score: 1.0, - weight: 1.0, - required: false, - output, - details: None, - duration_ms: 0, - } - } - - /// Create a failed result with zero score. - pub fn failed(name: String, verifier_type: VerifierType, output: String) -> Self { - Self { - name, - verifier_type, - passed: false, - score: 0.0, - weight: 1.0, - required: false, - output, - details: None, - duration_ms: 0, - } - } - - /// Set the weight for this result. - pub fn with_weight(mut self, weight: f64) -> Self { - self.weight = weight; - self - } - - /// Mark this verifier as required. - pub fn as_required(mut self) -> Self { - self.required = true; - self - } - - /// Set the score explicitly. - pub fn with_score(mut self, score: f64) -> Self { - self.score = score.clamp(0.0, 1.0); - self - } - - /// Set structured details. - pub fn with_details(mut self, details: JsonValue) -> Self { - self.details = Some(details); - self - } - - /// Set execution duration. - pub fn with_duration(mut self, duration_ms: u64) -> Self { - self.duration_ms = duration_ms; - self - } -} - -/// Composite evaluation result combining multiple verifier results. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EvaluationResult { - /// Unique ID for this evaluation - pub id: Uuid, - /// Step ID being evaluated - pub step_id: Uuid, - /// Whether all required verifiers passed - pub passed: bool, - /// Weighted composite score (0.0-1.0) - pub composite_score: f64, - /// Confidence level derived from score - pub confidence_level: ConfidenceLevel, - /// Individual verifier results - pub verifier_results: Vec<VerifierResult>, - /// Summary feedback for the step - pub summary: String, - /// Rework instructions if failed - pub rework_instructions: Option<String>, - /// Total evaluation duration in milliseconds - pub total_duration_ms: u64, -} - -impl EvaluationResult { - /// Create a new evaluation result from verifier results. - pub fn from_verifiers( - step_id: Uuid, - results: Vec<VerifierResult>, - green_threshold: f64, - yellow_threshold: f64, - ) -> Self { - let id = Uuid::new_v4(); - - // Check if any required verifier failed - let any_required_failed = results.iter().any(|r| r.required && !r.passed); - - // Calculate weighted composite score - let (total_weighted_score, total_weight) = - results - .iter() - .fold((0.0, 0.0), |(score_acc, weight_acc), r| { - (score_acc + r.score * r.weight, weight_acc + r.weight) - }); - - let composite_score = if total_weight > 0.0 { - total_weighted_score / total_weight - } else { - 0.0 - }; - - // Override confidence to red if any required verifier failed - let confidence_level = if any_required_failed { - ConfidenceLevel::Red - } else { - ConfidenceLevel::from_score(composite_score, green_threshold, yellow_threshold) - }; - - let passed = !any_required_failed && confidence_level != ConfidenceLevel::Red; - - // Generate summary - let passed_count = results.iter().filter(|r| r.passed).count(); - let total_count = results.len(); - let summary = format!( - "{}/{} verifiers passed, composite score: {:.2}, confidence: {}", - passed_count, total_count, composite_score, confidence_level - ); - - // Generate rework instructions if failed - let rework_instructions = if !passed { - let failed_verifiers: Vec<&str> = results - .iter() - .filter(|r| !r.passed) - .map(|r| r.name.as_str()) - .collect(); - Some(format!( - "Fix issues identified by: {}", - failed_verifiers.join(", ") - )) - } else { - None - }; - - let total_duration_ms = results.iter().map(|r| r.duration_ms).sum(); - - Self { - id, - step_id, - passed, - composite_score, - confidence_level, - verifier_results: results, - summary, - rework_instructions, - total_duration_ms, - } - } -} - -/// Error type for verification operations. -#[derive(Error, Debug)] -pub enum VerifierError { - #[error("Command execution failed: {0}")] - CommandFailed(String), - - #[error("Command timed out after {0}ms")] - Timeout(u64), - - #[error("Working directory not found: {0}")] - WorkingDirectoryNotFound(String), - - #[error("Verifier not configured: {0}")] - NotConfigured(String), - - #[error("Parse error: {0}")] - ParseError(String), - - #[error("LLM error: {0}")] - LlmError(String), - - #[error("IO error: {0}")] - Io(#[from] std::io::Error), -} - -/// Information about a verifier for serialization and database storage. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct VerifierInfo { - pub name: String, - pub verifier_type: String, - pub command: String, - pub working_directory: Option<String>, - pub detect_files: Vec<String>, - pub weight: f64, - pub required: bool, -} - -/// Verifier trait for pluggable verification implementations. -#[async_trait] -pub trait Verifier: Send + Sync { - /// Get the name of this verifier. - fn name(&self) -> &str; - - /// Get the type of this verifier. - fn verifier_type(&self) -> VerifierType; - - /// Get serializable info about this verifier. - fn info(&self) -> VerifierInfo; - - /// Check if this verifier is applicable to the given repository. - async fn is_applicable(&self, repo_path: &Path) -> bool; - - /// Run verification and return result. - async fn verify(&self, repo_path: &Path, context: &VerificationContext) - -> Result<VerifierResult, VerifierError>; -} - -/// Context provided to verifiers during execution. -#[derive(Debug, Clone)] -pub struct VerificationContext { - /// Step ID being verified - pub step_id: Uuid, - /// Contract ID if step has been instantiated - pub contract_id: Option<Uuid>, - /// Files that were modified in this step - pub modified_files: Vec<String>, - /// Step description for LLM context - pub step_description: String, - /// Acceptance criteria for LLM evaluation - pub acceptance_criteria: Vec<String>, - /// Additional context from directive - pub directive_context: String, -} - -/// Command-based verifier for running shell commands. -pub struct CommandVerifier { - name: String, - verifier_type: VerifierType, - command: String, - #[allow(dead_code)] - working_dir: Option<String>, - #[allow(dead_code)] - timeout_ms: u64, - required: bool, - /// Files/patterns that indicate this verifier is applicable - applicable_patterns: Vec<String>, -} - -impl CommandVerifier { - /// Create a new command verifier. - pub fn new( - name: impl Into<String>, - verifier_type: VerifierType, - command: impl Into<String>, - ) -> Self { - Self { - name: name.into(), - verifier_type, - command: command.into(), - working_dir: None, - timeout_ms: 300_000, // 5 minute default - required: false, - applicable_patterns: Vec::new(), - } - } - - /// Set the working directory. - #[allow(dead_code)] - pub fn with_working_dir(mut self, dir: impl Into<String>) -> Self { - self.working_dir = Some(dir.into()); - self - } - - /// Set the timeout in milliseconds. - #[allow(dead_code)] - pub fn with_timeout(mut self, timeout_ms: u64) -> Self { - self.timeout_ms = timeout_ms; - self - } - - /// Mark as required verifier. - pub fn as_required(mut self) -> Self { - self.required = true; - self - } - - /// Add applicability patterns (files that must exist). - pub fn with_patterns(mut self, patterns: Vec<String>) -> Self { - self.applicable_patterns = patterns; - self - } -} - -#[async_trait] -impl Verifier for CommandVerifier { - fn name(&self) -> &str { - &self.name - } - - fn verifier_type(&self) -> VerifierType { - self.verifier_type.clone() - } - - fn info(&self) -> VerifierInfo { - VerifierInfo { - name: self.name.clone(), - verifier_type: self.verifier_type.as_str().to_string(), - command: self.command.clone(), - working_directory: self.working_dir.clone(), - detect_files: self.applicable_patterns.clone(), - weight: 1.0, - required: self.required, - } - } - - async fn is_applicable(&self, repo_path: &Path) -> bool { - if self.applicable_patterns.is_empty() { - return true; - } - - for pattern in &self.applicable_patterns { - let check_path = repo_path.join(pattern); - if check_path.exists() { - return true; - } - } - false - } - - async fn verify( - &self, - repo_path: &Path, - _context: &VerificationContext, - ) -> Result<VerifierResult, VerifierError> { - let start = std::time::Instant::now(); - - let work_dir = self - .working_dir - .as_ref() - .map(|d| repo_path.join(d)) - .unwrap_or_else(|| repo_path.to_path_buf()); - - if !work_dir.exists() { - return Err(VerifierError::WorkingDirectoryNotFound( - work_dir.display().to_string(), - )); - } - - // Parse command into program and args - let parts: Vec<&str> = self.command.split_whitespace().collect(); - if parts.is_empty() { - return Err(VerifierError::CommandFailed( - "Empty command".to_string(), - )); - } - - let program = parts[0]; - let args = &parts[1..]; - - // Execute command - let output = tokio::process::Command::new(program) - .args(args) - .current_dir(&work_dir) - .output() - .await?; - - let duration_ms = start.elapsed().as_millis() as u64; - let stdout = String::from_utf8_lossy(&output.stdout); - let stderr = String::from_utf8_lossy(&output.stderr); - let combined_output = format!("{}\n{}", stdout, stderr); - - let passed = output.status.success(); - let score = if passed { 1.0 } else { 0.0 }; - - let mut result = VerifierResult { - name: self.name.clone(), - verifier_type: self.verifier_type.clone(), - passed, - score, - weight: 1.0, - required: self.required, - output: combined_output, - details: Some(serde_json::json!({ - "exit_code": output.status.code(), - "command": self.command, - "working_dir": work_dir.display().to_string(), - })), - duration_ms, - }; - - // Try to extract more detailed scoring from output - result = self.enhance_result(result, &stdout); - - Ok(result) - } -} - -impl CommandVerifier { - /// Enhance result with parsed details from output. - fn enhance_result(&self, mut result: VerifierResult, stdout: &str) -> VerifierResult { - match self.verifier_type { - VerifierType::TestRunner => { - // Try to parse test counts from common formats - if let Some((passed, failed, total)) = parse_test_output(stdout) { - result.details = Some(serde_json::json!({ - "tests_passed": passed, - "tests_failed": failed, - "tests_total": total, - "command": self.command, - })); - if total > 0 { - result.score = passed as f64 / total as f64; - } - } - } - VerifierType::Linter => { - // Try to parse lint error counts - if let Some(error_count) = parse_lint_output(stdout) { - result.details = Some(serde_json::json!({ - "errors": error_count, - "command": self.command, - })); - // Score decreases with more errors (up to 10 errors = 0) - result.score = (1.0 - (error_count as f64 / 10.0)).max(0.0); - } - } - _ => {} - } - result - } -} - -/// Parse test output for common formats (Jest, pytest, cargo test). -fn parse_test_output(output: &str) -> Option<(u32, u32, u32)> { - // Jest format: "Tests: X passed, Y failed, Z total" - if let Some(caps) = regex::Regex::new(r"Tests:\s*(\d+)\s*passed,\s*(\d+)\s*failed,\s*(\d+)\s*total") - .ok()? - .captures(output) - { - let passed: u32 = caps.get(1)?.as_str().parse().ok()?; - let failed: u32 = caps.get(2)?.as_str().parse().ok()?; - let total: u32 = caps.get(3)?.as_str().parse().ok()?; - return Some((passed, failed, total)); - } - - // pytest format: "X passed, Y failed" - if let Some(caps) = regex::Regex::new(r"(\d+)\s*passed(?:,\s*(\d+)\s*failed)?") - .ok()? - .captures(output) - { - let passed: u32 = caps.get(1)?.as_str().parse().ok()?; - let failed: u32 = caps.get(2).map(|m| m.as_str().parse().ok()).flatten().unwrap_or(0); - let total = passed + failed; - return Some((passed, failed, total)); - } - - // cargo test format: "test result: ok. X passed; Y failed;" - if let Some(caps) = regex::Regex::new(r"test result:.*?(\d+)\s*passed;\s*(\d+)\s*failed") - .ok()? - .captures(output) - { - let passed: u32 = caps.get(1)?.as_str().parse().ok()?; - let failed: u32 = caps.get(2)?.as_str().parse().ok()?; - let total = passed + failed; - return Some((passed, failed, total)); - } - - None -} - -/// Parse lint output for error counts. -fn parse_lint_output(output: &str) -> Option<u32> { - // ESLint format: "X problems (Y errors, Z warnings)" - if let Some(caps) = regex::Regex::new(r"(\d+)\s*problems?\s*\((\d+)\s*errors?") - .ok()? - .captures(output) - { - return caps.get(2)?.as_str().parse().ok(); - } - - // Clippy format: "warning: X warnings emitted" - if let Some(caps) = regex::Regex::new(r"warning:\s*(\d+)\s*warnings?\s*emitted") - .ok()? - .captures(output) - { - return caps.get(1)?.as_str().parse().ok(); - } - - None -} - -/// Auto-detect applicable verifiers for a repository. -pub async fn auto_detect_verifiers(repo_path: &Path) -> Vec<Box<dyn Verifier>> { - let mut verifiers: Vec<Box<dyn Verifier>> = Vec::new(); - - // Check for package.json (Node.js) - let package_json = repo_path.join("package.json"); - if package_json.exists() { - if let Ok(content) = tokio::fs::read_to_string(&package_json).await { - if let Ok(pkg) = serde_json::from_str::<serde_json::Value>(&content) { - if let Some(scripts) = pkg.get("scripts").and_then(|s| s.as_object()) { - // Test runner - if scripts.contains_key("test") { - verifiers.push(Box::new( - CommandVerifier::new("npm-test", VerifierType::TestRunner, "npm test") - .with_patterns(vec!["package.json".to_string()]) - .as_required(), - )); - } - - // Linter - if scripts.contains_key("lint") { - verifiers.push(Box::new( - CommandVerifier::new("npm-lint", VerifierType::Linter, "npm run lint") - .with_patterns(vec!["package.json".to_string()]), - )); - } - - // Build - if scripts.contains_key("build") { - verifiers.push(Box::new( - CommandVerifier::new("npm-build", VerifierType::Build, "npm run build") - .with_patterns(vec!["package.json".to_string()]) - .as_required(), - )); - } - - // Type check (for TypeScript projects) - if scripts.contains_key("typecheck") || scripts.contains_key("type-check") { - let cmd = if scripts.contains_key("typecheck") { - "npm run typecheck" - } else { - "npm run type-check" - }; - verifiers.push(Box::new( - CommandVerifier::new("npm-typecheck", VerifierType::TypeChecker, cmd) - .with_patterns(vec!["tsconfig.json".to_string()]), - )); - } - } - } - } - } - - // Check for Cargo.toml (Rust) - let cargo_toml = repo_path.join("Cargo.toml"); - if cargo_toml.exists() { - verifiers.push(Box::new( - CommandVerifier::new("cargo-test", VerifierType::TestRunner, "cargo test") - .with_patterns(vec!["Cargo.toml".to_string()]) - .as_required(), - )); - - verifiers.push(Box::new( - CommandVerifier::new("cargo-clippy", VerifierType::Linter, "cargo clippy -- -D warnings") - .with_patterns(vec!["Cargo.toml".to_string()]), - )); - - verifiers.push(Box::new( - CommandVerifier::new("cargo-build", VerifierType::Build, "cargo build") - .with_patterns(vec!["Cargo.toml".to_string()]) - .as_required(), - )); - } - - // Check for pyproject.toml or setup.py (Python) - let pyproject = repo_path.join("pyproject.toml"); - let setup_py = repo_path.join("setup.py"); - if pyproject.exists() || setup_py.exists() { - verifiers.push(Box::new( - CommandVerifier::new("pytest", VerifierType::TestRunner, "pytest") - .with_patterns(vec![ - "pyproject.toml".to_string(), - "setup.py".to_string(), - ]) - .as_required(), - )); - - verifiers.push(Box::new( - CommandVerifier::new("ruff", VerifierType::Linter, "ruff check .") - .with_patterns(vec!["pyproject.toml".to_string()]), - )); - } - - verifiers -} - -/// Composite evaluator that runs multiple verifiers and combines results. -pub struct CompositeEvaluator { - verifiers: Vec<Box<dyn Verifier>>, - green_threshold: f64, - yellow_threshold: f64, -} - -impl CompositeEvaluator { - /// Create a new composite evaluator with default thresholds. - pub fn new(verifiers: Vec<Box<dyn Verifier>>) -> Self { - Self { - verifiers, - green_threshold: 0.8, - yellow_threshold: 0.5, - } - } - - /// Set confidence thresholds. - pub fn with_thresholds(mut self, green: f64, yellow: f64) -> Self { - self.green_threshold = green; - self.yellow_threshold = yellow; - self - } - - /// Add a verifier. - pub fn add_verifier(mut self, verifier: Box<dyn Verifier>) -> Self { - self.verifiers.push(verifier); - self - } - - /// Run all applicable verifiers and return composite result. - pub async fn evaluate( - &self, - repo_path: &Path, - context: &VerificationContext, - ) -> EvaluationResult { - let mut results = Vec::new(); - - for verifier in &self.verifiers { - if !verifier.is_applicable(repo_path).await { - continue; - } - - match verifier.verify(repo_path, context).await { - Ok(result) => results.push(result), - Err(e) => { - // Convert error to failed result - results.push(VerifierResult::failed( - verifier.name().to_string(), - verifier.verifier_type(), - format!("Verifier error: {}", e), - )); - } - } - } - - EvaluationResult::from_verifiers( - context.step_id, - results, - self.green_threshold, - self.yellow_threshold, - ) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_confidence_level_from_score() { - assert_eq!( - ConfidenceLevel::from_score(0.9, 0.8, 0.5), - ConfidenceLevel::Green - ); - assert_eq!( - ConfidenceLevel::from_score(0.8, 0.8, 0.5), - ConfidenceLevel::Green - ); - assert_eq!( - ConfidenceLevel::from_score(0.6, 0.8, 0.5), - ConfidenceLevel::Yellow - ); - assert_eq!( - ConfidenceLevel::from_score(0.5, 0.8, 0.5), - ConfidenceLevel::Yellow - ); - assert_eq!( - ConfidenceLevel::from_score(0.4, 0.8, 0.5), - ConfidenceLevel::Red - ); - } - - #[test] - fn test_evaluation_result_composite_score() { - let results = vec![ - VerifierResult::passed("test1".into(), VerifierType::TestRunner, "OK".into()) - .with_weight(1.0), - VerifierResult::failed("test2".into(), VerifierType::Linter, "Failed".into()) - .with_weight(1.0), - ]; - - let eval = EvaluationResult::from_verifiers(Uuid::new_v4(), results, 0.8, 0.5); - assert!((eval.composite_score - 0.5).abs() < 0.001); - assert_eq!(eval.confidence_level, ConfidenceLevel::Yellow); - } - - #[test] - fn test_required_verifier_override() { - let results = vec![ - VerifierResult::passed("test1".into(), VerifierType::TestRunner, "OK".into()), - VerifierResult::failed("build".into(), VerifierType::Build, "Failed".into()) - .as_required(), - ]; - - let eval = EvaluationResult::from_verifiers(Uuid::new_v4(), results, 0.8, 0.5); - // Even though composite score is 0.5, required failure overrides to red - assert_eq!(eval.confidence_level, ConfidenceLevel::Red); - assert!(!eval.passed); - } - - #[test] - fn test_parse_test_output_jest() { - let output = "Tests: 10 passed, 2 failed, 12 total"; - let (passed, failed, total) = parse_test_output(output).unwrap(); - assert_eq!(passed, 10); - assert_eq!(failed, 2); - assert_eq!(total, 12); - } - - #[test] - fn test_parse_test_output_cargo() { - let output = "test result: ok. 25 passed; 0 failed;"; - let (passed, failed, total) = parse_test_output(output).unwrap(); - assert_eq!(passed, 25); - assert_eq!(failed, 0); - assert_eq!(total, 25); - } -} diff --git a/makima/src/server/handlers/contracts.rs b/makima/src/server/handlers/contracts.rs index 8a6ce0f..a83c72d 100644 --- a/makima/src/server/handlers/contracts.rs +++ b/makima/src/server/handlers/contracts.rs @@ -575,90 +575,7 @@ pub async fn update_contract( }), ).await; - // If contract is part of a directive chain step, update the step status - // and emit an event for the directive engine to process - let pool_for_step = pool.clone(); - let contract_id_for_step = contract.id; - tokio::spawn(async move { - // Look up the step by contract_id - match repository::get_step_by_contract_id(&pool_for_step, contract_id_for_step).await { - Ok(Some(step)) => { - // Get the chain to find the directive_id - let directive_id = match repository::get_directive_chain(&pool_for_step, step.chain_id).await { - Ok(Some(chain)) => chain.directive_id, - Ok(None) => { - tracing::warn!( - chain_id = %step.chain_id, - "Chain not found for step" - ); - return; - } - Err(e) => { - tracing::warn!( - chain_id = %step.chain_id, - error = %e, - "Failed to get chain for step" - ); - return; - } - }; - - // Update step status to 'evaluating' - if let Err(e) = repository::update_step_status(&pool_for_step, step.id, "evaluating").await { - tracing::warn!( - step_id = %step.id, - contract_id = %contract_id_for_step, - error = %e, - "Failed to update step status to evaluating" - ); - } else { - tracing::info!( - step_id = %step.id, - contract_id = %contract_id_for_step, - chain_id = %step.chain_id, - directive_id = %directive_id, - "Contract completed - step transitioned to evaluating" - ); - - // Emit directive event for contract completion - if let Err(e) = repository::emit_directive_event( - &pool_for_step, - directive_id, - Some(step.chain_id), - Some(step.id), - "contract_completed", - "info", - Some(serde_json::json!({ - "contract_id": contract_id_for_step, - "step_id": step.id, - "step_name": step.name - })), - "system", - None, - ).await { - tracing::warn!( - step_id = %step.id, - error = %e, - "Failed to emit contract_completed directive event" - ); - } - } - } - Ok(None) => { - tracing::debug!( - contract_id = %contract_id_for_step, - "Contract not linked to any directive chain step" - ); - } - Err(e) => { - tracing::warn!( - contract_id = %contract_id_for_step, - error = %e, - "Failed to look up step for completed contract" - ); - } - } - }); + // TODO: Directive engine integration (removed for reimplementation) } // Get summary with counts diff --git a/makima/src/server/handlers/directives.rs b/makima/src/server/handlers/directives.rs deleted file mode 100644 index 9c65c5e..0000000 --- a/makima/src/server/handlers/directives.rs +++ /dev/null @@ -1,2116 +0,0 @@ -//! API handlers for directives. -//! -//! Provides REST endpoints for managing directives, chains, steps, -//! evaluations, events, verifiers, and approvals. - -use axum::{ - extract::{Path, Query, State}, - http::StatusCode, - response::{ - sse::{Event, Sse}, - IntoResponse, - }, - Json, -}; -use futures::stream; -use serde::{Deserialize, Serialize}; -use std::convert::Infallible; -use std::time::Duration; -use uuid::Uuid; - -use crate::db::models::{ - AddStepRequest, CreateDirectiveRequest, CreateVerifierRequest, ReworkStepRequest, - UpdateCriteriaRequest, UpdateDirectiveRequest, UpdateRequirementsRequest, UpdateStepRequest, - UpdateVerifierRequest, -}; -use crate::db::repository; -use crate::server::auth::Authenticated; -use crate::server::messages::ApiError; -use crate::server::state::SharedState; - -/// Query parameters for listing directives -#[derive(Debug, Deserialize)] -pub struct ListDirectivesQuery { - pub status: Option<String>, -} - -/// Query parameters for listing events -#[derive(Debug, Deserialize)] -pub struct ListEventsQuery { - pub limit: Option<i64>, -} - -/// Query parameters for SSE stream authentication -/// EventSource API cannot set custom headers, so auth is passed via query params -#[derive(Debug, Deserialize)] -pub struct StreamAuthQuery { - pub token: Option<String>, - pub api_key: Option<String>, -} - -/// Query parameters for listing evaluations -#[derive(Debug, Deserialize)] -pub struct ListEvaluationsQuery { - pub limit: Option<i64>, - #[serde(rename = "stepId")] - pub step_id: Option<Uuid>, -} - -/// Response for directive creation -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct CreateDirectiveResponse { - pub id: Uuid, - pub title: String, - pub status: String, -} - -/// Response for approval actions -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ApprovalActionRequest { - pub response: Option<String>, -} - -// ============================================================================= -// Directive CRUD -// ============================================================================= - -/// Create a new directive -/// POST /api/v1/directives -pub async fn create_directive( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Json(req): Json<CreateDirectiveRequest>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - match repository::create_directive_for_owner(pool, auth.owner_id, req).await { - Ok(directive) => Json(CreateDirectiveResponse { - id: directive.id, - title: directive.title, - status: directive.status, - }) - .into_response(), - Err(e) => { - tracing::error!("Failed to create directive: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// List directives for the authenticated owner -/// GET /api/v1/directives -pub async fn list_directives( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Query(params): Query<ListDirectivesQuery>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - match repository::list_directives_for_owner(pool, auth.owner_id, params.status.as_deref()).await - { - Ok(directives) => { - let total = directives.len() as i64; - Json(serde_json::json!({ - "directives": directives, - "total": total, - })) - .into_response() - } - Err(e) => { - tracing::error!("Failed to list directives: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Get a directive with progress details -/// GET /api/v1/directives/:id -pub async fn get_directive( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - match repository::get_directive_with_progress(pool, id, auth.owner_id).await { - Ok(Some(directive)) => Json(directive).into_response(), - Ok(None) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response(), - Err(e) => { - tracing::error!("Failed to get directive: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Update a directive -/// PUT /api/v1/directives/:id -pub async fn update_directive( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, - Json(req): Json<UpdateDirectiveRequest>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - match repository::update_directive_for_owner(pool, id, auth.owner_id, req).await { - Ok(directive) => Json(directive).into_response(), - Err(repository::RepositoryError::VersionConflict { expected, actual }) => ( - StatusCode::CONFLICT, - Json(ApiError::new( - "VERSION_CONFLICT", - &format!( - "Version conflict: expected {}, got {}", - expected, actual - ), - )), - ) - .into_response(), - Err(e) => { - tracing::error!("Failed to update directive: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Archive a directive -/// DELETE /api/v1/directives/:id -pub async fn archive_directive( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - match repository::archive_directive_for_owner(pool, id, auth.owner_id).await { - Ok(true) => StatusCode::NO_CONTENT.into_response(), - Ok(false) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response(), - Err(e) => { - tracing::error!("Failed to archive directive: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -// ============================================================================= -// Directive Lifecycle -// ============================================================================= - -/// Start a directive (generate chain and begin execution) -/// POST /api/v1/directives/:id/start -pub async fn start_directive( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - // Start directive via orchestration engine - let engine = crate::orchestration::DirectiveEngine::new(pool.clone()); - match engine.start_directive(id).await { - Ok(planning) => { - // Auto-start the planning task on an available daemon - if let Some(daemon_id) = state.find_alternative_daemon(auth.owner_id, &[]) { - // Update task status to "starting" and assign daemon - let update_req = crate::db::models::UpdateTaskRequest { - status: Some("starting".to_string()), - daemon_id: Some(daemon_id), - ..Default::default() - }; - if let Err(e) = repository::update_task_for_owner( - pool, - planning.task_id, - auth.owner_id, - update_req, - ) - .await - { - tracing::warn!("Failed to update planning task status: {}", e); - } - - let command = crate::server::state::DaemonCommand::SpawnTask { - task_id: planning.task_id, - task_name: planning.task_name, - plan: planning.plan, - repo_url: planning.repository_url, - base_branch: planning.base_branch, - target_branch: None, - parent_task_id: None, - depth: 0, - is_orchestrator: false, - target_repo_path: None, - completion_action: Some("none".to_string()), - continue_from_task_id: None, - copy_files: None, - contract_id: Some(planning.contract_id), - is_supervisor: true, - autonomous_loop: false, - resume_session: false, - conversation_history: None, - patch_data: None, - patch_base_sha: None, - local_only: false, - auto_merge_local: false, - supervisor_worktree_task_id: None, - }; - - if let Err(e) = state.send_daemon_command(daemon_id, command).await { - tracing::warn!( - "Failed to auto-start planning task on daemon {}: {}", - daemon_id, - e - ); - } else { - tracing::info!( - "Auto-started planning task {} on daemon {} for directive {}", - planning.task_id, - daemon_id, - id - ); - } - } else { - tracing::warn!( - "No daemon available to auto-start planning task for directive {}", - id - ); - } - - // Return the updated directive with progress - match repository::get_directive_with_progress(pool, id, auth.owner_id).await { - Ok(Some(directive)) => Json(directive).into_response(), - Ok(None) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response(), - Err(e) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response(), - } - } - Err(e) => { - tracing::error!("Failed to start directive: {}", e); - ( - StatusCode::BAD_REQUEST, - Json(ApiError::new("START_FAILED", &e.to_string())), - ) - .into_response() - } - } -} - -/// Pause a directive -/// POST /api/v1/directives/:id/pause -pub async fn pause_directive( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - let engine = crate::orchestration::DirectiveEngine::new(pool.clone()); - match engine.pause_directive(id).await { - Ok(()) => match repository::get_directive(pool, id).await { - Ok(Some(directive)) => Json(directive).into_response(), - Ok(None) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response(), - Err(e) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response(), - }, - Err(e) => { - tracing::error!("Failed to pause directive: {}", e); - ( - StatusCode::BAD_REQUEST, - Json(ApiError::new("PAUSE_FAILED", &e.to_string())), - ) - .into_response() - } - } -} - -/// Resume a paused directive -/// POST /api/v1/directives/:id/resume -pub async fn resume_directive( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - let engine = crate::orchestration::DirectiveEngine::new(pool.clone()); - match engine.resume_directive(id).await { - Ok(()) => match repository::get_directive_with_progress(pool, id, auth.owner_id).await { - Ok(Some(directive)) => Json(directive).into_response(), - Ok(None) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response(), - Err(e) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response(), - }, - Err(e) => { - tracing::error!("Failed to resume directive: {}", e); - ( - StatusCode::BAD_REQUEST, - Json(ApiError::new("RESUME_FAILED", &e.to_string())), - ) - .into_response() - } - } -} - -/// Stop a directive (cannot be resumed) -/// POST /api/v1/directives/:id/stop -pub async fn stop_directive( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - let engine = crate::orchestration::DirectiveEngine::new(pool.clone()); - match engine.stop_directive(id).await { - Ok(()) => match repository::get_directive(pool, id).await { - Ok(Some(directive)) => Json(directive).into_response(), - Ok(None) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response(), - Err(e) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response(), - }, - Err(e) => { - tracing::error!("Failed to stop directive: {}", e); - ( - StatusCode::BAD_REQUEST, - Json(ApiError::new("STOP_FAILED", &e.to_string())), - ) - .into_response() - } - } -} - -// ============================================================================= -// Chain Management -// ============================================================================= - -/// Get current chain for a directive -/// GET /api/v1/directives/:id/chain -pub async fn get_chain( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - match repository::get_current_chain(pool, id).await { - Ok(Some(chain)) => { - match repository::list_chain_steps(pool, chain.id).await { - Ok(steps) => Json(serde_json::json!({ - "chain": chain, - "steps": steps, - })) - .into_response(), - Err(e) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response(), - } - } - Ok(None) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "No active chain")), - ) - .into_response(), - Err(e) => { - tracing::error!("Failed to get chain: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Get chain graph for DAG visualization -/// GET /api/v1/directives/:id/chain/graph -pub async fn get_chain_graph( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - // Get current chain - let chain = match repository::get_current_chain(pool, id).await { - Ok(Some(chain)) => chain, - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "No active chain")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - }; - - match repository::get_chain_graph(pool, chain.id).await { - Ok(graph) => Json(graph).into_response(), - Err(e) => { - tracing::error!("Failed to get chain graph: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Regenerate chain (force replan) -/// POST /api/v1/directives/:id/chain/replan -pub async fn replan_chain( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - let engine = crate::orchestration::DirectiveEngine::new(pool.clone()); - match engine.regenerate_chain(id, "Manual replan requested").await { - Ok(new_chain_id) => Json(serde_json::json!({ - "chainId": new_chain_id, - "message": "Chain regenerated successfully", - })) - .into_response(), - Err(e) => { - tracing::error!("Failed to replan chain: {}", e); - ( - StatusCode::BAD_REQUEST, - Json(ApiError::new("REPLAN_FAILED", &e.to_string())), - ) - .into_response() - } - } -} - -// ============================================================================= -// Step Management -// ============================================================================= - -/// Add a step to the current chain -/// POST /api/v1/directives/:id/chain/steps -pub async fn add_step( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, - Json(req): Json<AddStepRequest>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - // Get current chain - let chain = match repository::get_current_chain(pool, id).await { - Ok(Some(chain)) => chain, - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "No active chain")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - }; - - match repository::create_chain_step(pool, chain.id, req).await { - Ok(step) => (StatusCode::CREATED, Json(step)).into_response(), - Err(e) => { - tracing::error!("Failed to add step: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Get step details -/// GET /api/v1/directives/:id/chain/steps/:step_id -pub async fn get_step( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path((id, step_id)): Path<(Uuid, Uuid)>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - match repository::get_chain_step(pool, step_id).await { - Ok(Some(step)) => Json(step).into_response(), - Ok(None) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Step not found")), - ) - .into_response(), - Err(e) => { - tracing::error!("Failed to get step: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Update a step -/// PUT /api/v1/directives/:id/chain/steps/:step_id -pub async fn update_step( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path((id, step_id)): Path<(Uuid, Uuid)>, - Json(req): Json<UpdateStepRequest>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - match repository::update_chain_step(pool, step_id, req).await { - Ok(step) => Json(step).into_response(), - Err(e) => { - tracing::error!("Failed to update step: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Delete a step -/// DELETE /api/v1/directives/:id/chain/steps/:step_id -pub async fn delete_step( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path((id, step_id)): Path<(Uuid, Uuid)>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - match repository::delete_chain_step(pool, step_id).await { - Ok(true) => StatusCode::NO_CONTENT.into_response(), - Ok(false) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Step not found")), - ) - .into_response(), - Err(e) => { - tracing::error!("Failed to delete step: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Skip a step -/// POST /api/v1/directives/:id/chain/steps/:step_id/skip -pub async fn skip_step( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path((id, step_id)): Path<(Uuid, Uuid)>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - match repository::update_step_status(pool, step_id, "skipped").await { - Ok(step) => Json(step).into_response(), - Err(e) => { - tracing::error!("Failed to skip step: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -// ============================================================================= -// Evaluations -// ============================================================================= - -/// List evaluations for a directive -/// GET /api/v1/directives/:id/evaluations -pub async fn list_evaluations( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, - Query(params): Query<ListEvaluationsQuery>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - let result = if let Some(step_id) = params.step_id { - repository::list_step_evaluations(pool, step_id).await - } else { - repository::list_directive_evaluations(pool, id, params.limit).await - }; - - match result { - Ok(evaluations) => Json(evaluations).into_response(), - Err(e) => { - tracing::error!("Failed to list evaluations: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -// ============================================================================= -// Events -// ============================================================================= - -/// List events for a directive -/// GET /api/v1/directives/:id/events -pub async fn list_events( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, - Query(params): Query<ListEventsQuery>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - match repository::list_directive_events(pool, id, params.limit).await { - Ok(events) => Json(events).into_response(), - Err(e) => { - tracing::error!("Failed to list events: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// SSE stream of events for a directive -/// GET /api/v1/directives/:id/events/stream -/// -/// EventSource API cannot set custom headers, so authentication is accepted -/// via query parameters: ?token=<jwt> or ?api_key=<key> -pub async fn stream_events( - State(state): State<SharedState>, - Path(id): Path<Uuid>, - Query(auth_params): Query<StreamAuthQuery>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Authenticate via query params (EventSource cannot set headers) - let auth = if let Some(ref token) = auth_params.token { - // JWT token - let verifier = match state.jwt_verifier.as_ref() { - Some(v) => v, - None => { - return ( - StatusCode::UNAUTHORIZED, - Json(ApiError::new("AUTH_NOT_CONFIGURED", "Authentication not configured")), - ) - .into_response() - } - }; - let claims = match verifier.verify(token) { - Ok(c) => c, - Err(_) => { - return ( - StatusCode::UNAUTHORIZED, - Json(ApiError::new("INVALID_TOKEN", "Invalid authentication token")), - ) - .into_response() - } - }; - match crate::server::auth::resolve_owner_id_public(pool, claims.sub, claims.email.as_deref()).await { - Ok(owner_id) => crate::server::auth::AuthenticatedUser { - user_id: claims.sub, - owner_id, - auth_source: crate::server::auth::AuthSource::Jwt, - email: claims.email, - }, - Err(_) => { - return ( - StatusCode::UNAUTHORIZED, - Json(ApiError::new("USER_NOT_FOUND", "User not found")), - ) - .into_response() - } - } - } else if let Some(ref api_key) = auth_params.api_key { - // API key - match crate::server::auth::validate_api_key_public(pool, api_key).await { - Ok((user_id, owner_id)) => crate::server::auth::AuthenticatedUser { - user_id, - owner_id, - auth_source: crate::server::auth::AuthSource::ApiKey, - email: None, - }, - Err(_) => { - return ( - StatusCode::UNAUTHORIZED, - Json(ApiError::new("INVALID_API_KEY", "Invalid or revoked API key")), - ) - .into_response() - } - } - } else { - return ( - StatusCode::UNAUTHORIZED, - Json(ApiError::new("MISSING_TOKEN", "Authentication required via ?token= or ?api_key= query parameter")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - // Create SSE stream that polls for new events - let pool_clone = pool.clone(); - let stream = stream::unfold( - (pool_clone, id, None::<chrono::DateTime<chrono::Utc>>), - move |(pool, directive_id, last_seen)| async move { - // Wait a bit before next poll - tokio::time::sleep(Duration::from_secs(1)).await; - - // Get recent events - let events = repository::list_directive_events(&pool, directive_id, Some(10)) - .await - .unwrap_or_default(); - - // Filter to only new events - let new_events: Vec<_> = events - .into_iter() - .filter(|e| last_seen.map(|ls| e.created_at > ls).unwrap_or(true)) - .collect(); - - let new_last_seen = new_events.first().map(|e| e.created_at).or(last_seen); - - // Convert to SSE events - let sse_events: Vec<Result<Event, Infallible>> = new_events - .into_iter() - .map(|e| { - Ok(Event::default() - .event("directive_event") - .data(serde_json::to_string(&e).unwrap_or_default())) - }) - .collect(); - - Some((stream::iter(sse_events), (pool, directive_id, new_last_seen))) - }, - ); - - use futures::StreamExt; - Sse::new(stream.flatten()) - .keep_alive( - axum::response::sse::KeepAlive::new() - .interval(Duration::from_secs(15)) - .text("keepalive"), - ) - .into_response() -} - -// ============================================================================= -// Verifiers -// ============================================================================= - -/// List verifiers for a directive -/// GET /api/v1/directives/:id/verifiers -pub async fn list_verifiers( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - match repository::list_directive_verifiers(pool, id).await { - Ok(verifiers) => Json(verifiers).into_response(), - Err(e) => { - tracing::error!("Failed to list verifiers: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Add a verifier to a directive -/// POST /api/v1/directives/:id/verifiers -pub async fn add_verifier( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, - Json(req): Json<CreateVerifierRequest>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - match repository::create_directive_verifier( - pool, - id, - &req.name, - &req.verifier_type, - req.command.as_deref(), - req.working_directory.as_deref(), - false, // auto_detect - vec![], // detect_files - req.weight.unwrap_or(1.0), - req.required.unwrap_or(false), - ) - .await - { - Ok(verifier) => (StatusCode::CREATED, Json(verifier)).into_response(), - Err(e) => { - tracing::error!("Failed to add verifier: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Update a verifier -/// PUT /api/v1/directives/:id/verifiers/:verifier_id -pub async fn update_verifier( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path((id, verifier_id)): Path<(Uuid, Uuid)>, - Json(req): Json<UpdateVerifierRequest>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - match repository::update_directive_verifier( - pool, - verifier_id, - req.enabled, - req.command.as_deref(), - req.weight, - req.required, - ) - .await - { - Ok(verifier) => Json(verifier).into_response(), - Err(e) => { - tracing::error!("Failed to update verifier: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -// ============================================================================= -// Approvals -// ============================================================================= - -/// List pending approvals for a directive -/// GET /api/v1/directives/:id/approvals -pub async fn list_approvals( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - match repository::list_pending_approvals(pool, id).await { - Ok(approvals) => Json(approvals).into_response(), - Err(e) => { - tracing::error!("Failed to list approvals: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Approve a pending approval request -/// POST /api/v1/directives/:id/approvals/:approval_id/approve -pub async fn approve_request( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path((id, approval_id)): Path<(Uuid, Uuid)>, - Json(req): Json<ApprovalActionRequest>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - let engine = crate::orchestration::DirectiveEngine::new(pool.clone()); - match engine - .on_approval_resolved(approval_id, true, auth.owner_id) - .await - { - Ok(()) => { - match repository::resolve_approval( - pool, - approval_id, - "approved", - req.response.as_deref(), - auth.owner_id, - ) - .await - { - Ok(approval) => Json(approval).into_response(), - Err(e) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response(), - } - } - Err(e) => { - tracing::error!("Failed to process approval: {}", e); - ( - StatusCode::BAD_REQUEST, - Json(ApiError::new("APPROVAL_FAILED", &e.to_string())), - ) - .into_response() - } - } -} - -/// Deny a pending approval request -/// POST /api/v1/directives/:id/approvals/:approval_id/deny -pub async fn deny_request( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path((id, approval_id)): Path<(Uuid, Uuid)>, - Json(req): Json<ApprovalActionRequest>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - let engine = crate::orchestration::DirectiveEngine::new(pool.clone()); - match engine - .on_approval_resolved(approval_id, false, auth.owner_id) - .await - { - Ok(()) => { - match repository::resolve_approval( - pool, - approval_id, - "denied", - req.response.as_deref(), - auth.owner_id, - ) - .await - { - Ok(approval) => Json(approval).into_response(), - Err(e) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response(), - } - } - Err(e) => { - tracing::error!("Failed to process denial: {}", e); - ( - StatusCode::BAD_REQUEST, - Json(ApiError::new("DENIAL_FAILED", &e.to_string())), - ) - .into_response() - } - } -} - -// ============================================================================= -// Step Evaluation & Rework -// ============================================================================= - -/// Force re-evaluation of a step -/// POST /api/v1/directives/:id/steps/:step_id/evaluate -pub async fn evaluate_step( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path((id, step_id)): Path<(Uuid, Uuid)>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - // Set step to evaluating status - match repository::update_step_status(pool, step_id, "evaluating").await { - Ok(_) => {} - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - // Trigger evaluation via engine - let engine = crate::orchestration::DirectiveEngine::new(pool.clone()); - match engine.on_contract_completed(step_id).await { - Ok(()) => { - // Return updated step - match repository::get_chain_step(pool, step_id).await { - Ok(Some(step)) => Json(step).into_response(), - Ok(None) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Step not found")), - ) - .into_response(), - Err(e) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response(), - } - } - Err(e) => { - tracing::error!("Failed to evaluate step: {}", e); - ( - StatusCode::BAD_REQUEST, - Json(ApiError::new("EVALUATE_FAILED", &e.to_string())), - ) - .into_response() - } - } -} - -/// Trigger manual rework for a step -/// POST /api/v1/directives/:id/steps/:step_id/rework -pub async fn rework_step( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path((id, step_id)): Path<(Uuid, Uuid)>, - Json(req): Json<ReworkStepRequest>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - // Set step to rework status and increment rework count - match repository::update_step_status(pool, step_id, "rework").await { - Ok(_) => {} - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - let _ = repository::increment_step_rework_count(pool, step_id).await; - - // Emit rework event - let _ = repository::emit_directive_event( - pool, - id, - None, - Some(step_id), - "step_rework", - "info", - Some(serde_json::json!({ - "step_id": step_id, - "instructions": req.instructions, - "initiated_by": "user", - })), - "user", - Some(auth.owner_id), - ) - .await; - - // Return updated step - match repository::get_chain_step(pool, step_id).await { - Ok(Some(step)) => Json(step).into_response(), - Ok(None) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Step not found")), - ) - .into_response(), - Err(e) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response(), - } -} - -// ============================================================================= -// Auto-detect Verifiers -// ============================================================================= - -/// Auto-detect verifiers for a directive based on repository content -/// POST /api/v1/directives/:id/verifiers/auto-detect -pub async fn auto_detect_verifiers( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Get directive with ownership check - let directive = match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(d)) => d, - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - }; - - // Get repository path - let repo_path = directive - .local_path - .as_ref() - .map(std::path::PathBuf::from) - .unwrap_or_else(|| std::path::PathBuf::from(".")); - - // Auto-detect verifiers - let detected = crate::orchestration::auto_detect_verifiers(&repo_path).await; - - // Save detected verifiers to the database - let mut created = Vec::new(); - for verifier in &detected { - let info = verifier.info(); - match repository::create_directive_verifier( - pool, - id, - &info.name, - &info.verifier_type, - Some(&info.command), - info.working_directory.as_deref(), - true, // auto_detect - info.detect_files.clone(), - info.weight, - info.required, - ) - .await - { - Ok(v) => created.push(v), - Err(e) => { - tracing::warn!("Failed to create detected verifier '{}': {}", info.name, e); - } - } - } - - Json(serde_json::json!({ - "detected": created.len(), - "verifiers": created, - })) - .into_response() -} - -// ============================================================================= -// Requirements & Criteria -// ============================================================================= - -/// Update directive requirements -/// PUT /api/v1/directives/:id/requirements -pub async fn update_requirements( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, - Json(req): Json<UpdateRequirementsRequest>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Get directive with ownership check to get current version - let directive = match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(d)) => d, - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - }; - - // Build update request with just requirements - let update = UpdateDirectiveRequest { - title: None, - goal: None, - requirements: Some(serde_json::to_value(&req.requirements).unwrap_or_default()), - acceptance_criteria: None, - constraints: None, - external_dependencies: None, - autonomy_level: None, - confidence_threshold_green: None, - confidence_threshold_yellow: None, - max_total_cost_usd: None, - max_wall_time_minutes: None, - max_rework_cycles: None, - max_chain_regenerations: None, - version: directive.version, - }; - - match repository::update_directive_for_owner(pool, id, auth.owner_id, update).await { - Ok(directive) => Json(directive).into_response(), - Err(repository::RepositoryError::VersionConflict { expected, actual }) => ( - StatusCode::CONFLICT, - Json(ApiError::new( - "VERSION_CONFLICT", - &format!("Version conflict: expected {}, got {}", expected, actual), - )), - ) - .into_response(), - Err(e) => { - tracing::error!("Failed to update requirements: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Update directive acceptance criteria -/// PUT /api/v1/directives/:id/criteria -pub async fn update_criteria( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, - Json(req): Json<UpdateCriteriaRequest>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Get directive with ownership check to get current version - let directive = match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(d)) => d, - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - }; - - // Build update request with just acceptance criteria - let update = UpdateDirectiveRequest { - title: None, - goal: None, - requirements: None, - acceptance_criteria: Some( - serde_json::to_value(&req.acceptance_criteria).unwrap_or_default(), - ), - constraints: None, - external_dependencies: None, - autonomy_level: None, - confidence_threshold_green: None, - confidence_threshold_yellow: None, - max_total_cost_usd: None, - max_wall_time_minutes: None, - max_rework_cycles: None, - max_chain_regenerations: None, - version: directive.version, - }; - - match repository::update_directive_for_owner(pool, id, auth.owner_id, update).await { - Ok(directive) => Json(directive).into_response(), - Err(repository::RepositoryError::VersionConflict { expected, actual }) => ( - StatusCode::CONFLICT, - Json(ApiError::new( - "VERSION_CONFLICT", - &format!("Version conflict: expected {}, got {}", expected, actual), - )), - ) - .into_response(), - Err(e) => { - tracing::error!("Failed to update criteria: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -// ============================================================================= -// Spec Generation -// ============================================================================= - -/// Generate a specification from the directive's goal using LLM -/// POST /api/v1/directives/:id/generate-spec -pub async fn generate_spec( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Get directive with ownership check - let directive = match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(d)) => d, - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - }; - - // Use the planner to generate a spec from the goal - let planner = crate::orchestration::ChainPlanner::new(pool.clone()); - match planner.generate_spec(&directive).await { - Ok(spec) => { - // Update the directive with the generated spec - let update = UpdateDirectiveRequest { - title: spec.title, - goal: None, - requirements: Some(spec.requirements), - acceptance_criteria: Some(spec.acceptance_criteria), - constraints: spec.constraints, - external_dependencies: None, - autonomy_level: None, - confidence_threshold_green: None, - confidence_threshold_yellow: None, - max_total_cost_usd: None, - max_wall_time_minutes: None, - max_rework_cycles: None, - max_chain_regenerations: None, - version: directive.version, - }; - - match repository::update_directive_for_owner(pool, id, auth.owner_id, update).await { - Ok(updated) => Json(updated).into_response(), - Err(e) => { - tracing::error!("Failed to save generated spec: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - } - Err(e) => { - tracing::error!("Failed to generate spec: {}", e); - ( - StatusCode::BAD_REQUEST, - Json(ApiError::new("SPEC_GENERATION_FAILED", &e.to_string())), - ) - .into_response() - } - } -} diff --git a/makima/src/server/handlers/mesh_daemon.rs b/makima/src/server/handlers/mesh_daemon.rs index 9938145..beb4c15 100644 --- a/makima/src/server/handlers/mesh_daemon.rs +++ b/makima/src/server/handlers/mesh_daemon.rs @@ -1303,20 +1303,7 @@ async fn handle_daemon_connection(socket: WebSocket, state: SharedState, auth_re }), ).await; - // Check if this task's contract is a directive orchestrator - if let Some(contract_id) = updated_task.contract_id { - if let Ok(Some(directive)) = repository::get_directive_by_orchestrator_contract_id( - &pool, contract_id - ).await { - let engine = crate::orchestration::DirectiveEngine::new(pool.clone()); - if let Err(e) = engine.on_planning_complete(directive.id, success).await { - tracing::error!( - "Failed to handle planning completion for directive {}: {}", - directive.id, e - ); - } - } - } + // TODO: Directive engine integration (removed for reimplementation) } Ok(None) => { tracing::warn!( diff --git a/makima/src/server/handlers/mod.rs b/makima/src/server/handlers/mod.rs index d3fabf7..ae370c9 100644 --- a/makima/src/server/handlers/mod.rs +++ b/makima/src/server/handlers/mod.rs @@ -1,8 +1,6 @@ //! HTTP and WebSocket request handlers. pub mod api_keys; -// pub mod chains; // Removed - replaced by directives -pub mod directives; pub mod chat; pub mod contract_chat; pub mod contract_daemon; diff --git a/makima/src/server/mod.rs b/makima/src/server/mod.rs index 463a5f5..b7a4156 100644 --- a/makima/src/server/mod.rs +++ b/makima/src/server/mod.rs @@ -18,7 +18,7 @@ use tower_http::trace::TraceLayer; use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; -use crate::server::handlers::{api_keys, chat, contract_chat, contract_daemon, contract_discuss, contracts, directives, file_ws, files, history, listen, mesh, mesh_chat, mesh_daemon, mesh_merge, mesh_supervisor, mesh_ws, repository_history, speak, templates, transcript_analysis, users, versions}; +use crate::server::handlers::{api_keys, chat, contract_chat, contract_daemon, contract_discuss, contracts, file_ws, files, history, listen, mesh, mesh_chat, mesh_daemon, mesh_merge, mesh_supervisor, mesh_ws, repository_history, speak, templates, transcript_analysis, users, versions}; use crate::server::openapi::ApiDoc; use crate::server::state::SharedState; @@ -214,61 +214,6 @@ pub fn make_router(state: SharedState) -> Router { ) // Timeline endpoint (unified history for user) .route("/timeline", get(history::get_timeline)) - // Directive endpoints (replacement for chains) - .route( - "/directives", - get(directives::list_directives).post(directives::create_directive), - ) - .route( - "/directives/{id}", - get(directives::get_directive) - .put(directives::update_directive) - .delete(directives::archive_directive), - ) - .route("/directives/{id}/start", post(directives::start_directive)) - .route("/directives/{id}/pause", post(directives::pause_directive)) - .route("/directives/{id}/resume", post(directives::resume_directive)) - .route("/directives/{id}/stop", post(directives::stop_directive)) - .route("/directives/{id}/requirements", axum::routing::put(directives::update_requirements)) - .route("/directives/{id}/criteria", axum::routing::put(directives::update_criteria)) - .route("/directives/{id}/generate-spec", post(directives::generate_spec)) - // Directive chain management - .route("/directives/{id}/chain", get(directives::get_chain)) - .route("/directives/{id}/chain/graph", get(directives::get_chain_graph)) - .route("/directives/{id}/chain/replan", post(directives::replan_chain)) - // Directive step management - .route( - "/directives/{id}/chain/steps", - post(directives::add_step), - ) - .route( - "/directives/{id}/chain/steps/{step_id}", - get(directives::get_step) - .put(directives::update_step) - .delete(directives::delete_step), - ) - .route("/directives/{id}/chain/steps/{step_id}/skip", post(directives::skip_step)) - .route("/directives/{id}/chain/steps/{step_id}/evaluate", post(directives::evaluate_step)) - .route("/directives/{id}/chain/steps/{step_id}/rework", post(directives::rework_step)) - // Directive evaluations - .route("/directives/{id}/evaluations", get(directives::list_evaluations)) - // Directive events - .route("/directives/{id}/events", get(directives::list_events)) - .route("/directives/{id}/events/stream", get(directives::stream_events)) - // Directive verifiers - .route("/directives/{id}/verifiers/auto-detect", post(directives::auto_detect_verifiers)) - .route( - "/directives/{id}/verifiers", - get(directives::list_verifiers).post(directives::add_verifier), - ) - .route( - "/directives/{id}/verifiers/{verifier_id}", - axum::routing::put(directives::update_verifier), - ) - // Directive approvals - .route("/directives/{id}/approvals", get(directives::list_approvals)) - .route("/directives/{id}/approvals/{approval_id}/approve", post(directives::approve_request)) - .route("/directives/{id}/approvals/{approval_id}/deny", post(directives::deny_request)) // Contract type templates (built-in only) .route("/contract-types", get(templates::list_contract_types)) // Settings endpoints |
