diff options
| author | soryu <soryu@soryu.co> | 2026-01-15 03:26:28 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-15 03:26:28 +0000 |
| commit | eeafe072bc6bb81459f7d087b48fc921afe9cc11 (patch) | |
| tree | 7f835993edd732f8ff66d756391dedffe3d44e90 /makima/src/daemon | |
| parent | c61a2b9b9c988f5460f85980d4ddf285f1a730b5 (diff) | |
| download | soryu-eeafe072bc6bb81459f7d087b48fc921afe9cc11.tar.gz soryu-eeafe072bc6bb81459f7d087b48fc921afe9cc11.zip | |
Automatically derive repo URL and add notifications for input
Diffstat (limited to 'makima/src/daemon')
| -rw-r--r-- | makima/src/daemon/api/supervisor.rs | 28 | ||||
| -rw-r--r-- | makima/src/daemon/cli/mod.rs | 3 | ||||
| -rw-r--r-- | makima/src/daemon/cli/supervisor.rs | 26 | ||||
| -rw-r--r-- | makima/src/daemon/task/manager.rs | 80 | ||||
| -rw-r--r-- | makima/src/daemon/worktree/manager.rs | 183 |
5 files changed, 266 insertions, 54 deletions
diff --git a/makima/src/daemon/api/supervisor.rs b/makima/src/daemon/api/supervisor.rs index b691cc4..a1e8682 100644 --- a/makima/src/daemon/api/supervisor.rs +++ b/makima/src/daemon/api/supervisor.rs @@ -62,6 +62,17 @@ pub struct CheckpointRequest { pub message: String, } +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AskQuestionRequest { + pub question: String, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub choices: Vec<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub context: Option<String>, + pub timeout_seconds: i32, +} + // Generic response type for JSON output #[derive(Deserialize, Serialize)] pub struct JsonValue(pub serde_json::Value); @@ -183,4 +194,21 @@ impl ApiClient { self.get(&format!("/api/v1/contracts/{}/daemon/status", contract_id)) .await } + + /// Ask a question and wait for user feedback. + pub async fn supervisor_ask( + &self, + question: &str, + choices: Vec<String>, + context: Option<String>, + timeout_seconds: i32, + ) -> Result<JsonValue, ApiError> { + let req = AskQuestionRequest { + question: question.to_string(), + choices, + context, + timeout_seconds, + }; + self.post("/api/v1/mesh/supervisor/questions", &req).await + } } diff --git a/makima/src/daemon/cli/mod.rs b/makima/src/daemon/cli/mod.rs index 24c19c6..66c7941 100644 --- a/makima/src/daemon/cli/mod.rs +++ b/makima/src/daemon/cli/mod.rs @@ -76,6 +76,9 @@ pub enum SupervisorCommand { /// Get contract status Status(SupervisorArgs), + + /// Ask a question and wait for user feedback + Ask(supervisor::AskArgs), } /// Contract subcommands for task-contract interaction. diff --git a/makima/src/daemon/cli/supervisor.rs b/makima/src/daemon/cli/supervisor.rs index 00c7ff4..017730d 100644 --- a/makima/src/daemon/cli/supervisor.rs +++ b/makima/src/daemon/cli/supervisor.rs @@ -42,6 +42,10 @@ pub struct SpawnArgs { /// Checkpoint SHA to start from #[arg(long)] pub checkpoint: Option<String>, + + /// Repository URL (local path or remote URL). If not provided, will try to detect from current directory. + #[arg(long)] + pub repo: Option<String>, } /// Arguments for wait command. @@ -144,3 +148,25 @@ pub struct CheckpointArgs { /// Checkpoint message pub message: String, } + +/// Arguments for ask command (ask user a question). +#[derive(Args, Debug)] +pub struct AskArgs { + #[command(flatten)] + pub common: SupervisorArgs, + + /// The question to ask + pub question: String, + + /// Optional choices (comma-separated) + #[arg(long)] + pub choices: Option<String>, + + /// Context about what this relates to + #[arg(long)] + pub context: Option<String>, + + /// Timeout in seconds (default: 3600 = 1 hour) + #[arg(long, default_value = "3600")] + pub timeout: i32, +} diff --git a/makima/src/daemon/task/manager.rs b/makima/src/daemon/task/manager.rs index 8269083..3b4ffdd 100644 --- a/makima/src/daemon/task/manager.rs +++ b/makima/src/daemon/task/manager.rs @@ -27,12 +27,27 @@ fn generate_tool_key() -> String { } /// Check if output contains an OAuth authentication error. -fn is_oauth_auth_error(output: &str) -> bool { +/// Only checks system/error messages, not assistant responses (which could mention auth errors conversationally). +fn is_oauth_auth_error(output: &str, json_type: Option<&str>, is_stdout: bool) -> bool { + // Only check system messages or stderr output - not assistant messages + // which could mention auth errors in conversation + match json_type { + Some("assistant") | Some("user") | Some("result") => return false, + _ => {} + } + + // For stdout JSON messages, only check system/error types + if is_stdout && json_type.is_none() { + // Non-JSON stdout output - could be startup messages, check carefully + // Only match very specific patterns that wouldn't appear in conversation + } + // Match various authentication error patterns from Claude Code - if output.contains("Please run /login") { + // These patterns are specific enough that they shouldn't appear in normal conversation + if output.contains("Please run /login") && output.contains("authenticate") { return true; } - if output.contains("Invalid API key") { + if output.contains("Invalid API key") && output.contains("ANTHROPIC_API_KEY") { return true; } if output.contains("authentication_error") @@ -41,6 +56,10 @@ fn is_oauth_auth_error(output: &str) -> bool { { return true; } + // Check for Claude Code's specific error format + if output.contains("\"type\":\"error\"") && output.contains("authentication") { + return true; + } false } @@ -695,6 +714,51 @@ makima supervisor checkpoints [task_id] makima supervisor status ``` +### User Feedback +```bash +# Ask a free-form question +makima supervisor ask "Your question here" + +# Ask with choices (comma-separated) +makima supervisor ask "Choose an option" --choices "Option A,Option B,Option C" + +# Ask with context +makima supervisor ask "Ready to proceed?" --context "After completing task X" + +# Ask with custom timeout (default 1 hour) +makima supervisor ask "Question" --timeout 3600 +``` + +## User Feedback (Ask Command) + +You can ask the user questions when you need clarification or approval: + +```bash +# Ask a free-form question (waits for user to respond) +makima supervisor ask "What authentication method should I use?" + +# Ask with predefined choices +makima supervisor ask "Ready to create PR?" --choices "Yes,No,Need more changes" + +# Ask with context +makima supervisor ask "Should I proceed?" --context "Plan phase complete" +``` + +The ask command will block until the user responds (or timeout). Use this to: +- Clarify requirements before starting work +- Get approval before creating PRs +- Ask for guidance when tasks fail + +## Contract Phase Progression + +### For "Simple" contracts (Plan → Execute): +1. **Plan Phase**: Review the plan document and understand the goal +2. **Execute Phase**: Spawn tasks to implement the plan, then create PR +3. Mark contract as complete when PR is created + +### For "Specification" contracts (Research → Specify → Plan → Execute → Review): +Progress through each phase, spawning tasks as needed and asking for user feedback. + ## Key Points 1. **Create a makima branch first** - use `branch "makima/{name}"` for the contract's work @@ -703,6 +767,7 @@ makima supervisor status 4. **Never fire-and-forget** - always wait for each task before moving on 5. **Merge to your makima branch** - use `merge <task_id> --to "makima/{name}"` to collect completed work 6. **Create PR when done** - use `pr "makima/{name}" --title "..." --base main` +7. **Ask when unsure** - use `ask` to get user feedback on decisions ## Standard Workflow @@ -711,16 +776,19 @@ makima supervisor status - `spawn` - Create task - `wait` - Block until complete - `merge --to "makima/{name}"` - Merge to branch -3. `pr "makima/{name}" --title "..." --base main` - Create PR +3. `ask "Ready to create PR?"` - Get user approval +4. `pr "makima/{name}" --title "..." --base main` - Create PR ## Important Reminders - **ONLY YOU can spawn tasks** - regular tasks cannot create children - **NEVER implement anything yourself** - always spawn tasks - **ALWAYS create a makima branch** - use `makima/{name}` naming convention +- **ASK for feedback** when you need clarification or approval - Tasks run independently - you just coordinate - You will be resumed if interrupted - your conversation is preserved - Create checkpoints before major transitions +- **Mark contract complete** when PR is created by updating status --- @@ -2834,6 +2902,8 @@ impl TaskManagerInner { // Check for OAuth auth error before sending output let content_for_auth_check = line.content.clone(); + let json_type_for_auth_check = line.json_type.clone(); + let is_stdout_for_auth_check = line.is_stdout; let msg = DaemonMessage::task_output(task_id, line.content, false); if ws_tx.send(msg).await.is_err() { @@ -2842,7 +2912,7 @@ impl TaskManagerInner { } // Detect OAuth token expiration and trigger remote login flow - if !auth_error_handled && is_oauth_auth_error(&content_for_auth_check) { + if !auth_error_handled && is_oauth_auth_error(&content_for_auth_check, json_type_for_auth_check.as_deref(), is_stdout_for_auth_check) { auth_error_handled = true; tracing::warn!(task_id = %task_id, "OAuth authentication error detected, initiating remote login flow"); diff --git a/makima/src/daemon/worktree/manager.rs b/makima/src/daemon/worktree/manager.rs index 9af5dcb..ff0e9e7 100644 --- a/makima/src/daemon/worktree/manager.rs +++ b/makima/src/daemon/worktree/manager.rs @@ -333,34 +333,76 @@ impl WorktreeManager { // Create base directory tokio::fs::create_dir_all(&self.base_dir).await?; - tracing::info!( - task_id = %task_id, - worktree_path = %worktree_path.display(), - branch = %branch_name, - base_branch = %base_branch, - "Creating worktree from local branch" - ); + // Prune stale worktree entries first (handles "missing but registered" errors) + let _ = Command::new("git") + .args(["worktree", "prune"]) + .current_dir(source_repo) + .output() + .await; - // Create the worktree with a new branch based on the local base_branch - let output = Command::new("git") - .args([ - "worktree", - "add", - "-b", - &branch_name, - ]) - .arg(&worktree_path) - .arg(base_branch) + // Check if the branch already exists (e.g., from a previous run of the same task) + let branch_exists = Command::new("git") + .args(["rev-parse", "--verify", &format!("refs/heads/{}", branch_name)]) .current_dir(source_repo) .output() - .await?; + .await + .map(|o| o.status.success()) + .unwrap_or(false); - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - return Err(WorktreeError::GitCommand(format!( - "Failed to create worktree: {}", - stderr - ))); + if branch_exists { + tracing::info!( + task_id = %task_id, + worktree_path = %worktree_path.display(), + branch = %branch_name, + "Branch already exists, creating worktree from existing branch" + ); + + // Use existing branch - try without force first, then with force + let output = Command::new("git") + .args(["worktree", "add", "-f"]) + .arg(&worktree_path) + .arg(&branch_name) + .current_dir(source_repo) + .output() + .await?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(WorktreeError::GitCommand(format!( + "Failed to create worktree from existing branch: {}", + stderr + ))); + } + } else { + tracing::info!( + task_id = %task_id, + worktree_path = %worktree_path.display(), + branch = %branch_name, + base_branch = %base_branch, + "Creating worktree with new branch" + ); + + // Create the worktree with a new branch based on the local base_branch + let output = Command::new("git") + .args([ + "worktree", + "add", + "-b", + &branch_name, + ]) + .arg(&worktree_path) + .arg(base_branch) + .current_dir(source_repo) + .output() + .await?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(WorktreeError::GitCommand(format!( + "Failed to create worktree: {}", + stderr + ))); + } } tracing::info!( @@ -439,35 +481,78 @@ impl WorktreeManager { // Create base directory tokio::fs::create_dir_all(&self.base_dir).await?; - tracing::info!( - task_id = %task_id, - source_worktree = %source_worktree.display(), - worktree_path = %worktree_path.display(), - branch = %branch_name, - source_commit = %source_commit, - "Creating worktree from source task" - ); + // Prune stale worktree entries first (handles "missing but registered" errors) + let _ = Command::new("git") + .args(["worktree", "prune"]) + .current_dir(&source_repo) + .output() + .await; - // Create a new worktree based on the source commit - let output = Command::new("git") - .args([ - "worktree", - "add", - "-b", - &branch_name, - ]) - .arg(&worktree_path) - .arg(&source_commit) + // Check if the branch already exists (e.g., from a previous run of the same task) + let branch_exists = Command::new("git") + .args(["rev-parse", "--verify", &format!("refs/heads/{}", branch_name)]) .current_dir(&source_repo) .output() - .await?; + .await + .map(|o| o.status.success()) + .unwrap_or(false); - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - return Err(WorktreeError::GitCommand(format!( - "Failed to create worktree: {}", - stderr - ))); + if branch_exists { + tracing::info!( + task_id = %task_id, + source_worktree = %source_worktree.display(), + worktree_path = %worktree_path.display(), + branch = %branch_name, + "Branch already exists, creating worktree from existing branch" + ); + + // Use existing branch with force flag + let output = Command::new("git") + .args(["worktree", "add", "-f"]) + .arg(&worktree_path) + .arg(&branch_name) + .current_dir(&source_repo) + .output() + .await?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(WorktreeError::GitCommand(format!( + "Failed to create worktree from existing branch: {}", + stderr + ))); + } + } else { + tracing::info!( + task_id = %task_id, + source_worktree = %source_worktree.display(), + worktree_path = %worktree_path.display(), + branch = %branch_name, + source_commit = %source_commit, + "Creating worktree from source task with new branch" + ); + + // Create a new worktree based on the source commit + let output = Command::new("git") + .args([ + "worktree", + "add", + "-b", + &branch_name, + ]) + .arg(&worktree_path) + .arg(&source_commit) + .current_dir(&source_repo) + .output() + .await?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(WorktreeError::GitCommand(format!( + "Failed to create worktree: {}", + stderr + ))); + } } // Now copy uncommitted changes from source worktree |
