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)]
49#[non_exhaustive]
50pub enum FixType {
51 AuditFix,
53 GapsOnly,
55 FullExecute,
61}
62
63fn gsd_command_for(stage: Stage, phase: u32) -> String {
65 stage.gsd_command().replace("{N}", &phase.to_string())
66}
67
68fn ship_stage_prompt(phase: u32, review_angles: &[String]) -> String {
82 let code_review = format!("/gsd-code-review {phase}");
83 let ship = format!("/gsd-ship {phase}");
84 let review_angles = review_angles
85 .iter()
86 .map(|angle| format!("- {angle}"))
87 .collect::<Vec<_>>()
88 .join("\n");
89 format!(
90 "Run the Ship stage in two steps:\n\
91 \n\
92 1. Run `{code_review}` (non-interactive). This writes a `REVIEW.md` \
93 artifact with severity-classified findings. Review at high depth from \
94 every angle below:\n\
95 \n\
96 {review_angles}\n\
97 \n\
98 If your harness supports parallel finder subagents, dispatch one per \
99 angle; otherwise run each angle as a focused sequential pass. Merge \
100 and deduplicate every angle's findings into one `REVIEW.md`.\n\
101 2. Check `REVIEW.md` for the Critical-severity gate:\n\
102 \n\
103 - If `REVIEW.md` contains ANY finding at Critical severity: do NOT \
104 run `{ship}` at all. Your FINAL message must be exactly:\n\
105 \n\
106 DEVFLOW_RESULT: {{\"status\": \"failed\", \"reason\": \"review: <short summary of the Critical findings>\"}}\n\
107 \n\
108 - If `REVIEW.md` has NO Critical-severity findings: run `{ship}` and \
109 report the outcome via the normal completion protocol below.\n\
110 \n\
111 {COMPLETION_PROTOCOL}"
112 )
113}
114
115fn validate_stage_prompt(phase: u32) -> String {
124 let command = gsd_command_for(Stage::Validate, phase);
125 format!(
126 "Run the GSD workflow command for this stage:\n\n {command}\n\n\
127 ## Completion Protocol (REQUIRED)\n\
128 \n\
129 When all work is done, your FINAL message must be exactly one of:\n\
130 \n\
131 DEVFLOW_RESULT: {{\"status\": \"success\", \"verdict\": \"pass\"}}\n\
132 \n\
133 if validation found NO gaps, or:\n\
134 \n\
135 DEVFLOW_RESULT: {{\"status\": \"success\", \"verdict\": \"gaps\"}}\n\
136 \n\
137 if validation found gaps that still need fixing. The `verdict` field \
138 is REQUIRED for this stage — it is distinct from `status` (which only \
139 reports whether the validation task itself completed) and MUST be \
140 exactly the lowercase string `pass` or `gaps`.\n\
141 \n\
142 If something prevents completion:\n\
143 \n\
144 DEVFLOW_RESULT: {{\"status\": \"failed\", \"reason\": \"specific explanation\"}}\n\
145 \n\
146 DevFlow reads this line to decide whether the stage succeeded. \
147 Output nothing after it."
148 )
149}
150
151fn idempotent_stage_prompt(phase: u32) -> String {
169 let artifact = "PLAN.md";
170 let command = gsd_command_for(Stage::Plan, phase);
171 let padded = format!("{phase:02}");
172 format!(
173 "First check whether this stage's deliverable already exists:\n\
174 \n\
175 ls .planning/phases/{padded}-*/{padded}-*{artifact} 2>/dev/null\n\
176 \n\
177 - If it EXISTS: the stage's work is already done. Do NOT run the GSD \
178 command, do NOT ask for input, and do NOT modify the existing \
179 artifacts. Your FINAL message must be exactly:\n\
180 \n\
181 DEVFLOW_RESULT: {{\"status\": \"success\"}}\n\
182 \n\
183 - If it does NOT exist: run the GSD workflow command for this stage:\n\
184 \n\
185 \x20 {command}\n\
186 \n\
187 {COMPLETION_PROTOCOL}"
188 )
189}
190
191fn define_stage_prompt(phase: u32) -> String {
202 format!(
203 "This is the Define stage of a headless DevFlow run for phase {phase}.\n\
204 \n\
205 There is no agent work to perform here. Whether or not this phase's \
206 CONTEXT.md already exists, you must NOT run an interactive \
207 discuss-phase or interview command, and you must NOT ask for input \
208 — this run is headless and no operator is available to answer \
209 interactive questions. Do NOT modify any existing planning \
210 artifacts.\n\
211 \n\
212 {COMPLETION_PROTOCOL}"
213 )
214}
215
216pub fn stage_prompt(stage: Stage, phase: u32) -> String {
218 stage_prompt_with_project(stage, phase, None)
219}
220
221pub fn stage_prompt_for_project(stage: Stage, phase: u32, project_root: &Path) -> String {
227 stage_prompt_with_project(stage, phase, Some(project_root))
228}
229
230fn stage_prompt_with_project(stage: Stage, phase: u32, project_root: Option<&Path>) -> String {
231 if stage == Stage::Ship {
232 let review_angles = project_root
233 .and_then(crate::config::review_angles)
234 .unwrap_or_else(|| {
235 SHIP_REVIEW_ANGLES
236 .iter()
237 .map(|angle| (*angle).to_owned())
238 .collect()
239 });
240 return ship_stage_prompt(phase, &review_angles);
241 }
242 if stage == Stage::Validate {
243 return validate_stage_prompt(phase);
244 }
245 if stage == Stage::Define {
246 return define_stage_prompt(phase);
247 }
248 if stage == Stage::Plan {
249 return idempotent_stage_prompt(phase);
250 }
251 let command = gsd_command_for(stage, phase);
252 if stage == Stage::Code {
253 return format!(
254 "Run the GSD workflow command for this stage:\n\n {command}\n\n\
255 ## Advisory incremental self-review\n\
256 \n\
257 After each plan or wave lands, perform a quick, shallow self-check \
258 for doc accuracy, leaked data, CI/build correctness, and \
259 external-state claims. Record any drift in the working output and \
260 continue execution; the authoritative review happens during Ship. \
261 This check must not pause execution or request human input.\n\
262 \n\
263 {COMPLETION_PROTOCOL}"
264 );
265 }
266 format!(
267 "Run the GSD workflow command for this stage:\n\n {command}\n\n{COMPLETION_PROTOCOL}"
268 )
269}
270
271pub fn checkpoint_auto_decide_prompt(phase: u32) -> String {
284 format!(
285 "This is phase {phase} of a headless DevFlow run. You previously \
286 stopped at a human-blocking checkpoint, but no human operator is \
287 available to answer it — this run is unattended, and none is \
288 coming. DevFlow's policy is for you to resolve the checkpoint \
289 yourself, using your own best judgment, and continue the work. You \
290 MUST record your reasoning for the decision you made in your final \
291 message, so the decision is auditable after the fact.\n\
292 \n\
293 {COMPLETION_PROTOCOL}"
294 )
295}
296
297pub fn fix_prompt(fix_type: FixType, phase: u32) -> String {
299 let command = match fix_type {
300 FixType::AuditFix => format!("/gsd-audit-fix {phase}"),
301 FixType::GapsOnly => format!("/gsd-execute-phase {phase} --gaps-only"),
302 FixType::FullExecute => format!("/gsd-execute-phase {phase}"),
303 };
304 format!(
305 "Validation reported issues. Run the fix command for this loop:\n\n {command}\n\n{COMPLETION_PROTOCOL}"
306 )
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312
313 #[test]
314 fn each_stage_prompt_carries_its_gsd_command_and_marker() {
315 let cases = [
318 (Stage::Plan, "/gsd-plan-phase 11"),
319 (Stage::Code, "/gsd-execute-phase 11"),
320 (Stage::Validate, "/gsd-validate-phase 11"),
321 (Stage::Ship, "/gsd-ship 11"),
322 ];
323 for (stage, command) in cases {
324 let prompt = stage_prompt(stage, 11);
325 assert!(prompt.contains(command), "{stage} prompt missing {command}");
326 assert!(prompt.contains("DEVFLOW_RESULT"));
327 }
328 }
329
330 #[test]
331 fn phase_placeholder_is_substituted() {
332 assert!(stage_prompt(Stage::Code, 7).contains("/gsd-execute-phase 7"));
333 assert!(!stage_prompt(Stage::Code, 7).contains("{N}"));
334 }
335
336 #[test]
337 fn ship_prompt_sequences_code_review_before_ship() {
338 let prompt = stage_prompt(Stage::Ship, 13);
339 let review_pos = prompt
340 .find("/gsd-code-review 13")
341 .expect("Ship prompt must run /gsd-code-review {N}");
342 let ship_pos = prompt
343 .find("/gsd-ship 13")
344 .expect("Ship prompt must run /gsd-ship {N}");
345 assert!(
346 review_pos < ship_pos,
347 "code-review must be sequenced before ship"
348 );
349 }
350
351 #[test]
352 fn ship_prompt_defines_critical_gate_and_review_failed_contract() {
353 let prompt = stage_prompt(Stage::Ship, 13);
354 assert!(
355 prompt.contains("REVIEW.md"),
356 "Ship prompt must reference the REVIEW.md artifact"
357 );
358 assert!(
359 prompt.to_lowercase().contains("critical"),
360 "Ship prompt must name the Critical-severity gate"
361 );
362 assert!(
363 prompt.contains("do not run")
364 || prompt.contains("do NOT run")
365 || prompt.contains("DO NOT run"),
366 "Ship prompt must instruct the agent not to run /gsd-ship on Critical findings"
367 );
368 assert!(
369 prompt.contains("review:"),
370 "Ship prompt must define the review: ReviewFailed reason convention"
371 );
372 assert!(prompt.contains("DEVFLOW_RESULT"));
373 }
374
375 #[test]
376 fn ship_prompt_includes_multi_angle_conditional_review() {
377 let prompt = stage_prompt(Stage::Ship, 13);
378 for angle in [
379 "doc-accuracy cross-reference",
380 "security / leaked-data",
381 "CI/build correctness",
382 "external-state claims",
383 "generalist deep pass",
384 ] {
385 assert!(prompt.contains(angle), "Ship prompt missing angle: {angle}");
386 }
387 assert!(prompt.contains("parallel finder subagents"));
388 assert!(prompt.contains("focused sequential pass"));
389 assert!(prompt.contains("Merge and deduplicate"));
390 assert!(prompt.contains("REVIEW.md"));
391 }
392
393 #[test]
394 fn ship_prompt_uses_project_review_angle_override() {
395 let dir = tempfile::tempdir().unwrap();
396 std::fs::write(
397 dir.path().join("devflow.toml"),
398 "review_angles = [\"custom release evidence\", \"custom threat boundary\"]\n",
399 )
400 .unwrap();
401
402 let prompt = stage_prompt_for_project(Stage::Ship, 13, dir.path());
403
404 assert!(prompt.contains("custom release evidence"));
405 assert!(prompt.contains("custom threat boundary"));
406 assert!(!prompt.contains("doc-accuracy cross-reference"));
407 }
408
409 #[test]
410 fn code_stage_prompt_is_unchanged_single_command_template() {
411 let prompt = stage_prompt(Stage::Code, 9);
419 assert!(prompt.contains("/gsd-execute-phase 9"));
420 assert!(prompt.contains("DEVFLOW_RESULT"));
421 assert!(
422 !prompt.contains("/gsd-code-review"),
423 "Code prompt should not carry Ship-specific code-review sequencing"
424 );
425 assert!(
426 !prompt.contains("already exists"),
427 "Code prompt should not carry the Define/Plan idempotency contract"
428 );
429 assert!(prompt.contains("Advisory incremental self-review"));
430 for angle in [
431 "doc accuracy",
432 "leaked data",
433 "CI/build correctness",
434 "external-state claims",
435 ] {
436 assert!(prompt.contains(angle), "Code prompt missing angle: {angle}");
437 }
438 assert!(!prompt.contains("AskUserQuestion"));
439 assert!(!prompt.contains("request_user_input"));
440 }
441
442 #[test]
447 fn plan_prompt_is_idempotent() {
448 let prompt = stage_prompt(Stage::Plan, 9);
449 assert!(
450 prompt.contains("/gsd-plan-phase 9"),
451 "Plan prompt missing /gsd-plan-phase 9"
452 );
453 assert!(
454 prompt.contains("09-*PLAN.md"),
455 "Plan prompt must check for its pre-existing artifact"
456 );
457 assert!(
458 prompt.contains("Do NOT run the GSD command"),
459 "Plan prompt must no-op when the artifact exists"
460 );
461 assert!(
462 prompt.contains("do NOT ask for input"),
463 "Plan prompt must forbid interactive input"
464 );
465 assert!(prompt.contains("DEVFLOW_RESULT"));
466 }
467
468 #[test]
473 fn define_prompt_never_invokes_discuss_phase() {
474 let prompt = stage_prompt(Stage::Define, 9);
475 assert!(
476 !prompt.contains("/gsd-discuss-phase"),
477 "Define prompt must never invoke the interactive discuss-phase command (D-14)"
478 );
479 assert!(
480 prompt.contains("must NOT run") || prompt.contains("do NOT run"),
481 "Define prompt must forbid running an interactive interview headlessly"
482 );
483 assert!(
484 prompt.contains("do NOT ask for input") || prompt.contains("must NOT ask for input"),
485 "Define prompt must forbid requesting input"
486 );
487 assert!(
488 prompt.to_lowercase().contains("modify"),
489 "Define prompt must forbid modifying existing planning artifacts"
490 );
491 assert!(prompt.contains("DEVFLOW_RESULT"));
492 }
493
494 #[test]
495 fn validate_stage_prompt_requires_verdict() {
496 let prompt = stage_prompt(Stage::Validate, 13);
497 assert!(
498 prompt.contains("/gsd-validate-phase 13"),
499 "Validate prompt missing its GSD command"
500 );
501 assert!(
502 prompt.contains("\"verdict\": \"pass\""),
503 "Validate prompt must name the exact lowercase pass verdict"
504 );
505 assert!(
506 prompt.contains("\"verdict\": \"gaps\""),
507 "Validate prompt must name the exact lowercase gaps verdict"
508 );
509 assert!(prompt.contains("REQUIRED"));
510 assert!(prompt.contains("DEVFLOW_RESULT"));
511 }
512
513 #[test]
514 fn fix_prompts_select_the_right_command() {
515 assert!(fix_prompt(FixType::AuditFix, 11).contains("/gsd-audit-fix 11"));
516 assert!(fix_prompt(FixType::GapsOnly, 11).contains("--gaps-only"));
517 assert!(fix_prompt(FixType::AuditFix, 11).contains("DEVFLOW_RESULT"));
518
519 let full_execute_prompt = fix_prompt(FixType::FullExecute, 11);
521 assert!(full_execute_prompt.contains("/gsd-execute-phase 11"));
522 assert!(!full_execute_prompt.contains("--gaps-only"));
527 }
528
529 #[test]
533 fn checkpoint_auto_decide_prompt_is_deterministic() {
534 assert_eq!(
535 checkpoint_auto_decide_prompt(28),
536 checkpoint_auto_decide_prompt(28)
537 );
538 }
539
540 #[test]
541 fn checkpoint_auto_decide_prompt_terminates_with_completion_protocol() {
542 let prompt = checkpoint_auto_decide_prompt(28);
543 assert!(
544 prompt.ends_with(COMPLETION_PROTOCOL),
545 "the resumed session's exit must still be parseable by the same \
546 Layer 1 path as any other stage"
547 );
548 assert!(prompt.contains("DEVFLOW_RESULT"));
549 }
550
551 #[test]
552 fn checkpoint_auto_decide_prompt_states_no_operator_judgment_and_record_reasoning() {
553 let prompt = checkpoint_auto_decide_prompt(28).to_lowercase();
554 assert!(
555 prompt.contains("no human operator") || prompt.contains("nobody"),
556 "must state plainly that no operator is available"
557 );
558 assert!(
559 prompt.contains("judgment") || prompt.contains("judgement"),
560 "must instruct the agent to use its own judgment"
561 );
562 assert!(
563 prompt.contains("record") && prompt.contains("reasoning"),
564 "must require recording the reasoning in the final message, since \
565 this is the ONLY record of what was decided (D-07)"
566 );
567 }
568
569 #[test]
570 fn checkpoint_auto_decide_prompt_substitutes_phase_for_legibility() {
571 assert!(checkpoint_auto_decide_prompt(42).contains("phase 42"));
572 }
573}