Skip to content

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 under tests/meta/fixtures/modules_index/ + regenerated docs/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 daily sync-modules-index workflow opens the trunk sync PR (dispatch it manually for an immediate sync), and the develop docs deploy raises a docs-catalog-drift issue whenever git and the live registry disagree (details: the extract-module skill'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:

Sensors → Perception → World Models → Planners → Controllers → Interface → Hardware

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 build with common flags)
  • sws: Source workspace (source install/setup.bash)

Testing Philosophy

Goal: Enable autonomous debugging and testing by agents.

Testing Levels

  1. Module Level: Integration tests with mock inputs
  2. Verify module behavior in isolation
  3. Test with synthetic data
  4. Located in module's test/ directory
  5. Run in the robot container with colcon test (after bws) for the full ROS 2 build + test. The same co-located test source is collected by the root tests/ suite (the packages with unit tests are listed in tests/colcon_unit_test_packages.yaml), so airstack test -m unit runs it too. Marks are declared in tests/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+"
  1. Unit tests (pytest, unit mark): Fast, hermetic checks. Test source lives co-located with each ROS 2 package in <package>/test/ (standard colcon convention). tests/colcon_unit_test_packages.yaml lists which packages have unit tests, and tests/conftest.py collects them from there under --import-mode=importlib. To add a package's unit tests, list it in that YAML. Python unit tests run automatically in unit-tests.yml on ubuntu-latest; C++ gtests run through the system-tests.yml build_packages path. Example: airstack test -m unit -v. See add-unit-tests skill.

  2. System Level (tests/system/): Full simulation tests (Isaac Sim or Microsoft AirSim legacy)

  3. End-to-end autonomy stack testing
  4. Real sensor simulation
  5. Multi-robot scenarios
  6. 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 after max_job_minutes), plus an orphan sweep via osmo workflow list
  • runner-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 run airstack up = docker compose), registers with the JIT token, runs one job, then exits so OSMO reaps the pod
  • config.example.yaml — osmo_url / pool / platform / runner_image / resources / runner labels / repo
  • airstack-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 its bridge.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:

  1. Topic Connection Issues
  2. ❌ Hardcoding topic names in node code
  3. ✅ Use launch arguments for topic remapping
  4. ✅ Verify connections with ros2 topic info

  5. Integration Failures

  6. ❌ Forgetting to add module to layer bringup launch file
  7. ✅ Follow integrate_module_into_layer.md workflow
  8. ✅ Add package dependency to bringup package.xml

  9. Build Issues

  10. ❌ Missing dependencies in package.xml
  11. ✅ Declare all ROS 2 and external dependencies
  12. ❌ Not installing launch/config files in CMakeLists.txt
  13. ✅ Use install() directives for all resources

  14. Documentation Gaps

  15. ❌ Not updating mkdocs.yml navigation
  16. ✅ Add module to appropriate nav section
  17. ❌ Missing module README
  18. ✅ Use README template with all sections

  19. Launch File Issues

  20. ❌ Not using $(env ROBOT_NAME) for multi-robot support
  21. ✅ Always namespace with robot name
  22. ❌ Missing allow_substs="true" for parameter files
  23. ✅ Enable substitution for environment variables in configs

  24. Testing Oversights

  25. ❌ Only testing module in isolation
  26. ✅ Test in full autonomy stack context
  27. ✅ 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)
  • airstack command (not ./airstack.sh in 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

ROS 2 Documentation

AirStack Documentation

External Tools


For Agents: Start with the AI Agent Quick Guide and refer to .agents/skills/ for specific workflows. Study reference implementations before creating new modules.