Skip to content
Merged
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
16 changes: 16 additions & 0 deletions .changeset/387-artifact-routed-cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"agent-bundle": patch
---

Emit the routed CLI (`src/cli/**`) into every host artifact as
`<target>/bin/<plugin-name>.mjs` (plus `bin/<plugin-name>-flight.mjs` when a
command renders), not only into the npm package build, so installed skills,
hooks, and script routes can run it with `node <plugin-root>/bin/<plugin-name>.mjs`.
Every built-in target publishes the new `cli` adapter capability that admits
the bin; `inspect` accounts for it as a `cli` component, `inspect --bundler`
and the artifact manifest list it, and artifact validation admits the `cliBin`
layout. A target without the capability omits the bin with `AB4765`; a
host-emitted file colliding with the bin path fails the build with `AB4766`.
Script routes reach the bin as their `../bin/<plugin-name>.mjs` sibling;
skills and hooks reach it through the plugin-root token. The package build's
`dist/bin/<plugin-name>.js` is unchanged. (#419)
16 changes: 16 additions & 0 deletions docs/diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ gate a build, a validation, or a dev rebuild.
| `AB473x` | Migration nudges (informational; see below). |
| `AB474x`/`AB4750` | Prebuilt payloads and prebuilt entries (see below). |
| `AB4760` | The published `agent-bundle/meta` identity module evaluated outside every compiled surface and outside the Rstest presets (see below). |
| `AB4765`–`AB4766` | Artifact-hosted routed CLI: a target without the `cli` capability omits `bin/<name>.mjs`; a host-emitted file collides with it (see below). |
| `AB490x`/`AB492x` | Conventional host components (#100 stage 2): rules `src/rules/*.mdc` (`AB4900`–`AB4906`) and commands `src/commands/*.md` (`AB4920`–`AB4926`); see below. |
| `AB5000` | General CLI and adapter failures. |
| `AB60xx` | Built-artifact validation, including schema documents and referenced files (`AB6011`/`AB6012`: a target's required pinned-schema document is missing or invalid; `AB6025`: a manifest-declared `logo` path is missing from the artifact or escapes the deploy tree; `AB6034`: emitted Skill Markdown has no instruction body; `AB6035`–`AB6038`: Agent Plugins portable validation, see below). |
Expand Down Expand Up @@ -388,6 +389,21 @@ identity is never served as a real one.
| --- | --- | --- | --- |
| `AB4760` | error | A module evaluated the published `agent-bundle/meta` outside a surface Agent Bundle compiles — typically a unit test pool not built from the Rstest preset, or a hand-run script importing plugin source. | Run the test under `agentBundleRstest()` or `agentBundleBrowserRstest()` from `agent-bundle/rstest` (pass `include` to cover a plain unit pool), or compile the surface with `agent-bundle build`. In a custom test runner, alias `agent-bundle/meta` (`resolve.alias`, exact match) to a module with the named exports `{ name, packageName, packageVersion, version, meta }` — `meta` the frozen object of the other four, exported as both the named binding and the default export — computed from the project's `agent-bundle.config.ts` plugin name and `package.json` version; the `.agent-bundle/test/meta.mjs` module `agentBundleRstest()` writes is that module. |

## Artifact-hosted routed CLI (`AB4765`–`AB4766`)

A generated-mode `src/cli/**` surface compiles into the npm package bin
(`dist/bin/<name>.js`) **and** 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. See “The routed CLI shell” in
`docs/entry-conventions.md` for the layout and the sibling-path convention.

| Code | Severity | Trigger |
| --- | --- | --- |
| `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. |

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

A `mcp.servers.<id>` block for a server the route graph compiles in
Expand Down
66 changes: 63 additions & 3 deletions docs/entry-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ entries carry `provenance.kind: 'conventional'` in the normalized model.
| `src/mcp/<server>/apps/*.{ts,tsx}` | Browser MCP App entry compiled to self-contained HTML and registered on the generated server; static `config.resourceUri` is required. An optional `config.template` HTML shell resolves relative to the route module like its imports (`'./dashboard.html'`); the legacy project-root-relative form is accepted only while unambiguous (`AB4827` otherwise). Tools, resources, and prompts reference the App from their own static `config` with `appResourceUri('<app>')` from `agent-bundle/routes` or a shared `const` string literal instead of repeating the `ui://` literal. | Use a custom server or prefix the file with `_` |
| `src/scripts/<name>.ts` | Plain script compiled to `scripts/<name>.mjs` in every selected target artifact — the same pipeline explicit `scripts` entries use, with ordinary Node stdout/stderr semantics. A `scripts` entry that references the file claims it. Nested modules are hard errors (`AB4808`). A `bin` entry that references the file does **not** claim it: the module ships as both the npm bin and the artifact script (see [Which config keys claim a conventional module](#which-config-keys-claim-a-conventional-module)); export `main` or make the module self-executing, because a `default`-only module would run as the bin but ship as an inert script (`AB4738`). | Prefix a path segment with `_`, or claim the file with an explicit `scripts` entry |
| `src/scripts/<name>.tsx` | Rendered script: the async default component receives `{ argv, signal }` and renders through the Agent renderer with the CLI output contract (`--json`, `--ndjson`, TTY progress, piped Markdown). Compiles to `scripts/<name>.mjs` plus a `scripts/<name>-flight.mjs` react-server worker. The extension is the explicit, visible contract — plain `.ts` scripts are never wrapped in React behavior, and explicit `scripts` config entries stay plain regardless of extension. A `bin` entry that references a rendered script is `AB4737` unless the module exports both the default component (for the script) and a named `main` (for the bin envelope); with both, the module serves both surfaces. | Rename to `.ts`, prefix a path segment with `_`, or claim the file with an explicit `scripts` entry |
| `src/cli/**/*.{ts,tsx}` | Routed CLI commands compiled into one collision-checked command graph and one generated package executable named after `plugin.name` (superseding the `src/cli.ts` bin convention for the project). Nesting is identity: `src/cli/library/audit.ts` runs as `<bin> library audit`. Plain `.ts` commands execute directly and print one canonical JSON line; `.tsx` commands render through the dispatcher with the four output modes. | `bin: false`, `routes.cli: 'conventional'`, or prefix a path segment with `_` |
| `src/cli/**/*.{ts,tsx}` | Routed CLI commands compiled into one collision-checked command graph and one generated package executable named after `plugin.name` (superseding the `src/cli.ts` bin convention for the project), plus the same executable as `bin/<plugin-name>.mjs` in every selected host artifact whose target publishes the `cli` capability (all built-in targets). Nesting is identity: `src/cli/library/audit.ts` runs as `<bin> library audit`. Plain `.ts` commands execute directly and print one canonical JSON line; `.tsx` commands render through the dispatcher with the four output modes. | `bin: false`, `routes.cli: 'conventional'`, or prefix a path segment with `_` |
| `src/events/<family>/<event>.{ts,tsx}`, `src/events/stop.{ts,tsx}` | Semantic event route: the path is the canonical event family (`src/events/tool/after.tsx` is `tool/after`; `stop` is the one top-level family) and must be one of the admitted `canonicalAgentEvents`. The optional static `config` (`AgentEventRouteConfig`: `targets`, `tools`, `runtime: 'shared' \| 'standalone'`, `fallback`, `delivery`, `timeoutMs`) restricts hosts and selects the execution mode; the async default Server Component receives `AgentEventRouteProps` (`{ canonical, native, signal }`) and returns `Agent.*` output that the selected host adapter encodes into its native hook envelope. Application code never branches on host JSON or emits native hook documents; per-host support is a capability state (`supported`/`degraded`/`unavailable`/`prohibited`) surfaced by `inspect` and enforced at build time (`AB4817`, `AB4823`–`AB4825`). | Restrict `config.targets`, or prefix a path segment with `_` |
| `src/state.ts` | Project state definition: default-exports `defineState({ ... })`; generated MCP, routed-CLI, and rendered-script request scopes mount `(await agent()).state` and `.notices`. | `state: false`, or rename the file to `_state.ts` |
| `src/providers/<name>.{ts,tsx}` | Request context provider: default-exports a factory receiving `{ invocation, signal }`; its value is mounted at `(await agent()).providers.<camelCaseName>` for generated MCP and event routes, projected MCP commands, plain and rendered routed CLI commands, and rendered scripts. | Prefix the file with `_` |
Expand Down Expand Up @@ -150,9 +150,11 @@ for the generated worker or executable process.
Workspace-durable generated MCP workers store under
`$AGENT_BUNDLE_PLUGIN_ROOT/state`. If that host-provided anchor is absent,
the worker derives the artifact root from the parent of its own `mcp/`
directory. Routed CLI bins and rendered scripts use
directory. The npm package's routed CLI bin and rendered scripts use
`$AGENT_BUNDLE_PLUGIN_ROOT/state` when present and otherwise
`$PWD/.agent-bundle/state`. Notice authorization is deliberately permissive
`$PWD/.agent-bundle/state`; the artifact-hosted routed CLI bin
(`<target>/bin/<name>.mjs`) derives the artifact root from the parent of its
own `bin/` directory instead, like the MCP worker. Notice authorization is deliberately permissive
in generated mounting v1 (`authorized`); recipient/principal matching remains
enforced by the ledger, while application authorization policy is deferred.

Expand Down Expand Up @@ -381,6 +383,64 @@ machine output owns stdout. Rendered scripts
(`src/scripts/<name>.tsx`) share the same shell and output contract with
`{ argv, signal }` component props and status-derived exit codes.

#### The routed CLI inside host artifacts

The package bin only reaches users who install the npm package. Hooks,
skills, and script routes ship with the **host artifact**, so the build also
emits the same compiled command graph into every selected target whose
adapter publishes the `cli` capability — all built-in targets (`claude`,
`codex`, `cursor`, `portable`, `plugin`), because the artifact root is
already a plain directory Node executes `mcp/` and `scripts/` files from:

```text
artifact/<target>/
bin/<plugin-name>.mjs # the routed CLI: node bin/<plugin-name>.mjs <command> [args]
bin/<plugin-name>-flight.mjs # react-server worker, present when any command renders
scripts/<name>.mjs
mcp/…
```

The artifact bin is a self-contained ESM module with no shebang or
executable bit — invoke it as `node <plugin-root>/bin/<plugin-name>.mjs
<args>`, exactly like `scripts/*.mjs`. Help, argv parsing, output modes,
exit codes, and signals are identical to the package bin. One deliberate
difference: workspace-durable state without a host-supplied
`AGENT_BUNDLE_PLUGIN_ROOT` anchors on the **artifact root** (the parent of
`bin/`, the same fallback the generated MCP worker beside it uses) rather
than `$PWD/.agent-bundle/state`, so a co-installed CLI and server observe
one store. The npm package bin keeps its `cwd` fallback.

Reaching the bin from the other surfaces:

- **Skills and hooks** use the plugin-root token
(`agent-bundle:path:plugin-root`, or a host spelling such as
`${CLAUDE_PLUGIN_ROOT}`), lowered per host exactly like MCP entries:
`${CLAUDE_PLUGIN_ROOT}/bin/<plugin-name>.mjs` in Claude Skill Markdown and
hook commands, `${PLUGIN_ROOT}/…` in Codex hooks, `${CURSOR_PLUGIN_ROOT}/…`
in Cursor hooks. Only Claude documents Skill Markdown interpolation, so a
skill that spells the token is a Claude-only skill (`AB3008` elsewhere);
skills for other hosts describe the path relative to the plugin root
instead.
- **Script routes** use the sibling convention: from the compiled
`scripts/<name>.mjs`, the bin is `new URL('../bin/<plugin-name>.mjs',
import.meta.url)`, so a plain `src/scripts/<name>.ts` can
`spawn(process.execPath, [fileURLToPath(binUrl), ...argv])` and forward
stdio. `import.meta.url` is rewritten to the artifact location by the
bundle, never left pointing at `src/`.

`inspect` accounts for the bin as one `cli` component per target (selected,
or skipped with the host's `cli` capability judgment), `inspect --bundler`
dumps each target's `bin/` composition beside its scripts, and the artifact
manifest records both files with `bundle` provenance naming every command
route. Artifact validation admits the `bin/` layout only for adapters that
declare it (`cliBin`); because the compiler emits the CLI at exactly
`bin/<plugin-name>.mjs`, an adapter that publishes a supported `cli`
capability without that layout, or with a `cliBin` layout naming another
directory or omitting `.mjs`, is rejected at registration. A target without the
capability omits the bin and reports `AB4765`; a host-emitted file at the
same path (a Claude `claude.bin` directory shipping `<plugin-name>.mjs`) is
`AB4766`. The package build's `dist/bin/<plugin-name>.js` is unchanged.

#### Project generated MCP tools into the CLI (power tier)

`routes.mcpCommands` adds tools from generated MCP servers to the same command
Expand Down
8 changes: 6 additions & 2 deletions docs/framework-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,11 @@ the handwritten `runRscCli` compatibility path still serializes validated
results and never renders JSX. Routed `src/cli/**` commands and
`src/scripts/**` scripts follow one sentence: `.tsx` renders through the
Agent renderer (TTY progress, piped Markdown, `--json`, `--ndjson`); `.ts`
is plain.
is plain. The routed CLI ships twice from one build: as the npm package bin
(`dist/bin/<name>.js`) for users who install the package, and as
`bin/<name>.mjs` inside every host artifact so the plugin's own skills,
hooks, and scripts can run it with `node` from the installed plugin root
(see [Entry conventions](entry-conventions.md#the-routed-cli-inside-host-artifacts)).

## Release identity in source: `agent-bundle/meta`

Expand Down Expand Up @@ -276,7 +280,7 @@ only the string shorthand, not Rsbuild 2.x's per-asset `DistPathConfig` for
such paths as JavaScript, CSS, and SVG subdirectories.
`output.filename` templates, `output.assetPrefix`, and `output.cleanDistPath`
are also deliberately deferred: host packs have a framework-owned
`<target>/skills|mcp|scripts|assets/...` layout content-addressed by the
`<target>/skills|mcp|scripts|bin|assets/...` layout content-addressed by the
artifact manifest. Unlike machine-local Rsbuild config, the hashed, portable
release-identity config rejects absolute paths. The per-invocation CLI
`--output` flag can override the configured relative artifact root, but it is
Expand Down
9 changes: 9 additions & 0 deletions packages/agent-bundle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,15 @@ Skill Markdown are inert in the workbench renderer.

Top-level `scripts` is a record of stable output names to an entry path or `{ entry, targets? }`. JavaScript/TypeScript entries bundle to `scripts/<name>.mjs`; `.sh`, `.bash`, and `.py` entries copy byte-for-byte while preserving source modes. The generated `agent-bundle.manifest.json` records file digests for stable artifact validation.

A project with routed `src/cli/**` commands also ships that CLI inside every host artifact as
`<target>/bin/<plugin-name>.mjs` (plus `bin/<plugin-name>-flight.mjs` when a command renders), a
self-contained module run as `node <plugin-root>/bin/<plugin-name>.mjs <command>` — so a script
route can spawn its `../bin/<plugin-name>.mjs` sibling and a Claude skill can point at
`${CLAUDE_PLUGIN_ROOT}/bin/<plugin-name>.mjs` without a separate npm install. Every built-in target
publishes the `cli` capability that admits it; `inspect` accounts for it as a `cli` component, and
the manifest records both files with bundle provenance. The npm package bin under `dist/bin/` is
unchanged. See `docs/entry-conventions.md` for the layout and diagnostics (`AB4765`, `AB4766`).

### What gets hashed

Hash pins cover vendored external content whose ground truth lives outside this repository and can
Expand Down
11 changes: 11 additions & 0 deletions packages/agent-bundle/src/adapters/capability-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ export const supportedCapability = (evidence: CapabilityEvidence): CapabilitySta
state: 'supported',
});

/**
* The capability that admits the compiled routed CLI (`src/cli/**`) into a
* target's host artifact as `bin/<plugin-name>.mjs` (#387). It asks nothing of
* the host beyond what `scripts/` and `mcp/` entries already rely on — the
* artifact root is installed as a plain directory Node can execute from — so a
* target publishes it whenever its plugin root is such a directory. An adapter
* that publishes no row reads as an honest `unavailable`, and the bin is
* omitted from that target with an inspect entry naming the reason.
*/
export const cliBinCapability = 'cli';

export const unavailableCapability = (reason: string): CapabilityState => Object.freeze({
reason,
state: 'unavailable',
Expand Down
4 changes: 4 additions & 0 deletions packages/agent-bundle/src/adapters/claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
capabilityStateFromSupport,
eventRouteCapabilitiesFrom,
supportedEventRouteNamesFrom,
cliBinCapability,
supportedCapability,
unavailableCapability,
} from './capability-state.ts';
Expand Down Expand Up @@ -3272,6 +3273,9 @@ export const claudeAdapter: TargetAdapter = Object.freeze({
evidence,
'The pinned Claude plugin contract does not document message channel declarations.',
),
// The routed CLI bin rides the same plugin-root directory the pinned
// contract already executes `mcp/` and `scripts/` files from (#387).
[cliBinCapability]: supportedCapability(evidence),
commands: capabilityStateFromSupport(
capabilityTable.plugin.commands,
evidence,
Expand Down
4 changes: 4 additions & 0 deletions packages/agent-bundle/src/adapters/codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
capabilityStateFromSupport,
eventRouteCapabilitiesFrom,
supportedEventRouteNamesFrom,
cliBinCapability,
supportedCapability,
unavailableCapability,
} from './capability-state.ts';
Expand Down Expand Up @@ -1259,6 +1260,9 @@ export const codexAdapter: TargetAdapter = Object.freeze({
artifactLayout: standardArtifactLayout,
capabilities: Object.freeze({
...eventRouteCapabilitiesFrom(capabilityTable.hooks.eventRoutes, evidence),
// The routed CLI bin rides the same plugin-root directory the pinned
// contract already executes `mcp/` and `scripts/` files from (#387).
[cliBinCapability]: supportedCapability(evidence),
commands: unavailableCapability(
'The pinned Codex plugin contract (0.147.0) defines no commands component.',
),
Expand Down
4 changes: 4 additions & 0 deletions packages/agent-bundle/src/adapters/cursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
capabilityEvidence,
capabilityStateFromSupport,
eventRouteCapabilitiesFrom,
cliBinCapability,
supportedEventRouteNamesFrom,
supportedCapability,
unavailableCapability,
Expand Down Expand Up @@ -700,6 +701,9 @@ export const cursorAdapter: TargetAdapter = Object.freeze({
capabilities: Object.freeze({
...contractCapabilities,
...eventRouteCapabilitiesFrom(capabilityTable.hooks.eventRoutes, evidence),
// The routed CLI bin rides the same plugin-root directory the pinned
// contract already executes `mcp/` and `scripts/` files from (#387).
[cliBinCapability]: supportedCapability(evidence),
commands: capabilityStateFromSupport(
capabilityTable.plugin.commands,
evidence,
Expand Down
Loading
Loading