diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 4357ce49ff..5dc9fffa4e 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -50,9 +50,16 @@ jobs: # touches is distributed, so the AGPL/dual-license reasoning in the # header does not apply to it. # - # The @img/sharp-* tuples that used to sit here arrived through - # arenabench/web's lockfile; that folder left with the ejection to - # its own repository (#2380), so the exemption left with it. + # The @img/sharp-* tuples first arrived through arenabench/web's + # lockfile and left with its ejection (#2380). They are back through + # `website/`, the docs site: `next` lists sharp as an optional + # dependency for every platform, and fourteen of its tuples ship + # LGPL-3.0 libvips binaries. The site is `"private": true`, imports + # no `next/image` (so sharp is never loaded, even at build), and is + # deployed to Vercel — nothing here reaches the AGPL workspace or the + # commercial binary. The action has no purl globs, so each tuple is + # named; a new libvips tuple reds this gate again and is added the + # same way. #2532 tracks the recurrence. # # Must stay ABOVE `allow-licenses`: check-license-allowlist-parity.sh # reads that key's folded scalar by consuming every following indented @@ -66,7 +73,21 @@ jobs: # nothing — so a NEW libvips tuple reds this gate again and has to be # added by hand. #2532 tracks removing the recurrence instead. allow-dependencies-licenses: >- - pkg:githubactions/Swatinem/rust-cache + pkg:githubactions/Swatinem/rust-cache, + pkg:npm/%40img/sharp-libvips-darwin-arm64, + pkg:npm/%40img/sharp-libvips-darwin-x64, + pkg:npm/%40img/sharp-libvips-linux-arm, + pkg:npm/%40img/sharp-libvips-linux-arm64, + pkg:npm/%40img/sharp-libvips-linux-ppc64, + pkg:npm/%40img/sharp-libvips-linux-riscv64, + pkg:npm/%40img/sharp-libvips-linux-s390x, + pkg:npm/%40img/sharp-libvips-linux-x64, + pkg:npm/%40img/sharp-libvips-linuxmusl-arm64, + pkg:npm/%40img/sharp-libvips-linuxmusl-x64, + pkg:npm/%40img/sharp-wasm32, + pkg:npm/%40img/sharp-win32-arm64, + pkg:npm/%40img/sharp-win32-ia32, + pkg:npm/%40img/sharp-win32-x64 allow-licenses: >- AGPL-3.0-only, Apache-2.0, Apache-2.0 WITH LLVM-exception, MIT, BSD-2-Clause, BSD-3-Clause, ISC, Zlib, BSL-1.0, MPL-2.0, Unlicense, diff --git a/AGENTS.md b/AGENTS.md index 18e2b566fa..7e7397736d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1341,8 +1341,9 @@ empty and is meant to stay empty. ## Workspace layout — where a change goes -Twenty-nine crates, every one under the `crates/` directory (`crates/stella-core`, -`crates/stella-cli`, …; the two bench members stay under `bench/`). The +Every crate lives under the `crates/` directory (`crates/stella-core`, +`crates/stella-cli`, …; the two bench members stay under `bench/`); the +`members` list in the root `Cargo.toml` is the roll. The one-sentence rule of thumb below routes you to the right one; **each crate's own `README.md`** (linked from the table) then covers its boundary, layout, invariants, gotchas, and extension recipe in depth. Read that before changing @@ -1365,6 +1366,7 @@ the files you must plan around (see below). | Change the wrapper socket's **trait** — `TurnWrapper`, `admissible`, `judge`/`again`, the in-process/subprocess transports | [`stella-runtime`](crates/stella-runtime/README.md) | `src/wrapper/` (#3380, landed #3479, `doc:wrapper-socket`). Lives one layer above `stella-core` because `before_turn`/`after_turn` do I/O, which invariant 2 bans in the engine; consumes `stella-plugin`'s wire types rather than redefining them. It is reached four ways now: a `--pipeline` run, a goal round, a fleet attempt, and an auto-resolved plugin — see the crate's own README. | | Change how a plugin is **installed, listed, removed, or trusted** — `.stella/plugins/` and `~/.stella/plugins/` resolution, install consent, the project-tier trust gate | [`stella-cli`](crates/stella-cli/README.md) | `src/plugin_cmd.rs` + `src/plugin_cmd/{roster,process}.rs` — renders `stella_plugin::consent_text` before anything executes, gates a cloned repository's plugins on `project_code_execution_trusted()` (#3509), and is the one place `LoopGrant::permits_hook`/`permits_point` get consulted against an installed manifest. | | Decide whether a human is present to see/answer a mid-run prompt | [`stella-tty`](crates/stella-tty/README.md) | **A leaf with NO dependencies at all** (#3036) — one pure `human_can_answer(interactive_output, stdin_is_terminal, prompt_is_visible)`, which is what lets `stella-cli`'s approval prompts and `stella-model`'s credential prompt share one derivation without `stella-model` depending on `stella-cli` (invariant 1). | +| Read the real clock, wait on the real timer, or stand in for either under test, behind `stella-core`'s `Sleeper` and `Clock` ports | [`stella-time`](crates/stella-time/README.md) | **Near-leaf: `stella-core` is its only workspace dependency.** `TokioSleeper` (the engine's real sleeper and `now`), `WallClock` (Unix epoch, for a stamp another process reads) and `MonotonicClock` (one origin per process, for a span compared as a number), plus `test_util`'s `PausedSleeper` and `NoopSleeper` behind the `test-util` feature. One home, because `stella-serve` may not link `stella-cli` or `stella-runtime` and `stella-core` may not carry a timer (ADR 0042). | | Emit a diagnostic — a record explaining *why* the program did something | [`stella-diag`](crates/stella-diag/README.md) | **A leaf: `serde` only, so anything may depend on it.** Field values cannot hold a `String`, a `Path`, or model output — that is a compile error, not a review question. Design: [`docs/spec/diagnostics.md`](docs/spec/diagnostics.md). | | Compute a line-oriented unified diff (`@@` hunks, git's exact shape) | [`stella-diff`](crates/stella-diff/README.md) | **A leaf with NO dependencies at all** (#1511) — pure functions over borrowed strings, which is what lets [`stella-observatory`](crates/stella-observatory/README.md) and [`stella-cli`](crates/stella-cli/README.md) share one differ without costing the observatory its isolation. | | Strip ANSI escape sequences from tool output | [`stella-ansi`](crates/stella-ansi/README.md) | **A leaf with NO dependencies at all** — one pure function over a borrowed `&str`, extracted from `stella-tui` so [`stella-observatory`](crates/stella-observatory/README.md) could strip a colourised tool's output before it reaches the journal route's `
` without linking `ratatui`. `stella-tui`'s `ansi` module re-exports it and keeps only the `ratatui`-shaped emission half. |
@@ -1492,7 +1494,7 @@ a plan needs and the part that rarely changes:
 | `stella-store` | `src/tests.rs`, `src/lib.rs`, `src/usage.rs` |
 | `stella-tui` | `src/deck_ui.rs` |
 
-The other twenty-four crates carry no god files — keep it that way. Each crate's
+Every crate not named there carries no god files — keep it that way. Each crate's
 README repeats its own list under "God files — do not add lines", so the
 constraint is in view wherever planning starts.
 
diff --git a/Cargo.lock b/Cargo.lock
index 87c96cf4cf..c57bb0126b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3240,6 +3240,7 @@ dependencies = [
  "stella-records",
  "stella-runtime",
  "stella-store",
+ "stella-time",
  "stella-tools",
  "stella-transcript",
  "stella-tty",
@@ -3345,6 +3346,7 @@ dependencies = [
  "stella-core",
  "stella-protocol",
  "stella-store",
+ "stella-time",
  "stella-tools",
  "tempfile",
  "thiserror 2.0.20",
@@ -3529,6 +3531,7 @@ dependencies = [
  "stella-plugin",
  "stella-protocol",
  "stella-store",
+ "stella-time",
  "stella-tools",
  "tempfile",
  "thiserror 2.0.20",
@@ -3548,6 +3551,7 @@ dependencies = [
  "stella-core",
  "stella-engine",
  "stella-protocol",
+ "stella-time",
  "tempfile",
  "thiserror 2.0.20",
  "tokio",
@@ -3571,6 +3575,16 @@ dependencies = [
  "thiserror 2.0.20",
 ]
 
+[[package]]
+name = "stella-time"
+version = "0.9.416"
+dependencies = [
+ "async-trait",
+ "rand 0.10.2",
+ "stella-core",
+ "tokio",
+]
+
 [[package]]
 name = "stella-tool-facts"
 version = "0.9.416"
diff --git a/Cargo.toml b/Cargo.toml
index 6d2c47b062..5638e5f763 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -23,6 +23,7 @@ members = [
   "crates/stella-mcp",
   "crates/stella-engine",
   "crates/stella-runtime",
+  "crates/stella-time",
   "crates/stella-fleet",
   "crates/stella-observatory",
   "crates/stella-serve",
diff --git a/crates/stella-cli/Cargo.toml b/crates/stella-cli/Cargo.toml
index e40d57942f..88e22578e7 100644
--- a/crates/stella-cli/Cargo.toml
+++ b/crates/stella-cli/Cargo.toml
@@ -44,6 +44,8 @@ contextgraph-host.workspace = true
 # see the exact-version note in the root manifest.
 contextgraph-trace.workspace = true
 stella-core = { path = "../stella-core" }
+# The real sleeper and clocks behind stella-core's time ports.
+stella-time = { path = "../stella-time" }
 # The context-record plane — the typed taxonomy, the ingestion boundary, and
 # the registry the prompt's rule block renders from. It left `stella-core`
 # because the engine reached it through one hash call and nothing else;
diff --git a/crates/stella-cli/src/agent.rs b/crates/stella-cli/src/agent.rs
index e06ea73edb..8bb82fa4b8 100644
--- a/crates/stella-cli/src/agent.rs
+++ b/crates/stella-cli/src/agent.rs
@@ -37,7 +37,7 @@ use crate::memory::{
     turn_warrants_reflection,
 };
 use crate::plain::{self, accent};
-use crate::runtime::{SystemClock, TokioSleeper};
+use crate::runtime::{MonotonicClock, TokioSleeper};
 use crate::{OutputFormat, config::Config};
 use stella_context::EpisodeOutcome;
 
diff --git a/crates/stella-cli/src/agent/engine.rs b/crates/stella-cli/src/agent/engine.rs
index a3b31bc023..7376bde9ea 100644
--- a/crates/stella-cli/src/agent/engine.rs
+++ b/crates/stella-cli/src/agent/engine.rs
@@ -754,7 +754,7 @@ pub(crate) fn session_router(cfg: &Config, worker_ref: &ModelRef) -> Router {
     Router::new(
         wiring.pins,
         wiring.profiles,
-        CircuitBreaker::new(Box::new(SystemClock::new())),
+        CircuitBreaker::new(Box::new(MonotonicClock)),
     )
 }
 
diff --git a/crates/stella-cli/src/agent/persistence.rs b/crates/stella-cli/src/agent/persistence.rs
index 3bd518888c..8946e79255 100644
--- a/crates/stella-cli/src/agent/persistence.rs
+++ b/crates/stella-cli/src/agent/persistence.rs
@@ -223,7 +223,7 @@ pub(crate) fn spawn_renderer(
             crate::diag_boot::dx(),
             Some(crate::diag_boot::workspace_root()),
         );
-        // The stream-json sink's clock. Wall-anchored, not `SystemClock`: a
+        // The stream-json sink's clock. Wall-anchored, not `MonotonicClock`: a
         // journal stamp has to stay comparable across processes and runs, and a
         // per-construction origin is exactly the wrong shape for that (#2111).
         let clock = WallClock;
diff --git a/crates/stella-cli/src/agent/tests/engine_wiring.rs b/crates/stella-cli/src/agent/tests/engine_wiring.rs
index 72e46fd282..027fd9d299 100644
--- a/crates/stella-cli/src/agent/tests/engine_wiring.rs
+++ b/crates/stella-cli/src/agent/tests/engine_wiring.rs
@@ -209,7 +209,7 @@ fn the_settings_model_is_inert_without_the_fix_but_routes_with_it() {
     // The full round trip through the actual router — `SessionFallback` is the
     // one live `Router::resolve` caller, and `Role::Worker` is the one role it
     // asks for, which is why that pin is the one #3908 kept.
-    let breaker = CircuitBreaker::new(Box::new(SystemClock::new()));
+    let breaker = CircuitBreaker::new(Box::new(MonotonicClock));
     let router = Router::new(wiring.pins.clone(), wiring.profiles.clone(), breaker);
     assert_eq!(
         router.resolve(Role::Worker).unwrap().model_ref,
@@ -255,7 +255,7 @@ fn an_explicit_model_flag_outranks_the_settings_model() {
 
     // The round trip through the real router is the claim that matters: the
     // pin table being empty is only useful if resolution lands on the flag.
-    let breaker = CircuitBreaker::new(Box::new(SystemClock::new()));
+    let breaker = CircuitBreaker::new(Box::new(MonotonicClock));
     let router = Router::new(wiring.pins.clone(), wiring.profiles.clone(), breaker);
     assert_eq!(
         router.resolve(Role::Worker).unwrap().model_ref,
diff --git a/crates/stella-cli/src/fleet_cmd.rs b/crates/stella-cli/src/fleet_cmd.rs
index e12461304b..b1171e63f0 100644
--- a/crates/stella-cli/src/fleet_cmd.rs
+++ b/crates/stella-cli/src/fleet_cmd.rs
@@ -76,7 +76,7 @@ use tokio::sync::{mpsc, oneshot, watch};
 
 use crate::config::Config;
 use crate::lane_capabilities;
-use crate::runtime::{SystemClock, TokioSleeper, WallClock};
+use crate::runtime::{MonotonicClock, TokioSleeper, WallClock};
 // The trait is in scope for `AttemptPointStream::publish` below — a fleet
 // attempt publishes its own channel across the dispatch's points (#4730).
 use crate::wrapper_plugin::PointStream;
@@ -235,10 +235,10 @@ pub async fn run_fleet(
         WorktreeManager::new(SystemGitCli, root.clone()).with_run_scope(&run_id),
         ledger,
         agent::build_budget_guard(budget_limit),
-        // Wall-anchored, NOT `SystemClock`: every stamp this clock feeds is
+        // Wall-anchored, NOT `MonotonicClock`: every stamp this clock feeds is
         // a durable ledger row that must stay comparable across runs — the
         // warmth projection (#1222) reads a PRIOR run's `finished_at_ms`.
-        // `SystemClock`'s per-process origin made every run start near zero.
+        // A per-process origin would make every run start near zero.
         WallClock,
         {
             let mut config =
@@ -365,8 +365,7 @@ pub async fn run_fleet(
             );
         } else {
             let config = WatchConfig::default();
-            let monitor =
-                Monitor::new(SystemGhCli, Box::new(SystemClock::new())).with_config(config);
+            let monitor = Monitor::new(SystemGhCli, Box::new(MonotonicClock)).with_config(config);
             println!(
                 "  watching CI for {} fleet branch(es) — polling every {}s, wall cap {}m\n",
                 targets.len(),
diff --git a/crates/stella-cli/src/fleet_cmd/tests.rs b/crates/stella-cli/src/fleet_cmd/tests.rs
index 8522af93f6..0df580c00a 100644
--- a/crates/stella-cli/src/fleet_cmd/tests.rs
+++ b/crates/stella-cli/src/fleet_cmd/tests.rs
@@ -427,7 +427,7 @@ async fn watch_branch_reports_green_ci_and_open_pr() {
         Some(r#"{"state":"OPEN","isDraft":false}"#),
     );
     let calls = gh.calls.clone();
-    let monitor = Monitor::new(gh, Box::new(SystemClock::new()));
+    let monitor = Monitor::new(gh, Box::new(MonotonicClock));
 
     let watched = branch_watch::watch_branch(&monitor, "t1", "fleet/t1-abc").await;
     assert!(watched.is_green());
@@ -455,7 +455,7 @@ async fn watch_branch_red_ci_and_a_missing_pr_are_states_not_errors() {
         r#"[{"status":"completed","conclusion":"failure","name":"ci"}]"#,
         None,
     );
-    let monitor = Monitor::new(gh, Box::new(SystemClock::new()));
+    let monitor = Monitor::new(gh, Box::new(MonotonicClock));
 
     let watched = branch_watch::watch_branch(&monitor, "t1", "fleet/t1-abc").await;
     assert!(!watched.is_green());
@@ -475,7 +475,7 @@ async fn watch_branch_treats_a_ci_timeout_as_red() {
     // first decision (elapsed >= grace with a 0ms grace) — the watch ends
     // as NoRunsStarted without sleeping, and the branch is red.
     let gh = RoutedGh::new("[]", None);
-    let monitor = Monitor::new(gh, Box::new(SystemClock::new())).with_config(WatchConfig {
+    let monitor = Monitor::new(gh, Box::new(MonotonicClock)).with_config(WatchConfig {
         poll_interval_ms: 1,
         max_total_ms: 60_000,
         stall_timeout_ms: 60_000,
diff --git a/crates/stella-cli/src/runtime.rs b/crates/stella-cli/src/runtime.rs
index ac0f85bb81..9b0d2136a3 100644
--- a/crates/stella-cli/src/runtime.rs
+++ b/crates/stella-cli/src/runtime.rs
@@ -1,82 +1,9 @@
-//! Production implementations of `stella-core`'s time ports. The engine
-//! exports only the [`Clock`] and [`Sleeper`] traits (ports, not
-//! specific implementations), so the binary owns the concrete
-//! wall-clock/tokio impls and wires them at construction. This is what
-//! keeps `stella-core` free of production `tokio::time` calls.
+//! The binary's side of `stella-core`'s time ports. The engine exports only
+//! the `Sleeper` and `Clock` traits; the real sources behind them are
+//! `stella-time`'s, re-exported here under the names the rest of this crate
+//! wires at construction. One-shot runs also arm their task deadline here.
 
-use async_trait::async_trait;
-use rand::RngExt;
-use stella_core::ports::Clock;
-use stella_core::retry::Sleeper;
-
-/// The production clock: monotonic milliseconds since construction.
-pub struct SystemClock {
-    origin: std::time::Instant,
-}
-
-impl SystemClock {
-    pub fn new() -> Self {
-        Self {
-            origin: std::time::Instant::now(),
-        }
-    }
-}
-
-impl Default for SystemClock {
-    fn default() -> Self {
-        Self::new()
-    }
-}
-
-impl Clock for SystemClock {
-    fn now_ms(&self) -> u64 {
-        self.origin.elapsed().as_millis() as u64
-    }
-}
-
-/// A [`Clock`] anchored to the **wall** (Unix-epoch milliseconds), for
-/// stamps that must stay comparable across processes and runs — the fleet
-/// ledger's rows, where issue #1222's cache-warmth projection compares a
-/// prior run's `finished_at_ms` against "now". [`SystemClock`]'s
-/// per-construction origin is the right shape for in-process elapsed
-/// arithmetic (retry pacing, watch caps) and exactly the wrong one for a
-/// durable timestamp: two runs' stamps share no origin, so their difference
-/// means nothing. Not strictly monotonic (NTP can step it), which is why it
-/// does not replace [`SystemClock`] everywhere; a pre-epoch system clock
-/// reads as `0` rather than panicking.
-#[derive(Debug, Default, Clone, Copy)]
-pub struct WallClock;
-
-impl Clock for WallClock {
-    fn now_ms(&self) -> u64 {
-        std::time::SystemTime::now()
-            .duration_since(std::time::UNIX_EPOCH)
-            .map(|d| d.as_millis() as u64)
-            .unwrap_or(0)
-    }
-}
-
-/// The production [`Sleeper`]: `tokio::time::sleep` for the wait, and the
-/// OS entropy pool for the jitter that spreads concurrent retriers across
-/// the backoff window. Both live here so `stella-core` links neither a
-/// timer nor an entropy source.
-#[derive(Debug, Default, Clone, Copy)]
-pub struct TokioSleeper;
-
-#[async_trait]
-impl Sleeper for TokioSleeper {
-    async fn sleep(&self, duration_ms: u64) {
-        tokio::time::sleep(std::time::Duration::from_millis(duration_ms)).await;
-    }
-
-    fn now(&self) -> std::time::Instant {
-        std::time::Instant::now()
-    }
-
-    fn jitter(&self, upper: u64) -> u64 {
-        rand::rng().random_range(0..=upper)
-    }
-}
+pub use stella_time::{MonotonicClock, TokioSleeper, WallClock};
 
 /// The budget guard for a one-shot invocation, with its wall-clock task
 /// deadline armed (#1503).
@@ -123,28 +50,6 @@ pub(crate) fn one_shot_budget_guard(
 mod tests {
     use super::*;
 
-    #[test]
-    fn system_clock_starts_near_zero_and_advances_monotonically() {
-        let clock = SystemClock::new();
-        let first = clock.now_ms();
-        std::thread::sleep(std::time::Duration::from_millis(5));
-        let second = clock.now_ms();
-        assert!(second >= first, "clock must never go backwards");
-        assert!(
-            second - first >= 4,
-            "clock must actually advance with wall time"
-        );
-    }
-
-    #[test]
-    fn default_constructs_a_fresh_clock() {
-        let clock = SystemClock::default();
-        assert!(
-            clock.now_ms() < 1000,
-            "a freshly constructed clock starts near zero"
-        );
-    }
-
     /// The #1503 witness: #1481's deadline mechanism was proven by a driver
     /// test but nothing in the shipping binary armed it — this is the arming,
     /// so a one-shot task stops itself at a safe boundary instead of running
@@ -206,16 +111,4 @@ mod tests {
              harness's kill with its work discarded (#3868)"
         );
     }
-
-    #[test]
-    fn wall_clock_reads_epoch_milliseconds_not_a_process_origin() {
-        let now = WallClock.now_ms();
-        // Any plausible present is decades past the epoch — a process-origin
-        // clock would read near zero here, which is exactly the bug this
-        // clock exists to avoid for durable stamps.
-        assert!(
-            now > 1_600_000_000_000,
-            "wall clock must be epoch-anchored, got {now}"
-        );
-    }
 }
diff --git a/crates/stella-core/README.md b/crates/stella-core/README.md
index be4c5ee855..0f3f6bb2a7 100644
--- a/crates/stella-core/README.md
+++ b/crates/stella-core/README.md
@@ -161,7 +161,7 @@ lib.rs), never as a planning assumption.
 | [`src/estimator.rs`](src/estimator.rs) | Conservative token estimate plus `Calibration`/`CalibrationMap`, the per-model drift correction fed by reported usage. |
 | [`src/loop_detect.rs`](src/loop_detect.rs) + [`src/loop_detect/`](src/loop_detect) | `detect_loop()` — five rungs over `CallRecord`s: exact repeats, short cycles, stagnation, interleaved repeats, and a wrapped monotonic sweep. |
 | [`src/shell_text.rs`](src/shell_text.rs) | Reading a shell command as *text*: the quote-aware `shell_words` splitter, `is_operator_word`, and `bare_sleep_seconds` — the stall classifier the `bash` advisory and the engine's stall rung ([`src/driver/loop_escalation.rs`](src/driver/loop_escalation.rs)) share, so one operator list serves both (#2022). |
-| [`src/retry.rs`](src/retry.rs) | `RetryPolicy`, backoff computation, `retry_with_backoff*`, and the `Sleeper` port. |
+| [`src/retry.rs`](src/retry.rs) | `RetryPolicy`, backoff computation, `retry_with_backoff*`, `bounded`, and the `Sleeper` port; its real impl and its two test doubles are `stella-time`'s. |
 | [`src/starvation.rs`](src/starvation.rs) | Reasoning-starvation arithmetic: output-contract headroom, the empty-`length` signature, and the retry cap. Written to serve two chokepoints from one copy, because one copy is what makes a fix reach both (#2128, #2174): `stella-cli`'s standalone-call chokepoint, and the staged pipeline's management chokepoint until that crate was deleted (#3865). |
 | [`src/speculation.rs`](src/speculation.rs) | Early execution of read-only tool calls announced mid-stream (`pub(crate)`). |
 | [`src/receipts.rs`](src/receipts.rs) | `ReceiptLedger` — `BlockRegistered` + `StepManifest` context receipts, content-free (digests, never payloads). |
@@ -293,9 +293,11 @@ system message and the latest user message are never touched.
   with: a nested turn that dropped `gate`, `steering` and `hooks` at once.
   Do not hand-roll a child engine. Call
   [`src/subagent.rs`](src/subagent.rs)'s `run_sub_agent`, which constructs the
-  child in-crate and carries every seam. The crate still exports the `Sleeper`
-  port with no production implementation — wiring a real one is the binary's
-  job, and tests wire a no-op to run retries at zero wall-clock cost.
+  child in-crate and carries every seam. The crate exports the `Sleeper` and
+  `Clock` ports with no production implementation — `stella-time` holds
+  those, and every host passes them to `Engine::assemble`. Tests wire a
+  no-op or a paused-clock double, so a retry costs no wall clock and a
+  timeout still means what it says.
 - **A sub-agent's steering is filtered, not inherited.** `drain_steering` is
   destructive by contract, so a child that inherited the parent's `TurnSteering`
   would swallow a message the user addressed to the parent. `ChildSteering`
@@ -330,7 +332,8 @@ turn-driver audit witnesses; also `budget_boundaries.rs`,
 [`src/loop_detect.rs`](src/loop_detect.rs); a failing case writes its seed to
 `proptest-regressions/`, and that seed is committed. No feature flag, no env var, no
 fixture server and no network — driver tests wire scripted `Provider`s, counting
-`ToolExecutor`s and no-op `Sleeper`s, so the suite runs in seconds. Keep it that
+`ToolExecutor`s and sleeper doubles on a paused runtime, so the suite runs in
+seconds. Keep it that
 way: a test here that needs a file or a socket means the logic under test is in
 the wrong crate.
 
diff --git a/crates/stella-fleet/Cargo.toml b/crates/stella-fleet/Cargo.toml
index 3270a48f47..2d440d84d1 100644
--- a/crates/stella-fleet/Cargo.toml
+++ b/crates/stella-fleet/Cargo.toml
@@ -15,6 +15,8 @@ publish.workspace = true
 stella-autonomy = { path = "../stella-autonomy" }
 stella-protocol = { path = "../stella-protocol" }
 stella-core = { path = "../stella-core" }
+# The real sleeper and clocks behind stella-core's time ports.
+stella-time = { path = "../stella-time" }
 stella-store = { path = "../stella-store" }
 stella-tools = { path = "../stella-tools" }
 serde.workspace = true
diff --git a/crates/stella-fleet/src/monitor.rs b/crates/stella-fleet/src/monitor.rs
index f0907be713..1ac67d663d 100644
--- a/crates/stella-fleet/src/monitor.rs
+++ b/crates/stella-fleet/src/monitor.rs
@@ -142,24 +142,13 @@ async fn run_with_timeout(
 
 // Sleeper (deferred-wait pacing; injectable so caps are testable)
 
-/// The pacing seam for the poll loop — real impl sleeps, the test impl
-/// advances the injected [`Clock`] instead so a 2h cap is proven in
+/// The pacing seam for the poll loop: the engine's own [`Sleeper`] port, so
+/// the fleet keeps no second one. The real impl sleeps; the test impl
+/// advances the injected [`Clock`] instead, so a 2h cap is proven in
 /// microseconds.
-#[async_trait]
-pub trait Sleeper: Send + Sync {
-    async fn sleep(&self, ms: u64);
-}
-
-/// Production [`Sleeper`] — a real `tokio` sleep.
-#[derive(Debug, Clone, Copy, Default)]
-pub struct TokioSleeper;
-
-#[async_trait]
-impl Sleeper for TokioSleeper {
-    async fn sleep(&self, ms: u64) {
-        tokio::time::sleep(Duration::from_millis(ms)).await;
-    }
-}
+pub use stella_core::retry::Sleeper;
+/// Production [`Sleeper`] — a real `tokio` sleep, from `stella-time`.
+pub use stella_time::TokioSleeper;
 
 // Errors
 
@@ -837,6 +826,16 @@ mod tests {
         async fn sleep(&self, ms: u64) {
             self.0.fetch_add(ms, Ordering::SeqCst);
         }
+
+        // The monitor reads the injected `Clock`, never this: the port's
+        // `now` and `jitter` exist for the engine's retry ladder.
+        fn now(&self) -> std::time::Instant {
+            std::time::Instant::now()
+        }
+
+        fn jitter(&self, _upper: u64) -> u64 {
+            0
+        }
     }
 
     /// A scripted `gh`: records calls, pops one response per call, and
diff --git a/crates/stella-runtime/Cargo.toml b/crates/stella-runtime/Cargo.toml
index fa74fd3432..f5f1258fd6 100644
--- a/crates/stella-runtime/Cargo.toml
+++ b/crates/stella-runtime/Cargo.toml
@@ -11,6 +11,8 @@ publish.workspace = true
 [dependencies]
 stella-protocol = { path = "../stella-protocol" }
 stella-core = { path = "../stella-core" }
+# The real sleeper and clocks behind stella-core's time ports.
+stella-time = { path = "../stella-time" }
 stella-model = { path = "../stella-model" }
 stella-tools = { path = "../stella-tools" }
 stella-store = { path = "../stella-store" }
diff --git a/crates/stella-runtime/src/wrapper/dispatch.rs b/crates/stella-runtime/src/wrapper/dispatch.rs
index 5a101e803b..a0ca54d733 100644
--- a/crates/stella-runtime/src/wrapper/dispatch.rs
+++ b/crates/stella-runtime/src/wrapper/dispatch.rs
@@ -83,7 +83,7 @@ use stella_plugin::{
 use stella_protocol::completion::CompletionMessage;
 use stella_protocol::{GateBoard, LadderRung, LadderSnapshot, VerdictEvidence};
 
-use super::stamp::{HostClock, StampTiming};
+use super::stamp::StampTiming;
 use super::{
     ArbiterClaim, Arbitration, TurnHoldBudget, TurnWrapper, WrapperError, admissible, again,
     fold_stamps, judge, stamp,
@@ -585,7 +585,7 @@ impl WrapperDispatch {
             hold_grant: composition.hold_grant,
             host_max_holds: DEFAULT_HOST_MAX_HOLDS,
             context: None,
-            clock: Arc::new(HostClock),
+            clock: Arc::new(stella_time::WallClock),
         })
     }
 
diff --git a/crates/stella-runtime/src/wrapper/stamp.rs b/crates/stella-runtime/src/wrapper/stamp.rs
index 34a0281fa1..b2c67b9a43 100644
--- a/crates/stella-runtime/src/wrapper/stamp.rs
+++ b/crates/stella-runtime/src/wrapper/stamp.rs
@@ -13,9 +13,8 @@
 //!
 //! [`stamped`] reads no clock and touches no file. The caller passes the two
 //! times in, which is what lets a test pin a whole stamp to the byte.
-//! [`HostClock`] is the source a real run passes them from.
+//! `stella-time`'s `WallClock` is the source a real run passes them from.
 
-use stella_core::ports::Clock;
 use stella_plugin::{
     EvidenceProvenance, EvidenceSet, FlipObservation, TamperFinding, UndecidedReason, Verdict,
     VerdictRule,
@@ -28,28 +27,6 @@ use super::arbitration::ArbiterClaim;
 /// The name a stamp carries when the host reached the answer itself.
 pub const HOST_AUTHOR: &str = "engine";
 
-/// The host's own clock, counting from the Unix epoch.
-///
-/// Two stamps are compared across runs and across machines, so they have to
-/// count from a shared start. A clock that counts from the moment a process
-/// began would make the gap between two stamps mean nothing. `stella-cli`'s
-/// `WallClock` answers the same port the same way and for the same reason.
-///
-/// A system clock set before the epoch reads as `0` rather than failing: a
-/// wrong time is a bad stamp, and a run that stops for one is worse.
-#[derive(Debug, Default, Clone, Copy)]
-pub struct HostClock;
-
-impl Clock for HostClock {
-    fn now_ms(&self) -> u64 {
-        std::time::SystemTime::now()
-            .duration_since(std::time::UNIX_EPOCH)
-            .map_or(0, |since| {
-                u64::try_from(since.as_millis()).unwrap_or(u64::MAX)
-            })
-    }
-}
-
 /// When an observer decided, and how long it took.
 #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
 pub struct StampTiming {
@@ -182,8 +159,8 @@ pub fn stamped(
 /// verdict could not say whether a check answered, stood aside, or was never
 /// heard from. This is what carries them.
 ///
-/// They go **ahead** of the arbiter's stamp, because they arrived first and
-/// the list is arrival order — the same order the fold's rows are in, so a
+/// They go **ahead** of the arbiter's stamp. They arrived first, and the
+/// list is in arrival order. That is the order of the fold's rows too, so a
 /// reader of the record and a reader of the fold see one sequence.
 ///
 /// Every stamp on one record shares a hash. The preimage drops the stamp
diff --git a/crates/stella-serve/Cargo.toml b/crates/stella-serve/Cargo.toml
index 945ffd700f..72509abb31 100644
--- a/crates/stella-serve/Cargo.toml
+++ b/crates/stella-serve/Cargo.toml
@@ -27,6 +27,8 @@ schema = ["dep:schemars", "stella-protocol/schema"]
 schemars = { workspace = true, optional = true }
 stella-protocol = { path = "../stella-protocol" }
 stella-core = { path = "../stella-core" }
+# The real sleeper and clocks behind stella-core's time ports.
+stella-time = { path = "../stella-time" }
 # The step-scoped facade (#971 phase 1). `stella-serve` drives `run_step`
 # rather than `Engine::run_turn` so a turn can be cancelled at a step
 # boundary and checkpointed between steps (#1129).
diff --git a/crates/stella-serve/src/extensions.rs b/crates/stella-serve/src/extensions.rs
index d1262bb28e..fddf5f48e0 100644
--- a/crates/stella-serve/src/extensions.rs
+++ b/crates/stella-serve/src/extensions.rs
@@ -144,7 +144,7 @@ pub(crate) fn install_for_turn(
     if extensions.is_empty() {
         return None;
     }
-    let bus = HookBus::new(turn_id, crate::remote::WallClock);
+    let bus = HookBus::new(turn_id, stella_time::WallClock);
     for extension in extensions {
         extension.install(&bus);
     }
diff --git a/crates/stella-serve/src/remote.rs b/crates/stella-serve/src/remote.rs
index 085915eb08..ba3514a91b 100644
--- a/crates/stella-serve/src/remote.rs
+++ b/crates/stella-serve/src/remote.rs
@@ -16,14 +16,10 @@ use std::sync::atomic::{AtomicU64, Ordering};
 use std::time::{Duration, Instant};
 
 use async_trait::async_trait;
-use rand::RngExt;
 use serde_json::Value;
 use stella_core::bus::{self, HookBus, HookEventDraft, names as hook_names};
 use stella_core::hooks::decision::{GateVerdict, OperatorPosture, resolve_precedence};
-use stella_core::ports::{
-    AuthzGate, Clock, DispatchAdmission, DispatchGate, Principal, ToolExecutor,
-};
-use stella_core::retry::Sleeper;
+use stella_core::ports::{AuthzGate, DispatchAdmission, DispatchGate, Principal, ToolExecutor};
 use stella_protocol::{
     CompletionRequestRef, CompletionResult, Provider, ProviderError, ToolCallObserver, ToolOutput,
     ToolSchema,
@@ -129,47 +125,6 @@ fn forward_delta(observer: Option<&dyn ToolCallObserver>, delta: &ProviderDelta)
     }
 }
 
-/// A Tokio-backed [`Sleeper`] for the session runtime's retry backoff. The
-/// session runtime is built with the time driver enabled, so `sleep` resolves
-/// there. The jitter draws from the OS entropy pool, which is the host's to
-/// hold: `stella-core` takes the draw through the port and links no entropy
-/// source of its own.
-pub(crate) struct TokioSleeper;
-
-#[async_trait]
-impl Sleeper for TokioSleeper {
-    async fn sleep(&self, duration_ms: u64) {
-        tokio::time::sleep(std::time::Duration::from_millis(duration_ms)).await;
-    }
-
-    fn now(&self) -> std::time::Instant {
-        std::time::Instant::now()
-    }
-
-    fn jitter(&self, upper: u64) -> u64 {
-        rand::rng().random_range(0..=upper)
-    }
-}
-
-/// The host's own clock, counting from the Unix epoch, for the stamp on
-/// every hook event an installed extension sees. Those stamps are read on
-/// the other side of the wire and compared with the host's own, so they have
-/// to share the host's origin; `stella-cli`'s `WallClock` answers the same
-/// port the same way. A system clock set before the epoch reads as `0`
-/// rather than failing the turn.
-#[derive(Debug, Default, Clone, Copy)]
-pub(crate) struct WallClock;
-
-impl Clock for WallClock {
-    fn now_ms(&self) -> u64 {
-        std::time::SystemTime::now()
-            .duration_since(std::time::UNIX_EPOCH)
-            .map_or(0, |since| {
-                u64::try_from(since.as_millis()).unwrap_or(u64::MAX)
-            })
-    }
-}
-
 /// The `Provider` port as a reverse-RPC to the host. `complete_ref` emits a
 /// [`ServerFrame::ProviderRequest`] and blocks the step on the host's answer.
 pub(crate) struct RemoteProvider {
@@ -235,12 +190,12 @@ impl RemoteProvider {
 impl RemoteProvider {
     /// The one remoted completion path, shared by both trait methods.
     ///
-    /// With an observer this is what closes #1165: fragments the host POSTs to
-    /// `/v1/turns/{id}/provider-delta` land on the registered feed and are
+    /// With an observer this is what closes #1165. Fragments the host POSTs to
+    /// `/v1/turns/{id}/provider-delta` land on the registered feed. They are
     /// forwarded inline, so the engine's gate emits `TextDelta` / `Reasoning`
-    /// events and the frames flow into `FrameHistory` — ordering, `seq`, and
-    /// replay for free, exactly as with a local streaming adapter. Without an
-    /// observer the fragments are drained and dropped; the aggregated result
+    /// events and the frames flow into `FrameHistory`. Ordering, `seq` and
+    /// replay come for free, as with a local streaming adapter. Without an
+    /// observer the fragments are drained and dropped. The aggregated result
     /// stays authoritative either way.
     async fn complete_remoted(
         &self,
diff --git a/crates/stella-serve/src/session.rs b/crates/stella-serve/src/session.rs
index df0e46264f..e485f0a5f6 100644
--- a/crates/stella-serve/src/session.rs
+++ b/crates/stella-serve/src/session.rs
@@ -42,9 +42,8 @@ use crate::observe::SharedObserver;
 use crate::observe::event::{ServeEvent, SettledOutcome, TurnRef, TurnTally, millis};
 use crate::observe::tally::TallyFold;
 use crate::pending::Pending;
-use crate::remote::{
-    DEFAULT_REVERSE_REQUEST_TIMEOUT, RemoteProvider, RemoteToolExecutor, TokioSleeper,
-};
+use crate::remote::{DEFAULT_REVERSE_REQUEST_TIMEOUT, RemoteProvider, RemoteToolExecutor};
+use stella_time::TokioSleeper;
 
 /// Everything needed to run one turn. The host assembles this — it owns prompt
 /// construction, recall, model selection, and the tool set (advertised as
diff --git a/crates/stella-serve/src/subagents.rs b/crates/stella-serve/src/subagents.rs
index b94b6c0ee8..20e4807aec 100644
--- a/crates/stella-serve/src/subagents.rs
+++ b/crates/stella-serve/src/subagents.rs
@@ -270,7 +270,7 @@ impl SubAgentDispatcher for ServedSubAgents {
                         &*provider,
                         &read_only,
                         config,
-                        &crate::remote::TokioSleeper,
+                        &stella_time::TokioSleeper,
                         seams,
                     );
                     // Carve from the shared pool, run, settle. The lock is
diff --git a/crates/stella-time/Cargo.toml b/crates/stella-time/Cargo.toml
new file mode 100644
index 0000000000..89e5984e7f
--- /dev/null
+++ b/crates/stella-time/Cargo.toml
@@ -0,0 +1,30 @@
+[package]
+name = "stella-time"
+description = "The real time sources behind stella-core's Sleeper and Clock ports — one Tokio sleeper, one wall clock, one monotonic clock — shared by every host that assembles an engine, none of which may depend on another; plus, behind `test-util`, the two sleeper doubles every test against the engine shares."
+version.workspace = true
+edition.workspace = true
+rust-version.workspace = true
+license.workspace = true
+repository.workspace = true
+publish.workspace = true
+
+# The ports come from stella-core; the effects behind them come from the
+# runtime and the entropy pool. stella-core may link neither (its manifest
+# says so, and `make core-no-io` holds it there), and stella-serve may not
+# link stella-cli or stella-runtime, so the copies these replaced had no
+# home below all of their callers until this crate.
+[dependencies]
+stella-core = { path = "../stella-core" }
+tokio = { workspace = true, features = ["time"] }
+async-trait = { workspace = true }
+rand = { workspace = true }
+
+[features]
+# The two sleeper doubles every test against the engine shares: `NoopSleeper`
+# and `PausedSleeper` (`test_util`). Off in a release build. This is the
+# tokio-test shape: the doubles live beside the real sources, and the crate
+# under test takes them as a dev-dependency.
+test-util = []
+
+[dev-dependencies]
+tokio = { workspace = true, features = ["rt", "macros", "test-util"] }
diff --git a/crates/stella-time/README.md b/crates/stella-time/README.md
new file mode 100644
index 0000000000..f7430272fa
--- /dev/null
+++ b/crates/stella-time/README.md
@@ -0,0 +1,80 @@
+# stella-time
+
+The real time sources behind `stella-core`'s two time ports, and the two
+test doubles every test against the engine shares.
+
+```rust
+stella_time::TokioSleeper    // Sleeper: tokio::time::sleep, Instant::now, jitter from the OS
+stella_time::WallClock       // Clock: milliseconds since the Unix epoch
+stella_time::MonotonicClock  // Clock: milliseconds since this process's first read, never backwards
+
+stella_time::test_util::PausedSleeper  // Sleeper on tokio's paused clock (feature `test-util`)
+stella_time::test_util::NoopSleeper    // Sleeper that never waits (feature `test-util`)
+```
+
+`stella_core::retry::Sleeper` and `stella_core::ports::Clock` are the ports.
+The engine reads time, waits and times out only through them (ADR 0042). A
+host picks a source here and hands it to `Engine::assemble`.
+
+## Which source
+
+- **The engine, in a real run:** `TokioSleeper`. Its `now` is the monotonic
+  clock, so a deadline armed from it is checked on the same timeline.
+- **A stamp read by another process or a later run:** `WallClock`. A hook
+  event's stamp, a fleet ledger row, a verdict stamp. Two such stamps must
+  count from one start. The epoch is the only start every process agrees
+  on.
+- **A span measured inside one process and compared as a number:**
+  `MonotonicClock`. A circuit breaker's cooling window, a CI monitor's wall
+  cap. Every instance shares one origin, so two holders built at different
+  moments read one timeline.
+- **A test that arms a timeout:** `PausedSleeper`, on a paused tokio
+  runtime. A sleep is free while the runtime is idle and still lets a
+  pending call finish first. A sleeper that returned at once would fire
+  every engine timeout the moment a provider future waited on another task.
+- **A test that arms none:** `NoopSleeper`.
+
+## Boundary — does this change belong here?
+
+This crate implements the ports. It decides nothing. A change belongs here
+only if it is a new way to read the real clock, wait on the real timer, or
+stand in for either under test. What to do with a reading belongs in
+`stella-core`, where it is a pure function over the reading. A double with
+a special shape, such as a seeded jitter or a sleep that records its calls,
+belongs beside the one test that needs it.
+
+## Why it is a crate
+
+AGENTS.md § "When a new crate is justified" names two of its three cases.
+These items are the effects the ports keep out of `stella-core`. Its
+manifest may not take the Tokio timer or an entropy source, and
+`make core-no-io` holds it there. And they sit below every host.
+`stella-cli`, `stella-runtime` and `stella-serve` each build an engine, and
+`stella-serve` may not link the other two. Before this crate each host kept
+its own copy, and about thirty test files each kept a double.
+
+`test_util` lives here and not in `stella-core` for the same reason: a
+faithful sleeper double needs the Tokio timer, and `stella-core` may not
+link it. A crate that tests the engine takes this crate as a dev-dependency
+with the feature on; for `stella-core`'s integration tests that is a
+cycle, which cargo allows. That is the tokio / tokio-test shape.
+`stella-core`'s own unit tests are the one place that cannot: a lib's unit
+tests are a second build of the lib, and a dev-dependency that links the
+lib implements the trait for the first. They keep one copy, and
+`tests/one_home.rs` names it and fails on any other.
+
+## God files — do not add lines
+
+This crate has no god files. No file exceeds the gate's 1500-line ratchet
+(`scripts/check-file-size.sh`), and none may appear. A new file crossing
+1500 lines fails the gate outright, and `scripts/file-size-baseline.txt`
+accepts no new entries. When a file here nears the limit, split it before
+it crosses.
+
+## Consumers
+
+- `stella-cli` re-exports the three real sources from its `runtime` module.
+- `stella-serve` builds its engines on `TokioSleeper` and stamps its
+  extension bus from `WallClock`.
+- `stella-runtime` stamps wrapper verdicts from `WallClock`.
+- `stella-fleet` paces its CI monitor with `TokioSleeper`.
diff --git a/crates/stella-time/src/lib.rs b/crates/stella-time/src/lib.rs
new file mode 100644
index 0000000000..8e222925cc
--- /dev/null
+++ b/crates/stella-time/src/lib.rs
@@ -0,0 +1,164 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+// Copyright (c) 2026 Oxagen, Inc. Commercial licensing: licensing@oxagen.sh
+
+//! The real time sources behind `stella-core`'s two time ports.
+//!
+//! [`stella_core::retry::Sleeper`] and [`stella_core::ports::Clock`] are
+//! traits. The engine reads time, waits and times out only through them. A
+//! real run gets its answers here: a sleeper on the Tokio timer, a clock
+//! counting from the Unix epoch, and one counting from the first read. The
+//! README says why the three share one crate below every host, and
+//! `test_util` holds the two doubles every test against the engine shares.
+
+use std::sync::OnceLock;
+use std::time::{Instant, SystemTime, UNIX_EPOCH};
+
+use async_trait::async_trait;
+use rand::RngExt;
+use stella_core::ports::Clock;
+use stella_core::retry::Sleeper;
+
+#[cfg(feature = "test-util")]
+pub mod test_util;
+
+/// The real [`Sleeper`]. It waits on `tokio::time::sleep`, reads `now` off
+/// the monotonic clock, and draws its jitter from the OS entropy pool, which
+/// spreads retriers across the backoff window.
+///
+/// It needs a Tokio runtime with its time driver on. Every host here builds
+/// one.
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
+pub struct TokioSleeper;
+
+#[async_trait]
+impl Sleeper for TokioSleeper {
+    async fn sleep(&self, duration_ms: u64) {
+        tokio::time::sleep(std::time::Duration::from_millis(duration_ms)).await;
+    }
+
+    fn now(&self) -> Instant {
+        Instant::now()
+    }
+
+    fn jitter(&self, upper: u64) -> u64 {
+        rand::rng().random_range(0..=upper)
+    }
+}
+
+/// A [`Clock`] counting milliseconds from the Unix epoch.
+///
+/// For a stamp read by another process or a later run: a hook event on the
+/// far side of a socket, a fleet ledger row, a verdict stamp. Two such
+/// stamps must count from one start. The epoch is the only start every
+/// process agrees on.
+///
+/// Not for a deadline. The system clock can be stepped, by NTP or by hand.
+/// A deadline set against it moves with it. The engine's deadlines come off
+/// [`Sleeper::now`] instead.
+///
+/// A system clock set before the epoch reads as `0`. One past what `u64`
+/// holds reads as `u64::MAX`. A wrong time makes a bad stamp. A run that
+/// stops for one is worse.
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
+pub struct WallClock;
+
+impl Clock for WallClock {
+    fn now_ms(&self) -> u64 {
+        SystemTime::now()
+            .duration_since(UNIX_EPOCH)
+            .map_or(0, |since| {
+                u64::try_from(since.as_millis()).unwrap_or(u64::MAX)
+            })
+    }
+}
+
+/// The origin every [`MonotonicClock`] counts from: the first read in this
+/// process. One cell, so two instances built at different moments agree.
+static ORIGIN: OnceLock = OnceLock::new();
+
+/// A [`Clock`] counting milliseconds from the process's first read of it.
+/// It never goes backwards.
+///
+/// For a span measured inside one process and compared as a number: a
+/// circuit breaker's cooling window, a CI monitor's wall cap. Every
+/// instance shares the origin, so two holders built at different moments
+/// read one timeline. A clock whose origin was the moment it was built
+/// would put their readings apart, by the time between the two builds, and
+/// say nothing.
+///
+/// The first read in the process reads `0`.
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
+pub struct MonotonicClock;
+
+impl Clock for MonotonicClock {
+    fn now_ms(&self) -> u64 {
+        let origin = ORIGIN.get_or_init(Instant::now);
+        u64::try_from(origin.elapsed().as_millis()).unwrap_or(u64::MAX)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn the_monotonic_clock_never_goes_backwards() {
+        let first = MonotonicClock.now_ms();
+        std::thread::sleep(std::time::Duration::from_millis(5));
+        let second = MonotonicClock.now_ms();
+        assert!(second >= first, "clock must never go backwards");
+        assert!(
+            second - first >= 4,
+            "5ms of sleep should register: {first} -> {second}"
+        );
+    }
+
+    /// Two instances share one origin, so a reading taken on one is a
+    /// reading on the other.
+    #[test]
+    fn every_monotonic_clock_shares_one_origin() {
+        let first = MonotonicClock;
+        let mark = first.now_ms() + 10_000;
+        std::thread::sleep(std::time::Duration::from_millis(5));
+        let second = MonotonicClock;
+        let now = second.now_ms();
+        assert!(
+            now < mark,
+            "a fresh instance reads the same clock: {now} vs {mark}"
+        );
+        assert!(
+            mark - now <= 10_000,
+            "and it has moved on from the first read"
+        );
+    }
+
+    #[test]
+    fn the_wall_clock_counts_from_the_unix_epoch() {
+        // 2026-01-01T00:00:00Z. The test runs after that date. The point is
+        // the origin, not the exact reading.
+        let epoch_2026_ms: u64 = 1_767_225_600_000;
+        assert!(WallClock.now_ms() > epoch_2026_ms);
+    }
+
+    #[test]
+    fn jitter_stays_inside_the_window() {
+        for _ in 0..1_000 {
+            assert!(TokioSleeper.jitter(7) <= 7);
+        }
+        assert_eq!(TokioSleeper.jitter(0), 0);
+    }
+
+    #[tokio::test(start_paused = true)]
+    async fn the_sleeper_waits_on_the_tokio_timer() {
+        let before = tokio::time::Instant::now();
+        TokioSleeper.sleep(250).await;
+        assert!(before.elapsed() >= std::time::Duration::from_millis(250));
+    }
+
+    #[test]
+    fn the_sleepers_now_is_the_monotonic_clock() {
+        let a = TokioSleeper.now();
+        let b = TokioSleeper.now();
+        assert!(b >= a);
+    }
+}
diff --git a/crates/stella-time/src/test_util.rs b/crates/stella-time/src/test_util.rs
new file mode 100644
index 0000000000..67b9f8349f
--- /dev/null
+++ b/crates/stella-time/src/test_util.rs
@@ -0,0 +1,64 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+// Copyright (c) 2026 Oxagen, Inc. Commercial licensing: licensing@oxagen.sh
+
+//! The two [`Sleeper`] doubles every test against the engine shares.
+//!
+//! Behind the `test-util` feature, so a release build carries neither. A
+//! crate that tests the engine takes this crate as a dev-dependency with the
+//! feature on, the way tokio's tests take `tokio-test`.
+
+use std::time::Instant;
+
+use async_trait::async_trait;
+use stella_core::retry::Sleeper;
+
+/// A [`Sleeper`] on tokio's clock, for a test that runs its runtime paused.
+///
+/// A sleep costs nothing while the runtime is idle. It still lets a pending
+/// call finish first, and `now` reads the same virtual timeline. That is
+/// what a timeout is. So this is the double for any test that arms a tool
+/// or model timeout. A sleeper that returned at once would fire every
+/// engine timeout as soon as a provider future waited on another task.
+///
+/// Jitter is zero: a test that asserts on retry timing wants no spread.
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
+pub struct PausedSleeper;
+
+#[async_trait]
+impl Sleeper for PausedSleeper {
+    async fn sleep(&self, duration_ms: u64) {
+        tokio::time::sleep(std::time::Duration::from_millis(duration_ms)).await;
+    }
+
+    fn now(&self) -> Instant {
+        tokio::time::Instant::now().into_std()
+    }
+
+    fn jitter(&self, _upper: u64) -> u64 {
+        0
+    }
+}
+
+/// A [`Sleeper`] that never waits.
+///
+/// For a test that arms no timeout and wants a retry ladder to run at once.
+/// `now` reads the real monotonic clock, so a deadline set from it still
+/// means what it says. Do not drive a timed turn with it: see
+/// [`PausedSleeper`] for why.
+///
+/// Jitter is zero, for the same reason as [`PausedSleeper`]'s.
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
+pub struct NoopSleeper;
+
+#[async_trait]
+impl Sleeper for NoopSleeper {
+    async fn sleep(&self, _duration_ms: u64) {}
+
+    fn now(&self) -> Instant {
+        Instant::now()
+    }
+
+    fn jitter(&self, _upper: u64) -> u64 {
+        0
+    }
+}
diff --git a/crates/stella-time/tests/one_home.rs b/crates/stella-time/tests/one_home.rs
new file mode 100644
index 0000000000..eb629523d2
--- /dev/null
+++ b/crates/stella-time/tests/one_home.rs
@@ -0,0 +1,177 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+// Copyright (c) 2026 Oxagen, Inc. Commercial licensing: licensing@oxagen.sh
+
+//! The witness for this crate: the real time sources and the shared sleeper
+//! doubles live here and nowhere else.
+//!
+//! Three crates each kept a wall clock. Two kept a Tokio sleeper. One kept a
+//! sleeper trait of its own. This test reads the shipping tree and fails on
+//! the first copy that comes back. It reads source, which a test of "where
+//! does this live" has to do. `stella-core` would refuse that read. This
+//! crate does not.
+//!
+//! Test files are skipped here: the per-file test doubles are the next
+//! sweep, and it widens this scan to them.
+
+use std::fs;
+use std::path::{Path, PathBuf};
+
+/// A `Sleeper` impl in shipping source outside this crate that stays on
+/// purpose, with the reason. Each one is a test double with a shape a shared
+/// double cannot have, kept in the file its one test lives in.
+const SLEEPER_IMPLS_KEPT: &[(&str, &str)] = &[
+    (
+        "crates/stella-core/src/retry.rs",
+        "the retry tests' recording sleeper: it logs every requested delay and draws a seeded jitter",
+    ),
+    (
+        "crates/stella-fleet/src/monitor.rs",
+        "the monitor tests' advancing sleeper: a sleep moves the injected clock instead of waiting",
+    ),
+    (
+        "crates/stella-core/src/accounted_call.rs",
+        "two doubles beside the accounted-call tests; the test-double sweep retires them",
+    ),
+    (
+        "crates/stella-core/src/driver/dispatch.rs",
+        "a no-op double beside the dispatch tests; the test-double sweep retires it",
+    ),
+    (
+        "crates/stella-core/src/driver/drive.rs",
+        "a no-op double beside the drive tests; the test-double sweep retires it",
+    ),
+    (
+        "crates/stella-core/src/driver/restore.rs",
+        "a no-op double beside the restore tests; the test-double sweep retires it",
+    ),
+    (
+        "crates/stella-core/src/goal.rs",
+        "a no-op double beside the goal tests; the test-double sweep retires it",
+    ),
+];
+
+/// A clock struct named like one of ours that is not ours.
+const CLOCK_STRUCTS_KEPT: &[(&str, &str)] = &[(
+    "crates/stella-context/src/clock.rs",
+    "stella-context's own Clock trait, which reads RFC 3339 text rather than milliseconds",
+)];
+
+fn workspace_root() -> PathBuf {
+    Path::new(env!("CARGO_MANIFEST_DIR"))
+        .ancestors()
+        .nth(2)
+        .expect("crates/stella-time sits two levels below the workspace root")
+        .to_path_buf()
+}
+
+/// Every `.rs` file under `crates/*/src` and `crates/*/tests`, as paths
+/// relative to the workspace root, skipping this crate.
+fn rust_sources(root: &Path) -> Vec<(String, String)> {
+    let mut out = Vec::new();
+    let crates = root.join("crates");
+    for entry in fs::read_dir(&crates).expect("crates/ is readable") {
+        let dir = entry.expect("a crate directory").path();
+        if dir.file_name().is_some_and(|name| name == "stella-time") {
+            continue;
+        }
+        collect(&dir.join("src"), root, &mut out);
+    }
+    out.sort();
+    out
+}
+
+/// A test file, by the names `make core-no-io` uses: a `tests/` directory
+/// or a file called `tests.rs`.
+fn is_test_path(path: &Path) -> bool {
+    path.file_name().is_some_and(|name| name == "tests.rs")
+        || path.components().any(|part| part.as_os_str() == "tests")
+}
+
+fn collect(dir: &Path, root: &Path, out: &mut Vec<(String, String)>) {
+    let Ok(entries) = fs::read_dir(dir) else {
+        return;
+    };
+    for entry in entries {
+        let path = entry.expect("a directory entry").path();
+        if is_test_path(&path) {
+            continue;
+        }
+        if path.is_dir() {
+            collect(&path, root, out);
+        } else if path.extension().is_some_and(|ext| ext == "rs") {
+            let rel = path
+                .strip_prefix(root)
+                .expect("under the root")
+                .to_string_lossy()
+                .replace('\\', "/");
+            let text = fs::read_to_string(&path).expect("a source file is readable");
+            out.push((rel, text));
+        }
+    }
+}
+
+fn is_kept(path: &str, kept: &[(&str, &str)]) -> bool {
+    kept.iter().any(|(kept_path, _)| *kept_path == path)
+}
+
+#[test]
+fn every_sleeper_impl_outside_this_crate_is_one_a_shared_double_cannot_be() {
+    let root = workspace_root();
+    let mut strays = Vec::new();
+    for (path, text) in rust_sources(&root) {
+        let impls = text
+            .lines()
+            .filter(|line| {
+                let line = line.trim_start();
+                line.starts_with("impl ")
+                    && line.contains("Sleeper for ")
+                    && !line.contains("ParkSupervisor")
+            })
+            .count();
+        if impls > 0 && !is_kept(&path, SLEEPER_IMPLS_KEPT) {
+            strays.push(format!("{path}: {impls} impl(s)"));
+        }
+    }
+    assert!(
+        strays.is_empty(),
+        "a Sleeper impl outside stella-time is a copy of one it already has: take \
+         `stella_time::TokioSleeper`, or `stella_time::test_util::{{PausedSleeper, NoopSleeper}}` \
+         behind the `test-util` feature. Found:\n  {}",
+        strays.join("\n  ")
+    );
+}
+
+#[test]
+fn every_kept_sleeper_impl_is_still_there() {
+    let root = workspace_root();
+    for (path, reason) in SLEEPER_IMPLS_KEPT {
+        let text = fs::read_to_string(root.join(path)).expect("a kept path exists");
+        assert!(
+            text.contains("Sleeper for "),
+            "{path} has no Sleeper impl; drop it from SLEEPER_IMPLS_KEPT ({reason})"
+        );
+    }
+}
+
+#[test]
+fn no_crate_keeps_its_own_wall_or_monotonic_clock() {
+    let root = workspace_root();
+    let names = ["WallClock", "HostClock", "SystemClock", "MonotonicClock"];
+    let mut strays = Vec::new();
+    for (path, text) in rust_sources(&root) {
+        if is_kept(&path, CLOCK_STRUCTS_KEPT) {
+            continue;
+        }
+        for name in names {
+            let needle = format!("struct {name}");
+            if text.contains(&needle) {
+                strays.push(format!("{path}: {needle}"));
+            }
+        }
+    }
+    assert!(
+        strays.is_empty(),
+        "a clock struct outside stella-time is a copy of one it already has. Found:\n  {}",
+        strays.join("\n  ")
+    );
+}
diff --git a/docs/adr/0042-the-engine-reads-time-through-the-sleeper-port.md b/docs/adr/0042-the-engine-reads-time-through-the-sleeper-port.md
new file mode 100644
index 0000000000..c1aa003266
--- /dev/null
+++ b/docs/adr/0042-the-engine-reads-time-through-the-sleeper-port.md
@@ -0,0 +1,102 @@
+---
+id: adr/0042-the-engine-reads-time-through-the-sleeper-port
+title: "ADR 0042: The engine reads time through the Sleeper port"
+status: implemented
+---
+
+# ADR 0042: The engine reads time through the Sleeper port
+
+- Status: accepted
+- Date: 2026-09-10
+- Decides: `#6486`, `#6484`
+- Not part of the Phase 0 series.
+
+## Context
+
+`stella-core` has had a `Clock` port since the router's circuit breaker
+needed one. It reads `now_ms() -> u64`. One module used it. The deadline
+code read `std::time::Instant::now()` itself, in nineteen places, and nine
+more read `.elapsed()`, which is the same call in disguise. Four timeouts
+went to `tokio::time::timeout` directly. `#6482` counted the plain reads
+into a down-only baseline instead of removing them.
+
+A seam nobody routes through is a claim. A test that wanted to see a
+deadline fire had to arm a real one and wait. A turn could not be replayed
+from its record. And the prose about the crate's time source drifted from
+the code twice.
+
+Two sessions fixed this at once. One made every instant a `u64` reading of
+`Clock`, with a hand-moved clock as the double. The other put `now` on
+`Sleeper` and routed the timeouts through it. The second merged as
+`#6486`. This record states that decision and why it holds.
+
+The three hosts that build an engine each kept their own copy of the real
+sources. A wall clock lived in `stella-cli`, `stella-runtime` and
+`stella-serve`. A Tokio sleeper lived in two of them, and `stella-fleet`
+had a sleeper trait of its own. About thirty test files each wrote the same
+no-op sleeper. `stella-serve` may not link the other two, and `stella-core`
+may not carry a timer, so the copies had nowhere to go.
+
+## Decision
+
+**`retry::Sleeper` is the engine's whole time port.** `now() -> Instant`
+sits beside `sleep` and `jitter`. Every deadline the engine holds is an
+`Instant` from that reading. Every elapsed time is the difference of two.
+Every timeout is `retry::bounded`, a sleep racing a call through the same
+port. Nothing in the crate calls `Instant::now()`, `.elapsed()` or
+`tokio::time`; `make core-no-io` refuses all three, and its baseline is
+empty.
+
+**One port, not two, because a double cannot answer them apart.** A sleeper
+that suspends virtually has moved its own `now`. A timeout is a sleep
+racing a call. Tokio's paused runtime is the same shape: one virtual clock
+behind `sleep` and `Instant::now`. A `u64` reading on a second port would
+have made the test move two clocks and keep them in step by hand.
+
+**The reading is an `Instant`, not a `u64`.** The deadline arithmetic
+already spoke `Instant`, `tokio::time::Instant` converts to it, and a
+paused runtime produces one. The cost is that a record cannot hold an
+`Instant`. Replay against a host that answers `now` from a record is later
+work either way.
+
+**`ports::Clock` stays, for stamps.** The hook bus and the router read
+milliseconds from an epoch the holder picks, and a hook script on the far
+side of a socket compares those stamps with its own. That is a different
+question from a deadline, and it keeps its own port.
+
+**The real sources live in one crate, `stella-time`.** `TokioSleeper`
+waits on the Tokio timer, reads `now` off the monotonic clock, and draws
+its jitter from the OS entropy pool. `WallClock` counts from the Unix
+epoch, for a stamp another process reads. `MonotonicClock` counts from the
+first read, for a span compared as a number. AGENTS.md § "When a new crate
+is justified" allows the crate on two counts. It holds the effects the
+ports keep out of `stella-core`. And it sits below `stella-serve`, which
+may not link `stella-cli` or `stella-runtime`. `stella-fleet`'s own
+`Sleeper` trait is gone; its monitor takes the engine's.
+
+**The test doubles ship from `stella-time` behind a `test-util` feature.**
+`PausedSleeper` sleeps on tokio's clock and reads its `Instant`, for a test
+that arms a timeout on a paused runtime. `NoopSleeper` never waits, for a
+test that arms none. They live beside the real sources and not in
+`stella-core` because a faithful sleeper needs the Tokio timer, which
+`stella-core` may not link. `stella-core`'s integration tests and every
+other crate take it as a dev-dependency, which cargo allows in a cycle.
+That is the tokio / tokio-test shape. `stella-core`'s own unit tests keep
+one copy in `src/tests.rs`, because a lib's unit tests are a second build
+of the lib and a dev-dependency that links the lib implements the trait
+for the first. The compiler forces that copy, and the witness names it.
+
+## Consequences
+
+`stella-core`'s test suite runs on a paused clock. Backoff sleeps that used
+to cost real seconds cost nothing, and a timeout still lets a pending call
+finish first.
+
+Each host passes `stella_time::TokioSleeper` to `Engine::assemble`. Each
+test passes `PausedSleeper` or `NoopSleeper`. A new double with a special
+shape, such as a seeded jitter or a sleep that records its calls, stays
+beside the one test that needs it.
+
+`stella-time`'s `one_home` test reads the tree and fails on any other
+`impl Sleeper for` in shipping code, so the copies cannot come back
+unnoticed.
diff --git a/docs/adr/README.md b/docs/adr/README.md
index 67569284c4..e6d259822f 100644
--- a/docs/adr/README.md
+++ b/docs/adr/README.md
@@ -108,6 +108,7 @@ open; nothing before Phase 3 forces it.
 | [0039](0039-a-live-smoke-provider-is-armed-or-declared-unarmed.md) | A Live Smoke Provider Is Armed or Declared Unarmed | Accepted |
 | [0040](0040-host-does-not-pick-the-tests.md) | The Host Does Not Pick the Tests | Accepted |
 | [0041](0041-the-turn-clock-reaches-dispatch-by-value.md) | The Turn Clock Reaches Dispatch by Value | Accepted |
