summaryrefslogtreecommitdiff
path: root/makima/src/orchestration/directive.rs
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/orchestration/directive.rs')
-rw-r--r--makima/src/orchestration/directive.rs173
1 files changed, 173 insertions, 0 deletions
diff --git a/makima/src/orchestration/directive.rs b/makima/src/orchestration/directive.rs
index 25aaf1b..736715d 100644
--- a/makima/src/orchestration/directive.rs
+++ b/makima/src/orchestration/directive.rs
@@ -901,6 +901,179 @@ impl DirectiveOrchestrator {
}
}
+/// 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. Claim the directive for completion (returns error if already claimed)
+/// 3. Build branch names and prompt
+/// 4. Spawn the completion task and assign it
+///
+/// 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");
+ }
+
+ // Claim for completion
+ let placeholder_id = Uuid::new_v4();
+ let claimed =
+ repository::claim_directive_for_completion(pool, directive_id, placeholder_id).await?;
+ if !claimed {
+ anyhow::bail!("Directive already claimed for completion");
+ }
+
+ 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 = if directive.pr_url.is_some() {
+ build_verification_prompt(&directive, &directive_branch, base_branch)
+ } else {
+ 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
+ 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?;
+
+ // 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;
+
+ // Assign the real task as the completion task
+ repository::assign_completion_task(pool, directive_id, task.id).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,