Skip to content

Commit ec65738

Browse files
fix(build,validate,doctor): run the Claude host validator and --plugin-dir load check; doctor reads enabled (AB7327) (#475, #476) (#504)
* fix(validate,build,doctor): run the Claude host validator and --plugin-dir load check from build and validate; doctor validates installed copies and reads enabled (#475, #476) * test(packed-native): accept other hosts' informational reports in the packed Claude validation document * docs(site): add the missing lineage-detail table header the generated reference plugin renders (#490 follow-up) * chore(changeset): reference #504
1 parent 853c31a commit ec65738

23 files changed

Lines changed: 1324 additions & 165 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'agent-bundle': patch
3+
---
4+
5+
Run the Claude Code host validator where it was missing and turn it into a load verdict. `agent-bundle build` now runs the same Claude Code checks as `validate --artifact` over every built `claude` and `plugin` target (`--host-validation` on by default, `--no-host-validation` to skip, `--strict` to promote host warnings to errors), and both commands follow the two `claude plugin validate --strict` runs with a `claude --plugin-dir <dir> plugin list --json` load check: a row with `errors` is reported as `AB7325` (a warning when the only errors are uninstalled declared dependencies), no row as `AB7311`, an unreadable listing as `AB6022`; the report carries `load.status` (`loaded`, `refused`, `unregistered`, `failed`). Without `claude` on `PATH`, `build` spawns once and reports a single informational `AB6019`. `agent-bundle doctor --host claude --from <dir>` runs the validator over the bundle and every installed copy (findings keep `AB6019`–`AB6022`, prefixed with the copy they came from) and reads each row's `enabled` flag: an installed-but-disabled copy is reported as `disabled` with the new `AB7327` warning naming the `claude plugin enable` command. The native Claude proofs now validate in plugin mode through the shared runner and record the documented symlink warning. (#504)

‎docs/diagnostics.md‎

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ even when no error diagnostic was reported.
3636
| `AB7010`–`AB7013` | npm prepack inventory, artifact freshness, package bin targets, and release-version agreement. |
3737
| `AB7200`–`AB7202`, `AB7210`–`AB7211` | Development rebuilds and live host surfaces: rebuild admission and phase failures, development host install sync, and the dev-epoch contract gate (see below). |
3838
| `AB7xxx` | Project preparation and development rebuilds. |
39-
| `AB7300`–`AB7326` | Read-only install Doctor: host probes, installed inventory, bundle comparison and registration proof, runtime endpoint health and identity, durable-state inventory, static bytes-at-rest validation, foreign-install detection (`AB7321`; see below), Cursor plugin hook registration / marketplace staging (`AB7322`–`AB7324`; see below), host load refusal (`AB7326`; see below), and the Cursor Agent Plugins launch proof (`AB7326`; see below). |
39+
| `AB7300`–`AB7327` | Read-only install Doctor: host probes, installed inventory, bundle comparison and registration proof, runtime endpoint health and identity, durable-state inventory, static bytes-at-rest validation, foreign-install detection (`AB7321`; see below), Cursor plugin hook registration / marketplace staging (`AB7322`–`AB7324`; see below), host load refusal (`AB7325`; see below), the Cursor Agent Plugins launch proof (`AB7326`; see below), and a disabled Claude install (`AB7327`; see below). `AB7311` and `AB7325` are also emitted by `build` and `validate --artifact` from the Claude load check (see "Claude Code host validation"). |
4040
| `AB8200`–`AB8209` | Workbench development runtime routes (`/api/runtime/**`): `AB8200` development runtime provider configuration, load, or lifecycle failure, `AB8201` runtime/session/run not available, `AB8202` invalid route path, `AB8203` invalid request shape, `AB8204` stale runtime generation or MCP session revision (409), `AB8205` runtime request could not be completed, `AB8206` Workbench runtime client failure, `AB8207` Agent Document decoding needs the optional `@agent-bundle/runtime` peer (503), `AB8208` stored Flight could not be decoded as an Agent Document (409), `AB8209` decoded Agent Document over the 16 MiB budget (413) or an invalid document response. |
4141
| `AB8210`–`AB8214` | Workbench semantic lifecycle replay routes (`/api/lifecycles`, `/api/lifecycles/replays`): `AB8210` invalid path, `AB8211` malformed replay request or native envelope (400, carries the shared validator message), `AB8212` replay unavailable or could not be completed, `AB8213` stale manifest binding (409; the page repairs it with refresh → explicit re-run), `AB8214` replay over the 16 MiB budget (413). |
4242
| `AB8215`–`AB8218` | Workbench read-only host discovery route. |
@@ -46,11 +46,16 @@ even when no error diagnostic was reported.
4646
| `AB8xxx` | Development server configuration. |
4747
| `AB9xxx` | Eval selection, harnesses, and persisted runs. |
4848

49-
## Claude Code host validation (`AB6019`–`AB6022`)
49+
## Claude Code host validation (`AB6019`–`AB6022`, `AB7311`, `AB7325`)
5050

51-
`agent-bundle validate --artifact <dir>` runs the installed Claude Code
52-
validator for the `claude` and `plugin` targets when `--host-validation` is on.
53-
Claude Code decides what to check from the manifest it is pointed at: a run
51+
`agent-bundle validate --artifact <dir>` and `agent-bundle build` run the
52+
installed Claude Code validator for the `claude` and `plugin` targets when
53+
`--host-validation` is on (the default for both commands; `--no-host-validation`
54+
skips it, and programmatic `build()` calls skip it unless `hostValidation: true`
55+
is passed). `agent-bundle doctor --host claude --from <dir>` runs the same
56+
validator over the `--from` bundle and over every installed copy Claude lists
57+
for the plugin, prefixing each finding with `Bundle at …` or `Installed copy at
58+
… (scope …)`. Claude Code decides what to check from the manifest it is pointed at: a run
5459
against the bundle directory picks `.claude-plugin/marketplace.json` when it is
5560
present and never opens the plugin's skill, agent, command, or hook files
5661
(Claude Code docs, "Create and distribute a plugin marketplace" →
@@ -65,12 +70,31 @@ plugin run already reported. On Claude Code 2.1.259 or later both runs add
6570
releases fall back to the text report, attributed by its `Validating <type>:
6671
<file>` headers.
6772

73+
`claude plugin validate --strict` is not a load verdict: Claude Code 2.1.250
74+
through 2.1.260 accept manifests and component files (for example an invalid
75+
`monitors/monitors.json`, or a `hooks` field naming the auto-loaded
76+
`hooks/hooks.json`) that a session then refuses to load. So `build` and
77+
`validate --artifact` follow the two validation runs with a load check,
78+
`claude --plugin-dir <dir> plugin list --json`, and read the plugin's
79+
`<name>@inline` row: no `errors` is `load.status: 'loaded'`; `errors` is
80+
`refused` and `AB7325`; no row at all is `unregistered` and `AB7311`; a listing
81+
that cannot be read is `failed` and `AB6022`. The check is read-only (the
82+
listing writes nothing under `~/.claude`), is skipped with the two validation
83+
runs when `claude` is absent (`AB6019`), and is skipped when the bundle has no
84+
readable `.claude-plugin/plugin.json` name (the validation runs already report
85+
that manifest). Doctor does not repeat it: its registration proof and the
86+
inventory rows' `errors` already carry the same verdicts. Without `claude` on
87+
`PATH`, `build` spawns once, reports one `AB6019`, and marks the remaining
88+
`claude`/`plugin` targets `unavailable` without spawning again.
89+
6890
| Code | Severity | Meaning | Recovery |
6991
| --- | --- | --- | --- |
7092
| `AB6019` | info | The `claude` CLI is not installed or not on `PATH`, so host validation was skipped. Local pinned-schema validation (`AB6011`/`AB6012`) still runs. | Install Claude Code and ensure `claude` is on `PATH`, then rerun artifact validation. |
7193
| `AB6020` | warning (error in strict mode) / info | One Claude Code validation warning, or (info) one note, from the plugin or marketplace run. The message names the validated file and Claude Code's field path, for example `(hooks hooks/hooks.json): hooks: hooks.postToolUse: unknown hook event`. Claude Code tolerates these at load time; `agent-bundle validate --strict` promotes warnings to errors, mirroring `claude plugin validate --strict`. | Run `claude plugin validate <bundle-dir>/.claude-plugin/plugin.json --strict`, repair the reported Claude artifact, and rebuild. |
7294
| `AB6021` | error | One Claude Code validation error from the plugin or marketplace run, such as invalid JSON in `hooks/hooks.json`, frontmatter that fails to parse, or a duplicate plugin name in `marketplace.json`. Claude Code loads the plugin without the failing component or refuses the marketplace. | Same as `AB6020`. |
73-
| `AB6022` | error | The bounded `claude --version` probe or a validation run could not start, exited nonzero without a report, timed out, exceeded 1 MiB of output, or (2.1.259+) returned no JSON report; the message carries the CLI's stderr when there is one. | Verify the Claude CLI starts and responds, then rerun `claude plugin validate <bundle-dir>/.claude-plugin/plugin.json --strict`. |
95+
| `AB6022` | error | The bounded `claude --version` probe, a validation run, or the load check could not start, exited nonzero without a report, timed out, exceeded 1 MiB of output, (2.1.259+) returned no JSON report, or (load check) returned something other than a JSON array; the message carries the CLI's stderr when there is one. | Verify the Claude CLI starts and responds, then rerun `claude plugin validate <bundle-dir>/.claude-plugin/plugin.json --strict`. |
96+
| `AB7311` | error | The load check's `claude --plugin-dir <dir> plugin list --json` listed no `<name>@inline` row for the bundle (`load.status: 'unregistered'`): Claude Code did not register the directory as a plugin. Doctor emits the same code from its registration proof. | Inspect `claude --plugin-dir <bundle-dir> plugin list --json` and register the intended bundle. |
97+
| `AB7325` | error; warning when every `errors` entry is `Dependency "<name>@<marketplace>" is not installed …` (error under `--strict`) | The load check's row for the bundle carries `errors` (`load.status: 'refused'` with the strings verbatim): `claude plugin validate --strict` accepted the artifact, but a session would refuse to load it. A missing declared dependency is a property of the validating machine rather than of the artifact, so it is a warning and the build completes. Doctor emits the same code for installed copies and its registration proof (see "Host load refusal for Claude installs"). | Fix the artifact so `claude --plugin-dir <bundle-dir> plugin list --json` reports no `errors` for it, then rebuild; for a missing dependency, install it (`claude plugin install <name>@<marketplace>`) or validate where it is installed. |
7498

7599
## Cursor built-artifact validation (`AB6026`–`AB6029`)
76100

@@ -872,7 +896,29 @@ pinned Claude `plugin` schema now rejects (`AB6012` at `/hooks`).
872896
| Code | Severity | Trigger | Recovery |
873897
| --- | --- | --- | --- |
874898
| `AB7006` | error | `install claude` found `errors` on the plugin's row: after `claude plugin install` ran (the install itself exited 0, so the result would otherwise have been `installed`/`replaced`), or on a byte-identical existing copy that would otherwise have been reported `already-installed` (reinstalling the same bytes cannot help). The message carries the host's `errors` verbatim, the install path, and the scope. An unusable post-install listing leaves the result unverified rather than failing an install the host accepted. | Fix the artifact until `claude plugin list --json` shows no `errors` for it (the message names the cause), rebuild, and rerun `agent-bundle install claude --from <bundle-dir> --replace`. |
875-
| `AB7325` | error | Doctor found `errors` on the plugin's row in `claude plugin list --json` (inventory entry `state: 'failed'` with `errors`; `doctor --from` comparison `status: 'load-failed'` with `errors` instead of `current`/`stale`, since the installed bytes never reach a session) or on the `--plugin-dir` registration proof row (`bundle.state: 'failed'` with `errors`, replacing the `registered` verdict). The message carries the host's `errors` verbatim. | Same as `AB7006`: fix the artifact, rebuild, and reinstall with `--replace`. |
899+
| `AB7325` | error | Doctor found `errors` on the plugin's row in `claude plugin list --json` (inventory entry `state: 'failed'` with `errors`; `doctor --from` comparison `status: 'load-failed'` with `errors` instead of `current`/`stale`, since the installed bytes never reach a session) or on the `--plugin-dir` registration proof row (`bundle.state: 'failed'` with `errors`, replacing the `registered` verdict). The message carries the host's `errors` verbatim. `build` and `validate --artifact` emit the same code from their load check (see "Claude Code host validation"). | Same as `AB7006`: fix the artifact, rebuild, and reinstall with `--replace`. |
900+
901+
## Disabled Claude install (`AB7327`)
902+
903+
`claude plugin disable <plugin>` (or the `/plugin` menu) keeps a plugin
904+
installed but switched off: its row in `claude plugin list --json` reports
905+
`enabled: false`, and none of its hooks, MCP servers, or skills reach a session
906+
until `claude plugin enable` runs (Claude Code docs, "Plugins reference" →
907+
"plugin enable" / "plugin disable"). Reinstalling, even with `--replace`, does
908+
not enable it. `agent-bundle doctor --host claude` reads the flag: the
909+
inventory entry carries `enabled: false` with `state: 'disabled'` (instead of
910+
`installed`), and a `--from` comparison of that copy carries `enabled: false`
911+
next to its content verdict — a disabled copy can still be `current` or
912+
`stale`, and both facts are reported. Rows without a boolean `enabled` carry no
913+
flag and are `installed`. A row with `errors` is `failed` (`AB7325`) whatever
914+
its `enabled` value. A plugin that ships `defaultEnabled: false` in
915+
`plugin.json` installs disabled by design ("Plugins reference" → "Default
916+
enablement"); Doctor still reports `AB7327` for it, because the recovery is the
917+
same `claude plugin enable`.
918+
919+
| Code | Severity | Trigger | Recovery |
920+
| --- | --- | --- | --- |
921+
| `AB7327` | warning | `doctor --host claude --from <dir>` compared an installed copy whose row reports `enabled: false`. The message names the plugin, version, install path, and scope. | Run `claude plugin enable <name>@<marketplace> [--scope <scope>]` (or use `/plugin` in a session), then rerun Doctor; reinstalling does not enable a disabled plugin. |
876922

877923
The JSON report exposes the same facts: `hosts[].inventory.findings[].errors`,
878924
`hosts[].bundle.errors`, and `hosts[].bundle.comparison.errors`. The text

‎packages/agent-bundle/src/api.ts‎

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ import type { ProjectContext } from './core/project-context.ts';
107107
import type { NormalizedPlugin } from './core/types.ts';
108108
import {
109109
validateClaudePlugin,
110+
type ClaudePluginCommandRunner,
110111
type ClaudePluginValidationReport,
111112
} from './host-contracts/claude-plugin-validation.ts';
112113
import {
@@ -430,6 +431,18 @@ export interface InvalidInspectResult {
430431
export type InspectResult = ReadyInspectResult | InvalidInspectResult;
431432

432433
export interface BuildOptions extends ProjectOptions {
434+
/**
435+
* After the artifact is written, run the installed Claude developer
436+
* validator (`claude plugin validate --strict` against the emitted
437+
* `plugin.json` and `marketplace.json`) for every built `claude` and
438+
* `plugin` target, exactly as `validate --artifact` does. The CLI `build`
439+
* command requests this by default; programmatic artifact operations
440+
* (temporary artifacts, dev, evals) never do. Without `claude` on `PATH`
441+
* the run costs one failed spawn and reports a single `AB6019` info.
442+
*/
443+
readonly hostValidation?: boolean;
444+
/** Injectable only to make the Claude host validator deterministic in tests; production always spawns `claude`. */
445+
readonly hostValidationRunner?: ClaudePluginCommandRunner;
433446
readonly output?: string;
434447
/**
435448
* Also produce the framework-owned npm package build (`dist/` bin + lib
@@ -438,11 +451,16 @@ export interface BuildOptions extends ProjectOptions {
438451
* operations (temporary artifacts, dev, evals) never do.
439452
*/
440453
readonly packageOutputs?: boolean;
454+
/** Promote host-tool warnings to errors (`hostValidation` only). */
455+
readonly strict?: boolean;
441456
}
442457

443458
export interface BuildProjectResult {
444459
readonly build: BuildResult;
460+
/** Project diagnostics followed by the host-validation findings (`AB6019`–`AB6022`) when `hostValidation` ran. */
445461
readonly diagnostics: readonly Diagnostic[];
462+
/** One report per built `claude`/`plugin` target; present only when `hostValidation` was requested. */
463+
readonly hostValidation?: readonly ClaudePluginValidationReport[];
446464
readonly model: NormalizedPlugin;
447465
readonly packageBuild?: PackageBuildResult;
448466
readonly projectContext: ProjectContext;
@@ -1070,15 +1088,58 @@ export const build = async (options: BuildOptions): Promise<BuildProjectResult>
10701088
});
10711089
if (packageBuild !== undefined) assertPackageOutputSources(packageBuild, projectContext);
10721090
}
1091+
const hostValidation = options.hostValidation === true
1092+
? await buildHostValidation(result.manifest.targets.map((target) => target.name), output, options)
1093+
: undefined;
10731094
return Object.freeze({
10741095
build: result,
1075-
diagnostics: prepared.diagnostics,
1096+
diagnostics: hostValidation === undefined
1097+
? prepared.diagnostics
1098+
: freezeDiagnostics([...prepared.diagnostics, ...hostValidation.diagnostics]),
1099+
...(hostValidation === undefined ? {} : { hostValidation: hostValidation.reports }),
10761100
model,
10771101
...(packageBuild === undefined ? {} : { packageBuild }),
10781102
projectContext,
10791103
});
10801104
};
10811105

1106+
const claudeValidatedTargets: ReadonlySet<string> = new Set<HostValidatedTarget>(['claude', 'plugin']);
1107+
1108+
/**
1109+
* `build --host-validation`: the Claude developer validator (`plugin validate`
1110+
* over both manifests, then the `--plugin-dir … plugin list --json` load check)
1111+
* over every built `claude`/`plugin` target (#476). Targets run one after
1112+
* another: once the CLI proves absent (`AB6019`), the remaining targets are
1113+
* marked `unavailable` without another spawn, so a build without `claude` on
1114+
* `PATH` costs one failed spawn and reports the skip once.
1115+
*/
1116+
const buildHostValidation = async (
1117+
targets: readonly string[],
1118+
output: string,
1119+
options: Pick<BuildOptions, 'hostValidationRunner' | 'strict'>,
1120+
): Promise<{ readonly diagnostics: readonly Diagnostic[]; readonly reports: readonly ClaudePluginValidationReport[] }> => {
1121+
const reports: ClaudePluginValidationReport[] = [];
1122+
let unavailable = false;
1123+
for (const target of targets.filter((name) => claudeValidatedTargets.has(name))) {
1124+
if (unavailable) {
1125+
reports.push(Object.freeze({ diagnostics: freezeDiagnostics([]), host: 'claude', status: 'unavailable', target }));
1126+
continue;
1127+
}
1128+
const report = await validateClaudePlugin({
1129+
pluginDirectory: join(output, target),
1130+
...(options.hostValidationRunner === undefined ? {} : { run: options.hostValidationRunner }),
1131+
...(options.strict === undefined ? {} : { strict: options.strict }),
1132+
target,
1133+
});
1134+
reports.push(report);
1135+
unavailable = report.status === 'unavailable';
1136+
}
1137+
return Object.freeze({
1138+
diagnostics: freezeDiagnostics(reports.flatMap((report) => report.diagnostics)),
1139+
reports: Object.freeze(reports),
1140+
});
1141+
};
1142+
10821143
const execFile = promisify(executeFile);
10831144

10841145
export const prepack = async (options: BuildOptions): Promise<PrepackResult> => {

0 commit comments

Comments
 (0)