diff options
| author | soryu <soryu@soryu.co> | 2026-01-17 06:20:07 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-17 16:39:24 +0000 |
| commit | bfc5d837c6212a8253accfdf95ae1a2fd692df4e (patch) | |
| tree | 0cc78ff56fd28333d1e502176873d23a26d4c4a1 | |
| parent | 06fb883b2b7a49c7123722463d24b0b4e57c3277 (diff) | |
| download | soryu-bfc5d837c6212a8253accfdf95ae1a2fd692df4e.tar.gz soryu-bfc5d837c6212a8253accfdf95ae1a2fd692df4e.zip | |
feat: Add phase_guard for contract phase transitions
Implement phase_guard logic in the advance_phase tool. When a contract
has phase_guard enabled, the phase transition now:
1. Asks for user confirmation before advancing
2. Allows users to request changes to phase deliverables
3. Passes feedback to the task without advancing if changes requested
Changes:
- Add phase_guard field to Contract model and CreateContractRequest
- Add PhaseTransitionRequest, PhaseFileInfo, PhaseTaskInfo structs
- Update ChangePhaseRequest with confirmed and feedback fields
- Update ContractToolRequest::AdvancePhase with confirmed/feedback
- Modify advance_phase handling in contract_chat.rs with phase_guard logic
- Update change_phase endpoint in contracts.rs with phase_guard support
- Add database migration for phase_guard column
When phase_guard=false: Phase advances immediately (current behavior)
When phase_guard=true: Returns pending_confirmation status with deliverables
If user provides feedback: Returns feedback to task, doesn't advance
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
| -rw-r--r-- | makima/src/db/models.rs | 45 | ||||
| -rw-r--r-- | makima/src/llm/contract_tools.rs | 34 | ||||
| -rw-r--r-- | makima/src/server/handlers/contract_chat.rs | 86 | ||||
| -rw-r--r-- | makima/src/server/handlers/contracts.rs | 112 |
4 files changed, 260 insertions, 17 deletions
diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs index 33ef52e..99c8b8e 100644 --- a/makima/src/db/models.rs +++ b/makima/src/db/models.rs @@ -1458,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/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 48dd864..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, 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(), } |
