summaryrefslogtreecommitdiff
path: root/makima/src
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src')
-rw-r--r--makima/src/bin/makima.rs151
-rw-r--r--makima/src/daemon/api/directive.rs447
-rw-r--r--makima/src/daemon/api/mod.rs1
-rw-r--r--makima/src/daemon/cli/directive.rs186
-rw-r--r--makima/src/daemon/cli/mod.rs56
-rw-r--r--makima/src/daemon/skills/directive.md303
-rw-r--r--makima/src/daemon/skills/mod.rs4
-rw-r--r--makima/src/db/models.rs666
-rw-r--r--makima/src/db/repository.rs1169
-rw-r--r--makima/src/lib.rs1
-rw-r--r--makima/src/orchestration/engine.rs1335
-rw-r--r--makima/src/orchestration/mod.rs26
-rw-r--r--makima/src/orchestration/planner.rs848
-rw-r--r--makima/src/orchestration/verifier.rs833
-rw-r--r--makima/src/server/handlers/contracts.rs85
-rw-r--r--makima/src/server/handlers/directives.rs2116
-rw-r--r--makima/src/server/handlers/mesh_daemon.rs15
-rw-r--r--makima/src/server/handlers/mod.rs2
-rw-r--r--makima/src/server/mod.rs57
19 files changed, 4 insertions, 8297 deletions
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<dyn std::error::Error + Send + Sync>> {
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<dyn std::error::Error
}
}
-/// Run directive commands.
-async fn run_directive(
- cmd: DirectiveCommand,
-) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
- 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<i32, Vec<(&str, &str)>> =
- 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<Vec<ListItem>, Box<dyn std::error::Error + Send + Sync>> {
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<JsonValue, ApiError> {
- #[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<JsonValue, ApiError> {
- 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<JsonValue, ApiError> {
- self.get(&format!("/api/v1/directives/{}", directive_id))
- .await
- }
-
- /// Archive a directive.
- pub async fn archive_directive(&self, directive_id: Uuid) -> Result<JsonValue, ApiError> {
- 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<JsonValue, ApiError> {
- self.post_empty(&format!("/api/v1/directives/{}/start", directive_id))
- .await
- }
-
- /// Pause a directive.
- pub async fn pause_directive(&self, directive_id: Uuid) -> Result<JsonValue, ApiError> {
- 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<JsonValue, ApiError> {
- self.post_empty(&format!("/api/v1/directives/{}/resume", directive_id))
- .await
- }
-
- /// Stop a directive.
- pub async fn stop_directive(&self, directive_id: Uuid) -> Result<JsonValue, ApiError> {
- 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<JsonValue, ApiError> {
- 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<JsonValue, ApiError> {
- 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<JsonValue, ApiError> {
- 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<JsonValue, ApiError> {
- 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<JsonValue, ApiError> {
- #[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<JsonValue, ApiError> {
- #[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<JsonValue, ApiError> {
- 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<Vec<Uuid>>,
- ) -> Result<JsonValue, ApiError> {
- #[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<Vec<Uuid>>,
- }
- 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<JsonValue, ApiError> {
- 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<JsonValue, ApiError> {
- 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<JsonValue, ApiError> {
- 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<JsonValue, ApiError> {
- 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<JsonValue, ApiError> {
- #[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<JsonValue, ApiError> {
- 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<JsonValue, ApiError> {
- 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<JsonValue, ApiError> {
- #[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<JsonValue, ApiError> {
- 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<JsonValue, ApiError> {
- 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<JsonValue, ApiError> {
- #[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<JsonValue, ApiError> {
- #[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<JsonValue, ApiError> {
- 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<String>,
-
- /// 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<String>,
-
- /// 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<String>,
-}
-
-/// 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<String>,
-}
-
-/// 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 <directive-id>
-
-# Start execution (generates chain and begins)
-makima directive start <directive-id>
-
-# View chain steps
-makima directive steps <directive-id>
-
-# View DAG visualization
-makima directive graph <directive-id> --with-status
-
-# View recent events
-makima directive events <directive-id> --limit 20
-
-# Approve a pending request
-makima directive approve <directive-id> <approval-id> [--response "Looks good"]
-
-# Deny a pending request
-makima directive deny <directive-id> <approval-id> [--reason "Need more testing"]
-
-# Lifecycle commands
-makima directive pause <directive-id>
-makima directive resume <directive-id>
-makima directive stop <directive-id>
-makima directive archive <directive-id>
-```
-
-## 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 <approval-id>
-```
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
@@ -2596,672 +2596,6 @@ pub struct HeartbeatHistoryQuery {
}
// =============================================================================
-// 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<Self, Self::Err> {
- 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<f64>,
- pub max_wall_time_minutes: Option<i32>,
- pub max_rework_cycles: Option<i32>,
- pub max_chain_regenerations: Option<i32>,
- pub repository_url: Option<String>,
- pub local_path: Option<String>,
- pub base_branch: Option<String>,
- pub orchestrator_contract_id: Option<Uuid>,
- pub current_chain_id: Option<Uuid>,
- pub chain_generation_count: i32,
- pub total_cost_usd: f64,
- pub started_at: Option<DateTime<Utc>>,
- pub completed_at: Option<DateTime<Utc>>,
- pub version: i32,
- pub created_at: DateTime<Utc>,
- pub updated_at: DateTime<Utc>,
-}
-
-impl Directive {
- /// Parse status string to DirectiveStatus enum
- pub fn status_enum(&self) -> Result<DirectiveStatus, String> {
- 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<String>,
- pub rationale: Option<String>,
- pub planning_model: Option<String>,
- pub status: String,
- pub total_steps: i32,
- pub completed_steps: i32,
- pub failed_steps: i32,
- pub current_confidence: Option<f64>,
- pub started_at: Option<DateTime<Utc>>,
- pub completed_at: Option<DateTime<Utc>>,
- pub version: i32,
- pub created_at: DateTime<Utc>,
- pub updated_at: DateTime<Utc>,
-}
-
-/// 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<Self, Self::Err> {
- 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<String>,
- pub step_type: String,
- pub contract_type: String,
- pub initial_phase: Option<String>,
- pub task_plan: Option<String>,
- #[sqlx(default)]
- pub phases: Vec<String>,
- #[sqlx(default)]
- pub depends_on: Vec<Uuid>,
- pub parallel_group: Option<String>,
- #[sqlx(default)]
- pub requirement_ids: Vec<String>,
- #[sqlx(default)]
- pub acceptance_criteria_ids: Vec<String>,
- #[sqlx(json)]
- #[serde(default)]
- pub verifier_config: serde_json::Value,
- pub status: String,
- pub contract_id: Option<Uuid>,
- pub supervisor_task_id: Option<Uuid>,
- pub confidence_score: Option<f64>,
- pub confidence_level: Option<String>,
- pub evaluation_count: i32,
- pub rework_count: i32,
- pub last_evaluation_id: Option<Uuid>,
- pub editor_x: Option<f64>,
- pub editor_y: Option<f64>,
- pub order_index: i32,
- pub started_at: Option<DateTime<Utc>>,
- pub completed_at: Option<DateTime<Utc>>,
- pub created_at: DateTime<Utc>,
-}
-
-impl ChainStep {
- /// Parse status string to StepStatus enum
- pub fn status_enum(&self) -> Result<StepStatus, String> {
- 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<Uuid>,
- pub step_id: Option<Uuid>,
- pub contract_id: Option<Uuid>,
- pub evaluation_type: String,
- pub evaluation_number: i32,
- pub evaluator: Option<String>,
- pub passed: bool,
- pub overall_score: Option<f64>,
- pub confidence_level: Option<String>,
- #[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<String>,
- #[sqlx(json)]
- pub directive_snapshot: Option<serde_json::Value>,
- #[sqlx(json)]
- pub deliverables_snapshot: Option<serde_json::Value>,
- pub started_at: DateTime<Utc>,
- pub completed_at: Option<DateTime<Utc>>,
- pub created_at: DateTime<Utc>,
-}
-
-/// 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<Uuid>,
- pub step_id: Option<Uuid>,
- pub event_type: String,
- pub severity: String,
- #[sqlx(json)]
- pub event_data: Option<serde_json::Value>,
- pub actor_type: String,
- pub actor_id: Option<Uuid>,
- pub created_at: DateTime<Utc>,
-}
-
-/// 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<String>,
- pub working_directory: Option<String>,
- pub timeout_seconds: Option<i32>,
- #[sqlx(json)]
- #[serde(default)]
- pub environment: serde_json::Value,
- pub auto_detect: bool,
- #[sqlx(default)]
- pub detect_files: Vec<String>,
- pub weight: f64,
- pub required: bool,
- pub enabled: bool,
- pub last_run_at: Option<DateTime<Utc>>,
- #[sqlx(json)]
- pub last_result: Option<serde_json::Value>,
- pub created_at: DateTime<Utc>,
- pub updated_at: DateTime<Utc>,
-}
-
-/// 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<Uuid>,
- pub approval_type: String,
- pub description: String,
- #[sqlx(json)]
- pub context: Option<serde_json::Value>,
- pub urgency: String,
- pub status: String,
- pub response: Option<String>,
- pub responded_by: Option<Uuid>,
- pub responded_at: Option<DateTime<Utc>>,
- pub expires_at: Option<DateTime<Utc>>,
- pub created_at: DateTime<Utc>,
-}
-
-// =============================================================================
-// 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<String>,
- pub repository_url: Option<String>,
- pub local_path: Option<String>,
- pub base_branch: Option<String>,
- pub autonomy_level: Option<String>,
- pub requirements: Option<serde_json::Value>,
- pub acceptance_criteria: Option<serde_json::Value>,
- pub confidence_threshold_green: Option<f64>,
- pub confidence_threshold_yellow: Option<f64>,
- pub max_total_cost_usd: Option<f64>,
- pub max_wall_time_minutes: Option<i32>,
-}
-
-/// Request to update a directive
-#[derive(Debug, Deserialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct UpdateDirectiveRequest {
- pub title: Option<String>,
- pub goal: Option<String>,
- pub requirements: Option<serde_json::Value>,
- pub acceptance_criteria: Option<serde_json::Value>,
- pub constraints: Option<serde_json::Value>,
- pub external_dependencies: Option<serde_json::Value>,
- pub autonomy_level: Option<String>,
- pub confidence_threshold_green: Option<f64>,
- pub confidence_threshold_yellow: Option<f64>,
- pub max_total_cost_usd: Option<f64>,
- pub max_wall_time_minutes: Option<i32>,
- pub max_rework_cycles: Option<i32>,
- pub max_chain_regenerations: Option<i32>,
- 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<f64>,
- pub completed_steps: i32,
- pub total_steps: i32,
- pub chain_generation_count: i32,
- pub started_at: Option<DateTime<Utc>>,
- pub created_at: DateTime<Utc>,
-}
-
-/// 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<DirectiveChain>,
- pub steps: Vec<ChainStep>,
- pub recent_events: Vec<DirectiveEvent>,
- pub pending_approvals: Vec<DirectiveApproval>,
-}
-
-/// 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<String>,
- pub step_type: Option<String>,
- pub contract_type: Option<String>,
- pub initial_phase: Option<String>,
- pub task_plan: Option<String>,
- pub phases: Option<Vec<String>>,
- pub depends_on: Option<Vec<Uuid>>,
- pub parallel_group: Option<String>,
- pub requirement_ids: Option<Vec<String>>,
- pub acceptance_criteria_ids: Option<Vec<String>>,
- pub verifier_config: Option<serde_json::Value>,
- pub editor_x: Option<f64>,
- pub editor_y: Option<f64>,
-}
-
-/// Request to update a step
-#[derive(Debug, Deserialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct UpdateStepRequest {
- pub name: Option<String>,
- pub description: Option<String>,
- pub task_plan: Option<String>,
- pub depends_on: Option<Vec<Uuid>>,
- pub requirement_ids: Option<Vec<String>>,
- pub acceptance_criteria_ids: Option<Vec<String>>,
- pub verifier_config: Option<serde_json::Value>,
- pub editor_x: Option<f64>,
- pub editor_y: Option<f64>,
-}
-
-/// 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<DirectiveChainGraphNode>,
- pub edges: Vec<DirectiveChainGraphEdge>,
-}
-
-/// 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<f64>,
- pub confidence_level: Option<String>,
- pub contract_id: Option<Uuid>,
- pub editor_x: Option<f64>,
- pub editor_y: Option<f64>,
-}
-
-/// 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<ChainStep>,
- 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<String>,
- pub working_directory: Option<String>,
- pub timeout_seconds: Option<i32>,
- pub environment: Option<serde_json::Value>,
- pub weight: Option<f64>,
- pub required: Option<bool>,
-}
-
-/// Request to update a verifier
-#[derive(Debug, Deserialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct UpdateVerifierRequest {
- pub name: Option<String>,
- pub command: Option<String>,
- pub working_directory: Option<String>,
- pub timeout_seconds: Option<i32>,
- pub weight: Option<f64>,
- pub required: Option<bool>,
- pub enabled: Option<bool>,
-}
-
-/// Approval action request
-#[derive(Debug, Deserialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct ApprovalActionRequest {
- pub response: Option<String>,
-}
-
-/// Request to update directive requirements
-#[derive(Debug, Deserialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct UpdateRequirementsRequest {
- pub requirements: Vec<DirectiveRequirement>,
-}
-
-/// Request to update directive acceptance criteria
-#[derive(Debug, Deserialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct UpdateCriteriaRequest {
- pub acceptance_criteria: Vec<DirectiveAcceptanceCriterion>,
-}
-
-/// Request to trigger step rework
-#[derive(Debug, Deserialize, ToSchema)]
-#[serde(rename_all = "camelCase")]
-pub struct ReworkStepRequest {
- pub instructions: Option<String>,
-}
-
-/// 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<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- pub parent_id: Option<String>,
-}
-
-/// 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<String>,
- pub description: String,
- #[serde(default = "default_true")]
- pub testable: bool,
- pub verification_method: Option<String>,
-}
-
-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<String>,
- pub repository_url: Option<String>,
- pub repositories: Option<Vec<AddChainRepositoryRequest>>,
- pub loop_enabled: Option<bool>,
- pub loop_max_iterations: Option<i32>,
- pub loop_progress_check: Option<String>,
- pub contracts: Option<Vec<CreateChainContractRequest>>,
-}
-
-/// 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<String>,
- pub local_path: Option<String>,
- #[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<String>,
- #[serde(default)]
- pub contract_type: Option<String>,
- pub initial_phase: Option<String>,
- pub phases: Option<Vec<String>>,
- pub depends_on: Option<Vec<String>>,
- pub tasks: Option<Vec<CreateChainTaskRequest>>,
- pub deliverables: Option<Vec<CreateChainDeliverableRequest>>,
- pub editor_x: Option<f64>,
- pub editor_y: Option<f64>,
-}
-
-/// 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<String>,
-}
-
-// =============================================================================
// 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.
@@ -4905,1169 +4899,6 @@ pub async fn sync_supervisor_state(
}
// =============================================================================
-// 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<Directive, sqlx::Error> {
- 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<Option<Directive>, 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<Option<Directive>, 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<Vec<DirectiveSummary>, 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<Directive, RepositoryError> {
- // 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<Directive, sqlx::Error> {
- 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<Option<Directive>, 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<bool, sqlx::Error> {
- 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<Option<DirectiveWithProgress>, 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<DirectiveChain, sqlx::Error> {
- // 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<Option<DirectiveChain>, 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<Option<DirectiveChain>, 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<DirectiveChain, sqlx::Error> {
- 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<ChainStep, sqlx::Error> {
- 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<Option<ChainStep>, 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<Vec<ChainStep>, 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<ChainStep, sqlx::Error> {
- 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<bool, sqlx::Error> {
- // 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<Vec<ChainStep>, 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<ChainStep, sqlx::Error> {
- 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<Uuid>,
-) -> Result<ChainStep, sqlx::Error> {
- 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<ChainStep, sqlx::Error> {
- 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<ChainStep, sqlx::Error> {
- 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<DirectiveChainGraphResponse, sqlx::Error> {
- 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<DirectiveChainGraphNode> = 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<Uuid>,
- step_id: Option<Uuid>,
- contract_id: Option<Uuid>,
- evaluation_type: &str,
- evaluator: Option<&str>,
- passed: bool,
- overall_score: Option<f64>,
- 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<DirectiveEvaluation, sqlx::Error> {
- // 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<Vec<DirectiveEvaluation>, 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<i64>,
-) -> Result<Vec<DirectiveEvaluation>, 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<Uuid>,
- step_id: Option<Uuid>,
- event_type: &str,
- severity: &str,
- event_data: Option<serde_json::Value>,
- actor_type: &str,
- actor_id: Option<Uuid>,
-) -> Result<DirectiveEvent, sqlx::Error> {
- 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<i64>,
-) -> Result<Vec<DirectiveEvent>, 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<String>,
- weight: f64,
- required: bool,
-) -> Result<DirectiveVerifier, sqlx::Error> {
- 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<Vec<DirectiveVerifier>, 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<bool>,
- command: Option<&str>,
- weight: Option<f64>,
- required: Option<bool>,
-) -> Result<DirectiveVerifier, sqlx::Error> {
- 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<DirectiveVerifier, sqlx::Error> {
- 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<Uuid>,
- approval_type: &str,
- description: &str,
- context: Option<serde_json::Value>,
- urgency: &str,
- expires_at: Option<chrono::DateTime<Utc>>,
-) -> Result<DirectiveApproval, sqlx::Error> {
- 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<DirectiveApproval, sqlx::Error> {
- 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<Vec<DirectiveApproval>, 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<Option<ChainStep>, 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<String>,
- pub base_branch: Option<String>,
-}
-
-/// Main orchestration engine for directives.
-pub struct DirectiveEngine {
- pool: PgPool,
- planner: ChainPlanner,
- event_tx: Option<broadcast::Sender<EngineEvent>>,
-}
-
-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<EngineEvent>) -> 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<PlanningStartResult, EngineError> {
- 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<String, Uuid> = 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<Uuid> = 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<Uuid, EngineError> {
- 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, 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::<Vec<_>>();
-
- 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::<Vec<_>>();
-
- // 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<EvaluationResult, EngineError> {
- // 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<Uuid, EngineError> {
- 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<String> = 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<String> = 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<String> = 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<DirectiveEvent, EngineError> {
- 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<String>,
- /// IDs of requirements this step addresses
- pub requirement_ids: Vec<String>,
- /// Contract template fields
- pub contract_template: Option<ContractTemplate>,
-}
-
-/// 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<String>,
- /// Tasks within the contract
- pub tasks: Vec<TaskTemplate>,
- /// Deliverables expected
- pub deliverables: Vec<DeliverableTemplate>,
-}
-
-/// 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<GeneratedStep>,
-}
-
-/// Generated specification from LLM.
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct GeneratedSpec {
- /// Generated title (if improved from goal)
- pub title: Option<String>,
- /// Structured requirements
- pub requirements: serde_json::Value,
- /// Structured acceptance criteria
- pub acceptance_criteria: serde_json::Value,
- /// Constraints extracted from goal
- pub constraints: Option<serde_json::Value>,
-}
-
-/// 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<String>,
- /// Database pool for persistence
- #[allow(dead_code)]
- pool: Option<sqlx::PgPool>,
-}
-
-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<GeneratedSpec, PlannerError> {
- // 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<String> = 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<String> = 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<String> = 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<GeneratedChain, PlannerError> {
- // 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<String> = 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<String> = 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<Vec<&'a str>, 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<String, Uuid>,
- ) -> Vec<AddStepRequest> {
- chain
- .steps
- .iter()
- .map(|step| {
- let depends_on: Vec<Uuid> = 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<String, (f64, f64)> {
- let depths = self.get_step_depths(chain);
- let mut positions: HashMap<String, (f64, f64)> = HashMap::new();
-
- // Group by depth
- let mut by_depth: HashMap<usize, Vec<&str>> = 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<String, usize> {
- let mut depths: HashMap<String, usize> = HashMap::new();
-
- // Build dependency map
- let deps: HashMap<String, Vec<String>> = chain
- .steps
- .iter()
- .map(|s| (s.name.clone(), s.depends_on.clone()))
- .collect();
-
- fn compute_depth(
- name: &str,
- deps: &HashMap<String, Vec<String>>,
- depths: &mut HashMap<String, usize>,
- ) -> 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<String> = 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<String, PlannerError> {
- // 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<JsonValue>,
- /// 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<VerifierResult>,
- /// Summary feedback for the step
- pub summary: String,
- /// Rework instructions if failed
- pub rework_instructions: Option<String>,
- /// 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<VerifierResult>,
- 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<String>,
- pub detect_files: Vec<String>,
- 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<VerifierResult, VerifierError>;
-}
-
-/// 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<Uuid>,
- /// Files that were modified in this step
- pub modified_files: Vec<String>,
- /// Step description for LLM context
- pub step_description: String,
- /// Acceptance criteria for LLM evaluation
- pub acceptance_criteria: Vec<String>,
- /// 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<String>,
- #[allow(dead_code)]
- timeout_ms: u64,
- required: bool,
- /// Files/patterns that indicate this verifier is applicable
- applicable_patterns: Vec<String>,
-}
-
-impl CommandVerifier {
- /// Create a new command verifier.
- pub fn new(
- name: impl Into<String>,
- verifier_type: VerifierType,
- command: impl Into<String>,
- ) -> 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<String>) -> 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<String>) -> 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<VerifierResult, VerifierError> {
- 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<u32> {
- // 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<Box<dyn Verifier>> {
- let mut verifiers: Vec<Box<dyn Verifier>> = 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::<serde_json::Value>(&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<Box<dyn Verifier>>,
- green_threshold: f64,
- yellow_threshold: f64,
-}
-
-impl CompositeEvaluator {
- /// Create a new composite evaluator with default thresholds.
- pub fn new(verifiers: Vec<Box<dyn Verifier>>) -> 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<dyn Verifier>) -> 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<String>,
-}
-
-/// Query parameters for listing events
-#[derive(Debug, Deserialize)]
-pub struct ListEventsQuery {
- pub limit: Option<i64>,
-}
-
-/// 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<String>,
- pub api_key: Option<String>,
-}
-
-/// Query parameters for listing evaluations
-#[derive(Debug, Deserialize)]
-pub struct ListEvaluationsQuery {
- pub limit: Option<i64>,
- #[serde(rename = "stepId")]
- pub step_id: Option<Uuid>,
-}
-
-/// 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<String>,
-}
-
-// =============================================================================
-// Directive CRUD
-// =============================================================================
-
-/// Create a new directive
-/// POST /api/v1/directives
-pub async fn create_directive(
- State(state): State<SharedState>,
- Authenticated(auth): Authenticated,
- Json(req): Json<CreateDirectiveRequest>,
-) -> 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<SharedState>,
- Authenticated(auth): Authenticated,
- Query(params): Query<ListDirectivesQuery>,
-) -> 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<SharedState>,
- Authenticated(auth): Authenticated,
- Path(id): Path<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();
- };
-
- 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<SharedState>,
- Authenticated(auth): Authenticated,
- Path(id): Path<Uuid>,
- Json(req): Json<UpdateDirectiveRequest>,
-) -> 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<SharedState>,
- Authenticated(auth): Authenticated,
- Path(id): Path<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();
- };
-
- 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<SharedState>,
- Authenticated(auth): Authenticated,
- Path(id): Path<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()
- }
- }
-
- // 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<SharedState>,
- Authenticated(auth): Authenticated,
- Path(id): Path<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()
- }
- }
-
- 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<SharedState>,
- Authenticated(auth): Authenticated,
- Path(id): Path<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()
- }
- }
-
- 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<SharedState>,
- Authenticated(auth): Authenticated,
- Path(id): Path<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()
- }
- }
-
- 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<SharedState>,
- Authenticated(auth): Authenticated,
- Path(id): Path<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_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<SharedState>,
- Authenticated(auth): Authenticated,
- Path(id): Path<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()
- }
- }
-
- // 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<SharedState>,
- Authenticated(auth): Authenticated,
- Path(id): Path<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()
- }
- }
-
- 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<SharedState>,
- Authenticated(auth): Authenticated,
- Path(id): Path<Uuid>,
- Json(req): Json<AddStepRequest>,
-) -> 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<SharedState>,
- 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<SharedState>,
- Authenticated(auth): Authenticated,
- Path((id, step_id)): Path<(Uuid, Uuid)>,
- Json(req): Json<UpdateStepRequest>,
-) -> 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<SharedState>,
- 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<SharedState>,
- 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<SharedState>,
- Authenticated(auth): Authenticated,
- Path(id): Path<Uuid>,
- Query(params): Query<ListEvaluationsQuery>,
-) -> 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<SharedState>,
- Authenticated(auth): Authenticated,
- Path(id): Path<Uuid>,
- Query(params): Query<ListEventsQuery>,
-) -> 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=<jwt> or ?api_key=<key>
-pub async fn stream_events(
- State(state): State<SharedState>,
- Path(id): Path<Uuid>,
- Query(auth_params): Query<StreamAuthQuery>,
-) -> 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::<chrono::DateTime<chrono::Utc>>),
- 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<Result<Event, Infallible>> = 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<SharedState>,
- Authenticated(auth): Authenticated,
- Path(id): Path<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::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<SharedState>,
- Authenticated(auth): Authenticated,
- Path(id): Path<Uuid>,
- Json(req): Json<CreateVerifierRequest>,
-) -> 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<SharedState>,
- Authenticated(auth): Authenticated,
- Path((id, verifier_id)): Path<(Uuid, Uuid)>,
- Json(req): Json<UpdateVerifierRequest>,
-) -> 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<SharedState>,
- Authenticated(auth): Authenticated,
- Path(id): Path<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::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<SharedState>,
- Authenticated(auth): Authenticated,
- Path((id, approval_id)): Path<(Uuid, Uuid)>,
- Json(req): Json<ApprovalActionRequest>,
-) -> 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<SharedState>,
- Authenticated(auth): Authenticated,
- Path((id, approval_id)): Path<(Uuid, Uuid)>,
- Json(req): Json<ApprovalActionRequest>,
-) -> 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<SharedState>,
- 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<SharedState>,
- Authenticated(auth): Authenticated,
- Path((id, step_id)): Path<(Uuid, Uuid)>,
- Json(req): Json<ReworkStepRequest>,
-) -> 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<SharedState>,
- Authenticated(auth): Authenticated,
- Path(id): Path<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();
- };
-
- // 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<SharedState>,
- Authenticated(auth): Authenticated,
- Path(id): Path<Uuid>,
- Json(req): Json<UpdateRequirementsRequest>,
-) -> 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<SharedState>,
- Authenticated(auth): Authenticated,
- Path(id): Path<Uuid>,
- Json(req): Json<UpdateCriteriaRequest>,
-) -> 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<SharedState>,
- Authenticated(auth): Authenticated,
- Path(id): Path<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();
- };
-
- // 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