summaryrefslogtreecommitdiff
path: root/makima/src
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src')
-rw-r--r--makima/src/daemon/task/manager.rs22
-rw-r--r--makima/src/daemon/ws/protocol.rs3
-rw-r--r--makima/src/db/models.rs11
-rw-r--r--makima/src/server/handlers/contract_chat.rs11
-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.rs13
-rw-r--r--makima/src/server/state.rs3
8 files changed, 110 insertions, 10 deletions
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 {