@openclaw/plugin-inspector 0.3.11 → 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/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.3.12 - 2026-06-09
6
+
7
+ ### Changed
8
+
9
+ - Hide maintainer-facing `inspector-gap` findings from author-facing `check`, `ci`, and `batch` output by default, with `--include-inspector-gaps` for internal coverage reports.
10
+
5
11
  ## 0.3.11 - 2026-05-26
6
12
 
7
13
  ### Fixed
package/README.md CHANGED
@@ -170,6 +170,7 @@ Copy-ready examples live in:
170
170
  | `plugin-inspector config` | Print resolved plugin-root config as text or JSON. |
171
171
  | `plugin-inspector init` | Write starter config, scripts, and optional GitHub Actions workflow. |
172
172
  | `plugin-inspector report` | Run a fixture-suite config with many plugins. |
173
+ | `plugin-inspector batch` | Discover plugin roots under a folder and write one aggregate impact report. |
173
174
  | `plugin-inspector capture` | Runtime-capture one entrypoint directly. |
174
175
 
175
176
  Common options:
@@ -186,6 +187,7 @@ Common options:
186
187
  | `--mock-sdk` / `--sdk mock` | Use generated SDK and external-package mocks for runtime capture. |
187
188
  | `--real-sdk` / `--sdk real` | Use installed real SDK dependencies instead of mocks. |
188
189
  | `--allow-execute` | Permit commands that import plugin code. |
190
+ | `--include-inspector-gaps` | Include maintainer-facing scanner coverage gaps in `check`, `ci`, and `batch` output. |
189
191
  | `--json` | Print machine-readable JSON to stdout. |
190
192
  | `--sarif [path]` | Write SARIF from `check` or `inspect`; `ci` enables this by default. |
191
193
  | `--junit [path]` | Write JUnit XML from `check` or `inspect`; `ci` enables this by default. |
@@ -305,8 +307,9 @@ Important report sections:
305
307
  | `logs` | Informational inventory and coverage rows. |
306
308
  | `decisions` | Maintainer-facing follow-up or compatibility-policy decisions. |
307
309
 
308
- Issue classes currently flow through the reports as live issues, compat gaps,
309
- deprecation warnings, inspector gaps, upstream metadata, and fixture regressions.
310
+ Author-facing `check`, `ci`, and `batch` output hides inspector gaps by default
311
+ because they are scanner coverage backlog, not plugin-author fixes. Pass
312
+ `--include-inspector-gaps` for Crabpot-style maintainer coverage reports.
310
313
 
311
314
  ## CI Policy And Shared Reporting Primitives
312
315
 
@@ -404,6 +407,7 @@ Stable grouped facades:
404
407
  | `reports` | Render/write reports and classify issue findings. |
405
408
  | `contracts` | Build, render, validate, and write contract captures and coverage. |
406
409
  | `ci` | Build summaries, policy reports, execution results, SARIF, and JUnit outputs. |
410
+ | `batch` | Discover plugin roots and aggregate compatibility findings across a corpus. |
407
411
  | `runtime` | Build runtime profiles, profile diffs, ref diffs, and import-loop profiles. |
408
412
  | `synthetic` | Build and run synthetic probe plans. |
409
413
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/plugin-inspector",
3
- "version": "0.3.11",
3
+ "version": "0.3.12",
4
4
  "private": false,
5
5
  "description": "Offline compatibility inspector for OpenClaw plugins.",
6
6
  "type": "module",
