integration · diff

v1.2.3 to git:20260907.863840e

46 added, 248 removed. Audit A to A.

---
name: integration
- version: 1.2.3
- description: >
- Glue robotics modules into one running system: choose module boundaries,
- pick inter-module communication (ROS 2 topics/services/actions, zenoh,
- gRPC, REST, shared memory), and write solid Dockerfiles and docker-compose
- for robotics workloads. Use when: wiring components together; 'containerize
- this', 'dockerfile', 'docker compose', 'how should these modules talk',
- 'connect the planner to the controller', multi-process or multi-container
- robotics systems. Load after architect chose the stack and environments
- set the env strategy. Not for: choosing the overall stack (architect) or
- single-project env setup (environments).
+ description: Wire robotics modules with ROS 2 interfaces, Docker Compose, or non-ROS transports.
---
- # integration
-
- The glue-layer umbrella for robium. Once `architect` has picked a stack and
- `environments` has decided how a single module runs reproducibly, something
- has to decide how the *modules* find and talk to each other, and how they're
- packaged so the whole system starts up as one thing. This skill owns three
- tightly related decisions: where the module boundaries are, what comms
- crosses each boundary, and how Dockerfiles/docker-compose package and wire
- the result. It does not decide the stack (`architect`) or a single module's
- env reproducibility (`environments`); it decides how already-chosen modules
- become a running system.
-
- ## When to use this skill
-
- - Wiring two or more components together and the question is "how should
- these talk": a planner to a controller, a perception node to a policy, a
- sim to an app container.
- - The trigger phrases in the description: 'containerize this', 'dockerfile',
- 'docker compose', 'how should these modules talk', 'connect the planner to
- the controller'.
- - Any multi-process or multi-container robotics system, even a simple
- two-container sim+app pair.
- - Cross-references: go to the sibling skill instead when the question is:
- - The overall stack hasn't been chosen yet → `architect` (load first; it
- routes here once the stack and modules are known).
- - A single module's reproducibility (uv vs Docker, GPU passthrough, base
- image) rather than how modules talk to *each other* → `environments`.
- This skill assumes each module's own environment is already decided and
- focuses on cross-module wiring.
- - ROS 2 package/node/launch-file internals within one module → `ros2`.
- - Remote visualization transport (Foxglove bridge, rosbridge) →
- `foxglove`.
-
- ## Key directives
-
- - **Delegation posture: embed.** Module-boundary heuristics, the comms
- decision table, and Dockerfile/compose patterns for robotics workloads
- live in this skill and its references; this is core, every-multi-module-
- build content, not a thin pointer elsewhere.
- - **Prefer ROS 2 native comms inside a ROS system.** <!-- id: prefer-native-ros2-comms --> If two modules are both
- ROS 2 nodes in the same system, use topics/services/actions over DDS by
- default. Reach for a non-ROS transport (zenoh, gRPC, REST, shared memory)
- only when crossing a *system* boundary: a non-ROS peer, another team's
- service, a cloud endpoint, or a boundary DDS genuinely can't cross without
- extra config. Don't introduce a second transport inside a single ROS
- system "just in case."
- - **One process per container, unless there's a stated reason.** <!-- id: one-process-per-container --> Each
- container should run one supervisable unit (one ROS 2 node, or one tightly
- coupled node group that only makes sense together). Bundling unrelated
- nodes into one container to save a Dockerfile hides failures and blocks
- independent scaling/restart; if you do it anyway, write down why in the
- compose file or brief.
- - **DDS discovery across containers must be configured explicitly, never
- assumed.** <!-- id: dds-discovery-explicit-config --> Docker's default bridge network does not forward the multicast
- UDP traffic DDS's default discovery relies on; "it worked when I ran both
- nodes on the host" does not imply it will work in compose. Pick and state
- one discovery mechanism (host networking, a Discovery Server, static
- peers, or a zenoh router); see `references/compose-patterns.md`. This is
- the single most common integration bug in robium builds; treat an
- unconfigured discovery setup as a defect, not a "works on my machine" risk
- to defer.
- - **Never write RMW/DDS/zenoh version or default-status claims from memory.** <!-- id: no-rmw-facts-from-memory -->
- These have changed release-to-release (zenoh support, discovery-server
- defaults). Verify against [docs.ros.org](https://docs.ros.org/) and the
- [ros2/rmw_zenoh](https://github.com/ros2/rmw_zenoh) repo before repeating
- a claim in a real project; see `references/comms-selection.md` for what
- was verified and when.
-
- ## Quick start
-
- **1. Confirm the prerequisites are settled.** `docs/architecture-brief.md`
- should already record the chosen stack (`architect`) and each module's env
- strategy (`environments`). If either is missing, route back there first.
-
- **2. Decide module boundaries.** Split by **rate + failure domain**; see
- Decision guidance below. Write the resulting module list into the brief's
- module-breakdown section.
-
- **3. Pick comms per module pair.** Use the comms-choice table in Decision
- guidance; the default inside one ROS 2 system is native topics/services/
- actions. See `references/comms-selection.md` for the full picture including
- zenoh, gRPC, REST, and shared memory.
-
- **4. Write one Dockerfile per module.** <!-- id: multistage-dockerfile-per-module --> Multi-stage: a build stage with the
- full toolchain (colcon, compilers, apt build deps), a slim runtime stage
- that copies only the built `install/` output and runtime deps. See
- `references/dockerfile-guide.md` and `examples/Dockerfile.multistage-ros2`.
- This is a different concern from `environments`' Dockerfile.ros2 example
- (single-environment reproducibility); this one is about build quality for
- a module that ships as part of a multi-container system.
-
- **5. Wire modules with docker-compose.** <!-- id: compose-explicit-domain-discovery --> One service per container, explicit
- network mode, explicit `ROS_DOMAIN_ID`, and an explicit DDS discovery
- mechanism. See `references/compose-patterns.md` and
- `examples/docker-compose.ros2-app.yml`.
-
- **6. Verify discovery, not just that containers start.** <!-- id: verify-discovery-not-just-start --> `docker compose up`
- succeeding is not proof nodes can see each other; run `ros2 topic list` /
- `ros2 node list` from inside each container (or `ros2 doctor --report`) and
- confirm cross-container topics actually appear before calling the wiring
- done.
-
- ## Decision guidance
-
- ### Comms-choice table
-
- | Relationship | Default choice | Why / when to deviate |
- |---|---|---|
- | Same process, same language | Direct call / shared object, no IPC<!-- id: same-process-direct-call --> | Don't wrap this in ROS 2 messaging "for consistency": it adds serialization cost and a node boundary for no isolation benefit. |
- | Same-host, both ROS 2 nodes | ROS 2 topics/services/actions (default RMW, default DDS discovery)<!-- id: same-host-ros2-native-transport --> | Native; multicast discovery works unmodified on the host network or a single non-networked process. This is the default; deviate only for a stated reason. |
- | Cross-host or cross-container, both ROS 2 (one system) | Still ROS 2 topics/services/actions, but with discovery **explicitly** configured: host networking, a Fast DDS Discovery Server, static CycloneDDS peers, or rmw_zenoh with a router<!-- id: cross-container-ros2-explicit-discovery --> | Multicast usually doesn't cross container/host boundaries by default. Don't reach for a non-ROS transport just because discovery needs setup; fix discovery, keep the transport native. See `references/comms-selection.md`. |
- | Cross-boundary to a non-ROS peer (another team's service, a cloud API, a system that doesn't speak ROS/DDS) | gRPC (typed, streaming-capable, low overhead) or REST (simplest, most interoperable) at the boundary<!-- id: non-ros-boundary-grpc-rest --> | This is the one case where a non-ROS transport is the *default*, not the exception; don't leak DDS/RMW specifics across an org or system boundary. |
- | High-throughput same-host data (large images, point clouds, mostly within one host) | Shared memory: ROS 2 intra-process comms / loaned messages, or zenoh's SHM path if already on rmw_zenoh<!-- id: high-throughput-shared-memory --> | Adopt only when profiling shows serialization/copy overhead is the actual bottleneck, not a default starting point. |
-
- ### Module-boundary heuristics: split by rate + failure domain
-
- - **Rate mismatch** <!-- id: rate-mismatch-split --> → split. A 1 kHz control loop and a 10 Hz planner have
- no business sharing a process/executor; bundling them risks the slow one
- starving the fast one (or the fast one wasting the slow one's CPU budget).
- Each rate tier gets its own node/process so it can be scheduled and
- rate-limited independently.
- - **Failure domain** <!-- id: failure-domain-split --> → split. If module A crashing should not take down
- module B (a perception model that OOMs shouldn't kill the safety
- controller; a sim shouldn't kill the app that drives it), they're separate
- processes/containers so a restart policy can target just the failed one.
- - **Neither applies** <!-- id: neither-applies-keep-together --> → keep together. Two tightly coupled pieces with the
- same rate and no independent failure story (e.g., a filter and the node
- that owns it, with no reason either would run or fail alone) add pure
- comms overhead if split; one node, one container, is the simpler and
- correct default.
- - Write the resulting boundary list, and the rate/failure reasoning behind
- each split, into the brief's module-breakdown section; an undocumented
- boundary decision gets silently re-litigated by the next person.
-
- ## Platform gotchas
+ # Integration
- - **Docker's default bridge network drops DDS multicast.** <!-- id: docker-bridge-drops-dds-multicast --> The default
- Simple Discovery Protocol most DDS implementations use relies on
- multicast UDP, and Docker's default bridge network does not forward it
- between containers; two ROS 2 nodes in separate `docker compose`
- services will not discover each other with zero extra config. Use
- `network_mode: host` (Linux only) for the simplest fix, or an explicit
- discovery mechanism (Discovery Server, static peers, zenoh router) when
- host networking isn't available. See `references/compose-patterns.md`.
- - **`network_mode: host` is Linux-only in the way this skill means it.** <!-- id: network-mode-host-linux-only -->
- Docker Desktop on macOS/Windows does not give containers the host's real
- network namespace the way Linux does; don't rely on host networking as
- the discovery fix on a macOS/Windows dev machine (consistent with
- `environments`' "macOS has no native ROS 2" gotcha: Mac ROS 2 dev is
- already inside a VM/Docker layer). Use an explicit CycloneDDS peers list,
- a Discovery Server, or rmw_zenoh instead in that case; [this Husarnet
- writeup](https://husarnet.com/blog/ros2-docker) has a worked example of
- disabling multicast and pointing CycloneDDS at static peers.
- - **rmw_zenoh needs its own router process.** <!-- id: zenoh-needs-router-process --> Switching `RMW_IMPLEMENTATION`
- to `rmw_zenoh_cpp` doesn't work standalone: it requires a Zenoh router
- (`ros2 run rmw_zenoh_cpp rmw_zenohd`) running and reachable, or peer-to-
- peer config if deliberately skipping the router. In compose, that's its
- own service, not an assumption baked into one node's entrypoint. See
- `references/comms-selection.md`.
- - **`ROS_DOMAIN_ID` collisions are silent.** <!-- id: ros-domain-id-collision-silent --> Two unrelated ROS 2 systems on
- the same host-network segment with the same (default `0`) domain ID will
- discover and cross-talk with each other. Set an explicit, project-unique
- `ROS_DOMAIN_ID` in the compose file for every robium project, the same way
- you'd pick a non-default port. **But a pinned constant is a
- one-stack-per-host assumption**: the moment something runs *concurrent
- copies* of the same stack (an orchestrator spawning a container per
- visitor, a parallel test matrix, several sims on one CI box), that shared
- constant merges every copy's ROS graph into one. The symptom is not a
- discovery error but physics nonsense: two Gazebo `/clock` publishers on
- one graph, and
- `[robot_state_publisher] Moved backwards in time, re-publishing joint
- transforms!` flooding forever. Whatever spawns the instances must assign
- a **per-instance domain ID** (lowest free in 1–200, recorded on the
- container so it can be excluded while in use) instead of inheriting the
- project constant. Verified 2026-07-13 (nav-trial demo orchestrator).
+ Every module boundary needs an explicit contract: why the boundary exists, what
+ crosses it, how peers discover each other, and how failure is observed.
- ## Customization
+ ## Draw boundaries for a reason
- - **Fewer/more than two services:** the compose example is a minimal
- two-service (sim + app) shape; extend the same pattern (one service per
- module, shared `ROS_DOMAIN_ID`, one explicit discovery mechanism) for
- additional nodes rather than inventing a different wiring style per
- service.
- - **Adding a non-ROS peer:** add the boundary transport (gRPC/REST) as its
- own service or sidecar rather than piping it through the DDS network; keep
- the ROS-internal comms native per the key directive.
- - **Swapping the discovery mechanism:** `references/compose-patterns.md`
- covers host networking, Fast DDS Discovery Server, static CycloneDDS
- peers, and rmw_zenoh; pick one per project based on whether it's
- same-host (host networking is simplest) or cross-host/cross-network
- (Discovery Server or zenoh scale better); don't mix mechanisms within one
- project.
- - **Heavier build (large ML deps in one module):** extend
- `examples/Dockerfile.multistage-ros2`'s builder/runtime split rather than
- collapsing back to a single stage; see `references/dockerfile-guide.md`
- and cross-reference `environments`' Dockerfile.gpu-ml example for the
- GPU-specific multi-stage shape.
+ - Split components with different rates when sharing an executor or process can
+ starve the faster loop.
+ - Split failure domains when one component must restart, scale, or exhaust
+ memory without taking down another.
+ - Keep tightly coupled code together when neither rate nor failure isolation
+ justifies serialization and deployment overhead.
+ - Make each container a supervisable unit. Multiple processes may belong
+ together, but their shared lifecycle should be intentional.
- ## References
+ ## Choose the boundary contract
- - `references/comms-selection.md`: the full comms decision: ROS 2
- topics/services/actions in depth, rmw_zenoh and zenoh-plugin-ros2dds
- status and setup, gRPC/REST for non-ROS boundaries, shared memory, with
- verified sources.
- - `references/dockerfile-guide.md`: Dockerfile quality for a robotics
- module: multi-stage build/runtime split, one-process-per-container,
- layer-cache ordering, signal handling for `ros2 launch`.
- - `references/compose-patterns.md`: docker-compose wiring: network modes,
- `ROS_DOMAIN_ID`, DDS discovery mechanisms across containers/hosts,
- `depends_on`/healthchecks, volumes.
- - `examples/docker-compose.ros2-app.yml`: minimal two-service (sim + app)
- ROS 2 compose file with explicit DDS discovery config (status:
- unverified).
- - `examples/Dockerfile.multistage-ros2`: multi-stage build/runtime split
- for one ROS 2 module, complementary to `environments`'
- Dockerfile.ros2 example (status: unverified).
- - Upstream: [ROS 2 Topics vs Services vs
- Actions](https://docs.ros.org/en/rolling/How-To-Guides/Topics-Services-Actions.html),
- [rmw_zenoh](https://github.com/ros2/rmw_zenoh), [Fast DDS Discovery
- Server](https://fast-dds.docs.eprosima.com/en/latest/fastdds/ros2/discovery_server/ros2_discovery_server.html),
- [Docker Compose networking](https://docs.docker.com/compose/how-tos/networking/).
- Sibling skills: `architect` (routes here), `environments` (single-module
- env strategy, not duplicated here), `ros2` (node/launch internals),
- `foxglove` (remote viz transport).
+ - Inside one ROS 2 system, default to ROS topics, services, and actions.
+ - Crossing hosts or containers does not by itself require a new application
+ protocol; configure ROS discovery explicitly first.
+ - Use gRPC or REST at a genuine non-ROS system or organizational boundary.
+ - Use shared memory or zero-copy paths only after measurement shows copying is
+ the bottleneck.
+ - Treat names, schemas, QoS, time, backpressure, health, restart behavior, and
+ ownership as part of the interface.
- ## Changelog
+ Read [communication selection](references/comms-selection.md) when choosing
+ between ROS 2, DDS discovery options, Zenoh, gRPC, REST, or shared memory.
- <!-- One dated line per battle-tested change, added by skill-author hardening sessions. -->
+ ## Package the running system
- - 1.2.3 (2026-08-03): style pass; removed em dashes throughout (no content changes).
+ - Use separate build and runtime stages for shipped modules; leave compilers
+ and build-only dependencies behind.
+ - Give each service a health signal that proves readiness, not merely a live
+ process.
+ - Make network mode, `ROS_DOMAIN_ID`, discovery, volumes, devices, and
+ shutdown behavior visible in the composition.
+ - A successful `docker compose up` proves process creation, not communication.
+ Verify real cross-boundary messages.
- - 1.2.2 (2026-08-01): decision-table rows anchored (learning-engine Phase 1 follow-up); no content changes.
+ Read [the Dockerfile guide](references/dockerfile-guide.md) for build/runtime
+ structure and signal handling. Read [compose patterns](references/compose-patterns.md)
+ for DDS discovery, health, domains, and networking. Use the examples only as
+ starting shapes and verify them in the target environment.
- - 1.2.1 (2026-08-01): anchor IDs added to claim-bearing items (learning-engine Phase 1); no content changes.
+ Use `environments` for one module's reproducibility and GPU/runtime contract.
+ Use `ros2` when the failed interface is within a healthy ROS graph. Use
+ `foxglove` only when the boundary is specifically remote visualization.
- - 1.2.0 (2026-07-13): nav-trial demo absorption: corrected the
- `ROS_DOMAIN_ID` directive, which prescribed a pinned project constant
- unconditionally. That is a one-stack-per-host assumption; concurrent
- copies of a stack (orchestrator-per-visitor, parallel CI sims) merge
- their graphs on the shared constant and surface as duplicate `/clock`
- publishers + a `Moved backwards in time` flood. Spawners must assign a
- per-instance domain ID. Same nuance added to compose-patterns.md, whose
- examples hardcode 42.
+ ## Done
- - 1.1.0 (2026-07-11): nav-trial absorption: "Compose sharp edges" section
- in compose-patterns.md (profile-gated `build` no-op, shallow YAML merge
- keys dropping anchor env vars, `exec` bypassing ENTRYPOINT). macOS
- DDS-multicast gotcha confirmed load-bearing in a real build (drove a
- one-container-per-scenario design, verified end-to-end).
+ - Each split has a rate, failure, deployment, or ownership reason.
+ - Every boundary has an explicit protocol and interface contract.
+ - Peers discover each other in the real host/container topology.
+ - Health and shutdown behavior are observable and scoped to the failed unit.
+ - A real message or request proves each changed boundary end to end.