Skip to main content

mana_core/ops/
plan.rs

1use crate::unit::Unit;
2
3/// Build a decomposition prompt for a unit.
4///
5/// Embeds the unit's description, produces/requires, and decomposition rules
6/// so an agent can create well-scoped child units. Each child should be
7/// completable by a fast, non-thinking model in a single pass.
8pub fn build_decomposition_prompt(id: &str, unit: &Unit, strategy: Option<&str>) -> String {
9    let strategy_guidance = match strategy {
10        Some("feature") | Some("by-feature") => {
11            "Split by feature — each child is a vertical slice (types + impl + tests for one feature)."
12        }
13        Some("layer") | Some("by-layer") => {
14            "Split by layer — types/interfaces first, then implementation, then tests."
15        }
16        Some("file") | Some("by-file") => {
17            "Split by file — each child handles one file or closely related file group."
18        }
19        Some("phase") => {
20            "Split by phase — scaffold first, then core logic, then edge cases, then polish."
21        }
22        Some(other) => {
23            return build_prompt_text(id, unit, other);
24        }
25        None => "Choose the best strategy: by-feature (vertical slices), by-layer, or by-file.",
26    };
27
28    build_prompt_text(id, unit, strategy_guidance)
29}
30
31fn build_prompt_text(id: &str, unit: &Unit, strategy_guidance: &str) -> String {
32    let title = &unit.title;
33    let priority = unit.priority;
34    let description = unit.description.as_deref().unwrap_or("(no description)");
35
36    let mut dep_context = String::new();
37    if !unit.produces.is_empty() {
38        dep_context.push_str(&format!("\nProduces: {}\n", unit.produces.join(", ")));
39    }
40    if !unit.requires.is_empty() {
41        dep_context.push_str(&format!("Requires: {}\n", unit.requires.join(", ")));
42    }
43
44    format!(
45        r#"Decompose unit {id} into smaller child units.
46
47## Parent Unit
48- **ID:** {id}
49- **Title:** {title}
50- **Priority:** P{priority}
51{dep_context}
52## Strategy
53{strategy_guidance}
54
55## Goal
56Each child unit must be **completable by a fast, non-thinking model in a single pass**.
57This means: no design decisions, no ambiguity, no exploration. Every child should
58read like a recipe — follow the steps, pass verify, done.
59
60## What Makes a Unit One-Shottable
61- **Specific instructions** — exact file paths, function signatures, concrete steps
62- **Embedded context** — relevant types, patterns, and existing code in the description itself
63- **Small scope** — touches 1-3 files, writes 1-5 functions
64- **Clear acceptance** — the verify command tests exactly what matters
65- **No decisions left** — the "what" and "how" are both fully specified
66
67## Splitting Rules
68- Create **2-4 children** for medium units, **3-5** for large ones
69- **Maximize parallelism** — prefer independent units over sequential chains
70- Each child must have a **verify command** that exits 0 on success
71- Children should be independently testable where possible
72- Use `--produces` and `--requires` to express dependencies between siblings
73
74## Context Embedding Rules
75- **Embed context into descriptions** — don't reference files, include the relevant types/signatures
76- Include: concrete file paths, function signatures, type definitions
77- Include: specific steps, edge cases, error handling requirements
78- Be specific: "Add `fn validate_email(s: &str) -> bool` to `src/util.rs`" not "add validation"
79
80## How to Create Children
81Use `mana create` for each child unit:
82
83```
84mana create "child title" \
85  --parent {id} \
86  --priority {priority} \
87  --verify "test command that exits 0" \
88  --produces "artifact_name" \
89  --requires "artifact_from_sibling" \
90  --description "Full description with:
91- What to implement
92- Which files to modify (with paths)
93- Key types/signatures to use or create
94- Acceptance criteria
95- Edge cases to handle"
96```
97
98## Description Template
99A good child unit description includes:
1001. **What**: One clear sentence of what this child does
1012. **Files**: Specific file paths with what changes in each
1023. **Context**: Embedded type definitions, function signatures, patterns to follow
1034. **Acceptance**: Concrete criteria the verify command checks
1045. **Edge cases**: What could go wrong, what to handle
105
106## Your Task
1071. Read the parent unit's description below
1082. Examine referenced source files to understand the code
1093. Decide on a split strategy
1104. Create 2-5 child units using `mana create` commands
1115. Ensure every child has a verify command and enough embedded context
112   that a fast model can implement it without exploring the codebase
1136. After creating children, run `mana tree {id}` to show the result
114
115## Parent Unit Description
116{description}"#,
117    )
118}
119
120/// Escape a string for safe use as a single shell argument.
121pub fn shell_escape(s: &str) -> String {
122    let escaped = s.replace('\'', "'\\''");
123    format!("'{}'", escaped)
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn build_prompt_includes_rules() {
132        let unit = Unit::new("42", "Implement auth system");
133        let prompt = build_decomposition_prompt("42", &unit, None);
134
135        assert!(prompt.contains("Decompose unit 42"));
136        assert!(prompt.contains("Implement auth system"));
137        assert!(prompt.contains("non-thinking model"));
138        assert!(prompt.contains("Maximize parallelism"));
139        assert!(prompt.contains("Embed context"));
140        assert!(prompt.contains("verify command"));
141        assert!(prompt.contains("mana create"));
142        assert!(prompt.contains("--parent 42"));
143        assert!(prompt.contains("--produces"));
144        assert!(prompt.contains("--requires"));
145    }
146
147    #[test]
148    fn build_prompt_with_strategy() {
149        let unit = Unit::new("1", "Big task");
150        let prompt = build_decomposition_prompt("1", &unit, Some("by-feature"));
151        assert!(prompt.contains("vertical slice"));
152    }
153
154    #[test]
155    fn build_prompt_includes_produces_requires() {
156        let mut unit = Unit::new("5", "Task with deps");
157        unit.produces = vec!["auth_types".to_string(), "auth_middleware".to_string()];
158        unit.requires = vec!["db_connection".to_string()];
159
160        let prompt = build_decomposition_prompt("5", &unit, None);
161        assert!(prompt.contains("auth_types"));
162        assert!(prompt.contains("db_connection"));
163    }
164
165    #[test]
166    fn build_prompt_custom_strategy_passed_through() {
167        let unit = Unit::new("1", "Task");
168        let prompt = build_decomposition_prompt("1", &unit, Some("my custom approach"));
169        assert!(prompt.contains("my custom approach"));
170    }
171
172    #[test]
173    fn shell_escape_simple() {
174        assert_eq!(shell_escape("hello world"), "'hello world'");
175    }
176
177    #[test]
178    fn shell_escape_with_quotes() {
179        assert_eq!(shell_escape("it's here"), "'it'\\''s here'");
180    }
181}