diff options
| author | soryu <soryu@soryu.co> | 2026-02-17 16:48:39 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-02-17 16:48:39 +0000 |
| commit | aee6cda5fc8c44ebc45b274d07a1ed64052e3699 (patch) | |
| tree | b484ced697dab34004ceeec826e1b884162f0f49 /makima/src/server/handlers/directives.rs | |
| parent | 049fd3e8a15952627954678838ca5382c11ecd04 (diff) | |
| download | soryu-aee6cda5fc8c44ebc45b274d07a1ed64052e3699.tar.gz soryu-aee6cda5fc8c44ebc45b274d07a1ed64052e3699.zip | |
feat: smart cleanup, order linking, and improved PR titles (#69)
* feat: soryu-co/soryu: Reorder navigation: move Orders before Contracts
* feat: soryu-co/soryu: Generate PR titles from step content instead of directive title
* feat: soryu-co/soryu: Add orderId field to step creation and link orders to steps
* feat: soryu-co/soryu: Handle completed orders during plan-orders flow
* WIP: heartbeat checkpoint
* Merge origin/makima/soryu-co-soryu--handle-completed-orders-during-pla-5aa9a15b (resolved conflicts)
Diffstat (limited to 'makima/src/server/handlers/directives.rs')
| -rw-r--r-- | makima/src/server/handlers/directives.rs | 156 |
1 files changed, 134 insertions, 22 deletions
diff --git a/makima/src/server/handlers/directives.rs b/makima/src/server/handlers/directives.rs index b4b438a..56278a8 100644 --- a/makima/src/server/handlers/directives.rs +++ b/makima/src/server/handlers/directives.rs @@ -9,14 +9,14 @@ use axum::{ use uuid::Uuid; use crate::db::models::{ - CleanupTasksResponse, CreateDirectiveRequest, CreateTaskRequest, + CleanupResponse, CleanupTasksResponse, CreateDirectiveRequest, CreateTaskRequest, CreateDirectiveStepRequest, Directive, DirectiveListResponse, DirectiveStep, DirectiveWithSteps, PickUpOrdersResponse, UpdateDirectiveRequest, UpdateDirectiveStepRequest, UpdateGoalRequest, UpdateOrderRequest, }; use crate::db::repository; -use crate::orchestration::directive::build_order_pickup_prompt; +use crate::orchestration::directive::{build_cleanup_prompt, build_order_pickup_prompt}; use crate::server::auth::Authenticated; use crate::server::messages::ApiError; use crate::server::state::SharedState; @@ -872,20 +872,21 @@ pub async fn update_goal( // ============================================================================= -/// Clean up terminal tasks associated with a directive. +/// Clean up merged steps for an idle directive by spawning a verification task. #[utoipa::path( post, - path = "/api/v1/directives/{id}/cleanup-tasks", + path = "/api/v1/directives/{id}/cleanup", params(("id" = Uuid, Path, description = "Directive ID")), responses( - (status = 200, description = "Tasks cleaned up", body = CleanupTasksResponse), + (status = 200, description = "Cleanup task spawned", body = CleanupResponse), (status = 404, description = "Not found", body = ApiError), + (status = 409, description = "Directive is not idle", body = ApiError), (status = 503, description = "Database not configured", body = ApiError), ), security(("bearer_auth" = []), ("api_key" = [])), tag = "Directives" )] -pub async fn cleanup_tasks( +pub async fn cleanup_directive( State(state): State<SharedState>, Authenticated(auth): Authenticated, Path(id): Path<Uuid>, @@ -898,36 +899,147 @@ pub async fn cleanup_tasks( .into_response(); }; - // Verify directive ownership - match repository::get_directive_for_owner(pool, auth.owner_id, id).await { - Ok(Some(_)) => {} - Ok(None) => { + // Get the directive with steps and verify ownership + let (directive, _steps) = + match repository::get_directive_with_steps_for_owner(pool, auth.owner_id, id).await { + Ok(Some(ds)) => ds, + Ok(None) => { + return ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "Directive not found")), + ) + .into_response(); + } + Err(e) => { + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("GET_FAILED", &e.to_string())), + ) + .into_response(); + } + }; + + // Verify directive is idle + if directive.status != "idle" { + return ( + StatusCode::CONFLICT, + Json(ApiError::new( + "NOT_IDLE", + "Directive must be idle to run cleanup", + )), + ) + .into_response(); + } + + // Get completed step tasks for branch name computation + let step_tasks = match repository::get_completed_step_tasks(pool, id).await { + Ok(tasks) => tasks, + Err(e) => { + tracing::error!("Failed to get completed step tasks: {}", e); return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("GET_STEPS_FAILED", &e.to_string())), ) .into_response(); } + }; + + if step_tasks.is_empty() { + return Json(CleanupResponse { + message: "No completed steps to clean up".to_string(), + task_id: None, + }) + .into_response(); + } + + let pr_branch = match &directive.pr_branch { + Some(b) => b.clone(), + None => { + return Json(CleanupResponse { + message: "No PR branch set — nothing to verify against".to_string(), + task_id: None, + }) + .into_response(); + } + }; + + let base_branch = directive.base_branch.as_deref().unwrap_or("main"); + + // Build the cleanup prompt + let prompt = build_cleanup_prompt(&directive, &step_tasks, &pr_branch, base_branch); + + // Create the cleanup task (following pick_up_orders pattern) + let req = CreateTaskRequest { + contract_id: None, + name: format!("Cleanup: {}", directive.title), + description: Some("Directive cleanup — verify merged branches and remove merged steps".to_string()), + plan: prompt, + parent_task_id: None, + is_supervisor: false, + priority: 0, + repository_url: directive.repository_url.clone(), + base_branch: directive.base_branch.clone(), + target_branch: None, + merge_mode: None, + target_repo_path: None, + completion_action: None, + continue_from_task_id: None, + copy_files: None, + checkpoint_sha: None, + branched_from_task_id: None, + conversation_history: None, + supervisor_worktree_task_id: None, + directive_id: Some(directive.id), + directive_step_id: None, + }; + + let task = match repository::create_task_for_owner(pool, auth.owner_id, req).await { + Ok(t) => t, Err(e) => { + tracing::error!("Failed to create cleanup task: {}", e); return ( StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("GET_FAILED", &e.to_string())), + Json(ApiError::new("CREATE_TASK_FAILED", &e.to_string())), ) .into_response(); } + }; + + // Assign as orchestrator task + if let Err(e) = repository::assign_orchestrator_task(pool, id, task.id).await { + tracing::error!("Failed to assign orchestrator task: {}", e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("ASSIGN_TASK_FAILED", &e.to_string())), + ) + .into_response(); } - match repository::cleanup_directive_tasks(pool, auth.owner_id, id).await { - Ok(deleted) => Json(CleanupTasksResponse { deleted }).into_response(), - Err(e) => { - tracing::error!("Failed to cleanup directive tasks: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("CLEANUP_FAILED", &e.to_string())), - ) - .into_response() + // Cancel old planning tasks + let cancelled = repository::cancel_old_planning_tasks(pool, id, task.id).await; + if let Ok(count) = cancelled { + if count > 0 { + tracing::info!( + directive_id = %id, + cancelled_count = count, + "Cancelled old planning tasks superseded by cleanup" + ); } } + + // Set directive to active + if let Err(e) = repository::set_directive_status(pool, auth.owner_id, id, "active").await { + tracing::warn!("Failed to set directive status to active: {}", e); + } + + // Advance ready steps + let _ = repository::advance_directive_ready_steps(pool, id).await; + + Json(CleanupResponse { + message: format!("Cleanup task spawned for {} completed steps", step_tasks.len()), + task_id: Some(task.id), + }) + .into_response() } // ============================================================================= |