+| [0042](0042-the-engine-reads-time-through-the-sleeper-port.md) | The Engine Reads Time Through the Sleeper Port | Accepted |
 
 ADR 0013 draws the line between what Stella owes a caller that moves a session
 between machines (an artifact, a fingerprint, a version contract, a visible
@@ -256,6 +257,12 @@ a defaulted port method, because the key that carries it belongs to one tool.
 A call that cannot finish and still leave room to report back is refused before
 it starts, which is the only moment AGENTS.md #6 leaves.
 
+ADR 0042 records how the engine reads time at all: every instant, wait and
+timeout goes through `retry::Sleeper`, whose `now` joined `sleep` and `jitter`
+because one double has to answer all three from one timeline. The real sources
+live in one crate, `stella-time`, below every host, and the two test doubles
+ship from it behind a `test-util` feature.
+
 ## The number is a shared cell
 
 Take the number one past the highest in the table above. Two branches doing
diff --git a/docs/manifest.json b/docs/manifest.json
index 30bf33d1b9..45739ff094 100644
--- a/docs/manifest.json
+++ b/docs/manifest.json
@@ -210,6 +210,11 @@
       "status": "implemented",
       "title": "ADR 0041: The turn clock reaches dispatch by value"
     },
+    "adr/0042-the-engine-reads-time-through-the-sleeper-port": {
+      "path": "docs/adr/0042-the-engine-reads-time-through-the-sleeper-port.md",
+      "status": "implemented",
+      "title": "ADR 0042: The engine reads time through the Sleeper port"
+    },
     "agent-monitor-protocol": {
       "path": "docs/spec/agent-monitor-protocol.md",
       "status": "living",
diff --git a/website/pnpm-lock.yaml b/website/pnpm-lock.yaml
index 64aa0956d0..c748daeb1f 100644
--- a/website/pnpm-lock.yaml
+++ b/website/pnpm-lock.yaml
@@ -6,7 +6,8 @@ settings:
 
 overrides:
   postcss: '>=8.5.23'
-  sharp: '>=0.35.0'
+  sharp: '>=0.35.4'
+  js-yaml: ^4.3.2
   nanoid: ^3.3.18
 
 importers:
@@ -319,8 +320,8 @@ packages:
     resolution: {integrity: sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==}
     engines: {node: '>=18.0.0'}
 
-  '@emnapi/runtime@1.11.2':
-    resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==}
+  '@emnapi/runtime@1.11.3':
+    resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==}
 
   '@esbuild/aix-ppc64@0.25.4':
     resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==}
