summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--makima/frontend/src/components/NavStrip.tsx2
-rw-r--r--makima/frontend/src/components/contracts/ContractDetail.tsx4
-rw-r--r--makima/frontend/src/components/directives/DirectiveDetail.tsx2
-rw-r--r--makima/frontend/src/components/orders/OrderDetail.tsx114
-rw-r--r--makima/frontend/src/components/orders/OrderList.tsx2
-rw-r--r--makima/frontend/src/routes/contracts.tsx2
-rw-r--r--makima/src/db/models.rs8
-rw-r--r--makima/src/db/repository.rs90
-rw-r--r--makima/src/orchestration/directive.rs119
-rw-r--r--makima/src/server/handlers/directives.rs68
-rw-r--r--makima/src/server/handlers/orders.rs26
11 files changed, 331 insertions, 106 deletions
diff --git a/makima/frontend/src/components/NavStrip.tsx b/makima/frontend/src/components/NavStrip.tsx
index 5aba6a3..117f4e1 100644
--- a/makima/frontend/src/components/NavStrip.tsx
+++ b/makima/frontend/src/components/NavStrip.tsx
@@ -11,8 +11,8 @@ interface NavLink {
const NAV_LINKS: NavLink[] = [
{ label: "Listen", href: "/listen" },
{ label: "Directives", href: "/directives", requiresAuth: true },
- { label: "Contracts", href: "/contracts", requiresAuth: true },
{ label: "Orders", href: "/orders", requiresAuth: true },
+ { label: "Contracts", href: "/contracts", requiresAuth: true },
{ label: "Mesh", href: "/mesh", requiresAuth: true },
{ label: "History", href: "/history", requiresAuth: true },
];
diff --git a/makima/frontend/src/components/contracts/ContractDetail.tsx b/makima/frontend/src/components/contracts/ContractDetail.tsx
index e5e65f6..46b2212 100644
--- a/makima/frontend/src/components/contracts/ContractDetail.tsx
+++ b/makima/frontend/src/components/contracts/ContractDetail.tsx
@@ -102,7 +102,7 @@ export function ContractDetail({
];
return (
- <div className="panel h-full flex flex-col">
+ <div className="panel h-full flex flex-col min-h-0">
{/* Header */}
<div className="p-4 border-b border-dashed border-[rgba(117,170,252,0.35)]">
<div className="flex items-center justify-between mb-3">
@@ -224,7 +224,7 @@ export function ContractDetail({
</div>
{/* Tab content */}
- <div className="flex-1 overflow-y-auto p-4">
+ <div className="flex-1 overflow-y-auto p-4 min-h-0">
{activeTab === "overview" && (
<OverviewTab
contract={contract}
diff --git a/makima/frontend/src/components/directives/DirectiveDetail.tsx b/makima/frontend/src/components/directives/DirectiveDetail.tsx
index c8da7a0..c9dac37 100644
--- a/makima/frontend/src/components/directives/DirectiveDetail.tsx
+++ b/makima/frontend/src/components/directives/DirectiveDetail.tsx
@@ -355,7 +355,7 @@ export function DirectiveDetail({
disabled={pickingUpOrders}
className="text-[10px] font-mono text-[#c084fc] hover:text-[#d8b4fe] border border-[rgba(192,132,252,0.3)] rounded px-2 py-1 disabled:opacity-50"
>
- {pickingUpOrders ? "Picking up..." : "Pick Up Orders"}
+ {pickingUpOrders ? "Planning..." : "Plan Orders"}
</button>
<button
type="button"
diff --git a/makima/frontend/src/components/orders/OrderDetail.tsx b/makima/frontend/src/components/orders/OrderDetail.tsx
index 12c87d1..9c3ac97 100644
--- a/makima/frontend/src/components/orders/OrderDetail.tsx
+++ b/makima/frontend/src/components/orders/OrderDetail.tsx
@@ -60,6 +60,7 @@ export function OrderDetail({
const [editingLabels, setEditingLabels] = useState(false);
const [labelsText, setLabelsText] = useState(order.labels.join(", "));
const [showLinkDirective, setShowLinkDirective] = useState(false);
+ const [directiveSearch, setDirectiveSearch] = useState("");
const badge = STATUS_BADGE[order.status] || STATUS_BADGE.open;
const currentPriority = PRIORITY_OPTIONS.find((p) => p.value === order.priority) || PRIORITY_OPTIONS[4];
@@ -406,31 +407,96 @@ export function OrderDetail({
<div className="flex flex-col gap-2">
{/* Link to Directive */}
<div>
- <button
- type="button"
- onClick={() => setShowLinkDirective(!showLinkDirective)}
- className="text-[10px] font-mono text-[#75aafc] hover:text-white border border-[rgba(117,170,252,0.3)] rounded px-2 py-1 w-full text-left"
- >
- Link to Directive
- </button>
+ <div className="flex items-center gap-1.5">
+ <button
+ type="button"
+ onClick={() => {
+ setShowLinkDirective(!showLinkDirective);
+ setDirectiveSearch("");
+ }}
+ className="text-[10px] font-mono text-[#75aafc] hover:text-white border border-[rgba(117,170,252,0.3)] rounded px-2 py-1 flex-1 text-left"
+ >
+ {order.directiveId ? "Change Directive" : "Link to Directive"}
+ </button>
+ {order.directiveId && (
+ <button
+ type="button"
+ onClick={() => onUpdate({ directiveId: null, directiveStepId: null })}
+ className="text-[10px] font-mono text-red-400 hover:text-red-300 border border-red-800 rounded px-2 py-1"
+ title="Unlink directive"
+ >
+ Unlink
+ </button>
+ )}
+ </div>
{showLinkDirective && (
- <div className="mt-1 border border-[rgba(117,170,252,0.2)] bg-[#0a1525] max-h-32 overflow-y-auto rounded">
- {directives.length === 0 ? (
- <div className="px-3 py-2 text-[10px] font-mono text-[#556677]">
- No directives available
- </div>
- ) : (
- directives.map((d) => (
- <button
- key={d.id}
- type="button"
- onClick={() => handleLinkDirective(d.id)}
- className="w-full text-left px-3 py-1.5 text-[10px] font-mono text-[#9bc3ff] hover:bg-[rgba(117,170,252,0.1)] border-b border-[rgba(117,170,252,0.1)] last:border-b-0"
- >
- {d.title}
- </button>
- ))
- )}
+ <div className="mt-1 border border-[rgba(117,170,252,0.2)] bg-[#0a1525] rounded">
+ <div className="px-2 py-1.5 border-b border-[rgba(117,170,252,0.1)]">
+ <input
+ type="text"
+ value={directiveSearch}
+ onChange={(e) => setDirectiveSearch(e.target.value)}
+ placeholder="Search directives..."
+ autoFocus
+ className="w-full bg-transparent border-none outline-none text-[10px] font-mono text-[#75aafc] placeholder-[#556677]"
+ />
+ </div>
+ <div className="max-h-32 overflow-y-auto">
+ {directives.length === 0 ? (
+ <div className="px-3 py-2 text-[10px] font-mono text-[#556677]">
+ No directives available
+ </div>
+ ) : (
+ (() => {
+ const filtered = directives.filter((d) =>
+ d.title.toLowerCase().includes(directiveSearch.toLowerCase())
+ );
+ if (filtered.length === 0) {
+ return (
+ <div className="px-3 py-2 text-[10px] font-mono text-[#556677]">
+ No matching directives
+ </div>
+ );
+ }
+ return filtered.map((d) => {
+ const isLinked = d.id === order.directiveId;
+ const statusColors: Record<string, string> = {
+ draft: "text-[#556677] border-[#2a3a5a]",
+ active: "text-emerald-400 border-emerald-800",
+ idle: "text-[#7788aa] border-[#2a3a5a]",
+ paused: "text-yellow-400 border-yellow-800",
+ archived: "text-[#556677] border-[#2a3a5a]",
+ };
+ const sColor = statusColors[d.status] || statusColors.draft;
+ return (
+ <button
+ key={d.id}
+ type="button"
+ onClick={() => handleLinkDirective(d.id)}
+ className={`w-full text-left px-3 py-1.5 text-[10px] font-mono hover:bg-[rgba(117,170,252,0.1)] border-b border-[rgba(117,170,252,0.1)] last:border-b-0 ${
+ isLinked ? "bg-[rgba(117,170,252,0.08)] text-white" : "text-[#9bc3ff]"
+ }`}
+ >
+ <div className="flex items-center gap-1.5">
+ <span className={`shrink-0 text-[8px] font-mono ${sColor} border rounded px-1 py-0.5 uppercase`}>
+ {d.status}
+ </span>
+ <span className="truncate">{d.title}</span>
+ {isLinked && (
+ <span className="shrink-0 text-[8px] text-emerald-400">●</span>
+ )}
+ </div>
+ {d.repositoryUrl && (
+ <div className="text-[8px] text-[#556677] truncate mt-0.5">
+ {d.repositoryUrl}
+ </div>
+ )}
+ </button>
+ );
+ });
+ })()
+ )}
+ </div>
</div>
)}
</div>
diff --git a/makima/frontend/src/components/orders/OrderList.tsx b/makima/frontend/src/components/orders/OrderList.tsx
index 213a332..0ebd18d 100644
--- a/makima/frontend/src/components/orders/OrderList.tsx
+++ b/makima/frontend/src/components/orders/OrderList.tsx
@@ -4,7 +4,7 @@ import type { Order, OrderStatus, OrderPriority, OrderType } from "../../lib/api
const STATUS_BADGE: Record<OrderStatus, { color: string; label: string }> = {
open: { color: "text-[#75aafc] border-[rgba(117,170,252,0.4)]", label: "OPEN" },
in_progress: { color: "text-yellow-400 border-yellow-800", label: "IN PROGRESS" },
- under_review: { color: "text-purple-400 border-purple-800", label: "UNDER REVIEW" },
+ under_review: { color: "text-purple-400 border-purple-800", label: "REVIEW" },
done: { color: "text-emerald-400 border-emerald-800", label: "DONE" },
archived: { color: "text-[#556677] border-[#2a3a5a]", label: "ARCHIVED" },
};
diff --git a/makima/frontend/src/routes/contracts.tsx b/makima/frontend/src/routes/contracts.tsx
index 6d838ab..7046f66 100644
--- a/makima/frontend/src/routes/contracts.tsx
+++ b/makima/frontend/src/routes/contracts.tsx
@@ -542,7 +542,7 @@ function ContractsPageContent() {
</div>
{/* Right: Detail or Create */}
- <div className="flex-1 overflow-hidden flex flex-col">
+ <div className="flex-1 overflow-hidden flex flex-col min-h-0">
{error && (
<div className="p-3 bg-red-400/10 border border-red-400/30 text-red-400 font-mono text-sm">
{error}
diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs
index e53dcce..f9a34b8 100644
--- a/makima/src/db/models.rs
+++ b/makima/src/db/models.rs
@@ -2860,6 +2860,8 @@ pub struct CreateDirectiveStepRequest {
#[serde(default)]
pub order_index: i32,
pub generation: Option<i32>,
+ /// Optional order ID to auto-link this step to an order.
+ #[serde(default)]
pub order_id: Option<Uuid>,
}
@@ -2892,13 +2894,13 @@ pub struct Order {
pub description: Option<String>,
/// Priority: critical, high, medium, low, none
pub priority: String,
- /// Status: open, in_progress, done, archived
+ /// Status: open, in_progress, under_review, done, archived
pub status: String,
/// Type of work: feature, bug, spike, chore, improvement
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>,
@@ -2958,7 +2960,7 @@ pub struct OrderListResponse {
#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct OrderListQuery {
- /// Filter by status (e.g., "open", "in_progress", "done", "archived")
+ /// Filter by status (e.g., "open", "in_progress", "under_review", "done", "archived")
pub status: Option<String>,
/// Filter by order type (e.g., "feature", "bug", "spike", "chore", "improvement")
#[serde(rename = "type")]
diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs
index bc61bee..c7b0a1f 100644
--- a/makima/src/db/repository.rs
+++ b/makima/src/db/repository.rs
@@ -5400,6 +5400,7 @@ pub async fn create_directive_step(
req: CreateDirectiveStepRequest,
) -> Result<DirectiveStep, sqlx::Error> {
let generation = req.generation.unwrap_or(1);
+ let order_id = req.order_id;
let step = sqlx::query_as::<_, DirectiveStep>(
r#"
INSERT INTO directive_steps (directive_id, name, description, task_plan, depends_on, order_index, generation)
@@ -5417,15 +5418,15 @@ pub async fn create_directive_step(
.fetch_one(pool)
.await?;
- // Link the order to this step if an order_id was provided
- if let Some(order_id) = req.order_id {
- let _ = sqlx::query(
- "UPDATE orders SET directive_step_id = $1, updated_at = NOW() WHERE id = $2",
+ // If an order_id was provided, auto-link the order to this step
+ if let Some(oid) = order_id {
+ sqlx::query(
+ r#"UPDATE orders SET directive_step_id = $1, updated_at = NOW() WHERE id = $2"#,
)
.bind(step.id)
- .bind(order_id)
+ .bind(oid)
.execute(pool)
- .await;
+ .await?;
}
Ok(step)
@@ -6331,19 +6332,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
@@ -6354,6 +6357,7 @@ pub async fn get_available_orders_for_pickup(
"#,
)
.bind(owner_id)
+ .bind(directive_id)
.fetch_all(pool)
.await
}
@@ -6382,3 +6386,71 @@ 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
+}
+
+/// Reconcile directive orders by checking linked step statuses.
+/// - Orders linked to completed steps are marked "done"
+/// - Orders linked to running/ready steps are marked "under_review"
+/// Returns the count of orders updated.
+pub async fn reconcile_directive_orders(
+ pool: &PgPool,
+ owner_id: Uuid,
+ directive_id: Uuid,
+) -> Result<i64, sqlx::Error> {
+ let rows: Vec<(Uuid,)> = sqlx::query_as(
+ r#"
+ UPDATE orders o
+ SET status = CASE
+ WHEN ds.status = 'completed' THEN 'done'
+ WHEN ds.status IN ('running', 'ready') THEN 'under_review'
+ ELSE o.status
+ END,
+ updated_at = NOW()
+ FROM directive_steps ds
+ WHERE o.directive_step_id = ds.id
+ AND o.directive_id = $1
+ AND o.owner_id = $2
+ AND o.status NOT IN ('done', 'archived')
+ AND ds.status IN ('completed', 'running', 'ready')
+ RETURNING o.id
+ "#,
+ )
+ .bind(directive_id)
+ .bind(owner_id)
+ .fetch_all(pool)
+ .await?;
+
+ Ok(rows.len() as i64)
+}
+
diff --git a/makima/src/orchestration/directive.rs b/makima/src/orchestration/directive.rs
index 0a812ce..eb157a3 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,71 @@ 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.
+
+Goal: {goal}
+
+Steps completed:
+{step_summary}
+
+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. Generate a descriptive PR title and create a new PR:
+
+Based on the steps completed above, generate a descriptive PR title that summarizes the actual changes (not just the directive name "{title}"). The title should:
+- Be concise (under 72 characters)
+- Describe WHAT was done, not just the project name
+- Use conventional commit style if appropriate (feat:, fix:, refactor:, etc.)
+
+Then create the PR:
+```
+gh pr create --title "<YOUR_GENERATED_TITLE>" --body "{pr_body}" --head "$NEW_BRANCH" --base {base_branch}
+```
+Replace <YOUR_GENERATED_TITLE> with the concise descriptive title you generated.
+
+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 +1421,17 @@ git push origin {directive_branch}
Already-merged branches will be a no-op. If there are merge conflicts, resolve them sensibly.
"#,
title = directive.title,
+ goal = directive.goal,
+ 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
@@ -1374,10 +1451,18 @@ git checkout -b {directive_branch} origin/{base_branch}
git push -u origin {directive_branch}
```
-Then create the PR:
+Then generate a descriptive PR title and create the PR:
+
+Based on the steps completed above, generate a descriptive PR title that summarizes the actual changes (not just the directive name "{title}"). The title should:
+- Be concise (under 72 characters)
+- Describe WHAT was done, not just the project name
+- Use conventional commit style if appropriate (feat:, fix:, refactor:, etc.)
+For example, instead of "soryu-co/soryu - makima" use something like "Fix order lifecycle, PR update, and contracts overflow".
+
```
-gh pr create --title "{title}" --body "{pr_body}" --head {directive_branch} --base {base_branch}
+gh pr create --title "<YOUR_GENERATED_TITLE>" --body "{pr_body}" --head {directive_branch} --base {base_branch}
```
+Replace <YOUR_GENERATED_TITLE> with the concise descriptive title you generated.
IMPORTANT: After creating the PR, you MUST store the PR URL so the directive system can track it.
@@ -1437,9 +1522,15 @@ Done — the PR already exists.
git ls-remote --heads origin {pr_branch}
```
-4. If the branch exists, create the PR:
+4. If the branch exists, generate a descriptive PR title and create the PR:
+
+Based on the branch name and directive "{title}", generate a descriptive PR title that summarizes the actual changes. The title should:
+- Be concise (under 72 characters)
+- Describe WHAT was done, not just the project name
+- Use conventional commit style if appropriate (feat:, fix:, refactor:, etc.)
+
```
-gh pr create --title "{title}" --body "Directive PR verification — auto-created" --head {pr_branch} --base {base_branch}
+gh pr create --title "<YOUR_GENERATED_TITLE>" --body "Directive PR verification — auto-created" --head {pr_branch} --base {base_branch}
```
Then store the resulting URL:
```
@@ -1508,7 +1599,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() {
@@ -1652,7 +1743,7 @@ Submit steps using generation {generation}:
(Use --depends-on "uuid1,uuid2" for dependencies)
Or batch:
- makima directive batch-add-steps --json '[{{"name":"...","description":"...","taskPlan":"...","dependsOn":[],"orderIndex":0,"generation":{generation},"orderId":"<uuid-of-order>"}}]'
+ makima directive batch-add-steps --json '[{{"name":"...","description":"...","taskPlan":"...","dependsOn":[],"orderIndex":0,"generation":{generation},"orderId":"<order-uuid>"}}]'
DEPENDENCY WORKTREE CONTINUATION:
Each step runs in its own git worktree. How that worktree is initialised depends on dependsOn:
diff --git a/makima/src/server/handlers/directives.rs b/makima/src/server/handlers/directives.rs
index fd7ae81..b4b438a 100644
--- a/makima/src/server/handlers/directives.rs
+++ b/makima/src/server/handlers/directives.rs
@@ -1063,58 +1063,21 @@ pub async fn pick_up_orders(
}
};
- // Reconcile existing orders before picking up new ones
- if let Ok(existing_orders) = repository::list_orders(
- pool,
- auth.owner_id,
- Some("in_progress"),
- None,
- None,
- Some(id),
- None,
- )
- .await
- {
- for order in &existing_orders {
- if let Some(step_id) = order.directive_step_id {
- if let Ok(Some(step)) = repository::get_directive_step(pool, step_id).await {
- match step.status.as_str() {
- "completed" => {
- let order_update = UpdateOrderRequest {
- status: Some("done".to_string()),
- ..Default::default()
- };
- let _ = repository::update_order(
- pool,
- auth.owner_id,
- order.id,
- order_update,
- )
- .await;
- }
- "running" | "ready" | "pending" => {
- let order_update = UpdateOrderRequest {
- status: Some("under_review".to_string()),
- ..Default::default()
- };
- let _ = repository::update_order(
- pool,
- auth.owner_id,
- order.id,
- order_update,
- )
- .await;
- }
- "failed" => { /* keep as in_progress for re-planning */ }
- _ => {}
- }
- }
+ // Reconcile existing orders: mark done if step completed, under_review if step in progress
+ match repository::reconcile_directive_orders(pool, auth.owner_id, id).await {
+ Ok(count) => {
+ if count > 0 {
+ tracing::info!("Reconciled {} orders for directive {}", count, id);
}
}
+ Err(e) => {
+ tracing::warn!("Failed to reconcile directive orders: {}", e);
+ // Non-fatal: continue with pickup even if reconciliation fails
+ }
}
// 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);
@@ -1129,7 +1092,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,
})
@@ -1176,6 +1139,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,
@@ -1250,7 +1220,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) => {