diff options
| author | soryu <soryu@soryu.co> | 2026-02-10 14:50:07 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-02-10 14:50:07 +0000 |
| commit | 15b6e5fba161a194fe5427d7d29b0c4286423260 (patch) | |
| tree | fdd7bde229150cbb56d37714c23c2dc9db902f28 /makima/src/orchestration/directive.rs | |
| parent | 526edf672aae73c3670ab6141253bf92f1fbfe8c (diff) | |
| download | soryu-15b6e5fba161a194fe5427d7d29b0c4286423260.tar.gz soryu-15b6e5fba161a194fe5427d7d29b0c4286423260.zip | |
Add auto-PR creation for remote repos in directives
Diffstat (limited to 'makima/src/orchestration/directive.rs')
| -rw-r--r-- | makima/src/orchestration/directive.rs | 254 |
1 files changed, 252 insertions, 2 deletions
diff --git a/makima/src/orchestration/directive.rs b/makima/src/orchestration/directive.rs index 744977e..6121529 100644 --- a/makima/src/orchestration/directive.rs +++ b/makima/src/orchestration/directive.rs @@ -29,6 +29,7 @@ impl DirectiveOrchestrator { self.phase_execution().await?; self.phase_monitoring().await?; self.phase_replanning().await?; + self.phase_completion().await?; Ok(()) } @@ -330,7 +331,7 @@ impl DirectiveOrchestrator { target_branch: None, merge_mode: None, target_repo_path: None, - completion_action: None, + completion_action: Some("branch".to_string()), continue_from_task_id: None, copy_files: None, checkpoint_sha: None, @@ -395,7 +396,7 @@ impl DirectiveOrchestrator { depth: 0, is_orchestrator: false, target_repo_path: None, - completion_action: None, + completion_action: updated_task.completion_action.clone(), continue_from_task_id: None, copy_files: None, contract_id: None, @@ -437,6 +438,163 @@ impl DirectiveOrchestrator { } 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 { + 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() { + continue; + } + + 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), + ); + + // Compute step branch names using the same formula as execute_completion_action + 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, + ); + + 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) => { + // Store pr_branch on directive immediately + 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; + repository::assign_completion_task(&self.pool, directive.id, task_id).await?; + } + Err(e) => { + tracing::warn!( + directive_id = %directive.id, + error = %e, + "Failed to spawn completion task" + ); + } + } + } + + // Part 2: Monitor completion tasks + let checks = repository::get_completion_tasks_to_check(&self.pool).await?; + + for check in checks { + 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" + ); + 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?; + } + _ => { + // Still running + } + } + } + + 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) + } } /// Build the planning prompt for a directive. @@ -504,3 +662,95 @@ IMPORTANT: Each step's taskPlan must be self-contained. The executing instance w 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 directive.pr_url.is_some() { + // Re-completion: PR already exists, merge new branches into existing PR branch + format!( + r#"You are updating an existing PR for directive "{title}". + +The PR branch `{directive_branch}` already exists. Merge any new step branches into it. + +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, + directive_branch = directive_branch, + step_summary = step_summary, + merge_commands = merge_commands, + ) + } 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: +``` +gh pr create --title "{title}" --body "{pr_body}" --head {directive_branch} --base {base_branch} +``` + +After creating the PR, store the URL: +``` +makima directive update --pr-url "<the PR URL from gh pr create output>" +``` + +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('"', "\\\""), + ), + ) + } +} |
