summaryrefslogtreecommitdiff
path: root/makima/src/orchestration
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-10 14:50:07 +0000
committersoryu <soryu@soryu.co>2026-02-10 14:50:07 +0000
commit15b6e5fba161a194fe5427d7d29b0c4286423260 (patch)
treefdd7bde229150cbb56d37714c23c2dc9db902f28 /makima/src/orchestration
parent526edf672aae73c3670ab6141253bf92f1fbfe8c (diff)
downloadsoryu-15b6e5fba161a194fe5427d7d29b0c4286423260.tar.gz
soryu-15b6e5fba161a194fe5427d7d29b0c4286423260.zip
Add auto-PR creation for remote repos in directives
Diffstat (limited to 'makima/src/orchestration')
-rw-r--r--makima/src/orchestration/directive.rs254
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('"', "\\\""),
+ ),
+ )
+ }
+}