summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-27 01:25:29 +0000
committersoryu <soryu@soryu.co>2026-01-27 01:25:40 +0000
commitb0d0b4848b2fc8a44c2575e09a08b34aaf6e1484 (patch)
treebd0dedfd8a3623d01f28ff590e97a028bc5456c5
parentb28345d15730ffbefe81244d06c06fe13c30b0ea (diff)
downloadsoryu-b0d0b4848b2fc8a44c2575e09a08b34aaf6e1484.tar.gz
soryu-b0d0b4848b2fc8a44c2575e09a08b34aaf6e1484.zip
Default to shared worktree and add worktree endpoint
-rw-r--r--makima/src/bin/makima.rs1
-rw-r--r--makima/src/daemon/api/supervisor.rs4
-rw-r--r--makima/src/daemon/cli/supervisor.rs4
-rw-r--r--makima/src/daemon/task/manager.rs208
-rw-r--r--makima/src/daemon/ws/protocol.rs37
-rw-r--r--makima/src/db/repository.rs3
-rw-r--r--makima/src/server/handlers/contract_chat.rs1
-rw-r--r--makima/src/server/handlers/mesh.rs158
-rw-r--r--makima/src/server/handlers/mesh_chat.rs1
-rw-r--r--makima/src/server/handlers/mesh_supervisor.rs9
-rw-r--r--makima/src/server/mod.rs1
-rw-r--r--makima/src/server/state.rs9
12 files changed, 433 insertions, 3 deletions
diff --git a/makima/src/bin/makima.rs b/makima/src/bin/makima.rs
index 6976106..ffb9364 100644
--- a/makima/src/bin/makima.rs
+++ b/makima/src/bin/makima.rs
@@ -350,6 +350,7 @@ async fn run_supervisor(
contract_id: args.common.contract_id,
parent_task_id: args.parent,
checkpoint_sha: args.checkpoint,
+ use_own_worktree: args.own_worktree,
};
let result = client.supervisor_spawn(req).await?;
println!("{}", serde_json::to_string(&result.0)?);
diff --git a/makima/src/daemon/api/supervisor.rs b/makima/src/daemon/api/supervisor.rs
index e79a9bb..6b99de0 100644
--- a/makima/src/daemon/api/supervisor.rs
+++ b/makima/src/daemon/api/supervisor.rs
@@ -17,6 +17,10 @@ pub struct SpawnTaskRequest {
pub parent_task_id: Option<Uuid>,
#[serde(skip_serializing_if = "Option::is_none")]
pub checkpoint_sha: Option<String>,
+ /// If true, create a separate worktree for the task (requires merge after).
+ /// If false (default), the task shares the supervisor's worktree.
+ #[serde(default)]
+ pub use_own_worktree: bool,
}
#[derive(Serialize)]
diff --git a/makima/src/daemon/cli/supervisor.rs b/makima/src/daemon/cli/supervisor.rs
index 4f36fd8..09f61db 100644
--- a/makima/src/daemon/cli/supervisor.rs
+++ b/makima/src/daemon/cli/supervisor.rs
@@ -48,6 +48,10 @@ pub struct SpawnArgs {
/// Repository URL (local path or remote URL). If not provided, will try to detect from current directory.
#[arg(long)]
pub repo: Option<String>,
+
+ /// Create a separate worktree for the task (requires merge after). By default, tasks share the supervisor's worktree.
+ #[arg(long)]
+ pub own_worktree: bool,
}
/// Arguments for wait command.
diff --git a/makima/src/daemon/task/manager.rs b/makima/src/daemon/task/manager.rs
index 9dd4506..075234f 100644
--- a/makima/src/daemon/task/manager.rs
+++ b/makima/src/daemon/task/manager.rs
@@ -1696,6 +1696,7 @@ impl TaskManager {
patch_data,
patch_base_sha,
local_only,
+ supervisor_worktree_task_id,
} => {
tracing::info!(
task_id = %task_id,
@@ -1714,6 +1715,7 @@ impl TaskManager {
continue_from_task_id = ?continue_from_task_id,
copy_files = ?copy_files,
contract_id = ?contract_id,
+ supervisor_worktree_task_id = ?supervisor_worktree_task_id,
plan_len = plan.len(),
"Spawning new task"
);
@@ -1723,6 +1725,7 @@ impl TaskManager {
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,
+ supervisor_worktree_task_id,
).await?;
}
DaemonCommand::PauseTask { task_id } => {
@@ -1824,6 +1827,7 @@ impl TaskManager {
None, // patch_data - not available for respawn
None, // patch_base_sha - not available for respawn
local_only,
+ None, // supervisor_worktree_task_id - supervisors use their own worktree
).await {
tracing::error!(
task_id = %task_id,
@@ -1993,6 +1997,12 @@ impl TaskManager {
tracing::info!(task_id = %task_id, "Getting task diff");
self.handle_get_task_diff(task_id).await?;
}
+ DaemonCommand::GetWorktreeInfo {
+ task_id,
+ } => {
+ tracing::info!(task_id = %task_id, "Getting worktree info");
+ self.handle_get_worktree_info(task_id).await?;
+ }
DaemonCommand::CreateCheckpoint {
task_id,
message,
@@ -2057,6 +2067,7 @@ impl TaskManager {
patch_data: Option<String>,
patch_base_sha: Option<String>,
local_only: 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 ===");
@@ -2135,6 +2146,7 @@ impl TaskManager {
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,
+ supervisor_worktree_task_id,
).await {
tracing::error!(task_id = %task_id, error = %e, "Task execution failed");
inner.mark_failed(task_id, &e.to_string()).await;
@@ -3245,6 +3257,160 @@ impl TaskManager {
Ok(())
}
+ /// Handle GetWorktreeInfo command - get worktree files, stats, branch info.
+ async fn handle_get_worktree_info(
+ &self,
+ task_id: Uuid,
+ ) -> Result<(), DaemonError> {
+ // Get task's worktree path and branch
+ let task_info = {
+ let tasks = self.tasks.read().await;
+ tasks.get(&task_id).map(|t| (
+ t.worktree.as_ref().map(|w| w.path.clone()),
+ t.worktree.as_ref().map(|w| w.branch.clone()),
+ ))
+ };
+
+ let (worktree_path, branch) = match task_info {
+ Some((Some(path), branch)) => (Some(path), branch),
+ Some((None, _)) => (None, None),
+ None => (None, None),
+ };
+
+ if worktree_path.is_none() {
+ let msg = DaemonMessage::WorktreeInfoResult {
+ task_id,
+ success: true,
+ worktree_path: None,
+ exists: false,
+ files_changed: 0,
+ insertions: 0,
+ deletions: 0,
+ files: None,
+ branch: None,
+ head_sha: None,
+ error: None,
+ };
+ let _ = self.ws_tx.send(msg).await;
+ return Ok(());
+ }
+
+ let path = worktree_path.unwrap();
+ let path_str = path.to_string_lossy().to_string();
+
+ // Check if worktree exists
+ if !path.exists() {
+ let msg = DaemonMessage::WorktreeInfoResult {
+ task_id,
+ success: true,
+ worktree_path: Some(path_str),
+ exists: false,
+ files_changed: 0,
+ insertions: 0,
+ deletions: 0,
+ files: None,
+ branch,
+ head_sha: None,
+ error: None,
+ };
+ let _ = self.ws_tx.send(msg).await;
+ return Ok(());
+ }
+
+ // Get HEAD SHA
+ let head_sha = match tokio::process::Command::new("git")
+ .current_dir(&path)
+ .args(["rev-parse", "HEAD"])
+ .output()
+ .await
+ {
+ Ok(output) if output.status.success() => {
+ Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
+ }
+ _ => None,
+ };
+
+ // Get changed files with status using git status --porcelain
+ let status_output = tokio::process::Command::new("git")
+ .current_dir(&path)
+ .args(["status", "--porcelain"])
+ .output()
+ .await;
+
+ let status_lines: Vec<(String, String)> = match status_output {
+ Ok(output) if output.status.success() => {
+ String::from_utf8_lossy(&output.stdout)
+ .lines()
+ .filter_map(|line| {
+ if line.len() < 3 {
+ return None;
+ }
+ let status = line[0..2].trim().to_string();
+ let file_path = line[3..].to_string();
+ Some((file_path, status))
+ })
+ .collect()
+ }
+ _ => vec![],
+ };
+
+ // Get numstat for line counts (staged + unstaged)
+ let numstat_output = tokio::process::Command::new("git")
+ .current_dir(&path)
+ .args(["diff", "HEAD", "--numstat"])
+ .output()
+ .await;
+
+ let mut file_stats: std::collections::HashMap<String, (i32, i32)> = std::collections::HashMap::new();
+ if let Ok(output) = numstat_output {
+ if output.status.success() {
+ for line in String::from_utf8_lossy(&output.stdout).lines() {
+ let parts: Vec<&str> = line.split('\t').collect();
+ if parts.len() >= 3 {
+ let added = parts[0].parse::<i32>().unwrap_or(0);
+ let removed = parts[1].parse::<i32>().unwrap_or(0);
+ let file = parts[2].to_string();
+ file_stats.insert(file, (added, removed));
+ }
+ }
+ }
+ }
+
+ // Build file list with stats
+ let mut files_json = Vec::new();
+ let mut total_insertions = 0;
+ let mut total_deletions = 0;
+
+ for (file_path, status) in &status_lines {
+ let (lines_added, lines_removed) = file_stats.get(file_path).copied().unwrap_or((0, 0));
+ total_insertions += lines_added;
+ total_deletions += lines_removed;
+
+ files_json.push(serde_json::json!({
+ "path": file_path,
+ "status": status,
+ "linesAdded": lines_added,
+ "linesRemoved": lines_removed,
+ }));
+ }
+
+ let msg = DaemonMessage::WorktreeInfoResult {
+ task_id,
+ success: true,
+ worktree_path: Some(path_str),
+ exists: true,
+ files_changed: status_lines.len() as i32,
+ insertions: total_insertions,
+ deletions: total_deletions,
+ files: Some(serde_json::Value::Array(files_json)),
+ branch,
+ head_sha,
+ error: None,
+ };
+ let _ = self.ws_tx.send(msg).await;
+ Ok(())
+ }
+
/// Handle CreateCheckpoint command - stage all changes, commit, and get stats.
async fn handle_create_checkpoint(
&self,
@@ -3685,6 +3851,7 @@ impl TaskManagerInner {
patch_data: Option<String>,
patch_base_sha: Option<String>,
local_only: 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 ===");
@@ -3780,8 +3947,45 @@ impl TaskManagerInner {
};
// Determine working directory
- let has_existing_worktree = existing_worktree.is_some() || restored_from_patch.is_some();
- let working_dir = if let Some(existing) = existing_worktree {
+ // First check if we should share a supervisor's worktree
+ let shared_supervisor_worktree = if let Some(supervisor_task_id) = supervisor_worktree_task_id {
+ match self.find_worktree_for_task(supervisor_task_id).await {
+ Ok(path) => {
+ tracing::info!(
+ task_id = %task_id,
+ supervisor_task_id = %supervisor_task_id,
+ path = %path.display(),
+ "Using shared worktree from supervisor"
+ );
+ let msg = DaemonMessage::task_output(
+ task_id,
+ format!("Using shared worktree from supervisor: {}\n", path.display()),
+ false,
+ );
+ let _ = self.ws_tx.send(msg).await;
+ Some(path)
+ }
+ Err(e) => {
+ tracing::error!(
+ task_id = %task_id,
+ supervisor_task_id = %supervisor_task_id,
+ error = %e,
+ "Supervisor worktree not found"
+ );
+ return Err(DaemonError::Task(TaskError::SetupFailed(
+ format!("Supervisor worktree not found for task {}: {}", supervisor_task_id, e)
+ )));
+ }
+ }
+ } else {
+ None
+ };
+
+ let has_existing_worktree = existing_worktree.is_some() || restored_from_patch.is_some() || shared_supervisor_worktree.is_some();
+ let working_dir = if let Some(shared_path) = shared_supervisor_worktree {
+ // Use supervisor's worktree directly (no copy, no new branch)
+ shared_path
+ } else if let Some(existing) = existing_worktree {
// Reuse existing worktree for session resume
let msg = DaemonMessage::task_output(
task_id,
diff --git a/makima/src/daemon/ws/protocol.rs b/makima/src/daemon/ws/protocol.rs
index 018dc7b..6e4f5cf 100644
--- a/makima/src/daemon/ws/protocol.rs
+++ b/makima/src/daemon/ws/protocol.rs
@@ -270,6 +270,34 @@ pub enum DaemonMessage {
error: Option<String>,
},
+ /// Response to GetWorktreeInfo command.
+ WorktreeInfoResult {
+ #[serde(rename = "taskId")]
+ task_id: Uuid,
+ success: bool,
+ /// Path to the worktree directory
+ #[serde(rename = "worktreePath")]
+ worktree_path: Option<String>,
+ /// Whether the worktree exists
+ exists: bool,
+ /// Number of files changed
+ #[serde(rename = "filesChanged")]
+ files_changed: i32,
+ /// Total lines inserted
+ insertions: i32,
+ /// Total lines deleted
+ deletions: i32,
+ /// Changed files list: [{path, status, linesAdded, linesRemoved}]
+ files: Option<serde_json::Value>,
+ /// Current branch name
+ branch: Option<String>,
+ /// Current HEAD commit SHA
+ #[serde(rename = "headSha")]
+ head_sha: Option<String>,
+ /// Error message if failed
+ error: Option<String>,
+ },
+
/// Response to CreateCheckpoint command.
CheckpointCreated {
#[serde(rename = "taskId")]
@@ -449,6 +477,9 @@ pub enum DaemonCommand {
/// Whether the contract is in local-only mode (skips automatic completion actions).
#[serde(rename = "localOnly", default)]
local_only: 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>,
},
/// Pause a running task.
@@ -656,6 +687,12 @@ pub enum DaemonCommand {
task_id: Uuid,
},
+ /// Get worktree information (files, stats, branch) for a task.
+ GetWorktreeInfo {
+ #[serde(rename = "taskId")]
+ task_id: Uuid,
+ },
+
/// Create a checkpoint (stage changes, commit, get stats).
CreateCheckpoint {
#[serde(rename = "taskId")]
diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs
index de1712d..7c9154f 100644
--- a/makima/src/db/repository.rs
+++ b/makima/src/db/repository.rs
@@ -2152,7 +2152,7 @@ pub async fn create_contract_for_owner(
let contract_type = req.contract_type.as_deref().unwrap_or("simple");
// Validate contract type
- let valid_types = ["simple", "specification"];
+ let valid_types = ["simple", "specification", "execute"];
if !valid_types.contains(&contract_type) {
return Err(sqlx::Error::Protocol(format!(
"Invalid contract_type '{}'. Must be one of: {}",
@@ -2165,6 +2165,7 @@ pub async fn create_contract_for_owner(
let (valid_phases, default_phase): (&[&str], &str) = match contract_type {
"simple" => (&["plan", "execute"], "plan"),
"specification" => (&["research", "specify", "plan", "execute", "review"], "research"),
+ "execute" => (&["execute"], "execute"),
_ => (&["plan", "execute"], "plan"),
};
diff --git a/makima/src/server/handlers/contract_chat.rs b/makima/src/server/handlers/contract_chat.rs
index dac806a..98787be 100644
--- a/makima/src/server/handlers/contract_chat.rs
+++ b/makima/src/server/handlers/contract_chat.rs
@@ -1602,6 +1602,7 @@ async fn handle_contract_request(
patch_data: None,
patch_base_sha: None,
local_only,
+ supervisor_worktree_task_id: None, // Not spawned by supervisor
};
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 c4d862c..4b82ed7 100644
--- a/makima/src/server/handlers/mesh.rs
+++ b/makima/src/server/handlers/mesh.rs
@@ -705,6 +705,7 @@ pub async fn start_task(
patch_data: None,
patch_base_sha: None,
local_only,
+ supervisor_worktree_task_id: None, // Not spawned by supervisor
};
tracing::info!(
@@ -758,6 +759,7 @@ pub async fn start_task(
patch_data: None,
patch_base_sha: None,
local_only,
+ supervisor_worktree_task_id: None, // Not spawned by supervisor
};
if state.send_daemon_command(alt_daemon_id, alt_command).await.is_ok() {
@@ -1173,6 +1175,7 @@ pub async fn send_message(
patch_data: None,
patch_base_sha: None,
local_only,
+ supervisor_worktree_task_id: None, // Not spawned by supervisor
};
if state.send_daemon_command(new_daemon_id, spawn_cmd).await.is_ok() {
@@ -1890,6 +1893,158 @@ pub async fn clone_worktree(
.into_response()
}
+// =============================================================================
+// Worktree Info
+// =============================================================================
+
+/// Response for worktree info.
+#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct WorktreeInfoResponse {
+ /// Task ID.
+ pub task_id: Uuid,
+ /// Path to the worktree directory.
+ pub worktree_path: Option<String>,
+ /// Whether the worktree exists.
+ pub exists: bool,
+ /// Aggregate statistics.
+ pub stats: WorktreeStats,
+ /// Changed files list.
+ pub files: Vec<WorktreeFile>,
+ /// Current branch name.
+ pub branch: Option<String>,
+ /// Current HEAD commit SHA.
+ pub head_sha: Option<String>,
+}
+
+/// Statistics about worktree changes.
+#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct WorktreeStats {
+ /// Number of files changed.
+ pub files_changed: i32,
+ /// Total lines inserted.
+ pub insertions: i32,
+ /// Total lines deleted.
+ pub deletions: i32,
+}
+
+/// Information about a changed file in the worktree.
+#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct WorktreeFile {
+ /// File path relative to worktree root.
+ pub path: String,
+ /// Git status code (M, A, D, R, C, U, ?).
+ pub status: String,
+ /// Lines added.
+ pub lines_added: i32,
+ /// Lines removed.
+ pub lines_removed: i32,
+}
+
+/// Get worktree information for a task (files, stats, branch info).
+#[utoipa::path(
+ get,
+ path = "/api/v1/mesh/tasks/{id}/worktree-info",
+ params(
+ ("id" = Uuid, Path, description = "Task ID")
+ ),
+ responses(
+ (status = 200, description = "Worktree info", body = WorktreeInfoResponse),
+ (status = 401, description = "Unauthorized", body = ApiError),
+ (status = 404, description = "Task not found", body = ApiError),
+ (status = 503, description = "Database not configured or daemon not connected", body = ApiError),
+ (status = 500, description = "Internal server error", body = ApiError),
+ ),
+ security(
+ ("bearer_auth" = []),
+ ("api_key" = [])
+ ),
+ tag = "Mesh"
+)]
+pub async fn get_worktree_info(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+) -> impl IntoResponse {
+ let Some(ref pool) = state.db_pool else {
+ return (
+ StatusCode::SERVICE_UNAVAILABLE,
+ Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")),
+ )
+ .into_response();
+ };
+
+ // Get the task (scoped by owner)
+ let task = match repository::get_task_for_owner(pool, id, auth.owner_id).await {
+ Ok(Some(t)) => t,
+ Ok(None) => {
+ return (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Task not found")),
+ )
+ .into_response();
+ }
+ Err(e) => {
+ tracing::error!("Failed to get task {}: {}", id, e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DB_ERROR", e.to_string())),
+ )
+ .into_response();
+ }
+ };
+
+ // Get daemon running the task
+ let Some(daemon_id) = task.daemon_id else {
+ // Task has no daemon, return empty worktree info
+ return Json(WorktreeInfoResponse {
+ task_id: id,
+ worktree_path: None,
+ exists: false,
+ stats: WorktreeStats {
+ files_changed: 0,
+ insertions: 0,
+ deletions: 0,
+ },
+ files: vec![],
+ branch: None,
+ head_sha: None,
+ })
+ .into_response();
+ };
+
+ // Send GetWorktreeInfo command to daemon
+ let command = DaemonCommand::GetWorktreeInfo { task_id: id };
+
+ if let Err(e) = state.send_daemon_command(daemon_id, command).await {
+ tracing::error!("Failed to send GetWorktreeInfo command: {}", e);
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DAEMON_ERROR", e)),
+ )
+ .into_response();
+ }
+
+ // Return placeholder - actual data will be streamed via WebSocket
+ // For now, return empty data indicating the request is being processed
+ Json(WorktreeInfoResponse {
+ task_id: id,
+ worktree_path: None,
+ exists: false,
+ stats: WorktreeStats {
+ files_changed: 0,
+ insertions: 0,
+ deletions: 0,
+ },
+ files: vec![],
+ branch: None,
+ head_sha: None,
+ })
+ .into_response()
+}
+
/// Request to check if a target directory exists.
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
@@ -2350,6 +2505,7 @@ pub async fn reassign_task(
patch_data,
patch_base_sha,
local_only,
+ supervisor_worktree_task_id: None, // Not spawned by supervisor
};
tracing::info!(
@@ -2688,6 +2844,7 @@ pub async fn continue_task(
patch_data: None,
patch_base_sha: None,
local_only,
+ supervisor_worktree_task_id: None, // Not spawned by supervisor
};
tracing::info!(
@@ -3612,6 +3769,7 @@ pub async fn branch_task(
patch_data,
patch_base_sha,
local_only: false, // No contract, so not local_only
+ supervisor_worktree_task_id: None, // Not spawned by supervisor
};
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 ed6cfc0..a74eb4f 100644
--- a/makima/src/server/handlers/mesh_chat.rs
+++ b/makima/src/server/handlers/mesh_chat.rs
@@ -1165,6 +1165,7 @@ async fn handle_mesh_request(
patch_data: None,
patch_base_sha: None,
local_only,
+ supervisor_worktree_task_id: None, // Not spawned by supervisor
};
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 24ba4bb..1a2e47f 100644
--- a/makima/src/server/handlers/mesh_supervisor.rs
+++ b/makima/src/server/handlers/mesh_supervisor.rs
@@ -36,6 +36,10 @@ pub struct SpawnTaskRequest {
pub checkpoint_sha: Option<String>,
/// Repository URL for the task (optional - if not provided, will be looked up from contract).
pub repository_url: Option<String>,
+ /// If true, create a separate worktree for the task (requires merge after).
+ /// If false (default), the task shares the supervisor's worktree.
+ #[serde(default)]
+ pub use_own_worktree: bool,
}
/// Request to wait for task completion.
@@ -406,6 +410,8 @@ pub async fn try_start_pending_task(
patch_data,
patch_base_sha,
local_only: contract.local_only,
+ // For retried tasks, use their own worktree (they already have state from previous attempt)
+ supervisor_worktree_task_id: None,
};
if let Err(e) = state.send_daemon_command(daemon.id, cmd).await {
@@ -720,6 +726,8 @@ pub async fn spawn_task(
patch_data: None,
patch_base_sha: None,
local_only: contract.local_only,
+ // 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) },
};
if let Err(e) = state.send_daemon_command(daemon.id, cmd).await {
@@ -2248,6 +2256,7 @@ pub async fn resume_supervisor(
patch_data,
patch_base_sha,
local_only: contract.local_only,
+ supervisor_worktree_task_id: None, // Supervisor uses its own worktree
};
if let Err(e) = state.send_daemon_command(target_daemon_id, command).await {
diff --git a/makima/src/server/mod.rs b/makima/src/server/mod.rs
index b002a49..1c4229e 100644
--- a/makima/src/server/mod.rs
+++ b/makima/src/server/mod.rs
@@ -81,6 +81,7 @@ pub fn make_router(state: SharedState) -> Router {
.route("/mesh/tasks/{id}/message", post(mesh::send_message))
.route("/mesh/tasks/{id}/retry-completion", post(mesh::retry_completion_action))
.route("/mesh/tasks/{id}/clone", post(mesh::clone_worktree))
+ .route("/mesh/tasks/{id}/worktree-info", get(mesh::get_worktree_info))
.route("/mesh/tasks/{id}/check-target", post(mesh::check_target_exists))
.route("/mesh/tasks/{id}/reassign", post(mesh::reassign_task))
.route("/mesh/tasks/{id}/continue", post(mesh::continue_task))
diff --git a/makima/src/server/state.rs b/makima/src/server/state.rs
index 02a2328..b3cf27d 100644
--- a/makima/src/server/state.rs
+++ b/makima/src/server/state.rs
@@ -256,6 +256,9 @@ pub enum DaemonCommand {
/// Whether the contract is in local-only mode (skips automatic completion actions)
#[serde(rename = "localOnly", default)]
local_only: 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>,
},
/// Pause a running task
PauseTask {
@@ -451,6 +454,12 @@ pub enum DaemonCommand {
task_id: Uuid,
},
+ /// Get worktree information (files, stats, branch) for a task
+ GetWorktreeInfo {
+ #[serde(rename = "taskId")]
+ task_id: Uuid,
+ },
+
/// Create a git checkpoint (stage changes, commit, record stats)
CreateCheckpoint {
#[serde(rename = "taskId")]