dirge-agent 0.21.2

Minimalistic coding agent written in Rust, optimized for memory footprint and performance
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
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
//! Empirical capability estimation — steer from OBSERVED failure, not model
//! identity (dirge-5mtx.7).
//!
//! dirge ships ~14 behavioural thresholds (repair budgets, storm/streak
//! breakers, safe-state arming, compaction triggers, …) tuned by hand for an
//! unstated assumed model capability. That assumption is wrong often enough to
//! hurt: the tier a model deserves should derive from what the model in front
//! of us is ACTUALLY doing this run, not from its name or its provider.
//!
//! ## The finding that drives the design
//!
//! On a real reconnaissance scenario two models ran the same task:
//!
//! - glm — *reputedly* the stronger model — showed `errored_tool_calls=5`,
//!   `repair_invalid=4`, `max_failure_streak=3`: it was visibly flailing.
//! - deepseek-flash — *reputedly* the weaker model — showed `0 / 0 / 0`: it
//!   was coping perfectly.
//!
//! Reputation inverted reality. A model that is coping must be left alone
//! whichever model it is; a model that is visibly failing must get help sooner.
//! So the tier is estimated from OBSERVED FAILURE IN THIS RUN, never from model
//! identity or provider name.
//!
//! ## Design principles
//!
//! 1. **Observe, don't assume.** [`CapabilityTier`] is a pure function of the
//!    run's own failure counters ([`CapabilityCounters`]), read from the same
//!    observation-only tallies the `gate_tally` module already keeps. No model
//!    name, no provider, no static "fast/slow" flag feeds it.
//!
//! 2. **Rates, not counts.** Four failures mean different things over a 40-call
//!    run than over a 6-call run, so every signal is judged as a ratio over
//!    `tool_calls`. The signals are weighted by how damning they are:
//!    - **Strongest (weight 4)** — `repair_invalid` and `scavenged_calls`: the
//!      model could not produce a dispatchable tool call *at all* — the args
//!      were unrepairable, or it emitted tool-call-shaped text instead of a
//!      real call. The clearest "out of its depth" tell.
//!    - **Medium (weight 2)** — `hallucinated_tool_names` and
//!      `storm_suppressions`: the model tried a native dispatch but to a
//!      non-existent tool, or had to be reined in from rapid-fire repeats.
//!      Wrong, but still inside the dispatch grammar.
//!    - **Weakest (weight 1)** — `errored_tool_calls` and `repair_successful`:
//!      a call ran and failed (often environmental, often transient), or the
//!      model fumbled and then *recovered*. Friction, not stuckness.
//!
//! 3. **Hysteresis.** A single bad observation must not yank a coping run into
//!    [`CapabilityTier::Struggling`], nor must a single clean observation
//!    rescue a flailing one. [`CapabilityEstimator`] requires
//!    [`HYSTERESIS_FLIP_RUNS`] consecutive observations of a *different* tier
//!    before the published tier changes, in both directions.
//!
//! 4. **`Nominal` is a no-op.** [`CapabilityTier::scale`] returns `base`
//!    bit-identically for [`CapabilityTier::Nominal`] — until a run earns a
//!    different tier, every threshold behaves exactly as it did before this
//!    module existed.
//!
//! ## The formula (all integer arithmetic)
//!
//! Let `c_i` be a counter and `w_i` its weight (named constants below). The
//! weighted failure rate, in parts per thousand, is:
//!
//! ```text
//! rate_permille = ( sum of w_i * c_i ) * 1000 / tool_calls
//! ```
//!
//! The raw tier (`CapabilityCounters::raw_tier`) is then:
//!
//! - `tool_calls < MIN_CALLS_FOR_ESTIMATE` → [`CapabilityTier::Nominal`]
//!   (warm-up: too little data to judge).
//! - else `max_failure_streak >= STREAK_FORCE_STRUGGLING` →
//!   [`CapabilityTier::Struggling`] (an unbroken run of failures means stuck,
//!   independent of the overall rate).
//! - else `rate_permille < STRONG_MAX_PER_MILLE` → [`CapabilityTier::Strong`].
//! - else `rate_permille >= STRUGGLING_MIN_PER_MILLE` →
//!   [`CapabilityTier::Struggling`].
//! - otherwise → [`CapabilityTier::Nominal`].
//!
//! [`CapabilityTier::scale_threshold`] and [`CapabilityTier::scale_budget`]
//! map a base value to the tier's working value. Adaptation is
//! **one-directional** — the tier may add support, never remove it:
//!
//! ```text
//!               threshold ("intervene after N")   budget ("at most N")
//!   Nominal     base                              base
//!   Strong      base                              base       <- drives NOTHING
//!   Struggling  base / SUPPORT_SCALE  (sooner)    base * SUPPORT_SCALE (more)
//! ```
//!
//! `Strong` scaling identically to `Nominal` is deliberate and is the single
//! most important thing to understand here. The counters above observe
//! **tool-call mechanics only** — nothing in [`CapabilityCounters`] moves
//! based on whether the model verifies its work, checks the right gate, or
//! makes progress on the task. So a `Strong` reading is evidence about
//! argument hygiene and about nothing else, and it cannot license relaxing a
//! guard that fires on progress or verification. See [`CapabilityTier::Strong`]
//! for the two concrete failures that make the point.
//!
//! Two consequences worth knowing before wiring a new threshold here:
//!
//! - Deriving a guard whose trigger is unrelated to tool-call mechanics buys
//!   nothing, because only `Struggling` moves and `Struggling` is rare.
//! - A budget of exactly 1 cannot move at all, because `1 * 3 / 2` truncates
//!   straight back to 1. Routing a one-shot budget through the estimator
//!   looks like adaptation and changes nothing.
//!
//! Every weight, floor, threshold and ratio is a named constant below — there
//! are no magic numbers in the body. Tuning happens in one place.

