mur-common 2.24.4

Shared types and traits for the MUR ecosystem
Documentation
//! Workflow — a reusable sequence of steps captured from sessions.
//!
//! Workflows embed `KnowledgeBase` via `#[serde(flatten)]` so YAML stays flat.

use serde::{Deserialize, Serialize};

use crate::knowledge::KnowledgeBase;
use crate::schedule::Capability;

/// A MUR workflow — a captured, reusable sequence of steps.
///
/// YAML files in `~/.mur/workflows/` are the source of truth.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Workflow {
    /// Shared knowledge fields (flattened into YAML)
    #[serde(flatten)]
    pub base: KnowledgeBase,

    /// Ordered steps in this workflow
    #[serde(default)]
    pub steps: Vec<Step>,

    /// Variables/parameters for this workflow
    #[serde(default)]
    pub variables: Vec<Variable>,

    /// Session IDs this workflow was extracted from
    #[serde(default)]
    pub source_sessions: Vec<String>,

    /// Natural-language trigger description (e.g. "when deploying to production")
    #[serde(default)]
    pub trigger: String,

    /// Tools this workflow uses (e.g. ["cargo", "docker"])
    #[serde(default)]
    pub tools: Vec<String>,

    /// Published version number (incremented on each publish)
    #[serde(default)]
    pub published_version: u32,

    /// Permission level required to run this workflow
    #[serde(default)]
    pub permission: Permission,

    /// Cron schedule expression (e.g. "0 * * * *" for hourly)
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub schedule: Option<String>,

    /// Unique workflow ID (assigned by Commander, optional for CLI)
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub id: Option<String>,

    /// Notification preferences (Commander feature)
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub notify: Option<NotifyConfig>,

    /// Capabilities required to run this workflow
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub requires: Vec<Capability>,
}

// Allow `workflow.name`, `workflow.content`, etc. via auto-deref.
impl std::ops::Deref for Workflow {
    type Target = KnowledgeBase;
    fn deref(&self) -> &KnowledgeBase {
        &self.base
    }
}
impl std::ops::DerefMut for Workflow {
    fn deref_mut(&mut self) -> &mut KnowledgeBase {
        &mut self.base
    }
}

/// A single step in a workflow.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Step {
    /// Execution order (1-based)
    pub order: u32,
    /// Human-readable description of what this step does
    pub description: String,
    /// Shell command to execute (if any)
    #[serde(default)]
    pub command: Option<String>,
    /// Tool to use (e.g. "cargo", "npm")
    #[serde(default)]
    pub tool: Option<String>,
    /// Whether this step requires user approval before executing
    #[serde(default)]
    pub needs_approval: bool,
    /// What to do if this step fails
    #[serde(default)]
    pub on_failure: FailureAction,

    /// Commander extension: pause execution for manual inspection
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub breakpoint: Option<bool>,

    /// Commander extension: retry configuration
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub retry: Option<RetryConfig>,

    /// Commander extension: step timeout in seconds
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub timeout_secs: Option<u64>,
}

pub use crate::skill::manifest::{FailureAction, RetryConfig, VarType, Variable};

/// Notification level for workflow events.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum NotifyLevel {
    #[default]
    Silent,
    Normal,
    Alert,
}

/// Notification configuration for workflow execution results.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NotifyConfig {
    #[serde(default)]
    pub on_success: NotifyLevel,
    #[serde(default = "default_alert")]
    pub on_failure: NotifyLevel,
    #[serde(default = "default_normal")]
    pub on_anomaly: NotifyLevel,
}

fn default_alert() -> NotifyLevel {
    NotifyLevel::Alert
}
fn default_normal() -> NotifyLevel {
    NotifyLevel::Normal
}

impl Default for NotifyConfig {
    fn default() -> Self {
        Self {
            on_success: NotifyLevel::Silent,
            on_failure: NotifyLevel::Alert,
            on_anomaly: NotifyLevel::Normal,
        }
    }
}

/// Permission level for workflow execution.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum Permission {
    /// Read-only access
    #[default]
    Read,
    /// Read and write access
    Write,
    /// Execute only (no read/write of intermediate state)
    #[serde(rename = "execute_only")]
    ExecuteOnly,
}