Skip to main content

devflow_core/agents/
mod.rs

1//! Agent driver contract and implementations.
2//!
3//! Each driver knows how to render a stage prompt for its agent and wrap it
4//! into the CLI's non-interactive launch command. Prompt RENDERING is
5//! driver-owned ([`AgentDriver::render_prompt`]): Claude/OpenCode render the
6//! legacy slash-command text, Codex renders a Codex-native instruction.
7
8use crate::phase_id::PhaseId;
9use crate::state::AgentKind;
10use std::path::PathBuf;
11
12/// Capabilities a driver declares, enumerated as-needed (999.31 D-01).
13/// `#[non_exhaustive]` + `Default` so adding a field never breaks an existing
14/// driver (CONTEXT D-12).
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16#[non_exhaustive]
17pub struct DriverCapabilities {
18    /// Whether the agent has subagent/dispatch capability available in its
19    /// profile (e.g. Pi's `@bacnh85/pi-subagent` extension). Detected by probing
20    /// the installed CLI; `false` when absent or undetectable (fail-closed to
21    /// the baseline single-agent path).
22    pub subagent_dispatch: bool,
23}
24
25/// What a driver's sandbox needs from the launch environment. Reserved for
26/// 37-03 (Codex's writable-roots requirement).
27#[derive(Debug, Clone, Default)]
28#[non_exhaustive]
29pub struct SandboxRequirements {}
30
31/// One case from a driver's conformance contract (37-04).
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct ContractResult {
34    pub name: &'static str,
35    pub passed: bool,
36}
37
38/// Per-stage interactivity requirement a driver declares (999.31 / 31c),
39/// replacing the hardcoded Codex-Define check.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum InteractivityMode {
42    /// The stage can run headless with no pre-existing artifact or operator.
43    HeadlessSafe,
44    /// The stage needs a pre-existing artifact (e.g. Codex's Define needs a
45    /// CONTEXT.md written ahead of time — it cannot run the interactive
46    /// discuss-phase interview headless).
47    RequiresExistingArtifact,
48    /// The stage needs typed-subagent dispatch (e.g. `multi_agent_v2`).
49    RequiresTypedSubagents,
50    /// The stage cannot run headless at all.
51    InteractiveOnly,
52}
53
54/// A driver's health classification, distinguishing "installed" from
55/// "headless-usable" (999.31 / 31c).
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum DriverHealth {
58    /// The binary is absent — `ensure_agent_binary` fails before health runs.
59    BinaryAbsent,
60    /// Installed but not headless-usable (e.g. no provider credential).
61    NotHeadlessCapable(String),
62    /// Ready to run headless.
63    HeadlessCapable,
64}
65
66/// The modular driver contract (999.31): each agent owns its prompt rendering,
67/// command building, completion parsing, and health/capability discovery —
68/// instead of that logic being scattered across `prompt.rs`, `agents/*.rs`,
69/// `agent_result.rs`, and `preflight.rs`.
70pub trait AgentDriver {
71    /// Human-readable driver name.
72    fn name(&self) -> &'static str;
73
74    /// Capabilities this driver declares (as-needed; default empty).
75    fn capabilities(&self) -> DriverCapabilities {
76        DriverCapabilities::default()
77    }
78
79    /// Render the stage prompt for this agent from a [`crate::prompt::StageIntent`].
80    fn render_prompt(&self, intent: &crate::prompt::StageIntent) -> String;
81
82    /// Build the command and arguments to launch this agent headless.
83    fn build_command(
84        &self,
85        phase: PhaseId,
86        prompt: &str,
87        extra_writable_roots: &[PathBuf],
88    ) -> (&'static str, Vec<String>);
89
90    /// Parse this agent's completion signal out of captured output; `None` when
91    /// the transport is process-exit (no event stream to scan).
92    fn parse_completion(&self, _output: &str) -> Option<crate::agent_result::AgentResult> {
93        None
94    }
95
96    /// Driver-specific pre-launch health check.
97    fn health(&self, _state: &crate::state::State) -> Result<(), String> {
98        Ok(())
99    }
100
101    /// Extra environment variables for the agent process tree.
102    fn environment(&self) -> Vec<(String, String)> {
103        Vec::new()
104    }
105
106    /// Sandbox requirements for this agent's launch.
107    fn sandbox_requirements(&self) -> SandboxRequirements {
108        SandboxRequirements::default()
109    }
110
111    /// Discover capabilities from the installed CLI (e.g. `codex features list`).
112    fn discover(&self) -> Result<(), String> {
113        Ok(())
114    }
115
116    /// The conformance suite every driver must pass (37-04).
117    fn test_contract(&self) -> Vec<ContractResult> {
118        contract_checks(self)
119    }
120
121    /// The interactivity requirement for running `stage` headless.
122    fn interactivity_mode(&self, _stage: crate::stage::Stage) -> InteractivityMode {
123        InteractivityMode::HeadlessSafe
124    }
125
126    /// The directory holding this agent's GSD workflow files, used by the
127    /// workflow-reference renderer. Defaults to the Codex install; a driver
128    /// with a different install (e.g. Pi) overrides it.
129    fn workflow_root(&self) -> String {
130        "$HOME/.codex/gsd-core/workflows".to_string()
131    }
132
133    /// Classify this driver's health (the pass/fail [`AgentDriver::health`]
134    /// mapped onto the richer [`DriverHealth`]).
135    fn health_classification(&self, state: &crate::state::State) -> DriverHealth {
136        match self.health(state) {
137            Ok(()) => DriverHealth::HeadlessCapable,
138            Err(reason) => DriverHealth::NotHeadlessCapable(reason),
139        }
140    }
141}
142
143/// Shared conformance checks every driver's `test_contract` runs (37-04).
144/// A future driver (Antigravity, Hermes) plugs in by passing these — the
145/// extensibility proof CONTEXT D-02 asks for.
146fn contract_checks<D: AgentDriver + ?Sized>(driver: &D) -> Vec<ContractResult> {
147    let mut checks = vec![ContractResult {
148        name: "name is non-empty",
149        passed: !driver.name().is_empty(),
150    }];
151    for stage in [
152        crate::stage::Stage::Define,
153        crate::stage::Stage::Plan,
154        crate::stage::Stage::Code,
155        crate::stage::Stage::Validate,
156        crate::stage::Stage::Ship,
157    ] {
158        let intent = crate::prompt::StageIntent::for_stage(stage, PhaseId::new(1));
159        let prompt = driver.render_prompt(&intent);
160        checks.push(ContractResult {
161            name: "render_prompt states the completion contract",
162            passed: prompt.contains("DEVFLOW_RESULT"),
163        });
164    }
165    let (program, _args) = driver.build_command(PhaseId::new(1), "contract", &[]);
166    checks.push(ContractResult {
167        name: "build_command names a program",
168        passed: !program.is_empty(),
169    });
170    checks
171}
172
173/// Return the driver for a configured agent kind.
174pub fn driver_for(kind: AgentKind) -> Box<dyn AgentDriver> {
175    match kind {
176        AgentKind::Claude => Box::new(ClaudeDriver),
177        AgentKind::Codex => Box::new(CodexDriver),
178        AgentKind::OpenCode => Box::new(OpenCodeDriver),
179        AgentKind::Pi => Box::new(PiDriver),
180        AgentKind::Antigravity => Box::new(AntigravityDriver),
181        AgentKind::Hermes => Box::new(HermesDriver),
182    }
183}
184
185pub mod antigravity;
186pub mod claude;
187pub mod codex;
188pub mod hermes;
189pub mod opencode;
190pub mod pi;
191
192pub use antigravity::AntigravityDriver;
193pub use claude::ClaudeDriver;
194pub use codex::CodexDriver;
195pub use hermes::HermesDriver;
196pub use opencode::OpenCodeDriver;
197pub use pi::PiDriver;
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use crate::prompt::stage_prompt;
203    use crate::stage::Stage;
204
205    #[test]
206    fn driver_for_returns_correct_names() {
207        assert_eq!(driver_for(AgentKind::Claude).name(), "Claude Code");
208        assert_eq!(driver_for(AgentKind::Codex).name(), "OpenAI Codex");
209        assert_eq!(driver_for(AgentKind::OpenCode).name(), "OpenCode");
210        assert_eq!(driver_for(AgentKind::Pi).name(), "Pi");
211        assert_eq!(driver_for(AgentKind::Antigravity).name(), "Antigravity");
212        assert_eq!(driver_for(AgentKind::Hermes).name(), "Hermes");
213    }
214
215    /// 37-02: the drivers reproduce the legacy adapter byte-for-byte (the shim
216    /// delegated to them, so this guards against future drift now that the
217    /// legacy surface is removed).
218    #[test]
219    fn drivers_reproduce_legacy_adapter_behavior() {
220        let intent = crate::prompt::StageIntent::for_stage(Stage::Code, PhaseId::new(7));
221
222        // Claude: stream-json argv + byte-identical legacy prompt.
223        let (program, args) = ClaudeDriver.build_command(PhaseId::new(7), "x", &[]);
224        assert_eq!(program, "claude");
225        assert!(
226            args.windows(2)
227                .any(|w| w[0] == "--input-format" && w[1] == "stream-json")
228        );
229        assert_eq!(
230            ClaudeDriver.render_prompt(&intent),
231            crate::prompt::render_claude_style(&intent)
232        );
233
234        // OpenCode: positional `run <prompt>` + byte-identical legacy prompt.
235        let (program, args) = OpenCodeDriver.build_command(PhaseId::new(7), "x", &[]);
236        assert_eq!(program, "opencode");
237        assert_eq!(args, ["run", "x"]);
238        assert_eq!(
239            OpenCodeDriver.render_prompt(&intent),
240            crate::prompt::render_claude_style(&intent)
241        );
242    }
243
244    /// 37-03: Codex/Pi drivers. Codex carries the verified non-interactive
245    /// approval flag BEFORE `exec`; Pi keeps the Phase-36 `-p --no-approve`
246    /// argv and renders the de-Claude-ified workflow prompt.
247    #[test]
248    fn codex_and_pi_drivers_reproduce_legacy_behavior() {
249        let intent = crate::prompt::StageIntent::for_stage(Stage::Code, PhaseId::new(7));
250
251        let (program, args) = CodexDriver.build_command(PhaseId::new(7), "x", &[]);
252        assert_eq!(program, "codex");
253        assert_eq!(
254            &args[0..2],
255            ["-a", "never"],
256            "the global approval flag must precede `exec` (verified form): {args:?}"
257        );
258        assert!(args.contains(&"exec".to_string()));
259        assert!(
260            CodexDriver
261                .render_prompt(&intent)
262                .contains("execute-phase.md")
263        );
264        assert!(
265            !CodexDriver
266                .render_prompt(&intent)
267                .contains("/gsd-execute-phase")
268        );
269
270        let (program, args) = PiDriver.build_command(PhaseId::new(7), "x", &[]);
271        assert_eq!(program, "pi");
272        assert_eq!(args, ["-p", "--no-approve", "x"]);
273        assert!(PiDriver.render_prompt(&intent).contains("execute-phase.md"));
274        assert!(
275            !PiDriver
276                .render_prompt(&intent)
277                .contains("/gsd-execute-phase")
278        );
279    }
280
281    /// 37-04: every driver passes the shared conformance suite, and Codex
282    /// declares the Define/Plan interactivity requirement that replaces the
283    /// hardcoded Codex-Define check.
284    #[test]
285    fn every_driver_passes_the_conformance_suite() {
286        let drivers: [Box<dyn AgentDriver>; 6] = [
287            Box::new(ClaudeDriver),
288            Box::new(CodexDriver),
289            Box::new(OpenCodeDriver),
290            Box::new(PiDriver),
291            Box::new(AntigravityDriver),
292            Box::new(HermesDriver),
293        ];
294        for driver in &drivers {
295            let results = driver.test_contract();
296            assert!(
297                !results.is_empty(),
298                "{} has no conformance cases",
299                driver.name()
300            );
301            for result in &results {
302                assert!(
303                    result.passed,
304                    "{} failed conformance case {:?}",
305                    driver.name(),
306                    result.name
307                );
308            }
309        }
310    }
311
312    /// F6: the Antigravity enrollment is PROVEN by a uniquely-named test —
313    /// the generic `conformance` filter matched two pre-existing tests and
314    /// would pass with zero Antigravity code, so the enrollment needed a name
315    /// that can only match this one. Asserts the hardcoded array is now 5
316    /// drivers AND that the Antigravity driver passes all 7 contract checks.
317    #[test]
318    fn antigravity_conformance_enrollment() {
319        let drivers: [Box<dyn AgentDriver>; 5] = [
320            Box::new(ClaudeDriver),
321            Box::new(CodexDriver),
322            Box::new(OpenCodeDriver),
323            Box::new(PiDriver),
324            Box::new(AntigravityDriver),
325        ];
326        let antigravity = drivers
327            .iter()
328            .find(|d| d.name() == "Antigravity")
329            .expect("the Antigravity driver must be enrolled in the shared suite");
330        let results = antigravity.test_contract();
331        assert_eq!(
332            results.len(),
333            7,
334            "1 name + 5 per-stage DEVFLOW_RESULT prompts + 1 program"
335        );
336        for result in &results {
337            assert!(
338                result.passed,
339                "Antigravity failed conformance case {:?}",
340                result.name
341            );
342        }
343        // The whole suite still passes with the 5th driver present.
344        for driver in &drivers {
345            assert!(
346                driver.test_contract().iter().all(|r| r.passed),
347                "{} must pass the shared conformance suite",
348                driver.name()
349            );
350        }
351    }
352
353    /// Phase 42 Task 2 (D-06): the Hermes enrollment is PROVEN by a uniquely-named test.
354    /// Asserts the hardcoded array is now 6 drivers AND that the Hermes driver
355    /// passes all 7 contract checks.
356    #[test]
357    fn hermes_conformance_enrollment() {
358        let drivers: [Box<dyn AgentDriver>; 6] = [
359            Box::new(ClaudeDriver),
360            Box::new(CodexDriver),
361            Box::new(OpenCodeDriver),
362            Box::new(PiDriver),
363            Box::new(AntigravityDriver),
364            Box::new(HermesDriver),
365        ];
366        let hermes = drivers
367            .iter()
368            .find(|d| d.name() == "Hermes")
369            .expect("the Hermes driver must be enrolled in the shared suite");
370        let results = hermes.test_contract();
371        assert_eq!(
372            results.len(),
373            7,
374            "1 name + 5 per-stage DEVFLOW_RESULT prompts + 1 program"
375        );
376        for result in &results {
377            assert!(
378                result.passed,
379                "Hermes failed conformance case {:?}",
380                result.name
381            );
382        }
383        // The whole suite still passes with the 6th driver present.
384        for driver in &drivers {
385            assert!(
386                driver.test_contract().iter().all(|r| r.passed),
387                "{} must pass the shared conformance suite",
388                driver.name()
389            );
390        }
391    }
392
393    /// A deliberately-broken driver: empty render + empty program. The suite
394    /// must FAIL it — the negative control proving `test_contract` isn't
395    /// vacuous (code-review finding #7).
396    struct BrokenDriver;
397
398    impl AgentDriver for BrokenDriver {
399        fn name(&self) -> &'static str {
400            "broken"
401        }
402        fn render_prompt(&self, _intent: &crate::prompt::StageIntent) -> String {
403            String::new()
404        }
405        fn build_command(
406            &self,
407            _phase: PhaseId,
408            _prompt: &str,
409            _roots: &[PathBuf],
410        ) -> (&'static str, Vec<String>) {
411            ("", Vec::new())
412        }
413    }
414
415    #[test]
416    fn conformance_suite_fails_a_broken_driver() {
417        let results = BrokenDriver.test_contract();
418        assert!(
419            results.iter().any(|r| !r.passed),
420            "the conformance suite must fail a broken driver (empty render, empty program)"
421        );
422    }
423
424    /// The workflow renderer must preserve the per-stage contracts (code-review
425    /// findings #1-5): Validate verdict, Ship review gate, Define no-op, Plan
426    /// idempotency, and a per-driver workflow root.
427    #[test]
428    fn workflow_render_preserves_stage_contracts() {
429        use crate::prompt::StageIntent;
430        use crate::stage::Stage;
431
432        let codex = CodexDriver;
433
434        // Validate demands the verdict (finding #1).
435        let validate =
436            codex.render_prompt(&StageIntent::for_stage(Stage::Validate, PhaseId::new(7)));
437        assert!(validate.contains("\"verdict\": \"pass\""));
438        assert!(validate.contains("\"verdict\": \"gaps\""));
439
440        // Ship keeps the review gate (finding #2).
441        let ship = codex.render_prompt(&StageIntent::for_stage(Stage::Ship, PhaseId::new(7)));
442        assert!(ship.contains("Critical"));
443        assert!(ship.contains("review:"));
444
445        // Define is the D-14 no-op (finding #3).
446        let define = codex.render_prompt(&StageIntent::for_stage(Stage::Define, PhaseId::new(7)));
447        assert!(define.contains("must NOT run") || define.contains("do NOT run"));
448        assert!(!define.contains("discuss-phase.md"));
449
450        // Plan keeps the idempotency guard (finding #3).
451        let plan = codex.render_prompt(&StageIntent::for_stage(Stage::Plan, PhaseId::new(7)));
452        assert!(plan.contains("already exists"));
453
454        // Pi points at its own workflow root (finding #5).
455        let pi_code = PiDriver.render_prompt(&StageIntent::for_stage(Stage::Code, PhaseId::new(7)));
456        assert!(pi_code.contains("$HOME/.pi/agent/gsd-core/workflows"));
457        assert!(!pi_code.contains("$HOME/.codex/gsd-core"));
458    }
459
460    #[test]
461    fn codex_define_and_plan_require_an_existing_artifact() {
462        assert_eq!(
463            CodexDriver.interactivity_mode(crate::stage::Stage::Define),
464            InteractivityMode::RequiresExistingArtifact
465        );
466        assert_eq!(
467            CodexDriver.interactivity_mode(crate::stage::Stage::Plan),
468            InteractivityMode::RequiresExistingArtifact
469        );
470        assert_eq!(
471            CodexDriver.interactivity_mode(crate::stage::Stage::Code),
472            InteractivityMode::HeadlessSafe
473        );
474        assert_eq!(
475            ClaudeDriver.interactivity_mode(crate::stage::Stage::Define),
476            InteractivityMode::HeadlessSafe
477        );
478    }
479
480    /// The shared-prompt invariant is retired (999.31 / 37-01): Claude and
481    /// OpenCode still render byte-identical legacy text, but Codex now renders
482    /// a Codex-native instruction instead of the shared `/gsd-*` slash command.
483    #[test]
484    fn claude_and_opencode_stay_identical_but_codex_renders_native() {
485        let intent = crate::prompt::StageIntent::for_stage(Stage::Code, PhaseId::new(7));
486        let claude = driver_for(AgentKind::Claude).render_prompt(&intent);
487        let opencode = driver_for(AgentKind::OpenCode).render_prompt(&intent);
488        let codex = driver_for(AgentKind::Codex).render_prompt(&intent);
489
490        // Claude/OpenCode: byte-identical legacy text (zero regression).
491        assert_eq!(
492            claude, opencode,
493            "Claude and OpenCode must stay byte-identical after the migration"
494        );
495        assert_eq!(
496            claude,
497            stage_prompt(Stage::Code, PhaseId::new(7)),
498            "Claude must render the legacy stage_prompt text byte-for-byte (CONTEXT D-01)"
499        );
500
501        // Codex: native, NOT the shared slash-command text (the dogfood fix).
502        assert_ne!(
503            codex, claude,
504            "Codex must no longer render the shared slash-command text"
505        );
506        // Negative control, precise: no GSD slash COMMAND may appear (the
507        // `gsd-core` workflow-directory path is legitimate and must not trip
508        // a naive `/gsd-` substring check).
509        for command in [
510            "/gsd-discuss-phase",
511            "/gsd-plan-phase",
512            "/gsd-execute-phase",
513            "/gsd-validate-phase",
514            "/gsd-ship",
515            "/gsd-code-review",
516            "/gsd-audit-fix",
517        ] {
518            assert!(
519                !codex.contains(command),
520                "Codex render must not carry {command}: {codex}"
521            );
522        }
523        // Positive oracle: the native instruction references the workflow path,
524        // carries the --auto token, and states the completion contract (so an
525        // empty or \"do nothing\" string cannot pass).
526        assert!(codex.contains("execute-phase.md"));
527        assert!(codex.contains("--auto"));
528        assert!(codex.contains("DEVFLOW_RESULT"));
529    }
530
531    /// The Phase 31 launch contract, asserted as one thing because getting
532    /// only the flags right is the documented way to half-implement it: the
533    /// transport is `stream-json` in BOTH directions, and the prompt is not a
534    /// positional argument at all.
535    #[test]
536    fn claude_launches_headless_stream_json_without_positional_prompt() {
537        let prompt = stage_prompt(Stage::Code, PhaseId::new(3));
538        let (program, args) =
539            driver_for(AgentKind::Claude).build_command(PhaseId::new(3), &prompt, &[]);
540        assert_eq!(program, "claude");
541        assert!(args.iter().any(|a| a == "-p"));
542        assert!(
543            args.windows(2)
544                .any(|w| w[0] == "--input-format" && w[1] == "stream-json"),
545            "the INPUT format is what moves the initial turn onto stdin; \
546             flipping only the output format leaves the CLI with no first \
547             turn and it stalls headless: {args:?}"
548        );
549        assert!(
550            args.windows(2)
551                .any(|w| w[0] == "--output-format" && w[1] == "stream-json"),
552            "the OUTPUT format is what makes the capture a JSONL event stream \
553             the Layer 1 stream parser can read: {args:?}"
554        );
555        assert!(args.iter().any(|a| a == "--dangerously-skip-permissions"));
556        assert!(
557            !args.iter().any(|arg| arg.contains("DEVFLOW_RESULT")),
558            "no positional prompt: the initial user turn travels on stdin, \
559             written by the monitor: {args:?}"
560        );
561    }
562
563    #[test]
564    fn codex_wraps_prompt_in_exec_and_json() {
565        let prompt = stage_prompt(Stage::Code, PhaseId::new(7));
566        let (program, args) =
567            driver_for(AgentKind::Codex).build_command(PhaseId::new(7), &prompt, &[]);
568        assert_eq!(program, "codex");
569        let joined = args.join(" ");
570        assert!(joined.contains("exec"));
571        assert!(joined.contains("--sandbox workspace-write"));
572        assert!(joined.contains("--json"));
573    }
574
575    #[test]
576    fn opencode_wraps_prompt_in_run() {
577        let prompt = stage_prompt(Stage::Code, PhaseId::new(7));
578        let (program, args) =
579            driver_for(AgentKind::OpenCode).build_command(PhaseId::new(7), &prompt, &[]);
580        assert_eq!(program, "opencode");
581        assert_eq!(args, ["run", prompt.as_str()]);
582    }
583
584    /// 13-06 dogfood regression (Codex leg): linked-worktree git metadata
585    /// lives under the main repo's `.git/` — outside the workspace-write
586    /// sandbox — and Codex read-only-mounts the cwd's resolved git dir, so
587    /// BOTH the common `.git` and the worktree admin dir must be granted
588    /// (verified with `codex sandbox` probes). Without roots, no override.
589    #[test]
590    fn codex_grants_writable_roots_for_worktree_git_metadata() {
591        let prompt = stage_prompt(Stage::Code, PhaseId::new(7));
592        let roots = vec![
593            PathBuf::from("/repo/.git"),
594            PathBuf::from("/repo/.git/worktrees/phase-07"),
595        ];
596        let (_, args) =
597            driver_for(AgentKind::Codex).build_command(PhaseId::new(7), &prompt, &roots);
598        let joined = args.join(" ");
599        assert!(
600            joined.contains(
601                r#"-c sandbox_workspace_write.writable_roots=["/repo/.git","/repo/.git/worktrees/phase-07"]"#
602            ),
603            "codex must whitelist the common .git AND the worktree admin dir: {joined}"
604        );
605
606        let (_, args) = driver_for(AgentKind::Codex).build_command(PhaseId::new(7), &prompt, &[]);
607        assert!(
608            !args.join(" ").contains("writable_roots"),
609            "no override without an extra root"
610        );
611    }
612
613    /// 13-06 dogfood regression: signed commits fail inside the Codex
614    /// sandbox (no route to the operator's signing agent) — codex scopes an
615    /// unsigned-commit override to its own process tree via GIT_CONFIG_*
616    /// env; agents without a sandbox get no extra env.
617    #[test]
618    fn codex_disables_signing_via_env_others_do_not() {
619        let env = driver_for(AgentKind::Codex).environment();
620        assert!(env.contains(&("GIT_CONFIG_KEY_0".into(), "commit.gpgsign".into())));
621        assert!(env.contains(&("GIT_CONFIG_KEY_1".into(), "tag.gpgsign".into())));
622        assert!(driver_for(AgentKind::Claude).environment().is_empty());
623        assert!(driver_for(AgentKind::OpenCode).environment().is_empty());
624    }
625
626    /// D-13: `preflight`'s default body is `Ok(())` for every built-in
627    /// adapter — none of Claude/Codex/OpenCode override it in Phase 17 (no
628    /// reviewer-set storage exists yet in `state.rs`/`config.rs`, review
629    /// consensus #6).
630    #[test]
631    fn default_preflight_is_ok_for_built_in_adapters() {
632        let state = crate::state::State::new(
633            PhaseId::new(1),
634            AgentKind::Claude,
635            crate::mode::Mode::Auto,
636            PathBuf::from("/repo"),
637        );
638        assert!(driver_for(AgentKind::Claude).health(&state).is_ok());
639        assert!(driver_for(AgentKind::Codex).health(&state).is_ok());
640        assert!(driver_for(AgentKind::OpenCode).health(&state).is_ok());
641    }
642}