Skip to main content

minion_engine/cli/
init_templates.rs

1/// Template definitions for `minion init`
2pub struct Template {
3    pub name: &'static str,
4    pub description: &'static str,
5    pub content: &'static str,
6}
7
8pub const TEMPLATES: &[Template] = &[
9    Template {
10        name: "blank",
11        description: "Minimal workflow skeleton",
12        content: BLANK,
13    },
14    Template {
15        name: "fix-issue",
16        description: "Fetch a GitHub issue, plan and implement a fix, open a PR",
17        content: FIX_ISSUE,
18    },
19    Template {
20        name: "code-review",
21        description: "Run an AI-powered code review on a branch or PR",
22        content: CODE_REVIEW,
23    },
24    Template {
25        name: "security-audit",
26        description: "Perform a security audit on the codebase",
27        content: SECURITY_AUDIT,
28    },
29];
30
31pub fn get(name: &str) -> Option<&'static Template> {
32    TEMPLATES.iter().find(|t| t.name == name)
33}
34
35pub fn names() -> Vec<&'static str> {
36    TEMPLATES.iter().map(|t| t.name).collect()
37}
38
39// ─── Templates ───────────────────────────────────────────────────────────────
40
41const BLANK: &str = r#"name: {name}
42version: 1
43description: "Short description of what this workflow does"
44
45# Optional: global config applied to all steps
46config:
47  global:
48    timeout: 300s
49  cmd:
50    fail_on_error: true
51
52steps:
53  - name: hello
54    type: cmd
55    run: "echo 'Hello, {{ target }}!'"
56"#;
57
58const FIX_ISSUE: &str = r#"name: {name}
59version: 1
60description: "Fetch a GitHub issue, plan & implement a fix, open a PR"
61
62config:
63  global:
64    timeout: 600s
65  agent:
66    model: claude-sonnet-4-20250514
67    permissions: skip
68  cmd:
69    fail_on_error: true
70
71scopes:
72  lint_loop:
73    steps:
74      - name: lint
75        type: cmd
76        run: "cargo clippy --fix --allow-dirty 2>&1 || true"
77      - name: check_lint
78        type: gate
79        condition: "{{ steps.lint.exit_code == 0 }}"
80        on_pass: break
81    outputs: "{{ steps.lint.stdout }}"
82
83steps:
84  - name: fetch_issue
85    type: cmd
86    run: "gh issue view {{ target }} --json title,body,labels"
87
88  - name: find_files
89    type: cmd
90    run: "git ls-files | head -50"
91
92  - name: plan
93    type: agent
94    prompt: |
95      You are a senior engineer. Analyze this GitHub issue and plan a fix.
96      Issue: {{ steps.fetch_issue.stdout }}
97      Files: {{ steps.find_files.stdout }}
98      Provide a concise implementation plan.
99
100  - name: implement
101    type: agent
102    prompt: |
103      Implement the fix described in this plan:
104      {{ steps.plan.response }}
105      Make the minimal changes needed. Do not add unrelated improvements.
106
107  - name: lint_fix
108    type: repeat
109    scope: lint_loop
110    max_iterations: 3
111
112  - name: create_branch
113    type: cmd
114    run: "git checkout -b fix/issue-{{ target }}"
115
116  - name: commit
117    type: cmd
118    run: "git add -A && git commit -m 'fix: resolve issue #{{ target }}'"
119
120  - name: open_pr
121    type: cmd
122    run: |
123      gh pr create \
124        --title "fix: resolve issue #{{ target }}" \
125        --body "Closes #{{ target }}" \
126        --head "fix/issue-{{ target }}"
127"#;
128
129const CODE_REVIEW: &str = r#"name: {name}
130version: 1
131description: "AI-powered code review on a branch or PR"
132
133config:
134  global:
135    timeout: 300s
136  agent:
137    model: claude-sonnet-4-20250514
138    permissions: skip
139
140steps:
141  - name: get_diff
142    type: cmd
143    run: "git diff main...HEAD"
144
145  - name: get_files
146    type: cmd
147    run: "git diff --name-only main...HEAD"
148
149  - name: review
150    type: agent
151    prompt: |
152      You are a senior engineer performing a code review.
153
154      Changed files:
155      {{ steps.get_files.stdout }}
156
157      Diff:
158      {{ steps.get_diff.stdout }}
159
160      Review for:
161      1. Logic errors and bugs
162      2. Security vulnerabilities
163      3. Performance issues
164      4. Code quality and readability
165      5. Missing tests
166
167      Be specific. Reference line numbers where possible.
168
169  - name: summary
170    type: cmd
171    run: |
172      echo "=== Code Review Complete ==="
173      echo "{{ steps.review.response }}"
174"#;
175
176const SECURITY_AUDIT: &str = r#"name: {name}
177version: 1
178description: "Security audit of the codebase"
179
180config:
181  global:
182    timeout: 600s
183  agent:
184    model: claude-sonnet-4-20250514
185    permissions: skip
186  cmd:
187    fail_on_error: false
188
189steps:
190  - name: list_files
191    type: cmd
192    run: "git ls-files | grep -E '\\.(rs|py|js|ts|go|java|rb|php)$' | head -100"
193
194  - name: check_deps
195    type: cmd
196    run: "cargo audit 2>&1 || echo 'cargo audit not available'"
197
198  - name: find_secrets
199    type: cmd
200    run: |
201      grep -rn \
202        -e 'password\s*=' \
203        -e 'secret\s*=' \
204        -e 'api_key\s*=' \
205        -e 'token\s*=' \
206        --include='*.rs' --include='*.toml' . 2>/dev/null | head -20 || echo "No obvious secrets found"
207
208  - name: audit
209    type: agent
210    prompt: |
211      You are a security engineer performing a security audit.
212
213      Project files:
214      {{ steps.list_files.stdout }}
215
216      Dependency audit:
217      {{ steps.check_deps.stdout }}
218
219      Potential secrets scan:
220      {{ steps.find_secrets.stdout }}
221
222      Identify:
223      1. Hardcoded secrets or credentials
224      2. Injection vulnerabilities (SQL, command, path traversal)
225      3. Insecure dependencies
226      4. Missing input validation
227      5. Improper error handling that leaks info
228
229      Provide a severity-ranked list of findings with remediation steps.
230
231  - name: report
232    type: cmd
233    run: |
234      echo "=== Security Audit Report ==="
235      echo "{{ steps.audit.response }}"
236"#;
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    #[test]
243    fn all_template_names_are_unique() {
244        let mut names = names();
245        let original_len = names.len();
246        names.dedup();
247        assert_eq!(names.len(), original_len, "template names must be unique");
248    }
249
250    #[test]
251    fn get_blank_template() {
252        let t = get("blank").expect("blank template should exist");
253        assert_eq!(t.name, "blank");
254        assert!(!t.content.is_empty());
255    }
256
257    #[test]
258    fn get_nonexistent_template_returns_none() {
259        assert!(get("does-not-exist").is_none());
260    }
261
262    #[test]
263    fn all_templates_contain_name_placeholder() {
264        for t in TEMPLATES {
265            assert!(
266                t.content.contains("{name}"),
267                "template '{}' must contain {{name}} placeholder",
268                t.name
269            );
270        }
271    }
272
273    #[test]
274    fn init_creates_valid_yaml_for_each_template() {
275        use crate::workflow::parser;
276        use crate::workflow::validator;
277
278        for t in TEMPLATES {
279            let content = t.content.replace("{name}", "test-workflow");
280            let wf = parser::parse_str(&content)
281                .unwrap_or_else(|e| panic!("template '{}' failed to parse: {e}", t.name));
282            let errors = validator::validate(&wf);
283            assert!(
284                errors.is_empty(),
285                "template '{}' has validation errors: {:?}",
286                t.name,
287                errors
288            );
289        }
290    }
291}