From a6677bafe52d9988c9948df34c1635e4411c9591 Mon Sep 17 00:00:00 2001 From: soryu Date: Fri, 13 Feb 2026 19:19:39 +0000 Subject: Fix worktree branching for directive tasks and remove memories --- makima/src/daemon/api/directive.rs | 199 ---------------------------------- makima/src/daemon/cli/directive.rs | 47 -------- makima/src/daemon/cli/mod.rs | 18 --- makima/src/daemon/task/manager.rs | 144 ++++++++++++++++-------- makima/src/daemon/worktree/manager.rs | 79 ++++++++++++++ 5 files changed, 177 insertions(+), 310 deletions(-) (limited to 'makima/src/daemon') diff --git a/makima/src/daemon/api/directive.rs b/makima/src/daemon/api/directive.rs index fcc2ca5..a0cdab0 100644 --- a/makima/src/daemon/api/directive.rs +++ b/makima/src/daemon/api/directive.rs @@ -30,54 +30,6 @@ pub struct UpdateStepDepsRequest { pub depends_on: Vec, } -/// Percent-encode a string for use as a URL path segment. -/// -/// Encodes all characters except unreserved characters (alphanumeric, `-`, `.`, `_`, `~`). -fn percent_encode_path(s: &str) -> String { - let mut encoded = String::with_capacity(s.len()); - for byte in s.bytes() { - match byte { - b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => { - encoded.push(byte as char); - } - _ => { - encoded.push_str(&format!("%{:02X}", byte)); - } - } - } - encoded -} - -/// Request body for setting a single memory entry. -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -pub struct SetMemoryRequest { - pub key: String, - pub value: String, -} - -/// A single entry within a batch set request. -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -pub struct BatchMemoryEntry { - pub key: String, - pub value: String, -} - -/// Request body for setting multiple memory entries at once. -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -pub struct BatchSetMemoryRequest { - pub entries: Vec, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -pub struct MemorySetRequest { - pub value: String, -} - - impl ApiClient { /// List all directives. pub async fn list_directives(&self) -> Result { @@ -194,157 +146,6 @@ impl ApiClient { self.put(&format!("/api/v1/directives/{}", directive_id), &req).await } - // ── Directive Memory ────────────────────────────────────────────── - - /// List all memory entries for a directive. - pub async fn list_memories(&self, directive_id: Uuid) -> Result { - self.get(&format!("/api/v1/directives/{}/memory", directive_id)) - .await - } - - /// Get a single memory entry by key. - pub async fn get_memory( - &self, - directive_id: Uuid, - key: &str, - ) -> Result { - self.get(&format!( - "/api/v1/directives/{}/memory/{}", - directive_id, - percent_encode_path(key) - )) - .await - } - - /// Set (create or update) a single memory entry. - pub async fn set_memory( - &self, - directive_id: Uuid, - key: &str, - value: &str, - ) -> Result { - let req = SetMemoryRequest { - key: key.to_string(), - value: value.to_string(), - }; - self.put(&format!("/api/v1/directives/{}/memory", directive_id), &req) - .await - } - - /// Set multiple memory entries in a single request. - pub async fn batch_set_memories( - &self, - directive_id: Uuid, - entries: Vec<(String, String)>, - ) -> Result { - let req = BatchSetMemoryRequest { - entries: entries - .into_iter() - .map(|(key, value)| BatchMemoryEntry { key, value }) - .collect(), - }; - self.post( - &format!("/api/v1/directives/{}/memory/batch", directive_id), - &req, - ) - .await - } - - /// Delete a single memory entry by key. - pub async fn delete_memory( - &self, - directive_id: Uuid, - key: &str, - ) -> Result<(), ApiError> { - self.delete(&format!( - "/api/v1/directives/{}/memory/{}", - directive_id, - percent_encode_path(key) - )) - .await - } - - /// Clear all memory entries for a directive. - pub async fn clear_memories(&self, directive_id: Uuid) -> Result<(), ApiError> { - self.delete(&format!("/api/v1/directives/{}/memory", directive_id)) - .await - } - - // ── CLI-facing Directive Memory aliases ────────────────────────── - - /// Set a memory key-value pair for a directive (CLI-facing). - pub async fn directive_memory_set( - &self, - directive_id: Uuid, - key: &str, - value: &str, - ) -> Result { - let req = MemorySetRequest { - value: value.to_string(), - }; - self.put( - &format!("/api/v1/directives/{}/memory/{}", directive_id, key), - &req, - ) - .await - } - - /// Get a memory value by key for a directive (CLI-facing). - pub async fn directive_memory_get( - &self, - directive_id: Uuid, - key: &str, - ) -> Result { - self.get(&format!( - "/api/v1/directives/{}/memory/{}", - directive_id, key - )) - .await - } - - /// List all memory key-value pairs for a directive (CLI-facing). - pub async fn directive_memory_list( - &self, - directive_id: Uuid, - ) -> Result { - self.get(&format!("/api/v1/directives/{}/memory", directive_id)) - .await - } - - /// Delete a memory key for a directive (CLI-facing). - pub async fn directive_memory_delete( - &self, - directive_id: Uuid, - key: &str, - ) -> Result<(), ApiError> { - self.delete(&format!( - "/api/v1/directives/{}/memory/{}", - directive_id, key - )) - .await - } - - /// Clear all memory for a directive (CLI-facing). - pub async fn directive_memory_clear( - &self, - directive_id: Uuid, - ) -> Result<(), ApiError> { - self.delete(&format!("/api/v1/directives/{}/memory", directive_id)) - .await - } - - /// Batch set multiple memory key-value pairs for a directive (CLI-facing). - pub async fn directive_memory_batch_set( - &self, - directive_id: Uuid, - entries: serde_json::Value, - ) -> Result { - self.post( - &format!("/api/v1/directives/{}/memory/batch", directive_id), - &entries, - ) - .await - } } #[derive(Serialize)] diff --git a/makima/src/daemon/cli/directive.rs b/makima/src/daemon/cli/directive.rs index 8eded77..7c50c42 100644 --- a/makima/src/daemon/cli/directive.rs +++ b/makima/src/daemon/cli/directive.rs @@ -126,50 +126,3 @@ pub struct UpdateArgs { pub pr_branch: Option, } -/// Arguments for memory-set command. -#[derive(Args, Debug)] -pub struct MemorySetArgs { - #[command(flatten)] - pub common: DirectiveArgs, - - /// Memory key - pub key: String, - - /// Memory value - pub value: String, -} - -/// Arguments for memory-get command. -#[derive(Args, Debug)] -pub struct MemoryGetArgs { - #[command(flatten)] - pub common: DirectiveArgs, - - /// Memory key - pub key: String, -} - -/// Arguments for memory-list command (uses DirectiveArgs directly). - -/// Arguments for memory-delete command. -#[derive(Args, Debug)] -pub struct MemoryDeleteArgs { - #[command(flatten)] - pub common: DirectiveArgs, - - /// Memory key to delete - pub key: String, -} - -/// Arguments for memory-clear command (uses DirectiveArgs directly). - -/// Arguments for memory-batch-set command. -#[derive(Args, Debug)] -pub struct MemoryBatchSetArgs { - #[command(flatten)] - pub common: DirectiveArgs, - - /// JSON object of key-value pairs: {"key1":"value1","key2":"value2"} - #[arg(long)] - pub json: String, -} diff --git a/makima/src/daemon/cli/mod.rs b/makima/src/daemon/cli/mod.rs index a78e5f8..bcaaa70 100644 --- a/makima/src/daemon/cli/mod.rs +++ b/makima/src/daemon/cli/mod.rs @@ -249,24 +249,6 @@ pub enum DirectiveCommand { /// Update directive metadata (PR URL, etc.) Update(directive::UpdateArgs), - - /// Set a memory key-value pair for the directive - MemorySet(directive::MemorySetArgs), - - /// Get a memory value by key - MemoryGet(directive::MemoryGetArgs), - - /// List all memory key-value pairs - MemoryList(DirectiveArgs), - - /// Delete a memory key - MemoryDelete(directive::MemoryDeleteArgs), - - /// Clear all memory for the directive - MemoryClear(DirectiveArgs), - - /// Batch set multiple memory key-value pairs from JSON - MemoryBatchSet(directive::MemoryBatchSetArgs), } impl Cli { diff --git a/makima/src/daemon/task/manager.rs b/makima/src/daemon/task/manager.rs index 22b41d9..ce5a580 100644 --- a/makima/src/daemon/task/manager.rs +++ b/makima/src/daemon/task/manager.rs @@ -20,7 +20,7 @@ use crate::daemon::error::{DaemonError, TaskError, TaskResult}; use crate::daemon::process::{ClaudeInputMessage, ProcessManager}; use crate::daemon::storage; use crate::daemon::temp::TempManager; -use crate::daemon::worktree::{is_new_repo_request, ConflictResolution, WorktreeInfo, WorktreeManager}; +use crate::daemon::worktree::{is_new_repo_request, ConflictResolution, WorktreeError, WorktreeInfo, WorktreeManager}; use crate::daemon::db::local::LocalDb; use crate::daemon::ws::{BranchInfo, DaemonCommand, DaemonMessage}; @@ -4406,8 +4406,14 @@ impl TaskManagerInner { // Create worktree - either from scratch or copying from another task let task_name = format!("task-{}", &task_id.to_string()[..8]); let worktree_info = if let Some(from_task_id) = continue_from_task_id { - // Try to find the source task's worktree path - match self.find_worktree_for_task(from_task_id).await { + // Fallback chain for continuing from a previous task: + // 1. Try copying from existing worktree (fastest, preserves uncommitted changes) + // 2. Try creating from pushed branch (branch was pushed to remote) + // 3. Try restoring from saved patch data + // 4. Fail if none available + + // Step 1: Try copying from existing worktree + let copy_result = match self.find_worktree_for_task(from_task_id).await { Ok(source_worktree) => { let msg = DaemonMessage::task_output( task_id, @@ -4416,66 +4422,112 @@ impl TaskManagerInner { ); let _ = self.ws_tx.send(msg).await; - // Create worktree by copying from source task self.worktree_manager .create_worktree_from_task(&source_worktree, task_id, &task_name) .await - .map_err(|e| DaemonError::Task(TaskError::SetupFailed(e.to_string())))? } - Err(worktree_err) => { - // Source worktree not found - try to recover using patch data - if let (Some(patch_str), Some(base_sha)) = (&patch_data, &patch_base_sha) { - tracing::info!( - task_id = %task_id, - from_task_id = %from_task_id, - base_sha = %base_sha, - patch_len = patch_str.len(), - "Source worktree not found, attempting to restore from patch" - ); + Err(e) => Err(crate::daemon::worktree::WorktreeError::RepoNotFound(e.to_string())), + }; - let msg = DaemonMessage::task_output( - task_id, - format!("Source worktree unavailable, restoring from checkpoint patch...\n"), - false, - ); - let _ = self.ws_tx.send(msg).await; + match copy_result { + Ok(info) => info, + Err(copy_err) => { + tracing::warn!( + task_id = %task_id, + from_task_id = %from_task_id, + error = %copy_err, + "Failed to copy from source worktree, trying branch fallback" + ); + + // Step 2: Try creating from pushed branch + let msg = DaemonMessage::task_output( + task_id, + format!("Source worktree unavailable, checking for pushed branch...\n"), + false, + ); + let _ = self.ws_tx.send(msg).await; + + match self.worktree_manager + .create_worktree_from_task_branch(&source_repo, from_task_id, task_id, &task_name) + .await + { + Ok(info) => { + tracing::info!( + task_id = %task_id, + from_task_id = %from_task_id, + branch = %info.branch, + "Successfully created worktree from pushed branch" + ); + let msg = DaemonMessage::task_output( + task_id, + format!("Restored from pushed branch {}\n", info.branch), + false, + ); + let _ = self.ws_tx.send(msg).await; + info + } + Err(branch_err) => { + tracing::warn!( + task_id = %task_id, + from_task_id = %from_task_id, + error = %branch_err, + "No pushed branch found, trying patch fallback" + ); + + // Step 3: Try restoring from saved patch data + if let (Some(patch_str), Some(base_sha)) = (&patch_data, &patch_base_sha) { + tracing::info!( + task_id = %task_id, + from_task_id = %from_task_id, + base_sha = %base_sha, + patch_len = patch_str.len(), + "Attempting to restore from checkpoint patch" + ); - // Decode base64 patch data - match base64::Engine::decode(&base64::engine::general_purpose::STANDARD, patch_str) { - Ok(patch_bytes) => { - match self.worktree_manager.restore_from_patch( - source, + let msg = DaemonMessage::task_output( task_id, - &task_name, - base_sha, - &patch_bytes, - ).await { - Ok(worktree_info) => { - tracing::info!( - task_id = %task_id, - path = %worktree_info.path.display(), - "Successfully restored worktree from patch" - ); - worktree_info + format!("Restoring from checkpoint patch...\n"), + false, + ); + let _ = self.ws_tx.send(msg).await; + + match base64::Engine::decode(&base64::engine::general_purpose::STANDARD, patch_str) { + Ok(patch_bytes) => { + match self.worktree_manager.restore_from_patch( + source, + task_id, + &task_name, + base_sha, + &patch_bytes, + ).await { + Ok(worktree_info) => { + tracing::info!( + task_id = %task_id, + path = %worktree_info.path.display(), + "Successfully restored worktree from patch" + ); + worktree_info + } + Err(e) => { + return Err(DaemonError::Task(TaskError::SetupFailed( + format!("Cannot continue from task {}: worktree copy failed ({}), branch not found ({}), patch restore failed ({})", from_task_id, copy_err, branch_err, e) + ))); + } + } } Err(e) => { return Err(DaemonError::Task(TaskError::SetupFailed( - format!("Cannot continue from task {}: {} (patch restore also failed: {})", from_task_id, worktree_err, e) + format!("Cannot continue from task {}: worktree copy failed ({}), branch not found ({}), patch decode failed ({})", from_task_id, copy_err, branch_err, e) ))); } } - } - Err(e) => { + } else { + // Step 4: No fallback available return Err(DaemonError::Task(TaskError::SetupFailed( - format!("Cannot continue from task {}: {} (patch decode failed: {})", from_task_id, worktree_err, e) + format!("Cannot continue from task {}: worktree copy failed ({}), branch not found ({}), no patch data available", from_task_id, copy_err, branch_err) ))); } } - } else { - // No patch data available - fail with original error - return Err(DaemonError::Task(TaskError::SetupFailed( - format!("Cannot continue from task {}: {}", from_task_id, worktree_err) - ))); } } } diff --git a/makima/src/daemon/worktree/manager.rs b/makima/src/daemon/worktree/manager.rs index 20c93b1..04180b8 100644 --- a/makima/src/daemon/worktree/manager.rs +++ b/makima/src/daemon/worktree/manager.rs @@ -692,6 +692,85 @@ impl WorktreeManager { }) } + /// Find and create a worktree from a previously pushed branch for a task. + /// + /// Searches local and remote branches for ones matching the task's UUID, + /// then creates a new worktree based on that branch. + pub async fn create_worktree_from_task_branch( + &self, + source_repo: &Path, + from_task_id: Uuid, + new_task_id: Uuid, + new_task_name: &str, + ) -> Result { + let from_short_id = short_uuid(from_task_id); + let from_full_id = from_task_id.to_string(); + + tracing::info!( + from_task_id = %from_task_id, + new_task_id = %new_task_id, + "Searching for pushed branch to continue from" + ); + + // Fetch latest from remote first + let _ = Command::new("git") + .args(["fetch", "--all", "--prune"]) + .current_dir(source_repo) + .output() + .await; + + // Search for branches matching the task ID (both local and remote) + let output = Command::new("git") + .args(["branch", "-a", "--format=%(refname:short)"]) + .current_dir(source_repo) + .output() + .await?; + + if !output.status.success() { + return Err(WorktreeError::GitCommand( + "Failed to list branches".to_string(), + )); + } + + let branches_output = String::from_utf8_lossy(&output.stdout); + let mut found_branch: Option = None; + + for line in branches_output.lines() { + let b = line.trim(); + if b.is_empty() { + continue; + } + // Match branches containing the full UUID or short UUID + if b.contains(&from_full_id) || b.contains(&from_short_id) { + // Prefer local branches, but accept remote + if let Some(remote_branch) = b.strip_prefix("origin/") { + if found_branch.is_none() { + found_branch = Some(remote_branch.to_string()); + } + } else { + found_branch = Some(b.to_string()); + } + } + } + + let from_branch = found_branch.ok_or_else(|| { + WorktreeError::GitCommand(format!( + "No branch found for task {} in repository", + from_short_id + )) + })?; + + tracing::info!( + from_task_id = %from_task_id, + from_branch = %from_branch, + "Found pushed branch, creating worktree from it" + ); + + // Create a worktree using the found branch as the base + self.create_worktree(source_repo, new_task_id, new_task_name, &from_branch) + .await + } + /// Remove a worktree and optionally its branch. pub async fn remove_worktree( &self, -- cgit v1.2.3