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 | |
| 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')
| -rw-r--r-- | makima/src/db/models.rs | 10 | ||||
| -rw-r--r-- | makima/src/db/repository.rs | 3 | ||||
| -rw-r--r-- | makima/src/orchestration/directive.rs | 73 | ||||
| -rw-r--r-- | makima/src/server/handlers/directives.rs | 156 | ||||
| -rw-r--r-- | makima/src/server/mod.rs | 2 | ||||
| -rw-r--r-- | makima/src/server/openapi.rs | 6 |
6 files changed, 222 insertions, 28 deletions
diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs index f9a34b8..6b77563 100644 --- a/makima/src/db/models.rs +++ b/makima/src/db/models.rs @@ -2832,13 +2832,21 @@ pub struct UpdateGoalRequest { pub goal: String, } -/// Response for cleanup_directive_tasks. +/// Response for cleanup_directive_tasks (legacy). #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct CleanupTasksResponse { pub deleted: i64, } +/// Response for cleanup_directive endpoint. +#[derive(Debug, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct CleanupResponse { + pub message: String, + pub task_id: Option<Uuid>, +} + /// Response for pick_up_orders endpoint. #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs index c7b0a1f..8d7a70c 100644 --- a/makima/src/db/repository.rs +++ b/makima/src/db/repository.rs @@ -5188,6 +5188,7 @@ pub async fn cleanup_directive_tasks( /// Row type for completed step tasks. #[derive(Debug, Clone, sqlx::FromRow)] pub struct CompletedStepTask { + pub step_id: Uuid, pub step_name: String, pub task_id: Uuid, pub task_name: String, @@ -5321,7 +5322,7 @@ pub async fn get_completed_step_tasks( ) -> Result<Vec<CompletedStepTask>, sqlx::Error> { sqlx::query_as::<_, CompletedStepTask>( r#" - SELECT ds.name as step_name, ds.task_id, t.name as task_name + SELECT ds.id as step_id, ds.name as step_name, ds.task_id, t.name as task_name FROM directive_steps ds JOIN tasks t ON t.id = ds.task_id WHERE ds.directive_id = $1 diff --git a/makima/src/orchestration/directive.rs b/makima/src/orchestration/directive.rs index eb157a3..21053f3 100644 --- a/makima/src/orchestration/directive.rs +++ b/makima/src/orchestration/directive.rs @@ -1551,6 +1551,79 @@ IMPORTANT: You MUST run `makima directive update` with either `--pr-url` or `--s ) } +/// Build a prompt for cleaning up completed steps that have been merged into the PR branch. +/// +/// The prompt instructs the Claude instance to verify which step branches are +/// merged and remove merged steps, leaving unmerged steps alone. +pub fn build_cleanup_prompt( + directive: &crate::db::models::Directive, + step_tasks: &[crate::db::repository::CompletedStepTask], + pr_branch: &str, + base_branch: &str, +) -> String { + let step_list: String = step_tasks + .iter() + .map(|st| { + let branch = format!( + "makima/{}-{}", + crate::daemon::worktree::sanitize_name(&st.task_name), + crate::daemon::worktree::short_uuid(st.task_id), + ); + format!("- Step ID: {}, Name: \"{}\", Branch: {}", st.step_id, st.step_name, branch) + }) + .collect::<Vec<_>>() + .join("\n"); + + let pr_url_line = match &directive.pr_url { + Some(url) => format!("PR URL: {}", url), + None => String::new(), + }; + + format!( + r#"You are cleaning up completed steps for directive "{title}". + +The directive has a PR branch: {pr_branch} +Base branch: {base_branch} +{pr_url_line} + +Completed steps to verify: +{step_list} + +## Instructions + +1. First, fetch the latest remote state: +```bash +git fetch origin +``` + +2. Check which branches have been merged into the PR branch: +```bash +git branch -r --merged origin/{pr_branch} +``` + +3. For each completed step listed above, check if its branch appears in the merged list. + +4. For steps whose branches ARE merged: + - Remove the step (this also cleans up associated data): +```bash +makima directive remove-step <step_id> +``` + +5. For steps whose branches are NOT merged into the PR branch: + - Do NOT remove them. Leave them for the next PR cycle. + - Report them as skipped. + +6. After processing all steps, report a summary of what was cleaned up and what was left. + +IMPORTANT: Only remove steps whose task branches have been verified as merged. Never remove unmerged steps."#, + title = directive.title, + pr_branch = pr_branch, + base_branch = base_branch, + pr_url_line = pr_url_line, + step_list = step_list, + ) +} + /// Build a specialized planning prompt for picking up open orders. /// /// This prompt instructs the planner to evaluate available orders, select an 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() } // ============================================================================= diff --git a/makima/src/server/mod.rs b/makima/src/server/mod.rs index 6bd5ae0..2310ba3 100644 --- a/makima/src/server/mod.rs +++ b/makima/src/server/mod.rs @@ -237,7 +237,7 @@ pub fn make_router(state: SharedState) -> Router { .route("/directives/{id}/steps/{step_id}/fail", post(directives::fail_step)) .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}/cleanup", post(directives::cleanup_directive)) .route("/directives/{id}/create-pr", post(directives::create_pr)) .route("/directives/{id}/pick-up-orders", post(directives::pick_up_orders)) // Order endpoints diff --git a/makima/src/server/openapi.rs b/makima/src/server/openapi.rs index 87ca9c5..6065eeb 100644 --- a/makima/src/server/openapi.rs +++ b/makima/src/server/openapi.rs @@ -8,7 +8,7 @@ use crate::db::models::{ ChangePhaseRequest, Contract, ContractChatHistoryResponse, ContractChatMessageRecord, ContractEvent, ContractListResponse, ContractRepository, ContractSummary, ContractWithRelations, - CleanupTasksResponse, + CleanupResponse, CreateContractRequest, CreateDirectiveRequest, CreateDirectiveStepRequest, CreateFileRequest, CreateManagedRepositoryRequest, CreateOrderRequest, CreateTaskRequest, Daemon, DaemonDirectoriesResponse, @@ -128,7 +128,7 @@ use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage directives::fail_step, directives::skip_step, directives::update_goal, - directives::cleanup_tasks, + directives::cleanup_directive, directives::create_pr, // Order endpoints orders::list_orders, @@ -234,7 +234,7 @@ use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage UpdateGoalRequest, CreateDirectiveStepRequest, UpdateDirectiveStepRequest, - CleanupTasksResponse, + CleanupResponse, // Order schemas Order, OrderListResponse, |
