Skip to content

Making of Fleet Dispatch

LIVING DOCUMENT — update this file whenever a ticket lands a change worth a beat. Plain version: docs/plain/making-of.md. Beats below are drawn from git log and the BUILD-REPORT-*.md files this repo keeps per major feature; nothing here is invented.

Beat 1 — the core: a signed cross-PC command queue (09f3ce6)

Fleet Dispatch started as exactly what its name says: an HMAC-signed queue for running a command on another house PC, with a usage-governor gate so a dispatched job could not run during a RED or BLACKOUT tier. enqueue wrote a signed job to a shared OneDrive folder; a listener drained it, verified the signature, and ran it as claude -p or a PowerShell command.

Beat 2 — the LAN peer path (1675390)

enqueue/listen alone meant every dispatch round-tripped through OneDrive sync latency. The persistent listener (serve) added a direct HTTP path (send/dispatch) with its own security review pass — bearer-token pre-check before HMAC, replay guards, and the loopback + LAN-only bind posture that is still in place today.

Beat 3 — CI hardening (3bd6fc6 .. a3bd9ee)

A run of small, individually unglamorous CI fixes: pinning the .NET SDK install into a runner-writable directory (the self-hosted runner account can't write C:\Program Files\dotnet), moving DOTNET_INSTALL_DIR to step level (job-level runner.* context isn't valid), discovering the runners have no pwsh and switching to Windows PowerShell, and fixing the CLI's AssemblyName so dotnet publish actually emits FleetDispatch.exe instead of a name mismatch. Small fixes, but every one of them was a prerequisite for CI being trustworthy at all — a green CI that silently didn't build the right artifact is worse than a red one.

Beat 4 — LAN address selection, three times (48f4dbf, 7b3ff18, 8bcd609, 1345d7c, ff0dd1e, FLT-84)

The fleet sits behind wireless NETGEAR extenders that each run their own DHCP — a lease renewal or an SSID roam can silently change a box's 10.0.0.x address and orphan the pinned host, the URL reservation, and the peer registry entry. This took several passes to get right: first stop virtual-switch NICs (Hyper-V/WSL) from winning the "preferred LAN NIC" pick, then switch to preferring the Tailscale 100.64.0.0/10 address entirely (stable across the extenders), then fix the tailnet CIDR's netmask encoding (Windows stores it differently than expected), then scope the tailnet firewall rule to the Tailscale interface specifically (a RemoteAddress-only filter is a spoofable source-IP check, not a real boundary), then finally fix repair-fleet-listener.ps1 to actually run on Windows PowerShell 5.1 (it had been written assuming pwsh).

Beat 5 — the harness kind registry (62ac09c, 9cede5b, polyharness-parity P2)

Before this, "what kind of job can run" was two independently hardcoded claude|pwsh checks — one in the queue, one in the CLI — that could drift apart. HarnessRegistry replaced both with one source of truth and added four new kinds in the same pass: codex, agy, grok, ollama, plus odysseus (an HTTP kind executed listener-side against a fixed loopback port). The follow-up council review (9cede5b) caught and fixed an SSRF path in the odysseus kind, a type-confusion bug, and a resource leak — the kind of finding a second-pass review is specifically for.

Beat 6 — run_agent, the uniform agent contract (c85884c .. 04f7ee7, FLT-2)

A new kind, run_agent, gave the dispatcher a way to run a full agent session (worktree creation, timeout, retry, diff capture, validation command) rather than a bare CLI invocation. Landed with its own council review pass that caught status spoofing, a ref-injection path, and unbounded I/O; follow-ups fixed a codex agent_message schema mismatch, reaped descendant processes orphaned by an exited shell (via a Windows job object), and pinned agy to its worktree with --add-dir.

Beat 7 — cancel and steer lanes (b5dc6a8, 696e9ef, 49576ae, d2ec1ab, 24bc55c, FLT-70/71/101/103)

696e9ef is the FLT-70/71 incident fix that shows up throughout this codebase's comments today: schema-version skew was being reported as a generic HMAC failure, and — separately — a listener that failed to bind was still advertising itself as live in the peer registry, so a dead box looked healthy. Both got fixed with the same philosophy that's now load-bearing everywhere in this repo: name the failure precisely, and never claim success before the thing you're claiming is actually true.