// --- weights: how damning each failure signal is (see principle 2 above) ---

/// Weight for `errored_tool_calls`. Weakest: a call ran and failed, often for
/// environmental or transient reasons.
const W_ERRORED: u32 = 1;
/// Weight for `repair_successful`. Weakest: the model fumbled but then
/// recovered — friction, not stuckness.
const W_REPAIR_SUCCESS: u32 = 1;
/// Weight for `hallucinated_tool_names`. Medium: dispatched to a non-existent
/// tool, but still inside the call grammar.
const W_HALLUCINATED: u32 = 2;
/// Weight for `storm_suppressions`. Medium: had to be reined in from
/// rapid-fire repeats.
const W_STORM: u32 = 2;
/// Weight for `repair_invalid`. Strongest: the args were so malformed the
/// repair pass gave up — the model could not produce a dispatchable call.
const W_REPAIR_INVALID: u32 = 4;
/// Weight for `scavenged_calls`. Strongest: the model emitted tool-call-shaped
/// TEXT instead of a real call — never inside the dispatch grammar.
const W_SCAVENGED: u32 = 4;

// --- rate basis and tier boundaries ---

/// Fixed-point basis for the weighted failure rate: parts per thousand.
const PER_MILLE: u64 = 1_000;
/// Warm-up floor. Below this many tool calls there is too little data to judge,
/// so the run stays `Nominal`.
const MIN_CALLS_FOR_ESTIMATE: u32 = 5;
/// An unbroken streak of this many failures forces `Struggling` regardless of
/// the overall rate: a long failure streak means the model is stuck even when
/// the run as a whole looks tolerable.
const STREAK_FORCE_STRUGGLING: u32 = 3;
/// A weighted failure rate strictly below this (per-mille) earns `Strong`.
const STRONG_MAX_PER_MILLE: u64 = 50;
/// A weighted failure rate at or above this (per-mille) earns `Struggling`.
const STRUGGLING_MIN_PER_MILLE: u64 = 333;

// --- hysteresis ---

/// Consecutive observations of a *different* tier required before the
/// published tier flips. 2 means a single differing observation is absorbed.
const HYSTERESIS_FLIP_RUNS: u32 = 2;

// --- scale ratio (num, den); applied to `Struggling` ONLY ---

