diff options
| author | soryu <soryu@soryu.co> | 2026-01-15 11:57:43 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-15 17:12:04 +0000 |
| commit | 3efdab36ca61a6795454668881d5b925abe22bd3 (patch) | |
| tree | 0fd96e527f45a3da31dfc073b07cd55ba284e550 /makima/src/server | |
| parent | 63b2e347b2ecadc6a48062e10e0a7e19b6102631 (diff) | |
| download | soryu-3efdab36ca61a6795454668881d5b925abe22bd3.tar.gz soryu-3efdab36ca61a6795454668881d5b925abe22bd3.zip | |
Fixup: Add cleanup and isolation features to makima
Add comprehensive CLI documentation
- Create makima/docs/CLI.md with complete command reference for:
- makima server: HTTP/WebSocket server options
- makima daemon: Worker daemon configuration
- makima supervisor: Contract orchestration commands
- makima contract: Task-contract interaction commands
- Include configuration file examples and environment variables
- Add usage workflows for common scenarios
- Update makima/README.md with CLI overview and link to docs
Add GitHub Actions release workflow for v0.1.0
Creates automated release workflow that:
- Triggers on v* tag pushes
- Builds binaries for Linux x86_64, macOS x86_64, and macOS ARM64
- Uses Rust nightly toolchain (required for edition 2024)
- Packages binaries as .tar.gz archives
- Creates GitHub release with installation instructions
fix(ci): update macOS runner for x86_64 builds
Replace deprecated macos-13 runner with macos-15-intel for
x86_64-apple-darwin target. The macos-13 runner has been retired
by GitHub Actions.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add dismissing notifications and fix CLI task ID arg
Add worktree cleanup when contracts complete or are deleted (#21)
- Add CleanupWorktree daemon command variant
- Handle CleanupWorktree in daemon task manager
- Add cleanup_contract_worktrees helper function
- Trigger cleanup when contract status becomes 'completed'
- Trigger cleanup before contract deletion
Add Autonomous Loop Mode for persistent task completion (#20)
Implements the "Autonomous Loop Mode" feature inspired by Ralph for Claude Code.
This enables tasks to automatically restart and continue working until they
explicitly signal completion via a COMPLETION_GATE block.
Key features:
- Exit confirmation via COMPLETION_GATE: Tasks must output a <COMPLETION_GATE>
block with `ready: true` to signal completion. Without this, the task
auto-restarts using `claude --continue` to resume the conversation.
- Circuit breaker: Prevents infinite loops by detecting:
* Maximum iteration limit (default: 10)
* No progress for N consecutive iterations (default: 3)
* Same error repeated N times (default: 5)
- spawn_continue: New ProcessManager method to spawn Claude with the
`--continue` flag, resuming from the previous session state.
Toggle: Enable via `autonomous_loop` flag on contracts. When set, all tasks
spawned for that contract will run in autonomous loop mode.
Files changed:
- completion_gate.rs: COMPLETION_GATE parser and CircuitBreaker logic
- claude.rs: spawn_continue() for --continue mode spawning
- manager.rs: Autonomous loop iteration logic in run_task()
- protocol.rs: autonomousLoop field in DaemonCommand::SpawnTask
- models.rs/repository.rs: autonomous_loop column on contracts/tasks
- Migration: Adds autonomous_loop columns to contracts and tasks tables
Add get-task and output commands to supervisor CLI (#24)
Add two new supervisor subcommands:
- `makima supervisor task <task_id>` - Get individual task details
- `makima supervisor output <task_id>` - Get task output/claude log
This allows supervisors to fetch task details and claude output
directly from the CLI instead of using curl to call the task API.
Add optional bubblewrap sandboxing for Claude processes (#23)
Add --bubblewrap flag and process.bubblewrap config section to enable
running Claude Code in a bubblewrap sandbox for process isolation.
When enabled, claude processes run with filesystem restrictions:
- Root filesystem mounted read-only
- Working directory (worktree) mounted read-write
- Fresh /dev, /proc, /tmp
- Network access preserved for API calls
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'makima/src/server')
| -rw-r--r-- | makima/src/server/handlers/contract_chat.rs | 1 | ||||
| -rw-r--r-- | makima/src/server/handlers/contracts.rs | 116 | ||||
| -rw-r--r-- | makima/src/server/handlers/mesh_supervisor.rs | 39 | ||||
| -rw-r--r-- | makima/src/server/handlers/transcript_analysis.rs | 1 | ||||
| -rw-r--r-- | makima/src/server/state.rs | 9 |
5 files changed, 164 insertions, 2 deletions
diff --git a/makima/src/server/handlers/contract_chat.rs b/makima/src/server/handlers/contract_chat.rs index e2bd10e..101b257 100644 --- a/makima/src/server/handlers/contract_chat.rs +++ b/makima/src/server/handlers/contract_chat.rs @@ -2376,6 +2376,7 @@ async fn handle_contract_request( description: contract_description, contract_type: Some("specification".to_string()), initial_phase: Some("research".to_string()), + autonomous_loop: None, }; let contract = match repository::create_contract_for_owner(pool, owner_id, contract_req).await { diff --git a/makima/src/server/handlers/contracts.rs b/makima/src/server/handlers/contracts.rs index 3ce29e1..09f78e6 100644 --- a/makima/src/server/handlers/contracts.rs +++ b/makima/src/server/handlers/contracts.rs @@ -425,7 +425,7 @@ pub async fn update_contract( match repository::update_contract_for_owner(pool, id, auth.owner_id, req).await { Ok(Some(contract)) => { - // If contract is completed, stop the supervisor task + // If contract is completed, stop the supervisor task and clean up worktrees if contract.status == "completed" { if let Some(supervisor_task_id) = contract.supervisor_task_id { // Get the supervisor task to find its daemon @@ -456,6 +456,14 @@ pub async fn update_contract( } } } + + // Clean up all task worktrees for this contract + let pool_clone = pool.clone(); + let state_clone = state.clone(); + let contract_id = id; + tokio::spawn(async move { + cleanup_contract_worktrees(&pool_clone, &state_clone, contract_id).await; + }); } // Get summary with counts @@ -548,6 +556,30 @@ pub async fn delete_contract( .into_response(); }; + // First, verify contract exists and belongs to owner + match repository::get_contract_for_owner(pool, id, auth.owner_id).await { + Ok(Some(_)) => {} + Ok(None) => { + return ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "Contract not found")), + ) + .into_response(); + } + Err(e) => { + tracing::error!("Failed to get contract {}: {}", id, e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response(); + } + } + + // Clean up all task worktrees BEFORE deleting the contract + // (because CASCADE delete will remove tasks from DB) + cleanup_contract_worktrees(pool, &state, id).await; + match repository::delete_contract_for_owner(pool, id, auth.owner_id).await { Ok(true) => StatusCode::NO_CONTENT.into_response(), Ok(false) => ( @@ -1318,3 +1350,85 @@ pub async fn get_events( } } } + +// ============================================================================= +// Internal Helper Functions +// ============================================================================= + +/// Clean up all worktrees for tasks in a contract. +/// +/// This is called when a contract is completed or deleted to remove +/// all associated task worktrees from connected daemons. +async fn cleanup_contract_worktrees( + pool: &sqlx::PgPool, + state: &SharedState, + contract_id: Uuid, +) { + tracing::info!( + contract_id = %contract_id, + "Cleaning up worktrees for contract tasks" + ); + + // Get all tasks with worktree info for this contract + let tasks = match repository::list_contract_tasks_with_worktree_info(pool, contract_id).await { + Ok(tasks) => tasks, + Err(e) => { + tracing::error!( + contract_id = %contract_id, + error = %e, + "Failed to list tasks for worktree cleanup" + ); + return; + } + }; + + if tasks.is_empty() { + tracing::debug!( + contract_id = %contract_id, + "No tasks with worktrees to clean up" + ); + return; + } + + tracing::info!( + contract_id = %contract_id, + task_count = tasks.len(), + "Found tasks with worktrees to clean up" + ); + + // Send cleanup command to each task's daemon + for task in tasks { + if let Some(daemon_id) = task.daemon_id { + let cmd = crate::server::state::DaemonCommand::CleanupWorktree { + task_id: task.id, + delete_branch: true, // Delete the branch when contract is done + }; + + match state.send_daemon_command(daemon_id, cmd).await { + Ok(()) => { + tracing::info!( + task_id = %task.id, + daemon_id = %daemon_id, + contract_id = %contract_id, + "Sent worktree cleanup command" + ); + } + Err(e) => { + tracing::warn!( + task_id = %task.id, + daemon_id = %daemon_id, + contract_id = %contract_id, + error = %e, + "Failed to send worktree cleanup command (daemon may be offline)" + ); + } + } + } else { + tracing::debug!( + task_id = %task.id, + contract_id = %contract_id, + "Task has no daemon assigned, skipping worktree cleanup" + ); + } + } +} diff --git a/makima/src/server/handlers/mesh_supervisor.rs b/makima/src/server/handlers/mesh_supervisor.rs index d0fa4d1..3add89f 100644 --- a/makima/src/server/handlers/mesh_supervisor.rs +++ b/makima/src/server/handlers/mesh_supervisor.rs @@ -18,7 +18,7 @@ use crate::db::repository; use crate::server::auth::Authenticated; use crate::server::handlers::mesh::{extract_auth, AuthSource}; use crate::server::messages::ApiError; -use crate::server::state::{DaemonCommand, SharedState}; +use crate::server::state::{DaemonCommand, SharedState, TaskOutputNotification}; // ============================================================================= // Request/Response Types @@ -1311,6 +1311,43 @@ pub async fn ask_question( request.context.clone(), ); + // Broadcast question as task output entry for the task's chat + let question_data = serde_json::json!({ + "question_id": question_id.to_string(), + "choices": request.choices, + "context": request.context, + }); + state.broadcast_task_output(TaskOutputNotification { + task_id: supervisor_id, + owner_id: Some(owner_id), + message_type: "supervisor_question".to_string(), + content: request.question.clone(), + tool_name: None, + tool_input: Some(question_data.clone()), + is_error: None, + cost_usd: None, + duration_ms: None, + is_partial: false, + }); + + // Persist to database so it appears when reloading the page + // Use event_type "output" with messageType "supervisor_question" to match TaskOutputEntry format + if let Some(pool) = state.db_pool.as_ref() { + let event_data = serde_json::json!({ + "messageType": "supervisor_question", + "content": request.question, + "toolInput": question_data, + }); + let _ = repository::create_task_event( + pool, + supervisor_id, + "output", + None, + None, + Some(event_data), + ).await; + } + // Poll for response with timeout let timeout_duration = std::time::Duration::from_secs(request.timeout_seconds.max(1) as u64); let start = std::time::Instant::now(); diff --git a/makima/src/server/handlers/transcript_analysis.rs b/makima/src/server/handlers/transcript_analysis.rs index 2c38eea..275905e 100644 --- a/makima/src/server/handlers/transcript_analysis.rs +++ b/makima/src/server/handlers/transcript_analysis.rs @@ -276,6 +276,7 @@ pub async fn create_contract_from_analysis( description: contract_description, contract_type: Some("specification".to_string()), initial_phase: Some("research".to_string()), + autonomous_loop: None, }; let contract = match repository::create_contract_for_owner(pool, auth.owner_id, contract_req).await { diff --git a/makima/src/server/state.rs b/makima/src/server/state.rs index 495fc15..2a45d88 100644 --- a/makima/src/server/state.rs +++ b/makima/src/server/state.rs @@ -396,6 +396,15 @@ pub enum DaemonCommand { task_id: Uuid, }, + /// Clean up a task's worktree (used when contract is completed/deleted) + CleanupWorktree { + #[serde(rename = "taskId")] + task_id: Uuid, + /// Whether to delete the associated branch + #[serde(rename = "deleteBranch")] + delete_branch: bool, + }, + /// Error response Error { code: String, message: String }, } |
