devflow_core/outcome_policy.rs
1//! Pure outcome -> action policy table (D-08/D-11/D-12, 17-01).
2//!
3//! [`decide_action`] is the single exhaustive policy surface `advance()`
4//! (Plan 04) dispatches on. It has no I/O, no `CliError`, no filesystem, and
5//! no process spawn — deterministic pure function of `(Stage, AgentStatus)`.
6//! The `match` has NO wildcard arm: adding a future [`crate::agent_result::AgentStatus`]
7//! variant without extending this match is a compile error, which is the
8//! mechanism that prevents the D-01 regression class (a new/unhandled
9//! outcome silently advancing).
10
11use crate::agent_result::AgentStatus;
12use crate::stage::Stage;
13
14/// The action to take in response to an agent outcome at a given stage.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Action {
17 /// Advance to the next stage.
18 Advance,
19 /// Automatically resume/retry (e.g. rate limit — wait and retry).
20 AutoResume,
21 /// Gate for a human due to an infrastructure-class fault (OOM, agent
22 /// binary unavailable) — not the agent's fault.
23 GateInfra,
24 /// Gate for a human due to a review-worthy outcome (agent-reported
25 /// failure, or an indeterminate/unknown result that must never
26 /// silently advance).
27 GateReview,
28}
29
30/// Decide what to do given the outcome of a stage's agent run.
31///
32/// `stage` is part of the signature for Plan 04's dispatch even though the
33/// current mapping is stage-independent — kept for forward compatibility,
34/// not used in the match itself.
35///
36/// The match is exhaustive over every [`AgentStatus`] variant with NO
37/// wildcard arm — see the module doc comment.
38pub fn decide_action(_stage: Stage, outcome: AgentStatus) -> Action {
39 match outcome {
40 AgentStatus::Success => Action::Advance,
41 AgentStatus::RateLimited => Action::AutoResume,
42 AgentStatus::ResourceKilled => Action::GateInfra,
43 AgentStatus::AgentUnavailable => Action::GateInfra,
44 // DEFERRED (Plan 01 MEDIUM, OpenCode): Failed and Unknown map
45 // identically to GateReview. Intentional — both are non-advance
46 // outcomes today and the current phase needs no behavioral
47 // distinction between them. The distinction is NOT lost:
48 // AgentResult.decided_by_layer plus the underlying AgentStatus
49 // variant both survive into events.jsonl, so Phase 18's 18d
50 // reconciliation can differentiate a reported failure from a
51 // vanished process without a new Action variant. Revisit if 18d
52 // requires divergent routing.
53 AgentStatus::Failed => Action::GateReview,
54 AgentStatus::Unknown => Action::GateReview,
55 }
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61
62 #[test]
63 fn success_advances() {
64 assert_eq!(
65 decide_action(Stage::Code, AgentStatus::Success),
66 Action::Advance
67 );
68 }
69
70 #[test]
71 fn rate_limited_auto_resumes() {
72 assert_eq!(
73 decide_action(Stage::Code, AgentStatus::RateLimited),
74 Action::AutoResume
75 );
76 }
77
78 #[test]
79 fn resource_killed_gates_infra() {
80 assert_eq!(
81 decide_action(Stage::Code, AgentStatus::ResourceKilled),
82 Action::GateInfra
83 );
84 }
85
86 #[test]
87 fn agent_unavailable_gates_infra() {
88 assert_eq!(
89 decide_action(Stage::Code, AgentStatus::AgentUnavailable),
90 Action::GateInfra
91 );
92 }
93
94 #[test]
95 fn failed_gates_review() {
96 assert_eq!(
97 decide_action(Stage::Code, AgentStatus::Failed),
98 Action::GateReview
99 );
100 }
101
102 /// D-01: Unknown must NEVER map to Advance.
103 #[test]
104 fn unknown_gates_review_never_advances() {
105 assert_eq!(
106 decide_action(Stage::Code, AgentStatus::Unknown),
107 Action::GateReview
108 );
109 assert_ne!(
110 decide_action(Stage::Code, AgentStatus::Unknown),
111 Action::Advance
112 );
113 }
114
115 /// Determinism: repeated calls with identical inputs return identical
116 /// results (D-11/D-12 — pure function, no hidden state).
117 #[test]
118 fn decide_action_is_deterministic() {
119 for stage in [
120 Stage::Define,
121 Stage::Plan,
122 Stage::Code,
123 Stage::Validate,
124 Stage::Ship,
125 ] {
126 for outcome in [
127 AgentStatus::Success,
128 AgentStatus::Failed,
129 AgentStatus::RateLimited,
130 AgentStatus::Unknown,
131 AgentStatus::ResourceKilled,
132 AgentStatus::AgentUnavailable,
133 ] {
134 assert_eq!(decide_action(stage, outcome), decide_action(stage, outcome));
135 }
136 }
137 }
138}