{
  "schemaVersion": "1.0",
  "item": {
    "slug": "ros2-skill",
    "name": "ROS 2 Skill",
    "source": "tencent",
    "type": "skill",
    "category": "开发工具",
    "sourceUrl": "https://clawhub.ai/adityakamath/ros2-skill",
    "canonicalUrl": "https://clawhub.ai/adityakamath/ros2-skill",
    "targetPlatform": "OpenClaw"
  },
  "install": {
    "downloadMode": "redirect",
    "downloadUrl": "/downloads/ros2-skill",
    "sourceDownloadUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=ros2-skill",
    "sourcePlatform": "tencent",
    "targetPlatform": "OpenClaw",
    "installMethod": "Manual import",
    "extraction": "Extract archive",
    "prerequisites": [
      "OpenClaw"
    ],
    "packageFormat": "ZIP package",
    "includedAssets": [
      "EXAMPLES.md",
      "CHANGELOG.md",
      "README.md",
      "SKILL.md",
      "scripts/ros2_node.py",
      "scripts/ros2_control.py"
    ],
    "primaryDoc": "SKILL.md",
    "quickSetup": [
      "Download the package from Yavira.",
      "Extract the archive and review SKILL.md first.",
      "Import or place the package into your OpenClaw setup."
    ],
    "agentAssist": {
      "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
      "steps": [
        "Download the package from Yavira.",
        "Extract it into a folder your agent can access.",
        "Paste one of the prompts below and point your agent at the extracted folder."
      ],
      "prompts": [
        {
          "label": "New install",
          "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Then review README.md for any prerequisites, environment setup, or post-install checks. Tell me what you changed and call out any manual steps you could not complete."
        },
        {
          "label": "Upgrade existing",
          "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Then review README.md for any prerequisites, environment setup, or post-install checks. Summarize what changed and any follow-up checks I should run."
        }
      ]
    },
    "sourceHealth": {
      "source": "tencent",
      "status": "healthy",
      "reason": "direct_download_ok",
      "recommendedAction": "download",
      "checkedAt": "2026-05-07T17:22:31.273Z",
      "expiresAt": "2026-05-14T17:22:31.273Z",
      "httpStatus": 200,
      "finalUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=afrexai-annual-report",
      "contentType": "application/zip",
      "probeMethod": "head",
      "details": {
        "probeUrl": "https://wry-manatee-359.convex.site/api/v1/download?slug=afrexai-annual-report",
        "contentDisposition": "attachment; filename=\"afrexai-annual-report-1.0.0.zip\"",
        "redirectLocation": null,
        "bodySnippet": null
      },
      "scope": "source",
      "summary": "Source download looks usable.",
      "detail": "Yavira can redirect you to the upstream package for this source.",
      "primaryActionLabel": "Download for OpenClaw",
      "primaryActionHref": "/downloads/ros2-skill"
    },
    "validation": {
      "installChecklist": [
        "Use the Yavira download entry.",
        "Review SKILL.md after the package is downloaded.",
        "Confirm the extracted package contains the expected setup assets."
      ],
      "postInstallChecks": [
        "Confirm the extracted package includes the expected docs or setup files.",
        "Validate the skill or prompts are available in your target agent workspace.",
        "Capture any manual follow-up steps the agent could not complete."
      ]
    },
    "downloadPageUrl": "https://openagent3.xyz/downloads/ros2-skill",
    "agentPageUrl": "https://openagent3.xyz/skills/ros2-skill/agent",
    "manifestUrl": "https://openagent3.xyz/skills/ros2-skill/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/ros2-skill/agent.md"
  },
  "agentAssist": {
    "summary": "Hand the extracted package to your coding agent with a concrete install brief instead of figuring it out manually.",
    "steps": [
      "Download the package from Yavira.",
      "Extract it into a folder your agent can access.",
      "Paste one of the prompts below and point your agent at the extracted folder."
    ],
    "prompts": [
      {
        "label": "New install",
        "body": "I downloaded a skill package from Yavira. Read SKILL.md from the extracted folder and install it by following the included instructions. Then review README.md for any prerequisites, environment setup, or post-install checks. Tell me what you changed and call out any manual steps you could not complete."
      },
      {
        "label": "Upgrade existing",
        "body": "I downloaded an updated skill package from Yavira. Read SKILL.md from the extracted folder, compare it with my current installation, and upgrade it while preserving any custom configuration unless the package docs explicitly say otherwise. Then review README.md for any prerequisites, environment setup, or post-install checks. Summarize what changed and any follow-up checks I should run."
      }
    ]
  },
  "documentation": {
    "source": "clawhub",
    "primaryDoc": "SKILL.md",
    "sections": [
      {
        "title": "ROS 2 Skill",
        "body": "Controls and monitors ROS 2 robots directly via rclpy.\n\nArchitecture: Agent → ros2_cli.py → rclpy → ROS 2\n\nAll commands output JSON. Errors contain {\"error\": \"...\"}.\n\nFor full command reference with arguments, options, and output examples, see references/COMMANDS.md."
      },
      {
        "title": "Agent Behaviour Rules",
        "body": "These rules are absolute and apply to every request involving a ROS 2 robot."
      },
      {
        "title": "Rule 0 — Full introspection before every action (non-negotiable)",
        "body": "Before publishing to any topic, calling any service, or sending any action goal, you MUST complete the introspection steps below. There are no exceptions, not even for \"obvious\" or \"conventional\" names.\n\nThis rule exists because:\n\nThe velocity topic is not always /cmd_vel. It may be /base/cmd_vel, /robot/cmd_vel, /mobile_base/cmd_vel, or anything else.\nThe message type is not always Twist. Many robots use TwistStamped, and the payload structure differs.\nThe odometry topic is not always /odom. It may be /wheel_odom, /robot/odom, /base/odometry, etc.\nConvention-based guessing causes silent failures, wrong topics, and physical accidents.\n\nPre-flight introspection protocol — run ALL applicable steps before acting:\n\nAction typeRequired introspectionPublish to a topic1. topics find <msg_type> to discover the real topic name<br>2. topics type <discovered_topic> to confirm the exact type<br>3. interface proto <exact_type> to get the default payload templateCall a service1. services list or services find <srv_type> to discover the real name<br>2. services details <discovered_service> to get request/response fieldsSend an action goal1. actions list or actions find <action_type> to discover the real name<br>2. actions details <discovered_action> to get goal/result/feedback fieldsMove a robotFull Movement Workflow — see Rule 3 and the canonical section belowRead a sensortopics find <msg_type> to discover the topic; never subscribe to a hardcoded nameAny operation involving a nodenodes list first; never assume a node name\n\nParameter introspection is mandatory before any movement command. Velocity limits can live on any node — not just nodes with \"controller\" in the name. Before publishing velocity:\n\nRun nodes list to get every node currently running\nRun params list <NODE> on every single node in the list (run in parallel batches if there are many)\nFor each node, look for any parameter whose name contains max, limit, vel, speed, or accel (case-insensitive). These are candidates for velocity limits.\nRun params get <NODE>:<param> for every candidate found across all nodes\nIdentify the binding ceiling: the minimum across all discovered linear limit values and the minimum across all discovered angular/theta limit values\nCap your commanded velocity at that ceiling. If no limits are found on any node, use conservative defaults (0.2 m/s linear, 0.75 rad/s angular) and note this to the user.\n\nNever hardcode or assume:\n\n❌ Never use /cmd_vel without first discovering the velocity topic with topics find\n❌ Never use Twist payload without first confirming the type is not TwistStamped via topics type\n❌ Never use /odom without first discovering the odometry topic with topics find\n❌ Never use --yaw, --yaw-delta, or --field for rotation — the only correct flag is --rotate N --degrees (or --rotate N for radians). Use negative N for CW; --rotate sign and angular.z sign must always match.\n❌ Never assume any topic, service, action, or node name from ROS 2 convention\n❌ Never assume a message type from a topic name\n\nIntrospection commands return discovered names. Use those names — not the ones you expect."
      },
      {
        "title": "Rule 0.1 — Session-start checks (run once per session, before any task)",
        "body": "Before executing any user command in a new session, run these checks. They take seconds and catch the most common causes of silent failure.\n\nStep 1 — Run a health check:\n\npython3 {baseDir}/scripts/ros2_cli.py doctor\n\nIf the doctor reports critical failures (DDS issues, missing packages, no nodes), stop and tell the user. Do not attempt to operate a robot that fails its health check.\n\nStep 2 — Check for simulated time:\n\npython3 {baseDir}/scripts/ros2_cli.py topics find rosgraph_msgs/msg/Clock\n\nIf /clock is found, simulated time is in use. Verify it is actively publishing before issuing any timed command:\n\npython3 {baseDir}/scripts/ros2_cli.py topics subscribe /clock --max-messages 1 --timeout 3\n\nIf no message arrives: the simulator is paused or crashed — do not proceed with time-sensitive operations.\n\nStep 3 — Check lifecycle nodes (if any):\n\npython3 {baseDir}/scripts/ros2_cli.py lifecycle nodes\n\nIf lifecycle-managed nodes exist, check their states:\n\npython3 {baseDir}/scripts/ros2_cli.py lifecycle get <node>\n\nA node in unconfigured or inactive state will silently fail when its topics or services are used. Activate required nodes before proceeding.\n\nThese checks are session-level. Do not re-run for every command. Re-run only if the user relaunches the robot or if nodes appear/disappear unexpectedly."
      },
      {
        "title": "Rule 0.5 — Never hallucinate commands, flags, or names",
        "body": "If you are not certain a command, flag, topic name, or argument exists — verify it before using it. Do not guess.\n\nThe failure mode to avoid: inventing a flag like --yaw-delta or --rotate-degrees because it sounds plausible, then failing and asking the user for help. That is the worst possible outcome — the error was self-inflicted and the user had nothing to do with it.\n\nThe verification chain:\n\nCheck this skill first. The full command reference is in references/COMMANDS.md. If a flag or command is not listed there, it does not exist.\nIf still unsure, run --help on the exact subcommand before constructing the call. Every subcommand supports --help and prints its accepted flags without requiring a live ROS 2 graph. This is mandatory, not optional.\npython3 {baseDir}/scripts/ros2_cli.py topics publish-until --help\npython3 {baseDir}/scripts/ros2_cli.py topics publish-sequence --help\npython3 {baseDir}/scripts/ros2_cli.py actions send --help\n# etc. — use the exact subcommand you are about to call\n\n\nIf still stuck after checking both, ask the user. This is the only acceptable reason to ask — not because you assumed something and it failed.\n\n--help requires ROS 2 to be sourced. Running --help before ROS 2 is sourced will return a JSON error instead of help text. This is not a concern in practice — ROS 2 must be sourced before any robot operation (it is a hard precondition of this skill), so --help will always be available during normal use. If you see a Missing ROS 2 dependency error from --help, fix the ROS 2 environment first (see Setup section and Rule 0.1).\n\nNever:\n\nInvent a flag and try it, then report failure to the user\nAssume a capability exists because it would be logical or convenient\nAsk the user to resolve an error you caused by guessing"
      },
      {
        "title": "Rule 1 — Discover before you act, never ask",
        "body": "Never ask the user for names, types, or IDs that can be discovered from the live system. This includes topic names, service names, action names, node names, parameter names, message types, and controller names. Always query the robot first.\n\nWhat you needHow to discover itTopic nametopics list or topics find <msg_type>Topic message typetopics type <topic>Service nameservices list or services find <srv_type>Service request/response fieldsservices details <service>Action server nameactions list or actions find <action_type>Action goal/result/feedback fieldsactions details <action>Node namenodes listNode's topics, services, actionsnodes details <node>Parameter names on a nodeparams list <node>Parameter valueparams get <node:param>Parameter type and constraintsparams describe <node:param>Controller names and statescontrol list-controllersHardware componentscontrol list-hardware-componentsMessage / service / action type fieldsinterface show <type> or interface proto <type>\n\nOnly ask the user if:\n\nThe discovery command returns an empty result or an error, and\nThere is genuinely no other way to determine the information from the live system."
      },
      {
        "title": "Rule 2 — Use ros2-skill before saying you can't",
        "body": "Never tell the user you don't know how to do something with a ROS 2 robot without first checking whether ros2-skill has a command for it. This skill covers the full range of ROS 2 operations: topics, services, actions, parameters, nodes, lifecycle, controllers, diagnostics, battery, images, interfaces, presets, and more.\n\nWhen a task seems unfamiliar, look it up in the quick reference tables below before responding. Common operations that agents sometimes miss:\n\nTaskros2-skill commandCapture a camera imagetopics capture-image --topic <topic> --output <file>Read laser / camera / IMU / odom datatopics subscribe <topic>Call a ROS 2 serviceservices call <service> <json>Send a navigation or manipulation goalactions send <action> <json>Change a node parameter at runtimeparams set <node:param> <value>Save/restore a parameter configurationparams preset-save / params preset-loadActivate or deactivate a controllercontrol set-controller-state <name> active|inactiveRun a health checkdoctorEmergency stopestopCheck diagnosticstopics diagCheck batterytopics battery\n\nIf you genuinely cannot find a matching command after checking both the quick reference and the COMMANDS.md reference, say so clearly and explain what you checked — do not silently guess or use a partial solution."
      },
      {
        "title": "Rule 3 — Movement algorithm (always follow this sequence)",
        "body": "For any user request involving movement — regardless of whether a distance or angle is specified — follow this algorithm exactly. Do not skip steps. Do not ask the user anything that can be resolved by running a command.\n\nStep 1 — Discover the velocity command topic and confirm its exact type\n\nRun both searches in parallel:\n\ntopics find geometry_msgs/msg/Twist\ntopics find geometry_msgs/msg/TwistStamped\n\nRecord the discovered topic name — call it VEL_TOPIC. Then confirm the exact type:\n\ntopics type <VEL_TOPIC>\n\nUse the confirmed type to choose the payload structure:\n\ngeometry_msgs/msg/Twist: {\"linear\":{\"x\":0.2,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}\ngeometry_msgs/msg/TwistStamped: {\"header\":{\"stamp\":{\"sec\":0},\"frame_id\":\"\"},\"twist\":{\"linear\":{\"x\":0.2,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}}\n\nIf both find commands return results, run topics type on each and use the type returned — do not guess.\n\nStep 2 — Discover the odometry topic\n\ntopics find nav_msgs/msg/Odometry\n\nRecord the discovered topic name — call it ODOM_TOPIC.\n\nStep 3 — Choose the execution method\n\nSituationMethodDistance or angle specified and odometry foundpublish-until with --monitor <ODOM_TOPIC> — closed loop, stops on sensor feedbackDistance or angle specified and no odometrypublish-sequence with calculated duration — open loop. Tell the user: \"No odometry found. Running open-loop. Distance/angle accuracy is not guaranteed.\"No distance or angle specified (open-ended movement)publish-sequence with a stop command as the final message\n\nStep 4 — Execute using only discovered names\n\nUse VEL_TOPIC and ODOM_TOPIC from Steps 1–2. Never substitute /cmd_vel, /odom, or any other assumed name.\n\nDistance commands:\n\ntopics publish-until <VEL_TOPIC> '<payload>' --monitor <ODOM_TOPIC> --field pose.pose.position.x --delta <N> --timeout 30\n\nAngle/rotation commands — always use --rotate, never --field or --yaw:\n\n# CCW (left): positive --rotate + positive angular.z\ntopics publish-until <VEL_TOPIC> '<payload>' --monitor <ODOM_TOPIC> --rotate <+N> --degrees --timeout 30\n# CW (right):  negative --rotate + negative angular.z\ntopics publish-until <VEL_TOPIC> '<payload>' --monitor <ODOM_TOPIC> --rotate <-N> --degrees --timeout 30\n\n--rotate sign = direction. Positive = CCW. Negative = CW. angular.z sign must always match --rotate sign — mismatched signs cause timeout. There is no --yaw flag. Do not attempt to monitor orientation fields manually.\nOpen-ended or fallback (stop is always the last message):\n\ntopics publish-sequence <VEL_TOPIC> '[<move_payload>, <zero_payload>]' '[<duration>, 0.5]'\n\nSee the Movement Workflow section below for complete worked examples covering every case."
      },
      {
        "title": "Rule 4 — Infer the goal, resolve the details",
        "body": "When a user asks to do something, infer what they want at the goal level, then resolve all concrete details (topic names, types, field paths) from the live system.\n\nExamples:\n\n\"Take a picture\" → find compressed image topics (topics find sensor_msgs/msg/CompressedImage), capture from the first active result\n\"Move the robot forward\" → find velocity topic (topics find geometry_msgs/msg/Twist and TwistStamped), publish with the matching structure\n\"What is the battery level?\" → topics battery (auto-discovers BatteryState topics)\n\"List available controllers\" → control list-controllers\n\"What parameters does the camera node have?\" → nodes list to find the camera node name, then params list <node>"
      },
      {
        "title": "Rule 5 — Execute, don't ask",
        "body": "The user's message is the approval. Act on it.\n\nIf the intent is clear, execute immediately. Do not ask for confirmation, do not summarise what you are about to do and wait for a response, do not say \"I'll now run X — shall I proceed?\". Just run it.\n\nThis applies without exception to:\n\nRunning or launching nodes (launch new, run new)\nPublishing to topics\nCalling services\nSetting parameters\nStarting or stopping controllers\nAny command where the intent and target are unambiguous\n\nThe only time to stop and ask is when there is genuine ambiguity that cannot be resolved from the live system — for example:\n\nMultiple packages or launch files match and you cannot determine which one the user means\nA required argument has no match in --show-args and no reasonable fuzzy match exists\nThe user's request contradicts itself or is physically unsafe to guess\n\nEverything else: just do it.\n\nExplicit list of things that must never trigger a question:\n\n\"Should I run this command?\" — No. Run it.\n\"Would you like me to proceed?\" — No. Proceed.\n\"Do you want me to use X topic?\" — No. Use it.\n\"Shall I launch the file now?\" — No. Launch it.\n\"Do you want me to set this parameter?\" — No. Set it.\n\"I found Y — would you like me to use it?\" — No. Use it."
      },
      {
        "title": "Rule 6 — Minimal reporting by default",
        "body": "Keep output minimal. The user wants results, not narration.\n\nSituationWhat to reportOperation succeededOne line: what was done and the key outcome. Example: \"Done. Moved 1.02 m forward.\"Movement completedStart position, end position, actual distance/angle travelled, and any anomalies — nothing elseNo suitable topic/source foundClear error with what was searched and what to try nextSafety condition triggeredImmediate notification with what happened and what was sent (stop command)Operation failedError message with cause and recovery suggestion\n\nNever report by default:\n\nThe topic name selected, unless it is ambiguous or unexpected\nThe message type discovered\nIntermediate introspection results\nStep-by-step narration of what you are about to do\n\nReport everything (verbose mode) only when the user explicitly asks — e.g. \"show me what topics you found\", \"give me the full details\", \"what type did you use?\""
      },
      {
        "title": "1. Source ROS 2 environment",
        "body": "source /opt/ros/${ROS_DISTRO}/setup.bash"
      },
      {
        "title": "2. Install dependencies",
        "body": "pip install rclpy"
      },
      {
        "title": "3. Run on ROS 2 robot",
        "body": "The CLI must run on a machine with ROS 2 installed and sourced."
      },
      {
        "title": "Important: Check ROS 2 First",
        "body": "Before any operation, verify ROS 2 is available:\n\npython3 {baseDir}/scripts/ros2_cli.py version"
      },
      {
        "title": "Quick Decision Card",
        "body": "Every user request follows this pattern:\n\nUser: \"do X\"\nAgent thinks:\n  1. Is X about reading data? → Use TOPICS SUBSCRIBE\n  2. Is X about movement (any kind)? → Follow the Movement Workflow (Rule 3 / canonical section)\n  3. Is X a one-time trigger? → Use SERVICES CALL or ACTIONS SEND\n  4. Is X about system info? → Use LIST commands\n\nAgent does (for movement):\n  1. Find velocity topic: topics find geometry_msgs/Twist + TwistStamped\n  2. Find odometry topic: topics find nav_msgs/Odometry\n  3. Distance/angle specified + odom found → publish-until (closed loop)\n     Distance/angle specified + no odom → publish-sequence, notify user (open loop)\n     No distance/angle → publish-sequence with stop\n\nFor movement, always follow the Movement Workflow section. That section is the single source of truth."
      },
      {
        "title": "Agent Decision Framework (MANDATORY)",
        "body": "RULE: NEVER ask the user anything that can be discovered from the ROS 2 graph."
      },
      {
        "title": "Step 1: Understand User Intent",
        "body": "User says...Agent interprets as...Agent must...\"What topics exist?\"List topicsRun topics list\"What nodes exist?\"List nodesRun nodes list\"What services exist?\"List servicesRun services list\"What actions exist?\"List actionsRun actions list\"Read the LiDAR/scan\"Subscribe to LaserScanFind LaserScan topic → subscribe\"Read odometry/position\"Subscribe to OdometryFind Odometry topic → subscribe\"Read camera/image\"Subscribe to Image/CompressedImageFind Image topics → subscribe\"Read joint states/positions\"Subscribe to JointStateFind JointState topic → subscribe\"Read IMU/accelerometer\"Subscribe to ImuFind Imu topic → subscribe\"Read battery/power\"Subscribe to BatteryStateFind BatteryState topic → subscribe\"Read joystick/gamepad\"Subscribe to JoyFind Joy topic → subscribe\"Check robot diagnostics/health\"Subscribe to diagnosticsFind /diagnostics topic → subscribe\"Check TF/transforms\"Check TF topicsFind /tf, /tf_static topics → subscribe\"Move/drive/turn (mobile robot)\"Open-ended movement, no targetFind Twist/TwistStamped → publish-sequence with stop\"Move forward/back N meters\"Closed-loop distance → Movement Workflow Case AFind odom → publish-until --euclidean --field pose.pose.position --delta N (frame-independent Euclidean distance)\"Rotate N degrees / turn left/right / turn N radians\"Closed-loop rotation → Movement Workflow Case BFind odom → publish-until --rotate ±N --degrees (CCW = positive, CW = negative). Sign of --rotate MUST match sign of angular.z. Never use --field or --yaw for rotation\"Move arm/joint (manipulator)\"Publish JointTrajectoryFind JointTrajectory topic → publish\"Control gripper\"Publish GripperCommand or JointTrajectoryFind gripper topic → publish\"Stop the robot\"Publish zero velocityFind Twist/TwistStamped → topics type to confirm → publish zeros in confirmed type\"Emergency stop\"Publish zero velocityRun estop command\"Call /reset\"Call serviceFind service → call\"Navigate to...\"Send actionFind action → send goal\"Execute trajectory\"Send actionFind FollowJointTrajectory or ExecuteTrajectory → send\"Run launch file\"Launch fileFind package → find launch file → launch in tmux\"List running launches\"List sessionsRun launch list\"Kill launch\"Kill sessionRun launch kill <session>\"What controllers?\"List controllersRun control list-controllers\"What hardware?\"List hardwareRun control list-hardware-components\"What lifecycle nodes?\"List managed nodesRun lifecycle nodes\"Check lifecycle state\"Get node stateRun lifecycle get <node>\"Configure/activate lifecycle node\"Set lifecycle stateRun lifecycle set <node> <transition>\"Run diagnostics/health check\"Run doctorRun doctor\"Test connectivity\"Run multicast testRun doctor hello\"What parameters?\"List paramsFind node → params list\"What is the max speed?\"Get paramsFind controller → get velocity limits\"Save/load parameter config\"Use presetsRun params preset-save / params preset-load\"Check battery level\"Subscribe to BatteryStateRun topics battery or find BatteryState topic"
      },
      {
        "title": "Step 2: Find What Exists",
        "body": "ALWAYS start by exploring what's available:\n\n# These 4 commands tell you EVERYTHING about the system\npython3 {baseDir}/scripts/ros2_cli.py topics list      # All topics\npython3 {baseDir}/scripts/ros2_cli.py services list    # All services\npython3 {baseDir}/scripts/ros2_cli.py actions list    # All actions\npython3 {baseDir}/scripts/ros2_cli.py nodes list      # All nodes"
      },
      {
        "title": "Step 3: Search by Message Type",
        "body": "To find a topic/service/action, search by what you need:\n\nNeed to find...Search command...Velocity command topic (mobile)topics find geometry_msgs/Twist AND topics find geometry_msgs/TwistStamped → then topics type <result> to confirm exact typePosition/odom topictopics find nav_msgs/OdometryJoint positionstopics find sensor_msgs/JointStateJoint trajectory (arm control)topics find trajectory_msgs/JointTrajectoryLiDAR datatopics find sensor_msgs/LaserScanCamera feedtopics find sensor_msgs/Image OR topics find sensor_msgs/CompressedImageIMU datatopics find sensor_msgs/ImuJoysticktopics find sensor_msgs/JoyBattery/powertopics find sensor_msgs/BatteryStateTemperaturetopics find sensor_msgs/TemperaturePoint cloudstopics find sensor_msgs/PointCloud2TF transformsSubscribe to /tf or /tf_staticDiagnosticsSubscribe to /diagnosticsClock (simulated time)Subscribe to /clockService by typeservices find <service_type>Action by typeactions find <action_type>"
      },
      {
        "title": "Step 4: Get Message Structure",
        "body": "Before publishing or calling, always confirm the type and get the structure:\n\n# Confirm the exact message type of a discovered topic (critical — never skip this)\npython3 {baseDir}/scripts/ros2_cli.py topics type <discovered_topic>\n\n# Get field structure (for building payloads)\npython3 {baseDir}/scripts/ros2_cli.py topics message <confirmed_message_type>\n\n# Get default values (copy-paste template)\npython3 {baseDir}/scripts/ros2_cli.py interface proto <confirmed_message_type>\n\n# Get service/action request structure\npython3 {baseDir}/scripts/ros2_cli.py services details <service_name>\npython3 {baseDir}/scripts/ros2_cli.py actions details <action_name>"
      },
      {
        "title": "Step 5: Get Safety Limits (for movement)",
        "body": "ALWAYS check for velocity limits before publishing movement commands. Limits can be on ANY node — not just controller nodes. Scan every node.\n\n# Step 1: List every running node\npython3 {baseDir}/scripts/ros2_cli.py nodes list\n\n# Step 2: Dump all parameters from every node\n# Run in parallel — one params list per node\npython3 {baseDir}/scripts/ros2_cli.py params list <NODE_1>\npython3 {baseDir}/scripts/ros2_cli.py params list <NODE_2>\n# ... repeat for every node\n\n# Step 3: For each node, filter parameter names that contain:\n#   max, limit, vel, speed, accel (case-insensitive)\n# These are candidate velocity/acceleration limits.\n\n# Step 4: Retrieve the value of every candidate\npython3 {baseDir}/scripts/ros2_cli.py params get <NODE>:<candidate_param>\n# Repeat for every candidate across every node.\n\n# Step 5: Compute binding ceiling\n# linear_ceiling  = min of all discovered linear limit values\n# angular_ceiling = min of all discovered angular/theta limit values\n# velocity = min(requested_velocity, ceiling)\n# Never exceed the ceiling. Use 50% of the ceiling if unsure of the appropriate fraction.\n\nIf no limits are found on any node: use conservative defaults (0.2 m/s linear, 0.75 rad/s angular) and tell the user."
      },
      {
        "title": "Global Options",
        "body": "--timeout and --retries are global flags that apply to every command making service or action calls.\n\n--timeout must be placed before the command name (e.g. --timeout 10 services call …).\n--retries can be placed before the command name OR after it for services call, actions send, and actions cancel — both positions work.\n\nOptionDefaultDescription--timeout SECONDSper-command defaultOverride the per-command timeout globally--retries N1Total attempts before giving up; 1 = single attempt with no retry\n\npython3 {baseDir}/scripts/ros2_cli.py --timeout 30 params list /turtlesim\npython3 {baseDir}/scripts/ros2_cli.py --retries 3 services call /spawn '{}'"
      },
      {
        "title": "Rule 1: Never Ask User for These",
        "body": "The agent MUST discover these automatically:\n\nUser might ask...Agent must...\"What topic do I use?\"Use topics find <type> to discover\"What message type?\"Use topics type <topic> or topics find <type>\"What is the message structure?\"Use topics message <type> or interface show <type>\"What are the safety limits?\"Use params list on controller nodes\"Is there odometry?\"Use topics find nav_msgs/Odometry"
      },
      {
        "title": "Rule 2: Only Ask User After ALL Discovery Fails",
        "body": "ONLY ask the user when:\n\ntopics find geometry_msgs/Twist AND topics find geometry_msgs/TwistStamped both return empty\ntopics find nav_msgs/Odometry returns empty (and you need odometry for distance)\nYou've checked params on ALL controller nodes and found NO velocity limits\nThe service/action the user mentions doesn't exist in services list / actions list"
      },
      {
        "title": "Rule 3: Movement Requires Odometry Feedback",
        "body": "See Agent Behaviour Rule 3 above — it is absolute and overrides any example in this document.\n\nIn short: for any \"move N meters\" or \"rotate N degrees\" command, you MUST find the odometry topic first (topics find nav_msgs/Odometry) and use publish-until --monitor <odom_topic>. publish-sequence with a fixed time is forbidden when odometry is available.\n\nALWAYS apply safety limits: velocity = min(requested, max_velocity)"
      },
      {
        "title": "Rule 4: Always Stop After Movement",
        "body": "publish-until stops automatically when the odometry condition is met — no explicit stop step is needed.\n\nFor open-ended publishes (topics publish or publish-sequence fallback), the final message MUST be all zeros.\n\nAlways use the topic name and payload type discovered in Steps 1–2. <VEL_TOPIC> and <ODOM_TOPIC> are placeholders for your discovered values.\n\n# WRONG: open-ended publish with no stop, and hardcoded /cmd_vel — never do this\npython3 {baseDir}/scripts/ros2_cli.py topics publish /cmd_vel '{\"linear\":{\"x\":1.0}}'\n\n# CORRECT (distance specified, odometry available): use publish-until — stops itself\n# VEL_TOPIC and ODOM_TOPIC come from your introspection steps\npython3 {baseDir}/scripts/ros2_cli.py topics publish-until <VEL_TOPIC> \\\n  '<payload_matching_confirmed_type>' \\\n  --monitor <ODOM_TOPIC> --euclidean --field pose.pose.position --delta 1.0 --timeout 30\n\n# CORRECT (fallback only — no odometry, no distance specified): publish-sequence with stop at end\npython3 {baseDir}/scripts/ros2_cli.py topics publish-sequence <VEL_TOPIC> \\\n  '[<move_payload>, <zero_payload>]' \\\n  '[3.0, 0.5]'"
      },
      {
        "title": "Rule 5: Handle Multiple Same-Type Topics",
        "body": "When multiple topics of the same type exist (e.g., 2 cameras, 3 LiDARs):\n\nList all candidates:\npython3 {baseDir}/scripts/ros2_cli.py topics find sensor_msgs/Image\n# Returns: [\"/camera_front/image_raw\", \"/camera_rear/image_raw\", ...]\n\n\n\nSelect based on context or naming convention:\n\nFront camera: prefer topics with front, rgb, color in name\nRear camera: prefer topics with rear, back in name\nPrimary LiDAR: prefer topics with front, base, main in name\nDefault: use first topic in the list\n\n\n\nLet user know which one you're using:\n\n\"Found 3 camera topics. Using /camera_front/image_raw.\""
      },
      {
        "title": "Error Recovery",
        "body": "When commands fail, follow this recovery process:"
      },
      {
        "title": "Subscribe Timeouts",
        "body": "ErrorRecoveryTimeout waiting for message1. Check topics details <topic> to verify publisher exists<br>2. Try a different topic if multiple exist<br>3. Increase --duration or --timeoutNo messages received1. Verify publisher is running: topics details <topic><br>2. Check if topic requires subscription to trigger"
      },
      {
        "title": "Publish Failures",
        "body": "ErrorRecoveryCould not load message type1. Verify type: topics type <topic><br>2. Ensure ROS workspace is builtFailed to create publisher1. Check topic exists: topics list<br>2. Verify node has permission to publish"
      },
      {
        "title": "Service/Action Failures",
        "body": "ErrorRecoveryService not found1. Verify service exists: services list<br>2. Check service type: services type <service>Action not found1. Verify action exists: actions list<br>2. Check action type: actions type <action>Service call timeout1. Increase --timeout<br>2. Verify service server is runningAction goal rejected1. Check action details for goal requirements<br>2. Verify robot is in correct state"
      },
      {
        "title": "Parameter Failures",
        "body": "ErrorRecoveryNode not found1. Verify node exists: nodes list<br>2. Check namespaceParameter not found1. List params: params list <node><br>2. Parameter may not exist on this node"
      },
      {
        "title": "Movement / publish-until Failures",
        "body": "ErrorRecoverypublish-until times out without reaching target1. Immediately send estop — do not wait, do not retry, do not ask the user first<br>2. Subscribe to <ODOM_TOPIC> and check twist.twist.linear / twist.twist.angular: if any value > 0.01, the robot is still moving — keep estop sent and wait for it to stop before continuing<br>3. Then diagnose: topics details <ODOM_TOPIC> — if publisher count dropped to 0 mid-motion, odometry died; notify user<br>4. If odometry is healthy but robot is slow: increase --timeout and retry only after the robot has fully stoppedOdometry not updating during motion1. Immediately send zero-velocity: estop<br>2. Check topics details <ODOM_TOPIC> for publisher count and topics hz <ODOM_TOPIC> for rate<br>3. Do NOT continue publishing if odometry is stale — it is a runaway riskVelocity topic has no subscribers1. Check control list-controllers — the controller may be inactive<br>2. Activate the controller: control set-controller-state <name> active<br>3. Re-verify with topics details <VEL_TOPIC> before retryingpublish-until hangs / no feedback1. Verify monitor topic: topics details <ODOM_TOPIC><br>2. Verify the field path is correct: subscribe once and inspect field names<br>3. Check --timeout is set"
      },
      {
        "title": "Action Preemption — actions cancel vs estop",
        "body": "Use this decision table whenever an in-flight action goal needs to be stopped:\n\nSituationActionReasonGoal is running but user wants to abort gracefullyactions cancel <action>Sends a cancel request to the action server; the server winds down cleanlyGoal is running and the robot is moving unsafely / not stoppingestop first, then actions cancel <action>estop publishes zero velocity immediately; cancel cleans up the goal stateGoal was rejected or timed out (robot not moving)actions cancel <action>No motion risk; cancel clears the goal stateAction server crashed / no longer respondingestopNo action server to receive cancel; stop the actuators directlyGoal completed but robot is still drifting / coastingestopMotion is no longer governed by the goal; velocity command is needed\n\nRule: If in doubt, send estop first — it is always safe. Then send actions cancel to clean up goal state. Never skip estop when the robot is or may be moving.\n\nAlways retry failed operations:\n\nUse --retries 3 for unreliable services\nUse --timeout 30 for slow operations\nWait 1-2 seconds between retries"
      },
      {
        "title": "Topic and Service Discovery",
        "body": "Never guess topic names. Any time an operation involves a topic, discover the actual topic name from the live graph first."
      },
      {
        "title": "Images and Camera",
        "body": "Always prefer compressed topics - use much less bandwidth:\n\npython3 {baseDir}/scripts/ros2_cli.py topics find sensor_msgs/msg/CompressedImage\npython3 {baseDir}/scripts/ros2_cli.py topics find sensor_msgs/msg/Image\n\nUse topics capture-image --topic <discovered> - never subscribe for images."
      },
      {
        "title": "Velocity Commands (Twist vs TwistStamped)",
        "body": "Check both types to find the topic:\n\npython3 {baseDir}/scripts/ros2_cli.py topics find geometry_msgs/msg/Twist\npython3 {baseDir}/scripts/ros2_cli.py topics find geometry_msgs/msg/TwistStamped\n\nThen confirm the exact type of the discovered topic — do not assume from the find result:\n\npython3 {baseDir}/scripts/ros2_cli.py topics type <discovered_topic>\n\nUse the confirmed type to choose the payload:\n\ngeometry_msgs/msg/Twist: {\"linear\": {\"x\": 1.0, \"y\": 0.0, \"z\": 0.0}, \"angular\": {\"x\": 0.0, \"y\": 0.0, \"z\": 0.0}}\ngeometry_msgs/msg/TwistStamped: {\"header\": {\"stamp\": {\"sec\": 0}, \"frame_id\": \"\"}, \"twist\": {\"linear\": {\"x\": 1.0, \"y\": 0.0, \"z\": 0.0}, \"angular\": {\"x\": 0.0, \"y\": 0.0, \"z\": 0.0}}}"
      },
      {
        "title": "Quick lookup table",
        "body": "If you need...Run this...Then...Imagestopics find sensor_msgs/msg/ImageSubscribe to resultLiDARtopics find sensor_msgs/msg/LaserScanSubscribe to resultOdometrytopics find nav_msgs/msg/OdometrySubscribe to resultIMUtopics find sensor_msgs/msg/ImuSubscribe to resultJoint statestopics find sensor_msgs/msg/JointStateSubscribe to resultMove robottopics find geometry_msgs/msg/Twist AND topics find geometry_msgs/msg/TwistStamped → then topics type <result> to confirmPublish to discovered topicRun launch filelaunch new <package> <file>Runs in tmux sessionList running launcheslaunch listShows tmux sessionsKill launchlaunch kill <session>Kills tmux session"
      },
      {
        "title": "Auto-Discovery for Launch Files",
        "body": "When user says \"run the bringup\" or \"launch navigation\" (partial/ambiguous request):\n\nDiscover available packages:\nros2 pkg list  # Get all packages\n\n\n\nFind matching launch files:\nros2 pkg files <package>  # Find launch files in package\n\n\n\nIntelligent inference (use context):\n\n\"bringup\" → look for packages with bringup in name, or launch files named bringup.launch.py\n\"navigation\" → look for navigation2, nav2, or launch files with navigation\n\"camera\" → look for camera-related packages\n\n\n\nIf exactly one clear match found:\n\nLaunch it immediately — do not ask for confirmation (Rule 5)\n\n\n\nIf multiple candidates found and cannot be disambiguated:\n\nPresent options: \"Found 3 launch files: X, Y, Z. Which one?\" — this is the only case where asking is permitted\n\n\n\nIf no match found:\n\nSearch more broadly: check all packages for matching launch files\nIf still nothing: ask user for exact package/file name"
      },
      {
        "title": "NEVER hallucinate:",
        "body": "❌ Never invent a package name that doesn't exist\n❌ Never invent a launch file that doesn't exist\n❌ Never assume a package exists without checking\n❌ Never invent launch argument names not present in --show-args output\n❌ Never fuzzy-match argument names yourself — the script does this automatically against real --show-args output; always pass the user's original name and let the script resolve it\n❌ Never pass arguments that weren't explicitly provided by the user"
      },
      {
        "title": "ALWAYS verify:",
        "body": "✅ Check ros2 pkg list for package existence\n✅ Check ros2 pkg files <package> for launch files\n✅ Run ros2 launch <pkg> <file> --show-args to get valid arguments\n✅ Validate each user-provided argument against --show-args output"
      },
      {
        "title": "Rule: Only ask when genuinely ambiguous",
        "body": "Per Rule 5, the user's message is the approval. For launch files:\n\nExactly one match → launch it immediately, no confirmation needed\nMultiple matches with no way to disambiguate → list them and ask which one\nNo match at all → ask for exact package/file name"
      },
      {
        "title": "Local Workspace Sourcing",
        "body": "System ROS is assumed to be already sourced (via systemd service or manually). The skill automatically sources any local workspace on top of system ROS.\n\nSearch order:\n\nROS2_LOCAL_WS environment variable\n~/ros2_ws\n~/colcon_ws\n~/dev_ws\n~/workspace\n~/ros2\n\nBehavior:\n\nScenarioBehaviorWorkspace found + builtSource automatically, run silentlyWorkspace found + NOT builtWarn user, run without sourcingWorkspace NOT foundContinue without sourcing (system ROS only)\n\nOverride option:\n\n# Set environment variable before running\nexport ROS2_LOCAL_WS=~/my_robot_ws"
      },
      {
        "title": "TF2 Transforms",
        "body": "# List all coordinate frames\npython3 {baseDir}/scripts/ros2_cli.py tf list\n\n# Lookup transform between frames\npython3 {baseDir}/scripts/ros2_cli.py tf lookup base_link map\n\n# Echo transform continuously\npython3 {baseDir}/scripts/ros2_cli.py tf echo base_link map --count 10\n\n# Echo transform once\npython3 {baseDir}/scripts/ros2_cli.py tf echo base_link map --once\n\n# Monitor a specific frame\npython3 {baseDir}/scripts/ros2_cli.py tf monitor base_link --count 5\n\n# Publish static transform — named form\npython3 {baseDir}/scripts/ros2_cli.py tf static --from base_link --to sensor --xyz 1 2 3 --rpy 0 0 0\n\n# Publish static transform — positional form\npython3 {baseDir}/scripts/ros2_cli.py tf static 0 0 0 0 0 0 base_link odom\n\n# Convert quaternion to Euler (radians) — also: e2q, quat2euler\npython3 {baseDir}/scripts/ros2_cli.py tf euler-from-quaternion 0 0 0 1\n\n# Convert Euler to quaternion (radians) — also: q2e, euler2quat\npython3 {baseDir}/scripts/ros2_cli.py tf quaternion-from-euler 0 0 1.57\n\n# Convert quaternion to Euler (degrees) — also: e2qdeg\npython3 {baseDir}/scripts/ros2_cli.py tf euler-from-quaternion-deg 0 0 0 1\n\n# Convert Euler to quaternion (degrees) — also: q2edeg\npython3 {baseDir}/scripts/ros2_cli.py tf quaternion-from-euler-deg 0 0 90\n\n# Transform a point between frames — also: tp, point\npython3 {baseDir}/scripts/ros2_cli.py tf transform-point map base_link 1 0 0\n\n# Transform a vector between frames — also: tv, vector\npython3 {baseDir}/scripts/ros2_cli.py tf transform-vector map base_link 1 0 0"
      },
      {
        "title": "Run a Launch File",
        "body": "# Basic launch\npython3 {baseDir}/scripts/ros2_cli.py launch new navigation2 navigation2.launch.py\n\n# With arguments - MUST verify arguments exist first\npython3 {baseDir}/scripts/ros2_cli.py launch new navigation2 navigation2.launch.py arg1:=value arg2:=value"
      },
      {
        "title": "⚠️ STRICT: Launch Argument Validation",
        "body": "This is critical for safety. Passing incorrect arguments has caused accidents.\n\nRules, in order:\n\nAlways fetch available arguments first via --show-args before passing any args.\nExact match → pass as-is.\nNo exact match → fuzzy-match against the real available args only.\n\nIf a close match is found (e.g. \"mock\" → \"use_mock\") → use it, but notify the user.\nThe substitution is shown in arg_notices in the output.\n\n\nNo match at all → drop the argument, do NOT pass it, notify the user.\n\nThe launch still proceeds without that argument.\nThe user is told which argument was dropped and what the available args are.\n\n\nNever invent or assume argument names. Only use names that exist in --show-args output.\nIf --show-args fails → drop all user-provided args, notify the user, still launch."
      },
      {
        "title": "Run an Executable",
        "body": "Run a ROS 2 executable in a tmux session. Similar to launch commands but for single executables. Auto-detects executable names (e.g., \"teleop\" matches \"teleop_node\").\n\n# Run an executable\npython3 {baseDir}/scripts/ros2_cli.py run new lekiwi_control teleop\n\n# Run with arguments\npython3 {baseDir}/scripts/ros2_cli.py run new lekiwi_control teleop --arg1 value\n\n# Run with parameters\npython3 {baseDir}/scripts/ros2_cli.py run new lekiwi_control teleop --params \"speed:1.0\"\n\n# Run with presets\npython3 {baseDir}/scripts/ros2_cli.py run new lekiwi_control teleop --presets indoor\n\n# Run with config path\npython3 {baseDir}/scripts/ros2_cli.py run new lekiwi_control teleop --config-path /path/to/config"
      },
      {
        "title": "List Running Executables",
        "body": "python3 {baseDir}/scripts/ros2_cli.py run list"
      },
      {
        "title": "Kill an Executable Session",
        "body": "python3 {baseDir}/scripts/ros2_cli.py run kill run_lekiwi_control_teleop"
      },
      {
        "title": "Restart an Executable Session",
        "body": "python3 {baseDir}/scripts/ros2_cli.py run restart run_lekiwi_control_teleop"
      },
      {
        "title": "Run Session Collision Handling",
        "body": "Same as launch - if a session with the same name already exists, the command will fail with an error. Use run restart or run kill first."
      },
      {
        "title": "1. Explore a Robot System",
        "body": "python3 {baseDir}/scripts/ros2_cli.py version\npython3 {baseDir}/scripts/ros2_cli.py topics list\npython3 {baseDir}/scripts/ros2_cli.py nodes list\npython3 {baseDir}/scripts/ros2_cli.py services list\npython3 {baseDir}/scripts/ros2_cli.py actions list\n\n# Find a topic by type, then inspect it\n# (replace the type with whatever you're looking for)\npython3 {baseDir}/scripts/ros2_cli.py topics find geometry_msgs/msg/Twist\npython3 {baseDir}/scripts/ros2_cli.py topics find geometry_msgs/msg/TwistStamped\n# → get the discovered topic name, then:\npython3 {baseDir}/scripts/ros2_cli.py topics type <discovered_topic>\npython3 {baseDir}/scripts/ros2_cli.py interface proto <confirmed_type>"
      },
      {
        "title": "2. Move a Robot",
        "body": "Follow the Movement Workflow section. It covers all cases: distance commands, rotation commands, open-ended movement, and the no-odometry fallback. Always discover topics first — never assume names.\n\nEmergency stop:\n\npython3 {baseDir}/scripts/ros2_cli.py estop"
      },
      {
        "title": "3. Read Sensor Data",
        "body": "Always use auto-discovery first to find the correct sensor topics.\n\n# Step 1: Discover sensor topics by message type\npython3 {baseDir}/scripts/ros2_cli.py topics find sensor_msgs/msg/LaserScan\npython3 {baseDir}/scripts/ros2_cli.py topics find nav_msgs/msg/Odometry\npython3 {baseDir}/scripts/ros2_cli.py topics find sensor_msgs/msg/JointState\n# → record each discovered topic name\n\n# Step 2: Subscribe to discovered topics (use the names from Step 1, not hardcoded names)\npython3 {baseDir}/scripts/ros2_cli.py topics subscribe <LASER_TOPIC> --duration 3\npython3 {baseDir}/scripts/ros2_cli.py topics subscribe <ODOM_TOPIC> --duration 10 --max-messages 50\npython3 {baseDir}/scripts/ros2_cli.py topics subscribe <JOINT_STATE_TOPIC> --duration 5"
      },
      {
        "title": "4. Use Services",
        "body": "Always use auto-discovery first to find available services and their request/response structures.\n\n# Step 1: Discover available services\npython3 {baseDir}/scripts/ros2_cli.py services list\n\n# Step 2: Find services by type (optional)\npython3 {baseDir}/scripts/ros2_cli.py services find std_srvs/srv/Empty\npython3 {baseDir}/scripts/ros2_cli.py services find turtlesim/srv/Spawn\n\n# Step 3: Get service request/response structure\npython3 {baseDir}/scripts/ros2_cli.py services details /spawn\n\n# Step 4: Call the service with properly-structured payload\npython3 {baseDir}/scripts/ros2_cli.py services call /spawn \\\n  '{\"x\":3.0,\"y\":3.0,\"theta\":0.0,\"name\":\"turtle2\"}'"
      },
      {
        "title": "5. Actions",
        "body": "Always use auto-discovery first to find available actions and their goal/result structures.\n\n# Step 1: Discover available actions\npython3 {baseDir}/scripts/ros2_cli.py actions list\n\n# Step 2: Find actions by type (optional)\npython3 {baseDir}/scripts/ros2_cli.py actions find turtlesim/action/RotateAbsolute\npython3 {baseDir}/scripts/ros2_cli.py actions find nav2_msgs/action/NavigateToPose\n\n# Step 3: Get action goal/result structure\npython3 {baseDir}/scripts/ros2_cli.py actions details /turtle1/rotate_absolute\n\n# Step 4: Send goal with properly-structured payload\npython3 {baseDir}/scripts/ros2_cli.py actions send /turtle1/rotate_absolute \\\n  '{\"theta\":1.57}'\n\n# Step 5: Monitor feedback — always echo after sending a goal\npython3 {baseDir}/scripts/ros2_cli.py actions echo /turtle1/rotate_absolute --timeout 30\n\nAfter every actions send, immediately run actions echo on the same action server to monitor feedback. A stuck or rejected goal gives no signal without feedback monitoring. If actions echo returns no messages within --timeout, the goal may have been rejected, preempted, or the action server may have crashed — check with actions list and actions details, then decide between actions cancel (graceful abort) and estop (runaway/unsafe motion). See the Action Preemption table in the Error Recovery section."
      },
      {
        "title": "6. Change Parameters",
        "body": "Always use auto-discovery first to list available parameters for a node.\n\n# Step 1: Discover nodes\npython3 {baseDir}/scripts/ros2_cli.py nodes list\n\n# Step 2: List parameters for a node\npython3 {baseDir}/scripts/ros2_cli.py params list /turtlesim\n\n# Step 3: Get current parameter value\npython3 {baseDir}/scripts/ros2_cli.py params get /turtlesim:background_r\n\n# Step 4: Set parameter value\npython3 {baseDir}/scripts/ros2_cli.py params set /turtlesim:background_r 255\npython3 {baseDir}/scripts/ros2_cli.py params set /turtlesim:background_g 0\npython3 {baseDir}/scripts/ros2_cli.py params set /turtlesim:background_b 0\n\n# Step 5: Read back after set — always verify the change took effect\n# Some nodes silently reject changes (read-only params, out-of-range values)\npython3 {baseDir}/scripts/ros2_cli.py params get /turtlesim:background_r\npython3 {baseDir}/scripts/ros2_cli.py params get /turtlesim:background_g\npython3 {baseDir}/scripts/ros2_cli.py params get /turtlesim:background_b\n\nAfter every params set, always run params get on the same parameter to confirm the change was accepted. Nodes may silently ignore a set if the parameter is read-only or the value is out of range — the set call returns success even in those cases."
      },
      {
        "title": "7. Goal-Oriented Commands (publish-until)",
        "body": "For any goal with a sensor-based stop condition — joint angles, temperature limits, proximity, battery level — use publish-until with the appropriate monitor topic and condition flag. Always discover both the command topic and the monitor topic before executing — never hardcode names.\n\nUser intentDiscover command topicDiscover monitor topicConditionStop near obstacletopics find geometry_msgs/Twist + TwistStampedtopics find sensor_msgs/LaserScan → field ranges.0--below 0.5Stop at rangesametopics find sensor_msgs/Range → field range--below DStop at temperature—topics find sensor_msgs/Temperature → field temperature--above TStop at battery level—topics find sensor_msgs/BatteryState → field percentage--below PJoint reach angletopics find trajectory_msgs/JointTrajectory or similartopics find sensor_msgs/JointState → field position.0--equals A or --delta DMulti-joint distancesametopics find sensor_msgs/JointState → fields position.0 position.1--euclidean --delta D\n\n--euclidean computes sqrt(Σ(current_i - start_i)²) across all --field paths. Use it for curved or diagonal paths. Composite field shorthand: --field pose.pose.position auto-expands to x, y, z — equivalent to listing all three fields explicitly.\n\n# Example: stop when front range sensor reads < 0.5 m\n# Step 1: discover velocity topic\npython3 {baseDir}/scripts/ros2_cli.py topics find geometry_msgs/msg/Twist\npython3 {baseDir}/scripts/ros2_cli.py topics find geometry_msgs/msg/TwistStamped\n# → VEL_TOPIC = (result, e.g. /base/cmd_vel)\npython3 {baseDir}/scripts/ros2_cli.py topics type <VEL_TOPIC>\n# → confirms type, determines payload structure\n\n# Step 2: discover laser scan topic\npython3 {baseDir}/scripts/ros2_cli.py topics find sensor_msgs/msg/LaserScan\n# → SCAN_TOPIC = (result, e.g. /scan or /lidar/scan)\n\n# Step 3: execute with discovered names\npython3 {baseDir}/scripts/ros2_cli.py topics publish-until <VEL_TOPIC> \\\n  '<payload_matching_confirmed_type>' \\\n  --monitor <SCAN_TOPIC> --field ranges.0 --below 0.5 --timeout 30\n\n# Example: move a joint until it reaches 1.5 rad\n# Step 1: discover joint command topic\npython3 {baseDir}/scripts/ros2_cli.py topics find trajectory_msgs/msg/JointTrajectory\n# → or check nodes details for the arm controller node\n# → JOINT_CMD_TOPIC = (result)\n\n# Step 2: discover joint state topic\npython3 {baseDir}/scripts/ros2_cli.py topics find sensor_msgs/msg/JointState\n# → JOINT_STATE_TOPIC = (result)\n\n# Step 3: execute with discovered names\npython3 {baseDir}/scripts/ros2_cli.py topics publish-until <JOINT_CMD_TOPIC> \\\n  '{\"data\":0.5}' \\\n  --monitor <JOINT_STATE_TOPIC> --field position.0 --equals 1.5 --timeout 10\n\n<a name=\"movement-workflow-canonical\"></a>"
      },
      {
        "title": "Movement Workflow (canonical)",
        "body": "This is the single authoritative workflow for all robot movement. Rule 3 in the Agent Behaviour Rules above is a summary of this section. Always follow these steps in order.\n\nIn every example below, <VEL_TOPIC>, <ODOM_TOPIC>, and <PAYLOAD> are placeholders for values you discover in Steps 1–2. Never substitute /cmd_vel, /odom, or any other assumed name."
      },
      {
        "title": "Step 1 — Discover the velocity command topic and confirm its exact type",
        "body": "Run both searches in parallel:\n\npython3 {baseDir}/scripts/ros2_cli.py topics find geometry_msgs/msg/Twist\npython3 {baseDir}/scripts/ros2_cli.py topics find geometry_msgs/msg/TwistStamped\n\nTake the result — this is VEL_TOPIC. Then confirm the exact type:\n\npython3 {baseDir}/scripts/ros2_cli.py topics type <VEL_TOPIC>\n\nThe confirmed type determines the payload:\n\ngeometry_msgs/msg/Twist: {\"linear\":{\"x\":0.2,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}\ngeometry_msgs/msg/TwistStamped: {\"header\":{\"stamp\":{\"sec\":0},\"frame_id\":\"\"},\"twist\":{\"linear\":{\"x\":0.2,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}}\nZero/stop — Twist: {\"linear\":{\"x\":0,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}\nZero/stop — TwistStamped: {\"header\":{\"stamp\":{\"sec\":0},\"frame_id\":\"\"},\"twist\":{\"linear\":{\"x\":0,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}}\n\nIf both find commands return results, run topics type on each to get the definitive type — do not guess from the topic name. Then choose using this priority order:\n\nPrefer the topic with an active subscriber (topics details <topic> → subscriber_count > 0) — a subscribed topic means a controller is listening.\nIf both have subscribers, prefer the topic whose name contains cmd_vel or is at the root namespace (e.g. /cmd_vel over /robot/cmd_vel_stamped).\nIf still ambiguous, prefer TwistStamped over Twist (more modern standard).\nUse the selected topic as VEL_TOPIC for the rest of the workflow."
      },
      {
        "title": "Step 2 — Discover the odometry topic",
        "body": "python3 {baseDir}/scripts/ros2_cli.py topics find nav_msgs/msg/Odometry\n\nTake the result — this is ODOM_TOPIC."
      },
      {
        "title": "Step 2.5 — Verify topics are live, read safety limits, and capture start position",
        "body": "Before executing, verify both topics are active, discover velocity limits from controller parameters, and record the starting position.\n\nCheck if the robot is already in motion — do not command a moving robot:\n\npython3 {baseDir}/scripts/ros2_cli.py topics subscribe <ODOM_TOPIC> --max-messages 1 --timeout 3\n\nInspect the twist.twist.linear and twist.twist.angular fields. If any value is significantly non-zero (> 0.01 m/s or rad/s), the robot is already moving. Do not publish new velocity commands — stop it first with estop, wait for motion to cease, then proceed.\n\nVerify the odometry publish rate is adequate for closed-loop control:\n\npython3 {baseDir}/scripts/ros2_cli.py topics hz <ODOM_TOPIC>\n\nThe rate must be at least 10 Hz for reliable closed-loop control. If the rate is below 5 Hz: fall back to Case D (open-loop) and warn the user that odometry is too slow for safe closed-loop operation.\n\nDiscover velocity limits from ALL nodes (full-graph scan):\n\nVelocity limits can be declared on any node — not just nodes whose names suggest \"controller\". Scan every node:\n\npython3 {baseDir}/scripts/ros2_cli.py nodes list\n# → iterate over every node and run params list on each\npython3 {baseDir}/scripts/ros2_cli.py params list <NODE>\n# → filter parameter names containing: max, limit, vel, speed, accel (case-insensitive)\npython3 {baseDir}/scripts/ros2_cli.py params get <NODE>:<candidate_param>\n# → repeat for every candidate across every node\n\nCompute the binding ceiling:\n\nlinear_ceiling  = minimum of all discovered linear-axis limit values\nangular_ceiling = minimum of all discovered angular/theta limit values\n\nCap all commanded velocities at these ceilings. If no limits are found on any node, use conservative defaults (0.2 m/s linear, 0.75 rad/s angular) and note this.\n\nVerify the velocity topic has active subscribers (something must be listening — i.e., a controller):\n\npython3 {baseDir}/scripts/ros2_cli.py topics details <VEL_TOPIC>\n\nCheck the output for subscriber_count > 0. If no subscribers: the controller may not be running. Check control list-controllers and activate the correct controller before proceeding.\n\nCheck for emergency stop / safety interlock state:\n\npython3 {baseDir}/scripts/ros2_cli.py control list-controllers\n\nVerify that the motion controller is in active state. If it is inactive, unconfigured, or missing: do not publish velocity — activate it first or notify the user that the controller is not ready.\n\nVerify the odometry topic has an active publisher (not stale):\n\npython3 {baseDir}/scripts/ros2_cli.py topics details <ODOM_TOPIC>\n\nCheck publisher_count > 0. Then confirm it is actively publishing by subscribing briefly:\n\npython3 {baseDir}/scripts/ros2_cli.py topics subscribe <ODOM_TOPIC> --max-messages 1 --timeout 3\n\nIf no message arrives within the timeout: the odometry source is stale or not running. Fall back to Case D (open-loop) and notify the user.\n\nCapture start position (for distance reporting after motion):\n\npython3 {baseDir}/scripts/ros2_cli.py topics subscribe <ODOM_TOPIC> --max-messages 1\n\nRecord pose.pose.position.x, pose.pose.position.y from the result — this is the start pose."
      },
      {
        "title": "Step 3 — Execute based on intent and odometry availability",
        "body": "Case A — Distance specified, odometry available → publish-until --euclidean (closed loop)\n\nAlways use --euclidean --field pose.pose.position (expands to x, y, z) to measure Euclidean distance from start. This is frame-independent: it works correctly regardless of the robot's heading and stays accurate after any prior rotation. Do not use --field pose.pose.position.x alone — that only tracks displacement along the odom x-axis and gives wrong results once the robot is not aligned with it.\n\n# Step 1 result: VEL_TOPIC = <discovered, e.g. /base/cmd_vel or /robot/cmd_vel>\n# Step 1 type check: geometry_msgs/msg/Twist → use Twist payload\n# Step 2 result: ODOM_TOPIC = <discovered, e.g. /odom or /wheel_odom>\n\n# Move forward 1 meter (Twist) — Euclidean distance, frame-independent\npython3 {baseDir}/scripts/ros2_cli.py topics publish-until <VEL_TOPIC> \\\n  '{\"linear\":{\"x\":0.2,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}' \\\n  --monitor <ODOM_TOPIC> --euclidean --field pose.pose.position --delta 1.0 --timeout 30\n\n# Move forward 1 meter — TwistStamped variant (if type check returned TwistStamped)\npython3 {baseDir}/scripts/ros2_cli.py topics publish-until <VEL_TOPIC> \\\n  '{\"header\":{\"stamp\":{\"sec\":0},\"frame_id\":\"\"},\"twist\":{\"linear\":{\"x\":0.2,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}}' \\\n  --monitor <ODOM_TOPIC> --euclidean --field pose.pose.position --delta 1.0 --timeout 30\n\n# Move backward 0.5 meters (Euclidean — delta is always positive; direction is set by the velocity sign)\npython3 {baseDir}/scripts/ros2_cli.py topics publish-until <VEL_TOPIC> \\\n  '{\"linear\":{\"x\":-0.2,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}' \\\n  --monitor <ODOM_TOPIC> --euclidean --field pose.pose.position --delta 0.5 --timeout 30\n\n# Move 2 meters along any curved path\npython3 {baseDir}/scripts/ros2_cli.py topics publish-until <VEL_TOPIC> \\\n  '{\"linear\":{\"x\":0.2,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0.3}}' \\\n  --monitor <ODOM_TOPIC> --euclidean --field pose.pose.position --delta 2.0 --timeout 60\n\nNote on --delta sign with --euclidean: Euclidean distance is always non-negative, so --delta should always be a positive value — the direction of travel is determined entirely by the velocity command (sign of linear.x), not by the sign of --delta.\n\nObstacle avoidance during forward movement: publish-until supports one --monitor topic. If you need to stop before an obstacle (not at a fixed distance), use the LaserScan topic as the monitor instead of odometry:\n\n# Discover the LaserScan topic\npython3 {baseDir}/scripts/ros2_cli.py topics find sensor_msgs/msg/LaserScan\n# → SCAN_TOPIC = <result>\n\n# Move forward, stop when front range < 0.5 m\npython3 {baseDir}/scripts/ros2_cli.py topics publish-until <VEL_TOPIC> \\\n  '{\"linear\":{\"x\":0.2,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}' \\\n  --monitor <SCAN_TOPIC> --field ranges.0 --below 0.5 --timeout 30\n\nLimitation: A single publish-until call can only monitor one topic — you cannot simultaneously stop at a fixed distance AND stop on obstacle detection. If both are needed, set a conservative --timeout as an outer bound and use the most safety-critical condition as the --monitor. For precise distance, use odometry; for collision avoidance, use LaserScan.\n\nCase B — Rotation specified, odometry available → publish-until --rotate (closed loop)\n\n--rotate automatically extracts yaw from the odometry quaternion and handles angle wraparound. There is no --yaw flag. Do not use --field for rotation.\n\nDirection convention — --rotate sign and angular.z sign must always agree:\n\nDirection--rotateangular.zLeft / CCWpositive (e.g. --rotate 90 --degrees)positive (e.g. 0.5)Right / CWnegative (e.g. --rotate -90 --degrees)negative (e.g. -0.5)\n\n--rotate tells the monitor how far and in which direction to track. angular.z tells the robot which way to spin. They must match — a positive --rotate with negative angular.z means the robot turns CW while the monitor waits for CCW accumulation and will never stop.\n\nBefore constructing any rotation command, run --help to confirm accepted flags:\n\npython3 {baseDir}/scripts/ros2_cli.py topics publish-until --help\n\nDo not proceed until you have verified the flag names from the --help output. Never invent or assume flags.\n\n# Step 1 result: VEL_TOPIC = <discovered>\n# Step 2 result: ODOM_TOPIC = <discovered>\n\n# Rotate 90 degrees counter-clockwise — positive --rotate, positive angular.z (Twist)\npython3 {baseDir}/scripts/ros2_cli.py topics publish-until <VEL_TOPIC> \\\n  '{\"linear\":{\"x\":0,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0.5}}' \\\n  --monitor <ODOM_TOPIC> --rotate 90 --degrees --timeout 30\n\n# Rotate 45 degrees clockwise — negative --rotate, negative angular.z (Twist)\npython3 {baseDir}/scripts/ros2_cli.py topics publish-until <VEL_TOPIC> \\\n  '{\"linear\":{\"x\":0,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":-0.5}}' \\\n  --monitor <ODOM_TOPIC> --rotate -45 --degrees --timeout 30\n\n# Rotate 1.57 radians CCW (TwistStamped variant)\npython3 {baseDir}/scripts/ros2_cli.py topics publish-until <VEL_TOPIC> \\\n  '{\"header\":{\"stamp\":{\"sec\":0},\"frame_id\":\"\"},\"twist\":{\"linear\":{\"x\":0,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0.5}}}' \\\n  --monitor <ODOM_TOPIC> --rotate 1.5708 --timeout 30\n\nCase C — Open-ended movement (no distance/angle) → publish-sequence\n\nAlways include a zero-velocity stop as the final message. Use the payload structure matching the confirmed type from Step 1.\n\n# Step 1 result: VEL_TOPIC = <discovered>\n# Type check: geometry_msgs/msg/Twist → Twist payload\n\n# Drive forward for 3 seconds then stop (Twist)\npython3 {baseDir}/scripts/ros2_cli.py topics publish-sequence <VEL_TOPIC> \\\n  '[{\"linear\":{\"x\":0.2,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}},{\"linear\":{\"x\":0,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}]' \\\n  '[3.0, 0.5]'\n\n# Drive forward for 3 seconds then stop (TwistStamped variant)\npython3 {baseDir}/scripts/ros2_cli.py topics publish-sequence <VEL_TOPIC> \\\n  '[{\"header\":{\"stamp\":{\"sec\":0},\"frame_id\":\"\"},\"twist\":{\"linear\":{\"x\":0.2,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}},{\"header\":{\"stamp\":{\"sec\":0},\"frame_id\":\"\"},\"twist\":{\"linear\":{\"x\":0,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}}]' \\\n  '[3.0, 0.5]'\n\nCase D — Distance or angle specified, but odometry NOT available → publish-sequence (open loop)\n\nTell the user before executing: \"No odometry topic found. Running open-loop — distance/angle accuracy is not guaranteed.\" Then estimate duration = distance / velocity and use publish-sequence with a stop.\n\n# Step 1 result: VEL_TOPIC = <discovered>\n# Step 2 result: no odometry found\n\n# Estimate: 1 m at 0.2 m/s ≈ 5 s (Twist)\npython3 {baseDir}/scripts/ros2_cli.py topics publish-sequence <VEL_TOPIC> \\\n  '[{\"linear\":{\"x\":0.2,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}},{\"linear\":{\"x\":0,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}]' \\\n  '[5.0, 0.5]'\n\n# Estimate: 1 m at 0.2 m/s ≈ 5 s (TwistStamped variant)\npython3 {baseDir}/scripts/ros2_cli.py topics publish-sequence <VEL_TOPIC> \\\n  '[{\"header\":{\"stamp\":{\"sec\":0},\"frame_id\":\"\"},\"twist\":{\"linear\":{\"x\":0.2,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}},{\"header\":{\"stamp\":{\"sec\":0},\"frame_id\":\"\"},\"twist\":{\"linear\":{\"x\":0,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}}]' \\\n  '[5.0, 0.5]'"
      },
      {
        "title": "Step 4 — Report completion",
        "body": "After publish-until or publish-sequence completes, capture the end position and report to the user:\n\npython3 {baseDir}/scripts/ros2_cli.py topics subscribe <ODOM_TOPIC> --max-messages 1\n\nCompare with the start pose from Step 2.5. By default, report only: \"Done. Moved X.XX m.\" (or \"Done. Rotated X deg.\") and any errors or anomalies. Report detailed topic selections, odometry values, and per-step data only if the user explicitly asked for verbose output."
      },
      {
        "title": "1. Explore a Robot System",
        "body": "python3 {baseDir}/scripts/ros2_cli.py version\npython3 {baseDir}/scripts/ros2_cli.py topics list\npython3 {baseDir}/scripts/ros2_cli.py nodes list\npython3 {baseDir}/scripts/ros2_cli.py services list"
      },
      {
        "title": "2. Move a Robot",
        "body": "See the Movement Workflow section — it covers all cases with complete examples."
      },
      {
        "title": "3. Read Sensors",
        "body": "python3 {baseDir}/scripts/ros2_cli.py topics find sensor_msgs/msg/LaserScan\n# → subscribe to the discovered topic name, not /scan\npython3 {baseDir}/scripts/ros2_cli.py topics subscribe <LASER_TOPIC> --duration 3"
      },
      {
        "title": "4. Call a Service",
        "body": "# Step 1: Discover available services\npython3 {baseDir}/scripts/ros2_cli.py services list\n\n# Step 2: Get request/response structure for the service you want to call\npython3 {baseDir}/scripts/ros2_cli.py services details <DISCOVERED_SERVICE>\n\n# Step 3: Call with properly-structured payload\npython3 {baseDir}/scripts/ros2_cli.py services call <DISCOVERED_SERVICE> '<json_payload>'"
      },
      {
        "title": "5. Send an Action",
        "body": "# Step 1: Discover available actions\npython3 {baseDir}/scripts/ros2_cli.py actions list\n\n# Step 2: Get goal/result structure for the action you want to send\npython3 {baseDir}/scripts/ros2_cli.py actions details <DISCOVERED_ACTION>\n\n# Step 3: Send goal with properly-structured payload\npython3 {baseDir}/scripts/ros2_cli.py actions send <DISCOVERED_ACTION> '<json_goal>'"
      },
      {
        "title": "6. Move Forward N Meters / Rotate N Degrees",
        "body": "See the Movement Workflow section — Cases A and B cover distance and rotation with full examples."
      },
      {
        "title": "Safety Notes",
        "body": "Destructive commands (can move the robot or change state):\n\ntopics publish / topics publish-sequence — sends movement or control commands\ntopics publish-until — publishes continuously until a condition or timeout; always specify a conservative --timeout\ntopics publish-continuous — alias for topics publish; --duration / --timeout is optional (single-shot without it)\nservices call — can reset, spawn, kill, or change robot state\nparams set — modifies runtime parameters\nactions send — triggers robot actions (rotation, navigation, etc.)\ncontrol set-controller-state / control switch-controllers — activating or deactivating controllers affects what the robot can do\ncontrol set-hardware-component-state — driving hardware through lifecycle states can stop actuators or sensors\ncontrol reload-controller-libraries — stops all running controllers if --force-kill is used\n\nAlways stop the robot after movement. The last message in any publish-sequence should be all zeros, using the payload structure that matches the confirmed type (Twist or TwistStamped — from your topics type introspection step):\n\n// Twist stop\n{\"linear\":{\"x\":0,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}\n// TwistStamped stop\n{\"header\":{\"stamp\":{\"sec\":0},\"frame_id\":\"\"},\"twist\":{\"linear\":{\"x\":0,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}}\n\nAlways check JSON output for errors before proceeding.\n\nUse auto-discovery to avoid errors. Before any publish/call/send operation:\n\nUse topics find, services find, actions find to locate relevant endpoints\nUse topics type, services type, actions type to get message types\nUse topics message, services details, actions details to understand field structures\n\nNever ask the user for topic names or message types — discover them from the live ROS 2 graph."
      },
      {
        "title": "Agent Self-Correction Rules",
        "body": "If Agent Asks...Correction\"What topic should I use?\"Use topics find geometry_msgs/Twist (and TwistStamped)\"What message type?\"Use topics type <topic> to get it from the graph\"How do I know the message structure?\"Use topics message <type> or interface show <type>\"What are the velocity limits?\"Use params list on controller nodes\"Do I need odometry?\"Always check with topics find nav_msgs/Odometry first\"Is there a service called /X?\"Use services list to verify it exists\"How do I monitor yaw / heading?\"Use --rotate N --degrees (or radians). There is no --yaw or --yaw-delta flag. --rotate handles all yaw tracking internally.\"Should I use --field for rotation?\"No. Never use --field for rotation. Use --rotate.\"How do I rotate clockwise / right?\"Use --rotate -90 --degrees (negative angle) with angular.z: -0.5 (negative velocity). Sign of --rotate and angular.z must always match.\"Can --rotate be negative?\"Yes. Negative = CW. Positive = CCW. Zero is the only invalid value.\"What controller parameters exist?\"Use params list <controller_node>"
      },
      {
        "title": "Technical Troubleshooting",
        "body": "ProblemCauseSolutionMissing ROS 2 dependency: No module named 'X'A required ROS 2 package is not installedSource ROS 2: source /opt/ros/${ROS_DISTRO}/setup.bash; then install: sudo apt install ros-${ROS_DISTRO}-<package>rclpy not installedrclpy missing or wrong Python versionSource ROS 2 setup.bash; if Python version mismatch, run with python3.12 instead of python3ROS 2 not sourcedEnvironment not set upRun: source /opt/ros/${ROS_DISTRO}/setup.bashNo topics foundROS nodes not runningEnsure nodes are launched and workspace is sourcedService not foundService not availableUse services list to see available servicesParameter commands failNode doesn't have parametersSome nodes don't expose parametersAction commands failAction server not availableUse actions list to see available actionsInvalid JSON errorMalformed messageValidate JSON before passing (watch for single vs double quotes)Subscribe timeoutNo publisher on topicCheck topics details to verify publishers existpublish-sequence length errorArray mismatchmessages and durations arrays must have the same lengthpublish-until hangs / no feedbackWrong monitor topic or fieldUse Step 2–4 of the Goal-Oriented workflow to verify topic and fieldController manager service not availableros2_control not running or wrong namespaceCheck with nodes list; use --controller-manager to set the correct namespace"
      }
    ],
    "body": "ROS 2 Skill\n\nControls and monitors ROS 2 robots directly via rclpy.\n\nArchitecture: Agent → ros2_cli.py → rclpy → ROS 2\n\nAll commands output JSON. Errors contain {\"error\": \"...\"}.\n\nFor full command reference with arguments, options, and output examples, see references/COMMANDS.md.\n\nAgent Behaviour Rules\n\nThese rules are absolute and apply to every request involving a ROS 2 robot.\n\nRule 0 — Full introspection before every action (non-negotiable)\n\nBefore publishing to any topic, calling any service, or sending any action goal, you MUST complete the introspection steps below. There are no exceptions, not even for \"obvious\" or \"conventional\" names.\n\nThis rule exists because:\n\nThe velocity topic is not always /cmd_vel. It may be /base/cmd_vel, /robot/cmd_vel, /mobile_base/cmd_vel, or anything else.\nThe message type is not always Twist. Many robots use TwistStamped, and the payload structure differs.\nThe odometry topic is not always /odom. It may be /wheel_odom, /robot/odom, /base/odometry, etc.\nConvention-based guessing causes silent failures, wrong topics, and physical accidents.\n\nPre-flight introspection protocol — run ALL applicable steps before acting:\n\nAction type\tRequired introspection\nPublish to a topic\t1. topics find <msg_type> to discover the real topic name<br>2. topics type <discovered_topic> to confirm the exact type<br>3. interface proto <exact_type> to get the default payload template\nCall a service\t1. services list or services find <srv_type> to discover the real name<br>2. services details <discovered_service> to get request/response fields\nSend an action goal\t1. actions list or actions find <action_type> to discover the real name<br>2. actions details <discovered_action> to get goal/result/feedback fields\nMove a robot\tFull Movement Workflow — see Rule 3 and the canonical section below\nRead a sensor\ttopics find <msg_type> to discover the topic; never subscribe to a hardcoded name\nAny operation involving a node\tnodes list first; never assume a node name\n\nParameter introspection is mandatory before any movement command. Velocity limits can live on any node — not just nodes with \"controller\" in the name. Before publishing velocity:\n\nRun nodes list to get every node currently running\nRun params list <NODE> on every single node in the list (run in parallel batches if there are many)\nFor each node, look for any parameter whose name contains max, limit, vel, speed, or accel (case-insensitive). These are candidates for velocity limits.\nRun params get <NODE>:<param> for every candidate found across all nodes\nIdentify the binding ceiling: the minimum across all discovered linear limit values and the minimum across all discovered angular/theta limit values\nCap your commanded velocity at that ceiling. If no limits are found on any node, use conservative defaults (0.2 m/s linear, 0.75 rad/s angular) and note this to the user.\n\nNever hardcode or assume:\n\n❌ Never use /cmd_vel without first discovering the velocity topic with topics find\n❌ Never use Twist payload without first confirming the type is not TwistStamped via topics type\n❌ Never use /odom without first discovering the odometry topic with topics find\n❌ Never use --yaw, --yaw-delta, or --field for rotation — the only correct flag is --rotate N --degrees (or --rotate N for radians). Use negative N for CW; --rotate sign and angular.z sign must always match.\n❌ Never assume any topic, service, action, or node name from ROS 2 convention\n❌ Never assume a message type from a topic name\n\nIntrospection commands return discovered names. Use those names — not the ones you expect.\n\nRule 0.1 — Session-start checks (run once per session, before any task)\n\nBefore executing any user command in a new session, run these checks. They take seconds and catch the most common causes of silent failure.\n\nStep 1 — Run a health check:\n\npython3 {baseDir}/scripts/ros2_cli.py doctor\n\n\nIf the doctor reports critical failures (DDS issues, missing packages, no nodes), stop and tell the user. Do not attempt to operate a robot that fails its health check.\n\nStep 2 — Check for simulated time:\n\npython3 {baseDir}/scripts/ros2_cli.py topics find rosgraph_msgs/msg/Clock\n\n\nIf /clock is found, simulated time is in use. Verify it is actively publishing before issuing any timed command:\n\npython3 {baseDir}/scripts/ros2_cli.py topics subscribe /clock --max-messages 1 --timeout 3\n\n\nIf no message arrives: the simulator is paused or crashed — do not proceed with time-sensitive operations.\n\nStep 3 — Check lifecycle nodes (if any):\n\npython3 {baseDir}/scripts/ros2_cli.py lifecycle nodes\n\n\nIf lifecycle-managed nodes exist, check their states:\n\npython3 {baseDir}/scripts/ros2_cli.py lifecycle get <node>\n\n\nA node in unconfigured or inactive state will silently fail when its topics or services are used. Activate required nodes before proceeding.\n\nThese checks are session-level. Do not re-run for every command. Re-run only if the user relaunches the robot or if nodes appear/disappear unexpectedly.\n\nRule 0.5 — Never hallucinate commands, flags, or names\n\nIf you are not certain a command, flag, topic name, or argument exists — verify it before using it. Do not guess.\n\nThe failure mode to avoid: inventing a flag like --yaw-delta or --rotate-degrees because it sounds plausible, then failing and asking the user for help. That is the worst possible outcome — the error was self-inflicted and the user had nothing to do with it.\n\nThe verification chain:\n\nCheck this skill first. The full command reference is in references/COMMANDS.md. If a flag or command is not listed there, it does not exist.\nIf still unsure, run --help on the exact subcommand before constructing the call. Every subcommand supports --help and prints its accepted flags without requiring a live ROS 2 graph. This is mandatory, not optional.\npython3 {baseDir}/scripts/ros2_cli.py topics publish-until --help\npython3 {baseDir}/scripts/ros2_cli.py topics publish-sequence --help\npython3 {baseDir}/scripts/ros2_cli.py actions send --help\n# etc. — use the exact subcommand you are about to call\n\nIf still stuck after checking both, ask the user. This is the only acceptable reason to ask — not because you assumed something and it failed.\n\n--help requires ROS 2 to be sourced. Running --help before ROS 2 is sourced will return a JSON error instead of help text. This is not a concern in practice — ROS 2 must be sourced before any robot operation (it is a hard precondition of this skill), so --help will always be available during normal use. If you see a Missing ROS 2 dependency error from --help, fix the ROS 2 environment first (see Setup section and Rule 0.1).\n\nNever:\n\nInvent a flag and try it, then report failure to the user\nAssume a capability exists because it would be logical or convenient\nAsk the user to resolve an error you caused by guessing\nRule 1 — Discover before you act, never ask\n\nNever ask the user for names, types, or IDs that can be discovered from the live system. This includes topic names, service names, action names, node names, parameter names, message types, and controller names. Always query the robot first.\n\nWhat you need\tHow to discover it\nTopic name\ttopics list or topics find <msg_type>\nTopic message type\ttopics type <topic>\nService name\tservices list or services find <srv_type>\nService request/response fields\tservices details <service>\nAction server name\tactions list or actions find <action_type>\nAction goal/result/feedback fields\tactions details <action>\nNode name\tnodes list\nNode's topics, services, actions\tnodes details <node>\nParameter names on a node\tparams list <node>\nParameter value\tparams get <node:param>\nParameter type and constraints\tparams describe <node:param>\nController names and states\tcontrol list-controllers\nHardware components\tcontrol list-hardware-components\nMessage / service / action type fields\tinterface show <type> or interface proto <type>\n\nOnly ask the user if:\n\nThe discovery command returns an empty result or an error, and\nThere is genuinely no other way to determine the information from the live system.\nRule 2 — Use ros2-skill before saying you can't\n\nNever tell the user you don't know how to do something with a ROS 2 robot without first checking whether ros2-skill has a command for it. This skill covers the full range of ROS 2 operations: topics, services, actions, parameters, nodes, lifecycle, controllers, diagnostics, battery, images, interfaces, presets, and more.\n\nWhen a task seems unfamiliar, look it up in the quick reference tables below before responding. Common operations that agents sometimes miss:\n\nTask\tros2-skill command\nCapture a camera image\ttopics capture-image --topic <topic> --output <file>\nRead laser / camera / IMU / odom data\ttopics subscribe <topic>\nCall a ROS 2 service\tservices call <service> <json>\nSend a navigation or manipulation goal\tactions send <action> <json>\nChange a node parameter at runtime\tparams set <node:param> <value>\nSave/restore a parameter configuration\tparams preset-save / params preset-load\nActivate or deactivate a controller\tcontrol set-controller-state <name> active|inactive\nRun a health check\tdoctor\nEmergency stop\testop\nCheck diagnostics\ttopics diag\nCheck battery\ttopics battery\n\nIf you genuinely cannot find a matching command after checking both the quick reference and the COMMANDS.md reference, say so clearly and explain what you checked — do not silently guess or use a partial solution.\n\nRule 3 — Movement algorithm (always follow this sequence)\n\nFor any user request involving movement — regardless of whether a distance or angle is specified — follow this algorithm exactly. Do not skip steps. Do not ask the user anything that can be resolved by running a command.\n\nStep 1 — Discover the velocity command topic and confirm its exact type\n\nRun both searches in parallel:\n\ntopics find geometry_msgs/msg/Twist\ntopics find geometry_msgs/msg/TwistStamped\n\n\nRecord the discovered topic name — call it VEL_TOPIC. Then confirm the exact type:\n\ntopics type <VEL_TOPIC>\n\n\nUse the confirmed type to choose the payload structure:\n\ngeometry_msgs/msg/Twist: {\"linear\":{\"x\":0.2,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}\ngeometry_msgs/msg/TwistStamped: {\"header\":{\"stamp\":{\"sec\":0},\"frame_id\":\"\"},\"twist\":{\"linear\":{\"x\":0.2,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}}\n\nIf both find commands return results, run topics type on each and use the type returned — do not guess.\n\nStep 2 — Discover the odometry topic\n\ntopics find nav_msgs/msg/Odometry\n\n\nRecord the discovered topic name — call it ODOM_TOPIC.\n\nStep 3 — Choose the execution method\n\nSituation\tMethod\nDistance or angle specified and odometry found\tpublish-until with --monitor <ODOM_TOPIC> — closed loop, stops on sensor feedback\nDistance or angle specified and no odometry\tpublish-sequence with calculated duration — open loop. Tell the user: \"No odometry found. Running open-loop. Distance/angle accuracy is not guaranteed.\"\nNo distance or angle specified (open-ended movement)\tpublish-sequence with a stop command as the final message\n\nStep 4 — Execute using only discovered names\n\nUse VEL_TOPIC and ODOM_TOPIC from Steps 1–2. Never substitute /cmd_vel, /odom, or any other assumed name.\n\nDistance commands:\n\ntopics publish-until <VEL_TOPIC> '<payload>' --monitor <ODOM_TOPIC> --field pose.pose.position.x --delta <N> --timeout 30\n\n\nAngle/rotation commands — always use --rotate, never --field or --yaw:\n\n# CCW (left): positive --rotate + positive angular.z\ntopics publish-until <VEL_TOPIC> '<payload>' --monitor <ODOM_TOPIC> --rotate <+N> --degrees --timeout 30\n# CW (right):  negative --rotate + negative angular.z\ntopics publish-until <VEL_TOPIC> '<payload>' --monitor <ODOM_TOPIC> --rotate <-N> --degrees --timeout 30\n\n\n--rotate sign = direction. Positive = CCW. Negative = CW. angular.z sign must always match --rotate sign — mismatched signs cause timeout. There is no --yaw flag. Do not attempt to monitor orientation fields manually. Open-ended or fallback (stop is always the last message):\n\ntopics publish-sequence <VEL_TOPIC> '[<move_payload>, <zero_payload>]' '[<duration>, 0.5]'\n\n\nSee the Movement Workflow section below for complete worked examples covering every case.\n\nRule 4 — Infer the goal, resolve the details\n\nWhen a user asks to do something, infer what they want at the goal level, then resolve all concrete details (topic names, types, field paths) from the live system.\n\nExamples:\n\n\"Take a picture\" → find compressed image topics (topics find sensor_msgs/msg/CompressedImage), capture from the first active result\n\"Move the robot forward\" → find velocity topic (topics find geometry_msgs/msg/Twist and TwistStamped), publish with the matching structure\n\"What is the battery level?\" → topics battery (auto-discovers BatteryState topics)\n\"List available controllers\" → control list-controllers\n\"What parameters does the camera node have?\" → nodes list to find the camera node name, then params list <node>\nRule 5 — Execute, don't ask\n\nThe user's message is the approval. Act on it.\n\nIf the intent is clear, execute immediately. Do not ask for confirmation, do not summarise what you are about to do and wait for a response, do not say \"I'll now run X — shall I proceed?\". Just run it.\n\nThis applies without exception to:\n\nRunning or launching nodes (launch new, run new)\nPublishing to topics\nCalling services\nSetting parameters\nStarting or stopping controllers\nAny command where the intent and target are unambiguous\n\nThe only time to stop and ask is when there is genuine ambiguity that cannot be resolved from the live system — for example:\n\nMultiple packages or launch files match and you cannot determine which one the user means\nA required argument has no match in --show-args and no reasonable fuzzy match exists\nThe user's request contradicts itself or is physically unsafe to guess\n\nEverything else: just do it.\n\nExplicit list of things that must never trigger a question:\n\n\"Should I run this command?\" — No. Run it.\n\"Would you like me to proceed?\" — No. Proceed.\n\"Do you want me to use X topic?\" — No. Use it.\n\"Shall I launch the file now?\" — No. Launch it.\n\"Do you want me to set this parameter?\" — No. Set it.\n\"I found Y — would you like me to use it?\" — No. Use it.\nRule 6 — Minimal reporting by default\n\nKeep output minimal. The user wants results, not narration.\n\nSituation\tWhat to report\nOperation succeeded\tOne line: what was done and the key outcome. Example: \"Done. Moved 1.02 m forward.\"\nMovement completed\tStart position, end position, actual distance/angle travelled, and any anomalies — nothing else\nNo suitable topic/source found\tClear error with what was searched and what to try next\nSafety condition triggered\tImmediate notification with what happened and what was sent (stop command)\nOperation failed\tError message with cause and recovery suggestion\n\nNever report by default:\n\nThe topic name selected, unless it is ambiguous or unexpected\nThe message type discovered\nIntermediate introspection results\nStep-by-step narration of what you are about to do\n\nReport everything (verbose mode) only when the user explicitly asks — e.g. \"show me what topics you found\", \"give me the full details\", \"what type did you use?\"\n\nSetup\n1. Source ROS 2 environment\nsource /opt/ros/${ROS_DISTRO}/setup.bash\n\n2. Install dependencies\npip install rclpy\n\n3. Run on ROS 2 robot\n\nThe CLI must run on a machine with ROS 2 installed and sourced.\n\nImportant: Check ROS 2 First\n\nBefore any operation, verify ROS 2 is available:\n\npython3 {baseDir}/scripts/ros2_cli.py version\n\nQuick Decision Card\n\nEvery user request follows this pattern:\n\nUser: \"do X\"\nAgent thinks:\n  1. Is X about reading data? → Use TOPICS SUBSCRIBE\n  2. Is X about movement (any kind)? → Follow the Movement Workflow (Rule 3 / canonical section)\n  3. Is X a one-time trigger? → Use SERVICES CALL or ACTIONS SEND\n  4. Is X about system info? → Use LIST commands\n\nAgent does (for movement):\n  1. Find velocity topic: topics find geometry_msgs/Twist + TwistStamped\n  2. Find odometry topic: topics find nav_msgs/Odometry\n  3. Distance/angle specified + odom found → publish-until (closed loop)\n     Distance/angle specified + no odom → publish-sequence, notify user (open loop)\n     No distance/angle → publish-sequence with stop\n\n\nFor movement, always follow the Movement Workflow section. That section is the single source of truth.\n\nAgent Decision Framework (MANDATORY)\n\nRULE: NEVER ask the user anything that can be discovered from the ROS 2 graph.\n\nStep 1: Understand User Intent\nUser says...\tAgent interprets as...\tAgent must...\n\"What topics exist?\"\tList topics\tRun topics list\n\"What nodes exist?\"\tList nodes\tRun nodes list\n\"What services exist?\"\tList services\tRun services list\n\"What actions exist?\"\tList actions\tRun actions list\n\"Read the LiDAR/scan\"\tSubscribe to LaserScan\tFind LaserScan topic → subscribe\n\"Read odometry/position\"\tSubscribe to Odometry\tFind Odometry topic → subscribe\n\"Read camera/image\"\tSubscribe to Image/CompressedImage\tFind Image topics → subscribe\n\"Read joint states/positions\"\tSubscribe to JointState\tFind JointState topic → subscribe\n\"Read IMU/accelerometer\"\tSubscribe to Imu\tFind Imu topic → subscribe\n\"Read battery/power\"\tSubscribe to BatteryState\tFind BatteryState topic → subscribe\n\"Read joystick/gamepad\"\tSubscribe to Joy\tFind Joy topic → subscribe\n\"Check robot diagnostics/health\"\tSubscribe to diagnostics\tFind /diagnostics topic → subscribe\n\"Check TF/transforms\"\tCheck TF topics\tFind /tf, /tf_static topics → subscribe\n\"Move/drive/turn (mobile robot)\"\tOpen-ended movement, no target\tFind Twist/TwistStamped → publish-sequence with stop\n\"Move forward/back N meters\"\tClosed-loop distance → Movement Workflow Case A\tFind odom → publish-until --euclidean --field pose.pose.position --delta N (frame-independent Euclidean distance)\n\"Rotate N degrees / turn left/right / turn N radians\"\tClosed-loop rotation → Movement Workflow Case B\tFind odom → publish-until --rotate ±N --degrees (CCW = positive, CW = negative). Sign of --rotate MUST match sign of angular.z. Never use --field or --yaw for rotation\n\"Move arm/joint (manipulator)\"\tPublish JointTrajectory\tFind JointTrajectory topic → publish\n\"Control gripper\"\tPublish GripperCommand or JointTrajectory\tFind gripper topic → publish\n\"Stop the robot\"\tPublish zero velocity\tFind Twist/TwistStamped → topics type to confirm → publish zeros in confirmed type\n\"Emergency stop\"\tPublish zero velocity\tRun estop command\n\"Call /reset\"\tCall service\tFind service → call\n\"Navigate to...\"\tSend action\tFind action → send goal\n\"Execute trajectory\"\tSend action\tFind FollowJointTrajectory or ExecuteTrajectory → send\n\"Run launch file\"\tLaunch file\tFind package → find launch file → launch in tmux\n\"List running launches\"\tList sessions\tRun launch list\n\"Kill launch\"\tKill session\tRun launch kill <session>\n\"What controllers?\"\tList controllers\tRun control list-controllers\n\"What hardware?\"\tList hardware\tRun control list-hardware-components\n\"What lifecycle nodes?\"\tList managed nodes\tRun lifecycle nodes\n\"Check lifecycle state\"\tGet node state\tRun lifecycle get <node>\n\"Configure/activate lifecycle node\"\tSet lifecycle state\tRun lifecycle set <node> <transition>\n\"Run diagnostics/health check\"\tRun doctor\tRun doctor\n\"Test connectivity\"\tRun multicast test\tRun doctor hello\n\"What parameters?\"\tList params\tFind node → params list\n\"What is the max speed?\"\tGet params\tFind controller → get velocity limits\n\"Save/load parameter config\"\tUse presets\tRun params preset-save / params preset-load\n\"Check battery level\"\tSubscribe to BatteryState\tRun topics battery or find BatteryState topic\nStep 2: Find What Exists\n\nALWAYS start by exploring what's available:\n\n# These 4 commands tell you EVERYTHING about the system\npython3 {baseDir}/scripts/ros2_cli.py topics list      # All topics\npython3 {baseDir}/scripts/ros2_cli.py services list    # All services\npython3 {baseDir}/scripts/ros2_cli.py actions list    # All actions\npython3 {baseDir}/scripts/ros2_cli.py nodes list      # All nodes\n\nStep 3: Search by Message Type\n\nTo find a topic/service/action, search by what you need:\n\nNeed to find...\tSearch command...\nVelocity command topic (mobile)\ttopics find geometry_msgs/Twist AND topics find geometry_msgs/TwistStamped → then topics type <result> to confirm exact type\nPosition/odom topic\ttopics find nav_msgs/Odometry\nJoint positions\ttopics find sensor_msgs/JointState\nJoint trajectory (arm control)\ttopics find trajectory_msgs/JointTrajectory\nLiDAR data\ttopics find sensor_msgs/LaserScan\nCamera feed\ttopics find sensor_msgs/Image OR topics find sensor_msgs/CompressedImage\nIMU data\ttopics find sensor_msgs/Imu\nJoystick\ttopics find sensor_msgs/Joy\nBattery/power\ttopics find sensor_msgs/BatteryState\nTemperature\ttopics find sensor_msgs/Temperature\nPoint clouds\ttopics find sensor_msgs/PointCloud2\nTF transforms\tSubscribe to /tf or /tf_static\nDiagnostics\tSubscribe to /diagnostics\nClock (simulated time)\tSubscribe to /clock\nService by type\tservices find <service_type>\nAction by type\tactions find <action_type>\nStep 4: Get Message Structure\n\nBefore publishing or calling, always confirm the type and get the structure:\n\n# Confirm the exact message type of a discovered topic (critical — never skip this)\npython3 {baseDir}/scripts/ros2_cli.py topics type <discovered_topic>\n\n# Get field structure (for building payloads)\npython3 {baseDir}/scripts/ros2_cli.py topics message <confirmed_message_type>\n\n# Get default values (copy-paste template)\npython3 {baseDir}/scripts/ros2_cli.py interface proto <confirmed_message_type>\n\n# Get service/action request structure\npython3 {baseDir}/scripts/ros2_cli.py services details <service_name>\npython3 {baseDir}/scripts/ros2_cli.py actions details <action_name>\n\nStep 5: Get Safety Limits (for movement)\n\nALWAYS check for velocity limits before publishing movement commands. Limits can be on ANY node — not just controller nodes. Scan every node.\n\n# Step 1: List every running node\npython3 {baseDir}/scripts/ros2_cli.py nodes list\n\n# Step 2: Dump all parameters from every node\n# Run in parallel — one params list per node\npython3 {baseDir}/scripts/ros2_cli.py params list <NODE_1>\npython3 {baseDir}/scripts/ros2_cli.py params list <NODE_2>\n# ... repeat for every node\n\n# Step 3: For each node, filter parameter names that contain:\n#   max, limit, vel, speed, accel (case-insensitive)\n# These are candidate velocity/acceleration limits.\n\n# Step 4: Retrieve the value of every candidate\npython3 {baseDir}/scripts/ros2_cli.py params get <NODE>:<candidate_param>\n# Repeat for every candidate across every node.\n\n# Step 5: Compute binding ceiling\n# linear_ceiling  = min of all discovered linear limit values\n# angular_ceiling = min of all discovered angular/theta limit values\n# velocity = min(requested_velocity, ceiling)\n# Never exceed the ceiling. Use 50% of the ceiling if unsure of the appropriate fraction.\n\n\nIf no limits are found on any node: use conservative defaults (0.2 m/s linear, 0.75 rad/s angular) and tell the user.\n\nGlobal Options\n\n--timeout and --retries are global flags that apply to every command making service or action calls.\n\n--timeout must be placed before the command name (e.g. --timeout 10 services call …).\n--retries can be placed before the command name OR after it for services call, actions send, and actions cancel — both positions work.\nOption\tDefault\tDescription\n--timeout SECONDS\tper-command default\tOverride the per-command timeout globally\n--retries N\t1\tTotal attempts before giving up; 1 = single attempt with no retry\npython3 {baseDir}/scripts/ros2_cli.py --timeout 30 params list /turtlesim\npython3 {baseDir}/scripts/ros2_cli.py --retries 3 services call /spawn '{}'\n\nEXECUTION RULES (MUST FOLLOW)\nRule 1: Never Ask User for These\n\nThe agent MUST discover these automatically:\n\nUser might ask...\tAgent must...\n\"What topic do I use?\"\tUse topics find <type> to discover\n\"What message type?\"\tUse topics type <topic> or topics find <type>\n\"What is the message structure?\"\tUse topics message <type> or interface show <type>\n\"What are the safety limits?\"\tUse params list on controller nodes\n\"Is there odometry?\"\tUse topics find nav_msgs/Odometry\nRule 2: Only Ask User After ALL Discovery Fails\n\nONLY ask the user when:\n\ntopics find geometry_msgs/Twist AND topics find geometry_msgs/TwistStamped both return empty\ntopics find nav_msgs/Odometry returns empty (and you need odometry for distance)\nYou've checked params on ALL controller nodes and found NO velocity limits\nThe service/action the user mentions doesn't exist in services list / actions list\nRule 3: Movement Requires Odometry Feedback\n\nSee Agent Behaviour Rule 3 above — it is absolute and overrides any example in this document.\n\nIn short: for any \"move N meters\" or \"rotate N degrees\" command, you MUST find the odometry topic first (topics find nav_msgs/Odometry) and use publish-until --monitor <odom_topic>. publish-sequence with a fixed time is forbidden when odometry is available.\n\nALWAYS apply safety limits: velocity = min(requested, max_velocity)\nRule 4: Always Stop After Movement\n\npublish-until stops automatically when the odometry condition is met — no explicit stop step is needed.\n\nFor open-ended publishes (topics publish or publish-sequence fallback), the final message MUST be all zeros.\n\nAlways use the topic name and payload type discovered in Steps 1–2. <VEL_TOPIC> and <ODOM_TOPIC> are placeholders for your discovered values.\n\n# WRONG: open-ended publish with no stop, and hardcoded /cmd_vel — never do this\npython3 {baseDir}/scripts/ros2_cli.py topics publish /cmd_vel '{\"linear\":{\"x\":1.0}}'\n\n# CORRECT (distance specified, odometry available): use publish-until — stops itself\n# VEL_TOPIC and ODOM_TOPIC come from your introspection steps\npython3 {baseDir}/scripts/ros2_cli.py topics publish-until <VEL_TOPIC> \\\n  '<payload_matching_confirmed_type>' \\\n  --monitor <ODOM_TOPIC> --euclidean --field pose.pose.position --delta 1.0 --timeout 30\n\n# CORRECT (fallback only — no odometry, no distance specified): publish-sequence with stop at end\npython3 {baseDir}/scripts/ros2_cli.py topics publish-sequence <VEL_TOPIC> \\\n  '[<move_payload>, <zero_payload>]' \\\n  '[3.0, 0.5]'\n\nRule 5: Handle Multiple Same-Type Topics\n\nWhen multiple topics of the same type exist (e.g., 2 cameras, 3 LiDARs):\n\nList all candidates:\n\npython3 {baseDir}/scripts/ros2_cli.py topics find sensor_msgs/Image\n# Returns: [\"/camera_front/image_raw\", \"/camera_rear/image_raw\", ...]\n\n\nSelect based on context or naming convention:\n\nFront camera: prefer topics with front, rgb, color in name\nRear camera: prefer topics with rear, back in name\nPrimary LiDAR: prefer topics with front, base, main in name\nDefault: use first topic in the list\n\nLet user know which one you're using:\n\n\"Found 3 camera topics. Using /camera_front/image_raw.\"\nError Recovery\n\nWhen commands fail, follow this recovery process:\n\nSubscribe Timeouts\nError\tRecovery\nTimeout waiting for message\t1. Check topics details <topic> to verify publisher exists<br>2. Try a different topic if multiple exist<br>3. Increase --duration or --timeout\nNo messages received\t1. Verify publisher is running: topics details <topic><br>2. Check if topic requires subscription to trigger\nPublish Failures\nError\tRecovery\nCould not load message type\t1. Verify type: topics type <topic><br>2. Ensure ROS workspace is built\nFailed to create publisher\t1. Check topic exists: topics list<br>2. Verify node has permission to publish\nService/Action Failures\nError\tRecovery\nService not found\t1. Verify service exists: services list<br>2. Check service type: services type <service>\nAction not found\t1. Verify action exists: actions list<br>2. Check action type: actions type <action>\nService call timeout\t1. Increase --timeout<br>2. Verify service server is running\nAction goal rejected\t1. Check action details for goal requirements<br>2. Verify robot is in correct state\nParameter Failures\nError\tRecovery\nNode not found\t1. Verify node exists: nodes list<br>2. Check namespace\nParameter not found\t1. List params: params list <node><br>2. Parameter may not exist on this node\nMovement / publish-until Failures\nError\tRecovery\npublish-until times out without reaching target\t1. Immediately send estop — do not wait, do not retry, do not ask the user first<br>2. Subscribe to <ODOM_TOPIC> and check twist.twist.linear / twist.twist.angular: if any value > 0.01, the robot is still moving — keep estop sent and wait for it to stop before continuing<br>3. Then diagnose: topics details <ODOM_TOPIC> — if publisher count dropped to 0 mid-motion, odometry died; notify user<br>4. If odometry is healthy but robot is slow: increase --timeout and retry only after the robot has fully stopped\nOdometry not updating during motion\t1. Immediately send zero-velocity: estop<br>2. Check topics details <ODOM_TOPIC> for publisher count and topics hz <ODOM_TOPIC> for rate<br>3. Do NOT continue publishing if odometry is stale — it is a runaway risk\nVelocity topic has no subscribers\t1. Check control list-controllers — the controller may be inactive<br>2. Activate the controller: control set-controller-state <name> active<br>3. Re-verify with topics details <VEL_TOPIC> before retrying\npublish-until hangs / no feedback\t1. Verify monitor topic: topics details <ODOM_TOPIC><br>2. Verify the field path is correct: subscribe once and inspect field names<br>3. Check --timeout is set\nAction Preemption — actions cancel vs estop\n\nUse this decision table whenever an in-flight action goal needs to be stopped:\n\nSituation\tAction\tReason\nGoal is running but user wants to abort gracefully\tactions cancel <action>\tSends a cancel request to the action server; the server winds down cleanly\nGoal is running and the robot is moving unsafely / not stopping\testop first, then actions cancel <action>\testop publishes zero velocity immediately; cancel cleans up the goal state\nGoal was rejected or timed out (robot not moving)\tactions cancel <action>\tNo motion risk; cancel clears the goal state\nAction server crashed / no longer responding\testop\tNo action server to receive cancel; stop the actuators directly\nGoal completed but robot is still drifting / coasting\testop\tMotion is no longer governed by the goal; velocity command is needed\n\nRule: If in doubt, send estop first — it is always safe. Then send actions cancel to clean up goal state. Never skip estop when the robot is or may be moving.\n\nAlways retry failed operations:\n\nUse --retries 3 for unreliable services\nUse --timeout 30 for slow operations\nWait 1-2 seconds between retries\nTopic and Service Discovery\n\nNever guess topic names. Any time an operation involves a topic, discover the actual topic name from the live graph first.\n\nImages and Camera\n\nAlways prefer compressed topics - use much less bandwidth:\n\npython3 {baseDir}/scripts/ros2_cli.py topics find sensor_msgs/msg/CompressedImage\npython3 {baseDir}/scripts/ros2_cli.py topics find sensor_msgs/msg/Image\n\n\nUse topics capture-image --topic <discovered> - never subscribe for images.\n\nVelocity Commands (Twist vs TwistStamped)\n\nCheck both types to find the topic:\n\npython3 {baseDir}/scripts/ros2_cli.py topics find geometry_msgs/msg/Twist\npython3 {baseDir}/scripts/ros2_cli.py topics find geometry_msgs/msg/TwistStamped\n\n\nThen confirm the exact type of the discovered topic — do not assume from the find result:\n\npython3 {baseDir}/scripts/ros2_cli.py topics type <discovered_topic>\n\n\nUse the confirmed type to choose the payload:\n\ngeometry_msgs/msg/Twist: {\"linear\": {\"x\": 1.0, \"y\": 0.0, \"z\": 0.0}, \"angular\": {\"x\": 0.0, \"y\": 0.0, \"z\": 0.0}}\ngeometry_msgs/msg/TwistStamped: {\"header\": {\"stamp\": {\"sec\": 0}, \"frame_id\": \"\"}, \"twist\": {\"linear\": {\"x\": 1.0, \"y\": 0.0, \"z\": 0.0}, \"angular\": {\"x\": 0.0, \"y\": 0.0, \"z\": 0.0}}}\nQuick lookup table\nIf you need...\tRun this...\tThen...\nImages\ttopics find sensor_msgs/msg/Image\tSubscribe to result\nLiDAR\ttopics find sensor_msgs/msg/LaserScan\tSubscribe to result\nOdometry\ttopics find nav_msgs/msg/Odometry\tSubscribe to result\nIMU\ttopics find sensor_msgs/msg/Imu\tSubscribe to result\nJoint states\ttopics find sensor_msgs/msg/JointState\tSubscribe to result\nMove robot\ttopics find geometry_msgs/msg/Twist AND topics find geometry_msgs/msg/TwistStamped → then topics type <result> to confirm\tPublish to discovered topic\nRun launch file\tlaunch new <package> <file>\tRuns in tmux session\nList running launches\tlaunch list\tShows tmux sessions\nKill launch\tlaunch kill <session>\tKills tmux session\nLaunch Commands\nAuto-Discovery for Launch Files\n\nWhen user says \"run the bringup\" or \"launch navigation\" (partial/ambiguous request):\n\nDiscover available packages:\n\nros2 pkg list  # Get all packages\n\n\nFind matching launch files:\n\nros2 pkg files <package>  # Find launch files in package\n\n\nIntelligent inference (use context):\n\n\"bringup\" → look for packages with bringup in name, or launch files named bringup.launch.py\n\"navigation\" → look for navigation2, nav2, or launch files with navigation\n\"camera\" → look for camera-related packages\n\nIf exactly one clear match found:\n\nLaunch it immediately — do not ask for confirmation (Rule 5)\n\nIf multiple candidates found and cannot be disambiguated:\n\nPresent options: \"Found 3 launch files: X, Y, Z. Which one?\" — this is the only case where asking is permitted\n\nIf no match found:\n\nSearch more broadly: check all packages for matching launch files\nIf still nothing: ask user for exact package/file name\nNEVER hallucinate:\n❌ Never invent a package name that doesn't exist\n❌ Never invent a launch file that doesn't exist\n❌ Never assume a package exists without checking\n❌ Never invent launch argument names not present in --show-args output\n❌ Never fuzzy-match argument names yourself — the script does this automatically against real --show-args output; always pass the user's original name and let the script resolve it\n❌ Never pass arguments that weren't explicitly provided by the user\nALWAYS verify:\n✅ Check ros2 pkg list for package existence\n✅ Check ros2 pkg files <package> for launch files\n✅ Run ros2 launch <pkg> <file> --show-args to get valid arguments\n✅ Validate each user-provided argument against --show-args output\nRule: Only ask when genuinely ambiguous\n\nPer Rule 5, the user's message is the approval. For launch files:\n\nExactly one match → launch it immediately, no confirmation needed\nMultiple matches with no way to disambiguate → list them and ask which one\nNo match at all → ask for exact package/file name\nLocal Workspace Sourcing\n\nSystem ROS is assumed to be already sourced (via systemd service or manually). The skill automatically sources any local workspace on top of system ROS.\n\nSearch order:\n\nROS2_LOCAL_WS environment variable\n~/ros2_ws\n~/colcon_ws\n~/dev_ws\n~/workspace\n~/ros2\n\nBehavior:\n\nScenario\tBehavior\nWorkspace found + built\tSource automatically, run silently\nWorkspace found + NOT built\tWarn user, run without sourcing\nWorkspace NOT found\tContinue without sourcing (system ROS only)\n\nOverride option:\n\n# Set environment variable before running\nexport ROS2_LOCAL_WS=~/my_robot_ws\n\nTF2 Transforms\n# List all coordinate frames\npython3 {baseDir}/scripts/ros2_cli.py tf list\n\n# Lookup transform between frames\npython3 {baseDir}/scripts/ros2_cli.py tf lookup base_link map\n\n# Echo transform continuously\npython3 {baseDir}/scripts/ros2_cli.py tf echo base_link map --count 10\n\n# Echo transform once\npython3 {baseDir}/scripts/ros2_cli.py tf echo base_link map --once\n\n# Monitor a specific frame\npython3 {baseDir}/scripts/ros2_cli.py tf monitor base_link --count 5\n\n# Publish static transform — named form\npython3 {baseDir}/scripts/ros2_cli.py tf static --from base_link --to sensor --xyz 1 2 3 --rpy 0 0 0\n\n# Publish static transform — positional form\npython3 {baseDir}/scripts/ros2_cli.py tf static 0 0 0 0 0 0 base_link odom\n\n# Convert quaternion to Euler (radians) — also: e2q, quat2euler\npython3 {baseDir}/scripts/ros2_cli.py tf euler-from-quaternion 0 0 0 1\n\n# Convert Euler to quaternion (radians) — also: q2e, euler2quat\npython3 {baseDir}/scripts/ros2_cli.py tf quaternion-from-euler 0 0 1.57\n\n# Convert quaternion to Euler (degrees) — also: e2qdeg\npython3 {baseDir}/scripts/ros2_cli.py tf euler-from-quaternion-deg 0 0 0 1\n\n# Convert Euler to quaternion (degrees) — also: q2edeg\npython3 {baseDir}/scripts/ros2_cli.py tf quaternion-from-euler-deg 0 0 90\n\n# Transform a point between frames — also: tp, point\npython3 {baseDir}/scripts/ros2_cli.py tf transform-point map base_link 1 0 0\n\n# Transform a vector between frames — also: tv, vector\npython3 {baseDir}/scripts/ros2_cli.py tf transform-vector map base_link 1 0 0\n\nRun a Launch File\n# Basic launch\npython3 {baseDir}/scripts/ros2_cli.py launch new navigation2 navigation2.launch.py\n\n# With arguments - MUST verify arguments exist first\npython3 {baseDir}/scripts/ros2_cli.py launch new navigation2 navigation2.launch.py arg1:=value arg2:=value\n\n⚠️ STRICT: Launch Argument Validation\n\nThis is critical for safety. Passing incorrect arguments has caused accidents.\n\nRules, in order:\n\nAlways fetch available arguments first via --show-args before passing any args.\nExact match → pass as-is.\nNo exact match → fuzzy-match against the real available args only.\nIf a close match is found (e.g. \"mock\" → \"use_mock\") → use it, but notify the user.\nThe substitution is shown in arg_notices in the output.\nNo match at all → drop the argument, do NOT pass it, notify the user.\nThe launch still proceeds without that argument.\nThe user is told which argument was dropped and what the available args are.\nNever invent or assume argument names. Only use names that exist in --show-args output.\nIf --show-args fails → drop all user-provided args, notify the user, still launch.\nRun an Executable\n\nRun a ROS 2 executable in a tmux session. Similar to launch commands but for single executables. Auto-detects executable names (e.g., \"teleop\" matches \"teleop_node\").\n\n# Run an executable\npython3 {baseDir}/scripts/ros2_cli.py run new lekiwi_control teleop\n\n# Run with arguments\npython3 {baseDir}/scripts/ros2_cli.py run new lekiwi_control teleop --arg1 value\n\n# Run with parameters\npython3 {baseDir}/scripts/ros2_cli.py run new lekiwi_control teleop --params \"speed:1.0\"\n\n# Run with presets\npython3 {baseDir}/scripts/ros2_cli.py run new lekiwi_control teleop --presets indoor\n\n# Run with config path\npython3 {baseDir}/scripts/ros2_cli.py run new lekiwi_control teleop --config-path /path/to/config\n\nList Running Executables\npython3 {baseDir}/scripts/ros2_cli.py run list\n\nKill an Executable Session\npython3 {baseDir}/scripts/ros2_cli.py run kill run_lekiwi_control_teleop\n\nRestart an Executable Session\npython3 {baseDir}/scripts/ros2_cli.py run restart run_lekiwi_control_teleop\n\nRun Session Collision Handling\n\nSame as launch - if a session with the same name already exists, the command will fail with an error. Use run restart or run kill first.\n\nCommand Quick Reference\n1. Explore a Robot System\npython3 {baseDir}/scripts/ros2_cli.py version\npython3 {baseDir}/scripts/ros2_cli.py topics list\npython3 {baseDir}/scripts/ros2_cli.py nodes list\npython3 {baseDir}/scripts/ros2_cli.py services list\npython3 {baseDir}/scripts/ros2_cli.py actions list\n\n# Find a topic by type, then inspect it\n# (replace the type with whatever you're looking for)\npython3 {baseDir}/scripts/ros2_cli.py topics find geometry_msgs/msg/Twist\npython3 {baseDir}/scripts/ros2_cli.py topics find geometry_msgs/msg/TwistStamped\n# → get the discovered topic name, then:\npython3 {baseDir}/scripts/ros2_cli.py topics type <discovered_topic>\npython3 {baseDir}/scripts/ros2_cli.py interface proto <confirmed_type>\n\n2. Move a Robot\n\nFollow the Movement Workflow section. It covers all cases: distance commands, rotation commands, open-ended movement, and the no-odometry fallback. Always discover topics first — never assume names.\n\nEmergency stop:\n\npython3 {baseDir}/scripts/ros2_cli.py estop\n\n3. Read Sensor Data\n\nAlways use auto-discovery first to find the correct sensor topics.\n\n# Step 1: Discover sensor topics by message type\npython3 {baseDir}/scripts/ros2_cli.py topics find sensor_msgs/msg/LaserScan\npython3 {baseDir}/scripts/ros2_cli.py topics find nav_msgs/msg/Odometry\npython3 {baseDir}/scripts/ros2_cli.py topics find sensor_msgs/msg/JointState\n# → record each discovered topic name\n\n# Step 2: Subscribe to discovered topics (use the names from Step 1, not hardcoded names)\npython3 {baseDir}/scripts/ros2_cli.py topics subscribe <LASER_TOPIC> --duration 3\npython3 {baseDir}/scripts/ros2_cli.py topics subscribe <ODOM_TOPIC> --duration 10 --max-messages 50\npython3 {baseDir}/scripts/ros2_cli.py topics subscribe <JOINT_STATE_TOPIC> --duration 5\n\n4. Use Services\n\nAlways use auto-discovery first to find available services and their request/response structures.\n\n# Step 1: Discover available services\npython3 {baseDir}/scripts/ros2_cli.py services list\n\n# Step 2: Find services by type (optional)\npython3 {baseDir}/scripts/ros2_cli.py services find std_srvs/srv/Empty\npython3 {baseDir}/scripts/ros2_cli.py services find turtlesim/srv/Spawn\n\n# Step 3: Get service request/response structure\npython3 {baseDir}/scripts/ros2_cli.py services details /spawn\n\n# Step 4: Call the service with properly-structured payload\npython3 {baseDir}/scripts/ros2_cli.py services call /spawn \\\n  '{\"x\":3.0,\"y\":3.0,\"theta\":0.0,\"name\":\"turtle2\"}'\n\n5. Actions\n\nAlways use auto-discovery first to find available actions and their goal/result structures.\n\n# Step 1: Discover available actions\npython3 {baseDir}/scripts/ros2_cli.py actions list\n\n# Step 2: Find actions by type (optional)\npython3 {baseDir}/scripts/ros2_cli.py actions find turtlesim/action/RotateAbsolute\npython3 {baseDir}/scripts/ros2_cli.py actions find nav2_msgs/action/NavigateToPose\n\n# Step 3: Get action goal/result structure\npython3 {baseDir}/scripts/ros2_cli.py actions details /turtle1/rotate_absolute\n\n# Step 4: Send goal with properly-structured payload\npython3 {baseDir}/scripts/ros2_cli.py actions send /turtle1/rotate_absolute \\\n  '{\"theta\":1.57}'\n\n# Step 5: Monitor feedback — always echo after sending a goal\npython3 {baseDir}/scripts/ros2_cli.py actions echo /turtle1/rotate_absolute --timeout 30\n\n\nAfter every actions send, immediately run actions echo on the same action server to monitor feedback. A stuck or rejected goal gives no signal without feedback monitoring. If actions echo returns no messages within --timeout, the goal may have been rejected, preempted, or the action server may have crashed — check with actions list and actions details, then decide between actions cancel (graceful abort) and estop (runaway/unsafe motion). See the Action Preemption table in the Error Recovery section.\n\n6. Change Parameters\n\nAlways use auto-discovery first to list available parameters for a node.\n\n# Step 1: Discover nodes\npython3 {baseDir}/scripts/ros2_cli.py nodes list\n\n# Step 2: List parameters for a node\npython3 {baseDir}/scripts/ros2_cli.py params list /turtlesim\n\n# Step 3: Get current parameter value\npython3 {baseDir}/scripts/ros2_cli.py params get /turtlesim:background_r\n\n# Step 4: Set parameter value\npython3 {baseDir}/scripts/ros2_cli.py params set /turtlesim:background_r 255\npython3 {baseDir}/scripts/ros2_cli.py params set /turtlesim:background_g 0\npython3 {baseDir}/scripts/ros2_cli.py params set /turtlesim:background_b 0\n\n# Step 5: Read back after set — always verify the change took effect\n# Some nodes silently reject changes (read-only params, out-of-range values)\npython3 {baseDir}/scripts/ros2_cli.py params get /turtlesim:background_r\npython3 {baseDir}/scripts/ros2_cli.py params get /turtlesim:background_g\npython3 {baseDir}/scripts/ros2_cli.py params get /turtlesim:background_b\n\n\nAfter every params set, always run params get on the same parameter to confirm the change was accepted. Nodes may silently ignore a set if the parameter is read-only or the value is out of range — the set call returns success even in those cases.\n\n7. Goal-Oriented Commands (publish-until)\n\nFor any goal with a sensor-based stop condition — joint angles, temperature limits, proximity, battery level — use publish-until with the appropriate monitor topic and condition flag. Always discover both the command topic and the monitor topic before executing — never hardcode names.\n\nUser intent\tDiscover command topic\tDiscover monitor topic\tCondition\nStop near obstacle\ttopics find geometry_msgs/Twist + TwistStamped\ttopics find sensor_msgs/LaserScan → field ranges.0\t--below 0.5\nStop at range\tsame\ttopics find sensor_msgs/Range → field range\t--below D\nStop at temperature\t—\ttopics find sensor_msgs/Temperature → field temperature\t--above T\nStop at battery level\t—\ttopics find sensor_msgs/BatteryState → field percentage\t--below P\nJoint reach angle\ttopics find trajectory_msgs/JointTrajectory or similar\ttopics find sensor_msgs/JointState → field position.0\t--equals A or --delta D\nMulti-joint distance\tsame\ttopics find sensor_msgs/JointState → fields position.0 position.1\t--euclidean --delta D\n\n--euclidean computes sqrt(Σ(current_i - start_i)²) across all --field paths. Use it for curved or diagonal paths. Composite field shorthand: --field pose.pose.position auto-expands to x, y, z — equivalent to listing all three fields explicitly.\n\n# Example: stop when front range sensor reads < 0.5 m\n# Step 1: discover velocity topic\npython3 {baseDir}/scripts/ros2_cli.py topics find geometry_msgs/msg/Twist\npython3 {baseDir}/scripts/ros2_cli.py topics find geometry_msgs/msg/TwistStamped\n# → VEL_TOPIC = (result, e.g. /base/cmd_vel)\npython3 {baseDir}/scripts/ros2_cli.py topics type <VEL_TOPIC>\n# → confirms type, determines payload structure\n\n# Step 2: discover laser scan topic\npython3 {baseDir}/scripts/ros2_cli.py topics find sensor_msgs/msg/LaserScan\n# → SCAN_TOPIC = (result, e.g. /scan or /lidar/scan)\n\n# Step 3: execute with discovered names\npython3 {baseDir}/scripts/ros2_cli.py topics publish-until <VEL_TOPIC> \\\n  '<payload_matching_confirmed_type>' \\\n  --monitor <SCAN_TOPIC> --field ranges.0 --below 0.5 --timeout 30\n\n# Example: move a joint until it reaches 1.5 rad\n# Step 1: discover joint command topic\npython3 {baseDir}/scripts/ros2_cli.py topics find trajectory_msgs/msg/JointTrajectory\n# → or check nodes details for the arm controller node\n# → JOINT_CMD_TOPIC = (result)\n\n# Step 2: discover joint state topic\npython3 {baseDir}/scripts/ros2_cli.py topics find sensor_msgs/msg/JointState\n# → JOINT_STATE_TOPIC = (result)\n\n# Step 3: execute with discovered names\npython3 {baseDir}/scripts/ros2_cli.py topics publish-until <JOINT_CMD_TOPIC> \\\n  '{\"data\":0.5}' \\\n  --monitor <JOINT_STATE_TOPIC> --field position.0 --equals 1.5 --timeout 10\n\n\n<a name=\"movement-workflow-canonical\"></a>\n\nMovement Workflow (canonical)\n\nThis is the single authoritative workflow for all robot movement. Rule 3 in the Agent Behaviour Rules above is a summary of this section. Always follow these steps in order.\n\nIn every example below, <VEL_TOPIC>, <ODOM_TOPIC>, and <PAYLOAD> are placeholders for values you discover in Steps 1–2. Never substitute /cmd_vel, /odom, or any other assumed name.\n\nStep 1 — Discover the velocity command topic and confirm its exact type\n\nRun both searches in parallel:\n\npython3 {baseDir}/scripts/ros2_cli.py topics find geometry_msgs/msg/Twist\npython3 {baseDir}/scripts/ros2_cli.py topics find geometry_msgs/msg/TwistStamped\n\n\nTake the result — this is VEL_TOPIC. Then confirm the exact type:\n\npython3 {baseDir}/scripts/ros2_cli.py topics type <VEL_TOPIC>\n\n\nThe confirmed type determines the payload:\n\ngeometry_msgs/msg/Twist: {\"linear\":{\"x\":0.2,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}\ngeometry_msgs/msg/TwistStamped: {\"header\":{\"stamp\":{\"sec\":0},\"frame_id\":\"\"},\"twist\":{\"linear\":{\"x\":0.2,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}}\nZero/stop — Twist: {\"linear\":{\"x\":0,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}\nZero/stop — TwistStamped: {\"header\":{\"stamp\":{\"sec\":0},\"frame_id\":\"\"},\"twist\":{\"linear\":{\"x\":0,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}}\n\nIf both find commands return results, run topics type on each to get the definitive type — do not guess from the topic name. Then choose using this priority order:\n\nPrefer the topic with an active subscriber (topics details <topic> → subscriber_count > 0) — a subscribed topic means a controller is listening.\nIf both have subscribers, prefer the topic whose name contains cmd_vel or is at the root namespace (e.g. /cmd_vel over /robot/cmd_vel_stamped).\nIf still ambiguous, prefer TwistStamped over Twist (more modern standard). Use the selected topic as VEL_TOPIC for the rest of the workflow.\nStep 2 — Discover the odometry topic\npython3 {baseDir}/scripts/ros2_cli.py topics find nav_msgs/msg/Odometry\n\n\nTake the result — this is ODOM_TOPIC.\n\nStep 2.5 — Verify topics are live, read safety limits, and capture start position\n\nBefore executing, verify both topics are active, discover velocity limits from controller parameters, and record the starting position.\n\nCheck if the robot is already in motion — do not command a moving robot:\n\npython3 {baseDir}/scripts/ros2_cli.py topics subscribe <ODOM_TOPIC> --max-messages 1 --timeout 3\n\n\nInspect the twist.twist.linear and twist.twist.angular fields. If any value is significantly non-zero (> 0.01 m/s or rad/s), the robot is already moving. Do not publish new velocity commands — stop it first with estop, wait for motion to cease, then proceed.\n\nVerify the odometry publish rate is adequate for closed-loop control:\n\npython3 {baseDir}/scripts/ros2_cli.py topics hz <ODOM_TOPIC>\n\n\nThe rate must be at least 10 Hz for reliable closed-loop control. If the rate is below 5 Hz: fall back to Case D (open-loop) and warn the user that odometry is too slow for safe closed-loop operation.\n\nDiscover velocity limits from ALL nodes (full-graph scan):\n\nVelocity limits can be declared on any node — not just nodes whose names suggest \"controller\". Scan every node:\n\npython3 {baseDir}/scripts/ros2_cli.py nodes list\n# → iterate over every node and run params list on each\npython3 {baseDir}/scripts/ros2_cli.py params list <NODE>\n# → filter parameter names containing: max, limit, vel, speed, accel (case-insensitive)\npython3 {baseDir}/scripts/ros2_cli.py params get <NODE>:<candidate_param>\n# → repeat for every candidate across every node\n\n\nCompute the binding ceiling:\n\nlinear_ceiling = minimum of all discovered linear-axis limit values\nangular_ceiling = minimum of all discovered angular/theta limit values\n\nCap all commanded velocities at these ceilings. If no limits are found on any node, use conservative defaults (0.2 m/s linear, 0.75 rad/s angular) and note this.\n\nVerify the velocity topic has active subscribers (something must be listening — i.e., a controller):\n\npython3 {baseDir}/scripts/ros2_cli.py topics details <VEL_TOPIC>\n\n\nCheck the output for subscriber_count > 0. If no subscribers: the controller may not be running. Check control list-controllers and activate the correct controller before proceeding.\n\nCheck for emergency stop / safety interlock state:\n\npython3 {baseDir}/scripts/ros2_cli.py control list-controllers\n\n\nVerify that the motion controller is in active state. If it is inactive, unconfigured, or missing: do not publish velocity — activate it first or notify the user that the controller is not ready.\n\nVerify the odometry topic has an active publisher (not stale):\n\npython3 {baseDir}/scripts/ros2_cli.py topics details <ODOM_TOPIC>\n\n\nCheck publisher_count > 0. Then confirm it is actively publishing by subscribing briefly:\n\npython3 {baseDir}/scripts/ros2_cli.py topics subscribe <ODOM_TOPIC> --max-messages 1 --timeout 3\n\n\nIf no message arrives within the timeout: the odometry source is stale or not running. Fall back to Case D (open-loop) and notify the user.\n\nCapture start position (for distance reporting after motion):\n\npython3 {baseDir}/scripts/ros2_cli.py topics subscribe <ODOM_TOPIC> --max-messages 1\n\n\nRecord pose.pose.position.x, pose.pose.position.y from the result — this is the start pose.\n\nStep 3 — Execute based on intent and odometry availability\nCase A — Distance specified, odometry available → publish-until --euclidean (closed loop)\n\nAlways use --euclidean --field pose.pose.position (expands to x, y, z) to measure Euclidean distance from start. This is frame-independent: it works correctly regardless of the robot's heading and stays accurate after any prior rotation. Do not use --field pose.pose.position.x alone — that only tracks displacement along the odom x-axis and gives wrong results once the robot is not aligned with it.\n\n# Step 1 result: VEL_TOPIC = <discovered, e.g. /base/cmd_vel or /robot/cmd_vel>\n# Step 1 type check: geometry_msgs/msg/Twist → use Twist payload\n# Step 2 result: ODOM_TOPIC = <discovered, e.g. /odom or /wheel_odom>\n\n# Move forward 1 meter (Twist) — Euclidean distance, frame-independent\npython3 {baseDir}/scripts/ros2_cli.py topics publish-until <VEL_TOPIC> \\\n  '{\"linear\":{\"x\":0.2,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}' \\\n  --monitor <ODOM_TOPIC> --euclidean --field pose.pose.position --delta 1.0 --timeout 30\n\n# Move forward 1 meter — TwistStamped variant (if type check returned TwistStamped)\npython3 {baseDir}/scripts/ros2_cli.py topics publish-until <VEL_TOPIC> \\\n  '{\"header\":{\"stamp\":{\"sec\":0},\"frame_id\":\"\"},\"twist\":{\"linear\":{\"x\":0.2,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}}' \\\n  --monitor <ODOM_TOPIC> --euclidean --field pose.pose.position --delta 1.0 --timeout 30\n\n# Move backward 0.5 meters (Euclidean — delta is always positive; direction is set by the velocity sign)\npython3 {baseDir}/scripts/ros2_cli.py topics publish-until <VEL_TOPIC> \\\n  '{\"linear\":{\"x\":-0.2,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}' \\\n  --monitor <ODOM_TOPIC> --euclidean --field pose.pose.position --delta 0.5 --timeout 30\n\n# Move 2 meters along any curved path\npython3 {baseDir}/scripts/ros2_cli.py topics publish-until <VEL_TOPIC> \\\n  '{\"linear\":{\"x\":0.2,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0.3}}' \\\n  --monitor <ODOM_TOPIC> --euclidean --field pose.pose.position --delta 2.0 --timeout 60\n\n\nNote on --delta sign with --euclidean: Euclidean distance is always non-negative, so --delta should always be a positive value — the direction of travel is determined entirely by the velocity command (sign of linear.x), not by the sign of --delta.\n\nObstacle avoidance during forward movement: publish-until supports one --monitor topic. If you need to stop before an obstacle (not at a fixed distance), use the LaserScan topic as the monitor instead of odometry:\n\n# Discover the LaserScan topic\npython3 {baseDir}/scripts/ros2_cli.py topics find sensor_msgs/msg/LaserScan\n# → SCAN_TOPIC = <result>\n\n# Move forward, stop when front range < 0.5 m\npython3 {baseDir}/scripts/ros2_cli.py topics publish-until <VEL_TOPIC> \\\n  '{\"linear\":{\"x\":0.2,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}' \\\n  --monitor <SCAN_TOPIC> --field ranges.0 --below 0.5 --timeout 30\n\n\nLimitation: A single publish-until call can only monitor one topic — you cannot simultaneously stop at a fixed distance AND stop on obstacle detection. If both are needed, set a conservative --timeout as an outer bound and use the most safety-critical condition as the --monitor. For precise distance, use odometry; for collision avoidance, use LaserScan.\n\nCase B — Rotation specified, odometry available → publish-until --rotate (closed loop)\n\n--rotate automatically extracts yaw from the odometry quaternion and handles angle wraparound. There is no --yaw flag. Do not use --field for rotation.\n\nDirection convention — --rotate sign and angular.z sign must always agree:\n\nDirection\t--rotate\tangular.z\nLeft / CCW\tpositive (e.g. --rotate 90 --degrees)\tpositive (e.g. 0.5)\nRight / CW\tnegative (e.g. --rotate -90 --degrees)\tnegative (e.g. -0.5)\n\n--rotate tells the monitor how far and in which direction to track. angular.z tells the robot which way to spin. They must match — a positive --rotate with negative angular.z means the robot turns CW while the monitor waits for CCW accumulation and will never stop.\n\nBefore constructing any rotation command, run --help to confirm accepted flags:\n\npython3 {baseDir}/scripts/ros2_cli.py topics publish-until --help\n\n\nDo not proceed until you have verified the flag names from the --help output. Never invent or assume flags.\n\n# Step 1 result: VEL_TOPIC = <discovered>\n# Step 2 result: ODOM_TOPIC = <discovered>\n\n# Rotate 90 degrees counter-clockwise — positive --rotate, positive angular.z (Twist)\npython3 {baseDir}/scripts/ros2_cli.py topics publish-until <VEL_TOPIC> \\\n  '{\"linear\":{\"x\":0,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0.5}}' \\\n  --monitor <ODOM_TOPIC> --rotate 90 --degrees --timeout 30\n\n# Rotate 45 degrees clockwise — negative --rotate, negative angular.z (Twist)\npython3 {baseDir}/scripts/ros2_cli.py topics publish-until <VEL_TOPIC> \\\n  '{\"linear\":{\"x\":0,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":-0.5}}' \\\n  --monitor <ODOM_TOPIC> --rotate -45 --degrees --timeout 30\n\n# Rotate 1.57 radians CCW (TwistStamped variant)\npython3 {baseDir}/scripts/ros2_cli.py topics publish-until <VEL_TOPIC> \\\n  '{\"header\":{\"stamp\":{\"sec\":0},\"frame_id\":\"\"},\"twist\":{\"linear\":{\"x\":0,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0.5}}}' \\\n  --monitor <ODOM_TOPIC> --rotate 1.5708 --timeout 30\n\nCase C — Open-ended movement (no distance/angle) → publish-sequence\n\nAlways include a zero-velocity stop as the final message. Use the payload structure matching the confirmed type from Step 1.\n\n# Step 1 result: VEL_TOPIC = <discovered>\n# Type check: geometry_msgs/msg/Twist → Twist payload\n\n# Drive forward for 3 seconds then stop (Twist)\npython3 {baseDir}/scripts/ros2_cli.py topics publish-sequence <VEL_TOPIC> \\\n  '[{\"linear\":{\"x\":0.2,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}},{\"linear\":{\"x\":0,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}]' \\\n  '[3.0, 0.5]'\n\n# Drive forward for 3 seconds then stop (TwistStamped variant)\npython3 {baseDir}/scripts/ros2_cli.py topics publish-sequence <VEL_TOPIC> \\\n  '[{\"header\":{\"stamp\":{\"sec\":0},\"frame_id\":\"\"},\"twist\":{\"linear\":{\"x\":0.2,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}},{\"header\":{\"stamp\":{\"sec\":0},\"frame_id\":\"\"},\"twist\":{\"linear\":{\"x\":0,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}}]' \\\n  '[3.0, 0.5]'\n\nCase D — Distance or angle specified, but odometry NOT available → publish-sequence (open loop)\n\nTell the user before executing: \"No odometry topic found. Running open-loop — distance/angle accuracy is not guaranteed.\" Then estimate duration = distance / velocity and use publish-sequence with a stop.\n\n# Step 1 result: VEL_TOPIC = <discovered>\n# Step 2 result: no odometry found\n\n# Estimate: 1 m at 0.2 m/s ≈ 5 s (Twist)\npython3 {baseDir}/scripts/ros2_cli.py topics publish-sequence <VEL_TOPIC> \\\n  '[{\"linear\":{\"x\":0.2,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}},{\"linear\":{\"x\":0,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}]' \\\n  '[5.0, 0.5]'\n\n# Estimate: 1 m at 0.2 m/s ≈ 5 s (TwistStamped variant)\npython3 {baseDir}/scripts/ros2_cli.py topics publish-sequence <VEL_TOPIC> \\\n  '[{\"header\":{\"stamp\":{\"sec\":0},\"frame_id\":\"\"},\"twist\":{\"linear\":{\"x\":0.2,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}},{\"header\":{\"stamp\":{\"sec\":0},\"frame_id\":\"\"},\"twist\":{\"linear\":{\"x\":0,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}}]' \\\n  '[5.0, 0.5]'\n\nStep 4 — Report completion\n\nAfter publish-until or publish-sequence completes, capture the end position and report to the user:\n\npython3 {baseDir}/scripts/ros2_cli.py topics subscribe <ODOM_TOPIC> --max-messages 1\n\n\nCompare with the start pose from Step 2.5. By default, report only: \"Done. Moved X.XX m.\" (or \"Done. Rotated X deg.\") and any errors or anomalies. Report detailed topic selections, odometry values, and per-step data only if the user explicitly asked for verbose output.\n\nQuick Examples\n1. Explore a Robot System\npython3 {baseDir}/scripts/ros2_cli.py version\npython3 {baseDir}/scripts/ros2_cli.py topics list\npython3 {baseDir}/scripts/ros2_cli.py nodes list\npython3 {baseDir}/scripts/ros2_cli.py services list\n\n2. Move a Robot\n\nSee the Movement Workflow section — it covers all cases with complete examples.\n\n3. Read Sensors\npython3 {baseDir}/scripts/ros2_cli.py topics find sensor_msgs/msg/LaserScan\n# → subscribe to the discovered topic name, not /scan\npython3 {baseDir}/scripts/ros2_cli.py topics subscribe <LASER_TOPIC> --duration 3\n\n4. Call a Service\n# Step 1: Discover available services\npython3 {baseDir}/scripts/ros2_cli.py services list\n\n# Step 2: Get request/response structure for the service you want to call\npython3 {baseDir}/scripts/ros2_cli.py services details <DISCOVERED_SERVICE>\n\n# Step 3: Call with properly-structured payload\npython3 {baseDir}/scripts/ros2_cli.py services call <DISCOVERED_SERVICE> '<json_payload>'\n\n5. Send an Action\n# Step 1: Discover available actions\npython3 {baseDir}/scripts/ros2_cli.py actions list\n\n# Step 2: Get goal/result structure for the action you want to send\npython3 {baseDir}/scripts/ros2_cli.py actions details <DISCOVERED_ACTION>\n\n# Step 3: Send goal with properly-structured payload\npython3 {baseDir}/scripts/ros2_cli.py actions send <DISCOVERED_ACTION> '<json_goal>'\n\n6. Move Forward N Meters / Rotate N Degrees\n\nSee the Movement Workflow section — Cases A and B cover distance and rotation with full examples.\n\nSafety Notes\n\nDestructive commands (can move the robot or change state):\n\ntopics publish / topics publish-sequence — sends movement or control commands\ntopics publish-until — publishes continuously until a condition or timeout; always specify a conservative --timeout\ntopics publish-continuous — alias for topics publish; --duration / --timeout is optional (single-shot without it)\nservices call — can reset, spawn, kill, or change robot state\nparams set — modifies runtime parameters\nactions send — triggers robot actions (rotation, navigation, etc.)\ncontrol set-controller-state / control switch-controllers — activating or deactivating controllers affects what the robot can do\ncontrol set-hardware-component-state — driving hardware through lifecycle states can stop actuators or sensors\ncontrol reload-controller-libraries — stops all running controllers if --force-kill is used\n\nAlways stop the robot after movement. The last message in any publish-sequence should be all zeros, using the payload structure that matches the confirmed type (Twist or TwistStamped — from your topics type introspection step):\n\n// Twist stop\n{\"linear\":{\"x\":0,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}\n// TwistStamped stop\n{\"header\":{\"stamp\":{\"sec\":0},\"frame_id\":\"\"},\"twist\":{\"linear\":{\"x\":0,\"y\":0,\"z\":0},\"angular\":{\"x\":0,\"y\":0,\"z\":0}}}\n\n\nAlways check JSON output for errors before proceeding.\n\nUse auto-discovery to avoid errors. Before any publish/call/send operation:\n\nUse topics find, services find, actions find to locate relevant endpoints\nUse topics type, services type, actions type to get message types\nUse topics message, services details, actions details to understand field structures\n\nNever ask the user for topic names or message types — discover them from the live ROS 2 graph.\n\nTroubleshooting\nAgent Self-Correction Rules\nIf Agent Asks...\tCorrection\n\"What topic should I use?\"\tUse topics find geometry_msgs/Twist (and TwistStamped)\n\"What message type?\"\tUse topics type <topic> to get it from the graph\n\"How do I know the message structure?\"\tUse topics message <type> or interface show <type>\n\"What are the velocity limits?\"\tUse params list on controller nodes\n\"Do I need odometry?\"\tAlways check with topics find nav_msgs/Odometry first\n\"Is there a service called /X?\"\tUse services list to verify it exists\n\"How do I monitor yaw / heading?\"\tUse --rotate N --degrees (or radians). There is no --yaw or --yaw-delta flag. --rotate handles all yaw tracking internally.\n\"Should I use --field for rotation?\"\tNo. Never use --field for rotation. Use --rotate.\n\"How do I rotate clockwise / right?\"\tUse --rotate -90 --degrees (negative angle) with angular.z: -0.5 (negative velocity). Sign of --rotate and angular.z must always match.\n\"Can --rotate be negative?\"\tYes. Negative = CW. Positive = CCW. Zero is the only invalid value.\n\"What controller parameters exist?\"\tUse params list <controller_node>\nTechnical Troubleshooting\nProblem\tCause\tSolution\nMissing ROS 2 dependency: No module named 'X'\tA required ROS 2 package is not installed\tSource ROS 2: source /opt/ros/${ROS_DISTRO}/setup.bash; then install: sudo apt install ros-${ROS_DISTRO}-<package>\nrclpy not installed\trclpy missing or wrong Python version\tSource ROS 2 setup.bash; if Python version mismatch, run with python3.12 instead of python3\nROS 2 not sourced\tEnvironment not set up\tRun: source /opt/ros/${ROS_DISTRO}/setup.bash\nNo topics found\tROS nodes not running\tEnsure nodes are launched and workspace is sourced\nService not found\tService not available\tUse services list to see available services\nParameter commands fail\tNode doesn't have parameters\tSome nodes don't expose parameters\nAction commands fail\tAction server not available\tUse actions list to see available actions\nInvalid JSON error\tMalformed message\tValidate JSON before passing (watch for single vs double quotes)\nSubscribe timeout\tNo publisher on topic\tCheck topics details to verify publishers exist\npublish-sequence length error\tArray mismatch\tmessages and durations arrays must have the same length\npublish-until hangs / no feedback\tWrong monitor topic or field\tUse Step 2–4 of the Goal-Oriented workflow to verify topic and field\nController manager service not available\tros2_control not running or wrong namespace\tCheck with nodes list; use --controller-manager to set the correct namespace"
  },
  "trust": {
    "sourceLabel": "tencent",
    "provenanceUrl": "https://clawhub.ai/adityakamath/ros2-skill",
    "publisherUrl": "https://clawhub.ai/adityakamath/ros2-skill",
    "owner": "adityakamath",
    "version": "1.0.4",
    "license": null,
    "verificationStatus": "Indexed source record"
  },
  "links": {
    "detailUrl": "https://openagent3.xyz/skills/ros2-skill",
    "downloadUrl": "https://openagent3.xyz/downloads/ros2-skill",
    "agentUrl": "https://openagent3.xyz/skills/ros2-skill/agent",
    "manifestUrl": "https://openagent3.xyz/skills/ros2-skill/agent.json",
    "briefUrl": "https://openagent3.xyz/skills/ros2-skill/agent.md"
  }
}