/// The single adaptation ratio. Applied as `1/2` to a threshold (intervene at
/// half the base count) and as `3/2` to a budget (half again as many nudges
/// allowed) — see [`CapabilityTier::scale_threshold`] and
/// [`CapabilityTier::scale_budget`].
///
/// `Nominal` and `Strong` are BOTH `(1,1)`, i.e. bit-identical to the
/// pre-estimator constants. Adaptation is deliberately **one-directional**:
/// the tier may add support, never remove it. See
/// [`CapabilityTier::Strong`] for why.
const SUPPORT_SCALE_NUM: u64 = 3;
const SUPPORT_SCALE_DEN: u64 = 2;

/// A model's observed capability tier for the current run.
///
/// Derived purely from [`CapabilityCounters`] — never from model identity. See
/// the module docs for how it is computed and what [`CapabilityTier::scale`]
/// does to a threshold.
#[allow(dead_code)] // pending dirge-5mtx.7 loop-control wiring
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CapabilityTier {
    /// Default. Every threshold behaves exactly as it did before this module.
    Nominal,
    /// The model is coping well on tool-call mechanics: no malformed
    /// arguments, no invented tool names, few or no errored calls.
    ///
    /// # This tier deliberately drives NOTHING
    ///
    /// It scales identically to [`Self::Nominal`], and that is not an
    /// oversight. Adaptation here is one-directional — the tier may add
    /// support, never remove it — for a reason that is structural rather than
    /// statistical.
    ///
    /// [`CapabilityCounters`] observes tool-call mechanics *only*: errored
    /// calls, repaired arguments, invented names, scavenged text, storms,
    /// failure streaks. Not one of those fields changes based on whether the
    /// model verifies its work, makes progress on the task, or checks the
    /// right gate. So a `Strong` reading is evidence about **argument
    /// hygiene** and about nothing else, and cannot support relaxing a guard
    /// that fires on progress or verification.
    ///
    /// The epic's own record makes the point concretely. Both failures worth
    /// having a guard for came from models this estimator reads as `Strong`:
    ///
    /// - the 60-turn reconnaissance thrash (dirge-t5dh) was deepseek-flash
    ///   with a 0% tool-call error rate — flawless mechanics, zero files
    ///   written
    /// - the wrong-gate verification failure (dirge-w2de) came from the same
    ///   tier, which is what `docs/verification-discipline.md` is about
    ///
    /// An earlier cut of this work scaled `FAST_VERIFY_EDIT_THRESHOLD` up for
    /// `Strong`, on the argument that extra latitude for a model with no
    /// observed failures could not cause a nudge storm. True, but it inverts
    /// the risk: it relaxes verification pressure on precisely the class of
    /// model that produced the only verification failures on record. Removed.
    ///
    /// Keep observing it — it is the distinction the estimator discriminates
    /// most cleanly (deepseek 6/6 `Strong`, glm 3/6 `Nominal`, tracking
    /// failure exactly), which makes it useful telemetry and useful to the A/B
    /// harness. Just do not let it take a guard away.
    Strong,
    /// The model is visibly failing: thresholds are tightened so the loop
    /// intervenes and offers help sooner.
    ///
    /// # This tier is a SAFETY NET, and not firing is the normal outcome
    ///
    /// Measured across the supported capability range on the `recon-real`
    /// scenario, it never fired once:
    ///
    /// | model                | calls | errored | max streak | rep_invalid | tier    |
    /// |----------------------|-------|---------|------------|-------------|---------|
    /// | deepseek-flash       | ~22   | 0%      | 0          | 0           | strong  |
    /// | glm                  | ~19   | ~4%     | 1          | 1           | nominal |
    /// | Qwen3.6-27B-Q8 local | 20    | 15%     | 2          | 0           | nominal |
    ///
    /// Qwen3.6-27B is the agreed LOW BOUND of supported models — whatever
    /// works with it is good enough generally. So `Nominal` is the bottom of
    /// the supported range, and this tier sits BELOW that range: it is for a
    /// model doing materially worse than the low bound, or for a very
    /// difficult long-horizon task where even a capable model degrades.
    ///
    /// **Do not tune the weights or the streak override to make this fire.**
    /// It is supposed to be quiet in normal operation, the same way
    /// [`super::progress`]'s prologue bound is. Making it fire would start
    /// nudging models that are coping: qwen completed the task at a 15% error
    /// rate, and a run that succeeds is not one to intervene on.
    ///
    /// Equally, do not delete it as dead code. Reaching it requires a run
    /// worse than the low bound, which is exactly the case worth having a net
    /// for, and the observation wiring costs nothing when it stays quiet.
    Struggling,
}

