From 88a4f15ce1310f8ee8693835be14aa5280233f17 Mon Sep 17 00:00:00 2001 From: soryu Date: Thu, 5 Feb 2026 23:42:48 +0000 Subject: Add directive-first chain system redesign Redesigns the chain system with a directive-first architecture where Directive is the top-level entity (the "why/what") and Chains are generated execution plans (the "how") that can be dynamically modified. Backend: - Add database migration for directive system tables - Add Directive, DirectiveChain, ChainStep, DirectiveEvent models - Add DirectiveVerifier and DirectiveApproval models - Add orchestration module with engine, planner, and verifier - Add comprehensive API handlers for directives - Add daemon CLI commands for directive management - Add directive skill documentation - Integrate contract completion with directive engine - Add SSE endpoint for real-time directive events Frontend: - Add directives route with split-view layout - Add 6-tab detail view (Overview, Chain, Events, Evaluations, Approvals, Verifiers) - Add React Flow DAG visualization for chain steps - Add SSE subscription hook for real-time event updates - Add useDirectives and useDirectiveEventSubscription hooks - Add directive types and API functions Fixes: - Fix test failures in ws/protocol, task_output, completion_gate, patch - Fix word boundary matching in looks_like_task() - Fix parse_last() to find actual last completion gate - Fix create_export_patch when merge-base equals HEAD - Clean up clippy warnings in new code Co-Authored-By: Claude Opus 4.5 --- makima/frontend/src/hooks/useDirectives.ts | 298 +++++++++++++++++++++++++++++ 1 file changed, 298 insertions(+) create mode 100644 makima/frontend/src/hooks/useDirectives.ts (limited to 'makima/frontend/src/hooks/useDirectives.ts') 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; + createNewDirective: (req: CreateDirectiveRequest) => Promise; + updateExistingDirective: ( + directiveId: string, + req: UpdateDirectiveRequest + ) => Promise; + archiveExistingDirective: (directiveId: string) => Promise; + getDirectiveById: (directiveId: string) => Promise; + getGraph: (directiveId: string) => Promise; + start: (directiveId: string) => Promise; + pause: (directiveId: string) => Promise; + resume: (directiveId: string) => Promise; + stop: (directiveId: string) => Promise; +} + +export function useDirectives(statusFilter?: DirectiveStatus): UseDirectivesResult { + const [directives, setDirectives] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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([]); + const [isConnected, setIsConnected] = useState(false); + const [error, setError] = useState(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 }; +} -- cgit v1.2.3