diff options
| author | soryu <soryu@soryu.co> | 2026-01-30 03:28:40 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-30 03:28:40 +0000 |
| commit | 4c09aa13a50064a4145ef53021490d303e46bc5e (patch) | |
| tree | d1fa901d97e54a7840e5ac153270bdab66bb8c99 | |
| parent | dac1adb138f532245a36fa16524f1e4fb9990173 (diff) | |
| download | soryu-4c09aa13a50064a4145ef53021490d303e46bc5e.tar.gz soryu-4c09aa13a50064a4145ef53021490d303e46bc5e.zip | |
Add auto_merge_local option for local-only contractsmakima/auto-merge-local
When local_only=true on a contract, all completion actions are skipped.
This adds a new option auto_merge_local that, when enabled along with
local_only, will automatically merge completed task changes to the
master/main branch locally (without pushing or creating PRs).
Changes:
- Add auto_merge_local column to contracts table (migration)
- Add auto_merge_local field to Contract model and summary
- Update CreateContractRequest and UpdateContractRequest structs
- Update contract repository create/update functions
- Add auto_merge_local to WebSocket protocol StartTask command
- Pass auto_merge_local through spawn_task and run_task functions
- Modify task manager completion logic: if local_only=true AND
auto_merge_local=true, execute 'merge' completion action locally
- Update all server handlers to retrieve and pass auto_merge_local
- Add TypeScript types to frontend components
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
| -rw-r--r-- | frontend/src/components/ContractDetail.tsx | 2 | ||||
| -rw-r--r-- | frontend/src/components/ContractList.tsx | 2 | ||||
| -rw-r--r-- | makima/migrations/20260130000001_add_auto_merge_local.sql | 8 | ||||
| -rw-r--r-- | makima/src/bin/makima.rs | 1 | ||||
| -rw-r--r-- | makima/src/daemon/api/contract.rs | 2 | ||||
| -rw-r--r-- | makima/src/daemon/task/manager.rs | 40 | ||||
| -rw-r--r-- | makima/src/daemon/ws/protocol.rs | 3 | ||||
| -rw-r--r-- | makima/src/db/models.rs | 18 | ||||
| -rw-r--r-- | makima/src/db/repository.rs | 19 | ||||
| -rw-r--r-- | makima/src/server/handlers/contract_chat.rs | 12 | ||||
| -rw-r--r-- | makima/src/server/handlers/contracts.rs | 4 | ||||
| -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 | 4 | ||||
| -rw-r--r-- | makima/src/server/handlers/transcript_analysis.rs | 1 | ||||
| -rw-r--r-- | makima/src/server/state.rs | 3 |
16 files changed, 131 insertions, 45 deletions
diff --git a/frontend/src/components/ContractDetail.tsx b/frontend/src/components/ContractDetail.tsx index 72527ce..135c313 100644 --- a/frontend/src/components/ContractDetail.tsx +++ b/frontend/src/components/ContractDetail.tsx @@ -28,6 +28,8 @@ interface Contract { contract_type: string phase: string status: string + localOnly?: boolean + autoMergeLocal?: boolean version: number created_at: string } diff --git a/frontend/src/components/ContractList.tsx b/frontend/src/components/ContractList.tsx index 77012db..d6b332c 100644 --- a/frontend/src/components/ContractList.tsx +++ b/frontend/src/components/ContractList.tsx @@ -8,6 +8,8 @@ interface ContractSummary { contract_type: string phase: string status: string + localOnly?: boolean + autoMergeLocal?: boolean file_count: number task_count: number repository_count: number diff --git a/makima/migrations/20260130000001_add_auto_merge_local.sql b/makima/migrations/20260130000001_add_auto_merge_local.sql new file mode 100644 index 0000000..1dd5e5c --- /dev/null +++ b/makima/migrations/20260130000001_add_auto_merge_local.sql @@ -0,0 +1,8 @@ +-- Add auto_merge_local column to contracts table +-- When enabled alongside local_only, completed task changes will be automatically +-- merged to the master/main branch locally (without pushing or creating PRs). + +ALTER TABLE contracts +ADD COLUMN IF NOT EXISTS auto_merge_local BOOLEAN NOT NULL DEFAULT FALSE; + +COMMENT ON COLUMN contracts.auto_merge_local IS 'Whether to auto-merge to target branch locally when local_only mode is enabled'; diff --git a/makima/src/bin/makima.rs b/makima/src/bin/makima.rs index c637c36..e5b1a5e 100644 --- a/makima/src/bin/makima.rs +++ b/makima/src/bin/makima.rs @@ -1110,6 +1110,7 @@ async fn run_tui_loop( autonomous_loop: None, phase_guard: None, local_only: None, + auto_merge_local: None, red_team_enabled: None, red_team_prompt: None, }; diff --git a/makima/src/daemon/api/contract.rs b/makima/src/daemon/api/contract.rs index 445d676..7c76b40 100644 --- a/makima/src/daemon/api/contract.rs +++ b/makima/src/daemon/api/contract.rs @@ -69,6 +69,8 @@ pub struct CreateContractRequest { #[serde(skip_serializing_if = "Option::is_none")] pub local_only: Option<bool>, #[serde(skip_serializing_if = "Option::is_none")] + pub auto_merge_local: Option<bool>, + #[serde(skip_serializing_if = "Option::is_none")] pub red_team_enabled: Option<bool>, #[serde(skip_serializing_if = "Option::is_none")] pub red_team_prompt: Option<String>, diff --git a/makima/src/daemon/task/manager.rs b/makima/src/daemon/task/manager.rs index 1e04ca1..d246ac8 100644 --- a/makima/src/daemon/task/manager.rs +++ b/makima/src/daemon/task/manager.rs @@ -997,6 +997,8 @@ pub struct ManagedTask { pub autonomous_loop: bool, /// Whether the contract is in local-only mode (skips automatic completion actions). pub local_only: bool, + /// Whether to auto-merge to target branch locally when local_only mode is enabled. + pub auto_merge_local: bool, /// If set, merge this task's changes to the supervisor's worktree on completion (cross-daemon case). pub merge_to_supervisor_task_id: Option<Uuid>, /// If set, this task shares the worktree of the specified supervisor task. @@ -1730,6 +1732,7 @@ impl TaskManager { patch_data, patch_base_sha, local_only, + auto_merge_local, supervisor_worktree_task_id, } => { tracing::info!( @@ -1758,7 +1761,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, local_only, + conversation_history, patch_data, patch_base_sha, local_only, auto_merge_local, supervisor_worktree_task_id, ).await?; } @@ -1838,6 +1841,7 @@ impl TaskManager { let completion_action = task.completion_action.clone(); let contract_id = task.contract_id; let local_only = task.local_only; + let auto_merge_local = task.auto_merge_local; // Spawn in background to not block the command handler tokio::spawn(async move { @@ -1861,6 +1865,7 @@ impl TaskManager { None, // patch_data - not available for respawn None, // patch_base_sha - not available for respawn local_only, + auto_merge_local, None, // supervisor_worktree_task_id - supervisors use their own worktree ).await { tracing::error!( @@ -2117,6 +2122,7 @@ impl TaskManager { patch_data: Option<String>, patch_base_sha: Option<String>, local_only: bool, + auto_merge_local: bool, supervisor_worktree_task_id: Option<Uuid>, ) -> 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 ==="); @@ -2169,6 +2175,7 @@ impl TaskManager { concurrency_key, autonomous_loop, local_only, + auto_merge_local, merge_to_supervisor_task_id: None, // Set later if cross-daemon supervisor_worktree_task_id, created_at: Instant::now(), @@ -2197,7 +2204,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, local_only, + conversation_history, patch_data, patch_base_sha, local_only, auto_merge_local, supervisor_worktree_task_id, ).await { tracing::error!(task_id = %task_id, error = %e, "Task execution failed"); @@ -4203,6 +4210,7 @@ impl TaskManagerInner { patch_data: Option<String>, patch_base_sha: Option<String>, local_only: bool, + auto_merge_local: bool, supervisor_worktree_task_id: Option<Uuid>, ) -> 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 ==="); @@ -5398,14 +5406,30 @@ impl TaskManagerInner { } } - // Execute completion action if task succeeded (skip in local_only mode) + // Execute completion action if task succeeded (skip in local_only mode unless auto_merge_local is enabled) let completion_result = if success { if local_only { - tracing::info!( - task_id = %task_id, - "Skipping completion action - contract is in local_only mode" - ); - Ok(None) + if auto_merge_local { + // In local_only mode with auto_merge_local enabled, merge locally + tracing::info!( + task_id = %task_id, + "Local-only mode with auto_merge_local - executing local merge" + ); + self.execute_completion_action( + task_id, + &task_name, + &working_dir, + "merge", // Use merge action (not pr) + target_repo_path.as_deref(), + target_branch.as_deref(), + ).await + } else { + 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( diff --git a/makima/src/daemon/ws/protocol.rs b/makima/src/daemon/ws/protocol.rs index c396961..bfe6326 100644 --- a/makima/src/daemon/ws/protocol.rs +++ b/makima/src/daemon/ws/protocol.rs @@ -494,6 +494,9 @@ pub enum DaemonCommand { /// Whether the contract is in local-only mode (skips automatic completion actions). #[serde(rename = "localOnly", default)] local_only: bool, + /// Whether to auto-merge to target branch locally when local_only mode is enabled. + #[serde(rename = "autoMergeLocal", default)] + auto_merge_local: bool, /// Task ID to share worktree with (supervisor's task ID). If Some, use that task's worktree instead of creating a new one. #[serde(rename = "supervisorWorktreeTaskId", default, skip_serializing_if = "Option::is_none")] supervisor_worktree_task_id: Option<Uuid>, diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs index 4411747..a6b5b05 100644 --- a/makima/src/db/models.rs +++ b/makima/src/db/models.rs @@ -1448,6 +1448,11 @@ pub struct Contract { /// allowing users to manually handle code changes via patch files or other means. #[serde(default)] pub local_only: bool, + /// Whether to auto-merge to target branch locally when local_only mode is enabled. + /// When both local_only and auto_merge_local are true, completed task changes will be + /// automatically merged to the master/main branch locally (without pushing or creating PRs). + #[serde(default)] + pub auto_merge_local: bool, /// Whether to spawn a red team task to monitor work tasks. /// When enabled, a parallel task monitors outputs and can alert /// the supervisor about potential issues. @@ -1641,6 +1646,9 @@ pub struct ContractSummary { /// When true, tasks do not auto-execute completion actions and work stays in worktrees. #[serde(default)] pub local_only: bool, + /// When true with local_only, automatically merge completed tasks to target branch locally. + #[serde(default)] + pub auto_merge_local: bool, /// Whether red team monitoring is enabled for this contract. #[serde(default)] pub red_team_enabled: bool, @@ -1710,6 +1718,11 @@ pub struct CreateContractRequest { /// allowing users to manually handle code changes via patch files or other means. #[serde(default)] pub local_only: Option<bool>, + /// Enable auto-merge to target branch locally when local_only mode is enabled. + /// When both local_only and auto_merge_local are true, completed task changes will be + /// automatically merged to the master/main branch locally (without pushing or creating PRs). + #[serde(default)] + pub auto_merge_local: Option<bool>, /// Enable red team monitoring for this contract. /// When enabled, a parallel task monitors work task outputs /// and can alert the supervisor about potential issues. @@ -1745,6 +1758,11 @@ pub struct UpdateContractRequest { /// allowing users to manually handle code changes via patch files or other means. #[serde(default)] pub local_only: Option<bool>, + /// Enable or disable auto-merge to target branch locally when local_only mode is enabled. + /// When both local_only and auto_merge_local are true, completed task changes will be + /// automatically merged to the master/main branch locally (without pushing or creating PRs). + #[serde(default)] + pub auto_merge_local: Option<bool>, /// Version for optimistic locking pub version: Option<i32>, } diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs index 9a1bf2d..9fc2c84 100644 --- a/makima/src/db/repository.rs +++ b/makima/src/db/repository.rs @@ -2462,6 +2462,7 @@ 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); let local_only = req.local_only.unwrap_or(false); + let auto_merge_local = req.auto_merge_local.unwrap_or(false); let red_team_enabled = req.red_team_enabled.unwrap_or(false); // Serialize phase_config to JSON @@ -2469,8 +2470,8 @@ pub async fn create_contract_for_owner( sqlx::query_as::<_, Contract>( r#" - INSERT INTO contracts (owner_id, name, description, contract_type, phase, autonomous_loop, phase_guard, local_only, red_team_enabled, red_team_prompt, phase_config) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) + INSERT INTO contracts (owner_id, name, description, contract_type, phase, autonomous_loop, phase_guard, local_only, auto_merge_local, red_team_enabled, red_team_prompt, phase_config) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING * "#, ) @@ -2482,6 +2483,7 @@ pub async fn create_contract_for_owner( .bind(autonomous_loop) .bind(phase_guard) .bind(local_only) + .bind(auto_merge_local) .bind(red_team_enabled) .bind(&req.red_team_prompt) .bind(phase_config_json) @@ -2517,7 +2519,7 @@ pub async fn list_contracts_for_owner( r#" SELECT c.id, c.name, c.description, c.contract_type, c.phase, c.status, - c.supervisor_task_id, c.local_only, c.red_team_enabled, c.version, c.created_at, + c.supervisor_task_id, c.local_only, c.auto_merge_local, c.red_team_enabled, c.version, c.created_at, (SELECT COUNT(*) FROM files WHERE contract_id = c.id) as file_count, (SELECT COUNT(*) FROM tasks WHERE contract_id = c.id) as task_count, (SELECT COUNT(*) FROM contract_repositories WHERE contract_id = c.id) as repository_count @@ -2541,7 +2543,7 @@ pub async fn get_contract_summary_for_owner( r#" SELECT c.id, c.name, c.description, c.contract_type, c.phase, c.status, - c.supervisor_task_id, c.local_only, c.red_team_enabled, c.version, c.created_at, + c.supervisor_task_id, c.local_only, c.auto_merge_local, c.red_team_enabled, c.version, c.created_at, (SELECT COUNT(*) FROM files WHERE contract_id = c.id) as file_count, (SELECT COUNT(*) FROM tasks WHERE contract_id = c.id) as task_count, (SELECT COUNT(*) FROM contract_repositories WHERE contract_id = c.id) as repository_count @@ -2586,14 +2588,15 @@ pub async fn update_contract_for_owner( let autonomous_loop = req.autonomous_loop.unwrap_or(existing.autonomous_loop); let phase_guard = req.phase_guard.unwrap_or(existing.phase_guard); let local_only = req.local_only.unwrap_or(existing.local_only); + let auto_merge_local = req.auto_merge_local.unwrap_or(existing.auto_merge_local); 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, phase_guard = $9, local_only = $10, version = version + 1, updated_at = NOW() - WHERE id = $1 AND owner_id = $2 AND version = $11 + supervisor_task_id = $7, autonomous_loop = $8, phase_guard = $9, local_only = $10, auto_merge_local = $11, version = version + 1, updated_at = NOW() + WHERE id = $1 AND owner_id = $2 AND version = $12 RETURNING * "#, ) @@ -2607,6 +2610,7 @@ pub async fn update_contract_for_owner( .bind(autonomous_loop) .bind(phase_guard) .bind(local_only) + .bind(auto_merge_local) .bind(req.version.unwrap()) .fetch_optional(pool) .await? @@ -2615,7 +2619,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, phase_guard = $9, local_only = $10, version = version + 1, updated_at = NOW() + supervisor_task_id = $7, autonomous_loop = $8, phase_guard = $9, local_only = $10, auto_merge_local = $11, version = version + 1, updated_at = NOW() WHERE id = $1 AND owner_id = $2 RETURNING * "#, @@ -2630,6 +2634,7 @@ pub async fn update_contract_for_owner( .bind(autonomous_loop) .bind(phase_guard) .bind(local_only) + .bind(auto_merge_local) .fetch_optional(pool) .await? }; diff --git a/makima/src/server/handlers/contract_chat.rs b/makima/src/server/handlers/contract_chat.rs index a066595..b5255f5 100644 --- a/makima/src/server/handlers/contract_chat.rs +++ b/makima/src/server/handlers/contract_chat.rs @@ -1571,14 +1571,14 @@ 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 { + // Get local_only and auto_merge_local from contract if task has one + let (local_only, auto_merge_local) = 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, + Ok(Some(contract)) => (contract.local_only, contract.auto_merge_local), + _ => (false, false), } } else { - false + (false, false) }; // Send SpawnTask command to daemon @@ -1604,6 +1604,7 @@ async fn handle_contract_request( patch_data: None, patch_base_sha: None, local_only, + auto_merge_local, supervisor_worktree_task_id: None, // Not spawned by supervisor }; @@ -2593,6 +2594,7 @@ async fn handle_contract_request( autonomous_loop: None, phase_guard: None, local_only: None, + auto_merge_local: None, red_team_enabled: None, red_team_prompt: None, template_id: None, diff --git a/makima/src/server/handlers/contracts.rs b/makima/src/server/handlers/contracts.rs index 9979c1f..6c237dc 100644 --- a/makima/src/server/handlers/contracts.rs +++ b/makima/src/server/handlers/contracts.rs @@ -369,6 +369,7 @@ pub async fn create_contract( status: contract.status, supervisor_task_id: contract.supervisor_task_id, local_only: contract.local_only, + auto_merge_local: contract.auto_merge_local, red_team_enabled: contract.red_team_enabled, file_count: 0, task_count: 0, @@ -392,6 +393,7 @@ pub async fn create_contract( status: contract.status, supervisor_task_id: contract.supervisor_task_id, local_only: contract.local_only, + auto_merge_local: contract.auto_merge_local, red_team_enabled: contract.red_team_enabled, file_count: 0, task_count: 0, @@ -522,6 +524,7 @@ pub async fn update_contract( status: contract.status, supervisor_task_id: contract.supervisor_task_id, local_only: contract.local_only, + auto_merge_local: contract.auto_merge_local, red_team_enabled: contract.red_team_enabled, file_count: 0, task_count: 0, @@ -1408,6 +1411,7 @@ pub async fn change_phase( status: updated_contract.status, supervisor_task_id: updated_contract.supervisor_task_id, local_only: updated_contract.local_only, + auto_merge_local: updated_contract.auto_merge_local, red_team_enabled: updated_contract.red_team_enabled, file_count: 0, task_count: 0, diff --git a/makima/src/server/handlers/mesh.rs b/makima/src/server/handlers/mesh.rs index 9ef6248..af77b56 100644 --- a/makima/src/server/handlers/mesh.rs +++ b/makima/src/server/handlers/mesh.rs @@ -601,14 +601,14 @@ 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 { + // Get local_only and auto_merge_local flags from contract if task has one + let (local_only, auto_merge_local) = 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, + Ok(Some(contract)) => (contract.local_only, contract.auto_merge_local), + _ => (false, false), } } else { - false + (false, false) }; // Get list of daemons that have previously failed this task @@ -707,6 +707,7 @@ pub async fn start_task( patch_data: None, patch_base_sha: None, local_only, + auto_merge_local, supervisor_worktree_task_id: None, // Not spawned by supervisor }; @@ -761,6 +762,7 @@ pub async fn start_task( patch_data: None, patch_base_sha: None, local_only, + auto_merge_local, supervisor_worktree_task_id: None, // Not spawned by supervisor }; @@ -1144,14 +1146,14 @@ 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 { + // Get local_only and auto_merge_local from contract if task has one + let (local_only, auto_merge_local) = 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, + Ok(Some(contract)) => (contract.local_only, contract.auto_merge_local), + _ => (false, false), } } else { - false + (false, false) }; // Send spawn command to new daemon @@ -1177,6 +1179,7 @@ pub async fn send_message( patch_data: None, patch_base_sha: None, local_only, + auto_merge_local, supervisor_worktree_task_id: None, // Not spawned by supervisor }; @@ -2689,14 +2692,14 @@ 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 { + // Get local_only and auto_merge_local from contract if task has one + let (local_only, auto_merge_local) = 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, + Ok(Some(contract)) => (contract.local_only, contract.auto_merge_local), + _ => (false, false), } } else { - false + (false, false) }; // Send SpawnTask command to daemon for the new task @@ -2722,6 +2725,7 @@ pub async fn reassign_task( patch_data, patch_base_sha, local_only, + auto_merge_local, supervisor_worktree_task_id: None, // Not spawned by supervisor }; @@ -3028,14 +3032,14 @@ 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 { + // Get local_only and auto_merge_local from contract if task has one + let (local_only, auto_merge_local) = 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, + Ok(Some(contract)) => (contract.local_only, contract.auto_merge_local), + _ => (false, false), } } else { - false + (false, false) }; // Send SpawnTask command to daemon @@ -3061,6 +3065,7 @@ pub async fn continue_task( patch_data: None, patch_base_sha: None, local_only, + auto_merge_local, supervisor_worktree_task_id: None, // Not spawned by supervisor }; @@ -3989,6 +3994,7 @@ pub async fn branch_task( patch_data, patch_base_sha, local_only: false, // No contract, so not local_only + auto_merge_local: false, // No contract, so no auto_merge_local supervisor_worktree_task_id: None, // Not spawned by supervisor }; diff --git a/makima/src/server/handlers/mesh_chat.rs b/makima/src/server/handlers/mesh_chat.rs index 623e66d..eee899f 100644 --- a/makima/src/server/handlers/mesh_chat.rs +++ b/makima/src/server/handlers/mesh_chat.rs @@ -1133,14 +1133,14 @@ 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 { + // Get local_only and auto_merge_local from contract if task has one + let (local_only, auto_merge_local) = 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, + Ok(Some(contract)) => (contract.local_only, contract.auto_merge_local), + _ => (false, false), } } else { - false + (false, false) }; // Send SpawnTask command to daemon @@ -1166,6 +1166,7 @@ async fn handle_mesh_request( patch_data: None, patch_base_sha: None, local_only, + auto_merge_local, supervisor_worktree_task_id: None, // Not spawned by supervisor }; diff --git a/makima/src/server/handlers/mesh_supervisor.rs b/makima/src/server/handlers/mesh_supervisor.rs index 5e74251..3411ec0 100644 --- a/makima/src/server/handlers/mesh_supervisor.rs +++ b/makima/src/server/handlers/mesh_supervisor.rs @@ -410,6 +410,7 @@ pub async fn try_start_pending_task( patch_data, patch_base_sha, local_only: contract.local_only, + auto_merge_local: contract.auto_merge_local, // For retried tasks, use their own worktree (they already have state from previous attempt) supervisor_worktree_task_id: None, }; @@ -730,6 +731,7 @@ pub async fn spawn_task( patch_data: None, patch_base_sha: None, local_only: contract.local_only, + auto_merge_local: contract.auto_merge_local, // Share supervisor's worktree by default; separate worktree only when explicitly requested supervisor_worktree_task_id: if request.use_own_worktree { None } else { Some(supervisor_id) }, }; @@ -2257,6 +2259,7 @@ pub async fn resume_supervisor( patch_data, patch_base_sha, local_only: contract.local_only, + auto_merge_local: contract.auto_merge_local, supervisor_worktree_task_id: None, // Supervisor uses its own worktree }; @@ -2703,6 +2706,7 @@ pub async fn spawn_red_team_task( patch_data: None, patch_base_sha: None, local_only: true, // Red team is always local-only + auto_merge_local: false, // Red team doesn't auto-merge supervisor_worktree_task_id: None, }; diff --git a/makima/src/server/handlers/transcript_analysis.rs b/makima/src/server/handlers/transcript_analysis.rs index 920851c..d987d08 100644 --- a/makima/src/server/handlers/transcript_analysis.rs +++ b/makima/src/server/handlers/transcript_analysis.rs @@ -279,6 +279,7 @@ pub async fn create_contract_from_analysis( autonomous_loop: None, phase_guard: None, local_only: None, + auto_merge_local: None, red_team_enabled: None, red_team_prompt: None, template_id: None, diff --git a/makima/src/server/state.rs b/makima/src/server/state.rs index 8a9bb7f..f662e30 100644 --- a/makima/src/server/state.rs +++ b/makima/src/server/state.rs @@ -274,6 +274,9 @@ pub enum DaemonCommand { /// Whether the contract is in local-only mode (skips automatic completion actions) #[serde(rename = "localOnly", default)] local_only: bool, + /// Whether to auto-merge to target branch locally when local_only mode is enabled + #[serde(rename = "autoMergeLocal", default)] + auto_merge_local: bool, /// Task ID to share worktree with (supervisor's task ID). If Some, use that task's worktree instead of creating a new one. #[serde(rename = "supervisorWorktreeTaskId", default, skip_serializing_if = "Option::is_none")] supervisor_worktree_task_id: Option<Uuid>, |
