Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/555-composite-plugin-root.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"agent-bundle": minor
---

Emit one composite plugin root from `agent-bundle build`: every selected target is projected into the output directory itself instead of one `<output>/<target>/` directory per target, and `plugin` is no longer a target (`targets: ['plugin']` / `--target plugin` report `AB4100`). Claude Code owns the conventional `hooks/hooks.json` and `.mcp.json`; Codex beside it reads `.codex-plugin/hooks.json` and `.codex-plugin/mcp.json` through manifest pointers; Cursor beside another host reads `hooks/hooks-cursor.json`; the portable Agent Plugins pack beside other hosts is the root's `portable/` view with `portable/mcp/` shims onto the shared servers; a hook selecting Claude Code and Codex compiles to one host-detecting wrapper. Skills shared by several hosts must select all of them (`AB4104`), two projections writing different bytes to one path fail the build (`AB4105`), a target set that mixes an advanced adapter with a built-in host cannot share a root (`AB4106`), and `validate` and `inspect` judge the same root the build does. Codex's `agents/openai.yaml` skill sidecar is still written beside the shared `SKILL.md`. The artifact manifest lists the real hosts with root-relative paths, the hook index lists one entry per hook and host, `INSTALL.md` carries a section per selected host, `install`/`doctor`/`validate --artifact --host-validation` read the root and probe nothing nested under it, and `TargetRegistry.root(targets)` exposes the root's contracts to adapters and tooling (#569)
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import { defineConfig } from 'agent-bundle/config';

