diff options
Diffstat (limited to 'makima/src')
| -rw-r--r-- | makima/src/db/models.rs | 60 | ||||
| -rw-r--r-- | makima/src/db/repository.rs | 15 | ||||
| -rw-r--r-- | makima/src/llm/contract_tools.rs | 34 | ||||
| -rw-r--r-- | makima/src/server/handlers/contract_chat.rs | 87 | ||||
| -rw-r--r-- | makima/src/server/handlers/contracts.rs | 112 | ||||
| -rw-r--r-- | makima/src/server/handlers/mesh.rs | 1 | ||||
| -rw-r--r-- | makima/src/server/handlers/transcript_analysis.rs | 1 |
7 files changed, 288 insertions, 22 deletions
diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs index 72ba6f2..99c8b8e 100644 --- a/makima/src/db/models.rs +++ b/makima/src/db/models.rs @@ -1264,6 +1264,11 @@ pub struct Contract { /// without a COMPLETION_GATE indicating ready: true. #[serde(default)] pub autonomous_loop: bool, + /// Whether to wait for user confirmation before progressing to the next phase. + /// When enabled, the supervisor will pause and ask the user to review and approve + /// phase outputs (like plans, requirements, etc.) before continuing. + #[serde(default)] + pub phase_guard: bool, pub version: i32, pub created_at: DateTime<Utc>, pub updated_at: DateTime<Utc>, @@ -1389,6 +1394,11 @@ pub struct CreateContractRequest { /// without a COMPLETION_GATE indicating ready: true. #[serde(default)] pub autonomous_loop: Option<bool>, + /// Enable phase guard mode for this contract. + /// When enabled, the supervisor will pause and ask the user to review and approve + /// phase outputs before progressing to the next phase. + #[serde(default)] + pub phase_guard: Option<bool>, } /// Request payload for updating a contract @@ -1405,6 +1415,11 @@ pub struct UpdateContractRequest { /// Enable or disable autonomous loop mode for tasks in this contract. #[serde(default)] pub autonomous_loop: Option<bool>, + /// Enable or disable phase guard mode for this contract. + /// When enabled, the supervisor will pause and ask the user to review and approve + /// phase outputs before progressing to the next phase. + #[serde(default)] + pub phase_guard: Option<bool>, /// Version for optimistic locking pub version: Option<i32>, } @@ -1443,6 +1458,51 @@ pub struct CreateManagedRepositoryRequest { #[serde(rename_all = "camelCase")] pub struct ChangePhaseRequest { pub phase: String, + /// If phase_guard is enabled, this must be true to confirm the transition. + /// If not provided or false, returns phase deliverables for review. + #[serde(default)] + pub confirmed: Option<bool>, + /// User feedback for changes (used when not confirming) + #[serde(skip_serializing_if = "Option::is_none")] + pub feedback: Option<String>, +} + +/// Response for phase transition when phase_guard is enabled +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct PhaseTransitionRequest { + /// Current contract phase + pub current_phase: String, + /// Requested next phase + pub next_phase: String, + /// Summary of phase deliverables/outputs + pub deliverables_summary: String, + /// List of files created in this phase + pub phase_files: Vec<PhaseFileInfo>, + /// List of completed tasks in this phase + pub phase_tasks: Vec<PhaseTaskInfo>, + /// Whether user confirmation is required + pub requires_confirmation: bool, + /// Unique ID for tracking this transition request + pub transition_id: String, +} + +/// File info for phase transition review +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct PhaseFileInfo { + pub id: Uuid, + pub name: String, + pub description: Option<String>, +} + +/// Task info for phase transition review +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct PhaseTaskInfo { + pub id: Uuid, + pub name: String, + pub status: String, } /// Contract event record from the database diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs index 43b8e3a..3d1efd1 100644 --- a/makima/src/db/repository.rs +++ b/makima/src/db/repository.rs @@ -2136,11 +2136,12 @@ pub async fn create_contract_for_owner( } let autonomous_loop = req.autonomous_loop.unwrap_or(false); + let phase_guard = req.phase_guard.unwrap_or(false); sqlx::query_as::<_, Contract>( r#" - INSERT INTO contracts (owner_id, name, description, contract_type, phase, autonomous_loop) - VALUES ($1, $2, $3, $4, $5, $6) + INSERT INTO contracts (owner_id, name, description, contract_type, phase, autonomous_loop, phase_guard) + VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING * "#, ) @@ -2150,6 +2151,7 @@ pub async fn create_contract_for_owner( .bind(contract_type) .bind(phase) .bind(autonomous_loop) + .bind(phase_guard) .fetch_one(pool) .await } @@ -2249,14 +2251,15 @@ pub async fn update_contract_for_owner( let status = req.status.unwrap_or(existing.status); let supervisor_task_id = req.supervisor_task_id.or(existing.supervisor_task_id); let autonomous_loop = req.autonomous_loop.unwrap_or(existing.autonomous_loop); + let phase_guard = req.phase_guard.unwrap_or(existing.phase_guard); let result = if req.version.is_some() { sqlx::query_as::<_, Contract>( r#" UPDATE contracts SET name = $3, description = $4, phase = $5, status = $6, - supervisor_task_id = $7, autonomous_loop = $8, version = version + 1, updated_at = NOW() - WHERE id = $1 AND owner_id = $2 AND version = $9 + supervisor_task_id = $7, autonomous_loop = $8, phase_guard = $9, version = version + 1, updated_at = NOW() + WHERE id = $1 AND owner_id = $2 AND version = $10 RETURNING * "#, ) @@ -2268,6 +2271,7 @@ pub async fn update_contract_for_owner( .bind(&status) .bind(supervisor_task_id) .bind(autonomous_loop) + .bind(phase_guard) .bind(req.version.unwrap()) .fetch_optional(pool) .await? @@ -2276,7 +2280,7 @@ pub async fn update_contract_for_owner( r#" UPDATE contracts SET name = $3, description = $4, phase = $5, status = $6, - supervisor_task_id = $7, autonomous_loop = $8, version = version + 1, updated_at = NOW() + supervisor_task_id = $7, autonomous_loop = $8, phase_guard = $9, version = version + 1, updated_at = NOW() WHERE id = $1 AND owner_id = $2 RETURNING * "#, @@ -2289,6 +2293,7 @@ pub async fn update_contract_for_owner( .bind(&status) .bind(supervisor_task_id) .bind(autonomous_loop) + .bind(phase_guard) .fetch_optional(pool) .await? }; diff --git a/makima/src/llm/contract_tools.rs b/makima/src/llm/contract_tools.rs index 7a3d09a..07de1fe 100644 --- a/makima/src/llm/contract_tools.rs +++ b/makima/src/llm/contract_tools.rs @@ -203,7 +203,7 @@ pub static CONTRACT_TOOLS: once_cell::sync::Lazy<Vec<Tool>> = once_cell::sync::L }, Tool { name: "advance_phase".to_string(), - description: "Advance the contract to the NEXT phase in sequence. Phases progress: research -> specify -> plan -> execute -> review. You can ONLY advance forward one step. Always use suggest_phase_transition first to check readiness and find the correct next phase.".to_string(), + description: "Advance the contract to the NEXT phase in sequence. Phases progress: research -> specify -> plan -> execute -> review. You can ONLY advance forward one step. Always use suggest_phase_transition first to check readiness and find the correct next phase. If the contract has phase_guard enabled, this will first return a pending_confirmation status with phase deliverables for user review. Call again with confirmed=true to complete the transition, or with feedback to request changes.".to_string(), parameters: json!({ "type": "object", "properties": { @@ -211,6 +211,14 @@ pub static CONTRACT_TOOLS: once_cell::sync::Lazy<Vec<Tool>> = once_cell::sync::L "type": "string", "enum": ["specify", "plan", "execute", "review"], "description": "The next phase to transition to. Must be exactly one step ahead of current phase (e.g., research->specify, specify->plan, plan->execute, execute->review)" + }, + "confirmed": { + "type": "boolean", + "description": "Set to true to confirm the phase transition when phase_guard is enabled. If omitted or false, returns deliverables for review." + }, + "feedback": { + "type": "string", + "description": "User feedback when requesting changes instead of confirming the transition. The feedback will be passed back to the task to address." } }, "required": ["new_phase"] @@ -500,7 +508,13 @@ pub enum ContractToolRequest { // Phase management GetPhaseInfo, SuggestPhaseTransition, - AdvancePhase { new_phase: String }, + AdvancePhase { + new_phase: String, + /// Whether the user has confirmed the phase transition (for phase_guard) + confirmed: bool, + /// User feedback when they request changes instead of confirming + feedback: Option<String>, + }, // Repository management ListDaemonDirectories, @@ -870,12 +884,28 @@ fn parse_advance_phase(call: &super::tools::ToolCall) -> ContractToolExecutionRe return error_result("Invalid phase. Must be one of: research, specify, plan, execute, review"); } + // Parse optional confirmed flag (defaults to false for initial phase_guard check) + let confirmed = call + .arguments + .get("confirmed") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + + // Parse optional feedback (for when user requests changes) + let feedback = call + .arguments + .get("feedback") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + ContractToolExecutionResult { success: true, message: format!("Advancing to '{}' phase...", new_phase), data: None, request: Some(ContractToolRequest::AdvancePhase { new_phase: new_phase.to_string(), + confirmed, + feedback, }), pending_questions: None, } diff --git a/makima/src/server/handlers/contract_chat.rs b/makima/src/server/handlers/contract_chat.rs index 101b257..29ec620 100644 --- a/makima/src/server/handlers/contract_chat.rs +++ b/makima/src/server/handlers/contract_chat.rs @@ -1689,7 +1689,7 @@ async fn handle_contract_request( } } - ContractToolRequest::AdvancePhase { new_phase } => { + ContractToolRequest::AdvancePhase { new_phase, confirmed, feedback } => { let contract = match repository::get_contract_for_owner(pool, contract_id, owner_id).await { Ok(Some(c)) => c, Ok(None) => { @@ -1723,7 +1723,88 @@ async fn handle_contract_request( }; } - // Update phase + // Check if phase_guard is enabled + if contract.phase_guard { + // If user provided feedback, return it for the task to address + if let Some(ref user_feedback) = feedback { + return ContractRequestResult { + success: true, + message: format!( + "Phase transition to '{}' requires changes. User feedback: {}", + new_phase, user_feedback + ), + data: Some(json!({ + "status": "changes_requested", + "currentPhase": current_phase, + "requestedPhase": new_phase, + "feedback": user_feedback, + "action": "Address the user feedback and try again when ready" + })), + }; + } + + // If not confirmed, return pending confirmation with phase deliverables + if !confirmed { + // Get files created in this phase + let phase_files = match repository::list_files_in_contract(pool, contract_id, owner_id).await { + Ok(files) => files + .into_iter() + .filter(|f| f.contract_phase.as_deref() == Some(current_phase)) + .map(|f| json!({ + "id": f.id, + "name": f.name, + "description": f.description + })) + .collect::<Vec<_>>(), + Err(_) => Vec::new(), + }; + + // Get tasks completed in this contract + let phase_tasks = match repository::list_tasks_in_contract(pool, contract_id, owner_id).await { + Ok(tasks) => tasks + .into_iter() + .filter(|t| t.status == "done" || t.status == "completed") + .map(|t| json!({ + "id": t.id, + "name": t.name, + "status": t.status + })) + .collect::<Vec<_>>(), + Err(_) => Vec::new(), + }; + + // Build deliverables summary + let deliverables_summary = format!( + "Phase '{}' deliverables: {} files created, {} tasks completed.", + current_phase, + phase_files.len(), + phase_tasks.len() + ); + + let transition_id = uuid::Uuid::new_v4().to_string(); + + return ContractRequestResult { + success: true, + message: format!( + "Phase transition to '{}' requires user confirmation. Review the deliverables and call advance_phase again with confirmed=true to proceed, or provide feedback to request changes.", + new_phase + ), + data: Some(json!({ + "status": "pending_confirmation", + "transitionId": transition_id, + "currentPhase": current_phase, + "nextPhase": new_phase, + "deliverablesSummary": deliverables_summary, + "phaseFiles": phase_files, + "phaseTasks": phase_tasks, + "requiresConfirmation": true, + "instructions": "To proceed: call advance_phase with confirmed=true. To request changes: call advance_phase with feedback='your feedback here'" + })), + }; + } + } + + // Update phase (either phase_guard is disabled, or user confirmed) match repository::change_contract_phase_for_owner(pool, contract_id, owner_id, &new_phase).await { Ok(Some(updated)) => { // Get deliverables for the new phase @@ -1748,6 +1829,7 @@ async fn handle_contract_request( current_phase, new_phase, deliverables.guidance ), data: Some(json!({ + "status": "advanced", "previousPhase": current_phase, "newPhase": updated.phase, "phaseGuidance": deliverables.guidance, @@ -2377,6 +2459,7 @@ async fn handle_contract_request( contract_type: Some("specification".to_string()), initial_phase: Some("research".to_string()), autonomous_loop: None, + phase_guard: None, }; let contract = match repository::create_contract_for_owner(pool, owner_id, contract_req).await { diff --git a/makima/src/server/handlers/contracts.rs b/makima/src/server/handlers/contracts.rs index 684ab2b..4f4a94b 100644 --- a/makima/src/server/handlers/contracts.rs +++ b/makima/src/server/handlers/contracts.rs @@ -1267,14 +1267,100 @@ pub async fn change_phase( .into_response(); }; + // First, get the contract to check phase_guard + 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(); + } + }; + + // If phase_guard is enabled and not confirmed, return phase deliverables for review + if contract.phase_guard && !req.confirmed.unwrap_or(false) { + // If user provided feedback, return it + if let Some(ref feedback) = req.feedback { + return Json(serde_json::json!({ + "status": "changes_requested", + "currentPhase": contract.phase, + "requestedPhase": req.phase, + "feedback": feedback, + "message": "Feedback has been noted. Address the changes and try again." + })) + .into_response(); + } + + // Get files created in this phase + let phase_files = match repository::list_files_in_contract(pool, id, auth.owner_id).await { + Ok(files) => files + .into_iter() + .filter(|f| f.contract_phase.as_deref() == Some(&contract.phase)) + .map(|f| serde_json::json!({ + "id": f.id, + "name": f.name, + "description": f.description + })) + .collect::<Vec<_>>(), + Err(_) => Vec::new(), + }; + + // Get tasks completed in this contract + let phase_tasks = match repository::list_tasks_in_contract(pool, id, auth.owner_id).await { + Ok(tasks) => tasks + .into_iter() + .filter(|t| t.status == "done" || t.status == "completed") + .map(|t| serde_json::json!({ + "id": t.id, + "name": t.name, + "status": t.status + })) + .collect::<Vec<_>>(), + Err(_) => Vec::new(), + }; + + let deliverables_summary = format!( + "Phase '{}' deliverables: {} files created, {} tasks completed.", + contract.phase, + phase_files.len(), + phase_tasks.len() + ); + + let transition_id = uuid::Uuid::new_v4().to_string(); + + return Json(serde_json::json!({ + "status": "pending_confirmation", + "transitionId": transition_id, + "currentPhase": contract.phase, + "nextPhase": req.phase, + "deliverablesSummary": deliverables_summary, + "phaseFiles": phase_files, + "phaseTasks": phase_tasks, + "requiresConfirmation": true, + "message": "Phase transition requires confirmation. Set confirmed=true in the request to proceed." + })) + .into_response(); + } + + // Phase guard is disabled or user confirmed - proceed with phase change match repository::change_contract_phase_for_owner(pool, id, auth.owner_id, &req.phase).await { - Ok(Some(contract)) => { + Ok(Some(updated_contract)) => { // Notify supervisor of phase change - if let Some(supervisor_task_id) = contract.supervisor_task_id { + if let Some(supervisor_task_id) = updated_contract.supervisor_task_id { if let Ok(Some(supervisor)) = repository::get_task_for_owner(pool, supervisor_task_id, auth.owner_id).await { let state_clone = state.clone(); - let contract_id = contract.id; - let new_phase = contract.phase.clone(); + let contract_id = updated_contract.id; + let new_phase = updated_contract.phase.clone(); tokio::spawn(async move { state_clone.notify_supervisor_of_phase_change( supervisor.id, @@ -1302,21 +1388,21 @@ pub async fn change_phase( ).await; // Get summary with counts - match repository::get_contract_summary_for_owner(pool, contract.id, auth.owner_id).await + match repository::get_contract_summary_for_owner(pool, updated_contract.id, auth.owner_id).await { Ok(Some(summary)) => Json(summary).into_response(), _ => Json(ContractSummary { - id: contract.id, - name: contract.name, - description: contract.description, - contract_type: contract.contract_type, - phase: contract.phase, - status: contract.status, + id: updated_contract.id, + name: updated_contract.name, + description: updated_contract.description, + contract_type: updated_contract.contract_type, + phase: updated_contract.phase, + status: updated_contract.status, file_count: 0, task_count: 0, repository_count: 0, - version: contract.version, - created_at: contract.created_at, + version: updated_contract.version, + created_at: updated_contract.created_at, }) .into_response(), } diff --git a/makima/src/server/handlers/mesh.rs b/makima/src/server/handlers/mesh.rs index 5a08a49..f8df69f 100644 --- a/makima/src/server/handlers/mesh.rs +++ b/makima/src/server/handlers/mesh.rs @@ -3239,6 +3239,7 @@ pub async fn create_adhoc_task( contract_type: Some(CONTRACT_TYPE_TASK.to_string()), initial_phase: Some("execute".to_string()), // Skip planning autonomous_loop: Some(false), + phase_guard: None, }; let contract = match repository::create_contract_for_owner(pool, auth.owner_id, contract_req).await { diff --git a/makima/src/server/handlers/transcript_analysis.rs b/makima/src/server/handlers/transcript_analysis.rs index 275905e..99f9ea7 100644 --- a/makima/src/server/handlers/transcript_analysis.rs +++ b/makima/src/server/handlers/transcript_analysis.rs @@ -277,6 +277,7 @@ pub async fn create_contract_from_analysis( contract_type: Some("specification".to_string()), initial_phase: Some("research".to_string()), autonomous_loop: None, + phase_guard: None, }; let contract = match repository::create_contract_for_owner(pool, auth.owner_id, contract_req).await { |
