summaryrefslogblamecommitdiff
path: root/makima/src/orchestration/directive.rs
blob: 5c134d68f54fce9050edab6eba858339edd1068a (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11










                                                                          

                   
                                                              














                                                               














                                                                                                      













                                                                                                  
                                                                      






















                                                                                 
                                                                                
                                                                  
                                       









                                                                                
                                                                                   






                                                                                                                                 



































                                                                                                        
 




                                                                

























                                                                                              



                                                                                  
                                  
                                              




                                                                                         
                                                










                                                                            
                                          
                                          












                                                                 














                                                                                                








                                                                                  













                                                                                                   













                                                                                                                               





























                                                                                                   
                 


                                                                                                                    






                                                                                        
























                                                                                                  
                                

                           
                 


                                                                                                                                   










                                                                                          





                                                                          
 





                                                                                                  
 

                                                                                                  
 



















                                                                                                                          










































                                                                                             










                                                                                            





                                                                                              

                                                                                        








                                  
                                            













                                                                           
                                                          
                                  










                                                                                       

                                                                                    
 






                                                                                       



              
                                                                                  






                               
               




                                                                                      
                         











                                                                                                  












                                                                                                              










                                                                      
                                                                              
                                                                              





                                               

                                   


                                                      
                                                            








                                                                                          
                                 





                                                             
                                








                                                                                                     
             
     






                                                                                               
                                   

                                                                      

                                                     
                                                                              




                                                       
                                                 

                                                                        
                  
 

                                                                                                       

                                  
 



                                           
 




                                                                             
 









                                                                                  
 






                                                     
 
                                                                             

                                           
                                     




                                                            
                     


                                    















                                                                                                    










                                                                                




                                                         
                                                             
                          
                     
                 


                                                                                                                                 






                                                                                  







                                                                
 



                                                                                                 
                                                                           

                                                                                      

                                                                                            
                                                          








                                                                                   



























                                                                                                                            

                                                                           

                                                                                              

                                      
                             
                         
 










                                                                                                 
                 


                                                                                                                                  


             




                                                                                       
                                   

                                                           
                                                       
                 










                                                                                           
                                                                             











                                                                  













                                                                                                      




                                                         
                                                               
                          
                     
                 


                                                                                                                                   


             












































                                                                                       





























                                                                                            

 




                                                                             


                                                                          





















                                                                                     


















                                                                         
                                                                                                                  






                                                   
                                                                            

























                                                                             







                                                                                       






                                                                                               


































































                                                                                        




                                                        
                                                             



                                   
                                                                                                                                                       
                                 

                                                                                                           



































                                                                                                                                                                        

                          







                                                                                   
                                    






















                                                                                                                                                             

                                                  






















                                                                                                                                                                 
         































                                                                                                                                                                   
         














                                                                                                                                                                   
                                 





                                                                                                              




















                                                                                              


                                                                                                                  







                                                                                                                                 
                                 














                                                                                                          
 











                                                                                                                





















                                                                               


                                                                                      


                                                                       












                                                                                           




                













                                                                    


                                                                                                                 
   
                                                                                                          
   
                                                                                














                                                                                             















                                                                                             
                                  
                            
                                                
                                      

                                            




                                                                         


















                                                             




                                                                                                                              
   
                                                                                                               
   
                                                                                
 




                                                                                                 
   
                                                         
   
                                                                                                                                                               
















                                                                         











































                                                                                                                          
                                         

   
                                                                                                                 





                                  
















































                                                                                                                                                              
                                                             



                                                                                    
                                                      

































































































































                                                                                                                                                              

                                                                                                          





                                                                                                       
                                                                                                                                                                                    
























                                                                                                                
//! 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 base64::Engine;

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> {
        if let Err(e) = self.phase_planning().await {
            tracing::warn!(error = %e, "Directive phase_planning failed, continuing to next phase");
        }
        if let Err(e) = self.phase_execution().await {
            tracing::warn!(error = %e, "Directive phase_execution failed, continuing to next phase");
        }
        if let Err(e) = self.phase_monitoring().await {
            tracing::warn!(error = %e, "Directive phase_monitoring failed, continuing to next phase");
        }
        if let Err(e) = self.phase_replanning().await {
            tracing::warn!(error = %e, "Directive phase_replanning failed, continuing to next phase");
        }
        if let Err(e) = self.phase_completion().await {
            tracing::warn!(error = %e, "Directive phase_completion failed");
        }
        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.
    /// Also retries pending directive tasks that weren't dispatched previously.
    async fn phase_execution(&self) -> Result<(), anyhow::Error> {
        // Create tasks for ready steps
        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"
            );

            // Resolve dependency steps to their task IDs for worktree continuation
            let dep_tasks = match repository::get_step_dependency_tasks(&self.pool, &step.depends_on).await {
                Ok(deps) => deps,
                Err(e) => {
                    tracing::warn!(step_id = %step.step_id, error = %e, "Failed to resolve step dependencies — skipping step");
                    continue;
                }
            };
            let mut continue_from_task_id = dep_tasks.first().map(|d| d.task_id);

            // If no dependency tasks resolved, try to continue from previous work:
            // 1) Use the directive's PR branch as base (contains all previous merged work)
            // 2) Fall back to the last completed step's task for worktree continuation
            let effective_base_branch = if continue_from_task_id.is_none() {
                if step.pr_branch.is_some() {
                    tracing::info!(
                        step_id = %step.step_id,
                        pr_branch = ?step.pr_branch,
                        "Step has no deps — using directive PR branch as base"
                    );
                    step.pr_branch.as_deref()
                } else {
                    // No PR branch yet — try to continue from the last completed step's worktree
                    match repository::get_last_completed_step_task_id(
                        &self.pool,
                        step.directive_id,
                    )
                    .await
                    {
                        Ok(Some(task_id)) => {
                            tracing::info!(
                                step_id = %step.step_id,
                                continue_from = %task_id,
                                "Step has no deps, no PR branch — continuing from last completed task"
                            );
                            continue_from_task_id = Some(task_id);
                            step.base_branch.as_deref()
                        }
                        _ => step.base_branch.as_deref(),
                    }
                }
            } else {
                step.base_branch.as_deref()
            };

            let task_plan = step
                .task_plan
                .as_deref()
                .unwrap_or("Execute the step described below.");

            // Build merge instructions for additional dependencies (beyond the first)
            let merge_preamble = if dep_tasks.len() > 1 {
                use crate::daemon::worktree::{sanitize_name, short_uuid};
                let merge_lines: Vec<String> = dep_tasks[1..]
                    .iter()
                    .map(|d| {
                        let branch = format!(
                            "makima/task-{}-{}",
                            sanitize_name(&d.task_name),
                            short_uuid(d.task_id)
                        );
                        format!("git merge origin/{} --no-edit", branch)
                    })
                    .collect();
                format!(
                    "IMPORTANT — MERGE DEPENDENCY BRANCHES FIRST:\n\
                     This step continues from one dependency's worktree, but also depends on \
                     additional branches. Before starting work, run:\n\
                     ```\ngit fetch origin\n{}\n```\n\
                     Resolve any merge conflicts sensibly, then proceed.\n\n",
                    merge_lines.join("\n"),
                )
            } else {
                String::new()
            };

            let plan = format!(
                "You are executing a step in directive \"{directive_title}\".\n\n\
                 STEP: {step_name}\n\
                 DESCRIPTION: {description}\n\n\
                 {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.",
                directive_title = step.directive_title,
                step_name = step.step_name,
                description = step.step_description.as_deref().unwrap_or("(none)"),
                merge_preamble = merge_preamble,
                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(),
                    effective_base_branch,
                    continue_from_task_id,
                )
                .await
            {
                Ok(()) => {}
                Err(e) => {
                    tracing::warn!(
                        step_id = %step.step_id,
                        error = %e,
                        "Failed to spawn execution task for step"
                    );
                }
            }
        }

        // Retry pending directive tasks that weren't dispatched
        let pending = repository::get_pending_directive_tasks(&self.pool).await?;
        for task in pending {
            if self
                .try_dispatch_task(task.id, task.owner_id, &task.name, &task.plan, task.version)
                .await
            {
                // Task dispatched — mark its step as running if it has one
                if let Some(step_id) = task.directive_step_id {
                    let _ = repository::set_step_running(&self.pool, step_id).await;
                }
            }
        }

        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 {
            if let Err(e) = async {
                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?;

                        // Mark linked orders as done
                        if let Ok(linked_orders) = repository::get_orders_by_step_id(&self.pool, step.step_id).await {
                            for order in linked_orders {
                                if order.status != "done" && order.status != "archived" {
                                    let order_update = crate::db::models::UpdateOrderRequest {
                                        status: Some("done".to_string()),
                                        ..Default::default()
                                    };
                                    let _ = repository::update_order(&self.pool, order.owner_id, order.id, order_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?;
                    }
                    "paused" => {
                        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"
                        );
                    }
                    _ => {}
                }
                Ok::<(), anyhow::Error>(())
            }.await {
                tracing::warn!(step_id = %step.step_id, error = %e, "Error processing running step — continuing");
            }
        }

        // Check orchestrator (planning) tasks
        let orch_tasks = repository::get_orchestrator_tasks_to_check(&self.pool).await?;

        for orch in orch_tasks {
            if let Err(e) = async {
                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?;
                        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::<(), anyhow::Error>(())
            }.await {
                tracing::warn!(directive_id = %orch.directive_id, error = %e, "Error processing orchestrator task — continuing");
            }
        }

        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 {
            if let Err(e) = async {
                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 goal_history =
                    repository::get_directive_goal_history(&self.pool, directive.id, 3).await?;

                let plan =
                    build_planning_prompt(&directive, &existing_steps, generation, &goal_history);

                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::<(), anyhow::Error>(())
            }.await {
                tracing::warn!(directive_id = %directive.id, error = %e, "Error in re-planning directive — continuing");
            }
        }
        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?;

        // Cancel any old planning tasks for this directive (superseded by the new one)
        let cancelled =
            repository::cancel_old_planning_tasks(&self.pool, directive_id, task.id).await?;
        if cancelled > 0 {
            tracing::info!(
                directive_id = %directive_id,
                cancelled_count = cancelled,
                "Cancelled old planning tasks superseded by new plan"
            );
        }

        // 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.
    /// Links the task to the step but only marks the step as 'running' once dispatched.
    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>,
        continue_from_task_id: Option<Uuid>,
    ) -> 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: Some("branch".to_string()),
            continue_from_task_id,
            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?;

        // Link the task to the step (sets task_id) but keep step as 'ready' for now
        repository::link_task_to_step(&self.pool, step_id, task.id).await?;

        // Only mark step as 'running' if we can actually dispatch the task
        if self
            .try_dispatch_task(task.id, owner_id, &task.name, &task.plan, task.version)
            .await
        {
            repository::set_step_running(&self.pool, step_id).await?;
        }

        Ok(())
    }

    /// Try to dispatch a task to an available daemon. Returns true if dispatched.
    async fn try_dispatch_task(
        &self,
        task_id: Uuid,
        owner_id: Uuid,
        task_name: &str,
        plan: &str,
        version: i32,
    ) -> bool {
        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 false;
        };

        // 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)) => {
                // Load patch data from continue_from task for worktree recovery
                let (patch_data, patch_base_sha) = if let Some(from_id) = updated_task.continue_from_task_id {
                    match repository::get_latest_checkpoint_patch(&self.pool, from_id).await {
                        Ok(Some(patch)) => {
                            let encoded = base64::engine::general_purpose::STANDARD.encode(&patch.patch_data);
                            (Some(encoded), Some(patch.base_commit_sha))
                        }
                        _ => (None, None),
                    }
                } else {
                    (None, None)
                };

                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: updated_task.completion_action.clone(),
                    continue_from_task_id: updated_task.continue_from_task_id,
                    copy_files: None,
                    contract_id: None,
                    is_supervisor: false,
                    autonomous_loop: false,
                    resume_session: false,
                    conversation_history: None,
                    patch_data,
                    patch_base_sha,
                    local_only: false,
                    auto_merge_local: false,
                    supervisor_worktree_task_id: None,
                    directive_id: updated_task.directive_id,
                };

                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"
                    );
                    return false;
                } else {
                    tracing::info!(
                        task_id = %task_id,
                        daemon_id = %daemon_id,
                        "Dispatched directive task to daemon"
                    );
                    return true;
                }
            }
            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");
            }
        }
        false
    }

    /// Phase 5: Completion — spawn PR-creation tasks for idle directives.
    async fn phase_completion(&self) -> Result<(), anyhow::Error> {
        // Part 1: Spawn completion tasks for idle directives
        let directives = repository::get_idle_directives_needing_completion(&self.pool).await?;

        for directive in directives {
            if let Err(e) = async {
                // Skip if already claimed (completion_task_id is set)
                if directive.completion_task_id.is_some() {
                    tracing::debug!(
                        directive_id = %directive.id,
                        "Directive already has a completion task — skipping"
                    );
                    return Ok::<(), anyhow::Error>(());
                }

                tracing::info!(
                    directive_id = %directive.id,
                    title = %directive.title,
                    "Directive idle — spawning completion task for PR"
                );

                let step_tasks = repository::get_completed_step_tasks(&self.pool, directive.id).await?;
                if step_tasks.is_empty() {
                    return Ok(());
                }

                let base_branch = directive
                    .base_branch
                    .as_deref()
                    .unwrap_or("main");

                let directive_branch = format!(
                    "makima/directive-{}-{}",
                    crate::daemon::worktree::sanitize_name(&directive.title),
                    crate::daemon::worktree::short_uuid(directive.id),
                );

                let step_branches: Vec<String> = step_tasks
                    .iter()
                    .map(|st| {
                        format!(
                            "makima/{}-{}",
                            crate::daemon::worktree::sanitize_name(&st.task_name),
                            crate::daemon::worktree::short_uuid(st.task_id),
                        )
                    })
                    .collect();

                let prompt = build_completion_prompt(
                    &directive,
                    &step_tasks,
                    &step_branches,
                    &directive_branch,
                    base_branch,
                );

                // Create the task FIRST so we have a real task ID for the FK
                match self
                    .spawn_completion_task(
                        directive.id,
                        directive.owner_id,
                        format!("PR: {}", directive.title),
                        prompt,
                        directive.repository_url.as_deref(),
                        directive.base_branch.as_deref(),
                    )
                    .await
                {
                    Ok(task_id) => {
                        // Atomically claim with the REAL task ID (satisfies FK constraint)
                        let claimed = repository::claim_directive_for_completion(
                            &self.pool,
                            directive.id,
                            task_id,
                        )
                        .await?;

                        if !claimed {
                            tracing::debug!(
                                directive_id = %directive.id,
                                "Directive already claimed for completion — task will be orphaned"
                            );
                            return Ok(());
                        }

                        let update = crate::db::models::UpdateDirectiveRequest {
                            pr_branch: Some(directive_branch.clone()),
                            ..Default::default()
                        };
                        let _ = repository::update_directive_for_owner(
                            &self.pool,
                            directive.owner_id,
                            directive.id,
                            update,
                        )
                        .await;
                    }
                    Err(e) => {
                        tracing::warn!(
                            directive_id = %directive.id,
                            error = %e,
                            "Failed to spawn completion task"
                        );
                    }
                }
                Ok(())
            }.await {
                tracing::warn!(directive_id = %directive.id, error = %e, "Error processing directive completion — continuing");
            }
        }

        // Part 2: Monitor completion tasks
        let checks = repository::get_completion_tasks_to_check(&self.pool).await?;

        for check in checks {
            if let Err(e) = async {
                match check.task_status.as_str() {
                    "completed" | "merged" | "done" => {
                        tracing::info!(
                            directive_id = %check.directive_id,
                            task_id = %check.completion_task_id,
                            "Completion task finished"
                        );

                        if check.pr_url.is_none() {
                            match self.extract_pr_url_from_task(check.completion_task_id).await {
                                Ok(Some(url)) => {
                                    tracing::info!(
                                        directive_id = %check.directive_id,
                                        pr_url = %url,
                                        "Extracted PR URL from completion task output"
                                    );
                                    let update = crate::db::models::UpdateDirectiveRequest {
                                        pr_url: Some(url),
                                        ..Default::default()
                                    };
                                    let _ = repository::update_directive_for_owner(
                                        &self.pool,
                                        check.owner_id,
                                        check.directive_id,
                                        update,
                                    )
                                    .await;
                                }
                                Ok(None) => {
                                    if check.task_name.starts_with("Verify PR:") {
                                        tracing::warn!(
                                            directive_id = %check.directive_id,
                                            task_id = %check.completion_task_id,
                                            "Verification task finished but no PR URL found — marking directive completed"
                                        );
                                        let update = crate::db::models::UpdateDirectiveRequest {
                                            status: Some("archived".to_string()),
                                            ..Default::default()
                                        };
                                        let _ = repository::update_directive_for_owner(
                                            &self.pool,
                                            check.owner_id,
                                            check.directive_id,
                                            update,
                                        )
                                        .await;
                                    } else {
                                        tracing::warn!(
                                            directive_id = %check.directive_id,
                                            task_id = %check.completion_task_id,
                                            "Completion task finished but no PR URL found — will spawn verifier"
                                        );
                                    }
                                }
                                Err(e) => {
                                    tracing::warn!(
                                        directive_id = %check.directive_id,
                                        error = %e,
                                        "Failed to extract PR URL from completion task output"
                                    );
                                }
                            }
                        }

                        repository::clear_completion_task(&self.pool, check.directive_id).await?;
                    }
                    "failed" | "interrupted" => {
                        tracing::warn!(
                            directive_id = %check.directive_id,
                            task_id = %check.completion_task_id,
                            "Completion task failed"
                        );
                        repository::clear_completion_task(&self.pool, check.directive_id).await?;
                    }
                    _ => {}
                }
                Ok::<(), anyhow::Error>(())
            }.await {
                tracing::warn!(directive_id = %check.directive_id, error = %e, "Error processing completion task — continuing");
            }
        }

        // Part 3: Spawn verification tasks for directives with pr_branch but no pr_url
        let verify_directives =
            repository::get_directives_needing_verification(&self.pool).await?;

        for directive in verify_directives {
            if let Err(e) = async {
                // Skip if already claimed
                if directive.completion_task_id.is_some() {
                    return Ok::<(), anyhow::Error>(());
                }

                tracing::info!(
                    directive_id = %directive.id,
                    title = %directive.title,
                    "Directive has pr_branch but no pr_url — spawning verification task"
                );

                let pr_branch = directive.pr_branch.as_deref().unwrap_or("unknown");
                let base_branch = directive.base_branch.as_deref().unwrap_or("main");
                let prompt = build_verification_prompt(&directive, pr_branch, base_branch);

                // Create the task FIRST so we have a real task ID for the FK
                match self
                    .spawn_completion_task(
                        directive.id,
                        directive.owner_id,
                        format!("Verify PR: {}", directive.title),
                        prompt,
                        directive.repository_url.as_deref(),
                        directive.base_branch.as_deref(),
                    )
                    .await
                {
                    Ok(task_id) => {
                        // Atomically claim with the REAL task ID (satisfies FK constraint)
                        let claimed = repository::claim_directive_for_completion(
                            &self.pool,
                            directive.id,
                            task_id,
                        )
                        .await?;

                        if !claimed {
                            tracing::debug!(
                                directive_id = %directive.id,
                                "Directive already claimed for verification — task will be orphaned"
                            );
                        }
                    }
                    Err(e) => {
                        tracing::warn!(
                            directive_id = %directive.id,
                            error = %e,
                            "Failed to spawn verification task"
                        );
                    }
                }
                Ok(())
            }.await {
                tracing::warn!(directive_id = %directive.id, error = %e, "Error processing verification directive — continuing");
            }
        }

        Ok(())
    }

    /// Spawn a completion task that creates/updates a PR from step branches.
    async fn spawn_completion_task(
        &self,
        directive_id: Uuid,
        owner_id: Uuid,
        name: String,
        plan: String,
        repo_url: Option<&str>,
        base_branch: Option<&str>,
    ) -> Result<Uuid, anyhow::Error> {
        let req = CreateTaskRequest {
            contract_id: None,
            name,
            description: Some("Directive PR completion 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?;

        // Try to dispatch to a daemon
        self.try_dispatch_task(task.id, owner_id, &task.name, &task.plan, task.version)
            .await;

        Ok(task.id)
    }

    /// Extract a GitHub PR URL from a completion task's output events.
    /// Searches task output for patterns like `https://github.com/.../pull/123`.
    async fn extract_pr_url_from_task(
        &self,
        task_id: Uuid,
    ) -> Result<Option<String>, anyhow::Error> {
        let events = repository::get_task_output(&self.pool, task_id, Some(500)).await?;

        let pr_url_re = regex::Regex::new(r"https://github\.com/[^/\s]+/[^/\s]+/pull/\d+")?;

        // Search from most recent events backwards for the PR URL
        for event in events.iter().rev() {
            if let Some(ref data) = event.event_data {
                // Check the content field inside event_data JSON
                if let Some(content) = data.get("content").and_then(|c| c.as_str()) {
                    if let Some(m) = pr_url_re.find(content) {
                        return Ok(Some(m.as_str().to_string()));
                    }
                }
                // Also check the raw JSON string representation as fallback
                let data_str = data.to_string();
                if let Some(m) = pr_url_re.find(&data_str) {
                    return Ok(Some(m.as_str().to_string()));
                }
            }
        }

        Ok(None)
    }
}

/// Trigger a completion task (PR creation/update) for a directive.
///
/// This is the public entry point used by both the orchestrator tick and the
/// manual "create PR" API handler.  It encapsulates the full flow:
/// 1. Validate the directive has completed step tasks
/// 2. Create the completion task (so we have a real task ID)
/// 3. Atomically claim the directive for completion with the real task ID
/// 4. Dispatch the task to a daemon
///
/// Returns the created task ID on success.
pub async fn trigger_completion_task(
    pool: &PgPool,
    state: &SharedState,
    directive_id: Uuid,
    owner_id: Uuid,
) -> Result<Uuid, anyhow::Error> {
    let directive = repository::get_directive_for_owner(pool, owner_id, directive_id)
        .await?
        .ok_or_else(|| anyhow::anyhow!("Directive not found"))?;

    // Check for already-running completion task
    if directive.completion_task_id.is_some() {
        anyhow::bail!("A completion task is already running for this directive");
    }

    let step_tasks = repository::get_completed_step_tasks(pool, directive_id).await?;
    if step_tasks.is_empty() {
        anyhow::bail!("No completed steps with tasks found");
    }

    let base_branch = directive.base_branch.as_deref().unwrap_or("main");

    let directive_branch = format!(
        "makima/directive-{}-{}",
        crate::daemon::worktree::sanitize_name(&directive.title),
        crate::daemon::worktree::short_uuid(directive.id),
    );

    let step_branches: Vec<String> = step_tasks
        .iter()
        .map(|st| {
            format!(
                "makima/{}-{}",
                crate::daemon::worktree::sanitize_name(&st.task_name),
                crate::daemon::worktree::short_uuid(st.task_id),
            )
        })
        .collect();

    let prompt = build_completion_prompt(&directive, &step_tasks, &step_branches, &directive_branch, base_branch);

    let task_name = if directive.pr_url.is_some() {
        format!("Update PR: {}", directive.title)
    } else {
        format!("PR: {}", directive.title)
    };

    // Create the completion task FIRST so we have a real task ID for the FK
    let req = CreateTaskRequest {
        contract_id: None,
        name: task_name,
        description: Some("Directive PR completion task".to_string()),
        plan: prompt,
        parent_task_id: None,
        is_supervisor: false,
        priority: 0,
        repository_url: directive.repository_url.clone(),
        base_branch: directive.base_branch.clone(),
        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(pool, owner_id, req).await?;

    // Atomically claim the directive with the REAL task ID (satisfies FK constraint).
    // This prevents concurrent ticks from also spawning a completion task.
    let claimed =
        repository::claim_directive_for_completion(pool, directive_id, task.id).await?;
    if !claimed {
        anyhow::bail!("Directive already claimed for completion");
    }

    // Update pr_branch on the directive
    let update = crate::db::models::UpdateDirectiveRequest {
        pr_branch: Some(directive_branch),
        ..Default::default()
    };
    let _ = repository::update_directive_for_owner(pool, owner_id, directive_id, update).await;

    // Try to dispatch to a daemon
    if let Some(daemon_id) = state.find_alternative_daemon(owner_id, &[]) {
        let update_req = crate::db::models::UpdateTaskRequest {
            status: Some("starting".to_string()),
            daemon_id: Some(daemon_id),
            version: Some(task.version),
            ..Default::default()
        };

        if let Ok(Some(updated_task)) =
            repository::update_task_for_owner(pool, task.id, owner_id, update_req).await
        {
            let (patch_data, patch_base_sha) =
                if let Some(from_id) = updated_task.continue_from_task_id {
                    match repository::get_latest_checkpoint_patch(pool, from_id).await {
                        Ok(Some(patch)) => {
                            let encoded = base64::engine::general_purpose::STANDARD
                                .encode(&patch.patch_data);
                            (Some(encoded), Some(patch.base_commit_sha))
                        }
                        _ => (None, None),
                    }
                } else {
                    (None, None)
                };

            let command = DaemonCommand::SpawnTask {
                task_id: task.id,
                task_name: task.name.clone(),
                plan: task.plan.clone(),
                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: updated_task.completion_action.clone(),
                continue_from_task_id: updated_task.continue_from_task_id,
                copy_files: None,
                contract_id: None,
                is_supervisor: false,
                autonomous_loop: false,
                resume_session: false,
                conversation_history: None,
                patch_data,
                patch_base_sha,
                local_only: false,
                auto_merge_local: false,
                supervisor_worktree_task_id: None,
                directive_id: updated_task.directive_id,
            };

            if let Err(e) = state.send_daemon_command(daemon_id, command).await {
                tracing::warn!(
                    task_id = %task.id,
                    daemon_id = %daemon_id,
                    error = %e,
                    "Failed to dispatch completion task to daemon"
                );
            }
        }
    }

    Ok(task.id)
}

/// Build the planning prompt for a directive.
fn build_planning_prompt(
    directive: &crate::db::models::Directive,
    existing_steps: &[crate::db::models::DirectiveStep],
    generation: i32,
    goal_history: &[crate::db::models::DirectiveGoalHistory],
) -> String {
    let mut prompt = String::new();

    if !existing_steps.is_empty() {
        // ── RE-PLANNING header ──────────────────────────────────────
        prompt.push_str(&format!(
            "⚠️  RE-PLANNING: The GOAL has been updated — you must re-evaluate ALL existing steps.\n\
             Previous steps were planned for an earlier version of the goal. Some may no longer be \
             relevant. Review each step below and act according to the instructions per status category.\n\n",
        ));

        // ── Goal changes section ──────────────────────────────────
        if !goal_history.is_empty() {
            prompt.push_str("-- GOAL CHANGES --\n");
            prompt.push_str("The goal has been updated. Compare the previous and current goals to understand what changed:\n\n");
            for (i, entry) in goal_history.iter().enumerate() {
                if i == 0 {
                    prompt.push_str(&format!(
                        "PREVIOUS GOAL (replaced at {}):\n{}\n\n",
                        entry.created_at.format("%Y-%m-%d %H:%M:%S UTC"),
                        entry.goal
                    ));
                } else {
                    prompt.push_str(&format!(
                        "OLDER GOAL (version from {}):\n{}\n\n",
                        entry.created_at.format("%Y-%m-%d %H:%M:%S UTC"),
                        entry.goal
                    ));
                }
            }
            prompt.push_str(&format!(
                "CURRENT GOAL (what you must plan for):\n{}\n\n",
                directive.goal
            ));
            prompt.push_str(
                "IMPORTANT: Analyze what CHANGED between the previous goal and the current goal.\n\
                 - If the change is minor (e.g., clarification, small addition), try to KEEP existing pending steps and only add/modify what is needed for the delta.\n\
                 - If the change is major (e.g., completely different objective), you may need to remove most pending steps and create a fresh plan.\n\
                 - Always preserve completed and running steps - they represent work already done.\n\n",
            );
        }

        prompt.push_str(&format!(
            "EXISTING STEPS (generation {}):\n",
            generation - 1
        ));

        // Categorise steps by status
        let mut completed: Vec<&crate::db::models::DirectiveStep> = Vec::new();
        let mut running: Vec<&crate::db::models::DirectiveStep> = Vec::new();
        let mut pending_ready: Vec<&crate::db::models::DirectiveStep> = Vec::new();
        let mut failed: Vec<&crate::db::models::DirectiveStep> = Vec::new();
        let mut skipped: Vec<&crate::db::models::DirectiveStep> = Vec::new();

        for step in existing_steps {
            match step.status.as_str() {
                "completed" => completed.push(step),
                "running" => running.push(step),
                "pending" | "ready" => pending_ready.push(step),
                "failed" => failed.push(step),
                "skipped" => skipped.push(step),
                _ => pending_ready.push(step),
            }
        }

        // ── Completed steps ─────────────────────────────────────────
        if !completed.is_empty() {
            prompt.push_str("\n── COMPLETED steps (KEEP — work already done) ──\n");
            prompt.push_str("These steps have finished. Their work is committed and available.\n");
            prompt.push_str("Do NOT remove them. New steps can depend on them to inherit their changes.\n");
            let mut last_completed_id: Option<Uuid> = None;
            for step in &completed {
                prompt.push_str(&format!(
                    "  ✅ {} (id: {}): {}\n",
                    step.name,
                    step.id,
                    step.description.as_deref().unwrap_or("(no description)")
                ));
                last_completed_id = Some(step.id);
            }
            if let Some(last_id) = last_completed_id {
                prompt.push_str(&format!(
                    "\nNew steps that build on previous work SHOULD use --depends-on \"{}\" (the last completed step) \
                     so their worktree inherits all prior changes. Without this dependency, new steps start from a \
                     fresh checkout and won't see any of the work done by previous steps.\n",
                    last_id
                ));
            }
        }

        // ── Running steps ───────────────────────────────────────────
        if !running.is_empty() {
            prompt.push_str("\n── RUNNING steps (cannot remove — note if obsolete) ──\n");
            prompt.push_str("These steps are currently executing and cannot be removed or skipped.\n");
            prompt.push_str("If a running step is no longer relevant to the NEW goal, note it but do not attempt to remove it.\n");
            for step in &running {
                prompt.push_str(&format!(
                    "  🔄 {} (id: {}): {}\n",
                    step.name,
                    step.id,
                    step.description.as_deref().unwrap_or("(no description)")
                ));
            }
        }

        // ── Pending / Ready steps ───────────────────────────────────
        if !pending_ready.is_empty() {
            prompt.push_str("\n── PENDING/READY steps (EVALUATE — remove if no longer relevant) ──\n");
            prompt.push_str("These steps have NOT started yet. Evaluate each against the NEW goal:\n");
            prompt.push_str("  • If still relevant → leave it in place (no action needed).\n");
            prompt.push_str("  • If NO LONGER relevant → remove it with: makima directive remove-step <step_id>\n");
            for step in &pending_ready {
                prompt.push_str(&format!(
                    "  ⏳ [{}] {} (id: {}): {}\n",
                    step.status,
                    step.name,
                    step.id,
                    step.description.as_deref().unwrap_or("(no description)")
                ));
            }
        }

        // ── Failed steps ────────────────────────────────────────────
        if !failed.is_empty() {
            prompt.push_str("\n── FAILED steps (EVALUATE — remove if no longer relevant) ──\n");
            prompt.push_str("These steps failed. Evaluate each against the NEW goal:\n");
            prompt.push_str("  • If still relevant → remove the failed step and re-add a corrected version.\n");
            prompt.push_str("  • If NO LONGER relevant → remove it with: makima directive remove-step <step_id>\n");
            for step in &failed {
                prompt.push_str(&format!(
                    "  ❌ {} (id: {}): {}\n",
                    step.name,
                    step.id,
                    step.description.as_deref().unwrap_or("(no description)")
                ));
            }
        }

        // ── Skipped steps ───────────────────────────────────────────
        if !skipped.is_empty() {
            prompt.push_str("\n── SKIPPED steps (remove if no longer relevant) ──\n");
            for step in &skipped {
                prompt.push_str(&format!(
                    "  ⏭️  {} (id: {}): {}\n",
                    step.name,
                    step.id,
                    step.description.as_deref().unwrap_or("(no description)")
                ));
            }
        }

        // ── Instructions ────────────────────────────────────────────
        prompt.push_str(&format!(
            "\n── ACTION PLAN ──\n\
             1. First, remove any pending/ready/failed/skipped steps that are NOT relevant to the NEW goal:\n\
             \x20  makima directive remove-step <step_id>\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",
            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 phase number. Steps only start after ALL steps with a lower orderIndex complete.
  Steps with the same orderIndex run in parallel. Use ascending values (0, 1, 2, ...) to create sequential phases.
  Use dependsOn for fine-grained control within the same phase.

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}}]'

DEPENDENCY WORKTREE CONTINUATION:
Each step runs in its own git worktree. How that worktree is initialised depends on dependsOn:
- With dependsOn: the step continues from the first dependency's worktree (inheriting all committed and
  uncommitted changes). Additional dependencies are merged in as branches before work starts.
- Without dependsOn: the step starts from a FRESH worktree based on the base branch (or the PR branch if
  a PR already exists from previous completions).

Because of this, you MUST chain steps using dependsOn whenever one step's work builds on another's.
If step B modifies files created/changed by step A, step B MUST list step A in its dependsOn — otherwise
step B will start from a blank worktree and won't see step A's changes at all.

Guidelines:
- For sequential work, create a linear chain: step1 → step2 → step3 (each depends on the previous).
- Only omit dependsOn for truly independent steps that can start from a fresh checkout.
- Parallel steps that share no files can omit mutual dependencies, but if they both build on a prior step
  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.
"#,
        title = directive.title,
        goal = directive.goal,
        repo_section = match &directive.repository_url {
            Some(url) => format!("REPOSITORY: {}\n", url),
            None => String::new(),
        },
    ));

    prompt
}