#[allow(dead_code)] // pending dirge-5mtx.7 loop-control wiring
impl CapabilityTier {
    /// Stable lowercase wire name, for the `dirge::gates` telemetry line and
    /// anything scraping it. Kept stable across refactors — the A/B harness
    /// keys on these strings.
    pub fn as_str(self) -> &'static str {
        match self {
            CapabilityTier::Struggling => "struggling",
            CapabilityTier::Nominal => "nominal",
            CapabilityTier::Strong => "strong",
        }
    }

    /// Scale a THRESHOLD — "intervene after N". Lower means sooner.
    ///
    /// `Nominal` and `Strong` both return `base` bit-identically; only
    /// `Struggling` moves it, dividing by `SUPPORT_SCALE` so help arrives
    /// sooner. Integer arithmetic. See [`CapabilityTier::Strong`] for why
    /// `Strong` does not relax anything.
    ///
    /// FLOORED AT 1 for any non-zero base. `base / 2` truncates to 0 at
    /// `base == 1`, and a threshold of 0 fires unconditionally — every
    /// boundary, forever. Several real thresholds are exactly 1, so without
    /// the floor the Struggling tier would turn them into a nudge storm
    /// aimed at the model least able to cope with one. `base == 0` means the
    /// knob is off and stays off.
    pub fn scale_threshold(self, base: u32) -> u32 {
        if base == 0 {
            return 0;
        }
        match self {
            CapabilityTier::Nominal | CapabilityTier::Strong => base,
            CapabilityTier::Struggling => {
                ((base as u64 * SUPPORT_SCALE_DEN / SUPPORT_SCALE_NUM) as u32).max(1)
            }
        }
    }

    /// Scale a BUDGET — "at most N of these per run". Higher means more help.
    ///
    /// This is the OPPOSITE direction from [`Self::scale_threshold`] and the
    /// distinction is load-bearing. A struggling model should be helped
    /// *sooner* (lower threshold) and *more often* (higher budget). Running
    /// both through one scaler would halve the budget of the model that needs
    /// it most — turning `MAX_TRACK_NUDGES`, `MAX_VERIFY_NUDGES` and
    /// `MAX_PROLOGUE_NUDGES`, all of which are exactly 1, into 0 and
    /// disabling those nudges entirely for a failing run.
    ///
    /// `Nominal` and `Strong` are both bit-identical; only `Struggling` moves,
    /// multiplying by `SUPPORT_SCALE` for more nudges. Floored at 1 for a
    /// non-zero base for the same reason as above.
    ///
    /// Note the floor makes this a **no-op for any budget of exactly 1**:
    /// `1 * 3 / 2` truncates back to 1. `MAX_TRACK_NUDGES`,
    /// `MAX_VERIFY_NUDGES` and `MAX_PROLOGUE_NUDGES` are all 1, so routing
    /// them through here would look like adaptation and change nothing. Give
    /// a one-shot budget a larger base before deriving it, or leave it alone.
    pub fn scale_budget(self, base: u32) -> u32 {
        if base == 0 {
            return 0;
        }
        match self {
            CapabilityTier::Nominal | CapabilityTier::Strong => base,
            CapabilityTier::Struggling => {
                ((base as u64 * SUPPORT_SCALE_NUM / SUPPORT_SCALE_DEN) as u32).max(1)
            }
        }
    }
}

