summaryrefslogtreecommitdiff
path: root/makima/src/orchestration/directive.rs
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/orchestration/directive.rs')
-rw-r--r--makima/src/orchestration/directive.rs151
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