summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-17 00:40:32 +0000
committerGitHub <noreply@github.com>2026-02-17 00:40:32 +0000
commitb67b3f8e8d63361d9ff19f87fd608c03bfa3fd43 (patch)
treed5429658a90c4be9a2984617118d2c843cadfdf6
parent4bd40f047a6f4703945c6db2811d8feda27241d6 (diff)
downloadsoryu-b67b3f8e8d63361d9ff19f87fd608c03bfa3fd43.tar.gz
soryu-b67b3f8e8d63361d9ff19f87fd608c03bfa3fd43.zip
soryu-co/soryu - makima (#67)
* feat: soryu-co/soryu - makima: Fix contracts page scrolling overflow * WIP: heartbeat checkpoint * WIP: heartbeat checkpoint * WIP: heartbeat checkpoint * WIP: heartbeat checkpoint * feat: soryu-co/soryu - makima: Remove contract association from orders and make directive mandatory in UI * feat: soryu-co/soryu - makima: Add directive name to order metadata for searchability * feat: soryu-co/soryu - makima: Change directive link in orders to use search interface * feat: soryu-co/soryu - makima: Name directive PRs based on content not directive title * feat: soryu-co/soryu - makima: Add orderId to step creation and auto-link orders to steps * feat: soryu-co/soryu - makima: Add under_review status and auto-complete orders in plan flow
-rw-r--r--makima/frontend/src/components/orders/OrderDetail.tsx117
-rw-r--r--makima/frontend/src/components/orders/OrderList.tsx5
-rw-r--r--makima/frontend/src/lib/api.ts2
-rw-r--r--makima/src/db/models.rs7
-rw-r--r--makima/src/db/repository.rs53
-rw-r--r--makima/src/orchestration/directive.rs28
-rw-r--r--makima/src/server/handlers/directives.rs13
7 files changed, 187 insertions, 38 deletions
diff --git a/makima/frontend/src/components/orders/OrderDetail.tsx b/makima/frontend/src/components/orders/OrderDetail.tsx
index 9d4c00c..1b8d76e 100644
--- a/makima/frontend/src/components/orders/OrderDetail.tsx
+++ b/makima/frontend/src/components/orders/OrderDetail.tsx
@@ -11,6 +11,7 @@ import type {
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-amber-400 border-amber-800", label: "UNDER REVIEW" },
done: { color: "text-emerald-400 border-emerald-800", label: "DONE" },
archived: { color: "text-[#556677] border-[#2a3a5a]", label: "ARCHIVED" },
};
@@ -31,7 +32,7 @@ const TYPE_OPTIONS: { value: OrderType; color: string; label: string }[] = [
{ value: "improvement", color: "text-emerald-400", label: "Improvement" },
];
-const STATUS_OPTIONS: OrderStatus[] = ["open", "in_progress", "done", "archived"];
+const STATUS_OPTIONS: OrderStatus[] = ["open", "in_progress", "under_review", "done", "archived"];
interface OrderDetailProps {
order: Order;
@@ -59,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];
@@ -405,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 1d279f7..3d63c54 100644
--- a/makima/frontend/src/components/orders/OrderList.tsx
+++ b/makima/frontend/src/components/orders/OrderList.tsx
@@ -4,6 +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-amber-400 border-amber-800", label: "REVIEW" },
done: { color: "text-emerald-400 border-emerald-800", label: "DONE" },
archived: { color: "text-[#556677] border-[#2a3a5a]", label: "ARCHIVED" },
};
@@ -35,7 +36,7 @@ interface OrderListProps {
onTypeFilter: (t: OrderType | undefined) => void;
}
-const STATUS_OPTIONS: (OrderStatus | "all")[] = ["all", "open", "in_progress", "done", "archived"];
+const STATUS_OPTIONS: (OrderStatus | "all")[] = ["all", "open", "in_progress", "under_review", "done", "archived"];
const TYPE_OPTIONS: (OrderType | "all")[] = ["all", "feature", "bug", "spike", "chore", "improvement"];
export function OrderList({
@@ -105,7 +106,7 @@ export function OrderList({
: "text-[#556677] hover:text-[#7788aa] border border-transparent"
}`}
>
- {s === "all" ? "ALL" : s === "in_progress" ? "WIP" : s.toUpperCase()}
+ {s === "all" ? "ALL" : s === "in_progress" ? "WIP" : s === "under_review" ? "REVIEW" : s.toUpperCase()}
</button>
))}
</div>
diff --git a/makima/frontend/src/lib/api.ts b/makima/frontend/src/lib/api.ts
index 17bc20f..ed628f7 100644
--- a/makima/frontend/src/lib/api.ts
+++ b/makima/frontend/src/lib/api.ts
@@ -3281,7 +3281,7 @@ export async function pickUpOrders(directiveId: string): Promise<PickUpOrdersRes
// =============================================================================
export type OrderPriority = "critical" | "high" | "medium" | "low" | "none";
-export type OrderStatus = "open" | "in_progress" | "done" | "archived";
+export type OrderStatus = "open" | "in_progress" | "under_review" | "done" | "archived";
export type OrderType = "feature" | "bug" | "spike" | "chore" | "improvement";
export interface Order {
diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs
index 33b9795..f9a34b8 100644
--- a/makima/src/db/models.rs
+++ b/makima/src/db/models.rs
@@ -2860,6 +2860,9 @@ 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>,
}
/// Request to update a directive step.
@@ -2891,7 +2894,7 @@ 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,
@@ -2957,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 d274548..aa1203a 100644
--- a/makima/src/db/repository.rs
+++ b/makima/src/db/repository.rs
@@ -5387,7 +5387,8 @@ pub async fn create_directive_step(
req: CreateDirectiveStepRequest,
) -> Result<DirectiveStep, sqlx::Error> {
let generation = req.generation.unwrap_or(1);
- sqlx::query_as::<_, DirectiveStep>(
+ 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)
VALUES ($1, $2, $3, $4, $5, $6, $7)
@@ -5402,7 +5403,20 @@ pub async fn create_directive_step(
.bind(req.order_index)
.bind(generation)
.fetch_one(pool)
- .await
+ .await?;
+
+ // 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(oid)
+ .execute(pool)
+ .await?;
+ }
+
+ Ok(step)
}
/// Batch create multiple directive steps.
@@ -6392,3 +6406,38 @@ pub async fn get_orders_by_step_id(
.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 62f15d9..5c134d6 100644
--- a/makima/src/orchestration/directive.rs
+++ b/makima/src/orchestration/directive.rs
@@ -1359,6 +1359,11 @@ gh pr view {pr_url} --json state --jq '.state'
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 ""
@@ -1373,10 +1378,13 @@ git checkout -b "$NEW_BRANCH" origin/{base_branch}
git push -u origin "$NEW_BRANCH"
```
-3. Create a new PR:
+3. Create a new PR. Generate a concise, descriptive PR title (max 72 characters) based on the steps completed.
+The title should summarize what the changes actually accomplish — do NOT just use the directive name "{title}".
+Focus on the actual work done in the steps listed below.
```
-gh pr create --title "{title}" --body "{pr_body}" --head "$NEW_BRANCH" --base {base_branch}
+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:
```
@@ -1408,6 +1416,7 @@ 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,
@@ -1437,10 +1446,15 @@ git checkout -b {directive_branch} origin/{base_branch}
git push -u origin {directive_branch}
```
-Then create the PR:
+Then create the PR. You MUST generate a concise, descriptive PR title (max 72 characters) based on the steps completed above.
+The title should summarize what the changes actually accomplish — do NOT just use the directive name "{title}".
+For example, instead of "soryu-co/soryu - makima" use something like "Fix order lifecycle, PR update, and contracts overflow".
+Focus on the actual work done in the steps.
+
```
-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.
@@ -1576,7 +1590,7 @@ pub fn build_order_pickup_prompt(
Review them and create steps to fulfil them.\n\n");
for (i, order) in orders.iter().enumerate() {
prompt.push_str(&format!(
- " {}. [{}] [{}] {} (id: {})\n",
+ " {}. [{}] [{}] {} \n orderId: {}\n",
i + 1,
order.priority,
order.order_type,
@@ -1707,13 +1721,15 @@ For each order (or group of related orders), create one or more steps:
- dependsOn: UUIDs of steps this depends on (use IDs from previous add-step responses)
- orderIndex: Execution phase number. Steps only start after ALL steps with a lower orderIndex complete.
Steps with the same orderIndex run in parallel. Use ascending values (0, 1, 2, ...) to create sequential phases.
+- orderId: The UUID of the order this step fulfils. Include this so the order is automatically marked done
+ when the step completes. Use the orderId shown in the order listing above.
Submit steps using generation {generation}:
makima directive add-step "Step Name" --description "..." --task-plan "..." --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}}}]'
+ 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 15df6d5..cb59581 100644
--- a/makima/src/server/handlers/directives.rs
+++ b/makima/src/server/handlers/directives.rs
@@ -1062,6 +1062,19 @@ pub async fn pick_up_orders(
}
};
+ // 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, id).await {
Ok(o) => o,