From cb4f2fc40dbabb40de948512eee74c7e46264665 Mon Sep 17 00:00:00 2001 From: soryu Date: Sun, 25 Jan 2026 04:39:37 +0000 Subject: Add automatic phase transitions and fix PR creation --- makima/src/server/handlers/mesh_daemon.rs | 201 ++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) (limited to 'makima/src/server/handlers/mesh_daemon.rs') 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, }, + /// Response to MergeTaskToTarget command + MergeToTargetResult { + #[serde(rename = "taskId")] + task_id: Uuid, + success: bool, + message: String, + #[serde(rename = "commitSha")] + commit_sha: Option, + conflicts: Option>, + }, + /// Response to CreatePR command + #[serde(rename = "prCreated")] + PRCreated { + #[serde(rename = "taskId")] + task_id: Uuid, + success: bool, + message: String, + #[serde(rename = "prUrl")] + pr_url: Option, + #[serde(rename = "prNumber")] + pr_number: Option, + }, } /// 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 { + // 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::>(), + _ => 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 = files + .iter() + .map(|f| FileInfo { + id: f.id, + name: f.name.clone(), + contract_phase: f.contract_phase.clone(), + }) + .collect(); + + let task_infos: Vec = 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 { 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, -- cgit v1.2.3