diff options
Diffstat (limited to 'makima/src/orchestration/directive.rs')
| -rw-r--r-- | makima/src/orchestration/directive.rs | 151 |
1 files changed, 150 insertions, 1 deletions
diff --git a/makima/src/orchestration/directive.rs b/makima/src/orchestration/directive.rs index 80d8172..7897c2c 100644 --- a/makima/src/orchestration/directive.rs +++ b/makima/src/orchestration/directive.rs @@ -910,7 +910,7 @@ impl DirectiveOrchestrator { "Extracted PR URL from completion task output" ); let update = crate::db::models::UpdateDirectiveRequest { - pr_url: Some(url), + pr_url: Some(url.clone()), ..Default::default() }; let _ = repository::update_directive_for_owner( @@ -920,6 +920,36 @@ impl DirectiveOrchestrator { update, ) .await; + + // Lifecycle hook: mark the currently-active directive + // document as shipped (records pr_url + pr_branch and + // flips status to 'shipped'), then auto-create a fresh + // empty draft document under the same directive so the + // user has a clean slate for the next batch of work. + // + // Per the goal: "automatically create a new empty draft + // contract" once the previous contract ships. + // + // Idempotency: this branch is gated by + // `check.pr_url.is_none()` — `pr_url` is set on the + // directive in the same tick above, so a retry of the + // orchestrator will see `check.pr_url.is_some()` and + // skip the hook entirely. Inside the helper we also + // refuse to ship an already-shipped doc as a belt-and- + // braces guard. + if let Err(hook_err) = ship_active_document_for_directive( + &self.pool, + check.directive_id, + &url, + ) + .await + { + tracing::warn!( + directive_id = %check.directive_id, + error = %hook_err, + "Failed to ship active directive document — continuing" + ); + } } Ok(None) => { if check.task_name.starts_with("Verify PR:") { @@ -1157,6 +1187,125 @@ pub async fn remove_already_merged_steps( Ok(removed) } +/// Ship the currently-active directive document for `directive_id` and seed a +/// fresh empty draft document underneath the same directive. +/// +/// Called from the PR-raise completion path the moment the orchestrator +/// extracts a `pr_url` from the completion task's output (see Part 2 of +/// `phase_completion`). +/// +/// Selection rule: we only auto-ship when there is **exactly one** document +/// in `status = 'active'`. If there are zero or more than one, we log a TODO +/// and bail out — multi-document selection is the next step's UI work, where +/// the user explicitly chooses which contract a PR ships. +/// +/// On success we also auto-create a fresh empty draft document under the +/// same directive (per the goal's "automatically create a new empty draft +/// contract" requirement). The draft sits in `status = 'draft'` until the +/// user starts editing it. +/// +/// Idempotency: callers gate on `directive.pr_url.is_none()` so a retried +/// orchestrator tick won't re-fire this. As an extra guard we refuse to +/// ship a doc that is already in `shipped` state. +pub async fn ship_active_document_for_directive( + pool: &PgPool, + directive_id: Uuid, + pr_url: &str, +) -> Result<(), anyhow::Error> { + // Fetch the directive so we can pick up its pr_branch (set earlier in the + // completion flow) for the document record. + let directive = match repository::get_directive(pool, directive_id).await? { + Some(d) => d, + None => { + tracing::warn!( + directive_id = %directive_id, + "ship_active_document: directive not found" + ); + return Ok(()); + } + }; + let pr_branch = directive.pr_branch.as_deref(); + + let docs = repository::list_directive_documents(pool, directive_id).await?; + let active: Vec<_> = docs + .iter() + .filter(|d| d.status == "active") + .collect(); + + match active.len() { + 0 => { + tracing::info!( + directive_id = %directive_id, + "ship_active_document: no active document — nothing to ship" + ); + return Ok(()); + } + 1 => {} + n => { + // TODO(next-step): the new sidebar UI lets users pick which + // active document a PR ships. Until that lands we conservatively + // skip — better to leave docs unshipped than to ship the wrong + // one. + tracing::warn!( + directive_id = %directive_id, + active_count = n, + "ship_active_document: multiple active documents — leaving unshipped (UI pick coming)" + ); + return Ok(()); + } + } + + let target = active[0]; + + // Belt-and-braces idempotency: refuse to re-ship. + if target.status == "shipped" { + tracing::info!( + directive_id = %directive_id, + document_id = %target.id, + "ship_active_document: document already shipped — skipping" + ); + return Ok(()); + } + + let shipped = repository::mark_directive_document_shipped( + pool, + target.id, + pr_url, + pr_branch, + ) + .await?; + if let Some(s) = shipped { + tracing::info!( + directive_id = %directive_id, + document_id = %s.id, + pr_url = %pr_url, + "Marked directive document as shipped" + ); + } + + // Auto-create the fresh empty draft contract that succeeds the just- + // shipped one. Per the directive goal: "automatically create a new + // empty draft contract". + match repository::create_directive_document(pool, directive_id, "", "").await { + Ok(draft) => { + tracing::info!( + directive_id = %directive_id, + document_id = %draft.id, + "Created fresh empty draft directive document after shipping" + ); + } + Err(e) => { + tracing::warn!( + directive_id = %directive_id, + error = %e, + "Failed to auto-create draft directive document after shipping — continuing" + ); + } + } + + Ok(()) +} + /// Trigger a completion task (PR creation/update) for a directive. /// /// This is the public entry point used by both the orchestrator tick and the |
