Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/389-bin-claim-script-discovery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"agent-bundle": minor
---

Keep a conventional `src/scripts/<name>.ts` module in script discovery when a `bin` config entry also references it, so `agent-bundle build` emits both `dist/bin/<name>.js` and the artifact `scripts/<name>.mjs` and `agent-bundle inspect --json` lists the module under both `packageBuild.bins` and `scripts` instead of silently dropping the script. Explicit `scripts`, `hooks`, `lib`, and `mcp` entries still claim the module they reference. Report `AB4737` when a `bin` entry points at a rendered `src/scripts/<name>.tsx` script that does not export both its default Server Component and a named `main`, because the bin envelope calls `main` while the artifact script renders the component, and `AB4738` when it points at a plain `src/scripts/<name>.ts` script that exports a `default` but no `main`, because the artifact script would ship inert while the bin runs the default export. (#413)
67 changes: 62 additions & 5 deletions docs/diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,16 +195,27 @@ development-only fallback can never produce a release artifact, so
| `AB4011` | warning | `package.json` is unusable — unparsable, not a JSON object, or symlinked outside the project root. |
| `AB4013` | error (build) | `agent-bundle build` refuses a project with no release version: `plugin.version` is omitted and `package.json` declares no valid semantic version. |

## Migration nudges (`AB4730`–`AB4736`)
## Migration nudges and convention claims (`AB4730`–`AB4738`)

The entry conventions and the framework-owned stdio lifecycle shell (RFC #50)
replaced patterns consumers previously wrote by hand. When `validate`,
`inspect`, `build`, or `dev` prepares project source and finds one of those
pre-convention patterns, it reports a migration diagnostic. `AB4730`–`AB4735`
are **informational** nudges and never block anything. `AB4736` is an error:
the removed top-level authored-document locations are no longer discovered,
so the compiler refuses to omit them silently. The CLI prints these in human
`validate` output and includes them in every `--json` diagnostics array.
are **informational** nudges and never block anything. `AB4736`–`AB4738` are
errors: the removed top-level authored-document locations are no longer
discovered, and a conventional script whose `bin` entry would run an export
the artifact script ignores cannot ship on both surfaces, so the compiler
refuses to omit or misbuild them silently. The CLI prints these in
human `validate` output and includes them in every `--json` diagnostics array.

Which explicit config keys *claim* a conventional module out of discovery is
tabulated in `docs/entry-conventions.md` ("Which config keys claim a
conventional module"). In short: `scripts`, `hooks`, `lib`, and `mcp` entries
claim the module they reference; a `bin` entry claims every conventional
module **except** a safely named direct `src/scripts/<name>` child, which
keeps shipping as an artifact script beside the bin because the two outputs
are disjoint and both envelopes run the same `main`. That dual-surface shape
is intentional and raises no diagnostic.

### `AB4730` — self-connecting stdio MCP entry

Expand Down Expand Up @@ -276,6 +287,52 @@ Recover: move the document under `src/skills/`, `src/commands/`, or
`src/rules/`. Explicit `skills` paths remain valid anywhere. Published
artifact paths remain `skills/`, `commands/`, and `rules/`.

### `AB4737` — rendered script claimed as a package bin entry lacks `main` or the component

An explicit `bin` entry references a conventional rendered script
(`src/scripts/<name>.tsx` or `.jsx`) that does not export **both** an async
default Server Component and a named `main`. The component check is the
route compiler's own static scan — the default export must be an async
function, so `export default {}` does not count; a default re-exported from
another module (`export { default } from './component.tsx'`) cannot be judged
statically and is accepted (the rendered worker still verifies it at run
time). A plain `src/scripts/<name>.ts` module
ships happily on both surfaces — the npm bin envelope calls its `main(argv)`
and the artifact script is the same bundle — but a rendered script's default
export is an async Server Component the Agent renderer drives with
`{ argv, signal }` props. The bin envelope prefers a named `main` export and
only falls back to the default export, so without `main` it would call that
component as `main(argv)` and produce a bin that renders nothing; without the
default component, the bin works but `scripts/<name>.mjs` fails at run time
with no component to render. The compiler refuses either shape instead of
emitting a broken surface beside a working one. A rendered script that
exports both serves both surfaces and is not gated. The message names every
`bin` entry referencing the module and which export is missing.

Recover: export both an async default Server Component and a named
`main(argv)` from the module; point the `bin` entry at a plain module that exports `main`;
rename the script to `.ts` so one plain module ships as both the bin and the
artifact script; or prefix a path segment with `_` (`src/scripts/_name.tsx`)
to keep the module out of script discovery and bin-only.

### `AB4738` — plain script claimed as a package bin entry runs only as the bin

An explicit `bin` entry references a conventional plain script
(`src/scripts/<name>.ts`) that exports a `default` but no named `main`. Both
the bin envelope and the artifact-script envelope wrap a `main(argv)` export
and bundle a self-executing module (no `main`, no `default`) byte for byte,
so those shapes run identically on both surfaces. Only the bin envelope falls
back to invoking a default export: the artifact `scripts/<name>.mjs` would
merely define the function and exit, so a successful build would publish an
inert script beside a working bin. The detection is the same static export
scan the package build uses. The message names every `bin` entry referencing
the module.

Recover: export a named `main(argv)` so both surfaces run the same entry;
make the module self-executing (drop the default export and run at top
level); or prefix a path segment with `_` (`src/scripts/_name.ts`) to keep
the module out of script discovery and bin-only.

## Prebuilt payloads (`AB4740`–`AB4750`)

The `payload` block and `{ prebuilt: ... }` entries (see
Expand Down
34 changes: 32 additions & 2 deletions docs/entry-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ entries carry `provenance.kind: 'conventional'` in the normalized model.
| `src/mcp/<server-id>.ts` | Stdio entry for the declared MCP server `<server-id>` that names no `entry`, `command`, or `url`. | Declare `entry` explicitly |
| `src/mcp/<server>/{tools,resources,prompts}/*.{ts,tsx}` | Generated MCP server routes; path supplies identity and each executable module supplies static `config`, schemas, and one async default Server Component. | Set `routes.servers.<server>` to `custom`, `command`, or `remote` |
| `src/mcp/<server>/apps/*.{ts,tsx}` | Browser MCP App entry compiled to self-contained HTML and registered on the generated server; static `config.resourceUri` is required. An optional `config.template` HTML shell resolves relative to the route module like its imports (`'./dashboard.html'`); the legacy project-root-relative form is accepted only while unambiguous (`AB4827` otherwise). Tools, resources, and prompts reference the App from their own static `config` with `appResourceUri('<app>')` from `agent-bundle/routes` or a shared `const` string literal instead of repeating the `ui://` literal. | Use a custom server or prefix the file with `_` |
| `src/scripts/<name>.ts` | Plain script compiled to `scripts/<name>.mjs` in every selected target artifact — the same pipeline explicit `scripts` entries use, with ordinary Node stdout/stderr semantics. A `scripts` entry that references the file claims it. Nested modules are hard errors (`AB4808`). | Prefix a path segment with `_`, or claim the file with an explicit `scripts` entry |
| `src/scripts/<name>.tsx` | Rendered script: the async default component receives `{ argv, signal }` and renders through the Agent renderer with the CLI output contract (`--json`, `--ndjson`, TTY progress, piped Markdown). Compiles to `scripts/<name>.mjs` plus a `scripts/<name>-flight.mjs` react-server worker. The extension is the explicit, visible contract — plain `.ts` scripts are never wrapped in React behavior, and explicit `scripts` config entries stay plain regardless of extension. | Rename to `.ts`, prefix a path segment with `_`, or claim the file with an explicit `scripts` entry |
| `src/scripts/<name>.ts` | Plain script compiled to `scripts/<name>.mjs` in every selected target artifact — the same pipeline explicit `scripts` entries use, with ordinary Node stdout/stderr semantics. A `scripts` entry that references the file claims it. Nested modules are hard errors (`AB4808`). A `bin` entry that references the file does **not** claim it: the module ships as both the npm bin and the artifact script (see [Which config keys claim a conventional module](#which-config-keys-claim-a-conventional-module)); export `main` or make the module self-executing, because a `default`-only module would run as the bin but ship as an inert script (`AB4738`). | Prefix a path segment with `_`, or claim the file with an explicit `scripts` entry |
| `src/scripts/<name>.tsx` | Rendered script: the async default component receives `{ argv, signal }` and renders through the Agent renderer with the CLI output contract (`--json`, `--ndjson`, TTY progress, piped Markdown). Compiles to `scripts/<name>.mjs` plus a `scripts/<name>-flight.mjs` react-server worker. The extension is the explicit, visible contract — plain `.ts` scripts are never wrapped in React behavior, and explicit `scripts` config entries stay plain regardless of extension. A `bin` entry that references a rendered script is `AB4737` unless the module exports both the default component (for the script) and a named `main` (for the bin envelope); with both, the module serves both surfaces. | Rename to `.ts`, prefix a path segment with `_`, or claim the file with an explicit `scripts` entry |
| `src/cli/**/*.{ts,tsx}` | Routed CLI commands compiled into one collision-checked command graph and one generated package executable named after `plugin.name` (superseding the `src/cli.ts` bin convention for the project). Nesting is identity: `src/cli/library/audit.ts` runs as `<bin> library audit`. Plain `.ts` commands execute directly and print one canonical JSON line; `.tsx` commands render through the dispatcher with the four output modes. | `bin: false`, `routes.cli: 'conventional'`, or prefix a path segment with `_` |
| `src/events/<family>/<event>.{ts,tsx}`, `src/events/stop.{ts,tsx}` | Semantic event route: the path is the canonical event family (`src/events/tool/after.tsx` is `tool/after`; `stop` is the one top-level family) and must be one of the admitted `canonicalAgentEvents`. The optional static `config` (`AgentEventRouteConfig`: `targets`, `tools`, `runtime: 'shared' \| 'standalone'`, `fallback`, `delivery`, `timeoutMs`) restricts hosts and selects the execution mode; the async default Server Component receives `AgentEventRouteProps` (`{ canonical, native, signal }`) and returns `Agent.*` output that the selected host adapter encodes into its native hook envelope. Application code never branches on host JSON or emits native hook documents; per-host support is a capability state (`supported`/`degraded`/`unavailable`/`prohibited`) surfaced by `inspect` and enforced at build time (`AB4817`, `AB4823`–`AB4825`). | Restrict `config.targets`, or prefix a path segment with `_` |
| `src/state.ts` | Project state definition: default-exports `defineState({ ... })`; generated MCP, routed-CLI, and rendered-script request scopes mount `(await agent()).state` and `.notices`. | `state: false`, or rename the file to `_state.ts` |
Expand All @@ -87,6 +87,36 @@ entries carry `provenance.kind: 'conventional'` in the normalized model.
Route and package entry conventions match `.ts` and `.tsx` files exactly;
the state convention is specifically `src/state.ts`.

### Which config keys claim a conventional module

An explicit config entry that references a module under a conventional route
directory *claims* it: the module belongs to that declaration and leaves
conventional discovery. Claims are decided by the module path the entry
resolves to, so nothing is ever compiled twice into one artifact output. The
exception is `bin`: a bin compiles to `dist/bin/<name>.js`, which is disjoint
from every artifact output, and the bin envelope and the artifact-script
envelope run the same `main`, so a direct `src/scripts/<name>` child a `bin`
entry references stays a conventional script and ships on both surfaces. (A
nested `src/scripts/<dir>/<name>` module or one whose stem is not a safe route
identity — which the flat scripts artifact could not ship anyway — stays
claimed, so a bin-only entry there never turns into `AB4808` or `AB4803`.)
`inspect` shows such a module under both `packageBuild.bins` and `scripts`; no
diagnostic fires, because that is the intended "same entry, npm bin + hook
target" shape. `lib` is not an executable surface, so a `lib.entry` claims
its module like every other key.

| Config key | Claims a module under | Effect on a `src/scripts/<name>.ts` module it references |
| --- | --- | --- |
| `scripts.<name>` | every route directory | Claimed: the explicit entry ships it as `scripts/<name>.mjs`; the convention no longer applies. A *different* module under `src/scripts/` sharing the configured `<name>` is `AB4809`. |
| `hooks.<event>[].handler` | every route directory | Claimed: the module is a hook handler compiled under `hooks/`, not an artifact script. |
| `mcp.servers.<id>.entry`, `mcp.servers.<id>.apps.<app>.entry` / `.template` | every route directory | Claimed: the module is the server or App entry compiled under `mcp/` or `mcp-apps/`. |
| `lib.entry` | every route directory | Claimed: the module is the library entry compiled to `dist/<stem>.js`, not an artifact script. |
| `bin.<name>` | `src/cli/**`, `src/events/**`, `src/mcp/**`, `src/providers/*`, and any `src/scripts/**` module that is nested or unsafely named — **not** a safely named direct `src/scripts/<name>` child | Not claimed: the module ships as both `dist/bin/<name>.js` and `scripts/<name>.mjs`. The module must export `main` or be self-executing: a plain `default`-only module is `AB4738`, and a rendered `src/scripts/<name>.tsx` must export both the default component and `main`, otherwise `AB4737`. |

To ship a `src/scripts/` module as a bin only, prefix a path segment with `_`
(`src/scripts/_hauler.ts`): private segments opt the module out of discovery
while the `bin` entry still references it explicitly.

### Config beside a route-generated MCP server

A `mcp.servers.<server>` block whose `<server>` the route graph compiles in
Expand Down
8 changes: 7 additions & 1 deletion packages/agent-bundle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,13 @@ agent-bundle also owns the npm-facing package build: `bin` entries become self-e
`dist/bin/<name>.js` bundles (shebang, executable bit, generated `main(argv)` envelope) and the
optional `lib` entry becomes `dist/<stem>.js` with declarations (resolving `typescript` from the
project). The `src/cli.ts`, `src/index.ts`, and `src/mcp/<server-id>.ts` conventions fill these in
when the config is silent; config always wins and `bin: false` / `lib: false` opt out. MCP server
when the config is silent; config always wins and `bin: false` / `lib: false` opt out. An explicit
`scripts`, `hooks`, `lib`, or `mcp` entry claims the module it references out of conventional route
discovery, but a `bin` entry does not claim a `src/scripts/<name>.ts` module: the same file
ships as both `dist/bin/<name>.js` and the artifact `scripts/<name>.mjs` (the module must export
`main` or be self-executing: a `default`-only plain script is `AB4738`, and a rendered `.tsx` script
must export both its default component and `main` or it is `AB4737`; prefix a path segment with `_`
for a bin-only module). MCP server
entries that default-export a server factory are wrapped in the framework stdio lifecycle shell
(console-to-stderr guard with raw stdout restored for protocol frames, SIGINT 130 / SIGTERM 143,
stdin-EOF exit 0, bounded shutdown, heartbeat), also available directly from
Expand Down
Loading
Loading