From b6a29bb563499b2fd6280c742bd2106d66393112 Mon Sep 17 00:00:00 2001 From: soryu Date: Mon, 16 Feb 2026 15:09:25 +0000 Subject: Add pick-up-orders feature for directives (#64) * WIP: heartbeat checkpoint * WIP: heartbeat checkpoint * feat: soryu-co/soryu - makima: Add frontend pick-up-orders button and API integration * feat: soryu-co/soryu - makima: Add pick-up-orders backend endpoint and repository functions --- makima/src/server/handlers/directives.rs | 200 ++++++++++++++++++++++++++++++- makima/src/server/mod.rs | 1 + 2 files changed, 199 insertions(+), 2 deletions(-) (limited to 'makima/src/server') diff --git a/makima/src/server/handlers/directives.rs b/makima/src/server/handlers/directives.rs index 6060171..f03dccf 100644 --- a/makima/src/server/handlers/directives.rs +++ b/makima/src/server/handlers/directives.rs @@ -9,12 +9,13 @@ use axum::{ use uuid::Uuid; use crate::db::models::{ - CleanupTasksResponse, CreateDirectiveRequest, + CleanupTasksResponse, CreateDirectiveRequest, CreateTaskRequest, CreateDirectiveStepRequest, Directive, DirectiveListResponse, - DirectiveStep, DirectiveWithSteps, + DirectiveStep, DirectiveWithSteps, PickUpOrdersResponse, UpdateDirectiveRequest, UpdateDirectiveStepRequest, UpdateGoalRequest, }; use crate::db::repository; +use crate::orchestration::directive::build_order_pickup_prompt; use crate::server::auth::Authenticated; use crate::server::messages::ApiError; use crate::server::state::SharedState; @@ -927,3 +928,198 @@ pub async fn cleanup_tasks( } } } + +// ============================================================================= +// Order Pickup +// ============================================================================= + +/// Pick up available open orders for a directive by spawning a planning task. +#[utoipa::path( + post, + path = "/api/v1/directives/{id}/pick-up-orders", + params(("id" = Uuid, Path, description = "Directive ID")), + responses( + (status = 200, description = "Orders picked up", body = PickUpOrdersResponse), + (status = 404, description = "Not found", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + ), + security(("bearer_auth" = []), ("api_key" = [])), + tag = "Directives" +)] +pub async fn pick_up_orders( + State(state): State, + Authenticated(auth): Authenticated, + Path(id): Path, +) -> impl IntoResponse { + let Some(ref pool) = state.db_pool else { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), + ) + .into_response(); + }; + + // Verify directive ownership and get directive with steps + let (directive, steps) = + match repository::get_directive_with_steps_for_owner(pool, auth.owner_id, id).await { + Ok(Some((d, s))) => (d, s), + Ok(None) => { + return ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "Directive not found")), + ) + .into_response(); + } + Err(e) => { + tracing::error!("Failed to get directive: {}", e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("GET_FAILED", &e.to_string())), + ) + .into_response(); + } + }; + + // Fetch available orders + let orders = match repository::get_available_orders_for_pickup(pool, auth.owner_id).await { + Ok(o) => o, + Err(e) => { + tracing::error!("Failed to fetch available orders: {}", e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("FETCH_ORDERS_FAILED", &e.to_string())), + ) + .into_response(); + } + }; + + // If no orders available, return early + if orders.is_empty() { + return Json(PickUpOrdersResponse { + message: "No orders available to pick up".to_string(), + order_count: 0, + task_id: None, + }) + .into_response(); + } + + let order_count = orders.len() as i64; + let order_ids: Vec = orders.iter().map(|o| o.id).collect(); + + // Get generation and goal history for the planning prompt + let generation = + match repository::get_directive_max_generation(pool, id).await { + Ok(g) => g + 1, + Err(e) => { + tracing::error!("Failed to get max generation: {}", e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("GENERATION_FAILED", &e.to_string())), + ) + .into_response(); + } + }; + + let goal_history = match repository::get_directive_goal_history(pool, id, 3).await { + Ok(h) => h, + Err(e) => { + tracing::warn!("Failed to get goal history: {}", e); + vec![] + } + }; + + // Build the specialized planning prompt + let plan = build_order_pickup_prompt(&directive, &steps, &orders, generation, &goal_history); + + // Link orders to the directive + if let Err(e) = + repository::bulk_link_orders_to_directive(pool, auth.owner_id, &order_ids, id).await + { + tracing::error!("Failed to link orders to directive: {}", e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("LINK_ORDERS_FAILED", &e.to_string())), + ) + .into_response(); + } + + // Create the planning task + let req = CreateTaskRequest { + contract_id: None, + name: format!("Pick up orders: {}", directive.title), + description: Some("Directive order pickup planning task".to_string()), + plan, + 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 pickup planning task: {}", e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + 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(); + } + + // 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 order pickup" + ); + } + } + + // Set directive to active if draft/idle/paused + match directive.status.as_str() { + "draft" | "idle" | "paused" => { + 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(PickUpOrdersResponse { + message: format!("Picked up {} orders", order_count), + order_count, + task_id: Some(task.id), + }) + .into_response() +} diff --git a/makima/src/server/mod.rs b/makima/src/server/mod.rs index 29c55c4..ce18620 100644 --- a/makima/src/server/mod.rs +++ b/makima/src/server/mod.rs @@ -238,6 +238,7 @@ pub fn make_router(state: SharedState) -> Router { .route("/directives/{id}/steps/{step_id}/skip", post(directives::skip_step)) .route("/directives/{id}/goal", put(directives::update_goal)) .route("/directives/{id}/cleanup-tasks", post(directives::cleanup_tasks)) + .route("/directives/{id}/pick-up-orders", post(directives::pick_up_orders)) // Order endpoints .route( "/orders", -- cgit v1.2.3