summaryrefslogtreecommitdiff
path: root/makima/src/server
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/server')
-rw-r--r--makima/src/server/handlers/mesh_daemon.rs201
-rw-r--r--makima/src/server/handlers/templates.rs70
-rw-r--r--makima/src/server/state.rs10
3 files changed, 211 insertions, 70 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,
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,