diff options
Diffstat (limited to 'makima')
| -rw-r--r-- | makima/frontend/src/components/NavStrip.tsx | 1 | ||||
| -rw-r--r-- | makima/frontend/src/components/directives/DirectiveDetail.tsx | 200 | ||||
| -rw-r--r-- | makima/frontend/src/components/directives/DirectiveList.tsx | 135 | ||||
| -rw-r--r-- | makima/frontend/src/hooks/useDirectives.ts | 108 | ||||
| -rw-r--r-- | makima/frontend/src/lib/api.ts | 158 | ||||
| -rw-r--r-- | makima/frontend/src/main.tsx | 17 | ||||
| -rw-r--r-- | makima/frontend/src/routes/directives.tsx | 209 | ||||
| -rw-r--r-- | makima/src/bin/makima.rs | 64 | ||||
| -rw-r--r-- | makima/src/daemon/api/directive.rs | 54 | ||||
| -rw-r--r-- | makima/src/daemon/api/mod.rs | 1 | ||||
| -rw-r--r-- | makima/src/daemon/cli/directive.rs | 40 | ||||
| -rw-r--r-- | makima/src/daemon/cli/mod.rs | 28 | ||||
| -rw-r--r-- | makima/src/daemon/skills/directive.md | 68 | ||||
| -rw-r--r-- | makima/src/daemon/skills/mod.rs | 4 | ||||
| -rw-r--r-- | makima/src/db/models.rs | 234 | ||||
| -rw-r--r-- | makima/src/db/repository.rs | 283 | ||||
| -rw-r--r-- | makima/src/server/handlers/directives.rs | 440 | ||||
| -rw-r--r-- | makima/src/server/handlers/mod.rs | 1 | ||||
| -rw-r--r-- | makima/src/server/mod.rs | 15 | ||||
| -rw-r--r-- | makima/src/server/openapi.rs | 41 |
20 files changed, 2084 insertions, 17 deletions
diff --git a/makima/frontend/src/components/NavStrip.tsx b/makima/frontend/src/components/NavStrip.tsx index fb95c7f..f7e67db 100644 --- a/makima/frontend/src/components/NavStrip.tsx +++ b/makima/frontend/src/components/NavStrip.tsx @@ -11,6 +11,7 @@ interface NavLink { const NAV_LINKS: NavLink[] = [ { label: "Listen", href: "/listen" }, { label: "Contracts", href: "/contracts", requiresAuth: true }, + { label: "Directives", href: "/directives", requiresAuth: true }, { label: "Board", href: "/workflow", requiresAuth: true }, { label: "Mesh", href: "/mesh", requiresAuth: true }, { label: "History", href: "/history", requiresAuth: true }, diff --git a/makima/frontend/src/components/directives/DirectiveDetail.tsx b/makima/frontend/src/components/directives/DirectiveDetail.tsx new file mode 100644 index 0000000..3634a79 --- /dev/null +++ b/makima/frontend/src/components/directives/DirectiveDetail.tsx @@ -0,0 +1,200 @@ +import type { + DirectiveWithChains, + DirectiveStatus, + DirectiveChain, +} from "../../lib/api"; + +interface DirectiveDetailProps { + directive: DirectiveWithChains; + onBack: () => void; + onDelete?: (id: string) => void; +} + +const statusColors: Record<DirectiveStatus, string> = { + draft: "text-[#888]", + planning: "text-yellow-400", + active: "text-green-400", + paused: "text-orange-400", + completed: "text-blue-400", + archived: "text-[#555]", + failed: "text-red-400", +}; + +function ChainCard({ chain }: { chain: DirectiveChain }) { + return ( + <div className="p-3 border border-dashed border-[rgba(117,170,252,0.25)] bg-[rgba(117,170,252,0.03)]"> + <div className="flex items-center justify-between mb-1"> + <span className="font-mono text-xs text-[#dbe7ff]">{chain.name}</span> + <span className="font-mono text-[10px] text-[#7788aa] uppercase"> + gen {chain.generation} · {chain.status} + </span> + </div> + {chain.description && ( + <p className="font-mono text-[11px] text-[#7788aa] mb-1"> + {chain.description} + </p> + )} + <div className="flex gap-3 font-mono text-[10px] text-[#7788aa]"> + <span> + {chain.completedSteps}/{chain.totalSteps} steps + </span> + {chain.failedSteps > 0 && ( + <span className="text-red-400">{chain.failedSteps} failed</span> + )} + {chain.currentConfidence != null && ( + <span>confidence: {(chain.currentConfidence * 100).toFixed(0)}%</span> + )} + </div> + </div> + ); +} + +function JsonSection({ + label, + data, +}: { + label: string; + data: unknown[] | unknown; +}) { + const items = Array.isArray(data) ? data : []; + if (items.length === 0) return null; + + return ( + <div> + <h4 className="font-mono text-[10px] text-[#75aafc] uppercase tracking-wider mb-1"> + {label} + </h4> + <div className="font-mono text-xs text-[#9bb8d8] bg-[rgba(0,0,0,0.2)] p-2 max-h-32 overflow-y-auto"> + {items.map((item, i) => ( + <div key={i} className="mb-0.5"> + {typeof item === "string" ? item : JSON.stringify(item)} + </div> + ))} + </div> + </div> + ); +} + +export function DirectiveDetail({ + directive, + onBack, + onDelete, +}: DirectiveDetailProps) { + return ( + <div className="panel h-full flex flex-col"> + {/* Header */} + <div className="p-4 border-b border-dashed border-[rgba(117,170,252,0.35)]"> + <div className="flex items-center gap-2 mb-2"> + <button + onClick={onBack} + className="font-mono text-xs text-[#75aafc] hover:text-white transition-colors" + > + ← Back + </button> + <span + className={`font-mono text-[10px] uppercase ${ + statusColors[directive.status as DirectiveStatus] || "text-[#888]" + }`} + > + {directive.status} + </span> + <span className="font-mono text-[10px] text-[#7788aa]"> + v{directive.version} + </span> + {onDelete && ( + <button + onClick={() => onDelete(directive.id)} + className="ml-auto font-mono text-[10px] text-red-400 hover:text-red-300 transition-colors uppercase" + > + Delete + </button> + )} + </div> + <h2 className="font-mono text-sm text-[#dbe7ff]"> + {directive.title} + </h2> + </div> + + {/* Content */} + <div className="flex-1 overflow-y-auto p-4 space-y-4"> + {/* Goal */} + <div> + <h4 className="font-mono text-[10px] text-[#75aafc] uppercase tracking-wider mb-1"> + Goal + </h4> + <p className="font-mono text-xs text-[#9bb8d8] whitespace-pre-wrap"> + {directive.goal} + </p> + </div> + + {/* Config */} + <div className="grid grid-cols-2 gap-2"> + <div> + <span className="font-mono text-[10px] text-[#7788aa] uppercase"> + Autonomy + </span> + <div className="font-mono text-xs text-[#dbe7ff]"> + {directive.autonomyLevel} + </div> + </div> + <div> + <span className="font-mono text-[10px] text-[#7788aa] uppercase"> + Chains + </span> + <div className="font-mono text-xs text-[#dbe7ff]"> + {directive.chainGenerationCount} generated + </div> + </div> + <div> + <span className="font-mono text-[10px] text-[#7788aa] uppercase"> + Cost + </span> + <div className="font-mono text-xs text-[#dbe7ff]"> + ${directive.totalCostUsd.toFixed(2)} + </div> + </div> + {directive.repositoryUrl && ( + <div> + <span className="font-mono text-[10px] text-[#7788aa] uppercase"> + Repository + </span> + <div className="font-mono text-xs text-[#dbe7ff] truncate"> + {directive.repositoryUrl} + </div> + </div> + )} + </div> + + {/* Structured sections */} + <JsonSection label="Requirements" data={directive.requirements} /> + <JsonSection + label="Acceptance Criteria" + data={directive.acceptanceCriteria} + /> + <JsonSection label="Constraints" data={directive.constraints} /> + <JsonSection + label="External Dependencies" + data={directive.externalDependencies} + /> + + {/* Chains */} + <div> + <h4 className="font-mono text-[10px] text-[#75aafc] uppercase tracking-wider mb-2"> + Chains ({directive.chains.length}) + </h4> + {directive.chains.length === 0 ? ( + <p className="font-mono text-xs text-[#7788aa]"> + No chains yet. Chains are created during planning. + </p> + ) : ( + <div className="space-y-2"> + {directive.chains.map((chain) => ( + <ChainCard key={chain.id} chain={chain} /> + ))} + </div> + )} + </div> + </div> + </div> + ); +} diff --git a/makima/frontend/src/components/directives/DirectiveList.tsx b/makima/frontend/src/components/directives/DirectiveList.tsx new file mode 100644 index 0000000..a900b7b --- /dev/null +++ b/makima/frontend/src/components/directives/DirectiveList.tsx @@ -0,0 +1,135 @@ +import { useState } from "react"; +import type { DirectiveSummary, DirectiveStatus } from "../../lib/api"; + +interface DirectiveListProps { + directives: DirectiveSummary[]; + loading: boolean; + onSelect: (id: string) => void; + onCreate: () => void; + onDelete?: (directive: DirectiveSummary) => void; + selectedId?: string; +} + +const statusColors: Record<DirectiveStatus, string> = { + draft: "text-[#888]", + planning: "text-yellow-400", + active: "text-green-400", + paused: "text-orange-400", + completed: "text-blue-400", + archived: "text-[#555]", + failed: "text-red-400", +}; + +export function DirectiveList({ + directives, + loading, + onSelect, + onCreate, + onDelete, + selectedId, +}: DirectiveListProps) { + const [filter, setFilter] = useState<DirectiveStatus | "all">("all"); + + const filteredDirectives = + filter === "all" + ? directives + : directives.filter((d) => d.status === filter); + + if (loading) { + return ( + <div className="panel h-full flex items-center justify-center"> + <div className="font-mono text-[#9bc3ff] text-sm">Loading...</div> + </div> + ); + } + + return ( + <div className="panel h-full flex flex-col"> + {/* Header */} + <div className="p-4 border-b border-dashed border-[rgba(117,170,252,0.35)]"> + <div className="flex items-center justify-between mb-3"> + <h2 className="font-mono text-sm text-[#75aafc] uppercase tracking-wider"> + Directives + </h2> + <button + onClick={onCreate} + className="px-3 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase" + > + + New + </button> + </div> + {/* Filter tabs */} + <div className="flex gap-1 flex-wrap"> + {(["all", "draft", "planning", "active", "paused", "completed", "failed"] as const).map( + (status) => ( + <button + key={status} + onClick={() => setFilter(status)} + className={`px-2 py-0.5 font-mono text-[10px] uppercase tracking-wider border transition-colors ${ + filter === status + ? "bg-[#0f3c78] border-[#3f6fb3] text-[#dbe7ff]" + : "bg-transparent border-[rgba(117,170,252,0.2)] text-[#7788aa] hover:border-[rgba(117,170,252,0.4)]" + }`} + > + {status} + </button> + ) + )} + </div> + </div> + + {/* List */} + <div className="flex-1 overflow-y-auto"> + {filteredDirectives.length === 0 ? ( + <div className="p-4 text-center"> + <p className="font-mono text-sm text-[#7788aa]"> + {directives.length === 0 + ? "No directives yet" + : "No matching directives"} + </p> + </div> + ) : ( + filteredDirectives.map((directive) => ( + <div + key={directive.id} + onClick={() => onSelect(directive.id)} + onContextMenu={(e) => { + if (onDelete) { + e.preventDefault(); + } + }} + className={`p-3 border-b border-dashed border-[rgba(117,170,252,0.15)] cursor-pointer transition-colors hover:bg-[rgba(117,170,252,0.05)] ${ + selectedId === directive.id + ? "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} + </div> + <div className="font-mono text-xs text-[#7788aa] mt-0.5 line-clamp-2"> + {directive.goal} + </div> + </div> + <div className="flex flex-col items-end gap-1 shrink-0"> + <span + className={`font-mono text-[10px] uppercase ${ + statusColors[directive.status] || "text-[#888]" + }`} + > + {directive.status} + </span> + <span className="font-mono text-[10px] text-[#7788aa]"> + {directive.chainCount}ch / {directive.stepCount}st + </span> + </div> + </div> + </div> + )) + )} + </div> + </div> + ); +} diff --git a/makima/frontend/src/hooks/useDirectives.ts b/makima/frontend/src/hooks/useDirectives.ts new file mode 100644 index 0000000..001cf89 --- /dev/null +++ b/makima/frontend/src/hooks/useDirectives.ts @@ -0,0 +1,108 @@ +import { useState, useCallback, useEffect } from "react"; +import { + listDirectives, + getDirective, + createDirective, + updateDirective, + deleteDirective, + type DirectiveSummary, + type DirectiveWithChains, + type CreateDirectiveRequest, + type UpdateDirectiveRequest, +} from "../lib/api"; + +export function useDirectives() { + const [directives, setDirectives] = useState<DirectiveSummary[]>([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState<string | null>(null); + + const fetchDirectives = useCallback(async () => { + setLoading(true); + setError(null); + try { + const response = await listDirectives(); + setDirectives(response.directives); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to fetch directives"); + } finally { + setLoading(false); + } + }, []); + + const fetchDirective = useCallback( + async (id: string): Promise<DirectiveWithChains | null> => { + setError(null); + try { + return await getDirective(id); + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to fetch directive"); + return null; + } + }, + [] + ); + + const saveDirective = useCallback( + async (data: CreateDirectiveRequest): Promise<DirectiveSummary | null> => { + setError(null); + try { + const directive = await createDirective(data); + await fetchDirectives(); + return directive as unknown as DirectiveSummary; + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to create directive"); + return null; + } + }, + [fetchDirectives] + ); + + const editDirective = useCallback( + async ( + id: string, + data: UpdateDirectiveRequest + ): Promise<DirectiveSummary | null> => { + setError(null); + try { + const directive = await updateDirective(id, data); + await fetchDirectives(); + return directive as unknown as DirectiveSummary; + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to update directive"); + return null; + } + }, + [fetchDirectives] + ); + + const removeDirective = useCallback( + async (id: string): Promise<boolean> => { + setError(null); + try { + await deleteDirective(id); + await fetchDirectives(); + return true; + } catch (e) { + setError(e instanceof Error ? e.message : "Failed to delete directive"); + return false; + } + }, + [fetchDirectives] + ); + + // Initial fetch + useEffect(() => { + fetchDirectives(); + }, [fetchDirectives]); + + return { + directives, + loading, + error, + fetchDirectives, + fetchDirective, + saveDirective, + editDirective, + removeDirective, + }; +} diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts index 9f5ff88..6c450eb 100644 --- a/makima/frontend/src/lib/api.ts +++ b/makima/frontend/src/lib/api.ts @@ -3003,3 +3003,161 @@ export async function listTaskPatches(taskId: string, contractId: string): Promi return res.json(); } +// ============================================================================= +// Directive Types & API +// ============================================================================= + +export type DirectiveStatus = "draft" | "planning" | "active" | "paused" | "completed" | "archived" | "failed"; +export type AutonomyLevel = "full_auto" | "guardrails" | "manual"; + +export interface DirectiveSummary { + id: string; + title: string; + goal: string; + status: DirectiveStatus; + autonomyLevel: AutonomyLevel; + chainCount: number; + stepCount: number; + totalCostUsd: number; + version: number; + createdAt: string; + updatedAt: string; +} + +export interface Directive { + id: string; + ownerId: string; + title: string; + goal: string; + requirements: unknown[]; + acceptanceCriteria: unknown[]; + constraints: unknown[]; + externalDependencies: unknown[]; + status: DirectiveStatus; + autonomyLevel: AutonomyLevel; + confidenceThresholdGreen: number; + confidenceThresholdYellow: number; + maxTotalCostUsd: number | null; + maxWallTimeMinutes: number | null; + maxReworkCycles: number | null; + maxChainRegenerations: number | null; + repositoryUrl: string | null; + localPath: string | null; + baseBranch: string | null; + orchestratorContractId: string | null; + currentChainId: string | null; + chainGenerationCount: number; + totalCostUsd: number; + startedAt: string | null; + completedAt: string | null; + version: number; + createdAt: string; + updatedAt: string; +} + +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; +} + +export interface DirectiveWithChains extends Directive { + chains: DirectiveChain[]; +} + +export interface DirectiveListResponse { + directives: DirectiveSummary[]; + total: number; +} + +export interface CreateDirectiveRequest { + title: string; + goal: string; + requirements?: unknown[]; + acceptanceCriteria?: unknown[]; + constraints?: unknown[]; + externalDependencies?: unknown[]; + autonomyLevel?: AutonomyLevel; + repositoryUrl?: string; + localPath?: string; + baseBranch?: string; +} + +export interface UpdateDirectiveRequest { + title?: string; + goal?: string; + status?: DirectiveStatus; + autonomyLevel?: AutonomyLevel; + version?: number; +} + +export async function listDirectives(): Promise<DirectiveListResponse> { + const res = await authFetch(`${API_BASE}/api/v1/directives`); + if (!res.ok) { + throw new Error(`Failed to list directives: ${res.statusText}`); + } + return res.json(); +} + +export async function getDirective(id: string): Promise<DirectiveWithChains> { + const res = await authFetch(`${API_BASE}/api/v1/directives/${id}`); + if (!res.ok) { + throw new Error(`Failed to get directive: ${res.statusText}`); + } + return res.json(); +} + +export async function createDirective( + data: CreateDirectiveRequest +): Promise<Directive> { + const res = await authFetch(`${API_BASE}/api/v1/directives`, { + method: "POST", + body: JSON.stringify(data), + }); + if (!res.ok) { + throw new Error(`Failed to create directive: ${res.statusText}`); + } + return res.json(); +} + +export async function updateDirective( + id: string, + data: UpdateDirectiveRequest +): Promise<Directive> { + const res = await authFetch(`${API_BASE}/api/v1/directives/${id}`, { + method: "PUT", + body: JSON.stringify(data), + }); + if (res.status === 409) { + const conflict = (await res.json()) as ConflictErrorResponse; + throw new VersionConflictError(conflict); + } + if (!res.ok) { + throw new Error(`Failed to update directive: ${res.statusText}`); + } + return res.json(); +} + +export async function deleteDirective(id: string): Promise<void> { + const res = await authFetch(`${API_BASE}/api/v1/directives/${id}`, { + method: "DELETE", + }); + if (!res.ok) { + throw new Error(`Failed to delete directive: ${res.statusText}`); + } +} + diff --git a/makima/frontend/src/main.tsx b/makima/frontend/src/main.tsx index 50fffe4..f07a143 100644 --- a/makima/frontend/src/main.tsx +++ b/makima/frontend/src/main.tsx @@ -18,6 +18,7 @@ import HistoryPage from "./routes/history"; import LoginPage from "./routes/login"; import SettingsPage from "./routes/settings"; import ContractFilePage from "./routes/contract-file"; +import DirectivesPage from "./routes/directives"; import SpeakPage from "./routes/speak"; createRoot(document.getElementById("root")!).render( @@ -80,6 +81,22 @@ createRoot(document.getElementById("root")!).render( } /> <Route + path="/directives" + element={ + <ProtectedRoute> + <DirectivesPage /> + </ProtectedRoute> + } + /> + <Route + path="/directives/:id" + element={ + <ProtectedRoute> + <DirectivesPage /> + </ProtectedRoute> + } + /> + <Route path="/workflow" element={ <ProtectedRoute> diff --git a/makima/frontend/src/routes/directives.tsx b/makima/frontend/src/routes/directives.tsx new file mode 100644 index 0000000..89535e2 --- /dev/null +++ b/makima/frontend/src/routes/directives.tsx @@ -0,0 +1,209 @@ +import { useState, useCallback, useEffect } from "react"; +import { useParams, useNavigate } from "react-router"; +import { Masthead } from "../components/Masthead"; +import { DirectiveList } from "../components/directives/DirectiveList"; +import { DirectiveDetail } from "../components/directives/DirectiveDetail"; +import { useDirectives } from "../hooks/useDirectives"; +import { useAuth } from "../contexts/AuthContext"; +import type { + DirectiveWithChains, + CreateDirectiveRequest, +} from "../lib/api"; + +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]); + + 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> + ); + } + + return ( + <div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]"> + <Masthead showNav /> + <DirectivesContent /> + </div> + ); +} + +function DirectivesContent() { + const { id } = useParams<{ id?: string }>(); + const navigate = useNavigate(); + const { + directives, + loading, + error, + fetchDirective, + saveDirective, + removeDirective, + } = useDirectives(); + + const [selectedDirective, setSelectedDirective] = + useState<DirectiveWithChains | null>(null); + const [detailLoading, setDetailLoading] = useState(false); + const [showCreateForm, setShowCreateForm] = useState(false); + const [createTitle, setCreateTitle] = useState(""); + const [createGoal, setCreateGoal] = useState(""); + const [createRepoUrl, setCreateRepoUrl] = useState(""); + + // Load directive when ID changes + useEffect(() => { + if (id) { + setDetailLoading(true); + fetchDirective(id).then((d) => { + setSelectedDirective(d); + setDetailLoading(false); + }); + } else { + setSelectedDirective(null); + } + }, [id, fetchDirective]); + + const handleSelect = useCallback( + (directiveId: string) => { + navigate(`/directives/${directiveId}`); + }, + [navigate] + ); + + const handleBack = useCallback(() => { + navigate("/directives"); + }, [navigate]); + + const handleCreate = useCallback(async () => { + if (!createTitle.trim() || !createGoal.trim()) return; + + const data: CreateDirectiveRequest = { + title: createTitle.trim(), + goal: createGoal.trim(), + }; + if (createRepoUrl.trim()) { + data.repositoryUrl = createRepoUrl.trim(); + } + + const result = await saveDirective(data); + if (result) { + setShowCreateForm(false); + setCreateTitle(""); + setCreateGoal(""); + setCreateRepoUrl(""); + } + }, [createTitle, createGoal, createRepoUrl, saveDirective]); + + const handleDelete = useCallback( + async (directiveId: string) => { + const ok = await removeDirective(directiveId); + if (ok && id === directiveId) { + navigate("/directives"); + } + }, + [removeDirective, id, navigate] + ); + + // Detail view + if (id) { + if (detailLoading) { + return ( + <main className="flex-1 flex items-center justify-center"> + <p className="text-[#7788aa] font-mono text-sm">Loading directive...</p> + </main> + ); + } + if (!selectedDirective) { + return ( + <main className="flex-1 flex items-center justify-center"> + <p className="text-[#7788aa] font-mono text-sm">Directive not found</p> + </main> + ); + } + return ( + <main className="flex-1 p-4 max-w-4xl mx-auto w-full"> + <DirectiveDetail + directive={selectedDirective} + onBack={handleBack} + onDelete={handleDelete} + /> + </main> + ); + } + + // List view + return ( + <main className="flex-1 p-4 max-w-4xl mx-auto w-full"> + {error && ( + <div className="mb-4 p-2 border border-red-400/30 bg-red-400/5 font-mono text-xs text-red-400"> + {error} + </div> + )} + + {showCreateForm && ( + <div className="mb-4 p-4 border border-dashed border-[rgba(117,170,252,0.35)] bg-[rgba(117,170,252,0.03)]"> + <h3 className="font-mono text-xs text-[#75aafc] uppercase tracking-wider mb-3"> + New Directive + </h3> + <div className="space-y-2"> + <input + type="text" + placeholder="Title" + value={createTitle} + onChange={(e) => setCreateTitle(e.target.value)} + className="w-full px-2 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0a1628] border border-[rgba(117,170,252,0.3)] focus:border-[#75aafc] outline-none" + /> + <textarea + placeholder="Goal - what should be accomplished?" + value={createGoal} + onChange={(e) => setCreateGoal(e.target.value)} + rows={3} + className="w-full px-2 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0a1628] border border-[rgba(117,170,252,0.3)] focus:border-[#75aafc] outline-none resize-none" + /> + <input + type="text" + placeholder="Repository URL (optional)" + value={createRepoUrl} + onChange={(e) => setCreateRepoUrl(e.target.value)} + className="w-full px-2 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0a1628] border border-[rgba(117,170,252,0.3)] focus:border-[#75aafc] outline-none" + /> + <div className="flex gap-2"> + <button + onClick={handleCreate} + disabled={!createTitle.trim() || !createGoal.trim()} + className="px-3 py-1.5 font-mono text-xs text-[#dbe7ff] bg-[#0f3c78] border border-[#3f6fb3] hover:bg-[#153667] transition-colors uppercase disabled:opacity-50 disabled:cursor-not-allowed" + > + Create + </button> + <button + onClick={() => setShowCreateForm(false)} + className="px-3 py-1.5 font-mono text-xs text-[#7788aa] hover:text-[#dbe7ff] transition-colors uppercase" + > + Cancel + </button> + </div> + </div> + </div> + )} + + <DirectiveList + directives={directives} + loading={loading} + onSelect={handleSelect} + onCreate={() => setShowCreateForm(true)} + onDelete={(d) => handleDelete(d.id)} + selectedId={id} + /> + </main> + ); +} diff --git a/makima/src/bin/makima.rs b/makima/src/bin/makima.rs index ee5895c..308d689 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, - SupervisorCommand, ViewArgs, + DirectiveCommand, SupervisorCommand, ViewArgs, }; use makima::daemon::tui::{self, Action, App, ListItem, ViewType, TuiWsClient, WsEvent, OutputLine, OutputMessageType, WsConnectionState, RepositorySuggestion}; use makima::daemon::config::{DaemonConfig, RepoEntry}; @@ -29,6 +29,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { Commands::Daemon(args) => run_daemon(args).await, Commands::Supervisor(cmd) => run_supervisor(cmd).await, Commands::Contract(cmd) => run_contract(cmd).await, + Commands::Directive(cmd) => run_directive(cmd).await, Commands::View(args) => run_view(args).await, Commands::Config(cmd) => run_config(cmd).await, } @@ -711,6 +712,67 @@ async fn run_contract( Ok(()) } +/// Run a directive subcommand. +async fn run_directive( + cmd: DirectiveCommand, +) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { + match cmd { + DirectiveCommand::Status(args) => { + let client = ApiClient::new(args.api_url, args.api_key)?; + let result = client.directive_status(args.directive_id).await?; + println!("{}", serde_json::to_string(&result.0)?); + } + DirectiveCommand::Goals(args) => { + let client = ApiClient::new(args.api_url, args.api_key)?; + let result = client.directive_status(args.directive_id).await?; + // Extract goal-related fields from directive + let directive = &result.0; + let goals = serde_json::json!({ + "goal": directive.get("goal"), + "requirements": directive.get("requirements"), + "acceptanceCriteria": directive.get("acceptanceCriteria"), + "constraints": directive.get("constraints"), + "externalDependencies": directive.get("externalDependencies"), + }); + println!("{}", serde_json::to_string(&goals)?); + } + DirectiveCommand::Chains(args) => { + let client = ApiClient::new(args.api_url, args.api_key)?; + let result = client.directive_chains(args.directive_id).await?; + println!("{}", serde_json::to_string(&result.0)?); + } + DirectiveCommand::Chain(args) => { + let client = ApiClient::new(args.common.api_url, args.common.api_key)?; + let result = client + .directive_chain(args.common.directive_id, args.chain_id) + .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 + .directive_chain(args.common.directive_id, args.chain_id) + .await?; + // Extract steps from chain response + let steps = result.0.get("steps").cloned().unwrap_or(serde_json::json!([])); + println!("{}", serde_json::to_string(&steps)?); + } + DirectiveCommand::UpdateStatus(args) => { + let client = ApiClient::new(args.common.api_url, args.common.api_key)?; + let req = makima::daemon::api::directive::UpdateDirectiveRequest { + status: Some(args.status), + version: None, + }; + let result = client + .directive_update(args.common.directive_id, req) + .await?; + println!("{}", serde_json::to_string(&result.0)?); + } + } + + Ok(()) +} + /// Run the TUI view command. async fn run_view(args: ViewArgs) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { // Load CLI config for defaults diff --git a/makima/src/daemon/api/directive.rs b/makima/src/daemon/api/directive.rs new file mode 100644 index 0000000..0c8115a --- /dev/null +++ b/makima/src/daemon/api/directive.rs @@ -0,0 +1,54 @@ +//! Directive API methods. + +use serde::Serialize; +use uuid::Uuid; + +use super::client::{ApiClient, ApiError}; +use super::supervisor::JsonValue; + +/// Request to update a directive. +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UpdateDirectiveRequest { + #[serde(skip_serializing_if = "Option::is_none")] + pub status: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub version: Option<i32>, +} + +impl ApiClient { + /// Get directive status and details. + pub async fn directive_status(&self, directive_id: Uuid) -> Result<JsonValue, ApiError> { + self.get(&format!("/api/v1/directives/{}", directive_id)) + .await + } + + /// List chains for a directive. + pub async fn directive_chains(&self, directive_id: Uuid) -> Result<JsonValue, ApiError> { + self.get(&format!("/api/v1/directives/{}/chains", directive_id)) + .await + } + + /// Get a chain with its steps. + pub async fn directive_chain( + &self, + directive_id: Uuid, + chain_id: Uuid, + ) -> Result<JsonValue, ApiError> { + self.get(&format!( + "/api/v1/directives/{}/chains/{}", + directive_id, chain_id + )) + .await + } + + /// Update a directive. + pub async fn directive_update( + &self, + directive_id: Uuid, + req: UpdateDirectiveRequest, + ) -> Result<JsonValue, ApiError> { + self.put(&format!("/api/v1/directives/{}", directive_id), &req) + .await + } +} diff --git a/makima/src/daemon/api/mod.rs b/makima/src/daemon/api/mod.rs index 49d80e0..2d1efbf 100644 --- a/makima/src/daemon/api/mod.rs +++ b/makima/src/daemon/api/mod.rs @@ -2,6 +2,7 @@ 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 new file mode 100644 index 0000000..5ce88c5 --- /dev/null +++ b/makima/src/daemon/cli/directive.rs @@ -0,0 +1,40 @@ +//! Directive subcommand - directive orchestration commands. + +use clap::Args; +use uuid::Uuid; + +/// Common arguments for directive commands. +#[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, + + /// Directive ID + #[arg(long, env = "MAKIMA_DIRECTIVE_ID", global = true)] + pub directive_id: Uuid, +} + +/// Arguments for chain command (get specific chain). +#[derive(Args, Debug)] +pub struct ChainArgs { + #[command(flatten)] + pub common: DirectiveArgs, + + /// Chain ID to retrieve + pub chain_id: Uuid, +} + +/// Arguments for update-status command. +#[derive(Args, Debug)] +pub struct UpdateStatusArgs { + #[command(flatten)] + pub common: DirectiveArgs, + + /// New status (draft, planning, active, paused, completed, archived, failed) + pub status: String, +} diff --git a/makima/src/daemon/cli/mod.rs b/makima/src/daemon/cli/mod.rs index 0805edd..9fba216 100644 --- a/makima/src/daemon/cli/mod.rs +++ b/makima/src/daemon/cli/mod.rs @@ -3,6 +3,7 @@ pub mod config; pub mod contract; pub mod daemon; +pub mod directive; pub mod server; pub mod supervisor; pub mod view; @@ -12,6 +13,7 @@ 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; @@ -41,6 +43,10 @@ pub enum Commands { #[command(subcommand)] Contract(ContractCommand), + /// Directive commands for autonomous goal-driven execution + #[command(subcommand)] + Directive(DirectiveCommand), + /// Interactive TUI browser for contracts and tasks /// /// Provides a drill-down interface for browsing contracts, viewing their @@ -196,6 +202,28 @@ pub enum ContractCommand { CreateFile(contract::CreateFileArgs), } +/// Directive subcommands for autonomous goal-driven execution. +#[derive(Subcommand, Debug)] +pub enum DirectiveCommand { + /// Get directive status and details + Status(DirectiveArgs), + + /// Get goal, requirements, acceptance criteria + Goals(DirectiveArgs), + + /// List chains for the directive + Chains(DirectiveArgs), + + /// Get a chain with its steps + Chain(directive::ChainArgs), + + /// List steps in a chain + Steps(directive::ChainArgs), + + /// Update directive status + UpdateStatus(directive::UpdateStatusArgs), +} + 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 new file mode 100644 index 0000000..cdfdaa2 --- /dev/null +++ b/makima/src/daemon/skills/directive.md @@ -0,0 +1,68 @@ +--- +name: makima-directive +description: Directive orchestration tools for autonomous goal-driven execution. Use when working with directives, chains, steps, verifiers, and approvals. +--- + +# Makima Directive Commands + +These commands let orchestrators interact with directive state. Environment variables (`MAKIMA_API_URL`, `MAKIMA_API_KEY`, `MAKIMA_DIRECTIVE_ID`) are pre-configured by the daemon. + +## Status and Information + +### Get directive status +```bash +makima directive status +``` +Returns full directive details including status, autonomy level, thresholds, and tracking info. + +### Get directive goals +```bash +makima directive goals +``` +Returns the goal, requirements, acceptance criteria, constraints, and external dependencies. + +### List chains +```bash +makima directive chains +``` +Returns all chains (plan generations) for the directive, ordered by generation. + +### Get chain with steps +```bash +makima directive chain <chain_id> +``` +Returns a chain and all its steps with status, dependencies, and evaluation info. + +### List steps in a chain +```bash +makima directive steps <chain_id> +``` +Returns just the steps array from a chain. + +## Status Updates + +### Update directive status +```bash +makima directive update-status <status> +``` +Updates the directive status. Valid statuses: `draft`, `planning`, `active`, `paused`, `completed`, `archived`, `failed`. + +## Output Format + +All commands output JSON to stdout. + +Example workflow: +```bash +# Check directive details and goals +makima directive status +makima directive goals + +# List execution chains +makima directive chains + +# Get details of a specific chain +makima directive chain <chain_id> + +# Update status to active +makima directive update-status active +``` diff --git a/makima/src/daemon/skills/mod.rs b/makima/src/daemon/skills/mod.rs index 0b05f3a..6e5d0a8 100644 --- a/makima/src/daemon/skills/mod.rs +++ b/makima/src/daemon/skills/mod.rs @@ -9,8 +9,12 @@ 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 - directive orchestration commands +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 3b10cb5..ec4ee15 100644 --- a/makima/src/db/models.rs +++ b/makima/src/db/models.rs @@ -2690,3 +2690,237 @@ mod tests { assert_eq!(deserialized.progress, 50); } } + +// ============================================================================= +// Directive Types +// ============================================================================= + +/// Default autonomy level for directives +fn default_autonomy_level() -> String { + "guardrails".to_string() +} + +/// Default empty JSON array +fn default_json_array() -> serde_json::Value { + serde_json::json!([]) +} + +/// Default empty JSON object +fn default_json_object() -> serde_json::Value { + serde_json::json!({}) +} + +/// Default confidence threshold (green) +fn default_confidence_green() -> f64 { + 0.85 +} + +/// Default confidence threshold (yellow) +fn default_confidence_yellow() -> f64 { + 0.60 +} + +/// Default max rework cycles +fn default_max_rework_cycles() -> Option<i32> { + Some(3) +} + +/// Default max chain regenerations +fn default_max_chain_regenerations() -> Option<i32> { + Some(2) +} + +/// Full directive row from the database. +#[derive(Debug, Clone, FromRow, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct Directive { + pub id: Uuid, + pub owner_id: Uuid, + pub title: String, + pub goal: String, + #[sqlx(json)] + pub requirements: serde_json::Value, + #[sqlx(json)] + pub acceptance_criteria: serde_json::Value, + #[sqlx(json)] + pub constraints: serde_json::Value, + #[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>, +} + +/// Summary of a directive for list views. +#[derive(Debug, Clone, FromRow, Serialize, 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 chain_count: i64, + pub step_count: i64, + pub total_cost_usd: f64, + pub version: i32, + pub created_at: DateTime<Utc>, + pub updated_at: DateTime<Utc>, +} + +/// Response for directive list endpoint. +#[derive(Debug, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DirectiveListResponse { + pub directives: Vec<DirectiveSummary>, + pub total: i64, +} + +/// Request to create a new directive. +#[derive(Debug, Clone, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct CreateDirectiveRequest { + pub title: String, + pub goal: String, + #[serde(default = "default_json_array")] + pub requirements: serde_json::Value, + #[serde(default = "default_json_array")] + pub acceptance_criteria: serde_json::Value, + #[serde(default = "default_json_array")] + pub constraints: serde_json::Value, + #[serde(default = "default_json_array")] + pub external_dependencies: serde_json::Value, + #[serde(default = "default_autonomy_level")] + pub autonomy_level: String, + #[serde(default = "default_confidence_green")] + pub confidence_threshold_green: f64, + #[serde(default = "default_confidence_yellow")] + pub confidence_threshold_yellow: f64, + pub max_total_cost_usd: Option<f64>, + pub max_wall_time_minutes: Option<i32>, + #[serde(default = "default_max_rework_cycles")] + pub max_rework_cycles: Option<i32>, + #[serde(default = "default_max_chain_regenerations")] + pub max_chain_regenerations: Option<i32>, + pub repository_url: Option<String>, + pub local_path: Option<String>, + pub base_branch: Option<String>, +} + +/// Request to update an existing directive. +#[derive(Debug, Clone, 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 status: Option<String>, + 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 repository_url: Option<String>, + pub local_path: Option<String>, + pub base_branch: Option<String>, + /// Version for optimistic locking + pub version: Option<i32>, +} + +/// Directive with its chains for detail view. +#[derive(Debug, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct DirectiveWithChains { + #[serde(flatten)] + pub directive: Directive, + pub chains: Vec<DirectiveChain>, +} + +/// Full row from directive_chains table. +#[derive(Debug, Clone, FromRow, Serialize, 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>, +} + +/// Full row from chain_steps table. +#[derive(Debug, Clone, FromRow, Serialize, 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>, + 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>>, + #[sqlx(json)] + 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>, +} + +/// Chain with its steps for detail view. +#[derive(Debug, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct ChainWithSteps { + #[serde(flatten)] + pub chain: DirectiveChain, + pub steps: Vec<ChainStep>, +} diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs index 863d927..5949079 100644 --- a/makima/src/db/repository.rs +++ b/makima/src/db/repository.rs @@ -6,16 +6,17 @@ use sqlx::PgPool; use uuid::Uuid; use super::models::{ - CheckpointPatch, CheckpointPatchInfo, Contract, ContractChatConversation, + ChainStep, CheckpointPatch, CheckpointPatchInfo, Contract, ContractChatConversation, ContractChatMessageRecord, ContractEvent, ContractRepository, ContractSummary, ContractTypeTemplateRecord, ConversationMessage, ConversationSnapshot, - CreateContractRequest, CreateFileRequest, CreateTaskRequest, CreateTemplateRequest, - Daemon, DaemonTaskAssignment, DaemonWithCapacity, DeliverableDefinition, + CreateContractRequest, CreateDirectiveRequest, CreateFileRequest, CreateTaskRequest, + CreateTemplateRequest, Daemon, DaemonTaskAssignment, DaemonWithCapacity, + DeliverableDefinition, Directive, DirectiveChain, DirectiveSummary, File, FileSummary, FileVersion, HistoryEvent, HistoryQueryFilters, MeshChatConversation, MeshChatMessageRecord, PhaseChangeResult, PhaseConfig, PhaseDefinition, SupervisorHeartbeatRecord, SupervisorState, Task, TaskCheckpoint, - TaskEvent, TaskSummary, UpdateContractRequest, UpdateFileRequest, UpdateTaskRequest, - UpdateTemplateRequest, + TaskEvent, TaskSummary, UpdateContractRequest, UpdateDirectiveRequest, + UpdateFileRequest, UpdateTaskRequest, UpdateTemplateRequest, }; /// Repository error types. @@ -4910,3 +4911,275 @@ fn truncate_string(s: &str, max_len: usize) -> String { format!("{}...", &s[..max_len - 3]) } } + +// ============================================================================= +// Directive CRUD +// ============================================================================= + +/// Create a new directive, scoped to owner. +pub async fn create_directive_for_owner( + pool: &PgPool, + owner_id: Uuid, + req: CreateDirectiveRequest, +) -> Result<Directive, sqlx::Error> { + sqlx::query_as::<_, Directive>( + r#" + INSERT INTO directives ( + owner_id, title, goal, + requirements, acceptance_criteria, constraints, external_dependencies, + autonomy_level, confidence_threshold_green, confidence_threshold_yellow, + max_total_cost_usd, max_wall_time_minutes, max_rework_cycles, max_chain_regenerations, + repository_url, local_path, base_branch + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) + RETURNING * + "#, + ) + .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.repository_url) + .bind(&req.local_path) + .bind(&req.base_branch) + .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 +} + +/// List all directives for an owner, ordered by created_at DESC. +pub async fn list_directives_for_owner( + pool: &PgPool, + owner_id: Uuid, +) -> Result<Vec<DirectiveSummary>, sqlx::Error> { + sqlx::query_as::<_, DirectiveSummary>( + r#" + SELECT + d.id, d.title, d.goal, d.status, d.autonomy_level, + (SELECT COUNT(*) FROM directive_chains WHERE directive_id = d.id) as chain_count, + (SELECT COUNT(*) FROM chain_steps cs JOIN directive_chains dc ON cs.chain_id = dc.id WHERE dc.directive_id = d.id) as step_count, + d.total_cost_usd, d.version, d.created_at, d.updated_at + FROM directives d + WHERE d.owner_id = $1 + ORDER BY d.created_at DESC + "#, + ) + .bind(owner_id) + .fetch_all(pool) + .await +} + +/// Update a directive by ID with optimistic locking, scoped to owner. +pub async fn update_directive_for_owner( + pool: &PgPool, + id: Uuid, + owner_id: Uuid, + req: UpdateDirectiveRequest, +) -> Result<Option<Directive>, RepositoryError> { + let existing = get_directive_for_owner(pool, id, owner_id).await?; + let Some(existing) = existing else { + return Ok(None); + }; + + // Check version if provided (optimistic locking) + if let Some(expected_version) = req.version { + if existing.version != expected_version { + return Err(RepositoryError::VersionConflict { + expected: expected_version, + actual: existing.version, + }); + } + } + + // Apply updates + let title = req.title.unwrap_or(existing.title); + let goal = req.goal.unwrap_or(existing.goal); + let requirements = req.requirements.unwrap_or(existing.requirements); + let acceptance_criteria = req.acceptance_criteria.unwrap_or(existing.acceptance_criteria); + let constraints = req.constraints.unwrap_or(existing.constraints); + let external_dependencies = req.external_dependencies.unwrap_or(existing.external_dependencies); + let status = req.status.unwrap_or(existing.status); + let autonomy_level = req.autonomy_level.unwrap_or(existing.autonomy_level); + let confidence_threshold_green = req.confidence_threshold_green.unwrap_or(existing.confidence_threshold_green); + let confidence_threshold_yellow = req.confidence_threshold_yellow.unwrap_or(existing.confidence_threshold_yellow); + let max_total_cost_usd = req.max_total_cost_usd.or(existing.max_total_cost_usd); + let max_wall_time_minutes = req.max_wall_time_minutes.or(existing.max_wall_time_minutes); + let max_rework_cycles = req.max_rework_cycles.or(existing.max_rework_cycles); + let max_chain_regenerations = req.max_chain_regenerations.or(existing.max_chain_regenerations); + let repository_url = req.repository_url.or(existing.repository_url); + let local_path = req.local_path.or(existing.local_path); + let base_branch = req.base_branch.or(existing.base_branch); + + let result = if req.version.is_some() { + sqlx::query_as::<_, Directive>( + r#" + UPDATE directives + SET title = $3, goal = $4, + requirements = $5, acceptance_criteria = $6, constraints = $7, external_dependencies = $8, + status = $9, autonomy_level = $10, + confidence_threshold_green = $11, confidence_threshold_yellow = $12, + max_total_cost_usd = $13, max_wall_time_minutes = $14, + max_rework_cycles = $15, max_chain_regenerations = $16, + repository_url = $17, local_path = $18, base_branch = $19, + version = version + 1, updated_at = NOW() + WHERE id = $1 AND owner_id = $2 AND version = $20 + RETURNING * + "#, + ) + .bind(id) + .bind(owner_id) + .bind(&title) + .bind(&goal) + .bind(&requirements) + .bind(&acceptance_criteria) + .bind(&constraints) + .bind(&external_dependencies) + .bind(&status) + .bind(&autonomy_level) + .bind(confidence_threshold_green) + .bind(confidence_threshold_yellow) + .bind(max_total_cost_usd) + .bind(max_wall_time_minutes) + .bind(max_rework_cycles) + .bind(max_chain_regenerations) + .bind(&repository_url) + .bind(&local_path) + .bind(&base_branch) + .bind(req.version.unwrap()) + .fetch_optional(pool) + .await? + } else { + sqlx::query_as::<_, Directive>( + r#" + UPDATE directives + SET title = $3, goal = $4, + requirements = $5, acceptance_criteria = $6, constraints = $7, external_dependencies = $8, + status = $9, autonomy_level = $10, + confidence_threshold_green = $11, confidence_threshold_yellow = $12, + max_total_cost_usd = $13, max_wall_time_minutes = $14, + max_rework_cycles = $15, max_chain_regenerations = $16, + repository_url = $17, local_path = $18, base_branch = $19, + version = version + 1, updated_at = NOW() + WHERE id = $1 AND owner_id = $2 + RETURNING * + "#, + ) + .bind(id) + .bind(owner_id) + .bind(&title) + .bind(&goal) + .bind(&requirements) + .bind(&acceptance_criteria) + .bind(&constraints) + .bind(&external_dependencies) + .bind(&status) + .bind(&autonomy_level) + .bind(confidence_threshold_green) + .bind(confidence_threshold_yellow) + .bind(max_total_cost_usd) + .bind(max_wall_time_minutes) + .bind(max_rework_cycles) + .bind(max_chain_regenerations) + .bind(&repository_url) + .bind(&local_path) + .bind(&base_branch) + .fetch_optional(pool) + .await? + }; + + // If versioned update returned None, there was a race condition + if result.is_none() && req.version.is_some() { + if let Some(current) = get_directive_for_owner(pool, id, owner_id).await? { + return Err(RepositoryError::VersionConflict { + expected: req.version.unwrap(), + actual: current.version, + }); + } + } + + Ok(result) +} + +/// Delete a directive by ID, scoped to owner. +pub async fn delete_directive_for_owner( + pool: &PgPool, + id: Uuid, + owner_id: Uuid, +) -> Result<bool, sqlx::Error> { + let result = sqlx::query( + r#" + DELETE FROM directives + WHERE id = $1 AND owner_id = $2 + "#, + ) + .bind(id) + .bind(owner_id) + .execute(pool) + .await?; + + Ok(result.rows_affected() > 0) +} + +/// List chains for a directive (read-only). +pub async fn list_chains_for_directive( + pool: &PgPool, + directive_id: Uuid, +) -> Result<Vec<DirectiveChain>, sqlx::Error> { + sqlx::query_as::<_, DirectiveChain>( + r#" + SELECT * + FROM directive_chains + WHERE directive_id = $1 + ORDER BY generation DESC, created_at DESC + "#, + ) + .bind(directive_id) + .fetch_all(pool) + .await +} + +/// List steps for a chain (read-only). +pub async fn list_steps_for_chain( + pool: &PgPool, + chain_id: Uuid, +) -> Result<Vec<ChainStep>, sqlx::Error> { + sqlx::query_as::<_, ChainStep>( + r#" + SELECT * + FROM chain_steps + WHERE chain_id = $1 + ORDER BY order_index ASC, created_at ASC + "#, + ) + .bind(chain_id) + .fetch_all(pool) + .await +} diff --git a/makima/src/server/handlers/directives.rs b/makima/src/server/handlers/directives.rs new file mode 100644 index 0000000..a74f8ff --- /dev/null +++ b/makima/src/server/handlers/directives.rs @@ -0,0 +1,440 @@ +//! HTTP handlers for directive CRUD operations. + +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::IntoResponse, + Json, +}; +use uuid::Uuid; + +use crate::db::models::{ + ChainWithSteps, CreateDirectiveRequest, Directive, DirectiveChain, + DirectiveListResponse, DirectiveWithChains, UpdateDirectiveRequest, +}; +use crate::db::repository::{self, RepositoryError}; +use crate::server::auth::Authenticated; +use crate::server::messages::ApiError; +use crate::server::state::SharedState; + +/// List all directives for the authenticated user's owner. +#[utoipa::path( + get, + path = "/api/v1/directives", + responses( + (status = 200, description = "List of directives", body = DirectiveListResponse), + (status = 401, description = "Unauthorized", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError), + ), + security( + ("bearer_auth" = []), + ("api_key" = []) + ), + tag = "Directives" +)] +pub async fn list_directives( + State(state): State<SharedState>, + Authenticated(auth): Authenticated, +) -> 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).await { + Ok(directives) => { + let total = directives.len() as i64; + Json(DirectiveListResponse { directives, 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 by ID with its chains. +#[utoipa::path( + get, + path = "/api/v1/directives/{id}", + params( + ("id" = Uuid, Path, description = "Directive ID") + ), + responses( + (status = 200, description = "Directive details with chains", body = DirectiveWithChains), + (status = 401, description = "Unauthorized", body = ApiError), + (status = 404, description = "Directive not found", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError), + ), + security( + ("bearer_auth" = []), + ("api_key" = []) + ), + tag = "Directives" +)] +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(); + }; + + 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) => { + tracing::error!("Failed to get directive {}: {}", id, e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response(); + } + }; + + let chains = match repository::list_chains_for_directive(pool, id).await { + Ok(c) => c, + Err(e) => { + tracing::warn!("Failed to get chains for directive {}: {}", id, e); + Vec::new() + } + }; + + Json(DirectiveWithChains { directive, chains }).into_response() +} + +/// Create a new directive. +#[utoipa::path( + post, + path = "/api/v1/directives", + request_body = CreateDirectiveRequest, + responses( + (status = 201, description = "Directive created", body = Directive), + (status = 400, description = "Invalid request", body = ApiError), + (status = 401, description = "Unauthorized", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError), + ), + security( + ("bearer_auth" = []), + ("api_key" = []) + ), + tag = "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) => (StatusCode::CREATED, Json(directive)).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() + } + } +} + +/// Update an existing directive. +#[utoipa::path( + put, + path = "/api/v1/directives/{id}", + params( + ("id" = Uuid, Path, description = "Directive ID") + ), + request_body = UpdateDirectiveRequest, + responses( + (status = 200, description = "Directive updated", body = Directive), + (status = 401, description = "Unauthorized", body = ApiError), + (status = 404, description = "Directive not found", body = ApiError), + (status = 409, description = "Version conflict", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError), + ), + security( + ("bearer_auth" = []), + ("api_key" = []) + ), + tag = "Directives" +)] +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(Some(directive)) => Json(directive).into_response(), + Ok(None) => ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "Directive not found")), + ) + .into_response(), + Err(RepositoryError::VersionConflict { expected, actual }) => ( + StatusCode::CONFLICT, + Json(ApiError::new( + "VERSION_CONFLICT", + format!( + "Version conflict: expected {}, actual {}", + expected, actual + ), + )), + ) + .into_response(), + Err(RepositoryError::Database(e)) => { + tracing::error!("Failed to update directive {}: {}", id, e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response() + } + } +} + +/// Delete a directive. +#[utoipa::path( + delete, + path = "/api/v1/directives/{id}", + params( + ("id" = Uuid, Path, description = "Directive ID") + ), + responses( + (status = 204, description = "Directive deleted"), + (status = 401, description = "Unauthorized", body = ApiError), + (status = 404, description = "Directive not found", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError), + ), + security( + ("bearer_auth" = []), + ("api_key" = []) + ), + tag = "Directives" +)] +pub async fn delete_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::delete_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 delete directive {}: {}", id, e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response() + } + } +} + +/// List chains for a directive. +#[utoipa::path( + get, + path = "/api/v1/directives/{id}/chains", + params( + ("id" = Uuid, Path, description = "Directive ID") + ), + responses( + (status = 200, description = "List of chains", body = Vec<DirectiveChain>), + (status = 401, description = "Unauthorized", body = ApiError), + (status = 404, description = "Directive not found", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError), + ), + security( + ("bearer_auth" = []), + ("api_key" = []) + ), + tag = "Directives" +)] +pub async fn list_chains( + 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 directive exists and belongs to owner + 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) => { + tracing::error!("Failed to get directive {}: {}", id, e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response(); + } + } + + match repository::list_chains_for_directive(pool, id).await { + Ok(chains) => Json(chains).into_response(), + Err(e) => { + tracing::error!("Failed to list chains for directive {}: {}", id, e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response() + } + } +} + +/// Get a chain with its steps. +#[utoipa::path( + get, + path = "/api/v1/directives/{id}/chains/{chain_id}", + params( + ("id" = Uuid, Path, description = "Directive ID"), + ("chain_id" = Uuid, Path, description = "Chain ID") + ), + responses( + (status = 200, description = "Chain with steps", body = ChainWithSteps), + (status = 401, description = "Unauthorized", body = ApiError), + (status = 404, description = "Chain not found", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError), + ), + security( + ("bearer_auth" = []), + ("api_key" = []) + ), + tag = "Directives" +)] +pub async fn get_chain( + State(state): State<SharedState>, + Authenticated(auth): Authenticated, + Path((id, chain_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 directive exists and belongs to owner + 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) => { + tracing::error!("Failed to get directive {}: {}", id, e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response(); + } + } + + // Get the chain and verify it belongs to this directive + let chains = match repository::list_chains_for_directive(pool, id).await { + Ok(c) => c, + Err(e) => { + tracing::error!("Failed to list chains: {}", e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response(); + } + }; + + let chain = match chains.into_iter().find(|c| c.id == chain_id) { + Some(c) => c, + None => { + return ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "Chain not found")), + ) + .into_response(); + } + }; + + let steps = match repository::list_steps_for_chain(pool, chain_id).await { + Ok(s) => s, + Err(e) => { + tracing::warn!("Failed to get steps for chain {}: {}", chain_id, e); + Vec::new() + } + }; + + Json(ChainWithSteps { chain, steps }).into_response() +} diff --git a/makima/src/server/handlers/mod.rs b/makima/src/server/handlers/mod.rs index ae370c9..29cd09f 100644 --- a/makima/src/server/handlers/mod.rs +++ b/makima/src/server/handlers/mod.rs @@ -6,6 +6,7 @@ pub mod contract_chat; pub mod contract_daemon; pub mod contract_discuss; pub mod contracts; +pub mod directives; pub mod file_ws; pub mod files; pub mod history; diff --git a/makima/src/server/mod.rs b/makima/src/server/mod.rs index b7a4156..a429612 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, 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, 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::openapi::ApiDoc; use crate::server::state::SharedState; @@ -170,6 +170,19 @@ pub fn make_router(state: SharedState) -> Router { "/contracts/{id}/chat/history", get(contract_chat::get_contract_chat_history).delete(contract_chat::clear_contract_chat_history), ) + // Directive endpoints + .route( + "/directives", + get(directives::list_directives).post(directives::create_directive), + ) + .route( + "/directives/{id}", + get(directives::get_directive) + .put(directives::update_directive) + .delete(directives::delete_directive), + ) + .route("/directives/{id}/chains", get(directives::list_chains)) + .route("/directives/{id}/chains/{chain_id}", get(directives::get_chain)) // Contract supervisor resume endpoints .route("/contracts/{id}/supervisor/resume", post(mesh_supervisor::resume_supervisor)) .route("/contracts/{id}/supervisor/conversation/rewind", post(mesh_supervisor::rewind_conversation)) diff --git a/makima/src/server/openapi.rs b/makima/src/server/openapi.rs index a70342b..0e6912a 100644 --- a/makima/src/server/openapi.rs +++ b/makima/src/server/openapi.rs @@ -4,23 +4,25 @@ use utoipa::OpenApi; use crate::db::models::{ AddLocalRepositoryRequest, AddRemoteRepositoryRequest, BranchInfo, BranchListResponse, - BranchTaskRequest, BranchTaskResponse, ChangePhaseRequest, Contract, - ContractChatHistoryResponse, ContractChatMessageRecord, ContractEvent, ContractListResponse, - ContractRepository, ContractSummary, ContractWithRelations, CreateContractRequest, - CreateFileRequest, CreateManagedRepositoryRequest, CreateTaskRequest, Daemon, - DaemonDirectoriesResponse, DaemonDirectory, DaemonListResponse, File, FileListResponse, - FileSummary, MergeCommitRequest, MergeCompleteCheckResponse, MergeResolveRequest, - MergeResultResponse, MergeSkipRequest, MergeStartRequest, MergeStatusResponse, - MeshChatConversation, MeshChatHistoryResponse, MeshChatMessageRecord, RepositoryHistoryEntry, + BranchTaskRequest, BranchTaskResponse, ChainStep, ChainWithSteps, ChangePhaseRequest, + Contract, ContractChatHistoryResponse, ContractChatMessageRecord, ContractEvent, + ContractListResponse, ContractRepository, ContractSummary, ContractWithRelations, + CreateContractRequest, CreateDirectiveRequest, CreateFileRequest, + CreateManagedRepositoryRequest, CreateTaskRequest, Daemon, DaemonDirectoriesResponse, + DaemonDirectory, DaemonListResponse, Directive, DirectiveChain, DirectiveListResponse, + DirectiveSummary, DirectiveWithChains, File, FileListResponse, FileSummary, + MergeCommitRequest, MergeCompleteCheckResponse, MergeResolveRequest, MergeResultResponse, + MergeSkipRequest, MergeStartRequest, MergeStatusResponse, MeshChatConversation, + MeshChatHistoryResponse, MeshChatMessageRecord, RepositoryHistoryEntry, RepositoryHistoryListResponse, RepositorySuggestionsQuery, SendMessageRequest, Task, TaskEventListResponse, TaskListResponse, TaskSummary, TaskWithSubtasks, TranscriptEntry, - UpdateContractRequest, UpdateFileRequest, UpdateTaskRequest, + UpdateContractRequest, UpdateDirectiveRequest, UpdateFileRequest, UpdateTaskRequest, }; use crate::server::auth::{ ApiKey, ApiKeyInfoResponse, CreateApiKeyRequest, CreateApiKeyResponse, RefreshApiKeyRequest, RefreshApiKeyResponse, RevokeApiKeyResponse, }; -use crate::server::handlers::{api_keys, contract_chat, contract_discuss, contracts, files, listen, mesh, mesh_chat, mesh_merge, repository_history, users}; +use crate::server::handlers::{api_keys, contract_chat, contract_discuss, contracts, directives, files, listen, mesh, mesh_chat, mesh_merge, repository_history, users}; use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage, TranscriptMessage}; #[derive(OpenApi)] @@ -103,6 +105,14 @@ use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage repository_history::list_repository_history, repository_history::get_repository_suggestions, repository_history::delete_repository_history, + // Directive endpoints + directives::list_directives, + directives::get_directive, + directives::create_directive, + directives::update_directive, + directives::delete_directive, + directives::list_chains, + directives::get_chain, ), components( schemas( @@ -187,6 +197,16 @@ use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage RepositoryHistoryEntry, RepositoryHistoryListResponse, RepositorySuggestionsQuery, + // Directive schemas + Directive, + DirectiveSummary, + DirectiveListResponse, + DirectiveWithChains, + DirectiveChain, + ChainStep, + ChainWithSteps, + CreateDirectiveRequest, + UpdateDirectiveRequest, ) ), tags( @@ -197,6 +217,7 @@ use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage (name = "API Keys", description = "API key management for programmatic access"), (name = "Users", description = "User account management"), (name = "Settings", description = "User settings including repository history"), + (name = "Directives", description = "Directive management for autonomous goal-driven execution"), ) )] pub struct ApiDoc; |
