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,381 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * refresh-skill-deps.mjs
4
+ *
5
+ * Prevents the pnpm-hardlink-cascade we've hit three times running:
6
+ *
7
+ * 1. Add a new file to packages/core/src/ (e.g. capabilities.ts, mission.ts).
8
+ * 2. Build core. The workspace's dist/ has the new file.
9
+ * 3. External skill repos consume @agenticros/core via `file:` deps, so
10
+ * pnpm hardlinks them through a virtual store snapshot
11
+ * (.pnpm/@agenticros+core@file+..+agenticros+packages+core/...).
12
+ * 4. That snapshot is taken once at install time and is NEVER refreshed
13
+ * automatically when the source's dist/ gains new files.
14
+ * 5. The skill repo's @agenticros/core now silently lags behind. Imports
15
+ * that reference the new file fail with ERR_MODULE_NOT_FOUND.
16
+ * 6. `sync-skill-tools.mjs` runs, fails to import the skill, and (prior
17
+ * to the hardening done alongside this script) silently strips the
18
+ * skill's tools from contracts.tools + tools.alsoAllow.
19
+ * 7. The chat agent loses access to follow_robot, find_object, etc. The
20
+ * failure mode is silent until somebody asks "why can't the robot
21
+ * follow me anymore?"
22
+ *
23
+ * This script detects + repairs that cascade. It compares the workspace's
24
+ * packages/core/dist/ file list against each skill repo's resolved
25
+ * @agenticros/core/dist/. If anything is missing, it refreshes the skill
26
+ * repo's node_modules (pnpm install, escalating to a clean reinstall if
27
+ * pnpm decides to no-op).
28
+ *
29
+ * Usage
30
+ * -----
31
+ * node scripts/refresh-skill-deps.mjs # refresh stale skills
32
+ * node scripts/refresh-skill-deps.mjs --dry-run # check + report only
33
+ * node scripts/refresh-skill-deps.mjs --verbose # extra logging
34
+ * node scripts/refresh-skill-deps.mjs --force # always reinstall
35
+ * node scripts/refresh-skill-deps.mjs --skill <path> # extra path (repeatable)
36
+ * node scripts/refresh-skill-deps.mjs --quiet # suppress non-error log
37
+ *
38
+ * Discovery order for skill paths (deduped):
39
+ * 1. --skill <path> CLI flags
40
+ * 2. AGENTICROS_SKILL_PATHS env var (colon-separated)
41
+ * 3. OpenClaw config (OPENCLAW_CONFIG or ~/.openclaw/openclaw.json)
42
+ * under plugins.entries.agenticros.config.skillPaths
43
+ * 4. AgenticROS standalone config (AGENTICROS_CONFIG_PATH or
44
+ * ~/.agenticros/config.json) under skillPaths
45
+ *
46
+ * Exit codes:
47
+ * 0 — nothing was stale, OR every stale repo was refreshed successfully
48
+ * 1 — at least one stale repo could not be refreshed
49
+ * 2 — invalid arguments / unexpected error
50
+ */
51
+
52
+ import { readFileSync, existsSync, readdirSync, statSync, realpathSync, rmSync } from "node:fs";
53
+ import { join, resolve, dirname, relative, isAbsolute } from "node:path";
54
+ import { homedir } from "node:os";
55
+ import { fileURLToPath } from "node:url";
56
+ import { spawnSync } from "node:child_process";
57
+
58
+ const __filename = fileURLToPath(import.meta.url);
59
+ const __dirname = dirname(__filename);
60
+ const REPO_ROOT = resolve(__dirname, "..");
61
+ const CORE_DIST = join(REPO_ROOT, "packages", "core", "dist");
62
+
63
+ // ---------- CLI parsing ----------
64
+
65
+ function parseArgs(argv) {
66
+ const opts = {
67
+ dryRun: false,
68
+ verbose: false,
69
+ quiet: false,
70
+ force: false,
71
+ extraSkillPaths: [],
72
+ };
73
+ for (let i = 0; i < argv.length; i++) {
74
+ const a = argv[i];
75
+ if (a === "--dry-run" || a === "-n") opts.dryRun = true;
76
+ else if (a === "--verbose" || a === "-v") opts.verbose = true;
77
+ else if (a === "--quiet" || a === "-q") opts.quiet = true;
78
+ else if (a === "--force") opts.force = true;
79
+ else if (a === "--skill") opts.extraSkillPaths.push(argv[++i]);
80
+ else if (a.startsWith("--skill=")) opts.extraSkillPaths.push(a.slice("--skill=".length));
81
+ else if (a === "--help" || a === "-h") {
82
+ printHelp();
83
+ process.exit(0);
84
+ } else if (a.startsWith("-")) {
85
+ console.error(`[refresh-skill-deps] Unknown arg: ${a}`);
86
+ process.exit(2);
87
+ }
88
+ }
89
+ return opts;
90
+ }
91
+
92
+ function printHelp() {
93
+ console.log(`refresh-skill-deps — repair stale pnpm hardlinks in external skill repos.
94
+
95
+ Usage:
96
+ node scripts/refresh-skill-deps.mjs [options]
97
+
98
+ Options:
99
+ --dry-run, -n Report what's stale, don't refresh.
100
+ --verbose, -v Print per-file comparison details.
101
+ --quiet, -q Suppress all output except errors and summary.
102
+ --force Always reinstall, even if not stale.
103
+ --skill <path> Additional skill path to check (repeatable).
104
+ --help, -h Show this help.
105
+
106
+ Exit codes:
107
+ 0 — nothing was stale, OR every stale repo was refreshed.
108
+ 1 — at least one skill could not be refreshed.
109
+ 2 — invalid arguments.
110
+ `);
111
+ }
112
+
113
+ // ---------- Skill discovery ----------
114
+
115
+ function readJsonSafe(path) {
116
+ try {
117
+ return JSON.parse(readFileSync(path, "utf8"));
118
+ } catch {
119
+ return null;
120
+ }
121
+ }
122
+
123
+ export function discoverSkillPaths({ extraSkillPaths = [] } = {}) {
124
+ const out = new Set();
125
+ for (const p of extraSkillPaths) if (p) out.add(resolve(p));
126
+
127
+ if (process.env.AGENTICROS_SKILL_PATHS) {
128
+ for (const p of process.env.AGENTICROS_SKILL_PATHS.split(":")) {
129
+ if (p) out.add(resolve(p));
130
+ }
131
+ }
132
+
133
+ const openclawConfigPath =
134
+ process.env.OPENCLAW_CONFIG ||
135
+ join(homedir(), ".openclaw", "openclaw.json");
136
+ const openclawConfig = readJsonSafe(openclawConfigPath);
137
+ const pluginCfg =
138
+ openclawConfig?.plugins?.entries?.agenticros?.config ??
139
+ openclawConfig?.plugins?.agenticros?.config ??
140
+ null;
141
+ if (pluginCfg?.skillPaths && Array.isArray(pluginCfg.skillPaths)) {
142
+ for (const p of pluginCfg.skillPaths) if (p) out.add(resolve(p));
143
+ }
144
+
145
+ const agenticrosConfigPath =
146
+ process.env.AGENTICROS_CONFIG_PATH ||
147
+ join(homedir(), ".agenticros", "config.json");
148
+ const agenticrosConfig = readJsonSafe(agenticrosConfigPath);
149
+ if (agenticrosConfig?.skillPaths && Array.isArray(agenticrosConfig.skillPaths)) {
150
+ for (const p of agenticrosConfig.skillPaths) if (p) out.add(resolve(p));
151
+ }
152
+
153
+ return [...out];
154
+ }
155
+
156
+ // ---------- Staleness detection ----------
157
+
158
+ /**
159
+ * Recursively list relative file paths under a directory. Returns a sorted
160
+ * array. Returns an empty array if the directory doesn't exist.
161
+ */
162
+ export function listFilesRecursive(dir) {
163
+ const out = [];
164
+ if (!existsSync(dir)) return out;
165
+ const stack = [dir];
166
+ while (stack.length > 0) {
167
+ const cur = stack.pop();
168
+ let entries;
169
+ try {
170
+ entries = readdirSync(cur, { withFileTypes: true });
171
+ } catch {
172
+ continue;
173
+ }
174
+ for (const entry of entries) {
175
+ const full = join(cur, entry.name);
176
+ if (entry.isDirectory()) stack.push(full);
177
+ else if (entry.isFile()) out.push(relative(dir, full));
178
+ }
179
+ }
180
+ out.sort();
181
+ return out;
182
+ }
183
+
184
+ /**
185
+ * Compare a source file list (the truth) against a target file list (the
186
+ * skill's hardlinked copy) and return any files present in source but
187
+ * missing in target.
188
+ *
189
+ * Exported so unit tests can pin the comparison logic without touching fs.
190
+ */
191
+ export function findMissingFiles(sourceFiles, targetFiles) {
192
+ const targetSet = new Set(targetFiles);
193
+ return sourceFiles.filter((f) => !targetSet.has(f));
194
+ }
195
+
196
+ /**
197
+ * Check one skill repo for staleness against the workspace core's dist/.
198
+ *
199
+ * Returns: { skillPath, hasCoreLink, missingFiles, sourceCount, targetCount }
200
+ * - hasCoreLink: false → skill doesn't depend on @agenticros/core (skip).
201
+ * - missingFiles: non-empty → STALE; needs refresh.
202
+ */
203
+ export function checkSkillStaleness(skillPath, opts = {}) {
204
+ const sourceCoreDist = opts.sourceCoreDist ?? CORE_DIST;
205
+ const coreLink = join(skillPath, "node_modules", "@agenticros", "core");
206
+ if (!existsSync(coreLink)) {
207
+ return {
208
+ skillPath,
209
+ hasCoreLink: false,
210
+ missingFiles: [],
211
+ sourceCount: 0,
212
+ targetCount: 0,
213
+ };
214
+ }
215
+ const skillDist = join(coreLink, "dist");
216
+ const sourceFiles = listFilesRecursive(sourceCoreDist);
217
+ const targetFiles = listFilesRecursive(skillDist);
218
+ const missingFiles = findMissingFiles(sourceFiles, targetFiles);
219
+ return {
220
+ skillPath,
221
+ hasCoreLink: true,
222
+ missingFiles,
223
+ sourceCount: sourceFiles.length,
224
+ targetCount: targetFiles.length,
225
+ };
226
+ }
227
+
228
+ // ---------- Refresh logic ----------
229
+
230
+ function log(opts, level, ...m) {
231
+ if (level === "error") {
232
+ console.error("[refresh-skill-deps]", ...m);
233
+ return;
234
+ }
235
+ if (opts.quiet) return;
236
+ if (level === "verbose" && !opts.verbose) return;
237
+ console.log("[refresh-skill-deps]", ...m);
238
+ }
239
+
240
+ function runPnpm(args, cwd, opts) {
241
+ // --ignore-workspace prevents pnpm from picking up the agenticros
242
+ // workspace's pnpm-workspace.yaml when the skill repo sits next to it on
243
+ // disk. Without it pnpm errors with "packages field missing or empty"
244
+ // because it thinks the skill is part of a workspace that doesn't
245
+ // declare it. The skill repos are standalone projects with their own
246
+ // package.json + lockfile, so workspace mode is incorrect for them.
247
+ const finalArgs = ["--ignore-workspace", ...args];
248
+ log(opts, "verbose", `→ pnpm ${finalArgs.join(" ")} (cwd=${cwd})`);
249
+ const res = spawnSync("pnpm", finalArgs, {
250
+ cwd,
251
+ stdio: opts.verbose ? "inherit" : ["ignore", "pipe", "pipe"],
252
+ encoding: "utf8",
253
+ });
254
+ return { code: res.status ?? 1, stdout: res.stdout ?? "", stderr: res.stderr ?? "" };
255
+ }
256
+
257
+ function refreshSkill(skillPath, opts) {
258
+ // First try: plain pnpm install. Cheap, no-op when up to date.
259
+ const r1 = runPnpm(["install"], skillPath, opts);
260
+ // ERR_PNPM_IGNORED_BUILDS is just a warning about postinstall scripts;
261
+ // treat any non-zero with that marker as success.
262
+ const isBenignWarn = (out) =>
263
+ typeof out === "string" && out.includes("ERR_PNPM_IGNORED_BUILDS");
264
+ if (r1.code !== 0 && !isBenignWarn(r1.stderr) && !isBenignWarn(r1.stdout)) {
265
+ log(opts, "error", `pnpm install failed in ${skillPath}`);
266
+ if (r1.stderr) log(opts, "error", r1.stderr.trim());
267
+ return { ok: false, escalated: false };
268
+ }
269
+
270
+ const after = checkSkillStaleness(skillPath);
271
+ if (after.missingFiles.length === 0) {
272
+ log(opts, "info", ` ✓ refreshed via pnpm install`);
273
+ return { ok: true, escalated: false };
274
+ }
275
+
276
+ // pnpm decided it was already up to date but the hardlinks are still
277
+ // stale. This happens when the .pnpm virtual store hash matches even
278
+ // though dist/ contents changed. Escalate to a clean reinstall.
279
+ log(opts, "info", ` pnpm install no-op'd; escalating to clean reinstall`);
280
+ const nm = join(skillPath, "node_modules");
281
+ if (existsSync(nm)) {
282
+ try {
283
+ rmSync(nm, { recursive: true, force: true });
284
+ } catch (e) {
285
+ log(opts, "error", `failed to rm -rf ${nm}: ${e.message}`);
286
+ return { ok: false, escalated: true };
287
+ }
288
+ }
289
+ const r2 = runPnpm(["install"], skillPath, opts);
290
+ if (r2.code !== 0 && !isBenignWarn(r2.stderr) && !isBenignWarn(r2.stdout)) {
291
+ log(opts, "error", `clean pnpm install failed in ${skillPath}`);
292
+ if (r2.stderr) log(opts, "error", r2.stderr.trim());
293
+ return { ok: false, escalated: true };
294
+ }
295
+ const final = checkSkillStaleness(skillPath);
296
+ if (final.missingFiles.length > 0) {
297
+ log(opts, "error", `${skillPath} STILL stale after clean reinstall; missing ${final.missingFiles.length} file(s)`);
298
+ return { ok: false, escalated: true };
299
+ }
300
+ log(opts, "info", ` ✓ refreshed via clean reinstall`);
301
+ return { ok: true, escalated: true };
302
+ }
303
+
304
+ // ---------- Main ----------
305
+
306
+ async function main(argv) {
307
+ const opts = parseArgs(argv);
308
+ const skillPaths = discoverSkillPaths({ extraSkillPaths: opts.extraSkillPaths });
309
+ if (skillPaths.length === 0) {
310
+ log(opts, "info", "No skill paths discovered (checked --skill, AGENTICROS_SKILL_PATHS, ~/.openclaw/openclaw.json, ~/.agenticros/config.json).");
311
+ return 0;
312
+ }
313
+
314
+ log(opts, "info", `Checking ${skillPaths.length} skill path(s) against ${CORE_DIST}`);
315
+ if (!existsSync(CORE_DIST)) {
316
+ log(opts, "error", `packages/core/dist/ does not exist. Run \`pnpm --filter @agenticros/core build\` first.`);
317
+ return 2;
318
+ }
319
+
320
+ const sourceCount = listFilesRecursive(CORE_DIST).length;
321
+ let stale = 0;
322
+ let refreshed = 0;
323
+ let failed = 0;
324
+ let skipped = 0;
325
+
326
+ for (const skillPath of skillPaths) {
327
+ if (!existsSync(skillPath)) {
328
+ log(opts, "info", `· ${skillPath}: directory does not exist; skipping`);
329
+ skipped++;
330
+ continue;
331
+ }
332
+ const status = checkSkillStaleness(skillPath);
333
+ if (!status.hasCoreLink && !opts.force) {
334
+ log(opts, "verbose", `· ${skillPath}: no @agenticros/core dep; skipping`);
335
+ skipped++;
336
+ continue;
337
+ }
338
+ const isStale = status.missingFiles.length > 0;
339
+ if (!isStale && !opts.force) {
340
+ log(opts, "verbose", `· ${skillPath}: up to date (${status.targetCount}/${status.sourceCount} files)`);
341
+ continue;
342
+ }
343
+ stale++;
344
+ if (isStale) {
345
+ log(
346
+ opts,
347
+ "info",
348
+ `· ${skillPath}: STALE — missing ${status.missingFiles.length} file(s) in @agenticros/core/dist` +
349
+ (opts.verbose ? `: ${status.missingFiles.join(", ")}` : ""),
350
+ );
351
+ } else {
352
+ log(opts, "info", `· ${skillPath}: --force refresh`);
353
+ }
354
+ if (opts.dryRun) continue;
355
+ const result = refreshSkill(skillPath, opts);
356
+ if (result.ok) refreshed++;
357
+ else failed++;
358
+ }
359
+
360
+ const sum = `Summary: ${skillPaths.length} skill(s) checked — ${stale} stale, ${refreshed} refreshed, ${failed} failed, ${skipped} skipped. Core source has ${sourceCount} file(s).`;
361
+ if (opts.dryRun && stale > 0) {
362
+ log(opts, "info", sum);
363
+ log(opts, "info", "Re-run without --dry-run to refresh.");
364
+ return 0;
365
+ }
366
+ log(opts, "info", sum);
367
+ return failed > 0 ? 1 : 0;
368
+ }
369
+
370
+ // Run only when invoked directly (preserves importability for tests).
371
+ if (
372
+ import.meta.url === `file://${process.argv[1]}` ||
373
+ process.argv[1]?.endsWith("refresh-skill-deps.mjs")
374
+ ) {
375
+ main(process.argv.slice(2))
376
+ .then((code) => process.exit(code))
377
+ .catch((err) => {
378
+ console.error("[refresh-skill-deps] Unexpected error:", err);
379
+ process.exit(2);
380
+ });
381
+ }
File without changes
File without changes
File without changes
@@ -29,6 +29,12 @@
29
29
  # --robot-namespace N ROS2 namespace for cmd_vel (e.g. robot3946b404c33e4aa39a8d16deb1c5c593)
