@openclaw/plugin-inspector 0.3.10 → 0.3.12

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.
package/src/cli.js CHANGED
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import {
4
4
  loadPluginConfig,
5
5
  renderTextSummary,
6
+ runBatchAnalysis,
6
7
  sanitizeReportArtifact,
7
8
  runPluginCheck,
8
9
  } from "./index.js";
@@ -43,6 +44,8 @@ try {
43
44
  }
44
45
  } else if (command === "ci") {
45
46
  await runCi(commandArgs);
47
+ } else if (command === "batch") {
48
+ await runBatch(commandArgs);
46
49
  } else if (command === "capture") {
47
50
  await runCapture(commandArgs);
48
51
  } else {
@@ -53,6 +56,38 @@ try {
53
56
  process.exitCode = 1;
54
57
  }
55
58
 
59
+ async function runBatch(commandArgs) {
60
+ const inputDir = readFirstPositional(commandArgs, new Set(["--out", "--openclaw", "--concurrency"]));
61
+ const outDir = readFlag(commandArgs, "--out") ?? "reports";
62
+ const openclawPath = commandArgs.includes("--no-openclaw") ? false : readFlag(commandArgs, "--openclaw");
63
+ const concurrency = Number(readFlag(commandArgs, "--concurrency") ?? "4");
64
+ const json = commandArgs.includes("--json");
65
+ const check = commandArgs.includes("--check");
66
+ const keepPluginReports = commandArgs.includes("--keep-plugin-reports");
67
+ const includeInspectorGaps = commandArgs.includes("--include-inspector-gaps");
68
+ if (!inputDir) {
69
+ throw new Error("batch requires a folder of plugin roots");
70
+ }
71
+ const { report, paths } = await runBatchAnalysis({
72
+ rootDir: inputDir,
73
+ outDir,
74
+ openclawPath,
75
+ concurrency,
76
+ includeInspectorGaps,
77
+ keepPluginReports,
78
+ });
79
+
80
+ if (json) {
81
+ console.log(JSON.stringify(report, null, 2));
82
+ } else {
83
+ console.log(renderBatchTextSummary(report, paths));
84
+ }
85
+
86
+ if (check && report.summary.pluginsWithErrors > 0) {
87
+ throw new Error(`plugin-inspector batch found ${report.summary.pluginsWithErrors} plugin(s) with errors`);
88
+ }
89
+ }
90
+
56
91
  async function runConfig(commandArgs) {
57
92
  const configPath = readFlag(commandArgs, "--config");
58
93
  const pluginRoot = readFlag(commandArgs, "--plugin-root") ?? readFlag(commandArgs, "--root");
@@ -75,10 +110,12 @@ async function runCheck(commandArgs) {
75
110
  const mockSdk = readMockSdkFlag(commandArgs);
76
111
  const allowExecution = readAllowExecutionFlag(commandArgs);
77
112
  const ciOutputs = readCiOutputFlags(commandArgs);
113
+ const includeInspectorGaps = commandArgs.includes("--include-inspector-gaps");
78
114
  const { report, paths } = await runPluginCheck({
79
115
  allowExecution,
80
116
  capture,
81
117
  configPath,
118
+ includeInspectorGaps,
82
119
  mockSdk,
83
120
  openclawPath,
84
121
  outDir,
@@ -164,10 +201,12 @@ async function runCi(commandArgs) {
164
201
  const mockSdk = readMockSdkFlag(commandArgs);
165
202
  const allowExecution = readAllowExecutionFlag(commandArgs);
166
203
  const ciOutputs = readCiOutputFlags(commandArgs, { defaultEnabled: true });
204
+ const includeInspectorGaps = commandArgs.includes("--include-inspector-gaps");
167
205
  const { report, reportDir } = await runCiCompatibilityReport({
168
206
  allowExecution,
169
207
  capture,
170
208
  configPath,
209
+ includeInspectorGaps,
171
210
  mockSdk,
172
211
  openclawPath,
173
212
  outDir,
@@ -204,10 +243,19 @@ async function runCi(commandArgs) {
204
243
  }
205
244
  }
206
245
 
207
- async function runCiCompatibilityReport({ allowExecution, capture, configPath, mockSdk, openclawPath, outDir, pluginRoot }) {
246
+ async function runCiCompatibilityReport({
247
+ allowExecution,
248
+ capture,
249
+ configPath,
250
+ includeInspectorGaps,
251
+ mockSdk,
252
+ openclawPath,
253
+ outDir,
254
+ pluginRoot,
255
+ }) {
208
256
  if (configPath) {
209
257
  const config = await loadInspectorConfig(configPath, { cwd: pluginRoot });
210
- const report = await inspectCompatibilityFixtureSet(config, { openclawPath });
258
+ const report = await inspectCompatibilityFixtureSet(config, { includeInspectorGaps, openclawPath });
211
259
  await writeCompatibilityReport(report, { cwd: config.rootDir, outDir });
212
260
  return {
213
261
  report,
@@ -215,7 +263,15 @@ async function runCiCompatibilityReport({ allowExecution, capture, configPath, m
215
263
  };
216
264
  }
217
265
 
218
- const { report } = await runPluginCheck({ allowExecution, capture, mockSdk, openclawPath, outDir, pluginRoot });
266
+ const { report } = await runPluginCheck({
267
+ allowExecution,
268
+ capture,
269
+ includeInspectorGaps,
270
+ mockSdk,
271
+ openclawPath,
272
+ outDir,
273
+ pluginRoot,
274
+ });
219
275
  return {
220
276
  report,
221
277
  reportDir: path.resolve(pluginRoot ?? process.cwd(), outDir),
@@ -223,7 +279,7 @@ async function runCiCompatibilityReport({ allowExecution, capture, configPath, m
223
279
  }
224
280
 
225
281
  async function runCapture(commandArgs) {
226
- const entrypoint = commandArgs.find((arg) => !arg.startsWith("-"));
282
+ const entrypoint = findCaptureEntrypoint(commandArgs);
227
283
  const outputPath = readFlag(commandArgs, "--output");
228
284
  const pluginRoot = readFlag(commandArgs, "--plugin-root");
229
285
  const mockSdk = readMockSdkFlag(commandArgs) ?? commandArgs.includes("--mock-sdk");
@@ -252,6 +308,17 @@ function readFlag(commandArgs, name) {
252
308
  return commandArgs[index + 1] ?? null;
253
309
  }
254
310
 
311
+ function findCaptureEntrypoint(commandArgs) {
312
+ const flagsWithValues = new Set(["--output", "--plugin-root", "--sdk"]);
313
+ const consumedIndexes = new Set();
314
+ for (const [index, arg] of commandArgs.entries()) {
315
+ if (flagsWithValues.has(arg)) {
316
+ consumedIndexes.add(index + 1);
317
+ }
318
+ }
319
+ return commandArgs.find((arg, index) => !arg.startsWith("-") && !consumedIndexes.has(index)) ?? null;
320
+ }
321
+
255
322
  function readOptionalPathFlag(commandArgs, name, defaultPath) {
256
323
  const index = commandArgs.indexOf(name);
257
324
  if (index === -1) {
@@ -315,6 +382,34 @@ function renderCiTextSummary(summary) {
315
382
  ].join("\n");
316
383
  }
317
384
 
385
+ function renderBatchTextSummary(report, paths) {
386
+ const lines = [
387
+ "Plugin Inspector Batch",
388
+ `Plugins: ${report.summary.pluginCount}`,
389
+ `Plugins with errors: ${report.summary.pluginsWithErrors}`,
390
+ `Plugins with warnings: ${report.summary.pluginsWithWarnings}`,
391
+ `Finding codes: ${report.summary.findingCodeCount}`,
392
+ "",
393
+ "Reports:",
394
+ `- JSON: ${paths.jsonPath}`,
395
+ `- Markdown: ${paths.markdownPath}`,
396
+ ];
397
+ const topFinding = report.findingFrequency[0];
398
+ if (topFinding) {
399
+ lines.push("", `Top finding: ${topFinding.code} (${topFinding.plugins} plugin(s))`);
400
+ }
401
+ return lines.join("\n");
402
+ }
403
+
404
+ function readFirstPositional(args, valueFlags = new Set()) {
405
+ for (let index = 0; index < args.length; index += 1) {
406
+ const arg = args[index];
407
+ if (!arg.startsWith("-")) return arg;
408
+ if (valueFlags.has(arg)) index += 1;
409
+ }
410
+ return undefined;
411
+ }
412
+
318
413
  function initCommandSummary(result) {
319
414
  return {
320
415
  dryRun: result.dryRun,
@@ -342,16 +437,18 @@ function printHelp() {
342
437
 
343
438
  Usage:
344
439
  plugin-inspector
345
- plugin-inspector check [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--allow-execute] [--json]
440
+ plugin-inspector check [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--allow-execute] [--include-inspector-gaps] [--json]
346
441
  plugin-inspector config [--plugin-root <path>] [--config <path>] [--json]
347
442
  plugin-inspector init [--plugin-root <path>] [--config <path>] [--ci] [--scripts] [--package-manager npm|pnpm|yarn|bun] [--dry-run] [--json] [--force]
348
443
  plugin-inspector report --config <path> [--out <dir>] [--check] [--json]
444
+ plugin-inspector batch <folder> [--out <dir>] [--openclaw <path>] [--no-openclaw] [--concurrency <n>] [--keep-plugin-reports] [--include-inspector-gaps] [--check] [--json]
349
445
  plugin-inspector inspect [--plugin-root <path>] [--config <path>] [--out <dir>] [--check] [--json] [--sarif [path]] [--junit [path]] [--allow-execute]
350
- plugin-inspector ci [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--allow-execute] [--json] [--no-sarif] [--no-junit]
446
+ plugin-inspector ci [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--allow-execute] [--include-inspector-gaps] [--json] [--no-sarif] [--no-junit]
351
447
  plugin-inspector capture <entrypoint> [--mock-sdk|--real-sdk] [--allow-execute] [--plugin-root <path>] [--output <path>]
352
448
 
353
449
  Default check runs from the current plugin root and writes reports/ unless --out is set.
354
450
  CI writes SARIF and JUnit artifacts by default; check/inspect can write them with --sarif and --junit.
355
451
  Runtime capture is opt-in because it imports plugin code; use --runtime with --allow-execute or PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1.
452
+ Inspector coverage gaps are hidden by default; pass --include-inspector-gaps for maintainer coverage reports.
356
453
  `);
357
454
  }
@@ -58,9 +58,9 @@ export function renderCompatibilityMarkdownReport(report, options = {}) {
58
58
  options,
59
59
  ),
60
60
  "",
61
- "## Live Issues",
61
+ "## Other Live Issues",
62
62
  "",
63
- issuesTable(report.issues.filter((issue) => issue.issueClass === "live-issue"), options),
63
+ issuesTable(report.issues.filter((issue) => issue.issueClass === "live-issue" && issue.severity !== "P0"), options),
64
64
  "",
65
65
  "## Compat Gaps",
66
66
  "",
@@ -183,9 +183,9 @@ export function renderCompatibilityIssuesReport(report, options = {}) {
183
183
  options,
184
184
  ),
185
185
  "",
186
- "## Live Issues",
186
+ "## Other Live Issues",
187
187
  "",
188
- issuesTable(report.issues.filter((issue) => issue.issueClass === "live-issue"), options),
188
+ issuesTable(report.issues.filter((issue) => issue.issueClass === "live-issue" && issue.severity !== "P0"), options),
189
189
  "",
190
190
  "## Compat Gaps",
191
191
  "",
@@ -157,10 +157,15 @@ function requireCompatRecordReconciliation(report, errors) {
157
157
  .filter((finding) => finding.code === "missing-compat-record")
158
158
  .map((finding) => `${finding.fixture}:${finding.compatRecord}`),
159
159
  );
160
+ const compatGapRecords = new Set(
161
+ report.issues
162
+ .filter((issue) => issue.issueClass === "compat-gap" && issue.compatRecord)
163
+ .map((issue) => `${issue.fixture}:${issue.compatRecord}`),
164
+ );
160
165
 
161
166
  for (const finding of [...report.warnings, ...report.suggestions].filter((item) => item.compatRecord)) {
162
167
  const key = `${finding.fixture}:${finding.compatRecord}`;
163
- if (!presentRecords.has(key) && !missingRecords.has(key)) {
168
+ if (!presentRecords.has(key) && !missingRecords.has(key) && !compatGapRecords.has(key)) {
164
169
  errors.push(`${finding.fixture}: compat record ${finding.compatRecord} was not reconciled`);
165
170
  }
166
171
  }
@@ -114,6 +114,11 @@ export const contractProbeRules = {
114
114
  contract: "Package and OpenClaw manifest versions stay aligned for release compatibility reporting.",
115
115
  target: "package-loader",
116
116
  },
117
+ "manifest-name-missing": {
118
+ id: "manifest.metadata.name",
119
+ contract: "OpenClaw plugin manifests declare a human-readable display name for registry and tooling metadata.",
120
+ target: "manifest-loader",
121
+ },
117
122
  "package-plugin-api-compat-missing": {
118
123
  id: "package.compat.plugin-api-range",
119
124
  contract: "Package metadata declares the OpenClaw plugin API range used by the plugin.",
@@ -204,6 +204,7 @@ function summarizeArtifact({ artifactPath, parsed, rootDir }) {
204
204
  : "capture";
205
205
  const fixture = normalizedArtifactPath.split("/").at(-2) ?? "unknown";
206
206
  if (kind === "synthetic") {
207
+ const passed = (parsed.results ?? []).filter((result) => result.status === "pass");
207
208
  return {
208
209
  artifactPath: normalizedArtifactPath,
209
210
  fixture,
@@ -211,6 +212,8 @@ function summarizeArtifact({ artifactPath, parsed, rootDir }) {
211
212
  entrypoint: scrubPath(parsed.entrypoint, { rootDir }),
212
213
  status: parsed.status,
213
214
  summary: parsed.summary,
215
+ captured: passed.map(syntheticRuntimeCaptureKey).filter(Boolean),
216
+ passed,
214
217
  failures: (parsed.results ?? []).filter((result) => result.status === "fail"),
215
218
  blocked: (parsed.results ?? []).filter((result) => result.status === "blocked"),
216
219
  };
@@ -261,6 +264,13 @@ function summarizeArtifactResult(artifact) {
261
264
  return `${artifact.capturedCount} captured`;
262
265
  }
263
266
 
267
+ function syntheticRuntimeCaptureKey(result) {
268
+ if (!result.kind || !result.seam) {
269
+ return null;
270
+ }
271
+ return `${result.kind}:${result.seam}`;
272
+ }
273
+
264
274
  function auditFindingCount(parsed) {
265
275
  const vulnerabilities = parsed.metadata?.vulnerabilities;
266
276
  if (vulnerabilities && typeof vulnerabilities === "object") {
@@ -233,6 +233,26 @@ export function classifyPackageContracts({ fixture, inspection, fixtureReport })
233
233
  });
234
234
  }
235
235
 
236
+ const missingManifestNames = fixtureReport.pluginManifests.filter(
237
+ (manifest) => typeof manifest.name !== "string" || manifest.name.trim().length === 0,
238
+ );
239
+ if (missingManifestNames.length > 0) {
240
+ warnings.push({
241
+ fixture: fixture.id,
242
+ code: "manifest-name-missing",
243
+ level: "warning",
244
+ message: "openclaw.plugin.json does not declare a display name",
245
+ evidence: missingManifestNames.map((manifest) => manifest.path ?? "openclaw.plugin.json"),
246
+ });
247
+ decisions.push({
248
+ fixture: fixture.id,
249
+ decision: "plugin-upstream-fix",
250
+ seam: "manifest-metadata",
251
+ action: "Ask the plugin to declare openclaw.plugin.json name so registries and tools can derive a human-readable title.",
252
+ evidence: missingManifestNames.map((manifest) => manifest.path ?? "openclaw.plugin.json").join(", "),
253
+ });
254
+ }
255
+
236
256
  if (packageSummary.openclaw && !packageSummary.openclaw.compatPluginApi) {
237
257
  warnings.push({
238
258
  fixture: fixture.id,
@@ -340,9 +360,12 @@ export function classifyPackageContracts({ fixture, inspection, fixtureReport })
340
360
  });
341
361
  }
342
362
 
343
- const missingEntrypoints = packageSummary.openclaw?.entrypoints.filter((entrypoint) => !entrypoint.exists) ?? [];
363
+ const entrypoints = packageSummary.openclaw?.entrypoints ?? [];
364
+ const missingEntrypoints = entrypoints.filter((entrypoint) => !entrypoint.exists);
344
365
  const buildEntrypoints = missingEntrypoints.filter((entrypoint) => entrypoint.requiresBuild);
345
- const plainMissingEntrypoints = missingEntrypoints.filter((entrypoint) => !entrypoint.requiresBuild);
366
+ const plainMissingEntrypoints = missingEntrypoints.filter(
367
+ (entrypoint) => !entrypoint.requiresBuild && !hasUsablePackageRuntimeEntrypoint(entrypoint, packageSummary, entrypoints),
368
+ );
346
369
 
347
370
  if (buildEntrypoints.length > 0) {
348
371
  suggestions.push({
@@ -920,6 +943,46 @@ function collectOpenClawEntrypoints(packageDir, openclaw, options) {
920
943
  });
921
944
  }
922
945
 
946
+ function hasUsablePackageRuntimeEntrypoint(entrypoint, packageSummary, entrypoints) {
947
+ if (!isSourceEntrypoint(entrypoint.specifier)) {
948
+ return false;
949
+ }
950
+
951
+ const runtimeBuildSpecifier = runtimeBuildSpecifierFor(entrypoint.specifier);
952
+ if (
953
+ entrypoints.some(
954
+ (candidate) =>
955
+ candidate.exists &&
956
+ candidate.requiresBuild &&
957
+ normalizeEntrypointSpecifier(candidate.specifier) === normalizeEntrypointSpecifier(runtimeBuildSpecifier),
958
+ )
959
+ ) {
960
+ return true;
961
+ }
962
+
963
+ if (entrypoint.kind === "extension" && entrypoints.some((candidate) => candidate.kind === "runtimeExtension" && candidate.exists)) {
964
+ return true;
965
+ }
966
+
967
+ const packageDir = path.dirname(packageSummary.path);
968
+ return existsSync(path.resolve(packageDir, runtimeBuildSpecifier));
969
+ }
970
+
971
+ function isSourceEntrypoint(specifier) {
972
+ return /\.(?:ts|tsx)$/.test(specifier);
973
+ }
974
+
975
+ function runtimeBuildSpecifierFor(specifier) {
976
+ const normalized = normalizeEntrypointSpecifier(specifier);
977
+ const basename = path.posix.basename(normalized).replace(/\.(?:ts|tsx)$/, ".js");
978
+ return `./dist/${basename}`;
979
+ }
980
+
981
+ function normalizeEntrypointSpecifier(specifier) {
982
+ const normalized = specifier.replaceAll("\\", "/");
983
+ return normalized.startsWith("./") ? normalized : `./${normalized}`;
984
+ }
985
+
923
986
  async function findPackageFiles(root, options, depth = 0) {
924
987
  if (!existsSync(root) || depth > options.maxDepth) {
925
988
  return [];
@@ -1059,8 +1122,13 @@ function packageNpmPackIssues(packageSummary, fixtureReport) {
1059
1122
  });
1060
1123
  }
1061
1124
 
1062
- const missingEntrypoints = packageSummary.openclaw?.entrypoints
1063
- .filter((entrypoint) => !repoPathIncludedInNpmPack(packageSummary, entrypoint.relativePath))
1125
+ const entrypoints = packageSummary.openclaw?.entrypoints ?? [];
1126
+ const missingEntrypoints = entrypoints
1127
+ .filter(
1128
+ (entrypoint) =>
1129
+ !repoPathIncludedInNpmPack(packageSummary, entrypoint.relativePath) &&
1130
+ !hasPackagedRuntimeEntrypoint(entrypoint, packageSummary, entrypoints),
1131
+ )
1064
1132
  .map((entrypoint) => `${entrypoint.kind}:${entrypoint.specifier} -> ${entrypoint.relativePath}`) ?? [];
1065
1133
  if (missingEntrypoints.length > 0) {
1066
1134
  findings.push({
@@ -1088,6 +1156,35 @@ function packageNpmPackMissingMetadata(packageSummary, fixtureReport) {
1088
1156
  return missing;
1089
1157
  }
1090
1158
 
1159
+ function hasPackagedRuntimeEntrypoint(entrypoint, packageSummary, entrypoints) {
1160
+ if (!isSourceEntrypoint(entrypoint.specifier)) {
1161
+ return false;
1162
+ }
1163
+
1164
+ const runtimeBuildSpecifier = runtimeBuildSpecifierFor(entrypoint.specifier);
1165
+ const matchingRuntimeEntrypoint = entrypoints.find(
1166
+ (candidate) =>
1167
+ candidate.requiresBuild &&
1168
+ normalizeEntrypointSpecifier(candidate.specifier) === normalizeEntrypointSpecifier(runtimeBuildSpecifier),
1169
+ );
1170
+ if (matchingRuntimeEntrypoint && repoPathIncludedInNpmPack(packageSummary, matchingRuntimeEntrypoint.relativePath)) {
1171
+ return true;
1172
+ }
1173
+
1174
+ if (
1175
+ entrypoint.kind === "extension" &&
1176
+ entrypoints.some(
1177
+ (candidate) => candidate.kind === "runtimeExtension" && repoPathIncludedInNpmPack(packageSummary, candidate.relativePath),
1178
+ )
1179
+ ) {
1180
+ return true;
1181
+ }
1182
+
1183
+ const packageDir = path.posix.dirname(normalizeRepoPath(packageSummary.path));
1184
+ const runtimeBuildPath = path.posix.join(packageDir === "." ? "" : packageDir, normalizeEntrypointSpecifier(runtimeBuildSpecifier));
1185
+ return repoPathIncludedInNpmPack(packageSummary, runtimeBuildPath);
1186
+ }
1187
+
1091
1188
  function packageMinHostVersionDrift(packageSummary) {
1092
1189
  const openclaw = packageSummary.openclaw;
1093
1190
  if (!nonEmptyString(openclaw?.install?.minHostVersion) || !nonEmptyString(openclaw?.buildOpenClawVersion)) {
package/src/index.js CHANGED
@@ -14,8 +14,10 @@ import * as refDiffApi from "./ref-diff.js";
14
14
  import * as reportApi from "./report.js";
15
15
  import * as runtimeProfileApi from "./runtime-profile.js";
16
16
  import * as runtimeReconciliationApi from "./runtime-reconciliation.js";
17
+ import * as syntheticEntrypointApi from "./synthetic-entrypoint.js";
17
18
  import * as syntheticProbeSuiteApi from "./synthetic-probe-suite.js";
18
19
  import * as syntheticProbesApi from "./synthetic-probes.js";
20
+ import * as batchApi from "./batch.js";
19
21
 
20
22
  export const pluginRoot = Object.freeze({
21
23
  loadConfig: pluginApi.loadPluginConfig,
@@ -25,6 +27,12 @@ export const pluginRoot = Object.freeze({
25
27
  setup: pluginApi.setupPluginInspector,
26
28
  });
27
29
 
30
+ export const batch = Object.freeze({
31
+ discoverPluginRoots: batchApi.discoverPluginRoots,
32
+ run: batchApi.runBatchAnalysis,
33
+ writeReport: batchApi.writeBatchReport,
34
+ });
35
+
28
36
  export const fixtureSuites = Object.freeze({
29
37
  loadConfig: configApi.loadInspectorConfig,
30
38
  inspect: pluginApi.inspectCompatibilityFixtureSetConfig,
@@ -123,12 +131,18 @@ export const synthetic = Object.freeze({
123
131
  renderPlan: syntheticProbesApi.renderSyntheticProbeMarkdown,
124
132
  validatePlan: syntheticProbesApi.validateSyntheticProbePlan,
125
133
  runCaptured: syntheticProbesApi.runCapturedSyntheticProbes,
134
+ runEntrypoint: syntheticEntrypointApi.runEntrypointSyntheticProbes,
126
135
  registrationExecutionProfiles: syntheticProbesApi.syntheticRegistrationExecutionProfiles,
127
136
  defaultHookEvents: syntheticProbesApi.defaultSyntheticHookEvents,
128
137
  defaultHookContexts: syntheticProbesApi.defaultSyntheticHookContexts,
129
138
  defaultRegistrationArguments: syntheticProbesApi.defaultSyntheticRegistrationArguments,
130
139
  });
131
140
 
141
+ export {
142
+ discoverPluginRoots,
143
+ runBatchAnalysis,
144
+ writeBatchReport,
145
+ } from "./batch.js";
132
146
  export {
133
147
  capturePluginEntrypoint,
134
148
  buildFixtureSetColdImportReadiness,
@@ -234,6 +248,7 @@ export {
234
248
  applyRuntimeExecutionCoverage,
235
249
  buildRuntimeExecutionCoverage,
236
250
  } from "./runtime-reconciliation.js";
251
+ export { runEntrypointSyntheticProbes } from "./synthetic-entrypoint.js";
237
252
  export { buildSyntheticProbePlanFromReport } from "./synthetic-probe-suite.js";
238
253
  export {
239
254
  buildSyntheticProbePlan,
package/src/inspector.js CHANGED
@@ -37,6 +37,7 @@ export async function inspectCompatibilityFixtureSet(config, options = {}) {
37
37
  failures,
38
38
  generatedAt: options.generatedAt,
39
39
  executionResults: options.executionResults,
40
+ includeInspectorGaps: options.includeInspectorGaps,
40
41
  targetOpenClaw,
41
42
  buildFixtureReport: ({ fixture, inspection }) =>
42
43
  buildCompatibilityFixtureReport({
package/src/issues.js CHANGED
@@ -14,6 +14,7 @@ export const knownIssueCodes = new Set([
14
14
  "conversation-access-hook",
15
15
  "legacy-before-agent-start",
16
16
  "legacy-root-sdk-import",
17
+ "manifest-name-missing",
17
18
  "manifest-unknown-contracts",
18
19
  "manifest-unknown-fields",
19
20
  "missing-expected-seam",
@@ -111,6 +112,12 @@ export const issueMetadataByCode = {
111
112
  decision: "inspector-follow-up",
112
113
  title: "fixture no longer exposes an expected seam",
113
114
  },
115
+ "manifest-name-missing": {
116
+ severity: "P2",
117
+ owner: "plugin",
118
+ decision: "plugin-upstream-fix",
119
+ title: "manifest display name is missing",
120
+ },
114
121
  "manifest-unknown-contracts": {
115
122
  severity: "P1",
116
123
  owner: "plugin",
@@ -326,6 +333,10 @@ export function classifyIssueFinding(finding, targetOpenClaw, metadata = {}) {
326
333
  };
327
334
  }
328
335
 
336
+ export function isInspectorGapFinding(finding, targetOpenClaw) {
337
+ return issueMetadata(finding, targetOpenClaw).issueClass === "inspector-gap";
338
+ }
339
+
329
340
  export function summarizeIssueClasses(issues) {
330
341
  const summary = {
331
342
  "compat-gap": 0,
@@ -342,7 +353,10 @@ export function summarizeIssueClasses(issues) {
342
353
  }
343
354
 
344
355
  function issueClassFor(code, options) {
345
- if (["unknown-hook-name", "unknown-registration-name", "package-entrypoint-missing", "sdk-export-missing"].includes(code)) {
356
+ if (code === "sdk-export-missing" && options.compatRecord) {
357
+ return "compat-gap";
358
+ }
359
+ if (["unknown-hook-name", "unknown-registration-name", "package-entrypoint-missing"].includes(code)) {
346
360
  return "live-issue";
347
361
  }
348
362
  if (code === "missing-compat-record") {
@@ -369,6 +383,7 @@ function issueClassFor(code, options) {
369
383
  [
370
384
  "manifest-unknown-contracts",
371
385
  "manifest-unknown-fields",
386
+ "manifest-name-missing",
372
387
  "package-json-missing",
373
388
  "package-manifest-version-drift",
374
389
  "package-min-host-version-drift",
@@ -397,7 +412,7 @@ function severityForClass(code, defaultSeverity, options) {
397
412
  if (
398
413
  options.issueClass === "live-issue" &&
399
414
  ["none", "untracked"].includes(options.compatStatus) &&
400
- ["unknown-hook-name", "unknown-registration-name", "package-entrypoint-missing", "sdk-export-missing"].includes(code)
415
+ ["unknown-hook-name", "unknown-registration-name", "package-entrypoint-missing"].includes(code)
401
416
  ) {
402
417
  return "P0";
403
418
  }
package/src/report.js CHANGED
@@ -3,7 +3,7 @@ import { renderMarkdownTable, writeArtifacts, writeJsonMarkdownArtifacts } from
3
3
  import { renderCompatibilityIssuesReport, renderCompatibilityMarkdownReport } from "./compatibility-report.js";
4
4
  import { buildContractProbes } from "./contract-probes.js";
5
5
  import { classifyCompatibilityFixture } from "./fixture-summary.js";
6
- import { buildIssues, summarizeIssueClasses } from "./issues.js";
6
+ import { buildIssues, isInspectorGapFinding, summarizeIssueClasses } from "./issues.js";
7
7
  import { sanitizeReportArtifact } from "./report-sanitizer.js";
8
8
  import { applyRuntimeExecutionCoverage } from "./runtime-reconciliation.js";
9
9
 
@@ -141,18 +141,24 @@ export async function buildCompatibilityReport(options = {}) {
141
141
  decisions,
142
142
  });
143
143
 
144
+ const visibleWarnings = filterVisibleFindings(warnings, targetOpenClaw, options);
145
+ const visibleSuggestions = filterVisibleFindings(suggestions, targetOpenClaw, options);
144
146
  const runtimeCoverage = applyRuntimeExecutionCoverage({
145
- findings: [...warnings, ...suggestions],
147
+ findings: [...visibleWarnings, ...visibleSuggestions],
146
148
  executionResults: options.executionResults,
147
149
  });
148
150
  const issues = buildIssues({
149
151
  breakages,
150
- warnings,
151
- suggestions,
152
+ warnings: visibleWarnings,
153
+ suggestions: visibleSuggestions,
152
154
  targetOpenClaw,
153
155
  idPrefix: options.issueIdPrefix,
154
156
  });
155
- const contractProbes = buildContractProbes({ warnings, suggestions, fixtures: fixtureReports });
157
+ const contractProbes = buildContractProbes({
158
+ warnings: visibleWarnings,
159
+ suggestions: visibleSuggestions,
160
+ fixtures: fixtureReports,
161
+ });
156
162
  const issueSummary = summarizeIssueClasses(issues);
157
163
  const openIssues = issues.filter((issue) => issue.status !== "runtime-covered");
158
164
  const openIssueSummary = summarizeIssueClasses(openIssues);
@@ -165,8 +171,8 @@ export async function buildCompatibilityReport(options = {}) {
165
171
  fixtureCount: fixtureReports.length,
166
172
  highPriorityFixtures: fixtureReports.filter((fixture) => fixture.priority === "high").length,
167
173
  breakageCount: breakages.length,
168
- warningCount: warnings.length,
169
- suggestionCount: suggestions.length,
174
+ warningCount: visibleWarnings.length,
175
+ suggestionCount: visibleSuggestions.length,
170
176
  decisionCount: decisions.length,
171
177
  logCount: logs.length,
172
178
  issueCount: issues.length,
@@ -190,8 +196,8 @@ export async function buildCompatibilityReport(options = {}) {
190
196
  },
191
197
  fixtures: fixtureReports,
192
198
  breakages,
193
- warnings,
194
- suggestions,
199
+ warnings: visibleWarnings,
200
+ suggestions: visibleSuggestions,
195
201
  issues,
196
202
  contractProbes,
197
203
  logs,
@@ -199,6 +205,13 @@ export async function buildCompatibilityReport(options = {}) {
199
205
  };
200
206
  }
201
207
 
208
+ function filterVisibleFindings(findings, targetOpenClaw, options) {
209
+ if (options.includeInspectorGaps === true) {
210
+ return findings;
211
+ }
212
+ return findings.filter((finding) => !isInspectorGapFinding(finding, targetOpenClaw));
213
+ }
214
+
202
215
  export function classifyCompatRecordCoverage({ targetOpenClaw, findings, suggestions, logs, decisions }) {
203
216
  if (targetOpenClaw.status !== "ok") {
204
217
  logs.push({
@@ -225,6 +238,10 @@ export function classifyCompatRecordCoverage({ targetOpenClaw, findings, suggest
225
238
  continue;
226
239
  }
227
240
 
241
+ if (finding.code === "sdk-export-missing") {
242
+ continue;
243
+ }
244
+
228
245
  suggestions.push({
229
246
  fixture: finding.fixture,
230
247
  code: "missing-compat-record",
@@ -28,7 +28,7 @@ export function applyRuntimeExecutionCoverage({ findings = [], executionResults
28
28
  export function buildRuntimeExecutionCoverage(executionResults) {
29
29
  const fixtures = new Map();
30
30
  for (const artifact of executionResults?.artifacts ?? []) {
31
- if (artifact.kind !== "capture") {
31
+ if (!["capture", "synthetic"].includes(artifact.kind)) {
32
32
  continue;
33
33
  }
34
34
 
@@ -84,6 +84,9 @@ function expectedRuntimeCaptureKeys(finding) {
84
84
  if (finding.code === "conversation-access-hook") {
85
85
  return names.map((name) => `hook:${name}`);
86
86
  }
87
+ if (finding.code === "before-tool-call-probe") {
88
+ return ["hook:before_tool_call"];
89
+ }
87
90
  return [];
88
91
  }
89
92