summaryrefslogtreecommitdiff
path: root/makima/src/server/handlers/mesh_daemon.rs
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-25 04:39:37 +0000
committersoryu <soryu@soryu.co>2026-01-25 04:39:37 +0000
commitcb4f2fc40dbabb40de948512eee74c7e46264665 (patch)
tree0a4f142609dceaf6cf6eafa4fa6a59beff4b1f80 /makima/src/server/handlers/mesh_daemon.rs
parentc908854e7e3571c99cce9f46497ce5337ea0aed1 (diff)
downloadsoryu-cb4f2fc40dbabb40de948512eee74c7e46264665.tar.gz
soryu-cb4f2fc40dbabb40de948512eee74c7e46264665.zip
Add automatic phase transitions and fix PR creation
Diffstat (limited to 'makima/src/server/handlers/mesh_daemon.rs')
-rw-r--r--makima/src/server/handlers/mesh_daemon.rs201
1 files changed, 201 insertions, 0 deletions
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,