Skip to main content

minion_engine/cli/
init_templates.rs

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