Skip to content

Emit one composite plugin artifact; use targets to select the host projections inside it #555

Description

@ScriptedAlchemy

Decision

Agent Bundle should always emit one composite plugin artifact. plugin should not be a public target. The public targets remain claude, codex, cursor, and portable; selecting them controls which host projections are present inside the composite root.

The same directory should be publishable through Git, npm, or used locally without changing its internal layout. Distribution is a separate axis from host projection selection.

For example:

targets: ['claude', 'codex']

emits one root containing the Claude and Codex conventions plus shared implementation files. It must not emit sibling claude/, codex/, or plugin/ packs.

Adding cursor adds the Cursor projection and the Cursor lifecycle helper. Adding portable adds the Agent Plugins projection inside the same distributable, subject to the compatibility constraint below.

If targets is omitted, keep the current default inclusion policy (portable) unless that default is changed separately. The output shape is still one composite root.

Artifact contract

The publishable root contains the files required by the selected hosts, alongside shared compiled content:

package.json                         # when published to npm
.claude-plugin/plugin.json          # when claude is selected
.claude-plugin/marketplace.json
.codex-plugin/plugin.json           # when codex is selected
.agents/plugins/marketplace.json
.cursor-plugin/plugin.json          # when cursor is selected
.cursor-plugin/marketplace.json
plugin.json                         # portable projection, when compatible
.mcp.json / mcp.json / host variants
hooks/
skills/
scripts/
bin/
mcp/
mcp-apps/
assets/
INSTALL.md
install.mjs                         # only when a selected host needs it
agent-bundle.manifest.json

Shared byte-identical scripts, MCP servers, apps, assets, and skills are emitted once. Host manifests point to those shared files. A host-specific document or wrapper is emitted only when that host requires a different representation.

Target-scoped declarations retain their current meaning. A Claude-only component must not become visible to Codex merely because both projections share one filesystem root. Where a conventionally discovered directory cannot faithfully isolate a host-specific component, the builder must either emit a host-specific view supported by that host or fail with an explicit diagnostic. It must not silently widen the component's target scope.

The artifact manifest records the selected real host projections and their provenance. It does not record plugin as a target or use target names as physical output partitions.

Current implementation

The current code has most of the format-specific emitters, but the planner still treats output format and host selection as the same axis:

  • config/normalize.ts selects registered target names and synthesizes a plugin lowering target.
  • adapters/registry.ts registers pluginAdapter as an ordinary selectable target; portable is the current default.
  • build/build.ts plans each selected adapter independently and stages it at <artifactRoot>/<target.name>.
  • adapters/plugin.ts is an always-Claude-plus-Codex-plus-Cursor adapter. It filters resources through the synthetic plugin target, always invokes those host planners, and has static validation requirements for Claude and Codex.
  • api.ts, package inventory, artifact validation, dev/workbench services, eval harnesses, and test helpers assume that a host is found under <output>/<target>.
  • build/package-build.ts currently creates a sibling npm package whose installer reaches back to a nested artifact. The npm package root is not itself the plugin root.
  • install/install.ts probes the supplied root, then <root>/<host>, then <root>/plugin; install/doctor.ts probes the root and <root>/<host>. The composite root should become canonical in both.

This is not a default-target change. It requires separating two concepts:

  1. Projection selection: claude | codex | cursor | portable.
  2. Artifact assembly and distribution: one composite directory, optionally packed to npm or committed/published through Git.

Required changes

1. Remove plugin from the public target model

  • Reject targets: ['plugin'] and --target plugin as unknown.
  • Remove the synthetic plugin target from normalized components, host documents, manifests, hook indexes, diagnostics, and user-facing target lists.
  • Keep the existing target precedence and default inclusion behavior for real host projections.
  • The current pluginAdapter may remain as an internal composite assembler, but it must not be registered as a public target.

2. Plan selected projections into one stage

  • Replace independent <artifactRoot>/<target> stages with one stage rooted at artifactRoot.
  • Invoke only the planners for selected hosts.
  • Include a declaration when its target set intersects the selected projections, while preserving per-host scoping in emitted manifests.
  • Merge byte-identical entries once. Keep deterministic, fatal collision diagnostics for incompatible same-path bytes.
  • Compute capability and notice-delivery diagnostics over the selected projections rather than a fixed Claude/Codex/Cursor set.
  • Make artifact validation conditional on the projections actually selected.
  • Preserve native hooks for every selected host; the current composite path must not strip them through a hook-free intermediate model.

3. Resolve the portable/Codex/Cursor root collision

The portable adapter requires a root plugin.json and may emit root mcp.json. The current composite adapter intentionally omits root plugin.json because Codex can prefer it over .codex-plugin/plugin.json, which loses Codex hooks/apps and changes MCP lookup. Cursor also uses root MCP conventions.

portable + codex and portable + cursor therefore cannot be implemented as an unchecked file merge. Define and prove one of:

  • a namespaced portable view with install/materialization support while keeping one distributable root;
  • a merged representation proven to preserve all selected host features; or
  • a clear build-time incompatibility diagnostic for combinations that cannot yet be represented faithfully.

The intended end state is additive portable support, but the implementation must not claim that support by degrading another selected host.

4. Make the root distribution-neutral

The exact composite root should work in each of these forms:

  • Git/GitHub: committed at a repository root, artifact branch, release repository, or a documented subdirectory referenced by a root marketplace catalog.
  • npm: packed with package.json, host manifests, generated runtime files, and all referenced assets at the npm package root; never under artifact/<host> or artifact/plugin.
  • local directory: passed directly to host marketplace/install commands or Agent Bundle lifecycle commands.

