@openclaw/plugin-inspector 0.3.12 → 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,14 @@
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
+
5
13
  ## 0.3.12 - 2026-06-09
6
14
 
7
15
  ### Changed
package/README.md CHANGED
@@ -187,7 +187,7 @@ Common options:
187
187
  | `--mock-sdk` / `--sdk mock` | Use generated SDK and external-package mocks for runtime capture. |
188
188
  | `--real-sdk` / `--sdk real` | Use installed real SDK dependencies instead of mocks. |
189
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. |
190
+ | `--author-facing` | Limit `check`, `ci`, and `batch` reports to findings with `authorRemediation` guidance. |
191
191
  | `--json` | Print machine-readable JSON to stdout. |
192
192
  | `--sarif [path]` | Write SARIF from `check` or `inspect`; `ci` enables this by default. |
193
193
  | `--junit [path]` | Write JUnit XML from `check` or `inspect`; `ci` enables this by default. |
@@ -307,9 +307,10 @@ Important report sections:
307
307
  | `logs` | Informational inventory and coverage rows. |
308
308
  | `decisions` | Maintainer-facing follow-up or compatibility-policy decisions. |
309
309
 
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
+ 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`.
313
314
 
314
315
  ## CI Policy And Shared Reporting Primitives
315
316
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/plugin-inspector",
3
- "version": "0.3.12",
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,8 +43,8 @@ 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
- includeInspectorGaps: options.includeInspectorGaps,
48
48
  openclawPath: options.openclawPath,
49
49
  executionResults: options.executionResults,
50
50
  targetOpenClaw: options.targetOpenClaw,
@@ -59,8 +59,8 @@ export async function inspectFixtureSetConfig(options = {}) {
59
59
  export async function inspectCompatibilityFixtureSetConfig(options = {}) {
60
60
  const config = await loadFixtureSetConfig(options);
61
61
  return inspectCompatibilityFixtureSet(config, {
62
+ authorFacing: options.authorFacing,
62
63
  generatedAt: options.generatedAt,
63
- includeInspectorGaps: options.includeInspectorGaps,
64
64
  openclawPath: options.openclawPath,
65
65
  executionResults: options.executionResults,
66
66
  targetOpenClaw: options.targetOpenClaw,
@@ -114,8 +114,8 @@ export async function buildFixtureSetColdImportReadiness(options = {}) {
114
114
  const report =
115
115
  options.report ??
116
116
  (await inspectCompatibilityFixtureSet(config, {
117
+ authorFacing: options.authorFacing,
117
118
  generatedAt: options.generatedAt,
118
- includeInspectorGaps: options.includeInspectorGaps,
119
119
  openclawPath: options.openclawPath,
120
120
  executionResults: options.executionResults,
121
121
  targetOpenClaw: options.targetOpenClaw,
@@ -147,8 +147,8 @@ export async function buildFixtureSetWorkspacePlan(options = {}) {
147
147
  const report =
148
148
  options.report ??
149
149
  (await inspectCompatibilityFixtureSet(config, {
150
+ authorFacing: options.authorFacing,
150
151
  generatedAt: options.generatedAt,
151
- includeInspectorGaps: options.includeInspectorGaps,
152
152
  openclawPath: options.openclawPath,
153
153
  executionResults: options.executionResults,
154
154
  targetOpenClaw: options.targetOpenClaw,
package/src/batch.js CHANGED
@@ -108,9 +108,9 @@ async function inspectBatchPlugin(pluginRoot, options) {
108
108
  try {
109
109
  const { report } = await runPluginCheck({
110
110
  allowExecution: options.allowExecution,
111
+ authorFacing: options.authorFacing,
111
112
  capture: options.capture,
112
113
  configPath: options.configPath,
113
- includeInspectorGaps: options.includeInspectorGaps,
114
114
  mockSdk: options.mockSdk,
115
115
  openclawPath: options.openclawPath,
116
116
  outDir: options.outDir,
@@ -148,17 +148,20 @@ async function inspectBatchPlugin(pluginRoot, options) {
148
148
  }
149
149
 
150
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
- ),
151
+ const issueFindings = (report.issues ?? []).map((finding) =>
152
+ normalizeFinding(
153
+ finding,
154
+ finding.status === "blocking" || finding.severity === "P0" ? "error" : "warning",
158
155
  ),
156
+ );
157
+ const issueKeys = new Set(issueFindings.map(findingKey));
158
+ const rawFindings = [
159
+ ...(report.breakages ?? []).map((finding) => normalizeFinding(finding, "error")),
159
160
  ...(report.warnings ?? []).map((finding) => normalizeFinding(finding, "warning")),
160
161
  ...(report.suggestions ?? []).map((finding) => normalizeFinding(finding, "warning")),
161
- ];
162
+ ].filter((finding) => !issueKeys.has(findingKey(finding)));
163
+
164
+ return [...issueFindings, ...rawFindings];
162
165
  }
163
166
 
164
167
  function normalizeFinding(finding, kind) {
@@ -169,9 +172,18 @@ function normalizeFinding(finding, kind) {
169
172
  issueClass: finding.issueClass,
170
173
  message: finding.message ?? finding.title ?? "See plugin report.",
171
174
  evidence: finding.evidence,
175
+ ...(finding.authorRemediation ? { authorRemediation: finding.authorRemediation } : {}),
172
176
  };
173
177
  }
174
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
+
175
187
  function findingFrequencyRows(entries) {
176
188
  const byCode = new Map();
177
189
  for (const entry of entries) {
package/src/cli.js CHANGED
@@ -64,7 +64,7 @@ async function runBatch(commandArgs) {
64
64
  const json = commandArgs.includes("--json");
65
65
  const check = commandArgs.includes("--check");
66
66
  const keepPluginReports = commandArgs.includes("--keep-plugin-reports");
67
- const includeInspectorGaps = commandArgs.includes("--include-inspector-gaps");
67
+ const authorFacing = readAuthorFacingFlag(commandArgs);
68
68
  if (!inputDir) {
69
69
  throw new Error("batch requires a folder of plugin roots");
70
70
  }
@@ -73,7 +73,7 @@ async function runBatch(commandArgs) {
73
73
  outDir,
74
74
  openclawPath,
75
75
  concurrency,
76
- includeInspectorGaps,
76
+ authorFacing,
77
77
  keepPluginReports,
78
78
  });
79
79
 
@@ -110,12 +110,12 @@ async function runCheck(commandArgs) {
110
110
  const mockSdk = readMockSdkFlag(commandArgs);
111
111
  const allowExecution = readAllowExecutionFlag(commandArgs);
112
112
  const ciOutputs = readCiOutputFlags(commandArgs);
113
- const includeInspectorGaps = commandArgs.includes("--include-inspector-gaps");
113
+ const authorFacing = readAuthorFacingFlag(commandArgs);
114
114
  const { report, paths } = await runPluginCheck({
115
115
  allowExecution,
116
+ authorFacing,
116
117
  capture,
117
118
  configPath,
118
- includeInspectorGaps,
119
119
  mockSdk,
120
120
  openclawPath,
121
121
  outDir,
@@ -168,12 +168,18 @@ async function runInit(commandArgs) {
168
168
  async function runReport(command, commandArgs) {
169
169
  const configPath = readFlag(commandArgs, "--config");
170
170
  const outDir = readFlag(commandArgs, "--out") ?? "reports";
171
+ const openclawPath = commandArgs.includes("--no-openclaw") ? false : readFlag(commandArgs, "--openclaw");
171
172
  const check = commandArgs.includes("--check") || command === "ci";
172
173
  const json = commandArgs.includes("--json");
173
174
  const ciOutputs = readCiOutputFlags(commandArgs);
175
+ const authorFacing = readAuthorFacingFlag(commandArgs);
174
176
  const config = await loadInspectorConfig(configPath);
175
- const report = await inspectFixtureSet(config);
176
- 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 });
177
183
  await writeCiOutputArtifacts(report, {
178
184
  ...ciOutputs,
179
185
  cwd: path.dirname(paths.jsonPath),
@@ -201,12 +207,12 @@ async function runCi(commandArgs) {
201
207
  const mockSdk = readMockSdkFlag(commandArgs);
202
208
  const allowExecution = readAllowExecutionFlag(commandArgs);
203
209
  const ciOutputs = readCiOutputFlags(commandArgs, { defaultEnabled: true });
204
- const includeInspectorGaps = commandArgs.includes("--include-inspector-gaps");
210
+ const authorFacing = readAuthorFacingFlag(commandArgs);
205
211
  const { report, reportDir } = await runCiCompatibilityReport({
206
212
  allowExecution,
213
+ authorFacing,
207
214
  capture,
208
215
  configPath,
209
- includeInspectorGaps,
210
216
  mockSdk,
211
217
  openclawPath,
212
218
  outDir,
@@ -245,9 +251,9 @@ async function runCi(commandArgs) {
245
251
 
246
252
  async function runCiCompatibilityReport({
247
253
  allowExecution,
254
+ authorFacing,
248
255
  capture,
249
256
  configPath,
250
- includeInspectorGaps,
251
257
  mockSdk,
252
258
  openclawPath,
253
259
  outDir,
@@ -255,7 +261,7 @@ async function runCiCompatibilityReport({
255
261
  }) {
256
262
  if (configPath) {
257
263
  const config = await loadInspectorConfig(configPath, { cwd: pluginRoot });
258
- const report = await inspectCompatibilityFixtureSet(config, { includeInspectorGaps, openclawPath });
264
+ const report = await inspectCompatibilityFixtureSet(config, { authorFacing, openclawPath });
259
265
  await writeCompatibilityReport(report, { cwd: config.rootDir, outDir });
260
266
  return {
261
267
  report,
@@ -265,8 +271,8 @@ async function runCiCompatibilityReport({
265
271
 
266
272
  const { report } = await runPluginCheck({
267
273
  allowExecution,
274
+ authorFacing,
268
275
  capture,
269
- includeInspectorGaps,
270
276
  mockSdk,
271
277
  openclawPath,
272
278
  outDir,
@@ -373,6 +379,15 @@ function readAllowExecutionFlag(commandArgs) {
373
379
  return commandArgs.includes("--allow-execute");
374
380
  }
375
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
+
376
391
  function renderCiTextSummary(summary) {
377
392
  return [
378
393
  `Status: ${summary.status.toUpperCase()}`,
@@ -437,18 +452,18 @@ function printHelp() {
437
452
 
438
453
  Usage:
439
454
  plugin-inspector
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]
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]
441
456
  plugin-inspector config [--plugin-root <path>] [--config <path>] [--json]
442
457
  plugin-inspector init [--plugin-root <path>] [--config <path>] [--ci] [--scripts] [--package-manager npm|pnpm|yarn|bun] [--dry-run] [--json] [--force]
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]
445
- plugin-inspector inspect [--plugin-root <path>] [--config <path>] [--out <dir>] [--check] [--json] [--sarif [path]] [--junit [path]] [--allow-execute]
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]
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]
447
462
  plugin-inspector capture <entrypoint> [--mock-sdk|--real-sdk] [--allow-execute] [--plugin-root <path>] [--output <path>]
448
463
 
449
464
  Default check runs from the current plugin root and writes reports/ unless --out is set.
450
465
  CI writes SARIF and JUnit artifacts by default; check/inspect can write them with --sarif and --junit.
451
466
  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.
467
+ Default output includes author-facing and internal findings; pass --author-facing to show only findings with author remediation docs.
453
468
  `);
