summaryrefslogtreecommitdiff
path: root/makima/src
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-16 19:01:56 +0000
committerGitHub <noreply@github.com>2026-02-16 19:01:56 +0000
commit4bd40f047a6f4703945c6db2811d8feda27241d6 (patch)
treeb53feed7931309520c0886585487d143fc2957f4 /makima/src
parentb3de779d87450033f1e0361144c621a1d5f1dbf8 (diff)
downloadsoryu-4bd40f047a6f4703945c6db2811d8feda27241d6.tar.gz
soryu-4bd40f047a6f4703945c6db2811d8feda27241d6.zip
soryu-co/soryu - makima (#66)
* feat: soryu-co/soryu - makima: Fix contracts page scrolling overflow * WIP: heartbeat checkpoint * WIP: heartbeat checkpoint * WIP: heartbeat checkpoint * WIP: heartbeat checkpoint
Diffstat (limited to 'makima/src')
-rw-r--r--makima/src/db/models.rs2
-rw-r--r--makima/src/db/repository.rs42
-rw-r--r--makima/src/orchestration/directive.rs81
-rw-r--r--makima/src/server/handlers/directives.rs13
-rw-r--r--makima/src/server/handlers/orders.rs26
5 files changed, 147 insertions, 17 deletions
diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs
index 2951159..33b9795 100644
--- a/makima/src/db/models.rs
+++ b/makima/src/db/models.rs
@@ -2897,7 +2897,7 @@ pub struct Order {
pub order_type: String,
/// Flexible labels as JSON array of strings
pub labels: serde_json::Value,
- /// Linked directive (optional)
+ /// Linked directive (required for new orders, nullable for legacy rows)
pub directive_id: Option<Uuid>,
/// Linked directive step (optional)
pub directive_step_id: Option<Uuid>,
diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs
index cb6a0c6..d274548 100644
--- a/makima/src/db/repository.rs
+++ b/makima/src/db/repository.rs
@@ -6305,19 +6305,21 @@ pub async fn convert_order_to_step(
// Order Pickup
// =============================================================================
-/// Get available orders for pickup: open orders with no directive assigned,
+/// Get available orders for pickup: open orders with no directive assigned
+/// OR orders already linked to this specific directive that are not yet done,
/// sorted by priority (critical first) then creation date.
pub async fn get_available_orders_for_pickup(
pool: &PgPool,
owner_id: Uuid,
+ directive_id: Uuid,
) -> Result<Vec<Order>, sqlx::Error> {
sqlx::query_as::<_, Order>(
r#"
SELECT *
FROM orders
WHERE owner_id = $1
- AND status = 'open'
- AND directive_id IS NULL
+ AND status IN ('open', 'in_progress')
+ AND (directive_id IS NULL OR directive_id = $2)
ORDER BY CASE priority
WHEN 'critical' THEN 0
WHEN 'high' THEN 1
@@ -6328,6 +6330,7 @@ pub async fn get_available_orders_for_pickup(
"#,
)
.bind(owner_id)
+ .bind(directive_id)
.fetch_all(pool)
.await
}
@@ -6356,3 +6359,36 @@ pub async fn bulk_link_orders_to_directive(
Ok(result.rows_affected() as i64)
}
+/// Bulk update order status for a set of order IDs.
+/// Returns the count of updated rows.
+pub async fn bulk_update_order_status(
+ pool: &PgPool,
+ owner_id: Uuid,
+ order_ids: &[Uuid],
+ status: &str,
+) -> Result<i64, sqlx::Error> {
+ let result = sqlx::query(
+ r#"UPDATE orders SET status = $1, updated_at = NOW()
+ WHERE id = ANY($2) AND owner_id = $3"#,
+ )
+ .bind(status)
+ .bind(order_ids)
+ .bind(owner_id)
+ .execute(pool)
+ .await?;
+ Ok(result.rows_affected() as i64)
+}
+
+/// Get orders linked to a specific directive step.
+pub async fn get_orders_by_step_id(
+ pool: &PgPool,
+ step_id: Uuid,
+) -> Result<Vec<Order>, sqlx::Error> {
+ sqlx::query_as::<_, Order>(
+ r#"SELECT * FROM orders WHERE directive_step_id = $1"#,
+ )
+ .bind(step_id)
+ .fetch_all(pool)
+ .await
+}
+
diff --git a/makima/src/orchestration/directive.rs b/makima/src/orchestration/directive.rs
index 020c2e4..62f15d9 100644
--- a/makima/src/orchestration/directive.rs
+++ b/makima/src/orchestration/directive.rs
@@ -245,6 +245,20 @@ impl DirectiveOrchestrator {
..Default::default()
};
repository::update_directive_step(&self.pool, step.step_id, update).await?;
+
+ // Mark linked orders as done
+ if let Ok(linked_orders) = repository::get_orders_by_step_id(&self.pool, step.step_id).await {
+ for order in linked_orders {
+ if order.status != "done" && order.status != "archived" {
+ let order_update = crate::db::models::UpdateOrderRequest {
+ status: Some("done".to_string()),
+ ..Default::default()
+ };
+ let _ = repository::update_order(&self.pool, order.owner_id, order.id, order_update).await;
+ }
+ }
+ }
+
repository::advance_directive_ready_steps(&self.pool, step.directive_id)
.await?;
repository::check_directive_idle(&self.pool, step.directive_id).await?;
@@ -961,11 +975,7 @@ pub async fn trigger_completion_task(
})
.collect();
- let prompt = if directive.pr_url.is_some() {
- build_verification_prompt(&directive, &directive_branch, base_branch)
- } else {
- build_completion_prompt(&directive, &step_tasks, &step_branches, &directive_branch, base_branch)
- };
+ let prompt = build_completion_prompt(&directive, &step_tasks, &step_branches, &directive_branch, base_branch);
let task_name = if directive.pr_url.is_some() {
format!("Update PR: {}", directive.title)
@@ -1330,12 +1340,58 @@ fn build_completion_prompt(
.collect::<Vec<_>>()
.join("\n");
- if directive.pr_url.is_some() {
- // Re-completion: PR already exists, merge new branches into existing PR branch
+ if let Some(ref pr_url) = directive.pr_url {
+ // Re-completion: PR already exists — but it may have been merged or closed.
+ // We must check the PR state first and handle accordingly.
format!(
r#"You are updating an existing PR for directive "{title}".
-The PR branch `{directive_branch}` already exists. Merge any new step branches into it.
+IMPORTANT: The previous PR may have been merged or closed. You MUST check its state first.
+
+## Step 1: Check PR state
+
+Run this command to check the PR state:
+```
+gh pr view {pr_url} --json state --jq '.state'
+```
+
+## If the PR state is MERGED or CLOSED:
+
+The previous PR was already merged/closed. You need to create a NEW PR with a fresh branch.
+
+1. Clear the old PR URL:
+```
+makima directive update --pr-url ""
+```
+
+2. Create a fresh branch with a timestamp suffix to avoid collision:
+```
+git fetch origin
+NEW_BRANCH="{directive_branch}-v$(date +%s)"
+git checkout -b "$NEW_BRANCH" origin/{base_branch}
+{merge_commands}
+git push -u origin "$NEW_BRANCH"
+```
+
+3. Create a new PR:
+```
+gh pr create --title "{title}" --body "{pr_body}" --head "$NEW_BRANCH" --base {base_branch}
+```
+
+4. Store the new PR URL:
+```
+makima directive update --pr-url "<URL_FROM_GH_PR_CREATE>"
+```
+Replace the URL with the actual PR URL from the `gh pr create` output. This step is CRITICAL.
+
+5. Update the directive pr_branch to the new branch name:
+```
+makima directive update --pr-branch "$NEW_BRANCH"
+```
+
+## If the PR state is OPEN:
+
+The PR is still open. Merge new step branches into the existing PR branch.
Steps completed:
{step_summary}
@@ -1352,9 +1408,16 @@ git push origin {directive_branch}
Already-merged branches will be a no-op. If there are merge conflicts, resolve them sensibly.
"#,
title = directive.title,
+ pr_url = pr_url,
directive_branch = directive_branch,
+ base_branch = base_branch,
step_summary = step_summary,
merge_commands = merge_commands,
+ pr_body = format!(
+ "## Directive\\n\\n{}\\n\\n## Steps\\n\\n{}",
+ directive.goal.replace('\n', "\\n").replace('"', "\\\""),
+ step_summary.replace('\n', "\\n").replace('"', "\\\""),
+ ),
)
} else {
// First completion: create new PR
@@ -1508,7 +1571,7 @@ pub fn build_order_pickup_prompt(
}
// ── Orders being picked up ───────────────────────────────────
- prompt.push_str("== ORDERS AVAILABLE FOR PICKUP ==\n");
+ prompt.push_str("== ORDERS AVAILABLE FOR PLANNING ==\n");
prompt.push_str("The following open orders have been linked to this directive. \
Review them and create steps to fulfil them.\n\n");
for (i, order) in orders.iter().enumerate() {
diff --git a/makima/src/server/handlers/directives.rs b/makima/src/server/handlers/directives.rs
index 960da94..15df6d5 100644
--- a/makima/src/server/handlers/directives.rs
+++ b/makima/src/server/handlers/directives.rs
@@ -1063,7 +1063,7 @@ pub async fn pick_up_orders(
};
// Fetch available orders
- let orders = match repository::get_available_orders_for_pickup(pool, auth.owner_id).await {
+ let orders = match repository::get_available_orders_for_pickup(pool, auth.owner_id, id).await {
Ok(o) => o,
Err(e) => {
tracing::error!("Failed to fetch available orders: {}", e);
@@ -1078,7 +1078,7 @@ pub async fn pick_up_orders(
// If no orders available, return early
if orders.is_empty() {
return Json(PickUpOrdersResponse {
- message: "No orders available to pick up".to_string(),
+ message: "No orders available to plan".to_string(),
order_count: 0,
task_id: None,
})
@@ -1125,6 +1125,13 @@ pub async fn pick_up_orders(
.into_response();
}
+ // Mark picked-up orders as in_progress
+ if let Err(e) =
+ repository::bulk_update_order_status(pool, auth.owner_id, &order_ids, "in_progress").await
+ {
+ tracing::warn!("Failed to update order status to in_progress: {}", e);
+ }
+
// Create the planning task
let req = CreateTaskRequest {
contract_id: None,
@@ -1199,7 +1206,7 @@ pub async fn pick_up_orders(
let _ = repository::advance_directive_ready_steps(pool, id).await;
Json(PickUpOrdersResponse {
- message: format!("Picked up {} orders", order_count),
+ message: format!("Planning {} orders", order_count),
order_count,
task_id: Some(task.id),
})
diff --git a/makima/src/server/handlers/orders.rs b/makima/src/server/handlers/orders.rs
index cddf6a6..1251f79 100644
--- a/makima/src/server/handlers/orders.rs
+++ b/makima/src/server/handlers/orders.rs
@@ -81,13 +81,14 @@ pub async fn list_orders(
}
}
-/// Create a new order.
+/// Create a new order. A valid directive_id is required.
#[utoipa::path(
post,
path = "/api/v1/orders",
request_body = CreateOrderRequest,
responses(
(status = 201, description = "Order created", body = Order),
+ (status = 400, description = "Invalid directive_id", body = ApiError),
(status = 401, description = "Unauthorized", body = ApiError),
(status = 503, description = "Database not configured", body = ApiError),
),
@@ -107,6 +108,29 @@ pub async fn create_order(
.into_response();
};
+ // Validate the directive exists and belongs to this owner.
+ // directive_id is required by the CreateOrderRequest struct (Uuid, not Option<Uuid>).
+ match repository::get_directive_for_owner(pool, auth.owner_id, req.directive_id).await {
+ Ok(Some(_)) => {}
+ Ok(None) => {
+ return (
+ StatusCode::BAD_REQUEST,
+ Json(ApiError::new(
+ "INVALID_DIRECTIVE",
+ "directive_id is required and must reference a valid directive owned by you",
+ )),
+ )
+ .into_response();
+ }
+ Err(e) => {
+ return (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("VALIDATION_FAILED", &e.to_string())),
+ )
+ .into_response();
+ }
+ }
+
match repository::create_order(pool, auth.owner_id, req).await {
Ok(order) => (StatusCode::CREATED, Json(order)).into_response(),
Err(e) => {