30
30
  # --camera-topic T Camera topic for ros2_camera_snapshot / teleop
31
31
  # --skip-build Skip `pnpm build` (assume it's already done)
32
+ # --skip-refresh-skills
33
+ # Skip the skill-hardlink refresh step. By default this
34
+ # script runs scripts/refresh-skill-deps.mjs to keep
35
+ # external skill repos in lockstep with packages/core
36
+ # (prevents the pnpm cascade where new core exports
37
+ # silently disappear from the chat agent's tool list).
32
38
  # --no-systemd Skip systemd service tweaks
33
39
  # --no-restart Don't restart the gateway at the end
34
40
  # -h, --help Show this help
@@ -43,6 +49,7 @@ ZENOH_ENDPOINT=""
43
49
  ROBOT_NAMESPACE=""
44
50
  CAMERA_TOPIC=""
45
51
  SKIP_BUILD=false
52
+ SKIP_REFRESH_SKILLS=false
46
53
  NO_SYSTEMD=false
47
54
  NO_RESTART=false
48
55
 
@@ -58,9 +65,10 @@ while [[ $# -gt 0 ]]; do
58
65
  --robot-namespace) ROBOT_NAMESPACE="$2"; shift 2 ;;
59
66
  --camera-topic) CAMERA_TOPIC="$2"; shift 2 ;;
