codex-plugin-doctor 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -219,6 +219,8 @@ codex-plugin-doctor doctor runtime-plan . --markdown --output runtime-plan.md
219
219
  codex-plugin-doctor doctor runtime-policy .
220
220
  codex-plugin-doctor doctor runtime-policy . --json --output runtime-policy.json
221
221
  codex-plugin-doctor doctor review-bundle . --output review-bundle --sign-key-env CODEX_PLUGIN_DOCTOR_SIGNING_KEY
222
+ codex-plugin-doctor doctor review-bundle verify review-bundle --target . --sign-key-env CODEX_PLUGIN_DOCTOR_SIGNING_KEY
223
+ codex-plugin-doctor doctor review-bundle diff --before old-review-bundle --after review-bundle
222
224
  codex-plugin-doctor doctor mcp .
223
225
  codex-plugin-doctor doctor mcp . --json --output mcp-healthcheck.json
224
226
  codex-plugin-doctor doctor export --bundle .
@@ -290,7 +292,7 @@ codex-plugin-doctor check . --json --runtime --verbose-runtime
290
292
 
291
293
  `self-test` runs the bundled runtime-complete sample through static validation, runtime MCP probes, and the compatibility scorecard. It is the fastest post-install check after `npm install -g codex-plugin-doctor`.
292
294
 
293
- `doctor` checks the local environment, including package version, platform, Node version, npm global prefix, Codex home, and Codex plugin cache visibility. The text output also includes recommended next commands for self-test, installed plugin discovery, runtime checks, compatibility scoring, and CI setup. `doctor contract` publishes the machine-readable output contract, including public JSON schema surfaces, stable-through-1.0 compatibility metadata, and a frozen rule catalog digest. Add `--json` for automation or `--output output-contract.json` to write the contract to disk. `doctor corpus` runs the bundled validation corpus against healthy runtime, risky security, starter skill, and generic MCP packages, then reports whether each case matched its expected outcome. Add `--json` for automation or `--output validation-corpus.json` to write the corpus report to disk. `doctor npm <package>` runs a preinstall scan by packing the npm package with scripts disabled, extracting the publish tarball, and running validation, security, trust, and recommendation checks against the shipped contents. Use a published Codex plugin package as the target; scanning `codex-plugin-doctor` itself intentionally reports a missing plugin manifest because this CLI package is not a plugin package. Add `--json` for automation or `--output npm-preinstall.json` to write the report to disk. `doctor attest <path>` creates a local attestation with stable package/report digests, validation/security/compatibility/trust summary, and verification metadata. Add `--sign-key-env NAME` to attach a local HMAC-SHA256 signature without printing the secret, or `--json --output attestation.json` to write the artifact to disk. `doctor attest verify <attestation.json> --target <path> --sign-key-env NAME` recomputes the package fingerprint, report digest, and HMAC signature offline; verification intentionally treats `generatedAt`, `targetPath`, `verification`, and `signature.keyHint` as unsigned display metadata. `doctor runtime-plan <path>` creates a non-executing runtime plan that lists MCP server commands, safe probe methods, risk reasons, and a stable approval digest before any local server is started. Add `--markdown --output runtime-plan.md` to preserve a review-ready approval artifact with the execution boundary, checklist, servers, probes, and risk reasons. `doctor runtime-policy <path>` evaluates the same runtime plan and security signals, then recommends `allow`, `review`, `sandbox_recommended`, or `deny` before local MCP execution starts. `doctor review-bundle <path> --output <dir> --sign-key-env NAME` writes a signed review directory with runtime plan, runtime policy, attestation, release evidence, manifest, and Markdown summary files. `check --runtime --require-runtime-approval --runtime-approval-digest <digest>` refuses to run runtime probes unless the current plan digest matches the approved digest. `doctor release-evidence <path> --sign-key-env NAME` creates one redacted release bundle with signed attestation, offline verification, corpus, performance, security, trust, package metadata, git release gates, and runtime approval status. Strict release evidence requires a clean tagged worktree; use `--allow-dirty` or `--allow-untagged` only for local rehearsal. `doctor release-evidence verify <evidence.json> --target <path> --sign-key-env NAME` verifies a shared release evidence artifact offline against an explicit package path; the artifact target path is treated as display metadata, not trusted input. `doctor release-evidence asset <path> --tag <tag> --output <evidence.json> --sign-key-env NAME` writes a signed release evidence file and prints the `gh release upload` command; add `--upload` to run the upload through GitHub CLI with `--clobber`. `doctor inspector <path>` builds a safe MCP Inspector launch command from a packaged `.mcp.json` file without starting the Inspector proxy automatically. Use `--server <name>` when the package contains multiple MCP server entries. `doctor diff --before <path> --after <path>` compares two package roots and reports new findings, resolved findings, trust score delta, and whether risk increased. `doctor recommend <path>` turns validation, security, and compatibility signals into a prioritized action plan with blocker, high, medium, and info actions. Add `--json` for automation or `--output recommendations.json` to write the report to disk. `doctor trust <path>` creates a local trust score from package lifecycle scripts, dependency specs, and MCP security findings. Use it before release when you want supply-chain risks summarized as one score. `doctor perf <path>` profiles the shared package analysis pipeline and reports per-stage durations for validation, config, security, compatibility, trust, recommendations, and total runtime. Add `--max-total-ms <ms>` or repeatable `--max-stage-ms stage=ms` to fail CI when a budget is exceeded. `doctor mcp <path>` exposes the generic MCP static health report under the doctor command family without starting local MCP servers. `doctor export --bundle <path>` creates a redacted operator handoff bundle that includes validation JSON, security scorecard data, compatibility matrix, recommendations, and trust score in one file. `doctor snapshot` creates a redacted diagnostics bundle with environment health, client config readiness, installed plugin metadata, and next commands. Add `--json` for machine-readable output or `--output doctor-snapshot.json` to write the bundle to disk. `doctor clients` reports local Codex, Claude Desktop, Cursor, Cline, and Windsurf config readiness. `doctor --update-check` compares the installed CLI version with the latest npm version and prints the upgrade command when a newer release is available.
295
+ `doctor` checks the local environment, including package version, platform, Node version, npm global prefix, Codex home, and Codex plugin cache visibility. The text output also includes recommended next commands for self-test, installed plugin discovery, runtime checks, compatibility scoring, and CI setup. `doctor contract` publishes the machine-readable output contract, including public JSON schema surfaces, stable-through-1.0 compatibility metadata, and a frozen rule catalog digest. Add `--json` for automation or `--output output-contract.json` to write the contract to disk. `doctor corpus` runs the bundled validation corpus against healthy runtime, risky security, starter skill, and generic MCP packages, then reports whether each case matched its expected outcome. Add `--json` for automation or `--output validation-corpus.json` to write the corpus report to disk. `doctor npm <package>` runs a preinstall scan by packing the npm package with scripts disabled, extracting the publish tarball, and running validation, security, trust, and recommendation checks against the shipped contents. Use a published Codex plugin package as the target; scanning `codex-plugin-doctor` itself intentionally reports a missing plugin manifest because this CLI package is not a plugin package. Add `--json` for automation or `--output npm-preinstall.json` to write the report to disk. `doctor attest <path>` creates a local attestation with stable package/report digests, validation/security/compatibility/trust summary, and verification metadata. Add `--sign-key-env NAME` to attach a local HMAC-SHA256 signature without printing the secret, or `--json --output attestation.json` to write the artifact to disk. `doctor attest verify <attestation.json> --target <path> --sign-key-env NAME` recomputes the package fingerprint, report digest, and HMAC signature offline; verification intentionally treats `generatedAt`, `targetPath`, `verification`, and `signature.keyHint` as unsigned display metadata. `doctor runtime-plan <path>` creates a non-executing runtime plan that lists MCP server commands, safe probe methods, risk reasons, and a stable approval digest before any local server is started. Add `--markdown --output runtime-plan.md` to preserve a review-ready approval artifact with the execution boundary, checklist, servers, probes, and risk reasons. `doctor runtime-policy <path>` evaluates the same runtime plan and security signals, then recommends `allow`, `review`, `sandbox_recommended`, or `deny` before local MCP execution starts. `doctor review-bundle <path> --output <dir> --sign-key-env NAME` writes a signed review directory with runtime plan, runtime policy, attestation, release evidence, manifest, and Markdown summary files. `doctor review-bundle verify <bundle-dir> --target <path> --sign-key-env NAME` verifies the bundle manifest, expected files, runtime artifacts, signed attestation, and signed release evidence offline before a reviewer trusts the handoff. `doctor review-bundle diff --before <dir> --after <dir>` compares two review bundles and flags risk-increasing changes in status, runtime policy, release readiness, signatures, release evidence, and runtime plan digest. `check --runtime --require-runtime-approval --runtime-approval-digest <digest>` refuses to run runtime probes unless the current plan digest matches the approved digest. `doctor release-evidence <path> --sign-key-env NAME` creates one redacted release bundle with signed attestation, offline verification, corpus, performance, security, trust, package metadata, git release gates, and runtime approval status. Strict release evidence requires a clean tagged worktree; use `--allow-dirty` or `--allow-untagged` only for local rehearsal. `doctor release-evidence verify <evidence.json> --target <path> --sign-key-env NAME` verifies a shared release evidence artifact offline against an explicit package path; the artifact target path is treated as display metadata, not trusted input. `doctor release-evidence asset <path> --tag <tag> --output <evidence.json> --sign-key-env NAME` writes a signed release evidence file and prints the `gh release upload` command; add `--upload` to run the upload through GitHub CLI with `--clobber`. `doctor inspector <path>` builds a safe MCP Inspector launch command from a packaged `.mcp.json` file without starting the Inspector proxy automatically. Use `--server <name>` when the package contains multiple MCP server entries. `doctor diff --before <path> --after <path>` compares two package roots and reports new findings, resolved findings, trust score delta, and whether risk increased. `doctor recommend <path>` turns validation, security, and compatibility signals into a prioritized action plan with blocker, high, medium, and info actions. Add `--json` for automation or `--output recommendations.json` to write the report to disk. `doctor trust <path>` creates a local trust score from package lifecycle scripts, dependency specs, and MCP security findings. Use it before release when you want supply-chain risks summarized as one score. `doctor perf <path>` profiles the shared package analysis pipeline and reports per-stage durations for validation, config, security, compatibility, trust, recommendations, and total runtime. Add `--max-total-ms <ms>` or repeatable `--max-stage-ms stage=ms` to fail CI when a budget is exceeded. `doctor mcp <path>` exposes the generic MCP static health report under the doctor command family without starting local MCP servers. `doctor export --bundle <path>` creates a redacted operator handoff bundle that includes validation JSON, security scorecard data, compatibility matrix, recommendations, and trust score in one file. `doctor snapshot` creates a redacted diagnostics bundle with environment health, client config readiness, installed plugin metadata, and next commands. Add `--json` for machine-readable output or `--output doctor-snapshot.json` to write the bundle to disk. `doctor clients` reports local Codex, Claude Desktop, Cursor, Cline, and Windsurf config readiness. `doctor --update-check` compares the installed CLI version with the latest npm version and prints the upgrade command when a newer release is available.
294
296
 
