summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-30 03:28:40 +0000
committersoryu <soryu@soryu.co>2026-01-30 03:28:40 +0000
commit4c09aa13a50064a4145ef53021490d303e46bc5e (patch)
treed1fa901d97e54a7840e5ac153270bdab66bb8c99
parentdac1adb138f532245a36fa16524f1e4fb9990173 (diff)
downloadsoryu-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.tsx2
-rw-r--r--frontend/src/components/ContractList.tsx2
-rw-r--r--makima/migrations/20260130000001_add_auto_merge_local.sql8
-rw-r--r--makima/src/bin/makima.rs1
-rw-r--r--makima/src/daemon/api/contract.rs2
-rw-r--r--makima/src/daemon/task/manager.rs40
-rw-r--r--makima/src/daemon/ws/protocol.rs3
-rw-r--r--makima/src/db/models.rs18
-rw-r--r--makima/src/db/repository.rs19
-rw-r--r--makima/src/server/handlers/contract_chat.rs12
-rw-r--r--makima/src/server/handlers/contracts.rs4
-rw-r--r--makima/src/server/handlers/mesh.rs46
-rw-r--r--makima/src/server/handlers/mesh_chat.rs11
-rw-r--r--makima/src/server/handlers/mesh_supervisor.rs4
-rw-r--r--makima/src/server/handlers/transcript_analysis.rs1
-rw-r--r--makima/src/server/state.rs3
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>,