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
11 changes: 11 additions & 0 deletions .changeset/framework-owned-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"agent-bundle": minor
---

agent-bundle owns the build (RFC #50 Phase 1): one `agent-bundle.config.ts` now produces the npm package build alongside host artifacts, with framework-owned entry lifecycles and one bundler escape hatch.

- `bin` config (or the `src/cli.ts` convention) emits self-executing `dist/bin/<name>.js` bundles with a shebang, executable bit, and a generated `main(argv)` process envelope; artifact Scripts whose module exports `main` receive the same envelope.
- `lib` config (or the `src/index.ts` convention) emits a single-entry ESM library build with declarations, resolving `typescript` and tsconfig compiler options from the project.
- MCP server entries that default-export a server factory are wrapped in the new framework stdio lifecycle shell — console-to-stderr guard with raw stdout restored for protocol frames, SIGINT 130 / SIGTERM 143, stdin-EOF exit 0, bounded shutdown race, heartbeat — also public as `agent-bundle/mcp-entry`. Self-connecting entries keep their behavior byte for byte. The `src/mcp/<server-id>.ts` convention supplies the entry for servers naming no `entry`, `command`, or `url`.
- `tools.rsbuild` / `tools.rspack` is the single blessed bundler escape hatch, merged last into every synthesized config (scripts, MCP entries, hooks, MCP Apps, package build) and still bounded by the artifact invariant assertions.
- `agent-bundle mcp run --server <name> --target <target>` runs one built stdio server in the foreground, resolving its content-hashed generated entry from the target manifest.
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,63 @@ Skills follow the Agent Skills directory layout and may contain references and b

The compiler rejects unsafe output names, unsupported extensions, nonexistent or escaping source paths, unknown targets, and output collisions before it stages an artifact. It does not call Codex, Claude, or another host CLI, and it does not require API keys.

### One config: agent-bundle owns the build

`agent-bundle.config.ts` is the only build configuration a plugin project
needs — no consumer `rslib.config.ts`, no hand-written bin shims, no
hand-rolled stdio lifecycles, no launcher scripts. Alongside the host
artifacts, `agent-bundle build` also produces the node-consumable npm package
build when a project declares (or conventionally provides) one:

```ts
export default defineConfig({
plugin: { name: 'review-tools', version: '1.0.0' },
// npm-facing CLI binaries: dist/bin/<name>.js, self-executing with a
// shebang and executable bit; point package.json "bin" straight at it.
bin: { 'review-tools': './src/cli.ts' },
// Optional npm library output: dist/index.js (+ dist/*.d.ts). The profile
// is deliberately thin — one ESM entry, node target, optional dts.
lib: { entry: './src/index.ts', dts: true },
// THE bundler escape hatch: merged last into every config agent-bundle
// synthesizes (scripts, MCP entries, hooks, MCP Apps, bin/lib), still
// bounded by the artifact invariant assertions. Consumers never need a
// second bundler config file.
tools: {
rsbuild: { /* Rsbuild environment-config fragment */ },
rspack: (config) => { /* mutate the resolved Rspack config */ },
},
});
```

A `bin` entry that exports `main(argv)` (or a default function) receives the
generated process envelope: argv forwarding, `await`, and a numeric return
adopted as the exit code. Artifact `scripts` whose module exports `main` get
the same envelope; self-executing scripts keep their direct-bundle behavior.
Generating `lib` declarations resolves `typescript` from the project (install
it as a devDependency) and reuses the project `tsconfig.json` compiler
options scoped to the entry's source directory.

Entry-file conventions fill the config when it is silent, and config always
wins (`bin: false` / `lib: false` opt out): `src/cli.ts` becomes the bin named
after the plugin, `src/index.ts` becomes the library output, and
`src/mcp/<server-id>.ts` becomes the stdio entry of a declared MCP server that
names no `entry`, `command`, or `url`. See
[docs/entry-conventions.md](docs/entry-conventions.md) for the full contract.

An MCP server entry that **default-exports a server factory** is wrapped in
the framework-owned stdio lifecycle shell: console-to-stderr protection with
raw stdout restored for protocol frames, SIGINT/SIGTERM handling (exit 130 /
143), stdin-EOF detection (exit 0 so the client can respawn), a bounded
shutdown race against wedged transports, and heartbeat logging. Self-connecting
entries keep today's behavior byte for byte. The same lifecycle is available
directly from `agent-bundle/mcp-entry` for hand-rolled entries.

`agent-bundle mcp run --server <name> --target <target> [--artifact <path>]`
runs one built stdio server in the foreground, resolving its content-hashed
generated entry from the target manifest — no more parsing `mcp.json` from a
launcher script. Server state anchored on the plugin-data token persists under
`.agent-bundle/mcp-run/<target>/<server>`.

### Adapter-owned extensions

Ordinary projects need no runtime extension key. `AgentBundleConfig` explicitly
Expand Down
148 changes: 148 additions & 0 deletions docs/entry-conventions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Entry conventions and the framework-owned package build

agent-bundle is the build product for agent plugins the way Rslib is for
libraries: one `agent-bundle.config.ts`, one CLI, framework-owned entry
lifecycles, and a single blessed escape hatch into the bundler. This document
is the contract for the package build (`bin` / `lib`), the entry-file
conventions, the generated entry shells, and the `tools` escape hatch.

## The package build

`agent-bundle build` always emits host artifacts. When the project declares
`bin`/`lib` (or provides them by convention), the CLI build also produces the
node-consumable package build under `dist/` — the outputs `package.json`
`bin` and `exports` point at:

| Config | Output | Notes |
| --- | --- | --- |
| `bin: { '<name>': './src/cli.ts' }` | `dist/bin/<name>.js` | Self-executing ESM bundle, `#!/usr/bin/env node` shebang, executable bit. |
| `lib: { entry: './src/index.ts', dts: true }` | `dist/<stem>.js` + `dist/**/*.d.ts` | Single-entry ESM profile, node target, es2022 syntax. |

- The package build runs only for `agent-bundle build` (CLI, or
`build({ packageOutputs: true })` through the API). Programmatic artifact
operations — temporary artifacts, the dev workbench, evals — never write
`dist/`.
- Outputs are staged and published atomically, and their provenance
(bytes, SHA-256, sorted project-relative source inputs) is reported on the
build result exactly like artifact files.
- `dist` is a mandatory-ignored directory: package outputs never enter
project source snapshots or skill/asset discovery.
- An artifact `--output` that overlaps `dist` is rejected (`AB4706`).
- The `lib` profile is deliberately thin. A package that needs a multi-format
library matrix (UMD, multiple entries, per-format tsconfig) has outgrown the
profile and genuinely wants Rslib — that is the one case where a second
bundler config remains, by choice.

### Declarations

`lib.dts` defaults to `true`. Declaration generation resolves `typescript`
from the project (add it as a devDependency) and compiles the lib entry's
source directory as its own program: compiler options come from the project
`tsconfig.json` (via `extends`), `rootDir` is pinned to the entry's directory,
and only that subtree is included — test files never fail or pollute the
package build. Declarations land flat under `dist/`, one `.d.ts` per source
module.

## Entry-file conventions

Conventions fill the config when it is silent; config always wins. Discovered
entries carry `provenance.kind: 'conventional'` in the normalized model.

| Convention | Meaning | Opt out |
| --- | --- | --- |
| `src/cli.ts` | Package bin named after `plugin.name` (skipped when the name is not a safe output name). | `bin: false` |
| `src/index.ts` | Library output with declarations. | `lib: false` |
| `src/mcp/<server-id>.ts` | Stdio entry for the declared MCP server `<server-id>` that names no `entry`, `command`, or `url`. | Declare `entry` explicitly |

Conventions match `.ts` and `.tsx` files exactly.

## Generated entry shells

The framework provides the entry files consumers used to write by hand
(react-router's provided-entry trick). Every generated shell imports the
consumer module by absolute path and is bundled through the same Rslib
synthesis and invariant assertions as all generated executables.

### The executable envelope (bin + Scripts)

A `bin` entry — or an artifact `Script` — whose module exports `main` (or a
default function for bin entries) receives the generated process envelope:

```ts
// src/cli.ts — the whole CLI entry a consumer writes
export const main = async (argv: readonly string[]): Promise<number> => {
// ...
return 0;
};
```

The envelope awaits `main(process.argv.slice(2))`, adopts a numeric return as
the process exit code, and lets an escaped rejection surface through Node's
top-level failure path (stack to stderr, exit code 1). Self-executing modules
(no `main` export) bundle directly, byte for byte — existing Scripts keep
their behavior.

### The stdio MCP lifecycle shell

An MCP server entry that **default-exports a server factory** is served under
the framework lifecycle:

```ts
// src/mcp/curator.ts — the whole stdio entry a consumer writes
import { createRscMcpServer } from '@agent-bundle/rsc-runtime/plugin';
import { application } from '../application.js';

export default () => createRscMcpServer(application, 'curator');
```

The generated shell provides, in order: console-to-stderr redirection before
the consumer module evaluates, the factory call, raw `process.stdout.write`
restored for protocol frames, `StdioServerTransport` construction and
connect, SIGINT → exit 130, SIGTERM → exit 143, stdin EOF → exit 0 (so the
client can respawn), transport-close → exit 0, a 5-second bounded shutdown
race against wedged transports, and heartbeat/activity logging on stderr
(5-minute interval, 60-second activity throttle, labeled with the server
name).

Self-connecting entries — modules that construct and connect a transport at
top level without a default export — keep today's behavior byte for byte.

The same lifecycle is public API for hand-rolled entries:

```ts
import { redirectConsoleToStderr, runStdioServer } from 'agent-bundle/mcp-entry';
```

Export detection is a static scan of the entry source (comment-, string-, and
template-safe). The generated shells re-verify the export shape at runtime
with a clear error.

## `tools` — THE escape hatch

`tools.rsbuild` (an Rsbuild environment-config fragment) and `tools.rspack`
(an Rspack config object, mutator function, or array — Rslib semantics) merge
**last** into every bundler config agent-bundle synthesizes: artifact scripts,
MCP entries, hook wrappers, MCP App views, and the package build. This mirrors
Rslib's user-config-highest priority and Rspress's `builderConfig` position,
and it is the reason a consumer never needs a second bundler config file.

The hatch is bounded: the framework invariant hook runs after the consumer's
`tools.rspack`, and the resolved-config assertions still run after the merge.
A hatch value that breaks an artifact contract (async chunks, output roots,
self-containment) fails the build with a hard diagnostic instead of silently
overriding the contract. The hatch customizes *how code compiles*, never
*what the artifact promises*.

## `agent-bundle mcp run`

```sh
agent-bundle mcp run --server <name> --target <target> [--artifact <path>]
```

Runs one built stdio MCP server in the foreground with inherited stdio: the
content-hashed generated entry is resolved from the target's MCP manifest
(the job previously solved with bash launchers parsing `mcp.json`), path
tokens are resolved through the target adapter, and the child's exit code is
forwarded (SIGINT/SIGTERM forward to the child). Without `--artifact`, a
temporary artifact is built first. State anchored on the plugin-data token
persists under `.agent-bundle/mcp-run/<target>/<server>` in the project root.
42 changes: 23 additions & 19 deletions examples/audiobook-curator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,22 @@ pnpm --filter @agent-bundle-example/audiobook-curator build
Build and globally link the workspace package without a tarball:

```sh
pnpm --filter @agent-bundle-example/audiobook-curator build:cli
pnpm --filter @agent-bundle-example/audiobook-curator build
cd examples/audiobook-curator
ln -s "$(pwd)/bin/audiobook-curator.js" ~/.local/bin/audiobook-curator
ln -s "$(pwd)/dist/bin/audiobook-curator.js" ~/.local/bin/audiobook-curator
audiobook-curator --help
```

Choose any writable directory already on `PATH` in place of `~/.local/bin`.
This is a direct workspace link; it does not pack or install a tarball.

`build:bundle` writes complete Claude and Codex outputs beneath `artifact/`,
including each host's plugin metadata, Skill, bundled CLI script, and bundled MCP
server. The example uses only public `agent-bundle` and
`@agent-bundle/rsc-runtime` exports with `workspace:*` dependencies.
One `agent-bundle build` produces everything: complete Claude and Codex
outputs beneath `artifact/` (each host's plugin metadata, Skill, bundled CLI
script, and lifecycle-wrapped MCP server) plus the npm package build beneath
`dist/` (`dist/bin/audiobook-curator.js` for `package.json` `bin`,
`dist/index.js` and declarations for `exports`). The example uses only public
`agent-bundle` and `@agent-bundle/rsc-runtime` exports with `workspace:*`
dependencies.

## Source layout

Expand All @@ -52,9 +55,10 @@ server. The example uses only public `agent-bundle` and
`evidence.ts`, `conversion.ts`, `media-mutation.ts`, `integrity-audit.ts`,
`curator-core.ts`) over the shared `foundation.ts` and `media-process.ts`
primitives; `result.tsx` renders every receipt for MCP.
- `src/cli.ts`, `src/cli-entry.ts`, `src/mcp-server.ts`, and
`bin/audiobook-curator.js` are the entry shims for the CLI (test-injectable
runner, bundled `<Script>` entry, npm bin) and the stdio MCP server.
- `src/cli.ts` exports `main`; the framework's generated process envelope
turns it into both the bundled `<Script>` artifact entry and the npm bin.
`src/mcp-server.ts` default-exports a server factory served under the
framework's stdio lifecycle shell. No hand-written entry shims remain.

## Complete workflow

Expand Down Expand Up @@ -98,13 +102,13 @@ The completion contract and real-volume checklist are in

## Maintainer notes

This example carries two bundler configs: `agent-bundle.config.ts` (the
application, consumed by `agent-bundle build` for the `artifact/` host
outputs) and `rslib.config.ts` (a hand-written second build producing
`dist/` for the npm `bin`/`exports`, plus its `tsconfig.build.json`). The
duplication is a known framework gap — `agent-bundle build` does not yet emit
a node-consumable package build, so the same CLI is bundled twice from two
configs. When the framework owns the package build, delete `rslib.config.ts`,
`tsconfig.build.json`, and the `build:cli` script; `bin/audiobook-curator.js`
should then point at the framework's output. See the note at the top of
`rslib.config.ts`.
This example is the reference consumer of the framework-owned package build
("one config, agent-bundle owns the build"): `agent-bundle.config.ts` is a
pure pass-through of the RSC application's config, and the `src/cli.ts` /
`src/index.ts` conventions provide the npm bin and library outputs under
`dist/`. The former second bundler config (`rslib.config.ts`), its
`tsconfig.build.json`, the hand-written `bin/audiobook-curator.js` shim, the
self-executing `src/cli-entry.ts`, and the dual `build:cli`/`build:bundle`
script chain were all deleted when the framework took ownership. See
[`docs/entry-conventions.md`](../../docs/entry-conventions.md) for the
contract.
7 changes: 0 additions & 7 deletions examples/audiobook-curator/bin/audiobook-curator.js

This file was deleted.

10 changes: 3 additions & 7 deletions examples/audiobook-curator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@
"node": ">=22.19.0"
},
"files": [
"bin",
"dist",
"README.md"
],
"bin": {
"audiobook-curator": "./bin/audiobook-curator.js"
"audiobook-curator": "./dist/bin/audiobook-curator.js"
},
"exports": {
".": {
Expand All @@ -22,13 +21,11 @@
}
},
"scripts": {
"build": "pnpm build:cli && pnpm build:bundle",
"build:bundle": "agent-bundle build --json --output artifact",
"build:cli": "rslib build",
"build": "agent-bundle build --json --output artifact",
"check": "pnpm test && pnpm typecheck && pnpm build",
"dev": "agent-bundle dev",
"test": "rstest tests",
"typecheck": "tsc -p tsconfig.build.json --noEmit",
"typecheck": "tsc -p tsconfig.json --noEmit",
"validate": "agent-bundle validate --json"
},
"dependencies": {
Expand All @@ -38,7 +35,6 @@
"zod": "4.4.3"
},
"devDependencies": {
"@rslib/core": "0.23.2",
"@rstest/core": "0.11.10",
"@types/react": "19.2.18",
"agent-bundle": "workspace:*"
Expand Down
33 changes: 0 additions & 33 deletions examples/audiobook-curator/rslib.config.ts

This file was deleted.

4 changes: 3 additions & 1 deletion examples/audiobook-curator/src/application.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ export const createAudiobookCuratorApplication = (
version="1.0.0"
>
<Skill source="./skills/curate-audiobooks" />
<Script entry="./src/cli-entry.ts" name="audiobook-curator" />
{/* The framework process envelope invokes the exported `main`; no
hand-written self-executing entry file is needed. */}
<Script entry="./src/cli.ts" name="audiobook-curator" />
<McpServer entry="./src/mcp-server.ts" name="curator" />
{definitions.map((definition) => <Operation definition={definition} key={definition.id} />)}
</AgentBundle>,
Expand Down
3 changes: 0 additions & 3 deletions examples/audiobook-curator/src/cli-entry.ts

This file was deleted.

Loading
Loading