Fleet architecture design: multi-robot operations from day one#48
Conversation
Design doc for Mote's fleet layer: remote operation, live map/location feed, task dispatch, health, and OTA updates, with multiple robots as a day-one assumption. Answers the seven design questions (topology, transport, identity, site/map registry, ops UI, updates, security) with a recommendation and rationale each, a component diagram, explicit v1 non-goals, and a phased milestone breakdown (each milestone sized as one dispatchable task). Recommendations are grounded in existing seams — Sites (sites.py), mote_tasks task_server, the perception inference wire, and the pending prefix.dev release channel — with file:line citations, plus verifiable claims about the candidate transports (Tailscale, Foxglove, MQTT, Zenoh, rmw_zenoh). Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_017pPr82jfBGqViMiUFC5Rrk
MJohnson459
left a comment
There was a problem hiding this comment.
Overall a solid plan. I need more details on how this scales as that's an important consideration. A lot of the choices just have the positives, but lets be clear about the costs as well. Finally this introduces a solid pipeline for the robots, lets also consider what the pipeline looks like for the servers and other components as well.
|
|
||
| This is the crux, and the right answer is **layered**, not a single protocol. | ||
|
|
||
| #### Layer 0 — network substrate: Tailscale (WireGuard mesh) on everything |
There was a problem hiding this comment.
Tailscale sounds good. What are the limitations, costs, and tradeoffs?
There was a problem hiding this comment.
Added a Cost & tradeoffs block under Layer 0 (Tailscale): per-user pricing but robots are tagged devices ($1/device/mo past 50), the coordination plane is cloud-locked, DERP-relay latency when a direct path can't be punched, and a single flat tailnet doesn't multi-tenant. Escape hatch documented: self-hosted Headscale.
| - **Stable identity + encryption for free.** Every device gets a stable MagicDNS | ||
| name and an always-on encrypted link, and the free tier covers **100 devices / | ||
| 3 users with ACLs, MagicDNS, and subnet routing** — comfortably a hobby fleet | ||
| (see Tailscale pricing; **verify** current free-tier limits at adoption time). |
There was a problem hiding this comment.
What is the realistic cost scaling? This is an example project so how would this scale to say 10,000 robots across different sites/customers?
There was a problem hiding this comment.
Added a Scaling section with a per-component regime table and cost estimates. Concretely for 10k robots across sites/customers: Tailscale tagged-device overage lands around **$10k/mo**, which is exactly what forces the Regime-C pivot — drop the full mesh, have robots hold only an outbound TLS connection to a per-region EMQX/HiveMQ broker cluster (scales to millions), self-host Headscale, and move the registry to object storage + CDN. The point of the section: these are swaps behind the same seams (robot_id keying, per-robot topic tree, object-storable bundles), not a redesign.
| Foxglove's free tier is **5 connected devices, unlimited viewers, live | ||
| connections** (see [Foxglove pricing](https://foxglove.dev/pricing)) — enough for | ||
| a small fleet, with a clear paid upgrade if it grows. This is a **buy/adopt**, not | ||
| a build: reimplementing a ROS visualiser + teleop console is a large project with | ||
| no payoff. |
There was a problem hiding this comment.
The same question here - how would the cost scale to 10,000 robots etc?
There was a problem hiding this comment.
Covered in Foxglove's Cost & tradeoffs note and the Scaling table: free tier is 3 users / 5 devices (the 5-device cap is the first wall we hit), then per-seat/per-device paid tiers, Enterprise quote-only. At platform scale the option is Foxglove Enterprise or Rerun-offline + a custom multi-tenant live panel — and because Foxglove is adopted at a seam (it only consumes the robot's ROS graph), swapping it doesn't touch robot code.
| Everything below assumes this substrate. It is why the app-layer choices can stay | ||
| simple. | ||
|
|
||
| #### Layer 1 — rich live data + teleop: Foxglove (adopt, don't build) |
There was a problem hiding this comment.
Foxglove also looks good, what are the tradeoffs? Is this a full replacement for rviz? What alternatives were considered?
There was a problem hiding this comment.
Added Is it a full RViz replacement? No — RViz's interactive-marker primitives stay RViz-only, so we keep pixi run rviz for local dev and use Foxglove for remote/fleet viewing; they don't compete. Also added an Alternatives considered list: RViz (local only), Rerun (open source, but no live multi-user/cloud sessions — wrong for live teleop), rosbridge (superseded), build-our-own (no payoff). Plus the tradeoffs: vendor dependency + per-seat cost + camera/pointcloud bandwidth.
| a build: reimplementing a ROS visualiser + teleop console is a large project with | ||
| no payoff. | ||
|
|
||
| #### Layer 2 — fleet control plane: MQTT (`mote_agent` ↔ fleet server broker) |
There was a problem hiding this comment.
Same question, what are the downsides/tradeoffs for MQTT? Can a web UI actually connect to an MQTT broker directly, assuming we want position updates etc to be displayed?
There was a problem hiding this comment.
Added MQTT limitations: pub/sub only (request/response is a reply-topic+correlation-id convention we implement), no message history/replay (last retained value only — add a DB subscriber or Kafka bridge later), payloads must stay small (maps/frames go over HTTP/Foxglove instead), and Mosquitto doesn't cluster (swap to EMQX/HiveMQ at scale). And yes — a web UI connects to the broker directly via MQTT-over-WebSockets (MQTT.js): the dashboard subscribes to mote/+/pose and mote/+/health over WS and renders live positions with no polling and no middle service. That's now spelled out in Layer 2 and Q5.
| **PNG maps are a distribution asset, not just a rendering one.** Maps are PNG *by | ||
| design* so a browser can render them directly (`sites.py` docstring, `CLAUDE.md`). | ||
| That means the thin fleet UI (Q5) can show a floor map with a live pose dot | ||
| without any tiling/vector pipeline — the registry serves the PNG, the UI draws a | ||
| dot on it. |
There was a problem hiding this comment.
This works for the raw map, but then we will have things like the robot positions, zone areas, etc. Is this still good? What would the limitations be?
There was a problem hiding this comment.
Addressed in Q5 with the actual coordinate math (pose/zone metres → pixel via map.yaml resolution + origin, heading arrow from the quaternion) and an honest limits paragraph: it's a static top-down 2D raster overlay — fine for a roster thumbnail and one/a few robots, but not for dense multi-robot scenes (marker overlap), 3D/point clouds, live costmap/laser layers, or very large sites (needs pan/zoom). The rule: the thin overlay is the fleet-glance view; the moment an operator needs depth they deep-link into Foxglove. We deliberately don't grow it into a second Foxglove.
| The seam makes this small: the dashboard is a thin client over the MQTT control | ||
| plane (retained health topics → roster; publish → dispatch) plus the registry's | ||
| PNG maps. It is emphatically **not** a rebuild of Foxglove, and Foxglove is | ||
| emphatically **not** asked to be the fleet roster (it is per-connection, not a | ||
| fleet database). Each tool does the half it is good at. |
There was a problem hiding this comment.
Just to reiterate on an above point - does MQTT have any limitations in what clients can connect, or what data can be sent?
There was a problem hiding this comment.
Added to Layer 2. Clients: any MQTT client, including browsers over MQTT-over-WebSockets; who may connect is broker auth/ACL (per-robot creds, topic-prefix ACLs — Q7), not anonymous. Data limits: pub/sub only (no native request/response), only the last retained value per topic (no history/replay — that's Kafka's job; add a DB subscriber if we want a time-series), and payloads should stay small (control/telemetry, not maps or frames — those go over HTTP/Foxglove).
| 3. When told to update, the agent **installs the new version alongside the | ||
| current one** (a new pixi prefix / env, pinned by the lockfile — old env | ||
| untouched), then runs a **post-update health check** (does it launch? do the | ||
| controllers come up? a `sim-test`-style smoke gate). |
There was a problem hiding this comment.
This is sensible, but we would need to understand disk and network costs for this strategy, and possibly ways to minimise it.
There was a problem hiding this comment.
Agreed, and added the numbers + mitigations to M5/Q6. Disk: two envs is gigabytes, so keep exactly two slots (current + one rollback) and rely on conda/pixi hard-linking identical packages across envs — a point release costs disk only for changed packages (megabytes, not gigabytes). Network: conda fetches per-package, so an update pulls only the lockfile diff, not the whole env; at scale put a per-site channel mirror/CDN so N robots don't each pull from prefix.dev over WAN. Staging is a separate prefix, so a failed download never touches the running robot. (Flagged the hardlink/per-package behaviour as verify-on-Pi.)
|
|
||
| --- | ||
|
|
||
| ### 6. Updates — one mechanism, the prefix.dev channel, install-alongside + rollback |
There was a problem hiding this comment.
This make sense, but only covers the robot OTA. What about the inference server and fleet server?
There was a problem hiding this comment.
Added a whole "The other pipelines: fleet server & inference server" section. Inference server: same prefix.dev channel, own (torch, no-ROS) env, stateless so updates are blue/green on a second port then flip inference_host port; not availability-critical (robots idle-degrade when it's down). Fleet server: not on the robot OTA channel — it's server software, provisioned via IaC/container-compose and updated by normal blue/green image deploy; its state (retained broker state + registry) is what to back up; robot autonomy survives its downtime. Net: three pipelines, one release channel for the two ROS-software roles.
|
|
||
| --- | ||
|
|
||
| ## Milestones |
There was a problem hiding this comment.
These look good as tasks - be explicit if there are any dependencies between them. Consider scoping so we can dispatch many tasks in parallel.
There was a problem hiding this comment.
Added an explicit dependency graph at the top of Milestones plus per-milestone Depends/Blocks/Parallel-with annotations, and scoped for parallelism: after M0, M1 / M2 / Ms run in parallel (independent transports/roles); M4 and M5 run in parallel after M1; M7 is cross-cutting from M0. Also added Ms (server pipelines) as its own dispatchable task.
Revise docs/design/fleet.md per PR review: - Add a Cost & tradeoffs note to every transport choice (Tailscale, Foxglove, MQTT, inference/latency) so downsides sit next to upsides. - Add a Scaling section (homelab → small fleet → 1k-10k platform) with a per-component regime table, cost estimates, the three real breaking points (Tailscale per-device cost, Mosquitto/Foxglove caps, domain-ID range), and why each pivot is a swap behind a seam, not a redesign. - Add "The other pipelines" section: provisioning + updates + failure posture for the inference server and the fleet server, not just robot OTA. - Foxglove: is-it-an-rviz-replacement (no) + alternatives (Rerun, rosbridge). - MQTT: web-UI-over-WebSockets, broker scaling (EMQX/HiveMQ), pub/sub-only and no-replay limitations. - Zenoh: what it would actually buy and whether it simplifies other decisions. - Identity: server-first enrollment (fixes the id/domain allocation inversion), where the Tailscale key comes from, ROS_DOMAIN_ID reframed as per-LAN not fleet isolation + automatic assignment, full clean-Pi enrollment sequence. - Registry: retained-MQTT new-map notification, server-side validation. - Ops UI: map-overlay coordinate math + honest limits for positions/zones. - Updates: disk/network cost of install-alongside + minimisation (two slots, hardlink dedup, per-package fetch, per-site mirror). - Milestones: explicit dependency graph + parallelism, plus an Ms server- pipelines milestone. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_017pPr82jfBGqViMiUFC5Rrk
|
Thanks — addressed the three overall points across the revision (commit
Individual replies are on each inline thread. Note: I could not push (remote-write guard) — the commit is ready on |
| **Where the Tailscale key comes from.** The overlay join needs a Tailscale **auth | ||
| key** (or OAuth client), minted from the tailnet admin console/API | ||
| ([auth keys](https://tailscale.com/kb/1085/auth-keys)). In provisioning, the fleet | ||
| server (which holds the tailnet API credential) mints a **single-use, tagged, | ||
| pre-authorised** key at enroll time and hands it to the robot, so no human pastes a | ||
| key and the robot lands with the right ACL tag (`tag:robot`). Ephemeral/pre-auth | ||
| keys are exactly this use case. |
There was a problem hiding this comment.
When does this happen? If the robot needs a Tailscale key to join the network, how can it talk to the fleet server?
There was a problem hiding this comment.
Good catch — that was a real bootstrap trap. Fixed by making the Tailscale key a provisioning-time secret baked into the image, not a runtime fetch: at imaging time a single-use pre-auth key (and a short-lived enrollment token) are written into cloud-init user-data. On first boot the Pi runs tailscale up with that baked-in key and joins the tailnet before it ever contacts the fleet server; only then does mote enroll reach the server over the tailnet to get its id/site. So the robot is never in the position of needing the network to fetch the key that gets it onto the network.
| **`ROS_DOMAIN_ID` is a per-LAN multiplexer, not the fleet isolation mechanism.** | ||
| Domain IDs range **0–232 (safe range 0–101), and are a *per-machine/per-LAN* | ||
| concept — a computer runs out of them past ~120 co-located processes** | ||
| ([ROS_DOMAIN_ID docs](https://docs.ros.org/en/jazzy/Concepts/Intermediate/About-Domain-ID.html)). | ||
| They do **not** scale as global fleet isolation, and they don't need to: per Q1 the | ||
| robot's DDS graph never leaves the box, so two robots on *different* LANs never | ||
| interfere regardless of domain. `domain_id` only matters when **two robots share | ||
| one physical LAN** (e.g. a charging room), where distinct IDs stop their local | ||
| graphs from merging. Automatic management is therefore small: the server assigns | ||
| `domain_id` per *site* from a tiny pool (`hash(robot_id) mod 101`, or sequential | ||
| within a site) at enroll — collisions only matter within one LAN, which a per-site | ||
| pool of 101 covers comfortably. (Belt-and-braces alternatives if a LAN ever holds | ||
| >101 robots: a CycloneDDS peer-list XML to disable multicast discovery, or simply | ||
| that co-located robots don't federate anyway.) |
There was a problem hiding this comment.
Is there no way to limit DDS to one machine and avoid this problem?
There was a problem hiding this comment.
Yes — and that's the better answer, so I've switched to it. ROS 2 Jazzy has ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST (replaces the deprecated ROS_LOCALHOST_ONLY), which confines DDS discovery to the local machine. Since the agent and foxglove_bridge run on the same Pi (the agent is the sole egress), nothing off-box needs the robot's DDS graph, so we pin discovery to localhost. That dissolves the domain-ID problem entirely — two robots on one LAN can't see each other regardless of domain, so there's nothing to allocate or manage, and domain_id is dropped from robot.yaml. The one caveat (bench dev where a workstation shares the LAN graph) is a dev-time flag, not fleet ops.
| **Enrollment flow (new robot on a clean Pi) — the full sequence.** The earlier | ||
| draft skipped steps; here it is end to end, and note that **the prefix.dev channel | ||
| delivers the *ROS software*, not the OS-level setup** — the one-time `pixi run | ||
| setup` tasks (udev rules, wifi-powersave, systemd unit install; `CLAUDE.md`, | ||
| `mote_bringup/systemd/install.sh`) still have to run. Two honest options: |
There was a problem hiding this comment.
This shouldn't be a new section - fix the process above, don't repeat it.
There was a problem hiding this comment.
Agreed — merged. There's now a single "Provisioning a new robot — one flow" subsection; the duplicate expanded version is gone.
| - **Golden image (recommended at any real scale):** bake OS + pixi + the setup | ||
| tasks + the Tailscale client into a flashable Pi image, so a new robot is "flash, | ||
| boot, `mote enroll`." The image is versioned like any other artifact. |
There was a problem hiding this comment.
How does this fit with our current raspberry pi image? We use the raspberry pi imager to write an image and use raspberry pi connect for peace of mind.
There was a problem hiding this comment.
Reconciled it with the actual workflow rather than inventing a bespoke image. Raspberry Pi Imager's OS customization now writes cloud-init user-data to the boot partition (Imager 2.0+), which can install packages and run first-boot commands headlessly — so the "golden image" is just our stock Raspberry Pi OS image + a user-data template (Tailscale key, enroll token, first-boot provisioning), not a custom image to maintain. And Raspberry Pi Connect stays as the independent break-glass path (remote shell/screen over Pi's own relay, not dependent on our tailnet or fleet server) — so a robot with a botched key or broken agent is still recoverable. Tailscale = fleet data plane; Pi Connect = peace-of-mind fallback, exactly as today.
| **Its limitations — be honest about where it stops.** It is a static top-down 2D | ||
| raster overlay. It is *fine* for a roster thumbnail and one or a few robots; it is | ||
| *not* the tool for: dense multi-robot scenes (markers overlap, no clustering), 3D |
There was a problem hiding this comment.
I agree no laser or point clouds, but what about hundreds of robots? And large sites? We definitely want pan and zoom, the ability to follow a robot, etc.
There was a problem hiding this comment.
Fair — I'd under-scoped it. Q5 now describes a real interactive 2D fleet map, not a static thumbnail: the PNG is just the basemap, with a pannable/zoomable canvas (Canvas2D → WebGL as marker count grows), click-to-follow-a-robot, marker clustering + viewport culling so hundreds of robots stay smooth (positions are tiny MQTT payloads, so it's a rendering problem, not a data one), and tiled/pyramid basemaps for large sites (pre-rendered per map revision, which the registry already stores immutably), plus a floor switcher. What still deep-links to Foxglove is only the single-robot depth view — 3D, point clouds, live costmap/laser layers, teleop — not the 2D fleet picture.
| 3. Agent **installs the new version alongside the current one** (a new pixi | ||
| prefix pinned by the lockfile — old env untouched), then runs a **post-update | ||
| health check** (does it launch? do controllers come up? a `sim-test`-style smoke | ||
| gate). |
There was a problem hiding this comment.
Will we have CPU space to run two versions of the stack at the same time? How would that work with hardware connections? Or do we need to stop the old stack first, even if we keep the env "active".
There was a problem hiding this comment.
Important clarification, now in Q6: "install-alongside" is two envs on disk, never two stacks running. The Pi doesn't have the CPU for two stacks, and the hardware is exclusive anyway (one process set holds the servo/lidar/camera ports — mote_hardware opens the port in on_activate). So the new env is only staged on disk while the old stack runs; the cutover stops the old stack, then starts the new one, and because that's a brief downtime it's gated to when the robot is idle/charging, not mid-mission. Rollback = restart the old env we kept on disk. "Keep the old env active" meant installed-for-rollback, not running.
| - *Provisioning:* install pixi, join the tailnet (tagged `tag:inference`), run the | ||
| perception servers as a service — `pixi run inference` (Linux systemd) or the | ||
| equivalent Windows service (this box is the Windows/NVIDIA machine the parallel | ||
| inference-server Voro task productionizes; `pixi.toml` `inference`/`inference-rocm` | ||
| envs). | ||
| - *Updates:* **the same prefix.dev channel**, a *different package + env* (the | ||
| torch inference server, no ROS). It is **stateless** (code + model weights only), | ||
| so updates are trivial blue/green: stand up the new version on a second port, | ||
| health-check with a probe request, flip `inference_host` port. Model-weight | ||
| artifacts are versioned alongside. | ||
| - *Failure posture:* **not availability-critical** — the perception nodes idle | ||
| harmlessly when the server is unreachable (`depth_wire.py:70-76`, | ||
| `perception.yaml:10-12`), so the robot degrades (no depth-obstacle layer / no | ||
| open-vocab detect) rather than failing. Update it whenever. |
There was a problem hiding this comment.
This is very manual and prone to error - how can we smooth this out?
There was a problem hiding this comment.
Agreed, "update it whenever" was too loose. The inference server is now a managed compute node: it runs the same mote_agent in a compute-node mode, reports its version/health to the fleet server (shows up in the roster), and its updates are orchestrated/gated/reported like a robot's — not hand-run over SSH. Because it's stateless it's actually easier to automate than the robot: automated blue/green — stand up the new version on a second port, run a probe request as the health gate, flip the served port on success (and push the new inference_host port to its robots), auto-rollback on failure. No hardware exclusivity, so both versions really can run side-by-side during the check.
- Pin DDS to localhost (ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST, Jazzy) instead of allocating per-LAN domain IDs — dissolves the domain-ID problem entirely and drops domain_id from robot.yaml. - Merge the duplicated enrollment sections into one provisioning flow, and fix the Tailscale-key bootstrap paradox: the auth key is a provisioning-time secret baked into cloud-init user-data (Pi Imager), so the robot joins the tailnet before it ever contacts the fleet server. Reconcile with the existing Pi Imager + Raspberry Pi Connect (break-glass) workflow. - Updates: clarify install-alongside is two envs on disk, never two running stacks — hardware is exclusive, so cutover stops old then starts new, gated to when the robot is idle; rollback restarts the retained old env. - Inference server: make it a managed compute node with automated blue/green driven by the orchestrator (was "update it whenever"). - Fleet map: upgrade from a static thumbnail to a real pan/zoom/follow 2D map with marker clustering for hundreds of robots and tiled basemaps for large sites; only 3D/sensor-layers/teleop deep-link to Foxglove. - Remove the Locus reference. Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_017pPr82jfBGqViMiUFC5Rrk
Remove language that narrated the doc's own review history ("earlier draft",
"the review notes", "fixing the inversion", "was too manual") and trim filler
so fleet.md reads as a standalone design document. No design changes.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_017pPr82jfBGqViMiUFC5Rrk
Design review (voro #166)Verdict: approve with comments. All seven questions answered with recommendation + rationale, component diagram, non-goals, v0/v1 milestones sized as dispatchable tasks, verification ledger — every acceptance criterion met. Issues below are fixable in-place; nothing structural. Citation checkAll 26 repo citations were checked against main — 21 exact, 5 have stale line numbers (content accurate in every case):
Worth fixing since the doc's credibility rests on file:line grounding (they also appear in the M0–M6 seam lists). Two of the doc's (verify) items resolve positively and can be struck from the ledger:
Substantive comments (ordered by importance)
Nits
What's strong (keep exactly as-is)
|
#48) - Fix 5 stale file:line citations (verified against main): sites.py flip 254-258, artifact-check 396-406, use-map 432-441; task_server topics 67-68, grammar/status docstring 1-15. Updated in body + milestone seam lists. - Q3: add the CycloneDDS localhost participant-cap caveat (MaxAutoParticipantIndex ~32) as a (verify) item — the whole answer leans on the discovery-range setting. - Q1: replace the overstated exactly-once claim with the explicit single-in-flight-command ack rule, since task/command is a bare String with no correlation id. - Milestones: M0 no longer smuggles in a fleet server — it uses an operator-set id; server allocation + the enrollment endpoint move to M1 (where the registry exists). M1 gains a defined health schema. - Q4/M4: note the map-validation invariant must be an extracted ROS-free module shared with sites.py, not duplicated in the no-ROS fleet server. - Q5/Q7: define the operator/browser broker ACL (dispatch authz lives there since the browser publishes straight to the broker); flag battery as a future health field. - Ledger: strike the now-resolved foxglove-bridge + discovery-range items; M5 states the linux-aarch64 build requirement; soften the "buffers telemetry" line to "re-publishes current state on reconnect". Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_017pPr82jfBGqViMiUFC5Rrk
|
Thanks — thorough review. Addressed all of it (commit Citations — fixed all 5 stale line numbers (verified against main), in the body and the M0–M6 seam lists: Substantive
Nits — "buffers telemetry" softened to "re-publishes current state on reconnect"; M5 dependency line now names the linux-aarch64 build requirement; cost arithmetic confirmed. Verdict noted (approve with comments) — nothing structural changed, all in-place. |
|
Follow-up to the review — three milestone-level decisions for the revision:
|
…aces (#48) Follow-up review decisions: - Dispatch goes through the fleet API, not browser→broker (review comment 5, now decided). Browser is read-only on the broker (roster/pose over MQTT-over-WS); the dispatch box POSTs to the fleet API, which authorizes the operator, writes an audit record, then publishes task/command. Q5, Q7, M3 updated; operator broker credential is now subscribe-only. - Milestones that create a consumed surface must ship it as a documented, versioned schema/spec: added to the Milestones preamble and to M1 (topic tree + health payload), M3 (dispatch API), M4 (bundle/registry API) acceptance. - Fixed two shorthand citations the earlier line-number pass missed in the M4 seam list (:390-400 → :396-406, :426-434 → :432-441). (M0 operator-assigned identity — the other decision — already landed in df2359b.) Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_017pPr82jfBGqViMiUFC5Rrk
|
All three decisions applied (commit
Also fixed two shorthand citations in the M4 seam list that the earlier line-number pass missed ( |
Fleet architecture design doc: docs/design/fleet.md (design only, no implementation).
Answers the seven design questions with a recommendation + rationale each, grounded in existing repo seams (file:line citations) and verifiable tech claims (docs linked; unverified items flagged):
Includes a mermaid component diagram, explicit v1 non-goals (no traffic coordination, no map auto-merge, no cross-robot task allocation, no public-exposed services), and an 8-milestone breakdown (M0-M7) sized so each is one dispatchable follow-up task, with a v0 line (one robot fully remotely operable off-LAN) and a v1 line (second robot enrolled). Appendix lists the (verify) items.
Verified: doc committed on branch design/fleet-architecture (commit a71b0eb); pre-commit hooks pass. No implementation, per task scope.