From d670dcb72984cfa483063d161bb468704038895c Mon Sep 17 00:00:00 2001 From: soryu Date: Sat, 21 Feb 2026 19:33:44 +0000 Subject: feat: add directive ask command, log backfill & specialized DAG steps (#75) * feat: soryu-co/soryu - makima: Add makima directive ask CLI command * feat: soryu-co/soryu - makima: Update directive skill docs and planning prompt to support asking questions * feat: soryu-co/soryu - makima: Add log stream backfill for directive tasks * feat: soryu-co/soryu - makima: Update planning prompts to inform tasks they can ask questions * WIP: heartbeat checkpoint * feat: soryu-co/soryu - makima: Add ask command to directive SKILL.md documentation * feat: soryu-co/soryu - makima: Add log stream backfill for directive task output history * feat: soryu-co/soryu - makima: Update planning prompt to tell planning tasks they can ask questions * WIP: heartbeat checkpoint * feat: soryu-co/soryu - makima: Show Planning, PR, and Cleanup tasks as specialized steps in DAG --- .../src/components/directives/DirectiveDAG.tsx | 114 +++++++++++++-- .../src/components/directives/DirectiveDetail.tsx | 91 ++++++++---- .../components/directives/OrchestratorStepNode.tsx | 161 +++++++++++++++++++++ .../frontend/src/hooks/useMultiTaskSubscription.ts | 153 +++++++++++++++++++- makima/src/bin/makima.rs | 12 ++ makima/src/daemon/cli/directive.rs | 39 +++++ makima/src/daemon/cli/mod.rs | 3 + makima/src/daemon/skills/directive.md | 27 ++++ makima/src/orchestration/directive.rs | 64 +++++++- 9 files changed, 617 insertions(+), 47 deletions(-) create mode 100644 makima/frontend/src/components/directives/OrchestratorStepNode.tsx diff --git a/makima/frontend/src/components/directives/DirectiveDAG.tsx b/makima/frontend/src/components/directives/DirectiveDAG.tsx index 27a80ac..8c7def9 100644 --- a/makima/frontend/src/components/directives/DirectiveDAG.tsx +++ b/makima/frontend/src/components/directives/DirectiveDAG.tsx @@ -1,9 +1,31 @@ import { useMemo } from "react"; import type { DirectiveStep } from "../../lib/api"; import { StepNode } from "./StepNode"; +import { + OrchestratorStepNode, + type OrchestratorStepType, + type OrchestratorStepStatus, +} from "./OrchestratorStepNode"; + +export interface VirtualStep { + type: OrchestratorStepType; + taskId: string; + status: OrchestratorStepStatus; + label: string; + hasQuestions?: boolean; +} + +export interface SpecializedStep { + id: string; + name: string; + type: "orchestrator" | "completion"; + taskId: string; + status: "running" | "completed"; +} interface DirectiveDAGProps { steps: DirectiveStep[]; + specializedSteps?: SpecializedStep[]; onComplete?: (stepId: string) => void; onFail?: (stepId: string) => void; onSkip?: (stepId: string) => void; @@ -13,6 +35,13 @@ interface Layer { steps: DirectiveStep[]; } +/** Types that should appear before the regular DAG steps */ +const BEFORE_TYPES = new Set([ + "planning", + "replanning", + "plan-orders", +]); + function topoSort(steps: DirectiveStep[]): Layer[] { if (steps.length === 0) return []; @@ -31,10 +60,13 @@ function topoSort(steps: DirectiveStep[]): Layer[] { })); } -export function DirectiveDAG({ steps, onComplete, onFail, onSkip }: DirectiveDAGProps) { +export function DirectiveDAG({ steps, specializedSteps, onComplete, onFail, onSkip }: DirectiveDAGProps) { const layers = useMemo(() => topoSort(steps), [steps]); - if (steps.length === 0) { + const orchestratorSteps = specializedSteps?.filter(s => s.type === "orchestrator") ?? []; + const completionSteps = specializedSteps?.filter(s => s.type === "completion") ?? []; + + if (steps.length === 0 && orchestratorSteps.length === 0 && completionSteps.length === 0) { return (
No steps yet. Add steps to build the DAG. @@ -44,6 +76,19 @@ export function DirectiveDAG({ steps, onComplete, onFail, onSkip }: DirectiveDAG return (
+ {/* Orchestrator steps (Planning/Cleanup/Orders) - rendered above regular steps */} + {orchestratorSteps.map(step => ( + + ))} + + {/* Connector line if both orchestrator step and regular steps exist */} + {orchestratorSteps.length > 0 && layers.length > 0 && ( +
+
+
+ )} + + {/* Regular step layers */} {layers.map((layer, layerIdx) => (
{layerIdx > 0 && ( @@ -52,18 +97,69 @@ export function DirectiveDAG({ steps, onComplete, onFail, onSkip }: DirectiveDAG
)}
- {layer.steps.map((step) => ( - onComplete(step.id) : undefined} - onFail={onFail ? () => onFail(step.id) : undefined} - onSkip={onSkip ? () => onSkip(step.id) : undefined} + {afterSteps.map((vs) => ( + ))}
))} + + {/* Connector line if both regular steps and completion step exist */} + {completionSteps.length > 0 && layers.length > 0 && ( +
+
+
+ )} + + {/* Completion steps (PR creation) - rendered below regular steps */} + {completionSteps.map(step => ( + + ))} +
+ ); +} + +function SpecializedStepNode({ step }: { step: SpecializedStep }) { + const themeColors = step.type === "orchestrator" + ? { + bg: "bg-[#1a1a30]", + border: "border-[rgba(117,170,252,0.3)]", + text: "text-[#75aafc]", + dot: "bg-[#75aafc]", + label: step.name.startsWith("Cleanup") ? "CLEANUP" + : step.name.startsWith("Pick up") ? "ORDERS" + : "PLANNING", + } + : { + bg: "bg-[#1a1a10]", + border: "border-yellow-900/50", + text: "text-yellow-400", + dot: "bg-yellow-400", + label: "PR", + }; + + return ( +
+ + + {themeColors.label} + + + {step.name} + + + View task +
); } diff --git a/makima/frontend/src/components/directives/DirectiveDetail.tsx b/makima/frontend/src/components/directives/DirectiveDetail.tsx index 98940d0..171654d 100644 --- a/makima/frontend/src/components/directives/DirectiveDetail.tsx +++ b/makima/frontend/src/components/directives/DirectiveDetail.tsx @@ -1,6 +1,7 @@ import { useState, useMemo, useEffect, useRef } from "react"; import type { DirectiveWithSteps, DirectiveStatus, UpdateDirectiveRequest } from "../../lib/api"; import { DirectiveDAG } from "./DirectiveDAG"; +import type { SpecializedStep } from "./DirectiveDAG"; import { DirectiveLogStream } from "./DirectiveLogStream"; import { useMultiTaskSubscription } from "../../hooks/useMultiTaskSubscription"; import { useSupervisorQuestions } from "../../contexts/SupervisorQuestionsContext"; @@ -108,6 +109,33 @@ export function DirectiveDetail({ 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, @@ -149,6 +177,36 @@ export function DirectiveDetail({ setEditingGoal(false); }; + // Build virtual steps for orchestrator tasks to display in the DAG + const virtualSteps = useMemo(() => { + const steps: VirtualStep[] = []; + if (directive.orchestratorTaskId) { + const hasOrchestratorQuestions = directiveQuestions.some( + (q) => q.taskId === directive.orchestratorTaskId + ); + steps.push({ + type: "planning", + taskId: directive.orchestratorTaskId, + status: "running", + label: "Planning", + hasQuestions: hasOrchestratorQuestions, + }); + } + if (directive.completionTaskId) { + const hasCompletionQuestions = directiveQuestions.some( + (q) => q.taskId === directive.completionTaskId + ); + steps.push({ + type: directive.prUrl ? "pr-update" : "pr", + taskId: directive.completionTaskId, + status: "running", + label: directive.prUrl ? "Updating PR" : "Creating PR", + hasQuestions: hasCompletionQuestions, + }); + } + return steps; + }, [directive.orchestratorTaskId, directive.completionTaskId, directive.prUrl, directiveQuestions]); + return (
{/* Header */} @@ -217,22 +275,6 @@ export function DirectiveDetail({
- {/* Orchestrator planning indicator */} - {directive.orchestratorTaskId && ( -
- - - Planning in progress... - - - View task - -
- )} - {/* PR link */} {directive.prUrl && (
@@ -251,22 +293,6 @@ export function DirectiveDetail({
)} - {/* Completion task indicator */} - {directive.completionTaskId && ( -
- - - {directive.prUrl ? "Updating PR..." : "Creating PR..."} - - - View task - -
- )} - {/* Pending Questions */} {directiveQuestions.length > 0 && (
@@ -423,6 +449,7 @@ export function DirectiveDetail({ = { + planning: { + accent: "#75aafc", + bg: "bg-[#0d1a30]", + border: "border-[#75aafc]", + text: "text-[#75aafc]", + dot: "bg-[#75aafc]", + }, + replanning: { + accent: "#75aafc", + bg: "bg-[#0d1a30]", + border: "border-[#75aafc]", + text: "text-[#75aafc]", + dot: "bg-[#75aafc]", + }, + "plan-orders": { + accent: "#c084fc", + bg: "bg-[#1a0d30]", + border: "border-[#c084fc]", + text: "text-[#c084fc]", + dot: "bg-[#c084fc]", + }, + pr: { + accent: "#34d399", + bg: "bg-[#0a1a14]", + border: "border-[#34d399]", + text: "text-[#34d399]", + dot: "bg-[#34d399]", + }, + "pr-update": { + accent: "#34d399", + bg: "bg-[#0a1a14]", + border: "border-[#34d399]", + text: "text-[#34d399]", + dot: "bg-[#34d399]", + }, + cleanup: { + accent: "#7788aa", + bg: "bg-[#141a24]", + border: "border-[#7788aa]", + text: "text-[#7788aa]", + dot: "bg-[#7788aa]", + }, + verification: { + accent: "#7788aa", + bg: "bg-[#141a24]", + border: "border-[#7788aa]", + text: "text-[#7788aa]", + dot: "bg-[#7788aa]", + }, +}; + +const TYPE_LABELS: Record = { + planning: "PLANNING", + replanning: "REPLANNING", + "plan-orders": "PLAN ORDERS", + pr: "PR", + "pr-update": "PR UPDATE", + cleanup: "CLEANUP", + verification: "VERIFICATION", +}; + +const STATUS_LABELS: Record = { + pending: "PENDING", + running: "RUNNING", + completed: "DONE", + failed: "FAILED", +}; + +export function OrchestratorStepNode({ + type, + taskId, + status, + label, + hasQuestions, +}: OrchestratorStepNodeProps) { + const colors = TYPE_COLORS[type]; + const typeLabel = TYPE_LABELS[type]; + const statusLabel = STATUS_LABELS[status]; + + return ( +
+ {/* Type badge */} +
+
+ {/* Status dot */} + {status === "running" && ( + + )} + {status === "completed" && ( + + )} + {status === "failed" && ( + + )} + {status === "pending" && ( + + )} + + {typeLabel} + +
+
+ {hasQuestions && ( + + )} + + {statusLabel} + +
+
+ + {/* Label */} + + {label} + + + {/* Task link */} + + {status === "running" ? "View running task" : "View task"} + +
+ ); +} diff --git a/makima/frontend/src/hooks/useMultiTaskSubscription.ts b/makima/frontend/src/hooks/useMultiTaskSubscription.ts index 4303f1b..41489c7 100644 --- a/makima/frontend/src/hooks/useMultiTaskSubscription.ts +++ b/makima/frontend/src/hooks/useMultiTaskSubscription.ts @@ -1,5 +1,5 @@ import { useState, useCallback, useRef, useEffect, useMemo } from "react"; -import { TASK_SUBSCRIBE_ENDPOINT } from "../lib/api"; +import { TASK_SUBSCRIBE_ENDPOINT, getTaskOutput } from "../lib/api"; import type { TaskOutputEvent } from "./useTaskSubscription"; export interface MultiTaskOutputEntry extends TaskOutputEvent { @@ -7,6 +7,8 @@ export interface MultiTaskOutputEntry extends TaskOutputEvent { taskLabel: string; /** Timestamp when the entry was received */ receivedAt: number; + /** Whether this entry was backfilled from historical data (not live-streamed) */ + isBackfill?: boolean; } interface UseMultiTaskSubscriptionOptions { @@ -26,8 +28,11 @@ export function useMultiTaskSubscription(options: UseMultiTaskSubscriptionOption const wsRef = useRef(null); const reconnectTimeoutRef = useRef(null); const subscribedTasksRef = useRef>(new Set()); + const backfilledTasksRef = useRef>(new Set()); const taskMapRef = useRef(taskMap); const enabledRef = useRef(enabled); + /** Track which task IDs have already been backfilled to avoid re-fetching */ + const backfilledTasksRef = useRef>(new Set()); // Keep refs in sync useEffect(() => { @@ -38,6 +43,88 @@ export function useMultiTaskSubscription(options: UseMultiTaskSubscriptionOption enabledRef.current = enabled; }, [enabled]); + /** Max number of historical events to backfill per task */ + const MAX_BACKFILL_PER_TASK = 200; + + /** + * Convert a TaskEvent (from the REST API) into a MultiTaskOutputEntry. + * Only converts events with event_type === 'output'. + */ + const convertTaskEventToEntry = useCallback( + (event: TaskEvent): MultiTaskOutputEntry | null => { + if (event.eventType !== "output") return null; + const data = event.eventData; + if (!data) return null; + + return { + taskId: event.taskId, + messageType: (data.messageType as string) || "system", + content: (data.content as string) || "", + toolName: data.toolName as string | undefined, + toolInput: data.toolInput as Record | undefined, + isError: data.isError as boolean | undefined, + costUsd: data.costUsd as number | undefined, + durationMs: data.durationMs as number | undefined, + isPartial: false, + taskLabel: + taskMapRef.current.get(event.taskId) || event.taskId, + receivedAt: new Date(event.createdAt).getTime(), + isBackfill: true, + }; + }, + [] + ); + + /** + * Backfill historical log entries for a task from the REST API. + * Only fetches once per task ID (tracked in backfilledTasksRef). + */ + const backfillTask = useCallback( + async (taskId: string) => { + if (backfilledTasksRef.current.has(taskId)) return; + backfilledTasksRef.current.add(taskId); + + try { + const response = await listTaskEvents(taskId); + const events = response.events; + + // The API returns events in DESC order; reverse to get chronological ASC + const chronologicalEvents = [...events].reverse(); + + // Filter to output events and convert, limiting to MAX_BACKFILL_PER_TASK + const backfillEntries: MultiTaskOutputEntry[] = []; + for (const event of chronologicalEvents) { + const entry = convertTaskEventToEntry(event); + if (entry) { + backfillEntries.push(entry); + if (backfillEntries.length >= MAX_BACKFILL_PER_TASK) break; + } + } + + if (backfillEntries.length === 0) return; + + // Prepend historical entries before any existing live entries for this task, + // maintaining overall chronological order across all tasks + setEntries((prev) => { + // Merge backfill entries with existing entries, maintaining chronological order + const merged = [...backfillEntries, ...prev]; + // Sort by receivedAt to ensure proper chronological ordering + merged.sort((a, b) => a.receivedAt - b.receivedAt); + // Trim to maxEntries + if (merged.length > maxEntries) { + return merged.slice(merged.length - maxEntries); + } + return merged; + }); + } catch (e) { + console.error(`Failed to backfill task events for ${taskId}:`, e); + // Remove from backfilled set so it can be retried + backfilledTasksRef.current.delete(taskId); + } + }, + [convertTaskEventToEntry, maxEntries] + ); + // Derive task IDs from the map, stabilized to avoid unnecessary effect triggers const taskIdsKey = useMemo(() => Array.from(taskMap.keys()).sort().join(","), [taskMap]); const taskIds = useMemo(() => Array.from(taskMap.keys()), [taskIdsKey]); // eslint-disable-line react-hooks/exhaustive-deps @@ -56,6 +143,58 @@ export function useMultiTaskSubscription(options: UseMultiTaskSubscriptionOption } }, []); + const backfillTask = useCallback( + async (taskId: string, label: string) => { + if (backfilledTasksRef.current.has(taskId)) return; + backfilledTasksRef.current.add(taskId); + + try { + const response = await getTaskOutput(taskId); + if (response.entries.length === 0) return; + + const historicalEntries: MultiTaskOutputEntry[] = response.entries.map( + (entry) => ({ + taskId: entry.taskId, + messageType: entry.messageType, + content: entry.content, + toolName: entry.toolName, + toolInput: entry.toolInput, + isError: entry.isError, + costUsd: entry.costUsd, + durationMs: entry.durationMs, + isPartial: false, + taskLabel: label, + receivedAt: new Date(entry.createdAt || Date.now()).getTime(), + }) + ); + + setEntries((prev) => { + // De-duplicate by checking if content+taskId+messageType already exists + const existingKeys = new Set( + prev.map( + (e) => + `${e.taskId}:${e.messageType}:${e.content.slice(0, 100)}` + ) + ); + const newHistorical = historicalEntries.filter( + (e) => + !existingKeys.has( + `${e.taskId}:${e.messageType}:${e.content.slice(0, 100)}` + ) + ); + const combined = [...newHistorical, ...prev]; + if (combined.length > maxEntries) { + return combined.slice(combined.length - maxEntries); + } + return combined; + }); + } catch (e) { + console.error(`Failed to backfill task ${taskId}:`, e); + } + }, + [maxEntries] + ); + const connect = useCallback(() => { const currentState = wsRef.current?.readyState; if (currentState === WebSocket.OPEN || currentState === WebSocket.CONNECTING) { @@ -150,6 +289,11 @@ export function useMultiTaskSubscription(options: UseMultiTaskSubscriptionOption // Set desired subscriptions and connect subscribedTasksRef.current = newTaskIds; connect(); + // Backfill all initial tasks + for (const taskId of newTaskIds) { + const label = taskMapRef.current.get(taskId) || taskId; + backfillTask(taskId, label); + } return; } @@ -160,13 +304,15 @@ export function useMultiTaskSubscription(options: UseMultiTaskSubscriptionOption } } - // Subscribe to new tasks + // Subscribe to new tasks and backfill their history for (const newId of newTaskIds) { if (!subscribedTasksRef.current.has(newId)) { subscribeToTask(ws, newId); + const label = taskMapRef.current.get(newId) || newId; + backfillTask(newId, label); } } - }, [taskIds, enabled, connect, subscribeToTask, unsubscribeFromTask]); + }, [taskIds, enabled, connect, subscribeToTask, unsubscribeFromTask, backfillTask]); // Cleanup on unmount useEffect(() => { @@ -182,6 +328,7 @@ export function useMultiTaskSubscription(options: UseMultiTaskSubscriptionOption const clearEntries = useCallback(() => { setEntries([]); + backfilledTasksRef.current.clear(); }, []); return { diff --git a/makima/src/bin/makima.rs b/makima/src/bin/makima.rs index 070e28e..aaf5a08 100644 --- a/makima/src/bin/makima.rs +++ b/makima/src/bin/makima.rs @@ -800,6 +800,18 @@ async fn run_directive( .await?; println!("{}", serde_json::to_string(&result.0)?); } + DirectiveCommand::Ask(args) => { + let client = ApiClient::new(args.common.api_url.clone(), args.common.api_key.clone())?; + eprintln!("Asking user: {}...", args.question); + let choices = args + .choices + .map(|c| c.split(',').map(|s| s.trim().to_string()).collect()) + .unwrap_or_default(); + let result = client + .supervisor_ask(&args.question, choices, args.context, args.timeout, args.phaseguard, args.multi_select, args.non_blocking, args.question_type) + .await?; + println!("{}", serde_json::to_string(&result.0)?); + } } Ok(()) diff --git a/makima/src/daemon/cli/directive.rs b/makima/src/daemon/cli/directive.rs index 8a6a9f2..7c8451f 100644 --- a/makima/src/daemon/cli/directive.rs +++ b/makima/src/daemon/cli/directive.rs @@ -111,6 +111,45 @@ pub struct BatchAddStepsArgs { pub json: String, } +/// Arguments for ask command (ask user a question from directive context). +#[derive(Args, Debug)] +pub struct AskArgs { + #[command(flatten)] + pub common: DirectiveArgs, + + /// The question to ask + #[arg(index = 1)] + pub question: String, + + /// Optional choices (comma-separated) + #[arg(long)] + pub choices: Option, + + /// Context about what this relates to + #[arg(long)] + pub context: Option, + + /// Timeout in seconds (default: 3600 = 1 hour) + #[arg(long, default_value = "3600")] + pub timeout: i32, + + /// Block indefinitely until user responds (no timeout) + #[arg(long, default_value = "false")] + pub phaseguard: bool, + + /// Allow selecting multiple choices (response will be comma-separated) + #[arg(long, default_value = "false")] + pub multi_select: bool, + + /// Non-blocking mode - returns immediately without waiting for response + #[arg(long, default_value = "false")] + pub non_blocking: bool, + + /// Question type (general, phase_confirmation, contract_complete) + #[arg(long, default_value = "general")] + pub question_type: String, +} + /// Arguments for update command. #[derive(Args, Debug)] pub struct UpdateArgs { diff --git a/makima/src/daemon/cli/mod.rs b/makima/src/daemon/cli/mod.rs index bcaaa70..8063541 100644 --- a/makima/src/daemon/cli/mod.rs +++ b/makima/src/daemon/cli/mod.rs @@ -249,6 +249,9 @@ pub enum DirectiveCommand { /// Update directive metadata (PR URL, etc.) Update(directive::UpdateArgs), + + /// Ask a question and wait for user feedback + Ask(directive::AskArgs), } impl Cli { diff --git a/makima/src/daemon/skills/directive.md b/makima/src/daemon/skills/directive.md index 9d2b644..02de836 100644 --- a/makima/src/daemon/skills/directive.md +++ b/makima/src/daemon/skills/directive.md @@ -82,6 +82,32 @@ makima directive update --pr-url "" --pr-branch "" ``` Updates the directive's PR URL and/or PR branch. Used by completion tasks to store the PR URL after creating it. +### Ask User a Question +```bash +makima directive ask "" +``` +Asks the user a question and waits for their response. Questions appear on the directive page with a yellow indicator and can be answered inline. + +Options: +- `--choices "opt1,opt2,opt3"` - Provide choices for the user to select from +- `--context ""` - Additional context to help the user understand the question +- `--timeout ` - Wait timeout (default: 3600 = 1 hour) +- `--phaseguard` - Block indefinitely until the user responds (no timeout). Recommended for critical decisions during planning. +- `--multi-select` - Allow the user to select multiple choices +- `--non-blocking` - Return immediately without waiting for a response +- `--question-type ` - Question type + +**When to use:** +- During planning, when you need clarification on requirements or approach +- When there are multiple valid approaches and user preference matters +- When a decision requires domain knowledge you don't have +- Always use `--phaseguard` for questions that block progress (the reconcile mode on the directive also controls this) + +**Example:** +```bash +makima directive ask "Should we use REST or GraphQL for the new API?" --choices "REST,GraphQL" --context "The existing codebase uses REST but the frontend team prefers GraphQL" --phaseguard +``` + ## Memory Commands Directives have an optional key-value memory system that persists across steps and planning cycles. Use memory to share context, decisions, and learned information between steps — so downstream tasks don't need to re-discover what earlier steps already figured out. @@ -167,6 +193,7 @@ makima directive memory-batch-set --json '{"framework": "axum", "orm": "sqlx", " ### Initial Setup 1. Check the directive status to understand the goal 2. Decompose the goal into steps with clear dependencies + - If requirements are unclear, use `makima directive ask` to get clarification before finalizing the plan 3. Add steps using `add-step` with appropriate `--depends-on` flags 4. Start the directive with `start` 5. Steps with no dependencies will become `ready` immediately diff --git a/makima/src/orchestration/directive.rs b/makima/src/orchestration/directive.rs index 420b3e1..b91781c 100644 --- a/makima/src/orchestration/directive.rs +++ b/makima/src/orchestration/directive.rs @@ -165,7 +165,9 @@ impl DirectiveOrchestrator { {merge_preamble}\ INSTRUCTIONS:\n{task_plan}\n\ When done, the system will automatically mark this step as completed.\n\ - If you cannot complete the task, report the failure clearly.", + If you cannot complete the task, report the failure clearly.\n\n\ + If you need clarification or encounter a decision that requires user input, you can ask:\n\ + \x20 makima directive ask \"Your question\" --phaseguard", directive_title = step.directive_title, step_name = step.step_name, description = step.step_description.as_deref().unwrap_or("(none)"), @@ -1308,7 +1310,9 @@ fn build_planning_prompt( \x20 makima directive remove-step \n\ 2. Then, add new steps for the updated goal. Use generation {}.\n\ 3. New steps that build on completed work MUST use --depends-on to inherit the worktree.\n\ - 4. Ensure the new plan fully addresses the UPDATED goal.\n\n", + 4. Ensure the new plan fully addresses the UPDATED goal.\n\ + 5. If the updated goal is unclear or ambiguous, ask the user for clarification using:\n\ + \x20 makima directive ask \"\" --phaseguard\n\n", generation )); } @@ -1359,6 +1363,28 @@ Guidelines: they should BOTH list that prior step in dependsOn. IMPORTANT: Each step's taskPlan must be self-contained. The executing instance won't have your planning context. + +ASKING QUESTIONS: +If you need clarification from the user before finalizing the plan, you can ask questions: + makima directive ask "Your question here" + makima directive ask "Which approach?" --choices "Option A,Option B" --phaseguard + makima directive ask "Confirm this approach?" --context "Additional context here" --phaseguard + +Use --phaseguard for questions that block progress (the question will wait indefinitely for a response). +Without --phaseguard, questions timeout based on the directive's reconcile mode: +- Reconcile ON: questions block indefinitely until answered +- Reconcile OFF: questions timeout after 30 seconds with no response + +When to ask: +- Requirements are ambiguous and multiple interpretations are valid +- There are multiple equally valid technical approaches +- You need domain-specific knowledge that cannot be inferred from the codebase +- A decision has significant downstream impact and user preference matters + +Do NOT ask questions for: +- Implementation details you can determine from the codebase +- Standard engineering decisions with clear best practices +- Trivial choices that do not significantly affect the outcome "#, title = directive.title, goal = directive.goal, @@ -1480,6 +1506,9 @@ Already-merged branches will be a no-op. If a merge fails with conflicts: 1. First try: `git merge --abort` then retry with `git merge -X theirs --no-edit` 2. If that also fails, manually resolve the conflicts, `git add .`, and `git commit --no-edit` 3. Continue with remaining merges + +If you encounter issues you cannot resolve (e.g., persistent merge conflicts, PR update failures), you can ask for help: + makima directive ask "Your question" --phaseguard "#, title = directive.title, goal = directive.goal, @@ -1539,6 +1568,9 @@ For each step branch merge, if a merge fails with conflicts: 1. First try: `git merge --abort` then retry with `git merge -X theirs --no-edit` 2. If that also fails, manually resolve the conflicts, `git add .`, and `git commit --no-edit` 3. Continue with remaining merges + +If you encounter issues you cannot resolve (e.g., persistent merge conflicts, PR creation failures), you can ask for help: + makima directive ask "Your question" --phaseguard "#, title = directive.title, goal = directive.goal, @@ -1679,7 +1711,10 @@ makima directive remove-step 6. After processing all steps, report a summary of what was cleaned up and what was left. -IMPORTANT: Only remove steps whose task branches have been verified as merged. Never remove unmerged steps."#, +IMPORTANT: Only remove steps whose task branches have been verified as merged. Never remove unmerged steps. + +If you encounter issues you cannot resolve during cleanup, you can ask for help: + makima directive ask "Your question" --phaseguard"#, title = directive.title, pr_branch = pr_branch, base_branch = base_branch, @@ -1900,6 +1935,29 @@ Guidelines: they should BOTH list that prior step in dependsOn. IMPORTANT: Each step's taskPlan must be self-contained. The executing instance won't have your planning context. + +## Asking Questions + +If you need clarification about the goal, requirements, or implementation approach, you can ask the user: +```bash +makima directive ask "Your question here" +``` + +Options: +- `--choices "opt1,opt2,opt3"` - Provide choices +- `--context ""` - Additional context +- `--phaseguard` - Block until response (recommended for important questions) + +The question will appear in the directive UI. Behavior depends on reconcile mode: +- Reconcile ON: blocks until user responds +- Reconcile OFF: times out after 30s (use for non-critical questions) + +Use this when: +- The goal is ambiguous and could be interpreted multiple ways +- You need to choose between significantly different implementation approaches +- You discover constraints that affect the plan + +Do NOT ask questions for trivial decisions — use your best judgment. "#, generation = generation, )); -- cgit v1.2.3