new-system
Scaffold a new gz-sim system plugin under src/systems/<name>/ — header, source, CMake glue, plugin registration, and an integration test stub. Trigger when the user asks to create a new system, controller, or plugin that hooks into the simulation loop.
git clone --depth 1 https://github.com/harunkurtdev/ros2-claude-code-template /tmp/new-system && cp -r /tmp/new-system/.claude/skills/new-system ~/.claude/skills/new-systemSKILL.md
# Adding a new system plugin
A system in gz-sim is a shared library that implements one or more of
`ISystemConfigure`, `ISystemPreUpdate`, `ISystemUpdate`, `ISystemPostUpdate`,
`ISystemReset` and gets loaded via SDF `<plugin>` or `<server_config>`.
Directory convention: `src/systems/<snake_case>/<CamelCase>.{hh,cc}`.
## Step 1 — pick the directory + filename
* Directory: `snake_case` matching the plugin's purpose (e.g.
`fancy_drive`, `wheel_slip`).
* Class: `CamelCase` matching the directory (e.g. `FancyDrive`).
* SDF plugin name: `gz::sim::systems::<CamelCase>`.
## Step 2 — header template (`FancyDrive.hh`)
```cpp
#ifndef GZ_SIM_SYSTEMS_FANCYDRIVE_HH_
#define GZ_SIM_SYSTEMS_FANCYDRIVE_HH_
#include <memory>
#include <gz/sim/System.hh>
#include <gz/sim/config.hh>
#include <gz/utils/ImplPtr.hh>
namespace gz::sim
{
inline namespace GZ_SIM_VERSION_NAMESPACE {
namespace systems
{
class FancyDrivePrivate;
/// \brief One-paragraph description of what this system does.
///
/// SDF parameters:
/// <param_a> — what it does (default: ...)
/// <param_b> — what it does (default: ...)
class FancyDrive
: public System,
public ISystemConfigure,
public ISystemPreUpdate,
public ISystemPostUpdate
{
public: FancyDrive();
public: ~FancyDrive() override;
// System hooks
public: void Configure(const Entity &_entity,
const std::shared_ptr<const sdf::Element> &_sdf,
EntityComponentManager &_ecm,
EventManager &_eventMgr) override;
public: void PreUpdate(const UpdateInfo &_info,
EntityComponentManager &_ecm) override;
public: void PostUpdate(const UpdateInfo &_info,
const EntityComponentManager &_ecm) override;
/// \brief Private data via PIMPL.
GZ_UTILS_UNIQUE_IMPL_PTR(dataPtr)
};
}
}
}
#endif
```
## Step 3 — source template (`FancyDrive.cc`)
```cpp
#include "FancyDrive.hh"
#include <gz/plugin/Register.hh>
#include <gz/sim/Model.hh>
#include <gz/sim/components/JointVelocityCmd.hh>
namespace gz::sim::systems
{
class FancyDrivePrivate
{
public: Model model{kNullEntity};
// …cached handles, sdf params, transport node, etc.
};
FancyDrive::FancyDrive() : dataPtr(gz::utils::MakeUniqueImpl<FancyDrivePrivate>())
{
}
FancyDrive::~FancyDrive() = default;
void FancyDrive::Configure(const Entity &_entity,
const std::shared_ptr<const sdf::Element> &_sdf,
EntityComponentManager &_ecm,
EventManager &/*_eventMgr*/)
{
this->dataPtr->model = Model(_entity);
if (!this->dataPtr->model.Valid(_ecm))
{
gzerr << "FancyDrive should be attached to a model entity. "
<< "Failed to initialize." << std::endl;
return;
}
// parse _sdf params here…
}
void FancyDrive::PreUpdate(const UpdateInfo &_info,
EntityComponentManager &_ecm)
{
if (_info.paused) return;
// mutate _ecm: set joint velocity cmds, etc.
}
void FancyDrive::PostUpdate(const UpdateInfo &_info,
const EntityComponentManager &_ecm)
{
// read-only — publish telemetry, log, etc.
}
} // namespace gz::sim::systems
GZ_ADD_PLUGIN(gz::sim::systems::FancyDrive,
gz::sim::System,
gz::sim::systems::FancyDrive::ISystemConfigure,
gz::sim::systems::FancyDrive::ISystemPreUpdate,
gz::sim::systems::FancyDrive::ISystemPostUpdate)
GZ_ADD_PLUGIN_ALIAS(gz::sim::systems::FancyDrive,
"gz::sim::systems::FancyDrive")
```
The alias is what users actually type in their `<plugin filename="..."
name="gz::sim::systems::FancyDrive">` tags — keep it stable across releases.
## Step 4 — local CMake (`src/systems/fancy_drive/CMakeLists.txt`)
```cmake
gz_add_system(fancy-drive
SOURCES
FancyDrive.cc
PUBLIC_LINK_LIBS
gz-common${GZ_COMMON_VER}::gz-common${GZ_COMMON_VER}
gz-math${GZ_MATH_VER}::gz-math${GZ_MATH_VER}
gz-transport${GZ_TRANSPORT_VER}::gz-transport${GZ_TRANSPORT_VER}
)
```
Use existing sibling directories as the source of truth — the macro name
and link-libs list differs by feature set.
## Step 5 — register with parent CMake (`src/systems/CMakeLists.txt`)
Add an `add_subdirectory(fancy_drive)` entry alphabetically.
## Step 6 — integration test (`test/integration/fancy_drive_system.cc`)
Minimum viable test:
```cpp
#include <gtest/gtest.h>
#include <gz/sim/Server.hh>
#include <gz/sim/TestFixture.hh>
TEST(FancyDriveTest, LoadsCleanly)
{
gz::sim::TestFixture fixture("test/worlds/fancy_drive.sdf");
fixture.Server()->Run(true, 100, false);
// assertions on telemetry / final pose
}
```
Add the target in `test/integration/CMakeLists.txt`.
## Step 7 — example world
Add `examples/worlds/fancy_drive.sdf` with the minimum SDF a user needs
to try the system, and reference it from a short tutorial under
`tutorials/`.
## Step 8 — `Migration.md`
If the system is publicly named or replaces an older one, append a note
to `Migration.md`.
## Don't forget
* `pre-commit run --files <changed files>` before declaring done.
* Add the new system to `Changelog.md` under the upcoming-release section.Use proactively before opening a PR that adds or changes BehaviorTree.CPP nodes or BehaviorTree.ROS2 wrappers (RosActionNode/RosServiceNode/RosTopicPub/SubNode, TreeExecutionServer). Reviews a diff against BT.CPP v4 conventions — node base-class choice, non-blocking ticks, ports/blackboard typing, factory/plugin registration, XML v4, and the ROS 2 wrapper contract. Returns a punch list with file:line anchors, not a rewrite.
Use when a design decision touches Clean Architecture boundaries in a ROS 2 project — which layer a new behaviour belongs to, whether a port belongs in domain or application, whether a new node should be lifecycle-managed, whether to compose nodes or split packages. Returns an architectural recommendation with trade-offs, not implementation.
Use when a design decision touches the gz-sim ECS — where new state should live, which system phase should write it, how to avoid coupling, whether to add a component vs. a member variable, whether a new system should be split or merged with an existing one. Returns an architectural recommendation with trade-offs, not implementation.
Use proactively before opening any gz-sim PR. Reviews a diff against the project's C++17 style, ECS conventions, plugin registration patterns, CMake structure, test placement, Migration.md / Changelog.md expectations, and pre-commit configuration. Returns a punch list, not a rewrite.
Use proactively before opening a PR that adds or changes a ros2_control controller, broadcaster, or hardware component (incl. URDF <ros2_control> bringup). Reviews a diff against ros2_controllers / ros2_control_demos conventions — controller & hardware lifecycle, command/state interface configuration, real-time safety of update()/read()/write(), generate_parameter_library usage, pluginlib registration, chainable-controller correctness, URDF wiring, and tests. Returns a punch list with file:line anchors, not a rewrite.
Use proactively before opening any ROS 2 / Nav 2 PR. Reviews a diff against this template's Clean Architecture, ROS 2 communication, lifecycle, testing, and Nav 2 plugin conventions. Returns a punch list with file:line anchors, not a rewrite.
Use proactively before opening a PR that touches a VDA 5050 connector / fleet bridge. Reviews a diff against VDA 5050 v3.0.0 protocol compliance (topics, QoS, header rules, base/horizon, action state machine, schema validation) and the template's Clean Architecture for the MQTT↔Nav 2 bridge. Returns a punch list with file:line anchors, not a rewrite.
Build the colcon workspace (optionally a single package) and report the outcome.