summaryrefslogtreecommitdiff
path: root/makima/src/daemon
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-15 03:26:28 +0000
committersoryu <soryu@soryu.co>2026-01-15 03:26:28 +0000
commiteeafe072bc6bb81459f7d087b48fc921afe9cc11 (patch)
tree7f835993edd732f8ff66d756391dedffe3d44e90 /makima/src/daemon
parentc61a2b9b9c988f5460f85980d4ddf285f1a730b5 (diff)
downloadsoryu-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.rs28
-rw-r--r--makima/src/daemon/cli/mod.rs3
-rw-r--r--makima/src/daemon/cli/supervisor.rs26
-rw-r--r--makima/src/daemon/task/manager.rs80
-rw-r--r--makima/src/daemon/worktree/manager.rs183
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