diff options
| author | soryu <soryu@soryu.co> | 2026-01-27 01:25:29 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-27 01:25:40 +0000 |
| commit | b0d0b4848b2fc8a44c2575e09a08b34aaf6e1484 (patch) | |
| tree | bd0dedfd8a3623d01f28ff590e97a028bc5456c5 | |
| parent | b28345d15730ffbefe81244d06c06fe13c30b0ea (diff) | |
| download | soryu-b0d0b4848b2fc8a44c2575e09a08b34aaf6e1484.tar.gz soryu-b0d0b4848b2fc8a44c2575e09a08b34aaf6e1484.zip | |
Default to shared worktree and add worktree endpoint
| -rw-r--r-- | makima/src/bin/makima.rs | 1 | ||||
| -rw-r--r-- | makima/src/daemon/api/supervisor.rs | 4 | ||||
| -rw-r--r-- | makima/src/daemon/cli/supervisor.rs | 4 | ||||
| -rw-r--r-- | makima/src/daemon/task/manager.rs | 208 | ||||
| -rw-r--r-- | makima/src/daemon/ws/protocol.rs | 37 | ||||
| -rw-r--r-- | makima/src/db/repository.rs | 3 | ||||
| -rw-r--r-- | makima/src/server/handlers/contract_chat.rs | 1 | ||||
| -rw-r--r-- | makima/src/server/handlers/mesh.rs | 158 | ||||
| -rw-r--r-- | makima/src/server/handlers/mesh_chat.rs | 1 | ||||
| -rw-r--r-- | makima/src/server/handlers/mesh_supervisor.rs | 9 | ||||
| -rw-r--r-- | makima/src/server/mod.rs | 1 | ||||
| -rw-r--r-- | makima/src/server/state.rs | 9 |
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")] |