/// Accumulated failure signals for one run. The loop fills these from the same
/// observation-only tallies `gate_tally` already keeps; nothing here is
/// model-identity-derived. All fields start at zero via [`Default`].
#[allow(dead_code)] // pending dirge-5mtx.7 loop-control wiring
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct CapabilityCounters {
    /// Total tool calls dispatched this run — the denominator for every rate.
    pub tool_calls: u32,
    /// Calls that ran and returned an error (weight [`W_ERRORED`]).
    pub errored_tool_calls: u32,
    /// Calls whose args were too malformed to repair (weight [`W_REPAIR_INVALID`]).
    pub repair_invalid: u32,
    /// Calls that were malformed but were successfully repaired (weight
    /// [`W_REPAIR_SUCCESS`]).
    pub repair_successful: u32,
    /// Dispatches to a tool name that does not exist (weight [`W_HALLUCINATED`]).
    pub hallucinated_tool_names: u32,
    /// Times a tool-call storm was suppressed (weight [`W_STORM`]).
    pub storm_suppressions: u32,
    /// Tool-call-shaped TEXT scavenged from the assistant message instead of a
    /// real call (weight [`W_SCAVENGED`]).
    pub scavenged_calls: u32,
    /// Longest unbroken run of consecutive failures seen this run.
    pub max_failure_streak: u32,
}

#[allow(dead_code)] // pending dirge-5mtx.7 loop-control wiring
impl CapabilityCounters {
    /// Pure classification of these counters into a raw [`CapabilityTier`], with
    /// no hysteresis. See the module docs for the exact formula.
    fn raw_tier(&self) -> CapabilityTier {
        // Warm-up: too few calls to judge — stay Nominal (and avoid div-by-zero).
        if self.tool_calls < MIN_CALLS_FOR_ESTIMATE {
            return CapabilityTier::Nominal;
        }
        // Stuck override: an unbroken failure streak means the model is bogged
        // down even when the overall rate looks tolerable.
        if self.max_failure_streak >= STREAK_FORCE_STRUGGLING {
            return CapabilityTier::Struggling;
        }
        // Weighted failure rate, in parts per thousand, over tool_calls.
        let weighted = self.errored_tool_calls as u64 * W_ERRORED as u64
            + self.repair_successful as u64 * W_REPAIR_SUCCESS as u64
            + self.hallucinated_tool_names as u64 * W_HALLUCINATED as u64
            + self.storm_suppressions as u64 * W_STORM as u64
            + self.repair_invalid as u64 * W_REPAIR_INVALID as u64
            + self.scavenged_calls as u64 * W_SCAVENGED as u64;
        let rate_permille = weighted * PER_MILLE / self.tool_calls as u64;
        if rate_permille < STRONG_MAX_PER_MILLE {
            CapabilityTier::Strong
        } else if rate_permille >= STRUGGLING_MIN_PER_MILLE {
            CapabilityTier::Struggling
        } else {
            CapabilityTier::Nominal
        }
    }
}

/// Stateful capability estimator. Wraps [`CapabilityCounters::raw_tier`] with
/// hysteresis so the published tier cannot flap on a single observation.
///
/// Feed it a snapshot of the run's counters whenever the loop wants to
/// re-estimate; [`CapabilityEstimator::observe`] returns the (possibly
/// unchanged) published tier.
#[allow(dead_code)] // pending dirge-5mtx.7 loop-control wiring
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CapabilityEstimator {
    /// Currently published tier.
    tier: CapabilityTier,
    /// The differing tier a run is drifting toward, if any.
    candidate: Option<CapabilityTier>,
    /// Consecutive observations matching `candidate`.
    runs: u32,
}

#[allow(dead_code)] // pending dirge-5mtx.7 loop-control wiring
impl CapabilityEstimator {
    /// New estimator, published tier [`CapabilityTier::Nominal`].
    pub fn new() -> Self {
        Self {
            tier: CapabilityTier::Nominal,
            candidate: None,
            runs: 0,
        }
    }

    /// The currently published tier.
    pub fn tier(&self) -> CapabilityTier {
        self.tier
    }

    /// Re-estimate from a snapshot of the run's counters, applying hysteresis.
    ///
    /// Returns the published tier, which changes only after
    /// [`HYSTERESIS_FLIP_RUNS`] consecutive observations of a different raw
    /// tier.
    pub fn observe(&mut self, counters: &CapabilityCounters) -> CapabilityTier {
        let computed = counters.raw_tier();
        if computed == self.tier {
            // Back in line with the published tier: forget any drift.
            self.candidate = None;
            self.runs = 0;
        } else if self.candidate == Some(computed) {
            // Sustained drift in the same direction.
            self.runs += 1;
            if self.runs >= HYSTERESIS_FLIP_RUNS {
                self.tier = computed;
                self.candidate = None;
                self.runs = 0;
            }
        } else {
            // First observation of a new differing tier — don't flip yet.
            self.candidate = Some(computed);
            self.runs = 1;
        }
        self.tier
    }
}