The cancel lane (b5dc6a8) added a signed marker file that could kill an in-flight job — checked before both the availability and governor gates, since cancel is a control action. The steer lane started as a "kill and resubmit with new context" mechanism (49576ae, FLT-101 phase 1), then upgraded twice: first to a live, steerable Claude session via the stream-json protocol (d2ec1ab, FLT-103 phase 2) so a steer message reaches an actual in-flight conversation instead of restarting it, then to the same capability for Codex via its app-server protocol's turn/steer (24bc55c, FLT-103 cycle 2) — the strongest steering path in the fleet as of this writing.

Beat 8 — the RAM reaper (a3ca967, 783b7b5, FLT-55)

A dry-run, three-bucket process-attribution reaper for reclaiming RAM across the fleet, hardened in a follow-up council pass for attribution safety (making sure a reap decision correctly attributes a process to the bucket that's actually responsible for it before acting on it).

Beat 9 — drift-aware config deployment (86a620b, FLT-51)

Hash-verified, drift-aware deployment of tracked skills and peer config across the fleet — landed right before the two features below, giving the fleet a general "push config safely" mechanism independent of the job-dispatch path.

Beat 10 — the repo registry scanner (b40d522, FLT-126)

The fleet-wide repo inventory that Mission Control's RepoRegistryReader consumes needed a per-PC producer. RepoRegistryScanner scans approved roots only (never a full-disk walk), classifies main checkouts vs. linked worktrees via git rev-parse --git-dir/--git-common-dir, folds every worktree into its main checkout's entry via git worktree list --porcelain (so a worktree living on a completely different drive, e.g. C:\wt\..., still attaches correctly instead of leaking out as its own fake top-level repo), and signs the output with a byte-for-byte mirror of fleet-mission-control's own RepoRegistryReader.CanonicalString/Sign — matching JsonSerializerOptions, field order, and join format exactly, verified against a literal test copy of the reader's logic (ReaderMirror.cs) since there's no project reference across the two repos. Wired into serve as a boot-plus-daily-timer background loop; deliberately given no dedicated HarnessKind for on-demand triggering — it rides the existing pwsh kind instead, since a first-class kind would have meant touching three more subsystems for a command with no payload and no steering need.

Beat 11 — unavailable-mode: the hard gate, the detector, and jobClass (79d336e, FLT-128/129)

The newest major feature (and the day this doc set was written): the fleet had no way to distinguish "safe to run a heavy job on this PC right now" from "Andrew is mid-game or mid-stream on it." Built as two independently-verifiable pieces:

  • AvailabilityGate.Evaluate — a pure, memory-free function checked fresh on every single job, immediately before execution, with absolute authority the governor cannot override.
  • AvailabilityDetector — a separate always-on ~4s poll loop applying re-availability hysteresis (90s gaming / 120s streaming) purely for the published heartbeat, so Mission Control's view doesn't flicker, while the hard gate stays deliberately blind to that hysteresis.

The jobClass field this gate reads had to become part of the signed canonical, not an optional extra — an unsigned jobClass would have let anyone with queue-write (not the fleet key) relabel a heavy job as control to dodge the gate while the PC was gaming, which is a live security bypass, not a compatibility nicety. That's why this feature is wire-schema v2: adding jobClass to the canonical is exactly the kind of breaking change DispatchJob.SchemaVersion exists to make nameable, and this is the first time that mechanism has actually been exercised for real since it was built as a safety net back in Beat 7. 436 tests passing (60 new), 0 build warnings.

What's next

  • Mission Control's builder side of FLT-128/129 (reading availability off the heartbeat, writing the manual override file) — tracked on Mission Control's own board, not here.
  • The documented-but-not-built gaps: fullscreen+GPU detection for an unlisted game, pause-in-flight for a job that starts before a gaming/streaming trip, gating the repo-scan timer loop, and an admin UI for the game allowlist.
  • Promoting the Stryker mutation score from report-only to an enforcing gate once the self-hosted runner's VsTest-socket-collapse-as-killed-mutant inflation issue is resolved.