60
67
  --skip-build) SKIP_BUILD=true; shift ;;
68
+ --skip-refresh-skills) SKIP_REFRESH_SKILLS=true; shift ;;
61
69
  --no-systemd) NO_SYSTEMD=true; shift ;;
62
70
  --no-restart) NO_RESTART=true; shift ;;
63
- -h|--help) sed -n '2,38p' "$0"; exit 0 ;;
71
+ -h|--help) sed -n '2,45p' "$0"; exit 0 ;;
64
72
  *) echo "Unknown option: $1"; exit 1 ;;
65
73
  esac
66
74
  done
@@ -105,17 +113,36 @@ else
105
113
  fi
106
114
  echo ""
107
115
 
116
+ # 2b. Refresh external skill repos so they pick up new @agenticros/core
117
+ # exports. Skips itself when no skill paths are configured. See
118
+ # scripts/refresh-skill-deps.mjs for the cascade this prevents.
119
+ if [[ "$SKIP_REFRESH_SKILLS" != true ]]; then
120
+ echo "[2b/5] Refreshing external skill repos (use --skip-refresh-skills to opt out)..."
121
+ if ! node "$REPO_ROOT/scripts/refresh-skill-deps.mjs"; then
122
+ echo "WARN: skill refresh reported failures; continuing with deploy. Re-run \`pnpm refresh:skills\` manually to investigate." >&2
123
+ fi
124
+ echo ""
125
+ fi
126
+
108
127
  # 3. Flatten the plugin into a deploy directory.
