summaryrefslogtreecommitdiff
path: root/makima/src
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src')
-rw-r--r--makima/src/bin/makima.rs105
-rw-r--r--makima/src/daemon/api/directive.rs124
-rw-r--r--makima/src/daemon/api/mod.rs1
-rw-r--r--makima/src/daemon/cli/directive.rs101
-rw-r--r--makima/src/daemon/cli/mod.rs49
-rw-r--r--makima/src/daemon/skills/directive.md111
-rw-r--r--makima/src/daemon/skills/mod.rs4
-rw-r--r--makima/src/db/models.rs151
-rw-r--r--makima/src/db/repository.rs415
-rw-r--r--makima/src/llm/contract_tools.rs488
-rw-r--r--makima/src/server/handlers/contract_chat.rs29
-rw-r--r--makima/src/server/handlers/contracts.rs2
-rw-r--r--makima/src/server/handlers/directives.rs841
-rw-r--r--makima/src/server/handlers/mesh.rs8
-rw-r--r--makima/src/server/handlers/mesh_chat.rs2
-rw-r--r--makima/src/server/handlers/mesh_daemon.rs17
-rw-r--r--makima/src/server/handlers/mesh_supervisor.rs2
-rw-r--r--makima/src/server/handlers/mod.rs1
-rw-r--r--makima/src/server/handlers/transcript_analysis.rs4
-rw-r--r--makima/src/server/mod.rs26
-rw-r--r--makima/src/server/openapi.rs39
21 files changed, 2004 insertions, 516 deletions
diff --git a/makima/src/bin/makima.rs b/makima/src/bin/makima.rs
index ee5895c..639c88b 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,
- SupervisorCommand, ViewArgs,
+ DirectiveCommand, SupervisorCommand, ViewArgs,
};
use makima::daemon::tui::{self, Action, App, ListItem, ViewType, TuiWsClient, WsEvent, OutputLine, OutputMessageType, WsConnectionState, RepositorySuggestion};
use makima::daemon::config::{DaemonConfig, RepoEntry};
@@ -29,6 +29,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
Commands::Daemon(args) => run_daemon(args).await,
Commands::Supervisor(cmd) => run_supervisor(cmd).await,
Commands::Contract(cmd) => run_contract(cmd).await,
+ Commands::Directive(cmd) => run_directive(cmd).await,
Commands::View(args) => run_view(args).await,
Commands::Config(cmd) => run_config(cmd).await,
}
@@ -711,6 +712,108 @@ async fn run_contract(
Ok(())
}
+/// Run directive commands.
+async fn run_directive(
+ cmd: DirectiveCommand,
+) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
+ use makima::daemon::api::directive::*;
+
+ match cmd {
+ DirectiveCommand::List(args) => {
+ let client = ApiClient::new(args.api_url, args.api_key)?;
+ let result = client.list_directives().await?;
+ println!("{}", serde_json::to_string(&result.0)?);
+ }
+ DirectiveCommand::Get(args) | DirectiveCommand::Status(args) => {
+ let client = ApiClient::new(args.api_url, args.api_key)?;
+ let result = client.get_directive(args.directive_id).await?;
+ println!("{}", serde_json::to_string(&result.0)?);
+ }
+ DirectiveCommand::AddStep(args) => {
+ let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
+ let depends_on: Vec<uuid::Uuid> = args
+ .depends_on
+ .map(|d| {
+ d.split(',')
+ .filter_map(|s| uuid::Uuid::parse_str(s.trim()).ok())
+ .collect()
+ })
+ .unwrap_or_default();
+ let req = CreateStepRequest {
+ name: args.name,
+ description: args.description,
+ task_plan: args.task_plan,
+ depends_on,
+ order_index: args.order_index,
+ };
+ let result = client.directive_add_step(args.common.directive_id, req).await?;
+ println!("{}", serde_json::to_string(&result.0)?);
+ }
+ DirectiveCommand::RemoveStep(args) => {
+ let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
+ client.directive_remove_step(args.common.directive_id, args.step_id).await?;
+ println!(r#"{{"success": true}}"#);
+ }
+ DirectiveCommand::SetDeps(args) => {
+ let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
+ let depends_on: Vec<uuid::Uuid> = args
+ .depends_on
+ .split(',')
+ .filter_map(|s| uuid::Uuid::parse_str(s.trim()).ok())
+ .collect();
+ let result = client
+ .directive_set_deps(args.common.directive_id, args.step_id, depends_on)
+ .await?;
+ println!("{}", serde_json::to_string(&result.0)?);
+ }
+ DirectiveCommand::Start(args) => {
+ let client = ApiClient::new(args.api_url, args.api_key)?;
+ let result = client.directive_start(args.directive_id).await?;
+ println!("{}", serde_json::to_string(&result.0)?);
+ }
+ DirectiveCommand::Pause(args) => {
+ let client = ApiClient::new(args.api_url, args.api_key)?;
+ let result = client.directive_pause(args.directive_id).await?;
+ println!("{}", serde_json::to_string(&result.0)?);
+ }
+ DirectiveCommand::Advance(args) => {
+ let client = ApiClient::new(args.api_url, args.api_key)?;
+ let result = client.directive_advance(args.directive_id).await?;
+ println!("{}", serde_json::to_string(&result.0)?);
+ }
+ DirectiveCommand::CompleteStep(args) => {
+ let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
+ let result = client
+ .directive_complete_step(args.common.directive_id, args.step_id)
+ .await?;
+ println!("{}", serde_json::to_string(&result.0)?);
+ }
+ DirectiveCommand::FailStep(args) => {
+ let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
+ let result = client
+ .directive_fail_step(args.common.directive_id, args.step_id)
+ .await?;
+ println!("{}", serde_json::to_string(&result.0)?);
+ }
+ DirectiveCommand::SkipStep(args) => {
+ let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
+ let result = client
+ .directive_skip_step(args.common.directive_id, args.step_id)
+ .await?;
+ println!("{}", serde_json::to_string(&result.0)?);
+ }
+ DirectiveCommand::UpdateGoal(args) => {
+ let client = ApiClient::new(args.common.api_url, args.common.api_key)?;
+ let result = client
+ .directive_update_goal(args.common.directive_id, &args.goal)
+ .await?;
+ println!("{}", serde_json::to_string(&result.0)?);
+ }
+ }
+
+ Ok(())
+}
+
/// Run the TUI view command.
async fn run_view(args: ViewArgs) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Load CLI config for defaults
diff --git a/makima/src/daemon/api/directive.rs b/makima/src/daemon/api/directive.rs
new file mode 100644
index 0000000..fbd27fe
--- /dev/null
+++ b/makima/src/daemon/api/directive.rs
@@ -0,0 +1,124 @@
+//! Directive API methods.
+
+use serde::Serialize;
+use uuid::Uuid;
+
+use super::client::{ApiClient, ApiError};
+use super::supervisor::JsonValue;
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CreateStepRequest {
+ pub name: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub description: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub task_plan: Option<String>,
+ pub depends_on: Vec<Uuid>,
+ pub order_index: i32,
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct UpdateGoalRequest {
+ pub goal: String,
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct UpdateStepDepsRequest {
+ pub depends_on: Vec<Uuid>,
+}
+
+impl ApiClient {
+ /// List all directives.
+ pub async fn list_directives(&self) -> Result<JsonValue, ApiError> {
+ self.get("/api/v1/directives").await
+ }
+
+ /// Get a directive with its steps.
+ pub async fn get_directive(&self, directive_id: Uuid) -> Result<JsonValue, ApiError> {
+ self.get(&format!("/api/v1/directives/{}", directive_id)).await
+ }
+
+ /// Add a step to a directive.
+ pub async fn directive_add_step(
+ &self,
+ directive_id: Uuid,
+ req: CreateStepRequest,
+ ) -> Result<JsonValue, ApiError> {
+ self.post(&format!("/api/v1/directives/{}/steps", directive_id), &req).await
+ }
+
+ /// Remove a step from a directive.
+ pub async fn directive_remove_step(
+ &self,
+ directive_id: Uuid,
+ step_id: Uuid,
+ ) -> Result<(), ApiError> {
+ self.delete(&format!("/api/v1/directives/{}/steps/{}", directive_id, step_id)).await
+ }
+
+ /// Set dependencies for a step.
+ pub async fn directive_set_deps(
+ &self,
+ directive_id: Uuid,
+ step_id: Uuid,
+ depends_on: Vec<Uuid>,
+ ) -> Result<JsonValue, ApiError> {
+ let req = UpdateStepDepsRequest { depends_on };
+ self.put(&format!("/api/v1/directives/{}/steps/{}", directive_id, step_id), &req).await
+ }
+
+ /// Start a directive.
+ pub async fn directive_start(&self, directive_id: Uuid) -> Result<JsonValue, ApiError> {
+ self.post_empty(&format!("/api/v1/directives/{}/start", directive_id)).await
+ }
+
+ /// Pause a directive.
+ pub async fn directive_pause(&self, directive_id: Uuid) -> Result<JsonValue, ApiError> {
+ self.post_empty(&format!("/api/v1/directives/{}/pause", directive_id)).await
+ }
+
+ /// Advance the directive DAG.
+ pub async fn directive_advance(&self, directive_id: Uuid) -> Result<JsonValue, ApiError> {
+ self.post_empty(&format!("/api/v1/directives/{}/advance", directive_id)).await
+ }
+
+ /// Mark a step as completed.
+ pub async fn directive_complete_step(
+ &self,
+ directive_id: Uuid,
+ step_id: Uuid,
+ ) -> Result<JsonValue, ApiError> {
+ self.post_empty(&format!("/api/v1/directives/{}/steps/{}/complete", directive_id, step_id)).await
+ }
+
+ /// Mark a step as failed.
+ pub async fn directive_fail_step(
+ &self,
+ directive_id: Uuid,
+ step_id: Uuid,
+ ) -> Result<JsonValue, ApiError> {
+ self.post_empty(&format!("/api/v1/directives/{}/steps/{}/fail", directive_id, step_id)).await
+ }
+
+ /// Mark a step as skipped.
+ pub async fn directive_skip_step(
+ &self,
+ directive_id: Uuid,
+ step_id: Uuid,
+ ) -> Result<JsonValue, ApiError> {
+ self.post_empty(&format!("/api/v1/directives/{}/steps/{}/skip", directive_id, step_id)).await
+ }
+
+ /// Update the directive's goal.
+ pub async fn directive_update_goal(
+ &self,
+ directive_id: Uuid,
+ goal: &str,
+ ) -> Result<JsonValue, ApiError> {
+ let req = UpdateGoalRequest { goal: goal.to_string() };
+ self.put(&format!("/api/v1/directives/{}/goal", directive_id), &req).await
+ }
+}
diff --git a/makima/src/daemon/api/mod.rs b/makima/src/daemon/api/mod.rs
index 49d80e0..2d1efbf 100644
--- a/makima/src/daemon/api/mod.rs
+++ b/makima/src/daemon/api/mod.rs
@@ -2,6 +2,7 @@
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
new file mode 100644
index 0000000..5de60ed
--- /dev/null
+++ b/makima/src/daemon/cli/directive.rs
@@ -0,0 +1,101 @@
+//! Directive subcommand - directive management commands for orchestrator tasks.
+
+use clap::Args;
+use uuid::Uuid;
+
+/// Common arguments for directive commands.
+#[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,
+
+ /// Directive ID
+ #[arg(long, env = "MAKIMA_DIRECTIVE_ID", global = true)]
+ pub directive_id: Uuid,
+}
+
+/// Arguments for listing directives (no directive_id required).
+#[derive(Args, Debug, Clone)]
+pub struct DirectiveListArgs {
+ /// 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 add-step command.
+#[derive(Args, Debug)]
+pub struct AddStepArgs {
+ #[command(flatten)]
+ pub common: DirectiveArgs,
+
+ /// Step name
+ pub name: String,
+
+ /// Step description
+ #[arg(long)]
+ pub description: Option<String>,
+
+ /// Task plan for the step
+ #[arg(long)]
+ pub task_plan: Option<String>,
+
+ /// Comma-separated UUIDs of dependency steps
+ #[arg(long)]
+ pub depends_on: Option<String>,
+
+ /// Order index
+ #[arg(long, default_value = "0")]
+ pub order_index: i32,
+}
+
+/// Arguments for remove-step command.
+#[derive(Args, Debug)]
+pub struct RemoveStepArgs {
+ #[command(flatten)]
+ pub common: DirectiveArgs,
+
+ /// Step ID to remove
+ pub step_id: Uuid,
+}
+
+/// Arguments for set-deps command.
+#[derive(Args, Debug)]
+pub struct SetDepsArgs {
+ #[command(flatten)]
+ pub common: DirectiveArgs,
+
+ /// Step ID to update
+ pub step_id: Uuid,
+
+ /// Comma-separated UUIDs of dependency steps
+ pub depends_on: String,
+}
+
+/// Arguments for complete-step/fail-step/skip-step commands.
+#[derive(Args, Debug)]
+pub struct StepActionArgs {
+ #[command(flatten)]
+ pub common: DirectiveArgs,
+
+ /// Step ID
+ pub step_id: Uuid,
+}
+
+/// Arguments for update-goal command.
+#[derive(Args, Debug)]
+pub struct UpdateGoalArgs {
+ #[command(flatten)]
+ pub common: DirectiveArgs,
+
+ /// New goal text
+ pub goal: String,
+}
diff --git a/makima/src/daemon/cli/mod.rs b/makima/src/daemon/cli/mod.rs
index 0805edd..faafaea 100644
--- a/makima/src/daemon/cli/mod.rs
+++ b/makima/src/daemon/cli/mod.rs
@@ -3,6 +3,7 @@
pub mod config;
pub mod contract;
pub mod daemon;
+pub mod directive;
pub mod server;
pub mod supervisor;
pub mod view;
@@ -12,6 +13,7 @@ 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;
@@ -41,6 +43,10 @@ pub enum Commands {
#[command(subcommand)]
Contract(ContractCommand),
+ /// Directive commands for DAG-based project management
+ #[command(subcommand)]
+ Directive(DirectiveCommand),
+
/// Interactive TUI browser for contracts and tasks
///
/// Provides a drill-down interface for browsing contracts, viewing their
@@ -196,6 +202,49 @@ pub enum ContractCommand {
CreateFile(contract::CreateFileArgs),
}
+/// Directive subcommands for DAG-based project management.
+#[derive(Subcommand, Debug)]
+pub enum DirectiveCommand {
+ /// List all directives
+ List(directive::DirectiveListArgs),
+
+ /// Get directive status with steps
+ Get(DirectiveArgs),
+
+ /// Get directive status (alias for get)
+ Status(DirectiveArgs),
+
+ /// Add a step to the directive
+ AddStep(directive::AddStepArgs),
+
+ /// Remove a step from the directive
+ RemoveStep(directive::RemoveStepArgs),
+
+ /// Set dependencies for a step
+ SetDeps(directive::SetDepsArgs),
+
+ /// Start the directive (begin executing steps)
+ Start(DirectiveArgs),
+
+ /// Pause the directive
+ Pause(DirectiveArgs),
+
+ /// Advance the DAG (find newly-ready steps)
+ Advance(DirectiveArgs),
+
+ /// Mark a step as completed
+ CompleteStep(directive::StepActionArgs),
+
+ /// Mark a step as failed
+ FailStep(directive::StepActionArgs),
+
+ /// Mark a step as skipped
+ SkipStep(directive::StepActionArgs),
+
+ /// Update the directive's goal (triggers re-planning)
+ UpdateGoal(directive::UpdateGoalArgs),
+}
+
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
new file mode 100644
index 0000000..7c55cf8
--- /dev/null
+++ b/makima/src/daemon/skills/directive.md
@@ -0,0 +1,111 @@
+---
+name: makima-directive
+description: Directive commands for makima DAG-based project orchestration. Use these commands to manage long-lived directives with auto-progressing steps.
+---
+
+# Makima Directive Skill
+
+You are orchestrating a **directive** — a long-lived project managed through a DAG (directed acyclic graph) of steps. Unlike contracts which are finite and phase-based, directives are **ongoing and continuous**: they stay active as the project evolves, and new features/requirements can be added at any time.
+
+## Key Concepts
+
+- **Directive**: A long-lived top-level entity with a goal, repository info, and a mutable DAG of steps
+- **Steps**: Nodes in the DAG. Each step can spawn a task using the mesh infrastructure
+- **Auto-progression**: When a step completes, newly-ready steps (whose dependencies are met) automatically become ready
+- **Continuous evolution**: The goal can be updated at any time. When all steps complete, the directive goes `idle` (not completed) — waiting for new work
+- **Statuses**: `draft` → `active` ↔ `idle` → `archived`. Directives are never "completed" — they go idle and wait
+
+## Commands
+
+### Check Status
+```bash
+makima directive status
+```
+Returns the directive with all steps, their statuses, and dependency information.
+
+### Add a Step
+```bash
+makima directive add-step "Step Name" --description "What this step does" --task-plan "Detailed instructions for the task" --depends-on "uuid1,uuid2" --order-index 1
+```
+
+### Remove a Step
+```bash
+makima directive remove-step <step_id>
+```
+
+### Set Dependencies
+```bash
+makima directive set-deps <step_id> "dep_uuid1,dep_uuid2"
+```
+
+### Start the Directive
+```bash
+makima directive start
+```
+Sets status to `active` and advances any steps with no dependencies to `ready`.
+
+### Advance the DAG
+```bash
+makima directive advance
+```
+Finds newly-ready steps (all dependencies met) and marks them ready. If all steps are in terminal states, sets the directive to `idle`.
+
+### Complete a Step
+```bash
+makima directive complete-step <step_id>
+```
+
+### Fail a Step
+```bash
+makima directive fail-step <step_id>
+```
+
+### Skip a Step
+```bash
+makima directive skip-step <step_id>
+```
+
+### Update the Goal
+```bash
+makima directive update-goal "New or expanded goal text"
+```
+Updates the goal and bumps `goalUpdatedAt`. If the directive is `idle`, it reactivates to `active`.
+
+### Pause
+```bash
+makima directive pause
+```
+
+## Orchestration Workflow
+
+### Initial Setup
+1. Check the directive status to understand the goal
+2. Decompose the goal into steps with clear dependencies
+3. Add steps using `add-step` with appropriate `--depends-on` flags
+4. Start the directive with `start`
+5. Steps with no dependencies will become `ready` immediately
+
+### Monitoring and Advancing
+1. Periodically check status to see step progress
+2. When tasks complete, the DAG auto-advances — newly-ready steps appear
+3. Use `advance` to manually trigger DAG progression if needed
+4. Mark steps as complete/failed/skipped as appropriate
+
+### Re-planning (When Goal Updates)
+When the goal is updated (you'll see a new `goalUpdatedAt` timestamp):
+1. Check the current status to see completed and in-progress steps
+2. Identify what's new in the updated goal
+3. Add new steps that depend on existing completed steps as appropriate
+4. The DAG will auto-advance any newly-ready steps
+
+### Idle State
+When all steps complete, the directive enters `idle` state. This is normal — it means:
+- All current work is done
+- The directive is waiting for new requirements
+- When the user updates the goal, it reactivates automatically
+- You should add new steps based on the updated goal
+
+## Environment Variables
+- `MAKIMA_API_URL` - API server URL
+- `MAKIMA_API_KEY` - Authentication key
+- `MAKIMA_DIRECTIVE_ID` - Current directive ID (set automatically)
diff --git a/makima/src/daemon/skills/mod.rs b/makima/src/daemon/skills/mod.rs
index 0b05f3a..0c015ba 100644
--- a/makima/src/daemon/skills/mod.rs
+++ b/makima/src/daemon/skills/mod.rs
@@ -9,8 +9,12 @@ 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 - DAG-based project orchestration commands
+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 d0a0bd6..9159fd5 100644
--- a/makima/src/db/models.rs
+++ b/makima/src/db/models.rs
@@ -531,6 +531,14 @@ pub struct Task {
/// Standalone completed tasks can be dismissed by the user.
#[serde(default)]
pub hidden: bool,
+
+ // Directive association
+ /// Directive this task belongs to (for directive-driven tasks)
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub directive_id: Option<Uuid>,
+ /// Directive step this task executes
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub directive_step_id: Option<Uuid>,
}
impl Task {
@@ -656,6 +664,10 @@ pub struct CreateTaskRequest {
/// Task ID whose worktree this task shares. When set, this task reuses the supervisor's
/// worktree instead of creating its own, and should NOT have its worktree deleted during cleanup.
pub supervisor_worktree_task_id: Option<Uuid>,
+ /// Directive this task belongs to (for directive-driven tasks)
+ pub directive_id: Option<Uuid>,
+ /// Directive step this task executes
+ pub directive_step_id: Option<Uuid>,
}
/// Request payload for updating a task
@@ -2682,3 +2694,142 @@ mod tests {
}
// =============================================================================
+// Directive Types
+// =============================================================================
+
+/// A directive — a long-lived top-level entity for managing projects via a DAG of steps.
+#[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,
+ /// Status: draft, active, idle, paused, archived
+ pub status: String,
+ pub repository_url: Option<String>,
+ pub local_path: Option<String>,
+ pub base_branch: Option<String>,
+ pub orchestrator_task_id: Option<Uuid>,
+ pub goal_updated_at: DateTime<Utc>,
+ pub started_at: Option<DateTime<Utc>>,
+ pub version: i32,
+ pub created_at: DateTime<Utc>,
+ pub updated_at: DateTime<Utc>,
+}
+
+/// A step in a directive's DAG.
+#[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct DirectiveStep {
+ pub id: Uuid,
+ pub directive_id: Uuid,
+ pub name: String,
+ pub description: Option<String>,
+ pub task_plan: Option<String>,
+ pub depends_on: Vec<Uuid>,
+ /// Status: pending, ready, running, completed, failed, skipped
+ pub status: String,
+ pub task_id: Option<Uuid>,
+ pub order_index: i32,
+ pub generation: i32,
+ pub started_at: Option<DateTime<Utc>>,
+ pub completed_at: Option<DateTime<Utc>>,
+ pub created_at: DateTime<Utc>,
+}
+
+/// Directive with its steps for detail view.
+#[derive(Debug, Serialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct DirectiveWithSteps {
+ #[serde(flatten)]
+ pub directive: Directive,
+ pub steps: Vec<DirectiveStep>,
+}
+
+/// Summary for directive list views.
+#[derive(Debug, Clone, FromRow, Serialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct DirectiveSummary {
+ pub id: Uuid,
+ pub owner_id: Uuid,
+ pub title: String,
+ pub goal: String,
+ pub status: String,
+ pub repository_url: Option<String>,
+ pub orchestrator_task_id: Option<Uuid>,
+ pub version: i32,
+ pub created_at: DateTime<Utc>,
+ pub updated_at: DateTime<Utc>,
+ pub total_steps: i64,
+ pub completed_steps: i64,
+ pub running_steps: i64,
+ pub failed_steps: i64,
+}
+
+/// List response for directives.
+#[derive(Debug, Serialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct DirectiveListResponse {
+ pub directives: Vec<DirectiveSummary>,
+ pub total: i64,
+}
+
+/// Request to create a new directive.
+#[derive(Debug, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct CreateDirectiveRequest {
+ pub title: String,
+ pub goal: String,
+ pub repository_url: Option<String>,
+ pub local_path: Option<String>,
+ pub base_branch: Option<String>,
+}
+
+/// Request to update a directive.
+#[derive(Debug, Default, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct UpdateDirectiveRequest {
+ pub title: Option<String>,
+ pub goal: Option<String>,
+ pub status: Option<String>,
+ pub repository_url: Option<String>,
+ pub local_path: Option<String>,
+ pub base_branch: Option<String>,
+ pub orchestrator_task_id: Option<Uuid>,
+ pub version: Option<i32>,
+}
+
+/// Request to update a directive's goal (triggers re-planning).
+#[derive(Debug, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct UpdateGoalRequest {
+ pub goal: String,
+}
+
+/// Request to create a directive step.
+#[derive(Debug, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct CreateDirectiveStepRequest {
+ pub name: String,
+ pub description: Option<String>,
+ pub task_plan: Option<String>,
+ #[serde(default)]
+ pub depends_on: Vec<Uuid>,
+ #[serde(default)]
+ pub order_index: i32,
+ pub generation: Option<i32>,
+}
+
+/// Request to update a directive step.
+#[derive(Debug, Default, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct UpdateDirectiveStepRequest {
+ pub name: Option<String>,
+ pub description: Option<String>,
+ pub task_plan: Option<String>,
+ pub depends_on: Option<Vec<Uuid>>,
+ pub status: Option<String>,
+ pub task_id: Option<Uuid>,
+ pub order_index: Option<i32>,
+}
diff --git a/makima/src/db/repository.rs b/makima/src/db/repository.rs
index 4ed2298..f347fc7 100644
--- a/makima/src/db/repository.rs
+++ b/makima/src/db/repository.rs
@@ -11,7 +11,9 @@ use super::models::{
ContractTypeTemplateRecord, ConversationMessage, ConversationSnapshot,
CreateContractRequest, CreateFileRequest, CreateTaskRequest,
CreateTemplateRequest, Daemon, DaemonTaskAssignment, DaemonWithCapacity,
- DeliverableDefinition,
+ DeliverableDefinition, Directive, DirectiveStep, DirectiveSummary,
+ CreateDirectiveRequest, CreateDirectiveStepRequest, UpdateDirectiveRequest,
+ UpdateDirectiveStepRequest,
File, FileSummary, FileVersion, HistoryEvent, HistoryQueryFilters,
MeshChatConversation, MeshChatMessageRecord, PhaseChangeResult, PhaseConfig,
PhaseDefinition, SupervisorHeartbeatRecord, SupervisorState,
@@ -4912,3 +4914,414 @@ fn truncate_string(s: &str, max_len: usize) -> String {
}
}
+// =============================================================================
+// Directive CRUD
+// =============================================================================
+
+/// 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> {
+ sqlx::query_as::<_, Directive>(
+ r#"
+ INSERT INTO directives (owner_id, title, goal, repository_url, local_path, base_branch)
+ VALUES ($1, $2, $3, $4, $5, $6)
+ RETURNING *
+ "#,
+ )
+ .bind(owner_id)
+ .bind(&req.title)
+ .bind(&req.goal)
+ .bind(&req.repository_url)
+ .bind(&req.local_path)
+ .bind(&req.base_branch)
+ .fetch_one(pool)
+ .await
+}
+
+/// Get a single directive for an owner.
+pub async fn get_directive_for_owner(
+ pool: &PgPool,
+ owner_id: Uuid,
+ 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 with all its steps.
+pub async fn get_directive_with_steps_for_owner(
+ pool: &PgPool,
+ owner_id: Uuid,
+ id: Uuid,
+) -> Result<Option<(Directive, Vec<DirectiveStep>)>, sqlx::Error> {
+ let directive = sqlx::query_as::<_, Directive>(
+ r#"SELECT * FROM directives WHERE id = $1 AND owner_id = $2"#,
+ )
+ .bind(id)
+ .bind(owner_id)
+ .fetch_optional(pool)
+ .await?;
+
+ match directive {
+ Some(d) => {
+ let steps = list_directive_steps(pool, d.id).await?;
+ Ok(Some((d, steps)))
+ }
+ None => Ok(None),
+ }
+}
+
+/// List all directives for an owner with step counts.
+pub async fn list_directives_for_owner(
+ pool: &PgPool,
+ owner_id: Uuid,
+) -> Result<Vec<DirectiveSummary>, sqlx::Error> {
+ sqlx::query_as::<_, DirectiveSummary>(
+ r#"
+ SELECT
+ d.id, d.owner_id, d.title, d.goal, d.status, d.repository_url,
+ d.orchestrator_task_id, d.version, d.created_at, d.updated_at,
+ COALESCE((SELECT COUNT(*) FROM directive_steps WHERE directive_id = d.id), 0) as total_steps,
+ COALESCE((SELECT COUNT(*) FROM directive_steps WHERE directive_id = d.id AND status = 'completed'), 0) as completed_steps,
+ COALESCE((SELECT COUNT(*) FROM directive_steps WHERE directive_id = d.id AND status = 'running'), 0) as running_steps,
+ COALESCE((SELECT COUNT(*) FROM directive_steps WHERE directive_id = d.id AND status = 'failed'), 0) as failed_steps
+ FROM directives d
+ WHERE d.owner_id = $1
+ ORDER BY d.created_at DESC
+ "#,
+ )
+ .bind(owner_id)
+ .fetch_all(pool)
+ .await
+}
+
+/// Update a directive with optimistic locking.
+pub async fn update_directive_for_owner(
+ pool: &PgPool,
+ owner_id: Uuid,
+ id: Uuid,
+ req: UpdateDirectiveRequest,
+) -> Result<Option<Directive>, RepositoryError> {
+ let current = sqlx::query_as::<_, Directive>(
+ r#"SELECT * FROM directives WHERE id = $1 AND owner_id = $2"#,
+ )
+ .bind(id)
+ .bind(owner_id)
+ .fetch_optional(pool)
+ .await
+ .map_err(RepositoryError::Database)?;
+
+ let current = match current {
+ Some(c) => c,
+ None => return Ok(None),
+ };
+
+ if let Some(expected_version) = req.version {
+ if expected_version != current.version {
+ return Err(RepositoryError::VersionConflict {
+ expected: expected_version,
+ actual: current.version,
+ });
+ }
+ }
+
+ let title = req.title.as_deref().unwrap_or(&current.title);
+ let goal = req.goal.as_deref().unwrap_or(&current.goal);
+ let status = req.status.as_deref().unwrap_or(&current.status);
+ let repository_url = req.repository_url.as_deref().or(current.repository_url.as_deref());
+ let local_path = req.local_path.as_deref().or(current.local_path.as_deref());
+ let base_branch = req.base_branch.as_deref().or(current.base_branch.as_deref());
+ let orchestrator_task_id = req.orchestrator_task_id.or(current.orchestrator_task_id);
+
+ let result = sqlx::query_as::<_, Directive>(
+ r#"
+ UPDATE directives
+ SET title = $3, goal = $4, status = $5, repository_url = $6, local_path = $7,
+ base_branch = $8, orchestrator_task_id = $9, version = version + 1, updated_at = NOW()
+ WHERE id = $1 AND owner_id = $2
+ RETURNING *
+ "#,
+ )
+ .bind(id)
+ .bind(owner_id)
+ .bind(title)
+ .bind(goal)
+ .bind(status)
+ .bind(repository_url)
+ .bind(local_path)
+ .bind(base_branch)
+ .bind(orchestrator_task_id)
+ .fetch_optional(pool)
+ .await
+ .map_err(RepositoryError::Database)?;
+
+ Ok(result)
+}
+
+/// Delete a directive for an owner.
+pub async fn delete_directive_for_owner(
+ pool: &PgPool,
+ owner_id: Uuid,
+ id: Uuid,
+) -> Result<bool, sqlx::Error> {
+ let result = sqlx::query(
+ r#"DELETE FROM directives WHERE id = $1 AND owner_id = $2"#,
+ )
+ .bind(id)
+ .bind(owner_id)
+ .execute(pool)
+ .await?;
+
+ Ok(result.rows_affected() > 0)
+}
+
+// =============================================================================
+// Directive Step CRUD
+// =============================================================================
+
+/// List all steps for a directive, ordered by order_index.
+pub async fn list_directive_steps(
+ pool: &PgPool,
+ directive_id: Uuid,
+) -> Result<Vec<DirectiveStep>, sqlx::Error> {
+ sqlx::query_as::<_, DirectiveStep>(
+ r#"
+ SELECT * FROM directive_steps
+ WHERE directive_id = $1
+ ORDER BY order_index, created_at
+ "#,
+ )
+ .bind(directive_id)
+ .fetch_all(pool)
+ .await
+}
+
+/// Create a single directive step.
+pub async fn create_directive_step(
+ pool: &PgPool,
+ directive_id: Uuid,
+ req: CreateDirectiveStepRequest,
+) -> Result<DirectiveStep, sqlx::Error> {
+ let generation = req.generation.unwrap_or(1);
+ sqlx::query_as::<_, DirectiveStep>(
+ r#"
+ INSERT INTO directive_steps (directive_id, name, description, task_plan, depends_on, order_index, generation)
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
+ RETURNING *
+ "#,
+ )
+ .bind(directive_id)
+ .bind(&req.name)
+ .bind(&req.description)
+ .bind(&req.task_plan)
+ .bind(&req.depends_on)
+ .bind(req.order_index)
+ .bind(generation)
+ .fetch_one(pool)
+ .await
+}
+
+/// Batch create multiple directive steps.
+pub async fn batch_create_directive_steps(
+ pool: &PgPool,
+ directive_id: Uuid,
+ steps: Vec<CreateDirectiveStepRequest>,
+) -> Result<Vec<DirectiveStep>, sqlx::Error> {
+ let mut results = Vec::with_capacity(steps.len());
+ for req in steps {
+ let step = create_directive_step(pool, directive_id, req).await?;
+ results.push(step);
+ }
+ Ok(results)
+}
+
+/// Update a directive step.
+pub async fn update_directive_step(
+ pool: &PgPool,
+ step_id: Uuid,
+ req: UpdateDirectiveStepRequest,
+) -> Result<Option<DirectiveStep>, sqlx::Error> {
+ let current = sqlx::query_as::<_, DirectiveStep>(
+ r#"SELECT * FROM directive_steps WHERE id = $1"#,
+ )
+ .bind(step_id)
+ .fetch_optional(pool)
+ .await?;
+
+ let current = match current {
+ Some(c) => c,
+ None => return Ok(None),
+ };
+
+ let name = req.name.as_deref().unwrap_or(&current.name);
+ let description = req.description.as_deref().or(current.description.as_deref());
+ let task_plan = req.task_plan.as_deref().or(current.task_plan.as_deref());
+ let depends_on = req.depends_on.as_deref().unwrap_or(&current.depends_on);
+ let status = req.status.as_deref().unwrap_or(&current.status);
+ let task_id = req.task_id.or(current.task_id);
+ let order_index = req.order_index.unwrap_or(current.order_index);
+
+ // Set started_at when transitioning to running
+ let started_at = if status == "running" && current.status != "running" {
+ Some(Utc::now())
+ } else {
+ current.started_at
+ };
+
+ // Set completed_at when transitioning to terminal state
+ let completed_at = if matches!(status, "completed" | "failed" | "skipped")
+ && !matches!(current.status.as_str(), "completed" | "failed" | "skipped")
+ {
+ Some(Utc::now())
+ } else {
+ current.completed_at
+ };
+
+ sqlx::query_as::<_, DirectiveStep>(
+ r#"
+ UPDATE directive_steps
+ SET name = $2, description = $3, task_plan = $4, depends_on = $5,
+ status = $6, task_id = $7, order_index = $8, started_at = $9, completed_at = $10
+ WHERE id = $1
+ RETURNING *
+ "#,
+ )
+ .bind(step_id)
+ .bind(name)
+ .bind(description)
+ .bind(task_plan)
+ .bind(depends_on)
+ .bind(status)
+ .bind(task_id)
+ .bind(order_index)
+ .bind(started_at)
+ .bind(completed_at)
+ .fetch_optional(pool)
+ .await
+}
+
+/// Delete a directive step.
+pub async fn delete_directive_step(
+ pool: &PgPool,
+ step_id: Uuid,
+) -> Result<bool, sqlx::Error> {
+ let result = sqlx::query(r#"DELETE FROM directive_steps WHERE id = $1"#)
+ .bind(step_id)
+ .execute(pool)
+ .await?;
+
+ Ok(result.rows_affected() > 0)
+}
+
+// =============================================================================
+// Directive DAG Progression
+// =============================================================================
+
+/// Advance pending steps to ready if all their dependencies are in terminal states.
+/// Returns the newly-ready steps.
+pub async fn advance_directive_ready_steps(
+ pool: &PgPool,
+ directive_id: Uuid,
+) -> Result<Vec<DirectiveStep>, sqlx::Error> {
+ sqlx::query_as::<_, DirectiveStep>(
+ r#"
+ UPDATE directive_steps SET status = 'ready'
+ WHERE directive_id = $1 AND status = 'pending'
+ AND NOT EXISTS (
+ SELECT 1 FROM unnest(depends_on) AS dep_id
+ JOIN directive_steps ds ON ds.id = dep_id
+ WHERE ds.status NOT IN ('completed', 'skipped')
+ )
+ RETURNING *
+ "#,
+ )
+ .bind(directive_id)
+ .fetch_all(pool)
+ .await
+}
+
+/// Check if all steps in a directive are in terminal states.
+/// If so, set the directive to 'idle' (not completed — directives are ongoing).
+/// Returns true if the directive was set to idle.
+pub async fn check_directive_idle(
+ pool: &PgPool,
+ directive_id: Uuid,
+) -> Result<bool, sqlx::Error> {
+ let result = sqlx::query(
+ r#"
+ UPDATE directives SET status = 'idle', updated_at = NOW()
+ WHERE id = $1 AND status = 'active'
+ AND NOT EXISTS (
+ SELECT 1 FROM directive_steps
+ WHERE directive_id = $1
+ AND status NOT IN ('completed', 'failed', 'skipped')
+ )
+ AND EXISTS (
+ SELECT 1 FROM directive_steps WHERE directive_id = $1
+ )
+ "#,
+ )
+ .bind(directive_id)
+ .execute(pool)
+ .await?;
+
+ Ok(result.rows_affected() > 0)
+}
+
+/// Update a directive's goal and bump goal_updated_at. Reactivates if idle.
+pub async fn update_directive_goal(
+ pool: &PgPool,
+ owner_id: Uuid,
+ directive_id: Uuid,
+ goal: &str,
+) -> Result<Option<Directive>, sqlx::Error> {
+ sqlx::query_as::<_, Directive>(
+ r#"
+ UPDATE directives
+ SET goal = $3,
+ goal_updated_at = NOW(),
+ status = CASE WHEN status = 'idle' THEN 'active' ELSE status END,
+ updated_at = NOW(),
+ version = version + 1
+ WHERE id = $1 AND owner_id = $2
+ RETURNING *
+ "#,
+ )
+ .bind(directive_id)
+ .bind(owner_id)
+ .bind(goal)
+ .fetch_optional(pool)
+ .await
+}
+
+/// Set a directive's status (used for start/pause/archive transitions).
+pub async fn set_directive_status(
+ pool: &PgPool,
+ owner_id: Uuid,
+ directive_id: Uuid,
+ status: &str,
+) -> Result<Option<Directive>, sqlx::Error> {
+ let mut query = String::from(
+ r#"UPDATE directives SET status = $3, updated_at = NOW(), version = version + 1"#,
+ );
+ if status == "active" {
+ query.push_str(", started_at = COALESCE(started_at, NOW())");
+ }
+ query.push_str(" WHERE id = $1 AND owner_id = $2 RETURNING *");
+
+ sqlx::query_as::<_, Directive>(&query)
+ .bind(directive_id)
+ .bind(owner_id)
+ .bind(status)
+ .fetch_optional(pool)
+ .await
+}
+
diff --git a/makima/src/llm/contract_tools.rs b/makima/src/llm/contract_tools.rs
index 7f7e849..38d1a7e 100644
--- a/makima/src/llm/contract_tools.rs
+++ b/makima/src/llm/contract_tools.rs
@@ -460,214 +460,6 @@ pub static CONTRACT_TOOLS: once_cell::sync::Lazy<Vec<Tool>> = once_cell::sync::L
"required": ["file_id"]
}),
},
- // =============================================================================
- // Chain Directive Tools (for directive contracts orchestrating chains)
- // =============================================================================
- Tool {
- name: "create_chain_from_directive".to_string(),
- description: "Create a new chain that this directive contract will orchestrate. The chain starts in 'pending' status and contract definitions can be added. Only available to directive contracts.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "name": {
- "type": "string",
- "description": "Name for the chain"
- },
- "description": {
- "type": "string",
- "description": "Description of what the chain accomplishes"
- }
- },
- "required": ["name"]
- }),
- },
- Tool {
- name: "add_chain_contract".to_string(),
- description: "Add a contract definition to the chain being orchestrated. The contract will be created when its dependencies are met.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "name": {
- "type": "string",
- "description": "Contract name"
- },
- "description": {
- "type": "string",
- "description": "What this contract accomplishes"
- },
- "contract_type": {
- "type": "string",
- "enum": ["simple", "execute", "checkpoint"],
- "description": "Contract type (default: simple)"
- },
- "depends_on": {
- "type": "array",
- "items": { "type": "string" },
- "description": "Names of contracts this depends on"
- },
- "requirement_ids": {
- "type": "array",
- "items": { "type": "string" },
- "description": "Requirement IDs this contract addresses (for traceability)"
- }
- },
- "required": ["name"]
- }),
- },
- Tool {
- name: "set_chain_dependencies".to_string(),
- description: "Set which contracts depend on which other contracts in the chain.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "contract_name": {
- "type": "string",
- "description": "Name of contract that has dependencies"
- },
- "depends_on": {
- "type": "array",
- "items": { "type": "string" },
- "description": "Names of contracts it depends on"
- }
- },
- "required": ["contract_name", "depends_on"]
- }),
- },
- Tool {
- name: "modify_chain_contract".to_string(),
- description: "Update a contract definition in the chain.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "name": {
- "type": "string",
- "description": "Name of the contract to modify"
- },
- "new_name": {
- "type": "string",
- "description": "New name for the contract"
- },
- "description": {
- "type": "string",
- "description": "New description"
- },
- "add_requirement_ids": {
- "type": "array",
- "items": { "type": "string" },
- "description": "Requirement IDs to add"
- },
- "remove_requirement_ids": {
- "type": "array",
- "items": { "type": "string" },
- "description": "Requirement IDs to remove"
- }
- },
- "required": ["name"]
- }),
- },
- Tool {
- name: "remove_chain_contract".to_string(),
- description: "Remove a contract definition from the chain (only if not yet instantiated).".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "name": {
- "type": "string",
- "description": "Name of the contract to remove"
- }
- },
- "required": ["name"]
- }),
- },
- Tool {
- name: "preview_chain_dag".to_string(),
- description: "Generate a visual preview of the chain DAG structure for review.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {}
- }),
- },
- Tool {
- name: "validate_chain_directive".to_string(),
- description: "Validate the chain specification is complete and valid (no cycles, all dependencies exist, all requirements covered).".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {}
- }),
- },
- Tool {
- name: "finalize_chain_directive".to_string(),
- description: "Lock the directive and start chain execution. Call this after validation passes and user has approved (if phase_guard enabled).".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "auto_start": {
- "type": "boolean",
- "description": "Whether to immediately start the chain (default: true)"
- }
- }
- }),
- },
- Tool {
- name: "get_chain_status".to_string(),
- description: "Get current status of the chain being orchestrated, including contract statuses and progress.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {}
- }),
- },
- Tool {
- name: "get_uncovered_requirements".to_string(),
- description: "List requirements from the directive that are not yet mapped to any contract.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {}
- }),
- },
- Tool {
- name: "evaluate_contract_completion".to_string(),
- description: "Evaluate whether a completed chain contract meets the directive requirements. Use this after a contract completes to assess if it satisfies acceptance criteria.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "contract_id": {
- "type": "string",
- "description": "ID of the completed contract to evaluate"
- },
- "passed": {
- "type": "boolean",
- "description": "Whether the evaluation passed"
- },
- "feedback": {
- "type": "string",
- "description": "Evaluation feedback and rationale"
- },
- "rework_instructions": {
- "type": "string",
- "description": "Instructions for rework if evaluation failed"
- }
- },
- "required": ["contract_id", "passed", "feedback"]
- }),
- },
- Tool {
- name: "request_rework".to_string(),
- description: "Request rework on a completed contract that didn't meet requirements. This will block chain progression and notify the contract to address issues.".to_string(),
- parameters: json!({
- "type": "object",
- "properties": {
- "contract_id": {
- "type": "string",
- "description": "ID of the contract needing rework"
- },
- "feedback": {
- "type": "string",
- "description": "Detailed feedback on what needs to be fixed"
- }
- },
- "required": ["contract_id", "feedback"]
- }),
- },
]
});
@@ -755,49 +547,6 @@ pub enum ContractToolRequest {
include_action_items: bool,
},
- // Chain directive tools (for directive contracts)
- CreateChainFromDirective {
- name: String,
- description: Option<String>,
- },
- AddChainContract {
- name: String,
- description: Option<String>,
- contract_type: Option<String>,
- depends_on: Option<Vec<String>>,
- requirement_ids: Option<Vec<String>>,
- },
- SetChainDependencies {
- contract_name: String,
- depends_on: Vec<String>,
- },
- ModifyChainContract {
- name: String,
- new_name: Option<String>,
- description: Option<String>,
- add_requirement_ids: Option<Vec<String>>,
- remove_requirement_ids: Option<Vec<String>>,
- },
- RemoveChainContract {
- name: String,
- },
- PreviewChainDag,
- ValidateChainDirective,
- FinalizeChainDirective {
- auto_start: bool,
- },
- GetChainStatus,
- GetUncoveredRequirements,
- EvaluateContractCompletion {
- contract_id: Uuid,
- passed: bool,
- feedback: String,
- rework_instructions: Option<String>,
- },
- RequestRework {
- contract_id: Uuid,
- feedback: String,
- },
}
/// Task definition for chained task creation
@@ -869,20 +618,6 @@ pub fn parse_contract_tool_call(call: &super::tools::ToolCall) -> ContractToolEx
"analyze_transcript" => parse_analyze_transcript(call),
"create_contract_from_transcript" => parse_create_contract_from_transcript(call),
- // Chain directive tools
- "create_chain_from_directive" => parse_create_chain_from_directive(call),
- "add_chain_contract" => parse_add_chain_contract(call),
- "set_chain_dependencies" => parse_set_chain_dependencies(call),
- "modify_chain_contract" => parse_modify_chain_contract(call),
- "remove_chain_contract" => parse_remove_chain_contract(call),
- "preview_chain_dag" => parse_preview_chain_dag(),
- "validate_chain_directive" => parse_validate_chain_directive(),
- "finalize_chain_directive" => parse_finalize_chain_directive(call),
- "get_chain_status" => parse_get_chain_status(),
- "get_uncovered_requirements" => parse_get_uncovered_requirements(),
- "evaluate_contract_completion" => parse_evaluate_contract_completion(call),
- "request_rework" => parse_request_rework(call),
-
_ => ContractToolExecutionResult {
success: false,
message: format!("Unknown contract tool: {}", call.name),
@@ -1472,229 +1207,6 @@ fn parse_create_contract_from_transcript(call: &super::tools::ToolCall) -> Contr
}
// =============================================================================
-// Chain Directive Tool Parsing
-// =============================================================================
-
-fn parse_create_chain_from_directive(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- let name = call.arguments.get("name").and_then(|v| v.as_str()).map(|s| s.to_string());
- let Some(name) = name else {
- return error_result("Missing required parameter: name");
- };
- let description = call.arguments.get("description").and_then(|v| v.as_str()).map(|s| s.to_string());
-
- ContractToolExecutionResult {
- success: true,
- message: "Creating chain from directive...".to_string(),
- data: None,
- request: Some(ContractToolRequest::CreateChainFromDirective { name, description }),
- pending_questions: None,
- }
-}
-
-fn parse_add_chain_contract(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- let name = call.arguments.get("name").and_then(|v| v.as_str()).map(|s| s.to_string());
- let Some(name) = name else {
- return error_result("Missing required parameter: name");
- };
-
- let description = call.arguments.get("description").and_then(|v| v.as_str()).map(|s| s.to_string());
- let contract_type = call.arguments.get("contract_type").and_then(|v| v.as_str()).map(|s| s.to_string());
- let depends_on = call.arguments.get("depends_on").and_then(|v| {
- v.as_array().map(|arr| {
- arr.iter().filter_map(|item| item.as_str().map(|s| s.to_string())).collect()
- })
- });
- let requirement_ids = call.arguments.get("requirement_ids").and_then(|v| {
- v.as_array().map(|arr| {
- arr.iter().filter_map(|item| item.as_str().map(|s| s.to_string())).collect()
- })
- });
-
- ContractToolExecutionResult {
- success: true,
- message: format!("Adding contract '{}' to chain...", name),
- data: None,
- request: Some(ContractToolRequest::AddChainContract {
- name,
- description,
- contract_type,
- depends_on,
- requirement_ids,
- }),
- pending_questions: None,
- }
-}
-
-fn parse_set_chain_dependencies(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- let contract_name = call.arguments.get("contract_name").and_then(|v| v.as_str()).map(|s| s.to_string());
- let Some(contract_name) = contract_name else {
- return error_result("Missing required parameter: contract_name");
- };
-
- let depends_on = call.arguments.get("depends_on").and_then(|v| {
- v.as_array().map(|arr| {
- arr.iter().filter_map(|item| item.as_str().map(|s| s.to_string())).collect()
- })
- }).unwrap_or_default();
-
- ContractToolExecutionResult {
- success: true,
- message: format!("Setting dependencies for '{}'...", contract_name),
- data: None,
- request: Some(ContractToolRequest::SetChainDependencies { contract_name, depends_on }),
- pending_questions: None,
- }
-}
-
-fn parse_modify_chain_contract(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- let name = call.arguments.get("name").and_then(|v| v.as_str()).map(|s| s.to_string());
- let Some(name) = name else {
- return error_result("Missing required parameter: name");
- };
-
- let new_name = call.arguments.get("new_name").and_then(|v| v.as_str()).map(|s| s.to_string());
- let description = call.arguments.get("description").and_then(|v| v.as_str()).map(|s| s.to_string());
- let add_requirement_ids = call.arguments.get("add_requirement_ids").and_then(|v| {
- v.as_array().map(|arr| {
- arr.iter().filter_map(|item| item.as_str().map(|s| s.to_string())).collect()
- })
- });
- let remove_requirement_ids = call.arguments.get("remove_requirement_ids").and_then(|v| {
- v.as_array().map(|arr| {
- arr.iter().filter_map(|item| item.as_str().map(|s| s.to_string())).collect()
- })
- });
-
- ContractToolExecutionResult {
- success: true,
- message: format!("Modifying contract '{}'...", name),
- data: None,
- request: Some(ContractToolRequest::ModifyChainContract {
- name,
- new_name,
- description,
- add_requirement_ids,
- remove_requirement_ids,
- }),
- pending_questions: None,
- }
-}
-
-fn parse_remove_chain_contract(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- let name = call.arguments.get("name").and_then(|v| v.as_str()).map(|s| s.to_string());
- let Some(name) = name else {
- return error_result("Missing required parameter: name");
- };
-
- ContractToolExecutionResult {
- success: true,
- message: format!("Removing contract '{}'...", name),
- data: None,
- request: Some(ContractToolRequest::RemoveChainContract { name }),
- pending_questions: None,
- }
-}
-
-fn parse_preview_chain_dag() -> ContractToolExecutionResult {
- ContractToolExecutionResult {
- success: true,
- message: "Generating chain DAG preview...".to_string(),
- data: None,
- request: Some(ContractToolRequest::PreviewChainDag),
- pending_questions: None,
- }
-}
-
-fn parse_validate_chain_directive() -> ContractToolExecutionResult {
- ContractToolExecutionResult {
- success: true,
- message: "Validating chain directive...".to_string(),
- data: None,
- request: Some(ContractToolRequest::ValidateChainDirective),
- pending_questions: None,
- }
-}
-
-fn parse_finalize_chain_directive(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- let auto_start = call.arguments.get("auto_start").and_then(|v| v.as_bool()).unwrap_or(true);
-
- ContractToolExecutionResult {
- success: true,
- message: "Finalizing chain directive...".to_string(),
- data: None,
- request: Some(ContractToolRequest::FinalizeChainDirective { auto_start }),
- pending_questions: None,
- }
-}
-
-fn parse_get_chain_status() -> ContractToolExecutionResult {
- ContractToolExecutionResult {
- success: true,
- message: "Getting chain status...".to_string(),
- data: None,
- request: Some(ContractToolRequest::GetChainStatus),
- pending_questions: None,
- }
-}
-
-fn parse_get_uncovered_requirements() -> ContractToolExecutionResult {
- ContractToolExecutionResult {
- success: true,
- message: "Getting uncovered requirements...".to_string(),
- data: None,
- request: Some(ContractToolRequest::GetUncoveredRequirements),
- pending_questions: None,
- }
-}
-
-fn parse_evaluate_contract_completion(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- let contract_id = parse_uuid_arg(call, "contract_id");
- let Some(contract_id) = contract_id else {
- return error_result("Missing or invalid required parameter: contract_id");
- };
-
- let passed = call.arguments.get("passed").and_then(|v| v.as_bool()).unwrap_or(false);
- let feedback = call.arguments.get("feedback").and_then(|v| v.as_str()).map(|s| s.to_string());
- let Some(feedback) = feedback else {
- return error_result("Missing required parameter: feedback");
- };
- let rework_instructions = call.arguments.get("rework_instructions").and_then(|v| v.as_str()).map(|s| s.to_string());
-
- ContractToolExecutionResult {
- success: true,
- message: format!("Evaluating contract completion (passed: {})...", passed),
- data: None,
- request: Some(ContractToolRequest::EvaluateContractCompletion {
- contract_id,
- passed,
- feedback,
- rework_instructions,
- }),
- pending_questions: None,
- }
-}
-
-fn parse_request_rework(call: &super::tools::ToolCall) -> ContractToolExecutionResult {
- let contract_id = parse_uuid_arg(call, "contract_id");
- let Some(contract_id) = contract_id else {
- return error_result("Missing or invalid required parameter: contract_id");
- };
-
- let feedback = call.arguments.get("feedback").and_then(|v| v.as_str()).map(|s| s.to_string());
- let Some(feedback) = feedback else {
- return error_result("Missing required parameter: feedback");
- };
-
- ContractToolExecutionResult {
- success: true,
- message: "Requesting rework...".to_string(),
- data: None,
- request: Some(ContractToolRequest::RequestRework { contract_id, feedback }),
- pending_questions: None,
- }
-}
-
-// =============================================================================
// Helper Functions
// =============================================================================
diff --git a/makima/src/server/handlers/contract_chat.rs b/makima/src/server/handlers/contract_chat.rs
index 8153093..2c7a800 100644
--- a/makima/src/server/handlers/contract_chat.rs
+++ b/makima/src/server/handlers/contract_chat.rs
@@ -1368,6 +1368,8 @@ async fn handle_contract_request(
branched_from_task_id: None,
conversation_history: None,
supervisor_worktree_task_id: None, // Not spawned by supervisor
+ directive_id: None,
+ directive_step_id: None,
};
match repository::create_task_for_owner(pool, owner_id, create_req).await {
@@ -1465,6 +1467,8 @@ async fn handle_contract_request(
branched_from_task_id: None,
conversation_history: None,
supervisor_worktree_task_id: None, // Not spawned by supervisor
+ directive_id: None,
+ directive_step_id: None,
};
match repository::create_task_for_owner(pool, owner_id, create_req).await {
@@ -2217,6 +2221,8 @@ async fn handle_contract_request(
branched_from_task_id: None,
conversation_history: None,
supervisor_worktree_task_id: None, // Not spawned by supervisor
+ directive_id: None,
+ directive_step_id: None,
};
match repository::create_task_for_owner(pool, owner_id, create_req).await {
@@ -2737,6 +2743,8 @@ async fn handle_contract_request(
branched_from_task_id: None,
conversation_history: None,
supervisor_worktree_task_id: None, // Not spawned by supervisor
+ directive_id: None,
+ directive_step_id: None,
};
if repository::create_task_for_owner(pool, owner_id, task_req).await.is_ok() {
@@ -2766,27 +2774,6 @@ async fn handle_contract_request(
}
- // Chain directive tools - TEMPORARILY DISABLED
- // These tools will be reimplemented using the new directive system.
- // See the orchestration module for the new implementation.
- ContractToolRequest::CreateChainFromDirective { .. } |
- ContractToolRequest::AddChainContract { .. } |
- ContractToolRequest::SetChainDependencies { .. } |
- ContractToolRequest::ModifyChainContract { .. } |
- ContractToolRequest::RemoveChainContract { .. } |
- ContractToolRequest::PreviewChainDag |
- ContractToolRequest::ValidateChainDirective |
- ContractToolRequest::FinalizeChainDirective { .. } |
- ContractToolRequest::GetChainStatus |
- ContractToolRequest::GetUncoveredRequirements |
- ContractToolRequest::EvaluateContractCompletion { .. } |
- ContractToolRequest::RequestRework { .. } => {
- ContractRequestResult {
- success: false,
- message: "Chain directive tools are temporarily disabled. The directive system is being reimplemented.".to_string(),
- data: None,
- }
- }
}
}
diff --git a/makima/src/server/handlers/contracts.rs b/makima/src/server/handlers/contracts.rs
index dc15923..bdd4d40 100644
--- a/makima/src/server/handlers/contracts.rs
+++ b/makima/src/server/handlers/contracts.rs
@@ -369,6 +369,8 @@ pub async fn create_contract(
branched_from_task_id: None,
conversation_history: None,
supervisor_worktree_task_id: None, // Supervisor uses its own worktree
+ directive_id: None,
+ directive_step_id: None,
};
match repository::create_task_for_owner(pool, auth.owner_id, supervisor_req).await {
diff --git a/makima/src/server/handlers/directives.rs b/makima/src/server/handlers/directives.rs
new file mode 100644
index 0000000..d48ff74
--- /dev/null
+++ b/makima/src/server/handlers/directives.rs
@@ -0,0 +1,841 @@
+//! HTTP handlers for directive CRUD and DAG progression.
+
+use axum::{
+ extract::{Path, State},
+ http::StatusCode,
+ response::IntoResponse,
+ Json,
+};
+use uuid::Uuid;
+
+use crate::db::models::{
+ CreateDirectiveRequest, CreateDirectiveStepRequest, Directive, DirectiveListResponse,
+ DirectiveStep, DirectiveWithSteps, UpdateDirectiveRequest, UpdateDirectiveStepRequest,
+ UpdateGoalRequest,
+};
+use crate::db::repository;
+use crate::server::auth::Authenticated;
+use crate::server::messages::ApiError;
+use crate::server::state::SharedState;
+
+// =============================================================================
+// Directive CRUD
+// =============================================================================
+
+/// List all directives for the authenticated user.
+#[utoipa::path(
+ get,
+ path = "/api/v1/directives",
+ responses(
+ (status = 200, description = "List of directives", body = DirectiveListResponse),
+ (status = 401, description = "Unauthorized", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directives"
+)]
+pub async fn list_directives(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+) -> 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).await {
+ Ok(directives) => {
+ let total = directives.len() as i64;
+ Json(DirectiveListResponse { directives, total }).into_response()
+ }
+ Err(e) => {
+ tracing::error!("Failed to list directives: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("LIST_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Create a new directive.
+#[utoipa::path(
+ post,
+ path = "/api/v1/directives",
+ request_body = CreateDirectiveRequest,
+ responses(
+ (status = 201, description = "Directive created", body = Directive),
+ (status = 401, description = "Unauthorized", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "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) => (StatusCode::CREATED, Json(directive)).into_response(),
+ Err(e) => {
+ tracing::error!("Failed to create directive: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("CREATE_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Get a directive with all its steps.
+#[utoipa::path(
+ get,
+ path = "/api/v1/directives/{id}",
+ params(("id" = Uuid, Path, description = "Directive ID")),
+ responses(
+ (status = 200, description = "Directive with steps", body = DirectiveWithSteps),
+ (status = 404, description = "Not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directives"
+)]
+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_steps_for_owner(pool, auth.owner_id, id).await {
+ Ok(Some((directive, steps))) => {
+ Json(DirectiveWithSteps { directive, steps }).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("GET_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Update a directive.
+#[utoipa::path(
+ put,
+ path = "/api/v1/directives/{id}",
+ params(("id" = Uuid, Path, description = "Directive ID")),
+ request_body = UpdateDirectiveRequest,
+ responses(
+ (status = 200, description = "Directive updated", body = Directive),
+ (status = 404, description = "Not found", body = ApiError),
+ (status = 409, description = "Version conflict", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directives"
+)]
+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, auth.owner_id, id, req).await {
+ Ok(Some(directive)) => Json(directive).into_response(),
+ Ok(None) => (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response(),
+ Err(repository::RepositoryError::VersionConflict { expected, actual }) => (
+ StatusCode::CONFLICT,
+ Json(ApiError::new(
+ "VERSION_CONFLICT",
+ &format!("Expected version {}, but current is {}", expected, actual),
+ )),
+ )
+ .into_response(),
+ Err(e) => {
+ tracing::error!("Failed to update directive: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("UPDATE_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Delete a directive.
+#[utoipa::path(
+ delete,
+ path = "/api/v1/directives/{id}",
+ params(("id" = Uuid, Path, description = "Directive ID")),
+ responses(
+ (status = 204, description = "Deleted"),
+ (status = 404, description = "Not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directives"
+)]
+pub async fn delete_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::delete_directive_for_owner(pool, auth.owner_id, 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 delete directive: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("DELETE_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+// =============================================================================
+// Step CRUD
+// =============================================================================
+
+/// Create a step in a directive.
+#[utoipa::path(
+ post,
+ path = "/api/v1/directives/{id}/steps",
+ params(("id" = Uuid, Path, description = "Directive ID")),
+ request_body = CreateDirectiveStepRequest,
+ responses(
+ (status = 201, description = "Step created", body = DirectiveStep),
+ (status = 404, description = "Directive not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directives"
+)]
+pub async fn create_step(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+ Json(req): Json<CreateDirectiveStepRequest>,
+) -> 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 directive ownership
+ match repository::get_directive_for_owner(pool, auth.owner_id, 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("GET_FAILED", &e.to_string())),
+ )
+ .into_response();
+ }
+ }
+
+ match repository::create_directive_step(pool, id, req).await {
+ Ok(step) => (StatusCode::CREATED, Json(step)).into_response(),
+ Err(e) => {
+ tracing::error!("Failed to create step: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("CREATE_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Batch create steps in a directive.
+#[utoipa::path(
+ post,
+ path = "/api/v1/directives/{id}/steps/batch",
+ params(("id" = Uuid, Path, description = "Directive ID")),
+ request_body = Vec<CreateDirectiveStepRequest>,
+ responses(
+ (status = 201, description = "Steps created", body = Vec<DirectiveStep>),
+ (status = 404, description = "Directive not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directives"
+)]
+pub async fn batch_create_steps(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+ Json(steps): Json<Vec<CreateDirectiveStepRequest>>,
+) -> 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 directive ownership
+ match repository::get_directive_for_owner(pool, auth.owner_id, 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("GET_FAILED", &e.to_string())),
+ )
+ .into_response();
+ }
+ }
+
+ match repository::batch_create_directive_steps(pool, id, steps).await {
+ Ok(created) => (StatusCode::CREATED, Json(created)).into_response(),
+ Err(e) => {
+ tracing::error!("Failed to batch create steps: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("CREATE_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Update a step.
+#[utoipa::path(
+ put,
+ path = "/api/v1/directives/{id}/steps/{step_id}",
+ params(
+ ("id" = Uuid, Path, description = "Directive ID"),
+ ("step_id" = Uuid, Path, description = "Step ID"),
+ ),
+ request_body = UpdateDirectiveStepRequest,
+ responses(
+ (status = 200, description = "Step updated", body = DirectiveStep),
+ (status = 404, description = "Not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directives"
+)]
+pub async fn update_step(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path((id, step_id)): Path<(Uuid, Uuid)>,
+ Json(req): Json<UpdateDirectiveStepRequest>,
+) -> 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 directive ownership
+ match repository::get_directive_for_owner(pool, auth.owner_id, 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("GET_FAILED", &e.to_string())),
+ )
+ .into_response();
+ }
+ }
+
+ match repository::update_directive_step(pool, step_id, req).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 update step: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("UPDATE_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Delete a step.
+#[utoipa::path(
+ delete,
+ path = "/api/v1/directives/{id}/steps/{step_id}",
+ params(
+ ("id" = Uuid, Path, description = "Directive ID"),
+ ("step_id" = Uuid, Path, description = "Step ID"),
+ ),
+ responses(
+ (status = 204, description = "Deleted"),
+ (status = 404, description = "Not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directives"
+)]
+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 directive ownership
+ match repository::get_directive_for_owner(pool, auth.owner_id, 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("GET_FAILED", &e.to_string())),
+ )
+ .into_response();
+ }
+ }
+
+ match repository::delete_directive_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("DELETE_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+// =============================================================================
+// Directive Lifecycle Actions
+// =============================================================================
+
+/// Start a directive: sets status=active, advances ready steps.
+#[utoipa::path(
+ post,
+ path = "/api/v1/directives/{id}/start",
+ params(("id" = Uuid, Path, description = "Directive ID")),
+ responses(
+ (status = 200, description = "Directive started", body = DirectiveWithSteps),
+ (status = 404, description = "Not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directives"
+)]
+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();
+ };
+
+ // Set to active
+ match repository::set_directive_status(pool, auth.owner_id, id, "active").await {
+ Ok(Some(directive)) => {
+ // Advance ready steps
+ let _ = repository::advance_directive_ready_steps(pool, id).await;
+ let steps = repository::list_directive_steps(pool, id).await.unwrap_or_default();
+ Json(DirectiveWithSteps { directive, steps }).into_response()
+ }
+ Ok(None) => (
+ StatusCode::NOT_FOUND,
+ Json(ApiError::new("NOT_FOUND", "Directive not found")),
+ )
+ .into_response(),
+ Err(e) => {
+ tracing::error!("Failed to start directive: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("START_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Pause a directive.
+#[utoipa::path(
+ post,
+ path = "/api/v1/directives/{id}/pause",
+ params(("id" = Uuid, Path, description = "Directive ID")),
+ responses(
+ (status = 200, description = "Directive paused", body = Directive),
+ (status = 404, description = "Not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directives"
+)]
+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();
+ };
+
+ match repository::set_directive_status(pool, auth.owner_id, id, "paused").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("PAUSE_FAILED", &e.to_string())),
+ )
+ .into_response(),
+ }
+}
+
+/// Advance a directive: find newly-ready steps. If all steps done, set idle.
+#[utoipa::path(
+ post,
+ path = "/api/v1/directives/{id}/advance",
+ params(("id" = Uuid, Path, description = "Directive ID")),
+ responses(
+ (status = 200, description = "Advance result", body = DirectiveWithSteps),
+ (status = 404, description = "Not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directives"
+)]
+pub async fn advance_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
+ let directive = match repository::get_directive_for_owner(pool, auth.owner_id, 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("GET_FAILED", &e.to_string())),
+ )
+ .into_response();
+ }
+ };
+
+ // Advance ready steps
+ let _ = repository::advance_directive_ready_steps(pool, id).await;
+
+ // Check if idle
+ let _ = repository::check_directive_idle(pool, id).await;
+
+ // Return updated state
+ let directive = match repository::get_directive_for_owner(pool, auth.owner_id, id).await {
+ Ok(Some(d)) => d,
+ _ => directive,
+ };
+ let steps = repository::list_directive_steps(pool, id).await.unwrap_or_default();
+ Json(DirectiveWithSteps { directive, steps }).into_response()
+}
+
+/// Mark a step as completed.
+#[utoipa::path(
+ post,
+ path = "/api/v1/directives/{id}/steps/{step_id}/complete",
+ params(
+ ("id" = Uuid, Path, description = "Directive ID"),
+ ("step_id" = Uuid, Path, description = "Step ID"),
+ ),
+ responses(
+ (status = 200, description = "Step completed", body = DirectiveStep),
+ (status = 404, description = "Not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directives"
+)]
+pub async fn complete_step(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path((id, step_id)): Path<(Uuid, Uuid)>,
+) -> impl IntoResponse {
+ step_status_change(state, auth, id, step_id, "completed").await
+}
+
+/// Mark a step as failed.
+#[utoipa::path(
+ post,
+ path = "/api/v1/directives/{id}/steps/{step_id}/fail",
+ params(
+ ("id" = Uuid, Path, description = "Directive ID"),
+ ("step_id" = Uuid, Path, description = "Step ID"),
+ ),
+ responses(
+ (status = 200, description = "Step failed", body = DirectiveStep),
+ (status = 404, description = "Not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directives"
+)]
+pub async fn fail_step(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path((id, step_id)): Path<(Uuid, Uuid)>,
+) -> impl IntoResponse {
+ step_status_change(state, auth, id, step_id, "failed").await
+}
+
+/// Mark a step as skipped.
+#[utoipa::path(
+ post,
+ path = "/api/v1/directives/{id}/steps/{step_id}/skip",
+ params(
+ ("id" = Uuid, Path, description = "Directive ID"),
+ ("step_id" = Uuid, Path, description = "Step ID"),
+ ),
+ responses(
+ (status = 200, description = "Step skipped", body = DirectiveStep),
+ (status = 404, description = "Not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directives"
+)]
+pub async fn skip_step(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path((id, step_id)): Path<(Uuid, Uuid)>,
+) -> impl IntoResponse {
+ step_status_change(state, auth, id, step_id, "skipped").await
+}
+
+/// Helper for step status changes.
+async fn step_status_change(
+ state: SharedState,
+ auth: crate::server::auth::AuthenticatedUser,
+ directive_id: Uuid,
+ step_id: Uuid,
+ new_status: &str,
+) -> 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 directive ownership
+ match repository::get_directive_for_owner(pool, auth.owner_id, directive_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("GET_FAILED", &e.to_string())),
+ )
+ .into_response();
+ }
+ }
+
+ let req = UpdateDirectiveStepRequest {
+ status: Some(new_status.to_string()),
+ ..Default::default()
+ };
+
+ match repository::update_directive_step(pool, step_id, req).await {
+ Ok(Some(step)) => {
+ // After step status change, advance the DAG
+ let _ = repository::advance_directive_ready_steps(pool, directive_id).await;
+ let _ = repository::check_directive_idle(pool, directive_id).await;
+ 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 update step status: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("UPDATE_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
+
+/// Update a directive's goal (triggers re-planning).
+#[utoipa::path(
+ put,
+ path = "/api/v1/directives/{id}/goal",
+ params(("id" = Uuid, Path, description = "Directive ID")),
+ request_body = UpdateGoalRequest,
+ responses(
+ (status = 200, description = "Goal updated", body = Directive),
+ (status = 404, description = "Not found", body = ApiError),
+ (status = 503, description = "Database not configured", body = ApiError),
+ ),
+ security(("bearer_auth" = []), ("api_key" = [])),
+ tag = "Directives"
+)]
+pub async fn update_goal(
+ State(state): State<SharedState>,
+ Authenticated(auth): Authenticated,
+ Path(id): Path<Uuid>,
+ Json(req): Json<UpdateGoalRequest>,
+) -> 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_goal(pool, auth.owner_id, id, &req.goal).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 update goal: {}", e);
+ (
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(ApiError::new("UPDATE_FAILED", &e.to_string())),
+ )
+ .into_response()
+ }
+ }
+}
diff --git a/makima/src/server/handlers/mesh.rs b/makima/src/server/handlers/mesh.rs
index fe9ffc0..310bec8 100644
--- a/makima/src/server/handlers/mesh.rs
+++ b/makima/src/server/handlers/mesh.rs
@@ -2626,6 +2626,8 @@ pub async fn reassign_task(
branched_from_task_id: None,
conversation_history: None,
supervisor_worktree_task_id: None, // Not spawned by supervisor
+ directive_id: None,
+ directive_step_id: None,
};
let new_task = match repository::create_task_for_owner(pool, auth.owner_id, create_req).await {
@@ -3402,6 +3404,8 @@ pub async fn fork_task(
branched_from_task_id: None,
conversation_history: None,
supervisor_worktree_task_id: None, // Not spawned by supervisor
+ directive_id: None,
+ directive_step_id: None,
};
let new_task = match repository::create_task_for_owner(pool, auth.owner_id, create_req).await {
@@ -3560,6 +3564,8 @@ pub async fn resume_from_checkpoint(
branched_from_task_id: None,
conversation_history: None,
supervisor_worktree_task_id: None, // Not spawned by supervisor
+ directive_id: None,
+ directive_step_id: None,
};
let new_task = match repository::create_task_for_owner(pool, auth.owner_id, create_req).await {
@@ -3896,6 +3902,8 @@ pub async fn branch_task(
branched_from_task_id: Some(source_task_id),
conversation_history,
supervisor_worktree_task_id: None, // Not spawned by supervisor
+ directive_id: None,
+ directive_step_id: None,
};
let task = match repository::create_task_for_owner(pool, auth.owner_id, create_req).await {
diff --git a/makima/src/server/handlers/mesh_chat.rs b/makima/src/server/handlers/mesh_chat.rs
index a6a3a3c..cf56ab6 100644
--- a/makima/src/server/handlers/mesh_chat.rs
+++ b/makima/src/server/handlers/mesh_chat.rs
@@ -1021,6 +1021,8 @@ async fn handle_mesh_request(
branched_from_task_id: None,
conversation_history: None,
supervisor_worktree_task_id: None, // Not spawned by supervisor
+ directive_id: None,
+ directive_step_id: None,
};
match repository::create_task_for_owner(pool, owner_id, create_req).await {
diff --git a/makima/src/server/handlers/mesh_daemon.rs b/makima/src/server/handlers/mesh_daemon.rs
index 87b5e44..2ea7805 100644
--- a/makima/src/server/handlers/mesh_daemon.rs
+++ b/makima/src/server/handlers/mesh_daemon.rs
@@ -1303,6 +1303,23 @@ async fn handle_daemon_connection(socket: WebSocket, state: SharedState, auth_re
}),
).await;
+ // Auto-advance directive DAG when a directive step task completes
+ if let Some(step_id) = updated_task.directive_step_id {
+ let step_status = if updated_task.status == "done" { "completed" } else { "failed" };
+ let step_update = crate::db::models::UpdateDirectiveStepRequest {
+ status: Some(step_status.to_string()),
+ ..Default::default()
+ };
+ let _ = repository::update_directive_step(&pool, step_id, step_update).await;
+
+ if let Some(directive_id) = updated_task.directive_id {
+ // Advance newly-ready steps in the DAG
+ let _ = repository::advance_directive_ready_steps(&pool, directive_id).await;
+ // Check if all steps are done → set directive to idle
+ let _ = repository::check_directive_idle(&pool, directive_id).await;
+ }
+ }
+
}
Ok(None) => {
tracing::warn!(
diff --git a/makima/src/server/handlers/mesh_supervisor.rs b/makima/src/server/handlers/mesh_supervisor.rs
index 09758bb..8bf2534 100644
--- a/makima/src/server/handlers/mesh_supervisor.rs
+++ b/makima/src/server/handlers/mesh_supervisor.rs
@@ -629,6 +629,8 @@ pub async fn spawn_task(
branched_from_task_id: None,
conversation_history: None,
supervisor_worktree_task_id,
+ directive_id: None,
+ directive_step_id: None,
};
// Create task in DB
diff --git a/makima/src/server/handlers/mod.rs b/makima/src/server/handlers/mod.rs
index ae370c9..29cd09f 100644
--- a/makima/src/server/handlers/mod.rs
+++ b/makima/src/server/handlers/mod.rs
@@ -6,6 +6,7 @@ pub mod contract_chat;
pub mod contract_daemon;
pub mod contract_discuss;
pub mod contracts;
+pub mod directives;
pub mod file_ws;
pub mod files;
pub mod history;
diff --git a/makima/src/server/handlers/transcript_analysis.rs b/makima/src/server/handlers/transcript_analysis.rs
index 62c65a6..9261c0c 100644
--- a/makima/src/server/handlers/transcript_analysis.rs
+++ b/makima/src/server/handlers/transcript_analysis.rs
@@ -370,6 +370,8 @@ pub async fn create_contract_from_analysis(
branched_from_task_id: None,
conversation_history: None,
supervisor_worktree_task_id: None, // Not spawned by supervisor
+ directive_id: None,
+ directive_step_id: None,
};
if let Ok(t) = repository::create_task_for_owner(pool, auth.owner_id, task_req).await {
@@ -540,6 +542,8 @@ pub async fn update_contract_from_analysis(
branched_from_task_id: None,
conversation_history: None,
supervisor_worktree_task_id: None, // Not spawned by supervisor
+ directive_id: None,
+ directive_step_id: None,
};
if let Ok(t) = repository::create_task_for_owner(pool, auth.owner_id, task_req).await {
diff --git a/makima/src/server/mod.rs b/makima/src/server/mod.rs
index b7a4156..9e1ee50 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, 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, 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::openapi::ApiDoc;
use crate::server::state::SharedState;
@@ -212,6 +212,30 @@ pub fn make_router(state: SharedState) -> Router {
"/contracts/{id}/tasks/{task_id}",
post(contracts::add_task_to_contract).delete(contracts::remove_task_from_contract),
)
+ // Directive endpoints
+ .route(
+ "/directives",
+ get(directives::list_directives).post(directives::create_directive),
+ )
+ .route(
+ "/directives/{id}",
+ get(directives::get_directive)
+ .put(directives::update_directive)
+ .delete(directives::delete_directive),
+ )
+ .route("/directives/{id}/steps", post(directives::create_step))
+ .route("/directives/{id}/steps/batch", post(directives::batch_create_steps))
+ .route(
+ "/directives/{id}/steps/{step_id}",
+ put(directives::update_step).delete(directives::delete_step),
+ )
+ .route("/directives/{id}/start", post(directives::start_directive))
+ .route("/directives/{id}/pause", post(directives::pause_directive))
+ .route("/directives/{id}/advance", post(directives::advance_directive))
+ .route("/directives/{id}/steps/{step_id}/complete", post(directives::complete_step))
+ .route("/directives/{id}/steps/{step_id}/fail", post(directives::fail_step))
+ .route("/directives/{id}/steps/{step_id}/skip", post(directives::skip_step))
+ .route("/directives/{id}/goal", put(directives::update_goal))
// Timeline endpoint (unified history for user)
.route("/timeline", get(history::get_timeline))
// Contract type templates (built-in only)
diff --git a/makima/src/server/openapi.rs b/makima/src/server/openapi.rs
index 0b6bfba..4e3b85b 100644
--- a/makima/src/server/openapi.rs
+++ b/makima/src/server/openapi.rs
@@ -8,9 +8,10 @@ use crate::db::models::{
ChangePhaseRequest,
Contract, ContractChatHistoryResponse, ContractChatMessageRecord, ContractEvent,
ContractListResponse, ContractRepository, ContractSummary, ContractWithRelations,
- CreateContractRequest, CreateFileRequest,
+ CreateContractRequest, CreateDirectiveRequest, CreateDirectiveStepRequest, CreateFileRequest,
CreateManagedRepositoryRequest, CreateTaskRequest, Daemon, DaemonDirectoriesResponse,
- DaemonDirectory, DaemonListResponse,
+ DaemonDirectory, DaemonListResponse, Directive, DirectiveListResponse, DirectiveStep,
+ DirectiveSummary, DirectiveWithSteps,
File, FileListResponse, FileSummary,
MergeCommitRequest, MergeCompleteCheckResponse, MergeResolveRequest, MergeResultResponse,
MergeSkipRequest, MergeStartRequest, MergeStatusResponse, MeshChatConversation,
@@ -18,13 +19,14 @@ use crate::db::models::{
RepositoryHistoryListResponse, RepositorySuggestionsQuery, SendMessageRequest,
Task,
TaskEventListResponse, TaskListResponse, TaskSummary, TaskWithSubtasks, TranscriptEntry,
- UpdateContractRequest, UpdateFileRequest, UpdateTaskRequest,
+ UpdateContractRequest, UpdateDirectiveRequest, UpdateDirectiveStepRequest,
+ UpdateFileRequest, UpdateGoalRequest, UpdateTaskRequest,
};
use crate::server::auth::{
ApiKey, ApiKeyInfoResponse, CreateApiKeyRequest, CreateApiKeyResponse,
RefreshApiKeyRequest, RefreshApiKeyResponse, RevokeApiKeyResponse,
};
-use crate::server::handlers::{api_keys, contract_chat, contract_discuss, contracts, files, listen, mesh, mesh_chat, mesh_merge, repository_history, users};
+use crate::server::handlers::{api_keys, contract_chat, contract_discuss, contracts, directives, files, listen, mesh, mesh_chat, mesh_merge, repository_history, users};
use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage, TranscriptMessage};
#[derive(OpenApi)]
@@ -103,6 +105,23 @@ use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage
contract_chat::clear_contract_chat_history,
// Contract discuss endpoint
contract_discuss::discuss_contract_handler,
+ // Directive endpoints
+ directives::list_directives,
+ directives::create_directive,
+ directives::get_directive,
+ directives::update_directive,
+ directives::delete_directive,
+ directives::create_step,
+ directives::batch_create_steps,
+ directives::update_step,
+ directives::delete_step,
+ directives::start_directive,
+ directives::pause_directive,
+ directives::advance_directive,
+ directives::complete_step,
+ directives::fail_step,
+ directives::skip_step,
+ directives::update_goal,
// Repository history/settings endpoints
repository_history::list_repository_history,
repository_history::get_repository_suggestions,
@@ -187,6 +206,17 @@ use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage
AddLocalRepositoryRequest,
CreateManagedRepositoryRequest,
ChangePhaseRequest,
+ // Directive schemas
+ Directive,
+ DirectiveStep,
+ DirectiveWithSteps,
+ DirectiveSummary,
+ DirectiveListResponse,
+ CreateDirectiveRequest,
+ UpdateDirectiveRequest,
+ UpdateGoalRequest,
+ CreateDirectiveStepRequest,
+ UpdateDirectiveStepRequest,
// Repository history schemas
RepositoryHistoryEntry,
RepositoryHistoryListResponse,
@@ -200,6 +230,7 @@ use crate::server::messages::{ApiError, AudioEncoding, StartMessage, StopMessage
(name = "Contracts", description = "Contract management with workflow phases"),
(name = "API Keys", description = "API key management for programmatic access"),
(name = "Users", description = "User account management"),
+ (name = "Directives", description = "Directive management with DAG-based step progression"),
(name = "Settings", description = "User settings including repository history"),
)
)]