export default defineConfig({
plugin: { name: 'my-plugin', description: 'What it does.' }, // version comes from package.json
targets: ['plugin'],
targets: ['claude', 'codex', 'cursor'],
skills: ['src/skills/*'],
hooks: { sessionStart: { handler: './src/session-start.ts' } },
mcp: { servers: { tools: { entry: './src/mcp.ts' } } },
Expand All @@ -51,15 +51,15 @@ npx agent-bundle build --root . --output dist # write installable artifacts
npx agent-bundle dev --root . # local workbench with live rebuilds
```

`targets: ['plugin']` emits one multi-host bundle at `dist/plugin/`: `.claude-plugin/`, `.codex-plugin/`, and `.cursor-plugin/` manifests over shared `skills/`, `hooks/`, `mcp/`, and `scripts/` directories. The bundle's generated `AGENTS.md` explains how to install it into each host. Per-host layouts are available as the `claude`, `codex`, `cursor`, and `portable` targets.
Every build emits one plugin root — here `dist/` — that every selected target installs from. `targets: ['claude', 'codex', 'cursor']` places the `.claude-plugin/`, `.codex-plugin/`, and `.cursor-plugin/` manifests at that root over shared `skills/`, `hooks/`, `mcp/`, and `scripts/` directories (Codex reads its hooks and MCP documents under `.codex-plugin/`, Cursor its hooks from `hooks/hooks-cursor.json`, so nothing collides); a multi-host root also carries a generated `AGENTS.md` explaining how to install it into each host. The public targets are `claude`, `codex`, `cursor`, and `portable`; beside other hosts the portable Agent Plugins pack is the root's `portable/` directory.

The `portable` target is the [Agent Plugins open standard](https://agent-plugins.org/specification) (specification 1.0.0) adapter — the default target, and the layout Cursor, Codex, VS Code, GitHub Copilot, Kiro, and ChatGPT load natively (Claude Code consumes it only through CLI translation). It emits the closed root `plugin.json` (canonical `$schema`, `name`, `version`, `description`, plus `author`, `homepage`, `repository`, `license`, `keywords`, and reverse-domain `extensions` authored under the `portable` config key), `skills/<name>/SKILL.md`, and `mcp.json` with stdio and Streamable HTTP servers whose `args`, `env` values, and `cwd` use the standard's `${PLUGIN_ROOT}`/`${PLUGIN_DATA}` placeholders. Rules, commands, hooks, marketplaces, and client extension directories are honestly unavailable there because the v1 standard packages only skills and MCP servers. Both documents are validated against the vendored, hash-pinned 1.0.0 schemas and the normative text at plan time (`portable.mcp.*.standard`), after every ordinary build and `validate --artifact` (`AB6011`/`AB6012` plus the Agent Plugins byte lane `AB6035`–`AB6037`), under `validate --artifact --host-validation` (same lane with the `AB6038` provenance note), and by `agent-bundle doctor` for installed Cursor local plugins that declare the standard's `$schema` (`AB7320`); see [Diagnostics](docs/diagnostics.md#agent-plugins-portable-validation-ab6035ab6038). Pins live in `packages/agent-bundle/src/adapters/schemas/portable/PROVENANCE.json`; the capability table `packages/agent-bundle/src/adapters/capabilities/portable-1.0.0.json` carries a dated row for every standard feature.

Claude Code language servers are declared under `claude.lspServers`; the `claude` target and the Claude half of `plugin` emit the record as plugin-root `.lsp.json`. Agent Bundle expands path tokens only in `command`, `args`, `env`, and `workspaceFolder`, and it does not include the language-server binary — install that separately so the declared command is available on `PATH`. Codex, Cursor, and the portable format do not currently receive this host-scoped configuration.
Claude Code language servers are declared under `claude.lspServers`; the `claude` target emits the record as plugin-root `.lsp.json`. Agent Bundle expands path tokens only in `command`, `args`, `env`, and `workspaceFolder`, and it does not include the language-server binary — install that separately so the declared command is available on `PATH`. Codex, Cursor, and the portable format do not currently receive this host-scoped configuration.

Claude Code plugin defaults are declared under `claude.settings` and emitted as plugin-root `settings.json`, which Claude Code applies when the plugin is enabled. The pinned contract supports only `agent` and `subagentStatusLine`; Agent Bundle rejects any other key rather than shipping a default Claude Code would silently ignore, and it expands no path tokens here because `settings.json` is absent from the host's placeholder-substitution table. Because the plugin `agents/` component is still deferred, declaring `agent` also raises a warning: the referenced agent has to reach the plugin root some other way, such as a prebuilt payload.

Cursor Plugin manifest metadata is declared under `cursor.*` and emitted verbatim into `.cursor-plugin/plugin.json` by the `cursor` target and the Cursor half of `plugin`: `author` (`name`, optional `email`), `homepage`, `repository`, `license`, `keywords`, plus the schema-admitted `publisher`, `category`, `tags`, and `minClientVersions` (for example `{ cursor: '3.13.0' }`). Every field is validated against the pinned `cursor/plugins` manifest schema before emission — `author.url`, non-HTTP URLs, empty strings, and loose semver are rejected with `cursor.manifest.*` errors, and an invalid block emits no partial metadata. The Cursor artifact never mixes Agent Plugin (`plugin.json`, `${PLUGIN_ROOT}`) paths or tokens into the Cursor Plugin format; the portable target owns that format. The full documented-surface contract matrix (every Cursor hook event, cloud availability, hook options, marketplace and team-distribution surfaces, canvases, agents) lives as dated `supported` / `unavailable` rows in `packages/agent-bundle/src/adapters/capabilities/cursor-2026-08-28.json`.
Cursor Plugin manifest metadata is declared under `cursor.*` and emitted verbatim into `.cursor-plugin/plugin.json` by the `cursor` target: `author` (`name`, optional `email`), `homepage`, `repository`, `license`, `keywords`, plus the schema-admitted `publisher`, `category`, `tags`, and `minClientVersions` (for example `{ cursor: '3.13.0' }`). Every field is validated against the pinned `cursor/plugins` manifest schema before emission — `author.url`, non-HTTP URLs, empty strings, and loose semver are rejected with `cursor.manifest.*` errors, and an invalid block emits no partial metadata. The Cursor artifact never mixes Agent Plugin (`plugin.json`, `${PLUGIN_ROOT}`) paths or tokens into the Cursor Plugin format; the portable target owns that format. The full documented-surface contract matrix (every Cursor hook event, cloud availability, hook options, marketplace and team-distribution surfaces, canvases, agents) lives as dated `supported` / `unavailable` rows in `packages/agent-bundle/src/adapters/capabilities/cursor-2026-08-28.json`.

The same config also owns the npm package build — no second bundler config, bin shims, or hand-rolled stdio lifecycles. `bin` and `lib` entries (or the conventions `src/cli.ts`, `src/index.ts`, and `src/mcp/<server-id>.ts`) emit executable `dist/bin/<name>.js` bundles and a library output alongside the host artifacts; an MCP entry that default-exports a server factory runs under a framework-owned stdio lifecycle; `tools.rsbuild` / `tools.rspack` is the one bundler escape hatch. [Entry conventions](docs/entry-conventions.md) is the full contract, and [Framework mode](docs/framework-mode.md) is the whole authoring model on one screen: structure in config and conventions (`src/skills/<name>/SKILL.md` ships with no declaration at all), JSX only where something is rendered.

Expand All @@ -73,12 +73,12 @@ The same config also owns the npm package build — no second bundler config, bi
- `hooks list` / `hooks simulate` — inspect and simulate generated hooks
- `eval` — run eval suites against a built artifact

When validating a built `claude` or unified `plugin` target, Agent Bundle uses the installed
When validating a built root that projects `claude`, Agent Bundle uses the installed
Claude Code developer toolchain in addition to its pinned schemas. Use
`agent-bundle validate --artifact dist --strict` in CI; Claude's `--strict` findings remain
warnings locally unless Agent Bundle strict mode is requested. If `claude` is absent, validation
reports an explicit informational skip. For the install-free development loop, run
`claude --plugin-dir dist/claude plugin list --json` after building.
`claude --plugin-dir dist plugin list --json` after building.

The [package README](packages/agent-bundle/README.md) is the full reference: configuration semantics, the workbench, the optional Agent API, evals, and limitations.

Expand Down
33 changes: 29 additions & 4 deletions docs/diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ even when no error diagnostic was reported.
| --- | --- |
| `AB30xx` | Skill documents: Markdown parsing (`AB3000`–`AB3002`: unreadable, missing or malformed frontmatter) and rendered-skill compilation (`AB3003`: module failed to load, `AB3004`: missing/invalid default component or `frontmatter` export, `AB3005`: content outside the supported Markdown element subset). |
| `AB40xx` | Plugin metadata and Skill source validation (`AB4000`/`AB4001`: name/version; `AB4002`–`AB4007`: Skill fields; `AB4008`–`AB4011` and `AB4013`: release identity, see below; `AB4012`: declared `plugin.logo` is missing, not a file, or outside the project). |
| `AB41xx` | Normalized model invariants (unknown targets, duplicate IDs and outputs). |
| `AB41xx` | Normalized model invariants (`AB4100`: unknown target — `plugin` is no longer a target name; duplicate IDs and outputs) and composite plugin root composition (`AB4104`: a declaration selects some but not all of the hosts that share one root directory; `AB4105`: two host projections emitted different bytes for one root path; `AB4106`: a target set no root can hold — an advanced adapter beside a built-in host; see below). |
| `AB42xx` | Hook configuration and native hook sources. |
| `AB43xx` | MCP server and MCP App configuration (`AB4340`: a declaration for a route-generated server redeclares `entry`/`command`/`url`; see below). |
| `AB44xx` | Script configuration. |
Expand Down Expand Up @@ -50,7 +50,7 @@ even when no error diagnostic was reported.
## Claude Code host validation (`AB6019`–`AB6022`, `AB7311`, `AB7325`)

`agent-bundle validate --artifact <dir>` and `agent-bundle build` run the
installed Claude Code validator for the `claude` and `plugin` targets when
installed Claude Code validator over a built root that projects `claude` when
`--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
Expand Down Expand Up @@ -87,7 +87,7 @@ 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`/`plugin` targets `unavailable` without spawning again.
`claude` roots `unavailable` without spawning again.

| Code | Severity | Meaning | Recovery |
| --- | --- | --- | --- |
Expand Down Expand Up @@ -512,6 +512,31 @@ that claims the same path. See “The routed CLI shell” in
| `AB4765` | warning | The project has a routed CLI but a selected target's adapter publishes no supported `cli` capability, so that artifact ships no `bin/<name>.mjs`. Skills, hooks, and scripts in that artifact cannot invoke the routed CLI. `inspect` lists the same omission as an `unsupported-capability` skip of the `cli` component. Publish the capability (with a `cliBin` artifact layout) on the adapter, or keep references to the bin out of that target's surfaces. |
| `AB4766` | error (build) | A target plan already emits `bin/<name>.mjs` or `bin/<name>-flight.mjs` (for example a Claude `claude.bin` directory shipping a file of that name), compared case-insensitively because those are one file on macOS and Windows. The routed CLI owns those paths, so the build refuses instead of choosing. Rename or remove the host-emitted file, or set `bin: false` to keep it and drop the routed CLI executable. |

## Composite plugin root composition (`AB4104`–`AB4106`)

`agent-bundle build` emits one plugin root for every selected target (#555):
host manifests in their own directories over shared `skills/`, `scripts/`,
`mcp/`, `bin/`, and `assets/` content, with the portable (Agent Plugins)
projection namespaced under `portable/` beside other hosts. Where two hosts
would read one conventional document the composition relocates the one a host
reads through an explicit manifest pointer (Codex beside Claude Code reads
`.codex-plugin/hooks.json` and `.codex-plugin/mcp.json`; Cursor beside another
host reads `hooks/hooks-cursor.json`). Where it cannot isolate a declaration
it refuses instead of widening the declaration's host scope. `AB4104` and
`AB4105` come from the assembler for a root that projects two or more hosts,
name no `target` because the whole root is at issue, and are never emitted for
a single-host root; `validate` and `inspect` plan the same root the build
does, so they report them too. `AB4106` is judged on the normalized model,
before any planning: only the built-in hosts (`claude`, `codex`, `cursor`,
`portable`) know how to share a root, so an advanced registry's own adapter is
built alone, one target per `--output`.

| Code | Severity | Trigger |
| --- | --- | --- |
| `AB4104` | error | A declaration whose emitted file every projected host discovers conventionally selects some but not all of those hosts, so one root cannot hide it from the rest: a skill selects a strict subset of the hosts that share the root's `skills/` directory (the `portable/` view carries its own copy, so only the hosts reading the root's `skills/` must agree), or a command selects `cursor` in a root that also projects `claude` (`commands/` carries Claude Code frontmatter there, and Cursor reads plain Markdown prompts from the same directory). Select every projected host (or none of them) on the declaration, or build the hosts that differ into a separate `--output`. |
| `AB4105` | error | Two host projections of the root emitted different bytes for the same root-relative path — a generated document, a copied file, or a hook wrapper. Identical bytes merge silently and pool their source inputs; differing bytes are refused rather than letting one host overwrite another's file. Build the hosts that differ into a separate `--output`. |
| `AB4106` | error | The project selects two or more targets and one of them is not a built-in host — an adapter registered on an advanced `TargetRegistry` — so no plugin root can project the whole set. Named on that target, with its config provenance. Build the adapter's target alone into its own `--output`, and the built-in hosts into another. |

## Config beside a route-generated MCP server (`AB4340`)

A `mcp.servers.<id>` block for a server the route graph compiles in
Expand Down Expand Up @@ -1219,7 +1244,7 @@ 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` and unified `plugin` targets no longer emit (#470) and which the
`claude` projection no longer emits (#470) and which the
pinned Claude `plugin` schema now rejects (`AB6012` at `/hooks`).

| Code | Severity | Trigger | Recovery |
Expand Down
10 changes: 5 additions & 5 deletions docs/entry-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,9 @@ Each cross-request notice route is selected from the target host's pinned
`TargetRegistry.noticeDelivery(target)` (a local `NoticeDeliveryAdvertisement`
shape, structurally identical to the runtime's so it types for
`selectNoticeDeliveryRoutes` without making the optional `@agent-bundle/runtime`
peer a declaration dependency); the unified `plugin` target advertises the
intersection of its three hosts, and a target with no advertisement wires no
cross-request route. The `agent-bundle://notices/inbox` resource is registered
peer a declaration dependency); a root projecting several hosts wires only the
routes every projected host advertises (the intersection), and a target with
no advertisement wires no cross-request route. The `agent-bundle://notices/inbox` resource is registered
in the server and mounted in its worker only for stateful projects whose host
advertises `mcp-inbox` (the worker still mounts the ledger so routes can
publish; only the unadvertised read surface is withheld, and the reserved name
Expand Down Expand Up @@ -1293,8 +1293,8 @@ functions the build uses, so the dump cannot drift from what compiles.

Nothing is redacted (this is a local debugging surface), but two build-time
values are replaced with stable tokens so output is deterministic for one
project: the artifact output root (chosen per build) appears as
`<output>/<target>`, and the synthesized declaration tsconfig (a temporary
project: the artifact output root (chosen per build) appears as `<output>` —
one plugin root for every selected target (#555) — and the synthesized declaration tsconfig (a temporary
file generated per package build) appears as `<generated-dts-tsconfig>`. The
package build's output root appears as its published destination, `dist`,
although each real build stages outputs before publishing them atomically.
Expand Down
Loading
Loading