1use crate::stage::Stage;
9use std::path::Path;
10
11const SHIP_REVIEW_ANGLES: &[&str] = &[
12 "doc-accuracy cross-reference (do documented claims match source?)",
13 "security / leaked-data (does anything commit secrets, session data, or telemetry?)",
14 "CI/build correctness (can a failing step still report green?)",
15 "external-state claims (does the diff claim merges, tags, or deletions that are not actually true?)",
16 "one generalist deep pass",
17];
18
19const COMPLETION_PROTOCOL: &str = "\
21## Completion Protocol (REQUIRED)\n\
22\n\
23When all work is done, your FINAL message must be exactly:\n\
24\n\
25DEVFLOW_RESULT: {\"status\": \"success\"}\n\
26\n\
27If something prevents completion:\n\
28\n\
29DEVFLOW_RESULT: {\"status\": \"failed\", \"reason\": \"specific explanation\"}\n\
30\n\
31DevFlow reads this line to decide whether the stage succeeded. \
32Output nothing after it.";
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum FixType {
37 AuditFix,
39 GapsOnly,
41}
42
43fn gsd_command_for(stage: Stage, phase: u32) -> String {
45 stage.gsd_command().replace("{N}", &phase.to_string())
46}
47
48fn ship_stage_prompt(phase: u32, review_angles: &[String]) -> String {
62 let code_review = format!("/gsd-code-review {phase}");
63 let ship = format!("/gsd-ship {phase}");
64 let review_angles = review_angles
65 .iter()
66 .map(|angle| format!("- {angle}"))
67 .collect::<Vec<_>>()
68 .join("\n");
69 format!(
70 "Run the Ship stage in two steps:\n\
71 \n\
72 1. Run `{code_review}` (non-interactive). This writes a `REVIEW.md` \
73 artifact with severity-classified findings. Review at high depth from \
74 every angle below:\n\
75 \n\
76 {review_angles}\n\
77 \n\
78 If your harness supports parallel finder subagents, dispatch one per \
79 angle; otherwise run each angle as a focused sequential pass. Merge \
80 and deduplicate every angle's findings into one `REVIEW.md`.\n\
81 2. Check `REVIEW.md` for the Critical-severity gate:\n\
82 \n\
83 - If `REVIEW.md` contains ANY finding at Critical severity: do NOT \
84 run `{ship}` at all. Your FINAL message must be exactly:\n\
85 \n\
86 DEVFLOW_RESULT: {{\"status\": \"failed\", \"reason\": \"review: <short summary of the Critical findings>\"}}\n\
87 \n\
88 - If `REVIEW.md` has NO Critical-severity findings: run `{ship}` and \
89 report the outcome via the normal completion protocol below.\n\
90 \n\
91 {COMPLETION_PROTOCOL}"
92 )
93}
94
95fn validate_stage_prompt(phase: u32) -> String {
104 let command = gsd_command_for(Stage::Validate, phase);
105 format!(
106 "Run the GSD workflow command for this stage:\n\n {command}\n\n\
107 ## Completion Protocol (REQUIRED)\n\
108 \n\
109 When all work is done, your FINAL message must be exactly one of:\n\
110 \n\
111 DEVFLOW_RESULT: {{\"status\": \"success\", \"verdict\": \"pass\"}}\n\
112 \n\
113 if validation found NO gaps, or:\n\
114 \n\
115 DEVFLOW_RESULT: {{\"status\": \"success\", \"verdict\": \"gaps\"}}\n\
116 \n\
117 if validation found gaps that still need fixing. The `verdict` field \
118 is REQUIRED for this stage — it is distinct from `status` (which only \
119 reports whether the validation task itself completed) and MUST be \
120 exactly the lowercase string `pass` or `gaps`.\n\
121 \n\
122 If something prevents completion:\n\
123 \n\
124 DEVFLOW_RESULT: {{\"status\": \"failed\", \"reason\": \"specific explanation\"}}\n\
125 \n\
126 DevFlow reads this line to decide whether the stage succeeded. \
127 Output nothing after it."
128 )
129}
130
131fn idempotent_stage_prompt(stage: Stage, phase: u32) -> String {
143 let artifact = match stage {
144 Stage::Define => "CONTEXT.md",
145 _ => "PLAN.md",
146 };
147 let command = gsd_command_for(stage, phase);
148 let padded = format!("{phase:02}");
149 format!(
150 "First check whether this stage's deliverable already exists:\n\
151 \n\
152 ls .planning/phases/{padded}-*/{padded}-*{artifact} 2>/dev/null\n\
153 \n\
154 - If it EXISTS: the stage's work is already done. Do NOT run the GSD \
155 command, do NOT ask for input, and do NOT modify the existing \
156 artifacts. Your FINAL message must be exactly:\n\
157 \n\
158 DEVFLOW_RESULT: {{\"status\": \"success\"}}\n\
159 \n\
160 - If it does NOT exist: run the GSD workflow command for this stage:\n\
161 \n\
162 \x20 {command}\n\
163 \n\
164 {COMPLETION_PROTOCOL}"
165 )
166}
167
168pub fn stage_prompt(stage: Stage, phase: u32) -> String {
170 stage_prompt_with_project(stage, phase, None)
171}
172
173pub fn stage_prompt_for_project(stage: Stage, phase: u32, project_root: &Path) -> String {
179 stage_prompt_with_project(stage, phase, Some(project_root))
180}
181
182fn stage_prompt_with_project(stage: Stage, phase: u32, project_root: Option<&Path>) -> String {
183 if stage == Stage::Ship {
184 let review_angles = project_root
185 .and_then(crate::config::review_angles)
186 .unwrap_or_else(|| {
187 SHIP_REVIEW_ANGLES
188 .iter()
189 .map(|angle| (*angle).to_owned())
190 .collect()
191 });
192 return ship_stage_prompt(phase, &review_angles);
193 }
194 if stage == Stage::Validate {
195 return validate_stage_prompt(phase);
196 }
197 if matches!(stage, Stage::Define | Stage::Plan) {
198 return idempotent_stage_prompt(stage, phase);
199 }
200 let command = gsd_command_for(stage, phase);
201 if stage == Stage::Code {
202 return format!(
203 "Run the GSD workflow command for this stage:\n\n {command}\n\n\
204 ## Advisory incremental self-review\n\
205 \n\
206 After each plan or wave lands, perform a quick, shallow self-check \
207 for doc accuracy, leaked data, CI/build correctness, and \
208 external-state claims. Record any drift in the working output and \
209 continue execution; the authoritative review happens during Ship. \
210 This check must not pause execution or request human input.\n\
211 \n\
212 {COMPLETION_PROTOCOL}"
213 );
214 }
215 format!(
216 "Run the GSD workflow command for this stage:\n\n {command}\n\n{COMPLETION_PROTOCOL}"
217 )
218}
219
220pub fn fix_prompt(fix_type: FixType, phase: u32) -> String {
222 let command = match fix_type {
223 FixType::AuditFix => format!("/gsd-audit-fix {phase}"),
224 FixType::GapsOnly => format!("/gsd-execute-phase {phase} --gaps-only"),
225 };
226 format!(
227 "Validation reported issues. Run the fix command for this loop:\n\n {command}\n\n{COMPLETION_PROTOCOL}"
228 )
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn each_stage_prompt_carries_its_gsd_command_and_marker() {
237 let cases = [
238 (Stage::Define, "/gsd-discuss-phase 11"),
239 (Stage::Plan, "/gsd-plan-phase 11"),
240 (Stage::Code, "/gsd-execute-phase 11"),
241 (Stage::Validate, "/gsd-validate-phase 11"),
242 (Stage::Ship, "/gsd-ship 11"),
243 ];
244 for (stage, command) in cases {
245 let prompt = stage_prompt(stage, 11);
246 assert!(prompt.contains(command), "{stage} prompt missing {command}");
247 assert!(prompt.contains("DEVFLOW_RESULT"));
248 }
249 }
250
251 #[test]
252 fn phase_placeholder_is_substituted() {
253 assert!(stage_prompt(Stage::Code, 7).contains("/gsd-execute-phase 7"));
254 assert!(!stage_prompt(Stage::Code, 7).contains("{N}"));
255 }
256
257 #[test]
258 fn ship_prompt_sequences_code_review_before_ship() {
259 let prompt = stage_prompt(Stage::Ship, 13);
260 let review_pos = prompt
261 .find("/gsd-code-review 13")
262 .expect("Ship prompt must run /gsd-code-review {N}");
263 let ship_pos = prompt
264 .find("/gsd-ship 13")
265 .expect("Ship prompt must run /gsd-ship {N}");
266 assert!(
267 review_pos < ship_pos,
268 "code-review must be sequenced before ship"
269 );
270 }
271
272 #[test]
273 fn ship_prompt_defines_critical_gate_and_review_failed_contract() {
274 let prompt = stage_prompt(Stage::Ship, 13);
275 assert!(
276 prompt.contains("REVIEW.md"),
277 "Ship prompt must reference the REVIEW.md artifact"
278 );
279 assert!(
280 prompt.to_lowercase().contains("critical"),
281 "Ship prompt must name the Critical-severity gate"
282 );
283 assert!(
284 prompt.contains("do not run")
285 || prompt.contains("do NOT run")
286 || prompt.contains("DO NOT run"),
287 "Ship prompt must instruct the agent not to run /gsd-ship on Critical findings"
288 );
289 assert!(
290 prompt.contains("review:"),
291 "Ship prompt must define the review: ReviewFailed reason convention"
292 );
293 assert!(prompt.contains("DEVFLOW_RESULT"));
294 }
295
296 #[test]
297 fn ship_prompt_includes_multi_angle_conditional_review() {
298 let prompt = stage_prompt(Stage::Ship, 13);
299 for angle in [
300 "doc-accuracy cross-reference",
301 "security / leaked-data",
302 "CI/build correctness",
303 "external-state claims",
304 "generalist deep pass",
305 ] {
306 assert!(prompt.contains(angle), "Ship prompt missing angle: {angle}");
307 }
308 assert!(prompt.contains("parallel finder subagents"));
309 assert!(prompt.contains("focused sequential pass"));
310 assert!(prompt.contains("Merge and deduplicate"));
311 assert!(prompt.contains("REVIEW.md"));
312 }
313
314 #[test]
315 fn ship_prompt_uses_project_review_angle_override() {
316 let dir = tempfile::tempdir().unwrap();
317 std::fs::write(
318 dir.path().join("devflow.toml"),
319 "review_angles = [\"custom release evidence\", \"custom threat boundary\"]\n",
320 )
321 .unwrap();
322
323 let prompt = stage_prompt_for_project(Stage::Ship, 13, dir.path());
324
325 assert!(prompt.contains("custom release evidence"));
326 assert!(prompt.contains("custom threat boundary"));
327 assert!(!prompt.contains("doc-accuracy cross-reference"));
328 }
329
330 #[test]
331 fn code_stage_prompt_is_unchanged_single_command_template() {
332 let prompt = stage_prompt(Stage::Code, 9);
338 assert!(prompt.contains("/gsd-execute-phase 9"));
339 assert!(prompt.contains("DEVFLOW_RESULT"));
340 assert!(
341 !prompt.contains("/gsd-code-review"),
342 "Code prompt should not carry Ship-specific code-review sequencing"
343 );
344 assert!(
345 !prompt.contains("already exists"),
346 "Code prompt should not carry the Define/Plan idempotency contract"
347 );
348 assert!(prompt.contains("Advisory incremental self-review"));
349 for angle in [
350 "doc accuracy",
351 "leaked data",
352 "CI/build correctness",
353 "external-state claims",
354 ] {
355 assert!(prompt.contains(angle), "Code prompt missing angle: {angle}");
356 }
357 assert!(!prompt.contains("AskUserQuestion"));
358 assert!(!prompt.contains("request_user_input"));
359 }
360
361 #[test]
366 fn define_and_plan_prompts_are_idempotent() {
367 let cases = [
368 (Stage::Define, "/gsd-discuss-phase 9", "09-*CONTEXT.md"),
369 (Stage::Plan, "/gsd-plan-phase 9", "09-*PLAN.md"),
370 ];
371 for (stage, command, artifact_glob) in cases {
372 let prompt = stage_prompt(stage, 9);
373 assert!(prompt.contains(command), "{stage} prompt missing {command}");
374 assert!(
375 prompt.contains(artifact_glob),
376 "{stage} prompt must check for its pre-existing artifact"
377 );
378 assert!(
379 prompt.contains("Do NOT run the GSD command"),
380 "{stage} prompt must no-op when the artifact exists"
381 );
382 assert!(
383 prompt.contains("do NOT ask for input"),
384 "{stage} prompt must forbid interactive input"
385 );
386 assert!(prompt.contains("DEVFLOW_RESULT"));
387 }
388 }
389
390 #[test]
391 fn validate_stage_prompt_requires_verdict() {
392 let prompt = stage_prompt(Stage::Validate, 13);
393 assert!(
394 prompt.contains("/gsd-validate-phase 13"),
395 "Validate prompt missing its GSD command"
396 );
397 assert!(
398 prompt.contains("\"verdict\": \"pass\""),
399 "Validate prompt must name the exact lowercase pass verdict"
400 );
401 assert!(
402 prompt.contains("\"verdict\": \"gaps\""),
403 "Validate prompt must name the exact lowercase gaps verdict"
404 );
405 assert!(prompt.contains("REQUIRED"));
406 assert!(prompt.contains("DEVFLOW_RESULT"));
407 }
408
409 #[test]
410 fn fix_prompts_select_the_right_command() {
411 assert!(fix_prompt(FixType::AuditFix, 11).contains("/gsd-audit-fix 11"));
412 assert!(fix_prompt(FixType::GapsOnly, 11).contains("--gaps-only"));
413 assert!(fix_prompt(FixType::AuditFix, 11).contains("DEVFLOW_RESULT"));
414 }
415}