Skip to main content

claude_pool/
workflow.rs

1//! Workflow templates — preset chains for common patterns.
2//!
3//! Workflows define reusable multi-step pipelines with placeholders for customization.
4//! They simplify invoking common patterns like "issue to PR" or "refactor and test"
5//! without manually composing individual chain steps.
6
7use std::collections::HashMap;
8
9use serde::{Deserialize, Serialize};
10
11use crate::chain::{ChainStep, StepAction, StepFailurePolicy};
12use crate::types::SlotConfig;
13
14/// A workflow template — a preset chain with placeholders.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Workflow {
17    /// Unique workflow name (e.g. "issue_to_pr", "refactor_and_test").
18    pub name: String,
19
20    /// Human-readable description of what this workflow does.
21    pub description: String,
22
23    /// Template steps with placeholders.
24    pub steps: Vec<WorkflowStep>,
25
26    /// Argument definitions for this workflow.
27    pub arguments: Vec<WorkflowArgument>,
28}
29
30/// A step in a workflow template.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct WorkflowStep {
33    /// Step name (for logging and result tracking).
34    pub name: String,
35
36    /// Either an inline prompt or a skill reference (may contain placeholders).
37    pub action: StepAction,
38
39    /// Per-step config overrides (model, effort, etc.).
40    pub config: Option<SlotConfig>,
41
42    /// Failure policy for this step.
43    #[serde(default)]
44    pub failure_policy: StepFailurePolicy,
45}
46
47/// An argument accepted by a workflow.
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct WorkflowArgument {
50    /// Argument name (used as `{name}` in the template).
51    pub name: String,
52
53    /// Human-readable description.
54    pub description: String,
55
56    /// Whether this argument is required.
57    pub required: bool,
58}
59
60impl Workflow {
61    /// Instantiate this workflow by substituting placeholders with arguments.
62    ///
63    /// Validates that all required arguments are provided, then replaces
64    /// `{placeholder}` in prompts and skill arguments with values from the map.
65    pub fn instantiate(&self, args: &HashMap<String, String>) -> crate::Result<Vec<ChainStep>> {
66        // Validate required arguments.
67        for arg in &self.arguments {
68            if arg.required && !args.contains_key(&arg.name) {
69                return Err(crate::Error::Store(format!(
70                    "missing required argument '{}' for workflow '{}'",
71                    arg.name, self.name
72                )));
73            }
74        }
75
76        // Substitute placeholders in steps.
77        let mut steps = Vec::new();
78        for ws in &self.steps {
79            let action = match &ws.action {
80                StepAction::Prompt { prompt } => {
81                    let mut p = prompt.clone();
82                    for (key, value) in args {
83                        p = p.replace(&format!("{{{key}}}"), value);
84                    }
85                    StepAction::Prompt { prompt: p }
86                }
87                StepAction::Skill { skill, arguments } => {
88                    let mut args_substituted = arguments.clone();
89                    for value in args_substituted.values_mut() {
90                        for (arg_key, arg_value) in args {
91                            *value = value.replace(&format!("{{{arg_key}}}"), arg_value);
92                        }
93                    }
94                    StepAction::Skill {
95                        skill: skill.clone(),
96                        arguments: args_substituted,
97                    }
98                }
99            };
100
101            steps.push(ChainStep {
102                name: ws.name.clone(),
103                action,
104                config: ws.config.clone(),
105                failure_policy: ws.failure_policy.clone(),
106                output_vars: Default::default(),
107            });
108        }
109
110        Ok(steps)
111    }
112}
113
114/// Registry of available workflows.
115#[derive(Debug, Clone, Default)]
116pub struct WorkflowRegistry {
117    workflows: HashMap<String, Workflow>,
118}
119
120impl WorkflowRegistry {
121    /// Create a new empty registry.
122    pub fn new() -> Self {
123        Self::default()
124    }
125
126    /// Create a registry pre-loaded with built-in workflows.
127    pub fn with_builtins() -> Self {
128        let mut registry = Self::new();
129        for workflow in builtin_workflows() {
130            registry.register(workflow);
131        }
132        registry
133    }
134
135    /// Register a workflow.
136    pub fn register(&mut self, workflow: Workflow) {
137        self.workflows.insert(workflow.name.clone(), workflow);
138    }
139
140    /// Look up a workflow by name.
141    pub fn get(&self, name: &str) -> Option<&Workflow> {
142        self.workflows.get(name)
143    }
144
145    /// List all registered workflows.
146    pub fn list(&self) -> Vec<&Workflow> {
147        self.workflows.values().collect()
148    }
149
150    /// Remove a workflow by name.
151    pub fn remove(&mut self, name: &str) -> Option<Workflow> {
152        self.workflows.remove(name)
153    }
154}
155
156/// Built-in workflow definitions.
157pub fn builtin_workflows() -> Vec<Workflow> {
158    vec![
159        Workflow {
160            name: "issue_to_pr".into(),
161            description: "Take an issue description and implement a solution, creating a PR-ready commit."
162                .into(),
163            steps: vec![
164                WorkflowStep {
165                    name: "analyze_issue".into(),
166                    action: StepAction::Skill {
167                        skill: "summarize".into(),
168                        arguments: {
169                            let mut m = HashMap::new();
170                            m.insert("target".into(), "{issue_url}".into());
171                            m
172                        },
173                    },
174                    config: None,
175                    failure_policy: StepFailurePolicy::default(),
176                },
177                WorkflowStep {
178                    name: "implement_solution".into(),
179                    action: StepAction::Skill {
180                        skill: "implement".into(),
181                        arguments: {
182                            let mut m = HashMap::new();
183                            m.insert("description".into(), "{issue_url}".into());
184                            m
185                        },
186                    },
187                    config: None,
188                    failure_policy: StepFailurePolicy {
189                        retries: 1,
190                        recovery_prompt: Some(
191                            "Previous implementation failed. Try a different approach.".into(),
192                        ),
193                    },
194                },
195                WorkflowStep {
196                    name: "write_tests".into(),
197                    action: StepAction::Skill {
198                        skill: "write_tests".into(),
199                        arguments: {
200                            let mut m = HashMap::new();
201                            m.insert("target".into(), ".".into());
202                            m
203                        },
204                    },
205                    config: None,
206                    failure_policy: StepFailurePolicy::default(),
207                },
208                WorkflowStep {
209                    name: "run_checks".into(),
210                    action: StepAction::Skill {
211                        skill: "pre_push".into(),
212                        arguments: HashMap::new(),
213                    },
214                    config: None,
215                    failure_policy: StepFailurePolicy {
216                        retries: 2,
217                        recovery_prompt: Some("Fix failures and rerun checks.".into()),
218                    },
219                },
220            ],
221            arguments: vec![WorkflowArgument {
222                name: "issue_url".into(),
223                description: "GitHub issue URL or issue description".into(),
224                required: true,
225            }],
226        },
227        Workflow {
228            name: "refactor_and_test".into(),
229            description:
230                "Refactor code toward a goal, write/update tests, and verify success.".into(),
231            steps: vec![
232                WorkflowStep {
233                    name: "refactor".into(),
234                    action: StepAction::Skill {
235                        skill: "refactor".into(),
236                        arguments: {
237                            let mut m = HashMap::new();
238                            m.insert("target".into(), "{target_file}".into());
239                            m.insert("goal".into(), "{refactor_goal}".into());
240                            m
241                        },
242                    },
243                    config: None,
244                    failure_policy: StepFailurePolicy {
245                        retries: 1,
246                        recovery_prompt: None,
247                    },
248                },
249                WorkflowStep {
250                    name: "update_tests".into(),
251                    action: StepAction::Skill {
252                        skill: "write_tests".into(),
253                        arguments: {
254                            let mut m = HashMap::new();
255                            m.insert("target".into(), "{target_file}".into());
256                            m
257                        },
258                    },
259                    config: None,
260                    failure_policy: StepFailurePolicy::default(),
261                },
262                WorkflowStep {
263                    name: "verify_quality".into(),
264                    action: StepAction::Prompt {
265                        prompt: "Run tests on {target_file} and verify all tests pass.".into(),
266                    },
267                    config: None,
268                    failure_policy: StepFailurePolicy::default(),
269                },
270            ],
271            arguments: vec![
272                WorkflowArgument {
273                    name: "target_file".into(),
274                    description: "File or module to refactor".into(),
275                    required: true,
276                },
277                WorkflowArgument {
278                    name: "refactor_goal".into(),
279                    description: "What the refactoring should achieve".into(),
280                    required: true,
281                },
282            ],
283        },
284        Workflow {
285            name: "review_and_fix".into(),
286            description: "Review code or PR, identify issues, and apply fixes.".into(),
287            steps: vec![
288                WorkflowStep {
289                    name: "review".into(),
290                    action: StepAction::Skill {
291                        skill: "code_review".into(),
292                        arguments: {
293                            let mut m = HashMap::new();
294                            m.insert("target".into(), "{review_target}".into());
295                            m
296                        },
297                    },
298                    config: None,
299                    failure_policy: StepFailurePolicy::default(),
300                },
301                WorkflowStep {
302                    name: "apply_fixes".into(),
303                    action: StepAction::Prompt {
304                        prompt:
305                            "Based on the review feedback for {review_target}, apply all suggested fixes."
306                                .into(),
307                    },
308                    config: None,
309                    failure_policy: StepFailurePolicy {
310                        retries: 1,
311                        recovery_prompt: Some(
312                            "Review failed. Try a different approach to fixing the issues.".into(),
313                        ),
314                    },
315                },
316                WorkflowStep {
317                    name: "verify_fixes".into(),
318                    action: StepAction::Skill {
319                        skill: "code_review".into(),
320                        arguments: {
321                            let mut m = HashMap::new();
322                            m.insert("target".into(), "{review_target}".into());
323                            m
324                        },
325                    },
326                    config: None,
327                    failure_policy: StepFailurePolicy::default(),
328                },
329            ],
330            arguments: vec![WorkflowArgument {
331                name: "review_target".into(),
332                description: "Code, PR URL, or file path to review".into(),
333                required: true,
334            }],
335        },
336    ]
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342
343    #[test]
344    fn workflow_instantiation() {
345        let mut args = HashMap::new();
346        args.insert(
347            "issue_url".into(),
348            "https://github.com/owner/repo/issues/42".into(),
349        );
350
351        let registry = WorkflowRegistry::with_builtins();
352        let workflow = registry.get("issue_to_pr").expect("workflow not found");
353        let steps = workflow.instantiate(&args).expect("instantiation failed");
354
355        assert!(!steps.is_empty());
356        // Check that placeholders were substituted in first step
357        if let StepAction::Skill { arguments, .. } = &steps[0].action {
358            let target = arguments.get("target").expect("target argument missing");
359            assert_eq!(target, "https://github.com/owner/repo/issues/42");
360        } else {
361            panic!("expected skill action");
362        }
363    }
364
365    #[test]
366    fn missing_required_argument() {
367        let registry = WorkflowRegistry::with_builtins();
368        let workflow = registry.get("issue_to_pr").expect("workflow not found");
369        let result = workflow.instantiate(&HashMap::new());
370
371        assert!(result.is_err());
372        assert!(
373            result
374                .unwrap_err()
375                .to_string()
376                .contains("missing required argument")
377        );
378    }
379
380    #[test]
381    fn multiple_placeholders() {
382        let mut args = HashMap::new();
383        args.insert("target_file".into(), "src/lib.rs".into());
384        args.insert("refactor_goal".into(), "improve readability".into());
385
386        let registry = WorkflowRegistry::with_builtins();
387        let workflow = registry
388            .get("refactor_and_test")
389            .expect("workflow not found");
390        let steps = workflow.instantiate(&args).expect("instantiation failed");
391
392        assert!(!steps.is_empty());
393        if let StepAction::Skill { arguments, .. } = &steps[0].action {
394            assert_eq!(arguments.get("target").unwrap(), "src/lib.rs");
395            assert_eq!(arguments.get("goal").unwrap(), "improve readability");
396        } else {
397            panic!("expected skill action");
398        }
399    }
400
401    #[test]
402    fn builtin_workflows_registered() {
403        let registry = WorkflowRegistry::with_builtins();
404        assert!(registry.get("issue_to_pr").is_some());
405        assert!(registry.get("refactor_and_test").is_some());
406        assert!(registry.get("review_and_fix").is_some());
407        assert_eq!(registry.list().len(), 3);
408    }
409
410    #[test]
411    fn workflow_registration() {
412        let mut registry = WorkflowRegistry::new();
413        assert!(registry.get("test_workflow").is_none());
414
415        let workflow = Workflow {
416            name: "test_workflow".into(),
417            description: "Test workflow".into(),
418            steps: vec![],
419            arguments: vec![],
420        };
421        registry.register(workflow);
422
423        assert!(registry.get("test_workflow").is_some());
424        assert_eq!(registry.list().len(), 1);
425
426        let removed = registry.remove("test_workflow");
427        assert!(removed.is_some());
428        assert!(registry.get("test_workflow").is_none());
429    }
430}