@@ -658,160 +659,160 @@ packages:
     resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
     engines: {node: '>=18'}
 
-  '@img/sharp-darwin-arm64@0.35.3':
-    resolution: {integrity: sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==}
+  '@img/sharp-darwin-arm64@0.35.4':
+    resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==}
     engines: {node: '>=20.9.0'}
     cpu: [arm64]
     os: [darwin]
 
-  '@img/sharp-darwin-x64@0.35.3':
-    resolution: {integrity: sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==}
+  '@img/sharp-darwin-x64@0.35.4':
+    resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==}
     engines: {node: '>=20.9.0'}
     cpu: [x64]
     os: [darwin]
 
-  '@img/sharp-freebsd-wasm32@0.35.3':
-    resolution: {integrity: sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==}
+  '@img/sharp-freebsd-wasm32@0.35.4':
+    resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==}
     engines: {node: '>=20.9.0'}
     os: [freebsd]
 
-  '@img/sharp-libvips-darwin-arm64@1.3.2':
-    resolution: {integrity: sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==}
+  '@img/sharp-libvips-darwin-arm64@1.3.3':
+    resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==}
     cpu: [arm64]
     os: [darwin]
 
-  '@img/sharp-libvips-darwin-x64@1.3.2':
-    resolution: {integrity: sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==}
+  '@img/sharp-libvips-darwin-x64@1.3.3':
+    resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==}
     cpu: [x64]
     os: [darwin]
 
