summaryrefslogtreecommitdiff
path: root/makima
diff options
context:
space:
mode:
Diffstat (limited to 'makima')
-rw-r--r--makima/frontend/src/components/NavStrip.tsx1
-rw-r--r--makima/frontend/src/hooks/useDirectives.ts298
-rw-r--r--makima/frontend/src/lib/api.ts758
-rw-r--r--makima/frontend/src/main.tsx17
-rw-r--r--makima/frontend/src/routes/directives.tsx1254
-rw-r--r--makima/frontend/tsconfig.tsbuildinfo2
-rw-r--r--makima/migrations/20260206000000_create_directive_system.sql320
-rw-r--r--makima/src/bin/makima.rs151
-rw-r--r--makima/src/daemon/api/directive.rs162
-rw-r--r--makima/src/daemon/api/mod.rs1
-rw-r--r--makima/src/daemon/chain/parser.rs7
-rw-r--r--makima/src/daemon/chain/runner.rs12
-rw-r--r--makima/src/daemon/cli/directive.rs186
-rw-r--r--makima/src/daemon/cli/mod.rs56
-rw-r--r--makima/src/daemon/db/local.rs4
-rw-r--r--makima/src/daemon/process/claude_protocol.rs2
-rw-r--r--makima/src/daemon/skills/directive.md303
-rw-r--r--makima/src/daemon/skills/mod.rs6
-rw-r--r--makima/src/daemon/storage/patch.rs25
-rw-r--r--makima/src/daemon/task/completion_gate.rs21
-rw-r--r--makima/src/daemon/task/state.rs4
-rw-r--r--makima/src/daemon/temp.rs2
-rw-r--r--makima/src/daemon/worktree/manager.rs3
-rw-r--r--makima/src/daemon/ws/protocol.rs6
-rw-r--r--makima/src/db/models.rs1178
-rw-r--r--makima/src/db/repository.rs2420
-rw-r--r--makima/src/lib.rs1
-rw-r--r--makima/src/llm/contract_evaluator.rs564
-rw-r--r--makima/src/llm/mod.rs2
-rw-r--r--makima/src/llm/task_output.rs26
-rw-r--r--makima/src/orchestration/engine.rs976
-rw-r--r--makima/src/orchestration/mod.rs26
-rw-r--r--makima/src/orchestration/planner.rs742
-rw-r--r--makima/src/orchestration/verifier.rs806
-rw-r--r--makima/src/server/handlers/contract_chat.rs1223
-rw-r--r--makima/src/server/handlers/contracts.rs140
-rw-r--r--makima/src/server/handlers/directives.rs1488
-rw-r--r--makima/src/server/handlers/mod.rs3
-rw-r--r--makima/src/server/mod.rs72
39 files changed, 9115 insertions, 4153 deletions
diff --git a/makima/frontend/src/components/NavStrip.tsx b/makima/frontend/src/components/NavStrip.tsx
index 4f6cf32..ece07e4 100644
--- a/makima/frontend/src/components/NavStrip.tsx
+++ b/makima/frontend/src/components/NavStrip.tsx
@@ -10,6 +10,7 @@ interface NavLink {
const NAV_LINKS: NavLink[] = [
{ label: "Listen", href: "/listen" },
+ { label: "Directives", href: "/directives", requiresAuth: true },
{ label: "Chains", href: "/chains", requiresAuth: true },
{ label: "Contracts", href: "/contracts", requiresAuth: true },
{ label: "Board", href: "/workflow", requiresAuth: true },
diff --git a/makima/frontend/src/hooks/useDirectives.ts b/makima/frontend/src/hooks/useDirectives.ts
new file mode 100644
index 0000000..6e1654f
--- /dev/null
+++ b/makima/frontend/src/hooks/useDirectives.ts
@@ -0,0 +1,298 @@
+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 2f4ee62..80a43eb 100644
--- a/makima/frontend/src/lib/api.ts
+++ b/makima/frontend/src/lib/api.ts
@@ -3527,3 +3527,761 @@ export async function setChainRepositoryPrimary(
}
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 a7ba1a3..5a1b98e 100644
--- a/makima/frontend/src/main.tsx
+++ b/makima/frontend/src/main.tsx
@@ -13,6 +13,7 @@ import ListenPage from "./routes/listen";
import FilesPage from "./routes/files";
import ContractsPage from "./routes/contracts";
import ChainsPage from "./routes/chains";
+import DirectivesPage from "./routes/directives";
import WorkflowPage from "./routes/workflow";
import MeshPage from "./routes/mesh";
import HistoryPage from "./routes/history";
@@ -89,6 +90,22 @@ 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
new file mode 100644
index 0000000..51fd57a
--- /dev/null
+++ b/makima/frontend/src/routes/directives.tsx
@@ -0,0 +1,1254 @@
+import { useState, useCallback, useEffect, useMemo } from "react";
+import { useParams, useNavigate } from "react-router";
+import {
+ ReactFlow,
+ Edge,
+ Controls,
+ Background,
+ Handle,
+ Position,
+ BackgroundVariant,
+ MarkerType,
+} from "@xyflow/react";
+import "@xyflow/react/dist/style.css";
+import { Masthead } from "../components/Masthead";
+import { useDirectives, useDirectiveEventSubscription } from "../hooks/useDirectives";
+import { useAuth } from "../contexts/AuthContext";
+import type {
+ DirectiveSummary,
+ DirectiveWithProgress,
+ DirectiveGraphResponse,
+ DirectiveGraphNode,
+ CreateDirectiveRequest,
+ RepositoryHistoryEntry,
+ AutonomyLevel,
+ StepStatus,
+ ConfidenceLevel,
+} from "../lib/api";
+import { getRepositorySuggestions } 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]);
+
+ // 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,
+ getDirectiveById,
+ getGraph,
+ start,
+ pause,
+ resume,
+ stop,
+ } = useDirectives();
+
+ const [directiveDetail, setDirectiveDetail] = useState<DirectiveWithProgress | null>(null);
+ const [directiveGraph, setDirectiveGraph] = useState<DirectiveGraphResponse | null>(null);
+ const [detailLoading, setDetailLoading] = useState(false);
+ const [isCreating, setIsCreating] = useState(false);
+
+ // Load directive detail when ID changes
+ useEffect(() => {
+ if (id) {
+ setDetailLoading(true);
+ Promise.all([getDirectiveById(id), getGraph(id).catch(() => null)]).then(([directive, graph]) => {
+ setDirectiveDetail(directive);
+ setDirectiveGraph(graph);
+ setDetailLoading(false);
+ });
+ } else {
+ setDirectiveDetail(null);
+ setDirectiveGraph(null);
+ }
+ }, [id, getDirectiveById, getGraph]);
+
+ 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]
+ );
+
+ const handleRefresh = useCallback(async () => {
+ if (id) {
+ const [directive, graph] = await Promise.all([
+ getDirectiveById(id),
+ getGraph(id).catch(() => null),
+ ]);
+ setDirectiveDetail(directive);
+ setDirectiveGraph(graph);
+ }
+ }, [id, getDirectiveById, getGraph]);
+
+ const handleStart = useCallback(async () => {
+ if (id) {
+ await start(id);
+ handleRefresh();
+ }
+ }, [id, start, handleRefresh]);
+
+ const handlePause = useCallback(async () => {
+ if (id) {
+ await pause(id);
+ handleRefresh();
+ }
+ }, [id, pause, handleRefresh]);
+
+ const handleResume = useCallback(async () => {
+ if (id) {
+ await resume(id);
+ handleRefresh();
+ }
+ }, [id, resume, handleRefresh]);
+
+ const handleStop = useCallback(async () => {
+ if (id) {
+ await stop(id);
+ handleRefresh();
+ }
+ }, [id, stop, handleRefresh]);
+
+ 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={handleRefresh}
+ 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>
+ );
+}
+
+// =============================================================================
+// Directive List Component
+// =============================================================================
+
+interface DirectiveListProps {
+ directives: DirectiveSummary[];
+ loading: boolean;
+ onSelect: (id: string) => void;
+ onCreate: () => void;
+ selectedId?: string;
+ onArchive: (directive: DirectiveSummary) => void;
+}
+
+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>
+ );
+}
+
+interface DirectiveListItemProps {
+ directive: DirectiveSummary;
+ selected: boolean;
+ onClick: () => void;
+ onArchive: () => void;
+}
+
+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>
+ );
+}
+
+// =============================================================================
+// Directive Detail Component
+// =============================================================================
+
+interface DirectiveDetailProps {
+ directive: DirectiveWithProgress;
+ graph: DirectiveGraphResponse | null;
+ loading: boolean;
+ onBack: () => void;
+ onRefresh: () => void;
+ onStart: () => void;
+ onPause: () => void;
+ onResume: () => void;
+ onStop: () => void;
+}
+
+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]"
+ >
+ &larr; 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>
+ );
+}
+
+// =============================================================================
+// Tab Components
+// =============================================================================
+
+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>
+ );
+}
+
+// Step status colors for both list and DAG views
+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
+const confidenceColors: Record<ConfidenceLevel, string> = {
+ green: "#22c55e",
+ yellow: "#eab308",
+ red: "#ef4444",
+};
+
+// Node dimensions
+const NODE_WIDTH = 180;
+const NODE_HEIGHT = 70;
+
+// Custom node component for steps
+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>
+ );
+}
+
+// Node types for React Flow
+const nodeTypes = {
+ step: StepNodeComponent,
+};
+
+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>
+ );
+}
+
+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 ? "● Live" : "○ 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>
+ );
+}
+
+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>
+ );
+}
+
+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>
+ );
+}
+
+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>
+ );
+}
+
+// =============================================================================
+// Create Directive Modal
+// =============================================================================
+
+interface CreateDirectiveModalProps {
+ onSubmit: (goal: string, repositoryUrl: string | undefined, autonomyLevel: AutonomyLevel) => void;
+ onCancel: () => void;
+}
+
+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/tsconfig.tsbuildinfo b/makima/frontend/tsconfig.tsbuildinfo
index 425babe..966ee38 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/chains/chaineditor.tsx","./src/components/chains/chainlist.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/usechains.ts","./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/chains.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
+{"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/chains/chaineditor.tsx","./src/components/chains/chainlist.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/usechains.ts","./src/hooks/usecontracts.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/chains.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
diff --git a/makima/migrations/20260206000000_create_directive_system.sql b/makima/migrations/20260206000000_create_directive_system.sql
new file mode 100644
index 0000000..ed780ef
--- /dev/null
+++ b/makima/migrations/20260206000000_create_directive_system.sql
@@ -0,0 +1,320 @@
+-- ============================================================================
+-- Migration: create_directive_system.sql
+-- Replaces: chains, chain_contracts, chain_contract_definitions,
+-- chain_directives, contract_evaluations, chain_events,
+-- chain_repositories
+-- ============================================================================
+
+-- Drop old chain system tables (cascade to remove FKs)
+DROP TABLE IF EXISTS contract_evaluations CASCADE;
+DROP TABLE IF EXISTS chain_events CASCADE;
+DROP TABLE IF EXISTS chain_contract_definitions CASCADE;
+DROP TABLE IF EXISTS chain_contracts CASCADE;
+DROP TABLE IF EXISTS chain_directives CASCADE;
+DROP TABLE IF EXISTS chain_repositories CASCADE;
+DROP TABLE IF EXISTS chains CASCADE;
+
+-- Remove old chain-related columns from contracts
+ALTER TABLE contracts DROP COLUMN IF EXISTS chain_id;
+ALTER TABLE contracts DROP COLUMN IF EXISTS spawned_chain_id;
+ALTER TABLE contracts DROP COLUMN IF EXISTS is_chain_directive;
+
+-- ============================================================================
+-- 1. DIRECTIVES -- the top-level entity
+-- ============================================================================
+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',
+
+ -- Autonomy configuration
+ autonomy_level VARCHAR(32) NOT NULL DEFAULT 'guardrails',
+ 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);
+
+-- 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 -- generated execution plans
+-- ============================================================================
+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',
+
+ -- 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 -- nodes in the DAG
+-- ============================================================================
+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',
+
+ -- 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;
+
+-- ============================================================================
+-- 4. EVALUATIONS -- programmatic + LLM composite
+-- ============================================================================
+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 -- comprehensive audit stream
+-- ============================================================================
+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 -- pluggable verification config
+-- ============================================================================
+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 -- human-in-the-loop gates
+-- ============================================================================
+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',
+ 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 f9c981f..822b21f 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, ChainCommand,
- 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};
@@ -32,6 +32,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
Commands::View(args) => run_view(args).await,
Commands::Config(cmd) => run_config(cmd).await,
Commands::Chain(cmd) => run_chain(cmd).await,
+ Commands::Directive(cmd) => run_directive(cmd).await,
}
}
@@ -1021,6 +1022,154 @@ async fn run_chain(
Ok(())
}
+/// 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
new file mode 100644
index 0000000..5281d21
--- /dev/null
+++ b/makima/src/daemon/api/directive.rs
@@ -0,0 +1,162 @@
+//! 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
+ }
+}
diff --git a/makima/src/daemon/api/mod.rs b/makima/src/daemon/api/mod.rs
index 7868907..f1f52d0 100644
--- a/makima/src/daemon/api/mod.rs
+++ b/makima/src/daemon/api/mod.rs
@@ -3,6 +3,7 @@
pub mod chain;
pub mod client;
pub mod contract;
+pub mod directive;
pub mod supervisor;
pub use client::ApiClient;
diff --git a/makima/src/daemon/chain/parser.rs b/makima/src/daemon/chain/parser.rs
index 3851d1f..b32d0f2 100644
--- a/makima/src/daemon/chain/parser.rs
+++ b/makima/src/daemon/chain/parser.rs
@@ -395,7 +395,9 @@ contracts:
fn test_repo_alias() {
let yaml = r#"
name: Repo Chain
-repo: https://github.com/user/project
+repositories:
+ - name: main
+ repository_url: https://github.com/user/project
contracts:
- name: Phase1
tasks:
@@ -403,8 +405,9 @@ contracts:
plan: "Work on repo"
"#;
let chain = parse_chain_yaml(yaml).unwrap();
+ assert_eq!(chain.repositories.len(), 1);
assert_eq!(
- chain.repository_url,
+ chain.repositories[0].repository_url,
Some("https://github.com/user/project".to_string())
);
}
diff --git a/makima/src/daemon/chain/runner.rs b/makima/src/daemon/chain/runner.rs
index dfbcfa7..1814581 100644
--- a/makima/src/daemon/chain/runner.rs
+++ b/makima/src/daemon/chain/runner.rs
@@ -37,8 +37,10 @@ pub enum RunnerError {
/// Chain runner for creating and managing chains.
pub struct ChainRunner {
/// Base API URL
+ #[allow(dead_code)]
api_url: String,
/// API key for authentication
+ #[allow(dead_code)]
api_key: String,
}
@@ -116,6 +118,7 @@ impl ChainRunner {
CreateChainRequest {
name: chain.name.clone(),
description: chain.description.clone(),
+ repository_url: None, // Legacy field, repositories take precedence
repositories: if repositories.is_empty() {
None
} else {
@@ -242,7 +245,9 @@ mod tests {
let yaml = r#"
name: Test Chain
description: A test chain
-repo: https://github.com/test/repo
+repositories:
+ - name: main
+ repository_url: https://github.com/test/repo
contracts:
- name: Research
type: simple
@@ -270,8 +275,11 @@ loop:
assert_eq!(request.name, "Test Chain");
assert_eq!(request.description, Some("A test chain".to_string()));
+ // Repositories are now in a separate array
+ let repos = request.repositories.unwrap();
+ assert_eq!(repos.len(), 1);
assert_eq!(
- request.repository_url,
+ repos[0].repository_url,
Some("https://github.com/test/repo".to_string())
);
assert_eq!(request.loop_enabled, Some(true));
diff --git a/makima/src/daemon/cli/directive.rs b/makima/src/daemon/cli/directive.rs
new file mode 100644
index 0000000..a2bb34b
--- /dev/null
+++ b/makima/src/daemon/cli/directive.rs
@@ -0,0 +1,186 @@
+//! 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 035a784..91ef87c 100644
--- a/makima/src/daemon/cli/mod.rs
+++ b/makima/src/daemon/cli/mod.rs
@@ -4,6 +4,7 @@ pub mod chain;
pub mod config;
pub mod contract;
pub mod daemon;
+pub mod directive;
pub mod server;
pub mod supervisor;
pub mod view;
@@ -14,6 +15,7 @@ pub use chain::ChainArgs;
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;
@@ -68,6 +70,14 @@ pub enum Commands {
/// in parallel when no dependencies exist.
#[command(subcommand)]
Chain(ChainCommand),
+
+ /// 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.
@@ -248,6 +258,52 @@ pub enum ChainCommand {
Archive(chain::ArchiveArgs),
}
+/// 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/db/local.rs b/makima/src/daemon/db/local.rs
index f3ed45a..5b4ca5b 100644
--- a/makima/src/daemon/db/local.rs
+++ b/makima/src/daemon/db/local.rs
@@ -336,7 +336,9 @@ impl LocalDb {
#[cfg(test)]
mod tests {
- use crate::daemon::*;
+ use super::*;
+ use chrono::Utc;
+ use uuid::Uuid;
#[test]
fn test_open_memory() {
diff --git a/makima/src/daemon/process/claude_protocol.rs b/makima/src/daemon/process/claude_protocol.rs
index 96e5377..930152b 100644
--- a/makima/src/daemon/process/claude_protocol.rs
+++ b/makima/src/daemon/process/claude_protocol.rs
@@ -45,7 +45,7 @@ impl ClaudeInputMessage {
#[cfg(test)]
mod tests {
- use crate::daemon::*;
+ use super::*;
#[test]
fn test_user_message_serialization() {
diff --git a/makima/src/daemon/skills/directive.md b/makima/src/daemon/skills/directive.md
new file mode 100644
index 0000000..97e8e20
--- /dev/null
+++ b/makima/src/daemon/skills/directive.md
@@ -0,0 +1,303 @@
+---
+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 3b0c0dc..dafa9ec 100644
--- a/makima/src/daemon/skills/mod.rs
+++ b/makima/src/daemon/skills/mod.rs
@@ -9,12 +9,16 @@ 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");
-/// Chain skill content - multi-contract orchestration commands
+/// Chain skill content - multi-contract orchestration commands (legacy)
pub const CHAIN_SKILL: &str = include_str!("chain.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-chain", CHAIN_SKILL),
+ ("makima-directive", DIRECTIVE_SKILL),
];
diff --git a/makima/src/daemon/storage/patch.rs b/makima/src/daemon/storage/patch.rs
index 0da4eda..b374d15 100644
--- a/makima/src/daemon/storage/patch.rs
+++ b/makima/src/daemon/storage/patch.rs
@@ -227,6 +227,16 @@ pub async fn create_export_patch(
None
};
+ // Get current HEAD SHA for comparison
+ let head_sha = Command::new("git")
+ .current_dir(worktree_path)
+ .args(["rev-parse", "HEAD"])
+ .output()
+ .await
+ .ok()
+ .filter(|o| o.status.success())
+ .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string());
+
// If we couldn't find upstream, try common default branches
let base = if base.is_none() {
let default_branches = ["origin/main", "origin/master", "main", "master"];
@@ -241,14 +251,23 @@ pub async fn create_export_patch(
if let Ok(output) = merge_base {
if output.status.success() {
- found_base = Some(String::from_utf8_lossy(&output.stdout).trim().to_string());
- break;
+ let mb_sha = String::from_utf8_lossy(&output.stdout).trim().to_string();
+ // Skip if merge-base equals HEAD (would result in empty diff)
+ if head_sha.as_ref() != Some(&mb_sha) {
+ found_base = Some(mb_sha);
+ break;
+ }
}
}
}
found_base
} else {
- base
+ // Also check upstream base
+ if base.as_ref() == head_sha.as_ref() {
+ None
+ } else {
+ base
+ }
};
// If still nothing, get the first commit or use HEAD~1
diff --git a/makima/src/daemon/task/completion_gate.rs b/makima/src/daemon/task/completion_gate.rs
index 69b7c6a..40a6466 100644
--- a/makima/src/daemon/task/completion_gate.rs
+++ b/makima/src/daemon/task/completion_gate.rs
@@ -5,7 +5,7 @@
//! development framework.
//!
//! Format:
-//! ```
+//! ```text
//! <COMPLETION_GATE>
//! ready: true|false
//! reason: "explanation of completion status"
@@ -133,19 +133,18 @@ impl CompletionGate {
/// This is useful when Claude produces multiple completion gates during
/// a long-running task, and we want to use the final status.
pub fn parse_last(text: &str) -> Option<Self> {
+ let start_tag = "<COMPLETION_GATE>";
let end_tag = "</COMPLETION_GATE>";
- let mut last_gate = None;
- let mut search_start = 0;
- while let Some(end_idx) = text[search_start..].find(end_tag) {
- let absolute_end = search_start + end_idx + end_tag.len();
- if let Some(gate) = Self::parse(&text[..absolute_end]) {
- last_gate = Some(gate);
- }
- search_start = absolute_end;
- }
+ // Find the last occurrence of the start tag
+ let start_idx = text.rfind(start_tag)?;
+ let remaining = &text[start_idx..];
+
+ // Find the end tag after the last start tag
+ let end_idx = remaining.find(end_tag)?;
- last_gate
+ // Parse just this last gate
+ Self::parse(&remaining[..end_idx + end_tag.len()])
}
}
diff --git a/makima/src/daemon/task/state.rs b/makima/src/daemon/task/state.rs
index 7b59b62..fe73de1 100644
--- a/makima/src/daemon/task/state.rs
+++ b/makima/src/daemon/task/state.rs
@@ -124,9 +124,7 @@ impl Default for TaskState {
#[cfg(test)]
mod tests {
- #[allow(unused_imports)]
- use crate::daemon::*;
- use super::TaskState;
+ use super::*;
#[test]
fn test_valid_transitions() {
diff --git a/makima/src/daemon/temp.rs b/makima/src/daemon/temp.rs
index 42d4a28..015b21b 100644
--- a/makima/src/daemon/temp.rs
+++ b/makima/src/daemon/temp.rs
@@ -214,7 +214,7 @@ impl Default for TempManager {
#[cfg(test)]
mod tests {
- use crate::daemon::*;
+ use super::*;
#[test]
fn test_temp_manager_default_dir() {
diff --git a/makima/src/daemon/worktree/manager.rs b/makima/src/daemon/worktree/manager.rs
index 166e654..310627c 100644
--- a/makima/src/daemon/worktree/manager.rs
+++ b/makima/src/daemon/worktree/manager.rs
@@ -1949,7 +1949,8 @@ pub fn sanitize_name(name: &str) -> String {
#[cfg(test)]
mod tests {
- use crate::daemon::*;
+ use super::*;
+ use uuid::Uuid;
#[test]
fn test_extract_repo_name() {
diff --git a/makima/src/daemon/ws/protocol.rs b/makima/src/daemon/ws/protocol.rs
index 5c88038..574e864 100644
--- a/makima/src/daemon/ws/protocol.rs
+++ b/makima/src/daemon/ws/protocol.rs
@@ -907,7 +907,7 @@ impl DaemonMessage {
#[cfg(test)]
mod tests {
- use crate::daemon::*;
+ use super::*;
#[test]
fn test_daemon_message_serialization() {
@@ -920,7 +920,7 @@ mod tests {
#[test]
fn test_daemon_command_deserialization() {
- let json = r#"{"type":"spawnTask","taskId":"550e8400-e29b-41d4-a716-446655440000","plan":"Build the feature","repoUrl":"https://github.com/test/repo","baseBranch":"main","parentTaskId":null,"depth":0,"isOrchestrator":false}"#;
+ let json = r#"{"type":"spawnTask","taskId":"550e8400-e29b-41d4-a716-446655440000","taskName":"Build Feature","plan":"Build the feature","repoUrl":"https://github.com/test/repo","baseBranch":"main","parentTaskId":null,"depth":0,"isOrchestrator":false}"#;
let cmd: DaemonCommand = serde_json::from_str(json).unwrap();
match cmd {
DaemonCommand::SpawnTask {
@@ -945,7 +945,7 @@ mod tests {
#[test]
fn test_orchestrator_spawn_deserialization() {
- let json = r#"{"type":"spawnTask","taskId":"550e8400-e29b-41d4-a716-446655440000","plan":"Coordinate subtasks","repoUrl":"https://github.com/test/repo","baseBranch":"main","parentTaskId":null,"depth":0,"isOrchestrator":true}"#;
+ let json = r#"{"type":"spawnTask","taskId":"550e8400-e29b-41d4-a716-446655440000","taskName":"Coordinate","plan":"Coordinate subtasks","repoUrl":"https://github.com/test/repo","baseBranch":"main","parentTaskId":null,"depth":0,"isOrchestrator":true}"#;
let cmd: DaemonCommand = serde_json::from_str(json).unwrap();
match cmd {
DaemonCommand::SpawnTask {
diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs
index e861f1d..3a96165 100644
--- a/makima/src/db/models.rs
+++ b/makima/src/db/models.rs
@@ -1446,16 +1446,16 @@ pub struct Contract {
/// Use `get_phase_config()` to get the parsed PhaseConfig.
#[serde(skip_serializing_if = "Option::is_none")]
pub phase_config: Option<serde_json::Value>,
- /// Chain ID if this contract is part of a chain (DAG of contracts)
+ /// Directive ID if this contract is part of a directive's chain
#[serde(skip_serializing_if = "Option::is_none")]
- pub chain_id: Option<Uuid>,
- /// Reference to chain spawned by this directive contract
- #[serde(skip_serializing_if = "Option::is_none")]
- pub spawned_chain_id: Option<Uuid>,
- /// Whether this contract is a chain directive orchestrator
+ pub directive_id: Option<Uuid>,
+ /// Whether this contract is a directive orchestrator
#[serde(default)]
#[sqlx(default)]
- pub is_chain_directive: bool,
+ pub is_directive_orchestrator: bool,
+ /// Reference to directive spawned by this orchestrator contract
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub spawned_directive_id: Option<Uuid>,
pub version: i32,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
@@ -2596,914 +2596,648 @@ pub struct HeartbeatHistoryQuery {
}
// =============================================================================
-// Chains (DAG of contracts for multi-contract orchestration)
+// Directives (Goal-driven orchestration with chains of steps)
// =============================================================================
-/// Chain status determines the overall state of the chain
+/// Directive status
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
-#[serde(rename_all = "lowercase")]
-pub enum ChainStatus {
- /// Chain is actively running
+#[serde(rename_all = "snake_case")]
+pub enum DirectiveStatus {
+ Draft,
+ Planning,
Active,
- /// All contracts completed successfully
+ Paused,
Completed,
- /// Chain was manually archived
Archived,
+ Failed,
}
-impl Default for ChainStatus {
+impl Default for DirectiveStatus {
fn default() -> Self {
- ChainStatus::Active
+ DirectiveStatus::Draft
}
}
-impl std::fmt::Display for ChainStatus {
+impl std::fmt::Display for DirectiveStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
- ChainStatus::Active => write!(f, "active"),
- ChainStatus::Completed => write!(f, "completed"),
- ChainStatus::Archived => write!(f, "archived"),
+ 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 ChainStatus {
+impl std::str::FromStr for DirectiveStatus {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
- "active" => Ok(ChainStatus::Active),
- "completed" => Ok(ChainStatus::Completed),
- "archived" => Ok(ChainStatus::Archived),
- _ => Err(format!("Invalid chain status: {}", s)),
+ "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)),
}
}
}
-/// Chain - a directed acyclic graph (DAG) of contracts
-/// Fits Makima's control theme - she controls through invisible chains
+/// Directive - the top-level goal-driven orchestration entity
#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
-pub struct Chain {
+pub struct Directive {
pub id: Uuid,
pub owner_id: Uuid,
- pub name: String,
- pub description: Option<String>,
+ 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,
- /// Whether loop mode is enabled for iterative execution
- #[serde(default)]
- pub loop_enabled: bool,
- /// Maximum loop iterations (default: 10)
- pub loop_max_iterations: Option<i32>,
- /// Current loop iteration count
- pub loop_current_iteration: Option<i32>,
- /// Progress check prompt/criteria for evaluating loop completion
- pub loop_progress_check: Option<String>,
- /// Reference to the directive contract that created/orchestrates this chain
- pub directive_contract_id: Option<Uuid>,
- /// The directive document text (formal specification)
- pub directive_document: Option<String>,
- /// Whether LLM evaluation is enabled after contract completion
- #[serde(default = "default_evaluation_enabled")]
- #[sqlx(default)]
- pub evaluation_enabled: bool,
- /// Default pass threshold for evaluations (0.0-1.0)
- pub default_pass_threshold: Option<f64>,
- /// Default max retry attempts for evaluations
- pub default_max_retries: Option<i32>,
- /// Version for optimistic locking
+ 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>,
}
-fn default_evaluation_enabled() -> bool {
- true
+impl Directive {
+ /// Parse status string to DirectiveStatus enum
+ pub fn status_enum(&self) -> Result<DirectiveStatus, String> {
+ self.status.parse()
+ }
}
-/// Chain repository record from the database
+/// Directive chain - a generated execution plan (DAG) for a directive
#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
-pub struct ChainRepository {
+pub struct DirectiveChain {
pub id: Uuid,
- pub chain_id: Uuid,
+ pub directive_id: Uuid,
+ pub generation: i32,
pub name: String,
- pub repository_url: Option<String>,
- pub local_path: Option<String>,
- pub source_type: String,
+ pub description: Option<String>,
+ pub rationale: Option<String>,
+ pub planning_model: Option<String>,
pub status: String,
- pub is_primary: bool,
+ 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>,
}
-impl ChainRepository {
- /// Parse source_type string to RepositorySourceType enum
- pub fn source_type_enum(&self) -> Result<RepositorySourceType, String> {
- self.source_type.parse()
+/// 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
}
+}
- /// Parse status string to RepositoryStatus enum
- pub fn status_enum(&self) -> Result<RepositoryStatus, String> {
- self.status.parse()
+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 Chain {
- /// Parse status string to ChainStatus enum
- pub fn status_enum(&self) -> Result<ChainStatus, String> {
- self.status.parse()
+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 contract link - links contracts to chains with DAG dependency info
+/// Chain step - a node in the DAG execution plan
#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
-pub struct ChainContract {
+pub struct ChainStep {
pub id: Uuid,
pub chain_id: Uuid,
- pub contract_id: Uuid,
- /// Contract IDs this contract depends on (DAG edges)
+ 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 depends_on: Vec<Uuid>,
- /// Order for display/processing (topological sort order)
- pub order_index: i32,
- /// X position for GUI editor
- pub editor_x: Option<f64>,
- /// Y position for GUI editor
- pub editor_y: Option<f64>,
- /// Evaluation status: pending, evaluating, passed, failed, rework, escalated
- #[serde(default = "default_evaluation_status")]
+ pub phases: Vec<String>,
#[sqlx(default)]
- pub evaluation_status: String,
- /// Number of evaluation retry attempts
- #[serde(default)]
+ pub depends_on: Vec<Uuid>,
+ pub parallel_group: Option<String>,
#[sqlx(default)]
- pub evaluation_retry_count: i32,
- /// Maximum evaluation retry attempts (default: 3)
- #[serde(default = "default_max_evaluation_retries")]
+ pub requirement_ids: Vec<String>,
#[sqlx(default)]
- pub max_evaluation_retries: i32,
- /// Reference to the last evaluation result
+ 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>,
- /// Rework feedback/instructions from failed evaluation
- pub rework_feedback: Option<String>,
- /// When rework was started
- pub rework_started_at: Option<DateTime<Utc>>,
- /// When contract originally completed (before rework)
- pub original_completion_at: Option<DateTime<Utc>>,
+ 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>,
}
-fn default_evaluation_status() -> String {
- "pending".to_string()
+impl ChainStep {
+ /// Parse status string to StepStatus enum
+ pub fn status_enum(&self) -> Result<StepStatus, String> {
+ self.status.parse()
+ }
}
-fn default_max_evaluation_retries() -> i32 {
- 3
+/// 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
+ }
+ }
}
-/// Chain event for audit trail
+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 ChainEvent {
+pub struct DirectiveEvaluation {
pub id: Uuid,
- pub chain_id: Uuid,
- pub event_type: String,
+ 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)]
- pub event_data: Option<serde_json::Value>,
+ #[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>,
}
-/// Summary of a chain for list views
-#[derive(Debug, Clone, FromRow, Serialize, ToSchema)]
+/// Directive event - audit stream entry
+#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
-pub struct ChainSummary {
+pub struct DirectiveEvent {
pub id: Uuid,
- pub name: String,
- pub description: Option<String>,
- pub status: String,
- pub loop_enabled: bool,
- pub loop_current_iteration: Option<i32>,
- pub contract_count: i64,
- pub completed_count: i64,
- pub version: i32,
+ 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>,
}
-/// Chain with contracts for detail view
-#[derive(Debug, Serialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct ChainWithContracts {
- #[serde(flatten)]
- pub chain: Chain,
- pub contracts: Vec<ChainContractDetail>,
- pub repositories: Vec<ChainRepository>,
-}
-
-/// Contract detail within a chain (includes contract info + chain link info)
-#[derive(Debug, Clone, FromRow, Serialize, ToSchema)]
+/// Directive verifier - pluggable verification configuration
+#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
-pub struct ChainContractDetail {
- pub chain_contract_id: Uuid,
- pub contract_id: Uuid,
- pub contract_name: String,
- pub contract_status: String,
- pub contract_phase: String,
- #[sqlx(default)]
- pub depends_on: Vec<Uuid>,
- pub order_index: i32,
- pub editor_x: Option<f64>,
- pub editor_y: Option<f64>,
- /// Evaluation status: pending, passed, failed, rework
- #[sqlx(default)]
- pub evaluation_status: Option<String>,
- /// Number of evaluation retries
- #[sqlx(default)]
- pub evaluation_retry_count: i32,
- /// Maximum evaluation retry attempts
+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 max_evaluation_retries: i32,
- /// When the chain contract was created
+ 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>,
}
-/// DAG graph structure for visualization
-#[derive(Debug, Serialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct ChainGraphResponse {
- pub chain_id: Uuid,
- pub chain_name: String,
- pub chain_status: String,
- pub nodes: Vec<ChainGraphNode>,
- pub edges: Vec<ChainGraphEdge>,
-}
-
-/// Node in chain DAG graph
-#[derive(Debug, Serialize, ToSchema)]
+/// Directive approval - human-in-the-loop gate
+#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
-pub struct ChainGraphNode {
+pub struct DirectiveApproval {
pub id: Uuid,
- pub contract_id: Uuid,
- pub name: String,
+ 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 phase: String,
- pub x: f64,
- pub y: f64,
+ 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>,
}
-/// Edge in chain DAG graph
-#[derive(Debug, Clone, Serialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct ChainGraphEdge {
- pub from: Uuid,
- pub to: Uuid,
-}
+// =============================================================================
+// Directive Request/Response Types
+// =============================================================================
-/// Response for chain list endpoint
-#[derive(Debug, Serialize, ToSchema)]
+/// Request to create a directive from a goal
+#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
-pub struct ChainListResponse {
- pub chains: Vec<ChainSummary>,
- pub total: i64,
+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 payload for creating a new chain
-#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
+/// Request to update a directive
+#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
-pub struct CreateChainRequest {
- /// Name of the chain
- pub name: String,
- /// Optional description
- pub description: Option<String>,
- /// Repositories for this chain
- pub repositories: Option<Vec<AddChainRepositoryRequest>>,
- /// Enable loop mode for iterative execution
- #[serde(default)]
- pub loop_enabled: Option<bool>,
- /// Maximum loop iterations (default: 10)
- pub loop_max_iterations: Option<i32>,
- /// Progress check prompt for evaluating loop completion
- pub loop_progress_check: Option<String>,
- /// Contracts to create within this chain
- pub contracts: Option<Vec<CreateChainContractRequest>>,
+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,
}
-/// Request to add a repository to a chain
-#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
+/// Directive summary for list views
+#[derive(Debug, Clone, Serialize, FromRow, ToSchema)]
#[serde(rename_all = "camelCase")]
-pub struct AddChainRepositoryRequest {
- /// Display name for the repository
- pub name: String,
- /// Remote repository URL (for remote repos)
- pub repository_url: Option<String>,
- /// Local filesystem path (for local repos)
- pub local_path: Option<String>,
- /// Source type: remote, local, or managed
- #[serde(default = "default_source_type")]
- pub source_type: String,
- /// Whether this is the primary repository
- #[serde(default)]
- pub is_primary: bool,
+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>,
}
-fn default_source_type() -> String {
- "remote".to_string()
+/// 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 create a contract within a chain
-#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
+/// Request to add a step to a chain
+#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
-pub struct CreateChainContractRequest {
- /// Name of the contract
+pub struct AddStepRequest {
pub name: String,
- /// Optional description
pub description: Option<String>,
- /// Contract type
- #[serde(default)]
+ pub step_type: Option<String>,
pub contract_type: Option<String>,
- /// Initial phase
pub initial_phase: Option<String>,
- /// Phases for the contract
+ pub task_plan: Option<String>,
pub phases: Option<Vec<String>>,
- /// Names of contracts this depends on (resolved to IDs)
- pub depends_on: Option<Vec<String>>,
- /// Tasks to create in this contract
- pub tasks: Option<Vec<CreateChainTaskRequest>>,
- /// Deliverables for this contract
- pub deliverables: Option<Vec<CreateChainDeliverableRequest>>,
- /// Position in GUI editor
+ 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>,
}
-/// Task definition within a chain contract
-#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct CreateChainTaskRequest {
- pub name: String,
- pub plan: String,
-}
-
-/// Deliverable definition within a chain contract
-#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct CreateChainDeliverableRequest {
- pub id: String,
- pub name: String,
- pub priority: Option<String>,
-}
-
-/// Validation configuration for checkpoint contracts.
-/// Checkpoint contracts validate the outputs of their upstream dependencies
-/// before allowing downstream contracts to proceed.
-#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct CheckpointValidation {
- /// Check that all required deliverables from upstream contracts exist
- #[serde(default)]
- pub check_deliverables: bool,
-
- /// Run tests in the repository (requires repository to be configured)
- #[serde(default)]
- pub run_tests: bool,
-
- /// Custom validation instructions for Claude to execute.
- /// Claude will review the outputs of upstream contracts and verify they meet these criteria.
- pub check_content: Option<String>,
-
- /// Action to take on validation failure: "block" (default), "retry", "warn"
- /// - block: Fail the checkpoint and block downstream contracts
- /// - retry: Mark upstream contracts for retry (up to max_retries)
- /// - warn: Log warning but allow downstream to proceed
- #[serde(default = "default_checkpoint_on_failure")]
- pub on_failure: String,
-
- /// Maximum retry attempts for upstream contracts (when on_failure = "retry")
- #[serde(default = "default_checkpoint_max_retries")]
- pub max_retries: i32,
-}
-
-fn default_checkpoint_on_failure() -> String {
- "block".to_string()
-}
-
-fn default_checkpoint_max_retries() -> i32 {
- 3
-}
-
-impl Default for CheckpointValidation {
- fn default() -> Self {
- Self {
- check_deliverables: false,
- run_tests: false,
- check_content: None,
- on_failure: default_checkpoint_on_failure(),
- max_retries: default_checkpoint_max_retries(),
- }
- }
-}
-
-/// Request to update an existing chain
-#[derive(Debug, Clone, Deserialize, ToSchema)]
+/// Request to update a step
+#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
-pub struct UpdateChainRequest {
+pub struct UpdateStepRequest {
pub name: Option<String>,
pub description: Option<String>,
- pub status: Option<String>,
- pub loop_enabled: Option<bool>,
- pub loop_max_iterations: Option<i32>,
- pub loop_progress_check: Option<String>,
- /// Version for optimistic locking
- pub version: Option<i32>,
-}
-
-/// Request to add a contract to a chain
-#[derive(Debug, Clone, Deserialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct AddContractToChainRequest {
- /// Existing contract ID to add
- pub contract_id: Option<Uuid>,
- /// Or create a new contract with this definition
- pub new_contract: Option<CreateChainContractRequest>,
- /// Contract IDs this depends on
+ pub task_plan: Option<String>,
pub depends_on: Option<Vec<Uuid>>,
- /// Position in GUI editor
+ 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>,
}
-/// Editor data model for GUI chain editor
-#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct ChainEditorData {
- pub id: Option<Uuid>,
- pub name: String,
- pub description: Option<String>,
- pub repositories: Vec<ChainRepository>,
- pub loop_enabled: bool,
- pub loop_max_iterations: Option<i32>,
- pub loop_progress_check: Option<String>,
- pub nodes: Vec<ChainEditorNode>,
- pub edges: Vec<ChainEditorEdge>,
-}
-
-/// Node in chain editor
-#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct ChainEditorNode {
- pub id: String,
- pub x: f64,
- pub y: f64,
- pub contract: ChainEditorContract,
-}
-
-/// Contract data in chain editor node
-#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct ChainEditorContract {
- pub name: String,
- pub description: Option<String>,
- #[serde(rename = "type")]
- pub contract_type: String,
- pub phases: Vec<String>,
- pub tasks: Vec<ChainEditorTask>,
- pub deliverables: Vec<ChainEditorDeliverable>,
-}
-
-/// Task in chain editor
-#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct ChainEditorTask {
- pub name: String,
- pub plan: String,
-}
-
-/// Deliverable in chain editor
-#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct ChainEditorDeliverable {
- pub id: String,
- pub name: String,
- pub priority: String,
-}
-
-/// Edge in chain editor
-#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct ChainEditorEdge {
- pub from: String,
- pub to: String,
-}
-
-// =============================================================================
-// Chain Contract Definitions (stored specs for on-demand contract creation)
-// =============================================================================
-
-/// Contract definition within a chain - stored spec before actual contract is created
-#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)]
+/// Chain graph response for DAG visualization
+#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
-pub struct ChainContractDefinition {
- pub id: Uuid,
+pub struct DirectiveChainGraphResponse {
pub chain_id: Uuid,
- pub name: String,
- pub description: Option<String>,
- pub contract_type: String,
- pub initial_phase: Option<String>,
- /// Names of other definitions this depends on
- #[sqlx(default)]
- pub depends_on_names: Vec<String>,
- /// Task definitions as JSON: [{name, plan}, ...]
- pub tasks: Option<serde_json::Value>,
- /// Deliverable definitions as JSON: [{id, name, priority}, ...]
- pub deliverables: Option<serde_json::Value>,
- /// Validation configuration for checkpoint contracts (JSON)
- pub validation: Option<serde_json::Value>,
- /// Requirement IDs this contract addresses (for traceability)
- #[sqlx(default)]
- #[serde(default)]
- pub requirement_ids: Vec<String>,
- /// Acceptance criteria for this contract (JSON array)
- #[serde(default)]
- pub acceptance_criteria: Option<serde_json::Value>,
- /// Whether LLM evaluation is enabled for this contract
- #[serde(default = "default_evaluation_enabled")]
- #[sqlx(default)]
- pub evaluation_enabled: bool,
- /// Pass threshold for evaluation (0.0-1.0)
- pub pass_threshold: Option<f64>,
- /// Position in GUI editor
- pub editor_x: Option<f64>,
- pub editor_y: Option<f64>,
- pub order_index: i32,
- pub created_at: DateTime<Utc>,
+ pub directive_id: Uuid,
+ pub nodes: Vec<DirectiveChainGraphNode>,
+ pub edges: Vec<DirectiveChainGraphEdge>,
}
-/// Request to add a contract definition to a chain
-#[derive(Debug, Clone, Deserialize, ToSchema)]
+/// Node in directive chain graph
+#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
-pub struct AddContractDefinitionRequest {
+pub struct DirectiveChainGraphNode {
+ pub id: Uuid,
pub name: String,
- pub description: Option<String>,
- #[serde(default = "default_contract_type")]
- pub contract_type: String,
- pub initial_phase: Option<String>,
- /// Names of other definitions this depends on
- pub depends_on: Option<Vec<String>>,
- /// Task definitions
- pub tasks: Option<Vec<CreateChainTaskRequest>>,
- /// Deliverable definitions
- pub deliverables: Option<Vec<CreateChainDeliverableRequest>>,
- /// Validation configuration (for checkpoint contracts)
- pub validation: Option<CheckpointValidation>,
- /// Position in GUI editor
- pub editor_x: Option<f64>,
- pub editor_y: Option<f64>,
-}
-
-fn default_contract_type() -> String {
- "simple".to_string()
-}
-
-/// Request to update a contract definition
-#[derive(Debug, Clone, Deserialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct UpdateContractDefinitionRequest {
- pub name: Option<String>,
- pub description: Option<String>,
- pub contract_type: Option<String>,
- pub initial_phase: Option<String>,
- pub depends_on: Option<Vec<String>>,
- pub tasks: Option<Vec<CreateChainTaskRequest>>,
- pub deliverables: Option<Vec<CreateChainDeliverableRequest>>,
- /// Validation configuration (for checkpoint contracts)
- pub validation: Option<CheckpointValidation>,
+ 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>,
}
-/// Request to start a chain (kept for backwards compatibility)
-#[derive(Debug, Clone, Deserialize, ToSchema)]
+/// Edge in directive chain graph
+#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
-pub struct StartChainRequest {
- /// Repository URL (reserved for future use)
- pub repository_url: Option<String>,
+pub struct DirectiveChainGraphEdge {
+ pub source: Uuid,
+ pub target: Uuid,
}
-/// Response when starting a chain
-#[derive(Debug, Clone, Serialize, ToSchema)]
+/// Start directive response
+#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
-pub struct StartChainResponse {
+pub struct StartDirectiveResponse {
+ pub directive_id: Uuid,
pub chain_id: Uuid,
- /// Root contracts created (those with no dependencies)
- pub contracts_created: Vec<Uuid>,
+ pub chain_generation: i32,
+ pub steps: Vec<ChainStep>,
pub status: String,
}
-/// Graph node for definitions (before contracts are created)
-#[derive(Debug, Clone, Serialize, ToSchema)]
+/// Request to create a verifier
+#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
-pub struct ChainDefinitionGraphNode {
- pub id: Uuid,
+pub struct CreateVerifierRequest {
pub name: String,
- pub contract_type: String,
- pub x: f64,
- pub y: f64,
- /// Whether this definition has been instantiated as a contract
- pub is_instantiated: bool,
- /// The contract ID if instantiated
- pub contract_id: Option<Uuid>,
- pub contract_status: Option<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>,
}
-/// Graph response for definitions
-#[derive(Debug, Clone, Serialize, ToSchema)]
+/// Request to update a verifier
+#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
-pub struct ChainDefinitionGraphResponse {
- pub chain_id: Uuid,
- pub chain_name: String,
- pub chain_status: String,
- pub nodes: Vec<ChainDefinitionGraphNode>,
- pub edges: Vec<ChainGraphEdge>,
+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>,
}
-// =============================================================================
-// Chain Directives (formal specification documents for directive-driven chains)
-// =============================================================================
-
-/// Chain directive - formal specification document that drives chain creation and evaluation
-#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)]
+/// Approval action request
+#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
-pub struct ChainDirective {
- pub id: Uuid,
- pub chain_id: Uuid,
- pub version: i32,
- /// Requirements as JSON: [{ id, title, description, priority, category, parentId? }]
- #[sqlx(json)]
- pub requirements: serde_json::Value,
- /// Acceptance criteria as JSON: [{ id, requirementIds[], description, testable, verificationMethod }]
- #[sqlx(json)]
- pub acceptance_criteria: serde_json::Value,
- /// Constraints as JSON: [{ id, type, description, impact }]
- #[sqlx(json)]
- pub constraints: serde_json::Value,
- /// External dependencies as JSON: [{ id, name, type, status, requiredBy[] }]
- #[sqlx(json)]
- pub external_dependencies: serde_json::Value,
- /// Source type: 'manual', 'llm_generated', 'imported'
- pub source_type: String,
- pub created_at: DateTime<Utc>,
- pub updated_at: DateTime<Utc>,
+pub struct ApprovalActionRequest {
+ pub response: Option<String>,
}
-/// Requirement in a directive
+/// 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,
- /// Priority: 'must', 'should', 'could', 'wont'
pub priority: String,
- /// Category: 'feature', 'infrastructure', 'testing', etc.
pub category: Option<String>,
- /// Parent requirement ID for hierarchical requirements
+ #[serde(skip_serializing_if = "Option::is_none")]
pub parent_id: Option<String>,
}
-/// Acceptance criterion in a directive
+/// Directive acceptance criterion
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct DirectiveAcceptanceCriterion {
pub id: String,
- /// Requirement IDs this criterion validates
+ #[serde(default)]
pub requirement_ids: Vec<String>,
pub description: String,
+ #[serde(default = "default_true")]
pub testable: bool,
- /// Verification method: 'automated', 'manual', 'review', 'llm'
- pub verification_method: String,
+ pub verification_method: Option<String>,
}
-/// Constraint in a directive
-#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct DirectiveConstraint {
- pub id: String,
- /// Type: 'technical', 'business', 'time', 'resource'
- #[serde(rename = "type")]
- pub constraint_type: String,
- pub description: String,
- /// Impact: 'high', 'medium', 'low'
- pub impact: String,
+fn default_true() -> bool {
+ true
}
-/// External dependency in a directive
-#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct DirectiveExternalDependency {
- pub id: String,
- pub name: String,
- /// Type: 'api', 'service', 'library', 'data'
- #[serde(rename = "type")]
- pub dependency_type: String,
- /// Status: 'available', 'pending', 'blocked'
- pub status: String,
- /// Requirement IDs that need this dependency
- pub required_by: Vec<String>,
-}
+// 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.
-/// Request to create or update a chain directive
-#[derive(Debug, Clone, Deserialize, ToSchema)]
+// 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 CreateChainDirectiveRequest {
- pub requirements: Option<Vec<DirectiveRequirement>>,
- pub acceptance_criteria: Option<Vec<DirectiveAcceptanceCriterion>>,
- pub constraints: Option<Vec<DirectiveConstraint>>,
- pub external_dependencies: Option<Vec<DirectiveExternalDependency>>,
- pub source_type: Option<String>,
+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 initialize a directive-driven chain
-#[derive(Debug, Clone, Deserialize, ToSchema)]
+/// Request to add a repository to a chain (legacy - used by chain runner)
+#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
-pub struct InitChainRequest {
- /// High-level goal/description for the directive contract
- pub goal: String,
- /// Repository URL for chain contracts
+pub struct AddChainRepositoryRequest {
+ pub name: String,
pub repository_url: Option<String>,
- /// Local path for chain contracts
pub local_path: Option<String>,
- /// Whether to enable phase guard (user approval between phases)
+ #[serde(default = "default_source_type")]
+ pub source_type: String,
#[serde(default)]
- pub phase_guard: bool,
-}
-
-/// Response from initializing a directive-driven chain
-#[derive(Debug, Clone, Serialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct InitChainResponse {
- pub chain_id: Uuid,
- pub directive_contract_id: Uuid,
- pub supervisor_task_id: Option<Uuid>,
-}
-
-// =============================================================================
-// Contract Evaluations (LLM evaluation results for completed contracts)
-// =============================================================================
-
-/// Evaluation status for chain contracts
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum EvaluationStatus {
- /// Not yet evaluated
- Pending,
- /// Currently being evaluated
- Evaluating,
- /// Evaluation passed
- Passed,
- /// Evaluation failed
- Failed,
- /// Contract is being reworked after failed evaluation
- Rework,
- /// Max retries exceeded, escalated to user
- Escalated,
- /// User approved despite partial failure
- ApprovedWithIssues,
-}
-
-impl std::fmt::Display for EvaluationStatus {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Self::Pending => write!(f, "pending"),
- Self::Evaluating => write!(f, "evaluating"),
- Self::Passed => write!(f, "passed"),
- Self::Failed => write!(f, "failed"),
- Self::Rework => write!(f, "rework"),
- Self::Escalated => write!(f, "escalated"),
- Self::ApprovedWithIssues => write!(f, "approved_with_issues"),
- }
- }
-}
-
-impl std::str::FromStr for EvaluationStatus {
- type Err = String;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- match s.to_lowercase().as_str() {
- "pending" => Ok(Self::Pending),
- "evaluating" => Ok(Self::Evaluating),
- "passed" => Ok(Self::Passed),
- "failed" => Ok(Self::Failed),
- "rework" => Ok(Self::Rework),
- "escalated" => Ok(Self::Escalated),
- "approved_with_issues" => Ok(Self::ApprovedWithIssues),
- _ => Err(format!("Unknown evaluation status: {}", s)),
- }
- }
-}
-
-/// Contract evaluation - LLM evaluation result after contract completion
-#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct ContractEvaluation {
- pub id: Uuid,
- pub contract_id: Uuid,
- pub chain_id: Option<Uuid>,
- pub chain_contract_id: Option<Uuid>,
- /// Evaluation attempt number (1-based)
- pub evaluation_number: i32,
- /// Model used for evaluation
- pub evaluator_model: Option<String>,
- /// Whether the evaluation passed
- pub passed: bool,
- /// Overall score (0.0-1.0)
- pub overall_score: Option<f64>,
- /// Per-criterion results as JSON
- #[sqlx(json)]
- pub criteria_results: serde_json::Value,
- /// Summary feedback from the evaluator
- pub summary_feedback: String,
- /// Instructions for rework if evaluation failed
- pub rework_instructions: Option<String>,
- /// Snapshot of directive at evaluation time
- pub directive_snapshot: Option<serde_json::Value>,
- /// Snapshot of deliverables at evaluation time
- pub deliverables_snapshot: Option<serde_json::Value>,
- pub started_at: DateTime<Utc>,
- pub completed_at: Option<DateTime<Utc>>,
- pub created_at: DateTime<Utc>,
-}
-
-/// Per-criterion evaluation result
-#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct EvaluationCriterionResult {
- pub criterion_id: String,
- pub criterion_text: String,
- pub passed: bool,
- /// Score (0.0-1.0)
- pub score: f64,
- pub feedback: String,
- /// Evidence supporting the evaluation
- pub evidence: Vec<String>,
-}
-
-/// Request to create a contract evaluation
-#[derive(Debug, Clone, Deserialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct CreateContractEvaluationRequest {
- pub contract_id: Uuid,
- pub chain_id: Option<Uuid>,
- pub chain_contract_id: Option<Uuid>,
- pub evaluator_model: Option<String>,
- pub passed: bool,
- pub overall_score: Option<f64>,
- pub criteria_results: Vec<EvaluationCriterionResult>,
- pub summary_feedback: String,
- pub rework_instructions: Option<String>,
+ pub is_primary: bool,
}
-/// Summary of contract evaluation for list views
-#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct ContractEvaluationSummary {
- pub id: Uuid,
- pub contract_id: Uuid,
- pub evaluation_number: i32,
- pub passed: bool,
- pub overall_score: Option<f64>,
- pub summary_feedback: String,
- pub created_at: DateTime<Utc>,
+fn default_source_type() -> String {
+ "remote".to_string()
}
-/// Response listing evaluations for a chain or contract
-#[derive(Debug, Clone, Serialize, ToSchema)]
+/// Request to create a contract within a chain (legacy - used by chain runner)
+#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
-pub struct ContractEvaluationsResponse {
- pub evaluations: Vec<ContractEvaluationSummary>,
- pub total: i64,
+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>,
}
-/// Traceability matrix entry - maps requirements to contracts
-#[derive(Debug, Clone, Serialize, ToSchema)]
+/// Task definition within a chain contract (legacy - used by chain runner)
+#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
-pub struct TraceabilityEntry {
- pub requirement_id: String,
- pub requirement_title: String,
- pub contract_definition_ids: Vec<Uuid>,
- pub contract_definition_names: Vec<String>,
- pub acceptance_criteria_ids: Vec<String>,
+pub struct CreateChainTaskRequest {
+ pub name: String,
+ pub plan: String,
}
-/// Response for directive traceability
-#[derive(Debug, Clone, Serialize, ToSchema)]
+/// Deliverable definition within a chain contract (legacy - used by chain runner)
+#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
-pub struct DirectiveTraceabilityResponse {
- pub chain_id: Uuid,
- pub entries: Vec<TraceabilityEntry>,
- /// Requirements not mapped to any contract
- pub uncovered_requirements: Vec<String>,
+pub struct CreateChainDeliverableRequest {
+ pub id: String,
+ pub name: String,
+ pub priority: Option<String>,
}
// =============================================================================
diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs
index 7be7bc8..eeda4a5 100644
--- a/makima/src/db/repository.rs
+++ b/makima/src/db/repository.rs
@@ -6,23 +6,22 @@ use sqlx::PgPool;
use uuid::Uuid;
use super::models::{
- AddChainRepositoryRequest, AddContractDefinitionRequest, AddContractToChainRequest, Chain,
- ChainContract, ChainContractDefinition, ChainContractDetail, ChainDefinitionGraphNode,
- ChainDefinitionGraphResponse, ChainDirective, ChainEditorContract, ChainEditorData,
- ChainEditorDeliverable, ChainEditorEdge, ChainEditorNode, ChainEditorTask, ChainEvent,
- ChainGraphEdge, ChainGraphNode, ChainGraphResponse, ChainRepository, ChainSummary,
- ChainWithContracts, CheckpointPatch, CheckpointPatchInfo, Contract, ContractChatConversation,
- ContractChatMessageRecord, ContractEvaluation, ContractEvaluationSummary, ContractEvent,
- ContractRepository, ContractSummary, ContractTypeTemplateRecord, ConversationMessage,
- ConversationSnapshot, CreateChainDirectiveRequest, CreateChainRequest,
- CreateContractEvaluationRequest, CreateContractRequest, CreateFileRequest, CreateTaskRequest,
- CreateTemplateRequest, Daemon, DaemonTaskAssignment, DaemonWithCapacity, DeliverableDefinition,
- DirectiveTraceabilityResponse, EvaluationCriterionResult, File, FileSummary, FileVersion,
- HistoryEvent, HistoryQueryFilters, InitChainRequest, InitChainResponse, MeshChatConversation,
- MeshChatMessageRecord, PhaseChangeResult, PhaseConfig, PhaseDefinition,
- SupervisorHeartbeatRecord, SupervisorState, Task, TaskCheckpoint, TaskEvent, TaskSummary,
- TraceabilityEntry, UpdateChainRequest, UpdateContractDefinitionRequest, UpdateContractRequest,
- UpdateFileRequest, UpdateTaskRequest, UpdateTemplateRequest,
+ // Core types
+ CheckpointPatch, CheckpointPatchInfo, Contract, ContractChatConversation,
+ ContractChatMessageRecord, ContractEvent, ContractRepository, ContractSummary,
+ ContractTypeTemplateRecord, ConversationMessage, ConversationSnapshot,
+ CreateContractRequest, CreateFileRequest, CreateTaskRequest, CreateTemplateRequest,
+ Daemon, DaemonTaskAssignment, DaemonWithCapacity, DeliverableDefinition,
+ File, FileSummary, FileVersion, HistoryEvent, HistoryQueryFilters,
+ MeshChatConversation, MeshChatMessageRecord, PhaseChangeResult, PhaseConfig,
+ 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.
@@ -4906,46 +4905,140 @@ pub async fn sync_supervisor_state(
}
// =============================================================================
-// Chain Operations (DAG of contracts for multi-contract orchestration)
+// 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
-/// Create a new chain for a specific owner.
-pub async fn create_chain_for_owner(
+// =============================================================================
+// 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: CreateChainRequest,
-) -> Result<Chain, sqlx::Error> {
- let loop_enabled = req.loop_enabled.unwrap_or(false);
- let loop_max_iterations = req.loop_max_iterations.unwrap_or(10);
-
- sqlx::query_as::<_, Chain>(
- r#"
- INSERT INTO chains (owner_id, name, description, loop_enabled, loop_max_iterations, loop_progress_check)
- VALUES ($1, $2, $3, $4, $5, $6)
+ 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(&req.name)
- .bind(&req.description)
- .bind(loop_enabled)
- .bind(loop_max_iterations)
- .bind(&req.loop_progress_check)
+ .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 chain by ID, scoped to owner.
-pub async fn get_chain_for_owner(
+/// Get a directive by ID, scoped to owner.
+pub async fn get_directive_for_owner(
pool: &PgPool,
id: Uuid,
owner_id: Uuid,
-) -> Result<Option<Chain>, sqlx::Error> {
- sqlx::query_as::<_, Chain>(
+) -> Result<Option<Directive>, sqlx::Error> {
+ sqlx::query_as::<_, Directive>(
r#"
- SELECT *
- FROM chains
- WHERE id = $1 AND owner_id = $2
+ SELECT * FROM directives WHERE id = $1 AND owner_id = $2
"#,
)
.bind(id)
@@ -4954,817 +5047,485 @@ pub async fn get_chain_for_owner(
.await
}
-/// Get a chain by ID (no owner check - for internal use).
-pub async fn get_chain(pool: &PgPool, id: Uuid) -> Result<Option<Chain>, sqlx::Error> {
- sqlx::query_as::<_, Chain>(
- r#"
- SELECT *
- FROM chains
- WHERE id = $1
- "#,
+/// 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 chains for a specific owner.
-pub async fn list_chains_for_owner(
+/// List directives for an owner.
+pub async fn list_directives_for_owner(
pool: &PgPool,
owner_id: Uuid,
-) -> Result<Vec<ChainSummary>, sqlx::Error> {
- sqlx::query_as::<_, ChainSummary>(
- r#"
- SELECT
- c.id,
- c.name,
- c.description,
- c.status,
- c.loop_enabled,
- c.loop_current_iteration,
- COUNT(DISTINCT cc.contract_id) as contract_count,
- COUNT(DISTINCT CASE WHEN con.status = 'completed' THEN cc.contract_id END) as completed_count,
- c.version,
- c.created_at
- FROM chains c
- LEFT JOIN chain_contracts cc ON cc.chain_id = c.id
- LEFT JOIN contracts con ON con.id = cc.contract_id
- WHERE c.owner_id = $1
- GROUP BY c.id
- ORDER BY c.created_at DESC
- "#,
- )
- .bind(owner_id)
- .fetch_all(pool)
- .await
+ 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 chain.
-pub async fn update_chain_for_owner(
+/// Update a directive with optimistic locking.
+pub async fn update_directive_for_owner(
pool: &PgPool,
id: Uuid,
owner_id: Uuid,
- req: UpdateChainRequest,
-) -> Result<Chain, RepositoryError> {
- // First get current version if optimistic locking requested
- if let Some(expected_version) = req.version {
- let current: Option<(i32,)> = sqlx::query_as(
- "SELECT version FROM chains WHERE id = $1 AND owner_id = $2",
- )
- .bind(id)
- .bind(owner_id)
- .fetch_optional(pool)
- .await?;
+ 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 let Some((actual_version,)) = current {
- if actual_version != expected_version {
- return Err(RepositoryError::VersionConflict {
- expected: expected_version,
- actual: actual_version,
- });
- }
- }
+ if current != req.version {
+ return Err(RepositoryError::VersionConflict {
+ expected: req.version,
+ actual: current,
+ });
}
- let result = sqlx::query_as::<_, Chain>(
- r#"
- UPDATE chains
- SET
- name = COALESCE($3, name),
- description = COALESCE($4, description),
- status = COALESCE($5, status),
- loop_enabled = COALESCE($6, loop_enabled),
- loop_max_iterations = COALESCE($7, loop_max_iterations),
- loop_progress_check = COALESCE($8, loop_progress_check),
+ 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
+ WHERE id = $1 AND owner_id = $2 AND version = $16
RETURNING *
"#,
)
.bind(id)
.bind(owner_id)
- .bind(&req.name)
- .bind(&req.description)
- .bind(&req.status)
- .bind(req.loop_enabled)
- .bind(req.loop_max_iterations)
- .bind(&req.loop_progress_check)
+ .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(result)
+ Ok(directive)
}
-/// Delete (archive) a chain.
-pub async fn delete_chain_for_owner(
+/// Update directive status.
+pub async fn update_directive_status(
pool: &PgPool,
id: Uuid,
- owner_id: Uuid,
-) -> Result<bool, sqlx::Error> {
- let result = sqlx::query(
- r#"
- UPDATE chains
- 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)
-}
-
-/// Add a contract to a chain.
-pub async fn add_contract_to_chain(
- pool: &PgPool,
- chain_id: Uuid,
- contract_id: Uuid,
- depends_on: Vec<Uuid>,
- order_index: i32,
- editor_x: Option<f64>,
- editor_y: Option<f64>,
-) -> Result<ChainContract, sqlx::Error> {
- // Also update the contract's chain_id
- sqlx::query("UPDATE contracts SET chain_id = $1 WHERE id = $2")
- .bind(chain_id)
- .bind(contract_id)
- .execute(pool)
- .await?;
-
- sqlx::query_as::<_, ChainContract>(
+ status: &str,
+) -> Result<Directive, sqlx::Error> {
+ sqlx::query_as::<_, Directive>(
r#"
- INSERT INTO chain_contracts (chain_id, contract_id, depends_on, order_index, editor_x, editor_y)
- VALUES ($1, $2, $3, $4, $5, $6)
- ON CONFLICT (chain_id, contract_id) DO UPDATE SET
- depends_on = EXCLUDED.depends_on,
- order_index = EXCLUDED.order_index,
- editor_x = EXCLUDED.editor_x,
- editor_y = EXCLUDED.editor_y
+ 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(chain_id)
- .bind(contract_id)
- .bind(&depends_on)
- .bind(order_index)
- .bind(editor_x)
- .bind(editor_y)
+ .bind(id)
+ .bind(status)
.fetch_one(pool)
.await
}
-/// Remove a contract from a chain.
-pub async fn remove_contract_from_chain(
+/// Archive a directive (soft delete).
+pub async fn archive_directive_for_owner(
pool: &PgPool,
- chain_id: Uuid,
- contract_id: Uuid,
+ id: Uuid,
+ owner_id: Uuid,
) -> Result<bool, sqlx::Error> {
- // Clear the contract's chain_id
- sqlx::query("UPDATE contracts SET chain_id = NULL WHERE id = $1 AND chain_id = $2")
- .bind(contract_id)
- .bind(chain_id)
- .execute(pool)
- .await?;
-
let result = sqlx::query(
r#"
- DELETE FROM chain_contracts
- WHERE chain_id = $1 AND contract_id = $2
+ UPDATE directives SET status = 'archived', updated_at = NOW()
+ WHERE id = $1 AND owner_id = $2
"#,
)
- .bind(chain_id)
- .bind(contract_id)
+ .bind(id)
+ .bind(owner_id)
.execute(pool)
.await?;
-
Ok(result.rows_affected() > 0)
}
-/// List contracts in a chain with their details.
-pub async fn list_chain_contracts(
- pool: &PgPool,
- chain_id: Uuid,
-) -> Result<Vec<ChainContractDetail>, sqlx::Error> {
- sqlx::query_as::<_, ChainContractDetail>(
- r#"
- SELECT
- cc.id as chain_contract_id,
- cc.contract_id,
- c.name as contract_name,
- c.status as contract_status,
- c.phase as contract_phase,
- cc.depends_on,
- cc.order_index,
- cc.editor_x,
- cc.editor_y,
- cc.evaluation_status,
- cc.evaluation_retry_count,
- cc.max_evaluation_retries,
- cc.created_at
- FROM chain_contracts cc
- JOIN contracts c ON c.id = cc.contract_id
- WHERE cc.chain_id = $1
- ORDER BY cc.order_index ASC
- "#,
- )
- .bind(chain_id)
- .fetch_all(pool)
- .await
-}
-
-/// Get chain with all contracts for detail view.
-pub async fn get_chain_with_contracts(
+/// Get directive with full progress info.
+pub async fn get_directive_with_progress(
pool: &PgPool,
- chain_id: Uuid,
+ id: Uuid,
owner_id: Uuid,
-) -> Result<Option<ChainWithContracts>, sqlx::Error> {
- let chain = get_chain_for_owner(pool, chain_id, owner_id).await?;
-
- match chain {
- Some(chain) => {
- let contracts = list_chain_contracts(pool, chain_id).await?;
- let repositories = list_chain_repositories(pool, chain_id).await?;
- Ok(Some(ChainWithContracts {
- chain,
- contracts,
- repositories,
- }))
- }
- None => Ok(None),
- }
-}
+) -> Result<Option<DirectiveWithProgress>, sqlx::Error> {
+ let directive = match get_directive_for_owner(pool, id, owner_id).await? {
+ Some(d) => d,
+ None => return Ok(None),
+ };
-// =============================================================================
-// Chain Repository Operations
-// =============================================================================
+ let chain = if let Some(chain_id) = directive.current_chain_id {
+ get_directive_chain(pool, chain_id).await?
+ } else {
+ None
+ };
-/// List all repositories for a chain.
-pub async fn list_chain_repositories(
- pool: &PgPool,
- chain_id: Uuid,
-) -> Result<Vec<ChainRepository>, sqlx::Error> {
- sqlx::query_as::<_, ChainRepository>(
- r#"
- SELECT *
- FROM chain_repositories
- WHERE chain_id = $1
- ORDER BY is_primary DESC, created_at ASC
- "#,
- )
- .bind(chain_id)
- .fetch_all(pool)
- .await
-}
+ let steps = if let Some(ref c) = chain {
+ list_chain_steps(pool, c.id).await?
+ } else {
+ vec![]
+ };
-/// Get a chain repository by ID.
-pub async fn get_chain_repository(
- pool: &PgPool,
- chain_id: Uuid,
- repository_id: Uuid,
-) -> Result<Option<ChainRepository>, sqlx::Error> {
- sqlx::query_as::<_, ChainRepository>(
- r#"
- SELECT *
- FROM chain_repositories
- WHERE id = $1 AND chain_id = $2
- "#,
- )
- .bind(repository_id)
- .bind(chain_id)
- .fetch_optional(pool)
- .await
+ 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,
+ }))
}
-/// Add a repository to a chain.
-pub async fn add_chain_repository(
+// =============================================================================
+// Directive Chain Operations
+// =============================================================================
+
+/// Create a new chain generation for a directive.
+pub async fn create_directive_chain(
pool: &PgPool,
- chain_id: Uuid,
- req: &AddChainRepositoryRequest,
-) -> Result<ChainRepository, sqlx::Error> {
- // If is_primary, clear other primaries first
- if req.is_primary {
- sqlx::query(
- r#"
- UPDATE chain_repositories
- SET is_primary = false, updated_at = NOW()
- WHERE chain_id = $1 AND is_primary = true
- "#,
- )
- .bind(chain_id)
- .execute(pool)
- .await?;
- }
+ 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?;
- sqlx::query_as::<_, ChainRepository>(
+ let chain = sqlx::query_as::<_, DirectiveChain>(
r#"
- INSERT INTO chain_repositories (chain_id, name, repository_url, local_path, source_type, status, is_primary)
- VALUES ($1, $2, $3, $4, $5, 'ready', $6)
+ INSERT INTO directive_chains (directive_id, generation, name, description, rationale, planning_model)
+ VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *
"#,
)
- .bind(chain_id)
- .bind(&req.name)
- .bind(&req.repository_url)
- .bind(&req.local_path)
- .bind(&req.source_type)
- .bind(req.is_primary)
+ .bind(directive_id)
+ .bind(generation)
+ .bind(name)
+ .bind(description)
+ .bind(rationale)
+ .bind(planning_model)
.fetch_one(pool)
- .await
-}
-
-/// Delete a repository from a chain.
-pub async fn delete_chain_repository(
- pool: &PgPool,
- chain_id: Uuid,
- repository_id: Uuid,
-) -> Result<bool, sqlx::Error> {
- let result = sqlx::query(
- r#"
- DELETE FROM chain_repositories
- WHERE id = $1 AND chain_id = $2
- "#,
- )
- .bind(repository_id)
- .bind(chain_id)
- .execute(pool)
.await?;
- Ok(result.rows_affected() > 0)
-}
-
-/// Set a repository as primary for a chain.
-pub async fn set_chain_repository_primary(
- pool: &PgPool,
- chain_id: Uuid,
- repository_id: Uuid,
-) -> Result<ChainRepository, sqlx::Error> {
- // Clear existing primary
+ // Update directive to point to new chain and increment generation count
sqlx::query(
r#"
- UPDATE chain_repositories
- SET is_primary = false, updated_at = NOW()
- WHERE chain_id = $1 AND is_primary = true
+ UPDATE directives SET
+ current_chain_id = $2,
+ chain_generation_count = chain_generation_count + 1,
+ updated_at = NOW()
+ WHERE id = $1
"#,
)
- .bind(chain_id)
+ .bind(directive_id)
+ .bind(chain.id)
.execute(pool)
.await?;
- // Set new primary
- sqlx::query_as::<_, ChainRepository>(
- r#"
- UPDATE chain_repositories
- SET is_primary = true, updated_at = NOW()
- WHERE id = $1 AND chain_id = $2
- RETURNING *
- "#,
- )
- .bind(repository_id)
- .bind(chain_id)
- .fetch_one(pool)
- .await
+ Ok(chain)
}
-/// Get the primary repository for a chain.
-pub async fn get_chain_primary_repository(
- pool: &PgPool,
- chain_id: Uuid,
-) -> Result<Option<ChainRepository>, sqlx::Error> {
- sqlx::query_as::<_, ChainRepository>(
- r#"
- SELECT *
- FROM chain_repositories
- WHERE chain_id = $1 AND is_primary = true
- "#,
+/// 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(chain_id)
+ .bind(id)
.fetch_optional(pool)
.await
}
-/// Get chain graph structure for visualization.
-pub async fn get_chain_graph(
- pool: &PgPool,
- chain_id: Uuid,
-) -> Result<Option<ChainGraphResponse>, sqlx::Error> {
- let chain = get_chain(pool, chain_id).await?;
-
- match chain {
- Some(chain) => {
- let contracts = list_chain_contracts(pool, chain_id).await?;
-
- let nodes: Vec<ChainGraphNode> = contracts
- .iter()
- .map(|c| ChainGraphNode {
- id: c.chain_contract_id,
- contract_id: c.contract_id,
- name: c.contract_name.clone(),
- status: c.contract_status.clone(),
- phase: c.contract_phase.clone(),
- x: c.editor_x.unwrap_or(0.0),
- y: c.editor_y.unwrap_or(0.0),
- })
- .collect();
-
- let mut edges: Vec<ChainGraphEdge> = Vec::new();
- for contract in &contracts {
- for dep_id in &contract.depends_on {
- // Find the chain_contract_id for this dependency
- if let Some(dep) = contracts.iter().find(|c| c.contract_id == *dep_id) {
- edges.push(ChainGraphEdge {
- from: dep.chain_contract_id,
- to: contract.chain_contract_id,
- });
- }
- }
- }
-
- Ok(Some(ChainGraphResponse {
- chain_id: chain.id,
- chain_name: chain.name,
- chain_status: chain.status,
- nodes,
- edges,
- }))
- }
- None => Ok(None),
- }
-}
-
-/// Record a chain event.
-pub async fn record_chain_event(
- pool: &PgPool,
- chain_id: Uuid,
- event_type: &str,
- contract_id: Option<Uuid>,
- event_data: Option<serde_json::Value>,
-) -> Result<ChainEvent, sqlx::Error> {
- sqlx::query_as::<_, ChainEvent>(
+/// 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#"
- INSERT INTO chain_events (chain_id, event_type, contract_id, event_data)
- VALUES ($1, $2, $3, $4)
- RETURNING *
+ SELECT dc.* FROM directive_chains dc
+ JOIN directives d ON d.current_chain_id = dc.id
+ WHERE d.id = $1
"#,
)
- .bind(chain_id)
- .bind(event_type)
- .bind(contract_id)
- .bind(event_data)
- .fetch_one(pool)
+ .bind(directive_id)
+ .fetch_optional(pool)
.await
}
-/// List chain events.
-pub async fn list_chain_events(
+/// Update chain status.
+pub async fn update_chain_status(
pool: &PgPool,
chain_id: Uuid,
-) -> Result<Vec<ChainEvent>, sqlx::Error> {
- sqlx::query_as::<_, ChainEvent>(
- r#"
- SELECT *
- FROM chain_events
- WHERE chain_id = $1
- ORDER BY created_at DESC
- "#,
- )
- .bind(chain_id)
- .fetch_all(pool)
- .await
-}
-
-/// Increment chain loop iteration.
-pub async fn increment_chain_loop(pool: &PgPool, chain_id: Uuid) -> Result<Chain, sqlx::Error> {
- sqlx::query_as::<_, Chain>(
+ status: &str,
+) -> Result<DirectiveChain, sqlx::Error> {
+ sqlx::query_as::<_, DirectiveChain>(
r#"
- UPDATE chains
- SET loop_current_iteration = COALESCE(loop_current_iteration, 0) + 1,
+ 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
}
-/// Mark a chain as completed.
-pub async fn complete_chain(pool: &PgPool, chain_id: Uuid) -> Result<Chain, sqlx::Error> {
- sqlx::query_as::<_, Chain>(
+/// 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 chains
- SET status = 'completed',
- updated_at = NOW()
+ UPDATE directive_chains SET status = 'superseded', completed_at = NOW(), updated_at = NOW()
WHERE id = $1
- RETURNING *
- "#,
- )
- .bind(chain_id)
- .fetch_one(pool)
- .await
-}
-
-/// Get contracts in a chain that have no pending dependencies (ready to start).
-/// Returns contracts where all depends_on contracts are completed.
-pub async fn get_ready_chain_contracts(
- pool: &PgPool,
- chain_id: Uuid,
-) -> Result<Vec<ChainContractDetail>, sqlx::Error> {
- sqlx::query_as::<_, ChainContractDetail>(
- r#"
- SELECT
- cc.id as chain_contract_id,
- cc.contract_id,
- c.name as contract_name,
- c.status as contract_status,
- c.phase as contract_phase,
- cc.depends_on,
- cc.order_index,
- cc.editor_x,
- cc.editor_y
- FROM chain_contracts cc
- JOIN contracts c ON c.id = cc.contract_id
- WHERE cc.chain_id = $1
- AND c.status = 'active'
- AND (
- -- No dependencies
- cc.depends_on IS NULL
- OR array_length(cc.depends_on, 1) IS NULL
- OR array_length(cc.depends_on, 1) = 0
- -- Or all dependencies completed
- OR NOT EXISTS (
- SELECT 1
- FROM unnest(cc.depends_on) AS dep_id
- JOIN contracts dep ON dep.id = dep_id
- WHERE dep.status != 'completed'
- )
- )
- ORDER BY cc.order_index ASC
"#,
)
.bind(chain_id)
- .fetch_all(pool)
- .await
-}
-
-/// Check if all contracts in a chain are completed.
-pub async fn is_chain_complete(pool: &PgPool, chain_id: Uuid) -> Result<bool, sqlx::Error> {
- let result: (i64,) = sqlx::query_as(
- r#"
- SELECT COUNT(*)
- FROM chain_contracts cc
- JOIN contracts c ON c.id = cc.contract_id
- WHERE cc.chain_id = $1
- AND c.status != 'completed'
- "#,
- )
- .bind(chain_id)
- .fetch_one(pool)
+ .execute(pool)
.await?;
-
- Ok(result.0 == 0)
-}
-
-/// Get chain editor data for the GUI editor.
-pub async fn get_chain_editor_data(
- pool: &PgPool,
- chain_id: Uuid,
- owner_id: Uuid,
-) -> Result<Option<ChainEditorData>, sqlx::Error> {
- let chain = get_chain_for_owner(pool, chain_id, owner_id).await?;
-
- match chain {
- Some(chain) => {
- let contracts = list_chain_contracts(pool, chain_id).await?;
- let repositories = list_chain_repositories(pool, chain_id).await?;
-
- // Build nodes
- let nodes: Vec<ChainEditorNode> = contracts
- .iter()
- .map(|c| ChainEditorNode {
- id: c.contract_id.to_string(),
- x: c.editor_x.unwrap_or(0.0),
- y: c.editor_y.unwrap_or(0.0),
- contract: ChainEditorContract {
- name: c.contract_name.clone(),
- description: None, // Would need to join with full contract data
- contract_type: "simple".to_string(),
- phases: vec!["plan".to_string(), "execute".to_string()],
- tasks: vec![],
- deliverables: vec![],
- },
- })
- .collect();
-
- // Build edges
- let edges: Vec<ChainEditorEdge> = contracts
- .iter()
- .flat_map(|c| {
- c.depends_on.iter().map(move |dep_id| ChainEditorEdge {
- from: dep_id.to_string(),
- to: c.contract_id.to_string(),
- })
- })
- .collect();
-
- Ok(Some(ChainEditorData {
- id: Some(chain.id),
- name: chain.name,
- description: chain.description,
- repositories,
- loop_enabled: chain.loop_enabled,
- loop_max_iterations: chain.loop_max_iterations,
- loop_progress_check: chain.loop_progress_check,
- nodes,
- edges,
- }))
- }
- None => Ok(None),
- }
+ Ok(())
}
// =============================================================================
-// Chain Contract Definition Operations
+// Chain Step Operations
// =============================================================================
-/// Create a new contract definition in a chain.
-pub async fn create_chain_contract_definition(
+/// Create a new step in a chain.
+pub async fn create_chain_step(
pool: &PgPool,
chain_id: Uuid,
- req: AddContractDefinitionRequest,
-) -> Result<ChainContractDefinition, sqlx::Error> {
- // Get the next order index
- let max_order: Option<i32> = sqlx::query_scalar(
- "SELECT MAX(order_index) FROM chain_contract_definitions WHERE chain_id = $1",
+ 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 order_index = max_order.unwrap_or(-1) + 1;
-
- // Convert tasks, deliverables, and validation to JSON
- let tasks_json = req.tasks.as_ref().map(|t| serde_json::to_value(t).unwrap());
- let deliverables_json = req
- .deliverables
- .as_ref()
- .map(|d| serde_json::to_value(d).unwrap());
- let validation_json = req
- .validation
- .as_ref()
- .map(|v| serde_json::to_value(v).unwrap());
- let depends_on_names: Vec<String> = req.depends_on.unwrap_or_default();
-
- sqlx::query_as::<_, ChainContractDefinition>(
- r#"
- INSERT INTO chain_contract_definitions
- (chain_id, name, description, contract_type, initial_phase, depends_on_names, tasks, deliverables, validation, editor_x, editor_y, order_index)
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
+ 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(&req.contract_type)
+ .bind(&step_type)
+ .bind(&contract_type)
.bind(&req.initial_phase)
- .bind(&depends_on_names)
- .bind(&tasks_json)
- .bind(&deliverables_json)
- .bind(&validation_json)
- .bind(req.editor_x)
- .bind(req.editor_y)
+ .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
-}
+ .await?;
-/// List all contract definitions in a chain.
-pub async fn list_chain_contract_definitions(
- pool: &PgPool,
- chain_id: Uuid,
-) -> Result<Vec<ChainContractDefinition>, sqlx::Error> {
- sqlx::query_as::<_, ChainContractDefinition>(
- r#"
- SELECT * FROM chain_contract_definitions
- WHERE chain_id = $1
- ORDER BY order_index ASC
- "#,
+ // 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)
- .fetch_all(pool)
- .await
+ .execute(pool)
+ .await?;
+
+ Ok(step)
}
-/// Get a specific contract definition.
-pub async fn get_chain_contract_definition(
- pool: &PgPool,
- definition_id: Uuid,
-) -> Result<Option<ChainContractDefinition>, sqlx::Error> {
- sqlx::query_as::<_, ChainContractDefinition>(
- "SELECT * FROM chain_contract_definitions WHERE id = $1",
+/// 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(definition_id)
+ .bind(id)
.fetch_optional(pool)
.await
}
-/// Update a contract definition.
-pub async fn update_chain_contract_definition(
- pool: &PgPool,
- definition_id: Uuid,
- req: UpdateContractDefinitionRequest,
-) -> Result<ChainContractDefinition, sqlx::Error> {
- let tasks_json = req.tasks.as_ref().map(|t| serde_json::to_value(t).unwrap());
- let deliverables_json = req
- .deliverables
- .as_ref()
- .map(|d| serde_json::to_value(d).unwrap());
- let validation_json = req
- .validation
- .as_ref()
- .map(|v| serde_json::to_value(v).unwrap());
+/// 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
+}
- sqlx::query_as::<_, ChainContractDefinition>(
+/// 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_contract_definitions SET
+ UPDATE chain_steps SET
name = COALESCE($2, name),
description = COALESCE($3, description),
- contract_type = COALESCE($4, contract_type),
- initial_phase = COALESCE($5, initial_phase),
- depends_on_names = COALESCE($6, depends_on_names),
- tasks = COALESCE($7, tasks),
- deliverables = COALESCE($8, deliverables),
- validation = COALESCE($9, validation),
- editor_x = COALESCE($10, editor_x),
- editor_y = COALESCE($11, editor_y)
+ 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(definition_id)
+ .bind(step_id)
.bind(&req.name)
.bind(&req.description)
- .bind(&req.contract_type)
- .bind(&req.initial_phase)
+ .bind(&req.task_plan)
.bind(&req.depends_on)
- .bind(&tasks_json)
- .bind(&deliverables_json)
- .bind(&validation_json)
+ .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 contract definition.
-pub async fn delete_chain_contract_definition(
- pool: &PgPool,
- definition_id: Uuid,
-) -> Result<bool, sqlx::Error> {
- let result = sqlx::query("DELETE FROM chain_contract_definitions WHERE id = $1")
- .bind(definition_id)
+/// 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)
}
-/// Get definitions that are ready to be instantiated (all dependencies are satisfied).
-/// A definition is ready if all definitions it depends on have been instantiated as contracts
-/// and those contracts have completed.
-pub async fn get_ready_definitions(
- pool: &PgPool,
- chain_id: Uuid,
-) -> Result<Vec<ChainContractDefinition>, sqlx::Error> {
- sqlx::query_as::<_, ChainContractDefinition>(
- r#"
- SELECT d.*
- FROM chain_contract_definitions d
- WHERE d.chain_id = $1
- -- Not already instantiated
- AND NOT EXISTS (
- SELECT 1 FROM chain_contracts cc
- WHERE cc.definition_id = d.id
- )
- -- All dependencies satisfied (either no deps, or all deps have completed contracts)
- AND (
- cardinality(d.depends_on_names) = 0
- OR NOT EXISTS (
- SELECT 1 FROM unnest(d.depends_on_names) AS dep_name
- WHERE NOT EXISTS (
- SELECT 1 FROM chain_contract_definitions dep_def
- JOIN chain_contracts cc ON cc.definition_id = dep_def.id
- JOIN contracts c ON c.id = cc.contract_id
- WHERE dep_def.chain_id = d.chain_id
- AND dep_def.name = dep_name
- AND c.status = 'completed'
- )
- )
- )
- ORDER BY d.order_index ASC
+/// 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)
@@ -5772,909 +5533,500 @@ pub async fn get_ready_definitions(
.await
}
-/// Get the definition graph for visualization.
-pub async fn get_chain_definition_graph(
+/// Update step status.
+pub async fn update_step_status(
pool: &PgPool,
- chain_id: Uuid,
-) -> Result<Option<ChainDefinitionGraphResponse>, sqlx::Error> {
- let chain = sqlx::query_as::<_, Chain>("SELECT * FROM chains WHERE id = $1")
- .bind(chain_id)
- .fetch_optional(pool)
- .await?;
-
- let Some(chain) = chain else {
- return Ok(None);
- };
-
- let definitions = list_chain_contract_definitions(pool, chain_id).await?;
-
- // Get instantiated contracts for each definition
- let chain_contracts = list_chain_contracts(pool, chain_id).await?;
- let instantiated: std::collections::HashMap<Uuid, &ChainContractDetail> = chain_contracts
- .iter()
- .filter_map(|cc| {
- // Find definition_id from cc - we need to query this
- // For now, match by name
- definitions
- .iter()
- .find(|d| d.name == cc.contract_name)
- .map(|d| (d.id, cc))
- })
- .collect();
-
- let nodes: Vec<ChainDefinitionGraphNode> = definitions
- .iter()
- .map(|d| {
- let cc = instantiated.get(&d.id);
- ChainDefinitionGraphNode {
- id: d.id,
- name: d.name.clone(),
- contract_type: d.contract_type.clone(),
- x: d.editor_x.unwrap_or(0.0),
- y: d.editor_y.unwrap_or(0.0),
- is_instantiated: cc.is_some(),
- contract_id: cc.map(|c| c.contract_id),
- contract_status: cc.map(|c| c.contract_status.clone()),
- }
- })
- .collect();
-
- // Build edges from depends_on_names
- let name_to_id: std::collections::HashMap<&str, Uuid> =
- definitions.iter().map(|d| (d.name.as_str(), d.id)).collect();
-
- let edges: Vec<ChainGraphEdge> = definitions
- .iter()
- .flat_map(|d| {
- let target_id = d.id;
- let name_to_id = &name_to_id;
- d.depends_on_names.iter().filter_map(move |dep_name| {
- name_to_id
- .get(dep_name.as_str())
- .map(|&from_id| ChainGraphEdge { from: from_id, to: target_id })
- })
- })
- .collect();
-
- Ok(Some(ChainDefinitionGraphResponse {
- chain_id: chain.id,
- chain_name: chain.name,
- chain_status: chain.status,
- nodes,
- edges,
- }))
-}
-
-/// Update chain status.
-pub async fn update_chain_status(
- pool: &PgPool,
- chain_id: Uuid,
+ step_id: Uuid,
status: &str,
-) -> Result<(), sqlx::Error> {
- sqlx::query("UPDATE chains SET status = $2, updated_at = NOW() WHERE id = $1")
- .bind(chain_id)
- .bind(status)
- .execute(pool)
- .await?;
- Ok(())
-}
-
-// =============================================================================
-// Chain Progression
-// =============================================================================
-
-/// Result of chain progression check
-#[derive(Debug)]
-pub struct ChainProgressionResult {
- /// Contracts created from ready definitions
- pub contracts_created: Vec<Uuid>,
- /// Whether all definitions are instantiated and completed (chain is done)
- pub chain_completed: bool,
-}
-
-/// Progress a chain by creating contracts from ready definitions.
-///
-/// This is called when a contract in the chain completes. It:
-/// 1. Finds definitions whose dependencies are all satisfied (completed)
-/// 2. Creates contracts from those definitions
-/// 3. Links them to the chain
-/// 4. Checks if chain is complete (all definitions instantiated and completed)
-pub async fn progress_chain(
- pool: &PgPool,
- chain_id: Uuid,
- owner_id: Uuid,
-) -> Result<ChainProgressionResult, sqlx::Error> {
- let mut contracts_created = Vec::new();
-
- // Get all definitions for this chain
- let definitions = list_chain_contract_definitions(pool, chain_id).await?;
- if definitions.is_empty() {
- return Ok(ChainProgressionResult {
- contracts_created: vec![],
- chain_completed: true,
- });
- }
-
- // Get existing chain contracts to know what's already instantiated
- let chain_contracts = list_chain_contracts(pool, chain_id).await?;
-
- // Build a map of definition name -> instantiated contract status
- let instantiated: std::collections::HashMap<String, Option<String>> = chain_contracts
- .iter()
- .map(|cc| (cc.contract_name.clone(), Some(cc.contract_status.clone())))
- .collect();
-
- // Find definitions that are ready to be instantiated:
- // - Not yet instantiated
- // - All dependencies are instantiated AND completed
- for def in &definitions {
- // Skip if already instantiated
- if instantiated.contains_key(&def.name) {
- continue;
- }
-
- // Check if all dependencies are completed
- let deps_satisfied = def.depends_on_names.iter().all(|dep_name| {
- instantiated
- .get(dep_name)
- .map(|status| status.as_deref() == Some("completed"))
- .unwrap_or(false)
- });
-
- // Root definitions (no dependencies) are always ready
- let is_root = def.depends_on_names.is_empty();
-
- if is_root || deps_satisfied {
- // Create contract from definition
- match create_contract_from_definition(pool, chain_id, owner_id, def).await {
- Ok(contract_id) => {
- contracts_created.push(contract_id);
- tracing::info!(
- chain_id = %chain_id,
- definition_name = %def.name,
- contract_id = %contract_id,
- "Created contract from chain definition"
- );
- }
- Err(e) => {
- tracing::error!(
- chain_id = %chain_id,
- definition_name = %def.name,
- error = %e,
- "Failed to create contract from chain definition"
- );
- }
- }
- }
- }
-
- // Check if chain is complete (all definitions instantiated and completed)
- let updated_contracts = list_chain_contracts(pool, chain_id).await?;
- let all_instantiated = definitions.len() == updated_contracts.len();
- let all_completed = updated_contracts
- .iter()
- .all(|cc| cc.contract_status == "completed");
- let chain_completed = all_instantiated && all_completed;
-
- if chain_completed {
- update_chain_status(pool, chain_id, "completed").await?;
- tracing::info!(chain_id = %chain_id, "Chain completed - all contracts done");
- }
-
- Ok(ChainProgressionResult {
- contracts_created,
- chain_completed,
- })
-}
-
-/// Task definition parsed from JSON (matches chain YAML format)
-#[derive(Debug, Clone, serde::Deserialize)]
-struct ChainTaskDef {
- name: String,
- plan: String,
-}
-
-/// Validation config parsed from definition JSON
-#[derive(Debug, Clone, serde::Deserialize)]
-#[serde(rename_all = "camelCase")]
-struct ValidationConfig {
- #[serde(default)]
- check_deliverables: bool,
- #[serde(default)]
- run_tests: bool,
- check_content: Option<String>,
- #[serde(default = "default_on_failure_str")]
- on_failure: String,
- #[serde(default = "default_max_retries_val")]
- max_retries: i32,
-}
-
-fn default_on_failure_str() -> String {
- "block".to_string()
-}
-
-fn default_max_retries_val() -> i32 {
- 3
-}
-
-/// Generate a validation plan for a checkpoint contract.
-fn generate_checkpoint_plan(
- def: &ChainContractDefinition,
- upstream_contracts: &[&ChainContractDetail],
- validation: &ValidationConfig,
-) -> String {
- let upstream_names: Vec<&str> = upstream_contracts.iter().map(|c| c.contract_name.as_str()).collect();
-
- let mut plan = format!(
- r#"# Checkpoint Validation: {}
-
-You are validating the outputs of upstream contracts before allowing downstream work to proceed.
-
-## Upstream Contracts to Validate
-{}
-
-"#,
- def.name,
- upstream_names.iter().map(|n| format!("- {}", n)).collect::<Vec<_>>().join("\n")
- );
-
- // Add deliverables check section
- if validation.check_deliverables {
- plan.push_str(r#"## Deliverables Check
-Verify that all required deliverables from upstream contracts exist and are properly completed.
-
-Use the makima CLI to check contract status:
-```bash
-makima contract status <contract_id>
-```
-
-For each upstream contract, verify:
-1. Contract status is "completed"
-2. All required deliverables are marked as complete
-3. Deliverable content exists and is not empty
-
-"#);
- }
-
- // Add tests check section
- if validation.run_tests {
- plan.push_str(r#"## Tests Check
-Run the test suite to verify the codebase is in a good state.
-
-```bash
-# Run tests appropriate for the project type
-npm test # for Node.js projects
-cargo test # for Rust projects
-pytest # for Python projects
-go test ./... # for Go projects
-```
-
-Verify:
-1. All tests pass
-2. No new test failures introduced
-3. Test coverage is acceptable
-
-"#);
- }
-
- // Add custom content check section
- if let Some(content_check) = &validation.check_content {
- plan.push_str(&format!(r#"## Custom Validation Criteria
-{}
-
-"#, content_check));
- }
-
- // Add validation result section
- plan.push_str(&format!(r#"## Reporting Results
-
-After completing all validation checks, you must report the result:
-
-**If ALL checks pass:**
-Mark this checkpoint contract as completed using:
-```bash
-makima supervisor complete
-```
-
-**If ANY check fails (on_failure: "{}"):**
-"#, validation.on_failure));
-
- match validation.on_failure.as_str() {
- "block" => plan.push_str(r#"
-- Document the failure reason clearly
-- Do NOT mark the contract as complete
-- The chain will be blocked until issues are resolved manually
-"#),
- "retry" => plan.push_str(&format!(r#"
-- Document the failure reason
-- Request retry of the failed upstream contract (max {} retries)
-- Use: `makima supervisor ask "Upstream validation failed. Retry?" --choices "Yes,No"`
-"#, validation.max_retries)),
- "warn" => plan.push_str(r#"
-- Document the warning/issue found
-- Mark the contract as complete anyway (downstream will proceed)
-- Log the warning for visibility
-"#),
- _ => plan.push_str(r#"
-- Document the failure reason
-- Do NOT mark the contract as complete
-"#),
- }
-
- plan.push_str(r#"
-## Begin Validation
-
-Start by checking the status of each upstream contract, then proceed with the validation criteria above.
-"#);
-
- plan
-}
-
-/// Create a contract from a chain definition.
-async fn create_contract_from_definition(
- pool: &PgPool,
- chain_id: Uuid,
- owner_id: Uuid,
- def: &ChainContractDefinition,
-) -> Result<Uuid, sqlx::Error> {
- // Get the existing contracts to find dependency info
- let existing_contracts = list_chain_contracts(pool, chain_id).await?;
- let name_to_contract: std::collections::HashMap<&str, &ChainContractDetail> = existing_contracts
- .iter()
- .map(|cc| (cc.contract_name.as_str(), cc))
- .collect();
-
- // Resolve dependency names to contract details
- let upstream_contracts: Vec<&ChainContractDetail> = def
- .depends_on_names
- .iter()
- .filter_map(|name| name_to_contract.get(name.as_str()).copied())
- .collect();
-
- // Create the contract request with basic fields
- let req = CreateContractRequest {
- name: def.name.clone(),
- description: def.description.clone(),
- contract_type: Some(def.contract_type.clone()),
- initial_phase: def.initial_phase.clone(),
- template_id: None,
- autonomous_loop: None,
- phase_guard: None,
- local_only: None,
- auto_merge_local: None,
- };
-
- // Create the contract
- let contract = create_contract_for_owner(pool, owner_id, req).await?;
-
- // For checkpoint contracts, generate a validation plan
- if def.contract_type == "checkpoint" {
- // Parse validation config
- let validation: ValidationConfig = def
- .validation
- .as_ref()
- .and_then(|v| serde_json::from_value(v.clone()).ok())
- .unwrap_or(ValidationConfig {
- check_deliverables: true,
- run_tests: false,
- check_content: None,
- on_failure: default_on_failure_str(),
- max_retries: default_max_retries_val(),
- });
-
- // Generate validation plan
- let validation_plan = generate_checkpoint_plan(def, &upstream_contracts, &validation);
-
- // Create a supervisor task with the validation plan
- let task_req = CreateTaskRequest {
- contract_id: Some(contract.id),
- name: format!("Validate: {}", def.name),
- description: Some("Checkpoint validation task".to_string()),
- plan: validation_plan,
- parent_task_id: None,
- is_supervisor: true, // Checkpoint uses supervisor task for validation
- priority: 0,
- repository_url: None,
- base_branch: None,
- target_branch: None,
- merge_mode: None,
- target_repo_path: None,
- completion_action: None,
- continue_from_task_id: None,
- copy_files: None,
- checkpoint_sha: None,
- branched_from_task_id: None,
- conversation_history: None,
- supervisor_worktree_task_id: None,
- };
-
- if let Err(e) = create_task_for_owner(pool, owner_id, task_req).await {
- tracing::warn!(
- contract_id = %contract.id,
- error = %e,
- "Failed to create validation task for checkpoint contract"
- );
- }
+) -> 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?;
- // Set initial validation status
+ // Update chain completed_steps and failed_steps counts
+ if status == "passed" || status == "skipped" {
sqlx::query(
- "UPDATE chain_contracts SET validation_status = 'pending' WHERE chain_id = $1 AND contract_id = $2",
+ "UPDATE directive_chains SET completed_steps = completed_steps + 1, updated_at = NOW() WHERE id = $1"
)
- .bind(chain_id)
- .bind(contract.id)
+ .bind(step.chain_id)
.execute(pool)
.await?;
- } else {
- // Parse and create tasks from definition for regular contracts
- if let Some(tasks_json) = &def.tasks {
- if let Ok(tasks) = serde_json::from_value::<Vec<ChainTaskDef>>(tasks_json.clone()) {
- for task_def in tasks {
- let task_req = CreateTaskRequest {
- contract_id: Some(contract.id),
- name: task_def.name,
- description: None,
- plan: task_def.plan,
- parent_task_id: None,
- is_supervisor: false,
- priority: 0,
- repository_url: None,
- base_branch: None,
- target_branch: None,
- merge_mode: None,
- target_repo_path: None,
- completion_action: None,
- continue_from_task_id: None,
- copy_files: None,
- checkpoint_sha: None,
- branched_from_task_id: None,
- conversation_history: None,
- supervisor_worktree_task_id: None,
- };
- if let Err(e) = create_task_for_owner(pool, owner_id, task_req).await {
- tracing::warn!(
- contract_id = %contract.id,
- error = %e,
- "Failed to create task from chain definition"
- );
- }
- }
- }
- }
- }
-
- // Resolve dependency names to contract IDs
- let depends_on: Vec<Uuid> = upstream_contracts.iter().map(|c| c.contract_id).collect();
-
- // Link contract to chain
- add_contract_to_chain(
- pool,
- chain_id,
- contract.id,
- depends_on,
- def.order_index,
- def.editor_x,
- def.editor_y,
- )
- .await?;
-
- // Update chain_contracts with definition_id link
- sqlx::query(
- "UPDATE chain_contracts SET definition_id = $1 WHERE chain_id = $2 AND contract_id = $3",
- )
- .bind(def.id)
- .bind(chain_id)
- .bind(contract.id)
- .execute(pool)
- .await?;
-
- // Copy repositories from chain to contract
- let chain_repos = list_chain_repositories(pool, chain_id).await.unwrap_or_default();
- for repo in chain_repos {
- if let Some(url) = &repo.repository_url {
- // Remote repository
- if let Err(e) = add_remote_repository(pool, contract.id, &repo.name, url, repo.is_primary).await {
- tracing::warn!(
- contract_id = %contract.id,
- repo_name = %repo.name,
- error = %e,
- "Failed to copy repository from chain to contract"
- );
- }
- } else if let Some(path) = &repo.local_path {
- // Local repository
- if let Err(e) = add_local_repository(pool, contract.id, &repo.name, path, repo.is_primary).await {
- tracing::warn!(
- contract_id = %contract.id,
- repo_name = %repo.name,
- error = %e,
- "Failed to copy local repository from chain to contract"
- );
- }
- }
- }
-
- // Activate the contract so it can start
- sqlx::query("UPDATE contracts SET status = 'active' WHERE id = $1")
- .bind(contract.id)
+ } 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?;
+ }
- tracing::info!(
- contract_id = %contract.id,
- contract_name = %def.name,
- chain_id = %chain_id,
- "Contract created and activated from chain definition"
- );
-
- Ok(contract.id)
+ Ok(step)
}
-// =============================================================================
-// Chain Directives
-// =============================================================================
-
-/// Create a directive for a chain.
-pub async fn create_chain_directive(
+/// Link a step to a contract.
+pub async fn update_step_contract(
pool: &PgPool,
- chain_id: Uuid,
- req: CreateChainDirectiveRequest,
-) -> Result<ChainDirective, sqlx::Error> {
- let requirements = serde_json::to_value(&req.requirements.unwrap_or_default())
- .unwrap_or(serde_json::json!([]));
- let acceptance_criteria = serde_json::to_value(&req.acceptance_criteria.unwrap_or_default())
- .unwrap_or(serde_json::json!([]));
- let constraints =
- serde_json::to_value(&req.constraints.unwrap_or_default()).unwrap_or(serde_json::json!([]));
- let external_dependencies =
- serde_json::to_value(&req.external_dependencies.unwrap_or_default())
- .unwrap_or(serde_json::json!([]));
- let source_type = req.source_type.unwrap_or_else(|| "llm_generated".to_string());
-
- sqlx::query_as::<_, ChainDirective>(
- r#"
- INSERT INTO chain_directives (chain_id, requirements, acceptance_criteria, constraints, external_dependencies, source_type)
- VALUES ($1, $2, $3, $4, $5, $6)
+ 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(chain_id)
- .bind(&requirements)
- .bind(&acceptance_criteria)
- .bind(&constraints)
- .bind(&external_dependencies)
- .bind(&source_type)
+ .bind(step_id)
+ .bind(contract_id)
+ .bind(supervisor_task_id)
.fetch_one(pool)
.await
}
-/// Get the directive for a chain.
-pub async fn get_chain_directive(
+/// Update step confidence score and level.
+pub async fn update_step_confidence(
pool: &PgPool,
- chain_id: Uuid,
-) -> Result<Option<ChainDirective>, sqlx::Error> {
- sqlx::query_as::<_, ChainDirective>(
+ step_id: Uuid,
+ score: f64,
+ level: &str,
+ evaluation_id: Uuid,
+) -> Result<ChainStep, sqlx::Error> {
+ sqlx::query_as::<_, ChainStep>(
r#"
- SELECT *
- FROM chain_directives
- WHERE chain_id = $1
+ UPDATE chain_steps SET
+ confidence_score = $2,
+ confidence_level = $3,
+ last_evaluation_id = $4,
+ evaluation_count = evaluation_count + 1
+ WHERE id = $1
+ RETURNING *
"#,
)
- .bind(chain_id)
- .fetch_optional(pool)
+ .bind(step_id)
+ .bind(score)
+ .bind(level)
+ .bind(evaluation_id)
+ .fetch_one(pool)
.await
}
-/// Update a chain directive.
-pub async fn update_chain_directive(
- pool: &PgPool,
- chain_id: Uuid,
- req: CreateChainDirectiveRequest,
-) -> Result<ChainDirective, sqlx::Error> {
- let requirements = req
- .requirements
- .map(|r| serde_json::to_value(&r).unwrap_or(serde_json::json!([])));
- let acceptance_criteria = req
- .acceptance_criteria
- .map(|ac| serde_json::to_value(&ac).unwrap_or(serde_json::json!([])));
- let constraints = req
- .constraints
- .map(|c| serde_json::to_value(&c).unwrap_or(serde_json::json!([])));
- let external_dependencies = req
- .external_dependencies
- .map(|ed| serde_json::to_value(&ed).unwrap_or(serde_json::json!([])));
-
- sqlx::query_as::<_, ChainDirective>(
- r#"
- UPDATE chain_directives SET
- requirements = COALESCE($2, requirements),
- acceptance_criteria = COALESCE($3, acceptance_criteria),
- constraints = COALESCE($4, constraints),
- external_dependencies = COALESCE($5, external_dependencies),
- source_type = COALESCE($6, source_type),
- version = version + 1,
- updated_at = NOW()
- WHERE chain_id = $1
+/// 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(chain_id)
- .bind(&requirements)
- .bind(&acceptance_criteria)
- .bind(&constraints)
- .bind(&external_dependencies)
- .bind(&req.source_type)
+ .bind(step_id)
.fetch_one(pool)
.await
}
-/// Delete a chain directive.
-pub async fn delete_chain_directive(pool: &PgPool, chain_id: Uuid) -> Result<bool, sqlx::Error> {
- let result = sqlx::query("DELETE FROM chain_directives WHERE chain_id = $1")
- .bind(chain_id)
- .execute(pool)
- .await?;
- Ok(result.rows_affected() > 0)
-}
-
-/// Get directive traceability (requirement -> contract mapping).
-pub async fn get_directive_traceability(
+/// Get chain graph for visualization.
+pub async fn get_chain_graph(
pool: &PgPool,
chain_id: Uuid,
-) -> Result<DirectiveTraceabilityResponse, sqlx::Error> {
- // Get the directive
- let directive = get_chain_directive(pool, chain_id).await?;
-
- // Get all contract definitions with their requirement mappings
- let definitions = list_chain_contract_definitions(pool, chain_id).await?;
-
- // Parse requirements from directive
- let requirements: Vec<super::models::DirectiveRequirement> = directive
- .as_ref()
- .and_then(|d| serde_json::from_value(d.requirements.clone()).ok())
- .unwrap_or_default();
-
- // Build traceability entries
- let mut entries: Vec<TraceabilityEntry> = Vec::new();
- let mut covered_requirements: std::collections::HashSet<String> =
- std::collections::HashSet::new();
-
- for req in &requirements {
- let mut contract_def_ids: Vec<Uuid> = Vec::new();
- let mut contract_def_names: Vec<String> = Vec::new();
-
- for def in &definitions {
- if def.requirement_ids.contains(&req.id) {
- contract_def_ids.push(def.id);
- contract_def_names.push(def.name.clone());
- covered_requirements.insert(req.id.clone());
- }
+) -> 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,
+ });
}
-
- // Get acceptance criteria for this requirement
- let acceptance_criteria: Vec<super::models::DirectiveAcceptanceCriterion> = directive
- .as_ref()
- .and_then(|d| serde_json::from_value(d.acceptance_criteria.clone()).ok())
- .unwrap_or_default();
-
- let ac_ids: Vec<String> = acceptance_criteria
- .iter()
- .filter(|ac| ac.requirement_ids.contains(&req.id))
- .map(|ac| ac.id.clone())
- .collect();
-
- entries.push(TraceabilityEntry {
- requirement_id: req.id.clone(),
- requirement_title: req.title.clone(),
- contract_definition_ids: contract_def_ids,
- contract_definition_names: contract_def_names,
- acceptance_criteria_ids: ac_ids,
- });
}
- // Find uncovered requirements
- let uncovered: Vec<String> = requirements
- .iter()
- .filter(|r| !covered_requirements.contains(&r.id))
- .map(|r| r.id.clone())
- .collect();
-
- Ok(DirectiveTraceabilityResponse {
+ Ok(DirectiveChainGraphResponse {
chain_id,
- entries,
- uncovered_requirements: uncovered,
+ directive_id: chain.directive_id,
+ nodes,
+ edges,
})
}
// =============================================================================
-// Contract Evaluations
+// Directive Evaluation Operations
// =============================================================================
-/// Create a contract evaluation record.
-pub async fn create_contract_evaluation(
+/// Create a directive evaluation.
+pub async fn create_directive_evaluation(
pool: &PgPool,
- req: CreateContractEvaluationRequest,
-) -> Result<ContractEvaluation, sqlx::Error> {
- let criteria_results = serde_json::to_value(&req.criteria_results).unwrap_or(serde_json::json!([]));
+ 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::<_, ContractEvaluation>(
+ sqlx::query_as::<_, DirectiveEvaluation>(
r#"
- INSERT INTO contract_evaluations (
- contract_id, chain_id, chain_contract_id,
- evaluator_model, passed, overall_score,
- criteria_results, summary_feedback, rework_instructions,
+ 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, NOW())
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, NOW())
RETURNING *
"#,
)
- .bind(req.contract_id)
- .bind(req.chain_id)
- .bind(req.chain_contract_id)
- .bind(&req.evaluator_model)
- .bind(req.passed)
- .bind(req.overall_score)
+ .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(&req.summary_feedback)
- .bind(&req.rework_instructions)
+ .bind(summary_feedback)
+ .bind(rework_instructions)
.fetch_one(pool)
.await
}
-/// Get a contract evaluation by ID.
-pub async fn get_contract_evaluation(
+/// List evaluations for a step.
+pub async fn list_step_evaluations(
pool: &PgPool,
- id: Uuid,
-) -> Result<Option<ContractEvaluation>, sqlx::Error> {
- sqlx::query_as::<_, ContractEvaluation>(
- r#"
- SELECT *
- FROM contract_evaluations
- WHERE id = $1
- "#,
+ 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(id)
- .fetch_optional(pool)
+ .bind(step_id)
+ .fetch_all(pool)
.await
}
-/// List evaluations for a contract.
-pub async fn list_contract_evaluations(
+/// List evaluations for a directive.
+pub async fn list_directive_evaluations(
pool: &PgPool,
- contract_id: Uuid,
-) -> Result<Vec<ContractEvaluationSummary>, sqlx::Error> {
- sqlx::query_as::<_, ContractEvaluationSummary>(
- r#"
- SELECT id, contract_id, evaluation_number, passed, overall_score, summary_feedback, created_at
- FROM contract_evaluations
- WHERE contract_id = $1
- ORDER BY evaluation_number DESC
- "#,
+ 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(contract_id)
+ .bind(directive_id)
+ .bind(limit)
.fetch_all(pool)
.await
}
-/// List evaluations for a chain.
-pub async fn list_chain_evaluations(
+// =============================================================================
+// Directive Event Operations
+// =============================================================================
+
+/// Emit a directive event.
+pub async fn emit_directive_event(
pool: &PgPool,
- chain_id: Uuid,
-) -> Result<Vec<ContractEvaluationSummary>, sqlx::Error> {
- sqlx::query_as::<_, ContractEvaluationSummary>(
+ 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#"
- SELECT id, contract_id, evaluation_number, passed, overall_score, summary_feedback, created_at
- FROM contract_evaluations
- WHERE chain_id = $1
- ORDER BY created_at DESC
+ 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
}
-/// Get the latest evaluation for a chain contract.
-pub async fn get_latest_chain_contract_evaluation(
+/// Update a directive verifier.
+pub async fn update_directive_verifier(
pool: &PgPool,
- chain_contract_id: Uuid,
-) -> Result<Option<ContractEvaluation>, sqlx::Error> {
- sqlx::query_as::<_, ContractEvaluation>(
+ verifier_id: Uuid,
+ enabled: Option<bool>,
+ command: Option<&str>,
+ weight: Option<f64>,
+ required: Option<bool>,
+) -> Result<DirectiveVerifier, sqlx::Error> {
+ sqlx::query_as::<_, DirectiveVerifier>(
r#"
- SELECT *
- FROM contract_evaluations
- WHERE chain_contract_id = $1
- ORDER BY evaluation_number DESC
- LIMIT 1
+ 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(chain_contract_id)
- .fetch_optional(pool)
+ .bind(verifier_id)
+ .bind(enabled)
+ .bind(command)
+ .bind(weight)
+ .bind(required)
+ .fetch_one(pool)
.await
}
-/// Get the next evaluation number for a chain contract.
-pub async fn get_next_evaluation_number(
+/// Update verifier last run result.
+pub async fn update_verifier_result(
pool: &PgPool,
- chain_contract_id: Uuid,
-) -> Result<i32, sqlx::Error> {
- let result: Option<(i32,)> = sqlx::query_as(
+ verifier_id: Uuid,
+ result: serde_json::Value,
+) -> Result<DirectiveVerifier, sqlx::Error> {
+ sqlx::query_as::<_, DirectiveVerifier>(
r#"
- SELECT COALESCE(MAX(evaluation_number), 0) + 1 as next_number
- FROM contract_evaluations
- WHERE chain_contract_id = $1
+ UPDATE directive_verifiers SET last_run_at = NOW(), last_result = $2, updated_at = NOW()
+ WHERE id = $1
+ RETURNING *
"#,
)
- .bind(chain_contract_id)
- .fetch_optional(pool)
- .await?;
+ .bind(verifier_id)
+ .bind(result)
+ .fetch_one(pool)
+ .await
+}
- Ok(result.map(|(n,)| n).unwrap_or(1))
+// =============================================================================
+// 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
}
-/// Update chain contract evaluation status.
-pub async fn update_chain_contract_evaluation_status(
+/// Resolve an approval request.
+pub async fn resolve_approval(
pool: &PgPool,
- chain_contract_id: Uuid,
+ approval_id: Uuid,
status: &str,
- evaluation_id: Option<Uuid>,
- rework_feedback: Option<&str>,
-) -> Result<ChainContract, sqlx::Error> {
- sqlx::query_as::<_, ChainContract>(
- r#"
- UPDATE chain_contracts SET
- evaluation_status = $2,
- last_evaluation_id = COALESCE($3, last_evaluation_id),
- rework_feedback = COALESCE($4, rework_feedback),
- evaluation_retry_count = CASE
- WHEN $2 = 'rework' THEN evaluation_retry_count + 1
- ELSE evaluation_retry_count
- END,
- rework_started_at = CASE
- WHEN $2 = 'rework' THEN NOW()
- ELSE rework_started_at
- END
+ 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(chain_contract_id)
+ .bind(approval_id)
.bind(status)
- .bind(evaluation_id)
- .bind(rework_feedback)
+ .bind(response)
+ .bind(responded_by)
.fetch_one(pool)
.await
}
-/// Mark a chain contract's original completion time (before rework).
-pub async fn mark_chain_contract_original_completion(
+/// List pending approvals for a directive.
+pub async fn list_pending_approvals(
pool: &PgPool,
- chain_contract_id: Uuid,
-) -> Result<(), sqlx::Error> {
- sqlx::query(
+ directive_id: Uuid,
+) -> Result<Vec<DirectiveApproval>, sqlx::Error> {
+ sqlx::query_as::<_, DirectiveApproval>(
r#"
- UPDATE chain_contracts SET
- original_completion_at = COALESCE(original_completion_at, NOW())
- WHERE id = $1
+ 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(chain_contract_id)
- .execute(pool)
- .await?;
- Ok(())
+ .bind(directive_id)
+ .fetch_all(pool)
+ .await
}
-/// Get chain contract by contract ID.
-pub async fn get_chain_contract_by_contract_id(
+/// Get step by contract ID.
+pub async fn get_step_by_contract_id(
pool: &PgPool,
contract_id: Uuid,
-) -> Result<Option<ChainContract>, sqlx::Error> {
- sqlx::query_as::<_, ChainContract>(
- r#"
- SELECT *
- FROM chain_contracts
- WHERE contract_id = $1
- "#,
+) -> Result<Option<ChainStep>, sqlx::Error> {
+ sqlx::query_as::<_, ChainStep>(
+ "SELECT * FROM chain_steps WHERE contract_id = $1"
)
.bind(contract_id)
.fetch_optional(pool)
@@ -6682,103 +6034,9 @@ pub async fn get_chain_contract_by_contract_id(
}
// =============================================================================
-// Init Chain (Directive-Driven Chain Creation)
+// Helper Functions
// =============================================================================
-/// Initialize a directive-driven chain.
-/// Creates a directive contract and an empty chain linked to it.
-pub async fn init_chain_for_owner(
- pool: &PgPool,
- owner_id: Uuid,
- req: InitChainRequest,
-) -> Result<InitChainResponse, sqlx::Error> {
- // Create the directive contract
- // Note: "directive" contract type uses the "specification" phases by default
- let contract_req = CreateContractRequest {
- name: format!("Directive: {}", truncate_string(&req.goal, 50)),
- description: Some(req.goal.clone()),
- contract_type: Some("specification".to_string()), // Directive uses spec workflow
- template_id: None,
- initial_phase: Some("research".to_string()),
- phase_guard: Some(req.phase_guard),
- autonomous_loop: Some(false),
- local_only: Some(false),
- auto_merge_local: Some(false),
- };
-
- let contract = create_contract_for_owner(pool, owner_id, contract_req).await?;
-
- // Mark it as a chain directive
- sqlx::query("UPDATE contracts SET is_chain_directive = true WHERE id = $1")
- .bind(contract.id)
- .execute(pool)
- .await?;
-
- // Build repositories list from request
- let repositories = match (req.repository_url.as_ref(), req.local_path.as_ref()) {
- (Some(url), _) => Some(vec![AddChainRepositoryRequest {
- name: "Primary".to_string(),
- repository_url: Some(url.clone()),
- local_path: None,
- source_type: "remote".to_string(),
- is_primary: true,
- }]),
- (None, Some(path)) => Some(vec![AddChainRepositoryRequest {
- name: "Primary".to_string(),
- repository_url: None,
- local_path: Some(path.clone()),
- source_type: "local".to_string(),
- is_primary: true,
- }]),
- (None, None) => None,
- };
-
- // Create the chain with directive contract reference
- let chain_req = CreateChainRequest {
- name: truncate_string(&req.goal, 100),
- description: Some(req.goal),
- repositories,
- loop_enabled: Some(false),
- loop_max_iterations: None,
- loop_progress_check: None,
- contracts: None,
- };
-
- let chain = create_chain_for_owner(pool, owner_id, chain_req).await?;
-
- // Link directive contract to chain
- sqlx::query(
- r#"
- UPDATE chains SET directive_contract_id = $2 WHERE id = $1;
- UPDATE contracts SET spawned_chain_id = $1 WHERE id = $2;
- "#,
- )
- .bind(chain.id)
- .bind(contract.id)
- .execute(pool)
- .await?;
-
- // Create empty directive document
- create_chain_directive(
- pool,
- chain.id,
- CreateChainDirectiveRequest {
- requirements: Some(vec![]),
- acceptance_criteria: Some(vec![]),
- constraints: Some(vec![]),
- external_dependencies: Some(vec![]),
- source_type: Some("llm_generated".to_string()),
- },
- )
- .await?;
-
- Ok(InitChainResponse {
- chain_id: chain.id,
- directive_contract_id: contract.id,
- supervisor_task_id: contract.supervisor_task_id,
- })
-}
-
/// Helper to truncate string to max length
fn truncate_string(s: &str, max_len: usize) -> String {
if s.len() <= max_len {
diff --git a/makima/src/lib.rs b/makima/src/lib.rs
index 8d3db58..3bc460b 100644
--- a/makima/src/lib.rs
+++ b/makima/src/lib.rs
@@ -3,5 +3,6 @@ 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/llm/contract_evaluator.rs b/makima/src/llm/contract_evaluator.rs
index fcc4826..e63bbfa 100644
--- a/makima/src/llm/contract_evaluator.rs
+++ b/makima/src/llm/contract_evaluator.rs
@@ -1,25 +1,19 @@
//! Contract Evaluator - LLM-based evaluation of completed contracts against directive.
//!
-//! This module provides functionality for:
-//! - Gathering deliverables, files, and task outputs from completed contracts
-//! - Building evaluation prompts using directive and acceptance criteria
-//! - Calling LLM to evaluate work against requirements
-//! - Parsing evaluation responses
+//! This module will be reimplemented as part of the directive verification engine.
+//! See the orchestration module for the new evaluation system.
+//!
+//! The new evaluation system will provide:
+//! - Tiered verification (programmatic verifiers first, then LLM evaluation)
+//! - Composite confidence scoring (weighted combination of results)
+//! - Pluggable verifier interface (test runner, linter, build, type checker)
+//! - Proper integration with the directive chain steps
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use uuid::Uuid;
-use crate::db::{
- models::{
- ChainContract, ChainDirective, Contract, ContractEvaluation, CreateContractEvaluationRequest,
- DirectiveAcceptanceCriterion, DirectiveRequirement, EvaluationCriterionResult,
- },
- repository,
-};
-
-use super::claude::{ClaudeClient, ClaudeModel, Message, MessageContent};
-use super::tools::Tool;
+// use crate::db::models::{Contract, DirectiveAcceptanceCriterion, DirectiveRequirement};
/// Result of contract evaluation
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -30,526 +24,74 @@ pub struct ContractEvaluationResult {
/// Overall score from 0.0 to 1.0
pub overall_score: f64,
/// Results for each acceptance criterion
- pub criteria_results: Vec<EvaluationCriterionResult>,
+ pub criteria_results: Vec<EvaluationCriterionResultLegacy>,
/// Summary feedback from the evaluator
pub summary_feedback: String,
/// Instructions for rework if failed
pub rework_instructions: Option<String>,
}
-/// Context gathered for evaluation
-#[derive(Debug, Clone)]
-pub struct EvaluationContext {
- /// The contract being evaluated
- pub contract: Contract,
- /// The chain contract record
- pub chain_contract: ChainContract,
- /// The directive document
- pub directive: ChainDirective,
- /// Files associated with the contract
- pub files: Vec<FileContent>,
- /// Task outputs from the contract
- pub task_outputs: Vec<TaskOutput>,
- /// Deliverables marked as complete
- pub deliverables: Vec<DeliverableInfo>,
- /// Acceptance criteria specific to this contract
- pub acceptance_criteria: Vec<DirectiveAcceptanceCriterion>,
- /// Requirements mapped to this contract
- pub requirements: Vec<DirectiveRequirement>,
+/// Per-criterion evaluation result (legacy - kept for compatibility)
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct EvaluationCriterionResultLegacy {
+ pub criterion_id: String,
+ pub criterion_text: String,
+ pub passed: bool,
+ /// Score (0.0-1.0)
+ pub score: f64,
+ pub feedback: String,
+ /// Evidence supporting the evaluation
+ pub evidence: Vec<String>,
}
-/// File content for evaluation
-#[derive(Debug, Clone, Serialize)]
+/// File content for evaluation context
+#[derive(Debug, Clone)]
pub struct FileContent {
pub path: String,
- pub description: Option<String>,
pub content: String,
- pub is_deliverable: bool,
}
-/// Task output for evaluation
-#[derive(Debug, Clone, Serialize)]
-pub struct TaskOutput {
- pub task_name: String,
- pub output_summary: String,
- pub exit_code: Option<i32>,
-}
-
-/// Deliverable info for evaluation
-#[derive(Debug, Clone, Serialize)]
-pub struct DeliverableInfo {
- pub name: String,
- pub status: String,
- pub file_path: Option<String>,
-}
-
-/// Error types for evaluation
-#[derive(Debug, thiserror::Error)]
-pub enum EvaluationError {
- #[error("Database error: {0}")]
- Database(#[from] sqlx::Error),
-
- #[error("Contract not found: {0}")]
- ContractNotFound(Uuid),
-
- #[error("Chain contract not found for contract: {0}")]
- ChainContractNotFound(Uuid),
-
- #[error("Directive not found for chain: {0}")]
- DirectiveNotFound(Uuid),
-
- #[error("LLM evaluation failed: {0}")]
- LlmError(String),
-
- #[error("Failed to parse evaluation response: {0}")]
- ParseError(String),
-}
-
-/// Contract evaluator for directive-driven evaluation
+/// Contract evaluator for LLM-based assessment.
+///
+/// NOTE: This is a stub implementation. The full evaluation system will be
+/// implemented as part of the orchestration/verifier module.
pub struct ContractEvaluator {
- pool: PgPool,
- claude_client: ClaudeClient,
- model: ClaudeModel,
- /// Minimum score required to pass (default 0.8)
- pass_threshold: f64,
+ _pool: PgPool,
}
impl ContractEvaluator {
- /// Create a new evaluator
- pub fn new(pool: PgPool, claude_client: ClaudeClient) -> Self {
- Self {
- pool,
- claude_client,
- model: ClaudeModel::Sonnet,
- pass_threshold: 0.8,
- }
- }
-
- /// Set the LLM model to use for evaluation
- pub fn with_model(mut self, model: ClaudeModel) -> Self {
- self.model = model;
- self
- }
-
- /// Set the pass threshold
- pub fn with_pass_threshold(mut self, threshold: f64) -> Self {
- self.pass_threshold = threshold;
- self
+ /// Create a new contract evaluator.
+ pub fn new(pool: PgPool) -> Self {
+ Self { _pool: pool }
}
- /// Evaluate a completed contract against the directive
+ /// Evaluate a contract - stub implementation.
+ ///
+ /// This will be reimplemented in the orchestration module with:
+ /// - Programmatic verification (tests, lint, build)
+ /// - LLM evaluation
+ /// - Composite scoring
pub async fn evaluate_contract(
&self,
- contract_id: Uuid,
- owner_id: Uuid,
- ) -> Result<ContractEvaluationResult, EvaluationError> {
- // Gather evaluation context
- let context = self.gather_context(contract_id, owner_id).await?;
-
- // Build evaluation prompt
- let prompt = self.build_evaluation_prompt(&context);
-
- // Call LLM for evaluation
- let response = self.call_llm_for_evaluation(&prompt).await?;
-
- // Parse the response
- let result = self.parse_evaluation_response(&response, &context)?;
-
- Ok(result)
- }
-
- /// Gather all context needed for evaluation
- async fn gather_context(
- &self,
- contract_id: Uuid,
- owner_id: Uuid,
- ) -> Result<EvaluationContext, EvaluationError> {
- // Get contract
- let contract = repository::get_contract_for_owner(&self.pool, contract_id, owner_id)
- .await?
- .ok_or(EvaluationError::ContractNotFound(contract_id))?;
-
- // Get chain contract
- let chain_contract = repository::get_chain_contract_by_contract_id(&self.pool, contract_id)
- .await?
- .ok_or(EvaluationError::ChainContractNotFound(contract_id))?;
-
- // Get directive
- let directive = repository::get_chain_directive(&self.pool, chain_contract.chain_id)
- .await?
- .ok_or(EvaluationError::DirectiveNotFound(chain_contract.chain_id))?;
-
- // Get files directly from repository
- let contract_files = repository::list_files_in_contract(&self.pool, contract_id, owner_id)
- .await
- .unwrap_or_default();
-
- // Get tasks directly from repository
- let contract_tasks = repository::list_tasks_in_contract(&self.pool, contract_id, owner_id)
- .await
- .unwrap_or_default();
-
- // Build file contents from FileSummary
- // Note: FileSummary doesn't have content, so we use name and description
- let files: Vec<FileContent> = contract_files.iter().map(|f| {
- FileContent {
- path: f.repo_file_path.clone().unwrap_or_else(|| f.name.clone()),
- description: f.description.clone(),
- content: format!("[File: {} - content not loaded in summary view]", f.name),
- is_deliverable: false, // FileSummary doesn't track deliverable status
- }
- }).collect();
-
- // Build task outputs from TaskSummary
- let task_outputs: Vec<TaskOutput> = contract_tasks.iter().map(|t| {
- TaskOutput {
- task_name: t.name.clone(),
- output_summary: t.progress_summary.clone().unwrap_or_else(|| format!("Status: {}", t.status)),
- exit_code: None,
- }
- }).collect();
-
- // Build deliverables info from files marked as deliverables
- // Since FileSummary doesn't have deliverable info, we treat all files as potential deliverables
- let deliverables: Vec<DeliverableInfo> = contract_files.iter()
- .map(|f| DeliverableInfo {
- name: f.name.clone(),
- status: "complete".to_string(),
- file_path: f.repo_file_path.clone(),
- })
- .collect();
-
- // Parse requirements and acceptance criteria from directive
- let requirements: Vec<DirectiveRequirement> =
- serde_json::from_value(directive.requirements.clone()).unwrap_or_default();
-
- let all_criteria: Vec<DirectiveAcceptanceCriterion> =
- serde_json::from_value(directive.acceptance_criteria.clone()).unwrap_or_default();
-
- // Get contract definition to find mapped requirements
- // For now, use all acceptance criteria
- let acceptance_criteria = all_criteria;
-
- Ok(EvaluationContext {
- contract,
- chain_contract,
- directive,
- files,
- task_outputs,
- deliverables,
- acceptance_criteria,
- requirements,
- })
+ _contract_id: Uuid,
+ ) -> Result<ContractEvaluationResult, ContractEvaluatorError> {
+ // TODO: Implement using the new directive evaluation system
+ Err(ContractEvaluatorError::NotImplemented(
+ "Contract evaluator will be reimplemented with directive system".to_string(),
+ ))
}
-
- /// Build the evaluation prompt
- fn build_evaluation_prompt(&self, context: &EvaluationContext) -> String {
- let mut prompt = String::new();
-
- prompt.push_str("# Contract Completion Evaluation\n\n");
- prompt.push_str("You are evaluating whether a contract has been completed successfully against its requirements.\n\n");
-
- // Contract info
- prompt.push_str("## Contract Information\n\n");
- prompt.push_str(&format!("**Name:** {}\n", context.contract.name));
- if let Some(ref desc) = context.contract.description {
- prompt.push_str(&format!("**Description:** {}\n", desc));
- }
- prompt.push_str(&format!("**Type:** {}\n", context.contract.contract_type));
- prompt.push_str(&format!("**Phase:** {}\n", context.contract.phase));
- prompt.push_str("\n");
-
- // Requirements
- if !context.requirements.is_empty() {
- prompt.push_str("## Requirements\n\n");
- for req in &context.requirements {
- prompt.push_str(&format!("- **{}** ({}): {}\n", req.id, req.priority, req.title));
- if !req.description.is_empty() {
- prompt.push_str(&format!(" {}\n", req.description));
- }
- }
- prompt.push_str("\n");
- }
-
- // Acceptance criteria
- if !context.acceptance_criteria.is_empty() {
- prompt.push_str("## Acceptance Criteria\n\n");
- for (i, criterion) in context.acceptance_criteria.iter().enumerate() {
- prompt.push_str(&format!("{}. **{}**\n", i + 1, criterion.description));
- prompt.push_str(&format!(" - Testable: {}\n", criterion.testable));
- if !criterion.requirement_ids.is_empty() {
- prompt.push_str(&format!(" - Covers: {}\n", criterion.requirement_ids.join(", ")));
- }
- }
- prompt.push_str("\n");
- }
-
- // Deliverables
- if !context.deliverables.is_empty() {
- prompt.push_str("## Deliverables\n\n");
- for d in &context.deliverables {
- prompt.push_str(&format!("- {} ({})\n", d.name, d.status));
- }
- prompt.push_str("\n");
- }
-
- // Files
- if !context.files.is_empty() {
- prompt.push_str("## Files Created/Modified\n\n");
- for file in &context.files {
- prompt.push_str(&format!("### {}", file.path));
- if file.is_deliverable {
- prompt.push_str(" [DELIVERABLE]");
- }
- prompt.push_str("\n");
- if let Some(ref desc) = file.description {
- prompt.push_str(&format!("*{}*\n", desc));
- }
- // Truncate content if too long
- let content = if file.content.len() > 5000 {
- format!("{}...\n[Content truncated - {} chars total]",
- &file.content[..5000], file.content.len())
- } else {
- file.content.clone()
- };
- prompt.push_str("```\n");
- prompt.push_str(&content);
- prompt.push_str("\n```\n\n");
- }
- }
-
- // Task outputs
- if !context.task_outputs.is_empty() {
- prompt.push_str("## Task Outputs\n\n");
- for task in &context.task_outputs {
- prompt.push_str(&format!("### {}\n", task.task_name));
- prompt.push_str(&format!("{}\n\n", task.output_summary));
- }
- }
-
- // Evaluation instructions
- prompt.push_str("## Evaluation Instructions\n\n");
- prompt.push_str("Please evaluate the completed work against the requirements and acceptance criteria.\n\n");
- prompt.push_str("For each acceptance criterion, determine if it has been met and provide a brief explanation.\n\n");
- prompt.push_str("Respond with a JSON object in the following format:\n\n");
- prompt.push_str("```json\n");
- prompt.push_str(r#"{
- "passed": true/false,
- "overallScore": 0.0-1.0,
- "criteriaResults": [
- {
- "criterionId": "criterion identifier or index",
- "met": true/false,
- "score": 0.0-1.0,
- "feedback": "explanation of why criterion was/wasn't met"
- }
- ],
- "summaryFeedback": "overall summary of the evaluation",
- "reworkInstructions": "if failed, specific instructions for what needs to be fixed (null if passed)"
-}
-"#);
- prompt.push_str("```\n\n");
- prompt.push_str(&format!("The pass threshold is {}. ", self.pass_threshold));
- prompt.push_str("A contract passes if the overall score is >= the threshold AND all critical criteria are met.\n");
-
- prompt
- }
-
- /// Call LLM for evaluation
- async fn call_llm_for_evaluation(&self, prompt: &str) -> Result<String, EvaluationError> {
- let messages = vec![Message {
- role: "user".to_string(),
- content: MessageContent::Text(prompt.to_string()),
- }];
-
- // Use chat_with_tools with empty tools array for simple chat
- let empty_tools: Vec<Tool> = vec![];
- let result = self
- .claude_client
- .chat_with_tools(messages, &empty_tools)
- .await
- .map_err(|e| EvaluationError::LlmError(e.to_string()))?;
-
- // ChatResult.content is already an Option<String>
- let text = result.content.unwrap_or_default();
-
- Ok(text)
- }
-
- /// Parse the LLM response into an evaluation result
- fn parse_evaluation_response(
- &self,
- response: &str,
- context: &EvaluationContext,
- ) -> Result<ContractEvaluationResult, EvaluationError> {
- // Extract JSON from response (may be wrapped in markdown code blocks)
- let json_str = extract_json_from_response(response)?;
-
- // Parse the JSON
- let parsed: EvaluationResponseJson = serde_json::from_str(&json_str)
- .map_err(|e| EvaluationError::ParseError(format!("JSON parse error: {}", e)))?;
-
- // Convert to our result type
- let criteria_results: Vec<EvaluationCriterionResult> = parsed
- .criteria_results
- .into_iter()
- .map(|cr| EvaluationCriterionResult {
- criterion_id: cr.criterion_id.clone(),
- criterion_text: cr.criterion_id, // Use ID as text if not provided
- passed: cr.passed,
- score: cr.score,
- feedback: cr.feedback,
- evidence: vec![],
- })
- .collect();
-
- // Determine pass/fail based on threshold and results
- let passed = parsed.passed && parsed.overall_score >= self.pass_threshold;
-
- Ok(ContractEvaluationResult {
- passed,
- overall_score: parsed.overall_score,
- criteria_results,
- summary_feedback: parsed.summary_feedback,
- rework_instructions: if passed { None } else { parsed.rework_instructions },
- })
- }
-
- /// Save evaluation result to database
- pub async fn save_evaluation(
- &self,
- contract_id: Uuid,
- chain_id: Uuid,
- chain_contract_id: Uuid,
- result: &ContractEvaluationResult,
- ) -> Result<ContractEvaluation, EvaluationError> {
- let req = CreateContractEvaluationRequest {
- contract_id,
- chain_id: Some(chain_id),
- chain_contract_id: Some(chain_contract_id),
- evaluator_model: Some(format!("{:?}", self.model)),
- passed: result.passed,
- overall_score: Some(result.overall_score),
- criteria_results: result.criteria_results.clone(),
- summary_feedback: result.summary_feedback.clone(),
- rework_instructions: result.rework_instructions.clone(),
- };
-
- let evaluation = repository::create_contract_evaluation(&self.pool, req).await?;
-
- // Update chain contract status
- let status = if result.passed { "passed" } else { "failed" };
- repository::update_chain_contract_evaluation_status(
- &self.pool,
- chain_contract_id,
- status,
- Some(evaluation.id),
- result.rework_instructions.as_deref(),
- )
- .await?;
-
- Ok(evaluation)
- }
-}
-
-/// JSON structure for parsing LLM response
-#[derive(Debug, Deserialize)]
-#[serde(rename_all = "camelCase")]
-struct EvaluationResponseJson {
- passed: bool,
- overall_score: f64,
- criteria_results: Vec<CriterionResultJson>,
- summary_feedback: String,
- rework_instructions: Option<String>,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(rename_all = "camelCase")]
-struct CriterionResultJson {
- criterion_id: String,
- #[serde(alias = "met")]
- passed: bool,
- #[serde(default)]
- score: f64,
- feedback: String,
-}
-
-/// Extract JSON from a response that may contain markdown code blocks
-fn extract_json_from_response(response: &str) -> Result<String, EvaluationError> {
- // Try to find JSON in code blocks first
- 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 plain code blocks
- if let Some(start) = response.find("```") {
- let json_start = start + 3;
- // Skip any language identifier on the same line
- let actual_start = response[json_start..]
- .find('\n')
- .map(|i| json_start + i + 1)
- .unwrap_or(json_start);
- if let Some(end) = response[actual_start..].find("```") {
- return Ok(response[actual_start..actual_start + end].trim().to_string());
- }
- }
-
- // Try to find raw JSON (starts with {)
- if let Some(start) = response.find('{') {
- // Find matching closing brace
- let mut depth = 0;
- let mut end = start;
- for (i, c) in response[start..].char_indices() {
- match c {
- '{' => depth += 1,
- '}' => {
- depth -= 1;
- if depth == 0 {
- end = start + i + 1;
- break;
- }
- }
- _ => {}
- }
- }
- if end > start {
- return Ok(response[start..end].to_string());
- }
- }
-
- Err(EvaluationError::ParseError(
- "Could not find JSON in response".to_string(),
- ))
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_extract_json_from_code_block() {
- let response = r#"Here is the evaluation:
-
-```json
-{
- "passed": true,
- "overallScore": 0.85
}
-```
-Done."#;
+/// Error types for contract evaluation.
+#[derive(Debug, thiserror::Error)]
+pub enum ContractEvaluatorError {
+ #[error("Database error: {0}")]
+ Database(#[from] sqlx::Error),
- let json = extract_json_from_response(response).unwrap();
- assert!(json.contains("\"passed\": true"));
- }
+ #[error("LLM error: {0}")]
+ Llm(String),
- #[test]
- fn test_extract_json_raw() {
- let response = r#"The result is {"passed": false, "overallScore": 0.5}"#;
- let json = extract_json_from_response(response).unwrap();
- assert!(json.contains("\"passed\": false"));
- }
+ #[error("Not implemented: {0}")]
+ NotImplemented(String),
}
diff --git a/makima/src/llm/mod.rs b/makima/src/llm/mod.rs
index 702e1fd..6c9965c 100644
--- a/makima/src/llm/mod.rs
+++ b/makima/src/llm/mod.rs
@@ -46,7 +46,7 @@ pub use transcript_analyzer::{
calculate_speaker_stats, build_analysis_prompt, parse_analysis_response,
};
pub use contract_evaluator::{
- ContractEvaluator, ContractEvaluationResult, EvaluationContext, EvaluationError,
+ ContractEvaluator, ContractEvaluationResult, ContractEvaluatorError,
};
/// Available LLM providers and models
diff --git a/makima/src/llm/task_output.rs b/makima/src/llm/task_output.rs
index c5d709e..c7f6990 100644
--- a/makima/src/llm/task_output.rs
+++ b/makima/src/llm/task_output.rs
@@ -126,7 +126,7 @@ pub fn parse_tasks_from_breakdown(content: &str) -> TaskParseResult {
let heading_pattern = Regex::new(r"^##\s+(?:Phase\s*\d*:?\s*)?(.+)$").unwrap();
// Patterns for dependencies (inline)
- let depends_pattern = Regex::new(r"(?i)(?:depends on|after|requires):?\s*(.+)").unwrap();
+ let depends_pattern = Regex::new(r"(?i)\(?\s*(?:depends on|after|requires):?\s*([^)]+)\)?").unwrap();
for line in content.lines() {
let trimmed = line.trim();
@@ -226,7 +226,7 @@ pub fn parse_tasks_from_breakdown(content: &str) -> TaskParseResult {
}
}
-/// Check if text looks like a task (has action verbs)
+/// Check if text looks like a task (has action verbs at word boundaries)
fn looks_like_task(text: &str) -> bool {
let lower = text.to_lowercase();
let action_verbs = [
@@ -237,7 +237,27 @@ fn looks_like_task(text: &str) -> bool {
"disable", "install", "initialize", "define", "extend", "extract",
];
- action_verbs.iter().any(|verb| lower.starts_with(verb) || lower.contains(&format!(" {}", verb)))
+ // Check if text starts with an action verb (followed by space or end)
+ for verb in &action_verbs {
+ if lower.starts_with(verb) {
+ // Check for word boundary after verb
+ let after = &lower[verb.len()..];
+ if after.is_empty() || after.starts_with(' ') || after.starts_with('_') {
+ return true;
+ }
+ }
+ // Check if verb appears after space with word boundary
+ let pattern = format!(" {} ", verb);
+ let pattern_end = format!(" {}", verb);
+ if lower.contains(&pattern) {
+ return true;
+ }
+ // Check if verb is at the end of string after a space
+ if lower.ends_with(&pattern_end) && lower.len() > pattern_end.len() {
+ return true;
+ }
+ }
+ false
}
/// Analyze a completed task's output to suggest next actions
diff --git a/makima/src/orchestration/engine.rs b/makima/src/orchestration/engine.rs
new file mode 100644
index 0000000..5bbb99f
--- /dev/null
+++ b/makima/src/orchestration/engine.rs
@@ -0,0 +1,976 @@
+//! 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, 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,
+ },
+}
+
+/// 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 {
+ pool,
+ planner: ChainPlanner::new(),
+ 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: generate chain and begin execution.
+ pub async fn start_directive(&self, directive_id: Uuid) -> Result<(), 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?;
+
+ // Generate chain (placeholder - actual LLM call would go here)
+ let chain = self.generate_initial_chain(&directive).await?;
+
+ // Create chain in database
+ let db_chain = repository::create_directive_chain(
+ &self.pool,
+ directive_id,
+ &chain.name,
+ Some(&chain.description),
+ None, // rationale
+ None, // planning_model
+ )
+ .await?;
+
+ // Create steps
+ self.create_steps_from_chain(&db_chain.id, &chain).await?;
+
+ // Update directive to active
+ 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(),
+ });
+
+ // Start ready steps
+ self.advance_chain(directive_id).await?;
+
+ Ok(())
+ }
+
+ /// 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
+ // ========================================================================
+
+ /// Generate initial chain from directive.
+ async fn generate_initial_chain(
+ &self,
+ directive: &Directive,
+ ) -> Result<GeneratedChain, EngineError> {
+ // Build planning prompt
+ let _prompt = self.planner.build_planning_prompt(directive);
+
+ // TODO: Call LLM to generate chain
+ // For now, return a simple placeholder chain
+ let chain = 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: "Research and understand the requirements".to_string(),
+ depends_on: vec![],
+ requirement_ids: vec![],
+ contract_template: None,
+ },
+ super::planner::GeneratedStep {
+ name: "implement".to_string(),
+ step_type: "implement".to_string(),
+ description: "Implement the solution".to_string(),
+ 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,
+ },
+ ],
+ };
+
+ // Validate the chain
+ self.planner.validate_chain(&chain)?;
+
+ Ok(chain)
+ }
+
+ /// 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))?;
+
+ // Get completed and failed steps
+ let steps = repository::list_chain_steps(&self.pool, current_chain.id).await?;
+ let completed_steps: Vec<_> = steps.iter().filter(|s| s.status == "passed").collect();
+ let failed_step = steps.iter().find(|s| s.status == "failed");
+
+ // Build replan prompt
+ let _prompt = self.planner.build_replan_prompt(
+ &directive,
+ &completed_steps.iter().map(|s| (*s).clone()).collect::<Vec<_>>(),
+ failed_step.map(|s| &*s),
+ reason,
+ );
+
+ // TODO: Call LLM to regenerate chain
+ // For now, just create a new chain with similar structure
+ let new_chain = self.generate_initial_chain(&directive).await?;
+
+ // 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);
+
+ // TODO: Actually create the contract via the contracts handler
+ // For now, just update the step status to running
+ // In a full implementation, this would:
+ // 1. Create contract via POST /api/v1/contracts
+ // 2. Create supervisor task via POST /api/v1/tasks
+ // 3. Link contract and task to step
+ // 4. Update step status to running
+
+ // Placeholder: mark step as 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,
+ }),
+ "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(())
+ }
+
+ // ========================================================================
+ // 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
new file mode 100644
index 0000000..41913ca
--- /dev/null
+++ b/makima/src/orchestration/mod.rs
@@ -0,0 +1,26 @@
+//! 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};
+pub use planner::{ChainPlanner, PlannerError};
+pub use verifier::{
+ auto_detect_verifiers, CompositeEvaluator, ConfidenceLevel, EvaluationResult, Verifier,
+ VerifierError, VerifierResult, VerifierType,
+};
diff --git a/makima/src/orchestration/planner.rs b/makima/src/orchestration/planner.rs
new file mode 100644
index 0000000..cdca8a0
--- /dev/null
+++ b/makima/src/orchestration/planner.rs
@@ -0,0 +1,742 @@
+//! 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>,
+}
+
+/// 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>,
+}
+
+impl Default for ChainPlanner {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl ChainPlanner {
+ /// Create a new chain planner.
+ pub fn new() -> Self {
+ Self {
+ default_step_types: vec![
+ "research".to_string(),
+ "design".to_string(),
+ "implement".to_string(),
+ "test".to_string(),
+ "review".to_string(),
+ "document".to_string(),
+ ],
+ }
+ }
+
+ /// 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,
+ )
+ }
+}
+
+/// 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::new();
+ let chain = make_test_chain();
+ assert!(planner.validate_chain(&chain).is_ok());
+ }
+
+ #[test]
+ fn test_validate_chain_invalid_dependency() {
+ let planner = ChainPlanner::new();
+ 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::new();
+ 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::new();
+ 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
new file mode 100644
index 0000000..e98da50
--- /dev/null
+++ b/makima/src/orchestration/verifier.rs
@@ -0,0 +1,806 @@
+//! 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),
+}
+
+/// 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;
+
+ /// 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()
+ }
+
+ 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/contract_chat.rs b/makima/src/server/handlers/contract_chat.rs
index 06b3a7c..8153093 100644
--- a/makima/src/server/handlers/contract_chat.rs
+++ b/makima/src/server/handlers/contract_chat.rs
@@ -17,8 +17,6 @@ use uuid::Uuid;
use crate::db::{
models::{
ContractChatHistoryResponse, ContractWithRelations, CreateTaskRequest, UpdateFileRequest,
- AddContractDefinitionRequest, UpdateContractDefinitionRequest, CreateChainRequest,
- CreateChainDirectiveRequest, CreateContractEvaluationRequest,
},
repository,
};
@@ -2767,1211 +2765,26 @@ async fn handle_contract_request(
}
}
- // Chain directive tools - for directive contracts to create and manage chains
- ContractToolRequest::CreateChainFromDirective { name, description } => {
- // First, get the current contract to verify it's a directive contract
- let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => return ContractRequestResult {
- success: false,
- message: "Contract not found".to_string(),
- data: None,
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- // Check if contract already has a spawned chain
- if contract.spawned_chain_id.is_some() {
- return ContractRequestResult {
- success: false,
- message: "This contract already has a chain associated with it".to_string(),
- data: Some(json!({ "existing_chain_id": contract.spawned_chain_id })),
- };
- }
-
- // Create the chain
- let chain_req = CreateChainRequest {
- name: name.clone(),
- description: description.clone(),
- repositories: None,
- loop_enabled: None,
- loop_max_iterations: None,
- loop_progress_check: None,
- contracts: None,
- };
-
- let chain = match repository::create_chain_for_owner(pool, owner_id, chain_req).await {
- Ok(c) => c,
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Failed to create chain: {}", e),
- data: None,
- },
- };
-
- // Link the chain to this directive contract
- if let Err(e) = sqlx::query(
- r#"
- UPDATE chains SET directive_contract_id = $2, evaluation_enabled = true WHERE id = $1;
- UPDATE contracts SET spawned_chain_id = $1, is_chain_directive = true WHERE id = $2;
- "#,
- )
- .bind(chain.id)
- .bind(contract_id)
- .execute(pool)
- .await {
- return ContractRequestResult {
- success: false,
- message: format!("Failed to link chain to contract: {}", e),
- data: None,
- };
- }
-
- // Create empty directive for the chain
- let directive_req = CreateChainDirectiveRequest {
- requirements: Some(vec![]),
- acceptance_criteria: Some(vec![]),
- constraints: Some(vec![]),
- external_dependencies: Some(vec![]),
- source_type: Some("llm_generated".to_string()),
- };
-
- if let Err(e) = repository::create_chain_directive(pool, chain.id, directive_req).await {
- return ContractRequestResult {
- success: false,
- message: format!("Failed to create directive: {}", e),
- data: None,
- };
- }
-
- ContractRequestResult {
- success: true,
- message: format!("Created chain '{}' linked to this directive contract", name),
- data: Some(json!({
- "chain_id": chain.id,
- "chain_name": name,
- "description": description
- })),
- }
- }
-
- ContractToolRequest::AddChainContract { name, description, contract_type, depends_on, requirement_ids } => {
- // Get the contract's spawned chain
- let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => return ContractRequestResult {
- success: false,
- message: "Contract not found".to_string(),
- data: None,
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- let chain_id = match contract.spawned_chain_id {
- Some(id) => id,
- None => return ContractRequestResult {
- success: false,
- message: "This contract has no associated chain. Use create_chain_from_directive first.".to_string(),
- data: None,
- },
- };
-
- // Check for duplicate names
- let existing_defs = match repository::list_chain_contract_definitions(pool, chain_id).await {
- Ok(defs) => defs,
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Failed to list definitions: {}", e),
- data: None,
- },
- };
-
- if existing_defs.iter().any(|d| d.name == name) {
- return ContractRequestResult {
- success: false,
- message: format!("A contract definition with name '{}' already exists", name),
- data: None,
- };
- }
-
- // Create the contract definition
- let def_req = AddContractDefinitionRequest {
- name: name.clone(),
- description,
- contract_type: contract_type.unwrap_or_else(|| "implementation".to_string()),
- initial_phase: Some("research".to_string()),
- depends_on,
- tasks: None,
- deliverables: None,
- validation: None,
- editor_x: None,
- editor_y: None,
- };
-
- let definition = match repository::create_chain_contract_definition(pool, chain_id, def_req).await {
- Ok(d) => d,
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Failed to create contract definition: {}", e),
- data: None,
- },
- };
-
- // Update requirement_ids if provided
- if let Some(req_ids) = requirement_ids {
- if !req_ids.is_empty() {
- if let Err(e) = sqlx::query(
- "UPDATE chain_contract_definitions SET requirement_ids = $2 WHERE id = $1"
- )
- .bind(definition.id)
- .bind(&req_ids)
- .execute(pool)
- .await {
- tracing::warn!("Failed to set requirement_ids: {}", e);
- }
- }
- }
-
- ContractRequestResult {
- success: true,
- message: format!("Added contract '{}' to chain", name),
- data: Some(json!({
- "definition_id": definition.id,
- "name": name,
- "order_index": definition.order_index
- })),
- }
- }
-
- ContractToolRequest::SetChainDependencies { contract_name, depends_on } => {
- // Get the contract's spawned chain
- let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => return ContractRequestResult {
- success: false,
- message: "Contract not found".to_string(),
- data: None,
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- let chain_id = match contract.spawned_chain_id {
- Some(id) => id,
- None => return ContractRequestResult {
- success: false,
- message: "This contract has no associated chain".to_string(),
- data: None,
- },
- };
-
- // Find the definition by name
- let definitions = match repository::list_chain_contract_definitions(pool, chain_id).await {
- Ok(defs) => defs,
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Failed to list definitions: {}", e),
- data: None,
- },
- };
-
- let definition = match definitions.iter().find(|d| d.name == contract_name) {
- Some(d) => d,
- None => return ContractRequestResult {
- success: false,
- message: format!("No contract definition named '{}' found", contract_name),
- data: None,
- },
- };
-
- // Validate that all dependencies exist
- for dep_name in &depends_on {
- if !definitions.iter().any(|d| &d.name == dep_name) {
- return ContractRequestResult {
- success: false,
- message: format!("Dependency '{}' does not exist", dep_name),
- data: None,
- };
- }
- }
-
- // Check for circular dependencies (simple check)
- if depends_on.contains(&contract_name) {
- return ContractRequestResult {
- success: false,
- message: "A contract cannot depend on itself".to_string(),
- data: None,
- };
- }
-
- // Update dependencies
- let update_req = UpdateContractDefinitionRequest {
- name: None,
- description: None,
- contract_type: None,
- initial_phase: None,
- depends_on: Some(depends_on.clone()),
- tasks: None,
- deliverables: None,
- validation: None,
- editor_x: None,
- editor_y: None,
- };
-
- match repository::update_chain_contract_definition(pool, definition.id, update_req).await {
- Ok(_) => ContractRequestResult {
- success: true,
- message: format!("Updated dependencies for '{}'", contract_name),
- data: Some(json!({
- "contract_name": contract_name,
- "depends_on": depends_on
- })),
- },
- Err(e) => ContractRequestResult {
- success: false,
- message: format!("Failed to update dependencies: {}", e),
- data: None,
- },
- }
- }
-
- ContractToolRequest::ModifyChainContract { name, new_name, description, add_requirement_ids, remove_requirement_ids } => {
- // Get the contract's spawned chain
- let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => return ContractRequestResult {
- success: false,
- message: "Contract not found".to_string(),
- data: None,
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- let chain_id = match contract.spawned_chain_id {
- Some(id) => id,
- None => return ContractRequestResult {
- success: false,
- message: "This contract has no associated chain".to_string(),
- data: None,
- },
- };
-
- // Find the definition by name
- let definitions = match repository::list_chain_contract_definitions(pool, chain_id).await {
- Ok(defs) => defs,
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Failed to list definitions: {}", e),
- data: None,
- },
- };
-
- let definition = match definitions.iter().find(|d| d.name == name) {
- Some(d) => d.clone(),
- None => return ContractRequestResult {
- success: false,
- message: format!("No contract definition named '{}' found", name),
- data: None,
- },
- };
-
- // Check if new name would conflict
- if let Some(ref nn) = new_name {
- if nn != &name && definitions.iter().any(|d| &d.name == nn) {
- return ContractRequestResult {
- success: false,
- message: format!("A contract definition named '{}' already exists", nn),
- data: None,
- };
- }
- }
-
- // Update the definition
- let update_req = UpdateContractDefinitionRequest {
- name: new_name.clone(),
- description,
- contract_type: None,
- initial_phase: None,
- depends_on: None,
- tasks: None,
- deliverables: None,
- validation: None,
- editor_x: None,
- editor_y: None,
- };
-
- if let Err(e) = repository::update_chain_contract_definition(pool, definition.id, update_req).await {
- return ContractRequestResult {
- success: false,
- message: format!("Failed to update definition: {}", e),
- data: None,
- };
- }
-
- // Handle requirement_ids modifications
- let mut current_req_ids: Vec<String> = definition.requirement_ids.clone();
- if let Some(add_ids) = add_requirement_ids {
- for id in add_ids {
- if !current_req_ids.contains(&id) {
- current_req_ids.push(id);
- }
- }
- }
- if let Some(remove_ids) = remove_requirement_ids {
- current_req_ids.retain(|id| !remove_ids.contains(id));
- }
-
- if current_req_ids != definition.requirement_ids {
- if let Err(e) = sqlx::query(
- "UPDATE chain_contract_definitions SET requirement_ids = $2 WHERE id = $1"
- )
- .bind(definition.id)
- .bind(&current_req_ids)
- .execute(pool)
- .await {
- tracing::warn!("Failed to update requirement_ids: {}", e);
- }
- }
-
- ContractRequestResult {
- success: true,
- message: format!("Modified contract definition '{}'", new_name.as_ref().unwrap_or(&name)),
- data: Some(json!({
- "definition_id": definition.id,
- "name": new_name.as_ref().unwrap_or(&name),
- "requirement_ids": current_req_ids
- })),
- }
- }
-
- ContractToolRequest::RemoveChainContract { name } => {
- // Get the contract's spawned chain
- let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => return ContractRequestResult {
- success: false,
- message: "Contract not found".to_string(),
- data: None,
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- let chain_id = match contract.spawned_chain_id {
- Some(id) => id,
- None => return ContractRequestResult {
- success: false,
- message: "This contract has no associated chain".to_string(),
- data: None,
- },
- };
-
- // Find the definition by name
- let definitions = match repository::list_chain_contract_definitions(pool, chain_id).await {
- Ok(defs) => defs,
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Failed to list definitions: {}", e),
- data: None,
- },
- };
-
- let definition = match definitions.iter().find(|d| d.name == name) {
- Some(d) => d,
- None => return ContractRequestResult {
- success: false,
- message: format!("No contract definition named '{}' found", name),
- data: None,
- },
- };
-
- // Check if other definitions depend on this one
- let dependents: Vec<&str> = definitions.iter()
- .filter(|d| d.depends_on_names.contains(&name))
- .map(|d| d.name.as_str())
- .collect();
-
- if !dependents.is_empty() {
- return ContractRequestResult {
- success: false,
- message: format!("Cannot remove '{}': other contracts depend on it: {}", name, dependents.join(", ")),
- data: None,
- };
- }
-
- // Delete the definition
- match repository::delete_chain_contract_definition(pool, definition.id).await {
- Ok(true) => ContractRequestResult {
- success: true,
- message: format!("Removed contract definition '{}'", name),
- data: Some(json!({ "removed": name })),
- },
- Ok(false) => ContractRequestResult {
- success: false,
- message: format!("Failed to remove '{}': not found", name),
- data: None,
- },
- Err(e) => ContractRequestResult {
- success: false,
- message: format!("Failed to remove definition: {}", e),
- data: None,
- },
- }
- }
-
- ContractToolRequest::PreviewChainDag => {
- // Get the contract's spawned chain
- let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => return ContractRequestResult {
- success: false,
- message: "Contract not found".to_string(),
- data: None,
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- let chain_id = match contract.spawned_chain_id {
- Some(id) => id,
- None => return ContractRequestResult {
- success: false,
- message: "This contract has no associated chain".to_string(),
- data: None,
- },
- };
-
- // Get chain details and definitions
- let chain = match repository::get_chain_for_owner(pool, chain_id, owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => return ContractRequestResult {
- success: false,
- message: "Chain not found".to_string(),
- data: None,
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- let definitions = match repository::list_chain_contract_definitions(pool, chain_id).await {
- Ok(defs) => defs,
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Failed to list definitions: {}", e),
- data: None,
- },
- };
-
- // Build DAG representation
- let nodes: Vec<serde_json::Value> = definitions.iter().map(|d| {
- json!({
- "name": d.name,
- "description": d.description,
- "contract_type": d.contract_type,
- "depends_on": d.depends_on_names,
- "requirement_ids": d.requirement_ids
- })
- }).collect();
-
- // Build ASCII DAG representation
- let mut ascii_dag = String::new();
- ascii_dag.push_str(&format!("Chain: {} ({})\n", chain.name, chain.status));
- ascii_dag.push_str(&format!("Contracts: {}\n\n", definitions.len()));
-
- // Find root nodes (no dependencies)
- let roots: Vec<&str> = definitions.iter()
- .filter(|d| d.depends_on_names.is_empty())
- .map(|d| d.name.as_str())
- .collect();
-
- ascii_dag.push_str("Root contracts (no dependencies):\n");
- for root in &roots {
- ascii_dag.push_str(&format!(" [{}]\n", root));
- }
-
- ascii_dag.push_str("\nDependency relationships:\n");
- for def in &definitions {
- if !def.depends_on_names.is_empty() {
- ascii_dag.push_str(&format!(" {} <- {}\n", def.name, def.depends_on_names.join(", ")));
- }
- }
-
- ContractRequestResult {
- success: true,
- message: format!("Chain DAG preview with {} contracts", definitions.len()),
- data: Some(json!({
- "chain_id": chain_id,
- "chain_name": chain.name,
- "chain_status": chain.status,
- "contract_count": definitions.len(),
- "nodes": nodes,
- "ascii_dag": ascii_dag
- })),
- }
- }
-
- ContractToolRequest::ValidateChainDirective => {
- // Get the contract's spawned chain
- let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => return ContractRequestResult {
- success: false,
- message: "Contract not found".to_string(),
- data: None,
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- let chain_id = match contract.spawned_chain_id {
- Some(id) => id,
- None => return ContractRequestResult {
- success: false,
- message: "This contract has no associated chain".to_string(),
- data: None,
- },
- };
-
- let definitions = match repository::list_chain_contract_definitions(pool, chain_id).await {
- Ok(defs) => defs,
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Failed to list definitions: {}", e),
- data: None,
- },
- };
-
- let mut errors: Vec<String> = Vec::new();
- let mut warnings: Vec<String> = Vec::new();
-
- // Check for empty chain
- if definitions.is_empty() {
- errors.push("Chain has no contract definitions".to_string());
- }
-
- // Check for circular dependencies
- let def_names: std::collections::HashSet<String> = definitions.iter().map(|d| d.name.clone()).collect();
- for def in &definitions {
- for dep in &def.depends_on_names {
- if !def_names.contains(dep) {
- errors.push(format!("'{}' depends on non-existent contract '{}'", def.name, dep));
- }
- }
- }
-
- // Simple cycle detection using DFS
- fn has_cycle(
- name: &str,
- definitions: &[crate::db::models::ChainContractDefinition],
- visited: &mut std::collections::HashSet<String>,
- rec_stack: &mut std::collections::HashSet<String>,
- ) -> Option<String> {
- visited.insert(name.to_string());
- rec_stack.insert(name.to_string());
-
- if let Some(def) = definitions.iter().find(|d| d.name == name) {
- for dep in &def.depends_on_names {
- if !visited.contains(dep) {
- if let Some(cycle) = has_cycle(dep, definitions, visited, rec_stack) {
- return Some(cycle);
- }
- } else if rec_stack.contains(dep) {
- return Some(format!("{} -> {}", name, dep));
- }
- }
- }
-
- rec_stack.remove(name);
- None
- }
-
- let mut visited = std::collections::HashSet::new();
- for def in &definitions {
- if !visited.contains(&def.name) {
- let mut rec_stack = std::collections::HashSet::new();
- if let Some(cycle) = has_cycle(&def.name, &definitions, &mut visited, &mut rec_stack) {
- errors.push(format!("Circular dependency detected: {}", cycle));
- break;
- }
- }
- }
-
- // Check for orphan contracts (no one depends on them and they're not root)
- let roots: std::collections::HashSet<&str> = definitions.iter()
- .filter(|d| d.depends_on_names.is_empty())
- .map(|d| d.name.as_str())
- .collect();
-
- let depended_on: std::collections::HashSet<&str> = definitions.iter()
- .flat_map(|d| d.depends_on_names.iter().map(|s| s.as_str()))
- .collect();
-
- for def in &definitions {
- if !roots.contains(def.name.as_str()) && !depended_on.contains(def.name.as_str()) {
- warnings.push(format!("'{}' has dependencies but nothing depends on it (orphan leaf)", def.name));
- }
- }
-
- // Get directive to check requirement coverage
- if let Ok(Some(directive)) = repository::get_chain_directive(pool, chain_id).await {
- let requirements: Vec<crate::db::models::DirectiveRequirement> =
- serde_json::from_value(directive.requirements.clone()).unwrap_or_default();
-
- let covered: std::collections::HashSet<&str> = definitions.iter()
- .flat_map(|d| d.requirement_ids.iter().map(|s| s.as_str()))
- .collect();
-
- for req in &requirements {
- if !covered.contains(req.id.as_str()) {
- warnings.push(format!("Requirement '{}' ({}) is not covered by any contract", req.id, req.title));
- }
- }
- }
-
- let is_valid = errors.is_empty();
-
- ContractRequestResult {
- success: is_valid,
- message: if is_valid {
- format!("Chain is valid with {} contracts", definitions.len())
- } else {
- format!("Chain validation failed with {} errors", errors.len())
- },
- data: Some(json!({
- "valid": is_valid,
- "contract_count": definitions.len(),
- "errors": errors,
- "warnings": warnings
- })),
- }
- }
-
- ContractToolRequest::FinalizeChainDirective { auto_start } => {
- // Get the contract's spawned chain
- let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => return ContractRequestResult {
- success: false,
- message: "Contract not found".to_string(),
- data: None,
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- let chain_id = match contract.spawned_chain_id {
- Some(id) => id,
- None => return ContractRequestResult {
- success: false,
- message: "This contract has no associated chain".to_string(),
- data: None,
- },
- };
-
- // Get chain
- let chain = match repository::get_chain_for_owner(pool, chain_id, owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => return ContractRequestResult {
- success: false,
- message: "Chain not found".to_string(),
- data: None,
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- if chain.status != "pending" {
- return ContractRequestResult {
- success: false,
- message: format!("Chain is already {} - cannot finalize", chain.status),
- data: None,
- };
- }
-
- // Update chain status
- let new_status = if auto_start { "active" } else { "pending" };
- if let Err(e) = sqlx::query("UPDATE chains SET status = $2 WHERE id = $1")
- .bind(chain_id)
- .bind(new_status)
- .execute(pool)
- .await {
- return ContractRequestResult {
- success: false,
- message: format!("Failed to update chain status: {}", e),
- data: None,
- };
- }
-
- // If auto_start, trigger chain progression to create root contracts
- if auto_start {
- match repository::progress_chain(pool, chain_id, owner_id).await {
- Ok(result) => {
- ContractRequestResult {
- success: true,
- message: format!("Chain finalized and started. Created {} root contracts.", result.contracts_created.len()),
- data: Some(json!({
- "chain_id": chain_id,
- "status": "active",
- "contracts_created": result.contracts_created,
- "chain_completed": result.chain_completed
- })),
- }
- }
- Err(e) => ContractRequestResult {
- success: false,
- message: format!("Chain finalized but failed to start: {}", e),
- data: Some(json!({ "chain_id": chain_id, "status": "active" })),
- },
- }
- } else {
- ContractRequestResult {
- success: true,
- message: "Chain finalized but not started. Call finalize_chain_directive with auto_start=true to start.".to_string(),
- data: Some(json!({
- "chain_id": chain_id,
- "status": "pending"
- })),
- }
- }
- }
-
- ContractToolRequest::GetChainStatus => {
- // Get the contract's spawned chain
- let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => return ContractRequestResult {
- success: false,
- message: "Contract not found".to_string(),
- data: None,
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- let chain_id = match contract.spawned_chain_id {
- Some(id) => id,
- None => return ContractRequestResult {
- success: false,
- message: "This contract has no associated chain".to_string(),
- data: None,
- },
- };
-
- // Get chain details
- let chain = match repository::get_chain_for_owner(pool, chain_id, owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => return ContractRequestResult {
- success: false,
- message: "Chain not found".to_string(),
- data: None,
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- // Get definitions
- let definitions = match repository::list_chain_contract_definitions(pool, chain_id).await {
- Ok(defs) => defs,
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Failed to list definitions: {}", e),
- data: None,
- },
- };
-
- // Get instantiated contracts
- let chain_contracts = match repository::list_chain_contracts(pool, chain_id).await {
- Ok(cc) => cc,
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Failed to list chain contracts: {}", e),
- data: None,
- },
- };
-
- // Build status map
- let contract_statuses: Vec<serde_json::Value> = chain_contracts.iter().map(|cc| {
- json!({
- "name": cc.contract_name,
- "contract_id": cc.contract_id,
- "status": cc.contract_status,
- "phase": cc.contract_phase,
- "evaluation_status": cc.evaluation_status,
- "evaluation_retry_count": cc.evaluation_retry_count
- })
- }).collect();
-
- let completed = chain_contracts.iter().filter(|cc| cc.contract_status == "completed").count();
- let active = chain_contracts.iter().filter(|cc| cc.contract_status == "active").count();
- let pending = definitions.len() - chain_contracts.len();
-
- ContractRequestResult {
- success: true,
- message: format!("Chain '{}': {} completed, {} active, {} pending",
- chain.name, completed, active, pending),
- data: Some(json!({
- "chain_id": chain_id,
- "chain_name": chain.name,
- "chain_status": chain.status,
- "total_definitions": definitions.len(),
- "instantiated": chain_contracts.len(),
- "completed": completed,
- "active": active,
- "pending": pending,
- "contracts": contract_statuses
- })),
- }
- }
-
- ContractToolRequest::GetUncoveredRequirements => {
- // Get the contract's spawned chain
- let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => return ContractRequestResult {
- success: false,
- message: "Contract not found".to_string(),
- data: None,
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- let chain_id = match contract.spawned_chain_id {
- Some(id) => id,
- None => return ContractRequestResult {
- success: false,
- message: "This contract has no associated chain".to_string(),
- data: None,
- },
- };
-
- // Get directive
- let directive = match repository::get_chain_directive(pool, chain_id).await {
- Ok(Some(d)) => d,
- Ok(None) => return ContractRequestResult {
- success: true,
- message: "No directive found for this chain".to_string(),
- data: Some(json!({ "uncovered": [], "total_requirements": 0 })),
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- // Get definitions
- let definitions = match repository::list_chain_contract_definitions(pool, chain_id).await {
- Ok(defs) => defs,
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Failed to list definitions: {}", e),
- data: None,
- },
- };
-
- // Parse requirements
- let requirements: Vec<crate::db::models::DirectiveRequirement> =
- serde_json::from_value(directive.requirements.clone()).unwrap_or_default();
-
- // Find covered requirement IDs
- let covered: std::collections::HashSet<String> = definitions.iter()
- .flat_map(|d| d.requirement_ids.iter().cloned())
- .collect();
-
- // Find uncovered requirements
- let uncovered: Vec<serde_json::Value> = requirements.iter()
- .filter(|r| !covered.contains(&r.id))
- .map(|r| json!({
- "id": r.id,
- "title": r.title,
- "priority": r.priority
- }))
- .collect();
-
- ContractRequestResult {
- success: true,
- message: format!("{} of {} requirements are uncovered", uncovered.len(), requirements.len()),
- data: Some(json!({
- "uncovered": uncovered,
- "uncovered_count": uncovered.len(),
- "total_requirements": requirements.len(),
- "coverage_percent": if requirements.is_empty() { 100.0 } else {
- ((requirements.len() - uncovered.len()) as f64 / requirements.len() as f64 * 100.0).round()
- }
- })),
- }
- }
-
- ContractToolRequest::EvaluateContractCompletion { contract_id: target_contract_id, passed, feedback, rework_instructions } => {
- // Get the directive contract's spawned chain
- let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => return ContractRequestResult {
- success: false,
- message: "Contract not found".to_string(),
- data: None,
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- let chain_id = match contract.spawned_chain_id {
- Some(id) => id,
- None => return ContractRequestResult {
- success: false,
- message: "This contract has no associated chain".to_string(),
- data: None,
- },
- };
-
- // Verify the target contract is part of this chain
- let chain_contract = match repository::get_chain_contract_by_contract_id(pool, target_contract_id).await {
- Ok(Some(cc)) => cc,
- Ok(None) => return ContractRequestResult {
- success: false,
- message: format!("Contract {} is not part of a chain", target_contract_id),
- data: None,
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- if chain_contract.chain_id != chain_id {
- return ContractRequestResult {
- success: false,
- message: "Contract is not part of this directive's chain".to_string(),
- data: None,
- };
- }
-
- // Create evaluation record
- let eval_req = CreateContractEvaluationRequest {
- contract_id: target_contract_id,
- chain_id: Some(chain_id),
- chain_contract_id: Some(chain_contract.id),
- evaluator_model: Some("directive_contract".to_string()),
- passed,
- overall_score: if passed { Some(1.0) } else { Some(0.0) },
- criteria_results: vec![],
- summary_feedback: feedback.clone(),
- rework_instructions: rework_instructions.clone(),
- };
-
- let evaluation = match repository::create_contract_evaluation(pool, eval_req).await {
- Ok(e) => e,
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Failed to create evaluation: {}", e),
- data: None,
- },
- };
-
- // Update chain contract evaluation status
- let new_status = if passed { "passed" } else { "failed" };
- if let Err(e) = repository::update_chain_contract_evaluation_status(
- pool,
- chain_contract.id,
- new_status,
- Some(evaluation.id),
- None, // No rework feedback for passed/failed status
- ).await {
- tracing::warn!("Failed to update chain contract evaluation status: {}", e);
- }
-
- if passed {
- // Progress the chain to create downstream contracts
- match repository::progress_chain(pool, chain_id, owner_id).await {
- Ok(result) => ContractRequestResult {
- success: true,
- message: format!("Evaluation passed. Created {} downstream contracts.", result.contracts_created.len()),
- data: Some(json!({
- "evaluation_id": evaluation.id,
- "passed": true,
- "contracts_created": result.contracts_created,
- "chain_completed": result.chain_completed
- })),
- },
- Err(e) => ContractRequestResult {
- success: true,
- message: format!("Evaluation passed but failed to progress chain: {}", e),
- data: Some(json!({
- "evaluation_id": evaluation.id,
- "passed": true
- })),
- },
- }
- } else {
- // Mark contract for rework
- if let Err(e) = sqlx::query(
- r#"
- UPDATE chain_contracts SET evaluation_status = 'rework', rework_feedback = $2 WHERE id = $1;
- UPDATE contracts SET status = 'active' WHERE id = (SELECT contract_id FROM chain_contracts WHERE id = $1);
- "#
- )
- .bind(chain_contract.id)
- .bind(&rework_instructions)
- .execute(pool)
- .await {
- tracing::warn!("Failed to mark contract for rework: {}", e);
- }
-
- ContractRequestResult {
- success: true,
- message: format!("Evaluation failed. Contract marked for rework."),
- data: Some(json!({
- "evaluation_id": evaluation.id,
- "passed": false,
- "rework_instructions": rework_instructions,
- "retry_count": chain_contract.evaluation_retry_count + 1
- })),
- }
- }
- }
-
- ContractToolRequest::RequestRework { contract_id: target_contract_id, feedback } => {
- // Get the directive contract's spawned chain
- let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => return ContractRequestResult {
- success: false,
- message: "Contract not found".to_string(),
- data: None,
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- let chain_id = match contract.spawned_chain_id {
- Some(id) => id,
- None => return ContractRequestResult {
- success: false,
- message: "This contract has no associated chain".to_string(),
- data: None,
- },
- };
-
- // Verify the target contract is part of this chain
- let chain_contract = match repository::get_chain_contract_by_contract_id(pool, target_contract_id).await {
- Ok(Some(cc)) => cc,
- Ok(None) => return ContractRequestResult {
- success: false,
- message: format!("Contract {} is not part of a chain", target_contract_id),
- data: None,
- },
- Err(e) => return ContractRequestResult {
- success: false,
- message: format!("Database error: {}", e),
- data: None,
- },
- };
-
- if chain_contract.chain_id != chain_id {
- return ContractRequestResult {
- success: false,
- message: "Contract is not part of this directive's chain".to_string(),
- data: None,
- };
- }
-
- // Check retry count
- let max_retries = chain_contract.max_evaluation_retries;
- if chain_contract.evaluation_retry_count >= max_retries {
- return ContractRequestResult {
- success: false,
- message: format!("Contract has exceeded max retries ({}/{}). Escalate to user.",
- chain_contract.evaluation_retry_count, max_retries),
- data: Some(json!({
- "retry_count": chain_contract.evaluation_retry_count,
- "max_retries": max_retries,
- "escalation_required": true
- })),
- };
- }
-
- // Mark contract for rework and increment retry count
- if let Err(e) = sqlx::query(
- r#"
- UPDATE chain_contracts
- SET evaluation_status = 'rework',
- rework_feedback = $2,
- evaluation_retry_count = evaluation_retry_count + 1
- WHERE id = $1;
- UPDATE contracts SET status = 'active' WHERE id = (SELECT contract_id FROM chain_contracts WHERE id = $1);
- "#
- )
- .bind(chain_contract.id)
- .bind(&feedback)
- .execute(pool)
- .await {
- return ContractRequestResult {
- success: false,
- message: format!("Failed to request rework: {}", e),
- data: None,
- };
- }
+ // Chain directive tools - TEMPORARILY DISABLED
+ // These tools will be reimplemented using the new directive system.
+ // See the orchestration module for the new implementation.
+ ContractToolRequest::CreateChainFromDirective { .. } |
+ ContractToolRequest::AddChainContract { .. } |
+ ContractToolRequest::SetChainDependencies { .. } |
+ ContractToolRequest::ModifyChainContract { .. } |
+ ContractToolRequest::RemoveChainContract { .. } |
+ ContractToolRequest::PreviewChainDag |
+ ContractToolRequest::ValidateChainDirective |
+ ContractToolRequest::FinalizeChainDirective { .. } |
+ ContractToolRequest::GetChainStatus |
+ ContractToolRequest::GetUncoveredRequirements |
+ ContractToolRequest::EvaluateContractCompletion { .. } |
+ ContractToolRequest::RequestRework { .. } => {
ContractRequestResult {
- success: true,
- message: format!("Rework requested for contract. Retry {}/{}",
- chain_contract.evaluation_retry_count + 1, max_retries),
- data: Some(json!({
- "contract_id": target_contract_id,
- "retry_count": chain_contract.evaluation_retry_count + 1,
- "max_retries": max_retries,
- "feedback": feedback
- })),
+ success: false,
+ message: "Chain directive tools are temporarily disabled. The directive system is being reimplemented.".to_string(),
+ data: None,
}
}
}
diff --git a/makima/src/server/handlers/contracts.rs b/makima/src/server/handlers/contracts.rs
index 2b2fc26..8a6ce0f 100644
--- a/makima/src/server/handlers/contracts.rs
+++ b/makima/src/server/handlers/contracts.rs
@@ -575,78 +575,90 @@ pub async fn update_contract(
}),
).await;
- // If contract is part of a chain, check evaluation requirements
- if let Some(chain_id) = contract.chain_id {
- let pool_clone = pool.clone();
- let owner_id = auth.owner_id;
- let contract_id = contract.id;
- tokio::spawn(async move {
- // Check if chain has evaluation enabled
- let chain = match repository::get_chain_for_owner(&pool_clone, chain_id, owner_id).await {
- Ok(Some(c)) => c,
- Ok(None) => {
- tracing::warn!(chain_id = %chain_id, "Chain not found for progression");
- return;
- }
- Err(e) => {
- tracing::error!(chain_id = %chain_id, error = %e, "Failed to get chain");
- return;
- }
- };
-
- // If evaluation is enabled, mark contract for evaluation
- if chain.evaluation_enabled {
- // Mark the chain_contract as pending evaluation
- if let Ok(Some(chain_contract)) = repository::get_chain_contract_by_contract_id(&pool_clone, contract_id).await {
- if let Err(e) = repository::update_chain_contract_evaluation_status(
- &pool_clone,
- chain_contract.id,
- "pending_evaluation",
- None,
- None,
- ).await {
- tracing::error!(
- chain_id = %chain_id,
- contract_id = %contract_id,
- error = %e,
- "Failed to mark contract for evaluation"
- );
- } else {
- tracing::info!(
- chain_id = %chain_id,
- contract_id = %contract_id,
- "Contract marked for evaluation - waiting for directive contract to evaluate"
+ // 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;
}
- }
- // Don't progress chain - directive contract will evaluate and progress
- return;
- }
-
- // If evaluation is disabled, progress chain directly
- match repository::progress_chain(&pool_clone, chain_id, owner_id).await {
- Ok(result) => {
- if !result.contracts_created.is_empty() {
- tracing::info!(
- chain_id = %chain_id,
- contracts_created = ?result.contracts_created,
- "Chain progressed - created new contracts"
+ Err(e) => {
+ tracing::warn!(
+ chain_id = %step.chain_id,
+ error = %e,
+ "Failed to get chain for step"
);
+ return;
}
- if result.chain_completed {
- tracing::info!(chain_id = %chain_id, "Chain completed");
- }
- }
- Err(e) => {
- tracing::error!(
- chain_id = %chain_id,
+ };
+
+ // 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 progress chain after contract completion"
+ "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"
+ );
+ }
+ }
+ });
}
// Get summary with counts
diff --git a/makima/src/server/handlers/directives.rs b/makima/src/server/handlers/directives.rs
new file mode 100644
index 0000000..6f6c3f1
--- /dev/null
+++ b/makima/src/server/handlers/directives.rs
@@ -0,0 +1,1488 @@
+//! 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, UpdateDirectiveRequest,
+ 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 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) => Json(directives).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(()) => {
+ // 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
+pub async fn stream_events(
+ 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()
+ }
+ }
+
+ // 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()
+ }
+ }
+}
diff --git a/makima/src/server/handlers/mod.rs b/makima/src/server/handlers/mod.rs
index 5e172bc..d3fabf7 100644
--- a/makima/src/server/handlers/mod.rs
+++ b/makima/src/server/handlers/mod.rs
@@ -1,7 +1,8 @@
//! HTTP and WebSocket request handlers.
pub mod api_keys;
-pub mod chains;
+// 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 e5b55e7..927e9a5 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, chains, 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;
@@ -214,51 +214,55 @@ pub fn make_router(state: SharedState) -> Router {
)
// Timeline endpoint (unified history for user)
.route("/timeline", get(history::get_timeline))
- // Chain endpoints (multi-contract orchestration)
+ // Directive endpoints (replacement for chains)
.route(
- "/chains",
- get(chains::list_chains).post(chains::create_chain),
+ "/directives",
+ get(directives::list_directives).post(directives::create_directive),
)
- .route("/chains/init", post(chains::init_chain))
.route(
- "/chains/{id}",
- get(chains::get_chain)
- .put(chains::update_chain)
- .delete(chains::delete_chain),
+ "/directives/{id}",
+ get(directives::get_directive)
+ .put(directives::update_directive)
+ .delete(directives::archive_directive),
)
- .route("/chains/{id}/contracts", get(chains::get_chain_contracts))
- .route("/chains/{id}/graph", get(chains::get_chain_graph))
- .route("/chains/{id}/events", get(chains::get_chain_events))
- .route("/chains/{id}/editor", get(chains::get_chain_editor))
- // Chain contract definitions
+ .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))
+ // 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(
- "/chains/{id}/definitions",
- get(chains::list_chain_definitions).post(chains::create_chain_definition),
+ "/directives/{id}/chain/steps",
+ post(directives::add_step),
)
.route(
- "/chains/{chain_id}/definitions/{definition_id}",
- put(chains::update_chain_definition).delete(chains::delete_chain_definition),
+ "/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))
+ // 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(
- "/chains/{id}/definitions/graph",
- get(chains::get_chain_definition_graph),
+ "/directives/{id}/verifiers",
+ get(directives::list_verifiers).post(directives::add_verifier),
)
- // Chain control
- .route("/chains/{id}/start", post(chains::start_chain))
- .route("/chains/{id}/stop", post(chains::stop_chain))
- // Chain repositories
.route(
- "/chains/{id}/repositories",
- get(chains::list_chain_repositories).post(chains::add_chain_repository),
- )
- .route(
- "/chains/{chain_id}/repositories/{repository_id}",
- axum::routing::delete(chains::delete_chain_repository),
- )
- .route(
- "/chains/{chain_id}/repositories/{repository_id}/primary",
- put(chains::set_chain_repository_primary),
+ "/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