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            });
107        }
108
109        Ok(steps)
110    }
111}
112
113/// Registry of available workflows.
114#[derive(Debug, Clone, Default)]
115pub struct WorkflowRegistry {
116    workflows: HashMap<String, Workflow>,
117}
118
119impl WorkflowRegistry {
120    /// Create a new empty registry.
121    pub fn new() -> Self {
122        Self::default()
123    }
124
125    /// Create a registry pre-loaded with built-in workflows.
126    pub fn with_builtins() -> Self {
127        let mut registry = Self::new();
128        for workflow in builtin_workflows() {
129            registry.register(workflow);
130        }
131        registry
132    }
133
134    /// Register a workflow.
135    pub fn register(&mut self, workflow: Workflow) {
136        self.workflows.insert(workflow.name.clone(), workflow);
137    }
138
139    /// Look up a workflow by name.
140    pub fn get(&self, name: &str) -> Option<&Workflow> {
141        self.workflows.get(name)
142    }
143
144    /// List all registered workflows.
145    pub fn list(&self) -> Vec<&Workflow> {
146        self.workflows.values().collect()
147    }
148
149    /// Remove a workflow by name.
150    pub fn remove(&mut self, name: &str) -> Option<Workflow> {
151        self.workflows.remove(name)
152    }
153}
154
155/// Built-in workflow definitions.
156pub fn builtin_workflows() -> Vec<Workflow> {
157    vec![
158        Workflow {
159            name: "issue_to_pr".into(),
160            description: "Take an issue description and implement a solution, creating a PR-ready commit."
161                .into(),
162            steps: vec![
163                WorkflowStep {
164                    name: "analyze_issue".into(),
165                    action: StepAction::Skill {
166                        skill: "summarize".into(),
167                        arguments: {
168                            let mut m = HashMap::new();
169                            m.insert("target".into(), "{issue_url}".into());
170                            m
171                        },
172                    },
173                    config: None,
174                    failure_policy: StepFailurePolicy::default(),
175                },
176                WorkflowStep {
177                    name: "implement_solution".into(),
178                    action: StepAction::Skill {
179                        skill: "implement".into(),
180                        arguments: {
181                            let mut m = HashMap::new();
182                            m.insert("description".into(), "{issue_url}".into());
183                            m
184                        },
185                    },
186                    config: None,
187                    failure_policy: StepFailurePolicy {
188                        retries: 1,
189                        recovery_prompt: Some(
190                            "Previous implementation failed. Try a different approach.".into(),
191                        ),
192                    },
193                },
194                WorkflowStep {
195                    name: "write_tests".into(),
196                    action: StepAction::Skill {
197                        skill: "write_tests".into(),
198                        arguments: {
199                            let mut m = HashMap::new();
200                            m.insert("target".into(), ".".into());
201                            m
202                        },
203                    },
204                    config: None,
205                    failure_policy: StepFailurePolicy::default(),
206                },
207                WorkflowStep {
208                    name: "run_checks".into(),
209                    action: StepAction::Skill {
210                        skill: "pre_push".into(),
211                        arguments: HashMap::new(),
212                    },
213                    config: None,
214                    failure_policy: StepFailurePolicy {
215                        retries: 2,
216                        recovery_prompt: Some("Fix failures and rerun checks.".into()),
217                    },
218                },
219            ],
220            arguments: vec![WorkflowArgument {
221                name: "issue_url".into(),
222                description: "GitHub issue URL or issue description".into(),
223                required: true,
224            }],
225        },
226        Workflow {
227            name: "refactor_and_test".into(),
228            description:
229                "Refactor code toward a goal, write/update tests, and verify success.".into(),
230            steps: vec![
231                WorkflowStep {
232                    name: "refactor".into(),
233                    action: StepAction::Skill {
234                        skill: "refactor".into(),
235                        arguments: {
236                            let mut m = HashMap::new();
237                            m.insert("target".into(), "{target_file}".into());
238                            m.insert("goal".into(), "{refactor_goal}".into());
239                            m
240                        },
241                    },
242                    config: None,
243                    failure_policy: StepFailurePolicy {
244                        retries: 1,
245                        recovery_prompt: None,
246                    },
247                },
248                WorkflowStep {
249                    name: "update_tests".into(),
250                    action: StepAction::Skill {
251                        skill: "write_tests".into(),
252                        arguments: {
253                            let mut m = HashMap::new();
254                            m.insert("target".into(), "{target_file}".into());
255                            m
256                        },
257                    },
258                    config: None,
259                    failure_policy: StepFailurePolicy::default(),
260                },
261                WorkflowStep {
262                    name: "verify_quality".into(),
263                    action: StepAction::Prompt {
264                        prompt: "Run tests on {target_file} and verify all tests pass.".into(),
265                    },
266                    config: None,
267                    failure_policy: StepFailurePolicy::default(),
268                },
269            ],
270            arguments: vec![
271                WorkflowArgument {
272                    name: "target_file".into(),
273                    description: "File or module to refactor".into(),
274                    required: true,
275                },
276                WorkflowArgument {
277                    name: "refactor_goal".into(),
278                    description: "What the refactoring should achieve".into(),
279                    required: true,
280                },
281            ],
282        },
283        Workflow {
284            name: "review_and_fix".into(),
285            description: "Review code or PR, identify issues, and apply fixes.".into(),
286            steps: vec![
287                WorkflowStep {
288                    name: "review".into(),
289                    action: StepAction::Skill {
290                        skill: "code_review".into(),
291                        arguments: {
292                            let mut m = HashMap::new();
293                            m.insert("target".into(), "{review_target}".into());
294                            m
295                        },
296                    },
297                    config: None,
298                    failure_policy: StepFailurePolicy::default(),
299                },
300                WorkflowStep {
301                    name: "apply_fixes".into(),
302                    action: StepAction::Prompt {
303                        prompt:
304                            "Based on the review feedback for {review_target}, apply all suggested fixes."
305                                .into(),
306                    },
307                    config: None,
308                    failure_policy: StepFailurePolicy {
309                        retries: 1,
310                        recovery_prompt: Some(
311                            "Review failed. Try a different approach to fixing the issues.".into(),
312                        ),
313                    },
314                },
315                WorkflowStep {
316                    name: "verify_fixes".into(),
317                    action: StepAction::Skill {
318                        skill: "code_review".into(),
319                        arguments: {
320                            let mut m = HashMap::new();
321                            m.insert("target".into(), "{review_target}".into());
322                            m
323                        },
324                    },
325                    config: None,
326                    failure_policy: StepFailurePolicy::default(),
327                },
328            ],
329            arguments: vec![WorkflowArgument {
330                name: "review_target".into(),
331                description: "Code, PR URL, or file path to review".into(),
332                required: true,
333            }],
334        },
335    ]
336}
337
338#[cfg(test)]
339mod tests {
340    use super::*;
341
342    #[test]
343    fn workflow_instantiation() {
344        let mut args = HashMap::new();
345        args.insert(
346            "issue_url".into(),
347            "https://github.com/owner/repo/issues/42".into(),
348        );
349
350        let registry = WorkflowRegistry::with_builtins();
351        let workflow = registry.get("issue_to_pr").expect("workflow not found");
352        let steps = workflow.instantiate(&args).expect("instantiation failed");
353
354        assert!(!steps.is_empty());
355        // Check that placeholders were substituted in first step
356        if let StepAction::Skill { arguments, .. } = &steps[0].action {
357            let target = arguments.get("target").expect("target argument missing");
358            assert_eq!(target, "https://github.com/owner/repo/issues/42");
359        } else {
360            panic!("expected skill action");
361        }
362    }
363
364    #[test]
365    fn missing_required_argument() {
366        let registry = WorkflowRegistry::with_builtins();
367        let workflow = registry.get("issue_to_pr").expect("workflow not found");
368        let result = workflow.instantiate(&HashMap::new());
369
370        assert!(result.is_err());
371        assert!(
372            result
373                .unwrap_err()
374                .to_string()
375                .contains("missing required argument")
376        );
377    }
378
379    #[test]
380    fn multiple_placeholders() {
381        let mut args = HashMap::new();
382        args.insert("target_file".into(), "src/lib.rs".into());
383        args.insert("refactor_goal".into(), "improve readability".into());
384
385        let registry = WorkflowRegistry::with_builtins();
386        let workflow = registry
387            .get("refactor_and_test")
388            .expect("workflow not found");
389        let steps = workflow.instantiate(&args).expect("instantiation failed");
390
391        assert!(!steps.is_empty());
392        if let StepAction::Skill { arguments, .. } = &steps[0].action {
393            assert_eq!(arguments.get("target").unwrap(), "src/lib.rs");
394            assert_eq!(arguments.get("goal").unwrap(), "improve readability");
395        } else {
396            panic!("expected skill action");
397        }
398    }
399
400    #[test]
401    fn builtin_workflows_registered() {
402        let registry = WorkflowRegistry::with_builtins();
403        assert!(registry.get("issue_to_pr").is_some());
404        assert!(registry.get("refactor_and_test").is_some());
405        assert!(registry.get("review_and_fix").is_some());
406        assert_eq!(registry.list().len(), 3);
407    }
408
409    #[test]
410    fn workflow_registration() {
411        let mut registry = WorkflowRegistry::new();
412        assert!(registry.get("test_workflow").is_none());
413
414        let workflow = Workflow {
415            name: "test_workflow".into(),
416            description: "Test workflow".into(),
417            steps: vec![],
418            arguments: vec![],
419        };
420        registry.register(workflow);
421
422        assert!(registry.get("test_workflow").is_some());
423        assert_eq!(registry.list().len(), 1);
424
425        let removed = registry.remove("test_workflow");
426        assert!(removed.is_some());
427        assert!(registry.get("test_workflow").is_none());
428    }
429}