From 1923e155fcf4864f5e36a3a30782071403795588 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Mon, 31 Aug 2026 00:16:52 +0000 Subject: [PATCH 1/4] =?UTF-8?q?feat(build):=20agent-bundle=20owns=20the=20?= =?UTF-8?q?package=20build=20=E2=80=94=20bin/lib=20config,=20framework=20s?= =?UTF-8?q?tdio=20entry=20shell,=20entry=20conventions,=20tools=20hatch,?= =?UTF-8?q?=20mcp=20run=20(RFC=20#50=20Phase=201)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One agent-bundle.config.ts now produces the npm package build alongside host artifacts: bin/lib (or the src/cli.ts / src/index.ts / src/mcp/.ts conventions) lower onto the internal Rslib synthesis, factory-exporting MCP entries are wrapped in the framework stdio lifecycle shell (public as agent-bundle/mcp-entry), tools.rsbuild/tools.rspack is the single bounded bundler escape hatch, and `agent-bundle mcp run` launches a built stdio server from the target manifest. audiobook-curator migrates in-PR as proof: drops rslib.config.ts, tsconfig.build.json, and the bin shim with byte-equivalent CLI behavior. --- .changeset/framework-owned-build.md | 11 + README.md | 57 ++++ docs/entry-conventions.md | 148 +++++++++ examples/audiobook-curator/README.md | 42 +-- .../bin/audiobook-curator.js | 7 - examples/audiobook-curator/package.json | 10 +- examples/audiobook-curator/rslib.config.ts | 33 -- .../audiobook-curator/src/application.tsx | 4 +- examples/audiobook-curator/src/cli-entry.ts | 3 - examples/audiobook-curator/src/mcp-server.ts | 8 +- .../tests/application.test.tsx | 12 +- .../tests/audible-parity.test.ts | 2 +- examples/audiobook-curator/tests/cli.test.ts | 4 +- .../tests/evidence-parity.test.ts | 2 +- .../tests/integrity-audit-parity.test.ts | 2 +- .../tests/media-mutation-parity.test.ts | 2 +- .../tests/real-ffmpeg.test.ts | 2 +- .../audiobook-curator/tsconfig.build.json | 10 - examples/audiobook-curator/tsconfig.json | 13 + packages/agent-bundle/README.md | 15 +- packages/agent-bundle/package.json | 4 + packages/agent-bundle/rslib.config.ts | 1 + packages/agent-bundle/src/api.ts | 81 ++++- packages/agent-bundle/src/build/build.ts | 11 +- packages/agent-bundle/src/build/entries.ts | 55 +++- .../agent-bundle/src/build/entry-exports.ts | 156 +++++++++ .../agent-bundle/src/build/entry-shell.ts | 70 ++++ packages/agent-bundle/src/build/mcp-apps.ts | 83 +++-- .../agent-bundle/src/build/package-build.ts | 204 ++++++++++++ packages/agent-bundle/src/build/rslib.ts | 89 +++-- packages/agent-bundle/src/cli.ts | 20 +- packages/agent-bundle/src/config/normalize.ts | 136 +++++++- packages/agent-bundle/src/config/validate.ts | 151 ++++++++- .../agent-bundle/src/core/project-context.ts | 25 ++ packages/agent-bundle/src/core/types.ts | 83 +++++ .../src/dev/artifacts/artifact-service.ts | 1 + .../agent-bundle/src/dev/project-service.ts | 10 + packages/agent-bundle/src/mcp-entry.ts | 272 +++++++++++++++ packages/agent-bundle/src/services/mcp-run.ts | 147 +++++++++ .../agent-bundle/tests/entry-shell.test.ts | 87 +++++ packages/agent-bundle/tests/mcp-entry.test.ts | 309 ++++++++++++++++++ .../agent-bundle/tests/package-build.test.ts | 278 ++++++++++++++++ .../tests/package-conventions.test.ts | 265 +++++++++++++++ .../tests/packed-consumer.test.ts | 96 +++++- .../agent-bundle/tests/public-api.test.ts | 13 + pnpm-lock.yaml | 3 - rstest.integration-tests.ts | 1 + 47 files changed, 2863 insertions(+), 175 deletions(-) create mode 100644 .changeset/framework-owned-build.md create mode 100644 docs/entry-conventions.md delete mode 100755 examples/audiobook-curator/bin/audiobook-curator.js delete mode 100644 examples/audiobook-curator/rslib.config.ts delete mode 100644 examples/audiobook-curator/src/cli-entry.ts delete mode 100644 examples/audiobook-curator/tsconfig.build.json create mode 100644 examples/audiobook-curator/tsconfig.json create mode 100644 packages/agent-bundle/src/build/entry-exports.ts create mode 100644 packages/agent-bundle/src/build/entry-shell.ts create mode 100644 packages/agent-bundle/src/build/package-build.ts create mode 100644 packages/agent-bundle/src/mcp-entry.ts create mode 100644 packages/agent-bundle/src/services/mcp-run.ts create mode 100644 packages/agent-bundle/tests/entry-shell.test.ts create mode 100644 packages/agent-bundle/tests/mcp-entry.test.ts create mode 100644 packages/agent-bundle/tests/package-build.test.ts create mode 100644 packages/agent-bundle/tests/package-conventions.test.ts diff --git a/.changeset/framework-owned-build.md b/.changeset/framework-owned-build.md new file mode 100644 index 000000000..6ecb07027 --- /dev/null +++ b/.changeset/framework-owned-build.md @@ -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/.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/.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 --target ` runs one built stdio server in the foreground, resolving its content-hashed generated entry from the target manifest. diff --git a/README.md b/README.md index e666fe8b0..f4a434624 100644 --- a/README.md +++ b/README.md @@ -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/.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/.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 --target [--artifact ]` +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//`. + ### Adapter-owned extensions Ordinary projects need no runtime extension key. `AgentBundleConfig` explicitly diff --git a/docs/entry-conventions.md b/docs/entry-conventions.md new file mode 100644 index 000000000..7daa2e597 --- /dev/null +++ b/docs/entry-conventions.md @@ -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: { '': './src/cli.ts' }` | `dist/bin/.js` | Self-executing ESM bundle, `#!/usr/bin/env node` shebang, executable bit. | +| `lib: { entry: './src/index.ts', dts: true }` | `dist/.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/.ts` | Stdio entry for the declared MCP server `` 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 => { + // ... + 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 --target [--artifact ] +``` + +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//` in the project root. diff --git a/examples/audiobook-curator/README.md b/examples/audiobook-curator/README.md index 562bf9321..010737c73 100644 --- a/examples/audiobook-curator/README.md +++ b/examples/audiobook-curator/README.md @@ -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 @@ -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 `