use crate::stage::Stage;
const COMPLETION_PROTOCOL: &str = "\
## Completion Protocol (REQUIRED)\n\
\n\
When all work is done, your FINAL message must be exactly:\n\
\n\
DEVFLOW_RESULT: {\"status\": \"success\"}\n\
\n\
If something prevents completion:\n\
\n\
DEVFLOW_RESULT: {\"status\": \"failed\", \"reason\": \"specific explanation\"}\n\
\n\
DevFlow reads this line to decide whether the stage succeeded. \
Output nothing after it.";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FixType {
AuditFix,
GapsOnly,
}
fn gsd_command_for(stage: Stage, phase: u32) -> String {
stage.gsd_command().replace("{N}", &phase.to_string())
}
fn ship_stage_prompt(phase: u32) -> String {
let code_review = format!("/gsd-code-review {phase}");
let ship = format!("/gsd-ship {phase}");
format!(
"Run the Ship stage in two steps:\n\
\n\
1. Run `{code_review}` (non-interactive). This writes a `REVIEW.md` \
artifact with severity-classified findings.\n\
2. Check `REVIEW.md` for the Critical-severity gate:\n\
\n\
- If `REVIEW.md` contains ANY finding at Critical severity: do NOT \
run `{ship}` at all. Your FINAL message must be exactly:\n\
\n\
DEVFLOW_RESULT: {{\"status\": \"failed\", \"reason\": \"review: <short summary of the Critical findings>\"}}\n\
\n\
- If `REVIEW.md` has NO Critical-severity findings: run `{ship}` and \
report the outcome via the normal completion protocol below.\n\
\n\
{COMPLETION_PROTOCOL}"
)
}
fn validate_stage_prompt(phase: u32) -> String {
let command = gsd_command_for(Stage::Validate, phase);
format!(
"Run the GSD workflow command for this stage:\n\n {command}\n\n\
## Completion Protocol (REQUIRED)\n\
\n\
When all work is done, your FINAL message must be exactly one of:\n\
\n\
DEVFLOW_RESULT: {{\"status\": \"success\", \"verdict\": \"pass\"}}\n\
\n\
if validation found NO gaps, or:\n\
\n\
DEVFLOW_RESULT: {{\"status\": \"success\", \"verdict\": \"gaps\"}}\n\
\n\
if validation found gaps that still need fixing. The `verdict` field \
is REQUIRED for this stage — it is distinct from `status` (which only \
reports whether the validation task itself completed) and MUST be \
exactly the lowercase string `pass` or `gaps`.\n\
\n\
If something prevents completion:\n\
\n\
DEVFLOW_RESULT: {{\"status\": \"failed\", \"reason\": \"specific explanation\"}}\n\
\n\
DevFlow reads this line to decide whether the stage succeeded. \
Output nothing after it."
)
}
fn idempotent_stage_prompt(stage: Stage, phase: u32) -> String {
let artifact = match stage {
Stage::Define => "CONTEXT.md",
_ => "PLAN.md",
};
let command = gsd_command_for(stage, phase);
let padded = format!("{phase:02}");
format!(
"First check whether this stage's deliverable already exists:\n\
\n\
ls .planning/phases/{padded}-*/{padded}-*{artifact} 2>/dev/null\n\
\n\
- If it EXISTS: the stage's work is already done. Do NOT run the GSD \
command, do NOT ask for input, and do NOT modify the existing \
artifacts. Your FINAL message must be exactly:\n\
\n\
DEVFLOW_RESULT: {{\"status\": \"success\"}}\n\
\n\
- If it does NOT exist: run the GSD workflow command for this stage:\n\
\n\
\x20 {command}\n\
\n\
{COMPLETION_PROTOCOL}"
)
}
pub fn stage_prompt(stage: Stage, phase: u32) -> String {
if stage == Stage::Ship {
return ship_stage_prompt(phase);
}
if stage == Stage::Validate {
return validate_stage_prompt(phase);
}
if matches!(stage, Stage::Define | Stage::Plan) {
return idempotent_stage_prompt(stage, phase);
}
let command = gsd_command_for(stage, phase);
format!(
"Run the GSD workflow command for this stage:\n\n {command}\n\n{COMPLETION_PROTOCOL}"
)
}
pub fn fix_prompt(fix_type: FixType, phase: u32) -> String {
let command = match fix_type {
FixType::AuditFix => format!("/gsd-audit-fix {phase}"),
FixType::GapsOnly => format!("/gsd-execute-phase {phase} --gaps-only"),
};
format!(
"Validation reported issues. Run the fix command for this loop:\n\n {command}\n\n{COMPLETION_PROTOCOL}"
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn each_stage_prompt_carries_its_gsd_command_and_marker() {
let cases = [
(Stage::Define, "/gsd-discuss-phase 11"),
(Stage::Plan, "/gsd-plan-phase 11"),
(Stage::Code, "/gsd-execute-phase 11"),
(Stage::Validate, "/gsd-validate-phase 11"),
(Stage::Ship, "/gsd-ship 11"),
];
for (stage, command) in cases {
let prompt = stage_prompt(stage, 11);
assert!(prompt.contains(command), "{stage} prompt missing {command}");
assert!(prompt.contains("DEVFLOW_RESULT"));
}
}
#[test]
fn phase_placeholder_is_substituted() {
assert!(stage_prompt(Stage::Code, 7).contains("/gsd-execute-phase 7"));
assert!(!stage_prompt(Stage::Code, 7).contains("{N}"));
}
#[test]
fn ship_prompt_sequences_code_review_before_ship() {
let prompt = stage_prompt(Stage::Ship, 13);
let review_pos = prompt
.find("/gsd-code-review 13")
.expect("Ship prompt must run /gsd-code-review {N}");
let ship_pos = prompt
.find("/gsd-ship 13")
.expect("Ship prompt must run /gsd-ship {N}");
assert!(
review_pos < ship_pos,
"code-review must be sequenced before ship"
);
}
#[test]
fn ship_prompt_defines_critical_gate_and_review_failed_contract() {
let prompt = stage_prompt(Stage::Ship, 13);
assert!(
prompt.contains("REVIEW.md"),
"Ship prompt must reference the REVIEW.md artifact"
);
assert!(
prompt.to_lowercase().contains("critical"),
"Ship prompt must name the Critical-severity gate"
);
assert!(
prompt.contains("do not run")
|| prompt.contains("do NOT run")
|| prompt.contains("DO NOT run"),
"Ship prompt must instruct the agent not to run /gsd-ship on Critical findings"
);
assert!(
prompt.contains("review:"),
"Ship prompt must define the review: ReviewFailed reason convention"
);
assert!(prompt.contains("DEVFLOW_RESULT"));
}
#[test]
fn code_stage_prompt_is_unchanged_single_command_template() {
let prompt = stage_prompt(Stage::Code, 9);
assert!(prompt.contains("/gsd-execute-phase 9"));
assert!(prompt.contains("DEVFLOW_RESULT"));
assert!(
!prompt.contains("/gsd-code-review"),
"Code prompt should not carry Ship-specific code-review sequencing"
);
assert!(
!prompt.contains("already exists"),
"Code prompt should not carry the Define/Plan idempotency contract"
);
}
#[test]
fn define_and_plan_prompts_are_idempotent() {
let cases = [
(Stage::Define, "/gsd-discuss-phase 9", "09-*CONTEXT.md"),
(Stage::Plan, "/gsd-plan-phase 9", "09-*PLAN.md"),
];
for (stage, command, artifact_glob) in cases {
let prompt = stage_prompt(stage, 9);
assert!(prompt.contains(command), "{stage} prompt missing {command}");
assert!(
prompt.contains(artifact_glob),
"{stage} prompt must check for its pre-existing artifact"
);
assert!(
prompt.contains("Do NOT run the GSD command"),
"{stage} prompt must no-op when the artifact exists"
);
assert!(
prompt.contains("do NOT ask for input"),
"{stage} prompt must forbid interactive input"
);
assert!(prompt.contains("DEVFLOW_RESULT"));
}
}
#[test]
fn validate_stage_prompt_requires_verdict() {
let prompt = stage_prompt(Stage::Validate, 13);
assert!(
prompt.contains("/gsd-validate-phase 13"),
"Validate prompt missing its GSD command"
);
assert!(
prompt.contains("\"verdict\": \"pass\""),
"Validate prompt must name the exact lowercase pass verdict"
);
assert!(
prompt.contains("\"verdict\": \"gaps\""),
"Validate prompt must name the exact lowercase gaps verdict"
);
assert!(prompt.contains("REQUIRED"));
assert!(prompt.contains("DEVFLOW_RESULT"));
}
#[test]
fn fix_prompts_select_the_right_command() {
assert!(fix_prompt(FixType::AuditFix, 11).contains("/gsd-audit-fix 11"));
assert!(fix_prompt(FixType::GapsOnly, 11).contains("--gaps-only"));
assert!(fix_prompt(FixType::AuditFix, 11).contains("DEVFLOW_RESULT"));
}
}