109
128
  # OpenClaw 2026.6+ rejects `node_modules/<dep>` symlinks that resolve outside
110
129
  # the plugin install root. `pnpm deploy --prod` materialises every dep inside
111
130
  # the deploy directory, so all symlinks are safely contained.
131
+ #
132
+ # --config.strict-peer-dependencies=false: mem0ai has unmet peer ranges for
133
+ # pg / redis / @qdrant/js-client-rest that don't affect us (we only use the
134
+ # in-memory + JSON local backends and ship our own pinned mem0ai). Without
135
+ # this flag pnpm fails the deploy on what is effectively a non-issue.
112
136
  echo "[3/5] Building flat plugin deployment at $DEPLOY_DIR ..."
113
137
  mkdir -p "$(dirname "$DEPLOY_DIR")"
114
138
  rm -rf "$DEPLOY_DIR"
115
- (cd "$REPO_ROOT" && pnpm --filter ./packages/agenticros deploy --prod "$DEPLOY_DIR")
116
- # pnpm leaves one self-reference symlink (.pnpm/node_modules/agenticros → the
117
- # source path) that the safety scan will reject. It's not needed at runtime.
118
- rm -f "$DEPLOY_DIR/node_modules/.pnpm/node_modules/agenticros"
139
+ (cd "$REPO_ROOT" && pnpm --config.strict-peer-dependencies=false --filter ./packages/agenticros deploy --prod "$DEPLOY_DIR")
140
+ # pnpm leaves a self-reference symlink (.pnpm/node_modules/<pkg> → the source
141
+ # path) that OpenClaw's safety scan will reject. The plugin's npm name is
142
+ # `@agenticros/openclaw` (scoped), so the symlink lives under @agenticros/.
143
+ # Older builds used the bare name `agenticros`; clear both to be tolerant.
144
+ rm -rf "$DEPLOY_DIR/node_modules/.pnpm/node_modules/@agenticros/openclaw"
145
+ rm -f "$DEPLOY_DIR/node_modules/.pnpm/node_modules/agenticros"
119
146
 
