1use crate::stage::Stage;
9
10const COMPLETION_PROTOCOL: &str = "\
12## Completion Protocol (REQUIRED)\n\
13\n\
14When all work is done, your FINAL message must be exactly:\n\
15\n\
16DEVFLOW_RESULT: {\"status\": \"success\"}\n\
17\n\
18If something prevents completion:\n\
19\n\
20DEVFLOW_RESULT: {\"status\": \"failed\", \"reason\": \"specific explanation\"}\n\
21\n\
22DevFlow reads this line to decide whether the stage succeeded. \
23Output nothing after it.";
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum FixType {
28 AuditFix,
30 GapsOnly,
32}
33
34fn gsd_command_for(stage: Stage, phase: u32) -> String {
36 stage.gsd_command().replace("{N}", &phase.to_string())
37}
38
39fn ship_stage_prompt(phase: u32) -> String {
53 let code_review = format!("/gsd-code-review {phase}");
54 let ship = format!("/gsd-ship {phase}");
55 format!(
56 "Run the Ship stage in two steps:\n\
57 \n\
58 1. Run `{code_review}` (non-interactive). This writes a `REVIEW.md` \
59 artifact with severity-classified findings.\n\
60 2. Check `REVIEW.md` for the Critical-severity gate:\n\
61 \n\
62 - If `REVIEW.md` contains ANY finding at Critical severity: do NOT \
63 run `{ship}` at all. Your FINAL message must be exactly:\n\
64 \n\
65 DEVFLOW_RESULT: {{\"status\": \"failed\", \"reason\": \"review: <short summary of the Critical findings>\"}}\n\
66 \n\
67 - If `REVIEW.md` has NO Critical-severity findings: run `{ship}` and \
68 report the outcome via the normal completion protocol below.\n\
69 \n\
70 {COMPLETION_PROTOCOL}"
71 )
72}
73
74fn validate_stage_prompt(phase: u32) -> String {
83 let command = gsd_command_for(Stage::Validate, phase);
84 format!(
85 "Run the GSD workflow command for this stage:\n\n {command}\n\n\
86 ## Completion Protocol (REQUIRED)\n\
87 \n\
88 When all work is done, your FINAL message must be exactly one of:\n\
89 \n\
90 DEVFLOW_RESULT: {{\"status\": \"success\", \"verdict\": \"pass\"}}\n\
91 \n\
92 if validation found NO gaps, or:\n\
93 \n\
94 DEVFLOW_RESULT: {{\"status\": \"success\", \"verdict\": \"gaps\"}}\n\
95 \n\
96 if validation found gaps that still need fixing. The `verdict` field \
97 is REQUIRED for this stage — it is distinct from `status` (which only \
98 reports whether the validation task itself completed) and MUST be \
99 exactly the lowercase string `pass` or `gaps`.\n\
100 \n\
101 If something prevents completion:\n\
102 \n\
103 DEVFLOW_RESULT: {{\"status\": \"failed\", \"reason\": \"specific explanation\"}}\n\
104 \n\
105 DevFlow reads this line to decide whether the stage succeeded. \
106 Output nothing after it."
107 )
108}
109
110fn idempotent_stage_prompt(stage: Stage, phase: u32) -> String {
122 let artifact = match stage {
123 Stage::Define => "CONTEXT.md",
124 _ => "PLAN.md",
125 };
126 let command = gsd_command_for(stage, phase);
127 let padded = format!("{phase:02}");
128 format!(
129 "First check whether this stage's deliverable already exists:\n\
130 \n\
131 ls .planning/phases/{padded}-*/{padded}-*{artifact} 2>/dev/null\n\
132 \n\
133 - If it EXISTS: the stage's work is already done. Do NOT run the GSD \
134 command, do NOT ask for input, and do NOT modify the existing \
135 artifacts. Your FINAL message must be exactly:\n\
136 \n\
137 DEVFLOW_RESULT: {{\"status\": \"success\"}}\n\
138 \n\
139 - If it does NOT exist: run the GSD workflow command for this stage:\n\
140 \n\
141 \x20 {command}\n\
142 \n\
143 {COMPLETION_PROTOCOL}"
144 )
145}
146
147pub fn stage_prompt(stage: Stage, phase: u32) -> String {
149 if stage == Stage::Ship {
150 return ship_stage_prompt(phase);
151 }
152 if stage == Stage::Validate {
153 return validate_stage_prompt(phase);
154 }
155 if matches!(stage, Stage::Define | Stage::Plan) {
156 return idempotent_stage_prompt(stage, phase);
157 }
158 let command = gsd_command_for(stage, phase);
159 format!(
160 "Run the GSD workflow command for this stage:\n\n {command}\n\n{COMPLETION_PROTOCOL}"
161 )
162}
163
164pub fn fix_prompt(fix_type: FixType, phase: u32) -> String {
166 let command = match fix_type {
167 FixType::AuditFix => format!("/gsd-audit-fix {phase}"),
168 FixType::GapsOnly => format!("/gsd-execute-phase {phase} --gaps-only"),
169 };
170 format!(
171 "Validation reported issues. Run the fix command for this loop:\n\n {command}\n\n{COMPLETION_PROTOCOL}"
172 )
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn each_stage_prompt_carries_its_gsd_command_and_marker() {
181 let cases = [
182 (Stage::Define, "/gsd-discuss-phase 11"),
183 (Stage::Plan, "/gsd-plan-phase 11"),
184 (Stage::Code, "/gsd-execute-phase 11"),
185 (Stage::Validate, "/gsd-validate-phase 11"),
186 (Stage::Ship, "/gsd-ship 11"),
187 ];
188 for (stage, command) in cases {
189 let prompt = stage_prompt(stage, 11);
190 assert!(prompt.contains(command), "{stage} prompt missing {command}");
191 assert!(prompt.contains("DEVFLOW_RESULT"));
192 }
193 }
194
195 #[test]
196 fn phase_placeholder_is_substituted() {
197 assert!(stage_prompt(Stage::Code, 7).contains("/gsd-execute-phase 7"));
198 assert!(!stage_prompt(Stage::Code, 7).contains("{N}"));
199 }
200
201 #[test]
202 fn ship_prompt_sequences_code_review_before_ship() {
203 let prompt = stage_prompt(Stage::Ship, 13);
204 let review_pos = prompt
205 .find("/gsd-code-review 13")
206 .expect("Ship prompt must run /gsd-code-review {N}");
207 let ship_pos = prompt
208 .find("/gsd-ship 13")
209 .expect("Ship prompt must run /gsd-ship {N}");
210 assert!(
211 review_pos < ship_pos,
212 "code-review must be sequenced before ship"
213 );
214 }
215
216 #[test]
217 fn ship_prompt_defines_critical_gate_and_review_failed_contract() {
218 let prompt = stage_prompt(Stage::Ship, 13);
219 assert!(
220 prompt.contains("REVIEW.md"),
221 "Ship prompt must reference the REVIEW.md artifact"
222 );
223 assert!(
224 prompt.to_lowercase().contains("critical"),
225 "Ship prompt must name the Critical-severity gate"
226 );
227 assert!(
228 prompt.contains("do not run")
229 || prompt.contains("do NOT run")
230 || prompt.contains("DO NOT run"),
231 "Ship prompt must instruct the agent not to run /gsd-ship on Critical findings"
232 );
233 assert!(
234 prompt.contains("review:"),
235 "Ship prompt must define the review: ReviewFailed reason convention"
236 );
237 assert!(prompt.contains("DEVFLOW_RESULT"));
238 }
239
240 #[test]
241 fn code_stage_prompt_is_unchanged_single_command_template() {
242 let prompt = stage_prompt(Stage::Code, 9);
248 assert!(prompt.contains("/gsd-execute-phase 9"));
249 assert!(prompt.contains("DEVFLOW_RESULT"));
250 assert!(
251 !prompt.contains("/gsd-code-review"),
252 "Code prompt should not carry Ship-specific code-review sequencing"
253 );
254 assert!(
255 !prompt.contains("already exists"),
256 "Code prompt should not carry the Define/Plan idempotency contract"
257 );
258 }
259
260 #[test]
265 fn define_and_plan_prompts_are_idempotent() {
266 let cases = [
267 (Stage::Define, "/gsd-discuss-phase 9", "09-*CONTEXT.md"),
268 (Stage::Plan, "/gsd-plan-phase 9", "09-*PLAN.md"),
269 ];
270 for (stage, command, artifact_glob) in cases {
271 let prompt = stage_prompt(stage, 9);
272 assert!(prompt.contains(command), "{stage} prompt missing {command}");
273 assert!(
274 prompt.contains(artifact_glob),
275 "{stage} prompt must check for its pre-existing artifact"
276 );
277 assert!(
278 prompt.contains("Do NOT run the GSD command"),
279 "{stage} prompt must no-op when the artifact exists"
280 );
281 assert!(
282 prompt.contains("do NOT ask for input"),
283 "{stage} prompt must forbid interactive input"
284 );
285 assert!(prompt.contains("DEVFLOW_RESULT"));
286 }
287 }
288
289 #[test]
290 fn validate_stage_prompt_requires_verdict() {
291 let prompt = stage_prompt(Stage::Validate, 13);
292 assert!(
293 prompt.contains("/gsd-validate-phase 13"),
294 "Validate prompt missing its GSD command"
295 );
296 assert!(
297 prompt.contains("\"verdict\": \"pass\""),
298 "Validate prompt must name the exact lowercase pass verdict"
299 );
300 assert!(
301 prompt.contains("\"verdict\": \"gaps\""),
302 "Validate prompt must name the exact lowercase gaps verdict"
303 );
304 assert!(prompt.contains("REQUIRED"));
305 assert!(prompt.contains("DEVFLOW_RESULT"));
306 }
307
308 #[test]
309 fn fix_prompts_select_the_right_command() {
310 assert!(fix_prompt(FixType::AuditFix, 11).contains("/gsd-audit-fix 11"));
311 assert!(fix_prompt(FixType::GapsOnly, 11).contains("--gaps-only"));
312 assert!(fix_prompt(FixType::AuditFix, 11).contains("DEVFLOW_RESULT"));
313 }
314}