Git is the common discovery path for both Claude and Codex: users add an owner/repo, Git URL, or local marketplace, then install the named plugin. Both Claude and Codex marketplaces can use an npm package as the plugin source. Codex materializes the package with npm pack --ignore-scripts, requires the standard npm package/ archive root, verifies the package.json name against the requested package, and installs the extracted plugin root without running package scripts. The npm tarballs inspected for @prismer/claude-code-plugin, @ours.network/codex, and @useorgx/codex-plugin put .claude-plugin/ or .codex-plugin/, skills, hooks, and package.json at the same package root; that is the layout to produce.

Supported source and install mapping

npm is a plugin source inside a marketplace catalog, not a replacement for the marketplace itself. The user still adds a catalog and installs a named plugin; the host materializes the package declared by that catalog.

Distribution form Claude Codex Cursor Composite-root requirement
GitHub or Git marketplace claude plugin marketplace add owner/repo, then claude plugin install <plugin>@<marketplace> codex plugin marketplace add owner/repo, then codex plugin add <plugin>@<marketplace> The generated helper can stage a committed local marketplace; Cursor then requires its Customize import/install step Repository catalog resolves the composite root through ./ or a documented relative subdirectory
Local directory Native marketplace add/install commands accept the local catalog/root Native marketplace add/plugin add commands accept the local catalog/root node install.mjs copies the root safely into Cursor's local plugin directory Supplied directory is already the complete composite root
npm package Marketplace entry uses { "source": "npm", "package": "@scope/name", "version": "..." }; Claude installs the package Same source shape; Codex uses npm pack --ignore-scripts, validates the standard package/ archive root and package name, then installs the extracted plugin root No native npm-backed non-interactive install path is assumed; the bundled helper installs from the acquired package root Host manifests and every referenced file are at the npm package root, not under artifact/

The generated catalogs and INSTALL.md must use only source types actually supported by that host. The Git/local/npm delivery choice must not change the plugin's internal bytes or require a post-install Agent Bundle build.

Marketplace catalogs for a repository root and catalogs that resolve an npm package are different distribution metadata, but both must resolve to the same composite plugin root. Do not introduce npm, git, or plugin as host targets. If configuration is needed for distribution wrappers/catalogs, keep it separate from targets.

5. Generate installation surfaces from selected projections

INSTALL.md must contain only the selected hosts and exact commands for the chosen distribution form.

  • Claude and Codex should use their native marketplace/plugin CLIs whenever the source form is supported.
  • Cursor currently has marketplace management but no complete non-interactive install/uninstall verb. When cursor is selected, include the existing self-contained install.mjs and its uninstall path.
  • Preserve the existing Cursor local mode: receipt-owned, idempotent copy into ~/.cursor/plugins/local/<name>, replacement checks, --uninstall, and guarded data purge.
  • Preserve marketplace mode: stage a committed repository, print the required Cursor Customize import step, and report that removal of Cursor-owned cached imports is manual when the host exposes no CLI operation.
  • Emit install.mjs only when at least one selected projection needs it. If a future host CLI provides the complete lifecycle, the generated instructions can use it instead.
  • A requested install for an unselected host must fail with a clear missing-projection diagnostic.

6. Use one marketplace identity

Marketplace naming is currently inconsistent: Claude can configure its name while other adapters and install text hardcode <plugin>-marketplace. Normalize a single marketplace identity, use it in every selected manifest, instruction, receipt, install/uninstall command, and Doctor result, and validate host-reserved names. A reasonable default is the plugin name.

7. Update all composite-root consumers

Update artifact validation, package inventory, prepack, install, uninstall, Doctor, dev host sync, Workbench/playground, MCP services, eval harnesses, and installed-test helpers to resolve the composite root first and select a logical host view within it. Nested <root>/<host> and <root>/plugin probing may remain temporarily as an explicit backward-compatibility path, but new output and documentation use the root.

Generated AGENTS.md must be optional, selected-projection-aware, or placed so it cannot overwrite a repository's authored AGENTS.md when the composite is published at the repository root.

Acceptance tests

  1. targets: ['claude', 'codex'] emits one root with .claude-plugin/ and .codex-plugin/, no claude/, codex/, portable/, or plugin/ output partitions, and no Cursor projection.
  2. Cursor-only emits one root with Cursor files, INSTALL.md, and the self-contained install.mjs --uninstall lifecycle.
  3. plugin is rejected as a config or CLI target.
  4. Omitting targets emits one composite root with the documented default projection set.
  5. Reversing target order produces byte-identical output.
  6. Shared implementation files compile once; each selected host manifest references them correctly; host-scoped components do not leak.
  7. Selected native hook documents and wrappers survive composition; unselected host files are absent.
  8. The artifact manifest and hook index list real projections only.
  9. portable + codex and portable + cursor preserve the native host feature set or fail with the documented incompatibility diagnostic.
  10. npm pack --json shows manifests and plugin content at package/ root, with no artifact/**; installing the packed tarball uses no source checkout and no post-install Agent Bundle build.
  11. The same committed root passes native local/Git marketplace install checks for Claude and Codex, and the supported npm-source install checks.
  12. Install, replacement, uninstall, and Doctor are exercised from the composite root for every selected host. Cursor local and staged-marketplace receipt, purge, and manual-final-step behavior remain covered.
  13. Marketplace name configuration is consistent in all manifests, instructions, receipts, and lifecycle commands.
  14. Authored AGENTS.md preservation and generated-instruction behavior are covered.

References

Version audited

Agent Bundle main at 42539ff5f and issue/code state inspected on 2026-09-04.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions