From b8d563d45f14a2b1db1f684aa0a8dcd7e5b6ad56 Mon Sep 17 00:00:00 2001 From: soryu Date: Sat, 7 Feb 2026 00:01:50 +0000 Subject: Remove directives for reimplementation --- makima/src/bin/makima.rs | 151 +- makima/src/daemon/api/directive.rs | 447 ------ makima/src/daemon/api/mod.rs | 1 - makima/src/daemon/cli/directive.rs | 186 --- makima/src/daemon/cli/mod.rs | 56 - makima/src/daemon/skills/directive.md | 303 ----- makima/src/daemon/skills/mod.rs | 4 - makima/src/db/models.rs | 666 --------- makima/src/db/repository.rs | 1169 ---------------- makima/src/lib.rs | 1 - makima/src/orchestration/engine.rs | 1335 ------------------ makima/src/orchestration/mod.rs | 26 - makima/src/orchestration/planner.rs | 848 ------------ makima/src/orchestration/verifier.rs | 833 ------------ makima/src/server/handlers/contracts.rs | 85 +- makima/src/server/handlers/directives.rs | 2116 ----------------------------- makima/src/server/handlers/mesh_daemon.rs | 15 +- makima/src/server/handlers/mod.rs | 2 - makima/src/server/mod.rs | 57 +- 19 files changed, 4 insertions(+), 8297 deletions(-) delete mode 100644 makima/src/daemon/api/directive.rs delete mode 100644 makima/src/daemon/cli/directive.rs delete mode 100644 makima/src/daemon/skills/directive.md delete mode 100644 makima/src/orchestration/engine.rs delete mode 100644 makima/src/orchestration/mod.rs delete mode 100644 makima/src/orchestration/planner.rs delete mode 100644 makima/src/orchestration/verifier.rs delete mode 100644 makima/src/server/handlers/directives.rs (limited to 'makima/src') diff --git a/makima/src/bin/makima.rs b/makima/src/bin/makima.rs index d7646c2..ee5895c 100644 --- a/makima/src/bin/makima.rs +++ b/makima/src/bin/makima.rs @@ -7,7 +7,7 @@ use std::sync::Arc; use makima::daemon::api::{ApiClient, CreateContractRequest}; use makima::daemon::cli::{ Cli, CliConfig, Commands, ConfigCommand, ContractCommand, - DirectiveCommand, SupervisorCommand, ViewArgs, + SupervisorCommand, ViewArgs, }; use makima::daemon::tui::{self, Action, App, ListItem, ViewType, TuiWsClient, WsEvent, OutputLine, OutputMessageType, WsConnectionState, RepositorySuggestion}; use makima::daemon::config::{DaemonConfig, RepoEntry}; @@ -31,7 +31,6 @@ async fn main() -> Result<(), Box> { Commands::Contract(cmd) => run_contract(cmd).await, Commands::View(args) => run_view(args).await, Commands::Config(cmd) => run_config(cmd).await, - Commands::Directive(cmd) => run_directive(cmd).await, } } @@ -802,154 +801,6 @@ async fn run_config(cmd: ConfigCommand) -> Result<(), Box Result<(), Box> { - match cmd { - DirectiveCommand::Create(args) => { - let client = ApiClient::new(args.common.api_url, args.common.api_key)?; - let result = client - .create_directive(&args.goal, args.repository.as_deref(), &args.autonomy) - .await?; - println!("{}", serde_json::to_string(&result.0)?); - } - DirectiveCommand::Status(args) => { - let client = ApiClient::new(args.common.api_url, args.common.api_key)?; - let result = client.get_directive(args.directive_id).await?; - println!("{}", serde_json::to_string(&result.0)?); - } - DirectiveCommand::List(args) => { - let client = ApiClient::new(args.common.api_url, args.common.api_key)?; - let result = client - .list_directives(args.status.as_deref(), args.limit) - .await?; - println!("{}", serde_json::to_string(&result.0)?); - } - DirectiveCommand::Steps(args) => { - let client = ApiClient::new(args.common.api_url, args.common.api_key)?; - let result = client.get_directive_chain(args.directive_id).await?; - println!("{}", serde_json::to_string(&result.0)?); - } - DirectiveCommand::Graph(args) => { - let client = ApiClient::new(args.common.api_url, args.common.api_key)?; - let result = client.get_directive_graph(args.directive_id).await?; - - if args.with_status { - // Enhanced ASCII visualization with status - if let Some(nodes) = result.0.get("nodes").and_then(|v| v.as_array()) { - let mut by_depth: std::collections::HashMap> = - std::collections::HashMap::new(); - - for node in nodes { - let name = node.get("name").and_then(|v| v.as_str()).unwrap_or("?"); - let status = node - .get("status") - .and_then(|v| v.as_str()) - .unwrap_or("pending"); - let depth = node.get("depth").and_then(|v| v.as_i64()).unwrap_or(0) as i32; - by_depth.entry(depth).or_default().push((name, status)); - } - - let directive_name = result - .0 - .get("name") - .and_then(|v| v.as_str()) - .unwrap_or("Directive"); - println!("Directive: {}", directive_name); - println!(); - - let max_depth = by_depth.keys().max().copied().unwrap_or(0); - for depth in 0..=max_depth { - if let Some(steps) = by_depth.get(&depth) { - let indent = " ".repeat(depth as usize); - for (name, status) in steps { - let status_icon = match *status { - "passed" | "completed" => "\u{2713}", - "running" | "evaluating" => "\u{21bb}", - "failed" | "blocked" => "\u{2717}", - "rework" => "\u{21ba}", - "skipped" => "\u{2212}", - "ready" => "\u{25b7}", - _ => "\u{25cb}", - }; - println!("{}[{}] {} {}", indent, name, status_icon, status); - } - if depth < max_depth { - println!("{} |", indent); - println!("{} v", indent); - } - } - } - } - } else { - println!("{}", serde_json::to_string_pretty(&result.0)?); - } - } - DirectiveCommand::Events(args) => { - let client = ApiClient::new(args.common.api_url, args.common.api_key)?; - let result = client - .list_directive_events(args.directive_id, args.limit) - .await?; - println!("{}", serde_json::to_string(&result.0)?); - } - DirectiveCommand::Approve(args) => { - let client = ApiClient::new(args.common.api_url, args.common.api_key)?; - let result = client - .approve_directive_request( - args.directive_id, - args.approval_id, - args.response.as_deref(), - ) - .await?; - println!("{}", serde_json::to_string(&result.0)?); - } - DirectiveCommand::Deny(args) => { - let client = ApiClient::new(args.common.api_url, args.common.api_key)?; - let result = client - .deny_directive_request( - args.directive_id, - args.approval_id, - args.reason.as_deref(), - ) - .await?; - println!("{}", serde_json::to_string(&result.0)?); - } - DirectiveCommand::Start(args) => { - let client = ApiClient::new(args.common.api_url, args.common.api_key)?; - eprintln!("Starting directive {}...", args.directive_id); - let result = client.start_directive(args.directive_id).await?; - println!("{}", serde_json::to_string(&result.0)?); - } - DirectiveCommand::Pause(args) => { - let client = ApiClient::new(args.common.api_url, args.common.api_key)?; - eprintln!("Pausing directive {}...", args.directive_id); - let result = client.pause_directive(args.directive_id).await?; - println!("{}", serde_json::to_string(&result.0)?); - } - DirectiveCommand::Resume(args) => { - let client = ApiClient::new(args.common.api_url, args.common.api_key)?; - eprintln!("Resuming directive {}...", args.directive_id); - let result = client.resume_directive(args.directive_id).await?; - println!("{}", serde_json::to_string(&result.0)?); - } - DirectiveCommand::Stop(args) => { - let client = ApiClient::new(args.common.api_url, args.common.api_key)?; - eprintln!("Stopping directive {}...", args.directive_id); - let result = client.stop_directive(args.directive_id).await?; - println!("{}", serde_json::to_string(&result.0)?); - } - DirectiveCommand::Archive(args) => { - let client = ApiClient::new(args.common.api_url, args.common.api_key)?; - eprintln!("Archiving directive {}...", args.directive_id); - let result = client.archive_directive(args.directive_id).await?; - println!("{}", serde_json::to_string(&result.0)?); - } - } - - Ok(()) -} - /// Load contracts from API async fn load_contracts(client: &ApiClient) -> Result, Box> { let result = client.list_contracts().await?; diff --git a/makima/src/daemon/api/directive.rs b/makima/src/daemon/api/directive.rs deleted file mode 100644 index 48762d6..0000000 --- a/makima/src/daemon/api/directive.rs +++ /dev/null @@ -1,447 +0,0 @@ -//! Directive API methods. - -use uuid::Uuid; - -use super::client::{ApiClient, ApiError}; -use super::supervisor::JsonValue; - -impl ApiClient { - /// Create a new directive. - pub async fn create_directive( - &self, - goal: &str, - repository_url: Option<&str>, - autonomy_level: &str, - ) -> Result { - #[derive(serde::Serialize)] - #[serde(rename_all = "camelCase")] - struct CreateRequest<'a> { - goal: &'a str, - repository_url: Option<&'a str>, - autonomy_level: &'a str, - } - let req = CreateRequest { - goal, - repository_url, - autonomy_level, - }; - self.post("/api/v1/directives", &req).await - } - - /// List all directives for the authenticated user. - pub async fn list_directives( - &self, - status: Option<&str>, - limit: i32, - ) -> Result { - let mut params = Vec::new(); - if let Some(s) = status { - params.push(format!("status={}", s)); - } - params.push(format!("limit={}", limit)); - let query_string = format!("?{}", params.join("&")); - self.get(&format!("/api/v1/directives{}", query_string)) - .await - } - - /// Get a directive by ID (includes progress info). - pub async fn get_directive(&self, directive_id: Uuid) -> Result { - self.get(&format!("/api/v1/directives/{}", directive_id)) - .await - } - - /// Archive a directive. - pub async fn archive_directive(&self, directive_id: Uuid) -> Result { - self.delete_with_response(&format!("/api/v1/directives/{}", directive_id)) - .await - } - - /// Start a directive (plans and begins execution). - pub async fn start_directive(&self, directive_id: Uuid) -> Result { - self.post_empty(&format!("/api/v1/directives/{}/start", directive_id)) - .await - } - - /// Pause a directive. - pub async fn pause_directive(&self, directive_id: Uuid) -> Result { - self.post_empty(&format!("/api/v1/directives/{}/pause", directive_id)) - .await - } - - /// Resume a paused directive. - pub async fn resume_directive(&self, directive_id: Uuid) -> Result { - self.post_empty(&format!("/api/v1/directives/{}/resume", directive_id)) - .await - } - - /// Stop a directive. - pub async fn stop_directive(&self, directive_id: Uuid) -> Result { - self.post_empty(&format!("/api/v1/directives/{}/stop", directive_id)) - .await - } - - /// Get the current chain and steps for a directive. - pub async fn get_directive_chain(&self, directive_id: Uuid) -> Result { - self.get(&format!("/api/v1/directives/{}/chain", directive_id)) - .await - } - - /// Get directive DAG structure for visualization. - pub async fn get_directive_graph(&self, directive_id: Uuid) -> Result { - self.get(&format!("/api/v1/directives/{}/chain/graph", directive_id)) - .await - } - - /// List events for a directive. - pub async fn list_directive_events( - &self, - directive_id: Uuid, - limit: i32, - ) -> Result { - self.get(&format!( - "/api/v1/directives/{}/events?limit={}", - directive_id, limit - )) - .await - } - - /// List pending approvals for a directive. - pub async fn list_directive_approvals( - &self, - directive_id: Uuid, - ) -> Result { - self.get(&format!("/api/v1/directives/{}/approvals", directive_id)) - .await - } - - /// Approve an approval request. - pub async fn approve_directive_request( - &self, - directive_id: Uuid, - approval_id: Uuid, - response: Option<&str>, - ) -> Result { - #[derive(serde::Serialize)] - #[serde(rename_all = "camelCase")] - struct ApprovalRequest<'a> { - response: Option<&'a str>, - } - let req = ApprovalRequest { response }; - self.post( - &format!( - "/api/v1/directives/{}/approvals/{}/approve", - directive_id, approval_id - ), - &req, - ) - .await - } - - /// Deny an approval request. - pub async fn deny_directive_request( - &self, - directive_id: Uuid, - approval_id: Uuid, - response: Option<&str>, - ) -> Result { - #[derive(serde::Serialize)] - #[serde(rename_all = "camelCase")] - struct ApprovalRequest<'a> { - response: Option<&'a str>, - } - let req = ApprovalRequest { response }; - self.post( - &format!( - "/api/v1/directives/{}/approvals/{}/deny", - directive_id, approval_id - ), - &req, - ) - .await - } - - // ========================================================================= - // Chain operations - // ========================================================================= - - /// Force chain regeneration (replan). - pub async fn replan_directive_chain( - &self, - directive_id: Uuid, - ) -> Result { - self.post_empty(&format!( - "/api/v1/directives/{}/chain/replan", - directive_id - )) - .await - } - - // ========================================================================= - // Step management - // ========================================================================= - - /// Add a step to a directive's chain. - pub async fn add_directive_step( - &self, - directive_id: Uuid, - name: &str, - description: Option<&str>, - step_type: Option<&str>, - depends_on: Option>, - ) -> Result { - #[derive(serde::Serialize)] - #[serde(rename_all = "camelCase")] - struct AddStepReq<'a> { - name: &'a str, - #[serde(skip_serializing_if = "Option::is_none")] - description: Option<&'a str>, - #[serde(skip_serializing_if = "Option::is_none")] - step_type: Option<&'a str>, - #[serde(skip_serializing_if = "Option::is_none")] - depends_on: Option>, - } - let req = AddStepReq { - name, - description, - step_type, - depends_on, - }; - self.post( - &format!("/api/v1/directives/{}/chain/steps", directive_id), - &req, - ) - .await - } - - /// Get a step by ID. - pub async fn get_directive_step( - &self, - directive_id: Uuid, - step_id: Uuid, - ) -> Result { - self.get(&format!( - "/api/v1/directives/{}/chain/steps/{}", - directive_id, step_id - )) - .await - } - - /// Update a step. - pub async fn update_directive_step( - &self, - directive_id: Uuid, - step_id: Uuid, - update: serde_json::Value, - ) -> Result { - self.put( - &format!( - "/api/v1/directives/{}/chain/steps/{}", - directive_id, step_id - ), - &update, - ) - .await - } - - /// Delete a step. - pub async fn delete_directive_step( - &self, - directive_id: Uuid, - step_id: Uuid, - ) -> Result<(), ApiError> { - self.delete(&format!( - "/api/v1/directives/{}/chain/steps/{}", - directive_id, step_id - )) - .await - } - - /// Skip a step. - pub async fn skip_directive_step( - &self, - directive_id: Uuid, - step_id: Uuid, - ) -> Result { - self.post_empty(&format!( - "/api/v1/directives/{}/chain/steps/{}/skip", - directive_id, step_id - )) - .await - } - - /// Force re-evaluation of a step. - pub async fn evaluate_directive_step( - &self, - directive_id: Uuid, - step_id: Uuid, - ) -> Result { - self.post_empty(&format!( - "/api/v1/directives/{}/chain/steps/{}/evaluate", - directive_id, step_id - )) - .await - } - - /// Trigger manual rework for a step. - pub async fn rework_directive_step( - &self, - directive_id: Uuid, - step_id: Uuid, - instructions: Option<&str>, - ) -> Result { - #[derive(serde::Serialize)] - #[serde(rename_all = "camelCase")] - struct ReworkReq<'a> { - instructions: Option<&'a str>, - } - let req = ReworkReq { instructions }; - self.post( - &format!( - "/api/v1/directives/{}/chain/steps/{}/rework", - directive_id, step_id - ), - &req, - ) - .await - } - - // ========================================================================= - // Evaluations - // ========================================================================= - - /// List evaluations for a directive. - pub async fn list_directive_evaluations( - &self, - directive_id: Uuid, - ) -> Result { - self.get(&format!( - "/api/v1/directives/{}/evaluations", - directive_id - )) - .await - } - - // ========================================================================= - // Verifiers - // ========================================================================= - - /// List verifiers for a directive. - pub async fn list_directive_verifiers( - &self, - directive_id: Uuid, - ) -> Result { - self.get(&format!("/api/v1/directives/{}/verifiers", directive_id)) - .await - } - - /// Add a verifier to a directive. - pub async fn add_directive_verifier( - &self, - directive_id: Uuid, - name: &str, - verifier_type: &str, - command: Option<&str>, - ) -> Result { - #[derive(serde::Serialize)] - #[serde(rename_all = "camelCase")] - struct CreateVerifierReq<'a> { - name: &'a str, - verifier_type: &'a str, - #[serde(skip_serializing_if = "Option::is_none")] - command: Option<&'a str>, - } - let req = CreateVerifierReq { - name, - verifier_type, - command, - }; - self.post( - &format!("/api/v1/directives/{}/verifiers", directive_id), - &req, - ) - .await - } - - /// Update a verifier. - pub async fn update_directive_verifier( - &self, - directive_id: Uuid, - verifier_id: Uuid, - update: serde_json::Value, - ) -> Result { - self.put( - &format!( - "/api/v1/directives/{}/verifiers/{}", - directive_id, verifier_id - ), - &update, - ) - .await - } - - /// Auto-detect verifiers based on repository content. - pub async fn auto_detect_directive_verifiers( - &self, - directive_id: Uuid, - ) -> Result { - self.post_empty(&format!( - "/api/v1/directives/{}/verifiers/auto-detect", - directive_id - )) - .await - } - - // ========================================================================= - // Requirements & Spec - // ========================================================================= - - /// Update directive requirements. - pub async fn update_directive_requirements( - &self, - directive_id: Uuid, - requirements: serde_json::Value, - ) -> Result { - #[derive(serde::Serialize)] - #[serde(rename_all = "camelCase")] - struct UpdateReq { - requirements: serde_json::Value, - } - let req = UpdateReq { requirements }; - self.put( - &format!("/api/v1/directives/{}/requirements", directive_id), - &req, - ) - .await - } - - /// Update directive acceptance criteria. - pub async fn update_directive_criteria( - &self, - directive_id: Uuid, - acceptance_criteria: serde_json::Value, - ) -> Result { - #[derive(serde::Serialize)] - #[serde(rename_all = "camelCase")] - struct UpdateReq { - acceptance_criteria: serde_json::Value, - } - let req = UpdateReq { acceptance_criteria }; - self.put( - &format!("/api/v1/directives/{}/criteria", directive_id), - &req, - ) - .await - } - - /// Generate a specification from the directive's goal. - pub async fn generate_directive_spec( - &self, - directive_id: Uuid, - ) -> Result { - self.post_empty(&format!( - "/api/v1/directives/{}/generate-spec", - directive_id - )) - .await - } -} diff --git a/makima/src/daemon/api/mod.rs b/makima/src/daemon/api/mod.rs index 2d1efbf..49d80e0 100644 --- a/makima/src/daemon/api/mod.rs +++ b/makima/src/daemon/api/mod.rs @@ -2,7 +2,6 @@ pub mod client; pub mod contract; -pub mod directive; pub mod supervisor; pub use client::ApiClient; diff --git a/makima/src/daemon/cli/directive.rs b/makima/src/daemon/cli/directive.rs deleted file mode 100644 index a2bb34b..0000000 --- a/makima/src/daemon/cli/directive.rs +++ /dev/null @@ -1,186 +0,0 @@ -//! Directive CLI commands for autonomous goal-driven orchestration. -//! -//! Directives are top-level goals that the system works toward. Each directive -//! generates a chain of steps that are executed autonomously with configurable -//! guardrails. - -use clap::Args; -use uuid::Uuid; - -/// Common arguments for directive commands requiring API access. -#[derive(Args, Debug, Clone)] -pub struct DirectiveArgs { - /// API URL - #[arg(long, env = "MAKIMA_API_URL", default_value = "https://api.makima.jp", global = true)] - pub api_url: String, - - /// API key for authentication - #[arg(long, env = "MAKIMA_API_KEY", global = true)] - pub api_key: String, -} - -/// Arguments for the `create` command. -#[derive(Args, Debug)] -pub struct CreateArgs { - #[command(flatten)] - pub common: DirectiveArgs, - - /// The goal for the directive - #[arg(short, long)] - pub goal: String, - - /// Repository URL (optional) - #[arg(short, long)] - pub repository: Option, - - /// Autonomy level: full_auto, guardrails, or manual - #[arg(short, long, default_value = "guardrails")] - pub autonomy: String, -} - -/// Arguments for the `status` command. -#[derive(Args, Debug)] -pub struct StatusArgs { - #[command(flatten)] - pub common: DirectiveArgs, - - /// Directive ID - pub directive_id: Uuid, -} - -/// Arguments for the `list` command. -#[derive(Args, Debug)] -pub struct ListArgs { - #[command(flatten)] - pub common: DirectiveArgs, - - /// Filter by status (draft, planning, active, paused, completed, archived, failed) - #[arg(long)] - pub status: Option, - - /// Limit number of results - #[arg(long, default_value = "50")] - pub limit: i32, -} - -/// Arguments for the `steps` command. -#[derive(Args, Debug)] -pub struct StepsArgs { - #[command(flatten)] - pub common: DirectiveArgs, - - /// Directive ID - pub directive_id: Uuid, -} - -/// Arguments for the `events` command. -#[derive(Args, Debug)] -pub struct EventsArgs { - #[command(flatten)] - pub common: DirectiveArgs, - - /// Directive ID - pub directive_id: Uuid, - - /// Limit number of events - #[arg(long, default_value = "50")] - pub limit: i32, -} - -/// Arguments for the `approve` command. -#[derive(Args, Debug)] -pub struct ApproveArgs { - #[command(flatten)] - pub common: DirectiveArgs, - - /// Directive ID - pub directive_id: Uuid, - - /// Approval ID - pub approval_id: Uuid, - - /// Response message (optional) - #[arg(short, long)] - pub response: Option, -} - -/// Arguments for the `deny` command. -#[derive(Args, Debug)] -pub struct DenyArgs { - #[command(flatten)] - pub common: DirectiveArgs, - - /// Directive ID - pub directive_id: Uuid, - - /// Approval ID - pub approval_id: Uuid, - - /// Reason for denial (optional) - #[arg(short, long)] - pub reason: Option, -} - -/// Arguments for the `start` command. -#[derive(Args, Debug)] -pub struct StartArgs { - #[command(flatten)] - pub common: DirectiveArgs, - - /// Directive ID - pub directive_id: Uuid, -} - -/// Arguments for the `pause` command. -#[derive(Args, Debug)] -pub struct PauseArgs { - #[command(flatten)] - pub common: DirectiveArgs, - - /// Directive ID - pub directive_id: Uuid, -} - -/// Arguments for the `resume` command. -#[derive(Args, Debug)] -pub struct ResumeArgs { - #[command(flatten)] - pub common: DirectiveArgs, - - /// Directive ID - pub directive_id: Uuid, -} - -/// Arguments for the `stop` command. -#[derive(Args, Debug)] -pub struct StopArgs { - #[command(flatten)] - pub common: DirectiveArgs, - - /// Directive ID - pub directive_id: Uuid, -} - -/// Arguments for the `archive` command. -#[derive(Args, Debug)] -pub struct ArchiveArgs { - #[command(flatten)] - pub common: DirectiveArgs, - - /// Directive ID - pub directive_id: Uuid, -} - -/// Arguments for the `graph` command (ASCII DAG visualization). -#[derive(Args, Debug)] -pub struct GraphArgs { - #[command(flatten)] - pub common: DirectiveArgs, - - /// Directive ID - pub directive_id: Uuid, - - /// Show step status in nodes - #[arg(long)] - pub with_status: bool, -} diff --git a/makima/src/daemon/cli/mod.rs b/makima/src/daemon/cli/mod.rs index 77eee80..0805edd 100644 --- a/makima/src/daemon/cli/mod.rs +++ b/makima/src/daemon/cli/mod.rs @@ -3,7 +3,6 @@ pub mod config; pub mod contract; pub mod daemon; -pub mod directive; pub mod server; pub mod supervisor; pub mod view; @@ -13,7 +12,6 @@ use clap::{Parser, Subcommand}; pub use config::CliConfig; pub use contract::ContractArgs; pub use daemon::DaemonArgs; -pub use directive::DirectiveArgs; pub use server::ServerArgs; pub use supervisor::SupervisorArgs; pub use view::ViewArgs; @@ -60,14 +58,6 @@ pub enum Commands { /// Saves configuration to ~/.makima/config.toml for use by CLI commands. #[command(subcommand)] Config(ConfigCommand), - - /// Directive commands for autonomous goal-driven orchestration - /// - /// Directives are top-level goals that generate chains of steps executed - /// autonomously with configurable guardrails. Steps spawn contracts with - /// supervisors and are verified with programmatic and LLM evaluation. - #[command(subcommand)] - Directive(DirectiveCommand), } /// Config subcommands for CLI configuration. @@ -206,52 +196,6 @@ pub enum ContractCommand { CreateFile(contract::CreateFileArgs), } -/// Directive subcommands for autonomous goal-driven orchestration. -#[derive(Subcommand, Debug)] -pub enum DirectiveCommand { - /// Create a new directive from a goal - Create(directive::CreateArgs), - - /// Get directive status and progress - Status(directive::StatusArgs), - - /// List all directives - List(directive::ListArgs), - - /// List steps in the directive's chain - Steps(directive::StepsArgs), - - /// Display ASCII DAG visualization - /// - /// Shows the directive's chain structure as an ASCII graph with - /// steps as nodes and dependencies as edges. - Graph(directive::GraphArgs), - - /// Show recent events for a directive - Events(directive::EventsArgs), - - /// Approve a pending approval request - Approve(directive::ApproveArgs), - - /// Deny a pending approval request - Deny(directive::DenyArgs), - - /// Start a directive (generates chain and begins execution) - Start(directive::StartArgs), - - /// Pause a running directive - Pause(directive::PauseArgs), - - /// Resume a paused directive - Resume(directive::ResumeArgs), - - /// Stop a directive - Stop(directive::StopArgs), - - /// Archive a directive - Archive(directive::ArchiveArgs), -} - impl Cli { /// Parse command-line arguments pub fn parse_args() -> Self { diff --git a/makima/src/daemon/skills/directive.md b/makima/src/daemon/skills/directive.md deleted file mode 100644 index 97e8e20..0000000 --- a/makima/src/daemon/skills/directive.md +++ /dev/null @@ -1,303 +0,0 @@ ---- -name: makima-directive -description: Directive orchestration tools for autonomous goal-driven execution. Use when working with directives, chains, steps, verifiers, and approvals. ---- - -# Directive Orchestration Tools - -Directives are top-level goals that drive autonomous execution with configurable guardrails. Each directive generates a chain of steps that spawn contracts with supervisors, verified by programmatic checks and LLM evaluation. - -## Architecture - -``` -Directive (goal + requirements + acceptance criteria) - | - +-- Chain (generated DAG execution plan) - | +-- Step 1 (pending -> ready -> running -> evaluating -> passed) - | | +-- Contract (spawned when step reaches 'ready') - | | +-- Supervisor Task - | +-- Step 2 (depends_on: [Step 1]) - | +-- Step 3 (depends_on: [Step 1], parallel with Step 2) - | - +-- Verifiers (test runner, linter, build, type checker) - +-- Evaluations (programmatic + LLM composite scores) - +-- Events (audit stream) - +-- Approvals (human-in-the-loop gates) -``` - -## Status Flow - -### Directive Status -- `draft` - Created but not started -- `planning` - Generating chain from requirements -- `active` - Executing steps -- `paused` - Temporarily stopped -- `completed` - All steps passed -- `archived` - No longer active -- `failed` - Execution failed - -### Step Status -- `pending` - Waiting for dependencies -- `ready` - Dependencies met, ready to start -- `running` - Contract executing -- `evaluating` - Running verifiers -- `passed` - Evaluation succeeded -- `failed` - Evaluation failed, exceeded retries -- `rework` - Sent back for corrections -- `skipped` - Manually skipped -- `blocked` - Blocked by failed dependency - -## Autonomy Levels - -- `full_auto` - No approval gates, automatic progression -- `guardrails` - Request approval for yellow/red confidence scores -- `manual` - Request approval for all step completions - -## Confidence Scoring - -Each step evaluation produces a composite confidence score: - -1. **Programmatic verifiers** run first (tests, lint, build) - - Weight: 1.0 each - - If any required verifier fails: automatic RED - -2. **LLM evaluation** runs second - - Weight: 2.0 - - Evaluates against acceptance criteria - -3. **Composite score** computed from weighted average - - GREEN: >= configured threshold (default 0.8) - - YELLOW: >= yellow threshold (default 0.5) - - RED: below yellow threshold - -## CLI Commands - -```bash -# Create a new directive -makima directive create --goal "Add OAuth2 authentication" --repository https://github.com/org/repo - -# List directives -makima directive list [--status active] - -# Get directive status with progress -makima directive status - -# Start execution (generates chain and begins) -makima directive start - -# View chain steps -makima directive steps - -# View DAG visualization -makima directive graph --with-status - -# View recent events -makima directive events --limit 20 - -# Approve a pending request -makima directive approve [--response "Looks good"] - -# Deny a pending request -makima directive deny [--reason "Need more testing"] - -# Lifecycle commands -makima directive pause -makima directive resume -makima directive stop -makima directive archive -``` - -## API Endpoints - -### Directive CRUD -``` -POST /api/v1/directives # Create from goal -GET /api/v1/directives # List -GET /api/v1/directives/:id # Get with progress -PUT /api/v1/directives/:id # Update -DELETE /api/v1/directives/:id # Archive -``` - -### Lifecycle -``` -POST /api/v1/directives/:id/start # Plan + execute -POST /api/v1/directives/:id/pause # Pause -POST /api/v1/directives/:id/resume # Resume -POST /api/v1/directives/:id/stop # Stop -``` - -### Chain & Steps -``` -GET /api/v1/directives/:id/chain # Current chain + steps -GET /api/v1/directives/:id/chain/graph # DAG for visualization -POST /api/v1/directives/:id/chain/replan # Force regeneration -POST /api/v1/directives/:id/chain/steps # Add step -PUT /api/v1/directives/:id/chain/steps/:sid # Modify step -DELETE /api/v1/directives/:id/chain/steps/:sid # Remove step -``` - -### Step Operations -``` -GET /api/v1/directives/:id/steps/:sid # Step detail -POST /api/v1/directives/:id/steps/:sid/evaluate # Force re-evaluation -POST /api/v1/directives/:id/steps/:sid/skip # Skip step -POST /api/v1/directives/:id/steps/:sid/rework # Manual rework -``` - -### Monitoring -``` -GET /api/v1/directives/:id/evaluations # List evaluations -GET /api/v1/directives/:id/events # Event log (polling) -GET /api/v1/directives/:id/events/stream # Event stream (SSE) -``` - -### Verifiers -``` -GET /api/v1/directives/:id/verifiers # List verifiers -POST /api/v1/directives/:id/verifiers # Add verifier -PUT /api/v1/directives/:id/verifiers/:vid # Update verifier -POST /api/v1/directives/:id/verifiers/auto-detect # Auto-detect -``` - -### Approvals -``` -GET /api/v1/directives/:id/approvals # Pending approvals -POST /api/v1/directives/:id/approvals/:aid/approve # Approve -POST /api/v1/directives/:id/approvals/:aid/deny # Deny -``` - -## Creating a Directive - -### Request -```json -POST /api/v1/directives -{ - "goal": "Implement user authentication with OAuth2", - "repositoryUrl": "https://github.com/org/repo", - "autonomyLevel": "guardrails", - "confidenceThresholdGreen": 0.8, - "confidenceThresholdYellow": 0.5, - "maxReworkCycles": 3, - "maxTotalCostUsd": 100.0, - "maxWallTimeMinutes": 480 -} -``` - -### Response -```json -{ - "id": "uuid", - "title": "Implement user authentication with OAuth2", - "goal": "Implement user authentication with OAuth2", - "status": "draft", - "autonomyLevel": "guardrails", - "createdAt": "2026-02-05T12:00:00Z" -} -``` - -## Starting a Directive - -When you start a directive: -1. System generates requirements from the goal -2. Chain planner creates a DAG of steps -3. Root steps (no dependencies) transition to `ready` -4. Contracts spawn for ready steps with supervisors -5. Verifiers auto-detect from repository - -## Evaluation Flow - -When a contract completes: - -1. Step transitions to `evaluating` -2. **Programmatic verifiers** run (tests, lint, build) - - Each produces pass/fail + output -3. **LLM evaluation** runs - - Reviews code against acceptance criteria - - Provides feedback and score -4. **Composite score** computed -5. Based on confidence level and autonomy: - - GREEN: Step passes, downstream unblocks - - YELLOW (guardrails): Request approval - - RED: Initiate rework or request approval - -## Rework Flow - -When a step needs rework: - -1. Contract phase reset to editing -2. Supervisor receives rework instructions -3. Rework count incremented -4. If max reworks exceeded: escalate or fail - -## Event Types - -Events are logged for audit and monitoring: - -- `directive_created`, `directive_started`, `directive_paused`, `directive_completed` -- `chain_generated`, `chain_regenerated` -- `step_ready`, `step_started`, `step_evaluating`, `step_passed`, `step_failed` -- `rework_initiated`, `rework_completed` -- `approval_requested`, `approval_granted`, `approval_denied` -- `verifier_run`, `evaluation_completed` -- `circuit_breaker_triggered` - -## Verifier Configuration - -Verifiers can be auto-detected or manually configured: - -```json -POST /api/v1/directives/:id/verifiers -{ - "name": "Test Runner", - "verifierType": "test_runner", - "command": "npm test", - "workingDirectory": ".", - "timeoutSeconds": 300, - "weight": 1.0, - "required": true, - "enabled": true -} -``` - -### Auto-Detection - -The system detects verifiers from: -- `package.json` - npm test, npm run lint, npm run build -- `Cargo.toml` - cargo test, cargo clippy, cargo build -- `pyproject.toml` - pytest, ruff, mypy - -## Circuit Breakers - -Directives have built-in circuit breakers: - -- `maxTotalCostUsd` - Stop if cumulative cost exceeds limit -- `maxWallTimeMinutes` - Stop if elapsed time exceeds limit -- `maxReworkCycles` - Fail step after N rework attempts -- `maxChainRegenerations` - Fail if chain regenerated too many times - -## Example Workflow - -```bash -# 1. Create a directive -makima directive create \ - --goal "Add dark mode to the application" \ - --repository https://github.com/myorg/myapp \ - --autonomy guardrails - -# Returns directive ID: 123e4567-e89b-12d3-a456-426614174000 - -# 2. Start execution -makima directive start 123e4567-e89b-12d3-a456-426614174000 - -# 3. Monitor progress -makima directive status 123e4567-e89b-12d3-a456-426614174000 - -# 4. View the execution graph -makima directive graph 123e4567-e89b-12d3-a456-426614174000 --with-status - -# 5. Watch events -makima directive events 123e4567-e89b-12d3-a456-426614174000 - -# 6. If approval needed, approve or deny -makima directive approve 123e4567-e89b-12d3-a456-426614174000 -``` diff --git a/makima/src/daemon/skills/mod.rs b/makima/src/daemon/skills/mod.rs index c32f550..0b05f3a 100644 --- a/makima/src/daemon/skills/mod.rs +++ b/makima/src/daemon/skills/mod.rs @@ -9,12 +9,8 @@ pub const SUPERVISOR_SKILL: &str = include_str!("supervisor.md"); /// Contract skill content - task-contract interaction commands pub const CONTRACT_SKILL: &str = include_str!("contract.md"); -/// Directive skill content - autonomous goal-driven orchestration -pub const DIRECTIVE_SKILL: &str = include_str!("directive.md"); - /// All skills as (name, content) pairs for installation pub const ALL_SKILLS: &[(&str, &str)] = &[ ("makima-supervisor", SUPERVISOR_SKILL), ("makima-contract", CONTRACT_SKILL), - ("makima-directive", DIRECTIVE_SKILL), ]; diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs index f951751..3b10cb5 100644 --- a/makima/src/db/models.rs +++ b/makima/src/db/models.rs @@ -2595,672 +2595,6 @@ pub struct HeartbeatHistoryQuery { pub offset: Option, } -// ============================================================================= -// Directives (Goal-driven orchestration with chains of steps) -// ============================================================================= - -/// Directive status -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "snake_case")] -pub enum DirectiveStatus { - Draft, - Planning, - Active, - Paused, - Completed, - Archived, - Failed, -} - -impl Default for DirectiveStatus { - fn default() -> Self { - DirectiveStatus::Draft - } -} - -impl std::fmt::Display for DirectiveStatus { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - DirectiveStatus::Draft => write!(f, "draft"), - DirectiveStatus::Planning => write!(f, "planning"), - DirectiveStatus::Active => write!(f, "active"), - DirectiveStatus::Paused => write!(f, "paused"), - DirectiveStatus::Completed => write!(f, "completed"), - DirectiveStatus::Archived => write!(f, "archived"), - DirectiveStatus::Failed => write!(f, "failed"), - } - } -} - -impl std::str::FromStr for DirectiveStatus { - type Err = String; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "draft" => Ok(DirectiveStatus::Draft), - "planning" => Ok(DirectiveStatus::Planning), - "active" => Ok(DirectiveStatus::Active), - "paused" => Ok(DirectiveStatus::Paused), - "completed" => Ok(DirectiveStatus::Completed), - "archived" => Ok(DirectiveStatus::Archived), - "failed" => Ok(DirectiveStatus::Failed), - _ => Err(format!("Invalid directive status: {}", s)), - } - } -} - -/// Directive - the top-level goal-driven orchestration entity -#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct Directive { - pub id: Uuid, - pub owner_id: Uuid, - pub title: String, - pub goal: String, - /// Structured requirements: [{ id, title, description, priority, category }] - #[sqlx(json)] - pub requirements: serde_json::Value, - /// Acceptance criteria: [{ id, requirementIds, description, testable, verificationMethod }] - #[sqlx(json)] - pub acceptance_criteria: serde_json::Value, - /// Constraints: [{ id, type, description, impact }] - #[sqlx(json)] - pub constraints: serde_json::Value, - /// External dependencies: [{ id, name, type, status, requiredBy }] - #[sqlx(json)] - pub external_dependencies: serde_json::Value, - pub status: String, - pub autonomy_level: String, - pub confidence_threshold_green: f64, - pub confidence_threshold_yellow: f64, - pub max_total_cost_usd: Option, - pub max_wall_time_minutes: Option, - pub max_rework_cycles: Option, - pub max_chain_regenerations: Option, - pub repository_url: Option, - pub local_path: Option, - pub base_branch: Option, - pub orchestrator_contract_id: Option, - pub current_chain_id: Option, - pub chain_generation_count: i32, - pub total_cost_usd: f64, - pub started_at: Option>, - pub completed_at: Option>, - pub version: i32, - pub created_at: DateTime, - pub updated_at: DateTime, -} - -impl Directive { - /// Parse status string to DirectiveStatus enum - pub fn status_enum(&self) -> Result { - self.status.parse() - } -} - -/// Directive chain - a generated execution plan (DAG) for a directive -#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct DirectiveChain { - pub id: Uuid, - pub directive_id: Uuid, - pub generation: i32, - pub name: String, - pub description: Option, - pub rationale: Option, - pub planning_model: Option, - pub status: String, - pub total_steps: i32, - pub completed_steps: i32, - pub failed_steps: i32, - pub current_confidence: Option, - pub started_at: Option>, - pub completed_at: Option>, - pub version: i32, - pub created_at: DateTime, - pub updated_at: DateTime, -} - -/// Chain step status -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "snake_case")] -pub enum StepStatus { - Pending, - Ready, - Running, - Evaluating, - Passed, - Failed, - Rework, - Skipped, - Blocked, -} - -impl Default for StepStatus { - fn default() -> Self { - StepStatus::Pending - } -} - -impl std::fmt::Display for StepStatus { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - StepStatus::Pending => write!(f, "pending"), - StepStatus::Ready => write!(f, "ready"), - StepStatus::Running => write!(f, "running"), - StepStatus::Evaluating => write!(f, "evaluating"), - StepStatus::Passed => write!(f, "passed"), - StepStatus::Failed => write!(f, "failed"), - StepStatus::Rework => write!(f, "rework"), - StepStatus::Skipped => write!(f, "skipped"), - StepStatus::Blocked => write!(f, "blocked"), - } - } -} - -impl std::str::FromStr for StepStatus { - type Err = String; - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "pending" => Ok(StepStatus::Pending), - "ready" => Ok(StepStatus::Ready), - "running" => Ok(StepStatus::Running), - "evaluating" => Ok(StepStatus::Evaluating), - "passed" => Ok(StepStatus::Passed), - "failed" => Ok(StepStatus::Failed), - "rework" => Ok(StepStatus::Rework), - "skipped" => Ok(StepStatus::Skipped), - "blocked" => Ok(StepStatus::Blocked), - _ => Err(format!("Invalid step status: {}", s)), - } - } -} - -/// Chain step - a node in the DAG execution plan -#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct ChainStep { - pub id: Uuid, - pub chain_id: Uuid, - pub name: String, - pub description: Option, - pub step_type: String, - pub contract_type: String, - pub initial_phase: Option, - pub task_plan: Option, - #[sqlx(default)] - pub phases: Vec, - #[sqlx(default)] - pub depends_on: Vec, - pub parallel_group: Option, - #[sqlx(default)] - pub requirement_ids: Vec, - #[sqlx(default)] - pub acceptance_criteria_ids: Vec, - #[sqlx(json)] - #[serde(default)] - pub verifier_config: serde_json::Value, - pub status: String, - pub contract_id: Option, - pub supervisor_task_id: Option, - pub confidence_score: Option, - pub confidence_level: Option, - pub evaluation_count: i32, - pub rework_count: i32, - pub last_evaluation_id: Option, - pub editor_x: Option, - pub editor_y: Option, - pub order_index: i32, - pub started_at: Option>, - pub completed_at: Option>, - pub created_at: DateTime, -} - -impl ChainStep { - /// Parse status string to StepStatus enum - pub fn status_enum(&self) -> Result { - self.status.parse() - } -} - -/// Confidence level (traffic light) -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "snake_case")] -pub enum ConfidenceLevel { - Green, - Yellow, - Red, -} - -impl ConfidenceLevel { - pub fn from_score(score: f64, green_threshold: f64, yellow_threshold: f64) -> Self { - if score >= green_threshold { - Self::Green - } else if score >= yellow_threshold { - Self::Yellow - } else { - Self::Red - } - } -} - -impl std::fmt::Display for ConfidenceLevel { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ConfidenceLevel::Green => write!(f, "green"), - ConfidenceLevel::Yellow => write!(f, "yellow"), - ConfidenceLevel::Red => write!(f, "red"), - } - } -} - -/// Directive evaluation - composite programmatic + LLM evaluation result -#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct DirectiveEvaluation { - pub id: Uuid, - pub directive_id: Uuid, - pub chain_id: Option, - pub step_id: Option, - pub contract_id: Option, - pub evaluation_type: String, - pub evaluation_number: i32, - pub evaluator: Option, - pub passed: bool, - pub overall_score: Option, - pub confidence_level: Option, - #[sqlx(json)] - #[serde(default)] - pub programmatic_results: serde_json::Value, - #[sqlx(json)] - #[serde(default)] - pub llm_results: serde_json::Value, - #[sqlx(json)] - #[serde(default)] - pub criteria_results: serde_json::Value, - pub summary_feedback: String, - pub rework_instructions: Option, - #[sqlx(json)] - pub directive_snapshot: Option, - #[sqlx(json)] - pub deliverables_snapshot: Option, - pub started_at: DateTime, - pub completed_at: Option>, - pub created_at: DateTime, -} - -/// Directive event - audit stream entry -#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct DirectiveEvent { - pub id: Uuid, - pub directive_id: Uuid, - pub chain_id: Option, - pub step_id: Option, - pub event_type: String, - pub severity: String, - #[sqlx(json)] - pub event_data: Option, - pub actor_type: String, - pub actor_id: Option, - pub created_at: DateTime, -} - -/// Directive verifier - pluggable verification configuration -#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct DirectiveVerifier { - pub id: Uuid, - pub directive_id: Uuid, - pub name: String, - pub verifier_type: String, - pub command: Option, - pub working_directory: Option, - pub timeout_seconds: Option, - #[sqlx(json)] - #[serde(default)] - pub environment: serde_json::Value, - pub auto_detect: bool, - #[sqlx(default)] - pub detect_files: Vec, - pub weight: f64, - pub required: bool, - pub enabled: bool, - pub last_run_at: Option>, - #[sqlx(json)] - pub last_result: Option, - pub created_at: DateTime, - pub updated_at: DateTime, -} - -/// Directive approval - human-in-the-loop gate -#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct DirectiveApproval { - pub id: Uuid, - pub directive_id: Uuid, - pub step_id: Option, - pub approval_type: String, - pub description: String, - #[sqlx(json)] - pub context: Option, - pub urgency: String, - pub status: String, - pub response: Option, - pub responded_by: Option, - pub responded_at: Option>, - pub expires_at: Option>, - pub created_at: DateTime, -} - -// ============================================================================= -// Directive Request/Response Types -// ============================================================================= - -/// Request to create a directive from a goal -#[derive(Debug, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct CreateDirectiveRequest { - pub goal: String, - pub title: Option, - pub repository_url: Option, - pub local_path: Option, - pub base_branch: Option, - pub autonomy_level: Option, - pub requirements: Option, - pub acceptance_criteria: Option, - pub confidence_threshold_green: Option, - pub confidence_threshold_yellow: Option, - pub max_total_cost_usd: Option, - pub max_wall_time_minutes: Option, -} - -/// Request to update a directive -#[derive(Debug, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct UpdateDirectiveRequest { - pub title: Option, - pub goal: Option, - pub requirements: Option, - pub acceptance_criteria: Option, - pub constraints: Option, - pub external_dependencies: Option, - pub autonomy_level: Option, - pub confidence_threshold_green: Option, - pub confidence_threshold_yellow: Option, - pub max_total_cost_usd: Option, - pub max_wall_time_minutes: Option, - pub max_rework_cycles: Option, - pub max_chain_regenerations: Option, - pub version: i32, -} - -/// Directive summary for list views -#[derive(Debug, Clone, Serialize, FromRow, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct DirectiveSummary { - pub id: Uuid, - pub title: String, - pub goal: String, - pub status: String, - pub autonomy_level: String, - pub current_confidence: Option, - pub completed_steps: i32, - pub total_steps: i32, - pub chain_generation_count: i32, - pub started_at: Option>, - pub created_at: DateTime, -} - -/// Directive with progress, chain, events, and approvals -#[derive(Debug, Serialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct DirectiveWithProgress { - #[serde(flatten)] - pub directive: Directive, - pub chain: Option, - pub steps: Vec, - pub recent_events: Vec, - pub pending_approvals: Vec, -} - -/// Request to add a step to a chain -#[derive(Debug, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct AddStepRequest { - pub name: String, - pub description: Option, - pub step_type: Option, - pub contract_type: Option, - pub initial_phase: Option, - pub task_plan: Option, - pub phases: Option>, - pub depends_on: Option>, - pub parallel_group: Option, - pub requirement_ids: Option>, - pub acceptance_criteria_ids: Option>, - pub verifier_config: Option, - pub editor_x: Option, - pub editor_y: Option, -} - -/// Request to update a step -#[derive(Debug, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct UpdateStepRequest { - pub name: Option, - pub description: Option, - pub task_plan: Option, - pub depends_on: Option>, - pub requirement_ids: Option>, - pub acceptance_criteria_ids: Option>, - pub verifier_config: Option, - pub editor_x: Option, - pub editor_y: Option, -} - -/// Chain graph response for DAG visualization -#[derive(Debug, Serialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct DirectiveChainGraphResponse { - pub chain_id: Uuid, - pub directive_id: Uuid, - pub nodes: Vec, - pub edges: Vec, -} - -/// Node in directive chain graph -#[derive(Debug, Serialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct DirectiveChainGraphNode { - pub id: Uuid, - pub name: String, - pub step_type: String, - pub status: String, - pub confidence_score: Option, - pub confidence_level: Option, - pub contract_id: Option, - pub editor_x: Option, - pub editor_y: Option, -} - -/// Edge in directive chain graph -#[derive(Debug, Serialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct DirectiveChainGraphEdge { - pub source: Uuid, - pub target: Uuid, -} - -/// Start directive response -#[derive(Debug, Serialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct StartDirectiveResponse { - pub directive_id: Uuid, - pub chain_id: Uuid, - pub chain_generation: i32, - pub steps: Vec, - pub status: String, -} - -/// Request to create a verifier -#[derive(Debug, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct CreateVerifierRequest { - pub name: String, - pub verifier_type: String, - pub command: Option, - pub working_directory: Option, - pub timeout_seconds: Option, - pub environment: Option, - pub weight: Option, - pub required: Option, -} - -/// Request to update a verifier -#[derive(Debug, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct UpdateVerifierRequest { - pub name: Option, - pub command: Option, - pub working_directory: Option, - pub timeout_seconds: Option, - pub weight: Option, - pub required: Option, - pub enabled: Option, -} - -/// Approval action request -#[derive(Debug, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct ApprovalActionRequest { - pub response: Option, -} - -/// Request to update directive requirements -#[derive(Debug, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct UpdateRequirementsRequest { - pub requirements: Vec, -} - -/// Request to update directive acceptance criteria -#[derive(Debug, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct UpdateCriteriaRequest { - pub acceptance_criteria: Vec, -} - -/// Request to trigger step rework -#[derive(Debug, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct ReworkStepRequest { - pub instructions: Option, -} - -/// Directive requirement (shared type used in directive specification) -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct DirectiveRequirement { - pub id: String, - pub title: String, - pub description: String, - pub priority: String, - pub category: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub parent_id: Option, -} - -/// Directive acceptance criterion -#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct DirectiveAcceptanceCriterion { - pub id: String, - #[serde(default)] - pub requirement_ids: Vec, - pub description: String, - #[serde(default = "default_true")] - pub testable: bool, - pub verification_method: Option, -} - -fn default_true() -> bool { - true -} - -// Old chain types (Chain, ChainContract, ChainContractDefinition, ChainDirective, -// ContractEvaluation, ChainEvent, ChainRepository, etc.) have been replaced by -// the directive system above: Directive, DirectiveChain, ChainStep, -// DirectiveEvaluation, DirectiveEvent, DirectiveVerifier, DirectiveApproval. - -// Legacy types kept temporarily for chain runner/parser compatibility during migration. -// These will be removed once the chain daemon module is replaced. - -/// Request payload for creating a new chain (legacy - used by chain runner) -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CreateChainRequest { - pub name: String, - pub description: Option, - pub repository_url: Option, - pub repositories: Option>, - pub loop_enabled: Option, - pub loop_max_iterations: Option, - pub loop_progress_check: Option, - pub contracts: Option>, -} - -/// Request to add a repository to a chain (legacy - used by chain runner) -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AddChainRepositoryRequest { - pub name: String, - pub repository_url: Option, - pub local_path: Option, - #[serde(default = "default_source_type")] - pub source_type: String, - #[serde(default)] - pub is_primary: bool, -} - -fn default_source_type() -> String { - "remote".to_string() -} - -/// Request to create a contract within a chain (legacy - used by chain runner) -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CreateChainContractRequest { - pub name: String, - pub description: Option, - #[serde(default)] - pub contract_type: Option, - pub initial_phase: Option, - pub phases: Option>, - pub depends_on: Option>, - pub tasks: Option>, - pub deliverables: Option>, - pub editor_x: Option, - pub editor_y: Option, -} - -/// Task definition within a chain contract (legacy - used by chain runner) -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CreateChainTaskRequest { - pub name: String, - pub plan: String, -} - -/// Deliverable definition within a chain contract (legacy - used by chain runner) -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CreateChainDeliverableRequest { - pub id: String, - pub name: String, - pub priority: Option, -} - // ============================================================================= // Unit Tests // ============================================================================= diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs index cd806f0..863d927 100644 --- a/makima/src/db/repository.rs +++ b/makima/src/db/repository.rs @@ -6,7 +6,6 @@ use sqlx::PgPool; use uuid::Uuid; use super::models::{ - // Core types CheckpointPatch, CheckpointPatchInfo, Contract, ContractChatConversation, ContractChatMessageRecord, ContractEvent, ContractRepository, ContractSummary, ContractTypeTemplateRecord, ConversationMessage, ConversationSnapshot, @@ -17,11 +16,6 @@ use super::models::{ PhaseDefinition, SupervisorHeartbeatRecord, SupervisorState, Task, TaskCheckpoint, TaskEvent, TaskSummary, UpdateContractRequest, UpdateFileRequest, UpdateTaskRequest, UpdateTemplateRequest, - // Directive types - AddStepRequest, ChainStep, CreateDirectiveRequest, Directive, DirectiveApproval, - DirectiveChain, DirectiveChainGraphEdge, DirectiveChainGraphNode, DirectiveChainGraphResponse, - DirectiveEvaluation, DirectiveEvent, DirectiveSummary, DirectiveVerifier, - DirectiveWithProgress, UpdateDirectiveRequest, UpdateStepRequest, }; /// Repository error types. @@ -4904,1169 +4898,6 @@ pub async fn sync_supervisor_state( .await } -// ============================================================================= -// Directive Operations (top-level orchestration entity) -// ============================================================================= -// TODO: Implement directive CRUD functions -// - create_directive_for_owner -// - get_directive_for_owner -// - list_directives_for_owner -// - update_directive_for_owner -// - archive_directive_for_owner -// - update_directive_status - -// ============================================================================= -// Directive Chain Operations (generated execution plans) -// ============================================================================= -// TODO: Implement chain CRUD functions -// - create_directive_chain -// - get_current_chain -// - supersede_chain - -// ============================================================================= -// Chain Step Operations (nodes in the DAG) -// ============================================================================= -// TODO: Implement step CRUD functions -// - create_chain_step -// - update_chain_step -// - delete_chain_step -// - find_ready_steps -// - update_step_status -// - update_step_contract -// - update_step_confidence -// - increment_step_rework_count - -// ============================================================================= -// Directive Evaluation Operations -// ============================================================================= -// TODO: Implement evaluation functions -// - create_directive_evaluation -// - list_step_evaluations -// - list_directive_evaluations - -// ============================================================================= -// Directive Event Operations (audit stream) -// ============================================================================= -// TODO: Implement event functions -// - emit_directive_event -// - list_directive_events - -// ============================================================================= -// Directive Verifier Operations -// ============================================================================= -// TODO: Implement verifier CRUD functions -// - create_directive_verifier -// - list_directive_verifiers -// - update_directive_verifier - -// ============================================================================= -// Directive Approval Operations (human-in-the-loop) -// ============================================================================= -// TODO: Implement approval functions -// - create_approval_request -// - resolve_approval -// - list_pending_approvals - -// NOTE: Old chain functions removed. See git history for reference. -// Old functions included: create_chain_for_owner, get_chain_for_owner, -// list_chains_for_owner, update_chain_for_owner, delete_chain_for_owner, -// add_contract_to_chain, remove_contract_from_chain, list_chain_contracts, -// get_chain_with_contracts, list_chain_repositories, add_chain_repository, -// delete_chain_repository, set_chain_repository_primary, get_chain_graph, -// record_chain_event, list_chain_events, increment_chain_loop, complete_chain, -// get_ready_chain_contracts, is_chain_complete, get_chain_editor_data, -// create_chain_contract_definition, list_chain_contract_definitions, -// update_chain_contract_definition, delete_chain_contract_definition, -// get_chain_definition_graph, update_chain_status, progress_chain, -// create_chain_directive, get_chain_directive, update_chain_directive, -// delete_chain_directive, create_contract_evaluation, get_contract_evaluation, -// list_chain_evaluations, update_chain_contract_evaluation_status, -// mark_chain_contract_original_completion, get_chain_contract_by_contract_id, -// init_chain_for_owner. - -// ============================================================================= -// Directive Operations -// ============================================================================= - -/// Create a new directive for an owner. -pub async fn create_directive_for_owner( - pool: &PgPool, - owner_id: Uuid, - req: CreateDirectiveRequest, -) -> Result { - let title = req.title.unwrap_or_else(|| truncate_string(&req.goal, 100)); - let autonomy_level = req.autonomy_level.unwrap_or_else(|| "guardrails".to_string()); - let green_threshold = req.confidence_threshold_green.unwrap_or(0.85); - let yellow_threshold = req.confidence_threshold_yellow.unwrap_or(0.60); - let requirements = req.requirements.unwrap_or(serde_json::json!([])); - let acceptance_criteria = req.acceptance_criteria.unwrap_or(serde_json::json!([])); - - sqlx::query_as::<_, Directive>( - r#" - INSERT INTO directives ( - owner_id, title, goal, requirements, acceptance_criteria, - autonomy_level, confidence_threshold_green, confidence_threshold_yellow, - repository_url, local_path, base_branch, - max_total_cost_usd, max_wall_time_minutes - ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) - RETURNING * - "#, - ) - .bind(owner_id) - .bind(&title) - .bind(&req.goal) - .bind(&requirements) - .bind(&acceptance_criteria) - .bind(&autonomy_level) - .bind(green_threshold) - .bind(yellow_threshold) - .bind(&req.repository_url) - .bind(&req.local_path) - .bind(&req.base_branch) - .bind(req.max_total_cost_usd) - .bind(req.max_wall_time_minutes) - .fetch_one(pool) - .await -} - -/// Get a directive by ID, scoped to owner. -pub async fn get_directive_for_owner( - pool: &PgPool, - id: Uuid, - owner_id: Uuid, -) -> Result, sqlx::Error> { - sqlx::query_as::<_, Directive>( - r#" - SELECT * FROM directives WHERE id = $1 AND owner_id = $2 - "#, - ) - .bind(id) - .bind(owner_id) - .fetch_optional(pool) - .await -} - -/// Get a directive by ID (no owner check - for internal use). -pub async fn get_directive(pool: &PgPool, id: Uuid) -> Result, sqlx::Error> { - sqlx::query_as::<_, Directive>( - r#"SELECT * FROM directives WHERE id = $1"#, - ) - .bind(id) - .fetch_optional(pool) - .await -} - -/// List directives for an owner. -pub async fn list_directives_for_owner( - pool: &PgPool, - owner_id: Uuid, - status_filter: Option<&str>, -) -> Result, sqlx::Error> { - let query = if let Some(status) = status_filter { - sqlx::query_as::<_, DirectiveSummary>( - r#" - SELECT - d.id, d.title, d.goal, d.status, d.autonomy_level, - dc.current_confidence, - COALESCE(dc.completed_steps, 0) as completed_steps, - COALESCE(dc.total_steps, 0) as total_steps, - d.chain_generation_count, d.started_at, d.created_at - FROM directives d - LEFT JOIN directive_chains dc ON dc.id = d.current_chain_id - WHERE d.owner_id = $1 AND d.status = $2 - ORDER BY d.created_at DESC - "#, - ) - .bind(owner_id) - .bind(status) - } else { - sqlx::query_as::<_, DirectiveSummary>( - r#" - SELECT - d.id, d.title, d.goal, d.status, d.autonomy_level, - dc.current_confidence, - COALESCE(dc.completed_steps, 0) as completed_steps, - COALESCE(dc.total_steps, 0) as total_steps, - d.chain_generation_count, d.started_at, d.created_at - FROM directives d - LEFT JOIN directive_chains dc ON dc.id = d.current_chain_id - WHERE d.owner_id = $1 - ORDER BY d.created_at DESC - "#, - ) - .bind(owner_id) - }; - query.fetch_all(pool).await -} - -/// Update a directive with optimistic locking. -pub async fn update_directive_for_owner( - pool: &PgPool, - id: Uuid, - owner_id: Uuid, - req: UpdateDirectiveRequest, -) -> Result { - // First get current version - let current = sqlx::query_scalar::<_, i32>( - "SELECT version FROM directives WHERE id = $1 AND owner_id = $2" - ) - .bind(id) - .bind(owner_id) - .fetch_optional(pool) - .await? - .ok_or_else(|| RepositoryError::Database(sqlx::Error::RowNotFound))?; - - if current != req.version { - return Err(RepositoryError::VersionConflict { - expected: req.version, - actual: current, - }); - } - - let directive = sqlx::query_as::<_, Directive>( - r#" - UPDATE directives SET - title = COALESCE($3, title), - goal = COALESCE($4, goal), - requirements = COALESCE($5, requirements), - acceptance_criteria = COALESCE($6, acceptance_criteria), - constraints = COALESCE($7, constraints), - external_dependencies = COALESCE($8, external_dependencies), - autonomy_level = COALESCE($9, autonomy_level), - confidence_threshold_green = COALESCE($10, confidence_threshold_green), - confidence_threshold_yellow = COALESCE($11, confidence_threshold_yellow), - max_total_cost_usd = COALESCE($12, max_total_cost_usd), - max_wall_time_minutes = COALESCE($13, max_wall_time_minutes), - max_rework_cycles = COALESCE($14, max_rework_cycles), - max_chain_regenerations = COALESCE($15, max_chain_regenerations), - version = version + 1, - updated_at = NOW() - WHERE id = $1 AND owner_id = $2 AND version = $16 - RETURNING * - "#, - ) - .bind(id) - .bind(owner_id) - .bind(&req.title) - .bind(&req.goal) - .bind(&req.requirements) - .bind(&req.acceptance_criteria) - .bind(&req.constraints) - .bind(&req.external_dependencies) - .bind(&req.autonomy_level) - .bind(req.confidence_threshold_green) - .bind(req.confidence_threshold_yellow) - .bind(req.max_total_cost_usd) - .bind(req.max_wall_time_minutes) - .bind(req.max_rework_cycles) - .bind(req.max_chain_regenerations) - .bind(req.version) - .fetch_one(pool) - .await?; - - Ok(directive) -} - -/// Update directive status. -pub async fn update_directive_status( - pool: &PgPool, - id: Uuid, - status: &str, -) -> Result { - sqlx::query_as::<_, Directive>( - r#" - UPDATE directives SET - status = $2, - started_at = CASE WHEN $2 = 'active' AND started_at IS NULL THEN NOW() ELSE started_at END, - completed_at = CASE WHEN $2 IN ('completed', 'failed', 'archived') THEN NOW() ELSE completed_at END, - updated_at = NOW() - WHERE id = $1 - RETURNING * - "#, - ) - .bind(id) - .bind(status) - .fetch_one(pool) - .await -} - -/// Set the orchestrator contract ID for a directive. -pub async fn set_directive_orchestrator_contract( - pool: &PgPool, - directive_id: Uuid, - contract_id: Uuid, -) -> Result<(), sqlx::Error> { - sqlx::query( - r#" - UPDATE directives SET orchestrator_contract_id = $2, updated_at = NOW() - WHERE id = $1 - "#, - ) - .bind(directive_id) - .bind(contract_id) - .execute(pool) - .await?; - Ok(()) -} - -/// Find a directive by its orchestrator contract ID. -pub async fn get_directive_by_orchestrator_contract_id( - pool: &PgPool, - contract_id: Uuid, -) -> Result, sqlx::Error> { - sqlx::query_as::<_, Directive>( - r#" - SELECT * FROM directives WHERE orchestrator_contract_id = $1 - "#, - ) - .bind(contract_id) - .fetch_optional(pool) - .await -} - -/// Archive a directive (soft delete). -pub async fn archive_directive_for_owner( - pool: &PgPool, - id: Uuid, - owner_id: Uuid, -) -> Result { - let result = sqlx::query( - r#" - UPDATE directives SET status = 'archived', updated_at = NOW() - WHERE id = $1 AND owner_id = $2 - "#, - ) - .bind(id) - .bind(owner_id) - .execute(pool) - .await?; - Ok(result.rows_affected() > 0) -} - -/// Get directive with full progress info. -pub async fn get_directive_with_progress( - pool: &PgPool, - id: Uuid, - owner_id: Uuid, -) -> Result, sqlx::Error> { - let directive = match get_directive_for_owner(pool, id, owner_id).await? { - Some(d) => d, - None => return Ok(None), - }; - - let chain = if let Some(chain_id) = directive.current_chain_id { - get_directive_chain(pool, chain_id).await? - } else { - None - }; - - let steps = if let Some(ref c) = chain { - list_chain_steps(pool, c.id).await? - } else { - vec![] - }; - - let recent_events = list_directive_events(pool, id, Some(20)).await?; - let pending_approvals = list_pending_approvals(pool, id).await?; - - Ok(Some(DirectiveWithProgress { - directive, - chain, - steps, - recent_events, - pending_approvals, - })) -} - -// ============================================================================= -// Directive Chain Operations -// ============================================================================= - -/// Create a new chain generation for a directive. -pub async fn create_directive_chain( - pool: &PgPool, - directive_id: Uuid, - name: &str, - description: Option<&str>, - rationale: Option<&str>, - planning_model: Option<&str>, -) -> Result { - // Get next generation number - let generation = sqlx::query_scalar::<_, i32>( - "SELECT COALESCE(MAX(generation), 0) + 1 FROM directive_chains WHERE directive_id = $1" - ) - .bind(directive_id) - .fetch_one(pool) - .await?; - - let chain = sqlx::query_as::<_, DirectiveChain>( - r#" - INSERT INTO directive_chains (directive_id, generation, name, description, rationale, planning_model) - VALUES ($1, $2, $3, $4, $5, $6) - RETURNING * - "#, - ) - .bind(directive_id) - .bind(generation) - .bind(name) - .bind(description) - .bind(rationale) - .bind(planning_model) - .fetch_one(pool) - .await?; - - // Update directive to point to new chain and increment generation count - sqlx::query( - r#" - UPDATE directives SET - current_chain_id = $2, - chain_generation_count = chain_generation_count + 1, - updated_at = NOW() - WHERE id = $1 - "#, - ) - .bind(directive_id) - .bind(chain.id) - .execute(pool) - .await?; - - Ok(chain) -} - -/// Get a directive chain by ID. -pub async fn get_directive_chain(pool: &PgPool, id: Uuid) -> Result, sqlx::Error> { - sqlx::query_as::<_, DirectiveChain>( - "SELECT * FROM directive_chains WHERE id = $1" - ) - .bind(id) - .fetch_optional(pool) - .await -} - -/// Get the current chain for a directive. -pub async fn get_current_chain(pool: &PgPool, directive_id: Uuid) -> Result, sqlx::Error> { - sqlx::query_as::<_, DirectiveChain>( - r#" - SELECT dc.* FROM directive_chains dc - JOIN directives d ON d.current_chain_id = dc.id - WHERE d.id = $1 - "#, - ) - .bind(directive_id) - .fetch_optional(pool) - .await -} - -/// Update chain status. -pub async fn update_chain_status( - pool: &PgPool, - chain_id: Uuid, - status: &str, -) -> Result { - sqlx::query_as::<_, DirectiveChain>( - r#" - UPDATE directive_chains SET - status = $2, - started_at = CASE WHEN $2 = 'active' AND started_at IS NULL THEN NOW() ELSE started_at END, - completed_at = CASE WHEN $2 IN ('completed', 'failed', 'superseded') THEN NOW() ELSE completed_at END, - updated_at = NOW() - WHERE id = $1 - RETURNING * - "#, - ) - .bind(chain_id) - .bind(status) - .fetch_one(pool) - .await -} - -/// Supersede a chain (mark as superseded and update directive). -pub async fn supersede_chain(pool: &PgPool, chain_id: Uuid) -> Result<(), sqlx::Error> { - sqlx::query( - r#" - UPDATE directive_chains SET status = 'superseded', completed_at = NOW(), updated_at = NOW() - WHERE id = $1 - "#, - ) - .bind(chain_id) - .execute(pool) - .await?; - Ok(()) -} - -// ============================================================================= -// Chain Step Operations -// ============================================================================= - -/// Create a new step in a chain. -pub async fn create_chain_step( - pool: &PgPool, - chain_id: Uuid, - req: AddStepRequest, -) -> Result { - let step_type = req.step_type.unwrap_or_else(|| "execute".to_string()); - let contract_type = req.contract_type.unwrap_or_else(|| "simple".to_string()); - let phases = req.phases.unwrap_or_default(); - let depends_on = req.depends_on.unwrap_or_default(); - let requirement_ids = req.requirement_ids.unwrap_or_default(); - let acceptance_criteria_ids = req.acceptance_criteria_ids.unwrap_or_default(); - let verifier_config = req.verifier_config.unwrap_or(serde_json::json!({})); - - // Get next order index - let order_index = sqlx::query_scalar::<_, i32>( - "SELECT COALESCE(MAX(order_index), 0) + 1 FROM chain_steps WHERE chain_id = $1" - ) - .bind(chain_id) - .fetch_one(pool) - .await?; - - let step = sqlx::query_as::<_, ChainStep>( - r#" - INSERT INTO chain_steps ( - chain_id, name, description, step_type, contract_type, - initial_phase, task_plan, phases, depends_on, parallel_group, - requirement_ids, acceptance_criteria_ids, verifier_config, - editor_x, editor_y, order_index - ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) - RETURNING * - "#, - ) - .bind(chain_id) - .bind(&req.name) - .bind(&req.description) - .bind(&step_type) - .bind(&contract_type) - .bind(&req.initial_phase) - .bind(&req.task_plan) - .bind(&phases) - .bind(&depends_on) - .bind(&req.parallel_group) - .bind(&requirement_ids) - .bind(&acceptance_criteria_ids) - .bind(&verifier_config) - .bind(req.editor_x.unwrap_or(0.0)) - .bind(req.editor_y.unwrap_or(0.0)) - .bind(order_index) - .fetch_one(pool) - .await?; - - // Update chain total_steps count - sqlx::query( - "UPDATE directive_chains SET total_steps = total_steps + 1, updated_at = NOW() WHERE id = $1" - ) - .bind(chain_id) - .execute(pool) - .await?; - - Ok(step) -} - -/// Get a chain step by ID. -pub async fn get_chain_step(pool: &PgPool, id: Uuid) -> Result, sqlx::Error> { - sqlx::query_as::<_, ChainStep>( - "SELECT * FROM chain_steps WHERE id = $1" - ) - .bind(id) - .fetch_optional(pool) - .await -} - -/// List all steps in a chain. -pub async fn list_chain_steps(pool: &PgPool, chain_id: Uuid) -> Result, sqlx::Error> { - sqlx::query_as::<_, ChainStep>( - "SELECT * FROM chain_steps WHERE chain_id = $1 ORDER BY order_index" - ) - .bind(chain_id) - .fetch_all(pool) - .await -} - -/// Update a chain step. -pub async fn update_chain_step( - pool: &PgPool, - step_id: Uuid, - req: UpdateStepRequest, -) -> Result { - sqlx::query_as::<_, ChainStep>( - r#" - UPDATE chain_steps SET - name = COALESCE($2, name), - description = COALESCE($3, description), - task_plan = COALESCE($4, task_plan), - depends_on = COALESCE($5, depends_on), - requirement_ids = COALESCE($6, requirement_ids), - acceptance_criteria_ids = COALESCE($7, acceptance_criteria_ids), - verifier_config = COALESCE($8, verifier_config), - editor_x = COALESCE($9, editor_x), - editor_y = COALESCE($10, editor_y) - WHERE id = $1 - RETURNING * - "#, - ) - .bind(step_id) - .bind(&req.name) - .bind(&req.description) - .bind(&req.task_plan) - .bind(&req.depends_on) - .bind(&req.requirement_ids) - .bind(&req.acceptance_criteria_ids) - .bind(&req.verifier_config) - .bind(req.editor_x) - .bind(req.editor_y) - .fetch_one(pool) - .await -} - -/// Delete a chain step. -pub async fn delete_chain_step(pool: &PgPool, step_id: Uuid) -> Result { - // Get chain_id first for updating count - let chain_id = sqlx::query_scalar::<_, Uuid>( - "SELECT chain_id FROM chain_steps WHERE id = $1" - ) - .bind(step_id) - .fetch_optional(pool) - .await?; - - let result = sqlx::query("DELETE FROM chain_steps WHERE id = $1") - .bind(step_id) - .execute(pool) - .await?; - - // Update chain total_steps count - if let Some(cid) = chain_id { - sqlx::query( - "UPDATE directive_chains SET total_steps = total_steps - 1, updated_at = NOW() WHERE id = $1" - ) - .bind(cid) - .execute(pool) - .await?; - } - - Ok(result.rows_affected() > 0) -} - -/// Find steps that are ready to execute (all dependencies met, status=pending). -pub async fn find_ready_steps(pool: &PgPool, chain_id: Uuid) -> Result, sqlx::Error> { - sqlx::query_as::<_, ChainStep>( - r#" - SELECT s.* FROM chain_steps s - WHERE s.chain_id = $1 - AND s.status = 'pending' - AND NOT EXISTS ( - SELECT 1 FROM chain_steps dep - WHERE dep.id = ANY(s.depends_on) - AND dep.status NOT IN ('passed', 'skipped') - ) - ORDER BY s.order_index - "#, - ) - .bind(chain_id) - .fetch_all(pool) - .await -} - -/// Update step status. -pub async fn update_step_status( - pool: &PgPool, - step_id: Uuid, - status: &str, -) -> Result { - let step = sqlx::query_as::<_, ChainStep>( - r#" - UPDATE chain_steps SET - status = $2, - started_at = CASE WHEN $2 = 'running' AND started_at IS NULL THEN NOW() ELSE started_at END, - completed_at = CASE WHEN $2 IN ('passed', 'failed', 'skipped') THEN NOW() ELSE completed_at END - WHERE id = $1 - RETURNING * - "#, - ) - .bind(step_id) - .bind(status) - .fetch_one(pool) - .await?; - - // Update chain completed_steps and failed_steps counts - if status == "passed" || status == "skipped" { - sqlx::query( - "UPDATE directive_chains SET completed_steps = completed_steps + 1, updated_at = NOW() WHERE id = $1" - ) - .bind(step.chain_id) - .execute(pool) - .await?; - } else if status == "failed" { - sqlx::query( - "UPDATE directive_chains SET failed_steps = failed_steps + 1, updated_at = NOW() WHERE id = $1" - ) - .bind(step.chain_id) - .execute(pool) - .await?; - } - - Ok(step) -} - -/// Link a step to a contract. -pub async fn update_step_contract( - pool: &PgPool, - step_id: Uuid, - contract_id: Uuid, - supervisor_task_id: Option, -) -> Result { - sqlx::query_as::<_, ChainStep>( - r#" - UPDATE chain_steps SET contract_id = $2, supervisor_task_id = $3 - WHERE id = $1 - RETURNING * - "#, - ) - .bind(step_id) - .bind(contract_id) - .bind(supervisor_task_id) - .fetch_one(pool) - .await -} - -/// Update step confidence score and level. -pub async fn update_step_confidence( - pool: &PgPool, - step_id: Uuid, - score: f64, - level: &str, - evaluation_id: Uuid, -) -> Result { - sqlx::query_as::<_, ChainStep>( - r#" - UPDATE chain_steps SET - confidence_score = $2, - confidence_level = $3, - last_evaluation_id = $4, - evaluation_count = evaluation_count + 1 - WHERE id = $1 - RETURNING * - "#, - ) - .bind(step_id) - .bind(score) - .bind(level) - .bind(evaluation_id) - .fetch_one(pool) - .await -} - -/// Increment step rework count. -pub async fn increment_step_rework_count(pool: &PgPool, step_id: Uuid) -> Result { - sqlx::query_as::<_, ChainStep>( - r#" - UPDATE chain_steps SET rework_count = rework_count + 1, status = 'rework' - WHERE id = $1 - RETURNING * - "#, - ) - .bind(step_id) - .fetch_one(pool) - .await -} - -/// Get chain graph for visualization. -pub async fn get_chain_graph( - pool: &PgPool, - chain_id: Uuid, -) -> Result { - let chain = get_directive_chain(pool, chain_id).await? - .ok_or_else(|| sqlx::Error::RowNotFound)?; - - let steps = list_chain_steps(pool, chain_id).await?; - - let nodes: Vec = steps.iter().map(|s| { - DirectiveChainGraphNode { - id: s.id, - name: s.name.clone(), - step_type: s.step_type.clone(), - status: s.status.clone(), - confidence_score: s.confidence_score, - confidence_level: s.confidence_level.clone(), - contract_id: s.contract_id, - editor_x: s.editor_x, - editor_y: s.editor_y, - } - }).collect(); - - let mut edges = Vec::new(); - for step in &steps { - for dep_id in &step.depends_on { - edges.push(DirectiveChainGraphEdge { - source: *dep_id, - target: step.id, - }); - } - } - - Ok(DirectiveChainGraphResponse { - chain_id, - directive_id: chain.directive_id, - nodes, - edges, - }) -} - -// ============================================================================= -// Directive Evaluation Operations -// ============================================================================= - -/// Create a directive evaluation. -pub async fn create_directive_evaluation( - pool: &PgPool, - directive_id: Uuid, - chain_id: Option, - step_id: Option, - contract_id: Option, - evaluation_type: &str, - evaluator: Option<&str>, - passed: bool, - overall_score: Option, - confidence_level: Option<&str>, - programmatic_results: serde_json::Value, - llm_results: serde_json::Value, - criteria_results: serde_json::Value, - summary_feedback: &str, - rework_instructions: Option<&str>, -) -> Result { - // Get next evaluation number for this step/directive - let evaluation_number = if let Some(sid) = step_id { - sqlx::query_scalar::<_, i32>( - "SELECT COALESCE(MAX(evaluation_number), 0) + 1 FROM directive_evaluations WHERE step_id = $1" - ) - .bind(sid) - .fetch_one(pool) - .await? - } else { - sqlx::query_scalar::<_, i32>( - "SELECT COALESCE(MAX(evaluation_number), 0) + 1 FROM directive_evaluations WHERE directive_id = $1 AND step_id IS NULL" - ) - .bind(directive_id) - .fetch_one(pool) - .await? - }; - - sqlx::query_as::<_, DirectiveEvaluation>( - r#" - INSERT INTO directive_evaluations ( - directive_id, chain_id, step_id, contract_id, - evaluation_type, evaluation_number, evaluator, - passed, overall_score, confidence_level, - programmatic_results, llm_results, criteria_results, - summary_feedback, rework_instructions, - completed_at - ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, NOW()) - RETURNING * - "#, - ) - .bind(directive_id) - .bind(chain_id) - .bind(step_id) - .bind(contract_id) - .bind(evaluation_type) - .bind(evaluation_number) - .bind(evaluator) - .bind(passed) - .bind(overall_score) - .bind(confidence_level) - .bind(&programmatic_results) - .bind(&llm_results) - .bind(&criteria_results) - .bind(summary_feedback) - .bind(rework_instructions) - .fetch_one(pool) - .await -} - -/// List evaluations for a step. -pub async fn list_step_evaluations( - pool: &PgPool, - step_id: Uuid, -) -> Result, sqlx::Error> { - sqlx::query_as::<_, DirectiveEvaluation>( - "SELECT * FROM directive_evaluations WHERE step_id = $1 ORDER BY evaluation_number DESC" - ) - .bind(step_id) - .fetch_all(pool) - .await -} - -/// List evaluations for a directive. -pub async fn list_directive_evaluations( - pool: &PgPool, - directive_id: Uuid, - limit: Option, -) -> Result, sqlx::Error> { - let limit = limit.unwrap_or(100); - sqlx::query_as::<_, DirectiveEvaluation>( - "SELECT * FROM directive_evaluations WHERE directive_id = $1 ORDER BY created_at DESC LIMIT $2" - ) - .bind(directive_id) - .bind(limit) - .fetch_all(pool) - .await -} - -// ============================================================================= -// Directive Event Operations -// ============================================================================= - -/// Emit a directive event. -pub async fn emit_directive_event( - pool: &PgPool, - directive_id: Uuid, - chain_id: Option, - step_id: Option, - event_type: &str, - severity: &str, - event_data: Option, - actor_type: &str, - actor_id: Option, -) -> Result { - sqlx::query_as::<_, DirectiveEvent>( - r#" - INSERT INTO directive_events ( - directive_id, chain_id, step_id, event_type, severity, event_data, actor_type, actor_id - ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8) - RETURNING * - "#, - ) - .bind(directive_id) - .bind(chain_id) - .bind(step_id) - .bind(event_type) - .bind(severity) - .bind(event_data) - .bind(actor_type) - .bind(actor_id) - .fetch_one(pool) - .await -} - -/// List directive events. -pub async fn list_directive_events( - pool: &PgPool, - directive_id: Uuid, - limit: Option, -) -> Result, sqlx::Error> { - let limit = limit.unwrap_or(100); - sqlx::query_as::<_, DirectiveEvent>( - "SELECT * FROM directive_events WHERE directive_id = $1 ORDER BY created_at DESC LIMIT $2" - ) - .bind(directive_id) - .bind(limit) - .fetch_all(pool) - .await -} - -// ============================================================================= -// Directive Verifier Operations -// ============================================================================= - -/// Create a directive verifier. -pub async fn create_directive_verifier( - pool: &PgPool, - directive_id: Uuid, - name: &str, - verifier_type: &str, - command: Option<&str>, - working_directory: Option<&str>, - auto_detect: bool, - detect_files: Vec, - weight: f64, - required: bool, -) -> Result { - sqlx::query_as::<_, DirectiveVerifier>( - r#" - INSERT INTO directive_verifiers ( - directive_id, name, verifier_type, command, working_directory, - auto_detect, detect_files, weight, required - ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) - RETURNING * - "#, - ) - .bind(directive_id) - .bind(name) - .bind(verifier_type) - .bind(command) - .bind(working_directory) - .bind(auto_detect) - .bind(&detect_files) - .bind(weight) - .bind(required) - .fetch_one(pool) - .await -} - -/// List verifiers for a directive. -pub async fn list_directive_verifiers( - pool: &PgPool, - directive_id: Uuid, -) -> Result, sqlx::Error> { - sqlx::query_as::<_, DirectiveVerifier>( - "SELECT * FROM directive_verifiers WHERE directive_id = $1 ORDER BY name" - ) - .bind(directive_id) - .fetch_all(pool) - .await -} - -/// Update a directive verifier. -pub async fn update_directive_verifier( - pool: &PgPool, - verifier_id: Uuid, - enabled: Option, - command: Option<&str>, - weight: Option, - required: Option, -) -> Result { - sqlx::query_as::<_, DirectiveVerifier>( - r#" - UPDATE directive_verifiers SET - enabled = COALESCE($2, enabled), - command = COALESCE($3, command), - weight = COALESCE($4, weight), - required = COALESCE($5, required), - updated_at = NOW() - WHERE id = $1 - RETURNING * - "#, - ) - .bind(verifier_id) - .bind(enabled) - .bind(command) - .bind(weight) - .bind(required) - .fetch_one(pool) - .await -} - -/// Update verifier last run result. -pub async fn update_verifier_result( - pool: &PgPool, - verifier_id: Uuid, - result: serde_json::Value, -) -> Result { - sqlx::query_as::<_, DirectiveVerifier>( - r#" - UPDATE directive_verifiers SET last_run_at = NOW(), last_result = $2, updated_at = NOW() - WHERE id = $1 - RETURNING * - "#, - ) - .bind(verifier_id) - .bind(result) - .fetch_one(pool) - .await -} - -// ============================================================================= -// Directive Approval Operations -// ============================================================================= - -/// Create an approval request. -pub async fn create_approval_request( - pool: &PgPool, - directive_id: Uuid, - step_id: Option, - approval_type: &str, - description: &str, - context: Option, - urgency: &str, - expires_at: Option>, -) -> Result { - sqlx::query_as::<_, DirectiveApproval>( - r#" - INSERT INTO directive_approvals ( - directive_id, step_id, approval_type, description, context, urgency, expires_at - ) - VALUES ($1, $2, $3, $4, $5, $6, $7) - RETURNING * - "#, - ) - .bind(directive_id) - .bind(step_id) - .bind(approval_type) - .bind(description) - .bind(context) - .bind(urgency) - .bind(expires_at) - .fetch_one(pool) - .await -} - -/// Resolve an approval request. -pub async fn resolve_approval( - pool: &PgPool, - approval_id: Uuid, - status: &str, - response: Option<&str>, - responded_by: Uuid, -) -> Result { - sqlx::query_as::<_, DirectiveApproval>( - r#" - UPDATE directive_approvals SET - status = $2, - response = $3, - responded_by = $4, - responded_at = NOW() - WHERE id = $1 - RETURNING * - "#, - ) - .bind(approval_id) - .bind(status) - .bind(response) - .bind(responded_by) - .fetch_one(pool) - .await -} - -/// List pending approvals for a directive. -pub async fn list_pending_approvals( - pool: &PgPool, - directive_id: Uuid, -) -> Result, sqlx::Error> { - sqlx::query_as::<_, DirectiveApproval>( - r#" - SELECT * FROM directive_approvals - WHERE directive_id = $1 AND status = 'pending' - ORDER BY - CASE urgency - WHEN 'critical' THEN 1 - WHEN 'high' THEN 2 - WHEN 'normal' THEN 3 - ELSE 4 - END, - created_at - "#, - ) - .bind(directive_id) - .fetch_all(pool) - .await -} - -/// Get step by contract ID. -pub async fn get_step_by_contract_id( - pool: &PgPool, - contract_id: Uuid, -) -> Result, sqlx::Error> { - sqlx::query_as::<_, ChainStep>( - "SELECT * FROM chain_steps WHERE contract_id = $1" - ) - .bind(contract_id) - .fetch_optional(pool) - .await -} - // ============================================================================= // Helper Functions // ============================================================================= diff --git a/makima/src/lib.rs b/makima/src/lib.rs index 3bc460b..8d3db58 100644 --- a/makima/src/lib.rs +++ b/makima/src/lib.rs @@ -3,6 +3,5 @@ pub mod daemon; pub mod db; pub mod listen; pub mod llm; -pub mod orchestration; pub mod server; pub mod tts; diff --git a/makima/src/orchestration/engine.rs b/makima/src/orchestration/engine.rs deleted file mode 100644 index 9f7c3b1..0000000 --- a/makima/src/orchestration/engine.rs +++ /dev/null @@ -1,1335 +0,0 @@ -//! Directive orchestration engine. -//! -//! Manages the lifecycle of directives: -//! - Starts directives and generates initial chains -//! - Monitors step execution and triggers evaluations -//! - Handles rework, escalation, and chain regeneration -//! - Enforces circuit breakers (cost, time, rework limits) - -use std::collections::HashMap; - -use sqlx::PgPool; -use thiserror::Error; -use tokio::sync::broadcast; -use uuid::Uuid; - -use crate::db::models::{ - AddStepRequest, ChainStep, CreateContractRequest, CreateTaskRequest, Directive, - DirectiveEvent, UpdateStepRequest, -}; -use crate::db::repository::{self, RepositoryError}; - -use super::planner::{ChainPlanner, GeneratedChain, PlannerError}; -use super::verifier::{ - auto_detect_verifiers, CompositeEvaluator, ConfidenceLevel, EvaluationResult, - VerificationContext, -}; - -/// Error type for engine operations. -#[derive(Error, Debug)] -pub enum EngineError { - #[error("Database error: {0}")] - Database(#[from] sqlx::Error), - - #[error("Repository error: {0}")] - Repository(#[from] RepositoryError), - - #[error("Planner error: {0}")] - Planner(#[from] PlannerError), - - #[error("Directive not found: {0}")] - DirectiveNotFound(Uuid), - - #[error("Chain not found for directive: {0}")] - ChainNotFound(Uuid), - - #[error("Step not found: {0}")] - StepNotFound(Uuid), - - #[error("Invalid state transition: {from} -> {to}")] - InvalidStateTransition { from: String, to: String }, - - #[error("Circuit breaker triggered: {0}")] - CircuitBreaker(String), - - #[error("Directive is paused")] - DirectivePaused, - - #[error("Contract creation failed: {0}")] - ContractCreation(String), - - #[error("LLM error: {0}")] - LlmError(String), -} - -/// Event emitted by the engine for UI updates. -#[derive(Debug, Clone)] -pub enum EngineEvent { - /// Directive status changed - DirectiveStatusChanged { - directive_id: Uuid, - old_status: String, - new_status: String, - }, - /// Step status changed - StepStatusChanged { - directive_id: Uuid, - step_id: Uuid, - old_status: String, - new_status: String, - }, - /// Evaluation completed - EvaluationCompleted { - directive_id: Uuid, - step_id: Uuid, - passed: bool, - confidence_level: ConfidenceLevel, - }, - /// Approval required - ApprovalRequired { - directive_id: Uuid, - approval_id: Uuid, - approval_type: String, - }, - /// Chain regenerated - ChainRegenerated { - directive_id: Uuid, - old_chain_id: Uuid, - new_chain_id: Uuid, - }, -} - -/// Result from starting a directive, containing info needed for auto-start. -pub struct PlanningStartResult { - /// The planning task ID that needs to be started on a daemon - pub task_id: Uuid, - /// The owner ID for finding available daemons - pub owner_id: Uuid, - /// The planning task details needed for the SpawnTask command - pub task_name: String, - pub plan: String, - pub contract_id: Uuid, - pub repository_url: Option, - pub base_branch: Option, -} - -/// Main orchestration engine for directives. -pub struct DirectiveEngine { - pool: PgPool, - planner: ChainPlanner, - event_tx: Option>, -} - -impl DirectiveEngine { - /// Create a new directive engine. - pub fn new(pool: PgPool) -> Self { - Self { - planner: ChainPlanner::new(pool.clone()), - pool, - event_tx: None, - } - } - - /// Set the event broadcast channel for UI updates. - pub fn with_event_channel(mut self, tx: broadcast::Sender) -> Self { - self.event_tx = Some(tx); - self - } - - /// Emit an event if channel is configured. - fn emit_event(&self, event: EngineEvent) { - if let Some(tx) = &self.event_tx { - let _ = tx.send(event); - } - } - - // ======================================================================== - // Directive Lifecycle - // ======================================================================== - - /// Start a directive: spawn a planning contract+task to generate the chain. - /// Returns a `PlanningStartResult` so the caller can auto-start the task on a daemon. - pub async fn start_directive(&self, directive_id: Uuid) -> Result { - let directive = repository::get_directive(&self.pool, directive_id) - .await? - .ok_or(EngineError::DirectiveNotFound(directive_id))?; - - // Validate current state - if directive.status != "draft" && directive.status != "paused" { - return Err(EngineError::InvalidStateTransition { - from: directive.status, - to: "planning".to_string(), - }); - } - - // Update status to planning - repository::update_directive_status(&self.pool, directive_id, "planning").await?; - self.emit_directive_event( - directive_id, - "status_changed", - "info", - serde_json::json!({"old_status": directive.status, "new_status": "planning"}), - "system", - ) - .await?; - - // Create an empty chain for the planning task to populate - let chain_name = format!( - "{}-chain", - directive.title.to_lowercase().replace(' ', "-") - ); - let _db_chain = repository::create_directive_chain( - &self.pool, - directive_id, - &chain_name, - Some(&format!("Execution plan for: {}", directive.goal)), - None, // rationale - None, // planning_model - ) - .await?; - - // Create a planning contract (type "execute", no phase guard) - let contract = repository::create_contract_for_owner( - &self.pool, - directive.owner_id, - CreateContractRequest { - name: format!("{} - Planning", directive.title), - description: Some(format!( - "Planning contract for directive: {}", - directive.goal - )), - contract_type: Some("execute".to_string()), - template_id: None, - initial_phase: Some("execute".to_string()), - autonomous_loop: Some(true), - phase_guard: Some(false), - local_only: Some(false), - auto_merge_local: None, - }, - ) - .await - .map_err(|e| { - EngineError::ContractCreation(format!("Failed to create planning contract: {}", e)) - })?; - - // Build instructions for the planning task - let plan = self.build_planning_task_instructions(&directive); - - // Create the planning task - let task_name = format!("{} - Planning", directive.title); - let task = repository::create_task_for_owner( - &self.pool, - directive.owner_id, - CreateTaskRequest { - contract_id: Some(contract.id), - name: task_name.clone(), - description: Some(format!( - "Plan the execution chain for directive: {}", - directive.goal - )), - plan: plan.clone(), - parent_task_id: None, - is_supervisor: true, - priority: 5, - repository_url: directive.repository_url.clone(), - base_branch: directive.base_branch.clone(), - target_branch: None, - merge_mode: None, - target_repo_path: None, - completion_action: Some("none".to_string()), - continue_from_task_id: None, - copy_files: None, - checkpoint_sha: None, - branched_from_task_id: None, - conversation_history: None, - supervisor_worktree_task_id: None, - }, - ) - .await - .map_err(|e| { - EngineError::ContractCreation(format!("Failed to create planning task: {}", e)) - })?; - - // Link the supervisor task to the contract - if let Err(e) = repository::update_contract_supervisor( - &self.pool, - contract.id, - task.id, - ) - .await - { - tracing::warn!( - contract_id = %contract.id, - task_id = %task.id, - error = %e, - "Failed to link supervisor task to planning contract" - ); - } - - // Link the planning contract to the directive - repository::set_directive_orchestrator_contract( - &self.pool, - directive_id, - contract.id, - ) - .await?; - - self.emit_directive_event( - directive_id, - "planning_started", - "info", - serde_json::json!({ - "contract_id": contract.id, - "task_id": task.id, - "message": "Planning task spawned, waiting for chain generation", - }), - "system", - ) - .await?; - - Ok(PlanningStartResult { - task_id: task.id, - owner_id: directive.owner_id, - task_name, - plan, - contract_id: contract.id, - repository_url: directive.repository_url.clone(), - base_branch: directive.base_branch.clone(), - }) - } - - /// Pause a directive. - pub async fn pause_directive(&self, directive_id: Uuid) -> Result<(), EngineError> { - let directive = repository::get_directive(&self.pool, directive_id) - .await? - .ok_or(EngineError::DirectiveNotFound(directive_id))?; - - if directive.status != "active" { - return Err(EngineError::InvalidStateTransition { - from: directive.status, - to: "paused".to_string(), - }); - } - - repository::update_directive_status(&self.pool, directive_id, "paused").await?; - self.emit_event(EngineEvent::DirectiveStatusChanged { - directive_id, - old_status: "active".to_string(), - new_status: "paused".to_string(), - }); - - Ok(()) - } - - /// Resume a paused directive. - pub async fn resume_directive(&self, directive_id: Uuid) -> Result<(), EngineError> { - let directive = repository::get_directive(&self.pool, directive_id) - .await? - .ok_or(EngineError::DirectiveNotFound(directive_id))?; - - if directive.status != "paused" { - return Err(EngineError::InvalidStateTransition { - from: directive.status, - to: "active".to_string(), - }); - } - - repository::update_directive_status(&self.pool, directive_id, "active").await?; - self.emit_event(EngineEvent::DirectiveStatusChanged { - directive_id, - old_status: "paused".to_string(), - new_status: "active".to_string(), - }); - - // Continue execution - self.advance_chain(directive_id).await?; - - Ok(()) - } - - /// Stop a directive (cannot be resumed). - pub async fn stop_directive(&self, directive_id: Uuid) -> Result<(), EngineError> { - let directive = repository::get_directive(&self.pool, directive_id) - .await? - .ok_or(EngineError::DirectiveNotFound(directive_id))?; - - if directive.status == "completed" || directive.status == "failed" { - return Err(EngineError::InvalidStateTransition { - from: directive.status, - to: "failed".to_string(), - }); - } - - repository::update_directive_status(&self.pool, directive_id, "failed").await?; - self.emit_event(EngineEvent::DirectiveStatusChanged { - directive_id, - old_status: directive.status, - new_status: "failed".to_string(), - }); - - Ok(()) - } - - // ======================================================================== - // Chain Management - // ======================================================================== - - /// Build a default chain as a fallback. - fn build_default_chain(&self, directive: &Directive) -> GeneratedChain { - GeneratedChain { - name: format!( - "{}-chain", - directive.title.to_lowercase().replace(' ', "-") - ), - description: format!("Execution plan for: {}", directive.goal), - steps: vec![ - super::planner::GeneratedStep { - name: "research".to_string(), - step_type: "research".to_string(), - description: format!( - "Research and understand the requirements for: {}", - directive.goal - ), - depends_on: vec![], - requirement_ids: vec![], - contract_template: None, - }, - super::planner::GeneratedStep { - name: "implement".to_string(), - step_type: "implement".to_string(), - description: format!("Implement the solution for: {}", directive.goal), - depends_on: vec!["research".to_string()], - requirement_ids: vec![], - contract_template: None, - }, - super::planner::GeneratedStep { - name: "test".to_string(), - step_type: "test".to_string(), - description: "Test and verify the implementation".to_string(), - depends_on: vec!["implement".to_string()], - requirement_ids: vec![], - contract_template: None, - }, - ], - } - } - - /// Create database steps from a generated chain. - async fn create_steps_from_chain( - &self, - chain_id: &Uuid, - chain: &GeneratedChain, - ) -> Result<(), EngineError> { - // First pass: create all steps and build name-to-id map - let mut step_id_map: HashMap = HashMap::new(); - - // Get editor positions - let positions = self.planner.compute_editor_positions(chain); - - for step in &chain.steps { - let (editor_x, editor_y) = positions - .get(&step.name) - .copied() - .unwrap_or((100.0, 100.0)); - - let task_plan = step - .contract_template - .as_ref() - .and_then(|t| t.tasks.first()) - .map(|t| t.plan.clone()) - .or_else(|| Some(step.description.clone())); - - let request = AddStepRequest { - name: step.name.clone(), - description: Some(step.description.clone()), - step_type: Some(step.step_type.clone()), - contract_type: step.contract_template.as_ref().map(|t| t.contract_type.clone()), - initial_phase: Some("plan".to_string()), - task_plan, - phases: step.contract_template.as_ref().map(|t| t.phases.clone()), - depends_on: None, // Will update in second pass - parallel_group: None, - requirement_ids: Some(step.requirement_ids.clone()), - acceptance_criteria_ids: None, - verifier_config: None, - editor_x: Some(editor_x), - editor_y: Some(editor_y), - }; - - let db_step = repository::create_chain_step(&self.pool, *chain_id, request).await?; - step_id_map.insert(step.name.clone(), db_step.id); - } - - // Second pass: update dependencies - for step in &chain.steps { - if step.depends_on.is_empty() { - continue; - } - - let step_id = step_id_map.get(&step.name).unwrap(); - let dep_ids: Vec = step - .depends_on - .iter() - .filter_map(|name| step_id_map.get(name)) - .copied() - .collect(); - - // Update step with proper dependencies - let update = UpdateStepRequest { - name: None, - description: None, - task_plan: None, - depends_on: Some(dep_ids), - requirement_ids: None, - acceptance_criteria_ids: None, - verifier_config: None, - editor_x: None, - editor_y: None, - }; - - repository::update_chain_step(&self.pool, *step_id, update).await?; - } - - Ok(()) - } - - /// Regenerate chain while preserving completed steps. - pub async fn regenerate_chain( - &self, - directive_id: Uuid, - reason: &str, - ) -> Result { - let directive = repository::get_directive(&self.pool, directive_id) - .await? - .ok_or(EngineError::DirectiveNotFound(directive_id))?; - - let current_chain = repository::get_current_chain(&self.pool, directive_id) - .await? - .ok_or(EngineError::ChainNotFound(directive_id))?; - - // Use default chain for regeneration - // (planning contract handles initial generation; regeneration uses fallback) - let new_chain = self.build_default_chain(&directive); - - // Supersede old chain - repository::supersede_chain(&self.pool, current_chain.id).await?; - - // Create new chain - let db_chain = repository::create_directive_chain( - &self.pool, - directive_id, - &new_chain.name, - Some(&new_chain.description), - Some(reason), // rationale - None, // planning_model - ) - .await?; - - // Create steps - self.create_steps_from_chain(&db_chain.id, &new_chain).await?; - - self.emit_event(EngineEvent::ChainRegenerated { - directive_id, - old_chain_id: current_chain.id, - new_chain_id: db_chain.id, - }); - - // Continue execution - self.advance_chain(directive_id).await?; - - Ok(db_chain.id) - } - - // ======================================================================== - // Step Execution - // ======================================================================== - - /// Advance chain execution: find ready steps and start them. - pub async fn advance_chain(&self, directive_id: Uuid) -> Result<(), EngineError> { - let directive = repository::get_directive(&self.pool, directive_id) - .await? - .ok_or(EngineError::DirectiveNotFound(directive_id))?; - - // Check if directive is active - if directive.status == "paused" { - return Err(EngineError::DirectivePaused); - } - if directive.status != "active" { - return Ok(()); // Not an error, just nothing to do - } - - // Check circuit breakers - self.check_circuit_breakers(&directive).await?; - - // Get current chain - let chain = repository::get_current_chain(&self.pool, directive_id) - .await? - .ok_or(EngineError::ChainNotFound(directive_id))?; - - // Find ready steps (dependencies met, status=pending) - let ready_steps = repository::find_ready_steps(&self.pool, chain.id).await?; - - // Start each ready step - for step in ready_steps { - self.start_step(&directive, &step).await?; - } - - // Check if chain is complete - let all_steps = repository::list_chain_steps(&self.pool, chain.id).await?; - let all_passed = all_steps.iter().all(|s| s.status == "passed" || s.status == "skipped"); - let any_blocked = all_steps.iter().any(|s| s.status == "blocked" || s.status == "failed"); - - if all_passed && !all_steps.is_empty() { - // Complete the directive - self.complete_directive(directive_id).await?; - } else if any_blocked { - // Check if we should regenerate or fail - let failed_count = all_steps.iter().filter(|s| s.status == "failed").count(); - if failed_count > 3 { - // Too many failures, fail the directive - repository::update_directive_status(&self.pool, directive_id, "failed").await?; - } - } - - Ok(()) - } - - /// Start a step by creating its contract and supervisor task. - async fn start_step(&self, directive: &Directive, step: &ChainStep) -> Result<(), EngineError> { - // Update step status to ready - repository::update_step_status(&self.pool, step.id, "ready").await?; - self.emit_event(EngineEvent::StepStatusChanged { - directive_id: directive.id, - step_id: step.id, - old_status: "pending".to_string(), - new_status: "ready".to_string(), - }); - - // Get contract details from step template - let (name, description, contract_type, initial_phase) = - self.get_contract_details(directive, step); - - // Create contract for this step - let contract = repository::create_contract_for_owner( - &self.pool, - directive.owner_id, - CreateContractRequest { - name: name.clone(), - description: description.clone(), - contract_type: Some(contract_type), - template_id: None, - initial_phase: Some(initial_phase), - autonomous_loop: Some(directive.autonomy_level == "full_auto"), - phase_guard: Some(true), - local_only: Some(false), - auto_merge_local: None, - }, - ) - .await - .map_err(|e| EngineError::ContractCreation(format!("Failed to create contract: {}", e)))?; - - // Build task plan from step description and task_plan - let task_plan = step - .task_plan - .clone() - .unwrap_or_else(|| { - format!( - "## Step: {}\n\n{}\n\n## Directive Goal\n{}", - step.name, - description.as_deref().unwrap_or("Complete this step."), - directive.goal, - ) - }); - - // Create supervisor task linked to the contract - let task = repository::create_task_for_owner( - &self.pool, - directive.owner_id, - CreateTaskRequest { - contract_id: Some(contract.id), - name: name.clone(), - description: description.clone(), - plan: task_plan, - parent_task_id: None, - is_supervisor: true, - priority: 5, - repository_url: directive.repository_url.clone(), - base_branch: directive.base_branch.clone(), - target_branch: None, - merge_mode: Some("pr".to_string()), - target_repo_path: None, - completion_action: Some("pr".to_string()), - continue_from_task_id: None, - copy_files: None, - checkpoint_sha: None, - branched_from_task_id: None, - conversation_history: None, - supervisor_worktree_task_id: None, - }, - ) - .await - .map_err(|e| EngineError::ContractCreation(format!("Failed to create task: {}", e)))?; - - // Link contract and task to step - repository::update_step_contract(&self.pool, step.id, contract.id, Some(task.id)).await?; - - // Update step status to running - repository::update_step_status(&self.pool, step.id, "running").await?; - self.emit_event(EngineEvent::StepStatusChanged { - directive_id: directive.id, - step_id: step.id, - old_status: "ready".to_string(), - new_status: "running".to_string(), - }); - - self.emit_directive_event( - directive.id, - "step_started", - "info", - serde_json::json!({ - "step_id": step.id, - "step_name": step.name, - "contract_id": contract.id, - "task_id": task.id, - }), - "system", - ) - .await?; - - Ok(()) - } - - /// Build contract details from a step. - /// Returns (name, description, contract_type, initial_phase) - fn get_contract_details( - &self, - directive: &Directive, - step: &ChainStep, - ) -> (String, Option, String, String) { - let name = format!("{} - {}", directive.title, step.name); - let description = step.description.clone(); - let contract_type = step.contract_type.clone(); - let initial_phase = step.initial_phase.clone().unwrap_or_else(|| "plan".to_string()); - - (name, description, contract_type, initial_phase) - } - - // ======================================================================== - // Evaluation - // ======================================================================== - - /// Handle contract completion: evaluate the step. - pub async fn on_contract_completed( - &self, - contract_id: Uuid, - ) -> Result<(), EngineError> { - // Find the step for this contract - let step = repository::get_step_by_contract_id(&self.pool, contract_id) - .await? - .ok_or(EngineError::StepNotFound(contract_id))?; - - // Get directive - let chain = repository::get_directive_chain(&self.pool, step.chain_id) - .await? - .ok_or(EngineError::ChainNotFound(step.chain_id))?; - - let directive = repository::get_directive(&self.pool, chain.directive_id) - .await? - .ok_or(EngineError::DirectiveNotFound(chain.directive_id))?; - - // Update step status to evaluating - repository::update_step_status(&self.pool, step.id, "evaluating").await?; - self.emit_event(EngineEvent::StepStatusChanged { - directive_id: directive.id, - step_id: step.id, - old_status: "running".to_string(), - new_status: "evaluating".to_string(), - }); - - // Run evaluation - let result = self.evaluate_step(&directive, &step).await?; - - // Record evaluation - let programmatic_results = result - .verifier_results - .iter() - .filter(|r| r.verifier_type != super::verifier::VerifierType::Llm) - .map(|r| serde_json::to_value(r).unwrap_or(serde_json::Value::Null)) - .collect::>(); - - let llm_results = result - .verifier_results - .iter() - .filter(|r| r.verifier_type == super::verifier::VerifierType::Llm) - .map(|r| serde_json::to_value(r).unwrap_or(serde_json::Value::Null)) - .collect::>(); - - // Get chain_id from step - let chain_id = step.chain_id; - - let _evaluation = repository::create_directive_evaluation( - &self.pool, - directive.id, - Some(chain_id), - Some(step.id), - step.contract_id, - "composite", - Some("orchestration_engine"), - result.passed, - Some(result.composite_score), - Some(result.confidence_level.as_str()), - serde_json::Value::Array(programmatic_results), - serde_json::Value::Array(llm_results), - serde_json::Value::Null, // criteria_results - &result.summary, - result.rework_instructions.as_deref(), - ) - .await?; - - // Update step based on result - let new_status = match result.confidence_level { - ConfidenceLevel::Green => "passed", - ConfidenceLevel::Yellow => { - // Check autonomy level - if directive.autonomy_level == "full_auto" { - "passed" // Accept yellow in full auto mode - } else { - // Create approval request - self.request_approval( - &directive, - &step, - "step_review", - &format!( - "Step '{}' completed with yellow confidence ({:.0}%). Review required.", - step.name, - result.composite_score * 100.0 - ), - ) - .await?; - "evaluating" // Wait for approval - } - } - ConfidenceLevel::Red => { - // Initiate rework - self.initiate_rework(&directive, &step, &result).await?; - "rework" - } - }; - - repository::update_step_status(&self.pool, step.id, new_status).await?; - repository::update_step_confidence( - &self.pool, - step.id, - result.composite_score, - result.confidence_level.as_str(), - result.id, - ) - .await?; - - self.emit_event(EngineEvent::EvaluationCompleted { - directive_id: directive.id, - step_id: step.id, - passed: result.passed, - confidence_level: result.confidence_level, - }); - - // If passed, continue chain execution - if new_status == "passed" { - self.advance_chain(directive.id).await?; - } - - Ok(()) - } - - /// Evaluate a step using tiered verification. - async fn evaluate_step( - &self, - directive: &Directive, - step: &ChainStep, - ) -> Result { - // Get repository path - let repo_path = directive - .local_path - .as_ref() - .map(std::path::PathBuf::from) - .unwrap_or_else(|| std::path::PathBuf::from(".")); - - // Auto-detect verifiers - let verifiers = auto_detect_verifiers(&repo_path).await; - - // Build verification context - let context = VerificationContext { - step_id: step.id, - contract_id: step.contract_id, - modified_files: vec![], // TODO: Get from contract/git - step_description: step.description.clone().unwrap_or_default(), - acceptance_criteria: vec![], // TODO: Get from directive - directive_context: directive.goal.clone(), - }; - - // Run composite evaluation - let evaluator = CompositeEvaluator::new(verifiers) - .with_thresholds( - directive.confidence_threshold_green, - directive.confidence_threshold_yellow, - ); - - Ok(evaluator.evaluate(&repo_path, &context).await) - } - - /// Initiate rework for a failed step. - async fn initiate_rework( - &self, - directive: &Directive, - step: &ChainStep, - result: &EvaluationResult, - ) -> Result<(), EngineError> { - // Increment rework count - let updated_step = repository::increment_step_rework_count(&self.pool, step.id).await?; - - // Check rework limit - let max_rework = directive.max_rework_cycles.unwrap_or(3); - if updated_step.rework_count >= max_rework { - // Too many rework attempts, mark as blocked - repository::update_step_status(&self.pool, step.id, "blocked").await?; - self.emit_directive_event( - directive.id, - "step_blocked", - "warning", - serde_json::json!({ - "step_id": step.id, - "step_name": step.name, - "reason": "Max rework attempts reached", - }), - "system", - ) - .await?; - return Ok(()); - } - - // Log rework event - self.emit_directive_event( - directive.id, - "step_rework", - "info", - serde_json::json!({ - "step_id": step.id, - "step_name": step.name, - "rework_count": updated_step.rework_count, - "instructions": result.rework_instructions, - }), - "system", - ) - .await?; - - // TODO: Send rework instructions to supervisor task - // This would involve: - // 1. Reset contract phase to 'plan' - // 2. Send message to supervisor with rework instructions - // 3. Update step status to 'running' - - Ok(()) - } - - /// Request human approval for a step. - async fn request_approval( - &self, - directive: &Directive, - step: &ChainStep, - approval_type: &str, - description: &str, - ) -> Result { - let context = serde_json::json!({ - "step_id": step.id, - "step_name": step.name, - "confidence_score": step.confidence_score, - }); - - let approval = repository::create_approval_request( - &self.pool, - directive.id, - Some(step.id), - approval_type, - description, - Some(context), - "medium", - None, // expires_at - ) - .await?; - - self.emit_event(EngineEvent::ApprovalRequired { - directive_id: directive.id, - approval_id: approval.id, - approval_type: approval_type.to_string(), - }); - - Ok(approval.id) - } - - /// Handle approval resolution. - pub async fn on_approval_resolved( - &self, - approval_id: Uuid, - approved: bool, - responded_by: Uuid, - ) -> Result<(), EngineError> { - let status = if approved { "approved" } else { "denied" }; - let approval = repository::resolve_approval( - &self.pool, - approval_id, - status, - None, - responded_by, - ) - .await?; - - if let Some(step_id) = approval.step_id { - let step = repository::get_chain_step(&self.pool, step_id) - .await? - .ok_or(EngineError::StepNotFound(step_id))?; - - let chain = repository::get_directive_chain(&self.pool, step.chain_id) - .await? - .ok_or(EngineError::ChainNotFound(step.chain_id))?; - - if approved { - // Mark step as passed and continue - repository::update_step_status(&self.pool, step_id, "passed").await?; - self.advance_chain(chain.directive_id).await?; - } else { - // Mark step as failed/blocked - repository::update_step_status(&self.pool, step_id, "blocked").await?; - } - } - - Ok(()) - } - - // ======================================================================== - // Planning - // ======================================================================== - - /// Build the task instructions for the planning task. - fn build_planning_task_instructions(&self, directive: &Directive) -> String { - let requirements: Vec = directive - .requirements - .as_array() - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_object()) - .map(|obj| { - let id = obj.get("id").and_then(|v| v.as_str()).unwrap_or("?"); - let desc = obj - .get("description") - .and_then(|v| v.as_str()) - .unwrap_or("?"); - format!("- {}: {}", id, desc) - }) - .collect() - }) - .unwrap_or_default(); - - let criteria: Vec = directive - .acceptance_criteria - .as_array() - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_object()) - .map(|obj| { - let id = obj.get("id").and_then(|v| v.as_str()).unwrap_or("?"); - let criterion = obj - .get("criterion") - .and_then(|v| v.as_str()) - .unwrap_or("?"); - format!("- {}: {}", id, criterion) - }) - .collect() - }) - .unwrap_or_default(); - - let constraints: Vec = directive - .constraints - .as_array() - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_str()) - .map(|s| format!("- {}", s)) - .collect() - }) - .unwrap_or_default(); - - let repo_info = directive - .repository_url - .as_deref() - .unwrap_or("(not specified)"); - - format!( - r#"You are planning an execution chain for a directive. - -## Directive: {title} -## Goal -{goal} - -## Requirements -{requirements} - -## Acceptance Criteria -{criteria} - -## Constraints -{constraints} - -## Repository: {repo} - -## Your Task - -Analyze the repository and create a chain of execution steps. -For each step, add it via the API: - -```bash -curl -s -X POST "$MAKIMA_URL/api/v1/directives/{directive_id}/chain/steps" \ - -H "Authorization: Bearer $MAKIMA_API_KEY" \ - -H "Content-Type: application/json" \ - -d '{{ - "name": "step-name", - "description": "What this step accomplishes", - "step_type": "implement", - "depends_on": [], - "contract_type": "execute", - "initial_phase": "execute", - "task_plan": "Detailed instructions for the step executor" - }}' -``` - -### Step types -Use these step types: research, design, implement, test, review, document - -### Dependencies -Each step can depend on other steps by name. Use the `depends_on` field with an array of step names. -Steps with no dependencies will run in parallel. - -### Guidelines -1. Break the work into logical, independently executable steps -2. Each step should be completable by a single Claude Code session -3. Use dependencies to enforce ordering where needed -4. Include a "test" or "verify" step at the end -5. Keep step names in kebab-case -6. The `task_plan` field should contain detailed instructions for the agent that will execute the step - -When you have added all steps, your task is complete."#, - title = directive.title, - goal = directive.goal, - requirements = if requirements.is_empty() { - "(none)".to_string() - } else { - requirements.join("\n") - }, - criteria = if criteria.is_empty() { - "(none)".to_string() - } else { - criteria.join("\n") - }, - constraints = if constraints.is_empty() { - "(none)".to_string() - } else { - constraints.join("\n") - }, - repo = repo_info, - directive_id = directive.id, - ) - } - - /// Handle planning task completion. - pub async fn on_planning_complete( - &self, - directive_id: Uuid, - success: bool, - ) -> Result<(), EngineError> { - let directive = repository::get_directive(&self.pool, directive_id) - .await? - .ok_or(EngineError::DirectiveNotFound(directive_id))?; - - // Only process if directive is still in planning state - if directive.status != "planning" { - tracing::warn!( - "Directive {} is in state '{}', not 'planning'. Skipping planning completion.", - directive_id, - directive.status - ); - return Ok(()); - } - - // Get current chain - let chain = repository::get_current_chain(&self.pool, directive_id) - .await? - .ok_or(EngineError::ChainNotFound(directive_id))?; - - // Check if chain has steps - let steps = repository::list_chain_steps(&self.pool, chain.id).await?; - - if success && !steps.is_empty() { - tracing::info!( - "Planning completed successfully for directive {} with {} steps", - directive_id, - steps.len() - ); - } else { - // Fall back to default chain - let reason = if !success { - "Planning task failed" - } else { - "Planning task produced no steps" - }; - tracing::warn!( - "{} for directive {}, using default chain", - reason, - directive_id - ); - - let default_chain = self.build_default_chain(&directive); - self.create_steps_from_chain(&chain.id, &default_chain) - .await?; - } - - // Activate the directive - repository::update_directive_status(&self.pool, directive_id, "active").await?; - self.emit_event(EngineEvent::DirectiveStatusChanged { - directive_id, - old_status: "planning".to_string(), - new_status: "active".to_string(), - }); - - self.emit_directive_event( - directive_id, - "planning_completed", - "info", - serde_json::json!({"success": success}), - "system", - ) - .await?; - - // Start ready steps - self.advance_chain(directive_id).await?; - - Ok(()) - } - - // ======================================================================== - // Circuit Breakers - // ======================================================================== - - /// Check circuit breakers for a directive. - async fn check_circuit_breakers(&self, directive: &Directive) -> Result<(), EngineError> { - // Check cost limit - if let Some(max_cost) = directive.max_total_cost_usd { - let current_cost = directive.total_cost_usd; - if current_cost >= max_cost { - return Err(EngineError::CircuitBreaker(format!( - "Cost limit exceeded: ${:.2} >= ${:.2}", - current_cost, max_cost - ))); - } - } - - // Check time limit (stored in minutes) - if let Some(max_minutes) = directive.max_wall_time_minutes { - if let Some(started_at) = directive.started_at { - let elapsed = chrono::Utc::now().signed_duration_since(started_at); - let elapsed_minutes = elapsed.num_minutes(); - if elapsed_minutes >= max_minutes as i64 { - return Err(EngineError::CircuitBreaker(format!( - "Time limit exceeded: {} min >= {} min", - elapsed_minutes, max_minutes - ))); - } - } - } - - // Check chain generation limit - if let Some(max_gen) = directive.max_chain_regenerations { - let current_gen = directive.chain_generation_count; - if current_gen >= max_gen { - return Err(EngineError::CircuitBreaker(format!( - "Chain generation limit exceeded: {} >= {}", - current_gen, max_gen - ))); - } - } - - Ok(()) - } - - // ======================================================================== - // Completion - // ======================================================================== - - /// Complete a directive after all steps pass. - async fn complete_directive(&self, directive_id: Uuid) -> Result<(), EngineError> { - // Run final evaluation (optional) - // TODO: LLM evaluation of overall directive completion - - // Update directive status - repository::update_directive_status(&self.pool, directive_id, "completed").await?; - - self.emit_event(EngineEvent::DirectiveStatusChanged { - directive_id, - old_status: "active".to_string(), - new_status: "completed".to_string(), - }); - - self.emit_directive_event( - directive_id, - "directive_completed", - "info", - serde_json::json!({}), - "system", - ) - .await?; - - Ok(()) - } - - // ======================================================================== - // Event Logging - // ======================================================================== - - /// Emit a directive event to the database. - async fn emit_directive_event( - &self, - directive_id: Uuid, - event_type: &str, - severity: &str, - event_data: serde_json::Value, - actor_type: &str, - ) -> Result { - Ok(repository::emit_directive_event( - &self.pool, - directive_id, - None, // chain_id - None, // step_id - event_type, - severity, - Some(event_data), - actor_type, - None, // actor_id - ) - .await?) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_confidence_level_decision() { - // Green confidence should pass in all modes - assert_eq!(ConfidenceLevel::Green.as_str(), "green"); - - // Yellow confidence behavior depends on autonomy level - assert_eq!(ConfidenceLevel::Yellow.as_str(), "yellow"); - - // Red confidence should always trigger rework - assert_eq!(ConfidenceLevel::Red.as_str(), "red"); - } -} diff --git a/makima/src/orchestration/mod.rs b/makima/src/orchestration/mod.rs deleted file mode 100644 index 8fca5ba..0000000 --- a/makima/src/orchestration/mod.rs +++ /dev/null @@ -1,26 +0,0 @@ -//! Orchestration engine for directive-driven autonomous execution. -//! -//! This module provides the core orchestration capabilities: -//! - [`DirectiveEngine`]: Main orchestration loop that manages directive lifecycle -//! - [`ChainPlanner`]: LLM-based chain generation from directive goals -//! - [`Verifier`]: Pluggable verification system for step validation -//! -//! # Architecture -//! -//! The orchestration system follows a directive-first approach: -//! 1. Directives define goals, requirements, and acceptance criteria -//! 2. Chains are generated execution plans (DAGs of steps) -//! 3. Steps map to contracts that are created and monitored -//! 4. Tiered verification (programmatic first, then LLM) determines confidence -//! 5. Confidence scoring (green/yellow/red) drives autonomy decisions - -mod engine; -mod planner; -mod verifier; - -pub use engine::{DirectiveEngine, EngineError, PlanningStartResult}; -pub use planner::{ChainPlanner, GeneratedSpec, PlannerError}; -pub use verifier::{ - auto_detect_verifiers, CompositeEvaluator, ConfidenceLevel, EvaluationResult, Verifier, - VerifierError, VerifierInfo, VerifierResult, VerifierType, -}; diff --git a/makima/src/orchestration/planner.rs b/makima/src/orchestration/planner.rs deleted file mode 100644 index aec2e48..0000000 --- a/makima/src/orchestration/planner.rs +++ /dev/null @@ -1,848 +0,0 @@ -//! Chain planner for LLM-based execution plan generation. -//! -//! Generates chains (DAGs of steps) from directive goals and requirements. -//! Supports both initial plan generation and replanning while preserving -//! completed work. - -use serde::{Deserialize, Serialize}; -use std::collections::{HashMap, HashSet}; -use thiserror::Error; -use uuid::Uuid; - -use crate::db::models::{AddStepRequest, ChainStep, Directive}; - -/// Error type for planner operations. -#[derive(Error, Debug)] -pub enum PlannerError { - #[error("Cycle detected in DAG: {0}")] - CycleDetected(String), - - #[error("Invalid dependency: step '{step}' depends on unknown step '{dependency}'")] - InvalidDependency { step: String, dependency: String }, - - #[error("LLM generation failed: {0}")] - LlmError(String), - - #[error("Requirement not covered: {0}")] - RequirementNotCovered(String), - - #[error("Invalid plan: {0}")] - InvalidPlan(String), - - #[error("Empty plan generated")] - EmptyPlan, -} - -/// Generated step from LLM planning. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GeneratedStep { - /// Unique name within the chain - pub name: String, - /// Type of step (e.g., "research", "implement", "test", "review") - pub step_type: String, - /// Description of what this step accomplishes - pub description: String, - /// Names of steps this depends on - pub depends_on: Vec, - /// IDs of requirements this step addresses - pub requirement_ids: Vec, - /// Contract template fields - pub contract_template: Option, -} - -/// Template for contract creation from step. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ContractTemplate { - /// Contract name - pub name: String, - /// Contract description - pub description: String, - /// Contract type (e.g., "simple", "agentic") - pub contract_type: String, - /// Phases for the contract - pub phases: Vec, - /// Tasks within the contract - pub tasks: Vec, - /// Deliverables expected - pub deliverables: Vec, -} - -/// Template for task within contract. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TaskTemplate { - pub name: String, - pub plan: String, -} - -/// Template for deliverable. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DeliverableTemplate { - pub id: String, - pub name: String, - pub priority: String, -} - -/// Generated chain from planning. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GeneratedChain { - /// Name for the chain - pub name: String, - /// Description of the execution plan - pub description: String, - /// Steps in the chain - pub steps: Vec, -} - -/// Generated specification from LLM. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GeneratedSpec { - /// Generated title (if improved from goal) - pub title: Option, - /// Structured requirements - pub requirements: serde_json::Value, - /// Structured acceptance criteria - pub acceptance_criteria: serde_json::Value, - /// Constraints extracted from goal - pub constraints: Option, -} - -/// Chain planner for LLM-based plan generation. -pub struct ChainPlanner { - /// Default step types to suggest (reserved for future use) - #[allow(dead_code)] - default_step_types: Vec, - /// Database pool for persistence - #[allow(dead_code)] - pool: Option, -} - -impl Default for ChainPlanner { - fn default() -> Self { - Self::without_pool() - } -} - -impl ChainPlanner { - /// Create a new chain planner without database pool. - pub fn without_pool() -> Self { - Self { - default_step_types: vec![ - "research".to_string(), - "design".to_string(), - "implement".to_string(), - "test".to_string(), - "review".to_string(), - "document".to_string(), - ], - pool: None, - } - } - - /// Create a new chain planner (backwards compatible). - pub fn new(pool: sqlx::PgPool) -> Self { - Self { - default_step_types: vec![ - "research".to_string(), - "design".to_string(), - "implement".to_string(), - "test".to_string(), - "review".to_string(), - "document".to_string(), - ], - pool: Some(pool), - } - } - - /// Generate a specification from a directive's goal. - /// - /// Analyzes the goal text to produce structured requirements and - /// acceptance criteria. In production, this would call an LLM for - /// richer spec generation. - pub async fn generate_spec( - &self, - directive: &Directive, - ) -> Result { - // Build a prompt for spec generation - let prompt = format!( - r#"Analyze this goal and generate structured requirements and acceptance criteria. - -Goal: {} - -Generate a JSON response with: -- title: A concise title -- requirements: Array of {{id, title, description, priority, category}} -- acceptance_criteria: Array of {{id, requirementIds, description, testable, verificationMethod}} -- constraints: Array of constraint strings"#, - directive.goal - ); - - // For now, generate a basic spec from the goal text. - // When LLM integration is available, this will call the LLM with the prompt. - let _prompt = prompt; // Will be used when LLM is wired up - - let title = generate_title_from_goal(&directive.goal); - - let requirements = serde_json::json!([ - { - "id": "REQ-001", - "title": title, - "description": directive.goal, - "priority": "required", - "category": "core" - } - ]); - - let acceptance_criteria = serde_json::json!([ - { - "id": "AC-001", - "requirementIds": ["REQ-001"], - "description": format!("Goal is achieved: {}", directive.goal), - "testable": true, - "verificationMethod": "manual" - } - ]); - - Ok(GeneratedSpec { - title: Some(title), - requirements, - acceptance_criteria, - constraints: None, - }) - } - - /// Build a planning prompt for the LLM. - pub fn build_planning_prompt(&self, directive: &Directive) -> String { - let requirements: Vec = directive - .requirements - .as_array() - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_object()) - .map(|obj| { - let id = obj.get("id").and_then(|v| v.as_str()).unwrap_or("?"); - let desc = obj - .get("description") - .and_then(|v| v.as_str()) - .unwrap_or("?"); - format!("- {}: {}", id, desc) - }) - .collect() - }) - .unwrap_or_default(); - - let criteria: Vec = directive - .acceptance_criteria - .as_array() - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_object()) - .map(|obj| { - let id = obj.get("id").and_then(|v| v.as_str()).unwrap_or("?"); - let criterion = obj - .get("criterion") - .and_then(|v| v.as_str()) - .unwrap_or("?"); - format!("- {}: {}", id, criterion) - }) - .collect() - }) - .unwrap_or_default(); - - let constraints: Vec = directive - .constraints - .as_array() - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_str()) - .map(|s| format!("- {}", s)) - .collect() - }) - .unwrap_or_default(); - - format!( - r#"You are a software architect planning an execution chain for a coding task. - -## Directive Goal -{goal} - -## Requirements -{requirements} - -## Acceptance Criteria -{criteria} - -## Constraints -{constraints} - -## Instructions - -Create an execution plan as a chain of steps. Each step should: -1. Have a unique, descriptive name (kebab-case) -2. Specify its type (research, design, implement, test, review, document) -3. Declare dependencies on prior steps (if any) -4. Map to specific requirement IDs it addresses -5. Include a contract template with tasks and deliverables - -The chain should form a valid DAG (no cycles). Steps can run in parallel if they don't depend on each other. - -Respond with a JSON object in this format: -```json -{{ - "name": "chain-name", - "description": "Brief description of the plan", - "steps": [ - {{ - "name": "step-name", - "step_type": "implement", - "description": "What this step does", - "depends_on": ["prior-step-name"], - "requirement_ids": ["REQ-001"], - "contract_template": {{ - "name": "Contract Name", - "description": "Contract description", - "contract_type": "simple", - "phases": ["plan", "execute"], - "tasks": [ - {{"name": "Task 1", "plan": "Detailed plan for this task"}} - ], - "deliverables": [ - {{"id": "del-1", "name": "Deliverable 1", "priority": "required"}} - ] - }} - }} - ] -}} -``` - -Generate the optimal execution plan now."#, - goal = directive.goal, - requirements = requirements.join("\n"), - criteria = criteria.join("\n"), - constraints = constraints.join("\n"), - ) - } - - /// Parse LLM response into a generated chain. - pub fn parse_plan_response(&self, response: &str) -> Result { - // Extract JSON from response (may be wrapped in markdown code blocks) - let json_str = extract_json_from_response(response)?; - - let chain: GeneratedChain = serde_json::from_str(&json_str) - .map_err(|e| PlannerError::InvalidPlan(format!("JSON parse error: {}", e)))?; - - if chain.steps.is_empty() { - return Err(PlannerError::EmptyPlan); - } - - // Validate the chain - self.validate_chain(&chain)?; - - Ok(chain) - } - - /// Validate a generated chain. - pub fn validate_chain(&self, chain: &GeneratedChain) -> Result<(), PlannerError> { - // Build step name set - let step_names: HashSet<&str> = chain.steps.iter().map(|s| s.name.as_str()).collect(); - - // Check for duplicate names - if step_names.len() != chain.steps.len() { - return Err(PlannerError::InvalidPlan( - "Duplicate step names detected".to_string(), - )); - } - - // Validate dependencies exist - for step in &chain.steps { - for dep in &step.depends_on { - if !step_names.contains(dep.as_str()) { - return Err(PlannerError::InvalidDependency { - step: step.name.clone(), - dependency: dep.clone(), - }); - } - } - } - - // Check for cycles using DFS - self.detect_cycles(chain)?; - - Ok(()) - } - - /// Detect cycles in the chain DAG using DFS. - fn detect_cycles(&self, chain: &GeneratedChain) -> Result<(), PlannerError> { - let mut visited = HashSet::new(); - let mut rec_stack = HashSet::new(); - - // Build adjacency map - let adj: HashMap<&str, Vec<&str>> = chain - .steps - .iter() - .map(|s| (s.name.as_str(), s.depends_on.iter().map(|d| d.as_str()).collect())) - .collect(); - - for step in &chain.steps { - if !visited.contains(step.name.as_str()) { - if self.has_cycle(&step.name, &adj, &mut visited, &mut rec_stack) { - return Err(PlannerError::CycleDetected(step.name.clone())); - } - } - } - - Ok(()) - } - - fn has_cycle<'a>( - &self, - node: &'a str, - adj: &HashMap<&'a str, Vec<&'a str>>, - visited: &mut HashSet<&'a str>, - rec_stack: &mut HashSet<&'a str>, - ) -> bool { - visited.insert(node); - rec_stack.insert(node); - - if let Some(deps) = adj.get(node) { - for &dep in deps { - if !visited.contains(dep) { - if self.has_cycle(dep, adj, visited, rec_stack) { - return true; - } - } else if rec_stack.contains(dep) { - return true; - } - } - } - - rec_stack.remove(node); - false - } - - /// Check that all requirements are covered by at least one step. - pub fn check_requirement_coverage( - &self, - chain: &GeneratedChain, - directive: &Directive, - ) -> Result<(), PlannerError> { - let required_ids: HashSet = directive - .requirements - .as_array() - .map(|arr| { - arr.iter() - .filter_map(|v| v.get("id").and_then(|id| id.as_str())) - .map(|s| s.to_string()) - .collect() - }) - .unwrap_or_default(); - - let covered_ids: HashSet = chain - .steps - .iter() - .flat_map(|s| s.requirement_ids.clone()) - .collect(); - - for req_id in required_ids { - if !covered_ids.contains(&req_id) { - return Err(PlannerError::RequirementNotCovered(req_id)); - } - } - - Ok(()) - } - - /// Get topological order of steps. - pub fn topological_sort<'a>( - &self, - chain: &'a GeneratedChain, - ) -> Result, PlannerError> { - let mut in_degree: HashMap<&str, usize> = HashMap::new(); - let mut adj: HashMap<&str, Vec<&str>> = HashMap::new(); - - // Initialize - for step in &chain.steps { - in_degree.entry(step.name.as_str()).or_insert(0); - adj.entry(step.name.as_str()).or_insert_with(Vec::new); - } - - // Build graph (reversed - edges from dependency to dependent) - for step in &chain.steps { - for dep in &step.depends_on { - adj.entry(dep.as_str()) - .or_insert_with(Vec::new) - .push(step.name.as_str()); - *in_degree.entry(step.name.as_str()).or_insert(0) += 1; - } - } - - // Kahn's algorithm - let mut queue: Vec<&str> = in_degree - .iter() - .filter(|&(_, deg)| *deg == 0) - .map(|(&name, _)| name) - .collect(); - - let mut result = Vec::new(); - - while let Some(node) = queue.pop() { - result.push(node); - - if let Some(neighbors) = adj.get(node) { - for &neighbor in neighbors { - let deg = in_degree.get_mut(neighbor).unwrap(); - *deg -= 1; - if *deg == 0 { - queue.push(neighbor); - } - } - } - } - - if result.len() != chain.steps.len() { - return Err(PlannerError::CycleDetected( - "Cycle detected during topological sort".to_string(), - )); - } - - Ok(result) - } - - /// Convert generated steps to AddStepRequest for database insertion. - pub fn steps_to_requests( - &self, - chain: &GeneratedChain, - step_id_map: &HashMap, - ) -> Vec { - chain - .steps - .iter() - .map(|step| { - let depends_on: Vec = step - .depends_on - .iter() - .filter_map(|name| step_id_map.get(name)) - .copied() - .collect(); - - let task_plan = step - .contract_template - .as_ref() - .and_then(|t| t.tasks.first()) - .map(|t| t.plan.clone()); - - AddStepRequest { - name: step.name.clone(), - description: Some(step.description.clone()), - step_type: Some(step.step_type.clone()), - contract_type: step.contract_template.as_ref().map(|t| t.contract_type.clone()), - initial_phase: Some("plan".to_string()), - task_plan, - phases: step.contract_template.as_ref().map(|t| t.phases.clone()), - depends_on: Some(depends_on), - parallel_group: None, - requirement_ids: Some(step.requirement_ids.clone()), - acceptance_criteria_ids: None, - verifier_config: None, - editor_x: None, - editor_y: None, - } - }) - .collect() - } - - /// Compute editor positions for steps based on DAG layout. - pub fn compute_editor_positions( - &self, - chain: &GeneratedChain, - ) -> HashMap { - let depths = self.get_step_depths(chain); - let mut positions: HashMap = HashMap::new(); - - // Group by depth - let mut by_depth: HashMap> = HashMap::new(); - for step in &chain.steps { - let depth = depths.get(&step.name).copied().unwrap_or(0); - by_depth.entry(depth).or_default().push(&step.name); - } - - // Compute positions: x based on depth, y based on index within depth - let x_spacing = 250.0; - let y_spacing = 150.0; - - for (depth, steps) in &by_depth { - let x = (*depth as f64) * x_spacing + 100.0; - for (i, name) in steps.iter().enumerate() { - let y = (i as f64) * y_spacing + 100.0; - positions.insert(name.to_string(), (x, y)); - } - } - - positions - } - - /// Get depth of each step in the DAG. - fn get_step_depths(&self, chain: &GeneratedChain) -> HashMap { - let mut depths: HashMap = HashMap::new(); - - // Build dependency map - let deps: HashMap> = chain - .steps - .iter() - .map(|s| (s.name.clone(), s.depends_on.clone())) - .collect(); - - fn compute_depth( - name: &str, - deps: &HashMap>, - depths: &mut HashMap, - ) -> usize { - if let Some(&d) = depths.get(name) { - return d; - } - - let depth = deps - .get(name) - .map(|dep_list| { - dep_list - .iter() - .map(|d| compute_depth(d, deps, depths) + 1) - .max() - .unwrap_or(0) - }) - .unwrap_or(0); - - depths.insert(name.to_string(), depth); - depth - } - - for step in &chain.steps { - compute_depth(&step.name, &deps, &mut depths); - } - - depths - } - - /// Build a replanning prompt that preserves completed steps. - pub fn build_replan_prompt( - &self, - directive: &Directive, - completed_steps: &[ChainStep], - failed_step: Option<&ChainStep>, - reason: &str, - ) -> String { - let completed_summary: Vec = completed_steps - .iter() - .map(|s| format!("- {} ({}): completed", s.name, s.step_type)) - .collect(); - - let failed_summary = failed_step - .map(|s| format!("Failed step: {} - {}", s.name, s.description.as_deref().unwrap_or(""))) - .unwrap_or_default(); - - format!( - r#"You are a software architect replanning an execution chain. - -## Original Goal -{goal} - -## Completed Steps (preserve these) -{completed} - -## Failure Information -{failed} -Reason: {reason} - -## Instructions -Generate a new execution plan that: -1. Preserves all completed work -2. Addresses the failure -3. Continues toward the original goal - -Use the same JSON format as before. Do not include already completed steps."#, - goal = directive.goal, - completed = completed_summary.join("\n"), - failed = failed_summary, - reason = reason, - ) - } -} - -/// Generate a concise title from a goal string. -fn generate_title_from_goal(goal: &str) -> String { - // Take the first sentence or first 80 chars - let title = if let Some(pos) = goal.find('.') { - if pos < 100 { - &goal[..pos] - } else { - &goal[..80.min(goal.len())] - } - } else if goal.len() > 80 { - &goal[..80] - } else { - goal - }; - title.trim().to_string() -} - -/// Extract JSON from LLM response (handles markdown code blocks). -fn extract_json_from_response(response: &str) -> Result { - // Try to find JSON in code block - if let Some(start) = response.find("```json") { - let json_start = start + 7; - if let Some(end) = response[json_start..].find("```") { - return Ok(response[json_start..json_start + end].trim().to_string()); - } - } - - // Try to find JSON in generic code block - if let Some(start) = response.find("```") { - let block_start = start + 3; - // Skip language identifier if present - let json_start = response[block_start..] - .find('\n') - .map(|i| block_start + i + 1) - .unwrap_or(block_start); - if let Some(end) = response[json_start..].find("```") { - return Ok(response[json_start..json_start + end].trim().to_string()); - } - } - - // Try to parse the whole thing as JSON - if response.trim().starts_with('{') { - return Ok(response.trim().to_string()); - } - - Err(PlannerError::InvalidPlan( - "Could not extract JSON from response".to_string(), - )) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn make_test_chain() -> GeneratedChain { - GeneratedChain { - name: "test-chain".to_string(), - description: "Test chain".to_string(), - steps: vec![ - GeneratedStep { - name: "step-a".to_string(), - step_type: "research".to_string(), - description: "Research step".to_string(), - depends_on: vec![], - requirement_ids: vec!["REQ-001".to_string()], - contract_template: None, - }, - GeneratedStep { - name: "step-b".to_string(), - step_type: "implement".to_string(), - description: "Implementation step".to_string(), - depends_on: vec!["step-a".to_string()], - requirement_ids: vec!["REQ-002".to_string()], - contract_template: None, - }, - GeneratedStep { - name: "step-c".to_string(), - step_type: "test".to_string(), - description: "Test step".to_string(), - depends_on: vec!["step-b".to_string()], - requirement_ids: vec!["REQ-001".to_string()], - contract_template: None, - }, - ], - } - } - - #[test] - fn test_validate_chain_valid() { - let planner = ChainPlanner::without_pool(); - let chain = make_test_chain(); - assert!(planner.validate_chain(&chain).is_ok()); - } - - #[test] - fn test_validate_chain_invalid_dependency() { - let planner = ChainPlanner::without_pool(); - let mut chain = make_test_chain(); - chain.steps[1].depends_on = vec!["nonexistent".to_string()]; - - let result = planner.validate_chain(&chain); - assert!(matches!(result, Err(PlannerError::InvalidDependency { .. }))); - } - - #[test] - fn test_validate_chain_cycle() { - let planner = ChainPlanner::without_pool(); - let chain = GeneratedChain { - name: "cyclic".to_string(), - description: "Has cycle".to_string(), - steps: vec![ - GeneratedStep { - name: "a".to_string(), - step_type: "research".to_string(), - description: "A".to_string(), - depends_on: vec!["c".to_string()], - requirement_ids: vec![], - contract_template: None, - }, - GeneratedStep { - name: "b".to_string(), - step_type: "implement".to_string(), - description: "B".to_string(), - depends_on: vec!["a".to_string()], - requirement_ids: vec![], - contract_template: None, - }, - GeneratedStep { - name: "c".to_string(), - step_type: "test".to_string(), - description: "C".to_string(), - depends_on: vec!["b".to_string()], - requirement_ids: vec![], - contract_template: None, - }, - ], - }; - - let result = planner.validate_chain(&chain); - assert!(matches!(result, Err(PlannerError::CycleDetected(_)))); - } - - #[test] - fn test_topological_sort() { - let planner = ChainPlanner::without_pool(); - let chain = make_test_chain(); - let order = planner.topological_sort(&chain).unwrap(); - - // step-a must come before step-b, step-b before step-c - let pos_a = order.iter().position(|&n| n == "step-a").unwrap(); - let pos_b = order.iter().position(|&n| n == "step-b").unwrap(); - let pos_c = order.iter().position(|&n| n == "step-c").unwrap(); - - assert!(pos_a < pos_b); - assert!(pos_b < pos_c); - } - - #[test] - fn test_extract_json_from_code_block() { - let response = r#" -Here's the plan: - -```json -{"name": "test"} -``` - -That's it! -"#; - let json = extract_json_from_response(response).unwrap(); - assert_eq!(json, r#"{"name": "test"}"#); - } - - #[test] - fn test_extract_json_raw() { - let response = r#"{"name": "test"}"#; - let json = extract_json_from_response(response).unwrap(); - assert_eq!(json, r#"{"name": "test"}"#); - } -} diff --git a/makima/src/orchestration/verifier.rs b/makima/src/orchestration/verifier.rs deleted file mode 100644 index bc29e47..0000000 --- a/makima/src/orchestration/verifier.rs +++ /dev/null @@ -1,833 +0,0 @@ -//! Verification system for directive step evaluation. -//! -//! Provides tiered verification: programmatic verifiers run first, -//! then LLM evaluation if programmatic checks pass. Composite scoring -//! combines results with configurable weights. - -use async_trait::async_trait; -use serde::{Deserialize, Serialize}; -use serde_json::Value as JsonValue; -use std::path::Path; -use thiserror::Error; -use uuid::Uuid; - -/// Confidence level based on composite score and thresholds. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum ConfidenceLevel { - /// High confidence (score >= green threshold) - Green, - /// Medium confidence (score >= yellow threshold but < green) - Yellow, - /// Low confidence (score < yellow threshold) - Red, -} - -impl ConfidenceLevel { - /// Compute confidence level from score and thresholds. - pub fn from_score(score: f64, green_threshold: f64, yellow_threshold: f64) -> Self { - if score >= green_threshold { - ConfidenceLevel::Green - } else if score >= yellow_threshold { - ConfidenceLevel::Yellow - } else { - ConfidenceLevel::Red - } - } - - /// Convert to string for database storage. - pub fn as_str(&self) -> &'static str { - match self { - ConfidenceLevel::Green => "green", - ConfidenceLevel::Yellow => "yellow", - ConfidenceLevel::Red => "red", - } - } -} - -impl std::fmt::Display for ConfidenceLevel { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.as_str()) - } -} - -/// Type of verifier for categorization. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum VerifierType { - /// Run test suite (npm test, cargo test, pytest, etc.) - TestRunner, - /// Run linter (eslint, clippy, ruff, etc.) - Linter, - /// Run type checker (tsc, mypy, etc.) - TypeChecker, - /// Run build command (npm build, cargo build, etc.) - Build, - /// Custom command verifier - Custom, - /// LLM-based semantic evaluation - Llm, -} - -impl VerifierType { - pub fn as_str(&self) -> &'static str { - match self { - VerifierType::TestRunner => "test_runner", - VerifierType::Linter => "linter", - VerifierType::TypeChecker => "type_checker", - VerifierType::Build => "build", - VerifierType::Custom => "custom", - VerifierType::Llm => "llm", - } - } -} - -/// Result of a single verifier run. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct VerifierResult { - /// Name of the verifier - pub name: String, - /// Type of verifier - pub verifier_type: VerifierType, - /// Whether the verification passed - pub passed: bool, - /// Score from 0.0 to 1.0 (1.0 = perfect, 0.0 = complete failure) - pub score: f64, - /// Weight for composite scoring (default 1.0 for programmatic, 2.0 for LLM) - pub weight: f64, - /// Whether this verifier is required (failure = automatic red confidence) - pub required: bool, - /// Human-readable output/feedback - pub output: String, - /// Structured details (test counts, lint errors, etc.) - pub details: Option, - /// Execution time in milliseconds - pub duration_ms: u64, -} - -impl VerifierResult { - /// Create a passed result with full score. - pub fn passed(name: String, verifier_type: VerifierType, output: String) -> Self { - Self { - name, - verifier_type, - passed: true, - score: 1.0, - weight: 1.0, - required: false, - output, - details: None, - duration_ms: 0, - } - } - - /// Create a failed result with zero score. - pub fn failed(name: String, verifier_type: VerifierType, output: String) -> Self { - Self { - name, - verifier_type, - passed: false, - score: 0.0, - weight: 1.0, - required: false, - output, - details: None, - duration_ms: 0, - } - } - - /// Set the weight for this result. - pub fn with_weight(mut self, weight: f64) -> Self { - self.weight = weight; - self - } - - /// Mark this verifier as required. - pub fn as_required(mut self) -> Self { - self.required = true; - self - } - - /// Set the score explicitly. - pub fn with_score(mut self, score: f64) -> Self { - self.score = score.clamp(0.0, 1.0); - self - } - - /// Set structured details. - pub fn with_details(mut self, details: JsonValue) -> Self { - self.details = Some(details); - self - } - - /// Set execution duration. - pub fn with_duration(mut self, duration_ms: u64) -> Self { - self.duration_ms = duration_ms; - self - } -} - -/// Composite evaluation result combining multiple verifier results. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EvaluationResult { - /// Unique ID for this evaluation - pub id: Uuid, - /// Step ID being evaluated - pub step_id: Uuid, - /// Whether all required verifiers passed - pub passed: bool, - /// Weighted composite score (0.0-1.0) - pub composite_score: f64, - /// Confidence level derived from score - pub confidence_level: ConfidenceLevel, - /// Individual verifier results - pub verifier_results: Vec, - /// Summary feedback for the step - pub summary: String, - /// Rework instructions if failed - pub rework_instructions: Option, - /// Total evaluation duration in milliseconds - pub total_duration_ms: u64, -} - -impl EvaluationResult { - /// Create a new evaluation result from verifier results. - pub fn from_verifiers( - step_id: Uuid, - results: Vec, - green_threshold: f64, - yellow_threshold: f64, - ) -> Self { - let id = Uuid::new_v4(); - - // Check if any required verifier failed - let any_required_failed = results.iter().any(|r| r.required && !r.passed); - - // Calculate weighted composite score - let (total_weighted_score, total_weight) = - results - .iter() - .fold((0.0, 0.0), |(score_acc, weight_acc), r| { - (score_acc + r.score * r.weight, weight_acc + r.weight) - }); - - let composite_score = if total_weight > 0.0 { - total_weighted_score / total_weight - } else { - 0.0 - }; - - // Override confidence to red if any required verifier failed - let confidence_level = if any_required_failed { - ConfidenceLevel::Red - } else { - ConfidenceLevel::from_score(composite_score, green_threshold, yellow_threshold) - }; - - let passed = !any_required_failed && confidence_level != ConfidenceLevel::Red; - - // Generate summary - let passed_count = results.iter().filter(|r| r.passed).count(); - let total_count = results.len(); - let summary = format!( - "{}/{} verifiers passed, composite score: {:.2}, confidence: {}", - passed_count, total_count, composite_score, confidence_level - ); - - // Generate rework instructions if failed - let rework_instructions = if !passed { - let failed_verifiers: Vec<&str> = results - .iter() - .filter(|r| !r.passed) - .map(|r| r.name.as_str()) - .collect(); - Some(format!( - "Fix issues identified by: {}", - failed_verifiers.join(", ") - )) - } else { - None - }; - - let total_duration_ms = results.iter().map(|r| r.duration_ms).sum(); - - Self { - id, - step_id, - passed, - composite_score, - confidence_level, - verifier_results: results, - summary, - rework_instructions, - total_duration_ms, - } - } -} - -/// Error type for verification operations. -#[derive(Error, Debug)] -pub enum VerifierError { - #[error("Command execution failed: {0}")] - CommandFailed(String), - - #[error("Command timed out after {0}ms")] - Timeout(u64), - - #[error("Working directory not found: {0}")] - WorkingDirectoryNotFound(String), - - #[error("Verifier not configured: {0}")] - NotConfigured(String), - - #[error("Parse error: {0}")] - ParseError(String), - - #[error("LLM error: {0}")] - LlmError(String), - - #[error("IO error: {0}")] - Io(#[from] std::io::Error), -} - -/// Information about a verifier for serialization and database storage. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct VerifierInfo { - pub name: String, - pub verifier_type: String, - pub command: String, - pub working_directory: Option, - pub detect_files: Vec, - pub weight: f64, - pub required: bool, -} - -/// Verifier trait for pluggable verification implementations. -#[async_trait] -pub trait Verifier: Send + Sync { - /// Get the name of this verifier. - fn name(&self) -> &str; - - /// Get the type of this verifier. - fn verifier_type(&self) -> VerifierType; - - /// Get serializable info about this verifier. - fn info(&self) -> VerifierInfo; - - /// Check if this verifier is applicable to the given repository. - async fn is_applicable(&self, repo_path: &Path) -> bool; - - /// Run verification and return result. - async fn verify(&self, repo_path: &Path, context: &VerificationContext) - -> Result; -} - -/// Context provided to verifiers during execution. -#[derive(Debug, Clone)] -pub struct VerificationContext { - /// Step ID being verified - pub step_id: Uuid, - /// Contract ID if step has been instantiated - pub contract_id: Option, - /// Files that were modified in this step - pub modified_files: Vec, - /// Step description for LLM context - pub step_description: String, - /// Acceptance criteria for LLM evaluation - pub acceptance_criteria: Vec, - /// Additional context from directive - pub directive_context: String, -} - -/// Command-based verifier for running shell commands. -pub struct CommandVerifier { - name: String, - verifier_type: VerifierType, - command: String, - #[allow(dead_code)] - working_dir: Option, - #[allow(dead_code)] - timeout_ms: u64, - required: bool, - /// Files/patterns that indicate this verifier is applicable - applicable_patterns: Vec, -} - -impl CommandVerifier { - /// Create a new command verifier. - pub fn new( - name: impl Into, - verifier_type: VerifierType, - command: impl Into, - ) -> Self { - Self { - name: name.into(), - verifier_type, - command: command.into(), - working_dir: None, - timeout_ms: 300_000, // 5 minute default - required: false, - applicable_patterns: Vec::new(), - } - } - - /// Set the working directory. - #[allow(dead_code)] - pub fn with_working_dir(mut self, dir: impl Into) -> Self { - self.working_dir = Some(dir.into()); - self - } - - /// Set the timeout in milliseconds. - #[allow(dead_code)] - pub fn with_timeout(mut self, timeout_ms: u64) -> Self { - self.timeout_ms = timeout_ms; - self - } - - /// Mark as required verifier. - pub fn as_required(mut self) -> Self { - self.required = true; - self - } - - /// Add applicability patterns (files that must exist). - pub fn with_patterns(mut self, patterns: Vec) -> Self { - self.applicable_patterns = patterns; - self - } -} - -#[async_trait] -impl Verifier for CommandVerifier { - fn name(&self) -> &str { - &self.name - } - - fn verifier_type(&self) -> VerifierType { - self.verifier_type.clone() - } - - fn info(&self) -> VerifierInfo { - VerifierInfo { - name: self.name.clone(), - verifier_type: self.verifier_type.as_str().to_string(), - command: self.command.clone(), - working_directory: self.working_dir.clone(), - detect_files: self.applicable_patterns.clone(), - weight: 1.0, - required: self.required, - } - } - - async fn is_applicable(&self, repo_path: &Path) -> bool { - if self.applicable_patterns.is_empty() { - return true; - } - - for pattern in &self.applicable_patterns { - let check_path = repo_path.join(pattern); - if check_path.exists() { - return true; - } - } - false - } - - async fn verify( - &self, - repo_path: &Path, - _context: &VerificationContext, - ) -> Result { - let start = std::time::Instant::now(); - - let work_dir = self - .working_dir - .as_ref() - .map(|d| repo_path.join(d)) - .unwrap_or_else(|| repo_path.to_path_buf()); - - if !work_dir.exists() { - return Err(VerifierError::WorkingDirectoryNotFound( - work_dir.display().to_string(), - )); - } - - // Parse command into program and args - let parts: Vec<&str> = self.command.split_whitespace().collect(); - if parts.is_empty() { - return Err(VerifierError::CommandFailed( - "Empty command".to_string(), - )); - } - - let program = parts[0]; - let args = &parts[1..]; - - // Execute command - let output = tokio::process::Command::new(program) - .args(args) - .current_dir(&work_dir) - .output() - .await?; - - let duration_ms = start.elapsed().as_millis() as u64; - let stdout = String::from_utf8_lossy(&output.stdout); - let stderr = String::from_utf8_lossy(&output.stderr); - let combined_output = format!("{}\n{}", stdout, stderr); - - let passed = output.status.success(); - let score = if passed { 1.0 } else { 0.0 }; - - let mut result = VerifierResult { - name: self.name.clone(), - verifier_type: self.verifier_type.clone(), - passed, - score, - weight: 1.0, - required: self.required, - output: combined_output, - details: Some(serde_json::json!({ - "exit_code": output.status.code(), - "command": self.command, - "working_dir": work_dir.display().to_string(), - })), - duration_ms, - }; - - // Try to extract more detailed scoring from output - result = self.enhance_result(result, &stdout); - - Ok(result) - } -} - -impl CommandVerifier { - /// Enhance result with parsed details from output. - fn enhance_result(&self, mut result: VerifierResult, stdout: &str) -> VerifierResult { - match self.verifier_type { - VerifierType::TestRunner => { - // Try to parse test counts from common formats - if let Some((passed, failed, total)) = parse_test_output(stdout) { - result.details = Some(serde_json::json!({ - "tests_passed": passed, - "tests_failed": failed, - "tests_total": total, - "command": self.command, - })); - if total > 0 { - result.score = passed as f64 / total as f64; - } - } - } - VerifierType::Linter => { - // Try to parse lint error counts - if let Some(error_count) = parse_lint_output(stdout) { - result.details = Some(serde_json::json!({ - "errors": error_count, - "command": self.command, - })); - // Score decreases with more errors (up to 10 errors = 0) - result.score = (1.0 - (error_count as f64 / 10.0)).max(0.0); - } - } - _ => {} - } - result - } -} - -/// Parse test output for common formats (Jest, pytest, cargo test). -fn parse_test_output(output: &str) -> Option<(u32, u32, u32)> { - // Jest format: "Tests: X passed, Y failed, Z total" - if let Some(caps) = regex::Regex::new(r"Tests:\s*(\d+)\s*passed,\s*(\d+)\s*failed,\s*(\d+)\s*total") - .ok()? - .captures(output) - { - let passed: u32 = caps.get(1)?.as_str().parse().ok()?; - let failed: u32 = caps.get(2)?.as_str().parse().ok()?; - let total: u32 = caps.get(3)?.as_str().parse().ok()?; - return Some((passed, failed, total)); - } - - // pytest format: "X passed, Y failed" - if let Some(caps) = regex::Regex::new(r"(\d+)\s*passed(?:,\s*(\d+)\s*failed)?") - .ok()? - .captures(output) - { - let passed: u32 = caps.get(1)?.as_str().parse().ok()?; - let failed: u32 = caps.get(2).map(|m| m.as_str().parse().ok()).flatten().unwrap_or(0); - let total = passed + failed; - return Some((passed, failed, total)); - } - - // cargo test format: "test result: ok. X passed; Y failed;" - if let Some(caps) = regex::Regex::new(r"test result:.*?(\d+)\s*passed;\s*(\d+)\s*failed") - .ok()? - .captures(output) - { - let passed: u32 = caps.get(1)?.as_str().parse().ok()?; - let failed: u32 = caps.get(2)?.as_str().parse().ok()?; - let total = passed + failed; - return Some((passed, failed, total)); - } - - None -} - -/// Parse lint output for error counts. -fn parse_lint_output(output: &str) -> Option { - // ESLint format: "X problems (Y errors, Z warnings)" - if let Some(caps) = regex::Regex::new(r"(\d+)\s*problems?\s*\((\d+)\s*errors?") - .ok()? - .captures(output) - { - return caps.get(2)?.as_str().parse().ok(); - } - - // Clippy format: "warning: X warnings emitted" - if let Some(caps) = regex::Regex::new(r"warning:\s*(\d+)\s*warnings?\s*emitted") - .ok()? - .captures(output) - { - return caps.get(1)?.as_str().parse().ok(); - } - - None -} - -/// Auto-detect applicable verifiers for a repository. -pub async fn auto_detect_verifiers(repo_path: &Path) -> Vec> { - let mut verifiers: Vec> = Vec::new(); - - // Check for package.json (Node.js) - let package_json = repo_path.join("package.json"); - if package_json.exists() { - if let Ok(content) = tokio::fs::read_to_string(&package_json).await { - if let Ok(pkg) = serde_json::from_str::(&content) { - if let Some(scripts) = pkg.get("scripts").and_then(|s| s.as_object()) { - // Test runner - if scripts.contains_key("test") { - verifiers.push(Box::new( - CommandVerifier::new("npm-test", VerifierType::TestRunner, "npm test") - .with_patterns(vec!["package.json".to_string()]) - .as_required(), - )); - } - - // Linter - if scripts.contains_key("lint") { - verifiers.push(Box::new( - CommandVerifier::new("npm-lint", VerifierType::Linter, "npm run lint") - .with_patterns(vec!["package.json".to_string()]), - )); - } - - // Build - if scripts.contains_key("build") { - verifiers.push(Box::new( - CommandVerifier::new("npm-build", VerifierType::Build, "npm run build") - .with_patterns(vec!["package.json".to_string()]) - .as_required(), - )); - } - - // Type check (for TypeScript projects) - if scripts.contains_key("typecheck") || scripts.contains_key("type-check") { - let cmd = if scripts.contains_key("typecheck") { - "npm run typecheck" - } else { - "npm run type-check" - }; - verifiers.push(Box::new( - CommandVerifier::new("npm-typecheck", VerifierType::TypeChecker, cmd) - .with_patterns(vec!["tsconfig.json".to_string()]), - )); - } - } - } - } - } - - // Check for Cargo.toml (Rust) - let cargo_toml = repo_path.join("Cargo.toml"); - if cargo_toml.exists() { - verifiers.push(Box::new( - CommandVerifier::new("cargo-test", VerifierType::TestRunner, "cargo test") - .with_patterns(vec!["Cargo.toml".to_string()]) - .as_required(), - )); - - verifiers.push(Box::new( - CommandVerifier::new("cargo-clippy", VerifierType::Linter, "cargo clippy -- -D warnings") - .with_patterns(vec!["Cargo.toml".to_string()]), - )); - - verifiers.push(Box::new( - CommandVerifier::new("cargo-build", VerifierType::Build, "cargo build") - .with_patterns(vec!["Cargo.toml".to_string()]) - .as_required(), - )); - } - - // Check for pyproject.toml or setup.py (Python) - let pyproject = repo_path.join("pyproject.toml"); - let setup_py = repo_path.join("setup.py"); - if pyproject.exists() || setup_py.exists() { - verifiers.push(Box::new( - CommandVerifier::new("pytest", VerifierType::TestRunner, "pytest") - .with_patterns(vec![ - "pyproject.toml".to_string(), - "setup.py".to_string(), - ]) - .as_required(), - )); - - verifiers.push(Box::new( - CommandVerifier::new("ruff", VerifierType::Linter, "ruff check .") - .with_patterns(vec!["pyproject.toml".to_string()]), - )); - } - - verifiers -} - -/// Composite evaluator that runs multiple verifiers and combines results. -pub struct CompositeEvaluator { - verifiers: Vec>, - green_threshold: f64, - yellow_threshold: f64, -} - -impl CompositeEvaluator { - /// Create a new composite evaluator with default thresholds. - pub fn new(verifiers: Vec>) -> Self { - Self { - verifiers, - green_threshold: 0.8, - yellow_threshold: 0.5, - } - } - - /// Set confidence thresholds. - pub fn with_thresholds(mut self, green: f64, yellow: f64) -> Self { - self.green_threshold = green; - self.yellow_threshold = yellow; - self - } - - /// Add a verifier. - pub fn add_verifier(mut self, verifier: Box) -> Self { - self.verifiers.push(verifier); - self - } - - /// Run all applicable verifiers and return composite result. - pub async fn evaluate( - &self, - repo_path: &Path, - context: &VerificationContext, - ) -> EvaluationResult { - let mut results = Vec::new(); - - for verifier in &self.verifiers { - if !verifier.is_applicable(repo_path).await { - continue; - } - - match verifier.verify(repo_path, context).await { - Ok(result) => results.push(result), - Err(e) => { - // Convert error to failed result - results.push(VerifierResult::failed( - verifier.name().to_string(), - verifier.verifier_type(), - format!("Verifier error: {}", e), - )); - } - } - } - - EvaluationResult::from_verifiers( - context.step_id, - results, - self.green_threshold, - self.yellow_threshold, - ) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_confidence_level_from_score() { - assert_eq!( - ConfidenceLevel::from_score(0.9, 0.8, 0.5), - ConfidenceLevel::Green - ); - assert_eq!( - ConfidenceLevel::from_score(0.8, 0.8, 0.5), - ConfidenceLevel::Green - ); - assert_eq!( - ConfidenceLevel::from_score(0.6, 0.8, 0.5), - ConfidenceLevel::Yellow - ); - assert_eq!( - ConfidenceLevel::from_score(0.5, 0.8, 0.5), - ConfidenceLevel::Yellow - ); - assert_eq!( - ConfidenceLevel::from_score(0.4, 0.8, 0.5), - ConfidenceLevel::Red - ); - } - - #[test] - fn test_evaluation_result_composite_score() { - let results = vec![ - VerifierResult::passed("test1".into(), VerifierType::TestRunner, "OK".into()) - .with_weight(1.0), - VerifierResult::failed("test2".into(), VerifierType::Linter, "Failed".into()) - .with_weight(1.0), - ]; - - let eval = EvaluationResult::from_verifiers(Uuid::new_v4(), results, 0.8, 0.5); - assert!((eval.composite_score - 0.5).abs() < 0.001); - assert_eq!(eval.confidence_level, ConfidenceLevel::Yellow); - } - - #[test] - fn test_required_verifier_override() { - let results = vec![ - VerifierResult::passed("test1".into(), VerifierType::TestRunner, "OK".into()), - VerifierResult::failed("build".into(), VerifierType::Build, "Failed".into()) - .as_required(), - ]; - - let eval = EvaluationResult::from_verifiers(Uuid::new_v4(), results, 0.8, 0.5); - // Even though composite score is 0.5, required failure overrides to red - assert_eq!(eval.confidence_level, ConfidenceLevel::Red); - assert!(!eval.passed); - } - - #[test] - fn test_parse_test_output_jest() { - let output = "Tests: 10 passed, 2 failed, 12 total"; - let (passed, failed, total) = parse_test_output(output).unwrap(); - assert_eq!(passed, 10); - assert_eq!(failed, 2); - assert_eq!(total, 12); - } - - #[test] - fn test_parse_test_output_cargo() { - let output = "test result: ok. 25 passed; 0 failed;"; - let (passed, failed, total) = parse_test_output(output).unwrap(); - assert_eq!(passed, 25); - assert_eq!(failed, 0); - assert_eq!(total, 25); - } -} diff --git a/makima/src/server/handlers/contracts.rs b/makima/src/server/handlers/contracts.rs index 8a6ce0f..a83c72d 100644 --- a/makima/src/server/handlers/contracts.rs +++ b/makima/src/server/handlers/contracts.rs @@ -575,90 +575,7 @@ pub async fn update_contract( }), ).await; - // If contract is part of a directive chain step, update the step status - // and emit an event for the directive engine to process - let pool_for_step = pool.clone(); - let contract_id_for_step = contract.id; - tokio::spawn(async move { - // Look up the step by contract_id - match repository::get_step_by_contract_id(&pool_for_step, contract_id_for_step).await { - Ok(Some(step)) => { - // Get the chain to find the directive_id - let directive_id = match repository::get_directive_chain(&pool_for_step, step.chain_id).await { - Ok(Some(chain)) => chain.directive_id, - Ok(None) => { - tracing::warn!( - chain_id = %step.chain_id, - "Chain not found for step" - ); - return; - } - Err(e) => { - tracing::warn!( - chain_id = %step.chain_id, - error = %e, - "Failed to get chain for step" - ); - return; - } - }; - - // Update step status to 'evaluating' - if let Err(e) = repository::update_step_status(&pool_for_step, step.id, "evaluating").await { - tracing::warn!( - step_id = %step.id, - contract_id = %contract_id_for_step, - error = %e, - "Failed to update step status to evaluating" - ); - } else { - tracing::info!( - step_id = %step.id, - contract_id = %contract_id_for_step, - chain_id = %step.chain_id, - directive_id = %directive_id, - "Contract completed - step transitioned to evaluating" - ); - - // Emit directive event for contract completion - if let Err(e) = repository::emit_directive_event( - &pool_for_step, - directive_id, - Some(step.chain_id), - Some(step.id), - "contract_completed", - "info", - Some(serde_json::json!({ - "contract_id": contract_id_for_step, - "step_id": step.id, - "step_name": step.name - })), - "system", - None, - ).await { - tracing::warn!( - step_id = %step.id, - error = %e, - "Failed to emit contract_completed directive event" - ); - } - } - } - Ok(None) => { - tracing::debug!( - contract_id = %contract_id_for_step, - "Contract not linked to any directive chain step" - ); - } - Err(e) => { - tracing::warn!( - contract_id = %contract_id_for_step, - error = %e, - "Failed to look up step for completed contract" - ); - } - } - }); + // TODO: Directive engine integration (removed for reimplementation) } // Get summary with counts diff --git a/makima/src/server/handlers/directives.rs b/makima/src/server/handlers/directives.rs deleted file mode 100644 index 9c65c5e..0000000 --- a/makima/src/server/handlers/directives.rs +++ /dev/null @@ -1,2116 +0,0 @@ -//! API handlers for directives. -//! -//! Provides REST endpoints for managing directives, chains, steps, -//! evaluations, events, verifiers, and approvals. - -use axum::{ - extract::{Path, Query, State}, - http::StatusCode, - response::{ - sse::{Event, Sse}, - IntoResponse, - }, - Json, -}; -use futures::stream; -use serde::{Deserialize, Serialize}; -use std::convert::Infallible; -use std::time::Duration; -use uuid::Uuid; - -use crate::db::models::{ - AddStepRequest, CreateDirectiveRequest, CreateVerifierRequest, ReworkStepRequest, - UpdateCriteriaRequest, UpdateDirectiveRequest, UpdateRequirementsRequest, UpdateStepRequest, - UpdateVerifierRequest, -}; -use crate::db::repository; -use crate::server::auth::Authenticated; -use crate::server::messages::ApiError; -use crate::server::state::SharedState; - -/// Query parameters for listing directives -#[derive(Debug, Deserialize)] -pub struct ListDirectivesQuery { - pub status: Option, -} - -/// Query parameters for listing events -#[derive(Debug, Deserialize)] -pub struct ListEventsQuery { - pub limit: Option, -} - -/// Query parameters for SSE stream authentication -/// EventSource API cannot set custom headers, so auth is passed via query params -#[derive(Debug, Deserialize)] -pub struct StreamAuthQuery { - pub token: Option, - pub api_key: Option, -} - -/// Query parameters for listing evaluations -#[derive(Debug, Deserialize)] -pub struct ListEvaluationsQuery { - pub limit: Option, - #[serde(rename = "stepId")] - pub step_id: Option, -} - -/// Response for directive creation -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct CreateDirectiveResponse { - pub id: Uuid, - pub title: String, - pub status: String, -} - -/// Response for approval actions -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ApprovalActionRequest { - pub response: Option, -} - -// ============================================================================= -// Directive CRUD -// ============================================================================= - -/// Create a new directive -/// POST /api/v1/directives -pub async fn create_directive( - State(state): State, - Authenticated(auth): Authenticated, - Json(req): Json, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - match repository::create_directive_for_owner(pool, auth.owner_id, req).await { - Ok(directive) => Json(CreateDirectiveResponse { - id: directive.id, - title: directive.title, - status: directive.status, - }) - .into_response(), - Err(e) => { - tracing::error!("Failed to create directive: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// List directives for the authenticated owner -/// GET /api/v1/directives -pub async fn list_directives( - State(state): State, - Authenticated(auth): Authenticated, - Query(params): Query, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - match repository::list_directives_for_owner(pool, auth.owner_id, params.status.as_deref()).await - { - Ok(directives) => { - let total = directives.len() as i64; - Json(serde_json::json!({ - "directives": directives, - "total": total, - })) - .into_response() - } - Err(e) => { - tracing::error!("Failed to list directives: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Get a directive with progress details -/// GET /api/v1/directives/:id -pub async fn get_directive( - State(state): State, - Authenticated(auth): Authenticated, - Path(id): Path, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - match repository::get_directive_with_progress(pool, id, auth.owner_id).await { - Ok(Some(directive)) => Json(directive).into_response(), - Ok(None) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response(), - Err(e) => { - tracing::error!("Failed to get directive: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Update a directive -/// PUT /api/v1/directives/:id -pub async fn update_directive( - State(state): State, - Authenticated(auth): Authenticated, - Path(id): Path, - Json(req): Json, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - match repository::update_directive_for_owner(pool, id, auth.owner_id, req).await { - Ok(directive) => Json(directive).into_response(), - Err(repository::RepositoryError::VersionConflict { expected, actual }) => ( - StatusCode::CONFLICT, - Json(ApiError::new( - "VERSION_CONFLICT", - &format!( - "Version conflict: expected {}, got {}", - expected, actual - ), - )), - ) - .into_response(), - Err(e) => { - tracing::error!("Failed to update directive: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Archive a directive -/// DELETE /api/v1/directives/:id -pub async fn archive_directive( - State(state): State, - Authenticated(auth): Authenticated, - Path(id): Path, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - match repository::archive_directive_for_owner(pool, id, auth.owner_id).await { - Ok(true) => StatusCode::NO_CONTENT.into_response(), - Ok(false) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response(), - Err(e) => { - tracing::error!("Failed to archive directive: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -// ============================================================================= -// Directive Lifecycle -// ============================================================================= - -/// Start a directive (generate chain and begin execution) -/// POST /api/v1/directives/:id/start -pub async fn start_directive( - State(state): State, - Authenticated(auth): Authenticated, - Path(id): Path, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - // Start directive via orchestration engine - let engine = crate::orchestration::DirectiveEngine::new(pool.clone()); - match engine.start_directive(id).await { - Ok(planning) => { - // Auto-start the planning task on an available daemon - if let Some(daemon_id) = state.find_alternative_daemon(auth.owner_id, &[]) { - // Update task status to "starting" and assign daemon - let update_req = crate::db::models::UpdateTaskRequest { - status: Some("starting".to_string()), - daemon_id: Some(daemon_id), - ..Default::default() - }; - if let Err(e) = repository::update_task_for_owner( - pool, - planning.task_id, - auth.owner_id, - update_req, - ) - .await - { - tracing::warn!("Failed to update planning task status: {}", e); - } - - let command = crate::server::state::DaemonCommand::SpawnTask { - task_id: planning.task_id, - task_name: planning.task_name, - plan: planning.plan, - repo_url: planning.repository_url, - base_branch: planning.base_branch, - target_branch: None, - parent_task_id: None, - depth: 0, - is_orchestrator: false, - target_repo_path: None, - completion_action: Some("none".to_string()), - continue_from_task_id: None, - copy_files: None, - contract_id: Some(planning.contract_id), - is_supervisor: true, - autonomous_loop: false, - resume_session: false, - conversation_history: None, - patch_data: None, - patch_base_sha: None, - local_only: false, - auto_merge_local: false, - supervisor_worktree_task_id: None, - }; - - if let Err(e) = state.send_daemon_command(daemon_id, command).await { - tracing::warn!( - "Failed to auto-start planning task on daemon {}: {}", - daemon_id, - e - ); - } else { - tracing::info!( - "Auto-started planning task {} on daemon {} for directive {}", - planning.task_id, - daemon_id, - id - ); - } - } else { - tracing::warn!( - "No daemon available to auto-start planning task for directive {}", - id - ); - } - - // Return the updated directive with progress - match repository::get_directive_with_progress(pool, id, auth.owner_id).await { - Ok(Some(directive)) => Json(directive).into_response(), - Ok(None) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response(), - Err(e) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response(), - } - } - Err(e) => { - tracing::error!("Failed to start directive: {}", e); - ( - StatusCode::BAD_REQUEST, - Json(ApiError::new("START_FAILED", &e.to_string())), - ) - .into_response() - } - } -} - -/// Pause a directive -/// POST /api/v1/directives/:id/pause -pub async fn pause_directive( - State(state): State, - Authenticated(auth): Authenticated, - Path(id): Path, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - let engine = crate::orchestration::DirectiveEngine::new(pool.clone()); - match engine.pause_directive(id).await { - Ok(()) => match repository::get_directive(pool, id).await { - Ok(Some(directive)) => Json(directive).into_response(), - Ok(None) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response(), - Err(e) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response(), - }, - Err(e) => { - tracing::error!("Failed to pause directive: {}", e); - ( - StatusCode::BAD_REQUEST, - Json(ApiError::new("PAUSE_FAILED", &e.to_string())), - ) - .into_response() - } - } -} - -/// Resume a paused directive -/// POST /api/v1/directives/:id/resume -pub async fn resume_directive( - State(state): State, - Authenticated(auth): Authenticated, - Path(id): Path, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - let engine = crate::orchestration::DirectiveEngine::new(pool.clone()); - match engine.resume_directive(id).await { - Ok(()) => match repository::get_directive_with_progress(pool, id, auth.owner_id).await { - Ok(Some(directive)) => Json(directive).into_response(), - Ok(None) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response(), - Err(e) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response(), - }, - Err(e) => { - tracing::error!("Failed to resume directive: {}", e); - ( - StatusCode::BAD_REQUEST, - Json(ApiError::new("RESUME_FAILED", &e.to_string())), - ) - .into_response() - } - } -} - -/// Stop a directive (cannot be resumed) -/// POST /api/v1/directives/:id/stop -pub async fn stop_directive( - State(state): State, - Authenticated(auth): Authenticated, - Path(id): Path, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - let engine = crate::orchestration::DirectiveEngine::new(pool.clone()); - match engine.stop_directive(id).await { - Ok(()) => match repository::get_directive(pool, id).await { - Ok(Some(directive)) => Json(directive).into_response(), - Ok(None) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response(), - Err(e) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response(), - }, - Err(e) => { - tracing::error!("Failed to stop directive: {}", e); - ( - StatusCode::BAD_REQUEST, - Json(ApiError::new("STOP_FAILED", &e.to_string())), - ) - .into_response() - } - } -} - -// ============================================================================= -// Chain Management -// ============================================================================= - -/// Get current chain for a directive -/// GET /api/v1/directives/:id/chain -pub async fn get_chain( - State(state): State, - Authenticated(auth): Authenticated, - Path(id): Path, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - match repository::get_current_chain(pool, id).await { - Ok(Some(chain)) => { - match repository::list_chain_steps(pool, chain.id).await { - Ok(steps) => Json(serde_json::json!({ - "chain": chain, - "steps": steps, - })) - .into_response(), - Err(e) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response(), - } - } - Ok(None) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "No active chain")), - ) - .into_response(), - Err(e) => { - tracing::error!("Failed to get chain: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Get chain graph for DAG visualization -/// GET /api/v1/directives/:id/chain/graph -pub async fn get_chain_graph( - State(state): State, - Authenticated(auth): Authenticated, - Path(id): Path, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - // Get current chain - let chain = match repository::get_current_chain(pool, id).await { - Ok(Some(chain)) => chain, - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "No active chain")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - }; - - match repository::get_chain_graph(pool, chain.id).await { - Ok(graph) => Json(graph).into_response(), - Err(e) => { - tracing::error!("Failed to get chain graph: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Regenerate chain (force replan) -/// POST /api/v1/directives/:id/chain/replan -pub async fn replan_chain( - State(state): State, - Authenticated(auth): Authenticated, - Path(id): Path, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - let engine = crate::orchestration::DirectiveEngine::new(pool.clone()); - match engine.regenerate_chain(id, "Manual replan requested").await { - Ok(new_chain_id) => Json(serde_json::json!({ - "chainId": new_chain_id, - "message": "Chain regenerated successfully", - })) - .into_response(), - Err(e) => { - tracing::error!("Failed to replan chain: {}", e); - ( - StatusCode::BAD_REQUEST, - Json(ApiError::new("REPLAN_FAILED", &e.to_string())), - ) - .into_response() - } - } -} - -// ============================================================================= -// Step Management -// ============================================================================= - -/// Add a step to the current chain -/// POST /api/v1/directives/:id/chain/steps -pub async fn add_step( - State(state): State, - Authenticated(auth): Authenticated, - Path(id): Path, - Json(req): Json, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - // Get current chain - let chain = match repository::get_current_chain(pool, id).await { - Ok(Some(chain)) => chain, - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "No active chain")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - }; - - match repository::create_chain_step(pool, chain.id, req).await { - Ok(step) => (StatusCode::CREATED, Json(step)).into_response(), - Err(e) => { - tracing::error!("Failed to add step: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Get step details -/// GET /api/v1/directives/:id/chain/steps/:step_id -pub async fn get_step( - State(state): State, - Authenticated(auth): Authenticated, - Path((id, step_id)): Path<(Uuid, Uuid)>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - match repository::get_chain_step(pool, step_id).await { - Ok(Some(step)) => Json(step).into_response(), - Ok(None) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Step not found")), - ) - .into_response(), - Err(e) => { - tracing::error!("Failed to get step: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Update a step -/// PUT /api/v1/directives/:id/chain/steps/:step_id -pub async fn update_step( - State(state): State, - Authenticated(auth): Authenticated, - Path((id, step_id)): Path<(Uuid, Uuid)>, - Json(req): Json, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - match repository::update_chain_step(pool, step_id, req).await { - Ok(step) => Json(step).into_response(), - Err(e) => { - tracing::error!("Failed to update step: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Delete a step -/// DELETE /api/v1/directives/:id/chain/steps/:step_id -pub async fn delete_step( - State(state): State, - Authenticated(auth): Authenticated, - Path((id, step_id)): Path<(Uuid, Uuid)>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - match repository::delete_chain_step(pool, step_id).await { - Ok(true) => StatusCode::NO_CONTENT.into_response(), - Ok(false) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Step not found")), - ) - .into_response(), - Err(e) => { - tracing::error!("Failed to delete step: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Skip a step -/// POST /api/v1/directives/:id/chain/steps/:step_id/skip -pub async fn skip_step( - State(state): State, - Authenticated(auth): Authenticated, - Path((id, step_id)): Path<(Uuid, Uuid)>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - match repository::update_step_status(pool, step_id, "skipped").await { - Ok(step) => Json(step).into_response(), - Err(e) => { - tracing::error!("Failed to skip step: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -// ============================================================================= -// Evaluations -// ============================================================================= - -/// List evaluations for a directive -/// GET /api/v1/directives/:id/evaluations -pub async fn list_evaluations( - State(state): State, - Authenticated(auth): Authenticated, - Path(id): Path, - Query(params): Query, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - let result = if let Some(step_id) = params.step_id { - repository::list_step_evaluations(pool, step_id).await - } else { - repository::list_directive_evaluations(pool, id, params.limit).await - }; - - match result { - Ok(evaluations) => Json(evaluations).into_response(), - Err(e) => { - tracing::error!("Failed to list evaluations: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -// ============================================================================= -// Events -// ============================================================================= - -/// List events for a directive -/// GET /api/v1/directives/:id/events -pub async fn list_events( - State(state): State, - Authenticated(auth): Authenticated, - Path(id): Path, - Query(params): Query, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - match repository::list_directive_events(pool, id, params.limit).await { - Ok(events) => Json(events).into_response(), - Err(e) => { - tracing::error!("Failed to list events: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// SSE stream of events for a directive -/// GET /api/v1/directives/:id/events/stream -/// -/// EventSource API cannot set custom headers, so authentication is accepted -/// via query parameters: ?token= or ?api_key= -pub async fn stream_events( - State(state): State, - Path(id): Path, - Query(auth_params): Query, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Authenticate via query params (EventSource cannot set headers) - let auth = if let Some(ref token) = auth_params.token { - // JWT token - let verifier = match state.jwt_verifier.as_ref() { - Some(v) => v, - None => { - return ( - StatusCode::UNAUTHORIZED, - Json(ApiError::new("AUTH_NOT_CONFIGURED", "Authentication not configured")), - ) - .into_response() - } - }; - let claims = match verifier.verify(token) { - Ok(c) => c, - Err(_) => { - return ( - StatusCode::UNAUTHORIZED, - Json(ApiError::new("INVALID_TOKEN", "Invalid authentication token")), - ) - .into_response() - } - }; - match crate::server::auth::resolve_owner_id_public(pool, claims.sub, claims.email.as_deref()).await { - Ok(owner_id) => crate::server::auth::AuthenticatedUser { - user_id: claims.sub, - owner_id, - auth_source: crate::server::auth::AuthSource::Jwt, - email: claims.email, - }, - Err(_) => { - return ( - StatusCode::UNAUTHORIZED, - Json(ApiError::new("USER_NOT_FOUND", "User not found")), - ) - .into_response() - } - } - } else if let Some(ref api_key) = auth_params.api_key { - // API key - match crate::server::auth::validate_api_key_public(pool, api_key).await { - Ok((user_id, owner_id)) => crate::server::auth::AuthenticatedUser { - user_id, - owner_id, - auth_source: crate::server::auth::AuthSource::ApiKey, - email: None, - }, - Err(_) => { - return ( - StatusCode::UNAUTHORIZED, - Json(ApiError::new("INVALID_API_KEY", "Invalid or revoked API key")), - ) - .into_response() - } - } - } else { - return ( - StatusCode::UNAUTHORIZED, - Json(ApiError::new("MISSING_TOKEN", "Authentication required via ?token= or ?api_key= query parameter")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - // Create SSE stream that polls for new events - let pool_clone = pool.clone(); - let stream = stream::unfold( - (pool_clone, id, None::>), - move |(pool, directive_id, last_seen)| async move { - // Wait a bit before next poll - tokio::time::sleep(Duration::from_secs(1)).await; - - // Get recent events - let events = repository::list_directive_events(&pool, directive_id, Some(10)) - .await - .unwrap_or_default(); - - // Filter to only new events - let new_events: Vec<_> = events - .into_iter() - .filter(|e| last_seen.map(|ls| e.created_at > ls).unwrap_or(true)) - .collect(); - - let new_last_seen = new_events.first().map(|e| e.created_at).or(last_seen); - - // Convert to SSE events - let sse_events: Vec> = new_events - .into_iter() - .map(|e| { - Ok(Event::default() - .event("directive_event") - .data(serde_json::to_string(&e).unwrap_or_default())) - }) - .collect(); - - Some((stream::iter(sse_events), (pool, directive_id, new_last_seen))) - }, - ); - - use futures::StreamExt; - Sse::new(stream.flatten()) - .keep_alive( - axum::response::sse::KeepAlive::new() - .interval(Duration::from_secs(15)) - .text("keepalive"), - ) - .into_response() -} - -// ============================================================================= -// Verifiers -// ============================================================================= - -/// List verifiers for a directive -/// GET /api/v1/directives/:id/verifiers -pub async fn list_verifiers( - State(state): State, - Authenticated(auth): Authenticated, - Path(id): Path, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - match repository::list_directive_verifiers(pool, id).await { - Ok(verifiers) => Json(verifiers).into_response(), - Err(e) => { - tracing::error!("Failed to list verifiers: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Add a verifier to a directive -/// POST /api/v1/directives/:id/verifiers -pub async fn add_verifier( - State(state): State, - Authenticated(auth): Authenticated, - Path(id): Path, - Json(req): Json, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - match repository::create_directive_verifier( - pool, - id, - &req.name, - &req.verifier_type, - req.command.as_deref(), - req.working_directory.as_deref(), - false, // auto_detect - vec![], // detect_files - req.weight.unwrap_or(1.0), - req.required.unwrap_or(false), - ) - .await - { - Ok(verifier) => (StatusCode::CREATED, Json(verifier)).into_response(), - Err(e) => { - tracing::error!("Failed to add verifier: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Update a verifier -/// PUT /api/v1/directives/:id/verifiers/:verifier_id -pub async fn update_verifier( - State(state): State, - Authenticated(auth): Authenticated, - Path((id, verifier_id)): Path<(Uuid, Uuid)>, - Json(req): Json, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - match repository::update_directive_verifier( - pool, - verifier_id, - req.enabled, - req.command.as_deref(), - req.weight, - req.required, - ) - .await - { - Ok(verifier) => Json(verifier).into_response(), - Err(e) => { - tracing::error!("Failed to update verifier: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -// ============================================================================= -// Approvals -// ============================================================================= - -/// List pending approvals for a directive -/// GET /api/v1/directives/:id/approvals -pub async fn list_approvals( - State(state): State, - Authenticated(auth): Authenticated, - Path(id): Path, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - match repository::list_pending_approvals(pool, id).await { - Ok(approvals) => Json(approvals).into_response(), - Err(e) => { - tracing::error!("Failed to list approvals: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Approve a pending approval request -/// POST /api/v1/directives/:id/approvals/:approval_id/approve -pub async fn approve_request( - State(state): State, - Authenticated(auth): Authenticated, - Path((id, approval_id)): Path<(Uuid, Uuid)>, - Json(req): Json, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - let engine = crate::orchestration::DirectiveEngine::new(pool.clone()); - match engine - .on_approval_resolved(approval_id, true, auth.owner_id) - .await - { - Ok(()) => { - match repository::resolve_approval( - pool, - approval_id, - "approved", - req.response.as_deref(), - auth.owner_id, - ) - .await - { - Ok(approval) => Json(approval).into_response(), - Err(e) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response(), - } - } - Err(e) => { - tracing::error!("Failed to process approval: {}", e); - ( - StatusCode::BAD_REQUEST, - Json(ApiError::new("APPROVAL_FAILED", &e.to_string())), - ) - .into_response() - } - } -} - -/// Deny a pending approval request -/// POST /api/v1/directives/:id/approvals/:approval_id/deny -pub async fn deny_request( - State(state): State, - Authenticated(auth): Authenticated, - Path((id, approval_id)): Path<(Uuid, Uuid)>, - Json(req): Json, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - let engine = crate::orchestration::DirectiveEngine::new(pool.clone()); - match engine - .on_approval_resolved(approval_id, false, auth.owner_id) - .await - { - Ok(()) => { - match repository::resolve_approval( - pool, - approval_id, - "denied", - req.response.as_deref(), - auth.owner_id, - ) - .await - { - Ok(approval) => Json(approval).into_response(), - Err(e) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response(), - } - } - Err(e) => { - tracing::error!("Failed to process denial: {}", e); - ( - StatusCode::BAD_REQUEST, - Json(ApiError::new("DENIAL_FAILED", &e.to_string())), - ) - .into_response() - } - } -} - -// ============================================================================= -// Step Evaluation & Rework -// ============================================================================= - -/// Force re-evaluation of a step -/// POST /api/v1/directives/:id/steps/:step_id/evaluate -pub async fn evaluate_step( - State(state): State, - Authenticated(auth): Authenticated, - Path((id, step_id)): Path<(Uuid, Uuid)>, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - // Set step to evaluating status - match repository::update_step_status(pool, step_id, "evaluating").await { - Ok(_) => {} - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - // Trigger evaluation via engine - let engine = crate::orchestration::DirectiveEngine::new(pool.clone()); - match engine.on_contract_completed(step_id).await { - Ok(()) => { - // Return updated step - match repository::get_chain_step(pool, step_id).await { - Ok(Some(step)) => Json(step).into_response(), - Ok(None) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Step not found")), - ) - .into_response(), - Err(e) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response(), - } - } - Err(e) => { - tracing::error!("Failed to evaluate step: {}", e); - ( - StatusCode::BAD_REQUEST, - Json(ApiError::new("EVALUATE_FAILED", &e.to_string())), - ) - .into_response() - } - } -} - -/// Trigger manual rework for a step -/// POST /api/v1/directives/:id/steps/:step_id/rework -pub async fn rework_step( - State(state): State, - Authenticated(auth): Authenticated, - Path((id, step_id)): Path<(Uuid, Uuid)>, - Json(req): Json, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Verify ownership - match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(_)) => {} - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - // Set step to rework status and increment rework count - match repository::update_step_status(pool, step_id, "rework").await { - Ok(_) => {} - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - - let _ = repository::increment_step_rework_count(pool, step_id).await; - - // Emit rework event - let _ = repository::emit_directive_event( - pool, - id, - None, - Some(step_id), - "step_rework", - "info", - Some(serde_json::json!({ - "step_id": step_id, - "instructions": req.instructions, - "initiated_by": "user", - })), - "user", - Some(auth.owner_id), - ) - .await; - - // Return updated step - match repository::get_chain_step(pool, step_id).await { - Ok(Some(step)) => Json(step).into_response(), - Ok(None) => ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Step not found")), - ) - .into_response(), - Err(e) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response(), - } -} - -// ============================================================================= -// Auto-detect Verifiers -// ============================================================================= - -/// Auto-detect verifiers for a directive based on repository content -/// POST /api/v1/directives/:id/verifiers/auto-detect -pub async fn auto_detect_verifiers( - State(state): State, - Authenticated(auth): Authenticated, - Path(id): Path, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Get directive with ownership check - let directive = match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(d)) => d, - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - }; - - // Get repository path - let repo_path = directive - .local_path - .as_ref() - .map(std::path::PathBuf::from) - .unwrap_or_else(|| std::path::PathBuf::from(".")); - - // Auto-detect verifiers - let detected = crate::orchestration::auto_detect_verifiers(&repo_path).await; - - // Save detected verifiers to the database - let mut created = Vec::new(); - for verifier in &detected { - let info = verifier.info(); - match repository::create_directive_verifier( - pool, - id, - &info.name, - &info.verifier_type, - Some(&info.command), - info.working_directory.as_deref(), - true, // auto_detect - info.detect_files.clone(), - info.weight, - info.required, - ) - .await - { - Ok(v) => created.push(v), - Err(e) => { - tracing::warn!("Failed to create detected verifier '{}': {}", info.name, e); - } - } - } - - Json(serde_json::json!({ - "detected": created.len(), - "verifiers": created, - })) - .into_response() -} - -// ============================================================================= -// Requirements & Criteria -// ============================================================================= - -/// Update directive requirements -/// PUT /api/v1/directives/:id/requirements -pub async fn update_requirements( - State(state): State, - Authenticated(auth): Authenticated, - Path(id): Path, - Json(req): Json, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Get directive with ownership check to get current version - let directive = match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(d)) => d, - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - }; - - // Build update request with just requirements - let update = UpdateDirectiveRequest { - title: None, - goal: None, - requirements: Some(serde_json::to_value(&req.requirements).unwrap_or_default()), - acceptance_criteria: None, - constraints: None, - external_dependencies: None, - autonomy_level: None, - confidence_threshold_green: None, - confidence_threshold_yellow: None, - max_total_cost_usd: None, - max_wall_time_minutes: None, - max_rework_cycles: None, - max_chain_regenerations: None, - version: directive.version, - }; - - match repository::update_directive_for_owner(pool, id, auth.owner_id, update).await { - Ok(directive) => Json(directive).into_response(), - Err(repository::RepositoryError::VersionConflict { expected, actual }) => ( - StatusCode::CONFLICT, - Json(ApiError::new( - "VERSION_CONFLICT", - &format!("Version conflict: expected {}, got {}", expected, actual), - )), - ) - .into_response(), - Err(e) => { - tracing::error!("Failed to update requirements: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -/// Update directive acceptance criteria -/// PUT /api/v1/directives/:id/criteria -pub async fn update_criteria( - State(state): State, - Authenticated(auth): Authenticated, - Path(id): Path, - Json(req): Json, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Get directive with ownership check to get current version - let directive = match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(d)) => d, - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - }; - - // Build update request with just acceptance criteria - let update = UpdateDirectiveRequest { - title: None, - goal: None, - requirements: None, - acceptance_criteria: Some( - serde_json::to_value(&req.acceptance_criteria).unwrap_or_default(), - ), - constraints: None, - external_dependencies: None, - autonomy_level: None, - confidence_threshold_green: None, - confidence_threshold_yellow: None, - max_total_cost_usd: None, - max_wall_time_minutes: None, - max_rework_cycles: None, - max_chain_regenerations: None, - version: directive.version, - }; - - match repository::update_directive_for_owner(pool, id, auth.owner_id, update).await { - Ok(directive) => Json(directive).into_response(), - Err(repository::RepositoryError::VersionConflict { expected, actual }) => ( - StatusCode::CONFLICT, - Json(ApiError::new( - "VERSION_CONFLICT", - &format!("Version conflict: expected {}, got {}", expected, actual), - )), - ) - .into_response(), - Err(e) => { - tracing::error!("Failed to update criteria: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } -} - -// ============================================================================= -// Spec Generation -// ============================================================================= - -/// Generate a specification from the directive's goal using LLM -/// POST /api/v1/directives/:id/generate-spec -pub async fn generate_spec( - State(state): State, - Authenticated(auth): Authenticated, - Path(id): Path, -) -> impl IntoResponse { - let Some(ref pool) = state.db_pool else { - return ( - StatusCode::SERVICE_UNAVAILABLE, - Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), - ) - .into_response(); - }; - - // Get directive with ownership check - let directive = match repository::get_directive_for_owner(pool, id, auth.owner_id).await { - Ok(Some(d)) => d, - Ok(None) => { - return ( - StatusCode::NOT_FOUND, - Json(ApiError::new("NOT_FOUND", "Directive not found")), - ) - .into_response() - } - Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - }; - - // Use the planner to generate a spec from the goal - let planner = crate::orchestration::ChainPlanner::new(pool.clone()); - match planner.generate_spec(&directive).await { - Ok(spec) => { - // Update the directive with the generated spec - let update = UpdateDirectiveRequest { - title: spec.title, - goal: None, - requirements: Some(spec.requirements), - acceptance_criteria: Some(spec.acceptance_criteria), - constraints: spec.constraints, - external_dependencies: None, - autonomy_level: None, - confidence_threshold_green: None, - confidence_threshold_yellow: None, - max_total_cost_usd: None, - max_wall_time_minutes: None, - max_rework_cycles: None, - max_chain_regenerations: None, - version: directive.version, - }; - - match repository::update_directive_for_owner(pool, id, auth.owner_id, update).await { - Ok(updated) => Json(updated).into_response(), - Err(e) => { - tracing::error!("Failed to save generated spec: {}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ApiError::new("DB_ERROR", &e.to_string())), - ) - .into_response() - } - } - } - Err(e) => { - tracing::error!("Failed to generate spec: {}", e); - ( - StatusCode::BAD_REQUEST, - Json(ApiError::new("SPEC_GENERATION_FAILED", &e.to_string())), - ) - .into_response() - } - } -} diff --git a/makima/src/server/handlers/mesh_daemon.rs b/makima/src/server/handlers/mesh_daemon.rs index 9938145..beb4c15 100644 --- a/makima/src/server/handlers/mesh_daemon.rs +++ b/makima/src/server/handlers/mesh_daemon.rs @@ -1303,20 +1303,7 @@ async fn handle_daemon_connection(socket: WebSocket, state: SharedState, auth_re }), ).await; - // Check if this task's contract is a directive orchestrator - if let Some(contract_id) = updated_task.contract_id { - if let Ok(Some(directive)) = repository::get_directive_by_orchestrator_contract_id( - &pool, contract_id - ).await { - let engine = crate::orchestration::DirectiveEngine::new(pool.clone()); - if let Err(e) = engine.on_planning_complete(directive.id, success).await { - tracing::error!( - "Failed to handle planning completion for directive {}: {}", - directive.id, e - ); - } - } - } + // TODO: Directive engine integration (removed for reimplementation) } Ok(None) => { tracing::warn!( diff --git a/makima/src/server/handlers/mod.rs b/makima/src/server/handlers/mod.rs index d3fabf7..ae370c9 100644 --- a/makima/src/server/handlers/mod.rs +++ b/makima/src/server/handlers/mod.rs @@ -1,8 +1,6 @@ //! HTTP and WebSocket request handlers. pub mod api_keys; -// pub mod chains; // Removed - replaced by directives -pub mod directives; pub mod chat; pub mod contract_chat; pub mod contract_daemon; diff --git a/makima/src/server/mod.rs b/makima/src/server/mod.rs index 463a5f5..b7a4156 100644 --- a/makima/src/server/mod.rs +++ b/makima/src/server/mod.rs @@ -18,7 +18,7 @@ use tower_http::trace::TraceLayer; use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; -use crate::server::handlers::{api_keys, chat, contract_chat, contract_daemon, contract_discuss, contracts, directives, file_ws, files, history, listen, mesh, mesh_chat, mesh_daemon, mesh_merge, mesh_supervisor, mesh_ws, repository_history, speak, templates, transcript_analysis, users, versions}; +use crate::server::handlers::{api_keys, chat, contract_chat, contract_daemon, contract_discuss, contracts, file_ws, files, history, listen, mesh, mesh_chat, mesh_daemon, mesh_merge, mesh_supervisor, mesh_ws, repository_history, speak, templates, transcript_analysis, users, versions}; use crate::server::openapi::ApiDoc; use crate::server::state::SharedState; @@ -214,61 +214,6 @@ pub fn make_router(state: SharedState) -> Router { ) // Timeline endpoint (unified history for user) .route("/timeline", get(history::get_timeline)) - // Directive endpoints (replacement for chains) - .route( - "/directives", - get(directives::list_directives).post(directives::create_directive), - ) - .route( - "/directives/{id}", - get(directives::get_directive) - .put(directives::update_directive) - .delete(directives::archive_directive), - ) - .route("/directives/{id}/start", post(directives::start_directive)) - .route("/directives/{id}/pause", post(directives::pause_directive)) - .route("/directives/{id}/resume", post(directives::resume_directive)) - .route("/directives/{id}/stop", post(directives::stop_directive)) - .route("/directives/{id}/requirements", axum::routing::put(directives::update_requirements)) - .route("/directives/{id}/criteria", axum::routing::put(directives::update_criteria)) - .route("/directives/{id}/generate-spec", post(directives::generate_spec)) - // Directive chain management - .route("/directives/{id}/chain", get(directives::get_chain)) - .route("/directives/{id}/chain/graph", get(directives::get_chain_graph)) - .route("/directives/{id}/chain/replan", post(directives::replan_chain)) - // Directive step management - .route( - "/directives/{id}/chain/steps", - post(directives::add_step), - ) - .route( - "/directives/{id}/chain/steps/{step_id}", - get(directives::get_step) - .put(directives::update_step) - .delete(directives::delete_step), - ) - .route("/directives/{id}/chain/steps/{step_id}/skip", post(directives::skip_step)) - .route("/directives/{id}/chain/steps/{step_id}/evaluate", post(directives::evaluate_step)) - .route("/directives/{id}/chain/steps/{step_id}/rework", post(directives::rework_step)) - // Directive evaluations - .route("/directives/{id}/evaluations", get(directives::list_evaluations)) - // Directive events - .route("/directives/{id}/events", get(directives::list_events)) - .route("/directives/{id}/events/stream", get(directives::stream_events)) - // Directive verifiers - .route("/directives/{id}/verifiers/auto-detect", post(directives::auto_detect_verifiers)) - .route( - "/directives/{id}/verifiers", - get(directives::list_verifiers).post(directives::add_verifier), - ) - .route( - "/directives/{id}/verifiers/{verifier_id}", - axum::routing::put(directives::update_verifier), - ) - // Directive approvals - .route("/directives/{id}/approvals", get(directives::list_approvals)) - .route("/directives/{id}/approvals/{approval_id}/approve", post(directives::approve_request)) - .route("/directives/{id}/approvals/{approval_id}/deny", post(directives::deny_request)) // Contract type templates (built-in only) .route("/contract-types", get(templates::list_contract_types)) // Settings endpoints -- cgit v1.2.3