diff --git a/.codex/CODEX.md b/.codex/CODEX.md new file mode 100644 index 0000000..5a50e00 --- /dev/null +++ b/.codex/CODEX.md @@ -0,0 +1,55 @@ +# ROS2 Clean Architecture Project + +This project is set up with a comprehensive set of **Codex Skills** designed to facilitate ROS2 development following **Clean Architecture** principles. + +## Authoritative References + +- ROS2 Humble docs: `https://docs.ros.org/en/humble/index.html` +- Codex Skills docs: `https://developers.openai.com/codex/skills` +- Codex Rules docs: `https://developers.openai.com/codex/rules` +- OpenAI skills repository: `https://github.com/openai/skills` +- For ROS2 APIs and behavior, prioritize Humble documentation. +- For skill layout and conventions, follow Codex Skills docs first, then align with `openai/skills`. +- For command approval policies, follow Codex Rules docs and maintain `.codex/rules/default.rules`. + +## Available Skills + +The following skills are available in `.codex/skills` and can be used to guide development: + +| Skill Name | Description | Key Components | +| ----------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------- | +| **ros2-node-creation** | Create Clean Architecture compliant nodes | `BaseNode` template, dependency injection, QoS profiles (Python & C++) | +| **ros2-launch-config** | Modular launch files | Composition, `IncludeLaunchDescription`, parameter management, C++ executable support | +| **ros2-service-action** | Services and actions | Server/client wrappers, domain use case integration (Python & C++) | +| **ros2-messaging** | Pub/sub patterns | Domain-driven publishers, generic subscribers, thread-safe buffers, synchronization | +| **ros2-testing** | Testing strategy | Unit (domain), integration (node), E2E (launch), GTest/GMock support | +| **ros2-lifecycle** | Managed nodes | Lifecycle node templates, state transition management, lifecycle clients | +| **ros2-transforms** | TF2 management | TF2 wrappers avoiding domain dependency on `geometry_msgs` | +| **ros2-diagnostics** | Health monitoring | `diagnostic_updater` integration, health entities, frequency monitoring | +| **ros2-bag** | Data recording | Programmatic bag recording and replay utilities (`rosbag2`) | +| **ros2-dockerfile** | Docker build patterns | ROS2 Humble base images, rosdep/colcon layers, multi-stage runtime images | + +## Project Structure + +The project follows a strict separation of concerns: + +- **src/domain/**: Pure business logic, entities, and use cases. No ROS2 dependencies. +- **src/application/**: Application services and interfaces. Orchestrates logic. +- **src/infrastructure/**: ROS2 specific implementations (Nodes, Publishers, Subscribers). + +## Getting Started + +To use a skill, reference the skill file (e.g., `.codex/skills/ros2-node-creation/SKILL.md`) for templates and best practices. +For containerization, use `.codex/skills/ros2-dockerfile/SKILL.md` with `.codex/rules/ros2_dockerfile.md`. + +## Common Commands + +For a comprehensive list of ROS2 commands, build instructions, and debugging tools, please refer to: + +- **[ROS2 Commands Reference](.codex/commands/ros2.md)**: `colcon`, `ros2`, `rqt`, etc. + +### Quick Reference + +- **Build**: `colcon build --symlink-install` +- **Test**: `colcon test` +- **Source**: `source install/setup.bash` diff --git a/.codex/commands/ros2.md b/.codex/commands/ros2.md new file mode 100644 index 0000000..ea6f208 --- /dev/null +++ b/.codex/commands/ros2.md @@ -0,0 +1,72 @@ +# ROS2 Common Commands + +## Build System (colcon) + +| Action | Command | Description | +| ------------------- | ------------------------------------------- | ------------------------------------------------------------------------ | +| **Build Workspace** | `colcon build --symlink-install` | Build all packages with symlinks (allows python changes without rebuild) | +| **Build Package** | `colcon build --packages-select ` | Build a specific package only | +| **Build Up To** | `colcon build --packages-up-to ` | Build a package and its dependencies | +| **Clean Build** | `rm -rf build/ install/ log/` | Remove build artifacts (use with caution) | +| **Test** | `colcon test` | Run tests for all packages | +| **Test Output** | `colcon test-result --all` | Show test results | + +## Environment + +| Action | Command | Description | +| ------------------ | --------------------------- | ---------------------------- | ------------------------------- | +| **Source Overlay** | `source install/setup.bash` | Source the workspace overlay | +| **Check Env** | `printenv | grep ROS` | Check ROS environment variables | + +## Introspection (ros2) + +### Nodes + +| Command | Description | +| ---------------------------- | ------------------------------------------------ | +| `ros2 node list` | List all running nodes | +| `ros2 node info ` | Show subscribers, publishers, services of a node | + +### Topics + +| Command | Description | +| -------------------------------------- | ------------------------------------------------ | +| `ros2 topic list` | List all active topics | +| `ros2 topic echo ` | Print messages from a topic to screen | +| `ros2 topic info ` | Show message type and publisher/subscriber count | +| `ros2 topic pub ` | Publish a message manually | +| `ros2 topic hz ` | Check publishing frequency | +| `ros2 topic bw ` | Check bandwidth usage | + +### Services + +| Command | Description | +| ------------------------------------------- | ----------------------- | +| `ros2 service list` | List available services | +| `ros2 service call ` | Call a service manually | +| `ros2 service type ` | Show service type | + +### Actions + +| Command | Description | +| ---------------------------------------------- | ---------------------- | +| `ros2 action list` | List available actions | +| `ros2 action send_goal ` | Send an action goal | + +### Parameters + +| Command | Description | +| --------------------------------------- | ------------------------------------- | +| `ros2 param list` | List all parameters | +| `ros2 param get ` | Get a parameter value | +| `ros2 param set ` | Set a parameter value dynamically | +| `ros2 param dump ` | Dump all parameters of a node to YAML | + +## Debugging Implementation + +| Tool | Command | Description | +| --------------- | -------------------------------- | ----------------------------------------- | +| **RQT Graph** | `rqt_graph` | Visualize node and topic connections | +| **RQT Console** | `rqt_console` | View log messages with filtering | +| **TF Tree** | `ros2 run tf2_tools view_frames` | Generate PDF of TF tree | +| **Doctor** | `ros2 doctor` | Check for system issues/misconfigurations | diff --git a/.codex/rules/clean_architecture.md b/.codex/rules/clean_architecture.md new file mode 100644 index 0000000..63565d2 --- /dev/null +++ b/.codex/rules/clean_architecture.md @@ -0,0 +1,316 @@ +--- +description: Clean Architecture Principles for ROS2 Projects +--- + +# Clean Architecture Rules +## Authoritative Sources + +- ROS2 Humble main docs: `https://docs.ros.org/en/humble/index.html` +- Package development guide: `https://docs.ros.org/en/humble/How-To-Guides/Developing-a-ROS-2-Package.html` +- Tutorials index: `https://docs.ros.org/en/humble/Tutorials.html` +- Skill authoring patterns: `https://github.com/openai/skills` + +When this file conflicts with external references, prefer ROS2 Humble docs for ROS behavior and `openai/skills` for skill structure. + +This rule file defines how to apply Clean Architecture principles in ROS2 projects. + +## Layer Dependency Rules + +``` +┌─────────────────────────────────────────────────────────┐ +│ PRESENTATION LAYER │ +│ (CLI, GUI, API - User Interfaces) │ +└─────────────────────────┬───────────────────────────────┘ + │ depends on +┌─────────────────────────▼───────────────────────────────┐ +│ APPLICATION LAYER │ +│ (Use Cases, Application Services) │ +└─────────────────────────┬───────────────────────────────┘ + │ depends on +┌─────────────────────────▼───────────────────────────────┐ +│ DOMAIN LAYER │ +│ (Entities, Value Objects, Domain Interfaces) │ +└─────────────────────────▲───────────────────────────────┘ + │ implements +┌─────────────────────────┴───────────────────────────────┐ +│ INFRASTRUCTURE LAYER │ +│ (ROS2 Adapters, Hardware, Persistence) │ +└─────────────────────────────────────────────────────────┘ +``` + +**CRITICAL RULES:** + +1. The Domain layer must NOT depend on any outer layer. +2. The Application layer can only depend on the Domain layer. +3. The Infrastructure layer implements Domain interfaces. +4. The Presentation layer can only depend on the Application layer. + +## Domain Layer + +### Entities + +#### Python Example + +```python +# CORRECT: Pure Python, no external dependencies +from dataclasses import dataclass +from abc import ABC, abstractmethod +from uuid import UUID, uuid4 + +@dataclass +class Entity(ABC): + """Base entity with unique identifier.""" + id: UUID = None + + def __post_init__(self): + if self.id is None: + self.id = uuid4() + + def __eq__(self, other): + if not isinstance(other, Entity): + return False + return self.id == other.id + + def __hash__(self): + return hash(self.id) + +@dataclass +class Robot(Entity): + """Robot domain entity.""" + name: str + max_velocity: float + max_acceleration: float +``` + +#### C++ Example + +```cpp +// CORRECT: Pure C++, no ROS2 dependencies +#pragma once +#include +#include + +namespace domain::entities { + +struct Robot { + std::string id; + std::string name; + double max_velocity; + double max_acceleration; + + bool operator==(const Robot& other) const { + return id == other.id; + } +}; + +} // namespace +``` + +### Value Objects + +#### Python Example + +```python +# CORRECT: Immutable, no identity +from dataclasses import dataclass + +@dataclass(frozen=True) +class Position: + """Position value object.""" + x: float + y: float + z: float = 0.0 + + def distance_to(self, other: 'Position') -> float: + return ((self.x - other.x)**2 + + (self.y - other.y)**2 + + (self.z - other.z)**2) ** 0.5 +``` + +#### C++ Example + +```cpp +// CORRECT: Immutable struct (const members or accessors) +#pragma once +#include + +namespace domain::values { + +struct Position { + const double x; + const double y; + const double z; + + Position(double x, double y, double z = 0.0) : x(x), y(y), z(z) {} + + double distance_to(const Position& other) const { + return std::sqrt(std::pow(x - other.x, 2) + + std::pow(y - other.y, 2) + + std::pow(z - other.z, 2)); + } +}; + +} // namespace +``` + +### Domain Interfaces (Ports) + +#### Python Example + +```python +# CORRECT: Abstract interface in domain layer +from abc import ABC, abstractmethod +from typing import Optional, List + +class RobotRepository(ABC): + """Port for robot data access.""" + + @abstractmethod + def get_by_id(self, robot_id: UUID) -> Optional[Robot]: + pass + + @abstractmethod + def save(self, robot: Robot) -> Robot: + pass +``` + +#### C++ Example + +```cpp +// CORRECT: Abstract base class (interface) +#pragma once +#include "domain/entities/robot.hpp" +#include +#include + +namespace domain::interfaces { + +class IRobotRepository { +public: + virtual ~IRobotRepository() = default; + virtual std::optional get_by_id(const std::string& id) = 0; + virtual void save(const entities::Robot& robot) = 0; +}; + +} // namespace +``` + +## Application Layer + +### Use Cases + +#### Python Example + +```python +# CORRECT: Business logic orchestration +class MoveRobotUseCase: + def __init__(self, motion_controller: MotionController): + self._motion_controller = motion_controller + + def execute(self, target: Position) -> bool: + return self._motion_controller.move_to(target) +``` + +#### C++ Example + +```cpp +// CORRECT: Business logic orchestration +#pragma once +#include "domain/interfaces/motion_controller.hpp" + +namespace application::use_cases { + +class MoveRobotUseCase { +public: + explicit MoveRobotUseCase(std::shared_ptr controller) + : controller_(controller) {} + + bool execute(const domain::values::Position& target) { + return controller_->move_to(target); + } + +private: + std::shared_ptr controller_; +}; + +} // namespace +``` + +## Infrastructure Layer + +### ROS2 Adapters + +#### Python Example + +```python +# CORRECT: Implements domain interface +import rclpy +from rclpy.node import Node +from geometry_msgs.msg import Twist + +class ROS2MotionController(MotionController): + """ROS2 adapter implementing MotionController port.""" + # ... implementation ... +``` + +#### C++ Example + +```cpp +// CORRECT: Implements domain interface +#include "domain/interfaces/motion_controller.hpp" +#include +#include + +namespace infrastructure::ros2::adapters { + +class ROS2MotionController : public domain::interfaces::IMotionController { +public: + explicit ROS2MotionController(rclcpp::Node::SharedPtr node) : node_(node) { + pub_ = node_->create_publisher("cmd_vel", 10); + } + + bool move_to(const domain::values::Position& target) override { + // Implementation... + return true; + } + +private: + rclcpp::Node::SharedPtr node_; + rclcpp::Publisher::SharedPtr pub_; +}; + +} // namespace +``` + +## Anti-Patterns to Avoid + +### Python + +```python +# INCORRECT: Domain importing ROS2 +from rclpy.node import Node # Domain should NOT import ROS2! + +class Robot(Entity): + def __init__(self, node: Node): # WRONG! + self.node = node + +# INCORRECT: Business logic in infrastructure +class ROS2MotionController: + def move_to(self, position): + # WRONG: Business rules should be in domain/application + if self.battery_level < 20: + return False +``` + +### C++ + +```cpp +// INCORRECT: Domain header including ROS2 +#include // WRONG in Domain! + +namespace domain::entities { + class Robot { + rclcpp::Node::SharedPtr node_; // WRONG! + }; +} +``` diff --git a/.codex/rules/default.rules b/.codex/rules/default.rules new file mode 100644 index 0000000..a807a7b --- /dev/null +++ b/.codex/rules/default.rules @@ -0,0 +1,64 @@ +# Codex command execution policy for this repository. +# Reference: https://developers.openai.com/codex/rules + +# Allow common ROS2 development commands outside sandbox. +prefix_rule( + pattern = [["colcon", "ros2", "rosdep", "vcs", "pytest"]], + decision = "allow", + justification = "Core ROS2 development and testing commands are allowed.", + match = [ + "colcon build --symlink-install", + "colcon test --packages-select my_pkg", + "ros2 topic list", + "rosdep install --from-paths src --ignore-src -r -y", + "pytest -q", + ], + not_match = [ + "python3 -m pytest -q", + ], +) + +# Allow routine git inspection and local commits. +prefix_rule( + pattern = ["git", ["status", "diff", "log", "add", "commit"]], + decision = "allow", + justification = "Local git inspection and commit operations are part of normal workflow.", + match = [ + "git status", + "git diff", + "git log --oneline -20", + "git add .", + "git commit -m update", + ], + not_match = [ + "git push origin main", + ], +) + +# Prompt before pushing code remotely. +prefix_rule( + pattern = ["git", "push"], + decision = "prompt", + justification = "Remote writes require explicit approval.", +) + +# Prompt before Docker usage due to system/network impact. +prefix_rule( + pattern = ["docker"], + decision = "prompt", + justification = "Docker commands can affect system state and should be reviewed.", +) + +# Forbid dangerous destructive commands. +prefix_rule( + pattern = ["rm", "-rf"], + decision = "forbidden", + justification = "Use safer targeted deletes (`rm -f `) instead of recursive force removal.", +) + +# Forbid privileged escalation. +prefix_rule( + pattern = ["sudo"], + decision = "forbidden", + justification = "Do not use sudo in Codex sessions; request explicit user-run steps instead.", +) diff --git a/.codex/rules/robot_specific.md b/.codex/rules/robot_specific.md new file mode 100644 index 0000000..ca158dd --- /dev/null +++ b/.codex/rules/robot_specific.md @@ -0,0 +1,167 @@ +--- +description: Robot-Specific Standards (URDF, TF2, Navigation) +--- + +# Robot-Specific Standards +## Authoritative Sources + +- ROS2 Humble main docs: `https://docs.ros.org/en/humble/index.html` +- Package development guide: `https://docs.ros.org/en/humble/How-To-Guides/Developing-a-ROS-2-Package.html` +- Tutorials index: `https://docs.ros.org/en/humble/Tutorials.html` +- Skill authoring patterns: `https://github.com/openai/skills` + +When this file conflicts with external references, prefer ROS2 Humble docs for ROS behavior and `openai/skills` for skill structure. + +## URDF/Xacro Best Practices + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +## TF2 Frame Tree + +``` +map + └── odom + └── base_footprint + └── base_link + ├── lidar_link + ├── camera_link + │ └── camera_optical + ├── imu_link + └── left_wheel +``` + +## Navigation Stack Configuration + +```yaml +# nav2_params.yaml +bt_navigator: + ros__parameters: + global_frame: map + robot_base_frame: base_link + odom_topic: /odom + +controller_server: + ros__parameters: + controller_frequency: 20.0 + FollowPath: + plugin: dwb_core::DWBLocalPlanner + max_vel_x: 0.5 + max_vel_theta: 1.0 + +planner_server: + ros__parameters: + GridBased: + plugin: nav2_navfn_planner/NavfnPlanner + tolerance: 0.5 + use_astar: true +``` + +## Sensor Integration + +### Python Example + +```python +# Sensor data publishing pattern +from sensor_msgs.msg import LaserScan, Imu, Image + +class SensorNode(Node): + def __init__(self): + super().__init__('sensor_node') + + # LiDAR publisher + self.lidar_pub = self.create_publisher( + LaserScan, 'scan', + qos_profile_sensor_data + ) + + # Camera publisher + self.camera_pub = self.create_publisher( + Image, 'camera/image_raw', + qos_profile_sensor_data + ) + + # IMU publisher + self.imu_pub = self.create_publisher( + Imu, 'imu/data', + qos_profile_sensor_data + ) +``` + +## Motor Control Pattern + +### Python Example + +```python +from geometry_msgs.msg import Twist + +class MotorController(Node): + def __init__(self): + super().__init__('motor_controller') + + self.cmd_sub = self.create_subscription( + Twist, 'cmd_vel', + self.cmd_callback, 10 + ) + + # Hardware interface + self._left_motor = Motor(pin=17) + self._right_motor = Motor(pin=18) + + def cmd_callback(self, msg: Twist): + # Differential drive kinematics + linear = msg.linear.x + angular = msg.angular.z + + left_vel = linear - angular * self.wheel_base / 2 + right_vel = linear + angular * self.wheel_base / 2 + + self._left_motor.set_velocity(left_vel) + self._right_motor.set_velocity(right_vel) +``` diff --git a/.codex/rules/ros2_communication.md b/.codex/rules/ros2_communication.md new file mode 100644 index 0000000..39f7dc4 --- /dev/null +++ b/.codex/rules/ros2_communication.md @@ -0,0 +1,304 @@ +--- +description: ROS2 Communication Standards (Topics, QoS, Messages) +--- + +# ROS2 Communication Standards +## Authoritative Sources + +- ROS2 Humble main docs: `https://docs.ros.org/en/humble/index.html` +- Package development guide: `https://docs.ros.org/en/humble/How-To-Guides/Developing-a-ROS-2-Package.html` +- Tutorials index: `https://docs.ros.org/en/humble/Tutorials.html` +- Skill authoring patterns: `https://github.com/openai/skills` + +When this file conflicts with external references, prefer ROS2 Humble docs for ROS behavior and `openai/skills` for skill structure. + +This rule file defines communication standards in ROS2. + +## Topic Naming Conventions + +``` +# Format: /// +# Examples: + +# Sensor topics +/robot/sensors/lidar/scan +/robot/sensors/camera/image_raw +/robot/sensors/imu/data + +# Control topics +/robot/control/cmd_vel +/robot/control/joint_commands + +# State topics +/robot/state/odometry +/robot/state/battery +/robot/state/joint_states + +# Navigation topics +/robot/navigation/goal +/robot/navigation/path +/robot/navigation/costmap + +# Perception topics +/robot/perception/detections +/robot/perception/markers +``` + +### Naming Rules + +| Rule | Example | +| ------------------------------ | ------------------------------ | +| Use lowercase | `/robot/cmd_vel` | +| Use underscores for multi-word | `/joint_states` | +| Avoid abbreviations | `/camera/image` not `/cam/img` | +| Use namespaces | `/robot1/cmd_vel` | +| Be descriptive | `/laser_scan` not `/ls` | + +## QoS Profiles + +### Python Example + +```python +from rclpy.qos import ( + QoSProfile, + QoSReliabilityPolicy, + QoSDurabilityPolicy, + QoSHistoryPolicy, + QoSLivelinessPolicy +) + +# Sensor Data QoS - High frequency, best effort +SENSOR_QOS = QoSProfile( + reliability=QoSReliabilityPolicy.BEST_EFFORT, + durability=QoSDurabilityPolicy.VOLATILE, + history=QoSHistoryPolicy.KEEP_LAST, + depth=5 +) + +# Control Commands QoS - Reliable delivery +CONTROL_QOS = QoSProfile( + reliability=QoSReliabilityPolicy.RELIABLE, + durability=QoSDurabilityPolicy.VOLATILE, + history=QoSHistoryPolicy.KEEP_LAST, + depth=10 +) + +# State/Configuration QoS - Latched topics +STATE_QOS = QoSProfile( + reliability=QoSReliabilityPolicy.RELIABLE, + durability=QoSDurabilityPolicy.TRANSIENT_LOCAL, + history=QoSHistoryPolicy.KEEP_LAST, + depth=1 +) +``` + +### C++ Example + +```cpp +#include + +// Sensor Data QoS +static const rclcpp::QoS SENSOR_QOS = rclcpp::QoS(5) + .reliability(rmw_qos_reliability_policy_t::RMW_QOS_POLICY_RELIABILITY_BEST_EFFORT) + .durability(rmw_qos_durability_policy_t::RMW_QOS_POLICY_DURABILITY_VOLATILE); + +// Control Commands QoS +static const rclcpp::QoS CONTROL_QOS = rclcpp::QoS(10) + .reliability(rmw_qos_reliability_policy_t::RMW_QOS_POLICY_RELIABILITY_RELIABLE) + .durability(rmw_qos_durability_policy_t::RMW_QOS_POLICY_DURABILITY_VOLATILE); + +// State/Configuration QoS +static const rclcpp::QoS STATE_QOS = rclcpp::QoS(1) + .reliability(rmw_qos_reliability_policy_t::RMW_QOS_POLICY_RELIABILITY_RELIABLE) + .durability(rmw_qos_durability_policy_t::RMW_QOS_POLICY_DURABILITY_TRANSIENT_LOCAL); +``` + +### QoS Selection Guide + +| Data Type | Reliability | Durability | Depth | Example | +| ------------------ | ----------- | --------------- | ----- | ----------------- | +| Sensor (high freq) | BEST_EFFORT | VOLATILE | 1-5 | LiDAR, Camera | +| Commands | RELIABLE | VOLATILE | 10 | cmd_vel | +| State/Config | RELIABLE | TRANSIENT_LOCAL | 1 | robot_description | +| Transforms | RELIABLE | VOLATILE | 100 | tf | +| Map | RELIABLE | TRANSIENT_LOCAL | 1 | occupancy_grid | + +## Custom Message Definition + +``` +# File: msg/RobotState.msg +# Header for timestamp and frame +std_msgs/Header header + +# Position +geometry_msgs/Point position + +# Orientation (quaternion) +geometry_msgs/Quaternion orientation + +# Velocity +geometry_msgs/Twist velocity + +# Status enum +uint8 STATUS_IDLE = 0 +uint8 STATUS_MOVING = 1 +uint8 STATUS_ERROR = 2 +uint8 status + +# Battery percentage (0-100) +float32 battery_level + +# Array of sensor readings +float64[] sensor_readings +``` + +## Custom Service Definition + +``` +# File: srv/SetRobotMode.srv +# Request +--- +# Mode to set +uint8 MODE_MANUAL = 0 +uint8 MODE_AUTO = 1 +uint8 MODE_EMERGENCY = 2 +uint8 mode + +# Optional parameters +string[] parameters +--- +# Response +bool success +string message +float64 transition_time +``` + +## Custom Action Definition + +``` +# File: action/NavigateToGoal.action +# Goal +--- +geometry_msgs/PoseStamped target_pose +float32 max_velocity +bool allow_replanning +--- +# Result +bool success +string message +float64 total_time +float64 total_distance +--- +# Feedback +geometry_msgs/PoseStamped current_pose +float32 distance_remaining +float32 estimated_time_remaining +uint8 recovery_count +``` + +## Message Package Structure + +``` +robot_interfaces/ +├── CMakeLists.txt +├── package.xml +├── msg/ +│ ├── RobotState.msg +├── srv/ +│ ├── SetRobotMode.srv +└── action/ + └── NavigateToGoal.action +``` + +### CMakeLists.txt for Message Package + +```cmake +cmake_minimum_required(VERSION 3.8) +project(robot_interfaces) + +find_package(ament_cmake REQUIRED) +find_package(rosidl_default_generators REQUIRED) +find_package(std_msgs REQUIRED) +find_package(geometry_msgs REQUIRED) +find_package(action_msgs REQUIRED) + +rosidl_generate_interfaces(${PROJECT_NAME} + "msg/RobotState.msg" + "srv/SetRobotMode.srv" + "action/NavigateToGoal.action" + DEPENDENCIES std_msgs geometry_msgs action_msgs +) + +ament_export_dependencies(rosidl_default_runtime) +ament_package() +``` + +### package.xml for Message Package + +```xml + + + robot_interfaces + 0.1.0 + Robot interface definitions + Developer + Apache-2.0 + + ament_cmake + rosidl_default_generators + + std_msgs + geometry_msgs + action_msgs + + rosidl_default_runtime + + rosidl_interface_packages + + + ament_cmake + + +``` + +## TF2 Transform Best Practices + +### Python Example + +```python +import tf2_ros +from geometry_msgs.msg import TransformStamped + +class TransformPublisher: + """Best practices for TF2 transforms.""" + + def __init__(self, node): + self._node = node + + # Static transform broadcaster (for fixed transforms) + self._static_broadcaster = tf2_ros.StaticTransformBroadcaster(node) + + # Dynamic transform broadcaster + self._dynamic_broadcaster = tf2_ros.TransformBroadcaster(node) +``` + +### C++ Example + +```cpp +#include +#include + +class TransformPublisher { +public: + TransformPublisher(rclcpp::Node::SharedPtr node) + : node_(node) { + static_broadcaster_ = std::make_shared(node); + dynamic_broadcaster_ = std::make_shared(node); + } + // ... +private: + rclcpp::Node::SharedPtr node_; + std::shared_ptr static_broadcaster_; + std::shared_ptr dynamic_broadcaster_; +}; +``` diff --git a/.codex/rules/ros2_dockerfile.md b/.codex/rules/ros2_dockerfile.md new file mode 100644 index 0000000..d84fe8e --- /dev/null +++ b/.codex/rules/ros2_dockerfile.md @@ -0,0 +1,68 @@ +--- +description: ROS2 Dockerfile Standards (Humble) +--- + +# ROS2 Dockerfile Standards +## Authoritative Sources + +- ROS2 Humble docs: `https://docs.ros.org/en/humble/index.html` +- ROS2 package development guide: `https://docs.ros.org/en/humble/How-To-Guides/Developing-a-ROS-2-Package.html` +- OpenAI skill conventions: `https://github.com/openai/skills` + +When this file conflicts with external references, prefer ROS2 Humble docs for ROS behavior and dependency setup. + +## Base Image Policy + +- Prefer official ROS images, starting with `ros:humble-ros-base`. +- Use `ros:humble-desktop` only when GUI tooling is required. +- Keep architecture explicit when needed (`--platform=linux/amd64` or `linux/arm64`). + +## Dockerfile Layering Rules + +- Put apt dependency install before source copy to maximize cache reuse. +- Use one `apt-get update` + `apt-get install` layer, then clean apt lists in the same layer. +- Do not run `apt upgrade` inside Dockerfiles unless explicitly required. +- Copy dependency manifests (`package.xml`, `requirements.txt`) before full source. + +## Dependency Rules (ROS2 + Colcon) + +- Always run `rosdep install` against `src` before `colcon build`. +- Use `--merge-install` and `--symlink-install` for dev images. +- Use `--merge-install` without symlinks for runtime-focused images. +- Source `/opt/ros/humble/setup.bash` before `rosdep` and `colcon` commands. + +## Runtime and Security Rules + +- Create and run as a non-root user for normal runtime operations. +- Keep entrypoint deterministic: source ROS environment, then `exec "$@"`. +- Do not hardcode secrets or tokens in Dockerfiles or image layers. + +## Minimal Dev Dockerfile Pattern + +```dockerfile +FROM ros:humble-ros-base + +SHELL ["/bin/bash", "-c"] +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + python3-colcon-common-extensions \ + python3-rosdep \ + git \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /ws +COPY src /ws/src + +RUN source /opt/ros/humble/setup.bash && \ + rosdep update && \ + rosdep install --from-paths src --ignore-src -r -y && \ + colcon build --merge-install --symlink-install +``` + +## Multi-Stage Build Guideline + +- Stage 1 (`builder`): install build toolchain + build workspace. +- Stage 2 (`runtime`): copy only install artifacts and runtime deps. +- Runtime stage should avoid compilers unless needed by runtime plugins. diff --git a/.codex/rules/ros2_general.md b/.codex/rules/ros2_general.md new file mode 100644 index 0000000..a36f644 --- /dev/null +++ b/.codex/rules/ros2_general.md @@ -0,0 +1,203 @@ +--- +description: ROS2 General Conventions and Best Practices +--- + +# ROS2 General Conventions +## Authoritative Sources + +- ROS2 Humble main docs: `https://docs.ros.org/en/humble/index.html` +- Package development guide: `https://docs.ros.org/en/humble/How-To-Guides/Developing-a-ROS-2-Package.html` +- Tutorials index: `https://docs.ros.org/en/humble/Tutorials.html` +- Skill authoring patterns: `https://github.com/openai/skills` + +When this file conflicts with external references, prefer ROS2 Humble docs for ROS behavior and `openai/skills` for skill structure. + +This rule file defines general conventions and best practices for ROS2 projects. + +## Package Naming + +``` +# Format: __ +# Examples: +robot_navigation_core +robot_perception_msgs +robot_control_interfaces +``` + +- Package names must be in **snake_case**. +- Use only lowercase letters and underscores. +- Names should be short and descriptive. + +## File Structure + +``` +package_name/ +├── package.xml # Package manifest +├── setup.py # Python package setup +├── setup.cfg # Setup configuration (Python) +├── CMakeLists.txt # Build configuration (C++) +├── resource/ +│ └── package_name # Marker file +├── package_name/ # Python source +│ ├── __init__.py +│ └── node_file.py +├── src/ # C++ source +│ └── node_file.cpp +├── include/ # C++ headers +│ └── package_name/ +│ └── node_file.hpp +├── launch/ +│ └── node_launch.py +├── config/ +│ └── params.yaml +└── test/ + └── test_node.py +``` + +## CMakeLists.txt (C++ Packages) + +```cmake +cmake_minimum_required(VERSION 3.8) +project(package_name) + +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) + +# Add targets here +add_executable(my_node src/node_file.cpp) +target_include_directories(my_node PUBLIC + $ + $) +ament_target_dependencies(my_node rclcpp) + +install(TARGETS my_node + DESTINATION lib/${PROJECT_NAME}) + +install(DIRECTORY include/ + DESTINATION include) + +if(BUILD_TESTING) + find_package(ament_lint_auto REQUIRED) + ament_lint_auto_find_test_dependencies() +endif() + +ament_package() +``` + +## Python Setup Best Practices + +```python +from setuptools import find_packages, setup + +package_name = 'package_name' + +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/node_launch.py']), + ('share/' + package_name + '/config', + ['config/params.yaml']), + ], + install_requires=['setuptools'], + zip_safe=True, + maintainer='Your Name', + maintainer_email='your.email@example.com', + description='Package description', + license='Apache-2.0', + tests_require=['pytest'], + entry_points={ + 'console_scripts': [ + 'node_name = package_name.node_file:main', + ], + }, +) +``` + +## Launch Files + +```python +from launch import LaunchDescription +from launch_ros.actions import Node +from launch.actions import DeclareLaunchArgument +from launch.substitutions import LaunchConfiguration + +def generate_launch_description(): + # Declare arguments + use_sim_time = DeclareLaunchArgument( + 'use_sim_time', + default_value='false', + description='Use simulation time' + ) + + # Define nodes + my_node = Node( + package='package_name', + executable='node_name', + name='node_name', + parameters=[{ + 'use_sim_time': LaunchConfiguration('use_sim_time') + }], + output='screen' + ) + + return LaunchDescription([ + use_sim_time, + my_node, + ]) +``` + +## Parameter Files (YAML) + +```yaml +/**: + ros__parameters: + # Node-specific parameters + update_rate: 10.0 + + # Nested parameters + sensor: + frame_id: "base_link" + range_min: 0.1 + range_max: 10.0 +``` + +## Logging Best Practices + +```python +# Use appropriate log levels +self.get_logger().debug('Detailed debug info') +self.get_logger().info('Normal operation info') +self.get_logger().warn('Warning message') +self.get_logger().error('Error message') +self.get_logger().fatal('Fatal error') + +# Use throttled logging for high-frequency messages +self.get_logger().info('Status update', throttle_duration_sec=1.0) + +# Use once logging for initialization +self.get_logger().info('Node initialized', once=True) +``` + +## Workspace Organization + +``` +ros2_ws/ +├── src/ +│ ├── robot_core/ # Core packages +│ ├── robot_interfaces/ # Message/service definitions +│ ├── robot_bringup/ # Launch and config +│ └── third_party/ # External dependencies +├── build/ # Build directory +├── install/ # Install directory +└── log/ # Log directory +``` diff --git a/.codex/rules/ros2_nodes.md b/.codex/rules/ros2_nodes.md new file mode 100644 index 0000000..44b3c8e --- /dev/null +++ b/.codex/rules/ros2_nodes.md @@ -0,0 +1,290 @@ +--- +description: ROS2 Node Development Standards +--- + +# ROS2 Node Standards +## Authoritative Sources + +- ROS2 Humble main docs: `https://docs.ros.org/en/humble/index.html` +- Package development guide: `https://docs.ros.org/en/humble/How-To-Guides/Developing-a-ROS-2-Package.html` +- Tutorials index: `https://docs.ros.org/en/humble/Tutorials.html` +- Skill authoring patterns: `https://github.com/openai/skills` + +When this file conflicts with external references, prefer ROS2 Humble docs for ROS behavior and `openai/skills` for skill structure. + +This rule file defines how to create and configure ROS2 nodes. + +## Node Base Class + +### Python Example + +```python +import rclpy +from rclpy.node import Node +from rclpy.executors import MultiThreadedExecutor +from rclpy.callback_groups import MutuallyExclusiveCallbackGroup, ReentrantCallbackGroup + +class BaseRobotNode(Node): + """Base class for all robot nodes following Clean Architecture.""" + + def __init__(self, node_name: str, **kwargs): + super().__init__(node_name, **kwargs) + + # Declare common parameters + self.declare_parameter('update_rate', 10.0) + self.declare_parameter('use_sim_time', False) + + # Get parameters + self._update_rate = self.get_parameter('update_rate').value + + # Setup logging + self.get_logger().info(f'{node_name} initialized') + + def create_rate_timer(self, callback): + """Create timer based on update_rate parameter.""" + period = 1.0 / self._update_rate + return self.create_timer(period, callback) + + def safe_destroy(self): + """Safely destroy the node.""" + self.get_logger().info('Shutting down...') + self.destroy_node() +``` + +### C++ Example + +```cpp +#include +#include + +class BaseRobotNode : public rclcpp::Node { +public: + BaseRobotNode(const std::string& node_name, const rclcpp::NodeOptions& options = rclcpp::NodeOptions()) + : Node(node_name, options) { + + // Declare common parameters + this->declare_parameter("update_rate", 10.0); + this->declare_parameter("use_sim_time", false); + + update_rate_ = this->get_parameter("update_rate").as_double(); + + RCLCPP_INFO(this->get_logger(), "%s initialized", node_name.c_str()); + } + +protected: + rclcpp::TimerBase::SharedPtr create_rate_timer(std::function callback) { + auto period = std::chrono::duration(1.0 / update_rate_); + return this->create_wall_timer(period, callback); + } + + double update_rate_; +}; +``` + +## Publisher Pattern + +### Python Example + +```python +from rclpy.qos import QoSProfile, ReliabilityPolicy, DurabilityPolicy + +class PublisherMixin: + """Mixin for standardized publisher creation.""" + + def create_reliable_publisher(self, msg_type, topic_name, queue_size=10): + """Create a reliable publisher for critical data.""" + qos = QoSProfile( + reliability=ReliabilityPolicy.RELIABLE, + durability=DurabilityPolicy.TRANSIENT_LOCAL, + depth=queue_size + ) + return self.create_publisher(msg_type, topic_name, qos) + + def create_sensor_publisher(self, msg_type, topic_name, queue_size=10): + """Create a best-effort publisher for sensor data.""" + qos = QoSProfile( + reliability=ReliabilityPolicy.BEST_EFFORT, + durability=DurabilityPolicy.VOLATILE, + depth=queue_size + ) + return self.create_publisher(msg_type, topic_name, qos) +``` + +### C++ Example + +```cpp +// In your node class or mixin +template +typename rclcpp::Publisher::SharedPtr create_reliable_publisher( + const std::string& topic_name, size_t queue_size = 10) { + + rclcpp::QoS qos(queue_size); + qos.reliable(); + qos.transient_local(); + + return this->create_publisher(topic_name, qos); +} + +template +typename rclcpp::Publisher::SharedPtr create_sensor_publisher( + const std::string& topic_name, size_t queue_size = 10) { + + rclcpp::QoS qos(queue_size); + qos.best_effort(); + qos.durability_volatile(); + + return this->create_publisher(topic_name, qos); +} +``` + +## Subscriber Pattern + +### Python Example + +```python +from rclpy.qos import qos_profile_sensor_data, qos_profile_system_default + +class SubscriberMixin: + """Mixin for standardized subscriber creation.""" + + def create_sensor_subscriber(self, msg_type, topic_name, callback): + """Create subscriber for sensor data (best-effort).""" + return self.create_subscription( + msg_type, + topic_name, + callback, + qos_profile_sensor_data + ) + + def create_reliable_subscriber(self, msg_type, topic_name, callback): + """Create subscriber for reliable data.""" + return self.create_subscription( + msg_type, + topic_name, + callback, + qos_profile_system_default + ) +``` + +## Service Pattern + +### Python Example + +```python +from rclpy.service import Service +from example_interfaces.srv import SetBool + +class ServiceNode(BaseRobotNode): + """Example node with service.""" + + def __init__(self): + super().__init__('service_node') + + # Create service + self._srv = self.create_service( + SetBool, + 'enable_feature', + self._enable_callback + ) + + def _enable_callback(self, request, response): + """Service callback.""" + try: + # Business logic here + self._feature_enabled = request.data + response.success = True + response.message = f"Feature {'enabled' if request.data else 'disabled'}" + except Exception as e: + response.success = False + response.message = str(e) + return response +``` + +### C++ Example + +```cpp +#include + +class ServiceNode : public BaseRobotNode { +public: + ServiceNode() : BaseRobotNode("service_node") { + using namespace std::placeholders; + srv_ = this->create_service( + "enable_feature", + std::bind(&ServiceNode::enable_callback, this, _1, _2)); + } + +private: + void enable_callback( + const std::shared_ptr request, + std::shared_ptr response) { + + try { + // Business Logic + response->success = true; + response->message = request->data ? "Feature enabled" : "Feature disabled"; + } catch (const std::exception& e) { + response->success = false; + response->message = e.what(); + } + } + rclcpp::Service::SharedPtr srv_; +}; +``` + +## Action Server Pattern + +### Python Example + +See `ros2_nodes.md` for full Python example. + +### C++ Example + +```cpp +#include +#include + +class ActionServerNode : public BaseRobotNode { +public: + using Fibonacci = action_tutorials_interfaces::action::Fibonacci; + using GoalHandle = rclcpp_action::ServerGoalHandle; + + ActionServerNode() : BaseRobotNode("action_server") { + action_server_ = rclcpp_action::create_server( + this, + "fibonacci", + std::bind(&ActionServerNode::handle_goal, this, std::placeholders::_1, std::placeholders::_2), + std::bind(&ActionServerNode::handle_cancel, this, std::placeholders::_1), + std::bind(&ActionServerNode::handle_accepted, this, std::placeholders::_1) + ); + } + // ... callback implementations ... +private: + rclcpp_action::Server::SharedPtr action_server_; +}; +``` + +## Lifecycle Node Pattern + +### Python Example + +See `ros2_nodes.md` for full Python example. + +### C++ Example + +```cpp +#include + +class ManagedRobotNode : public rclcpp_lifecycle::LifecycleNode { +public: + ManagedRobotNode(const std::string& node_name) : LifecycleNode(node_name) {} + + rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn + on_configure(const rclcpp_lifecycle::State&) { + RCLCPP_INFO(get_logger(), "Configuring..."); + // publisher_ = this->create_publisher... + return rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn::SUCCESS; + } + // Implement on_activate, on_deactivate, on_cleanup... +}; +``` diff --git a/.codex/rules/testing.md b/.codex/rules/testing.md new file mode 100644 index 0000000..c52f990 --- /dev/null +++ b/.codex/rules/testing.md @@ -0,0 +1,144 @@ +--- +description: ROS2 Testing Standards +--- + +# ROS2 Testing Standards +## Authoritative Sources + +- ROS2 Humble main docs: `https://docs.ros.org/en/humble/index.html` +- Package development guide: `https://docs.ros.org/en/humble/How-To-Guides/Developing-a-ROS-2-Package.html` +- Tutorials index: `https://docs.ros.org/en/humble/Tutorials.html` +- Skill authoring patterns: `https://github.com/openai/skills` + +When this file conflicts with external references, prefer ROS2 Humble docs for ROS behavior and `openai/skills` for skill structure. + +## Test Structure + +``` +package_name/test/ +├── unit/ # Pure Python/C++ tests +├── integration/ # ROS2 node tests +└── e2e/ # Full system/Launch tests +``` + +## Unit Tests + +### Python (pytest) + +```python +import pytest +from domain.entities.robot import Robot + +class TestRobot: + def test_creation(self): + robot = Robot(name="Bot", max_velocity=1.0) + assert robot.name == "Bot" + + def test_invalid_velocity(self): + with pytest.raises(ValueError): + Robot(name="Bot", max_velocity=-1.0) +``` + +### C++ (GTest) + +```cpp +#include +#include "domain/entities/robot.hpp" + +TEST(RobotTest, Creation) { + domain::entities::Robot robot; + robot.name = "Bot"; + EXPECT_EQ(robot.name, "Bot"); +} +``` + +## Integration Tests + +### Python (rclpy) + +```python +import pytest +import rclpy +from rclpy.node import Node +from std_msgs.msg import String + +@pytest.fixture(scope='module') +def ros2_context(): + rclpy.init() + yield + rclpy.shutdown() + +@pytest.fixture +def test_node(ros2_context): + node = Node('test_node') + yield node + node.destroy_node() + +def test_publisher(test_node): + received = [] + sub = test_node.create_subscription( + String, 'test', lambda m: received.append(m), 10 + ) + pub = test_node.create_publisher(String, 'test', 10) + + msg = String(data='Hello') + pub.publish(msg) + + rclpy.spin_once(test_node, timeout_sec=1.0) + assert len(received) >= 1 +``` + +### C++ (GTest + rclcpp) + +```cpp +#include +#include + +class NodeIntegrationTest : public ::testing::Test { +protected: + void SetUp() override { + rclcpp::init(0, nullptr); + node_ = std::make_shared("test_node"); + } + + void TearDown() override { + rclcpp::shutdown(); + } + + rclcpp::Node::SharedPtr node_; +}; + +TEST_F(NodeIntegrationTest, SimpleSpin) { + auto start = node_->get_clock()->now(); + rclcpp::spin_some(node_); + // Assert logic... +} +``` + +## Launch Tests (E2E) + +```python +import launch_testing +from launch import LaunchDescription +from launch_ros.actions import Node + +@launch_testing.markers.keep_alive +def generate_test_description(): + return LaunchDescription([ + Node(package='pkg', executable='node'), + launch_testing.actions.ReadyToTest() + ]) +``` + +## Test Commands + +```bash +# All tests +colcon test + +# Specific package +colcon test --packages-select my_pkg + +# With coverage (Python) +pytest --cov=my_pkg --cov-report=html +``` diff --git a/.codex/settings.json b/.codex/settings.json new file mode 100644 index 0000000..135f356 --- /dev/null +++ b/.codex/settings.json @@ -0,0 +1,47 @@ +{ + "permissions": { + "allow": [ + "Bash(tree:*)", + "Bash(wc:*)", + "Bash(pip install:*)", + "Bash(python3 -m pip install:*)", + "Bash(python3:*)", + "WebFetch(domain:*)", + "Bash(npm run:*)", + "Bash(npm test:*)", + "Bash(npm install:*)", + "Bash(npm run dev:*)", + "Bash(npm run build:*)", + "Bash(npm run lint:*)", + "Bash(npx:*)", + "Bash(pnpm:*)", + "Bash(yarn:*)", + "Bash(bun:*)", + "Bash(python3:*)", + "Bash(python:*)", + "Bash(git diff:*)", + "Bash(git status:*)", + "Bash(git log:*)", + "Bash(git add:*)", + "Bash(git commit:*)", + "Bash(colcon build:*)", + "Bash(colcon test:*)", + "Bash(ros2:*)", + "Bash(rosdep:*)", + "Bash(vcs:*)", + "Bash(source:*)", + "Bash(pytest:*)" + ], + "deny": [ + "Bash(sudo:*)", + "Bash(docker:*)", + "Bash(rm -rf:*)", + "Read(./.env)", + "Read(~/.ssh/**)" + ], + "ask": [ + "Bash(git push:*)" + ], + "defaultMode": "acceptEdits" + } +} diff --git a/.codex/skills/ros2-bag/SKILL.md b/.codex/skills/ros2-bag/SKILL.md new file mode 100644 index 0000000..51103fc --- /dev/null +++ b/.codex/skills/ros2-bag/SKILL.md @@ -0,0 +1,141 @@ +--- +name: ros2-bag +description: Use when creating ROS2 bag recording, playback, or analysis workflows with rosbag2 in Humble. +--- + +# ROS2 Bag Utility Skill +## Authoritative Sources + +- ROS2 Humble docs: `https://docs.ros.org/en/humble/index.html` +- Codex Skills docs: `https://developers.openai.com/codex/skills` +- ROS2 tutorials (nodes, launch, services/actions, tf2, testing, rosbag2): `https://docs.ros.org/en/humble/Tutorials.html` +- Codex skill conventions and examples: `https://github.com/openai/skills` + +Use ROS2 Humble docs as source of truth for APIs/behavior and `openai/skills` for skill structure and trigger wording. + +This skill provides guides for programmatic data recording (rosbag2) and replay mechanism adhering to Clean Architecture. + +## Domain Layer + +Defines the interface for recording and playback control. + +```python +# domain/interfaces/data_recorder.py +class IDataRecorder(ABC): + @abstractmethod + def start_recording(self, config: RecordingConfig) -> bool: + pass + + @abstractmethod + def stop_recording(self) -> None: + pass +``` + +## Infrastructure Layer (Python) + +Use the Humble rosbag2 tutorials and API docs as baseline, then adapt into infrastructure adapters so domain/application layers stay ROS-agnostic. + +## Infrastructure Layer (C++) + +Using `rosbag2_cpp` library. + +```cpp +// infrastructure/ros2/bag/bag_recorder.hpp +#pragma once +#include +#include +#include + +namespace infrastructure::ros2::bag { + +class BagRecorder { +public: + explicit BagRecorder(rclcpp::Node::SharedPtr node) : node_(node) {} + + bool start_recording(const std::string& bag_name, const std::vector& topics) { + try { + writer_ = std::make_unique(); + + rosbag2_storage::StorageOptions storage_options; + storage_options.uri = bag_name; + storage_options.storage_id = "sqlite3"; + + rosbag2_cpp::ConverterOptions converter_options; + converter_options.input_serialization_format = "cdr"; + converter_options.output_serialization_format = "cdr"; + + writer_->open(storage_options, converter_options); + + // Subscribe to topics and write + for (const auto& topic : topics) { + // Determine topic type dynamically or use GenericSubscribe (Humble+) + create_recorder_subscription(topic); + } + + is_recording_ = true; + return true; + } catch (const std::exception& e) { + RCLCPP_ERROR(node_->get_logger(), "Failed to open bag: %s", e.what()); + return false; + } + } + + void stop_recording() { + writer_.reset(); // Closes the bag + subs_.clear(); + is_recording_ = false; + } + + template + void write_message(const std::string& topic, const T& msg) { + if (is_recording_ && writer_) { + auto timestamp = node_->get_clock()->now(); + writer_->write(msg, topic, timestamp); + } + } + +private: + rclcpp::Node::SharedPtr node_; + std::unique_ptr writer_; + std::vector subs_; + bool is_recording_ = false; + + void create_recorder_subscription(const std::string& topic) { + // Implementation for generic subscription... + } +}; + +} // namespace +``` + +### Bag Reader Helper (C++) + +```cpp +// infrastructure/ros2/bag/bag_reader.hpp +#include + +class BagReader { +public: + explicit BagReader(const std::string& bag_path) { + reader_ = std::make_unique(); + reader_->open(bag_path); + } + + void read_all() { + while (reader_->has_next()) { + auto bag_message = reader_->read_next(); + // Deserialization logic... + } + } + +private: + std::unique_ptr reader_; +}; +``` + +## Best Practices + +1. **Storage ID**: `sqlite3` is default, `mcap` is recommended for performance. +2. **Serialization**: Use `cdr`. +3. **Filtering**: Record only necessary topics to save disk space. +4. **Splitting**: Configure bag splitting for long-duration recordings. diff --git a/.codex/skills/ros2-diagnostics/SKILL.md b/.codex/skills/ros2-diagnostics/SKILL.md new file mode 100644 index 0000000..b87ae1a --- /dev/null +++ b/.codex/skills/ros2-diagnostics/SKILL.md @@ -0,0 +1,129 @@ +--- +name: ros2-diagnostics +description: Use when adding diagnostic_updater health checks, status reporting, and frequency monitoring in ROS2 Humble. +--- + +# ROS2 Diagnostics Skill +## Authoritative Sources + +- ROS2 Humble docs: `https://docs.ros.org/en/humble/index.html` +- Codex Skills docs: `https://developers.openai.com/codex/skills` +- ROS2 tutorials (nodes, launch, services/actions, tf2, testing, rosbag2): `https://docs.ros.org/en/humble/Tutorials.html` +- Codex skill conventions and examples: `https://github.com/openai/skills` + +Use ROS2 Humble docs as source of truth for APIs/behavior and `openai/skills` for skill structure and trigger wording. + +This skill demonstrates how to integrate standard ROS2 diagnostics tools for health monitoring within a Clean Architecture. + +## Domain Layer + +```python +# domain/entities/health.py +class HealthLevel(Enum): + OK = 0 + WARN = 1 + ERROR = 2 + STALE = 3 + +@dataclass +class ComponentHealth: + name: str + level: HealthLevel + message: str + values: dict +``` + +## Infrastructure Layer (Python) + +Use `diagnostic_updater` patterns from ROS2 Humble documentation and keep health semantics mapped through domain entities first. + +## Infrastructure Layer (C++) + +Using `diagnostic_updater` package in C++. + +```cpp +// infrastructure/ros2/diagnostics/diagnostics_manager.hpp +#pragma once +#include +#include +#include "domain/interfaces/diagnostics_port.hpp" + +namespace infrastructure::ros2::diagnostics { + +class DiagnosticsManager { +public: + explicit DiagnosticsManager(rclcpp::Node::SharedPtr node) + : node_(node), updater_(node) { + updater_.setHardwareID(node->get_name()); + } + + void register_monitor(const std::string& name, + std::function callback) { + updater_.add(name, callback); + } + + // Example callback wrapper for domain entities + void check_component(diagnostic_updater::DiagnosticStatusWrapper& stat) { + // Retrieve health from domain service + // auto health = domain_service_->get_health(); + + // stat.summary(health.level, health.message); + // stat.add("temp", health.value); + } + +private: + rclcpp::Node::SharedPtr node_; + diagnostic_updater::Updater updater_; +}; + +} // namespace +``` + +### Frequency Status Example (C++) + +```cpp +// infrastructure/ros2/diagnostics/frequency_monitor.hpp +#include // For TopicDiagnostic + +class FrequencyMonitor { +public: + FrequencyMonitor(diagnostic_updater::Updater& updater, + const std::string& topic_name, + double min_freq, double max_freq) { + + diagnostic_updater::FrequencyStatusParam freq_param(&min_freq, &max_freq, 0.1, 10); + + monitor_ = std::make_unique( + topic_name, updater, freq_param); + } + + void tick() { + monitor_->tick(); + } + +private: + std::unique_ptr monitor_; +}; +``` + +## Use Case Integration + +```cpp +// application/services/motor_controller.cpp +void MotorController::check_temp(diagnostic_updater::DiagnosticStatusWrapper& stat) { + double temp = read_temp(); + if (temp > 80.0) { + stat.summary(diagnostic_msgs::msg::DiagnosticStatus::ERROR, "Overheating"); + } else { + stat.summary(diagnostic_msgs::msg::DiagnosticStatus::OK, "Normal"); + } + stat.add("temp", temp); +} +``` + +## Best Practices + +1. **Hardware ID**: Always set a unique Hardware ID in the updater. +2. **Frequency Monitoring**: Use `TopicDiagnostic` to monitor publication rates. +3. **Meaningful Messages**: Provide descriptive error messages. +4. **Standard Levels**: Stick to OK, WARN, ERROR, STALE semantics. diff --git a/.codex/skills/ros2-dockerfile/SKILL.md b/.codex/skills/ros2-dockerfile/SKILL.md new file mode 100644 index 0000000..c89b5bc --- /dev/null +++ b/.codex/skills/ros2-dockerfile/SKILL.md @@ -0,0 +1,105 @@ +--- +name: ros2-dockerfile +description: Use when creating or updating ROS2 Humble Dockerfiles for development, CI, or runtime images. +--- + +# ROS2 Dockerfile Skill +## Authoritative Sources + +- ROS2 Humble docs: `https://docs.ros.org/en/humble/index.html` +- Codex Skills docs: `https://developers.openai.com/codex/skills` +- ROS2 tutorials: `https://docs.ros.org/en/humble/Tutorials.html` +- Codex skill conventions and examples: `https://github.com/openai/skills` + +Use ROS2 Humble docs as source of truth for dependency and build behavior. + +## When To Use + +- Build a new Dockerfile for ROS2 packages/workspaces. +- Convert local build flow (`rosdep`, `colcon`) into container flow. +- Create dev/runtime split images or CI-friendly images. + +## Workflow + +1. Choose base image (`ros:humble-ros-base` by default). +2. Install system/build dependencies in one apt layer and clean apt cache. +3. Copy workspace source and run `rosdep install` from `/ws/src`. +4. Build with `colcon build --merge-install` (add `--symlink-install` for dev). +5. Add entrypoint that sources `/opt/ros/humble/setup.bash` and workspace install setup. +6. Switch to non-root user for runtime execution. + +## Dev Dockerfile Template + +```dockerfile +FROM ros:humble-ros-base +SHELL ["/bin/bash", "-c"] +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + python3-colcon-common-extensions \ + python3-rosdep \ + python3-vcstool \ + git \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /ws +COPY src /ws/src + +RUN source /opt/ros/humble/setup.bash && \ + rosdep update && \ + rosdep install --from-paths src --ignore-src -r -y && \ + colcon build --merge-install --symlink-install + +COPY docker/ros_entrypoint.sh /ros_entrypoint.sh +RUN chmod +x /ros_entrypoint.sh +ENTRYPOINT ["/ros_entrypoint.sh"] +CMD ["bash"] +``` + +## Runtime Multi-Stage Template + +```dockerfile +FROM ros:humble-ros-base AS builder +SHELL ["/bin/bash", "-c"] +ENV DEBIAN_FRONTEND=noninteractive +WORKDIR /ws + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential python3-colcon-common-extensions python3-rosdep \ + && rm -rf /var/lib/apt/lists/* + +COPY src /ws/src +RUN source /opt/ros/humble/setup.bash && \ + rosdep update && \ + rosdep install --from-paths src --ignore-src -r -y && \ + colcon build --merge-install + +FROM ros:humble-ros-base +SHELL ["/bin/bash", "-c"] +WORKDIR /ws +COPY --from=builder /ws/install /ws/install +COPY docker/ros_entrypoint.sh /ros_entrypoint.sh +RUN chmod +x /ros_entrypoint.sh +ENTRYPOINT ["/ros_entrypoint.sh"] +CMD ["ros2", "launch", "my_bringup", "system.launch.py"] +``` + +## Entrypoint Template + +```bash +#!/usr/bin/env bash +set -e +source /opt/ros/humble/setup.bash +if [ -f /ws/install/setup.bash ]; then + source /ws/install/setup.bash +fi +exec "$@" +``` + +## Checklist + +- `rosdep install` runs successfully in image build. +- `colcon build` completes in container. +- Container starts with ROS environments sourced. +- Image runs without root for application process where possible. diff --git a/.codex/skills/ros2-launch-config/SKILL.md b/.codex/skills/ros2-launch-config/SKILL.md new file mode 100644 index 0000000..a9b9ebd --- /dev/null +++ b/.codex/skills/ros2-launch-config/SKILL.md @@ -0,0 +1,204 @@ +--- +name: ros2-launch-config +description: Use when building modular ROS2 launch files and parameter wiring for Humble Python/C++ nodes. +--- + +# ROS2 Launch & Configuration Skill +## Authoritative Sources + +- ROS2 Humble docs: `https://docs.ros.org/en/humble/index.html` +- Codex Skills docs: `https://developers.openai.com/codex/skills` +- ROS2 tutorials (nodes, launch, services/actions, tf2, testing, rosbag2): `https://docs.ros.org/en/humble/Tutorials.html` +- Codex skill conventions and examples: `https://github.com/openai/skills` + +Use ROS2 Humble docs as source of truth for APIs/behavior and `openai/skills` for skill structure and trigger wording. + +This skill provides a guide for creating modular and reusable ROS2 launch files, which are written in Python for both C++ and Python nodes. + +## Launch File Structure + +``` +packages/ +└── robot_core/ + └── launch/ + ├── robot_launch.py # Main launch file + ├── sensors_launch.py # Sensor subsystem + ├── navigation_launch.py # Navigation subsystem + └── includes/ + ├── common.py # Common functions + └── defaults.py # Default values +``` + +## Basic Launch File Template + +```python +#!/usr/bin/env python3 +""" +Launch file: robot_launch.py +Description: Main robot system launch file +""" + +import os +from ament_index_python.packages import get_package_share_directory + +from launch import LaunchDescription +from launch.actions import ( + DeclareLaunchArgument, + IncludeLaunchDescription, + GroupAction, + SetEnvironmentVariable, + LogInfo +) +from launch.conditions import IfCondition, UnlessCondition +from launch.substitutions import ( + LaunchConfiguration, + PathJoinSubstitution +) +from launch.launch_description_sources import PythonLaunchDescriptionSource +from launch_ros.actions import Node, SetParameter +from launch_ros.substitutions import FindPackageShare + + +def generate_launch_description(): + """Generate launch description.""" + + # Package paths + pkg_robot_core = get_package_share_directory('robot_core') + + # Launch arguments + declared_arguments = [ + DeclareLaunchArgument( + 'robot_name', + default_value='robot_1', + description='Robot namespace' + ), + DeclareLaunchArgument( + 'use_sim', + default_value='false', + description='Use simulation mode' + ), + DeclareLaunchArgument( + 'config_file', + default_value=os.path.join(pkg_robot_core, 'config', 'params.yaml'), + description='Path to parameter file' + ), + ] + + # Configurations + robot_name = LaunchConfiguration('robot_name') + use_sim = LaunchConfiguration('use_sim') + config_file = LaunchConfiguration('config_file') + + # Environment setup + env_setup = [ + SetEnvironmentVariable('RCUTILS_COLORIZED_OUTPUT', '1'), + ] + + # Global parameters + global_params = SetParameter(name='use_sim_time', value=use_sim) + + # C++ Node Example + cpp_node = Node( + package='robot_cpp_pkg', + executable='robot_controller_cpp', # Name of the C++ executable in CMakeLists.txt + name='controller', + namespace=robot_name, + parameters=[config_file], + output='screen' + ) + + # Python Node Example + py_node = Node( + package='robot_py_pkg', + executable='sensor_node.py', # Name of the script or entry point + name='sensors', + namespace=robot_name, + parameters=[config_file], + output='screen', + condition=UnlessCondition(use_sim) + ) + + return LaunchDescription( + declared_arguments + + env_setup + + [global_params, cpp_node, py_node] + ) +``` + +## Parameter File Structure (YAML) + +```yaml +# config/params.yaml +/**: + ros__parameters: + # Global parameters + use_sim_time: false + log_level: "info" + +robot_state: + ros__parameters: + # Robot state node parameters + update_rate: 100.0 + frame_id: "base_link" + + # Nested parameters + position_filter: + type: "kalman" + process_noise: 0.01 + +navigation: + ros__parameters: + max_velocity: 1.5 + planner: + type: "astar" +``` + +## Loading Parameters in C++ + +```cpp +// Within a ROS2 Node +void load_parameters() { + this->declare_parameter("update_rate", 10.0); + this->declare_parameter("position_filter.type", "default"); + + double rate = this->get_parameter("update_rate").as_double(); + std::string filter_type = this->get_parameter("position_filter.type").as_string(); + + RCLCPP_INFO(this->get_logger(), "Rate: %f, Filter: %s", rate, filter_type.c_str()); +} +``` + +## Lifecycle Node Launch integration + +```python +from launch_ros.actions import LifecycleNode +from launch_ros.events.lifecycle import ChangeState +from lifecycle_msgs.msg import Transition +from launch.actions import EmitEvent, RegisterEventHandler +from launch.event_handlers import OnProcessStart + +def generate_launch_description(): + # ... + driver_node = LifecycleNode( + package='robot_drivers', + executable='lidar_driver', + name='lidar', + namespace='', + output='screen' + ) + + # Auto-configure on start + configure_event = RegisterEventHandler( + OnProcessStart( + target_action=driver_node, + on_start=[ + EmitEvent(event=ChangeState( + lifecycle_node_matcher=lambda n: n == driver_node, + transition_id=Transition.TRANSITION_CONFIGURE, + )), + ] + ) + ) + + return LaunchDescription([driver_node, configure_event]) +``` diff --git a/.codex/skills/ros2-lifecycle/SKILL.md b/.codex/skills/ros2-lifecycle/SKILL.md new file mode 100644 index 0000000..d363c4f --- /dev/null +++ b/.codex/skills/ros2-lifecycle/SKILL.md @@ -0,0 +1,147 @@ +--- +name: ros2-lifecycle +description: Use when implementing managed lifecycle nodes, transitions, and lifecycle clients in ROS2 Humble. +--- + +# ROS2 Lifecycle Nodes Skill +## Authoritative Sources + +- ROS2 Humble docs: `https://docs.ros.org/en/humble/index.html` +- Codex Skills docs: `https://developers.openai.com/codex/skills` +- ROS2 tutorials (nodes, launch, services/actions, tf2, testing, rosbag2): `https://docs.ros.org/en/humble/Tutorials.html` +- Codex skill conventions and examples: `https://github.com/openai/skills` + +Use ROS2 Humble docs as source of truth for APIs/behavior and `openai/skills` for skill structure and trigger wording. + +## Lifecycle States + +``` + +---------+ + | UNKNOWN | + +----+----+ + | + create() + v + +------+------+ + | UNCONFIGURED|<----+ + +------+------+ | + | | + configure() cleanup() + v | + +------+------+ | + | INACTIVE |------+ + +------+------+ + | + activate() + v + +------+------+ + | ACTIVE | + +------+------+ + | + deactivate() + v + +------+------+ + | INACTIVE | + +-------------+ +``` + +## Python Implementation + +Implement Python lifecycle nodes following the Humble `rclpy.lifecycle` tutorial patterns, then keep transition/business logic in application/domain layers. + +## C++ Implementation + +### Lifecycle Node Template + +```cpp +// infrastructure/ros2/nodes/managed_node.hpp +#pragma once + +#include +#include +#include +#include + +namespace infrastructure::ros2::nodes { + +using CallbackReturn = rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn; + +class ManagedNode : public rclcpp_lifecycle::LifecycleNode { +public: + explicit ManagedNode(const std::string& node_name, bool intra_process_comms = false) + : rclcpp_lifecycle::LifecycleNode(node_name, + rclcpp::NodeOptions().use_intra_process_comms(intra_process_comms)) {} + + CallbackReturn on_configure(const rclcpp_lifecycle::State &) override { + RCLCPP_INFO(get_logger(), "Configuring..."); + pub_ = this->create_publisher("topic", 10); + return CallbackReturn::SUCCESS; + } + + CallbackReturn on_activate(const rclcpp_lifecycle::State &) override { + RCLCPP_INFO(get_logger(), "Activating..."); + pub_->on_activate(); + // Start timers, etc. + return CallbackReturn::SUCCESS; + } + + CallbackReturn on_deactivate(const rclcpp_lifecycle::State &) override { + RCLCPP_INFO(get_logger(), "Deactivating..."); + pub_->on_deactivate(); + return CallbackReturn::SUCCESS; + } + + CallbackReturn on_cleanup(const rclcpp_lifecycle::State &) override { + RCLCPP_INFO(get_logger(), "Cleaning up..."); + pub_.reset(); + return CallbackReturn::SUCCESS; + } + + CallbackReturn on_shutdown(const rclcpp_lifecycle::State &) override { + RCLCPP_INFO(get_logger(), "Shutting down..."); + return CallbackReturn::SUCCESS; + } + +private: + rclcpp_lifecycle::LifecyclePublisher::SharedPtr pub_; +}; + +} // namespace +``` + +### Lifecycle Client (C++) + +Allows controlling the state of another node using the `lifeycle_msgs` services. + +```cpp +// infrastructure/ros2/lifecycle/lifecycle_client.hpp +#include +#include +#include + +class LifecycleClient { +public: + LifecycleClient(rclcpp::Node::SharedPtr node, const std::string& target_node) { + client_ = node->create_client( + target_node + "/change_state"); + } + + bool change_state(std::uint8_t transition_id) { + auto request = std::make_shared(); + request->transition.id = transition_id; + + // ... async call logic ... + return true; + } + +private: + rclcpp::Client::SharedPtr client_; +}; +``` + +## Best Practices + +- **Resource Management**: Allocate resources (memory, connections) in `on_configure` and release them in `on_cleanup`. +- **Activity**: Only publish data or perform main logic when in the **ACTIVE** state. +- **Error Handling**: Use `on_error` callback to handle failures during transitions. +- **Launch Integration**: Use `LifecycleNode` in launch files to automatically manage states. diff --git a/.codex/skills/ros2-messaging/SKILL.md b/.codex/skills/ros2-messaging/SKILL.md new file mode 100644 index 0000000..40e0e59 --- /dev/null +++ b/.codex/skills/ros2-messaging/SKILL.md @@ -0,0 +1,188 @@ +--- +name: ros2-messaging +description: Use when designing ROS2 Humble pub/sub messaging patterns, QoS choices, and message-domain mapping. +--- + +# ROS2 Messaging Patterns Skill +## Authoritative Sources + +- ROS2 Humble docs: `https://docs.ros.org/en/humble/index.html` +- Codex Skills docs: `https://developers.openai.com/codex/skills` +- ROS2 tutorials (nodes, launch, services/actions, tf2, testing, rosbag2): `https://docs.ros.org/en/humble/Tutorials.html` +- Codex skill conventions and examples: `https://github.com/openai/skills` + +Use ROS2 Humble docs as source of truth for APIs/behavior and `openai/skills` for skill structure and trigger wording. + +This skill covers ROS2 messaging patterns adhering to Clean Architecture principles, protecting the domain layer from ROS2 dependencies. + +## Publisher/Subscriber Patterns + +### 1. Domain-Driven Publisher (Python) + +Implement the Python publisher/subscriber adapter using Humble `rclpy` tutorials and map ROS messages to domain entities in infrastructure. + +### 2. Domain-Driven Publisher (C++) + +```cpp +// infrastructure/ros2/publishers/state_publisher.hpp +#pragma once +#include +#include "domain/entities/robot_state.hpp" +#include "robot_interfaces/msg/robot_state.hpp" + +namespace infrastructure::ros2::publishers { + +template +class IStatePublisher { +public: + virtual void publish(const T& state) = 0; + virtual ~IStatePublisher() = default; +}; + +class RobotStatePublisher : public IStatePublisher { +public: + RobotStatePublisher(rclcpp::Node::SharedPtr node, const std::string& topic, const rclcpp::QoS& qos) + : node_(node) { + publisher_ = node_->create_publisher(topic, qos); + } + + void publish(const domain::entities::RobotState& state) override { + robot_interfaces::msg::RobotState msg; + msg.mode = static_cast(state.mode); + // ... mapping ... + publisher_->publish(msg); + } + +private: + rclcpp::Node::SharedPtr node_; + rclcpp::Publisher::SharedPtr publisher_; +}; + +} // namespace +``` + +### 3. Generic Subscriber Handler (C++) + +```cpp +// infrastructure/ros2/subscribers/base_subscriber.hpp +#pragma once +#include +#include + +namespace infrastructure::ros2::subscribers { + +template +class BaseSubscriber { +public: + BaseSubscriber(rclcpp::Node::SharedPtr node, + const std::string& topic, + const rclcpp::QoS& qos, + std::function callback) + : callback_(callback) { + + subscription_ = node->create_subscription( + topic, qos, + [this](const typename MsgT::SharedPtr msg) { + this->handle_message(msg); + } + ); + } + + virtual ~BaseSubscriber() = default; + +protected: + virtual EntityT convert_to_entity(const typename MsgT::SharedPtr msg) = 0; + +private: + void handle_message(const typename MsgT::SharedPtr msg) { + auto entity = convert_to_entity(msg); + callback_(entity); + } + + rclcpp::Subscription::SharedPtr subscription_; + std::function callback_; +}; + +} // namespace +``` + +## Synchronization (C++) + +Using `message_filters` in C++. + +```cpp +// infrastructure/ros2/sync/time_sync.hpp +#pragma once +#include +#include +#include +#include +#include +#include + +namespace infrastructure::ros2::sync { + +class CameraSync { +public: + CameraSync(rclcpp::Node::SharedPtr node) { + image_sub_.subscribe(node, "image_raw"); + info_sub_.subscribe(node, "camera_info"); + + sync_ = std::make_shared(MySyncPolicy(10), image_sub_, info_sub_); + sync_->registerCallback(&CameraSync::callback, this); + } + +private: + void callback(const sensor_msgs::msg::Image::ConstSharedPtr& image, + const sensor_msgs::msg::CameraInfo::ConstSharedPtr& info) { + // Handle synchronized messages + } + + message_filters::Subscriber image_sub_; + message_filters::Subscriber info_sub_; + + typedef message_filters::sync_policies::ApproximateTime< + sensor_msgs::msg::Image, sensor_msgs::msg::CameraInfo> MySyncPolicy; + typedef message_filters::Synchronizer Sync; + std::shared_ptr sync_; +}; + +} // namespace +``` + +## Event Bus using ROS2 (C++) + +```cpp +// infrastructure/ros2/events/event_bus.hpp +#pragma once +#include +#include +#include // External dependency + +class EventBus { +public: + EventBus(rclcpp::Node::SharedPtr node) { + pub_ = node->create_publisher("/events/all", 10); + // ... subscriber setup ... + } + + void emit(const std::string& type, const nlohmann::json& payload) { + std_msgs::msg::String msg; + nlohmann::json data; + data["type"] = type; + data["payload"] = payload; + msg.data = data.dump(); + pub_->publish(msg); + } + +private: + rclcpp::Publisher::SharedPtr pub_; +}; +``` + +## Best Practices + +- **Isolation**: Keep domain entities independent of ROS2 message types. +- **Conversion**: Perform message-to-entity conversion in the infrastructure layer. +- **Thread Safety**: Use `std::mutex` for shared resources in C++ callbacks. +- **QoS**: Select appropriate QoS profiles (Reliable vs Best Effort). diff --git a/.codex/skills/ros2-node-creation/SKILL.md b/.codex/skills/ros2-node-creation/SKILL.md new file mode 100644 index 0000000..cb3e07b --- /dev/null +++ b/.codex/skills/ros2-node-creation/SKILL.md @@ -0,0 +1,249 @@ +--- +name: ros2-node-creation +description: Use when creating new ROS2 Humble nodes with clean layering, dependency injection, and reusable node scaffolds. +--- + +# ROS2 Node Creation Skill +## Authoritative Sources + +- ROS2 Humble docs: `https://docs.ros.org/en/humble/index.html` +- Codex Skills docs: `https://developers.openai.com/codex/skills` +- ROS2 tutorials (nodes, launch, services/actions, tf2, testing, rosbag2): `https://docs.ros.org/en/humble/Tutorials.html` +- Codex skill conventions and examples: `https://github.com/openai/skills` + +Use ROS2 Humble docs as source of truth for APIs/behavior and `openai/skills` for skill structure and trigger wording. + +This skill is used to create ROS2 nodes that adhere to Clean Architecture principles. It covers both Python and C++ implementations. + +## Directory Structure + +``` +src/ +├── domain/ # Business Logic Layer +│ ├── entities/ # Core business objects +│ ├── repositories/ # Repository interfaces (abstract) +│ └── use_cases/ # Business rules +├── application/ # Application Layer +│ ├── services/ # Application services +│ └── interfaces/ # Port interfaces +└── infrastructure/ # Infrastructure Layer + └── ros2/ + ├── nodes/ # ROS2 Node implementations + ├── publishers/ # Publisher adapters + ├── subscribers/ # Subscriber adapters + └── services/ # Service adapters +``` + +## Python Implementation + +### 1. Base Node Template + +```python +#!/usr/bin/env python3 +""" +ROS2 Node: [NodeName] +Description: [Node Description] +""" + +import rclpy +from rclpy.node import Node +from rclpy.qos import QoSProfile, ReliabilityPolicy, HistoryPolicy + +from typing import Optional, Callable +from abc import ABC, abstractmethod + + +class BaseNode(Node, ABC): + """Base class for all nodes.""" + + def __init__(self, node_name: str): + super().__init__(node_name) + self._setup_parameters() + self._setup_publishers() + self._setup_subscribers() + self._setup_services() + self._setup_timers() + self.get_logger().info(f'{node_name} initialized') + + @abstractmethod + def _setup_parameters(self) -> None: + """Define ROS2 parameters.""" + pass + + @abstractmethod + def _setup_publishers(self) -> None: + """Create publishers.""" + pass + + @abstractmethod + def _setup_subscribers(self) -> None: + """Create subscribers.""" + pass + + def _setup_services(self) -> None: + """Create services (optional).""" + pass + + def _setup_timers(self) -> None: + """Create timers (optional).""" + pass + + def get_default_qos(self) -> QoSProfile: + """Default QoS profile.""" + return QoSProfile( + reliability=ReliabilityPolicy.RELIABLE, + history=HistoryPolicy.KEEP_LAST, + depth=10 + ) +``` + +### 2. Concrete Node Implementation + +```python +# Build concrete node logic from Humble node tutorials and keep this template as the structural baseline. +# Avoid placeholder stubs in generated code. +``` + +## C++ Implementation + +### 1. Base Node Template (Header) + +```cpp +// infrastructure/ros2/nodes/base_node.hpp +#pragma once + +#include +#include +#include + +namespace infrastructure::ros2::nodes { + +class BaseNode : public rclcpp::Node { +public: + explicit BaseNode(const std::string& node_name, + const rclcpp::NodeOptions& options = rclcpp::NodeOptions()); + virtual ~BaseNode() = default; + +protected: + virtual void setup_parameters() = 0; + virtual void setup_publishers() = 0; + virtual void setup_subscribers() = 0; + virtual void setup_services() {} + virtual void setup_timers() {} + + rclcpp::QoS get_default_qos() const; +}; + +} // namespace infrastructure::ros2::nodes +``` + +### 2. Base Node Template (Source) + +```cpp +// infrastructure/ros2/nodes/base_node.cpp +#include "infrastructure/ros2/nodes/base_node.hpp" + +namespace infrastructure::ros2::nodes { + +BaseNode::BaseNode(const std::string& node_name, const rclcpp::NodeOptions& options) + : Node(node_name, options) { + + // Virtual calls in constructor are dangerous in C++, + // but common in ROS2 if careful or using an init() method. + // Better pattern: Call these in a separate init() or distinct lifecycle. + // For simplicity in this template, we assume derived classes handle initialization + // or use the lifecycle node pattern. +} + +rclcpp::QoS BaseNode::get_default_qos() const { + return rclcpp::QoS(10) + .reliability(rmw_qos_reliability_policy_t::RMW_QOS_POLICY_RELIABILITY_RELIABLE) + .history(rmw_qos_history_policy_t::RMW_QOS_POLICY_HISTORY_KEEP_LAST); +} + +} // namespace infrastructure::ros2::nodes +``` + +### 3. Concrete Node Implementation (C++) + +```cpp +// infrastructure/ros2/nodes/sensor_node.hpp +#pragma once + +#include "infrastructure/ros2/nodes/base_node.hpp" +#include "application/services/sensor_service.hpp" +#include +#include + +namespace infrastructure::ros2::nodes { + +class SensorNode : public BaseNode { +public: + explicit SensorNode(const rclcpp::NodeOptions& options = rclcpp::NodeOptions()); + + // Dependency Injection + void set_sensor_service(std::shared_ptr service); + +protected: + void setup_parameters() override; + void setup_publishers() override; + void setup_subscribers() override; + void setup_timers() override; + +private: + void raw_callback(const std_msgs::msg::Float64::SharedPtr msg); + void timer_callback(); + + std::shared_ptr sensor_service_; + + rclcpp::Publisher::SharedPtr temp_pub_; + rclcpp::Subscription::SharedPtr raw_sub_; + rclcpp::TimerBase::SharedPtr timer_; + + double update_rate_; + std::string sensor_topic_; +}; + +} // namespace infrastructure::ros2::nodes +``` + +### 4. Dependency Injection (C++) + +```cpp +// application/services/sensor_service.hpp +#pragma once +#include "domain/entities/sensor_data.hpp" + +namespace application::services { + +class ISensorService { +public: + virtual ~ISensorService() = default; + virtual domain::entities::SensorData process(double raw_data) = 0; +}; + +} // namespace application::services +``` + +## QoS Profiles (C++) + +```cpp +// infrastructure/ros2/qos_profiles.hpp +#pragma once +#include + +namespace infrastructure::ros2 { + +class QoSProfiles { +public: + static rclcpp::QoS sensor_data() { + return rclcpp::QoS(1).best_effort().durability_volatile(); + } + + static rclcpp::QoS command() { + return rclcpp::QoS(10).reliable().transient_local(); + } +}; + +} // namespace +``` diff --git a/.codex/skills/ros2-service-action/SKILL.md b/.codex/skills/ros2-service-action/SKILL.md new file mode 100644 index 0000000..281d3fc --- /dev/null +++ b/.codex/skills/ros2-service-action/SKILL.md @@ -0,0 +1,225 @@ +--- +name: ros2-service-action +description: Use when implementing ROS2 Humble services and actions with clear domain/application/infrastructure boundaries. +--- + +# ROS2 Service & Action Skill +## Authoritative Sources + +- ROS2 Humble docs: `https://docs.ros.org/en/humble/index.html` +- Codex Skills docs: `https://developers.openai.com/codex/skills` +- ROS2 tutorials (nodes, launch, services/actions, tf2, testing, rosbag2): `https://docs.ros.org/en/humble/Tutorials.html` +- Codex skill conventions and examples: `https://github.com/openai/skills` + +Use ROS2 Humble docs as source of truth for APIs/behavior and `openai/skills` for skill structure and trigger wording. + +This skill provides a guide for implementing ROS2 Services and Actions adhering to Clean Architecture principles. + +## Service Implementation + +### 1. Service Definition (.srv) + +``` +# srv/SetRobotMode.srv +# Request +string mode # "idle", "active", "emergency" +bool force_change # Force mode change + +--- + +# Response +bool success +string message +string previous_mode +``` + +### 2. Infrastructure Layer - Service Server (Python) + +Implement Python service/action adapters from Humble tutorials and convert request/response messages at infrastructure boundaries. + +### 3. Infrastructure Layer - Service Server (C++) + +```cpp +// infrastructure/ros2/services/robot_mode_service.hpp +#pragma once + +#include +#include "robot_interfaces/srv/set_robot_mode.hpp" +#include "domain/use_cases/set_robot_mode.hpp" + +namespace infrastructure::ros2::services { + +class RobotModeServiceNode : public rclcpp::Node { +public: + explicit RobotModeServiceNode( + std::shared_ptr use_case, + const rclcpp::NodeOptions& options = rclcpp::NodeOptions()); + +private: + void handle_set_mode( + const std::shared_ptr request, + std::shared_ptr response); + + std::shared_ptr use_case_; + rclcpp::Service::SharedPtr service_; +}; + +} // namespace + +// infrastructure/ros2/services/robot_mode_service.cpp +#include "infrastructure/ros2/services/robot_mode_service.hpp" + +namespace infrastructure::ros2::services { + +RobotModeServiceNode::RobotModeServiceNode( + std::shared_ptr use_case, + const rclcpp::NodeOptions& options) + : Node("robot_mode_service", options), use_case_(use_case) { + + using namespace std::placeholders; + service_ = this->create_service( + "/robot/set_mode", + std::bind(&RobotModeServiceNode::handle_set_mode, this, _1, _2) + ); +} + +void RobotModeServiceNode::handle_set_mode( + const std::shared_ptr request, + std::shared_ptr response) { + + // Conversion from message to domain object would go here + // ... + + // Execute use case + // auto result = use_case_->execute(...); + + // Map result to response + // response->success = result.success; +} + +} // namespace +``` + +### 4. Service Client (C++) + +```cpp +// infrastructure/ros2/clients/robot_mode_client.hpp +#pragma once +#include +#include "robot_interfaces/srv/set_robot_mode.hpp" + +namespace infrastructure::ros2::clients { + +class RobotModeClient { +public: + explicit RobotModeClient(rclcpp::Node::SharedPtr node); + + std::future> + set_mode_async(const std::string& mode, bool force = false); + +private: + rclcpp::Node::SharedPtr node_; + rclcpp::Client::SharedPtr client_; +}; + +} // namespace +``` + +## Action Implementation + +### 1. Action Definition (.action) + +``` +# action/NavigateToPoint.action +# Goal +geometry_msgs/Point target_point +float32 max_velocity +bool avoid_obstacles + +--- + +# Result +bool success +string message +float32 total_distance +float32 total_time + +--- + +# Feedback +geometry_msgs/Point current_position +float32 distance_remaining +float32 estimated_time +string status +``` + +### 2. Action Server (C++) + +```cpp +// infrastructure/ros2/actions/navigation_action_server.hpp +#pragma once + +#include +#include +#include "robot_interfaces/action/navigate_to_point.hpp" +#include "domain/use_cases/navigate_to_point.hpp" + +namespace infrastructure::ros2::actions { + +class NavigationActionServer : public rclcpp::Node { +public: + using NavigateToPoint = robot_interfaces::action::NavigateToPoint; + using GoalHandle = rclcpp_action::ServerGoalHandle; + + explicit NavigationActionServer( + std::shared_ptr use_case, + const rclcpp::NodeOptions& options = rclcpp::NodeOptions()); + +private: + rclcpp_action::GoalResponse handle_goal( + const rclcpp_action::GoalUUID& uuid, + std::shared_ptr goal); + + rclcpp_action::CancelResponse handle_cancel( + const std::shared_ptr goal_handle); + + void handle_accepted(const std::shared_ptr goal_handle); + void execute(const std::shared_ptr goal_handle); + + std::shared_ptr use_case_; + rclcpp_action::Server::SharedPtr action_server_; +}; + +} // namespace +``` + +### 3. Action Client (C++) + +```cpp +// infrastructure/ros2/clients/navigation_client.hpp +#pragma once + +#include +#include +#include "robot_interfaces/action/navigate_to_point.hpp" + +namespace infrastructure::ros2::clients { + +class NavigationClient { +public: + using NavigateToPoint = robot_interfaces::action::NavigateToPoint; + using GoalHandle = rclcpp_action::ClientGoalHandle; + + explicit NavigationClient(rclcpp::Node::SharedPtr node); + + std::shared_future> navigate_to( + double x, double y, double z, + std::function feedback_cb); + +private: + rclcpp::Node::SharedPtr node_; + rclcpp_action::Client::SharedPtr client_; +}; + +} // namespace +``` diff --git a/.codex/skills/ros2-testing/SKILL.md b/.codex/skills/ros2-testing/SKILL.md new file mode 100644 index 0000000..e9ff953 --- /dev/null +++ b/.codex/skills/ros2-testing/SKILL.md @@ -0,0 +1,169 @@ +--- +name: ros2-testing +description: Use when defining unit, integration, and launch-testing strategy for ROS2 Humble Python and C++ packages. +--- + +# ROS2 Testing Skill +## Authoritative Sources + +- ROS2 Humble docs: `https://docs.ros.org/en/humble/index.html` +- Codex Skills docs: `https://developers.openai.com/codex/skills` +- ROS2 tutorials (nodes, launch, services/actions, tf2, testing, rosbag2): `https://docs.ros.org/en/humble/Tutorials.html` +- Codex skill conventions and examples: `https://github.com/openai/skills` + +Use ROS2 Humble docs as source of truth for APIs/behavior and `openai/skills` for skill structure and trigger wording. + +This skill provides test strategies for ROS2 applications adhering to Clean Architecture principles, covering Unit, Integration, and E2E tests in both Python and C++. + +## Test Pyramid + +``` + /\ + / \ E2E Tests (Launch Tests / System Tests) + /----\ + / \ Integration Tests (Node / Component Tests) + /--------\ + / \ Unit Tests (Domain / Application Logic) + /--------------\ +``` + +## Directory Structure + +``` +tests/ +├── unit/ +│ ├── domain/ +│ └── application/ +├── integration/ +│ └── ros2/ +└── e2e/ + └── launch_tests/ +``` + +## Python Testing + +### 1. Unit Tests (Domain Layer) + +Use standard `pytest` unit tests for domain/application code (no ROS dependencies), and keep ROS test harness only in integration/E2E suites. + +### 2. Integration Tests (Node Test) + +```python +# tests/integration/ros2/nodes/test_sensor_node.py +import pytest +import rclpy +from rclpy.node import Node +from std_msgs.msg import Float64 + +@pytest.fixture(scope='module') +def ros_context(): + rclpy.init() + yield + rclpy.shutdown() + +@pytest.fixture +def test_node(ros_context): + node = Node('test_helper') + yield node + node.destroy_node() + +def test_sensor_integration(test_node): + # Verify node behavior by subscribing/publishing + pass +``` + +## C++ Testing (GTest) + +### 1. Unit Tests (Domain Layer) + +```cpp +// tests/unit/domain/use_cases/test_robot_controller.cpp +#include +#include +#include "domain/use_cases/robot_controller.hpp" +#include "domain/entities/robot_state.hpp" + +using namespace domain; +using ::testing::Return; + +class MockRobotRepository : public repositories::IRobotRepository { +public: + MOCK_METHOD(entities::RobotState, get_state, (), (override)); + MOCK_METHOD(void, set_mode, (entities::RobotMode), (override)); +}; + +TEST(RobotControllerTest, StartFromIdle) { + auto mock_repo = std::make_shared(); + use_cases::RobotControllerUseCase use_case(mock_repo); + + EXPECT_CALL(*mock_repo, get_state()) + .WillOnce(Return(entities::RobotState{entities::RobotMode::IDLE})); + + EXPECT_CALL(*mock_repo, set_mode(entities::RobotMode::ACTIVE)); + + auto result = use_case.start(); + EXPECT_TRUE(result.success); +} +``` + +### 2. Integration Tests (Node/Component) + +```cpp +// tests/integration/ros2/test_sensor_node.cpp +#include +#include +#include "infrastructure/ros2/nodes/sensor_node.hpp" + +class SensorNodeTest : public ::testing::Test { +protected: + void SetUp() override { + rclcpp::init(0, nullptr); + node_ = std::make_shared(); + } + + void TearDown() override { + rclcpp::shutdown(); + } + + std::shared_ptr node_; +}; + +TEST_F(SensorNodeTest, Initialization) { + EXPECT_STREQ(node_->get_name(), "sensor_node"); +} +``` + +### 3. Launch Tests (Python with GTest) + +You can run GTest executables from launch files to perform system-level tests. + +```python +# tests/e2e/launch_tests/system_test.launch.py +from launch import LaunchDescription +from launch_ros.actions import Node +from launch_testing.actions import ReadyToTest + +def generate_launch_description(): + # Launch system under test + app_node = Node(package='my_robot', executable='main_node') + + # Launch GTest runner + test_runner = Node( + package='my_robot', + executable='system_integration_test', + output='screen' + ) + + return LaunchDescription([ + app_node, + test_runner, + ReadyToTest() + ]) +``` + +## Best Practices + +- **Mocking**: Use `unittest.mock` for Python and `gmock` for C++. +- **Separation**: Keep domain logic usage in unit tests strictly separate from ROS2 dependencies. +- **Fixtures**: Use `pytest.fixture` and GTest `SetUp/TearDown` to manage ROS2 context (`rclpy.init/shutdown`). +- **Async Testing**: For C++ integration tests, use `rclcpp::spin_some` or `wait_for_future` to handle async operations. diff --git a/.codex/skills/ros2-transforms/SKILL.md b/.codex/skills/ros2-transforms/SKILL.md new file mode 100644 index 0000000..b3a44c0 --- /dev/null +++ b/.codex/skills/ros2-transforms/SKILL.md @@ -0,0 +1,160 @@ +--- +name: ros2-transforms +description: Use when implementing TF2 transform lookup/broadcast patterns in ROS2 Humble without leaking ROS types into domain. +--- + +# ROS2 Transforms (TF2) Skill +## Authoritative Sources + +- ROS2 Humble docs: `https://docs.ros.org/en/humble/index.html` +- Codex Skills docs: `https://developers.openai.com/codex/skills` +- ROS2 tutorials (nodes, launch, services/actions, tf2, testing, rosbag2): `https://docs.ros.org/en/humble/Tutorials.html` +- Codex skill conventions and examples: `https://github.com/openai/skills` + +Use ROS2 Humble docs as source of truth for APIs/behavior and `openai/skills` for skill structure and trigger wording. + +This skill demonstrates how to use the ROS2 TF2 (Transform Library) while adhering to Clean Architecture principles. The Domain layer should be protected from direct TF2 dependencies by using Repository or Service patterns. + +## Domain Layer + +The domain layer should not depend on TF2 or `geometry_msgs`. + +```python +# domain/entities/pose.py +from dataclasses import dataclass + +@dataclass +class Pose: + position: tuple # (x, y, z) + orientation: tuple # (x, y, z, w) + frame_id: str + timestamp: float +``` + +```cpp +// domain/entities/pose.hpp +#pragma once +#include + +namespace domain::entities { + +struct Point3D { double x, y, z; }; +struct Quaternion { double x, y, z, w; }; + +struct Pose { + Point3D position; + Quaternion orientation; + std::string frame_id; + double timestamp; +}; + +} // namespace +``` + +## Infrastructure Layer + +TF2 implementation resides here. + +### TF2 Wrapper Service (Python) + +Implement Python TF2 wrappers using Humble tf2 tutorials and keep conversion to domain pose/value objects inside infrastructure services. + +### TF2 Wrapper Service (C++) + +```cpp +// infrastructure/ros2/services/tf_service.hpp +#pragma once +#include +#include +#include +#include +#include "domain/interfaces/transform_service.hpp" + +namespace infrastructure::ros2::services { + +class TFService : public domain::interfaces::ITransformService { +public: + explicit TFService(rclcpp::Node::SharedPtr node); + + std::optional get_transform( + const std::string& target_frame, + const std::string& source_frame) override; + +private: + rclcpp::Node::SharedPtr node_; + std::shared_ptr tf_buffer_; + std::shared_ptr tf_listener_; +}; + +} // namespace + +// infrastructure/ros2/services/tf_service.cpp +#include "infrastructure/ros2/services/tf_service.hpp" + +namespace infrastructure::ros2::services { + +TFService::TFService(rclcpp::Node::SharedPtr node) : node_(node) { + tf_buffer_ = std::make_shared(node_->get_clock()); + tf_listener_ = std::make_shared(*tf_buffer_); +} + +std::optional TFService::get_transform( + const std::string& target_frame, + const std::string& source_frame) { + + try { + geometry_msgs::msg::TransformStamped t = tf_buffer_->lookupTransform( + target_frame, source_frame, tf2::TimePointZero); + + return domain::entities::Pose{ + {t.transform.translation.x, t.transform.translation.y, t.transform.translation.z}, + {t.transform.rotation.x, t.transform.rotation.y, t.transform.rotation.z, t.transform.rotation.w}, + t.header.frame_id, + rclcpp::Time(t.header.stamp).seconds() + }; + } catch (const tf2::TransformException & ex) { + RCLCPP_WARN(node_->get_logger(), "Could not transform %s to %s: %s", + source_frame.c_str(), target_frame.c_str(), ex.what()); + return std::nullopt; + } +} + +} // namespace +``` + +### Static Transform Broadcaster (C++) + +```cpp +// infrastructure/ros2/helpers/tf_publisher.hpp +#include + +class TFPublisher { +public: + TFPublisher(rclcpp::Node::SharedPtr node) : node_(node) { + static_broadcaster_ = std::make_shared(node); + } + + void publish_static(const std::string& parent, const std::string& child, + const std::array& trans, + const std::array& rot) { + geometry_msgs::msg::TransformStamped t; + t.header.stamp = node_->get_clock()->now(); + t.header.frame_id = parent; + t.child_frame_id = child; + t.transform.translation.x = trans[0]; + // ... + static_broadcaster_->sendTransform(t); + } + +private: + rclcpp::Node::SharedPtr node_; + std::shared_ptr static_broadcaster_; +}; +``` + +## Best Practices + +1. **Isolate Dependencies**: Domain Use Cases should never import `tf2_ros`. +2. **Exception Handling**: Always catch `tf2::TransformException`. +3. **Buffer Cache Time**: Default is usually 10s. +4. **Time Sync**: Use precise timestamps (not `Time(0)`) for data synchronization when possible. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..760c017 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,86 @@ +# AGENTS.md + +This file defines repository-level instructions for Codex in this ROS2/robotics project. + +## Scope And Priority + +- These instructions apply to the whole repository. +- If a deeper `AGENTS.md` exists in a subdirectory, the deeper file overrides this file for files under that path. +- Direct user instructions override this file. + +## Project Context + +- Domain: ROS2 robotics software with Clean Architecture boundaries. +- Target ROS distro: Humble. +- Primary guidance root: `.codex/`. +- Skills location: `.codex/skills/`. + +## Required References + +- ROS2 docs: `https://docs.ros.org/en/humble/index.html` +- Codex AGENTS.md guide: `https://developers.openai.com/codex/guides/agents-md` +- Codex skills docs: `https://developers.openai.com/codex/skills` +- Codex rules docs: `https://developers.openai.com/codex/rules` +- OpenAI skills examples: `https://github.com/openai/skills` + +When there is a conflict: +1. Prefer explicit user instructions. +2. Prefer ROS2 Humble docs for ROS behavior/APIs. +3. Prefer Codex docs for agent/skills/rules conventions. + +## What Codex Should Load First + +1. `.codex/CODEX.md` +2. `.codex/rules/*.md` +3. `.codex/rules/default.rules` +4. Relevant skill from `.codex/skills//SKILL.md` +5. `.codex/commands/ros2.md` when command help is needed + +Load only relevant files; avoid loading all skills/rules if not needed. + +## Robotics Engineering Constraints + +- Keep Clean Architecture boundaries: + - Domain: no ROS dependencies. + - Application: orchestrates use-cases. + - Infrastructure: ROS2 nodes/messages/adapters. +- Prefer deterministic behavior in control loops and callbacks. +- Use ROS2 QoS intentionally for sensor, control, and state topics. +- For TF2, keep ROS message types out of domain entities. +- For hardware/safety paths, fail safely and log actionable diagnostics. + +## Implementation Workflow + +1. Understand request and identify affected packages/nodes. +2. Select matching skill from `.codex/skills/`. +3. Apply relevant rules from `.codex/rules/`. +4. Implement minimal changes needed. +5. Run targeted validation (`colcon test`, package tests, lint if available). +6. Summarize changes, assumptions, and any unvalidated areas. + +## Skill Usage Rules + +- Trigger by direct name (for example, `$ros2-dockerfile`) or semantic match. +- Use kebab-case skill names. +- Read `SKILL.md` first; read additional files only when required. +- Keep `.codex/skills` as the source of truth. + +## Docker And Deployment + +- Follow `.codex/rules/ros2_dockerfile.md` for Dockerfile changes. +- Prefer `ros:humble-ros-base` unless desktop tools are needed. +- Run `rosdep` before `colcon build` inside images. +- Use multi-stage builds for runtime images when possible. + +## Command Safety + +- Follow `.codex/rules/default.rules` policy. +- Never use destructive commands unless explicitly requested. +- Treat `git push`, Docker changes, and system-level commands as approval-sensitive operations. + +## Quality Bar + +- Keep edits focused and reversible. +- Do not introduce placeholder code like "TODO implement later" for requested functionality. +- Update nearby docs/instructions when behavior or workflows change. +- If tests are not run, state that clearly.