Skip to content

RFC: agent-bundle owns the build — one config, framework entry conventions #50

Closed

Description

@ScriptedAlchemy

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 what agent-bundle build can't:
    • dist/cli.js from src/cli.ts — consumed by the hand-written npm bin shim bin/audiobook-curator.js (7 lines, package.json bin field at examples/audiobook-curator/package.json:15-17);
    • dist/index.js + dist/index.d.ts from src/index.ts — the package's library export (package.json:18-23), with dts: 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, and transport.onmessage wrapping 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 (redirect console.* to stderr before any import can run, hand process.stdout.write back to the transport right before serving), because a stray console.log corrupts 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 compiled agent-bundle/mcp-apps registry, call runMovieLibraryServerOnStdio(<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, per mcpEntryAliasPattern at packages/agent-bundle/src/config/normalize.ts:75 and normalizeMcpServer at normalize.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 of artifact/portable/mcp.json with an inline node -e, and exec node "${ENTRY}". The CLI has mcp list and mcp 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. RslibConfig is an Rsbuild config plus a lib: LibConfig[] array. composeCreateRsbuildConfig (packages/core/src/config.ts#L1943) turns each lib entry 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 (id or generated format{index}); pruneEnvironments (config.ts#L2094) implements --lib selection.
  • One rsbuild instance. createRslib (createRslib.ts#L73) makes a single createRsbuild({ callerName: 'rslib', config: { …, environments } }) call (L116-128) and exposes build / inspectConfig / startMFDevServer. This is exactly the API packages/agent-bundle/src/build/rslib.ts consumes.
  • Escape hatch without a second file: the user config is the rsbuild surface — tools.rspack, output.*, plugins pass through at highest priority. And @rslib/core re-exports rsbuild and rspack wholesale (index.ts#L48-L50) so consumers never install the bundler.
  • Executables are the product's job: experiments.exe compiles 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 web and node environments into the host config via api.modifyRsbuildConfig + mergeRsbuildConfig (src/index.ts#L847, #L948-L1015), including synthesized entries and rspack.experiments.VirtualModulesPlugin virtual 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/index is 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.ts and loads its own react-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, own defineConfig) whose rsbuild escape hatch is a single field: builderConfig. The lowering is one merge in initRsbuild (node/initRsbuild.ts#L592-L618): internal framework config < each rspress-plugin's builderConfig contribution < the user's builderConfig < programmatic extras — then createRsbuild({ callerName: 'rspress' }).
  • Its own plugin system layered above the bundler: PluginDriver hooks are domain-shaped — addPages, addRuntimeModules, routeGenerated, extendPageData, modifySearchIndexData (node/PluginDriver.ts#L14-L24) — and an rspress plugin may also carry builderConfig to reach the bundler.
  • Conventions vs config: content identity comes from the filesystem (docs dir scanned by RouteService in dev.ts#L37-L41, _meta.json nav/sidebar, public/, theme/ dir); behavior and site metadata come from config fields; eject is 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-config family 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 one lib[] entry per generated executable — stable id: agent-bundle-<name>, format: 'esm', syntax: 'es2022', output.target: 'node', splitChunks: false, virtual modules via rspack.experiments.VirtualModulesPlugin, and its own internal tools.rspack hook (rslib.ts:139-153) — then asserts the resolved bundler config honors the artifact invariants (assertExecutableConfig, rslib.ts:60-85). compileMcpApps does the same on raw Rsbuild with one environment per app (build/mcp-apps.ts:148-181). Scripts, MCP entries (with the agent-bundle/mcp-apps virtual 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 wrapped rsbuild 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 */ },
  },
});
  • bin subsumes audiobook-curator's rslib.config.ts cli entry, the bin/audiobook-curator.js shim, and the build:cli script. A bin entry that names an artifact Script (same source) is bundled once and emitted to both destinations.
  • lib subsumes the index entry + 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 existing agent-bundle/mcp-apps injection and hook wrappers) provides in order: console-to-stderr redirection before the user module evaluates, stdout handback to the transport, StdioServerTransport construction and connect, SIGINT/SIGTERM + stdin-EOF handling, shutdown-timeout race, and heartbeat/activity logging (opt-out per server). The user entry shrinks to export default () => createServer(...). This deletes stdio-lifecycle.ts, console-guard.ts, and the naive mcp-server.ts bootstrap 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 .env loading (rsbuild's loadEnv, as createRslib does at createRslib.ts#L76-L81), exec node. This deletes the resolve-and-exec core of run-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.rspack in agent-bundle.config.ts are merged last into every synthesized config (rslib's "user config highest priority", config.ts#L1996-L2004; rspress's builderConfig position, initRsbuild.ts#L603-L609), with optional per-entry scoping (bin.<name>.tools, mcp.servers.<name>.tools) if flat proves too blunt.
  • The existing inspectConfig assertions 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 --bundler to dump the lowered rslib/rsbuild/rspack configs (rslib inspect writes them to disk, cli/commands.ts#L183-L204; the internal call already exists at build/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):

  1. bin and lib config fields lowered through buildWithRslib; generated bin shim; dist/ outputs.
  2. Framework stdio shell for entry: servers whose default export is a factory (detected statically; self-executing entries keep today's behavior byte-for-byte).
  3. tools.rsbuild/tools.rspack passthrough + inspect --bundler.
  4. agent-bundle mcp run.
  5. 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:

  1. Convention discovery (src/cli.ts, src/mcp/<server-id>.ts, src/index.ts) behind provenance: 'conventional', config always winning.
  2. Deprecation diagnostics (warning-level) for self-connecting entry: modules and for projects where a sibling rslib.config.ts builds entries agent-bundle now owns.
  3. 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

  1. lib scope: single-entry ESM+dts profile (proposed) vs delegating multi-format library needs to a real rslib.config.ts — the one case where a second config would remain by choice, for a product that genuinely is a library first.
  2. Should bin emit 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 rslib experiments.exe is a possible later profile.
  3. 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?
  4. Does mcp run belong under mcp or as a top-level agent-bundle run <server>?

Activity

  1. ScriptedAlchemy commented on Aug 30, 2026

    @ScriptedAlchemy
    OwnerAuthor

    Posting the parallel audit's boilerplate catalog referenced in §1 as pending ("A parallel audit is producing a complete boilerplate catalog") — this is that evidence, reconciled against both checkouts.

    Agent-bundle consumer boilerplate catalog

    Evidence base for the "one config, agent-bundle owns the build" RFC. Two real
    consumers were audited:

    • audiobook-curator — ScriptedAlchemy/agent-bundle examples/audiobook-curator,
      branch refactor/audiobook-curator-structure (line references below are to
      that branch; pre-refactor references to main 9a31306 are called out
      explicitly). One CLI, one stdio MCP server, one Skill, 16 operations.
    • movie-library — /fast/projects/agent-plugins/movie-library (external
      consumer, pinned to pkg.pr.new previews of agent-bundle @ 9a31306). Five
      stdio MCP servers, one MCP-App widget, two CLI Scripts, one Skill,
      46 operations. Its PORT-NOTES.md is the authoritative port record.

    Categories:

    • (a) entry-point / lifecycle boilerplate — code every author writes to
      turn a defineRscAgentBundle application into runnable processes.
    • (b) build-layer boilerplate — extra bundler configs, build scripts, and
      launcher/installer scripts required because agent-bundle build only emits
      host artifacts. (b) is the headline.

    (b) Build-layer boilerplate

    The dual-config defect

    Both consumers carry an agent-bundle.config.ts that is a pure
    pass-through
    of the RSC application's config — the file exists only because
    the build CLI resolves it by name:

    Meanwhile audiobook-curator ALSO carries rslib.config.ts — a second config
    for the same rspack-family bundler. agent-bundle is itself rslib-shaped; a
    consumer should not need to configure the bundler twice.

    What rslib.config.ts covers that agent-bundle build does not

    agent-bundle build --output artifact DOES bundle the CLI: it emits
    artifact/{claude,codex}/scripts/audiobook-curator.mjs from 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/ that
    package.json bin/exports can point at for npm install -g / library
    consumption. That single gap forces, in audiobook-curator:

    1. examples/audiobook-curator/rslib.config.ts:10-33 — second bundler config
      building dist/cli.js + dist/index.js (+ .d.ts) from
      src/cli.ts/src/index.ts. The same CLI is bundled twice from two
      configs (rslib dist/cli.js 163 kB vs agent-bundle
      artifact/*/scripts/audiobook-curator.mjs 1.36 MB).
    2. examples/audiobook-curator/tsconfig.build.json:1-10 — a third config
      file, existing only to serve the rslib build (rootDir: ./src).
    3. 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 ...".
    4. examples/audiobook-curator/package.json:10-23 — files/bin/exports
      hand-wired to the rslib dist/, not to anything agent-bundle produced.
    5. examples/audiobook-curator/bin/audiobook-curator.js:1-7 — hand-written
      bin 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:

    1. movie-library/scripts/run-stdio.sh:1-92 — launcher required to run any
      of the five servers: resolves the hashed bundle filename by parsing
      artifact/portable/mcp.json at launch (run-stdio.sh:73-86, "hashes
      change per build — never hardcode them"), loads .env, exports the
      state-root env vars, and exec nodes the entry.
    2. movie-library/scripts/run-stdio.sh:88-92 — no source-run fallback: the
      stdio entries import the build-time virtual module
      agent-bundle/mcp-apps, whose source-mode stub throws by design, so every
      dev iteration requires a full agent-bundle build first
      (PORT-NOTES.md:357-360).
    3. movie-library/scripts/install-kimi.mjs (150 lines) and
      scripts/install-opencode.mjs (173 lines) — per-host installers for hosts
      the framework has no target adapter for; both fail closed unless
      artifact/portable/mcp.json exists and lists every server id
      (PORT-NOTES.md:108-113).
    4. movie-library/src/http-server.ts (198 lines) + src/dev-http.ts (6) +
      package.json:13 ("dev:http": "tsx src/dev-http.ts") — hand-rolled
      loopback HTTP bridge because the framework offers no dev server for MCP
      over HTTP.
    5. 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.ts or the application file itself) from
    which agent-bundle build:

    • emits host artifacts (already true),
    • optionally emits a node package build (dist/ + generated bin shims +
      exports wiring) — deletes items 1-5 of audiobook-curator's list,
    • exposes a stable non-hashed entry alias or a agent-bundle run <server>
      launcher — deletes run-stdio.sh's mcp.json parsing,
    • supports source-mode execution of MCP-App-bearing entries (dev loop) —
      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:

    1. src/cli.ts:1-35 — the runner/envelope: wraps runRscCli, re-exposes an
      operations-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.
    2. src/cli-entry.ts:1-3 — the 3-line <Script> entry
      (await main(process.argv.slice(2))). This is copy-paste, not
      convention: nothing in the framework generates or names it, it is simply
      what every author will paste.
    3. bin/audiobook-curator.js:1-7 — the npm bin shim (also counted under (b)).
    4. 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, so
      pre-refactor every one of 15 operations hand-rolled its spread block
      inside the 808-line application.tsx (main 9a31306,
      examples/audiobook-curator/src/application.tsx:225-286 for the toolkit,
      ~292-760 for the per-operation parse blocks).

    MCP server entries and stdio lifecycle

    The two consumers land at opposite, equally bad poles:

    • audiobook-curator src/mcp-server.ts:1-8 — the minimal entry:
      createRscMcpServer(...).connect(new StdioServerTransport()). No signal
      handling, no stdin-EOF exit, no heartbeat, no stdout protocol guard —
      robustness is silently missing because the framework provides none.
    • movie-library hand-rolled everything the minimal entry lacks:
      • src/stdio-lifecycle.ts:1-198 — heartbeat (interval + throttled
        activity 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 be
        the 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-identical
        14-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

    Application composition

    • Pre-refactor application.tsx (main 9a31306) was 808 lines: operations
      interface + defaults + shared schemas + argv toolkit + 15 inline
      defineOperation blocks + the JSX tree. Nothing in the framework prevents
      feature-module composition — after this refactor application.tsx is 91
      lines of pure composition over src/operations/* — but nothing encourages
      it either: no scaffold, no documented convention, and every example ships
      a single-file tree.
    • movie-library reached the same structure by necessity
      (src/streams/core-tools.ts, src/streams/backend-tools.ts registrar
      modules feeding the bridge; src/server-map.ts:1-169 as an exhaustive
      tool→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 a
    path 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.ts and mcp-server.ts in audiobook-curator and all five
    src/mcp/*.ts entries in movie-library.


    Deletion table (what each proposed convention removes)

    Proposed convention audiobook-curator deletes movie-library deletes
    Package build owned by agent-bundle (bin/exports/dist) rslib.config.ts (33), tsconfig.build.json (10), bin/audiobook-curator.js (7), build:cli script, dual build pipeline — (not npm-distributed)
    Stable entry alias / agent-bundle run — scripts/run-stdio.sh mcp.json parsing (:73-86), most of the launcher
    Source-mode dev execution of bundle-only entries — the "no tsx fallback" constraint (run-stdio.sh:88-92), faster dev loop
    Generated CLI entry + process envelope src/cli-entry.ts (3), most of src/cli.ts (35) 2 Script entries
    Managed stdio lifecycle + console guard robustness gap in src/mcp-server.ts (8) src/stdio-lifecycle.ts (198), src/mcp/console-guard.ts (17), 5 × entry files (~75)
    CLI arg-parsing helpers (or schema-derived parsing) src/operations/cli-arguments.ts (72) equivalent parsing in its CLI scripts
    First-class listing extras (#43, landed in #48) — toolListingExtras in src/operations.ts + src/server.ts (77)

    Prior consumer-driven fixes for cross-reference: #42 (MCP Apps in elements),
    #43 (listing title/_meta), #44 (lowerMcpResult undefined semantics) — all
    addressed by #48 — and #49 (state-root anchoring, AGENT_BUNDLE_PLUGIN_ROOT).

  2. ScriptedAlchemy commented on Aug 31, 2026

    @ScriptedAlchemy
    OwnerAuthor

    Phase 1 (additive) is implemented in #52 — this issue stays open to track Phases 2–3.

    Delivered, mapped to §5 Phase 1:

    1. bin / lib config lowered through buildWithRslib — dist/bin/<name>.js self-executing bundles (shebang + executable bit + generated main(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+dts lib profile (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.
    2. Framework stdio shell for factory entries — public agent-bundle/mcp-entry (port of movie-library's 198-line stdio-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 zero agent-bundle imports.
    3. tools.rsbuild / tools.rspack passthrough — 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 --bundler was deliberately deferred (see below).
    4. agent-bundle mcp run --server <name> --target <t> (open question 4 resolved: under mcp, symmetric with list/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>.
    5. audiobook-curator migrated in-tree: deleted rslib.config.ts, tsconfig.build.json, bin/audiobook-curator.js, src/cli-entry.ts, and the dual build:cli/build:bundle chain (+ the @rslib/core devDep); agent-bundle.config.ts stays a pure pass-through with conventions supplying bin/lib. Its check and 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>.ts convention discovery landed now (behind provenance: 'conventional', config always winning) since the audiobook migration exercises it end to end.

    Remaining for Phase 2: deprecation diagnostics (self-connecting entry: modules, sibling rslib.config.ts builds), movie-library dropping its lifecycle/launcher/stubs upstream, inspect --bundler, and dev-watch parity for bin/lib outputs (§3.5). Phase 3: create-agent-bundle scaffold-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 list against a conventional factory server).

  3. ScriptedAlchemy commented on Aug 31, 2026

    @ScriptedAlchemy
    OwnerAuthor

    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@da5df1d
    • https://pkg.pr.new/ScriptedAlchemy/agent-bundle/@agent-bundle/rsc-runtime@da5df1d

    This issue stays open for Phases 2–3 (deprecation diagnostics, movie-library upstream migration, inspect --bundler, dev-watch parity for bin/lib, create-agent-bundle).

  4. ScriptedAlchemy commented on Aug 31, 2026

    @ScriptedAlchemy
    OwnerAuthor

    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:

    1. Deprecation/nudge diagnostics — informational (never error) AB473x codes at validate time: AB4730 for 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), and AB4731/AB4732/AB4733 when src/cli.ts, src/index.ts, or src/mcp/<server-id>.ts exists but explicit config shadows it (bin: false / lib: false opt-outs stay silent). Human validate output now prints non-error diagnostics; all examples validate clean. Documented in the new docs/diagnostics.md reference.
    2. 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, the dist/ 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.
    3. Dev-watch parity for bin/lib (§3.5) — agent-bundle dev rebuilds 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 (normalized bin/lib + tools hatch, functions by source text) is unchanged; config file, package.json, tsconfig.json, manual/initial invalidations, and prior failures always rebuild. Failures surface as one AB7103 warning without invalidating the committed epoch; removing the last package entry in-session removes the outputs that session published. Boundary stated in docs/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/66a7961 previews · Remaining: Phase 3 — create-agent-bundle scaffold-by-default, plus the deferred adapter prebuilt-payload mode from the examples plan (Q9).

  5. ScriptedAlchemy commented on Aug 31, 2026

    @ScriptedAlchemy
    OwnerAuthor

    Phase 3 (create-agent-bundle scaffold-by-default) is delivered: #62 landed on main as 13210fdcbc41165da7199054ac7fa68b10d01d96 (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 unified plugin target enforces too), so every target selection scaffolds a project that validates.

    Delivered, mapped to §5 Phase 3:

    1. New workspace package packages/create-agent-bundle — npm create agent-bundle / npx create-agent-bundle, rslib-built single ESM bundle with zero runtime dependencies (@clack/prompts inlined as a devDependency, the create-rstack tarball 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).
    2. UX modeled on 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 is node:util parseArgs.
    3. Three checked-in templates teaching the convention layout — minimal (skills-only, mirrors skills-starter), mcp-server (conventional src/mcp/<id>.ts factory entry under the lifecycle shell + one script, mirrors mcp-app's post-Phase-2 shape), cli-tool (src/cli.ts bin convention + src/index.ts lib 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 a check gate; scaffolded output validates with zero diagnostics including the AB473x nudges.
    4. Framework pinning without npm — templates carry agent-bundle: "workspace:*" as the placeholder; the scaffolder pins it to --framework-version verbatim or derives the paired pkg.pr.new preview from its own -preview-<sha> version at run time (same commit ⇒ same sha ⇒ the pairing rule from docs/preview-packages.md holds automatically). Non-preview builds refuse to guess: npm view create-agent-bundle is a 404 (name unclaimed), but agent-bundle on npm remains an unrelated package, so a semver fallback would install the wrong thing. The real-name decision stays deferred.
    5. Tests — unit coverage for flag/prompt resolution and template emission; a packed-pool e2e (in test:packed, not the integration pool) that installs the scaffolder from its real npm pack tarball, scaffolds each template against the real agent-bundle tarball (--framework-version file:…, no network registry for the framework), runs each project's own check end 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.md scaffolder 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).

  6. added a commit that references this issue on Aug 31, 2026
  7. ScriptedAlchemy commented on Aug 31, 2026

    @ScriptedAlchemy
    OwnerAuthor

    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:

    1. Top-level payload config block — payload: { <dest>: { source, targets } } packages a consumer-built tree byte-for-byte into every selected artifact target, with { prebuilt: … } markers pointing mcp.servers.<id>.entry and hook handlers (plus shell-safe args) at files inside a declared payload.
    2. Stable paths, content-addressed integrity — prebuilt files keep their declared paths (the framework didn't compile them, so it can't rewrite internal references), but every payload file lands in the artifact manifest with its SHA-256 under the new prebuilt file kind and hashes into project.sourceInputs, so the project revision tracks payload bytes.
    3. Zero adapter changes — a prebuilt MCP entry normalizes to a command-shaped stdio server anchored on the plugin-root token, so all four adapters lower it natively (anchor env injection included); prebuilt hooks emit node "<root>/<path>" <args…> into the generated hooks/hooks.json.
    4. Diagnostics AB4740–AB4750 (documented in docs/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.
    5. The consumer proof, in the same PR — examples/rsc-agent-runtime migrated off its hand-rolled packaging: scripts/package-hosts.mjs (75 lines) and packaging/ (7 host manifests, 88 lines) deleted; agent-bundle.config.ts is now the single declaration and agent-bundle build emits what the bespoke script used to.

    Final phase ledger:

    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-bundle is taken on npm by an unrelated 0.1.2 package; create-agent-bundle is 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.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions