diff options
| author | soryu <soryu@soryu.co> | 2026-05-08 16:34:11 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-05-08 16:34:11 +0100 |
| commit | dce7f50e503dc374aaf879df33e725af16c4cc78 (patch) | |
| tree | 50b6aad1aa47e56b61f0700e224028bb7578cb91 /makima/src/db/repository.rs | |
| parent | e4f1622a0f0ac74707cc1c9810e0b99e948d1319 (diff) | |
| download | soryu-dce7f50e503dc374aaf879df33e725af16c4cc78.tar.gz soryu-dce7f50e503dc374aaf879df33e725af16c4cc78.zip | |
feat(directives): drop directives.goal — orchestration reads contract body (#132)
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/db/repository.rs')
| -rw-r--r-- | makima/src/db/repository.rs | 200 |
1 files changed, 67 insertions, 133 deletions
diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs index e58f58c..20f3268 100644 --- a/makima/src/db/repository.rs +++ b/makima/src/db/repository.rs @@ -12,7 +12,7 @@ use super::models::{ CreateContractRequest, CreateFileRequest, CreateTaskRequest, CreateTemplateRequest, Daemon, DaemonTaskAssignment, DaemonWithCapacity, DeliverableDefinition, Directive, DirectiveDocument, DirectiveStep, DirectiveSummary, - CreateDirectiveRequest, CreateDirectiveStepRequest, DirectiveGoalHistory, + CreateDirectiveRequest, CreateDirectiveStepRequest, UpdateDirectiveRequest, UpdateDirectiveStepRequest, CreateOrderRequest, Order, UpdateOrderRequest, CreateDirectiveOrderGroupRequest, DirectiveOrderGroup, UpdateDirectiveOrderGroupRequest, @@ -5125,27 +5125,78 @@ fn truncate_string(s: &str, max_len: usize) -> String { // ============================================================================= /// Create a new directive for an owner. +/// +/// If `req.contract_body` is set, also auto-creates a first contract +/// with that body so the directive is immediately ready to start. Both +/// inserts run in the same transaction. pub async fn create_directive_for_owner( pool: &PgPool, owner_id: Uuid, req: CreateDirectiveRequest, ) -> Result<Directive, sqlx::Error> { - sqlx::query_as::<_, Directive>( + let mut tx = pool.begin().await?; + + let directive = sqlx::query_as::<_, Directive>( r#" - INSERT INTO directives (owner_id, title, goal, repository_url, local_path, base_branch, reconcile_mode) - VALUES ($1, $2, $3, $4, $5, $6, $7) + INSERT INTO directives (owner_id, title, repository_url, local_path, base_branch, reconcile_mode) + VALUES ($1, $2, $3, $4, $5, $6) RETURNING * "#, ) .bind(owner_id) .bind(&req.title) - .bind(&req.goal) .bind(&req.repository_url) .bind(&req.local_path) .bind(&req.base_branch) .bind(req.reconcile_mode.as_deref().unwrap_or("auto")) - .fetch_one(pool) - .await + .fetch_one(&mut *tx) + .await?; + + if let Some(body) = &req.contract_body { + sqlx::query( + r#" + INSERT INTO directive_documents (directive_id, title, body, status, position) + VALUES ($1, '', $2, 'draft', 0) + "#, + ) + .bind(directive.id) + .bind(body) + .execute(&mut *tx) + .await?; + } + + tx.commit().await?; + Ok(directive) +} + +/// Resolve the body of the directive's "current spec" — the active +/// contract's body, falling back to the most-recently-updated draft if +/// none is active. Returns empty string when the directive has no +/// usable contracts (orchestrator should refuse to spawn in that case). +pub async fn get_active_contract_body( + pool: &PgPool, + directive_id: Uuid, +) -> Result<String, sqlx::Error> { + let row: Option<(String,)> = sqlx::query_as( + r#" + SELECT body FROM directive_documents + WHERE directive_id = $1 + AND status IN ('active', 'queued', 'draft') + ORDER BY + CASE status + WHEN 'active' THEN 0 + WHEN 'queued' THEN 1 + WHEN 'draft' THEN 2 + ELSE 3 + END, + updated_at DESC + LIMIT 1 + "#, + ) + .bind(directive_id) + .fetch_optional(pool) + .await?; + Ok(row.map(|r| r.0).unwrap_or_default()) } /// Get a single directive for an owner. @@ -5212,7 +5263,7 @@ pub async fn list_directives_for_owner( sqlx::query_as::<_, DirectiveSummary>( r#" SELECT - d.id, d.owner_id, d.title, d.goal, d.status, d.repository_url, + d.id, d.owner_id, d.title, d.status, d.repository_url, d.orchestrator_task_id, d.pr_url, d.completion_task_id, d.reconcile_mode, d.version, d.created_at, d.updated_at, @@ -5271,8 +5322,6 @@ pub async fn update_directive_for_owner( } let title = req.title.as_deref().unwrap_or(¤t.title); - let goal = req.goal.as_deref().unwrap_or(¤t.goal); - let goal_changed = goal != current.goal; let status = req.status.as_deref().unwrap_or(¤t.status); let repository_url = req.repository_url.as_deref().or(current.repository_url.as_deref()); let local_path = req.local_path.as_deref().or(current.local_path.as_deref()); @@ -5285,10 +5334,9 @@ pub async fn update_directive_for_owner( let result = sqlx::query_as::<_, Directive>( r#" UPDATE directives - SET title = $3, goal = $4, status = $5, repository_url = $6, local_path = $7, - base_branch = $8, orchestrator_task_id = $9, pr_url = $10, pr_branch = $11, - reconcile_mode = $12, - goal_updated_at = CASE WHEN $13 THEN NOW() ELSE goal_updated_at END, + SET title = $3, status = $4, repository_url = $5, local_path = $6, + base_branch = $7, orchestrator_task_id = $8, pr_url = $9, pr_branch = $10, + reconcile_mode = $11, version = version + 1, updated_at = NOW() WHERE id = $1 AND owner_id = $2 RETURNING * @@ -5297,7 +5345,6 @@ pub async fn update_directive_for_owner( .bind(id) .bind(owner_id) .bind(title) - .bind(goal) .bind(status) .bind(repository_url) .bind(local_path) @@ -5306,7 +5353,6 @@ pub async fn update_directive_for_owner( .bind(pr_url) .bind(pr_branch) .bind(reconcile_mode) - .bind(goal_changed) .fetch_optional(pool) .await .map_err(RepositoryError::Database)?; @@ -6454,45 +6500,6 @@ pub async fn check_directive_idle( Ok(result.rows_affected() > 0) } -/// Update a directive's goal and bump goal_updated_at. -/// Reactivates draft/idle/paused/inactive directives and clears any stale -/// orchestrator task so that planning/replanning triggers on the next -/// reconciler tick. -/// -/// `draft` flips because the document-mode UI treats the first goal save as -/// the implicit "start". `inactive` flips because editing a contract whose -/// last revision was already shipped is the way the user kicks off an -/// amendment — the planner picks it up via phase_planning/replanning and -/// uses get_latest_merged_revision to learn the BEFORE→AFTER diff. -pub async fn update_directive_goal( - pool: &PgPool, - owner_id: Uuid, - directive_id: Uuid, - goal: &str, -) -> Result<Option<Directive>, sqlx::Error> { - sqlx::query_as::<_, Directive>( - r#" - UPDATE directives - SET goal = $3, - goal_updated_at = NOW(), - status = CASE - WHEN status IN ('draft', 'idle', 'paused', 'inactive') THEN 'active' - ELSE status - END, - orchestrator_task_id = NULL, - updated_at = NOW(), - version = version + 1 - WHERE id = $1 AND owner_id = $2 - RETURNING * - "#, - ) - .bind(directive_id) - .bind(owner_id) - .bind(goal) - .fetch_optional(pool) - .await -} - /// Mark a directive 'inactive'. Used at the moment a PR is raised — at that /// point the contract's current iteration is "shipped" and editing the goal /// (Stage 4) starts an amendment cycle. Idempotent: no-op if status is @@ -6517,11 +6524,10 @@ pub async fn set_directive_inactive( Ok(()) } -/// Reset a directive for a "new draft" cycle: clear the goal back to empty, -/// flip status to 'draft', and detach the current pr_url / pr_branch / -/// orchestrator linkage so the next goal save starts fresh. Prior revisions -/// remain in `directive_revisions` as the historical record. Used by the -/// sidebar's "New draft" right-click on inactive contracts. +/// Reset a directive for a "new draft" cycle: flip status to 'draft' and +/// detach the current pr_url / pr_branch / orchestrator linkage so the +/// next contract activation starts fresh. Prior revisions remain in +/// `directive_revisions` as the historical record. pub async fn reset_directive_for_new_draft( pool: &PgPool, owner_id: Uuid, @@ -6530,9 +6536,7 @@ pub async fn reset_directive_for_new_draft( sqlx::query_as::<_, Directive>( r#" UPDATE directives - SET goal = '', - goal_updated_at = NOW(), - status = 'draft', + SET status = 'draft', pr_url = NULL, pr_branch = NULL, orchestrator_task_id = NULL, @@ -6549,40 +6553,6 @@ pub async fn reset_directive_for_new_draft( .await } -/// Update a directive's goal WITHOUT clearing the orchestrator task id. -/// -/// This is the path used by the goal-edit interrupt cycle: when a small goal -/// edit arrives while a planner is already running, we want to keep the -/// planner attached so a `SendMessage` can summarise the change in-flight -/// instead of cancelling and respawning. We still bump `goal_updated_at` so -/// the timestamp reflects the edit, but we do NOT trigger replanning by -/// clearing the orchestrator task. We also do not flip status from -/// idle/paused → active here, since by definition a planner is already -/// running. -pub async fn update_directive_goal_keep_orchestrator( - pool: &PgPool, - owner_id: Uuid, - directive_id: Uuid, - goal: &str, -) -> Result<Option<Directive>, sqlx::Error> { - sqlx::query_as::<_, Directive>( - r#" - UPDATE directives - SET goal = $3, - goal_updated_at = NOW(), - updated_at = NOW(), - version = version + 1 - WHERE id = $1 AND owner_id = $2 - RETURNING * - "#, - ) - .bind(directive_id) - .bind(owner_id) - .bind(goal) - .fetch_optional(pool) - .await -} - // ============================================================================= // Directive Revisions — per-PR snapshots of the contract content. // ============================================================================= @@ -6701,42 +6671,6 @@ pub async fn get_latest_merged_revision( .await } -/// Save a goal to the directive goal history. -pub async fn save_directive_goal_history( - pool: &PgPool, - directive_id: Uuid, - goal: &str, -) -> Result<(), sqlx::Error> { - sqlx::query( - r#"INSERT INTO directive_goal_history (directive_id, goal) - VALUES ($1, $2)"#, - ) - .bind(directive_id) - .bind(goal) - .execute(pool) - .await?; - Ok(()) -} - -/// Get recent goal history for a directive (most recent first), limited to limit entries. -pub async fn get_directive_goal_history( - pool: &PgPool, - directive_id: Uuid, - limit: i64, -) -> Result<Vec<DirectiveGoalHistory>, sqlx::Error> { - sqlx::query_as::<_, DirectiveGoalHistory>( - r#"SELECT id, directive_id, goal, created_at - FROM directive_goal_history - WHERE directive_id = $1 - ORDER BY created_at DESC - LIMIT $2"#, - ) - .bind(directive_id) - .bind(limit) - .fetch_all(pool) - .await -} - /// Set a directive's status (used for start/pause/archive transitions). pub async fn set_directive_status( pool: &PgPool, |
