agentleFS
enesbirlikenesbirlik/claude-code-robotics/skills/ros2-workspace/SKILL.md

ros2-workspace

Skill2 starsChanged 54 days agoMITView on GitHub

Use when creating, structuring, or modifying ROS 2 (Humble/Jazzy) packages and workspaces — package.xml/CMakeLists.txt conventions, Python (rclpy) and C++ (rclcpp) node patterns, executors, launch files, and colcon build/test workflows.

The file

Show its rules and text

We split this skill into the rules it gives an agent

20 rules. About 0 are common, 0 are distinctive, and 20 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 20

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Never mix message/service/action definitions into a package that also has other build dependents — put custom interfaces in their own _msgs …

    line 27
  2. Very specific:

    Never commit build/, install/, or log/ — the plugin's .gitignore template already excludes them.

    line 32
  3. Very specific:

    Every runtime import/link dependency is a <depend> (or split <build_depend> / <exec_depend> only when build-time and run-time deps …

    line 66
  4. Very specific:

    Always declare ament_lint_auto + ament_lint_common as test_depend so colcon test runs static checks (ament_cpplint, ament_flake8, …

    line 69
  5. Very specific:

    Every installed executable target gets an explicit install(TARGETS ...).

    line 122
  6. Very specific:

    Every console_scripts entry point maps executable_name = package.module:main.

    line 168
  7. Very specific:

    Don't forget the resource/<package_name> marker file (empty) — without it ros2 pkg list and ros2 run won't find the package.

    line 170
  8. Very specific:

    Always declare parameters with declare_parameter before reading them — never assume a default exists implicitly.

    line 232
  9. Very specific:

    Use rclpy.spin(node) for single-purpose nodes.

    line 238
  10. Very specific:

    Always destroy_node() then rclpy.shutdown() in a finally block so Ctrl+C and exceptions both clean up the node's middleware resources.

    line 242
  11. Very specific:

    Prefer composition-friendly node classes (inherit rclcpp::Node, no raw main-only logic) so the node can later be loaded as a component …

    line 303
  12. Very specific:

    Use rclcpp::SensorDataQoS() / rclcpp::SystemDefaultsQoS() helpers instead of hand-rolling rclcpp::QoS profiles unless you need a genuinely …

    line 305
  13. Very specific:

    Never call shared_from_this() inside a constructor — the shared_ptr doesn't exist yet.

    line 307
  14. Very specific:

    If you need it (e.g. to register a callback that captures this as a shared_ptr), do it from a separate init() method called right after …

    line 308
  15. Very specific:

    Use rclcpp::executors::MultiThreadedExecutor with explicit CallbackGroups only when you have a proven concurrency need (e.g.

    line 310
  16. Very specific:

    Every tunable value exposed as a DeclareLaunchArgument, never hardcoded, so the same launch file works in simulation and on hardware.

    line 344
  17. Very specific:

    Use FindPackageShare + PathJoinSubstitution to locate config/URDF files — never a hardcoded absolute path.

    line 346
  18. Very specific:

    Prefer one launch file per package that composes into a top-level bringup launch file via IncludeLaunchDescription, instead of one …

    line 348
  19. Very specific:

    Use --symlink-install in development workspaces so Python source and non-compiled resources update without a rebuild.

    line 363
  20. Very specific:

    Always re-run colcon test after touching package.xml dependencies or CMake install rules — ament_lint catches missing/unused dependencies …

    line 367

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

ROS 2 Workspace Engineering

Rules and templates for producing ROS 2 packages that build cleanly with colcon, pass ament_lint, and follow the conventions expected in Humble and Jazzy.

1. Workspace layout

ros2_ws/
├── src/
│   ├── my_robot_bringup/          # launch + top-level config, no code
│   ├── my_robot_description/      # URDF/Xacro + meshes (see urdf-xacro-builder skill)
│   ├── my_robot_msgs/             # custom .msg/.srv/.action, ament_cmake only
│   └── my_robot_controller/       # the actual node(s), ament_cmake or ament_python
├── install/                       # generated by colcon build — never hand-edit, gitignore it
├── build/                         # generated — gitignore it
└── log/                           # generated — gitignore it

Rules:

  • One package = one clear responsibility (bringup, description, msgs, driver, algorithm). Never mix message/service/action definitions into a package that also has other build dependents — put custom interfaces in their own _msgs or _interfaces package to avoid circular dependencies.
  • Package names are snake_case, never start with a digit, and should be prefixed with the robot/project name (my_robot_controller, not controller).
  • Never commit build/, install/, or log/ — the plugin's .gitignore template already excludes them.

2. package.xml (format 3)

<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
  <name>my_robot_controller</name>
  <version>0.1.0</version>
  <description>Velocity controller node for my_robot.</description>
  <maintainer email="you@example.com">Your Name</maintainer>
  <license>Apache-2.0</license>

  <buildtool_depend>ament_cmake</buildtool_depend>

  <depend>rclcpp</depend>
  <depend>geometry_msgs</depend>
  <depend>sensor_msgs</depend>

  <test_depend>ament_lint_auto</test_depend>
  <test_depend>ament_lint_common</test_depend>

  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>

For a Python package, use <build_type>ament_python</build_type> and <buildtool_depend>ament_python</buildtool_depend>.

Rules:

  • Every runtime import/link dependency is a <depend> (or split <build_depend> / <exec_depend> only when build-time and run-time deps genuinely differ — otherwise prefer the combined <depend> tag for readability).
  • Always declare ament_lint_auto + ament_lint_common as test_depend so colcon test runs static checks (ament_cpplint, ament_flake8, ament_xmllint, ...).
  • <version> follows SemVer and should be bumped alongside CHANGELOG.rst entries.

3. C++ package: CMakeLists.txt

cmake_minimum_required(VERSION 3.8)
project(my_robot_controller)

if(NOT CMAKE_CXX_STANDARD)
  set(CMAKE_CXX_STANDARD 17)
  set(CMAKE_CXX_STANDARD_REQUIRED ON)
endif()
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(geometry_msgs REQUIRED)
find_package(sensor_msgs REQUIRED)

add_executable(velocity_controller_node
  src/velocity_controller_node.cpp
)
ament_target_dependencies(velocity_controller_node
  rclcpp
  geometry_msgs
  sensor_msgs
)
target_include_directories(velocity_controller_node PRIVATE
  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
)

install(TARGETS velocity_controller_node
  DESTINATION lib/${PROJECT_NAME}
)
install(DIRECTORY launch config
  DESTINATION share/${PROJECT_NAME}
  OPTIONAL
)

if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  ament_lint_auto_find_test_dependencies()
endif()

ament_package()

Rules:

  • Warnings-as-errors are opt-in per repo, but -Wall -Wextra -Wpedantic is always on.
  • Every installed executable target gets an explicit install(TARGETS ...). Every shared launch/, config/, urdf/, meshes/ directory gets an install(DIRECTORY ...).
  • ament_package() must be the last line.

4. Python package: setup.py / setup.cfg

# setup.py
from setuptools import find_packages, setup

package_name = 'my_robot_controller'

setup(
    name=package_name,
    version='0.1.0',
    packages=find_packages(exclude=['test']),
    data_files=[
        ('share/ament_index/resource_index/packages',
            ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml']),
        ('share/' + package_name + '/launch', ['launch/controller.launch.py']),
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    maintainer='Your Name',
    maintainer_email='you@example.com',
    description='Velocity controller node for my_robot.',
    license='Apache-2.0',
    tests_require=['pytest'],
    entry_points={
        'console_scripts': [
            'velocity_controller_node = my_robot_controller.velocity_controller_node:main',
        ],
    },
)
# setup.cfg
[develop]
script_dir=$base/lib/my_robot_controller
[install]
install_scripts=$base/lib/my_robot_controller

Rules:

  • Every console_scripts entry point maps executable_name = package.module:main; the main() function must exist and take no required arguments.
  • Don't forget the resource/<package_name> marker file (empty) — without it ros2 pkg list and ros2 run won't find the package.

5. rclpy node template (Python)

#!/usr/bin/env python3
from rclpy.node import Node
from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy
from geometry_msgs.msg import Twist
from sensor_msgs.msg import LaserScan
import rclpy


class VelocityControllerNode(Node):
    def __init__(self) -> None:
        super().__init__('velocity_controller_node')

        self.declare_parameter('max_linear_speed', 0.5)
        self.declare_parameter('scan_topic', '/scan')
        self._max_linear_speed = self.get_parameter('max_linear_speed').value

        sensor_qos = QoSProfile(
            reliability=ReliabilityPolicy.BEST_EFFORT,
            history=HistoryPolicy.KEEP_LAST,
            depth=5,
        )
        self._scan_sub = self.create_subscription(
            LaserScan,
            self.get_parameter('scan_topic').value,
            self._on_scan,
            sensor_qos,
        )
        self._cmd_pub = self.create_publisher(Twist, 'cmd_vel', 10)
        self._timer = self.create_timer(0.1, self._on_timer)

    def _on_scan(self, msg: LaserScan) -> None:
        self._latest_scan = msg

    def _on_timer(self) -> None:
        cmd = Twist()
        cmd.linear.x = self._max_linear_speed
        self._cmd_pub.publish(cmd)


def main(args: list[str] | None = None) -> None:
    rclpy.init(args=args)
    node = VelocityControllerNode()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.shutdown()


if __name__ == '__main__':
    main()

Rules:

  • Always declare parameters with declare_parameter before reading them — never assume a default exists implicitly.
  • Choose QoS explicitly. Sensor data: BEST_EFFORT + shallow KEEP_LAST depth. Commands and state: RELIABLE + KEEP_LAST with a depth matched to consumer speed. Never leave QoS at an implicit default when publisher and subscriber are in different packages — mismatches silently drop all messages.
  • Use rclpy.spin(node) for single-purpose nodes; use MultiThreadedExecutor with explicit ReentrantCallbackGroup/MutuallyExclusiveCallbackGroup assignment only when you have callbacks that must run concurrently (e.g. a long-running service alongside a fast timer) — don't reach for it by default, it adds real concurrency hazards.
  • Always destroy_node() then rclpy.shutdown() in a finally block so Ctrl+C and exceptions both clean up the node's middleware resources.

6. rclcpp node template (C++)

#include <memory>
#include <rclcpp/rclcpp.hpp>
#include <geometry_msgs/msg/twist.hpp>
#include <sensor_msgs/msg/laser_scan.hpp>

class VelocityControllerNode : public rclcpp::Node
{
public:
  VelocityControllerNode()
  : Node("velocity_controller_node")
  {
    max_linear_speed_ = this->declare_parameter("max_linear_speed", 0.5);
    const auto scan_topic = this->declare_parameter("scan_topic", std::string("/scan"));

    auto sensor_qos = rclcpp::SensorDataQoS();
    scan_sub_ = this->create_subscription<sensor_msgs::msg::LaserScan>(
      scan_topic, sensor_qos,
      std::bind(&VelocityControllerNode::onScan, this, std::placeholders::_1));

    cmd_pub_ = this->create_publisher<geometry_msgs::msg::Twist>("cmd_vel", 10);
    timer_ = this->create_wall_timer(
      std::chrono::milliseconds(100),
      std::bind(&VelocityControllerNode::onTimer, this));
  }

private:
  void onScan(const sensor_msgs::msg::LaserScan::SharedPtr msg)
  {
    latest_scan_ = msg;
  }

  void onTimer()
  {
    geometry_msgs::msg::Twist cmd;
    cmd.linear.x = max_linear_speed_;
    cmd_pub_->publish(cmd);
  }

  double max_linear_speed_;
  sensor_msgs::msg::LaserScan::SharedPtr latest_scan_;
  rclcpp::Subscription<sensor_msgs::msg::LaserScan>::SharedPtr scan_sub_;
  rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr cmd_pub_;
  rclcpp::TimerBase::SharedPtr timer_;
};

int main(int argc, char ** argv)
{
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<VelocityControllerNode>());
  rclcpp::shutdown();
  return 0;
}

Rules:

  • Prefer composition-friendly node classes (inherit rclcpp::Node, no raw main-only logic) so the node can later be loaded as a component without rewriting it.
  • Use rclcpp::SensorDataQoS() / rclcpp::SystemDefaultsQoS() helpers instead of hand-rolling rclcpp::QoS profiles unless you need a genuinely custom depth/durability.
  • Never call shared_from_this() inside a constructor — the shared_ptr doesn't exist yet. If you need it (e.g. to register a callback that captures this as a shared_ptr), do it from a separate init() method called right after construction.
  • Use rclcpp::executors::MultiThreadedExecutor with explicit CallbackGroups only when you have a proven concurrency need (e.g. a blocking service call must not stall a control-loop timer); default to the implicit single-threaded rclcpp::spin.

7. Launch files

from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration, PathJoinSubstitution
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare


def generate_launch_description() -> LaunchDescription:
    max_speed_arg = DeclareLaunchArgument(
        'max_linear_speed', default_value='0.5',
        description='Maximum commanded linear speed (m/s).',
    )

    controller_node = Node(
        package='my_robot_controller',
        executable='velocity_controller_node',
        name='velocity_controller_node',
        output='screen',
        parameters=[{
            'max_linear_speed': LaunchConfiguration('max_linear_speed'),
        }],
    )

    return LaunchDescription([max_speed_arg, controller_node])

Rules:

  • Every tunable value exposed as a DeclareLaunchArgument, never hardcoded, so the same launch file works in simulation and on hardware.
  • Use FindPackageShare + PathJoinSubstitution to locate config/URDF files — never a hardcoded absolute path.
  • Prefer one launch file per package that composes into a top-level bringup launch file via IncludeLaunchDescription, instead of one monolithic launch file per robot.

8. colcon build & test workflow

# From the workspace root (parent of src/):
colcon build --symlink-install                     # symlink python/config for fast iteration
colcon build --packages-select my_robot_controller  # scope a build to one package
colcon test --packages-select my_robot_controller
colcon test-result --verbose
source install/setup.bash                            # re-source after every structural change

Rules:

  • Use --symlink-install in development workspaces so Python source and non-compiled resources update without a rebuild.
  • Scope builds with --packages-select while iterating; only build the whole workspace before a final integration check.
  • Always re-run colcon test after touching package.xml dependencies or CMake install rules — ament_lint catches missing/unused dependencies that only surface at test time.
  • This plugin's colcon_build_check hook automatically runs a scoped colcon build --packages-select <pkg> whenever a new node source file is created inside a detected colcon workspace — no need to trigger it manually during normal editing.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.