Skip to content

run_guard.hpp

RunGuard — the run-scoped deadline owner and the guaranteed END-OF-RUN ACTION.

This header declares 4 types (29 members).

Extracted from include/shulib/sequence/run_guard.hpp — this page is that header's documentation, reformatted, so it cannot disagree with the code. Prose about how to think about the API lives in the user guide; worked recipes live in the cookbook; this page is the complete, mechanical list of what exists.

Contents

enum class GuardedWaitResult

enum class GuardedWaitResult

The guard's wait verdict (banner: verdict honesty). DISTINCT from WaitResult on purpose: RunExpired is a fact about the RUN, not the wait, and it must be impossible to read as success.

enum class, declared at include/shulib/sequence/run_guard.hpp:153.

GuardedWaitResult::Satisfied

Satisfied

the predicate became true before any deadline

enumerator, declared at include/shulib/sequence/run_guard.hpp:154.

GuardedWaitResult::TimedOut

TimedOut

the WAIT's own timeout elapsed first (run still live)

enumerator, declared at include/shulib/sequence/run_guard.hpp:155.

GuardedWaitResult::RunExpired

RunExpired

the RUN's deadline passed — stop scoring; wins ties with Satisfied (a satisfied-but-expired wait must still halt the chain — the measured predicate-folding trap)

enumerator, declared at include/shulib/sequence/run_guard.hpp:156.

struct RunGuardConfig

struct RunGuardConfig

One run's schedule + reach. Everything is REQUIRED and caller-supplied: there is deliberately no default here to invent (banner).

struct, declared at include/shulib/sequence/run_guard.hpp:163.

RunGuardConfig::endActionAt

units::Time endActionAt{0.0}

When scoring stops and the end action starts, measured from run() start. The caller computes the lead ("park takes ~6 s") — the library has no number to offer that would not be an invented one.

field, declared at include/shulib/sequence/run_guard.hpp:167.

RunGuardConfig::hardStopAt

units::Time hardStopAt{0.0}

The unconditional safe floor, measured from run() start. At this instant every device is forced safe and everything — the end action included — is refused. Must be >= endActionAt; the gap is the end action's runway (equal instants = zero runway: legal, and the end action's motions will all be refused — supply distinct instants if it must MOVE).

field, declared at include/shulib/sequence/run_guard.hpp:174.

RunGuardConfig::mechanisms

std::span<hal::IMechanism* const> mechanisms{}

Every mechanism the run touches (may be empty). cancel-all reaches operations through the claim's registered claimant (mechanism.hpp); list a mechanism here or the guard cannot see it at the deadline.

field, declared at include/shulib/sequence/run_guard.hpp:178.

RunGuardConfig::validate

void validate() const

Reject a schedule that could not mean anything, before a run arms: both instants finite, endActionAt > 0, hardStopAt >= endActionAt, and no null in mechanisms. run() calls it at the door, so a bad number is a loud error at the call site instead of a deadline that silently never arrives.

function, declared at include/shulib/sequence/run_guard.hpp:184.

struct RunGuardReport

struct RunGuardReport

What one guarded run did — the guard's own account, kept SEPARATE from every motion verdict the caller's code saw (banner: verdict honesty).

struct, declared at include/shulib/sequence/run_guard.hpp:198.

RunGuardReport::scoringCut

bool scoringCut = false

True iff the deadline latched scoring off (false: scoring returned on its own and the end action started early — the caller was done).

field, declared at include/shulib/sequence/run_guard.hpp:201.

RunGuardReport::endActionRan

bool endActionRan = false

the callable was invoked (always, unless a throw unwound run())

field, declared at include/shulib/sequence/run_guard.hpp:202.

RunGuardReport::endActionSucceeded

bool endActionSucceeded = false

its verdict, per the four accepted return types

field, declared at include/shulib/sequence/run_guard.hpp:203.

RunGuardReport::floorFired

bool floorFired = false

hardStopAt arrived during the run

field, declared at include/shulib/sequence/run_guard.hpp:204.

RunGuardReport::postExpiryCancels

int postExpiryCancels = 0

