@openclaw/plugin-inspector 0.3.11 → 0.3.13

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,20 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.3.13 - 2026-06-09
6
+
7
+ ### Changed
8
+
9
+ - Add `authorRemediation.summary` and `authorRemediation.docsUrl` guidance to author-facing compatibility issues and Markdown reports.
10
+ - Add `--author-facing` for `check`, `ci`, and `batch` reports while keeping default output complete for internal coverage findings.
11
+ - Replace the recent `--include-inspector-gaps` option with a clear error pointing to `--author-facing`.
12
+
13
+ ## 0.3.12 - 2026-06-09
14
+
15
+ ### Changed
16
+
17
+ - Hide maintainer-facing `inspector-gap` findings from author-facing `check`, `ci`, and `batch` output by default, with `--include-inspector-gaps` for internal coverage reports.
18
+
5
19
  ## 0.3.11 - 2026-05-26
6
20
 
7
21
  ### 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
+ | `--author-facing` | Limit `check`, `ci`, and `batch` reports to findings with `authorRemediation` guidance. |
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,10 @@ 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
+ Default `check`, `ci`, and `batch` reports include both author-facing and
311
+ internal findings. Pass `--author-facing` when producing plugin-author output;
312
+ that filtered view includes only findings with `authorRemediation.summary` and
313
+ `authorRemediation.docsUrl`.
310
314
 
311
315
  ## CI Policy And Shared Reporting Primitives
312
316
 
@@ -404,6 +408,7 @@ Stable grouped facades:
404
408
  | `reports` | Render/write reports and classify issue findings. |
405
409
  | `contracts` | Build, render, validate, and write contract captures and coverage. |
406
410
  | `ci` | Build summaries, policy reports, execution results, SARIF, and JUnit outputs. |
411
+ | `batch` | Discover plugin roots and aggregate compatibility findings across a corpus. |
407
412
  | `runtime` | Build runtime profiles, profile diffs, ref diffs, and import-loop profiles. |
408
413
  | `synthetic` | Build and run synthetic probe plans. |
409
414
 
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.13",
4
4
  "private": false,
5
5
  "description": "Offline compatibility inspector for OpenClaw plugins.",
6
6
  "type": "module",
