diff options
| author | soryu <soryu@soryu.co> | 2026-02-14 02:44:57 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-02-14 02:44:57 +0000 |
| commit | 41a24585e8c771132b47b28ab1b5869040744af0 (patch) | |
| tree | 8c73692520977dced7bfa6e4d24c937c83523c46 | |
| parent | 50ba4f81aff469b79868136e5b07dfb30f9782e6 (diff) | |
| download | soryu-41a24585e8c771132b47b28ab1b5869040744af0.tar.gz soryu-41a24585e8c771132b47b28ab1b5869040744af0.zip | |
WIP: heartbeat checkpointmakima/soryu-co-soryu---makima--add-question-support-for--525448aa
| -rw-r--r-- | makima/frontend/src/components/directives/DirectiveDetail.tsx | 131 | ||||
| -rw-r--r-- | makima/frontend/src/lib/api.ts | 10 | ||||
| -rw-r--r-- | makima/frontend/src/routes/directives.tsx | 3 | ||||
| -rw-r--r-- | makima/src/orchestration/directive.rs | 10 | ||||
| -rw-r--r-- | makima/src/server/handlers/mesh_supervisor.rs | 11 |
5 files changed, 159 insertions, 6 deletions
diff --git a/makima/frontend/src/components/directives/DirectiveDetail.tsx b/makima/frontend/src/components/directives/DirectiveDetail.tsx index b73463d..5128c26 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<DirectiveStatus, { color: string; label: string }> = { 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, @@ -53,6 +56,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<string>(); + 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 +170,26 @@ export function DirectiveDetail({ </div> )} + {/* Reconcile mode toggle */} + <div className="flex items-center gap-2 mb-2"> + <button + type="button" + onClick={() => onUpdate({ reconcileMode: !directive.reconcileMode })} + className={`text-[10px] font-mono border rounded px-2 py-0.5 transition-colors ${ + directive.reconcileMode + ? "text-amber-400 border-amber-800 bg-amber-900/20" + : "text-[#556677] border-[#2a3a5a] hover:text-[#7788aa]" + }`} + > + {directive.reconcileMode ? "Reconcile: ON" : "Reconcile: OFF"} + </button> + <span className="text-[9px] font-mono text-[#445566]"> + {directive.reconcileMode + ? "Questions pause execution" + : "Questions timeout after 30s"} + </span> + </div> + {/* Orchestrator planning indicator */} {directive.orchestratorTaskId && ( <div className="flex items-center gap-2 mb-2 px-2 py-1.5 bg-[#1a1a30] border border-[rgba(117,170,252,0.2)] rounded"> @@ -199,6 +240,20 @@ export function DirectiveDetail({ </div> )} + {/* Pending Questions */} + {directiveQuestions.length > 0 && ( + <div className="mb-2 space-y-2"> + {directiveQuestions.map((q) => ( + <DirectiveQuestionCard + key={q.questionId} + question={q} + taskName={taskMap.get(q.taskId) || "Task"} + onAnswer={(response) => submitAnswer(q.questionId, response)} + /> + ))} + </div> + )} + {/* Controls */} <div className="flex flex-wrap gap-2"> {(directive.status === "draft" || directive.status === "paused") && ( @@ -342,3 +397,77 @@ export function DirectiveDetail({ </div> ); } + +/** 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 ( + <div className="px-2 py-2 bg-[#1a1020] border border-purple-900/50 rounded"> + <div className="flex items-center gap-1.5 mb-1"> + <span className="inline-block w-2 h-2 rounded-full bg-purple-400 animate-pulse" /> + <span className="text-[9px] font-mono text-purple-400 uppercase"> + Question from {taskName} + </span> + </div> + <p className="text-[11px] font-mono text-white mb-1.5">{question.question}</p> + {question.context && ( + <p className="text-[9px] font-mono text-[#556677] mb-1.5">{question.context}</p> + )} + {question.choices.length > 0 ? ( + <div className="flex flex-wrap gap-1"> + {question.choices.map((choice) => ( + <button + key={choice} + type="button" + disabled={submitting} + onClick={() => handleSubmit(choice)} + className="text-[10px] font-mono text-purple-300 hover:text-white border border-purple-800 hover:border-purple-600 rounded px-2 py-0.5 disabled:opacity-50" + > + {choice} + </button> + ))} + </div> + ) : ( + <div className="flex gap-1"> + <input + type="text" + value={customResponse} + onChange={(e) => 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} + /> + <button + type="button" + disabled={submitting || !customResponse.trim()} + onClick={() => handleSubmit(customResponse.trim())} + className="text-[10px] font-mono text-purple-300 hover:text-white border border-purple-800 rounded px-2 py-0.5 disabled:opacity-50" + > + Send + </button> + </div> + )} + </div> + ); +} diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts index 480041c..353faad 100644 --- a/makima/frontend/src/lib/api.ts +++ b/makima/frontend/src/lib/api.ts @@ -2241,6 +2241,8 @@ export interface PendingQuestion { questionId: string; taskId: string; contractId: string; + /** Directive this question relates to (if from a directive task) */ + directiveId?: string | null; question: string; choices: string[]; context: string | null; @@ -3025,6 +3027,8 @@ export interface Directive { completionTaskId: string | null; /** Whether the memory system is enabled for this directive */ memoryEnabled: boolean; + /** Whether questions pause execution indefinitely until answered */ + reconcileMode: boolean; goalUpdatedAt: string; startedAt: string | null; version: number; @@ -3064,6 +3068,8 @@ export interface DirectiveSummary { completionTaskId: string | null; /** Whether the memory system is enabled for this directive */ memoryEnabled: boolean; + /** Whether questions pause execution indefinitely until answered */ + reconcileMode: boolean; version: number; createdAt: string; updatedAt: string; @@ -3086,6 +3092,8 @@ export interface CreateDirectiveRequest { baseBranch?: string; /** Enable the memory system for this directive (default: false) */ memoryEnabled?: boolean; + /** Whether questions pause execution indefinitely until answered (default: false) */ + reconcileMode?: boolean; } export interface UpdateDirectiveRequest { @@ -3098,6 +3106,8 @@ export interface UpdateDirectiveRequest { orchestratorTaskId?: string; /** Enable or disable the memory system for this directive */ memoryEnabled?: boolean; + /** Whether questions pause execution indefinitely until answered */ + reconcileMode?: boolean; version?: number; } diff --git a/makima/frontend/src/routes/directives.tsx b/makima/frontend/src/routes/directives.tsx index ca4437c..643cfee 100644 --- a/makima/frontend/src/routes/directives.tsx +++ b/makima/frontend/src/routes/directives.tsx @@ -12,7 +12,7 @@ export default function DirectivesPage() { const navigate = useNavigate(); const { id: selectedId } = useParams<{ id: string }>(); const { directives, loading: listLoading, create, remove } = useDirectives(); - const { directive, refresh: refreshDetail, start, pause, advance, completeStep, failStep, skipStep, updateGoal, cleanupTasks } = useDirective(selectedId); + const { directive, refresh: refreshDetail, update, start, pause, advance, completeStep, failStep, skipStep, updateGoal, cleanupTasks } = useDirective(selectedId); const [showCreate, setShowCreate] = useState(false); const [newTitle, setNewTitle] = useState(""); @@ -207,6 +207,7 @@ export default function DirectivesPage() { onFailStep={failStep} onSkipStep={skipStep} onUpdateGoal={updateGoal} + onUpdate={update} onDelete={handleDelete} onRefresh={refreshDetail} onCleanupTasks={cleanupTasks} diff --git a/makima/src/orchestration/directive.rs b/makima/src/orchestration/directive.rs index ea8009d..6d5d63d 100644 --- a/makima/src/orchestration/directive.rs +++ b/makima/src/orchestration/directive.rs @@ -248,6 +248,16 @@ impl DirectiveOrchestrator { .await?; repository::check_directive_idle(&self.pool, step.directive_id).await?; } + "paused" => { + // Task is paused (e.g., waiting for user answer in reconcile mode) + // Keep step in running status — task will auto-resume when answered + tracing::debug!( + step_id = %step.step_id, + directive_id = %step.directive_id, + task_id = %step.task_id, + "Step task paused (waiting for user response) — keeping step running" + ); + } _ => { // Still running — do nothing } diff --git a/makima/src/server/handlers/mesh_supervisor.rs b/makima/src/server/handlers/mesh_supervisor.rs index 7d4ab46..90c6dc7 100644 --- a/makima/src/server/handlers/mesh_supervisor.rs +++ b/makima/src/server/handlers/mesh_supervisor.rs @@ -260,11 +260,11 @@ async fn verify_supervisor_auth( ) })?; - // Verify task is a supervisor - if !task.is_supervisor { + // Verify task is a supervisor or a directive task + if !task.is_supervisor && task.directive_id.is_none() { return Err(( StatusCode::FORBIDDEN, - Json(ApiError::new("NOT_SUPERVISOR", "Only supervisor tasks can use these endpoints")), + Json(ApiError::new("NOT_SUPERVISOR", "Only supervisor or directive tasks can use these endpoints")), )); } @@ -1879,7 +1879,10 @@ pub async fn ask_question( state.remove_pending_question(question_id); // Clear pending question from supervisor state on timeout (Task 3.3) - clear_pending_question(pool, contract_id, question_id).await; + // Skip for directive context — no supervisor_states for directives + if let Some(cid) = contract_id { + clear_pending_question(pool, cid, question_id).await; + } return ( StatusCode::REQUEST_TIMEOUT, |