Scheduler cancels the guard performed after the latch (the first is the cut; the rest are refused retries). Zero plant travel either way.

field, declared at include/shulib/sequence/run_guard.hpp:207.

RunGuardReport::anonymousClaimsReleased

int anonymousClaimsReleased = 0

Anonymous claims force-released at cancel-all (should be zero — register claimants).

field, declared at include/shulib/sequence/run_guard.hpp:210.

RunGuardReport::pacesSeen

int pacesSeen = 0

pace() calls observed while the run was live. ZERO after a run whose scoring did real work means the Chassis was NOT constructed with this guard as its pacer — the guard was never in the loop and its guarantee never applied (Warn-logged).

field, declared at include/shulib/sequence/run_guard.hpp:215.

RunGuardReport::scoringEnded

units::Time scoringEnded{0.0}

clock at scoring()'s return, from run start

field, declared at include/shulib/sequence/run_guard.hpp:216.

RunGuardReport::endActionEnded

units::Time endActionEnded{0.0}

clock at the end action's return, from run start

field, declared at include/shulib/sequence/run_guard.hpp:217.

class RunGuard

class RunGuard final : public motion::ITickPacer

The run-scoped deadline owner (file banner). Construct it around the real pacer, give the Chassis the guard AS its pacer, then wrap the whole auton in run(). Inert by construction: until run() is live, pace() is a pure pass-through — zero clock reads, zero behavior change (the D3 §2.1 instruction: a deadline must be opt-in and inert by default; wiring the guard in must not change an existing routine by one tick). motion::ITickPacer& real = ...; // plant pacer / R1's delay sequence::RunGuard guard{real}; chassis::Chassis chassis{deps, guard, cfg}; // the guard IS the pacer ... const sequence::RunGuardReport rep = guard.run(chassis, runCfg, [&] { / scoring: Routine chain, verbs, guard.waitFor(...) / }, [&] { / end action: YOUR pose, YOUR re-verify / return true; }); Not copyable/movable: the Chassis holds a reference to it as the pacer.

class, declared at include/shulib/sequence/run_guard.hpp:236.

RunGuard::RunGuard

explicit RunGuard(motion::ITickPacer& inner) noexcept

inner advances the real world (host: step the plant; robot: delay to the tick boundary) and must outlive the guard.

function, declared at include/shulib/sequence/run_guard.hpp:240.

RunGuard::RunGuard (overload 2)

RunGuard(const RunGuard&) = delete

Pinned where it is constructed: the Chassis holds this object BY REFERENCE as its pacer, so a copy would be paced by nobody and a move would leave the Chassis pacing a corpse. The destructor releases nothing — the guard owns no device and holds only non-owning pointers to the inner pacer and, while a run is live, the chassis's scheduler, clock and telemetry.

function, declared at include/shulib/sequence/run_guard.hpp:247.

RunGuard::RunGuard (overload 3)

RunGuard(RunGuard&&) = delete

Covered by the comment on RunGuard (overload 2) — one comment documents this run of special members.

function, declared at include/shulib/sequence/run_guard.hpp:248.

RunGuard::operator=

RunGuard& operator=(const RunGuard&) = delete

Covered by the comment on RunGuard (overload 2) — one comment documents this run of special members.

function, declared at include/shulib/sequence/run_guard.hpp:249.

RunGuard::operator= (overload 2)

RunGuard& operator=(RunGuard&&) = delete

Covered by the comment on RunGuard (overload 2) — one comment documents this run of special members.

function, declared at include/shulib/sequence/run_guard.hpp:250.

RunGuard::~RunGuard

~RunGuard() override = default

Covered by the comment on RunGuard (overload 2) — one comment documents this run of special members.

function, declared at include/shulib/sequence/run_guard.hpp:251.

RunGuard::pace

void pace() override

The pacer seam (banner: how the deadline reaches running code). The deadline checks run BEFORE the world advances — the ordering is load-bearing (0.0000 in vs 10.79 in of post-deadline travel, measured) and pinned by test. Inert pass-through when no run is live.

function, declared at include/shulib/sequence/run_guard.hpp:257.

RunGuard::expired