454
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
@@ -217,7 +217,7 @@ export {
217
217
  validateImportLoopProfile,
218
218
  writeImportLoopProfile,
219
219
  } from "./import-loop-profile.js";
220
- export { classifyIssueFinding, issueId, knownIssueCodes } from "./issues.js";
220
+ export { classifyIssueFinding, issueId, isAuthorFacingFinding, knownIssueCodes } from "./issues.js";
221
221
  export { inspectFixtureSet, inspectPlugin, inspectSourceText } from "./inspector.js";
222
222
  export { openClawTargetPathCandidates, readOpenClawTargetSurface } from "./openclaw-target.js";
223
223
  export {
package/src/inspector.js CHANGED
@@ -35,9 +35,9 @@ 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
- includeInspectorGaps: options.includeInspectorGaps,
41
41
  targetOpenClaw,
42
42
  buildFixtureReport: ({ fixture, inspection }) =>
43
43
  buildCompatibilityFixtureReport({
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
  }
@@ -337,6 +516,10 @@ export function isInspectorGapFinding(finding, targetOpenClaw) {
337
516
  return issueMetadata(finding, targetOpenClaw).issueClass === "inspector-gap";
338
517
  }
339
518
 
519
+ export function isAuthorFacingFinding(finding, targetOpenClaw) {
520
+ return Boolean(issueMetadata(finding, targetOpenClaw).authorRemediation);
521
+ }
522
+
340
523
  export function summarizeIssueClasses(issues) {
341
524
  const summary = {
342
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, isInspectorGapFinding, 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,14 +141,16 @@ export async function buildCompatibilityReport(options = {}) {
141
141
  decisions,
142
142
  });
143
143
 
144
+ const visibleBreakages = filterVisibleFindings(breakages, targetOpenClaw, options);
144
145
  const visibleWarnings = filterVisibleFindings(warnings, targetOpenClaw, options);
145
146
  const visibleSuggestions = filterVisibleFindings(suggestions, targetOpenClaw, options);
147
+ const visibleDecisions = options.authorFacing === true ? [] : decisions;
146
148
  const runtimeCoverage = applyRuntimeExecutionCoverage({
147
149
  findings: [...visibleWarnings, ...visibleSuggestions],
148
150
  executionResults: options.executionResults,
149
151
  });
150
152
  const issues = buildIssues({
151
- breakages,
153
+ breakages: visibleBreakages,
152
154
  warnings: visibleWarnings,
153
155
  suggestions: visibleSuggestions,
154
156
  targetOpenClaw,
@@ -166,14 +168,14 @@ export async function buildCompatibilityReport(options = {}) {
166
168
  return {
167
169
  generatedAt: options.generatedAt ?? "deterministic",
168
170
  targetOpenClaw,
169
- status: breakages.length === 0 ? "pass" : "fail",
171
+ status: visibleBreakages.length === 0 ? "pass" : "fail",
170
172
  summary: {
171
173
  fixtureCount: fixtureReports.length,
172
174
  highPriorityFixtures: fixtureReports.filter((fixture) => fixture.priority === "high").length,
173
- breakageCount: breakages.length,
175
+ breakageCount: visibleBreakages.length,
174
176
  warningCount: visibleWarnings.length,
175
177
  suggestionCount: visibleSuggestions.length,
176
- decisionCount: decisions.length,
178
+ decisionCount: visibleDecisions.length,
177
179
  logCount: logs.length,
178
180
  issueCount: issues.length,
179
181
  openIssueCount: openIssues.length,
@@ -195,21 +197,21 @@ export async function buildCompatibilityReport(options = {}) {
195
197
  contractProbeCount: contractProbes.length,
196
198
  },
197
199
  fixtures: fixtureReports,
198
- breakages,
200
+ breakages: visibleBreakages,
199
201
  warnings: visibleWarnings,
200
202
  suggestions: visibleSuggestions,
201
203
  issues,
202
204
  contractProbes,
203
205
  logs,
204
- decisions,
206
+ decisions: visibleDecisions,
205
207
  };
206
208
  }
207
209
 
208
210
  function filterVisibleFindings(findings, targetOpenClaw, options) {
209
- if (options.includeInspectorGaps === true) {
211
+ if (options.authorFacing !== true) {
210
212
  return findings;
211
213
  }
212
- return findings.filter((finding) => !isInspectorGapFinding(finding, targetOpenClaw));
214
+ return findings.filter((finding) => isAuthorFacingFinding(finding, targetOpenClaw));
213
215
  }
214
216
 
215
217
  export function classifyCompatRecordCoverage({ targetOpenClaw, findings, suggestions, logs, decisions }) {