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(phase: u32) -> String {
149 let artifact = "PLAN.md";
150 let command = gsd_command_for(Stage::Plan, phase);
151 let padded = format!("{phase:02}");
152 format!(
153 "First check whether this stage's deliverable already exists:\n\
154 \n\
155 ls .planning/phases/{padded}-*/{padded}-*{artifact} 2>/dev/null\n\
156 \n\
157 - If it EXISTS: the stage's work is already done. Do NOT run the GSD \
158 command, do NOT ask for input, and do NOT modify the existing \
159 artifacts. Your FINAL message must be exactly:\n\
160 \n\
161 DEVFLOW_RESULT: {{\"status\": \"success\"}}\n\
162 \n\
163 - If it does NOT exist: run the GSD workflow command for this stage:\n\
164 \n\
165 \x20 {command}\n\
166 \n\
167 {COMPLETION_PROTOCOL}"
168 )
169}
170
171fn define_stage_prompt(phase: u32) -> String {
182 format!(
183 "This is the Define stage of a headless DevFlow run for phase {phase}.\n\
184 \n\
185 There is no agent work to perform here. Whether or not this phase's \
186 CONTEXT.md already exists, you must NOT run an interactive \
187 discuss-phase or interview command, and you must NOT ask for input \
188 — this run is headless and no operator is available to answer \
189 interactive questions. Do NOT modify any existing planning \
190 artifacts.\n\
191 \n\
192 {COMPLETION_PROTOCOL}"
193 )
194}
195
196pub fn stage_prompt(stage: Stage, phase: u32) -> String {
198 stage_prompt_with_project(stage, phase, None)
199}
200
201pub fn stage_prompt_for_project(stage: Stage, phase: u32, project_root: &Path) -> String {
207 stage_prompt_with_project(stage, phase, Some(project_root))
208}
209
210fn stage_prompt_with_project(stage: Stage, phase: u32, project_root: Option<&Path>) -> String {
211 if stage == Stage::Ship {
212 let review_angles = project_root
213 .and_then(crate::config::review_angles)
214 .unwrap_or_else(|| {
215 SHIP_REVIEW_ANGLES
216 .iter()
217 .map(|angle| (*angle).to_owned())
218 .collect()
219 });
220 return ship_stage_prompt(phase, &review_angles);
221 }
222 if stage == Stage::Validate {
223 return validate_stage_prompt(phase);
224 }
225 if stage == Stage::Define {
226 return define_stage_prompt(phase);
227 }
228 if stage == Stage::Plan {
229 return idempotent_stage_prompt(phase);
230 }
231 let command = gsd_command_for(stage, phase);
232 if stage == Stage::Code {
233 return format!(
234 "Run the GSD workflow command for this stage:\n\n {command}\n\n\
235 ## Advisory incremental self-review\n\
236 \n\
237 After each plan or wave lands, perform a quick, shallow self-check \
238 for doc accuracy, leaked data, CI/build correctness, and \
239 external-state claims. Record any drift in the working output and \
240 continue execution; the authoritative review happens during Ship. \
241 This check must not pause execution or request human input.\n\
242 \n\
243 {COMPLETION_PROTOCOL}"
244 );
245 }
246 format!(
247 "Run the GSD workflow command for this stage:\n\n {command}\n\n{COMPLETION_PROTOCOL}"
248 )
249}
250
251pub fn checkpoint_auto_decide_prompt(phase: u32) -> String {
264 format!(
265 "This is phase {phase} of a headless DevFlow run. You previously \
266 stopped at a human-blocking checkpoint, but no human operator is \
267 available to answer it — this run is unattended, and none is \
268 coming. DevFlow's policy is for you to resolve the checkpoint \
269 yourself, using your own best judgment, and continue the work. You \
270 MUST record your reasoning for the decision you made in your final \
271 message, so the decision is auditable after the fact.\n\
272 \n\
273 {COMPLETION_PROTOCOL}"
274 )
275}
276
277pub fn fix_prompt(fix_type: FixType, phase: u32) -> String {
279 let command = match fix_type {
280 FixType::AuditFix => format!("/gsd-audit-fix {phase}"),
281 FixType::GapsOnly => format!("/gsd-execute-phase {phase} --gaps-only"),
282 };
283 format!(
284 "Validation reported issues. Run the fix command for this loop:\n\n {command}\n\n{COMPLETION_PROTOCOL}"
285 )
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291
292 #[test]
293 fn each_stage_prompt_carries_its_gsd_command_and_marker() {
294 let cases = [
297 (Stage::Plan, "/gsd-plan-phase 11"),
298 (Stage::Code, "/gsd-execute-phase 11"),
299 (Stage::Validate, "/gsd-validate-phase 11"),
300 (Stage::Ship, "/gsd-ship 11"),
301 ];
302 for (stage, command) in cases {
303 let prompt = stage_prompt(stage, 11);
304 assert!(prompt.contains(command), "{stage} prompt missing {command}");
305 assert!(prompt.contains("DEVFLOW_RESULT"));
306 }
307 }
308
309 #[test]
310 fn phase_placeholder_is_substituted() {
311 assert!(stage_prompt(Stage::Code, 7).contains("/gsd-execute-phase 7"));
312 assert!(!stage_prompt(Stage::Code, 7).contains("{N}"));
313 }
314
315 #[test]
316 fn ship_prompt_sequences_code_review_before_ship() {
317 let prompt = stage_prompt(Stage::Ship, 13);
318 let review_pos = prompt
319 .find("/gsd-code-review 13")
320 .expect("Ship prompt must run /gsd-code-review {N}");
321 let ship_pos = prompt
322 .find("/gsd-ship 13")
323 .expect("Ship prompt must run /gsd-ship {N}");
324 assert!(
325 review_pos < ship_pos,
326 "code-review must be sequenced before ship"
327 );
328 }
329
330 #[test]
331 fn ship_prompt_defines_critical_gate_and_review_failed_contract() {
332 let prompt = stage_prompt(Stage::Ship, 13);
333 assert!(
334 prompt.contains("REVIEW.md"),
335 "Ship prompt must reference the REVIEW.md artifact"
336 );
337 assert!(
338 prompt.to_lowercase().contains("critical"),
339 "Ship prompt must name the Critical-severity gate"
340 );
341 assert!(
342 prompt.contains("do not run")
343 || prompt.contains("do NOT run")
344 || prompt.contains("DO NOT run"),
345 "Ship prompt must instruct the agent not to run /gsd-ship on Critical findings"
346 );
347 assert!(
348 prompt.contains("review:"),
349 "Ship prompt must define the review: ReviewFailed reason convention"
350 );
351 assert!(prompt.contains("DEVFLOW_RESULT"));
352 }
353
354 #[test]
355 fn ship_prompt_includes_multi_angle_conditional_review() {
356 let prompt = stage_prompt(Stage::Ship, 13);
357 for angle in [
358 "doc-accuracy cross-reference",
359 "security / leaked-data",
360 "CI/build correctness",
361 "external-state claims",
362 "generalist deep pass",
363 ] {
364 assert!(prompt.contains(angle), "Ship prompt missing angle: {angle}");
365 }
366 assert!(prompt.contains("parallel finder subagents"));
367 assert!(prompt.contains("focused sequential pass"));
368 assert!(prompt.contains("Merge and deduplicate"));
369 assert!(prompt.contains("REVIEW.md"));
370 }
371
372 #[test]
373 fn ship_prompt_uses_project_review_angle_override() {
374 let dir = tempfile::tempdir().unwrap();
375 std::fs::write(
376 dir.path().join("devflow.toml"),
377 "review_angles = [\"custom release evidence\", \"custom threat boundary\"]\n",
378 )
379 .unwrap();
380
381 let prompt = stage_prompt_for_project(Stage::Ship, 13, dir.path());
382
383 assert!(prompt.contains("custom release evidence"));
384 assert!(prompt.contains("custom threat boundary"));
385 assert!(!prompt.contains("doc-accuracy cross-reference"));
386 }
387
388 #[test]
389 fn code_stage_prompt_is_unchanged_single_command_template() {
390 let prompt = stage_prompt(Stage::Code, 9);
398 assert!(prompt.contains("/gsd-execute-phase 9"));
399 assert!(prompt.contains("DEVFLOW_RESULT"));
400 assert!(
401 !prompt.contains("/gsd-code-review"),
402 "Code prompt should not carry Ship-specific code-review sequencing"
403 );
404 assert!(
405 !prompt.contains("already exists"),
406 "Code prompt should not carry the Define/Plan idempotency contract"
407 );
408 assert!(prompt.contains("Advisory incremental self-review"));
409 for angle in [
410 "doc accuracy",
411 "leaked data",
412 "CI/build correctness",
413 "external-state claims",
414 ] {
415 assert!(prompt.contains(angle), "Code prompt missing angle: {angle}");
416 }
417 assert!(!prompt.contains("AskUserQuestion"));
418 assert!(!prompt.contains("request_user_input"));
419 }
420
421 #[test]
426 fn plan_prompt_is_idempotent() {
427 let prompt = stage_prompt(Stage::Plan, 9);
428 assert!(
429 prompt.contains("/gsd-plan-phase 9"),
430 "Plan prompt missing /gsd-plan-phase 9"
431 );
432 assert!(
433 prompt.contains("09-*PLAN.md"),
434 "Plan prompt must check for its pre-existing artifact"
435 );
436 assert!(
437 prompt.contains("Do NOT run the GSD command"),
438 "Plan prompt must no-op when the artifact exists"
439 );
440 assert!(
441 prompt.contains("do NOT ask for input"),
442 "Plan prompt must forbid interactive input"
443 );
444 assert!(prompt.contains("DEVFLOW_RESULT"));
445 }
446
447 #[test]
452 fn define_prompt_never_invokes_discuss_phase() {
453 let prompt = stage_prompt(Stage::Define, 9);
454 assert!(
455 !prompt.contains("/gsd-discuss-phase"),
456 "Define prompt must never invoke the interactive discuss-phase command (D-14)"
457 );
458 assert!(
459 prompt.contains("must NOT run") || prompt.contains("do NOT run"),
460 "Define prompt must forbid running an interactive interview headlessly"
461 );
462 assert!(
463 prompt.contains("do NOT ask for input") || prompt.contains("must NOT ask for input"),
464 "Define prompt must forbid requesting input"
465 );
466 assert!(
467 prompt.to_lowercase().contains("modify"),
468 "Define prompt must forbid modifying existing planning artifacts"
469 );
470 assert!(prompt.contains("DEVFLOW_RESULT"));
471 }
472
473 #[test]
474 fn validate_stage_prompt_requires_verdict() {
475 let prompt = stage_prompt(Stage::Validate, 13);
476 assert!(
477 prompt.contains("/gsd-validate-phase 13"),
478 "Validate prompt missing its GSD command"
479 );
480 assert!(
481 prompt.contains("\"verdict\": \"pass\""),
482 "Validate prompt must name the exact lowercase pass verdict"
483 );
484 assert!(
485 prompt.contains("\"verdict\": \"gaps\""),
486 "Validate prompt must name the exact lowercase gaps verdict"
487 );
488 assert!(prompt.contains("REQUIRED"));
489 assert!(prompt.contains("DEVFLOW_RESULT"));
490 }
491
492 #[test]
493 fn fix_prompts_select_the_right_command() {
494 assert!(fix_prompt(FixType::AuditFix, 11).contains("/gsd-audit-fix 11"));
495 assert!(fix_prompt(FixType::GapsOnly, 11).contains("--gaps-only"));
496 assert!(fix_prompt(FixType::AuditFix, 11).contains("DEVFLOW_RESULT"));
497 }
498
499 #[test]
503 fn checkpoint_auto_decide_prompt_is_deterministic() {
504 assert_eq!(
505 checkpoint_auto_decide_prompt(28),
506 checkpoint_auto_decide_prompt(28)
507 );
508 }
509
510 #[test]
511 fn checkpoint_auto_decide_prompt_terminates_with_completion_protocol() {
512 let prompt = checkpoint_auto_decide_prompt(28);
513 assert!(
514 prompt.ends_with(COMPLETION_PROTOCOL),
515 "the resumed session's exit must still be parseable by the same \
516 Layer 1 path as any other stage"
517 );
518 assert!(prompt.contains("DEVFLOW_RESULT"));
519 }
520
521 #[test]
522 fn checkpoint_auto_decide_prompt_states_no_operator_judgment_and_record_reasoning() {
523 let prompt = checkpoint_auto_decide_prompt(28).to_lowercase();
524 assert!(
525 prompt.contains("no human operator") || prompt.contains("nobody"),
526 "must state plainly that no operator is available"
527 );
528 assert!(
529 prompt.contains("judgment") || prompt.contains("judgement"),
530 "must instruct the agent to use its own judgment"
531 );
532 assert!(
533 prompt.contains("record") && prompt.contains("reasoning"),
534 "must require recording the reasoning in the final message, since \
535 this is the ONLY record of what was decided (D-07)"
536 );
537 }
538
539 #[test]
540 fn checkpoint_auto_decide_prompt_substitutes_phase_for_legibility() {
541 assert!(checkpoint_auto_decide_prompt(42).contains("phase 42"));
542 }
543}