1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
//! Execution mode and the mode-driven gate decision.
//!
//! Mode is a per-session CLI flag on `devflow start` — there is no config file
//! and no per-phase toggling.
//!
//! - **Auto:** Define and Plan run once. Code ↔ Validate auto-loop until clean.
//! Then Ship. The only human gate is at Ship — unless Validate fails
//! [`MAX_CONSECUTIVE_FAILURES`] times in a row, which forces a gate.
//! - **Supervise:** Same pipeline, but Validate always fires a gate to Hermes →
//! Human before advancing to Ship.
use crateStage;
use ;
use fmt;
use FromStr;
/// Number of consecutive Validate failures in Auto mode before a gate is forced.
pub const MAX_CONSECUTIVE_FAILURES: u32 = 3;
/// Ceiling for [`crate::state::State::infra_failures`] before an
/// infrastructure-class fault chain (OOM/`ResourceKilled`, missing agent
/// binary/`AgentUnavailable`) forces a terminal gate (D-08, 17-01).
///
/// Deliberately more lenient than [`MAX_CONSECUTIVE_FAILURES`] (3): infra
/// faults are not the agent's fault, so a higher ceiling tolerates transient
/// cloud outages/OOM blips that a 3-ceiling would abort prematurely, while
/// still bounding a stuck loop to at most 5 unobserved cycles before a
/// terminal abort. Any increment of `infra_failures` must use
/// `saturating_add` so a long-running stuck loop cannot overflow `u32`. The
/// CLI's `transition()` resets `infra_failures` to 0 unconditionally on
/// every successful stage transition (CR-01, 17-06 gap closure) — this
/// reset is what makes the "5 unobserved cycles" ceiling bound a stuck loop
/// rather than a phase's entire lifetime. Unlike `infra_failures`,
/// `consecutive_failures`' reset is conditional — see
/// [`transition_resets_consecutive_failures`] — the two counters no longer
/// share a single reset condition (18d, WR-11).
pub const MAX_INFRA_FAILURES: u32 = 5;
/// Ceiling for [`crate::state::State::preflight_retries`] before a
/// preflight gate's `GateAction::LoopBack` recursion aborts rather than
/// polling another 7-day gate timeout (18f, D-18f backstop). A failing
/// preflight is a readiness problem the operator is actively being asked
/// about right now, not a transient infrastructure blip, so this takes the
/// tighter [`MAX_CONSECUTIVE_FAILURES`]-style ceiling rather than the more
/// lenient [`MAX_INFRA_FAILURES`]. Unlike those two counters, this one is
/// NOT reset by `transition()` — it is reset by preflight success and by
/// human approval (`GateAction::Advance`), both inside `run_preflight`
/// (`devflow-cli/src/main.rs`).
pub const MAX_PREFLIGHT_RETRIES: u32 = 3;
/// Ceiling for [`crate::state::State::checkpoint_resumes`] before a
/// checkpoint auto-decide relaunch (D-03/D-04, 28-03) stops resuming and
/// falls through to the never-silent gate instead, its context naming the
/// exhaustion. Bounds consecutive `claude --resume` relaunches for one
/// stage's agent run against a checkpoint that keeps re-firing.
///
/// Takes the tighter [`MAX_CONSECUTIVE_FAILURES`]-style ceiling rather than
/// the more lenient [`MAX_INFRA_FAILURES`]: a re-firing checkpoint is a
/// decision the agent is failing to close on its own, not a transient
/// infrastructure blip, so it does not deserve the same tolerance an OOM
/// blip or a missing binary gets. An unbounded resume loop here would be
/// structurally the same "gates hang forever" failure class D-09
/// (`28-CONTEXT.md`) documents — this ceiling is what keeps it from becoming
/// that.
///
/// Any increment of `checkpoint_resumes` must use `saturating_add`, exactly
/// like [`Self::infra_failures`] and [`Self::preflight_retries`], so a stuck
/// loop cannot overflow `u32`. Reset to 0 by every ORDINARY fresh stage
/// launch (`pipeline_launch::launch_stage_inner`) — never by `transition()`
/// — so the ceiling bounds one stage's resume budget, not a phase's entire
/// lifetime, the same distinction [`MAX_INFRA_FAILURES`]'s doc comment draws
/// for `infra_failures`. On exhaustion: fall through to the never-silent
/// gate with a reason naming the exhaustion — never a silent stop, never an
/// unbounded loop.
pub const MAX_CHECKPOINT_RESUMES: u32 = 3;
/// 28-03 (Task 1): the ceiling must be a small, positive, bounded number —
/// greater than zero (or a checkpoint could never resume even once) and no
/// larger than the more lenient infra ceiling (a re-firing checkpoint gets
/// LESS tolerance than a transient infra blip, not more). A compile-time
/// assertion rather than a runtime `#[test]` because both operands are
/// `const` — clippy's `assertions_on_constants` correctly flags a runtime
/// test here as unable to ever fail at runtime; this const block still
/// fails the BUILD if a future edit violates the invariant.
const _: = assert!;
/// Whether `transition()` should zero
/// [`crate::state::State::consecutive_failures`] when moving from `from` to
/// `to`.
///
/// `consecutive_failures` is meant to count repeated Code↔Validate CYCLES —
/// each cycle is a full loop through Code, then Validate, then (on failure)
/// back to Code again. But the Code→Validate hop is crossed on *every
/// single cycle*, including the ones that are about to fail. Resetting the
/// counter on that specific hop means it can never accumulate past 1, so
/// [`MAX_CONSECUTIVE_FAILURES`] — the ceiling that exists specifically to
/// bound this loop — is unreachable (18d). Every other transition is
/// genuine forward progress out of the Code↔Validate loop (or the initial
/// Define→Plan→Code entry into it) and correctly clears the counter.
///
/// This rule deliberately does NOT apply to
/// [`crate::state::State::infra_failures`], whose unconditional reset in
/// `transition()` is correct for its own semantics: infra faults accumulate
/// within a single stage's repeated failures and are routed through
/// `handle_infra_outcome` → `gate_or_abort_infra` → `handle_stage_failure`,
/// whose retry arms call `launch_stage` directly and never cross
/// `transition()` at all. Widening this predicate's shape onto
/// `infra_failures` would silently convert [`MAX_INFRA_FAILURES`] from a
/// stuck-loop bound into a phase-lifetime bound — the exact regression
/// 17-06 was written to prevent.
/// Whether a Validate failure represents forward progress since the last
/// recorded failure (999.66, D-03) — i.e. whether Code produced new commits
/// on the phase's feature branch since
/// [`crate::state::State::last_validate_failure_commit_count`] was last
/// observed.
///
/// `previous` is the baseline recorded at the prior failure;
/// `current` is the commit count observed at THIS failure.
///
/// `None` for `previous` reports progress: it means no prior failure has
/// been recorded, so there is no streak to continue — the first failure of
/// a phase, and the first failure observed after resuming state written
/// before this baseline field existed, must both begin a fresh streak
/// rather than extend a nonexistent one.
///
/// The comparison is strictly greater, not merely not-equal: a count that
/// went DOWN means the branch was rewound or rebuilt, which is not evidence
/// that the problem Validate reported was addressed. Treating a decrease as
/// progress would hand a free counter reset to exactly the situation least
/// likely to deserve one.
///
/// **What this predicate does not establish.** A `true` result means new
/// commits exist, not that those commits addressed anything. An agent that
/// commits something trivial on every cycle resets the streak every cycle
/// and never reaches [`MAX_CONSECUTIVE_FAILURES`]. This is the accepted,
/// documented weakness of the commit-count signal recorded in
/// `33-RESEARCH.md`'s D-03 Recommendation and Assumptions Log A1 — the same
/// weakness `evaluate_layer2`'s own "no work done" gate already carries,
/// which a single trivial commit also already defeats today. It is a real
/// narrowing of the guarantee that `MAX_CONSECUTIVE_FAILURES` bounds a
/// genuinely stuck loop, and it is deliberately NOT strengthened here with a
/// lines-changed or files-touched threshold — that is a follow-up if the
/// assumption proves wrong, not a speculative heuristic to add to the
/// safety gate's path now.
/// How DevFlow drives the pipeline for a session.
/// Error returned when parsing an unsupported mode name.
;