Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
14fd01c
feat(targets): remove the plugin target from the public target model …
ScriptedAlchemy Sep 4, 2026
27625cb
feat(build): plan the selected projections into one composite root (#…
ScriptedAlchemy Sep 5, 2026
c399633
Merge origin/main into feat/555-composite-root-w1
ScriptedAlchemy Sep 5, 2026
5d59c40
Merge origin/main (#583) into feat/555-composite-root-w1
ScriptedAlchemy Sep 5, 2026
e10164a
feat(build): AB4106 for mixed advanced-registry selections; validate/…
ScriptedAlchemy Sep 5, 2026
4dfeb01
Merge origin/main into feat/555-composite-root-w1
ScriptedAlchemy Sep 5, 2026
c7612b5
test(packed): read the composite root's bin/ and single mcp/ entry in…
ScriptedAlchemy Sep 5, 2026
f171840
Merge origin/main (#580) into feat/555-composite-root-w1
ScriptedAlchemy Sep 5, 2026
e550073
fix(inspect): describe the same event allowed-targets set the build b…
ScriptedAlchemy Sep 5, 2026
91a99af
Merge origin/main (#585) into feat/555-composite-root-w1
ScriptedAlchemy Sep 5, 2026
06e2272
build: carry #585's MCP App compile diagnostics through the composite…
ScriptedAlchemy Sep 5, 2026
a36c3d6
test(cli): MCP App compile report attributes the view to the composit…
ScriptedAlchemy Sep 5, 2026
4424053
Merge remote-tracking branch 'origin/main' into feat/555-composite-ro…
ScriptedAlchemy Sep 5, 2026
4eef2ff
build: identify the event runtime by the artifact alone; port #569's …
ScriptedAlchemy Sep 5, 2026
2b32f26
Merge remote-tracking branch 'origin/main' into feat/555-composite-ro…
ScriptedAlchemy Sep 5, 2026
6250a1a
test(prepack): #588's prebuilt payload lands in the composite root (h…
ScriptedAlchemy Sep 5, 2026
f9f61af
changeset: the event runtime endpoint is the artifact's alone (#592 b…
ScriptedAlchemy Sep 5, 2026
df35c13
review: lineage fallback is the one host whose MCP document lists the…
ScriptedAlchemy Sep 5, 2026
2d49a9b
build: host the composite root's event runtime per selected host's fi…
ScriptedAlchemy Sep 5, 2026
68a154e
test(browser): mount each MCP App as one host of the composite select…
ScriptedAlchemy Sep 5, 2026
2c2fb98
Merge remote-tracking branch 'origin/main' into w1-merge
ScriptedAlchemy Sep 5, 2026
021a164
docs: fold #599's per-code diagnostics rows into the composite root —…
ScriptedAlchemy Sep 5, 2026
6558432
review: judge built-in hosts by adapter identity in AB4106 and --host…
ScriptedAlchemy Sep 5, 2026
511464d
docs: AB4106 judges the shipped adapters by identity
ScriptedAlchemy Sep 5, 2026
323dc9b
Merge branch 'main' into feat/555-composite-root-w1
ScriptedAlchemy Sep 5, 2026
064a74c
test: exercise host validation by adapter identity through validate -…
ScriptedAlchemy Sep 5, 2026
1b770a4
test: validate --artifact takes the project root too
ScriptedAlchemy Sep 5, 2026
ac2e92c
test: a custom adapter under the portable name owes no install surfac…
ScriptedAlchemy Sep 5, 2026
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
6 changes: 6 additions & 0 deletions .changeset/555-composite-plugin-root.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"agent-bundle": minor
"create-agent-bundle": minor
---

Emit one composite plugin root: `agent-bundle build` writes a single directory at the artifact output and `targets` (`claude`, `codex`, `cursor`, `portable`; default `portable`) selects which host projections it carries, so there is no `artifact/<host>` partition — host manifests sit in their dotfolders at the root (`.claude-plugin/`, `.codex-plugin/`, `.cursor-plugin/`, `plugin.json`), Codex and Cursor hook/MCP documents move beside their manifests (`.codex-plugin/hooks.json`, `.codex-plugin/mcp.json`, `.cursor-plugin/hooks.json`, `.cursor-plugin/mcp.json`; `AB6027`, `AB6032`, and `AB7320` name those paths), and `skills/`, `hooks/`, `mcp/`, `scripts/`, `bin/`, and `INSTALL.md` are emitted once, with `install.mjs` beside them whenever `cursor` or `portable` is selected. A hook shared by several selected hosts compiles to `hooks/<name>.<host>.mjs` per host, each wrapper baking its own host, so generated hook wrappers no longer read `AGENT_BUNDLE_HOOK_HOST`; compiled MCP entries, scripts, and CLI bins are attributed to the sorted composite identity (`claude+codex`), and reordering `targets` yields byte-identical output. Remove the `plugin` target: `targets: ['plugin']` and `--target plugin` fail with `AB4100`, the generated `AGENTS.md` is gone, `create-agent-bundle --target` accepts only the four hosts, and the advanced-registry hooks `TargetAdapter.lowersConfigExtensions`, `TargetRegistry.lowersConfigExtension()`, and `NormalizationTargetRegistry.lowersConfigExtension()` are removed. Two selected projections planning one path with different bytes fail with `AB4103`; a command or rule scoped to a subset of the selected hosts that another selected host discovers conventionally (`commands/`, `rules/`) fails with `AB4105`; a selection that mixes an adapter registered on an advanced `TargetRegistry` with any other target fails with `AB4106` — `validate`, `inspect`, and `build` report all three on the same composite root. `agent-bundle install <host> --from <root>` and `doctor --from <root>` read the host manifest directly under the root (no `<root>/<host>` lookup; `AB7001` when it is absent), `mcp run`, `serve-app`, and `dev proxy --target <host>` resolve that host's MCP document in the same root, the `agent-bundle/test` `openInstalledHostMcpServer` harness reads the composite root as every host's bundle root, the warm event runtime's endpoint is identified by the artifact alone (its epoch and root directory, never the selection) with the invoking host carried on each hook request, each selected host reaching it through the first generated MCP server its own MCP document lists, the composite root's `INSTALL.md`/`install.mjs`, the `AB4106` refusal, and `--host-validation` judge the shipped host adapters by identity (an advanced `TargetRegistry` adapter named like a built-in host earns no install surface and is held to no host validator; `TargetRegistry.builtInHost()`/`builtInHosts()` expose the judgment), and `agentBundleBrowserRstest()` compiles every MCP App once for the project's whole selection while its `target` option names the host each app mounts as (default: the app's first declared target the project selects), `inspect --bundler` reports `distPath.root` as `<output>` with root-relative output paths and the composite identity as `target`, and the dev server, Workbench, and eval harnesses stage the composite root as one epoch (#578)
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ coverage/
# Playwright traces @rstest/playwright keeps for failed browser tests on CI (packages/workbench/tests/support/workbench-e2e.ts)
.rstest/
artifacts/
!packages/*/src/**/artifacts/
*.log
# Every example's `pnpm build` (and `pnpm examples:check`) writes here.
examples/*/artifact/
Expand Down
28 changes: 14 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# agent-bundle

agent-bundle compiles an agent plugin — skills, hooks, MCP servers, and scripts, described by one typed config — into installable artifacts for Claude Code, Codex, and Cursor, plus a portable layout. You write the plugin once; the compiler emits each host's manifests and wrappers.
agent-bundle compiles an agent plugin — skills, hooks, MCP servers, and scripts, described by one typed config — into one installable plugin root that Claude Code, Codex, Cursor, and portable Agent Plugins clients load. You write the plugin once; the compiler emits each selected host's manifests and wrappers into that root.

Documentation: [scriptedalchemy.github.io/agent-bundle](https://scriptedalchemy.github.io/agent-bundle/) (English and 简体中文; guide, reference, generated type API, host capability matrices).

Expand Down 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 @@ -47,38 +47,38 @@ export default defineConfig({
Then build, or work interactively:

```sh
npx agent-bundle build --root . --output dist # write installable artifacts
npx agent-bundle build --root . # write the plugin root to artifact/
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.
`agent-bundle build` writes one composite plugin root (`artifact/` by default; `--output` or `output.distPath` relocates it), and `targets` selects which host projections it carries: the `.claude-plugin/`, `.codex-plugin/`, and `.cursor-plugin/` manifests and the portable `plugin.json` sit at the root over shared `skills/`, `hooks/`, `mcp/`, and `scripts/` directories, emitted once. Every selected host installs from that same directory, and the generated `INSTALL.md` explains how. Omitting `targets` emits only the `portable` projection.

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.
The `portable` target is the [Agent Plugins open standard](https://agent-plugins.org/specification) (specification 1.0.0) adapter — the default projection, 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` projection 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` projection: `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 Plugin documents never mix Agent Plugin (`plugin.json`, `${PLUGIN_ROOT}`) paths or tokens into the Cursor Plugin format; the portable projection owns that format, even when both share one root. 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.
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 plugin root; 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.

## Commands

- `build` — validate the project and write an artifact (plus the `bin`/`lib` package build when declared)
- `build` — validate the project and write the plugin root (plus the `bin`/`lib` package build when declared)
- `validate` — check project source, or a built artifact with `--artifact <dir>`
- `inspect` — show the normalized configuration and per-target plans; `--bundler` dumps the synthesized bundler configs (post-`tools`-hatch merge)
- `inspect` — show the normalized configuration and each selected host's projection plan; `--bundler` dumps the synthesized bundler configs (post-`tools`-hatch merge)
- `dev` — serve the local development workbench and rebuild the `dist/` package build when its inputs change; `--install-host <claude|codex|cursor>` installs a development variant whose stable `dev proxy` MCP command hot-swaps epochs behind the host's open connection and re-syncs hooks and Skills on every adopted rebuild (see [Framework mode › Live development into hosts](docs/framework-mode.md#live-development-into-hosts))
- `mcp list` / `mcp invoke` / `mcp run` — list, invoke, or run an artifact's MCP servers locally
- `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
Claude Code developer toolchain in addition to its pinned schemas. Use
`agent-bundle validate --artifact dist --strict` in CI; Claude's `--strict` findings remain
When the `claude` projection is selected, validating the built root also runs the installed
Claude Code developer toolchain in addition to the pinned schemas. Use
`agent-bundle validate --artifact artifact --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 artifact 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
8 changes: 4 additions & 4 deletions docs/canvases/agent-bundle-walkthrough.canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ export default async function AfterFileEdit({ canonical, native, signal }: Agent
);
}`;

const CLAUDE_HOOKS_JSON = `// claude/hooks/hooks.json - real emitted bytes (examples/hooks-and-scripts)
const CLAUDE_HOOKS_JSON = `// hooks/hooks.json - real emitted bytes (examples/hooks-and-scripts)
{
"hooks": {
"SessionStart": [
Expand Down Expand Up @@ -935,9 +935,9 @@ ctx.actor.state === 'available'
</Text>
<MonoBlock
dim
text={`agent-bundle install claude --from artifact/claude --scope user
agent-bundle install codex --from artifact/codex
agent-bundle install cursor --from artifact/cursor # staged copy`}
text={`agent-bundle install claude --from artifact --scope user
agent-bundle install codex --from artifact
agent-bundle install cursor --from artifact # staged copy`}
/>
<Text size="small" tone="secondary">
Every target directory is independently distributable with a generated INSTALL.md;
Expand Down
Loading
Loading