orchestrator: Leave retry and terminal decisions to the orchestrator

WalkVerdict now reports observation only: Failed{checkpoint, cause}
replaces Retry/Dead/retries_left — the state machine's
ComponentStatus.retry and Recovering→RecoveryFailed path already own
those decisions, and a second counter could only agree or disagree
with the first. max_retries leaves the table for the same reason: a
retry re-resets the device and re-runs the whole walk, so budgets are
per boot attempt, owned where boot attempts are owned. The device's
own judgment still flows up as FailureCause::{TimedOut,
DeviceRetriable, DeviceFatal} — the one input the retry decision
needs.

Assisted-by: Claude:claude-fable-5
Signed-off-by: Christina Quast <christina.quast@9elements.com>
diff --git a/services/orchestrator/capabilities/src/boot_watch.rs b/services/orchestrator/capabilities/src/boot_watch.rs
index 28de726..6c05cc8 100644
--- a/services/orchestrator/capabilities/src/boot_watch.rs
+++ b/services/orchestrator/capabilities/src/boot_watch.rs
@@ -19,9 +19,15 @@
 
 /// Everything the orchestrator needs to know about a boot walk.
 ///
-/// Deliberately free of device and error types: the orchestrator acts the
-/// same whatever the cause, so the concrete detail is logged by the walk
-/// while it is still in scope, not carried across the seam.
+/// Observation only: the walk judges checkpoint windows, never lives.
+/// Retry counts and terminal calls belong to the orchestrator state
+/// machine (`ComponentStatus.retry`, the `Recovering` → `RecoveryFailed`
+/// path) — a verdict that carried a retry budget would be a second owner
+/// for the same decision, free to disagree with the first.
+///
+/// Deliberately free of device and error types: the concrete detail is
+/// logged by the walk while it is still in scope, not carried across the
+/// seam.
 ///
 /// Intentionally exhaustive (not `#[non_exhaustive]`): adding a verdict is
 /// a breaking change, so the compiler forces every consumer — in particular
@@ -35,29 +41,31 @@
     },
     /// Every checkpoint passed — the device is up.
     Complete,
-    /// The attempt failed — a window expired, or the device reported
-    /// [`FailedRetriable`](crate::BootStatus::FailedRetriable) (which ends
-    /// the wait early) — and retry budget remains; the window is re-armed
-    /// from the poll that judged it. The caller re-resets the device and
-    /// keeps polling — what a retry re-runs is the caller's policy.
-    Retry {
-        /// The checkpoint that failed.
+    /// This boot attempt failed at `checkpoint`; the walk is over.
+    /// Whether to try again, recover, or give up is the orchestrator's
+    /// decision — a retry re-resets the device and starts a fresh walk.
+    Failed {
+        /// The checkpoint the attempt died at.
         checkpoint: &'static str,
-        /// Attempts left after this one.
-        retries_left: u8,
-        /// When the re-armed window expires: the judging poll's
-        /// `now_millis` plus the checkpoint's `timeout`. The caller
-        /// schedules against this exactly as it does for `Waiting` —
-        /// no deadline arithmetic of its own.
-        deadline_millis: u64,
+        /// Why it died — the one input the retry decision needs.
+        cause: FailureCause,
     },
-    /// This boot is dead: retry budget exhausted, or the device reported
-    /// [`FailedFatal`](crate::BootStatus::FailedFatal) — a verdict no
-    /// remaining budget can overturn. Recovery is the caller's move.
-    Dead {
-        /// The checkpoint the boot died at.
-        checkpoint: &'static str,
-    },
+}
+
+/// Why a boot attempt failed at a checkpoint.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum FailureCause {
+    /// The window expired; the device reported nothing.
+    TimedOut,
+    /// The device reported a failure worth another attempt
+    /// ([`FailedRetriable`](crate::BootStatus::FailedRetriable)) — the
+    /// wait ended early.
+    DeviceRetriable,
+    /// The device reported a terminal failure
+    /// ([`FailedFatal`](crate::BootStatus::FailedFatal)) — re-running the
+    /// same image cannot change the verdict, whatever retry budget the
+    /// orchestrator has left.
+    DeviceFatal,
 }
 
 #[cfg(test)]
