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
110pub use crate::skill::manifest::{FailureAction, RetryConfig, VarType, Variable};
111
112/// Notification level for workflow events.
113#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
114#[serde(rename_all = "snake_case")]
115pub enum NotifyLevel {
116    #[default]
117    Silent,
118    Normal,
119    Alert,
120}
121
122/// Notification configuration for workflow execution results.
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct NotifyConfig {
125    #[serde(default)]
126    pub on_success: NotifyLevel,
127    #[serde(default = "default_alert")]
128    pub on_failure: NotifyLevel,
129    #[serde(default = "default_normal")]
130    pub on_anomaly: NotifyLevel,
131}
132
133fn default_alert() -> NotifyLevel {
134    NotifyLevel::Alert
135}
136fn default_normal() -> NotifyLevel {
137    NotifyLevel::Normal
138}
139
140impl Default for NotifyConfig {
141    fn default() -> Self {
142        Self {
143            on_success: NotifyLevel::Silent,
144            on_failure: NotifyLevel::Alert,
145            on_anomaly: NotifyLevel::Normal,
146        }
147    }
148}
149
150/// Permission level for workflow execution.
151#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
152#[serde(rename_all = "lowercase")]
153pub enum Permission {
154    /// Read-only access
155    #[default]
156    Read,
157    /// Read and write access
158    Write,
159    /// Execute only (no read/write of intermediate state)
160    #[serde(rename = "execute_only")]
161    ExecuteOnly,
162}