AGENTS.md¶
This file provides guidance to AI coding agents (OpenHands, Claude Code, etc.) when working with the AirStack repository.
Quick Start for AI Agents¶
Project: AirStack - Comprehensive autonomous aerial robotics stack developed by AirLab CMU
Stack: ROS 2 Jazzy | Docker-based development | Isaac Sim with Pegasus extension | Microsoft AirSim (legacy, UE4) | Field robotics
Primary Goal: Enable agents to understand the architecture, implement new algorithms/modules, and integrate them correctly into the layered autonomy stack.
Repository Purpose¶
AirStack provides a complete end-to-end system for autonomous drone operations including: - Modular autonomy stack (interface, sensors, perception, local planning, global planning, behavior) - High-fidelity simulation environments (Isaac Sim with Pegasus extension, Microsoft AirSim legacy/UE4) - Ground Control Station for mission planning and monitoring - Multi-robot coordination capabilities - Hardware deployment tools
The architecture is designed to allow easy swapping of algorithm modules (e.g., different planners, controllers, perception systems) through a standardized ROS 2 interface pattern.
Modular AirStack (RFC #379/#380)¶
AirStack is transitioning from a monolith to modules (thin external repos with a small module.yaml, pulled on demand: airstack module add <url> --version <pin>), stacks (self-contained topology folders under stacks/ — pinned modules.repos, XML launch entry points, CI-observed wiring.md), and fleets (config/fleets/ — who exists, which vehicle, which stack, which ground hosts). Before touching bringup launch files, read:
- Docs: Module & Stack Catalog (marketplace, generated by
tools/gen_docs_catalog.py) · Modules · Stacks · Fleets · Module CI · Modular AirStack Walkthrough - Skills:
create-module,create-stack,integrate-module-into-layer(now stack-centric),configure-multi-robot(fleets) - Registry: castacks/airstack-modules-index — one YAML per registered module/stack; DECLARED compat in the entry, VERIFIED compat CI-stamped under
compat/. Registering a module is TWO merges: the registry-repo PR and the trunk PR (fixture entries undertests/meta/fixtures/modules_index/+ regenerateddocs/modules/). The docs deploy regenerates the catalog from the live registry, so a merged trunk PR with an unmerged registry PR silently drops the module from the published site — merge the registry PR first. The trunk half is automated: the dailysync-modules-indexworkflow opens the trunk sync PR (dispatch it manually for an immediate sync), and the develop docs deploy raises adocs-catalog-driftissue whenever git and the live registry disagree (details: theextract-moduleskill's registration step)
Repository Architecture¶
High-Level Structure¶
AirStack/
├── robot/ # Onboard autonomy stack (ROS 2 Jazzy)
│ └── ros_ws/src/ # Layered autonomy modules
│ ├── interface/ # Hardware interface & safety
│ ├── sensors/ # Sensor integration
│ ├── perception/ # State estimation & perception
│ ├── local/ # Local planning, world models, control
│ ├── global/ # Global planning & mapping
│ └── behavior/ # High-level mission execution
├── simulation/isaac-sim/ # Isaac Sim (Pegasus extension)
├── simulation/ms-airsim/ # AirSim (UE4 + PX4 SITL)
├── gcs/ # Ground Control Station
├── common/ # Shared packages & utilities
├── docs/ # MkDocs documentation
├── mkdocs.yml # MkDocs config file
├── tests/ # System tests (pytest) + metrics reporting
├── .github/
│ ├── workflows/ # GitHub Actions CI (system-tests, docker-build, etc.)
│ └── orchestrator/ # OSMO-backed ephemeral self-hosted runners
└── .agents/skills/ # Detailed workflow guides for agents
Layered Autonomy Pattern¶
The autonomy stack follows a layered architecture where data flows through processing stages:
Each layer has:
- Module packages: Individual algorithm implementations (e.g., droan_local_planner)
- Stack entry launch files: The launch topology and topic remapping live in the selected stack (e.g., stacks/full_default/launch/stack.launch.xml), not in per-layer bringup packages (the old local_bringup-style packages were removed)
Key Insight: Understanding "what connects to what" is critical. See Integration Checklist and System Architecture.
Standard Topic Patterns¶
Modules communicate via ROS 2 topics. Common standard topics:
| Topic Pattern | Type | Purpose |
|---|---|---|
/{robot_name}/odometry |
nav_msgs/Odometry | Robot state estimation |
/{robot_name}/global_plan |
nav_msgs/Path | Global waypoint path |
/{robot_name}/trajectory_controller/trajectory_override |
airstack_msgs/TrajectoryXYZVYaw | Direct trajectory commands |
/{robot_name}/trajectory_controller/trajectory_segment_to_add |
airstack_msgs/TrajectoryXYZVYaw | Planned trajectory segment |
/{robot_name}/trajectory_controller/look_ahead |
geometry_msgs/PointStamped | Look-ahead point for planning |
Note: Topics are remapped in bringup launch files to connect modules. Input/output topics should be configurable via launch arguments.
See Integration Checklist for comprehensive topic conventions.
Common Workflows (Skills)¶
For detailed step-by-step instructions, refer to the .agents/skills/ directory:
| Skill | When to Use |
|---|---|
| add-ros2-package | Creating a new algorithm module package |
| add-task-executor | Implementing a task executor as a ROS 2 action server |
| create-module | Packaging a capability as a standalone module repo (thin module.yaml manifest, canonical-default launch args, test_stack/, CI caller) per RFC #379 |
| extract-module | Extracting an existing in-tree capability into a standalone module repo: history extraction, trunk-removal checklist, duplicate-package sequencing, stale-colcon-cache warning, module CI |
| create-stack | Creating a stack folder (airstack stack new), editing entry launch files, bootstrapping wiring.md, split stacks + bridge.yaml |
| integrate-module-into-layer | Integrating a ROS 2 module into a stack (entry launch file, single-locus wiring rule, wiring.md regeneration) — the old layer-bringup workflow is legacy |
| write-launch-file | Authoring ROS 2 launch files with AirStack conventions (ROBOT_NAME namespacing, topic remapping, allow_substs) |
| write-isaac-sim-scene | Creating custom simulation scenes |
| visualize-in-foxglove | Adding topic visualization to Foxglove/GCS |
| attach-gossip-payload | Broadcasting custom ROS messages to peers via PeerProfile gossip payloads |
| debug-module | Autonomous debugging of ROS 2 modules |
| update-documentation | Documenting new modules and updating mkdocs |
| write-mkdocs-documentation | Writing effective MkDocs documentation: organization, flow, visuals, markdown syntax, navigation structure, module vs system docs, quality checks |
| docker-build-profiles | Compose profiles and build args — adding a robot profile, YAML quoting, the L4T/Jetson build chain, module-owned deps entering images via module layers (airstack module lock --build) |
| test-in-simulation | End-to-end simulation testing of a module |
| add-unit-tests | Adding Python or C++ unit tests to a ROS 2 package (co-located test/ dir listed in colcon_unit_test_packages.yaml, CI workflow, extending to sim/GCS) |
| run-system-tests | Running the pytest system test harness (marks, MetricsRecorder, /pytest PR trigger) |
| add-behavior-tree-node | Creating behavior tree nodes |
| use-airstack-cli | Using the airstack CLI and the non-interactive docker exec pattern |
| configure-multi-robot | Setting up multiple robots — fleet files (--fleet, heterogeneous + split placement) and legacy NUM_ROBOTS, ROBOT_NAME namespacing, ROS_DOMAIN_ID isolation |
| bump-version-and-release | Bumping .env VERSION and recording the change in the versioned Release Notes (docs/release_notes/index.md) before merge to clear the version-check gate — every hotfix on main must add its own dated ## X.Y.Z — YYYY-MM-DD patch-notes section |
| capture-discovered-knowledge | After long context-discovery / surprising findings, persist to AGENTS.md or a new skill so the next agent doesn't redo the work |
| use-feature-notebook | At the start of EVERY feature implementation: create notebook/NNN-feature-slug/design_spec.md, store test artifacts under results/, write results/results_summary.md, and populate the PR from it |
Agent Workflow Example:
1. Create a notebook entry — follow use-feature-notebook to write notebook/NNN-feature-slug/design_spec.md (problem context, proposed implementation, lettered test plan) before writing code
2. Study reference implementation for module type
3. Follow add_ros2_package.md to create package structure
4. Implement algorithm with proper topic interfaces
5. Follow integrate_module_into_layer.md to add to bringup
6. Follow update_documentation.md to document
7. Follow debug_module.md and test_in_simulation.md to verify, saving artifacts under notebook/NNN-feature-slug/results/<letter>-<section>/
8. Write results/results_summary.md (embedded tables + figures) and populate the PR body from it
Also see: AI Agent Quick Guide
Feature Notebook (notebook/)¶
Every feature an agent implements gets a numbered entry under notebook/ at the repo root — a local lab journal that survives the agent session:
notebook/001-add-new-planner/
├── design_spec.md # BEFORE coding: problem context from the session, proposed implementation, lettered test plan
└── results/
├── results_summary.md # AFTER testing: self-contained doc with embedded tables + figures, per-section verdicts
├── a-planner-core/ # Raw artifacts per test-plan section (letters match design_spec.md)
└── b-planner-hyperparameters/
notebook/ is gitignored — local-only on each developer's machine. Never commit it or reference its paths from committed code. Its content leaves the machine one way: the feature's PR description is populated from design_spec.md (motivation, what changed) and results_summary.md (validation tables, figures uploaded as PR attachments).
Full workflow and templates: .agents/skills/use-feature-notebook
Reference Implementations¶
Study these well-structured modules as examples for different types:
| Module Type | Reference Package | Location |
|---|---|---|
| Local Planner | DROAN Local Planner | robot/ros_ws/src/local/planners/droan_local_planner |
| Local World Model | Disparity Expansion | robot/ros_ws/src/local/world_models/disparity_expansion |
| Controller | Trajectory Controller | robot/ros_ws/src/local/controls/trajectory_controller |
| Global Planner | Random Walk | robot/ros_ws/src/global/planners/random_walk |
| Global World Model | VDB Mapping | robot/ros_ws/src/global/world_models/vdb_mapping_ros2 |
| Behavior | Drone Safety Monitor | robot/ros_ws/src/behavior/drone_safety_monitor |
Each reference shows: - Package structure (CMakeLists.txt, package.xml, config, launch) - ROS 2 node implementation patterns - Topic subscription/publishing - Parameter configuration - README documentation
Development Commands¶
AirStack CLI Tool¶
The repository uses a custom CLI tool for common operations:
# Setup and installation
airstack setup # Configure AirStack and add to PATH
airstack install # Install Docker and dependencies
# Container management
airstack up [service] # Start services (robot, isaac-sim, gcs)
airstack up --sim isaac|airsim --robots N # Intent flags: derive profiles/URDF/sim script (add --headless, --play/--no-play, --no-autolaunch, --wait, --dry-run, --config-only)
airstack up --stack <name>[:<entry>] --sim isaac # Stack launch (RFC #379): stacks/<name>/launch/<entry>.launch.xml — the ONLY dispatch (no --stack = full_default; legacy AUTONOMY_ROLE was removed); see docs/development/stacks.md
airstack up --fleet <name> --sim isaac # Fleet launch (RFC #380): config/fleets/<name>.yaml drives identity/placement/spawns; see docs/development/fleets.md
airstack fleet list|generate <name> # Fleet files: table / per-robot compose for heterogeneous fleets
airstack sync # Sync from airstack.yaml: modules, external stack repos, fleet validation
# Modules, stacks, doctor (RFC #379; catalog: docs/modules/index.md)
airstack module add <url> --version <tag|sha> # Pin + sync an external module (branches refused); local paths allowed
airstack module list|sync|remove <name> # Table / (re)clone+validate+overlay+hooks / drop entry and artifacts
airstack module create --in-tree <name> # Scaffold a module boundary in a fork (RFC #379 §11)
airstack module doctor [--drift] # Validate manifests+overlay; --drift classifies fork changes (never blocks)
airstack stack list|new <src> <dst>|diff <a> <b> # Stacks: table / copy a reference stack / compare generated wiring
airstack doctor [--live|--snapshot] [--stack NAME] # Observe-and-report checks; --live diffs the RUNNING graph vs wiring.md
airstack ready # Wait until the stack is flight-ready (containers → sim /clock → nodes → PX4); --json for scripts
airstack down [service] # Stop services
airstack status # Show container status
airstack connect [name] # Connect to running container (tmux)
airstack logs [name] # View container logs (tmux output is mirrored to docker logs)
# Development tasks
airstack images build # Build Docker images (ROS workspaces build inside containers via `bws`)
airstack test # Run tests
airstack docs # Build and serve documentation
Docker Development Workflow¶
All development happens inside Docker containers. To run commands in the robot container:
# Start robot container without autolaunch (for development)
airstack up robot-desktop --no-autolaunch
# Build ROS 2 workspace (inside container)
docker exec airstack-robot-desktop-1 bash -c "bws --packages-select <package_name>"
# Build with debug symbols
docker exec airstack-robot-desktop-1 bash -c "bws --packages-select <package_name> --cmake-args '-DCMAKE_BUILD_TYPE=Debug'"
# Source workspace (inside container)
docker exec airstack-robot-desktop-1 bash -c "sws"
# Run a launch file
docker exec airstack-robot-desktop-1 bash -c "sws && ros2 launch <package> <launch_file>"
# List ROS 2 nodes
docker exec airstack-robot-desktop-1 bash -c "ros2 node list"
# Echo a topic
docker exec airstack-robot-desktop-1 bash -c "ros2 topic echo <topic_name> --once"
Important: Do NOT run commands in interactive mode as you can get stuck on prompts. Always use docker exec <container> bash -c "<command>".
ROS 2 Aliases (inside containers)¶
bws: Build workspace (colcon buildwith common flags)sws: Source workspace (source install/setup.bash)
Testing Philosophy¶
Goal: Enable autonomous debugging and testing by agents.
Testing Levels¶
- Module Level: Integration tests with mock inputs
- Verify module behavior in isolation
- Test with synthetic data
- Located in module's
test/directory - Run in the robot container with
colcon test(afterbws) for the full ROS 2 build + test. The same co-located test source is collected by the roottests/suite (the packages with unit tests are listed intests/colcon_unit_test_packages.yaml), soairstack test -m unitruns it too. Marks are declared intests/pytest.ini(unit,build_docker,build_packages,integration,liveliness,sensors,takeoff_hover_land,autonomy).
docker exec airstack-robot-desktop-1 bash -c "sws && colcon test --packages-select lidar_point_cloud_filter --event-handlers console_direct+"
-
Unit tests (
pytest,unitmark): Fast, hermetic checks. Test source lives co-located with each ROS 2 package in<package>/test/(standard colcon convention).tests/colcon_unit_test_packages.yamllists which packages have unit tests, andtests/conftest.pycollects them from there under--import-mode=importlib. To add a package's unit tests, list it in that YAML. Python unit tests run automatically inunit-tests.ymlonubuntu-latest; C++ gtests run through thesystem-tests.ymlbuild_packagespath. Example:airstack test -m unit -v. Seeadd-unit-testsskill. -
System Level (
tests/system/): Full simulation tests (Isaac Sim or Microsoft AirSim legacy) - End-to-end autonomy stack testing
- Real sensor simulation
- Multi-robot scenarios
- Pytest modules in
tests/system/— see below
System Test Suite (tests/system/)¶
Pytest-based system tests live under tests/system/. They bring up the full Docker stack (sim + robot + GCS) and verify container health, ROS 2 node presence, compute usage, sensor topic streams (sensors mark), and end-to-end flight behavior.
| File | Mark | What it tests | Hardware |
|---|---|---|---|
tests/system/test_build_docker.py |
build_docker |
Docker image builds (robot-desktop, gcs, isaac-sim, ms-airsim) | Docker |
tests/system/test_build_packages.py |
build_packages |
colcon build inside each container |
Docker |
tests/system/test_liveliness.py |
liveliness |
Stack bring-up: containers, /clock readiness, tmux, sentinel ROS 2 nodes, compute, infra-only stability poll |
Docker, GPU, sim license |
tests/system/test_sensors.py |
sensors |
Topic Hz (Isaac: batched sim + robot ros2 topic hz; filtered LiDAR echo-once + validation script), RTF, sensor stability time-series |
Docker, GPU, sim license |
tests/system/test_takeoff_hover_land.py |
takeoff_hover_land |
4-phase flight chain (PX4 ready → takeoff → hover → land) per (sim, num_robots, iter, velocity) | Docker, GPU, sim license |
tests/system/test_fixed_trajectory.py |
autonomy |
4-phase flight chain (PX4 ready → takeoff → execute Circle/Figure8/Racetrack/Line trajectory → land) per (sim, num_robots, iter, trajectory_type); records cross-track error and path RMSE | Docker, GPU, sim license |
tests/system/test_waypoint_flight.py |
waypoint_flight |
4-phase flight chain (PX4 ready → takeoff → NavigateTask waypoint route → land) per (sim, num_robots, iter); pass/fail judged on the odometry track by the standalone tests/waypoint_checker.py (in-order corridor arrival within --waypoint-tolerance, final goal within --goal-tolerance, per-waypoint --waypoint-timeout) |
Docker, GPU, sim license |
The pytest hooks and the airstack_env / robot_autonomy_stack fixtures live in tests/conftest.py; the shared helpers are split by concern into the tests/harness/ package (session, discovery, commands, containers, metrics (with MetricsRecorder), run_meta, test_ids, sim, collection) and re-exported through conftest, so from conftest import <name> still resolves. Each run produces a timestamped directory under tests/results/<timestamp>/ with summary.txt, results.xml, run_meta.json (schema v2: completion state, failure_class, campaign fingerprint over tests + behavior-changing CLI config), metrics.json, and — on bring-up/readiness failures — a bounded diagnostics/ bundle (no per-test log files — live output streams to the terminal via log_cli). tests/parse_metrics.py compares only fingerprint-identical, complete simulation campaigns; numeric metric deltas are advisory (exit 0) and never fail CI — only real test failures, infrastructure/prerequisite failures, and report-integrity errors (exit 2) do.
Run via the CLI (containerized runner — no local Python needed):
airstack test -m unit -v
airstack test -m "build_docker or build_packages" -v
airstack test -m liveliness --sim msairsim --num-robots 1 --stress-iterations 1 -v
airstack test -m sensors --sim isaacsim --num-robots 1 --stress-iterations 1 -v
airstack test -m takeoff_hover_land --sim msairsim --takeoff-velocities 0.5,1,2 -v
airstack test -m autonomy --sim msairsim --trajectory-types Circle,Figure8,Racetrack,Line -v
Full reference: tests/README.md — including liveliness vs
sensors (infra vs topic streams), class-scoped airstack_env (two bring-ups
when you select both marks with and), and Isaac Sim batching of
ros2 topic hz plus LiDAR echo-once / ENABLE_LIDAR for pytest.
Autonomous Debugging Approach¶
When a module doesn't work:
1. Verify module is running (ros2 node list)
2. Check topic connections (ros2 topic info, ros2 topic hz)
3. Inspect data quality (ros2 topic echo)
4. Review logs (docker logs airstack-robot-desktop-1)
5. Compare with reference implementation
6. Add instrumentation (debug publishers, logging)
7. Create minimal reproduction test
See detailed debugging workflow: .agents/skills/debug-module
CI/CD¶
GitHub Actions workflows live in .github/workflows/:
| Workflow | Trigger | Purpose |
|---|---|---|
unit-tests.yml |
PR to main/develop opened, synchronized, or reopened |
Runs Python unit tests and harness contracts on ubuntu-latest |
system-tests.yml |
PR opened/synchronized/reopened, /pytest PR comment (write-access only), or workflow_dispatch |
Runs automatic package builds or selected simulation marks on an ephemeral GPU runner; only complete simulation campaigns are compared in metrics reports |
docker-build.yml |
Push to main/develop that changes .env (VERSION=), or manual dispatch |
Builds, pushes, and cosign-signs all compose images on the ephemeral runner |
check-version-increment.yml |
Pull request | Validates .env VERSION= is valid semver and strictly greater than the base branch |
sync-modules-index.yml |
Daily schedule, or manual dispatch after merging a registry PR | Mirrors the live module registry into tests/meta/fixtures/modules_index/ + the committed docs/modules/ pages and opens the trunk sync PR when they disagree (the develop docs deploy's drift alarm files a docs-catalog-drift issue meanwhile) |
deploy_docs_from_{main,develop,release}.yaml |
Push to the matching branch (docs/**, mkdocs.yml, *.md) |
Publishes versioned MkDocs site via mike |
/pytest PR comments trigger system-tests.yml for users with write access (OWNER/MEMBER/COLLABORATOR), pulling args from the first line of the comment (e.g. /pytest -m liveliness --sim msairsim). Fork PRs are blocked — same-repo only — to keep arbitrary code off the self-hosted runner.
Ephemeral Runner Orchestrator¶
GPU-required jobs (runs-on: [self-hosted, airstack-ephemeral]) execute on ephemeral pods scheduled by NVIDIA OSMO — one per job, destroyed on completion. The GitHub side is unchanged from the old OpenStack backend (same labels, JIT tokens, fork guard); only the spawn target moved from "create a Nova VM" to "submit an OSMO workflow". The orchestrator service code lives in .github/orchestrator/:
orchestrator.py— Python service: spawn loop polls GitHub for queued jobs matching configured runner labels, mints single-use JIT runner tokens, and submits one OSMO workflow per job (osmo workflow submit); reap loop cancels the workflow when the job completes (or aftermax_job_minutes), plus an orphan sweep viaosmo workflow listrunner-workflow.yaml.j2+runner.Dockerfile+runner-entrypoint.sh— the per-job worker: a privileged, GPU-enabled OSMO task (prebaked image) that starts an inner Docker daemon (the tests runairstack up= docker compose), registers with the JIT token, runs one job, then exits so OSMO reaps the podconfig.example.yaml— osmo_url / pool / platform / runner_image / resources / runner labels / repoairstack-orchestrator.service+setup.sh— systemd unit and one-time installer
Why ephemeral: clean Docker cache per run, no leaked containers; the GitHub PAT and the OSMO service-account token live only on the orchestrator host (workers receive a single-use JIT token bound to one runner registration). CI authenticates to OSMO as a shared, non-personal service account scoped to a dedicated CI GPU pool, so runs don't consume individuals' quotas. The CI pool's platform must have "Privileged Mode Allowed" enabled (docker-in-docker). State map at /var/lib/airstack-orchestrator/state.json; logs via journalctl -u airstack-orchestrator.service -f.
Nested DinD needs a non-overlayfs Docker data-root. The OSMO pod's root filesystem is overlayfs, and Linux rejects a directory on overlayfs as an overlay upperdir (EINVAL). A dockerd storing data on the pod rootfs pulls images fine but fails every build step that needs a real mount, with errors that masquerade as apt-get/WORKDIR failures:
failed to solve: ... mount source: "overlay", target: ".../buildkit/containerd-overlayfs/cachemounts/...", err: invalid argument
runner-entrypoint.sh picks a backend by attempting a real overlay mount, preferring a loopback ext4 image at /var/lib/docker (real overlay2), then a real filesystem already mounted in the pod, then fuse-overlayfs, then vfs. Landing on vfs means builds will be slow and probably run out of disk — check the [runner-entrypoint] storage: line in the job log first when Docker builds misbehave. Details: orchestrator README → Nested DinD and overlayfs.
Docker layer cache is a floating tag, not the versioned one. Every compose service lists two cache_from entries: the versioned image (airstack:v${VERSION}_<suffix>) and a floating one (airstack:${CACHE_TAG:-cache}_<suffix>). Only the floating tag can ever hit on a PR — check-version-increment forces VERSION up on every PR, so the versioned tag it builds under has by definition never been pushed. Reading and writing are separate switches: AIRSTACK_REGISTRY_CACHE=1 (set by system-tests.yml) pulls and builds with BUILDKIT_INLINE_CACHE=1, while AIRSTACK_REGISTRY_CACHE_PUSH=1 (set only by docker-build.yml on main/develop) also publishes both tags. PR runs stay read-only so an unmerged branch can't poison the shared cache or publish an unreleased version. If you add a service with a build: section, give it both entries or its builds will always be cold.
Publish retags when image inputs are unchanged. docker-build.yml runs .github/workflows/scripts/docker_image_plan.py on VERSION bumps: each service gets a content fingerprint (org.airstack.content-fingerprint). If the previous versioned image already has that label, the job registry-retags (imagetools create) instead of rebuilding; only changed services rebuild (and refresh cache_*). Use workflow_dispatch with force_rebuild=true to rebuild everything. PR build_docker tests still perform real builds.
Setup, debugging a failed job, and exec-into-worker procedures: .github/orchestrator/README.md (also exposed as tests/ci-cd-orchestrator.md symlink for the docs site).
Documentation Requirements¶
When implementing a new feature/module, you must:
1. Module README.md¶
Create README.md in the package directory with:
- Overview and purpose
- Algorithm description
- Architecture diagram (mermaid)
- Dependencies and interfaces (input/output topics, parameters)
- Configuration
- Usage examples
Template: See .agents/skills/add-ros2-package/assets/package_template/README.md
2. Update mkdocs.yml¶
Add the module README to the navigation structure:
nav:
- Reference:
- Autonomy Packages:
- Local:
- Your Module: robot/ros_ws/src/local/planners/your_package/README.md
The nav is organized by Diátaxis tabs (Tutorials / How-to Guides / Reference / Concepts); package READMEs live under Reference → Autonomy Packages. Pick the quadrant for any new system-level page with the decision tree in docs/development/intermediate/documentation.md.
The same-dir plugin allows linking to README files outside the docs/ directory.
3. System-Level Documentation (if needed)¶
For major features or cross-cutting concerns, create docs in docs/:
- Tutorials: docs/tutorials/<feature>.md
- Integration guides: docs/robot/autonomy/<layer>/<topic>.md
4. Update Layer Overview¶
Edit docs/robot/autonomy/<layer>/index.md to mention the new module.
Complete workflow: .agents/skills/update-documentation
Package Templates¶
Use standardized templates when creating new packages:
Location: .agents/skills/add-ros2-package/assets/package_template/
Templates include:
- CMakeLists.txt (C++ template with TODOs)
- setup.py (Python template with TODOs)
- package.xml (dependency template)
- config/template.yaml (parameter configuration)
- launch/template.launch.xml (launch file with remapping)
- README.md (comprehensive documentation template)
- Example source files with best practices
Follow the template structure for consistency across the codebase.
Docker Architecture¶
Each major component has its own Docker container:
- robot: ROS 2 autonomy stack (Jazzy)
- isaac-sim: NVIDIA Isaac Sim with Pegasus extension (profile: desktop or robot)
- airsim: AirSim UE4 binary + PX4 SITL + ROS 2 bridge (profile: ms-airsim)
- gcs: Ground Control Station
- docs: Documentation building (MkDocs)
Configuration:
- Main compose file: docker-compose.yaml (includes all component compose files)
- Environment variables: top-level .env (image tags, VERSION, NUM_ROBOTS, ROBOT_NAME_MAP_CONFIG_FILE, ISAAC_SIM_SCRIPT_NAME, AIRSTACK_STACK_DIR, etc.)
- Per-container shell init: robot/docker/.bashrc — resolves ROBOT_NAME and ROS_DOMAIN_ID at startup (see Multi-Robot Configuration below)
Networking: Custom bridge network (172.31.0.0/24) for inter-container communication.
Multi-Robot Configuration¶
Fleet-first (RFC #380, opt-in): a fleet file under config/fleets/ declares who exists, which vehicle (config/vehicles/), which stack, and which ground hosts run split-stack offboard halves. airstack up --fleet <name> validates it, derives NUM_ROBOTS, selects the generic Isaac fleet spawner (fleet_spawn.py), and — for heterogeneous fleets — includes generated per-robot compose services (airstack fleet generate). Containers resolve their whole fleet entry via tools/fleet/resolve_fleet.py when FLEET_CONFIG_FILE is set. Guide: docs/development/fleets.md. Without a fleet, everything below is unchanged (the default).
Legacy multi-robot is implemented via Docker Compose replicas, not multiple namespaces in one container. Setting NUM_ROBOTS=3 in .env spawns three separate containers (airstack-robot-desktop-1, -2, -3) via deploy.replicas: ${NUM_ROBOTS:-1} in robot/docker/docker-compose.yaml.
ROBOT_NAME is not set directly in .env. Each container computes it at startup: robot/docker/.bashrc reads ROBOT_NAME_SOURCE (container_name or hostname) and runs resolve_robot_name.py against the mapping in robot/docker/robot_name_map/ (default: default_robot_name_map.yaml). The resolver exports both ROBOT_NAME and ROS_DOMAIN_ID — robot N gets domain N by default, so each robot is on its own DDS partition.
The autonomy topology is selected by a stack (AIRSTACK_STACK_DIR / AIRSTACK_STACK_ENTRY, exported by airstack up --stack <name>[:<entry>]), dispatched in robot/ros_ws/src/autonomy_bringup/launch/robot.launch.xml. The legacy AUTONOMY_ROLE dispatch was removed — a set AUTONOMY_ROLE is a preflight error. Reference stacks (see docs/development/stacks.md):
- full_default — every autonomy module runs on this machine (sim/dev desktop, autonomous Jetson); the default when no stack is selected, machine-proven graph-identical to the old
AUTONOMY_ROLE=full - lite_default — lite modules only (interface, sensors, perception, local planning, behavior); no global/logging
- lite_offload_global — split stack:
:onboard(lite, on the vehicle) +:offboard(global planning on a ground host), bridged per itsbridge.yaml(generated DDS-router config)
For Isaac Sim, the default ISAAC_SIM_SCRIPT_NAME=example_one_px4_pegasus_launch_script.py only spawns a single drone. Multi-robot Isaac Sim requires ISAAC_SIM_SCRIPT_NAME=example_multi_px4_pegasus_launch_script.py (the system test harness sets this automatically when --num-robots > 1).
Full workflow: .agents/skills/configure-multi-robot
Critical Pitfalls to Avoid¶
Common mistakes when adding modules:
- Topic Connection Issues
- ❌ Hardcoding topic names in node code
- ✅ Use launch arguments for topic remapping
-
✅ Verify connections with
ros2 topic info -
Integration Failures
- ❌ Forgetting to add module to layer bringup launch file
- ✅ Follow
integrate_module_into_layer.mdworkflow -
✅ Add package dependency to bringup
package.xml -
Build Issues
- ❌ Missing dependencies in
package.xml - ✅ Declare all ROS 2 and external dependencies
- ❌ Not installing launch/config files in
CMakeLists.txt -
✅ Use
install()directives for all resources -
Documentation Gaps
- ❌ Not updating
mkdocs.ymlnavigation - ✅ Add module to appropriate nav section
- ❌ Missing module README
-
✅ Use README template with all sections
-
Launch File Issues
- ❌ Not using
$(env ROBOT_NAME)for multi-robot support - ✅ Always namespace with robot name
- ❌ Missing
allow_substs="true"for parameter files -
✅ Enable substitution for environment variables in configs
-
Testing Oversights
- ❌ Only testing module in isolation
- ✅ Test in full autonomy stack context
- ✅ Verify in Isaac Sim or Microsoft AirSim (legacy) simulation
Key Differences from CLAUDE.md¶
This guide supersedes CLAUDE.md (which now symlinks here). Key updates:
- ROS 2 Jazzy (was Humble)
airstackcommand (not./airstack.shin most contexts)- Skills directory for detailed workflows
- Module integration focus with checklist and templates
- Autonomous debugging guidance for AI agents
- Package templates for consistency
- Documentation automation requirements
Additional Resources¶
Comprehensive Guides¶
- AI Agent Quick Guide - Quick reference for agents
- Integration Checklist - Module integration requirements
- System Architecture - Architecture diagrams and data flow
- Contributing Guide - Development workflow and PR process
ROS 2 Documentation¶
AirStack Documentation¶
- Getting Started
- Autonomy Stack Overview
- Isaac Sim Setup
- Microsoft AirSim (legacy) Setup
- Testing Guide
External Tools¶
- MkDocs Material - Documentation framework
- Pegasus Simulator - Isaac Sim extension
For Agents: Start with the AI Agent Quick Guide and refer to .agents/skills/ for specific workflows. Study reference implementations before creating new modules.