@@ -94,13 +102,13 @@
         };
         let mut nic = ScriptedWalk {
             verdicts: &[
-                WalkVerdict::Retry {
+                WalkVerdict::Failed {
                     checkpoint: "heartbeat",
-                    retries_left: 1,
-                    deadline_millis: 30_000,
+                    cause: FailureCause::TimedOut,
                 },
-                WalkVerdict::Dead {
+                WalkVerdict::Failed {
                     checkpoint: "heartbeat",
+                    cause: FailureCause::DeviceFatal,
                 },
             ],
             next: 0,
@@ -117,10 +125,9 @@
                 WalkVerdict::Waiting {
                     deadline_millis: 90_000
                 },
-                WalkVerdict::Retry {
+                WalkVerdict::Failed {
                     checkpoint: "heartbeat",
-                    retries_left: 1,
-                    deadline_millis: 30_000
+                    cause: FailureCause::TimedOut
                 },
             ]
         );
@@ -128,8 +135,9 @@
             second,
             [
                 WalkVerdict::Complete,
-                WalkVerdict::Dead {
-                    checkpoint: "heartbeat"
+                WalkVerdict::Failed {
+                    checkpoint: "heartbeat",
+                    cause: FailureCause::DeviceFatal
                 },
             ]
         );
diff --git a/services/orchestrator/capabilities/src/lib.rs b/services/orchestrator/capabilities/src/lib.rs
index 8a65574..85f7019 100644
--- a/services/orchestrator/capabilities/src/lib.rs
+++ b/services/orchestrator/capabilities/src/lib.rs
@@ -33,5 +33,5 @@
 
 pub use boot_control::BootControl;
 pub use boot_status::BootStatus;
-pub use boot_watch::{BootWatch, WalkVerdict};
+pub use boot_watch::{BootWatch, FailureCause, WalkVerdict};
 pub use evidence::EvidenceReader;
diff --git a/services/orchestrator/config/src/lib.rs b/services/orchestrator/config/src/lib.rs
index f4144dc..ffc34ab 100644
--- a/services/orchestrator/config/src/lib.rs
+++ b/services/orchestrator/config/src/lib.rs
@@ -7,8 +7,10 @@
 
 #![cfg_attr(not(test), no_std)]
 
-/// One boot checkpoint: a signal the orchestrator waits for, how long it
-/// waits per attempt, and how many failed attempts it tolerates.
+/// One boot checkpoint: a signal the orchestrator waits for, and how long
+/// it waits. Retry policy is deliberately not table data: a retry
+/// re-resets the device and re-runs the whole walk, so budgets are
+/// per boot attempt and owned by the orchestrator state machine.
 ///
 /// `signal` is a board-defined id — the schema attaches no meaning to it
 /// and names no signal kinds. Each board defines its own vocabulary (a
@@ -28,9 +30,6 @@
     /// Window for one attempt at this checkpoint. Expiry is the
     /// orchestrator's own judgment; hung devices report nothing.
     pub timeout: core::time::Duration,
-    /// Attempts allowed beyond the first before the failure is final.
-    /// `0` means the one attempt is all the device gets.
-    pub max_retries: u8,
 }
 
 /// One managed downstream device, as declared by the board config.
@@ -55,7 +54,8 @@
     pub reset_signal: R,
     /// Boot checkpoints, in the order the device passes them. The device
     /// counts as booted when the last one is reached; a checkpoint whose
-    /// window and retry budget are exhausted fails the boot.
+    /// window expires fails the attempt — whether to retry or recover is
+    /// the orchestrator's decision, not table data.
     pub checkpoints: &'static [BootCheckpoint<G>],
 }
 
@@ -133,7 +133,6 @@
         name: "boot-complete",
         signal: 0,
         timeout: Duration::from_secs(1),
-        max_retries: 1,
     };
 
     const DEVICE: DeviceConfig<u8, u8> = DeviceConfig {
diff --git a/target/mock/devices.rs b/target/mock/devices.rs
index a5fa71a..6008e1b 100644
--- a/target/mock/devices.rs
+++ b/target/mock/devices.rs
@@ -39,7 +39,6 @@
             name: "boot-complete",
             signal: MockSignal::Gpio(12),
             timeout: Duration::from_secs(90),
-            max_retries: 1,
         }],
     },
     // PLDM device (NIC archetype): self-updating, SPDM-capable. Two
@@ -53,13 +52,11 @@
                 name: "mctp-ready",
                 signal: MockSignal::MctpReady,
                 timeout: Duration::from_secs(20),
-                max_retries: 2,
             },
             BootCheckpoint {
                 name: "heartbeat",
                 signal: MockSignal::Heartbeat,
                 timeout: Duration::from_secs(10),
-                max_retries: 0,
             },
         ],
     },