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

RFC #50 Phase 2, framework side. `validate`/`inspect`/`build`/`dev` now
report informational migration nudges (never errors — migrations stay
optional): `AB4730` for a self-connecting stdio MCP entry that a
default-exported factory would upgrade to the framework lifecycle shell, and
`AB4731`/`AB4732`/`AB4733` when `src/cli.ts`, `src/index.ts`, or
`src/mcp/<server-id>.ts` is present but shadowed by explicit configuration.
`agent-bundle inspect --bundler` dumps the synthesized Rslib/Rsbuild
configuration for every generated output — artifact scripts, MCP entries,
hook wrappers, MCP App views, and the `dist/` package build — post-`tools`-
hatch merge with the invariant hook visible, composed by the same functions
the build lowers so the dump cannot drift. `agent-bundle dev` extends the
debounced, serialized rebuild pass to the framework-owned package build:
`dist/` bin/lib outputs rebuild when their provenance-tracked inputs change,
and a package build failure surfaces as one `AB7103` warning without
invalidating the committed artifact epoch. New `docs/diagnostics.md`
reference documents the diagnostic families and the new codes.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ The same config also owns the npm package build — no second bundler config, bi

- `build` — validate the project and write an artifact (plus the `bin`/`lib` package build when declared)
- `validate` — check project source, or a built artifact with `--artifact <dir>`
- `inspect` — show the normalized configuration and per-target plans
- `dev` — serve the local development workbench
- `inspect` — show the normalized configuration and per-target plans; `--bundler` dumps the synthesized bundler configs (post-`tools`-hatch merge)
- `dev` — serve the local development workbench and rebuild the `dist/` package build when its inputs change
- `mcp list` / `mcp invoke` / `mcp run` — list, invoke, or run an artifact's MCP servers locally
- `hooks list` / `hooks simulate` — inspect and simulate generated hooks
- `eval` — run eval suites against a built artifact
Expand Down
85 changes: 85 additions & 0 deletions docs/diagnostics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Diagnostics reference

Every agent-bundle failure or nudge is one structured diagnostic: a stable
`code` (`AB` + four digits), a `severity` (`error`, `warning`, or `info`), a
`message`, and usually a `sourcePath` and a `recovery` hint. Commands exit
nonzero only when an **error** diagnostic is present; warnings and infos never
gate a build, a validation, or a dev rebuild.

## Code families

| Family | Area |
| --- | --- |
| `AB30xx` | Skill Markdown parsing (missing or malformed frontmatter). |
| `AB40xx` | Plugin metadata and Skill source validation. |
| `AB41xx` | Normalized model invariants (unknown targets, duplicate IDs and outputs). |
| `AB42xx` | Hook configuration and native hook sources. |
| `AB43xx` | MCP server and MCP App configuration. |
| `AB44xx` | Script configuration. |
| `AB4500` | Registered config extensions (strict finite JSON). |
| `AB46xx` | Assets and the generated-runtime floor. |
| `AB470x` | Package build `bin` configuration (`AB4706`: artifact output overlaps `dist`). |
| `AB471x` | Package build `lib` configuration. |
| `AB472x` | The `tools.rsbuild` / `tools.rspack` escape hatch. |
| `AB473x` | Migration nudges (informational; see below). |
| `AB5000` | General CLI and adapter failures. |
| `AB7xxx` | Project preparation and development rebuilds. |
| `AB8xxx` | Development server configuration. |
| `AB9xxx` | Eval selection, harnesses, and persisted runs. |

## Migration nudges (`AB4730`–`AB4733`)

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 an **informational** nudge. Nudges are
never errors and never block anything — migrations stay optional, per the
RFC's additive-first principle. The CLI prints them in human `validate`
output and includes them in every `--json` diagnostics array.

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

A local MCP server entry module (explicit `entry:` or the conventional
`src/mcp/<server-id>.ts`) has no default export, so the build bundles it
byte-for-byte instead of wrapping it in the framework stdio lifecycle shell
(console-to-stderr guard, SIGINT/SIGTERM, stdin-EOF exit, bounded shutdown,
heartbeat). The detection is the same static default-export scan the build
uses, so the nudge and the build always agree.

Adopt: default-export a server factory from the entry module. Silence: keep
the self-connecting entry — its behavior is preserved exactly.

### `AB4731` — `src/cli.ts` shadowed by explicit `bin` config

`src/cli.ts` (or `.tsx`) exists, but the explicit `bin` configuration never
references it, so the conventional package bin is silently shadowed.
`bin: false` is a deliberate opt-out and stays silent.

