Diagnostics reference
This page is a build-time copy of docs/diagnostics.md, the repository's diagnostics contract. Change that file, not this page.
Every agent-bundle failure or nudge is one structured diagnostic: a stable
code (AB + four digits), a severity (error, warning, or info), a
message, and usually a sourcePath and a recovery hint. The
diagnostic-gated commands (build, prepack, validate, doctor, install,
dev) exit nonzero only when an error diagnostic is present; warnings and
infos never gate a build, a validation, or a dev rebuild. eval and inspect
additionally exit 1 for a failing or inconclusive trial or an invalid model
even when no error diagnostic was reported.
Code families
Skill documents and Skill IR lowering (AB3000–AB3010)
A skill directory (src/skills/<name>/, or a directory an explicit skills
entry names) holds a hand-authored SKILL.md or a rendered source module
(SKILL.tsx/SKILL.ts; docs/framework-mode.md). Parsing runs at discovery
and reports AB3000–AB3005 on the document; the Skill IR is then read from
the frontmatter (AB3006) during source validation, and the IR is lowered once
per selected skill host, amp, claude, codex, cursor, portable,
with AB3008–AB3010 judged on
the normalized model and carrying target. A skill whose frontmatter declares
no host extension and whose body carries no token passes through byte for byte
and is never lowered. AB3007 is not assigned.
Plugin metadata and Skill source validation (AB4000–AB4007, AB4012)
plugin.name is the host-native slug every manifest carries; plugin.version
is retired and rejected because package.json is the sole release-version
source; plugin.logo is an optional project-relative image. Each discovered skill is checked
against the pinned Agent Skills frontmatter schema
(schemas/agent-skills/frontmatter.schema.json: name and description
required, closed portable field shapes), its directory, its resources, and
the other skills. AB4009–AB4011 and AB4013 are the release
identity codes (see "Release identity" below). Every row is reported on the
config file (AB4000, AB4001, AB4012) or the skill source.
Normalized model invariants (AB4100–AB4102)
validateModel runs over the normalized plugin after source validation and
normalization, on every command that prepares a project. These codes judge
the model rather than the config text, so they also hold for a model handed
to the API directly. Source-level duplicates (AB4006, AB4408, AB4802)
are usually reported first; AB4101 and AB4102 are the model-level
backstop.
Hook configuration and native hook sources (AB4200–AB4212)
hooks.<event> accepts a handler path, an entry object (handler,
targets, tools, timeout, args), or an array of those; see
website/docs/en/guide/authoring/hooks.mdx. AB4200–AB4205 and
AB4210–AB4212 are source checks reported on the config file;
AB4203/AB4204 are repeated on the normalized model with target;
AB4206–AB4209 judge the target-native hook documents an adapter's
nativeHooks key names (claude.nativeHooks, codex.nativeHooks). A hook
without targets inherits the selected targets that support hooks, so a host
without a hook surface (portable) is skipped there without a diagnostic;
naming it explicitly is AB4204. Prebuilt handlers ({ prebuilt }) and
args are judged by AB4744–AB4746.
MCP server and MCP App configuration (AB4300–AB4339)
mcp.servers.<id> declares one of entry (a local module compiled into a
stdio server; the conventional src/mcp/<id>.ts supplies it when all three
are absent), command (an external stdio command), or url (a remote
streamable-http server); apps declares MCP App views for a server with a
local entry. AB4300–AB4319, AB4322–AB4335, and AB4338 are source
checks reported on the config file and name the server or App;
AB4320, AB4321, AB4336, AB4337, and AB4339 are judged on the
normalized model. A server the route graph compiles in generated mode is
judged by AB4340 (see below) plus the shared local-entry field rules
(AB4305, AB4308–AB4312) and the App rules.
Script configuration (AB4400–AB4408)
scripts.<name> is an entry path or { entry, targets }. A bundled entry
(.js, .jsx, .mjs, .cjs, .ts, .tsx, .mts, .cts) compiles to
scripts/<name>.mjs; .sh, .bash, and .py entries are copied and keep
their extension. Conventional src/scripts/ routes are judged by
AB4808/AB4809 and AB4737/AB4738 instead. Every row is reported on the
config file; AB4406 is repeated on the normalized model.
Registered config extensions (AB4500)
The host-scoped config keys (claude, codex, cursor, portable) are
extensions the target adapters register. Normalization deep-clones each
declared extension as strict finite JSON before any adapter reads it; a value
that cannot be cloned aborts normalization, and validate, build,
inspect, and dev report the failed preparation under this code on the
config file.
Assets and the generated-runtime floor (AB4600–AB4602)
assets lists project files or globs copied into the artifact's assets/;
runtime.node raises the minimum Node.js version of the generated
executables above the framework default (22.12.0). The two validators share
three codes; the message tells them apart.
Package build configuration: bin, output, and lib (AB4700–AB4716)
bin (false, or { <name>: '<entry>' } / { <name>: { entry } }) and
lib (false, an entry path, or { entry, dts }) declare the framework-owned
npm package build under dist/ (docs/framework-mode.md, "Package
entries"); output.distPath moves the artifact output. Bundled entries must be
JavaScript or TypeScript modules (.js, .jsx, .mjs, .cjs, .ts, .tsx,
.mts, .cts). Every row but AB4706 is reported on the config file.
AB4716, the declaration-emit failure of a lib entry with dts, has its own
section, "Declaration generation", below.
General CLI and adapter failures (AB5000)
AB5000 is the catch-all for a failure that reached the CLI or the build
without a structured diagnostic of its own. Structured failures never fall
through to it: declaration-emit failures are AB4716, MCP App compile
failures AB4770, and every validator code keeps its own number.
Claude Code host validation (AB6019–AB6022, AB7311, AB7325)
agent-bundle validate --artifact <dir> and agent-bundle build run the
installed Claude Code validator over the composite root when claude is selected and
--host-validation is on (the default for both commands; --no-host-validation
skips it, and programmatic build() calls skip it unless hostValidation: true
is passed). agent-bundle doctor --host claude --from <dir> runs the same
validator over the --from bundle and over every installed copy Claude lists
for the plugin, prefixing each finding with Bundle at … or Installed copy at … (scope …). Claude Code decides what to check from the manifest it is pointed at: a run
against the bundle directory picks .claude-plugin/marketplace.json when it is
present and never opens the plugin's skill, agent, command, or hook files
(Claude Code docs, "Create and distribute a plugin marketplace" →
"Marketplace validation errors"). Agent Bundle emits both manifests side by
side, so it runs claude plugin validate <dir>/.claude-plugin/plugin.json --strict
first, which covers plugin.json, hooks/hooks.json, and the skills/,
agents/, and commands/ directories, and then
claude plugin validate <dir>/.claude-plugin/marketplace.json --strict, dropping
the marketplace run's plugins[N] plugin.json → copies of manifest findings the
plugin run already reported. Both runs ask for --json first (Claude Code
2.1.259 or later; the pinned 2.1.260 prints it) and each finding is attributed
to its file (generatedPath); when the CLI rejects the flag (unknown option '--json') or the probed version predates 2.1.259, the runs fall back to the
text report, attributed by its Validating <type>: <file> headers.
claude plugin validate --strict is not a load verdict: Claude Code 2.1.250
through 2.1.260 accept manifests and component files (for example an invalid
monitors/monitors.json, or a hooks field naming the auto-loaded
hooks/hooks.json) that a session then refuses to load. So build and
validate --artifact follow the two validation runs with a load check,
claude --plugin-dir <dir> plugin list --json, and read the plugin's
<name>@inline row: no errors is load.status: 'loaded'; errors is
refused and AB7325; no row at all is unregistered and AB7311; a listing
that cannot be read is failed and AB6022. The check is read-only (the
listing writes nothing under ~/.claude), is skipped with the two validation
runs when claude is absent (AB6019), and is skipped when the bundle has no
readable .claude-plugin/plugin.json name (the validation runs already report
that manifest). Doctor does not repeat it: its registration proof and the
inventory rows' errors already carry the same verdicts. Without claude on
PATH, build spawns once, reports one AB6019, and marks the remaining
claude projection unavailable without spawning again.
Cursor built-artifact validation (AB6026–AB6029)
Codex host validation (AB6030–AB6033)
Codex 0.147.0 publishes plugin installation commands but no plugin-validation developer tool. Agent Bundle therefore validates built Codex JSON documents against its vendored pinned schemas and treats the app-server schema generator as a separate drift signal, never as a substitute plugin contract.
Agent Skills emitted spec lint (AB6034)
Agent Bundle evaluated @skill-tools/core@0.2.2 on 2026-09-02 and found a
genuine Agent Skills utility whose lint is nevertheless lower fidelity than
the pinned specification contract. Its parser does not pin a specification
revision, misses closed-frontmatter rules already enforced here, and contains
an internally inconsistent description-length warning. The package is not a
runtime dependency; the one missing mandatory rule it identified is enforced
locally against emitted bytes.
Agent Plugins portable validation (AB6035–AB6038)
The portable target is the Agent Plugins open standard
(specification 1.0.0) adapter. Its contract is pinned in
packages/agent-bundle/src/adapters/schemas/portable/PROVENANCE.json (schema
hashes, specification repository commit, retrieval and re-verification dates).
The standard publishes machine-readable schemas plus normative text the schemas
cannot express; the text wins on conflict, so validation runs both lanes and
never spawns a client CLI (the standard publishes no reference validator).
Validation happens at three moments, all fail-closed:
- Plan time (
agent-bundle build/validate): the emittedplugin.jsonandmcp.jsonare validated against the pinned schemas before they are written (portable.schema.plugin,portable.schema.mcp), authored manifest metadata is checked field by field (portable.manifest.<field>.invalid), MCP path tokens are refused where the standard forbids them (portable.mcp.token.*), and the normative MCP rules the schemas cannot express are applied to each server as it will be written (portable.mcp.{command,cwd,env,url,headers}.standard: command form, cwd containment, env-key placeholders, URL form, header names/values/casing). These target-scoped codes are errors, so a standard-invalid server never reaches an artifact. - Artifact time (
agent-bundle build,validate --artifact): the generic target-contract pass reports a missing required document asAB6011and a pinned-schema rejection asAB6012, and the Agent Plugins byte lane below (AB6035–AB6037) runs over every tree emitted by the built-in portable adapter, so a standard-invalid layout fails the ordinary build before publication (a tree already carrying a symlink or other unsupported entry is reported asAB6013and never read by this lane).validate --artifact --host-validationadditionally returns the same lane as aportablehost validation report with theAB6038provenance note. - Installed bytes (
agent-bundle doctor): a Cursor local plugin whose rootplugin.jsondeclares an Agent Plugins$schemais validated with the same byte lane and reported underAB7320(an error marks the entrycorrupt).
npm prepack gate (AB7010–AB7015)
AB7014 counts exactly three sources of evidence that a consumer needs a
declared package:
runtimeDependencieson a prebuilt payload (definePrebuilt);- a packed
.d.ts,.d.mts, or.d.ctsreference as TypeScript'spreProcessFilereads it:import/export … from,import x = require("…"),import("…")types, module augmentations in an external-module file, or/// <reference types="…" />(the named package and its@typestwin). An ambientdeclare module "x"in a file with no imports declares that module and counts for nothing. A#subpathreaches packages through the manifest'simportsmap; relative and built-in specifiers name nothing; - the consumer-side
preinstall,install, orpostinstallscript grammar, including scripts reached through package-manager run and direct script commands, naming the package or running one of itsbincommands.
The framework's own runtime modules bundled into a generated executable load
no package at run time (generated-module-evidence.test.ts holds that), so
there is no framework process-dependency record to read as a fourth source.
A package the compiler bundled is not used. When one is otherwise unused, the
diagnostic names the dist bundles that inlined it. Expression requests
(import(expr), require(expr)) remain outside the compiler's view; the
emitted-module walk still fails a compiled module on an expression import()
(AB6005 has a non-literal dynamic import) and on a literal import the
build was told to ignore (rspackIgnore/webpackIgnore; AB6005 loads "<request>", which the compiler neither bundled nor recorded as an external),
while require(expr), require.resolve(…), createRequire(…)(…), and
import.meta.resolve(…) are not imports and are outside both.
Declaration generation (AB4716)
A lib entry with dts enabled compiles its source directory as its own
TypeScript program. When that declaration emit fails, the bundler aborts with
one prose line naming only its own environment, so the framework replays
declaration emit over the same synthesized project (the consumer's own
typescript, the same tsconfig, --declaration --emitDeclarationOnly) and
reports one AB4716 error per recovered TypeScript diagnostic, each
carrying the file, the (line,column) position, the TS code, and the
compiler's message, plus a sourcePath:
When no diagnostic can be recovered, the project has no resolvable
typescript, or the replay passes because the failure was elsewhere in
declaration generation, the failure still reports as a single AB4716
carrying the bundler's own message. Declaration failures never fall through
to the AB5000 catch-all, whose dev-lock meaning previously misdirected
triage.
The recovery hint names the trap these failures share: declaration-emit
errors such as TS4023 (an exported value whose inferred type names a type
its module does not export) are invisible to tsc --noEmit, so a green
typecheck script proves nothing about them. Reproduce them with
tsc --declaration --emitDeclarationOnly over the lib entry source
directory.
MCP App view compilation (AB4770–AB4772)
MCP App views compile through Rsbuild with its logging silenced
(logLevel: 'silent'), so the bundler never prints on its own. The framework
reads the Rspack stats of every App environment instead and reports one
AB4770 error per Rspack error, each carrying the failing module as a
project-relative path (forward slashes; absolute when the module lives outside
the project root), the line:column the bundler reported, and the bundler's
message, ANSI colours, the miette frame glyphs, and code-frame lines
stripped, the remaining lines joined into one, plus a sourcePath naming the
failing module:
The third line is the shape without a location: Rspack attributes a
tsconfig.json whose extends target is missing to no module, so the message
carries only the bundler text and sourcePath falls back to the App's entry
source. A compile that fails without a single stats error still reports one
AB4770 with the bundler's own message. More than 20 errors on one App are
cut at 20, and the last diagnostic ends with … and N more errors (run the compile with logLevel error via tools.rsbuild for the full list). App compile
failures never fall through to the AB5000 catch-all, and agent-bundle dev
shows the same AB4770 rows in the Workbench Overview's Diagnostics table,
the Source column is the failing file, instead of
AB7100 "Unable to compile the build: Rspack build failed.".
Rspack warnings that are not on the framework's ignore list report as
AB4771 warnings of the same shape, with produced a warning while compiling in place of failed to compile. They never fail the build and are
returned beside the compiled Apps (build.diagnostics in
agent-bundle build --json). The ignore list is the documented constant in
packages/agent-bundle/src/build/mcp-app-diagnostics.ts, one comment per
entry citing the warning text it drops and why it is noise; it may be empty.
Every App is measured after it is emitted: the UTF-8 bytes of the
self-contained HTML and their gzip size, what a compressing transport would
carry. AB4772 is the size advisory, one warning per App. A view that
reaches its host through agent-bundle/app carries the framework's client
and nothing else from the protocol stack; one that imports
@modelcontextprotocol/ext-apps 2.x instead starts at about 249 kB (65 kB
gzip): zod 4, @modelcontextprotocol/client, @modelcontextprotocol/core,
and ext-apps itself. A 1.x view, which also carries zod 3,
@modelcontextprotocol/sdk, and zod-to-json-schema, starts at about 445 kB
(106 kB gzip). The advisory bound of 1 MiB (1,048,576 bytes) sits at roughly
2.4× the 1.x floor and at half the 2 MiB (2,097,152 bytes) bound above
which the Workbench and serve-app hosts refuse the resource and the Rstest
browser harness refuses to mount it. The advisory fires when a production
build emits 1 MiB or more, and in either compile mode when the document
exceeds 2 MiB (the view will not render in those hosts). The message names
the raw and gzip sizes, the bound that was crossed, and the five largest
modules from the stats (project-relative, or node_modules/<package>/…), with
sizes 1024-based to one decimal, a trailing .0 dropped (427.1 KiB,
1.3 MiB, 2 MiB). Both thresholds are fixed; no configuration key moves
them.
agent-bundle dev compiles views unminified so the Workbench preview is
readable, about 2.7× the production bytes. A view whose readable document
would exceed the 2 MiB host bound is recompiled with the production profile
so the preview still renders it, and one AB4772 reports the substitution
instead: MCP App "<name>" readable development output compiled to <size>, above the 2 MiB bound the Workbench and serve-app hosts accept; the preview renders the production build (<size>, <gzip> gzip) instead; largest modules: …. The production sizes in that notice stand in for the 1 MiB advisory, so
a substituted view never carries two size advisories. When the production
build itself exceeds 2 MiB the substitution buys nothing: the notice is not
emitted, the preview receives that production document, and the ordinary
over-bound AB4772 names its sizes so the author knows the view does not
render. The 1 MiB advisory is a production concern and never fires on
readable output.
Release identity (AB4009–AB4011, AB4013)
package.json is authoritative for release identity (issue #94): its name
and version become the packageName and packageVersion axes carried on
the project context, artifact manifests, inspect output, and dev status.
plugin.name stays the host-native slug and is never derived from the npm
package name.
A project with no valid package.json version has no release identity.
Development commands (dev, inspect,
validate) keep running on the labeled 0.0.0-dev.<short-revision> fallback,
because an unpackaged scratch project is a normal development state. A
development-only fallback can never produce a release artifact, so
agent-bundle build alone refuses it with AB4013.
Entry conventions and convention claims (AB4730–AB4738)
The entry conventions and the framework-owned stdio lifecycle shell (RFC #50)
define how a project's modules reach an artifact. When validate, inspect,
build, or dev prepares project source, it reports what the conventions
refuse and what they silently shadow. AB4730, AB4737, and AB4738 are
errors: an entry the framework cannot wrap, and a conventional script
whose bin entry would run an export the artifact script ignores, cannot
ship, so the compiler refuses to misbuild them silently. AB4731–AB4735
are informational nudges for a confusable state where explicit
configuration shadows a conventional file on disk, and never block anything.
AB4736 is retired. The CLI prints these in human validate output and
includes them in every --json diagnostics array.
Which explicit config keys claim a conventional module out of discovery is
tabulated in docs/entry-conventions.md ("Which config keys claim a
conventional module"). In short: scripts, hooks, lib, and mcp entries
claim the module they reference; a bin entry claims every conventional
module except a safely named direct src/scripts/<name> child, which
keeps shipping as an artifact script beside the bin because the two outputs
are disjoint and both envelopes run the same main. That dual-surface shape
is intentional and raises no diagnostic.
AB4730 stdio MCP entry without a server factory
A local MCP server entry module (explicit entry: or the conventional
src/mcp/<server-id>.ts) has no default export. Every local entry is wrapped
in the framework stdio lifecycle shell (console-to-stderr guard,
SIGINT/SIGTERM, stdin-EOF exit, bounded shutdown, heartbeat), and the shell
calls the module's default export to build the server, so a module without
one cannot be built. Validation finds the default export with a static scan of
the entry's top-level statements. A CommonJS entry's top-level
module.exports = <factory> counts as that default export, because the
bundler exposes the assigned value as the module's default, unless the file
declares its own module binding. A narrower exports.foo = … or
module.exports.foo = … does not count.
Recover: default-export the server factory from the entry module, or declare
a prebuilt server with command or url, which the framework launches
as-is and never wraps.
AB4731 src/cli.ts shadowed by explicit bin config
src/cli.ts (or .tsx) exists, but the explicit bin configuration never
references it, so the conventional package bin is silently shadowed.
bin: false is a deliberate opt-out and stays silent.
Adopt: remove the explicit bin configuration, or point one entry at the
file. Silence: remove the file, or keep the explicit config knowingly.
AB4732 src/index.ts shadowed by explicit lib config
src/index.ts (or .tsx) exists, but the explicit lib configuration
points elsewhere. lib: false is a deliberate opt-out and stays silent.
Adopt: remove the explicit lib configuration, or point it at the file.
Silence: remove the file, or keep the explicit config knowingly.
AB4733 src/mcp/<server-id>.ts shadowed by explicit server config
The conventional stdio entry file exists for a declared server, but that
server names an explicit entry, command, or url that does not resolve
to it, a confusable state where the file on disk is not what runs.
Adopt: drop the explicit entry/command/url so the convention applies.
Silence: remove the shadowed file.
AB4734 conventional skill shadowed by explicit skills config
A src/skills/<name>/SKILL.md (or rendered SKILL.tsx/SKILL.ts) directory
exists, but the explicit skills configuration does not cover it, the
conventional skill is silently shadowed. When config is silent, every
src/skills/<name>/ directory ships by convention and this nudge never fires.
Adopt: remove the explicit skills configuration so the convention applies,
or add the directory to skills. Silence: remove the directory.
AB4735 rendered skill source shadowed by hand-authored SKILL.md
A skill directory contains both a hand-authored SKILL.md and a rendered
skill source (SKILL.tsx/SKILL.ts). The authored file wins, an authored
document beats a generated one, so the component module never compiles.
Adopt: remove SKILL.md so the rendered skill compiles at build. Silence:
remove the component module.
AB4736 retired
The report of documents left in the removed top-level locations
(skills/<name>/SKILL.md, commands/*.md, rules/*.mdc). Discovery reads
src/skills/<name>/SKILL.md, src/commands/*.md, and src/rules/*.mdc
only, so a document at the top level is now ignored without a diagnostic. An
explicit skills path still names a skill directory anywhere in the project,
including the top level. Published artifact paths remain skills/,
commands/, and rules/. The code is never reused.
AB4737 rendered script claimed as a package bin entry lacks main or the component
An explicit bin entry references a conventional rendered script
(src/scripts/<name>.tsx or .jsx) that does not declare both a default
component and a named main. Inspection checks export presence; the
runtime checks callability. Components may be sync or async, and the bundler
resolves re-exports. A plain src/scripts/<name>.ts module
ships happily on both surfaces, the npm bin envelope calls its main(argv)
and the artifact script is the same bundle, but a rendered script's default
export is a Server Component the Agent renderer drives with
{ argv, signal } props. The bin envelope prefers a named main export and
only falls back to the default export, so without main it would call that
component as main(argv) and produce a bin that renders nothing; without the
default component, the bin works but scripts/<name>.mjs fails at run time
with no component to render. The compiler refuses either shape instead of
emitting a broken surface beside a working one. A rendered script that
exports both serves both surfaces and is not gated. The message names every
bin entry referencing the module and which export is missing.
Recover: export both a default Server Component and a named
main(argv) from the module; point the bin entry at a plain module that exports main;
rename the script to .ts so one plain module ships as both the bin and the
artifact script; or prefix a path segment with _ (src/scripts/_name.tsx)
to keep the module out of script discovery and bin-only.
AB4738 plain script claimed as a package bin entry runs only as the bin
An explicit bin entry references a conventional plain script
(src/scripts/<name>.ts) that exports a default but no named main. Both
the bin envelope and the artifact-script envelope wrap a main(argv) export
and bundle a self-executing module (no main, no default) byte for byte,
so those shapes run identically on both surfaces. Only the bin envelope falls
back to invoking a default export: the artifact scripts/<name>.mjs would
merely define the function and exit, so a successful build would publish an
inert script beside a working bin. The detection is the same static export
scan the package build uses. The message names every bin entry referencing
the module.
Recover: export a named main(argv) so both surfaces run the same entry;
make the module self-executing (drop the default export and run at top
level); or prefix a path segment with _ (src/scripts/_name.ts) to keep
the module out of script discovery and bin-only.
Prebuilt payloads (AB4740–AB4751)
The payload block and { prebuilt: ... } entries (see
docs/entry-conventions.md) package files the framework did not compile.
The consumer's own build produces them, so their diagnostics split by
moment: configuration mistakes are validation errors, a payload that has
simply not been built yet is a validation warning that only
agent-bundle build escalates, and freshness is an info nudge.
Build-time identity outside the compiler (AB4760)
agent-bundle/meta (see docs/entry-conventions.md) is a reserved specifier
the compiler replaces in every compiled surface with the project's exact
{ name, packageName, packageVersion, version }. The published
dist/meta.js module behind that specifier therefore never carries an
identity of its own: every binding, name, version, packageName,
packageVersion, meta, and the default export, throws this diagnostic at
module evaluation, so a module that reaches it fails on import rather than
observing a fabricated identity. The thrown value is an Error named
AgentBundleMetaUnavailableError whose code, recovery, and structured
diagnostic fields carry the same data the message prints, so a bare node
process and a test runner both show the fix. The importing module is not
observable from a module evaluated through ESM linking, so the message names
the situation, not a file; the runner's own "failed to load" line names the
file.
Unit tests are the common way to reach it (issue #386): a plain Rstest pool
imports a source module that imports agent-bundle/meta, no compiled surface
replaced the specifier, and every test that touches that module fails at
import. agentBundleRstest() and agentBundleBrowserRstest() prevent this by
aliasing the specifier to .agent-bundle/test/meta.mjs, generated from the
same compiler pass. When that pass produced no plugin model (the configuration
could not be loaded or normalized) there is no identity to stamp, so the
aliased module throws the same AB4760 naming the compiler diagnostics and
the recovery "fix them, then rerun Rstest", the manifest's placeholder
identity is never served as a real one.
Rendered skills (src/skills/<name>/SKILL.tsx) evaluate during discovery,
and the skill loader aliases the specifier to the same generated module fed
from the identity normalization stamps into the model, so a skill importing
agent-bundle/meta compiles under validate, build, inspect, dev, and
inspectWorkbenchSurface (#440). Only a direct parseSkill call without a
project identity leaves the specifier to resolve as the project resolves
agent-bundle; the published module then raises this diagnostic inside the
skill's AB3003.
The composite plugin root (AB4100, AB4103, AB4105, AB4106)
build emits one composite root at the artifact directory (#555). The
targets list selects which host projections the root carries,
amp, claude, codex, cursor, portable. claude, codex, cursor, and
portable read the artifact directory itself as their plugin root: their
manifests sit in .claude-plugin/, .codex-plugin/, .cursor-plugin/, or the
portable root plugin.json, while skills/, hooks/, mcp/, scripts/,
bin/, and INSTALL.md are shared. Amp is the exception: its plugin root is
the nested .amp/plugins/<plugin>/ directory, self-contained down to its own
skills/ and hooks/, and install amp copies that subdirectory rather than
the composite root. There is no <root>/<host>/ partition and no plugin
target: the composite is the output. Only those five
built-in hosts share a root; an adapter registered on an advanced
TargetRegistry is built alone (AB4106). validate and inspect plan the
same composite root the build stages, so they report AB4103 and AB4105
exactly where build would refuse.
Artifact-hosted routed CLI and npm lifecycle paths (AB4765–AB4768)
A generated-mode src/cli/** surface compiles once into every host artifact whose adapter
publishes a supported cli capability, as bin/<plugin-name>.mjs (plus
bin/<plugin-name>-flight.mjs when any command renders). Every built-in
target hosts it; the two codes cover a target that does not and a host file
that claims the same path. The npm root copies the manifest-declared bin
unchanged and points package.json at it rather than recompiling the graph.
See “The routed CLI shell” in
docs/entry-conventions.md for the layout and the sibling-path convention.
Config beside a route-generated MCP server (AB4340)
A mcp.servers.<id> block for a server the route graph compiles in
generated mode augments that server (env, args, targets, apps,
transport: 'stdio'), see the precedence table in
Entry conventions.
The local-entry field rules apply to it unchanged (AB4305, AB4308–AB4312,
AB432x), and it never triggers AB4304 or AB4322: the route modules are
its entry.
Web surface exposure (AB4341)
The optional web config key selects among MCP Apps already declared under
mcp.servers.<id>.apps and is the only input the artifact-resident browser
host consults (via the manifest web section). Validation is one code with
nine message shapes.
Conventional host components: rules and commands (AB4900–AB4908, AB4920–AB4928)
Conventional src/rules/*.mdc documents compile to the Rule IR (closed
frontmatter: description, globs, alwaysApply, plus the bundle-only
targets key that is peeled before emission) and src/commands/*.md
documents compile to the Command IR (closed frontmatter: description,
argumentHint, allowedTools, model, disableModelInvocation, plus
targets). Each host lowers only the surfaces its pinned capability table
supports; a document without targets is emitted where supported and
accounted as skipped with the host's judgment elsewhere (see
agent-bundle inspect), while a document that explicitly names a host without
the surface is a build error, unsupported components fail before artifact
publication rather than shipping as a broken half. Identity paths are
canonicalized so the model digest is root-independent.
Every frontmatter field is also a component feature (#100): each host
publishes one <kind>.<feature> capability row per field it can express
(commands.argumentHint, rules.globs, …; see
Host components). A component
that uses a feature the target's row does not support is judged per target:
an explicitly named target fails closed (AB4907 / AB4927), while an
implicitly selected target still receives the component minus the feature and
the omission is reported as a warning with the host's reason (AB4908 /
AB4928) and on the selected component in inspect (omittedFeatures).
Targets whose kind row is itself unsupported are judged by the kind-level codes
above, never per feature. Skills keep their own closed per-host schemas
(AB3006, AB3008, AB3010).
The bundler escape hatch (AB4720–AB4726)
tools.rsbuild and tools.rspack are validated with the rest of the config
source, so a malformed or colliding hatch is an error before any bundler
runs, in validate, build, inspect, and dev alike. AB4720–AB4723
check the shape: tools must be an object whose only keys are rsbuild
(an Rsbuild environment-config object) and rspack (an Rspack config
object, a mutator function, or an array of both).
AB4724 checks tools.rsbuild.plugins against the Rsbuild plugins the
framework registers itself, currently @rsbuild/plugin-react
(rsbuild:react), which every synthesized Rslib entry and every MCP App
view carries, whatever the view's entry extension. The hatch merges beside the framework profile
(mergeRslibConfig / mergeRsbuildConfig concatenate plugins arrays), and
Rsbuild's plugin manager appends every plugin it is handed without deduping by
name, so re-adding pluginReact() would register it twice. The check is
static: plugin objects are matched by name, nested arrays are flattened the
way Rsbuild flattens them, false/null/undefined holes are skipped, and a
plugin supplied as a Promise is not inspected. It is an error rather than a
warning for the same reason as its siblings: a config problem with one
deterministic fix, reported once at the source, so no build ever runs a
framework-owned plugin twice by accident.
Route graph, state, layout, and provider conventions (AB4800–AB4845, AB4940–AB4942)
The route-graph compiler discovers conventional route modules
(src/mcp/<server>/{tools,resources,prompts,apps}/*, src/events/*/*,
src/providers/*, src/cli/**, src/scripts/**) and the shared layout
modules (src/layout.*, src/mcp/<server>/layout.*) into one immutable IR.
A .cli.{ts,tsx} module under src/mcp/** is reserved as a CLI surface
projection of a sibling tool, not a route: discovery records it and pairs
it, and never derives a tool:<server>/<stem>.cli identity from it.
Discovery is not a packaging choice, so every collision is a hard error
and the compiler never silently picks a side. Modules that explicit
scripts, hooks, bin, lib, or mcp configuration references are
claimed by that declaration and never become routes, config always wins.
agent-bundle inspect --routes dumps the compiled graph.
Each route's config export is extracted statically, the module is parsed
with the TypeScript compiler, never executed, from a single top-level
export const config = <expression> declaration. The accepted expression
grammar is: object literals whose property names are identifiers, string
literals, or numeric literals (no computed names, spreads, shorthand
references, methods, or accessors); array literals without spreads or holes;
string literals and substitution-free template literals; numeric literals,
optionally wrapped in unary +/-; true, false, and null; and
as/satisfies casts, non-null assertions, and parentheses around any
accepted form. Two constrained reference forms are accepted for string
values, so an MCP App's resourceUri never has to be repeated as a literal
in every tool that opens it:
- A local
conststring-literal identifier. A top-levelconst X = '<literal>'in the route module. Imported values and alias chains are not evaluated. appResourceUri('<app>')imported fromagent-bundle/routes. The compiler resolves the reference to the target App route's staticconfig.resourceUriwhile compiling the graph. The App must belong to the referencing route's own generated server, a generated server registers exactly its own Apps, so another server's URI could never be read through it. References are'<app>','<server>/<app>','app:<server>/<app>', or a module path relative to the referencing file ('../apps/dashboard', with or without its.ts/.tsxextension, a.js/.jsxspelling maps onto the TypeScript source, and any other suffix is part of the App name). The argument may be a string literal or a const identifier of the first form. An unknown reference, another server's App, an App whose ownresourceUriis not a static string, or any reference from a non-MCP route, isAB4826, and the route compiles with the empty config beside it. Routes of a server that is not generated (custom/command/remote, or anAB4800conflict) never ship their config, so their references are left as authored rather than reported. Whether referenced or written as a literal, an advertised_meta.ui.resourceUrimust name an App the server builds for every target it ships to (AB4828otherwise). At run time the helper returns the reference unchanged: generated servers read the compiled config, never the module's evaluatedconfig, so use the const form when the URI is also needed inside the component.
Anything else, any other identifier, a call, a package import, a relative
import that leaves the project or whose chain does not end in a
string-literal export const, is dynamic: the route compiles with an empty
config beside a named AB4806 error whose recovery names both reference
forms. A module without a config export compiles silently with an empty
config.
An MCP App route's config.template resolves relative to the route
module, the way its imports do (template: './dashboard.html'). When that
path names no existing file, AB4827 names the resolved candidate. The IR
keeps the authored path (so the
graph digest stays machine-independent) and the normalized model carries the
resolved absolute file. Config-declared Apps (mcp.servers.<server>.apps)
keep resolving entry and template from the project root, where the config
file lives.
Generated route declarations are published at .agent-bundle/routes.d.ts from
the same graph. Development writes a sibling temporary file and renames it over
the prior complete declaration atomically; invalid source retains the prior
last-good file, while a successful route-free, provider-free preparation
removes it. Beside AgentBundleRoutes, a graph with conventional providers
declares AgentBundleProviders (ProviderKey, ProviderValue<Key>), each
camel-cased key mapped to its factory's awaited return type, in execution
order, and augments @agent-bundle/runtime's AgentProviderValues so
await (await agent()).provider(key) observes that type in projects whose
TypeScript program includes the file. Provider-free graphs emit no
augmentation, so the declaration never references a module the project has no
reason to depend on. The file is only as good as the program that compiles
it: create-agent-bundle templates and the examples/* projects list
".agent-bundle/routes.d.ts" in tsconfig.json include (a literal entry,
because **/* never descends into dot-directories), while the file itself
stays gitignored. Framework validation publishes declarations without reconstructing
TypeScript programs. Include the file in every consuming tsconfig and run the project's
actual TypeScript check, including each relevant project in a referenced workspace.
Conventional src/scripts/ routes ship through the same pipeline as
explicit scripts entries (#102 stage 1): a plain module directly under
src/scripts/ compiles once to scripts/<name>.mjs in the plugin root, shared
by every selected host, with provenance.kind: 'conventional'. A rendered module
(src/scripts/<name>.tsx/.jsx, #102 stage 3) compiles to the same
scripts/<name>.mjs plus a sibling scripts/<name>-flight.mjs react-server
worker: its default component receives { argv, signal } and renders
through the Agent renderer with the full CLI output contract (--json,
--ndjson, interactive TTY progress, piped Markdown); the framework dialect
reserves exactly --json and --ndjson, every other argument passes
through as argv, and the exit code derives from the final document status
(0 on success, 1 otherwise). Explicit scripts config entries keep
ordinary Node semantics regardless of extension, config always wins, and
only the conventional route contract opts into rendering. Script routes
neither pipeline can ship are hard errors (AB4808/AB4809), never silent
omissions.
Conventional src/cli/** routes compile into one collision-checked command
graph (#102 stages 2-3): the file path below the CLI root is the command
nesting (src/cli/library/audit.ts runs as <bin> library audit), the
static config export supplies description, aliases, positionals, and
the exitCode policy, and the graph feeds one framework-generated package
executable named after the plugin (dist/bin/<plugin-name>.js), replacing
the src/cli.ts convention for that project. Every command route exports
inputSchema and resultSchema zod schemas plus one default function
receiving { input, signal }, and runs inside the typed Agent request
context. A plain (.ts) command executes directly and writes one canonical
JSON line to stdout. A rendered (.tsx) command's default Server
Component renders through the runtime dispatcher against a sibling
dist/bin/<plugin-name>-flight.mjs react-server worker with four output
modes: interactive TTY updates progress in place before the final document;
piped output emits exactly one final Markdown document (no partial
fallbacks); --json emits the canonical validated final value; --ndjson
emits the sequence-numbered render-event stream (an Agent Bundle CLI/script
dialect, never MCP JSON-RPC, never written to an MCP server's stdout).
Diagnostics go to stderr; machine output owns stdout. Exit codes: 0 on
success (or the validated result's integer exitCode under
config.exitCode: 'result'), 1 on execution/render failure, 2 on usage or
input-validation failure, 130/143 after SIGINT/SIGTERM. --help, --json,
--ndjson, and --version are owned by the generated shell. An
inputSchema rejection is reported one issue per line in CLI terms,
Invalid value for <target>: expected <expectation>; received <JSON>., then
the usage line; --json writes one {"error":{"code":"CLI_INPUT_INVALID", ...}} line to stderr and --ndjson one type: "error" event (#465).
The power-tier routes.mcpCommands option projects tools from generated MCP
servers into that same command graph. Each tool becomes
<server> <tool> with one optional --input '<JSON object>' argument;
mutation-capable tools also require the enforced --yes flag. Missing or
malformed annotations.readOnlyHint is mutation-capable by default. Include
and exclude patterns use only literal text plus *; every declared pattern
must match at least one eligible <server>:<tool> identity so misspellings
fail during compilation. Projected commands invoke the same tool render and
request-context contracts as the generated MCP server, and their mcp
metadata records server, tool, and confirmation provenance in inspect.
Runtime inputSchema and resultSchema may be imported or created by a factory.
The compiler checks export presence; the generated executable loads the actual schemas.
TypeScript infers route input/output types from those exports, including defaults and transforms.
For execution-free inspection, Workbench forms, and named CLI flags, declare literal
config.inputJsonSchema. It accepts an object with additionalProperties: false, scalar
string/number/boolean properties, string enums, arrays of scalars, and optional
required, description, and default metadata. The runtime schema remains authoritative
for validation, defaults, and transforms; metadata does not replace it.
A standalone CLI route or tool projection without this metadata accepts --input JSON.
Declare input: 'json' explicitly to select that mode even when metadata exists.
Flag or positional mappings require metadata; missing tool metadata reports AB4845.
Malformed metadata or invalid standalone argv policy reports AB4814.
The compiler does not interpret Zod expressions or follow schema alias chains;
AB4838 and AB4839 are retired.
Static contracts are route-local metadata with id contract:<route-module>#inputJsonSchema.
Imported runtime schemas do not create shared static contracts.
A generated tool may also carry an opt-in CLI surface projection: a
colocated <tool>.cli.{ts,tsx} beside the tool route. The module is never
a route, RouteContract.routes does not list it, and the compiled
command's routeId stays the tool id. inspect --routes prints
cli.commands[].projection (module, mapInput, defaults?, relaxed?) and the
mapped options[] (key, option, aliases). A projection that cannot
compile has no correct partial output, so every finding is an error
(AB4843–AB4845).
An event route uses a .ts lightweight handler or a .tsx rendered handler. A .ts
handler can return ctx.render('./name.view.js', data) to load its separate .view.tsx
sibling. Data must be strict JSON. The compiler uses explicit module boundaries without
extracting closures. Invalid view modules and event helper/path mismatches report
AB4840. Definition helpers require a direct default call
with an inline object literal; computed options and wrappers report AB4810.
Providers load on first context.provider('<key>') access. Concurrent readers share the
request's promise, including failures. The same semantics apply to tools, events, and CLI
projections; no static provider subset declaration is required.
Read-only Doctor durable-state inventory (AB7316)
agent-bundle doctor inventories workspace-durable SQLite stores by directory
entry and filesystem metadata only. It never opens a database or creates
SQLite lock or shared-memory files.
Read-only Doctor operator env inventory (AB7331)
An installed pack's shells read <plugin root>/.env and .env.local at
launch (#469) to fill variables the host did not set. Doctor reports whether
those files are present and how many variables each declares, never a name
or a value, so an operator can see that a credential-configured pack is, or
is not, configured. Absent files are the normal case and produce no
diagnostic.
Read-only Doctor framework state roots (AB7332 retired)
Doctor resolves every installed MCP server's framework state root from its
canonical code root, declared environment, and execution directory. It reports
the servers, source, receipt ownership, current purgeability, existence, and
writability. A runtime location without matching receipt ownership remains
visible but is never deletion authority. In particular, a receipt with no
state block (written before the install could record ownership) cannot turn
the current environment or home into purge authority; Doctor reports that
observed root as unrecorded and retained. A state/ directory beside the
plugin is an ordinary unowned entry: it is never inventoried as durable
state, never purged, and is listed by uninstall as retained.
Read-only Doctor marketplace sources (AB7333)
Read-only Doctor Grok Bot report (AB7334)
agent-bundle doctor --host grokbot (never part of the default host set) reads
the receipts install grokbot wrote under ~/.grokbot/agent-bundle (or
$GROK_BOT_HOME/agent-bundle) and, on the Grok Bot computer, every completed
copy of the plugin in <agent-data>/plugins/cache/<marketplace>/<plugin>/<commit>/
together with the server-assigned plugin id from <agent-data>/plugin-skills/cache.json
(GROK_BOT_AGENT_DATA_DIR, then /home/box/agent-data, ~/.grokbot/agent-data,
and the macOS application-support root). It never touches the account-level
install. A plugin that contributes no skill has no skill-index row, so its id reads
as not assigned even when the cache copy exists. A cache copy install grokbot
sideloaded is not reported as an install until Grok Bot's index names it; the
sideload has its own report (AB7335).
Read-only runtime identity introspection (AB7317–AB7318)
Read-only Doctor bundle resolution (AB7306)
doctor --from <root> resolves the bundle exactly as install does, through
agent-bundle.manifest.json (install/identity.ts readBundleIdentity), and
reports the host's bundle finding as failed when that resolution throws.
The identity reader's own refusals keep their code (AB7001, below) and the
message install would print, with Doctor's recovery attached; AB7306 is
reserved for everything else.
Read-only Doctor static validation (AB7319–AB7320)
Doctor reuses the pinned, process-free host document and loader validators. These checks read installed or supplied bundle bytes only; they never invoke a host CLI, repair a bundle, or perform a live protocol exchange.
Install replacement and Doctor install comparison (AB7005, AB7307–AB7309, AB7321)
agent-bundle install <host> and the emitted standalone install.mjs share one
replace policy, and agent-bundle doctor --from <bundle-dir> reports the same
verdict read-only. Every Cursor copy an agent-bundle installer places carries an
install receipt, .agent-bundle-install.json, beside the plugin manifest:
Format 2 (#101) adds the lifecycle fields that agent-bundle uninstall
consumes: mode (local, marketplace, or host-cli), scope, the
registrations the installer performed in order, the hostDirectories it
created under the host root on the way to the plugin root (pruned by uninstall
once empty; a directory the host made is never touched), and updatedAt
(installedAt stays the first install). Host-CLI installs (Claude, Codex) and
Cursor marketplace-mode staging cannot carry a receipt inside a host-owned or
committed tree, so theirs live in an Agent Bundle-owned store,
<host root>/agent-bundle/receipts/<plugin>.<marketplace>.<scope>.json for
Claude and Codex and <plugin>.marketplace.json for Cursor staging
(~/.claude or $CLAUDE_CONFIG_DIR, ~/.codex or $CODEX_HOME,
~/.cursor), with files: []. They own no files, only the registrations and
the content hash. The host identifies a registration as <plugin>@<marketplace>,
so the same plugin installed from two marketplaces is two installs with two
receipts. A Claude project / local scope registration belongs to
the working directory the host verbs ran in (the bundle root), so those
receipts are keyed <plugin>.<marketplace>.<scope>.<12-hex digest of projectRoot>.json and
record projectRoot: two projects installing the same plugin at the same scope
are two receipts. The <host>-marketplace registration is recorded only when
the install actually created it, plugin marketplace list --json did not list
the marketplace beforehand (or the receipted install it replaces recorded it);
a marketplace that already existed, or one whose state could not be read,
is not claimed, and uninstall then retains it and says why. Between
plugin marketplace add and the receipt write those registrations exist only
in memory, so if the plugin install or the receipt write fails the install
reverses what did complete, the plugin (plugin uninstall … --keep-data /
plugin remove) when it was installed, then the marketplace when this run
created it, before rethrowing; a failed reversal is reported with the exact
host commands to run before retrying. Nothing is left registered without a
receipt to record it. Only agent-bundle-install-receipt/2 is read: a
format 1 receipt (written by #420), a current-format receipt missing any
field, and a malformed one all read as absent, and the copy they sit in is
foreign.
The receipt never participates in the content hash, and neither do empty
directories or runtime roots (state/): only regular files are plugin content,
so the artifact hash, the installed tree, and the receipt always describe the
same entries. Ownership of an existing
destination is decided as receipt (a receipt naming this plugin) or
foreign (anything else, a copy installed before receipts existed
included). Claude and Codex copies are located through the host's own
plugin list --json inventory (Doctor runs it once per host and also lists every
installed plugin from it; AB7303 is emitted only when that listing is unusable);
the host owns those copies, so replacement runs claude plugin uninstall --keep-data + install or Codex add-only (codex plugin add, preserving
plugin settings and nested MCP overrides in config.toml). A Codex replace
whose plugin list row is enabled: false or omits enabled is refused
(AB7004) before any host verb: the native plugin CLI has no qualified
settings-preserving update API, and native add would set plugin-level enabled to true.
Every AB7005, AB7308, AB7309, and AB7321 message carries the comparison
installed <name>@<version> content <hash> vs artifact <name>@<version> content <hash> (same version, different content | different version | same content).
Cursor replacement is in place and touches owned files only: stale owned files
are removed and the emptied directories the installer itself created
(directories in the receipt) are pruned, staged files are renamed over their
predecessors, and the receipt lands last. Entries the installer does not own,
a state/ directory beside the plugin included, and any directory that already
existed before the installer wrote beneath it, are never removed or rewritten;
when a rebuilt artifact introduces a path that an existing unowned entry already
occupies, replacement aborts before any change (AB7004, "Refusing to overwrite
unowned files") and names the colliding paths. Receipt file and directory lists
are validated as strict POSIX-relative paths (no backslashes, no
.././empty segments, no drive letters, nothing under a runtime root such as
state/) before they can drive a deletion; a receipt that fails validation
reads as absent, and a receipt that is not a regular file (a symbolic link, a
FIFO) is refused outright (AB7004) before it is read. The same rules apply to
the artifact itself: a file whose path could not round-trip through a receipt
(a backslash in a POSIX name, reserved characters, a trailing dot or space) is
refused (AB7004) before anything is staged. Every other failure of a local
Cursor install, a ~/.cursor that exists but cannot be inspected, an
inventory, staging, or receipt write that fails, is reported the same way, as
AB7004 with target: cursor and the underlying message; a missing
~/.cursor is AB7002. The staging directory is removed before the failure is
reported, so a refused or failed replacement never leaves a
plugins/local/.<name>.stage-* directory behind, and the installed copy is
untouched.
A Cursor destination that holds the remnant receipt described below is what
uninstall --keep-data leaves behind around retained entries: install fills
it back in as an installed (not a replacement, not a foreign refusal), and
Doctor reports it as missing with an AB7307 info instead of AB7321 or
AB7304. When uninstall retained unowned entries (a state/ directory
beside the plugin, operator files), both the inventory finding and the --from
bundle finding read the directory and the AB7307 message names those retained
entries and points at removing them by hand, since uninstall never will.
Preserved state named by AB7307 is only this home's real, non-empty
PLUGIN_DATA directory, so a remnant whose data has since been removed or
emptied is reported as exhausted, with the default uninstall that consumes
it as the recovery. Without the remnant receipt, a directory holding only
state/ proves nothing: install refuses it as foreign (AB7005), Doctor
reports the entry as corrupt (AB7304) and the destination as foreign
(AB7321), and the operator removes it by hand.
Managed uninstall (AB7007–AB7009)
agent-bundle uninstall <host> [--from <bundle-dir>] [--scope <scope>] [--mode local|marketplace] [--keep-data | --purge-data --confirm-purge] [--force] [--plan] [--json] and the emitted install.mjs --uninstall are the
receipt-owned reverse of install (#101; the maintainer's 2026-09-01 G4
deferral of mutation was reversed on 2026-09-03 with the request to fix every
open issue). Every mutation is opt-in and bounded by the receipt:
- Cursor local: removes exactly the receipt's
files, prunes itsdirectoriesand the plugin root once empty, then thehostDirectoriesthe install created (~/.cursor/plugins/local,~/.cursor/pluginsin a fresh home). Unowned entries are listed as retained and never removed, files by path, and unowned directories that hold nothing retained asname/(the prune only ever touches owned directories, so they survive too); astate/directory beside the plugin is one of them. When the plugin root survives (retained state or unowned entries), a remnant receipt,files: [],registrations: [], the carriedhostDirectories, is written there so a later purge can still prune the created directories and Doctor can explain the directory. When the receipt records a Cursor placeholder expansion (cursorExpansion, written by the emittedinstall.mjsfor an Agent Plugins pack), itsPLUGIN_DATAdirectory (~/.cursor/agent-bundle/plugin-data/<name>) is receipt-owned durable state: a written one is kept (the plugin root then survives with a remnant receipt carrying the expansion, so a later--purge-data --confirm-purgestill finds it) or purged; an empty, installer-created one is pruned together with itsagent-bundle/plugin-dataandagent-bundleparents once they empty out; a recorded path outside this home'splugin-datais never touched and thedata.detailsays so; a symlinkedagent-bundleorplugin-dataancestor is refused (AB7007) before anything is read or removed, since a recursive purge of the leaf would follow it outside the Cursor home. Doctor'sAB7307names the directory as preserved state only when it is that same real, non-empty directory. - Cursor marketplace: verifies the staged repository's
HEADagainst the commit the store receipt recorded and its working tree against that commit (git --no-optional-locks status --porcelain --untracked-files=all --ignored=matching, the same probe Doctor uses: any uncommitted, untracked, or ignored entry, or a tree that cannot be verified because git is missing orstatusfails, is refused withAB7007until--force, since the removal is recursive and those entries are not receipt-owned), then removes the repository wholesale and the receipt; a copy Cursor imported into~/.cursor/plugins/cache(recognised by the receipted commit and version, not the version the bundle may have been rebuilt to) is Cursor-owned and is reportedmanualwith the Customize step innextSteps. - Claude / Codex: reads
<host> plugin list --json(an unusable listing fails closed,AB7004), compares the cached copy with the receipt, runsclaude plugin uninstall <id> --scope <scope> --keep-data/codex plugin remove <id>, thenplugin marketplace remove <marketplace>and removes the store receipt. Becauseplugin marketplace removeapplies to every scope, the marketplace isretainedwhen the receipt does not record Agent Bundle registering it (it pre-existed the install, or there is no receipt), when another installed plugin still names it, when another store receipt (another project's scoped install) installs from it, whether that receipt records the marketplace registration or only its plugin, since a plugin installed after the marketplace existed still needs it, when the same plugin is installed at another Claude scope or in another project (live row, Claude's cross-projectplugins/installed_plugins.jsonregistry, which also records hand-madeproject/localinstalls elsewhere that have no receipt and are invisible toplugin list --jsonrun here, or stored receipt), or whenplugin marketplace list --json, the dependency re-read ofplugin list --json, that registry, or any receipt in the store cannot be read (a failed read is not proof that nothing depends on it: an unreadable receipt may be the very dependent or ownership heir, so the dependency set is unknown and a purge of shared state is refused withAB7008too). When the receipt being consumed is the one recording that Agent Bundle registered the marketplace and a dependent keeps the marketplace alive, that claim is not lost with the receipt: it moves to a dependent's store receipt (the first not already recording it, at that receipt's scope) so the last uninstall can still remove the marketplace; when every dependent is a live row with no receipt to carry it, the registrationdetailsays the marketplace now counts as user-owned. A registration the host no longer holds isalready-absent, so a receipt orphaned behind Agent Bundle's back is consumed without running any host verb.
Durable runtime state (the framework state roots the receipt's state block
records with ownership evidence, plus web-data and a recorded PLUGIN_DATA
directory) is kept by default; --keep-data says so explicitly. --purge-data
removes it only with --confirm-purge, and only the roots whose recorded
ownership is currently provable; a root the receipt lists as unowned, or a
receipt with no state block at all, retains the observed root as unproven.
A host-registered Claude/Codex copy with no store receipt is never purged:
--force --purge-data --confirm-purge is refused with AB7009 before any host
verb runs, since nothing proves the bundle owns its web-data or Claude's
plugins/data/<id>/; --force without --purge-data still uninstalls it
through the host verbs and keeps that data.
The typed data.outcome is honest per host: kept / purged / absent
(Cursor local, Agent Bundle's own doing), retained-by-host (Claude 2.1.257
orphans the cached copy for its ~14-day grace period; a purge additionally
removes the owned state roots and plugins/data/<id>/), removed-by-host
(codex-cli 0.147.0 deletes the cached tree on plugin remove), and
unavailable (Codex with no external state to preserve; a staged Cursor
marketplace holds no runtime state). --plan computes the same
report, exact absolute paths, registrations, data decision, without opening a
writer; planned directories are exactly the ones the run would prune (purged
state roots first, then every owned directory that would be left empty, and for
store receipts the <host root>/agent-bundle/receipts and
<host root>/agent-bundle directories, plus Cursor's
agent-bundle/marketplaces, once the last entry leaves them), never a
directory kept alive by retained state or unowned entries: removed in a
--plan result equals removed in the completed one. A second run after a
successful uninstall is a not-installed no-op. When --keep-data left
preserved data (a written PLUGIN_DATA directory) or retained unowned entries
behind under a Cursor local root, the remnant receipt written there stays in
place (receipt.status: 'remnant') and a rerun without --purge-data is the
same not-installed no-op for as long as that data or those entries are still
there; --purge-data --confirm-purge removes the preserved state and prunes
the root once nothing unowned remains. Once the preserved data has been removed
or emptied by hand (an empty PLUGIN_DATA directory holds no data, so it is
pruned like an installer-created directory rather than kept) and the retained
entries are gone,
the remnant guards nothing, and the next run, with or without --purge-data,
consumes it: the receipt, the empty plugin root, and the host and
plugin-data directories it recorded. Doctor reports such a remnant as
exhausted (AB7307) instead of claiming preserved state that is gone.
The Cursor and portable host-install proofs (tests/host-install-proof.test.ts,
tests/packed-host-install-proof.test.ts) snapshot the isolated home before
install and after uninstall and require them byte-identical; the Claude and
Codex proofs require zero Agent Bundle residue and classify every remaining
host-owned entry (Claude: orphaned cache copy with .orphaned_at, empty
installed_plugins.json / known_marketplaces.json, settings.json, session
bookkeeping; Codex: an empty config.toml and empty cache directories).
Read-only Doctor lifecycle receipts and activation states (AB7328–AB7330)
With --from, Doctor reports each host bundle's lifecycle as four typed
observations, placed (bytes at the host's install location), registered
(the host's registry names the plugin), enabled (the host reports it
enabled/trusted), active (loaded by a live host process), each either
observed with the host evidence that made it true or false, or unavailable
with the reason no pinned read-only surface exposes it, and a stage (the
furthest observed-true stage; absent when placement is observed false,
unknown when it is unobservable). Doctor never guesses an activation state.
Doctor also inventories the Agent Bundle receipt store under each host root
(hosts[].receipts) and cross-checks every receipt against the host, and
reports the in-tree receipt of every Cursor local copy (receipt on the
inventory finding and on the bundle finding).
Live development into hosts (AB7200–AB7202, AB7210–AB7211, AB8024–AB8025)
agent-bundle dev keeps a host's one stdio MCP process connected while it
swaps the generated plugin behind it (dev proxy), re-syncs opted-in
development installs (--install-host) on every adopted epoch, and, when a
project declares dev.contracts, gates host-facing adoption on the
development contract matrix. Every failure on that path is a structured
diagnostic; none of them silently changes what a host serves. A failing gate
is not a build failure: the epoch publishes to the Workbench playground, and
the Overview page's Host adoption section names both the published and the
host-facing build together with the failed checks.
Development rebuild compilation and publication (AB7100–AB7102)
Every agent-bundle dev rebuild compiles the project into a build attempt,
validates the artifact, proves the project source did not change underneath
it, and publishes the result as an immutable epoch. One-shot agent-bundle build and programmatic build() use the same source-input snapshot check
before accepting a compiled artifact, so compiler metadata from preparation
cannot be published against a different tree Rspack compiled. Structured
diagnostics thrown along the dev path, the AB4770 compile errors of an
MCP App view, the artifact validation codes, pass through to the failed
attempt unchanged, so the Workbench Overview and the build.failed Logs
entry show the real finding. AB7100 is only what remains: the fallback for
a throw in that pass that carried no structured diagnostics, and the code of
a cleanup failure after the attempt settled. sourcePath on all three codes
is the project's config file.
Development package build (AB7103)
agent-bundle dev rebuilds the framework-owned package build (dist/ bin
and lib outputs) inside the same serialized rebuild pass that publishes
artifact epochs. A package build failure never invalidates the artifact epoch
that already committed; it surfaces as one AB7103 warning on the
succeeded build attempt, and the package build retries on the next
invalidation. See docs/entry-conventions.md for the dev-watch contract.
Read-only Doctor Cursor hook registration and marketplace staging (AB7322–AB7324)
Cursor delivers a plugin's hooks from its .cursor-plugin/plugin.json hooks
declaration (observed 2026-09-03 on Cursor 3.18.25; see
docs/audits/2026-09-03-cursor-plugin-hooks-registration.md). Doctor proves
that registration statically and never writes ~/.cursor/hooks.json.
The installer side reuses the AB700x codes: AB7002 when git is missing
in marketplace mode, AB7003 when a mode is passed for a non-Cursor host, when
marketplace mode is requested for a bundle without .cursor-plugin/plugin.json,
or when the bundle contains nested Git metadata (.git, which git add would
record as an empty gitlink), AB7004 when a git step fails or the committed
tree does not hold the staged bundle bytes (the installer disables text,
eol, filter, ident and working-tree-encoding attributes through
.git/info/attributes, adds with core.autocrlf=false, and proves every
blob id in git ls-tree -r HEAD against the staged files; requires Git ≥ 2.29
for git init --object-format=sha1), and AB7005 for staged version or
content collisions (including a working tree that differs from committed HEAD).
Host load refusal for Claude installs (AB7006, AB7325)
claude plugin install and claude plugin validate --strict both accept a
plugin that Claude Code then refuses at load time; the refusal surfaces only
as the errors array on that plugin's row in claude plugin list --json
(Claude Code 2.1.259 shape: id, version, scope, enabled, installPath,
installedAt, lastUpdated, optional mcpServers, and errors, a nonempty
array of strings present only on a refused plugin; healthy rows omit the key,
and the refused row still reports enabled: true). A refused copy is
installed but contributes no hooks, MCP servers, or skills to a session, so
agent-bundle install claude and agent-bundle doctor --host claude read
that array instead of treating every listed row as a healthy install. The
observed instance is the manifest hooks pointer at the auto-loaded
hooks/hooks.json ("Hook load failed: Duplicate hooks file detected …
manifest.hooks should only reference additional hook files"), which the
claude projection no longer emits (#470) and which the
pinned Claude plugin schema now rejects (AB6012 at /hooks).
Disabled Claude install (AB7327)
claude plugin disable <plugin> (or the /plugin menu) keeps a plugin
installed but switched off: its row in claude plugin list --json reports
enabled: false, and none of its hooks, MCP servers, or skills reach a session
until claude plugin enable runs (Claude Code docs, "Plugins reference" →
"plugin enable" / "plugin disable"). Reinstalling, even with --replace, does
not enable it. agent-bundle doctor --host claude reads the flag: the
inventory entry carries enabled: false with state: 'disabled' (instead of
installed), and a --from comparison of that copy carries enabled: false
next to its content verdict, a disabled copy can still be current or
stale, and both facts are reported. Rows without a boolean enabled carry no
flag and are installed. A row with errors is failed (AB7325) whatever
its enabled value. A plugin that ships defaultEnabled: false in
plugin.json installs disabled by design ("Plugins reference" → "Default
enablement"); Doctor still reports AB7327 for it, because the recovery is the
same claude plugin enable.
The JSON report exposes the same facts: hosts[].inventory.findings[].errors,
hosts[].bundle.errors, and hosts[].bundle.comparison.errors. The text
report prints the comparison as installed copy: load failed (installed <version>, refused by the host: <errors>).
Read-only Doctor Cursor Agent Plugins launch proof (AB7326)
Cursor 3.18.25 loads Agent Plugins 1.0.0 packages from
~/.cursor/plugins/local/<name> but spawns their stdio servers without
expanding ${PLUGIN_ROOT} / ${PLUGIN_DATA} in args, env values, or
cwd, without providing the reserved PLUGIN_ROOT / PLUGIN_DATA variables
(spec §9.1), with an omitted cwd defaulting to the home directory, and with
plugin-relative ./ commands resolved against the workspace folder (spec
§7.2.1); see docs/audits/2026-09-03-agent-plugins-cursor-ide-proof.md. The
emitted portable install.mjs therefore rewrites mcp.json in the Cursor copy
only, absolute plugin root, ~/.cursor/agent-bundle/plugin-data/<name>
(created) for the data directory, plugin-root cwd, resolved ./ command, and
PLUGIN_ROOT / PLUGIN_DATA in every stdio server's environment, and records
the substituted values plus the pre-expansion document in the install receipt
(cursorExpansion). Doctor validates the Agent Plugins contract (AB7320)
against that recorded document and proves the expansion against the installed
bytes. Provenance is always derived: nothing here claims Cursor expands the
placeholders itself.
Built-artifact validation (AB6000–AB6018, AB6023–AB6025, AB6039)
agent-bundle build validates the staged tree before it writes the manifest
(validateArtifactFiles: filesystem entries, generated JSON documents, the
compile evidence record against the planned file table (AB6039), and
generated JavaScript modules), validates the finished artifact against its
manifest, and re-checks the validated snapshot after the staging tree is
renamed into place. agent-bundle validate --artifact <dir> runs the same
validator over a built directory, agent-bundle dev runs it over every
rebuild before publishing the epoch, and the Workbench artifact inspection
runs it over each epoch it reads (AB6200). Every code here is an error
whose recovery is fixed per code in the artifact diagnostic registry
(artifactDiagnosticRecoveries); generatedPath names the offending artifact
file (agent-bundle.manifest.json for manifest-level findings) and target
names the host target namespace when the check is per target.
agent-bundle build writes agent-bundle.compile-evidence.json at the
artifact root and lists it in agent-bundle.manifest.json as a generated
file. The record is what the compiler service reported about each file it
emitted, bound to those bytes: one assets[] entry per compiled file
(bundle kind, bin/*.mjs, scripts/*.mjs, hooks/*.mjs, mcp/*.mjs,
Flight workers, mcp-apps/*.html) holds path, sha256, the kept
externals (kind artifact-relative or builtin, externalType,
issuers, request, userRequest, and target for a sibling), and the
inlined packages. Record-level fields are policy closed-world-externals@1,
producer { name: 'agent-bundle', rspack, version }, coverage.rewritable
(true when a tools.rspack or tools.rsbuild hatch took part, so emitted
bytes may differ from the module graph), and coverage.unobserved.
agent-bundle validate --artifact re-checks a listed record against the
manifest file table without reading JavaScript (AB6039). The package build
writes a separate agent-bundle.package-compile-evidence.json record for
package-only bins and libraries; generated plugin bytes remain covered by the
artifact record. coverage.unobserved lists the load forms Rslib leaves verbatim in
the bundle, so the compiler neither bundles nor records them:
import(<expression>), require(<expression>), require.resolve(…),
createRequire(…)(…), import.meta.resolve(…). No externals recorded
therefore does not prove the absence of such a load.
Workbench artifact inspection (AB6200–AB6202)
The Workbench artifact pages (/api/artifacts/epochs/<id>,
/api/artifacts/diff) read published epochs through the artifact inspection
service. Each refusal is an ArtifactInspectionServiceError whose
diagnostics carry one of these error codes; the route reports the
service failure as AB8064, AB8065, or AB8066 and forwards the
diagnostics in the response body.
Host installation, uninstallation, and project preparation (AB7000–AB7004)
The AB7000–AB7004 codes are shared by two families of emitters with
distinct meanings. agent-bundle install and agent-bundle uninstall
(install/install.ts, install/uninstall.ts, install/cursor-marketplace.ts)
throw them as DiagnosticErrors with target set to the host; the
public install command wraps any non-diagnostic failure as AB7004. The
development project service (dev/project-service.ts) and inspectProject
emit them as error diagnostics on a failed preparation with sourcePath
set to the config file and a fixed recovery ending in "then inspect again".
The install-time collision (AB7005) and load-refusal (AB7006) codes, and
the uninstall refusals AB7007–AB7009, have their own sections above.
Development server (AB80xx)
agent-bundle dev serves the Workbench and its JSON API from one loopback
foreground server (dev/foreground-server.ts) whose route modules under
dev/** share the helpers in dev/http.ts. Every refusal is a request
diagnostic, { code, message, status }, written as the JSON body
{ "diagnostic": { "code", "message" } } with the listed HTTP status; a
thrown request diagnostic anywhere in a handler is written the same way. The
codes are fixed per route module, so the same code covers every message the
module emits with it. The Workbench browser clients reuse the owning route's
"could not be completed" code (AB8019, AB8033, AB8043, AB8063,
AB8073, AB8083, AB8093, AB8123) for a response they cannot decode or an
HTTP failure that carried no diagnostic body, AB8015–AB8017 for a request
the browser refuses to send, and AB8003 when the page origin is not one the
foreground server accepts.
Server configuration and shared transport
MCP sessions (/api/mcp/sessions/**)
MCP App previews (/api/mcp/apps/**, /api/mcp/sessions/<id>/apps)
Hook playground (/api/hooks/**)
Prompt playground (/api/playground/**)
Artifact epochs (/api/artifacts/**)
Evals (/api/evals/**)
Development logs (/api/logs/replay, /api/logs/stream)
Route manifest (/api/routes/manifest)
Eval refusals through the API and CLI (AB9001–AB9005, AB9007–AB9011)
runEvals and compareEvals (and agent-bundle eval / eval compare on top
of them) map every EvalServiceError to one error diagnostic whose
message is the service's own and whose recovery is fixed per code, thrown
as a DiagnosticError. The dev-server eval routes report the same service
conditions under their own AB807x/AB808x codes (see "Development server").