diff options
| author | soryu <soryu@soryu.co> | 2026-02-17 00:40:32 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-02-17 00:40:32 +0000 |
| commit | b67b3f8e8d63361d9ff19f87fd608c03bfa3fd43 (patch) | |
| tree | d5429658a90c4be9a2984617118d2c843cadfdf6 | |
| parent | 4bd40f047a6f4703945c6db2811d8feda27241d6 (diff) | |
| download | soryu-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.tsx | 117 | ||||
| -rw-r--r-- | makima/frontend/src/components/orders/OrderList.tsx | 5 | ||||
| -rw-r--r-- | makima/frontend/src/lib/api.ts | 2 | ||||
| -rw-r--r-- | makima/src/db/models.rs | 7 | ||||
| -rw-r--r-- | makima/src/db/repository.rs | 53 | ||||
| -rw-r--r-- | makima/src/orchestration/directive.rs | 28 | ||||
| -rw-r--r-- | makima/src/server/handlers/directives.rs | 13 |
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, |