-  '@img/sharp-libvips-linux-arm64@1.3.2':
-    resolution: {integrity: sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==}
+  '@img/sharp-libvips-linux-arm64@1.3.3':
+    resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==}
     cpu: [arm64]
     os: [linux]
     libc: [glibc]
 
-  '@img/sharp-libvips-linux-arm@1.3.2':
-    resolution: {integrity: sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==}
+  '@img/sharp-libvips-linux-arm@1.3.3':
+    resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==}
     cpu: [arm]
     os: [linux]
     libc: [glibc]
 
-  '@img/sharp-libvips-linux-ppc64@1.3.2':
-    resolution: {integrity: sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==}
+  '@img/sharp-libvips-linux-ppc64@1.3.3':
+    resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==}
     cpu: [ppc64]
     os: [linux]
     libc: [glibc]
 
-  '@img/sharp-libvips-linux-riscv64@1.3.2':
-    resolution: {integrity: sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==}
+  '@img/sharp-libvips-linux-riscv64@1.3.3':
+    resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==}
     cpu: [riscv64]
     os: [linux]
     libc: [glibc]
 
-  '@img/sharp-libvips-linux-s390x@1.3.2':
-    resolution: {integrity: sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==}
+  '@img/sharp-libvips-linux-s390x@1.3.3':
+    resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==}
     cpu: [s390x]
     os: [linux]
     libc: [glibc]
 
