diff options
Diffstat (limited to 'makima')
| -rw-r--r-- | makima/migrations/20250127000000_add_local_only.sql | 8 | ||||
| -rw-r--r-- | makima/src/daemon/task/manager.rs | 22 | ||||
| -rw-r--r-- | makima/src/daemon/ws/protocol.rs | 3 | ||||
| -rw-r--r-- | makima/src/db/models.rs | 11 | ||||
| -rw-r--r-- | makima/src/server/handlers/contract_chat.rs | 11 | ||||
| -rw-r--r-- | makima/src/server/handlers/mesh.rs | 46 | ||||
| -rw-r--r-- | makima/src/server/handlers/mesh_chat.rs | 11 | ||||
| -rw-r--r-- | makima/src/server/handlers/mesh_supervisor.rs | 13 | ||||
| -rw-r--r-- | makima/src/server/state.rs | 3 |
9 files changed, 118 insertions, 10 deletions
diff --git a/makima/migrations/20250127000000_add_local_only.sql b/makima/migrations/20250127000000_add_local_only.sql new file mode 100644 index 0000000..2cd594e --- /dev/null +++ b/makima/migrations/20250127000000_add_local_only.sql @@ -0,0 +1,8 @@ +-- Add local_only column to contracts table +-- When enabled, automatic completion actions (branch, merge, pr) are skipped, +-- allowing users to manually handle code changes via patch files or other means. + +ALTER TABLE contracts +ADD COLUMN IF NOT EXISTS local_only BOOLEAN NOT NULL DEFAULT FALSE; + +COMMENT ON COLUMN contracts.local_only IS 'Whether to skip automatic completion actions (branch, merge, pr) for this contract'; diff --git a/makima/src/daemon/task/manager.rs b/makima/src/daemon/task/manager.rs index 8abff3f..27018e3 100644 --- a/makima/src/daemon/task/manager.rs +++ b/makima/src/daemon/task/manager.rs @@ -995,6 +995,8 @@ pub struct ManagedTask { pub concurrency_key: Uuid, /// Whether to run in autonomous loop mode. pub autonomous_loop: bool, + /// Whether the contract is in local-only mode (skips automatic completion actions). + pub local_only: bool, /// Time task was created. pub created_at: Instant, /// Time task started running. @@ -1692,6 +1694,7 @@ impl TaskManager { conversation_history, patch_data, patch_base_sha, + local_only, } => { tracing::info!( task_id = %task_id, @@ -1718,7 +1721,7 @@ impl TaskManager { parent_task_id, depth, is_orchestrator, is_supervisor, target_repo_path, completion_action, continue_from_task_id, copy_files, contract_id, autonomous_loop, resume_session, - conversation_history, patch_data, patch_base_sha, + conversation_history, patch_data, patch_base_sha, local_only, ).await?; } DaemonCommand::PauseTask { task_id } => { @@ -1796,6 +1799,7 @@ impl TaskManager { let target_repo_path = task.target_repo_path.clone(); let completion_action = task.completion_action.clone(); let contract_id = task.contract_id; + let local_only = task.local_only; // Spawn in background to not block the command handler tokio::spawn(async move { @@ -1818,6 +1822,7 @@ impl TaskManager { None, // conversation_history - not needed for fresh respawn None, // patch_data - not available for respawn None, // patch_base_sha - not available for respawn + local_only, ).await { tracing::error!( task_id = %task_id, @@ -2046,6 +2051,7 @@ impl TaskManager { conversation_history: Option<serde_json::Value>, patch_data: Option<String>, patch_base_sha: Option<String>, + local_only: bool, ) -> TaskResult<()> { tracing::info!(task_id = %task_id, is_orchestrator = is_orchestrator, is_supervisor = is_supervisor, depth = depth, patch_available = patch_data.is_some(), "=== SPAWN_TASK START ==="); @@ -2096,6 +2102,7 @@ impl TaskManager { contract_id, concurrency_key, autonomous_loop, + local_only, created_at: Instant::now(), started_at: None, completed_at: None, @@ -2122,7 +2129,7 @@ impl TaskManager { task_id, task_name, plan, repo_url, base_branch, target_branch, is_orchestrator, is_supervisor, target_repo_path, completion_action, continue_from_task_id, copy_files, contract_id, autonomous_loop, resume_session, - conversation_history, patch_data, patch_base_sha, + conversation_history, patch_data, patch_base_sha, local_only, ).await { tracing::error!(task_id = %task_id, error = %e, "Task execution failed"); inner.mark_failed(task_id, &e.to_string()).await; @@ -3570,6 +3577,7 @@ impl TaskManagerInner { conversation_history: Option<serde_json::Value>, patch_data: Option<String>, patch_base_sha: Option<String>, + local_only: bool, ) -> Result<(), DaemonError> { tracing::info!(task_id = %task_id, is_orchestrator = is_orchestrator, is_supervisor = is_supervisor, resume_session = resume_session, has_patch = patch_data.is_some(), "=== RUN_TASK START ==="); @@ -4704,9 +4712,15 @@ impl TaskManagerInner { } } - // Execute completion action if task succeeded + // Execute completion action if task succeeded (skip in local_only mode) let completion_result = if success { - if let Some(ref action) = completion_action { + if local_only { + tracing::info!( + task_id = %task_id, + "Skipping completion action - contract is in local_only mode" + ); + Ok(None) + } else if let Some(ref action) = completion_action { if action != "none" { self.execute_completion_action( task_id, diff --git a/makima/src/daemon/ws/protocol.rs b/makima/src/daemon/ws/protocol.rs index 2e7caef..ed651ec 100644 --- a/makima/src/daemon/ws/protocol.rs +++ b/makima/src/daemon/ws/protocol.rs @@ -422,6 +422,9 @@ pub enum DaemonCommand { /// Commit SHA to apply the patch on top of. #[serde(rename = "patchBaseSha", default, skip_serializing_if = "Option::is_none")] patch_base_sha: Option<String>, + /// Whether the contract is in local-only mode (skips automatic completion actions). + #[serde(rename = "localOnly", default)] + local_only: bool, }, /// Pause a running task. diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs index 21e5370..9c2d072 100644 --- a/makima/src/db/models.rs +++ b/makima/src/db/models.rs @@ -1326,8 +1326,9 @@ pub struct Contract { #[sqlx(json)] #[serde(default)] pub completed_deliverables: serde_json::Value, - /// When true, tasks do not auto-execute completion actions and work stays in worktrees. - /// Used for local-only workflows where changes are managed manually. + /// Whether this contract operates in local-only mode. + /// When enabled, automatic completion actions (branch, merge, pr) are skipped, + /// allowing users to manually handle code changes via patch files or other means. #[serde(default)] pub local_only: bool, pub version: i32, @@ -1503,7 +1504,8 @@ pub struct CreateContractRequest { #[serde(default)] pub phase_guard: Option<bool>, /// Enable local-only mode for this contract. - /// When true, tasks do not auto-execute completion actions and work stays in worktrees. + /// When enabled, automatic completion actions (branch, merge, pr) are skipped, + /// allowing users to manually handle code changes via patch files or other means. #[serde(default)] pub local_only: Option<bool>, } @@ -1528,7 +1530,8 @@ pub struct UpdateContractRequest { #[serde(default)] pub phase_guard: Option<bool>, /// Enable or disable local-only mode for this contract. - /// When true, tasks do not auto-execute completion actions and work stays in worktrees. + /// When enabled, automatic completion actions (branch, merge, pr) are skipped, + /// allowing users to manually handle code changes via patch files or other means. #[serde(default)] pub local_only: Option<bool>, /// Version for optimistic locking diff --git a/makima/src/server/handlers/contract_chat.rs b/makima/src/server/handlers/contract_chat.rs index 8c5509e..e6ee8d4 100644 --- a/makima/src/server/handlers/contract_chat.rs +++ b/makima/src/server/handlers/contract_chat.rs @@ -1567,6 +1567,16 @@ async fn handle_contract_request( } }; + // Get local_only from contract if task has one + let local_only = if let Some(contract_id) = task.contract_id { + match repository::get_contract_for_owner(pool, contract_id, owner_id).await { + Ok(Some(contract)) => contract.local_only, + _ => false, + } + } else { + false + }; + // Send SpawnTask command to daemon let command = DaemonCommand::SpawnTask { task_id, @@ -1589,6 +1599,7 @@ async fn handle_contract_request( conversation_history: None, patch_data: None, patch_base_sha: None, + local_only, }; if let Err(e) = command_sender.send(command).await { diff --git a/makima/src/server/handlers/mesh.rs b/makima/src/server/handlers/mesh.rs index 545d1ea..19958e7 100644 --- a/makima/src/server/handlers/mesh.rs +++ b/makima/src/server/handlers/mesh.rs @@ -599,6 +599,16 @@ pub async fn start_task( .into_response(); } + // Get local_only flag from contract if task has one + let local_only = if let Some(contract_id) = task.contract_id { + match repository::get_contract_for_owner(pool, contract_id, auth.owner_id).await { + Ok(Some(contract)) => contract.local_only, + _ => false, + } + } else { + false + }; + // Get list of daemons that have previously failed this task let mut exclude_daemon_ids: Vec<Uuid> = task.failed_daemon_ids.clone().unwrap_or_default(); @@ -694,6 +704,7 @@ pub async fn start_task( conversation_history: None, patch_data: None, patch_base_sha: None, + local_only, }; tracing::info!( @@ -746,6 +757,7 @@ pub async fn start_task( conversation_history: None, patch_data: None, patch_base_sha: None, + local_only, }; if state.send_daemon_command(alt_daemon_id, alt_command).await.is_ok() { @@ -1128,6 +1140,16 @@ pub async fn send_message( }; if let Ok(Some(updated_task)) = repository::update_task_for_owner(pool, id, auth.owner_id, update_req).await { + // Get local_only from contract if task has one + let local_only = if let Some(contract_id) = updated_task.contract_id { + match repository::get_contract_for_owner(pool, contract_id, auth.owner_id).await { + Ok(Some(contract)) => contract.local_only, + _ => false, + } + } else { + false + }; + // Send spawn command to new daemon let spawn_cmd = DaemonCommand::SpawnTask { task_id: id, @@ -1150,6 +1172,7 @@ pub async fn send_message( conversation_history: None, patch_data: None, patch_base_sha: None, + local_only, }; if state.send_daemon_command(new_daemon_id, spawn_cmd).await.is_ok() { @@ -2293,6 +2316,16 @@ pub async fn reassign_task( } }; + // Get local_only from contract if task has one + let local_only = if let Some(contract_id) = task.contract_id { + match repository::get_contract_for_owner(pool, contract_id, auth.owner_id).await { + Ok(Some(contract)) => contract.local_only, + _ => false, + } + } else { + false + }; + // Send SpawnTask command to daemon for the new task let command = DaemonCommand::SpawnTask { task_id: new_task.id, @@ -2315,6 +2348,7 @@ pub async fn reassign_task( conversation_history: None, patch_data, patch_base_sha, + local_only, }; tracing::info!( @@ -2620,6 +2654,16 @@ pub async fn continue_task( }; let is_orchestrator = task.depth == 0 && subtask_count > 0; + // Get local_only from contract if task has one + let local_only = if let Some(contract_id) = task.contract_id { + match repository::get_contract_for_owner(pool, contract_id, auth.owner_id).await { + Ok(Some(contract)) => contract.local_only, + _ => false, + } + } else { + false + }; + // Send SpawnTask command to daemon let command = DaemonCommand::SpawnTask { task_id: id, @@ -2642,6 +2686,7 @@ pub async fn continue_task( conversation_history: None, patch_data: None, patch_base_sha: None, + local_only, }; tracing::info!( @@ -3562,6 +3607,7 @@ pub async fn branch_task( conversation_history: updated_task.conversation_state.clone(), patch_data, patch_base_sha, + local_only: false, // No contract, so not local_only }; if let Err(e) = state.send_daemon_command(target_daemon_id, command).await { diff --git a/makima/src/server/handlers/mesh_chat.rs b/makima/src/server/handlers/mesh_chat.rs index 1ff0724..eb35728 100644 --- a/makima/src/server/handlers/mesh_chat.rs +++ b/makima/src/server/handlers/mesh_chat.rs @@ -1131,6 +1131,16 @@ async fn handle_mesh_request( } }; + // Get local_only from contract if task has one + let local_only = if let Some(contract_id) = task.contract_id { + match repository::get_contract_for_owner(pool, contract_id, owner_id).await { + Ok(Some(contract)) => contract.local_only, + _ => false, + } + } else { + false + }; + // Send SpawnTask command to daemon let command = DaemonCommand::SpawnTask { task_id, @@ -1153,6 +1163,7 @@ async fn handle_mesh_request( conversation_history: None, patch_data: None, patch_base_sha: None, + local_only, }; match state.send_daemon_command(target_daemon_id, command).await { diff --git a/makima/src/server/handlers/mesh_supervisor.rs b/makima/src/server/handlers/mesh_supervisor.rs index 1b476ef..4ecb4dc 100644 --- a/makima/src/server/handlers/mesh_supervisor.rs +++ b/makima/src/server/handlers/mesh_supervisor.rs @@ -297,6 +297,12 @@ pub async fn try_start_pending_task( return Ok(None); } + // Get contract to check local_only flag + let contract = repository::get_contract_for_owner(pool, contract_id, owner_id) + .await + .map_err(|e| format!("Failed to get contract: {}", e))? + .ok_or_else(|| "Contract not found".to_string())?; + // Try each pending task until we find one we can start for task in &pending_tasks { // Get excluded daemon IDs for this task (daemons that have already failed it) @@ -399,6 +405,7 @@ pub async fn try_start_pending_task( conversation_history: None, patch_data, patch_base_sha, + local_only: contract.local_only, }; if let Err(e) = state.send_daemon_command(daemon.id, cmd).await { @@ -532,8 +539,8 @@ pub async fn spawn_task( let pool = state.db_pool.as_ref().unwrap(); - // Verify contract exists - let _contract = match repository::get_contract_for_owner(pool, request.contract_id, owner_id).await { + // Verify contract exists and get local_only flag + let contract = match repository::get_contract_for_owner(pool, request.contract_id, owner_id).await { Ok(Some(c)) => c, Ok(None) => { return ( @@ -711,6 +718,7 @@ pub async fn spawn_task( conversation_history: None, patch_data: None, patch_base_sha: None, + local_only: contract.local_only, }; if let Err(e) = state.send_daemon_command(daemon.id, cmd).await { @@ -2094,6 +2102,7 @@ pub async fn resume_supervisor( conversation_history: Some(supervisor_state.conversation_history.clone()), // Fallback if worktree missing patch_data, patch_base_sha, + local_only: contract.local_only, }; if let Err(e) = state.send_daemon_command(target_daemon_id, command).await { diff --git a/makima/src/server/state.rs b/makima/src/server/state.rs index b954efe..854c881 100644 --- a/makima/src/server/state.rs +++ b/makima/src/server/state.rs @@ -223,6 +223,9 @@ pub enum DaemonCommand { /// Commit SHA to apply the patch on top of #[serde(rename = "patchBaseSha", default, skip_serializing_if = "Option::is_none")] patch_base_sha: Option<String>, + /// Whether the contract is in local-only mode (skips automatic completion actions) + #[serde(rename = "localOnly", default)] + local_only: bool, }, /// Pause a running task PauseTask { |