package/src/advanced.js CHANGED
@@ -77,6 +77,7 @@ export {
77
77
  classifyIssueFinding,
78
78
  deprecatedCompatRecords,
79
79
  issueId,
80
+ isAuthorFacingFinding,
80
81
  issueMetadata,
81
82
  issueMetadataByCode,
82
83
  knownIssueCodes,
package/src/api.js CHANGED
@@ -43,6 +43,7 @@ export async function loadPluginConfig(options = {}) {
43
43
  export async function inspectPluginRoot(options = {}) {
44
44
  const config = await loadPluginConfig(options);
45
45
  return inspectCompatibilityFixtureSet(config, {
46
+ authorFacing: options.authorFacing,
46
47
  generatedAt: options.generatedAt,
47
48
  openclawPath: options.openclawPath,
48
49
  executionResults: options.executionResults,
@@ -58,6 +59,7 @@ export async function inspectFixtureSetConfig(options = {}) {
58
59
  export async function inspectCompatibilityFixtureSetConfig(options = {}) {
59
60
  const config = await loadFixtureSetConfig(options);
60
61
  return inspectCompatibilityFixtureSet(config, {
62
+ authorFacing: options.authorFacing,
61
63
  generatedAt: options.generatedAt,
62
64
  openclawPath: options.openclawPath,
63
65
  executionResults: options.executionResults,
@@ -112,6 +114,7 @@ export async function buildFixtureSetColdImportReadiness(options = {}) {
112
114
  const report =
113
115
  options.report ??
114
116
  (await inspectCompatibilityFixtureSet(config, {
117
+ authorFacing: options.authorFacing,
115
118
  generatedAt: options.generatedAt,
116
119
  openclawPath: options.openclawPath,
117
120
  executionResults: options.executionResults,
@@ -144,6 +147,7 @@ export async function buildFixtureSetWorkspacePlan(options = {}) {
144
147
  const report =
145
148
  options.report ??
146
149
  (await inspectCompatibilityFixtureSet(config, {
150
+ authorFacing: options.authorFacing,
147
151
  generatedAt: options.generatedAt,
148
152
  openclawPath: options.openclawPath,
149
153
  executionResults: options.executionResults,
package/src/batch.js ADDED
@@ -0,0 +1,309 @@
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
+ authorFacing: options.authorFacing,
112
+ capture: options.capture,
113
+ configPath: options.configPath,
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
+ const issueFindings = (report.issues ?? []).map((finding) =>
152
+ normalizeFinding(
153
+ finding,
154
+ finding.status === "blocking" || finding.severity === "P0" ? "error" : "warning",
155
+ ),
156
+ );
157
+ const issueKeys = new Set(issueFindings.map(findingKey));
158
+ const rawFindings = [
159
+ ...(report.breakages ?? []).map((finding) => normalizeFinding(finding, "error")),
160
+ ...(report.warnings ?? []).map((finding) => normalizeFinding(finding, "warning")),
161
+ ...(report.suggestions ?? []).map((finding) => normalizeFinding(finding, "warning")),
162
+ ].filter((finding) => !issueKeys.has(findingKey(finding)));
163
+
164
+ return [...issueFindings, ...rawFindings];
165
+ }
166
+
167
+ function normalizeFinding(finding, kind) {
168
+ return {
169
+ kind,
170
+ code: finding.code ?? "plugin-inspector-finding",
171
+ severity: finding.severity,
172
+ issueClass: finding.issueClass,
173
+ message: finding.message ?? finding.title ?? "See plugin report.",
174
+ evidence: finding.evidence,
175
+ ...(finding.authorRemediation ? { authorRemediation: finding.authorRemediation } : {}),
176
+ };
177
+ }
178
+
179
+ function findingKey(finding) {
180
+ return [
181
+ finding.fixture ?? "",
182
+ finding.code ?? "",
183
+ ...(Array.isArray(finding.evidence) ? finding.evidence : []),
184
+ ].join("\n");
185
+ }
186
+
187
+ function findingFrequencyRows(entries) {
188
+ const byCode = new Map();
189
+ for (const entry of entries) {
190
+ const seenForPlugin = new Set();
191
+ for (const finding of entry.findings) {
192
+ const current = byCode.get(finding.code) ?? {
193
+ code: finding.code,
194
+ count: 0,
195
+ plugins: 0,
196
+ errors: 0,
197
+ warnings: 0,
198
+ };
199
+ current.count += 1;
200
+ if (!seenForPlugin.has(finding.code)) {
201
+ current.plugins += 1;
202
+ seenForPlugin.add(finding.code);
203
+ }
204
+ if (finding.kind === "error") current.errors += 1;
205
+ else current.warnings += 1;
206
+ byCode.set(finding.code, current);
207
+ }
208
+ }
209
+ return [...byCode.values()].sort((a, b) => b.plugins - a.plugins || b.count - a.count);
210
+ }
211
+
212
+ function renderBatchMarkdown(report) {
213
+ return [
214
+ "# Plugin Inspector Batch Report",
215
+ "",
216
+ `Generated: ${report.generatedAt}`,
217
+ `Root: ${report.rootDir}`,
218
+ "",
219
+ "## Summary",
220
+ "",
221
+ renderMarkdownTable(
222
+ [
223
+ ["Plugins", report.summary.pluginCount],
224
+ ["Passed", report.summary.passed],
225
+ ["Failed", report.summary.failed],
226
+ ["Plugins with errors", report.summary.pluginsWithErrors],
227
+ ["Plugins with warnings", report.summary.pluginsWithWarnings],
228
+ ["Errors", report.summary.errorCount],
229
+ ["Warnings", report.summary.warningCount],
230
+ ],
231
+ ["Metric", "Value"],
232
+ ),
233
+ "",
234
+ "## Finding Frequency",
235
+ "",
236
+ report.findingFrequency.length
237
+ ? renderMarkdownTable(
238
+ report.findingFrequency.map((row) => [
239
+ row.code,
240
+ row.plugins,
241
+ row.count,
242
+ row.errors,
243
+ row.warnings,
244
+ ]),
245
+ ["Code", "Plugins", "Findings", "Errors", "Warnings"],
246
+ )
247
+ : "_No findings._",
248
+ "",
249
+ "## Plugins",
250
+ "",
251
+ report.plugins.length
252
+ ? renderMarkdownTable(
253
+ report.plugins.map((plugin) => [
254
+ plugin.packageName,
255
+ plugin.relativePath,
256
+ plugin.status,
257
+ plugin.errorCount,
258
+ plugin.warningCount,
259
+ ]),
260
+ ["Package", "Path", "Status", "Errors", "Warnings"],
261
+ )
262
+ : "_No plugin roots discovered._",
263
+ ].join("\n");
264
+ }
265
+
266
+ async function runWithConcurrency(items, concurrency, worker) {
267
+ let index = 0;
268
+ const runners = Array.from({ length: Math.min(concurrency, items.length) }, async () => {
269
+ while (index < items.length) {
270
+ const item = items[index];
271
+ index += 1;
272
+ await worker(item);
273
+ }
274
+ });
275
+ await Promise.all(runners);
276
+ }
277
+
278
+ async function isPluginRoot(dir) {
279
+ if (existsSync(path.join(dir, "plugin-inspector.config.json"))) return true;
280
+ if (existsSync(path.join(dir, ".plugin-inspector.json"))) return true;
281
+ if (existsSync(path.join(dir, "openclaw.plugin.json"))) return true;
282
+ const packageJsonPath = path.join(dir, "package.json");
283
+ if (!existsSync(packageJsonPath)) return false;
284
+ try {
285
+ const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
286
+ return Boolean(packageJson.openclaw || packageJson.pluginInspector || packageJson["plugin-inspector"]);
287
+ } catch {
288
+ return false;
289
+ }
290
+ }
291
+
292
+ function packageNameFromReport(report) {
293
+ return (
294
+ report.fixtures?.[0]?.package?.packageJson?.name ??
295
+ report.fixtures?.[0]?.package?.name ??
296
+ report.fixtures?.[0]?.name ??
297
+ report.fixtures?.[0]?.id ??
298
+ "plugin"
299
+ );
300
+ }
301
+
302
+ function slugForPath(value) {
303
+ return (
304
+ String(value)
305
+ .replaceAll(path.sep, "-")
306
+ .replace(/[^a-zA-Z0-9._-]+/g, "-")
307
+ .replace(/^-+|-+$/g, "") || "plugin"
308
+ );
309
+ }
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 authorFacing = readAuthorFacingFlag(commandArgs);
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
+ authorFacing,
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,8 +110,10 @@ async function runCheck(commandArgs) {
75
110
  const mockSdk = readMockSdkFlag(commandArgs);
76
111
  const allowExecution = readAllowExecutionFlag(commandArgs);
77
112
  const ciOutputs = readCiOutputFlags(commandArgs);
113
+ const authorFacing = readAuthorFacingFlag(commandArgs);
78
114
  const { report, paths } = await runPluginCheck({
79
115
  allowExecution,
116
+ authorFacing,
80
117
  capture,
81
118
  configPath,
82
119
  mockSdk,
@@ -131,12 +168,18 @@ async function runInit(commandArgs) {
131
168
  async function runReport(command, commandArgs) {
132
169
  const configPath = readFlag(commandArgs, "--config");
133
170
  const outDir = readFlag(commandArgs, "--out") ?? "reports";
171
+ const openclawPath = commandArgs.includes("--no-openclaw") ? false : readFlag(commandArgs, "--openclaw");
134
172
  const check = commandArgs.includes("--check") || command === "ci";
135
173
  const json = commandArgs.includes("--json");
136
174
  const ciOutputs = readCiOutputFlags(commandArgs);
175
+ const authorFacing = readAuthorFacingFlag(commandArgs);
137
176
  const config = await loadInspectorConfig(configPath);
138
- const report = await inspectFixtureSet(config);
139
- const paths = await writeReport(report, { outDir });
177
+ const report = authorFacing
178
+ ? await inspectCompatibilityFixtureSet(config, { authorFacing, openclawPath })
179
+ : await inspectFixtureSet(config);
180
+ const paths = authorFacing
181
+ ? await writeCompatibilityReport(report, { cwd: config.rootDir, outDir })
182
+ : await writeReport(report, { outDir });
140
183
  await writeCiOutputArtifacts(report, {
141
184
  ...ciOutputs,
142
185
  cwd: path.dirname(paths.jsonPath),
@@ -164,8 +207,10 @@ async function runCi(commandArgs) {
164
207
  const mockSdk = readMockSdkFlag(commandArgs);
165
208
  const allowExecution = readAllowExecutionFlag(commandArgs);
166
209
  const ciOutputs = readCiOutputFlags(commandArgs, { defaultEnabled: true });
210
+ const authorFacing = readAuthorFacingFlag(commandArgs);
167
211
  const { report, reportDir } = await runCiCompatibilityReport({
168
212
  allowExecution,
213
+ authorFacing,
169
214
  capture,
170
215
  configPath,
171
216
  mockSdk,
@@ -204,10 +249,19 @@ async function runCi(commandArgs) {
204
249
  }
205
250
  }
206
251
 
207
- async function runCiCompatibilityReport({ allowExecution, capture, configPath, mockSdk, openclawPath, outDir, pluginRoot }) {
252
+ async function runCiCompatibilityReport({
253
+ allowExecution,
254
+ authorFacing,
255
+ capture,
256
+ configPath,
257
+ mockSdk,
258
+ openclawPath,
259
+ outDir,
260
+ pluginRoot,
261
+ }) {
208
262
  if (configPath) {
209
263
  const config = await loadInspectorConfig(configPath, { cwd: pluginRoot });
210
- const report = await inspectCompatibilityFixtureSet(config, { openclawPath });
264
+ const report = await inspectCompatibilityFixtureSet(config, { authorFacing, openclawPath });
211
265
  await writeCompatibilityReport(report, { cwd: config.rootDir, outDir });
212
266
  return {
213
267
  report,
@@ -215,7 +269,15 @@ async function runCiCompatibilityReport({ allowExecution, capture, configPath, m
215
269
  };
216
270
  }
217
271
 
218
- const { report } = await runPluginCheck({ allowExecution, capture, mockSdk, openclawPath, outDir, pluginRoot });
272
+ const { report } = await runPluginCheck({
273
+ allowExecution,
274
+ authorFacing,
275
+ capture,
276
+ mockSdk,
277
+ openclawPath,
278
+ outDir,
279
+ pluginRoot,
280
+ });
219
281
  return {
220
282
  report,
221
283
  reportDir: path.resolve(pluginRoot ?? process.cwd(), outDir),
@@ -317,6 +379,15 @@ function readAllowExecutionFlag(commandArgs) {
317
379
  return commandArgs.includes("--allow-execute");
318
380
  }
319
381
 
382
+ function readAuthorFacingFlag(commandArgs) {
383
+ if (commandArgs.includes("--include-inspector-gaps")) {
384
+ throw new Error(
385
+ "--include-inspector-gaps has been replaced by --author-facing; default output now includes internal findings.",
386
+ );
387
+ }
388
+ return commandArgs.includes("--author-facing");
389
+ }
390
+
320
391
  function renderCiTextSummary(summary) {
321
392
  return [
322
393
  `Status: ${summary.status.toUpperCase()}`,
@@ -326,6 +397,34 @@ function renderCiTextSummary(summary) {
326
397
  ].join("\n");
327
398
  }
328
399
 
400
+ function renderBatchTextSummary(report, paths) {
401
+ const lines = [
402
+ "Plugin Inspector Batch",
403
+ `Plugins: ${report.summary.pluginCount}`,
404
+ `Plugins with errors: ${report.summary.pluginsWithErrors}`,
405
+ `Plugins with warnings: ${report.summary.pluginsWithWarnings}`,
406
+ `Finding codes: ${report.summary.findingCodeCount}`,
407
+ "",
408
+ "Reports:",
409
+ `- JSON: ${paths.jsonPath}`,
410
+ `- Markdown: ${paths.markdownPath}`,
411
+ ];
412
+ const topFinding = report.findingFrequency[0];
413
+ if (topFinding) {
414
+ lines.push("", `Top finding: ${topFinding.code} (${topFinding.plugins} plugin(s))`);
415
+ }
416
+ return lines.join("\n");
417
+ }
418
+
419
+ function readFirstPositional(args, valueFlags = new Set()) {
420
+ for (let index = 0; index < args.length; index += 1) {
421
+ const arg = args[index];
422
+ if (!arg.startsWith("-")) return arg;
423
+ if (valueFlags.has(arg)) index += 1;
424
+ }
425
+ return undefined;
426
+ }
427
+
329
428
  function initCommandSummary(result) {
330
429
  return {
331
430
  dryRun: result.dryRun,
@@ -353,16 +452,18 @@ function printHelp() {
353
452
 
354
453
  Usage:
355
454
  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]
455
+ plugin-inspector check [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--allow-execute] [--author-facing] [--json]
357
456
  plugin-inspector config [--plugin-root <path>] [--config <path>] [--json]
358
457
  plugin-inspector init [--plugin-root <path>] [--config <path>] [--ci] [--scripts] [--package-manager npm|pnpm|yarn|bun] [--dry-run] [--json] [--force]
359
- plugin-inspector report --config <path> [--out <dir>] [--check] [--json]
360
- 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]
458
+ plugin-inspector report --config <path> [--out <dir>] [--openclaw <path>] [--no-openclaw] [--author-facing] [--check] [--json]
459
+ plugin-inspector batch <folder> [--out <dir>] [--openclaw <path>] [--no-openclaw] [--concurrency <n>] [--keep-plugin-reports] [--author-facing] [--check] [--json]
460
+ plugin-inspector inspect [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--author-facing] [--check] [--json] [--sarif [path]] [--junit [path]] [--allow-execute]
461
+ plugin-inspector ci [--plugin-root <path>] [--config <path>] [--out <dir>] [--openclaw <path>] [--no-openclaw] [--runtime] [--mock-sdk|--real-sdk] [--allow-execute] [--author-facing] [--json] [--no-sarif] [--no-junit]
362
462
  plugin-inspector capture <entrypoint> [--mock-sdk|--real-sdk] [--allow-execute] [--plugin-root <path>] [--output <path>]
363
463
 
364
464
  Default check runs from the current plugin root and writes reports/ unless --out is set.
365
465
  CI writes SARIF and JUnit artifacts by default; check/inspect can write them with --sarif and --junit.
366
466
  Runtime capture is opt-in because it imports plugin code; use --runtime with --allow-execute or PLUGIN_INSPECTOR_EXECUTE_ISOLATED=1.
467
+ Default output includes author-facing and internal findings; pass --author-facing to show only findings with author remediation docs.
367
468
  `);
368
469
  }
@@ -250,10 +250,23 @@ function issueBlock(issue, options) {
250
250
  ` - state: ${issueState(issue)}`,
251
251
  " - evidence:",
252
252
  ...evidenceList(issue.evidence, options).map((item) => ` - ${item}`),
253
+ ...remediationList(issue),
253
254
  ...runtimeCoverageList(issue, options),
254
255
  ].join("\n");
255
256
  }
256
257
 
258
+ function remediationList(issue) {
259
+ const remediation = issue.authorRemediation;
260
+ if (!remediation?.summary) {
261
+ return [];
262
+ }
263
+ return [
264
+ " - author remediation:",
265
+ ` - ${remediation.summary}`,
266
+ ` - docs: ${remediation.docsUrl}`,
267
+ ];
268
+ }
269
+
257
270
  function issueState(issue) {
258
271
  const flags = [
259
272
  issue.status,
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,
@@ -205,7 +217,7 @@ export {
205
217
  validateImportLoopProfile,
206
218
  writeImportLoopProfile,
207
219
  } from "./import-loop-profile.js";
208
- export { classifyIssueFinding, issueId, knownIssueCodes } from "./issues.js";
220
+ export { classifyIssueFinding, issueId, isAuthorFacingFinding, knownIssueCodes } from "./issues.js";
209
221
  export { inspectFixtureSet, inspectPlugin, inspectSourceText } from "./inspector.js";
210
222
  export { openClawTargetPathCandidates, readOpenClawTargetSurface } from "./openclaw-target.js";
211
223
  export {
package/src/inspector.js CHANGED
@@ -35,6 +35,7 @@ export async function inspectCompatibilityFixtureSet(config, options = {}) {
35
35
  config,
36
36
  inspections,
37
37
  failures,
38
+ authorFacing: options.authorFacing,
38
39
  generatedAt: options.generatedAt,
39
40
  executionResults: options.executionResults,
40
41
  targetOpenClaw,
package/src/issues.js CHANGED
@@ -45,6 +45,12 @@ export const knownIssueCodes = new Set([
45
45
  "unrecognized-security-manifest",
46
46
  ]);
47
47
 
48
+ const authorRemediationDocsUrl = (code) => `https://docs.openclaw.ai/clawhub/plugin-validation-fixes#${code}`;
49
+
50
+ const authorRemediation = (summary) => ({ summary });
51
+
52
+ const migrationRemediation = authorRemediation;
53
+
48
54
  export const issueMetadataByCode = {
49
55
  "before-tool-call-probe": {
50
56
  severity: "P1",
@@ -63,6 +69,13 @@ export const issueMetadataByCode = {
63
69
  owner: "core",
64
70
  decision: "core-compat-adapter",
65
71
  title: "channelEnvVars legacy manifest metadata must stay covered",
72
+ authorRemediation: migrationRemediation(
73
+ "Move legacy channel environment variable metadata into the current setup/config metadata while keeping the old field until your supported OpenClaw range no longer needs it.",
74
+ [
75
+ "Mirror each channel environment variable into the current setup or provider configuration metadata.",
76
+ "Keep channelEnvVars only as backwards compatibility for older OpenClaw versions you still support.",
77
+ ],
78
+ ),
66
79
  },
67
80
  "conversation-access-hook": {
68
81
  severity: "P1",
@@ -75,12 +88,27 @@ export const issueMetadataByCode = {
75
88
  owner: "core",
76
89
  decision: "core-compat-adapter",
77
90
  title: "legacy before_agent_start hook compatibility is still used",
91
+ authorRemediation: migrationRemediation(
92
+ "Replace the legacy before_agent_start hook with the current prompt/model hooks.",
93
+ [
94
+ "Move model-selection work to before_model_resolve when possible.",
95
+ "Move prompt mutation work to before_prompt_build.",
96
+ "Keep before_agent_start only if your declared compatibility range still includes OpenClaw versions that require it.",
97
+ ],
98
+ ),
78
99
  },
79
100
  "legacy-root-sdk-import": {
80
101
  severity: "P2",
81
102
  owner: "core",
82
103
  decision: "core-compat-adapter",
83
104
  title: "root plugin SDK barrel is still used by fixtures",
105
+ authorRemediation: migrationRemediation(
106
+ "Prefer focused public plugin SDK subpath imports instead of the legacy root barrel.",
107
+ [
108
+ "Replace imports from openclaw/plugin-sdk with the documented subpath for the API you use.",
109
+ "Keep the root import only while supporting older OpenClaw versions that do not expose the subpath.",
110
+ ],
111
+ ),
84
112
  },
85
113
  "sdk-export-missing": {
86
114
  severity: "P1",
@@ -93,12 +121,26 @@ export const issueMetadataByCode = {
93
121
  owner: "plugin",
94
122
  decision: "plugin-upstream-fix",
95
123
  title: "plugin imports reserved bundled-plugin SDK compatibility subpaths",
124
+ authorRemediation: authorRemediation(
125
+ "Stop importing reserved bundled-plugin SDK compatibility paths.",
126
+ [
127
+ "Replace reserved OpenClaw internal SDK imports with documented public openclaw/plugin-sdk subpaths.",
128
+ "If no public API exists for the behavior, vendor a plugin-local helper or request a public OpenClaw API.",
129
+ ],
130
+ ),
96
131
  },
97
132
  "security-manifest-schema-unavailable": {
98
133
  severity: "P3",
99
134
  owner: "plugin",
100
135
  decision: "plugin-upstream-fix",
101
136
  title: "plugin security manifest references an unavailable schema",
137
+ authorRemediation: authorRemediation(
138
+ "Remove or update the unsupported security manifest schema reference.",
139
+ [
140
+ "Delete the schema URL from openclaw.security.json if it is advisory-only.",
141
+ "Use a documented versioned schema once OpenClaw publishes one.",
142
+ ],
143
+ ),
102
144
  },
103
145
  "missing-compat-record": {
104
146
  severity: "P1",
@@ -117,18 +159,37 @@ export const issueMetadataByCode = {
117
159
  owner: "plugin",
118
160
  decision: "plugin-upstream-fix",
119
161
  title: "manifest display name is missing",
162
+ authorRemediation: authorRemediation(
163
+ "Add a display name to the plugin manifest.",
164
+ ["Set a non-empty name field in openclaw.plugin.json."],
165
+ '{\n "name": "My Plugin"\n}',
166
+ ),
120
167
  },
121
168
  "manifest-unknown-contracts": {
122
169
  severity: "P1",
123
170
  owner: "plugin",
124
171
  decision: "plugin-upstream-fix",
125
172
  title: "manifest declares unsupported contract keys",
173
+ authorRemediation: authorRemediation(
174
+ "Remove unsupported manifest contract keys or move them to a documented OpenClaw contract field.",
175
+ [
176
+ "Compare the contracts object to the OpenClaw manifest fields supported by your target version.",
177
+ "Delete custom contract keys unless OpenClaw has a versioned schema for them.",
178
+ ],
179
+ ),
126
180
  },
127
181
  "manifest-unknown-fields": {
128
182
  severity: "P2",
129
183
  owner: "plugin",
130
184
  decision: "plugin-upstream-fix",
131
185
  title: "manifest uses unsupported top-level fields",
186
+ authorRemediation: authorRemediation(
187
+ "Move unsupported top-level manifest fields into supported package metadata or remove them.",
188
+ [
189
+ "Keep openclaw.plugin.json limited to fields supported by the target OpenClaw manifest schema.",
190
+ "Move package-level metadata into package.json openclaw metadata when that field is supported.",
191
+ ],
192
+ ),
132
193
  },
133
194
  "package-build-artifact-entrypoint": {
134
195
  severity: "P2",
@@ -147,72 +208,159 @@ export const issueMetadataByCode = {
147
208
  owner: "plugin",
148
209
  decision: "plugin-upstream-fix",
149
210
  title: "OpenClaw package entrypoint is missing",
211
+ authorRemediation: authorRemediation(
212
+ "Publish the entrypoint declared in OpenClaw package metadata or update the metadata to point at an existing file.",
213
+ [
214
+ "Check package.json openclaw.extensions and openclaw.runtimeExtensions.",
215
+ "Ensure the referenced file exists in the published artifact, usually under dist/ after build.",
216
+ ],
217
+ ),
150
218
  },
151
219
  "package-install-metadata-incomplete": {
152
220
  severity: "P2",
153
221
  owner: "plugin",
154
222
  decision: "plugin-upstream-fix",
155
223
  title: "OpenClaw package install metadata is incomplete",
224
+ authorRemediation: authorRemediation(
225
+ "Complete the OpenClaw install metadata so ClawHub can identify the install target.",
226
+ [
227
+ "Fill package.json openclaw.install with the supported release target.",
228
+ "Align clawhubSpec, npmSpec, and defaultChoice with the package you publish.",
229
+ ],
230
+ ),
156
231
  },
157
232
  "package-json-missing": {
158
233
  severity: "P2",
159
234
  owner: "plugin",
160
235
  decision: "plugin-upstream-fix",
161
236
  title: "package metadata is missing",
237
+ authorRemediation: authorRemediation(
238
+ "Add a package.json to the plugin package.",
239
+ [
240
+ "Include the package name and version.",
241
+ "Add an openclaw metadata block describing extensions, compatibility, and install details.",
242
+ ],
243
+ ),
162
244
  },
163
245
  "package-manifest-version-drift": {
164
246
  severity: "P2",
165
247
  owner: "plugin",
166
248
  decision: "plugin-upstream-fix",
167
249
  title: "package and manifest versions drift",
250
+ authorRemediation: authorRemediation(
251
+ "Align the plugin version declared in package.json and openclaw.plugin.json.",
252
+ [
253
+ "Use the same version in both files, or remove stale manifest version metadata if package.json is authoritative.",
254
+ "Republish with a new package version after changing published metadata.",
255
+ ],
256
+ ),
168
257
  },
169
258
  "package-min-host-version-drift": {
170
259
  severity: "P2",
171
260
  owner: "plugin",
172
261
  decision: "plugin-upstream-fix",
173
262
  title: "OpenClaw package minimum host version drifts from build target",
263
+ authorRemediation: authorRemediation(
264
+ "Set the package minimum host version to the OpenClaw version range the plugin was built and tested against.",
265
+ [
266
+ "Update package.json openclaw.install.minHostVersion or compatibility metadata.",
267
+ "Keep it semver-compatible with the target OpenClaw build version.",
268
+ ],
269
+ ),
174
270
  },
175
271
  "package-npm-pack-entrypoint-missing": {
176
272
  severity: "P1",
177
273
  owner: "plugin",
178
274
  decision: "plugin-upstream-fix",
179
275
  title: "advertised npm artifact is missing OpenClaw entrypoints",
276
+ authorRemediation: authorRemediation(
277
+ "Include the declared OpenClaw entrypoints in the npm-packed artifact.",
278
+ [
279
+ "Run npm pack locally and inspect the tarball contents.",
280
+ "Update package.json files so dist files and manifests are included.",
281
+ "Build before packing if the entrypoint is generated.",
282
+ ],
283
+ ),
180
284
  },
181
285
  "package-npm-pack-metadata-missing": {
182
286
  severity: "P2",
183
287
  owner: "plugin",
184
288
  decision: "plugin-upstream-fix",
185
289
  title: "advertised npm artifact is missing OpenClaw metadata",
290
+ authorRemediation: authorRemediation(
291
+ "Include OpenClaw metadata files in the npm-packed artifact.",
292
+ [
293
+ "Run npm pack locally and inspect package.json and OpenClaw manifest files.",
294
+ "Update package.json files so required metadata is not excluded.",
295
+ ],
296
+ ),
186
297
  },
187
298
  "package-npm-pack-unavailable": {
188
299
  severity: "P1",
189
300
  owner: "plugin",
190
301
  decision: "plugin-upstream-fix",
191
302
  title: "advertised npm artifact cannot be packed",
303
+ authorRemediation: authorRemediation(
304
+ "Make the package packable before publishing it through ClawHub.",
305
+ [
306
+ "Remove private:true if this package is intended to publish.",
307
+ "Ensure package.json has a valid name and version.",
308
+ "Fix package scripts or files entries that make npm pack fail.",
309
+ ],
310
+ ),
192
311
  },
193
312
  "package-openclaw-entry-missing": {
194
313
  severity: "P2",
195
314
  owner: "plugin",
196
315
  decision: "plugin-upstream-fix",
197
316
  title: "OpenClaw package entrypoint metadata is missing",
317
+ authorRemediation: authorRemediation(
318
+ "Declare the plugin runtime entrypoint in package.json OpenClaw metadata.",
319
+ [
320
+ "Add openclaw.extensions for extension entrypoints.",
321
+ "Add openclaw.runtimeExtensions when the plugin has runtime-side code.",
322
+ ],
323
+ ),
198
324
  },
199
325
  "package-openclaw-metadata-missing": {
200
326
  severity: "P2",
201
327
  owner: "plugin",
202
328
  decision: "plugin-upstream-fix",
203
329
  title: "OpenClaw package metadata is missing",
330
+ authorRemediation: authorRemediation(
331
+ "Add the package.json openclaw metadata block.",
332
+ [
333
+ "Describe extension entrypoints, plugin API compatibility, and install metadata.",
334
+ "Keep package metadata in sync with openclaw.plugin.json when both files are present.",
335
+ ],
336
+ ),
204
337
  },
205
338
  "package-openclaw-unsupported-metadata": {
206
339
  severity: "P2",
207
340
  owner: "plugin",
208
341
  decision: "plugin-upstream-fix",
209
342
  title: "package declares unsupported OpenClaw metadata",
343
+ authorRemediation: authorRemediation(
344
+ "Remove unsupported OpenClaw package metadata fields.",
345
+ [
346
+ "Delete openclaw.bundle and other fields not accepted by the current package schema.",
347
+ "Move bundle-specific data to documented manifest fields when available.",
348
+ ],
349
+ ),
210
350
  },
211
351
  "package-plugin-api-compat-missing": {
212
352
  severity: "P2",
213
353
  owner: "plugin",
214
354
  decision: "plugin-upstream-fix",
215
355
  title: "plugin API compatibility range is missing",
356
+ authorRemediation: authorRemediation(
357
+ "Declare the OpenClaw plugin API range this package supports.",
358
+ [
359
+ "Add package.json `openclaw.compat.pluginApi` with the OpenClaw plugin API range you tested.",
360
+ "If known, include the OpenClaw build/version used to produce the package metadata.",
361
+ ],
362
+ '"openclaw": {\n "compat": {\n "pluginApi": ">=0.1.0"\n }\n}',
363
+ ),
216
364
  },
217
365
  "package-typescript-source-entrypoint": {
218
366
  severity: "P2",
@@ -225,6 +373,13 @@ export const issueMetadataByCode = {
225
373
  owner: "core",
226
374
  decision: "core-compat-adapter",
227
375
  title: "providerAuthEnvVars legacy manifest metadata must stay covered",
376
+ authorRemediation: migrationRemediation(
377
+ "Move legacy provider authentication environment variables into current provider setup metadata.",
378
+ [
379
+ "Mirror providerAuthEnvVars into setup.providers[].envVars or the current provider-choice metadata.",
380
+ "Keep the legacy field only while supporting older OpenClaw versions that still read it.",
381
+ ],
382
+ ),
228
383
  },
229
384
  "registration-capture-gap": {
230
385
  severity: "P2",
@@ -255,6 +410,13 @@ export const issueMetadataByCode = {
255
410
  owner: "plugin",
256
411
  decision: "plugin-upstream-fix",
257
412
  title: "plugin ships an unsupported security manifest",
413
+ authorRemediation: authorRemediation(
414
+ "Remove unsupported security manifest files until OpenClaw documents a versioned security manifest schema.",
415
+ [
416
+ "Delete openclaw.security.json if it is advisory-only and not consumed by OpenClaw.",
417
+ "Reintroduce it only when the schema and ClawHub behavior are documented.",
418
+ ],
419
+ ),
258
420
  },
259
421
  };
260
422
 
@@ -284,6 +446,14 @@ export function buildIssues({ breakages = [], warnings = [], suggestions = [], t
284
446
  evidence: finding.evidence ?? [],
285
447
  compatRecord: finding.compatRecord ?? null,
286
448
  runtimeCoverage: finding.runtimeCoverage ?? null,
449
+ ...(finding.authorRemediation
450
+ ? {
451
+ authorRemediation: {
452
+ summary: finding.authorRemediation.summary,
453
+ docsUrl: authorRemediationDocsUrl(finding.code),
454
+ },
455
+ }
456
+ : {}),
287
457
  }));
288
458
  }
289
459
 
@@ -305,9 +475,18 @@ export function issueMetadata(finding, targetOpenClaw) {
305
475
  decision: "inspector-follow-up",
306
476
  title: finding.message,
307
477
  };
478
+ const authorMetadata = metadata.authorRemediation
479
+ ? {
480
+ authorRemediation: {
481
+ summary: metadata.authorRemediation.summary,
482
+ docsUrl: authorRemediationDocsUrl(finding.code),
483
+ },
484
+ }
485
+ : {};
308
486
  return {
309
487
  ...finding,
310
488
  ...metadata,
489
+ ...authorMetadata,
311
490
  ...classifyIssueFinding(finding, targetOpenClaw, metadata),
312
491
  };
313
492
  }
@@ -333,6 +512,14 @@ export function classifyIssueFinding(finding, targetOpenClaw, metadata = {}) {
333
512
  };
334
513
  }
335
514
 
515
+ export function isInspectorGapFinding(finding, targetOpenClaw) {
516
+ return issueMetadata(finding, targetOpenClaw).issueClass === "inspector-gap";
517
+ }
518
+
519
+ export function isAuthorFacingFinding(finding, targetOpenClaw) {
520
+ return Boolean(issueMetadata(finding, targetOpenClaw).authorRemediation);
521
+ }
522
+
336
523
  export function summarizeIssueClasses(issues) {
337
524
  const summary = {
338
525
  "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, isAuthorFacingFinding, 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,26 @@ export async function buildCompatibilityReport(options = {}) {
141
141
  decisions,
142
142
  });
143
143
 
144
+ const visibleBreakages = filterVisibleFindings(breakages, targetOpenClaw, options);
145
+ const visibleWarnings = filterVisibleFindings(warnings, targetOpenClaw, options);
146
+ const visibleSuggestions = filterVisibleFindings(suggestions, targetOpenClaw, options);
147
+ const visibleDecisions = options.authorFacing === true ? [] : decisions;
144
148
  const runtimeCoverage = applyRuntimeExecutionCoverage({
145
- findings: [...warnings, ...suggestions],
149
+ findings: [...visibleWarnings, ...visibleSuggestions],
146
150
  executionResults: options.executionResults,
147
151
  });
148
152
  const issues = buildIssues({
149
- breakages,
150
- warnings,
151
- suggestions,
153
+ breakages: visibleBreakages,
154
+ warnings: visibleWarnings,
155
+ suggestions: visibleSuggestions,
152
156
  targetOpenClaw,
153
157
  idPrefix: options.issueIdPrefix,
154
158
  });
155
- const contractProbes = buildContractProbes({ warnings, suggestions, fixtures: fixtureReports });
159
+ const contractProbes = buildContractProbes({
160
+ warnings: visibleWarnings,
161
+ suggestions: visibleSuggestions,
162
+ fixtures: fixtureReports,
163
+ });
156
164
  const issueSummary = summarizeIssueClasses(issues);
157
165
  const openIssues = issues.filter((issue) => issue.status !== "runtime-covered");
158
166
  const openIssueSummary = summarizeIssueClasses(openIssues);
@@ -160,14 +168,14 @@ export async function buildCompatibilityReport(options = {}) {
160
168
  return {
161
169
  generatedAt: options.generatedAt ?? "deterministic",
162
170
  targetOpenClaw,
163
- status: breakages.length === 0 ? "pass" : "fail",
171
+ status: visibleBreakages.length === 0 ? "pass" : "fail",
164
172
  summary: {
165
173
  fixtureCount: fixtureReports.length,
166
174
  highPriorityFixtures: fixtureReports.filter((fixture) => fixture.priority === "high").length,
167
- breakageCount: breakages.length,
168
- warningCount: warnings.length,
169
- suggestionCount: suggestions.length,
170
- decisionCount: decisions.length,
175
+ breakageCount: visibleBreakages.length,
176
+ warningCount: visibleWarnings.length,
177
+ suggestionCount: visibleSuggestions.length,
178
+ decisionCount: visibleDecisions.length,
171
179
  logCount: logs.length,
172
180
  issueCount: issues.length,
173
181
  openIssueCount: openIssues.length,
@@ -189,16 +197,23 @@ export async function buildCompatibilityReport(options = {}) {
189
197
  contractProbeCount: contractProbes.length,
190
198
  },
191
199
  fixtures: fixtureReports,
192
- breakages,
193
- warnings,
194
- suggestions,
200
+ breakages: visibleBreakages,
201
+ warnings: visibleWarnings,
202
+ suggestions: visibleSuggestions,
195
203
  issues,
196
204
  contractProbes,
197
205
  logs,
198
- decisions,
206
+ decisions: visibleDecisions,
199
207
  };
200
208
  }
201
209
 
210
+ function filterVisibleFindings(findings, targetOpenClaw, options) {
211
+ if (options.authorFacing !== true) {
212
+ return findings;
213
+ }
214
+ return findings.filter((finding) => isAuthorFacingFinding(finding, targetOpenClaw));
215
+ }
216
+
202
217
  export function classifyCompatRecordCoverage({ targetOpenClaw, findings, suggestions, logs, decisions }) {
203
218
  if (targetOpenClaw.status !== "ok") {
204
219
  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(),