1use crate::phase_id::PhaseId;
9use crate::stage::Stage;
10use std::path::Path;
11
12const SHIP_REVIEW_ANGLES: &[&str] = &[
13 "doc-accuracy cross-reference (do documented claims match source?)",
14 "security / leaked-data (does anything commit secrets, session data, or telemetry?)",
15 "CI/build correctness (can a failing step still report green?)",
16 "external-state claims (does the diff claim merges, tags, or deletions that are not actually true?)",
17 "one generalist deep pass",
18];
19
20const AUTO_CHAIN_PRESERVING_FLAG: &str = "--auto";
41
42const COMPLETION_PROTOCOL: &str = "\
44## Completion Protocol (REQUIRED)\n\
45\n\
46When all work is done, your FINAL message must be exactly:\n\
47\n\
48DEVFLOW_RESULT: {\"status\": \"success\"}\n\
49\n\
50If something prevents completion:\n\
51\n\
52DEVFLOW_RESULT: {\"status\": \"failed\", \"reason\": \"specific explanation\"}\n\
53\n\
54DevFlow reads this line to decide whether the stage succeeded. \
55Output nothing after it.";
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72#[non_exhaustive]
73pub enum FixType {
74 AuditFix,
76 GapsOnly,
78 FullExecute,
84}
85
86fn gsd_command_for(stage: Stage, phase: PhaseId) -> String {
88 stage.gsd_command().replace("{N}", &phase.to_string())
89}
90
91fn ship_stage_prompt(phase: PhaseId, review_angles: &[String]) -> String {
105 let code_review = format!("/gsd-code-review {phase}");
106 let ship = format!("/gsd-ship {phase}");
107 let review_angles = review_angles
108 .iter()
109 .map(|angle| format!("- {angle}"))
110 .collect::<Vec<_>>()
111 .join("\n");
112 format!(
113 "Run the Ship stage in two steps:\n\
114 \n\
115 1. Run `{code_review}` (non-interactive). This writes a `REVIEW.md` \
116 artifact with severity-classified findings. Review at high depth from \
117 every angle below:\n\
118 \n\
119 {review_angles}\n\
120 \n\
121 If your harness supports parallel finder subagents, dispatch one per \
122 angle; otherwise run each angle as a focused sequential pass. Merge \
123 and deduplicate every angle's findings into one `REVIEW.md`.\n\
124 2. Check `REVIEW.md` for the Critical-severity gate:\n\
125 \n\
126 - If `REVIEW.md` contains ANY finding at Critical severity: do NOT \
127 run `{ship}` at all. Your FINAL message must be exactly:\n\
128 \n\
129 DEVFLOW_RESULT: {{\"status\": \"failed\", \"reason\": \"review: <short summary of the Critical findings>\"}}\n\
130 \n\
131 - If `REVIEW.md` has NO Critical-severity findings: run `{ship}` and \
132 report the outcome via the normal completion protocol below.\n\
133 \n\
134 {COMPLETION_PROTOCOL}"
135 )
136}
137
138fn validate_stage_prompt(phase: PhaseId) -> String {
147 let command = gsd_command_for(Stage::Validate, phase);
148 format!(
149 "Run the GSD workflow command for this stage:\n\n {command}\n\n\
150 ## Completion Protocol (REQUIRED)\n\
151 \n\
152 When all work is done, your FINAL message must be exactly one of:\n\
153 \n\
154 DEVFLOW_RESULT: {{\"status\": \"success\", \"verdict\": \"pass\"}}\n\
155 \n\
156 if validation found NO gaps, or:\n\
157 \n\
158 DEVFLOW_RESULT: {{\"status\": \"success\", \"verdict\": \"gaps\"}}\n\
159 \n\
160 if validation found gaps that still need fixing. The `verdict` field \
161 is REQUIRED for this stage — it is distinct from `status` (which only \
162 reports whether the validation task itself completed) and MUST be \
163 exactly the lowercase string `pass` or `gaps`.\n\
164 \n\
165 If something prevents completion:\n\
166 \n\
167 DEVFLOW_RESULT: {{\"status\": \"failed\", \"reason\": \"specific explanation\"}}\n\
168 \n\
169 DevFlow reads this line to decide whether the stage succeeded. \
170 Output nothing after it."
171 )
172}
173
174fn idempotent_stage_prompt(phase: PhaseId) -> String {
192 let artifact = "PLAN.md";
193 let command = gsd_command_for(Stage::Plan, phase);
194 let padded = phase.padded();
195 format!(
196 "First check whether this stage's deliverable already exists:\n\
197 \n\
198 ls .planning/phases/{padded}-*/{padded}-*{artifact} 2>/dev/null\n\
199 \n\
200 - If it EXISTS: the stage's work is already done. Do NOT run the GSD \
201 command, do NOT ask for input, and do NOT modify the existing \
202 artifacts. Your FINAL message must be exactly:\n\
203 \n\
204 DEVFLOW_RESULT: {{\"status\": \"success\"}}\n\
205 \n\
206 - If it does NOT exist: run the GSD workflow command for this stage:\n\
207 \n\
208 \x20 {command}\n\
209 \n\
210 {COMPLETION_PROTOCOL}"
211 )
212}
213
214fn define_stage_prompt(phase: PhaseId) -> String {
225 format!(
226 "This is the Define stage of a headless DevFlow run for phase {phase}.\n\
227 \n\
228 There is no agent work to perform here. Whether or not this phase's \
229 CONTEXT.md already exists, you must NOT run an interactive \
230 discuss-phase or interview command, and you must NOT ask for input \
231 — this run is headless and no operator is available to answer \
232 interactive questions. Do NOT modify any existing planning \
233 artifacts.\n\
234 \n\
235 {COMPLETION_PROTOCOL}"
236 )
237}
238
239pub fn stage_prompt(stage: Stage, phase: PhaseId) -> String {
241 stage_prompt_with_project(stage, phase, None)
242}
243
244pub fn stage_prompt_for_project(stage: Stage, phase: PhaseId, project_root: &Path) -> String {
250 stage_prompt_with_project(stage, phase, Some(project_root))
251}
252
253fn stage_prompt_with_project(stage: Stage, phase: PhaseId, project_root: Option<&Path>) -> String {
254 if stage == Stage::Ship {
255 let review_angles = project_root
256 .and_then(crate::config::review_angles)
257 .unwrap_or_else(|| {
258 SHIP_REVIEW_ANGLES
259 .iter()
260 .map(|angle| (*angle).to_owned())
261 .collect()
262 });
263 return ship_stage_prompt(phase, &review_angles);
264 }
265 if stage == Stage::Validate {
266 return validate_stage_prompt(phase);
267 }
268 if stage == Stage::Define {
269 return define_stage_prompt(phase);
270 }
271 if stage == Stage::Plan {
272 return idempotent_stage_prompt(phase);
273 }
274 let command = gsd_command_for(stage, phase);
275 if stage == Stage::Code {
276 let command = format!("{command} {AUTO_CHAIN_PRESERVING_FLAG}");
291 return format!(
292 "Run the GSD workflow command for this stage:\n\n {command}\n\n\
293 ## Advisory incremental self-review\n\
294 \n\
295 After each plan or wave lands, perform a quick, shallow self-check \
296 for doc accuracy, leaked data, CI/build correctness, and \
297 external-state claims. Record any drift in the working output and \
298 continue execution; the authoritative review happens during Ship. \
299 This check must not pause execution or request human input.\n\
300 \n\
301 {COMPLETION_PROTOCOL}"
302 );
303 }
304 format!(
305 "Run the GSD workflow command for this stage:\n\n {command}\n\n{COMPLETION_PROTOCOL}"
306 )
307}
308
309pub fn checkpoint_auto_decide_prompt(phase: PhaseId) -> String {
322 format!(
323 "This is phase {phase} of a headless DevFlow run. You previously \
324 stopped at a human-blocking checkpoint, but no human operator is \
325 available to answer it — this run is unattended, and none is \
326 coming. DevFlow's policy is for you to resolve the checkpoint \
327 yourself, using your own best judgment, and continue the work. You \
328 MUST record your reasoning for the decision you made in your final \
329 message, so the decision is auditable after the fact.\n\
330 \n\
331 {COMPLETION_PROTOCOL}"
332 )
333}
334
335pub fn fix_prompt(fix_type: FixType, phase: PhaseId) -> String {
352 let command = match fix_type {
353 FixType::AuditFix => format!("/gsd-audit-fix {phase}"),
354 FixType::GapsOnly => {
355 format!("/gsd-execute-phase {phase} --gaps-only {AUTO_CHAIN_PRESERVING_FLAG}")
356 }
357 FixType::FullExecute => {
358 format!("/gsd-execute-phase {phase} {AUTO_CHAIN_PRESERVING_FLAG}")
359 }
360 };
361 format!(
362 "Validation reported issues. Run the fix command for this loop:\n\n {command}\n\n{COMPLETION_PROTOCOL}"
363 )
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369
370 #[test]
371 fn each_stage_prompt_carries_its_gsd_command_and_marker() {
372 let cases = [
375 (Stage::Plan, "/gsd-plan-phase 11"),
376 (Stage::Code, "/gsd-execute-phase 11"),
377 (Stage::Validate, "/gsd-validate-phase 11"),
378 (Stage::Ship, "/gsd-ship 11"),
379 ];
380 for (stage, command) in cases {
381 let prompt = stage_prompt(stage, PhaseId::new(11));
382 assert!(prompt.contains(command), "{stage} prompt missing {command}");
383 assert!(prompt.contains("DEVFLOW_RESULT"));
384 }
385 }
386
387 #[test]
388 fn phase_placeholder_is_substituted() {
389 assert!(stage_prompt(Stage::Code, PhaseId::new(7)).contains("/gsd-execute-phase 7"));
390 assert!(!stage_prompt(Stage::Code, PhaseId::new(7)).contains("{N}"));
391 }
392
393 #[test]
394 fn ship_prompt_sequences_code_review_before_ship() {
395 let prompt = stage_prompt(Stage::Ship, PhaseId::new(13));
396 let review_pos = prompt
397 .find("/gsd-code-review 13")
398 .expect("Ship prompt must run /gsd-code-review {N}");
399 let ship_pos = prompt
400 .find("/gsd-ship 13")
401 .expect("Ship prompt must run /gsd-ship {N}");
402 assert!(
403 review_pos < ship_pos,
404 "code-review must be sequenced before ship"
405 );
406 }
407
408 #[test]
409 fn ship_prompt_defines_critical_gate_and_review_failed_contract() {
410 let prompt = stage_prompt(Stage::Ship, PhaseId::new(13));
411 assert!(
412 prompt.contains("REVIEW.md"),
413 "Ship prompt must reference the REVIEW.md artifact"
414 );
415 assert!(
416 prompt.to_lowercase().contains("critical"),
417 "Ship prompt must name the Critical-severity gate"
418 );
419 assert!(
420 prompt.contains("do not run")
421 || prompt.contains("do NOT run")
422 || prompt.contains("DO NOT run"),
423 "Ship prompt must instruct the agent not to run /gsd-ship on Critical findings"
424 );
425 assert!(
426 prompt.contains("review:"),
427 "Ship prompt must define the review: ReviewFailed reason convention"
428 );
429 assert!(prompt.contains("DEVFLOW_RESULT"));
430 }
431
432 #[test]
433 fn ship_prompt_includes_multi_angle_conditional_review() {
434 let prompt = stage_prompt(Stage::Ship, PhaseId::new(13));
435 for angle in [
436 "doc-accuracy cross-reference",
437 "security / leaked-data",
438 "CI/build correctness",
439 "external-state claims",
440 "generalist deep pass",
441 ] {
442 assert!(prompt.contains(angle), "Ship prompt missing angle: {angle}");
443 }
444 assert!(prompt.contains("parallel finder subagents"));
445 assert!(prompt.contains("focused sequential pass"));
446 assert!(prompt.contains("Merge and deduplicate"));
447 assert!(prompt.contains("REVIEW.md"));
448 }
449
450 #[test]
451 fn ship_prompt_uses_project_review_angle_override() {
452 let dir = tempfile::tempdir().unwrap();
453 std::fs::write(
454 dir.path().join("devflow.toml"),
455 "review_angles = [\"custom release evidence\", \"custom threat boundary\"]\n",
456 )
457 .unwrap();
458
459 let prompt = stage_prompt_for_project(Stage::Ship, PhaseId::new(13), dir.path());
460
461 assert!(prompt.contains("custom release evidence"));
462 assert!(prompt.contains("custom threat boundary"));
463 assert!(!prompt.contains("doc-accuracy cross-reference"));
464 }
465
466 #[test]
467 fn code_stage_prompt_is_unchanged_single_command_template() {
468 let prompt = stage_prompt(Stage::Code, PhaseId::new(9));
476 assert!(prompt.contains("/gsd-execute-phase 9"));
477 assert!(prompt.contains("DEVFLOW_RESULT"));
478 assert!(
479 !prompt.contains("/gsd-code-review"),
480 "Code prompt should not carry Ship-specific code-review sequencing"
481 );
482 assert!(
483 !prompt.contains("already exists"),
484 "Code prompt should not carry the Define/Plan idempotency contract"
485 );
486 assert!(prompt.contains("Advisory incremental self-review"));
487 for angle in [
488 "doc accuracy",
489 "leaked data",
490 "CI/build correctness",
491 "external-state claims",
492 ] {
493 assert!(prompt.contains(angle), "Code prompt missing angle: {angle}");
494 }
495 assert!(!prompt.contains("AskUserQuestion"));
496 assert!(!prompt.contains("request_user_input"));
497 }
498
499 #[test]
504 fn plan_prompt_is_idempotent() {
505 let prompt = stage_prompt(Stage::Plan, PhaseId::new(9));
506 assert!(
507 prompt.contains("/gsd-plan-phase 9"),
508 "Plan prompt missing /gsd-plan-phase 9"
509 );
510 assert!(
511 prompt.contains("09-*PLAN.md"),
512 "Plan prompt must check for its pre-existing artifact"
513 );
514 assert!(
515 prompt.contains("Do NOT run the GSD command"),
516 "Plan prompt must no-op when the artifact exists"
517 );
518 assert!(
519 prompt.contains("do NOT ask for input"),
520 "Plan prompt must forbid interactive input"
521 );
522 assert!(prompt.contains("DEVFLOW_RESULT"));
523 }
524
525 #[test]
530 fn define_prompt_never_invokes_discuss_phase() {
531 let prompt = stage_prompt(Stage::Define, PhaseId::new(9));
532 assert!(
533 !prompt.contains("/gsd-discuss-phase"),
534 "Define prompt must never invoke the interactive discuss-phase command (D-14)"
535 );
536 assert!(
537 prompt.contains("must NOT run") || prompt.contains("do NOT run"),
538 "Define prompt must forbid running an interactive interview headlessly"
539 );
540 assert!(
541 prompt.contains("do NOT ask for input") || prompt.contains("must NOT ask for input"),
542 "Define prompt must forbid requesting input"
543 );
544 assert!(
545 prompt.to_lowercase().contains("modify"),
546 "Define prompt must forbid modifying existing planning artifacts"
547 );
548 assert!(prompt.contains("DEVFLOW_RESULT"));
549 }
550
551 #[test]
552 fn validate_stage_prompt_requires_verdict() {
553 let prompt = stage_prompt(Stage::Validate, PhaseId::new(13));
554 assert!(
555 prompt.contains("/gsd-validate-phase 13"),
556 "Validate prompt missing its GSD command"
557 );
558 assert!(
559 prompt.contains("\"verdict\": \"pass\""),
560 "Validate prompt must name the exact lowercase pass verdict"
561 );
562 assert!(
563 prompt.contains("\"verdict\": \"gaps\""),
564 "Validate prompt must name the exact lowercase gaps verdict"
565 );
566 assert!(prompt.contains("REQUIRED"));
567 assert!(prompt.contains("DEVFLOW_RESULT"));
568 }
569
570 #[test]
571 fn fix_prompts_select_the_right_command() {
572 assert!(fix_prompt(FixType::AuditFix, PhaseId::new(11)).contains("/gsd-audit-fix 11"));
573 assert!(fix_prompt(FixType::GapsOnly, PhaseId::new(11)).contains("--gaps-only"));
574 assert!(fix_prompt(FixType::AuditFix, PhaseId::new(11)).contains("DEVFLOW_RESULT"));
575
576 let full_execute_prompt = fix_prompt(FixType::FullExecute, PhaseId::new(11));
578 assert!(full_execute_prompt.contains("/gsd-execute-phase 11"));
579 assert!(!full_execute_prompt.contains("--gaps-only"));
584 }
585
586 #[test]
594 fn fix_prompts_carry_the_chain_flag_token_only_where_it_reaches_execute_phase() {
595 let phase = PhaseId::new(11);
596
597 assert!(
598 fix_prompt(FixType::GapsOnly, phase).contains(AUTO_CHAIN_PRESERVING_FLAG),
599 "the --gaps-only fix loop reaches execute-phase.md, so it meets the \
600 sync-clear step and needs the token exactly as the first Code pass does"
601 );
602 assert!(
603 fix_prompt(FixType::FullExecute, phase).contains(AUTO_CHAIN_PRESERVING_FLAG),
604 "the full-execute loop-back reaches execute-phase.md too"
605 );
606 assert!(
607 !fix_prompt(FixType::AuditFix, phase).contains(AUTO_CHAIN_PRESERVING_FLAG),
608 "audit-fix routes to /gsd-audit-fix and never reaches execute-phase.md, \
609 so it never meets the sync-clear step the token exists to skip"
610 );
611 }
612
613 #[test]
616 fn the_code_prompt_carries_the_chain_flag_token() {
617 let prompt = stage_prompt(Stage::Code, PhaseId::new(11));
618 assert!(prompt.contains(&format!(
619 "/gsd-execute-phase 11 {AUTO_CHAIN_PRESERVING_FLAG}"
620 )));
621 }
622
623 #[test]
632 fn the_plan_prompt_never_carries_the_chain_flag_token() {
633 let plan = stage_prompt(Stage::Plan, PhaseId::new(11));
634 assert!(
635 !plan.contains(AUTO_CHAIN_PRESERVING_FLAG),
636 "the Plan prompt must not chain into execute-phase (D-04)"
637 );
638 assert!(plan.contains("/gsd-plan-phase 11"));
641 }
642
643 #[test]
647 fn checkpoint_auto_decide_prompt_is_deterministic() {
648 assert_eq!(
649 checkpoint_auto_decide_prompt(PhaseId::new(28)),
650 checkpoint_auto_decide_prompt(PhaseId::new(28))
651 );
652 }
653
654 #[test]
655 fn checkpoint_auto_decide_prompt_terminates_with_completion_protocol() {
656 let prompt = checkpoint_auto_decide_prompt(PhaseId::new(28));
657 assert!(
658 prompt.ends_with(COMPLETION_PROTOCOL),
659 "the resumed session's exit must still be parseable by the same \
660 Layer 1 path as any other stage"
661 );
662 assert!(prompt.contains("DEVFLOW_RESULT"));
663 }
664
665 #[test]
666 fn checkpoint_auto_decide_prompt_states_no_operator_judgment_and_record_reasoning() {
667 let prompt = checkpoint_auto_decide_prompt(PhaseId::new(28)).to_lowercase();
668 assert!(
669 prompt.contains("no human operator") || prompt.contains("nobody"),
670 "must state plainly that no operator is available"
671 );
672 assert!(
673 prompt.contains("judgment") || prompt.contains("judgement"),
674 "must instruct the agent to use its own judgment"
675 );
676 assert!(
677 prompt.contains("record") && prompt.contains("reasoning"),
678 "must require recording the reasoning in the final message, since \
679 this is the ONLY record of what was decided (D-07)"
680 );
681 }
682
683 #[test]
684 fn checkpoint_auto_decide_prompt_substitutes_phase_for_legibility() {
685 assert!(checkpoint_auto_decide_prompt(PhaseId::new(42)).contains("phase 42"));
686 }
687}