summaryrefslogtreecommitdiff
path: root/makima/src/daemon
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/daemon')
-rw-r--r--makima/src/daemon/api/directive.rs199
-rw-r--r--makima/src/daemon/cli/directive.rs47
-rw-r--r--makima/src/daemon/cli/mod.rs18
-rw-r--r--makima/src/daemon/task/manager.rs144
-rw-r--r--makima/src/daemon/worktree/manager.rs79
5 files changed, 177 insertions, 310 deletions
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<Uuid>,
}
-/// 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<BatchMemoryEntry>,
-}
-
-#[derive(Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct MemorySetRequest {
- pub value: String,
-}
-
-
impl ApiClient {
/// List all directives.
pub async fn list_directives(&self) -> Result<JsonValue, ApiError> {
@@ -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<JsonValue, ApiError> {
- 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<JsonValue, ApiError> {
- 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<JsonValue, ApiError> {
- 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<JsonValue, ApiError> {
- 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<JsonValue, ApiError> {
- 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<JsonValue, ApiError> {
- 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<JsonValue, ApiError> {
- 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<JsonValue, ApiError> {
- 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<String>,
}
-/// 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<WorktreeInfo, WorktreeError> {
+ 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<String> = 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,