summaryrefslogtreecommitdiff
path: root/makima/src/server/handlers
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/server/handlers')
-rw-r--r--makima/src/server/handlers/contract_chat.rs225
-rw-r--r--makima/src/server/handlers/contract_daemon.rs38
-rw-r--r--makima/src/server/handlers/contracts.rs130
-rw-r--r--makima/src/server/handlers/mesh_daemon.rs50
-rw-r--r--makima/src/server/handlers/templates.rs106
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)
// =============================================================================