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
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
//! 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::phase_validate_failures`] — the total
/// number of Validate failures recorded for one PHASE, accumulated without
/// regard to forward progress (999.78/WR-01, D-07).
///
/// **Why it sits meaningfully above [`MAX_CONSECUTIVE_FAILURES`] (3).** This
/// is a backstop for the case where the streak keeps resetting, not a
/// competing primary bound. `consecutive_failures` is cleared whenever
/// [`consecutive_failures_made_progress`] reports new commits, and the Code
/// stage's fix command is a GSD command that routinely commits `.planning/`
/// artifacts even when no source changed — so "commits something trivial
/// every cycle" is the ORDINARY behaviour of the thing in that slot. A phase
/// in that state never reaches the streak ceiling. Setting this one low
/// enough to compete would make the coarser signal primary and change when
/// ordinary, genuinely-converging runs gate; ten leaves the streak ceiling
/// the first thing a stuck loop meets, and catches only the loops the streak
/// ceiling structurally cannot.
///
/// Exhausting it fires a human gate and the run stays alive (D-07). It must
/// never introduce an abort path: aborting is destructive and irreversible
/// relative to gating, and a phase one cycle from converging would be killed
/// by a bound whose only purpose is to summon a human.
pub const MAX_PHASE_VALIDATE_FAILURES: u32 = 10;
/// 35-04: the phase ceiling must sit strictly above the streak ceiling, or it
/// stops being a backstop and becomes a competing primary bound that changes
/// when ordinary runs gate. A compile-time assertion rather than a `#[test]`
/// for the reason [`MAX_CHECKPOINT_RESUMES`]' own const block gives: both
/// operands are `const`, so a runtime test could never fail at runtime, and
/// clippy's `assertions_on_constants` correctly says so.
const _: = assert!;
/// 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 [`crate::state::State::infra_failures`] and [`crate::state::State::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.
/// Whether the per-phase Validate-failure total has reached
/// [`MAX_PHASE_VALIDATE_FAILURES`] (999.78, F-6).
///
/// **Why this exists as a named predicate rather than an inline comparison.**
/// [`Mode::should_gate`]'s `Stage::Validate` arm returns `true`
/// unconditionally in [`Mode::Supervise`], so in that mode the ceiling
/// condition and the ordinary-gate condition overlap completely and "a gate
/// fired" carries no information about WHY. Two sites need that distinction
/// and cannot get it from `should_gate`'s boolean:
///
/// - the Validate gate message, whose ceiling clause must appear only at the
/// ceiling — keyed on gating instead, it would appear on every Supervise
/// message and mean nothing;
/// - the reset of [`crate::state::State::phase_validate_failures`] on
/// operator approval, which keyed on gating would clear the total at every
/// Supervise failure so it could never accumulate at all — an unbounded
/// loop wearing a gate on every cycle.
///
/// This is the SINGLE implementation of the comparison. No caller may
/// re-derive it: a second copy is exactly the drift hazard that made
/// `should_gate` take the total as a parameter instead of checking it at the
/// call site, and it must not reappear in a new form here.
/// How DevFlow drives the pipeline for a session.
/// Error returned when parsing an unsupported mode name.
;