From dce7f50e503dc374aaf879df33e725af16c4cc78 Mon Sep 17 00:00:00 2001 From: soryu Date: Fri, 8 May 2026 16:34:11 +0100 Subject: feat(directives): drop directives.goal — orchestration reads contract body (#132) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hard cut. The unified contracts surface owns spec text now; the directive itself is just a folder. The orchestrator daemon reads the active contract's body when it spawns, replans, or runs completion. Schema (migration 20260510000000): - DROP TABLE directive_goal_history - ALTER TABLE directives DROP COLUMN goal - ALTER TABLE directives DROP COLUMN goal_updated_at New repo helper: - get_active_contract_body(directive_id) — picks the active|queued|draft contract (in that order), most-recent first. Backend cuts: - Directive / DirectiveSummary / CreateDirectiveRequest / UpdateDirectiveRequest lose goal & goalUpdatedAt. - CreateDirectiveRequest gains optional `contractBody` — when provided, create_directive_for_owner auto-creates a first contract with that body in the same transaction. - Removed: update_directive_goal, update_directive_goal_keep_orchestrator, save_directive_goal_history, get_directive_goal_history, DirectiveGoalHistory model, UpdateGoalRequest. - Removed handlers::directives::update_goal + the /directives/{id}/goal route. - orchestration::directive::build_planning_prompt / build_completion_prompt / build_order_pickup_prompt now take a `contract_body: &str` instead of `goal_history`. classify_goal_change + try_interrupt_planner_with_goal_edit + GoalChangeKind + GoalEditInterruptResult removed (they were only useful for the small-vs-large goal-edit interrupt cycle). CLI: - `makima directive update-goal` removed (UpdateGoalArgs deleted, Commands enum trimmed, ApiClient::directive_update_goal + UpdateGoalRequest deleted). Frontend: - Directive / DirectiveSummary / CreateDirectiveRequest types lose goal & goalUpdatedAt; CreateDirectiveRequest gains `contractBody`. - useDirective drops updateGoal helper. - api.ts updateDirectiveGoal removed. - Legacy DirectiveList + DirectiveDetail components deleted; the /directives route now always renders the document-mode page. The user-settings documentModeEnabled flag is no longer consulted at the route level. - NewContractModal passes body via contractBody. Co-authored-by: Claude Opus 4.7 (1M context) --- .../src/components/directives/DirectiveDetail.tsx | 603 --------------------- .../src/components/directives/DirectiveList.tsx | 140 ----- makima/frontend/src/hooks/useDirectives.ts | 9 +- makima/frontend/src/lib/api.ts | 19 +- makima/frontend/src/routes/directives.tsx | 324 +---------- makima/frontend/src/routes/document-directives.tsx | 4 +- .../20260510000000_drop_directive_goal.sql | 16 + makima/src/bin/makima.rs | 7 - makima/src/daemon/api/directive.rs | 16 - makima/src/daemon/cli/directive.rs | 10 +- makima/src/daemon/cli/mod.rs | 3 - makima/src/db/models.rs | 40 +- makima/src/db/repository.rs | 200 +++---- makima/src/orchestration/directive.rs | 365 ++----------- makima/src/server/handlers/directives.rs | 203 +------ makima/src/server/mod.rs | 1 - makima/src/server/openapi.rs | 4 +- 17 files changed, 198 insertions(+), 1766 deletions(-) delete mode 100644 makima/frontend/src/components/directives/DirectiveDetail.tsx delete mode 100644 makima/frontend/src/components/directives/DirectiveList.tsx create mode 100644 makima/migrations/20260510000000_drop_directive_goal.sql diff --git a/makima/frontend/src/components/directives/DirectiveDetail.tsx b/makima/frontend/src/components/directives/DirectiveDetail.tsx deleted file mode 100644 index 4931afa..0000000 --- a/makima/frontend/src/components/directives/DirectiveDetail.tsx +++ /dev/null @@ -1,603 +0,0 @@ -import { useState, useMemo, useEffect, useRef } from "react"; -import type { DirectiveWithSteps, DirectiveStatus, UpdateDirectiveRequest, DirectiveOrderGroup, CreateDOGRequest, UpdateDOGRequest } from "../../lib/api"; -import { DirectiveDAG } from "./DirectiveDAG"; -import type { SpecializedStep } from "./DirectiveDAG"; -import { DirectiveLogStream } from "./DirectiveLogStream"; -import { TaskSlideOutPanel } from "./TaskSlideOutPanel"; -import { DOGList } from "./DOGList"; -import { useMultiTaskSubscription } from "../../hooks/useMultiTaskSubscription"; -import { useSupervisorQuestions } from "../../contexts/SupervisorQuestionsContext"; - -const STATUS_BADGE: Record = { - draft: { color: "text-[#7788aa] border-[#2a3a5a]", label: "DRAFT" }, - active: { color: "text-green-400 border-green-800", label: "ACTIVE" }, - idle: { color: "text-yellow-400 border-yellow-800", label: "IDLE" }, - paused: { color: "text-orange-400 border-orange-800", label: "PAUSED" }, - inactive: { color: "text-[#9bc3ff] border-[#3f6fb3]", label: "INACTIVE" }, - archived: { color: "text-[#556677] border-[#2a3a5a]", label: "ARCHIVED" }, -}; - -interface DirectiveDetailProps { - directive: DirectiveWithSteps; - onStart: () => void; - onPause: () => void; - onAdvance: () => void; - onCompleteStep: (stepId: string) => void; - onFailStep: (stepId: string) => void; - onSkipStep: (stepId: string) => void; - onUpdateGoal: (goal: string) => void; - onUpdate: (req: UpdateDirectiveRequest) => void; - onDelete: () => void; - onRefresh: () => void; - onCleanup: () => void; - onPickUpOrders: () => Promise<{ message: string; orderCount: number; taskId: string | null } | null>; - onCreatePR: () => Promise; - dogs: DirectiveOrderGroup[]; - dogsLoading: boolean; - onCreateDog: (req: CreateDOGRequest) => Promise; - onUpdateDog: (dogId: string, req: UpdateDOGRequest) => Promise; - onDeleteDog: (dogId: string) => Promise; - onPickUpDogOrders: (dogId: string) => Promise; -} - -export function DirectiveDetail({ - directive, - onStart, - onPause, - onAdvance, - onCompleteStep, - onFailStep, - onSkipStep, - onUpdateGoal, - onUpdate, - onDelete, - onRefresh, - onCleanup, - onPickUpOrders, - onCreatePR, - dogs, - dogsLoading, - onCreateDog, - onUpdateDog, - onDeleteDog, - onPickUpDogOrders, -}: DirectiveDetailProps) { - const [activeTab, setActiveTab] = useState<"steps" | "dogs">("steps"); - const [editingGoal, setEditingGoal] = useState(false); - const [goalText, setGoalText] = useState(directive.goal); - const [visibleTaskIds, setVisibleTaskIds] = useState | null>(null); - const [pickingUpOrders, setPickingUpOrders] = useState(false); - const [pickUpResult, setPickUpResult] = useState(null); - const [creatingPR, setCreatingPR] = useState(false); - const [slideOutTaskId, setSlideOutTaskId] = useState(null); - - const handleViewTask = (taskId: string) => { - setSlideOutTaskId(taskId); - }; - - // Sync goalText and reset editing state when directive changes - useEffect(() => { - setGoalText(directive.goal); - setEditingGoal(false); - }, [directive.id, directive.goal]); - const [searchQuery, setSearchQuery] = useState(""); - const [isLogCollapsed, setIsLogCollapsed] = useState(true); - const prevHadRunningRef = useRef(false); - const badge = STATUS_BADGE[directive.status] || STATUS_BADGE.draft; - - const completedSteps = directive.steps.filter((s) => s.status === "completed").length; - const totalSteps = directive.steps.length; - const progress = totalSteps > 0 ? Math.round((completedSteps / totalSteps) * 100) : 0; - // Get pending questions for this directive's tasks - const { pendingQuestions, submitAnswer } = useSupervisorQuestions(); - const directiveTaskIds = useMemo(() => { - const ids = new Set(); - if (directive.orchestratorTaskId) ids.add(directive.orchestratorTaskId); - for (const step of directive.steps) { - if (step.taskId) ids.add(step.taskId); - } - return ids; - }, [directive.orchestratorTaskId, directive.steps]); - - const directiveQuestions = useMemo( - () => pendingQuestions.filter((q) => - q.directiveId === directive.id || directiveTaskIds.has(q.taskId) - ), - [pendingQuestions, directive.id, directiveTaskIds] - ); - - // Build task map from directive steps and orchestrator - // Derive a stable key from the actual task IDs to avoid recreating the map on every poll - const taskMapKey = useMemo(() => { - const parts: string[] = []; - if (directive.orchestratorTaskId) parts.push(`o:${directive.orchestratorTaskId}`); - for (const step of directive.steps) { - if (step.taskId) parts.push(`${step.id}:${step.taskId}`); - } - return parts.join(","); - }, [directive.orchestratorTaskId, directive.steps]); - - const taskMap = useMemo(() => { - const map = new Map(); - if (directive.orchestratorTaskId) { - map.set(directive.orchestratorTaskId, "Orchestrator"); - } - for (const step of directive.steps) { - if (step.taskId) { - map.set(step.taskId, step.name); - } - } - return map; - }, [taskMapKey]); // eslint-disable-line react-hooks/exhaustive-deps - - // Build specialized steps for DAG visualization - const specializedSteps = useMemo(() => { - const steps: SpecializedStep[] = []; - - if (directive.orchestratorTaskId) { - steps.push({ - id: `orchestrator-${directive.orchestratorTaskId}`, - name: taskMap.get(directive.orchestratorTaskId) || "Planning", - type: "orchestrator", - taskId: directive.orchestratorTaskId, - status: "running", - }); - } - - if (directive.completionTaskId) { - steps.push({ - id: `completion-${directive.completionTaskId}`, - name: directive.prUrl ? "Updating PR" : "Creating PR", - type: "completion", - taskId: directive.completionTaskId, - status: "running", - }); - } - - return steps; - }, [directive.orchestratorTaskId, directive.completionTaskId, directive.prUrl, taskMap]); - - // Subscribe to all task outputs - const { connected, entries, clearEntries } = useMultiTaskSubscription({ - taskMap, - enabled: taskMap.size > 0, - }); - - // Auto-expand log panel when tasks start running - const hasRunningTasks = directive.steps.some((s) => s.status === "running") || - !!directive.orchestratorTaskId; - - useEffect(() => { - if (hasRunningTasks && !prevHadRunningRef.current) { - setIsLogCollapsed(false); - } - prevHadRunningRef.current = hasRunningTasks; - }, [hasRunningTasks]); - - const handlePickUpOrders = async () => { - setPickingUpOrders(true); - setPickUpResult(null); - try { - const result = await onPickUpOrders(); - if (result) { - setPickUpResult(result.message); - setTimeout(() => setPickUpResult(null), 5000); - } - } catch (e) { - setPickUpResult(e instanceof Error ? e.message : "Failed to pick up orders"); - setTimeout(() => setPickUpResult(null), 5000); - } finally { - setPickingUpOrders(false); - } - }; - - const handleGoalSave = () => { - if (goalText.trim() && goalText !== directive.goal) { - onUpdateGoal(goalText.trim()); - } - setEditingGoal(false); - }; - - - // Find the task name for the slide-out panel - const slideOutTaskName = slideOutTaskId - ? (directive.steps.find((s) => s.taskId === slideOutTaskId)?.name ?? - taskMap.get(slideOutTaskId) ?? - undefined) - : undefined; - - return ( - <> -
- {/* Header */} -
-
-

- {directive.title} -

-
- - {badge.label} - - -
-
- - {/* Progress bar */} - {totalSteps > 0 && ( -
-
-
-
- - {completedSteps}/{totalSteps} steps - -
- )} - - {/* Repo info */} - {(directive.repositoryUrl || directive.localPath) && ( -
- {directive.repositoryUrl || directive.localPath} - {directive.baseBranch && ` @ ${directive.baseBranch}`} -
- )} - - {/* Reconcile mode toggle */} -
-
- {(["auto", "semi-auto", "manual"] as const).map((mode) => { - const isActive = directive.reconcileMode === mode; - const modeStyles: Record = { - auto: isActive ? "text-[#9bc3ff] bg-[#1a2540]" : "text-[#445566] hover:text-[#7788aa]", - "semi-auto": isActive ? "text-amber-400 bg-amber-900/20" : "text-[#445566] hover:text-[#7788aa]", - manual: isActive ? "text-orange-400 bg-orange-900/20" : "text-[#445566] hover:text-[#7788aa]", - }; - const labels: Record = { auto: "Auto", "semi-auto": "Semi", manual: "Manual" }; - return ( - - ); - })} -
- - {directive.reconcileMode === "auto" && "Questions timeout after 30s"} - {directive.reconcileMode === "semi-auto" && "Questions pause execution"} - {directive.reconcileMode === "manual" && "Tasks ask clarifying questions"} - -
- - {/* PR link */} - {directive.prUrl && ( -
- - - PR created - - - {directive.prUrl} - -
- )} - - {/* Pending Questions */} - {directiveQuestions.length > 0 && ( -
- {directiveQuestions.map((q) => ( - submitAnswer(q.questionId, response)} - /> - ))} -
- )} - - {/* Controls */} -
- {(directive.status === "draft" || directive.status === "paused") && ( - - )} - {directive.status === "active" && ( - <> - - - - )} - {directive.status === "idle" && ( -
- - All steps done. Update goal to add new work. - - - -
- )} - {completedSteps > 0 && !directive.completionTaskId && ( - - )} - - -
- - {pickUpResult && ( -
- {pickUpResult} -
- )} -
- - {/* Goal */} -
-
- - Goal - - {!editingGoal && ( - - )} -
- {editingGoal ? ( -
-