Adopt: remove the explicit `bin` configuration, or point one entry at the
file. Silence: remove the file, or keep the explicit config knowingly.

### `AB4732` — `src/index.ts` shadowed by explicit `lib` config

`src/index.ts` (or `.tsx`) exists, but the explicit `lib` configuration
points elsewhere. `lib: false` is a deliberate opt-out and stays silent.

Adopt: remove the explicit `lib` configuration, or point it at the file.
Silence: remove the file, or keep the explicit config knowingly.

### `AB4733` — `src/mcp/<server-id>.ts` shadowed by explicit server config

The conventional stdio entry file exists for a declared server, but that
server names an explicit `entry`, `command`, or `url` that does not resolve
to it — a confusable state where the file on disk is not what runs.

Adopt: drop the explicit `entry`/`command`/`url` so the convention applies.
Silence: remove the shadowed file.

## Development package build (`AB7103`)

`agent-bundle dev` rebuilds the framework-owned package build (`dist/` bin
and lib outputs) inside the same serialized rebuild pass that publishes
artifact epochs. A package build failure never invalidates the artifact epoch
that already committed; it surfaces as one `AB7103` **warning** on the
succeeded build attempt, and the package build retries on the next
invalidation. See `docs/entry-conventions.md` for the dev-watch contract.
65 changes: 61 additions & 4 deletions docs/entry-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ node-consumable package build under `dist/` — the outputs `package.json`
| `bin: { '<name>': './src/cli.ts' }` | `dist/bin/<name>.js` | Self-executing ESM bundle, `#!/usr/bin/env node` shebang, executable bit. |
| `lib: { entry: './src/index.ts', dts: true }` | `dist/<stem>.js` + `dist/**/*.d.ts` | Single-entry ESM profile, node target, es2022 syntax. |

- The package build runs only for `agent-bundle build` (CLI, or
`build({ packageOutputs: true })` through the API). Programmatic artifact
operations — temporary artifacts, the dev workbench, evals — never write
`dist/`.
- The package build runs for `agent-bundle build` (CLI, or
`build({ packageOutputs: true })` through the API) and inside the
`agent-bundle dev` rebuild loop (see “Dev-watch of the package build”
below). Other programmatic artifact operations — temporary artifacts,
evals — never write `dist/`.
- Outputs are staged and published atomically, and their provenance
(bytes, SHA-256, sorted project-relative source inputs) is reported on the
build result exactly like artifact files.
Expand Down Expand Up @@ -56,6 +57,16 @@ entries carry `provenance.kind: 'conventional'` in the normalized model.

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

### Migration nudges

Source validation reports **informational** nudges (never errors — migrations
stay optional) when a project exhibits a pre-convention pattern: `AB4730` for
a self-connecting stdio entry that a default-exported factory would upgrade
to the framework lifecycle shell, and `AB4731`/`AB4732`/`AB4733` when
`src/cli.ts`, `src/index.ts`, or `src/mcp/<server-id>.ts` exists but explicit
configuration shadows it. `bin: false` / `lib: false` opt-outs stay silent.
See `docs/diagnostics.md` for each trigger and how to adopt or silence it.

## Generated entry shells

The framework provides the entry files consumers used to write by hand
Expand Down Expand Up @@ -133,6 +144,52 @@ self-containment) fails the build with a hard diagnostic instead of silently
overriding the contract. The hatch customizes *how code compiles*, never
*what the artifact promises*.

### `agent-bundle inspect --bundler`

```sh
agent-bundle inspect --bundler [--target <t>] [--json]
```

Dumps the synthesized bundler configuration for every output the build
composes — artifact scripts, MCP entries, hook wrappers, the per-target MCP
Apps Rsbuild config, and the `dist/` package build — exactly as the build
lowers it: the framework profile with the consumer `tools` hatch merged over
it and the invariant hook appended last (functions render as
`[function <name>]`). Entries the framework wraps also carry the generated
wrapper module source (`generatedEntry`). The composition comes from the same
functions the build uses, so the dump cannot drift from what compiles.

Nothing is redacted (this is a local debugging surface), but two build-time
values are replaced with stable tokens so output is deterministic for one
project: the artifact output root (chosen per build) appears as
`<output>/<target>`, and the synthesized declaration tsconfig (a temporary
file generated per package build) appears as `<generated-dts-tsconfig>`. The
package build's output root appears as its published destination, `dist`,
although each real build stages outputs before publishing them atomically.
Resolved post-bundler internals stay Rslib's domain; this surfaces
agent-bundle's own composition, which is where the `tools` hatch lands.

## Dev-watch of the package build

