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/honest-packed-entry-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'agent-bundle': patch
---

Preserve provider and exit-code semantics across generated CLI workers, make bare prepack output non-overlapping, and harden packed installer and README validation.
20 changes: 14 additions & 6 deletions docs/entry-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,21 @@ node-consumable package build under `dist/` — the outputs `package.json`
- When package outputs and at least one Claude, Codex, or Cursor host pack are
built inside the project, the framework also emits one self-contained
package-relative installer. It is `dist/bin/<plugin-name>.js` when that name
is free, otherwise `dist/bin/<plugin-name>-install.js`. Declare the matching
`package.json` `bin` value. Its grammar is
is free, otherwise `dist/bin/<plugin-name>-install.js`; if both are occupied,
a numeric suffix (`-install-2`, `-install-3`, …) guarantees a free name.
Declare the matching `package.json` `bin` value. Its grammar is
`install <host> [--scope <scope>] [--json]`; help lists only built hosts.
The baked URL resolves the shipped artifact directory from `import.meta.url`,
never the caller's working directory, and delegates to the same
`installBundle` implementation as `agent-bundle install`.
- `agent-bundle prepack [--root <root>] [--output <artifact>] [--json]` runs
the release build and `npm pack --dry-run --json --ignore-scripts`, then
gates the exact package/artifact inventory, manifest hashes, package bin
targets, and release-version agreement. Use it as an npm `prepack` script;
`--ignore-scripts` prevents recursion and npm install never runs the host
installer.
targets, and release-version agreement. With no `--output`, prepack uses
configured `output.distPath` when present and otherwise writes artifacts to
`artifact/`, leaving the package build in `dist/`. Use it as an npm `prepack`
script; `--ignore-scripts` prevents recursion and npm install never runs the
host installer.
- 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”
Expand Down Expand Up @@ -78,7 +81,7 @@ entries carry `provenance.kind: 'conventional'` in the normalized model.
| `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/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/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` |
| `src/providers/<name>.{ts,tsx}` | Request context provider: default-exports a factory receiving `{ invocation, signal }`; its value is mounted at `(await agent()).providers.<camelCaseName>` for generated MCP and event routes. | Prefix the file with `_` |
| `src/providers/<name>.{ts,tsx}` | Request context provider: default-exports a factory receiving `{ invocation, signal }`; its value is mounted at `(await agent()).providers.<camelCaseName>` for generated MCP and event routes, projected MCP commands, rendered routed CLI commands, and rendered scripts. | Prefix the file with `_` |

Route and package entry conventions match `.ts` and `.tsx` files exactly;
the state convention is specifically `src/state.ts`.
Expand Down Expand Up @@ -335,6 +338,11 @@ present. Help and `agent-bundle inspect --routes` expose the source server,
tool, and confirmation policy, and collisions with custom command paths,
groups, or aliases fail with `AB4813`.

A tool whose validated result carries an integer `exitCode` can declare
`config.exitCode: 'result'`; projection preserves that policy so represented
domain failures exit nonzero. Other projected tools retain the success-status
policy and exit zero only for a successful rendered document.

