summaryrefslogtreecommitdiff
path: root/makima/src/db/repository.rs
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-05-08 16:34:11 +0100
committerGitHub <noreply@github.com>2026-05-08 16:34:11 +0100
commitdce7f50e503dc374aaf879df33e725af16c4cc78 (patch)
tree50b6aad1aa47e56b61f0700e224028bb7578cb91 /makima/src/db/repository.rs
parente4f1622a0f0ac74707cc1c9810e0b99e948d1319 (diff)
downloadsoryu-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.rs200
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(&current.title);
- let goal = req.goal.as_deref().unwrap_or(&current.goal);
- let goal_changed = goal != current.goal;
let status = req.status.as_deref().unwrap_or(&current.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,