-  '@img/sharp-libvips-linux-x64@1.3.2':
-    resolution: {integrity: sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==}
+  '@img/sharp-libvips-linux-x64@1.3.3':
+    resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==}
     cpu: [x64]
     os: [linux]
     libc: [glibc]
 
-  '@img/sharp-libvips-linuxmusl-arm64@1.3.2':
-    resolution: {integrity: sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==}
+  '@img/sharp-libvips-linuxmusl-arm64@1.3.3':
+    resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==}
     cpu: [arm64]
     os: [linux]
     libc: [musl]
 
-  '@img/sharp-libvips-linuxmusl-x64@1.3.2':
-    resolution: {integrity: sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==}
+  '@img/sharp-libvips-linuxmusl-x64@1.3.3':
+    resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==}
     cpu: [x64]
     os: [linux]
     libc: [musl]
 
-  '@img/sharp-linux-arm64@0.35.3':
-    resolution: {integrity: sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==}
+  '@img/sharp-linux-arm64@0.35.4':
+    resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==}
     engines: {node: '>=20.9.0'}
     cpu: [arm64]
     os: [linux]
     libc: [glibc]
 
-  '@img/sharp-linux-arm@0.35.3':
-    resolution: {integrity: sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==}
+  '@img/sharp-linux-arm@0.35.4':
+    resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==}
     engines: {node: '>=20.9.0'}
     cpu: [arm]
     os: [linux]
     libc: [glibc]
 
