summaryrefslogtreecommitdiff
path: root/makima/src/daemon/task
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 /makima/src/daemon/task
parentb28345d15730ffbefe81244d06c06fe13c30b0ea (diff)
downloadsoryu-b0d0b4848b2fc8a44c2575e09a08b34aaf6e1484.tar.gz
soryu-b0d0b4848b2fc8a44c2575e09a08b34aaf6e1484.zip
Default to shared worktree and add worktree endpoint
Diffstat (limited to 'makima/src/daemon/task')
-rw-r--r--makima/src/daemon/task/manager.rs208
1 files changed, 206 insertions, 2 deletions
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,