/// Build the prompt for a completion task that creates or updates a PR.
fn build_completion_prompt(
    directive: &crate::db::models::Directive,
    step_tasks: &[crate::db::repository::CompletedStepTask],
    step_branches: &[String],
    directive_branch: &str,
    base_branch: &str,
) -> String {
    let merge_commands: String = step_branches
        .iter()
        .map(|b| format!("git merge origin/{} --no-edit", b))
        .collect::<Vec<_>>()
        .join("\n");

    let step_summary: String = step_tasks
        .iter()
        .zip(step_branches.iter())
        .map(|(st, branch)| format!("- {} (branch: {})", st.step_name, branch))
        .collect::<Vec<_>>()
        .join("\n");

    if let Some(ref pr_url) = directive.pr_url {
        // Re-completion: PR already exists — but it may have been merged or closed.
        // We must check the PR state first and handle accordingly.
        format!(
            r#"You are updating an existing PR for directive "{title}".

IMPORTANT: The previous PR may have been merged or closed. You MUST check its state first.

## Step 1: Check PR state

Run this command to check the PR state:
```
gh pr view {pr_url} --json state --jq '.state'
```

## If the PR state is MERGED or CLOSED:

The previous PR was already merged/closed. You need to create a NEW PR with a fresh branch.

Goal: {goal}

Steps completed:
{step_summary}

1. Clear the old PR URL:
```
makima directive update --pr-url ""
```

2. Create a fresh branch with a timestamp suffix to avoid collision:
```
git fetch origin
NEW_BRANCH="{directive_branch}-v$(date +%s)"
git checkout -b "$NEW_BRANCH" origin/{base_branch}
{merge_commands}
git push -u origin "$NEW_BRANCH"
```

3. Create a new PR. Generate a concise, descriptive PR title (max 72 characters) based on the steps completed.
The title should summarize what the changes actually accomplish — do NOT just use the directive name "{title}".
Focus on the actual work done in the steps listed below.
```
gh pr create --title "<YOUR_GENERATED_TITLE>" --body "{pr_body}" --head "$NEW_BRANCH" --base {base_branch}
```
Replace <YOUR_GENERATED_TITLE> with the concise descriptive title you generated.

4. Store the new PR URL:
```
makima directive update --pr-url "<URL_FROM_GH_PR_CREATE>"
```
Replace the URL with the actual PR URL from the `gh pr create` output. This step is CRITICAL.

5. Update the directive pr_branch to the new branch name:
```
makima directive update --pr-branch "$NEW_BRANCH"
```

## If the PR state is OPEN:

The PR is still open. Merge new step branches into the existing PR branch.

Steps completed:
{step_summary}

Run these commands:
```
git fetch origin
git checkout {directive_branch}
git pull origin {directive_branch}
{merge_commands}
git push origin {directive_branch}
```

Already-merged branches will be a no-op. If there are merge conflicts, resolve them sensibly.
"#,
            title = directive.title,
            goal = directive.goal,
            pr_url = pr_url,
            directive_branch = directive_branch,
            base_branch = base_branch,
            step_summary = step_summary,
            merge_commands = merge_commands,
            pr_body = format!(
                "## Directive\\n\\n{}\\n\\n## Steps\\n\\n{}",
                directive.goal.replace('\n', "\\n").replace('"', "\\\""),
                step_summary.replace('\n', "\\n").replace('"', "\\\""),
            ),
        )
    } else {
        // First completion: create new PR
        format!(
            r#"You are creating a PR for directive "{title}".

Goal: {goal}

Steps completed:
{step_summary}

Run these commands to create a combined branch and PR:
```
git fetch origin
git checkout -b {directive_branch} origin/{base_branch}
{merge_commands}
git push -u origin {directive_branch}
```

Then create the PR. You MUST generate a concise, descriptive PR title (max 72 characters) based on the steps completed above.
The title should summarize what the changes actually accomplish — do NOT just use the directive name "{title}".
For example, instead of "soryu-co/soryu - makima" use something like "Fix order lifecycle, PR update, and contracts overflow".
Focus on the actual work done in the steps.

```
gh pr create --title "<YOUR_GENERATED_TITLE>" --body "{pr_body}" --head {directive_branch} --base {base_branch}
```
Replace <YOUR_GENERATED_TITLE> with the concise descriptive title you generated.

IMPORTANT: After creating the PR, you MUST store the PR URL so the directive system can track it.

1. Run `gh pr create` as shown above and capture its output
2. The output will contain the PR URL (e.g., https://github.com/owner/repo/pull/123)
3. Then run this command to store the URL:
```
makima directive update --pr-url "https://github.com/..."
```
Replace the URL with the actual PR URL from the `gh pr create` output. This step is CRITICAL — the PR will not be tracked by the directive system without it.

If there are merge conflicts, resolve them sensibly before pushing.
"#,
            title = directive.title,
            goal = directive.goal,
            directive_branch = directive_branch,
            base_branch = base_branch,
            step_summary = step_summary,
            merge_commands = merge_commands,
            pr_body = format!(
                "## Directive\\n\\n{}\\n\\n## Steps\\n\\n{}",
                directive.goal.replace('\n', "\\n").replace('"', "\\\""),
                step_summary.replace('\n', "\\n").replace('"', "\\\""),
            ),
        )
    }
}

/// Build a prompt for verifying whether a PR was created for a directive.
/// This is a one-shot task: if it can't find or create the PR, the directive
/// will be marked completed to avoid infinite retries.
fn build_verification_prompt(
    directive: &crate::db::models::Directive,
    pr_branch: &str,
    base_branch: &str,
) -> String {
    format!(
        r#"You are verifying whether a PR exists for directive "{title}".

The completion task already ran and pushed branch `{pr_branch}`, but the PR URL was not captured.

Follow these steps IN ORDER:

1. Check if a PR already exists for this branch:
```
gh pr list --head {pr_branch} --json url --jq '.[0].url'
```

2. If the command outputs a URL, store it:
```
makima directive update --pr-url "<URL_FROM_ABOVE>"
```
Done — the PR already exists.

3. If no PR was found, check if the branch exists on the remote:
```
git ls-remote --heads origin {pr_branch}
```

4. If the branch exists, create the PR:
```
gh pr create --title "{title}" --body "Directive PR verification — auto-created" --head {pr_branch} --base {base_branch}
```
Then store the resulting URL:
```
makima directive update --pr-url "<URL_FROM_GH_PR_CREATE>"
```

5. If the branch does NOT exist on the remote, the work was likely merged directly.
Mark the directive as completed:
```
makima directive update --status archived
```

IMPORTANT: You MUST run `makima directive update` with either `--pr-url` or `--status archived` before finishing.
"#,
        title = directive.title,
        pr_branch = pr_branch,
        base_branch = base_branch,
    )
}

/// Build a specialized planning prompt for picking up open orders.
///
/// This prompt instructs the planner to evaluate available orders, select an
/// adequate number based on priority and directive capacity, and create steps
/// to fulfil them.
pub fn build_order_pickup_prompt(
    directive: &crate::db::models::Directive,
    existing_steps: &[crate::db::models::DirectiveStep],
    orders: &[crate::db::models::Order],
    generation: i32,
    goal_history: &[crate::db::models::DirectiveGoalHistory],
) -> String {
    let mut prompt = String::new();

    // ── Directive context ──────────────────────────────────────────
    prompt.push_str(&format!(
        "You are planning work for directive \"{title}\".\n\n\
         GOAL: {goal}\n\
         {repo_section}\n",
        title = directive.title,
        goal = directive.goal,
        repo_section = match &directive.repository_url {
            Some(url) => format!("REPOSITORY: {}\n", url),
            None => String::new(),
        },
    ));

    // ── Goal history (if any) ─────────────────────────────────────
    if !goal_history.is_empty() {
        prompt.push_str("-- GOAL CHANGES --\n");
        for (i, entry) in goal_history.iter().enumerate() {
            if i == 0 {
                prompt.push_str(&format!(
                    "PREVIOUS GOAL (replaced at {}):\n{}\n\n",
                    entry.created_at.format("%Y-%m-%d %H:%M:%S UTC"),
                    entry.goal
                ));
            } else {
                prompt.push_str(&format!(
                    "OLDER GOAL (version from {}):\n{}\n\n",
                    entry.created_at.format("%Y-%m-%d %H:%M:%S UTC"),
                    entry.goal
                ));
            }
        }
    }

    // ── Orders being picked up ───────────────────────────────────
    prompt.push_str("== ORDERS AVAILABLE FOR PLANNING ==\n");
    prompt.push_str("The following open orders have been linked to this directive. \
                     Review them and create steps to fulfil them.\n\n");
    for (i, order) in orders.iter().enumerate() {
        prompt.push_str(&format!(
            "  {}. [{}] [{}] {} \n     orderId: {}\n",
            i + 1,
            order.priority,
            order.order_type,
            order.title,
            order.id,
        ));
        if let Some(ref desc) = order.description {
            prompt.push_str(&format!("     Description: {}\n", desc));
        }
    }
    prompt.push('\n');

    // ── Existing steps ───────────────────────────────────────────
    if !existing_steps.is_empty() {
        let mut completed: Vec<&crate::db::models::DirectiveStep> = Vec::new();
        let mut running: Vec<&crate::db::models::DirectiveStep> = Vec::new();
        let mut pending_ready: Vec<&crate::db::models::DirectiveStep> = Vec::new();
        let mut failed: Vec<&crate::db::models::DirectiveStep> = Vec::new();

        for step in existing_steps {
            match step.status.as_str() {
                "completed" => completed.push(step),
                "running" => running.push(step),
                "pending" | "ready" => pending_ready.push(step),
                "failed" => failed.push(step),
                _ => pending_ready.push(step),
            }
        }

        prompt.push_str("== EXISTING STEPS ==\n");

        if !completed.is_empty() {
            prompt.push_str("\n── COMPLETED steps (work already done) ──\n");
            let mut last_completed_id: Option<uuid::Uuid> = None;
            for step in &completed {
                prompt.push_str(&format!(
                    "  ✅ {} (id: {}): {}\n",
                    step.name,
                    step.id,
                    step.description.as_deref().unwrap_or("(no description)")
                ));
                last_completed_id = Some(step.id);
            }
            if let Some(last_id) = last_completed_id {
                prompt.push_str(&format!(
                    "\nNew steps that build on previous work SHOULD use --depends-on \"{}\" \
                     so their worktree inherits all prior changes.\n",
                    last_id
                ));
            }
        }

        if !running.is_empty() {
            prompt.push_str("\n── RUNNING steps (in progress) ──\n");
            for step in &running {
                prompt.push_str(&format!(
                    "  🔄 {} (id: {}): {}\n",
                    step.name,
                    step.id,
                    step.description.as_deref().unwrap_or("(no description)")
                ));
            }
        }

        if !pending_ready.is_empty() {
            prompt.push_str("\n── PENDING/READY steps (not yet started) ──\n");
            for step in &pending_ready {
                prompt.push_str(&format!(
                    "  ⏳ [{}] {} (id: {}): {}\n",
                    step.status,
                    step.name,
                    step.id,
                    step.description.as_deref().unwrap_or("(no description)")
                ));
            }
        }

        if !failed.is_empty() {
            prompt.push_str("\n── FAILED steps ──\n");
            for step in &failed {
                prompt.push_str(&format!(
                    "  ❌ {} (id: {}): {}\n",
                    step.name,
                    step.id,
                    step.description.as_deref().unwrap_or("(no description)")
                ));
            }
        }

        // Determine whether to create fresh steps or combine with existing
        let all_terminal = existing_steps
            .iter()
            .all(|s| matches!(s.status.as_str(), "completed" | "failed" | "skipped"));

        if all_terminal {
            prompt.push_str(
                "\nAll existing steps are in terminal state (completed/failed/skipped). \
                 Create FRESH steps from the orders above.\n\n",
            );
        } else if !pending_ready.is_empty() || !running.is_empty() {
            prompt.push_str(
                "\nThere are existing active/pending steps. Evaluate whether to KEEP them \
                 and ADD new steps from the orders, creating a combined plan. \
                 Do not duplicate work already covered by existing steps.\n\n",
            );
        }
    }

    // ── Order selection guidance ─────────────────────────────────
    prompt.push_str(&format!(
        "== ORDER SELECTION GUIDANCE ==\n\
         You do NOT need to pick up ALL orders. Select an ADEQUATE number based on:\n\
         - Priority: prefer critical and high priority orders first\n\
         - Directive scope: consider the directive's current goal and capacity\n\
         - Avoid overloading: don't assign too many orders to a single directive\n\
         - The orders are already linked to this directive — focus on creating steps\n\n\
         If some orders are not relevant to this directive's goal or would overload it, \
         you may leave them for a future pickup cycle.\n\n"
    ));

    // ── Step creation instructions ───────────────────────────────
    prompt.push_str(&format!(
        r#"== STEP CREATION INSTRUCTIONS ==
For each order (or group of related orders), create one or more steps:
- 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 phase number. Steps only start after ALL steps with a lower orderIndex complete.
  Steps with the same orderIndex run in parallel. Use ascending values (0, 1, 2, ...) to create sequential phases.
- orderId: The UUID of the order this step fulfils. Include this so the order is automatically marked done
  when the step completes. Use the orderId shown in the order listing above.

Submit steps using generation {generation}:
  makima directive add-step "Step Name" --description "..." --task-plan "..." --generation {generation}
  (Use --depends-on "uuid1,uuid2" for dependencies)

Or batch:
  makima directive batch-add-steps --json '[{{"name":"...","description":"...","taskPlan":"...","dependsOn":[],"orderIndex":0,"generation":{generation},"orderId":"<order-uuid>"}}]'

DEPENDENCY WORKTREE CONTINUATION:
Each step runs in its own git worktree. How that worktree is initialised depends on dependsOn:
- With dependsOn: the step continues from the first dependency's worktree (inheriting all committed and
  uncommitted changes). Additional dependencies are merged in as branches before work starts.
- Without dependsOn: the step starts from a FRESH worktree based on the base branch (or the PR branch if
  a PR already exists from previous completions).

Because of this, you MUST chain steps using dependsOn whenever one step's work builds on another's.
If step B modifies files created/changed by step A, step B MUST list step A in its dependsOn — otherwise
step B will start from a blank worktree and won't see step A's changes at all.

Guidelines:
- For sequential work, create a linear chain: step1 → step2 → step3 (each depends on the previous).
- Only omit dependsOn for truly independent steps that can start from a fresh checkout.
- Parallel steps that share no files can omit mutual dependencies, but if they both build on a prior step
  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.
"#,
        generation = generation,
    ));

    prompt
}