-  '@img/sharp-linux-ppc64@0.35.3':
-    resolution: {integrity: sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==}
+  '@img/sharp-linux-ppc64@0.35.4':
+    resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==}
     engines: {node: '>=20.9.0'}
     cpu: [ppc64]
     os: [linux]
     libc: [glibc]
 
-  '@img/sharp-linux-riscv64@0.35.3':
-    resolution: {integrity: sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==}
+  '@img/sharp-linux-riscv64@0.35.4':
+    resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==}
     engines: {node: '>=20.9.0'}
     cpu: [riscv64]
     os: [linux]
     libc: [glibc]
 
-  '@img/sharp-linux-s390x@0.35.3':
-    resolution: {integrity: sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==}
+  '@img/sharp-linux-s390x@0.35.4':
+    resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==}
     engines: {node: '>=20.9.0'}
     cpu: [s390x]
     os: [linux]
     libc: [glibc]
 
-  '@img/sharp-linux-x64@0.35.3':
-    resolution: {integrity: sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==}
+  '@img/sharp-linux-x64@0.35.4':
+    resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==}
     engines: {node: '>=20.9.0'}
     cpu: [x64]
     os: [linux]
     libc: [glibc]
 
-  '@img/sharp-linuxmusl-arm64@0.35.3':
-    resolution: {integrity: sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==}
+  '@img/sharp-linuxmusl-arm64@0.35.4':
+    resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==}
     engines: {node: '>=20.9.0'}
     cpu: [arm64]
     os: [linux]
     libc: [musl]
 