295
297
  `audit --installed` runs a local ecosystem audit against every discovered Codex plugin in the installed plugin cache. Add `--security` to include security scorecards, `--compat` to include the all-client compatibility matrix, and `--json --output local-audit.json` when you want a shareable machine-readable report. Add `--cache` to reuse unchanged plugin results between runs; add `--changed` to only report plugins whose fingerprint changed since the last cached audit. Use `--cache-file path/to/audit-cache.json` when CI or scripted runs need an explicit cache location.
296
298
 
@@ -356,9 +358,9 @@ jobs:
356
358
  runs-on: ubuntu-latest
357
359
  steps:
358
360
  - uses: actions/checkout@v5
359
- - uses: Esquetta/CodexPluginDoctor@v1.5.0
361
+ - uses: Esquetta/CodexPluginDoctor@v1.7.0
360
362
  with:
361
- version: "1.5.0"
363
+ version: "1.7.0"
362
364
  path: .
363
365
  runtime: "true"
364
366
  policy: codex-publish
@@ -89,6 +89,18 @@ const publicSchemaDefinitions = [
89
89
  outputKind: "doctor.review.bundle",
90
90
  required: ["schemaVersion", "kind", "generatedAt", "version", "targetPath", "outputDirectory", "status", "exitCode", "summary", "files"]
91
91
  },
