diff options
| author | soryu <soryu@soryu.co> | 2026-02-09 02:35:36 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-02-09 02:35:36 +0000 |
| commit | a2646a828febbdac798a206655a15eae7e463bca (patch) | |
| tree | 7736396d87f6bf4dd50a2d3e91525534a36adf00 | |
| parent | 9c92d9235a0d1258fff9f7e625b0463c4952c45f (diff) | |
| download | soryu-a2646a828febbdac798a206655a15eae7e463bca.tar.gz soryu-a2646a828febbdac798a206655a15eae7e463bca.zip | |
Add directive init
| -rw-r--r-- | makima/frontend/src/components/directives/DirectiveDetail.tsx | 16 | ||||
| -rw-r--r-- | makima/frontend/src/components/directives/StepNode.tsx | 8 | ||||
| -rw-r--r-- | makima/src/bin/makima.rs | 9 | ||||
| -rw-r--r-- | makima/src/daemon/api/directive.rs | 13 | ||||
| -rw-r--r-- | makima/src/daemon/cli/directive.rs | 11 | ||||
| -rw-r--r-- | makima/src/daemon/cli/mod.rs | 3 | ||||
| -rw-r--r-- | makima/src/db/repository.rs | 223 | ||||
| -rw-r--r-- | makima/src/orchestration/directive.rs | 478 | ||||
| -rw-r--r-- | makima/src/orchestration/mod.rs | 2 | ||||
| -rw-r--r-- | makima/src/server/mod.rs | 19 |
10 files changed, 781 insertions, 1 deletions
diff --git a/makima/frontend/src/components/directives/DirectiveDetail.tsx b/makima/frontend/src/components/directives/DirectiveDetail.tsx index abd2c55..1340482 100644 --- a/makima/frontend/src/components/directives/DirectiveDetail.tsx +++ b/makima/frontend/src/components/directives/DirectiveDetail.tsx @@ -98,6 +98,22 @@ export function DirectiveDetail({ </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"> + <span className="inline-block w-2 h-2 rounded-full bg-[#75aafc] animate-pulse" /> + <span className="text-[10px] font-mono text-[#75aafc]"> + Planning in progress... + </span> + <a + href={`/mesh/${directive.orchestratorTaskId}`} + className="text-[9px] font-mono text-[#556677] hover:text-[#75aafc] underline ml-auto" + > + View task + </a> + </div> + )} + {/* Controls */} <div className="flex flex-wrap gap-2"> {(directive.status === "draft" || directive.status === "paused") && ( diff --git a/makima/frontend/src/components/directives/StepNode.tsx b/makima/frontend/src/components/directives/StepNode.tsx index fa91956..2844b4a 100644 --- a/makima/frontend/src/components/directives/StepNode.tsx +++ b/makima/frontend/src/components/directives/StepNode.tsx @@ -46,6 +46,14 @@ export function StepNode({ step, onComplete, onFail, onSkip }: StepNodeProps) { {step.description} </p> )} + {step.taskId && ( + <a + href={`/mesh/${step.taskId}`} + className="text-[9px] font-mono text-[#556677] hover:text-[#75aafc] underline block mb-1" + > + {step.status === "running" ? "Auto-executing..." : "View task"} + </a> + )} {(step.status === "running" || step.status === "ready") && ( <div className="flex gap-1 mt-1"> {onComplete && ( diff --git a/makima/src/bin/makima.rs b/makima/src/bin/makima.rs index 639c88b..a0889d0 100644 --- a/makima/src/bin/makima.rs +++ b/makima/src/bin/makima.rs @@ -809,6 +809,15 @@ async fn run_directive( .await?; println!("{}", serde_json::to_string(&result.0)?); } + DirectiveCommand::BatchAddSteps(args) => { + let client = ApiClient::new(args.common.api_url, args.common.api_key)?; + let steps: serde_json::Value = serde_json::from_str(&args.json) + .map_err(|e| format!("Invalid JSON: {}", e))?; + let result = client + .directive_batch_add_steps(args.common.directive_id, steps) + .await?; + println!("{}", serde_json::to_string(&result.0)?); + } } Ok(()) diff --git a/makima/src/daemon/api/directive.rs b/makima/src/daemon/api/directive.rs index fbd27fe..cd21692 100644 --- a/makima/src/daemon/api/directive.rs +++ b/makima/src/daemon/api/directive.rs @@ -112,6 +112,19 @@ impl ApiClient { self.post_empty(&format!("/api/v1/directives/{}/steps/{}/skip", directive_id, step_id)).await } + /// Batch add steps to a directive. + pub async fn directive_batch_add_steps( + &self, + directive_id: Uuid, + steps: serde_json::Value, + ) -> Result<JsonValue, ApiError> { + self.post( + &format!("/api/v1/directives/{}/steps/batch", directive_id), + &steps, + ) + .await + } + /// Update the directive's goal. pub async fn directive_update_goal( &self, diff --git a/makima/src/daemon/cli/directive.rs b/makima/src/daemon/cli/directive.rs index 5de60ed..cd94a56 100644 --- a/makima/src/daemon/cli/directive.rs +++ b/makima/src/daemon/cli/directive.rs @@ -99,3 +99,14 @@ pub struct UpdateGoalArgs { /// New goal text pub goal: String, } + +/// Arguments for batch-add-steps command. +#[derive(Args, Debug)] +pub struct BatchAddStepsArgs { + #[command(flatten)] + pub common: DirectiveArgs, + + /// JSON array of steps: [{"name":"...","description":"...","taskPlan":"...","dependsOn":[],"orderIndex":0}] + #[arg(long)] + pub json: String, +} diff --git a/makima/src/daemon/cli/mod.rs b/makima/src/daemon/cli/mod.rs index faafaea..98923d9 100644 --- a/makima/src/daemon/cli/mod.rs +++ b/makima/src/daemon/cli/mod.rs @@ -243,6 +243,9 @@ pub enum DirectiveCommand { /// Update the directive's goal (triggers re-planning) UpdateGoal(directive::UpdateGoalArgs), + + /// Batch add multiple steps from JSON + BatchAddSteps(directive::BatchAddStepsArgs), } impl Cli { diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs index f347fc7..9ec5275 100644 --- a/makima/src/db/repository.rs +++ b/makima/src/db/repository.rs @@ -5325,3 +5325,226 @@ pub async fn set_directive_status( .await } +// ============================================================================= +// Directive Orchestrator Queries +// ============================================================================= + +/// Get active directives that need planning (no steps, no orchestrator task). +pub async fn get_directives_needing_planning( + pool: &PgPool, +) -> Result<Vec<Directive>, sqlx::Error> { + sqlx::query_as::<_, Directive>( + r#" + SELECT d.* FROM directives d + WHERE d.status = 'active' + AND d.orchestrator_task_id IS NULL + AND NOT EXISTS ( + SELECT 1 FROM directive_steps WHERE directive_id = d.id + ) + "#, + ) + .fetch_all(pool) + .await +} + +/// A step joined with minimal directive info for dispatch. +#[derive(Debug, Clone, sqlx::FromRow)] +pub struct StepForDispatch { + // Step fields + pub step_id: Uuid, + pub directive_id: Uuid, + pub step_name: String, + pub step_description: Option<String>, + pub task_plan: Option<String>, + pub order_index: i32, + pub generation: i32, + // Directive fields + pub owner_id: Uuid, + pub directive_title: String, + pub repository_url: Option<String>, + pub base_branch: Option<String>, +} + +/// Get ready steps that need task dispatch. +pub async fn get_ready_steps_for_dispatch( + pool: &PgPool, +) -> Result<Vec<StepForDispatch>, sqlx::Error> { + sqlx::query_as::<_, StepForDispatch>( + r#" + SELECT + ds.id AS step_id, + ds.directive_id, + ds.name AS step_name, + ds.description AS step_description, + ds.task_plan, + ds.order_index, + ds.generation, + d.owner_id, + d.title AS directive_title, + d.repository_url, + d.base_branch + FROM directive_steps ds + JOIN directives d ON d.id = ds.directive_id + WHERE ds.status = 'ready' + AND ds.task_id IS NULL + AND d.status = 'active' + ORDER BY ds.order_index + "#, + ) + .fetch_all(pool) + .await +} + +/// A running step joined with its task's current status. +#[derive(Debug, Clone, sqlx::FromRow)] +pub struct RunningStepWithTask { + pub step_id: Uuid, + pub directive_id: Uuid, + pub task_id: Uuid, + pub task_status: String, +} + +/// Get running steps with their task status for monitoring. +pub async fn get_running_steps_with_tasks( + pool: &PgPool, +) -> Result<Vec<RunningStepWithTask>, sqlx::Error> { + sqlx::query_as::<_, RunningStepWithTask>( + r#" + SELECT + ds.id AS step_id, + ds.directive_id, + ds.task_id AS "task_id!", + t.status AS task_status + FROM directive_steps ds + JOIN tasks t ON t.id = ds.task_id + WHERE ds.status = 'running' + AND ds.task_id IS NOT NULL + "#, + ) + .fetch_all(pool) + .await +} + +/// An orchestrator task to check (directive with pending planning task). +#[derive(Debug, Clone, sqlx::FromRow)] +pub struct OrchestratorTaskCheck { + pub directive_id: Uuid, + pub orchestrator_task_id: Uuid, + pub task_status: String, + pub owner_id: Uuid, +} + +/// Get directives with orchestrator tasks to check completion. +pub async fn get_orchestrator_tasks_to_check( + pool: &PgPool, +) -> Result<Vec<OrchestratorTaskCheck>, sqlx::Error> { + sqlx::query_as::<_, OrchestratorTaskCheck>( + r#" + SELECT + d.id AS directive_id, + d.orchestrator_task_id AS "orchestrator_task_id!", + t.status AS task_status, + d.owner_id + FROM directives d + JOIN tasks t ON t.id = d.orchestrator_task_id + WHERE d.orchestrator_task_id IS NOT NULL + AND d.status = 'active' + "#, + ) + .fetch_all(pool) + .await +} + +/// Get active directives that need re-planning (goal updated after latest step). +pub async fn get_directives_needing_replanning( + pool: &PgPool, +) -> Result<Vec<Directive>, sqlx::Error> { + sqlx::query_as::<_, Directive>( + r#" + SELECT d.* FROM directives d + WHERE d.status = 'active' + AND d.orchestrator_task_id IS NULL + AND EXISTS ( + SELECT 1 FROM directive_steps WHERE directive_id = d.id + ) + AND d.goal_updated_at > ( + SELECT COALESCE(MAX(ds.created_at), '1970-01-01'::timestamptz) + FROM directive_steps ds WHERE ds.directive_id = d.id + ) + "#, + ) + .fetch_all(pool) + .await +} + +/// Assign a task to a step and set status to running. +pub async fn assign_task_to_step( + pool: &PgPool, + step_id: Uuid, + task_id: Uuid, +) -> Result<Option<DirectiveStep>, sqlx::Error> { + sqlx::query_as::<_, DirectiveStep>( + r#" + UPDATE directive_steps + SET task_id = $2, status = 'running', started_at = NOW() + WHERE id = $1 + RETURNING * + "#, + ) + .bind(step_id) + .bind(task_id) + .fetch_optional(pool) + .await +} + +/// Set the orchestrator_task_id on a directive. +pub async fn assign_orchestrator_task( + pool: &PgPool, + directive_id: Uuid, + task_id: Uuid, +) -> Result<(), sqlx::Error> { + sqlx::query( + r#" + UPDATE directives + SET orchestrator_task_id = $2, updated_at = NOW() + WHERE id = $1 + "#, + ) + .bind(directive_id) + .bind(task_id) + .execute(pool) + .await?; + Ok(()) +} + +/// Clear the orchestrator_task_id on a directive. +pub async fn clear_orchestrator_task( + pool: &PgPool, + directive_id: Uuid, +) -> Result<(), sqlx::Error> { + sqlx::query( + r#" + UPDATE directives + SET orchestrator_task_id = NULL, updated_at = NOW() + WHERE id = $1 + "#, + ) + .bind(directive_id) + .execute(pool) + .await?; + Ok(()) +} + +/// Get the max generation number for steps in a directive. +pub async fn get_directive_max_generation( + pool: &PgPool, + directive_id: Uuid, +) -> Result<i32, sqlx::Error> { + let row: (Option<i32>,) = sqlx::query_as( + r#"SELECT MAX(generation) FROM directive_steps WHERE directive_id = $1"#, + ) + .bind(directive_id) + .fetch_one(pool) + .await?; + Ok(row.0.unwrap_or(0)) +} diff --git a/makima/src/orchestration/directive.rs b/makima/src/orchestration/directive.rs new file mode 100644 index 0000000..22003af --- /dev/null +++ b/makima/src/orchestration/directive.rs @@ -0,0 +1,478 @@ +//! Directive orchestrator — automates the full directive lifecycle. +//! +//! Runs as a background loop, polling every 15s to: +//! 1. Plan: active directives with no steps → spawn planning task +//! 2. Execute: ready steps → spawn execution tasks +//! 3. Monitor: detect task completion → update step status, advance DAG +//! 4. Re-plan: goal updated → spawn new planning task + +use sqlx::PgPool; +use uuid::Uuid; + +use crate::db::models::{CreateTaskRequest, UpdateTaskRequest}; +use crate::db::repository; +use crate::server::state::{DaemonCommand, SharedState}; + +pub struct DirectiveOrchestrator { + pool: PgPool, + state: SharedState, +} + +impl DirectiveOrchestrator { + pub fn new(pool: PgPool, state: SharedState) -> Self { + Self { pool, state } + } + + /// Run one orchestration tick — called every 15s. + pub async fn tick(&mut self) -> Result<(), anyhow::Error> { + self.phase_planning().await?; + self.phase_execution().await?; + self.phase_monitoring().await?; + self.phase_replanning().await?; + Ok(()) + } + + /// Phase 1: Active directives with no steps and no orchestrator task → spawn planning task. + async fn phase_planning(&self) -> Result<(), anyhow::Error> { + let directives = repository::get_directives_needing_planning(&self.pool).await?; + + for directive in directives { + tracing::info!( + directive_id = %directive.id, + title = %directive.title, + "Directive needs planning — spawning planning task" + ); + + let plan = build_planning_prompt(&directive, &[], 1); + + if let Err(e) = self + .spawn_orchestrator_task( + directive.id, + directive.owner_id, + format!("Plan: {}", directive.title), + plan, + directive.repository_url.as_deref(), + directive.base_branch.as_deref(), + ) + .await + { + tracing::warn!( + directive_id = %directive.id, + error = %e, + "Failed to spawn planning task" + ); + } + } + Ok(()) + } + + /// Phase 2: Ready steps with no task → create execution task and dispatch. + async fn phase_execution(&self) -> Result<(), anyhow::Error> { + let steps = repository::get_ready_steps_for_dispatch(&self.pool).await?; + + for step in steps { + tracing::info!( + step_id = %step.step_id, + directive_id = %step.directive_id, + step_name = %step.step_name, + "Dispatching execution task for ready step" + ); + + let task_plan = step + .task_plan + .as_deref() + .unwrap_or("Execute the step described below."); + + let plan = format!( + "You are executing a step in directive \"{directive_title}\".\n\n\ + STEP: {step_name}\n\ + DESCRIPTION: {description}\n\n\ + INSTRUCTIONS:\n{task_plan}\n\n\ + When done, the system will automatically mark this step as completed.\n\ + If you cannot complete the task, report the failure clearly.", + directive_title = step.directive_title, + step_name = step.step_name, + description = step.step_description.as_deref().unwrap_or("(none)"), + task_plan = task_plan, + ); + + match self + .spawn_step_task( + step.step_id, + step.directive_id, + step.owner_id, + format!("{}: {}", step.directive_title, step.step_name), + plan, + step.repository_url.as_deref(), + step.base_branch.as_deref(), + ) + .await + { + Ok(()) => {} + Err(e) => { + tracing::warn!( + step_id = %step.step_id, + error = %e, + "Failed to spawn execution task for step" + ); + } + } + } + Ok(()) + } + + /// Phase 3: Monitor running steps and orchestrator tasks. + async fn phase_monitoring(&self) -> Result<(), anyhow::Error> { + // Check running steps + let running = repository::get_running_steps_with_tasks(&self.pool).await?; + + for step in running { + match step.task_status.as_str() { + "completed" | "merged" | "done" => { + tracing::info!( + step_id = %step.step_id, + directive_id = %step.directive_id, + task_id = %step.task_id, + "Step task completed — updating step to completed" + ); + let update = crate::db::models::UpdateDirectiveStepRequest { + status: Some("completed".to_string()), + ..Default::default() + }; + repository::update_directive_step(&self.pool, step.step_id, update).await?; + repository::advance_directive_ready_steps(&self.pool, step.directive_id) + .await?; + repository::check_directive_idle(&self.pool, step.directive_id).await?; + } + "failed" | "interrupted" => { + tracing::warn!( + step_id = %step.step_id, + directive_id = %step.directive_id, + task_id = %step.task_id, + task_status = %step.task_status, + "Step task failed — updating step to failed" + ); + let update = crate::db::models::UpdateDirectiveStepRequest { + status: Some("failed".to_string()), + ..Default::default() + }; + repository::update_directive_step(&self.pool, step.step_id, update).await?; + repository::advance_directive_ready_steps(&self.pool, step.directive_id) + .await?; + repository::check_directive_idle(&self.pool, step.directive_id).await?; + } + _ => { + // Still running — do nothing + } + } + } + + // Check orchestrator (planning) tasks + let orch_tasks = repository::get_orchestrator_tasks_to_check(&self.pool).await?; + + for orch in orch_tasks { + match orch.task_status.as_str() { + "completed" | "merged" | "done" => { + tracing::info!( + directive_id = %orch.directive_id, + task_id = %orch.orchestrator_task_id, + "Planning task completed — clearing orchestrator task" + ); + repository::clear_orchestrator_task(&self.pool, orch.directive_id).await?; + // Advance DAG — planning task should have created steps + repository::advance_directive_ready_steps(&self.pool, orch.directive_id) + .await?; + } + "failed" | "interrupted" => { + tracing::warn!( + directive_id = %orch.directive_id, + task_id = %orch.orchestrator_task_id, + "Planning task failed — pausing directive" + ); + repository::clear_orchestrator_task(&self.pool, orch.directive_id).await?; + repository::set_directive_status( + &self.pool, + orch.owner_id, + orch.directive_id, + "paused", + ) + .await?; + } + _ => {} + } + } + + Ok(()) + } + + /// Phase 4: Re-planning — goal updated after latest step creation. + async fn phase_replanning(&self) -> Result<(), anyhow::Error> { + let directives = repository::get_directives_needing_replanning(&self.pool).await?; + + for directive in directives { + tracing::info!( + directive_id = %directive.id, + title = %directive.title, + "Directive goal updated — spawning re-planning task" + ); + + let existing_steps = + repository::list_directive_steps(&self.pool, directive.id).await?; + let generation = + repository::get_directive_max_generation(&self.pool, directive.id).await? + 1; + + let plan = build_planning_prompt(&directive, &existing_steps, generation); + + if let Err(e) = self + .spawn_orchestrator_task( + directive.id, + directive.owner_id, + format!("Re-plan: {}", directive.title), + plan, + directive.repository_url.as_deref(), + directive.base_branch.as_deref(), + ) + .await + { + tracing::warn!( + directive_id = %directive.id, + error = %e, + "Failed to spawn re-planning task" + ); + } + } + Ok(()) + } + + /// Spawn a planning/re-planning task and assign it as the directive's orchestrator task. + async fn spawn_orchestrator_task( + &self, + directive_id: Uuid, + owner_id: Uuid, + name: String, + plan: String, + repo_url: Option<&str>, + base_branch: Option<&str>, + ) -> Result<(), anyhow::Error> { + let req = CreateTaskRequest { + contract_id: None, + name, + description: Some("Directive planning task".to_string()), + plan, + parent_task_id: None, + is_supervisor: false, + priority: 0, + repository_url: repo_url.map(|s| s.to_string()), + base_branch: base_branch.map(|s| s.to_string()), + target_branch: None, + merge_mode: None, + target_repo_path: None, + completion_action: None, + continue_from_task_id: None, + copy_files: None, + checkpoint_sha: None, + branched_from_task_id: None, + conversation_history: None, + supervisor_worktree_task_id: None, + directive_id: Some(directive_id), + directive_step_id: None, + }; + + let task = repository::create_task_for_owner(&self.pool, owner_id, req).await?; + + repository::assign_orchestrator_task(&self.pool, directive_id, task.id).await?; + + // Try to dispatch to a daemon + self.try_dispatch_task(task.id, owner_id, &task.name, &task.plan, task.version).await; + + Ok(()) + } + + /// Spawn an execution task for a step and assign it. + async fn spawn_step_task( + &self, + step_id: Uuid, + directive_id: Uuid, + owner_id: Uuid, + name: String, + plan: String, + repo_url: Option<&str>, + base_branch: Option<&str>, + ) -> Result<(), anyhow::Error> { + let req = CreateTaskRequest { + contract_id: None, + name, + description: Some("Directive step execution task".to_string()), + plan, + parent_task_id: None, + is_supervisor: false, + priority: 0, + repository_url: repo_url.map(|s| s.to_string()), + base_branch: base_branch.map(|s| s.to_string()), + target_branch: None, + merge_mode: None, + target_repo_path: None, + completion_action: None, + continue_from_task_id: None, + copy_files: None, + checkpoint_sha: None, + branched_from_task_id: None, + conversation_history: None, + supervisor_worktree_task_id: None, + directive_id: Some(directive_id), + directive_step_id: Some(step_id), + }; + + let task = repository::create_task_for_owner(&self.pool, owner_id, req).await?; + + repository::assign_task_to_step(&self.pool, step_id, task.id).await?; + + // Try to dispatch to a daemon + self.try_dispatch_task(task.id, owner_id, &task.name, &task.plan, task.version).await; + + Ok(()) + } + + /// Try to dispatch a task to an available daemon. If none available, leave pending. + async fn try_dispatch_task( + &self, + task_id: Uuid, + owner_id: Uuid, + task_name: &str, + plan: &str, + version: i32, + ) { + let Some(daemon_id) = self.state.find_alternative_daemon(owner_id, &[]) else { + tracing::info!( + task_id = %task_id, + "No daemon available for directive task — leaving pending for retry" + ); + return; + }; + + // Update task status to starting and assign daemon + let update_req = UpdateTaskRequest { + status: Some("starting".to_string()), + daemon_id: Some(daemon_id), + version: Some(version), + ..Default::default() + }; + + match repository::update_task_for_owner(&self.pool, task_id, owner_id, update_req).await { + Ok(Some(updated_task)) => { + let command = DaemonCommand::SpawnTask { + task_id, + task_name: task_name.to_string(), + plan: plan.to_string(), + repo_url: updated_task.repository_url.clone(), + base_branch: updated_task.base_branch.clone(), + target_branch: updated_task.target_branch.clone(), + parent_task_id: None, + depth: 0, + is_orchestrator: false, + target_repo_path: None, + completion_action: None, + continue_from_task_id: None, + copy_files: None, + contract_id: None, + is_supervisor: false, + autonomous_loop: false, + resume_session: false, + conversation_history: None, + patch_data: None, + patch_base_sha: None, + local_only: false, + auto_merge_local: false, + supervisor_worktree_task_id: None, + }; + + if let Err(e) = self.state.send_daemon_command(daemon_id, command).await { + tracing::warn!( + task_id = %task_id, + daemon_id = %daemon_id, + error = %e, + "Failed to send SpawnTask to daemon for directive task" + ); + } else { + tracing::info!( + task_id = %task_id, + daemon_id = %daemon_id, + "Dispatched directive task to daemon" + ); + } + } + Ok(None) => { + tracing::warn!(task_id = %task_id, "Task not found when trying to dispatch"); + } + Err(e) => { + tracing::warn!(task_id = %task_id, error = %e, "Failed to update task for dispatch"); + } + } + } +} + +/// Build the planning prompt for a directive. +fn build_planning_prompt( + directive: &crate::db::models::Directive, + existing_steps: &[crate::db::models::DirectiveStep], + generation: i32, +) -> String { + let mut prompt = String::new(); + + if !existing_steps.is_empty() { + prompt.push_str(&format!( + "EXISTING STEPS (generation {}):\n", + generation - 1 + )); + for step in existing_steps { + prompt.push_str(&format!( + "- {} [{}]: {}\n", + step.name, + step.status, + step.description.as_deref().unwrap_or("(no description)") + )); + } + prompt.push_str(&format!( + "\nAdd new steps that build on or complement existing work. Use generation {}.\n\n", + generation + )); + } + + prompt.push_str(&format!( + r#"You are planning the implementation of a directive. + +DIRECTIVE: "{title}" +GOAL: {goal} +{repo_section} +Your job: +1. Explore the repository to understand the codebase +2. Decompose the goal into concrete, ordered steps +3. Each step = one task for a Claude Code instance to execute +4. Submit ALL steps using the batch command or individual add-step commands + +For each step, define: +- name: Short imperative title (e.g., "Add user authentication middleware") +- description: What to do and acceptance criteria +- taskPlan: Full instructions for the Claude instance (include file paths, patterns to follow) +- dependsOn: UUIDs of steps this depends on (use IDs from previous add-step responses) +- orderIndex: Execution order hint + +Submit steps: + makima directive add-step "Step Name" --description "..." --task-plan "..." + (Use --depends-on "uuid1,uuid2" for dependencies, referencing IDs from earlier add-step outputs) + +Or batch: + makima directive batch-add-steps --json '[{{"name":"...","description":"...","taskPlan":"...","dependsOn":[],"orderIndex":0}}]' + +IMPORTANT: Each step's taskPlan must be self-contained. The executing instance won't have your planning context. +"#, + title = directive.title, + goal = directive.goal, + repo_section = match &directive.repository_url { + Some(url) => format!("REPOSITORY: {}\n", url), + None => String::new(), + }, + )); + + prompt +} diff --git a/makima/src/orchestration/mod.rs b/makima/src/orchestration/mod.rs index 8b13789..e7ffb70 100644 --- a/makima/src/orchestration/mod.rs +++ b/makima/src/orchestration/mod.rs @@ -1 +1 @@ - +pub mod directive; diff --git a/makima/src/server/mod.rs b/makima/src/server/mod.rs index 7a1391b..4cb4296 100644 --- a/makima/src/server/mod.rs +++ b/makima/src/server/mod.rs @@ -481,6 +481,25 @@ pub async fn run_server(state: SharedState, addr: &str) -> anyhow::Result<()> { "Retry orchestrator started (interval: {}s)", RETRY_ORCHESTRATOR_INTERVAL_SECS ); + + // Spawn directive orchestrator - automates directive lifecycle + let directive_pool = pool.clone(); + let directive_state = state.clone(); + tokio::spawn(async move { + let mut orch = crate::orchestration::directive::DirectiveOrchestrator::new( + directive_pool, + directive_state, + ); + let mut interval = tokio::time::interval(std::time::Duration::from_secs(15)); + loop { + interval.tick().await; + if let Err(e) = orch.tick().await { + tracing::warn!(error = %e, "Directive orchestrator tick failed"); + } + } + }); + + tracing::info!("Directive orchestrator started (interval: 15s)"); } let app = make_router(state); |