impl Default for CapabilityEstimator {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// A clean run (all signals zero) past the warm-up floor.
    fn clean(calls: u32) -> CapabilityCounters {
        CapabilityCounters {
            tool_calls: calls,
            ..Default::default()
        }
    }

    #[test]
    fn warm_up_returns_nominal_below_floor() {
        // Below MIN_CALLS_FOR_ESTIMATE even a failure-heavy snapshot is Nominal:
        // too little data to judge, and this avoids div-by-zero.
        let c = CapabilityCounters {
            tool_calls: MIN_CALLS_FOR_ESTIMATE - 1,
            errored_tool_calls: 4,
            repair_invalid: 4,
            max_failure_streak: 4,
            ..Default::default()
        };
        assert_eq!(c.raw_tier(), CapabilityTier::Nominal);
    }

    #[test]
    fn clean_stream_reaches_strong() {
        // A clean stream past the floor is Strong — the model is coping.
        assert_eq!(clean(10).raw_tier(), CapabilityTier::Strong);
        assert_ne!(clean(10).raw_tier(), CapabilityTier::Struggling);

        // Through the estimator it stays Nominal during the first observation
        // (hysteresis) and reaches Strong after a second — never Struggling.
        let mut est = CapabilityEstimator::new();
        let snapshot = clean(10);
        est.observe(&snapshot);
        assert_ne!(est.tier(), CapabilityTier::Struggling);
        est.observe(&snapshot);
        assert_eq!(est.tier(), CapabilityTier::Strong);
    }

    #[test]
    fn heavy_repair_and_scavenge_struggles() {
        // repair_invalid and scavenged_calls are the strongest tells; a run
        // heavy in them is Struggling even at a modest absolute count.
        let c = CapabilityCounters {
            tool_calls: 12,
            repair_invalid: 3,
            scavenged_calls: 2,
            ..Default::default()
        };
        assert_eq!(c.raw_tier(), CapabilityTier::Struggling);

        // The same picture through the estimator flips to Struggling after the
        // hysteresis window.
        let mut est = CapabilityEstimator::new();
        est.observe(&c);
        assert_eq!(est.tier(), CapabilityTier::Nominal);
        est.observe(&c);
        assert_eq!(est.tier(), CapabilityTier::Struggling);
    }

    #[test]
    fn hysteresis_blocks_single_observation_flip_both_directions() {
        // --- Strong held against one Struggling blip ---
        let mut est = CapabilityEstimator::new();
        let good = clean(20);
        let bad = CapabilityCounters {
            tool_calls: 6,
            repair_invalid: 4,
            scavenged_calls: 2,
            ..Default::default()
        };
        // Establish Strong.
        est.observe(&good);
        est.observe(&good);
        assert_eq!(est.tier(), CapabilityTier::Strong);
        // A single bad observation must NOT flip it.
        est.observe(&bad);
        assert_eq!(
            est.tier(),
            CapabilityTier::Strong,
            "single bad observation must not flip Strong -> Struggling"
        );
        // Back to clean: still Strong.
        est.observe(&good);
        assert_eq!(est.tier(), CapabilityTier::Strong);

        // --- Struggling held against one clean blip ---
        let mut est = CapabilityEstimator::new();
        est.observe(&bad);
        est.observe(&bad);
        assert_eq!(est.tier(), CapabilityTier::Struggling);
        // A single clean observation must NOT rescue it.
        est.observe(&good);
        assert_eq!(
            est.tier(),
            CapabilityTier::Struggling,
            "single clean observation must not flip Struggling -> Strong"
        );
        est.observe(&bad);
        assert_eq!(est.tier(), CapabilityTier::Struggling);
    }

