From 9aadbc7958d39d181c0dd0600e2b7c30bb6c391a Mon Sep 17 00:00:00 2001 From: soryu Date: Sat, 14 Feb 2026 21:29:26 +0000 Subject: Makima system improvements: Orders, directive questions, PR creation fix, bug fixes (#62) * feat: soryu-co/soryu - makima: Fix directive goal update bug - stale closure issue * WIP: heartbeat checkpoint * WIP: heartbeat checkpoint * feat: soryu-co/soryu - makima: Create Orders database schema and backend API * feat: soryu-co/soryu - makima: Fix task Claude instance not receiving user inputs from input box * WIP: heartbeat checkpoint * feat: soryu-co/soryu - makima: Build Orders frontend page replacing the Board page * WIP: heartbeat checkpoint * WIP: heartbeat checkpoint * feat: soryu-co/soryu - makima: Fix directive PR creation system --- makima/frontend/src/components/NavStrip.tsx | 2 +- .../src/components/directives/DirectiveDetail.tsx | 139 +++++- makima/frontend/src/components/mesh/TaskOutput.tsx | 5 +- .../frontend/src/components/orders/OrderDetail.tsx | 530 +++++++++++++++++++++ .../frontend/src/components/orders/OrderList.tsx | 188 ++++++++ .../src/components/workflow/PhaseColumn.tsx | 126 ----- .../src/components/workflow/WorkflowBoard.tsx | 98 ---- .../components/workflow/WorkflowContractCard.tsx | 76 --- makima/frontend/src/hooks/useOrders.ts | 123 +++++ makima/frontend/src/lib/api.ts | 145 ++++++ makima/frontend/src/main.tsx | 14 +- makima/frontend/src/routes/directives.tsx | 3 +- makima/frontend/src/routes/mesh.tsx | 2 +- makima/frontend/src/routes/orders.tsx | 238 +++++++++ makima/frontend/src/routes/workflow.tsx | 250 ---------- makima/frontend/tsconfig.tsbuildinfo | 2 +- makima/migrations/20260214000000_create_orders.sql | 38 ++ .../20260214100000_directive_reconcile_mode.sql | 4 + makima/src/daemon/skills/directive.md | 6 + makima/src/daemon/task/manager.rs | 23 +- makima/src/db/models.rs | 125 +++++ makima/src/db/repository.rs | 316 +++++++++++- makima/src/orchestration/directive.rs | 88 +++- makima/src/server/handlers/mesh.rs | 10 +- makima/src/server/handlers/mesh_supervisor.rs | 90 +++- makima/src/server/handlers/mod.rs | 1 + makima/src/server/handlers/orders.rs | 443 +++++++++++++++++ makima/src/server/mod.rs | 16 +- makima/src/server/openapi.rs | 33 +- makima/src/server/state.rs | 30 +- 30 files changed, 2558 insertions(+), 606 deletions(-) create mode 100644 makima/frontend/src/components/orders/OrderDetail.tsx create mode 100644 makima/frontend/src/components/orders/OrderList.tsx delete mode 100644 makima/frontend/src/components/workflow/PhaseColumn.tsx delete mode 100644 makima/frontend/src/components/workflow/WorkflowBoard.tsx delete mode 100644 makima/frontend/src/components/workflow/WorkflowContractCard.tsx create mode 100644 makima/frontend/src/hooks/useOrders.ts create mode 100644 makima/frontend/src/routes/orders.tsx delete mode 100644 makima/frontend/src/routes/workflow.tsx create mode 100644 makima/migrations/20260214000000_create_orders.sql create mode 100644 makima/migrations/20260214100000_directive_reconcile_mode.sql create mode 100644 makima/src/server/handlers/orders.rs (limited to 'makima') diff --git a/makima/frontend/src/components/NavStrip.tsx b/makima/frontend/src/components/NavStrip.tsx index 9bb7777..5aba6a3 100644 --- a/makima/frontend/src/components/NavStrip.tsx +++ b/makima/frontend/src/components/NavStrip.tsx @@ -12,7 +12,7 @@ const NAV_LINKS: NavLink[] = [ { label: "Listen", href: "/listen" }, { label: "Directives", href: "/directives", requiresAuth: true }, { label: "Contracts", href: "/contracts", requiresAuth: true }, - { label: "Board", href: "/workflow", requiresAuth: true }, + { label: "Orders", href: "/orders", requiresAuth: true }, { label: "Mesh", href: "/mesh", requiresAuth: true }, { label: "History", href: "/history", requiresAuth: true }, ]; diff --git a/makima/frontend/src/components/directives/DirectiveDetail.tsx b/makima/frontend/src/components/directives/DirectiveDetail.tsx index b73463d..e278939 100644 --- a/makima/frontend/src/components/directives/DirectiveDetail.tsx +++ b/makima/frontend/src/components/directives/DirectiveDetail.tsx @@ -1,8 +1,9 @@ import { useState, useMemo, useEffect, useRef } from "react"; -import type { DirectiveWithSteps, DirectiveStatus } from "../../lib/api"; +import type { DirectiveWithSteps, DirectiveStatus, UpdateDirectiveRequest } from "../../lib/api"; import { DirectiveDAG } from "./DirectiveDAG"; import { DirectiveLogStream } from "./DirectiveLogStream"; import { useMultiTaskSubscription } from "../../hooks/useMultiTaskSubscription"; +import { useSupervisorQuestions } from "../../contexts/SupervisorQuestionsContext"; const STATUS_BADGE: Record = { draft: { color: "text-[#7788aa] border-[#2a3a5a]", label: "DRAFT" }, @@ -21,6 +22,7 @@ interface DirectiveDetailProps { onFailStep: (stepId: string) => void; onSkipStep: (stepId: string) => void; onUpdateGoal: (goal: string) => void; + onUpdate: (req: UpdateDirectiveRequest) => void; onDelete: () => void; onRefresh: () => void; onCleanupTasks: () => void; @@ -35,6 +37,7 @@ export function DirectiveDetail({ onFailStep, onSkipStep, onUpdateGoal, + onUpdate, onDelete, onRefresh, onCleanupTasks, @@ -42,6 +45,12 @@ export function DirectiveDetail({ const [editingGoal, setEditingGoal] = useState(false); const [goalText, setGoalText] = useState(directive.goal); const [visibleTaskIds, setVisibleTaskIds] = useState | null>(null); + + // 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); @@ -53,6 +62,24 @@ export function DirectiveDetail({ const terminalStatuses = new Set(["completed", "failed", "skipped"]); const hasTerminalTasks = directive.steps.some((s) => s.taskId && terminalStatuses.has(s.status)); + // 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(() => { @@ -149,6 +176,26 @@ export function DirectiveDetail({ )} + {/* Reconcile mode toggle */} +
+ + + {directive.reconcileMode + ? "Questions pause execution" + : "Questions timeout after 30s"} + +
+ {/* Orchestrator planning indicator */} {directive.orchestratorTaskId && (
@@ -199,6 +246,20 @@ export function DirectiveDetail({
)} + {/* Pending Questions */} + {directiveQuestions.length > 0 && ( +
+ {directiveQuestions.map((q) => ( + submitAnswer(q.questionId, response)} + /> + ))} +
+ )} + {/* Controls */}
{(directive.status === "draft" || directive.status === "paused") && ( @@ -235,7 +296,7 @@ export function DirectiveDetail({
); } + +/** Inline question card for directive pending questions */ +function DirectiveQuestionCard({ + question, + taskName, + onAnswer, +}: { + question: { questionId: string; question: string; choices: string[]; context: string | null }; + taskName: string; + onAnswer: (response: string) => void; +}) { + const [customResponse, setCustomResponse] = useState(""); + const [submitting, setSubmitting] = useState(false); + + const handleSubmit = async (response: string) => { + setSubmitting(true); + await onAnswer(response); + setSubmitting(false); + }; + + return ( +
+
+ + + Question from {taskName} + +
+

{question.question}

+ {question.context && ( +

{question.context}

+ )} + {question.choices.length > 0 ? ( +
+ {question.choices.map((choice) => ( + + ))} +
+ ) : ( +
+ setCustomResponse(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter" && customResponse.trim()) { + handleSubmit(customResponse.trim()); + } + }} + placeholder="Type your answer..." + className="flex-1 bg-[#0a0618] border border-purple-900/50 rounded px-2 py-0.5 text-[10px] font-mono text-white placeholder:text-[#445566]" + disabled={submitting} + /> + +
+ )} +
+ ); +} diff --git a/makima/frontend/src/components/mesh/TaskOutput.tsx b/makima/frontend/src/components/mesh/TaskOutput.tsx index f49c366..2db4250 100644 --- a/makima/frontend/src/components/mesh/TaskOutput.tsx +++ b/makima/frontend/src/components/mesh/TaskOutput.tsx @@ -77,7 +77,10 @@ export function TaskOutput({ setInputValue(""); inputRef.current?.focus(); } catch (err) { - setInputError(err instanceof Error ? err.message : "Failed to send input"); + const errorMsg = err instanceof Error ? err.message : "Failed to send input"; + setInputError(errorMsg); + // Auto-dismiss error after 5 seconds + setTimeout(() => setInputError(null), 5000); } finally { setSendingInput(false); } diff --git a/makima/frontend/src/components/orders/OrderDetail.tsx b/makima/frontend/src/components/orders/OrderDetail.tsx new file mode 100644 index 0000000..7f8a95d --- /dev/null +++ b/makima/frontend/src/components/orders/OrderDetail.tsx @@ -0,0 +1,530 @@ +import { useState } from "react"; +import type { + Order, + OrderStatus, + OrderPriority, + OrderType, + UpdateOrderRequest, + DirectiveSummary, +} from "../../lib/api"; + +const STATUS_BADGE: Record = { + open: { color: "text-[#75aafc] border-[rgba(117,170,252,0.4)]", label: "OPEN" }, + in_progress: { color: "text-yellow-400 border-yellow-800", label: "IN PROGRESS" }, + done: { color: "text-emerald-400 border-emerald-800", label: "DONE" }, + archived: { color: "text-[#556677] border-[#2a3a5a]", label: "ARCHIVED" }, +}; + +const PRIORITY_OPTIONS: { value: OrderPriority; color: string; label: string }[] = [ + { value: "critical", color: "text-red-400 border-red-800", label: "Critical" }, + { value: "high", color: "text-orange-400 border-orange-800", label: "High" }, + { value: "medium", color: "text-yellow-400 border-yellow-800", label: "Medium" }, + { value: "low", color: "text-[#75aafc] border-[rgba(117,170,252,0.4)]", label: "Low" }, + { value: "none", color: "text-[#556677] border-[#2a3a5a]", label: "None" }, +]; + +const TYPE_OPTIONS: { value: OrderType; color: string; label: string }[] = [ + { value: "feature", color: "text-[#75aafc]", label: "Feature" }, + { value: "bug", color: "text-red-400", label: "Bug" }, + { value: "spike", color: "text-yellow-400", label: "Spike" }, + { value: "chore", color: "text-[#7788aa]", label: "Chore" }, + { value: "improvement", color: "text-emerald-400", label: "Improvement" }, +]; + +const STATUS_OPTIONS: OrderStatus[] = ["open", "in_progress", "done", "archived"]; + +interface OrderDetailProps { + order: Order; + directives: DirectiveSummary[]; + onUpdate: (req: UpdateOrderRequest) => Promise; + onDelete: () => void; + onLinkDirective: (directiveId: string) => Promise; + onLinkContract: (contractId: string) => Promise; + onConvertToStep: (directiveId: string) => Promise; + onRefresh: () => void; +} + +export function OrderDetail({ + order, + directives, + onUpdate, + onDelete, + onLinkDirective, + onLinkContract, + onConvertToStep, + onRefresh, +}: OrderDetailProps) { + const [editingTitle, setEditingTitle] = useState(false); + const [titleText, setTitleText] = useState(order.title); + const [editingDesc, setEditingDesc] = useState(false); + const [descText, setDescText] = useState(order.description || ""); + const [editingLabels, setEditingLabels] = useState(false); + const [labelsText, setLabelsText] = useState(order.labels.join(", ")); + const [showLinkDirective, setShowLinkDirective] = useState(false); + const [showLinkContract, setShowLinkContract] = useState(false); + const [contractIdInput, setContractIdInput] = useState(""); + const [showConvertToStep, setShowConvertToStep] = useState(false); + + const badge = STATUS_BADGE[order.status] || STATUS_BADGE.open; + const currentPriority = PRIORITY_OPTIONS.find((p) => p.value === order.priority) || PRIORITY_OPTIONS[4]; + const currentType = TYPE_OPTIONS.find((t) => t.value === order.orderType) || TYPE_OPTIONS[0]; + + const handleTitleSave = async () => { + if (titleText.trim() && titleText !== order.title) { + await onUpdate({ title: titleText.trim() }); + } + setEditingTitle(false); + }; + + const handleDescSave = async () => { + const newDesc = descText.trim() || null; + if (newDesc !== order.description) { + await onUpdate({ description: newDesc }); + } + setEditingDesc(false); + }; + + const handleLabelsSave = async () => { + const newLabels = labelsText + .split(",") + .map((l) => l.trim()) + .filter((l) => l.length > 0); + await onUpdate({ labels: newLabels }); + setEditingLabels(false); + }; + + const handleStatusChange = async (status: OrderStatus) => { + await onUpdate({ status }); + }; + + const handlePriorityChange = async (priority: OrderPriority) => { + await onUpdate({ priority }); + }; + + const handleTypeChange = async (orderType: OrderType) => { + await onUpdate({ orderType }); + }; + + const handleLinkDirective = async (directiveId: string) => { + await onLinkDirective(directiveId); + setShowLinkDirective(false); + }; + + const handleLinkContract = async () => { + if (!contractIdInput.trim()) return; + await onLinkContract(contractIdInput.trim()); + setContractIdInput(""); + setShowLinkContract(false); + }; + + const handleConvertToStep = async (directiveId: string) => { + await onConvertToStep(directiveId); + setShowConvertToStep(false); + }; + + return ( +
+ {/* Header */} +
+
+ {editingTitle ? ( +
+ setTitleText(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") handleTitleSave(); + if (e.key === "Escape") setEditingTitle(false); + }} + autoFocus + className="flex-1 bg-[#0a1628] border border-[rgba(117,170,252,0.2)] rounded px-2 py-1 text-[14px] font-mono text-white" + /> + + +
+ ) : ( +

{ + setTitleText(order.title); + setEditingTitle(true); + }} + > + {order.title} +

+ )} +
+ + {badge.label} + + +
+
+ + {/* Type + Priority inline */} +
+ + {currentType.label} + + / + + {currentPriority.label} + +
+ + {/* Linked entities */} + {order.directiveId && ( + + )} + {order.contractId && ( + + )} + {order.directiveStepId && ( +
+ Step: {order.directiveStepId.slice(0, 8)}... +
+ )} + + {/* Controls */} +
+ +
+
+ + {/* Status selector */} +
+
+ + Status + +
+
+ {STATUS_OPTIONS.map((s) => { + const sBadge = STATUS_BADGE[s]; + return ( + + ); + })} +
+
+ + {/* Priority selector */} +
+
+ + Priority + +
+
+ {PRIORITY_OPTIONS.map((p) => ( + + ))} +
+
+ + {/* Type selector */} +
+
+ + Type + +
+
+ {TYPE_OPTIONS.map((t) => ( + + ))} +
+
+ + {/* Description */} +
+
+ + Description + + {!editingDesc && ( + + )} +
+ {editingDesc ? ( +
+