//! Database models for the files table. use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::FromRow; use utoipa::ToSchema; use uuid::Uuid; /// Default max retries for task daemon failover (3 attempts) fn default_max_retries() -> i32 { 3 } /// Flexible datetime deserialization module. /// Accepts both date-only ("2026-01-15") and full ISO 8601 datetime ("2026-01-15T00:00:00Z") formats. pub mod flexible_datetime { use chrono::{DateTime, NaiveDate, NaiveTime, TimeZone, Utc}; use serde::{self, Deserialize, Deserializer}; /// Deserializes a datetime from either date-only or full datetime format. pub fn deserialize<'de, D>(deserializer: D) -> Result>, D::Error> where D: Deserializer<'de>, { let s: Option = Option::deserialize(deserializer)?; match s { None => Ok(None), Some(s) if s.is_empty() => Ok(None), Some(s) => { // Try full datetime first (RFC 3339 / ISO 8601) if let Ok(dt) = DateTime::parse_from_rfc3339(&s) { return Ok(Some(dt.with_timezone(&Utc))); } // Try date-only format (YYYY-MM-DD) and convert to start of day UTC if let Ok(date) = NaiveDate::parse_from_str(&s, "%Y-%m-%d") { let datetime = date.and_time(NaiveTime::MIN); return Ok(Some(Utc.from_utc_datetime(&datetime))); } Err(serde::de::Error::custom(format!( "Invalid datetime format '{}'. Expected ISO 8601 datetime (e.g., '2026-01-15T00:00:00Z') or date (e.g., '2026-01-15')", s ))) } } } } /// TranscriptEntry stored in JSONB - matches frontend TranscriptEntry #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct TranscriptEntry { pub id: String, pub speaker: String, pub start: f32, pub end: f32, pub text: String, pub is_final: bool, } /// Chart type for visualization elements #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "lowercase")] pub enum ChartType { Line, Bar, Pie, Area, } /// Body element types for structured file content #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(tag = "type", rename_all = "camelCase")] pub enum BodyElement { /// Heading element (h1-h6) Heading { level: u8, text: String }, /// Paragraph text Paragraph { text: String }, /// Code block with optional language Code { language: Option, content: String, }, /// List (ordered or unordered) List { ordered: bool, items: Vec, }, /// Chart visualization Chart { #[serde(rename = "chartType")] chart_type: ChartType, title: Option, data: serde_json::Value, config: Option, }, /// Image element (deferred for MVP) Image { src: String, alt: Option, caption: Option, }, /// Raw markdown content - renders as formatted markdown, edits as raw text Markdown { content: String }, } /// File record from the database. #[derive(Debug, Clone, FromRow, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct File { pub id: Uuid, pub owner_id: Uuid, /// Contract this file belongs to (optional) pub contract_id: Option, /// Phase of the contract when file was added (e.g., "research", "specify") pub contract_phase: Option, pub name: String, pub description: Option, #[sqlx(json)] pub transcript: Vec, pub location: Option, /// AI-generated summary of the transcript pub summary: Option, /// Structured body content (headings, paragraphs, charts) #[sqlx(json)] pub body: Vec, /// Version number for optimistic locking pub version: i32, /// Path to linked repository file (e.g., "README.md", "docs/design.md") pub repo_file_path: Option, /// When the file was last synced from the repository pub repo_synced_at: Option>, /// Sync status: 'none', 'synced', 'modified', 'conflict' pub repo_sync_status: Option, pub created_at: DateTime, pub updated_at: DateTime, } /// Request payload for creating a new file. #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct CreateFileRequest { /// Contract this file belongs to (required - files must belong to a contract) pub contract_id: Uuid, /// Name of the file (auto-generated if not provided) pub name: Option, /// Optional description pub description: Option, /// Transcript entries (default to empty) #[serde(default)] pub transcript: Vec, /// Storage location (e.g., s3://bucket/path) - not used yet pub location: Option, /// Initial body elements (e.g., from a template) #[serde(default)] pub body: Vec, /// Path to linked repository file (e.g., "README.md") pub repo_file_path: Option, /// Contract phase this file belongs to (for deliverable tracking) pub contract_phase: Option, } /// Request payload for updating an existing file. #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct UpdateFileRequest { /// New name (optional) pub name: Option, /// New description (optional) pub description: Option, /// New transcript (optional) pub transcript: Option>, /// AI-generated summary (optional) pub summary: Option, /// Structured body content (optional) pub body: Option>, /// Version for optimistic locking (required for updates from frontend) pub version: Option, /// Path to linked repository file (e.g., "README.md") pub repo_file_path: Option, } /// Response for file list endpoint. #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct FileListResponse { pub files: Vec, pub total: i64, } /// Summary of a file for list views (excludes full transcript). #[derive(Debug, Clone, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct FileSummary { pub id: Uuid, /// Contract this file belongs to pub contract_id: Option, /// Contract name (joined from contracts table) pub contract_name: Option, /// Phase when file was added to contract pub contract_phase: Option, pub name: String, pub description: Option, pub transcript_count: usize, /// Duration derived from last transcript end time pub duration: Option, /// Version number for optimistic locking pub version: i32, /// Path to linked repository file pub repo_file_path: Option, /// Sync status with repository pub repo_sync_status: Option, pub created_at: DateTime, pub updated_at: DateTime, } impl From for FileSummary { fn from(file: File) -> Self { let duration = file .transcript .iter() .map(|t| t.end) .fold(0.0_f32, f32::max); Self { id: file.id, contract_id: file.contract_id, contract_name: None, // Not available from File alone, requires JOIN contract_phase: file.contract_phase, name: file.name, description: file.description, transcript_count: file.transcript.len(), duration: if duration > 0.0 { Some(duration) } else { None }, version: file.version, repo_file_path: file.repo_file_path, repo_sync_status: file.repo_sync_status, created_at: file.created_at, updated_at: file.updated_at, } } } // ============================================================================= // Version History Types // ============================================================================= /// Source of a version change #[derive(Debug, Clone, Serialize, Deserialize, ToSchema, sqlx::Type)] #[sqlx(type_name = "varchar")] #[serde(rename_all = "lowercase")] pub enum VersionSource { #[sqlx(rename = "user")] User, #[sqlx(rename = "llm")] Llm, #[sqlx(rename = "system")] System, } impl std::fmt::Display for VersionSource { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { VersionSource::User => write!(f, "user"), VersionSource::Llm => write!(f, "llm"), VersionSource::System => write!(f, "system"), } } } impl std::str::FromStr for VersionSource { type Err = String; fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "user" => Ok(VersionSource::User), "llm" => Ok(VersionSource::Llm), "system" => Ok(VersionSource::System), _ => Err(format!("Unknown version source: {}", s)), } } } /// Full version record from the database #[derive(Debug, Clone, FromRow, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct FileVersion { pub id: Uuid, pub file_id: Uuid, pub version: i32, pub name: String, pub description: Option, pub summary: Option, #[sqlx(json)] pub body: Vec, pub source: String, pub change_description: Option, pub created_at: DateTime, } /// Summary of a version for list views #[derive(Debug, Clone, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct FileVersionSummary { pub version: i32, pub source: String, pub created_at: DateTime, pub change_description: Option, } impl From for FileVersionSummary { fn from(v: FileVersion) -> Self { Self { version: v.version, source: v.source, created_at: v.created_at, change_description: v.change_description, } } } /// Response for version list endpoint #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct FileVersionListResponse { pub versions: Vec, pub total: i64, } /// Request to restore a file to a previous version #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct RestoreVersionRequest { /// The version to restore to pub target_version: i32, /// The current version (for optimistic locking) pub current_version: i32, } // ============================================================================= // Mesh/Task Types // ============================================================================= /// Task status for orchestrating Claude Code instances #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "lowercase")] pub enum TaskStatus { Pending, Running, Paused, Blocked, Done, Failed, Merged, } impl std::fmt::Display for TaskStatus { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { TaskStatus::Pending => write!(f, "pending"), TaskStatus::Running => write!(f, "running"), TaskStatus::Paused => write!(f, "paused"), TaskStatus::Blocked => write!(f, "blocked"), TaskStatus::Done => write!(f, "done"), TaskStatus::Failed => write!(f, "failed"), TaskStatus::Merged => write!(f, "merged"), } } } impl std::str::FromStr for TaskStatus { type Err = String; fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "pending" => Ok(TaskStatus::Pending), "running" => Ok(TaskStatus::Running), "paused" => Ok(TaskStatus::Paused), "blocked" => Ok(TaskStatus::Blocked), "done" => Ok(TaskStatus::Done), "failed" => Ok(TaskStatus::Failed), "merged" => Ok(TaskStatus::Merged), _ => Err(format!("Unknown task status: {}", s)), } } } /// Merge mode for task completion #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "lowercase")] pub enum MergeMode { /// Create a PR for review Pr, /// Auto-merge to target branch Auto, /// Manual merge by user Manual, } impl std::fmt::Display for MergeMode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { MergeMode::Pr => write!(f, "pr"), MergeMode::Auto => write!(f, "auto"), MergeMode::Manual => write!(f, "manual"), } } } impl std::str::FromStr for MergeMode { type Err = String; fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "pr" => Ok(MergeMode::Pr), "auto" => Ok(MergeMode::Auto), "manual" => Ok(MergeMode::Manual), _ => Err(format!("Unknown merge mode: {}", s)), } } } /// Task record from the database #[derive(Debug, Clone, FromRow, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct Task { pub id: Uuid, pub owner_id: Uuid, /// Contract this task belongs to (required for new tasks) pub contract_id: Option, pub parent_task_id: Option, /// Depth in task hierarchy (no longer constrained) pub depth: i32, pub name: String, pub description: Option, pub status: String, pub priority: i32, pub plan: String, // Supervisor flag /// True for contract supervisor tasks. Only supervisors can spawn new tasks. #[serde(default)] pub is_supervisor: bool, // Daemon/container info pub daemon_id: Option, pub container_id: Option, pub overlay_path: Option, // Repository info pub repository_url: Option, pub base_branch: Option, pub target_branch: Option, // Merge settings pub merge_mode: Option, pub pr_url: Option, // Completion action settings /// Path to user's local repository (outside ~/.makima) pub target_repo_path: Option, /// Action on completion: "none", "branch", "merge", "pr" pub completion_action: Option, // Progress tracking pub progress_summary: Option, pub last_output: Option, pub error_message: Option, // Git checkpoint tracking /// Git commit SHA of the most recent checkpoint #[serde(skip_serializing_if = "Option::is_none")] pub last_checkpoint_sha: Option, /// Number of checkpoints created by this task #[serde(default)] pub checkpoint_count: i32, /// Message from the most recent checkpoint #[serde(skip_serializing_if = "Option::is_none")] pub checkpoint_message: Option, // Conversation state for resumption /// Saved conversation context for task/supervisor resumption #[serde(skip_serializing_if = "Option::is_none")] pub conversation_state: Option, // Daemon migration tracking /// Previous daemon if task was migrated #[serde(skip_serializing_if = "Option::is_none")] pub migrated_from_daemon_id: Option, /// Most recent daemon that worked on this task #[serde(skip_serializing_if = "Option::is_none")] pub last_active_daemon_id: Option, // Timestamps pub started_at: Option>, pub completed_at: Option>, pub version: i32, pub created_at: DateTime, pub updated_at: DateTime, // Task continuation /// Task ID to continue from (copy worktree from this task when starting). /// Used for sequential subtask dependencies. #[serde(skip_serializing_if = "Option::is_none")] pub continue_from_task_id: Option, /// Files to copy from parent task's worktree when starting. #[serde(skip_serializing_if = "Option::is_none")] pub copy_files: Option, // Retry tracking for daemon failover /// Number of times this task has been retried after daemon failure #[serde(default)] pub retry_count: i32, /// Maximum retry attempts before marking as permanently failed #[serde(default = "default_max_retries")] pub max_retries: i32, /// Array of daemon IDs that have failed this task (excluded from retry) #[serde(skip_serializing_if = "Option::is_none")] pub failed_daemon_ids: Option>, /// When the task was last interrupted due to daemon disconnect #[serde(skip_serializing_if = "Option::is_none")] pub interrupted_at: Option>, // Task branching /// Source task ID when this task was branched from another task's conversation. /// Used to track the origin of "what if" explorations. #[serde(skip_serializing_if = "Option::is_none")] pub branched_from_task_id: Option, // UI visibility /// Whether this task is hidden from the UI (user dismissed it). /// 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, /// Directive step this task executes #[serde(skip_serializing_if = "Option::is_none")] pub directive_step_id: Option, /// Directive document this task is an artifact of. Set when the /// orchestrator (or any task-creation path) knows which document /// triggered the work — so when that document ships, the sidebar can /// move its tasks alongside it under shipped/. Nullable for legacy / /// non-directive tasks. #[serde(skip_serializing_if = "Option::is_none")] pub directive_document_id: Option, } impl Task { /// Parse status string to TaskStatus enum pub fn status_enum(&self) -> Result { self.status.parse() } /// Parse merge_mode string to MergeMode enum pub fn merge_mode_enum(&self) -> Option> { self.merge_mode.as_ref().map(|s| s.parse()) } } /// Summary of a task for list views #[derive(Debug, Clone, FromRow, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct TaskSummary { pub id: Uuid, /// Contract this task belongs to pub contract_id: Option, /// Contract name (joined from contracts table) pub contract_name: Option, /// Contract phase (joined from contracts table) pub contract_phase: Option, /// Contract status (joined from contracts table): 'active', 'completed', 'archived' pub contract_status: Option, pub parent_task_id: Option, /// Depth in task hierarchy: 0=orchestrator (top-level), 1=subtask (max) pub depth: i32, pub name: String, pub status: String, pub priority: i32, pub progress_summary: Option, pub subtask_count: i64, pub version: i32, /// True for contract supervisor tasks #[serde(default)] pub is_supervisor: bool, /// Whether this task is hidden from the UI (user dismissed it) #[serde(default)] pub hidden: bool, pub created_at: DateTime, pub updated_at: DateTime, } /// Convert a full Task to a TaskSummary impl From for TaskSummary { fn from(task: Task) -> Self { Self { id: task.id, contract_id: task.contract_id, contract_name: None, // Not available from Task directly contract_phase: None, // Not available from Task directly contract_status: None, // Not available from Task directly parent_task_id: task.parent_task_id, depth: task.depth, name: task.name, status: task.status, priority: task.priority, progress_summary: task.progress_summary, subtask_count: 0, // Would need separate query version: task.version, is_supervisor: task.is_supervisor, hidden: task.hidden, created_at: task.created_at, updated_at: task.updated_at, } } } /// Response for task list endpoint #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct TaskListResponse { pub tasks: Vec, pub total: i64, } /// Request payload for creating a new task #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct CreateTaskRequest { /// Contract this task belongs to (optional for branched/anonymous tasks) pub contract_id: Option, /// Name of the task pub name: String, /// Optional description pub description: Option, /// The plan/instructions for Claude Code pub plan: String, /// Parent task ID (for subtasks) pub parent_task_id: Option, /// True for contract supervisor tasks. Only supervisors can spawn new tasks. #[serde(default)] pub is_supervisor: bool, /// Priority (higher = more urgent) #[serde(default)] pub priority: i32, /// Repository URL pub repository_url: Option, /// Base branch for overlay pub base_branch: Option, /// Target branch to merge into pub target_branch: Option, /// Merge mode (pr, auto, manual) pub merge_mode: Option, /// Path to user's local repository (outside ~/.makima) pub target_repo_path: Option, /// Action on completion: "none", "branch", "merge", "pr" pub completion_action: Option, /// Task ID to continue from (copy worktree from this task when starting) pub continue_from_task_id: Option, /// Files to copy from parent task's worktree when starting #[serde(skip_serializing_if = "Option::is_none")] pub copy_files: Option>, /// Checkpoint SHA to branch from (optional) pub checkpoint_sha: Option, /// Source task ID when branching from another task's conversation pub branched_from_task_id: Option, /// Conversation history to initialize the task with (JSON array of messages) pub conversation_history: Option, /// 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, /// Directive this task belongs to (for directive-driven tasks) pub directive_id: Option, /// Directive step this task executes pub directive_step_id: Option, } /// Request payload for updating a task #[derive(Debug, Default, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct UpdateTaskRequest { pub name: Option, pub description: Option, pub plan: Option, pub status: Option, pub priority: Option, pub progress_summary: Option, pub last_output: Option, pub error_message: Option, pub merge_mode: Option, pub pr_url: Option, /// Repository URL for the task (e.g., when updating supervisor with repo info) pub repository_url: Option, /// Path to user's local repository (outside ~/.makima) pub target_repo_path: Option, /// Action on completion: "none", "branch", "merge", "pr" pub completion_action: Option, /// The daemon currently running this task pub daemon_id: Option, /// Explicitly clear daemon_id (set to NULL) #[serde(default)] pub clear_daemon_id: bool, /// Whether this task is hidden from the UI (user dismissed it) pub hidden: Option, /// Version for optimistic locking pub version: Option, } /// Task with its subtasks for detail view #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct TaskWithSubtasks { #[serde(flatten)] pub task: Task, pub subtasks: Vec, } /// Request to send a message to a running task's stdin. #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct SendMessageRequest { /// The message to send to the task's stdin. pub message: String, } /// Default for include_conversation field in BranchTaskRequest fn default_include_conversation() -> bool { true } /// Request to branch a task from an existing task's conversation. /// Creates a new anonymous task that continues from the source task's state. #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct BranchTaskRequest { /// The initial message/instructions for the branched task pub message: String, /// Optional name for the branched task (auto-generated if not provided) pub name: Option, /// Whether to include conversation history from the source task (default: true) #[serde(default = "default_include_conversation")] pub include_conversation: bool, } /// Response from branching a task. #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct BranchTaskResponse { /// The newly created branched task pub task: Task, /// Number of conversation messages included from source task pub message_count: usize, /// Daemon ID if the task was started (None if no daemon available) pub daemon_id: Option, } // ============================================================================= // Daemon Types // ============================================================================= /// Daemon status #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "lowercase")] pub enum DaemonStatus { Connected, Disconnected, Unhealthy, } impl std::fmt::Display for DaemonStatus { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { DaemonStatus::Connected => write!(f, "connected"), DaemonStatus::Disconnected => write!(f, "disconnected"), DaemonStatus::Unhealthy => write!(f, "unhealthy"), } } } impl std::str::FromStr for DaemonStatus { type Err = String; fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "connected" => Ok(DaemonStatus::Connected), "disconnected" => Ok(DaemonStatus::Disconnected), "unhealthy" => Ok(DaemonStatus::Unhealthy), _ => Err(format!("Unknown daemon status: {}", s)), } } } /// Connected daemon record from the database #[derive(Debug, Clone, FromRow, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct Daemon { pub id: Uuid, pub owner_id: Uuid, pub connection_id: String, pub hostname: Option, pub machine_id: Option, pub max_concurrent_tasks: i32, pub current_task_count: i32, pub status: String, pub last_heartbeat_at: DateTime, pub connected_at: DateTime, pub disconnected_at: Option>, } impl Daemon { /// Parse status string to DaemonStatus enum pub fn status_enum(&self) -> Result { self.status.parse() } } /// Response for daemon list endpoint #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct DaemonListResponse { pub daemons: Vec, pub total: i64, } /// Response for daemon directories endpoint #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct DaemonDirectoriesResponse { /// List of suggested directories from connected daemons pub directories: Vec, } /// A suggested directory from a daemon #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct DaemonDirectory { /// Path to the directory pub path: String, /// Display label for the directory pub label: String, /// Type of directory: "working", "makima", "worktrees" pub directory_type: String, /// Daemon hostname this directory is from pub hostname: Option, /// Whether the directory already exists (for validation) #[serde(skip_serializing_if = "Option::is_none")] pub exists: Option, } // ============================================================================= // Task Event Types // ============================================================================= /// Task event record from the database #[derive(Debug, Clone, FromRow, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct TaskEvent { pub id: Uuid, pub task_id: Uuid, pub event_type: String, pub previous_status: Option, pub new_status: Option, #[sqlx(json)] pub event_data: Option, pub created_at: DateTime, } /// Response for task events list endpoint #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct TaskEventListResponse { pub events: Vec, pub total: i64, } /// A single output entry from a Claude Code task #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct TaskOutputEntry { pub id: Uuid, pub task_id: Uuid, /// Message type: "assistant", "tool_use", "tool_result", "result", "system", "error", "raw" pub message_type: String, /// Main text content pub content: String, /// Tool name if tool_use message #[serde(skip_serializing_if = "Option::is_none")] pub tool_name: Option, /// Tool input JSON if tool_use message #[serde(skip_serializing_if = "Option::is_none")] pub tool_input: Option, /// Whether tool result was an error #[serde(skip_serializing_if = "Option::is_none")] pub is_error: Option, /// Cost in USD if result message #[serde(skip_serializing_if = "Option::is_none")] pub cost_usd: Option, /// Duration in ms if result message #[serde(skip_serializing_if = "Option::is_none")] pub duration_ms: Option, /// Timestamp when this output was recorded pub created_at: DateTime, } impl TaskOutputEntry { /// Convert a TaskEvent with event_type='output' to a TaskOutputEntry pub fn from_task_event(event: TaskEvent) -> Option { if event.event_type != "output" { return None; } let data = event.event_data?; Some(Self { id: event.id, task_id: event.task_id, message_type: data.get("messageType")?.as_str()?.to_string(), content: data.get("content")?.as_str().unwrap_or("").to_string(), tool_name: data.get("toolName").and_then(|v| v.as_str()).map(|s| s.to_string()), tool_input: data.get("toolInput").cloned(), is_error: data.get("isError").and_then(|v| v.as_bool()), cost_usd: data.get("costUsd").and_then(|v| v.as_f64()), duration_ms: data.get("durationMs").and_then(|v| v.as_u64()), created_at: event.created_at, }) } } /// Response for task output history endpoint #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct TaskOutputResponse { pub entries: Vec, pub total: usize, pub task_id: Uuid, } // ============================================================================= // Mesh Chat History Types // ============================================================================= /// Mesh chat conversation for persisting history #[derive(Debug, Clone, FromRow, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct MeshChatConversation { pub id: Uuid, pub owner_id: Uuid, pub name: Option, pub is_active: bool, pub created_at: DateTime, pub updated_at: DateTime, } /// Individual message in a mesh chat conversation #[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct MeshChatMessageRecord { pub id: Uuid, pub conversation_id: Uuid, pub role: String, pub content: String, pub context_type: String, pub context_task_id: Option, /// Tool calls made during this message (JSON, nullable) pub tool_calls: Option, /// Pending questions requiring user response (JSON, nullable) pub pending_questions: Option, pub created_at: DateTime, } /// Response for chat history endpoint #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct MeshChatHistoryResponse { pub conversation_id: Uuid, pub messages: Vec, } // ============================================================================= // Contract Chat History Types // ============================================================================= /// Conversation thread for contract chat (scoped to a specific contract) #[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct ContractChatConversation { pub id: Uuid, pub contract_id: Uuid, pub owner_id: Uuid, pub name: Option, pub is_active: bool, pub created_at: DateTime, pub updated_at: DateTime, } /// Individual message in a contract chat conversation #[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct ContractChatMessageRecord { pub id: Uuid, pub conversation_id: Uuid, pub role: String, pub content: String, /// Tool calls made during this message (JSON, nullable) pub tool_calls: Option, /// Pending questions requiring user response (JSON, nullable) pub pending_questions: Option, pub created_at: DateTime, } /// Response for contract chat history endpoint #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct ContractChatHistoryResponse { pub contract_id: Uuid, pub conversation_id: Uuid, pub messages: Vec, } // ============================================================================= // Merge API Types // ============================================================================= /// Information about a task branch #[derive(Debug, Clone, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct BranchInfo { /// Full branch name pub name: String, /// Task ID extracted from branch name (if parseable) pub task_id: Option, /// Whether this branch has been merged pub is_merged: bool, /// Short SHA of the last commit pub last_commit: String, /// Subject line of the last commit pub last_commit_message: String, } /// Response for branch list endpoint #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct BranchListResponse { pub branches: Vec, } /// Request to start a merge #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct MergeStartRequest { /// Branch name to merge pub source_branch: String, } /// Current merge state #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct MergeStatusResponse { /// Whether a merge is in progress pub in_progress: bool, /// Branch being merged (if in progress) pub source_branch: Option, /// Files with unresolved conflicts pub conflicted_files: Vec, } /// Request to resolve a conflict #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct MergeResolveRequest { /// File path to resolve pub file: String, /// Resolution strategy: "ours" or "theirs" pub strategy: String, } /// Request to commit a merge #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct MergeCommitRequest { /// Commit message pub message: String, } /// Request to skip a subtask branch #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct MergeSkipRequest { /// Subtask ID to skip pub subtask_id: Uuid, /// Reason for skipping pub reason: String, } /// Result of a merge operation #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct MergeResultResponse { /// Whether the operation succeeded pub success: bool, /// Human-readable message pub message: String, /// Commit SHA (if a commit was created) pub commit_sha: Option, /// Conflicted files (if conflicts occurred) pub conflicts: Option>, } /// Response to check if all branches are merged #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct MergeCompleteCheckResponse { /// Whether the orchestrator can mark itself as complete pub can_complete: bool, /// Branches not yet merged or skipped pub unmerged_branches: Vec, /// Count of merged branches pub merged_count: u32, /// Count of skipped branches pub skipped_count: u32, } // ============================================================================= // Contract Type Templates (User-defined) // ============================================================================= /// A phase definition within a contract template #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct PhaseDefinition { /// Phase identifier (e.g., "research", "plan", "execute") pub id: String, /// Display name for the phase pub name: String, /// Order in the workflow (0-indexed) pub order: i32, } /// A deliverable definition within a phase #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct DeliverableDefinition { /// Deliverable identifier (e.g., "plan-document", "pull-request") pub id: String, /// Display name for the deliverable pub name: String, /// Priority: "required", "recommended", or "optional" #[serde(default = "default_priority")] pub priority: String, } fn default_priority() -> String { "required".to_string() } /// Phase configuration stored on a contract (copied from template at creation) #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct PhaseConfig { /// Ordered list of phases in the workflow pub phases: Vec, /// Default starting phase pub default_phase: String, /// Deliverables per phase: { "phase_id": [deliverables] } #[serde(default)] pub deliverables: std::collections::HashMap>, } /// Contract type template record from the database #[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct ContractTypeTemplateRecord { pub id: Uuid, pub owner_id: Uuid, pub name: String, pub description: Option, #[sqlx(json)] pub phases: Vec, pub default_phase: String, #[sqlx(json)] pub deliverables: Option>>, pub version: i32, pub created_at: DateTime, pub updated_at: DateTime, } /// Request to create a new contract type template #[derive(Debug, Clone, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct CreateTemplateRequest { pub name: String, pub description: Option, pub phases: Vec, pub default_phase: String, pub deliverables: Option>>, } /// Request to update a contract type template #[derive(Debug, Clone, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct UpdateTemplateRequest { pub name: Option, pub description: Option, pub phases: Option>, pub default_phase: Option, pub deliverables: Option>>, /// Version for optimistic locking pub version: Option, } /// Summary of a contract type template for list views #[derive(Debug, Clone, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct ContractTypeTemplateSummary { pub id: Uuid, pub name: String, pub description: Option, pub phases: Vec, pub default_phase: String, pub is_builtin: bool, pub version: i32, pub created_at: DateTime, } // ============================================================================= // Contract Types // ============================================================================= /// Contract type determines the workflow and required documents #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "lowercase")] pub enum ContractType { /// Simple Plan -> Execute workflow (default) /// - Plan phase: requires a "Plan" document /// - Execute phase: requires a "PR" document Simple, /// Specification-based development with TDD /// - Research: requires "Research Notes" document /// - Specify: requires "Requirements Document" /// - Plan: requires "Plan" document /// - Execute: requires "PR" document /// - Review: requires "Release Notes" document Specification, /// Execute-only workflow with no deliverables /// - Only has "execute" phase /// - NO deliverables at all - just execute tasks directly Execute, } impl Default for ContractType { fn default() -> Self { ContractType::Simple } } impl std::fmt::Display for ContractType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ContractType::Simple => write!(f, "simple"), ContractType::Specification => write!(f, "specification"), ContractType::Execute => write!(f, "execute"), } } } impl std::str::FromStr for ContractType { type Err = String; fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "simple" => Ok(ContractType::Simple), "specification" => Ok(ContractType::Specification), "execute" => Ok(ContractType::Execute), _ => Err(format!("Unknown contract type: {}", s)), } } } /// Contract phase for workflow progression #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "lowercase")] pub enum ContractPhase { Research, Specify, Plan, Execute, Review, } impl std::fmt::Display for ContractPhase { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ContractPhase::Research => write!(f, "research"), ContractPhase::Specify => write!(f, "specify"), ContractPhase::Plan => write!(f, "plan"), ContractPhase::Execute => write!(f, "execute"), ContractPhase::Review => write!(f, "review"), } } } impl std::str::FromStr for ContractPhase { type Err = String; fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "research" => Ok(ContractPhase::Research), "specify" => Ok(ContractPhase::Specify), "plan" => Ok(ContractPhase::Plan), "execute" => Ok(ContractPhase::Execute), "review" => Ok(ContractPhase::Review), _ => Err(format!("Unknown contract phase: {}", s)), } } } /// Contract status #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "lowercase")] pub enum ContractStatus { Active, Completed, Archived, } impl std::fmt::Display for ContractStatus { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ContractStatus::Active => write!(f, "active"), ContractStatus::Completed => write!(f, "completed"), ContractStatus::Archived => write!(f, "archived"), } } } impl std::str::FromStr for ContractStatus { type Err = String; fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "active" => Ok(ContractStatus::Active), "completed" => Ok(ContractStatus::Completed), "archived" => Ok(ContractStatus::Archived), _ => Err(format!("Unknown contract status: {}", s)), } } } /// Repository source type #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "lowercase")] pub enum RepositorySourceType { /// Existing remote repo (GitHub, GitLab, etc) Remote, /// Existing local repo Local, /// New repo created/managed by Makima daemon Managed, } impl std::fmt::Display for RepositorySourceType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { RepositorySourceType::Remote => write!(f, "remote"), RepositorySourceType::Local => write!(f, "local"), RepositorySourceType::Managed => write!(f, "managed"), } } } impl std::str::FromStr for RepositorySourceType { type Err = String; fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "remote" => Ok(RepositorySourceType::Remote), "local" => Ok(RepositorySourceType::Local), "managed" => Ok(RepositorySourceType::Managed), _ => Err(format!("Unknown repository source type: {}", s)), } } } /// Repository status (for managed repos) #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "lowercase")] pub enum RepositoryStatus { /// Repo is usable Ready, /// Waiting for daemon to create Pending, /// Daemon is creating the repo Creating, /// Creation failed Failed, } impl std::fmt::Display for RepositoryStatus { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { RepositoryStatus::Ready => write!(f, "ready"), RepositoryStatus::Pending => write!(f, "pending"), RepositoryStatus::Creating => write!(f, "creating"), RepositoryStatus::Failed => write!(f, "failed"), } } } impl std::str::FromStr for RepositoryStatus { type Err = String; fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "ready" => Ok(RepositoryStatus::Ready), "pending" => Ok(RepositoryStatus::Pending), "creating" => Ok(RepositoryStatus::Creating), "failed" => Ok(RepositoryStatus::Failed), _ => Err(format!("Unknown repository status: {}", s)), } } } /// Contract record from the database #[derive(Debug, Clone, FromRow, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct Contract { pub id: Uuid, pub owner_id: Uuid, pub name: String, pub description: Option, /// Contract type: "simple" or "specification" pub contract_type: String, pub phase: String, pub status: String, /// The long-running supervisor task that orchestrates this contract #[serde(skip_serializing_if = "Option::is_none")] pub supervisor_task_id: Option, /// Whether tasks for this contract should run in autonomous loop mode. /// When enabled, tasks will automatically restart with --continue if they exit /// without a COMPLETION_GATE indicating ready: true. #[serde(default)] pub autonomous_loop: bool, /// Whether to wait for user confirmation before progressing to the next phase. /// When enabled, the supervisor will pause and ask the user to review and approve /// phase outputs (like plans, requirements, etc.) before continuing. #[serde(default)] pub phase_guard: bool, /// Completed deliverables per phase. /// Structure: { "plan": ["plan-document"], "execute": ["pull-request"] } #[sqlx(json)] #[serde(default)] pub completed_deliverables: serde_json::Value, /// Whether this contract operates in local-only mode. /// When enabled, automatic completion actions (branch, merge, pr) are skipped, /// allowing users to manually handle code changes via patch files or other means. #[serde(default)] pub local_only: bool, /// Whether to auto-merge to target branch locally when local_only mode is enabled. /// When both local_only and auto_merge_local are true, completed task changes will be /// automatically merged to the master/main branch locally (without pushing or creating PRs). #[serde(default)] pub auto_merge_local: bool, /// Phase configuration copied from template at contract creation (raw JSON). /// When present, this overrides the built-in contract type phases. /// Use `get_phase_config()` to get the parsed PhaseConfig. #[serde(skip_serializing_if = "Option::is_none")] pub phase_config: Option, pub version: i32, pub created_at: DateTime, pub updated_at: DateTime, } impl Contract { /// Parse contract_type string to ContractType enum pub fn contract_type_enum(&self) -> Result { self.contract_type.parse() } /// Parse phase string to ContractPhase enum pub fn phase_enum(&self) -> Result { self.phase.parse() } /// Parse status string to ContractStatus enum pub fn status_enum(&self) -> Result { self.status.parse() } /// Get valid phase IDs for this contract (as strings) pub fn valid_phase_ids(&self) -> Vec { // Check phase_config first (for custom templates) if let Some(config) = self.get_phase_config() { let mut phases: Vec<_> = config.phases.iter().collect(); phases.sort_by_key(|p| p.order); return phases.iter().map(|p| p.id.clone()).collect(); } // Fall back to built-in contract types match self.contract_type.as_str() { "simple" => vec!["plan".to_string(), "execute".to_string()], "specification" => vec![ "research".to_string(), "specify".to_string(), "plan".to_string(), "execute".to_string(), "review".to_string(), ], "execute" => vec!["execute".to_string()], _ => vec!["plan".to_string(), "execute".to_string()], } } /// Get valid phases for this contract type (as ContractPhase enums) /// Note: For custom templates with non-standard phases, this only returns /// phases that map to the ContractPhase enum. pub fn valid_phases(&self) -> Vec { self.valid_phase_ids() .iter() .filter_map(|id| id.parse::().ok()) .collect() } /// Get the initial phase ID for this contract type (as string) pub fn initial_phase_id(&self) -> String { // Check phase_config first (for custom templates) if let Some(config) = self.get_phase_config() { return config.default_phase.clone(); } // Fall back to built-in contract types match self.contract_type.as_str() { "specification" => "research".to_string(), "execute" => "execute".to_string(), _ => "plan".to_string(), } } /// Get the initial phase for this contract type (as ContractPhase enum) pub fn initial_phase(&self) -> ContractPhase { self.initial_phase_id() .parse() .unwrap_or(ContractPhase::Plan) } /// Get the terminal phase ID for this contract type (as string) pub fn terminal_phase_id(&self) -> String { // Check phase_config first (for custom templates) if let Some(config) = self.get_phase_config() { // Last phase in sorted order is the terminal phase let mut phases: Vec<_> = config.phases.iter().collect(); phases.sort_by_key(|p| p.order); if let Some(last) = phases.last() { return last.id.clone(); } } // Fall back to built-in contract types match self.contract_type.as_str() { "specification" => "review".to_string(), _ => "execute".to_string(), } } /// Get the terminal phase for this contract type (phase where contract can be completed) pub fn terminal_phase(&self) -> ContractPhase { self.terminal_phase_id() .parse() .unwrap_or(ContractPhase::Execute) } /// Check if a phase ID is valid for this contract pub fn is_valid_phase(&self, phase_id: &str) -> bool { self.valid_phase_ids().contains(&phase_id.to_string()) } /// Get the phase configuration for custom templates pub fn get_phase_config(&self) -> Option { self.phase_config .as_ref() .and_then(|v| serde_json::from_value(v.clone()).ok()) } /// Get completed deliverable IDs for a specific phase pub fn get_completed_deliverables(&self, phase: &str) -> Vec { self.completed_deliverables .get(phase) .and_then(|v| v.as_array()) .map(|arr| { arr.iter() .filter_map(|v| v.as_str().map(String::from)) .collect() }) .unwrap_or_default() } /// Check if a specific deliverable is marked as complete for a phase pub fn is_deliverable_complete(&self, phase: &str, deliverable_id: &str) -> bool { self.get_completed_deliverables(phase) .contains(&deliverable_id.to_string()) } } /// Contract repository record from the database #[derive(Debug, Clone, FromRow, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct ContractRepository { pub id: Uuid, pub contract_id: Uuid, pub name: String, pub repository_url: Option, pub local_path: Option, pub source_type: String, pub status: String, pub is_primary: bool, pub created_at: DateTime, pub updated_at: DateTime, } impl ContractRepository { /// Parse source_type string to RepositorySourceType enum pub fn source_type_enum(&self) -> Result { self.source_type.parse() } /// Parse status string to RepositoryStatus enum pub fn status_enum(&self) -> Result { self.status.parse() } } /// Summary of a contract for list views #[derive(Debug, Clone, FromRow, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct ContractSummary { pub id: Uuid, pub name: String, pub description: Option, /// Contract type: "simple" or "specification" pub contract_type: String, pub phase: String, pub status: String, /// Supervisor task ID for contract orchestration pub supervisor_task_id: Option, /// When true, tasks do not auto-execute completion actions and work stays in worktrees. #[serde(default)] pub local_only: bool, /// When true with local_only, automatically merge completed tasks to target branch locally. #[serde(default)] pub auto_merge_local: bool, pub file_count: i64, pub task_count: i64, pub repository_count: i64, pub version: i32, pub created_at: DateTime, } /// Contract with all relations for detail view #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct ContractWithRelations { #[serde(flatten)] pub contract: Contract, pub repositories: Vec, pub files: Vec, pub tasks: Vec, } /// Response for contract list endpoint #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct ContractListResponse { pub contracts: Vec, pub total: i64, } /// Request payload for creating a new contract #[derive(Debug, Clone, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct CreateContractRequest { /// Name of the contract pub name: String, /// Optional description pub description: Option, /// Contract type: "simple" (default), "specification", "execute", or a custom template name. /// For built-in types: /// - simple: Plan -> Execute workflow /// - specification: Research -> Specify -> Plan -> Execute -> Review /// - execute: Execute only /// For custom templates, use the template name or provide template_id. #[serde(default)] pub contract_type: Option, /// UUID of a custom template to use. If provided, this takes precedence over contract_type. /// The template's phase configuration will be copied to the contract. #[serde(default)] pub template_id: Option, /// Initial phase to start in (defaults based on contract_type or template) /// - simple: defaults to "plan" /// - specification: defaults to "research" #[serde(default)] pub initial_phase: Option, /// Enable autonomous loop mode for tasks in this contract. /// When enabled, tasks automatically restart with --continue if they exit /// without a COMPLETION_GATE indicating ready: true. #[serde(default)] pub autonomous_loop: Option, /// Enable phase guard mode for this contract. /// When enabled, the supervisor will pause and ask the user to review and approve /// phase outputs before progressing to the next phase. #[serde(default)] pub phase_guard: Option, /// Enable local-only mode for this contract. /// When enabled, automatic completion actions (branch, merge, pr) are skipped, /// allowing users to manually handle code changes via patch files or other means. #[serde(default)] pub local_only: Option, /// Enable auto-merge to target branch locally when local_only mode is enabled. /// When both local_only and auto_merge_local are true, completed task changes will be /// automatically merged to the master/main branch locally (without pushing or creating PRs). #[serde(default)] pub auto_merge_local: Option, } /// Request payload for updating a contract #[derive(Debug, Default, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct UpdateContractRequest { pub name: Option, pub description: Option, pub phase: Option, pub status: Option, /// Supervisor task ID for contract orchestration #[serde(skip_serializing_if = "Option::is_none")] pub supervisor_task_id: Option, /// Enable or disable autonomous loop mode for tasks in this contract. #[serde(default)] pub autonomous_loop: Option, /// Enable or disable phase guard mode for this contract. /// When enabled, the supervisor will pause and ask the user to review and approve /// phase outputs before progressing to the next phase. #[serde(default)] pub phase_guard: Option, /// Enable or disable local-only mode for this contract. /// When enabled, automatic completion actions (branch, merge, pr) are skipped, /// allowing users to manually handle code changes via patch files or other means. #[serde(default)] pub local_only: Option, /// Enable or disable auto-merge to target branch locally when local_only mode is enabled. /// When both local_only and auto_merge_local are true, completed task changes will be /// automatically merged to the master/main branch locally (without pushing or creating PRs). #[serde(default)] pub auto_merge_local: Option, /// Version for optimistic locking pub version: Option, } /// Request to add a remote repository to a contract #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct AddRemoteRepositoryRequest { pub name: String, pub repository_url: String, #[serde(default)] pub is_primary: bool, } /// Request to add a local repository to a contract #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct AddLocalRepositoryRequest { pub name: String, pub local_path: String, #[serde(default)] pub is_primary: bool, } /// Request to create a managed repository (daemon will create it) #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct CreateManagedRepositoryRequest { pub name: String, #[serde(default)] pub is_primary: bool, } /// Request to change contract phase #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct ChangePhaseRequest { pub phase: String, /// If phase_guard is enabled, this must be true to confirm the transition. /// If not provided or false, returns phase deliverables for review. #[serde(default)] pub confirmed: Option, /// User feedback for changes (used when not confirming) #[serde(skip_serializing_if = "Option::is_none")] pub feedback: Option, /// Expected version for optimistic locking. If provided, the phase change /// will only succeed if the current contract version matches. #[serde(skip_serializing_if = "Option::is_none")] pub expected_version: Option, } /// Result of a phase change operation, supporting explicit conflict detection. #[derive(Debug, Clone)] pub enum PhaseChangeResult { /// Phase change succeeded, returning the updated contract Success(Contract), /// Version conflict: the contract was modified concurrently VersionConflict { /// The version the client expected expected: i32, /// The actual current version in the database actual: i32, /// The current phase of the contract current_phase: String, }, /// Validation failed (e.g., invalid phase transition) ValidationFailed { /// Human-readable reason for the failure reason: String, /// List of missing requirements for the phase transition missing_requirements: Vec, }, /// The caller is not authorized to change this contract's phase Unauthorized, /// The contract was not found NotFound, } /// Response for phase transition when phase_guard is enabled #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct PhaseTransitionRequest { /// Current contract phase pub current_phase: String, /// Requested next phase pub next_phase: String, /// Summary of phase deliverables/outputs pub deliverables_summary: String, /// List of files created in this phase pub phase_files: Vec, /// List of completed tasks in this phase pub phase_tasks: Vec, /// Whether user confirmation is required pub requires_confirmation: bool, /// Unique ID for tracking this transition request pub transition_id: String, } /// File info for phase transition review #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct PhaseFileInfo { pub id: Uuid, pub name: String, pub description: Option, } /// Task info for phase transition review #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct PhaseTaskInfo { pub id: Uuid, pub name: String, pub status: String, } /// Contract event record from the database #[derive(Debug, Clone, FromRow, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct ContractEvent { pub id: Uuid, pub contract_id: Uuid, pub event_type: String, pub previous_phase: Option, pub new_phase: Option, #[sqlx(json)] pub event_data: Option, pub created_at: DateTime, } /// Response for contract events list endpoint #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct ContractEventListResponse { pub events: Vec, pub total: i64, } // ============================================================================ // Task Checkpoints (for git checkpoint tracking) // ============================================================================ /// Task checkpoint record - represents a git commit checkpoint #[derive(Debug, Clone, FromRow, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct TaskCheckpoint { pub id: Uuid, pub task_id: Uuid, /// Sequential checkpoint number within this task pub checkpoint_number: i32, /// Git commit SHA pub commit_sha: String, /// Git branch name pub branch_name: String, /// Commit message pub message: String, /// Files changed in this commit: [{path, action: 'A'|'M'|'D'}] #[sqlx(json)] pub files_changed: Option, /// Lines added in this commit pub lines_added: Option, /// Lines removed in this commit pub lines_removed: Option, pub created_at: DateTime, } /// Request to create a checkpoint #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct CreateCheckpointRequest { /// Commit message pub message: String, } /// Response for checkpoint list endpoint #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct CheckpointListResponse { pub checkpoints: Vec, pub total: i64, } // ============================================================================ // Supervisor State (for supervisor resumability) // ============================================================================ /// Supervisor state for contract supervisor tasks /// Enables resumption after interruption #[derive(Debug, Clone, FromRow, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct SupervisorState { pub id: Uuid, pub contract_id: Uuid, pub task_id: Uuid, /// Full Claude conversation history for resumption #[sqlx(json)] pub conversation_history: serde_json::Value, /// Last checkpoint this supervisor created pub last_checkpoint_id: Option, /// Tasks the supervisor is waiting on #[sqlx(try_from = "Vec")] pub pending_task_ids: Vec, /// Current contract phase when supervisor was last active pub phase: String, /// When supervisor was last active pub last_activity: DateTime, pub created_at: DateTime, pub updated_at: DateTime, /// Current supervisor state (initializing, idle, working, waiting_for_user, etc.) pub state: String, /// Human-readable description of current activity pub current_activity: Option, /// Progress percentage (0-100) pub progress: i32, /// Error message when state is failed or blocked pub error_message: Option, /// Tasks spawned by this supervisor #[sqlx(try_from = "Vec")] pub spawned_task_ids: Vec, /// Pending questions awaiting user response #[sqlx(json)] pub pending_questions: serde_json::Value, /// Number of times this supervisor has been restored pub restoration_count: i32, /// Timestamp of last restoration pub last_restored_at: Option>, /// Source of last restoration (daemon_restart, task_reassignment, manual) pub restoration_source: Option, } /// Pending question structure for supervisor state #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct PendingQuestion { /// Unique question ID pub id: Uuid, /// The question text pub question: String, /// Optional choices (empty for free-form) #[serde(default)] pub choices: Vec, /// Optional context pub context: Option, /// Question type: general, phase_confirmation, contract_complete #[serde(default = "default_question_type")] pub question_type: String, /// When the question was asked pub asked_at: DateTime, } fn default_question_type() -> String { "general".to_string() } /// Request to update supervisor state #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct UpdateSupervisorStateRequest { /// Updated conversation history pub conversation_history: Option, /// Tasks the supervisor is waiting on pub pending_task_ids: Option>, /// Current contract phase pub phase: Option, /// Current supervisor state pub state: Option, /// Current activity description pub current_activity: Option, /// Progress percentage pub progress: Option, /// Error message pub error_message: Option, /// Spawned task IDs pub spawned_task_ids: Option>, /// Pending questions pub pending_questions: Option, } /// Restoration context returned when restoring a supervisor #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct SupervisorRestorationContext { /// Whether restoration was successful pub success: bool, /// Previous state before restoration pub previous_state: SupervisorStateEnum, /// Restored conversation history pub conversation_history: serde_json::Value, /// Pending questions that need re-delivery pub pending_questions: Vec, /// Tasks still being waited on pub waiting_task_ids: Vec, /// Spawned tasks to check status of pub spawned_task_ids: Vec, /// Restoration count (incremented) pub restoration_count: i32, /// Context message for Claude pub restoration_context_message: String, /// Any warnings during restoration pub warnings: Vec, } /// Validation result for supervisor state consistency #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StateValidationResult { pub is_valid: bool, pub issues: Vec, /// Suggested recovery action pub recovery_action: StateRecoveryAction, } /// Action to take when state validation fails #[derive(Debug, Clone, Serialize, Deserialize)] pub enum StateRecoveryAction { /// State is valid, proceed with restoration Proceed, /// Start from last checkpoint UseCheckpoint, /// Start fresh StartFresh, /// Manual intervention required ManualIntervention, } // ============================================================================ // Daemon Task Assignments (for multi-daemon support) // ============================================================================ /// Daemon task assignment record #[derive(Debug, Clone, FromRow, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct DaemonTaskAssignment { pub id: Uuid, pub daemon_id: Uuid, pub task_id: Uuid, pub assigned_at: DateTime, /// Status: 'active', 'migrating', 'completed' pub status: String, } /// Extended daemon info for selection #[derive(Debug, Clone, FromRow, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct DaemonWithCapacity { pub id: Uuid, pub owner_id: Uuid, pub connection_id: String, pub hostname: Option, pub machine_id: Option, pub max_concurrent_tasks: i32, pub current_task_count: i32, pub capacity_score: Option, pub task_queue_length: Option, pub supports_migration: Option, pub status: String, pub last_heartbeat_at: DateTime, pub connected_at: DateTime, } // ============================================================================ // Repository History (for storing and suggesting previously used repositories) // ============================================================================ /// Repository history entry - tracks previously used repositories for suggestions #[derive(Debug, Clone, FromRow, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct RepositoryHistoryEntry { pub id: Uuid, pub owner_id: Uuid, pub name: String, pub repository_url: Option, pub local_path: Option, pub source_type: String, pub use_count: i32, pub last_used_at: DateTime, pub created_at: DateTime, } /// Response for repository history list endpoint #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct RepositoryHistoryListResponse { pub entries: Vec, pub total: i64, } /// Request for getting repository suggestions #[derive(Debug, Deserialize, ToSchema)] pub struct RepositorySuggestionsQuery { /// Filter by source type: 'remote' or 'local' pub source_type: Option, /// Optional search query to filter by name or URL/path pub query: Option, /// Limit results (default: 10) pub limit: Option, } // ============================================================================= // Resume and History System Types // ============================================================================= /// Conversation snapshot for task resumption #[derive(Debug, Clone, FromRow, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct ConversationSnapshot { pub id: Uuid, pub task_id: Uuid, pub checkpoint_id: Option, /// Snapshot type: 'auto', 'manual', 'checkpoint' pub snapshot_type: String, pub message_count: i32, #[sqlx(json)] pub conversation_state: serde_json::Value, #[sqlx(json)] pub metadata: Option, pub created_at: DateTime, } /// History event for contract/task history tracking #[derive(Debug, Clone, FromRow, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct HistoryEvent { pub id: Uuid, pub owner_id: Uuid, pub contract_id: Option, pub task_id: Option, pub event_type: String, pub event_subtype: Option, pub phase: Option, #[sqlx(json)] pub event_data: serde_json::Value, pub created_at: DateTime, } /// Unified conversation message for API responses #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct ConversationMessage { pub id: String, /// Message role: 'user', 'assistant', 'system', 'tool' pub role: String, pub content: String, pub timestamp: DateTime, #[serde(skip_serializing_if = "Option::is_none")] pub tool_calls: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub tool_name: Option, #[serde(skip_serializing_if = "Option::is_none")] pub tool_input: Option, #[serde(skip_serializing_if = "Option::is_none")] pub tool_result: Option, #[serde(skip_serializing_if = "Option::is_none")] pub is_error: Option, #[serde(skip_serializing_if = "Option::is_none")] pub token_count: Option, #[serde(skip_serializing_if = "Option::is_none")] pub cost_usd: Option, } /// Tool call information within a conversation message #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct ToolCallInfo { pub id: String, pub name: String, pub input: serde_json::Value, } /// Query filters for history endpoints #[derive(Debug, Deserialize, ToSchema, Default)] #[serde(rename_all = "camelCase")] pub struct HistoryQueryFilters { pub phase: Option, pub event_types: Option>, #[serde(default, deserialize_with = "flexible_datetime::deserialize")] pub from: Option>, #[serde(default, deserialize_with = "flexible_datetime::deserialize")] pub to: Option>, pub limit: Option, pub cursor: Option, } /// Request to resume a supervisor #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct ResumeSupervisorRequest { pub target_daemon_id: Option, /// Resume mode: 'continue', 'restart_phase', 'from_checkpoint' pub resume_mode: String, pub checkpoint_id: Option, pub additional_context: Option, } /// Request to resume from a checkpoint #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct ResumeFromCheckpointRequest { pub task_name: Option, pub plan: String, pub include_conversation: Option, pub target_daemon_id: Option, } /// Request to rewind a task to a checkpoint #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct RewindTaskRequest { pub checkpoint_id: Option, pub checkpoint_sha: Option, /// Preserve mode: 'discard', 'create_branch', 'stash' pub preserve_mode: String, pub branch_name: Option, } /// Request to rewind a conversation #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct RewindConversationRequest { pub to_message_id: Option, pub to_timestamp: Option>, pub by_message_count: Option, pub rewind_code: Option, } /// Request to fork a task #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct ForkTaskRequest { /// Fork from type: 'checkpoint', 'timestamp', 'message_id' pub fork_from_type: String, pub fork_from_value: String, pub new_task_name: String, pub new_task_plan: String, pub include_conversation: Option, pub create_branch: Option, pub branch_name: Option, } /// Response for contract history endpoint #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct ContractHistoryResponse { pub contract_id: Uuid, pub entries: Vec, pub total_count: i64, pub cursor: Option, } /// Response for task conversation endpoint #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct TaskConversationResponse { pub task_id: Uuid, pub task_name: String, pub status: String, pub messages: Vec, pub total_tokens: Option, pub total_cost: Option, } /// Response for supervisor conversation endpoint #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct SupervisorConversationResponse { pub contract_id: Uuid, pub supervisor_task_id: Uuid, pub phase: String, pub last_activity: DateTime, pub pending_task_ids: Vec, pub messages: Vec, pub spawned_tasks: Vec, } /// Reference to a task for history/conversation responses #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct TaskReference { pub task_id: Uuid, pub task_name: String, pub status: String, pub created_at: DateTime, pub completed_at: Option>, } /// Response for task rewind operation #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct RewindTaskResponse { pub task_id: Uuid, pub rewinded_to: CheckpointInfo, pub preserved_as: Option, } /// Checkpoint information in rewind response #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct CheckpointInfo { pub checkpoint_number: i32, pub sha: String, pub message: String, } /// Preserved state information in rewind response #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct PreservedState { /// State type: 'branch' or 'stash' pub state_type: String, pub reference: String, } /// Response for task fork operation #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct ForkTaskResponse { pub new_task_id: Uuid, pub source_task_id: Uuid, pub fork_point: ForkPoint, pub branch_name: Option, pub conversation_included: bool, pub message_count: Option, } /// Fork point information in fork response #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct ForkPoint { pub fork_type: String, pub checkpoint: Option, pub timestamp: DateTime, } // ============================================================================ // Checkpoint Patches (for task recovery when worktrees are lost) // ============================================================================ /// A stored git patch for checkpoint recovery. /// Enables task recovery when local worktrees are deleted or corrupted. #[derive(Debug, Clone, FromRow, Serialize)] #[serde(rename_all = "camelCase")] pub struct CheckpointPatch { pub id: Uuid, pub task_id: Uuid, /// Optional link to a task_checkpoint record pub checkpoint_id: Option, /// The commit SHA that the patch should be applied on top of pub base_commit_sha: String, /// Compressed git diff data (gzip) #[sqlx(rename = "patch_data")] #[serde(skip)] // Don't serialize binary data to JSON pub patch_data: Vec, /// Size of the compressed patch in bytes pub patch_size_bytes: i32, /// Number of files affected by this patch pub files_count: i32, pub created_at: DateTime, /// When this patch expires and will be automatically deleted pub expires_at: DateTime, } /// Response for checkpoint patch (without binary data) #[derive(Debug, Clone, FromRow, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct CheckpointPatchInfo { pub id: Uuid, pub task_id: Uuid, pub checkpoint_id: Option, pub base_commit_sha: String, pub patch_size_bytes: i32, pub files_count: i32, pub created_at: DateTime, pub expires_at: DateTime, } // ============================================================================ // Red Team Types // ============================================================================ // ============================================================================= // Supervisor State and Heartbeat Types // ============================================================================= /// Supervisor state for contract supervisor tasks. /// Captures detailed activity state for monitoring and restoration. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "snake_case")] pub enum SupervisorStateEnum { /// Supervisor is starting up Initializing, /// Supervisor is idle, waiting for work Idle, /// Supervisor is actively working Working, /// Supervisor is waiting for user input/confirmation WaitingForUser, /// Supervisor is waiting for spawned tasks to complete WaitingForTasks, /// Supervisor is blocked (external dependency, error, etc.) Blocked, /// Supervisor has completed its contract Completed, /// Supervisor has failed Failed, /// Supervisor was interrupted (daemon crash, etc.) Interrupted, } impl Default for SupervisorStateEnum { fn default() -> Self { SupervisorStateEnum::Initializing } } impl std::fmt::Display for SupervisorStateEnum { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { SupervisorStateEnum::Initializing => write!(f, "initializing"), SupervisorStateEnum::Idle => write!(f, "idle"), SupervisorStateEnum::Working => write!(f, "working"), SupervisorStateEnum::WaitingForUser => write!(f, "waiting_for_user"), SupervisorStateEnum::WaitingForTasks => write!(f, "waiting_for_tasks"), SupervisorStateEnum::Blocked => write!(f, "blocked"), SupervisorStateEnum::Completed => write!(f, "completed"), SupervisorStateEnum::Failed => write!(f, "failed"), SupervisorStateEnum::Interrupted => write!(f, "interrupted"), } } } impl std::str::FromStr for SupervisorStateEnum { type Err = String; fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "initializing" => Ok(SupervisorStateEnum::Initializing), "idle" => Ok(SupervisorStateEnum::Idle), "working" => Ok(SupervisorStateEnum::Working), "waiting_for_user" | "waitingforuser" => Ok(SupervisorStateEnum::WaitingForUser), "waiting_for_tasks" | "waitingfortasks" => Ok(SupervisorStateEnum::WaitingForTasks), "blocked" => Ok(SupervisorStateEnum::Blocked), "completed" => Ok(SupervisorStateEnum::Completed), "failed" => Ok(SupervisorStateEnum::Failed), "interrupted" => Ok(SupervisorStateEnum::Interrupted), _ => Err(format!("Unknown supervisor state: {}", s)), } } } /// Enhanced heartbeat record for supervisor task monitoring. /// Stored in the database for historical analysis and dead supervisor detection. #[derive(Debug, Clone, FromRow, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct SupervisorHeartbeatRecord { pub id: Uuid, pub supervisor_task_id: Uuid, pub contract_id: Uuid, pub state: String, pub phase: String, pub current_activity: Option, pub progress: i32, #[sqlx(try_from = "Vec")] pub pending_task_ids: Vec, pub timestamp: DateTime, } /// Request payload for sending a supervisor heartbeat. #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct SupervisorHeartbeatRequest { pub task_id: Uuid, pub contract_id: Uuid, pub state: SupervisorStateEnum, pub phase: String, pub current_activity: Option, /// Progress percentage (0-100) pub progress: u8, pub pending_task_ids: Vec, } // ============================================================================ // Supervisor Status API Types // ============================================================================ /// Response for supervisor status endpoint #[derive(Debug, Clone, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct SupervisorStatusResponse { /// The supervisor task ID pub task_id: Uuid, /// Current supervisor state (from supervisor_states table) pub state: String, /// Current contract phase pub phase: String, /// Description of current activity (from task progress_summary) pub current_activity: Option, /// Progress percentage (0-100) pub progress: Option, /// When the supervisor last updated its state pub last_heartbeat: DateTime, /// Task IDs the supervisor is currently waiting on pub pending_task_ids: Vec, /// Whether the supervisor is currently running pub is_running: bool, } /// Individual heartbeat entry for history #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct SupervisorHeartbeatEntry { /// Timestamp of this heartbeat pub timestamp: DateTime, /// Supervisor state at this time pub state: String, /// Activity description at this time pub activity: Option, /// Progress at this time pub progress: Option, /// Contract phase at this time pub phase: String, /// Pending task IDs at this time pub pending_task_ids: Vec, } /// Response for supervisor heartbeat history endpoint #[derive(Debug, Clone, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct SupervisorHeartbeatHistoryResponse { /// List of heartbeat entries pub heartbeats: Vec, /// Total count of heartbeats (for pagination) pub total: i64, } /// Response for supervisor sync endpoint #[derive(Debug, Clone, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct SupervisorSyncResponse { /// Whether the sync was successful pub synced: bool, /// Current supervisor state after sync pub state: String, /// Optional message about the sync result pub message: Option, } /// Query parameters for heartbeat history endpoint #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct HeartbeatHistoryQuery { /// Maximum number of heartbeats to return (default: 10) pub limit: Option, /// Offset for pagination (default: 0) pub offset: Option, } // ============================================================================= // Unit Tests // ============================================================================= #[cfg(test)] mod tests { use super::*; use uuid::Uuid; #[test] fn test_supervisor_state_enum_display() { assert_eq!(SupervisorStateEnum::Initializing.to_string(), "initializing"); assert_eq!(SupervisorStateEnum::Idle.to_string(), "idle"); assert_eq!(SupervisorStateEnum::Working.to_string(), "working"); assert_eq!(SupervisorStateEnum::WaitingForUser.to_string(), "waiting_for_user"); assert_eq!(SupervisorStateEnum::WaitingForTasks.to_string(), "waiting_for_tasks"); assert_eq!(SupervisorStateEnum::Blocked.to_string(), "blocked"); assert_eq!(SupervisorStateEnum::Completed.to_string(), "completed"); assert_eq!(SupervisorStateEnum::Failed.to_string(), "failed"); assert_eq!(SupervisorStateEnum::Interrupted.to_string(), "interrupted"); } #[test] fn test_supervisor_state_enum_from_str() { // Standard lowercase assert_eq!("initializing".parse::().unwrap(), SupervisorStateEnum::Initializing); assert_eq!("idle".parse::().unwrap(), SupervisorStateEnum::Idle); assert_eq!("working".parse::().unwrap(), SupervisorStateEnum::Working); assert_eq!("waiting_for_user".parse::().unwrap(), SupervisorStateEnum::WaitingForUser); assert_eq!("waiting_for_tasks".parse::().unwrap(), SupervisorStateEnum::WaitingForTasks); assert_eq!("blocked".parse::().unwrap(), SupervisorStateEnum::Blocked); assert_eq!("completed".parse::().unwrap(), SupervisorStateEnum::Completed); assert_eq!("failed".parse::().unwrap(), SupervisorStateEnum::Failed); assert_eq!("interrupted".parse::().unwrap(), SupervisorStateEnum::Interrupted); // Case insensitive assert_eq!("WORKING".parse::().unwrap(), SupervisorStateEnum::Working); assert_eq!("Working".parse::().unwrap(), SupervisorStateEnum::Working); // Alternative formats assert_eq!("waitingforuser".parse::().unwrap(), SupervisorStateEnum::WaitingForUser); assert_eq!("waitingfortasks".parse::().unwrap(), SupervisorStateEnum::WaitingForTasks); // Invalid state assert!("invalid_state".parse::().is_err()); } #[test] fn test_supervisor_state_enum_serialization() { // Test JSON serialization let state = SupervisorStateEnum::Working; let json = serde_json::to_string(&state).unwrap(); assert_eq!(json, "\"working\""); // Test JSON deserialization let deserialized: SupervisorStateEnum = serde_json::from_str("\"working\"").unwrap(); assert_eq!(deserialized, SupervisorStateEnum::Working); // Test underscore variants let json = "\"waiting_for_user\""; let deserialized: SupervisorStateEnum = serde_json::from_str(json).unwrap(); assert_eq!(deserialized, SupervisorStateEnum::WaitingForUser); } #[test] fn test_supervisor_state_enum_default() { let default_state = SupervisorStateEnum::default(); assert_eq!(default_state, SupervisorStateEnum::Initializing); } #[test] fn test_supervisor_heartbeat_request_serialization() { let request = SupervisorHeartbeatRequest { task_id: Uuid::nil(), contract_id: Uuid::nil(), state: SupervisorStateEnum::Working, phase: "execute".to_string(), current_activity: Some("Implementing feature".to_string()), progress: 50, pending_task_ids: vec![Uuid::nil()], }; let json = serde_json::to_string(&request).unwrap(); assert!(json.contains("\"state\":\"working\"")); assert!(json.contains("\"phase\":\"execute\"")); assert!(json.contains("\"progress\":50")); assert!(json.contains("\"currentActivity\":\"Implementing feature\"")); // Test deserialization let deserialized: SupervisorHeartbeatRequest = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.state, SupervisorStateEnum::Working); assert_eq!(deserialized.phase, "execute"); assert_eq!(deserialized.progress, 50); } } // ============================================================================= // 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, pub local_path: Option, pub base_branch: Option, pub orchestrator_task_id: Option, pub pr_url: Option, pub pr_branch: Option, pub completion_task_id: Option, /// Question timeout mode: "auto" (30s timeout), "semi-auto" (block indefinitely), "manual" (block + ask many questions) pub reconcile_mode: String, pub goal_updated_at: DateTime, pub started_at: Option>, pub version: i32, pub created_at: DateTime, pub updated_at: DateTime, /// True for the per-owner scratchpad directive. Auto-created on first /// orphan-task creation. Hidden from the directive list; surfaced to /// users via the sidebar's `tmp/` folder. Tasks attached to a tmp /// directive are auto-deleted after 30 days. #[serde(default)] pub is_tmp: bool, } /// A historical record of a directive goal change. #[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct DirectiveGoalHistory { pub id: Uuid, pub directive_id: Uuid, pub goal: String, pub created_at: DateTime, } /// Per-PR snapshot of a directive's goal — the immutable record of what the /// contract said at the moment a PR was raised. Frozen at PR-creation time; /// `pr_state` mirrors the PR's GitHub lifecycle ('open' | 'merged' | 'closed'). #[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct DirectiveRevision { pub id: Uuid, pub directive_id: Uuid, pub content: String, pub pr_url: String, pub pr_branch: Option, pub pr_state: String, pub version: i32, pub frozen_at: DateTime, } /// 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, pub task_plan: Option, pub depends_on: Vec, /// Status: pending, ready, running, completed, failed, skipped pub status: String, pub task_id: Option, /// Optional contract ID for contract-backed execution. pub contract_id: Option, /// Optional contract type (e.g. "simple", "specification", "execute"). /// When set, the orchestrator creates a contract instead of a standalone task. pub contract_type: Option, pub order_index: i32, pub generation: i32, pub started_at: Option>, pub completed_at: Option>, pub created_at: DateTime, /// Directive document this step belongs to. When the document ships, its /// steps move with it under shipped/. Nullable for legacy steps and for /// directives that don't yet have a document. #[serde(skip_serializing_if = "Option::is_none")] pub directive_document_id: Option, } /// 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, } /// 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, pub orchestrator_task_id: Option, pub pr_url: Option, pub completion_task_id: Option, /// Question timeout mode: "auto" (30s timeout), "semi-auto" (block indefinitely), "manual" (block + ask many questions) pub reconcile_mode: String, pub version: i32, pub created_at: DateTime, pub updated_at: DateTime, 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, 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, pub local_path: Option, pub base_branch: Option, /// Question timeout mode: "auto", "semi-auto", or "manual" pub reconcile_mode: Option, } /// Request to update a directive. #[derive(Debug, Default, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct UpdateDirectiveRequest { pub title: Option, pub goal: Option, pub status: Option, pub repository_url: Option, pub local_path: Option, pub base_branch: Option, pub orchestrator_task_id: Option, pub pr_url: Option, pub pr_branch: Option, /// Question timeout mode: "auto", "semi-auto", or "manual" pub reconcile_mode: Option, pub version: Option, } /// Request to update a directive's goal (triggers re-planning). #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct UpdateGoalRequest { pub goal: String, } /// Response for cleanup_directive_tasks (legacy). #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct CleanupTasksResponse { pub deleted: i64, } /// Response for cleanup_directive endpoint. #[derive(Debug, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct CleanupResponse { pub message: String, pub task_id: Option, } /// Response for pick_up_orders endpoint. #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct PickUpOrdersResponse { pub message: String, pub order_count: i64, pub task_id: Option, } /// Request to create a directive step. #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct CreateDirectiveStepRequest { pub name: String, pub description: Option, pub task_plan: Option, #[serde(default)] pub depends_on: Vec, #[serde(default)] pub order_index: i32, pub generation: Option, /// Optional order ID to auto-link this step to an order. #[serde(default)] pub order_id: Option, /// Optional: create a contract for this step instead of a standalone task. /// Valid values: "simple", "specification", "execute" #[serde(default)] pub contract_type: Option, /// Optional: attach this step to a specific directive document. When /// omitted, the repository falls back to the directive's most-recently /// updated active document (if any). #[serde(default)] pub directive_document_id: Option, } /// Request to update a directive step. #[derive(Debug, Default, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct UpdateDirectiveStepRequest { pub name: Option, pub description: Option, pub task_plan: Option, pub depends_on: Option>, pub status: Option, pub task_id: Option, pub order_index: Option, } // ============================================================================= // Directive Document Types // ============================================================================= /// A directive document — the user-facing "contract" in the unified /// directive UI. One of N specs owned by a directive. Each runs /// sequentially in the directive's shared worktree (or, when /// `merge_mode = 'own_pr'`, on its own branch). `position` defines /// queue order. /// /// Naming note: this struct stays `DirectiveDocument` internally because /// a legacy `Contract` struct (from the pre-directive contracts system) /// still exists. The API and frontend expose this as "Contract"; the /// table/struct rename lands once legacy contracts are dropped (Phase 5). #[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct DirectiveDocument { pub id: Uuid, pub directive_id: Uuid, pub title: String, pub body: String, /// Status: draft, active, shipped, archived pub status: String, pub pr_url: Option, pub pr_branch: Option, pub shipped_at: Option>, pub archived_at: Option>, pub version: i32, /// Queue position within the parent directive (0-indexed). Lower /// numbers run earlier; only one contract is active at a time. pub position: i32, /// Where this contract's commits land. `shared` (default): the /// directive's branch. `own_pr`: a contract-specific branch + PR. pub merge_mode: String, pub created_at: DateTime, pub updated_at: DateTime, } // ============================================================================= // Order Types // ============================================================================= /// An order — a card-based work item (feature, bug, spike, chore, improvement) /// similar to GitHub Issues or Linear cards. Orders are linked to directives /// for execution. #[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct Order { pub id: Uuid, pub owner_id: Uuid, pub title: String, pub description: Option, /// Priority: critical, high, medium, low, none pub priority: String, /// Status: open, in_progress, under_review, done, archived pub status: String, /// Type of work: feature, bug, spike, chore, improvement pub order_type: String, /// Flexible labels as JSON array of strings pub labels: serde_json::Value, /// Linked directive (required for new orders, nullable for legacy rows) pub directive_id: Option, /// Linked directive step (optional) pub directive_step_id: Option, /// Denormalized directive name for searchability (auto-populated by DB trigger) pub directive_name: Option, /// Repository context pub repository_url: Option, /// Optional DOG (Directive Order Group) this order belongs to pub dog_id: Option, pub created_at: DateTime, pub updated_at: DateTime, } /// Request to create a new order. #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct CreateOrderRequest { pub title: String, pub description: Option, pub priority: Option, pub status: Option, pub order_type: Option, #[serde(default = "default_empty_labels")] pub labels: serde_json::Value, /// Directive ID is required for new orders. pub directive_id: Uuid, pub repository_url: Option, /// Optional DOG (Directive Order Group) to assign this order to. pub dog_id: Option, } /// Default empty JSON array for labels. fn default_empty_labels() -> serde_json::Value { serde_json::json!([]) } /// Request to update an existing order. #[derive(Debug, Default, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct UpdateOrderRequest { pub title: Option, pub description: Option, pub priority: Option, pub status: Option, pub order_type: Option, pub labels: Option, pub directive_id: Option, pub directive_step_id: Option, pub repository_url: Option, /// Optional DOG (Directive Order Group) to assign/reassign this order to. pub dog_id: Option, } /// Response for order list endpoint. #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct OrderListResponse { pub orders: Vec, pub total: i64, } /// Query parameters for listing orders. #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct OrderListQuery { /// Filter by status (e.g., "open", "in_progress", "under_review", "done", "archived") pub status: Option, /// Filter by order type (e.g., "feature", "bug", "spike", "chore", "improvement") #[serde(rename = "type")] pub order_type: Option, /// Filter by priority (e.g., "critical", "high", "medium", "low", "none") pub priority: Option, /// Filter by linked directive ID pub directive_id: Option, /// Filter by DOG (Directive Order Group) ID pub dog_id: Option, /// Text search across title, description, and directive_name (case-insensitive) pub search: Option, } /// Request body for linking an order to a directive. #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct LinkDirectiveRequest { pub directive_id: Uuid, } // ============================================================================= // Directive Order Group (DOG) Types // ============================================================================= /// A Directive Order Group (DOG) — an epic-like grouping of orders within a directive. /// DOGs allow organizing related orders under a common theme or goal. #[derive(Debug, Clone, FromRow, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct DirectiveOrderGroup { pub id: Uuid, pub directive_id: Uuid, pub owner_id: Uuid, pub name: String, pub description: Option, /// Status: open, in_progress, done, archived pub status: String, pub created_at: DateTime, pub updated_at: DateTime, } /// Request to create a new Directive Order Group (DOG). #[derive(Debug, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct CreateDirectiveOrderGroupRequest { pub name: String, pub description: Option, } /// Request to update a Directive Order Group (DOG). #[derive(Debug, Default, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct UpdateDirectiveOrderGroupRequest { pub name: Option, pub description: Option, pub status: Option, } /// Response for DOG list endpoint. #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct DirectiveOrderGroupListResponse { pub dogs: Vec, pub total: i64, }