    /// Adaptation is ONE-DIRECTIONAL: the tier may add support, never remove
    /// it. `Nominal` and `Strong` are both the bit-identical no-op; only
    /// `Struggling` moves a threshold. See `CapabilityTier::Strong` for why —
    /// the counters observe tool-call mechanics only, so a `Strong` reading
    /// cannot justify relaxing a progress or verification guard.
    #[test]
    fn scale_is_identity_for_nominal_and_strong() {
        for base in [0u32, 1, 7, 42, 1_000, u32::MAX] {
            assert_eq!(
                CapabilityTier::Nominal.scale_threshold(base),
                base,
                "Nominal must return base bit-identically"
            );
            assert_eq!(
                CapabilityTier::Strong.scale_threshold(base),
                base,
                "Strong must NOT relax a threshold — it drives nothing"
            );
        }
        // Only Struggling moves, and only toward earlier intervention.
        assert_eq!(CapabilityTier::Struggling.scale_threshold(10), 6);
        assert!(CapabilityTier::Struggling.scale_threshold(10) < 10);
    }

    #[test]
    fn rates_not_counts() {
        // Same absolute failure count (4 errored calls), different denominators.
        // 4 failures over 40 calls is a 10% rate -> Nominal.
        let diluted = CapabilityCounters {
            tool_calls: 40,
            errored_tool_calls: 4,
            ..Default::default()
        };
        // 4 failures over 6 calls is a ~67% rate -> Struggling.
        let concentrated = CapabilityCounters {
            tool_calls: 6,
            errored_tool_calls: 4,
            ..Default::default()
        };
        let diluted_tier = diluted.raw_tier();
        let concentrated_tier = concentrated.raw_tier();
        assert_eq!(diluted_tier, CapabilityTier::Nominal);
        assert_eq!(concentrated_tier, CapabilityTier::Struggling);
        // The diluted run ranks strictly better (less help needed).
        fn rank(t: CapabilityTier) -> u8 {
            match t {
                CapabilityTier::Strong => 0,
                CapabilityTier::Nominal => 1,
                CapabilityTier::Struggling => 2,
            }
        }
        assert!(rank(diluted_tier) < rank(concentrated_tier));
    }

    /// A threshold of 1 must never scale to 0. `base * 1 / 2` truncates to
    /// zero, and a zero threshold fires unconditionally — on EVERY boundary,
    /// at the model least able to absorb a nudge storm. MAX_TRACK_NUDGES,
    /// MAX_VERIFY_NUDGES and MAX_PROLOGUE_NUDGES are all exactly 1, so this
    /// is the common case, not an edge case.
    #[test]
    fn struggling_threshold_never_truncates_to_zero() {
        for base in 1..=4u32 {
            assert!(
                CapabilityTier::Struggling.scale_threshold(base) >= 1,
                "scale_threshold({base}) must not zero out the gate"
            );
        }
        // An explicitly-off knob stays off.
        assert_eq!(CapabilityTier::Struggling.scale_threshold(0), 0);
        assert_eq!(CapabilityTier::Strong.scale_threshold(0), 0);
    }

    /// Budgets scale the OPPOSITE way from thresholds. A struggling model
    /// should be helped sooner (lower threshold) AND more often (higher
    /// budget). Running both through one scaler would halve the budget of the
    /// run that needs it most.
    #[test]
    fn budget_scales_opposite_to_threshold() {
        let base = 4;
        assert!(
            CapabilityTier::Struggling.scale_budget(base) > base,
            "a failing run gets MORE help, not less"
        );
        assert!(
            CapabilityTier::Struggling.scale_threshold(base) < base,
            "...and gets it sooner — the two axes move opposite ways"
        );
        // Nominal and Strong are the bit-identical no-op on both axes.
        for tier in [CapabilityTier::Nominal, CapabilityTier::Strong] {
            assert_eq!(tier.scale_budget(base), base);
            assert_eq!(tier.scale_threshold(base), base);
        }
        // And budgets floor at 1 too.
        for b in 1..=4u32 {
            assert!(CapabilityTier::Struggling.scale_budget(b) >= 1);
        }
    }

