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.
In this repository5 other files it gives its agents.
- embedded-ros-bridgeSkill
- nav2-moveit2-configSkill
- ros2-controlSkill
- ros2-ecosystem-packagesSkill
- urdf-xacro-builderSkill
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
- Very specific: line 27
Never mix message/service/action definitions into a package that also has other build dependents — put custom interfaces in their own _msgs …
- Very specific: line 32
Never commit build/, install/, or log/ — the plugin's .gitignore template already excludes them.
- Very specific: line 66
Every runtime import/link dependency is a <depend> (or split <build_depend> / <exec_depend> only when build-time and run-time deps …
- Very specific: line 69
Always declare ament_lint_auto + ament_lint_common as test_depend so colcon test runs static checks (ament_cpplint, ament_flake8, …
- Very specific: line 122
Every installed executable target gets an explicit install(TARGETS ...).
- Very specific: line 168
Every console_scripts entry point maps executable_name = package.module:main.
- Very specific: line 170
Don't forget the resource/<package_name> marker file (empty) — without it ros2 pkg list and ros2 run won't find the package.
- Very specific: line 232
Always declare parameters with declare_parameter before reading them — never assume a default exists implicitly.
- Very specific: line 238
Use rclpy.spin(node) for single-purpose nodes.
- Very specific: line 242
Always destroy_node() then rclpy.shutdown() in a finally block so Ctrl+C and exceptions both clean up the node's middleware resources.
- Very specific: line 303
Prefer composition-friendly node classes (inherit rclcpp::Node, no raw main-only logic) so the node can later be loaded as a component …
- Very specific: line 305
Use rclcpp::SensorDataQoS() / rclcpp::SystemDefaultsQoS() helpers instead of hand-rolling rclcpp::QoS profiles unless you need a genuinely …
- Very specific: line 307
Never call shared_from_this() inside a constructor — the shared_ptr doesn't exist yet.
- Very specific: line 308
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 …
- Very specific: line 310
Use rclcpp::executors::MultiThreadedExecutor with explicit CallbackGroups only when you have a proven concurrency need (e.g.
- Very specific: line 344
Every tunable value exposed as a DeclareLaunchArgument, never hardcoded, so the same launch file works in simulation and on hardware.
- Very specific: line 346
Use FindPackageShare + PathJoinSubstitution to locate config/URDF files — never a hardcoded absolute path.
- Very specific: line 348
Prefer one launch file per package that composes into a top-level bringup launch file via IncludeLaunchDescription, instead of one …
- Very specific: line 363
Use --symlink-install in development workspaces so Python source and non-compiled resources update without a rebuild.
- Very specific: line 367
Always re-run colcon test after touching package.xml dependencies or CMake install rules — ament_lint catches missing/unused dependencies …
≈ “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.
Files
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 itRules:
- 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
_msgsor_interfacespackage 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, notcontroller). - Never commit
build/,install/, orlog/— the plugin's.gitignoretemplate 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_commonastest_dependsocolcon testruns static checks (ament_cpplint,ament_flake8,ament_xmllint, ...). <version>follows SemVer and should be bumped alongsideCHANGELOG.rstentries.
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 -Wpedanticis always on. - Every installed executable target gets an explicit
install(TARGETS ...). Every sharedlaunch/,config/,urdf/,meshes/directory gets aninstall(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_controllerRules:
- Every
console_scriptsentry point mapsexecutable_name = package.module:main; themain()function must exist and take no required arguments. - Don't forget the
resource/<package_name>marker file (empty) — without itros2 pkg listandros2 runwon'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_parameterbefore reading them — never assume a default exists implicitly. - Choose QoS explicitly. Sensor data:
BEST_EFFORT+ shallowKEEP_LASTdepth. Commands and state:RELIABLE+KEEP_LASTwith 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; useMultiThreadedExecutorwith explicitReentrantCallbackGroup/MutuallyExclusiveCallbackGroupassignment 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()thenrclpy.shutdown()in afinallyblock 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 rawmain-only logic) so the node can later be loaded as a component without rewriting it. - Use
rclcpp::SensorDataQoS()/rclcpp::SystemDefaultsQoS()helpers instead of hand-rollingrclcpp::QoSprofiles unless you need a genuinely custom depth/durability. - Never call
shared_from_this()inside a constructor — theshared_ptrdoesn't exist yet. If you need it (e.g. to register a callback that capturesthisas ashared_ptr), do it from a separateinit()method called right after construction. - Use
rclcpp::executors::MultiThreadedExecutorwith explicitCallbackGroups 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-threadedrclcpp::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+PathJoinSubstitutionto locate config/URDF files — never a hardcoded absolute path. - Prefer one launch file per package that composes into a top-level
bringuplaunch file viaIncludeLaunchDescription, 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 changeRules:
- Use
--symlink-installin development workspaces so Python source and non-compiled resources update without a rebuild. - Scope builds with
--packages-selectwhile iterating; only build the whole workspace before a final integration check. - Always re-run
colcon testafter touchingpackage.xmldependencies or CMake install rules —ament_lintcatches missing/unused dependencies that only surface at test time. - This plugin's
colcon_build_checkhook automatically runs a scopedcolcon 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.
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
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.

