diff options
| author | soryu <soryu@soryu.co> | 2026-02-14 21:29:26 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-02-14 21:29:26 +0000 |
| commit | 9aadbc7958d39d181c0dd0600e2b7c30bb6c391a (patch) | |
| tree | ef8bed9718c39041191b58a284ee31f5d8d32521 /makima/src/db | |
| parent | c1e55ce4fec79f9909b957f86bd7fa8b76939746 (diff) | |
| download | soryu-9aadbc7958d39d181c0dd0600e2b7c30bb6c391a.tar.gz soryu-9aadbc7958d39d181c0dd0600e2b7c30bb6c391a.zip | |
Makima system improvements: Orders, directive questions, PR creation fix, bug fixes (#62)
* feat: soryu-co/soryu - makima: Fix directive goal update bug - stale closure issue
* WIP: heartbeat checkpoint
* WIP: heartbeat checkpoint
* feat: soryu-co/soryu - makima: Create Orders database schema and backend API
* feat: soryu-co/soryu - makima: Fix task Claude instance not receiving user inputs from input box
* WIP: heartbeat checkpoint
* feat: soryu-co/soryu - makima: Build Orders frontend page replacing the Board page
* WIP: heartbeat checkpoint
* WIP: heartbeat checkpoint
* feat: soryu-co/soryu - makima: Fix directive PR creation system
Diffstat (limited to 'makima/src/db')
| -rw-r--r-- | makima/src/db/models.rs | 125 | ||||
| -rw-r--r-- | makima/src/db/repository.rs | 316 |
2 files changed, 438 insertions, 3 deletions
diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs index 131dffc..6ec6cf4 100644 --- a/makima/src/db/models.rs +++ b/makima/src/db/models.rs @@ -2714,6 +2714,8 @@ pub struct Directive { pub pr_url: Option<String>, pub pr_branch: Option<String>, pub completion_task_id: Option<Uuid>, + /// Whether questions pause execution indefinitely until answered + pub reconcile_mode: bool, pub goal_updated_at: DateTime<Utc>, pub started_at: Option<DateTime<Utc>>, pub version: i32, @@ -2763,6 +2765,8 @@ pub struct DirectiveSummary { pub orchestrator_task_id: Option<Uuid>, pub pr_url: Option<String>, pub completion_task_id: Option<Uuid>, + /// Whether questions pause execution indefinitely until answered + pub reconcile_mode: bool, pub version: i32, pub created_at: DateTime<Utc>, pub updated_at: DateTime<Utc>, @@ -2789,6 +2793,8 @@ pub struct CreateDirectiveRequest { pub repository_url: Option<String>, pub local_path: Option<String>, pub base_branch: Option<String>, + /// Whether questions pause execution indefinitely until answered + pub reconcile_mode: Option<bool>, } /// Request to update a directive. @@ -2804,6 +2810,8 @@ pub struct UpdateDirectiveRequest { pub orchestrator_task_id: Option<Uuid>, pub pr_url: Option<String>, pub pr_branch: Option<String>, + /// Whether questions pause execution indefinitely until answered + pub reconcile_mode: Option<bool>, pub version: Option<i32>, } @@ -2848,3 +2856,120 @@ pub struct UpdateDirectiveStepRequest { pub order_index: Option<i32>, } +// ============================================================================= +// Order Types +// ============================================================================= + +/// An order — a card-based work item (feature, bug, spike, chore, improvement) +/// similar to GitHub Issues or Linear cards. Orders can be linked to directives +/// or contracts for execution. +#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct Order { + pub id: Uuid, + pub owner_id: Uuid, + pub title: String, + pub description: Option<String>, + /// Priority: critical, high, medium, low, none + pub priority: String, + /// Status: open, in_progress, 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) + pub directive_id: Option<Uuid>, + /// Linked directive step (optional) + pub directive_step_id: Option<Uuid>, + /// Linked contract (optional) + pub contract_id: Option<Uuid>, + /// Repository context + pub repository_url: Option<String>, + pub created_at: DateTime<Utc>, + pub updated_at: DateTime<Utc>, +} + +/// Request to create a new order. +#[derive(Debug, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct CreateOrderRequest { + pub title: String, + pub description: Option<String>, + pub priority: Option<String>, + pub status: Option<String>, + pub order_type: Option<String>, + #[serde(default = "default_empty_labels")] + pub labels: serde_json::Value, + pub directive_id: Option<Uuid>, + pub contract_id: Option<Uuid>, + pub repository_url: Option<String>, +} + +/// Default empty JSON array for labels. +fn default_empty_labels() -> serde_json::Value { + serde_json::json!([]) +} + +/// Request to update an existing order. +#[derive(Debug, Default, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct UpdateOrderRequest { + pub title: Option<String>, + pub description: Option<String>, + pub priority: Option<String>, + pub status: Option<String>, + pub order_type: Option<String>, + pub labels: Option<serde_json::Value>, + pub directive_id: Option<Uuid>, + pub directive_step_id: Option<Uuid>, + pub contract_id: Option<Uuid>, + pub repository_url: Option<String>, +} + +/// Response for order list endpoint. +#[derive(Debug, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct OrderListResponse { + pub orders: Vec<Order>, + pub total: i64, +} + +/// Query parameters for listing orders. +#[derive(Debug, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct OrderListQuery { + /// Filter by status (e.g., "open", "in_progress", "done", "archived") + pub status: Option<String>, + /// Filter by order type (e.g., "feature", "bug", "spike", "chore", "improvement") + #[serde(rename = "type")] + pub order_type: Option<String>, + /// Filter by priority (e.g., "critical", "high", "medium", "low", "none") + pub priority: Option<String>, + /// Filter by linked directive ID + pub directive_id: Option<Uuid>, + /// Filter by linked contract ID + pub contract_id: Option<Uuid>, +} + +/// Request body for linking an order to a directive. +#[derive(Debug, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct LinkDirectiveRequest { + pub directive_id: Uuid, +} + +/// Request body for linking an order to a contract. +#[derive(Debug, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct LinkContractRequest { + pub contract_id: Uuid, +} + +/// Request body for converting an order to a directive step. +#[derive(Debug, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct ConvertToStepRequest { + pub directive_id: Uuid, +} + diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs index d8168f6..ed4a1fa 100644 --- a/makima/src/db/repository.rs +++ b/makima/src/db/repository.rs @@ -14,6 +14,7 @@ use super::models::{ DeliverableDefinition, Directive, DirectiveStep, DirectiveSummary, CreateDirectiveRequest, CreateDirectiveStepRequest, UpdateDirectiveRequest, UpdateDirectiveStepRequest, + CreateOrderRequest, Order, UpdateOrderRequest, File, FileSummary, FileVersion, HistoryEvent, HistoryQueryFilters, MeshChatConversation, MeshChatMessageRecord, PhaseChangeResult, PhaseConfig, PhaseDefinition, SupervisorHeartbeatRecord, SupervisorState, @@ -4929,8 +4930,8 @@ pub async fn create_directive_for_owner( ) -> Result<Directive, sqlx::Error> { sqlx::query_as::<_, Directive>( r#" - INSERT INTO directives (owner_id, title, goal, repository_url, local_path, base_branch) - VALUES ($1, $2, $3, $4, $5, $6) + INSERT INTO directives (owner_id, title, goal, repository_url, local_path, base_branch, reconcile_mode) + VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING * "#, ) @@ -4940,6 +4941,7 @@ pub async fn create_directive_for_owner( .bind(&req.repository_url) .bind(&req.local_path) .bind(&req.base_branch) + .bind(req.reconcile_mode.unwrap_or(false)) .fetch_one(pool) .await } @@ -4992,6 +4994,7 @@ pub async fn list_directives_for_owner( SELECT d.id, d.owner_id, d.title, d.goal, d.status, d.repository_url, d.orchestrator_task_id, d.pr_url, d.completion_task_id, + d.reconcile_mode, d.version, d.created_at, d.updated_at, COALESCE(s.total_steps, 0) as total_steps, COALESCE(s.completed_steps, 0) as completed_steps, @@ -5055,12 +5058,14 @@ pub async fn update_directive_for_owner( let orchestrator_task_id = req.orchestrator_task_id.or(current.orchestrator_task_id); let pr_url = req.pr_url.as_deref().or(current.pr_url.as_deref()); let pr_branch = req.pr_branch.as_deref().or(current.pr_branch.as_deref()); + let reconcile_mode = req.reconcile_mode.unwrap_or(current.reconcile_mode); let result = sqlx::query_as::<_, Directive>( r#" UPDATE directives SET title = $3, goal = $4, status = $5, repository_url = $6, local_path = $7, base_branch = $8, orchestrator_task_id = $9, pr_url = $10, pr_branch = $11, + reconcile_mode = $12, version = version + 1, updated_at = NOW() WHERE id = $1 AND owner_id = $2 RETURNING * @@ -5077,6 +5082,7 @@ pub async fn update_directive_for_owner( .bind(orchestrator_task_id) .bind(pr_url) .bind(pr_branch) + .bind(reconcile_mode) .fetch_optional(pool) .await .map_err(RepositoryError::Database)?; @@ -5188,6 +5194,7 @@ pub struct CompletedStepTask { #[derive(Debug, Clone, sqlx::FromRow)] pub struct DirectiveCompletionCheck { pub directive_id: Uuid, + pub owner_id: Uuid, pub completion_task_id: Uuid, pub task_status: String, pub pr_url: Option<String>, @@ -5224,7 +5231,7 @@ pub async fn get_completion_tasks_to_check( ) -> Result<Vec<DirectiveCompletionCheck>, sqlx::Error> { sqlx::query_as::<_, DirectiveCompletionCheck>( r#" - SELECT d.id as directive_id, d.completion_task_id, t.status as task_status, d.pr_url + SELECT d.id as directive_id, d.owner_id, d.completion_task_id, t.status as task_status, d.pr_url FROM directives d JOIN tasks t ON t.id = d.completion_task_id WHERE d.completion_task_id IS NOT NULL @@ -5917,3 +5924,306 @@ pub async fn get_directive_max_generation( Ok(row.0.unwrap_or(0)) } +// ============================================================================= +// Order CRUD +// ============================================================================= + +/// Create a new order for the given owner. +pub async fn create_order( + pool: &PgPool, + owner_id: Uuid, + req: CreateOrderRequest, +) -> Result<Order, sqlx::Error> { + let priority = req.priority.as_deref().unwrap_or("medium"); + let status = req.status.as_deref().unwrap_or("open"); + let order_type = req.order_type.as_deref().unwrap_or("feature"); + + sqlx::query_as::<_, Order>( + r#" + INSERT INTO orders (owner_id, title, description, priority, status, order_type, labels, directive_id, contract_id, repository_url) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) + RETURNING * + "#, + ) + .bind(owner_id) + .bind(&req.title) + .bind(&req.description) + .bind(priority) + .bind(status) + .bind(order_type) + .bind(&req.labels) + .bind(req.directive_id) + .bind(req.contract_id) + .bind(&req.repository_url) + .fetch_one(pool) + .await +} + +/// List orders for the given owner with optional filters. +pub async fn list_orders( + pool: &PgPool, + owner_id: Uuid, + status_filter: Option<&str>, + type_filter: Option<&str>, + priority_filter: Option<&str>, + directive_id_filter: Option<Uuid>, + contract_id_filter: Option<Uuid>, +) -> Result<Vec<Order>, sqlx::Error> { + // Build dynamic query with optional filters + let mut query = String::from("SELECT * FROM orders WHERE owner_id = $1"); + let mut param_idx = 2u32; + + if status_filter.is_some() { + query.push_str(&format!(" AND status = ${}", param_idx)); + param_idx += 1; + } + if type_filter.is_some() { + query.push_str(&format!(" AND order_type = ${}", param_idx)); + param_idx += 1; + } + if priority_filter.is_some() { + query.push_str(&format!(" AND priority = ${}", param_idx)); + param_idx += 1; + } + if directive_id_filter.is_some() { + query.push_str(&format!(" AND directive_id = ${}", param_idx)); + param_idx += 1; + } + if contract_id_filter.is_some() { + query.push_str(&format!(" AND contract_id = ${}", param_idx)); + let _ = param_idx; // suppress unused warning + } + query.push_str(" ORDER BY created_at DESC"); + + let mut q = sqlx::query_as::<_, Order>(&query).bind(owner_id); + + if let Some(s) = status_filter { + q = q.bind(s); + } + if let Some(t) = type_filter { + q = q.bind(t); + } + if let Some(p) = priority_filter { + q = q.bind(p); + } + if let Some(d) = directive_id_filter { + q = q.bind(d); + } + if let Some(c) = contract_id_filter { + q = q.bind(c); + } + + q.fetch_all(pool).await +} + +/// Get a single order by ID (owner-scoped). +pub async fn get_order( + pool: &PgPool, + owner_id: Uuid, + order_id: Uuid, +) -> Result<Option<Order>, sqlx::Error> { + sqlx::query_as::<_, Order>( + r#"SELECT * FROM orders WHERE id = $1 AND owner_id = $2"#, + ) + .bind(order_id) + .bind(owner_id) + .fetch_optional(pool) + .await +} + +/// Update an order (owner-scoped). Uses COALESCE pattern to only update provided fields. +pub async fn update_order( + pool: &PgPool, + owner_id: Uuid, + order_id: Uuid, + req: UpdateOrderRequest, +) -> Result<Option<Order>, sqlx::Error> { + let current = sqlx::query_as::<_, Order>( + r#"SELECT * FROM orders WHERE id = $1 AND owner_id = $2"#, + ) + .bind(order_id) + .bind(owner_id) + .fetch_optional(pool) + .await?; + + let current = match current { + Some(c) => c, + None => return Ok(None), + }; + + let title = req.title.as_deref().unwrap_or(¤t.title); + let description = req.description.as_deref().or(current.description.as_deref()); + let priority = req.priority.as_deref().unwrap_or(¤t.priority); + let status = req.status.as_deref().unwrap_or(¤t.status); + let order_type = req.order_type.as_deref().unwrap_or(¤t.order_type); + let labels = req.labels.as_ref().unwrap_or(¤t.labels); + let directive_id = req.directive_id.or(current.directive_id); + let directive_step_id = req.directive_step_id.or(current.directive_step_id); + let contract_id = req.contract_id.or(current.contract_id); + let repository_url = req.repository_url.as_deref().or(current.repository_url.as_deref()); + + sqlx::query_as::<_, Order>( + r#" + UPDATE orders + SET title = $3, description = $4, priority = $5, status = $6, + order_type = $7, labels = $8, directive_id = $9, directive_step_id = $10, + contract_id = $11, repository_url = $12, updated_at = NOW() + WHERE id = $1 AND owner_id = $2 + RETURNING * + "#, + ) + .bind(order_id) + .bind(owner_id) + .bind(title) + .bind(description) + .bind(priority) + .bind(status) + .bind(order_type) + .bind(labels) + .bind(directive_id) + .bind(directive_step_id) + .bind(contract_id) + .bind(repository_url) + .fetch_optional(pool) + .await +} + +/// Delete an order (owner-scoped). Returns true if a row was deleted. +pub async fn delete_order( + pool: &PgPool, + owner_id: Uuid, + order_id: Uuid, +) -> Result<bool, sqlx::Error> { + let result = sqlx::query( + r#"DELETE FROM orders WHERE id = $1 AND owner_id = $2"#, + ) + .bind(order_id) + .bind(owner_id) + .execute(pool) + .await?; + + Ok(result.rows_affected() > 0) +} + +/// Link an order to a directive. +pub async fn link_order_to_directive( + pool: &PgPool, + owner_id: Uuid, + order_id: Uuid, + directive_id: Uuid, +) -> Result<Option<Order>, sqlx::Error> { + sqlx::query_as::<_, Order>( + r#" + UPDATE orders + SET directive_id = $3, updated_at = NOW() + WHERE id = $1 AND owner_id = $2 + RETURNING * + "#, + ) + .bind(order_id) + .bind(owner_id) + .bind(directive_id) + .fetch_optional(pool) + .await +} + +/// Link an order to a contract. +pub async fn link_order_to_contract( + pool: &PgPool, + owner_id: Uuid, + order_id: Uuid, + contract_id: Uuid, +) -> Result<Option<Order>, sqlx::Error> { + sqlx::query_as::<_, Order>( + r#" + UPDATE orders + SET contract_id = $3, updated_at = NOW() + WHERE id = $1 AND owner_id = $2 + RETURNING * + "#, + ) + .bind(order_id) + .bind(owner_id) + .bind(contract_id) + .fetch_optional(pool) + .await +} + +/// Convert an order to a directive step. Creates a new DirectiveStep from the order's +/// title and description, links the order to both the directive and the new step, +/// and returns the created step. +pub async fn convert_order_to_step( + pool: &PgPool, + owner_id: Uuid, + order_id: Uuid, + directive_id: Uuid, +) -> Result<Option<DirectiveStep>, sqlx::Error> { + // Verify the order exists and belongs to this owner + let order = sqlx::query_as::<_, Order>( + r#"SELECT * FROM orders WHERE id = $1 AND owner_id = $2"#, + ) + .bind(order_id) + .bind(owner_id) + .fetch_optional(pool) + .await?; + + let order = match order { + Some(o) => o, + None => return Ok(None), + }; + + // Verify the directive exists and belongs to this owner + let directive = sqlx::query_as::<_, Directive>( + r#"SELECT * FROM directives WHERE id = $1 AND owner_id = $2"#, + ) + .bind(directive_id) + .bind(owner_id) + .fetch_optional(pool) + .await?; + + if directive.is_none() { + return Ok(None); + } + + // Get the next order_index for this directive + let max_index: (Option<i32>,) = sqlx::query_as( + r#"SELECT MAX(order_index) FROM directive_steps WHERE directive_id = $1"#, + ) + .bind(directive_id) + .fetch_one(pool) + .await?; + let next_index = max_index.0.unwrap_or(-1) + 1; + + // Create the directive step from order data + let step = sqlx::query_as::<_, DirectiveStep>( + r#" + INSERT INTO directive_steps (directive_id, name, description, order_index) + VALUES ($1, $2, $3, $4) + RETURNING * + "#, + ) + .bind(directive_id) + .bind(&order.title) + .bind(&order.description) + .bind(next_index) + .fetch_one(pool) + .await?; + + // Link the order to the directive and the new step + sqlx::query( + r#" + UPDATE orders + SET directive_id = $3, directive_step_id = $4, updated_at = NOW() + WHERE id = $1 AND owner_id = $2 + "#, + ) + .bind(order_id) + .bind(owner_id) + .bind(directive_id) + .bind(step.id) + .execute(pool) + .await?; + + Ok(Some(step)) +} + |
