summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-17 06:20:07 +0000
committersoryu <soryu@soryu.co>2026-01-17 16:39:24 +0000
commitbfc5d837c6212a8253accfdf95ae1a2fd692df4e (patch)
tree0cc78ff56fd28333d1e502176873d23a26d4c4a1
parent06fb883b2b7a49c7123722463d24b0b4e57c3277 (diff)
downloadsoryu-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.rs45
-rw-r--r--makima/src/llm/contract_tools.rs34
-rw-r--r--makima/src/server/handlers/contract_chat.rs86
-rw-r--r--makima/src/server/handlers/contracts.rs112
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(),
}