[[nodiscard]] bool expired() const

True once the CURRENT phase's deadline has passed: endActionAt during scoring, hardStopAt during the end action. The retry-loop idiom: while (!guard.expired() && ...) { ... } — an unconditional retry loop is the one stall the guard cannot end (banner, honesty section).

function, declared at include/shulib/sequence/run_guard.hpp:277.

RunGuard::remaining

[[nodiscard]] units::Time remaining() const

Time left before the current phase's deadline (never negative). During the end action this counts down to the hard stop — the "hold position until the buzzer" budget.

function, declared at include/shulib/sequence/run_guard.hpp:285.

RunGuard::running

[[nodiscard]] bool running() const noexcept

True only while run() is executing — scoring OR the end action. That window is exactly when expired(), remaining(), waitFor() and pause() may be called at all (outside it they trip a precondition) and exactly when pace() checks deadlines rather than passing straight through. False before the first run and again the moment run() returns: the robot belongs to the caller then.

function, declared at include/shulib/sequence/run_guard.hpp:296.

RunGuard::waitFor

template <typename Pred> [[nodiscard]] GuardedWaitResult waitFor(Pred&& pred, units::Time timeout)

Block until pred holds, the wait's own timeout elapses, or the run's live deadline passes — the return says which, and RunExpired wins a tie with Satisfied (banner: verdict honesty). Implemented over C2's waitUntil with a composite predicate, so every C2 guard (finite timeout, stalled-pace loudness, no blocking verbs in pred) applies unchanged; at the deadline it returns with zero latency and pred is not called again — a scoring predicate that ticks an operation stops being ticked the instant scoring time is over (the latch, applied to waits). The active motion keeps ticking throughout, exactly as C2's wait — until the pace-side latch cuts it.

function, declared at include/shulib/sequence/run_guard.hpp:311.

RunGuard::pause

[[nodiscard]] GuardedWaitResult pause(units::Time duration)

Sleep duration, or less if the run's live deadline arrives first — Satisfied means the full duration was slept, RunExpired means the run cut it short (TimedOut is unreachable: the sleep IS the timeout). The deadline-aware twin of Chassis::wait / Routine::pause, which cannot be cut (banner: T4) — the "wait for the alliance partner, but never past the budget" beat. duration must be finite and > 0.

function, declared at include/shulib/sequence/run_guard.hpp:331.

RunGuard::run

template <typename Scoring, typename EndAction> RunGuardReport run(chassis::Chassis& chassis, const RunGuardConfig& config, Scoring&& scoring, EndAction&& endAction)

Execute one guarded run (file banner carries the whole design): 1. arm — capture the run start from the chassis clock; deadlines become absolute instants; the pacer checks go live; 2. scoring() — your auton, written against the ordinary frozen surface (Routine chains, blocking verbs, guard.waitFor). It ends when it returns — early because it finished, or because the deadline cut its motions/waits and its chain stopped; 3. cancel-all — active motion cancelled, every listed mechanism's claimant cancelled, claims cleared, declared safe states applied. STRICTLY before step 4 (a stalled operation's unreleased claim would make the end action's own operation throw at start()); 4. endAction() — YOUR final act, running in your own call context through the same public verbs, bounded by the hard floor. Return void (always "performed"), bool, ExitReason (Settled = success) or MechanismOutcome (Succeeded = success) — then()'s exact convention. Its verdict lands in the report and the log, never in any motion verdict your scoring code saw; 5. final cancel-all + disarm — the guard hands the robot back safe and goes inert. If scoring() or endAction() THROWS (a precondition — a programming error), the guard cancels-all and safes on the unwind and RETHROWS: a broken program stays loud, and the guard does not drive to a pose on its behalf (converting a throw into a park would hide the bug). chassis MUST be the one constructed with THIS guard as its pacer — the guard has no way to verify that wiring, so it counts: a finished run that saw zero pace() calls Warn-logs that the guarantee never applied (RunGuardReport::pacesSeen).

function, declared at include/shulib/sequence/run_guard.hpp:376.

Design commentary, from the header

The header opens with the reasoning behind these shapes. It is reproduced here in full because a reference that only lists signatures teaches nobody why.

