agenticros 0.1.18 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/dist/__tests__/robot-config.test.d.ts +2 -0
  2. package/dist/__tests__/robot-config.test.d.ts.map +1 -0
  3. package/dist/__tests__/robot-config.test.js +374 -0
  4. package/dist/__tests__/robot-config.test.js.map +1 -0
  5. package/dist/__tests__/transport-shorthand.test.d.ts +2 -0
  6. package/dist/__tests__/transport-shorthand.test.d.ts.map +1 -0
  7. package/dist/__tests__/transport-shorthand.test.js +88 -0
  8. package/dist/__tests__/transport-shorthand.test.js.map +1 -0
  9. package/dist/commands/robots.d.ts +57 -0
  10. package/dist/commands/robots.d.ts.map +1 -0
  11. package/dist/commands/robots.js +678 -0
  12. package/dist/commands/robots.js.map +1 -0
  13. package/dist/commands/skills.d.ts.map +1 -1
  14. package/dist/commands/skills.js +125 -2
  15. package/dist/commands/skills.js.map +1 -1
  16. package/dist/index.js +29 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/menu.js +22 -4
  19. package/dist/menu.js.map +1 -1
  20. package/dist/util/marketplace.d.ts +57 -0
  21. package/dist/util/marketplace.d.ts.map +1 -0
  22. package/dist/util/marketplace.js +144 -0
  23. package/dist/util/marketplace.js.map +1 -0
  24. package/dist/util/mcp-discovery.d.ts +57 -0
  25. package/dist/util/mcp-discovery.d.ts.map +1 -0
  26. package/dist/util/mcp-discovery.js +153 -0
  27. package/dist/util/mcp-discovery.js.map +1 -0
  28. package/dist/util/robot-config.d.ts +167 -0
  29. package/dist/util/robot-config.d.ts.map +1 -0
  30. package/dist/util/robot-config.js +375 -0
  31. package/dist/util/robot-config.js.map +1 -0
  32. package/dist/util/skills.d.ts +6 -6
  33. package/dist/util/skills.d.ts.map +1 -1
  34. package/dist/util/skills.js +14 -9
  35. package/dist/util/skills.js.map +1 -1
  36. package/dist/util/transport-shorthand.d.ts +66 -0
  37. package/dist/util/transport-shorthand.d.ts.map +1 -0
  38. package/dist/util/transport-shorthand.js +102 -0
  39. package/dist/util/transport-shorthand.js.map +1 -0
  40. package/package.json +12 -10
  41. package/runtime/BUNDLE.json +1 -1
  42. package/runtime/README.md +62 -1
  43. package/runtime/package.json +6 -1
  44. package/runtime/packages/agenticros/openclaw.plugin.json +7 -1
  45. package/runtime/packages/agenticros/package.json +2 -1
  46. package/runtime/packages/agenticros/src/__tests__/capabilities-plugin.test.ts +568 -0
  47. package/runtime/packages/agenticros/src/config-page.ts +1 -1
  48. package/runtime/packages/agenticros/src/mission-registry.ts +22 -0
  49. package/runtime/packages/agenticros/src/service.ts +120 -2
  50. package/runtime/packages/agenticros/src/skill-loader.ts +109 -30
  51. package/runtime/packages/agenticros/src/tools/_robot-helpers.ts +68 -0
  52. package/runtime/packages/agenticros/src/tools/index.ts +49 -9
  53. package/runtime/packages/agenticros/src/tools/mission-cancel.ts +70 -0
  54. package/runtime/packages/agenticros/src/tools/ros2-action.ts +11 -4
  55. package/runtime/packages/agenticros/src/tools/ros2-camera.ts +10 -4
  56. package/runtime/packages/agenticros/src/tools/ros2-capabilities.ts +63 -0
  57. package/runtime/packages/agenticros/src/tools/ros2-depth-distance.ts +11 -4
  58. package/runtime/packages/agenticros/src/tools/ros2-discover.ts +65 -0
  59. package/runtime/packages/agenticros/src/tools/ros2-find-robots-for.ts +128 -0
  60. package/runtime/packages/agenticros/src/tools/ros2-mission.ts +311 -0
  61. package/runtime/packages/agenticros/src/tools/ros2-param.ts +20 -7
  62. package/runtime/packages/agenticros/src/tools/ros2-publish.ts +15 -8
  63. package/runtime/packages/agenticros/src/tools/ros2-robots.ts +52 -0
  64. package/runtime/packages/agenticros/src/tools/ros2-service.ts +11 -4
  65. package/runtime/packages/agenticros/src/tools/ros2-subscribe.ts +11 -4
  66. package/runtime/packages/agenticros-claude-code/README.md +52 -9
  67. package/runtime/packages/agenticros-claude-code/dist/__tests__/capabilities-mcp.test.d.ts +2 -0
  68. package/runtime/packages/agenticros-claude-code/dist/__tests__/capabilities-mcp.test.d.ts.map +1 -0
  69. package/runtime/packages/agenticros-claude-code/dist/__tests__/capabilities-mcp.test.js +773 -0
  70. package/runtime/packages/agenticros-claude-code/dist/__tests__/capabilities-mcp.test.js.map +1 -0
  71. package/runtime/packages/agenticros-claude-code/dist/__tests__/follow-me-registry.test.d.ts +2 -0
  72. package/runtime/packages/agenticros-claude-code/dist/__tests__/follow-me-registry.test.d.ts.map +1 -0
  73. package/runtime/packages/agenticros-claude-code/dist/__tests__/follow-me-registry.test.js +97 -0
  74. package/runtime/packages/agenticros-claude-code/dist/__tests__/follow-me-registry.test.js.map +1 -0
  75. package/runtime/packages/agenticros-claude-code/dist/find-object/find-object.d.ts +2 -2
  76. package/runtime/packages/agenticros-claude-code/dist/find-object/find-object.d.ts.map +1 -1
  77. package/runtime/packages/agenticros-claude-code/dist/find-object/find-object.js +5 -5
  78. package/runtime/packages/agenticros-claude-code/dist/find-object/find-object.js.map +1 -1
  79. package/runtime/packages/agenticros-claude-code/dist/follow-me/depth-loop.d.ts +109 -0
  80. package/runtime/packages/agenticros-claude-code/dist/follow-me/depth-loop.d.ts.map +1 -0
  81. package/runtime/packages/agenticros-claude-code/dist/follow-me/depth-loop.js +392 -0
  82. package/runtime/packages/agenticros-claude-code/dist/follow-me/depth-loop.js.map +1 -0
  83. package/runtime/packages/agenticros-claude-code/dist/follow-me/loop.d.ts +8 -3
  84. package/runtime/packages/agenticros-claude-code/dist/follow-me/loop.d.ts.map +1 -1
  85. package/runtime/packages/agenticros-claude-code/dist/follow-me/loop.js +30 -12
  86. package/runtime/packages/agenticros-claude-code/dist/follow-me/loop.js.map +1 -1
  87. package/runtime/packages/agenticros-claude-code/dist/index.js +5 -5
  88. package/runtime/packages/agenticros-claude-code/dist/index.js.map +1 -1
  89. package/runtime/packages/agenticros-claude-code/dist/mission-registry.d.ts +16 -0
  90. package/runtime/packages/agenticros-claude-code/dist/mission-registry.d.ts.map +1 -0
  91. package/runtime/packages/agenticros-claude-code/dist/mission-registry.js +19 -0
  92. package/runtime/packages/agenticros-claude-code/dist/mission-registry.js.map +1 -0
  93. package/runtime/packages/agenticros-claude-code/dist/tools.d.ts +9 -0
  94. package/runtime/packages/agenticros-claude-code/dist/tools.d.ts.map +1 -1
  95. package/runtime/packages/agenticros-claude-code/dist/tools.js +700 -54
  96. package/runtime/packages/agenticros-claude-code/dist/tools.js.map +1 -1
  97. package/runtime/packages/agenticros-claude-code/dist/transport.d.ts +28 -8
  98. package/runtime/packages/agenticros-claude-code/dist/transport.d.ts.map +1 -1
  99. package/runtime/packages/agenticros-claude-code/dist/transport.js +37 -34
  100. package/runtime/packages/agenticros-claude-code/dist/transport.js.map +1 -1
  101. package/runtime/packages/agenticros-claude-code/package.json +2 -1
  102. package/runtime/packages/agenticros-claude-code/src/__tests__/capabilities-mcp.test.ts +965 -0
  103. package/runtime/packages/agenticros-claude-code/src/__tests__/follow-me-registry.test.ts +119 -0
  104. package/runtime/packages/agenticros-claude-code/src/find-object/find-object.ts +7 -6
  105. package/runtime/packages/agenticros-claude-code/src/follow-me/depth-loop.ts +41 -9
  106. package/runtime/packages/agenticros-claude-code/src/follow-me/loop.ts +40 -11
  107. package/runtime/packages/agenticros-claude-code/src/index.ts +5 -5
  108. package/runtime/packages/agenticros-claude-code/src/mission-registry.ts +21 -0
  109. package/runtime/packages/agenticros-claude-code/src/tools.ts +668 -56
  110. package/runtime/packages/agenticros-claude-code/src/transport.ts +43 -43
  111. package/runtime/packages/agenticros-gemini/package.json +1 -0
  112. package/runtime/packages/agenticros-gemini/src/__tests__/mission-cancel.test.ts +197 -0
  113. package/runtime/packages/agenticros-gemini/src/__tests__/mission-transcripts.test.ts +178 -0
  114. package/runtime/packages/agenticros-gemini/src/tools.ts +504 -21
  115. package/runtime/packages/agenticros-gemini/src/transport.ts +40 -43
  116. package/runtime/packages/core/LICENSE +192 -0
  117. package/runtime/packages/core/README.md +88 -0
  118. package/runtime/packages/core/package.json +36 -3
  119. package/runtime/packages/core/src/__tests__/capabilities.test.ts +266 -0
  120. package/runtime/packages/core/src/__tests__/discovery.test.ts +206 -0
  121. package/runtime/packages/core/src/__tests__/find-robots-for.test.ts +258 -0
  122. package/runtime/packages/core/src/__tests__/mission-cancel-transcript.test.ts +371 -0
  123. package/runtime/packages/core/src/__tests__/mission-transcript-sink.test.ts +189 -0
  124. package/runtime/packages/core/src/__tests__/mission.test.ts +501 -0
  125. package/runtime/packages/core/src/__tests__/planner.test.ts +279 -0
  126. package/runtime/packages/core/src/__tests__/robots.test.ts +406 -0
  127. package/runtime/packages/core/src/__tests__/topic-utils.test.ts +105 -0
  128. package/runtime/packages/core/src/__tests__/transport-pool.test.ts +295 -0
  129. package/runtime/packages/core/src/capabilities.ts +314 -0
  130. package/runtime/packages/core/src/config.ts +221 -39
  131. package/runtime/packages/core/src/discovery.ts +193 -0
  132. package/runtime/packages/core/src/find-robots-for.ts +164 -0
  133. package/runtime/packages/core/src/index.ts +67 -1
  134. package/runtime/packages/core/src/mission-registry.ts +142 -0
  135. package/runtime/packages/core/src/mission-transcript-sink.ts +81 -0
  136. package/runtime/packages/core/src/mission.ts +574 -0
  137. package/runtime/packages/core/src/planner/index.ts +564 -0
  138. package/runtime/packages/core/src/robots.ts +282 -0
  139. package/runtime/packages/core/src/topic-utils.ts +47 -9
  140. package/runtime/packages/core/src/transport-pool.ts +171 -0
  141. package/runtime/ros2_ws/src/agenticros_discovery/agenticros_discovery/discovery_node.py +108 -13
  142. package/runtime/ros2_ws/src/agenticros_msgs/CMakeLists.txt +1 -0
  143. package/runtime/ros2_ws/src/agenticros_msgs/msg/RobotInfo.msg +44 -0
  144. package/runtime/scripts/__tests__/refresh-skill-deps.test.mjs +222 -0
  145. package/runtime/scripts/configure_agenticros.sh +0 -0
  146. package/runtime/scripts/configure_for_sim.sh +0 -0
  147. package/runtime/scripts/fix-openclaw-control-ui-path.sh +0 -0
  148. package/runtime/scripts/install_cli.sh +0 -0
  149. package/runtime/scripts/install_rosbridge_from_source.sh +0 -0
  150. package/runtime/scripts/onboard_robot.sh +0 -0
  151. package/runtime/scripts/pack-runtime.mjs +0 -0
  152. package/runtime/scripts/refresh-skill-deps.mjs +381 -0
  153. package/runtime/scripts/run_demo_native.sh +0 -0
  154. package/runtime/scripts/run_nemoclaw_host_stack.sh +0 -0
  155. package/runtime/scripts/run_robot_rosbridge.sh +0 -0
  156. package/runtime/scripts/setup_gateway_plugin.sh +32 -5
  157. package/runtime/scripts/setup_robot.sh +0 -0
  158. package/runtime/scripts/setup_workspace.sh +0 -0
  159. package/runtime/scripts/sim/run_sim.sh +0 -0
  160. package/runtime/scripts/smoke_test_nemoclaw.sh +0 -0
  161. package/runtime/scripts/start_demo.sh +0 -0
  162. package/runtime/scripts/sync-skill-tools.mjs +69 -12
  163. package/runtime/scripts/use-openclaw-2026.2.26.sh +0 -0
  164. package/runtime/scripts/use-openclaw-2026.3.11.sh +0 -0
