diff options
| author | soryu <soryu@soryu.co> | 2026-05-08 16:33:36 +0100 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-05-08 16:33:36 +0100 |
| commit | 7af816032fbc54d5e0a8e94d4a000f307cd3b370 (patch) | |
| tree | 50b6aad1aa47e56b61f0700e224028bb7578cb91 /makima/src/server/handlers/directives.rs | |
| parent | e4f1622a0f0ac74707cc1c9810e0b99e948d1319 (diff) | |
| download | soryu-7af816032fbc54d5e0a8e94d4a000f307cd3b370.tar.gz soryu-7af816032fbc54d5e0a8e94d4a000f307cd3b370.zip | |
feat(directives): drop directives.goal — orchestration reads contract bodydrop-directive-goal
Hard cut. The unified contracts surface owns spec text now; the
directive itself is just a folder. The orchestrator daemon reads the
active contract's body when it spawns, replans, or runs completion.
Schema (migration 20260510000000):
- DROP TABLE directive_goal_history
- ALTER TABLE directives DROP COLUMN goal
- ALTER TABLE directives DROP COLUMN goal_updated_at
New repo helper:
- get_active_contract_body(directive_id) — picks the
active|queued|draft contract (in that order), most-recent first.
Backend cuts:
- Directive / DirectiveSummary / CreateDirectiveRequest /
UpdateDirectiveRequest lose goal & goalUpdatedAt.
- CreateDirectiveRequest gains optional `contractBody` — when
provided, create_directive_for_owner auto-creates a first contract
with that body in the same transaction.
- Removed: update_directive_goal, update_directive_goal_keep_orchestrator,
save_directive_goal_history, get_directive_goal_history,
DirectiveGoalHistory model, UpdateGoalRequest.
- Removed handlers::directives::update_goal + the
/directives/{id}/goal route.
- orchestration::directive::build_planning_prompt /
build_completion_prompt / build_order_pickup_prompt now take a
`contract_body: &str` instead of `goal_history`. classify_goal_change
+ try_interrupt_planner_with_goal_edit + GoalChangeKind +
GoalEditInterruptResult removed (they were only useful for the
small-vs-large goal-edit interrupt cycle).
CLI:
- `makima directive update-goal` removed (UpdateGoalArgs deleted,
Commands enum trimmed, ApiClient::directive_update_goal +
UpdateGoalRequest deleted).
Frontend:
- Directive / DirectiveSummary / CreateDirectiveRequest types lose
goal & goalUpdatedAt; CreateDirectiveRequest gains `contractBody`.
- useDirective drops updateGoal helper.
- api.ts updateDirectiveGoal removed.
- Legacy DirectiveList + DirectiveDetail components deleted; the
/directives route now always renders the document-mode page.
The user-settings documentModeEnabled flag is no longer
consulted at the route level.
- NewContractModal passes body via contractBody.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'makima/src/server/handlers/directives.rs')
| -rw-r--r-- | makima/src/server/handlers/directives.rs | 203 |
1 files changed, 29 insertions, 174 deletions
diff --git a/makima/src/server/handlers/directives.rs b/makima/src/server/handlers/directives.rs index 7b13f1c..6d99179 100644 --- a/makima/src/server/handlers/directives.rs +++ b/makima/src/server/handlers/directives.rs @@ -13,7 +13,7 @@ use crate::db::models::{ CreateDirectiveStepRequest, Directive, DirectiveListResponse, DirectiveRevision, DirectiveStep, DirectiveWithSteps, PickUpOrdersResponse, Task, - UpdateDirectiveRequest, UpdateDirectiveStepRequest, UpdateGoalRequest, + UpdateDirectiveRequest, UpdateDirectiveStepRequest, CreateDirectiveOrderGroupRequest, DirectiveOrderGroup, DirectiveOrderGroupListResponse, UpdateDirectiveOrderGroupRequest, OrderListResponse, @@ -22,9 +22,8 @@ use serde::Serialize; use utoipa::ToSchema; use crate::db::repository; use crate::orchestration::directive::{ - build_cleanup_prompt, build_order_pickup_prompt, classify_goal_change, - try_cancel_running_planner, try_interrupt_planner_with_goal_edit, - GoalChangeKind, GoalEditInterruptResult, + build_cleanup_prompt, build_order_pickup_prompt, + try_cancel_running_planner, }; use crate::server::auth::Authenticated; use crate::server::messages::ApiError; @@ -200,15 +199,19 @@ pub async fn update_directive( match repository::update_directive_for_owner(pool, auth.owner_id, id, req).await { Ok(Some(directive)) => { // Detect "PR was just raised" — pr_url went from None to Some. - // Snapshot the current goal as a revision tied to this PR. - // Best-effort: a snapshot failure should not fail the update, - // because the directive's pr_url has already been written. + // Snapshot the active contract's body as a revision tied to + // this PR. Best-effort: a snapshot failure should not fail + // the update, because the directive's pr_url has already + // been written. if before_pr_url.is_none() { if let Some(ref new_pr_url) = directive.pr_url { + let snapshot_body = repository::get_active_contract_body(pool, directive.id) + .await + .unwrap_or_default(); if let Err(e) = repository::create_directive_revision( pool, directive.id, - &directive.goal, + &snapshot_body, new_pr_url, directive.pr_branch.as_deref(), ) @@ -859,152 +862,10 @@ async fn step_status_change( } } -/// Update a directive's goal (triggers re-planning). -#[utoipa::path( - put, - path = "/api/v1/directives/{id}/goal", - params(("id" = Uuid, Path, description = "Directive ID")), - request_body = UpdateGoalRequest, - responses( - (status = 200, description = "Goal updated", body = Directive), - (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 update_goal( - State(state): State<SharedState>, - Authenticated(auth): Authenticated, - Path(id): Path<Uuid>, - Json(req): Json<UpdateGoalRequest>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Fetch the current directive so we can: - // 1. Save the old goal to history (best-effort). - // 2. Decide whether to fire a goal-edit interrupt at a running planner. - let current = match repository::get_directive_for_owner(pool, auth.owner_id, id).await { - Ok(Some(d)) => Some(d), - Ok(None) => None, - Err(e) => { - tracing::warn!( - directive_id = %id, - error = %e, - "Failed to fetch current directive for goal history — continuing with goal update" - ); - None - } - }; - - // Save old goal to history before overwriting (best-effort). - if let Some(ref current) = current { - if let Err(e) = repository::save_directive_goal_history(pool, id, ¤t.goal).await { - tracing::warn!( - directive_id = %id, - error = %e, - "Failed to save goal history before update — continuing with goal update" - ); - } - } - - // Goal-edit interrupt cycle: if a planner task is currently running for - // this directive AND the goal change classifies as 'small', interrupt the - // running planner via SendMessage instead of clearing it (which would - // trigger a fresh replan on the next orchestrator tick). - let mut interrupted = false; - if let Some(ref current) = current { - if current.orchestrator_task_id.is_some() - && classify_goal_change(¤t.goal, &req.goal) == GoalChangeKind::Small - { - match try_interrupt_planner_with_goal_edit( - pool, - &state, - id, - ¤t.goal, - &req.goal, - ) - .await - { - Ok(GoalEditInterruptResult::Sent) => { - interrupted = true; - } - Ok(GoalEditInterruptResult::Skipped) => {} - Err(e) => { - tracing::warn!( - directive_id = %id, - error = %e, - "Goal-edit interrupt attempt errored — falling back to replan" - ); - } - } - } - } - - // If we successfully interrupted a running planner, persist the new goal - // WITHOUT clearing the orchestrator task — the planner will react to the - // SendMessage and adjust in-flight. Otherwise, fall through to the normal - // path which clears orchestrator_task_id and lets phase_replanning kick - // in on the next tick. - // - // CRITICAL: when going down the "clear" path, we must also CANCEL the - // running planner. Otherwise the orphaned task keeps producing add-step - // calls based on the old goal, racing the freshly-spawned replanner. - if !interrupted { - if let Some(ref current) = current { - if let Some(orch_task_id) = current.orchestrator_task_id { - if let Err(e) = try_cancel_running_planner(pool, &state, id, orch_task_id).await { - tracing::warn!( - directive_id = %id, - task_id = %orch_task_id, - error = %e, - "Failed to cancel orphaned planner — proceeding with clear anyway" - ); - } - } - } - } - - let update_result = if interrupted { - repository::update_directive_goal_keep_orchestrator(pool, auth.owner_id, id, &req.goal) - .await - } else { - repository::update_directive_goal(pool, auth.owner_id, id, &req.goal).await - }; - - let response = match update_result { - Ok(Some(directive)) => Json(directive).into_response(), - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response(); - } - Err(e) => { - tracing::error!("Failed to update goal: {}", e); - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("UPDATE_FAILED", &e.to_string())), - ) - .into_response(); - } - }; - - // Nudge the directive reconciler so the user does not wait up to 15s for - // the next interval tick before the new planner is spawned (clear path) or - // the small-edit interrupt is consumed (keep path). Best-effort: if the - // channel is full or closed we just rely on the normal interval. - state.kick_directive_reconciler(); - - response -} +// (Goal updates now flow through the contracts API. The directive's +// orchestrator reads the active contract's body when it spawns or +// replans — see repository::get_active_contract_body and the +// orchestration module.) // ============================================================================= // Task Cleanup @@ -1404,16 +1265,13 @@ pub async fn pick_up_orders( } }; - 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); + // Build the specialized planning prompt. The orchestrator reads the + // active contract's body itself when it picks up the task; we just + // pass the directive shape + steps + orders + generation here. + let contract_body = repository::get_active_contract_body(pool, id) + .await + .unwrap_or_default(); + let plan = build_order_pickup_prompt(&directive, &steps, &orders, generation, &contract_body); // Link orders to the directive if let Err(e) = @@ -1984,16 +1842,13 @@ pub async fn pick_up_dog_orders( } }; - 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); + // Build the specialized planning prompt. The orchestrator reads the + // active contract's body itself when it picks up the task; we just + // pass the directive shape + steps + orders + generation here. + let contract_body = repository::get_active_contract_body(pool, id) + .await + .unwrap_or_default(); + let plan = build_order_pickup_prompt(&directive, &steps, &orders, generation, &contract_body); // Link orders to the directive if let Err(e) = |