    /// A budget of exactly 1 cannot be adapted, because `1 * 3 / 2` truncates
    /// straight back to 1. Several real budgets are 1 (`MAX_TRACK_NUDGES`,
    /// `MAX_VERIFY_NUDGES`, `MAX_PROLOGUE_NUDGES`), so wiring them through
    /// the estimator would look like adaptation and do nothing. Pinned so the
    /// no-op is a documented property rather than a surprise.
    #[test]
    fn a_budget_of_one_is_a_structural_no_op() {
        for tier in [
            CapabilityTier::Struggling,
            CapabilityTier::Nominal,
            CapabilityTier::Strong,
        ] {
            assert_eq!(
                tier.scale_budget(1),
                1,
                "{tier:?} must leave a one-shot budget alone"
            );
        }
    }

    // ---------------------------------------------------------------------
    // Grounding tests: the estimator run against counters ACTUALLY OBSERVED
    // on the recon-real scenario (an extract of this repo's own agent loop),
    // not synthetic streams.
    //
    // These matter because run-to-run variance on that scenario is ~2x on
    // turns and tool calls, so nothing about steering can be validated by
    // comparing means at affordable sample sizes (dirge-5mtx.6, FM-5). What
    // CAN be checked is that the estimator's classification of a run matches
    // what a human reading that run's counters would say. That is a
    // structural claim, and it holds at n=1.
    //
    // The headline observation: on the SAME task, the stronger model (glm)
    // was the one visibly failing while the weaker one (deepseek-flash) was
    // clean. Any estimator keyed on model identity gets this exactly
    // backwards, which is why tier is derived from observed failure only.
    // ---------------------------------------------------------------------

    /// Settle the estimator on a steady stream (hysteresis needs agreement).
    fn settled(counters: &CapabilityCounters) -> CapabilityTier {
        let mut est = CapabilityEstimator::new();
        for _ in 0..3 {
            est.observe(counters);
        }
        est.tier()
    }

    #[test]
    fn observed_glm_thrash_run_reads_as_struggling() {
        // glm, recon-real: turns=36 tools=40 err=5 streak=3 rep_inv=4.
        let obs = CapabilityCounters {
            tool_calls: 40,
            errored_tool_calls: 5,
            repair_invalid: 4,
            max_failure_streak: 3,
            ..Default::default()
        };
        assert_eq!(
            settled(&obs),
            CapabilityTier::Struggling,
            "4 undispatchable calls and a 3-long failure streak is a run in trouble"
        );
    }

    #[test]
    fn observed_deepseek_clean_run_is_not_struggling() {
        // deepseek-flash, recon-real: turns=33 tools=35, zero failures of
        // every kind. The WEAKER model, coping fine on the same task.
        let obs = CapabilityCounters {
            tool_calls: 35,
            ..Default::default()
        };
        assert_ne!(
            settled(&obs),
            CapabilityTier::Struggling,
            "a clean run must never be branded struggling because the model is small"
        );
    }

    /// The pair, stated as one invariant: identical task, and the estimator
    /// must rank them by how the run went rather than by which model it was.
    #[test]
    fn tier_tracks_the_run_not_the_model() {
        let strong_model_bad_run = CapabilityCounters {
            tool_calls: 40,
            errored_tool_calls: 5,
            repair_invalid: 4,
            max_failure_streak: 3,
            ..Default::default()
        };
        let weak_model_good_run = CapabilityCounters {
            tool_calls: 35,
            ..Default::default()
        };
        assert_eq!(settled(&strong_model_bad_run), CapabilityTier::Struggling);
        assert_ne!(settled(&weak_model_good_run), CapabilityTier::Struggling);
    }

    /// glm's OTHER recon-real run — same model, same task, 20 calls with one
    /// error and a 1-long streak. It must not be branded Struggling: the
    /// difference between this and the run above is the run, not the model,
    /// and that is precisely the discrimination being asked for.
    #[test]
    fn same_model_clean_run_ranks_above_its_own_bad_run() {
        let good = CapabilityCounters {
            tool_calls: 20,
            errored_tool_calls: 1,
            repair_invalid: 1,
            max_failure_streak: 1,
            ..Default::default()
        };
        assert_ne!(settled(&good), CapabilityTier::Struggling);
    }
}