@@ -0,0 +1,568 @@
1
+ /**
2
+ * Smoke test for the AgenticROS OpenClaw plugin.
3
+ *
4
+ * Loads the compiled plugin via require() — the same path OpenClaw's plugin
5
+ * host uses — and asserts the Phase 1 capability surface registers correctly:
6
+ *
7
+ * 1. plugin.register() returns synchronously with all 10 base tools
8
+ * registered (no Promise tail that would drop tools from OpenClaw's
9
+ * sync snapshot).
10
+ * 2. `ros2_list_capabilities` is one of those tools.
11
+ * 3. Its execute() returns a well-formed payload containing the 6
12
+ * intrinsic verbs in BUILTIN_CAPABILITIES.
13
+ * 4. The plugin manifest's contracts.tools allowlist includes
14
+ * `ros2_list_capabilities` — without this, OpenClaw's tool profile
15
+ * would silently filter it out.
16
+ *
17
+ * This is the regression net for the cascade we hit on 2026-06-10 where a
18
+ * stale plugin-deploy meant the tool registered fine in the workspace but
19
+ * not in the live gateway, and `sync-skill-tools.mjs` separately
20
+ * overwrote the manifest because its hardcoded CORE_TOOLS list lagged the
21
+ * actual core toolset.
22
+ */
23
+ import { test } from "node:test";
24
+ import assert from "node:assert/strict";
25
+ import { createRequire } from "node:module";
26
+ import { readFileSync } from "node:fs";
27
+ import { join, dirname } from "node:path";
28
+ import { fileURLToPath } from "node:url";
29
+
30
+ const __filename = fileURLToPath(import.meta.url);
31
+ const __dirname = dirname(__filename);
32
+
33
+ // Walk from dist/__tests__/ up to the agenticros package root (workspace),
34
+ // regardless of whether the test is run from the workspace or from a deploy.
35
+ // dist/__tests__/foo.test.js -> dist/__tests__ -> dist -> packages/agenticros
36
+ const PKG_ROOT = join(__dirname, "..", "..");
37
+ const PLUGIN_ENTRY = join(PKG_ROOT, "dist", "index.js");
38
+ const MANIFEST = join(PKG_ROOT, "openclaw.plugin.json");
39
+
40
+ interface CapturedTool {
41
+ name: string;
42
+ execute?: (toolCallId: string, params: Record<string, unknown>) => Promise<unknown>;
43
+ }
44
+
45
+ function makeStubApi(): {
46
+ api: Record<string, unknown>;
47
+ captured: {
48
+ tools: CapturedTool[];
49
+ httpRoutes: unknown[];
50
+ commands: unknown[];
51
+ contexts: unknown[];
52
+ hooks: unknown[];
53
+ events: Record<string, unknown[]>;
54
+ };
55
+ } {
56
+ const captured = {
57
+ tools: [] as CapturedTool[],
58
+ httpRoutes: [] as unknown[],
59
+ commands: [] as unknown[],
60
+ contexts: [] as unknown[],
61
+ hooks: [] as unknown[],
62
+ events: {} as Record<string, unknown[]>,
63
+ };
64
+ const logger = {
65
+ info: () => {},
66
+ warn: () => {},
67
+ error: () => {},
68
+ debug: () => {},
69
+ };
70
+ return {
71
+ captured,
72
+ api: {
73
+ pluginConfig: {},
74
+ logger,
75
+ registerTool: (def: CapturedTool) => captured.tools.push(def),
76
+ registerHook: (h: unknown) => captured.hooks.push(h),
77
+ registerCommand: (c: unknown) => captured.commands.push(c),
78
+ registerContext: (c: unknown) => captured.contexts.push(c),
79
+ registerHttpRoute: (r: unknown) => captured.httpRoutes.push(r),
80
+ registerService: () => {},
81
+ on: (eventName: string, handler: unknown) => {
82
+ (captured.events[eventName] ??= []).push(handler);
83
+ },
84
+ off: () => {},
85
+ emit: () => {},
86
+ },
87
+ };
88
+ }
89
+
90
+ interface PluginShape {
91
+ id: string;
92
+ name: string;
93
+ register: (api: Record<string, unknown>) => void | Promise<void>;
94
+ }
95
+
96
+ function loadPlugin(): PluginShape {
97
+ const req = createRequire(import.meta.url);
98
+ const mod = req(PLUGIN_ENTRY) as { default?: PluginShape } & PluginShape;
99
+ const plugin = mod.default ?? mod;
100
+ return plugin;
101
+ }
102
+
103
+ test("plugin: register() is synchronous and produces the base 15-tool surface", () => {
104
+ const plugin = loadPlugin();
105
+ assert.equal(plugin.id, "agenticros");
106
+ assert.equal(plugin.name, "AgenticROS");
107
+ assert.equal(typeof plugin.register, "function");
108
+
109
+ const { api, captured } = makeStubApi();
110
+ const result = plugin.register(api);
111
+
112
+ // register() must return undefined (not a Promise) so OpenClaw's
113
+ // captured.tools snapshot includes everything.
114
+ assert.equal(result, undefined, "register() must be synchronous (returning Promise drops tools from OpenClaw snapshot)");
115
+
116
+ const toolNames = captured.tools.map((t) => t.name);
117
+ const expected = [
118
+ "ros2_publish",
119
+ "ros2_subscribe_once",
120
+ "ros2_service_call",
121
+ "ros2_action_goal",
122
+ "ros2_param_get",
123
+ "ros2_param_set",
124
+ "ros2_list_topics",
125
+ "ros2_camera_snapshot",
126
+ "ros2_depth_distance",
127
+ "ros2_list_capabilities",
128
+ "ros2_list_robots",
129
+ "ros2_discover_robots",
130
+ "ros2_find_robots_for",
131
+ "run_mission",
132
+ // Phase 1.f — cancel an in-flight mission by id.
133
+ "mission_cancel",
134
+ ];
135
+ for (const name of expected) {
136
+ assert.ok(toolNames.includes(name), `Expected base tool ${name} in registered set`);
137
+ }
138
+ });
139
+
140
+ test("plugin: ros2_list_capabilities is registered with an execute() function", () => {
141
+ const { api, captured } = makeStubApi();
142
+ loadPlugin().register(api);
143
+ const tool = captured.tools.find((t) => t.name === "ros2_list_capabilities");
144
+ assert.ok(tool, "ros2_list_capabilities should be registered");
145
+ assert.equal(typeof tool.execute, "function", "registered tool must expose execute()");
146
+ });
147
+
148
+ test("plugin: ros2_list_capabilities.execute() returns the 6 intrinsic verbs", async () => {
149
+ const { api, captured } = makeStubApi();
150
+ loadPlugin().register(api);
151
+ const tool = captured.tools.find((t) => t.name === "ros2_list_capabilities");
152
+ assert.ok(tool?.execute);
153
+ const result = (await tool.execute("test-call", {})) as {
154
+ content: Array<{ type: string; text: string }>;
155
+ details: Record<string, unknown>;
156
+ };
157
+ assert.ok(result.content?.[0]?.text, "execute() must return text content");
158
+
159
+ const payload = JSON.parse(result.content[0].text) as {
160
+ success: boolean;
161
+ total: number;
162
+ intrinsic_count: number;
163
+ skill_count: number;
164
+ capabilities: Array<{ id: string; verb: string; source?: { kind: string } }>;
165
+ };
166
+ assert.equal(payload.success, true);
167
+ assert.ok(payload.total >= 6, `total should be at least 6 (intrinsic floor), got ${payload.total}`);
168
+ assert.equal(payload.intrinsic_count, 6, "should expose exactly 6 intrinsic verbs");
169
+
170
+ const intrinsicIds = payload.capabilities
171
+ .filter((c) => c.source?.kind === "builtin")
172
+ .map((c) => c.id)
173
+ .sort();
174
+ assert.deepEqual(intrinsicIds, [
175
+ "drive_base",
176
+ "list_topics",
177
+ "measure_depth",
178
+ "publish_topic",
179
+ "subscribe_once",
180
+ "take_snapshot",
181
+ ]);
182
+
183
+ // details payload should mirror the text payload (OpenClaw uses richer
184
+ // results when the host requests them; should not disagree with text).
185
+ assert.equal(payload.total, result.details.total);
186
+ assert.equal(payload.intrinsic_count, result.details.intrinsic_count);
187
+ });
188
+
189
+ test("manifest: contracts.tools allowlists ros2_list_capabilities", () => {
190
+ const manifest = JSON.parse(readFileSync(MANIFEST, "utf-8")) as {
191
+ contracts: { tools: string[] };
192
+ };
193
+ assert.ok(
194
+ Array.isArray(manifest.contracts.tools),
195
+ "manifest.contracts.tools must be an array",
196
+ );
197
+ assert.ok(
198
+ manifest.contracts.tools.includes("ros2_list_capabilities"),
199
+ "ros2_list_capabilities must be in contracts.tools — otherwise OpenClaw tool profiles will filter it out",
200
+ );
201
+ });
202
+
203
+ test("manifest: every registered base tool is also in contracts.tools", () => {
204
+ const { api, captured } = makeStubApi();
205
+ loadPlugin().register(api);
206
+ const registered = captured.tools.map((t) => t.name);
207
+
208
+ const manifest = JSON.parse(readFileSync(MANIFEST, "utf-8")) as {
209
+ contracts: { tools: string[] };
210
+ };
211
+ const allowlist = new Set(manifest.contracts.tools);
212
+
213
+ for (const name of registered) {
214
+ assert.ok(
215
+ allowlist.has(name),
216
+ `Tool ${name} is registered by the plugin but missing from contracts.tools — OpenClaw will silently drop it`,
217
+ );
218
+ }
219
+ });
220
+
221
+ test("plugin: ros2_list_capabilities with unknown robot_id returns a clean tool error (Phase 1.d)", async () => {
222
+ const { api, captured } = makeStubApi();
223
+ loadPlugin().register(api);
224
+ const tool = captured.tools.find((t) => t.name === "ros2_list_capabilities");
225
+ assert.ok(tool?.execute);
226
+ const result = (await tool.execute("test-call", { robot_id: "no-such-robot" })) as {
227
+ content: Array<{ type: string; text: string }>;
228
+ details: { success?: boolean; error?: string };
229
+ };
230
+ assert.equal(result.details.success, false, "unknown robot_id must surface success:false");
231
+ assert.ok(result.details.error?.includes("no-such-robot"));
232
+ assert.ok(
233
+ result.details.error?.toLowerCase().includes("ros2_list_robots"),
234
+ "unknown robot_id error should recommend ros2_list_robots",
235
+ );
236
+ });
237
+
238
+ test("plugin: ros2_publish exposes robot_id in its parameter schema (Phase 1.d)", () => {
239
+ const { api, captured } = makeStubApi();
240
+ loadPlugin().register(api);
241
+ const tool = captured.tools.find((t) => t.name === "ros2_publish") as
242
+ | (CapturedTool & { parameters?: { properties?: Record<string, unknown> } })
243
+ | undefined;
244
+ assert.ok(tool, "ros2_publish must be registered");
245
+ const props = tool.parameters?.properties ?? {};
246
+ assert.ok(
247
+ Object.prototype.hasOwnProperty.call(props, "robot_id"),
248
+ `ros2_publish should advertise robot_id in its TypeBox schema; got: ${Object.keys(props).join(", ")}`,
249
+ );
250
+ });
251
+
252
+ test("plugin: ros2_find_robots_for is registered and execute() returns a ranked match list (Phase 1.e)", async () => {
253
+ // Pins the new fleet-filter tool into the OpenClaw plugin's
254
+ // synchronous register() snapshot. Executes it with an empty query
255
+ // — that should return the whole fleet (legacy fallback = 1 robot)
256
+ // with online=null on every match (no online filter was applied).
257
+ const { api, captured } = makeStubApi();
258
+ loadPlugin().register(api);
259
+ const tool = captured.tools.find((t) => t.name === "ros2_find_robots_for") as
260
+ | (CapturedTool & { parameters?: { properties?: Record<string, unknown> } })
261
+ | undefined;
262
+ assert.ok(tool, "ros2_find_robots_for must be registered");
263
+ // All three filter axes need to appear in the TypeBox schema so the
264
+ // agent can see them via tools/list.
265
+ const props = tool!.parameters?.properties ?? {};
266
+ assert.ok(props["capability"], "must advertise 'capability' parameter");
267
+ assert.ok(props["kind"], "must advertise 'kind' parameter");
268
+ assert.ok(props["online"], "must advertise 'online' parameter");
269
+
270
+ const result = (await tool!.execute!("test-call", {})) as {
271
+ content: Array<{ type: string; text: string }>;
272
+ details: {
273
+ success: boolean;
274
+ total: number;
275
+ query: Record<string, unknown>;
276
+ robots: Array<{ id: string; online: boolean | null; kind: string }>;
277
+ };
278
+ };
279
+ assert.equal(result.details.success, true);
280
+ assert.ok(result.details.total >= 1, "empty query should return >=1 robot from the fleet");
281
+ for (const r of result.details.robots) {
282
+ assert.equal(r.online, null, "online must be null when 'online' filter is omitted");
283
+ }
284
+ });
285
+
286
+ test("plugin: ros2_find_robots_for with bogus capability returns total=0 (no false positives)", async () => {
287
+ const { api, captured } = makeStubApi();
288
+ loadPlugin().register(api);
289
+ const tool = captured.tools.find((t) => t.name === "ros2_find_robots_for");
290
+ assert.ok(tool?.execute);
291
+ const result = (await tool.execute!("test-call", {
292
+ capability: "no_such_verb_should_never_exist",
293
+ })) as { details: { success: boolean; total: number } };
294
+ assert.equal(result.details.success, true);
295
+ assert.equal(result.details.total, 0);
296
+ });
297
+
298
+ test("manifest: contracts.tools allowlists ros2_find_robots_for", () => {
299
+ // OpenClaw filters by the manifest allowlist; if we add a tool but
300
+ // forget the contracts entry, the gateway silently drops it from the
301
+ // model's tool list. The cascade we hit on 2026-06-10 motivated this
302
+ // sibling test for every newly-added tool.
303
+ const manifest = JSON.parse(readFileSync(MANIFEST, "utf-8")) as {
304
+ contracts: { tools: string[] };
305
+ };
306
+ assert.ok(manifest.contracts.tools.includes("ros2_find_robots_for"));
307
+ });
308
+
309
+ test("plugin: ros2_list_robots is registered and execute() returns the expected shape", async () => {
310
+ const { api, captured } = makeStubApi();
311
+ loadPlugin().register(api);
312
+ const tool = captured.tools.find((t) => t.name === "ros2_list_robots");
313
+ assert.ok(tool?.execute, "ros2_list_robots should be registered with an execute()");
314
+
315
+ const result = (await tool.execute("test-call", {})) as {
316
+ content: Array<{ type: string; text: string }>;
317
+ details: {
318
+ success: boolean;
319
+ total: number;
320
+ active_robot_id: string;
321
+ robots: Array<{ id: string; name: string; source: string }>;
322
+ };
323
+ };
324
+ assert.equal(result.details.success, true);
325
+ assert.ok(result.details.total >= 1, "should report at least one robot (legacy fallback floor)");
326
+ assert.ok(typeof result.details.active_robot_id === "string" && result.details.active_robot_id.length > 0);
327
+ const active = result.details.robots.find((r) => r.id === result.details.active_robot_id);
328
+ assert.ok(active, "active_robot_id should refer to an entry in robots[]");
329
+ // Text payload should mirror details (every adapter returns the same shape).
330
+ const parsed = JSON.parse(result.content[0].text) as typeof result.details;
331
+ assert.equal(parsed.total, result.details.total);
332
+ assert.equal(parsed.active_robot_id, result.details.active_robot_id);
333
+ });
334
+
335
+ test("plugin: ros2_discover_robots is registered and listed in contracts.tools (Phase 1.d discovery)", () => {
336
+ const { api, captured } = makeStubApi();
337
+ loadPlugin().register(api);
338
+ const tool = captured.tools.find((t) => t.name === "ros2_discover_robots");
339
+ assert.ok(tool, "ros2_discover_robots should be registered by the plugin");
340
+ assert.equal(typeof tool.execute, "function", "ros2_discover_robots must expose execute()");
341
+
342
+ const manifest = JSON.parse(readFileSync(MANIFEST, "utf-8")) as {
343
+ contracts: { tools: string[] };
344
+ };
345
+ assert.ok(
346
+ manifest.contracts.tools.includes("ros2_discover_robots"),
347
+ "ros2_discover_robots must be in contracts.tools — otherwise OpenClaw tool profiles would silently filter it out",
348
+ );
349
+ });
350
+
351
+ test("plugin: run_mission is registered with an execute() function", () => {
352
+ const { api, captured } = makeStubApi();
353
+ loadPlugin().register(api);
354
+ const tool = captured.tools.find((t) => t.name === "run_mission");
355
+ assert.ok(tool, "run_mission should be registered");
356
+ assert.equal(typeof tool.execute, "function", "run_mission must expose execute()");
357
+ });
358
+
359
+ test("plugin: run_mission.execute() rejects malformed input cleanly", async () => {
360
+ const { api, captured } = makeStubApi();
361
+ loadPlugin().register(api);
362
+ const tool = captured.tools.find((t) => t.name === "run_mission");
363
+ assert.ok(tool?.execute);
364
+
365
+ // No mission arg at all — must return a structured error, never throw.
366
+ const noArgs = (await tool.execute("test-call", {})) as {
367
+ content: Array<{ type: string; text: string }>;
368
+ details: { success?: boolean; error?: string };
369
+ };
370
+ assert.ok(noArgs.content?.[0]?.text);
371
+ assert.equal(noArgs.details.success, false);
372
+
373
+ // mission.steps not an array.
374
+ const badSteps = (await tool.execute("test-call", { mission: { steps: "not-an-array" } })) as {
375
+ content: Array<{ type: string; text: string }>;
376
+ details: { success?: boolean; error?: string };
377
+ };
378
+ assert.equal(badSteps.details.success, false);
379
+ });
380
+
381
+ test("plugin: run_mission.execute() succeeds with an empty steps array (no sub-dispatch)", async () => {
382
+ const { api, captured } = makeStubApi();
383
+ loadPlugin().register(api);
384
+ const tool = captured.tools.find((t) => t.name === "run_mission");
385
+ assert.ok(tool?.execute);
386
+
387
+ // Empty mission — no sub-tool calls happen, so no transport needed.
388
+ const result = (await tool.execute("test-call", {
389
+ mission: { name: "noop", steps: [] },
390
+ })) as {
391
+ content: Array<{ type: string; text: string }>;
392
+ details: {
393
+ status: string;
394
+ steps_run: number;
395
+ steps_total: number;
396
+ summary: string;
397
+ };
398
+ };
399
+ assert.equal(result.details.status, "ok");
400
+ assert.equal(result.details.steps_run, 0);
401
+ assert.equal(result.details.steps_total, 0);
402
+ assert.ok(result.details.summary.includes("noop"));
403
+ });
404
+
405
+ // --- Phase 1.f: mission_cancel surface + semantics ---
406
+ //
407
+ // These tests pin the contract that mission_cancel is in the OpenClaw
408
+ // tool surface, behaves idempotently, and that run_mission echoes the
409
+ // mission_id needed to invoke it.
410
+
411
+ test("plugin: mission_cancel is registered with an execute() function (Phase 1.f)", () => {
412
+ const { api, captured } = makeStubApi();
413
+ loadPlugin().register(api);
414
+ const tool = captured.tools.find((t) => t.name === "mission_cancel") as
415
+ | (CapturedTool & { parameters?: { properties?: Record<string, unknown>; required?: string[] } })
416
+ | undefined;
417
+ assert.ok(tool, "mission_cancel must be registered alongside run_mission");
418
+ assert.equal(typeof tool.execute, "function", "mission_cancel must expose execute()");
419
+ const props = tool.parameters?.properties ?? {};
420
+ assert.ok(props["mission_id"], "must advertise 'mission_id' in its TypeBox schema");
421
+ assert.ok(props["reason"], "must advertise optional 'reason'");
422
+ });
423
+
424
+ test("manifest: contracts.tools allowlists mission_cancel (Phase 1.f)", () => {
425
+ const manifest = JSON.parse(readFileSync(MANIFEST, "utf-8")) as {
426
+ contracts: { tools: string[] };
427
+ };
428
+ assert.ok(
429
+ manifest.contracts.tools.includes("mission_cancel"),
430
+ "mission_cancel must be in contracts.tools — otherwise OpenClaw filters it out",
431
+ );
432
+ });
433
+
434
+ test("plugin: mission_cancel.execute() on unknown id returns found=false (no-op, no error)", async () => {
435
+ const { api, captured } = makeStubApi();
436
+ loadPlugin().register(api);
437
+ const tool = captured.tools.find((t) => t.name === "mission_cancel");
438
+ assert.ok(tool?.execute);
439
+ const result = (await tool.execute("test-call", { mission_id: "mn_unknown_xyz" })) as {
440
+ content: Array<{ type: string; text: string }>;
441
+ details: {
442
+ success: boolean;
443
+ mission_id: string;
444
+ found: boolean;
445
+ already_cancelled: boolean;
446
+ reason: string | null;
447
+ };
448
+ };
449
+ assert.equal(result.details.success, true);
450
+ assert.equal(result.details.found, false);
451
+ assert.equal(result.details.already_cancelled, false);
452
+ assert.equal(result.details.mission_id, "mn_unknown_xyz");
453
+ assert.equal(result.details.reason, null);
454
+ // Text payload mirrors details for adapter consistency.
455
+ const parsed = JSON.parse(result.content[0].text) as typeof result.details;
456
+ assert.equal(parsed.found, false);
457
+ });
458
+
459
+ test("plugin: mission_cancel.execute() rejects empty / missing mission_id with success=false", async () => {
460
+ const { api, captured } = makeStubApi();
461
+ loadPlugin().register(api);
462
+ const tool = captured.tools.find((t) => t.name === "mission_cancel");
463
+ assert.ok(tool?.execute);
464
+
465
+ const empty = (await tool.execute("test-call", { mission_id: " " })) as {
466
+ details: { success: boolean; error?: string };
467
+ };
468
+ assert.equal(empty.details.success, false);
469
+ assert.ok(empty.details.error?.toLowerCase().includes("mission_id"));
470
+ });
471
+
472
+ test("plugin: run_mission echoes a mission_id in details so mission_cancel can target it", async () => {
473
+ const { api, captured } = makeStubApi();
474
+ loadPlugin().register(api);
475
+ const tool = captured.tools.find((t) => t.name === "run_mission");
476
+ assert.ok(tool?.execute);
477
+ const result = (await tool.execute("test-call", {
478
+ mission: { name: "yield mission_id", steps: [] },
479
+ })) as {
480
+ details: { status: string; mission_id?: string };
481
+ };
482
+ assert.ok(
483
+ typeof result.details.mission_id === "string" && result.details.mission_id.startsWith("mn_"),
484
+ `run_mission.details must include a mission_id; got: ${JSON.stringify(result.details)}`,
485
+ );
486
+ });
487
+
488
+ // --- Phase 1.g: run_mission { goal } natural-language compile path ---
489
+ //
490
+ // The planner is exercised end-to-end through the OpenClaw tool API —
491
+ // confirms the adapter accepts `goal` as an alternative to `mission`
492
+ // and that compile failures surface a clean { success: false, ... }
493
+ // details payload (so the LLM's tool-result handler can self-correct).
494
+
495
+ test("plugin: run_mission accepts `goal` and emits a compiled plan in details.planner (Phase 1.g)", async () => {
496
+ const { api, captured } = makeStubApi();
497
+ loadPlugin().register(api);
498
+ const tool = captured.tools.find((t) => t.name === "run_mission");
499
+ assert.ok(tool?.execute);
500
+ const result = (await tool.execute("call-1", { goal: "take a picture" })) as {
501
+ details: {
502
+ status: string;
503
+ mission_id?: string;
504
+ planner?: {
505
+ compiled_from_goal: string;
506
+ candidates: Array<{ capability_id: string }>;
507
+ };
508
+ steps?: Array<{ capability: string }>;
509
+ };
510
+ };
511
+ assert.equal(result.details.planner?.compiled_from_goal, "take a picture");
512
+ assert.equal(result.details.planner?.candidates[0].capability_id, "take_snapshot");
513
+ // The compiled step shape must round-trip into the details payload —
514
+ // this is what the agent sees and uses to inspect the run.
515
+ assert.ok((result.details.steps ?? []).length >= 1);
516
+ assert.equal(result.details.steps?.[0].capability, "take_snapshot");
517
+ assert.ok(
518
+ typeof result.details.mission_id === "string" && result.details.mission_id.startsWith("mn_"),
519
+ );
520
+ });
521
+
522
+ test("plugin: run_mission { goal } compound 'find … and drive toward it' compiles to 2 steps (Phase 1.g)", async () => {
523
+ const { api, captured } = makeStubApi();
524
+ loadPlugin().register(api);
525
+ const tool = captured.tools.find((t) => t.name === "run_mission");
526
+ assert.ok(tool?.execute);
527
+ const result = (await tool.execute("call-2", {
528
+ goal: "find a chair and drive toward it",
529
+ })) as {
530
+ details: {
531
+ planner?: { candidates: Array<{ capability_id: string }> };
532
+ steps_total?: number;
533
+ steps?: Array<{ id: string; capability: string }>;
534
+ };
535
+ };
536
+ // The planner ONLY emits find_object when the skill is in the
537
+ // registry. The test plugin runs against the built-in registry
538
+ // only (no fixture skill), so find_object should NOT be emitted —
539
+ // confirming the planner's "no fabricated calls" guarantee.
540
+ // We just assert the response is structured (no crash, status set).
541
+ assert.ok(result.details, "details payload must be present");
542
+ });
543
+
544
+ test("plugin: run_mission { goal: 'paint the wall' } surfaces a compile error in details (Phase 1.g)", async () => {
545
+ const { api, captured } = makeStubApi();
546
+ loadPlugin().register(api);
547
+ const tool = captured.tools.find((t) => t.name === "run_mission");
548
+ assert.ok(tool?.execute);
549
+ const result = (await tool.execute("call-3", { goal: "paint the wall blue" })) as {
550
+ details: { success: boolean; error?: string; suggestions?: string[] };
551
+ };
552
+ assert.equal(result.details.success, false);
553
+ assert.ok(result.details.error);
554
+ assert.ok(Array.isArray(result.details.suggestions) && result.details.suggestions.length > 0);
555
+ });
556
+
557
+ test("plugin: run_mission with NEITHER mission nor goal returns a clear error (Phase 1.g)", async () => {
558
+ const { api, captured } = makeStubApi();
559
+ loadPlugin().register(api);
560
+ const tool = captured.tools.find((t) => t.name === "run_mission");
561
+ assert.ok(tool?.execute);
562
+ const result = (await tool.execute("call-4", {})) as {
563
+ details: { success: boolean; error?: string };
564
+ };
565
+ assert.equal(result.details.success, false);
566
+ assert.ok(result.details.error?.toLowerCase().includes("mission"));
567
+ assert.ok(result.details.error?.toLowerCase().includes("goal"));
568
+ });
@@ -125,7 +125,7 @@ export function getConfigPageHtml(): string {
125
125
  <div class="field">
126
126
  <label for="skillPaths">Skill paths (comma-separated)</label>
127
127
  <input type="text" id="skillPaths" name="skillPaths" placeholder="e.g. /path/to/skills" />
128
- <div class="field-hint">Directories to scan for skill packages (package.json with agenticrosSkill: true).</div>
128
+ <div class="field-hint">Directories to scan for skill packages (package.json with an "agenticros" block).</div>
129
129
  </div>
130
130
  </section>
131
131
  <section>
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Per-process singleton mission registry for the OpenClaw plugin.
3
+ *
4
+ * Each `run_mission` invocation registers a fresh `mission_id` here so
5
+ * a sibling `mission_cancel` tool call can flip the cancellation token
6
+ * mid-run. The runner picks that up at the next step boundary and
7
+ * gracefully marks remaining steps as "cancelled".
8
+ *
9
+ * Lives at the module scope (not per-request) because tool calls
10
+ * dispatch independently — the cancel tool has no other way to find
11
+ * the in-flight mission. Mirrors the same pattern in the Claude Code
12
+ * MCP server and the Gemini CLI adapter. See
13
+ * docs/strategy-ai-agents-plus-ros.md §4 Phase 1.f.
14
+ */
15
+
16
+ import { MissionRegistry } from "@agenticros/core";
17
+
18
+ const registry = new MissionRegistry();
19
+
20
+ export function getMissionRegistry(): MissionRegistry {
21
+ return registry;
22
+ }