diff options
| author | soryu <soryu@soryu.co> | 2026-01-25 04:39:37 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-25 04:39:37 +0000 |
| commit | cb4f2fc40dbabb40de948512eee74c7e46264665 (patch) | |
| tree | 0a4f142609dceaf6cf6eafa4fa6a59beff4b1f80 | |
| parent | c908854e7e3571c99cce9f46497ce5337ea0aed1 (diff) | |
| download | soryu-cb4f2fc40dbabb40de948512eee74c7e46264665.tar.gz soryu-cb4f2fc40dbabb40de948512eee74c7e46264665.zip | |
Add automatic phase transitions and fix PR creation
| -rw-r--r-- | makima/src/daemon/task/manager.rs | 124 | ||||
| -rw-r--r-- | makima/src/server/handlers/mesh_daemon.rs | 201 | ||||
| -rw-r--r-- | makima/src/server/handlers/templates.rs | 70 | ||||
| -rw-r--r-- | makima/src/server/state.rs | 10 |
4 files changed, 304 insertions, 101 deletions
diff --git a/makima/src/daemon/task/manager.rs b/makima/src/daemon/task/manager.rs index 6ba0f52..74a37bf 100644 --- a/makima/src/daemon/task/manager.rs +++ b/makima/src/daemon/task/manager.rs @@ -840,6 +840,26 @@ Valid phases: `specify`, `plan`, `execute`, `review` - Create checkpoints before major transitions - **Mark contract complete** when PR is created by updating status +## MANDATORY ACTIONS + +### [ACTION REQUIRED] Messages +When you receive an `[ACTION REQUIRED]` message from the system: +1. Read the directive carefully +2. Execute the specified command IMMEDIATELY +3. Do NOT ignore or defer these actions +4. These messages indicate conditions have been met and action is needed NOW + +### Phase Completion Checklist +After all tasks are "done" and merged, you MUST take the following actions: + +**If in execute phase:** +1. Create PR immediately: `makima supervisor pr "makima/{name}" --title "..." --base main` +2. After PR created: + - Simple contract: Mark complete with `makima supervisor complete` + - Specification contract: Advance to review with `makima supervisor advance-phase review` + +**Never leave a contract hanging** - when work is done, create the PR and complete/advance. + --- "#; @@ -2924,58 +2944,100 @@ impl TaskManager { body: Option<String>, base_branch: String, ) -> Result<(), DaemonError> { - // Get task's worktree path - let worktree_path = { + // Get task's worktree path and base branch + let (worktree_path, task_base_branch) = { let tasks = self.tasks.read().await; - tasks.get(&task_id) + let worktree = tasks.get(&task_id) .and_then(|t| t.worktree.as_ref()) - .map(|w| w.path.clone()) + .map(|w| w.path.clone()); + let base = tasks.get(&task_id) + .and_then(|t| t.base_branch.clone()); + (worktree, base) }; + // Use task's base_branch if the provided one is the default "main" and task has a detected one + let effective_base_branch = if base_branch == "main" { + task_base_branch.unwrap_or(base_branch) + } else { + base_branch + }; + + tracing::info!( + task_id = %task_id, + effective_base_branch = %effective_base_branch, + worktree_exists = worktree_path.is_some(), + "Creating PR with effective base branch" + ); + let (success, message, pr_url, pr_number) = if let Some(path) = worktree_path { // Push the current branch first + tracing::info!(path = %path.display(), "Pushing branch to origin"); let push_result = tokio::process::Command::new("git") .current_dir(&path) .args(["push", "-u", "origin", "HEAD"]) .output() .await; - if let Err(e) = push_result { - (false, format!("Failed to push branch: {}", e), None, None) - } else { - // Create PR using gh CLI - let mut pr_cmd = tokio::process::Command::new("gh"); - pr_cmd.current_dir(&path); - pr_cmd.args(["pr", "create", "--title", &title, "--base", &base_branch]); - - if let Some(ref body_text) = body { - pr_cmd.args(["--body", body_text]); - } else { - pr_cmd.args(["--body", ""]); + match push_result { + Err(e) => { + tracing::error!(error = %e, "Failed to execute git push"); + (false, format!("Failed to push branch: {}", e), None, None) } - - match pr_cmd.output().await { - Ok(output) if output.status.success() => { - let stdout = String::from_utf8_lossy(&output.stdout); - // gh pr create outputs the PR URL - let url = stdout.lines().last().map(|s| s.trim().to_string()); - // Extract PR number from URL - let number = url.as_ref().and_then(|u| { - u.split('/').last().and_then(|n| n.parse::<i32>().ok()) - }); - (true, "Pull request created".to_string(), url, number) + Ok(output) if !output.status.success() => { + let stderr = String::from_utf8_lossy(&output.stderr); + tracing::error!(stderr = %stderr, "git push failed"); + (false, format!("Failed to push branch: {}", stderr), None, None) + } + Ok(_) => { + tracing::info!("Branch pushed successfully, creating PR"); + // Create PR using gh CLI + let mut pr_cmd = tokio::process::Command::new("gh"); + pr_cmd.current_dir(&path); + pr_cmd.args(["pr", "create", "--title", &title, "--base", &effective_base_branch]); + + if let Some(ref body_text) = body { + pr_cmd.args(["--body", body_text]); + } else { + pr_cmd.args(["--body", ""]); } - Ok(output) => { - let stderr = String::from_utf8_lossy(&output.stderr); - (false, format!("Failed to create PR: {}", stderr), None, None) + + match pr_cmd.output().await { + Ok(output) if output.status.success() => { + let stdout = String::from_utf8_lossy(&output.stdout); + // gh pr create outputs the PR URL + let url = stdout.lines().last().map(|s| s.trim().to_string()); + // Extract PR number from URL + let number = url.as_ref().and_then(|u| { + u.split('/').last().and_then(|n| n.parse::<i32>().ok()) + }); + tracing::info!(pr_url = ?url, pr_number = ?number, "PR created successfully"); + (true, "Pull request created".to_string(), url, number) + } + Ok(output) => { + let stderr = String::from_utf8_lossy(&output.stderr); + tracing::error!(stderr = %stderr, "gh pr create failed"); + (false, format!("Failed to create PR: {}", stderr), None, None) + } + Err(e) => { + tracing::error!(error = %e, "Failed to execute gh command"); + (false, format!("Failed to run gh: {}", e), None, None) + } } - Err(e) => (false, format!("Failed to run gh: {}", e), None, None), } } } else { + tracing::error!(task_id = %task_id, "Task not found or has no worktree"); (false, format!("Task {} not found or has no worktree", task_id), None, None) }; + tracing::info!( + task_id = %task_id, + success = success, + message = %message, + pr_url = ?pr_url, + "PR creation completed" + ); + let msg = DaemonMessage::PRCreated { task_id, success, diff --git a/makima/src/server/handlers/mesh_daemon.rs b/makima/src/server/handlers/mesh_daemon.rs index 53ee806..f5a3c10 100644 --- a/makima/src/server/handlers/mesh_daemon.rs +++ b/makima/src/server/handlers/mesh_daemon.rs @@ -23,6 +23,7 @@ use uuid::Uuid; use crate::db::models::Task; use crate::db::repository; +use crate::llm::{check_deliverables_met, FileInfo, TaskInfo}; use crate::server::auth::{hash_api_key, API_KEY_HEADER}; use crate::server::messages::ApiError; use crate::server::state::{ @@ -447,6 +448,28 @@ pub enum DaemonMessage { /// Error message if operation failed error: Option<String>, }, + /// Response to MergeTaskToTarget command + MergeToTargetResult { + #[serde(rename = "taskId")] + task_id: Uuid, + success: bool, + message: String, + #[serde(rename = "commitSha")] + commit_sha: Option<String>, + conflicts: Option<Vec<String>>, + }, + /// Response to CreatePR command + #[serde(rename = "prCreated")] + PRCreated { + #[serde(rename = "taskId")] + task_id: Uuid, + success: bool, + message: String, + #[serde(rename = "prUrl")] + pr_url: Option<String>, + #[serde(rename = "prNumber")] + pr_number: Option<i32>, + }, } /// Validated daemon authentication result. @@ -458,6 +481,89 @@ struct DaemonAuthResult { owner_id: Uuid, } +/// Compute an action directive for the supervisor based on deliverable status. +/// Returns an [ACTION REQUIRED] message if all deliverables are met. +async fn compute_action_directive( + pool: &sqlx::PgPool, + contract_id: Uuid, + owner_id: Uuid, +) -> Option<String> { + // Get contract + let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await { + Ok(Some(c)) => c, + _ => return None, + }; + + // Get files + let files = match repository::list_files_in_contract(pool, contract_id, owner_id).await { + Ok(f) => f, + _ => return None, + }; + + // Get tasks (non-supervisor only) + let tasks = match repository::list_tasks_by_contract(pool, contract_id, owner_id).await { + Ok(t) => t.into_iter().filter(|t| !t.is_supervisor).collect::<Vec<_>>(), + _ => return None, + }; + + // Get repositories + let repos = match repository::list_contract_repositories(pool, contract_id).await { + Ok(r) => r, + _ => return None, + }; + + // Convert to FileInfo and TaskInfo for check_deliverables_met + let file_infos: Vec<FileInfo> = files + .iter() + .map(|f| FileInfo { + id: f.id, + name: f.name.clone(), + contract_phase: f.contract_phase.clone(), + }) + .collect(); + + let task_infos: Vec<TaskInfo> = tasks + .iter() + .map(|t| TaskInfo { + id: t.id, + name: t.name.clone(), + status: t.status.clone(), + }) + .collect(); + + let has_repository = !repos.is_empty(); + + // Check if any task has a PR URL set + let pr_url = tasks.iter().find_map(|t| t.pr_url.as_deref()); + + // Check deliverables + let check = check_deliverables_met( + &contract.phase, + &contract.contract_type, + &file_infos, + &task_infos, + has_repository, + pr_url, + ); + + // Only generate directive if deliverables are met and we're in execute phase + if check.deliverables_met && contract.phase == "execute" { + // All tasks done, need to create PR + if pr_url.is_none() || pr_url.unwrap_or("").is_empty() { + let done_count = task_infos.iter().filter(|t| t.status == "done").count(); + return Some(format!( + "[ACTION REQUIRED] All {} task(s) completed successfully.\n\ + You MUST now create a PR:\n\ + 1. Ensure all changes are merged to your makima branch\n\ + 2. Create PR: `makima supervisor pr \"makima/...\" --title \"...\" --base main`", + done_count + )); + } + } + + None +} + /// Validate an API key and return (user_id, owner_id). async fn validate_daemon_api_key(pool: &sqlx::PgPool, key: &str) -> Result<DaemonAuthResult, String> { let key_hash = hash_api_key(key); @@ -946,6 +1052,13 @@ async fn handle_daemon_connection(socket: WebSocket, state: SharedState, auth_re // Don't notify for supervisor tasks (they don't report to themselves) if !updated_task.is_supervisor { if let Ok(Some(supervisor)) = repository::get_contract_supervisor_task(&pool, contract_id).await { + // Compute action directive if task completed successfully + let action_directive = if updated_task.status == "done" { + compute_action_directive(&pool, contract_id, owner_id).await + } else { + None + }; + state.notify_supervisor_of_task_completion( supervisor.id, supervisor.daemon_id, @@ -954,6 +1067,7 @@ async fn handle_daemon_connection(socket: WebSocket, state: SharedState, auth_re &updated_task.status, updated_task.progress_summary.as_deref(), updated_task.error_message.as_deref(), + action_directive.as_deref(), ).await; } } @@ -1561,6 +1675,93 @@ async fn handle_daemon_connection(socket: WebSocket, state: SharedState, auth_re }); } } + Ok(DaemonMessage::MergeToTargetResult { + task_id, + success, + message, + commit_sha: _, + conflicts: _, + }) => { + tracing::info!( + task_id = %task_id, + success = success, + "Merge to target result received" + ); + + // On successful merge, notify supervisor to check if all merges complete + if success { + if let Some(pool) = state.db_pool.as_ref() { + if let Ok(Some(task)) = repository::get_task(pool, task_id).await { + if let Some(contract_id) = task.contract_id { + if let Ok(Some(supervisor)) = repository::get_contract_supervisor_task(pool, contract_id).await { + let prompt = format!( + "[INFO] Merge completed: {}\n\ + Check if all tasks are merged with `makima supervisor tasks`.\n\ + If ready, create PR with `makima supervisor pr`.", + message + ); + let _ = state.notify_supervisor( + supervisor.id, + supervisor.daemon_id, + &prompt, + ).await; + } + } + } + } + } + } + Ok(DaemonMessage::PRCreated { + task_id, + success, + message, + pr_url, + pr_number: _, + }) => { + tracing::info!( + task_id = %task_id, + success = success, + pr_url = ?pr_url, + "PR created result received" + ); + + // On successful PR creation, notify supervisor of next steps + if success { + if let Some(pool) = state.db_pool.as_ref() { + if let Ok(Some(task)) = repository::get_task(pool, task_id).await { + if let Some(contract_id) = task.contract_id { + // Get contract to determine next action + if let Ok(Some(contract)) = repository::get_contract_for_owner(pool, contract_id, task.owner_id).await { + if let Ok(Some(supervisor)) = repository::get_contract_supervisor_task(pool, contract_id).await { + let next_action = match (contract.contract_type.as_str(), contract.phase.as_str()) { + ("simple", "execute") => { + "Mark contract complete with `makima supervisor complete`".to_string() + } + ("specification", "execute") => { + "Advance to review phase with `makima supervisor advance-phase review`".to_string() + } + _ => "Check contract status with `makima supervisor status`".to_string() + }; + + let prompt = format!( + "[ACTION REQUIRED] PR created successfully!\n\ + PR: {}\n\n\ + Next step: {}", + pr_url.as_deref().unwrap_or(&message), + next_action + ); + let _ = state.notify_supervisor( + supervisor.id, + supervisor.daemon_id, + &prompt, + ).await; + } + } + } + } + } + } + } Ok(DaemonMessage::GitConfigInherited { success, user_email, diff --git a/makima/src/server/handlers/templates.rs b/makima/src/server/handlers/templates.rs index 5cad44f..6d95e86 100644 --- a/makima/src/server/handlers/templates.rs +++ b/makima/src/server/handlers/templates.rs @@ -7,76 +7,6 @@ use utoipa::ToSchema; use crate::llm::templates; use crate::llm::templates::ContractTypeTemplate; -// ============================================================================= -// Contract Type Templates -// ============================================================================= - -/// Contract type template for API response -#[derive(Debug, Serialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct ContractTypeTemplate { - /// Template identifier (e.g., "simple", "specification") - pub id: String, - /// Display name - pub name: String, - /// Description of the contract type workflow - pub description: String, - /// Ordered list of phases for this contract type - pub phases: Vec<String>, - /// Default starting phase - pub default_phase: String, - /// Whether this is a built-in type (always available) - pub is_builtin: bool, -} - -/// Response for listing contract types -#[derive(Debug, Serialize, ToSchema)] -pub struct ListContractTypesResponse { - pub types: Vec<ContractTypeTemplate>, -} - -/// List available contract type templates -#[utoipa::path( - get, - path = "/api/v1/contract-types", - responses( - (status = 200, description = "Contract types retrieved successfully", body = ListContractTypesResponse) - ), - tag = "contract-types" -)] -pub async fn list_contract_types() -> impl IntoResponse { - let types = vec![ - ContractTypeTemplate { - id: "simple".to_string(), - name: "Simple".to_string(), - description: "Plan \u{2192} Execute: Simple workflow with a plan document".to_string(), - phases: vec!["plan".to_string(), "execute".to_string()], - default_phase: "plan".to_string(), - is_builtin: true, - }, - ContractTypeTemplate { - id: "specification".to_string(), - name: "Specification".to_string(), - description: "Research \u{2192} Specify \u{2192} Plan \u{2192} Execute \u{2192} Review: Full specification-driven development with TDD".to_string(), - phases: vec![ - "research".to_string(), - "specify".to_string(), - "plan".to_string(), - "execute".to_string(), - "review".to_string(), - ], - default_phase: "research".to_string(), - is_builtin: true, - }, - ]; - - ( - StatusCode::OK, - Json(ListContractTypesResponse { types }), - ) - .into_response() -} - /// Query parameters for listing templates #[derive(Debug, Deserialize, ToSchema)] pub struct ListTemplatesQuery { diff --git a/makima/src/server/state.rs b/makima/src/server/state.rs index 32c0af3..b954efe 100644 --- a/makima/src/server/state.rs +++ b/makima/src/server/state.rs @@ -1065,6 +1065,9 @@ impl AppState { } /// Format and send a task completion notification to a supervisor. + /// + /// If `action_directive` is provided, it will be appended to the message + /// as an [ACTION REQUIRED] block to prompt the supervisor to take action. pub async fn notify_supervisor_of_task_completion( &self, supervisor_task_id: Uuid, @@ -1074,6 +1077,7 @@ impl AppState { status: &str, progress_summary: Option<&str>, error_message: Option<&str>, + action_directive: Option<&str>, ) { let mut message = format!( "TASK_COMPLETED task_id={} name=\"{}\" status={}", @@ -1091,6 +1095,12 @@ impl AppState { message.push_str(&format!(" error=\"{}\"", escaped)); } + // Append action directive if provided + if let Some(directive) = action_directive { + message.push_str("\n\n"); + message.push_str(directive); + } + if let Err(e) = self.notify_supervisor( supervisor_task_id, supervisor_daemon_id, |