-  '@img/sharp-linuxmusl-x64@0.35.3':
-    resolution: {integrity: sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==}
+  '@img/sharp-linuxmusl-x64@0.35.4':
+    resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==}
     engines: {node: '>=20.9.0'}
     cpu: [x64]
     os: [linux]
     libc: [musl]
 
-  '@img/sharp-wasm32@0.35.3':
-    resolution: {integrity: sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==}
+  '@img/sharp-wasm32@0.35.4':
+    resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==}
     engines: {node: '>=20.9.0'}
 
-  '@img/sharp-webcontainers-wasm32@0.35.3':
-    resolution: {integrity: sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==}
+  '@img/sharp-webcontainers-wasm32@0.35.4':
+    resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==}
     engines: {node: '>=20.9.0'}
     cpu: [wasm32]
 
-  '@img/sharp-win32-arm64@0.35.3':
-    resolution: {integrity: sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==}
+  '@img/sharp-win32-arm64@0.35.4':
+    resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==}
     engines: {node: '>=20.9.0'}
     cpu: [arm64]
     os: [win32]
 
-  '@img/sharp-win32-ia32@0.35.3':
-    resolution: {integrity: sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==}
+  '@img/sharp-win32-ia32@0.35.4':
+    resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==}
     engines: {node: ^20.9.0}
     cpu: [ia32]
     os: [win32]
 
-  '@img/sharp-win32-x64@0.35.3':
-    resolution: {integrity: sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==}
+  '@img/sharp-win32-x64@0.35.4':
+    resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==}
     engines: {node: '>=20.9.0'}
     cpu: [x64]
     os: [win32]
@@ -2176,8 +2177,8 @@ packages:
     resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
     hasBin: true
 
-  js-yaml@4.3.1:
-    resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==}
+  js-yaml@4.3.2:
+    resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==}
     hasBin: true
 
   lightningcss-android-arm64@1.32.0:
@@ -2721,8 +2722,8 @@ packages:
   setprototypeof@1.2.0:
     resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
 
-  sharp@0.35.3:
-    resolution: {integrity: sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==}
+  sharp@0.35.4:
+    resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==}
     engines: {node: '>=20.9.0'}
     peerDependencies:
       '@types/node': '*'
@@ -3501,7 +3502,7 @@ snapshots:
 
   '@aws/lambda-invoke-store@0.3.0': {}
 
-  '@emnapi/runtime@1.11.2':
+  '@emnapi/runtime@1.11.3':
     dependencies:
       tslib: 2.8.1
     optional: true
@@ -3684,108 +3685,108 @@ snapshots:
   '@img/colour@1.1.0':
     optional: true
 
-  '@img/sharp-darwin-arm64@0.35.3':
+  '@img/sharp-darwin-arm64@0.35.4':
     optionalDependencies:
-      '@img/sharp-libvips-darwin-arm64': 1.3.2
+      '@img/sharp-libvips-darwin-arm64': 1.3.3
     optional: true
 
-  '@img/sharp-darwin-x64@0.35.3':
+  '@img/sharp-darwin-x64@0.35.4':
     optionalDependencies:
-      '@img/sharp-libvips-darwin-x64': 1.3.2
+      '@img/sharp-libvips-darwin-x64': 1.3.3
     optional: true
 
-  '@img/sharp-freebsd-wasm32@0.35.3':
+  '@img/sharp-freebsd-wasm32@0.35.4':
     dependencies:
-      '@img/sharp-wasm32': 0.35.3
+      '@img/sharp-wasm32': 0.35.4
     optional: true
 
-  '@img/sharp-libvips-darwin-arm64@1.3.2':
+  '@img/sharp-libvips-darwin-arm64@1.3.3':
     optional: true
 
-  '@img/sharp-libvips-darwin-x64@1.3.2':
+  '@img/sharp-libvips-darwin-x64@1.3.3':
     optional: true
 
