Repository navigation
RFC: agent-bundle owns the build — one config, framework entry conventions #50
Description
Summary
agent-bundle should be to agent plugins what Rslib is to libraries and Rspress is to docs sites: the build product. One agent-bundle.config.ts, one CLI, framework-owned entry lifecycles, and a single blessed escape hatch into the bundler — no consumer-owned rslib.config.ts, no hand-written bin shims, no hand-rolled stdio lifecycles, no bash launchers.
The internals already prove the pattern: agent-bundle build synthesizes complete Rslib/Rsbuild configs for everything it compiles today (scripts, MCP entries, hooks, MCP Apps). This RFC generalizes that to the outputs consumers still build by hand — npm bin CLIs, library exports, and stdio server entry lifecycles — and adds the entry-file conventions both real consumers already converged on independently.
All file:line references to this repo are at 9a31306 (current main). External references: web-infra-dev/rslib@v0.23.2 (the version this repo installs), web-infra-dev/rspress@v2.0.21, rstackjs/rsbuild-plugin-react-router@df208f5.
1. Problem: two consumers, same boilerplate
1.1 audiobook-curator ships two bundler configs for one project
examples/audiobook-curator carries both configs side by side:
agent-bundle.config.ts(4 lines) — a pass-through of the RSC application's config.rslib.config.ts(26 lines) — a second, consumer-owned bundler config whose only job is to produce whatagent-bundle buildcan't:dist/cli.jsfromsrc/cli.ts— consumed by the hand-written npm bin shimbin/audiobook-curator.js(7 lines,package.jsonbinfield atexamples/audiobook-curator/package.json:15-17);dist/index.js+dist/index.d.tsfromsrc/index.ts— the package's library export (package.json:18-23), withdts: true.
The build script chains the two products manually (examples/audiobook-curator/package.json:25-27):
"build": "pnpm build:cli && pnpm build:bundle",
"build:bundle": "agent-bundle build --json --output artifact",
"build:cli": "rslib build",Worse, the same CLI is bundled twice by two different configs: the application declares <Script entry="./src/cli-entry.ts" name="audiobook-curator" /> (examples/audiobook-curator/src/application.tsx:800), so agent-bundle build already bundles the CLI into artifact/<target>/scripts/audiobook-curator.mjs — while rslib build bundles the identical logic from src/cli.ts into dist/cli.js for npm. src/cli-entry.ts (4 lines) exists only because the artifact needs a self-executing wrapper around the main() that src/cli.ts exports for the bin shim. Three files (cli.ts, cli-entry.ts, bin/audiobook-curator.js) and two bundler configs express one idea: "this plugin has a CLI named audiobook-curator".
The MCP server side hand-rolls its own bootstrap too — examples/audiobook-curator/src/mcp-server.ts:1-9 creates the server and calls connect(new StdioServerTransport()) with no signal handling, no stdin-EOF detection, no shutdown path at all.
1.2 movie-library hand-rolled the launcher and lifecycle the framework never provided
The external consumer at agent-plugins/movie-library (ported across #42–#47) needed the robust version of that same bootstrap and had to write all of it:
src/stdio-lifecycle.ts(198 lines) — heartbeat logging, SIGINT/SIGTERM handling, stdin-EOF detection ("so the client can respawn us"), a shutdown-timeout race against wedged transports, andtransport.onmessagewrapping for activity tracking. Its own header admits it's a port of a legacy script "adapted to the five-server topology". None of this is movie-domain code; it is framework-grade stdio plumbing.src/mcp/console-guard.ts(17 lines) — the stdout-protection dance (redirectconsole.*to stderr before any import can run, handprocess.stdout.writeback to the transport right before serving), because a strayconsole.logcorrupts JSON-RPC framing. Also framework-grade: every stdio MCP server in existence needs exactly this.- Five near-identical entry stubs
src/mcp/{public,ipt,mam,seedbox,library}.ts(14–17 lines each) whose entire body is: console-guard first-import ritual, import the compiledagent-bundle/mcp-appsregistry, callrunMovieLibraryServerOnStdio(<id>, …). scripts/run-stdio.sh(95 lines) — a bash launcher that exists because generated MCP entry filenames are content-hashed (mcp/mcp-<name>-<hash8>.mjs, permcpEntryAliasPatternatpackages/agent-bundle/src/config/normalize.ts:75andnormalizeMcpServeratnormalize.ts:224-234) and nothing in the CLI will run one by name: the script parses a server id, loads.env, pins state-dir env vars, resolves the hashed entry out ofartifact/portable/mcp.jsonwith an inlinenode -e, andexec node "${ENTRY}". The CLI hasmcp listandmcp invoke(packages/agent-bundle/src/cli.ts:414-452) but no way to run a server in the foreground.
Two independent consumers, same conclusion: the framework compiles their MCP entries but makes them own the entry's runtime shell — and one of them shipped the naive 9-line version while the other needed 198 lines to do it right. That asymmetry is the strongest possible argument that the lifecycle belongs in the framework.
A parallel audit is producing a complete boilerplate catalog (
/tmp/agent-bundle-boilerplate-catalog.md); it had not landed by filing time. The inventory above is first-hand from both checkouts and can be reconciled against the catalog when it appears.
2. How the reference products structure "a product on top of rsbuild"
2.1 Rslib — product-wrapping-rsbuild: own CLI, one config, inline escape hatch
- One config that is the product's domain model.
RslibConfigis an Rsbuild config plus alib: LibConfig[]array.composeCreateRsbuildConfig(packages/core/src/config.ts#L1943) turns eachlibentry into an rsbuild config via a three-layer merge with documented priority — built-in constants < composed lib config < raw user config — and strips the lib-only fields (id, bundle, format, syntax, dts, shims, …) from the passthrough (config.ts#L2008-L2024). - One user config → N internal environments.
composeRsbuildEnvironments(config.ts#L2039) keys one rsbuild environment per lib (idor generatedformat{index});pruneEnvironments(config.ts#L2094) implements--libselection. - One rsbuild instance.
createRslib(createRslib.ts#L73) makes a singlecreateRsbuild({ callerName: 'rslib', config: { …, environments } })call (L116-128) and exposesbuild/inspectConfig/startMFDevServer. This is exactly the APIpackages/agent-bundle/src/build/rslib.tsconsumes. - Escape hatch without a second file: the user config is the rsbuild surface —
tools.rspack,output.*,pluginspass through at highest priority. And@rslib/corere-exportsrsbuildandrspackwholesale (index.ts#L48-L50) so consumers never install the bundler. - Executables are the product's job:
experiments.execompiles Node SEA binaries as a per-lib option (exe/index.ts#L149) — Rslib considers "turn an entry into an executable" in-scope for the build product, not the consumer's problem. - CLI:
rslib=build(default) /inspect/mf-dev(cli/commands.ts#L89); libraries watch-rebuild rather than dev-serve.
2.2 rsbuild-plugin-react-router — plugin-into-rsbuild: conventions and provided entries, but two config files
- The plugin injects complete
webandnodeenvironments into the host config viaapi.modifyRsbuildConfig+mergeRsbuildConfig(src/index.ts#L847, #L948-L1015), including synthesized entries andrspack.experiments.VirtualModulesPluginvirtual modules (index.ts#L842) — the same mechanics agent-bundle already uses internally. - Convention with template fallback is its best trick:
entry.client/entry.server/entry.rsc*are resolved in the app directory, and if the user file doesn't exist the framework supplies its own template (src/entry-paths.ts#L21-L34);server/indexis the custom-server convention (entry-paths.ts#L40). Users write zero entry files until they need to eject one. - But it does not achieve one config file. The plugin is registered in
rsbuild.config.tsand loads its ownreact-router.config.ts(index.ts#L234-L262, warning when missing). The plugin-into-bundler model inherently splits identity across two files — precisely the smell this RFC is removing.
2.3 Rspress — full framework: own CLI, own config, own plugin system, file conventions
- CLI:
rspress [root](dev, the default command),build,preview,eject(packages/core/src/cli/index.ts#L34, L102, L129, L167). - One config (
rspress.config.ts, owndefineConfig) whose rsbuild escape hatch is a single field:builderConfig. The lowering is one merge ininitRsbuild(node/initRsbuild.ts#L592-L618): internal framework config < each rspress-plugin'sbuilderConfigcontribution < the user'sbuilderConfig< programmatic extras — thencreateRsbuild({ callerName: 'rspress' }). - Its own plugin system layered above the bundler:
PluginDriverhooks are domain-shaped —addPages,addRuntimeModules,routeGenerated,extendPageData,modifySearchIndexData(node/PluginDriver.ts#L14-L24) — and an rspress plugin may also carrybuilderConfigto reach the bundler. - Conventions vs config: content identity comes from the filesystem (docs dir scanned by
RouteServiceindev.ts#L37-L41,_meta.jsonnav/sidebar,public/,theme/dir); behavior and site metadata come from config fields;ejectis the escape hatch for theme conventions.
2.4 Where agent-bundle already sits on that spectrum
agent-bundle is already structurally an Rspress-class product, not a plugin:
- Own CLI with more surface than rslib or rspress:
dev/build/validate/eval(+compare) /inspect/mcp list|invoke/hooks list|simulate(packages/agent-bundle/src/cli.ts:308-486). - Own config +
defineConfig(packages/agent-bundle/src/core/types.ts:116-129, 325-327), loaded via the same@rstackjs/load-configfamily the Rstack tools use (packages/agent-bundle/package.json). - Own domain plugin layer: target adapters (claude/codex/cursor/portable) that plan artifacts and extend config by declaration merging (
AgentBundleConfigExtensions,core/types.ts:87-89). - Internal lowering that mirrors Rslib's exactly:
buildWithRslib(packages/agent-bundle/src/build/rslib.ts:87-157) synthesizes onelib[]entry per generated executable — stableid: agent-bundle-<name>,format: 'esm',syntax: 'es2022',output.target: 'node',splitChunks: false, virtual modules viarspack.experiments.VirtualModulesPlugin, and its own internaltools.rspackhook (rslib.ts:139-153) — then asserts the resolved bundler config honors the artifact invariants (assertExecutableConfig, rslib.ts:60-85).compileMcpAppsdoes the same on raw Rsbuild with one environment per app (build/mcp-apps.ts:148-181). Scripts, MCP entries (with theagent-bundle/mcp-appsvirtual registry,build/entries.ts:170-186), and hooks (entries.ts:212-236) all flow through this. - A real dev server (the Workbench,
src/dev/*), not a wrappedrsbuild dev.
What it is missing is narrow and specific: it doesn't own the consumer's npm-facing outputs (bin + library), it doesn't provide the entry runtime shell for stdio servers, it has no entry-file conventions, and it exposes no bundler escape hatch at all (today the hatch is "write a second config file for a second bundler run").
3. Design: the one-config model
3.1 What agent-bundle.config.ts grows to express
export default defineConfig({
plugin: { name: 'audiobook-curator', version: '1.0.0' },
targets: ['claude', 'codex'],
skills: ['./skills/curate-audiobooks'],
// NEW — npm-facing CLI binaries. Key = bin name, value = entry (or { entry, dts }).
// Lowered to a synthesized rslib lib entry; emits dist/cli.js + a generated
// shebang bin shim (or a directly executable output), and writes/updates
// nothing the user must hand-maintain.
bin: { 'audiobook-curator': './src/cli.ts' },
// NEW — optional npm library output for packages that are also importable.
// Lowered to rslib with dts. Absent for artifact-only plugins (movie-library).
lib: { entry: './src/index.ts', dts: true },
mcp: {
servers: {
curator: {
// EXISTING field, NEW semantics available: when the entry module's
// default export is a server factory (not a self-connecting script),
// the framework wraps it in its own generated stdio shell.
entry: './src/mcp/curator.ts',
},
},
},
scripts: { /* unchanged */ },
// NEW — the one blessed escape hatch, mirroring rslib's passthrough and
// rspress's builderConfig: applied last onto every config agent-bundle
// synthesizes, still subject to the artifact invariant assertions.
tools: {
rsbuild: { /* EnvironmentConfig fragment */ },
rspack: (config) => { /* mutate */ },
},
});binsubsumes audiobook-curator'srslib.config.tsclientry, thebin/audiobook-curator.jsshim, and thebuild:cliscript. Abinentry that names an artifactScript(same source) is bundled once and emitted to both destinations.libsubsumes theindexentry +dts: true. This is deliberately a thin profile (entry, dts, format fixed to ESM/node like everything else agent-bundle emits), not a re-implementation of Rslib's full matrix — a consumer who needs UMD/multi-format library builds has outgrown the profile and genuinely wants Rslib; the RFC only claims the agent-plugin-shaped case both consumers actually have.- Framework-owned stdio shell for
mcp.servers[].entry: the generated wrapper (a virtual module, exactly like the existingagent-bundle/mcp-appsinjection and hook wrappers) provides in order: console-to-stderr redirection before the user module evaluates, stdout handback to the transport,StdioServerTransportconstruction andconnect, SIGINT/SIGTERM + stdin-EOF handling, shutdown-timeout race, and heartbeat/activity logging (opt-out per server). The user entry shrinks toexport default () => createServer(...). This deletesstdio-lifecycle.ts,console-guard.ts, and the naivemcp-server.tsbootstrap in one move, and makes every agent-bundle stdio server robust by default instead of only the ones whose authors wrote 198 lines. agent-bundle mcp run --server <name> [--target <t>]— the missing foreground runner: resolve the (content-hashed) entry from the artifact manifest, apply.envloading (rsbuild'sloadEnv, ascreateRslibdoes at createRslib.ts#L76-L81), exec node. This deletes the resolve-and-exec core ofrun-stdio.sh.
3.2 Entry-point conventions (what both consumers already invented)
Convention with config override, react-router-style (user file wins, config field overrides, framework template fills the runtime shell):
| Convention | Meaning | Prior art in consumers (hand-made today) |
|---|---|---|
src/cli.ts |
The plugin's CLI: exports main/default; becomes bin.<plugin-name> and, when also declared as a Script, the artifact script — one bundle, two destinations |
audiobook-curator src/cli.ts + src/cli-entry.ts + bin/audiobook-curator.js |
src/mcp/<server-id>.ts |
Stdio entry for MCP server <server-id>, default-exporting a server factory; lifecycle shell provided by the framework |
movie-library src/mcp/{public,ipt,mam,seedbox,library}.ts — five stubs around one hand-rolled lifecycle; audiobook src/mcp-server.ts |
src/index.ts |
Library export → lib output with dts (only when present and lib not explicitly disabled) |
audiobook-curator src/index.ts via rslib.config.ts |
skills/<id>/SKILL.md, assets/ |
Already conventional today (README, assets/ copy convention) |
both |
Conventions are discovered at normalization time with provenance.kind: 'conventional' — the enum already exists (core/types.ts:131) precisely for this.
3.3 Internal lowering: generalize the pattern build/rslib.ts already proves
Mirror Rslib's composeRsbuildEnvironments: one composition step turns the normalized model into the full set of synthesized lib entries/environments — today's scripts + MCP entries + hooks + apps, plus the new bin and lib outputs — with per-kind profiles (artifact executables keep asyncChunks: false + no-hash; bin/lib emit to dist/ with dts where asked). buildWithRslib's existing structure needs no redesign: it already takes an arbitrary entry list with virtual-module support and returns provenance evidence. The invariant assertions (assertExecutableConfig, assertResolvedViewConfig) stay and become the boundary the escape hatch cannot cross.
3.4 Escape hatch semantics
tools.rsbuild/tools.rspackinagent-bundle.config.tsare merged last into every synthesized config (rslib's "user config highest priority", config.ts#L1996-L2004; rspress'sbuilderConfigposition, initRsbuild.ts#L603-L609), with optional per-entry scoping (bin.<name>.tools,mcp.servers.<name>.tools) if flat proves too blunt.- The existing
inspectConfigassertions still run after the merge: a hatch value that breaks an artifact contract (async chunks, output roots, self-containment) is a hard diagnostic, not a silent override. The hatch customizes how code compiles, never what the artifact promises. - Add
agent-bundle inspect --bundlerto dump the lowered rslib/rsbuild/rspack configs (rslibinspectwrites them to disk, cli/commands.ts#L183-L204; the internal call already exists atbuild/rslib.ts:159).
3.5 Dev flow
agent-bundle dev stays the single dev command and the Workbench stays its face. Additions: the watcher extends to bin/lib outputs (rebuild-on-change parity with rslib build --watch — libraries don't need a server, per rslib's model), and mcp run gives the foreground-process story the launcher script was compensating for. Framework-mode react-router is the shape witness here: one dev command serves every environment the framework composed; consumers never run the bundler directly.
4. Exact boilerplate deleted, per consumer
audiobook-curator (with bin + lib + conventions):
| File | Lines | Fate |
|---|---|---|
rslib.config.ts |
26 | deleted |
bin/audiobook-curator.js |
7 | generated by the build |
src/cli-entry.ts |
4 | deleted (one CLI bundle, two destinations) |
src/mcp-server.ts transport bootstrap |
9 → ~3 | entry becomes a factory export; shell provided |
package.json build/build:cli/build:bundle chain + @rslib/core devDep |
3 scripts + 1 dep | "build": "agent-bundle build" |
movie-library (with lifecycle + conventions + mcp run):
| File | Lines | Fate |
|---|---|---|
src/stdio-lifecycle.ts |
198 | deleted (framework shell) |
src/mcp/console-guard.ts |
17 | deleted (shell owns stdout protection) |
src/mcp/{public,ipt,mam,seedbox,library}.ts |
5 × 14–17 | shrink to one-line factory exports, or vanish entirely under the src/mcp/<server-id>.ts convention |
scripts/run-stdio.sh |
95 | replaced by agent-bundle mcp run --server <id> (a thin alias may keep the .env/state-dir operator ergonomics) |
Roughly 420 lines of consumer-owned framework plumbing across the two projects, plus the second bundler config and dependency, plus the dual-build script choreography.
5. Migration: additive first, deprecations later
Phase 1 — additive (no breaking change):
binandlibconfig fields lowered throughbuildWithRslib; generated bin shim;dist/outputs.- Framework stdio shell for
entry:servers whose default export is a factory (detected statically; self-executing entries keep today's behavior byte-for-byte). tools.rsbuild/tools.rspackpassthrough +inspect --bundler.agent-bundle mcp run.- audiobook-curator migrates in-tree as the reference: delete
rslib.config.ts,cli-entry.ts, the bin shim, and the script chain.
Phase 2 — conventions + deprecations:
- Convention discovery (
src/cli.ts,src/mcp/<server-id>.ts,src/index.ts) behindprovenance: 'conventional', config always winning. - Deprecation diagnostics (warning-level) for self-connecting
entry:modules and for projects where a siblingrslib.config.tsbuilds entries agent-bundle now owns. - movie-library drops lifecycle/launcher/stubs upstream.
Phase 3 — scaffold-by-default: create-agent-bundle / docs teach the convention layout; config fields remain the override, per rspress's conventions-with-eject discipline.
6. Alternatives considered
A. Plugin-into-rsbuild (react-router style) — ship rsbuild-plugin-agent-bundle; consumers run rsbuild build/dev. Rejected: it institutionalizes two config files (react-router itself still loads react-router.config.ts next to rsbuild.config.ts, index.ts#L234); it would surrender the CLI surface agent-bundle already has that rsbuild cannot express (validate, eval, mcp, hooks, the Workbench); and agent-bundle's product is a multi-target artifact compiler with validation and provenance, not a web app where the bundler is the natural top of the stack. The plugin model's genuinely good idea — framework-provided entry templates with user override — is adopted into §3.1–3.2 without adopting the topology.
B. Product-wrapping-rsbuild with own CLI + one config (rslib style) — own the build end to end, express the domain in one config, lower onto rsbuild environments internally, escape-hatch inline. This is the recommended core, and it is the smallest step: build/rslib.ts already implements rslib's own internal shape (synthesized lib[], virtual modules, internal tools.rspack), and createRslib remains the lowering target so agent-bundle inherits dts, syntax targeting, and — later, if wanted — experiments.exe SEA binaries for free.
C. Full framework with own plugin system + conventions (rspress style) — agent-bundle already has the rspress anatomy (own CLI, own config, target adapters as the domain plugin layer, Workbench dev server, RSC runtime as the app-level API). Adopt rspress's discipline — one blessed builderConfig-equivalent hatch, conventions for identity with config for behavior, an eject path — but do not invent a new consumer-facing plugin API in this RFC; adapters and the RSC runtime already fill that seat.
Recommendation: B's build ownership + C's conventions/hatch discipline + A's provided-entry-templates trick. On the spectrum, agent-bundle lands next to Rspress: a framework whose bundler is an implementation detail — which is where its own internals have quietly been pointing all along.
7. Prior art trail (consumer-driven)
The movie-library port generated the evidence that consumers reconstruct missing framework surface by hand: #42 (MCP Apps not declarable in the RSC tree), #43 (tool listing _meta dropped, breaking widget binding), #44 (lowerMcpResult vs SDK serialization), #45 + #46 (preview install pairing), #47 (stdio server cwd anchor on bare installs — resolved by the AGENT_BUNDLE_PLUGIN_ROOT anchor now injected into every generated stdio entry, core/types.ts:335-345). Each was one missing slice of "the framework owns the runtime contract of what it emits"; this RFC is the build-and-entry-layer completion of the same principle.
8. Open questions
libscope: single-entry ESM+dts profile (proposed) vs delegating multi-format library needs to a realrslib.config.ts— the one case where a second config would remain by choice, for a product that genuinely is a library first.- Should
binemit a shim (#!/usr/bin/env node+ import, npm-friendly) or write the shebang into the bundle itself? Shim proposed for source-map/Windows friendliness; SEA via rslibexperiments.exeis a possible later profile. - Heartbeat defaults in the stdio shell (movie-library's 5-min interval / 60-s activity throttle) — adopt, or ship silent-by-default with opt-in logging?
- Does
mcp runbelong undermcpor as a top-levelagent-bundle run <server>?
Activity
ScriptedAlchemy commentedon Aug 30, 2026
Agent-bundle consumer boilerplate catalog
Evidence base for the "one config, agent-bundle owns the build" RFC. Two real
consumers were audited:
ScriptedAlchemy/agent-bundleexamples/audiobook-curator,branch
refactor/audiobook-curator-structure(line references below are tothat branch; pre-refactor references to main
9a31306are called outexplicitly). One CLI, one stdio MCP server, one Skill, 16 operations.
/fast/projects/agent-plugins/movie-library(externalconsumer, pinned to pkg.pr.new previews of agent-bundle @
9a31306). Fivestdio MCP servers, one MCP-App widget, two CLI Scripts, one Skill,
46 operations. Its
PORT-NOTES.mdis the authoritative port record.Categories:
turn a
defineRscAgentBundleapplication into runnable processes.launcher/installer scripts required because
agent-bundle buildonly emitshost artifacts. (b) is the headline.
(b) Build-layer boilerplate
The dual-config defect
Both consumers carry an
agent-bundle.config.tsthat is a purepass-through of the RSC application's config — the file exists only because
the build CLI resolves it by name:
examples/audiobook-curator/agent-bundle.config.ts:1-3— 3 lines:export default audiobookCuratorApplication.config.movie-library/agent-bundle.config.ts:1-16— identical pass-through(
const config: AgentBundleConfig = movieLibraryApplication.config); itsdoc comment records that its former only job (the widget-App splice, gap
defineRscAgentBundle: no way to declare MCP Apps (mcp.servers[].apps) in the RSC element tree #42, and the per-server state-root env splice, fixed by fix(adapters): anchor emitted stdio MCP entries with AGENT_BUNDLE_PLUGIN_ROOT and restore claude cwd #49) is gone.
Meanwhile audiobook-curator ALSO carries
rslib.config.ts— a second configfor the same rspack-family bundler. agent-bundle is itself rslib-shaped; a
consumer should not need to configure the bundler twice.
What
rslib.config.tscovers thatagent-bundle builddoes notagent-bundle build --output artifactDOES bundle the CLI: it emitsartifact/{claude,codex}/scripts/audiobook-curator.mjsfrom the<Script>entry (verified from build JSON; ~1.36 MB bundle per target). What it does
NOT produce is a node-consumable package build — a
dist/thatpackage.jsonbin/exportscan point at fornpm install -g/ libraryconsumption. That single gap forces, in audiobook-curator:
examples/audiobook-curator/rslib.config.ts:10-33— second bundler configbuilding
dist/cli.js+dist/index.js(+.d.ts) fromsrc/cli.ts/src/index.ts. The same CLI is bundled twice from twoconfigs (rslib
dist/cli.js163 kB vs agent-bundleartifact/*/scripts/audiobook-curator.mjs1.36 MB).examples/audiobook-curator/tsconfig.build.json:1-10— a third configfile, existing only to serve the rslib build (
rootDir: ./src).examples/audiobook-curator/package.json:25-27— dual build pipeline:"build": "pnpm build:cli && pnpm build:bundle","build:cli": "rslib build","build:bundle": "agent-bundle build ...".examples/audiobook-curator/package.json:10-23—files/bin/exportshand-wired to the rslib
dist/, not to anything agent-bundle produced.examples/audiobook-curator/bin/audiobook-curator.js:1-7— hand-writtenbin shim (
#!/usr/bin/env node+import { main } from '../dist/cli.js');the framework generates no bin.
audiobook-curator is the only example in the repo with an
rslib.config.ts— and it is the only example that ships an installable CLI.The correlation is the gap.
movie-library's build-layer equivalents
movie-library avoids a second bundler config (its two CLIs are
<Script>elements and it is not npm-distributed) but pays the build-layer tax in
scripts:
movie-library/scripts/run-stdio.sh:1-92— launcher required to run anyof the five servers: resolves the hashed bundle filename by parsing
artifact/portable/mcp.jsonat launch (run-stdio.sh:73-86, "hasheschange per build — never hardcode them"), loads
.env, exports thestate-root env vars, and
exec nodes the entry.movie-library/scripts/run-stdio.sh:88-92— no source-run fallback: thestdio entries import the build-time virtual module
agent-bundle/mcp-apps, whose source-mode stub throws by design, so everydev iteration requires a full
agent-bundle buildfirst(
PORT-NOTES.md:357-360).movie-library/scripts/install-kimi.mjs(150 lines) andscripts/install-opencode.mjs(173 lines) — per-host installers for hoststhe framework has no target adapter for; both fail closed unless
artifact/portable/mcp.jsonexists and lists every server id(
PORT-NOTES.md:108-113).movie-library/src/http-server.ts(198 lines) +src/dev-http.ts(6) +package.json:13("dev:http": "tsx src/dev-http.ts") — hand-rolledloopback HTTP bridge because the framework offers no dev server for MCP
over HTTP.
movie-library/package.json:20—"start": "bash scripts/run-stdio.sh";package.json:21— a hand-rolled smoke harness (scripts/smoke-test.ts)to verify the built artifact boots, because the framework build has no
post-build boot check.
RFC shape this evidence supports
One config (
agent-bundle.config.tsor the application file itself) fromwhich
agent-bundle build:dist/+ generated bin shims +exportswiring) — deletes items 1-5 of audiobook-curator's list,agent-bundle run <server>launcher — deletes
run-stdio.sh's mcp.json parsing,deletes the no-fallback constraint.
(a) Entry-point / lifecycle boilerplate
CLI entry chain (audiobook-curator)
One CLI requires four hand-written layers on top of
runRscCli:src/cli.ts:1-35— the runner/envelope: wrapsrunRscCli, re-exposes anoperations-injection seam for tests, and
main()with try/catch →stderr →
process.exitCode. Every consumer must invent this envelope;the framework defines exit codes 0/1/2 but no process wrapper.
src/cli-entry.ts:1-3— the 3-line<Script>entry(
await main(process.argv.slice(2))). This is copy-paste, notconvention: nothing in the framework generates or names it, it is simply
what every author will paste.
bin/audiobook-curator.js:1-7— the npm bin shim (also counted under (b)).src/operations/cli-arguments.ts:1-72— a hand-rolled argv toolkit(option lookup, flag/valued-option validation, positionals, choices,
number coercion, optional-field spreads). The framework's entire CLI
parsing contract is
cli.parse: (argv) => TInput(
packages/rsc-runtime/src/operation.ts:10-16) — zero parsing support, sopre-refactor every one of 15 operations hand-rolled its spread block
inside the 808-line
application.tsx(main9a31306,examples/audiobook-curator/src/application.tsx:225-286for the toolkit,~292-760for the per-operation parse blocks).MCP server entries and stdio lifecycle
The two consumers land at opposite, equally bad poles:
src/mcp-server.ts:1-8— the minimal entry:createRscMcpServer(...).connect(new StdioServerTransport()). No signalhandling, no stdin-EOF exit, no heartbeat, no stdout protocol guard —
robustness is silently missing because the framework provides none.
src/stdio-lifecycle.ts:1-198— heartbeat (interval + throttledactivity logging), SIGINT→130 / SIGTERM→143 / stdin-EOF→0, bounded
5s shutdown race, unref'd keepalive, testable structural types.
src/mcp/console-guard.ts:1-17— stdout-is-protocol guard that must bethe FIRST import of every entry (console → stderr before any module
side effect prints to the JSON-RPC channel).
src/mcp/{public,ipt,mam,seedbox,library}.ts— five near-identical14-17-line stdio entries (guard import → registry import → serve under
the managed lifecycle).
Every stdio MCP server in every consumer needs exactly this lifecycle; it
belongs in the framework (e.g.
serveRscMcpOnStdio(application, serverName)with guard + lifecycle built in).
Operations bridge
src/operations.ts:1-159— theregisterAppTool→defineOperation bridge, including
jsonWireClone(gap lowerMcpResult throws on undefined values inside structuredContent where MCP SDK serialization drops them #44workaround) and the
toolListingExtrasside table (gap createRscMcpServer drops tool listing title and _meta (breaks MCP Apps widget binding via _meta.ui.resourceUri) #43 workaround;createRscMcpServer drops tool listing title and _meta (breaks MCP Apps widget binding via _meta.ui.resourceUri) #43's listing
title/_metapassthrough landed upstream in rsc-runtime: first-class MCP Apps, listing title/_meta passthrough, SDK wire semantics for undefined #48, the sidetable is scheduled for deletion), plus
src/server.ts:1-77, a customserver factory replacing
createRscMcpServer.9a31306)src/mcp-tools.tsx:1-52— the samebridge shape hand-rolled AGAIN (operations →
CallToolResultvialowerMcpResult) even though nothing consumed it; deleted as dead code inthis refactor. Two consumers independently writing the same bridge —
one out of necessity, one out of imitation — is the signature of a
missing convention.
Application composition
application.tsx(main9a31306) was 808 lines: operationsinterface + defaults + shared schemas + argv toolkit + 15 inline
defineOperationblocks + the JSX tree. Nothing in the framework preventsfeature-module composition — after this refactor
application.tsxis 91lines of pure composition over
src/operations/*— but nothing encouragesit either: no scaffold, no documented convention, and every example ships
a single-file tree.
(
src/streams/core-tools.ts,src/streams/backend-tools.tsregistrarmodules feeding the bridge;
src/server-map.ts:1-169as an exhaustivetool→server map).
Config duality of the entries themselves
The
<Script entry="./src/cli-entry.ts">/<McpServer entry="./src/mcp-server.ts">elements (
packages/rsc-runtime/src/plugin-definition.ts:189-208) take apath to a file the author must write, and the framework then bundles that
file. The framework knows the application, knows the server name, and knows
the transport — the entry files contain zero information the tree does not
already have. Generated (or virtual) default entries would delete
cli-entry.tsandmcp-server.tsin audiobook-curator and all fivesrc/mcp/*.tsentries in movie-library.Deletion table (what each proposed convention removes)
rslib.config.ts(33),tsconfig.build.json(10),bin/audiobook-curator.js(7),build:cliscript, dualbuildpipelineagent-bundle runscripts/run-stdio.shmcp.json parsing (:73-86), most of the launcherrun-stdio.sh:88-92), faster dev loopsrc/cli-entry.ts(3), most ofsrc/cli.ts(35)src/mcp-server.ts(8)src/stdio-lifecycle.ts(198),src/mcp/console-guard.ts(17), 5 × entry files (~75)src/operations/cli-arguments.ts(72)toolListingExtrasinsrc/operations.ts+src/server.ts(77)Prior consumer-driven fixes for cross-reference: #42 (MCP Apps in elements),
#43 (listing title/_meta), #44 (
lowerMcpResultundefined semantics) — alladdressed by #48 — and #49 (state-root anchoring,
AGENT_BUNDLE_PLUGIN_ROOT).feat(agent-bundle): framework-owned build — package outputs, stdio en…
ScriptedAlchemy commentedon Aug 31, 2026
Phase 1 (additive) is implemented in #52 — this issue stays open to track Phases 2–3.
Delivered, mapped to §5 Phase 1:
bin/libconfig lowered throughbuildWithRslib—dist/bin/<name>.jsself-executing bundles (shebang + executable bit + generatedmain(argv)envelope; open question 2 resolved in favor of writing the shebang into the bundle rather than a separate shim — one file, nothing to keep in sync) and the thin single-entry ESM+dtslibprofile (open question 1 resolved as proposed; multi-format libraries remain Rslib's job by choice). Declarations synthesize a tsconfig extending the project's own, pinned to the entry's source subtree.agent-bundle/mcp-entry(port of movie-library's 198-linestdio-lifecycle.ts+ console-guard shape): guard-before-evaluation, raw-stdout restore for protocol frames, SIGINT 130 / SIGTERM 143, stdin-EOF 0, 5s bounded shutdown, heartbeat (open question 3 resolved: movie-library's defaults, on by default, stderr-only). Factory detection is a static default-export scan; self-connecting entries stay byte-for-byte. Generated wrappers inline the shell via a bundler alias, so emitted artifacts keep zeroagent-bundleimports.tools.rsbuild/tools.rspackpassthrough — merged last into every synthesized config (scripts, MCP entries, hooks, MCP Apps, package build) with the framework invariant hook appended after the consumer's and the resolved-config assertions still binding.inspect --bundlerwas deliberately deferred (see below).agent-bundle mcp run --server <name> --target <t>(open question 4 resolved: undermcp, symmetric withlist/invoke) — resolves the content-hashed entry through the same target MCP runtime contract, runs foreground with inherited stdio, forwards exit codes; plugin-data state persists under.agent-bundle/mcp-run/<target>/<server>.rslib.config.ts,tsconfig.build.json,bin/audiobook-curator.js,src/cli-entry.ts, and the dualbuild:cli/build:bundlechain (+ the@rslib/coredevDep);agent-bundle.config.tsstays a pure pass-through with conventions supplying bin/lib. Itscheckand parity suites are green with byte-equivalent CLI behavior.Pulled forward from Phase 2: the
src/cli.ts/src/index.ts/src/mcp/<server-id>.tsconvention discovery landed now (behindprovenance: 'conventional', config always winning) since the audiobook migration exercises it end to end.Remaining for Phase 2: deprecation diagnostics (self-connecting
entry:modules, siblingrslib.config.tsbuilds), movie-library dropping its lifecycle/launcher/stubs upstream,inspect --bundler, and dev-watch parity forbin/liboutputs (§3.5). Phase 3:create-agent-bundlescaffold-by-default.Docs: README "One config" section,
docs/entry-conventions.md, package README. The packed-consumer suite now proves the whole story from an installed tarball alone (framework-built bin/lib/dts, packaged lifecycle shell,mcp listagainst a conventional factory server).feat(build): agent-bundle owns the package build — bin/lib, entry con…
ScriptedAlchemy commentedon Aug 31, 2026
Phase 1 merged: #52 landed on main as
da5df1d4588ecb405e836d60256317b452c74eed(squash). All CI green (Verify 22.19/24/26, Release gates, Examples check, packed suite including the new tarball-only framework-build scenario), plus two review hardenings: colocated tests are excluded from the synthesized declaration program, and MCP server names that aren't a plain path segment map to content-addressed state directories under.agent-bundle/mcp-run.Consumers can pin the preview for this commit:
https://pkg.pr.new/ScriptedAlchemy/agent-bundle@da5df1dhttps://pkg.pr.new/ScriptedAlchemy/agent-bundle/@agent-bundle/rsc-runtime@da5df1dThis issue stays open for Phases 2–3 (deprecation diagnostics, movie-library upstream migration,
inspect --bundler, dev-watch parity for bin/lib,create-agent-bundle).refactor(examples/hooks-and-scripts): export main and let the generat…
refactor(examples/mcp-app): factory entry, src/mcp/<server-id>.ts con…
refactor(examples/rsc-agent-runtime): adopt the public agent-bundle/m…
feat: migration nudges, inspect --bundler, and dev-watch of bin/lib (…
ScriptedAlchemy commentedon Aug 31, 2026
Phase 2 framework side is delivered: #60 landed on main as
66a7961c1b59f24c2baa11e8efd0c9422712c900(squash). All CI green (Verify 22.19/24/26, Release gates, Examples check, previews).Delivered, mapped to §5 Phase 2 and the Phase-1 remaining list:
AB473xcodes at validate time:AB4730for a self-connecting local stdio entry (the same static default-export scan the build uses to decide the lifecycle-shell wrap, so nudge and build cannot disagree), andAB4731/AB4732/AB4733whensrc/cli.ts,src/index.ts, orsrc/mcp/<server-id>.tsexists but explicit config shadows it (bin: false/lib: falseopt-outs stay silent). Humanvalidateoutput now prints non-error diagnostics; all examples validate clean. Documented in the newdocs/diagnostics.mdreference.inspect --bundler(§3.4) — a third inspect focus dumping the synthesized Rslib/Rsbuild config for every generated output (scripts, MCP entries, hooks, MCP App views, thedist/package build), post-tools-hatch merge with the invariant hook visible; composed by the same extracted functions the build lowers (composeEntryLibConfig,composeMcpAppsRsbuildConfig), deterministic via two tokens (<output>/<target>,<generated-dts-tsconfig>), generated wrapper sources included.agent-bundle devrebuilds the package build inside the same debounced, serialized rebuild pass that publishes epochs, skipping only when no invalidated path was a provenance-tracked input and the rebuild identity (normalizedbin/lib+toolshatch, functions by source text) is unchanged; config file,package.json,tsconfig.json, manual/initial invalidations, and prior failures always rebuild. Failures surface as oneAB7103warning without invalidating the committed epoch; removing the last package entry in-session removes the outputs that session published. Boundary stated indocs/entry-conventions.md: brand-new module-resolution shadowing is picked up on the next tracked change, not instantly.The consumer-side Phase 2 example migrations landed independently (#55, #56, #57).
Phase ledger: Phase 1 ✅ (#52) · Phase 2 framework side ✅ (#60) · Phase 2 movie-library upstream migration — external repo, unblocked by the
da5df1d/66a7961previews · Remaining: Phase 3 —create-agent-bundlescaffold-by-default, plus the deferred adapter prebuilt-payload mode from the examples plan (Q9).fix(mcp-run): load the project-root .env set and expand env state anc…
feat: create-agent-bundle scaffolding package (RFC #50 Phase 3) (#62)
ScriptedAlchemy commentedon Aug 31, 2026
Phase 3 (
create-agent-bundlescaffold-by-default) is delivered: #62 landed on main as13210fdcbc41165da7199054ac7fa68b10d01d96(squash). All CI green (Verify 22.19/24/26, Release gates including the new packed e2e, Examples check, previews), plus one review hardening: the derived plugin name is sanitized to Cursor's lowercase kebab-case contract (which the unifiedplugintarget enforces too), so every target selection scaffolds a project that validates.Delivered, mapped to §5 Phase 3:
packages/create-agent-bundle—npm create agent-bundle/npx create-agent-bundle, rslib-built single ESM bundle with zero runtime dependencies (@clack/promptsinlined as a devDependency, thecreate-rstacktarball shape), published through the same pkg.pr.new preview channel (preview:publish), publint-clean, versioned by the existing changesets flow (first release:0.0.0→ minor).rstackjs/create-rstack— prompt-driven (name, template, host targets) with full non-interactive flags (--template,--targets,--package-manager,--no-install,--framework-version); adopted its project-name semantics, user-agent PM detection, cancel-exits-0, and "dir + template on the command line means scripted, ask nothing"; flag parsing isnode:utilparseArgs.minimal(skills-only, mirrors skills-starter),mcp-server(conventionalsrc/mcp/<id>.tsfactory entry under the lifecycle shell + one script, mirrors mcp-app's post-Phase-2 shape),cli-tool(src/cli.tsbin convention +src/index.tslib with dts, one bundle two destinations, mirrors audiobook-curator's post-Phase-1 shape without the domain). Each: config, manifest, tsconfig, one passing test, README, and acheckgate; scaffolded output validates with zero diagnostics including the AB473x nudges.agent-bundle: "workspace:*"as the placeholder; the scaffolder pins it to--framework-versionverbatim or derives the paired pkg.pr.new preview from its own-preview-<sha>version at run time (same commit ⇒ same sha ⇒ the pairing rule fromdocs/preview-packages.mdholds automatically). Non-preview builds refuse to guess:npm view create-agent-bundleis a 404 (name unclaimed), butagent-bundleon npm remains an unrelated package, so a semver fallback would install the wrong thing. The real-name decision stays deferred.test:packed, not the integration pool) that installs the scaffolder from its realnpm packtarball, scaffolds each template against the real agent-bundle tarball (--framework-version file:…, no network registry for the framework), runs each project's owncheckend to end, and asserts the lifecycle-shell wrap,mcp list/invoke, and the framework-built bin/lib/dts.Docs: package README, root README quick-start subsection,
docs/preview-packages.mdscaffolder note.Phase ledger: Phase 1 ✅ (#52) · Phase 2 framework side ✅ (#60) · Phase 2 movie-library upstream migration — external repo, unblocked by the previews · Phase 3 ✅ (#62). Remaining from the examples plan: the deferred adapter prebuilt-payload mode (Q9).
feat: prebuilt payload adapter mode (RFC #50 Phase 3) (#71)
ScriptedAlchemy commentedon Aug 31, 2026
Phase 3 is complete and the RFC is delivered: #71 (the deferred adapter prebuilt-payload mode, the examples plan's Q9 — the last open item) landed on main as
28830bd5(squash). All CI green (Verify 22.19/24/26, Release gates, Examples check, previews). Zero unresolved review threads; the Codex findings were addressed in-branch before merge.Delivered in #71, mapped to the deferred Q9 scope:
payloadconfig block —payload: { <dest>: { source, targets } }packages a consumer-built tree byte-for-byte into every selected artifact target, with{ prebuilt: … }markers pointingmcp.servers.<id>.entryand hookhandlers (plus shell-safeargs) at files inside a declared payload.prebuiltfile kind and hashes intoproject.sourceInputs, so the project revision tracks payload bytes.node "<root>/<path>" <args…>into the generatedhooks/hooks.json.AB4740–AB4750(documented indocs/diagnostics.md) — config errors, not-yet-built warnings that keep clean-checkout dev flows working, reference/args errors, three build-time refusals, and a staleness nudge.examples/rsc-agent-runtimemigrated off its hand-rolled packaging:scripts/package-hosts.mjs(75 lines) andpackaging/(7 host manifests, 88 lines) deleted;agent-bundle.config.tsis now the single declaration andagent-bundle buildemits what the bespoke script used to.Final phase ledger:
da5df1d4): framework-owned package build (bin/lib), entry conventions, stdio lifecycle shell,toolshatch,mcp run.66a7961c): AB473x nudges,inspect --bundler, dev-watch of bin/lib. Example migrations refactor(examples/hooks-and-scripts): export main and let the generated envelope own the process (RFC #50 Phase 2) #55/refactor(examples/mcp-app): factory entry, src/mcp/<server-id>.ts convention, and the mcp run doc flow (RFC #50 Phase 2) #56/examples/rsc-agent-runtime: adopt the public agent-bundle/mcp-entry stdio lifecycle (RFC #50 Phase 2) #57. Follow-up operator seams mcp run loads no .env: RFC #50 §3.1 promised loadEnv, so operator-credential consumers must keep wrapper scripts #58/mcp run expands ${PLUGIN_ROOT} and the AGENT_BUNDLE_PLUGIN_ROOT anchor to the artifact target root, fragmenting durable state per rebuild #59 closed by fix(mcp-run): load the project-root .env set and expand env state anchors to the project root #61 (1286f5b2).13210fdc):create-agent-bundle, three templates, packed e2e. Prebuilt payloads feat: prebuilt payload adapter mode (RFC #50 Phase 3) #71 (28830bd5), itemized above.Remaining: nothing in the RFC's scope. The only item gating real npm publishing is the package-name decision, which is a product decision rather than build-ownership work — split out to #81 with the verified facts (
agent-bundleis taken on npm by an unrelated 0.1.2 package;create-agent-bundleis unclaimed per the #62 finding, re-verified today; nothing is published under@agent-bundle/*). The pkg.pr.new preview channel (#40/#46) keeps consumers unblocked in the interim.Closing this RFC as delivered.