diff options
Diffstat (limited to 'makima/src/server/handlers')
| -rw-r--r-- | makima/src/server/handlers/contract_chat.rs | 225 | ||||
| -rw-r--r-- | makima/src/server/handlers/contract_daemon.rs | 38 | ||||
| -rw-r--r-- | makima/src/server/handlers/contracts.rs | 130 | ||||
| -rw-r--r-- | makima/src/server/handlers/mesh_daemon.rs | 50 | ||||
| -rw-r--r-- | makima/src/server/handlers/templates.rs | 106 |
5 files changed, 245 insertions, 304 deletions
diff --git a/makima/src/server/handlers/contract_chat.rs b/makima/src/server/handlers/contract_chat.rs index 28c3436..e035368 100644 --- a/makima/src/server/handlers/contract_chat.rs +++ b/makima/src/server/handlers/contract_chat.rs @@ -19,11 +19,11 @@ use crate::db::{ repository, }; use crate::llm::{ - all_templates, analyze_task_output, body_to_markdown, format_checklist_markdown, + analyze_task_output, body_to_markdown, format_checklist_markdown, format_parsed_tasks, parse_tasks_from_breakdown, claude::{self, ClaudeClient, ClaudeError, ClaudeModel}, groq::{GroqClient, GroqError, Message, ToolCallResponse}, - parse_contract_tool_call, templates_for_phase, ContractToolRequest, FileInfo, + parse_contract_tool_call, ContractToolRequest, LlmModel, TaskInfo, ToolCall, ToolResult, UserQuestion, CONTRACT_TOOLS, format_transcript_for_analysis, calculate_speaker_stats, build_analysis_prompt, parse_analysis_response, @@ -441,36 +441,29 @@ fn build_contract_context(contract: &crate::db::models::ContractWithRelations) - context.push_str(&format!("Description: {}\n", desc)); } - // Build phase checklist - let file_infos: Vec<FileInfo> = contract.files.iter().map(|f| FileInfo { - id: f.id, - name: f.name.clone(), - contract_phase: f.contract_phase.clone(), - }).collect(); + // Get completed deliverables for the current phase + let completed_deliverables = c.get_completed_deliverables(&c.phase); + // Build task infos for checklist let task_infos: Vec<TaskInfo> = contract.tasks.iter().map(|t| TaskInfo { - id: t.id, name: t.name.clone(), status: t.status.clone(), }).collect(); let has_repository = !contract.repositories.is_empty(); - let phase_checklist = crate::llm::get_phase_checklist_for_type(&c.phase, &file_infos, &task_infos, has_repository, &c.contract_type); + let phase_checklist = crate::llm::get_phase_checklist_for_type(&c.phase, &completed_deliverables, &task_infos, has_repository, &c.contract_type); // Add phase checklist to context context.push_str("\n"); context.push_str(&format_checklist_markdown(&phase_checklist)); // Add deliverable check result for phase transition readiness - // Note: pr_url is not available in TaskSummary, so we pass None here - // Full PR checking should be done via the check_deliverables_met tool let deliverable_check = crate::llm::check_deliverables_met( &c.phase, &c.contract_type, - &file_infos, + &completed_deliverables, &task_infos, has_repository, - None, // pr_url not available in TaskSummary ); // Add deliverable prompt guidance @@ -1204,23 +1197,7 @@ async fn handle_contract_request( } } - ContractToolRequest::CreateFileFromTemplate { - template_id, - name, - description, - } => { - // Find the template - let templates = all_templates(); - let template = templates.iter().find(|t| t.id == template_id); - - let Some(template) = template else { - return ContractRequestResult { - success: false, - message: format!("Template '{}' not found", template_id), - data: None, - }; - }; - + ContractToolRequest::CreateEmptyFile { name, description } => { // Verify contract exists and get current phase let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await { Ok(Some(c)) => c, @@ -1240,32 +1217,25 @@ async fn handle_contract_request( } }; - // Use template's phase if available, otherwise use contract's current phase - let contract_phase = Some(template.phase.clone()).or(Some(contract.phase.clone())); - - // Create the file (contract_id is now required) + // Create the file with current contract phase let create_req = crate::db::models::CreateFileRequest { contract_id, name: Some(name.clone()), description, - body: template.suggested_body.clone(), + body: Vec::new(), transcript: Vec::new(), location: None, repo_file_path: None, - contract_phase, + contract_phase: Some(contract.phase.clone()), }; match repository::create_file_for_owner(pool, owner_id, create_req).await { Ok(file) => ContractRequestResult { success: true, - message: format!( - "Created file '{}' from template '{}'", - name, template.name - ), + message: format!("Created empty file '{}'", name), data: Some(json!({ "fileId": file.id, "name": file.name, - "templateId": template_id, })), }, Err(e) => ContractRequestResult { @@ -1276,8 +1246,11 @@ async fn handle_contract_request( } } - ContractToolRequest::CreateEmptyFile { name, description } => { - // Verify contract exists and get current phase + ContractToolRequest::MarkDeliverableComplete { + deliverable_id, + phase, + } => { + // Get the contract to determine current phase and contract type let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await { Ok(Some(c)) => c, Ok(None) => { @@ -1296,61 +1269,60 @@ async fn handle_contract_request( } }; - // Create the file with current contract phase - let create_req = crate::db::models::CreateFileRequest { - contract_id, - name: Some(name.clone()), - description, - body: Vec::new(), - transcript: Vec::new(), - location: None, - repo_file_path: None, - contract_phase: Some(contract.phase.clone()), - }; + // Use specified phase or default to current contract phase + let target_phase = phase.unwrap_or_else(|| contract.phase.clone()); - match repository::create_file_for_owner(pool, owner_id, create_req).await { - Ok(file) => ContractRequestResult { + // Validate the deliverable ID exists for this phase/contract type + let phase_deliverables = crate::llm::get_phase_deliverables_for_type(&target_phase, &contract.contract_type); + let deliverable_exists = phase_deliverables.deliverables.iter().any(|d| d.id == deliverable_id); + + if !deliverable_exists { + let valid_ids: Vec<&str> = phase_deliverables.deliverables.iter().map(|d| d.id.as_str()).collect(); + return ContractRequestResult { + success: false, + message: format!( + "Invalid deliverable_id '{}' for {} phase. Valid IDs: {:?}", + deliverable_id, target_phase, valid_ids + ), + data: None, + }; + } + + // Check if already completed + if contract.is_deliverable_complete(&target_phase, &deliverable_id) { + return ContractRequestResult { success: true, - message: format!("Created empty file '{}'", name), + message: format!("Deliverable '{}' is already marked complete for {} phase", deliverable_id, target_phase), data: Some(json!({ - "fileId": file.id, - "name": file.name, + "deliverableId": deliverable_id, + "phase": target_phase, + "alreadyComplete": true, })), - }, + }; + } + + // Mark the deliverable as complete + match repository::mark_deliverable_complete(pool, contract_id, &target_phase, &deliverable_id).await { + Ok(updated_contract) => { + let completed = updated_contract.get_completed_deliverables(&target_phase); + ContractRequestResult { + success: true, + message: format!("Marked deliverable '{}' as complete for {} phase", deliverable_id, target_phase), + data: Some(json!({ + "deliverableId": deliverable_id, + "phase": target_phase, + "completedDeliverables": completed, + })), + } + } Err(e) => ContractRequestResult { success: false, - message: format!("Failed to create file: {}", e), + message: format!("Failed to mark deliverable complete: {}", e), data: None, }, } } - ContractToolRequest::ListAvailableTemplates { phase } => { - let templates = if let Some(p) = phase { - templates_for_phase(&p) - } else { - all_templates() - }; - - let template_data: Vec<serde_json::Value> = templates - .iter() - .map(|t| { - json!({ - "id": t.id, - "name": t.name, - "phase": t.phase, - "description": t.description, - }) - }) - .collect(); - - ContractRequestResult { - success: true, - message: format!("Found {} templates", templates.len()), - data: Some(json!({ "templates": template_data })), - } - } - ContractToolRequest::CreateContractTask { name, plan, @@ -1666,8 +1638,8 @@ async fn handle_contract_request( }; let phase_info = get_phase_description(&contract.phase); - let templates = templates_for_phase(&contract.phase); - let template_names: Vec<String> = templates.iter().map(|t| t.name.clone()).collect(); + let phase_deliverables = crate::llm::get_phase_deliverables_for_type(&contract.phase, &contract.contract_type); + let deliverable_names: Vec<String> = phase_deliverables.deliverables.iter().map(|d| d.name.clone()).collect(); ContractRequestResult { success: true, @@ -1676,7 +1648,8 @@ async fn handle_contract_request( "phase": contract.phase, "description": phase_info.0, "activities": phase_info.1, - "suggestedTemplates": template_names, + "deliverables": deliverable_names, + "guidance": phase_deliverables.guidance, "nextPhase": get_next_phase(&contract.phase), })), } @@ -1764,30 +1737,22 @@ async fn handle_contract_request( } }; - let file_infos: Vec<FileInfo> = cwr.files.iter().map(|f| FileInfo { - id: f.id, - name: f.name.clone(), - contract_phase: f.contract_phase.clone(), - }).collect(); + // Get completed deliverables for the current phase + let completed_deliverables = cwr.contract.get_completed_deliverables(current_phase); let task_infos: Vec<TaskInfo> = cwr.tasks.iter().map(|t| TaskInfo { - id: t.id, name: t.name.clone(), status: t.status.clone(), }).collect(); let has_repository = !cwr.repositories.is_empty(); - // Note: pr_url is not available in TaskSummary, so we skip PR checking here - // For simple contracts, the PR deliverable check will need to be done - // by fetching full task details if needed let check_result = crate::llm::check_deliverables_met( current_phase, &contract.contract_type, - &file_infos, + &completed_deliverables, &task_infos, has_repository, - None, // pr_url not available in TaskSummary ); // Block transition if deliverables are not met @@ -1895,17 +1860,17 @@ async fn handle_contract_request( match repository::change_contract_phase_for_owner(pool, contract_id, owner_id, &new_phase).await { Ok(Some(updated)) => { // Get deliverables for the new phase (using contract type) - let deliverables = crate::llm::get_phase_deliverables_for_type(&new_phase, &contract.contract_type); + let phase_deliverables = crate::llm::get_phase_deliverables_for_type(&new_phase, &contract.contract_type); - // Build suggested files list - let suggested_files: Vec<serde_json::Value> = deliverables - .recommended_files + // Build deliverables list + let deliverables_list: Vec<serde_json::Value> = phase_deliverables + .deliverables .iter() - .map(|f| json!({ - "templateId": f.template_id, - "name": f.name_suggestion, - "priority": format!("{:?}", f.priority).to_lowercase(), - "description": f.description, + .map(|d| json!({ + "id": d.id, + "name": d.name, + "priority": format!("{:?}", d.priority).to_lowercase(), + "description": d.description, })) .collect(); @@ -1913,16 +1878,16 @@ async fn handle_contract_request( success: true, message: format!( "Advanced contract from '{}' to '{}' phase. {}", - current_phase, new_phase, deliverables.guidance + current_phase, new_phase, phase_deliverables.guidance ), data: Some(json!({ "status": "advanced", "previousPhase": current_phase, "newPhase": updated.phase, - "phaseGuidance": deliverables.guidance, - "suggestedFiles": suggested_files, - "requiresRepository": deliverables.requires_repository, - "requiresTasks": deliverables.requires_tasks, + "phaseGuidance": phase_deliverables.guidance, + "deliverables": deliverables_list, + "requiresRepository": phase_deliverables.requires_repository, + "requiresTasks": phase_deliverables.requires_tasks, })), } }, @@ -2028,20 +1993,15 @@ async fn handle_contract_request( ContractToolRequest::GetPhaseChecklist => { match get_contract_with_relations(pool, contract_id, owner_id).await { Ok(Some(cwr)) => { - let file_infos: Vec<FileInfo> = cwr.files.iter().map(|f| FileInfo { - id: f.id, - name: f.name.clone(), - contract_phase: f.contract_phase.clone(), - }).collect(); + let completed_deliverables = cwr.contract.get_completed_deliverables(&cwr.contract.phase); let task_infos: Vec<TaskInfo> = cwr.tasks.iter().map(|t| TaskInfo { - id: t.id, name: t.name.clone(), status: t.status.clone(), }).collect(); let has_repository = !cwr.repositories.is_empty(); - let checklist = crate::llm::get_phase_checklist_for_type(&cwr.contract.phase, &file_infos, &task_infos, has_repository, &cwr.contract.contract_type); + let checklist = crate::llm::get_phase_checklist_for_type(&cwr.contract.phase, &completed_deliverables, &task_infos, has_repository, &cwr.contract.contract_type); ContractRequestResult { success: true, @@ -2049,7 +2009,7 @@ async fn handle_contract_request( data: Some(json!({ "phase": checklist.phase, "completionPercentage": checklist.completion_percentage, - "deliverables": checklist.file_deliverables, + "deliverables": checklist.deliverables, "hasRepository": checklist.has_repository, "repositoryRequired": checklist.repository_required, "taskStats": checklist.task_stats, @@ -2074,41 +2034,30 @@ async fn handle_contract_request( ContractToolRequest::CheckDeliverablesMet => { match get_contract_with_relations(pool, contract_id, owner_id).await { Ok(Some(cwr)) => { - let file_infos: Vec<FileInfo> = cwr.files.iter().map(|f| FileInfo { - id: f.id, - name: f.name.clone(), - contract_phase: f.contract_phase.clone(), - }).collect(); + let completed_deliverables = cwr.contract.get_completed_deliverables(&cwr.contract.phase); let task_infos: Vec<TaskInfo> = cwr.tasks.iter().map(|t| TaskInfo { - id: t.id, name: t.name.clone(), status: t.status.clone(), }).collect(); let has_repository = !cwr.repositories.is_empty(); - // Note: pr_url is not available in TaskSummary - // For simple contracts needing PR checking, full task details would need to be fetched - // For now, we pass None and the LLM can guide the user to ensure a PR exists - let check_result = crate::llm::check_deliverables_met( &cwr.contract.phase, &cwr.contract.contract_type, - &file_infos, + &completed_deliverables, &task_infos, has_repository, - None, // pr_url not available in TaskSummary ); // Check if we should auto-progress let auto_progress = crate::llm::should_auto_progress( &cwr.contract.phase, &cwr.contract.contract_type, - &file_infos, + &completed_deliverables, &task_infos, has_repository, - None, // pr_url not available in TaskSummary cwr.contract.autonomous_loop, ); diff --git a/makima/src/server/handlers/contract_daemon.rs b/makima/src/server/handlers/contract_daemon.rs index 5b23831..5f56f06 100644 --- a/makima/src/server/handlers/contract_daemon.rs +++ b/makima/src/server/handlers/contract_daemon.rs @@ -15,7 +15,7 @@ use utoipa::ToSchema; use uuid::Uuid; use crate::db::{models::FileSummary, repository}; -use crate::llm::phase_guidance::{self, FileInfo, PhaseChecklist, TaskInfo}; +use crate::llm::phase_guidance::{self, PhaseChecklist, TaskInfo}; use crate::server::auth::Authenticated; use crate::server::messages::ApiError; use crate::server::state::SharedState; @@ -242,28 +242,14 @@ pub async fn get_contract_checklist( } }; - // Get files for this contract - let files = match repository::list_files_in_contract(pool, id, auth.owner_id).await { - Ok(f) => f - .into_iter() - .map(|f| FileInfo { - id: f.id, - name: f.name, - contract_phase: f.contract_phase, - }) - .collect::<Vec<_>>(), - Err(e) => { - tracing::warn!("Failed to get files for contract {}: {}", id, e); - Vec::new() - } - }; + // Get completed deliverables for the current phase + let completed_deliverables = contract.get_completed_deliverables(&contract.phase); // Get tasks for this contract let tasks = match repository::list_tasks_in_contract(pool, id, auth.owner_id).await { Ok(t) => t .into_iter() .map(|t| TaskInfo { - id: t.id, name: t.name, status: t.status, }) @@ -280,7 +266,7 @@ pub async fn get_contract_checklist( Err(_) => false, }; - let checklist = phase_guidance::get_phase_checklist_for_type(&contract.phase, &files, &tasks, has_repository, &contract.contract_type); + let checklist = phase_guidance::get_phase_checklist_for_type(&contract.phase, &completed_deliverables, &tasks, has_repository, &contract.contract_type); Json(checklist).into_response() } @@ -463,24 +449,14 @@ pub async fn get_suggest_action( } }; - // Get files and tasks for checklist - let files = repository::list_files_in_contract(pool, id, auth.owner_id) - .await - .unwrap_or_default() - .into_iter() - .map(|f| FileInfo { - id: f.id, - name: f.name, - contract_phase: f.contract_phase, - }) - .collect::<Vec<_>>(); + // Get completed deliverables and tasks for checklist + let completed_deliverables = contract.get_completed_deliverables(&contract.phase); let tasks = repository::list_tasks_in_contract(pool, id, auth.owner_id) .await .unwrap_or_default() .into_iter() .map(|t| TaskInfo { - id: t.id, name: t.name, status: t.status, }) @@ -491,7 +467,7 @@ pub async fn get_suggest_action( .map(|r| !r.is_empty()) .unwrap_or(false); - let checklist = phase_guidance::get_phase_checklist_for_type(&contract.phase, &files, &tasks, has_repository, &contract.contract_type); + let checklist = phase_guidance::get_phase_checklist_for_type(&contract.phase, &completed_deliverables, &tasks, has_repository, &contract.contract_type); // Determine suggested action based on checklist let (action, description) = if !checklist.suggestions.is_empty() { diff --git a/makima/src/server/handlers/contracts.rs b/makima/src/server/handlers/contracts.rs index f16f33d..de3164c 100644 --- a/makima/src/server/handlers/contracts.rs +++ b/makima/src/server/handlers/contracts.rs @@ -6,6 +6,8 @@ use axum::{ response::IntoResponse, Json, }; +use serde::Deserialize; +use utoipa::ToSchema; use uuid::Uuid; use crate::db::models::{ @@ -1423,6 +1425,134 @@ pub async fn change_phase( } // ============================================================================= +// Deliverables +// ============================================================================= + +/// Request body for marking a deliverable complete +#[derive(Debug, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct MarkDeliverableRequest { + /// The deliverable ID to mark as complete (e.g., 'plan-document', 'pull-request') + pub deliverable_id: String, + /// Phase the deliverable belongs to. Defaults to current contract phase if not specified. + pub phase: Option<String>, +} + +/// Mark a deliverable as complete for a contract phase. +#[utoipa::path( + post, + path = "/api/v1/contracts/{id}/deliverables/complete", + params( + ("id" = Uuid, Path, description = "Contract ID") + ), + request_body = MarkDeliverableRequest, + responses( + (status = 200, description = "Deliverable marked complete", body = serde_json::Value), + (status = 400, description = "Invalid deliverable ID", body = ApiError), + (status = 401, description = "Unauthorized", body = ApiError), + (status = 404, description = "Contract not found", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError), + ), + security( + ("bearer_auth" = []), + ("api_key" = []) + ), + tag = "Contracts" +)] +pub async fn mark_deliverable_complete( + State(state): State<SharedState>, + Authenticated(auth): Authenticated, + Path(id): Path<Uuid>, + Json(req): Json<MarkDeliverableRequest>, +) -> impl IntoResponse { + let Some(ref pool) = state.db_pool else { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), + ) + .into_response(); + }; + + // Get contract + let contract = match repository::get_contract_for_owner(pool, id, auth.owner_id).await { + Ok(Some(c)) => c, + Ok(None) => { + return ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "Contract not found")), + ) + .into_response(); + } + Err(e) => { + tracing::error!("Failed to get contract {}: {}", id, e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response(); + } + }; + + // Use specified phase or default to current contract phase + let target_phase = req.phase.unwrap_or_else(|| contract.phase.clone()); + + // Validate the deliverable ID exists for this phase/contract type + let phase_deliverables = crate::llm::get_phase_deliverables_for_type(&target_phase, &contract.contract_type); + let deliverable = phase_deliverables.deliverables.iter().find(|d| d.id == req.deliverable_id); + + if deliverable.is_none() { + let valid_ids: Vec<&str> = phase_deliverables.deliverables.iter().map(|d| d.id.as_str()).collect(); + return ( + StatusCode::BAD_REQUEST, + Json(ApiError::new( + "INVALID_DELIVERABLE", + format!( + "Invalid deliverable_id '{}' for {} phase (contract type: {}). Valid IDs: {:?}", + req.deliverable_id, target_phase, contract.contract_type, valid_ids + ), + )), + ) + .into_response(); + } + + // Check if already completed + if contract.is_deliverable_complete(&target_phase, &req.deliverable_id) { + return Json(serde_json::json!({ + "success": true, + "message": format!("Deliverable '{}' is already marked complete for {} phase", req.deliverable_id, target_phase), + "deliverableId": req.deliverable_id, + "phase": target_phase, + "alreadyComplete": true, + })) + .into_response(); + } + + // Mark the deliverable as complete + match repository::mark_deliverable_complete(pool, id, &target_phase, &req.deliverable_id).await { + Ok(updated_contract) => { + let completed = updated_contract.get_completed_deliverables(&target_phase); + Json(serde_json::json!({ + "success": true, + "message": format!("Marked deliverable '{}' as complete for {} phase", req.deliverable_id, target_phase), + "deliverableId": req.deliverable_id, + "phase": target_phase, + "completedDeliverables": completed, + })) + .into_response() + } + Err(e) => { + tracing::error!("Failed to mark deliverable complete for contract {}: {}", id, e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response() + } + } +} + +// ============================================================================= // Events // ============================================================================= diff --git a/makima/src/server/handlers/mesh_daemon.rs b/makima/src/server/handlers/mesh_daemon.rs index f5a3c10..270118f 100644 --- a/makima/src/server/handlers/mesh_daemon.rs +++ b/makima/src/server/handlers/mesh_daemon.rs @@ -23,7 +23,7 @@ use uuid::Uuid; use crate::db::models::Task; use crate::db::repository; -use crate::llm::{check_deliverables_met, FileInfo, TaskInfo}; +use crate::llm::{check_deliverables_met, TaskInfo}; use crate::server::auth::{hash_api_key, API_KEY_HEADER}; use crate::server::messages::ApiError; use crate::server::state::{ @@ -494,12 +494,6 @@ async fn compute_action_directive( _ => 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<_>>(), @@ -512,20 +506,12 @@ async fn compute_action_directive( _ => 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(); + // Get completed deliverables for the current phase + let completed_deliverables = contract.get_completed_deliverables(&contract.phase); let task_infos: Vec<TaskInfo> = tasks .iter() .map(|t| TaskInfo { - id: t.id, name: t.name.clone(), status: t.status.clone(), }) @@ -533,29 +519,29 @@ async fn compute_action_directive( 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( + // Check deliverables (unused, but kept for future reference) + let _check = check_deliverables_met( &contract.phase, &contract.contract_type, - &file_infos, + &completed_deliverables, &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(); + // Generate directive based on deliverable status + if contract.phase == "execute" { + // Check if all tasks are done but PR deliverable is not marked complete + let all_tasks_done = !task_infos.is_empty() + && task_infos.iter().all(|t| t.status == "done"); + let pr_deliverable_complete = completed_deliverables.contains(&"pull-request".to_string()); + + if all_tasks_done && !pr_deliverable_complete { + let done_count = task_infos.len(); 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`", + You MUST now create a PR and mark the 'pull-request' deliverable as complete:\n\ + 1. Ensure all changes are merged to your branch\n\ + 2. Create PR and then call mark_deliverable_complete with deliverable_id='pull-request'", done_count )); } diff --git a/makima/src/server/handlers/templates.rs b/makima/src/server/handlers/templates.rs index 6d95e86..c73007e 100644 --- a/makima/src/server/handlers/templates.rs +++ b/makima/src/server/handlers/templates.rs @@ -1,112 +1,12 @@ -//! Templates API handler. +//! Contract types API handler. -use axum::{extract::Query, http::StatusCode, response::IntoResponse, Json}; -use serde::{Deserialize, Serialize}; +use axum::{http::StatusCode, response::IntoResponse, Json}; +use serde::Serialize; use utoipa::ToSchema; use crate::llm::templates; use crate::llm::templates::ContractTypeTemplate; -/// Query parameters for listing templates -#[derive(Debug, Deserialize, ToSchema)] -pub struct ListTemplatesQuery { - /// Filter by contract phase (research, specify, plan, execute, review) - pub phase: Option<String>, -} - -/// Template summary for API response -#[derive(Debug, Serialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct TemplateSummary { - /// Template identifier - pub id: String, - /// Display name - pub name: String, - /// Contract phase this template is designed for - pub phase: String, - /// Brief description - pub description: String, - /// Number of body elements in the template - pub element_count: usize, -} - -/// Response for listing templates -#[derive(Debug, Serialize, ToSchema)] -pub struct ListTemplatesResponse { - pub templates: Vec<TemplateSummary>, -} - -/// List available file templates -#[utoipa::path( - get, - path = "/api/v1/templates", - params( - ("phase" = Option<String>, Query, description = "Filter by contract phase") - ), - responses( - (status = 200, description = "Templates retrieved successfully", body = ListTemplatesResponse) - ), - tag = "templates" -)] -pub async fn list_templates( - Query(query): Query<ListTemplatesQuery>, -) -> impl IntoResponse { - let template_list = match query.phase.as_deref() { - Some(phase) => templates::templates_for_phase(phase), - None => templates::all_templates(), - }; - - let summaries: Vec<TemplateSummary> = template_list - .iter() - .map(|t| TemplateSummary { - id: t.id.clone(), - name: t.name.clone(), - phase: t.phase.clone(), - description: t.description.clone(), - element_count: t.suggested_body.len(), - }) - .collect(); - - ( - StatusCode::OK, - Json(ListTemplatesResponse { - templates: summaries, - }), - ) - .into_response() -} - -/// Get a specific template by ID -#[utoipa::path( - get, - path = "/api/v1/templates/{id}", - params( - ("id" = String, Path, description = "Template ID") - ), - responses( - (status = 200, description = "Template retrieved successfully", body = templates::FileTemplate), - (status = 404, description = "Template not found") - ), - tag = "templates" -)] -pub async fn get_template( - axum::extract::Path(id): axum::extract::Path<String>, -) -> impl IntoResponse { - let all = templates::all_templates(); - let template = all.into_iter().find(|t| t.id == id); - - match template { - Some(t) => (StatusCode::OK, Json(serde_json::json!(t))).into_response(), - None => ( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ - "error": format!("Template '{}' not found", id) - })), - ) - .into_response(), - } -} - // ============================================================================= // Contract Type Templates (Workflow Definitions) // ============================================================================= |
