summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-09 02:35:36 +0000
committersoryu <soryu@soryu.co>2026-02-09 02:35:36 +0000
commita2646a828febbdac798a206655a15eae7e463bca (patch)
tree7736396d87f6bf4dd50a2d3e91525534a36adf00
parent9c92d9235a0d1258fff9f7e625b0463c4952c45f (diff)
downloadsoryu-a2646a828febbdac798a206655a15eae7e463bca.tar.gz
soryu-a2646a828febbdac798a206655a15eae7e463bca.zip
Add directive init
-rw-r--r--makima/frontend/src/components/directives/DirectiveDetail.tsx16
-rw-r--r--makima/frontend/src/components/directives/StepNode.tsx8
-rw-r--r--makima/src/bin/makima.rs9
-rw-r--r--makima/src/daemon/api/directive.rs13
-rw-r--r--makima/src/daemon/cli/directive.rs11
-rw-r--r--makima/src/daemon/cli/mod.rs3
-rw-r--r--makima/src/db/repository.rs223
-rw-r--r--makima/src/orchestration/directive.rs478
-rw-r--r--makima/src/orchestration/mod.rs2
-rw-r--r--makima/src/server/mod.rs19
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);