summaryrefslogtreecommitdiff
path: root/makima/src/server/handlers
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/server/handlers')
-rw-r--r--makima/src/server/handlers/contract_chat.rs1
-rw-r--r--makima/src/server/handlers/contracts.rs116
-rw-r--r--makima/src/server/handlers/mesh_supervisor.rs39
-rw-r--r--makima/src/server/handlers/transcript_analysis.rs1
4 files changed, 155 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 {