Skip to main content

mur_common/
workflow.rs

1//! Workflow — a reusable sequence of steps captured from sessions.
2//!
3//! Workflows embed `KnowledgeBase` via `#[serde(flatten)]` so YAML stays flat.
4
5use serde::{Deserialize, Serialize};
6
7use crate::knowledge::KnowledgeBase;
8use crate::schedule::Capability;
9
10/// A MUR workflow — a captured, reusable sequence of steps.
11///
12/// YAML files in `~/.mur/workflows/` are the source of truth.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct Workflow {
15    /// Shared knowledge fields (flattened into YAML)
16    #[serde(flatten)]
17    pub base: KnowledgeBase,
18
19    /// Ordered steps in this workflow
20    #[serde(default)]
21    pub steps: Vec<Step>,
22
23    /// Variables/parameters for this workflow
24    #[serde(default)]
25    pub variables: Vec<Variable>,
26
27    /// Session IDs this workflow was extracted from
28    #[serde(default)]
29    pub source_sessions: Vec<String>,
30
31    /// Natural-language trigger description (e.g. "when deploying to production")
32    #[serde(default)]
33    pub trigger: String,
34
35    /// Tools this workflow uses (e.g. ["cargo", "docker"])
36    #[serde(default)]
37    pub tools: Vec<String>,
38
39    /// Published version number (incremented on each publish)
40    #[serde(default)]
41    pub published_version: u32,
42
43    /// Permission level required to run this workflow
44    #[serde(default)]
45    pub permission: Permission,
46
47    /// Cron schedule expression (e.g. "0 * * * *" for hourly)
48    #[serde(default, skip_serializing_if = "Option::is_none")]
49    pub schedule: Option<String>,
50
51    /// Unique workflow ID (assigned by Commander, optional for CLI)
52    #[serde(default, skip_serializing_if = "Option::is_none")]
53    pub id: Option<String>,
54
55    /// Notification preferences (Commander feature)
56    #[serde(default, skip_serializing_if = "Option::is_none")]
57    pub notify: Option<NotifyConfig>,
58
59    /// Capabilities required to run this workflow
60    #[serde(default, skip_serializing_if = "Vec::is_empty")]
61    pub requires: Vec<Capability>,
62}
63
64// Allow `workflow.name`, `workflow.content`, etc. via auto-deref.
65impl std::ops::Deref for Workflow {
66    type Target = KnowledgeBase;
67    fn deref(&self) -> &KnowledgeBase {
68        &self.base
69    }
70}
71impl std::ops::DerefMut for Workflow {
72    fn deref_mut(&mut self) -> &mut KnowledgeBase {
73        &mut self.base
74    }
75}
76
77/// A single step in a workflow.
78#[derive(Debug, Clone, Default, Serialize, Deserialize)]
79pub struct Step {
80    /// Execution order (1-based)
81    pub order: u32,
82    /// Human-readable description of what this step does
83    pub description: String,
84    /// Shell command to execute (if any)
85    #[serde(default)]
86    pub command: Option<String>,
87    /// Tool to use (e.g. "cargo", "npm")
88    #[serde(default)]
89    pub tool: Option<String>,
90    /// Whether this step requires user approval before executing
91    #[serde(default)]
92    pub needs_approval: bool,
93    /// What to do if this step fails
94    #[serde(default)]
95    pub on_failure: FailureAction,
96
97    /// Commander extension: pause execution for manual inspection
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    pub breakpoint: Option<bool>,
100
101    /// Commander extension: retry configuration
102    #[serde(default, skip_serializing_if = "Option::is_none")]
103    pub retry: Option<RetryConfig>,
104
105    /// Commander extension: step timeout in seconds
106    #[serde(default, skip_serializing_if = "Option::is_none")]
107    pub timeout_secs: Option<u64>,
108}
109
110/// Commander extension: retry configuration for a workflow step.
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct RetryConfig {
113    pub max_retries: u32,
114    #[serde(default)]
115    pub backoff_secs: Option<u64>,
116}
117
118/// What to do when a workflow step fails.
119#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
120#[serde(rename_all = "lowercase")]
121pub enum FailureAction {
122    /// Skip this step and continue
123    Skip,
124    /// Abort the entire workflow
125    #[default]
126    Abort,
127    /// Retry the step
128    Retry,
129}
130
131/// A variable/parameter for a workflow.
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct Variable {
134    /// Variable name
135    pub name: String,
136    /// Type of the variable
137    #[serde(rename = "type", default)]
138    pub var_type: VarType,
139    /// Whether this variable must be provided
140    #[serde(default)]
141    pub required: bool,
142    /// Default value (as string)
143    #[serde(default)]
144    pub default_value: Option<String>,
145    /// Human-readable description
146    #[serde(default)]
147    pub description: String,
148}
149
150/// Variable types for workflow parameters.
151#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
152#[serde(rename_all = "lowercase")]
153pub enum VarType {
154    #[default]
155    String,
156    Path,
157    Url,
158    Number,
159    Bool,
160    /// Array of strings (e.g., multiple URLs, multiple product names)
161    Array,
162}
163
164impl std::fmt::Display for VarType {
165    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166        match self {
167            VarType::String => write!(f, "string"),
168            VarType::Path => write!(f, "path"),
169            VarType::Url => write!(f, "url"),
170            VarType::Number => write!(f, "number"),
171            VarType::Bool => write!(f, "bool"),
172            VarType::Array => write!(f, "array"),
173        }
174    }
175}
176
177/// Notification level for workflow events.
178#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
179#[serde(rename_all = "snake_case")]
180pub enum NotifyLevel {
181    #[default]
182    Silent,
183    Normal,
184    Alert,
185}
186
187/// Notification configuration for workflow execution results.
188#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct NotifyConfig {
190    #[serde(default)]
191    pub on_success: NotifyLevel,
192    #[serde(default = "default_alert")]
193    pub on_failure: NotifyLevel,
194    #[serde(default = "default_normal")]
195    pub on_anomaly: NotifyLevel,
196}
197
198fn default_alert() -> NotifyLevel {
199    NotifyLevel::Alert
200}
201fn default_normal() -> NotifyLevel {
202    NotifyLevel::Normal
203}
204
205impl Default for NotifyConfig {
206    fn default() -> Self {
207        Self {
208            on_success: NotifyLevel::Silent,
209            on_failure: NotifyLevel::Alert,
210            on_anomaly: NotifyLevel::Normal,
211        }
212    }
213}
214
215/// Permission level for workflow execution.
216#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
217#[serde(rename_all = "lowercase")]
218pub enum Permission {
219    /// Read-only access
220    #[default]
221    Read,
222    /// Read and write access
223    Write,
224    /// Execute only (no read/write of intermediate state)
225    #[serde(rename = "execute_only")]
226    ExecuteOnly,
227}