package/src/api.js CHANGED
@@ -44,6 +44,7 @@ export async function inspectPluginRoot(options = {}) {
44
44
  const config = await loadPluginConfig(options);
45
45
  return inspectCompatibilityFixtureSet(config, {
46
46
  generatedAt: options.generatedAt,
47
+ includeInspectorGaps: options.includeInspectorGaps,
47
48
  openclawPath: options.openclawPath,
48
49
  executionResults: options.executionResults,
49
50
  targetOpenClaw: options.targetOpenClaw,
@@ -59,6 +60,7 @@ export async function inspectCompatibilityFixtureSetConfig(options = {}) {
59
60
  const config = await loadFixtureSetConfig(options);
60
61
  return inspectCompatibilityFixtureSet(config, {
61
62
  generatedAt: options.generatedAt,
63
+ includeInspectorGaps: options.includeInspectorGaps,
62
64
  openclawPath: options.openclawPath,
63
65
  executionResults: options.executionResults,
64
66
  targetOpenClaw: options.targetOpenClaw,
@@ -113,6 +115,7 @@ export async function buildFixtureSetColdImportReadiness(options = {}) {
113
115
  options.report ??
114
116
  (await inspectCompatibilityFixtureSet(config, {
115
117
  generatedAt: options.generatedAt,
118
+ includeInspectorGaps: options.includeInspectorGaps,
116
119
  openclawPath: options.openclawPath,
117
120
  executionResults: options.executionResults,
118
121
  targetOpenClaw: options.targetOpenClaw,
@@ -145,6 +148,7 @@ export async function buildFixtureSetWorkspacePlan(options = {}) {
145
148
  options.report ??
146
149
  (await inspectCompatibilityFixtureSet(config, {
147
150
  generatedAt: options.generatedAt,
151
+ includeInspectorGaps: options.includeInspectorGaps,
148
152
  openclawPath: options.openclawPath,
149
153
  executionResults: options.executionResults,
150
154
  targetOpenClaw: options.targetOpenClaw,
package/src/batch.js ADDED
@@ -0,0 +1,297 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdtemp, readFile, readdir, rm } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { renderMarkdownTable, writeJsonMarkdownArtifacts } from "./artifacts.js";
6
+ import { runPluginCheck } from "./api.js";
7
+
8
+ const ignoredDirs = new Set([
9
+ ".git",
10
+ ".hg",
11
+ ".svn",
12
+ "node_modules",
13
+ "reports",
14
+ "dist",
15
+ "build",
16
+ ".plugin-inspector",
17
+ ]);
18
+
19
+ export async function runBatchAnalysis(options = {}) {
20
+ const rootDir = path.resolve(options.rootDir ?? options.inputDir ?? process.cwd());
21
+ const outDir = options.outDir ?? "reports";
22
+ const outRoot = path.resolve(rootDir, outDir);
23
+ const concurrency = Math.max(1, Math.min(Math.round(options.concurrency ?? 4), 32));
24
+ const keepPluginReports = options.keepPluginReports === true;
25
+ const pluginRoots = await discoverPluginRoots(rootDir);
26
+ const tempRoot = keepPluginReports ? null : await mkdtemp(path.join(os.tmpdir(), "plugin-inspector-batch-"));
27
+ const entries = [];
28
+
29
+ try {
30
+ await runWithConcurrency(pluginRoots, concurrency, async (pluginRoot) => {
31
+ const reportsRoot = keepPluginReports
32
+ ? path.join(outRoot, "plugins", slugForPath(path.relative(rootDir, pluginRoot)))
33
+ : path.join(tempRoot, slugForPath(path.relative(rootDir, pluginRoot)));
34
+ entries.push(
35
+ await inspectBatchPlugin(pluginRoot, {
36
+ ...options,
37
+ rootDir,
38
+ outDir: reportsRoot,
39
+ openclawPath: options.openclawPath,
40
+ }),
41
+ );
42
+ });
43
+ } finally {
44
+ if (tempRoot) await rm(tempRoot, { recursive: true, force: true });
45
+ }
46
+
47
+ entries.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
48
+ const report = buildBatchReport({
49
+ rootDir,
50
+ entries,
51
+ generatedAt: options.generatedAt ?? new Date().toISOString(),
52
+ });
53
+ const paths = await writeBatchReport(report, { outDir: outRoot, check: options.checkArtifacts });
54
+ return { report, paths };
55
+ }
56
+
57
+ export async function discoverPluginRoots(rootDir) {
58
+ const roots = [];
59
+ await walk(path.resolve(rootDir));
60
+ roots.sort();
61
+ return roots;
62
+
63
+ async function walk(dir) {
64
+ if (await isPluginRoot(dir)) {
65
+ roots.push(dir);
66
+ return;
67
+ }
68
+ const entries = await readdir(dir, { withFileTypes: true });
69
+ for (const entry of entries) {
70
+ if (!entry.isDirectory() || ignoredDirs.has(entry.name)) continue;
71
+ await walk(path.join(dir, entry.name));
72
+ }
73
+ }
74
+ }
75
+
76
+ export async function writeBatchReport(report, options = {}) {
77
+ return writeJsonMarkdownArtifacts({
78
+ jsonPath: path.join(options.outDir ?? "reports", "plugin-inspector-batch-report.json"),
79
+ markdownPath: path.join(options.outDir ?? "reports", "plugin-inspector-batch-report.md"),
80
+ json: report,
81
+ markdown: renderBatchMarkdown(report),
82
+ check: options.check,
83
+ });
84
+ }
85
+
86
+ function buildBatchReport({ rootDir, entries, generatedAt }) {
87
+ const findingFrequency = findingFrequencyRows(entries);
88
+ const summary = {
89
+ pluginCount: entries.length,
90
+ passed: entries.filter((entry) => entry.status === "pass").length,
91
+ failed: entries.filter((entry) => entry.status !== "pass").length,
92
+ pluginsWithErrors: entries.filter((entry) => entry.errorCount > 0).length,
93
+ pluginsWithWarnings: entries.filter((entry) => entry.warningCount > 0).length,
94
+ errorCount: entries.reduce((sum, entry) => sum + entry.errorCount, 0),
95
+ warningCount: entries.reduce((sum, entry) => sum + entry.warningCount, 0),
96
+ findingCodeCount: findingFrequency.length,
97
+ };
98
+ return {
99
+ generatedAt,
100
+ rootDir,
101
+ summary,
102
+ findingFrequency,
103
+ plugins: entries,
104
+ };
105
+ }
106
+
107
+ async function inspectBatchPlugin(pluginRoot, options) {
108
+ try {
109
+ const { report } = await runPluginCheck({
110
+ allowExecution: options.allowExecution,
111
+ capture: options.capture,
112
+ configPath: options.configPath,
113
+ includeInspectorGaps: options.includeInspectorGaps,
114
+ mockSdk: options.mockSdk,
115
+ openclawPath: options.openclawPath,
116
+ outDir: options.outDir,
117
+ pluginRoot,
118
+ });
119
+ const findings = normalizeReportFindings(report);
120
+ return {
121
+ pluginRoot,
122
+ relativePath: path.relative(options.rootDir ?? process.cwd(), pluginRoot) || ".",
123
+ status: report.status,
124
+ packageName: packageNameFromReport(report),
125
+ targetOpenClaw: report.targetOpenClaw,
126
+ errorCount: findings.filter((finding) => finding.kind === "error").length,
127
+ warningCount: findings.filter((finding) => finding.kind === "warning").length,
128
+ findings,
129
+ };
130
+ } catch (error) {
131
+ return {
132
+ pluginRoot,
133
+ relativePath: path.relative(options.rootDir ?? process.cwd(), pluginRoot) || ".",
134
+ status: "error",
135
+ packageName: path.basename(pluginRoot),
136
+ targetOpenClaw: null,
137
+ errorCount: 1,
138
+ warningCount: 0,
139
+ findings: [
140
+ {
141
+ kind: "error",
142
+ code: "plugin-inspector-batch-failure",
143
+ message: error instanceof Error ? error.message : String(error),
144
+ },
145
+ ],
146
+ };
147
+ }
148
+ }
149
+
150
+ function normalizeReportFindings(report) {
151
+ return [
152
+ ...(report.breakages ?? []).map((finding) => normalizeFinding(finding, "error")),
153
+ ...(report.issues ?? []).map((finding) =>
154
+ normalizeFinding(
155
+ finding,
156
+ finding.status === "blocking" || finding.severity === "P0" ? "error" : "warning",
157
+ ),
158
+ ),
159
+ ...(report.warnings ?? []).map((finding) => normalizeFinding(finding, "warning")),
160
+ ...(report.suggestions ?? []).map((finding) => normalizeFinding(finding, "warning")),
161
+ ];
162
+ }
163
+
164
+ function normalizeFinding(finding, kind) {
165
+ return {
166
+ kind,
167
+ code: finding.code ?? "plugin-inspector-finding",
168
+ severity: finding.severity,
169
+ issueClass: finding.issueClass,
170
+ message: finding.message ?? finding.title ?? "See plugin report.",
171
+ evidence: finding.evidence,
172
+ };
173
+ }
174
+
175
+ function findingFrequencyRows(entries) {
176
+ const byCode = new Map();
177
+ for (const entry of entries) {
178
+ const seenForPlugin = new Set();
179
+ for (const finding of entry.findings) {
180
+ const current = byCode.get(finding.code) ?? {
181
+ code: finding.code,
182
+ count: 0,
183
+ plugins: 0,
184
+ errors: 0,
185
+ warnings: 0,
186
+ };
187
+ current.count += 1;
188
+ if (!seenForPlugin.has(finding.code)) {
189
+ current.plugins += 1;
190
+ seenForPlugin.add(finding.code);
191
+ }
192
+ if (finding.kind === "error") current.errors += 1;
193
+ else current.warnings += 1;
194
+ byCode.set(finding.code, current);
195
+ }
196
+ }
197
+ return [...byCode.values()].sort((a, b) => b.plugins - a.plugins || b.count - a.count);
198
+ }
199
+
200
+ function renderBatchMarkdown(report) {
201
+ return [
202
+ "# Plugin Inspector Batch Report",
203
+ "",
204
+ `Generated: ${report.generatedAt}`,
205
+ `Root: ${report.rootDir}`,
206
+ "",
207
+ "## Summary",
208
+ "",
209
+ renderMarkdownTable(
210
+ [
211
+ ["Plugins", report.summary.pluginCount],
212
+ ["Passed", report.summary.passed],
213
+ ["Failed", report.summary.failed],
214
+ ["Plugins with errors", report.summary.pluginsWithErrors],
215
+ ["Plugins with warnings", report.summary.pluginsWithWarnings],
216
+ ["Errors", report.summary.errorCount],
217
+ ["Warnings", report.summary.warningCount],
218
+ ],
219
+ ["Metric", "Value"],
220
+ ),
221
+ "",
222
+ "## Finding Frequency",
223
+ "",
224
+ report.findingFrequency.length
225
+ ? renderMarkdownTable(
226
+ report.findingFrequency.map((row) => [
227
+ row.code,
228
+ row.plugins,
229
+ row.count,
230
+ row.errors,
231
+ row.warnings,
232
+ ]),
233
+ ["Code", "Plugins", "Findings", "Errors", "Warnings"],
234
+ )
235
+ : "_No findings._",
236
+ "",
237
+ "## Plugins",
238
+ "",
239
+ report.plugins.length
240
+ ? renderMarkdownTable(
241
+ report.plugins.map((plugin) => [
242
+ plugin.packageName,
243
+ plugin.relativePath,
244
+ plugin.status,
245
+ plugin.errorCount,
246
+ plugin.warningCount,
247
+ ]),
248
+ ["Package", "Path", "Status", "Errors", "Warnings"],
249
+ )
250
+ : "_No plugin roots discovered._",
251
+ ].join("\n");
252
+ }
253
+
254
+ async function runWithConcurrency(items, concurrency, worker) {
255
+ let index = 0;
256
+ const runners = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
257
+ while (index < items.length) {
258
+ const item = items[index];
259
+ index += 1;
260
+ await worker(item);
261
+ }
262
+ });
263
+ await Promise.all(runners);
264
+ }
265
+
266
+ async function isPluginRoot(dir) {
267
+ if (existsSync(path.join(dir, "plugin-inspector.config.json"))) return true;
268
+ if (existsSync(path.join(dir, ".plugin-inspector.json"))) return true;
269
+ if (existsSync(path.join(dir, "openclaw.plugin.json"))) return true;
270
+ const packageJsonPath = path.join(dir, "package.json");
271
+ if (!existsSync(packageJsonPath)) return false;
272
+ try {
273
+ const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
274
+ return Boolean(packageJson.openclaw || packageJson.pluginInspector || packageJson["plugin-inspector"]);
275
+ } catch {
276
+ return false;
277
+ }
278
+ }
279
+
280
+ function packageNameFromReport(report) {
281
+ return (
282
+ report.fixtures?.[0]?.package?.packageJson?.name ??
283
+ report.fixtures?.[0]?.package?.name ??
284
+ report.fixtures?.[0]?.name ??
285
+ report.fixtures?.[0]?.id ??
286
+ "plugin"
287
+ );
288
+ }
289
+
290
+ function slugForPath(value) {
291
+ return (
292
+ String(value)
293
+ .replaceAll(path.sep, "-")
294
+ .replace(/[^a-zA-Z0-9._-]+/g, "-")
295
+ .replace(/^-+|-+$/g, "") || "plugin"
296
+ );
297
+ }
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),
@@ -326,6 +382,34 @@ function renderCiTextSummary(summary) {
326
382
  ].join("\n");
327
383
  }
328
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
+
329
413
  function initCommandSummary(result) {
330
414
  return {
331
415
  dryRun: result.dryRun,
@@ -353,16 +437,18 @@ function printHelp() {
353
437
 
354
438
  Usage:
355
439
  plugin-inspector
356
- 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]
357
441
  plugin-inspector config [--plugin-root <path>] [--config <path>] [--json]
358
442
  plugin-inspector init [--plugin-root <path>] [--config <path>] [--ci] [--scripts] [--package-manager npm|pnpm|yarn|bun] [--dry-run] [--json] [--force]
359
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]
360
445
  plugin-inspector inspect [--plugin-root <path>] [--config <path>] [--out <dir>] [--check] [--json] [--sarif [path]] [--junit [path]] [--allow-execute]
361
- 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]
362
447
  plugin-inspector capture <entrypoint> [--mock-sdk|--real-sdk] [--allow-execute] [--plugin-root <path>] [--output <path>]
363
448
 
364
449
  Default check runs from the current plugin root and writes reports/ unless --out is set.
365
450
  CI writes SARIF and JUnit artifacts by default; check/inspect can write them with --sarif and --junit.
366
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.
367
453
  `);
368
454
  }
package/src/index.js CHANGED
@@ -17,6 +17,7 @@ import * as runtimeReconciliationApi from "./runtime-reconciliation.js";
17
17
  import * as syntheticEntrypointApi from "./synthetic-entrypoint.js";
18
18
  import * as syntheticProbeSuiteApi from "./synthetic-probe-suite.js";
19
19
  import * as syntheticProbesApi from "./synthetic-probes.js";
20
+ import * as batchApi from "./batch.js";
20
21
 
21
22
  export const pluginRoot = Object.freeze({
22
23
  loadConfig: pluginApi.loadPluginConfig,
@@ -26,6 +27,12 @@ export const pluginRoot = Object.freeze({
26
27
  setup: pluginApi.setupPluginInspector,
27
28
  });
28
29
 
30
+ export const batch = Object.freeze({
31
+ discoverPluginRoots: batchApi.discoverPluginRoots,
32
+ run: batchApi.runBatchAnalysis,
33
+ writeReport: batchApi.writeBatchReport,
34
+ });
35
+
29
36
  export const fixtureSuites = Object.freeze({
30
37
  loadConfig: configApi.loadInspectorConfig,
31
38
  inspect: pluginApi.inspectCompatibilityFixtureSetConfig,
@@ -131,6 +138,11 @@ export const synthetic = Object.freeze({
131
138
  defaultRegistrationArguments: syntheticProbesApi.defaultSyntheticRegistrationArguments,
132
139
  });
133
140
 
141
+ export {
142
+ discoverPluginRoots,
143
+ runBatchAnalysis,
144
+ writeBatchReport,
145
+ } from "./batch.js";
134
146
  export {
135
147
  capturePluginEntrypoint,
136
148
  buildFixtureSetColdImportReadiness,
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
@@ -333,6 +333,10 @@ export function classifyIssueFinding(finding, targetOpenClaw, metadata = {}) {
333
333
  };
334
334
  }
335
335
 
336
+ export function isInspectorGapFinding(finding, targetOpenClaw) {
337
+ return issueMetadata(finding, targetOpenClaw).issueClass === "inspector-gap";
338
+ }
339
+
336
340
  export function summarizeIssueClasses(issues) {
337
341
  const summary = {
338
342
  "compat-gap": 0,
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({
package/src/sdk-mock.js CHANGED
@@ -671,6 +671,24 @@ function resolveExistingSourcePath(target) {
671
671
  ` : ""}
672
672
  function createMockValue(name) {
673
673
  function fn(...args) {
674
+ if (name === "resolveDefaultAgentDir") {
675
+ return mockAgentDir();
676
+ }
677
+ if (name === "resolveAgentDir") {
678
+ return mockAgentDir(args[1]);
679
+ }
680
+ if (name === "resolveUserPath") {
681
+ return typeof args[0] === "string" ? args[0] : mockAgentDir();
682
+ }
683
+ if (name === "resolveAuthProfileOrder") {
684
+ return [];
685
+ }
686
+ if (name === "resolveWindowsSpawnProgram") {
687
+ return mockWindowsSpawnProgram(args[0]);
688
+ }
689
+ if (name === "materializeWindowsSpawnProgram") {
690
+ return mockWindowsSpawnInvocation(args[0], args[1]);
691
+ }
674
692
  if (name === "resolvePreferredOpenClawTmpDir") {
675
693
  return process.env.TMPDIR || "/tmp";
676
694
  }
@@ -707,6 +725,80 @@ function createMockValue(name) {
707
725
  });
708
726
  }
709
727
 
728
+ function mockAgentDir(agentId = "main") {
729
+ const base = process.env.TMPDIR || process.env.TEMP || process.env.TMP || "/tmp";
730
+ const safeAgentId = String(agentId || "main").replace(/[^a-zA-Z0-9._-]/g, "-");
731
+ return base.replace(/[\\/]+$/, "") + "/plugin-inspector-openclaw/agents/" + safeAgentId + "/agent";
732
+ }
733
+
734
+ function mockWindowsSpawnProgram(params = {}) {
735
+ return {
736
+ command: typeof params.command === "string" && params.command.trim() ? params.command : process.execPath,
737
+ leadingArgv: [],
738
+ resolution: "mock",
739
+ packageName: typeof params.packageName === "string" ? params.packageName : undefined,
740
+ };
741
+ }
742
+
743
+ function mockWindowsSpawnInvocation(program = {}, argv = []) {
744
+ const command = typeof program.command === "string" && program.command.trim() ? program.command : process.execPath;
745
+ if (program.packageName === "@openai/codex") {
746
+ return {
747
+ command: process.execPath,
748
+ argv: ["-e", mockCodexAppServerScript()],
749
+ resolution: program.resolution ?? "mock",
750
+ windowsHide: true,
751
+ };
752
+ }
753
+ return {
754
+ command,
755
+ argv: [...(Array.isArray(program.leadingArgv) ? program.leadingArgv : []), ...(Array.isArray(argv) ? argv : [])],
756
+ resolution: program.resolution ?? "mock",
757
+ shell: program.shell,
758
+ windowsHide: program.windowsHide,
759
+ };
760
+ }
761
+
762
+ function mockCodexAppServerScript() {
763
+ return [
764
+ "const readline = require('node:readline');",
765
+ "const rl = readline.createInterface({ input: process.stdin });",
766
+ "let idleTimer;",
767
+ "function scheduleIdleExit() {",
768
+ " if (idleTimer) clearTimeout(idleTimer);",
769
+ " idleTimer = setTimeout(() => process.exit(0), 1000);",
770
+ "}",
771
+ "function write(id, result) { process.stdout.write(JSON.stringify({ id, result }) + String.fromCharCode(10)); }",
772
+ "rl.on('line', (line) => {",
773
+ " let message;",
774
+ " try { message = JSON.parse(line); } catch { return; }",
775
+ " if (message.id === undefined || message.id === null) return;",
776
+ " switch (message.method) {",
777
+ " case 'initialize':",
778
+ " write(message.id, { userAgent: 'openclaw/999.0.0 (plugin-inspector mock)' });",
779
+ " break;",
780
+ " case 'model/list':",
781
+ " write(message.id, { data: [] });",
782
+ " break;",
783
+ " case 'thread/list':",
784
+ " case 'mcpServerStatus/list':",
785
+ " case 'skills/list':",
786
+ " write(message.id, { data: [] });",
787
+ " break;",
788
+ " case 'account/read':",
789
+ " write(message.id, null);",
790
+ " break;",
791
+ " case 'account/rateLimits/read':",
792
+ " write(message.id, null);",
793
+ " break;",
794
+ " default:",
795
+ " process.stdout.write(JSON.stringify({ id: message.id, error: { code: -32601, message: 'mock method not implemented' } }) + String.fromCharCode(10));",
796
+ " }",
797
+ " scheduleIdleExit();",
798
+ "});",
799
+ ].join("\\n");
800
+ }
801
+
710
802
  function createZNamespace() {
711
803
  const namespace = {
712
804
  any: () => createSchema(),