summaryrefslogtreecommitdiff
path: root/makima/src/server/handlers/directives.rs
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/server/handlers/directives.rs')
-rw-r--r--makima/src/server/handlers/directives.rs203
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, &current.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(&current.goal, &req.goal) == GoalChangeKind::Small
- {
- match try_interrupt_planner_with_goal_edit(
- pool,
- &state,
- id,
- &current.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) =