This is an in-house projection over the compiled route graph, per gate G7; it
does not depend on MCPorter or introduce a second command model. MCPorter can
still be pointed independently at the generated MCP server when a live-server
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const operation = outputOperations(defaultOutputOperations).audit;
export const config = {
annotations: { readOnlyHint: false },
description: 'Validate chapter structure, optional conversion mapping, file/audio hashes, probe facts, and optional full decode.',
exitCode: 'result',
};
export const inputSchema = operation.inputSchema;
export const resultSchema = operation.resultSchema;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const operation = discoveryOperations(defaultDiscoveryOperations).libraryAudit;
export const config = {
annotations: { readOnlyHint: false },
description: 'Audit audiobook library metadata, duplicates, and multipart evidence without deletion advice.',
exitCode: 'result',
};
export const inputSchema = operation.inputSchema;
export const resultSchema = operation.resultSchema;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const operation = evidenceOperations(defaultEvidenceOperations).acousticIdentify
export const config = {
annotations: { openWorldHint: true, readOnlyHint: false },
description: 'Try ranked Audible candidates, retaining skips/errors and stopping at the first acoustic match by default.',
exitCode: 'result',
};
export const inputSchema = operation.inputSchema;
export const resultSchema = operation.resultSchema;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const operation = discoveryOperations(defaultDiscoveryOperations).inventory;
export const config = {
annotations: { readOnlyHint: false },
description: 'Inventory source audio with retained per-file probe evidence.',
exitCode: 'result',
};
export const inputSchema = z.object({
source: z.string().min(1).max(4096).describe('Source audio path to inventory.'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const operation = audibleOperations(defaultAudibleOperations).audibleSearch;
export const config = {
annotations: { openWorldHint: true, readOnlyHint: false },
description: 'Search Audible regions and return ranked identity evidence requiring human review.',
exitCode: 'result',
};
export const inputSchema = operation.inputSchema;
export const resultSchema = operation.resultSchema;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const operation = evidenceOperations(defaultEvidenceOperations).acousticVerify;
export const config = {
annotations: { openWorldHint: true, readOnlyHint: false },
description: 'Compare a bounded Audible sample with local audio through an optional Audiolocate Python capability.',
exitCode: 'result',
};
export const inputSchema = operation.inputSchema;
export const resultSchema = operation.resultSchema;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const operation = evidenceOperations(defaultEvidenceOperations).whisperVerify;
export const config = {
annotations: { readOnlyHint: false },
description: 'Extract and transcribe distributed PCM windows for human language, story, and narrator review.',
exitCode: 'result',
};
export const inputSchema = operation.inputSchema;
export const resultSchema = operation.resultSchema;
Expand Down
12 changes: 12 additions & 0 deletions examples/audiobook-curator/tests/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@ describe('audiobook-curator routed CLI', () => {
expect(projectedCommands).toHaveLength(16);
expect(projectedCommands.every((command) =>
command.path[0] === 'curator' && command.rendered)).toBe(true);
expect(projectedCommands
.filter((command) => command.exitCode === 'result')
.map((command) => command.path.join(' '))
.sort()).toEqual([
'curator audit_audiobook',
'curator audit_library',
'curator identify_audible_sample',
'curator inventory_sources',
'curator search_audible',
'curator verify_audible_sample',
'curator verify_with_whisper',
]);

// inspect [--max-files N] <root>
const inspect = byName.get('inspect')!;
Expand Down
2 changes: 2 additions & 0 deletions packages/agent-bundle/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export {
serializeArtifactManifest,
} from './build/manifest.ts';
import { composeBundlerInspection, type BundlerInspection } from './build/inspect-bundler.ts';
import { defaultPackageArtifactDistPath } from './config/normalize.ts';
export type { BundlerInspection, BundlerInspectionEntry } from './build/inspect-bundler.ts';
import { validateArtifact, validateArtifactWithSnapshot } from './build/validate-artifact.ts';
import { freezeDiagnostics, hasErrors, DiagnosticError, type Diagnostic } from './core/diagnostics.ts';
Expand Down Expand Up @@ -701,6 +702,7 @@ export const build = async (options: BuildOptions): Promise<BuildProjectResult>
: [resolveOutput(root, options.output)];
const prepared = await new ProjectService({
...options,
...(options.packageOutputs === true ? { artifactDistPathDefault: defaultPackageArtifactDistPath } : {}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Correct the CLI help for the new artifact default

When build or prepack is run without --output and the config has no output.distPath, this branch now selects artifact/, but both option descriptions still advertise default dist in packages/agent-bundle/src/cli.ts:479 and :489. Users following --help will consequently look for or package the wrong directory, so the CLI descriptions should reflect the operation-specific default.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #397 (merged as d25a9c6). agent-bundle build --help and prepack --help now state the artifact default for --output that both commands actually use (docs/framework-mode.md clarifies the output.distPath interplay); tests/cli.test.ts asserts the help text.

...(outputRoots === undefined ? {} : { outputRoots }),
root,
}).prepare('build');
Expand Down
1 change: 1 addition & 0 deletions packages/agent-bundle/src/build/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ export const build = async (options: BuildOptions): Promise<BuildResult> => {
cwd: options.projectRoot,
meta,
outDir: target.root,
providers: options.model.providers ?? [],
...(options.model.state === undefined ? {} : { state: options.model.state }),
...tools,
},
Expand Down
8 changes: 7 additions & 1 deletion packages/agent-bundle/src/build/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ export const compileEntries = async (
readonly cwd: string;
readonly meta: AgentBundleMeta;
readonly outDir: string;
readonly providers?: readonly CompiledProvider[];
readonly state?: NormalizedStateDefinition;
readonly tools?: AgentBundleToolsConfig;
},
Expand All @@ -174,6 +175,10 @@ export const compileEntries = async (
entries: await Promise.all(bundled.flatMap((entry) => {
const { name, rendered, source, sourceInputs } = entry;
if (rendered !== undefined) {
const workerSourceInputs = Object.freeze([...new Set([
...sourceInputs,
...(options.providers ?? []).map((provider) => provider.source),
])]);
// A rendered script route (#102 stage 3): the entry projects the
// dispatcher's render-event stream onto the CLI output contract and
// a sibling react-server worker executes the component.
Expand All @@ -198,8 +203,9 @@ export const compileEntries = async (
reactServer: true as const,
rscManifest: true as const,
source,
sourceInputs,
sourceInputs: workerSourceInputs,
virtualSource: generatedRenderedRouteWorkerSource({
...(options.providers === undefined ? {} : { providers: options.providers }),
routes: [{
config: emptyRouteConfig,
id: rendered.routeId,
Expand Down
Loading
Loading