summaryrefslogtreecommitdiff
path: root/makima/src/db
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-14 21:29:26 +0000
committerGitHub <noreply@github.com>2026-02-14 21:29:26 +0000
commit9aadbc7958d39d181c0dd0600e2b7c30bb6c391a (patch)
treeef8bed9718c39041191b58a284ee31f5d8d32521 /makima/src/db
parentc1e55ce4fec79f9909b957f86bd7fa8b76939746 (diff)
downloadsoryu-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.rs125
-rw-r--r--makima/src/db/repository.rs316
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(&current.title);
+ let description = req.description.as_deref().or(current.description.as_deref());
+ let priority = req.priority.as_deref().unwrap_or(&current.priority);
+ let status = req.status.as_deref().unwrap_or(&current.status);
+ let order_type = req.order_type.as_deref().unwrap_or(&current.order_type);
+ let labels = req.labels.as_ref().unwrap_or(&current.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))
+}
+