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
42pub const 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, PartialEq, Eq)]
67pub enum StageIntent {
68 Define {
69 phase: PhaseId,
70 },
71 Plan {
72 phase: PhaseId,
73 },
74 Code {
75 phase: PhaseId,
76 fix: Option<FixType>,
77 },
78 Validate {
79 phase: PhaseId,
80 },
81 Ship {
82 phase: PhaseId,
83 review_angles: Vec<String>,
84 },
85}
86
87impl StageIntent {
88 pub fn stage(&self) -> Stage {
90 match self {
91 StageIntent::Define { .. } => Stage::Define,
92 StageIntent::Plan { .. } => Stage::Plan,
93 StageIntent::Code { .. } => Stage::Code,
94 StageIntent::Validate { .. } => Stage::Validate,
95 StageIntent::Ship { .. } => Stage::Ship,
96 }
97 }
98
99 pub fn for_stage(stage: Stage, phase: PhaseId) -> Self {
103 Self::for_stage_in_project(stage, phase, None)
104 }
105
106 pub fn for_stage_in_project(stage: Stage, phase: PhaseId, project_root: Option<&Path>) -> Self {
108 match stage {
109 Stage::Define => StageIntent::Define { phase },
110 Stage::Plan => StageIntent::Plan { phase },
111 Stage::Code => StageIntent::Code { phase, fix: None },
112 Stage::Validate => StageIntent::Validate { phase },
113 Stage::Ship => {
114 let review_angles = project_root
115 .and_then(crate::config::review_angles)
116 .unwrap_or_else(|| {
117 SHIP_REVIEW_ANGLES
118 .iter()
119 .map(|angle| (*angle).to_owned())
120 .collect()
121 });
122 StageIntent::Ship {
123 phase,
124 review_angles,
125 }
126 }
127 }
128 }
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146#[non_exhaustive]
147pub enum FixType {
148 AuditFix,
150 GapsOnly,
152 FullExecute,
158}
159
160fn gsd_command_for(stage: Stage, phase: PhaseId) -> String {
162 stage.gsd_command().replace("{N}", &phase.to_string())
163}
164
165fn ship_stage_prompt(phase: PhaseId, review_angles: &[String]) -> String {
179 let code_review = format!("/gsd-code-review {phase}");
180 let ship = format!("/gsd-ship {phase}");
181 let review_angles = review_angles
182 .iter()
183 .map(|angle| format!("- {angle}"))
184 .collect::<Vec<_>>()
185 .join("\n");
186 format!(
187 "Run the Ship stage in two steps:\n\
188 \n\
189 1. Run `{code_review}` (non-interactive). This writes a `REVIEW.md` \
190 artifact with severity-classified findings. Review at high depth from \
191 every angle below:\n\
192 \n\
193 {review_angles}\n\
194 \n\
195 If your harness supports parallel finder subagents, dispatch one per \
196 angle; otherwise run each angle as a focused sequential pass. Merge \
197 and deduplicate every angle's findings into one `REVIEW.md`.\n\
198 2. Check `REVIEW.md` for the Critical-severity gate:\n\
199 \n\
200 - If `REVIEW.md` contains ANY finding at Critical severity: do NOT \
201 run `{ship}` at all. Your FINAL message must be exactly:\n\
202 \n\
203 DEVFLOW_RESULT: {{\"status\": \"failed\", \"reason\": \"review: <short summary of the Critical findings>\"}}\n\
204 \n\
205 - If `REVIEW.md` has NO Critical-severity findings: run `{ship}` and \
206 report the outcome via the normal completion protocol below.\n\
207 \n\
208 {COMPLETION_PROTOCOL}"
209 )
210}
211
212const VALIDATE_VERDICT_CONTRACT: &str = "\
224## Completion Protocol (REQUIRED)\n\
225\n\
226When all work is done, your FINAL message must be exactly one of:\n\
227\n\
228DEVFLOW_RESULT: {\"status\": \"success\", \"verdict\": \"pass\"}\n\
229\n\
230if validation found NO gaps, or:\n\
231\n\
232DEVFLOW_RESULT: {\"status\": \"success\", \"verdict\": \"gaps\"}\n\
233\n\
234if validation found gaps that still need fixing. The `verdict` field is \
235REQUIRED for this stage — it is distinct from `status` (which only reports \
236whether the validation task itself completed) and MUST be exactly the \
237lowercase string `pass` or `gaps`.\n\
238\n\
239If something prevents completion:\n\
240\n\
241DEVFLOW_RESULT: {\"status\": \"failed\", \"reason\": \"specific explanation\"}\n\
242\n\
243DevFlow reads this line to decide whether the stage succeeded. \
244Output nothing after it.";
245
246fn validate_stage_prompt(phase: PhaseId) -> String {
247 let command = gsd_command_for(Stage::Validate, phase);
248 format!(
249 "Run the GSD workflow command for this stage:\n\n {command}\n\n{VALIDATE_VERDICT_CONTRACT}"
250 )
251}
252
253fn idempotent_stage_prompt(phase: PhaseId) -> String {
271 let artifact = "PLAN.md";
272 let command = gsd_command_for(Stage::Plan, phase);
273 let padded = phase.padded();
274 format!(
275 "First check whether this stage's deliverable already exists:\n\
276 \n\
277 ls .planning/phases/{padded}-*/{padded}-*{artifact} 2>/dev/null\n\
278 \n\
279 - If it EXISTS: the stage's work is already done. Do NOT run the GSD \
280 command, do NOT ask for input, and do NOT modify the existing \
281 artifacts. Your FINAL message must be exactly:\n\
282 \n\
283 DEVFLOW_RESULT: {{\"status\": \"success\"}}\n\
284 \n\
285 - If it does NOT exist: run the GSD workflow command for this stage:\n\
286 \n\
287 \x20 {command}\n\
288 \n\
289 {COMPLETION_PROTOCOL}"
290 )
291}
292
293fn define_stage_prompt(phase: PhaseId) -> String {
304 format!(
305 "This is the Define stage of a headless DevFlow run for phase {phase}.\n\
306 \n\
307 There is no agent work to perform here. Whether or not this phase's \
308 CONTEXT.md already exists, you must NOT run an interactive \
309 discuss-phase or interview command, and you must NOT ask for input \
310 — this run is headless and no operator is available to answer \
311 interactive questions. Do NOT modify any existing planning \
312 artifacts.\n\
313 \n\
314 {COMPLETION_PROTOCOL}"
315 )
316}
317
318pub fn stage_prompt(stage: Stage, phase: PhaseId) -> String {
320 stage_prompt_with_project(stage, phase, None)
321}
322
323pub fn stage_prompt_for_project(stage: Stage, phase: PhaseId, project_root: &Path) -> String {
329 stage_prompt_with_project(stage, phase, Some(project_root))
330}
331
332fn code_stage_prompt(phase: PhaseId) -> String {
339 let command = format!(
340 "{} {AUTO_CHAIN_PRESERVING_FLAG}",
341 gsd_command_for(Stage::Code, phase)
342 );
343 format!(
344 "Run the GSD workflow command for this stage:\n\n {command}\n\n\
345 ## Advisory incremental self-review\n\
346 \n\
347 After each plan or wave lands, perform a quick, shallow self-check \
348 for doc accuracy, leaked data, CI/build correctness, and \
349 external-state claims. Record any drift in the working output and \
350 continue execution; the authoritative review happens during Ship. \
351 This check must not pause execution or request human input.\n\
352 \n\
353 {COMPLETION_PROTOCOL}"
354 )
355}
356
357pub fn render_claude_style(intent: &StageIntent) -> String {
364 match intent {
365 StageIntent::Define { phase } => define_stage_prompt(*phase),
366 StageIntent::Plan { phase } => idempotent_stage_prompt(*phase),
367 StageIntent::Code { phase, fix: None } => code_stage_prompt(*phase),
368 StageIntent::Code {
369 phase,
370 fix: Some(fix),
371 } => fix_prompt(*fix, *phase),
372 StageIntent::Validate { phase } => validate_stage_prompt(*phase),
373 StageIntent::Ship {
374 phase,
375 review_angles,
376 } => ship_stage_prompt(*phase, review_angles),
377 }
378}
379
380pub fn render_workflow_style(intent: &StageIntent, workflow_root: &str) -> String {
391 match intent {
392 StageIntent::Define { phase } => define_stage_prompt(*phase),
395 StageIntent::Plan { phase } => workflow_plan_prompt(*phase, workflow_root),
396 StageIntent::Code { phase, fix } => workflow_code_prompt(*phase, *fix, workflow_root),
397 StageIntent::Validate { phase } => workflow_validate_prompt(*phase, workflow_root),
398 StageIntent::Ship {
399 phase,
400 review_angles,
401 } => workflow_ship_prompt(*phase, review_angles, workflow_root),
402 }
403}
404
405fn workflow_plan_prompt(phase: PhaseId, workflow_root: &str) -> String {
406 let artifact = "PLAN.md";
407 let padded = phase.padded();
408 format!(
409 "First check whether this stage's deliverable already exists:\n\
410 \n\
411 ls .planning/phases/{padded}-*/{padded}-*{artifact} 2>/dev/null\n\
412 \n\
413 - If it EXISTS: the stage's work is already done. Do NOT run the \
414 workflow, do NOT ask for input, and do NOT modify the existing \
415 artifacts. Your FINAL message must be exactly:\n\
416 \n\
417 DEVFLOW_RESULT: {{\"status\": \"success\"}}\n\
418 \n\
419 - If it does NOT exist: read and follow the GSD workflow file at \
420 {workflow_root}/plan-phase.md for phase {phase}.\n\
421 \n\
422 {COMPLETION_PROTOCOL}"
423 )
424}
425
426fn workflow_code_prompt(phase: PhaseId, fix: Option<FixType>, workflow_root: &str) -> String {
427 match fix {
428 Some(FixType::AuditFix) => format!(
429 "Read and follow the GSD workflow file at {workflow_root}/audit-fix.md for \
430 phase {phase}.\n\n{COMPLETION_PROTOCOL}"
431 ),
432 Some(FixType::GapsOnly) => format!(
433 "Read and follow the GSD workflow file at {workflow_root}/execute-phase.md for \
434 phase {phase} --auto --gaps-only. The `--auto` and `--gaps-only` flags are part \
435 of the workflow invocation and must be preserved verbatim.\n\n{COMPLETION_PROTOCOL}"
436 ),
437 Some(FixType::FullExecute) | None => format!(
438 "Read and follow the GSD workflow file at {workflow_root}/execute-phase.md for \
439 phase {phase} --auto. The `--auto` flag is part of the workflow invocation and \
440 must be preserved verbatim.\n\n\
441 ## Advisory incremental self-review\n\
442 \n\
443 After each plan or wave lands, perform a quick, shallow self-check \
444 for doc accuracy, leaked data, CI/build correctness, and \
445 external-state claims. Record any drift in the working output and \
446 continue execution; the authoritative review happens during Ship. \
447 This check must not pause execution or request human input.\n\
448 \n\
449 {COMPLETION_PROTOCOL}"
450 ),
451 }
452}
453
454fn workflow_validate_prompt(phase: PhaseId, workflow_root: &str) -> String {
455 format!(
456 "Read and follow the GSD workflow file at {workflow_root}/validate-phase.md for \
457 phase {phase}.\n\n{VALIDATE_VERDICT_CONTRACT}"
458 )
459}
460
461fn workflow_ship_prompt(phase: PhaseId, review_angles: &[String], workflow_root: &str) -> String {
462 let review_angles = review_angles
463 .iter()
464 .map(|angle| format!("- {angle}"))
465 .collect::<Vec<_>>()
466 .join("\n");
467 format!(
468 "Run the Ship stage in two steps:\n\
469 \n\
470 1. Read and follow the GSD workflow file at {workflow_root}/code-review.md for \
471 phase {phase}. This writes a REVIEW.md artifact with severity-classified findings. \
472 Review at high depth from every angle below:\n\
473 \n\
474 {review_angles}\n\
475 \n\
476 If your harness supports parallel finder subagents, dispatch one per angle; otherwise \
477 run each angle as a focused sequential pass. Merge and deduplicate every angle's \
478 findings into one REVIEW.md.\n\
479 2. Check REVIEW.md for the Critical-severity gate:\n\
480 \n\
481 - If REVIEW.md contains ANY finding at Critical severity: do NOT run the ship workflow \
482 at all. Your FINAL message must be exactly:\n\
483 \n\
484 DEVFLOW_RESULT: {{\"status\": \"failed\", \"reason\": \"review: <short summary of the \
485 Critical findings>\"}}\n\
486 \n\
487 - If REVIEW.md has NO Critical-severity findings: read and follow the GSD workflow file \
488 at {workflow_root}/ship.md for phase {phase} and report the outcome via the normal \
489 completion protocol below.\n\
490 \n\
491 {COMPLETION_PROTOCOL}"
492 )
493}
494
495fn stage_prompt_with_project(stage: Stage, phase: PhaseId, project_root: Option<&Path>) -> String {
496 render_claude_style(&StageIntent::for_stage_in_project(
497 stage,
498 phase,
499 project_root,
500 ))
501}
502
503pub fn checkpoint_auto_decide_prompt(phase: PhaseId) -> String {
516 format!(
517 "This is phase {phase} of a headless DevFlow run. You previously \
518 stopped at a human-blocking checkpoint, but no human operator is \
519 available to answer it — this run is unattended, and none is \
520 coming. DevFlow's policy is for you to resolve the checkpoint \
521 yourself, using your own best judgment, and continue the work. You \
522 MUST record your reasoning for the decision you made in your final \
523 message, so the decision is auditable after the fact.\n\
524 \n\
525 {COMPLETION_PROTOCOL}"
526 )
527}
528
529pub fn fix_prompt(fix_type: FixType, phase: PhaseId) -> String {
546 let command = match fix_type {
547 FixType::AuditFix => format!("/gsd-audit-fix {phase}"),
548 FixType::GapsOnly => {
549 format!("/gsd-execute-phase {phase} --gaps-only {AUTO_CHAIN_PRESERVING_FLAG}")
550 }
551 FixType::FullExecute => {
552 format!("/gsd-execute-phase {phase} {AUTO_CHAIN_PRESERVING_FLAG}")
553 }
554 };
555 format!(
556 "Validation reported issues. Run the fix command for this loop:\n\n {command}\n\n{COMPLETION_PROTOCOL}"
557 )
558}
559
560#[cfg(test)]
561mod tests {
562 use super::*;
563
564 #[test]
565 fn each_stage_prompt_carries_its_gsd_command_and_marker() {
566 let cases = [
569 (Stage::Plan, "/gsd-plan-phase 11"),
570 (Stage::Code, "/gsd-execute-phase 11"),
571 (Stage::Validate, "/gsd-validate-phase 11"),
572 (Stage::Ship, "/gsd-ship 11"),
573 ];
574 for (stage, command) in cases {
575 let prompt = stage_prompt(stage, PhaseId::new(11));
576 assert!(prompt.contains(command), "{stage} prompt missing {command}");
577 assert!(prompt.contains("DEVFLOW_RESULT"));
578 }
579 }
580
581 #[test]
582 fn phase_placeholder_is_substituted() {
583 assert!(stage_prompt(Stage::Code, PhaseId::new(7)).contains("/gsd-execute-phase 7"));
584 assert!(!stage_prompt(Stage::Code, PhaseId::new(7)).contains("{N}"));
585 }
586
587 #[test]
588 fn ship_prompt_sequences_code_review_before_ship() {
589 let prompt = stage_prompt(Stage::Ship, PhaseId::new(13));
590 let review_pos = prompt
591 .find("/gsd-code-review 13")
592 .expect("Ship prompt must run /gsd-code-review {N}");
593 let ship_pos = prompt
594 .find("/gsd-ship 13")
595 .expect("Ship prompt must run /gsd-ship {N}");
596 assert!(
597 review_pos < ship_pos,
598 "code-review must be sequenced before ship"
599 );
600 }
601
602 #[test]
603 fn ship_prompt_defines_critical_gate_and_review_failed_contract() {
604 let prompt = stage_prompt(Stage::Ship, PhaseId::new(13));
605 assert!(
606 prompt.contains("REVIEW.md"),
607 "Ship prompt must reference the REVIEW.md artifact"
608 );
609 assert!(
610 prompt.to_lowercase().contains("critical"),
611 "Ship prompt must name the Critical-severity gate"
612 );
613 assert!(
614 prompt.contains("do not run")
615 || prompt.contains("do NOT run")
616 || prompt.contains("DO NOT run"),
617 "Ship prompt must instruct the agent not to run /gsd-ship on Critical findings"
618 );
619 assert!(
620 prompt.contains("review:"),
621 "Ship prompt must define the review: ReviewFailed reason convention"
622 );
623 assert!(prompt.contains("DEVFLOW_RESULT"));
624 }
625
626 #[test]
627 fn ship_prompt_includes_multi_angle_conditional_review() {
628 let prompt = stage_prompt(Stage::Ship, PhaseId::new(13));
629 for angle in [
630 "doc-accuracy cross-reference",
631 "security / leaked-data",
632 "CI/build correctness",
633 "external-state claims",
634 "generalist deep pass",
635 ] {
636 assert!(prompt.contains(angle), "Ship prompt missing angle: {angle}");
637 }
638 assert!(prompt.contains("parallel finder subagents"));
639 assert!(prompt.contains("focused sequential pass"));
640 assert!(prompt.contains("Merge and deduplicate"));
641 assert!(prompt.contains("REVIEW.md"));
642 }
643
644 #[test]
645 fn ship_prompt_uses_project_review_angle_override() {
646 let dir = tempfile::tempdir().unwrap();
647 std::fs::write(
648 dir.path().join("devflow.toml"),
649 "review_angles = [\"custom release evidence\", \"custom threat boundary\"]\n",
650 )
651 .unwrap();
652
653 let prompt = stage_prompt_for_project(Stage::Ship, PhaseId::new(13), dir.path());
654
655 assert!(prompt.contains("custom release evidence"));
656 assert!(prompt.contains("custom threat boundary"));
657 assert!(!prompt.contains("doc-accuracy cross-reference"));
658 }
659
660 #[test]
661 fn code_stage_prompt_is_unchanged_single_command_template() {
662 let prompt = stage_prompt(Stage::Code, PhaseId::new(9));
670 assert!(prompt.contains("/gsd-execute-phase 9"));
671 assert!(prompt.contains("DEVFLOW_RESULT"));
672 assert!(
673 !prompt.contains("/gsd-code-review"),
674 "Code prompt should not carry Ship-specific code-review sequencing"
675 );
676 assert!(
677 !prompt.contains("already exists"),
678 "Code prompt should not carry the Define/Plan idempotency contract"
679 );
680 assert!(prompt.contains("Advisory incremental self-review"));
681 for angle in [
682 "doc accuracy",
683 "leaked data",
684 "CI/build correctness",
685 "external-state claims",
686 ] {
687 assert!(prompt.contains(angle), "Code prompt missing angle: {angle}");
688 }
689 assert!(!prompt.contains("AskUserQuestion"));
690 assert!(!prompt.contains("request_user_input"));
691 }
692
693 #[test]
698 fn plan_prompt_is_idempotent() {
699 let prompt = stage_prompt(Stage::Plan, PhaseId::new(9));
700 assert!(
701 prompt.contains("/gsd-plan-phase 9"),
702 "Plan prompt missing /gsd-plan-phase 9"
703 );
704 assert!(
705 prompt.contains("09-*PLAN.md"),
706 "Plan prompt must check for its pre-existing artifact"
707 );
708 assert!(
709 prompt.contains("Do NOT run the GSD command"),
710 "Plan prompt must no-op when the artifact exists"
711 );
712 assert!(
713 prompt.contains("do NOT ask for input"),
714 "Plan prompt must forbid interactive input"
715 );
716 assert!(prompt.contains("DEVFLOW_RESULT"));
717 }
718
719 #[test]
724 fn define_prompt_never_invokes_discuss_phase() {
725 let prompt = stage_prompt(Stage::Define, PhaseId::new(9));
726 assert!(
727 !prompt.contains("/gsd-discuss-phase"),
728 "Define prompt must never invoke the interactive discuss-phase command (D-14)"
729 );
730 assert!(
731 prompt.contains("must NOT run") || prompt.contains("do NOT run"),
732 "Define prompt must forbid running an interactive interview headlessly"
733 );
734 assert!(
735 prompt.contains("do NOT ask for input") || prompt.contains("must NOT ask for input"),
736 "Define prompt must forbid requesting input"
737 );
738 assert!(
739 prompt.to_lowercase().contains("modify"),
740 "Define prompt must forbid modifying existing planning artifacts"
741 );
742 assert!(prompt.contains("DEVFLOW_RESULT"));
743 }
744
745 #[test]
746 fn validate_stage_prompt_requires_verdict() {
747 let prompt = stage_prompt(Stage::Validate, PhaseId::new(13));
748 assert!(
749 prompt.contains("/gsd-validate-phase 13"),
750 "Validate prompt missing its GSD command"
751 );
752 assert!(
753 prompt.contains("\"verdict\": \"pass\""),
754 "Validate prompt must name the exact lowercase pass verdict"
755 );
756 assert!(
757 prompt.contains("\"verdict\": \"gaps\""),
758 "Validate prompt must name the exact lowercase gaps verdict"
759 );
760 assert!(prompt.contains("REQUIRED"));
761 assert!(prompt.contains("DEVFLOW_RESULT"));
762 }
763
764 #[test]
765 fn fix_prompts_select_the_right_command() {
766 assert!(fix_prompt(FixType::AuditFix, PhaseId::new(11)).contains("/gsd-audit-fix 11"));
767 assert!(fix_prompt(FixType::GapsOnly, PhaseId::new(11)).contains("--gaps-only"));
768 assert!(fix_prompt(FixType::AuditFix, PhaseId::new(11)).contains("DEVFLOW_RESULT"));
769
770 let full_execute_prompt = fix_prompt(FixType::FullExecute, PhaseId::new(11));
772 assert!(full_execute_prompt.contains("/gsd-execute-phase 11"));
773 assert!(!full_execute_prompt.contains("--gaps-only"));
778 }
779
780 #[test]
788 fn fix_prompts_carry_the_chain_flag_token_only_where_it_reaches_execute_phase() {
789 let phase = PhaseId::new(11);
790
791 assert!(
792 fix_prompt(FixType::GapsOnly, phase).contains(AUTO_CHAIN_PRESERVING_FLAG),
793 "the --gaps-only fix loop reaches execute-phase.md, so it meets the \
794 sync-clear step and needs the token exactly as the first Code pass does"
795 );
796 assert!(
797 fix_prompt(FixType::FullExecute, phase).contains(AUTO_CHAIN_PRESERVING_FLAG),
798 "the full-execute loop-back reaches execute-phase.md too"
799 );
800 assert!(
801 !fix_prompt(FixType::AuditFix, phase).contains(AUTO_CHAIN_PRESERVING_FLAG),
802 "audit-fix routes to /gsd-audit-fix and never reaches execute-phase.md, \
803 so it never meets the sync-clear step the token exists to skip"
804 );
805 }
806
807 #[test]
810 fn the_code_prompt_carries_the_chain_flag_token() {
811 let prompt = stage_prompt(Stage::Code, PhaseId::new(11));
812 assert!(prompt.contains(&format!(
813 "/gsd-execute-phase 11 {AUTO_CHAIN_PRESERVING_FLAG}"
814 )));
815 }
816
817 #[test]
826 fn the_plan_prompt_never_carries_the_chain_flag_token() {
827 let plan = stage_prompt(Stage::Plan, PhaseId::new(11));
828 assert!(
829 !plan.contains(AUTO_CHAIN_PRESERVING_FLAG),
830 "the Plan prompt must not chain into execute-phase (D-04)"
831 );
832 assert!(plan.contains("/gsd-plan-phase 11"));
835 }
836
837 #[test]
841 fn checkpoint_auto_decide_prompt_is_deterministic() {
842 assert_eq!(
843 checkpoint_auto_decide_prompt(PhaseId::new(28)),
844 checkpoint_auto_decide_prompt(PhaseId::new(28))
845 );
846 }
847
848 #[test]
849 fn checkpoint_auto_decide_prompt_terminates_with_completion_protocol() {
850 let prompt = checkpoint_auto_decide_prompt(PhaseId::new(28));
851 assert!(
852 prompt.ends_with(COMPLETION_PROTOCOL),
853 "the resumed session's exit must still be parseable by the same \
854 Layer 1 path as any other stage"
855 );
856 assert!(prompt.contains("DEVFLOW_RESULT"));
857 }
858
859 #[test]
860 fn checkpoint_auto_decide_prompt_states_no_operator_judgment_and_record_reasoning() {
861 let prompt = checkpoint_auto_decide_prompt(PhaseId::new(28)).to_lowercase();
862 assert!(
863 prompt.contains("no human operator") || prompt.contains("nobody"),
864 "must state plainly that no operator is available"
865 );
866 assert!(
867 prompt.contains("judgment") || prompt.contains("judgement"),
868 "must instruct the agent to use its own judgment"
869 );
870 assert!(
871 prompt.contains("record") && prompt.contains("reasoning"),
872 "must require recording the reasoning in the final message, since \
873 this is the ONLY record of what was decided (D-07)"
874 );
875 }
876
877 #[test]
878 fn checkpoint_auto_decide_prompt_substitutes_phase_for_legibility() {
879 assert!(checkpoint_auto_decide_prompt(PhaseId::new(42)).contains("phase 42"));
880 }
881}