summaryrefslogtreecommitdiff
path: root/makima/src
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-17 16:48:39 +0000
committerGitHub <noreply@github.com>2026-02-17 16:48:39 +0000
commitaee6cda5fc8c44ebc45b274d07a1ed64052e3699 (patch)
treeb484ced697dab34004ceeec826e1b884162f0f49 /makima/src
parent049fd3e8a15952627954678838ca5382c11ecd04 (diff)
downloadsoryu-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.rs10
-rw-r--r--makima/src/db/repository.rs3
-rw-r--r--makima/src/orchestration/directive.rs73
-rw-r--r--makima/src/server/handlers/directives.rs156
-rw-r--r--makima/src/server/mod.rs2
-rw-r--r--makima/src/server/openapi.rs6
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,