120
147
  # `pnpm deploy --prod` skips lifecycle scripts, so rclnodejs's postinstall
121
148
  # (which runs `node scripts/generate_messages.js` to materialise ROS message
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -56,6 +56,14 @@ const CORE_TOOLS = [
56
56
  "ros2_param_get",
57
57
  "ros2_param_set",
58
58
  "ros2_list_topics",
59
+ "ros2_list_capabilities",
60
+ "ros2_list_robots",
61
+ "ros2_discover_robots",
62
+ // Phase 1.e fleet-capability filter — `ros2_find_robots_for(capability, kind, online)`.
63
+ "ros2_find_robots_for",
64
+ "run_mission",
65
+ // Phase 1.f mission control — cancel a running mission by id.
66
+ "mission_cancel",
59
67
  "ros2_camera_snapshot",
60
68
  "ros2_depth_distance",
61
69
  // Memory tools (registered conditionally at runtime when memory.enabled=true).
@@ -122,8 +130,8 @@ function findSkillEntryFromPath(dirPath) {
122
130
  warn(`Invalid package.json in ${dirPath}: ${e?.message ?? e}`);
123
131
  return null;
124
132
  }
125
- if (!pkg.agenticrosSkill) {
126
- warn(`Not an AgenticROS skill (missing agenticrosSkill flag): ${dirPath}`);
133
+ if (!pkg.agenticros || typeof pkg.agenticros !== "object" || typeof pkg.agenticros.id !== "string") {
134
+ warn(`Not an AgenticROS skill (missing "agenticros": { "id": "..." } block): ${dirPath}`);
127
135
  return null;
128
136
  }
129
137
  const main = pkg.main ?? "index.js";
@@ -206,14 +214,21 @@ async function discoverSkillTools(entryPath, packageName) {
206
214
  try {
207
215
  mod = await import(url);
208
216
  } catch (e) {
209
- warn(`Failed to import ${packageName} (${entryPath}): ${e?.message ?? e}`);
210
- return [];
217
+ const errMsg = e?.message ?? String(e);
218
+ warn(`Failed to import ${packageName} (${entryPath}): ${errMsg}`);
219
+ // Detect the classic pnpm-hardlink-cascade signature: an
220
+ // ERR_MODULE_NOT_FOUND for something inside @agenticros/core/dist/.
221
+ // When we see it, surface it as a structured failure so the caller can
222
+ // preserve the skill's previously-known tools instead of silently
223
+ // stripping them.
224
+ const isCoreCascade = /@agenticros\+?core.*dist\/.+\.js/i.test(errMsg);
225
+ return { tools: [], importFailed: true, cascadeSuspected: isCoreCascade };
211
226
  }
212
227
  const registerSkill =
213
228
  mod.registerSkill ?? mod.default?.registerSkill ?? mod.default;
214
229
  if (typeof registerSkill !== "function") {
215
230
  warn(`${packageName}: no registerSkill export — skipping discovery`);
216
- return [];
231
+ return { tools: [], importFailed: false };
217
232
  }
218
233
  const { tools, api } = createSpyApi();
219
234
  const ctx = createStubContext();
@@ -235,7 +250,7 @@ async function discoverSkillTools(entryPath, packageName) {
235
250
  `${packageName} registerSkill threw during discovery — captured ${tools.length} tools before error: ${e?.message ?? e}`,
236
251
  );
237
252
  }
238
- return tools;
253
+ return { tools, importFailed: false };
239
254
  }
240
255
 
241
256
  function uniqueOrdered(...lists) {
@@ -277,13 +292,18 @@ async function main() {
277
292
  log(`skillPackages: ${skillPackages.length === 0 ? "(none)" : skillPackages.join(", ")}`);
278
293
 
279
294
  const discovered = new Map(); // skillId/packageName -> tools[]
295
+ const importFailures = []; // [{ name, cascadeSuspected }]
280
296
 
281
297
  for (const pkg of skillPackages) {
282
298
  const entry = resolvePackageEntry(pkg);
283
299
  if (!entry) continue;
284
300
  vlog(`Resolving package "${pkg}" → ${entry}`);
285
- const tools = await discoverSkillTools(entry, pkg);
286
- if (tools.length > 0) discovered.set(pkg, tools);
301
+ const result = await discoverSkillTools(entry, pkg);
302
+ if (result.importFailed) {
303
+ importFailures.push({ name: pkg, cascadeSuspected: result.cascadeSuspected });
304
+ continue;
305
+ }
306
+ if (result.tools.length > 0) discovered.set(pkg, result.tools);
287
307
  else vlog(` no tools discovered for ${pkg}`);
288
308
  }
289
309
 
@@ -292,8 +312,12 @@ async function main() {
292
312
  const resolved = findSkillEntryFromPath(absDir);
293
313
  if (!resolved) continue;
294
314
  vlog(`Resolving path "${absDir}" → ${resolved.entry} (${resolved.packageName})`);
295
- const tools = await discoverSkillTools(resolved.entry, resolved.packageName);
296
- if (tools.length > 0) discovered.set(resolved.packageName, tools);
315
+ const result = await discoverSkillTools(resolved.entry, resolved.packageName);
316
+ if (result.importFailed) {
317
+ importFailures.push({ name: resolved.packageName, cascadeSuspected: result.cascadeSuspected });
318
+ continue;
319
+ }
320
+ if (result.tools.length > 0) discovered.set(resolved.packageName, result.tools);
297
321
  else vlog(` no tools discovered for ${resolved.packageName}`);
298
322
  }
299
323
 
@@ -305,12 +329,45 @@ async function main() {
305
329
  }
306
330
  }
307
331
 
308
- const skillTools = [...discovered.values()].flat();
309
- const merged = uniqueOrdered(CORE_TOOLS, skillTools);
310
332
  const previous = Array.isArray(manifest?.contracts?.tools)
311
333
  ? manifest.contracts.tools.slice()
312
334
  : [];
313
335
 
336
+ // If any skill failed to import, preserve its presumed entries from the
337
+ // previous manifest instead of silently stripping them. We can't tell
338
+ // exactly which tools belonged to that specific skill, but we know any
339
+ // tool in `previous` that's not in CORE_TOOLS and not in the freshly
340
+ // discovered set must have come from one of these skills — and dropping
341
+ // them silently was exactly the regression we hit on 2026-06-10.
342
+ const coreSet = new Set(CORE_TOOLS);
343
+ const freshSkillTools = [...discovered.values()].flat();
344
+ const freshSet = new Set(freshSkillTools);
345
+ let preservedTools = [];
346
+ if (importFailures.length > 0) {
347
+ preservedTools = previous.filter(
348
+ (t) => typeof t === "string" && !coreSet.has(t) && !freshSet.has(t),
349
+ );
350
+ const failedNames = importFailures.map((f) => f.name).join(", ");
351
+ warn(`Some skill(s) failed to import: ${failedNames}`);
352
+ if (preservedTools.length > 0) {
353
+ warn(
354
+ `Preserving ${preservedTools.length} previously-known tool(s) from contracts.tools so the chat agent does not silently lose them: ${preservedTools.join(", ")}`,
355
+ );
356
+ }
357
+ const cascade = importFailures.some((f) => f.cascadeSuspected);
358
+ if (cascade) {
359
+ warn(
360
+ "Cascade signature detected (ERR_MODULE_NOT_FOUND inside @agenticros/core/dist). " +
361
+ "Run `pnpm refresh:skills` to repair the stale pnpm hardlinks, then re-run this sync.",
362
+ );
363
+ } else {
364
+ warn("Re-run this sync after fixing the import error to refresh the manifest.");
365
+ }
366
+ }
367
+
368
+ const skillTools = [...freshSkillTools, ...preservedTools];
369
+ const merged = uniqueOrdered(CORE_TOOLS, skillTools);
370
+
314
371
  if (arraysEqual(previous, merged)) {
315
372
  log("Manifest is already up to date.");
316
373
  return;
File without changes
File without changes