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 | |
| parent | 526edf672aae73c3670ab6141253bf92f1fbfe8c (diff) | |
| download | soryu-15b6e5fba161a194fe5427d7d29b0c4286423260.tar.gz soryu-15b6e5fba161a194fe5427d7d29b0c4286423260.zip | |
Add auto-PR creation for remote repos in directives
| -rw-r--r-- | makima/frontend/src/components/directives/DirectiveDetail.tsx | 34 | ||||
| -rw-r--r-- | makima/frontend/src/lib/api.ts | 5 | ||||
| -rw-r--r-- | makima/migrations/20260210000001_add_directive_pr_fields.sql | 3 | ||||
| -rw-r--r-- | makima/src/bin/makima.rs | 7 | ||||
| -rw-r--r-- | makima/src/daemon/api/directive.rs | 20 | ||||
| -rw-r--r-- | makima/src/daemon/cli/directive.rs | 15 | ||||
| -rw-r--r-- | makima/src/daemon/cli/mod.rs | 3 | ||||
| -rw-r--r-- | makima/src/daemon/task/manager.rs | 45 | ||||
| -rw-r--r-- | makima/src/daemon/worktree/manager.rs | 92 | ||||
| -rw-r--r-- | makima/src/db/models.rs | 7 | ||||
| -rw-r--r-- | makima/src/db/repository.rs | 123 | ||||
| -rw-r--r-- | makima/src/orchestration/directive.rs | 254 |
12 files changed, 590 insertions, 18 deletions
diff --git a/makima/frontend/src/components/directives/DirectiveDetail.tsx b/makima/frontend/src/components/directives/DirectiveDetail.tsx index 1340482..616c5d2 100644 --- a/makima/frontend/src/components/directives/DirectiveDetail.tsx +++ b/makima/frontend/src/components/directives/DirectiveDetail.tsx @@ -114,6 +114,40 @@ export function DirectiveDetail({ </div> )} + {/* PR link */} + {directive.prUrl && ( + <div className="flex items-center gap-2 mb-2 px-2 py-1.5 bg-[#0a1a10] border border-emerald-900 rounded"> + <span className="inline-block w-2 h-2 rounded-full bg-emerald-400" /> + <span className="text-[10px] font-mono text-emerald-400"> + PR created + </span> + <a + href={directive.prUrl} + target="_blank" + rel="noopener noreferrer" + className="text-[10px] font-mono text-emerald-400 hover:text-emerald-300 underline ml-auto truncate max-w-[200px]" + > + {directive.prUrl} + </a> + </div> + )} + + {/* Completion task indicator */} + {directive.completionTaskId && !directive.prUrl && ( + <div className="flex items-center gap-2 mb-2 px-2 py-1.5 bg-[#1a1a10] border border-yellow-900 rounded"> + <span className="inline-block w-2 h-2 rounded-full bg-yellow-400 animate-pulse" /> + <span className="text-[10px] font-mono text-yellow-400"> + Creating PR... + </span> + <a + href={`/mesh/${directive.completionTaskId}`} + className="text-[9px] font-mono text-[#556677] hover:text-yellow-400 underline ml-auto" + > + View task + </a> + </div> + )} + {/* Controls */} <div className="flex flex-wrap gap-2"> {(directive.status === "draft" || directive.status === "paused") && ( diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts index b1422df..40e160e 100644 --- a/makima/frontend/src/lib/api.ts +++ b/makima/frontend/src/lib/api.ts @@ -3020,6 +3020,9 @@ export interface Directive { localPath: string | null; baseBranch: string | null; orchestratorTaskId: string | null; + prUrl: string | null; + prBranch: string | null; + completionTaskId: string | null; goalUpdatedAt: string; startedAt: string | null; version: number; @@ -3055,6 +3058,8 @@ export interface DirectiveSummary { status: DirectiveStatus; repositoryUrl: string | null; orchestratorTaskId: string | null; + prUrl: string | null; + completionTaskId: string | null; version: number; createdAt: string; updatedAt: string; diff --git a/makima/migrations/20260210000001_add_directive_pr_fields.sql b/makima/migrations/20260210000001_add_directive_pr_fields.sql new file mode 100644 index 0000000..e72bb39 --- /dev/null +++ b/makima/migrations/20260210000001_add_directive_pr_fields.sql @@ -0,0 +1,3 @@ +ALTER TABLE directives ADD COLUMN pr_url TEXT; +ALTER TABLE directives ADD COLUMN pr_branch VARCHAR(255); +ALTER TABLE directives ADD COLUMN completion_task_id UUID REFERENCES tasks(id) ON DELETE SET NULL; diff --git a/makima/src/bin/makima.rs b/makima/src/bin/makima.rs index a0889d0..c2c9beb 100644 --- a/makima/src/bin/makima.rs +++ b/makima/src/bin/makima.rs @@ -818,6 +818,13 @@ async fn run_directive( .await?; println!("{}", serde_json::to_string(&result.0)?); } + DirectiveCommand::Update(args) => { + let client = ApiClient::new(args.common.api_url, args.common.api_key)?; + let result = client + .directive_update(args.common.directive_id, args.pr_url, args.pr_branch) + .await?; + println!("{}", serde_json::to_string(&result.0)?); + } } Ok(()) diff --git a/makima/src/daemon/api/directive.rs b/makima/src/daemon/api/directive.rs index cd21692..5886766 100644 --- a/makima/src/daemon/api/directive.rs +++ b/makima/src/daemon/api/directive.rs @@ -134,4 +134,24 @@ impl ApiClient { let req = UpdateGoalRequest { goal: goal.to_string() }; self.put(&format!("/api/v1/directives/{}/goal", directive_id), &req).await } + + /// Update directive metadata (PR URL, PR branch, etc.) + pub async fn directive_update( + &self, + directive_id: Uuid, + pr_url: Option<String>, + pr_branch: Option<String>, + ) -> Result<JsonValue, ApiError> { + let req = UpdateDirectiveMetadataRequest { pr_url, pr_branch }; + self.put(&format!("/api/v1/directives/{}", directive_id), &req).await + } +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct UpdateDirectiveMetadataRequest { + #[serde(skip_serializing_if = "Option::is_none")] + pub pr_url: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub pr_branch: Option<String>, } diff --git a/makima/src/daemon/cli/directive.rs b/makima/src/daemon/cli/directive.rs index cd94a56..2e6ac1d 100644 --- a/makima/src/daemon/cli/directive.rs +++ b/makima/src/daemon/cli/directive.rs @@ -110,3 +110,18 @@ pub struct BatchAddStepsArgs { #[arg(long)] pub json: String, } + +/// Arguments for update command. +#[derive(Args, Debug)] +pub struct UpdateArgs { + #[command(flatten)] + pub common: DirectiveArgs, + + /// PR URL to store on the directive + #[arg(long)] + pub pr_url: Option<String>, + + /// PR branch name to store on the directive + #[arg(long)] + pub pr_branch: Option<String>, +} diff --git a/makima/src/daemon/cli/mod.rs b/makima/src/daemon/cli/mod.rs index 98923d9..bcaaa70 100644 --- a/makima/src/daemon/cli/mod.rs +++ b/makima/src/daemon/cli/mod.rs @@ -246,6 +246,9 @@ pub enum DirectiveCommand { /// Batch add multiple steps from JSON BatchAddSteps(directive::BatchAddStepsArgs), + + /// Update directive metadata (PR URL, etc.) + Update(directive::UpdateArgs), } impl Cli { diff --git a/makima/src/daemon/task/manager.rs b/makima/src/daemon/task/manager.rs index a24f527..22b41d9 100644 --- a/makima/src/daemon/task/manager.rs +++ b/makima/src/daemon/task/manager.rs @@ -5587,8 +5587,8 @@ impl TaskManagerInner { let target_repo = match target_repo_path { Some(path) => Some(crate::daemon::worktree::expand_tilde(path)), None => { - if action == "pr" { - // For PR action, check if worktree has an origin remote we can use directly + if action == "pr" || action == "branch" { + // For PR/branch action without target_repo, use origin directly None } else { tracing::warn!(task_id = %task_id, "No target_repo_path configured, skipping completion action"); @@ -5633,19 +5633,36 @@ impl TaskManagerInner { match action { "branch" => { - let target_repo = target_repo.ok_or_else(|| "No target_repo_path configured for branch action".to_string())?; - // Just push the branch to target repo - self.worktree_manager - .push_to_target_repo(worktree_path, &target_repo, &branch_name, task_name) - .await - .map_err(|e| e.to_string())?; + match target_repo { + Some(target_repo) => { + // Push branch to local target repo + self.worktree_manager + .push_to_target_repo(worktree_path, &target_repo, &branch_name, task_name) + .await + .map_err(|e| e.to_string())?; - let msg = DaemonMessage::task_output( - task_id, - format!("Branch '{}' pushed to {}\n", branch_name, target_repo.display()), - false, - ); - let _ = self.ws_tx.send(msg).await; + let msg = DaemonMessage::task_output( + task_id, + format!("Branch '{}' pushed to {}\n", branch_name, target_repo.display()), + false, + ); + let _ = self.ws_tx.send(msg).await; + } + None => { + // Push branch to origin (GitHub) + self.worktree_manager + .push_branch_to_origin(worktree_path, &branch_name, task_name) + .await + .map_err(|e| e.to_string())?; + + let msg = DaemonMessage::task_output( + task_id, + format!("Branch '{}' pushed to origin\n", branch_name), + false, + ); + let _ = self.ws_tx.send(msg).await; + } + } Ok(None) } "merge" => { diff --git a/makima/src/daemon/worktree/manager.rs b/makima/src/daemon/worktree/manager.rs index 310627c..20c93b1 100644 --- a/makima/src/daemon/worktree/manager.rs +++ b/makima/src/daemon/worktree/manager.rs @@ -1417,6 +1417,98 @@ impl WorktreeManager { Ok(()) } + /// Push a worktree branch to origin (the upstream GitHub remote). + /// Simpler than push_to_target_repo — just pushes to origin. + pub async fn push_branch_to_origin( + &self, + worktree_path: &Path, + branch_name: &str, + task_name: &str, + ) -> Result<(), WorktreeError> { + tracing::info!( + worktree = %worktree_path.display(), + branch = %branch_name, + "Pushing branch to origin" + ); + + // Stage all changes + let output = Command::new("git") + .args(["add", "-A"]) + .current_dir(worktree_path) + .output() + .await?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(WorktreeError::GitCommand(format!( + "Failed to stage changes: {}", + stderr + ))); + } + + // Check if there are staged changes to commit + let output = Command::new("git") + .args(["diff", "--cached", "--quiet"]) + .current_dir(worktree_path) + .output() + .await?; + + // Exit code 1 means there are staged changes + if !output.status.success() { + tracing::info!("Committing staged changes before push to origin"); + + let commit_message = format!("feat: {}", task_name); + let output = Command::new("git") + .args(["commit", "-m", &commit_message]) + .current_dir(worktree_path) + .output() + .await?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(WorktreeError::GitCommand(format!( + "Failed to commit changes: {}", + stderr + ))); + } + } + + // Ensure there are commits to push + let output = Command::new("git") + .args(["log", "--oneline", "-1"]) + .current_dir(worktree_path) + .output() + .await?; + + if !output.status.success() { + return Err(WorktreeError::GitCommand( + "No commits in worktree".to_string(), + )); + } + + // Push to origin + let output = Command::new("git") + .args(["push", "-u", "origin", &format!("HEAD:{}", branch_name)]) + .current_dir(worktree_path) + .output() + .await?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(WorktreeError::GitCommand(format!( + "Failed to push to origin: {}", + stderr + ))); + } + + tracing::info!( + branch = %branch_name, + "Branch pushed to origin successfully" + ); + + Ok(()) + } + /// Merge a branch into the target branch in the target repository. /// /// This pushes the branch first (if needed), then performs a merge in the target repo. diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs index 9159fd5..542339f 100644 --- a/makima/src/db/models.rs +++ b/makima/src/db/models.rs @@ -2711,6 +2711,9 @@ pub struct Directive { pub local_path: Option<String>, pub base_branch: Option<String>, pub orchestrator_task_id: Option<Uuid>, + pub pr_url: Option<String>, + pub pr_branch: Option<String>, + pub completion_task_id: Option<Uuid>, pub goal_updated_at: DateTime<Utc>, pub started_at: Option<DateTime<Utc>>, pub version: i32, @@ -2758,6 +2761,8 @@ pub struct DirectiveSummary { pub status: String, pub repository_url: Option<String>, pub orchestrator_task_id: Option<Uuid>, + pub pr_url: Option<String>, + pub completion_task_id: Option<Uuid>, pub version: i32, pub created_at: DateTime<Utc>, pub updated_at: DateTime<Utc>, @@ -2797,6 +2802,8 @@ pub struct UpdateDirectiveRequest { pub local_path: Option<String>, pub base_branch: Option<String>, pub orchestrator_task_id: Option<Uuid>, + pub pr_url: Option<String>, + pub pr_branch: Option<String>, pub version: Option<i32>, } diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs index 323e74e..358ab48 100644 --- a/makima/src/db/repository.rs +++ b/makima/src/db/repository.rs @@ -4991,7 +4991,8 @@ pub async fn list_directives_for_owner( r#" SELECT d.id, d.owner_id, d.title, d.goal, d.status, d.repository_url, - d.orchestrator_task_id, d.version, d.created_at, d.updated_at, + d.orchestrator_task_id, d.pr_url, d.completion_task_id, + d.version, d.created_at, d.updated_at, COALESCE((SELECT COUNT(*) FROM directive_steps WHERE directive_id = d.id), 0) as total_steps, COALESCE((SELECT COUNT(*) FROM directive_steps WHERE directive_id = d.id AND status = 'completed'), 0) as completed_steps, COALESCE((SELECT COUNT(*) FROM directive_steps WHERE directive_id = d.id AND status = 'running'), 0) as running_steps, @@ -5043,12 +5044,15 @@ pub async fn update_directive_for_owner( let local_path = req.local_path.as_deref().or(current.local_path.as_deref()); let base_branch = req.base_branch.as_deref().or(current.base_branch.as_deref()); let orchestrator_task_id = req.orchestrator_task_id.or(current.orchestrator_task_id); + let pr_url = req.pr_url.as_deref().or(current.pr_url.as_deref()); + let pr_branch = req.pr_branch.as_deref().or(current.pr_branch.as_deref()); let result = sqlx::query_as::<_, Directive>( r#" UPDATE directives SET title = $3, goal = $4, status = $5, repository_url = $6, local_path = $7, - base_branch = $8, orchestrator_task_id = $9, version = version + 1, updated_at = NOW() + base_branch = $8, orchestrator_task_id = $9, pr_url = $10, pr_branch = $11, + version = version + 1, updated_at = NOW() WHERE id = $1 AND owner_id = $2 RETURNING * "#, @@ -5062,6 +5066,8 @@ pub async fn update_directive_for_owner( .bind(local_path) .bind(base_branch) .bind(orchestrator_task_id) + .bind(pr_url) + .bind(pr_branch) .fetch_optional(pool) .await .map_err(RepositoryError::Database)?; @@ -5096,6 +5102,119 @@ pub async fn delete_directive_for_owner( } // ============================================================================= +// Directive Completion Helpers +// ============================================================================= + +/// Row type for completed step tasks. +#[derive(Debug, Clone, sqlx::FromRow)] +pub struct CompletedStepTask { + pub step_name: String, + pub task_id: Uuid, + pub task_name: String, +} + +/// Row type for directive completion task status check. +#[derive(Debug, Clone, sqlx::FromRow)] +pub struct DirectiveCompletionCheck { + pub directive_id: Uuid, + pub completion_task_id: Uuid, + pub task_status: String, + pub pr_url: Option<String>, +} + +/// Get idle directives that need a completion task spawned. +/// Conditions: status = 'idle', no completion_task_id, has repository_url, +/// and has at least one completed step with a task_id. +pub async fn get_idle_directives_needing_completion( + pool: &PgPool, +) -> Result<Vec<Directive>, sqlx::Error> { + sqlx::query_as::<_, Directive>( + r#" + SELECT d.* + FROM directives d + WHERE d.status = 'idle' + AND d.completion_task_id IS NULL + AND d.repository_url IS NOT NULL + AND EXISTS ( + SELECT 1 FROM directive_steps ds + WHERE ds.directive_id = d.id + AND ds.status = 'completed' + AND ds.task_id IS NOT NULL + ) + "#, + ) + .fetch_all(pool) + .await +} + +/// Get directives with active completion tasks, joined with task status. +pub async fn get_completion_tasks_to_check( + pool: &PgPool, +) -> Result<Vec<DirectiveCompletionCheck>, sqlx::Error> { + sqlx::query_as::<_, DirectiveCompletionCheck>( + r#" + SELECT d.id as directive_id, d.completion_task_id, t.status as task_status, d.pr_url + FROM directives d + JOIN tasks t ON t.id = d.completion_task_id + WHERE d.completion_task_id IS NOT NULL + "#, + ) + .fetch_all(pool) + .await +} + +/// Assign a completion task to a directive. +pub async fn assign_completion_task( + pool: &PgPool, + directive_id: Uuid, + task_id: Uuid, +) -> Result<(), sqlx::Error> { + sqlx::query( + r#"UPDATE directives SET completion_task_id = $2, updated_at = NOW() WHERE id = $1"#, + ) + .bind(directive_id) + .bind(task_id) + .execute(pool) + .await?; + Ok(()) +} + +/// Clear the completion task from a directive. +pub async fn clear_completion_task( + pool: &PgPool, + directive_id: Uuid, +) -> Result<(), sqlx::Error> { + sqlx::query( + r#"UPDATE directives SET completion_task_id = NULL, updated_at = NOW() WHERE id = $1"#, + ) + .bind(directive_id) + .execute(pool) + .await?; + Ok(()) +} + +/// Get completed step tasks for a directive (steps that have completed with an assigned task). +pub async fn get_completed_step_tasks( + pool: &PgPool, + directive_id: Uuid, +) -> Result<Vec<CompletedStepTask>, sqlx::Error> { + sqlx::query_as::<_, CompletedStepTask>( + r#" + SELECT ds.name as step_name, ds.task_id, t.name as task_name + FROM directive_steps ds + JOIN tasks t ON t.id = ds.task_id + WHERE ds.directive_id = $1 + AND ds.status = 'completed' + AND ds.task_id IS NOT NULL + ORDER BY ds.order_index, ds.created_at + "#, + ) + .bind(directive_id) + .fetch_all(pool) + .await +} + +// ============================================================================= // Directive Step CRUD // ============================================================================= 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('"', "\\\""), + ), + ) + } +} |