`agent-bundle dev` rebuilds the `dist/` bin and lib outputs inside the same
debounced, serialized rebuild pass that publishes artifact epochs, with a
provenance-based incremental boundary: after a successful package build, the
sorted source inputs of every emitted file (recorded from bundler stats) are
kept, and the next rebuild is skipped unless an invalidated path was one of
those inputs, the configuration file, `package.json`, or `tsconfig.json`
changed, the rebuild identity changed — the normalized `bin`/`lib`
declaration plus the `tools` escape hatch, with hatch functions compared by
source text — the invalidation was manual or initial, or the previous
package build failed. When every package entry disappears within a live
session (entries removed or opted out), the outputs that session previously
published are removed; outputs from earlier sessions are untouched, matching
`agent-bundle build`. A package build failure never invalidates the
committed artifact epoch — it surfaces as one `AB7103` warning on the
succeeded attempt and retries on the next invalidation. The boundary this
does **not** cover: a brand-new file that changes module resolution without
touching a tracked input is picked up on the next tracked change, not
instantly.

## `agent-bundle mcp run`

```sh
Expand Down
3 changes: 2 additions & 1 deletion packages/agent-bundle/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,12 @@ the repository's `docs/entry-conventions.md` for the full contract.
| `agent-bundle build` | Build a validated artifact from source, plus the declared `dist/` package build. |
| `agent-bundle validate` | Validate project source, or an artifact with `--artifact`. |
| `agent-bundle inspect` | Inspect normalized targets and adapter plans from source. |
| `agent-bundle inspect --bundler` | Dump the synthesized Rslib/Rsbuild configs (post-`tools`-hatch merge) for every generated output. |
| `agent-bundle mcp list` / `mcp invoke` | List or invoke one MCP tool from an artifact. |
| `agent-bundle mcp run` | Run one built stdio MCP server in the foreground, resolving its hashed entry. |
| `agent-bundle hooks list` / `hooks simulate` | List generated hooks, or run one emitted wrapper. |
| `agent-bundle eval` | Run deterministic or native Claude/Codex eval suites and record a run. |
| `agent-bundle dev` | Serve the packaged developer workbench on loopback. |
| `agent-bundle dev` | Serve the packaged developer workbench on loopback; rebuilds the `dist/` package build when its inputs change. |

`validate --artifact`, `mcp`, and `hooks` work against a built artifact with project sources deleted.

Expand Down
25 changes: 24 additions & 1 deletion packages/agent-bundle/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export {
parseArtifactManifest,
serializeArtifactManifest,
} from './build/manifest.ts';
import { composeBundlerInspection, type BundlerInspection } from './build/inspect-bundler.ts';
export type { BundlerInspection, BundlerInspectionEntry } from './build/inspect-bundler.ts';
import { validateArtifact } from './build/validate-artifact.ts';
import { freezeDiagnostics, hasErrors, DiagnosticError, type Diagnostic } from './core/diagnostics.ts';
export type { Diagnostic, DiagnosticSeverity } from './core/diagnostics.ts';
Expand Down Expand Up @@ -197,7 +199,7 @@ export interface InspectionPlan {
}

export interface InspectOptions extends ProjectOptions {
readonly focus?: 'hooks' | 'skills';
readonly focus?: 'bundler' | 'hooks' | 'skills';
readonly target?: string;
}

Expand All @@ -207,6 +209,7 @@ export interface ReadyInspectResult {
readonly plans: readonly InspectionPlan[];
readonly projectContext: ProjectContext;
readonly selected?: {
readonly bundler?: BundlerInspection;
readonly hooks?: NormalizedPlugin['hooks'];
readonly skills?: NormalizedPlugin['skills'];
};
Expand Down Expand Up @@ -434,9 +437,29 @@ export const inspect = async (options: InspectOptions): Promise<InspectResult> =
),
]));
}
let bundler: BundlerInspection | undefined;
if (options.focus === 'bundler') {
try {
bundler = await composeBundlerInspection({
model,
targets: plans.map((plan) => ({ hookEntries: plan.hookEntries, name: plan.target })),
...(prepared.tools === undefined ? {} : { tools: prepared.tools }),
});
} catch {
return invalidInspection(freezeDiagnostics([
...prepared.diagnostics,
projectDiagnostic(
'AB7001',
'Unable to compose the bundler inspection.',
{ sourcePath: prepared.configPath },
),
]));
}
}
const selected = options.focus === undefined
? undefined
: Object.freeze({
...(bundler === undefined ? {} : { bundler }),
...(options.focus === 'hooks' ? { hooks: model.hooks } : {}),
...(options.focus === 'skills' ? { skills: model.skills } : {}),
});
Expand Down
Loading
Loading