-  '@img/sharp-libvips-linux-arm64@1.3.2':
+  '@img/sharp-libvips-linux-arm64@1.3.3':
     optional: true
 
-  '@img/sharp-libvips-linux-arm@1.3.2':
+  '@img/sharp-libvips-linux-arm@1.3.3':
     optional: true
 
-  '@img/sharp-libvips-linux-ppc64@1.3.2':
+  '@img/sharp-libvips-linux-ppc64@1.3.3':
     optional: true
 
-  '@img/sharp-libvips-linux-riscv64@1.3.2':
+  '@img/sharp-libvips-linux-riscv64@1.3.3':
     optional: true
 
-  '@img/sharp-libvips-linux-s390x@1.3.2':
+  '@img/sharp-libvips-linux-s390x@1.3.3':
     optional: true
 
-  '@img/sharp-libvips-linux-x64@1.3.2':
+  '@img/sharp-libvips-linux-x64@1.3.3':
     optional: true
 
-  '@img/sharp-libvips-linuxmusl-arm64@1.3.2':
+  '@img/sharp-libvips-linuxmusl-arm64@1.3.3':
     optional: true
 
-  '@img/sharp-libvips-linuxmusl-x64@1.3.2':
+  '@img/sharp-libvips-linuxmusl-x64@1.3.3':
     optional: true
 
-  '@img/sharp-linux-arm64@0.35.3':
+  '@img/sharp-linux-arm64@0.35.4':
     optionalDependencies:
-      '@img/sharp-libvips-linux-arm64': 1.3.2
+      '@img/sharp-libvips-linux-arm64': 1.3.3
     optional: true
 
-  '@img/sharp-linux-arm@0.35.3':
+  '@img/sharp-linux-arm@0.35.4':
     optionalDependencies:
-      '@img/sharp-libvips-linux-arm': 1.3.2
+      '@img/sharp-libvips-linux-arm': 1.3.3
     optional: true
 
-  '@img/sharp-linux-ppc64@0.35.3':
+  '@img/sharp-linux-ppc64@0.35.4':
     optionalDependencies:
-      '@img/sharp-libvips-linux-ppc64': 1.3.2
+      '@img/sharp-libvips-linux-ppc64': 1.3.3
     optional: true
 
-  '@img/sharp-linux-riscv64@0.35.3':
+  '@img/sharp-linux-riscv64@0.35.4':
     optionalDependencies:
-      '@img/sharp-libvips-linux-riscv64': 1.3.2
+      '@img/sharp-libvips-linux-riscv64': 1.3.3
     optional: true
 
-  '@img/sharp-linux-s390x@0.35.3':
+  '@img/sharp-linux-s390x@0.35.4':
     optionalDependencies:
-      '@img/sharp-libvips-linux-s390x': 1.3.2
+      '@img/sharp-libvips-linux-s390x': 1.3.3
     optional: true
 
-  '@img/sharp-linux-x64@0.35.3':
+  '@img/sharp-linux-x64@0.35.4':
     optionalDependencies:
-      '@img/sharp-libvips-linux-x64': 1.3.2
+      '@img/sharp-libvips-linux-x64': 1.3.3
     optional: true
 
-  '@img/sharp-linuxmusl-arm64@0.35.3':
+  '@img/sharp-linuxmusl-arm64@0.35.4':
     optionalDependencies:
-      '@img/sharp-libvips-linuxmusl-arm64': 1.3.2
+      '@img/sharp-libvips-linuxmusl-arm64': 1.3.3
     optional: true
 
-  '@img/sharp-linuxmusl-x64@0.35.3':
+  '@img/sharp-linuxmusl-x64@0.35.4':
     optionalDependencies:
-      '@img/sharp-libvips-linuxmusl-x64': 1.3.2
+      '@img/sharp-libvips-linuxmusl-x64': 1.3.3
     optional: true
 
-  '@img/sharp-wasm32@0.35.3':
+  '@img/sharp-wasm32@0.35.4':
     dependencies:
-      '@emnapi/runtime': 1.11.2
+      '@emnapi/runtime': 1.11.3
     optional: true
 
-  '@img/sharp-webcontainers-wasm32@0.35.3':
+  '@img/sharp-webcontainers-wasm32@0.35.4':
     dependencies:
-      '@img/sharp-wasm32': 0.35.3
+      '@img/sharp-wasm32': 0.35.4
     optional: true
 
-  '@img/sharp-win32-arm64@0.35.3':
+  '@img/sharp-win32-arm64@0.35.4':
     optional: true
 
-  '@img/sharp-win32-ia32@0.35.3':
+  '@img/sharp-win32-ia32@0.35.4':
     optional: true
 
-  '@img/sharp-win32-x64@0.35.3':
+  '@img/sharp-win32-x64@0.35.4':
     optional: true
 
   '@jridgewell/gen-mapping@0.3.13':
@@ -4963,7 +4964,7 @@ snapshots:
       github-slugger: 2.0.0
       hast-util-to-estree: 3.1.3
       hast-util-to-jsx-runtime: 2.3.6
-      js-yaml: 4.3.1
+      js-yaml: 4.3.2
       mdast-util-mdx: 3.0.0
       mdast-util-to-markdown: 2.1.2
       remark: 15.0.1
@@ -4997,7 +4998,7 @@ snapshots:
       esbuild: 0.28.1
       estree-util-value-to-estree: 3.5.0
       fumadocs-core: 16.9.3(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.15)(lucide-react@0.556.0(react@19.2.6))(next@16.3.3(@types/node@25.9.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(zod@4.4.3)
-      js-yaml: 4.3.1
+      js-yaml: 4.3.2
       mdast-util-mdx: 3.0.0
       picocolors: 1.1.1
       picomatch: 4.0.5
@@ -5243,7 +5244,7 @@ snapshots:
 
   jiti@2.7.0: {}
 
-  js-yaml@4.3.1:
+  js-yaml@4.3.2:
     dependencies:
       argparse: 2.0.1
 
@@ -5815,7 +5816,7 @@ snapshots:
       '@next/swc-linux-x64-musl': 16.3.3
       '@next/swc-win32-arm64-msvc': 16.3.3
       '@next/swc-win32-x64-msvc': 16.3.3
-      sharp: 0.35.3(@types/node@25.9.1)
+      sharp: 0.35.4(@types/node@25.9.1)
     transitivePeerDependencies:
       - '@babel/core'
       - '@types/node'
@@ -6084,37 +6085,37 @@ snapshots:
 
   setprototypeof@1.2.0: {}
 
-  sharp@0.35.3(@types/node@25.9.1):
+  sharp@0.35.4(@types/node@25.9.1):
     dependencies:
       '@img/colour': 1.1.0
       detect-libc: 2.1.2
       semver: 7.8.5
     optionalDependencies:
-      '@img/sharp-darwin-arm64': 0.35.3
-      '@img/sharp-darwin-x64': 0.35.3
-      '@img/sharp-freebsd-wasm32': 0.35.3
-      '@img/sharp-libvips-darwin-arm64': 1.3.2
-      '@img/sharp-libvips-darwin-x64': 1.3.2
-      '@img/sharp-libvips-linux-arm': 1.3.2
-      '@img/sharp-libvips-linux-arm64': 1.3.2
-      '@img/sharp-libvips-linux-ppc64': 1.3.2
-      '@img/sharp-libvips-linux-riscv64': 1.3.2
-      '@img/sharp-libvips-linux-s390x': 1.3.2
-      '@img/sharp-libvips-linux-x64': 1.3.2
-      '@img/sharp-libvips-linuxmusl-arm64': 1.3.2
-      '@img/sharp-libvips-linuxmusl-x64': 1.3.2
-      '@img/sharp-linux-arm': 0.35.3
-      '@img/sharp-linux-arm64': 0.35.3
-      '@img/sharp-linux-ppc64': 0.35.3
-      '@img/sharp-linux-riscv64': 0.35.3
-      '@img/sharp-linux-s390x': 0.35.3
-      '@img/sharp-linux-x64': 0.35.3
-      '@img/sharp-linuxmusl-arm64': 0.35.3
-      '@img/sharp-linuxmusl-x64': 0.35.3
-      '@img/sharp-webcontainers-wasm32': 0.35.3
-      '@img/sharp-win32-arm64': 0.35.3
-      '@img/sharp-win32-ia32': 0.35.3
-      '@img/sharp-win32-x64': 0.35.3
+      '@img/sharp-darwin-arm64': 0.35.4
+      '@img/sharp-darwin-x64': 0.35.4
+      '@img/sharp-freebsd-wasm32': 0.35.4
+      '@img/sharp-libvips-darwin-arm64': 1.3.3
+      '@img/sharp-libvips-darwin-x64': 1.3.3
+      '@img/sharp-libvips-linux-arm': 1.3.3
+      '@img/sharp-libvips-linux-arm64': 1.3.3
+      '@img/sharp-libvips-linux-ppc64': 1.3.3
+      '@img/sharp-libvips-linux-riscv64': 1.3.3
+      '@img/sharp-libvips-linux-s390x': 1.3.3
+      '@img/sharp-libvips-linux-x64': 1.3.3
+      '@img/sharp-libvips-linuxmusl-arm64': 1.3.3
+      '@img/sharp-libvips-linuxmusl-x64': 1.3.3
+      '@img/sharp-linux-arm': 0.35.4
+      '@img/sharp-linux-arm64': 0.35.4
+      '@img/sharp-linux-ppc64': 0.35.4
+      '@img/sharp-linux-riscv64': 0.35.4
+      '@img/sharp-linux-s390x': 0.35.4
+      '@img/sharp-linux-x64': 0.35.4
+      '@img/sharp-linuxmusl-arm64': 0.35.4
+      '@img/sharp-linuxmusl-x64': 0.35.4
+      '@img/sharp-webcontainers-wasm32': 0.35.4
+      '@img/sharp-win32-arm64': 0.35.4
+      '@img/sharp-win32-ia32': 0.35.4
+      '@img/sharp-win32-x64': 0.35.4
       '@types/node': 25.9.1
     optional: true
 
diff --git a/website/pnpm-workspace.yaml b/website/pnpm-workspace.yaml
index 315a893e44..d23eb01d39 100644
--- a/website/pnpm-workspace.yaml
+++ b/website/pnpm-workspace.yaml
@@ -44,7 +44,14 @@ minimumReleaseAgeExclude:
 # direct `postcss` devDependency in `package.json` moving together.
 overrides:
   postcss: ">=8.5.23"
-  sharp: ">=0.35.0"
+  # sharp <0.35.4 and js-yaml <4.3.2 each carry a high-severity advisory
+  # (GHSA-rgj7-g3m4-5g8c, GHSA-2883-xcg3-v3hh). Both arrive transitively —
+  # sharp through `next`, js-yaml through `fumadocs-core` and `fumadocs-mdx` —
+  # so, as with postcss above, no direct bump reaches them; the floor does.
+  # js-yaml takes a caret, like nanoid below: a bare floor resolves to 5.x,
+  # which fumadocs does not call.
+  sharp: ">=0.35.4"
+  js-yaml: "^4.3.2"
   # nanoid <3.3.18: a custom alphabet with a non-integer or zero `size` makes
   # the generator loop indefinitely (GHSA high, patched at 3.3.18). It arrives
   # transitively through postcss@8.5.23, which both `next` and