92
+ {
93
+ id: "doctor.review.bundle.verification.json",
94
+ command: "codex-plugin-doctor doctor review-bundle verify <bundle-dir> --target <path> --json",
95
+ outputKind: "doctor.review.bundle.verification",
96
+ required: ["schemaVersion", "kind", "generatedAt", "bundleDirectory", "targetPath", "status", "exitCode", "summary", "checks", "attestation", "releaseEvidence"]
97
+ },
98
+ {
99
+ id: "doctor.review.bundle.diff.json",
100
+ command: "codex-plugin-doctor doctor review-bundle diff --before <dir> --after <dir> --json",
101
+ outputKind: "doctor.review.bundle.diff",
102
+ required: ["schemaVersion", "kind", "generatedAt", "beforeDirectory", "afterDirectory", "status", "exitCode", "summary", "before", "after", "changes"]
103
+ },
92
104
  {
93
105
  id: "doctor.export.bundle.json",
94
106
  command: "codex-plugin-doctor doctor export --bundle <path> --json",
@@ -1,5 +1,5 @@
1
- import { type DoctorAttestation } from "./attestation.js";
2
- import { type DoctorReleaseEvidenceReport } from "./release-evidence.js";
1
+ import { type DoctorAttestationVerificationReport, type DoctorAttestation } from "./attestation.js";
2
+ import { type DoctorReleaseEvidenceReport, type DoctorReleaseEvidenceVerificationReport } from "./release-evidence.js";
3
3
  import { type DoctorRuntimePlan } from "./runtime-plan.js";
4
4
  import { type DoctorRuntimePolicyReport } from "./runtime-policy.js";
5
5
  export interface BuildDoctorReviewBundleOptions {
@@ -42,6 +42,78 @@ export interface DoctorReviewBundle {
42
42
  attestation: DoctorAttestation;
43
43
  releaseEvidence: DoctorReleaseEvidenceReport;
44
44
  }
45
+ export interface DoctorReviewBundleVerificationReport {
46
+ schemaVersion: "1.0.0";
47
+ kind: "doctor.review.bundle.verification";
48
+ generatedAt: string;
49
+ bundleDirectory: string;
50
+ targetPath: string;
51
+ status: "pass" | "fail";
52
+ exitCode: 0 | 1;
53
+ summary: {
54
+ manifest: "pass" | "fail";
55
+ files: "pass" | "fail";
56
+ runtimePlan: "pass" | "fail";
57
+ runtimePolicy: "pass" | "fail";
58
+ attestation: "pass" | "fail";
59
+ releaseEvidence: "pass" | "fail";
60
+ };
61
+ checks: Array<{
62
+ id: string;
63
+ status: "pass" | "fail";
64
+ message: string;
65
+ }>;
66
+ attestation: DoctorAttestationVerificationReport | null;
67
+ releaseEvidence: DoctorReleaseEvidenceVerificationReport | null;
68
+ }
69
+ export interface DoctorReviewBundleDiffReport {
70
+ schemaVersion: "1.0.0";
71
+ kind: "doctor.review.bundle.diff";
72
+ generatedAt: string;
73
+ beforeDirectory: string;
74
+ afterDirectory: string;
75
+ status: "pass" | "warn" | "fail";
76
+ exitCode: 0 | 1;
77
+ summary: {
78
+ changed: boolean;
79
+ statusChanged: boolean;
80
+ runtimePolicyChanged: boolean;
81
+ releaseReadyChanged: boolean;
82
+ riskIncreased: boolean;
83
+ changeCount: number;
84
+ };
85
+ before: DoctorReviewBundleDiffSnapshot | null;
86
+ after: DoctorReviewBundleDiffSnapshot | null;
87
+ changes: DoctorReviewBundleDiffChange[];
88
+ }
89
+ export interface DoctorReviewBundleDiffSnapshot {
90
+ targetPath: string;
91
+ version: string;
92
+ status: DoctorReviewBundleManifest["status"];
93
+ runtimePolicy: DoctorReviewBundleManifest["summary"]["runtimePolicy"];
94
+ releaseReady: boolean;
95
+ attestation: DoctorReviewBundleManifest["summary"]["attestation"];
96
+ releaseEvidence: DoctorReviewBundleManifest["summary"]["releaseEvidence"];
97
+ runtimePlanDigest: string | null;
98
+ releaseEvidenceStatus: DoctorReleaseEvidenceReport["status"] | null;
99
+ releaseEvidenceReady: boolean | null;
100
+ }
101
+ export interface DoctorReviewBundleDiffChange {
102
+ field: string;
103
+ before: string | boolean | null;
104
+ after: string | boolean | null;
105
+ severity: "info" | "warn" | "fail";
106
+ message: string;
107
+ }
45
108
  export declare function buildDoctorReviewBundle(targetPath: string, options: BuildDoctorReviewBundleOptions): Promise<DoctorReviewBundle>;
46
109
  export declare function renderDoctorReviewBundleJson(bundle: DoctorReviewBundle): string;
110
+ export declare function diffDoctorReviewBundles(beforeDirectory: string, afterDirectory: string): Promise<DoctorReviewBundleDiffReport>;
111
+ export declare function verifyDoctorReviewBundle(bundleDirectory: string, options: {
112
+ signingKey: string;
113
+ targetPath: string;
114
+ }): Promise<DoctorReviewBundleVerificationReport>;
115
+ export declare function renderDoctorReviewBundleVerificationJson(report: DoctorReviewBundleVerificationReport): string;
116
+ export declare function renderDoctorReviewBundleVerification(report: DoctorReviewBundleVerificationReport): string;
117
+ export declare function renderDoctorReviewBundleDiffJson(report: DoctorReviewBundleDiffReport): string;
118
+ export declare function renderDoctorReviewBundleDiff(report: DoctorReviewBundleDiffReport): string;
47
119
  export declare function renderDoctorReviewBundle(bundle: DoctorReviewBundle): string;
@@ -1,10 +1,35 @@
1
- import { mkdir, writeFile } from "node:fs/promises";
1
+ import { mkdir, stat, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { buildDoctorAttestation, renderDoctorAttestationJson } from "./attestation.js";
4
- import { buildDoctorReleaseEvidenceReport, renderDoctorReleaseEvidenceJson } from "./release-evidence.js";
3
+ import { buildDoctorAttestation, renderDoctorAttestationJson, verifyDoctorAttestation } from "./attestation.js";
4
+ import { buildDoctorReleaseEvidenceReport, renderDoctorReleaseEvidenceJson, verifyDoctorReleaseEvidence } from "./release-evidence.js";
5
5
  import { buildDoctorRuntimePlan, renderDoctorRuntimePlanJson, renderDoctorRuntimePlanMarkdown } from "./runtime-plan.js";
6
6
  import { buildDoctorRuntimePolicyReport, renderDoctorRuntimePolicy, renderDoctorRuntimePolicyJson } from "./runtime-policy.js";
7
7
  import { packageVersion } from "../version.js";
8
+ import { readJsonFile } from "./read-json-file.js";
9
+ function isPlainObject(value) {
10
+ return typeof value === "object" && value !== null && !Array.isArray(value);
11
+ }
12
+ function isDoctorReviewBundleManifest(value) {
13
+ return isPlainObject(value) &&
14
+ value.schemaVersion === "1.0.0" &&
15
+ value.kind === "doctor.review.bundle" &&
16
+ typeof value.targetPath === "string" &&
17
+ typeof value.outputDirectory === "string" &&
18
+ isPlainObject(value.summary) &&
19
+ isPlainObject(value.files);
20
+ }
21
+ function statusRank(status) {
22
+ return status === "fail" ? 2 : status === "warn" ? 1 : 0;
23
+ }
24
+ function runtimePolicyRank(policy) {
25
+ return policy === "deny"
26
+ ? 3
27
+ : policy === "sandbox_recommended"
28
+ ? 2
29
+ : policy === "review"
30
+ ? 1
31
+ : 0;
32
+ }
8
33
  function relativeBundleFiles() {
9
34
  return {
10
35
  manifest: "manifest.json",
@@ -117,6 +142,352 @@ export async function buildDoctorReviewBundle(targetPath, options) {
117
142
  export function renderDoctorReviewBundleJson(bundle) {
118
143
  return JSON.stringify(bundle.manifest, null, 2);
119
144
  }
145
+ async function readBundleJsonFile(bundleDirectory, relativePath) {
146
+ return readJsonFile(path.join(bundleDirectory, relativePath));
147
+ }
148
+ async function readBundleDiffSnapshot(bundleDirectory) {
149
+ const manifestArtifact = await readBundleJsonFile(bundleDirectory, "manifest.json");
150
+ if (!isDoctorReviewBundleManifest(manifestArtifact)) {
151
+ return null;
152
+ }
153
+ const files = manifestArtifact.files;
154
+ let runtimePlanDigest = null;
155
+ let releaseEvidenceStatus = null;
156
+ let releaseEvidenceReady = null;
157
+ try {
158
+ const runtimePlan = await readBundleJsonFile(bundleDirectory, files.runtimePlanJson);
159
+ runtimePlanDigest = isPlainObject(runtimePlan) && typeof runtimePlan.digest === "string"
160
+ ? runtimePlan.digest
161
+ : null;
162
+ }
163
+ catch {
164
+ runtimePlanDigest = null;
165
+ }
166
+ try {
167
+ const releaseEvidence = await readBundleJsonFile(bundleDirectory, files.releaseEvidenceJson);
168
+ releaseEvidenceStatus = isPlainObject(releaseEvidence) &&
169
+ (releaseEvidence.status === "pass" || releaseEvidence.status === "fail")
170
+ ? releaseEvidence.status
171
+ : null;
172
+ releaseEvidenceReady = isPlainObject(releaseEvidence) && typeof releaseEvidence.releaseReady === "boolean"
173
+ ? releaseEvidence.releaseReady
174
+ : null;
175
+ }
176
+ catch {
177
+ releaseEvidenceStatus = null;
178
+ releaseEvidenceReady = null;
179
+ }
180
+ return {
181
+ targetPath: manifestArtifact.targetPath,
182
+ version: manifestArtifact.version,
183
+ status: manifestArtifact.status,
184
+ runtimePolicy: manifestArtifact.summary.runtimePolicy,
185
+ releaseReady: manifestArtifact.summary.releaseReady,
186
+ attestation: manifestArtifact.summary.attestation,
187
+ releaseEvidence: manifestArtifact.summary.releaseEvidence,
188
+ runtimePlanDigest,
189
+ releaseEvidenceStatus,
190
+ releaseEvidenceReady
191
+ };
192
+ }
193
+ function createDiffChange(field, before, after, severity, message) {
194
+ return before === after
195
+ ? null
196
+ : {
197
+ field,
198
+ before,
199
+ after,
200
+ severity,
201
+ message
202
+ };
203
+ }
204
+ function diffBundleSnapshots(before, after) {
205
+ const changes = [
206
+ createDiffChange("targetPath", before.targetPath, after.targetPath, "warn", "The bundle target path changed."),
207
+ createDiffChange("version", before.version, after.version, "info", "The doctor version changed."),
208
+ createDiffChange("status", before.status, after.status, statusRank(after.status) > statusRank(before.status) ? "fail" : "info", "The review bundle status changed."),
209
+ createDiffChange("runtimePolicy", before.runtimePolicy, after.runtimePolicy, runtimePolicyRank(after.runtimePolicy) > runtimePolicyRank(before.runtimePolicy) ? "warn" : "info", "The runtime policy decision changed."),
210
+ createDiffChange("releaseReady", before.releaseReady, after.releaseReady, before.releaseReady && !after.releaseReady ? "fail" : "info", "The release readiness flag changed."),
211
+ createDiffChange("attestation", before.attestation, after.attestation, statusRank(after.attestation) > statusRank(before.attestation) ? "fail" : "info", "The attestation summary changed."),
212
+ createDiffChange("releaseEvidence", before.releaseEvidence, after.releaseEvidence, statusRank(after.releaseEvidence) > statusRank(before.releaseEvidence) ? "fail" : "info", "The release evidence summary changed."),
213
+ createDiffChange("runtimePlanDigest", before.runtimePlanDigest, after.runtimePlanDigest, "warn", "The runtime plan digest changed."),
214
+ createDiffChange("releaseEvidenceStatus", before.releaseEvidenceStatus, after.releaseEvidenceStatus, after.releaseEvidenceStatus === "fail" ? "fail" : "info", "The embedded release evidence status changed."),
215
+ createDiffChange("releaseEvidenceReady", before.releaseEvidenceReady, after.releaseEvidenceReady, before.releaseEvidenceReady === true && after.releaseEvidenceReady === false ? "fail" : "info", "The embedded release evidence readiness changed.")
216
+ ];
217
+ return changes.filter((change) => change !== null);
218
+ }
219
+ export async function diffDoctorReviewBundles(beforeDirectory, afterDirectory) {
220
+ const resolvedBeforeDirectory = path.resolve(beforeDirectory);
221
+ const resolvedAfterDirectory = path.resolve(afterDirectory);
222
+ const [before, after] = await Promise.all([
223
+ readBundleDiffSnapshot(resolvedBeforeDirectory),
224
+ readBundleDiffSnapshot(resolvedAfterDirectory)
225
+ ]);
226
+ if (!before || !after) {
227
+ const changes = [
228
+ {
229
+ field: before ? "after" : "before",
230
+ before: before ? "valid" : "invalid",
231
+ after: after ? "valid" : "invalid",
232
+ severity: "fail",
233
+ message: "One or more review bundles could not be read as valid bundle directories."
234
+ }
235
+ ];
236
+ return {
237
+ schemaVersion: "1.0.0",
238
+ kind: "doctor.review.bundle.diff",
239
+ generatedAt: new Date().toISOString(),
240
+ beforeDirectory: resolvedBeforeDirectory,
241
+ afterDirectory: resolvedAfterDirectory,
242
+ status: "fail",
243
+ exitCode: 1,
244
+ summary: {
245
+ changed: true,
246
+ statusChanged: false,
247
+ runtimePolicyChanged: false,
248
+ releaseReadyChanged: false,
249
+ riskIncreased: true,
250
+ changeCount: changes.length
251
+ },
252
+ before,
253
+ after,
254
+ changes
255
+ };
256
+ }
257
+ const changes = diffBundleSnapshots(before, after);
258
+ const riskIncreased = changes.some((change) => change.severity === "fail" || change.severity === "warn");
259
+ const status = changes.some((change) => change.severity === "fail")
260
+ ? "fail"
261
+ : riskIncreased
262
+ ? "warn"
263
+ : "pass";
264
+ return {
265
+ schemaVersion: "1.0.0",
266
+ kind: "doctor.review.bundle.diff",
267
+ generatedAt: new Date().toISOString(),
268
+ beforeDirectory: resolvedBeforeDirectory,
269
+ afterDirectory: resolvedAfterDirectory,
270
+ status,
271
+ exitCode: status === "fail" ? 1 : 0,
272
+ summary: {
273
+ changed: changes.length > 0,
274
+ statusChanged: before.status !== after.status,
275
+ runtimePolicyChanged: before.runtimePolicy !== after.runtimePolicy,
276
+ releaseReadyChanged: before.releaseReady !== after.releaseReady,
277
+ riskIncreased,
278
+ changeCount: changes.length
279
+ },
280
+ before,
281
+ after,
282
+ changes
283
+ };
284
+ }
285
+ export async function verifyDoctorReviewBundle(bundleDirectory, options) {
286
+ const resolvedBundleDirectory = path.resolve(bundleDirectory);
287
+ const targetPath = path.resolve(options.targetPath);
288
+ const checks = [];
289
+ let manifest = null;
290
+ let runtimePlanStatus = "fail";
291
+ let runtimePolicyStatus = "fail";
292
+ let attestation = null;
293
+ let releaseEvidence = null;
294
+ try {
295
+ const manifestArtifact = await readBundleJsonFile(resolvedBundleDirectory, "manifest.json");
296
+ if (isDoctorReviewBundleManifest(manifestArtifact)) {
297
+ manifest = manifestArtifact;
298
+ checks.push({
299
+ id: "review_bundle.manifest.valid",
300
+ status: "pass",
301
+ message: "The review bundle manifest has the expected schema and kind."
302
+ });
303
+ }
304
+ else {
305
+ checks.push({
306
+ id: "review_bundle.manifest.valid",
307
+ status: "fail",
308
+ message: "The review bundle manifest is missing or invalid."
309
+ });
310
+ }
311
+ }
312
+ catch {
313
+ checks.push({
314
+ id: "review_bundle.manifest.valid",
315
+ status: "fail",
316
+ message: "The review bundle manifest could not be read."
317
+ });
318
+ }
319
+ const files = manifest?.files ?? relativeBundleFiles();
320
+ for (const [fileKey, relativePath] of Object.entries(files)) {
321
+ try {
322
+ const fileStat = await stat(path.join(resolvedBundleDirectory, relativePath));
323
+ checks.push({
324
+ id: `review_bundle.file.${fileKey}`,
325
+ status: fileStat.isFile() ? "pass" : "fail",
326
+ message: fileStat.isFile()
327
+ ? `${relativePath} is present.`
328
+ : `${relativePath} is not a regular file.`
329
+ });
330
+ }
331
+ catch {
332
+ checks.push({
333
+ id: `review_bundle.file.${fileKey}`,
334
+ status: "fail",
335
+ message: `${relativePath} is missing.`
336
+ });
337
+ }
338
+ }
339
+ try {
340
+ const runtimePlan = await readBundleJsonFile(resolvedBundleDirectory, files.runtimePlanJson);
341
+ runtimePlanStatus = isPlainObject(runtimePlan) && runtimePlan.kind === "doctor.runtime.plan" ? "pass" : "fail";
342
+ checks.push({
343
+ id: "review_bundle.runtime_plan",
344
+ status: runtimePlanStatus,
345
+ message: runtimePlanStatus === "pass"
346
+ ? "The runtime plan artifact has the expected kind."
347
+ : "The runtime plan artifact is invalid."
348
+ });
349
+ }
350
+ catch {
351
+ checks.push({
352
+ id: "review_bundle.runtime_plan",
353
+ status: "fail",
354
+ message: "The runtime plan artifact could not be read."
355
+ });
356
+ }
357
+ try {
358
+ const runtimePolicy = await readBundleJsonFile(resolvedBundleDirectory, files.runtimePolicyJson);
359
+ runtimePolicyStatus = isPlainObject(runtimePolicy) && runtimePolicy.kind === "doctor.runtime.policy" ? "pass" : "fail";
360
+ checks.push({
361
+ id: "review_bundle.runtime_policy",
362
+ status: runtimePolicyStatus,
363
+ message: runtimePolicyStatus === "pass"
364
+ ? "The runtime policy artifact has the expected kind."
365
+ : "The runtime policy artifact is invalid."
366
+ });
367
+ }
368
+ catch {
369
+ checks.push({
370
+ id: "review_bundle.runtime_policy",
371
+ status: "fail",
372
+ message: "The runtime policy artifact could not be read."
373
+ });
374
+ }
375
+ try {
376
+ attestation = await verifyDoctorAttestation(path.join(resolvedBundleDirectory, files.attestationJson), targetPath, { signingKey: options.signingKey });
377
+ checks.push({
378
+ id: "review_bundle.attestation",
379
+ status: attestation.status,
380
+ message: attestation.status === "pass"
381
+ ? "The bundled attestation verifies against the target package."
382
+ : "The bundled attestation does not verify against the target package."
383
+ });
384
+ }
385
+ catch {
386
+ checks.push({
387
+ id: "review_bundle.attestation",
388
+ status: "fail",
389
+ message: "The bundled attestation could not be verified."
390
+ });
391
+ }
392
+ try {
393
+ releaseEvidence = await verifyDoctorReleaseEvidence(path.join(resolvedBundleDirectory, files.releaseEvidenceJson), {
394
+ signingKey: options.signingKey,
395
+ targetPath
396
+ });
397
+ checks.push({
398
+ id: "review_bundle.release_evidence",
399
+ status: releaseEvidence.status,
400
+ message: releaseEvidence.status === "pass"
401
+ ? "The bundled release evidence verifies against the target package."
402
+ : "The bundled release evidence does not verify against the target package."
403
+ });
404
+ }
405
+ catch {
406
+ checks.push({
407
+ id: "review_bundle.release_evidence",
408
+ status: "fail",
409
+ message: "The bundled release evidence could not be verified."
410
+ });
411
+ }
412
+ const failedChecks = checks.filter((check) => check.status === "fail");
413
+ const fileChecks = checks.filter((check) => check.id.startsWith("review_bundle.file."));
414
+ const manifestStatus = checks.find((check) => check.id === "review_bundle.manifest.valid")?.status ?? "fail";
415
+ return {
416
+ schemaVersion: "1.0.0",
417
+ kind: "doctor.review.bundle.verification",
418
+ generatedAt: new Date().toISOString(),
419
+ bundleDirectory: resolvedBundleDirectory,
420
+ targetPath,
421
+ status: failedChecks.length === 0 ? "pass" : "fail",
422
+ exitCode: failedChecks.length === 0 ? 0 : 1,
423
+ summary: {
424
+ manifest: manifestStatus,
425
+ files: fileChecks.every((check) => check.status === "pass") ? "pass" : "fail",
426
+ runtimePlan: runtimePlanStatus,
427
+ runtimePolicy: runtimePolicyStatus,
428
+ attestation: attestation?.status ?? "fail",
429
+ releaseEvidence: releaseEvidence?.status ?? "fail"
430
+ },
431
+ checks,
432
+ attestation,
433
+ releaseEvidence
434
+ };
435
+ }
436
+ export function renderDoctorReviewBundleVerificationJson(report) {
437
+ return JSON.stringify(report, null, 2);
438
+ }
439
+ export function renderDoctorReviewBundleVerification(report) {
440
+ const lines = [
441
+ "Doctor Review Bundle Verification",
442
+ "=================================",
443
+ `Bundle: ${report.bundleDirectory}`,
444
+ `Target: ${report.targetPath}`,
445
+ `Status: ${report.status.toUpperCase()}`,
446
+ `Manifest: ${report.summary.manifest.toUpperCase()}`,
447
+ `Files: ${report.summary.files.toUpperCase()}`,
448
+ `Runtime plan: ${report.summary.runtimePlan.toUpperCase()}`,
449
+ `Runtime policy: ${report.summary.runtimePolicy.toUpperCase()}`,
450
+ `Attestation: ${report.summary.attestation.toUpperCase()}`,
451
+ `Release evidence: ${report.summary.releaseEvidence.toUpperCase()}`,
452
+ "",
453
+ "Checks",
454
+ "------"
455
+ ];
456
+ for (const check of report.checks) {
457
+ lines.push(`${check.status === "pass" ? "PASS" : "FAIL"} ${check.id}`);
458
+ lines.push(` ${check.message}`);
459
+ }
460
+ return lines.join("\n");
461
+ }
462
+ export function renderDoctorReviewBundleDiffJson(report) {
463
+ return JSON.stringify(report, null, 2);
464
+ }
465
+ export function renderDoctorReviewBundleDiff(report) {
466
+ const lines = [
467
+ "Doctor Review Bundle Diff",
468
+ "=========================",
469
+ `Before: ${report.beforeDirectory}`,
470
+ `After: ${report.afterDirectory}`,
471
+ `Status: ${report.status.toUpperCase()}`,
472
+ `Changed: ${report.summary.changed ? "yes" : "no"}`,
473
+ `Risk increased: ${report.summary.riskIncreased ? "yes" : "no"}`,
474
+ `Changes: ${report.summary.changeCount}`,
475
+ "",
476
+ "Changes",
477
+ "-------"
478
+ ];
479
+ if (report.changes.length === 0) {
480
+ lines.push("No changes.");
481
+ return lines.join("\n");
482
+ }
483
+ for (const change of report.changes) {
484
+ lines.push(`${change.severity.toUpperCase()} ${change.field}`);
485
+ lines.push(` Before: ${change.before ?? "unknown"}`);
486
+ lines.push(` After: ${change.after ?? "unknown"}`);
487
+ lines.push(` ${change.message}`);
488
+ }
489
+ return lines.join("\n");
490
+ }
120
491
  export function renderDoctorReviewBundle(bundle) {
121
492
  return [
122
493
  "Doctor Review Bundle",
package/dist/index.d.ts CHANGED
@@ -11,7 +11,7 @@ export { buildDoctorExportBundleFromAnalysis, buildDoctorRecommendationsFromAnal
11
11
  export { buildDoctorPerformanceReport, renderDoctorPerformanceReport, renderDoctorPerformanceReportJson, type BuildDoctorPerformanceReportOptions, type DoctorPerformanceReport, type DoctorPerformanceStage, type DoctorPerformanceStageName } from "./core/performance-report.js";
12
12
  export { buildDoctorRuntimePlan, evaluateRuntimeApproval, renderDoctorRuntimePlan, renderDoctorRuntimePlanMarkdown, renderDoctorRuntimePlanJson, runtimeApprovalPassed, type DoctorRuntimePlan, type RuntimeApprovalReport, type RuntimePlanServer } from "./core/runtime-plan.js";
13
13
  export { buildDoctorRuntimePolicyReport, renderDoctorRuntimePolicy, renderDoctorRuntimePolicyJson, type DoctorRuntimePolicyReport, type RuntimePolicyDecision, type RuntimePolicyRecommendation } from "./core/runtime-policy.js";
14
- export { buildDoctorReviewBundle, renderDoctorReviewBundle, renderDoctorReviewBundleJson, type BuildDoctorReviewBundleOptions, type DoctorReviewBundle, type DoctorReviewBundleManifest } from "./core/review-bundle.js";
14
+ export { buildDoctorReviewBundle, diffDoctorReviewBundles, renderDoctorReviewBundle, renderDoctorReviewBundleDiff, renderDoctorReviewBundleDiffJson, renderDoctorReviewBundleJson, renderDoctorReviewBundleVerification, renderDoctorReviewBundleVerificationJson, verifyDoctorReviewBundle, type BuildDoctorReviewBundleOptions, type DoctorReviewBundle, type DoctorReviewBundleDiffChange, type DoctorReviewBundleDiffReport, type DoctorReviewBundleDiffSnapshot, type DoctorReviewBundleManifest, type DoctorReviewBundleVerificationReport } from "./core/review-bundle.js";
15
15
  export { buildDoctorReleaseEvidenceAssetReport, buildDoctorReleaseEvidenceReport, renderDoctorReleaseEvidenceAsset, renderDoctorReleaseEvidenceAssetJson, renderDoctorReleaseEvidence, renderDoctorReleaseEvidenceJson, renderDoctorReleaseEvidenceVerification, renderDoctorReleaseEvidenceVerificationJson, verifyDoctorReleaseEvidence, type BuildDoctorReleaseEvidenceOptions, type DoctorReleaseEvidenceAssetReport, type DoctorReleaseEvidenceGitMetadata, type DoctorReleaseEvidencePackageMetadata, type DoctorReleaseEvidenceReport, type DoctorReleaseEvidenceVerificationReport } from "./core/release-evidence.js";
16
16
  export { buildDoctorNpmPackageReport, renderDoctorNpmPackageReport, renderDoctorNpmPackageReportJson, type BuildDoctorNpmPackageReportOptions, type DoctorNpmPackageReport } from "./core/npm-package-doctor.js";
17
17
  export { buildDoctorRiskDiffReport, renderDoctorRiskDiffReport, renderDoctorRiskDiffReportJson, type BuildDoctorRiskDiffReportOptions, type DoctorRiskDiffReport, type RiskDiffFinding, type RiskFindingCategory } from "./core/risk-diff.js";
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ export { buildDoctorExportBundleFromAnalysis, buildDoctorRecommendationsFromAnal
11
11
  export { buildDoctorPerformanceReport, renderDoctorPerformanceReport, renderDoctorPerformanceReportJson } from "./core/performance-report.js";
12
12
  export { buildDoctorRuntimePlan, evaluateRuntimeApproval, renderDoctorRuntimePlan, renderDoctorRuntimePlanMarkdown, renderDoctorRuntimePlanJson, runtimeApprovalPassed } from "./core/runtime-plan.js";
13
13
  export { buildDoctorRuntimePolicyReport, renderDoctorRuntimePolicy, renderDoctorRuntimePolicyJson } from "./core/runtime-policy.js";
14
- export { buildDoctorReviewBundle, renderDoctorReviewBundle, renderDoctorReviewBundleJson } from "./core/review-bundle.js";
14
+ export { buildDoctorReviewBundle, diffDoctorReviewBundles, renderDoctorReviewBundle, renderDoctorReviewBundleDiff, renderDoctorReviewBundleDiffJson, renderDoctorReviewBundleJson, renderDoctorReviewBundleVerification, renderDoctorReviewBundleVerificationJson, verifyDoctorReviewBundle } from "./core/review-bundle.js";
15
15
  export { buildDoctorReleaseEvidenceAssetReport, buildDoctorReleaseEvidenceReport, renderDoctorReleaseEvidenceAsset, renderDoctorReleaseEvidenceAssetJson, renderDoctorReleaseEvidence, renderDoctorReleaseEvidenceJson, renderDoctorReleaseEvidenceVerification, renderDoctorReleaseEvidenceVerificationJson, verifyDoctorReleaseEvidence } from "./core/release-evidence.js";
16
16
  export { buildDoctorNpmPackageReport, renderDoctorNpmPackageReport, renderDoctorNpmPackageReportJson } from "./core/npm-package-doctor.js";
17
17
  export { buildDoctorRiskDiffReport, renderDoctorRiskDiffReport, renderDoctorRiskDiffReportJson } from "./core/risk-diff.js";
package/dist/run-cli.js CHANGED
@@ -22,7 +22,7 @@ import { buildDoctorValidationCorpusReport, renderDoctorValidationCorpusJson, re
22
22
  import { buildDoctorPerformanceReport, renderDoctorPerformanceReport, renderDoctorPerformanceReportJson } from "./core/performance-report.js";
23
23
  import { buildDoctorRuntimePlan, evaluateRuntimeApproval, renderDoctorRuntimePlan, renderDoctorRuntimePlanMarkdown, renderDoctorRuntimePlanJson, runtimeApprovalPassed } from "./core/runtime-plan.js";
24
24
  import { buildDoctorRuntimePolicyReport, renderDoctorRuntimePolicy, renderDoctorRuntimePolicyJson } from "./core/runtime-policy.js";
25
- import { buildDoctorReviewBundle, renderDoctorReviewBundle, renderDoctorReviewBundleJson } from "./core/review-bundle.js";
25
+ import { buildDoctorReviewBundle, diffDoctorReviewBundles, renderDoctorReviewBundle, renderDoctorReviewBundleDiff, renderDoctorReviewBundleDiffJson, renderDoctorReviewBundleJson, renderDoctorReviewBundleVerification, renderDoctorReviewBundleVerificationJson, verifyDoctorReviewBundle } from "./core/review-bundle.js";
26
26
  import { buildDoctorReleaseEvidenceAssetReport, buildDoctorReleaseEvidenceReport, renderDoctorReleaseEvidenceAsset, renderDoctorReleaseEvidenceAssetJson, renderDoctorReleaseEvidence, renderDoctorReleaseEvidenceJson, renderDoctorReleaseEvidenceVerification, renderDoctorReleaseEvidenceVerificationJson, verifyDoctorReleaseEvidence } from "./core/release-evidence.js";
27
27
  import { buildDoctorNpmPackageReport, renderDoctorNpmPackageReport, renderDoctorNpmPackageReportJson } from "./core/npm-package-doctor.js";
28
28
  import { buildDoctorRiskDiffReport, renderDoctorRiskDiffReport, renderDoctorRiskDiffReportJson } from "./core/risk-diff.js";
@@ -73,7 +73,7 @@ const defaultIo = {
73
73
  }
74
74
  };
75
75
  function printUsage(io) {
76
- io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--policy codex-publish|mcp-strict|security] [--compat] [--json|--markdown|--badge-json|--badge-markdown] [--output <path>] [--history <path>] [--runtime] [--require-runtime-approval --runtime-approval-digest <digest>] [--verbose-runtime] [--explain] [--no-animations] [--ascii]\n codex-plugin-doctor audit --installed [filter] [--policy codex-publish|mcp-strict|security] [--security] [--compat] [--json] [--output <path>] [--cache] [--changed]\n codex-plugin-doctor mcp <path> [--json] [--output <path>]\n codex-plugin-doctor security <path> [--policy security] [--json|--scorecard]\n codex-plugin-doctor compat <path> [--all|--client <client>] [--json] [--scorecard] [--output <path>] [--install-preview|--apply --backup]\n codex-plugin-doctor fix <path> (--dry-run|--interactive --backup|--apply --backup)\n codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]\n codex-plugin-doctor doctor [npm <package>|contract|corpus|runtime-plan <path> [--json|--markdown] [--output <path>]|runtime-policy <path> [--json] [--output <path>]|review-bundle <path> --output <dir> --sign-key-env NAME [--json] [--allow-dirty] [--allow-untagged]|attest <path> [--sign-key-env NAME]|attest verify <attestation.json> --target <path> --sign-key-env NAME|release-evidence <path> --sign-key-env NAME [--allow-dirty] [--allow-untagged] [--require-runtime-approval --runtime-approval-digest <digest>]|release-evidence verify <evidence.json> --target <path> --sign-key-env NAME|release-evidence asset <path> --tag <tag> --output <evidence.json> --sign-key-env NAME [--upload]|mcp <path>|inspector <path>|diff --before <path> --after <path>|recommend <path>|trust <path>|perf <path> [--max-total-ms <ms>] [--max-stage-ms stage=ms]|export --bundle <path>|snapshot|clients|--json|--update-check]\n codex-plugin-doctor init [path] [--template skill-only|mcp-stdio|mcp-http|full-runtime]\n codex-plugin-doctor init-ci [path]\n codex-plugin-doctor self-test\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version\n\nFirst run:\n codex-plugin-doctor doctor\n codex-plugin-doctor self-test\n codex-plugin-doctor init my-plugin\n codex-plugin-doctor check . --runtime --explain");
76
+ io.writeStderr("Usage: codex-plugin-doctor check <path|--installed> [filter] [--policy codex-publish|mcp-strict|security] [--compat] [--json|--markdown|--badge-json|--badge-markdown] [--output <path>] [--history <path>] [--runtime] [--require-runtime-approval --runtime-approval-digest <digest>] [--verbose-runtime] [--explain] [--no-animations] [--ascii]\n codex-plugin-doctor audit --installed [filter] [--policy codex-publish|mcp-strict|security] [--security] [--compat] [--json] [--output <path>] [--cache] [--changed]\n codex-plugin-doctor mcp <path> [--json] [--output <path>]\n codex-plugin-doctor security <path> [--policy security] [--json|--scorecard]\n codex-plugin-doctor compat <path> [--all|--client <client>] [--json] [--scorecard] [--output <path>] [--install-preview|--apply --backup]\n codex-plugin-doctor fix <path> (--dry-run|--interactive --backup|--apply --backup)\n codex-plugin-doctor history <history.jsonl> [--json] [--fail-on-regression]\n codex-plugin-doctor doctor [npm <package>|contract|corpus|runtime-plan <path> [--json|--markdown] [--output <path>]|runtime-policy <path> [--json] [--output <path>]|review-bundle <path> --output <dir> --sign-key-env NAME [--json] [--allow-dirty] [--allow-untagged]|review-bundle verify <bundle-dir> --target <path> --sign-key-env NAME [--json]|review-bundle diff --before <dir> --after <dir> [--json]|attest <path> [--sign-key-env NAME]|attest verify <attestation.json> --target <path> --sign-key-env NAME|release-evidence <path> --sign-key-env NAME [--allow-dirty] [--allow-untagged] [--require-runtime-approval --runtime-approval-digest <digest>]|release-evidence verify <evidence.json> --target <path> --sign-key-env NAME|release-evidence asset <path> --tag <tag> --output <evidence.json> --sign-key-env NAME [--upload]|mcp <path>|inspector <path>|diff --before <path> --after <path>|recommend <path>|trust <path>|perf <path> [--max-total-ms <ms>] [--max-stage-ms stage=ms]|export --bundle <path>|snapshot|clients|--json|--update-check]\n codex-plugin-doctor init [path] [--template skill-only|mcp-stdio|mcp-http|full-runtime]\n codex-plugin-doctor init-ci [path]\n codex-plugin-doctor self-test\n codex-plugin-doctor list --installed\n codex-plugin-doctor explain <finding-id>\n codex-plugin-doctor --version\n\nFirst run:\n codex-plugin-doctor doctor\n codex-plugin-doctor self-test\n codex-plugin-doctor init my-plugin\n codex-plugin-doctor check . --runtime --explain");
77
77
  }
78
78
  const performanceStageNames = new Set([
79
79
  "validation",
@@ -414,6 +414,79 @@ export async function runCli(args, io = defaultIo, options = {}) {
414
414
  return report.exitCode;
415
415
  }
416
416
  if (maybePath === "review-bundle") {
417
+ if (remainingArgs[0] === "diff") {
418
+ const diffFlags = remainingArgs.slice(1);
419
+ const jsonOutput = diffFlags.includes("--json");
420
+ const beforeIndex = diffFlags.indexOf("--before");
421
+ const afterIndex = diffFlags.indexOf("--after");
422
+ const beforeDirectory = beforeIndex === -1 ? null : diffFlags[beforeIndex + 1];
423
+ const afterDirectory = afterIndex === -1 ? null : diffFlags[afterIndex + 1];
424
+ if (beforeIndex === -1) {
425
+ io.writeStderr("Missing before bundle directory. Use --before <dir>.");
426
+ return 2;
427
+ }
428
+ if (!beforeDirectory || beforeDirectory.startsWith("--")) {
429
+ io.writeStderr("Missing directory after --before.");
430
+ return 2;
431
+ }
432
+ if (afterIndex === -1) {
433
+ io.writeStderr("Missing after bundle directory. Use --after <dir>.");
434
+ return 2;
435
+ }
436
+ if (!afterDirectory || afterDirectory.startsWith("--")) {
437
+ io.writeStderr("Missing directory after --after.");
438
+ return 2;
439
+ }
440
+ const report = await diffDoctorReviewBundles(beforeDirectory, afterDirectory);
441
+ io.writeStdout(jsonOutput
442
+ ? renderDoctorReviewBundleDiffJson(report)
443
+ : renderDoctorReviewBundleDiff(report));
444
+ return report.exitCode;
445
+ }
446
+ if (remainingArgs[0] === "verify") {
447
+ const bundleDirectory = remainingArgs[1] && !remainingArgs[1].startsWith("--")
448
+ ? remainingArgs[1]
449
+ : null;
450
+ const verifyFlags = bundleDirectory ? remainingArgs.slice(2) : remainingArgs.slice(1);
451
+ const jsonOutput = verifyFlags.includes("--json");
452
+ const targetIndex = verifyFlags.indexOf("--target");
453
+ const targetPath = targetIndex === -1 ? null : verifyFlags[targetIndex + 1];
454
+ const signKeyEnvIndex = verifyFlags.indexOf("--sign-key-env");
455
+ const signKeyEnv = signKeyEnvIndex === -1 ? null : verifyFlags[signKeyEnvIndex + 1];
456
+ if (!bundleDirectory) {
457
+ io.writeStderr("Missing review bundle directory.");
458
+ return 2;
459
+ }
460
+ if (targetIndex === -1) {
461
+ io.writeStderr("Missing target path. Use --target <path>.");
462
+ return 2;
463
+ }
464
+ if (!targetPath || targetPath.startsWith("--")) {
465
+ io.writeStderr("Missing path after --target.");
466
+ return 2;
467
+ }
468
+ if (signKeyEnvIndex === -1) {
469
+ io.writeStderr("Missing signing key. Use --sign-key-env <name>.");
470
+ return 2;
471
+ }
472
+ if (!signKeyEnv || signKeyEnv.startsWith("--")) {
473
+ io.writeStderr("Missing environment variable name after --sign-key-env.");
474
+ return 2;
475
+ }
476
+ const signingKey = terminalContext.env[signKeyEnv];
477
+ if (!signingKey) {
478
+ io.writeStderr(`Signing key environment variable is not set: ${signKeyEnv}`);
479
+ return 2;
480
+ }
481
+ const report = await verifyDoctorReviewBundle(bundleDirectory, {
482
+ signingKey,
483
+ targetPath
484
+ });
485
+ io.writeStdout(jsonOutput
486
+ ? renderDoctorReviewBundleVerificationJson(report)
487
+ : renderDoctorReviewBundleVerification(report));
488
+ return report.exitCode;
489
+ }
417
490
  const targetPath = remainingArgs[0] && !remainingArgs[0].startsWith("--")
418
491
  ? remainingArgs[0]
419
492
  : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-plugin-doctor",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "CLI-first validator for Codex plugins, skills, and MCP package surfaces with runtime MCP protocol validation.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",