The header’s own reasoning — 128 lines, click to expand
 RunGuard — the run-scoped deadline owner and the guaranteed END-OF-RUN ACTION
 (chunk F2, WS8/M4; master plan §14's non-negotiable, D-8's discharge).

 Before this file, every bound in the tree was scoped to ONE motion, ONE wait,
 or ONE mechanism operation. Nothing bounded a whole routine and nothing knew
 when the match ends: eleven steps at the 5 s default timeout is a legal
 200-second routine inside a 15-second match, behaving exactly as designed.
 RunGuard is the owner that outlives a motion: it holds a run-scoped start
 time, two caller-supplied deadlines, a latch, and one caller-supplied action
 that fires when scoring time is up — D-8 (the routine-level watchdog) and
 the end-of-run action as ONE primitive with two policies, not two features.

 ── What "guaranteed" means here, and what it does not (read before quoting) ────────
 F2 proves a SCHEDULING property, against the host plant: a deliberately
 stalled scoring loop still ends with the caller's end action performed, with
 the clock driven to the match limit. It CANNOT claim the timing margin is
 right on a real brain — real loop rate under load and PROS call latency are
 unmeasured until R4 — and NOTHING PREEMPTS PURE USER CODE: there are no
 background tasks, so code that never lets a finished shulib call end its
 loop (an unconditional retry `while` that ignores guard.expired()) keeps
 control forever and the end action runs only when it returns. The guard
 makes every shulib call after the deadline finish quickly and refuses to let
 new ones make progress; it cannot take the CPU from you. Two lateness holes
 are structural and documented at the waits section below.

 ── The library refuses to know your strategy ───────────────────────────────────────
 No field coordinate, no park pose, no default lead time, no default match
 length lives here or anywhere in shulib. The library knows only that SOME
 caller-supplied action fires at SOME caller-supplied instant: a team ending
 tucked against a goal supplies that pose; a team raising a lift under a
 height limit supplies that; §14's own robots supply a park AND a Toggle
 re-verify (the action is a callable, so it composes). A default lead time
 would be an invented number governing whether the robot scores — HA-51's
 invented 5 s default is the cautionary tale, cited on purpose. Both
 instants are REQUIRED, validated, and relative to run() start.

 ── Two instants, because two different things want to happen (T2) ──────────────────
   * endActionAt — STOP SCORING, early enough to still reach the end
     position: the scoring latch fires, the active motion is cut, mechanisms
     are cancelled into their declared safe states (cancel-all STRICTLY
     precedes act: a stalled operation's unreleased claim would otherwise
     make the end action's own operation throw at start() — measured), and
     the end action runs with the remaining runway.
   * hardStopAt — BE SAFE, unconditionally: every device is forced safe, and
     every MOTION and every WAIT is refused from here on — including the end
     action's own. What is NOT refused is the end action's INVOCATION: run()
     has no floor-fired guard and reports endActionRan = true even when the
     floor already fired during scoring, which is reachable exactly in the
     case this banner's own honesty section describes (scoring code that keeps
     the CPU past both deadlines). The guard cannot preempt caller code, so
     what it can refuse is what the action ASKS FOR, not that it runs; the
     field doc on endActionRan states the same thing. Fires
     even if the end action is still running — safety is not negotiable,
     going somewhere is. Safing is the library's to own once a deadline
     exists at all; where to GO is strategy and stays the caller's.

 ── How the deadline actually reaches running code (T1 — the measured design) ───────
 The ONLY seam that regains control mid-motion is the tick pacer, so RunGuard
 IS an ITickPacer: construct it around the real pacer and hand it to the
 Chassis constructor. Every pace() while a run is live checks the deadlines
 BEFORE advancing the world — that ordering is load-bearing and pinned by
 test: check-then-step measured 0.0000 in of post-deadline travel,
 step-then-check measured 10.79 in. At the cut, the guard calls
 scheduler.cancel() from pace() — legal, and since F2 pinned in C2's
 re-entrancy list — which unwinds waitUntilSettled on the same iteration:
 the blocking verb returns Cancelled (its honest verdict: the motion WAS
 cancelled), a Routine records the stop and skips the rest, and control
 returns to the caller, who is now standing after run()'s scoring call.
 run() then performs cancel-all and the end action — in the caller's own
 call context, through the frozen facade's ordinary blocking verbs, so F2
 adds NO second loop owner and never re-implements C2's loop.
   Rejected: a supervisory scheduler.tick() loop (a second loop owner
 duplicating what the existing waits already do); async()-from-pace() to
 hijack the caller's wait into driving the end action (measured to WORK and
 to LIE — the caller's moveTo returned Settled describing the park, zero log
 lines; the worst outcome measured in the campaign); a THROWING pacer
 (measured: 11.4 V under Coast); cancel-only expiry with no latch (measured
 INERT — cancel/restart resets the deceleration and the run arrives sooner).

 ── The latch (T7): after the deadline, motions are refused, not raced ──────────────
 Once endActionAt passes, any motion outside the end action is cancelled at
 the next pace() — one commanded tick, zero plant travel (the ordering pin
 above), every occurrence counted, the first Warn-logged. A retrying caller
 gets each retry cut the same way and the world does not move. The end
 action is EXEMPT (the guard knows when it is running it); the hard floor
 exempts nothing. After run() returns the guard is inert again — the robot
 belongs to the caller (post-run code, driver control) and a guard that kept
 refusing forever would fight the next mode.

 ── The waits (T4): what is deadline-aware and what CANNOT be ───────────────────────
 waitFor()/pause() here are deadline-aware: they return RunExpired at the
 live deadline with zero latency (implemented as a composite predicate over
 C2's own waitUntil — reusing its guards, not its shape) and RunExpired WINS
 a tie with Satisfied, because a chain that reads "satisfied" keeps scoring
 past the buzzer (measured: a deadline folded into a plain predicate returns
 Satisfied, which Routine::waitFor maps to success). The FROZEN F10/F6 waits
 (Routine::pause / Routine::waitFor / Chassis::wait / Chassis::waitUntil)
 CANNOT be deadline-aware without a breaking change: a scheduler-level wait
 checks its predicate and its own timeout and nothing else — 2801 cancels
 were fired into one and all were invisible. The lateness bound for frozen
 code is: THE UNEXPIRED REMAINDER OF THE WAIT'S OWN TIMEOUT at the instant
 the deadline fires, summed over every wait/pause step executed after that
 instant (a Routine usually pays one term: its first post-deadline motion is
 refused and stops the chain; consecutive pauses each pay). Budget waits
 tightly or use the guard's own.

 ── Reaching the mechanisms (T6) ────────────────────────────────────────────────────
 cancel-all walks the caller-supplied span<hal::IMechanism*>: a registered
 claimant (mechanism.hpp's F2 hook) is cancelled — inert, safe, claim
 released; an ANONYMOUS claim is force-released with a Warn (the guard
 cannot render an unknown operation inert — register a claimant); then
 applySafeState() lands the declared state regardless. applySafeState alone
 is NOT enough and the guard never relies on it alone: a live operation
 re-commands its voltage on its next tick, restoring voltage but not brake
 mode — the half-safe `brake=Hold, V=9.0` that passes any mode-only check.

 ── Verdict honesty (T5) ────────────────────────────────────────────────────────────
 GuardedWaitResult is a sequence-layer vocabulary, minted because no existing
 one can say "the RUN's budget expired" distinctly from "this wait's own
 timeout elapsed" — WaitResult::Satisfied does not mean success and
 RoutineStopCause is Tier-2's. Map in, never re-mean. The end action's
 verdict is reported in the RunGuardReport and logged — never as the
 caller's motion verdict, and never silently.

 FREEZES NOTHING. Single consumer today (F4's student-authored routines are
 hardware-gated; D1 ruled G2 out) — the register says "open by design" out
 loud, and nothing here may freeze on one consumer's evidence.

Last updated 2026-08-15 — published from the main release branch, commit fa545ff. This site tracks releases, so between them it can lag the repository — that lag is deliberate, and it is what keeps the development log off the public site. For the current state of the code, see the repository.