diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml
new file mode 100644
--- /dev/null
+++ b/.github/workflows/fuzz.yml
@@ -0,0 +1,97 @@
+name: Fuzz
+
+# Nightly soak of the command-sequence fuzzer (`crates/engine/tests/it/command_sequence_fuzz.rs`):
+# random command sequences checked against undo/redo, XLSX round-trip and full-recalc invariants.
+# `cargo xtask ci` runs it with a tiny budget; this job runs 200 seeds of 200 steps so a sequence bug
+# is found without anyone remembering to soak by hand. It is not a required check. A failing seed
+# fails this run only (the job and the Actions tab), never `main`, and uploads the failing seeds with
+# their shrunk command sequences as an artifact. A pull request that touches this file or the harness
+# runs a small budget (20 by 50) so the workflow is exercised before it reaches `main`.
+on:
+  pull_request:
+    paths: [".github/workflows/fuzz.yml", "crates/engine/tests/it/command_sequence_fuzz.rs"]
+  schedule:
+    - cron: "17 3 * * *"
+  workflow_dispatch:
+    inputs:
+      seeds:
+        description: "Seeds to run (GRIDCRAFT_FUZZ_SEEDS)"
+        default: "200"
+      steps:
+        description: "Commands per seed (GRIDCRAFT_FUZZ_STEPS)"
+        default: "200"
+
+permissions:
+  contents: read
+
+concurrency:
+  group: fuzz-${{ github.ref }}
+  cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+jobs:
+  fuzz:
+    runs-on: ubuntu-24.04
+    timeout-minutes: 90
+    env:
+      CARGO_TERM_COLOR: never
+      CARGO_INCREMENTAL: 0
+      CARGO_NET_RETRY: 10
+      RUST_BACKTRACE: 1
+      # The engine and its dependencies have no egui, fonts or system libraries, so no apt packages
+      # and no craft-fonts checkout.
+      GRIDCRAFT_FUZZ_SEEDS: ${{ inputs.seeds || (github.event_name == 'pull_request' && '20' || '200') }}
+      GRIDCRAFT_FUZZ_STEPS: ${{ inputs.steps || (github.event_name == 'pull_request' && '50' || '200') }}
+      GRIDCRAFT_FUZZ_KEEP_GOING: "1"
+      GRIDCRAFT_FUZZ_SHRINK: "1"
+    steps:
+      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+        with:
+          persist-credentials: false
+
+      # The channel in rust-toolchain.toml wins, as in the suite's gate workflow.
+      - name: Install Rust
+        run: |
+          set -euo pipefail
+          toolchain=stable
+          if [ -f rust-toolchain.toml ]; then
+            channel=$(sed -n 's/^[[:space:]]*channel[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' rust-toolchain.toml)
+            toolchain=${channel:-$toolchain}
+          fi
+          rustup toolchain install "$toolchain" --profile minimal --no-self-update
+          rustup default "$toolchain"
+          rustc --version
+
+      # Only the default branch writes the cache, so manual runs on other branches only read it.
+      - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
+        with:
+          prefix-key: fuzz-rust
+          key: ${{ hashFiles('Cargo.toml') }}
+          save-if: ${{ github.ref_name == github.event.repository.default_branch }}
+          cache-on-failure: true
+
+      - name: Fuzz command sequences
+        run: |
+          set -o pipefail
+          mkdir -p fuzz-out
+          # A zero count builds an empty seed list and passes without running anything.
+          for count in "$GRIDCRAFT_FUZZ_SEEDS" "$GRIDCRAFT_FUZZ_STEPS"; do
+            if [[ ! "$count" =~ ^[0-9]+$ ]] || (( 10#$count == 0 )); then
+              echo "Fuzz counts must be positive integers, got '$count'" >&2
+              exit 1
+            fi
+          done
+          cargo test --release -p gridcraft-engine --test it command_sequence_fuzz:: 2>&1 | tee fuzz-out/fuzz.log
+
+      - name: Collect failing seeds
+        if: failure()
+        run: |
+          grep -oE '^command-sequence fuzz failure: seed [0-9]+' fuzz-out/fuzz.log | grep -oE '[0-9]+$' > fuzz-out/failing-seeds.txt || true
+          echo "Failing seeds: $(tr '\n' ' ' < fuzz-out/failing-seeds.txt)" | tee -a "$GITHUB_STEP_SUMMARY"
+          echo "Replay one with: GRIDCRAFT_FUZZ_SEED=<seed> GRIDCRAFT_FUZZ_STEPS=$GRIDCRAFT_FUZZ_STEPS GRIDCRAFT_FUZZ_SHRINK=1 cargo test -p gridcraft-engine --test it command_sequence_fuzz::" >> "$GITHUB_STEP_SUMMARY"
+
+      - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
+        if: failure()
+        with:
+          name: fuzz-failures
+          path: fuzz-out/
+          retention-days: 30
diff --git a/ROADMAP.md b/ROADMAP.md
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -135,6 +135,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in
 
 | Date | What landed |
 |---|---|
+| 2026-10-26 | A nightly workflow soaks the command-sequence fuzzer at 200 seeds by 200 steps and uploads the failing seeds with shrunk sequences (`.github/workflows/fuzz.yml`; not a required check) |
 | 2026-10-25 | AutoSum over a selection does what Excel does: a selected row gets one total to its right (in its last cell if that is empty), not a one-cell total under each cell; a block whose last column is empty gets each row's total there, one whose last row is empty each column's, and with both the corner gets the total of the block; otherwise totals go under each column as before |
 | 2026-10-24 | Go To lists the named ranges of every sheet (`Sheet2!Name` for a name scoped to another sheet; hidden names, constants and formulas left out) and the references entered before, a double-click goes to an entry, and the Reference box takes several names or references separated by commas (`Price, Type`, `B14:C22,F19:G30`) |
 | 2026-10-24 | Status bar: a right-click opens a menu that sets which selection statistics show (Average, Count and Sum by default; Numerical Count, Minimum and Maximum are new), kept with the preferences (`view.statusBar`) |
diff --git a/docs/releasing.md b/docs/releasing.md
--- a/docs/releasing.md
+++ b/docs/releasing.md
@@ -286,8 +286,15 @@ row in `ATTRIBUTION.md` (`cargo xtask assets`).
 - `.github/workflows/windows-arm64.yml` (on `release` pushes): packages ARM64 on x64, then
   installs, runs and uninstalls the MSI on a Windows 11 ARM64 runner.
 - `.github/workflows/freebsd.yml`: FreeBSD build + tests (manual run from `release`).
-
-**Only the `release` branch builds.** No build workflow runs on `main` or pull requests; every
-build job checks `github.ref == 'refs/heads/release'`, so a manual run from another branch is
-skipped. Only `packaging-lint.yml` (no build, no secrets) runs on `main` and PRs.
+- `.github/workflows/fuzz.yml` (nightly at 03:17 UTC, or run it by hand from the Actions tab): builds
+  in release and soaks the command-sequence fuzzer at 200 seeds by 200 steps with
+  `GRIDCRAFT_FUZZ_KEEP_GOING` and `GRIDCRAFT_FUZZ_SHRINK`. It is not a required check; a pull request that touches the workflow
+  or the fuzz harness runs it at 20 seeds by 50 steps. A failure fails that run only (never `main`) and uploads the `fuzz-failures`
+  artifact: the full log, `failing-seeds.txt` and each failing seed's shrunk command sequence. Replay
+  with the `GRIDCRAFT_FUZZ_SEED=<seed>` line in the run summary.
+
+**Only the `release` branch builds installers.** No installer build workflow runs on `main` or pull
+requests; every build job checks `github.ref == 'refs/heads/release'`, so a manual run from another
+branch is skipped. Only `packaging-lint.yml` (no build, no secrets) runs on `main` and PRs, besides
+the CI gate and the test-only `fuzz.yml` (nightly, and on PRs that touch it or the fuzz harness).
 - Locally: `cargo xtask ci` (fmt, clippy, tests, assets, layers, wasm, parity check).
