Skip to main content

devflow_core/
prompt.rs

1//! Stage-specific agent prompts.
2//!
3//! Prompts are minimal: each stage hands the agent its GSD slash command
4//! (from [`Stage::gsd_command`]) and the `DEVFLOW_RESULT` completion contract.
5//! There is no long instruction template — the GSD command carries the process,
6//! and DevFlow only needs the structured completion marker back.
7
8use 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
20/// The token that stops GSD from wiping `workflow._auto_chain_active` before
21/// it is read (35.1-01, RESEARCH Pitfall 1).
22///
23/// `execute-phase.md:161-165` clears that flag at the top of every invocation
24/// whose `$ARGUMENTS` does not carry this token. DevFlow sets the flag in the
25/// monitor immediately before launching the child, so without this the flag is
26/// wiped by the very command it was set for and `checkpoint_handling` never
27/// auto-approves anything.
28///
29/// **The token alone enables nothing.** GSD's `check auto-mode` reads
30/// `.planning/config.json` and nothing else — a full grep of
31/// `execute-phase.md` for `--auto` / `--chain` / `AUTO_CHAIN` / `auto_advance`
32/// returns exactly three hits, all inside that sync-clear block. So within
33/// `execute-phase.md` this token's only effect is to skip the clear. The
34/// mode gate that decides whether checkpoints may actually be auto-approved
35/// lives on the config write, in `pipeline_launch::auto_chain_flag_eligible`.
36///
37/// Named rather than inlined so the three command strings that must carry it
38/// (Code, and `fix_prompt`'s two `execute-phase` arms) cannot drift apart, and
39/// so a reader of any one of them can find this explanation.
40const AUTO_CHAIN_PRESERVING_FLAG: &str = "--auto";
41
42/// The completion contract every agent must honor as its final message.
43const 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/// A fix variant used when looping Code ↔ Validate.
58///
59/// `#[non_exhaustive]`: operator decision, 2026-08-04. This enum is public in
60/// the published `devflow-core` crate, so adding a variant is already a
61/// breaking change for any external crate matching on it exhaustively — this
62/// release (`FullExecute`, added for D-01) already pays that cost. Paying for
63/// `#[non_exhaustive]` at the same time makes every later variant addition
64/// additive instead of breaking again, the same reasoning `State` records for
65/// its own `#[non_exhaustive]` (`state.rs:30-31`). Verified empirically before
66/// applying: the only `match` over a `FixType` value anywhere in the
67/// workspace is `fix_prompt` below, which lives in this crate and is
68/// therefore unaffected by the attribute — no wildcard arm is needed, here or
69/// anywhere else in the workspace, and none should be added to `fix_prompt`
70/// itself (see its doc comment).
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72#[non_exhaustive]
73pub enum FixType {
74    /// Run the GSD audit-fix pipeline over review findings.
75    AuditFix,
76    /// Re-run execution targeting only the gaps left by validation.
77    GapsOnly,
78    /// Re-run the phase's remaining plans with the plain, unflagged
79    /// `/gsd-execute-phase {N}` command, because the phase is mid-arc rather
80    /// than defective — D-01 (33-CONTEXT.md): a phase with no
81    /// `{N}-VERIFICATION.md` yet has not been judged, so `--gaps-only` would
82    /// match zero plans and gate unresolvably.
83    FullExecute,
84}
85
86/// Substitute the `{N}` phase placeholder in a GSD command string.
87fn gsd_command_for(stage: Stage, phase: PhaseId) -> String {
88    stage.gsd_command().replace("{N}", &phase.to_string())
89}
90
91/// The Ship stage's dedicated prompt.
92///
93/// Headless-safety rationale: `/gsd-ship`'s own `optional_review` step is an
94/// interactive `AskUserQuestion` with undefined behavior under
95/// `--dangerously-skip-permissions` (RESEARCH Pitfall 2). Rather than relying
96/// on that step being skipped, this prompt sidesteps it entirely: the agent
97/// runs `/gsd-code-review {N}` first (non-interactive; writes `REVIEW.md`
98/// with severity-classified findings), and MUST NOT run `/gsd-ship {N}` at
99/// all if `REVIEW.md` contains any Critical-severity finding — instead it
100/// reports a `review:`-prefixed failure. Only a clean (no-Critical) review
101/// proceeds to `/gsd-ship {N}`. The `review:` reason prefix is the
102/// ReviewFailed contract that `handle_ship_failure` matches (trimmed,
103/// case-folded) to loop back to Code with `AuditFix`.
104fn 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
138/// The Validate stage's dedicated prompt.
139///
140/// 13b verdict-vs-ran: `status` only reports whether the stage's task (running
141/// `/gsd-validate-phase {N}`) completed — it says nothing about whether
142/// validation itself passed. This prompt REQUIRES a distinct `verdict` field
143/// so `advance()`'s Validate arm can tell "the agent ran validation" apart
144/// from "validation passed," and never advances to Ship on a bare `status:
145/// success` for this stage.
146fn 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
174/// The Plan stage's idempotency contract.
175///
176/// Headless-safety rationale (13-06 dogfood finding, Codex leg): GSD's
177/// plan-phase demands an interactive "Overwrite/Append/Cancel" decision
178/// when the phase's PLAN.md already exists, and headless Codex cannot
179/// answer it (`request_user_input is unavailable`) — the stage would fail on
180/// every retry, forever. When the stage's deliverable already exists, the
181/// stage's work is done: re-running it must be a no-op success, not an
182/// interactive dead end. This is idempotency for a completed stage, NOT the
183/// v1 skip-stage config flags removed by the 2026-06-19 architecture
184/// decision — a stage with no pre-existing artifact still runs in full.
185///
186/// D-14 update: Define used to share this branch (dispatched by a `stage`
187/// parameter), but its missing-artifact arm ran an interactive interview
188/// command that cannot be answered headlessly. That branch was deleted
189/// rather than made conditional — see [`define_stage_prompt`] for Define's
190/// actual (always-no-op) contract. This function now serves only Plan.
191fn 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
214/// The Define stage's dedicated prompt (D-14).
215///
216/// Headless-safety rationale: `idempotent_stage_prompt`'s missing-artifact
217/// arm used to run an interactive interview command for Define — one that
218/// hangs or errors under `claude -p` with no operator present to answer it
219/// (T-28-08). D-14 settles the fix as deletion, not disambiguation: the
220/// Define stage never runs that command in a DevFlow launch, whether or not
221/// CONTEXT.md already exists. The operator decides whether to run the
222/// interview before invoking `devflow start`; DevFlow makes no runtime
223/// accommodation for that choice.
224fn 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
239/// Build the prompt for a stage of a phase.
240pub fn stage_prompt(stage: Stage, phase: PhaseId) -> String {
241    stage_prompt_with_project(stage, phase, None)
242}
243
244/// Build a stage prompt with project-local configuration applied.
245///
246/// The CLI uses this entry point after resolving the canonical project root;
247/// library callers that have no project context keep using [`stage_prompt`]
248/// and receive built-in defaults.
249pub 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        // The Code arm ONLY (D-04/D-05, 35.1-01 Pitfall 1). `execute-phase.md`
277        // wipes `workflow._auto_chain_active` at the top of every invocation
278        // whose `$ARGUMENTS` lacks this token, which would clear the flag
279        // DevFlow just set before `checkpoint_handling` ever reads it. Within
280        // `execute-phase.md` the token's only effect is to skip that clear — it
281        // does not chain and it sets nothing, so it is safe to send
282        // unconditionally here even though the config write that gives it
283        // meaning is gated on `Mode::Auto` (F-2).
284        //
285        // Deliberately NOT applied to `gsd_command_for` or
286        // `Stage::gsd_command`: both are shared with Plan via
287        // `idempotent_stage_prompt`, and the same flag makes `plan-phase.md`
288        // chain into `execute-phase.md`. Leaking the token into the Plan prompt
289        // is precisely the D-04 defect.
290        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
309/// The synthesized instruction sent into a resumed Claude session when a
310/// confirmed human-blocking checkpoint has nobody available to answer it
311/// (D-03, 28-CONTEXT.md): DevFlow's default, unconditional policy — no flag,
312/// no config toggle — is for the agent to resolve the checkpoint itself,
313/// using its own judgment, and record why.
314///
315/// Deliberately deterministic: no timestamp, no random content, no varying
316/// state. Two calls for the same `phase` produce byte-identical strings, so
317/// the `checkpoint_auto_decided` audit event (D-07, plan 28-03) can quote
318/// this exact instruction without churning on every resume. `phase` is
319/// included only for operator legibility in the captured stdout — the
320/// instruction's meaning does not depend on it.
321pub 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
335/// Build a fix prompt used on Code → Validate loop-backs.
336///
337/// Both arms that dispatch to `execute-phase` carry
338/// [`AUTO_CHAIN_PRESERVING_FLAG`], for the same reason the Code prompt does:
339/// they reach `execute-phase.md`'s sync-clear step, and without the token that
340/// step wipes the chain flag DevFlow just set. The `--gaps-only` loop is
341/// named explicitly by ROADMAP criterion 1 — a fix pass gets exactly the same
342/// treatment as the first Code pass, or the phase's unattended behaviour
343/// changes the moment validation reports a gap.
344///
345/// `AuditFix` is deliberately left alone: it routes to `/gsd-audit-fix`, never
346/// reaches `execute-phase.md`, and so never meets the sync-clear step.
347///
348/// Flag ORDER within the command string does not matter — GSD extracts
349/// `--`-prefixed tokens position-independently
350/// (`references/phase-argument-parsing.md`).
351pub 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        // Define is excluded here (D-14): its prompt never contains its GSD
373        // command — see `define_prompt_never_invokes_discuss_phase` below.
374        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        // Validate is excluded here (Task 2, 13-05): it now gets its own
469        // dedicated prompt requiring a verdict — see
470        // `validate_stage_prompt_requires_verdict` below. Define and Plan
471        // are excluded too: Plan carries the idempotency contract (see
472        // `plan_prompt_is_idempotent` below); Define carries its own D-14
473        // always-no-op contract (see `define_prompt_never_invokes_discuss_phase`
474        // below).
475        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    /// 13-06 dogfood regression (Codex leg), Plan half only after the D-14
500    /// split: GSD's plan-phase demands an interactive decision when PLAN.md
501    /// already exists, which headless Codex can never answer — Plan must
502    /// no-op with success when its deliverable pre-exists. See T-28-09.
503    #[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    /// D-14: the Define stage must never invoke the interactive
526    /// discuss-phase command, whether or not CONTEXT.md exists — the branch
527    /// that did so is deleted, not disambiguated. Regression guard for
528    /// T-28-08 (a headless run has no operator to answer it).
529    #[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        // D-01: FullExecute renders the plain, unflagged execute command.
577        let full_execute_prompt = fix_prompt(FixType::FullExecute, PhaseId::new(11));
578        assert!(full_execute_prompt.contains("/gsd-execute-phase 11"));
579        // Negative control: without this, FullExecute's command string would
580        // just be a substring of GapsOnly's — this proves the two are
581        // actually distinguishable, not that FullExecute merely contains
582        // GapsOnly's prefix.
583        assert!(!full_execute_prompt.contains("--gaps-only"));
584    }
585
586    /// The flag-preserving token belongs on exactly the command strings that
587    /// reach `execute-phase.md`'s sync-clear step, and nowhere else.
588    ///
589    /// All three `FixType` arms are asserted, present AND absent, so this test
590    /// distinguishes "added where it belongs" from "added everywhere" — the
591    /// same habit `fix_prompts_select_the_right_command` above already uses for
592    /// `--gaps-only`.
593    #[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    /// The first Code pass and the fix loop must be treated identically —
614    /// ROADMAP criterion 1 names the fix loop explicitly.
615    #[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    /// Criterion 3a / D-04: the Plan prompt must NEVER carry the token.
624    ///
625    /// The flag that would enable checkpoint auto-approval at Plan is the same
626    /// flag that makes `plan-phase.md` chain into `execute-phase.md`
627    /// (`plan-phase.md:1564`) — which double-executes the Code stage and
628    /// misattributes its commits. This is ROADMAP criterion 3, and it is why
629    /// the token is appended inside the `Stage::Code` arm rather than in
630    /// `gsd_command_for`, which Plan shares.
631    #[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        // Negative control: the Plan prompt DOES carry its own command, so the
639        // assertion above is about the token and not about an empty string.
640        assert!(plan.contains("/gsd-plan-phase 11"));
641    }
642
643    /// D-03/D-07 (28-03): the audit event quotes this instruction verbatim,
644    /// so it must be byte-identical across calls for the same phase — no
645    /// timestamp, no random content that would churn the recorded string.
646    #[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}